Skip to content

fix(cli): dev watcher 重建后不再无声——重建成功后自动重启 serve 子进程(默认开,--no-restart 可退) (#5148) - #6150

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-5148-dev-watcher-honesty
Aug 7, 2026
Merged

fix(cli): dev watcher 重建后不再无声——重建成功后自动重启 serve 子进程(默认开,--no-restart 可退) (#5148)#6150
hotlong merged 1 commit into
mainfrom
claude/issue-5148-dev-watcher-honesty

Conversation

@hotlong

@hotlonghotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes#5148

选档:第 2 档(重建成功后自动重启 serve 子进程,nodemon 式),默认开启

三档验收(分诊 2026-08-06 03:57Z)中选 第 2 档,理由:

  • 第 3 档(仅诚实提示)修不掉根因成本:提示后仍要人手重启,edit/verify 循环每轮多一次手工动作;且「看到提示才知道要重启」对 AI agent 循环是持续税。
  • 第 1 档(热换元数据)是 ADR-0008 的目标态,但当前运行时的 reload 路径只做了一部分:MetadataPlugin 的 artifact watcher 会重载 registry、为新对象补 DDL + seeds(15.1)、广播 SSE HMR;而编译产物里的 hook 体既有对象的视图注册面只在 boot 装载(issue A/B 两节实测钉死)。把这条路补全是运行时级工程,超出本单 CLI 缝的范围。
  • 第 2 档以确定性换实现量:boot-time 全量装载是当前唯一保证「运行中 = 盘上」的路径,重启即收敛,且实现完全落在 dev.ts 既有的 fork 缝上。

实测重启一轮(小 app、sqlite)约 3–5s,由 kernel 优雅停机 + 全量 boot 构成;对 dev 循环可接受,且换来的是结论可信。

状态坑的处理(派发单点名的三项)

风险处理
端口占用竞态严格 SIGTERM → 等 exit → 再 spawn 串行;旧进程退出后才起新进程,不并发抢端口(Node server 带 SO_REUSEADDR,TIME_WAIT 不阻塞重绑)
sqlite 文件锁同一串行序保证旧连接先关;dev.db / --fresh tempdir 环境变量由父进程持有,重启间保持同一数据库,seed-admin 幂等(仅空库)
处理中的请求随重启丢弃 —— nodemon 式 dev 的固有语义,较「静默陈旧」是严格改善;PR 不掩饰这一点
连续保存既有 250ms 编译防抖 + coordinator 在 stopping 态吸收后续重启请求(即将到来的 boot 读的就是最新 artifact),合并为一次重启
卡死子进程SIGTERM 8s 未退出 → SIGKILL 升级,响亮打印
重启失败spawn 失败或重启后的子进程自行退出 → 响亮打印并以子进程码退出 dev(保持 #5148 前「父随子退」契约)
信号与孤儿父进程 SIGINT/SIGTERM 转发给子进程(覆盖非 TTY 场景);process.on('exit') 兜底 SIGTERM,任何父进程退出路径都不留孤儿 server

退出语义(与改动前对齐)

不是 coordinator 发起的子进程退出(崩溃、外部 kill)仍然让父进程以 code ?? 0 跟随退出 —— 与改动前 serveChild.on('exit', code => process.exit(code ?? 0)) 完全一致;重启后的子进程若 boot 即崩,响亮打印后同样退出(未引入 nodemon 的 crashed-waiting 态,保持语义变更最小)。

逃生口 --no-restart

保留旧「只重建不重启」行为,但消灭其静默:每次重建都打印「⚠ auto-restart is off … the running server keeps the build it booted with」,watch banner 也按当前模式如实措辞(旧 banner 暗示热更新,✓ recompiled — server will auto-reload 一并删除)。

启动期变体(作者 08-05 21:34Z 补充,分诊纳入验收)

boot 时(未传 --compile、artifact 已存在)比对 objectstack.config.ts + src/**(剔除 watcher 同一套 ignore:node_modules / .git / .objectstack / dist / .test.)与 artifact 的 mtime;任一源更新则响亮警告并点名修法,不 gate 启动:

⚠ dist/objectstack.json is OLDER than your sources — this boot serves the STALE build.
newest source: src/hello.view.ts (25s newer than the artifact)
fix: run `objectstack build` or start with `--compile`, or save a watched file to trigger a rebuild + restart.

staleness 走查与 chokidar watcher 共享同一份 ignore 列表(DEV_WATCH_IGNORED),两者判定同一源面,不会出现「能触发重建的文件不参与陈旧判定」的缝。

可测缝

新模块 packages/cli/src/utils/dev-restart.ts(沿用 resolveDefaultDevDbUrl 的抽缝先例):

  • assessArtifactStaleness — 启动期陈旧判定(真实 tmpdir + utimes 控制 mtime 测试);
  • ServeRestartCoordinator — 重启决策状态机(idle → running ⇄ stopping → shutdown),以 FakeChild 注入测试:重启序、吸收合并、自行退出跟随、shutdown 优先于 respawn、SIGKILL 升级、spawn 失败响亮、孤儿兜底、旧子进程迟到 exit 事件忽略;
  • formatMtimeGap / isDevWatchIgnored — 纯函数测试。

22 个用例,dev-restart.test.ts

手测记录(2026-08-07,worktree 构建的 CLI,scratch app:1 object + 1 view + 1 beforeInsert hook,sqlite/--fresh)

按 issue A/B 两节方向复现,方向在动手前预判(A:hook 新体无需手动重启即生效;B:视图新 label 无需手动重启即出现;改前两者恒 stale——issue 实测钉死)。

1)B 节(视图元数据),默认档(自动重启):

  • 基线:GET /api/v1/meta/views?object=probe_item"label":"Probe List v1";

  • 不停 server,改 src/hello.view.ts 的 label → v2,日志:

    ↻ recompiling (src/hello.view.ts)...
    ✓ recompiled in 2648ms
    ↻ restarting server to apply the new build (src/hello.view.ts)...
    [HonoServerPlugin] Server stopped
    ...
    ✓ server restarted — the new build is live
    
  • 复读同一端点 → label now: Probe List v2 ✅(无任何手动动作)。

2)A 节(hook 体):

  • 基线:POST /api/v1/data/probe_item → 记录 stamp: hook-v1(beforeInsert hook 写入);
  • 不停 server,改 hook 体 hook-v1hook-v2,等重启完成,再 POST → new record stamp: hook-v2 ✅。

3)连续保存合并: 同时改 view + hook 两个文件 → 日志仅一轮 ↻ recompiling (2 files)... → ✓ recompiled → ↻ restarting → ✓ server restarted,复读两面均为 v3 ✅。

4)信号与孤儿: 对 dev 父进程 PID 执行 kill -INT(非 TTY,无进程组代发)→ 父子进程均退出、端口释放、--fresh tempdir 清除 ✅。四轮手测结束后 lsof 复查 47231-47234 全部空闲,无遗留进程/tempdir。

5)--no-restart 档: banner 变为 👁 watching … — rebuild on change (auto-restart off — running server keeps its build);改 label v3→v4 后:

✓ recompiled in 2331ms — artifact updated on disk
⚠ auto-restart is off (--no-restart): the running server keeps the build it booted with — restart dev to apply

实测 server 仍答 Probe List v3 而盘上 artifact 已是 v4——旧行为保留但不再无声 ✅(顺带:子进程内 MetadataPlugin 的 reload 在本轮同样未令 /meta/views 收敛,与 issue B 节在 origin/main 上的结论一致,前提复核成立)。

6)启动期变体: 无 server 运行时 touch src/hello.hook.ts,不带 --compile 起 dev:

⚠ dist/objectstack.json is OLDER than your sources — this boot serves the STALE build.
newest source: src/hello.hook.ts (56s newer than the artifact)
fix: run `objectstack build` or start with `--compile`, or save a watched file to trigger a rebuild + restart.

boot 未被 gate(照常起服)✅。反向: artifact 比源新时(watcher 刚重建过)同一检查静默 ✅——第一次跑该测试时另一实例的 watcher 恰好在 touch 后 4s 重建了 artifact,staleness 检查如实判「不陈旧」不打印,恰好把反方向也验了。

不做/顺带说明


Generated by Claude Code

…ilent artifact/server divergence (#5148)
The dev watcher rebuilt dist/objectstack.json and printed "server will
auto-reload", but the running serve child only partially receives a rebuilt
artifact: MetadataPlugin's artifact watcher re-ingests the registry, syncs
DDL + seeds for NEW objects and broadcasts the SSE HMR event, while hook
bodies and already-registered view metadata from the compiled bundle are
applied at boot only. #5148 measured both staying stale — every dev
edit/verify loop could produce a false conclusion in either direction.
Boot-time load is the one path that applies the whole artifact, so dev now
supervises its serve child nodemon-style via ServeRestartCoordinator
(packages/cli/src/utils/dev-restart.ts):
- after each successful rebuild: SIGTERM the child (kernel shuts down
gracefully), spawn a replacement on exit; requests during teardown
coalesce; SIGKILL escalation after 8s; a failed respawn is loud and
exits dev; parent SIGINT/SIGTERM is forwarded and a process-exit hook
guarantees no orphaned server.
- --no-restart keeps the rebuild-only behavior but says on EVERY rebuild
that the running server keeps the build it booted with; the watch
banner states the active mode instead of implying hot reload.
- startup variant (#5148 comment 2026-08-05): when objectstack.config.ts
/ src/** are newer than the artifact at boot, warn loudly (newest
source + remedy) without gating the boot. The staleness walk shares
the watcher's ignore list so both judge the same source surface.
Seams unit-tested (22 cases): staleness assessment on real tmpdirs,
coordinator state machine with a fake child (restart sequencing,
coalescing, self-exit follow, shutdown-beats-respawn, force-kill,
spawn failure, orphan guard, late exit events).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015a5qkLzpGXhLL2F5gvJ7dD
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 7, 2026 3:41am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx(via packages/cli)
  • content/docs/api/client-sdk.mdx(via @objectstack/cli)
  • content/docs/api/data-flow.mdx(via @objectstack/cli)
  • content/docs/api/environment-routing.mdx(via @objectstack/cli)
  • content/docs/api/error-catalog.mdx(via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx(via packages/cli)
  • content/docs/deployment/backup-restore.mdx(via @objectstack/cli)
  • content/docs/deployment/cli.mdx(via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx(via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx(via packages/cli)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/cli)
  • content/docs/kernel/runtime-services/index.mdx(via packages/cli)
  • content/docs/permissions/authentication.mdx(via @objectstack/cli)
  • content/docs/plugins/index.mdx(via @objectstack/cli)
  • content/docs/plugins/packages.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx(via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx(via @objectstack/cli)
  • content/docs/releases/v16.mdx(via @objectstack/cli)
  • content/docs/releases/v17.mdx(via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectstack dev watcher rebuilds dist/objectstack.json but the running server keeps serving the metadata it booted with

2 participants

@hotlong@claude