create-walle 0.9.11 → 0.9.13

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 (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
@@ -102,6 +102,38 @@ function getCodexThreadTitleById(threadId, homeDir = process.env.HOME) {
102
102
  return title ? title.slice(0, 80) : null;
103
103
  }
104
104
 
105
+ function normalizeTitleText(value) {
106
+ return String(value || '').replace(/\s+/g, ' ').trim();
107
+ }
108
+
109
+ function isGenericCodingSessionLabel(label, agentType = 'codex') {
110
+ const text = normalizeTitleText(label);
111
+ if (!text) return true;
112
+ const lower = text.toLowerCase();
113
+ if (agentType === 'codex') {
114
+ return lower === 'codex'
115
+ || lower === 'codex session'
116
+ || lower === 'new codex session'
117
+ || lower === 'coding...'
118
+ || /^codex:\s*(~|\/|\.\.?\/|$)/i.test(text);
119
+ }
120
+ return lower === 'coding...'
121
+ || lower === 'new session'
122
+ || lower === 'session';
123
+ }
124
+
125
+ function shouldApplyCodexAutoTitle({ session, existingTitle } = {}) {
126
+ if (existingTitle && existingTitle.userRenamed) return false;
127
+ for (const candidate of [session?.label, existingTitle?.title]) {
128
+ const text = normalizeTitleText(candidate);
129
+ if (text && !/^Codex:\s*/i.test(text) && !isGenericCodingSessionLabel(text, 'codex')) return false;
130
+ }
131
+ const title = normalizeTitleText(existingTitle?.title || session?.label || '');
132
+ if (!title) return true;
133
+ if (/^Codex:\s*/i.test(title) && !isGenericCodingSessionLabel(title, 'codex')) return true;
134
+ return isGenericCodingSessionLabel(title, 'codex');
135
+ }
136
+
105
137
  function resolveModelToRegistry(modelName) {
106
138
  if (!modelName) return null;
107
139
  let provider = null;
@@ -182,10 +214,24 @@ function createCodingAgentModelSync({ sessions, dbModule, broadcastSessionList }
182
214
  const existingTitle = dbModule.getSessionTitleNew(session.id);
183
215
  if (existingTitle && existingTitle.userRenamed) {
184
216
  session._codexTitleSynced = true;
217
+ } else if (!shouldApplyCodexAutoTitle({ session, existingTitle })) {
218
+ const liveLabel = normalizeTitleText(session.label);
219
+ if (liveLabel
220
+ && !isGenericCodingSessionLabel(liveLabel, 'codex')
221
+ && (!existingTitle || normalizeTitleText(existingTitle.title) !== liveLabel)) {
222
+ dbModule.setSessionTitleNew(session.id, liveLabel, false);
223
+ changed = true;
224
+ }
225
+ session._codexTitleSynced = true;
185
226
  } else {
227
+ // Title sync must require the exact Codex thread id. Matching by cwd
228
+ // and spawn time is useful for one-shot link recovery, but unsafe for
229
+ // periodic title refresh: multiple active Codex sessions often share
230
+ // the same repo and Codex can persist names after CTM has already
231
+ // rendered the tab.
186
232
  const codexTitle = session._claudeSessionId
187
233
  ? getCodexThreadTitleById(session._claudeSessionId)
188
- : getCodexThreadTitle(session.cwd, session._ctmOriginalCreatedAt || session.createdAt);
234
+ : null;
189
235
  if (codexTitle) {
190
236
  session.label = `Codex: ${codexTitle}`;
191
237
  session._codexTitleSynced = true;
@@ -214,6 +260,8 @@ module.exports = {
214
260
  getCodexThreadTitleById,
215
261
  getCodexLatestModel,
216
262
  getCodexThreadTitle,
263
+ isGenericCodingSessionLabel,
217
264
  resolveModelToRegistry,
265
+ shouldApplyCodexAutoTitle,
218
266
  CC_ALIAS_MAP,
219
267
  };
@@ -0,0 +1,421 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+
5
+ const WAITING_TTL_MS = 30 * 60 * 1000;
6
+
7
+ function nowMs() {
8
+ return Date.now();
9
+ }
10
+
11
+ function toMs(value) {
12
+ const n = Number(value);
13
+ return Number.isFinite(n) && n > 0 ? n : nowMs();
14
+ }
15
+
16
+ function normalizeStatus(status) {
17
+ if (status === 'busy') return 'running';
18
+ if (status === 'waiting_input') return 'waiting';
19
+ if (status === 'running' || status === 'waiting' || status === 'idle' || status === 'exited') return status;
20
+ return 'unknown';
21
+ }
22
+
23
+ function statusFromServerMessage(msg) {
24
+ if (!msg || typeof msg !== 'object') return null;
25
+
26
+ if (msg.type === 'stream-status') {
27
+ return {
28
+ sessionId: msg.sessionId,
29
+ ctmSessionId: msg.ctmSessionId || null,
30
+ agentSessionId: msg.sessionId || null,
31
+ status: msg.newStatus || msg.status,
32
+ source: 'stream',
33
+ reason: msg.reason || 'stream-status',
34
+ timestamp: msg.timestamp,
35
+ lastActivity: msg.lastActivity,
36
+ };
37
+ }
38
+
39
+ if (msg.type === 'session.status') {
40
+ const source = msg.source || 'status';
41
+ const status = msg.working ? 'running' : source === 'terminal-prompt' ? 'waiting' : 'idle';
42
+ return {
43
+ sessionId: msg.id,
44
+ ctmSessionId: msg.id,
45
+ status,
46
+ source: `authoritative:${source}`,
47
+ reason: source,
48
+ timestamp: msg.timestamp,
49
+ };
50
+ }
51
+
52
+ if (msg.type === 'waiting-for-input') {
53
+ return {
54
+ sessionId: msg.id,
55
+ ctmSessionId: msg.id,
56
+ status: 'waiting',
57
+ source: 'waiting-for-input',
58
+ reason: msg.reason || 'input',
59
+ timestamp: msg.timestamp,
60
+ };
61
+ }
62
+
63
+ if (msg.type === 'session-resumed') {
64
+ return {
65
+ sessionId: msg.id,
66
+ ctmSessionId: msg.id,
67
+ status: 'running',
68
+ source: 'session-resumed',
69
+ reason: 'terminal-output',
70
+ timestamp: msg.timestamp,
71
+ };
72
+ }
73
+
74
+ if (msg.type === 'approval-decision') {
75
+ return {
76
+ sessionId: msg.sessionId,
77
+ ctmSessionId: msg.sessionId,
78
+ status: msg.decision === 'approved' ? 'running' : 'waiting',
79
+ source: 'approval',
80
+ reason: msg.decision || 'decision',
81
+ timestamp: msg.timestamp,
82
+ decisionId: msg.decisionId,
83
+ };
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ class SessionCapture extends EventEmitter {
90
+ constructor({ sessionStream } = {}) {
91
+ super();
92
+ this._sessionStream = null;
93
+ this._records = new Map();
94
+ this._ctmToAgent = new Map();
95
+ this._agentToCtm = new Map();
96
+ this._unsubs = [];
97
+ if (sessionStream) this.attachSessionStream(sessionStream);
98
+ }
99
+
100
+ attachSessionStream(sessionStream) {
101
+ this.detach();
102
+ this._sessionStream = sessionStream || null;
103
+ if (!sessionStream) return;
104
+
105
+ const onLink = (link) => {
106
+ this.linkIdentity(link.ctmSessionId, link.agentSessionId);
107
+ };
108
+ const onUnlink = (link) => {
109
+ this.ingestStatus({
110
+ sessionId: link.agentSessionId,
111
+ ctmSessionId: link.ctmSessionId,
112
+ agentSessionId: link.agentSessionId,
113
+ status: 'exited',
114
+ source: 'stream',
115
+ reason: 'session-exit',
116
+ timestamp: nowMs(),
117
+ });
118
+ };
119
+ const onStatus = (statusEvt) => {
120
+ this.ingestStreamStatus(statusEvt);
121
+ };
122
+ const onEvent = (evt) => {
123
+ this.emit('capture-event', evt);
124
+ };
125
+
126
+ sessionStream.on('link', onLink);
127
+ sessionStream.on('unlink', onUnlink);
128
+ sessionStream.on('status', onStatus);
129
+ sessionStream.on('event', onEvent);
130
+ sessionStream.on('event:update', onEvent);
131
+ this._unsubs.push(() => sessionStream.off('link', onLink));
132
+ this._unsubs.push(() => sessionStream.off('unlink', onUnlink));
133
+ this._unsubs.push(() => sessionStream.off('status', onStatus));
134
+ this._unsubs.push(() => sessionStream.off('event', onEvent));
135
+ this._unsubs.push(() => sessionStream.off('event:update', onEvent));
136
+ }
137
+
138
+ detach() {
139
+ for (const unsub of this._unsubs.splice(0)) {
140
+ try { unsub(); } catch {}
141
+ }
142
+ this._sessionStream = null;
143
+ }
144
+
145
+ linkIdentity(ctmSessionId, agentSessionId) {
146
+ if (!agentSessionId && !ctmSessionId) return;
147
+ if (ctmSessionId && agentSessionId) {
148
+ this._ctmToAgent.set(ctmSessionId, agentSessionId);
149
+ this._agentToCtm.set(agentSessionId, ctmSessionId);
150
+ this._mergeRecord(agentSessionId, ctmSessionId);
151
+ }
152
+ const record = this._recordFor(ctmSessionId || agentSessionId);
153
+ if (ctmSessionId) record.ctmSessionId = ctmSessionId;
154
+ if (agentSessionId) record.agentSessionId = agentSessionId;
155
+ }
156
+
157
+ resolveAgentSessionId(sessionId) {
158
+ return this._ctmToAgent.get(sessionId) || sessionId;
159
+ }
160
+
161
+ getRecentEvents(sessionId, limit = 50) {
162
+ if (!this._sessionStream) return [];
163
+ return this._sessionStream.getRecentEvents(this.resolveAgentSessionId(sessionId), limit);
164
+ }
165
+
166
+ getSummary(sessionId, turns = 3) {
167
+ if (!this._sessionStream) return null;
168
+ const agentId = this.resolveAgentSessionId(sessionId);
169
+ const summary = this._sessionStream.getSummary(agentId, turns);
170
+ if (!summary) return null;
171
+ const projected = this.getStatus(agentId, summary.status);
172
+ return {
173
+ ...summary,
174
+ status: projected.status,
175
+ captureStatus: projected.status,
176
+ statusEvidence: projected.evidence,
177
+ };
178
+ }
179
+
180
+ subscribe(sessionId, cb) {
181
+ if (!this._sessionStream) return () => {};
182
+ return this._sessionStream.subscribe(this.resolveAgentSessionId(sessionId), cb);
183
+ }
184
+
185
+ subscribeAll(cb) {
186
+ if (!this._sessionStream) return () => {};
187
+ return this._sessionStream.subscribeAll(cb);
188
+ }
189
+
190
+ linkSession(ctmSessionId, agentSessionId, jsonlPath) {
191
+ this.linkIdentity(ctmSessionId, agentSessionId);
192
+ if (this._sessionStream && typeof this._sessionStream.linkSession === 'function') {
193
+ this._sessionStream.linkSession(ctmSessionId, agentSessionId, jsonlPath);
194
+ }
195
+ }
196
+
197
+ removeSession(agentSessionId) {
198
+ const ctmSessionId = this._agentToCtm.get(agentSessionId) || null;
199
+ this.ingestStatus({
200
+ sessionId: agentSessionId,
201
+ ctmSessionId,
202
+ agentSessionId,
203
+ status: 'exited',
204
+ source: 'capture',
205
+ reason: 'session-exit',
206
+ timestamp: nowMs(),
207
+ });
208
+ if (this._sessionStream && typeof this._sessionStream.removeSession === 'function') {
209
+ this._sessionStream.removeSession(agentSessionId);
210
+ }
211
+ }
212
+
213
+ feedPtyActivity(ctmSessionId) {
214
+ this.ingestStatus({
215
+ sessionId: ctmSessionId,
216
+ ctmSessionId,
217
+ status: 'running',
218
+ source: 'pty-activity',
219
+ reason: 'pty-activity',
220
+ timestamp: nowMs(),
221
+ });
222
+ if (this._sessionStream && typeof this._sessionStream.feedPtyActivity === 'function') {
223
+ this._sessionStream.feedPtyActivity(ctmSessionId);
224
+ }
225
+ }
226
+
227
+ ingestServerMessage(msg) {
228
+ const evidence = statusFromServerMessage(msg);
229
+ if (!evidence) return null;
230
+ const projected = this.ingestStatus(evidence);
231
+ this.emit('capture-event', {
232
+ schemaVersion: 1,
233
+ eventId: `server:${msg.type}:${evidence.sessionId || evidence.ctmSessionId}:${evidence.timestamp || nowMs()}`,
234
+ captureKind: 'lifecycle',
235
+ type: msg.type,
236
+ source: evidence.source,
237
+ sessionId: evidence.agentSessionId || evidence.sessionId,
238
+ ctmSessionId: evidence.ctmSessionId || evidence.sessionId,
239
+ agentSessionId: evidence.agentSessionId || null,
240
+ timestamp: evidence.timestamp || nowMs(),
241
+ status: projected.status,
242
+ data: msg,
243
+ });
244
+ return projected;
245
+ }
246
+
247
+ ingestStreamStatus(statusEvt = {}) {
248
+ return this.ingestStatus({
249
+ sessionId: statusEvt.sessionId,
250
+ ctmSessionId: statusEvt.ctmSessionId || null,
251
+ agentSessionId: statusEvt.sessionId || null,
252
+ status: statusEvt.newStatus || statusEvt.status,
253
+ source: 'stream',
254
+ reason: statusEvt.reason || 'stream-status',
255
+ timestamp: statusEvt.timestamp,
256
+ lastActivity: statusEvt.lastActivity,
257
+ eventCount: statusEvt.eventCount,
258
+ });
259
+ }
260
+
261
+ ingestStatus(input = {}) {
262
+ const status = normalizeStatus(input.status);
263
+ if (status === 'unknown') return this.getStatus(input.sessionId || input.ctmSessionId || input.agentSessionId, status);
264
+ this.linkIdentity(input.ctmSessionId, input.agentSessionId || input.sessionId);
265
+ const record = this._recordFor(input.ctmSessionId || input.sessionId || input.agentSessionId);
266
+ const timestamp = toMs(input.timestamp);
267
+ if (input.ctmSessionId) record.ctmSessionId = input.ctmSessionId;
268
+ if (input.agentSessionId) record.agentSessionId = input.agentSessionId;
269
+ if (!record.agentSessionId && input.sessionId && this._agentToCtm.has(input.sessionId)) record.agentSessionId = input.sessionId;
270
+ if (!record.ctmSessionId && input.sessionId && this._ctmToAgent.has(input.sessionId)) record.ctmSessionId = input.sessionId;
271
+
272
+ if (status === 'running' || status === 'exited') {
273
+ delete record.evidence['waiting-for-input'];
274
+ delete record.evidence.approval;
275
+ }
276
+ if (status === 'exited') {
277
+ record.evidence = {};
278
+ }
279
+
280
+ record.evidence[input.source || 'capture'] = {
281
+ status,
282
+ source: input.source || 'capture',
283
+ reason: input.reason || '',
284
+ timestamp,
285
+ decisionId: input.decisionId,
286
+ };
287
+ record.lastUpdated = Math.max(record.lastUpdated || 0, timestamp);
288
+ if (input.lastActivity) record.lastActivity = Math.max(record.lastActivity || 0, input.lastActivity);
289
+ else record.lastActivity = Math.max(record.lastActivity || 0, timestamp);
290
+ if (input.eventCount != null) record.eventCount = input.eventCount;
291
+
292
+ const projected = this._project(record);
293
+ this.emit('status', projected);
294
+ return projected;
295
+ }
296
+
297
+ getStatus(sessionId, fallbackStatus = 'unknown') {
298
+ const record = this._records.get(this._canonicalId(sessionId));
299
+ if (!record) {
300
+ return {
301
+ sessionId,
302
+ ctmSessionId: this._agentToCtm.get(sessionId) || sessionId || null,
303
+ agentSessionId: this._ctmToAgent.get(sessionId) || sessionId || null,
304
+ status: normalizeStatus(fallbackStatus),
305
+ evidence: [],
306
+ };
307
+ }
308
+ return this._project(record, fallbackStatus);
309
+ }
310
+
311
+ getAllStatuses() {
312
+ const results = [];
313
+ const seen = new Set();
314
+ if (this._sessionStream && typeof this._sessionStream.getAllStatuses === 'function') {
315
+ for (const st of this._sessionStream.getAllStatuses()) {
316
+ this.ingestStreamStatus({ ...st, timestamp: nowMs(), reason: 'snapshot' });
317
+ const projected = this.getStatus(st.sessionId, st.status);
318
+ const row = {
319
+ ...st,
320
+ status: projected.status,
321
+ captureStatus: projected.status,
322
+ statusEvidence: projected.evidence,
323
+ };
324
+ results.push(row);
325
+ seen.add(this._canonicalId(st.sessionId));
326
+ }
327
+ }
328
+
329
+ for (const [id, record] of this._records) {
330
+ if (seen.has(id)) continue;
331
+ const projected = this._project(record);
332
+ if (projected.status === 'unknown') continue;
333
+ results.push({
334
+ sessionId: record.agentSessionId || id,
335
+ ctmSessionId: record.ctmSessionId || null,
336
+ status: projected.status,
337
+ captureStatus: projected.status,
338
+ lastActivity: record.lastActivity || record.lastUpdated || 0,
339
+ eventCount: record.eventCount || 0,
340
+ statusEvidence: projected.evidence,
341
+ });
342
+ }
343
+ return results;
344
+ }
345
+
346
+ _canonicalId(sessionId) {
347
+ if (!sessionId) return '';
348
+ return this._agentToCtm.get(sessionId) || sessionId;
349
+ }
350
+
351
+ _recordFor(sessionId) {
352
+ const id = this._canonicalId(sessionId);
353
+ let record = this._records.get(id);
354
+ if (!record) {
355
+ record = {
356
+ sessionId: id,
357
+ ctmSessionId: this._agentToCtm.get(sessionId) || sessionId || null,
358
+ agentSessionId: this._ctmToAgent.get(sessionId) || null,
359
+ evidence: {},
360
+ lastUpdated: 0,
361
+ lastActivity: 0,
362
+ eventCount: 0,
363
+ };
364
+ this._records.set(id, record);
365
+ }
366
+ return record;
367
+ }
368
+
369
+ _mergeRecord(fromId, toId) {
370
+ if (!fromId || !toId || fromId === toId) return;
371
+ const fromKey = this._records.has(fromId) ? fromId : this._canonicalId(fromId);
372
+ const toKey = this._canonicalId(toId);
373
+ if (fromKey === toKey) return;
374
+ const from = this._records.get(fromKey);
375
+ if (!from) return;
376
+ const to = this._recordFor(toKey);
377
+ to.ctmSessionId = to.ctmSessionId || toKey;
378
+ to.agentSessionId = to.agentSessionId || from.agentSessionId || fromId;
379
+ to.lastUpdated = Math.max(to.lastUpdated || 0, from.lastUpdated || 0);
380
+ to.lastActivity = Math.max(to.lastActivity || 0, from.lastActivity || 0);
381
+ to.eventCount = Math.max(to.eventCount || 0, from.eventCount || 0);
382
+ for (const [source, ev] of Object.entries(from.evidence || {})) {
383
+ if (!to.evidence[source] || (ev.timestamp || 0) >= (to.evidence[source].timestamp || 0)) {
384
+ to.evidence[source] = ev;
385
+ }
386
+ }
387
+ this._records.delete(fromKey);
388
+ }
389
+
390
+ _project(record, fallbackStatus = 'unknown') {
391
+ const evidence = Object.values(record.evidence || {})
392
+ .filter(Boolean)
393
+ .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
394
+ const latest = evidence[0] || null;
395
+ const waiting = evidence.find((ev) => ev.status === 'waiting' && (nowMs() - (ev.timestamp || 0)) < WAITING_TTL_MS);
396
+ const exited = evidence.find((ev) => ev.status === 'exited');
397
+ let status = normalizeStatus(fallbackStatus);
398
+ if (latest) status = latest.status;
399
+ if (waiting && (!latest || waiting.timestamp >= latest.timestamp || latest.status !== 'running')) status = 'waiting';
400
+ if (exited && (!latest || exited.timestamp >= latest.timestamp)) status = 'exited';
401
+ return {
402
+ sessionId: record.agentSessionId || record.sessionId,
403
+ ctmSessionId: record.ctmSessionId || null,
404
+ agentSessionId: record.agentSessionId || null,
405
+ status: normalizeStatus(status),
406
+ lastActivity: record.lastActivity || record.lastUpdated || 0,
407
+ evidence: evidence.map((ev) => ({
408
+ source: ev.source,
409
+ status: ev.status,
410
+ reason: ev.reason,
411
+ timestamp: ev.timestamp,
412
+ })),
413
+ };
414
+ }
415
+ }
416
+
417
+ module.exports = {
418
+ SessionCapture,
419
+ normalizeStatus,
420
+ statusFromServerMessage,
421
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { StringDecoder } = require('string_decoder');
5
6
  const { getResumeSpec: getAgentResumeSpec, normalizeAgentType } = require('./agent-capabilities');
6
7
 
7
8
  const DEFAULT_CODEX_LOOKBACK_SEC = 10;
@@ -88,6 +89,24 @@ function getCodexThreadById(threadId, homeDir = process.env.HOME) {
88
89
  }
89
90
  }
90
91
 
92
+ function getCodexThreadResumeCwd(threadId, {
93
+ fallbackCwd = '',
94
+ homeDir = process.env.HOME,
95
+ } = {}) {
96
+ if (!threadId) return fallbackCwd || '';
97
+
98
+ const thread = getCodexThreadById(threadId, homeDir);
99
+ if (thread?.cwd) return thread.cwd;
100
+
101
+ const files = findCodexSessionFiles(threadId, path.join(homeDir, '.codex', 'sessions'));
102
+ for (const filePath of files) {
103
+ const row = _readCodexRolloutMetadata(filePath);
104
+ if (row?.cwd) return row.cwd;
105
+ }
106
+
107
+ return fallbackCwd || '';
108
+ }
109
+
91
110
  function readFilePrefix(filePath, maxBytes = 64 * 1024) {
92
111
  let fd = null;
93
112
  try {
@@ -344,24 +363,123 @@ function codexMessageFromEntry(entry) {
344
363
  return null;
345
364
  }
346
365
 
347
- function parseCodexJsonlIntoMessages(content, messages) {
348
- const lines = String(content || '').split('\n').filter(Boolean);
349
- const seenUsers = new Set();
366
+ function codexSessionMetaFromEntry(entry) {
367
+ if (!entry || entry.type !== 'session_meta') return null;
368
+ const payload = entry.payload || {};
369
+ return {
370
+ id: payload.id || '',
371
+ cwd: payload.cwd || '',
372
+ model: payload.model || payload.model_id || '',
373
+ git_branch: payload.git_branch || '',
374
+ timestamp: payload.timestamp || entry.timestamp || '',
375
+ };
376
+ }
377
+
378
+ function appendCodexEntryMessage(entry, messages, seenUsers) {
379
+ if (!entry || typeof entry !== 'object') return false;
380
+ const msg = codexMessageFromEntry(entry);
381
+ if (!msg) return false;
382
+ if (msg.role === 'user') {
383
+ const key = codexUserKey(msg.text);
384
+ if (!key || seenUsers.has(key)) return false;
385
+ seenUsers.add(key);
386
+ }
387
+ messages.push(msg);
388
+ return true;
389
+ }
390
+
391
+ function parseCodexJsonlLine(line, messages, state) {
392
+ if (!line || !line.trim()) return false;
393
+ let entry;
394
+ try { entry = JSON.parse(line); } catch { return false; }
395
+ if (!entry || typeof entry !== 'object') return false;
396
+ if (!state.sessionMeta) {
397
+ const meta = codexSessionMetaFromEntry(entry);
398
+ if (meta?.id) state.sessionMeta = meta;
399
+ }
400
+ return appendCodexEntryMessage(entry, messages, state.seenUsers);
401
+ }
350
402
 
351
- for (const line of lines) {
352
- let entry;
353
- try { entry = JSON.parse(line); } catch { continue; }
354
- if (!entry || typeof entry !== 'object') continue;
355
-
356
- const msg = codexMessageFromEntry(entry);
357
- if (!msg) continue;
358
- if (msg.role === 'user') {
359
- const key = codexUserKey(msg.text);
360
- if (!key || seenUsers.has(key)) continue;
361
- seenUsers.add(key);
403
+ function parseCodexJsonlIntoMessages(content, messages, options = {}) {
404
+ const raw = String(content || '');
405
+ const state = {
406
+ seenUsers: options.seenUsers || new Set(),
407
+ sessionMeta: null,
408
+ };
409
+ const before = messages.length;
410
+ let linesRead = 0;
411
+
412
+ let start = 0;
413
+ while (start <= raw.length) {
414
+ let end = raw.indexOf('\n', start);
415
+ if (end === -1) end = raw.length;
416
+ const line = raw.slice(start, end);
417
+ if (line) {
418
+ linesRead++;
419
+ parseCodexJsonlLine(line, messages, state);
420
+ }
421
+ if (end === raw.length) break;
422
+ start = end + 1;
423
+ }
424
+
425
+ return {
426
+ bytesRead: Buffer.byteLength(raw),
427
+ linesRead,
428
+ messagesAdded: messages.length - before,
429
+ sessionMeta: state.sessionMeta,
430
+ };
431
+ }
432
+
433
+ function parseCodexJsonlFileIntoMessages(filePath, messages, options = {}) {
434
+ const state = {
435
+ seenUsers: options.seenUsers || new Set(),
436
+ sessionMeta: null,
437
+ };
438
+ const before = messages.length;
439
+ const chunkSize = options.chunkSize || 1024 * 1024;
440
+ const buf = Buffer.alloc(chunkSize);
441
+ const decoder = new StringDecoder('utf8');
442
+ let bytesReadTotal = 0;
443
+ let linesRead = 0;
444
+ let partial = '';
445
+ let fd = null;
446
+
447
+ try {
448
+ fd = fs.openSync(filePath, 'r');
449
+ while (true) {
450
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
451
+ if (bytesRead <= 0) break;
452
+ bytesReadTotal += bytesRead;
453
+ const text = partial + decoder.write(buf.subarray(0, bytesRead));
454
+ let start = 0;
455
+ while (true) {
456
+ const end = text.indexOf('\n', start);
457
+ if (end === -1) break;
458
+ const line = text.slice(start, end);
459
+ if (line) {
460
+ linesRead++;
461
+ parseCodexJsonlLine(line, messages, state);
462
+ }
463
+ start = end + 1;
464
+ }
465
+ partial = text.slice(start);
466
+ }
467
+ const tail = decoder.end();
468
+ if (tail) partial += tail;
469
+ if (partial && partial.trim()) {
470
+ linesRead++;
471
+ parseCodexJsonlLine(partial, messages, state);
362
472
  }
363
- messages.push(msg);
473
+ } finally {
474
+ try { if (fd != null) fs.closeSync(fd); } catch {}
364
475
  }
476
+
477
+ return {
478
+ bytesRead: bytesReadTotal,
479
+ linesRead,
480
+ messagesAdded: messages.length - before,
481
+ sessionMeta: state.sessionMeta,
482
+ };
365
483
  }
366
484
 
367
485
  function commandLooksLikeCodex(value) {
@@ -427,8 +545,10 @@ module.exports = {
427
545
  findCodexThreadFromRolloutsForSession,
428
546
  findCodexThreadForCtmSession,
429
547
  findCodexThreadForSession,
548
+ getCodexThreadResumeCwd,
430
549
  getCodexThreadById,
431
550
  getResumeSpec,
432
551
  parseSessionStartMs,
433
552
  parseCodexJsonlIntoMessages,
553
+ parseCodexJsonlFileIntoMessages,
434
554
  };