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
@@ -5,7 +5,9 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { StringDecoder } = require('string_decoder');
7
7
 
8
- const { codexMessageFromEntry, codexRolloutIdFromPath, codexUserKey } = require('./session-history');
8
+ const { codexMessageFromEntry, codexRolloutIdFromPath, createCodexUserDeduper } = require('./session-history');
9
+ const { isProviderGeneratedUserContextText } = require('./provider-user-context');
10
+ const { sortTimelineMessages } = require('./timeline-order');
9
11
 
10
12
  const PARSER_VERSION = 1;
11
13
  const DEFAULT_CHUNK_BYTES = 1024 * 1024;
@@ -16,6 +18,16 @@ function sha1(value) {
16
18
  return crypto.createHash('sha1').update(String(value || '')).digest('hex');
17
19
  }
18
20
 
21
+ function parseMetadataJson(value) {
22
+ if (!value) return {};
23
+ try {
24
+ const parsed = JSON.parse(String(value));
25
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
19
31
  function normalizeProvider(provider, filePath = '') {
20
32
  const raw = String(provider || '').trim().toLowerCase();
21
33
  if (raw) return raw;
@@ -28,6 +40,48 @@ function sourceIdFromPath(filePath) {
28
40
  || path.basename(filePath || '').replace(/\.jsonl(\.bak)?$/, '');
29
41
  }
30
42
 
43
+ function isSubagentTranscriptPath(filePath) {
44
+ return String(filePath || '').replace(/\\/g, '/').includes('/subagents/');
45
+ }
46
+
47
+ function loadedTranscriptMessageFromRow(row) {
48
+ const metadata = parseMetadataJson(row.metadata_json);
49
+ const text = String(row.text || '');
50
+ let role = row.role;
51
+ let agentLabel = metadata.agentLabel || metadata.roleLabel || '';
52
+ let roleLabel = metadata.roleLabel || metadata.agentLabel || '';
53
+ const sourceKind = metadata.sourceKind || (isSubagentTranscriptPath(row.jsonl_path) ? 'subagent' : '');
54
+
55
+ if (role === 'user' && isProviderGeneratedUserContextText(text)) return null;
56
+
57
+ if (sourceKind === 'subagent') {
58
+ metadata.sourceKind = 'subagent';
59
+ if (role === 'user') {
60
+ metadata.originalRole = metadata.originalRole || 'user';
61
+ metadata.promptKind = metadata.promptKind || 'subagent';
62
+ role = 'system';
63
+ agentLabel = agentLabel || 'Subagent prompt';
64
+ roleLabel = roleLabel || 'Subagent prompt';
65
+ } else if (!agentLabel && role === 'assistant') {
66
+ agentLabel = 'Subagent';
67
+ roleLabel = roleLabel || 'Subagent';
68
+ }
69
+ }
70
+
71
+ return {
72
+ role,
73
+ text,
74
+ timestamp: row.timestamp || '',
75
+ metadata,
76
+ source: 'transcript-events',
77
+ provider: row.provider || '',
78
+ agentSessionId: row.agent_session_id,
79
+ ctmSessionId: row.ctm_session_id || '',
80
+ agentLabel: agentLabel || undefined,
81
+ roleLabel: roleLabel || undefined,
82
+ };
83
+ }
84
+
31
85
  function ensureTranscriptTables(db, options = {}) {
32
86
  db.exec(`
33
87
  CREATE TABLE IF NOT EXISTS transcript_files (
@@ -76,6 +130,33 @@ function ensureTranscriptTables(db, options = {}) {
76
130
  ON transcript_events(timestamp);
77
131
  `);
78
132
 
133
+ // Phase 1 of the blob→rows migration: enrich transcript_events so it can be the
134
+ // PRIMARY conversation store (not just a search overlay). Additive, idempotent —
135
+ // SQLite has no ADD COLUMN IF NOT EXISTS, so each ALTER is guarded individually.
136
+ // turn_ordinal – turn group (a user prompt + its answer/tool calls); assigned at
137
+ // ingest, drives turn-mode pagination + render grouping.
138
+ // token_count – cheap per-row token estimate; summed for the session total.
139
+ // uuid/parent_uuid – Claude message identity (promoted from metadata_json) for
140
+ // streamed-partial dedup and parentUuid collapse parity.
141
+ // model/usage_json – per-message model + provider usage when present.
142
+ // content_json – optional inline rich content for recent rows; empty rows
143
+ // hydrate on demand from (jsonl_path, byte_start, byte_end).
144
+ for (const ddl of [
145
+ 'ALTER TABLE transcript_events ADD COLUMN turn_ordinal INTEGER DEFAULT 0',
146
+ 'ALTER TABLE transcript_events ADD COLUMN token_count INTEGER DEFAULT 0',
147
+ "ALTER TABLE transcript_events ADD COLUMN uuid TEXT DEFAULT ''",
148
+ "ALTER TABLE transcript_events ADD COLUMN parent_uuid TEXT DEFAULT ''",
149
+ "ALTER TABLE transcript_events ADD COLUMN model TEXT DEFAULT ''",
150
+ "ALTER TABLE transcript_events ADD COLUMN usage_json TEXT DEFAULT ''",
151
+ "ALTER TABLE transcript_events ADD COLUMN content_json TEXT DEFAULT ''",
152
+ ]) {
153
+ try { db.exec(ddl); } catch { /* column already exists */ }
154
+ }
155
+ try {
156
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_transcript_events_ctm_turn
157
+ ON transcript_events(ctm_session_id, turn_ordinal, seq);`);
158
+ } catch { /* index already exists / column missing on an old build */ }
159
+
79
160
  try {
80
161
  db.exec(`
81
162
  CREATE VIRTUAL TABLE IF NOT EXISTS transcript_events_fts USING fts5(
@@ -129,6 +210,13 @@ function truncateVisibleText(text) {
129
210
  return `${value.slice(0, MAX_VISIBLE_TEXT)}...truncated (${value.length} chars)`;
130
211
  }
131
212
 
213
+ // Cheap per-row token estimate (~4 chars/token). Feeds an incremental session
214
+ // total; exact provider-authoritative counts are recomputed on the live path.
215
+ function estimateRowTokens(text) {
216
+ const len = String(text || '').length;
217
+ return len ? Math.max(1, Math.ceil(len / 4)) : 0;
218
+ }
219
+
132
220
  function messageFromEntry(entry, provider, state = {}) {
133
221
  if (!entry || typeof entry !== 'object') return null;
134
222
 
@@ -145,9 +233,8 @@ function messageFromEntry(entry, provider, state = {}) {
145
233
  const msg = codexMessageFromEntry(entry);
146
234
  if (!msg) return null;
147
235
  if (msg.role === 'user') {
148
- const key = codexUserKey(msg.text);
149
- if (!key || state.codexSeenUsers?.has(key)) return null;
150
- if (state.codexSeenUsers) state.codexSeenUsers.add(key);
236
+ if (!state.codexUserDeduper) state.codexUserDeduper = createCodexUserDeduper();
237
+ if (!state.codexUserDeduper.remember(msg.text, msg.timestamp)) return null;
151
238
  }
152
239
  return { role: msg.role, text: truncateVisibleText(msg.text), timestamp: msg.timestamp || '', metadata: {} };
153
240
  }
@@ -167,6 +254,8 @@ function messageFromEntry(entry, provider, state = {}) {
167
254
  let text = '';
168
255
  if (entry.partType === 'tool_call') text = `[Tool: ${data.name || 'tool'}]`;
169
256
  else if (entry.partType === 'tool_result') text = `[Tool result: ${data.name || 'tool'}]`;
257
+ else if (entry.partType === 'artifact') text = artifactText(data);
258
+ else if (entry.partType === 'capability_routed') text = capabilityRouteText(data);
170
259
  else if (entry.partType === 'error') text = `[Wall-E error] ${data.message || ''}`.trim();
171
260
  else if (entry.partType === 'cancelled') text = '[Wall-E cancelled]';
172
261
  return text ? { role: 'system', text, timestamp: entry.timestamp || '', metadata: { partType: entry.partType || '' } } : null;
@@ -179,11 +268,18 @@ function messageFromEntry(entry, provider, state = {}) {
179
268
  text = isToolResult ? text : cleanClaudeUserText(text);
180
269
  text = truncateVisibleText(text);
181
270
  if (!text) return null;
271
+ if (!isToolResult && isProviderGeneratedUserContextText(text)) return null;
272
+ const isSubagentTask = !isToolResult && state.sourceKind === 'subagent';
182
273
  return {
183
- role: isToolResult ? 'system' : 'user',
274
+ role: isToolResult || isSubagentTask ? 'system' : 'user',
184
275
  text,
185
276
  timestamp: entry.timestamp || '',
186
- metadata: { uuid: entry.uuid || '', cwd: entry.cwd || '', gitBranch: entry.gitBranch || '' },
277
+ metadata: {
278
+ uuid: entry.uuid || '',
279
+ cwd: entry.cwd || '',
280
+ gitBranch: entry.gitBranch || '',
281
+ ...(isSubagentTask ? { sourceKind: 'subagent', originalRole: 'user', promptKind: 'subagent', roleLabel: 'Subagent prompt', agentLabel: 'Subagent prompt' } : {}),
282
+ },
187
283
  };
188
284
  }
189
285
 
@@ -201,6 +297,24 @@ function messageFromEntry(entry, provider, state = {}) {
201
297
  return null;
202
298
  }
203
299
 
300
+ function artifactText(data = {}) {
301
+ const artifact = data.artifact || {};
302
+ const kind = artifact.kind || data.type || 'artifact';
303
+ const label = artifact.path || artifact.originalPath || artifact.artifactId || '';
304
+ const bits = [`[Artifact: ${kind}]`];
305
+ if (artifact.bytes) bits.push(`${artifact.bytes} bytes`);
306
+ if (artifact.metadata?.page_count) bits.push(`${artifact.metadata.page_count} pages`);
307
+ if (label) bits.push(label);
308
+ return bits.join(' ');
309
+ }
310
+
311
+ function capabilityRouteText(data = {}) {
312
+ const capabilities = Array.isArray(data.capabilities) ? data.capabilities : [];
313
+ if (!capabilities.length) return '';
314
+ const names = capabilities.map((capability) => capability.id || capability.label).filter(Boolean);
315
+ return names.length ? `[Wall-E capability routes] ${names.join(', ')}` : '';
316
+ }
317
+
204
318
  function readCompleteLines(filePath, { startOffset, maxBytes, partialLine, trimLeadingPartial }) {
205
319
  const fd = fs.openSync(filePath, 'r');
206
320
  const decoder = new StringDecoder('utf8');
@@ -349,7 +463,11 @@ function ingestJsonlFile(db, options = {}) {
349
463
  trimLeadingPartial: cursor.trimLeadingPartial,
350
464
  });
351
465
 
352
- const state = { codexSeenUsers: new Set() };
466
+ const state = {
467
+ codexUserDeduper: createCodexUserDeduper(),
468
+ sourceFile: filePath,
469
+ sourceKind: isSubagentTranscriptPath(filePath) ? 'subagent' : '',
470
+ };
353
471
  const messages = [];
354
472
  for (const lineInfo of read.lines) {
355
473
  let entry;
@@ -359,13 +477,17 @@ function ingestJsonlFile(db, options = {}) {
359
477
  messages.push({ ...message, byteStart: lineInfo.byteStart, byteEnd: lineInfo.byteEnd });
360
478
  }
361
479
 
362
- const nextSeqRow = db.prepare('SELECT COALESCE(MAX(seq) + 1, 0) AS nextSeq FROM transcript_events WHERE agent_session_id = ?').get(agentSessionId);
480
+ // Seed seq AND turn_ordinal from the durable max so a restart mid-session never
481
+ // resets either (crash-safe resume — turns must stay monotonic across boots).
482
+ const nextSeqRow = db.prepare('SELECT COALESCE(MAX(seq) + 1, 0) AS nextSeq, COALESCE(MAX(turn_ordinal), 0) AS maxTurn FROM transcript_events WHERE agent_session_id = ?').get(agentSessionId);
363
483
  let seq = Number(nextSeqRow?.nextSeq || 0);
484
+ let turn = Number(nextSeqRow?.maxTurn || 0);
364
485
  let inserted = 0;
365
486
  const insertEvent = db.prepare(`
366
487
  INSERT OR IGNORE INTO transcript_events
367
- (ctm_session_id, agent_session_id, provider, jsonl_path, seq, role, timestamp, content, byte_start, byte_end, event_hash, metadata_json)
368
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
488
+ (ctm_session_id, agent_session_id, provider, jsonl_path, seq, role, timestamp, content, byte_start, byte_end, event_hash, metadata_json,
489
+ turn_ordinal, token_count, uuid, parent_uuid, model, usage_json, content_json)
490
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
369
491
  `);
370
492
  const insertEventFts = hasTranscriptEventsFts(db)
371
493
  ? db.prepare('INSERT INTO transcript_events_fts(rowid, content) VALUES (?, ?)')
@@ -390,8 +512,24 @@ function ingestJsonlFile(db, options = {}) {
390
512
  updated_at=datetime('now')
391
513
  `);
392
514
 
393
- const txn = db.transaction(() => {
394
- for (const message of messages) {
515
+ // Insert in bounded sub-transactions so a large catch-up ingest never holds the SQLite
516
+ // write lock for the whole file. One whole-file db.transaction() was observed holding the
517
+ // lock ~20s on the primary, bloating the WAL (no checkpoint can run mid-transaction) →
518
+ // reader/read-pool requests time out → callers fall back to the main thread (the bursty
519
+ // freezes). Each chunk is its own transaction = its own lock acquire/release, so
520
+ // checkpoints and readers interleave and the WAL stays bounded. INSERT OR IGNORE
521
+ // (event_hash) keeps re-runs idempotent, so the file cursor is advanced once at the end —
522
+ // a crash mid-ingest simply re-processes from the old offset without duplicating rows.
523
+ const CHUNK = Math.max(25, Math.min(5000, Number(options.ingestChunkSize || process.env.CTM_TRANSCRIPT_INGEST_CHUNK || 250)));
524
+ const insertChunk = db.transaction((slice) => {
525
+ for (const message of slice) {
526
+ // A turn = one real user prompt + the assistant answer(s)/tool calls that
527
+ // follow it. messageFromEntry already maps tool-results and subagent prompts
528
+ // to role 'system', so role==='user' is a genuine prompt → start a new turn.
529
+ // Assistant/system rows inherit the current turn. (Read-side turn grouping in
530
+ // Phase 3 keys on this; it mirrors the renderer's user-message turn boundary.)
531
+ if (message.role === 'user') turn++;
532
+ const meta = message.metadata || {};
395
533
  const hash = eventHash(agentSessionId, message);
396
534
  const info = insertEvent.run(
397
535
  ctmSessionId,
@@ -405,7 +543,14 @@ function ingestJsonlFile(db, options = {}) {
405
543
  message.byteStart || 0,
406
544
  message.byteEnd || 0,
407
545
  hash,
408
- JSON.stringify(message.metadata || {})
546
+ JSON.stringify(meta),
547
+ turn,
548
+ estimateRowTokens(message.text),
549
+ String(meta.uuid || ''),
550
+ String(meta.parentUuid || ''),
551
+ String(meta.model || ''),
552
+ '', // usage_json: enriched later; empty keeps the row small
553
+ '' // content_json: rich content hydrated on demand from byte offsets
409
554
  );
410
555
  if (info.changes > 0) {
411
556
  if (insertEventFts) {
@@ -415,21 +560,25 @@ function ingestJsonlFile(db, options = {}) {
415
560
  seq++;
416
561
  }
417
562
  }
418
- upsertFile.run(
419
- filePath,
420
- agentSessionId,
421
- ctmSessionId,
422
- provider,
423
- String(stat.dev ?? ''),
424
- String(stat.ino ?? ''),
425
- stat.size,
426
- stat.mtimeMs,
427
- read.nextOffset,
428
- read.partialLine || '',
429
- PARSER_VERSION
430
- );
431
563
  });
432
- txn();
564
+ for (let i = 0; i < messages.length; i += CHUNK) {
565
+ insertChunk(messages.slice(i, i + CHUNK));
566
+ }
567
+ // Advance the file cursor only after all events are committed. INSERT OR IGNORE makes a
568
+ // re-run from the old offset idempotent, so a crash between chunks loses no data.
569
+ upsertFile.run(
570
+ filePath,
571
+ agentSessionId,
572
+ ctmSessionId,
573
+ provider,
574
+ String(stat.dev ?? ''),
575
+ String(stat.ino ?? ''),
576
+ stat.size,
577
+ stat.mtimeMs,
578
+ read.nextOffset,
579
+ read.partialLine || '',
580
+ PARSER_VERSION
581
+ );
433
582
 
434
583
  return {
435
584
  inserted,
@@ -509,7 +658,7 @@ function loadTranscriptMessages(db, ids = []) {
509
658
  if (!cleanIds.length) return [];
510
659
  const placeholders = cleanIds.map(() => '?').join(',');
511
660
  return db.prepare(`
512
- SELECT ctm_session_id, agent_session_id, provider, role, content AS text, timestamp, seq, jsonl_path
661
+ SELECT ctm_session_id, agent_session_id, provider, role, content AS text, timestamp, seq, jsonl_path, metadata_json
513
662
  FROM transcript_events
514
663
  WHERE ctm_session_id IN (${placeholders})
515
664
  OR agent_session_id IN (${placeholders})
@@ -518,14 +667,166 @@ function loadTranscriptMessages(db, ids = []) {
518
667
  timestamp,
519
668
  agent_session_id,
520
669
  seq
521
- `).all(...cleanIds, ...cleanIds).map(row => ({
522
- role: row.role,
523
- text: row.text,
524
- timestamp: row.timestamp || '',
525
- source: 'transcript-events',
526
- provider: row.provider || '',
527
- agentSessionId: row.agent_session_id,
528
- }));
670
+ `).all(...cleanIds, ...cleanIds)
671
+ .map(loadedTranscriptMessageFromRow)
672
+ .filter(Boolean);
673
+ }
674
+
675
+ // ---------------------------------------------------------------------------
676
+ // Phase 3: keyset/offset-paginated reads over transcript_events, so a session's
677
+ // conversation is served from per-message ROWS (≤ limit materialized) instead of
678
+ // JSON.parse-ing a multi-GB blob. Reproduces lib/message-pagination.js semantics
679
+ // (newest-first offset; chronological within a page) so the API/response shape and
680
+ // the renderer are unchanged. Page reads are O(limit) on the (ctm_session_id, seq)
681
+ // / (ctm_session_id, turn_ordinal) indexes — the 28-33s blob-parse freeze is gone.
682
+ // ---------------------------------------------------------------------------
683
+
684
+ function _resolveSessionRowIds(db, ids) {
685
+ const list = Array.isArray(ids) ? ids : [ids];
686
+ const seed = list.find(Boolean) || '';
687
+ return transcriptSourceIds(db, seed, list);
688
+ }
689
+
690
+ // True only when the row store can serve this session: the migrated columns exist
691
+ // AND at least one row is present for the id set. Callers fall back to the legacy
692
+ // blob/JSONL ladder otherwise, so a half-migrated session never breaks.
693
+ function sessionMessagesRowsAvailable(db, ids) {
694
+ try {
695
+ ensureTranscriptTables(db, { skipFtsBackfill: true });
696
+ const cols = db.prepare("PRAGMA table_info(transcript_events)").all().map(c => c.name);
697
+ if (!cols.includes('turn_ordinal')) return false;
698
+ const sids = _resolveSessionRowIds(db, ids);
699
+ if (!sids.length) return false;
700
+ const ph = sids.map(() => '?').join(',');
701
+ const row = db.prepare(`SELECT 1 FROM transcript_events WHERE ctm_session_id IN (${ph}) OR agent_session_id IN (${ph}) LIMIT 1`).get(...sids, ...sids);
702
+ if (!row) return false;
703
+ // FRESHNESS GATE: the rows must be CAUGHT UP to every source file. Stat the LIVE file
704
+ // (not transcript_files.size, which is itself only as fresh as the last ingest): if the
705
+ // file has grown past the durable ingest cursor (offset), the rows are missing its recent
706
+ // tail — serving them would show a stale conversation (caught a real case on a giant Codex
707
+ // session whose rows lagged the file by ~10h). When stale, return false so the caller uses
708
+ // the legacy blob/JSONL path, which reads the live file and is always complete. 64KB
709
+ // tolerates the trailing partial line; anything more is real lag.
710
+ const files = db.prepare(
711
+ `SELECT jsonl_path, offset FROM transcript_files WHERE ctm_session_id IN (${ph}) OR agent_session_id IN (${ph})`
712
+ ).all(...sids, ...sids);
713
+ if (!files.length) return false;
714
+ for (const f of files) {
715
+ let size = 0;
716
+ try { size = fs.statSync(f.jsonl_path).size; } catch { return false; } // file gone → use legacy path
717
+ if (size - Number(f.offset || 0) > 65536) return false;
718
+ }
719
+ return true;
720
+ } catch { return false; }
721
+ }
722
+
723
+ function _whereIds(sids) {
724
+ const ph = sids.map(() => '?').join(',');
725
+ return { clause: `(ctm_session_id IN (${ph}) OR agent_session_id IN (${ph}))`, args: [...sids, ...sids] };
726
+ }
727
+
728
+ const _ROW_ORDER = `CASE WHEN timestamp IS NULL OR timestamp = '' THEN 1 ELSE 0 END, timestamp, agent_session_id, seq`;
729
+ const _ROW_COLS = `ctm_session_id, agent_session_id, provider, role, content AS text, timestamp, seq, jsonl_path, byte_start, byte_end, metadata_json, turn_ordinal`;
730
+
731
+ function countSessionMessages(db, ids) {
732
+ const sids = _resolveSessionRowIds(db, ids);
733
+ if (!sids.length) return 0;
734
+ const w = _whereIds(sids);
735
+ return Number(db.prepare(`SELECT COUNT(*) AS c FROM transcript_events WHERE ${w.clause}`).get(...w.args)?.c || 0);
736
+ }
737
+
738
+ // Newest-first offset (offset 0 = the most recent `limit`), chronological within the
739
+ // page — matches lib/message-pagination.js paginateMessages().
740
+ function getSessionMessagesPage(db, ids, { offset = 0, limit = 200 } = {}) {
741
+ ensureTranscriptTables(db, { skipFtsBackfill: true });
742
+ const sids = _resolveSessionRowIds(db, ids);
743
+ if (!sids.length) return { messages: [], total: 0, has_more: false, next_offset: Number(offset) || 0 };
744
+ const w = _whereIds(sids);
745
+ const total = Number(db.prepare(`SELECT COUNT(*) AS c FROM transcript_events WHERE ${w.clause}`).get(...w.args)?.c || 0);
746
+ const lim = Math.max(1, Math.min(1000, Number(limit) || 200));
747
+ const off = Math.max(0, Number(offset) || 0);
748
+ const start = Math.max(0, total - off - lim);
749
+ const take = Math.max(0, Math.min(lim, total - off - start));
750
+ const raw = take === 0 ? [] : db.prepare(
751
+ `SELECT ${_ROW_COLS} FROM transcript_events WHERE ${w.clause} ORDER BY ${_ROW_ORDER} LIMIT ? OFFSET ?`
752
+ ).all(...w.args, take, start);
753
+ const messages = raw.map(loadedTranscriptMessageFromRow).filter(Boolean);
754
+ return { messages, total, has_more: start > 0, next_offset: off + take, _rows: raw };
755
+ }
756
+
757
+ // Newest-first by turn (offset 0 = the most recent `limit` turns). Returns all rows
758
+ // belonging to the page's turns in chronological order — matches paginateMessageTurns().
759
+ function getSessionTurnsPage(db, ids, { offset = 0, limit = 50 } = {}) {
760
+ ensureTranscriptTables(db, { skipFtsBackfill: true });
761
+ const sids = _resolveSessionRowIds(db, ids);
762
+ const empty = { messages: [], total: 0, has_more: false, next_offset: Number(offset) || 0, page_kind: 'turns', turn_count: 0 };
763
+ if (!sids.length) return empty;
764
+ const w = _whereIds(sids);
765
+ const turns = db.prepare(`SELECT DISTINCT turn_ordinal FROM transcript_events WHERE ${w.clause} ORDER BY turn_ordinal`).all(...w.args).map(r => Number(r.turn_ordinal));
766
+ const totalTurns = turns.length;
767
+ const lim = Math.max(1, Math.min(1000, Number(limit) || 50));
768
+ const off = Math.max(0, Number(offset) || 0);
769
+ const startIdx = Math.max(0, totalTurns - off - lim);
770
+ const endIdx = Math.max(startIdx, totalTurns - off);
771
+ const pageTurns = turns.slice(startIdx, endIdx);
772
+ if (!pageTurns.length) return { ...empty, total: totalTurns, has_more: startIdx > 0 };
773
+ const minT = pageTurns[0], maxT = pageTurns[pageTurns.length - 1];
774
+ const raw = db.prepare(
775
+ `SELECT ${_ROW_COLS} FROM transcript_events WHERE ${w.clause} AND turn_ordinal BETWEEN ? AND ? ORDER BY ${_ROW_ORDER}`
776
+ ).all(...w.args, minT, maxT);
777
+ const messages = raw.map(loadedTranscriptMessageFromRow).filter(Boolean);
778
+ return {
779
+ messages, total: totalTurns, has_more: startIdx > 0, next_offset: off + pageTurns.length,
780
+ page_kind: 'turns', turn_count: pageTurns.length, first_turn_index: minT, last_turn_index: maxT, _rows: raw,
781
+ };
782
+ }
783
+
784
+ // Phase 4 (existing-user backfill): enrich rows that were ingested BEFORE Phase 2 and
785
+ // therefore have turn_ordinal=0 / token_count=0. Walks one agent session's rows in order
786
+ // and assigns turn_ordinal (++ on each real user prompt) + a token estimate, in bounded
787
+ // UPDATE transactions. Idempotent: only runs for a session that has user rows but no turns
788
+ // yet (after it runs, MAX(turn_ordinal) > 0, so it never repeats). Returns rows updated.
789
+ function enrichTranscriptRowsForSession(db, agentSessionId) {
790
+ ensureTranscriptTables(db, { skipFtsBackfill: true });
791
+ const aid = String(agentSessionId || '').trim();
792
+ if (!aid) return 0;
793
+ const stat = db.prepare(`SELECT MAX(turn_ordinal) AS maxTurn,
794
+ SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END) AS users
795
+ FROM transcript_events WHERE agent_session_id = ?`).get(aid);
796
+ if (!stat || Number(stat.maxTurn || 0) > 0 || Number(stat.users || 0) === 0) return 0;
797
+ const rows = db.prepare('SELECT id, role, content FROM transcript_events WHERE agent_session_id = ? ORDER BY seq').all(aid);
798
+ const upd = db.prepare('UPDATE transcript_events SET turn_ordinal = ?, token_count = ? WHERE id = ?');
799
+ let turn = 0, n = 0;
800
+ const tx = db.transaction((batch) => {
801
+ for (const r of batch) {
802
+ if (r.role === 'user') turn++;
803
+ upd.run(turn, estimateRowTokens(r.content), r.id);
804
+ n++;
805
+ }
806
+ });
807
+ const CHUNK = 500;
808
+ for (let i = 0; i < rows.length; i += CHUNK) tx(rows.slice(i, i + CHUNK));
809
+ return n;
810
+ }
811
+
812
+ // Find agent sessions whose rows still need enrichment (have user rows, no turns yet),
813
+ // bounded — for a background sweep that migrates existing users without a big-bang.
814
+ function findUnenrichedSessions(db, limit = 25) {
815
+ ensureTranscriptTables(db, { skipFtsBackfill: true });
816
+ try {
817
+ return db.prepare(`SELECT agent_session_id FROM transcript_events
818
+ GROUP BY agent_session_id
819
+ HAVING MAX(turn_ordinal) = 0 AND SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END) > 0
820
+ LIMIT ?`).all(Math.max(1, Math.min(500, Number(limit) || 25))).map(r => r.agent_session_id);
821
+ } catch { return []; }
822
+ }
823
+
824
+ // Session token total from per-row estimates (no full-array rebuild).
825
+ function getSessionTokenSum(db, ids) {
826
+ const sids = _resolveSessionRowIds(db, ids);
827
+ if (!sids.length) return 0;
828
+ const w = _whereIds(sids);
829
+ return Number(db.prepare(`SELECT COALESCE(SUM(token_count), 0) AS t FROM transcript_events WHERE ${w.clause}`).get(...w.args)?.t || 0);
529
830
  }
530
831
 
531
832
  function mergeMessageLists(baseMessages, overlayMessages) {
@@ -543,13 +844,7 @@ function mergeMessageLists(baseMessages, overlayMessages) {
543
844
  };
544
845
  for (const message of Array.isArray(baseMessages) ? baseMessages : []) add(message);
545
846
  for (const message of Array.isArray(overlayMessages) ? overlayMessages : []) add(message);
546
- merged.sort((a, b) => {
547
- const at = String(a.timestamp || '');
548
- const bt = String(b.timestamp || '');
549
- if (at !== bt) return at.localeCompare(bt);
550
- return String(a.role || '').localeCompare(String(b.role || ''));
551
- });
552
- return merged;
847
+ return sortTimelineMessages(merged);
553
848
  }
554
849
 
555
850
  function catchUpTranscriptFiles(db, options = {}) {
@@ -641,7 +936,17 @@ module.exports = {
641
936
  markTranscriptFileError,
642
937
  mergeMessageLists,
643
938
  messageFromEntry,
939
+ isSubagentTranscriptPath,
644
940
  normalizeProvider,
645
941
  sourceIdFromPath,
646
942
  transcriptSourceIds,
943
+ // Phase 3 row-store readers
944
+ sessionMessagesRowsAvailable,
945
+ countSessionMessages,
946
+ getSessionMessagesPage,
947
+ getSessionTurnsPage,
948
+ getSessionTokenSum,
949
+ // Phase 4 existing-user enrichment backfill
950
+ enrichTranscriptRowsForSession,
951
+ findUnenrichedSessions,
647
952
  };
@@ -94,11 +94,59 @@ function trustedForwardedHost(req) {
94
94
  );
95
95
  }
96
96
 
97
+ function cleanIpHeaderValue(value) {
98
+ let raw = stripHeaderQuotes(firstHeaderValue(value));
99
+ if (!raw || raw.toLowerCase() === 'unknown') return '';
100
+ if (raw.startsWith('[')) {
101
+ const end = raw.indexOf(']');
102
+ raw = end >= 0 ? raw.slice(1, end) : stripIpv6Brackets(raw);
103
+ } else if (net.isIP(raw) === 0) {
104
+ const colonCount = (raw.match(/:/g) || []).length;
105
+ if (colonCount === 1 && /^\d+\.\d+\.\d+\.\d+:\d+$/.test(raw)) {
106
+ raw = raw.replace(/:\d+$/, '');
107
+ }
108
+ }
109
+ raw = stripIpv6Brackets(raw);
110
+ return net.isIP(raw) ? raw : '';
111
+ }
112
+
113
+ function forwardedClientIp(req) {
114
+ const forwarded = parseForwardedHeader(req?.headers?.forwarded || '');
115
+ for (const value of [
116
+ req?.headers?.['x-forwarded-for'],
117
+ req?.headers?.['x-real-ip'],
118
+ req?.headers?.['cf-connecting-ip'],
119
+ forwarded.for,
120
+ ]) {
121
+ const ip = cleanIpHeaderValue(value);
122
+ if (ip) return ip;
123
+ }
124
+ return '';
125
+ }
126
+
127
+ function requestClientIp(req) {
128
+ const direct = stripIpv6Brackets(req?.socket?.remoteAddress || '');
129
+ if (!isLoopbackAddress(direct)) return direct;
130
+ const tunnelHost = trustedForwardedHost(req) || cleanHostHeaderValue(req?.headers?.host);
131
+ if (!isManagedHttpsTunnelHost(tunnelHost)) return direct;
132
+ const forwardedIp = forwardedClientIp(req);
133
+ if (forwardedIp && !isLoopbackAddress(forwardedIp)) return forwardedIp;
134
+ const tunnelName = hostHeaderName(tunnelHost);
135
+ return tunnelName ? `tunnel:${tunnelName}` : direct;
136
+ }
137
+
97
138
  function isLoopbackAddress(address) {
98
139
  const h = stripIpv6Brackets(address).toLowerCase();
99
140
  return LOOPBACK_HOSTS.has(h) || h === '::ffff:127.0.0.1';
100
141
  }
101
142
 
143
+ function primaryProcessIpLockoutExemptions(primaryHost) {
144
+ const out = new Set(['127.0.0.1', '::1']);
145
+ const host = stripIpv6Brackets(primaryHost).trim();
146
+ if (host && !isLoopbackHost(host)) out.add(host);
147
+ return Array.from(out);
148
+ }
149
+
102
150
  function hasNonLoopbackBrowserOrigin(req) {
103
151
  for (const value of [req?.headers?.origin, req?.headers?.referer, req?.headers?.referrer]) {
104
152
  const raw = firstHeaderValue(value);
@@ -112,12 +160,45 @@ function hasNonLoopbackBrowserOrigin(req) {
112
160
  return false;
113
161
  }
114
162
 
163
+ // Proxy/tunnel forwarding headers a genuinely local browser or CLI never sends.
164
+ // A devtunnel (or any reverse proxy) forwards remote traffic to CTM over the
165
+ // loopback interface, so socket.remoteAddress alone cannot tell local apart from
166
+ // tunneled. Azure Dev Tunnels and every standard proxy stamp X-Forwarded-* /
167
+ // Forwarded on the request; their presence on a loopback peer means the request
168
+ // originated off-box and must NOT be trusted as loopback admin.
169
+ const PROXY_FORWARDING_HEADERS = Object.freeze([
170
+ 'x-forwarded-for',
171
+ 'x-forwarded-host',
172
+ 'x-forwarded-proto',
173
+ 'forwarded',
174
+ 'x-real-ip',
175
+ 'x-ms-original-host',
176
+ 'x-original-host',
177
+ 'cf-connecting-ip',
178
+ ]);
179
+
180
+ function hasProxyForwardingMarkers(req) {
181
+ const headers = req?.headers || {};
182
+ for (const name of PROXY_FORWARDING_HEADERS) {
183
+ const value = headers[name];
184
+ if (value !== undefined && value !== null && String(value).trim() !== '') return true;
185
+ }
186
+ return false;
187
+ }
188
+
189
+ // SECURITY INVARIANT: loopback === fully-trusted local admin, but ONLY for
190
+ // requests that are provably on-box. Trust requires ALL of: (1) a loopback socket
191
+ // peer, (2) no proxy/tunnel forwarding markers, (3) no non-loopback browser
192
+ // Origin/Referer, and (4) a loopback (or absent) Host. A managed tunnel forwards
193
+ // remote clients to 127.0.0.1, so any signal that the request originated off-box
194
+ // means it must authenticate with a device token like any other remote client.
115
195
  function isLoopbackRequest(req) {
116
196
  if (!isLoopbackAddress(req?.socket?.remoteAddress || '')) return false;
197
+ if (hasProxyForwardingMarkers(req)) return false;
117
198
  const forwardedHost = trustedForwardedHost(req);
118
199
  if (forwardedHost && !isLoopbackHost(hostHeaderName(forwardedHost))) return false;
119
- const host = hostHeaderName(req?.headers?.host || '');
120
200
  if (hasNonLoopbackBrowserOrigin(req)) return false;
201
+ const host = hostHeaderName(req?.headers?.host || '');
121
202
  return !host || isLoopbackHost(host);
122
203
  }
123
204
 
@@ -296,6 +377,8 @@ module.exports = {
296
377
  normalizeBindHost,
297
378
  originAllowed,
298
379
  parseAllowedOrigins,
380
+ primaryProcessIpLockoutExemptions,
381
+ requestClientIp,
299
382
  requestBrowserOrigin,
300
383
  requestOrigin,
301
384
  requestProtocol,