@ours.network/fleet 2.3.2 → 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.
- package/dist/application/task-room-service.d.ts +8 -0
- package/dist/application/task-room-service.js +15 -0
- package/dist/build-info.json +4 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +22 -0
- package/dist/ops.js +6 -0
- package/dist/rooms-tasks/cli.js +24 -0
- package/dist/rooms-tasks/deletion.js +4 -0
- package/dist/rooms-tasks/task-contributions.d.ts +50 -0
- package/dist/rooms-tasks/task-contributions.js +169 -0
- package/dist/rooms-tasks/task-review.js +2 -0
- package/dist/rooms-tasks/task-state.d.ts +18 -1
- package/dist/rooms-tasks/task-state.js +56 -1
- package/dist/rooms-tasks/types.d.ts +17 -0
- package/dist/rooms-tasks/workspace-removal.d.ts +85 -0
- package/dist/rooms-tasks/workspace-removal.js +633 -0
- package/dist/rooms-tasks/workspace.d.ts +9 -1
- package/dist/rooms-tasks/workspace.js +13 -1
- package/dist/spawn.js +2 -0
- package/dist/web/server.js +44 -1
- package/docs/task-progress-review.md +2 -0
- package/docs/task-workspaces.md +51 -1
- package/package.json +1 -1
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) {
|
package/dist/web/server.js
CHANGED
|
@@ -23,8 +23,10 @@ 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';
|
|
29
|
+
import { CONTRIBUTION_CUTOFF_MAX, CONTRIBUTION_PAGE_MAX, listTaskContributions, readTaskContributions } from '../rooms-tasks/task-contributions.js';
|
|
28
30
|
import { MissionKeyError } from '../rooms-tasks/mission-keys.js';
|
|
29
31
|
import { appendTaskProgress, getTaskReview, taskProgressSnapshot, publicProgressEntry, publicReview, readTaskReviewArtifact, setTaskReviewMarkdown, taskContributionSummary, TaskReviewError, } from '../rooms-tasks/task-review.js';
|
|
30
32
|
import { isProvider } from '../subscriptions/store.js';
|
|
@@ -389,7 +391,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
389
391
|
/** Browser task payloads carry Progress/To review counts; bodies have their own routes. */
|
|
390
392
|
const wireTask = (task) => {
|
|
391
393
|
const { progress: _progress, review: _review, member_naming: _naming, ...rest } = task;
|
|
392
|
-
return { ...rest, ...taskContributionSummary(task) };
|
|
394
|
+
return { ...rest, ...taskContributionSummary(task), working_files: workingFilesSummary(task) };
|
|
393
395
|
};
|
|
394
396
|
const wireTaskResult = (result) => {
|
|
395
397
|
const value = result;
|
|
@@ -624,6 +626,14 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
624
626
|
recovery: `Repeat DELETE /api/v1/tasks/${submitted}?confirm=${submitted}.`,
|
|
625
627
|
};
|
|
626
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
|
+
});
|
|
627
637
|
app.post('/api/v1/tasks', async (request, reply) => {
|
|
628
638
|
auth.authenticate(request, true);
|
|
629
639
|
const body = request.body;
|
|
@@ -686,6 +696,39 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
686
696
|
reply.code(201);
|
|
687
697
|
return { entry: publicProgressEntry(result.entry), replayed: result.replayed };
|
|
688
698
|
});
|
|
699
|
+
/**
|
|
700
|
+
* Progress and To review of every task, newest first, for the notification list. The first page
|
|
701
|
+
* (no `before`) also carries the cutoff a bulk read may acknowledge up to: each task with unread
|
|
702
|
+
* items as it was read, one task at a time (per task, not one atomic instant across tasks).
|
|
703
|
+
*/
|
|
704
|
+
app.get('/api/v1/contributions', async (request) => {
|
|
705
|
+
auth.authenticate(request);
|
|
706
|
+
const query = request.query;
|
|
707
|
+
if (query.unread !== undefined && !['true', 'false'].includes(query.unread))
|
|
708
|
+
throw new FleetError('invalid_request', 'unread must be true or false');
|
|
709
|
+
if (query.limit !== undefined && !/^\d{1,3}$/.test(query.limit))
|
|
710
|
+
throw new FleetError('invalid_request', `limit must be 1-${CONTRIBUTION_PAGE_MAX}`);
|
|
711
|
+
const page = await reviewApi(() => listTaskContributions({
|
|
712
|
+
...(query.before !== undefined ? { before: query.before } : {}),
|
|
713
|
+
...(query.limit !== undefined ? { limit: Number(query.limit) } : {}),
|
|
714
|
+
unread: query.unread === 'true',
|
|
715
|
+
}));
|
|
716
|
+
const { cutoff, ...rest } = page;
|
|
717
|
+
return query.before === undefined ? page : rest;
|
|
718
|
+
});
|
|
719
|
+
/** Bulk read up to a captured cutoff, at most `CONTRIBUTION_CUTOFF_MAX` tasks per request, with a result per task. */
|
|
720
|
+
app.post('/api/v1/contributions/read', async (request) => {
|
|
721
|
+
auth.authenticate(request, true);
|
|
722
|
+
const cutoff = request.body?.cutoff;
|
|
723
|
+
const valid = (item) => !!item && typeof item === 'object' && Object.keys(item).length === 4
|
|
724
|
+
&& typeof item.task_id === 'string' && /^[0-9a-z]{9}[0-9a-f]{8}$/.test(item.task_id) && Number.isSafeInteger(item.progress) && item.progress >= 0
|
|
725
|
+
&& (item.progress_last === null || (typeof item.progress_last === 'string' && item.progress_last.length <= 64))
|
|
726
|
+
&& Number.isSafeInteger(item.review) && item.review >= 0;
|
|
727
|
+
if (!Array.isArray(cutoff) || cutoff.length > CONTRIBUTION_CUTOFF_MAX || !cutoff.every(valid)
|
|
728
|
+
|| new Set(cutoff.map(item => item.task_id)).size !== cutoff.length)
|
|
729
|
+
throw new FleetError('invalid_request', `cutoff must list at most ${CONTRIBUTION_CUTOFF_MAX} distinct tasks as captured`);
|
|
730
|
+
return { results: await reviewApi(() => readTaskContributions(cutoff)) };
|
|
731
|
+
});
|
|
689
732
|
/** Acknowledge Progress entries the browser actually showed; returns the task's remaining unread. */
|
|
690
733
|
app.post('/api/v1/tasks/:id/progress/read', async (request) => {
|
|
691
734
|
auth.authenticate(request, true);
|
|
@@ -155,6 +155,8 @@ workspace-device bearer. `GET /api/v1/meta` reports API minor version 1.
|
|
|
155
155
|
| `GET /tasks/:id/review` | `{review: {revision, markdown, updated_at, updated_by, artifacts: [{id, name, mime, size, sha256, added_at, added_by}]}}` or `{review: null}`. |
|
|
156
156
|
| `PUT /tasks/:id/review` | `{markdown, expectedMarkdown}`; 409 when the Markdown changed meanwhile. |
|
|
157
157
|
| `GET /tasks/:id/review/artifacts/:artifactId` | The retained bytes as `application/octet-stream` attachment with `Content-Security-Policy: default-src 'none'; sandbox`, `nosniff` and `no-store`. Another task's artifact ID is 404. |
|
|
158
|
+
| `GET /contributions?limit=1-100&before=&unread=true` | Every task's Progress entries and current To review revision for the notification list, newest first: `{items: [{id, kind, task_id, mission_key?, task_title, task_state, at, excerpt, unread, entry_id? \| revision?}], next_cursor, unread_total, unavailable}`. The cursor is a position, valid after its item is read or deleted. Tasks pending deletion are left out; `unavailable` counts unreadable task records. The first page also has `cutoff: [{task_id, progress, progress_last, review}]` for every task with unread items, each captured from that task's own read (per task, not one instant across tasks). |
|
|
159
|
+
| `POST /contributions/read` | `{cutoff}` with at most 200 distinct tasks from a first page: reads each task's first `progress` entries and review revision `review`, never later ones, one task change each. `{results: [{task_id, status}]}` in request order, `status` `read` (with `read`: items that were unread and are read now), `changed` (the task no longer matches its cutoff; nothing written), `gone`, `deleting` or `failed`. |
|
|
158
160
|
|
|
159
161
|
The HTTP API has no upload route: the browser never submits a file path.
|
|
160
162
|
|
package/docs/task-workspaces.md
CHANGED
|
@@ -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
|
|
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
|
+
"version": "2.3.4",
|
|
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",
|