@ours.network/fleet 1.0.5 → 1.1.0-nightly.10
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 +122 -133
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +14 -25
- package/dist/application/role-creation-service.d.ts +5 -24
- package/dist/application/role-creation-service.js +27 -67
- package/dist/application/role-removal-service.js +6 -3
- package/dist/application/role-repository.d.ts +1 -2
- package/dist/application/role-repository.js +7 -7
- package/dist/application/session-control.d.ts +0 -12
- package/dist/application/session-control.js +0 -42
- package/dist/application/session-mutations.d.ts +5 -5
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/application/task-room-service.js +154 -16
- package/dist/application/types.d.ts +1 -8
- package/dist/briefing.js +12 -11
- package/dist/build-info.json +5 -5
- package/dist/cli.js +143 -63
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +66 -6
- package/dist/config.js +398 -43
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -2
- 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 +103 -127
- package/dist/doctor.js +18 -11
- package/dist/fleet-command-audit.d.ts +160 -0
- package/dist/fleet-command-audit.js +433 -0
- package/dist/fleet-proxy.d.ts +3 -1
- package/dist/fleet-proxy.js +30 -8
- package/dist/generated-agent-source.d.ts +9 -0
- package/dist/generated-agent-source.js +53 -0
- package/dist/harness/acp-session-transport.d.ts +8 -0
- package/dist/harness/acp-session-transport.js +3 -0
- package/dist/harness/agent-session.d.ts +51 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +24 -0
- package/dist/harness/claude-code-session.js +36 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +41 -81
- package/dist/harness/codex-session.d.ts +24 -0
- package/dist/harness/codex-session.js +36 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +55 -45
- package/dist/harness/registry.js +4 -0
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/init-guidance.d.ts +1 -0
- package/dist/init-guidance.js +6 -0
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +4 -6
- package/dist/model-env.js +5 -8
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/ops.js +4 -2
- package/dist/owner-channel/channel.d.ts +58 -2
- package/dist/owner-channel/channel.js +269 -29
- package/dist/owner-channel/commands.d.ts +2 -0
- package/dist/owner-channel/commands.js +31 -13
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -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.d.ts +3 -2
- package/dist/resolved-plan.js +21 -5
- package/dist/rooms-tasks/cli.js +225 -40
- package/dist/rooms-tasks/config.js +24 -13
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/provision.js +98 -36
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +10 -16
- package/dist/rooms-tasks/types.js +1 -4
- package/dist/runner.d.ts +13 -16
- package/dist/runner.js +110 -122
- package/dist/sensitive-config.d.ts +2 -0
- package/dist/sensitive-config.js +4 -0
- package/dist/session/acp.d.ts +7 -2
- package/dist/session/acp.js +29 -6
- package/dist/session/activity.d.ts +2 -3
- package/dist/session/activity.js +2 -3
- package/dist/session/arbiter.d.ts +4 -4
- package/dist/session/control.d.ts +17 -4
- package/dist/session/control.js +35 -0
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.d.ts +8 -33
- package/dist/spawn.js +53 -213
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +30 -18
- package/dist/watchdog/run.js +12 -36
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/fleet-config-service.d.ts +11 -8
- package/dist/web/fleet-config-service.js +384 -183
- package/dist/web/runtime.js +4 -15
- package/dist/web/server.d.ts +1 -3
- package/dist/web/server.js +3 -14
- package/dist/web/topology-promote.js +26 -19
- package/dist/web/topology.js +2 -2
- package/dist/web/yaml-document-edit.js +2 -0
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
- package/dist/web-app/index.html +2 -2
- package/examples/fleet/agents/Alice.yaml +11 -0
- package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
- package/examples/fleet/brains/claude-default.yaml +5 -0
- package/examples/fleet/roles/coordinator.yaml +5 -0
- package/examples/fleet/roles/developer.yaml +3 -0
- package/examples/fleet.yaml +74 -0
- package/package.json +5 -8
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +5 -0
- package/dist/session/tmux.d.ts +0 -28
- package/dist/session/tmux.js +0 -80
- package/dist/web/terminal/bridge.d.ts +0 -27
- package/dist/web/terminal/bridge.js +0 -317
- package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
- package/dist/web-app/assets/index-BCBK78hw.js +0 -10
- package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
package/dist/runner.js
CHANGED
|
@@ -5,16 +5,15 @@ import { parse } from 'yaml';
|
|
|
5
5
|
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
|
-
import { Tmux } from './tmux.js';
|
|
9
8
|
import { createMonitor, probeIdentityPresence, } from './monitor.js';
|
|
10
|
-
import {
|
|
9
|
+
import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
|
|
10
|
+
import { recoverAgentIdentity } from './agent-recovery-gate.js';
|
|
11
|
+
import { realExec } from './exec.js';
|
|
11
12
|
import { resolveIsolation } from './isolation/policy.js';
|
|
12
13
|
import { selectIsolationBackend } from './isolation/registry.js';
|
|
13
14
|
import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
|
|
14
15
|
import { resolveLaunchRuntime } from './isolation/runtime.js';
|
|
15
|
-
import { AcpSession } from './session/acp.js';
|
|
16
16
|
import { controlRequest, RoleControlServer } from './session/control.js';
|
|
17
|
-
import { TmuxSession } from './session/tmux.js';
|
|
18
17
|
import { ACP_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus } from './session/types.js';
|
|
19
18
|
import { effectiveModelForRole, modelRecoveryHeld, reconcileModelRecovery, recordModelFailure, classifyFailureText, } from './model-recovery.js';
|
|
20
19
|
import { rotateWorklog } from './worklog.js';
|
|
@@ -27,8 +26,15 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
|
|
|
27
26
|
import { effectivePermissionMode } from './permissions.js';
|
|
28
27
|
import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
|
|
29
28
|
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
|
|
29
|
+
export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
|
|
30
|
+
export class SupervisorRecycleRequiredError extends Error {
|
|
31
|
+
code = SUPERVISOR_RECYCLE_REQUIRED;
|
|
32
|
+
constructor() {
|
|
33
|
+
super('owner channel requires a fresh supervisor process after unproven client quiescence');
|
|
34
|
+
this.name = 'SupervisorRecycleRequiredError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
30
37
|
const defaultDeps = () => ({
|
|
31
|
-
tmux: new Tmux(),
|
|
32
38
|
exec: realExec,
|
|
33
39
|
cpuDelegated: () => cpuControllerDelegated(),
|
|
34
40
|
isAlive: pid => { try {
|
|
@@ -42,9 +48,10 @@ const defaultDeps = () => ({
|
|
|
42
48
|
now: () => Date.now(),
|
|
43
49
|
log: line => process.stderr.write(line + '\n'),
|
|
44
50
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
51
|
+
probeGeneration: env => probeDaemonGeneration((url, init) => globalThis.fetch(url, init), env),
|
|
45
52
|
createMonitor: opts => createMonitor(opts),
|
|
46
53
|
createOwnerChannel: opts => new OwnerChannel(opts),
|
|
47
|
-
|
|
54
|
+
startAgentSession: (adapter, options) => adapter.start(options),
|
|
48
55
|
createControlServer: (stateDir, session, log) => new RoleControlServer(stateDir, session, log),
|
|
49
56
|
acquireOwnerBinder: (stateDir, role, identity) => acquireOwnerBinderLease(stateDir, role, identity),
|
|
50
57
|
reportOwnerStartupFailure: async (stateDir) => {
|
|
@@ -133,38 +140,10 @@ export function recordMonitorOwner(dir, owner) {
|
|
|
133
140
|
catch { /* ownership diagnostics must never take the role down */ }
|
|
134
141
|
return owner === 'fleet' && previous === 'native';
|
|
135
142
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Compose the tmux pane shell command: env prefix + argv + exit-status capture.
|
|
138
|
-
* `paneArgv` defaults to `launch.argv`; when isolation is active the caller passes
|
|
139
|
-
* the sandbox-wrapped argv (e.g. `bwrap … -- claude …`). The `env` prefix and the
|
|
140
|
-
* `echo $? > exitfile` capture stay host-side, outside the sandbox, so the runner
|
|
141
|
-
* still sees the real exit code.
|
|
142
|
-
*/
|
|
143
|
-
export function buildPaneCommand(launch, roleEnv, exitStatusPath, paneArgv = launch.argv) {
|
|
144
|
-
const env = {
|
|
145
|
-
PATH: process.env.PATH ?? '', COLORTERM: 'truecolor', ...launch.env, ...(roleEnv ?? {}),
|
|
146
|
-
};
|
|
147
|
-
delete env[OBSOLETE_OURS_AUTOSTART_ENV];
|
|
148
|
-
// Interactive panes should advertise colour even when the supervisor itself
|
|
149
|
-
// was launched with NO_COLOR. A role may still deliberately opt back in to
|
|
150
|
-
// NO_COLOR (or replace COLORTERM) through its explicit env block.
|
|
151
|
-
const unsetNoColor = Object.prototype.hasOwnProperty.call(roleEnv ?? {}, 'NO_COLOR')
|
|
152
|
-
? '' : '-u NO_COLOR ';
|
|
153
|
-
const envPfx = 'env -u OURS_AUTOSTART ' + unsetNoColor
|
|
154
|
-
+ Object.entries(env).map(([k, v]) => `${k}=${shq(v)}`).join(' ');
|
|
155
|
-
const cmd = paneArgv.map(shq).join(' ');
|
|
156
|
-
// Write a structured record, not a bare number: the wait status alone cannot
|
|
157
|
-
// say whether the file is missing because the program never exited or because
|
|
158
|
-
// nothing ever wrote it. `printf` is POSIX; no shell branching is needed
|
|
159
|
-
// because classification happens in one place, in TypeScript.
|
|
160
|
-
const record = `'{"version":1,"backend":"tmux","status":'"$__ofs"'}'`;
|
|
161
|
-
return `${envPfx} ${cmd}; __ofs=$?; printf %s ${record} > ${shq(exitStatusPath)}`;
|
|
162
|
-
}
|
|
163
143
|
/** Adapt runner deps and the role's daemon-profile overrides for the monitor. */
|
|
164
144
|
function monitorDeps(deps, roleEnv) {
|
|
165
145
|
return {
|
|
166
146
|
fetch: deps.fetch,
|
|
167
|
-
tmux: deps.tmux,
|
|
168
147
|
isAlive: deps.isAlive,
|
|
169
148
|
sleep: deps.sleep,
|
|
170
149
|
now: deps.now,
|
|
@@ -484,16 +463,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
484
463
|
writeFileSync(bootedFile, `${new Date(deps.now()).toISOString()} ${mode}\n`);
|
|
485
464
|
const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
|
|
486
465
|
const prep = await adapter.prepareSession(role, { stateDir: dir, runCwd });
|
|
487
|
-
const sessionBackend = role.session ?? '
|
|
488
|
-
let launch =
|
|
489
|
-
|
|
490
|
-
if (!adapter.buildAcpLaunch)
|
|
491
|
-
throw new Error(`harness '${role.harness}' does not support the ACP session backend`);
|
|
492
|
-
return adapter.buildAcpLaunch(role, prep);
|
|
493
|
-
})()
|
|
494
|
-
: adapter.buildLaunch(role, mode, { sessionId }, prep);
|
|
495
|
-
// Isolation is additive: only roles that declare `isolation:` are wrapped. The
|
|
496
|
-
// env prefix + exit capture in buildPaneCommand stay host-side.
|
|
466
|
+
const sessionBackend = role.session ?? 'acp';
|
|
467
|
+
let launch = adapter.agentSession.prepareLaunch(role, prep);
|
|
468
|
+
// Isolation is additive: only roles that declare `isolation:` are wrapped.
|
|
497
469
|
let wrappedArgv = launch.argv;
|
|
498
470
|
if (role.isolation) {
|
|
499
471
|
// Start with the same durable context that config validation and doctor judged,
|
|
@@ -530,7 +502,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
530
502
|
// user unit concurrently on boot; `ours-fleet up`/restart-all bulk-start) does not
|
|
531
503
|
// hit the harness/API rate limit at once. Time-based via a shared launch gate, so
|
|
532
504
|
// a lone start or a solo crash-restart waits zero. Applied right before the harness
|
|
533
|
-
//
|
|
505
|
+
// agent-session start; the cheap monitor prime still runs immediately after.
|
|
534
506
|
if (staggerMs > 0) {
|
|
535
507
|
const slot = await reserveLaunchSlot(stateRoot(), staggerMs, deps);
|
|
536
508
|
const wait = slot - deps.now();
|
|
@@ -571,11 +543,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
571
543
|
name, identity: role.identity, agentDir: dir, cfg: role.monitor,
|
|
572
544
|
deps: resolvedMonitorDeps,
|
|
573
545
|
}) : null;
|
|
546
|
+
const daemonObserver = new DaemonGenerationObserver();
|
|
547
|
+
const initialDaemon = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
|
|
548
|
+
let sessionStartedWithoutDaemonBaseline = initialDaemon.kind !== 'baseline';
|
|
574
549
|
if (monitor)
|
|
575
550
|
await monitor.prime({ resetCursor: resetMonitorCursor });
|
|
576
551
|
rmSync(exitFile, { force: true });
|
|
577
552
|
let pid;
|
|
578
|
-
let
|
|
553
|
+
let agentSession;
|
|
579
554
|
let control;
|
|
580
555
|
let unsubscribeRecovery;
|
|
581
556
|
let monitorLoop;
|
|
@@ -583,14 +558,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
583
558
|
let sessionClosed = false;
|
|
584
559
|
let ownerChannel;
|
|
585
560
|
let ownerBinder;
|
|
586
|
-
const pendingFleetSpawnNotices = [];
|
|
587
561
|
let loopManager;
|
|
588
562
|
let arbiter;
|
|
563
|
+
let recoveryController;
|
|
589
564
|
let reloadLoopConfig;
|
|
590
565
|
let loopGeneration = JSON.stringify((role.loops ?? []).map(loop => [
|
|
591
566
|
loop.name, loop.definitionHash, loop.promptHash,
|
|
592
567
|
]));
|
|
593
|
-
|
|
568
|
+
{
|
|
594
569
|
const perms = role.permissions ?? resolvePermissions(undefined, undefined);
|
|
595
570
|
// Say once, at startup, that this role will decide permission requests by
|
|
596
571
|
// itself. Without it the only trace of an auto-denied tool call is a turn
|
|
@@ -598,34 +573,16 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
598
573
|
if (perms.unattended === 'deny')
|
|
599
574
|
deps.log(`[${name}] permission policy: unattended=deny — with no console attached, ` +
|
|
600
575
|
`permission requests are automatically denied once each (reject_once) and the turn continues`);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
argv: wrappedArgv,
|
|
604
|
-
cwd: runCwd,
|
|
605
|
-
|
|
606
|
-
stateDir: dir,
|
|
607
|
-
mode,
|
|
608
|
-
permissions: perms,
|
|
609
|
-
modeId: adapter.acpPermissionModeId?.(role),
|
|
610
|
-
// The role's declared MCP servers, and the bundled agent's `_meta`
|
|
611
|
-
// vocabulary for the options it takes no flag for. Both come from the
|
|
612
|
-
// ADAPTER and from `prep`: the ACP launch cannot carry `prep.argv`, so this
|
|
613
|
-
// is the route by which harness_options that used to be silently dropped
|
|
614
|
-
// for an ACP role actually reach the session.
|
|
615
|
-
mcpServers: adapter.acpMcpServers?.(role),
|
|
616
|
-
sessionMeta: adapter.acpSessionMeta?.(role, prep),
|
|
617
|
-
permissionMode: effectivePermissionMode(role),
|
|
618
|
-
// Provenance travels with the exact ACP launch. Keeping it out of a
|
|
619
|
-
// role-only adapter hook prevents a PATH fallback or resolver skew from
|
|
620
|
-
// claiming metadata trust for an argv it did not authenticate.
|
|
621
|
-
permissionMetadataSource: launch.permissionMetadataSource,
|
|
622
|
-
scrubObsoleteOursAutostart: true,
|
|
623
|
-
log: deps.log,
|
|
576
|
+
agentSession = await deps.startAgentSession(adapter.agentSession, {
|
|
577
|
+
role, prep,
|
|
578
|
+
launch: { ...launch, argv: wrappedArgv, env: harnessChildEnv(role, launch.env, dir) },
|
|
579
|
+
cwd: runCwd, stateDir: dir, mode, permissions: perms,
|
|
580
|
+
permissionMode: effectivePermissionMode(role), log: deps.log,
|
|
624
581
|
});
|
|
625
|
-
pid =
|
|
626
|
-
arbiter = new RoleTurnArbiter(
|
|
582
|
+
pid = agentSession.pid;
|
|
583
|
+
arbiter = new RoleTurnArbiter(agentSession);
|
|
627
584
|
sessionHandle = arbiter;
|
|
628
|
-
unsubscribeRecovery =
|
|
585
|
+
unsubscribeRecovery = agentSession.subscribe(event => {
|
|
629
586
|
if (event.kind !== 'error' || !event.text)
|
|
630
587
|
return;
|
|
631
588
|
const evidence = classifyFailureText(event.text, 'acp', new Date(deps.now()).toISOString());
|
|
@@ -647,7 +604,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
647
604
|
+ `${notifyError?.message ?? String(notifyError)}`);
|
|
648
605
|
}
|
|
649
606
|
}
|
|
650
|
-
await
|
|
607
|
+
await agentSession.close();
|
|
651
608
|
unsubscribeRecovery?.();
|
|
652
609
|
throw new Error(`[${name}] owner channel failed to start: `
|
|
653
610
|
+ `${error?.message ?? String(error)}`);
|
|
@@ -659,25 +616,23 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
659
616
|
}
|
|
660
617
|
catch (error) {
|
|
661
618
|
ownerBinder?.release();
|
|
662
|
-
await
|
|
619
|
+
await agentSession.close();
|
|
663
620
|
unsubscribeRecovery?.();
|
|
664
621
|
throw error;
|
|
665
622
|
}
|
|
666
623
|
control.setFleetSpawner(async (requested) => {
|
|
667
624
|
const event = await executeManagedSpawn(role, configPath, requested, deps.log);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (ownerChannel?.notifyFleetSpawn) {
|
|
625
|
+
// Room-member launches are collected with their Task/Room transaction so
|
|
626
|
+
// causal delivery is Task → Room → Agents → Room active → Task active.
|
|
627
|
+
if (!requested.roomMemberStartup && role.owner_channel && ownerChannel?.notifyFleetSpawn) {
|
|
671
628
|
try {
|
|
672
629
|
await ownerChannel.notifyFleetSpawn(event);
|
|
673
630
|
}
|
|
674
631
|
catch (error) {
|
|
675
|
-
deps.log(`[${name}]
|
|
632
|
+
deps.log(`[${name}] Agent lifecycle notice delivery failed: `
|
|
676
633
|
+ `${error?.message ?? String(error)}`);
|
|
677
634
|
}
|
|
678
635
|
}
|
|
679
|
-
else
|
|
680
|
-
pendingFleetSpawnNotices.push(event);
|
|
681
636
|
return event;
|
|
682
637
|
});
|
|
683
638
|
resolvedMonitorDeps.delivery = {
|
|
@@ -740,7 +695,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
740
695
|
const started = await starting;
|
|
741
696
|
// A temporary role's first turn can be the active turn when an ours wake
|
|
742
697
|
// needs immediate attention. A typed console/monitor cancellation ends
|
|
743
|
-
// only that turn: the already-live
|
|
698
|
+
// only that turn: the already-live agent session and any queued wake remain
|
|
744
699
|
// valid. Keep every unproven cancellation, refusal, shutdown, and genuine
|
|
745
700
|
// failure terminal so a role that never accepted its briefing is not
|
|
746
701
|
// silently reported as healthy.
|
|
@@ -749,7 +704,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
749
704
|
monitor?.stop();
|
|
750
705
|
await control.close();
|
|
751
706
|
ownerBinder?.release();
|
|
752
|
-
await
|
|
707
|
+
await agentSession.close();
|
|
753
708
|
unsubscribeRecovery?.();
|
|
754
709
|
if (modelRecovery) {
|
|
755
710
|
if (monitorLoop)
|
|
@@ -795,23 +750,19 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
795
750
|
await ownerChannel.close().catch(() => undefined);
|
|
796
751
|
await control.close();
|
|
797
752
|
ownerBinder?.release();
|
|
798
|
-
await
|
|
753
|
+
await agentSession.close();
|
|
799
754
|
unsubscribeRecovery?.();
|
|
800
755
|
throw new Error(`[${name}] owner channel failed to start: `
|
|
801
756
|
+ `${error?.message ?? String(error)}`);
|
|
802
757
|
}
|
|
803
|
-
for (const event of pendingFleetSpawnNotices.splice(0)) {
|
|
804
|
-
try {
|
|
805
|
-
await ownerChannel.notifyFleetSpawn?.(event);
|
|
806
|
-
}
|
|
807
|
-
catch (error) {
|
|
808
|
-
deps.log(`[${name}] deferred spawned-agent owner notice failed: `
|
|
809
|
-
+ `${error?.message ?? String(error)}`);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
758
|
}
|
|
813
|
-
if (ownerChannel)
|
|
759
|
+
if (ownerChannel) {
|
|
814
760
|
control.setOwnerChannel(ownerChannel);
|
|
761
|
+
control.setFleetAuditor({
|
|
762
|
+
begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
|
|
763
|
+
finish: input => ownerChannel.finishFleetCommandAudit(input),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
815
766
|
reloadLoopConfig = async () => {
|
|
816
767
|
const nextRole = findRole(loadConfig(configPath), name);
|
|
817
768
|
const definitions = nextRole.loops ?? [];
|
|
@@ -854,34 +805,53 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
854
805
|
}
|
|
855
806
|
}
|
|
856
807
|
}
|
|
857
|
-
else {
|
|
858
|
-
await deps.tmux.kill(name);
|
|
859
|
-
await deps.tmux.newSession(name, runCwd, buildPaneCommand(launch, role.env, exitFile, wrappedArgv));
|
|
860
|
-
let panePid = null;
|
|
861
|
-
for (let i = 0; i < 40 && panePid === null; i++) {
|
|
862
|
-
panePid = await deps.tmux.panePid(name);
|
|
863
|
-
if (panePid === null)
|
|
864
|
-
await deps.sleep(250);
|
|
865
|
-
}
|
|
866
|
-
if (panePid === null)
|
|
867
|
-
throw new Error(`[${name}] could not resolve tmux pane pid`);
|
|
868
|
-
pid = panePid;
|
|
869
|
-
sessionHandle = new TmuxSession(name, pid, deps.tmux, deps.isAlive);
|
|
870
|
-
}
|
|
871
808
|
deps.log(`[${name}] up; pid=${pid} cwd=${runCwd} harness=${role.harness} session=${sessionBackend} mode=${mode}`);
|
|
872
809
|
// The monitor loop lives exactly as long as the session: it starts once the
|
|
873
810
|
// pane pid is known and is stopped when that pid dies (task dies with runner).
|
|
874
811
|
monitorLoop ??= monitor?.run(pid);
|
|
812
|
+
recoveryController = new RoleRecoveryController({
|
|
813
|
+
role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
|
|
814
|
+
log: deps.log,
|
|
815
|
+
recoverAgent: async () => {
|
|
816
|
+
const evidence = await recoverAgentIdentity(arbiter, role.identity);
|
|
817
|
+
return evidence.ok ? { ok: true } : { ok: false, reason: evidence.reason };
|
|
818
|
+
},
|
|
819
|
+
recoverOwner: async (epoch) => {
|
|
820
|
+
if (!ownerChannel?.recover)
|
|
821
|
+
return { ok: true };
|
|
822
|
+
try {
|
|
823
|
+
await ownerChannel.recover(epoch);
|
|
824
|
+
return { ok: true };
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
return { ok: false, reason: error instanceof Error
|
|
828
|
+
? `OWNER_${error.name.toUpperCase()}` : 'OWNER_UNKNOWN_ERROR' };
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
});
|
|
875
832
|
const start = deps.now();
|
|
876
833
|
let nextLoopReloadAt = deps.now() + 30_000;
|
|
877
834
|
let nextIdentityPollAt = deps.now();
|
|
835
|
+
let nextDaemonProbeAt = deps.now();
|
|
878
836
|
let lastReloadError = '';
|
|
879
837
|
let identityObserved = false;
|
|
880
838
|
let identityAbsentSince;
|
|
881
839
|
let retirementReason;
|
|
840
|
+
let supervisorRecycleRequired = false;
|
|
882
841
|
while (sessionHandle.isAlive()) {
|
|
883
842
|
await deps.sleep(temp ? 500 : 2000);
|
|
884
843
|
const now = deps.now();
|
|
844
|
+
if (now >= nextDaemonProbeAt) {
|
|
845
|
+
nextDaemonProbeAt = now + 2_000;
|
|
846
|
+
const observation = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
|
|
847
|
+
if (observation.kind === 'lost')
|
|
848
|
+
recoveryController.noteLoss(observation.reason);
|
|
849
|
+
if (observation.kind === 'changed' || observation.kind === 'available'
|
|
850
|
+
|| (observation.kind === 'baseline' && sessionStartedWithoutDaemonBaseline)) {
|
|
851
|
+
sessionStartedWithoutDaemonBaseline = false;
|
|
852
|
+
void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
885
855
|
if (temp && deps.shouldStop?.()) {
|
|
886
856
|
retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
|
|
887
857
|
deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
|
|
@@ -899,7 +869,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
899
869
|
else if (presence.state === 'absent' && identityObserved) {
|
|
900
870
|
identityAbsentSince ??= now;
|
|
901
871
|
// Require a continuous, time-bounded run of authoritative absence.
|
|
902
|
-
// The first positive observation is the readiness gate: cold
|
|
872
|
+
// The first positive observation is the readiness gate: cold harness
|
|
903
873
|
// starts may spend minutes loading the harness and briefing before the
|
|
904
874
|
// agent creates/binds its identity, and absence before then is not a
|
|
905
875
|
// close event. After readiness, debounce a real disappearance.
|
|
@@ -932,6 +902,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
932
902
|
control?.setLoopManager(undefined);
|
|
933
903
|
await loopManager.stop();
|
|
934
904
|
}
|
|
905
|
+
// Let already-settled path operations publish their aggregate result before
|
|
906
|
+
// the shutdown fence invalidates the epoch. This never waits on I/O.
|
|
907
|
+
await Promise.resolve();
|
|
908
|
+
await Promise.resolve();
|
|
909
|
+
recoveryController?.cancel();
|
|
935
910
|
control?.setConfigReloader(undefined);
|
|
936
911
|
// Close the authenticated control route before releasing the binder lease;
|
|
937
912
|
// otherwise the predecessor can unlink the replacement's new socket.
|
|
@@ -942,25 +917,25 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
942
917
|
}
|
|
943
918
|
if (ownerChannel)
|
|
944
919
|
await ownerChannel.close();
|
|
945
|
-
|
|
920
|
+
if (ownerChannel?.binderReleaseSafe?.() === false) {
|
|
921
|
+
deps.log(`[${name}] owner binder retained because client quiescence was not proven at shutdown`);
|
|
922
|
+
supervisorRecycleRequired = true;
|
|
923
|
+
}
|
|
924
|
+
else
|
|
925
|
+
ownerBinder?.release();
|
|
946
926
|
if (monitor) {
|
|
947
927
|
monitor.stop();
|
|
948
928
|
await monitorLoop;
|
|
949
929
|
}
|
|
950
930
|
unsubscribeRecovery?.();
|
|
951
|
-
if (
|
|
952
|
-
await
|
|
931
|
+
if (agentSession && !sessionClosed)
|
|
932
|
+
await agentSession.close();
|
|
953
933
|
const elapsed = (deps.now() - start) / 1000;
|
|
954
934
|
// Establish what actually happened before deciding anything. Absence of a
|
|
955
935
|
// record is `unknown` — except when the console itself is gone, which is a
|
|
956
936
|
// different event with a different consequence.
|
|
957
|
-
const exitRecord =
|
|
958
|
-
|
|
959
|
-
?? { version: 1, class: 'unknown', detail: 'the ACP agent stopped without reporting an exit' }
|
|
960
|
-
: readExitRecord(exitFile)
|
|
961
|
-
?? (await deps.tmux.has(name)
|
|
962
|
-
? { version: 1, class: 'unknown', detail: 'the pane process ended without writing an exit record' }
|
|
963
|
-
: { version: 1, class: 'session-destroyed', detail: `the tmux session '${name}' no longer exists` });
|
|
937
|
+
const exitRecord = agentSession.exitResult()
|
|
938
|
+
?? { version: 1, class: 'unknown', detail: 'the agent session stopped without reporting an exit' };
|
|
964
939
|
writeFileSync(exitFile, JSON.stringify({
|
|
965
940
|
...exitRecord, at: new Date(deps.now()).toISOString(), elapsedSecs: Number(elapsed.toFixed(1)),
|
|
966
941
|
}) + '\n');
|
|
@@ -996,6 +971,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
996
971
|
}
|
|
997
972
|
else
|
|
998
973
|
deps.log(`[${name}] ${exitRecord.detail} (${elapsed.toFixed(0)}s) -> next start RESUMES context`);
|
|
974
|
+
if (supervisorRecycleRequired)
|
|
975
|
+
throw new SupervisorRecycleRequiredError();
|
|
999
976
|
return {
|
|
1000
977
|
elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
|
|
1001
978
|
...(retirementReason ? { retirementReason } : {}),
|
|
@@ -1072,6 +1049,16 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
|
|
|
1072
1049
|
result = await attempt(name, { configPath: opts.configPath, allowResumeRotation: !ledger.resumeDiscarded }, deps);
|
|
1073
1050
|
}
|
|
1074
1051
|
catch (e) {
|
|
1052
|
+
if (e instanceof SupervisorRecycleRequiredError) {
|
|
1053
|
+
writeRestartLedger(dir, {
|
|
1054
|
+
...ledger,
|
|
1055
|
+
lastReason: SUPERVISOR_RECYCLE_REQUIRED,
|
|
1056
|
+
nextDelayMs: 0,
|
|
1057
|
+
updatedAt: stamp(),
|
|
1058
|
+
});
|
|
1059
|
+
deps.log(`[${name}] supervisor recycle required: ${SUPERVISOR_RECYCLE_REQUIRED}`);
|
|
1060
|
+
throw e;
|
|
1061
|
+
}
|
|
1075
1062
|
// A session that could not even start is an immediate failure like any
|
|
1076
1063
|
// other; it must count, or an unstartable role loops forever.
|
|
1077
1064
|
result = {
|
|
@@ -1165,7 +1152,7 @@ function fastFailSecsFor(name, configPath) {
|
|
|
1165
1152
|
}
|
|
1166
1153
|
}
|
|
1167
1154
|
/** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
|
|
1168
|
-
export async function runTemp(name, deps = {}) {
|
|
1155
|
+
export async function runTemp(name, deps = {}, attempt = runOnce) {
|
|
1169
1156
|
const dir = agentDir(name, true);
|
|
1170
1157
|
await markTempSupervisorActive(dir);
|
|
1171
1158
|
let signal;
|
|
@@ -1176,7 +1163,7 @@ export async function runTemp(name, deps = {}) {
|
|
|
1176
1163
|
let result;
|
|
1177
1164
|
let failure;
|
|
1178
1165
|
try {
|
|
1179
|
-
result = await
|
|
1166
|
+
result = await attempt(name, { temp: true }, {
|
|
1180
1167
|
...deps,
|
|
1181
1168
|
shouldStop: () => Boolean(signal) || (deps.shouldStop?.() ?? false),
|
|
1182
1169
|
});
|
|
@@ -1190,6 +1177,7 @@ export async function runTemp(name, deps = {}) {
|
|
|
1190
1177
|
const requested = requestedTempStopReason(dir);
|
|
1191
1178
|
const reason = requested
|
|
1192
1179
|
?? result?.retirementReason
|
|
1180
|
+
?? (failure instanceof SupervisorRecycleRequiredError ? 'supervisor-recycle' : undefined)
|
|
1193
1181
|
?? (signal ? 'supervisor-signal' : failure ? 'startup-failure' : 'session-ended');
|
|
1194
1182
|
// A service-manager stop can make the child connection close before the
|
|
1195
1183
|
// runner reaches its normal loop. That is still a successful requested
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CommonPermissions } from '../config.js';
|
|
|
3
3
|
import type { AcpMcpServer } from '../harness/types.js';
|
|
4
4
|
import { ConversationEventStore } from './conversation-store.js';
|
|
5
5
|
import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
6
|
-
import type { ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent,
|
|
6
|
+
import type { ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent, AgentSession, RuntimeSelectorMetadata, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
|
|
7
7
|
/** Bound safe-boundary waiting without turning a hung tool into cancellation. */
|
|
8
8
|
export declare const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120000;
|
|
9
9
|
/**
|
|
@@ -36,6 +36,11 @@ export interface AcpSessionOptions {
|
|
|
36
36
|
permissions: CommonPermissions;
|
|
37
37
|
/** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
|
|
38
38
|
modeId?: string;
|
|
39
|
+
/** Ordered explicit Brain choices that must be applied before readiness. */
|
|
40
|
+
configSelections?: Array<{
|
|
41
|
+
configId: string;
|
|
42
|
+
value: string;
|
|
43
|
+
}>;
|
|
39
44
|
/** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
|
|
40
45
|
permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
|
|
41
46
|
/** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
|
|
@@ -81,7 +86,7 @@ export declare function classifyStopReason(stopReason: string | undefined): Turn
|
|
|
81
86
|
* Persistent ACP v1 client. It is the sole owner of the agent's stdio; all
|
|
82
87
|
* human/automation attachment happens through the fleet role-control protocol.
|
|
83
88
|
*/
|
|
84
|
-
export declare class AcpSession implements
|
|
89
|
+
export declare class AcpSession implements AgentSession {
|
|
85
90
|
private readonly options;
|
|
86
91
|
readonly backend: "acp";
|
|
87
92
|
readonly pid: number;
|
package/dist/session/acp.js
CHANGED
|
@@ -281,8 +281,7 @@ export class AcpSession {
|
|
|
281
281
|
clearTimeout(this.cancelForceKill);
|
|
282
282
|
this.cancelForceKill = undefined;
|
|
283
283
|
this.releaseSteeringOccupancy('adapter exited');
|
|
284
|
-
// Record the child's real exit code/signal
|
|
285
|
-
// shell's `$?`; here the truth is available, so keep it.
|
|
284
|
+
// Record the child's real exit code/signal while the truth is available.
|
|
286
285
|
const classified = classifyChildExit(code, signal);
|
|
287
286
|
this.exit = this.cancelRecoveryReason
|
|
288
287
|
? { ...classified, detail: `${this.cancelRecoveryReason}; ${classified.detail}` }
|
|
@@ -1028,13 +1027,15 @@ export class AcpSession {
|
|
|
1028
1027
|
const persisted = this.options.mode === 'resume' && existsSync(this.sessionFile)
|
|
1029
1028
|
? readFileSync(this.sessionFile, 'utf8').trim()
|
|
1030
1029
|
: '';
|
|
1030
|
+
let advertisedConfigOptions;
|
|
1031
1031
|
if (persisted && this.capabilities?.sessionCapabilities?.resume != null) {
|
|
1032
1032
|
const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
|
|
1033
1033
|
sessionId: persisted,
|
|
1034
1034
|
cwd: this.options.cwd,
|
|
1035
1035
|
mcpServers: this.declaredMcpServers(),
|
|
1036
1036
|
});
|
|
1037
|
-
|
|
1037
|
+
advertisedConfigOptions = resumed.configOptions;
|
|
1038
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1038
1039
|
this.sessionId = persisted;
|
|
1039
1040
|
}
|
|
1040
1041
|
else if (persisted && this.capabilities?.loadSession) {
|
|
@@ -1047,7 +1048,8 @@ export class AcpSession {
|
|
|
1047
1048
|
cwd: this.options.cwd,
|
|
1048
1049
|
mcpServers: this.declaredMcpServers(),
|
|
1049
1050
|
});
|
|
1050
|
-
|
|
1051
|
+
advertisedConfigOptions = loaded.configOptions;
|
|
1052
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1051
1053
|
}
|
|
1052
1054
|
finally {
|
|
1053
1055
|
this.replaying = false;
|
|
@@ -1061,8 +1063,29 @@ export class AcpSession {
|
|
|
1061
1063
|
...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
|
|
1062
1064
|
});
|
|
1063
1065
|
this.sessionId = created.sessionId;
|
|
1064
|
-
|
|
1066
|
+
advertisedConfigOptions = created.configOptions;
|
|
1067
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1065
1068
|
}
|
|
1069
|
+
for (const selection of this.options.configSelections ?? []) {
|
|
1070
|
+
if (!advertisedConfigOptions?.some(option => option.id === selection.configId))
|
|
1071
|
+
throw new Error(`ACP agent did not advertise required session config option '${selection.configId}'`);
|
|
1072
|
+
let configured;
|
|
1073
|
+
try {
|
|
1074
|
+
configured = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId: selection.configId, value: selection.value });
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
throw new Error(`ACP agent refused required session config option '${selection.configId}' value '${selection.value}': `
|
|
1078
|
+
+ (error instanceof Error ? error.message : String(error)));
|
|
1079
|
+
}
|
|
1080
|
+
advertisedConfigOptions = configured.configOptions;
|
|
1081
|
+
this.captureRuntimeMetadata(advertisedConfigOptions);
|
|
1082
|
+
const applied = advertisedConfigOptions.find(option => option.id === selection.configId);
|
|
1083
|
+
if (applied?.currentValue !== selection.value)
|
|
1084
|
+
throw new Error(`ACP agent did not apply required session config option '${selection.configId}' value '${selection.value}'`);
|
|
1085
|
+
}
|
|
1086
|
+
// Do not persist a session until every required Brain choice is live. A
|
|
1087
|
+
// failed startup closes the ACP session; persisting its id first would make
|
|
1088
|
+
// a later resume repeatedly target that invalid session.
|
|
1066
1089
|
writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
|
|
1067
1090
|
// Deliver the configured permission mode whichever way the session came up
|
|
1068
1091
|
// (new, resume or load) — the launch flag never reaches an ACP agent. A
|
|
@@ -1476,7 +1499,7 @@ export class AcpSession {
|
|
|
1476
1499
|
...(normalized.adapterMeta ? { adapterMeta: normalized.adapterMeta } : {}),
|
|
1477
1500
|
});
|
|
1478
1501
|
}
|
|
1479
|
-
// ── conversation ledger access (
|
|
1502
|
+
// ── conversation ledger access (AgentSession) ─────────────────────────────
|
|
1480
1503
|
conversationPage(request = {}) {
|
|
1481
1504
|
const floor = Number(this.conversationStartCursor ?? 0);
|
|
1482
1505
|
const requested = Number(request.after ?? 0);
|
|
@@ -18,9 +18,8 @@ export interface ObservedActivity {
|
|
|
18
18
|
lastUpdateAt?: string;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Classify agent-side activity. `unobservable` is NOT `quiet`:
|
|
22
|
-
*
|
|
23
|
-
* reported as "doing nothing".
|
|
21
|
+
* Classify agent-side activity. `unobservable` is NOT `quiet`: absent evidence
|
|
22
|
+
* must never be reported as "doing nothing".
|
|
24
23
|
*/
|
|
25
24
|
export declare function classifyActivity(activity: SessionActivity | undefined, now?: number): ObservedActivity;
|
|
26
25
|
/**
|
package/dist/session/activity.js
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export const ACTIVITY_WINDOW_MS = 60_000;
|
|
13
13
|
/**
|
|
14
|
-
* Classify agent-side activity. `unobservable` is NOT `quiet`:
|
|
15
|
-
*
|
|
16
|
-
* reported as "doing nothing".
|
|
14
|
+
* Classify agent-side activity. `unobservable` is NOT `quiet`: absent evidence
|
|
15
|
+
* must never be reported as "doing nothing".
|
|
17
16
|
*/
|
|
18
17
|
export function classifyActivity(activity, now = Date.now()) {
|
|
19
18
|
if (!activity)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ConversationHandlePage, ExitRecord, InterruptOutcome, PromptOrigin, QueuedPrompt, SessionEvent,
|
|
1
|
+
import type { ConversationHandlePage, ExitRecord, InterruptOutcome, PromptOrigin, QueuedPrompt, SessionEvent, AgentSession, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnResult } from './types.js';
|
|
2
2
|
import type { ConversationEventV1, ConversationSnapshot, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
3
3
|
export type ScheduledAttempt = {
|
|
4
4
|
state: 'started';
|
|
@@ -15,9 +15,9 @@ export type ScheduledAttempt = {
|
|
|
15
15
|
* producers retain ACP queue semantics while making their unsettled claim
|
|
16
16
|
* visible before another producer can inspect idle state.
|
|
17
17
|
*/
|
|
18
|
-
export declare class RoleTurnArbiter implements
|
|
18
|
+
export declare class RoleTurnArbiter implements AgentSession {
|
|
19
19
|
private readonly session;
|
|
20
|
-
readonly backend:
|
|
20
|
+
readonly backend: "acp";
|
|
21
21
|
readonly pid: number;
|
|
22
22
|
private tail;
|
|
23
23
|
private unsettled;
|
|
@@ -26,7 +26,7 @@ export declare class RoleTurnArbiter implements SessionHandle {
|
|
|
26
26
|
private generation;
|
|
27
27
|
/** Resolves the instant the CURRENT generation is retired. */
|
|
28
28
|
private retirement;
|
|
29
|
-
constructor(session:
|
|
29
|
+
constructor(session: AgentSession);
|
|
30
30
|
/**
|
|
31
31
|
* Waiters race the tail against their own generation's retirement, so a
|
|
32
32
|
* single operation that never settles cannot own the boundary forever. A
|