@ours.network/fleet 1.2.0-nightly.53 → 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.
- package/README.md +22 -12
- package/dist/agent-ours/bridge.js +10 -3
- package/dist/agent-ours/file-rpc.d.ts +8 -0
- package/dist/agent-ours/file-rpc.js +11 -1
- package/dist/agent-ours/harness.js +47 -3
- package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
- package/dist/agent-ours/mcp-endpoint.js +4 -0
- package/dist/agent-ours/service.d.ts +6 -1
- package/dist/agent-ours/service.js +73 -8
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/supervisor-ours-tools.js +3 -1
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +11 -0
- package/dist/config.js +13 -2
- package/dist/file-delivery/reader.d.ts +17 -0
- package/dist/file-delivery/reader.js +94 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-app-server-proxy.js +6 -2
- package/dist/harness/codex-file-inventory.d.ts +15 -0
- package/dist/harness/codex-file-inventory.js +113 -0
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +2 -1
- package/dist/isolation/policy.js +4 -1
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +161 -22
- package/dist/session/acp.d.ts +5 -0
- package/dist/session/acp.js +68 -8
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +3 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +7 -3
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { randomUUID
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { loadConfig, loadRoomsConfig, splitRootFor } from '../config.js';
|
|
6
6
|
import { agentDir, stateRoot, defaultConfigPath } from '../paths.js';
|
|
7
7
|
import { controlRequest } from '../session/control.js';
|
|
8
8
|
import { spawnTemp } from '../spawn.js';
|
|
9
|
-
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
10
9
|
import { layoutSupervisorId } from './layout-control.js';
|
|
11
10
|
import { RoomLayout } from './layout.js';
|
|
12
11
|
import { assertLayoutFile, validLayoutKey, readRoomLayout, readRoomLayouts } from './layout-config.js';
|
|
13
12
|
import { createCoworkAdapter } from './cowork-adapter.js';
|
|
14
13
|
import { callRemoteLayoutBinding } from './layout-binding-client.js';
|
|
15
14
|
import { readClientProfile } from '../client-profile.js';
|
|
15
|
+
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
16
|
+
import { layoutOwnedMemberName, retireOwnedLayoutMember } from './layout-member-retirement.js';
|
|
16
17
|
export class NativeLayoutSupervisor {
|
|
17
18
|
configPath;
|
|
18
19
|
runId;
|
|
19
20
|
templates;
|
|
21
|
+
taskSupervised;
|
|
20
22
|
id;
|
|
21
23
|
constructor(configPath, runId, templates) {
|
|
22
24
|
this.configPath = configPath;
|
|
@@ -24,6 +26,7 @@ export class NativeLayoutSupervisor {
|
|
|
24
26
|
this.templates = templates;
|
|
25
27
|
mkdirSync(stateRoot(), { recursive: true, mode: 0o700 });
|
|
26
28
|
this.id = layoutSupervisorId();
|
|
29
|
+
this.taskSupervised = runId.startsWith('task-') && process.env.OURS_FLEET_SUPERVISOR !== 'none';
|
|
27
30
|
}
|
|
28
31
|
async request(agent, temporary, request) {
|
|
29
32
|
if (!/^[A-Za-z0-9_-]+$/.test(agent))
|
|
@@ -48,8 +51,14 @@ export class NativeLayoutSupervisor {
|
|
|
48
51
|
const definition = this.templates[template];
|
|
49
52
|
if (!definition)
|
|
50
53
|
throw Error(`agent template not found in run snapshot: ${template}`);
|
|
51
|
-
const name =
|
|
54
|
+
const name = layoutOwnedMemberName(this.runId, key);
|
|
55
|
+
const taskId = this.runId.startsWith('task-') ? this.runId.slice(5) : undefined;
|
|
56
|
+
const task = taskId ? (await import('./task-state.js')).getTask(taskId) : undefined;
|
|
57
|
+
if (task && task.layout?.run_id !== this.runId)
|
|
58
|
+
throw Error('TASK_LAYOUT_OWNER_MISMATCH');
|
|
52
59
|
await spawnTemp({ name, temp: true, configPath: this.configPath, agentDefinition: definition,
|
|
60
|
+
...(task ? { taskOwner: { taskId: task.task_id, layout: { runId: this.runId, participant: key },
|
|
61
|
+
creationActionId: `${this.runId}:${key}` } } : {}),
|
|
53
62
|
creationActionId: `${this.runId}:${key}`, surface: 'cli' }, fileURLToPath(new URL('../cli.js', import.meta.url)));
|
|
54
63
|
const deadline = Date.now() + 120_000;
|
|
55
64
|
let last;
|
|
@@ -90,17 +99,26 @@ export class NativeLayoutSupervisor {
|
|
|
90
99
|
throw Error('cannot retire a borrowed cross-Fleet instance');
|
|
91
100
|
if (i.supervisor !== this.id || !i.agent || !/^[A-Za-z0-9_-]+$/.test(i.agent) || i.temporary !== true)
|
|
92
101
|
throw Error('invalid local temporary layout instance');
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
if (!this.runId.startsWith('task-')) {
|
|
103
|
+
const dir = agentDir(i.agent, true);
|
|
104
|
+
const stopped = async () => !existsSync(dir) || await tempSupervisorLiveness(dir) === 'stopped';
|
|
105
|
+
if (await stopped())
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
await this.call(i, { action: 'retire' });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (!await stopped())
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
96
114
|
return;
|
|
97
|
-
try {
|
|
98
|
-
await this.call(i, { action: 'retire' });
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (!await stopped())
|
|
102
|
-
throw error;
|
|
103
115
|
}
|
|
116
|
+
const snapshot = JSON.parse(readFileSync(layoutRunPath(this.runId), 'utf8'));
|
|
117
|
+
const owned = Object.entries(snapshot.participants).filter(([, participant]) => participant.owned
|
|
118
|
+
&& participant.instance?.agent === i.agent && JSON.stringify(participant.instance) === JSON.stringify(i));
|
|
119
|
+
if (owned.length !== 1)
|
|
120
|
+
throw Error('LAYOUT_MEMBER_NOT_OWNED');
|
|
121
|
+
await retireOwnedLayoutMember(i, this.runId, owned[0][0], {}, owned[0][1].task_supervised);
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
export const layoutRunPath = (id) => {
|
|
@@ -49,6 +49,7 @@ export interface LayoutAssignment {
|
|
|
49
49
|
contract?: string;
|
|
50
50
|
}
|
|
51
51
|
export interface LayoutSupervisor {
|
|
52
|
+
readonly taskSupervised?: boolean;
|
|
52
53
|
id: string;
|
|
53
54
|
/** Must verify current launch, CID, live session and standalone ownership. */
|
|
54
55
|
verify(instance: LayoutInstance): Promise<void>;
|
|
@@ -67,6 +68,7 @@ type ParticipantState = {
|
|
|
67
68
|
retired?: boolean;
|
|
68
69
|
participant?: string;
|
|
69
70
|
room?: string;
|
|
71
|
+
task_supervised?: boolean;
|
|
70
72
|
};
|
|
71
73
|
type RoomState = {
|
|
72
74
|
spec: LayoutRoom;
|
|
@@ -74,6 +76,7 @@ type RoomState = {
|
|
|
74
76
|
ready: string[];
|
|
75
77
|
state: 'provisioning' | 'active' | 'closed';
|
|
76
78
|
owner_ready?: boolean;
|
|
79
|
+
deleted?: boolean;
|
|
77
80
|
};
|
|
78
81
|
export interface RoomLayoutState {
|
|
79
82
|
version: 1;
|
|
@@ -110,5 +113,9 @@ export declare class RoomLayout {
|
|
|
110
113
|
private archiveRoom;
|
|
111
114
|
closeRoom(actor: string, key: string): Promise<void>;
|
|
112
115
|
close(actor: string): Promise<void>;
|
|
116
|
+
/** Explicit deletion retains a per-room checkpoint until the run is erased.
|
|
117
|
+
* Finish/cancel continue to use close(), preserving archived rooms.
|
|
118
|
+
*/
|
|
119
|
+
delete(actor: string): Promise<void>;
|
|
113
120
|
}
|
|
114
121
|
export {};
|
|
@@ -6,6 +6,7 @@ import { createHash } from 'node:crypto';
|
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { canonicalJson } from '../canonical-json.js';
|
|
8
8
|
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
9
|
+
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
9
10
|
const ownerReference = (owner) => ({ cid: owner.cid, role: owner.role, fingerprint: createHash('sha256').update(owner.invite).digest('hex') });
|
|
10
11
|
const own = (o, key) => Object.hasOwn(o, key);
|
|
11
12
|
function need(ok, message) { if (!ok)
|
|
@@ -133,6 +134,8 @@ export class RoomLayout {
|
|
|
133
134
|
if (!p.instance) {
|
|
134
135
|
const template = s.definition.participants[member]?.agent_template;
|
|
135
136
|
need(template, `no factory for ${member}`);
|
|
137
|
+
if (this.supervisor.taskSupervised)
|
|
138
|
+
p.task_supervised = true;
|
|
136
139
|
await this.mutation(s, `spawn:${resolvedKey}`, () => this.supervisor.spawn(resolvedKey, template), i => { p.instance = i; });
|
|
137
140
|
await this.verify(p.instance);
|
|
138
141
|
}
|
|
@@ -247,4 +250,36 @@ export class RoomLayout {
|
|
|
247
250
|
this.save(s);
|
|
248
251
|
}, true);
|
|
249
252
|
}
|
|
253
|
+
/** Explicit deletion retains a per-room checkpoint until the run is erased.
|
|
254
|
+
* Finish/cancel continue to use close(), preserving archived rooms.
|
|
255
|
+
*/
|
|
256
|
+
async delete(actor) {
|
|
257
|
+
await this.close(actor);
|
|
258
|
+
await this.operation(actor, async (state) => {
|
|
259
|
+
for (const room of Object.values(state.rooms)) {
|
|
260
|
+
if (!room.native || room.deleted)
|
|
261
|
+
continue;
|
|
262
|
+
let remote;
|
|
263
|
+
try {
|
|
264
|
+
remote = await this.cowork.getRoom(room.native.room_id);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
if (remote) {
|
|
271
|
+
need(remote.identity_cid === room.native.identity_cid, 'native room identity changed during deletion');
|
|
272
|
+
try {
|
|
273
|
+
await this.cowork.deleteRoom(room.native.room_id);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
room.deleted = true;
|
|
281
|
+
this.save(state);
|
|
282
|
+
}
|
|
283
|
+
}, true);
|
|
284
|
+
}
|
|
250
285
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
2
|
+
export interface LegacyTaskOwner {
|
|
3
|
+
taskId: string;
|
|
4
|
+
roomId: string;
|
|
5
|
+
roomIdentityCid: string;
|
|
6
|
+
creationActionId: string;
|
|
7
|
+
}
|
|
8
|
+
interface AdoptionDeps {
|
|
9
|
+
liveness?: typeof tempSupervisorLiveness;
|
|
10
|
+
verifyIdentity?(name: string, cid: string): Promise<void>;
|
|
11
|
+
/** Crash seam: the role snapshot is durable before the supervisor owner. */
|
|
12
|
+
beforeOwnerCommit?(): void;
|
|
13
|
+
}
|
|
14
|
+
/** Caller holds task and room operation locks. No service is started here.
|
|
15
|
+
* Running legacy supervisors remain untouched; stopped exact state receives
|
|
16
|
+
* only ownership metadata, preserving launch, identity and conversation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function adoptLegacyTaskMember(owner: LegacyTaskOwner, name: string, deps?: AdoptionDeps): Promise<'adopted' | 'running-legacy' | 'already-durable'>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { parse, stringify } from 'yaml';
|
|
5
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
6
|
+
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
7
|
+
import { binderKey } from '../agent-ours/state.js';
|
|
8
|
+
import { readClientProfile } from '../client-profile.js';
|
|
9
|
+
import { readProvenance } from '../creation.js';
|
|
10
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
11
|
+
import { readTempSupervisor, tempSupervisorLiveness, TEMP_SUPERVISOR_FILE } from '../temp-lifecycle.js';
|
|
12
|
+
import { getTask } from './task-state.js';
|
|
13
|
+
import { getRoomRecord, updateMemberStartup } from './room-state.js';
|
|
14
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
15
|
+
function bounded(path) {
|
|
16
|
+
const stat = lstatSync(path);
|
|
17
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
18
|
+
throw Error('LEGACY_TASK_MEMBER_UNSAFE_PROOF');
|
|
19
|
+
return readFileSync(path, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
async function verifyIdentity(name, cid, env) {
|
|
22
|
+
const profile = readClientProfile(env);
|
|
23
|
+
const client = await attachOursClient({ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
|
|
24
|
+
credentialPath: profile.credentialPath, sessionMode: 'external', env: {}, leaseToken: `task-migration-${randomUUID()}` });
|
|
25
|
+
try {
|
|
26
|
+
const identities = await client.listIdentities();
|
|
27
|
+
const found = identities.find(value => value.name === name);
|
|
28
|
+
if (!found || !('cid' in found) || found.cid.toLowerCase() !== cid.toLowerCase())
|
|
29
|
+
throw Error('LEGACY_TASK_MEMBER_IDENTITY_MISMATCH');
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
try {
|
|
33
|
+
await client.releaseLease();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await client.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Caller holds task and room operation locks. No service is started here.
|
|
41
|
+
* Running legacy supervisors remain untouched; stopped exact state receives
|
|
42
|
+
* only ownership metadata, preserving launch, identity and conversation.
|
|
43
|
+
*/
|
|
44
|
+
export async function adoptLegacyTaskMember(owner, name, deps = {}) {
|
|
45
|
+
return withFileLock(join(stateRoot(), 'locks', 'temp-supervisors', encodeURIComponent(name)), async () => {
|
|
46
|
+
assertMemberNotPermanent(name);
|
|
47
|
+
const dir = agentDir(name, true), task = getTask(owner.taskId), room = getRoomRecord(owner.roomId);
|
|
48
|
+
const seat = room?.member_seats.find(value => value.role_name === name);
|
|
49
|
+
const supervisor = readTempSupervisor(dir);
|
|
50
|
+
const rolePath = join(dir, 'role.yaml'), roleBytes = bounded(rolePath);
|
|
51
|
+
const role = parse(roleBytes);
|
|
52
|
+
const startup = role?.roomMemberStartup, provenance = readProvenance(dir);
|
|
53
|
+
if (task.deletion || task.terminal_intent || !['provisioning', 'active', 'review'].includes(task.state)
|
|
54
|
+
|| !room || room.close || !['provisioning', 'active'].includes(room.state) || room.task_id !== owner.taskId
|
|
55
|
+
|| task.room_id !== owner.roomId || room.room_identity_cid !== owner.roomIdentityCid
|
|
56
|
+
|| task.room_identity_cid !== owner.roomIdentityCid || !seat || seat.retirement
|
|
57
|
+
|| !supervisor || supervisor.role !== name || seat.launch?.launch_id !== supervisor.launchId
|
|
58
|
+
|| seat.launch.action_id !== owner.creationActionId || provenance?.creationActionId !== owner.creationActionId
|
|
59
|
+
|| provenance.role !== name || role.name !== name || role.identity !== name
|
|
60
|
+
|| startup?.identity_name !== name || startup.room_id !== owner.roomId
|
|
61
|
+
|| startup.room_identity_cid !== owner.roomIdentityCid || startup.invite_id !== seat.invite_id
|
|
62
|
+
|| startup.role !== seat.cowork_role || startup.invite !== ''
|
|
63
|
+
|| (startup.task_id !== undefined && startup.task_id !== owner.taskId)
|
|
64
|
+
|| (room.workspace && (role.cwd !== room.workspace.path || JSON.stringify(startup.workspace) !== JSON.stringify(room.workspace)))
|
|
65
|
+
|| bounded(join(dir, '.identity')).trim() !== name)
|
|
66
|
+
throw Error('LEGACY_TASK_MEMBER_OWNERSHIP_MISMATCH');
|
|
67
|
+
if (supervisor.taskOwner) {
|
|
68
|
+
if (Object.entries(owner).some(([key, value]) => supervisor.taskOwner[key] !== value)
|
|
69
|
+
|| startup.task_id !== owner.taskId)
|
|
70
|
+
throw Error('LEGACY_TASK_MEMBER_OWNER_MISMATCH');
|
|
71
|
+
if (supervisor.kind !== 'detached' && process.env.OURS_FLEET_SUPERVISOR !== 'none')
|
|
72
|
+
updateMemberStartup(owner.roomId, name, { launch: { ...seat.launch, task_supervised: true } });
|
|
73
|
+
return 'already-durable';
|
|
74
|
+
}
|
|
75
|
+
const live = await (deps.liveness ?? tempSupervisorLiveness)(dir);
|
|
76
|
+
if (live === 'running')
|
|
77
|
+
return 'running-legacy';
|
|
78
|
+
if (live !== 'stopped')
|
|
79
|
+
throw Error('LEGACY_TASK_MEMBER_LIVENESS_UNKNOWN');
|
|
80
|
+
const env = { ...process.env, ...role.env };
|
|
81
|
+
const profile = readClientProfile(env);
|
|
82
|
+
const privateDir = join(stateRoot(), 'private-ours', binderKey(profile.expectedInstanceId, name));
|
|
83
|
+
const state = JSON.parse(bounded(join(privateDir, 'state.json')));
|
|
84
|
+
const instance = JSON.parse(bounded(join(privateDir, 'instance.json')));
|
|
85
|
+
const externalOwner = JSON.parse(bounded(join(privateDir, 'owner.json')));
|
|
86
|
+
const launch = JSON.parse(bounded(join(stateRoot(), 'private-ours', 'launches', binderKey('temporary', name) + '.json')));
|
|
87
|
+
if (!seat.identity_cid || state.name !== name || state.daemon !== profile.expectedInstanceId
|
|
88
|
+
|| state.lifetime !== 'temporary' || state.action !== owner.creationActionId
|
|
89
|
+
|| state.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()
|
|
90
|
+
|| !['READY', 'SERVING', 'RECOVERING'].includes(state.phase)
|
|
91
|
+
|| instance.role !== name || instance.temporary !== true || instance.instance !== state.instance
|
|
92
|
+
|| externalOwner.instance !== state.instance || typeof externalOwner.token !== 'string' || !externalOwner.token
|
|
93
|
+
|| state.room?.id !== owner.roomId || state.room.cid !== owner.roomIdentityCid
|
|
94
|
+
|| state.room.agentCid.toLowerCase() !== seat.identity_cid.toLowerCase() || state.room.action !== seat.invite_id
|
|
95
|
+
|| launch.role !== name || launch.identity !== name || launch.action !== owner.creationActionId)
|
|
96
|
+
throw Error('LEGACY_TASK_MEMBER_RUNTIME_NOT_RESUMABLE');
|
|
97
|
+
const sessionPath = join(dir, role.session === 'codex-app-server' ? '.session-id' : '.acp-session-id');
|
|
98
|
+
if (!bounded(sessionPath).trim())
|
|
99
|
+
throw Error('LEGACY_TASK_MEMBER_CONVERSATION_MISSING');
|
|
100
|
+
if (deps.verifyIdentity)
|
|
101
|
+
await deps.verifyIdentity(name, seat.identity_cid);
|
|
102
|
+
else
|
|
103
|
+
await verifyIdentity(name, seat.identity_cid, env);
|
|
104
|
+
assertMemberNotPermanent(name);
|
|
105
|
+
if (bounded(rolePath) !== roleBytes || JSON.stringify(readTempSupervisor(dir)) !== JSON.stringify(supervisor))
|
|
106
|
+
throw Error('LEGACY_TASK_MEMBER_STATE_CHANGED');
|
|
107
|
+
// Crash after this write is healed by retrying the same stopped adoption.
|
|
108
|
+
startup.task_id = owner.taskId;
|
|
109
|
+
replaceFileAtomically(rolePath, stringify(role));
|
|
110
|
+
if (supervisor.kind !== 'detached' && process.env.OURS_FLEET_SUPERVISOR !== 'none')
|
|
111
|
+
updateMemberStartup(owner.roomId, name, { launch: { ...seat.launch, task_supervised: true } });
|
|
112
|
+
deps.beforeOwnerCommit?.();
|
|
113
|
+
replaceFileAtomically(join(dir, TEMP_SUPERVISOR_FILE), JSON.stringify({ ...supervisor, taskOwner: owner }, null, 2) + '\n');
|
|
114
|
+
return 'adopted';
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** A room seat is never authority to retire a standalone persistent agent.
|
|
2
|
+
* Check both role-name collisions and a permanent role using the seat's
|
|
3
|
+
* identity name. Missing legacy task state must not bypass this fence.
|
|
4
|
+
*/
|
|
5
|
+
export declare function assertMemberNotPermanent(name: string): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { agentDir, agentsRoot } from '../paths.js';
|
|
4
|
+
/** A room seat is never authority to retire a standalone persistent agent.
|
|
5
|
+
* Check both role-name collisions and a permanent role using the seat's
|
|
6
|
+
* identity name. Missing legacy task state must not bypass this fence.
|
|
7
|
+
*/
|
|
8
|
+
export function assertMemberNotPermanent(name) {
|
|
9
|
+
if (!/^[a-zA-Z0-9_-]{1,120}$/.test(name))
|
|
10
|
+
throw new Error('Invalid retirement member name');
|
|
11
|
+
if (existsSync(agentDir(name)))
|
|
12
|
+
throw new Error(`room member '${name}' conflicts with permanent agent state; refusing retirement`);
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = readdirSync(agentsRoot(), { withFileTypes: true });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT')
|
|
19
|
+
return;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
24
|
+
continue;
|
|
25
|
+
const path = join(agentsRoot(), entry.name, '.identity');
|
|
26
|
+
let stat;
|
|
27
|
+
try {
|
|
28
|
+
stat = lstatSync(path);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
continue;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024)
|
|
36
|
+
throw new Error('Permanent identity ownership proof is unsafe; refusing member retirement');
|
|
37
|
+
if (readFileSync(path, 'utf8').trim() === name)
|
|
38
|
+
throw new Error(`room member '${name}' identity belongs to permanent agent '${entry.name}'; refusing retirement`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -7,7 +7,8 @@ import { parse } from 'yaml';
|
|
|
7
7
|
import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
|
|
8
8
|
import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
9
9
|
import { storedRoomLaunchPolicy } from './types.js';
|
|
10
|
-
import { spawnTemp } from '../spawn.js';
|
|
10
|
+
import { spawnTemp, resumeTaskSupervisor } from '../spawn.js';
|
|
11
|
+
import { adoptLegacyTaskMember } from './legacy-task-member.js';
|
|
11
12
|
import { effectivePermissionMode } from '../permissions.js';
|
|
12
13
|
import { selectionOrigin, summarizeResolvedLaunch, } from '../lifecycle-summary.js';
|
|
13
14
|
import { canonicalJson } from '../canonical-json.js';
|
|
@@ -207,6 +208,22 @@ async function retainRunningLaunch(input) {
|
|
|
207
208
|
} });
|
|
208
209
|
return true;
|
|
209
210
|
}
|
|
211
|
+
if (supervisor.taskOwner) {
|
|
212
|
+
await resumeTaskSupervisor(member.name, provision.binPath);
|
|
213
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
214
|
+
...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
|
|
215
|
+
presentation: retainedLaunch.presentation ?? presentationFromStatePath(dir, settings, member.coworkRole),
|
|
216
|
+
updated_at: new Date().toISOString(),
|
|
217
|
+
} });
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
if (provision.taskId) {
|
|
221
|
+
const adopted = await adoptLegacyTaskMember({ taskId: provision.taskId, roomId: provision.roomId,
|
|
222
|
+
roomIdentityCid, creationActionId: retainedLaunch.action_id }, member.name);
|
|
223
|
+
if (adopted !== 'running-legacy')
|
|
224
|
+
await resumeTaskSupervisor(member.name, provision.binPath);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
210
227
|
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
211
228
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
212
229
|
...retainedLaunch, state: 'stopped', launch_id: supervisor.launchId,
|
|
@@ -215,6 +232,8 @@ async function retainRunningLaunch(input) {
|
|
|
215
232
|
return false;
|
|
216
233
|
}
|
|
217
234
|
if (seat.launch?.state === 'launched' && !existsSync(dir)) {
|
|
235
|
+
if (provision.taskId)
|
|
236
|
+
throw Error(`retained task member ${member.name} is missing; explicit reconciliation required`);
|
|
218
237
|
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
219
238
|
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
220
239
|
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
@@ -249,6 +268,7 @@ async function launchMember(input) {
|
|
|
249
268
|
const actionId = randomUUID();
|
|
250
269
|
let effectiveActionId = actionId;
|
|
251
270
|
const attempt = (seat.launch?.attempt ?? 0) + 1;
|
|
271
|
+
const taskSupervised = Boolean(provision.taskId) && process.env.OURS_FLEET_SUPERVISOR !== 'none';
|
|
252
272
|
const taskSha = sha256Text(startup.task);
|
|
253
273
|
const effectiveAgentDefinition = structuredClone(settings.definition);
|
|
254
274
|
const { projection: agentDefinition, fingerprint: agentFingerprint } = launchDefinition(effectiveAgentDefinition);
|
|
@@ -256,6 +276,7 @@ async function launchMember(input) {
|
|
|
256
276
|
? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
|
|
257
277
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
258
278
|
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
279
|
+
...(taskSupervised ? { task_supervised: true } : {}),
|
|
259
280
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
260
281
|
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
261
282
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
@@ -280,6 +301,7 @@ async function launchMember(input) {
|
|
|
280
301
|
if (launched.creationActionId !== actionId) {
|
|
281
302
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
282
303
|
state: 'intent', attempt, action_id: launched.creationActionId,
|
|
304
|
+
...(taskSupervised ? { task_supervised: true } : {}),
|
|
283
305
|
mission_sha256: taskSha, agent_definition: agentDefinition,
|
|
284
306
|
agent_fingerprint: agentFingerprint,
|
|
285
307
|
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
@@ -297,6 +319,7 @@ async function launchMember(input) {
|
|
|
297
319
|
: presentationFromStatePath(launchedDir, settings, member.coworkRole);
|
|
298
320
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
299
321
|
state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
|
|
322
|
+
...(taskSupervised ? { task_supervised: true } : {}),
|
|
300
323
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
301
324
|
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
302
325
|
presentation,
|
|
@@ -307,6 +330,7 @@ async function launchMember(input) {
|
|
|
307
330
|
catch (error) {
|
|
308
331
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
309
332
|
state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
|
|
333
|
+
...(taskSupervised ? { task_supervised: true } : {}),
|
|
310
334
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
311
335
|
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
312
336
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
@@ -503,6 +527,7 @@ async function provisionMembersUnlocked(input) {
|
|
|
503
527
|
member,
|
|
504
528
|
settings: settings.get(member.name),
|
|
505
529
|
startup: {
|
|
530
|
+
...(taskId ? { task_id: taskId } : {}),
|
|
506
531
|
workspace: existing.workspace,
|
|
507
532
|
room_id: roomId,
|
|
508
533
|
room_identity_cid: roomIdentityCid,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
2
|
+
import { resumeTaskSupervisor } from '../spawn.js';
|
|
3
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
4
|
+
export type MemberRecovery = {
|
|
5
|
+
name: string;
|
|
6
|
+
status: 'running' | 'resumed' | 'migration_pending' | 'recovery_reset';
|
|
7
|
+
};
|
|
8
|
+
export interface TaskMemberRecoveryDeps {
|
|
9
|
+
liveness?: typeof tempSupervisorLiveness;
|
|
10
|
+
resume?: typeof resumeTaskSupervisor;
|
|
11
|
+
adopt?(taskId: string, roomId: string, name: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** Explicit supported recovery; never creates an identity, seat, invite, or session ID. */
|
|
14
|
+
export declare function recoverTaskMembers(input: {
|
|
15
|
+
taskId: string;
|
|
16
|
+
binPath: string;
|
|
17
|
+
cowork?: Pick<CoworkAdapter, 'getRoom'>;
|
|
18
|
+
deps?: TaskMemberRecoveryDeps;
|
|
19
|
+
}): Promise<MemberRecovery[]>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { withFileLock } from '../atomic-file.js';
|
|
5
|
+
import { agentDir, stateRoot } from '../paths.js';
|
|
6
|
+
import { readProvenance } from '../creation.js';
|
|
7
|
+
import { readTempSupervisor, tempSupervisorLiveness, requestedTempStopReason } from '../temp-lifecycle.js';
|
|
8
|
+
import { taskSupervisorMayRun } from '../task-supervision.js';
|
|
9
|
+
import { resumeTaskSupervisor } from '../spawn.js';
|
|
10
|
+
import { resetRestartLedger, readRestartLedger } from '../runner.js';
|
|
11
|
+
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
12
|
+
import { getTask } from './task-state.js';
|
|
13
|
+
import { getRoomRecord } from './room-state.js';
|
|
14
|
+
import { roomCloseLockPath, CLOSE_LOCK_STALE_MS } from './close.js';
|
|
15
|
+
import { taskOperationLockPath, TASK_OPERATION_LOCK_STALE_MS } from './terminal.js';
|
|
16
|
+
import { adoptLegacyTaskMember } from './legacy-task-member.js';
|
|
17
|
+
/** Explicit supported recovery; never creates an identity, seat, invite, or session ID. */
|
|
18
|
+
export async function recoverTaskMembers(input) {
|
|
19
|
+
const deps = input.deps ?? {};
|
|
20
|
+
return withFileLock(taskOperationLockPath(input.taskId), async () => {
|
|
21
|
+
const task = getTask(input.taskId);
|
|
22
|
+
if (!['provisioning', 'active', 'review'].includes(task.state) || task.deletion || task.terminal_intent)
|
|
23
|
+
throw Error('Task is not open for member recovery');
|
|
24
|
+
const recover = async (name, roomId) => {
|
|
25
|
+
const dir = agentDir(name, true), metadata = readTempSupervisor(dir);
|
|
26
|
+
if (!metadata || metadata.role !== name)
|
|
27
|
+
throw Error('Exact supervisor state missing; explicit reconciliation required');
|
|
28
|
+
if (requestedTempStopReason(dir))
|
|
29
|
+
throw Error('Task member has an explicit retirement request');
|
|
30
|
+
const live = await (deps.liveness ?? tempSupervisorLiveness)(dir);
|
|
31
|
+
if (live === 'unknown')
|
|
32
|
+
throw Error('Supervisor liveness unknown; refusing recovery');
|
|
33
|
+
if (!metadata.taskOwner) {
|
|
34
|
+
if (live === 'running')
|
|
35
|
+
return { name, status: 'migration_pending' };
|
|
36
|
+
if (!roomId)
|
|
37
|
+
throw Error('Legacy layout member requires explicit reconciliation');
|
|
38
|
+
if (deps.adopt)
|
|
39
|
+
await deps.adopt(input.taskId, roomId, name);
|
|
40
|
+
else {
|
|
41
|
+
const room = getRoomRecord(roomId);
|
|
42
|
+
const seat = room.member_seats.find(member => member.role_name === name);
|
|
43
|
+
const adopted = await adoptLegacyTaskMember({ taskId: input.taskId, roomId,
|
|
44
|
+
roomIdentityCid: room.room_identity_cid, creationActionId: seat.launch.action_id }, name);
|
|
45
|
+
if (adopted === 'running-legacy')
|
|
46
|
+
return { name, status: 'migration_pending' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!taskSupervisorMayRun(name))
|
|
50
|
+
throw Error('Task member is retired');
|
|
51
|
+
const reset = readRestartLedger(dir).circuit === 'open';
|
|
52
|
+
if (reset)
|
|
53
|
+
resetRestartLedger(dir);
|
|
54
|
+
if (live === 'running') {
|
|
55
|
+
return { name, status: reset ? 'recovery_reset' : 'running' };
|
|
56
|
+
}
|
|
57
|
+
await (deps.resume ?? resumeTaskSupervisor)(name, input.binPath);
|
|
58
|
+
return { name, status: 'resumed' };
|
|
59
|
+
};
|
|
60
|
+
if (task.layout) {
|
|
61
|
+
const path = join(stateRoot(), 'layouts', `${task.layout.run_id}.json`);
|
|
62
|
+
return withFileLock(path + '.lock', async () => {
|
|
63
|
+
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
64
|
+
if (state.closed || state.closing || state.uncertain)
|
|
65
|
+
throw Error('Layout requires reconciliation before recovery');
|
|
66
|
+
const result = [];
|
|
67
|
+
for (const member of Object.values(state.participants)) {
|
|
68
|
+
if (!member.owned || member.retired)
|
|
69
|
+
continue;
|
|
70
|
+
if (!member.instance?.agent || member.instance.remote)
|
|
71
|
+
throw Error('Layout owned instance evidence missing');
|
|
72
|
+
result.push(await recover(member.instance.agent));
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!task.room_id || !input.cowork)
|
|
78
|
+
throw Error('Task room required for member recovery');
|
|
79
|
+
return withFileLock(roomCloseLockPath(task.room_id), async () => {
|
|
80
|
+
const room = getRoomRecord(task.room_id);
|
|
81
|
+
if (!room || room.task_id !== task.task_id || room.close || !['active', 'provisioning'].includes(room.state)
|
|
82
|
+
|| !room.room_identity_cid || room.room_identity_cid !== task.room_identity_cid)
|
|
83
|
+
throw Error('Exact open task room required');
|
|
84
|
+
const remote = await input.cowork.getRoom(room.room_id);
|
|
85
|
+
if (!remote || remote.identity_cid !== room.room_identity_cid || remote.state !== 'active')
|
|
86
|
+
throw Error('Exact active Cowork room required');
|
|
87
|
+
// Validate the complete admitted roster before any member is resumed.
|
|
88
|
+
for (const seat of room.member_seats) {
|
|
89
|
+
const dir = agentDir(seat.role_name, true), metadata = readTempSupervisor(dir), provenance = readProvenance(dir);
|
|
90
|
+
const role = parse(readFileSync(join(dir, 'role.yaml'), 'utf8')), startup = role.roomMemberStartup;
|
|
91
|
+
const found = remote.seats.filter(item => item.display_name === seat.role_name && item.seat_state !== 'removed');
|
|
92
|
+
const ready = readRoomReadiness(room.room_identity_cid, seat.role_name);
|
|
93
|
+
if (seat.retirement || !seat.identity_cid || !seat.invite_id || !seat.launch?.action_id || !seat.launch.launch_id
|
|
94
|
+
|| metadata?.role !== seat.role_name || metadata.launchId !== seat.launch.launch_id
|
|
95
|
+
|| provenance?.role !== seat.role_name || provenance.creationActionId !== seat.launch.action_id
|
|
96
|
+
|| role.identity !== seat.role_name || startup?.identity_name !== seat.role_name
|
|
97
|
+
|| startup.room_id !== room.room_id || startup.room_identity_cid !== room.room_identity_cid
|
|
98
|
+
|| startup.invite_id !== seat.invite_id || startup.role !== seat.cowork_role
|
|
99
|
+
|| !ready || ready.cid !== seat.identity_cid || ready.room !== room.room_id || ready.invite !== seat.invite_id
|
|
100
|
+
|| found.length !== 1 || found[0].identity_cid !== seat.identity_cid || found[0].invite_id !== seat.invite_id
|
|
101
|
+
|| found[0].role !== seat.cowork_role || found[0].seat_state !== 'active')
|
|
102
|
+
throw Error(`Exact admitted member proof missing: ${seat.role_name}`);
|
|
103
|
+
}
|
|
104
|
+
const result = [];
|
|
105
|
+
for (const seat of room.member_seats)
|
|
106
|
+
result.push(await recover(seat.role_name, room.room_id));
|
|
107
|
+
return result;
|
|
108
|
+
}, {}, CLOSE_LOCK_STALE_MS);
|
|
109
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
110
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Exec } from '../exec.js';
|
|
2
|
+
/** Registration retirement precedes every identity-absence shortcut. A proven
|
|
3
|
+
* legacy transient/detached launch never owned a retained registration.
|
|
4
|
+
* Missing state still requires exact catalog ownership before unregistering.
|
|
5
|
+
*/
|
|
6
|
+
export declare function retireTaskMemberService(name: string, expected: {
|
|
7
|
+
taskId: string;
|
|
8
|
+
creationActionId?: string;
|
|
9
|
+
launchId?: string;
|
|
10
|
+
taskSupervised?: boolean;
|
|
11
|
+
}, exec?: Exec): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { agentDir } from '../paths.js';
|
|
3
|
+
import { readTempSupervisor, tempArchiveForCreationAction, tempArchiveForLaunch } from '../temp-lifecycle.js';
|
|
4
|
+
import { unregisterTaskMember, hasTaskMemberRegistration } from '../supervisor/catalog.js';
|
|
5
|
+
import { assertMemberNotPermanent } from './member-ownership.js';
|
|
6
|
+
/** Registration retirement precedes every identity-absence shortcut. A proven
|
|
7
|
+
* legacy transient/detached launch never owned a retained registration.
|
|
8
|
+
* Missing state still requires exact catalog ownership before unregistering.
|
|
9
|
+
*/
|
|
10
|
+
export async function retireTaskMemberService(name, expected, exec) {
|
|
11
|
+
assertMemberNotPermanent(name);
|
|
12
|
+
const archive = expected.creationActionId ? tempArchiveForCreationAction(name, expected.creationActionId)
|
|
13
|
+
: expected.launchId ? { path: tempArchiveForLaunch(name, expected.launchId) } : undefined;
|
|
14
|
+
const source = existsSync(agentDir(name, true)) ? agentDir(name, true) : archive?.path;
|
|
15
|
+
const metadata = source ? readTempSupervisor(source) : undefined;
|
|
16
|
+
const hasServiceEvidence = hasTaskMemberRegistration(name);
|
|
17
|
+
const durableMetadata = metadata?.taskOwner && metadata.kind === 'fleet-managed';
|
|
18
|
+
if (!hasServiceEvidence && !expected.taskSupervised && !durableMetadata)
|
|
19
|
+
return;
|
|
20
|
+
await unregisterTaskMember(name, expected, { exec });
|
|
21
|
+
}
|