specrails-core 6.6.4 → 6.7.0
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/agent-runtime/capabilities.d.ts +5 -2
- package/dist/agent-runtime/capabilities.js +8 -2
- package/dist/agent-runtime/change-scope.d.ts +2 -0
- package/dist/agent-runtime/change-scope.js +5 -0
- package/dist/agent-runtime/cli-executor.js +7 -0
- package/dist/agent-runtime/cli.d.ts +6 -1
- package/dist/agent-runtime/cli.js +15 -4
- package/dist/agent-runtime/compact/reviewer.js +3 -1
- package/dist/agent-runtime/config.js +16 -5
- package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/efficiency-summary.js +7 -1
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
- package/dist/agent-runtime/engine/cli.d.ts +12 -0
- package/dist/agent-runtime/engine/cli.js +15 -4
- package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-schema.js +1 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-types.js +1 -1
- package/dist/agent-runtime/engine/definition-validator.js +8 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
- package/dist/agent-runtime/engine/invocation-context.js +2 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
- package/dist/agent-runtime/engine/piece-registry.js +1 -1
- package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
- package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
- package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
- package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
- package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
- package/dist/agent-runtime/engine/pieces/index.js +2 -1
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
- package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
- package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
- package/dist/agent-runtime/engine/preflight.d.ts +2 -1
- package/dist/agent-runtime/engine/preflight.js +3 -1
- package/dist/agent-runtime/engine/regex.d.ts +12 -2
- package/dist/agent-runtime/engine/regex.js +25 -11
- package/dist/agent-runtime/engine/run-status.d.ts +34 -0
- package/dist/agent-runtime/engine/run-status.js +7 -1
- package/dist/agent-runtime/engine/runs.d.ts +56 -0
- package/dist/agent-runtime/engine/runs.js +88 -5
- package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
- package/dist/agent-runtime/engine/steering/inbox.js +21 -1
- package/dist/agent-runtime/engine/storage/private-path.js +7 -1
- package/dist/agent-runtime/evaluation.js +1 -1
- package/dist/agent-runtime/executor-types.d.ts +19 -0
- package/dist/agent-runtime/executor-types.js +10 -0
- package/dist/agent-runtime/failure-classification.d.ts +51 -0
- package/dist/agent-runtime/failure-classification.js +110 -0
- package/dist/agent-runtime/graph/archive-preflight.d.ts +3 -18
- package/dist/agent-runtime/graph/archive-preflight.js +6 -51
- package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
- package/dist/agent-runtime/graph/artifacts.js +6 -2
- package/dist/agent-runtime/graph/baseline.d.ts +50 -0
- package/dist/agent-runtime/graph/baseline.js +145 -0
- package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
- package/dist/agent-runtime/graph/capability-map.js +65 -0
- package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
- package/dist/agent-runtime/graph/clarifications.js +158 -0
- package/dist/agent-runtime/graph/nodes.js +178 -19
- package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
- package/dist/agent-runtime/graph/review-brief-record.js +26 -0
- package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
- package/dist/agent-runtime/graph/review-brief.js +127 -0
- package/dist/agent-runtime/graph/roles.js +5 -3
- package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
- package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
- package/dist/agent-runtime/graph/state.d.ts +8 -0
- package/dist/agent-runtime/index.d.ts +7 -0
- package/dist/agent-runtime/index.js +7 -0
- package/dist/agent-runtime/openspec.d.ts +2 -1
- package/dist/agent-runtime/openspec.js +11 -5
- package/dist/agent-runtime/project-rules.d.ts +139 -0
- package/dist/agent-runtime/project-rules.js +683 -0
- package/dist/agent-runtime/prompts.d.ts +32 -1
- package/dist/agent-runtime/prompts.js +180 -7
- package/dist/agent-runtime/role-routing.d.ts +2 -1
- package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
- package/dist/agent-runtime/session/application/active-session.js +163 -7
- package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
- package/dist/agent-runtime/session/domain/descriptor.js +5 -0
- package/dist/agent-runtime/session/domain/interruption.js +1 -0
- package/dist/agent-runtime/session/domain/policy.js +2 -0
- package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
- package/dist/agent-runtime/session/domain/redaction.js +71 -0
- package/dist/agent-runtime/session/domain/types.d.ts +17 -1
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
- package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
- package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
- package/dist/agent-runtime/session/host/protocol.js +10 -2
- package/dist/agent-runtime/session/host/server.js +6 -5
- package/dist/agent-runtime/session/index.d.ts +1 -1
- package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
- package/dist/agent-runtime/session/ports.d.ts +6 -2
- package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
- package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
- package/dist/installer/cli.js +10 -1
- package/dist/installer/commands/init.js +15 -4
- package/dist/installer/phases/prereqs.d.ts +1 -1
- package/dist/installer/phases/prereqs.js +2 -2
- package/dist/installer/phases/scaffold.js +59 -11
- package/dist/pipeline/pipeline-state.d.ts +15 -0
- package/dist/pipeline/pipeline-state.js +36 -3
- package/dist/pipeline/spec-trace.d.ts +134 -0
- package/dist/pipeline/spec-trace.js +392 -0
- package/dist/shared/secret-redaction.d.ts +42 -0
- package/dist/shared/secret-redaction.js +184 -0
- package/integration-contract.json +177 -7
- package/package.json +8 -8
- package/pinned-versions.json +1 -1
- package/schemas/agent-runtime.schema.json +3 -1
- package/schemas/workflow-definition.schema.json +1 -0
- package/templates/settings/codex-config.toml +3 -2
|
@@ -42,11 +42,26 @@ export class ActiveSession {
|
|
|
42
42
|
descriptor;
|
|
43
43
|
/** Inputs held back for drivers without a native input queue. */
|
|
44
44
|
heldInputs = [];
|
|
45
|
+
/** Interrupting inputs held for drivers without a native input queue; released before `heldInputs`. */
|
|
46
|
+
interruptQueue = [];
|
|
47
|
+
/**
|
|
48
|
+
* The turn an `interrupt` delivery is cutting and the interrupting inputs that
|
|
49
|
+
* have not started yet. Cleared when one of them starts a turn.
|
|
50
|
+
*/
|
|
51
|
+
pendingInterrupt = null;
|
|
45
52
|
/** Inputs the application wrote on its own (policy-driven turns). */
|
|
46
53
|
systemInputIds = new Set();
|
|
47
54
|
pendingDeferred = null;
|
|
48
55
|
roster = null;
|
|
49
56
|
handoffs = 0;
|
|
57
|
+
/** Tool calls (the turn's and its sub-agents') that started and have not completed. */
|
|
58
|
+
toolsInFlight = new Set();
|
|
59
|
+
/**
|
|
60
|
+
* Text the turn streams around its tool calls arrives as separate messages
|
|
61
|
+
* with no whitespace between them. `broken` = text was streamed and a tool
|
|
62
|
+
* call followed, so the next text starts a new paragraph.
|
|
63
|
+
*/
|
|
64
|
+
textSegment = 'none';
|
|
50
65
|
finishedSinceLastTurn = new Set();
|
|
51
66
|
/** Finished delegated sub-agents whose results the agent has not received yet. */
|
|
52
67
|
delegatedResults = new Map();
|
|
@@ -201,12 +216,95 @@ export class ActiveSession {
|
|
|
201
216
|
}
|
|
202
217
|
// ── Delivery and process lifecycle ────────────────────────────────────────
|
|
203
218
|
async deliver(input) {
|
|
219
|
+
if (input.delivery === 'interrupt')
|
|
220
|
+
return this.deliverInterrupt(input);
|
|
204
221
|
const caps = this.descriptor.capabilities;
|
|
205
222
|
const turnRunning = this.state.openTurn !== null;
|
|
206
223
|
if (!caps.nativeInputQueue && turnRunning && !(input.delivery === 'steer' && caps.steer)) {
|
|
207
224
|
this.heldInputs.push(input);
|
|
208
225
|
return;
|
|
209
226
|
}
|
|
227
|
+
await this.write(input);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Cut the running turn and run `input` next. Idle sessions deliver it as a
|
|
231
|
+
* queued input. A driver with a native queue and a native primitive does both
|
|
232
|
+
* in one call; otherwise the application stops the turn and releases the
|
|
233
|
+
* input first when the turn ends (Codex: `turn/interrupt` then `turn/start`).
|
|
234
|
+
* A second interrupt while one is pending does not stop again: it follows the
|
|
235
|
+
* pending input, ahead of queued ones.
|
|
236
|
+
*/
|
|
237
|
+
async deliverInterrupt(input) {
|
|
238
|
+
const turn = this.state.openTurn;
|
|
239
|
+
const driver = this.driver;
|
|
240
|
+
const pending = this.pendingInterrupt;
|
|
241
|
+
if (!pending && (!turn || !driver))
|
|
242
|
+
return this.deliver({ ...input, delivery: 'queue' });
|
|
243
|
+
const caps = this.descriptor.capabilities;
|
|
244
|
+
if (pending)
|
|
245
|
+
pending.inputs.push(input);
|
|
246
|
+
else
|
|
247
|
+
this.pendingInterrupt = { turnId: turn.turnId, inputs: [input] };
|
|
248
|
+
if (!caps.nativeInputQueue) {
|
|
249
|
+
this.interruptQueue.push(input);
|
|
250
|
+
// A later interrupt waits behind the pending one; it is released when that one's turn ends.
|
|
251
|
+
if (!pending)
|
|
252
|
+
await this.stopForInterrupt(driver, turn.turnId);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (caps.interrupt === 'native') {
|
|
256
|
+
if (!pending)
|
|
257
|
+
this.armInterruptGrace(turn.turnId);
|
|
258
|
+
await this.write(input);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!pending)
|
|
262
|
+
await this.stopForInterrupt(driver, turn.turnId);
|
|
263
|
+
await this.write({ ...input, delivery: 'queue' });
|
|
264
|
+
}
|
|
265
|
+
async stopForInterrupt(driver, turnId) {
|
|
266
|
+
this.armInterruptGrace(turnId);
|
|
267
|
+
try {
|
|
268
|
+
await driver.interrupt();
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// The input is held either way: it runs when the turn ends, or after the grace retires the process.
|
|
272
|
+
this.commit([{ type: 'provider.diagnostic', level: 'warning', code: 'provider.interrupt_failed', message: error.message.slice(0, 2_000) }]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
armInterruptGrace(turnId) {
|
|
276
|
+
this.timers.arm('interruptGrace', this.deps.interruptGraceMs ?? 5_000, () => {
|
|
277
|
+
if (this.state.openTurn?.turnId === turnId)
|
|
278
|
+
void this.retireForInterrupt();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The provider did not end the cut turn in time: retire the process without
|
|
283
|
+
* losing the interrupting input or the inputs the application still holds,
|
|
284
|
+
* then deliver them to a fresh process (the provider session is resumed).
|
|
285
|
+
*/
|
|
286
|
+
async retireForInterrupt() {
|
|
287
|
+
const caps = this.descriptor.capabilities;
|
|
288
|
+
const waiting = (input) => {
|
|
289
|
+
const state = this.state.inputs[input.inputId]?.state;
|
|
290
|
+
return state === 'accepted' || state === 'queued';
|
|
291
|
+
};
|
|
292
|
+
const held = caps.nativeInputQueue ? (this.pendingInterrupt?.inputs ?? []) : [...this.interruptQueue, ...this.heldInputs];
|
|
293
|
+
const carried = held.filter(waiting).map((input) => ({ ...input, delivery: input.delivery === 'interrupt' ? 'queue' : input.delivery }));
|
|
294
|
+
await this.retire('user_stop', { turnStatus: 'interrupted', subagentPhase: 'interrupted', reason: 'user_interrupt', completeStarted: true, keepInputIds: new Set(carried.map((input) => input.inputId)) });
|
|
295
|
+
if (this.state.status === 'closed' || carried.length === 0)
|
|
296
|
+
return;
|
|
297
|
+
if (caps.nativeInputQueue) {
|
|
298
|
+
for (const input of carried)
|
|
299
|
+
await this.write(input).catch(() => undefined);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const [first, ...rest] = carried;
|
|
303
|
+
this.heldInputs.push(...rest);
|
|
304
|
+
await this.write(first).catch(() => undefined);
|
|
305
|
+
}
|
|
306
|
+
/** Write one input to the provider; a failed write leaves the input `interrupted`, never half-sent. */
|
|
307
|
+
async write(input) {
|
|
210
308
|
try {
|
|
211
309
|
const driver = await this.ensureDriver();
|
|
212
310
|
await driver.send(input);
|
|
@@ -216,6 +314,7 @@ export class ActiveSession {
|
|
|
216
314
|
if (current === 'accepted' || current === 'queued') {
|
|
217
315
|
this.commit([{ type: 'input.state', inputId: input.inputId, state: 'interrupted', reason: 'delivery_failed' }]);
|
|
218
316
|
}
|
|
317
|
+
this.forgetPendingInterrupt([input.inputId]);
|
|
219
318
|
throw isSessionError(error) ? error : new SessionError('internal', `Could not deliver input: ${error.message}`);
|
|
220
319
|
}
|
|
221
320
|
}
|
|
@@ -271,6 +370,7 @@ export class ActiveSession {
|
|
|
271
370
|
}
|
|
272
371
|
}
|
|
273
372
|
handleDriverEvent(event) {
|
|
373
|
+
this.trackToolInFlight(event);
|
|
274
374
|
this.markActivity();
|
|
275
375
|
switch (event.kind) {
|
|
276
376
|
case 'process.started':
|
|
@@ -283,6 +383,7 @@ export class ActiveSession {
|
|
|
283
383
|
this.driver = null;
|
|
284
384
|
this.liveGeneration = null;
|
|
285
385
|
this.timers.cancel('idle', 'stall', 'backgroundMax', 'turnInactivity', 'settle', 'interruptGrace');
|
|
386
|
+
this.toolsInFlight.clear();
|
|
286
387
|
this.endRunningWork({ turnStatus: 'failed', subagentPhase: 'interrupted', reason: 'crashed', error: `Provider process exited (code ${event.exitCode ?? 'none'}${event.signal ? `, signal ${event.signal}` : ''})` });
|
|
287
388
|
this.commit([{ type: 'session.process', state: 'exited', generation: this.generation, exitCode: event.exitCode, reason: 'crashed' }]);
|
|
288
389
|
this.deps.touched(this);
|
|
@@ -299,6 +400,8 @@ export class ActiveSession {
|
|
|
299
400
|
const input = this.state.inputs[event.inputId];
|
|
300
401
|
if (!input || input.state === event.state)
|
|
301
402
|
return;
|
|
403
|
+
if (event.state === 'rejected')
|
|
404
|
+
this.forgetPendingInterrupt([event.inputId]);
|
|
302
405
|
this.commit([{ type: 'input.state', inputId: event.inputId, state: event.state, ...(this.state.openTurn && event.state !== 'queued' ? { turnId: this.state.openTurn.turnId } : {}), ...(event.reason ? { reason: event.reason } : {}) }]);
|
|
303
406
|
return;
|
|
304
407
|
}
|
|
@@ -306,11 +409,13 @@ export class ActiveSession {
|
|
|
306
409
|
return this.startTurn(event.trigger, event.inputIds);
|
|
307
410
|
case 'turn.output': {
|
|
308
411
|
const turn = this.requireTurn();
|
|
309
|
-
this.commitBuffered(this.output.add({ type: 'turn.output', turnId: turn.turnId, channel: event.channel, delta: event.delta }));
|
|
412
|
+
this.commitBuffered(this.output.add({ type: 'turn.output', turnId: turn.turnId, channel: event.channel, delta: event.channel === 'text' ? this.separateTextSegment(event.delta) : event.delta }));
|
|
310
413
|
return;
|
|
311
414
|
}
|
|
312
415
|
case 'turn.tool': {
|
|
313
416
|
const turn = this.requireTurn();
|
|
417
|
+
if (this.textSegment === 'open')
|
|
418
|
+
this.textSegment = 'broken';
|
|
314
419
|
const { kind: _kind, ...tool } = event;
|
|
315
420
|
this.commit([{ type: 'turn.tool', turnId: turn.turnId, ...tool }]);
|
|
316
421
|
return;
|
|
@@ -369,6 +474,9 @@ export class ActiveSession {
|
|
|
369
474
|
this.handoffs = 0;
|
|
370
475
|
}
|
|
371
476
|
this.finishedSinceLastTurn.clear();
|
|
477
|
+
this.textSegment = 'none';
|
|
478
|
+
if (this.pendingInterrupt?.inputs.some((input) => inputIds.includes(input.inputId)))
|
|
479
|
+
this.pendingInterrupt = null;
|
|
372
480
|
this.timers.cancel('idle', 'stall');
|
|
373
481
|
this.commit([
|
|
374
482
|
...(this.state.phase === 'turn' ? [] : [{ type: 'session.phase', phase: 'turn' }]),
|
|
@@ -376,12 +484,20 @@ export class ActiveSession {
|
|
|
376
484
|
]);
|
|
377
485
|
this.armTurnInactivity();
|
|
378
486
|
}
|
|
379
|
-
completeTurn(
|
|
487
|
+
completeTurn(reportedStatus, reportedText, reported, error) {
|
|
380
488
|
const turn = this.requireTurn();
|
|
381
489
|
this.timers.cancel('turnInactivity', 'interruptGrace');
|
|
490
|
+
this.toolsInFlight.clear();
|
|
491
|
+
// A stop the application asked for to run an interrupting input is an interruption.
|
|
492
|
+
const status = reportedStatus === 'stopped' && this.pendingInterrupt?.turnId === turn.turnId ? 'interrupted' : reportedStatus;
|
|
493
|
+
// Commit buffered deltas first so a cut turn keeps the partial text it streamed.
|
|
494
|
+
if (this.output.hasPending()) {
|
|
495
|
+
this.timers.cancel('flush');
|
|
496
|
+
this.commit(this.output.flush());
|
|
497
|
+
}
|
|
498
|
+
const text = reportedText || (status === 'interrupted' ? this.state.openTurn?.text ?? '' : '');
|
|
382
499
|
const { usage, baselineOption } = this.turnUsage(reported);
|
|
383
500
|
this.commit([
|
|
384
|
-
...this.output.flush(),
|
|
385
501
|
...this.output.drainTurn(turn.turnId),
|
|
386
502
|
{ type: 'turn.completed', turnId: turn.turnId, status, text, usage, ...(error ? { error } : {}) },
|
|
387
503
|
...this.completeTurnInputs(turn.inputIds, turn.turnId),
|
|
@@ -456,6 +572,9 @@ export class ActiveSession {
|
|
|
456
572
|
return;
|
|
457
573
|
if (this.finishedSinceLastTurn.size === 0 || this.handoffs >= policy.limits.maxSettleHandoffs || !this.driver)
|
|
458
574
|
return;
|
|
575
|
+
// A user correction runs first; the next settle point collects the sub-agents.
|
|
576
|
+
if (this.pendingInterrupt)
|
|
577
|
+
return;
|
|
459
578
|
this.handoffs += 1;
|
|
460
579
|
const inputId = this.deps.ids.input();
|
|
461
580
|
this.systemInputIds.add(inputId);
|
|
@@ -466,6 +585,9 @@ export class ActiveSession {
|
|
|
466
585
|
maybeDeliverDelegatedResults() {
|
|
467
586
|
if (this.delegatedResults.size === 0 || this.state.openTurn || this.state.status === 'closed' || this.closing)
|
|
468
587
|
return false;
|
|
588
|
+
// A user correction runs first; the results follow at the next settle point.
|
|
589
|
+
if (this.pendingInterrupt)
|
|
590
|
+
return false;
|
|
469
591
|
if (this.handoffs >= this.config.policy.limits.maxSettleHandoffs)
|
|
470
592
|
return false;
|
|
471
593
|
const results = [...this.delegatedResults.values()];
|
|
@@ -546,9 +668,27 @@ export class ActiveSession {
|
|
|
546
668
|
idleTeardown(reason) {
|
|
547
669
|
return { turnStatus: 'interrupted', subagentPhase: 'interrupted', reason };
|
|
548
670
|
}
|
|
671
|
+
separateTextSegment(delta) {
|
|
672
|
+
const separate = this.textSegment === 'broken' && !/^\s*\n/.test(delta);
|
|
673
|
+
this.textSegment = 'open';
|
|
674
|
+
return separate ? `\n\n${delta}` : delta;
|
|
675
|
+
}
|
|
676
|
+
trackToolInFlight(event) {
|
|
677
|
+
const tool = event.kind === 'turn.tool' ? event : event.kind === 'subagent.output' ? event.tool : undefined;
|
|
678
|
+
if (!tool?.toolUseId)
|
|
679
|
+
return;
|
|
680
|
+
if (tool.phase === 'started')
|
|
681
|
+
this.toolsInFlight.add(tool.toolUseId);
|
|
682
|
+
else
|
|
683
|
+
this.toolsInFlight.delete(tool.toolUseId);
|
|
684
|
+
}
|
|
549
685
|
armTurnInactivity() {
|
|
550
|
-
|
|
551
|
-
|
|
686
|
+
const limits = this.config.policy.limits;
|
|
687
|
+
const waitingOnTool = this.toolsInFlight.size > 0;
|
|
688
|
+
const limit = waitingOnTool ? Math.max(limits.turnInactivityMs, limits.toolInactivityMs) : limits.turnInactivityMs;
|
|
689
|
+
this.timers.arm('turnInactivity', limit, () => {
|
|
690
|
+
const error = waitingOnTool ? 'A tool call produced no provider activity before the tool inactivity limit' : 'No provider activity before the inactivity limit';
|
|
691
|
+
void this.retire('stalled', { turnStatus: 'failed', subagentPhase: 'interrupted', reason: 'stalled', error });
|
|
552
692
|
});
|
|
553
693
|
}
|
|
554
694
|
markActivity() {
|
|
@@ -575,9 +715,17 @@ export class ActiveSession {
|
|
|
575
715
|
async releaseHeldInput() {
|
|
576
716
|
if (this.state.openTurn)
|
|
577
717
|
return;
|
|
578
|
-
const next = this.heldInputs.shift();
|
|
718
|
+
const next = this.interruptQueue.shift() ?? this.heldInputs.shift();
|
|
579
719
|
if (next)
|
|
580
|
-
await this.deliver(next).catch(() => undefined);
|
|
720
|
+
await this.deliver(next.delivery === 'interrupt' ? { ...next, delivery: 'queue' } : next).catch(() => undefined);
|
|
721
|
+
}
|
|
722
|
+
forgetPendingInterrupt(inputIds) {
|
|
723
|
+
const pending = this.pendingInterrupt;
|
|
724
|
+
if (!pending)
|
|
725
|
+
return;
|
|
726
|
+
pending.inputs = pending.inputs.filter((input) => !inputIds.includes(input.inputId));
|
|
727
|
+
if (pending.inputs.length === 0)
|
|
728
|
+
this.pendingInterrupt = null;
|
|
581
729
|
}
|
|
582
730
|
/** End whatever was running; used by retire and unexpected exits. */
|
|
583
731
|
endRunningWork(teardown) {
|
|
@@ -605,11 +753,19 @@ export class ActiveSession {
|
|
|
605
753
|
}
|
|
606
754
|
}
|
|
607
755
|
for (const input of Object.values(this.state.inputs)) {
|
|
756
|
+
if (teardown.keepInputIds?.has(input.inputId))
|
|
757
|
+
continue;
|
|
758
|
+
if (input.state === 'started' && teardown.completeStarted) {
|
|
759
|
+
bodies.push({ type: 'input.state', inputId: input.inputId, state: 'completed', ...(input.turnId ? { turnId: input.turnId } : {}) });
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
608
762
|
if (input.state === 'accepted' || input.state === 'queued' || input.state === 'started') {
|
|
609
763
|
bodies.push({ type: 'input.state', inputId: input.inputId, state: 'interrupted', reason: teardown.reason });
|
|
610
764
|
}
|
|
611
765
|
}
|
|
612
766
|
this.heldInputs.length = 0;
|
|
767
|
+
this.interruptQueue.length = 0;
|
|
768
|
+
this.pendingInterrupt = null;
|
|
613
769
|
// With delegated work still running the session stays in the background.
|
|
614
770
|
const nextPhase = this.delegatedLive().length > 0 ? 'background' : 'idle';
|
|
615
771
|
if (this.state.phase !== nextPhase || turn)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SessionEventEnvelope } from '../domain/events.js';
|
|
2
2
|
import { type SessionPolicyInput } from '../domain/policy.js';
|
|
3
3
|
import { type SessionSnapshot } from '../domain/snapshot.js';
|
|
4
|
-
import type { Attachment, DriverDescriptor } from '../domain/types.js';
|
|
4
|
+
import type { Attachment, DriverDescriptor, InputDelivery } from '../domain/types.js';
|
|
5
5
|
import type { RateCard } from '../domain/usage.js';
|
|
6
6
|
import type { Clock, DriverCatalog, Ids, SessionJournal, SessionSummary } from '../ports.js';
|
|
7
7
|
import { type DelegateParams, type DelegatedResult } from './delegation.js';
|
|
@@ -38,7 +38,7 @@ export interface SendParams {
|
|
|
38
38
|
inputId: string;
|
|
39
39
|
text: string;
|
|
40
40
|
attachments?: Attachment[];
|
|
41
|
-
delivery:
|
|
41
|
+
delivery: InputDelivery;
|
|
42
42
|
}
|
|
43
43
|
export type SessionListener = (envelopes: SessionEventEnvelope[]) => void;
|
|
44
44
|
export declare const DEFAULT_MAX_RESIDENT = 6;
|
|
@@ -27,6 +27,11 @@ export function validateDescriptor(descriptor) {
|
|
|
27
27
|
fail('a native input queue requires a resident process');
|
|
28
28
|
if (!caps.resident && caps.autonomousContinuation)
|
|
29
29
|
fail('autonomous continuation requires a resident process');
|
|
30
|
+
if (caps.interrupt !== undefined && caps.interrupt !== 'native' && caps.interrupt !== 'emulated')
|
|
31
|
+
fail('capabilities.interrupt must be native|emulated when present');
|
|
32
|
+
// A native interrupt without a turn-level stop that keeps the session is meaningless for one-shot drivers.
|
|
33
|
+
if (caps.interrupt === 'native' && !caps.resident)
|
|
34
|
+
fail('a native interrupt requires a resident process');
|
|
30
35
|
if (!COST.has(caps.usage?.costUsd) || !TOKENS.has(caps.usage?.tokens))
|
|
31
36
|
fail('capabilities.usage is invalid');
|
|
32
37
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const REASONS = Object.freeze({
|
|
2
2
|
user_stop: 'stopped by the user',
|
|
3
|
+
user_interrupt: 'interrupted by a newer user message',
|
|
3
4
|
host_request: 'stopped by the host',
|
|
4
5
|
restart: 'interrupted by an application restart',
|
|
5
6
|
shutdown: 'interrupted when the application closed',
|
|
@@ -5,6 +5,7 @@ export const DEFAULT_LIMITS = Object.freeze({
|
|
|
5
5
|
stallMs: 15 * 60_000,
|
|
6
6
|
backgroundMaxMs: 2 * 60 * 60_000,
|
|
7
7
|
turnInactivityMs: 5 * 60_000,
|
|
8
|
+
toolInactivityMs: 60 * 60_000,
|
|
8
9
|
maxSettleHandoffs: 3,
|
|
9
10
|
settleDebounceMs: 1_500,
|
|
10
11
|
});
|
|
@@ -13,6 +14,7 @@ const LIMIT_BOUNDS = Object.freeze({
|
|
|
13
14
|
stallMs: [1_000, 24 * 60 * 60_000],
|
|
14
15
|
backgroundMaxMs: [1_000, 7 * 24 * 60 * 60_000],
|
|
15
16
|
turnInactivityMs: [1_000, 24 * 60 * 60_000],
|
|
17
|
+
toolInactivityMs: [1_000, 24 * 60 * 60_000],
|
|
16
18
|
maxSettleHandoffs: [0, 20],
|
|
17
19
|
settleDebounceMs: [0, 60_000],
|
|
18
20
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SessionEvent } from './events.js';
|
|
2
|
+
import type { SessionSnapshot } from './snapshot.js';
|
|
3
|
+
/**
|
|
4
|
+
* Secret redaction at the journal boundary (docs/agent-sessions/journal.md).
|
|
5
|
+
*
|
|
6
|
+
* Only free text and tool I/O are redacted. Identifiers, sequence numbers,
|
|
7
|
+
* phases, usage numbers and host-supplied configuration (`systemPrompt`,
|
|
8
|
+
* `session.updated.changes`, `policy`) are kept as they are: configuration is
|
|
9
|
+
* read back from the snapshot to resume a session, so changing it would change
|
|
10
|
+
* what the provider receives. Event shapes never change: a redacted field keeps
|
|
11
|
+
* its type and an absent field stays absent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function redactSessionEvent(event: SessionEvent): SessionEvent;
|
|
14
|
+
/**
|
|
15
|
+
* Redact the free text a snapshot carries (open turn text, sub-agent
|
|
16
|
+
* descriptions, reasons and results). Input fingerprints, configuration and
|
|
17
|
+
* usage are untouched.
|
|
18
|
+
*/
|
|
19
|
+
export declare function redactSessionSnapshot(snapshot: SessionSnapshot): SessionSnapshot;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { redactSecrets, redactSecretsDeep } from '../../../shared/secret-redaction.js';
|
|
2
|
+
/**
|
|
3
|
+
* Secret redaction at the journal boundary (docs/agent-sessions/journal.md).
|
|
4
|
+
*
|
|
5
|
+
* Only free text and tool I/O are redacted. Identifiers, sequence numbers,
|
|
6
|
+
* phases, usage numbers and host-supplied configuration (`systemPrompt`,
|
|
7
|
+
* `session.updated.changes`, `policy`) are kept as they are: configuration is
|
|
8
|
+
* read back from the snapshot to resume a session, so changing it would change
|
|
9
|
+
* what the provider receives. Event shapes never change: a redacted field keeps
|
|
10
|
+
* its type and an absent field stays absent.
|
|
11
|
+
*/
|
|
12
|
+
export function redactSessionEvent(event) {
|
|
13
|
+
switch (event.type) {
|
|
14
|
+
case 'input.accepted':
|
|
15
|
+
return { ...event, text: redactSecrets(event.text) };
|
|
16
|
+
case 'turn.output':
|
|
17
|
+
return { ...event, delta: redactSecrets(event.delta) };
|
|
18
|
+
case 'turn.tool':
|
|
19
|
+
return { ...event, ...redactTool(event) };
|
|
20
|
+
case 'turn.completed':
|
|
21
|
+
return { ...event, text: redactSecrets(event.text), ...(event.error === undefined ? {} : { error: redactSecrets(event.error) }) };
|
|
22
|
+
case 'subagent.started':
|
|
23
|
+
return { ...event, description: redactSecrets(event.description), ...(event.prompt === undefined ? {} : { prompt: redactSecrets(event.prompt) }) };
|
|
24
|
+
case 'subagent.output':
|
|
25
|
+
return {
|
|
26
|
+
...event,
|
|
27
|
+
...(event.delta === undefined ? {} : { delta: redactSecrets(event.delta) }),
|
|
28
|
+
...(event.tool === undefined ? {} : { tool: { ...event.tool, ...redactTool(event.tool) } }),
|
|
29
|
+
};
|
|
30
|
+
case 'subagent.result':
|
|
31
|
+
return { ...event, summary: redactSecrets(event.summary) };
|
|
32
|
+
case 'subagent.phase':
|
|
33
|
+
case 'input.state':
|
|
34
|
+
case 'session.process':
|
|
35
|
+
return event.reason === undefined ? event : { ...event, reason: redactSecrets(event.reason) };
|
|
36
|
+
case 'session.closed':
|
|
37
|
+
return { ...event, reason: redactSecrets(event.reason) };
|
|
38
|
+
case 'provider.diagnostic':
|
|
39
|
+
return { ...event, message: redactSecrets(event.message) };
|
|
40
|
+
default:
|
|
41
|
+
return event;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function redactTool(tool) {
|
|
45
|
+
return {
|
|
46
|
+
...(tool.input === undefined ? {} : { input: redactSecretsDeep(tool.input) }),
|
|
47
|
+
...(tool.output === undefined ? {} : { output: redactSecrets(tool.output) }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Redact the free text a snapshot carries (open turn text, sub-agent
|
|
52
|
+
* descriptions, reasons and results). Input fingerprints, configuration and
|
|
53
|
+
* usage are untouched.
|
|
54
|
+
*/
|
|
55
|
+
export function redactSessionSnapshot(snapshot) {
|
|
56
|
+
const subagents = {};
|
|
57
|
+
for (const [id, node] of Object.entries(snapshot.subagents)) {
|
|
58
|
+
subagents[id] = {
|
|
59
|
+
...node,
|
|
60
|
+
description: redactSecrets(node.description),
|
|
61
|
+
reason: node.reason === null ? null : redactSecrets(node.reason),
|
|
62
|
+
resultSummary: node.resultSummary === null ? null : redactSecrets(node.resultSummary),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...snapshot,
|
|
67
|
+
openTurn: snapshot.openTurn === null ? null : { ...snapshot.openTurn, text: redactSecrets(snapshot.openTurn.text) },
|
|
68
|
+
subagents,
|
|
69
|
+
closedReason: snapshot.closedReason === null ? null : redactSecrets(snapshot.closedReason),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -7,7 +7,11 @@ export type SessionPhase = 'idle' | 'turn' | 'background';
|
|
|
7
7
|
export type SessionStatus = 'open' | 'closed';
|
|
8
8
|
export type TurnOrigin = 'user' | 'subagent' | 'system';
|
|
9
9
|
export type TurnStatus = 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `queue`: run as a later turn. `steer`: deliver into the running turn.
|
|
12
|
+
* `interrupt`: cut the running turn and run this input next (like `queue` when idle).
|
|
13
|
+
*/
|
|
14
|
+
export type InputDelivery = 'queue' | 'steer' | 'interrupt';
|
|
11
15
|
export type InputState = 'accepted' | 'queued' | 'started' | 'completed' | 'rejected' | 'interrupted';
|
|
12
16
|
export type SubagentKind = 'foreground' | 'background';
|
|
13
17
|
export type SubagentPhase = 'running' | 'idle' | 'failed' | 'stopped' | 'killed' | 'interrupted';
|
|
@@ -52,6 +56,12 @@ export interface SessionLimits {
|
|
|
52
56
|
backgroundMaxMs: number;
|
|
53
57
|
/** Fail a running turn when no provider frame (including sub-agent frames) arrives for this long. */
|
|
54
58
|
turnInactivityMs: number;
|
|
59
|
+
/**
|
|
60
|
+
* Replaces `turnInactivityMs` while a tool call (the turn's or a sub-agent's)
|
|
61
|
+
* has started and not completed: a long build or test run is silent until
|
|
62
|
+
* it returns, so it gets a wider bound instead of failing the turn.
|
|
63
|
+
*/
|
|
64
|
+
toolInactivityMs: number;
|
|
55
65
|
/** Maximum policy-driven `resume-agent` turns per settle cycle. */
|
|
56
66
|
maxSettleHandoffs: number;
|
|
57
67
|
/** Debounce before an empty sub-agent roster counts as settled. */
|
|
@@ -114,6 +124,12 @@ export interface DriverCapabilities {
|
|
|
114
124
|
steer: boolean;
|
|
115
125
|
/** The tool set can be restricted (`policy.tools` other than the default). */
|
|
116
126
|
toolFiltering: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* How `delivery: 'interrupt'` cuts a running turn. `native`: the provider's own
|
|
129
|
+
* stop primitive, keeping its session. `emulated`: the runtime stops the turn
|
|
130
|
+
* as `session.interrupt` does, then runs the input next. Absent on older Cores.
|
|
131
|
+
*/
|
|
132
|
+
interrupt?: 'native' | 'emulated';
|
|
117
133
|
usage: UsageSemantics;
|
|
118
134
|
}
|
|
119
135
|
export interface DriverDescriptor {
|
|
@@ -2,6 +2,20 @@ import type { DriverOpenSpec } from '../../ports.js';
|
|
|
2
2
|
/** Tools that launch sub-agents in Claude Code; removed when policy disables sub-agents. */
|
|
3
3
|
export declare const CLAUDE_SUBAGENT_TOOLS: readonly ['Agent', 'Task'];
|
|
4
4
|
export declare const CLAUDE_READ_ONLY_TOOLS: readonly ['Read', 'Grep', 'Glob'];
|
|
5
|
+
/**
|
|
6
|
+
* Built-in tools that change files or run commands. A read-only session that
|
|
7
|
+
* allows native sub-agents also denies them: deny rules bind every sub-agent of
|
|
8
|
+
* the session, including project-defined agents that name their own tools.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLAUDE_WRITE_TOOLS: readonly ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash'];
|
|
11
|
+
/**
|
|
12
|
+
* The read-only tool set. Native sub-agents keep the parent's restriction
|
|
13
|
+
* (verified live with Claude Code 2.1.309: a general-purpose sub-agent of a
|
|
14
|
+
* `--tools Read,Grep,Glob,Agent` session saw only those tools and could not
|
|
15
|
+
* write), so the sub-agent tool is offered when the policy allows them.
|
|
16
|
+
* `Task` is the tool's legacy name; unknown names in `--tools` are ignored.
|
|
17
|
+
*/
|
|
18
|
+
export declare function claudeReadOnlyTools(policy: DriverOpenSpec['policy']): string[];
|
|
5
19
|
/**
|
|
6
20
|
* Map an open spec + policy to Claude Code argv. Lists are passed as one
|
|
7
21
|
* comma-separated argument (variadic flags would otherwise swallow later args).
|
|
@@ -2,6 +2,22 @@ import { nativeSubagentsAllowed } from '../../domain/policy.js';
|
|
|
2
2
|
/** Tools that launch sub-agents in Claude Code; removed when policy disables sub-agents. */
|
|
3
3
|
export const CLAUDE_SUBAGENT_TOOLS = ['Agent', 'Task'];
|
|
4
4
|
export const CLAUDE_READ_ONLY_TOOLS = ['Read', 'Grep', 'Glob'];
|
|
5
|
+
/**
|
|
6
|
+
* Built-in tools that change files or run commands. A read-only session that
|
|
7
|
+
* allows native sub-agents also denies them: deny rules bind every sub-agent of
|
|
8
|
+
* the session, including project-defined agents that name their own tools.
|
|
9
|
+
*/
|
|
10
|
+
export const CLAUDE_WRITE_TOOLS = ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash'];
|
|
11
|
+
/**
|
|
12
|
+
* The read-only tool set. Native sub-agents keep the parent's restriction
|
|
13
|
+
* (verified live with Claude Code 2.1.309: a general-purpose sub-agent of a
|
|
14
|
+
* `--tools Read,Grep,Glob,Agent` session saw only those tools and could not
|
|
15
|
+
* write), so the sub-agent tool is offered when the policy allows them.
|
|
16
|
+
* `Task` is the tool's legacy name; unknown names in `--tools` are ignored.
|
|
17
|
+
*/
|
|
18
|
+
export function claudeReadOnlyTools(policy) {
|
|
19
|
+
return [...CLAUDE_READ_ONLY_TOOLS, ...(nativeSubagentsAllowed(policy) ? CLAUDE_SUBAGENT_TOOLS : [])];
|
|
20
|
+
}
|
|
5
21
|
function mcpConfig(servers) {
|
|
6
22
|
return JSON.stringify({
|
|
7
23
|
mcpServers: Object.fromEntries(servers.map((server) => [server.name, server.url
|
|
@@ -39,10 +55,11 @@ export function claudeArgs(spec, options = {}) {
|
|
|
39
55
|
if (policy.tools.mode === 'none')
|
|
40
56
|
args.push('--tools', '');
|
|
41
57
|
else if (policy.tools.mode === 'read-only')
|
|
42
|
-
args.push('--tools',
|
|
58
|
+
args.push('--tools', claudeReadOnlyTools(policy).join(','));
|
|
43
59
|
if (policy.tools.allow?.length)
|
|
44
60
|
args.push('--allowedTools', policy.tools.allow.join(','));
|
|
45
|
-
const
|
|
61
|
+
const readOnlySubagents = policy.tools.mode === 'read-only' && nativeSubagentsAllowed(policy);
|
|
62
|
+
const denied = [...new Set([...(policy.tools.deny ?? []), ...(!nativeSubagentsAllowed(policy) ? CLAUDE_SUBAGENT_TOOLS : []), ...(readOnlySubagents ? CLAUDE_WRITE_TOOLS : [])])];
|
|
46
63
|
if (denied.length)
|
|
47
64
|
args.push('--disallowedTools', denied.join(','));
|
|
48
65
|
if (policy.mcp.servers.length)
|
|
@@ -19,6 +19,7 @@ export const CLAUDE_DESCRIPTOR = Object.freeze({
|
|
|
19
19
|
autonomousContinuation: true,
|
|
20
20
|
steer: true,
|
|
21
21
|
toolFiltering: true,
|
|
22
|
+
interrupt: 'native',
|
|
22
23
|
usage: Object.freeze({ costUsd: 'session-cumulative', tokens: 'per-turn' }),
|
|
23
24
|
}),
|
|
24
25
|
});
|
|
@@ -47,6 +48,12 @@ class ClaudeDriverSession {
|
|
|
47
48
|
this.cleanup = cleanup;
|
|
48
49
|
}
|
|
49
50
|
async send(input) {
|
|
51
|
+
// Interrupt delivery: stop the running turn (once per cut turn), then put the
|
|
52
|
+
// input first. Verified on 2.1.299: after the stop, `now` runs the input alone
|
|
53
|
+
// in the next turn and already queued commands follow; `next` or the default
|
|
54
|
+
// would batch it behind them.
|
|
55
|
+
if (input.delivery === 'interrupt' && this.translator.interruptible)
|
|
56
|
+
this.writeInterrupt();
|
|
50
57
|
this.translator.noteInput(input.inputId);
|
|
51
58
|
const frame = {
|
|
52
59
|
type: 'user',
|
|
@@ -54,13 +61,16 @@ class ClaudeDriverSession {
|
|
|
54
61
|
session_id: '',
|
|
55
62
|
parent_tool_use_id: null,
|
|
56
63
|
// `next` delivers into the running turn; otherwise the CLI queues it for the next turn.
|
|
57
|
-
...(input.delivery === 'steer' ? { priority: 'next' } : {}),
|
|
64
|
+
...(input.delivery === 'steer' ? { priority: 'next' } : input.delivery === 'interrupt' ? { priority: 'now' } : {}),
|
|
58
65
|
message: { role: 'user', content: userContent(input) },
|
|
59
66
|
};
|
|
60
67
|
if (!this.process.write(JSON.stringify(frame)))
|
|
61
68
|
throw new Error('Claude input is closed');
|
|
62
69
|
}
|
|
63
70
|
async interrupt() {
|
|
71
|
+
this.writeInterrupt();
|
|
72
|
+
}
|
|
73
|
+
writeInterrupt() {
|
|
64
74
|
this.translator.noteInterrupt();
|
|
65
75
|
this.process.write(JSON.stringify({ type: 'control_request', request_id: randomUUID(), request: { subtype: 'interrupt' } }));
|
|
66
76
|
}
|
|
@@ -15,6 +15,8 @@ export declare class ClaudeTranslator {
|
|
|
15
15
|
private turnOpen;
|
|
16
16
|
private interruptRequested;
|
|
17
17
|
private readonly written;
|
|
18
|
+
/** Inputs the CLI reported started; a later `cancelled` only echoes their interrupted turn. */
|
|
19
|
+
private readonly started;
|
|
18
20
|
private startedInputs;
|
|
19
21
|
/** tool_use id → task id of the agent that issued it (null = main agent). */
|
|
20
22
|
private readonly toolOwner;
|
|
@@ -26,6 +28,8 @@ export declare class ClaudeTranslator {
|
|
|
26
28
|
/** The driver wrote this input; its receipts and replays belong to us. */
|
|
27
29
|
noteInput(inputId: string): void;
|
|
28
30
|
noteInterrupt(): void;
|
|
31
|
+
/** A turn is running and no stop was requested for it yet. */
|
|
32
|
+
get interruptible(): boolean;
|
|
29
33
|
translate(frame: Frame): DriverEvent[];
|
|
30
34
|
private system;
|
|
31
35
|
private taskStarted;
|
|
@@ -6,7 +6,8 @@ const TASK_PHASE = Object.freeze({
|
|
|
6
6
|
});
|
|
7
7
|
/** System subtypes that carry nothing the session model needs. */
|
|
8
8
|
const IGNORED_SYSTEM = new Set(['thinking_tokens', 'task_summary', 'post_turn_summary', 'status', 'hook_response', 'hook_started', 'compact_boundary']);
|
|
9
|
-
|
|
9
|
+
// `control_response` acknowledges our interrupt (`still_queued` lists commands that survive it).
|
|
10
|
+
const IGNORED_TYPES = new Set(['rate_limit_event', 'stream_event', 'keep_alive', 'control_response']);
|
|
10
11
|
function str(value) {
|
|
11
12
|
return typeof value === 'string' ? value : undefined;
|
|
12
13
|
}
|
|
@@ -25,6 +26,8 @@ export class ClaudeTranslator {
|
|
|
25
26
|
turnOpen = false;
|
|
26
27
|
interruptRequested = false;
|
|
27
28
|
written = new Set();
|
|
29
|
+
/** Inputs the CLI reported started; a later `cancelled` only echoes their interrupted turn. */
|
|
30
|
+
started = new Set();
|
|
28
31
|
startedInputs = [];
|
|
29
32
|
/** tool_use id → task id of the agent that issued it (null = main agent). */
|
|
30
33
|
toolOwner = new Map();
|
|
@@ -37,6 +40,8 @@ export class ClaudeTranslator {
|
|
|
37
40
|
noteInput(inputId) { this.written.add(inputId); }
|
|
38
41
|
noteInterrupt() { if (this.turnOpen)
|
|
39
42
|
this.interruptRequested = true; }
|
|
43
|
+
/** A turn is running and no stop was requested for it yet. */
|
|
44
|
+
get interruptible() { return this.turnOpen && !this.interruptRequested; }
|
|
40
45
|
translate(frame) {
|
|
41
46
|
const type = str(frame.type);
|
|
42
47
|
switch (type) {
|
|
@@ -145,8 +150,16 @@ export class ClaudeTranslator {
|
|
|
145
150
|
const state = str(frame.state);
|
|
146
151
|
if (!inputId || !this.written.has(inputId))
|
|
147
152
|
return [];
|
|
148
|
-
if (state === 'started'
|
|
149
|
-
this.
|
|
153
|
+
if (state === 'started') {
|
|
154
|
+
this.started.add(inputId);
|
|
155
|
+
if (!this.turnOpen)
|
|
156
|
+
this.startedInputs.push(inputId);
|
|
157
|
+
}
|
|
158
|
+
if (state === 'completed')
|
|
159
|
+
this.started.delete(inputId);
|
|
160
|
+
// After an interrupt the CLI reports the cut turn's input `cancelled`; the turn result already settled it.
|
|
161
|
+
if (state === 'cancelled' && this.started.has(inputId))
|
|
162
|
+
return [];
|
|
150
163
|
if (state === 'queued' || state === 'started' || state === 'completed')
|
|
151
164
|
return [{ kind: 'input.receipt', inputId, state }];
|
|
152
165
|
if (state === 'failed' || state === 'rejected' || state === 'cancelled')
|
|
@@ -18,6 +18,8 @@ export const CODEX_DESCRIPTOR = Object.freeze({
|
|
|
18
18
|
autonomousContinuation: false,
|
|
19
19
|
steer: true,
|
|
20
20
|
toolFiltering: false,
|
|
21
|
+
// `turn/interrupt`, then `turn/start` once the turn completed (sequenced by the application).
|
|
22
|
+
interrupt: 'native',
|
|
21
23
|
usage: Object.freeze({ costUsd: 'none', tokens: 'cumulative' }),
|
|
22
24
|
}),
|
|
23
25
|
});
|