@ours.network/fleet 1.2.0-nightly.1 → 1.2.0-nightly.11
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 +110 -21
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +64 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +53 -0
- package/dist/agent-ours/runtime.js +247 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +364 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +121 -0
- package/dist/application/task-room-service.d.ts +6 -2
- package/dist/application/task-room-service.js +78 -21
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +63 -2
- package/dist/client-profile.d.ts +1 -0
- package/dist/client-profile.js +18 -3
- package/dist/config.d.ts +1 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -14
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +32 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +29 -10
- package/dist/fleet-command-audit.js +3 -2
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +24 -4
- package/dist/owner-channel/commands.js +5 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +6 -1
- package/dist/rooms-tasks/close.d.ts +3 -0
- package/dist/rooms-tasks/close.js +95 -17
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +10 -2
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.js +40 -3
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +75 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +11 -0
- package/dist/rooms-tasks/provision.js +138 -26
- package/dist/rooms-tasks/room-state.js +13 -0
- package/dist/rooms-tasks/task-state.d.ts +13 -3
- package/dist/rooms-tasks/task-state.js +42 -6
- package/dist/rooms-tasks/types.d.ts +12 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +14 -0
- package/dist/runner.js +527 -495
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/systemd.js +5 -0
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/docs/task-workspaces.md +98 -0
- package/package.json +6 -3
- package/presets/fleet/roles/Coordinator.yaml +22 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface SupervisorToolRequest {
|
|
2
|
+
tool: string;
|
|
3
|
+
arguments?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
/** CLI and REST use the same fixed-identity MCP server as the agent harness. */
|
|
6
|
+
export declare class SupervisorOursTools {
|
|
7
|
+
private withClient;
|
|
8
|
+
list(role: string): Promise<{
|
|
9
|
+
tools: {
|
|
10
|
+
inputSchema: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
type: "object";
|
|
13
|
+
properties?: Record<string, object> | undefined;
|
|
14
|
+
required?: string[] | undefined;
|
|
15
|
+
};
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | undefined;
|
|
18
|
+
outputSchema?: {
|
|
19
|
+
[x: string]: unknown;
|
|
20
|
+
type: "object";
|
|
21
|
+
properties?: Record<string, object> | undefined;
|
|
22
|
+
required?: string[] | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
annotations?: {
|
|
25
|
+
title?: string | undefined;
|
|
26
|
+
readOnlyHint?: boolean | undefined;
|
|
27
|
+
destructiveHint?: boolean | undefined;
|
|
28
|
+
idempotentHint?: boolean | undefined;
|
|
29
|
+
openWorldHint?: boolean | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
execution?: {
|
|
32
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
_meta?: Record<string, unknown> | undefined;
|
|
35
|
+
icons?: {
|
|
36
|
+
src: string;
|
|
37
|
+
mimeType?: string | undefined;
|
|
38
|
+
sizes?: string[] | undefined;
|
|
39
|
+
theme?: "light" | "dark" | undefined;
|
|
40
|
+
}[] | undefined;
|
|
41
|
+
title?: string | undefined;
|
|
42
|
+
}[];
|
|
43
|
+
_meta?: {
|
|
44
|
+
[x: string]: unknown;
|
|
45
|
+
progressToken?: string | number | undefined;
|
|
46
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
47
|
+
taskId: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
nextCursor?: string | undefined;
|
|
51
|
+
agent: string;
|
|
52
|
+
identity: {
|
|
53
|
+
name: string;
|
|
54
|
+
cid: string;
|
|
55
|
+
generation: number;
|
|
56
|
+
};
|
|
57
|
+
}>;
|
|
58
|
+
call(role: string, request: SupervisorToolRequest): Promise<{
|
|
59
|
+
agent: string;
|
|
60
|
+
identity: {
|
|
61
|
+
name: string;
|
|
62
|
+
cid: string;
|
|
63
|
+
generation: number;
|
|
64
|
+
};
|
|
65
|
+
result: {
|
|
66
|
+
[x: string]: unknown;
|
|
67
|
+
content: ({
|
|
68
|
+
type: "text";
|
|
69
|
+
text: string;
|
|
70
|
+
annotations?: {
|
|
71
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
72
|
+
priority?: number | undefined;
|
|
73
|
+
lastModified?: string | undefined;
|
|
74
|
+
} | undefined;
|
|
75
|
+
_meta?: Record<string, unknown> | undefined;
|
|
76
|
+
} | {
|
|
77
|
+
type: "image";
|
|
78
|
+
data: string;
|
|
79
|
+
mimeType: string;
|
|
80
|
+
annotations?: {
|
|
81
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
82
|
+
priority?: number | undefined;
|
|
83
|
+
lastModified?: string | undefined;
|
|
84
|
+
} | undefined;
|
|
85
|
+
_meta?: Record<string, unknown> | undefined;
|
|
86
|
+
} | {
|
|
87
|
+
type: "audio";
|
|
88
|
+
data: string;
|
|
89
|
+
mimeType: string;
|
|
90
|
+
annotations?: {
|
|
91
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
92
|
+
priority?: number | undefined;
|
|
93
|
+
lastModified?: string | undefined;
|
|
94
|
+
} | undefined;
|
|
95
|
+
_meta?: Record<string, unknown> | undefined;
|
|
96
|
+
} | {
|
|
97
|
+
type: "resource";
|
|
98
|
+
resource: {
|
|
99
|
+
uri: string;
|
|
100
|
+
text: string;
|
|
101
|
+
mimeType?: string | undefined;
|
|
102
|
+
_meta?: Record<string, unknown> | undefined;
|
|
103
|
+
} | {
|
|
104
|
+
uri: string;
|
|
105
|
+
blob: string;
|
|
106
|
+
mimeType?: string | undefined;
|
|
107
|
+
_meta?: Record<string, unknown> | undefined;
|
|
108
|
+
};
|
|
109
|
+
annotations?: {
|
|
110
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
111
|
+
priority?: number | undefined;
|
|
112
|
+
lastModified?: string | undefined;
|
|
113
|
+
} | undefined;
|
|
114
|
+
_meta?: Record<string, unknown> | undefined;
|
|
115
|
+
} | {
|
|
116
|
+
uri: string;
|
|
117
|
+
name: string;
|
|
118
|
+
type: "resource_link";
|
|
119
|
+
description?: string | undefined;
|
|
120
|
+
mimeType?: string | undefined;
|
|
121
|
+
size?: number | undefined;
|
|
122
|
+
annotations?: {
|
|
123
|
+
audience?: ("user" | "assistant")[] | undefined;
|
|
124
|
+
priority?: number | undefined;
|
|
125
|
+
lastModified?: string | undefined;
|
|
126
|
+
} | undefined;
|
|
127
|
+
_meta?: {
|
|
128
|
+
[x: string]: unknown;
|
|
129
|
+
} | undefined;
|
|
130
|
+
icons?: {
|
|
131
|
+
src: string;
|
|
132
|
+
mimeType?: string | undefined;
|
|
133
|
+
sizes?: string[] | undefined;
|
|
134
|
+
theme?: "light" | "dark" | undefined;
|
|
135
|
+
}[] | undefined;
|
|
136
|
+
title?: string | undefined;
|
|
137
|
+
})[];
|
|
138
|
+
_meta?: {
|
|
139
|
+
[x: string]: unknown;
|
|
140
|
+
progressToken?: string | number | undefined;
|
|
141
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
142
|
+
taskId: string;
|
|
143
|
+
} | undefined;
|
|
144
|
+
} | undefined;
|
|
145
|
+
structuredContent?: Record<string, unknown> | undefined;
|
|
146
|
+
isError?: boolean | undefined;
|
|
147
|
+
} | {
|
|
148
|
+
[x: string]: unknown;
|
|
149
|
+
toolResult: unknown;
|
|
150
|
+
_meta?: {
|
|
151
|
+
[x: string]: unknown;
|
|
152
|
+
progressToken?: string | number | undefined;
|
|
153
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
154
|
+
taskId: string;
|
|
155
|
+
} | undefined;
|
|
156
|
+
} | undefined;
|
|
157
|
+
};
|
|
158
|
+
}>;
|
|
159
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
+
import { agentDir } from '../paths.js';
|
|
8
|
+
import { ROLE_NAME_RE } from '../config.js';
|
|
9
|
+
import { legacySupervisorIdentity } from './legacy-supervisor-identity.js';
|
|
10
|
+
import { FleetError } from './errors.js';
|
|
11
|
+
/** CLI and REST use the same fixed-identity MCP server as the agent harness. */
|
|
12
|
+
export class SupervisorOursTools {
|
|
13
|
+
async withClient(role, work) {
|
|
14
|
+
if (!ROLE_NAME_RE.test(role))
|
|
15
|
+
throw new FleetError('invalid_request', 'invalid agent name');
|
|
16
|
+
const findCandidates = () => [false, true].map(temporary => ({ temporary, dir: agentDir(role, temporary) }))
|
|
17
|
+
.map(row => ({ ...row, path: join(row.dir, '.ours-bridge', 'descriptor.json') }))
|
|
18
|
+
.filter(row => existsSync(row.path));
|
|
19
|
+
const candidates = findCandidates();
|
|
20
|
+
if (candidates.length !== 1)
|
|
21
|
+
throw new FleetError('capability_unavailable', 'agent supervisor endpoint is missing or ambiguous');
|
|
22
|
+
const selected = candidates[0];
|
|
23
|
+
let descriptor;
|
|
24
|
+
let identityName;
|
|
25
|
+
try {
|
|
26
|
+
descriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
|
|
27
|
+
identityName = readFileSync(join(selected.dir, '.identity'), 'utf8').trim();
|
|
28
|
+
if (!descriptor || typeof descriptor !== 'object')
|
|
29
|
+
throw new Error();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new FleetError('capability_unavailable', 'supervisor identity metadata cannot be read');
|
|
33
|
+
}
|
|
34
|
+
const legacy = ['role', 'identity', 'cid'].every(key => descriptor[key] === undefined);
|
|
35
|
+
const identity = legacy
|
|
36
|
+
? legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation)
|
|
37
|
+
: { name: identityName, cid: descriptor.cid, generation: descriptor.generation };
|
|
38
|
+
if (legacy && descriptor.socket !== join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`))
|
|
39
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor socket does not match the selected agent');
|
|
40
|
+
if ((!legacy && (descriptor.role !== role || descriptor.identity !== identityName))
|
|
41
|
+
|| !/^[a-f0-9]{64}$/i.test(identity.cid ?? '') || !Number.isSafeInteger(descriptor.generation) || descriptor.generation < 1)
|
|
42
|
+
throw new FleetError('capability_unavailable', 'supervisor identity metadata is unavailable or mismatched');
|
|
43
|
+
const verifySelection = () => {
|
|
44
|
+
try {
|
|
45
|
+
const current = findCandidates();
|
|
46
|
+
const freshDescriptor = JSON.parse(readFileSync(selected.path, 'utf8'));
|
|
47
|
+
if (current.length !== 1 || current[0].path !== selected.path
|
|
48
|
+
|| readFileSync(join(selected.dir, '.identity'), 'utf8').trim() !== identityName
|
|
49
|
+
|| ['role', 'identity', 'cid', 'socket', 'capability', 'generation']
|
|
50
|
+
.some(key => freshDescriptor[key] !== descriptor[key]))
|
|
51
|
+
throw new Error();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new FleetError('capability_unavailable', 'selected supervisor assignment changed');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const client = new Client({ name: 'ours-fleet-supervisor-tools', version: '1' });
|
|
58
|
+
const transport = new StdioClientTransport({
|
|
59
|
+
command: process.execPath,
|
|
60
|
+
args: [fileURLToPath(new URL('../agent-ours/bridge.js', import.meta.url))],
|
|
61
|
+
env: { ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined)),
|
|
62
|
+
FLEET_OURS_BRIDGE_DESCRIPTOR: selected.path,
|
|
63
|
+
FLEET_OURS_BRIDGE_EXPECTED: JSON.stringify({ role: descriptor.role, identity: descriptor.identity, cid: descriptor.cid, generation: descriptor.generation,
|
|
64
|
+
transportDigest: createHash('sha256').update(JSON.stringify([descriptor.socket, descriptor.capability, descriptor.generation])).digest('hex') }) },
|
|
65
|
+
stderr: 'pipe',
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
await client.connect(transport);
|
|
69
|
+
const verify = async () => {
|
|
70
|
+
if (!legacy) {
|
|
71
|
+
verifySelection();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const actual = await client.callTool({ name: 'current_identity', arguments: {} });
|
|
75
|
+
const blocks = actual.content;
|
|
76
|
+
const lines = blocks?.length === 1 && blocks[0].type === 'text' ? blocks[0].text?.split('\n') : undefined;
|
|
77
|
+
const prefix = `Bound to "${identity.name}" (${identity.cid})`;
|
|
78
|
+
const suffix = lines?.[0].startsWith(prefix) ? lines[0].slice(prefix.length) : undefined;
|
|
79
|
+
const roleSuffix = typeof suffix === 'string' && /^ — role "[^"\r\n]+" under root "[^"\r\n]+"\.$/.test(suffix);
|
|
80
|
+
const temporaryLine = 'TEMPORARY identity owned by the Fleet supervisor for this logical agent instance. Bridge or harness disconnect retains it; terminal supervisor release deletes local state with best-effort peer notices.';
|
|
81
|
+
if (actual.isError || !(roleSuffix || (selected.temporary && suffix === '.'))
|
|
82
|
+
|| (selected.temporary ? lines?.[1] !== temporaryLine : lines?.[1]?.startsWith('TEMPORARY')))
|
|
83
|
+
throw new FleetError('capability_unavailable', 'connected supervisor identity does not match legacy ownership proof');
|
|
84
|
+
const fresh = legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation);
|
|
85
|
+
if (!('proof' in identity) || fresh.proof !== identity.proof)
|
|
86
|
+
throw new FleetError('capability_unavailable', 'legacy supervisor identity changed');
|
|
87
|
+
verifySelection();
|
|
88
|
+
};
|
|
89
|
+
await verify();
|
|
90
|
+
return await work(client, { name: identity.name, cid: identity.cid, generation: identity.generation }, verify);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof FleetError)
|
|
94
|
+
throw error;
|
|
95
|
+
// Do not leak arguments, invites, capabilities, or raw transport errors.
|
|
96
|
+
throw new FleetError('control_unavailable', 'supervisor tool request failed; its outcome may be unknown, do not retry a mutation automatically', { retryable: false });
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await client.close().catch(() => { });
|
|
100
|
+
await transport.close().catch(() => { });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
list(role) {
|
|
104
|
+
return this.withClient(role, async (client, identity) => ({
|
|
105
|
+
agent: role, identity, ...(await client.listTools()),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
call(role, request) {
|
|
109
|
+
if (!request || typeof request.tool !== 'string'
|
|
110
|
+
|| (request.arguments !== undefined && (!request.arguments || typeof request.arguments !== 'object' || Array.isArray(request.arguments))))
|
|
111
|
+
throw new FleetError('invalid_request', 'tool and object arguments are required');
|
|
112
|
+
return this.withClient(role, async (client, identity, verify) => {
|
|
113
|
+
const { tools } = await client.listTools();
|
|
114
|
+
if (!tools.some(tool => tool.name === request.tool))
|
|
115
|
+
throw new FleetError('forbidden', 'tool is not exposed by the fixed-identity supervisor');
|
|
116
|
+
await verify();
|
|
117
|
+
const result = await client.callTool({ name: request.tool, arguments: request.arguments ?? {} });
|
|
118
|
+
return { agent: role, identity, result };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LiveReadinessDeps } from '../rooms-tasks/live-readiness.js';
|
|
1
2
|
import { type FleetConfig } from '../config.js';
|
|
2
3
|
import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
3
4
|
import { provisionMembers } from '../rooms-tasks/provision.js';
|
|
@@ -57,9 +58,9 @@ export interface TaskProvisioningOutcome {
|
|
|
57
58
|
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
58
59
|
export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
|
|
59
60
|
export declare class TaskRoomApplicationError extends Error {
|
|
60
|
-
readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
|
|
61
|
+
readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready';
|
|
61
62
|
readonly fields: Readonly<Record<string, string>>;
|
|
62
|
-
constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found', message: string, fields?: Readonly<Record<string, string>>);
|
|
63
|
+
constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready', message: string, fields?: Readonly<Record<string, string>>);
|
|
63
64
|
}
|
|
64
65
|
export interface CreateTaskRequest {
|
|
65
66
|
actor: TaskRoomActor;
|
|
@@ -87,6 +88,7 @@ export interface CreateRoomRequest {
|
|
|
87
88
|
}
|
|
88
89
|
export declare function resolveRoomLaunchPolicy(template: TemplateSnapshot | undefined, override: boolean | undefined): RoomLaunchPolicy;
|
|
89
90
|
export interface TaskRoomServiceDeps {
|
|
91
|
+
liveReadiness?: LiveReadinessDeps;
|
|
90
92
|
loadConfiguration?(path?: string): FleetConfig;
|
|
91
93
|
cowork?(config: FleetConfig): CoworkAdapter;
|
|
92
94
|
binPath?(): string;
|
|
@@ -151,6 +153,7 @@ export declare class TaskRoomApplicationService {
|
|
|
151
153
|
task: TaskRecord;
|
|
152
154
|
orchestration: RoomOrchestrationRecord | undefined;
|
|
153
155
|
};
|
|
156
|
+
/** Durable provisioning progress only; use active task start/work for a live readiness check. */
|
|
154
157
|
taskProvisioningOutcome(taskId: string): TaskProvisioningOutcome;
|
|
155
158
|
awaitTaskProvisioning(input: {
|
|
156
159
|
actor: TaskRoomActor;
|
|
@@ -300,6 +303,7 @@ export declare class TaskRoomApplicationService {
|
|
|
300
303
|
taskId: string;
|
|
301
304
|
outcome?: TaskOutcome;
|
|
302
305
|
}): Promise<TaskSettlementPlan>;
|
|
306
|
+
private assertTaskLiveReadiness;
|
|
303
307
|
ensureTaskWork(input: {
|
|
304
308
|
actor: TaskRoomActor;
|
|
305
309
|
taskId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
|
|
2
3
|
import { ConfigError, loadConfig } from '../config.js';
|
|
3
4
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
5
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
@@ -136,7 +137,7 @@ export class TaskRoomApplicationService {
|
|
|
136
137
|
}
|
|
137
138
|
catch (error) {
|
|
138
139
|
if (error instanceof CoworkUnavailableError)
|
|
139
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
140
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
140
141
|
// Once the durable Room exists, provisioning errors are resumable
|
|
141
142
|
// saga state. Return that explicit state so the command can launch a
|
|
142
143
|
// continuation and report the durable in-progress outcome.
|
|
@@ -223,6 +224,7 @@ export class TaskRoomApplicationService {
|
|
|
223
224
|
const task = readTask(taskId);
|
|
224
225
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
225
226
|
}
|
|
227
|
+
/** Durable provisioning progress only; use active task start/work for a live readiness check. */
|
|
226
228
|
taskProvisioningOutcome(taskId) {
|
|
227
229
|
const task = readTask(taskId);
|
|
228
230
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
@@ -234,17 +236,23 @@ export class TaskRoomApplicationService {
|
|
|
234
236
|
const ready = task.state === 'active' && room?.state === 'active'
|
|
235
237
|
&& active === expected && launched === expected;
|
|
236
238
|
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
237
|
-
const nextAction =
|
|
238
|
-
? `
|
|
239
|
-
: room?.
|
|
240
|
-
? `
|
|
241
|
-
: room?.provisioning_detail === '
|
|
242
|
-
? `
|
|
243
|
-
: room?.provisioning_detail === '
|
|
244
|
-
? `
|
|
245
|
-
:
|
|
246
|
-
? `
|
|
247
|
-
:
|
|
239
|
+
const nextAction = task.terminal_intent
|
|
240
|
+
? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
|
|
241
|
+
: room?.state === 'closing' || room?.state === 'closed'
|
|
242
|
+
? `Complete room cleanup; do not restart provisioning.`
|
|
243
|
+
: room?.provisioning_detail === 'member_failed'
|
|
244
|
+
? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
|
|
245
|
+
: room?.provisioning_detail === 'waiting_owner_authorization'
|
|
246
|
+
? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
|
|
247
|
+
: room?.provisioning_detail === 'waiting_owner_invite'
|
|
248
|
+
? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
|
|
249
|
+
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
250
|
+
? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
|
|
251
|
+
: room?.provisioning_detail === 'waiting_cowork'
|
|
252
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
253
|
+
: failed
|
|
254
|
+
? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
|
|
255
|
+
: undefined;
|
|
248
256
|
return {
|
|
249
257
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
250
258
|
launch: {
|
|
@@ -265,6 +273,10 @@ export class TaskRoomApplicationService {
|
|
|
265
273
|
const deadline = now() + (input.waitMs ?? 60_000);
|
|
266
274
|
for (;;) {
|
|
267
275
|
const outcome = this.taskProvisioningOutcome(input.taskId);
|
|
276
|
+
if (outcome.kind === 'ready') {
|
|
277
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
278
|
+
await this.assertTaskLiveReadiness(outcome.task, outcome.room, cfg);
|
|
279
|
+
}
|
|
268
280
|
if (outcome.kind !== 'in_progress' || now() >= deadline) {
|
|
269
281
|
recordTaskProvisioningOutcome(outcome);
|
|
270
282
|
return outcome;
|
|
@@ -410,7 +422,9 @@ export class TaskRoomApplicationService {
|
|
|
410
422
|
let task = readTask(input.taskId);
|
|
411
423
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
412
424
|
const issues = [];
|
|
413
|
-
if (task.state !== 'provisioning'
|
|
425
|
+
if (task.state !== 'provisioning' || task.terminal_intent
|
|
426
|
+
|| room?.state === 'closing' || room?.state === 'closed'
|
|
427
|
+
|| room?.provisioning_detail === 'member_failed')
|
|
414
428
|
return {
|
|
415
429
|
kind: 'no_op', task, room, issues,
|
|
416
430
|
};
|
|
@@ -649,6 +663,14 @@ export class TaskRoomApplicationService {
|
|
|
649
663
|
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
650
664
|
return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
|
|
651
665
|
}
|
|
666
|
+
async assertTaskLiveReadiness(task, room, cfg) {
|
|
667
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg)
|
|
668
|
+
: createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config });
|
|
669
|
+
const issue = await taskLiveReadiness(task, room, cowork, this.deps.liveReadiness);
|
|
670
|
+
if (issue)
|
|
671
|
+
throw new TaskRoomApplicationError('task_not_ready', `Task readiness is ${issue.state}: ${issue.reason}. Ask Fleet Coordinator to inspect the room and member status and choose supported recovery; do not blindly relaunch or reuse invites.`, { task: task.task_id, room: task.room_id ?? 'missing', readiness: issue.state,
|
|
672
|
+
reason: issue.reason, ...(issue.member ? { member: issue.member } : {}) });
|
|
673
|
+
}
|
|
652
674
|
async ensureTaskWork(input) {
|
|
653
675
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
654
676
|
let task = readTask(input.taskId);
|
|
@@ -673,8 +695,10 @@ export class TaskRoomApplicationService {
|
|
|
673
695
|
const pinned = durableTemplate;
|
|
674
696
|
if (!pinned)
|
|
675
697
|
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
676
|
-
if (durableRequestMatches)
|
|
698
|
+
if (durableRequestMatches) {
|
|
699
|
+
await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
|
|
677
700
|
return { task, status: 'already_active' };
|
|
701
|
+
}
|
|
678
702
|
const templateName = input.template ?? pinned.name;
|
|
679
703
|
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
680
704
|
if (!definition)
|
|
@@ -687,6 +711,7 @@ export class TaskRoomApplicationService {
|
|
|
687
711
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
688
712
|
});
|
|
689
713
|
}
|
|
714
|
+
await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
|
|
690
715
|
return { task, status: 'already_active' };
|
|
691
716
|
}
|
|
692
717
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
@@ -784,7 +809,7 @@ export class TaskRoomApplicationService {
|
|
|
784
809
|
}
|
|
785
810
|
catch (error) {
|
|
786
811
|
if (error instanceof CoworkUnavailableError)
|
|
787
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
812
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
788
813
|
const current = readTask(task.task_id);
|
|
789
814
|
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
790
815
|
throw error;
|
|
@@ -802,7 +827,7 @@ export class TaskRoomApplicationService {
|
|
|
802
827
|
}
|
|
803
828
|
catch (error) {
|
|
804
829
|
if (error instanceof CoworkUnavailableError)
|
|
805
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
830
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
806
831
|
task = readTask(task.task_id);
|
|
807
832
|
return { task, status: 'in_progress' };
|
|
808
833
|
}
|
|
@@ -820,12 +845,16 @@ export class TaskRoomApplicationService {
|
|
|
820
845
|
}
|
|
821
846
|
catch (error) {
|
|
822
847
|
if (error instanceof CoworkUnavailableError)
|
|
823
|
-
persistBlockTask(task.task_id, 'Cowork management
|
|
848
|
+
persistBlockTask(task.task_id, 'Cowork management is unavailable');
|
|
824
849
|
task = readTask(task.task_id);
|
|
825
850
|
}
|
|
826
851
|
}
|
|
827
852
|
const resultingRoom = getRoomRecord(task.room_id);
|
|
828
|
-
|
|
853
|
+
if (task.state === 'active' && resultingRoom?.state === 'active') {
|
|
854
|
+
await this.assertTaskLiveReadiness(task, resultingRoom, cfg);
|
|
855
|
+
return { task, status: 'ready' };
|
|
856
|
+
}
|
|
857
|
+
return { task, status: 'in_progress' };
|
|
829
858
|
}
|
|
830
859
|
createTemplate(cfg, requested, noRoom) {
|
|
831
860
|
if (noRoom)
|
|
@@ -864,6 +893,7 @@ export class TaskRoomApplicationService {
|
|
|
864
893
|
const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
|
|
865
894
|
let launchTemplate;
|
|
866
895
|
let room;
|
|
896
|
+
let reusedRoom = false;
|
|
867
897
|
try {
|
|
868
898
|
launchTemplate = template ? (template.launch_snapshot_hash ? template
|
|
869
899
|
: sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
|
|
@@ -877,19 +907,45 @@ export class TaskRoomApplicationService {
|
|
|
877
907
|
const fresh = readTask(task.task_id);
|
|
878
908
|
if (fresh.deletion?.status === 'pending')
|
|
879
909
|
throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
|
|
910
|
+
// A concurrent caller or a restart after local Room publication must
|
|
911
|
+
// reuse the task's one durable room, never create a second workspace/room.
|
|
912
|
+
const candidates = listRoomRecords().filter(record => record.task_id === task.task_id);
|
|
913
|
+
if (candidates.length > 1)
|
|
914
|
+
throw new Error('ambiguous task room ownership');
|
|
915
|
+
const existing = fresh.room_id ? getRoomRecord(fresh.room_id) : candidates[0];
|
|
916
|
+
if (fresh.room_id && !existing)
|
|
917
|
+
throw new Error('recorded task room is missing; refusing duplicate creation');
|
|
918
|
+
if (existing) {
|
|
919
|
+
reusedRoom = true;
|
|
920
|
+
// Heal a crash between durable room publication and task linkage.
|
|
921
|
+
// Return a resumable phase, leaving owner admission/member launch to
|
|
922
|
+
// the existing continuation rather than racing the original creator.
|
|
923
|
+
const resumable = ['persist_intent', 'create_room'].includes(existing.saga.phase)
|
|
924
|
+
? advanceSaga(existing.room_id, attachOwner ? 'attach_owner' : 'create_members', attachOwner ? 2 : 3)
|
|
925
|
+
: existing;
|
|
926
|
+
onCreated(resumable);
|
|
927
|
+
return resumable;
|
|
928
|
+
}
|
|
880
929
|
}
|
|
930
|
+
const requiredRoles = new Map();
|
|
931
|
+
if (attachOwner)
|
|
932
|
+
requiredRoles.set(rooms.owner.role, 1);
|
|
933
|
+
for (const member of launchTemplate?.members ?? [])
|
|
934
|
+
requiredRoles.set(member.role, (requiredRoles.get(member.role) ?? 0) + member.count);
|
|
881
935
|
const created = await cowork.createRoom({
|
|
882
936
|
room_name: roomName, goal: task.goal?.trim() || task.title,
|
|
883
937
|
briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
|
|
884
938
|
quiet_membership: launchTemplate?.room?.quiet_membership,
|
|
885
939
|
anonymous: policy.anonymous,
|
|
940
|
+
activation_requirements: [...requiredRoles].map(([role, count]) => ({ role, count })),
|
|
886
941
|
});
|
|
887
942
|
const record = createRoomRecord({
|
|
888
943
|
room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
|
|
889
944
|
task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
|
|
890
945
|
});
|
|
891
|
-
|
|
892
|
-
|
|
946
|
+
const resumable = advanceSaga(record.room_id, attachOwner ? 'attach_owner' : 'create_members', attachOwner ? 2 : 3);
|
|
947
|
+
onCreated(resumable);
|
|
948
|
+
return resumable;
|
|
893
949
|
};
|
|
894
950
|
room = task.task_id
|
|
895
951
|
? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
|
|
@@ -902,7 +958,8 @@ export class TaskRoomApplicationService {
|
|
|
902
958
|
throw error;
|
|
903
959
|
}
|
|
904
960
|
unlockSnapshot?.();
|
|
905
|
-
|
|
961
|
+
if (reusedRoom)
|
|
962
|
+
return room;
|
|
906
963
|
if (attachOwner) {
|
|
907
964
|
room = advanceSaga(room.room_id, 'attach_owner', 2);
|
|
908
965
|
await this.setOwnerRoomCommands(cowork, room.room_id, rooms.owner.role);
|