condukt 0.6.19 → 0.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -180
- package/dist/runtimes/copilot/copilot-backend.d.ts +2 -0
- package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/lifecycle-events.d.ts +9 -4
- package/dist/runtimes/copilot/lifecycle-events.d.ts.map +1 -1
- package/dist/runtimes/copilot/lifecycle-events.js +74 -38
- package/dist/runtimes/copilot/lifecycle-events.js.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.d.ts +1 -1
- package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
- package/dist/runtimes/copilot/sdk-backend.js +309 -69
- package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
- package/dist/runtimes/copilot/subprocess-backend.js +3 -3
- package/dist/runtimes/copilot/subprocess-backend.js.map +1 -1
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +13 -4
- package/dist/src/scheduler.js.map +1 -1
- package/dist/state/reducer.d.ts.map +1 -1
- package/dist/state/reducer.js +5 -0
- package/dist/state/reducer.js.map +1 -1
- package/dist/ui/tool-display/ThinkingSection.js +13 -13
- package/package.json +144 -144
- package/ui/style.css +1 -1
|
@@ -50,6 +50,18 @@ const cp = __importStar(require("child_process"));
|
|
|
50
50
|
const fs = __importStar(require("fs"));
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
52
|
const lifecycle_events_1 = require("./lifecycle-events");
|
|
53
|
+
const NAMED_SDK_EVENTS = new Set([
|
|
54
|
+
'assistant.message', 'assistant.message_delta',
|
|
55
|
+
'assistant.reasoning', 'assistant.reasoning_delta',
|
|
56
|
+
'assistant.intent', 'assistant.usage',
|
|
57
|
+
'tool.execution_start', 'tool.execution_complete',
|
|
58
|
+
'tool.execution_partial_result',
|
|
59
|
+
'session.idle', 'session.task_complete', 'session.error',
|
|
60
|
+
'model.call_failure', 'abort',
|
|
61
|
+
'session.compaction_start', 'session.compaction_complete',
|
|
62
|
+
'subagent.started', 'subagent.completed', 'subagent.failed',
|
|
63
|
+
'permission.requested',
|
|
64
|
+
]);
|
|
53
65
|
// ---------------------------------------------------------------------------
|
|
54
66
|
// PATH hardening: shared logic with SubprocessBackend
|
|
55
67
|
// ---------------------------------------------------------------------------
|
|
@@ -197,8 +209,13 @@ class SdkSession {
|
|
|
197
209
|
timeoutTimer = null;
|
|
198
210
|
heartbeatTimer = null;
|
|
199
211
|
compactionTimer = null;
|
|
212
|
+
abortGraceTimer = null;
|
|
200
213
|
compactionInProgress = false;
|
|
214
|
+
turnSettled = false;
|
|
215
|
+
compactionRecoveryInProgress = false;
|
|
201
216
|
aborted = false;
|
|
217
|
+
_turnText = new Map();
|
|
218
|
+
intentionalAborts = new WeakSet();
|
|
202
219
|
/**
|
|
203
220
|
* Maps toolCallId -> toolName for attributing tool_complete events.
|
|
204
221
|
* Populated from assistant.message toolRequests and tool.execution_start.
|
|
@@ -231,10 +248,21 @@ class SdkSession {
|
|
|
231
248
|
* Matches SubprocessBackend.send() contract: fire-and-forget, events stream via on().
|
|
232
249
|
*/
|
|
233
250
|
send(prompt) {
|
|
251
|
+
if (this.aborted) {
|
|
252
|
+
this.emitError(new Error('Session is aborted'));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// Detach the preceding SDK handle synchronously before resetting shared turn
|
|
256
|
+
// state. Any late events from it then fail the per-handler isActive() guard.
|
|
257
|
+
this.clearTimers();
|
|
258
|
+
void this._cleanup();
|
|
259
|
+
this._turnText.clear();
|
|
260
|
+
this._toolCallNames.clear();
|
|
261
|
+
this._callIdToParent.clear();
|
|
262
|
+
this._pendingPartials.clear();
|
|
263
|
+
this.turnSettled = false;
|
|
234
264
|
this._run(prompt).catch((err) => {
|
|
235
|
-
|
|
236
|
-
this.emitError(err instanceof Error ? err : new Error(String(err)));
|
|
237
|
-
}
|
|
265
|
+
this.fail(err instanceof Error ? err : new Error(String(err)), 'session.run');
|
|
238
266
|
});
|
|
239
267
|
}
|
|
240
268
|
/**
|
|
@@ -286,7 +314,15 @@ class SdkSession {
|
|
|
286
314
|
onPermissionRequest: approveAll,
|
|
287
315
|
workingDirectory: this.config.cwd,
|
|
288
316
|
reasoningEffort: this.config.thinkingBudget,
|
|
317
|
+
// Registered before createSession issues its RPC, closing the early-event
|
|
318
|
+
// gap for session.start and *_loaded events. Named payload handlers are
|
|
319
|
+
// wired after the handle exists; this hook handles class-wide liveness and
|
|
320
|
+
// future failure-shaped events without duplicating named event output.
|
|
321
|
+
onEvent: (e) => this.handleEarlyEvent(e),
|
|
289
322
|
};
|
|
323
|
+
if (this.config.contextTier) {
|
|
324
|
+
sessionConfig.contextTier = this.config.contextTier;
|
|
325
|
+
}
|
|
290
326
|
// CLI 1.0.11+ discovers MCP servers, skills, and custom instructions from
|
|
291
327
|
// configDir. Without this, the CLI searches workingDirectory (which is often
|
|
292
328
|
// a temp execution dir) and fails to find the project's .copilot/ config.
|
|
@@ -317,6 +353,13 @@ class SdkSession {
|
|
|
317
353
|
bufferExhaustionThreshold: 0.90,
|
|
318
354
|
};
|
|
319
355
|
const sdkSession = await client.createSession(sessionConfig);
|
|
356
|
+
if (this.aborted) {
|
|
357
|
+
try {
|
|
358
|
+
await sdkSession.disconnect();
|
|
359
|
+
}
|
|
360
|
+
catch { /* Ignore inert early-abort handle */ }
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
320
363
|
this._sdkSession = sdkSession;
|
|
321
364
|
// Set autopilot mode explicitly (matches SubprocessBackend's --autopilot flag)
|
|
322
365
|
try {
|
|
@@ -326,19 +369,21 @@ class SdkSession {
|
|
|
326
369
|
// SDK may not support mode.set — continue without it
|
|
327
370
|
}
|
|
328
371
|
// ---------------------------------------------------------------
|
|
329
|
-
//
|
|
372
|
+
// Wire SDK events -> CopilotSession events
|
|
373
|
+
// ---------------------------------------------------------------
|
|
374
|
+
this._wireEvents(sdkSession);
|
|
330
375
|
// ---------------------------------------------------------------
|
|
376
|
+
// Set up turn timers only while this handle is still active
|
|
377
|
+
// ---------------------------------------------------------------
|
|
378
|
+
if (this.aborted || this._sdkSession !== sdkSession)
|
|
379
|
+
return;
|
|
331
380
|
this.timeoutTimer = setTimeout(() => {
|
|
332
|
-
this.
|
|
333
|
-
|
|
381
|
+
if (this.aborted || this._sdkSession !== sdkSession)
|
|
382
|
+
return;
|
|
383
|
+
this.fail(new Error(`Session timed out after ${this.config.timeout}s`), 'timeout');
|
|
334
384
|
}, this.config.timeout * 1000);
|
|
335
|
-
// Set up heartbeat timeout
|
|
336
385
|
this.resetHeartbeat();
|
|
337
386
|
// ---------------------------------------------------------------
|
|
338
|
-
// Wire SDK events -> CopilotSession events
|
|
339
|
-
// ---------------------------------------------------------------
|
|
340
|
-
this._wireEvents(sdkSession);
|
|
341
|
-
// ---------------------------------------------------------------
|
|
342
387
|
// Send the prompt (fire-and-forget; events stream via handlers)
|
|
343
388
|
// ---------------------------------------------------------------
|
|
344
389
|
await sdkSession.send({ prompt });
|
|
@@ -347,16 +392,25 @@ class SdkSession {
|
|
|
347
392
|
* Wire all SDK session events to CopilotSession event emissions.
|
|
348
393
|
*/
|
|
349
394
|
_wireEvents(sdkSession) {
|
|
395
|
+
const isActive = () => this._sdkSession === sdkSession && !this.aborted;
|
|
350
396
|
// --- Assistant text response ---
|
|
351
397
|
sdkSession.on('assistant.message', (e) => {
|
|
352
|
-
if (
|
|
398
|
+
if (!isActive())
|
|
353
399
|
return;
|
|
354
400
|
this.resetHeartbeat();
|
|
355
401
|
const data = e.data;
|
|
356
402
|
const content = typeof data?.content === 'string' ? data.content : '';
|
|
357
403
|
const parentToolCallId = typeof data?.parentToolCallId === 'string' ? data.parentToolCallId : undefined;
|
|
358
|
-
|
|
359
|
-
|
|
404
|
+
const streamKey = parentToolCallId ?? '__root__';
|
|
405
|
+
if (content) {
|
|
406
|
+
const streamed = this._turnText.get(streamKey) ?? '';
|
|
407
|
+
const remainder = content === streamed
|
|
408
|
+
? ''
|
|
409
|
+
: content.startsWith(streamed) ? content.slice(streamed.length) : content;
|
|
410
|
+
if (remainder)
|
|
411
|
+
this.emit('text', remainder, parentToolCallId);
|
|
412
|
+
}
|
|
413
|
+
this._turnText.delete(streamKey);
|
|
360
414
|
// Pre-seed _toolCallNames from tool requests so tool.execution_complete
|
|
361
415
|
// can resolve names even if tool.execution_start lacks a toolCallId.
|
|
362
416
|
const toolRequests = Array.isArray(data?.toolRequests) ? data.toolRequests : [];
|
|
@@ -369,18 +423,21 @@ class SdkSession {
|
|
|
369
423
|
});
|
|
370
424
|
// --- Assistant text delta (streaming) ---
|
|
371
425
|
sdkSession.on('assistant.message_delta', (e) => {
|
|
372
|
-
if (
|
|
426
|
+
if (!isActive())
|
|
373
427
|
return;
|
|
374
428
|
this.resetHeartbeat();
|
|
375
429
|
const data = e.data;
|
|
376
430
|
const delta = typeof data?.deltaContent === 'string' ? data.deltaContent : '';
|
|
377
431
|
const parentToolCallId = typeof data?.parentToolCallId === 'string' ? data.parentToolCallId : undefined;
|
|
378
|
-
if (delta)
|
|
432
|
+
if (delta) {
|
|
433
|
+
const streamKey = parentToolCallId ?? '__root__';
|
|
434
|
+
this._turnText.set(streamKey, (this._turnText.get(streamKey) ?? '') + delta);
|
|
379
435
|
this.emit('text', delta, parentToolCallId);
|
|
436
|
+
}
|
|
380
437
|
});
|
|
381
438
|
// --- Reasoning ---
|
|
382
439
|
sdkSession.on('assistant.reasoning', (e) => {
|
|
383
|
-
if (
|
|
440
|
+
if (!isActive())
|
|
384
441
|
return;
|
|
385
442
|
this.resetHeartbeat();
|
|
386
443
|
const content = typeof e.data?.content === 'string' ? e.data.content : '';
|
|
@@ -388,7 +445,7 @@ class SdkSession {
|
|
|
388
445
|
this.emit('reasoning', content);
|
|
389
446
|
});
|
|
390
447
|
sdkSession.on('assistant.reasoning_delta', (e) => {
|
|
391
|
-
if (
|
|
448
|
+
if (!isActive())
|
|
392
449
|
return;
|
|
393
450
|
this.resetHeartbeat();
|
|
394
451
|
const delta = typeof e.data?.deltaContent === 'string' ? e.data.deltaContent : '';
|
|
@@ -397,7 +454,7 @@ class SdkSession {
|
|
|
397
454
|
});
|
|
398
455
|
// --- Tool execution start ---
|
|
399
456
|
sdkSession.on('tool.execution_start', (e) => {
|
|
400
|
-
if (
|
|
457
|
+
if (!isActive())
|
|
401
458
|
return;
|
|
402
459
|
this.resetHeartbeat();
|
|
403
460
|
const data = e.data;
|
|
@@ -426,7 +483,7 @@ class SdkSession {
|
|
|
426
483
|
});
|
|
427
484
|
// --- Tool execution complete ---
|
|
428
485
|
sdkSession.on('tool.execution_complete', (e) => {
|
|
429
|
-
if (
|
|
486
|
+
if (!isActive())
|
|
430
487
|
return;
|
|
431
488
|
this.resetHeartbeat();
|
|
432
489
|
const data = e.data;
|
|
@@ -457,7 +514,7 @@ class SdkSession {
|
|
|
457
514
|
// the SDK. We look it up from the _callIdToParent map populated by
|
|
458
515
|
// tool.execution_start.
|
|
459
516
|
sdkSession.on('tool.execution_partial_result', (e) => {
|
|
460
|
-
if (
|
|
517
|
+
if (!isActive())
|
|
461
518
|
return;
|
|
462
519
|
this.resetHeartbeat();
|
|
463
520
|
const data = e.data;
|
|
@@ -481,37 +538,68 @@ class SdkSession {
|
|
|
481
538
|
});
|
|
482
539
|
// --- Session idle (agent finished all work) ---
|
|
483
540
|
sdkSession.on('session.idle', () => {
|
|
484
|
-
if (this.
|
|
541
|
+
if (!isActive() || this.compactionRecoveryInProgress)
|
|
485
542
|
return;
|
|
486
|
-
this.
|
|
487
|
-
this.emit('idle');
|
|
488
|
-
this._cleanup();
|
|
543
|
+
this.settleIdle();
|
|
489
544
|
});
|
|
490
545
|
// --- task_complete → idle (some models fire this instead of session.idle) ---
|
|
491
546
|
sdkSession.on('session.task_complete', () => {
|
|
492
|
-
if (this.
|
|
547
|
+
if (!isActive() || this.compactionRecoveryInProgress)
|
|
493
548
|
return;
|
|
494
|
-
this.
|
|
495
|
-
this.emit('idle');
|
|
496
|
-
this._cleanup();
|
|
549
|
+
this.settleIdle();
|
|
497
550
|
});
|
|
498
|
-
//
|
|
551
|
+
// The CLI normally follows an abort event with session.idle. If that
|
|
552
|
+
// terminal event is lost, fail quickly rather than waiting for heartbeat.
|
|
553
|
+
sdkSession.on('abort', (e) => {
|
|
554
|
+
if (!isActive() || this.compactionRecoveryInProgress)
|
|
555
|
+
return;
|
|
556
|
+
if (this.intentionalAborts.delete(sdkSession))
|
|
557
|
+
return;
|
|
558
|
+
if (this.abortGraceTimer)
|
|
559
|
+
clearTimeout(this.abortGraceTimer);
|
|
560
|
+
this.abortGraceTimer = setTimeout(() => {
|
|
561
|
+
this.abortGraceTimer = null;
|
|
562
|
+
if (!isActive())
|
|
563
|
+
return;
|
|
564
|
+
const reason = typeof e.data?.reason === 'string' ? `: ${e.data.reason}` : '';
|
|
565
|
+
this.fail(new Error(`SDK turn aborted${reason}`), 'abort', e.data);
|
|
566
|
+
}, 1000);
|
|
567
|
+
});
|
|
568
|
+
// --- Session/model errors ---
|
|
499
569
|
sdkSession.on('session.error', (e) => {
|
|
500
|
-
if (
|
|
570
|
+
if (!isActive())
|
|
571
|
+
return;
|
|
572
|
+
// A rate-limit error eligible for automatic model switching is followed by
|
|
573
|
+
// auto_mode_switch.requested. Let the headless policy resolve that request
|
|
574
|
+
// instead of tearing down the session before it can recover.
|
|
575
|
+
if (e.data?.eligibleForAutoSwitch === true) {
|
|
576
|
+
this.resetHeartbeat();
|
|
501
577
|
return;
|
|
502
|
-
this.clearTimers();
|
|
503
|
-
const msg = typeof e.data?.message === 'string' ? e.data.message : 'Unknown session error';
|
|
504
|
-
try {
|
|
505
|
-
process.stderr.write(`[SdkBackend] session.error: ${msg}\n`);
|
|
506
578
|
}
|
|
507
|
-
|
|
508
|
-
this.
|
|
579
|
+
const msg = typeof e.data?.message === 'string' ? e.data.message : 'Unknown session error';
|
|
580
|
+
this.fail(new Error(msg), 'session.error', e.data);
|
|
581
|
+
});
|
|
582
|
+
// model.call_failure is telemetry for a failed LLM request, but the SDK has
|
|
583
|
+
// no API to resume/retry that in-flight turn. A second send() would append a
|
|
584
|
+
// duplicate user message rather than replaying the failed call, so fail the
|
|
585
|
+
// condukt session immediately instead of waiting for its heartbeat timeout.
|
|
586
|
+
sdkSession.on('model.call_failure', (e) => {
|
|
587
|
+
if (!isActive())
|
|
588
|
+
return;
|
|
589
|
+
const data = e.data;
|
|
590
|
+
const detail = typeof data?.errorMessage === 'string'
|
|
591
|
+
? data.errorMessage
|
|
592
|
+
: typeof data?.errorCode === 'string'
|
|
593
|
+
? data.errorCode
|
|
594
|
+
: 'Unknown model call failure';
|
|
595
|
+
const status = typeof data?.statusCode === 'number' ? ` (HTTP ${data.statusCode})` : '';
|
|
596
|
+
this.fail(new Error(`Model call failed${status}: ${detail}`), 'model.call_failure', data);
|
|
509
597
|
});
|
|
510
598
|
// --- Context compaction (infinite sessions) ---
|
|
511
599
|
// During compaction the model goes silent. SUSPEND the heartbeat entirely
|
|
512
600
|
// (not reset) to prevent killing the session. Hard timeout remains as safety net.
|
|
513
601
|
sdkSession.on('session.compaction_start', () => {
|
|
514
|
-
if (
|
|
602
|
+
if (!isActive())
|
|
515
603
|
return;
|
|
516
604
|
this.compactionInProgress = true;
|
|
517
605
|
// SUSPEND heartbeat — compaction silence is expected
|
|
@@ -528,8 +616,10 @@ class SdkSession {
|
|
|
528
616
|
// Recovery: if compaction doesn't complete within 3 min, escalate.
|
|
529
617
|
// Capture session to local to avoid TOCTOU null dereference after await.
|
|
530
618
|
const session = this._sdkSession;
|
|
619
|
+
if (!session || !isActive())
|
|
620
|
+
return;
|
|
531
621
|
this.compactionTimer = setTimeout(async () => {
|
|
532
|
-
if (!this.compactionInProgress ||
|
|
622
|
+
if (!this.compactionInProgress || !isActive() || !session)
|
|
533
623
|
return;
|
|
534
624
|
try {
|
|
535
625
|
try {
|
|
@@ -538,13 +628,13 @@ class SdkSession {
|
|
|
538
628
|
catch { /* */ }
|
|
539
629
|
await session.rpc.compaction.compact();
|
|
540
630
|
// Re-check guards after await — session may have been torn down
|
|
541
|
-
if (!this.compactionInProgress ||
|
|
631
|
+
if (!this.compactionInProgress || !isActive())
|
|
542
632
|
return;
|
|
543
633
|
// If force-compact works, compaction_complete will fire naturally
|
|
544
634
|
}
|
|
545
635
|
catch {
|
|
546
636
|
// Re-check guards after await
|
|
547
|
-
if (
|
|
637
|
+
if (!isActive())
|
|
548
638
|
return;
|
|
549
639
|
try {
|
|
550
640
|
process.stderr.write('[SdkBackend] Force compact failed — aborting + re-sending\n');
|
|
@@ -555,17 +645,30 @@ class SdkSession {
|
|
|
555
645
|
// SDK docs: "The session remains valid and can continue to be used for new messages."
|
|
556
646
|
// Wait for idle after abort before sending, to avoid racing with in-flight processing.
|
|
557
647
|
this.compactionInProgress = false;
|
|
648
|
+
this.compactionRecoveryInProgress = true;
|
|
649
|
+
this._turnText.clear();
|
|
558
650
|
try {
|
|
651
|
+
this.intentionalAborts.add(session);
|
|
559
652
|
await session.abort();
|
|
653
|
+
// Some SDK versions emit abort synchronously, others just after the
|
|
654
|
+
// request resolves. Keep the marker through the settling window.
|
|
655
|
+
if (this.abortGraceTimer) {
|
|
656
|
+
clearTimeout(this.abortGraceTimer);
|
|
657
|
+
this.abortGraceTimer = null;
|
|
658
|
+
}
|
|
659
|
+
// WeakSet entries are removed when the delayed intentional abort event
|
|
660
|
+
// arrives. The handle itself becomes collectible after session cleanup.
|
|
560
661
|
// Wait briefly for the abort to settle before re-sending.
|
|
561
662
|
// The SDK resolves abort() on acknowledgement, not on idle.
|
|
562
663
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
563
|
-
if (
|
|
664
|
+
if (!isActive())
|
|
564
665
|
return;
|
|
565
666
|
await session.send({ prompt: 'Continue from where you left off.' });
|
|
667
|
+
this.compactionRecoveryInProgress = false;
|
|
566
668
|
this.resetHeartbeat();
|
|
567
669
|
}
|
|
568
670
|
catch {
|
|
671
|
+
this.compactionRecoveryInProgress = false;
|
|
569
672
|
try {
|
|
570
673
|
process.stderr.write('[SdkBackend] Recovery failed — restarting heartbeat as safety net\n');
|
|
571
674
|
}
|
|
@@ -576,7 +679,7 @@ class SdkSession {
|
|
|
576
679
|
}, 3 * 60 * 1000);
|
|
577
680
|
});
|
|
578
681
|
sdkSession.on('session.compaction_complete', (e) => {
|
|
579
|
-
if (
|
|
682
|
+
if (!isActive())
|
|
580
683
|
return;
|
|
581
684
|
this.compactionInProgress = false;
|
|
582
685
|
if (this.compactionTimer) {
|
|
@@ -607,7 +710,7 @@ class SdkSession {
|
|
|
607
710
|
// The synthetic tool_start/tool_complete dual-emit is removed — sub-agent
|
|
608
711
|
// grouping is handled by SubagentSectionPart in the UI layer.
|
|
609
712
|
sdkSession.on('subagent.started', (e) => {
|
|
610
|
-
if (
|
|
713
|
+
if (!isActive())
|
|
611
714
|
return;
|
|
612
715
|
this.resetHeartbeat();
|
|
613
716
|
const data = e.data;
|
|
@@ -616,7 +719,7 @@ class SdkSession {
|
|
|
616
719
|
this.emit('subagent_start', name, { ...data, toolCallId });
|
|
617
720
|
});
|
|
618
721
|
sdkSession.on('subagent.completed', (e) => {
|
|
619
|
-
if (
|
|
722
|
+
if (!isActive())
|
|
620
723
|
return;
|
|
621
724
|
this.resetHeartbeat();
|
|
622
725
|
const data = e.data;
|
|
@@ -625,7 +728,7 @@ class SdkSession {
|
|
|
625
728
|
this.emit('subagent_end', name, { ...data, toolCallId });
|
|
626
729
|
});
|
|
627
730
|
sdkSession.on('subagent.failed', (e) => {
|
|
628
|
-
if (
|
|
731
|
+
if (!isActive())
|
|
629
732
|
return;
|
|
630
733
|
this.resetHeartbeat();
|
|
631
734
|
const data = e.data;
|
|
@@ -636,7 +739,7 @@ class SdkSession {
|
|
|
636
739
|
});
|
|
637
740
|
// --- Rich events (optional; consumers can subscribe or ignore) ---
|
|
638
741
|
sdkSession.on('assistant.intent', (e) => {
|
|
639
|
-
if (
|
|
742
|
+
if (!isActive())
|
|
640
743
|
return;
|
|
641
744
|
this.resetHeartbeat();
|
|
642
745
|
const intent = typeof e.data?.intent === 'string' ? e.data.intent : '';
|
|
@@ -644,37 +747,24 @@ class SdkSession {
|
|
|
644
747
|
this.emit('intent', intent);
|
|
645
748
|
});
|
|
646
749
|
sdkSession.on('assistant.usage', (e) => {
|
|
647
|
-
if (
|
|
750
|
+
if (!isActive())
|
|
648
751
|
return;
|
|
649
752
|
this.resetHeartbeat();
|
|
650
753
|
this.emit('usage', e.data ?? {});
|
|
651
754
|
});
|
|
652
755
|
sdkSession.on('permission.requested', (e) => {
|
|
653
|
-
if (
|
|
756
|
+
if (!isActive())
|
|
654
757
|
return;
|
|
655
758
|
this.resetHeartbeat();
|
|
656
759
|
this.emit('permission', e.data ?? {});
|
|
657
760
|
});
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
'assistant.reasoning', 'assistant.reasoning_delta',
|
|
662
|
-
'assistant.intent', 'assistant.usage',
|
|
663
|
-
'tool.execution_start', 'tool.execution_complete',
|
|
664
|
-
'tool.execution_partial_result',
|
|
665
|
-
'session.idle', 'session.task_complete', 'session.error',
|
|
666
|
-
'session.compaction_start', 'session.compaction_complete',
|
|
667
|
-
'subagent.started', 'subagent.completed', 'subagent.failed',
|
|
668
|
-
'permission.requested',
|
|
669
|
-
]);
|
|
761
|
+
// Class-level handling for the full SDK event surface. Named handlers above
|
|
762
|
+
// retain payload-specific mapping; this dispatcher supplies liveness,
|
|
763
|
+
// terminal fallbacks, pending-request policies, and future-event safety.
|
|
670
764
|
sdkSession.on((e) => {
|
|
671
|
-
if (
|
|
765
|
+
if (!isActive())
|
|
672
766
|
return;
|
|
673
|
-
|
|
674
|
-
&& !HANDLED_EVENT_TYPES.has(e.type)
|
|
675
|
-
&& !lifecycle_events_1.LIFECYCLE_EVENT_TYPES.has(e.type)) {
|
|
676
|
-
process.stderr.write(`[SdkBackend] Unhandled event: ${e.type}\n`);
|
|
677
|
-
}
|
|
767
|
+
this.dispatchClassEvent(sdkSession, e);
|
|
678
768
|
});
|
|
679
769
|
}
|
|
680
770
|
on(event, handler) {
|
|
@@ -741,13 +831,158 @@ class SdkSession {
|
|
|
741
831
|
catch { /* closed stream */ }
|
|
742
832
|
this.emit('error', err);
|
|
743
833
|
}
|
|
834
|
+
settleIdle() {
|
|
835
|
+
if (this.aborted || this.turnSettled)
|
|
836
|
+
return;
|
|
837
|
+
this.turnSettled = true;
|
|
838
|
+
this._turnText.clear();
|
|
839
|
+
this.clearTimers();
|
|
840
|
+
this.emit('idle');
|
|
841
|
+
void this._cleanup();
|
|
842
|
+
}
|
|
843
|
+
fail(err, eventType, data) {
|
|
844
|
+
if (this.aborted || this.turnSettled)
|
|
845
|
+
return;
|
|
846
|
+
this.turnSettled = true;
|
|
847
|
+
this.clearTimers();
|
|
848
|
+
try {
|
|
849
|
+
process.stderr.write(`[SdkBackend] ${eventType}: ${this.serializeEventDataCompact(eventType, data)}\n`);
|
|
850
|
+
}
|
|
851
|
+
catch { /* closed stream */ }
|
|
852
|
+
this.emitError(err);
|
|
853
|
+
this.aborted = true;
|
|
854
|
+
void this._cleanup();
|
|
855
|
+
}
|
|
856
|
+
handleEarlyEvent(e) {
|
|
857
|
+
if (this.aborted || !e || typeof e.type !== 'string')
|
|
858
|
+
return;
|
|
859
|
+
// Once the active handle exists, the session catch-all is authoritative.
|
|
860
|
+
// Only the pre-handle creation window is handled here.
|
|
861
|
+
if (this._sdkSession)
|
|
862
|
+
return;
|
|
863
|
+
const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
|
|
864
|
+
if (eventClass === 'informational')
|
|
865
|
+
return;
|
|
866
|
+
if (this.isFailureShapedEvent(e)) {
|
|
867
|
+
const payload = this.serializeEventDataCompact(e.type, e.data);
|
|
868
|
+
try {
|
|
869
|
+
process.stderr.write(`[SdkBackend] Early failure event: ${e.type} data=${payload}\n`);
|
|
870
|
+
}
|
|
871
|
+
catch { /* */ }
|
|
872
|
+
this.fail(new Error(`Early SDK failure event: ${e.type}`), e.type, e.data);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
dispatchClassEvent(sdkSession, e) {
|
|
876
|
+
if (!e || typeof e.type !== 'string' || NAMED_SDK_EVENTS.has(e.type))
|
|
877
|
+
return;
|
|
878
|
+
const eventClass = (0, lifecycle_events_1.classifySdkEvent)(e.type);
|
|
879
|
+
if (e.type === 'session.shutdown' && e.data?.shutdownType === 'error') {
|
|
880
|
+
const reason = typeof e.data.errorReason === 'string'
|
|
881
|
+
? e.data.errorReason
|
|
882
|
+
: 'SDK session shut down abnormally';
|
|
883
|
+
this.fail(new Error(reason), 'session.shutdown', e.data);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (eventClass === 'terminal-success') {
|
|
887
|
+
this.settleIdle();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (eventClass === 'terminal-failure') {
|
|
891
|
+
this.fail(new Error(`SDK terminal failure: ${e.type}`), e.type, e.data);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (eventClass === 'streaming-liveness') {
|
|
895
|
+
this.resetHeartbeat();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (eventClass === 'pending-request') {
|
|
899
|
+
this.resetHeartbeat();
|
|
900
|
+
void this.resolvePendingRequest(sdkSession, e);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (eventClass === 'informational')
|
|
904
|
+
return;
|
|
905
|
+
const payload = this.serializeEventDataCompact(e.type, e.data);
|
|
906
|
+
try {
|
|
907
|
+
process.stderr.write(`[SdkBackend] Unknown event: ${e.type} data=${payload}\n`);
|
|
908
|
+
}
|
|
909
|
+
catch { /* */ }
|
|
910
|
+
if (this.isFailureShapedEvent(e)) {
|
|
911
|
+
this.fail(new Error(`Unknown SDK failure event: ${e.type}`), e.type, e.data);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
serializeEventData(data) {
|
|
915
|
+
try {
|
|
916
|
+
return JSON.stringify(data ?? {});
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return '(unstringifiable payload)';
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
serializeEventDataCompact(type, data) {
|
|
923
|
+
if (type === 'session.binary_asset')
|
|
924
|
+
return '(binary payload omitted)';
|
|
925
|
+
const serialized = this.serializeEventData(data);
|
|
926
|
+
return serialized.length > 2000 ? `${serialized.slice(0, 2000)}…` : serialized;
|
|
927
|
+
}
|
|
928
|
+
isFailureShapedEvent(e) {
|
|
929
|
+
const type = e.type ?? '';
|
|
930
|
+
if (/(?:^|[._-])(error|failure|fatal)(?:$|[._-])/i.test(type))
|
|
931
|
+
return true;
|
|
932
|
+
const data = e.data;
|
|
933
|
+
if (!data)
|
|
934
|
+
return false;
|
|
935
|
+
return data.failed === true
|
|
936
|
+
|| typeof data.error === 'string'
|
|
937
|
+
|| (data.error != null && typeof data.error === 'object');
|
|
938
|
+
}
|
|
939
|
+
async resolvePendingRequest(sdkSession, e) {
|
|
940
|
+
if (this.aborted || this._sdkSession !== sdkSession)
|
|
941
|
+
return;
|
|
942
|
+
const requestId = typeof e.data?.requestId === 'string' ? e.data.requestId : '';
|
|
943
|
+
if (!requestId) {
|
|
944
|
+
this.fail(new Error(`Pending SDK request missing requestId: ${e.type}`), e.type ?? 'pending-request', e.data);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
switch (e.type) {
|
|
949
|
+
case 'sampling.requested':
|
|
950
|
+
await sdkSession.rpc.mcp.cancelSamplingExecution({ requestId });
|
|
951
|
+
break;
|
|
952
|
+
case 'auto_mode_switch.requested':
|
|
953
|
+
// Approve this turn only. Do not persist a user preference from a
|
|
954
|
+
// headless execution engine.
|
|
955
|
+
await sdkSession.rpc.ui.handlePendingAutoModeSwitch({ requestId, response: 'yes' });
|
|
956
|
+
break;
|
|
957
|
+
case 'session_limits_exhausted.requested':
|
|
958
|
+
await sdkSession.rpc.ui.handlePendingSessionLimitsExhausted({
|
|
959
|
+
requestId,
|
|
960
|
+
response: { action: 'cancel' },
|
|
961
|
+
});
|
|
962
|
+
break;
|
|
963
|
+
case 'mcp.headers_refresh_required':
|
|
964
|
+
// The runtime has its own timeout fallback; registering no response
|
|
965
|
+
// avoids inventing credentials in a headless process.
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
if (this.aborted || this._sdkSession !== sdkSession)
|
|
971
|
+
return;
|
|
972
|
+
this.fail(err instanceof Error ? err : new Error(String(err)), `${e.type}.policy`, e.data);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
744
975
|
resetHeartbeat() {
|
|
976
|
+
if (this.aborted || this.turnSettled || !this._sdkSession)
|
|
977
|
+
return;
|
|
978
|
+
const sdkSession = this._sdkSession;
|
|
745
979
|
if (this.heartbeatTimer) {
|
|
746
980
|
clearTimeout(this.heartbeatTimer);
|
|
747
981
|
}
|
|
748
982
|
this.heartbeatTimer = setTimeout(() => {
|
|
749
|
-
this.
|
|
750
|
-
|
|
983
|
+
if (this.aborted || this.turnSettled || this._sdkSession !== sdkSession)
|
|
984
|
+
return;
|
|
985
|
+
this.fail(new Error(`No output for ${this.config.heartbeatTimeout}s (heartbeat timeout)`), 'heartbeat');
|
|
751
986
|
}, this.config.heartbeatTimeout * 1000);
|
|
752
987
|
}
|
|
753
988
|
clearTimers() {
|
|
@@ -763,7 +998,12 @@ class SdkSession {
|
|
|
763
998
|
clearTimeout(this.compactionTimer);
|
|
764
999
|
this.compactionTimer = null;
|
|
765
1000
|
}
|
|
1001
|
+
if (this.abortGraceTimer) {
|
|
1002
|
+
clearTimeout(this.abortGraceTimer);
|
|
1003
|
+
this.abortGraceTimer = null;
|
|
1004
|
+
}
|
|
766
1005
|
this.compactionInProgress = false;
|
|
1006
|
+
this.compactionRecoveryInProgress = false;
|
|
767
1007
|
}
|
|
768
1008
|
}
|
|
769
1009
|
//# sourceMappingURL=sdk-backend.js.map
|