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.
Files changed (76) hide show
  1. package/LICENSE +211 -0
  2. package/README.md +112 -0
  3. package/SECURITY.md +27 -0
  4. package/SUPPORT.md +43 -0
  5. package/package.json +44 -0
  6. package/public/client-shared.js +1831 -0
  7. package/public/favicon.svg +11 -0
  8. package/public/host.html +29 -0
  9. package/public/host.js +2079 -0
  10. package/public/index.html +28 -0
  11. package/public/index.js +98 -0
  12. package/public/live-bridge-lifecycle.js +258 -0
  13. package/public/live-bridge-retry-state.js +61 -0
  14. package/public/live-selection-intent.js +79 -0
  15. package/public/remote-operator-state.js +316 -0
  16. package/public/remote.html +167 -0
  17. package/public/remote.js +3967 -0
  18. package/public/styles.css +2793 -0
  19. package/public/surface-view-state.js +89 -0
  20. package/public/voice-dictation.js +45 -0
  21. package/src/bin/desktop-rehydration-smoke.mjs +111 -0
  22. package/src/bin/dextunnel.mjs +41 -0
  23. package/src/bin/doctor.mjs +48 -0
  24. package/src/bin/launch-attest.mjs +39 -0
  25. package/src/bin/launch-status.mjs +49 -0
  26. package/src/bin/mobile-link-proxy.mjs +221 -0
  27. package/src/bin/mobile-proof.mjs +164 -0
  28. package/src/bin/mobile-transport-smoke.mjs +200 -0
  29. package/src/bin/probe-codex-app-server-write.mjs +36 -0
  30. package/src/bin/probe-codex-app-server.mjs +30 -0
  31. package/src/lib/agent-room-context.mjs +54 -0
  32. package/src/lib/agent-room-runtime.mjs +355 -0
  33. package/src/lib/agent-room-service.mjs +335 -0
  34. package/src/lib/agent-room-state.mjs +406 -0
  35. package/src/lib/agent-room-store.mjs +71 -0
  36. package/src/lib/agent-room-text.mjs +48 -0
  37. package/src/lib/app-server-contract.mjs +66 -0
  38. package/src/lib/app-server-runtime.mjs +60 -0
  39. package/src/lib/attachment-service.mjs +119 -0
  40. package/src/lib/bridge-api-handler.mjs +719 -0
  41. package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
  42. package/src/lib/bridge-status-builder.mjs +60 -0
  43. package/src/lib/codex-app-server-client.mjs +1511 -0
  44. package/src/lib/companion-state.mjs +453 -0
  45. package/src/lib/control-lease-service.mjs +180 -0
  46. package/src/lib/debug-harness-service.mjs +173 -0
  47. package/src/lib/desktop-integration.mjs +146 -0
  48. package/src/lib/desktop-rehydration-smoke.mjs +269 -0
  49. package/src/lib/dextunnel-cli.mjs +122 -0
  50. package/src/lib/discovery-docs.mjs +1321 -0
  51. package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
  52. package/src/lib/install-preflight.mjs +373 -0
  53. package/src/lib/interaction-resolution-service.mjs +185 -0
  54. package/src/lib/interaction-state.mjs +360 -0
  55. package/src/lib/launch-release-bar.mjs +158 -0
  56. package/src/lib/live-control-state.mjs +107 -0
  57. package/src/lib/live-payload-builder.mjs +298 -0
  58. package/src/lib/live-selection-transition-state.mjs +49 -0
  59. package/src/lib/live-transcript-state.mjs +549 -0
  60. package/src/lib/mobile-network-profile.mjs +39 -0
  61. package/src/lib/mock-codex-adapter.mjs +62 -0
  62. package/src/lib/operator-diagnostics.mjs +82 -0
  63. package/src/lib/repo-changes-service.mjs +527 -0
  64. package/src/lib/runtime-config.mjs +106 -0
  65. package/src/lib/selection-state-service.mjs +214 -0
  66. package/src/lib/session-store.mjs +355 -0
  67. package/src/lib/shared-room-state.mjs +473 -0
  68. package/src/lib/shared-selection-state.mjs +40 -0
  69. package/src/lib/sse-hub.mjs +35 -0
  70. package/src/lib/static-surface-service.mjs +71 -0
  71. package/src/lib/surface-access.mjs +189 -0
  72. package/src/lib/surface-presence-service.mjs +118 -0
  73. package/src/lib/surface-request-guard.mjs +52 -0
  74. package/src/lib/thread-sync-state.mjs +536 -0
  75. package/src/lib/watcher-lifecycle.mjs +287 -0
  76. 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
+ }