create-walle 0.9.13 → 0.9.15
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.
- package/README.md +8 -3
- package/bin/create-walle.js +232 -32
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/api-prompts.js +11 -2
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/db.js +94 -75
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
- package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
- package/template/claude-task-manager/fuzzy-utils.js +10 -2
- package/template/claude-task-manager/git-utils.js +140 -10
- package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +38 -5
- package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
- package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
- package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
- package/template/claude-task-manager/lib/session-history.js +309 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/session-stream.js +253 -20
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
- package/template/claude-task-manager/lib/walle-transcript.js +1 -3
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/package.json +1 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +71 -0
- package/template/claude-task-manager/public/index.html +2388 -429
- package/template/claude-task-manager/public/js/message-renderer.js +314 -35
- package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
- package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +396 -55
- package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
- package/template/claude-task-manager/public/js/walle-session.js +234 -26
- package/template/claude-task-manager/public/js/walle.js +143 -2
- package/template/claude-task-manager/server.js +1402 -433
- package/template/claude-task-manager/session-integrity.js +77 -28
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent-runners/claude-code.js +2 -0
- package/template/wall-e/agent.js +63 -8
- package/template/wall-e/api-walle.js +330 -52
- package/template/wall-e/brain.js +291 -42
- package/template/wall-e/chat.js +172 -15
- package/template/wall-e/coding/compaction-service.js +19 -5
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding/workspace-replay.js +1 -4
- package/template/wall-e/coding-orchestrator.js +250 -80
- package/template/wall-e/compat.js +0 -28
- package/template/wall-e/context/context-builder.js +3 -1
- package/template/wall-e/embeddings.js +2 -7
- package/template/wall-e/eval/agent-runner.js +30 -9
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/cc-replay.js +1 -0
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/debug-agent003.js +1 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/eval/run-model-comparison.js +1 -0
- package/template/wall-e/eval/swebench-adapter.js +1 -0
- package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -2
- package/template/wall-e/lib/mcp-integration.js +336 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/loops/initiative.js +87 -2
- package/template/wall-e/mcp-server.js +872 -19
- package/template/wall-e/memory/ctm-context-client.js +230 -0
- package/template/wall-e/memory/ctm-session-context.js +1376 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
- package/template/wall-e/server.js +30 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
- package/template/wall-e/skills/skill-planner.js +86 -4
- package/template/wall-e/slack/socket-mode-listener.js +276 -0
- package/template/wall-e/telemetry.js +70 -2
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +4 -4
- package/template/builder-journal.md +0 -17
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const fsp = require('fs/promises');
|
|
3
4
|
const EventEmitter = require('events');
|
|
4
5
|
const { looksLikeCompactContinuation } = require('./compact-stitch');
|
|
@@ -312,11 +313,16 @@ class SessionStream extends EventEmitter {
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
if (!displayPromptEntry) displayPromptEntry = lastPromptEntry;
|
|
316
|
+
const promptSnapshot = this._promptCacheSnapshot(st);
|
|
317
|
+
const latestPrompt = this._buildPromptPayload(lastPromptEntry);
|
|
318
|
+
const displayPrompt = this._buildPromptPayload(displayPromptEntry);
|
|
315
319
|
const summaryRecord = this._usableSummary(st.cachedSummary);
|
|
316
|
-
const
|
|
317
|
-
const
|
|
320
|
+
const aiSummary = this._buildAiSummary(st.cachedSummary, summaryRecord, promptSnapshot, sessionId);
|
|
321
|
+
const currentTask = this._buildCurrentTask(aiSummary, latestPrompt, displayPrompt);
|
|
322
|
+
const summary = aiSummary.text || (st.cachedSummary?.model === 'fallback' ? st.cachedSummary.text : null);
|
|
323
|
+
const intent = this._buildIntent(currentTask, displayPromptEntry, lastPromptEntry);
|
|
318
324
|
const progress = this._buildProgress(st, intent);
|
|
319
|
-
if (
|
|
325
|
+
if (aiSummary.status === 'fallback' || aiSummary.status === 'stale' || aiSummary.status === 'rejected') {
|
|
320
326
|
this._maybeRetryFallbackSummary(sessionId, st);
|
|
321
327
|
}
|
|
322
328
|
|
|
@@ -328,6 +334,9 @@ class SessionStream extends EventEmitter {
|
|
|
328
334
|
summarySource: summaryRecord?.model || null,
|
|
329
335
|
lastPrompt: this._promptText(lastPromptEntry),
|
|
330
336
|
displayPrompt: this._promptText(displayPromptEntry),
|
|
337
|
+
latestPrompt,
|
|
338
|
+
aiSummary,
|
|
339
|
+
currentTask,
|
|
331
340
|
intent,
|
|
332
341
|
progress,
|
|
333
342
|
turnCount: mergedTurns.length,
|
|
@@ -658,6 +667,29 @@ class SessionStream extends EventEmitter {
|
|
|
658
667
|
}
|
|
659
668
|
|
|
660
669
|
_processCodexEntry(agentSessionId, st, entry) {
|
|
670
|
+
if (entry?.type === 'event_msg' && entry.payload?.type === 'task_complete' && entry.payload.last_agent_message) {
|
|
671
|
+
const timestamp = eventTimestampMs(entry);
|
|
672
|
+
const rawText = String(entry.payload.last_agent_message || '').trim();
|
|
673
|
+
const text = rawText.length > MAX_TEXT_LEN ? rawText.slice(0, MAX_TEXT_LEN) + `...truncated (${rawText.length} chars)` : rawText;
|
|
674
|
+
const evt = decorateStreamEvent({
|
|
675
|
+
type: 'turn_complete',
|
|
676
|
+
sessionId: agentSessionId,
|
|
677
|
+
ctmSessionId: st.ctmSessionId,
|
|
678
|
+
timestamp,
|
|
679
|
+
seq: st.seq++,
|
|
680
|
+
data: {
|
|
681
|
+
text,
|
|
682
|
+
turnId: entry.payload.turn_id || '',
|
|
683
|
+
durationMs: entry.payload.duration_ms || 0,
|
|
684
|
+
},
|
|
685
|
+
}, 'codex-jsonl');
|
|
686
|
+
// Do not append this lifecycle event to the ring buffer. The latest
|
|
687
|
+
// assistant message should remain the status/summary anchor; this event
|
|
688
|
+
// exists for Codex terminal recovery subscribers.
|
|
689
|
+
this._emitEvent(agentSessionId, evt);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
661
693
|
const msg = codexMessageFromEntry(entry);
|
|
662
694
|
if (!msg) return false;
|
|
663
695
|
|
|
@@ -755,6 +787,42 @@ class SessionStream extends EventEmitter {
|
|
|
755
787
|
return Number.isFinite(entry.timestamp) ? entry.timestamp : 0;
|
|
756
788
|
}
|
|
757
789
|
|
|
790
|
+
_buildPromptPayload(entry) {
|
|
791
|
+
const text = this._promptText(entry);
|
|
792
|
+
const timestamp = this._promptTimestamp(entry);
|
|
793
|
+
return {
|
|
794
|
+
text: text || null,
|
|
795
|
+
timestamp,
|
|
796
|
+
isContentRich: this._isContentRich(text),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
_promptCacheSnapshot(st) {
|
|
801
|
+
const entries = (st?.userPromptCache || [])
|
|
802
|
+
.filter(entry => this._promptText(entry))
|
|
803
|
+
.map(entry => ({
|
|
804
|
+
text: this._promptText(entry),
|
|
805
|
+
timestamp: this._promptTimestamp(entry),
|
|
806
|
+
}));
|
|
807
|
+
const prompts = entries.map(entry => entry.text);
|
|
808
|
+
const latestEntry = entries[entries.length - 1] || null;
|
|
809
|
+
return {
|
|
810
|
+
entries,
|
|
811
|
+
prompts,
|
|
812
|
+
latestEntry,
|
|
813
|
+
latestText: latestEntry?.text || '',
|
|
814
|
+
latestTimestamp: latestEntry?.timestamp || 0,
|
|
815
|
+
promptCount: prompts.length,
|
|
816
|
+
promptHash: this._promptCacheHash(prompts),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
_promptCacheHash(prompts) {
|
|
821
|
+
const list = Array.isArray(prompts) ? prompts.filter(Boolean) : [];
|
|
822
|
+
if (list.length === 0) return '';
|
|
823
|
+
return crypto.createHash('sha1').update(list.join('\u0000')).digest('hex');
|
|
824
|
+
}
|
|
825
|
+
|
|
758
826
|
_computeStatus(st) {
|
|
759
827
|
const now = Date.now();
|
|
760
828
|
// PTY activity within 5s = running
|
|
@@ -904,18 +972,150 @@ class SessionStream extends EventEmitter {
|
|
|
904
972
|
return false;
|
|
905
973
|
}
|
|
906
974
|
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
const
|
|
975
|
+
_buildAiSummary(cachedSummary, summaryRecord, promptSnapshot, agentSessionId) {
|
|
976
|
+
const timerPending = this._summaryTimers.has(agentSessionId);
|
|
977
|
+
const latestTimestamp = promptSnapshot?.latestTimestamp || 0;
|
|
978
|
+
const currentHash = promptSnapshot?.promptHash || '';
|
|
979
|
+
const currentCount = promptSnapshot?.promptCount || 0;
|
|
980
|
+
|
|
910
981
|
if (summaryRecord?.text) {
|
|
982
|
+
const coveredTimestamp = Number.isFinite(summaryRecord.promptTimestamp)
|
|
983
|
+
? summaryRecord.promptTimestamp
|
|
984
|
+
: (summaryRecord.timestamp || 0);
|
|
985
|
+
const hasHash = !!summaryRecord.promptHash;
|
|
986
|
+
const fresh = hasHash
|
|
987
|
+
? summaryRecord.promptHash === currentHash
|
|
988
|
+
: (!latestTimestamp || (summaryRecord.timestamp || 0) >= latestTimestamp);
|
|
989
|
+
const status = fresh ? 'fresh' : (timerPending ? 'updating' : 'stale');
|
|
911
990
|
return {
|
|
912
991
|
text: summaryRecord.text,
|
|
913
992
|
source: 'ai-summary',
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
timestamp: this._promptTimestamp(promptEntry),
|
|
993
|
+
status,
|
|
994
|
+
freshness: status,
|
|
995
|
+
model: summaryRecord.model || null,
|
|
918
996
|
updatedAt: summaryRecord.timestamp || 0,
|
|
997
|
+
promptTimestamp: coveredTimestamp || 0,
|
|
998
|
+
promptCount: summaryRecord.promptCount || summaryRecord.turnCount || 0,
|
|
999
|
+
currentPromptCount: currentCount,
|
|
1000
|
+
promptHash: summaryRecord.promptHash || null,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (cachedSummary?.model === 'fallback') {
|
|
1005
|
+
return {
|
|
1006
|
+
text: this._sanitizeSummary(cachedSummary.text || '') || null,
|
|
1007
|
+
source: 'fallback',
|
|
1008
|
+
status: timerPending ? 'updating' : 'fallback',
|
|
1009
|
+
freshness: timerPending ? 'updating' : 'fallback',
|
|
1010
|
+
model: 'fallback',
|
|
1011
|
+
updatedAt: cachedSummary.timestamp || 0,
|
|
1012
|
+
promptTimestamp: cachedSummary.promptTimestamp || 0,
|
|
1013
|
+
promptCount: cachedSummary.promptCount || cachedSummary.turnCount || 0,
|
|
1014
|
+
currentPromptCount: currentCount,
|
|
1015
|
+
promptHash: cachedSummary.promptHash || null,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (cachedSummary && cachedSummary.model) {
|
|
1020
|
+
return {
|
|
1021
|
+
text: null,
|
|
1022
|
+
source: 'ai-summary',
|
|
1023
|
+
status: 'rejected',
|
|
1024
|
+
freshness: 'rejected',
|
|
1025
|
+
model: cachedSummary.model || null,
|
|
1026
|
+
updatedAt: cachedSummary.timestamp || 0,
|
|
1027
|
+
promptTimestamp: cachedSummary.promptTimestamp || 0,
|
|
1028
|
+
promptCount: cachedSummary.promptCount || cachedSummary.turnCount || 0,
|
|
1029
|
+
currentPromptCount: currentCount,
|
|
1030
|
+
promptHash: cachedSummary.promptHash || null,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return {
|
|
1035
|
+
text: null,
|
|
1036
|
+
source: 'none',
|
|
1037
|
+
status: timerPending ? 'updating' : 'missing',
|
|
1038
|
+
freshness: timerPending ? 'updating' : 'missing',
|
|
1039
|
+
model: null,
|
|
1040
|
+
updatedAt: 0,
|
|
1041
|
+
promptTimestamp: 0,
|
|
1042
|
+
promptCount: 0,
|
|
1043
|
+
currentPromptCount: currentCount,
|
|
1044
|
+
promptHash: null,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
_buildCurrentTask(aiSummary, latestPrompt, displayPrompt) {
|
|
1049
|
+
const freshAi = aiSummary?.text && aiSummary.status === 'fresh';
|
|
1050
|
+
const prompt = (latestPrompt?.isContentRich || !displayPrompt?.text)
|
|
1051
|
+
? latestPrompt
|
|
1052
|
+
: displayPrompt;
|
|
1053
|
+
const promptText = prompt?.text || null;
|
|
1054
|
+
const promptTimestamp = prompt?.timestamp || 0;
|
|
1055
|
+
|
|
1056
|
+
if (freshAi) {
|
|
1057
|
+
return {
|
|
1058
|
+
text: aiSummary.text,
|
|
1059
|
+
source: 'ai-summary',
|
|
1060
|
+
confidence: 'high',
|
|
1061
|
+
freshness: 'fresh',
|
|
1062
|
+
prompt: promptText,
|
|
1063
|
+
fullPrompt: promptText,
|
|
1064
|
+
timestamp: promptTimestamp,
|
|
1065
|
+
promptTimestamp,
|
|
1066
|
+
updatedAt: aiSummary.updatedAt || 0,
|
|
1067
|
+
aiUpdatedAt: aiSummary.updatedAt || 0,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (promptText) {
|
|
1072
|
+
const staleAiPresent = aiSummary?.source === 'ai-summary' && aiSummary?.text && aiSummary.status !== 'fresh';
|
|
1073
|
+
const source = staleAiPresent && prompt === latestPrompt ? 'latest-prompt' : 'prompt-fallback';
|
|
1074
|
+
const freshness = staleAiPresent
|
|
1075
|
+
? (aiSummary.status === 'updating' ? 'updating' : 'stale')
|
|
1076
|
+
: (aiSummary?.status === 'updating' ? 'updating' : 'fallback');
|
|
1077
|
+
return {
|
|
1078
|
+
text: promptText,
|
|
1079
|
+
source,
|
|
1080
|
+
confidence: this._isContentRich(promptText) ? 'medium' : 'low',
|
|
1081
|
+
freshness,
|
|
1082
|
+
prompt: promptText,
|
|
1083
|
+
fullPrompt: promptText,
|
|
1084
|
+
timestamp: promptTimestamp,
|
|
1085
|
+
promptTimestamp,
|
|
1086
|
+
updatedAt: promptTimestamp,
|
|
1087
|
+
aiUpdatedAt: aiSummary?.updatedAt || 0,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
text: null,
|
|
1093
|
+
source: 'missing',
|
|
1094
|
+
confidence: 'low',
|
|
1095
|
+
freshness: aiSummary?.status || 'missing',
|
|
1096
|
+
prompt: null,
|
|
1097
|
+
fullPrompt: null,
|
|
1098
|
+
timestamp: 0,
|
|
1099
|
+
promptTimestamp: 0,
|
|
1100
|
+
updatedAt: 0,
|
|
1101
|
+
aiUpdatedAt: aiSummary?.updatedAt || 0,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
_buildIntent(currentTask, displayPromptEntry, lastPromptEntry) {
|
|
1106
|
+
const promptEntry = displayPromptEntry || lastPromptEntry;
|
|
1107
|
+
const promptText = this._promptText(promptEntry);
|
|
1108
|
+
if (currentTask?.text) {
|
|
1109
|
+
return {
|
|
1110
|
+
text: currentTask.text,
|
|
1111
|
+
source: currentTask.source || 'missing',
|
|
1112
|
+
confidence: currentTask.confidence || 'low',
|
|
1113
|
+
freshness: currentTask.freshness || 'missing',
|
|
1114
|
+
prompt: currentTask.prompt || promptText || null,
|
|
1115
|
+
fullPrompt: currentTask.fullPrompt || promptText || null,
|
|
1116
|
+
timestamp: currentTask.timestamp || this._promptTimestamp(promptEntry),
|
|
1117
|
+
updatedAt: currentTask.updatedAt || currentTask.timestamp || 0,
|
|
1118
|
+
aiUpdatedAt: currentTask.aiUpdatedAt || 0,
|
|
919
1119
|
};
|
|
920
1120
|
}
|
|
921
1121
|
if (promptText) {
|
|
@@ -923,20 +1123,24 @@ class SessionStream extends EventEmitter {
|
|
|
923
1123
|
text: promptText,
|
|
924
1124
|
source: 'prompt-fallback',
|
|
925
1125
|
confidence: this._isContentRich(promptText) ? 'medium' : 'low',
|
|
1126
|
+
freshness: 'fallback',
|
|
926
1127
|
prompt: promptText,
|
|
927
1128
|
fullPrompt: promptText,
|
|
928
1129
|
timestamp: this._promptTimestamp(promptEntry),
|
|
929
1130
|
updatedAt: this._promptTimestamp(promptEntry),
|
|
1131
|
+
aiUpdatedAt: 0,
|
|
930
1132
|
};
|
|
931
1133
|
}
|
|
932
1134
|
return {
|
|
933
1135
|
text: null,
|
|
934
1136
|
source: 'missing',
|
|
935
1137
|
confidence: 'low',
|
|
1138
|
+
freshness: 'missing',
|
|
936
1139
|
prompt: null,
|
|
937
1140
|
fullPrompt: null,
|
|
938
1141
|
timestamp: 0,
|
|
939
1142
|
updatedAt: 0,
|
|
1143
|
+
aiUpdatedAt: 0,
|
|
940
1144
|
};
|
|
941
1145
|
}
|
|
942
1146
|
|
|
@@ -1056,12 +1260,9 @@ class SessionStream extends EventEmitter {
|
|
|
1056
1260
|
|
|
1057
1261
|
async _generateSummary(agentSessionId, st) {
|
|
1058
1262
|
st.lastSummaryAttempt = Date.now();
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const turnsText = userPrompts.map((t, i) => `Prompt ${i + 1}: ${t}`).join('\n');
|
|
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.';
|
|
1263
|
+
const summaryInput = this._buildSummaryInput(st);
|
|
1264
|
+
if (!summaryInput) return;
|
|
1265
|
+
const { turnsText, sysPrompt, promptSnapshot } = summaryInput;
|
|
1065
1266
|
|
|
1066
1267
|
// Tier 0: configured Wall-E provider (DeepSeek/OpenAI/Claude/Gemini/etc.).
|
|
1067
1268
|
let summaryText = '';
|
|
@@ -1085,7 +1286,10 @@ class SessionStream extends EventEmitter {
|
|
|
1085
1286
|
if (summaryText) {
|
|
1086
1287
|
st.cachedSummary = {
|
|
1087
1288
|
text: truncateText(summaryText, SUMMARY_TEXT_LIMIT),
|
|
1088
|
-
turnCount:
|
|
1289
|
+
turnCount: promptSnapshot.promptCount,
|
|
1290
|
+
promptCount: promptSnapshot.promptCount,
|
|
1291
|
+
promptHash: promptSnapshot.promptHash,
|
|
1292
|
+
promptTimestamp: promptSnapshot.latestTimestamp || 0,
|
|
1089
1293
|
model: usedModel,
|
|
1090
1294
|
timestamp: Date.now(),
|
|
1091
1295
|
};
|
|
@@ -1102,10 +1306,31 @@ class SessionStream extends EventEmitter {
|
|
|
1102
1306
|
this._emitEvent(agentSessionId, summaryEvt);
|
|
1103
1307
|
} else {
|
|
1104
1308
|
// Tier 3: heuristic fallback — picks the most recent content-rich prompt
|
|
1105
|
-
st.cachedSummary = this._fallbackSummary(
|
|
1309
|
+
st.cachedSummary = this._fallbackSummary(promptSnapshot.prompts, promptSnapshot);
|
|
1106
1310
|
}
|
|
1107
1311
|
}
|
|
1108
1312
|
|
|
1313
|
+
_buildSummaryInput(st) {
|
|
1314
|
+
const promptSnapshot = this._promptCacheSnapshot(st);
|
|
1315
|
+
if (promptSnapshot.prompts.length === 0) return null;
|
|
1316
|
+
|
|
1317
|
+
const latest = promptSnapshot.latestText;
|
|
1318
|
+
const recentContext = promptSnapshot.prompts.slice(0, -1).slice(-3);
|
|
1319
|
+
const parts = ['Latest prompt:', latest];
|
|
1320
|
+
if (recentContext.length) {
|
|
1321
|
+
parts.push('', 'Recent context:');
|
|
1322
|
+
recentContext.forEach((text, idx) => {
|
|
1323
|
+
parts.push(`${idx + 1}. ${text}`);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
turnsText: parts.join('\n'),
|
|
1329
|
+
sysPrompt: "Summarize the user's current task. Prioritize the latest prompt. Use older prompts only as context. Return 8-14 words. Return only the summary, no quotes or prefix.",
|
|
1330
|
+
promptSnapshot,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1109
1334
|
/** Tier 0: CTM-injected Wall-E provider. Returns { text, model } or null. */
|
|
1110
1335
|
async _tryConfiguredSummary(turnsText, sysPrompt) {
|
|
1111
1336
|
if (!this._summaryProvider) return null;
|
|
@@ -1205,12 +1430,20 @@ class SessionStream extends EventEmitter {
|
|
|
1205
1430
|
} catch { return null; }
|
|
1206
1431
|
}
|
|
1207
1432
|
|
|
1208
|
-
_fallbackSummary(userPrompts) {
|
|
1433
|
+
_fallbackSummary(userPrompts, promptSnapshot = null) {
|
|
1209
1434
|
// Prefer a content-rich prompt over the latest one — "/cmd yes go ahead"
|
|
1210
1435
|
// tells the user nothing about what the session is doing.
|
|
1211
1436
|
const picked = this._pickContentRichPrompt(userPrompts) || userPrompts[userPrompts.length - 1] || '';
|
|
1212
1437
|
const text = this._sanitizeSummary(picked.split('\n')[0].slice(0, 80));
|
|
1213
|
-
return {
|
|
1438
|
+
return {
|
|
1439
|
+
text,
|
|
1440
|
+
turnCount: userPrompts.length,
|
|
1441
|
+
promptCount: promptSnapshot?.promptCount || userPrompts.length,
|
|
1442
|
+
promptHash: promptSnapshot?.promptHash || this._promptCacheHash(userPrompts),
|
|
1443
|
+
promptTimestamp: promptSnapshot?.latestTimestamp || 0,
|
|
1444
|
+
model: 'fallback',
|
|
1445
|
+
timestamp: Date.now(),
|
|
1446
|
+
};
|
|
1214
1447
|
}
|
|
1215
1448
|
|
|
1216
1449
|
/**
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const STICKY_ATTENTION_MS = 12 * 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
function compactText(value) {
|
|
8
|
+
if (!value) return '';
|
|
9
|
+
if (typeof value === 'string') return value.replace(/\s+/g, ' ').trim();
|
|
10
|
+
if (typeof value === 'object') {
|
|
11
|
+
return compactText(value.text || value.summary || value.phase || value.next || value.reason || '');
|
|
12
|
+
}
|
|
13
|
+
return String(value).replace(/\s+/g, ' ').trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function truncateText(value, max = 700) {
|
|
17
|
+
const text = compactText(value);
|
|
18
|
+
if (text.length <= max) return text;
|
|
19
|
+
return `${text.slice(0, Math.max(0, max - 1)).trim()}...`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeSeverity(value) {
|
|
23
|
+
const text = String(value || '').toLowerCase().trim();
|
|
24
|
+
if (text === 'failure' || text === 'fail' || text === 'blocked' || text === 'blocker') return 'failure';
|
|
25
|
+
if (text === 'warning' || text === 'warn' || text === 'caution' || text === 'risk') return 'warning';
|
|
26
|
+
return 'none';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeConfidence(value) {
|
|
30
|
+
const text = String(value || '').toLowerCase().trim();
|
|
31
|
+
if (text === 'high' || text === 'medium' || text === 'low') return text;
|
|
32
|
+
return 'medium';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeAttention(value, defaults = {}) {
|
|
36
|
+
if (!value || typeof value !== 'object') return null;
|
|
37
|
+
const severity = normalizeSeverity(value.severity || value.level || value.kind);
|
|
38
|
+
const recommendation = truncateText(
|
|
39
|
+
value.recommendation || value.reason || value.summary || defaults.recommendation || '',
|
|
40
|
+
220
|
|
41
|
+
);
|
|
42
|
+
const evidence = Array.isArray(value.evidence)
|
|
43
|
+
? value.evidence.map(item => truncateText(item, 100)).filter(Boolean).slice(0, 3)
|
|
44
|
+
: [];
|
|
45
|
+
return {
|
|
46
|
+
severity,
|
|
47
|
+
actionLabel: severity === 'failure' ? 'Inspect' : severity === 'warning' ? 'Review' : '',
|
|
48
|
+
recommendation: recommendation || defaultRecommendation(severity),
|
|
49
|
+
confidence: normalizeConfidence(value.confidence || defaults.confidence),
|
|
50
|
+
evidence,
|
|
51
|
+
source: value.source || defaults.source || 'unknown',
|
|
52
|
+
model: value.model || defaults.model || '',
|
|
53
|
+
updatedAt: value.updatedAt || defaults.updatedAt || null,
|
|
54
|
+
sticky: !!value.sticky,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultRecommendation(severity) {
|
|
59
|
+
if (severity === 'failure') return 'A current blocker or failure appears to need attention.';
|
|
60
|
+
if (severity === 'warning') return 'A current warning appears to need attention.';
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function attentionContextHash(text) {
|
|
65
|
+
return crypto.createHash('sha1').update(String(text || '')).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildStandupAttentionContext({ session = {}, summary = {}, status = {} } = {}) {
|
|
69
|
+
const evidence = Array.isArray(summary.statusEvidence) ? summary.statusEvidence : [];
|
|
70
|
+
const progress = summary.progress || {};
|
|
71
|
+
const lines = [
|
|
72
|
+
`Title: ${compactText(session.label || summary.displayPrompt || summary.lastPrompt || session.id || '')}`,
|
|
73
|
+
`Agent: ${compactText(session.agentType || session.agent || session.type || '')}`,
|
|
74
|
+
`Status: ${compactText(session.standupStatus || session.liveStatus || session.serverState || status.status || summary.status || '')}`,
|
|
75
|
+
`Waiting: ${compactText(session.waitingReason || status.reason || '')}`,
|
|
76
|
+
`Intent: ${compactText(summary.intent || summary.displayPrompt || summary.lastPrompt || '')}`,
|
|
77
|
+
`Summary: ${compactText(summary.summary || '')}`,
|
|
78
|
+
`Progress: ${compactText(progress)}`,
|
|
79
|
+
`Evidence: ${evidence.map(compactText).filter(Boolean).join('; ')}`,
|
|
80
|
+
].filter(line => !/:\s*$/.test(line));
|
|
81
|
+
const text = truncateText(lines.join('\n'), 2500);
|
|
82
|
+
return {
|
|
83
|
+
text,
|
|
84
|
+
hash: attentionContextHash(text),
|
|
85
|
+
hasAttentionLanguage: hasAttentionLanguage(text),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasAttentionLanguage(text) {
|
|
90
|
+
return /\b(warn(?:ing|ed)?|caution|risk|blocked|blocker|failed|failing|failure|error|exception|permission denied|cannot proceed|stuck|needs attention)\b/i.test(String(text || ''));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasResolutionSignal(text) {
|
|
94
|
+
return /\b(resolved|fixed|cleared|no longer|not blocked|not a blocker|no blockers?|not a failure|no failures?|not an error|no errors?|no warnings?|unblocked|passed|verified|working now|succeeded|successful|recovered)\b/i.test(String(text || ''));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function heuristicStandupAttention(context) {
|
|
98
|
+
const text = String(context?.text || context || '');
|
|
99
|
+
const lower = text.toLowerCase();
|
|
100
|
+
const resolved = hasResolutionSignal(lower);
|
|
101
|
+
const hardFailure = /\b(cannot proceed|blocked|blocker|stuck|permission denied|fatal|panic|segmentation fault|uncaught|unhandled exception|traceback|command failed|build failed|tests? failed|npm err!)\b/i.test(lower);
|
|
102
|
+
const warning = /\b(warn(?:ing|ed)?|caution|risk|heads up|possible issue|needs attention)\b/i.test(lower);
|
|
103
|
+
const weakFailure = /\b(failed|failing|failure|error|exception)\b/i.test(lower);
|
|
104
|
+
|
|
105
|
+
if (resolved && !warning && !hardFailure) {
|
|
106
|
+
return normalizeAttention({ severity: 'none', source: 'heuristic', confidence: 'medium' });
|
|
107
|
+
}
|
|
108
|
+
if (hardFailure && !resolved) {
|
|
109
|
+
return normalizeAttention({
|
|
110
|
+
severity: 'failure',
|
|
111
|
+
recommendation: 'A current blocker or failure appears to need attention.',
|
|
112
|
+
evidence: [firstMatchingPhrase(text, /(cannot proceed|blocked|blocker|stuck|permission denied|fatal|unhandled exception|command failed|build failed|tests? failed)/i)],
|
|
113
|
+
source: 'heuristic',
|
|
114
|
+
confidence: 'medium',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (warning) {
|
|
118
|
+
return normalizeAttention({
|
|
119
|
+
severity: 'warning',
|
|
120
|
+
recommendation: 'A current warning appears to need attention.',
|
|
121
|
+
evidence: [firstMatchingPhrase(text, /(warn(?:ing|ed)?|caution|risk|possible issue|needs attention)/i)],
|
|
122
|
+
source: 'heuristic',
|
|
123
|
+
confidence: 'medium',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (weakFailure && !resolved) {
|
|
127
|
+
return normalizeAttention({
|
|
128
|
+
severity: 'warning',
|
|
129
|
+
recommendation: 'Failure or error language appears in the latest context; review whether it is still relevant.',
|
|
130
|
+
evidence: [firstMatchingPhrase(text, /(failed|failing|failure|error|exception)/i)],
|
|
131
|
+
source: 'heuristic',
|
|
132
|
+
confidence: 'low',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return normalizeAttention({ severity: 'none', source: 'heuristic', confidence: 'medium' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function firstMatchingPhrase(text, regex) {
|
|
139
|
+
const match = String(text || '').match(regex);
|
|
140
|
+
return match ? match[0] : '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseStandupAttentionResult(text, defaults = {}) {
|
|
144
|
+
const raw = String(text || '').trim();
|
|
145
|
+
if (!raw) return null;
|
|
146
|
+
const cleaned = raw
|
|
147
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
148
|
+
.replace(/\s*```$/i, '')
|
|
149
|
+
.trim();
|
|
150
|
+
const start = cleaned.indexOf('{');
|
|
151
|
+
const end = cleaned.lastIndexOf('}');
|
|
152
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
153
|
+
try {
|
|
154
|
+
return normalizeAttention(JSON.parse(cleaned.slice(start, end + 1)), defaults);
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function mergeStickyStandupAttention(previous, next, contextText, now = Date.now(), opts = {}) {
|
|
161
|
+
const stickyMs = Number.isFinite(opts.stickyMs) ? opts.stickyMs : STICKY_ATTENTION_MS;
|
|
162
|
+
const normalizedNext = normalizeAttention(next, { updatedAt: new Date(now).toISOString() })
|
|
163
|
+
|| normalizeAttention({ severity: 'none', source: 'none' }, { updatedAt: new Date(now).toISOString() });
|
|
164
|
+
const normalizedPrevious = normalizeAttention(previous);
|
|
165
|
+
|
|
166
|
+
if (normalizedNext.severity === 'failure' || normalizedNext.severity === 'warning') {
|
|
167
|
+
return {
|
|
168
|
+
...normalizedNext,
|
|
169
|
+
updatedAt: normalizedNext.updatedAt || new Date(now).toISOString(),
|
|
170
|
+
sticky: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!normalizedPrevious || !['failure', 'warning'].includes(normalizedPrevious.severity)) {
|
|
175
|
+
return normalizedNext;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (hasResolutionSignal(contextText)) return normalizedNext;
|
|
179
|
+
|
|
180
|
+
const previousMs = Date.parse(normalizedPrevious.updatedAt || '') || now;
|
|
181
|
+
if (now - previousMs > stickyMs) return normalizedNext;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...normalizedPrevious,
|
|
185
|
+
updatedAt: normalizedPrevious.updatedAt || new Date(now).toISOString(),
|
|
186
|
+
sticky: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
STICKY_ATTENTION_MS,
|
|
192
|
+
buildStandupAttentionContext,
|
|
193
|
+
hasAttentionLanguage,
|
|
194
|
+
hasResolutionSignal,
|
|
195
|
+
heuristicStandupAttention,
|
|
196
|
+
mergeStickyStandupAttention,
|
|
197
|
+
normalizeAttention,
|
|
198
|
+
normalizeSeverity,
|
|
199
|
+
parseStandupAttentionResult,
|
|
200
|
+
};
|
|
@@ -12,12 +12,18 @@
|
|
|
12
12
|
const { SessionStateBus } = require('./session-state-bus');
|
|
13
13
|
const { runHook, buildHookEnv } = require('./hook-executor');
|
|
14
14
|
const { resolveHooks, commandForState } = require('./status-hooks-config');
|
|
15
|
+
const { listStateDetectors } = require('../workers/state-detectors');
|
|
15
16
|
|
|
16
17
|
const _sessionMeta = new Map(); // sessionId -> { providerId, cwd, branch, tabTitle }
|
|
17
18
|
|
|
18
19
|
const bus = new SessionStateBus();
|
|
19
|
-
// Default per the user spec: 1500ms for Claude Code (gap #4).
|
|
20
|
-
|
|
20
|
+
// Default per the user spec: 1500ms for Claude Code (gap #4). Provider
|
|
21
|
+
// detectors can widen this for bursty TUIs such as Codex.
|
|
22
|
+
for (const detector of listStateDetectors()) {
|
|
23
|
+
if (detector && Number.isFinite(detector.idleDebounceMs)) {
|
|
24
|
+
bus.setProviderDebounce(detector.providerId, detector.idleDebounceMs);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
bus.setDefaultDebounce(1500);
|
|
22
28
|
|
|
23
29
|
let _dbModule = null;
|