codekin 0.5.0 → 0.5.1
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/dist/assets/{index-CfBnNU24.js → index-B8opKRtJ.js} +50 -50
- package/dist/assets/index-wajPH8o6.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/dist/approval-manager.d.ts +2 -0
- package/server/dist/approval-manager.js +4 -1
- package/server/dist/approval-manager.js.map +1 -1
- package/server/dist/claude-process.d.ts +0 -4
- package/server/dist/claude-process.js +2 -66
- package/server/dist/claude-process.js.map +1 -1
- package/server/dist/orchestrator-children.js +16 -10
- package/server/dist/orchestrator-children.js.map +1 -1
- package/server/dist/orchestrator-reports.js +1 -1
- package/server/dist/plan-manager.d.ts +74 -0
- package/server/dist/plan-manager.js +121 -0
- package/server/dist/plan-manager.js.map +1 -0
- package/server/dist/session-manager.d.ts +19 -4
- package/server/dist/session-manager.js +171 -47
- package/server/dist/session-manager.js.map +1 -1
- package/server/dist/session-persistence.js +2 -1
- package/server/dist/session-persistence.js.map +1 -1
- package/server/dist/tsconfig.tsbuildinfo +1 -1
- package/server/dist/types.d.ts +6 -3
- package/server/dist/types.js.map +1 -1
- package/dist/assets/index-BwKZeT4V.css +0 -1
|
@@ -24,6 +24,7 @@ import { homedir } from 'os';
|
|
|
24
24
|
import path from 'path';
|
|
25
25
|
import { promisify } from 'util';
|
|
26
26
|
import { ClaudeProcess } from './claude-process.js';
|
|
27
|
+
import { PlanManager } from './plan-manager.js';
|
|
27
28
|
import { SessionArchive } from './session-archive.js';
|
|
28
29
|
import { cleanupWorkspace } from './webhook-workspace.js';
|
|
29
30
|
import { PORT } from './config.js';
|
|
@@ -36,8 +37,6 @@ import { evaluateRestart } from './session-restart-scheduler.js';
|
|
|
36
37
|
const execFileAsync = promisify(execFile);
|
|
37
38
|
/** Max messages retained in a session's output history buffer. */
|
|
38
39
|
const MAX_HISTORY = 2000;
|
|
39
|
-
/** No-output duration before emitting a stall warning (5 minutes). */
|
|
40
|
-
const STALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
41
40
|
/** Max API error retries per turn before giving up. */
|
|
42
41
|
const MAX_API_RETRIES = 3;
|
|
43
42
|
/** Base delay for API error retry (doubles each attempt: 3s, 6s, 12s). */
|
|
@@ -91,6 +90,10 @@ export class SessionManager {
|
|
|
91
90
|
rename: (sessionId, newName) => this.rename(sessionId, newName),
|
|
92
91
|
});
|
|
93
92
|
this.sessionPersistence.restoreFromDisk();
|
|
93
|
+
// Wire PlanManager events for restored sessions
|
|
94
|
+
for (const session of this.sessions.values()) {
|
|
95
|
+
this.wirePlanManager(session);
|
|
96
|
+
}
|
|
94
97
|
}
|
|
95
98
|
// ---------------------------------------------------------------------------
|
|
96
99
|
// Approval — direct accessor (callers use sessions.approvalManager.xxx)
|
|
@@ -143,7 +146,6 @@ export class SessionManager {
|
|
|
143
146
|
restartCount: 0,
|
|
144
147
|
lastRestartAt: null,
|
|
145
148
|
_stoppedByUser: false,
|
|
146
|
-
_stallTimer: null,
|
|
147
149
|
_wasActiveBeforeRestart: false,
|
|
148
150
|
_apiRetryCount: 0,
|
|
149
151
|
_turnCount: 0,
|
|
@@ -152,7 +154,9 @@ export class SessionManager {
|
|
|
152
154
|
pendingControlRequests: new Map(),
|
|
153
155
|
pendingToolApprovals: new Map(),
|
|
154
156
|
_leaveGraceTimer: null,
|
|
157
|
+
planManager: new PlanManager(),
|
|
155
158
|
};
|
|
159
|
+
this.wirePlanManager(session);
|
|
156
160
|
this.sessions.set(id, session);
|
|
157
161
|
this.persistToDisk();
|
|
158
162
|
this._globalBroadcast?.({ type: 'sessions_updated' });
|
|
@@ -327,6 +331,11 @@ export class SessionManager {
|
|
|
327
331
|
* The `willRestart` flag indicates whether the session will be auto-restarted. */
|
|
328
332
|
onSessionExit(listener) {
|
|
329
333
|
this._exitListeners.push(listener);
|
|
334
|
+
return () => {
|
|
335
|
+
const idx = this._exitListeners.indexOf(listener);
|
|
336
|
+
if (idx >= 0)
|
|
337
|
+
this._exitListeners.splice(idx, 1);
|
|
338
|
+
};
|
|
330
339
|
}
|
|
331
340
|
/** Register a listener called when any session emits a prompt (permission request or question). */
|
|
332
341
|
onSessionPrompt(listener) {
|
|
@@ -335,6 +344,11 @@ export class SessionManager {
|
|
|
335
344
|
/** Register a listener called when any session completes a turn (result event). */
|
|
336
345
|
onSessionResult(listener) {
|
|
337
346
|
this._resultListeners.push(listener);
|
|
347
|
+
return () => {
|
|
348
|
+
const idx = this._resultListeners.indexOf(listener);
|
|
349
|
+
if (idx >= 0)
|
|
350
|
+
this._resultListeners.splice(idx, 1);
|
|
351
|
+
};
|
|
338
352
|
}
|
|
339
353
|
get(id) {
|
|
340
354
|
return this.sessions.get(id);
|
|
@@ -488,7 +502,6 @@ export class SessionManager {
|
|
|
488
502
|
return false;
|
|
489
503
|
// Prevent auto-restart when deleting
|
|
490
504
|
session._stoppedByUser = true;
|
|
491
|
-
this.clearStallTimer(session);
|
|
492
505
|
if (session._apiRetryTimer)
|
|
493
506
|
clearTimeout(session._apiRetryTimer);
|
|
494
507
|
if (session._namingTimer)
|
|
@@ -603,7 +616,6 @@ export class SessionManager {
|
|
|
603
616
|
this.wireClaudeEvents(cp, session, sessionId);
|
|
604
617
|
cp.start();
|
|
605
618
|
session.claudeProcess = cp;
|
|
606
|
-
this.resetStallTimer(session);
|
|
607
619
|
this._globalBroadcast?.({ type: 'sessions_updated' });
|
|
608
620
|
const startMsg = { type: 'claude_started', sessionId };
|
|
609
621
|
this.addToHistory(session, startMsg);
|
|
@@ -622,11 +634,23 @@ export class SessionManager {
|
|
|
622
634
|
cp.on('image', (base64Data, mediaType) => this.onImageEvent(session, base64Data, mediaType));
|
|
623
635
|
cp.on('tool_active', (toolName, toolInput) => this.onToolActiveEvent(session, toolName, toolInput));
|
|
624
636
|
cp.on('tool_done', (toolName, summary) => this.onToolDoneEvent(session, toolName, summary));
|
|
625
|
-
cp.on('planning_mode', (active) => {
|
|
637
|
+
cp.on('planning_mode', (active) => {
|
|
638
|
+
// Route EnterPlanMode through PlanManager for UI state tracking.
|
|
639
|
+
// ExitPlanMode (active=false) is ignored here — the PreToolUse hook
|
|
640
|
+
// is the enforcement gate, and it calls handleExitPlanModeApproval()
|
|
641
|
+
// which transitions PlanManager to 'reviewing'.
|
|
642
|
+
if (active) {
|
|
643
|
+
session.planManager.onEnterPlanMode();
|
|
644
|
+
}
|
|
645
|
+
// ExitPlanMode stream event intentionally ignored — hook handles it.
|
|
646
|
+
});
|
|
626
647
|
cp.on('todo_update', (tasks) => { this.broadcastAndHistory(session, { type: 'todo_update', tasks }); });
|
|
627
648
|
cp.on('prompt', (...args) => this.onPromptEvent(session, ...args));
|
|
628
649
|
cp.on('control_request', (requestId, toolName, toolInput) => this.onControlRequestEvent(cp, session, sessionId, requestId, toolName, toolInput));
|
|
629
|
-
cp.on('result', (result, isError) => {
|
|
650
|
+
cp.on('result', (result, isError) => {
|
|
651
|
+
session.planManager.onTurnEnd();
|
|
652
|
+
this.handleClaudeResult(session, sessionId, result, isError);
|
|
653
|
+
});
|
|
630
654
|
cp.on('error', (message) => this.broadcast(session, { type: 'error', message }));
|
|
631
655
|
cp.on('exit', (code, signal) => { cp.removeAllListeners(); this.handleClaudeExit(session, sessionId, code, signal); });
|
|
632
656
|
}
|
|
@@ -635,6 +659,20 @@ export class SessionManager {
|
|
|
635
659
|
this.addToHistory(session, msg);
|
|
636
660
|
this.broadcast(session, msg);
|
|
637
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Wire PlanManager events for a session.
|
|
664
|
+
* Called once at session creation (not per-process, since PlanManager outlives restarts).
|
|
665
|
+
* Idempotent — guards against double-wiring on restore + restart.
|
|
666
|
+
*/
|
|
667
|
+
wirePlanManager(session) {
|
|
668
|
+
if (session._planManagerWired)
|
|
669
|
+
return;
|
|
670
|
+
session._planManagerWired = true;
|
|
671
|
+
const pm = session.planManager;
|
|
672
|
+
pm.on('planning_mode', (active) => {
|
|
673
|
+
this.broadcastAndHistory(session, { type: 'planning_mode', active });
|
|
674
|
+
});
|
|
675
|
+
}
|
|
638
676
|
onSystemInit(cp, session, model) {
|
|
639
677
|
session.claudeSessionId = cp.getSessionId();
|
|
640
678
|
// Only show model message on first init or when model actually changes
|
|
@@ -644,30 +682,24 @@ export class SessionManager {
|
|
|
644
682
|
}
|
|
645
683
|
}
|
|
646
684
|
onTextEvent(session, sessionId, text) {
|
|
647
|
-
this.resetStallTimer(session);
|
|
648
685
|
this.broadcastAndHistory(session, { type: 'output', data: text });
|
|
649
686
|
if (session.name.startsWith('hub:') && !session._namingTimer) {
|
|
650
687
|
this.scheduleSessionNaming(sessionId);
|
|
651
688
|
}
|
|
652
689
|
}
|
|
653
690
|
onThinkingEvent(session, summary) {
|
|
654
|
-
this.resetStallTimer(session);
|
|
655
691
|
this.broadcast(session, { type: 'thinking', summary });
|
|
656
692
|
}
|
|
657
693
|
onToolOutputEvent(session, content, isError) {
|
|
658
|
-
this.resetStallTimer(session);
|
|
659
694
|
this.broadcastAndHistory(session, { type: 'tool_output', content, isError });
|
|
660
695
|
}
|
|
661
696
|
onImageEvent(session, base64, mediaType) {
|
|
662
|
-
this.resetStallTimer(session);
|
|
663
697
|
this.broadcastAndHistory(session, { type: 'image', base64, mediaType });
|
|
664
698
|
}
|
|
665
699
|
onToolActiveEvent(session, toolName, toolInput) {
|
|
666
|
-
this.resetStallTimer(session);
|
|
667
700
|
this.broadcastAndHistory(session, { type: 'tool_active', toolName, toolInput });
|
|
668
701
|
}
|
|
669
702
|
onToolDoneEvent(session, toolName, summary) {
|
|
670
|
-
this.resetStallTimer(session);
|
|
671
703
|
this.broadcastAndHistory(session, { type: 'tool_done', toolName, summary });
|
|
672
704
|
}
|
|
673
705
|
onPromptEvent(session, promptType, question, options, multiSelect, toolName, toolInput, requestId, questions) {
|
|
@@ -806,6 +838,16 @@ export class SessionManager {
|
|
|
806
838
|
this.broadcast(session, msg);
|
|
807
839
|
}
|
|
808
840
|
}
|
|
841
|
+
// Suppress noise from orchestrator/agent sessions: if the entire turn's
|
|
842
|
+
// text output is a short, low-value phrase, strip it from history so it
|
|
843
|
+
// doesn't pollute the chat or replay on rejoin.
|
|
844
|
+
if ((session.source === 'orchestrator' || session.source === 'agent') && !isError) {
|
|
845
|
+
const turnText = this.extractCurrentTurnText(session);
|
|
846
|
+
if (turnText && turnText.length < 80 && /^(no response requested|please approve|nothing to do|no action needed|acknowledged)[.!]?$/i.test(turnText.trim())) {
|
|
847
|
+
this.stripCurrentTurnOutput(session);
|
|
848
|
+
console.log(`[noise-filter] suppressed orchestrator noise: "${turnText.trim().slice(0, 60)}"`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
809
851
|
const resultMsg = { type: 'result' };
|
|
810
852
|
this.addToHistory(session, resultMsg);
|
|
811
853
|
this.broadcast(session, resultMsg);
|
|
@@ -835,7 +877,7 @@ export class SessionManager {
|
|
|
835
877
|
handleClaudeExit(session, sessionId, code, signal) {
|
|
836
878
|
session.claudeProcess = null;
|
|
837
879
|
session.isProcessing = false;
|
|
838
|
-
|
|
880
|
+
session.planManager.reset();
|
|
839
881
|
this._globalBroadcast?.({ type: 'sessions_updated' });
|
|
840
882
|
const action = evaluateRestart({
|
|
841
883
|
restartCount: session.restartCount,
|
|
@@ -965,6 +1007,8 @@ export class SessionManager {
|
|
|
965
1007
|
const session = this.sessions.get(sessionId);
|
|
966
1008
|
if (!session)
|
|
967
1009
|
return;
|
|
1010
|
+
// ExitPlanMode approvals are handled through the normal pendingToolApprovals
|
|
1011
|
+
// path (routed via the PreToolUse hook). No special plan_review_ prefix needed.
|
|
968
1012
|
// Check for pending tool approval from PreToolUse hook
|
|
969
1013
|
if (!requestId) {
|
|
970
1014
|
const totalPending = session.pendingToolApprovals.size + session.pendingControlRequests.size;
|
|
@@ -1045,6 +1089,27 @@ export class SessionManager {
|
|
|
1045
1089
|
this.broadcast(session, { type: 'prompt_dismiss', requestId: approval.requestId });
|
|
1046
1090
|
return;
|
|
1047
1091
|
}
|
|
1092
|
+
// ExitPlanMode: route through PlanManager for state tracking.
|
|
1093
|
+
// The hook will convert allow→deny-with-approval-message (CLI workaround).
|
|
1094
|
+
if (approval.toolName === 'ExitPlanMode') {
|
|
1095
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
1096
|
+
const isDeny = first === 'deny';
|
|
1097
|
+
if (isDeny) {
|
|
1098
|
+
// Extract feedback text if present (value may be ['deny', 'feedback text'])
|
|
1099
|
+
const feedback = Array.isArray(value) && value.length > 1 ? value[1] : undefined;
|
|
1100
|
+
const reason = session.planManager.deny(approval.requestId, feedback);
|
|
1101
|
+
console.log(`[plan-approval] denied: ${reason}`);
|
|
1102
|
+
approval.resolve({ allow: false, always: false });
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
session.planManager.approve(approval.requestId);
|
|
1106
|
+
console.log(`[plan-approval] approved`);
|
|
1107
|
+
approval.resolve({ allow: true, always: false });
|
|
1108
|
+
}
|
|
1109
|
+
session.pendingToolApprovals.delete(approval.requestId);
|
|
1110
|
+
this.broadcast(session, { type: 'prompt_dismiss', requestId: approval.requestId });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1048
1113
|
const { isDeny, isAlwaysAllow, isApprovePattern } = this.decodeApprovalValue(value);
|
|
1049
1114
|
if (isAlwaysAllow && !isDeny) {
|
|
1050
1115
|
this._approvalManager.saveAlwaysAllow(session.groupDir ?? session.workingDir, approval.toolName, approval.toolInput);
|
|
@@ -1056,12 +1121,6 @@ export class SessionManager {
|
|
|
1056
1121
|
approval.resolve({ allow: !isDeny, always: isAlwaysAllow || isApprovePattern });
|
|
1057
1122
|
session.pendingToolApprovals.delete(approval.requestId);
|
|
1058
1123
|
this.broadcast(session, { type: 'prompt_dismiss', requestId: approval.requestId });
|
|
1059
|
-
// When ExitPlanMode is approved via the PreToolUse hook, immediately clear
|
|
1060
|
-
// pending state and emit planning_mode:false. The control_request path may
|
|
1061
|
-
// never arrive (or arrive as is_error=true), so this ensures plan mode exits.
|
|
1062
|
-
if (approval.toolName === 'ExitPlanMode' && !isDeny) {
|
|
1063
|
-
session.claudeProcess?.clearPendingExitPlanMode();
|
|
1064
|
-
}
|
|
1065
1124
|
}
|
|
1066
1125
|
/**
|
|
1067
1126
|
* Send an AskUserQuestion control response, mapping the user's answer(s) into
|
|
@@ -1132,6 +1191,11 @@ export class SessionManager {
|
|
|
1132
1191
|
return Promise.resolve({ allow: true, always: false });
|
|
1133
1192
|
}
|
|
1134
1193
|
console.log(`[tool-approval] requesting approval: session=${sessionId} tool=${toolName} clients=${session.clients.size}`);
|
|
1194
|
+
// ExitPlanMode: route through PlanManager state machine for plan-specific
|
|
1195
|
+
// approval UI. The hook blocks until we resolve the promise.
|
|
1196
|
+
if (toolName === 'ExitPlanMode') {
|
|
1197
|
+
return this.handleExitPlanModeApproval(session, sessionId);
|
|
1198
|
+
}
|
|
1135
1199
|
// Prevent double-gating: if a control_request already created a pending
|
|
1136
1200
|
// entry for this tool, auto-approve the control_request and let the hook
|
|
1137
1201
|
// take over as the sole approval gate. This is the reverse of the check
|
|
@@ -1167,7 +1231,7 @@ export class SessionManager {
|
|
|
1167
1231
|
this.broadcast(session, { type: 'prompt_dismiss', requestId: approvalRequestId });
|
|
1168
1232
|
resolve({ allow: false, always: false });
|
|
1169
1233
|
}
|
|
1170
|
-
}, isQuestion
|
|
1234
|
+
}, isQuestion ? 300_000 : (session.source === 'agent' ? 300_000 : 60_000)); // 5 min for questions & agent children, 1 min for interactive
|
|
1171
1235
|
let promptMsg;
|
|
1172
1236
|
if (isQuestion) {
|
|
1173
1237
|
// AskUserQuestion: extract structured questions from toolInput.questions
|
|
@@ -1242,6 +1306,66 @@ export class SessionManager {
|
|
|
1242
1306
|
}
|
|
1243
1307
|
});
|
|
1244
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Handle ExitPlanMode approval through PlanManager.
|
|
1311
|
+
* Shows a plan-specific approval prompt (Approve/Reject) and blocks the hook
|
|
1312
|
+
* until the user responds. On approve, returns allow:true (the hook will use
|
|
1313
|
+
* the deny-with-approval-message workaround). On deny, returns allow:false.
|
|
1314
|
+
*/
|
|
1315
|
+
handleExitPlanModeApproval(session, sessionId) {
|
|
1316
|
+
const reviewId = session.planManager.onExitPlanModeRequested();
|
|
1317
|
+
if (!reviewId) {
|
|
1318
|
+
// Not in planning state — fall through to allow (CLI handles natively)
|
|
1319
|
+
console.log(`[plan-approval] ExitPlanMode but PlanManager not in planning state, allowing`);
|
|
1320
|
+
return Promise.resolve({ allow: true, always: false });
|
|
1321
|
+
}
|
|
1322
|
+
return new Promise((resolve) => {
|
|
1323
|
+
const timer = { id: null };
|
|
1324
|
+
const wrappedResolve = (result) => {
|
|
1325
|
+
if (timer.id)
|
|
1326
|
+
clearTimeout(timer.id);
|
|
1327
|
+
resolve(result);
|
|
1328
|
+
};
|
|
1329
|
+
// Timeout: auto-deny after 5 minutes to prevent leaked promises
|
|
1330
|
+
timer.id = setTimeout(() => {
|
|
1331
|
+
if (session.pendingToolApprovals.has(reviewId)) {
|
|
1332
|
+
console.log(`[plan-approval] timed out, auto-denying`);
|
|
1333
|
+
session.pendingToolApprovals.delete(reviewId);
|
|
1334
|
+
session.planManager.deny(reviewId);
|
|
1335
|
+
this.broadcast(session, { type: 'prompt_dismiss', requestId: reviewId });
|
|
1336
|
+
resolve({ allow: false, always: false });
|
|
1337
|
+
}
|
|
1338
|
+
}, 300_000);
|
|
1339
|
+
const promptMsg = {
|
|
1340
|
+
type: 'prompt',
|
|
1341
|
+
promptType: 'permission',
|
|
1342
|
+
question: 'Approve plan and start implementation?',
|
|
1343
|
+
options: [
|
|
1344
|
+
{ label: 'Approve', value: 'allow' },
|
|
1345
|
+
{ label: 'Reject', value: 'deny' },
|
|
1346
|
+
],
|
|
1347
|
+
toolName: 'ExitPlanMode',
|
|
1348
|
+
requestId: reviewId,
|
|
1349
|
+
};
|
|
1350
|
+
session.pendingToolApprovals.set(reviewId, {
|
|
1351
|
+
resolve: wrappedResolve,
|
|
1352
|
+
toolName: 'ExitPlanMode',
|
|
1353
|
+
toolInput: {},
|
|
1354
|
+
requestId: reviewId,
|
|
1355
|
+
promptMsg,
|
|
1356
|
+
});
|
|
1357
|
+
this.broadcast(session, promptMsg);
|
|
1358
|
+
if (session.clients.size === 0) {
|
|
1359
|
+
this._globalBroadcast?.({ ...promptMsg, sessionId, sessionName: session.name });
|
|
1360
|
+
}
|
|
1361
|
+
for (const listener of this._promptListeners) {
|
|
1362
|
+
try {
|
|
1363
|
+
listener(sessionId, 'permission', 'ExitPlanMode', reviewId);
|
|
1364
|
+
}
|
|
1365
|
+
catch { /* listener error */ }
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1245
1369
|
/**
|
|
1246
1370
|
* Check if a tool invocation can be auto-approved without prompting the user.
|
|
1247
1371
|
* Returns 'registry' if matched by auto-approval rules, 'session' if matched
|
|
@@ -1301,8 +1425,6 @@ export class SessionManager {
|
|
|
1301
1425
|
const filePath = String(toolInput.file_path || '');
|
|
1302
1426
|
return `Allow Read? \`${filePath}\``;
|
|
1303
1427
|
}
|
|
1304
|
-
case 'ExitPlanMode':
|
|
1305
|
-
return 'Approve plan and start implementation?';
|
|
1306
1428
|
default:
|
|
1307
1429
|
return `Allow ${toolName}?`;
|
|
1308
1430
|
}
|
|
@@ -1359,7 +1481,6 @@ export class SessionManager {
|
|
|
1359
1481
|
const session = this.sessions.get(sessionId);
|
|
1360
1482
|
if (session?.claudeProcess) {
|
|
1361
1483
|
session._stoppedByUser = true;
|
|
1362
|
-
this.clearStallTimer(session);
|
|
1363
1484
|
if (session._apiRetryTimer)
|
|
1364
1485
|
clearTimeout(session._apiRetryTimer);
|
|
1365
1486
|
session.claudeProcess.removeAllListeners();
|
|
@@ -1379,7 +1500,6 @@ export class SessionManager {
|
|
|
1379
1500
|
return;
|
|
1380
1501
|
const cp = session.claudeProcess;
|
|
1381
1502
|
session._stoppedByUser = true;
|
|
1382
|
-
this.clearStallTimer(session);
|
|
1383
1503
|
if (session._apiRetryTimer)
|
|
1384
1504
|
clearTimeout(session._apiRetryTimer);
|
|
1385
1505
|
cp.removeAllListeners();
|
|
@@ -1396,6 +1516,32 @@ export class SessionManager {
|
|
|
1396
1516
|
isRetryableApiError(text) {
|
|
1397
1517
|
return API_RETRY_PATTERNS.some((pattern) => pattern.test(text));
|
|
1398
1518
|
}
|
|
1519
|
+
/** Extract the concatenated text output from the current turn (after the last 'result' in history). */
|
|
1520
|
+
extractCurrentTurnText(session) {
|
|
1521
|
+
let text = '';
|
|
1522
|
+
for (let i = session.outputHistory.length - 1; i >= 0; i--) {
|
|
1523
|
+
const msg = session.outputHistory[i];
|
|
1524
|
+
if (msg.type === 'result')
|
|
1525
|
+
break;
|
|
1526
|
+
if (msg.type === 'output')
|
|
1527
|
+
text = msg.data + text;
|
|
1528
|
+
}
|
|
1529
|
+
return text;
|
|
1530
|
+
}
|
|
1531
|
+
/** Remove output messages from the current turn in history (after the last 'result'). */
|
|
1532
|
+
stripCurrentTurnOutput(session) {
|
|
1533
|
+
let cutIndex = session.outputHistory.length;
|
|
1534
|
+
for (let i = session.outputHistory.length - 1; i >= 0; i--) {
|
|
1535
|
+
const msg = session.outputHistory[i];
|
|
1536
|
+
if (msg.type === 'result')
|
|
1537
|
+
break;
|
|
1538
|
+
if (msg.type === 'output') {
|
|
1539
|
+
cutIndex = i;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
// Remove output entries from cutIndex onwards (keep non-output entries like tool events)
|
|
1543
|
+
session.outputHistory = session.outputHistory.filter((msg, idx) => idx < cutIndex || msg.type !== 'output');
|
|
1544
|
+
}
|
|
1399
1545
|
/**
|
|
1400
1546
|
* Build a condensed text summary of a session's conversation history.
|
|
1401
1547
|
* Used as context when auto-starting Claude for sessions without a saved
|
|
@@ -1459,27 +1605,6 @@ export class SessionManager {
|
|
|
1459
1605
|
}
|
|
1460
1606
|
return `[This session was interrupted by a server restart. Here is the previous conversation for context:]\n${context}\n[End of previous context. The user's new message follows.]`;
|
|
1461
1607
|
}
|
|
1462
|
-
resetStallTimer(session) {
|
|
1463
|
-
this.clearStallTimer(session);
|
|
1464
|
-
session._stallTimer = setTimeout(() => {
|
|
1465
|
-
session._stallTimer = null;
|
|
1466
|
-
if (!session.claudeProcess?.isAlive())
|
|
1467
|
-
return;
|
|
1468
|
-
const msg = {
|
|
1469
|
-
type: 'system_message',
|
|
1470
|
-
subtype: 'stall',
|
|
1471
|
-
text: 'No output for 5 minutes. The process may be stalled.',
|
|
1472
|
-
};
|
|
1473
|
-
this.addToHistory(session, msg);
|
|
1474
|
-
this.broadcast(session, msg);
|
|
1475
|
-
}, STALL_TIMEOUT_MS);
|
|
1476
|
-
}
|
|
1477
|
-
clearStallTimer(session) {
|
|
1478
|
-
if (session._stallTimer) {
|
|
1479
|
-
clearTimeout(session._stallTimer);
|
|
1480
|
-
session._stallTimer = null;
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
1608
|
/**
|
|
1484
1609
|
* Append a message to a session's output history for replay.
|
|
1485
1610
|
* Merges consecutive 'output' chunks into a single entry to save space.
|
|
@@ -1571,7 +1696,6 @@ export class SessionManager {
|
|
|
1571
1696
|
session.claudeProcess.stop();
|
|
1572
1697
|
}));
|
|
1573
1698
|
}
|
|
1574
|
-
this.clearStallTimer(session);
|
|
1575
1699
|
}
|
|
1576
1700
|
this.archive.shutdown();
|
|
1577
1701
|
if (exitPromises.length === 0)
|