codekin 0.5.1 → 0.5.2
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-84JYN21S.js +178 -0
- package/dist/assets/index-C0Iuc3iT.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +30 -28
- package/server/dist/claude-process.d.ts +26 -22
- package/server/dist/claude-process.js +74 -71
- package/server/dist/claude-process.js.map +1 -1
- package/server/dist/native-permissions.js +1 -1
- package/server/dist/native-permissions.js.map +1 -1
- package/server/dist/orchestrator-manager.js +4 -1
- package/server/dist/orchestrator-manager.js.map +1 -1
- package/server/dist/orchestrator-routes.d.ts +3 -1
- package/server/dist/orchestrator-routes.js +3 -3
- package/server/dist/orchestrator-routes.js.map +1 -1
- package/server/dist/session-archive.js +2 -2
- package/server/dist/session-archive.js.map +1 -1
- package/server/dist/session-manager.d.ts +37 -1
- package/server/dist/session-manager.js +222 -52
- package/server/dist/session-manager.js.map +1 -1
- package/server/dist/session-persistence.js +2 -0
- package/server/dist/session-persistence.js.map +1 -1
- package/server/dist/tsconfig.tsbuildinfo +1 -1
- package/server/dist/types.d.ts +9 -1
- package/server/dist/types.js +1 -1
- package/server/dist/types.js.map +1 -1
- package/server/dist/upload-routes.js +3 -2
- package/server/dist/upload-routes.js.map +1 -1
- package/server/dist/webhook-config.js +9 -0
- package/server/dist/webhook-config.js.map +1 -1
- package/server/dist/webhook-handler.js +13 -0
- package/server/dist/webhook-handler.js.map +1 -1
- package/server/dist/webhook-types.d.ts +1 -0
- package/server/dist/workflow-loader.js +21 -21
- package/server/dist/workflow-loader.js.map +1 -1
- package/server/dist/ws-server.js +4 -0
- package/server/dist/ws-server.js.map +1 -1
- package/dist/assets/index-B8opKRtJ.js +0 -186
- package/dist/assets/index-wajPH8o6.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-archive.js","sourceRoot":"","sources":["../session-archive.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AACrC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAA;AAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,oBAAoB,CAAC,CAAA;AAEpD,wCAAwC;AACxC,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEhC,8CAA8C;AAC9C,MAAM,mBAAmB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAmB1C,MAAM,OAAO,cAAc;IACjB,EAAE,CAA+B;IACjC,YAAY,GAA0C,IAAI,CAAA;IAElE,YAAY,MAAe;QACzB,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACnE,MAAM,YAAY,GAAG,MAAM,IAAI,OAAO,CAAA;QACtC,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAA;QACpC,IAAI,YAAY,KAAK,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC;gBAAC,SAAS,CAAC,YAAY,EAAE,KAAK,CAAC,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,6CAA6C,CAAC,CAAC;QAChG,CAAC;QACD,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;QACpC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;QACnC,IAAI,CAAC,UAAU,EAAE,CAAA;QACjB,IAAI,CAAC,iBAAiB,EAAE,CAAA;IAC1B,CAAC;IAEO,UAAU;QAChB,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;KAmBZ,CAAC,CAAA;QAEF,oCAAoC;QACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAClG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,iDAAiD,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,sBAAsB,CAAC,CAAC,CAAA;QAC1H,CAAC;IACH,CAAC;IAED,sDAAsD;IACtD,OAAO,CAAC,OAQP;QACC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;KAGf,CAAC,CAAC,GAAG,CACJ,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,QAAQ,IAAI,IAAI,EACxB,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,OAAO,EACf,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,aAAa,CAAC,CACtC,CAAA;IACH,CAAC;IAED,6FAA6F;IAC7F,IAAI,CAAC,UAAmB;QACtB,MAAM,IAAI,GAAG,UAAU;YACrB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;;;SAMf,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"session-archive.js","sourceRoot":"","sources":["../session-archive.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,7 +60,17 @@ export declare class SessionManager {
|
|
|
60
60
|
private sessionPersistence;
|
|
61
61
|
/** Delegated diff operations (git diff, discard changes). */
|
|
62
62
|
private diffManager;
|
|
63
|
+
/** Interval handle for the idle session reaper. */
|
|
64
|
+
private _idleReaperInterval;
|
|
63
65
|
constructor();
|
|
66
|
+
/**
|
|
67
|
+
* Stop Claude processes for sessions that have been idle too long.
|
|
68
|
+
* A session is idle when it has no connected clients and no activity
|
|
69
|
+
* for IDLE_SESSION_TIMEOUT_MS. Only stops the process — does not delete
|
|
70
|
+
* the session, so it can be resumed later via --resume.
|
|
71
|
+
* Headless sessions (webhook, workflow, stepflow) are exempt.
|
|
72
|
+
*/
|
|
73
|
+
private reapIdleSessions;
|
|
64
74
|
/** Direct access to the approval manager for callers that need repo-level approval operations. */
|
|
65
75
|
get approvalManager(): ApprovalManager;
|
|
66
76
|
/** Schedule session naming via AI provider. */
|
|
@@ -145,6 +155,13 @@ export declare class SessionManager {
|
|
|
145
155
|
* Wires up all event handlers for streaming text, tools, prompts, and auto-restart.
|
|
146
156
|
*/
|
|
147
157
|
startClaude(sessionId: string): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Wait for a session's Claude process to emit its system_init event,
|
|
160
|
+
* indicating it is ready to accept input. Resolves immediately if the
|
|
161
|
+
* session already has a claudeSessionId (process previously initialized).
|
|
162
|
+
* Times out after `timeoutMs` (default 30s) to avoid hanging indefinitely.
|
|
163
|
+
*/
|
|
164
|
+
waitForReady(sessionId: string, timeoutMs?: number): Promise<void>;
|
|
148
165
|
/**
|
|
149
166
|
* Attach all ClaudeProcess event listeners for a session.
|
|
150
167
|
* Extracted from startClaude() to keep that method focused on process setup.
|
|
@@ -173,6 +190,22 @@ export declare class SessionManager {
|
|
|
173
190
|
* session naming on first completed turn.
|
|
174
191
|
*/
|
|
175
192
|
private handleClaudeResult;
|
|
193
|
+
/**
|
|
194
|
+
* Emit a notification when the session has had enough turns that Claude's
|
|
195
|
+
* context window may start compressing older messages. Uses simple turn-count
|
|
196
|
+
* heuristic — imprecise but zero-risk and protocol-independent.
|
|
197
|
+
*/
|
|
198
|
+
private checkContextWarning;
|
|
199
|
+
/**
|
|
200
|
+
* Detect transient API errors and schedule an automatic retry.
|
|
201
|
+
* Returns true if a retry was scheduled (caller should skip result broadcast).
|
|
202
|
+
*/
|
|
203
|
+
private handleApiRetry;
|
|
204
|
+
/**
|
|
205
|
+
* Broadcast the turn result, suppress orchestrator noise, notify listeners,
|
|
206
|
+
* and trigger session naming if needed.
|
|
207
|
+
*/
|
|
208
|
+
private finalizeResult;
|
|
176
209
|
/**
|
|
177
210
|
* Handle a Claude process 'exit' event: clean up state, notify exit listeners,
|
|
178
211
|
* and either auto-restart (within limits) or broadcast the final exit message.
|
|
@@ -258,9 +291,12 @@ export declare class SessionManager {
|
|
|
258
291
|
* Caps output at ~4000 chars, keeping the most recent exchanges.
|
|
259
292
|
*/
|
|
260
293
|
private buildSessionContext;
|
|
294
|
+
/** Max size of a single output chunk in the history buffer. */
|
|
295
|
+
private static readonly MAX_OUTPUT_CHUNK;
|
|
261
296
|
/**
|
|
262
297
|
* Append a message to a session's output history for replay.
|
|
263
|
-
* Merges consecutive 'output' chunks
|
|
298
|
+
* Merges consecutive 'output' chunks up to MAX_OUTPUT_CHUNK to save space,
|
|
299
|
+
* and splits oversized outputs into multiple entries to bound replay cost.
|
|
264
300
|
*/
|
|
265
301
|
addToHistory(session: Session, msg: WsServerMessage): void;
|
|
266
302
|
/** Send a message to all connected clients of a session, with back-pressure protection. */
|
|
@@ -41,6 +41,16 @@ const MAX_HISTORY = 2000;
|
|
|
41
41
|
const MAX_API_RETRIES = 3;
|
|
42
42
|
/** Base delay for API error retry (doubles each attempt: 3s, 6s, 12s). */
|
|
43
43
|
const API_RETRY_BASE_DELAY_MS = 3000;
|
|
44
|
+
/** How long a session can be idle (no clients, no activity) before its process is stopped. */
|
|
45
|
+
const IDLE_SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
46
|
+
/** How often to check for idle sessions. */
|
|
47
|
+
const IDLE_CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
48
|
+
/** How old a dead session must be before automatic pruning (7 days). */
|
|
49
|
+
const STALE_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
50
|
+
/** Number of Claude turns before showing a context compression warning. */
|
|
51
|
+
const CONTEXT_WARNING_TURN_THRESHOLD = 15;
|
|
52
|
+
/** Second warning at this threshold. */
|
|
53
|
+
const CONTEXT_CRITICAL_TURN_THRESHOLD = 25;
|
|
44
54
|
/** Patterns in result text that indicate a transient API error worth retrying. */
|
|
45
55
|
const API_RETRY_PATTERNS = [
|
|
46
56
|
/api_error/i,
|
|
@@ -79,6 +89,8 @@ export class SessionManager {
|
|
|
79
89
|
sessionPersistence;
|
|
80
90
|
/** Delegated diff operations (git diff, discard changes). */
|
|
81
91
|
diffManager;
|
|
92
|
+
/** Interval handle for the idle session reaper. */
|
|
93
|
+
_idleReaperInterval = null;
|
|
82
94
|
constructor() {
|
|
83
95
|
this.archive = new SessionArchive();
|
|
84
96
|
this._approvalManager = new ApprovalManager();
|
|
@@ -94,6 +106,58 @@ export class SessionManager {
|
|
|
94
106
|
for (const session of this.sessions.values()) {
|
|
95
107
|
this.wirePlanManager(session);
|
|
96
108
|
}
|
|
109
|
+
// Start idle session reaper
|
|
110
|
+
this._idleReaperInterval = setInterval(() => this.reapIdleSessions(), IDLE_CHECK_INTERVAL_MS);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Stop Claude processes for sessions that have been idle too long.
|
|
114
|
+
* A session is idle when it has no connected clients and no activity
|
|
115
|
+
* for IDLE_SESSION_TIMEOUT_MS. Only stops the process — does not delete
|
|
116
|
+
* the session, so it can be resumed later via --resume.
|
|
117
|
+
* Headless sessions (webhook, workflow, stepflow) are exempt.
|
|
118
|
+
*/
|
|
119
|
+
reapIdleSessions() {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
for (const session of this.sessions.values()) {
|
|
122
|
+
// Skip headless sessions — they are managed by their own lifecycles
|
|
123
|
+
if (session.source === 'webhook' || session.source === 'workflow' || session.source === 'stepflow')
|
|
124
|
+
continue;
|
|
125
|
+
// Skip sessions with connected clients or no running process
|
|
126
|
+
if (session.clients.size > 0 || !session.claudeProcess?.isAlive())
|
|
127
|
+
continue;
|
|
128
|
+
// Skip sessions that are actively processing
|
|
129
|
+
if (session.isProcessing)
|
|
130
|
+
continue;
|
|
131
|
+
const idleMs = now - session._lastActivityAt;
|
|
132
|
+
if (idleMs > IDLE_SESSION_TIMEOUT_MS) {
|
|
133
|
+
console.log(`[idle-reaper] stopping idle session=${session.id} name="${session.name}" idle=${Math.round(idleMs / 60_000)}min`);
|
|
134
|
+
session._stoppedByUser = true; // prevent auto-restart
|
|
135
|
+
session.claudeProcess.removeAllListeners();
|
|
136
|
+
session.claudeProcess.stop();
|
|
137
|
+
session.claudeProcess = null;
|
|
138
|
+
session.isProcessing = false;
|
|
139
|
+
const msg = { type: 'system_message', subtype: 'exit', text: 'Claude process stopped due to inactivity. It will resume when you send a new message.' };
|
|
140
|
+
this.addToHistory(session, msg);
|
|
141
|
+
this.persistToDiskDebounced();
|
|
142
|
+
this._globalBroadcast?.({ type: 'sessions_updated' });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Prune stale sessions: no process, no clients, older than STALE_SESSION_AGE_MS
|
|
146
|
+
const staleIds = [];
|
|
147
|
+
for (const session of this.sessions.values()) {
|
|
148
|
+
if (session.claudeProcess?.isAlive())
|
|
149
|
+
continue;
|
|
150
|
+
if (session.clients.size > 0)
|
|
151
|
+
continue;
|
|
152
|
+
const ageMs = now - new Date(session.created).getTime();
|
|
153
|
+
if (ageMs > STALE_SESSION_AGE_MS) {
|
|
154
|
+
staleIds.push(session.id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const id of staleIds) {
|
|
158
|
+
console.log(`[idle-reaper] pruning stale session=${id} (age > ${STALE_SESSION_AGE_MS / 86_400_000}d)`);
|
|
159
|
+
this.delete(id);
|
|
160
|
+
}
|
|
97
161
|
}
|
|
98
162
|
// ---------------------------------------------------------------------------
|
|
99
163
|
// Approval — direct accessor (callers use sessions.approvalManager.xxx)
|
|
@@ -149,11 +213,13 @@ export class SessionManager {
|
|
|
149
213
|
_wasActiveBeforeRestart: false,
|
|
150
214
|
_apiRetryCount: 0,
|
|
151
215
|
_turnCount: 0,
|
|
216
|
+
_claudeTurnCount: 0,
|
|
152
217
|
_namingAttempts: 0,
|
|
153
218
|
isProcessing: false,
|
|
154
219
|
pendingControlRequests: new Map(),
|
|
155
220
|
pendingToolApprovals: new Map(),
|
|
156
221
|
_leaveGraceTimer: null,
|
|
222
|
+
_lastActivityAt: Date.now(),
|
|
157
223
|
planManager: new PlanManager(),
|
|
158
224
|
};
|
|
159
225
|
this.wirePlanManager(session);
|
|
@@ -400,7 +466,7 @@ export class SessionManager {
|
|
|
400
466
|
groupDir: s.groupDir,
|
|
401
467
|
worktreePath: s.worktreePath,
|
|
402
468
|
connectedClients: s.clients.size,
|
|
403
|
-
lastActivity: s.
|
|
469
|
+
lastActivity: new Date(s._lastActivityAt).toISOString(),
|
|
404
470
|
source: s.source,
|
|
405
471
|
}));
|
|
406
472
|
}
|
|
@@ -417,7 +483,7 @@ export class SessionManager {
|
|
|
417
483
|
groupDir: s.groupDir,
|
|
418
484
|
worktreePath: s.worktreePath,
|
|
419
485
|
connectedClients: s.clients.size,
|
|
420
|
-
lastActivity: s.
|
|
486
|
+
lastActivity: new Date(s._lastActivityAt).toISOString(),
|
|
421
487
|
source: s.source,
|
|
422
488
|
}));
|
|
423
489
|
}
|
|
@@ -444,6 +510,7 @@ export class SessionManager {
|
|
|
444
510
|
}
|
|
445
511
|
session.clients.add(ws);
|
|
446
512
|
this.clientSessionMap.set(ws, sessionId);
|
|
513
|
+
session._lastActivityAt = Date.now();
|
|
447
514
|
// Re-broadcast pending tool approval prompts (PreToolUse hook path)
|
|
448
515
|
for (const pending of session.pendingToolApprovals.values()) {
|
|
449
516
|
if (pending.promptMsg) {
|
|
@@ -612,7 +679,14 @@ export class SessionManager {
|
|
|
612
679
|
const repoDir = session.groupDir ?? session.workingDir;
|
|
613
680
|
const registryPatterns = this._approvalManager.getAllowedToolsForRepo(repoDir);
|
|
614
681
|
const mergedAllowedTools = [...new Set([...(session.allowedTools || []), ...registryPatterns])];
|
|
615
|
-
const cp = new ClaudeProcess(session.workingDir,
|
|
682
|
+
const cp = new ClaudeProcess(session.workingDir, {
|
|
683
|
+
sessionId: session.claudeSessionId || undefined,
|
|
684
|
+
extraEnv,
|
|
685
|
+
model: session.model,
|
|
686
|
+
permissionMode: session.permissionMode,
|
|
687
|
+
resume,
|
|
688
|
+
allowedTools: mergedAllowedTools,
|
|
689
|
+
});
|
|
616
690
|
this.wireClaudeEvents(cp, session, sessionId);
|
|
617
691
|
cp.start();
|
|
618
692
|
session.claudeProcess = cp;
|
|
@@ -622,6 +696,30 @@ export class SessionManager {
|
|
|
622
696
|
this.broadcast(session, startMsg);
|
|
623
697
|
return true;
|
|
624
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Wait for a session's Claude process to emit its system_init event,
|
|
701
|
+
* indicating it is ready to accept input. Resolves immediately if the
|
|
702
|
+
* session already has a claudeSessionId (process previously initialized).
|
|
703
|
+
* Times out after `timeoutMs` (default 30s) to avoid hanging indefinitely.
|
|
704
|
+
*/
|
|
705
|
+
waitForReady(sessionId, timeoutMs = 30_000) {
|
|
706
|
+
const session = this.sessions.get(sessionId);
|
|
707
|
+
if (!session?.claudeProcess)
|
|
708
|
+
return Promise.resolve();
|
|
709
|
+
// If the process already completed init in a prior turn, resolve immediately
|
|
710
|
+
if (session.claudeSessionId)
|
|
711
|
+
return Promise.resolve();
|
|
712
|
+
return new Promise((resolve) => {
|
|
713
|
+
const timer = setTimeout(() => {
|
|
714
|
+
console.warn(`[waitForReady] Timed out waiting for system_init on ${sessionId} after ${timeoutMs}ms`);
|
|
715
|
+
resolve();
|
|
716
|
+
}, timeoutMs);
|
|
717
|
+
session.claudeProcess.once('system_init', () => {
|
|
718
|
+
clearTimeout(timer);
|
|
719
|
+
resolve();
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
}
|
|
625
723
|
/**
|
|
626
724
|
* Attach all ClaudeProcess event listeners for a session.
|
|
627
725
|
* Extracted from startClaude() to keep that method focused on process setup.
|
|
@@ -792,55 +890,104 @@ export class SessionManager {
|
|
|
792
890
|
*/
|
|
793
891
|
handleClaudeResult(session, sessionId, result, isError) {
|
|
794
892
|
session.isProcessing = false;
|
|
893
|
+
session._claudeTurnCount++;
|
|
795
894
|
this._globalBroadcast?.({ type: 'sessions_updated' });
|
|
796
|
-
//
|
|
797
|
-
if (isError && session
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
// All retries exhausted
|
|
823
|
-
const exhaustedMsg = {
|
|
895
|
+
// Attempt API retry for transient errors — returns true if a retry was scheduled
|
|
896
|
+
if (isError && this.handleApiRetry(session, sessionId, result)) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
// Warn about context window pressure at turn thresholds
|
|
900
|
+
this.checkContextWarning(session);
|
|
901
|
+
this.finalizeResult(session, sessionId, result, isError);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Emit a notification when the session has had enough turns that Claude's
|
|
905
|
+
* context window may start compressing older messages. Uses simple turn-count
|
|
906
|
+
* heuristic — imprecise but zero-risk and protocol-independent.
|
|
907
|
+
*/
|
|
908
|
+
checkContextWarning(session) {
|
|
909
|
+
const turns = session._claudeTurnCount;
|
|
910
|
+
if (turns === CONTEXT_WARNING_TURN_THRESHOLD) {
|
|
911
|
+
const msg = {
|
|
912
|
+
type: 'system_message',
|
|
913
|
+
subtype: 'notification',
|
|
914
|
+
text: `This session has ${turns} turns. Claude may begin compressing older messages from its context window. Earlier parts of the conversation may no longer be fully available to Claude.`,
|
|
915
|
+
};
|
|
916
|
+
this.broadcastAndHistory(session, msg);
|
|
917
|
+
session._contextWarningShown = true;
|
|
918
|
+
}
|
|
919
|
+
else if (turns === CONTEXT_CRITICAL_TURN_THRESHOLD) {
|
|
920
|
+
const msg = {
|
|
824
921
|
type: 'system_message',
|
|
825
|
-
subtype: '
|
|
826
|
-
text: `
|
|
922
|
+
subtype: 'notification',
|
|
923
|
+
text: `This session has ${turns} turns. Claude's context window is likely under pressure — older messages may have been compressed or dropped. Consider starting a new session for best results.`,
|
|
827
924
|
};
|
|
828
|
-
this.
|
|
829
|
-
|
|
925
|
+
this.broadcastAndHistory(session, msg);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Detect transient API errors and schedule an automatic retry.
|
|
930
|
+
* Returns true if a retry was scheduled (caller should skip result broadcast).
|
|
931
|
+
*/
|
|
932
|
+
handleApiRetry(session, sessionId, result) {
|
|
933
|
+
if (!session._lastUserInput || !this.isRetryableApiError(result)) {
|
|
830
934
|
session._apiRetryCount = 0;
|
|
935
|
+
return false;
|
|
831
936
|
}
|
|
832
|
-
|
|
833
|
-
|
|
937
|
+
// Skip retry if the original input is older than 60 seconds — context has likely moved on
|
|
938
|
+
if (session._lastUserInputAt && Date.now() - session._lastUserInputAt > 60_000) {
|
|
939
|
+
console.log(`[api-retry] skipping stale retry for session=${sessionId} (input age=${Math.round((Date.now() - session._lastUserInputAt) / 1000)}s)`);
|
|
834
940
|
session._apiRetryCount = 0;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
if (session._apiRetryCount < MAX_API_RETRIES) {
|
|
944
|
+
session._apiRetryCount++;
|
|
945
|
+
const attempt = session._apiRetryCount;
|
|
946
|
+
const delay = API_RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
947
|
+
const retryMsg = {
|
|
948
|
+
type: 'system_message',
|
|
949
|
+
subtype: 'restart',
|
|
950
|
+
text: `API error (transient). Retrying automatically in ${delay / 1000}s (attempt ${attempt}/${MAX_API_RETRIES})...`,
|
|
951
|
+
};
|
|
952
|
+
this.addToHistory(session, retryMsg);
|
|
953
|
+
this.broadcast(session, retryMsg);
|
|
954
|
+
console.log(`[api-retry] session=${sessionId} attempt=${attempt}/${MAX_API_RETRIES} delay=${delay}ms error=${result.slice(0, 200)}`);
|
|
955
|
+
if (session._apiRetryTimer)
|
|
956
|
+
clearTimeout(session._apiRetryTimer);
|
|
957
|
+
session._apiRetryTimer = setTimeout(() => {
|
|
958
|
+
session._apiRetryTimer = undefined;
|
|
959
|
+
if (!session.claudeProcess?.isAlive() || session._stoppedByUser)
|
|
960
|
+
return;
|
|
961
|
+
console.log(`[api-retry] resending message for session=${sessionId} attempt=${attempt}`);
|
|
962
|
+
session.claudeProcess.sendMessage(session._lastUserInput);
|
|
963
|
+
}, delay);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
// All retries exhausted
|
|
967
|
+
const exhaustedMsg = {
|
|
968
|
+
type: 'system_message',
|
|
969
|
+
subtype: 'error',
|
|
970
|
+
text: `API error persisted after ${MAX_API_RETRIES} retries. ${result}`,
|
|
971
|
+
};
|
|
972
|
+
this.addToHistory(session, exhaustedMsg);
|
|
973
|
+
this.broadcast(session, exhaustedMsg);
|
|
974
|
+
session._apiRetryCount = 0;
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Broadcast the turn result, suppress orchestrator noise, notify listeners,
|
|
979
|
+
* and trigger session naming if needed.
|
|
980
|
+
*/
|
|
981
|
+
finalizeResult(session, sessionId, result, isError) {
|
|
982
|
+
session._apiRetryCount = 0;
|
|
983
|
+
session._lastUserInput = undefined;
|
|
984
|
+
session._lastUserInputAt = undefined;
|
|
985
|
+
if (isError) {
|
|
986
|
+
const msg = { type: 'system_message', subtype: 'error', text: result };
|
|
987
|
+
this.addToHistory(session, msg);
|
|
988
|
+
this.broadcast(session, msg);
|
|
840
989
|
}
|
|
841
|
-
// Suppress noise from orchestrator/agent sessions
|
|
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.
|
|
990
|
+
// Suppress noise from orchestrator/agent sessions
|
|
844
991
|
if ((session.source === 'orchestrator' || session.source === 'agent') && !isError) {
|
|
845
992
|
const turnText = this.extractCurrentTurnText(session);
|
|
846
993
|
if (turnText && turnText.length < 80 && /^(no response requested|please approve|nothing to do|no action needed|acknowledged)[.!]?$/i.test(turnText.trim())) {
|
|
@@ -851,14 +998,13 @@ export class SessionManager {
|
|
|
851
998
|
const resultMsg = { type: 'result' };
|
|
852
999
|
this.addToHistory(session, resultMsg);
|
|
853
1000
|
this.broadcast(session, resultMsg);
|
|
854
|
-
// Notify result listeners (orchestrator, child monitor, etc.)
|
|
855
1001
|
for (const listener of this._resultListeners) {
|
|
856
1002
|
try {
|
|
857
1003
|
listener(sessionId, isError);
|
|
858
1004
|
}
|
|
859
1005
|
catch { /* listener error */ }
|
|
860
1006
|
}
|
|
861
|
-
// If session is still unnamed after first response, name it now
|
|
1007
|
+
// If session is still unnamed after first response, name it now
|
|
862
1008
|
if (session.name.startsWith('hub:') && session._namingAttempts === 0) {
|
|
863
1009
|
if (session._namingTimer) {
|
|
864
1010
|
clearTimeout(session._namingTimer);
|
|
@@ -958,8 +1104,11 @@ export class SessionManager {
|
|
|
958
1104
|
const session = this.sessions.get(sessionId);
|
|
959
1105
|
if (!session)
|
|
960
1106
|
return;
|
|
1107
|
+
session._lastActivityAt = Date.now();
|
|
1108
|
+
// Reset stopped-by-user flag so idle-reaped sessions can auto-start
|
|
1109
|
+
session._stoppedByUser = false;
|
|
961
1110
|
if (!session.claudeProcess?.isAlive()) {
|
|
962
|
-
// Claude not running (e.g. after server restart) — auto-start first.
|
|
1111
|
+
// Claude not running (e.g. after server restart or idle reap) — auto-start first.
|
|
963
1112
|
// Claude CLI in -p mode waits for first input before emitting init,
|
|
964
1113
|
// so we write directly to the stdin pipe buffer (no waiting for init).
|
|
965
1114
|
this.startClaude(sessionId);
|
|
@@ -972,6 +1121,7 @@ export class SessionManager {
|
|
|
972
1121
|
if (context) {
|
|
973
1122
|
const combined = context + '\n\n' + data;
|
|
974
1123
|
session._lastUserInput = combined;
|
|
1124
|
+
session._lastUserInputAt = Date.now();
|
|
975
1125
|
session._apiRetryCount = 0;
|
|
976
1126
|
if (!session.isProcessing) {
|
|
977
1127
|
session.isProcessing = true;
|
|
@@ -991,6 +1141,7 @@ export class SessionManager {
|
|
|
991
1141
|
this.retrySessionNamingOnInteraction(sessionId);
|
|
992
1142
|
}
|
|
993
1143
|
session._lastUserInput = data;
|
|
1144
|
+
session._lastUserInputAt = Date.now();
|
|
994
1145
|
session._apiRetryCount = 0;
|
|
995
1146
|
if (!session.isProcessing) {
|
|
996
1147
|
session.isProcessing = true;
|
|
@@ -1007,6 +1158,7 @@ export class SessionManager {
|
|
|
1007
1158
|
const session = this.sessions.get(sessionId);
|
|
1008
1159
|
if (!session)
|
|
1009
1160
|
return;
|
|
1161
|
+
session._lastActivityAt = Date.now();
|
|
1010
1162
|
// ExitPlanMode approvals are handled through the normal pendingToolApprovals
|
|
1011
1163
|
// path (routed via the PreToolUse hook). No special plan_review_ prefix needed.
|
|
1012
1164
|
// Check for pending tool approval from PreToolUse hook
|
|
@@ -1379,7 +1531,7 @@ export class SessionManager {
|
|
|
1379
1531
|
if (session.allowedTools && this.matchesAllowedTools(session.allowedTools, toolName, toolInput)) {
|
|
1380
1532
|
return 'session';
|
|
1381
1533
|
}
|
|
1382
|
-
if (session.clients.size === 0 && (session.source === 'webhook' || session.source === 'workflow' || session.source === 'stepflow')) {
|
|
1534
|
+
if (session.clients.size === 0 && (session.source === 'webhook' || session.source === 'workflow' || session.source === 'stepflow' || session.source === 'orchestrator')) {
|
|
1383
1535
|
return 'headless';
|
|
1384
1536
|
}
|
|
1385
1537
|
return 'prompt';
|
|
@@ -1605,22 +1757,36 @@ export class SessionManager {
|
|
|
1605
1757
|
}
|
|
1606
1758
|
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.]`;
|
|
1607
1759
|
}
|
|
1760
|
+
/** Max size of a single output chunk in the history buffer. */
|
|
1761
|
+
static MAX_OUTPUT_CHUNK = 50_000; // 50KB
|
|
1608
1762
|
/**
|
|
1609
1763
|
* Append a message to a session's output history for replay.
|
|
1610
|
-
* Merges consecutive 'output' chunks
|
|
1764
|
+
* Merges consecutive 'output' chunks up to MAX_OUTPUT_CHUNK to save space,
|
|
1765
|
+
* and splits oversized outputs into multiple entries to bound replay cost.
|
|
1611
1766
|
*/
|
|
1612
1767
|
addToHistory(session, msg) {
|
|
1613
1768
|
if (msg.type === 'output') {
|
|
1614
1769
|
const last = session.outputHistory[session.outputHistory.length - 1];
|
|
1615
|
-
if (last?.type === 'output' && last.data.length <
|
|
1770
|
+
if (last?.type === 'output' && last.data.length < SessionManager.MAX_OUTPUT_CHUNK) {
|
|
1616
1771
|
last.data += msg.data;
|
|
1617
1772
|
this.persistToDiskDebounced();
|
|
1618
1773
|
return;
|
|
1619
1774
|
}
|
|
1775
|
+
// Split oversized output into bounded chunks
|
|
1776
|
+
if (msg.data.length > SessionManager.MAX_OUTPUT_CHUNK) {
|
|
1777
|
+
for (let i = 0; i < msg.data.length; i += SessionManager.MAX_OUTPUT_CHUNK) {
|
|
1778
|
+
session.outputHistory.push({ type: 'output', data: msg.data.slice(i, i + SessionManager.MAX_OUTPUT_CHUNK) });
|
|
1779
|
+
}
|
|
1780
|
+
if (session.outputHistory.length > MAX_HISTORY) {
|
|
1781
|
+
session.outputHistory.splice(0, session.outputHistory.length - MAX_HISTORY);
|
|
1782
|
+
}
|
|
1783
|
+
this.persistToDiskDebounced();
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1620
1786
|
}
|
|
1621
1787
|
session.outputHistory.push(msg);
|
|
1622
1788
|
if (session.outputHistory.length > MAX_HISTORY) {
|
|
1623
|
-
session.outputHistory
|
|
1789
|
+
session.outputHistory.splice(0, session.outputHistory.length - MAX_HISTORY);
|
|
1624
1790
|
}
|
|
1625
1791
|
this.persistToDiskDebounced();
|
|
1626
1792
|
}
|
|
@@ -1674,6 +1840,10 @@ export class SessionManager {
|
|
|
1674
1840
|
/** Graceful shutdown: complete in-progress tasks, persist state, kill all processes.
|
|
1675
1841
|
* Returns a promise that resolves once all Claude processes have exited. */
|
|
1676
1842
|
shutdown() {
|
|
1843
|
+
if (this._idleReaperInterval) {
|
|
1844
|
+
clearInterval(this._idleReaperInterval);
|
|
1845
|
+
this._idleReaperInterval = null;
|
|
1846
|
+
}
|
|
1677
1847
|
// Complete in-progress tasks for active sessions before persisting.
|
|
1678
1848
|
// This handles self-deploy: the commit/push task was the last step, and
|
|
1679
1849
|
// the server restart means it succeeded. Without this, restored sessions
|