create-walle 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -125,6 +125,12 @@ const { decodeFramedJson: _decodeFramedJson } = require('./framed-json-codec');
125
125
  // by omission.
126
126
  const ISOLATE_INTERACTIVE = process.env.CTM_READPOOL_ISOLATE_INTERACTIVE !== '0';
127
127
  const REQUEST_CLASSES = new Set(['interactive', 'service', 'batch']);
128
+
129
+ // WP-0 M-1/M-4 (docs/hub-local-cpu-memory-burn-2026-10-31.html §7): per-op CPU and heap
130
+ // attribution, aggregated pool-side for perf-summary. See _attachOpPerfTap/_recordOpPerf.
131
+ const PERF_HEAP_TOP_LIMIT = 20; // fixed ring: the 20 largest |heap+external| deltas seen
132
+ const _PERF_TAP = Symbol('ctmReadPoolPerfTap');
133
+ const _perfAttributionOn = () => boolFlag('CTM_PERF_ATTRIBUTION', true);
128
134
  // Counts are a WAITING backstop (WP-3 D-5): a class refuses with QUEUE_FULL only when its waiter list is full.
129
135
  const DEFAULT_QUEUE_CAPACITIES = Object.freeze({ interactive: 256, service: 256, batch: 128 });
130
136
  // D-7a/D-8 (idle-wake freeze design §5, §8 WP-1b): the service-class coalesce line is a
@@ -306,6 +312,12 @@ class CtmReadPoolClient {
306
312
  // ever gated by it; the map itself is maintained for every op so getStatus().perOp can
307
313
  // report a non-zero count for an uncapped op too.
308
314
  this._inFlightByOp = new Map();
315
+ // WP-0 M-1/M-4: per-op CPU totals (µs, converted to ms at read time so repeated reads
316
+ // cannot drift) and the bounded top-20 heap-delta ring. Both bounded: _cpuByOp is keyed by
317
+ // op name (the registry declares a fixed set, plus 'unknown'), _heapTopDeltas by
318
+ // PERF_HEAP_TOP_LIMIT. Unused (and empty) when CTM_PERF_ATTRIBUTION=0.
319
+ this._cpuByOp = new Map();
320
+ this._heapTopDeltas = [];
309
321
  // D-7a (WP-1b): one coalesce line per service singleflight key per COALESCE_LOG_INTERVAL_MS.
310
322
  this._lastCoalesceLogAt = new Map();
311
323
  this._closed = false;
@@ -334,8 +346,8 @@ class CtmReadPoolClient {
334
346
  historyReaderContext: this.#historyReaderContext,
335
347
  onMemoryLimitDeath: (occupiedEntries) => this._recordMemoryLimitDeath(occupiedEntries),
336
348
  onRefusal: (refusal) => this._recordRefusal(refusal),
337
- onRespawn: (worker) => this._recordRespawn(worker),
338
- onReady: () => this._drainClassWaiters(),
349
+ onRespawn: (worker) => { this._attachOpPerfTap(worker); this._recordRespawn(worker); },
350
+ onReady: (worker) => { this._attachOpPerfTap(worker); this._drainClassWaiters(); },
339
351
  onTiming: (kind, requestClass, ms) => this._recordTiming(kind, requestClass, ms),
340
352
  onGhostRetire: (worker, ghost, ranMs, spanMs) => this._recordGhostRetire(worker, ghost, ranMs, spanMs),
341
353
  onGhostTerminate: (worker, ghost, ranMs, spanMs) => this._recordGhostTerminate(worker, ghost, ranMs, spanMs),
@@ -1190,6 +1202,76 @@ class CtmReadPoolClient {
1190
1202
  return perOp;
1191
1203
  }
1192
1204
 
1205
+ // WP-0 M-1/M-4 (hub-local-cpu-memory-burn-2026-10-31.html §7): read-only observation of the
1206
+ // response messages the worker handle already consumes. The worker stamps each response with
1207
+ // `perf = { op, cpuUserUs, cpuSystemUs, heapDeltaBytes, externalDeltaBytes }`; this listener
1208
+ // keeps the per-op CPU totals and the top-20 heap deltas. It runs on the SAME event as the
1209
+ // handle's own response accounting (so counts track responseByOp, modulo a response that
1210
+ // arrives after its caller gave up -- the handle has already dropped that requestId, and this
1211
+ // listener counts it because the op really did run), touches nothing the handle owns, and
1212
+ // adds one symbol property per native worker. Attached from the ready/respawn hooks, since the
1213
+ // handle creates (and replaces, on respawn) the native worker internally.
1214
+ _attachOpPerfTap(handle) {
1215
+ if (!_perfAttributionOn()) return;
1216
+ const native = handle?.worker;
1217
+ if (!native || typeof native.on !== 'function' || native[_PERF_TAP] === true) return;
1218
+ try {
1219
+ native[_PERF_TAP] = true;
1220
+ native.on('message', (msg) => this._recordOpPerf(msg));
1221
+ } catch { /* expected: a worker torn down mid-attach has no responses left to attribute; the respawn re-attaches */ }
1222
+ }
1223
+
1224
+ _recordOpPerf(msg) {
1225
+ if (!_perfAttributionOn()) return;
1226
+ const perf = msg && msg.type === 'response' ? msg.perf : null;
1227
+ if (!perf || typeof perf !== 'object') return;
1228
+ const op = String(perf.op || 'unknown');
1229
+ const totals = this._cpuByOp.get(op) || { count: 0, userUs: 0, systemUs: 0 };
1230
+ totals.count += 1;
1231
+ // Clamp negatives away: a µs delta cannot be negative, and a clock read that somehow is
1232
+ // must not subtract from the op's own total.
1233
+ totals.userUs += Math.max(0, Number(perf.cpuUserUs) || 0);
1234
+ totals.systemUs += Math.max(0, Number(perf.cpuSystemUs) || 0);
1235
+ this._cpuByOp.set(op, totals);
1236
+ const heapDeltaBytes = Number(perf.heapDeltaBytes) || 0;
1237
+ const externalDeltaBytes = Number(perf.externalDeltaBytes) || 0;
1238
+ if (heapDeltaBytes === 0 && externalDeltaBytes === 0) return;
1239
+ this._pushHeapDelta({ op, heapDeltaBytes, externalDeltaBytes, at: Date.now() });
1240
+ }
1241
+
1242
+ // Bounded top-20 by heapDeltaBytes+externalDeltaBytes descending; while full, an entry that
1243
+ // does not beat the smallest is dropped (no allocation, no sort). Keeps the sign of both
1244
+ // deltas -- a GC-heavy or buffer-releasing op is a negative row, which is information too.
1245
+ _pushHeapDelta(entry) {
1246
+ const weight = (row) => row.heapDeltaBytes + row.externalDeltaBytes;
1247
+ if (this._heapTopDeltas.length < PERF_HEAP_TOP_LIMIT) {
1248
+ this._heapTopDeltas.push(entry);
1249
+ this._heapTopDeltas.sort((a, b) => weight(b) - weight(a));
1250
+ return;
1251
+ }
1252
+ if (weight(entry) <= weight(this._heapTopDeltas[this._heapTopDeltas.length - 1])) return;
1253
+ this._heapTopDeltas[this._heapTopDeltas.length - 1] = entry;
1254
+ this._heapTopDeltas.sort((a, b) => weight(b) - weight(a));
1255
+ }
1256
+
1257
+ _cpuByOpStatus() {
1258
+ if (!_perfAttributionOn()) return {};
1259
+ const out = {};
1260
+ for (const [op, totals] of this._cpuByOp) {
1261
+ out[op] = {
1262
+ count: totals.count,
1263
+ userMs: Math.round((totals.userUs / 1000) * 10) / 10,
1264
+ systemMs: Math.round((totals.systemUs / 1000) * 10) / 10,
1265
+ };
1266
+ }
1267
+ return out;
1268
+ }
1269
+
1270
+ _heapTopDeltasStatus() {
1271
+ if (!_perfAttributionOn()) return [];
1272
+ return this._heapTopDeltas.map((row) => ({ ...row }));
1273
+ }
1274
+
1193
1275
  getStatus() {
1194
1276
  const responseByOp = {};
1195
1277
  const responseByReason = {};
@@ -1317,6 +1399,11 @@ class CtmReadPoolClient {
1317
1399
  peakInputBytes: this._workers.reduce((peak, w) => Math.max(peak, w.peakInputBytes), 0),
1318
1400
  responseByOp,
1319
1401
  responseByReason,
1402
+ // WP-0 M-1/M-4 (§7): per-op CPU (ms, 0.1 precision) beside responseByOp -- same op key,
1403
+ // same response event -- and the bounded top-20 heap deltas. Empty when
1404
+ // CTM_PERF_ATTRIBUTION=0.
1405
+ cpuByOp: this._cpuByOpStatus(),
1406
+ heapTopDeltas: this._heapTopDeltasStatus(),
1320
1407
  };
1321
1408
  }
1322
1409
 
@@ -194,13 +194,18 @@ function createPoolWorkerClass({
194
194
  workerData: { historyReaderContext: this.#historyReaderContext },
195
195
  });
196
196
  this.worker = worker;
197
+ // D-2 (docs/temp-file-leaks-disk-full-2026-10-25.html §8): captured at spawn, because a dead
198
+ // worker's own isolate can no longer report it. Threads share this process's pid, so pairing it
199
+ // with `_terminateIssued` below is the only evidence that a stopped thread left decode temps
200
+ // stranded mid-write (the write completed, its rename never ran).
201
+ const workerThreadId = worker.threadId;
197
202
 
198
203
  const failStart = (err) => {
199
204
  if (this.worker !== worker) return;
200
205
  if (this.ready) return;
201
206
  this.startPromise = null;
202
207
  this.worker = null;
203
- try { worker.terminate(); } catch { /* expected: best-effort termination of a worker that failed to start */ }
208
+ try { this._terminateWorker(worker); } catch { /* expected: best-effort termination of a worker that failed to start */ }
204
209
  reject(err instanceof Error ? err : new Error(String(err || 'read-pool worker failed to start')));
205
210
  };
206
211
 
@@ -357,6 +362,17 @@ function createPoolWorkerClass({
357
362
  this._rejectAll(err);
358
363
  });
359
364
  worker.on('exit', (code) => {
365
+ // D-2: a thread stopped mid-op (crash, or our own terminate) can never run its own cleanup,
366
+ // so the temps IT left are reclaimed here by ownership. Deliberately before the `this.worker
367
+ // !== worker` staleness guard: a replaced handle is exactly the case where nothing else would
368
+ // ever look at these files. Never throws -- a hygiene sweep must not disturb exit handling.
369
+ if (code !== 0 || worker._terminateIssued) {
370
+ // Fire-and-forget: the reclaim walks the whole cache, so it must not run synchronously on
371
+ // this (main) loop. The listener stays synchronous; a failure is only ever logged.
372
+ const tid = Number.isInteger(workerThreadId) ? workerThreadId : worker.threadId;
373
+ require('./transcript-source').removeCacheTempsForThreadAsync(process.pid, tid)
374
+ .catch((error) => this.logger?.warn?.(`[read-pool] cache temp cleanup failed: ${error?.message || error}`));
375
+ }
360
376
  if (this.worker !== worker) return;
361
377
  const wasReady = this.ready;
362
378
  this.ready = false;
@@ -377,6 +393,15 @@ function createPoolWorkerClass({
377
393
  return this.startPromise;
378
394
  }
379
395
 
396
+ // D-2: every termination goes through here so the Worker object itself records that this thread was
397
+ // STOPPED (rather than closing itself), which is what makes its stranded decode temps reclaimable
398
+ // in the 'exit' listener. Returns worker.terminate()'s promise for the close() timeout path.
399
+ _terminateWorker(worker) {
400
+ if (!worker) return Promise.resolve();
401
+ worker._terminateIssued = true;
402
+ return worker.terminate();
403
+ }
404
+
380
405
  async request(op, payload, timeoutMs, lifecycle = {}) {
381
406
  let lifecycleDone = false;
382
407
  const completeLifecycle = () => {
@@ -624,7 +649,7 @@ function createPoolWorkerClass({
624
649
  // terminate immediately, dropping whatever this worker was carrying. The lazy
625
650
  // drain above is the default; this only exists as an escape hatch.
626
651
  this.retiring = true;
627
- try { this.worker?.terminate(); } catch { /* expected: worker already exited or never started — the rollback path's only goal is that it is not running */ }
652
+ try { this._terminateWorker(this.worker); } catch { /* expected: worker already exited or never started — the rollback path's only goal is that it is not running */ }
628
653
  return;
629
654
  }
630
655
  this.retiring = true;
@@ -693,7 +718,7 @@ function createPoolWorkerClass({
693
718
  const finish = () => { if (done) return; done = true; resolve(); };
694
719
  let timer = null;
695
720
  if (timeoutMs > 0) {
696
- timer = setTimeout(() => { worker.terminate().finally(finish); }, timeoutMs);
721
+ timer = setTimeout(() => { this._terminateWorker(worker).finally(finish); }, timeoutMs);
697
722
  if (typeof timer.unref === 'function') timer.unref();
698
723
  }
699
724
  const onMessage = (msg) => {
@@ -704,7 +729,7 @@ function createPoolWorkerClass({
704
729
  };
705
730
  worker.on('message', onMessage);
706
731
  try { worker.postMessage({ type: 'close' }); }
707
- catch { /* expected: close post race with an exiting worker; termination completes the lifecycle */ if (timer) clearTimeout(timer); worker.off('message', onMessage); worker.terminate().finally(finish); }
732
+ catch { /* expected: close post race with an exiting worker; termination completes the lifecycle */ if (timer) clearTimeout(timer); worker.off('message', onMessage); this._terminateWorker(worker).finally(finish); }
708
733
  });
709
734
  if (this.worker === worker) {
710
735
  this.ready = false;
@@ -110,6 +110,17 @@ function mapAgent(agent, parentRow) {
110
110
  };
111
111
  }
112
112
 
113
+ /**
114
+ * WP-0 (S-1, fleet-session-lists-hub-search-2026-11-01.html §6.1): the OWNER of a catalog row — the
115
+ * instance that executes it, falling back to its current owner. Unlike `machineFactsForRecentRow`
116
+ * this is unconditional: a box has no registered machines at all, so the tag path yields nothing
117
+ * there, and owner truth is exactly what a forwarded `owner=self` request filters on. Exported so
118
+ * the search projection (lib/api/session-messages.js) reads the same expression.
119
+ */
120
+ function recentRowOwnerInstanceId(row) {
121
+ return String(row?.execution_host_instance_id || row?.current_owner_instance_id || '').trim();
122
+ }
123
+
113
124
  /**
114
125
  * WP-3 (remote-hub-followups D-10): the machine tag for a recent row — the row's EXECUTION host
115
126
  * (falling back to its current owner) when that id names a REGISTERED machine and is not this host
@@ -202,6 +213,9 @@ function mapRecentSessionRow(row, options = {}) {
202
213
  parentAgentSessionId: row.parent_agent_session_id || primary?.parentAgentSessionId || '',
203
214
  agentNickname: row.agent_nickname || primary?.agentNickname || '',
204
215
  agentRole: row.agent_role || primary?.agentRole || '',
216
+ // WP-0 (S-1): owner truth, unconditionally — a box registers no machines, so the machine tag
217
+ // above never fires there and `owner=self` filters on THIS key. Empty string = unknown owner.
218
+ ownerInstanceId: recentRowOwnerInstanceId(row),
205
219
  // WP-3 (D-10): the machine tag, only when this row's execution host is a registered box.
206
220
  ...(machineFactsForRecentRow(row, options) || {}),
207
221
  };
@@ -259,5 +273,6 @@ module.exports = {
259
273
  mapRecentSessionRowsCooperatively,
260
274
  pickDisplayTitle,
261
275
  queryRecentCatalogSessions,
276
+ recentRowOwnerInstanceId,
262
277
  rowAgentSessionIds,
263
278
  };
@@ -92,22 +92,53 @@ function activityMs(session) {
92
92
  // session lives. Machine labeling MUST therefore use the machine's minted instance id
93
93
  // (`machine`/`machineName`, filled by lib/remote-machine-catalog.js) and NEVER a hostname: any
94
94
  // hostname-based label would silently claim a box's session is on this host (D-3).
95
- function buildRecentSessionCatalogResponse(input = {}) {
96
- const shape = normalizeRecentCatalogShape(input.query);
97
- const hostname = String(input.hostname || '');
98
- const pinned = new Set((input.pinnedIds || []).map(String));
99
- const sessions = (Array.isArray(input.sessions) ? input.sessions : [])
100
- .map((session) => ({ ...session, hostname }))
101
- .sort((a, b) => activityMs(b) - activityMs(a));
102
- const selected = sessions.slice(0, shape.limit);
95
+ /**
96
+ * WP-0 (S-2, fleet-session-lists-hub-search-2026-11-01.html §5 D-3, §6.1): the limit + starred rule,
97
+ * extracted pure so the owner filter can run BEFORE the limit.
98
+ *
99
+ * @param {object[]} sessions mapped recent rows (each carries `ownerInstanceId`, S-1)
100
+ * @param {{limit:number, pinnedIds?:string[], ownerLocalIds?:string[]|null}} opts
101
+ * `ownerLocalIds` null/undefined = keep every row (today's behaviour). Otherwise a row is kept
102
+ * iff it names no owner at all (`''` — an unknown/legacy owner) or names one of these ids. This
103
+ * filter runs BEFORE the activityMs sort + slice, so foreign copies can never crowd out a box's
104
+ * own sessions (the 3,775 Spark copies in the design doc's §1.1).
105
+ * @returns {object[]} activityMs desc, sliced to `limit`, then starred/pinned rows past it
106
+ */
107
+ function selectRecentCatalogRows(sessions, { limit, pinnedIds = [], ownerLocalIds = null } = {}) {
108
+ const list = Array.isArray(sessions) ? sessions : [];
109
+ const localIds = Array.isArray(ownerLocalIds) ? ownerLocalIds : null;
110
+ const cap = Math.max(0, Number(limit) || 0);
111
+ const filtered = localIds
112
+ ? list.filter((session) => {
113
+ const owner = String(session?.ownerInstanceId || '').trim();
114
+ return !owner || localIds.includes(owner);
115
+ })
116
+ : list.slice();
117
+ const sorted = filtered.sort((a, b) => activityMs(b) - activityMs(a));
118
+ const selected = sorted.slice(0, cap);
103
119
  const selectedIds = new Set(selected.map((session) => String(session.sessionId || session.id || '')));
104
- for (const session of sessions) {
120
+ for (const session of sorted) {
105
121
  const id = String(session.sessionId || session.id || '');
106
- if (!selectedIds.has(id) && (session.starred || pinned.has(id))) {
122
+ if (!selectedIds.has(id) && (session.starred || pinnedIds.includes(id))) {
107
123
  selected.push(session);
108
124
  selectedIds.add(id);
109
125
  }
110
126
  }
127
+ return selected;
128
+ }
129
+
130
+ function buildRecentSessionCatalogResponse(input = {}) {
131
+ const shape = normalizeRecentCatalogShape(input.query);
132
+ const hostname = String(input.hostname || '');
133
+ const sessions = (Array.isArray(input.sessions) ? input.sessions : [])
134
+ .map((session) => ({ ...session, hostname }));
135
+ // WP-0 (S-2): only `owner=self` filters; every other value (including absent) keeps today's
136
+ // output byte for byte, apart from the `ownerInstanceId` key each mapped row now carries.
137
+ const selected = selectRecentCatalogRows(sessions, {
138
+ limit: shape.limit,
139
+ pinnedIds: (input.pinnedIds || []).map(String),
140
+ ownerLocalIds: input.ownerFilter === 'self' ? input.ownerLocalIds : null,
141
+ });
111
142
  const buffer = Buffer.from(JSON.stringify({
112
143
  sessions: selected,
113
144
  lastActiveProject: input.lastActiveProject || null,
@@ -126,6 +157,19 @@ function buildRecentSessionCatalogVariantBundle(input = {}) {
126
157
  const variants = [];
127
158
  const limits = [...new Set((input.limits || [1, 50, 100, 200, 500])
128
159
  .map((limit) => normalizeRecentCatalogShape({ limit }).limit))];
160
+ // WP-0 (S-3): the owner dimension of the key is emitted ONLY when this host actually holds a
161
+ // row it does not own — then `|self` is a genuinely different answer and the hub can ask for it.
162
+ // With no foreign row the all-variant IS the self answer, so no `|self` key exists (and the
163
+ // lookup falls back), keeping every existing key byte-identical.
164
+ // Fail OPEN: an empty lineage (identity not minted yet) cannot tell owned rows from copies, so no
165
+ // `|self` variant is built and an owner=self request falls back to the all-variant. The hub's
166
+ // merge still drops rows owned by its own lineage (fleet-session-lists D-4).
167
+ const ownerLocalIds = Array.isArray(input.ownerLocalIds) && input.ownerLocalIds.length ? input.ownerLocalIds : null;
168
+ const hasForeignRow = !!ownerLocalIds && (Array.isArray(input.sessions) ? input.sessions : [])
169
+ .some((session) => {
170
+ const owner = String(session?.ownerInstanceId || '').trim();
171
+ return !!owner && !ownerLocalIds.includes(owner);
172
+ });
129
173
  for (const limit of limits) {
130
174
  for (const stale of [false, true]) {
131
175
  const key = `${limit}|${stale ? 1 : 0}`;
@@ -133,6 +177,18 @@ function buildRecentSessionCatalogVariantBundle(input = {}) {
133
177
  ...input,
134
178
  query: { limit },
135
179
  stale,
180
+ ownerFilter: '',
181
+ });
182
+ variants.push({ key: Buffer.from(key, 'utf8'), body: response.buffer });
183
+ }
184
+ if (!hasForeignRow) continue;
185
+ for (const stale of [false, true]) {
186
+ const key = `${limit}|${stale ? 1 : 0}|self`;
187
+ const response = buildRecentSessionCatalogResponse({
188
+ ...input,
189
+ query: { limit },
190
+ stale,
191
+ ownerFilter: 'self',
136
192
  });
137
193
  variants.push({ key: Buffer.from(key, 'utf8'), body: response.buffer });
138
194
  }
@@ -184,5 +240,6 @@ module.exports = {
184
240
  buildRecentSessionCatalogVariantBundle,
185
241
  decodeRecentSessionCatalogVariantBundle,
186
242
  normalizeRecentCatalogShape,
243
+ selectRecentCatalogRows,
187
244
  snapshotRecentCatalogSessions,
188
245
  };
@@ -6,6 +6,14 @@ const crypto = require('node:crypto');
6
6
  const { execFile } = require('node:child_process');
7
7
  const { readPrivateJson } = require('./recovery-private-file');
8
8
 
9
+ // The writer witness is read with the system `lsof`, from fixed absolute paths only: a
10
+ // PATH-resolved binary is caller-controlled and must not be able to answer (or refuse) a
11
+ // piece of recovery evidence. macOS installs lsof under /usr/sbin, Debian-family Linux
12
+ // under /usr/bin, so the probe walks both in that order -- same rule as
13
+ // bin/deflake-source-snapshot.js's `lsofCwdUsers` (ENOENT moves on, any other refusal is
14
+ // unknown), minus its leading bare-`lsof` PATH candidate.
15
+ const LSOF_CANDIDATES = Object.freeze(['/usr/sbin/lsof', '/usr/bin/lsof']);
16
+
9
17
  // Controller ownership is rewritten by normal adoption; provider binding,
10
18
  // workspace, origin, model and all other stable columns remain in the witness.
11
19
  const VOLATILE_RECIPE_FIELDS = new Set(['pid', 'ctm_pid', 'ctm_instance_id', 'current_owner_instance_id', 'updated_at', 'last_heartbeat', 'last_activity', 'last_user_prompt_input_at', 'last_meaningful_activity_at', 'attention_dismissed_at', 'attention_dismissed_input_at']);
@@ -51,17 +59,24 @@ async function verifyWriterWitness(operation, { execFileImpl = execFile } = {})
51
59
  if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== operation.original.uid || String(stat.ino) !== String(witness.lockFile.inode) || String(stat.dev) !== String(witness.lockFile.dev)) return false;
52
60
  // This is continuity of an explicitly selected owner's descriptor, not a
53
61
  // generic claim that an open fd proves advisory-lock ownership.
54
- return new Promise((resolve, reject) => execFileImpl('/usr/sbin/lsof', ['-nP', '-Fpf', '--', lockPath], { timeout: 3000, maxBuffer: 64 * 1024 }, (error, stdout) => {
55
- // Evidence is tri-state: exit 1 with no holder is a real change, but any
56
- // other sampler failure (timeout, signal, spawn error) is `unknown`, which
57
- // callers must treat as a retryable hold rather than "the writer changed".
58
- if (error) {
59
- if (error.code === 1 && !String(stdout).trim()) return resolve(false);
60
- return reject(Object.assign(new Error(`writer witness unavailable: lsof code=${error.code ?? ''} signal=${error.signal ?? ''} killed=${!!error.killed}`), { code: 'RECOVERY_EVIDENCE_UNKNOWN' }));
61
- }
62
- const holders = String(stdout).split('\n').filter((line) => /^p\d+$/.test(line)).map((line) => Number(line.slice(1)));
63
- resolve(holders.length === 1 && holders[0] === operation.original.pid);
64
- }));
62
+ return new Promise((resolve, reject) => {
63
+ const probe = (index) => execFileImpl(LSOF_CANDIDATES[index], ['-nP', '-Fpf', '--', lockPath], { timeout: 3000, maxBuffer: 64 * 1024 }, (error, stdout) => {
64
+ // Only a candidate that is not installed at this location (ENOENT) falls through to
65
+ // the next one: a sampler that ran and refused must not be bypassed by a second
66
+ // binary, so every other failure is decided by this candidate alone.
67
+ if (error?.code === 'ENOENT' && index + 1 < LSOF_CANDIDATES.length) return probe(index + 1);
68
+ // Evidence is tri-state: exit 1 with no holder is a real change, but any
69
+ // other sampler failure (timeout, signal, spawn error) is `unknown`, which
70
+ // callers must treat as a retryable hold rather than "the writer changed".
71
+ if (error) {
72
+ if (error.code === 1 && !String(stdout).trim()) return resolve(false);
73
+ return reject(Object.assign(new Error(`writer witness unavailable: lsof code=${error.code ?? ''} signal=${error.signal ?? ''} killed=${!!error.killed}`), { code: 'RECOVERY_EVIDENCE_UNKNOWN' }));
74
+ }
75
+ const holders = String(stdout).split('\n').filter((line) => /^p\d+$/.test(line)).map((line) => Number(line.slice(1)));
76
+ resolve(holders.length === 1 && holders[0] === operation.original.pid);
77
+ });
78
+ probe(0);
79
+ });
65
80
  }
66
81
 
67
82
  async function verifyPreservedEvidence(operation, tasks) {
@@ -22,7 +22,7 @@ const path = require('node:path');
22
22
  const { WebSocket } = require('ws');
23
23
 
24
24
  const { atomicWriteFileSync } = require('../atomic-write');
25
- const { createMux, TYPE, CONTROL_SUBPROTOCOL } = require('../../ctm-relay/lib/protocol.js');
25
+ const { createMux, TYPE, CONTROL_SUBPROTOCOL, OPEN_KINDS } = require('../../ctm-relay/lib/protocol.js');
26
26
 
27
27
  const DEFAULT_LOCAL_TARGET = Object.freeze({ host: '127.0.0.1', port: 3456 });
28
28
  const DEFAULT_PING_INTERVAL_MS = 25_000;
@@ -55,6 +55,35 @@ function hostFor(relayUrl, relayHost) {
55
55
  return new URL(relayUrl).host;
56
56
  }
57
57
 
58
+ // SNI names a host, not an endpoint: `r.walle.sh:8443` becomes `r.walle.sh`, and
59
+ // a bracketed IPv6 literal loses its brackets.
60
+ function hostnameOnly(host) {
61
+ const text = String(host);
62
+ if (text.startsWith('[')) {
63
+ const end = text.indexOf(']');
64
+ return end === -1 ? text : text.slice(1, end);
65
+ }
66
+ const colon = text.lastIndexOf(':');
67
+ return colon === -1 ? text : text.slice(0, colon);
68
+ }
69
+
70
+ // The TLS seam (WP-11b): the rev 2 edge routes on SNI, so a client that reaches
71
+ // the relay by address (`https://127.0.0.1:<port>`) still has to name the host it
72
+ // speaks for, and a test CA has to ride along in `ca`. The seam is `options.tls`,
73
+ // spread into the WebSocket options verbatim.
74
+ //
75
+ // Production passes neither: `relayUrl = https://r.walle.sh` with no explicit
76
+ // relayHost means the requested host IS the URL's host, so nothing is added and
77
+ // `ws` derives everything from the URL exactly as it did before.
78
+ function tlsOptionsFor(relayUrl, relayHost, tls) {
79
+ const options = tls && typeof tls === 'object' ? Object.assign({}, tls) : {};
80
+ if (typeof relayHost !== 'string' || relayHost.length === 0) return options;
81
+ if (options.servername) return options;
82
+ if (relayHost === new URL(relayUrl).host) return options;
83
+ options.servername = hostnameOnly(relayHost);
84
+ return options;
85
+ }
86
+
58
87
  function forwardHeaders(raw, { keepUpgrade = false } = {}) {
59
88
  const out = {};
60
89
  for (const [key, value] of Object.entries(raw || {})) {
@@ -120,6 +149,7 @@ function createRelayClient(options = {}) {
120
149
 
121
150
  const connectUrl = connectUrlFor(relayUrl); // throws on a malformed relayUrl, at construction
122
151
  const relayHost = hostFor(relayUrl, options.relayHost);
152
+ const tlsOptions = tlsOptionsFor(relayUrl, options.relayHost, options.tls);
123
153
  const localTarget = Object.assign({}, DEFAULT_LOCAL_TARGET, options.localTarget || {});
124
154
  const stateFile = options.stateFile || null;
125
155
  const clientKind = options.clientKind || 'node';
@@ -351,6 +381,9 @@ function createRelayClient(options = {}) {
351
381
  if (stopping) return;
352
382
  transition('connecting');
353
383
  const ws = new WebSocket(connectUrl, CONTROL_SUBPROTOCOL, {
384
+ // `ca` / `servername` / anything else the caller supplied for TLS. Empty in
385
+ // production, so the options object is unchanged there.
386
+ ...tlsOptions,
354
387
  headers: {
355
388
  // The control plane is routable on the base domain only; tests dial
356
389
  // 127.0.0.1 while presenting the real Host.
@@ -407,11 +440,32 @@ function createRelayClient(options = {}) {
407
440
 
408
441
  // --- OPEN handling -------------------------------------------------------
409
442
 
443
+ // Dispatch by kind, explicitly. Rev 1 was `kind === 'upgrade' ? serveUpgrade :
444
+ // serveHttp`, so a missing or unknown kind was silently served as an HTTP
445
+ // request to the user's workbench. Never guess: anything outside OPEN_KINDS
446
+ // (rev 2 has one kind, 'tls') is a protocol violation and is RST with
447
+ // 'client_error', with no request made to the local target.
410
448
  function handleStream(stream) {
411
449
  const meta = stream.meta || {};
412
450
  stream.on('error', (err) => warn('relay-client stream error', err));
413
- if (meta.kind === 'upgrade') serveUpgrade(stream, meta);
414
- else serveHttp(stream, meta);
451
+ const kind = meta.kind;
452
+ if (!OPEN_KINDS.includes(kind)) {
453
+ // Missing, non-string, or a kind this client does not know.
454
+ stream.rst('client_error');
455
+ return;
456
+ }
457
+ switch (kind) {
458
+ case 'upgrade':
459
+ serveUpgrade(stream, meta);
460
+ return;
461
+ case 'http':
462
+ serveHttp(stream, meta);
463
+ return;
464
+ default:
465
+ // 'tls': known to the protocol, not served by this client yet.
466
+ // WP-14 terminates TLS here.
467
+ stream.rst('client_error');
468
+ }
415
469
  }
416
470
 
417
471
  function serveHttp(stream, meta) {
@@ -13,6 +13,7 @@
13
13
 
14
14
  const transcriptSource = require('./transcript-source');
15
15
  const path = require('path');
16
+ const { NOOP_LAP, createPerfTrace, perfPhasesJson } = require('./perf-phase-trace');
16
17
  const { readJsonlHeader, normalizeCwd, readFirstUserMessage, wordOverlap } = require('./session-verify.js');
17
18
  const { detectAgentType, normalizeAgentType } = require('./agent-capabilities.js');
18
19
  const {
@@ -96,15 +97,21 @@ function readAgentFirstUserMessage(filePath, agentType) {
96
97
  * @param {string} [opts.projectsDir] — defaults to ~/.claude/projects
97
98
  * @returns {{ tabs: Array, summary: Object }}
98
99
  */
99
- function auditRelink({ db, projectsDir }) {
100
+ function _auditRelinkInner({ db, projectsDir }, trace) {
101
+ const lap = trace ? trace.lap : NOOP_LAP;
102
+ // a nested call already inside a lap: reported in counters, not phases
103
+ const countCall = (name, fn) => (trace ? trace.timed(name, fn) : fn());
100
104
  const claudeProjectsDir = projectsDir || path.join(process.env.HOME || '', '.claude', 'projects');
101
105
  const hasStartupTasks = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='startup_tasks'").get();
102
106
  if (!hasStartupTasks) {
107
+ lap('startup_tasks_probe', 0);
103
108
  return { tabs: [], summary: { total: 0, clean: 0, cross_linked: 0, stale: 0, orphan: 0, unclassified: 0, model_mismatch: 0 } };
104
109
  }
110
+ lap('startup_tasks_probe', 1);
105
111
 
106
112
  const stCols = new Set(db.prepare('PRAGMA table_info(startup_tasks)').all().map(c => c.name));
107
113
  const ctmCols = new Set(db.prepare('PRAGMA table_info(ctm_sessions)').all().map(c => c.name));
114
+ lap('schema_probe', 2);
108
115
  const stSelect = (name, alias = name, fallback = "''") => stCols.has(name)
109
116
  ? `st.${name} AS ${alias}`
110
117
  : `${fallback} AS ${alias}`;
@@ -135,6 +142,7 @@ function auditRelink({ db, projectsDir }) {
135
142
  LEFT JOIN ctm_sessions cs ON cs.id = st.ctm_session_id
136
143
  ORDER BY created_at
137
144
  `).all();
145
+ lap('load_startup_rows', rows.length);
138
146
 
139
147
  const agentCols = hasAgentSessions
140
148
  ? new Set(db.prepare('PRAGMA table_info(agent_sessions)').all().map(c => c.name))
@@ -162,6 +170,7 @@ function auditRelink({ db, projectsDir }) {
162
170
  agentsByCtm.get(agent.ctm_session_id).push(agent);
163
171
  ownerByAgent.set(agent.agent_session_id, agent);
164
172
  }
173
+ lap('load_agent_rows', agentRows.length);
165
174
 
166
175
  // Build a set of all ctm_session_ids — used to detect classic cross-link signature:
167
176
  // tab A's agent_session_id == tab B's ctm_session_id (means A got linked to B's original JSONL).
@@ -178,6 +187,7 @@ function auditRelink({ db, projectsDir }) {
178
187
  argsResumeId: extractResumeTarget(agentType, args)?.sessionId || null,
179
188
  });
180
189
  }
190
+ lap('build_startup_index', rows.length);
181
191
 
182
192
  const results = [];
183
193
  for (const row of rows) {
@@ -200,7 +210,7 @@ function auditRelink({ db, projectsDir }) {
200
210
  for (const ag of agentList) {
201
211
  const agType = normalizeAgentType(ag.provider || '') || agentType;
202
212
  const filePath = ag.jsonl_path || (agType === 'codex' ? '' : path.join(projectDir, `${ag.agent_session_id}.jsonl`));
203
- const fileView = transcriptSource.statTranscriptSync(filePath);
213
+ const fileView = countCall('stat_transcript', () => transcriptSource.statTranscriptSync(filePath));
204
214
  const exists = fileView.exists;
205
215
  let header = null;
206
216
  let stat = null;
@@ -208,10 +218,10 @@ function auditRelink({ db, projectsDir }) {
208
218
  let contentOverlap = null;
209
219
  if (exists) {
210
220
  stat = fileView;
211
- header = readAgentHeader(filePath, agType);
221
+ header = countCall('read_header', () => readAgentHeader(filePath, agType));
212
222
  // Only bother reading first user message if we have a tab message to compare
213
223
  if (tabFirstMsg && tabFirstMsg.length >= 10) {
214
- fileFirstUserMsg = readAgentFirstUserMessage(filePath, agType);
224
+ fileFirstUserMsg = countCall('read_first_user_message', () => readAgentFirstUserMessage(filePath, agType));
215
225
  if (fileFirstUserMsg) {
216
226
  contentOverlap = wordOverlap(tabFirstMsg, fileFirstUserMsg);
217
227
  }
@@ -239,7 +249,7 @@ function auditRelink({ db, projectsDir }) {
239
249
  let codexStateModel = '';
240
250
  if (agType === 'codex') {
241
251
  try {
242
- codexStateModel = String(getCodexStateThreadById(ag.agent_session_id, process.env.HOME)?.model || '');
252
+ codexStateModel = String(countCall('codex_state_read', () => getCodexStateThreadById(ag.agent_session_id, process.env.HOME))?.model || '');
243
253
  } catch { codexStateModel = ''; /* expected: best-effort audit column */ }
244
254
  }
245
255
  agents.push({
@@ -382,6 +392,7 @@ function auditRelink({ db, projectsDir }) {
382
392
  details,
383
393
  });
384
394
  }
395
+ lap('tab_loop', rows.length);
385
396
 
386
397
  const summary = {
387
398
  total: results.length,
@@ -392,8 +403,26 @@ function auditRelink({ db, projectsDir }) {
392
403
  unclassified: results.filter(r => r.verdict === 'unclassified').length,
393
404
  model_mismatch: results.filter(r => r.model_mismatch === true).length,
394
405
  };
406
+ lap('build_summary', results.length);
395
407
 
396
408
  return { tabs: results, summary };
397
409
  }
398
410
 
411
+ /**
412
+ * Perf-attribution wrapper (WP-0 / M-3): runs the audit with a phase trace, attaches it as
413
+ * `result.phases` (additive field — tabs/summary are unchanged) and logs one
414
+ * `[session-integrity] phases {...}` line for this run. Measurement only: with
415
+ * CTM_PERF_ATTRIBUTION off the trace is null, so nothing is attached or logged.
416
+ */
417
+ function auditRelink(opts) {
418
+ const trace = createPerfTrace();
419
+ const result = _auditRelinkInner(opts, trace);
420
+ if (trace) {
421
+ trace.lap('finalize', 1);
422
+ result.phases = trace.phases;
423
+ console.log(`[session-integrity] phases ${perfPhasesJson('relink', trace)}`);
424
+ }
425
+ return result;
426
+ }
427
+
399
428
  module.exports = { auditRelink };