@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.
Files changed (91) hide show
  1. package/README.md +41 -50
  2. package/dist/application/capabilities.d.ts +0 -1
  3. package/dist/application/capabilities.js +3 -13
  4. package/dist/application/errors.d.ts +1 -1
  5. package/dist/application/fleet-query-service.d.ts +0 -3
  6. package/dist/application/fleet-query-service.js +1 -24
  7. package/dist/application/role-creation-service.d.ts +5 -5
  8. package/dist/application/role-creation-service.js +5 -7
  9. package/dist/application/role-repository.d.ts +1 -2
  10. package/dist/application/role-repository.js +7 -7
  11. package/dist/application/session-control.d.ts +0 -12
  12. package/dist/application/session-control.js +0 -42
  13. package/dist/application/session-mutations.d.ts +5 -5
  14. package/dist/application/task-room-service.d.ts +37 -2
  15. package/dist/application/task-room-service.js +49 -7
  16. package/dist/application/types.d.ts +0 -7
  17. package/dist/atomic-file.d.ts +5 -3
  18. package/dist/atomic-file.js +32 -8
  19. package/dist/briefing.js +1 -4
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +9 -23
  22. package/dist/config.d.ts +7 -7
  23. package/dist/config.js +13 -34
  24. package/dist/docs.d.ts +1 -1
  25. package/dist/docs.js +46 -30
  26. package/dist/doctor.js +0 -7
  27. package/dist/fleet-proxy.d.ts +1 -1
  28. package/dist/harness/acp-session-transport.d.ts +8 -0
  29. package/dist/harness/acp-session-transport.js +3 -0
  30. package/dist/harness/agent-session.d.ts +33 -0
  31. package/dist/harness/agent-session.js +1 -0
  32. package/dist/harness/claude-code-session.d.ts +18 -0
  33. package/dist/harness/claude-code-session.js +28 -0
  34. package/dist/harness/claude-code.d.ts +2 -1
  35. package/dist/harness/claude-code.js +29 -81
  36. package/dist/harness/codex-session.d.ts +18 -0
  37. package/dist/harness/codex-session.js +28 -0
  38. package/dist/harness/codex.d.ts +2 -1
  39. package/dist/harness/codex.js +35 -45
  40. package/dist/harness/types.d.ts +6 -38
  41. package/dist/index.d.ts +3 -5
  42. package/dist/index.js +1 -3
  43. package/dist/isolation/resources.d.ts +3 -7
  44. package/dist/isolation/resources.js +3 -7
  45. package/dist/model-env.d.ts +2 -3
  46. package/dist/model-env.js +2 -3
  47. package/dist/monitor.d.ts +1 -24
  48. package/dist/monitor.js +5 -140
  49. package/dist/owner-channel/attachments.d.ts +2 -4
  50. package/dist/owner-channel/attachments.js +6 -39
  51. package/dist/owner-channel/channel.d.ts +2 -2
  52. package/dist/owner-channel/channel.js +16 -3
  53. package/dist/owner-channel/commands.d.ts +18 -1
  54. package/dist/owner-channel/commands.js +73 -4
  55. package/dist/rooms-tasks/cli.js +121 -2
  56. package/dist/rooms-tasks/task-lists.d.ts +19 -0
  57. package/dist/rooms-tasks/task-lists.js +96 -0
  58. package/dist/rooms-tasks/task-state.d.ts +3 -0
  59. package/dist/rooms-tasks/task-state.js +281 -175
  60. package/dist/rooms-tasks/types.d.ts +11 -1
  61. package/dist/runner.d.ts +5 -15
  62. package/dist/runner.js +26 -102
  63. package/dist/session/acp.d.ts +2 -2
  64. package/dist/session/acp.js +2 -3
  65. package/dist/session/activity.d.ts +2 -3
  66. package/dist/session/activity.js +2 -3
  67. package/dist/session/arbiter.d.ts +4 -4
  68. package/dist/session/control.d.ts +3 -3
  69. package/dist/session/types.d.ts +11 -5
  70. package/dist/spawn.js +8 -9
  71. package/dist/supervisor/none.d.ts +1 -1
  72. package/dist/supervisor/none.js +2 -2
  73. package/dist/tmux.d.ts +1 -14
  74. package/dist/tmux.js +1 -51
  75. package/dist/watchdog/config.js +6 -4
  76. package/dist/watchdog/run.js +8 -23
  77. package/dist/web/auth.d.ts +1 -1
  78. package/dist/web/runtime.js +6 -15
  79. package/dist/web/server.d.ts +3 -3
  80. package/dist/web/server.js +89 -15
  81. package/dist/web/topology-promote.js +2 -5
  82. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  83. package/dist/web-app/assets/{index-BCBK78hw.js → index-DhMDVRU1.js} +6 -6
  84. package/dist/web-app/index.html +2 -2
  85. package/package.json +3 -8
  86. package/dist/session/tmux.d.ts +0 -28
  87. package/dist/session/tmux.js +0 -80
  88. package/dist/web/terminal/bridge.d.ts +0 -27
  89. package/dist/web/terminal/bridge.js +0 -317
  90. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  91. 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, shq } from './exec.js';
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
- startAcpSession: opts => AcpSession.start(opts),
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 ?? 'tmux';
488
- let launch = sessionBackend === 'acp'
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
- // launch (tmux.newSession); the cheap monitor prime still runs immediately after.
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 acpSession;
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
- if (sessionBackend === 'acp') {
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
- acpSession = await deps.startAcpSession({
602
- name,
603
- argv: wrappedArgv,
604
- cwd: runCwd,
605
- env: harnessChildEnv(role, launch.env, dir),
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 = acpSession.pid;
626
- arbiter = new RoleTurnArbiter(acpSession);
568
+ pid = agentSession.pid;
569
+ arbiter = new RoleTurnArbiter(agentSession);
627
570
  sessionHandle = arbiter;
628
- unsubscribeRecovery = acpSession.subscribe(event => {
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 acpSession.close();
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 acpSession.close();
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 ACP session and any queued wake remain
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 acpSession.close();
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 acpSession.close();
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 tmux
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 (acpSession && !sessionClosed)
952
- await acpSession.close();
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 = acpSession
958
- ? acpSession.exitResult()
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');
@@ -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, RuntimeSelectorMetadata, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
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 SessionHandle {
84
+ export declare class AcpSession implements AgentSession {
85
85
  private readonly options;
86
86
  readonly backend: "acp";
87
87
  readonly pid: number;
@@ -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. The tmux path can only see a
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 (SessionHandle) ─────────────────────────────
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`: a backend that
22
- * cannot see the agent (tmux) has no evidence, and no evidence must never be
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
  /**
@@ -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`: a backend that
15
- * cannot see the agent (tmux) has no evidence, and no evidence must never be
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, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnResult } from './types.js';
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 SessionHandle {
18
+ export declare class RoleTurnArbiter implements AgentSession {
19
19
  private readonly session;
20
- readonly backend: import("../config.js").SessionBackendId;
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: SessionHandle);
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, SessionHandle, SessionSnapshot } from './types.js';
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: SessionHandle, since: number): RetainedEventPage;
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: SessionHandle, log: (line: string) => void);
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. */
@@ -96,7 +96,7 @@ export interface InterruptOutcome {
96
96
  reasonCode?: string;
97
97
  }
98
98
  /**
99
- * What a `SessionHandle.interrupt` implementation may resolve. Before 0.17.1 the
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 as the pane shell saw it (tmux only). */
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. Absent on backends
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
- export interface SessionHandle {
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 && !['tmux', 'acp'].includes(o.session))
127
- throw new Error(`invalid --session '${o.session}'; allowed: tmux, acp`);
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 ?? 'tmux';
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, 'tmux')),
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 ?? 'tmux';
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 NOT inside a tmux session named <name>.
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: sessions are plain tmux, nothing survives a reboot and
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
  */
@@ -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: sessions are plain tmux, nothing survives a reboot and
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: sessions are plain tmux (no reboot survival)']; },
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; all session handling in the core goes through this. */
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; all session handling in the core goes through this. */
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
  }
@@ -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, tmux session, and run lock with no error until they collide at
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 ?? 'tmux';
65
- if (sessionRaw !== 'tmux' && sessionRaw !== 'acp')
66
- throw new ConfigError(`${where}: session must be 'tmux' or 'acp'`);
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`);