@ours.network/fleet 1.2.0-nightly.10 → 1.2.0-nightly.12

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 CHANGED
@@ -1752,3 +1752,18 @@ or message delivery. File tools resolve paths in the invoking CLI process or
1752
1752
  Fleet web server's filesystem context, with that process's access permissions.
1753
1753
 
1754
1754
  Task workspace ownership, retention, safe deletion and legacy migration are documented in [Task workspaces](docs/task-workspaces.md).
1755
+
1756
+ ### Active task readiness after interruption
1757
+
1758
+ Task and room list states, and `taskProvisioningOutcome`, describe durable lifecycle
1759
+ and provisioning progress; they are not live health checks. Retrying `task start`
1760
+ or `task work` on an active task checks the current Cowork room identity and seats,
1761
+ member launch ownership, supervisor liveness, and authenticated session status
1762
+ before reporting success. Waiting for provisioning completion also checks live
1763
+ readiness. This is a point-in-time observation, not proof of an agent-authored reply.
1764
+
1765
+ A `task_not_ready` result distinguishes confirmed degradation from unknown health
1766
+ (for example, an unavailable control socket). Ask Fleet Coordinator to inspect room
1767
+ and member status and choose supported recovery. These checks do not restart agents,
1768
+ recreate identities, redeem invites, or rewrite lifecycle state. Temporary room agents
1769
+ remain transient across host reboot; retained launch records do not restore them.
@@ -1,3 +1,4 @@
1
+ import { type LiveReadinessDeps } from '../rooms-tasks/live-readiness.js';
1
2
  import { type FleetConfig } from '../config.js';
2
3
  import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
3
4
  import { provisionMembers } from '../rooms-tasks/provision.js';
@@ -57,9 +58,9 @@ export interface TaskProvisioningOutcome {
57
58
  /** Repairable terminal observation derived entirely from durable Task/Room state. */
58
59
  export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
59
60
  export declare class TaskRoomApplicationError extends Error {
60
- readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
61
+ readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready';
61
62
  readonly fields: Readonly<Record<string, string>>;
62
- constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found', message: string, fields?: Readonly<Record<string, string>>);
63
+ constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready', message: string, fields?: Readonly<Record<string, string>>);
63
64
  }
64
65
  export interface CreateTaskRequest {
65
66
  actor: TaskRoomActor;
@@ -87,6 +88,7 @@ export interface CreateRoomRequest {
87
88
  }
88
89
  export declare function resolveRoomLaunchPolicy(template: TemplateSnapshot | undefined, override: boolean | undefined): RoomLaunchPolicy;
89
90
  export interface TaskRoomServiceDeps {
91
+ liveReadiness?: LiveReadinessDeps;
90
92
  loadConfiguration?(path?: string): FleetConfig;
91
93
  cowork?(config: FleetConfig): CoworkAdapter;
92
94
  binPath?(): string;
@@ -151,6 +153,7 @@ export declare class TaskRoomApplicationService {
151
153
  task: TaskRecord;
152
154
  orchestration: RoomOrchestrationRecord | undefined;
153
155
  };
156
+ /** Durable provisioning progress only; use active task start/work for a live readiness check. */
154
157
  taskProvisioningOutcome(taskId: string): TaskProvisioningOutcome;
155
158
  awaitTaskProvisioning(input: {
156
159
  actor: TaskRoomActor;
@@ -300,6 +303,7 @@ export declare class TaskRoomApplicationService {
300
303
  taskId: string;
301
304
  outcome?: TaskOutcome;
302
305
  }): Promise<TaskSettlementPlan>;
306
+ private assertTaskLiveReadiness;
303
307
  ensureTaskWork(input: {
304
308
  actor: TaskRoomActor;
305
309
  taskId: string;
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
2
3
  import { ConfigError, loadConfig } from '../config.js';
3
4
  import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
4
5
  import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
@@ -223,6 +224,7 @@ export class TaskRoomApplicationService {
223
224
  const task = readTask(taskId);
224
225
  return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
225
226
  }
227
+ /** Durable provisioning progress only; use active task start/work for a live readiness check. */
226
228
  taskProvisioningOutcome(taskId) {
227
229
  const task = readTask(taskId);
228
230
  const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
@@ -271,6 +273,10 @@ export class TaskRoomApplicationService {
271
273
  const deadline = now() + (input.waitMs ?? 60_000);
272
274
  for (;;) {
273
275
  const outcome = this.taskProvisioningOutcome(input.taskId);
276
+ if (outcome.kind === 'ready') {
277
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
278
+ await this.assertTaskLiveReadiness(outcome.task, outcome.room, cfg);
279
+ }
274
280
  if (outcome.kind !== 'in_progress' || now() >= deadline) {
275
281
  recordTaskProvisioningOutcome(outcome);
276
282
  return outcome;
@@ -657,6 +663,14 @@ export class TaskRoomApplicationService {
657
663
  (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
658
664
  return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
659
665
  }
666
+ async assertTaskLiveReadiness(task, room, cfg) {
667
+ const cowork = this.deps.cowork ? this.deps.cowork(cfg)
668
+ : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config });
669
+ const issue = await taskLiveReadiness(task, room, cowork, this.deps.liveReadiness);
670
+ if (issue)
671
+ throw new TaskRoomApplicationError('task_not_ready', `Task readiness is ${issue.state}: ${issue.reason}. Ask Fleet Coordinator to inspect the room and member status and choose supported recovery; do not blindly relaunch or reuse invites.`, { task: task.task_id, room: task.room_id ?? 'missing', readiness: issue.state,
672
+ reason: issue.reason, ...(issue.member ? { member: issue.member } : {}) });
673
+ }
660
674
  async ensureTaskWork(input) {
661
675
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
662
676
  let task = readTask(input.taskId);
@@ -681,8 +695,10 @@ export class TaskRoomApplicationService {
681
695
  const pinned = durableTemplate;
682
696
  if (!pinned)
683
697
  throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
684
- if (durableRequestMatches)
698
+ if (durableRequestMatches) {
699
+ await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
685
700
  return { task, status: 'already_active' };
701
+ }
686
702
  const templateName = input.template ?? pinned.name;
687
703
  const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
688
704
  if (!definition)
@@ -695,6 +711,7 @@ export class TaskRoomApplicationService {
695
711
  provisioned: `${pinned.name}@${pinned.version}`
696
712
  });
697
713
  }
714
+ await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
698
715
  return { task, status: 'already_active' };
699
716
  }
700
717
  let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
@@ -833,7 +850,11 @@ export class TaskRoomApplicationService {
833
850
  }
834
851
  }
835
852
  const resultingRoom = getRoomRecord(task.room_id);
836
- return { task, status: task.state === 'active' && resultingRoom?.state === 'active' ? 'ready' : 'in_progress' };
853
+ if (task.state === 'active' && resultingRoom?.state === 'active') {
854
+ await this.assertTaskLiveReadiness(task, resultingRoom, cfg);
855
+ return { task, status: 'ready' };
856
+ }
857
+ return { task, status: 'in_progress' };
837
858
  }
838
859
  createTemplate(cfg, requested, noRoom) {
839
860
  if (noRoom)
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.10",
3
- "buildId": "cb4ba2817213",
4
- "commit": "cb2c392932a631dcb0a073bef469e50d7114d93e",
2
+ "version": "1.2.0-nightly.12",
3
+ "buildId": "6f07096c9a0c",
4
+ "commit": "7e323845714c5b638db197673b84f28fe00a2f75",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-24T09:16:57.652Z",
6
+ "builtAt": "2026-09-24T13:14:00.219Z",
7
7
  "capabilities": [
8
8
  "cowork.http-management-v1",
9
9
  "monitor.interrupt.after_tool"
@@ -883,8 +883,15 @@ export class OwnerChannel {
883
883
  const recovered = [];
884
884
  for (const claim of this.messageRecovery.list()) {
885
885
  const item = await this.client.getHistoryItem(claim.wireId);
886
- if (!item)
887
- throw new Error(`journaled owner message ${claim.wireId} is missing from persistent history`);
886
+ if (!item) {
887
+ // The daemon has definitively lost this body. Retrying the claim would
888
+ // block every later owner message, including after a supervisor restart.
889
+ // Keep transient lookup errors retryable; only an explicit absence skips.
890
+ this.messageRecovery.pruneHandled(wireId => wireId === claim.wireId);
891
+ this.options.log(`[${this.options.role}] skipped journaled owner message ${claim.wireId} `
892
+ + `(seq=${claim.seq}): missing from persistent history`);
893
+ continue;
894
+ }
888
895
  recovered.push(this.historyMessage(item, claim));
889
896
  }
890
897
  const listed = await this.client.listIncomingMessages();
@@ -47,6 +47,11 @@ function isKnownOwnerRoomState(message) {
47
47
  ].some(pattern => pattern.test(message));
48
48
  }
49
49
  function ownerTaskFailure(error) {
50
+ if (error instanceof TaskRoomApplicationError && error.code === 'task_not_ready')
51
+ return {
52
+ kind: 'state', detail: error.message,
53
+ action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery.',
54
+ };
50
55
  if (error instanceof TaskListError)
51
56
  return {
52
57
  kind: error.code === 'list_not_found' ? 'not_found'
@@ -75,6 +75,11 @@ async function presentDetachedProvisioningOutcome(outcome) {
75
75
  function taskRoomPublicFailure(error) {
76
76
  const f = error.fields;
77
77
  switch (error.code) {
78
+ case 'task_not_ready': return {
79
+ legacy: `task readiness ${f.readiness}: ${f.reason}`, kind: 'state',
80
+ detail: `Task ${f.task} readiness is ${f.readiness}: ${f.reason}${f.member ? ` (${f.member})` : ''}.`,
81
+ action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery. Do not blindly relaunch agents or reuse invites.',
82
+ };
78
83
  case 'task_confirmation_mismatch': return {
79
84
  legacy: 'confirmation ID must match task ID', kind: 'usage',
80
85
  detail: 'The two task IDs must match.', action: 'Repeat the same task ID twice.',
@@ -0,0 +1,18 @@
1
+ import { readRoomReadiness } from '../agent-ours/service.js';
2
+ import { controlRequest } from '../session/control.js';
3
+ import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
4
+ import type { CoworkAdapter } from './cowork-adapter.js';
5
+ import type { RoomOrchestrationRecord, TaskRecord } from './types.js';
6
+ export type TaskReadinessIssue = {
7
+ state: 'degraded' | 'unknown';
8
+ reason: string;
9
+ member?: string;
10
+ };
11
+ export interface LiveReadinessDeps {
12
+ supervisor?: typeof readTempSupervisor;
13
+ liveness?: typeof tempSupervisorLiveness;
14
+ readiness?: typeof readRoomReadiness;
15
+ control?: typeof controlRequest;
16
+ }
17
+ /** Read-only observation. A failed probe never grants authority to replace a seat. */
18
+ export declare function taskLiveReadiness(task: TaskRecord, room: RoomOrchestrationRecord | undefined, cowork: Pick<CoworkAdapter, 'getRoom'>, deps?: LiveReadinessDeps): Promise<TaskReadinessIssue | undefined>;
@@ -0,0 +1,75 @@
1
+ import { realExec } from '../exec.js';
2
+ import { agentDir } from '../paths.js';
3
+ import { readRoomReadiness } from '../agent-ours/service.js';
4
+ import { controlRequest } from '../session/control.js';
5
+ import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
6
+ /** Read-only observation. A failed probe never grants authority to replace a seat. */
7
+ export async function taskLiveReadiness(task, room, cowork, deps = {}) {
8
+ const degraded = (reason, member) => ({ state: 'degraded', reason, member });
9
+ const unknown = (reason, member) => ({ state: 'unknown', reason, member });
10
+ if (!room || task.state !== 'active' || task.room_id !== room.room_id || room.state !== 'active' || room.close || task.terminal_intent
11
+ || room.task_id !== task.task_id || !room.room_identity_cid
12
+ || task.room_identity_cid !== room.room_identity_cid)
13
+ return degraded('room_record_mismatch');
14
+ const expected = room.template_snapshot?.members.reduce((n, member) => n + member.count, 0);
15
+ if (expected === undefined || room.member_seats.length !== expected
16
+ || new Set(room.member_seats.map(seat => seat.role_name)).size !== expected)
17
+ return degraded('member_roster_mismatch');
18
+ let remote;
19
+ try {
20
+ remote = await cowork.getRoom(room.room_id);
21
+ }
22
+ catch {
23
+ return unknown('room_probe_unavailable');
24
+ }
25
+ if (!remote || remote.room_id !== room.room_id || remote.identity_cid !== room.room_identity_cid
26
+ || remote.state !== 'active')
27
+ return degraded('room_identity_or_state_mismatch');
28
+ if (room.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === room.owner_seat_cid && seat.seat_state === 'active'))
29
+ return degraded('owner_seat_missing');
30
+ for (const seat of room.member_seats) {
31
+ const member = seat.role_name;
32
+ if (seat.seat_state !== 'active' || !seat.identity_cid || !seat.invite_id
33
+ || seat.launch?.state !== 'launched' || !seat.launch.launch_id || seat.retirement)
34
+ return degraded('member_record_incomplete', member);
35
+ const matches = remote.seats.filter(item => item.display_name === member && item.seat_state !== 'removed');
36
+ if (matches.length !== 1 || matches[0].identity_cid !== seat.identity_cid
37
+ || matches[0].invite_id !== seat.invite_id || matches[0].role !== seat.cowork_role
38
+ || matches[0].seat_state !== 'active')
39
+ return degraded('member_seat_mismatch', member);
40
+ const dir = agentDir(member, true);
41
+ try {
42
+ const supervisor = (deps.supervisor ?? readTempSupervisor)(dir);
43
+ if (!supervisor)
44
+ return unknown('supervisor_record_missing', member);
45
+ if (supervisor.role !== member || supervisor.launchId !== seat.launch.launch_id)
46
+ return degraded('supervisor_launch_mismatch', member);
47
+ const live = await (deps.liveness ?? tempSupervisorLiveness)(dir, {
48
+ exec: (cmd, args, opts) => realExec(cmd, args, { ...opts, timeout: 2_000 }),
49
+ });
50
+ if (live === 'stopped')
51
+ return degraded('supervisor_stopped', member);
52
+ if (live !== 'running')
53
+ return unknown('supervisor_liveness_unknown', member);
54
+ const ready = (deps.readiness ?? readRoomReadiness)(room.room_identity_cid, member);
55
+ if (!ready || ready.room !== room.room_id || ready.invite !== seat.invite_id || ready.cid !== seat.identity_cid)
56
+ return degraded('member_readiness_mismatch', member);
57
+ }
58
+ catch {
59
+ return unknown('supervisor_probe_unavailable', member);
60
+ }
61
+ try {
62
+ const response = await (deps.control ?? controlRequest)(dir, { command: 'status' }, 2_000);
63
+ if (!response.ok)
64
+ return unknown('control_unavailable', member);
65
+ const status = response.result;
66
+ if (status?.alive === false || status?.readiness === 'failed')
67
+ return degraded('agent_not_ready', member);
68
+ if (status?.alive !== true || !['idle', 'running', 'awaiting_permission'].includes(status.readiness ?? ''))
69
+ return unknown('agent_readiness_unknown', member);
70
+ }
71
+ catch {
72
+ return unknown('control_unavailable', member);
73
+ }
74
+ }
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.10",
3
+ "version": "1.2.0-nightly.12",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",