dextunnel 0.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/LICENSE +211 -0
- package/README.md +112 -0
- package/SECURITY.md +27 -0
- package/SUPPORT.md +43 -0
- package/package.json +44 -0
- package/public/client-shared.js +1831 -0
- package/public/favicon.svg +11 -0
- package/public/host.html +29 -0
- package/public/host.js +2079 -0
- package/public/index.html +28 -0
- package/public/index.js +98 -0
- package/public/live-bridge-lifecycle.js +258 -0
- package/public/live-bridge-retry-state.js +61 -0
- package/public/live-selection-intent.js +79 -0
- package/public/remote-operator-state.js +316 -0
- package/public/remote.html +167 -0
- package/public/remote.js +3967 -0
- package/public/styles.css +2793 -0
- package/public/surface-view-state.js +89 -0
- package/public/voice-dictation.js +45 -0
- package/src/bin/desktop-rehydration-smoke.mjs +111 -0
- package/src/bin/dextunnel.mjs +41 -0
- package/src/bin/doctor.mjs +48 -0
- package/src/bin/launch-attest.mjs +39 -0
- package/src/bin/launch-status.mjs +49 -0
- package/src/bin/mobile-link-proxy.mjs +221 -0
- package/src/bin/mobile-proof.mjs +164 -0
- package/src/bin/mobile-transport-smoke.mjs +200 -0
- package/src/bin/probe-codex-app-server-write.mjs +36 -0
- package/src/bin/probe-codex-app-server.mjs +30 -0
- package/src/lib/agent-room-context.mjs +54 -0
- package/src/lib/agent-room-runtime.mjs +355 -0
- package/src/lib/agent-room-service.mjs +335 -0
- package/src/lib/agent-room-state.mjs +406 -0
- package/src/lib/agent-room-store.mjs +71 -0
- package/src/lib/agent-room-text.mjs +48 -0
- package/src/lib/app-server-contract.mjs +66 -0
- package/src/lib/app-server-runtime.mjs +60 -0
- package/src/lib/attachment-service.mjs +119 -0
- package/src/lib/bridge-api-handler.mjs +719 -0
- package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
- package/src/lib/bridge-status-builder.mjs +60 -0
- package/src/lib/codex-app-server-client.mjs +1511 -0
- package/src/lib/companion-state.mjs +453 -0
- package/src/lib/control-lease-service.mjs +180 -0
- package/src/lib/debug-harness-service.mjs +173 -0
- package/src/lib/desktop-integration.mjs +146 -0
- package/src/lib/desktop-rehydration-smoke.mjs +269 -0
- package/src/lib/dextunnel-cli.mjs +122 -0
- package/src/lib/discovery-docs.mjs +1321 -0
- package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
- package/src/lib/install-preflight.mjs +373 -0
- package/src/lib/interaction-resolution-service.mjs +185 -0
- package/src/lib/interaction-state.mjs +360 -0
- package/src/lib/launch-release-bar.mjs +158 -0
- package/src/lib/live-control-state.mjs +107 -0
- package/src/lib/live-payload-builder.mjs +298 -0
- package/src/lib/live-selection-transition-state.mjs +49 -0
- package/src/lib/live-transcript-state.mjs +549 -0
- package/src/lib/mobile-network-profile.mjs +39 -0
- package/src/lib/mock-codex-adapter.mjs +62 -0
- package/src/lib/operator-diagnostics.mjs +82 -0
- package/src/lib/repo-changes-service.mjs +527 -0
- package/src/lib/runtime-config.mjs +106 -0
- package/src/lib/selection-state-service.mjs +214 -0
- package/src/lib/session-store.mjs +355 -0
- package/src/lib/shared-room-state.mjs +473 -0
- package/src/lib/shared-selection-state.mjs +40 -0
- package/src/lib/sse-hub.mjs +35 -0
- package/src/lib/static-surface-service.mjs +71 -0
- package/src/lib/surface-access.mjs +189 -0
- package/src/lib/surface-presence-service.mjs +118 -0
- package/src/lib/surface-request-guard.mjs +52 -0
- package/src/lib/thread-sync-state.mjs +536 -0
- package/src/lib/watcher-lifecycle.mjs +287 -0
- package/src/server.mjs +1446 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { AGENT_ROOM_MEMBER_IDS } from "./agent-room-state.mjs";
|
|
7
|
+
import { normalizeAgentRoomReply } from "./agent-room-text.mjs";
|
|
8
|
+
|
|
9
|
+
function scriptPath(...parts) {
|
|
10
|
+
return path.join(process.env.HOME || "", ".agents", "skills", ...parts);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildLanePrompt(participantId, promptText = "") {
|
|
14
|
+
return [
|
|
15
|
+
`You are ${participantId} in the Dextunnel council room.`,
|
|
16
|
+
"This is an advisory-only group discussion, not the main Codex control lane.",
|
|
17
|
+
"Reply as that named participant, directly to the room.",
|
|
18
|
+
"Be concise but useful. It is okay to disagree with the other participants.",
|
|
19
|
+
"Do not claim you executed tools or changed files unless the provided context explicitly says so.",
|
|
20
|
+
"",
|
|
21
|
+
"Latest room prompt:",
|
|
22
|
+
promptText
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveOracleLaneConfig() {
|
|
27
|
+
const remoteChrome = String(
|
|
28
|
+
process.env.DEXTUNNEL_ORACLE_REMOTE_CHROME || process.env.ORACLE_REMOTE_CHROME || ""
|
|
29
|
+
).trim();
|
|
30
|
+
const projectUrl = String(
|
|
31
|
+
process.env.DEXTUNNEL_ORACLE_PROJECT_URL || process.env.ORACLE_PROJECT_URL || ""
|
|
32
|
+
).trim();
|
|
33
|
+
|
|
34
|
+
if (!remoteChrome || !projectUrl) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Oracle lane requires DEXTUNNEL_ORACLE_REMOTE_CHROME/ORACLE_REMOTE_CHROME and DEXTUNNEL_ORACLE_PROJECT_URL/ORACLE_PROJECT_URL."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { projectUrl, remoteChrome };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function spawnAndCollect(command, args, { cwd = process.cwd(), env = process.env, stdin = "" } = {}) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const child = spawn(command, args, {
|
|
46
|
+
cwd,
|
|
47
|
+
env,
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
49
|
+
});
|
|
50
|
+
let stdout = "";
|
|
51
|
+
let stderr = "";
|
|
52
|
+
|
|
53
|
+
child.stdout.setEncoding("utf8");
|
|
54
|
+
child.stderr.setEncoding("utf8");
|
|
55
|
+
child.stdout.on("data", (chunk) => {
|
|
56
|
+
stdout += chunk;
|
|
57
|
+
});
|
|
58
|
+
child.stderr.on("data", (chunk) => {
|
|
59
|
+
stderr += chunk;
|
|
60
|
+
});
|
|
61
|
+
child.on("error", reject);
|
|
62
|
+
child.on("close", (code) => {
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
resolve({ stderr, stdout });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
reject(new Error(stderr.trim() || stdout.trim() || `${command} exited with ${code}`));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (stdin) {
|
|
71
|
+
child.stdin.write(stdin);
|
|
72
|
+
}
|
|
73
|
+
child.stdin.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runLoggedWrapper(script, args, options = {}) {
|
|
78
|
+
if (!script) {
|
|
79
|
+
throw new Error("Missing lane wrapper.");
|
|
80
|
+
}
|
|
81
|
+
return spawnAndCollect(script, args, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runNixLane({
|
|
85
|
+
codexBinaryPath,
|
|
86
|
+
contextFile,
|
|
87
|
+
cwd = process.cwd(),
|
|
88
|
+
promptText,
|
|
89
|
+
roundDir
|
|
90
|
+
} = {}) {
|
|
91
|
+
await mkdir(roundDir, { recursive: true });
|
|
92
|
+
const requestFile = path.join(roundDir, "nix.request.txt");
|
|
93
|
+
const request = [
|
|
94
|
+
"You are Nix, a thoughtful Dextunnel council-room participant.",
|
|
95
|
+
"This is advisory discussion only.",
|
|
96
|
+
"Reply in first person as Nix, directly to the room, with a concise but concrete response.",
|
|
97
|
+
"",
|
|
98
|
+
"# Prompt",
|
|
99
|
+
buildLanePrompt("nix", promptText),
|
|
100
|
+
"",
|
|
101
|
+
"# Context",
|
|
102
|
+
await readFile(contextFile, "utf8")
|
|
103
|
+
].join("\n");
|
|
104
|
+
|
|
105
|
+
await writeFile(requestFile, request, "utf8");
|
|
106
|
+
|
|
107
|
+
const { stdout } = await spawnAndCollect(
|
|
108
|
+
codexBinaryPath || "codex",
|
|
109
|
+
[
|
|
110
|
+
"exec",
|
|
111
|
+
"--ephemeral",
|
|
112
|
+
"--sandbox",
|
|
113
|
+
"read-only",
|
|
114
|
+
"--skip-git-repo-check",
|
|
115
|
+
"-C",
|
|
116
|
+
cwd,
|
|
117
|
+
"-"
|
|
118
|
+
],
|
|
119
|
+
{
|
|
120
|
+
cwd,
|
|
121
|
+
stdin: request
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return stdout.trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createAgentRoomRuntime({
|
|
129
|
+
artifactsDir,
|
|
130
|
+
codexBinaryPath,
|
|
131
|
+
cwd = process.cwd(),
|
|
132
|
+
fake = false,
|
|
133
|
+
fakeFailures = {},
|
|
134
|
+
now = () => new Date().toISOString(),
|
|
135
|
+
participantRunner = null,
|
|
136
|
+
participantTimeoutMs = 5 * 60 * 1000
|
|
137
|
+
} = {}) {
|
|
138
|
+
if (!artifactsDir) {
|
|
139
|
+
throw new Error("createAgentRoomRuntime requires artifactsDir.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const pendingFakeFailures = new Map(
|
|
143
|
+
Object.entries(fakeFailures || {})
|
|
144
|
+
.map(([participantId, spec]) => {
|
|
145
|
+
const nextParticipantId = String(participantId || "").trim().toLowerCase();
|
|
146
|
+
const mode = String(spec?.mode || "").trim().toLowerCase();
|
|
147
|
+
const count = Math.max(1, Number(spec?.count || 1) || 1);
|
|
148
|
+
if (!nextParticipantId || !["timeout", "malformed", "error"].includes(mode)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return [nextParticipantId, { count, mode }];
|
|
152
|
+
})
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
async function createRoundDir(threadId, roundId) {
|
|
157
|
+
const root = path.join(artifactsDir, sanitize(threadId), sanitize(roundId));
|
|
158
|
+
await mkdir(root, { recursive: true });
|
|
159
|
+
return mkdtemp(path.join(root, "run-"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function executeParticipant({
|
|
163
|
+
contextFile,
|
|
164
|
+
participantId,
|
|
165
|
+
promptText,
|
|
166
|
+
roundDir
|
|
167
|
+
} = {}) {
|
|
168
|
+
if (participantRunner) {
|
|
169
|
+
return participantRunner({
|
|
170
|
+
contextFile,
|
|
171
|
+
participantId,
|
|
172
|
+
promptText,
|
|
173
|
+
roundDir
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (fake) {
|
|
178
|
+
const failureSpec = pendingFakeFailures.get(participantId);
|
|
179
|
+
if (failureSpec) {
|
|
180
|
+
if (failureSpec.count > 1) {
|
|
181
|
+
pendingFakeFailures.set(participantId, {
|
|
182
|
+
...failureSpec,
|
|
183
|
+
count: failureSpec.count - 1
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
pendingFakeFailures.delete(participantId);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (failureSpec.mode === "timeout") {
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
191
|
+
throw new Error(`${participantId} timed out after ${participantTimeoutMs}ms.`);
|
|
192
|
+
} else if (failureSpec.mode === "malformed") {
|
|
193
|
+
return " ";
|
|
194
|
+
} else {
|
|
195
|
+
throw new Error(`${participantId} fake lane failure.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, participantId === "oracle" ? 80 : 20));
|
|
199
|
+
return `${participantId}: ${promptText}`.trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
switch (participantId) {
|
|
203
|
+
case "spark": {
|
|
204
|
+
const { stdout } = await runLoggedWrapper(
|
|
205
|
+
scriptPath("spark", "scripts", "spark_logged.sh"),
|
|
206
|
+
[
|
|
207
|
+
"--slug",
|
|
208
|
+
`agent-room-${participantId}`,
|
|
209
|
+
"--context-file",
|
|
210
|
+
contextFile,
|
|
211
|
+
"--prompt",
|
|
212
|
+
buildLanePrompt(participantId, promptText)
|
|
213
|
+
],
|
|
214
|
+
{ cwd }
|
|
215
|
+
);
|
|
216
|
+
return normalizeAgentRoomReply(participantId, stdout);
|
|
217
|
+
}
|
|
218
|
+
case "gemini": {
|
|
219
|
+
const { stdout } = await runLoggedWrapper(
|
|
220
|
+
scriptPath("gemini", "scripts", "gemini_logged.sh"),
|
|
221
|
+
[
|
|
222
|
+
"--slug",
|
|
223
|
+
`agent-room-${participantId}`,
|
|
224
|
+
"--context-file",
|
|
225
|
+
contextFile,
|
|
226
|
+
"--prompt",
|
|
227
|
+
buildLanePrompt(participantId, promptText)
|
|
228
|
+
],
|
|
229
|
+
{ cwd }
|
|
230
|
+
);
|
|
231
|
+
return normalizeAgentRoomReply(participantId, stdout);
|
|
232
|
+
}
|
|
233
|
+
case "claude": {
|
|
234
|
+
const { stdout } = await runLoggedWrapper(
|
|
235
|
+
scriptPath("claude", "scripts", "claude_logged.sh"),
|
|
236
|
+
[
|
|
237
|
+
"--slug",
|
|
238
|
+
`agent-room-${participantId}`,
|
|
239
|
+
"--context-file",
|
|
240
|
+
contextFile,
|
|
241
|
+
"--prompt",
|
|
242
|
+
buildLanePrompt(participantId, promptText)
|
|
243
|
+
],
|
|
244
|
+
{ cwd }
|
|
245
|
+
);
|
|
246
|
+
return normalizeAgentRoomReply(participantId, stdout);
|
|
247
|
+
}
|
|
248
|
+
case "oracle": {
|
|
249
|
+
const { projectUrl, remoteChrome } = resolveOracleLaneConfig();
|
|
250
|
+
const { stdout } = await runLoggedWrapper(
|
|
251
|
+
scriptPath("oracle", "scripts", "oracle_logged.sh"),
|
|
252
|
+
[
|
|
253
|
+
"--engine",
|
|
254
|
+
"browser",
|
|
255
|
+
"--remote-chrome",
|
|
256
|
+
remoteChrome,
|
|
257
|
+
"--chatgpt-url",
|
|
258
|
+
projectUrl,
|
|
259
|
+
"--browser-model-strategy",
|
|
260
|
+
"current",
|
|
261
|
+
"--file",
|
|
262
|
+
contextFile,
|
|
263
|
+
"-p",
|
|
264
|
+
buildLanePrompt(participantId, promptText)
|
|
265
|
+
],
|
|
266
|
+
{ cwd }
|
|
267
|
+
);
|
|
268
|
+
return normalizeAgentRoomReply(participantId, stdout);
|
|
269
|
+
}
|
|
270
|
+
case "nix":
|
|
271
|
+
return normalizeAgentRoomReply(
|
|
272
|
+
participantId,
|
|
273
|
+
await runNixLane({
|
|
274
|
+
codexBinaryPath,
|
|
275
|
+
contextFile,
|
|
276
|
+
cwd,
|
|
277
|
+
promptText,
|
|
278
|
+
roundDir
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
default:
|
|
282
|
+
throw new Error(`Unsupported council participant: ${participantId}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runParticipant(args = {}) {
|
|
287
|
+
const { participantId } = args;
|
|
288
|
+
const timeoutLabel = `${participantId} timed out after ${participantTimeoutMs}ms.`;
|
|
289
|
+
let timer = null;
|
|
290
|
+
try {
|
|
291
|
+
const raw = await Promise.race([
|
|
292
|
+
executeParticipant(args),
|
|
293
|
+
new Promise((_, reject) => {
|
|
294
|
+
timer = setTimeout(() => {
|
|
295
|
+
reject(new Error(timeoutLabel));
|
|
296
|
+
}, participantTimeoutMs);
|
|
297
|
+
timer.unref?.();
|
|
298
|
+
})
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const normalized = normalizeAgentRoomReply(participantId, raw || "");
|
|
302
|
+
if (!normalized.trim()) {
|
|
303
|
+
throw new Error(`${participantId} returned a malformed reply.`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return normalized;
|
|
307
|
+
} finally {
|
|
308
|
+
if (timer) {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function runRound({
|
|
315
|
+
contextMarkdown,
|
|
316
|
+
participantIds = AGENT_ROOM_MEMBER_IDS,
|
|
317
|
+
promptText,
|
|
318
|
+
roundId,
|
|
319
|
+
threadId
|
|
320
|
+
} = {}) {
|
|
321
|
+
const roundDir = await createRoundDir(threadId, roundId);
|
|
322
|
+
const contextFile = path.join(roundDir, "context.md");
|
|
323
|
+
await writeFile(contextFile, contextMarkdown, "utf8");
|
|
324
|
+
|
|
325
|
+
return Promise.allSettled(
|
|
326
|
+
participantIds.map(async (participantId) => ({
|
|
327
|
+
participantId,
|
|
328
|
+
text: await runParticipant({
|
|
329
|
+
contextFile,
|
|
330
|
+
participantId,
|
|
331
|
+
promptText,
|
|
332
|
+
roundDir
|
|
333
|
+
})
|
|
334
|
+
}))
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
async prepareRound({ contextMarkdown, roundId, threadId } = {}) {
|
|
340
|
+
const roundDir = await createRoundDir(threadId, roundId);
|
|
341
|
+
const contextFile = path.join(roundDir, "context.md");
|
|
342
|
+
await writeFile(contextFile, contextMarkdown, "utf8");
|
|
343
|
+
return {
|
|
344
|
+
contextFile,
|
|
345
|
+
roundDir
|
|
346
|
+
};
|
|
347
|
+
},
|
|
348
|
+
runParticipant,
|
|
349
|
+
runRound
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function sanitize(value = "") {
|
|
354
|
+
return String(value || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-") || "item";
|
|
355
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
export function createAgentRoomService({
|
|
2
|
+
buildParticipant,
|
|
3
|
+
broadcast = () => {},
|
|
4
|
+
codexAppServer,
|
|
5
|
+
defaultAgentRoomState,
|
|
6
|
+
getBuildAgentRoomContextMarkdown = () => (() => ""),
|
|
7
|
+
getLivePayload = () => ({}),
|
|
8
|
+
getAgentRoomRetryRound,
|
|
9
|
+
interruptAgentRoomRound,
|
|
10
|
+
liveState,
|
|
11
|
+
mapThreadToCompanionSnapshot,
|
|
12
|
+
normalizeAgentRoomState,
|
|
13
|
+
nowIso = () => new Date().toISOString(),
|
|
14
|
+
persistState = async () => {},
|
|
15
|
+
randomId = () => `${Date.now()}`,
|
|
16
|
+
runtime,
|
|
17
|
+
setAgentRoomEnabled,
|
|
18
|
+
settleAgentRoomParticipant,
|
|
19
|
+
startAgentRoomRound,
|
|
20
|
+
store
|
|
21
|
+
} = {}) {
|
|
22
|
+
function getThreadAgentRoomState(threadId) {
|
|
23
|
+
const id = String(threadId || "").trim();
|
|
24
|
+
if (!id) {
|
|
25
|
+
return defaultAgentRoomState();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return liveState.agentRoomByThreadId[id] || defaultAgentRoomState({ threadId: id });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setThreadAgentRoomState(threadId, state) {
|
|
32
|
+
const id = String(threadId || "").trim();
|
|
33
|
+
if (!id) {
|
|
34
|
+
return defaultAgentRoomState();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nextState = normalizeAgentRoomState(state, { threadId: id });
|
|
38
|
+
liveState.agentRoomByThreadId = {
|
|
39
|
+
...liveState.agentRoomByThreadId,
|
|
40
|
+
[id]: nextState
|
|
41
|
+
};
|
|
42
|
+
return nextState;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadThreadAgentRoomState(threadId) {
|
|
46
|
+
const id = String(threadId || "").trim();
|
|
47
|
+
if (!id) {
|
|
48
|
+
return defaultAgentRoomState();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const existing = liveState.agentRoomByThreadId[id];
|
|
52
|
+
if (existing) {
|
|
53
|
+
return existing;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const loaded = await store.load(id);
|
|
57
|
+
const nextState = setThreadAgentRoomState(id, loaded);
|
|
58
|
+
await persistState(id, nextState);
|
|
59
|
+
return nextState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function persistThreadAgentRoomState(threadId, state = null) {
|
|
63
|
+
const id = String(threadId || "").trim();
|
|
64
|
+
if (!id) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await persistState(id, state || getThreadAgentRoomState(id));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function agentRoomMemberParticipant(participantId, state = null) {
|
|
72
|
+
const activeRound = state?.currentRound || null;
|
|
73
|
+
const pending = activeRound?.pendingParticipantIds?.includes(participantId);
|
|
74
|
+
const completed = activeRound?.completedParticipantIds?.includes(participantId);
|
|
75
|
+
const failed = activeRound?.failedParticipantIds?.includes(participantId);
|
|
76
|
+
const metaLabel = pending ? "thinking" : failed ? "failed" : completed ? "replied" : "room";
|
|
77
|
+
return buildParticipant(participantId, {
|
|
78
|
+
metaLabel,
|
|
79
|
+
state: pending ? "ready" : failed ? "dormant" : completed ? "ready" : "dormant"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildSelectedAgentRoomState(threadId = liveState.selectedThreadId || null) {
|
|
84
|
+
const id = String(threadId || "").trim();
|
|
85
|
+
const state = getThreadAgentRoomState(id);
|
|
86
|
+
const messages = state.messages
|
|
87
|
+
.map((message) => ({
|
|
88
|
+
...message,
|
|
89
|
+
kind:
|
|
90
|
+
String(message.note || "").startsWith("lane ") || message.note === "malformed reply"
|
|
91
|
+
? "commentary"
|
|
92
|
+
: "message",
|
|
93
|
+
lane: message.lane || message.participantId,
|
|
94
|
+
origin: message.origin || message.participantId,
|
|
95
|
+
participant: agentRoomMemberParticipant(message.participantId, state)
|
|
96
|
+
}))
|
|
97
|
+
.sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime());
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
enabled: Boolean(state.enabled),
|
|
101
|
+
memberIds: [...state.memberIds],
|
|
102
|
+
messages,
|
|
103
|
+
participants: state.memberIds.map((participantId) => agentRoomMemberParticipant(participantId, state)),
|
|
104
|
+
round:
|
|
105
|
+
state.currentRound
|
|
106
|
+
? {
|
|
107
|
+
...state.currentRound,
|
|
108
|
+
canRetryFailed:
|
|
109
|
+
state.currentRound.status !== "running" &&
|
|
110
|
+
Boolean(state.currentRound.promptText) &&
|
|
111
|
+
state.currentRound.failedParticipantIds.length > 0,
|
|
112
|
+
completedCount: state.currentRound.completedParticipantIds.length,
|
|
113
|
+
failedCount: state.currentRound.failedParticipantIds.length,
|
|
114
|
+
pendingCount: state.currentRound.pendingParticipantIds.length
|
|
115
|
+
}
|
|
116
|
+
: null,
|
|
117
|
+
threadId: state.threadId,
|
|
118
|
+
updatedAt: state.updatedAt || null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadThreadSnapshot(threadId) {
|
|
123
|
+
let snapshot =
|
|
124
|
+
liveState.selectedThreadSnapshot?.thread?.id === threadId
|
|
125
|
+
? liveState.selectedThreadSnapshot
|
|
126
|
+
: null;
|
|
127
|
+
if (!snapshot) {
|
|
128
|
+
try {
|
|
129
|
+
const thread = await codexAppServer.readThread(threadId, true);
|
|
130
|
+
snapshot = thread ? mapThreadToCompanionSnapshot(thread, { limit: 60 }) : null;
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
return snapshot;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function settleRoundParticipant({ participantId, roundId, text = "", threadId, timestamp, error = null }) {
|
|
137
|
+
const refreshed = await loadThreadAgentRoomState(threadId);
|
|
138
|
+
if (!refreshed.enabled || refreshed.currentRound?.id !== roundId) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const nextState = setThreadAgentRoomState(
|
|
142
|
+
threadId,
|
|
143
|
+
settleAgentRoomParticipant(refreshed, {
|
|
144
|
+
error,
|
|
145
|
+
messageId: randomId(),
|
|
146
|
+
participantId,
|
|
147
|
+
roundId,
|
|
148
|
+
text,
|
|
149
|
+
timestamp
|
|
150
|
+
})
|
|
151
|
+
);
|
|
152
|
+
await persistThreadAgentRoomState(threadId, nextState);
|
|
153
|
+
broadcast("live", getLivePayload());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runAgentRoomRound({ promptText, roundId, threadId } = {}) {
|
|
157
|
+
const initialState = await loadThreadAgentRoomState(threadId);
|
|
158
|
+
if (!initialState.enabled || initialState.currentRound?.id !== roundId) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const snapshot = await loadThreadSnapshot(threadId);
|
|
163
|
+
const buildAgentRoomContextMarkdown = getBuildAgentRoomContextMarkdown();
|
|
164
|
+
const prepared = await runtime.prepareRound({
|
|
165
|
+
contextMarkdown: buildAgentRoomContextMarkdown({
|
|
166
|
+
roomState: initialState,
|
|
167
|
+
snapshot,
|
|
168
|
+
threadId
|
|
169
|
+
}),
|
|
170
|
+
roundId,
|
|
171
|
+
threadId
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
for (const participantId of initialState.currentRound.participantIds) {
|
|
175
|
+
const current = await loadThreadAgentRoomState(threadId);
|
|
176
|
+
if (!current.enabled || current.currentRound?.id !== roundId) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await persistState(prepared.contextFile, buildAgentRoomContextMarkdown({
|
|
181
|
+
roomState: current,
|
|
182
|
+
snapshot,
|
|
183
|
+
threadId
|
|
184
|
+
}), { raw: true });
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const text = await runtime.runParticipant({
|
|
188
|
+
contextFile: prepared.contextFile,
|
|
189
|
+
participantId,
|
|
190
|
+
promptText,
|
|
191
|
+
roundDir: prepared.roundDir
|
|
192
|
+
});
|
|
193
|
+
await settleRoundParticipant({
|
|
194
|
+
participantId,
|
|
195
|
+
roundId,
|
|
196
|
+
text,
|
|
197
|
+
threadId,
|
|
198
|
+
timestamp: nowIso()
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
await settleRoundParticipant({
|
|
202
|
+
error: error.message,
|
|
203
|
+
participantId,
|
|
204
|
+
roundId,
|
|
205
|
+
text: "",
|
|
206
|
+
threadId,
|
|
207
|
+
timestamp: nowIso()
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function updateAgentRoom({ action = "", memberIds = null, text = "", threadId = null } = {}) {
|
|
214
|
+
const id = String(threadId || liveState.selectedThreadId || "").trim();
|
|
215
|
+
if (!id) {
|
|
216
|
+
throw new Error("Select a live session before using the council room.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const current = await loadThreadAgentRoomState(id);
|
|
220
|
+
if (action === "enable") {
|
|
221
|
+
const nextState = setThreadAgentRoomState(id, setAgentRoomEnabled(current, true, {
|
|
222
|
+
memberIds,
|
|
223
|
+
timestamp: nowIso()
|
|
224
|
+
}));
|
|
225
|
+
await persistThreadAgentRoomState(id, nextState);
|
|
226
|
+
return {
|
|
227
|
+
message: "Council room enabled.",
|
|
228
|
+
state: nextState
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (action === "disable") {
|
|
233
|
+
const nextState = setThreadAgentRoomState(
|
|
234
|
+
id,
|
|
235
|
+
setAgentRoomEnabled(
|
|
236
|
+
current.currentRound
|
|
237
|
+
? interruptAgentRoomRound(current, {
|
|
238
|
+
note: "Council room disabled. Active discussion stopped.",
|
|
239
|
+
timestamp: nowIso()
|
|
240
|
+
})
|
|
241
|
+
: current,
|
|
242
|
+
false,
|
|
243
|
+
{
|
|
244
|
+
timestamp: nowIso()
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
await persistThreadAgentRoomState(id, nextState);
|
|
249
|
+
return {
|
|
250
|
+
message: "Council room disabled.",
|
|
251
|
+
state: nextState
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (action === "send") {
|
|
256
|
+
const promptText = String(text || "").trim();
|
|
257
|
+
if (!current.enabled) {
|
|
258
|
+
throw new Error("Enable the council room before sending to it.");
|
|
259
|
+
}
|
|
260
|
+
if (!promptText) {
|
|
261
|
+
throw new Error("Council room messages cannot be empty.");
|
|
262
|
+
}
|
|
263
|
+
const roundId = randomId();
|
|
264
|
+
const nextState = setThreadAgentRoomState(
|
|
265
|
+
id,
|
|
266
|
+
startAgentRoomRound(current, {
|
|
267
|
+
messageId: randomId(),
|
|
268
|
+
participantIds: current.memberIds,
|
|
269
|
+
roundId,
|
|
270
|
+
text: promptText,
|
|
271
|
+
timestamp: nowIso()
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
await persistThreadAgentRoomState(id, nextState);
|
|
275
|
+
void runAgentRoomRound({
|
|
276
|
+
promptText,
|
|
277
|
+
roundId,
|
|
278
|
+
threadId: id
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
message: "Council round started.",
|
|
282
|
+
state: nextState
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (action === "retry") {
|
|
287
|
+
if (!current.enabled) {
|
|
288
|
+
throw new Error("Enable the council room before retrying it.");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const retry = getAgentRoomRetryRound(current);
|
|
292
|
+
if (!retry) {
|
|
293
|
+
throw new Error("There is no failed council round ready to retry.");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const roundId = randomId();
|
|
297
|
+
const nextState = setThreadAgentRoomState(
|
|
298
|
+
id,
|
|
299
|
+
startAgentRoomRound(current, {
|
|
300
|
+
messageId: randomId(),
|
|
301
|
+
note: retry.note,
|
|
302
|
+
participantIds: retry.participantIds,
|
|
303
|
+
promptText: retry.promptText,
|
|
304
|
+
retryCount: retry.retryCount,
|
|
305
|
+
roundId,
|
|
306
|
+
text: retry.promptText,
|
|
307
|
+
timestamp: nowIso()
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
await persistThreadAgentRoomState(id, nextState);
|
|
311
|
+
void runAgentRoomRound({
|
|
312
|
+
promptText: retry.promptText,
|
|
313
|
+
roundId,
|
|
314
|
+
threadId: id
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
message: "Retrying failed council participants.",
|
|
318
|
+
state: nextState
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
throw new Error(`Unsupported council room action: ${action}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
agentRoomMemberParticipant,
|
|
327
|
+
buildSelectedAgentRoomState,
|
|
328
|
+
getThreadAgentRoomState,
|
|
329
|
+
loadThreadAgentRoomState,
|
|
330
|
+
persistThreadAgentRoomState,
|
|
331
|
+
runAgentRoomRound,
|
|
332
|
+
setThreadAgentRoomState,
|
|
333
|
+
updateAgentRoom
|
|
334
|
+
};
|
|
335
|
+
}
|