@ours.network/fleet 1.1.0-nightly.8 → 1.1.0
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/README.md +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +4 -4
- package/dist/application/role-creation-service.js +9 -2
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +92 -39
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -28
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +130 -55
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +193 -55
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +59 -9
- package/dist/fleet-command-audit.js +426 -63
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +631 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +21 -5
- package/dist/owner-channel/channel.js +312 -138
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +450 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +434 -279
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -56
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +92 -33
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +2 -2
- package/dist/session/control.js +18 -2
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +77 -9
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, realpathSync
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { parse } from 'yaml';
|
|
4
5
|
import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
|
|
5
6
|
import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
7
|
+
import { storedRoomLaunchPolicy } from './types.js';
|
|
6
8
|
import { spawnTemp } from '../spawn.js';
|
|
7
|
-
import {
|
|
9
|
+
import { effectivePermissionMode } from '../permissions.js';
|
|
10
|
+
import { selectionOrigin, summarizeResolvedLaunch, } from '../lifecycle-summary.js';
|
|
11
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
12
|
+
import { readLaunchSnapshot, redactLaunchDefinition } from './launch-snapshot.js';
|
|
8
13
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
9
14
|
import { controlRequest } from '../session/control.js';
|
|
10
15
|
import { SessionControlError } from '../session/types.js';
|
|
11
16
|
import { closeManagedRoom } from './close.js';
|
|
17
|
+
import { withFileLock } from '../atomic-file.js';
|
|
18
|
+
import { TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath } from './terminal.js';
|
|
19
|
+
import { TaskStateError, taskDeletionState } from './task-state.js';
|
|
12
20
|
import { buildRoomMemberTask, sha256Text } from './member-startup.js';
|
|
13
21
|
import { agentDir } from '../paths.js';
|
|
14
22
|
import { readProvenance } from '../creation.js';
|
|
@@ -21,23 +29,9 @@ export function getBinPath() {
|
|
|
21
29
|
return process.argv[1];
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
|
-
function canonicalJson(value) {
|
|
25
|
-
if (Array.isArray(value))
|
|
26
|
-
return `[${value.map(canonicalJson).join(',')}]`;
|
|
27
|
-
if (value && typeof value === 'object')
|
|
28
|
-
return `{${Object.entries(value)
|
|
29
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
30
|
-
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
|
|
31
|
-
return JSON.stringify(value);
|
|
32
|
-
}
|
|
33
32
|
function launchDefinition(definition) {
|
|
34
|
-
const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : { kind: 'inline' };
|
|
35
33
|
return {
|
|
36
|
-
projection:
|
|
37
|
-
brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
|
|
38
|
-
operationalFields: Object.keys(definition)
|
|
39
|
-
.filter(key => key !== 'brain' && key !== 'role').sort(),
|
|
40
|
-
},
|
|
34
|
+
projection: redactLaunchDefinition(definition),
|
|
41
35
|
fingerprint: createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
42
36
|
};
|
|
43
37
|
}
|
|
@@ -61,8 +55,27 @@ async function spawnRoomMember(options, binPath) {
|
|
|
61
55
|
statePath: result.statePath,
|
|
62
56
|
creationActionId: result.creationActionId,
|
|
63
57
|
callerRole: result.caller,
|
|
58
|
+
...(result.configuration ? { configuration: result.configuration } : {}),
|
|
64
59
|
};
|
|
65
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Launch-boundary capture: the exact ResolvedRole the spawn persisted,
|
|
63
|
+
* whitelisted into the operator-facing presentation. Capture is required for
|
|
64
|
+
* every launched room member — launchMatches has already proved role.yaml
|
|
65
|
+
* readable, and effectivePermissionMode is total over registered harnesses —
|
|
66
|
+
* so a failure here is a capture defect and must surface, not degrade to the
|
|
67
|
+
* legacy rendering.
|
|
68
|
+
*/
|
|
69
|
+
function presentationFromStatePath(statePath, settings, coworkRole) {
|
|
70
|
+
const resolved = parse(readFileSync(join(statePath, 'role.yaml'), 'utf8'));
|
|
71
|
+
return summarizeResolvedLaunch(resolved, {
|
|
72
|
+
role: selectionOrigin(settings.definition.role),
|
|
73
|
+
brain: selectionOrigin(settings.definition.brain),
|
|
74
|
+
template: settings.template,
|
|
75
|
+
permissionMode: effectivePermissionMode(resolved),
|
|
76
|
+
missionFallback: coworkRole,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
66
79
|
function shortId(id) { return id.slice(0, 8); }
|
|
67
80
|
function expandMembers(template, prefix) {
|
|
68
81
|
const result = [];
|
|
@@ -72,39 +85,41 @@ function expandMembers(template, prefix) {
|
|
|
72
85
|
name: `${prefix}-${slot.slot}-${i}`,
|
|
73
86
|
slot: slot.slot,
|
|
74
87
|
coworkRole: slot.role,
|
|
75
|
-
|
|
88
|
+
agentTemplate: slot.agent_template,
|
|
89
|
+
launchDefinitionId: slot.launch_definition_id ?? slot.agent_template,
|
|
90
|
+
agentTemplateHash: slot.agent_template_hash,
|
|
91
|
+
loopSource: slot.loop_source ?? 'omitted',
|
|
76
92
|
});
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
return result;
|
|
80
96
|
}
|
|
81
|
-
function settingsFor(member, cfg) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
loadConfig(cfg.files[0], { additionalAgent: {
|
|
86
|
-
id: `RoomPreflight${member.name.replace(/[^A-Za-z0-9_-]/g, '')}`.slice(0, 64),
|
|
87
|
-
definition: member.agent,
|
|
88
|
-
} });
|
|
89
|
-
return { definition: structuredClone(member.agent),
|
|
90
|
-
...('inline' in role && typeof role.inline.persona === 'string'
|
|
91
|
-
? { persona: role.inline.persona } : {}) };
|
|
92
|
-
}
|
|
93
|
-
const refRole = findRole(cfg, member.agent.ref);
|
|
94
|
-
const definition = cfg.agentDefinitions?.[member.agent.ref];
|
|
97
|
+
function settingsFor(member, cfg, sealed) {
|
|
98
|
+
const definition = sealed
|
|
99
|
+
? sealed[member.launchDefinitionId]
|
|
100
|
+
: cfg.agentTemplates?.[member.agentTemplate];
|
|
95
101
|
if (!definition)
|
|
96
|
-
throw new Error(
|
|
102
|
+
throw new Error(sealed
|
|
103
|
+
? `Sealed Agent definition '${member.launchDefinitionId}' not found`
|
|
104
|
+
: `Agent Template '${member.agentTemplate}' not found`);
|
|
105
|
+
const role = definition.role;
|
|
97
106
|
return {
|
|
98
107
|
definition: structuredClone(definition),
|
|
99
|
-
|
|
108
|
+
...('inline' in role && typeof role.inline.persona === 'string'
|
|
109
|
+
? { persona: role.inline.persona } : {}),
|
|
110
|
+
template: member.agentTemplate,
|
|
111
|
+
templateHash: member.agentTemplateHash
|
|
112
|
+
?? createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
113
|
+
loopSource: member.loopSource,
|
|
100
114
|
};
|
|
101
115
|
}
|
|
102
|
-
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
116
|
+
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid, anonymous) {
|
|
103
117
|
return buildRoomMemberTask({
|
|
104
118
|
taskId: input.taskId,
|
|
105
119
|
roomId: input.roomId,
|
|
106
120
|
roomIdentityCid,
|
|
107
121
|
ownerSeatCid,
|
|
122
|
+
anonymous,
|
|
108
123
|
goal: input.goal,
|
|
109
124
|
brief: input.brief,
|
|
110
125
|
contract: input.template.contract,
|
|
@@ -119,7 +134,7 @@ function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCi
|
|
|
119
134
|
})),
|
|
120
135
|
});
|
|
121
136
|
}
|
|
122
|
-
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId) {
|
|
137
|
+
function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid, expectedInviteId, anonymous = false) {
|
|
123
138
|
const provenance = readProvenance(dir);
|
|
124
139
|
if (provenance?.creationActionId !== actionId || provenance.role !== member.name)
|
|
125
140
|
return false;
|
|
@@ -131,6 +146,7 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
131
146
|
&& startup.room_identity_cid === roomIdentityCid
|
|
132
147
|
&& startup.identity_name === member.name
|
|
133
148
|
&& startup.role === member.coworkRole
|
|
149
|
+
&& (startup.anonymous ?? false) === anonymous
|
|
134
150
|
&& sha256Text(startup.task ?? '') === taskSha
|
|
135
151
|
&& (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
|
|
136
152
|
&& typeof startup.invite === 'string'
|
|
@@ -141,21 +157,22 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
async function retainRunningLaunch(input) {
|
|
144
|
-
const { provision, member, task, roomIdentityCid } = input;
|
|
160
|
+
const { provision, member, settings, task, roomIdentityCid } = input;
|
|
145
161
|
let seat = getRoomRecord(provision.roomId).member_seats
|
|
146
162
|
.find(candidate => candidate.role_name === member.name);
|
|
147
163
|
const dir = agentDir(member.name, true);
|
|
148
164
|
const taskSha = sha256Text(task);
|
|
165
|
+
const anonymous = storedRoomLaunchPolicy(getRoomRecord(provision.roomId)?.room_policy).anonymous;
|
|
149
166
|
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched'
|
|
150
167
|
|| seat.launch?.state === 'failed') && existsSync(dir)) {
|
|
151
|
-
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
168
|
+
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
|
|
152
169
|
const provenance = readProvenance(dir);
|
|
153
170
|
const adoptable = (seat.launch.state === 'intent' || seat.launch.state === 'failed')
|
|
154
171
|
&& Boolean(seat.launch.caller_role)
|
|
155
172
|
&& provenance?.surface === 'agent'
|
|
156
173
|
&& provenance.callerRole === seat.launch.caller_role
|
|
157
174
|
&& typeof provenance.creationActionId === 'string'
|
|
158
|
-
&& launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id);
|
|
175
|
+
&& launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous);
|
|
159
176
|
if (!adoptable)
|
|
160
177
|
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
161
178
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
@@ -173,9 +190,14 @@ async function retainRunningLaunch(input) {
|
|
|
173
190
|
throw new Error(`existing launch for ${member.name} has unknown liveness`);
|
|
174
191
|
const retainedLaunch = seat.launch;
|
|
175
192
|
if (live === 'running') {
|
|
193
|
+
// A crash between spawn and the seat update (or a pre-upgrade launch)
|
|
194
|
+
// can retain a running member without a captured presentation; backfill
|
|
195
|
+
// it from the same persisted resolved role the match was proved against.
|
|
196
|
+
const presentation = retainedLaunch.presentation
|
|
197
|
+
?? presentationFromStatePath(dir, settings, member.coworkRole);
|
|
176
198
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
177
199
|
...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
|
|
178
|
-
updated_at: new Date().toISOString(),
|
|
200
|
+
presentation, updated_at: new Date().toISOString(),
|
|
179
201
|
} });
|
|
180
202
|
return true;
|
|
181
203
|
}
|
|
@@ -190,7 +212,7 @@ async function retainRunningLaunch(input) {
|
|
|
190
212
|
if (!seat.launch.launch_id || !seat.launch.action_id)
|
|
191
213
|
throw new Error(`missing durable launch identity for disappeared ${member.name}`);
|
|
192
214
|
const archive = await secureStoppedTempArchive(member.name, seat.launch.launch_id);
|
|
193
|
-
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
215
|
+
if (!launchMatches(archive, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
|
|
194
216
|
throw new Error(`archive for disappeared ${member.name} does not match its durable intent`);
|
|
195
217
|
}
|
|
196
218
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
@@ -202,7 +224,7 @@ async function retainRunningLaunch(input) {
|
|
|
202
224
|
if (!seat.launch.action_id)
|
|
203
225
|
throw new Error(`missing action ID for disappeared launch intent ${member.name}`);
|
|
204
226
|
const archive = tempArchiveForCreationAction(member.name, seat.launch.action_id);
|
|
205
|
-
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
227
|
+
if (!archive || !launchMatches(archive.path, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id, anonymous)) {
|
|
206
228
|
throw new Error(`launch intent for ${member.name} has no exact live or terminated archive evidence`);
|
|
207
229
|
}
|
|
208
230
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
@@ -212,7 +234,25 @@ async function retainRunningLaunch(input) {
|
|
|
212
234
|
}
|
|
213
235
|
return false;
|
|
214
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Member-launch publication window: for a task-bound room, the durable launch
|
|
239
|
+
* intent, spawn, and launch record publish under the common task-operation
|
|
240
|
+
* lock so an accepted deletion linearizes strictly before (bounded abort — no
|
|
241
|
+
* spawn happens) or strictly after (the seat's launch record is visible to
|
|
242
|
+
* deletion retirement). The lock is held per member only, never across
|
|
243
|
+
* seat-wait loops.
|
|
244
|
+
*/
|
|
215
245
|
async function launchMember(input) {
|
|
246
|
+
const taskId = input.provision.taskId;
|
|
247
|
+
if (!taskId)
|
|
248
|
+
return launchMemberUnlocked(input);
|
|
249
|
+
return withFileLock(taskOperationLockPath(taskId), () => {
|
|
250
|
+
if (taskDeletionState(taskId) !== 'none')
|
|
251
|
+
throw new Error(`task ${taskId} is pending deletion; aborting member launch for ${input.member.name}`);
|
|
252
|
+
return launchMemberUnlocked(input);
|
|
253
|
+
}, {}, TASK_OPERATION_LOCK_STALE_MS);
|
|
254
|
+
}
|
|
255
|
+
async function launchMemberUnlocked(input) {
|
|
216
256
|
const { provision, member, settings, startup } = input;
|
|
217
257
|
const seat = getRoomRecord(provision.roomId).member_seats
|
|
218
258
|
.find(candidate => candidate.role_name === member.name);
|
|
@@ -227,6 +267,7 @@ async function launchMember(input) {
|
|
|
227
267
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
228
268
|
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
229
269
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
270
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
230
271
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
231
272
|
updated_at: new Date().toISOString(),
|
|
232
273
|
} });
|
|
@@ -236,6 +277,9 @@ async function launchMember(input) {
|
|
|
236
277
|
temp: true,
|
|
237
278
|
identity: member.name,
|
|
238
279
|
agentDefinition: settings.definition,
|
|
280
|
+
...(settings.loopSource === 'cli' && settings.definition.loops === undefined
|
|
281
|
+
? { noLoops: true } : {}),
|
|
282
|
+
loopSource: settings.loopSource,
|
|
239
283
|
surface: 'agent',
|
|
240
284
|
creationActionId: actionId,
|
|
241
285
|
roomMemberStartup: startup,
|
|
@@ -246,17 +290,25 @@ async function launchMember(input) {
|
|
|
246
290
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
247
291
|
state: 'intent', attempt, action_id: launched.creationActionId,
|
|
248
292
|
mission_sha256: taskSha, agent_definition: agentDefinition,
|
|
249
|
-
agent_fingerprint: agentFingerprint,
|
|
293
|
+
agent_fingerprint: agentFingerprint,
|
|
294
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
295
|
+
updated_at: new Date().toISOString(),
|
|
250
296
|
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
251
297
|
} });
|
|
252
298
|
}
|
|
253
299
|
const supervisor = readTempSupervisor(launchedDir);
|
|
254
|
-
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id)) {
|
|
300
|
+
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id, startup.anonymous ?? false)) {
|
|
255
301
|
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
256
302
|
}
|
|
303
|
+
const presentation = launched.configuration
|
|
304
|
+
? { ...launched.configuration, template: settings.template,
|
|
305
|
+
...(launched.configuration.mission ? {} : { mission: member.coworkRole }) }
|
|
306
|
+
: presentationFromStatePath(launchedDir, settings, member.coworkRole);
|
|
257
307
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
258
308
|
state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
|
|
259
309
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
310
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
311
|
+
presentation,
|
|
260
312
|
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
261
313
|
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
262
314
|
} });
|
|
@@ -265,6 +317,7 @@ async function launchMember(input) {
|
|
|
265
317
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
266
318
|
state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
|
|
267
319
|
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
320
|
+
agent_template: settings.template, agent_template_hash: settings.templateHash,
|
|
268
321
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
269
322
|
updated_at: new Date().toISOString(),
|
|
270
323
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -286,9 +339,10 @@ function exactCoworkSeat(observed, member, expectedInviteId) {
|
|
|
286
339
|
}
|
|
287
340
|
return seat;
|
|
288
341
|
}
|
|
289
|
-
function reconcileMemberSeats(roomId, members, observed) {
|
|
342
|
+
function reconcileMemberSeats(roomId, members, observed, ownerSeatCid) {
|
|
290
343
|
const current = getRoomRecord(roomId);
|
|
291
|
-
let complete =
|
|
344
|
+
let complete = !ownerSeatCid || observed.some(seat => seat.identity_cid.toLowerCase() === ownerSeatCid.toLowerCase()
|
|
345
|
+
&& seat.seat_state === 'active');
|
|
292
346
|
const seats = current.member_seats.map(seat => {
|
|
293
347
|
const member = members.find(candidate => candidate.name === seat.role_name);
|
|
294
348
|
const found = exactCoworkSeat(observed, member, seat.invite_id);
|
|
@@ -306,24 +360,35 @@ function reconcileMemberSeats(roomId, members, observed) {
|
|
|
306
360
|
updateMemberSeats(roomId, seats);
|
|
307
361
|
return { complete, seats };
|
|
308
362
|
}
|
|
363
|
+
function assertCoworkRoomPolicy(room, expectedAnonymous) {
|
|
364
|
+
if ((room.anonymous ?? false) !== expectedAnonymous)
|
|
365
|
+
throw new Error(`Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(expectedAnonymous)})`);
|
|
366
|
+
}
|
|
309
367
|
export async function provisionMembers(input) {
|
|
310
368
|
const { cfg, cowork, roomId, taskId, template } = input;
|
|
369
|
+
// Deletion-epoch pre-check; each member launch re-checks under the lock.
|
|
370
|
+
if (taskId && taskDeletionState(taskId) !== 'none')
|
|
371
|
+
throw new Error(`task ${taskId} is pending deletion; refusing to provision members`);
|
|
311
372
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
312
373
|
const members = expandMembers(template, prefix);
|
|
313
374
|
// Resolve every Agent before persisting launch intent or touching Cowork membership.
|
|
314
|
-
const
|
|
375
|
+
const sealed = template.launch_snapshot_hash
|
|
376
|
+
? readLaunchSnapshot(template.launch_snapshot_hash) : undefined;
|
|
377
|
+
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg, sealed)]));
|
|
315
378
|
const existing = getRoomRecord(roomId);
|
|
316
379
|
if (!existing?.room_identity_cid)
|
|
317
380
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
318
381
|
const roomIdentityCid = existing.room_identity_cid;
|
|
319
382
|
const ownerSeatCid = existing.owner_seat_cid ?? null;
|
|
383
|
+
const roomPolicy = storedRoomLaunchPolicy(existing.room_policy);
|
|
320
384
|
const persistedNames = new Set(existing.member_seats.map(seat => seat.role_name));
|
|
321
385
|
const resuming = members.length > 0
|
|
322
386
|
&& members.every(member => persistedNames.has(member.name));
|
|
323
387
|
if (!resuming) {
|
|
324
388
|
advanceSaga(roomId, 'create_members', 3);
|
|
325
389
|
updateMemberSeats(roomId, members.map(member => {
|
|
326
|
-
const
|
|
390
|
+
const memberSettings = settings.get(member.name);
|
|
391
|
+
const evidence = launchDefinition(memberSettings.definition);
|
|
327
392
|
return ({
|
|
328
393
|
role_name: member.name,
|
|
329
394
|
slot: member.slot,
|
|
@@ -331,6 +396,8 @@ export async function provisionMembers(input) {
|
|
|
331
396
|
seat_state: 'pending',
|
|
332
397
|
launch: { state: 'pending', attempt: 0,
|
|
333
398
|
agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
|
|
399
|
+
agent_template: memberSettings.template,
|
|
400
|
+
agent_template_hash: memberSettings.templateHash,
|
|
334
401
|
updated_at: new Date().toISOString() },
|
|
335
402
|
});
|
|
336
403
|
}));
|
|
@@ -343,7 +410,7 @@ export async function provisionMembers(input) {
|
|
|
343
410
|
throw new Error(`Agent definition drift for ${member.name}; durable launch intent does not match current configuration`);
|
|
344
411
|
}
|
|
345
412
|
}
|
|
346
|
-
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
|
|
413
|
+
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid, roomPolicy.anonymous)]));
|
|
347
414
|
const policy = {
|
|
348
415
|
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
349
416
|
initialDelayMs: input.startupWait?.initialDelayMs ?? 250,
|
|
@@ -354,20 +421,23 @@ export async function provisionMembers(input) {
|
|
|
354
421
|
advanceSaga(roomId, 'join_role_groups', 4);
|
|
355
422
|
try {
|
|
356
423
|
const initialRoom = await cowork.recoverRoom(roomId);
|
|
357
|
-
|
|
424
|
+
assertCoworkRoomPolicy(initialRoom, roomPolicy.anonymous);
|
|
425
|
+
reconcileMemberSeats(roomId, members, initialRoom.seats, existing.owner_seat_cid);
|
|
358
426
|
for (const member of members) {
|
|
359
427
|
const task = tasks.get(member.name);
|
|
360
428
|
const currentSeat = getRoomRecord(roomId).member_seats
|
|
361
429
|
.find(seat => seat.role_name === member.name);
|
|
362
430
|
if (currentSeat.seat_state === 'active') {
|
|
363
431
|
if (!await retainRunningLaunch({
|
|
364
|
-
provision: input, member, task, roomIdentityCid,
|
|
432
|
+
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
365
433
|
})) {
|
|
366
434
|
throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
|
|
367
435
|
}
|
|
368
436
|
continue;
|
|
369
437
|
}
|
|
370
|
-
if (await retainRunningLaunch({
|
|
438
|
+
if (await retainRunningLaunch({
|
|
439
|
+
provision: input, member, settings: settings.get(member.name), task, roomIdentityCid,
|
|
440
|
+
}))
|
|
371
441
|
continue;
|
|
372
442
|
const issued = await cowork.issueInvite(roomId, {
|
|
373
443
|
mode: 'one_time', role: member.coworkRole, min_accepts: 1,
|
|
@@ -388,6 +458,7 @@ export async function provisionMembers(input) {
|
|
|
388
458
|
role: member.coworkRole,
|
|
389
459
|
task,
|
|
390
460
|
owner_seat_cid: ownerSeatCid,
|
|
461
|
+
anonymous: roomPolicy.anonymous,
|
|
391
462
|
},
|
|
392
463
|
});
|
|
393
464
|
}
|
|
@@ -399,9 +470,18 @@ export async function provisionMembers(input) {
|
|
|
399
470
|
}
|
|
400
471
|
catch (error) {
|
|
401
472
|
const reason = error instanceof Error ? error.message : String(error);
|
|
402
|
-
setSagaError(roomId, reason, 'Member invite or launch failed.
|
|
403
|
-
if (taskId)
|
|
404
|
-
|
|
473
|
+
setSagaError(roomId, reason, 'Member invite or launch failed. Inspect role logs, then retry with `task start`.', 'member_failed');
|
|
474
|
+
if (taskId) {
|
|
475
|
+
// A deletion-pending (or already-terminal) task rejects the block
|
|
476
|
+
// overlay; the original launch failure must still propagate.
|
|
477
|
+
try {
|
|
478
|
+
blockTask(taskId, reason);
|
|
479
|
+
}
|
|
480
|
+
catch (blockError) {
|
|
481
|
+
if (!(blockError instanceof TaskStateError))
|
|
482
|
+
throw blockError;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
405
485
|
throw error;
|
|
406
486
|
}
|
|
407
487
|
advanceSaga(roomId, 'wait_seats', 5);
|
|
@@ -409,7 +489,8 @@ export async function provisionMembers(input) {
|
|
|
409
489
|
let delay = policy.initialDelayMs;
|
|
410
490
|
for (;;) {
|
|
411
491
|
const remote = await cowork.recoverRoom(roomId);
|
|
412
|
-
|
|
492
|
+
assertCoworkRoomPolicy(remote, roomPolicy.anonymous);
|
|
493
|
+
const reconciled = reconcileMemberSeats(roomId, members, remote.seats, ownerSeatCid ?? undefined);
|
|
413
494
|
if (reconciled.complete)
|
|
414
495
|
break;
|
|
415
496
|
if (policy.now() >= deadline) {
|
|
@@ -420,6 +501,15 @@ export async function provisionMembers(input) {
|
|
|
420
501
|
await policy.sleep(delay);
|
|
421
502
|
delay = Math.min(policy.maxDelayMs, Math.max(delay + 1, delay * 2));
|
|
422
503
|
}
|
|
504
|
+
// Admission alone is insufficient: every expected member must still have
|
|
505
|
+
// the exact live launch that owns its authenticated Cowork seat.
|
|
506
|
+
for (const member of members) {
|
|
507
|
+
if (!await retainRunningLaunch({
|
|
508
|
+
provision: input, member, settings: settings.get(member.name),
|
|
509
|
+
task: tasks.get(member.name), roomIdentityCid,
|
|
510
|
+
}))
|
|
511
|
+
throw new Error(`active Cowork seat ${member.name} has no matching live Fleet launch`);
|
|
512
|
+
}
|
|
423
513
|
if (taskId) {
|
|
424
514
|
const taskMembers = getRoomRecord(roomId).member_seats.map(seat => {
|
|
425
515
|
if (!seat.identity_cid)
|
|
@@ -9,6 +9,7 @@ export interface CreateRoomInput {
|
|
|
9
9
|
room_identity_cid?: string;
|
|
10
10
|
task_id?: string;
|
|
11
11
|
template_snapshot?: import('./types.js').TemplateSnapshot;
|
|
12
|
+
room_policy?: import('./types.js').RoomLaunchPolicy;
|
|
12
13
|
}
|
|
13
14
|
export declare function createRoomRecord(input: CreateRoomInput): RoomOrchestrationRecord;
|
|
14
15
|
export declare function getRoomRecord(id: string): RoomOrchestrationRecord | undefined;
|
|
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:f
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
4
4
|
import { stateRoot } from '../paths.js';
|
|
5
|
+
import { storedRoomLaunchPolicy } from './types.js';
|
|
6
|
+
import { releaseLaunchSnapshot } from './launch-snapshot.js';
|
|
5
7
|
export const roomsDir = () => join(stateRoot(), 'rooms');
|
|
6
8
|
function roomPath(id) { return join(roomsDir(), `${id}.json`); }
|
|
7
9
|
export class RoomStateError extends Error {
|
|
@@ -18,8 +20,13 @@ function writeRoom(record) {
|
|
|
18
20
|
}
|
|
19
21
|
export function createRoomRecord(input) {
|
|
20
22
|
const existing = getRoomRecord(input.room_id);
|
|
21
|
-
if (existing)
|
|
23
|
+
if (existing) {
|
|
24
|
+
const before = storedRoomLaunchPolicy(existing.room_policy);
|
|
25
|
+
const requested = storedRoomLaunchPolicy(input.room_policy);
|
|
26
|
+
if (JSON.stringify(before) !== JSON.stringify(requested))
|
|
27
|
+
throw new RoomStateError(`room ${input.room_id} launch policy mismatch`);
|
|
22
28
|
return existing;
|
|
29
|
+
}
|
|
23
30
|
const record = {
|
|
24
31
|
room_id: input.room_id,
|
|
25
32
|
room_name: input.room_name,
|
|
@@ -27,6 +34,7 @@ export function createRoomRecord(input) {
|
|
|
27
34
|
room_identity_cid: input.room_identity_cid,
|
|
28
35
|
task_id: input.task_id,
|
|
29
36
|
template_snapshot: input.template_snapshot,
|
|
37
|
+
room_policy: input.room_policy,
|
|
30
38
|
saga: { phase: 'persist_intent', step_index: 0 },
|
|
31
39
|
member_seats: [],
|
|
32
40
|
state: 'provisioning',
|
|
@@ -46,8 +54,11 @@ export function getRoomRecord(id) {
|
|
|
46
54
|
/** Remove a terminal orchestration record from the live room inventory. */
|
|
47
55
|
export function deleteRoomRecord(id) {
|
|
48
56
|
const path = roomPath(id);
|
|
57
|
+
const hash = getRoomRecord(id)?.template_snapshot?.launch_snapshot_hash;
|
|
49
58
|
if (existsSync(path))
|
|
50
59
|
rmSync(path);
|
|
60
|
+
if (hash)
|
|
61
|
+
releaseLaunchSnapshot(hash);
|
|
51
62
|
}
|
|
52
63
|
export function listRoomRecords(filter) {
|
|
53
64
|
const dir = roomsDir();
|
|
@@ -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
|
+
}
|