@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21
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 +204 -55
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +6 -25
- package/dist/application/role-creation-service.js +34 -72
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +58 -6
- package/dist/application/task-room-service.js +435 -79
- package/dist/briefing.js +74 -24
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +241 -76
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +75 -3
- package/dist/config.js +274 -34
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +192 -66
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +165 -0
- package/dist/fleet-command-audit.js +555 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/fleet-proxy.js +30 -8
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +629 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +58 -0
- package/dist/owner-channel/channel.js +321 -42
- package/dist/owner-channel/commands.d.ts +6 -1
- package/dist/owner-channel/commands.js +34 -19
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +414 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +514 -75
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/config.js +16 -14
- package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
- package/dist/rooms-tasks/cowork-adapter.js +6 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/provision.js +184 -49
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +96 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +124 -43
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +38 -0
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +15 -31
- package/dist/spawn.js +117 -237
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web/topology-promote.js +8 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +3 -2
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agent_templates/Critic.yaml +4 -0
- package/presets/fleet/agent_templates/Developer.yaml +4 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +19 -0
- package/presets/fleet/roles/Developer.yaml +19 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
- package/presets/fleet/room_templates/pair.yaml +10 -0
- package/presets/fleet/room_templates/single.yaml +7 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withFileLock } from '../atomic-file.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
3
|
+
import { type CoworkAdapter } from './cowork-adapter.js';
|
|
3
4
|
import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
4
5
|
export interface RoomCloseDeps {
|
|
5
6
|
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
@@ -11,6 +12,13 @@ export interface RoomCloseDeps {
|
|
|
11
12
|
removeIdentity?(seat: RoomMemberSeat): Promise<void>;
|
|
12
13
|
lock?: typeof withFileLock;
|
|
13
14
|
}
|
|
15
|
+
export declare function inspectMember(seat: RoomMemberSeat): Promise<{
|
|
16
|
+
launchId: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function waitForLivenessAbsent(role: string, launchId: string, lifecycleDeps?: TempLifecycleDeps): Promise<void>;
|
|
19
|
+
/** Report whether any daemon identity — under any name — carries this exact CID. */
|
|
20
|
+
export declare function identityCidPresent(cid: string): Promise<boolean>;
|
|
21
|
+
export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
|
|
14
22
|
/** One forward-only room close saga shared by every Fleet entry point. */
|
|
15
23
|
export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
|
|
16
24
|
export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
|
|
@@ -5,6 +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 { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
|
|
8
|
+
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
8
9
|
import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
|
|
9
10
|
const CLOSE_LOCK_STALE_MS = 5 * 60_000;
|
|
10
11
|
const STOP_POLLS = 50;
|
|
@@ -26,7 +27,7 @@ function exactMemberIdentity(seat) {
|
|
|
26
27
|
throw new Error(`room member '${seat.role_name}' temp state binds identity '${identity}'; refusing mismatched retirement`);
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
async function inspectMember(seat) {
|
|
30
|
+
export async function inspectMember(seat) {
|
|
30
31
|
exactMemberIdentity(seat);
|
|
31
32
|
const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
|
|
32
33
|
if (!supervisor || supervisor.role !== seat.role_name) {
|
|
@@ -34,7 +35,7 @@ async function inspectMember(seat) {
|
|
|
34
35
|
}
|
|
35
36
|
return { launchId: supervisor.launchId };
|
|
36
37
|
}
|
|
37
|
-
async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {}) {
|
|
38
|
+
export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {}) {
|
|
38
39
|
const dir = agentDir(role, true);
|
|
39
40
|
const sleep = lifecycleDeps.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
40
41
|
for (let attempt = 0; attempt < STOP_POLLS; attempt++) {
|
|
@@ -68,7 +69,14 @@ async function withIdentityClient(work) {
|
|
|
68
69
|
function listedIdentity(rows, name) {
|
|
69
70
|
return rows.find(row => row.name === name);
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
/** Report whether any daemon identity — under any name — carries this exact CID. */
|
|
73
|
+
export async function identityCidPresent(cid) {
|
|
74
|
+
return withIdentityClient(async (client) => {
|
|
75
|
+
const rows = await client.listIdentities();
|
|
76
|
+
return rows.some(row => row.cid?.toLowerCase() === cid.toLowerCase());
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function removeExactMemberIdentity(seat) {
|
|
72
80
|
await withIdentityClient(async (client) => {
|
|
73
81
|
const before = listedIdentity(await client.listIdentities(), seat.role_name);
|
|
74
82
|
if (!before)
|
|
@@ -172,7 +180,15 @@ export async function closeManagedRoom(input) {
|
|
|
172
180
|
export async function deleteLegacyClosedRooms(input) {
|
|
173
181
|
const deleted = [];
|
|
174
182
|
for (const room of listRoomRecords({ state: 'closed' })) {
|
|
175
|
-
|
|
183
|
+
try {
|
|
184
|
+
await input.cowork.deleteRoom(room.room_id);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// The retained Fleet record is the legacy state being migrated. If the
|
|
188
|
+
// exact Cowork room is already absent, the desired deletion is complete.
|
|
189
|
+
if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
176
192
|
deleteRoomRecord(room.room_id);
|
|
177
193
|
deleted.push(room.room_id);
|
|
178
194
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK,
|
|
4
|
-
import { BUILTIN_TEMPLATES } from './templates.js';
|
|
3
|
+
import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, TEMPLATE_ROOM_KEYS as TRK, } from './types.js';
|
|
5
4
|
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
6
5
|
const CID_RE = /^[0-9a-fA-F]{64}$/;
|
|
7
6
|
export class RoomsTasksConfigError extends Error {
|
|
@@ -116,9 +115,6 @@ export function validateRoomTemplatesConfig(raw, path) {
|
|
|
116
115
|
if (!isPlainObject(tplRaw))
|
|
117
116
|
throw new RoomsTasksConfigError(path, `room_templates.${name}: must be a mapping`);
|
|
118
117
|
rejectUnknown(tplRaw, [...TPK, 'name'], path, `room_templates.${name}`);
|
|
119
|
-
const isBuiltinOverride = BUILTIN_TEMPLATES.some(b => b.name === name);
|
|
120
|
-
if (isBuiltinOverride && !tplRaw.override_builtin)
|
|
121
|
-
throw new RoomsTasksConfigError(path, `room_templates.${name}: overrides built-in template; set override_builtin: true and bump version`);
|
|
122
118
|
const version = tplRaw.version;
|
|
123
119
|
if (version === undefined || !Number.isInteger(version) || version < 1)
|
|
124
120
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.version: required positive integer`);
|
|
@@ -136,25 +132,31 @@ export function validateRoomTemplatesConfig(raw, path) {
|
|
|
136
132
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
|
|
137
133
|
if (!Number.isInteger(m.count) || m.count < 1)
|
|
138
134
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
|
|
139
|
-
if (
|
|
140
|
-
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].
|
|
141
|
-
if (m.
|
|
142
|
-
|
|
143
|
-
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].overrides: must be a mapping`);
|
|
144
|
-
rejectUnknown(m.overrides, TOK, path, `room_templates.${name}.members[${i}].overrides`);
|
|
145
|
-
}
|
|
135
|
+
if (m.agent !== undefined)
|
|
136
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: persistent Agent references are no longer allowed; use agent_template: <ID>`);
|
|
137
|
+
if (typeof m.agent_template !== 'string' || !m.agent_template.trim())
|
|
138
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent_template: required non-blank Agent Template ID`);
|
|
146
139
|
return {
|
|
147
140
|
slot: m.slot,
|
|
148
141
|
role: m.role,
|
|
149
142
|
count: m.count,
|
|
150
|
-
|
|
151
|
-
overrides: m.overrides,
|
|
143
|
+
agent_template: m.agent_template,
|
|
152
144
|
};
|
|
153
145
|
});
|
|
146
|
+
const slots = new Set();
|
|
147
|
+
for (const member of members) {
|
|
148
|
+
if (slots.has(member.slot))
|
|
149
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members: duplicate slot '${member.slot}'`);
|
|
150
|
+
slots.add(member.slot);
|
|
151
|
+
}
|
|
154
152
|
let room;
|
|
155
153
|
if (tplRaw.room !== undefined) {
|
|
156
154
|
if (!isPlainObject(tplRaw.room))
|
|
157
155
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.room: must be a mapping`);
|
|
156
|
+
rejectUnknown(tplRaw.room, TRK, path, `room_templates.${name}.room`);
|
|
157
|
+
for (const key of TRK)
|
|
158
|
+
if (tplRaw.room[key] !== undefined && typeof tplRaw.room[key] !== 'boolean')
|
|
159
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.room.${key}: must be a boolean`);
|
|
158
160
|
room = {
|
|
159
161
|
quiet_membership: tplRaw.room.quiet_membership,
|
|
160
162
|
anonymous: tplRaw.room.anonymous,
|
|
@@ -47,6 +47,11 @@ function roomState(value, operation) {
|
|
|
47
47
|
throw new CoworkProtocolError(operation, 'room state is invalid');
|
|
48
48
|
return value;
|
|
49
49
|
}
|
|
50
|
+
function boolean(value, operation, label) {
|
|
51
|
+
if (typeof value !== 'boolean')
|
|
52
|
+
throw new CoworkProtocolError(operation, `${label} must be a boolean`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
50
55
|
function seatState(value, operation) {
|
|
51
56
|
if (value !== 'pending' && value !== 'active' && value !== 'removed')
|
|
52
57
|
throw new CoworkProtocolError(operation, 'seat state is invalid');
|
|
@@ -86,6 +91,7 @@ function projectRoom(value, operation) {
|
|
|
86
91
|
identity_cid: text(room.identity_cid, operation, 'room.identity_cid'),
|
|
87
92
|
room_name: string(room.room_name, operation, 'room.room_name'),
|
|
88
93
|
state: roomState(room.state, operation),
|
|
94
|
+
anonymous: room.anonymous === undefined ? false : boolean(room.anonymous, operation, 'room.anonymous'),
|
|
89
95
|
seats: room.seats.map((seat) => projectSeat(seat, operation)),
|
|
90
96
|
role_briefings: projectedBriefings,
|
|
91
97
|
...(typeof mission?.goal === 'string' ? { goal: mission.goal } : {}),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type RoomCloseDeps } from './close.js';
|
|
2
|
+
import { type CoworkAdapter } from './cowork-adapter.js';
|
|
3
|
+
import { type TaskDeletionAcceptance } from './task-state.js';
|
|
4
|
+
import type { TaskDeletionActor, TaskRecord } from './types.js';
|
|
5
|
+
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
6
|
+
type DeletionCowork = Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'>;
|
|
7
|
+
export interface TaskDeletionSettleDeps {
|
|
8
|
+
roomClose?: RoomCloseDeps;
|
|
9
|
+
/** Test seam for the temp-state existence proof. */
|
|
10
|
+
hasTempState?(name: string): boolean;
|
|
11
|
+
/** Test seam for the CID-wide daemon identity scan. */
|
|
12
|
+
identityCidPresent?(cid: string): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
export interface TaskDeletionSettleResult {
|
|
15
|
+
task_id: string;
|
|
16
|
+
deleted: boolean;
|
|
17
|
+
/** Lifecycle state observed before unlink, for completion audit. */
|
|
18
|
+
previous_state?: TaskRecord['state'];
|
|
19
|
+
/** Title observed before unlink, for completion audit. */
|
|
20
|
+
title?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Accept a permanent task deletion under the common task-operation lock so it
|
|
24
|
+
* serializes against terminal settlement, recovery, and the room-publication
|
|
25
|
+
* window. Acceptance is durable before any side effect and never requires
|
|
26
|
+
* Cowork availability; settlement runs separately and converges through
|
|
27
|
+
* retries.
|
|
28
|
+
*/
|
|
29
|
+
export declare function acceptTaskDeletion(taskId: string, actor: TaskDeletionActor): Promise<TaskDeletionAcceptance>;
|
|
30
|
+
/** Persist a settlement failure under the same task-operation lock. */
|
|
31
|
+
export declare function recordTaskDeletionError(taskId: string, error: string, recoveryHint: string): Promise<TaskRecord>;
|
|
32
|
+
/**
|
|
33
|
+
* Converge an accepted deletion to physical absence in two lock-ordered
|
|
34
|
+
* stages. Cleanup stage (task-operation lock): per surviving room record,
|
|
35
|
+
* register late seats → close saga (retires members) → durable evidence
|
|
36
|
+
* checkpoint → tolerant remote delete → local record delete; then
|
|
37
|
+
* cursor-based retirement for members with no room record and tolerant remote
|
|
38
|
+
* close+delete of a recorded room whose local record is gone. Finalization
|
|
39
|
+
* stage (launch-snapshot lock → task-operation lock, matching the global
|
|
40
|
+
* provisioning order): re-verify quiescence, delete the owned launch snapshot
|
|
41
|
+
* while excluding only this deleting task from the reference scan, and unlink
|
|
42
|
+
* the task record LAST — every crash seam leaves the hidden intent
|
|
43
|
+
* recoverable. Failures are recorded on the intent; the task stays hidden and
|
|
44
|
+
* recoverable, never falsely settled.
|
|
45
|
+
*/
|
|
46
|
+
export declare function settleTaskDeletion(input: {
|
|
47
|
+
taskId: string;
|
|
48
|
+
/** Lazy: resolved only when room work exists, so no-room tasks settle without Cowork config. */
|
|
49
|
+
cowork: () => DeletionCowork;
|
|
50
|
+
deps?: TaskDeletionSettleDeps;
|
|
51
|
+
}): Promise<TaskDeletionSettleResult>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { withFileLock } from '../atomic-file.js';
|
|
3
|
+
import { agentDir } from '../paths.js';
|
|
4
|
+
import { secureStoppedTempArchive, stopTempSupervisor } from '../temp-lifecycle.js';
|
|
5
|
+
import { closeManagedRoom, identityCidPresent, inspectMember, removeExactMemberIdentity, waitForLivenessAbsent, } from './close.js';
|
|
6
|
+
import { CoworkProtocolError } from './cowork-adapter.js';
|
|
7
|
+
import { deleteRoomRecord, getRoomRecord, listRoomRecords } from './room-state.js';
|
|
8
|
+
import { acquireLaunchSnapshotLock, releaseLaunchSnapshotForDeletingTask } from './launch-snapshot.js';
|
|
9
|
+
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
10
|
+
import { advanceTaskDeletionMember, beginTaskDeletionIntent, completeTaskDeletionReceipt, ensureTaskDeletionReceipt, getDeletingTask, importTaskDeletionRetirementEvidence, setTaskDeletionError, TaskStateError, unlinkDeletedTask, upsertTaskDeletionMembersFromSeats, } from './task-state.js';
|
|
11
|
+
export { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
12
|
+
import { DELETION_MEMBER_ABSENT_VERIFIED } from './task-state.js';
|
|
13
|
+
/**
|
|
14
|
+
* Accept a permanent task deletion under the common task-operation lock so it
|
|
15
|
+
* serializes against terminal settlement, recovery, and the room-publication
|
|
16
|
+
* window. Acceptance is durable before any side effect and never requires
|
|
17
|
+
* Cowork availability; settlement runs separately and converges through
|
|
18
|
+
* retries.
|
|
19
|
+
*/
|
|
20
|
+
export function acceptTaskDeletion(taskId, actor) {
|
|
21
|
+
return withFileLock(taskOperationLockPath(taskId), () => beginTaskDeletionIntent(taskId, actor), {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
22
|
+
}
|
|
23
|
+
/** Persist a settlement failure under the same task-operation lock. */
|
|
24
|
+
export function recordTaskDeletionError(taskId, error, recoveryHint) {
|
|
25
|
+
return withFileLock(taskOperationLockPath(taskId), () => setTaskDeletionError(taskId, error, recoveryHint), {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
26
|
+
}
|
|
27
|
+
function errorText(error) {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
/** Already-missing remote rooms are a settled outcome, not a failure. */
|
|
31
|
+
function tolerantCowork(cowork) {
|
|
32
|
+
const tolerate = async (work) => {
|
|
33
|
+
try {
|
|
34
|
+
await work();
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof CoworkProtocolError && error.code === 'not_found')
|
|
38
|
+
return;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
closeRoom: roomId => tolerate(() => cowork.closeRoom(roomId)),
|
|
44
|
+
deleteRoom: roomId => tolerate(() => cowork.deleteRoom(roomId)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function cursorSeat(cursor) {
|
|
48
|
+
return {
|
|
49
|
+
role_name: cursor.name, identity_cid: cursor.identity_cid,
|
|
50
|
+
slot: cursor.name, cowork_role: 'member', seat_state: 'active',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Retire one member from its durable deletion cursor when no room record
|
|
55
|
+
* carries the seat: the same evidence chain as close.ts, resumed at the
|
|
56
|
+
* recorded phase. A member with no temp state is verified absent through the
|
|
57
|
+
* CID-wide identity scan; a same-name stranger identity is never touched, and
|
|
58
|
+
* the recorded CID surviving under any name blocks settlement.
|
|
59
|
+
*/
|
|
60
|
+
async function retireCursorMember(taskId, cursor, deps) {
|
|
61
|
+
let phase = cursor.phase;
|
|
62
|
+
let launchId = cursor.launch_id;
|
|
63
|
+
if (phase === 'identity_absent')
|
|
64
|
+
return;
|
|
65
|
+
const seat = cursorSeat(cursor);
|
|
66
|
+
const roomClose = deps.roomClose ?? {};
|
|
67
|
+
if (phase === 'pending') {
|
|
68
|
+
const hasState = (deps.hasTempState ?? (name => existsSync(agentDir(name, true))))(cursor.name);
|
|
69
|
+
if (!hasState) {
|
|
70
|
+
const present = await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid);
|
|
71
|
+
if (present) {
|
|
72
|
+
// Remove only an exact name+CID match; a stranger under the same name
|
|
73
|
+
// stays untouched and removeExactMemberIdentity refuses the mismatch.
|
|
74
|
+
// The recorded CID surviving under a different name throws below on
|
|
75
|
+
// its post-removal verification and blocks settlement.
|
|
76
|
+
await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
|
|
77
|
+
if (await (deps.identityCidPresent ?? identityCidPresent)(cursor.identity_cid))
|
|
78
|
+
throw new TaskStateError(`deletion member '${cursor.name}' identity CID survives under another name; refusing to claim retirement`);
|
|
79
|
+
}
|
|
80
|
+
advanceTaskDeletionMember(taskId, cursor.name, 'identity_absent', DELETION_MEMBER_ABSENT_VERIFIED);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const ownership = await (roomClose.inspectMember ?? inspectMember)(seat);
|
|
84
|
+
advanceTaskDeletionMember(taskId, cursor.name, 'stop_requested', ownership.launchId);
|
|
85
|
+
phase = 'stop_requested';
|
|
86
|
+
launchId = ownership.launchId;
|
|
87
|
+
}
|
|
88
|
+
if (phase === 'stop_requested') {
|
|
89
|
+
await (roomClose.requestStop
|
|
90
|
+
?? (async (role) => { await stopTempSupervisor(role); }))(cursor.name);
|
|
91
|
+
await (roomClose.waitForLivenessAbsent ?? waitForLivenessAbsent)(cursor.name, launchId);
|
|
92
|
+
advanceTaskDeletionMember(taskId, cursor.name, 'liveness_absent');
|
|
93
|
+
phase = 'liveness_absent';
|
|
94
|
+
}
|
|
95
|
+
if (phase === 'liveness_absent') {
|
|
96
|
+
const archivePath = await (roomClose.secureArchive ?? secureStoppedTempArchive)(cursor.name, launchId);
|
|
97
|
+
advanceTaskDeletionMember(taskId, cursor.name, 'archive_secured', undefined, archivePath);
|
|
98
|
+
phase = 'archive_secured';
|
|
99
|
+
}
|
|
100
|
+
if (phase === 'archive_secured') {
|
|
101
|
+
await (roomClose.removeIdentity ?? removeExactMemberIdentity)(seat);
|
|
102
|
+
advanceTaskDeletionMember(taskId, cursor.name, 'identity_absent');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Converge an accepted deletion to physical absence in two lock-ordered
|
|
107
|
+
* stages. Cleanup stage (task-operation lock): per surviving room record,
|
|
108
|
+
* register late seats → close saga (retires members) → durable evidence
|
|
109
|
+
* checkpoint → tolerant remote delete → local record delete; then
|
|
110
|
+
* cursor-based retirement for members with no room record and tolerant remote
|
|
111
|
+
* close+delete of a recorded room whose local record is gone. Finalization
|
|
112
|
+
* stage (launch-snapshot lock → task-operation lock, matching the global
|
|
113
|
+
* provisioning order): re-verify quiescence, delete the owned launch snapshot
|
|
114
|
+
* while excluding only this deleting task from the reference scan, and unlink
|
|
115
|
+
* the task record LAST — every crash seam leaves the hidden intent
|
|
116
|
+
* recoverable. Failures are recorded on the intent; the task stays hidden and
|
|
117
|
+
* recoverable, never falsely settled.
|
|
118
|
+
*/
|
|
119
|
+
export async function settleTaskDeletion(input) {
|
|
120
|
+
const { taskId } = input;
|
|
121
|
+
const deps = input.deps ?? {};
|
|
122
|
+
const recoveryHint = `Re-run 'ours-fleet task delete ${taskId} ${taskId}' or 'ours-fleet task recover ${taskId}'.`;
|
|
123
|
+
const cleanup = await withFileLock(taskOperationLockPath(taskId), async () => {
|
|
124
|
+
let task;
|
|
125
|
+
try {
|
|
126
|
+
task = getDeletingTask(taskId);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof TaskStateError && /task not found/.test(error.message))
|
|
130
|
+
return { kind: 'already_absent' };
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
if (task.deletion?.status !== 'pending')
|
|
134
|
+
throw new TaskStateError(`task ${taskId} has no pending deletion`);
|
|
135
|
+
try {
|
|
136
|
+
// Audit evidence precedes every side effect; a receipt write failure
|
|
137
|
+
// aborts settlement (fail closed).
|
|
138
|
+
ensureTaskDeletionReceipt(taskId);
|
|
139
|
+
const records = listRoomRecords().filter(room => room.task_id === taskId);
|
|
140
|
+
const recordIds = new Set(records.map(room => room.room_id));
|
|
141
|
+
const recordedRoomId = task.deletion.room_id ?? task.room_id;
|
|
142
|
+
const needsCowork = records.length > 0 || recordedRoomId !== undefined;
|
|
143
|
+
const cowork = needsCowork ? tolerantCowork(input.cowork()) : undefined;
|
|
144
|
+
for (const record of records) {
|
|
145
|
+
upsertTaskDeletionMembersFromSeats(taskId, record.member_seats);
|
|
146
|
+
await closeManagedRoom({ roomId: record.room_id, cowork: cowork, deps: deps.roomClose });
|
|
147
|
+
const closed = getRoomRecord(record.room_id);
|
|
148
|
+
importTaskDeletionRetirementEvidence(taskId, closed?.member_seats ?? record.member_seats);
|
|
149
|
+
await cowork.deleteRoom(record.room_id);
|
|
150
|
+
deleteRoomRecord(record.room_id);
|
|
151
|
+
}
|
|
152
|
+
// Members whose room record is gone (crash after record deletion, or
|
|
153
|
+
// legacy state): resume from the durable cursors.
|
|
154
|
+
for (const cursor of getDeletingTask(taskId).deletion.members) {
|
|
155
|
+
await retireCursorMember(taskId, cursor, deps);
|
|
156
|
+
}
|
|
157
|
+
// A recorded room without a local record may still be live remotely.
|
|
158
|
+
if (recordedRoomId && !recordIds.has(recordedRoomId) && !getRoomRecord(recordedRoomId)) {
|
|
159
|
+
await cowork.closeRoom(recordedRoomId);
|
|
160
|
+
await cowork.deleteRoom(recordedRoomId);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
kind: 'cleaned',
|
|
164
|
+
snapshotHash: task.execution_plan?.snapshot.launch_snapshot_hash,
|
|
165
|
+
previousState: task.state,
|
|
166
|
+
title: task.title,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
setTaskDeletionError(taskId, errorText(error), recoveryHint);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
174
|
+
if (cleanup.kind === 'already_absent') {
|
|
175
|
+
// Heal a crash between unlink and receipt completion.
|
|
176
|
+
completeTaskDeletionReceipt(taskId);
|
|
177
|
+
return { task_id: taskId, deleted: false };
|
|
178
|
+
}
|
|
179
|
+
const finalize = () => withFileLock(taskOperationLockPath(taskId), () => {
|
|
180
|
+
try {
|
|
181
|
+
const task = getDeletingTask(taskId);
|
|
182
|
+
if (task.deletion?.status !== 'pending')
|
|
183
|
+
throw new TaskStateError(`task ${taskId} has no pending deletion`);
|
|
184
|
+
if (listRoomRecords().some(room => room.task_id === taskId))
|
|
185
|
+
throw new TaskStateError(`task ${taskId} room records reappeared during deletion finalization`);
|
|
186
|
+
if (task.deletion.members.some(member => member.phase !== 'identity_absent'))
|
|
187
|
+
throw new TaskStateError(`task ${taskId} has unretired members at deletion finalization`);
|
|
188
|
+
if (cleanup.snapshotHash)
|
|
189
|
+
releaseLaunchSnapshotForDeletingTask(cleanup.snapshotHash, taskId);
|
|
190
|
+
unlinkDeletedTask(taskId);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
setTaskDeletionError(taskId, errorText(error), recoveryHint);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
// After unlink the task can no longer carry errors; receipt completion is
|
|
197
|
+
// idempotent and the already-absent retry path heals a crash here.
|
|
198
|
+
completeTaskDeletionReceipt(taskId);
|
|
199
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
200
|
+
if (cleanup.snapshotHash) {
|
|
201
|
+
// Global lock order: launch-snapshot lock → task-operation lock.
|
|
202
|
+
const releaseSnapshotLock = acquireLaunchSnapshotLock();
|
|
203
|
+
try {
|
|
204
|
+
await finalize();
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
releaseSnapshotLock();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
await finalize();
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
task_id: taskId, deleted: true,
|
|
215
|
+
previous_state: cleanup.previousState, title: cleanup.title,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
export declare function fleetWorkerEnv(source?: NodeJS.ProcessEnv): Record<string, string>;
|
|
1
2
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
2
3
|
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
export function
|
|
5
|
-
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
3
|
+
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV } from '../fleet-proxy.js';
|
|
4
|
+
export function fleetWorkerEnv(source = process.env) {
|
|
6
5
|
const inheritedKeys = [
|
|
7
6
|
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
|
|
8
7
|
'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
|
|
9
8
|
];
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
10
|
+
// Explicit defense: trusted settle/recover/delete/close workers are effects
|
|
11
|
+
// nested inside the outer audited command, never fresh agent attempts.
|
|
12
|
+
delete env[FLEET_PROXY_STATE_DIR_ENV];
|
|
13
|
+
delete env[FLEET_PROXY_CALLER_ENV];
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
17
|
+
export function launchFleetWorker(args, operation, configPath) {
|
|
18
|
+
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
19
|
+
const env = fleetWorkerEnv();
|
|
20
|
+
const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
12
21
|
const suffix = randomUUID().slice(0, 8);
|
|
13
22
|
const safeOperation = operation.replace(/[^A-Za-z0-9_.-]/g, '-');
|
|
14
23
|
return new Promise((resolve, reject) => {
|
|
@@ -29,10 +38,6 @@ export function launchFleetWorker(args, operation, configPath) {
|
|
|
29
38
|
], { timeout: 15_000 }, error => error ? reject(error) : resolve());
|
|
30
39
|
return;
|
|
31
40
|
}
|
|
32
|
-
const env = Object.fromEntries(inherited.map(value => {
|
|
33
|
-
const split = value.indexOf('=');
|
|
34
|
-
return [value.slice(0, split), value.slice(split + 1)];
|
|
35
|
-
}));
|
|
36
41
|
const child = spawn(process.execPath, cli, { detached: true, stdio: 'ignore', env });
|
|
37
42
|
child.once('error', reject);
|
|
38
43
|
child.once('spawn', () => { child.unref(); resolve(); });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentTemplateDefinition } from '../config.js';
|
|
2
|
+
/** Cross-process exclusion used from seal through durable Room publication. */
|
|
3
|
+
export declare function acquireLaunchSnapshotLock(options?: {
|
|
4
|
+
beforePublish?: (claimPath: string, lockPath: string) => void;
|
|
5
|
+
}): () => void;
|
|
6
|
+
export declare function redactLaunchDefinition(value: unknown, key?: string): unknown;
|
|
7
|
+
export declare function sealLaunchSnapshot(definitions: Record<string, AgentTemplateDefinition>): string;
|
|
8
|
+
export declare function readLaunchSnapshot(hash: string): Record<string, AgentTemplateDefinition>;
|
|
9
|
+
/** Delete a sealed snapshot only after no retained Task/Room record references it. */
|
|
10
|
+
export declare function releaseLaunchSnapshot(hash: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Deletion-finalization variant: the caller already holds the global
|
|
13
|
+
* launch-snapshot lock and is about to unlink the deletion-pending task
|
|
14
|
+
* `taskId`. The reference scan excludes exactly that task file — every other
|
|
15
|
+
* retained Task/Room reference still protects the snapshot. Deleting the
|
|
16
|
+
* snapshot BEFORE the task unlink keeps every crash seam recoverable: the
|
|
17
|
+
* hidden intent survives, and a retry sees the snapshot already absent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function releaseLaunchSnapshotForDeletingTask(hash: string, taskId: string): void;
|