cc-viewer 1.9.18 → 1.9.19
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/cli.js +34 -0
- package/dist/assets/{App-COyZwmVt.js → App-5GFhfhAM.js} +2 -2
- package/dist/assets/{MdxEditorPanel-BsHDgZCY.js → MdxEditorPanel-C71H-8ZP.js} +1 -1
- package/dist/assets/{Mobile-B1H_PrNl.js → Mobile-CAqle3TP.js} +1 -1
- package/dist/assets/{ProxyStatsModal-CSQNNwpU.js → ProxyStatsModal-B0ySgEsq.js} +1 -1
- package/dist/assets/{index-CPn0gvr6.js → index-C4_-ImaE.js} +2 -2
- package/dist/assets/{seqResourceLoaders-DZA-JHK0.css → seqResourceLoaders-C8O3LxzJ.css} +1 -1
- package/dist/assets/seqResourceLoaders-CXYXv_fq.js +2 -0
- package/dist/index.html +1 -1
- package/dist/voice-packs/default/agentLoopStart.wav +0 -0
- package/dist/voice-packs/default/pack.json +11 -0
- package/dist/voice-packs/sanguo/agentLoopStart.wav +0 -0
- package/dist/voice-packs/sanguo/pack.json +11 -0
- package/node_modules/@ccv/core/src/voice-pack-events.js +12 -3
- package/package.json +2 -2
- package/server/lib/attach-buffer.js +164 -0
- package/server/lib/plugin-loader.js +2 -0
- package/server/lib/plugin-runtime.js +12 -2
- package/server/lib/sdk-manager.js +301 -58
- package/server/lib/v2/live-feed.js +108 -7
- package/server/lib/v2/meta-rows.js +35 -4
- package/server/routes/events.js +64 -1
- package/server/server.js +80 -3
- package/dist/assets/seqResourceLoaders-Cm1JnNsu.js +0 -2
|
@@ -189,6 +189,12 @@ const RETRY_CONTINUE_PROMPT =
|
|
|
189
189
|
// How often to re-broadcast sdk-session-state while a turn is active (keeps the client's
|
|
190
190
|
// Stop button/spinner alive past its 30s stale sweep and re-seeds it after a view switch).
|
|
191
191
|
const STATE_HEARTBEAT_MS = 10 * 1000;
|
|
192
|
+
// Message-level activity coalescing: every non-boundary SDK message (assistant/user/
|
|
193
|
+
// tool_progress/system task_*/status/api_retry/compact) refreshes turn liveness, but
|
|
194
|
+
// rebroadcasting per message would flood the terminal WS on busy turns. Activity frames
|
|
195
|
+
// reuse the sdk-session-state shape (heartbeat+activity flags) so clients need no new
|
|
196
|
+
// handler, and are capped at one per ACTIVITY_MIN_INTERVAL_MS per turn.
|
|
197
|
+
const ACTIVITY_MIN_INTERVAL_MS = 1000;
|
|
192
198
|
// getContextUsage control requests have no SDK-side timeout — bound the wait so a live-but-
|
|
193
199
|
// unresponsive child can't wedge the in-flight gate forever.
|
|
194
200
|
const CTX_USAGE_TIMEOUT_MS = 10 * 1000;
|
|
@@ -205,6 +211,11 @@ let _onTurnEnd = null; // SDK mode has no Stop hook (ensureHooks() skipped) —
|
|
|
205
211
|
let _onQueryError = null; // query-level failure → (message: string, meta: {cwd, sessionId, instanceKey}) => void; cli.js forwards it as a scoped sdk-error WS frame (dialogue row + toast)
|
|
206
212
|
let _broadcastWs = null;
|
|
207
213
|
let _runWaterfallHook = null;
|
|
214
|
+
// Authoritative per-turn loop boundaries (voice/plugin/SSE agent_loop_*). Unlike the
|
|
215
|
+
// debounced turn_end, these fire exactly once per accepted turn at push time (start) and
|
|
216
|
+
// at the turn's real settlement (end) — see _emitAgentLoopStart/_emitAgentLoopEnd.
|
|
217
|
+
let _onAgentLoopStart = null;
|
|
218
|
+
let _onAgentLoopEnd = null;
|
|
208
219
|
// Durable process-level wiring registered ONCE by cli.js. Unlike the mutable callbacks
|
|
209
220
|
// above (which stopSession clears), this survives a workspace stop→relaunch so a launch
|
|
210
221
|
// route that omits the callbacks still restores them (see _adoptProcessWiring). Tests
|
|
@@ -216,6 +227,8 @@ export function setSdkProcessWiring(w) {
|
|
|
216
227
|
onQueryError: w.onQueryError || null,
|
|
217
228
|
broadcastWs: w.broadcastWs || null,
|
|
218
229
|
runWaterfallHook: w.runWaterfallHook || null,
|
|
230
|
+
onAgentLoopStart: w.onAgentLoopStart || null,
|
|
231
|
+
onAgentLoopEnd: w.onAgentLoopEnd || null,
|
|
219
232
|
};
|
|
220
233
|
}
|
|
221
234
|
function _adoptProcessWiring() {
|
|
@@ -224,6 +237,8 @@ function _adoptProcessWiring() {
|
|
|
224
237
|
if (!_onQueryError) _onQueryError = _processWiring.onQueryError;
|
|
225
238
|
if (!_broadcastWs) _broadcastWs = _processWiring.broadcastWs;
|
|
226
239
|
if (!_runWaterfallHook) _runWaterfallHook = _processWiring.runWaterfallHook;
|
|
240
|
+
if (!_onAgentLoopStart) _onAgentLoopStart = _processWiring.onAgentLoopStart;
|
|
241
|
+
if (!_onAgentLoopEnd) _onAgentLoopEnd = _processWiring.onAgentLoopEnd;
|
|
227
242
|
}
|
|
228
243
|
|
|
229
244
|
// Process-level SDK LAUNCH-CONFIG template (2026-10-12). cli.js registers it once at boot
|
|
@@ -402,7 +417,18 @@ function _createSession(key, cwd) {
|
|
|
402
417
|
// second Stop would silently no-op).
|
|
403
418
|
let _settleGraceTimer = null; // backstop after interrupt: force-settle if no result arrives
|
|
404
419
|
let _lostResultTimer = null; // lost-result backstop handle (cleared on turn settle/teardown)
|
|
405
|
-
|
|
420
|
+
// Generation guard for late results (replaces the old _lateResultTombstones counter).
|
|
421
|
+
// Result frames carry no turn id, so a force-settled turn's late result is
|
|
422
|
+
// indistinguishable from the NEXT turn's result on arrival order alone. The counter
|
|
423
|
+
// solved this by counting "results still owed", but it only reset on teardown/reset —
|
|
424
|
+
// so when the owed result NEVER arrived (the lost-result case), the count outlived its
|
|
425
|
+
// turn and swallowed the NEXT turn's result (no shift, no turn_end → that turn then
|
|
426
|
+
// force-settled as turn-stuck too). The guard instead binds the debt to the force-settled
|
|
427
|
+
// turn's epoch: a result is discarded only while it provably belongs to the dead
|
|
428
|
+
// generation. Once a NEWER turn is in flight, its own result pairs normally — the stale
|
|
429
|
+
// debt is dropped (the old result may never come, and must not tax the live turn).
|
|
430
|
+
let _turnEpoch = 0; // advanced per _beginTurn; rec.epoch snapshots it
|
|
431
|
+
let _lateResultEpoch = 0; // epoch of the force-settled turn awaiting a late result (0 = none)
|
|
406
432
|
let _consecutiveDeaths = 0; // backstop against respawn storms in a broken environment
|
|
407
433
|
// Activity probe (replaces the old fixed 25min silence watchdog): re-armed on every
|
|
408
434
|
// incoming SDK message. When a turn stays silent past ACTIVITY_PROBE_IDLE_MS it pings
|
|
@@ -414,6 +440,11 @@ function _createSession(key, cwd) {
|
|
|
414
440
|
let _activityProbe = null;
|
|
415
441
|
let _probeFailures = 0;
|
|
416
442
|
let _probeEpoch = 0; // bumped on any message/interrupt → invalidates an in-flight probe
|
|
443
|
+
// Monotonic inbound-message counter, bumped ONCE per received SDK message (not by
|
|
444
|
+
// watchdog arms/clears like _probeEpoch). The lost-result backstop snapshots it at
|
|
445
|
+
// arm time: any message arriving afterwards proves the head turn is producing work,
|
|
446
|
+
// so a mis-armed conviction (uncorrelatable-idle hazard) disarms instead of firing.
|
|
447
|
+
let _inboundMsgSeq = 0;
|
|
417
448
|
// Suspect generation: set when an interrupt grace expires with no result (the CLI never
|
|
418
449
|
// answered our Stop) or a probe convicts the child. The wedged query is torn down LAZILY
|
|
419
450
|
// — on the next push rather than at conviction time — so we never SIGTERM a child that
|
|
@@ -437,6 +468,12 @@ function _createSession(key, cwd) {
|
|
|
437
468
|
// reconnect replay like _lastInitSnapshot; reset alongside it on switch/reset.
|
|
438
469
|
let _lastSessionState = null;
|
|
439
470
|
let _stateHeartbeat = null; // setInterval re-broadcasting sdk-session-state while a turn is active
|
|
471
|
+
// Message-activity coalescer state (see ACTIVITY_MIN_INTERVAL_MS): pending throttled
|
|
472
|
+
// activity frame plus the wall-clock turn stamp used as the wire-level turnStartedAt.
|
|
473
|
+
let _lastActivityAt = 0;
|
|
474
|
+
let _activityPending = null; // { kind, rec }
|
|
475
|
+
let _activityTimer = null;
|
|
476
|
+
let _lastTurnStartedAt = 0; // wire turnStartedAt for frames emitted with no head turn (idle settles)
|
|
440
477
|
let _lastContextUsage = null; // last sdk-context-usage frame for WS reconnect replay
|
|
441
478
|
let _ctxUsageFetching = false; // one getContextUsage control request in flight
|
|
442
479
|
let _ctxUsageQueued = false; // a turn-end/compact arrived while in flight → rerun once (coalesce)
|
|
@@ -514,9 +551,13 @@ function _createSession(key, cwd) {
|
|
|
514
551
|
const rec = {
|
|
515
552
|
id: 'turn_' + (++_turnSeq).toString(36) + '_' + Math.random().toString(36).slice(2, 8),
|
|
516
553
|
resolve, interrupted: false, settled: false,
|
|
554
|
+
epoch: ++_turnEpoch, // generation this turn belongs to — the late-result guard keys off it
|
|
555
|
+
startedAt: Date.now(), // wall-clock turn stamp — the wire-level turnStartedAt (restart-safe, unlike the counter epoch)
|
|
556
|
+
loopStarted: false, loopEnded: false, // idempotence for the agent_loop boundary callbacks
|
|
517
557
|
apiRetry: { count: 0, attempt: 0, maxRetries: 0, lastStatus: null, lastError: null },
|
|
518
558
|
};
|
|
519
559
|
_pendingTurns.push(rec);
|
|
560
|
+
_lastTurnStartedAt = rec.startedAt;
|
|
520
561
|
return { rec, promise };
|
|
521
562
|
}
|
|
522
563
|
|
|
@@ -552,17 +593,38 @@ function _createSession(key, cwd) {
|
|
|
552
593
|
* pending timer, and the counter is reset by a successful/answered probe (or a full
|
|
553
594
|
* teardown/reset) — otherwise conviction at MAX_FAILURES would be unreachable. */
|
|
554
595
|
function _armTurnWatchdog() {
|
|
596
|
+
// NOTE: cancel + bump the epoch FIRST (any arm is triggered by observed activity
|
|
597
|
+
// and must invalidate an in-flight probe), then decide whether a timer is needed.
|
|
555
598
|
_clearTurnWatchdog();
|
|
556
599
|
if (_pendingTurns.length === 0) return;
|
|
600
|
+
if (_pendingApprovals.size > 0) {
|
|
601
|
+
// Approval pending = the child is parked awaiting our control response, which
|
|
602
|
+
// itself proves liveness — do not even probe. No timer is armed here; probing
|
|
603
|
+
// resumes via _rearmWatchdogAfterApprovalSettled when the last approval settles.
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
557
606
|
_activityProbe = setTimeout(() => {
|
|
558
607
|
_activityProbe = null;
|
|
559
608
|
if (_pendingTurns.length === 0) return;
|
|
560
|
-
if (_pendingApprovals.size > 0) { _armTurnWatchdog(); return; } // approval
|
|
609
|
+
if (_pendingApprovals.size > 0) { _armTurnWatchdog(); return; } // approval parked mid-wait — stand down
|
|
561
610
|
_runActivityProbe();
|
|
562
611
|
}, _probeFailures === 0 ? ACTIVITY_PROBE_IDLE_MS : ACTIVITY_PROBE_INTERVAL_MS);
|
|
563
612
|
if (_activityProbe.unref) _activityProbe.unref();
|
|
564
613
|
}
|
|
565
614
|
|
|
615
|
+
/** Re-arm the activity probe after the LAST pending approval settles (answered,
|
|
616
|
+
* cancelled, or timed out). _armTurnWatchdog deliberately arms NO timer while an
|
|
617
|
+
* approval is parked (the park itself proves liveness), so without this hook a
|
|
618
|
+
* child that wedges AFTER the approval clears would never be probed again — the
|
|
619
|
+
* watchdog would be permanently disarmed for the rest of the turn. Only re-arm
|
|
620
|
+
* when the approval map just emptied; a still-pending sibling approval keeps the
|
|
621
|
+
* park (and its liveness guarantee) intact. */
|
|
622
|
+
function _rearmWatchdogAfterApprovalSettled() {
|
|
623
|
+
if (_pendingTurns.length === 0) return;
|
|
624
|
+
if (_pendingApprovals.size > 0) return;
|
|
625
|
+
_armTurnWatchdog();
|
|
626
|
+
}
|
|
627
|
+
|
|
566
628
|
function _clearTurnWatchdog() {
|
|
567
629
|
if (_activityProbe) { clearTimeout(_activityProbe); _activityProbe = null; }
|
|
568
630
|
// Any arm/clear is triggered by observed activity (a message re-arms on every frame,
|
|
@@ -642,11 +704,15 @@ function _createSession(key, cwd) {
|
|
|
642
704
|
// and leaving them would replay a ghost modal on attach AND suppress probing on the
|
|
643
705
|
// rebuilt generation (size > 0 = "alive"). They time out / deny as their own path.
|
|
644
706
|
_cancelAllApprovals();
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
|
|
707
|
+
// Mark the result that a merely-slow child may still emit before the lazy teardown —
|
|
708
|
+
// otherwise it would fire a spurious turn_end for an already-failed turn. Bound to the
|
|
709
|
+
// head turn's epoch so it cannot tax a later turn if the result never arrives.
|
|
710
|
+
if (_pendingTurns[0]) _lateResultEpoch = _pendingTurns[0].epoch;
|
|
711
|
+
const stuckRecs = [..._pendingTurns];
|
|
648
712
|
_settleAllTurns();
|
|
649
|
-
|
|
713
|
+
for (const r of stuckRecs) _emitAgentLoopEnd(r, 'turn-stuck');
|
|
714
|
+
_announceSessionState('idle', stalledRec || null);
|
|
715
|
+
_clearActivity();
|
|
650
716
|
// Retry is only possible if a session id was captured to resume — a first turn that
|
|
651
717
|
// wedges before any init frame has nothing to continue, so don't offer [retry].
|
|
652
718
|
const retryable = !!(_sessionId || _launchResume);
|
|
@@ -799,7 +865,10 @@ function _createSession(key, cwd) {
|
|
|
799
865
|
// The CLI's own session_state_changed(running) arrives later and is idempotent
|
|
800
866
|
// against this — on CLIs that never emit it (no env gate / older build) this is
|
|
801
867
|
// the only "on" signal, paired with the result-idle broadcast in _processMessage.
|
|
802
|
-
_announceSessionState('running');
|
|
868
|
+
_announceSessionState('running', rec);
|
|
869
|
+
// Authoritative loop start (voice/plugin/SSE agent_loop_start) — fires once, only
|
|
870
|
+
// after the push is accepted (queued/undelivered messages never ring).
|
|
871
|
+
_emitAgentLoopStart(rec);
|
|
803
872
|
_armTurnWatchdog();
|
|
804
873
|
return { started: true, promise, turnId: rec.id };
|
|
805
874
|
}
|
|
@@ -1078,7 +1147,9 @@ function _createSession(key, cwd) {
|
|
|
1078
1147
|
// so a probe that timed out on the PREVIOUS quiet stretch cannot convict a turn
|
|
1079
1148
|
// that has since demonstrably produced output.
|
|
1080
1149
|
_probeFailures = 0;
|
|
1150
|
+
_inboundMsgSeq++;
|
|
1081
1151
|
_armTurnWatchdog(); // reset the activity probe on observed activity
|
|
1152
|
+
_noteActivity(msg); // coalesced UI liveness (≤1/s sdk-session-state activity frame)
|
|
1082
1153
|
}
|
|
1083
1154
|
_onResidentDeath(query, inputQueue, null);
|
|
1084
1155
|
} catch (err) {
|
|
@@ -1111,12 +1182,20 @@ function _createSession(key, cwd) {
|
|
|
1111
1182
|
_clearGraceTimer();
|
|
1112
1183
|
_clearTurnWatchdog();
|
|
1113
1184
|
_clearStateHeartbeat();
|
|
1114
|
-
|
|
1185
|
+
_clearActivity();
|
|
1186
|
+
// Clear any stale late-result debt: the dead generation's frames are filtered by the
|
|
1187
|
+
// _activeQuery guard, so the owed result can never arrive — leaving the debt would
|
|
1188
|
+
// suppress the next generation's api_retry frames (and its retry affordance).
|
|
1189
|
+
_lateResultEpoch = 0;
|
|
1115
1190
|
const hadTurns = _pendingTurns.length > 0;
|
|
1116
1191
|
const headTurn = _pendingTurns[0] || null;
|
|
1117
1192
|
// Session died mid-turn: no result will arrive, so without this the spinner would
|
|
1118
|
-
// hang forever. The turn is unambiguously over.
|
|
1119
|
-
|
|
1193
|
+
// hang forever. The turn is unambiguously over. `headTurn` stamps the idle with the
|
|
1194
|
+
// dead turn's id, and every pending turn reports its loop end (session-ended).
|
|
1195
|
+
_announceSessionState('idle', headTurn);
|
|
1196
|
+
// Only a turn that actually STARTED reports an end — a record that never reached the
|
|
1197
|
+
// push (construction raced with this death) must not emit an end with no matching start.
|
|
1198
|
+
if (hadTurns) { for (const rec of _pendingTurns) { if (rec.loopStarted) _emitAgentLoopEnd(rec, 'session-ended'); } }
|
|
1120
1199
|
if (err && err.name !== 'AbortError') {
|
|
1121
1200
|
console.error('[SDK] Resident query error:', err.message);
|
|
1122
1201
|
_notifyQueryError(err);
|
|
@@ -1184,11 +1263,19 @@ function _createSession(key, cwd) {
|
|
|
1184
1263
|
_clearTurnWatchdog();
|
|
1185
1264
|
_probeFailures = 0;
|
|
1186
1265
|
_clearStateHeartbeat();
|
|
1187
|
-
|
|
1266
|
+
_clearActivity();
|
|
1267
|
+
// Teardown filters the dead generation's late frames via _activeQuery, so its owed
|
|
1268
|
+
// result can never arrive — clear the debt or it would suppress the rebuilt turn's
|
|
1269
|
+
// api_retry frames. (_lastFailedTurn survives on purpose; the debt must not.)
|
|
1270
|
+
_lateResultEpoch = 0;
|
|
1188
1271
|
// NOTE: _lastFailedTurn deliberately survives teardown — the lazy rebuild path
|
|
1189
1272
|
// (_startTurn's suspect-generation teardown) must keep the recorded failure so the
|
|
1190
1273
|
// manual [retry] affordance stays available. User-facing session boundaries clear it
|
|
1191
1274
|
// instead: _resetFullState / _switchToSession (which funnel close/teardown/stop).
|
|
1275
|
+
// Teardown is also a loop boundary: any turn that STARTED must report its end, or
|
|
1276
|
+
// SSE clients / onAgentLoopEnd plugins see a start with no end (killed workspace,
|
|
1277
|
+
// session switch mid-turn). Guard on loopStarted so a never-pushed record stays silent.
|
|
1278
|
+
for (const rec of _pendingTurns) { if (rec.loopStarted) _emitAgentLoopEnd(rec, 'session-ended'); }
|
|
1192
1279
|
_settleAllTurns();
|
|
1193
1280
|
if (inputQueue) inputQueue.close();
|
|
1194
1281
|
if (query) {
|
|
@@ -1285,17 +1372,87 @@ function _createSession(key, cwd) {
|
|
|
1285
1372
|
* Announce a turn-level session state to WS clients (spinner source in SDK mode).
|
|
1286
1373
|
* State-change guarded + retained for reconnect replay. Broadcast failures are
|
|
1287
1374
|
* swallowed inside this helper — the message loop must never die on a UI signal.
|
|
1375
|
+
*
|
|
1376
|
+
* Every frame carries turn correlation (turnId + turnStartedAt): the CLI's
|
|
1377
|
+
* session_state_changed has no turn id of its own, so a late frame from the PREVIOUS
|
|
1378
|
+
* turn (e.g. its idle arriving after a fresh push) would otherwise clear the new
|
|
1379
|
+
* turn's spinner. `rec` is the turn this state belongs to — internal settle paths
|
|
1380
|
+
* pass it explicitly because the FIFO head may already have shifted.
|
|
1288
1381
|
*/
|
|
1289
|
-
function _announceSessionState(state) {
|
|
1382
|
+
function _announceSessionState(state, rec = null) {
|
|
1290
1383
|
const valid = state === 'idle' || state === 'running' || state === 'requires_action';
|
|
1291
1384
|
if (!valid || state === _lastSessionState) return;
|
|
1292
1385
|
_lastSessionState = state;
|
|
1386
|
+
const owner = rec || _pendingTurns[0] || null;
|
|
1293
1387
|
// _broadcast carries the routing envelope (cwd + sessionId + instanceKey) — every
|
|
1294
1388
|
// frame needs it so concurrent sessions' clients filter correctly.
|
|
1295
|
-
_broadcast({
|
|
1389
|
+
_broadcast({
|
|
1390
|
+
type: 'sdk-session-state', state,
|
|
1391
|
+
turnId: owner ? owner.id : null,
|
|
1392
|
+
turnStartedAt: owner ? owner.startedAt : _lastTurnStartedAt,
|
|
1393
|
+
});
|
|
1296
1394
|
_syncStateHeartbeat();
|
|
1297
1395
|
}
|
|
1298
1396
|
|
|
1397
|
+
/** One-shot agent-loop boundary callbacks (voice/plugin/SSE agent_loop_*). Idempotent
|
|
1398
|
+
* per turn record — every settle path (result, interrupt grace, lost result, stuck
|
|
1399
|
+
* recovery, death) funnels through _emitAgentLoopEnd exactly once. Callback throws are
|
|
1400
|
+
* isolated so a broken host can never wedge the message loop. */
|
|
1401
|
+
function _emitAgentLoopStart(rec) {
|
|
1402
|
+
if (!rec || rec.loopStarted) return;
|
|
1403
|
+
rec.loopStarted = true;
|
|
1404
|
+
if (!_onAgentLoopStart) return;
|
|
1405
|
+
try { _onAgentLoopStart({ sessionId: _sessionId || null, ts: Date.now(), cwd: _cwd, instanceKey, turnId: rec.id, turnStartedAt: rec.startedAt }); }
|
|
1406
|
+
catch (err) { reportSwallowed('sdk-manager.agent-loop-start', err); }
|
|
1407
|
+
}
|
|
1408
|
+
function _emitAgentLoopEnd(rec, reason) {
|
|
1409
|
+
if (!rec || rec.loopEnded) return;
|
|
1410
|
+
rec.loopEnded = true;
|
|
1411
|
+
if (!_onAgentLoopEnd) return;
|
|
1412
|
+
try { _onAgentLoopEnd({ sessionId: _sessionId || null, ts: Date.now(), cwd: _cwd, instanceKey, turnId: rec.id, turnStartedAt: rec.startedAt, reason }); }
|
|
1413
|
+
catch (err) { reportSwallowed('sdk-manager.agent-loop-end', err); }
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** Coalesced message-level liveness: any non-boundary SDK message (assistant/user/
|
|
1417
|
+
* tool_progress / system task_* frames / status / api_retry / compact …) proves the
|
|
1418
|
+
* loop is alive mid-turn — including tool/compaction/hook phases the proxy's per-API
|
|
1419
|
+
* streaming_status cannot see. Frames reuse the sdk-session-state shape with
|
|
1420
|
+
* heartbeat+activity flags (no new client handler) and are capped at one per
|
|
1421
|
+
* ACTIVITY_MIN_INTERVAL_MS. */
|
|
1422
|
+
function _noteActivity(msg) {
|
|
1423
|
+
if (_pendingTurns.length === 0 || !msg) return;
|
|
1424
|
+
const t = msg.type;
|
|
1425
|
+
if (t === 'result') return; // boundary, not progress
|
|
1426
|
+
if (t === 'system' && msg.subtype === 'session_state_changed') return; // state frame, not activity
|
|
1427
|
+
const rec = _pendingTurns[0];
|
|
1428
|
+
const kind = t === 'system' ? `system:${msg.subtype || ''}`
|
|
1429
|
+
: t === 'stream_event' ? `stream:${(msg.event && msg.event.type) || ''}` : t;
|
|
1430
|
+
_activityPending = { kind, rec };
|
|
1431
|
+
const wait = Math.max(0, ACTIVITY_MIN_INTERVAL_MS - (Date.now() - _lastActivityAt));
|
|
1432
|
+
if (wait === 0) _flushActivity();
|
|
1433
|
+
else if (!_activityTimer) {
|
|
1434
|
+
_activityTimer = setTimeout(_flushActivity, wait);
|
|
1435
|
+
if (_activityTimer.unref) _activityTimer.unref();
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
function _flushActivity() {
|
|
1439
|
+
if (_activityTimer) { clearTimeout(_activityTimer); _activityTimer = null; }
|
|
1440
|
+
const p = _activityPending; _activityPending = null;
|
|
1441
|
+
if (!p || p.rec.settled || _pendingTurns[0] !== p.rec) return;
|
|
1442
|
+
if (_lastSessionState !== 'running' && _lastSessionState !== 'requires_action') return;
|
|
1443
|
+
_lastActivityAt = Date.now();
|
|
1444
|
+
_broadcast({
|
|
1445
|
+
type: 'sdk-session-state', state: _lastSessionState, heartbeat: true, activity: true,
|
|
1446
|
+
kind: p.kind, turnId: p.rec.id, turnStartedAt: p.rec.startedAt,
|
|
1447
|
+
pendingApprovals: _pendingApprovals.size,
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
function _clearActivity() {
|
|
1451
|
+
if (_activityTimer) { clearTimeout(_activityTimer); _activityTimer = null; }
|
|
1452
|
+
_activityPending = null;
|
|
1453
|
+
_lastActivityAt = 0;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1299
1456
|
// The in-flight turn is the FIFO HEAD (the result branch shifts from the head).
|
|
1300
1457
|
const _currentTurnId = () => (_pendingTurns.length ? _pendingTurns[0].id : null);
|
|
1301
1458
|
|
|
@@ -1309,9 +1466,11 @@ function _createSession(key, cwd) {
|
|
|
1309
1466
|
if (active && !_stateHeartbeat) {
|
|
1310
1467
|
_stateHeartbeat = setInterval(() => {
|
|
1311
1468
|
if (_lastSessionState !== 'running' && _lastSessionState !== 'requires_action') return;
|
|
1469
|
+
const head = _pendingTurns[0] || null;
|
|
1312
1470
|
_broadcast({
|
|
1313
1471
|
type: 'sdk-session-state', state: _lastSessionState, heartbeat: true,
|
|
1314
|
-
turnId: _currentTurnId(),
|
|
1472
|
+
turnId: _currentTurnId(), turnStartedAt: head ? head.startedAt : _lastTurnStartedAt,
|
|
1473
|
+
pendingApprovals: _pendingApprovals.size,
|
|
1315
1474
|
});
|
|
1316
1475
|
}, STATE_HEARTBEAT_MS);
|
|
1317
1476
|
if (_stateHeartbeat.unref) _stateHeartbeat.unref();
|
|
@@ -1341,24 +1500,42 @@ function _createSession(key, cwd) {
|
|
|
1341
1500
|
// which cli.js injects into the child env; on CLIs that never emit it the
|
|
1342
1501
|
// push/result fallbacks keep the spinner correct at result granularity.
|
|
1343
1502
|
if (msg.subtype === 'session_state_changed') {
|
|
1344
|
-
|
|
1345
|
-
//
|
|
1346
|
-
//
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
|
|
1503
|
+
// Late-idle suppression (LOAD-BEARING gate): session_state_changed carries no
|
|
1504
|
+
// turn id, so an idle that settles the PREVIOUS turn can arrive after a fresh
|
|
1505
|
+
// push. If the head turn has never been seen running/requires_action by the CLI,
|
|
1506
|
+
// this idle cannot belong to it — drop it, or it would kill the new turn's
|
|
1507
|
+
// spinner. The frontend's turnStartedAt guard is NOT a substitute here: a stale
|
|
1508
|
+
// idle processed AFTER the new push is stamped with the NEW head's epoch, so the
|
|
1509
|
+
// client cannot tell it apart — only this server-side cliStateSeen gate can (the
|
|
1510
|
+
// client guard only helps against transport-level reordering of older-stamped
|
|
1511
|
+
// frames). Residual hazard (new turn's running lands before the old idle): the
|
|
1512
|
+
// gate passes it; the lost-result backstop's armedSeq check below disarms the
|
|
1513
|
+
// resulting mis-armed conviction when the head turn is still producing messages.
|
|
1514
|
+
const head = _pendingTurns[0] || null;
|
|
1515
|
+
const staleIdle = msg.state === 'idle' && head && !head.cliStateSeen;
|
|
1516
|
+
if (!staleIdle) {
|
|
1517
|
+
_announceSessionState(msg.state, head);
|
|
1518
|
+
// A CLI-emitted running/requires_action proves the head turn is genuinely
|
|
1519
|
+
// in-flight on the child (vs a freshly-drained turn whose own CLI frames have
|
|
1520
|
+
// not arrived yet). Lost-result only arms for such a turn — a LATE idle frame
|
|
1521
|
+
// from the previous turn must not convict the just-pushed one.
|
|
1522
|
+
if ((msg.state === 'running' || msg.state === 'requires_action') && head) head.cliStateSeen = true;
|
|
1523
|
+
// Lost-result backstop: the CLI says the turn settled (idle) but no result
|
|
1524
|
+
// message arrived for our pending FIFO. Without this _queryBusy would wedge
|
|
1525
|
+
// forever now that the 25min silence watchdog is gone. Give the result a short
|
|
1526
|
+
// grace to arrive first (frame ordering within a turn end is not guaranteed),
|
|
1527
|
+
// then settle as a retryable failure.
|
|
1528
|
+
if (msg.state === 'idle' && head && head.cliStateSeen) _armLostResultBackstop();
|
|
1529
|
+
}
|
|
1356
1530
|
} else if (msg.subtype === 'api_retry') {
|
|
1357
1531
|
// The CLI's own retry of a retryable API error (502/429/5xx/network). It carries
|
|
1358
1532
|
// no turn id, so we attribute it to the head pending turn — approximate but
|
|
1359
|
-
// correct in the common single-turn case. Skipped while a
|
|
1360
|
-
//
|
|
1361
|
-
|
|
1533
|
+
// correct in the common single-turn case. Skipped only while a late-result debt
|
|
1534
|
+
// is pending AND no live turn could own the frame (a late retry of an
|
|
1535
|
+
// already-interrupted turn must not relight the bar). The gate mirrors the
|
|
1536
|
+
// result branch's orphan rule: a stale debt must never suppress a live turn's
|
|
1537
|
+
// own retry frames, and must not outlive the teardown/reset that orphaned it.
|
|
1538
|
+
if (_lateResultEpoch === 0 || _pendingTurns.length > 0) {
|
|
1362
1539
|
const rec = _pendingTurns.length ? _pendingTurns[0] : null;
|
|
1363
1540
|
if (rec) {
|
|
1364
1541
|
rec.apiRetry.count += 1;
|
|
@@ -1443,26 +1620,49 @@ function _createSession(key, cwd) {
|
|
|
1443
1620
|
|
|
1444
1621
|
case 'result': {
|
|
1445
1622
|
if (msg.session_id) _sessionId = msg.session_id;
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1623
|
+
// Background-task wake results are NOT turn boundaries: while local_agent /
|
|
1624
|
+
// local_workflow tasks are active the CLI holds the turn's real result back, and
|
|
1625
|
+
// each answered task notification can yield a synthetic result (num_turns: 0,
|
|
1626
|
+
// empty result). Treating one as end-of-turn fires a phantom turn_end (and would
|
|
1627
|
+
// ring the voice pack / clear the spinner mid-loop), so skip it entirely.
|
|
1628
|
+
if (msg.origin && msg.origin.kind === 'task-notification') break;
|
|
1629
|
+
// Generation guard for late results. Result frames carry no turn id, so a result
|
|
1630
|
+
// that arrives AFTER its turn was force-settled is indistinguishable from the NEXT
|
|
1631
|
+
// turn's result on arrival order alone. The guard resolves this by generation:
|
|
1632
|
+
// a force-settled turn is REMOVED from the FIFO (shift / settleAll), so when a
|
|
1633
|
+
// result arrives and the FIFO is EMPTY, nothing live could own it — it is the dead
|
|
1634
|
+
// generation's late result and is discarded (no turn_end, no error attribution).
|
|
1635
|
+
//
|
|
1636
|
+
// Deliberate trade-off (NOT a guarantee): when a NEWER turn is already in flight,
|
|
1637
|
+
// an arriving result is paired with it (`head.epoch > _lateResultEpoch`), because
|
|
1638
|
+
// swallowing the live turn's own result is the worse failure (that was the original
|
|
1639
|
+
// bug — the stuck turn got mis-settled as turn-stuck and re-ran). The cost is that
|
|
1640
|
+
// a genuinely-late result of the dead generation CAN be mis-paired to the live turn
|
|
1641
|
+
// here (an early/extra turn_end, or a wrong error toast attributed to it) — accepted
|
|
1642
|
+
// because the live turn must not lose its own result. This window is narrow: the
|
|
1643
|
+
// late result must arrive after the backstop AND after a newer turn has started.
|
|
1644
|
+
if (_lateResultEpoch !== 0) {
|
|
1645
|
+
const head = _pendingTurns[0] || null;
|
|
1646
|
+
if (!head) {
|
|
1647
|
+
// No live turn could own this result — it is the dead generation's late result.
|
|
1648
|
+
_lateResultEpoch = 0;
|
|
1649
|
+
_clearGraceTimer();
|
|
1650
|
+
_queryBusy = false;
|
|
1651
|
+
_turnInFlight = false;
|
|
1652
|
+
// The dead generation is over — but a NEWER turn may already be in flight
|
|
1653
|
+
// (Stop → grace settle → user re-sent → late result of the old turn arrives).
|
|
1654
|
+
// Only announce idle when no turn is pending, else we'd kill turn 2's spinner.
|
|
1655
|
+
if (_pendingTurns.length === 0) {
|
|
1656
|
+
_announceSessionState('idle');
|
|
1657
|
+
_clearTurnWatchdog();
|
|
1658
|
+
_clearActivity();
|
|
1659
|
+
}
|
|
1660
|
+
_maybeDrain();
|
|
1661
|
+
break;
|
|
1463
1662
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1663
|
+
// A NEWER generation is in flight: this result is that live turn's own, so fall
|
|
1664
|
+
// through to pair it normally — and drop the never-arriving late result's debt.
|
|
1665
|
+
_lateResultEpoch = 0;
|
|
1466
1666
|
}
|
|
1467
1667
|
const rec = _pendingTurns.shift();
|
|
1468
1668
|
// 502/retry state machine. An error result after observed CLI retries records a
|
|
@@ -1496,7 +1696,12 @@ function _createSession(key, cwd) {
|
|
|
1496
1696
|
// in CLI mode — fires once per user-prompt response when the whole chain
|
|
1497
1697
|
// (assistant text + all tool calls + final reply) completes. SDK mode
|
|
1498
1698
|
// doesn't go through ensureHooks() so this in-process callback is the
|
|
1499
|
-
// only way the renderer learns the turn is over.
|
|
1699
|
+
// only way the renderer learns the turn is over. NOTE: deliberately NOT
|
|
1700
|
+
// gated on `rec` — the late-result trade-off (see the generation-guard
|
|
1701
|
+
// comment above) pairs a dead turn's late result with the live turn, and
|
|
1702
|
+
// the live turn's OWN result can then arrive with an empty FIFO; phantom
|
|
1703
|
+
// ringing from background tasks is prevented by the task-notification
|
|
1704
|
+
// filter above, not by requiring a FIFO record.
|
|
1500
1705
|
if (_onTurnEnd) {
|
|
1501
1706
|
// Per-workspace (2026-10): carry the session's own cwd so the server
|
|
1502
1707
|
// can route turn_end to THIS workspace's SSE scope (SDK mode has no
|
|
@@ -1505,13 +1710,23 @@ function _createSession(key, cwd) {
|
|
|
1505
1710
|
catch (err) { console.warn('[sdk-manager] onTurnEnd threw:', err?.message); }
|
|
1506
1711
|
}
|
|
1507
1712
|
if (rec) _settleTurn(rec);
|
|
1713
|
+
// Authoritative loop end for THIS turn — immediate, per-turn (turnId/turnStartedAt
|
|
1714
|
+
// correlated), unlike the debounced turn_end above. `is_error`/non-success
|
|
1715
|
+
// subtypes distinguish error ends; interrupted turns report their own reason.
|
|
1716
|
+
if (rec) {
|
|
1717
|
+
const errored = msg.is_error === true || (msg.subtype && msg.subtype !== 'success');
|
|
1718
|
+
_emitAgentLoopEnd(rec, rec.interrupted ? 'interrupted' : (errored ? 'error' : 'success'));
|
|
1719
|
+
}
|
|
1508
1720
|
_clearGraceTimer();
|
|
1721
|
+
_clearActivity();
|
|
1509
1722
|
_queryBusy = false;
|
|
1510
1723
|
_turnInFlight = false;
|
|
1511
1724
|
// Turn-level spinner clear at result time — immediate, unlike the debounced
|
|
1512
1725
|
// `turn_end` SSE (voice-pack semantics). Idempotent against a real
|
|
1513
|
-
// session_state_changed(idle) arriving before/after.
|
|
1514
|
-
|
|
1726
|
+
// session_state_changed(idle) arriving before/after. `rec` is passed explicitly:
|
|
1727
|
+
// the FIFO head has already shifted, so the default head lookup would stamp this
|
|
1728
|
+
// idle with the NEXT turn's id.
|
|
1729
|
+
_announceSessionState('idle', rec);
|
|
1515
1730
|
if (_pendingTurns.length === 0) _clearTurnWatchdog();
|
|
1516
1731
|
_maybeDrain();
|
|
1517
1732
|
// Refresh the context-usage panel AFTER the turn fully settles (fire-and-forget,
|
|
@@ -1988,6 +2203,7 @@ function _createSession(key, cwd) {
|
|
|
1988
2203
|
// Close the modal on every client — a silent deny would leave it hanging forever.
|
|
1989
2204
|
_broadcastApprovalDismiss(kind, id, 'timeout');
|
|
1990
2205
|
resolve(null);
|
|
2206
|
+
_rearmWatchdogAfterApprovalSettled();
|
|
1991
2207
|
}, timeoutMs);
|
|
1992
2208
|
const entry = {
|
|
1993
2209
|
kind, // 'ask' | 'plan' | 'perm' — lets cancelApproval distinguish types so an
|
|
@@ -2006,6 +2222,7 @@ function _createSession(key, cwd) {
|
|
|
2006
2222
|
if (cur?.auto) cancelAutoApproveState(cur.auto);
|
|
2007
2223
|
_pendingApprovals.delete(id);
|
|
2008
2224
|
resolve(value);
|
|
2225
|
+
_rearmWatchdogAfterApprovalSettled();
|
|
2009
2226
|
},
|
|
2010
2227
|
};
|
|
2011
2228
|
_pendingApprovals.set(id, entry);
|
|
@@ -2149,14 +2366,16 @@ function _createSession(key, cwd) {
|
|
|
2149
2366
|
const rec = _pendingTurns[0];
|
|
2150
2367
|
if (!rec || !rec.interrupted) return;
|
|
2151
2368
|
console.warn('[sdk-manager] no result within interrupt grace — settling the turn and marking the generation suspect');
|
|
2152
|
-
|
|
2369
|
+
_lateResultEpoch = rec.epoch; // discard this generation's late result, never a later turn's
|
|
2153
2370
|
_pendingTurns.shift();
|
|
2154
2371
|
_settleTurn(rec);
|
|
2372
|
+
_emitAgentLoopEnd(rec, 'interrupt-timeout');
|
|
2155
2373
|
_queryBusy = false;
|
|
2156
2374
|
_suspectGeneration = true;
|
|
2157
2375
|
// The CLI never answered the interrupt — no result is coming, so clear the
|
|
2158
2376
|
// spinner here rather than waiting on a dead signal.
|
|
2159
|
-
_announceSessionState('idle');
|
|
2377
|
+
_announceSessionState('idle', rec);
|
|
2378
|
+
_clearActivity();
|
|
2160
2379
|
if (_pendingTurns.length === 0) _clearTurnWatchdog();
|
|
2161
2380
|
_maybeDrain();
|
|
2162
2381
|
}, INTERRUPT_GRACE_MS);
|
|
@@ -2174,17 +2393,32 @@ function _createSession(key, cwd) {
|
|
|
2174
2393
|
// backstop from turn A must never settle turn B (which may be parked on the SAME idle
|
|
2175
2394
|
// state). The interrupt-grace path owns user-cancelled turns — skip those here.
|
|
2176
2395
|
const armedRec = _pendingTurns[0] || null;
|
|
2396
|
+
const armedSeq = _inboundMsgSeq; // any message arriving after arming bumps this
|
|
2177
2397
|
_lostResultTimer = setTimeout(() => {
|
|
2178
2398
|
_lostResultTimer = null;
|
|
2179
2399
|
const rec = _pendingTurns[0];
|
|
2180
2400
|
if (!rec || rec !== armedRec || rec.interrupted) return; // stale arm / different turn / user-cancelled
|
|
2181
2401
|
if (_queryBusy === false) return;
|
|
2182
2402
|
if (_lastSessionState !== 'idle') return; // a newer turn started running — not lost
|
|
2403
|
+
// Mitigation for the uncorrelatable-idle hazard: if the accepted idle actually
|
|
2404
|
+
// belonged to the PREVIOUS turn (the new turn's `running` landed first, so
|
|
2405
|
+
// cliStateSeen was already true and the gate passed it), the head turn may still
|
|
2406
|
+
// be alive. A message arriving after arming proves the head turn is producing
|
|
2407
|
+
// work — not lost. Disarm rather than convict it as turn-stuck. (armedSeq is read
|
|
2408
|
+
// inside _processMessage, BEFORE the drain loop increments the seq for the very
|
|
2409
|
+
// idle that armed us — so exactly ONE bump is expected from the arming message;
|
|
2410
|
+
// anything beyond it is post-arm activity.)
|
|
2411
|
+
if (_inboundMsgSeq > armedSeq + 1) return;
|
|
2183
2412
|
console.warn('[sdk-manager] session idle but a turn result never arrived — settling as lost result');
|
|
2184
|
-
//
|
|
2413
|
+
// Mark the head turn's epoch as owing a result: without it a late result would
|
|
2185
2414
|
// consume the NEXT turn's FIFO record (settling it early and mispairing turn-end).
|
|
2186
|
-
|
|
2415
|
+
// Binding to the epoch (not a count) is the fix for the swallowed-next-result bug —
|
|
2416
|
+
// this result never arriving must not let the debt outlive its generation.
|
|
2417
|
+
if (_pendingTurns[0]) _lateResultEpoch = _pendingTurns[0].epoch;
|
|
2418
|
+
const lostRecs = [..._pendingTurns];
|
|
2187
2419
|
_settleAllTurns();
|
|
2420
|
+
_clearActivity();
|
|
2421
|
+
for (const r of lostRecs) _emitAgentLoopEnd(r, 'lost-result');
|
|
2188
2422
|
const retryable = !!(_sessionId || _launchResume);
|
|
2189
2423
|
_lastFailedTurn = {
|
|
2190
2424
|
turnId: rec.id || null, retryable, apiStatus: rec.apiRetry?.lastStatus ?? null,
|
|
@@ -2424,10 +2658,12 @@ function _createSession(key, cwd) {
|
|
|
2424
2658
|
if (_lastSessionState && _lastSessionState !== 'idle') _announceSessionState('idle');
|
|
2425
2659
|
_lastSessionState = null;
|
|
2426
2660
|
_clearStateHeartbeat();
|
|
2661
|
+
_clearActivity();
|
|
2662
|
+
_lastTurnStartedAt = 0;
|
|
2427
2663
|
_clearContextUsage('session-switched');
|
|
2428
2664
|
_clearMcpStatus('session-switched');
|
|
2429
2665
|
_pendingTurns = [];
|
|
2430
|
-
|
|
2666
|
+
_lateResultEpoch = 0; // session boundary: a stale debt must not follow into the new session
|
|
2431
2667
|
_consecutiveDeaths = 0;
|
|
2432
2668
|
_suspectGeneration = false;
|
|
2433
2669
|
_lastFailedTurn = null;
|
|
@@ -2483,7 +2719,8 @@ function _createSession(key, cwd) {
|
|
|
2483
2719
|
getSdkInitSnapshot: () => _lastInitSnapshot,
|
|
2484
2720
|
getSessionStateSnapshot: () => {
|
|
2485
2721
|
if (!_lastSessionState) return null;
|
|
2486
|
-
|
|
2722
|
+
const head = _pendingTurns[0] || null;
|
|
2723
|
+
return { type: 'sdk-session-state', state: _lastSessionState, sessionId: _sessionId || null, cwd: _cwd, instanceKey, turnId: _currentTurnId(), turnStartedAt: head ? head.startedAt : _lastTurnStartedAt, pendingApprovals: _pendingApprovals.size };
|
|
2487
2724
|
},
|
|
2488
2725
|
getContextUsageSnapshot: () => _lastContextUsage,
|
|
2489
2726
|
getMcpStatusSnapshot: () => _lastMcpStatus,
|
|
@@ -2619,6 +2856,8 @@ export function __isEphemeralHostForTests(cwd) {
|
|
|
2619
2856
|
* @param {function} deps.broadcastWs — (msg) => void, terminal-WS broadcast for approvals
|
|
2620
2857
|
* @param {string} [deps.permissionMode]
|
|
2621
2858
|
* @param {function} [deps.runWaterfallHook] — plugin waterfall (onPlanRequest/onAskRequest/onPermRequest)
|
|
2859
|
+
* @param {function} [deps.onAgentLoopStart] — ({sessionId, ts, cwd, instanceKey, turnId, turnStartedAt}) => void, fired once per accepted turn
|
|
2860
|
+
* @param {function} [deps.onAgentLoopEnd] — (same + reason) => void, fired once at the turn's real settlement
|
|
2622
2861
|
* @param {object} [deps.env] — child env (must already contain ANTHROPIC_BASE_URL → ccv proxy)
|
|
2623
2862
|
* @param {object} [deps.settings] — merged Settings object (env.ANTHROPIC_BASE_URL double-injection)
|
|
2624
2863
|
* @param {string} [deps.model] — --model lifted from user args (options.model)
|
|
@@ -2628,7 +2867,7 @@ export function __isEphemeralHostForTests(cwd) {
|
|
|
2628
2867
|
* must come from the same selection as PTY mode, else the SDK spawns a PATH claude that
|
|
2629
2868
|
* host security tooling (e.g. agent-security headless guards) may SIGKILL on spawn
|
|
2630
2869
|
*/
|
|
2631
|
-
export function initSdkSession(cwd, projectName, { onTurnEnd, onQueryError, broadcastWs, permissionMode, runWaterfallHook, env, settings, model, launchResume, userArgs, claudeExecutable } = {}) {
|
|
2870
|
+
export function initSdkSession(cwd, projectName, { onTurnEnd, onQueryError, broadcastWs, permissionMode, runWaterfallHook, onAgentLoopStart, onAgentLoopEnd, env, settings, model, launchResume, userArgs, claudeExecutable } = {}) {
|
|
2632
2871
|
void projectName; // reserved (display name comes from the wire path)
|
|
2633
2872
|
// Process-global callbacks (cli.js registers one wiring; every session reuses it). Only
|
|
2634
2873
|
// OVERWRITE when a callback is actually passed — a later per-workspace init (the launch
|
|
@@ -2638,6 +2877,8 @@ export function initSdkSession(cwd, projectName, { onTurnEnd, onQueryError, broa
|
|
|
2638
2877
|
if (onQueryError !== undefined) _onQueryError = onQueryError || null;
|
|
2639
2878
|
if (broadcastWs !== undefined) _broadcastWs = broadcastWs;
|
|
2640
2879
|
if (runWaterfallHook !== undefined) _runWaterfallHook = runWaterfallHook || null;
|
|
2880
|
+
if (onAgentLoopStart !== undefined) _onAgentLoopStart = onAgentLoopStart || null;
|
|
2881
|
+
if (onAgentLoopEnd !== undefined) _onAgentLoopEnd = onAgentLoopEnd || null;
|
|
2641
2882
|
// Restore any callback stopSession() cleared from the durable process wiring (cli.js's
|
|
2642
2883
|
// registration). A workspace stop→relaunch omits these callbacks, so without this every
|
|
2643
2884
|
// sdk-* frame would silently stop broadcasting after returning to the workspace list.
|
|
@@ -2819,6 +3060,8 @@ export function stopSession() {
|
|
|
2819
3060
|
_onQueryError = null;
|
|
2820
3061
|
_broadcastWs = null;
|
|
2821
3062
|
_runWaterfallHook = null;
|
|
3063
|
+
_onAgentLoopStart = null;
|
|
3064
|
+
_onAgentLoopEnd = null;
|
|
2822
3065
|
}
|
|
2823
3066
|
|
|
2824
3067
|
/**
|