@ours.network/fleet 2.3.3 → 2.3.5

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/spawn.js CHANGED
@@ -2,6 +2,7 @@ import { taskSupervisorMayRun } from './task-supervision.js';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { storeRoomSecret, storeTemporaryLaunch, preparePermanentAssignment } from './agent-ours/service.js';
4
4
  import { ensureWorkspace } from './rooms-tasks/workspace.js';
5
+ import { assertCwdNotInRemovedTaskFolder } from './rooms-tasks/workspace-removal.js';
5
6
  import { spawn as spawnChild } from 'node:child_process';
6
7
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
8
  import { join } from 'node:path';
@@ -324,6 +325,7 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
324
325
  if (o.isolationFile)
325
326
  readIsolationFile(o.isolationFile); // fail before reserving
326
327
  const prepared = resolvedSpawn(o); // canonical validation before mutation
328
+ assertCwdNotInRemovedTaskFolder(prepared.role.cwd);
327
329
  if ((o.taskOwner || o.roomMemberStartup?.task_id) && !getAdapter(prepared.role.harness).supportsResume)
328
330
  throw Error('TASK_HARNESS_RESUME_UNSUPPORTED: task agents require a resumable harness');
329
331
  if (o.roomMemberStartup?.workspace) {
@@ -23,6 +23,7 @@ import { AuditSink } from './audit.js';
23
23
  import { WebAuth } from './auth.js';
24
24
  import { FleetEventBus } from './events.js';
25
25
  import { ROLE_NAME_RE } from '../config.js';
26
+ import { workingFilesSummary } from '../rooms-tasks/workspace-removal.js';
26
27
  import { TaskListError } from '../rooms-tasks/task-lists.js';
27
28
  import { acknowledgeTaskProgress, acknowledgeTaskReview, contributionUnread, resolveTaskRef, TaskStateError } from '../rooms-tasks/task-state.js';
28
29
  import { CONTRIBUTION_CUTOFF_MAX, CONTRIBUTION_PAGE_MAX, listTaskContributions, readTaskContributions } from '../rooms-tasks/task-contributions.js';
@@ -390,7 +391,7 @@ export async function buildWebServer(services, boundary, options = {}) {
390
391
  /** Browser task payloads carry Progress/To review counts; bodies have their own routes. */
391
392
  const wireTask = (task) => {
392
393
  const { progress: _progress, review: _review, member_naming: _naming, ...rest } = task;
393
- return { ...rest, ...taskContributionSummary(task) };
394
+ return { ...rest, ...taskContributionSummary(task), working_files: workingFilesSummary(task) };
394
395
  };
395
396
  const wireTaskResult = (result) => {
396
397
  const value = result;
@@ -625,6 +626,14 @@ export async function buildWebServer(services, boundary, options = {}) {
625
626
  recovery: `Repeat DELETE /api/v1/tasks/${submitted}?confirm=${submitted}.`,
626
627
  };
627
628
  });
629
+ // Removes only the Done task's own working folder; the task, Progress, To review files and chat history stay.
630
+ app.post('/api/v1/tasks/:id/working-files/remove', async (request) => {
631
+ auth.authenticate(request, true);
632
+ const result = await taskApi(() => requireTaskRooms().removeTaskWorkingFiles({
633
+ actor: { kind: 'local_control', surface: 'web' }, taskId: taskRef(request),
634
+ }));
635
+ return { task: wireTask(result.task), result: result.result, mentions: result.mentions };
636
+ });
628
637
  app.post('/api/v1/tasks', async (request, reply) => {
629
638
  auth.authenticate(request, true);
630
639
  const body = request.body;
@@ -30,6 +30,7 @@ complete internal worktree registries, including dirty and stale owned entries.
30
30
  | Operation | Workspace and execution artifacts | Member runtime evidence |
31
31
  | --- | --- | --- |
32
32
  | Complete, fail, cancel | Retained while Task is recorded | Retained in Fleet recovery archives |
33
+ | Explicit working-files removal (Done Task only) | Task workspace removed; Task, Progress and To review files retained | Retained in Fleet recovery archives |
33
34
  | Linked Room retirement or deletion | Task workspace retained | Retained |
34
35
  | Internal standalone Room retirement | Room workspace retained | Retained |
35
36
  | Explicit Task deletion | Removed after member retirement, with the task's retained To review files | All proven owned retired launches removed |
@@ -39,7 +40,8 @@ Internal room close means retirement, not destructive Task deletion.
39
40
  The existing CLI `room close ID ID` remains a deprecated alias for explicit
40
41
  `room delete ID ID`: it deletes standalone room-owned artifacts, but always
41
42
  retains a linked Task workspace. Its repeated-ID confirmation is unchanged. To remove a Task's
42
- artifacts, explicitly delete the Task. Cancellation is not deletion. Automatic
43
+ artifacts, explicitly delete the Task, or, for a Done Task, remove only its working files
44
+ (`task remove-files ID`, below). Cancellation is not deletion. Automatic
43
45
  compatibility cleanup of legacy closed Rooms skips records with owned workspaces.
44
46
  These choices reconcile close/remove wording with the requirement that completed,
45
47
  recorded Tasks retain all artifacts.
@@ -223,3 +225,51 @@ requires a supported Home Screen installation and granted device/browser permiss
223
225
  reopen the workspace to reconcile an expired subscription and explicitly re-enable
224
226
  permission if it was revoked. Every received push must display a notification to
225
227
  honor Safari's user-visible push requirement.
228
+
229
+ ## Removing a Done Task's working files
230
+
231
+ `ours-fleet task remove-files ID` (API `POST /api/v1/tasks/ID/working-files/remove`)
232
+ frees disk space without deleting the Task. It removes only the recorded owned
233
+ workspace through the same guarded deletion as Task deletion (marker, Git
234
+ registration, symlink and mount checks, token tombstone). It never collects or
235
+ erases recovery archives, member artifacts or the To review store, and never
236
+ closes rooms or stops agents. It runs under the task operation lock and refuses
237
+ with the reason unless all of these hold:
238
+
239
+ - the Task is Done with its completion settled, and is not being deleted;
240
+ - no room record names the Task or its workspace, and every room record is readable;
241
+ - a layout run exists, is closed, is not uncertain, and has retired every owned participant;
242
+ - no supervisor registration belongs to the Task;
243
+ - no unarchived temporary agent state belongs to the Task or works in the
244
+ folder (cwd or startup workspace, literally or through symlinks), and all
245
+ such state is readable;
246
+ - no permanent agent's recorded `.cwd` or configured cwd is in the folder.
247
+
248
+ The intent is recorded before a final in-use scan. Agent state that names a
249
+ task folder is created under that folder's use lock and refuses a folder
250
+ whose removal began, so a concurrent launch is either seen by the scan or
251
+ refused. After the folder is renamed to its tombstone and before anything
252
+ is removed, every process's cwd, root and open files are checked (Linux
253
+ `/proc`). A process that entered the folder earlier restores the folder and
254
+ refuses the removal. Platforms without this check refuse the removal.
255
+ A same-user process whose cwd and descriptors are unreadable (for example
256
+ non-dumpable, or in a container) refuses the removal unless it is a zombie.
257
+ Nothing readable proves such a process holds nothing in the folder: a
258
+ retained cwd or descriptor survives namespace and chroot changes, and a user
259
+ service manager may keep descriptors in its store. The refusal names each
260
+ such process by pid and kernel name (never its command line), with
261
+ read-only context such as "own mount namespace without the folder".
262
+ Another user's unreadable process is skipped when a private ancestor closes
263
+ the folder to it. Superuser processes are outside the check. On hosts that
264
+ run rootless containers or a user service manager, this strict check can
265
+ make removal unavailable while those processes run. Only a folder
266
+ that this run renamed intact is restored. A partly removed tombstone from an
267
+ earlier run stays in place with the refusal recorded, so a later retry can
268
+ finish it.
269
+
270
+ The Task records `workspace_removal` (started, completed, error), shown as
271
+ `working_files` in API payloads. A retry resumes an unfinished tombstone, and
272
+ repeating a finished removal changes nothing. A folder already missing on the
273
+ first run is recorded as such. A create replay never recreates the folder, and
274
+ To review files can no longer be attached from it. Explicit Task deletion
275
+ still works and first finishes an interrupted removal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -50,7 +50,7 @@
50
50
  "@agentclientprotocol/sdk": "^1.3.0",
51
51
  "@fastify/websocket": "^11.2.0",
52
52
  "@ours.network/cli": "4.2.0",
53
- "@ours.network/mcp": "2.0.3",
53
+ "@ours.network/mcp": "2.0.4",
54
54
  "@ours.network/sdk": "5.2.0",
55
55
  "commander": "^12.1.0",
56
56
  "fastify": "^5.4.0",