@ours.network/fleet 1.0.4 → 1.1.0-nightly.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 +41 -50
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/errors.d.ts +1 -1
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +1 -24
- package/dist/application/role-creation-service.d.ts +5 -5
- package/dist/application/role-creation-service.js +5 -7
- 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 +37 -2
- package/dist/application/task-room-service.js +49 -7
- package/dist/application/types.d.ts +0 -7
- package/dist/atomic-file.d.ts +5 -3
- package/dist/atomic-file.js +32 -8
- package/dist/briefing.js +1 -4
- package/dist/build-info.json +5 -5
- package/dist/cli.js +9 -23
- package/dist/config.d.ts +7 -7
- package/dist/config.js +13 -34
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +46 -30
- package/dist/doctor.js +0 -7
- package/dist/fleet-proxy.d.ts +1 -1
- 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 +33 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +18 -0
- package/dist/harness/claude-code-session.js +28 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +29 -81
- package/dist/harness/codex-session.d.ts +18 -0
- package/dist/harness/codex-session.js +28 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +35 -45
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -3
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +2 -3
- package/dist/model-env.js +2 -3
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/owner-channel/attachments.d.ts +2 -4
- package/dist/owner-channel/attachments.js +6 -39
- package/dist/owner-channel/channel.d.ts +2 -2
- package/dist/owner-channel/channel.js +16 -3
- package/dist/owner-channel/commands.d.ts +18 -1
- package/dist/owner-channel/commands.js +73 -4
- package/dist/rooms-tasks/cli.js +121 -2
- package/dist/rooms-tasks/task-lists.d.ts +19 -0
- package/dist/rooms-tasks/task-lists.js +96 -0
- package/dist/rooms-tasks/task-state.d.ts +3 -0
- package/dist/rooms-tasks/task-state.js +281 -175
- package/dist/rooms-tasks/types.d.ts +11 -1
- package/dist/runner.d.ts +5 -15
- package/dist/runner.js +26 -102
- package/dist/session/acp.d.ts +2 -2
- package/dist/session/acp.js +2 -3
- 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 +3 -3
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.js +8 -9
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.js +6 -4
- package/dist/watchdog/run.js +8 -23
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/runtime.js +6 -15
- package/dist/web/server.d.ts +3 -3
- package/dist/web/server.js +89 -15
- package/dist/web/topology-promote.js +2 -5
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/{index-BCBK78hw.js → index-DhMDVRU1.js} +6 -6
- package/dist/web-app/index.html +2 -2
- package/package.json +3 -8
- 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-DuC-xnX4.css +0 -1
package/dist/runner.js
CHANGED
|
@@ -5,16 +5,13 @@ 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 { realExec
|
|
9
|
+
import { realExec } from './exec.js';
|
|
11
10
|
import { resolveIsolation } from './isolation/policy.js';
|
|
12
11
|
import { selectIsolationBackend } from './isolation/registry.js';
|
|
13
12
|
import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
|
|
14
13
|
import { resolveLaunchRuntime } from './isolation/runtime.js';
|
|
15
|
-
import { AcpSession } from './session/acp.js';
|
|
16
14
|
import { controlRequest, RoleControlServer } from './session/control.js';
|
|
17
|
-
import { TmuxSession } from './session/tmux.js';
|
|
18
15
|
import { ACP_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus } from './session/types.js';
|
|
19
16
|
import { effectiveModelForRole, modelRecoveryHeld, reconcileModelRecovery, recordModelFailure, classifyFailureText, } from './model-recovery.js';
|
|
20
17
|
import { rotateWorklog } from './worklog.js';
|
|
@@ -28,7 +25,6 @@ import { effectivePermissionMode } from './permissions.js';
|
|
|
28
25
|
import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
|
|
29
26
|
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
|
|
30
27
|
const defaultDeps = () => ({
|
|
31
|
-
tmux: new Tmux(),
|
|
32
28
|
exec: realExec,
|
|
33
29
|
cpuDelegated: () => cpuControllerDelegated(),
|
|
34
30
|
isAlive: pid => { try {
|
|
@@ -44,7 +40,7 @@ const defaultDeps = () => ({
|
|
|
44
40
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
45
41
|
createMonitor: opts => createMonitor(opts),
|
|
46
42
|
createOwnerChannel: opts => new OwnerChannel(opts),
|
|
47
|
-
|
|
43
|
+
startAgentSession: (adapter, options) => adapter.start(options),
|
|
48
44
|
createControlServer: (stateDir, session, log) => new RoleControlServer(stateDir, session, log),
|
|
49
45
|
acquireOwnerBinder: (stateDir, role, identity) => acquireOwnerBinderLease(stateDir, role, identity),
|
|
50
46
|
reportOwnerStartupFailure: async (stateDir) => {
|
|
@@ -133,38 +129,10 @@ export function recordMonitorOwner(dir, owner) {
|
|
|
133
129
|
catch { /* ownership diagnostics must never take the role down */ }
|
|
134
130
|
return owner === 'fleet' && previous === 'native';
|
|
135
131
|
}
|
|
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
132
|
/** Adapt runner deps and the role's daemon-profile overrides for the monitor. */
|
|
164
133
|
function monitorDeps(deps, roleEnv) {
|
|
165
134
|
return {
|
|
166
135
|
fetch: deps.fetch,
|
|
167
|
-
tmux: deps.tmux,
|
|
168
136
|
isAlive: deps.isAlive,
|
|
169
137
|
sleep: deps.sleep,
|
|
170
138
|
now: deps.now,
|
|
@@ -484,16 +452,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
484
452
|
writeFileSync(bootedFile, `${new Date(deps.now()).toISOString()} ${mode}\n`);
|
|
485
453
|
const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
|
|
486
454
|
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.
|
|
455
|
+
const sessionBackend = role.session ?? 'acp';
|
|
456
|
+
let launch = adapter.agentSession.prepareLaunch(role, prep);
|
|
457
|
+
// Isolation is additive: only roles that declare `isolation:` are wrapped.
|
|
497
458
|
let wrappedArgv = launch.argv;
|
|
498
459
|
if (role.isolation) {
|
|
499
460
|
// Start with the same durable context that config validation and doctor judged,
|
|
@@ -530,7 +491,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
530
491
|
// user unit concurrently on boot; `ours-fleet up`/restart-all bulk-start) does not
|
|
531
492
|
// hit the harness/API rate limit at once. Time-based via a shared launch gate, so
|
|
532
493
|
// a lone start or a solo crash-restart waits zero. Applied right before the harness
|
|
533
|
-
//
|
|
494
|
+
// agent-session start; the cheap monitor prime still runs immediately after.
|
|
534
495
|
if (staggerMs > 0) {
|
|
535
496
|
const slot = await reserveLaunchSlot(stateRoot(), staggerMs, deps);
|
|
536
497
|
const wait = slot - deps.now();
|
|
@@ -575,7 +536,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
575
536
|
await monitor.prime({ resetCursor: resetMonitorCursor });
|
|
576
537
|
rmSync(exitFile, { force: true });
|
|
577
538
|
let pid;
|
|
578
|
-
let
|
|
539
|
+
let agentSession;
|
|
579
540
|
let control;
|
|
580
541
|
let unsubscribeRecovery;
|
|
581
542
|
let monitorLoop;
|
|
@@ -590,7 +551,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
590
551
|
let loopGeneration = JSON.stringify((role.loops ?? []).map(loop => [
|
|
591
552
|
loop.name, loop.definitionHash, loop.promptHash,
|
|
592
553
|
]));
|
|
593
|
-
|
|
554
|
+
{
|
|
594
555
|
const perms = role.permissions ?? resolvePermissions(undefined, undefined);
|
|
595
556
|
// Say once, at startup, that this role will decide permission requests by
|
|
596
557
|
// itself. Without it the only trace of an auto-denied tool call is a turn
|
|
@@ -598,34 +559,16 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
598
559
|
if (perms.unattended === 'deny')
|
|
599
560
|
deps.log(`[${name}] permission policy: unattended=deny — with no console attached, ` +
|
|
600
561
|
`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,
|
|
562
|
+
agentSession = await deps.startAgentSession(adapter.agentSession, {
|
|
563
|
+
role, prep,
|
|
564
|
+
launch: { ...launch, argv: wrappedArgv, env: harnessChildEnv(role, launch.env, dir) },
|
|
565
|
+
cwd: runCwd, stateDir: dir, mode, permissions: perms,
|
|
566
|
+
permissionMode: effectivePermissionMode(role), log: deps.log,
|
|
624
567
|
});
|
|
625
|
-
pid =
|
|
626
|
-
arbiter = new RoleTurnArbiter(
|
|
568
|
+
pid = agentSession.pid;
|
|
569
|
+
arbiter = new RoleTurnArbiter(agentSession);
|
|
627
570
|
sessionHandle = arbiter;
|
|
628
|
-
unsubscribeRecovery =
|
|
571
|
+
unsubscribeRecovery = agentSession.subscribe(event => {
|
|
629
572
|
if (event.kind !== 'error' || !event.text)
|
|
630
573
|
return;
|
|
631
574
|
const evidence = classifyFailureText(event.text, 'acp', new Date(deps.now()).toISOString());
|
|
@@ -647,7 +590,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
647
590
|
+ `${notifyError?.message ?? String(notifyError)}`);
|
|
648
591
|
}
|
|
649
592
|
}
|
|
650
|
-
await
|
|
593
|
+
await agentSession.close();
|
|
651
594
|
unsubscribeRecovery?.();
|
|
652
595
|
throw new Error(`[${name}] owner channel failed to start: `
|
|
653
596
|
+ `${error?.message ?? String(error)}`);
|
|
@@ -659,7 +602,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
659
602
|
}
|
|
660
603
|
catch (error) {
|
|
661
604
|
ownerBinder?.release();
|
|
662
|
-
await
|
|
605
|
+
await agentSession.close();
|
|
663
606
|
unsubscribeRecovery?.();
|
|
664
607
|
throw error;
|
|
665
608
|
}
|
|
@@ -740,7 +683,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
740
683
|
const started = await starting;
|
|
741
684
|
// A temporary role's first turn can be the active turn when an ours wake
|
|
742
685
|
// needs immediate attention. A typed console/monitor cancellation ends
|
|
743
|
-
// only that turn: the already-live
|
|
686
|
+
// only that turn: the already-live agent session and any queued wake remain
|
|
744
687
|
// valid. Keep every unproven cancellation, refusal, shutdown, and genuine
|
|
745
688
|
// failure terminal so a role that never accepted its briefing is not
|
|
746
689
|
// silently reported as healthy.
|
|
@@ -749,7 +692,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
749
692
|
monitor?.stop();
|
|
750
693
|
await control.close();
|
|
751
694
|
ownerBinder?.release();
|
|
752
|
-
await
|
|
695
|
+
await agentSession.close();
|
|
753
696
|
unsubscribeRecovery?.();
|
|
754
697
|
if (modelRecovery) {
|
|
755
698
|
if (monitorLoop)
|
|
@@ -795,7 +738,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
795
738
|
await ownerChannel.close().catch(() => undefined);
|
|
796
739
|
await control.close();
|
|
797
740
|
ownerBinder?.release();
|
|
798
|
-
await
|
|
741
|
+
await agentSession.close();
|
|
799
742
|
unsubscribeRecovery?.();
|
|
800
743
|
throw new Error(`[${name}] owner channel failed to start: `
|
|
801
744
|
+ `${error?.message ?? String(error)}`);
|
|
@@ -854,20 +797,6 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
854
797
|
}
|
|
855
798
|
}
|
|
856
799
|
}
|
|
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
800
|
deps.log(`[${name}] up; pid=${pid} cwd=${runCwd} harness=${role.harness} session=${sessionBackend} mode=${mode}`);
|
|
872
801
|
// The monitor loop lives exactly as long as the session: it starts once the
|
|
873
802
|
// pane pid is known and is stopped when that pid dies (task dies with runner).
|
|
@@ -899,7 +828,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
899
828
|
else if (presence.state === 'absent' && identityObserved) {
|
|
900
829
|
identityAbsentSince ??= now;
|
|
901
830
|
// Require a continuous, time-bounded run of authoritative absence.
|
|
902
|
-
// The first positive observation is the readiness gate: cold
|
|
831
|
+
// The first positive observation is the readiness gate: cold harness
|
|
903
832
|
// starts may spend minutes loading the harness and briefing before the
|
|
904
833
|
// agent creates/binds its identity, and absence before then is not a
|
|
905
834
|
// close event. After readiness, debounce a real disappearance.
|
|
@@ -948,19 +877,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
948
877
|
await monitorLoop;
|
|
949
878
|
}
|
|
950
879
|
unsubscribeRecovery?.();
|
|
951
|
-
if (
|
|
952
|
-
await
|
|
880
|
+
if (agentSession && !sessionClosed)
|
|
881
|
+
await agentSession.close();
|
|
953
882
|
const elapsed = (deps.now() - start) / 1000;
|
|
954
883
|
// Establish what actually happened before deciding anything. Absence of a
|
|
955
884
|
// record is `unknown` — except when the console itself is gone, which is a
|
|
956
885
|
// 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` });
|
|
886
|
+
const exitRecord = agentSession.exitResult()
|
|
887
|
+
?? { version: 1, class: 'unknown', detail: 'the agent session stopped without reporting an exit' };
|
|
964
888
|
writeFileSync(exitFile, JSON.stringify({
|
|
965
889
|
...exitRecord, at: new Date(deps.now()).toISOString(), elapsedSecs: Number(elapsed.toFixed(1)),
|
|
966
890
|
}) + '\n');
|
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
|
/**
|
|
@@ -81,7 +81,7 @@ export declare function classifyStopReason(stopReason: string | undefined): Turn
|
|
|
81
81
|
* Persistent ACP v1 client. It is the sole owner of the agent's stdio; all
|
|
82
82
|
* human/automation attachment happens through the fleet role-control protocol.
|
|
83
83
|
*/
|
|
84
|
-
export declare class AcpSession implements
|
|
84
|
+
export declare class AcpSession implements AgentSession {
|
|
85
85
|
private readonly options;
|
|
86
86
|
readonly backend: "acp";
|
|
87
87
|
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}` }
|
|
@@ -1476,7 +1475,7 @@ export class AcpSession {
|
|
|
1476
1475
|
...(normalized.adapterMeta ? { adapterMeta: normalized.adapterMeta } : {}),
|
|
1477
1476
|
});
|
|
1478
1477
|
}
|
|
1479
|
-
// ── conversation ledger access (
|
|
1478
|
+
// ── conversation ledger access (AgentSession) ─────────────────────────────
|
|
1480
1479
|
conversationPage(request = {}) {
|
|
1481
1480
|
const floor = Number(this.conversationStartCursor ?? 0);
|
|
1482
1481
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Socket } from 'node:net';
|
|
2
|
-
import type { ControlFailureKind, SessionEvent,
|
|
2
|
+
import type { AgentSession, ControlFailureKind, SessionEvent, SessionSnapshot } from './types.js';
|
|
3
3
|
import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
|
|
4
4
|
import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
|
|
5
5
|
import type { SpawnOpts } from '../spawn.js';
|
|
@@ -48,7 +48,7 @@ export interface RetainedEventPage {
|
|
|
48
48
|
truncated: boolean;
|
|
49
49
|
}
|
|
50
50
|
/** The one retained-range projection shared by polling and live-follow admission. */
|
|
51
|
-
export declare function retainedEventPage(session:
|
|
51
|
+
export declare function retainedEventPage(session: AgentSession, since: number): RetainedEventPage;
|
|
52
52
|
/**
|
|
53
53
|
* One line saying what a control failure does — and does not — prove about the
|
|
54
54
|
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
|
@@ -105,7 +105,7 @@ export declare class RoleControlServer {
|
|
|
105
105
|
private loopManager?;
|
|
106
106
|
private reloadConfig?;
|
|
107
107
|
private fleetSpawner?;
|
|
108
|
-
constructor(stateDir: string, session:
|
|
108
|
+
constructor(stateDir: string, session: AgentSession, log: (line: string) => void);
|
|
109
109
|
start(): Promise<void>;
|
|
110
110
|
close(): Promise<void>;
|
|
111
111
|
/** Attach only the already-started supervisor-owned channel client. */
|
package/dist/session/types.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export interface InterruptOutcome {
|
|
|
96
96
|
reasonCode?: string;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
* What
|
|
99
|
+
* What an `AgentSession.interrupt` implementation may resolve. Before 0.17.1 the
|
|
100
100
|
* contract was `Promise<void>`, and resolving at all meant the cancellation had
|
|
101
101
|
* taken effect cooperatively — so an implementation written against that
|
|
102
102
|
* contract stays valid and keeps its exact meaning. Only in-tree consumers read
|
|
@@ -146,7 +146,7 @@ export interface ExitRecord {
|
|
|
146
146
|
code?: number;
|
|
147
147
|
/** Signal that killed it, when one did. */
|
|
148
148
|
signal?: string;
|
|
149
|
-
/** Raw wait status
|
|
149
|
+
/** Raw wait status retained when migrating a legacy shell-exit record. */
|
|
150
150
|
status?: number;
|
|
151
151
|
at?: string;
|
|
152
152
|
/** One line an operator can read. */
|
|
@@ -194,8 +194,7 @@ export interface SessionSnapshot {
|
|
|
194
194
|
};
|
|
195
195
|
/**
|
|
196
196
|
* Observed agent activity, independent of turn occupancy: the evidence a
|
|
197
|
-
* human-facing surface needs before calling a role idle.
|
|
198
|
-
* that cannot observe the agent at all (tmux), which is itself honest — no
|
|
197
|
+
* human-facing surface needs before calling a role idle. When absent, no
|
|
199
198
|
* evidence is not evidence of inactivity.
|
|
200
199
|
*/
|
|
201
200
|
activity?: SessionActivity;
|
|
@@ -259,7 +258,12 @@ export interface ConversationHandlePage {
|
|
|
259
258
|
hasMore: boolean;
|
|
260
259
|
snapshot: ConversationSnapshot;
|
|
261
260
|
}
|
|
262
|
-
|
|
261
|
+
/**
|
|
262
|
+
* The live, harness-neutral contract between Fleet and one agent session.
|
|
263
|
+
* Harness adapters construct this handle; orchestration code must not depend
|
|
264
|
+
* on the ACP transport which currently implements it.
|
|
265
|
+
*/
|
|
266
|
+
export interface AgentSession {
|
|
263
267
|
readonly backend: SessionBackendId;
|
|
264
268
|
readonly pid: number;
|
|
265
269
|
isAlive(): boolean;
|
|
@@ -301,3 +305,5 @@ export interface SessionHandle {
|
|
|
301
305
|
exitResult(): ExitRecord | null;
|
|
302
306
|
close(): Promise<void>;
|
|
303
307
|
}
|
|
308
|
+
/** @deprecated Public compatibility name; use AgentSession. */
|
|
309
|
+
export type SessionHandle = AgentSession;
|
package/dist/spawn.js
CHANGED
|
@@ -123,8 +123,10 @@ export function readIsolationFile(path) {
|
|
|
123
123
|
export function validateSpawnOpts(o) {
|
|
124
124
|
if (o.mission !== undefined && o.missionFile)
|
|
125
125
|
throw new Error('--mission and --mission-file are mutually exclusive');
|
|
126
|
-
if (o.session
|
|
127
|
-
throw new Error(
|
|
126
|
+
if (o.session === 'tmux')
|
|
127
|
+
throw new Error("invalid --session 'tmux'; tmux is no longer supported; use --session acp");
|
|
128
|
+
if (o.session && o.session !== 'acp')
|
|
129
|
+
throw new Error(`invalid --session '${o.session}'; allowed: acp`);
|
|
128
130
|
if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
|
|
129
131
|
throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
|
|
130
132
|
if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
|
|
@@ -203,7 +205,7 @@ export function spawnDryRun(o) {
|
|
|
203
205
|
...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
|
|
204
206
|
});
|
|
205
207
|
const model = modelEnv.model;
|
|
206
|
-
const session = raw.session ?? cfg.defaults.session ?? '
|
|
208
|
+
const session = raw.session ?? cfg.defaults.session ?? 'acp';
|
|
207
209
|
const resolvedRole = {
|
|
208
210
|
...raw,
|
|
209
211
|
name: o.name,
|
|
@@ -255,7 +257,7 @@ function provenanceSettings(o, defaults) {
|
|
|
255
257
|
const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
|
|
256
258
|
return {
|
|
257
259
|
harness: tagged('harness', provenanceOf(o.harness, defaults.harness, 'claude-code')),
|
|
258
|
-
session: tagged('session', provenanceOf(o.session, defaults.session, '
|
|
260
|
+
session: tagged('session', provenanceOf(o.session, defaults.session, 'acp')),
|
|
259
261
|
identity: o.identity
|
|
260
262
|
? { value: o.identity, source: 'cli' }
|
|
261
263
|
: { value: o.name, source: 'built-in' }, // defaults to the role name
|
|
@@ -417,7 +419,7 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
|
|
|
417
419
|
...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
|
|
418
420
|
});
|
|
419
421
|
const model = modelEnv.model;
|
|
420
|
-
const session = o.session ?? cfg.defaults.session ?? '
|
|
422
|
+
const session = o.session ?? cfg.defaults.session ?? 'acp';
|
|
421
423
|
const role = {
|
|
422
424
|
...fromOpts, // includes `isolation` when --isolation-file was given
|
|
423
425
|
name: o.name,
|
|
@@ -467,10 +469,7 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
|
|
|
467
469
|
if (cfg.startStaggerMs > 0)
|
|
468
470
|
writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
|
|
469
471
|
prepareTempSupervisor(dir, o.name);
|
|
470
|
-
// Run the supervisor independently
|
|
471
|
-
// `_run-temp` -> runOnce() creates AND kills the tmux session <name> for the
|
|
472
|
-
// agent itself; a supervisor sharing that session name would SIGHUP its own
|
|
473
|
-
// process before the agent ever launches. On a service-managed host the temp
|
|
472
|
+
// Run the supervisor independently. On a service-managed host the temp
|
|
474
473
|
// runner gets its own transient unit/job, so stopping the coordinator's unit
|
|
475
474
|
// cannot kill a live worker in the coordinator's cgroup.
|
|
476
475
|
onStage?.('starting_temp');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
2
|
import type { SupervisorBackend } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
* No supervision:
|
|
4
|
+
* No service supervision: tmux contains the direct runner process, nothing survives a reboot and
|
|
5
5
|
* nothing restarts on crash. Used for temp agents and CI tests
|
|
6
6
|
* (OURS_FLEET_SUPERVISOR=none).
|
|
7
7
|
*/
|
package/dist/supervisor/none.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Tmux, tmuxArgs } from '../tmux.js';
|
|
2
2
|
import { realExec, shq } from '../exec.js';
|
|
3
3
|
/**
|
|
4
|
-
* No supervision:
|
|
4
|
+
* No service supervision: tmux contains the direct runner process, nothing survives a reboot and
|
|
5
5
|
* nothing restarts on crash. Used for temp agents and CI tests
|
|
6
6
|
* (OURS_FLEET_SUPERVISOR=none).
|
|
7
7
|
*/
|
|
@@ -9,7 +9,7 @@ export function makeNoneBackend(exec = realExec) {
|
|
|
9
9
|
const tmux = new Tmux(exec);
|
|
10
10
|
return {
|
|
11
11
|
id: 'none',
|
|
12
|
-
async init() { return ['no supervisor:
|
|
12
|
+
async init() { return ['no service supervisor: direct runner containment only (no reboot survival)']; },
|
|
13
13
|
async install(name, binPath) {
|
|
14
14
|
const existed = await tmux.kill(name); // true when a session was there
|
|
15
15
|
await tmux.newSession(name, process.cwd(), `${shq(binPath)} _run ${shq(name)}`);
|
package/dist/tmux.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare const tmuxSocket: (session: string) => string;
|
|
|
20
20
|
* the shared default server and re-creates #32.
|
|
21
21
|
*/
|
|
22
22
|
export declare const tmuxArgs: (session: string, args: string[]) => string[];
|
|
23
|
-
/** Thin tmux wrapper
|
|
23
|
+
/** Thin tmux wrapper retained only for supervisor=none process containment. */
|
|
24
24
|
export declare class Tmux {
|
|
25
25
|
private exec;
|
|
26
26
|
constructor(exec?: Exec);
|
|
@@ -32,17 +32,4 @@ export declare class Tmux {
|
|
|
32
32
|
* program inside it exited on its own".
|
|
33
33
|
*/
|
|
34
34
|
kill(name: string): Promise<boolean>;
|
|
35
|
-
capture(name: string, lines?: number): Promise<string>;
|
|
36
|
-
/** Bounded ANSI/history seed for the browser terminal projection. */
|
|
37
|
-
captureHistory(name: string, lines?: number): Promise<string>;
|
|
38
|
-
panePid(name: string): Promise<number | null>;
|
|
39
|
-
/**
|
|
40
|
-
* List the live sessions among `names`, asking each server in turn.
|
|
41
|
-
* There is no fleet-wide `tmux ls` any more: a session per server means the
|
|
42
|
-
* caller says which sessions to ask about. A server that is not running
|
|
43
|
-
* answers non-zero and contributes nothing.
|
|
44
|
-
*/
|
|
45
|
-
list(names: readonly string[]): Promise<string>;
|
|
46
|
-
sendText(name: string, text: string): Promise<void>;
|
|
47
|
-
sendKey(name: string, key: string): Promise<void>;
|
|
48
35
|
}
|
package/dist/tmux.js
CHANGED
|
@@ -20,7 +20,7 @@ export const tmuxSocket = (session) => `${TMUX_SOCKET_PREFIX}${session}`;
|
|
|
20
20
|
* the shared default server and re-creates #32.
|
|
21
21
|
*/
|
|
22
22
|
export const tmuxArgs = (session, args) => ['-L', tmuxSocket(session), ...args];
|
|
23
|
-
/** Thin tmux wrapper
|
|
23
|
+
/** Thin tmux wrapper retained only for supervisor=none process containment. */
|
|
24
24
|
export class Tmux {
|
|
25
25
|
exec;
|
|
26
26
|
constructor(exec = realExec) {
|
|
@@ -42,54 +42,4 @@ export class Tmux {
|
|
|
42
42
|
async kill(name) {
|
|
43
43
|
return (await this.exec('tmux', tmuxArgs(name, ['kill-session', '-t', name]))).code === 0;
|
|
44
44
|
}
|
|
45
|
-
async capture(name, lines = 40) {
|
|
46
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p']));
|
|
47
|
-
if (r.code !== 0)
|
|
48
|
-
throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
|
|
49
|
-
const all = r.stdout.replace(/\n+$/, '').split('\n');
|
|
50
|
-
return all.slice(-lines).join('\n');
|
|
51
|
-
}
|
|
52
|
-
/** Bounded ANSI/history seed for the browser terminal projection. */
|
|
53
|
-
async captureHistory(name, lines = 5_000) {
|
|
54
|
-
const bounded = Math.min(Math.max(Math.trunc(lines), 1), 20_000);
|
|
55
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p', '-e', '-J', '-S', `-${bounded}`]));
|
|
56
|
-
if (r.code !== 0)
|
|
57
|
-
throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
|
|
58
|
-
return Buffer.from(r.stdout).subarray(0, 4 * 1024 * 1024).toString();
|
|
59
|
-
}
|
|
60
|
-
async panePid(name) {
|
|
61
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['list-panes', '-t', name, '-F', '#{pane_pid}']));
|
|
62
|
-
if (r.code !== 0)
|
|
63
|
-
return null;
|
|
64
|
-
const pid = parseInt(r.stdout.trim().split('\n')[0], 10);
|
|
65
|
-
return Number.isFinite(pid) ? pid : null;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* List the live sessions among `names`, asking each server in turn.
|
|
69
|
-
* There is no fleet-wide `tmux ls` any more: a session per server means the
|
|
70
|
-
* caller says which sessions to ask about. A server that is not running
|
|
71
|
-
* answers non-zero and contributes nothing.
|
|
72
|
-
*/
|
|
73
|
-
async list(names) {
|
|
74
|
-
const lines = [];
|
|
75
|
-
for (const name of names) {
|
|
76
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['ls']));
|
|
77
|
-
if (r.code === 0 && r.stdout.trim())
|
|
78
|
-
lines.push(r.stdout.trimEnd());
|
|
79
|
-
}
|
|
80
|
-
return lines.join('\n');
|
|
81
|
-
}
|
|
82
|
-
async sendText(name, text) {
|
|
83
|
-
let r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, '-l', text]));
|
|
84
|
-
if (r.code !== 0)
|
|
85
|
-
throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
|
|
86
|
-
r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, 'Enter']));
|
|
87
|
-
if (r.code !== 0)
|
|
88
|
-
throw new Error(`tmux send-keys Enter '${name}' failed: ${r.stderr.trim()}`);
|
|
89
|
-
}
|
|
90
|
-
async sendKey(name, key) {
|
|
91
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, key]));
|
|
92
|
-
if (r.code !== 0)
|
|
93
|
-
throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
|
|
94
|
-
}
|
|
95
45
|
}
|
package/dist/watchdog/config.js
CHANGED
|
@@ -31,7 +31,7 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
31
31
|
const roleIdentities = new Set(roles.map(r => r.identity));
|
|
32
32
|
// Tracks identity -> owning watchdog name across entries:
|
|
33
33
|
// two watchdogs declaring the same identity would otherwise share a temp
|
|
34
|
-
// dir,
|
|
34
|
+
// dir, agent session, and run lock with no error until they collide at
|
|
35
35
|
// runtime.
|
|
36
36
|
const watchdogIdentities = new Map();
|
|
37
37
|
const out = [];
|
|
@@ -61,9 +61,11 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
61
61
|
throw new ConfigError(`${where}: identity '${identity}' collides with watchdog '${collidingWatchdog}'`);
|
|
62
62
|
watchdogIdentities.set(identity, name);
|
|
63
63
|
const harness = w.harness ?? defaults.harness ?? 'claude-code';
|
|
64
|
-
const sessionRaw = w.session ?? defaults.session ?? '
|
|
65
|
-
if (sessionRaw
|
|
66
|
-
throw new ConfigError(`${where}: session
|
|
64
|
+
const sessionRaw = w.session ?? defaults.session ?? 'acp';
|
|
65
|
+
if (sessionRaw === 'tmux')
|
|
66
|
+
throw new ConfigError(`${where}: session 'tmux' is no longer supported; use session: acp`);
|
|
67
|
+
if (sessionRaw !== 'acp')
|
|
68
|
+
throw new ConfigError(`${where}: session must be 'acp'`);
|
|
67
69
|
if (w.prompt_file !== undefined) {
|
|
68
70
|
if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
|
|
69
71
|
throw new ConfigError(`${where}: prompt_file must be an absolute path`);
|