@ours.network/fleet 1.2.0-nightly.54 → 1.2.0-nightly.55

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 (84) hide show
  1. package/README.md +20 -12
  2. package/dist/agent-ours/service.d.ts +4 -1
  3. package/dist/agent-ours/service.js +31 -7
  4. package/dist/application/log-service.js +8 -8
  5. package/dist/application/role-creation-service.js +2 -1
  6. package/dist/application/role-removal-service.js +7 -1
  7. package/dist/application/role-repository.js +17 -0
  8. package/dist/application/task-layouts.js +12 -2
  9. package/dist/application/task-room-service.js +19 -17
  10. package/dist/application/types.d.ts +2 -0
  11. package/dist/build-info.json +4 -4
  12. package/dist/cli.js +31 -3
  13. package/dist/config.d.ts +2 -0
  14. package/dist/harness/agent-session.d.ts +2 -0
  15. package/dist/harness/claude-code-session.js +1 -1
  16. package/dist/harness/codex-session.js +2 -2
  17. package/dist/managed-recovery.d.ts +13 -0
  18. package/dist/managed-recovery.js +31 -0
  19. package/dist/ops.d.ts +2 -0
  20. package/dist/ops.js +15 -2
  21. package/dist/rooms-tasks/cli.js +21 -0
  22. package/dist/rooms-tasks/close.d.ts +2 -0
  23. package/dist/rooms-tasks/close.js +66 -1
  24. package/dist/rooms-tasks/deletion.js +10 -0
  25. package/dist/rooms-tasks/erasure.js +4 -2
  26. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  27. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  28. package/dist/rooms-tasks/layout-service.d.ts +1 -0
  29. package/dist/rooms-tasks/layout-service.js +31 -13
  30. package/dist/rooms-tasks/layout.d.ts +7 -0
  31. package/dist/rooms-tasks/layout.js +35 -0
  32. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  33. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  34. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  35. package/dist/rooms-tasks/member-ownership.js +40 -0
  36. package/dist/rooms-tasks/provision.js +26 -1
  37. package/dist/rooms-tasks/recovery.d.ts +19 -0
  38. package/dist/rooms-tasks/recovery.js +110 -0
  39. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  40. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  41. package/dist/rooms-tasks/task-state.d.ts +1 -0
  42. package/dist/rooms-tasks/task-state.js +16 -1
  43. package/dist/rooms-tasks/types.d.ts +3 -0
  44. package/dist/rooms-tasks/workspace.d.ts +2 -0
  45. package/dist/rooms-tasks/workspace.js +5 -0
  46. package/dist/runner.d.ts +6 -0
  47. package/dist/runner.js +160 -22
  48. package/dist/session/acp.d.ts +1 -0
  49. package/dist/session/acp.js +7 -0
  50. package/dist/session/arbiter.d.ts +5 -0
  51. package/dist/session/arbiter.js +16 -2
  52. package/dist/session/codex-app-server.d.ts +1 -0
  53. package/dist/session/codex-app-server.js +5 -0
  54. package/dist/session/types.d.ts +2 -0
  55. package/dist/spawn.d.ts +5 -1
  56. package/dist/spawn.js +32 -3
  57. package/dist/startup-readiness.d.ts +1 -1
  58. package/dist/startup-readiness.js +3 -3
  59. package/dist/supervisor/adoption.d.ts +8 -0
  60. package/dist/supervisor/adoption.js +78 -0
  61. package/dist/supervisor/catalog.d.ts +62 -0
  62. package/dist/supervisor/catalog.js +254 -0
  63. package/dist/supervisor/fleet.d.ts +8 -0
  64. package/dist/supervisor/fleet.js +89 -0
  65. package/dist/supervisor/index.js +3 -6
  66. package/dist/supervisor/launchd.d.ts +1 -1
  67. package/dist/supervisor/launchd.js +24 -12
  68. package/dist/supervisor/legacy.d.ts +35 -0
  69. package/dist/supervisor/legacy.js +524 -0
  70. package/dist/supervisor/manager.d.ts +15 -0
  71. package/dist/supervisor/manager.js +258 -0
  72. package/dist/supervisor/scope.d.ts +4 -0
  73. package/dist/supervisor/scope.js +10 -0
  74. package/dist/supervisor/systemd.d.ts +2 -2
  75. package/dist/supervisor/systemd.js +14 -14
  76. package/dist/task-supervision.d.ts +9 -0
  77. package/dist/task-supervision.js +145 -0
  78. package/dist/temp-lifecycle.d.ts +17 -7
  79. package/dist/temp-lifecycle.js +85 -53
  80. package/dist/temp-supervisor-recovery.d.ts +1 -1
  81. package/dist/temp-supervisor-recovery.js +2 -2
  82. package/docs/fleet-migration.md +54 -0
  83. package/docs/task-workspaces.md +26 -0
  84. package/package.json +4 -1
package/README.md CHANGED
@@ -10,8 +10,9 @@ turns such sessions into **roles**: long-lived agents that
10
10
 
11
11
  - **run through structured managed sessions** behind one provider-neutral
12
12
  agent-session interface, which you can inspect or prompt,
13
- - are **supervised** — systemd (Linux) or launchd (macOS) restarts them on crash
14
- and brings them back after a reboot,
13
+ - are **supervised** — one Fleet systemd service (Linux) or LaunchAgent (macOS)
14
+ runs their existing agent runners, restarts them on crash, and restores the
15
+ desired running roster at boot or login,
15
16
  - **resume their context** across restarts (when the harness supports it),
16
17
  - **bind an ours.network identity**, so you — and every other agent — can message
17
18
  them by name over an end-to-end-encrypted channel
@@ -44,11 +45,11 @@ brain: { inline: { harness: codex, session: codex-app-server } }
44
45
 
45
46
  ```
46
47
  ~/fleet.yaml + ~/fleet/{agents,agent_templates,roles,brains,room_templates}/*.yaml your declaration
47
- │ ours-fleet up
48
+ │ ours-fleet init, then up
48
49
  ▼
49
- briefing.md per role ──► agent session adapter ──► native app-server or ACP transport
50
- ▲ │
51
- systemd --user / launchd ───────┘ restart on crash, start at boot/login
50
+ one Fleet OS service ──► private member catalog + existing agent runners
51
+ │
52
+ briefing.md per member ──► session adapter ──► native app-server or ACP transport
52
53
  ```
53
54
 
54
55
  Each role gets a state dir (`~/.ours-fleet/agents/<Name>/`) holding its briefing,
@@ -81,9 +82,10 @@ The state dir contract:
81
82
  | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
82
83
  | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `ours-install client --config /absolute/private/profile.json --integrations fleet --fleet-settings /absolute/fleet-settings.json` |
83
84
 
84
- Linux only: `ours-fleet init` enables *linger* so roles run without a login session
85
- and survive reboots. macOS: launchd agents start **at login** (no linger
86
- equivalent); logs land in `~/.ours-fleet/logs/`.
85
+ Linux only: `ours-fleet init` enables *linger* so Fleet runs without a login session
86
+ and restores running members after reboot. On macOS the Fleet LaunchAgent starts
87
+ **at login** (no linger equivalent). Member logs live in each member's state
88
+ directory; use `ours-fleet logs <Name>` to read them.
87
89
 
88
90
  ## Install
89
91
 
@@ -103,6 +105,12 @@ maintained adapters require Node 22 or newer.
103
105
  Each OS user manages their own fleet — to host roles under a sandboxed account,
104
106
  become that account and repeat.
105
107
 
108
+ When upgrading from per-agent services, coordinate package installation with
109
+ explicit `ours-fleet init`. Preserve operator readiness hooks on the common
110
+ parent before migration; new task/temporary creation requires that parent.
111
+ See the [migration and rollback runbook](docs/fleet-migration.md)
112
+ for changed stop/restart behaviour and legacy task transfer limits.
113
+
106
114
  ## Quickstart
107
115
 
108
116
  ```sh
@@ -331,9 +339,9 @@ Installs predating the stamp are compared by hashing their `dist/`, so two of
331
339
  those are still told apart.
332
340
 
333
341
  A permanent role brought up via `-c custom.yaml` remembers that file (`.config-path`
334
- in its state dir) across supervisor-triggered restarts — systemd/launchd re-invoke the
335
- agent process with no arguments, so without this the role would silently fall back to
336
- the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
342
+ in its state dir and its private member registration) across supervisor-triggered
343
+ restarts. Its runner resolves that original configuration instead of falling back
344
+ to the default `~/fleet.yaml`.
337
345
 
338
346
  ## split configuration field reference
339
347
 
@@ -22,4 +22,7 @@ export declare function readRoomReadiness(roomCid: string, name: string): {
22
22
  generation: number;
23
23
  } | undefined;
24
24
  /** Explicit logical-instance termination, independent of harness/bridge transport. */
25
- export declare function releaseManagedAgent(role: ResolvedRole): Promise<void>;
25
+ export declare function releaseManagedAgent(role: ResolvedRole, expected?: {
26
+ cid: string;
27
+ action: string;
28
+ }): Promise<void>;
@@ -3,10 +3,11 @@ import { ArtifactStore } from '../file-delivery/store.js';
3
3
  import { resolve, basename, extname } from 'node:path';
4
4
  import { socketPath as privateSocketPath } from '../socket-path.js';
5
5
  import { randomBytes, randomUUID } from 'node:crypto';
6
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { attachOursClient } from '@ours.network/sdk/client';
9
9
  import { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
10
+ import { assertSafeAncestors } from '../rooms-tasks/workspace.js';
10
11
  import { readClientProfile } from '../client-profile.js';
11
12
  import { stateRoot } from '../paths.js';
12
13
  import { createCoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
@@ -359,24 +360,47 @@ export function readRoomReadiness(roomCid, name) {
359
360
  }
360
361
  }
361
362
  /** Explicit logical-instance termination, independent of harness/bridge transport. */
362
- export async function releaseManagedAgent(role) {
363
+ export async function releaseManagedAgent(role, expected) {
363
364
  const profile = readClientProfile({ ...process.env, ...role.env });
364
365
  if (!profile)
365
366
  throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
366
367
  const root = privateRuntimeRoot(), dir = join(root, binderKey(profile.expectedInstanceId, role.identity));
367
- if (!existsSync(join(dir, 'instance.json')))
368
+ if (!existsSync(join(dir, 'instance.json'))) {
369
+ if (expected)
370
+ throw Error('MEMBER_RUNTIME_RETIREMENT_MISSING');
368
371
  return;
369
- const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
372
+ }
373
+ const read = (file) => {
374
+ const path = join(dir, file);
375
+ if (expected) {
376
+ assertSafeAncestors(dir);
377
+ const stat = lstatSync(path);
378
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
379
+ throw Error('MEMBER_RUNTIME_UNSAFE_PROOF');
380
+ }
381
+ return JSON.parse(readFileSync(path, 'utf8'));
382
+ };
383
+ const instance = read('instance.json');
384
+ if (expected) {
385
+ read('state.json');
386
+ read('owner.json');
387
+ }
370
388
  if (instance.role !== role.name)
371
389
  throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
372
390
  const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance.instance);
373
391
  try {
374
392
  const state = controller.journal.read();
375
- if (!state || state.phase === 'RELEASED')
393
+ if (expected && (!state || state.name !== role.identity || state.daemon !== profile.expectedInstanceId
394
+ || state.instance !== instance.instance || state.lifetime !== 'temporary' || instance.temporary !== true
395
+ || state.cid !== expected.cid || state.action !== expected.action))
396
+ throw Error('MEMBER_RUNTIME_RETIREMENT_MISMATCH');
397
+ if (!state)
376
398
  return;
377
- const owner = JSON.parse(readFileSync(join(dir, 'owner.json'), 'utf8'));
378
- if (owner.instance !== state.instance)
399
+ const owner = read('owner.json');
400
+ if (owner.instance !== state.instance || typeof owner.token !== 'string' || !owner.token)
379
401
  throw Error('OWNER_RECORD_MISMATCH');
402
+ if (state.phase === 'RELEASED')
403
+ return;
380
404
  const client = await attachOursClient({
381
405
  endpoint: profile.endpoint,
382
406
  expectedInstanceId: profile.expectedInstanceId,
@@ -3,7 +3,6 @@ import { createHmac, randomBytes } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
4
  import { realExec } from '../exec.js';
5
5
  import { logsRoot } from '../paths.js';
6
- import { unitFor } from '../supervisor/systemd.js';
7
6
  import { ROLE_NAME_RE } from '../config.js';
8
7
  import { FleetError } from './errors.js';
9
8
  const STANDARD_REDACTIONS = [
@@ -78,12 +77,13 @@ export class StructuredLogService {
78
77
  }
79
78
  async tail(roleId, requested, cursor) {
80
79
  const limit = Math.min(Math.max(requested, 1), 1_000);
81
- const args = this.backend.id === 'systemd'
82
- ? {
83
- cmd: 'journalctl',
84
- args: ['--user', '-u', unitFor(roleId), '-o', 'json', '--no-pager', '-n', String(limit)],
85
- }
86
- : this.backend.logsArgs(roleId, false);
80
+ const source = this.backend.logsArgs(roleId, false);
81
+ const args = { cmd: source.cmd, args: [...source.args] };
82
+ const count = args.args.indexOf('-n');
83
+ if (count >= 0)
84
+ args.args[count + 1] = String(limit);
85
+ if (args.cmd === 'journalctl')
86
+ args.args.push('-o', 'json', '--no-pager', '-n', String(limit));
87
87
  if (!['journalctl', 'tail', 'tmux'].includes(args.cmd))
88
88
  throw new FleetError('capability_unavailable', 'log backend is unsupported');
89
89
  const result = await this.exec(args.cmd, args.args);
@@ -93,7 +93,7 @@ export class StructuredLogService {
93
93
  const parsed = lines.map((line, index) => {
94
94
  let text = line;
95
95
  let at;
96
- if (this.backend.id === 'systemd') {
96
+ if (args.cmd === 'journalctl') {
97
97
  try {
98
98
  const json = JSON.parse(line);
99
99
  text = typeof json.MESSAGE === 'string' ? json.MESSAGE : line;
@@ -59,7 +59,8 @@ export class RoleCreationService {
59
59
  }
60
60
  async createManaged(caller, requested) {
61
61
  const plan = this.previewSpawn({ origin: 'managed', caller, options: requested });
62
- const creationActionId = randomUUID();
62
+ const creationActionId = requested.roomMemberStartup?.task_id
63
+ ? requested.creationActionId ?? randomUUID() : randomUUID();
63
64
  plan.options.creationActionId = creationActionId;
64
65
  const statePath = await this.launchSync(plan.options);
65
66
  const selectionSummary = (kind) => {
@@ -3,7 +3,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs'
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { loadConfig, ROLE_NAME_RE } from '../config.js';
5
5
  import { agentDir, stateRoot } from '../paths.js';
6
- import { rmRole } from '../ops.js';
6
+ import { assertStandaloneRoleRemoval, rmRole } from '../ops.js';
7
7
  import { FleetError } from './errors.js';
8
8
  import { provesGeneratedAgentSource } from '../generated-agent-source.js';
9
9
  export class RoleRemovalService {
@@ -19,6 +19,12 @@ export class RoleRemovalService {
19
19
  if (!ROLE_NAME_RE.test(requested))
20
20
  throw new FleetError('invalid_request', 'invalid role name');
21
21
  const cfg = loadConfig(this.options.configPath);
22
+ try {
23
+ assertStandaloneRoleRemoval(cfg, requested);
24
+ }
25
+ catch (error) {
26
+ throw new FleetError('conflict', error instanceof Error ? error.message : String(error));
27
+ }
22
28
  const names = new Set(cfg.roles.map(role => role.name));
23
29
  for (const root of [dirname(agentDir('_')), dirname(agentDir('_', true))]) {
24
30
  try {
@@ -5,6 +5,21 @@ import { loadConfig, resolveSnapshotMonitor, resolvePermissions, ROLE_NAME_RE, }
5
5
  import { agentsRoot, tmpRoot } from '../paths.js';
6
6
  import { readSpawnLineage } from '../web/topology.js';
7
7
  const MAX_SNAPSHOT_BYTES = 256 * 1024;
8
+ function retainedTaskId(dir, name) {
9
+ try {
10
+ const path = join(dir, '.temp-supervisor.json');
11
+ const stat = lstatSync(path);
12
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024)
13
+ return undefined;
14
+ const record = JSON.parse(readFileSync(path, 'utf8'));
15
+ const id = record?.taskOwner?.taskId;
16
+ return record?.version === 1 && record?.role === name && typeof id === 'string'
17
+ && /^[0-9a-z]{9}[0-9a-f]{8}$/.test(id) ? id : undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
8
23
  function safeDirs(root) {
9
24
  try {
10
25
  return readdirSync(root, { withFileTypes: true })
@@ -128,10 +143,12 @@ export class RoleRepository {
128
143
  const stateRef = inTemporary ? { lifetime: 'temporary' }
129
144
  : inPermanent ? { lifetime: 'permanent' } : undefined;
130
145
  const tempStateDir = inTemporary ? join(temporaryRoot, name) : undefined;
146
+ const taskId = !config && tempStateDir ? retainedTaskId(tempStateDir, name) : undefined;
131
147
  return {
132
148
  id: name,
133
149
  lifetime: config ? 'permanent' : inTemporary ? 'temporary' : 'orphan',
134
150
  configured: Boolean(config),
151
+ ...(taskId ? { taskId } : {}),
135
152
  config: config ? view(config) : tempRole ? view(tempRole) : undefined,
136
153
  stateRef,
137
154
  stateHealth,
@@ -16,6 +16,9 @@ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
16
16
  import { layoutSupervisorId } from '../rooms-tasks/layout-control.js';
17
17
  import { layoutRunPath, RoomLayoutService } from '../rooms-tasks/layout-service.js';
18
18
  import { FleetError } from './errors.js';
19
+ import { ownedLayoutRetirementSeats } from '../rooms-tasks/layout-member-retirement.js';
20
+ import { eraseMemberArtifacts } from '../rooms-tasks/erasure.js';
21
+ import { assertTaskRegistrationsAbsent } from '../supervisor/catalog.js';
19
22
  /** Hash of the authorable definition only (never the source file path). */
20
23
  export function layoutDefinitionHash(definition) {
21
24
  const { version, description, participants, rooms } = definition;
@@ -238,8 +241,15 @@ export class TaskLayouts {
238
241
  const link = task.layout;
239
242
  if (!link)
240
243
  return;
241
- if (this.runExists(link))
242
- await this.service().open(link.run_id, true).close(layoutSupervisorId());
244
+ if (this.runExists(link)) {
245
+ const engine = this.service().open(link.run_id, true);
246
+ await engine.delete(layoutSupervisorId());
247
+ const state = engine.snapshot();
248
+ const rooms = Object.values(state.rooms).flatMap(room => room.native ? [room.native.room_id] : []);
249
+ assertTaskRegistrationsAbsent(task.task_id);
250
+ await eraseMemberArtifacts('task', task.task_id, ownedLayoutRetirementSeats(state, link.run_id), rooms);
251
+ }
252
+ assertTaskRegistrationsAbsent(task.task_id);
243
253
  await this.withTaskLock(link.run_id, async () => {
244
254
  for (const path of [layoutRunPath(link.run_id), this.provenancePath(link.run_id), taskBriefPath(task)])
245
255
  rmSync(path, { force: true });
@@ -378,23 +378,25 @@ export class TaskRoomApplicationService {
378
378
  const ready = task.state === 'active' && room?.state === 'active'
379
379
  && active === expected && launched === expected;
380
380
  const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
381
- const nextAction = task.terminal_intent
382
- ? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
383
- : room?.state === 'closing' || room?.state === 'closed'
384
- ? `Complete room cleanup; do not restart provisioning.`
385
- : room?.provisioning_detail === 'member_failed'
386
- ? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
387
- : room?.provisioning_detail === 'waiting_owner_authorization'
388
- ? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
389
- : room?.provisioning_detail === 'waiting_owner_invite'
390
- ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
391
- : room?.provisioning_detail === 'owner_cid_mismatch'
392
- ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
393
- : room?.provisioning_detail === 'waiting_cowork'
394
- ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
395
- : failed
396
- ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
397
- : undefined;
381
+ const nextAction = blocker === 'FLEET_SERVICE_NOT_INSTALLED: run ours-fleet init'
382
+ ? `Run ours-fleet init, then retry ours-fleet task start ${task.task_id}.`
383
+ : task.terminal_intent
384
+ ? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
385
+ : room?.state === 'closing' || room?.state === 'closed'
386
+ ? `Complete room cleanup; do not restart provisioning.`
387
+ : room?.provisioning_detail === 'member_failed'
388
+ ? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
389
+ : room?.provisioning_detail === 'waiting_owner_authorization'
390
+ ? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
391
+ : room?.provisioning_detail === 'waiting_owner_invite'
392
+ ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
393
+ : room?.provisioning_detail === 'owner_cid_mismatch'
394
+ ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
395
+ : room?.provisioning_detail === 'waiting_cowork'
396
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
397
+ : failed
398
+ ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
399
+ : undefined;
398
400
  return {
399
401
  kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
400
402
  launch: {
@@ -26,6 +26,8 @@ export interface RoleRecord {
26
26
  id: string;
27
27
  lifetime: RoleLifetime;
28
28
  configured: boolean;
29
+ /** Read-only display ownership; never authorizes lifecycle operations. */
30
+ taskId?: string;
29
31
  config?: ResolvedRoleView;
30
32
  stateRef?: {
31
33
  lifetime: 'permanent' | 'temporary';
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.54",
3
- "buildId": "b10c893ceda0",
4
- "commit": "4da7d4782c1caf686836806bc8606fc50887edc5",
2
+ "version": "1.2.0-nightly.55",
3
+ "buildId": "6e4199ab6d73",
4
+ "commit": "a66362bc99a9896700192f5ff2c8454a0e209766",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-08T10:22:35.148Z",
6
+ "builtAt": "2026-10-09T12:16:42.572Z",
7
7
  "capabilities": [
8
8
  "managed-cli.codex-reasoning-effort-v1",
9
9
  "managed-cli.setup-v1",
package/dist/cli.js CHANGED
@@ -617,7 +617,7 @@ cOpt(program.command('status <name>').description('declared persistent Agent uni
617
617
  + `${ledger.lastTermination.runStartedAt ?? 'unknown'}) died without an orderly exit; `
618
618
  + `observed ${ledger.lastTermination.observedAt}`
619
619
  + `\n abrupt terminations recorded: ${ledger.abruptTerminations ?? 1}`
620
- + `\n cause (signal / OOM-kill): journalctl --user -u ours-fleet-agent@${name}.service`);
620
+ + `\n inspect member output: ours-fleet logs ${name}`);
621
621
  const modelStatus = joinPath(agentDir(name), '.model-status');
622
622
  if (existsSync(modelStatus)) {
623
623
  try {
@@ -1867,6 +1867,33 @@ program.command('_wait-daemon <name>', { hidden: true }).description('internal:
1867
1867
  die(e);
1868
1868
  }
1869
1869
  });
1870
+ program.command('_wait-temp-daemon <name>', { hidden: true })
1871
+ .action(async (name) => {
1872
+ try {
1873
+ await waitForRoleDaemon(name, undefined, {}, true);
1874
+ }
1875
+ catch (error) {
1876
+ die(error);
1877
+ }
1878
+ });
1879
+ program.command('_run-fleet', { hidden: true }).action(async () => {
1880
+ try {
1881
+ const { runFleetManager } = await import('./supervisor/manager.js');
1882
+ await runFleetManager(process.argv[1]);
1883
+ }
1884
+ catch (error) {
1885
+ die(error);
1886
+ }
1887
+ });
1888
+ program.command('_run-managed <key> <generation>', { hidden: true }).action(async (key, generation) => {
1889
+ try {
1890
+ const { runManagedMember } = await import('./supervisor/manager.js');
1891
+ await runManagedMember(key, generation);
1892
+ }
1893
+ catch (error) {
1894
+ die(error);
1895
+ }
1896
+ });
1870
1897
  program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
1871
1898
  .option('-c, --configuration <file>')
1872
1899
  .action(async (name, opts) => {
@@ -1889,9 +1916,10 @@ program.command('_run-temp <name>', { hidden: true }).description('internal: tem
1889
1916
  }
1890
1917
  });
1891
1918
  program.command('_run-temp-worker <name>', { hidden: true })
1892
- .action(async (name) => {
1919
+ .option('--suspend-on-stop')
1920
+ .action(async (name, options) => {
1893
1921
  try {
1894
- await runTemp(name);
1922
+ await runTemp(name, { suspendOnStop: options.suspendOnStop === true });
1895
1923
  }
1896
1924
  catch (error) {
1897
1925
  if (error instanceof SupervisorRecycleRequiredError)
package/dist/config.d.ts CHANGED
@@ -198,6 +198,8 @@ export interface AgentTemplateInstance {
198
198
  }
199
199
  /** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
200
200
  export interface RoomMemberStartup {
201
+ /** Task ownership; absent for standalone rooms and legacy snapshots. */
202
+ task_id?: string;
201
203
  workspace?: import('./rooms-tasks/workspace.js').OwnedWorkspace;
202
204
  room_id: string;
203
205
  room_identity_cid: string;
@@ -30,6 +30,8 @@ export interface AgentSessionStartOptions {
30
30
  cwd: string;
31
31
  stateDir: string;
32
32
  mode: 'fresh' | 'resume';
33
+ /** Durable task context must never silently fall back to a fresh session. */
34
+ requireResume?: boolean;
33
35
  permissions: CommonPermissions;
34
36
  permissionMode: {
35
37
  fleetMode: FleetPermissionMode;
@@ -24,7 +24,7 @@ export class ClaudeCodeAgentSessionAdapter {
24
24
  return this.transport({
25
25
  name: role.name, harness: role.harness,
26
26
  argv: launch.argv, cwd: options.cwd, env: launch.env,
27
- stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
27
+ stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
28
28
  modeId: this.strategy.permissionModeId(role),
29
29
  mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
30
30
  inheritEnvironment: options.managedOurs ? false : undefined,
@@ -32,7 +32,7 @@ export class CodexAgentSessionAdapter {
32
32
  if (role.session === 'codex-app-server')
33
33
  return this.nativeTransport({
34
34
  name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
35
- stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
35
+ stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
36
36
  permissionMode: options.permissionMode,
37
37
  model: role.model, effort: role.effort,
38
38
  approvalPolicy: this.strategy.approvalPolicy(role),
@@ -44,7 +44,7 @@ export class CodexAgentSessionAdapter {
44
44
  return this.transport({
45
45
  name: role.name, harness: role.harness,
46
46
  argv: launch.argv, cwd: options.cwd, env: launch.env,
47
- stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
47
+ stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
48
48
  modeId: this.strategy.permissionModeId(role),
49
49
  mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
50
50
  inheritEnvironment: options.managedOurs ? false : undefined,
@@ -0,0 +1,13 @@
1
+ /** Body-free recovery cursor. Dispatching is uncertain after a crash, never
2
+ * permission to replay an initial task which may already have caused effects. */
3
+ export interface ManagedRecovery {
4
+ version: 1;
5
+ session: 'pending' | 'established';
6
+ readiness: boolean;
7
+ initial: 'pending' | 'dispatching' | 'completed';
8
+ active: boolean;
9
+ }
10
+ export declare const managedRecoveryPath: (dir: string) => string;
11
+ export declare function readManagedRecovery(dir: string): ManagedRecovery | undefined;
12
+ export declare function writeManagedRecovery(dir: string, value: ManagedRecovery): void;
13
+ export declare function firstManagedSessionMayStart(dir: string): boolean;
@@ -0,0 +1,31 @@
1
+ import { lstatSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { replaceFileAtomically } from './atomic-file.js';
4
+ export const managedRecoveryPath = (dir) => join(dir, '.managed-recovery.json');
5
+ export function readManagedRecovery(dir) {
6
+ const path = managedRecoveryPath(dir);
7
+ let stat;
8
+ try {
9
+ stat = lstatSync(path);
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT')
13
+ return;
14
+ throw error;
15
+ }
16
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
17
+ throw Error('MANAGED_RECOVERY_UNSAFE_PROOF');
18
+ const value = JSON.parse(readFileSync(path, 'utf8'));
19
+ if (value.version !== 1 || !['pending', 'established'].includes(value.session)
20
+ || !['pending', 'dispatching', 'completed'].includes(value.initial)
21
+ || typeof value.readiness !== 'boolean' || typeof value.active !== 'boolean')
22
+ throw Error('MANAGED_RECOVERY_INVALID_PROOF');
23
+ return value;
24
+ }
25
+ export function writeManagedRecovery(dir, value) {
26
+ replaceFileAtomically(managedRecoveryPath(dir), JSON.stringify(value) + '\n');
27
+ }
28
+ export function firstManagedSessionMayStart(dir) {
29
+ const proof = readManagedRecovery(dir);
30
+ return Boolean(proof?.session === 'pending' && proof.initial === 'pending' && !proof.readiness && !proof.active);
31
+ }
package/dist/ops.d.ts CHANGED
@@ -54,5 +54,7 @@ export declare function up(cfg: FleetConfig, names: string[], deps: OpsDeps, con
54
54
  export declare function down(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
55
55
  /** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
56
56
  export declare function restartRoles(cfg: FleetConfig, names: string[], deps: OpsDeps, mode: 'keep' | 'fresh', configPath?: string): Promise<void>;
57
+ /** Read-only admission check shared by CLI removal and web preview, before any archive. */
58
+ export declare function assertStandaloneRoleRemoval(cfg: FleetConfig, name: string): void;
57
59
  /** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
58
60
  export declare function rmRole(cfg: FleetConfig, name: string, deps: OpsDeps): Promise<void>;
package/dist/ops.js CHANGED
@@ -7,8 +7,8 @@ import { findRole } from './config.js';
7
7
  import { getAdapter } from './harness/registry.js';
8
8
  import { generateBriefing } from './briefing.js';
9
9
  import { managedCliBriefing } from './managed-cli.js';
10
- import { resetRestartLedger } from './runner.js';
11
- import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
10
+ import { resetRestartLedger, loadTempRole } from './runner.js';
11
+ import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, readTempSupervisor, } from './temp-lifecycle.js';
12
12
  import { realExec } from './exec.js';
13
13
  import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
14
14
  import { provesGeneratedAgentSource } from './generated-agent-source.js';
@@ -223,8 +223,21 @@ export async function restartRoles(cfg, names, deps, mode, configPath) {
223
223
  : `↻ ${role.name} — restarted (resumes; briefing re-synced)`);
224
224
  }
225
225
  }
226
+ /** Read-only admission check shared by CLI removal and web preview, before any archive. */
227
+ export function assertStandaloneRoleRemoval(cfg, name) {
228
+ if (cfg.roles.some(role => role.name === name) || !/^[A-Za-z0-9_-]+$/.test(name))
229
+ return;
230
+ const dir = agentDir(name, true);
231
+ if (!existsSync(dir))
232
+ return;
233
+ const taskId = readTempSupervisor(dir)?.taskOwner?.taskId
234
+ ?? (existsSync(join(dir, 'role.yaml')) ? loadTempRole(name).roomMemberStartup?.task_id : undefined);
235
+ if (taskId)
236
+ throw Error(`TASK_MEMBER_IS_TASK_OWNED: ${name} belongs to task ${taskId}; use the owning task lifecycle; if its record is missing or corrupt, preserve evidence and restore the original records before retrying task delete`);
237
+ }
226
238
  /** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
227
239
  export async function rmRole(cfg, name, deps) {
240
+ assertStandaloneRoleRemoval(cfg, name);
228
241
  const temporaryDir = /^[A-Za-z0-9_-]+$/.test(name) ? agentDir(name, true) : '';
229
242
  const configured = cfg.roles.find(role => role.name === name);
230
243
  if (!configured && temporaryDir && existsSync(temporaryDir)) {
@@ -891,6 +891,27 @@ export function registerTaskCommands(parent, cOpt) {
891
891
  dieTaskRoom(e);
892
892
  }
893
893
  });
894
+ cOpt(taskCmd.command('recover-members <id>'))
895
+ .description('resume proven stopped task members in-place; never create identities or replay invites')
896
+ .option('--json', 'JSON output')
897
+ .action(async (id, opts) => {
898
+ try {
899
+ const { recoverTaskMembers } = await import('./recovery.js');
900
+ const task = getTask(id), cfg = loadConfig(opts.configuration);
901
+ const members = await recoverTaskMembers({ taskId: id, binPath: getBinPath(),
902
+ cowork: task.layout ? undefined : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config }) });
903
+ if (opts.json)
904
+ console.log(JSON.stringify({ schema_version: 1, task_id: id, members }, null, 2));
905
+ else
906
+ for (const member of members)
907
+ console.log(`${member.name}: ${member.status}`);
908
+ }
909
+ catch (e) {
910
+ if (opts.json)
911
+ die(e);
912
+ dieTaskRoom(e);
913
+ }
914
+ });
894
915
  cOpt(taskCmd.command('start <id>'))
895
916
  .description('idempotently select a plan, provision, and start a task')
896
917
  .option('--template <name>', 'room template')
@@ -1,3 +1,4 @@
1
+ import { retireTaskMemberService } from './task-service-retirement.js';
1
2
  import { withFileLock } from '../atomic-file.js';
2
3
  import { type TempLifecycleDeps } from '../temp-lifecycle.js';
3
4
  import { type CoworkAdapter } from './cowork-adapter.js';
@@ -5,6 +6,7 @@ import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
5
6
  export declare const CLOSE_LOCK_STALE_MS: number;
6
7
  export declare function roomCloseLockPath(roomId: string): string;
7
8
  export interface RoomCloseDeps {
9
+ retireTaskService?: typeof retireTaskMemberService;
8
10
  inspectMember?(seat: RoomMemberSeat): Promise<{
9
11
  launchId: string;
10
12
  }>;