@ours.network/fleet 1.1.0-nightly.22 → 1.1.0-nightly.24
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.js +26 -4
- package/dist/build-info.json +4 -4
- package/dist/owner-channel/channel.d.ts +2 -0
- package/dist/owner-channel/channel.js +41 -23
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/runner.js +45 -17
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from
|
|
|
16
16
|
import { withFileLock } from '../atomic-file.js';
|
|
17
17
|
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
18
18
|
import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
19
|
+
import { deriveTaskRoomName } from '../rooms-tasks/task-room-name.js';
|
|
19
20
|
export class TaskRoomApplicationError extends Error {
|
|
20
21
|
code;
|
|
21
22
|
fields;
|
|
@@ -399,8 +400,28 @@ export class TaskRoomApplicationService {
|
|
|
399
400
|
return {
|
|
400
401
|
kind: TASK_TERMINAL_STATES.includes(task.state) ? 'terminal' : 'no_op', task, room, issues,
|
|
401
402
|
};
|
|
402
|
-
if (!room)
|
|
403
|
-
|
|
403
|
+
if (!room) {
|
|
404
|
+
if (!task.template)
|
|
405
|
+
return {
|
|
406
|
+
kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
|
|
407
|
+
};
|
|
408
|
+
try {
|
|
409
|
+
const template = task.execution_plan?.snapshot ?? this.existingTemplate(cfg, task);
|
|
410
|
+
const roomPolicy = task.execution_plan
|
|
411
|
+
? storedRoomLaunchPolicy(task.execution_plan.room_policy)
|
|
412
|
+
: resolveRoomLaunchPolicy(template, undefined);
|
|
413
|
+
await this.provisionRoom(cfg, task, template, created => {
|
|
414
|
+
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
415
|
+
}, undefined, roomPolicy);
|
|
416
|
+
task = readTask(task.task_id);
|
|
417
|
+
room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
418
|
+
return { kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }] };
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
|
|
422
|
+
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
404
425
|
if (room.provisioning_detail === 'waiting_cowork')
|
|
405
426
|
issues.push({ code: 'waiting_cowork' });
|
|
406
427
|
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
@@ -829,6 +850,7 @@ export class TaskRoomApplicationService {
|
|
|
829
850
|
throw new ConfigError('rooms: configuration is required before creating or querying rooms');
|
|
830
851
|
return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
|
|
831
852
|
})();
|
|
853
|
+
const roomName = task.task_id ? deriveTaskRoomName(task.title, task.task_id) : task.title;
|
|
832
854
|
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
833
855
|
let launchTemplate;
|
|
834
856
|
let room;
|
|
@@ -847,13 +869,13 @@ export class TaskRoomApplicationService {
|
|
|
847
869
|
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
848
870
|
}
|
|
849
871
|
const created = await cowork.createRoom({
|
|
850
|
-
room_name:
|
|
872
|
+
room_name: roomName, goal: task.goal?.trim() || task.title,
|
|
851
873
|
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
852
874
|
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
853
875
|
anonymous: policy.anonymous,
|
|
854
876
|
});
|
|
855
877
|
const record = createRoomRecord({
|
|
856
|
-
room_id: created.room_id, room_name:
|
|
878
|
+
room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
|
|
857
879
|
task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
|
|
858
880
|
});
|
|
859
881
|
onCreated(record);
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.1.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.1.0-nightly.24",
|
|
3
|
+
"buildId": "30fec1b573b6",
|
|
4
|
+
"commit": "93f6e23dd6eb3642ee12bc01cba53a2aba838ab1",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-02T11:45:37.603Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
|
@@ -225,6 +225,8 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
225
225
|
resourceIds?: Record<string, string>;
|
|
226
226
|
presentations?: FleetAuditPresentation[];
|
|
227
227
|
}): Promise<FleetAuditAttempt>;
|
|
228
|
+
private flushPendingFleetCommandAudits;
|
|
229
|
+
private deliverFleetCommandOutcome;
|
|
228
230
|
recover(epoch: string): Promise<void>;
|
|
229
231
|
private recoveryStage;
|
|
230
232
|
private writeShutdownState;
|
|
@@ -199,6 +199,7 @@ export class OwnerChannel {
|
|
|
199
199
|
}
|
|
200
200
|
this.startedOnce = true;
|
|
201
201
|
this.ready = true;
|
|
202
|
+
await this.flushPendingFleetCommandAudits();
|
|
202
203
|
// Do not make role startup wait for an old owner request to finish a turn.
|
|
203
204
|
// watchLoop itself drains before every establishment, including this first
|
|
204
205
|
// one, so there is no drain-to-tip race.
|
|
@@ -301,33 +302,50 @@ export class OwnerChannel {
|
|
|
301
302
|
return attempt;
|
|
302
303
|
if (attempt.outcome?.delivery === 'uncertain')
|
|
303
304
|
return attempt;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
uncertain = true;
|
|
323
|
-
this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
|
|
324
|
-
}
|
|
325
|
-
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
|
|
326
|
-
return attempt;
|
|
305
|
+
// Before the Owner sink is attached, ordinary commands have nothing to
|
|
306
|
+
// deliver. Lifecycle presentations remain in the never-attempted
|
|
307
|
+
// `sending` state and are flushed exactly once when start() makes the
|
|
308
|
+
// authenticated sink ready. A restart converts them to `uncertain`, so
|
|
309
|
+
// effects are never replayed after an unproven delivery boundary.
|
|
310
|
+
if (!attempt.outcome?.presentations?.length)
|
|
311
|
+
return this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
|
|
312
|
+
if (!this.ready)
|
|
313
|
+
return attempt;
|
|
314
|
+
return this.deliverFleetCommandOutcome(attempt);
|
|
327
315
|
});
|
|
328
316
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
329
317
|
return run;
|
|
330
318
|
}
|
|
319
|
+
async flushPendingFleetCommandAudits() {
|
|
320
|
+
for (const attempt of this.commandAudits.list())
|
|
321
|
+
if (attempt.outcome?.delivery === 'sending')
|
|
322
|
+
await this.deliverFleetCommandOutcome(attempt);
|
|
323
|
+
}
|
|
324
|
+
async deliverFleetCommandOutcome(attempt) {
|
|
325
|
+
let uncertain = false;
|
|
326
|
+
try {
|
|
327
|
+
for (const event of attempt.outcome?.presentations ?? []) {
|
|
328
|
+
try {
|
|
329
|
+
await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (error instanceof DuplicateSendError) {
|
|
333
|
+
if (error.status !== 'delivered')
|
|
334
|
+
uncertain = true;
|
|
335
|
+
this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
uncertain = true;
|
|
344
|
+
this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
|
|
345
|
+
}
|
|
346
|
+
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
|
|
347
|
+
return attempt;
|
|
348
|
+
}
|
|
331
349
|
recover(epoch) {
|
|
332
350
|
if (this.stopping)
|
|
333
351
|
return Promise.reject(new Error('owner channel is stopping'));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork room identities are named `ours-cowork:<room name>` and both the
|
|
3
|
+
* room and identity contracts count NFC-normalized Unicode code points.
|
|
4
|
+
* Keeping the complete task ID inside that shared budget makes every derived
|
|
5
|
+
* name stable, directly correlated, and unique even when the title is empty,
|
|
6
|
+
* sanitized, or truncated.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COWORK_ROOM_IDENTITY_PREFIX = "ours-cowork:";
|
|
9
|
+
export declare const COWORK_IDENTITY_NAME_MAX_CODE_POINTS = 64;
|
|
10
|
+
export declare const TASK_ROOM_NAME_MAX_CODE_POINTS: number;
|
|
11
|
+
/** Canonical Cowork room name for a Fleet task; never mutates the task title. */
|
|
12
|
+
export declare function deriveTaskRoomName(title: string, taskId: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cowork room identities are named `ours-cowork:<room name>` and both the
|
|
3
|
+
* room and identity contracts count NFC-normalized Unicode code points.
|
|
4
|
+
* Keeping the complete task ID inside that shared budget makes every derived
|
|
5
|
+
* name stable, directly correlated, and unique even when the title is empty,
|
|
6
|
+
* sanitized, or truncated.
|
|
7
|
+
*/
|
|
8
|
+
export const COWORK_ROOM_IDENTITY_PREFIX = 'ours-cowork:';
|
|
9
|
+
export const COWORK_IDENTITY_NAME_MAX_CODE_POINTS = 64;
|
|
10
|
+
export const TASK_ROOM_NAME_MAX_CODE_POINTS = COWORK_IDENTITY_NAME_MAX_CODE_POINTS
|
|
11
|
+
- Array.from(COWORK_ROOM_IDENTITY_PREFIX).length;
|
|
12
|
+
const TASK_ID_PATTERN = /^[0-9a-z]{9}[0-9a-f]{8}$/;
|
|
13
|
+
const FORBIDDEN = /[\\/\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
|
|
14
|
+
const DASH_SEPARATOR = /\p{Pd}/u;
|
|
15
|
+
const WHITESPACE = /\s/u;
|
|
16
|
+
const FALLBACK_TITLE = 'Task';
|
|
17
|
+
function sanitizeTitle(title) {
|
|
18
|
+
const normalized = title.normalize('NFC');
|
|
19
|
+
let result = '';
|
|
20
|
+
let pending;
|
|
21
|
+
for (const character of normalized) {
|
|
22
|
+
if (FORBIDDEN.test(character) || DASH_SEPARATOR.test(character)) {
|
|
23
|
+
pending = 'separator';
|
|
24
|
+
}
|
|
25
|
+
else if (WHITESPACE.test(character)) {
|
|
26
|
+
if (pending !== 'separator')
|
|
27
|
+
pending = 'space';
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
if (result && pending)
|
|
31
|
+
result += pending === 'separator' ? ' - ' : ' ';
|
|
32
|
+
result += character;
|
|
33
|
+
pending = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/** Truncate to a code-point budget without cutting an extended grapheme. */
|
|
39
|
+
function graphemeBound(value, maxCodePoints) {
|
|
40
|
+
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
41
|
+
let result = '';
|
|
42
|
+
let length = 0;
|
|
43
|
+
for (const { segment } of segmenter.segment(value)) {
|
|
44
|
+
const segmentLength = Array.from(segment).length;
|
|
45
|
+
if (length + segmentLength > maxCodePoints)
|
|
46
|
+
break;
|
|
47
|
+
result += segment;
|
|
48
|
+
length += segmentLength;
|
|
49
|
+
}
|
|
50
|
+
return result.trimEnd();
|
|
51
|
+
}
|
|
52
|
+
/** Canonical Cowork room name for a Fleet task; never mutates the task title. */
|
|
53
|
+
export function deriveTaskRoomName(title, taskId) {
|
|
54
|
+
if (!TASK_ID_PATTERN.test(taskId))
|
|
55
|
+
throw new Error(`invalid canonical task ID: ${taskId}`);
|
|
56
|
+
const suffix = ` [${taskId}]`;
|
|
57
|
+
const titleBudget = TASK_ROOM_NAME_MAX_CODE_POINTS - Array.from(suffix).length;
|
|
58
|
+
const readable = sanitizeTitle(title);
|
|
59
|
+
const bounded = graphemeBound(readable || FALLBACK_TITLE, titleBudget) || FALLBACK_TITLE;
|
|
60
|
+
return `${bounded}${suffix}`;
|
|
61
|
+
}
|
package/dist/runner.js
CHANGED
|
@@ -26,6 +26,7 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
|
|
|
26
26
|
import { effectivePermissionMode } from './permissions.js';
|
|
27
27
|
import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
|
|
28
28
|
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
|
|
29
|
+
import { FleetCommandAuditStore } from './fleet-command-audit.js';
|
|
29
30
|
export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
|
|
30
31
|
export class SupervisorRecycleRequiredError extends Error {
|
|
31
32
|
code = SUPERVISOR_RECYCLE_REQUIRED;
|
|
@@ -70,6 +71,31 @@ const defaultDeps = () => ({
|
|
|
70
71
|
});
|
|
71
72
|
const MONITOR_OWNER_FILE = '.monitor-owner';
|
|
72
73
|
const OBSOLETE_OURS_AUTOSTART_ENV = 'OURS_AUTOSTART';
|
|
74
|
+
function localFleetAuditor(stateDir, caller, log) {
|
|
75
|
+
const store = new FleetCommandAuditStore(join(stateDir, '.fleet-command-audit.json'));
|
|
76
|
+
return {
|
|
77
|
+
async begin(requestId, argv) {
|
|
78
|
+
let attempt = store.begin(requestId, caller, argv);
|
|
79
|
+
if (attempt.invocation === 'sending') {
|
|
80
|
+
log(`[${caller}] fleet proxy command ${attempt.correlationId} `
|
|
81
|
+
+ `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
|
|
82
|
+
attempt = store.invocation(attempt.correlationId, caller, 'delivered');
|
|
83
|
+
}
|
|
84
|
+
return attempt;
|
|
85
|
+
},
|
|
86
|
+
async finish(input) {
|
|
87
|
+
let attempt = store.finish(input.correlationId, caller, {
|
|
88
|
+
class: input.class, effect: input.effect,
|
|
89
|
+
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
90
|
+
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
91
|
+
...(input.presentations ? { presentations: input.presentations } : {}),
|
|
92
|
+
});
|
|
93
|
+
if (attempt.outcome?.delivery === 'sending')
|
|
94
|
+
attempt = store.outcome(attempt.correlationId, caller, 'delivered');
|
|
95
|
+
return attempt;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
73
99
|
/** Environment injected only into the managed harness process. */
|
|
74
100
|
export function managedFleetProxyEnv(role, stateDir) {
|
|
75
101
|
const roleEnv = { ...(role.env ?? {}) };
|
|
@@ -612,11 +638,28 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
612
638
|
+ `${error?.message ?? String(error)}`);
|
|
613
639
|
}
|
|
614
640
|
}
|
|
615
|
-
control = deps.createControlServer(dir, arbiter, deps.log);
|
|
616
641
|
try {
|
|
642
|
+
if (role.owner_channel)
|
|
643
|
+
ownerChannel = deps.createOwnerChannel({
|
|
644
|
+
role: name,
|
|
645
|
+
harness: role.harness,
|
|
646
|
+
config: role.owner_channel,
|
|
647
|
+
session: arbiter,
|
|
648
|
+
stateDir: dir,
|
|
649
|
+
env: role.env,
|
|
650
|
+
log: deps.log,
|
|
651
|
+
...(ownerBinder ? { binderLease: ownerBinder } : {}),
|
|
652
|
+
...(configPath ? { configPath } : {}),
|
|
653
|
+
});
|
|
654
|
+
control = deps.createControlServer(dir, arbiter, deps.log);
|
|
655
|
+
control.setFleetAuditor(ownerChannel ? {
|
|
656
|
+
begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
|
|
657
|
+
finish: input => ownerChannel.finishFleetCommandAudit(input),
|
|
658
|
+
} : localFleetAuditor(dir, name, deps.log));
|
|
617
659
|
await control.start();
|
|
618
660
|
}
|
|
619
661
|
catch (error) {
|
|
662
|
+
await ownerChannel?.close().catch(() => undefined);
|
|
620
663
|
ownerBinder?.release();
|
|
621
664
|
await agentSession.close();
|
|
622
665
|
unsubscribeRecovery?.();
|
|
@@ -731,18 +774,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
731
774
|
deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
|
|
732
775
|
+ 'keeping temporary supervisor alive');
|
|
733
776
|
sessionStartupComplete = true;
|
|
734
|
-
if (
|
|
735
|
-
ownerChannel = deps.createOwnerChannel({
|
|
736
|
-
role: name,
|
|
737
|
-
harness: role.harness,
|
|
738
|
-
config: role.owner_channel,
|
|
739
|
-
session: arbiter,
|
|
740
|
-
stateDir: dir,
|
|
741
|
-
env: role.env,
|
|
742
|
-
log: deps.log,
|
|
743
|
-
...(ownerBinder ? { binderLease: ownerBinder } : {}),
|
|
744
|
-
...(configPath ? { configPath } : {}),
|
|
745
|
-
});
|
|
777
|
+
if (ownerChannel) {
|
|
746
778
|
try {
|
|
747
779
|
await ownerChannel.start();
|
|
748
780
|
}
|
|
@@ -761,10 +793,6 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
761
793
|
}
|
|
762
794
|
if (ownerChannel) {
|
|
763
795
|
control.setOwnerChannel(ownerChannel);
|
|
764
|
-
control.setFleetAuditor({
|
|
765
|
-
begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
|
|
766
|
-
finish: input => ownerChannel.finishFleetCommandAudit(input),
|
|
767
|
-
});
|
|
768
796
|
}
|
|
769
797
|
reloadLoopConfig = async () => {
|
|
770
798
|
const nextRole = findRole(loadConfig(configPath), name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.1.0-nightly.
|
|
3
|
+
"version": "1.1.0-nightly.24",
|
|
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",
|