@ours.network/fleet 1.2.0-nightly.7 → 1.2.0-nightly.8

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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.7",
3
- "buildId": "f5cfd3c1f10c",
4
- "commit": "a5d6f736733fa419e6acf7306affb4a662261bbf",
2
+ "version": "1.2.0-nightly.8",
3
+ "buildId": "d7d0675388e4",
4
+ "commit": "cd6d7ee4e2a7afcb77c4a7aaba09c82c8ec162d7",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-23T20:14:37.547Z",
6
+ "builtAt": "2026-09-23T20:30:52.283Z",
7
7
  "capabilities": [
8
8
  "cowork.http-management-v1",
9
9
  "monitor.interrupt.after_tool"
@@ -0,0 +1,4 @@
1
+ import type { ArchivedMemberAbsence, RoomMemberSeat } from './types.js';
2
+ /** Revalidate live facts; a persisted timestamp is never cleanup authority. */
3
+ export declare function verifyArchivedAbsence(proof: ArchivedMemberAbsence): Promise<void>;
4
+ export declare function proveArchivedAbsence(seat: RoomMemberSeat): Promise<ArchivedMemberAbsence>;
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { agentDir } from '../paths.js';
4
+ import { tempArchiveForLaunch, tempArchiveForCreationAction, tempSupervisorLiveness } from '../temp-lifecycle.js';
5
+ import { assertMemberIdentityAbsent } from './close.js';
6
+ /** Revalidate live facts; a persisted timestamp is never cleanup authority. */
7
+ export async function verifyArchivedAbsence(proof) {
8
+ const absent = () => {
9
+ if (existsSync(agentDir(proof.name, true)))
10
+ throw new Error('Archived member has replacement live state');
11
+ };
12
+ absent();
13
+ const archive = tempArchiveForLaunch(proof.name, proof.launch_id);
14
+ const action = tempArchiveForCreationAction(proof.name, proof.action_id);
15
+ if (!archive || archive !== proof.archive_path || action?.path !== archive
16
+ || action.launchId !== proof.launch_id
17
+ || readFileSync(join(archive, '.identity'), 'utf8').trim() !== proof.name)
18
+ throw new Error('Archived member absence ownership proof mismatch');
19
+ if (await tempSupervisorLiveness(archive) !== 'stopped')
20
+ throw new Error('Archived member supervisor absence is not proven');
21
+ await assertMemberIdentityAbsent({
22
+ role_name: proof.name, slot: 'archived', cowork_role: 'archived', seat_state: 'removed',
23
+ });
24
+ absent();
25
+ }
26
+ export async function proveArchivedAbsence(seat) {
27
+ if (seat.identity_cid || seat.retirement?.phase !== 'identity_absent'
28
+ || !seat.launch?.launch_id || !seat.launch.action_id
29
+ || seat.launch.launch_id !== seat.retirement.launch_id || !seat.retirement.archive_path)
30
+ throw new Error('Missing exact archived member absence evidence');
31
+ const proof = {
32
+ name: seat.role_name, launch_id: seat.launch.launch_id, action_id: seat.launch.action_id,
33
+ archive_path: seat.retirement.archive_path, checked_at: new Date().toISOString(),
34
+ };
35
+ await verifyArchivedAbsence(proof);
36
+ return proof;
37
+ }
@@ -20,6 +20,7 @@ export declare function waitForLivenessAbsent(role: string, launchId: string, li
20
20
  /** Report whether any daemon identity — under any name — carries this exact CID. */
21
21
  export declare function identityCidPresent(cid: string): Promise<boolean>;
22
22
  export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
23
+ export declare function assertMemberIdentityAbsent(seat: RoomMemberSeat): Promise<void>;
23
24
  /** One forward-only room close saga shared by every Fleet entry point. */
24
25
  export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
25
26
  export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
@@ -5,7 +5,7 @@ import { attachOursClient } from '@ours.network/sdk/client';
5
5
  import { withFileLock } from '../atomic-file.js';
6
6
  import { agentDir, stateRoot } from '../paths.js';
7
7
  import { readClientProfile } from '../client-profile.js';
8
- import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
8
+ import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
9
9
  import { CoworkProtocolError } from './cowork-adapter.js';
10
10
  import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
11
11
  const CLOSE_LOCK_STALE_MS = 5 * 60_000;
@@ -119,7 +119,7 @@ export async function removeExactMemberIdentity(seat) {
119
119
  }
120
120
  });
121
121
  }
122
- async function assertMemberIdentityAbsent(seat) {
122
+ export async function assertMemberIdentityAbsent(seat) {
123
123
  await withIdentityClient(async (client) => {
124
124
  const rows = await client.listIdentities();
125
125
  if (rows.some(row => row.name === seat.role_name ||
@@ -141,6 +141,27 @@ async function retireMember(roomId, seat, deps) {
141
141
  return;
142
142
  }
143
143
  if (!retirement) {
144
+ if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
145
+ const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
146
+ const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
147
+ if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
148
+ if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
149
+ throw new Error(`room member '${current.role_name}' archive identity mismatch`);
150
+ if (await tempSupervisorLiveness(archived) !== 'stopped')
151
+ throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
152
+ // A terminated launch can archive itself before room retirement begins.
153
+ // Accept its exact durable provenance only when no identity needs removal.
154
+ await assertMemberIdentityAbsent(current);
155
+ const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
156
+ if (existsSync(agentDir(current.role_name, true))
157
+ || latest?.launch?.launch_id !== current.launch.launch_id
158
+ || latest?.launch?.action_id !== current.launch.action_id
159
+ || latest?.identity_cid !== current.identity_cid)
160
+ throw new Error(`room member '${current.role_name}' changed during archived retirement proof`);
161
+ advanceMemberRetirement(roomId, current.role_name, 'identity_absent', current.launch.launch_id, archived);
162
+ return;
163
+ }
164
+ }
144
165
  if (current.launch?.state === 'failed' && !existsSync(agentDir(current.role_name, true))) {
145
166
  // A failure before applyRole (for example invite-secret validation) has
146
167
  // no supervisor to stop or archive. Settle only proven absence; this
@@ -1,3 +1,4 @@
1
+ import { proveArchivedAbsence, verifyArchivedAbsence } from './archived-absence.js';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { withFileLock } from '../atomic-file.js';
3
4
  import { agentDir } from '../paths.js';
@@ -145,10 +146,30 @@ export async function settleTaskDeletion(input) {
145
146
  upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
146
147
  await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
147
148
  const closed = getRoomRecord(record.room_id);
148
- importTaskDeletionRetirementEvidence(taskId, closed?.member_seats ?? record.member_seats);
149
+ const seats = closed?.member_seats ?? record.member_seats;
150
+ const proofs = [];
151
+ for (const seat of seats) {
152
+ if (!seat.identity_cid && seat.retirement?.launch_id !== 'never-launched')
153
+ proofs.push(await proveArchivedAbsence(seat));
154
+ }
155
+ // No await between the last live-state fence and durable checkpoint.
156
+ for (const proof of proofs) {
157
+ const latest = getRoomRecord(record.room_id)?.member_seats.find(seat => seat.role_name === proof.name);
158
+ if (existsSync(agentDir(proof.name, true)) || latest?.identity_cid
159
+ || latest?.launch?.launch_id !== proof.launch_id || latest.launch.action_id !== proof.action_id)
160
+ throw new Error('Archived absence seat changed before checkpoint');
161
+ }
162
+ importTaskDeletionRetirementEvidence(taskId, seats, proofs);
149
163
  await cowork.deleteRoom(record.room_id);
164
+ for (const proof of proofs)
165
+ await verifyArchivedAbsence(proof);
166
+ for (const proof of proofs)
167
+ if (existsSync(agentDir(proof.name, true)))
168
+ throw new Error('Archived member replacement before room unlink');
150
169
  deleteRoomRecord(record.room_id);
151
170
  }
171
+ for (const proof of getDeletingTask(taskId).deletion.archived_absences ?? [])
172
+ await verifyArchivedAbsence(proof);
152
173
  // Members whose room record is gone (crash after record deletion, or
153
174
  // legacy state): resume from the durable cursors.
154
175
  for (const cursor of getDeletingTask(taskId).deletion.members) {
@@ -176,7 +197,7 @@ export async function settleTaskDeletion(input) {
176
197
  completeTaskDeletionReceipt(taskId);
177
198
  return { task_id: taskId, deleted: false };
178
199
  }
179
- const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
200
+ const finalize = () => withFileLock(taskOperationLockPath(taskId), async () => {
180
201
  try {
181
202
  const task = getDeletingTask(taskId);
182
203
  if (task.deletion?.status !== 'pending')
@@ -185,6 +206,11 @@ export async function settleTaskDeletion(input) {
185
206
  throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
186
207
  if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
187
208
  throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
209
+ for (const proof of task.deletion.archived_absences ?? [])
210
+ await verifyArchivedAbsence(proof);
211
+ for (const proof of task.deletion.archived_absences ?? [])
212
+ if (existsSync(agentDir(proof.name, true)))
213
+ throw new Error('Archived member replacement before task unlink');
188
214
  if (cleanup.snapshotHash)
189
215
  releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
190
216
  unlinkDeletedTask(taskId);
@@ -1,3 +1,4 @@
1
+ import type { ArchivedMemberAbsence } from './types.js';
1
2
  import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase } from './types.js';
2
3
  export declare const tasksDir: () => string;
3
4
  export declare class TaskStateError extends Error {
@@ -74,6 +75,7 @@ export interface TaskDeletionReceipt {
74
75
  original_state: TaskState;
75
76
  room_id?: string;
76
77
  member_count: number;
78
+ archived_absences?: ArchivedMemberAbsence[];
77
79
  settled_at?: string;
78
80
  result?: 'deleted';
79
81
  }
@@ -122,6 +124,10 @@ export declare function advanceTaskDeletionMember(id: string, name: string, phas
122
124
  interface SeatEvidence {
123
125
  role_name: string;
124
126
  identity_cid?: string;
127
+ launch?: {
128
+ launch_id?: string;
129
+ action_id?: string;
130
+ };
125
131
  retirement?: {
126
132
  phase: TaskDeletionMemberPhase;
127
133
  launch_id: string;
@@ -146,10 +152,10 @@ export declare function upsertTaskDeletionMembersFromSeats(id: string, seats: Re
146
152
  *
147
153
  * The room saga is trusted for phase jumps, but partial or corrupt retained
148
154
  * records must not become false success: every seat must be identity_absent;
149
- * a real launch requires archive evidence; an identity-less seat is accepted
150
- * only via the never-launched proof.
155
+ * a real launch requires archive evidence; an identity-less seat requires
156
+ * never-launched proof or a freshly verified archived-absence checkpoint.
151
157
  */
152
- export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>): TaskRecord;
158
+ export declare function importTaskDeletionRetirementEvidence(id: string, seats: ReadonlyArray<SeatEvidence>, proofs?: ReadonlyArray<ArchivedMemberAbsence>): TaskRecord;
153
159
  /**
154
160
  * Physically remove a deletion-pending task record. Settlement-only: callers
155
161
  * must have completed member retirement and room cleanup first. Missing
@@ -505,7 +505,8 @@ function writeDeletionReceiptForIntent(stored) {
505
505
  actor: deletion.actor,
506
506
  original_state: stored.state,
507
507
  room_id: deletion.room_id,
508
- member_count: deletion.members.length,
508
+ member_count: deletion.members.length + (deletion.archived_absences?.length ?? 0),
509
+ ...(deletion.archived_absences?.length ? { archived_absences: deletion.archived_absences } : {}),
509
510
  };
510
511
  replaceFileAtomically(deletionReceiptPath(stored.task_id), JSON.stringify(receipt, null, 2) + '\n');
511
512
  }
@@ -688,10 +689,10 @@ export function upsertTaskDeletionMembersFromSeats(id, seats) {
688
689
  *
689
690
  * The room saga is trusted for phase jumps, but partial or corrupt retained
690
691
  * records must not become false success: every seat must be identity_absent;
691
- * a real launch requires archive evidence; an identity-less seat is accepted
692
- * only via the never-launched proof.
692
+ * a real launch requires archive evidence; an identity-less seat requires
693
+ * never-launched proof or a freshly verified archived-absence checkpoint.
693
694
  */
694
- export function importTaskDeletionRetirementEvidence(id, seats) {
695
+ export function importTaskDeletionRetirementEvidence(id, seats, proofs = []) {
695
696
  return withTaskLock(id, () => {
696
697
  assertCanonicalTaskId(id);
697
698
  const stored = JSON.parse(readFileSync(taskPath(id), 'utf8'));
@@ -708,6 +709,18 @@ export function importTaskDeletionRetirementEvidence(id, seats) {
708
709
  if (!seat.identity_cid) {
709
710
  if (neverLaunched)
710
711
  continue; // provably never held a managed identity
712
+ const proof = proofs.find(value => value.name === seat.role_name);
713
+ if (proof && proof.launch_id === evidence.launch_id && proof.archive_path === evidence.archive_path
714
+ && proof.launch_id === seat.launch?.launch_id && proof.action_id === seat.launch?.action_id) {
715
+ const existing = stored.deletion.archived_absences?.find(value => value.name === proof.name);
716
+ if (existing && (existing.launch_id !== proof.launch_id || existing.action_id !== proof.action_id
717
+ || existing.archive_path !== proof.archive_path))
718
+ throw new TaskStateError('Archived absence checkpoint ownership is immutable');
719
+ stored.deletion.archived_absences ??= [];
720
+ if (!existing)
721
+ stored.deletion.archived_absences.push({ ...proof });
722
+ continue;
723
+ }
711
724
  throw new TaskStateError(`task ${id} deletion member '${seat.role_name}' has retirement evidence but no identity CID`);
712
725
  }
713
726
  let cursor = findCursorForSeat(id, stored, seat);
@@ -729,6 +742,7 @@ export function importTaskDeletionRetirementEvidence(id, seats) {
729
742
  cursor.updated_at = now;
730
743
  }
731
744
  writeTask(stored);
745
+ writeDeletionReceiptForIntent(stored);
732
746
  return presentTaskLenient(stored);
733
747
  });
734
748
  }
@@ -73,6 +73,13 @@ export interface TaskDeletionMemberCursor {
73
73
  * evidence. While pending, the task is hidden from normal operation and every
74
74
  * lifecycle mutation or room publication is rejected.
75
75
  */
76
+ export interface ArchivedMemberAbsence {
77
+ name: string;
78
+ launch_id: string;
79
+ action_id: string;
80
+ archive_path: string;
81
+ checked_at: string;
82
+ }
76
83
  export interface TaskDeletionIntent {
77
84
  status: 'pending';
78
85
  accepted_at: string;
@@ -80,6 +87,7 @@ export interface TaskDeletionIntent {
80
87
  room_id?: string;
81
88
  /** Snapshot of managed members at acceptance; the missing-room retirement evidence. */
82
89
  members: TaskDeletionMemberCursor[];
90
+ archived_absences?: ArchivedMemberAbsence[];
83
91
  error?: string;
84
92
  error_at?: string;
85
93
  recovery_hint?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.7",
3
+ "version": "1.2.0-nightly.8",
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",