@ours.network/fleet 1.1.0-nightly.3 → 1.1.0-nightly.30
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 +244 -40
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- 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 +19 -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 +93 -40
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -24
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +217 -51
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +38 -3
- package/dist/config.js +240 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +213 -42
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +181 -0
- package/dist/fleet-command-audit.js +738 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- 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 +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -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 +4 -3
- 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 +75 -3
- package/dist/owner-channel/channel.js +531 -123
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +112 -36
- 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/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- 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 +519 -261
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -29
- 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 +9 -0
- package/dist/rooms-tasks/external-worker.js +52 -9
- 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 -56
- 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 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +94 -8
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +167 -56
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- 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 +14 -1
- package/dist/session/control.js +51 -0
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- 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 +5 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/agent_templates/Critic.yaml +5 -0
- package/presets/fleet/agent_templates/Developer.yaml +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- 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 +30 -0
- package/presets/fleet/roles/Critic.yaml +28 -0
- package/presets/fleet/roles/Developer.yaml +28 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +13 -0
- package/presets/fleet/room_templates/single.yaml +9 -0
- package/presets/fleet/room_templates/team.yaml +16 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
package/dist/runner.js
CHANGED
|
@@ -6,13 +6,15 @@ import { agentDir, stateRoot } from './paths.js';
|
|
|
6
6
|
import { loadConfig, findRole, isolationContextFor, resolveMonitorConfig, resolvePermissions, } from './config.js';
|
|
7
7
|
import { getAdapter } from './harness/registry.js';
|
|
8
8
|
import { createMonitor, probeIdentityPresence, } from './monitor.js';
|
|
9
|
+
import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
|
|
10
|
+
import { recoverAgentIdentity } from './agent-recovery-gate.js';
|
|
9
11
|
import { realExec } from './exec.js';
|
|
10
12
|
import { resolveIsolation } from './isolation/policy.js';
|
|
11
13
|
import { selectIsolationBackend } from './isolation/registry.js';
|
|
12
14
|
import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
|
|
13
15
|
import { resolveLaunchRuntime } from './isolation/runtime.js';
|
|
14
16
|
import { controlRequest, RoleControlServer } from './session/control.js';
|
|
15
|
-
import { ACP_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus } from './session/types.js';
|
|
17
|
+
import { ACP_CANCEL_DEADLINE_EXCEEDED, CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus, } from './session/types.js';
|
|
16
18
|
import { effectiveModelForRole, modelRecoveryHeld, reconcileModelRecovery, recordModelFailure, classifyFailureText, } from './model-recovery.js';
|
|
17
19
|
import { rotateWorklog } from './worklog.js';
|
|
18
20
|
import { reconcilePermanentRoleIdentities } from './creation.js';
|
|
@@ -24,6 +26,15 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
|
|
|
24
26
|
import { effectivePermissionMode } from './permissions.js';
|
|
25
27
|
import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
|
|
26
28
|
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
|
|
29
|
+
import { FleetCommandAuditStore } from './fleet-command-audit.js';
|
|
30
|
+
export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
|
|
31
|
+
export class SupervisorRecycleRequiredError extends Error {
|
|
32
|
+
code = SUPERVISOR_RECYCLE_REQUIRED;
|
|
33
|
+
constructor() {
|
|
34
|
+
super('owner channel requires a fresh supervisor process after unproven client quiescence');
|
|
35
|
+
this.name = 'SupervisorRecycleRequiredError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
27
38
|
const defaultDeps = () => ({
|
|
28
39
|
exec: realExec,
|
|
29
40
|
cpuDelegated: () => cpuControllerDelegated(),
|
|
@@ -38,10 +49,12 @@ const defaultDeps = () => ({
|
|
|
38
49
|
now: () => Date.now(),
|
|
39
50
|
log: line => process.stderr.write(line + '\n'),
|
|
40
51
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
52
|
+
probeGeneration: env => probeDaemonGeneration((url, init) => globalThis.fetch(url, init), env),
|
|
41
53
|
createMonitor: opts => createMonitor(opts),
|
|
42
54
|
createOwnerChannel: opts => new OwnerChannel(opts),
|
|
43
55
|
startAgentSession: (adapter, options) => adapter.start(options),
|
|
44
56
|
createControlServer: (stateDir, session, log) => new RoleControlServer(stateDir, session, log),
|
|
57
|
+
createLoopManager: (...args) => new ScheduledLoopManager(...args),
|
|
45
58
|
acquireOwnerBinder: (stateDir, role, identity) => acquireOwnerBinderLease(stateDir, role, identity),
|
|
46
59
|
reportOwnerStartupFailure: async (stateDir) => {
|
|
47
60
|
const response = await controlRequest(stateDir, {
|
|
@@ -58,6 +71,32 @@ const defaultDeps = () => ({
|
|
|
58
71
|
});
|
|
59
72
|
const MONITOR_OWNER_FILE = '.monitor-owner';
|
|
60
73
|
const OBSOLETE_OURS_AUTOSTART_ENV = 'OURS_AUTOSTART';
|
|
74
|
+
function localFleetAuditor(stateDir, caller, log) {
|
|
75
|
+
const store = new FleetCommandAuditStore(join(stateDir, '.fleet-command-audit.json'));
|
|
76
|
+
return {
|
|
77
|
+
async begin(requestId, argv) {
|
|
78
|
+
let attempt = store.begin(requestId, caller, argv);
|
|
79
|
+
if (attempt.invocation === 'sending') {
|
|
80
|
+
log(`[${caller}] fleet proxy command ${attempt.correlationId} `
|
|
81
|
+
+ `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
|
|
82
|
+
attempt = store.invocation(attempt.correlationId, caller, 'delivered');
|
|
83
|
+
}
|
|
84
|
+
return attempt;
|
|
85
|
+
},
|
|
86
|
+
async finish(input) {
|
|
87
|
+
let attempt = store.finish(input.correlationId, caller, {
|
|
88
|
+
class: input.class, effect: input.effect,
|
|
89
|
+
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
90
|
+
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
91
|
+
...(input.presentations ? { presentations: input.presentations } : {}),
|
|
92
|
+
});
|
|
93
|
+
if (attempt.outcome?.delivery === 'sending')
|
|
94
|
+
attempt = store.outcome(attempt.correlationId, caller, 'delivered');
|
|
95
|
+
return attempt;
|
|
96
|
+
},
|
|
97
|
+
async present() { },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
61
100
|
/** Environment injected only into the managed harness process. */
|
|
62
101
|
export function managedFleetProxyEnv(role, stateDir) {
|
|
63
102
|
const roleEnv = { ...(role.env ?? {}) };
|
|
@@ -453,6 +492,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
453
492
|
const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
|
|
454
493
|
const prep = await adapter.prepareSession(role, { stateDir: dir, runCwd });
|
|
455
494
|
const sessionBackend = role.session ?? 'acp';
|
|
495
|
+
const sessionLabel = sessionBackend === 'acp' ? 'ACP' : 'Codex app-server';
|
|
456
496
|
let launch = adapter.agentSession.prepareLaunch(role, prep);
|
|
457
497
|
// Isolation is additive: only roles that declare `isolation:` are wrapped.
|
|
458
498
|
let wrappedArgv = launch.argv;
|
|
@@ -532,6 +572,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
532
572
|
name, identity: role.identity, agentDir: dir, cfg: role.monitor,
|
|
533
573
|
deps: resolvedMonitorDeps,
|
|
534
574
|
}) : null;
|
|
575
|
+
const daemonObserver = new DaemonGenerationObserver();
|
|
576
|
+
const initialDaemon = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
|
|
577
|
+
let sessionStartedWithoutDaemonBaseline = initialDaemon.kind !== 'baseline';
|
|
535
578
|
if (monitor)
|
|
536
579
|
await monitor.prime({ resetCursor: resetMonitorCursor });
|
|
537
580
|
rmSync(exitFile, { force: true });
|
|
@@ -540,13 +583,13 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
540
583
|
let control;
|
|
541
584
|
let unsubscribeRecovery;
|
|
542
585
|
let monitorLoop;
|
|
543
|
-
let
|
|
586
|
+
let sessionStartupComplete = false;
|
|
544
587
|
let sessionClosed = false;
|
|
545
588
|
let ownerChannel;
|
|
546
589
|
let ownerBinder;
|
|
547
|
-
const pendingFleetSpawnNotices = [];
|
|
548
590
|
let loopManager;
|
|
549
591
|
let arbiter;
|
|
592
|
+
let recoveryController;
|
|
550
593
|
let reloadLoopConfig;
|
|
551
594
|
let loopGeneration = JSON.stringify((role.loops ?? []).map(loop => [
|
|
552
595
|
loop.name, loop.definitionHash, loop.promptHash,
|
|
@@ -571,7 +614,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
571
614
|
unsubscribeRecovery = agentSession.subscribe(event => {
|
|
572
615
|
if (event.kind !== 'error' || !event.text)
|
|
573
616
|
return;
|
|
574
|
-
const evidence = classifyFailureText(event.text,
|
|
617
|
+
const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
|
|
575
618
|
if (evidence)
|
|
576
619
|
resolvedMonitorDeps.onFailureEvidence?.(evidence);
|
|
577
620
|
});
|
|
@@ -596,33 +639,50 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
596
639
|
+ `${error?.message ?? String(error)}`);
|
|
597
640
|
}
|
|
598
641
|
}
|
|
599
|
-
control = deps.createControlServer(dir, arbiter, deps.log);
|
|
600
642
|
try {
|
|
643
|
+
if (role.owner_channel)
|
|
644
|
+
ownerChannel = deps.createOwnerChannel({
|
|
645
|
+
role: name,
|
|
646
|
+
harness: role.harness,
|
|
647
|
+
config: role.owner_channel,
|
|
648
|
+
session: arbiter,
|
|
649
|
+
stateDir: dir,
|
|
650
|
+
env: role.env,
|
|
651
|
+
log: deps.log,
|
|
652
|
+
...(ownerBinder ? { binderLease: ownerBinder } : {}),
|
|
653
|
+
...(configPath ? { configPath } : {}),
|
|
654
|
+
});
|
|
655
|
+
control = deps.createControlServer(dir, arbiter, deps.log);
|
|
656
|
+
control.setFleetAuditor(ownerChannel ? {
|
|
657
|
+
begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
|
|
658
|
+
finish: input => ownerChannel.finishFleetCommandAudit(input),
|
|
659
|
+
present: presentations => ownerChannel.notifyFleetLifecycle(presentations),
|
|
660
|
+
} : localFleetAuditor(dir, name, deps.log));
|
|
601
661
|
await control.start();
|
|
602
662
|
}
|
|
603
663
|
catch (error) {
|
|
664
|
+
await ownerChannel?.close().catch(() => undefined);
|
|
604
665
|
ownerBinder?.release();
|
|
605
666
|
await agentSession.close();
|
|
606
667
|
unsubscribeRecovery?.();
|
|
607
668
|
throw error;
|
|
608
669
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
670
|
+
if (!role.roomMemberStartup)
|
|
671
|
+
control.setFleetSpawner(async (requested) => {
|
|
672
|
+
const event = await executeManagedSpawn(role, configPath, requested, deps.log);
|
|
673
|
+
// Room-member launches are collected with their Task/Room transaction so
|
|
674
|
+
// causal delivery is Task → Room → Agents → Room active → Task active.
|
|
675
|
+
if (!requested.roomMemberStartup && role.owner_channel && ownerChannel?.notifyFleetSpawn) {
|
|
676
|
+
try {
|
|
677
|
+
await ownerChannel.notifyFleetSpawn(event);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
deps.log(`[${name}] Agent lifecycle notice delivery failed: `
|
|
681
|
+
+ `${error?.message ?? String(error)}`);
|
|
682
|
+
}
|
|
620
683
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
pendingFleetSpawnNotices.push(event);
|
|
624
|
-
return event;
|
|
625
|
-
});
|
|
684
|
+
return event;
|
|
685
|
+
});
|
|
626
686
|
resolvedMonitorDeps.delivery = {
|
|
627
687
|
// A wake is only delivered when its turn TERMINATES successfully. A
|
|
628
688
|
// refusal or a cancellation reached the agent and was not acted on, so
|
|
@@ -633,7 +693,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
633
693
|
// steer into the live turn instead; after it completes, honor the
|
|
634
694
|
// configured interrupt policy normally.
|
|
635
695
|
const policy = options?.interrupt;
|
|
636
|
-
const interrupt = policy === true &&
|
|
696
|
+
const interrupt = policy === true && sessionStartupComplete;
|
|
637
697
|
const promptOptions = {
|
|
638
698
|
interrupt, steer: true,
|
|
639
699
|
...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
|
|
@@ -641,7 +701,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
641
701
|
};
|
|
642
702
|
// Startup is already a protected boundary: as with immediate mode,
|
|
643
703
|
// steer rather than waiting on/cancelling the runner-owned first turn.
|
|
644
|
-
const result = policy === 'after_tool' &&
|
|
704
|
+
const result = policy === 'after_tool' && sessionStartupComplete
|
|
645
705
|
? await arbiter.submitPromptAfterTool(text, promptOptions)
|
|
646
706
|
: await arbiter.submitPrompt(text, promptOptions);
|
|
647
707
|
const steered = result.accepted
|
|
@@ -702,32 +762,21 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
702
762
|
exit: {
|
|
703
763
|
version: 1,
|
|
704
764
|
class: 'program-exit',
|
|
705
|
-
detail:
|
|
765
|
+
detail: `${sessionLabel} startup triggered model recovery (${modelRecovery})`,
|
|
706
766
|
},
|
|
707
767
|
rotated: false,
|
|
708
768
|
mode,
|
|
709
769
|
modelRecovery,
|
|
710
770
|
};
|
|
711
771
|
}
|
|
712
|
-
throw new Error(`[${name}]
|
|
772
|
+
throw new Error(`[${name}] ${sessionLabel} startup prompt ${started.outcome}` +
|
|
713
773
|
`${started.detail ? `: ${started.detail}` : ''}`);
|
|
714
774
|
}
|
|
715
775
|
if (interruptedForWake)
|
|
716
|
-
deps.log(`[${name}]
|
|
776
|
+
deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
|
|
717
777
|
+ 'keeping temporary supervisor alive');
|
|
718
|
-
|
|
719
|
-
if (
|
|
720
|
-
ownerChannel = deps.createOwnerChannel({
|
|
721
|
-
role: name,
|
|
722
|
-
harness: role.harness,
|
|
723
|
-
config: role.owner_channel,
|
|
724
|
-
session: arbiter,
|
|
725
|
-
stateDir: dir,
|
|
726
|
-
env: role.env,
|
|
727
|
-
log: deps.log,
|
|
728
|
-
...(ownerBinder ? { binderLease: ownerBinder } : {}),
|
|
729
|
-
...(configPath ? { configPath } : {}),
|
|
730
|
-
});
|
|
778
|
+
sessionStartupComplete = true;
|
|
779
|
+
if (ownerChannel) {
|
|
731
780
|
try {
|
|
732
781
|
await ownerChannel.start();
|
|
733
782
|
}
|
|
@@ -743,18 +792,10 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
743
792
|
throw new Error(`[${name}] owner channel failed to start: `
|
|
744
793
|
+ `${error?.message ?? String(error)}`);
|
|
745
794
|
}
|
|
746
|
-
for (const event of pendingFleetSpawnNotices.splice(0)) {
|
|
747
|
-
try {
|
|
748
|
-
await ownerChannel.notifyFleetSpawn?.(event);
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
deps.log(`[${name}] deferred spawned-agent owner notice failed: `
|
|
752
|
-
+ `${error?.message ?? String(error)}`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
795
|
}
|
|
756
|
-
if (ownerChannel)
|
|
796
|
+
if (ownerChannel) {
|
|
757
797
|
control.setOwnerChannel(ownerChannel);
|
|
798
|
+
}
|
|
758
799
|
reloadLoopConfig = async () => {
|
|
759
800
|
const nextRole = findRole(loadConfig(configPath), name);
|
|
760
801
|
const definitions = nextRole.loops ?? [];
|
|
@@ -764,7 +805,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
764
805
|
if (generation === loopGeneration && (loopManager || !definitions.length))
|
|
765
806
|
return { changed: false, loops: definitions.length };
|
|
766
807
|
if (!loopManager && definitions.length) {
|
|
767
|
-
loopManager =
|
|
808
|
+
loopManager = deps.createLoopManager(name, definitions, dir, arbiter, {
|
|
768
809
|
now: deps.now,
|
|
769
810
|
setTimer: (callback, ms) => setTimeout(callback, ms),
|
|
770
811
|
clearTimer: timer => clearTimeout(timer),
|
|
@@ -780,10 +821,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
780
821
|
deps.log(`[${name}] scheduled loops reloaded (${definitions.length} definitions)`);
|
|
781
822
|
return { changed: true, loops: definitions.length };
|
|
782
823
|
};
|
|
783
|
-
|
|
824
|
+
// Temporary agents are immutable launch snapshots: never re-resolve mutable Fleet YAML.
|
|
825
|
+
if (!temp)
|
|
826
|
+
control.setConfigReloader(reloadLoopConfig);
|
|
784
827
|
if (role.loops?.length) {
|
|
785
828
|
try {
|
|
786
|
-
loopManager =
|
|
829
|
+
loopManager = deps.createLoopManager(name, role.loops, dir, arbiter, {
|
|
787
830
|
now: deps.now,
|
|
788
831
|
setTimer: (callback, ms) => setTimeout(callback, ms),
|
|
789
832
|
clearTimer: timer => clearTimeout(timer),
|
|
@@ -793,6 +836,17 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
793
836
|
loopManager.start();
|
|
794
837
|
}
|
|
795
838
|
catch (error) {
|
|
839
|
+
if (temp) {
|
|
840
|
+
monitor?.stop();
|
|
841
|
+
if (monitorLoop)
|
|
842
|
+
await monitorLoop;
|
|
843
|
+
await control.close();
|
|
844
|
+
ownerBinder?.release();
|
|
845
|
+
await agentSession.close();
|
|
846
|
+
unsubscribeRecovery?.();
|
|
847
|
+
throw new Error(`[${name}] configured temporary loop manager failed to start: `
|
|
848
|
+
+ `${error?.message ?? String(error)}`);
|
|
849
|
+
}
|
|
796
850
|
deps.log(`[${name}] scheduled loop manager unavailable: ${error?.name ?? 'Error'}`);
|
|
797
851
|
}
|
|
798
852
|
}
|
|
@@ -801,16 +855,49 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
801
855
|
// The monitor loop lives exactly as long as the session: it starts once the
|
|
802
856
|
// pane pid is known and is stopped when that pid dies (task dies with runner).
|
|
803
857
|
monitorLoop ??= monitor?.run(pid);
|
|
858
|
+
recoveryController = new RoleRecoveryController({
|
|
859
|
+
role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
|
|
860
|
+
log: deps.log,
|
|
861
|
+
recoverAgent: async () => {
|
|
862
|
+
const evidence = await recoverAgentIdentity(arbiter, role.identity);
|
|
863
|
+
return evidence.ok ? { ok: true } : { ok: false, reason: evidence.reason };
|
|
864
|
+
},
|
|
865
|
+
recoverOwner: async (epoch) => {
|
|
866
|
+
if (!ownerChannel?.recover)
|
|
867
|
+
return { ok: true };
|
|
868
|
+
try {
|
|
869
|
+
await ownerChannel.recover(epoch);
|
|
870
|
+
return { ok: true };
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
return { ok: false, reason: error instanceof Error
|
|
874
|
+
? `OWNER_${error.name.toUpperCase()}` : 'OWNER_UNKNOWN_ERROR' };
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
});
|
|
804
878
|
const start = deps.now();
|
|
805
879
|
let nextLoopReloadAt = deps.now() + 30_000;
|
|
806
880
|
let nextIdentityPollAt = deps.now();
|
|
881
|
+
let nextDaemonProbeAt = deps.now();
|
|
807
882
|
let lastReloadError = '';
|
|
808
883
|
let identityObserved = false;
|
|
809
884
|
let identityAbsentSince;
|
|
810
885
|
let retirementReason;
|
|
886
|
+
let supervisorRecycleRequired = false;
|
|
811
887
|
while (sessionHandle.isAlive()) {
|
|
812
888
|
await deps.sleep(temp ? 500 : 2000);
|
|
813
889
|
const now = deps.now();
|
|
890
|
+
if (now >= nextDaemonProbeAt) {
|
|
891
|
+
nextDaemonProbeAt = now + 2_000;
|
|
892
|
+
const observation = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
|
|
893
|
+
if (observation.kind === 'lost')
|
|
894
|
+
recoveryController.noteLoss(observation.reason);
|
|
895
|
+
if (observation.kind === 'changed' || observation.kind === 'available'
|
|
896
|
+
|| (observation.kind === 'baseline' && sessionStartedWithoutDaemonBaseline)) {
|
|
897
|
+
sessionStartedWithoutDaemonBaseline = false;
|
|
898
|
+
void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
814
901
|
if (temp && deps.shouldStop?.()) {
|
|
815
902
|
retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
|
|
816
903
|
deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
|
|
@@ -861,6 +948,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
861
948
|
control?.setLoopManager(undefined);
|
|
862
949
|
await loopManager.stop();
|
|
863
950
|
}
|
|
951
|
+
// Let already-settled path operations publish their aggregate result before
|
|
952
|
+
// the shutdown fence invalidates the epoch. This never waits on I/O.
|
|
953
|
+
await Promise.resolve();
|
|
954
|
+
await Promise.resolve();
|
|
955
|
+
recoveryController?.cancel();
|
|
864
956
|
control?.setConfigReloader(undefined);
|
|
865
957
|
// Close the authenticated control route before releasing the binder lease;
|
|
866
958
|
// otherwise the predecessor can unlink the replacement's new socket.
|
|
@@ -871,7 +963,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
871
963
|
}
|
|
872
964
|
if (ownerChannel)
|
|
873
965
|
await ownerChannel.close();
|
|
874
|
-
|
|
966
|
+
if (ownerChannel?.binderReleaseSafe?.() === false) {
|
|
967
|
+
deps.log(`[${name}] owner binder retained because client quiescence was not proven at shutdown`);
|
|
968
|
+
supervisorRecycleRequired = true;
|
|
969
|
+
}
|
|
970
|
+
else
|
|
971
|
+
ownerBinder?.release();
|
|
875
972
|
if (monitor) {
|
|
876
973
|
monitor.stop();
|
|
877
974
|
await monitorLoop;
|
|
@@ -895,7 +992,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
895
992
|
rotated = true;
|
|
896
993
|
deps.log(`[${name}] ${why} -> rotated session-id; next start is FRESH`);
|
|
897
994
|
};
|
|
898
|
-
if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
|
|
995
|
+
if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
|
|
996
|
+
|| exitRecord.detail.includes(CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED))
|
|
899
997
|
// This is a deliberate adapter reclamation, not evidence that resume state
|
|
900
998
|
// is poisoned. Preserve the context even when the resumed generation hits
|
|
901
999
|
// the same bound immediately; runSupervised still counts the fast exit and
|
|
@@ -920,6 +1018,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
920
1018
|
}
|
|
921
1019
|
else
|
|
922
1020
|
deps.log(`[${name}] ${exitRecord.detail} (${elapsed.toFixed(0)}s) -> next start RESUMES context`);
|
|
1021
|
+
if (supervisorRecycleRequired)
|
|
1022
|
+
throw new SupervisorRecycleRequiredError();
|
|
923
1023
|
return {
|
|
924
1024
|
elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
|
|
925
1025
|
...(retirementReason ? { retirementReason } : {}),
|
|
@@ -996,6 +1096,16 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
996
1096
|
result = await attempt(name, { configPath: opts.configPath, allowResumeRotation: !ledger.resumeDiscarded }, deps);
|
|
997
1097
|
}
|
|
998
1098
|
catch (e) {
|
|
1099
|
+
if (e instanceof SupervisorRecycleRequiredError) {
|
|
1100
|
+
writeRestartLedger(dir, {
|
|
1101
|
+
...ledger,
|
|
1102
|
+
lastReason: SUPERVISOR_RECYCLE_REQUIRED,
|
|
1103
|
+
nextDelayMs: 0,
|
|
1104
|
+
updatedAt: stamp(),
|
|
1105
|
+
});
|
|
1106
|
+
deps.log(`[${name}] supervisor recycle required: ${SUPERVISOR_RECYCLE_REQUIRED}`);
|
|
1107
|
+
throw e;
|
|
1108
|
+
}
|
|
999
1109
|
// A session that could not even start is an immediate failure like any
|
|
1000
1110
|
// other; it must count, or an unstartable role loops forever.
|
|
1001
1111
|
result = {
|
|
@@ -1089,7 +1199,7 @@ function fastFailSecsFor(name, configPath) {
|
|
|
1089
1199
|
}
|
|
1090
1200
|
}
|
|
1091
1201
|
/** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
|
|
1092
|
-
export async function runTemp(name, deps = {}) {
|
|
1202
|
+
export async function runTemp(name, deps = {}, attempt = runOnce) {
|
|
1093
1203
|
const dir = agentDir(name, true);
|
|
1094
1204
|
await markTempSupervisorActive(dir);
|
|
1095
1205
|
let signal;
|
|
@@ -1100,7 +1210,7 @@ export async function runTemp(name, deps = {}) {
|
|
|
1100
1210
|
let result;
|
|
1101
1211
|
let failure;
|
|
1102
1212
|
try {
|
|
1103
|
-
result = await
|
|
1213
|
+
result = await attempt(name, { temp: true }, {
|
|
1104
1214
|
...deps,
|
|
1105
1215
|
shouldStop: () => Boolean(signal) || (deps.shouldStop?.() ?? false),
|
|
1106
1216
|
});
|
|
@@ -1114,6 +1224,7 @@ export async function runTemp(name, deps = {}) {
|
|
|
1114
1224
|
const requested = requestedTempStopReason(dir);
|
|
1115
1225
|
const reason = requested
|
|
1116
1226
|
?? result?.retirementReason
|
|
1227
|
+
?? (failure instanceof SupervisorRecycleRequiredError ? 'supervisor-recycle' : undefined)
|
|
1117
1228
|
?? (signal ? 'supervisor-signal' : failure ? 'startup-failure' : 'session-ended');
|
|
1118
1229
|
// A service-manager stop can make the child connection close before the
|
|
1119
1230
|
// runner reaches its normal loop. That is still a successful requested
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export declare function promptContentBlocks(text: string, origin?: PromptOrigin)
|
|
|
28
28
|
export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
|
|
29
29
|
export interface AcpSessionOptions {
|
|
30
30
|
name: string;
|
|
31
|
+
/** Harness identity used only for honest optional capability reporting. */
|
|
32
|
+
harness?: string;
|
|
31
33
|
argv: string[];
|
|
32
34
|
cwd: string;
|
|
33
35
|
env: Record<string, string>;
|
|
@@ -36,6 +38,11 @@ export interface AcpSessionOptions {
|
|
|
36
38
|
permissions: CommonPermissions;
|
|
37
39
|
/** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
|
|
38
40
|
modeId?: string;
|
|
41
|
+
/** Ordered explicit Brain choices that must be applied before readiness. */
|
|
42
|
+
configSelections?: Array<{
|
|
43
|
+
configId: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}>;
|
|
39
46
|
/** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
|
|
40
47
|
permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
|
|
41
48
|
/** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
|
|
@@ -84,6 +91,7 @@ export declare function classifyStopReason(stopReason: string | undefined): Turn
|
|
|
84
91
|
export declare class AcpSession implements AgentSession {
|
|
85
92
|
private readonly options;
|
|
86
93
|
readonly backend: "acp";
|
|
94
|
+
readonly capabilities: import("./types.js").AgentSessionCapabilities;
|
|
87
95
|
readonly pid: number;
|
|
88
96
|
private readonly child;
|
|
89
97
|
private readonly events;
|
|
@@ -110,7 +118,7 @@ export declare class AcpSession implements AgentSession {
|
|
|
110
118
|
private queueDepth;
|
|
111
119
|
private exit;
|
|
112
120
|
private steeringSupported;
|
|
113
|
-
private
|
|
121
|
+
private agentCapabilities?;
|
|
114
122
|
private runtimeModel?;
|
|
115
123
|
private reasoningEffort?;
|
|
116
124
|
private controllerCount;
|
package/dist/session/acp.js
CHANGED
|
@@ -7,7 +7,7 @@ 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 { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, turnResult, } from './types.js';
|
|
10
|
+
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, sessionBackendCapabilities, turnResult, } from './types.js';
|
|
11
11
|
const CANCEL_SETTLE_GRACE_MS = 15_000;
|
|
12
12
|
const CANCEL_TERMINATE_GRACE_MS = 5_000;
|
|
13
13
|
/** A permission no human answered is eventually a decision nobody made. */
|
|
@@ -206,6 +206,7 @@ export function classifyStopReason(stopReason) {
|
|
|
206
206
|
export class AcpSession {
|
|
207
207
|
options;
|
|
208
208
|
backend = 'acp';
|
|
209
|
+
capabilities;
|
|
209
210
|
pid;
|
|
210
211
|
child;
|
|
211
212
|
events;
|
|
@@ -232,7 +233,7 @@ export class AcpSession {
|
|
|
232
233
|
queueDepth = 0;
|
|
233
234
|
exit = null;
|
|
234
235
|
steeringSupported = false;
|
|
235
|
-
|
|
236
|
+
agentCapabilities;
|
|
236
237
|
runtimeModel;
|
|
237
238
|
reasoningEffort;
|
|
238
239
|
controllerCount = 0;
|
|
@@ -265,6 +266,7 @@ export class AcpSession {
|
|
|
265
266
|
this.child = child;
|
|
266
267
|
this.connection = connection;
|
|
267
268
|
this.pid = child.pid ?? -1;
|
|
269
|
+
this.capabilities = sessionBackendCapabilities('acp', options.harness);
|
|
268
270
|
this.events = new SessionEvents(join(options.stateDir, '.session-events.jsonl'));
|
|
269
271
|
this.conversation = new ConversationEventStore(join(options.stateDir, '.conversation'), {
|
|
270
272
|
roleId: options.name, log: line => options.log(`[${options.name}] ${line}`),
|
|
@@ -994,7 +996,7 @@ export class AcpSession {
|
|
|
994
996
|
for (const [permissionId, pending] of [...this.pendingPermissions])
|
|
995
997
|
this.settlePendingAutomatically(permissionId, pending, 'cancelled', undefined, 'the session closed while this request was pending');
|
|
996
998
|
this.releaseAllTools();
|
|
997
|
-
if (this.sessionId && this.
|
|
999
|
+
if (this.sessionId && this.agentCapabilities?.sessionCapabilities?.close != null) {
|
|
998
1000
|
await this.connection.agent.request(acp.methods.agent.session.close, { sessionId: this.sessionId }).catch(() => undefined);
|
|
999
1001
|
}
|
|
1000
1002
|
this.connection.close();
|
|
@@ -1020,23 +1022,25 @@ export class AcpSession {
|
|
|
1020
1022
|
});
|
|
1021
1023
|
if (initialized.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
1022
1024
|
throw new Error(`ACP protocol mismatch: agent selected ${initialized.protocolVersion}, client supports ${acp.PROTOCOL_VERSION}`);
|
|
1023
|
-
this.
|
|
1025
|
+
this.agentCapabilities = initialized.agentCapabilities;
|
|
1024
1026
|
const steering = initialized._meta?.steering;
|
|
1025
1027
|
this.steeringSupported = steering !== null && typeof steering === 'object'
|
|
1026
1028
|
&& steering.supported === true;
|
|
1027
1029
|
const persisted = this.options.mode === 'resume' && existsSync(this.sessionFile)
|
|
1028
1030
|
? readFileSync(this.sessionFile, 'utf8').trim()
|
|
1029
1031
|
: '';
|
|
1030
|
-
|
|
1032
|
+
let advertisedConfigOptions;
|
|
1033
|
+
if (persisted && this.agentCapabilities?.sessionCapabilities?.resume != null) {
|
|
1031
1034
|
const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
|
|
1032
1035
|
sessionId: persisted,
|
|
1033
1036
|
cwd: this.options.cwd,
|
|
1034
1037
|
mcpServers: this.declaredMcpServers(),
|
|
1035
1038
|
});
|
|
1036
|
-
|
|
1039
|
+
advertisedConfigOptions = resumed.configOptions;
|
|
1040
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1037
1041
|
this.sessionId = persisted;
|
|
1038
1042
|
}
|
|
1039
|
-
else if (persisted && this.
|
|
1043
|
+
else if (persisted && this.agentCapabilities?.loadSession) {
|
|
1040
1044
|
// `session/load` replays prior history as ordinary updates before the
|
|
1041
1045
|
// response; those records carry `agent_replay` provenance, never `agent`.
|
|
1042
1046
|
this.replaying = true;
|
|
@@ -1046,7 +1050,8 @@ export class AcpSession {
|
|
|
1046
1050
|
cwd: this.options.cwd,
|
|
1047
1051
|
mcpServers: this.declaredMcpServers(),
|
|
1048
1052
|
});
|
|
1049
|
-
|
|
1053
|
+
advertisedConfigOptions = loaded.configOptions;
|
|
1054
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1050
1055
|
}
|
|
1051
1056
|
finally {
|
|
1052
1057
|
this.replaying = false;
|
|
@@ -1060,8 +1065,29 @@ export class AcpSession {
|
|
|
1060
1065
|
...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
|
|
1061
1066
|
});
|
|
1062
1067
|
this.sessionId = created.sessionId;
|
|
1063
|
-
|
|
1068
|
+
advertisedConfigOptions = created.configOptions;
|
|
1069
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1070
|
+
}
|
|
1071
|
+
for (const selection of this.options.configSelections ?? []) {
|
|
1072
|
+
if (!advertisedConfigOptions?.some(option => option.id === selection.configId))
|
|
1073
|
+
throw new Error(`ACP agent did not advertise required session config option '${selection.configId}'`);
|
|
1074
|
+
let configured;
|
|
1075
|
+
try {
|
|
1076
|
+
configured = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId: selection.configId, value: selection.value });
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
throw new Error(`ACP agent refused required session config option '${selection.configId}' value '${selection.value}': `
|
|
1080
|
+
+ (error instanceof Error ? error.message : String(error)));
|
|
1081
|
+
}
|
|
1082
|
+
advertisedConfigOptions = configured.configOptions;
|
|
1083
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1084
|
+
const applied = advertisedConfigOptions.find(option => option.id === selection.configId);
|
|
1085
|
+
if (applied?.currentValue !== selection.value)
|
|
1086
|
+
throw new Error(`ACP agent did not apply required session config option '${selection.configId}' value '${selection.value}'`);
|
|
1064
1087
|
}
|
|
1088
|
+
// Do not persist a session until every required Brain choice is live. A
|
|
1089
|
+
// failed startup closes the ACP session; persisting its id first would make
|
|
1090
|
+
// a later resume repeatedly target that invalid session.
|
|
1065
1091
|
writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
|
|
1066
1092
|
// Deliver the configured permission mode whichever way the session came up
|
|
1067
1093
|
// (new, resume or load) — the launch flag never reaches an ACP agent. A
|
|
@@ -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
|