codekin 0.5.1 → 0.5.3
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 +223 -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) {
|
|
@@ -593,6 +660,7 @@ export class SessionManager {
|
|
|
593
660
|
CODEKIN_TOKEN: sessionToken,
|
|
594
661
|
CODEKIN_AUTH_TOKEN: sessionToken,
|
|
595
662
|
CODEKIN_SESSION_TYPE: session.source || 'manual',
|
|
663
|
+
...(session.permissionMode === 'dangerouslySkipPermissions' ? { CODEKIN_SKIP_PERMISSIONS: '1' } : {}),
|
|
596
664
|
};
|
|
597
665
|
// Pass CLAUDE_PROJECT_DIR so hooks and CLAUDE.md resolve correctly
|
|
598
666
|
// even when the session's working directory differs from the project root
|
|
@@ -612,7 +680,14 @@ export class SessionManager {
|
|
|
612
680
|
const repoDir = session.groupDir ?? session.workingDir;
|
|
613
681
|
const registryPatterns = this._approvalManager.getAllowedToolsForRepo(repoDir);
|
|
614
682
|
const mergedAllowedTools = [...new Set([...(session.allowedTools || []), ...registryPatterns])];
|
|
615
|
-
const cp = new ClaudeProcess(session.workingDir,
|
|
683
|
+
const cp = new ClaudeProcess(session.workingDir, {
|
|
684
|
+
sessionId: session.claudeSessionId || undefined,
|
|
685
|
+
extraEnv,
|
|
686
|
+
model: session.model,
|
|
687
|
+
permissionMode: session.permissionMode,
|
|
688
|
+
resume,
|
|
689
|
+
allowedTools: mergedAllowedTools,
|
|
690
|
+
});
|
|
616
691
|
this.wireClaudeEvents(cp, session, sessionId);
|
|
617
692
|
cp.start();
|
|
618
693
|
session.claudeProcess = cp;
|
|
@@ -622,6 +697,30 @@ export class SessionManager {
|
|
|
622
697
|
this.broadcast(session, startMsg);
|
|
623
698
|
return true;
|
|
624
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Wait for a session's Claude process to emit its system_init event,
|
|
702
|
+
* indicating it is ready to accept input. Resolves immediately if the
|
|
703
|
+
* session already has a claudeSessionId (process previously initialized).
|
|
704
|
+
* Times out after `timeoutMs` (default 30s) to avoid hanging indefinitely.
|
|
705
|
+
*/
|
|
706
|
+
waitForReady(sessionId, timeoutMs = 30_000) {
|
|
707
|
+
const session = this.sessions.get(sessionId);
|
|
708
|
+
if (!session?.claudeProcess)
|
|
709
|
+
return Promise.resolve();
|
|
710
|
+
// If the process already completed init in a prior turn, resolve immediately
|
|
711
|
+
if (session.claudeSessionId)
|
|
712
|
+
return Promise.resolve();
|
|
713
|
+
return new Promise((resolve) => {
|
|
714
|
+
const timer = setTimeout(() => {
|
|
715
|
+
console.warn(`[waitForReady] Timed out waiting for system_init on ${sessionId} after ${timeoutMs}ms`);
|
|
716
|
+
resolve();
|
|
717
|
+
}, timeoutMs);
|
|
718
|
+
session.claudeProcess.once('system_init', () => {
|
|
719
|
+
clearTimeout(timer);
|
|
720
|
+
resolve();
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
}
|
|
625
724
|
/**
|
|
626
725
|
* Attach all ClaudeProcess event listeners for a session.
|
|
627
726
|
* Extracted from startClaude() to keep that method focused on process setup.
|
|
@@ -792,55 +891,104 @@ export class SessionManager {
|
|
|
792
891
|
*/
|
|
793
892
|
handleClaudeResult(session, sessionId, result, isError) {
|
|
794
893
|
session.isProcessing = false;
|
|
894
|
+
session._claudeTurnCount++;
|
|
795
895
|
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 = {
|
|
896
|
+
// Attempt API retry for transient errors — returns true if a retry was scheduled
|
|
897
|
+
if (isError && this.handleApiRetry(session, sessionId, result)) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
// Warn about context window pressure at turn thresholds
|
|
901
|
+
this.checkContextWarning(session);
|
|
902
|
+
this.finalizeResult(session, sessionId, result, isError);
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Emit a notification when the session has had enough turns that Claude's
|
|
906
|
+
* context window may start compressing older messages. Uses simple turn-count
|
|
907
|
+
* heuristic — imprecise but zero-risk and protocol-independent.
|
|
908
|
+
*/
|
|
909
|
+
checkContextWarning(session) {
|
|
910
|
+
const turns = session._claudeTurnCount;
|
|
911
|
+
if (turns === CONTEXT_WARNING_TURN_THRESHOLD) {
|
|
912
|
+
const msg = {
|
|
913
|
+
type: 'system_message',
|
|
914
|
+
subtype: 'notification',
|
|
915
|
+
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.`,
|
|
916
|
+
};
|
|
917
|
+
this.broadcastAndHistory(session, msg);
|
|
918
|
+
session._contextWarningShown = true;
|
|
919
|
+
}
|
|
920
|
+
else if (turns === CONTEXT_CRITICAL_TURN_THRESHOLD) {
|
|
921
|
+
const msg = {
|
|
824
922
|
type: 'system_message',
|
|
825
|
-
subtype: '
|
|
826
|
-
text: `
|
|
923
|
+
subtype: 'notification',
|
|
924
|
+
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
925
|
};
|
|
828
|
-
this.
|
|
829
|
-
|
|
926
|
+
this.broadcastAndHistory(session, msg);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Detect transient API errors and schedule an automatic retry.
|
|
931
|
+
* Returns true if a retry was scheduled (caller should skip result broadcast).
|
|
932
|
+
*/
|
|
933
|
+
handleApiRetry(session, sessionId, result) {
|
|
934
|
+
if (!session._lastUserInput || !this.isRetryableApiError(result)) {
|
|
830
935
|
session._apiRetryCount = 0;
|
|
936
|
+
return false;
|
|
831
937
|
}
|
|
832
|
-
|
|
833
|
-
|
|
938
|
+
// Skip retry if the original input is older than 60 seconds — context has likely moved on
|
|
939
|
+
if (session._lastUserInputAt && Date.now() - session._lastUserInputAt > 60_000) {
|
|
940
|
+
console.log(`[api-retry] skipping stale retry for session=${sessionId} (input age=${Math.round((Date.now() - session._lastUserInputAt) / 1000)}s)`);
|
|
834
941
|
session._apiRetryCount = 0;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
if (session._apiRetryCount < MAX_API_RETRIES) {
|
|
945
|
+
session._apiRetryCount++;
|
|
946
|
+
const attempt = session._apiRetryCount;
|
|
947
|
+
const delay = API_RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
948
|
+
const retryMsg = {
|
|
949
|
+
type: 'system_message',
|
|
950
|
+
subtype: 'restart',
|
|
951
|
+
text: `API error (transient). Retrying automatically in ${delay / 1000}s (attempt ${attempt}/${MAX_API_RETRIES})...`,
|
|
952
|
+
};
|
|
953
|
+
this.addToHistory(session, retryMsg);
|
|
954
|
+
this.broadcast(session, retryMsg);
|
|
955
|
+
console.log(`[api-retry] session=${sessionId} attempt=${attempt}/${MAX_API_RETRIES} delay=${delay}ms error=${result.slice(0, 200)}`);
|
|
956
|
+
if (session._apiRetryTimer)
|
|
957
|
+
clearTimeout(session._apiRetryTimer);
|
|
958
|
+
session._apiRetryTimer = setTimeout(() => {
|
|
959
|
+
session._apiRetryTimer = undefined;
|
|
960
|
+
if (!session.claudeProcess?.isAlive() || session._stoppedByUser)
|
|
961
|
+
return;
|
|
962
|
+
console.log(`[api-retry] resending message for session=${sessionId} attempt=${attempt}`);
|
|
963
|
+
session.claudeProcess.sendMessage(session._lastUserInput);
|
|
964
|
+
}, delay);
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
// All retries exhausted
|
|
968
|
+
const exhaustedMsg = {
|
|
969
|
+
type: 'system_message',
|
|
970
|
+
subtype: 'error',
|
|
971
|
+
text: `API error persisted after ${MAX_API_RETRIES} retries. ${result}`,
|
|
972
|
+
};
|
|
973
|
+
this.addToHistory(session, exhaustedMsg);
|
|
974
|
+
this.broadcast(session, exhaustedMsg);
|
|
975
|
+
session._apiRetryCount = 0;
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Broadcast the turn result, suppress orchestrator noise, notify listeners,
|
|
980
|
+
* and trigger session naming if needed.
|
|
981
|
+
*/
|
|
982
|
+
finalizeResult(session, sessionId, result, isError) {
|
|
983
|
+
session._apiRetryCount = 0;
|
|
984
|
+
session._lastUserInput = undefined;
|
|
985
|
+
session._lastUserInputAt = undefined;
|
|
986
|
+
if (isError) {
|
|
987
|
+
const msg = { type: 'system_message', subtype: 'error', text: result };
|
|
988
|
+
this.addToHistory(session, msg);
|
|
989
|
+
this.broadcast(session, msg);
|
|
840
990
|
}
|
|
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.
|
|
991
|
+
// Suppress noise from orchestrator/agent sessions
|
|
844
992
|
if ((session.source === 'orchestrator' || session.source === 'agent') && !isError) {
|
|
845
993
|
const turnText = this.extractCurrentTurnText(session);
|
|
846
994
|
if (turnText && turnText.length < 80 && /^(no response requested|please approve|nothing to do|no action needed|acknowledged)[.!]?$/i.test(turnText.trim())) {
|
|
@@ -851,14 +999,13 @@ export class SessionManager {
|
|
|
851
999
|
const resultMsg = { type: 'result' };
|
|
852
1000
|
this.addToHistory(session, resultMsg);
|
|
853
1001
|
this.broadcast(session, resultMsg);
|
|
854
|
-
// Notify result listeners (orchestrator, child monitor, etc.)
|
|
855
1002
|
for (const listener of this._resultListeners) {
|
|
856
1003
|
try {
|
|
857
1004
|
listener(sessionId, isError);
|
|
858
1005
|
}
|
|
859
1006
|
catch { /* listener error */ }
|
|
860
1007
|
}
|
|
861
|
-
// If session is still unnamed after first response, name it now
|
|
1008
|
+
// If session is still unnamed after first response, name it now
|
|
862
1009
|
if (session.name.startsWith('hub:') && session._namingAttempts === 0) {
|
|
863
1010
|
if (session._namingTimer) {
|
|
864
1011
|
clearTimeout(session._namingTimer);
|
|
@@ -958,8 +1105,11 @@ export class SessionManager {
|
|
|
958
1105
|
const session = this.sessions.get(sessionId);
|
|
959
1106
|
if (!session)
|
|
960
1107
|
return;
|
|
1108
|
+
session._lastActivityAt = Date.now();
|
|
1109
|
+
// Reset stopped-by-user flag so idle-reaped sessions can auto-start
|
|
1110
|
+
session._stoppedByUser = false;
|
|
961
1111
|
if (!session.claudeProcess?.isAlive()) {
|
|
962
|
-
// Claude not running (e.g. after server restart) — auto-start first.
|
|
1112
|
+
// Claude not running (e.g. after server restart or idle reap) — auto-start first.
|
|
963
1113
|
// Claude CLI in -p mode waits for first input before emitting init,
|
|
964
1114
|
// so we write directly to the stdin pipe buffer (no waiting for init).
|
|
965
1115
|
this.startClaude(sessionId);
|
|
@@ -972,6 +1122,7 @@ export class SessionManager {
|
|
|
972
1122
|
if (context) {
|
|
973
1123
|
const combined = context + '\n\n' + data;
|
|
974
1124
|
session._lastUserInput = combined;
|
|
1125
|
+
session._lastUserInputAt = Date.now();
|
|
975
1126
|
session._apiRetryCount = 0;
|
|
976
1127
|
if (!session.isProcessing) {
|
|
977
1128
|
session.isProcessing = true;
|
|
@@ -991,6 +1142,7 @@ export class SessionManager {
|
|
|
991
1142
|
this.retrySessionNamingOnInteraction(sessionId);
|
|
992
1143
|
}
|
|
993
1144
|
session._lastUserInput = data;
|
|
1145
|
+
session._lastUserInputAt = Date.now();
|
|
994
1146
|
session._apiRetryCount = 0;
|
|
995
1147
|
if (!session.isProcessing) {
|
|
996
1148
|
session.isProcessing = true;
|
|
@@ -1007,6 +1159,7 @@ export class SessionManager {
|
|
|
1007
1159
|
const session = this.sessions.get(sessionId);
|
|
1008
1160
|
if (!session)
|
|
1009
1161
|
return;
|
|
1162
|
+
session._lastActivityAt = Date.now();
|
|
1010
1163
|
// ExitPlanMode approvals are handled through the normal pendingToolApprovals
|
|
1011
1164
|
// path (routed via the PreToolUse hook). No special plan_review_ prefix needed.
|
|
1012
1165
|
// Check for pending tool approval from PreToolUse hook
|
|
@@ -1379,7 +1532,7 @@ export class SessionManager {
|
|
|
1379
1532
|
if (session.allowedTools && this.matchesAllowedTools(session.allowedTools, toolName, toolInput)) {
|
|
1380
1533
|
return 'session';
|
|
1381
1534
|
}
|
|
1382
|
-
if (session.clients.size === 0 && (session.source === 'webhook' || session.source === 'workflow' || session.source === 'stepflow')) {
|
|
1535
|
+
if (session.clients.size === 0 && (session.source === 'webhook' || session.source === 'workflow' || session.source === 'stepflow' || session.source === 'orchestrator')) {
|
|
1383
1536
|
return 'headless';
|
|
1384
1537
|
}
|
|
1385
1538
|
return 'prompt';
|
|
@@ -1605,22 +1758,36 @@ export class SessionManager {
|
|
|
1605
1758
|
}
|
|
1606
1759
|
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
1760
|
}
|
|
1761
|
+
/** Max size of a single output chunk in the history buffer. */
|
|
1762
|
+
static MAX_OUTPUT_CHUNK = 50_000; // 50KB
|
|
1608
1763
|
/**
|
|
1609
1764
|
* Append a message to a session's output history for replay.
|
|
1610
|
-
* Merges consecutive 'output' chunks
|
|
1765
|
+
* Merges consecutive 'output' chunks up to MAX_OUTPUT_CHUNK to save space,
|
|
1766
|
+
* and splits oversized outputs into multiple entries to bound replay cost.
|
|
1611
1767
|
*/
|
|
1612
1768
|
addToHistory(session, msg) {
|
|
1613
1769
|
if (msg.type === 'output') {
|
|
1614
1770
|
const last = session.outputHistory[session.outputHistory.length - 1];
|
|
1615
|
-
if (last?.type === 'output' && last.data.length <
|
|
1771
|
+
if (last?.type === 'output' && last.data.length < SessionManager.MAX_OUTPUT_CHUNK) {
|
|
1616
1772
|
last.data += msg.data;
|
|
1617
1773
|
this.persistToDiskDebounced();
|
|
1618
1774
|
return;
|
|
1619
1775
|
}
|
|
1776
|
+
// Split oversized output into bounded chunks
|
|
1777
|
+
if (msg.data.length > SessionManager.MAX_OUTPUT_CHUNK) {
|
|
1778
|
+
for (let i = 0; i < msg.data.length; i += SessionManager.MAX_OUTPUT_CHUNK) {
|
|
1779
|
+
session.outputHistory.push({ type: 'output', data: msg.data.slice(i, i + SessionManager.MAX_OUTPUT_CHUNK) });
|
|
1780
|
+
}
|
|
1781
|
+
if (session.outputHistory.length > MAX_HISTORY) {
|
|
1782
|
+
session.outputHistory.splice(0, session.outputHistory.length - MAX_HISTORY);
|
|
1783
|
+
}
|
|
1784
|
+
this.persistToDiskDebounced();
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1620
1787
|
}
|
|
1621
1788
|
session.outputHistory.push(msg);
|
|
1622
1789
|
if (session.outputHistory.length > MAX_HISTORY) {
|
|
1623
|
-
session.outputHistory
|
|
1790
|
+
session.outputHistory.splice(0, session.outputHistory.length - MAX_HISTORY);
|
|
1624
1791
|
}
|
|
1625
1792
|
this.persistToDiskDebounced();
|
|
1626
1793
|
}
|
|
@@ -1674,6 +1841,10 @@ export class SessionManager {
|
|
|
1674
1841
|
/** Graceful shutdown: complete in-progress tasks, persist state, kill all processes.
|
|
1675
1842
|
* Returns a promise that resolves once all Claude processes have exited. */
|
|
1676
1843
|
shutdown() {
|
|
1844
|
+
if (this._idleReaperInterval) {
|
|
1845
|
+
clearInterval(this._idleReaperInterval);
|
|
1846
|
+
this._idleReaperInterval = null;
|
|
1847
|
+
}
|
|
1677
1848
|
// Complete in-progress tasks for active sessions before persisting.
|
|
1678
1849
|
// This handles self-deploy: the commit/push task was the last step, and
|
|
1679
1850
|
// the server restart means it succeeded. Without this, restored sessions
|