@ours.network/fleet 2.3.3 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import type { ArchivedMemberAbsence } from './types.js';
2
- import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase, TaskNotificationEvent } from './types.js';
2
+ import type { TaskRecord, TaskState, TaskOrigin, TaskTemplateRef, TaskOutcome, TaskMemberRole, TaskTerminalIntent, TaskDeletionActor, TaskDeletionMemberPhase, TaskNotificationEvent, TaskWorkspaceRemoval } from './types.js';
3
3
  import { type LabelCandidate } from './member-naming.js';
4
4
  import { type TaskNotificationManifest } from './notification-manifest.js';
5
5
  export declare const tasksDir: () => string;
@@ -295,4 +295,9 @@ export declare function updateTaskContribution<T>(id: string, change: (task: Tas
295
295
  changed: boolean;
296
296
  }): T;
297
297
  export declare function updateTaskBrief(id: string, brief: string, expectedBrief: string): TaskRecord;
298
+ /**
299
+ * Record a step of an explicit working-folder removal. Callers hold the task
300
+ * operation lock; lifecycle state is untouched.
301
+ */
302
+ export declare function updateTaskWorkspaceRemoval(id: string, change: (removal: TaskWorkspaceRemoval | undefined) => TaskWorkspaceRemoval | undefined): TaskRecord;
298
303
  export {};
@@ -518,7 +518,8 @@ export function createTask(input) {
518
518
  const fresh = readTask(outcome.taskId);
519
519
  if (outcome.kind === 'existing')
520
520
  assertNoPendingDeletion(fresh);
521
- if (fresh.workspace)
521
+ // A replayed create never recreates a folder the Owner removed.
522
+ if (fresh.workspace && !fresh.workspace_removal)
522
523
  ensureWorkspace(fresh.workspace);
523
524
  return fresh;
524
525
  });
@@ -1502,3 +1503,21 @@ export function updateTaskBrief(id, brief, expectedBrief) {
1502
1503
  return task;
1503
1504
  });
1504
1505
  }
1506
+ /**
1507
+ * Record a step of an explicit working-folder removal. Callers hold the task
1508
+ * operation lock; lifecycle state is untouched.
1509
+ */
1510
+ export function updateTaskWorkspaceRemoval(id, change) {
1511
+ assertCanonicalTaskId(id);
1512
+ return withTaskLock(id, () => {
1513
+ const task = readTask(id);
1514
+ assertNoPendingDeletion(task);
1515
+ const next = change(task.workspace_removal);
1516
+ if (next)
1517
+ task.workspace_removal = next;
1518
+ else
1519
+ delete task.workspace_removal;
1520
+ writeTask(task);
1521
+ return task;
1522
+ });
1523
+ }
@@ -99,6 +99,21 @@ export interface TaskDeletionIntent {
99
99
  first_failure?: string;
100
100
  first_recovery_hint?: string;
101
101
  }
102
+ /**
103
+ * Explicit removal of a Done task's owned working folder. The task record,
104
+ * Progress, To review files and agent archives stay; only the folder goes.
105
+ * `started_at` without `completed_at` means removal began and has not been
106
+ * confirmed: it may still be running or have been interrupted.
107
+ */
108
+ export interface TaskWorkspaceRemoval {
109
+ started_at: string;
110
+ actor: TaskDeletionActor;
111
+ completed_at?: string;
112
+ /** The recorded folder was already absent when removal first ran. */
113
+ already_missing?: true;
114
+ error?: string;
115
+ error_at?: string;
116
+ }
102
117
  export interface TaskLayoutLink {
103
118
  name: string;
104
119
  /** Deterministic `task-<task_id>` Room Layout run ID. */
@@ -173,6 +188,8 @@ export interface TaskRecord {
173
188
  /** Rooms whose notifications belong to this task; only grows. */
174
189
  notification_manifest?: import('./notification-manifest.js').TaskNotificationManifest;
175
190
  workspace?: import('./workspace.js').OwnedWorkspace;
191
+ /** Set once the Owner asked to remove the working folder; the record is never recreated from it. */
192
+ workspace_removal?: TaskWorkspaceRemoval;
176
193
  task_id: string;
177
194
  /**
178
195
  * Immutable human mission key (`MSN-1`), issued once from a durable
@@ -0,0 +1,85 @@
1
+ import type { TaskDeletionActor, TaskRecord } from './types.js';
2
+ /**
3
+ * Explicit Owner removal of a Done task's working folder. Only the exact
4
+ * recorded task-owned folder is removed, through the guarded workspace
5
+ * deletion (marker, Git registration, symlink and mount fences). The task
6
+ * record, Progress, To review files (a separate store), agent archives and
7
+ * rooms' history are never touched. Every precondition is re-read under the
8
+ * task-operation lock that serializes terminal settlement, deletion and
9
+ * recovery; anything that cannot be proven idle is refused, never stopped.
10
+ */
11
+ export type WorkspaceRemovalResult = 'removed' | 'already_removed' | 'already_missing';
12
+ export interface WorkspaceRemovalOutcome {
13
+ task: TaskRecord;
14
+ result: WorkspaceRemovalResult;
15
+ /** Retained text fields that mention the removed folder; those paths no longer resolve. */
16
+ mentions: string[];
17
+ }
18
+ export interface WorkspaceRemovalDeps {
19
+ /** Working directories of configured permanent agents; undefined when the configuration is unreadable. */
20
+ permanentCwds?: readonly (string | undefined)[];
21
+ /** Test seam: processes whose cwd, root or open files are inside `path`, or cannot be read. */
22
+ processesUsing?(path: string, context: ProcessCheckContext): ProcessUse;
23
+ }
24
+ /**
25
+ * Every process whose cwd, root or open file descriptor is inside `path`, or
26
+ * whose use cannot be excluded. Only Linux exposes this without extra tools;
27
+ * elsewhere removal is refused. When a process's entries are unreadable (for
28
+ * example non-dumpable, or in a container), it is excluded only by evidence:
29
+ * it is a zombie, its mount table cannot reach the folder's filesystem
30
+ * location, it is the user's service manager (attested by the system manager)
31
+ * whose own records name nothing in the folder, or it is that manager's PAM
32
+ * helper started before the folder existed; otherwise it is reported.
33
+ * Other users' unreadable processes are skipped when a private ancestor
34
+ * closes the folder to them; a superuser process is outside this check.
35
+ */
36
+ /** A same-user process whose cwd and descriptors cannot be read. */
37
+ export interface UnverifiedProcess {
38
+ token: string;
39
+ description: string;
40
+ }
41
+ export interface ProcessUse {
42
+ /** Processes proven to use the folder. */
43
+ users: string[];
44
+ /** Processes whose use can be neither proven nor excluded. */
45
+ unverified: UnverifiedProcess[];
46
+ }
47
+ export interface ProcessCheckContext {
48
+ /** The folder's recorded path; configuration names it, not its tombstone. */
49
+ original: string;
50
+ /** When the folder came into existence (ms since the epoch). */
51
+ createdAt: number;
52
+ }
53
+ export declare function processesUsing(path: string, context: ProcessCheckContext): ProcessUse;
54
+ declare const run: (command: string, args: string[]) => string;
55
+ /**
56
+ * Whether the user service manager may use the folder, from its own read-only
57
+ * records: its attested working directory, socket units' paths and every
58
+ * file descriptor store (systemd 254+). Undefined when none names the folder;
59
+ * otherwise the reason, including any record that cannot be read.
60
+ */
61
+ export declare function userManagerUse(uid: number, roots: string[], exec?: typeof run): string | undefined;
62
+ /** True when some mount in the table exposes the filesystem location, or anything under it. */
63
+ export declare function reaches(mountinfo: string, target: {
64
+ device: string;
65
+ inside: string;
66
+ }): boolean;
67
+ export declare function assertTaskWorkspaceRemovable(task: TaskRecord, deps: WorkspaceRemovalDeps): void;
68
+ export declare function removeTaskWorkspace(taskId: string, actor: TaskDeletionActor, deps?: WorkspaceRemovalDeps): Promise<WorkspaceRemovalOutcome>;
69
+ export interface WorkingFilesSummary {
70
+ /** `removing`: removal began and is not confirmed — still running or interrupted; retry resumes it. */
71
+ state: 'none' | 'present' | 'removing' | 'failed' | 'removed';
72
+ started_at?: string;
73
+ removed_at?: string;
74
+ error?: string;
75
+ }
76
+ /** Record-only readback for task payloads; never touches the filesystem. */
77
+ export declare function workingFilesSummary(task: TaskRecord): WorkingFilesSummary;
78
+ export declare function assertCwdNotInRemovedTaskFolder(cwd: string | undefined): void;
79
+ /**
80
+ * Launch fence: an agent never starts in, and so never recreates, the folder
81
+ * of a task whose working files were removed. Creating its state under the
82
+ * folder's use lock makes it visible to a concurrent removal's scan.
83
+ */
84
+ export declare function withTaskFolderUseFence<T>(cwd: string | undefined, work: () => T): T;
85
+ export {};