@ours.network/fleet 1.1.0-nightly.9 → 1.1.1
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 +285 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +4 -4
- package/dist/application/role-creation-service.js +8 -1
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +92 -39
- package/dist/application/task-room-service.js +562 -289
- package/dist/briefing.js +79 -22
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +129 -47
- package/dist/config.d.ts +42 -3
- package/dist/config.js +202 -29
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +195 -41
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +22 -1
- package/dist/fleet-command-audit.js +355 -50
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +5 -1
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +9 -2
- package/dist/harness/codex-session.js +21 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +631 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +19 -3
- package/dist/owner-channel/channel.js +301 -120
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- package/dist/permissions.js +1 -1
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +450 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +361 -285
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -83
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +97 -47
- package/dist/session/acp.d.ts +27 -1
- package/dist/session/acp.js +307 -17
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/control.js +17 -1
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/stall-watchdog.d.ts +60 -0
- package/dist/session/stall-watchdog.js +210 -0
- package/dist/session/types.d.ts +29 -8
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
package/dist/session/acp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { Readable, Writable } from 'node:stream';
|
|
@@ -7,7 +7,8 @@ import * as acp from '@agentclientprotocol/sdk';
|
|
|
7
7
|
import { normalizeSessionUpdate } from './conversation-normalizer.js';
|
|
8
8
|
import { ConversationEventStore } from './conversation-store.js';
|
|
9
9
|
import { SessionEvents } from './events.js';
|
|
10
|
-
import {
|
|
10
|
+
import { DEFAULT_STALL_TIMEOUT_MS, STALL_RECOVERY_PROMPT, StallWatchdog, StallToolHistory, hasStallRecoveryClaim } from './stall-watchdog.js';
|
|
11
|
+
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, sessionBackendCapabilities, turnResult, } from './types.js';
|
|
11
12
|
const CANCEL_SETTLE_GRACE_MS = 15_000;
|
|
12
13
|
const CANCEL_TERMINATE_GRACE_MS = 5_000;
|
|
13
14
|
/** A permission no human answered is eventually a decision nobody made. */
|
|
@@ -158,6 +159,7 @@ function canonicallyWithin(root, candidates) {
|
|
|
158
159
|
function conversationSource(origin) {
|
|
159
160
|
switch (origin?.kind) {
|
|
160
161
|
case 'owner-admin-console': return { source: 'owner_admin_console', persistBody: true };
|
|
162
|
+
case 'stall-watchdog': return { source: 'fleet_monitor', persistBody: false };
|
|
161
163
|
case 'startup': return { source: 'startup', persistBody: true };
|
|
162
164
|
case 'owner': return { source: 'owner_channel', persistBody: false };
|
|
163
165
|
case 'fleet-monitor': return { source: 'fleet_monitor', persistBody: false };
|
|
@@ -187,6 +189,12 @@ export function runtimeSelector(options, category) {
|
|
|
187
189
|
const selected = choices.find(choice => choice.value === option.currentValue);
|
|
188
190
|
return { value: option.currentValue, ...(selected?.name ? { label: selected.name } : {}) };
|
|
189
191
|
}
|
|
192
|
+
function reasoningFromModelId(modelId) {
|
|
193
|
+
if (typeof modelId !== 'string')
|
|
194
|
+
return undefined;
|
|
195
|
+
const match = modelId.match(/\[([^\]]+)\]$/u);
|
|
196
|
+
return match?.[1] ? { value: match[1] } : undefined;
|
|
197
|
+
}
|
|
190
198
|
/**
|
|
191
199
|
* Classify an ACP `stopReason` into a terminal outcome. A refusal and a
|
|
192
200
|
* cancellation are the two ways a delivered prompt ends without being carried
|
|
@@ -206,6 +214,7 @@ export function classifyStopReason(stopReason) {
|
|
|
206
214
|
export class AcpSession {
|
|
207
215
|
options;
|
|
208
216
|
backend = 'acp';
|
|
217
|
+
capabilities;
|
|
209
218
|
pid;
|
|
210
219
|
child;
|
|
211
220
|
events;
|
|
@@ -232,7 +241,7 @@ export class AcpSession {
|
|
|
232
241
|
queueDepth = 0;
|
|
233
242
|
exit = null;
|
|
234
243
|
steeringSupported = false;
|
|
235
|
-
|
|
244
|
+
agentCapabilities;
|
|
236
245
|
runtimeModel;
|
|
237
246
|
reasoningEffort;
|
|
238
247
|
controllerCount = 0;
|
|
@@ -258,6 +267,15 @@ export class AcpSession {
|
|
|
258
267
|
terminate;
|
|
259
268
|
/** ACP-authenticated in-flight calls, including independently reserved permissions. */
|
|
260
269
|
activeToolCalls = new Map();
|
|
270
|
+
stallWatchdog;
|
|
271
|
+
stallToolHistory;
|
|
272
|
+
stallRecoveryClaimed = false;
|
|
273
|
+
managedTurnCount = 0;
|
|
274
|
+
steeringWasUsed = false;
|
|
275
|
+
steeringRequests = 0;
|
|
276
|
+
retryNativeTurnId;
|
|
277
|
+
stallTimer;
|
|
278
|
+
stallAttempt;
|
|
261
279
|
toolBoundaryWaiters = new Set();
|
|
262
280
|
activeTurn;
|
|
263
281
|
constructor(options, child, connection) {
|
|
@@ -265,6 +283,7 @@ export class AcpSession {
|
|
|
265
283
|
this.child = child;
|
|
266
284
|
this.connection = connection;
|
|
267
285
|
this.pid = child.pid ?? -1;
|
|
286
|
+
this.capabilities = sessionBackendCapabilities('acp', options.harness);
|
|
268
287
|
this.events = new SessionEvents(join(options.stateDir, '.session-events.jsonl'));
|
|
269
288
|
this.conversation = new ConversationEventStore(join(options.stateDir, '.conversation'), {
|
|
270
289
|
roleId: options.name, log: line => options.log(`[${options.name}] ${line}`),
|
|
@@ -277,6 +296,9 @@ export class AcpSession {
|
|
|
277
296
|
this.terminated.catch(() => undefined);
|
|
278
297
|
child.stderr.on('data', chunk => options.log(`[${options.name}] acp: ${String(chunk).trimEnd()}`));
|
|
279
298
|
child.once('exit', (code, signal) => {
|
|
299
|
+
if (this.stallTimer)
|
|
300
|
+
clearInterval(this.stallTimer);
|
|
301
|
+
this.stallTimer = undefined;
|
|
280
302
|
if (this.cancelForceKill)
|
|
281
303
|
clearTimeout(this.cancelForceKill);
|
|
282
304
|
this.cancelForceKill = undefined;
|
|
@@ -333,7 +355,8 @@ export class AcpSession {
|
|
|
333
355
|
let instance;
|
|
334
356
|
const app = acp.client({ name: 'ours-fleet' })
|
|
335
357
|
.onNotification(acp.methods.client.session.update, ({ params }) => {
|
|
336
|
-
instance
|
|
358
|
+
if (instance && (!instance.sessionId || params.sessionId === instance.sessionId))
|
|
359
|
+
instance.recordUpdate(params.update);
|
|
337
360
|
})
|
|
338
361
|
.onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
|
|
339
362
|
if (!instance)
|
|
@@ -345,6 +368,7 @@ export class AcpSession {
|
|
|
345
368
|
instance = new AcpSession(options, child, connection);
|
|
346
369
|
try {
|
|
347
370
|
await instance.initialize();
|
|
371
|
+
instance.startStallWatchdog();
|
|
348
372
|
instance.recoverOpenPrompts();
|
|
349
373
|
return instance;
|
|
350
374
|
}
|
|
@@ -463,10 +487,14 @@ export class AcpSession {
|
|
|
463
487
|
reserveTool(toolCallId) {
|
|
464
488
|
if (toolCallId)
|
|
465
489
|
this.toolCall(toolCallId).lifecycle = true;
|
|
490
|
+
else if (this.activeTurn)
|
|
491
|
+
this.activeTurn.boundaryUnknown = true;
|
|
466
492
|
}
|
|
467
493
|
reservePermission(toolCallId, permissionId) {
|
|
468
494
|
if (toolCallId)
|
|
469
495
|
this.toolCall(toolCallId).permissions.set(permissionId, 'pending');
|
|
496
|
+
else if (this.activeTurn)
|
|
497
|
+
this.activeTurn.boundaryUnknown = true;
|
|
470
498
|
}
|
|
471
499
|
allowPermission(toolCallId, permissionId) {
|
|
472
500
|
if (!toolCallId)
|
|
@@ -551,6 +579,8 @@ export class AcpSession {
|
|
|
551
579
|
* steering response from becoming a tight replay loop.
|
|
552
580
|
*/
|
|
553
581
|
async steerOrQueueWake(text, options) {
|
|
582
|
+
if (this.stallRecoveryClaimed)
|
|
583
|
+
return this.submitPrompt(text, { ...options, interrupt: false, steer: false });
|
|
554
584
|
const steered = await this.steerPrompt(text);
|
|
555
585
|
if (steered.accepted || steered.detail !== 'ACP steering failed'
|
|
556
586
|
|| this.closing || !this.isAlive())
|
|
@@ -614,6 +644,8 @@ export class AcpSession {
|
|
|
614
644
|
* for it is what turned a busy agent into a timeout and then into "dead".
|
|
615
645
|
*/
|
|
616
646
|
async queuePrompt(text, options = {}) {
|
|
647
|
+
if (this.stallRecoveryClaimed && options.origin?.kind === 'fleet-monitor')
|
|
648
|
+
options = { ...options, interrupt: false, steer: false };
|
|
617
649
|
if (this.cancelRecoveryReason)
|
|
618
650
|
throw new SessionControlError('control-unavailable', 'ACP adapter restart is in progress after the cancellation deadline', ACP_CANCEL_DEADLINE_EXCEEDED);
|
|
619
651
|
if (this.closing || !this.sessionId || !this.isAlive())
|
|
@@ -773,6 +805,8 @@ export class AcpSession {
|
|
|
773
805
|
}
|
|
774
806
|
}
|
|
775
807
|
async cancelActive(source) {
|
|
808
|
+
if (this.stallAttempt && source !== 'stall-watchdog' && source !== 'fleet-monitor')
|
|
809
|
+
this.stallAttempt.superseded = true;
|
|
776
810
|
if (!this.sessionId)
|
|
777
811
|
return;
|
|
778
812
|
const active = this.activeTurn;
|
|
@@ -981,6 +1015,11 @@ export class AcpSession {
|
|
|
981
1015
|
}
|
|
982
1016
|
async close() {
|
|
983
1017
|
this.closing = true;
|
|
1018
|
+
if (this.stallTimer)
|
|
1019
|
+
clearInterval(this.stallTimer);
|
|
1020
|
+
this.stallTimer = undefined;
|
|
1021
|
+
if (this.stallAttempt)
|
|
1022
|
+
this.stallAttempt.superseded = true;
|
|
984
1023
|
if (this.cancelEscalation)
|
|
985
1024
|
clearTimeout(this.cancelEscalation);
|
|
986
1025
|
this.cancelEscalation = undefined;
|
|
@@ -994,7 +1033,7 @@ export class AcpSession {
|
|
|
994
1033
|
for (const [permissionId, pending] of [...this.pendingPermissions])
|
|
995
1034
|
this.settlePendingAutomatically(permissionId, pending, 'cancelled', undefined, 'the session closed while this request was pending');
|
|
996
1035
|
this.releaseAllTools();
|
|
997
|
-
if (this.sessionId && this.
|
|
1036
|
+
if (this.sessionId && this.agentCapabilities?.sessionCapabilities?.close != null) {
|
|
998
1037
|
await this.connection.agent.request(acp.methods.agent.session.close, { sessionId: this.sessionId }).catch(() => undefined);
|
|
999
1038
|
}
|
|
1000
1039
|
this.connection.close();
|
|
@@ -1020,7 +1059,7 @@ export class AcpSession {
|
|
|
1020
1059
|
});
|
|
1021
1060
|
if (initialized.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
1022
1061
|
throw new Error(`ACP protocol mismatch: agent selected ${initialized.protocolVersion}, client supports ${acp.PROTOCOL_VERSION}`);
|
|
1023
|
-
this.
|
|
1062
|
+
this.agentCapabilities = initialized.agentCapabilities;
|
|
1024
1063
|
const steering = initialized._meta?.steering;
|
|
1025
1064
|
this.steeringSupported = steering !== null && typeof steering === 'object'
|
|
1026
1065
|
&& steering.supported === true;
|
|
@@ -1028,17 +1067,19 @@ export class AcpSession {
|
|
|
1028
1067
|
? readFileSync(this.sessionFile, 'utf8').trim()
|
|
1029
1068
|
: '';
|
|
1030
1069
|
let advertisedConfigOptions;
|
|
1031
|
-
|
|
1070
|
+
let advertisedModelId;
|
|
1071
|
+
if (persisted && this.agentCapabilities?.sessionCapabilities?.resume != null) {
|
|
1032
1072
|
const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
|
|
1033
1073
|
sessionId: persisted,
|
|
1034
1074
|
cwd: this.options.cwd,
|
|
1035
1075
|
mcpServers: this.declaredMcpServers(),
|
|
1036
1076
|
});
|
|
1037
1077
|
advertisedConfigOptions = resumed.configOptions;
|
|
1038
|
-
|
|
1078
|
+
advertisedModelId = resumed.models?.currentModelId;
|
|
1079
|
+
this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
|
|
1039
1080
|
this.sessionId = persisted;
|
|
1040
1081
|
}
|
|
1041
|
-
else if (persisted && this.
|
|
1082
|
+
else if (persisted && this.agentCapabilities?.loadSession) {
|
|
1042
1083
|
// `session/load` replays prior history as ordinary updates before the
|
|
1043
1084
|
// response; those records carry `agent_replay` provenance, never `agent`.
|
|
1044
1085
|
this.replaying = true;
|
|
@@ -1049,7 +1090,8 @@ export class AcpSession {
|
|
|
1049
1090
|
mcpServers: this.declaredMcpServers(),
|
|
1050
1091
|
});
|
|
1051
1092
|
advertisedConfigOptions = loaded.configOptions;
|
|
1052
|
-
|
|
1093
|
+
advertisedModelId = loaded.models?.currentModelId;
|
|
1094
|
+
this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
|
|
1053
1095
|
}
|
|
1054
1096
|
finally {
|
|
1055
1097
|
this.replaying = false;
|
|
@@ -1064,11 +1106,16 @@ export class AcpSession {
|
|
|
1064
1106
|
});
|
|
1065
1107
|
this.sessionId = created.sessionId;
|
|
1066
1108
|
advertisedConfigOptions = created.configOptions;
|
|
1067
|
-
|
|
1109
|
+
advertisedModelId = created.models?.currentModelId;
|
|
1110
|
+
this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
|
|
1068
1111
|
}
|
|
1069
1112
|
for (const selection of this.options.configSelections ?? []) {
|
|
1070
|
-
if (!advertisedConfigOptions?.some(option => option.id === selection.configId))
|
|
1113
|
+
if (!advertisedConfigOptions?.some(option => option.id === selection.configId)) {
|
|
1114
|
+
if (selection.configId === 'reasoning_effort'
|
|
1115
|
+
&& reasoningFromModelId(advertisedModelId)?.value === selection.value)
|
|
1116
|
+
continue;
|
|
1071
1117
|
throw new Error(`ACP agent did not advertise required session config option '${selection.configId}'`);
|
|
1118
|
+
}
|
|
1072
1119
|
let configured;
|
|
1073
1120
|
try {
|
|
1074
1121
|
configured = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId: selection.configId, value: selection.value });
|
|
@@ -1078,7 +1125,7 @@ export class AcpSession {
|
|
|
1078
1125
|
+ (error instanceof Error ? error.message : String(error)));
|
|
1079
1126
|
}
|
|
1080
1127
|
advertisedConfigOptions = configured.configOptions;
|
|
1081
|
-
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1128
|
+
this.captureRuntimeMetadata(advertisedConfigOptions, advertisedModelId);
|
|
1082
1129
|
const applied = advertisedConfigOptions.find(option => option.id === selection.configId);
|
|
1083
1130
|
if (applied?.currentValue !== selection.value)
|
|
1084
1131
|
throw new Error(`ACP agent did not apply required session config option '${selection.configId}' value '${selection.value}'`);
|
|
@@ -1110,17 +1157,162 @@ export class AcpSession {
|
|
|
1110
1157
|
acpSessionId: this.sessionId, payload: { status: 'idle' },
|
|
1111
1158
|
});
|
|
1112
1159
|
}
|
|
1113
|
-
captureRuntimeMetadata(options) {
|
|
1160
|
+
captureRuntimeMetadata(options, modelId) {
|
|
1114
1161
|
this.runtimeModel = runtimeSelector(options, 'model');
|
|
1115
|
-
this.reasoningEffort = runtimeSelector(options, 'thought_level');
|
|
1162
|
+
this.reasoningEffort = runtimeSelector(options, 'thought_level') ?? reasoningFromModelId(modelId);
|
|
1163
|
+
}
|
|
1164
|
+
startStallWatchdog() {
|
|
1165
|
+
if (!this.options.stallRecovery)
|
|
1166
|
+
return;
|
|
1167
|
+
const timeoutMs = this.options.stallRecovery.timeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
1168
|
+
this.stallRecoveryClaimed = hasStallRecoveryClaim(this.options.stateDir, this.sessionId);
|
|
1169
|
+
this.stallToolHistory = new StallToolHistory(this.options.stateDir, this.sessionId, this.options.mode === 'resume');
|
|
1170
|
+
this.stallWatchdog = new StallWatchdog({
|
|
1171
|
+
stateDir: this.options.stateDir, timeoutMs, previouslyClaimed: this.stallRecoveryClaimed, now: () => Date.now(),
|
|
1172
|
+
observe: () => this.stallObservation(),
|
|
1173
|
+
recover: (observed, report) => this.recoverStall(observed, report),
|
|
1174
|
+
diagnostic: diagnostic => {
|
|
1175
|
+
if (['interrupt_requested', 'blocked_previous_attempt', 'blocked_persistence'].includes(diagnostic.status))
|
|
1176
|
+
this.stallRecoveryClaimed = true;
|
|
1177
|
+
this.events.emit('stall_recovery', { status: diagnostic.status, stallDiagnostic: diagnostic });
|
|
1178
|
+
this.options.log(`[${this.options.name}] ACP stall recovery: ${diagnostic.status}; `
|
|
1179
|
+
+ 'inspect structured session events before continuing; never replay uncertain side effects');
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
this.stallTimer = setInterval(() => this.checkStallWatchdog(), this.options.stallRecovery.tickMs ?? Math.min(10_000, timeoutMs));
|
|
1183
|
+
this.stallTimer.unref?.();
|
|
1184
|
+
}
|
|
1185
|
+
checkStallWatchdog() {
|
|
1186
|
+
void this.stallWatchdog?.tick();
|
|
1187
|
+
const attempt = this.stallAttempt;
|
|
1188
|
+
if (attempt?.recoveryStartedAt === undefined || attempt.blocked || attempt.superseded)
|
|
1189
|
+
return;
|
|
1190
|
+
const observed = this.stallObservation();
|
|
1191
|
+
if (!observed?.safe || observed.turnId !== attempt.recoveryId)
|
|
1192
|
+
return;
|
|
1193
|
+
const last = observed.lastProgressAt || attempt.recoveryStartedAt;
|
|
1194
|
+
const timeoutMs = this.options.stallRecovery?.timeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
1195
|
+
if (Date.now() - last >= timeoutMs * 2) {
|
|
1196
|
+
attempt.blocked = true;
|
|
1197
|
+
try {
|
|
1198
|
+
attempt.report('blocked_restall');
|
|
1199
|
+
}
|
|
1200
|
+
catch { /* durable claim already prevents retry */ }
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
stallObservation() {
|
|
1204
|
+
const active = this.activeTurn;
|
|
1205
|
+
if (!active || !this.sessionId)
|
|
1206
|
+
return undefined;
|
|
1207
|
+
return {
|
|
1208
|
+
sessionId: this.sessionId, generation: this.sessionGeneration, turnId: active.id,
|
|
1209
|
+
startedAt: active.startedAt, lastProgressAt: active.lastProgressAt, progressCount: active.progressCount,
|
|
1210
|
+
transportFailures: active.transportFailures,
|
|
1211
|
+
boundaryEvidenceAvailable: this.stallToolHistory?.available() !== false,
|
|
1212
|
+
safe: this.isAlive() && !this.closing && this.readiness === 'running'
|
|
1213
|
+
&& !active.cancellationSource && !active.boundaryUnknown && !this.steeringOccupied
|
|
1214
|
+
&& this.steeringRequests === 0 && this.stallToolHistory?.available() !== false
|
|
1215
|
+
&& this.activeToolCalls.size === 0 && this.pendingPermissions.size === 0,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async recoverStall(observed, report) {
|
|
1219
|
+
const current = this.stallObservation();
|
|
1220
|
+
if (!current?.safe || current.turnId !== observed.turnId
|
|
1221
|
+
|| current.lastProgressAt !== observed.lastProgressAt || current.progressCount !== observed.progressCount) {
|
|
1222
|
+
report('superseded');
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const active = this.activeTurn;
|
|
1226
|
+
let resolveReady;
|
|
1227
|
+
const ready = new Promise(resolve => { resolveReady = resolve; });
|
|
1228
|
+
const attempt = this.stallAttempt = {
|
|
1229
|
+
turnId: active.id, recoveryId: randomUUID(), ready, superseded: false,
|
|
1230
|
+
report, resumed: false, blocked: false,
|
|
1231
|
+
};
|
|
1232
|
+
active.cancellationSource = 'stall-watchdog';
|
|
1233
|
+
// This intentionally does not use cancelActive: automatic recovery may
|
|
1234
|
+
// never enter its SIGTERM/SIGKILL escalation or settle a permission.
|
|
1235
|
+
let timer;
|
|
1236
|
+
try {
|
|
1237
|
+
const completed = await Promise.race([
|
|
1238
|
+
(async () => {
|
|
1239
|
+
await this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.sessionId });
|
|
1240
|
+
await active.settled;
|
|
1241
|
+
return true;
|
|
1242
|
+
})(),
|
|
1243
|
+
new Promise(resolve => {
|
|
1244
|
+
timer = setTimeout(() => resolve(false), this.options.stallRecovery?.cancelWaitMs ?? CANCEL_SETTLE_GRACE_MS);
|
|
1245
|
+
timer.unref?.();
|
|
1246
|
+
}),
|
|
1247
|
+
]);
|
|
1248
|
+
if (!completed || attempt.superseded || this.closing || !this.isAlive()) {
|
|
1249
|
+
attempt.blocked = true;
|
|
1250
|
+
report(attempt.superseded || this.closing ? 'superseded' : 'blocked_cancel');
|
|
1251
|
+
resolveReady(false);
|
|
1252
|
+
}
|
|
1253
|
+
else
|
|
1254
|
+
resolveReady(true);
|
|
1255
|
+
}
|
|
1256
|
+
catch {
|
|
1257
|
+
attempt.blocked = true;
|
|
1258
|
+
try {
|
|
1259
|
+
report('blocked_cancel');
|
|
1260
|
+
}
|
|
1261
|
+
finally {
|
|
1262
|
+
resolveReady(false);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
finally {
|
|
1266
|
+
if (timer)
|
|
1267
|
+
clearTimeout(timer);
|
|
1268
|
+
}
|
|
1116
1269
|
}
|
|
1270
|
+
/** Keep the original queue slot (including startup) until recovery finishes. */
|
|
1117
1271
|
async runPrompt(text, turnId = randomUUID(), origin) {
|
|
1272
|
+
const result = await this.runSinglePrompt(text, turnId, origin);
|
|
1273
|
+
const attempt = this.stallAttempt;
|
|
1274
|
+
if (!attempt || attempt.turnId !== turnId)
|
|
1275
|
+
return result;
|
|
1276
|
+
const ready = await attempt.ready;
|
|
1277
|
+
if (attempt.superseded || this.closing)
|
|
1278
|
+
return result;
|
|
1279
|
+
if (!ready || result.outcome !== 'cancelled' || result.cancellationSource !== 'stall-watchdog') {
|
|
1280
|
+
// An RPC error/refusal/ambiguous terminal answer to cancellation is not
|
|
1281
|
+
// permission to replay work or to fail startup and restart the process.
|
|
1282
|
+
if (!attempt.blocked) {
|
|
1283
|
+
attempt.blocked = true;
|
|
1284
|
+
try {
|
|
1285
|
+
attempt.report('blocked_cancel');
|
|
1286
|
+
}
|
|
1287
|
+
catch { /* claim remains durable */ }
|
|
1288
|
+
}
|
|
1289
|
+
return turnResult(true, 'cancelled', 'diagnostic cancellation requires operator attention', undefined, 'stall-watchdog');
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
attempt.report('recovery_started');
|
|
1293
|
+
attempt.recoveryStartedAt = Date.now();
|
|
1294
|
+
const recoveryOrigin = origin?.kind === 'scheduled-loop'
|
|
1295
|
+
? origin : { kind: 'stall-watchdog' };
|
|
1296
|
+
this.admitToLedger(attempt.recoveryId, STALL_RECOVERY_PROMPT, 0, { origin: recoveryOrigin });
|
|
1297
|
+
const recovered = await this.runSinglePrompt(STALL_RECOVERY_PROMPT, attempt.recoveryId, recoveryOrigin);
|
|
1298
|
+
attempt.report(attempt.superseded ? 'superseded'
|
|
1299
|
+
: recovered.succeeded && attempt.resumed ? 'recovery_completed' : 'blocked_recovery');
|
|
1300
|
+
// A failed diagnostic turn must not make startup tear down the session.
|
|
1301
|
+
return recovered.succeeded && attempt.resumed ? recovered : turnResult(true, 'cancelled', 'diagnostic recovery requires operator attention', undefined, 'stall-watchdog');
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
return turnResult(true, 'cancelled', 'diagnostic recovery requires operator attention', undefined, 'stall-watchdog');
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async runSinglePrompt(text, turnId = randomUUID(), origin) {
|
|
1118
1308
|
if (!this.sessionId || !this.isAlive())
|
|
1119
1309
|
return turnResult(false, 'failed', this.lastError ?? 'ACP session is offline');
|
|
1120
1310
|
this.readiness = 'running';
|
|
1311
|
+
this.managedTurnCount++;
|
|
1312
|
+
this.retryNativeTurnId = undefined;
|
|
1121
1313
|
let settle;
|
|
1122
1314
|
const settled = new Promise(resolve => { settle = resolve; });
|
|
1123
|
-
this.activeTurn = { id: turnId, output: '', origin, settled, settle };
|
|
1315
|
+
this.activeTurn = { toolEvidence: new Map(), startedAt: Date.now(), toolIds: new Set(), lastProgressAt: 0, progressCount: 0, transportFailures: 0, boundaryUnknown: false, id: turnId, output: '', origin, settled, settle };
|
|
1124
1316
|
this.events.emit('state', { turnId, status: 'running', origin });
|
|
1125
1317
|
this.conversation.appendSafe({
|
|
1126
1318
|
kind: 'prompt.started', sessionGeneration: this.sessionGeneration,
|
|
@@ -1164,7 +1356,8 @@ export class AcpSession {
|
|
|
1164
1356
|
this.lastError = origin?.kind === 'scheduled-loop' ? 'scheduled-loop turn failed' : detail;
|
|
1165
1357
|
this.readiness = this.isAlive() ? 'idle' : 'failed';
|
|
1166
1358
|
this.events.emit('error', {
|
|
1167
|
-
turnId, origin
|
|
1359
|
+
turnId, origin: this.activeTurn?.cancellationSource === 'stall-watchdog'
|
|
1360
|
+
? { kind: 'stall-watchdog' } : origin,
|
|
1168
1361
|
text: origin?.kind === 'scheduled-loop' ? 'scheduled-loop turn failed' : this.lastError,
|
|
1169
1362
|
});
|
|
1170
1363
|
if (this.isAlive())
|
|
@@ -1192,8 +1385,10 @@ export class AcpSession {
|
|
|
1192
1385
|
}
|
|
1193
1386
|
}
|
|
1194
1387
|
async steerPrompt(text) {
|
|
1388
|
+
this.steeringWasUsed = true;
|
|
1195
1389
|
if (!this.sessionId || !this.isAlive())
|
|
1196
1390
|
return turnResult(false, 'failed', this.lastError ?? 'ACP session is offline');
|
|
1391
|
+
this.steeringRequests++;
|
|
1197
1392
|
try {
|
|
1198
1393
|
const response = await Promise.race([
|
|
1199
1394
|
this.connection.agent.request('_session/steering', {
|
|
@@ -1218,6 +1413,9 @@ export class AcpSession {
|
|
|
1218
1413
|
this.events.emit('error', { text: detail });
|
|
1219
1414
|
return turnResult(false, 'failed', detail);
|
|
1220
1415
|
}
|
|
1416
|
+
finally {
|
|
1417
|
+
this.steeringRequests--;
|
|
1418
|
+
}
|
|
1221
1419
|
}
|
|
1222
1420
|
requestPermission(params) {
|
|
1223
1421
|
// `kinds` is a PRIORITY order. Scanning the agent's option array instead
|
|
@@ -1393,6 +1591,48 @@ export class AcpSession {
|
|
|
1393
1591
|
&& params.options.some(option => option.optionId === 'allow_once' && option.kind === 'allow_once')
|
|
1394
1592
|
&& params.options.some(option => option.optionId === 'decline' && option.kind === 'reject_once');
|
|
1395
1593
|
}
|
|
1594
|
+
/** Pinned codex-acp 1.1.7 structured metadata, never stderr or assistant text. */
|
|
1595
|
+
recordStallMetadata(update) {
|
|
1596
|
+
if (this.options.permissionMetadataSource !== 'codex-acp'
|
|
1597
|
+
|| update.sessionUpdate !== 'session_info_update' || !this.activeTurn)
|
|
1598
|
+
return;
|
|
1599
|
+
const meta = update._meta?.codex;
|
|
1600
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta))
|
|
1601
|
+
return;
|
|
1602
|
+
const codex = meta;
|
|
1603
|
+
const status = codex.threadStatus;
|
|
1604
|
+
if (Object.prototype.hasOwnProperty.call(codex, 'threadStatus')) {
|
|
1605
|
+
const value = status && typeof status === 'object' && !Array.isArray(status)
|
|
1606
|
+
? status : {};
|
|
1607
|
+
// Unknown or modal thread status is a permanent conservative fence for
|
|
1608
|
+
// this turn. A later delayed idle/active status must not clear it.
|
|
1609
|
+
if (value.type !== 'active' || !Array.isArray(value.activeFlags)
|
|
1610
|
+
|| value.activeFlags.length > 0)
|
|
1611
|
+
this.activeTurn.boundaryUnknown = true;
|
|
1612
|
+
}
|
|
1613
|
+
const error = codex.error;
|
|
1614
|
+
if (!error || typeof error !== 'object' || Array.isArray(error))
|
|
1615
|
+
return;
|
|
1616
|
+
const value = error;
|
|
1617
|
+
const info = value.codexErrorInfo;
|
|
1618
|
+
if (value.willRetry !== true || typeof value.turnId !== 'string' || !value.turnId
|
|
1619
|
+
|| !info || typeof info !== 'object' || Array.isArray(info))
|
|
1620
|
+
return;
|
|
1621
|
+
if (!['responseStreamConnectionFailed', 'responseStreamDisconnected']
|
|
1622
|
+
.some(key => Object.prototype.hasOwnProperty.call(info, key)))
|
|
1623
|
+
return;
|
|
1624
|
+
// ACP 1.1.7 does not expose a native-turn-to-prompt mapping. Only the first
|
|
1625
|
+
// fresh managed turn with no steering can be correlated without guessing;
|
|
1626
|
+
// later/resumed turns retain the conservative generic no-progress path.
|
|
1627
|
+
if (this.options.mode !== 'fresh' || this.managedTurnCount !== 1 || this.steeringWasUsed)
|
|
1628
|
+
return;
|
|
1629
|
+
if (this.retryNativeTurnId && this.retryNativeTurnId !== value.turnId) {
|
|
1630
|
+
this.activeTurn.boundaryUnknown = true;
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
this.retryNativeTurnId = value.turnId;
|
|
1634
|
+
this.activeTurn.transportFailures++;
|
|
1635
|
+
}
|
|
1396
1636
|
recordUpdate(update) {
|
|
1397
1637
|
// Replayed history is not current activity: `session/load` would otherwise
|
|
1398
1638
|
// make a cold session look like it had just been working. The same reason
|
|
@@ -1402,6 +1642,53 @@ export class AcpSession {
|
|
|
1402
1642
|
this.lastUpdateAt = new Date().toISOString();
|
|
1403
1643
|
this.refreshSteeringOccupancy();
|
|
1404
1644
|
}
|
|
1645
|
+
if (!this.replaying && this.activeTurn) {
|
|
1646
|
+
this.recordStallMetadata(update);
|
|
1647
|
+
const active = this.activeTurn;
|
|
1648
|
+
const kind = update.sessionUpdate;
|
|
1649
|
+
if (this.options.stallRecovery && (kind === 'tool_call' || kind === 'tool_call_update')) {
|
|
1650
|
+
if (!update.toolCallId || active.toolIds.size >= 4096)
|
|
1651
|
+
active.boundaryUnknown = true;
|
|
1652
|
+
else
|
|
1653
|
+
active.toolIds.add(update.toolCallId);
|
|
1654
|
+
if (this.stallToolHistory?.observe(update.toolCallId, active.id) === false)
|
|
1655
|
+
active.boundaryUnknown = true;
|
|
1656
|
+
}
|
|
1657
|
+
let meaningful = ((kind === 'agent_message_chunk' || kind === 'agent_thought_chunk')
|
|
1658
|
+
&& (update.content.type !== 'text' || update.content.text.length > 0))
|
|
1659
|
+
|| kind === 'tool_call' || kind === 'tool_call_update' || kind === 'plan';
|
|
1660
|
+
if (this.options.stallRecovery && (kind === 'tool_call' || kind === 'tool_call_update' || kind === 'plan')) {
|
|
1661
|
+
const fingerprint = createHash('sha256').update(JSON.stringify(update)).digest('hex');
|
|
1662
|
+
if (kind === 'plan') {
|
|
1663
|
+
meaningful = fingerprint !== active.planEvidence;
|
|
1664
|
+
active.planEvidence = fingerprint;
|
|
1665
|
+
}
|
|
1666
|
+
else if (active.toolEvidence.size < 4096 || active.toolEvidence.has(update.toolCallId)) {
|
|
1667
|
+
meaningful = fingerprint !== active.toolEvidence.get(update.toolCallId);
|
|
1668
|
+
active.toolEvidence.set(update.toolCallId, fingerprint);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (this.options.stallRecovery && ![
|
|
1672
|
+
'agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan',
|
|
1673
|
+
'available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update',
|
|
1674
|
+
].includes(kind))
|
|
1675
|
+
active.boundaryUnknown = true;
|
|
1676
|
+
if (meaningful) {
|
|
1677
|
+
active.lastProgressAt = Date.now();
|
|
1678
|
+
active.progressCount++;
|
|
1679
|
+
active.transportFailures = 0;
|
|
1680
|
+
const attempt = this.stallAttempt;
|
|
1681
|
+
if (attempt?.recoveryId === active.id && !attempt.resumed) {
|
|
1682
|
+
attempt.resumed = true;
|
|
1683
|
+
try {
|
|
1684
|
+
attempt.report('progress_resumed');
|
|
1685
|
+
}
|
|
1686
|
+
catch {
|
|
1687
|
+
attempt.blocked = true;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1405
1692
|
const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
|
|
1406
1693
|
const messagePhase = update.sessionUpdate === 'agent_message_chunk'
|
|
1407
1694
|
? this.codexMessagePhase(update) : undefined;
|
|
@@ -1459,6 +1746,9 @@ export class AcpSession {
|
|
|
1459
1746
|
this.releaseTool(update.toolCallId);
|
|
1460
1747
|
else if (update.status !== undefined)
|
|
1461
1748
|
this.reserveTool(update.toolCallId);
|
|
1749
|
+
else if (this.options.stallRecovery && this.activeTurn
|
|
1750
|
+
&& !this.activeToolCalls.has(update.toolCallId))
|
|
1751
|
+
this.activeTurn.boundaryUnknown = true;
|
|
1462
1752
|
break;
|
|
1463
1753
|
default:
|
|
1464
1754
|
break;
|
|
@@ -17,8 +17,9 @@ export type ScheduledAttempt = {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare class RoleTurnArbiter implements AgentSession {
|
|
19
19
|
private readonly session;
|
|
20
|
-
readonly backend: "
|
|
20
|
+
readonly backend: import("../config.js").SessionBackendId;
|
|
21
21
|
readonly pid: number;
|
|
22
|
+
readonly capabilities: import("./types.js").AgentSessionCapabilities | undefined;
|
|
22
23
|
private tail;
|
|
23
24
|
private unsettled;
|
|
24
25
|
private stopping;
|
package/dist/session/arbiter.js
CHANGED
|
@@ -25,6 +25,7 @@ export class RoleTurnArbiter {
|
|
|
25
25
|
session;
|
|
26
26
|
backend;
|
|
27
27
|
pid;
|
|
28
|
+
capabilities;
|
|
28
29
|
tail = Promise.resolve();
|
|
29
30
|
unsettled = 0;
|
|
30
31
|
stopping = false;
|
|
@@ -36,6 +37,7 @@ export class RoleTurnArbiter {
|
|
|
36
37
|
this.session = session;
|
|
37
38
|
this.backend = session.backend;
|
|
38
39
|
this.pid = session.pid;
|
|
40
|
+
this.capabilities = session.capabilities;
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
43
|
* Waiters race the tail against their own generation's retirement, so a
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
import type { ExitRecord } from './types.js';
|
|
3
|
+
type JsonObject = Record<string, unknown>;
|
|
4
|
+
type RequestId = string | number;
|
|
5
|
+
export interface CodexAppServerTransportOptions {
|
|
6
|
+
name: string;
|
|
7
|
+
argv: string[];
|
|
8
|
+
cwd: string;
|
|
9
|
+
env: Record<string, string>;
|
|
10
|
+
log(line: string): void;
|
|
11
|
+
requestTimeoutMs?: number;
|
|
12
|
+
onNotification?(method: string, params: JsonObject): void;
|
|
13
|
+
onRequest?(method: string, id: RequestId, params: JsonObject): void;
|
|
14
|
+
onExit?(exit: ExitRecord): void;
|
|
15
|
+
}
|
|
16
|
+
export interface CodexAppServerConnection {
|
|
17
|
+
readonly pid: number;
|
|
18
|
+
readonly child: Pick<ChildProcessWithoutNullStreams, 'kill'>;
|
|
19
|
+
isAlive(): boolean;
|
|
20
|
+
exitResult(): ExitRecord | null;
|
|
21
|
+
request<T = unknown>(method: string, params?: JsonObject, timeoutMs?: number): Promise<T>;
|
|
22
|
+
notify(method: string, params?: JsonObject): void;
|
|
23
|
+
respond(id: RequestId, result: unknown): void;
|
|
24
|
+
respondError(id: RequestId, code: number, message: string): void;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export type CodexAppServerTransportFactory = (options: CodexAppServerTransportOptions) => Promise<CodexAppServerConnection>;
|
|
28
|
+
/** Minimal, version-tolerant JSONL client for Codex app-server's stdio transport. */
|
|
29
|
+
export declare class CodexAppServerTransport implements CodexAppServerConnection {
|
|
30
|
+
private readonly options;
|
|
31
|
+
readonly child: ChildProcessWithoutNullStreams;
|
|
32
|
+
readonly pid: number;
|
|
33
|
+
private nextId;
|
|
34
|
+
private readonly pending;
|
|
35
|
+
private readonly requestTimeoutMs;
|
|
36
|
+
private readonly exited;
|
|
37
|
+
private resolveExited;
|
|
38
|
+
private closed;
|
|
39
|
+
private exit;
|
|
40
|
+
private constructor();
|
|
41
|
+
static start(options: CodexAppServerTransportOptions): Promise<CodexAppServerTransport>;
|
|
42
|
+
isAlive(): boolean;
|
|
43
|
+
exitResult(): ExitRecord | null;
|
|
44
|
+
request<T = unknown>(method: string, params?: JsonObject, timeoutMs?: number): Promise<T>;
|
|
45
|
+
notify(method: string, params?: JsonObject): void;
|
|
46
|
+
respond(id: RequestId, result: unknown): void;
|
|
47
|
+
respondError(id: RequestId, code: number, message: string): void;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
private waitForExit;
|
|
50
|
+
private write;
|
|
51
|
+
private receive;
|
|
52
|
+
}
|
|
53
|
+
export {};
|