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
@@ -16,6 +16,7 @@ const path = require('path');
16
16
  const os = require('os');
17
17
  const { verifyCandidateForTab, readFirstUserMessage, wordOverlap } = require('./session-verify.js');
18
18
  const { verifyLineage } = require('./session-lineage.js');
19
+ const claudeDesktopSessions = require('./claude-desktop-sessions');
19
20
 
20
21
  const HOSTNAME = os.hostname();
21
22
  const fsp = fs.promises;
@@ -133,8 +134,10 @@ function registerSessionJobs(scheduler, deps) {
133
134
  for (let index = 0; index < files.length; index++) {
134
135
  const { filePath, projectPath, projectEntry } = files[index];
135
136
  try {
136
- const sessionId = path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
137
- const stat = await fsp.stat(filePath);
137
+ const virtualSession = claudeDesktopSessions.parseVirtualSessionPath(filePath);
138
+ const sessionId = virtualSession ? virtualSession.sessionId : path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
139
+ const statPath = claudeDesktopSessions.sourcePathForStat(filePath);
140
+ const stat = await fsp.stat(statPath);
138
141
  const modifiedAt = stat.mtime.toISOString();
139
142
  const existing = existingMap.get(sessionId);
140
143
 
@@ -174,6 +177,7 @@ function registerSessionJobs(scheduler, deps) {
174
177
  fileSize: parsed.fileSize,
175
178
  modifiedAt: parsed.modifiedAt,
176
179
  firstMessage: parsed.firstMessage || '',
180
+ createdAt: parsed.timestamp || '',
177
181
  provider: parsed.agent || 'claude',
178
182
  model: parsed.modelId || '',
179
183
  gitBranch: parsed.gitBranch || '',
@@ -191,6 +195,7 @@ function registerSessionJobs(scheduler, deps) {
191
195
  fileSize: parsed.fileSize,
192
196
  modifiedAt: parsed.modifiedAt,
193
197
  firstMessage: parsed.firstMessage || '',
198
+ createdAt: parsed.timestamp || '',
194
199
  provider: parsed.agent || 'claude',
195
200
  model: parsed.modelId || '',
196
201
  gitBranch: parsed.gitBranch || '',
@@ -234,7 +239,7 @@ function registerSessionJobs(scheduler, deps) {
234
239
  const rows = db.prepare(
235
240
  "SELECT id, jsonl_path FROM sessions WHERE jsonl_path != '' AND jsonl_path IS NOT NULL AND (user_msg_count = 0 OR user_msg_count IS NULL)"
236
241
  ).all();
237
- const toDelete = rows.filter(r => !fs.existsSync(r.jsonl_path)).map(r => r.id);
242
+ const toDelete = rows.filter(r => !fs.existsSync(claudeDesktopSessions.sourcePathForStat(r.jsonl_path))).map(r => r.id);
238
243
  if (toDelete.length > 0) {
239
244
  const txn = db.transaction(() => {
240
245
  const stmt = db.prepare("DELETE FROM sessions WHERE id = ?");
@@ -249,7 +254,7 @@ function registerSessionJobs(scheduler, deps) {
249
254
  const rows = db.prepare(
250
255
  "SELECT a.agent_session_id, a.jsonl_path, a.ctm_session_id FROM agent_sessions a WHERE a.jsonl_path != '' AND a.jsonl_path IS NOT NULL"
251
256
  ).all();
252
- const missingFiles = rows.filter(r => !fs.existsSync(r.jsonl_path));
257
+ const missingFiles = rows.filter(r => !fs.existsSync(claudeDesktopSessions.sourcePathForStat(r.jsonl_path)));
253
258
  if (missingFiles.length > 0) {
254
259
  const txn = db.transaction(() => {
255
260
  const delAgent = db.prepare("DELETE FROM agent_sessions WHERE agent_session_id = ?");
@@ -296,7 +301,7 @@ function registerSessionJobs(scheduler, deps) {
296
301
  try {
297
302
  if (!row.jsonl_path) continue;
298
303
  let stat;
299
- try { stat = await fsp.stat(row.jsonl_path); } catch { continue; }
304
+ try { stat = await fsp.stat(claudeDesktopSessions.sourcePathForStat(row.jsonl_path)); } catch { continue; }
300
305
  const newMtime = stat.mtime.toISOString();
301
306
  if (row.file_size !== stat.size || row.modified_at !== newMtime) {
302
307
  updateMetadata.run(stat.size, newMtime, row.agent_session_id);
@@ -11,6 +11,7 @@ const PTY_ACTIVE_WINDOW_MS = 5000;
11
11
  const JSONL_ACTIVE_WINDOW_MS = 10000;
12
12
  const IDLE_THRESHOLD_MS = 30000;
13
13
  const SUMMARY_DEBOUNCE_MS = 2000;
14
+ const SUMMARY_RETRY_COOLDOWN_MS = 60000;
14
15
  const PROMPT_CACHE_TEXT_LIMIT = 4000;
15
16
  const SUMMARY_TEXT_LIMIT = 220;
16
17
  const PROGRESS_BULLET_LIMIT = 3;
@@ -149,6 +150,35 @@ function truncateText(text, limit) {
149
150
  return t.slice(0, Math.max(0, limit - 3)).trimEnd() + '...';
150
151
  }
151
152
 
153
+ function roleForEventType(type) {
154
+ if (type === 'user' || type === 'assistant') return type;
155
+ if (type === 'tool_result') return 'tool';
156
+ if (type === 'summary') return 'system';
157
+ return 'event';
158
+ }
159
+
160
+ function captureKindForEventType(type) {
161
+ if (type === 'user' || type === 'assistant') return 'message';
162
+ if (type === 'tool_result') return 'tool_result';
163
+ if (type === 'summary') return 'summary';
164
+ return 'event';
165
+ }
166
+
167
+ function decorateStreamEvent(evt, source = 'jsonl') {
168
+ if (!evt || typeof evt !== 'object') return evt;
169
+ const out = {
170
+ ...evt,
171
+ schemaVersion: 1,
172
+ eventId: evt.eventId || `${evt.sessionId || 'session'}:${evt.seq ?? 'x'}:${evt.type || 'event'}`,
173
+ captureKind: evt.captureKind || captureKindForEventType(evt.type),
174
+ role: evt.role || roleForEventType(evt.type),
175
+ source: evt.source || source,
176
+ agentSessionId: evt.agentSessionId || evt.sessionId,
177
+ };
178
+ if (out.text == null && out.data && typeof out.data.text === 'string') out.text = out.data.text;
179
+ return out;
180
+ }
181
+
152
182
  // --- SessionStream: EventEmitter-based structured event stream ---
153
183
 
154
184
  class SessionStream extends EventEmitter {
@@ -158,12 +188,13 @@ class SessionStream extends EventEmitter {
158
188
  * @param {Map} opts.sessions - CTM in-memory sessions map
159
189
  * @param {number} [opts.ringSize=200]
160
190
  */
161
- constructor({ jsonlWatcher, sessions, ringSize = DEFAULT_RING_SIZE, dbModule } = {}) {
191
+ constructor({ jsonlWatcher, sessions, ringSize = DEFAULT_RING_SIZE, dbModule, summaryProvider } = {}) {
162
192
  super();
163
193
  this._jsonlWatcher = jsonlWatcher;
164
194
  this._sessions = sessions;
165
195
  this._ringSize = ringSize;
166
196
  this._dbModule = dbModule || null;
197
+ this._summaryProvider = typeof summaryProvider === 'function' ? summaryProvider : null;
167
198
  this._tailer = new JsonlTailer();
168
199
  this._stopped = false;
169
200
 
@@ -285,6 +316,9 @@ class SessionStream extends EventEmitter {
285
316
  const summary = summaryRecord?.text || (st.cachedSummary?.model === 'fallback' ? st.cachedSummary.text : null);
286
317
  const intent = this._buildIntent(summaryRecord, displayPromptEntry, lastPromptEntry);
287
318
  const progress = this._buildProgress(st, intent);
319
+ if (!summaryRecord && st.cachedSummary?.model === 'fallback') {
320
+ this._maybeRetryFallbackSummary(sessionId, st);
321
+ }
288
322
 
289
323
  return {
290
324
  sessionId,
@@ -374,12 +408,14 @@ class SessionStream extends EventEmitter {
374
408
  if (jsonlPath) {
375
409
  this._readAndProcess(agentSessionId, jsonlPath, { drain: true, rememberStat: true }).catch(() => {});
376
410
  }
411
+ this.emit('link', { ctmSessionId, agentSessionId, jsonlPath });
377
412
  }
378
413
 
379
414
  /** Clean up on session exit */
380
415
  removeSession(agentSessionId) {
381
416
  const st = this._state.get(agentSessionId);
382
417
  if (!st) return;
418
+ const ctmSessionId = st.ctmSessionId;
383
419
  // Emit final status
384
420
  const oldStatus = this._computeStatus(st);
385
421
  if (oldStatus !== 'exited') {
@@ -391,6 +427,7 @@ class SessionStream extends EventEmitter {
391
427
  const timer = this._summaryTimers.get(agentSessionId);
392
428
  if (timer) { clearTimeout(timer); this._summaryTimers.delete(agentSessionId); }
393
429
  this._state.delete(agentSessionId);
430
+ this.emit('unlink', { ctmSessionId, agentSessionId });
394
431
  }
395
432
 
396
433
  /** Get the set of actively tracked session IDs */
@@ -444,6 +481,7 @@ class SessionStream extends EventEmitter {
444
481
  lastJsonlActivity: 0,
445
482
  lastAssistantParent: null,
446
483
  cachedSummary: null,
484
+ lastSummaryAttempt: 0,
447
485
  lastKnownSize: 0,
448
486
  lastKnownMtime: 0,
449
487
  };
@@ -514,7 +552,7 @@ class SessionStream extends EventEmitter {
514
552
  let text = extractText(content);
515
553
  if (text.length > MAX_TEXT_LEN) text = text.slice(0, MAX_TEXT_LEN) + `...truncated (${text.length} chars)`;
516
554
 
517
- const evt = {
555
+ const evt = decorateStreamEvent({
518
556
  type: 'user',
519
557
  sessionId: agentSessionId,
520
558
  ctmSessionId: st.ctmSessionId,
@@ -527,7 +565,7 @@ class SessionStream extends EventEmitter {
527
565
  gitBranch: entry.gitBranch || undefined,
528
566
  parentUuid: entry.uuid || undefined,
529
567
  },
530
- };
568
+ }, 'claude-jsonl');
531
569
  st.ring.push(evt);
532
570
  if (st.ring.length > this._ringSize) st.ring.shift();
533
571
  this._markJsonlActivity(st, timestamp);
@@ -550,7 +588,7 @@ class SessionStream extends EventEmitter {
550
588
  // Find and replace last assistant event with this parentUuid
551
589
  for (let i = st.ring.length - 1; i >= 0; i--) {
552
590
  if (st.ring[i].type === 'assistant' && st.ring[i].data.parentUuid === parentUuid) {
553
- st.ring[i] = {
591
+ st.ring[i] = decorateStreamEvent({
554
592
  ...st.ring[i],
555
593
  timestamp,
556
594
  data: {
@@ -560,7 +598,7 @@ class SessionStream extends EventEmitter {
560
598
  toolUses: toolUses.length ? toolUses : undefined,
561
599
  parentUuid,
562
600
  },
563
- };
601
+ }, 'claude-jsonl');
564
602
  this._markJsonlActivity(st, timestamp);
565
603
  this._emitEvent(agentSessionId, { ...st.ring[i], _update: true }, 'event:update');
566
604
  return;
@@ -569,7 +607,7 @@ class SessionStream extends EventEmitter {
569
607
  }
570
608
 
571
609
  st.lastAssistantParent = parentUuid;
572
- const evt = {
610
+ const evt = decorateStreamEvent({
573
611
  type: 'assistant',
574
612
  sessionId: agentSessionId,
575
613
  ctmSessionId: st.ctmSessionId,
@@ -582,7 +620,7 @@ class SessionStream extends EventEmitter {
582
620
  toolUses: toolUses.length ? toolUses : undefined,
583
621
  parentUuid,
584
622
  },
585
- };
623
+ }, 'claude-jsonl');
586
624
  st.ring.push(evt);
587
625
  if (st.ring.length > this._ringSize) st.ring.shift();
588
626
 
@@ -601,7 +639,7 @@ class SessionStream extends EventEmitter {
601
639
  let text = extractText(content || '');
602
640
  if (text.length > MAX_TEXT_LEN) text = text.slice(0, MAX_TEXT_LEN) + `...truncated (${text.length} chars)`;
603
641
 
604
- const evt = {
642
+ const evt = decorateStreamEvent({
605
643
  type: 'tool_result',
606
644
  sessionId: agentSessionId,
607
645
  ctmSessionId: st.ctmSessionId,
@@ -611,7 +649,7 @@ class SessionStream extends EventEmitter {
611
649
  text,
612
650
  contentBlocks: stripBase64Blobs(Array.isArray(content) ? content : []),
613
651
  },
614
- };
652
+ }, 'claude-jsonl');
615
653
  st.ring.push(evt);
616
654
  if (st.ring.length > this._ringSize) st.ring.shift();
617
655
  this._markJsonlActivity(st, timestamp);
@@ -628,7 +666,7 @@ class SessionStream extends EventEmitter {
628
666
  if (!key || st.codexUserSeen.has(key)) return true;
629
667
  st.codexUserSeen.add(key);
630
668
  const timestamp = eventTimestampMs({ timestamp: msg.timestamp });
631
- const evt = {
669
+ const evt = decorateStreamEvent({
632
670
  type: 'user',
633
671
  sessionId: agentSessionId,
634
672
  ctmSessionId: st.ctmSessionId,
@@ -638,7 +676,7 @@ class SessionStream extends EventEmitter {
638
676
  text: msg.text.length > MAX_TEXT_LEN ? msg.text.slice(0, MAX_TEXT_LEN) + `...truncated (${msg.text.length} chars)` : msg.text,
639
677
  contentBlocks: [{ type: 'text', text: msg.text }],
640
678
  },
641
- };
679
+ }, 'codex-jsonl');
642
680
  st.ring.push(evt);
643
681
  if (st.ring.length > this._ringSize) st.ring.shift();
644
682
  this._markJsonlActivity(st, timestamp);
@@ -651,14 +689,14 @@ class SessionStream extends EventEmitter {
651
689
  const timestamp = eventTimestampMs({ timestamp: msg.timestamp });
652
690
  const text = msg.text.length > MAX_TEXT_LEN ? msg.text.slice(0, MAX_TEXT_LEN) + `...truncated (${msg.text.length} chars)` : msg.text;
653
691
  const oldStatus = this._computeStatus(st);
654
- const evt = {
692
+ const evt = decorateStreamEvent({
655
693
  type: 'assistant',
656
694
  sessionId: agentSessionId,
657
695
  ctmSessionId: st.ctmSessionId,
658
696
  timestamp,
659
697
  seq: st.seq++,
660
698
  data: { text, contentBlocks: [{ type: 'text', text }] },
661
- };
699
+ }, 'codex-jsonl');
662
700
  st.ring.push(evt);
663
701
  if (st.ring.length > this._ringSize) st.ring.shift();
664
702
  this._markJsonlActivity(st, timestamp);
@@ -1007,7 +1045,17 @@ class SessionStream extends EventEmitter {
1007
1045
  }, SUMMARY_DEBOUNCE_MS));
1008
1046
  }
1009
1047
 
1048
+ _maybeRetryFallbackSummary(agentSessionId, st) {
1049
+ if (!this._summaryProvider || !st) return;
1050
+ if (this._summaryTimers.has(agentSessionId)) return;
1051
+ const now = Date.now();
1052
+ const lastAttempt = st.lastSummaryAttempt || st.cachedSummary?.timestamp || 0;
1053
+ if (lastAttempt && now - lastAttempt < SUMMARY_RETRY_COOLDOWN_MS) return;
1054
+ this._debounceSummary(agentSessionId, st);
1055
+ }
1056
+
1010
1057
  async _generateSummary(agentSessionId, st) {
1058
+ st.lastSummaryAttempt = Date.now();
1011
1059
  // Use userPromptCache (survives ring buffer churn from tool_result-only events)
1012
1060
  const userPrompts = st.userPromptCache.map((entry) => this._promptText(entry)).filter(Boolean);
1013
1061
  if (userPrompts.length === 0) return;
@@ -1015,11 +1063,18 @@ class SessionStream extends EventEmitter {
1015
1063
  const turnsText = userPrompts.map((t, i) => `Prompt ${i + 1}: ${t}`).join('\n');
1016
1064
  const sysPrompt = 'Summarize what the user is working on based on their prompts. 10-15 words. Return ONLY the summary, no quotes or prefix.';
1017
1065
 
1018
- // Tier 1: cloud Anthropic (when API key is present)
1066
+ // Tier 0: configured Wall-E provider (DeepSeek/OpenAI/Claude/Gemini/etc.).
1019
1067
  let summaryText = '';
1020
1068
  let usedModel = null;
1021
- const cloud = await this._tryCloudSummary(turnsText, sysPrompt);
1022
- if (cloud) { summaryText = cloud.text; usedModel = cloud.model; }
1069
+
1070
+ const configured = await this._tryConfiguredSummary(turnsText, sysPrompt);
1071
+ if (configured) { summaryText = configured.text; usedModel = configured.model; }
1072
+
1073
+ // Tier 1: legacy cloud Anthropic (when API key is present)
1074
+ if (!summaryText) {
1075
+ const cloud = await this._tryCloudSummary(turnsText, sysPrompt);
1076
+ if (cloud) { summaryText = cloud.text; usedModel = cloud.model; }
1077
+ }
1023
1078
 
1024
1079
  // Tier 2: local Ollama fallback (when cloud is unavailable or failed)
1025
1080
  if (!summaryText) {
@@ -1034,14 +1089,14 @@ class SessionStream extends EventEmitter {
1034
1089
  model: usedModel,
1035
1090
  timestamp: Date.now(),
1036
1091
  };
1037
- const summaryEvt = {
1092
+ const summaryEvt = decorateStreamEvent({
1038
1093
  type: 'summary',
1039
1094
  sessionId: agentSessionId,
1040
1095
  ctmSessionId: st.ctmSessionId,
1041
1096
  timestamp: Date.now(),
1042
1097
  seq: st.seq++,
1043
1098
  data: st.cachedSummary,
1044
- };
1099
+ }, 'summary');
1045
1100
  st.ring.push(summaryEvt);
1046
1101
  if (st.ring.length > this._ringSize) st.ring.shift();
1047
1102
  this._emitEvent(agentSessionId, summaryEvt);
@@ -1051,6 +1106,19 @@ class SessionStream extends EventEmitter {
1051
1106
  }
1052
1107
  }
1053
1108
 
1109
+ /** Tier 0: CTM-injected Wall-E provider. Returns { text, model } or null. */
1110
+ async _tryConfiguredSummary(turnsText, sysPrompt) {
1111
+ if (!this._summaryProvider) return null;
1112
+ try {
1113
+ const result = await this._summaryProvider({ turnsText, sysPrompt });
1114
+ const text = this._sanitizeSummary(result?.text || '');
1115
+ return text ? { text, model: result?.model || 'configured' } : null;
1116
+ } catch (e) {
1117
+ console.warn('[session-stream] configured summary failed:', e.message);
1118
+ return null;
1119
+ }
1120
+ }
1121
+
1054
1122
  /** Tier 1: Anthropic cloud. Returns { text, model } or null. */
1055
1123
  async _tryCloudSummary(turnsText, sysPrompt) {
1056
1124
  const apiKey = process.env.ANTHROPIC_API_KEY || '';
@@ -1186,4 +1254,4 @@ SessionStream._ACK_TOKENS = new Set([
1186
1254
  'a', 'an', 'the', 'to', 'main', 'that', 'this',
1187
1255
  ]);
1188
1256
 
1189
- module.exports = { SessionStream, JsonlTailer };
1257
+ module.exports = { SessionStream, JsonlTailer, decorateStreamEvent };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const SETUP_PROVIDER_TYPES = Object.freeze([
4
+ 'anthropic',
5
+ 'openai',
6
+ 'google',
7
+ 'ollama',
8
+ 'deepseek',
9
+ ]);
10
+
11
+ const SETUP_PROVIDER_NAMES = Object.freeze({
12
+ anthropic: 'Anthropic',
13
+ openai: 'OpenAI',
14
+ google: 'Google Gemini',
15
+ ollama: 'Ollama (Local)',
16
+ deepseek: 'DeepSeek',
17
+ });
18
+
19
+ const SETUP_PROVIDER_ENV_KEYS = Object.freeze({
20
+ anthropic: 'ANTHROPIC_API_KEY',
21
+ openai: 'OPENAI_API_KEY',
22
+ google: 'GOOGLE_API_KEY',
23
+ deepseek: 'DEEPSEEK_API_KEY',
24
+ });
25
+
26
+ const SETUP_LOCAL_PROVIDER_TYPES = Object.freeze([
27
+ 'ollama',
28
+ 'mlx',
29
+ ]);
30
+
31
+ const SETUP_KEYLESS_AUTH_METHODS = Object.freeze({
32
+ anthropic: ['claude_cli', 'oauth_proxy', 'devbox'],
33
+ openai: ['codex_cli'],
34
+ });
35
+
36
+ function sanitizeSetupProviderType(value) {
37
+ return typeof value === 'string'
38
+ ? value.toLowerCase().replace(/[^a-z]/g, '').slice(0, 20)
39
+ : '';
40
+ }
41
+
42
+ function sanitizeSetupModel(value) {
43
+ return typeof value === 'string'
44
+ ? value.replace(/[\r\n\s]/g, '').slice(0, 100)
45
+ : '';
46
+ }
47
+
48
+ function isSetupProviderType(value) {
49
+ return SETUP_PROVIDER_TYPES.includes(sanitizeSetupProviderType(value));
50
+ }
51
+
52
+ function setupProviderTypeList() {
53
+ return SETUP_PROVIDER_TYPES.join(', ');
54
+ }
55
+
56
+ function resolveSetupDefaultSelection({ type, model, storedModel }) {
57
+ const providerType = sanitizeSetupProviderType(type);
58
+ if (!isSetupProviderType(providerType)) {
59
+ return {
60
+ ok: false,
61
+ type: providerType,
62
+ error: 'Invalid provider type. Must be one of: ' + setupProviderTypeList(),
63
+ };
64
+ }
65
+ const requestedModel = sanitizeSetupModel(model);
66
+ const fallbackModel = sanitizeSetupModel(storedModel);
67
+ return {
68
+ ok: true,
69
+ type: providerType,
70
+ requestedModel,
71
+ targetModel: requestedModel || fallbackModel,
72
+ };
73
+ }
74
+
75
+ function setupProviderAuthMethodHasRuntimeAccess(type, authMethod) {
76
+ const providerType = sanitizeSetupProviderType(type);
77
+ const method = typeof authMethod === 'string' ? authMethod.trim() : '';
78
+ return (SETUP_KEYLESS_AUTH_METHODS[providerType] || []).includes(method);
79
+ }
80
+
81
+ function setupProviderHasRuntimeAccess({ type, env = process.env, authMethod = '', hasStoredKey = false } = {}) {
82
+ const providerType = sanitizeSetupProviderType(type);
83
+ if (SETUP_LOCAL_PROVIDER_TYPES.includes(providerType)) return true;
84
+ if (hasStoredKey) return true;
85
+ if (setupProviderAuthMethodHasRuntimeAccess(providerType, authMethod)) return true;
86
+
87
+ if (providerType === 'anthropic') {
88
+ return !!(env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_BASE_URL);
89
+ }
90
+ if (providerType === 'openai') {
91
+ return !!env.OPENAI_API_KEY;
92
+ }
93
+ if (providerType === 'google') {
94
+ return !!(env.GOOGLE_API_KEY || env.GEMINI_API_KEY);
95
+ }
96
+ if (providerType === 'deepseek') {
97
+ return !!env.DEEPSEEK_API_KEY;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ module.exports = {
103
+ SETUP_PROVIDER_TYPES,
104
+ SETUP_PROVIDER_NAMES,
105
+ SETUP_PROVIDER_ENV_KEYS,
106
+ SETUP_LOCAL_PROVIDER_TYPES,
107
+ SETUP_KEYLESS_AUTH_METHODS,
108
+ sanitizeSetupProviderType,
109
+ sanitizeSetupModel,
110
+ isSetupProviderType,
111
+ setupProviderTypeList,
112
+ resolveSetupDefaultSelection,
113
+ setupProviderAuthMethodHasRuntimeAccess,
114
+ setupProviderHasRuntimeAccess,
115
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ function cloneToolCalls(toolCalls) {
6
+ return JSON.parse(JSON.stringify(toolCalls || []));
7
+ }
8
+
9
+ function applyWalleToolEvent(toolCalls, event) {
10
+ if (!event || typeof event !== 'object') return toolCalls;
11
+ if (event.type === 'tool_call') {
12
+ toolCalls.push({
13
+ name: event.tool || event.name || 'tool',
14
+ summary: event.summary || '',
15
+ args: event.args || null,
16
+ status: 'working',
17
+ output: '',
18
+ });
19
+ return toolCalls;
20
+ }
21
+ if (event.type === 'tool_result' || event.type === 'tool_done') {
22
+ if (!toolCalls.length) return toolCalls;
23
+ const idx = toolCalls.length - 1;
24
+ const tc = toolCalls[idx];
25
+ tc.status = event.error ? 'error' : 'done';
26
+ tc.summary = event.summary || tc.summary || '';
27
+ tc.output = event.output || event.result || '';
28
+ }
29
+ return toolCalls;
30
+ }
31
+
32
+ function readWalleCtmHistory(filePath) {
33
+ let raw = '';
34
+ try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return []; }
35
+ const messages = [];
36
+ let pendingToolCalls = [];
37
+ for (const line of raw.split('\n')) {
38
+ if (!line.trim()) continue;
39
+ let entry;
40
+ try { entry = JSON.parse(line); } catch { continue; }
41
+ if (entry.type === 'user') {
42
+ messages.push({
43
+ role: 'user',
44
+ content: entry.content || '',
45
+ timestamp: entry.timestamp || 0,
46
+ });
47
+ continue;
48
+ }
49
+ if (entry.type === 'tool_call' || entry.type === 'tool_result' || entry.type === 'tool_done') {
50
+ applyWalleToolEvent(pendingToolCalls, entry);
51
+ continue;
52
+ }
53
+ if (entry.type === 'assistant') {
54
+ messages.push({
55
+ role: 'assistant',
56
+ content: entry.content || '',
57
+ model: entry.model || '',
58
+ latency_ms: entry.latencyMs || entry.latency_ms || 0,
59
+ timestamp: entry.timestamp || 0,
60
+ toolCalls: entry.toolCalls || cloneToolCalls(pendingToolCalls),
61
+ });
62
+ pendingToolCalls = [];
63
+ }
64
+ }
65
+ return messages;
66
+ }
67
+
68
+ module.exports = {
69
+ applyWalleToolEvent,
70
+ cloneToolCalls,
71
+ readWalleCtmHistory,
72
+ };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function pathExists(p) {
7
+ if (!p || typeof p !== 'string') return false;
8
+ try { return fs.existsSync(path.resolve(p)); } catch { return false; }
9
+ }
10
+
11
+ function firstExistingPath(paths) {
12
+ for (const p of paths) {
13
+ if (pathExists(p)) return path.resolve(p);
14
+ }
15
+ return '';
16
+ }
17
+
18
+ function sessionLabel(session) {
19
+ return session?.label || session?.meta?.label || '';
20
+ }
21
+
22
+ function resolveWalleChatContext({ session, contextSession, requestedCwd } = {}) {
23
+ const candidates = [];
24
+ if (requestedCwd) candidates.push({ cwd: requestedCwd, source: 'request', sessionId: null, label: '' });
25
+ if (contextSession && contextSession.id !== session?.id) {
26
+ candidates.push(
27
+ { cwd: contextSession.worktree_path, source: 'context_worktree', sessionId: contextSession.id, label: sessionLabel(contextSession) },
28
+ { cwd: contextSession.cwd, source: 'context_session', sessionId: contextSession.id, label: sessionLabel(contextSession) },
29
+ { cwd: contextSession.meta?.worktree_path, source: 'context_worktree', sessionId: contextSession.id, label: sessionLabel(contextSession) },
30
+ { cwd: contextSession.meta?.cwd, source: 'context_session', sessionId: contextSession.id, label: sessionLabel(contextSession) },
31
+ );
32
+ }
33
+ candidates.push(
34
+ { cwd: session?.worktree_path, source: 'walle_worktree', sessionId: session?.id || null, label: sessionLabel(session) },
35
+ { cwd: session?.cwd, source: 'walle_session', sessionId: session?.id || null, label: sessionLabel(session) },
36
+ { cwd: session?.meta?.worktree_path, source: 'walle_worktree', sessionId: session?.id || null, label: sessionLabel(session) },
37
+ { cwd: session?.meta?.cwd, source: 'walle_session', sessionId: session?.id || null, label: sessionLabel(session) },
38
+ );
39
+
40
+ for (const candidate of candidates) {
41
+ const cwd = firstExistingPath([candidate.cwd]);
42
+ if (!cwd) continue;
43
+ return {
44
+ cwd,
45
+ source: candidate.source,
46
+ contextSessionId: candidate.sessionId,
47
+ contextSessionLabel: candidate.label,
48
+ };
49
+ }
50
+
51
+ return {
52
+ cwd: '',
53
+ source: 'none',
54
+ contextSessionId: null,
55
+ contextSessionLabel: '',
56
+ };
57
+ }
58
+
59
+ module.exports = {
60
+ resolveWalleChatContext,
61
+ };