@ours.network/fleet 1.1.0-nightly.11 → 1.1.0-nightly.13
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/application/role-creation-service.js +8 -1
- package/dist/application/task-room-service.d.ts +44 -4
- package/dist/application/task-room-service.js +137 -20
- package/dist/build-info.json +4 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +18 -5
- package/dist/fleet-command-audit.d.ts +4 -0
- package/dist/fleet-command-audit.js +126 -22
- package/dist/fleet-proxy.d.ts +3 -0
- package/dist/lifecycle-summary.d.ts +118 -0
- package/dist/lifecycle-summary.js +161 -0
- package/dist/owner-channel/channel.d.ts +2 -0
- package/dist/owner-channel/channel.js +45 -9
- package/dist/owner-channel/commands.d.ts +4 -1
- package/dist/owner-channel/commands.js +3 -6
- package/dist/rooms-tasks/cli.js +151 -19
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +9 -0
- package/dist/rooms-tasks/launch-snapshot.js +18 -1
- package/dist/rooms-tasks/provision.js +84 -10
- package/dist/rooms-tasks/task-state.d.ts +113 -3
- package/dist/rooms-tasks/task-state.js +333 -9
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +48 -0
- package/dist/web/server.js +28 -1
- package/package.json +1 -1
|
@@ -17,6 +17,8 @@ export interface OwnerFleetOps {
|
|
|
17
17
|
closeRoom(roomId: string): Promise<void>;
|
|
18
18
|
/** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
|
|
19
19
|
settleTask(taskId: string): Promise<void>;
|
|
20
|
+
/** Settle an accepted task deletion outside the caller role's supervisor lifecycle. */
|
|
21
|
+
settleTaskDeletion(taskId: string): Promise<void>;
|
|
20
22
|
recoverTask(taskId: string): Promise<void>;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
@@ -84,7 +86,8 @@ export interface OwnerCommandContext {
|
|
|
84
86
|
blockTask(taskId: string, reason: string): TaskRecord;
|
|
85
87
|
unblockTask(taskId: string): TaskRecord;
|
|
86
88
|
reviewTask(taskId: string): TaskRecord;
|
|
87
|
-
|
|
89
|
+
/** Accept a permanent any-state deletion, acknowledge it, then launch the external delete worker. */
|
|
90
|
+
deleteTask(taskId: string): Promise<void>;
|
|
88
91
|
listRoomQueries(filter?: {
|
|
89
92
|
state?: 'active' | 'provisioning';
|
|
90
93
|
}): ReturnType<TaskRoomApplicationService['listRooms']>;
|
|
@@ -36,7 +36,7 @@ function isKnownOwnerTaskState(message) {
|
|
|
36
36
|
/^task is not blocked$/u,
|
|
37
37
|
new RegExp(`^task ${SAFE_ID_WORD} already has a conflicting '(?:done|cancelled)' terminal intent$`, 'u'),
|
|
38
38
|
new RegExp(`^task ${SAFE_ID_WORD} is already in terminal state '${TASK_STATE_WORD}'$`, 'u'),
|
|
39
|
-
new RegExp(`^
|
|
39
|
+
new RegExp(`^task ${SAFE_ID_WORD} is pending deletion; run 'ours-fleet task delete ${SAFE_ID_WORD} ${SAFE_ID_WORD}' to retry cleanup$`, 'u'),
|
|
40
40
|
].some(pattern => pattern.test(message));
|
|
41
41
|
}
|
|
42
42
|
function isKnownOwnerRoomState(message) {
|
|
@@ -461,11 +461,7 @@ export const ownerCommands = [
|
|
|
461
461
|
case 'delete': {
|
|
462
462
|
if (rest.length !== 2 || rest[0] !== rest[1])
|
|
463
463
|
throw new OwnerCommandUsageError('destructive: /task delete <id> <id> — provide the task ID twice');
|
|
464
|
-
|
|
465
|
-
await ctx.reply(renderMarkdownResult({
|
|
466
|
-
icon: '🗑️', title: deleted ? 'Task deleted' : 'Task already absent',
|
|
467
|
-
fields: [{ label: 'ID', value: rest[0], kind: 'code' }],
|
|
468
|
-
}));
|
|
464
|
+
await ctx.deleteTask(rest[0]);
|
|
469
465
|
break;
|
|
470
466
|
}
|
|
471
467
|
case 'recover': {
|
|
@@ -734,6 +730,7 @@ export function fleetCliOps(role, configPath) {
|
|
|
734
730
|
}),
|
|
735
731
|
closeRoom: roomId => launchFleetWorker(['room', '_delete', roomId], `room-delete-${roomId}`, configPath),
|
|
736
732
|
settleTask: taskId => launchFleetWorker(['task', '_settle', taskId], `task-settle-${taskId}`, configPath),
|
|
733
|
+
settleTaskDeletion: taskId => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, configPath),
|
|
737
734
|
recoverTask: taskId => launchFleetWorker(['task', '_recover', taskId], `task-recover-${taskId}`, configPath),
|
|
738
735
|
};
|
|
739
736
|
}
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -29,13 +29,14 @@ function commandArgv(command) {
|
|
|
29
29
|
root = root.parent;
|
|
30
30
|
return root.rawArgs ?? process.argv.slice(2);
|
|
31
31
|
}
|
|
32
|
-
import { getTask, activateTask, TaskStateError, } from './task-state.js';
|
|
32
|
+
import { getTask, getDeletingTask, activateTask, TaskStateError, } from './task-state.js';
|
|
33
33
|
import { createRoomRecord, getRoomRecord, advanceSaga, setOwnerSeat, setSagaError, activateRoom, RoomStateError, } from './room-state.js';
|
|
34
34
|
import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
|
|
35
35
|
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
|
|
36
36
|
import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
37
37
|
import { TaskListError } from './task-lists.js';
|
|
38
38
|
import { recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
|
|
39
|
+
import { renderAgentConfiguration } from '../lifecycle-summary.js';
|
|
39
40
|
class TaskRoomPublicError extends Error {
|
|
40
41
|
code;
|
|
41
42
|
fields;
|
|
@@ -152,7 +153,7 @@ function isKnownTaskStateMessage(message) {
|
|
|
152
153
|
new RegExp(`^task ${SAFE_ID_WORD} is not tied to room ${SAFE_ID_WORD}$`, 'u'),
|
|
153
154
|
new RegExp(`^task ${SAFE_ID_WORD} has no terminal intent$`, 'u'),
|
|
154
155
|
new RegExp(`^task ${SAFE_ID_WORD} reached terminal state '${TASK_STATE_WORD}' outside its intent$`, 'u'),
|
|
155
|
-
new RegExp(`^
|
|
156
|
+
new RegExp(`^task ${SAFE_ID_WORD} is pending deletion; run 'ours-fleet task delete ${SAFE_ID_WORD} ${SAFE_ID_WORD}' to retry cleanup$`, 'u'),
|
|
156
157
|
].some(pattern => pattern.test(message));
|
|
157
158
|
}
|
|
158
159
|
function isKnownRoomStateMessage(message) {
|
|
@@ -240,6 +241,14 @@ const taskActionMarkdown = (title, task, fields = []) => {
|
|
|
240
241
|
],
|
|
241
242
|
});
|
|
242
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* Shared launch-configuration line for CLI member listings. Pre-upgrade seats
|
|
246
|
+
* without a captured presentation keep the minimal listing rather than
|
|
247
|
+
* claiming resolved facts. Output is already escaped (markdownItems boundary).
|
|
248
|
+
*/
|
|
249
|
+
function seatConfigurationSuffix(presentation) {
|
|
250
|
+
return presentation ? ` — ${renderAgentConfiguration(presentation)}` : '';
|
|
251
|
+
}
|
|
243
252
|
function safeSelectionSummary(definition, kind) {
|
|
244
253
|
const selected = definition?.[kind];
|
|
245
254
|
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
@@ -279,6 +288,7 @@ function auditTask(operation, task, previousState, newState = task.state, revisi
|
|
|
279
288
|
recordFleetAuditResource('room', task.room_id);
|
|
280
289
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
281
290
|
const definitions = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.agent_definition]) ?? []);
|
|
291
|
+
const presentations = new Map(room?.member_seats.map(seat => [seat.role_name, seat.launch?.presentation]) ?? []);
|
|
282
292
|
const semanticOperation = operation === 'recover'
|
|
283
293
|
? newState === 'active' ? 'work' : newState === 'done' ? 'done'
|
|
284
294
|
: newState === 'cancelled' ? 'cancel' : undefined
|
|
@@ -294,7 +304,8 @@ function auditTask(operation, task, previousState, newState = task.state, revisi
|
|
|
294
304
|
brain: safeSelectionSummary(definitions.get(member.name), 'brain'),
|
|
295
305
|
role: safeSelectionSummary(definitions.get(member.name), 'role') === 'unresolved'
|
|
296
306
|
? member.cowork_role : safeSelectionSummary(definitions.get(member.name), 'role'),
|
|
297
|
-
permissions: safePermissionsSummary(definitions.get(member.name))
|
|
307
|
+
permissions: safePermissionsSummary(definitions.get(member.name)),
|
|
308
|
+
configuration: presentations.get(member.name) })) });
|
|
298
309
|
}
|
|
299
310
|
function auditRoom(operation, room, previousState, newState = room.state) {
|
|
300
311
|
if (previousState === newState)
|
|
@@ -314,7 +325,8 @@ function auditRoom(operation, room, previousState, newState = room.state) {
|
|
|
314
325
|
brain: safeSelectionSummary(member.launch?.agent_definition, 'brain'),
|
|
315
326
|
role: safeSelectionSummary(member.launch?.agent_definition, 'role') === 'unresolved'
|
|
316
327
|
? member.cowork_role : safeSelectionSummary(member.launch?.agent_definition, 'role'),
|
|
317
|
-
permissions: safePermissionsSummary(member.launch?.agent_definition)
|
|
328
|
+
permissions: safePermissionsSummary(member.launch?.agent_definition),
|
|
329
|
+
configuration: member.launch?.presentation })) });
|
|
318
330
|
}
|
|
319
331
|
const roomActionMarkdown = (title, room, fields = []) => {
|
|
320
332
|
recordFleetAuditResource('room', room.room_id);
|
|
@@ -365,6 +377,49 @@ async function launchTaskSettleWorker(taskId, configPath) {
|
|
|
365
377
|
eventId: task.terminal_intent?.accepted_at ?? task.created_at });
|
|
366
378
|
return { task, timedOut: true };
|
|
367
379
|
}
|
|
380
|
+
/** Launch the deletion settle worker and wait boundedly for physical absence. */
|
|
381
|
+
async function launchTaskDeleteWorker(taskId, configPath) {
|
|
382
|
+
const readDeletion = () => {
|
|
383
|
+
try {
|
|
384
|
+
const task = getDeletingTask(taskId);
|
|
385
|
+
return { present: true, errorAt: task.deletion?.error_at, error: task.deletion?.error };
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// Only proven physical absence counts as deleted; anything else propagates.
|
|
389
|
+
if (error instanceof TaskStateError && /task not found/.test(error.message))
|
|
390
|
+
return { present: false };
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
const before = readDeletion();
|
|
395
|
+
if (!before.present)
|
|
396
|
+
return { deleted: true, timedOut: false };
|
|
397
|
+
try {
|
|
398
|
+
await launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, configPath);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
await taskRoomService(configPath).recordDeletionError({
|
|
402
|
+
actor: { kind: 'local_control', surface: 'cli' }, taskId, error: errorText(error),
|
|
403
|
+
recoveryHint: `External delete worker failed to start. Retry task delete ${taskId} ${taskId}.`,
|
|
404
|
+
}).catch(() => { });
|
|
405
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
|
|
406
|
+
state: 'deleting', category: 'settlement_failed', eventId: new Date().toISOString() });
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
const deadline = Date.now() + PUBLIC_SETTLE_WAIT_MS;
|
|
410
|
+
while (Date.now() < deadline) {
|
|
411
|
+
const current = readDeletion();
|
|
412
|
+
if (!current.present)
|
|
413
|
+
return { deleted: true, timedOut: false };
|
|
414
|
+
if (current.errorAt !== before.errorAt && current.error) {
|
|
415
|
+
throw new TaskStateError(current.error);
|
|
416
|
+
}
|
|
417
|
+
await sleep(PUBLIC_SETTLE_POLL_MS);
|
|
418
|
+
}
|
|
419
|
+
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: taskId,
|
|
420
|
+
state: 'deleting', category: 'settlement_pending', eventId: new Date().toISOString() });
|
|
421
|
+
return { deleted: false, timedOut: true };
|
|
422
|
+
}
|
|
368
423
|
async function launchRoomDeleteWorker(roomId, configPath) {
|
|
369
424
|
const previousErrorAt = getRoomRecord(roomId)?.close?.error_at;
|
|
370
425
|
try {
|
|
@@ -831,7 +886,9 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
831
886
|
],
|
|
832
887
|
sections: [
|
|
833
888
|
...(t.member_roles.length ? [{
|
|
834
|
-
heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`
|
|
889
|
+
heading: 'Members', markdownItems: t.member_roles.map(m => `${markdownCode(m.name)} — ${markdownProse(m.cowork_role)} — ${markdownCode(m.identity_cid)}`
|
|
890
|
+
+ seatConfigurationSuffix(room?.member_seats
|
|
891
|
+
.find(seat => seat.role_name === m.name)?.launch?.presentation)),
|
|
835
892
|
}] : []),
|
|
836
893
|
...(room ? roomStartupSections(room) : []),
|
|
837
894
|
],
|
|
@@ -1030,28 +1087,46 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1030
1087
|
}
|
|
1031
1088
|
});
|
|
1032
1089
|
cOpt(taskCmd.command('delete <id> <confirm-id>'))
|
|
1033
|
-
.description('delete a
|
|
1090
|
+
.description('permanently delete a task in any lifecycle state (requires ID twice for confirmation)')
|
|
1034
1091
|
.option('--json', 'JSON output')
|
|
1035
|
-
.action((id, confirmId, opts) => {
|
|
1092
|
+
.action(async (id, confirmId, opts) => {
|
|
1036
1093
|
try {
|
|
1037
1094
|
if (id !== confirmId)
|
|
1038
1095
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
1039
|
-
|
|
1040
|
-
try {
|
|
1041
|
-
prior = getTask(id);
|
|
1042
|
-
}
|
|
1043
|
-
catch { /* idempotent already-absent delete */ }
|
|
1044
|
-
const deleted = taskRoomService().deleteTask({
|
|
1096
|
+
const accepted = await taskRoomService(opts.configuration).requestTaskDeletion({
|
|
1045
1097
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
1046
1098
|
});
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1099
|
+
if (accepted.status === 'already_absent') {
|
|
1100
|
+
if (opts.json) {
|
|
1101
|
+
console.log(JSON.stringify({
|
|
1102
|
+
schema_version: 1, task_id: id, deleted: false, already_absent: true,
|
|
1103
|
+
}, null, 2));
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
console.log(renderMarkdownResult({
|
|
1107
|
+
icon: '🗑️', title: 'Task already absent',
|
|
1108
|
+
fields: [{ label: 'ID', value: id, kind: 'code' }],
|
|
1109
|
+
}));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const settled = await launchTaskDeleteWorker(id, opts.configuration);
|
|
1049
1113
|
if (opts.json) {
|
|
1050
|
-
console.log(JSON.stringify({
|
|
1114
|
+
console.log(JSON.stringify({
|
|
1115
|
+
schema_version: 1, task_id: id, accepted: true,
|
|
1116
|
+
deleted: settled.deleted, pending: settled.timedOut,
|
|
1117
|
+
}, null, 2));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (!settled.deleted) {
|
|
1121
|
+
console.log(renderMarkdownFailure({
|
|
1122
|
+
kind: 'pending', subject: `task delete ${id} ${id}`,
|
|
1123
|
+
detail: 'The deletion was accepted and cleanup is still settling.',
|
|
1124
|
+
action: `Re-run ours-fleet task delete ${id} ${id} or ours-fleet task recover ${id}.`,
|
|
1125
|
+
}));
|
|
1051
1126
|
return;
|
|
1052
1127
|
}
|
|
1053
1128
|
console.log(renderMarkdownResult({
|
|
1054
|
-
icon: '🗑️', title:
|
|
1129
|
+
icon: '🗑️', title: 'Task deleted',
|
|
1055
1130
|
fields: [{ label: 'ID', value: id, kind: 'code' }],
|
|
1056
1131
|
}));
|
|
1057
1132
|
}
|
|
@@ -1070,6 +1145,25 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1070
1145
|
const app = taskRoomService(opts.configuration);
|
|
1071
1146
|
const actor = { kind: 'local_control', surface: 'cli' };
|
|
1072
1147
|
const begin = await app.beginTaskRecovery({ actor, taskId: id });
|
|
1148
|
+
if (begin.kind === 'deletion_worker_required') {
|
|
1149
|
+
const settled = await launchTaskDeleteWorker(id, opts.configuration);
|
|
1150
|
+
const payload = { schema_version: 1, task_id: id, deleted: settled.deleted };
|
|
1151
|
+
if (opts.json) {
|
|
1152
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
console.log(settled.deleted
|
|
1156
|
+
? renderMarkdownResult({
|
|
1157
|
+
icon: '🗑️', title: 'Task deletion completed by recovery',
|
|
1158
|
+
fields: [{ label: 'ID', value: id, kind: 'code' }],
|
|
1159
|
+
})
|
|
1160
|
+
: renderMarkdownFailure({
|
|
1161
|
+
kind: 'pending', subject: `task recover ${id}`,
|
|
1162
|
+
detail: 'The task is pending deletion and cleanup is still settling.',
|
|
1163
|
+
action: `Run ours-fleet task delete ${id} ${id} to retry.`,
|
|
1164
|
+
}));
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1073
1167
|
const recovered = begin.kind === 'terminal_worker_required'
|
|
1074
1168
|
? await (async () => {
|
|
1075
1169
|
const settled = await launchTaskSettleWorker(id, opts.configuration);
|
|
@@ -1148,6 +1242,35 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1148
1242
|
dieTaskRoom(e);
|
|
1149
1243
|
}
|
|
1150
1244
|
});
|
|
1245
|
+
cOpt(taskCmd.command('_settle_delete <id>', { hidden: true }))
|
|
1246
|
+
.description('internal: settle a previously accepted task deletion')
|
|
1247
|
+
.option('--json', 'JSON output')
|
|
1248
|
+
.action(async (id, opts) => {
|
|
1249
|
+
try {
|
|
1250
|
+
const app = taskRoomService(opts.configuration);
|
|
1251
|
+
const result = await app.settleTaskDeletion({
|
|
1252
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
1253
|
+
});
|
|
1254
|
+
if (opts.json) {
|
|
1255
|
+
console.log(JSON.stringify({ schema_version: 1, ...result }, null, 2));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
console.log(renderMarkdownResult({
|
|
1259
|
+
icon: '🗑️', title: result.deleted ? 'Task deletion settled' : 'Task already absent',
|
|
1260
|
+
fields: [{ label: 'ID', value: id, kind: 'code' }],
|
|
1261
|
+
}));
|
|
1262
|
+
}
|
|
1263
|
+
catch (e) {
|
|
1264
|
+
await taskRoomService(opts.configuration).recordDeletionError({
|
|
1265
|
+
actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
|
|
1266
|
+
error: errorText(e),
|
|
1267
|
+
recoveryHint: `External delete worker failed. Retry task delete ${id} ${id}.`,
|
|
1268
|
+
}).catch(() => { });
|
|
1269
|
+
if (opts.json)
|
|
1270
|
+
die(e);
|
|
1271
|
+
dieTaskRoom(e);
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1151
1274
|
cOpt(taskCmd.command('_recover <id>', { hidden: true }))
|
|
1152
1275
|
.description('internal: settle and continue task recovery')
|
|
1153
1276
|
.option('--json', 'JSON output')
|
|
@@ -1156,6 +1279,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1156
1279
|
const actor = { kind: 'internal_worker', surface: 'cli' };
|
|
1157
1280
|
try {
|
|
1158
1281
|
const begin = await app.beginTaskRecovery({ actor, taskId: id });
|
|
1282
|
+
if (begin.kind === 'deletion_worker_required') {
|
|
1283
|
+
const settled = await app.settleTaskDeletion({ actor, taskId: id });
|
|
1284
|
+
console.log(JSON.stringify({ schema_version: 1, deletion: settled }, null, 2));
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1159
1287
|
const result = begin.kind === 'terminal_worker_required'
|
|
1160
1288
|
? await (async () => {
|
|
1161
1289
|
try {
|
|
@@ -1375,7 +1503,9 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1375
1503
|
],
|
|
1376
1504
|
sections: [
|
|
1377
1505
|
...(cowork.seats.length ? [{
|
|
1378
|
-
heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
|
|
1506
|
+
heading: 'Members', markdownItems: cowork.seats.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
|
|
1507
|
+
+ seatConfigurationSuffix(r?.member_seats
|
|
1508
|
+
.find(seat => seat.identity_cid === s.identity_cid)?.launch?.presentation)),
|
|
1379
1509
|
}] : []),
|
|
1380
1510
|
...(r ? roomStartupSections(r) : []),
|
|
1381
1511
|
],
|
|
@@ -1437,7 +1567,9 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1437
1567
|
],
|
|
1438
1568
|
sections: [{
|
|
1439
1569
|
heading: 'Members',
|
|
1440
|
-
...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
|
|
1570
|
+
...(members.length ? { markdownItems: members.map(s => `${markdownCode(s.identity_cid)} — ${markdownProse(s.role)} — ${markdownProse(s.seat_state)}`
|
|
1571
|
+
+ seatConfigurationSuffix(r?.member_seats
|
|
1572
|
+
.find(seat => seat.identity_cid === s.identity_cid)?.launch?.presentation)) }
|
|
1441
1573
|
: { items: ['No members found.'] }),
|
|
1442
1574
|
}],
|
|
1443
1575
|
}));
|
|
@@ -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
|
}
|
|
@@ -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>;
|