create-walle 0.9.21 → 0.9.23

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 (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. package/template/wall-e/eval/weekly-eval-loop.js +0 -241
@@ -18,8 +18,12 @@ const { verifyCandidateForTab, readFirstUserMessage, wordOverlap } = require('./
18
18
  const { verifyLineage } = require('./session-lineage.js');
19
19
  const claudeDesktopSessions = require('./claude-desktop-sessions');
20
20
  const { detectAgentType, normalizeAgentType } = require('./agent-capabilities');
21
- const { resolveRestoreArgsFromAgentSessions } = require('./session-restore');
21
+ const {
22
+ resolveRestoreArgsFromAgentSessions,
23
+ resolveRestoreCwdFromAgentSession,
24
+ } = require('./session-restore');
22
25
  const { findCodexSessionFilesByFilename } = require('./session-history');
26
+ const { runSync } = require('./perf-tracker');
23
27
 
24
28
  const HOSTNAME = os.hostname();
25
29
  const fsp = fs.promises;
@@ -58,6 +62,26 @@ function statFileIfReadable(filePath) {
58
62
  }
59
63
  }
60
64
 
65
+ // Short-TTL readdir cache: the session-relink scan readdir's a Claude project dir per STALE
66
+ // session, so multiple stale sessions sharing a project dir each re-listed it within one tick.
67
+ // Coalesce those (1.5s TTL) to one readdirSync per dir per scan. Project-dir contents change
68
+ // slowly (a new JSONL appears on /resume), so a brief TTL is safe.
69
+ const _relinkReaddirCache = new Map(); // dir -> { at, files }
70
+ const _RELINK_READDIR_TTL_MS = Math.max(0, Number(process.env.CTM_RELINK_READDIR_TTL_MS) || 1500);
71
+ function _cachedRelinkReaddir(dir) {
72
+ const now = Date.now();
73
+ if (_RELINK_READDIR_TTL_MS > 0) {
74
+ const hit = _relinkReaddirCache.get(dir);
75
+ if (hit && (now - hit.at) < _RELINK_READDIR_TTL_MS) return hit.files;
76
+ }
77
+ const files = fs.readdirSync(dir);
78
+ if (_RELINK_READDIR_TTL_MS > 0) {
79
+ _relinkReaddirCache.set(dir, { at: now, files });
80
+ if (_relinkReaddirCache.size > 256) _relinkReaddirCache.delete(_relinkReaddirCache.keys().next().value);
81
+ }
82
+ return files;
83
+ }
84
+
61
85
  function nextCursorAfterBatch(start, batchLength, totalRows, deletedRows = 0) {
62
86
  if (batchLength <= 0 || start + batchLength >= totalRows) return 0;
63
87
  return Math.max(0, start + batchLength - Math.max(0, deletedRows));
@@ -82,6 +106,48 @@ function resolveAgentSessionJsonlInfo(row, opts = {}) {
82
106
  return null;
83
107
  }
84
108
 
109
+ // Same resolution as resolveAgentSessionJsonlInfo, but the primary-path stat comes from a
110
+ // pre-computed off-thread `statFiles` result (Map<statPath,{exists,size,mtimeMs}>) instead of a
111
+ // synchronous fs.statSync on the main loop. Only the rare codex readdir fallback (primary path
112
+ // missing on a codex row) drops to main. The synthetic stat carries the {size, mtime} the caller
113
+ // reads (mtime is a Date so `.toISOString()` matches the live-stat path).
114
+ function resolveJsonlInfoFromStatMap(row, statMap) {
115
+ const storedPath = row?.jsonl_path || '';
116
+ const statPath = claudeDesktopSessions.sourcePathForStat(storedPath);
117
+ const st = statPath ? statMap.get(statPath) : null;
118
+ if (st && st.exists) {
119
+ return { filePath: storedPath, stat: { size: st.size, mtime: new Date(st.mtimeMs) } };
120
+ }
121
+ const provider = normalizeAgentType(row?.provider || '') || row?.provider || '';
122
+ if (provider === 'codex' && row?.agent_session_id) {
123
+ // Primary path absent on a codex row → the only alternate location is a readdir scan; do it
124
+ // on main (rare). Skip the redundant primary statSync we already know missed.
125
+ return resolveAgentSessionJsonlInfo(row);
126
+ }
127
+ return null; // non-codex miss → no file (matches resolveAgentSessionJsonlInfo → null)
128
+ }
129
+
130
+ function parsedThreadLineage(parsed = {}) {
131
+ return {
132
+ threadSource: String(parsed.threadSource || parsed.thread_source || '').trim().toLowerCase(),
133
+ parentAgentSessionId: String(
134
+ parsed.parentAgentSessionId
135
+ || parsed.parent_agent_session_id
136
+ || parsed.parentThreadId
137
+ || parsed.parent_thread_id
138
+ || ''
139
+ ).trim(),
140
+ agentNickname: String(parsed.agentNickname || parsed.agent_nickname || '').trim(),
141
+ agentRole: String(parsed.agentRole || parsed.agent_role || '').trim(),
142
+ };
143
+ }
144
+
145
+ function isProviderChildParsedSession(parsed = {}) {
146
+ if (normalizeAgentType(parsed.agent || parsed.provider || '') !== 'codex') return false;
147
+ const lineage = parsedThreadLineage(parsed);
148
+ return lineage.threadSource === 'subagent' && !!lineage.parentAgentSessionId;
149
+ }
150
+
85
151
  /**
86
152
  * Register all session-related scheduler jobs.
87
153
  * @param {import('./scheduler').Scheduler} scheduler
@@ -108,7 +174,9 @@ function registerSessionJobs(scheduler, deps) {
108
174
  runAutoTitleGeneration, healStartupTaskSessionIds,
109
175
  sessionIntegrity, getAllSessionFiles, telemetry,
110
176
  syncCodingSessionModels,
111
- isRestoreInProgress, getServerStartedAt,
177
+ statFilesOffThread,
178
+ isRestoreInProgress, getServerStartedAt, getRestoreCompletedAt, isDbWarming, isDbWorkerDegraded,
179
+ getLastUserInteractiveInputAt,
112
180
  } = deps;
113
181
 
114
182
  // Grace window for session-relink: the job is disabled during startup restore
@@ -118,6 +186,7 @@ function registerSessionJobs(scheduler, deps) {
118
186
  // paths handle legit mappings in real time; the periodic relink is only a
119
187
  // safety net for context-overflow continuations, which can wait 5 minutes.
120
188
  const RELINK_GRACE_MS = 5 * 60 * 1000;
189
+ const STARTUP_MAINTENANCE_GRACE_MS = positiveInteger(process.env.CTM_STARTUP_MAINTENANCE_GRACE_MS, 5 * 60 * 1000);
121
190
  function _isInRelinkGraceWindow() {
122
191
  if (typeof isRestoreInProgress === 'function' && isRestoreInProgress()) return true;
123
192
  if (typeof getServerStartedAt === 'function') {
@@ -126,21 +195,73 @@ function registerSessionJobs(scheduler, deps) {
126
195
  }
127
196
  return false;
128
197
  }
198
+ function _isInStartupMaintenanceQuietWindow() {
199
+ if (typeof isRestoreInProgress === 'function' && isRestoreInProgress()) return true;
200
+ // Defer while the single serial db-owner worker is degraded under load: piling more
201
+ // maintenance onto a struggling worker deepens the contention cascade that wedges the
202
+ // cold-boot storm. The scheduler re-runs the job once the worker recovers.
203
+ if (typeof isDbWorkerDegraded === 'function' && isDbWorkerDegraded()) return true;
204
+ // isDbWarming() is currently a retired compatibility gate; keep honoring it
205
+ // here so any future SQLite-owned warmup can defer maintenance safely. The
206
+ // fixed startup grace below remains the active maintenance quiet window.
207
+ if (typeof isDbWarming === 'function' && isDbWarming()) return true;
208
+ if (typeof getServerStartedAt === 'function') {
209
+ const startedAt = Number(getServerStartedAt());
210
+ if (Number.isFinite(startedAt) && startedAt > 0 && (Date.now() - startedAt) < STARTUP_MAINTENANCE_GRACE_MS) {
211
+ return true;
212
+ }
213
+ }
214
+ if (typeof getRestoreCompletedAt === 'function') {
215
+ const completedAt = Number(getRestoreCompletedAt());
216
+ if (Number.isFinite(completedAt) && completedAt > 0 && (Date.now() - completedAt) < STARTUP_MAINTENANCE_GRACE_MS) {
217
+ return true;
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+ // Interactive guard: heavy maintenance jobs (the io-pool scans) must not START
223
+ // their synchronous DB/fs work within INTERACTIVE_DEFER_MS of a user keystroke.
224
+ // Those scans block the event loop, and a keystroke that lands while one runs
225
+ // stalls until the loop frees — the measured cause of per-keystroke lag. The
226
+ // jobs are safety nets, so deferring a few seconds is harmless. INTERACTIVE_MAX_
227
+ // DEFER_MS caps the defer so continuous typing can't starve maintenance forever.
228
+ const INTERACTIVE_DEFER_MS = positiveInteger(process.env.CTM_MAINT_INTERACTIVE_DEFER_MS, 1500);
229
+ const INTERACTIVE_MAX_DEFER_MS = positiveInteger(process.env.CTM_MAINT_INTERACTIVE_MAX_DEFER_MS, 20000);
230
+ let _interactiveDeferSince = 0;
231
+ function _userInteractingNow() {
232
+ if (typeof getLastUserInteractiveInputAt !== 'function') return false;
233
+ const last = Number(getLastUserInteractiveInputAt()) || 0;
234
+ return last > 0 && (Date.now() - last) < INTERACTIVE_DEFER_MS;
235
+ }
236
+ function _canRunStartupMaintenance() {
237
+ if (_isInStartupMaintenanceQuietWindow()) return false;
238
+ if (_userInteractingNow()) {
239
+ const now = Date.now();
240
+ if (!_interactiveDeferSince) _interactiveDeferSince = now;
241
+ // Starvation cap: once we've been deferring for MAX_DEFER, let a run through
242
+ // (then re-arm the window so we keep yielding to bursts of typing).
243
+ if (now - _interactiveDeferSince < INTERACTIVE_MAX_DEFER_MS) return false;
244
+ }
245
+ _interactiveDeferSince = 0;
246
+ return true;
247
+ }
129
248
 
130
249
  let sessionMetadataSyncCursor = 0;
131
250
  let ghostReaperLegacyCursor = 0;
132
251
  let ghostReaperAgentCursor = 0;
133
252
 
134
253
  // --- jsonl-reconciliation: safety-net full scan (catches fs.watch misses) ---
135
- // Runs after startup settles, then every 10min. fs.watch is the primary
136
- // detector; this is the reliability guarantee. Keeping the initial full scan
137
- // out of the first interaction window prevents typing stalls after restart.
254
+ // Runs immediately, then every 10min. fs.watch is the primary detector; this
255
+ // is the reliability guarantee. The work itself is bounded and yields between
256
+ // batches, so freshness is not traded for arbitrary startup quiet time.
138
257
  scheduler.register({
139
258
  name: 'jsonl-reconciliation',
140
- intervalMs: 15 * 60 * 1000,
141
- startDelayMs: 9 * 60 * 1000,
259
+ heavy: true, // long synchronous DB-map build + per-file parse; cap to 1 heavy/tick
260
+ intervalMs: 10 * 60 * 1000,
261
+ startDelayMs: 0,
142
262
  priority: 4,
143
263
  pool: 'io',
264
+ shouldRun: _canRunStartupMaintenance,
144
265
  run: async () => {
145
266
  const files = await getAllSessionFilesAsync();
146
267
  const db = dbModule.getDb();
@@ -158,10 +279,15 @@ function registerSessionJobs(scheduler, deps) {
158
279
  const claimedMap = {};
159
280
  const existingMap = new Map();
160
281
  try {
161
- for (const r of db.prepare("SELECT agent_session_id, ctm_session_id, file_size, modified_at FROM agent_sessions").all()) {
162
- existingMap.set(r.agent_session_id, r);
163
- if (r.ctm_session_id) claimedMap[r.agent_session_id] = r.ctm_session_id;
164
- }
282
+ // This loads EVERY agent_sessions row in one synchronous span; it grows
283
+ // with session count and is a prime event-loop-block suspect. Wrap it so
284
+ // a [perf-lag] block here is attributed by name instead of `(unknown)`.
285
+ runSync('jsonl-reconciliation:loadAgentSessionsMap', () => {
286
+ for (const r of db.prepare("SELECT agent_session_id, ctm_session_id, file_size, modified_at FROM agent_sessions").all()) {
287
+ existingMap.set(r.agent_session_id, r);
288
+ if (r.ctm_session_id) claimedMap[r.agent_session_id] = r.ctm_session_id;
289
+ }
290
+ });
165
291
  } catch (e) { console.error('[jsonl-reconciliation] metadata map build error:', e.message); }
166
292
 
167
293
  const updateMetadata = db.prepare("UPDATE agent_sessions SET file_size = ?, modified_at = ?, updated_at = datetime('now') WHERE agent_session_id = ?");
@@ -194,19 +320,20 @@ function registerSessionJobs(scheduler, deps) {
194
320
  }
195
321
 
196
322
  for (let index = 0; index < files.length; index++) {
197
- const { filePath, projectPath, projectEntry } = files[index];
323
+ const { filePath, projectPath, projectEntry, sessionId: discoveredSessionId } = files[index];
198
324
  try {
199
325
  const virtualSession = claudeDesktopSessions.parseVirtualSessionPath(filePath);
200
- const sessionId = virtualSession ? virtualSession.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
326
+ const fileSessionId = virtualSession ? virtualSession.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
327
+ let agentSessionId = String(discoveredSessionId || fileSessionId || '').trim();
201
328
  const statPath = claudeDesktopSessions.sourcePathForStat(filePath);
202
329
  const stat = await fsp.stat(statPath);
203
330
  const modifiedAt = stat.mtime.toISOString();
204
- const existing = existingMap.get(sessionId);
331
+ let existing = existingMap.get(agentSessionId) || existingMap.get(fileSessionId);
205
332
 
206
333
  if (existing) {
207
334
  // Update metadata if stale
208
335
  if (existing.file_size !== stat.size || existing.modified_at !== modifiedAt) {
209
- pendingMetadataUpdates.push({ sessionId, fileSize: stat.size, modifiedAt });
336
+ pendingMetadataUpdates.push({ sessionId: existing.agent_session_id || agentSessionId, fileSize: stat.size, modifiedAt });
210
337
  existing.file_size = stat.size;
211
338
  existing.modified_at = modifiedAt;
212
339
  upserted++;
@@ -219,8 +346,22 @@ function registerSessionJobs(scheduler, deps) {
219
346
  // New file not in DB — parse and insert. parseSessionFile reads a
220
347
  // bounded head/tail slice synchronously, so yield around it during
221
348
  // startup scans rather than wrapping hundreds of parses in one turn.
349
+ // Wrap the sync parse so a [perf-lag] block lands as a named span.
222
350
  await maybeYield();
223
- const parsed = parseSessionFile(filePath, projectPath, projectEntry);
351
+ const parsed = runSync('jsonl-reconciliation:parseSessionFile', () => parseSessionFile(filePath, projectPath, projectEntry));
352
+ agentSessionId = String(parsed.sessionId || agentSessionId || fileSessionId || '').trim();
353
+ existing = existingMap.get(agentSessionId);
354
+ if (existing) {
355
+ if (existing.file_size !== stat.size || existing.modified_at !== modifiedAt) {
356
+ pendingMetadataUpdates.push({ sessionId: agentSessionId, fileSize: stat.size, modifiedAt });
357
+ existing.file_size = stat.size;
358
+ existing.modified_at = modifiedAt;
359
+ upserted++;
360
+ if (pendingMetadataUpdates.length >= 25) flushMetadataUpdates();
361
+ }
362
+ await maybeYield(index % 20 === 19);
363
+ continue;
364
+ }
224
365
 
225
366
  // Skip ephemeral/aborted sessions (tiny file, no content) — prevents ghost rows
226
367
  if (parsed.fileSize < 1024 && !parsed.userMsgCount && !parsed.title && !parsed.firstMessage) {
@@ -228,29 +369,73 @@ function registerSessionJobs(scheduler, deps) {
228
369
  continue;
229
370
  }
230
371
 
231
- const ctmTabId = claimedMap[sessionId];
372
+ const lineage = parsedThreadLineage(parsed);
373
+ const parsedProvider = normalizeAgentType(parsed.agent || '') || parsed.agent || 'claude';
374
+ if (isProviderChildParsedSession(parsed)) {
375
+ try {
376
+ dbModule.upsertAgentSessionIdentity(agentSessionId, {
377
+ provider: parsedProvider,
378
+ providerResumeId: agentSessionId,
379
+ projectEntry: parsed.projectEntry || projectEntry,
380
+ projectPath: parsed.project || parsed.cwd || projectPath,
381
+ cwd: parsed.cwd || parsed.project || projectPath,
382
+ title: parsed.title || '',
383
+ firstMessage: parsed.firstMessage || '',
384
+ jsonlPath: filePath,
385
+ fileSize: parsed.fileSize || stat.size,
386
+ modifiedAt: parsed.modifiedAt || modifiedAt,
387
+ createdAt: parsed.timestamp || '',
388
+ model: parsed.modelId || parsed.model || '',
389
+ gitBranch: parsed.gitBranch || '',
390
+ slug: parsed.slug || '',
391
+ userMsgCount: parsed.userMsgCount || 0,
392
+ hostname: HOSTNAME,
393
+ ...lineage,
394
+ });
395
+ existingMap.set(agentSessionId, {
396
+ agent_session_id: agentSessionId,
397
+ ctm_session_id: null,
398
+ file_size: parsed.fileSize || stat.size,
399
+ modified_at: parsed.modifiedAt || modifiedAt,
400
+ });
401
+ upserted++;
402
+ } catch (e) {
403
+ console.error(`[jsonl-reconciliation] child identity upsert error for ${agentSessionId}:`, e.message);
404
+ }
405
+ await maybeYield(index % 10 === 9);
406
+ continue;
407
+ }
408
+
409
+ const ctmTabId = claimedMap[agentSessionId];
410
+ const parsedProjectEntry = parsed.projectEntry || projectEntry;
411
+ const parsedProjectPath = parsed.project || parsed.cwd || projectPath;
232
412
 
233
413
  if (ctmTabId) {
234
414
  // Already claimed by a CTM tab — update metadata
235
415
  pendingUpserts.push({ id: ctmTabId, data: {
236
- agentSessionId: sessionId,
237
- projectEntry, projectPath,
416
+ agentSessionId,
417
+ projectEntry: parsedProjectEntry,
418
+ projectPath: parsedProjectPath,
419
+ cwd: parsed.cwd || parsedProjectPath,
238
420
  jsonlPath: filePath,
239
421
  fileSize: parsed.fileSize,
240
422
  modifiedAt: parsed.modifiedAt,
241
423
  firstMessage: parsed.firstMessage || '',
242
424
  createdAt: parsed.timestamp || '',
243
- provider: parsed.agent || 'claude',
244
- model: parsed.modelId || '',
425
+ provider: parsedProvider,
426
+ model: parsed.modelId || parsed.model || '',
245
427
  gitBranch: parsed.gitBranch || '',
246
428
  slug: parsed.slug || '',
247
429
  userMsgCount: parsed.userMsgCount || 0,
430
+ hostname: HOSTNAME,
431
+ ...lineage,
248
432
  } });
249
433
  } else {
250
434
  // Standalone session — auto-create ctm_sessions + agent_sessions
251
- pendingUpserts.push({ id: sessionId, data: {
252
- agentSessionId: sessionId,
253
- projectEntry, projectPath,
435
+ pendingUpserts.push({ id: agentSessionId, data: {
436
+ agentSessionId,
437
+ projectEntry: parsedProjectEntry,
438
+ projectPath: parsedProjectPath,
254
439
  cwd: parsed.cwd || projectPath,
255
440
  title: parsed.title || '',
256
441
  jsonlPath: filePath,
@@ -258,12 +443,13 @@ function registerSessionJobs(scheduler, deps) {
258
443
  modifiedAt: parsed.modifiedAt,
259
444
  firstMessage: parsed.firstMessage || '',
260
445
  createdAt: parsed.timestamp || '',
261
- provider: parsed.agent || 'claude',
262
- model: parsed.modelId || '',
446
+ provider: parsedProvider,
447
+ model: parsed.modelId || parsed.model || '',
263
448
  gitBranch: parsed.gitBranch || '',
264
449
  slug: parsed.slug || '',
265
450
  userMsgCount: parsed.userMsgCount || 0,
266
451
  hostname: HOSTNAME,
452
+ ...lineage,
267
453
  } });
268
454
  }
269
455
  if (pendingUpserts.length >= 3) {
@@ -281,15 +467,17 @@ function registerSessionJobs(scheduler, deps) {
281
467
  });
282
468
 
283
469
  // --- ghost-reaper: remove DB rows whose JSONL files no longer exist ---
284
- // Runs hourly, well after startup, and checks a bounded round-robin batch. The
285
- // old full-table scan could spend minutes walking Codex history paths and stall
286
- // interactive terminal input.
470
+ // Checks a bounded round-robin batch. The old full-table scan could spend
471
+ // minutes walking Codex history paths; batching is the protection, not a long
472
+ // startup delay.
287
473
  scheduler.register({
288
474
  name: 'ghost-reaper',
289
- intervalMs: 60 * 60 * 1000,
290
- startDelayMs: 15 * 60 * 1000,
475
+ heavy: true,
476
+ intervalMs: 30 * 60 * 1000,
477
+ startDelayMs: 0,
291
478
  priority: 8,
292
479
  pool: 'io',
480
+ shouldRun: _canRunStartupMaintenance,
293
481
  run: async () => {
294
482
  const db = dbModule.getDb();
295
483
  let reaped = 0;
@@ -358,10 +546,12 @@ function registerSessionJobs(scheduler, deps) {
358
546
  // --- session-metadata-sync: update file metadata for active sessions ---
359
547
  scheduler.register({
360
548
  name: 'session-metadata-sync',
361
- intervalMs: 5 * 60 * 1000,
362
- startDelayMs: 5 * 60 * 1000,
549
+ heavy: true,
550
+ intervalMs: 30 * 1000,
551
+ startDelayMs: 0,
363
552
  priority: 5,
364
553
  pool: 'io',
554
+ shouldRun: _canRunStartupMaintenance,
365
555
  run: async () => {
366
556
  if (typeof deps.isConversationImportRunning === 'function' && deps.isConversationImportRunning()) {
367
557
  return { updated: 0, skipped: 'conversation_import_running' };
@@ -373,11 +563,16 @@ function registerSessionJobs(scheduler, deps) {
373
563
  const whereClause = hasProviderColumn
374
564
  ? "(jsonl_path != '' OR provider = 'codex')"
375
565
  : "jsonl_path != ''";
376
- const rows = db.prepare(`SELECT agent_session_id, jsonl_path, file_size, modified_at, ${providerExpr} FROM agent_sessions WHERE ${whereClause} ORDER BY agent_session_id`).all();
377
566
  const maxRows = positiveInteger(process.env.CTM_METADATA_SYNC_ROWS_PER_RUN, 50);
378
- const start = Math.min(sessionMetadataSyncCursor, Math.max(0, rows.length - 1));
379
- const batch = rows.slice(start, Math.min(start + maxRows, rows.length));
380
- sessionMetadataSyncCursor = (start + batch.length) >= rows.length ? 0 : start + batch.length;
567
+ // Load only this run's batch via SQL LIMIT/OFFSET. The previous `.all()`
568
+ // pulled EVERY agent_sessions row into memory each tick (then sliced 50)
569
+ // a synchronous main-thread load that grew with session count and was a
570
+ // top event-loop-block offender. Keyset over the stable agent_session_id
571
+ // order; wrap to the start when a short page signals the end.
572
+ const batch = db.prepare(
573
+ `SELECT agent_session_id, jsonl_path, file_size, modified_at, ${providerExpr} FROM agent_sessions WHERE ${whereClause} ORDER BY agent_session_id LIMIT ? OFFSET ?`
574
+ ).all(maxRows, sessionMetadataSyncCursor);
575
+ sessionMetadataSyncCursor = batch.length < maxRows ? 0 : sessionMetadataSyncCursor + maxRows;
381
576
  const updateMetadata = db.prepare("UPDATE agent_sessions SET jsonl_path = ?, file_size = ?, modified_at = ?, updated_at = datetime('now') WHERE agent_session_id = ?");
382
577
  let lastYieldAt = Date.now();
383
578
 
@@ -387,10 +582,23 @@ function registerSessionJobs(scheduler, deps) {
387
582
  lastYieldAt = Date.now();
388
583
  }
389
584
 
585
+ // Offload the per-row fs.statSync to the read-pool as ONE batched `statFiles` call instead
586
+ // of a synchronous stat loop on the main event loop (the freeze). On pool unavailable/timeout
587
+ // statMap is null → fall back to the on-main resolver (only 50 rows). CTM_METADATA_SYNC_OFFLOAD=0 opts out.
588
+ let statMap = null;
589
+ if (process.env.CTM_METADATA_SYNC_OFFLOAD !== '0' && typeof statFilesOffThread === 'function') {
590
+ const statPaths = [...new Set(batch
591
+ .map((r) => claudeDesktopSessions.sourcePathForStat(r.jsonl_path || ''))
592
+ .filter(Boolean))];
593
+ try {
594
+ statMap = await statFilesOffThread(statPaths, positiveInteger(process.env.CTM_METADATA_SYNC_READPOOL_TIMEOUT_MS, 10000));
595
+ } catch { statMap = null; }
596
+ }
597
+
390
598
  for (let index = 0; index < batch.length; index++) {
391
599
  const row = batch[index];
392
600
  try {
393
- const fileInfo = resolveAgentSessionJsonlInfo(row);
601
+ const fileInfo = statMap ? resolveJsonlInfoFromStatMap(row, statMap) : resolveAgentSessionJsonlInfo(row);
394
602
  if (!fileInfo) continue;
395
603
  const stat = fileInfo.stat;
396
604
  const newMtime = stat.mtime.toISOString();
@@ -401,17 +609,18 @@ function registerSessionJobs(scheduler, deps) {
401
609
  } catch (e) { console.error(`[session-metadata-sync] error for ${row.agent_session_id}:`, e.message); }
402
610
  await maybeYield(index % 25 === 24);
403
611
  }
404
- return { updated, checked: batch.length, total: rows.length, partial: sessionMetadataSyncCursor !== 0 };
612
+ return { updated, checked: batch.length, partial: sessionMetadataSyncCursor !== 0 };
405
613
  },
406
614
  });
407
615
 
408
616
  // --- auto-title: AI-generated titles ---
409
617
  scheduler.register({
410
618
  name: 'auto-title',
411
- intervalMs: 15 * 60 * 1000,
412
- startDelayMs: 13 * 60 * 1000,
619
+ intervalMs: 5 * 60 * 1000,
620
+ startDelayMs: 30 * 1000,
413
621
  priority: 7,
414
622
  pool: 'llm',
623
+ shouldRun: _canRunStartupMaintenance,
415
624
  run: async () => {
416
625
  await runAutoTitleGeneration();
417
626
  return { done: true };
@@ -424,10 +633,11 @@ function registerSessionJobs(scheduler, deps) {
424
633
  // synthesize startup_tasks mappings from live runtime state.
425
634
  scheduler.register({
426
635
  name: 'session-healer',
427
- intervalMs: 10 * 60 * 1000,
428
- startDelayMs: 7 * 60 * 1000,
636
+ intervalMs: 5 * 60 * 1000,
637
+ startDelayMs: 30 * 1000,
429
638
  priority: 6,
430
639
  pool: 'io',
640
+ shouldRun: _canRunStartupMaintenance,
431
641
  run: async () => {
432
642
  await healStartupTaskSessionIds();
433
643
  return { done: true };
@@ -438,8 +648,9 @@ function registerSessionJobs(scheduler, deps) {
438
648
  scheduler.register({
439
649
  name: 'session-integrity',
440
650
  intervalMs: 30 * 60 * 1000,
441
- startDelayMs: 20 * 60 * 1000,
651
+ startDelayMs: 60 * 1000,
442
652
  priority: 8,
653
+ shouldRun: _canRunStartupMaintenance,
443
654
  run: async () => {
444
655
  const result = deps.runSessionIntegrity
445
656
  ? await deps.runSessionIntegrity()
@@ -450,6 +661,55 @@ function registerSessionJobs(scheduler, deps) {
450
661
  },
451
662
  });
452
663
 
664
+ // --- approval-self-adapt: keep the learned approval-detection rule set healthy
665
+ // (retire rules whose patterns no longer compile/are unsafe). The behavioral
666
+ // learning signals fire inline on user input; this is the periodic GC. ---
667
+ if (deps.approvalSelfAdapt && deps.approvalAiRefinement) {
668
+ scheduler.register({
669
+ name: 'approval-self-adapt',
670
+ intervalMs: 45 * 60 * 1000,
671
+ startDelayMs: 90 * 1000,
672
+ priority: 4,
673
+ pool: 'io',
674
+ // runSelfAdaptMaintenance is synchronous and has been seen blocking the
675
+ // event loop for several seconds (recompiles/validates the learned rule
676
+ // set). Defer it out of the startup-quiet window and away from active
677
+ // typing — the interactive guard's starvation cap still forces it to run.
678
+ shouldRun: _canRunStartupMaintenance,
679
+ run: async () => {
680
+ const result = deps.approvalSelfAdapt.runSelfAdaptMaintenance({
681
+ dbModule,
682
+ refinement: deps.approvalAiRefinement,
683
+ });
684
+ if (result && result.disabled) {
685
+ console.log(`[approval-self-adapt] maintenance: checked=${result.checked} retired=${result.disabled} kept=${result.kept}`);
686
+ }
687
+ return result || { checked: 0 };
688
+ },
689
+ });
690
+ }
691
+
692
+ // --- walle-health-watchdog: detect unrecoverable Wall-E failures and raise
693
+ // a repair incident (notify + offer full-auto coding-agent repair). ---
694
+ if (deps.walleRepair) {
695
+ scheduler.register({
696
+ name: 'walle-health-watchdog',
697
+ intervalMs: 20000,
698
+ startDelayMs: 45000,
699
+ priority: 7,
700
+ pool: 'io',
701
+ // Health detection a few seconds late is fine; never let the 20s watchdog
702
+ // tick block keystroke echo. Defer during the startup-quiet window (Wall-E
703
+ // is still booting → avoids false-positive failure detection) and while
704
+ // the user is typing. Starvation cap bounds the defer to ~20s.
705
+ shouldRun: () => process.env.WALLE_REPAIR_DISABLED !== '1' && _canRunStartupMaintenance(),
706
+ run: async () => {
707
+ const result = await deps.walleRepair.onWatchdogTick();
708
+ return result || { ticked: true };
709
+ },
710
+ });
711
+ }
712
+
453
713
  // --- session-relink: detect continuation files for active sessions ---
454
714
  scheduler.register({
455
715
  name: 'session-relink',
@@ -474,7 +734,7 @@ function registerSessionJobs(scheduler, deps) {
474
734
 
475
735
  let newerMatch = null;
476
736
  let newerMtime = 0;
477
- for (const file of fs.readdirSync(session._claudeProjectDir)) {
737
+ for (const file of _cachedRelinkReaddir(session._claudeProjectDir)) {
478
738
  if (!file.endsWith('.jsonl')) continue;
479
739
  const csId = file.replace(/\.jsonl$/, '');
480
740
  if (csId === session._claudeSessionId) continue;
@@ -561,11 +821,19 @@ function registerSessionJobs(scheduler, deps) {
561
821
  if (syncCodingSessionModels) {
562
822
  scheduler.register({
563
823
  name: 'model-sync',
564
- intervalMs: 30000,
824
+ intervalMs: Math.max(3000, Number(process.env.CTM_MODEL_SYNC_INTERVAL_MS) || 5000),
565
825
  startDelayMs: 5000,
566
826
  priority: 9,
567
- run: () => {
568
- syncCodingSessionModels();
827
+ shouldRun: _canRunStartupMaintenance,
828
+ run: async () => {
829
+ // Prefer the off-thread variant: the claude JSONL scan + Codex state-DB reads run on the
830
+ // read-pool worker; only the small apply (model→registry, title decision, session mutate)
831
+ // stays on main. Falls back to the synchronous version when offload is disabled.
832
+ if (process.env.CTM_MODEL_SYNC_OFFLOAD !== '0' && typeof deps.syncCodingSessionModelsOffThread === 'function') {
833
+ await deps.syncCodingSessionModelsOffThread();
834
+ } else {
835
+ syncCodingSessionModels();
836
+ }
569
837
  return { done: true };
570
838
  },
571
839
  });
@@ -575,29 +843,62 @@ function registerSessionJobs(scheduler, deps) {
575
843
  if (deps.runIncrementalConversationImport) {
576
844
  scheduler.register({
577
845
  name: 'conversation-import',
846
+ heavy: true,
578
847
  intervalMs: 10 * 60 * 1000,
579
- startDelayMs: 11 * 60 * 1000,
848
+ startDelayMs: 0,
580
849
  priority: 6,
581
850
  pool: 'io',
851
+ shouldRun: _canRunStartupMaintenance,
582
852
  run: async () => {
583
853
  return await deps.runIncrementalConversationImport();
584
854
  },
585
855
  });
586
856
  }
587
857
 
858
+ // --- cursor-conversation-import: import Cursor blob-graph stores → session_conversations ---
859
+ // Cursor conversations aren't JSONL; they're a content-addressed blob graph that is
860
+ // expensive to reconstruct. Import them on a cadence (off the main thread, on the db-owner
861
+ // worker) so the cache read path serves them instead of reconstructing on every UI poll.
862
+ // Signature-gated inside the job, so idle stores cost ~nothing; runs even when there are no
863
+ // cursor sessions (then it's a no-op query).
864
+ if (deps.runCursorConversationImport) {
865
+ scheduler.register({
866
+ name: 'cursor-conversation-import',
867
+ heavy: true,
868
+ intervalMs: Math.max(60 * 1000, Number(process.env.CTM_CURSOR_IMPORT_INTERVAL_MS) || 3 * 60 * 1000),
869
+ startDelayMs: 15 * 1000,
870
+ priority: 7,
871
+ pool: 'io',
872
+ shouldRun: _canRunStartupMaintenance,
873
+ run: async () => {
874
+ return await deps.runCursorConversationImport();
875
+ },
876
+ });
877
+ }
878
+
588
879
  // --- message-backfill: extract session_messages from session_conversations for FTS ---
589
880
  if (deps.backfillSessionMessages) {
590
881
  scheduler.register({
591
882
  name: 'message-backfill',
883
+ heavy: true,
592
884
  intervalMs: 10 * 60 * 1000,
593
- startDelayMs: 15 * 60 * 1000,
885
+ startDelayMs: 0,
886
+ dependsOn: ['conversation-import'],
887
+ cascadeFrom: ['conversation-import'],
888
+ // Only cascade when conversation-import actually imported a session. The default
889
+ // cascade check fires on ANY numeric > 0 in the import result — including `total`
890
+ // (sessions on disk) and `scanned` — so an idle import scan that imported nothing
891
+ // still triggered a backfill on nearly every 5s tick (347 runs / 33 min in one
892
+ // sample, churning the db-owner worker + write lock). Gate on real new work.
893
+ cascadeCheck: (result) => Number(result && result.imported) > 0,
594
894
  priority: 8,
595
895
  pool: 'io',
896
+ shouldRun: _canRunStartupMaintenance,
596
897
  run: async () => {
597
- const count = deps.backfillSessionMessagesAsync
898
+ const result = deps.backfillSessionMessagesAsync
598
899
  ? await deps.backfillSessionMessagesAsync({ limit: 1, chunkSize: 10 })
599
900
  : deps.backfillSessionMessages();
600
- return { extracted: count };
901
+ return result && typeof result === 'object' ? result : { extracted: Number(result || 0) };
601
902
  },
602
903
  });
603
904
  }
@@ -609,10 +910,11 @@ function registerSessionJobs(scheduler, deps) {
609
910
  scheduler.register({
610
911
  name: 'persist-agent-session-id',
611
912
  intervalMs: 60 * 1000,
612
- startDelayMs: 6 * 60 * 1000,
913
+ startDelayMs: 45 * 1000,
613
914
  priority: 6,
614
915
  pool: 'io',
615
- run: () => {
916
+ shouldRun: _canRunStartupMaintenance,
917
+ run: async () => {
616
918
  let synced = 0;
617
919
  let skippedNoMapping = 0;
618
920
  try {
@@ -620,7 +922,12 @@ function registerSessionJobs(scheduler, deps) {
620
922
  const rows = dbModule.getDb().prepare(
621
923
  `SELECT * FROM startup_tasks WHERE (cmd LIKE '%claude%' OR cmd LIKE '%codex%' OR cmd LIKE '%gemini%')`
622
924
  ).all();
925
+ // Per-row resolution does several DB queries + a cwd fs check; over many rows this
926
+ // is an unbounded synchronous main-thread scan. Yield to the loop every ~25ms so a
927
+ // large startup_tasks table can't stall keystroke echo (matches session-metadata-sync).
928
+ let _lastYield = Date.now();
623
929
  for (const r of rows) {
930
+ if (Date.now() - _lastYield > 25) { await yieldToEventLoop(); _lastYield = Date.now(); }
624
931
  const ctmSessionId = r.ctm_session_id;
625
932
  const session = sessions.get(ctmSessionId);
626
933
  const agentType = detectAgentType(r.cmd || session?.cmd || '') || normalizeAgentType(r.type || session?.type || '');
@@ -647,7 +954,14 @@ function registerSessionJobs(scheduler, deps) {
647
954
  const projectDir = choice.row?.jsonl_path ? path.dirname(choice.row.jsonl_path) : (r.agent_project_dir || r.claude_project_dir || '');
648
955
  const currentProjectDir = r.agent_project_dir || r.claude_project_dir || '';
649
956
  const hasLegacyProjectDir = cols.has('agent_project_dir') || cols.has('claude_project_dir');
650
- if (currentArgs === nextArgs && (!hasLegacyProjectDir || currentProjectDir === projectDir)) continue;
957
+ const cwdChoice = resolveRestoreCwdFromAgentSession({
958
+ task: { ...r, cwd: r.cwd, ctm_session_id: ctmSessionId, session_id: ctmSessionId },
959
+ row: choice.row,
960
+ agentType,
961
+ fsExists: fs.existsSync,
962
+ });
963
+ const needsCwdHeal = !!(cwdChoice.changed && cwdChoice.cwd && cwdChoice.cwd !== r.cwd);
964
+ if (currentArgs === nextArgs && (!hasLegacyProjectDir || currentProjectDir === projectDir) && !needsCwdHeal) continue;
651
965
  try {
652
966
  dbModule.updateStartupTaskAgentSession(
653
967
  ctmSessionId,
@@ -655,6 +969,9 @@ function registerSessionJobs(scheduler, deps) {
655
969
  projectDir,
656
970
  choice.args
657
971
  );
972
+ if (needsCwdHeal && typeof dbModule.updateStartupTaskCwd === 'function') {
973
+ dbModule.updateStartupTaskCwd(ctmSessionId, cwdChoice.cwd);
974
+ }
658
975
  synced++;
659
976
  } catch (e) { console.error('[persist-agent-session-id] update error:', e.message); }
660
977
  }
@@ -671,15 +988,36 @@ function registerSessionJobs(scheduler, deps) {
671
988
  * @param {import('./session-stream').SessionStream} deps.sessionStream
672
989
  */
673
990
  function registerStreamJobs(scheduler, deps) {
674
- const { sessionStream } = deps;
991
+ const { sessionStream, isRestoreInProgress, getServerStartedAt, getRestoreCompletedAt, isDbWarming, isDbWorkerDegraded } = deps;
675
992
  if (!sessionStream) return;
993
+ const STARTUP_MAINTENANCE_GRACE_MS = positiveInteger(process.env.CTM_STARTUP_MAINTENANCE_GRACE_MS, 5 * 60 * 1000);
994
+ function _canRunStartupMaintenance() {
995
+ if (typeof isRestoreInProgress === 'function' && isRestoreInProgress()) return false;
996
+ // Defer while the serial db-owner worker is degraded under load (cold-boot contention cascade).
997
+ if (typeof isDbWorkerDegraded === 'function' && isDbWorkerDegraded()) return false;
998
+ if (typeof isDbWarming === 'function' && isDbWarming()) return false;
999
+ if (typeof getServerStartedAt === 'function') {
1000
+ const startedAt = Number(getServerStartedAt());
1001
+ if (Number.isFinite(startedAt) && startedAt > 0 && (Date.now() - startedAt) < STARTUP_MAINTENANCE_GRACE_MS) {
1002
+ return false;
1003
+ }
1004
+ }
1005
+ if (typeof getRestoreCompletedAt === 'function') {
1006
+ const completedAt = Number(getRestoreCompletedAt());
1007
+ if (Number.isFinite(completedAt) && completedAt > 0 && (Date.now() - completedAt) < STARTUP_MAINTENANCE_GRACE_MS) {
1008
+ return false;
1009
+ }
1010
+ }
1011
+ return true;
1012
+ }
676
1013
 
677
1014
  // stream-cleanup: remove stale non-CTM sessions inactive >30min
678
1015
  scheduler.register({
679
1016
  name: 'stream-cleanup',
680
1017
  intervalMs: 10 * 60 * 1000,
681
- startDelayMs: 5 * 60 * 1000,
1018
+ startDelayMs: 30 * 1000,
682
1019
  priority: 9,
1020
+ shouldRun: _canRunStartupMaintenance,
683
1021
  run: () => {
684
1022
  const cleaned = sessionStream.cleanStale();
685
1023
  return { cleaned };
@@ -689,10 +1027,12 @@ function registerStreamJobs(scheduler, deps) {
689
1027
  // stream-reconciliation: stat all tracked files, trigger incremental read if changed
690
1028
  scheduler.register({
691
1029
  name: 'stream-reconciliation',
692
- intervalMs: 10 * 60 * 1000,
693
- startDelayMs: 8 * 60 * 1000,
1030
+ heavy: true,
1031
+ intervalMs: 60 * 1000,
1032
+ startDelayMs: 30 * 1000,
694
1033
  priority: 5,
695
1034
  pool: 'io',
1035
+ shouldRun: _canRunStartupMaintenance,
696
1036
  run: async () => {
697
1037
  return await sessionStream.reconcile({ maxSessions: 12, maxChunksPerSession: 4 });
698
1038
  },