@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.
- package/dist/build-info.json +4 -4
- package/dist/rooms-tasks/archived-absence.d.ts +4 -0
- package/dist/rooms-tasks/archived-absence.js +37 -0
- package/dist/rooms-tasks/close.d.ts +1 -0
- package/dist/rooms-tasks/close.js +23 -2
- package/dist/rooms-tasks/deletion.js +28 -2
- package/dist/rooms-tasks/task-state.d.ts +9 -3
- package/dist/rooms-tasks/task-state.js +18 -4
- package/dist/rooms-tasks/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.8",
|
|
3
|
+
"buildId": "d7d0675388e4",
|
|
4
|
+
"commit": "cd6d7ee4e2a7afcb77c4a7aaba09c82c8ec162d7",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-23T20:
|
|
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
|
-
|
|
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
|
|
150
|
-
*
|
|
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
|
|
692
|
-
*
|
|
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.
|
|
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",
|