@ours.network/fleet 1.1.0-nightly.23 → 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.
@@ -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
- return { kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room' };
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: task.title, goal: task.goal?.trim() || task.title,
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: task.title, room_identity_cid: created.identity_cid,
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);
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.23",
3
- "buildId": "de853a9ef3d8",
4
- "commit": "f970d01c44d8a5cad2627c1bdde833e412508613",
2
+ "version": "1.1.0-nightly.24",
3
+ "buildId": "30fec1b573b6",
4
+ "commit": "93f6e23dd6eb3642ee12bc01cba53a2aba838ab1",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-02T07:30:34.670Z",
6
+ "builtAt": "2026-09-02T11:45:37.603Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.1.0-nightly.23",
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",