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,406 @@
|
|
|
1
|
+
import { normalizeAgentRoomReply } from "./agent-room-text.mjs";
|
|
2
|
+
|
|
3
|
+
export const AGENT_ROOM_MEMBER_IDS = ["nix", "spark", "gemini", "claude", "oracle"];
|
|
4
|
+
const ROOM_MESSAGE_LIMIT = 120;
|
|
5
|
+
|
|
6
|
+
function isoNow() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function uniqueIds(values = []) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const result = [];
|
|
13
|
+
for (const value of values) {
|
|
14
|
+
const id = String(value || "").trim().toLowerCase();
|
|
15
|
+
if (!id || seen.has(id)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
seen.add(id);
|
|
19
|
+
result.push(id);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeMemberIds(memberIds = AGENT_ROOM_MEMBER_IDS) {
|
|
25
|
+
const normalized = uniqueIds(memberIds).filter((id) => AGENT_ROOM_MEMBER_IDS.includes(id));
|
|
26
|
+
return normalized.length ? normalized : [...AGENT_ROOM_MEMBER_IDS];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRoundMemberIds(memberIds, fallback = []) {
|
|
30
|
+
if (!Array.isArray(memberIds)) {
|
|
31
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return uniqueIds(memberIds).filter((id) => AGENT_ROOM_MEMBER_IDS.includes(id));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeRoundAttemptMap(rawValue, participantIds = []) {
|
|
38
|
+
const source = rawValue && typeof rawValue === "object" ? rawValue : {};
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const participantId of participantIds) {
|
|
41
|
+
const value = Number(source[participantId] || 0);
|
|
42
|
+
result[participantId] = Number.isFinite(value) && value > 0 ? value : 0;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeRoundErrorMap(rawValue, participantIds = []) {
|
|
48
|
+
const source = rawValue && typeof rawValue === "object" ? rawValue : {};
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const participantId of participantIds) {
|
|
51
|
+
const value = String(source[participantId] || "").trim();
|
|
52
|
+
result[participantId] = value || null;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clampMessages(messages = []) {
|
|
58
|
+
if (!Array.isArray(messages) || messages.length <= ROOM_MESSAGE_LIMIT) {
|
|
59
|
+
return Array.isArray(messages) ? messages : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return messages.slice(-ROOM_MESSAGE_LIMIT);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function defaultAgentRoomState({
|
|
66
|
+
enabled = false,
|
|
67
|
+
memberIds = AGENT_ROOM_MEMBER_IDS,
|
|
68
|
+
threadId = "",
|
|
69
|
+
timestamp = isoNow()
|
|
70
|
+
} = {}) {
|
|
71
|
+
return {
|
|
72
|
+
currentRound: null,
|
|
73
|
+
enabled: Boolean(enabled),
|
|
74
|
+
memberIds: normalizeMemberIds(memberIds),
|
|
75
|
+
messages: [],
|
|
76
|
+
threadId: String(threadId || "").trim() || null,
|
|
77
|
+
updatedAt: timestamp
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function normalizeAgentRoomState(rawState, { threadId = "", timestamp = isoNow() } = {}) {
|
|
82
|
+
const base = rawState && typeof rawState === "object" ? rawState : defaultAgentRoomState({ threadId, timestamp });
|
|
83
|
+
const nextThreadId = String(threadId || base.threadId || "").trim() || null;
|
|
84
|
+
const memberIds = normalizeMemberIds(base.memberIds);
|
|
85
|
+
const messages = clampMessages(
|
|
86
|
+
(Array.isArray(base.messages) ? base.messages : [])
|
|
87
|
+
.filter((message) => message && typeof message === "object")
|
|
88
|
+
.map((message) => {
|
|
89
|
+
const participantId =
|
|
90
|
+
String(message.participantId || message.lane || message.origin || "").trim().toLowerCase() || "system";
|
|
91
|
+
return {
|
|
92
|
+
id: String(message.id || "").trim() || null,
|
|
93
|
+
lane: String(message.lane || message.participantId || message.origin || "").trim().toLowerCase() || "system",
|
|
94
|
+
note: String(message.note || "").trim() || null,
|
|
95
|
+
origin: String(message.origin || message.participantId || message.lane || "").trim().toLowerCase() || "system",
|
|
96
|
+
participantId,
|
|
97
|
+
role: message.role === "user" ? "user" : "assistant",
|
|
98
|
+
roundId: String(message.roundId || "").trim() || null,
|
|
99
|
+
text:
|
|
100
|
+
message.role === "user"
|
|
101
|
+
? String(message.text || "").trim()
|
|
102
|
+
: normalizeAgentRoomReply(participantId, message.text || ""),
|
|
103
|
+
timestamp: String(message.timestamp || "").trim() || timestamp
|
|
104
|
+
};
|
|
105
|
+
})
|
|
106
|
+
.filter((message) => message.id && message.text)
|
|
107
|
+
.sort((a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime())
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
let currentRound = null;
|
|
111
|
+
if (base.currentRound && typeof base.currentRound === "object") {
|
|
112
|
+
currentRound = {
|
|
113
|
+
attemptsByParticipant: {},
|
|
114
|
+
completedAt: String(base.currentRound.completedAt || "").trim() || null,
|
|
115
|
+
id: String(base.currentRound.id || "").trim() || null,
|
|
116
|
+
lastErrorByParticipant: {},
|
|
117
|
+
messageId: String(base.currentRound.messageId || "").trim() || null,
|
|
118
|
+
participantIds: normalizeMemberIds(base.currentRound.participantIds || memberIds),
|
|
119
|
+
pendingParticipantIds: normalizeRoundMemberIds(base.currentRound.pendingParticipantIds, []),
|
|
120
|
+
promptText: String(base.currentRound.promptText || "").trim() || null,
|
|
121
|
+
retryCount: Math.max(0, Number(base.currentRound.retryCount || 0) || 0),
|
|
122
|
+
completedParticipantIds: normalizeRoundMemberIds(base.currentRound.completedParticipantIds, []),
|
|
123
|
+
failedParticipantIds: normalizeRoundMemberIds(base.currentRound.failedParticipantIds, []),
|
|
124
|
+
startedAt: String(base.currentRound.startedAt || "").trim() || timestamp,
|
|
125
|
+
status: String(base.currentRound.status || "").trim() || "running"
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
currentRound.attemptsByParticipant = normalizeRoundAttemptMap(
|
|
129
|
+
base.currentRound.attemptsByParticipant,
|
|
130
|
+
currentRound.participantIds
|
|
131
|
+
);
|
|
132
|
+
currentRound.lastErrorByParticipant = normalizeRoundErrorMap(
|
|
133
|
+
base.currentRound.lastErrorByParticipant,
|
|
134
|
+
currentRound.participantIds
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!currentRound.id) {
|
|
138
|
+
currentRound = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
currentRound,
|
|
144
|
+
enabled: Boolean(base.enabled),
|
|
145
|
+
memberIds,
|
|
146
|
+
messages,
|
|
147
|
+
threadId: nextThreadId,
|
|
148
|
+
updatedAt: String(base.updatedAt || "").trim() || timestamp
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function setAgentRoomEnabled(state, enabled, { memberIds = null, timestamp = isoNow() } = {}) {
|
|
153
|
+
const current = normalizeAgentRoomState(state, { timestamp });
|
|
154
|
+
return {
|
|
155
|
+
...current,
|
|
156
|
+
enabled: Boolean(enabled),
|
|
157
|
+
memberIds: memberIds ? normalizeMemberIds(memberIds) : current.memberIds,
|
|
158
|
+
updatedAt: timestamp
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createAgentRoomMessage({
|
|
163
|
+
id,
|
|
164
|
+
participantId,
|
|
165
|
+
role = "assistant",
|
|
166
|
+
text,
|
|
167
|
+
timestamp = isoNow(),
|
|
168
|
+
roundId = null,
|
|
169
|
+
note = null
|
|
170
|
+
} = {}) {
|
|
171
|
+
const nextParticipantId = String(participantId || "").trim().toLowerCase() || "system";
|
|
172
|
+
return {
|
|
173
|
+
id: String(id || "").trim() || null,
|
|
174
|
+
lane: nextParticipantId,
|
|
175
|
+
note: String(note || "").trim() || null,
|
|
176
|
+
origin: nextParticipantId,
|
|
177
|
+
participantId: nextParticipantId,
|
|
178
|
+
role: role === "user" ? "user" : "assistant",
|
|
179
|
+
roundId: String(roundId || "").trim() || null,
|
|
180
|
+
text: String(text || "").trim(),
|
|
181
|
+
timestamp
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function appendAgentRoomMessages(state, messages = [], { timestamp = isoNow() } = {}) {
|
|
186
|
+
const current = normalizeAgentRoomState(state, { timestamp });
|
|
187
|
+
const nextMessages = clampMessages([
|
|
188
|
+
...current.messages,
|
|
189
|
+
...messages
|
|
190
|
+
.map((message) => normalizeAgentRoomState({
|
|
191
|
+
enabled: current.enabled,
|
|
192
|
+
memberIds: current.memberIds,
|
|
193
|
+
messages: [message],
|
|
194
|
+
threadId: current.threadId
|
|
195
|
+
}, { timestamp }).messages[0])
|
|
196
|
+
.filter(Boolean)
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...current,
|
|
201
|
+
messages: nextMessages,
|
|
202
|
+
updatedAt: timestamp
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function startAgentRoomRound(
|
|
207
|
+
state,
|
|
208
|
+
{
|
|
209
|
+
messageId,
|
|
210
|
+
note = null,
|
|
211
|
+
participantIds = null,
|
|
212
|
+
promptText = null,
|
|
213
|
+
roundId,
|
|
214
|
+
retryCount = 0,
|
|
215
|
+
text,
|
|
216
|
+
timestamp = isoNow(),
|
|
217
|
+
userParticipantId = "remote"
|
|
218
|
+
} = {}
|
|
219
|
+
) {
|
|
220
|
+
const current = normalizeAgentRoomState(state, { timestamp });
|
|
221
|
+
if (current.currentRound?.status === "running") {
|
|
222
|
+
throw new Error("The council room is already discussing something.");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const nextParticipantIds = normalizeMemberIds(participantIds || current.memberIds);
|
|
226
|
+
const nextRoundId = String(roundId || "").trim();
|
|
227
|
+
const nextMessageId = String(messageId || "").trim();
|
|
228
|
+
const userMessage = createAgentRoomMessage({
|
|
229
|
+
id: nextMessageId,
|
|
230
|
+
note: note || `council room / ${nextParticipantIds.length} participants`,
|
|
231
|
+
participantId: userParticipantId,
|
|
232
|
+
role: "user",
|
|
233
|
+
roundId: nextRoundId,
|
|
234
|
+
text,
|
|
235
|
+
timestamp
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
...appendAgentRoomMessages(current, [userMessage], { timestamp }),
|
|
240
|
+
currentRound: {
|
|
241
|
+
attemptsByParticipant: normalizeRoundAttemptMap(null, nextParticipantIds),
|
|
242
|
+
completedAt: null,
|
|
243
|
+
completedParticipantIds: [],
|
|
244
|
+
failedParticipantIds: [],
|
|
245
|
+
id: nextRoundId,
|
|
246
|
+
lastErrorByParticipant: normalizeRoundErrorMap(null, nextParticipantIds),
|
|
247
|
+
messageId: nextMessageId,
|
|
248
|
+
participantIds: nextParticipantIds,
|
|
249
|
+
pendingParticipantIds: [...nextParticipantIds],
|
|
250
|
+
promptText: String(promptText || text || "").trim() || null,
|
|
251
|
+
retryCount: Math.max(0, Number(retryCount || 0) || 0),
|
|
252
|
+
startedAt: timestamp,
|
|
253
|
+
status: "running"
|
|
254
|
+
},
|
|
255
|
+
enabled: true,
|
|
256
|
+
memberIds: current.memberIds,
|
|
257
|
+
updatedAt: timestamp
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function settleAgentRoomParticipant(
|
|
262
|
+
state,
|
|
263
|
+
{
|
|
264
|
+
error = null,
|
|
265
|
+
messageId,
|
|
266
|
+
participantId,
|
|
267
|
+
roundId,
|
|
268
|
+
text,
|
|
269
|
+
timestamp = isoNow()
|
|
270
|
+
} = {}
|
|
271
|
+
) {
|
|
272
|
+
const current = normalizeAgentRoomState(state, { timestamp });
|
|
273
|
+
if (!current.currentRound || current.currentRound.id !== String(roundId || "").trim()) {
|
|
274
|
+
return current;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const nextParticipantId = String(participantId || "").trim().toLowerCase();
|
|
278
|
+
if (!nextParticipantId) {
|
|
279
|
+
return current;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const pendingParticipantIds = current.currentRound.pendingParticipantIds.filter((id) => id !== nextParticipantId);
|
|
283
|
+
const attemptsByParticipant = {
|
|
284
|
+
...current.currentRound.attemptsByParticipant,
|
|
285
|
+
[nextParticipantId]: Number(current.currentRound.attemptsByParticipant?.[nextParticipantId] || 0) + 1
|
|
286
|
+
};
|
|
287
|
+
const lastErrorByParticipant = {
|
|
288
|
+
...current.currentRound.lastErrorByParticipant,
|
|
289
|
+
[nextParticipantId]: error ? String(error || "").trim() || "Unknown error." : null
|
|
290
|
+
};
|
|
291
|
+
const failedParticipantIds = error
|
|
292
|
+
? uniqueIds([...current.currentRound.failedParticipantIds, nextParticipantId])
|
|
293
|
+
: current.currentRound.failedParticipantIds.filter((id) => id !== nextParticipantId);
|
|
294
|
+
const completedParticipantIds = error
|
|
295
|
+
? current.currentRound.completedParticipantIds.filter((id) => id !== nextParticipantId)
|
|
296
|
+
: uniqueIds([...current.currentRound.completedParticipantIds, nextParticipantId]);
|
|
297
|
+
const done = pendingParticipantIds.length === 0;
|
|
298
|
+
const status = done ? (failedParticipantIds.length ? "partial" : "complete") : "running";
|
|
299
|
+
const failureNote = /timeout/i.test(String(error || ""))
|
|
300
|
+
? "lane timeout"
|
|
301
|
+
: /malformed/i.test(String(error || ""))
|
|
302
|
+
? "malformed reply"
|
|
303
|
+
: "lane failure";
|
|
304
|
+
|
|
305
|
+
const replyMessage = createAgentRoomMessage({
|
|
306
|
+
id: String(messageId || "").trim() || `${nextParticipantId}:${roundId}:${timestamp}`,
|
|
307
|
+
note: error ? failureNote : "council reply",
|
|
308
|
+
participantId: nextParticipantId,
|
|
309
|
+
role: "assistant",
|
|
310
|
+
roundId,
|
|
311
|
+
text: error ? `${nextParticipantId} failed: ${String(error || "Unknown error.").trim()}` : text,
|
|
312
|
+
timestamp
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const nextState = appendAgentRoomMessages(current, [replyMessage], { timestamp });
|
|
316
|
+
return {
|
|
317
|
+
...nextState,
|
|
318
|
+
currentRound: done
|
|
319
|
+
? {
|
|
320
|
+
...current.currentRound,
|
|
321
|
+
attemptsByParticipant,
|
|
322
|
+
completedAt: timestamp,
|
|
323
|
+
completedParticipantIds,
|
|
324
|
+
failedParticipantIds,
|
|
325
|
+
lastErrorByParticipant,
|
|
326
|
+
pendingParticipantIds,
|
|
327
|
+
status
|
|
328
|
+
}
|
|
329
|
+
: {
|
|
330
|
+
...current.currentRound,
|
|
331
|
+
attemptsByParticipant,
|
|
332
|
+
completedAt: null,
|
|
333
|
+
completedParticipantIds,
|
|
334
|
+
failedParticipantIds,
|
|
335
|
+
lastErrorByParticipant,
|
|
336
|
+
pendingParticipantIds,
|
|
337
|
+
status
|
|
338
|
+
},
|
|
339
|
+
updatedAt: timestamp
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function finalizeAgentRoomRound(state, { roundId, timestamp = isoNow() } = {}) {
|
|
344
|
+
const current = normalizeAgentRoomState(state, { timestamp });
|
|
345
|
+
if (!current.currentRound || current.currentRound.id !== String(roundId || "").trim()) {
|
|
346
|
+
return current;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
...current,
|
|
351
|
+
currentRound: {
|
|
352
|
+
...current.currentRound,
|
|
353
|
+
status: current.currentRound.failedParticipantIds.length ? "partial" : "complete"
|
|
354
|
+
},
|
|
355
|
+
updatedAt: timestamp
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function interruptAgentRoomRound(
|
|
360
|
+
state,
|
|
361
|
+
{ note = "Council round interrupted.", timestamp = isoNow() } = {}
|
|
362
|
+
) {
|
|
363
|
+
const current = normalizeAgentRoomState(state, { timestamp });
|
|
364
|
+
if (!current.currentRound) {
|
|
365
|
+
return current;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const interrupted = appendAgentRoomMessages(current, [
|
|
369
|
+
createAgentRoomMessage({
|
|
370
|
+
id: `system:${current.currentRound.id}:interrupted`,
|
|
371
|
+
note: "council room",
|
|
372
|
+
participantId: "system",
|
|
373
|
+
role: "assistant",
|
|
374
|
+
roundId: current.currentRound.id,
|
|
375
|
+
text: note,
|
|
376
|
+
timestamp
|
|
377
|
+
})
|
|
378
|
+
], { timestamp });
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
...interrupted,
|
|
382
|
+
currentRound: null,
|
|
383
|
+
updatedAt: timestamp
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function getAgentRoomRetryRound(state) {
|
|
388
|
+
const current = normalizeAgentRoomState(state);
|
|
389
|
+
const round = current.currentRound;
|
|
390
|
+
if (!round || round.status === "running") {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const participantIds = round.failedParticipantIds || [];
|
|
395
|
+
if (!participantIds.length || !round.promptText) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
note: `council retry / ${participantIds.length} participants`,
|
|
401
|
+
participantIds: [...participantIds],
|
|
402
|
+
promptText: round.promptText,
|
|
403
|
+
retryCount: Number(round.retryCount || 0) + 1,
|
|
404
|
+
roundId: round.id
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
defaultAgentRoomState,
|
|
6
|
+
interruptAgentRoomRound,
|
|
7
|
+
normalizeAgentRoomState
|
|
8
|
+
} from "./agent-room-state.mjs";
|
|
9
|
+
|
|
10
|
+
function sanitizeThreadId(threadId = "") {
|
|
11
|
+
const normalized = String(threadId || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
12
|
+
return normalized || "thread";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createAgentRoomStore({
|
|
16
|
+
baseDir,
|
|
17
|
+
now = () => new Date().toISOString()
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (!baseDir) {
|
|
20
|
+
throw new Error("createAgentRoomStore requires a baseDir.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function filePathForThread(threadId) {
|
|
24
|
+
return path.join(baseDir, `${sanitizeThreadId(threadId)}.json`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function load(threadId) {
|
|
28
|
+
const id = String(threadId || "").trim();
|
|
29
|
+
if (!id) {
|
|
30
|
+
return defaultAgentRoomState();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(await readFile(filePathForThread(id), "utf8"));
|
|
35
|
+
const normalized = normalizeAgentRoomState(raw, { threadId: id, timestamp: now() });
|
|
36
|
+
if (normalized.currentRound?.status === "running") {
|
|
37
|
+
return interruptAgentRoomRound(normalized, {
|
|
38
|
+
note: "Council round stopped when Dextunnel restarted.",
|
|
39
|
+
timestamp: now()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
} catch {
|
|
44
|
+
return defaultAgentRoomState({
|
|
45
|
+
enabled: false,
|
|
46
|
+
threadId: id,
|
|
47
|
+
timestamp: now()
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function save(threadId, state) {
|
|
53
|
+
const id = String(threadId || "").trim();
|
|
54
|
+
if (!id) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await mkdir(baseDir, { recursive: true });
|
|
59
|
+
await writeFile(
|
|
60
|
+
filePathForThread(id),
|
|
61
|
+
JSON.stringify(normalizeAgentRoomState(state, { threadId: id, timestamp: now() }), null, 2),
|
|
62
|
+
"utf8"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
filePathForThread,
|
|
68
|
+
load,
|
|
69
|
+
save
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function stripTrailingTelemetryLines(text = "") {
|
|
2
|
+
const lines = String(text || "").split("\n");
|
|
3
|
+
while (lines.length > 0) {
|
|
4
|
+
const lastLine = String(lines.at(-1) || "").trim();
|
|
5
|
+
if (!lastLine) {
|
|
6
|
+
lines.pop();
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (/^files=\d+\s*$/i.test(lastLine)) {
|
|
10
|
+
lines.pop();
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (/^\d+[mh]\d*[sm]?(?:\s+\d+[sm])?\s+·\s+/i.test(lastLine) || /^\d+(?:\.\d+)?s\s+·\s+/i.test(lastLine)) {
|
|
14
|
+
lines.pop();
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
return lines.join("\n").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeAgentRoomReply(participantId, rawText = "") {
|
|
23
|
+
let text = String(rawText || "").replace(/\r\n/g, "\n").trim();
|
|
24
|
+
if (!text) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const answerMatch = /(?:^|\n)Answer:\s*/g;
|
|
29
|
+
let lastAnswerIndex = -1;
|
|
30
|
+
for (const match of text.matchAll(answerMatch)) {
|
|
31
|
+
lastAnswerIndex = match.index + match[0].length;
|
|
32
|
+
}
|
|
33
|
+
if (lastAnswerIndex >= 0) {
|
|
34
|
+
text = text.slice(lastAnswerIndex).trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
text = stripTrailingTelemetryLines(text);
|
|
38
|
+
|
|
39
|
+
if (participantId === "oracle" && lastAnswerIndex < 0) {
|
|
40
|
+
text = text
|
|
41
|
+
.replace(/^.*?Launching .*?\n/si, "")
|
|
42
|
+
.replace(/^[^\n]*oracle[^\n]*\n/i, "")
|
|
43
|
+
.trim();
|
|
44
|
+
text = stripTrailingTelemetryLines(text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const APP_SERVER_RPC_METHODS = [
|
|
2
|
+
"thread/list",
|
|
3
|
+
"thread/read",
|
|
4
|
+
"thread/resume",
|
|
5
|
+
"thread/start",
|
|
6
|
+
"turn/start",
|
|
7
|
+
"turn/steer",
|
|
8
|
+
"turn/interrupt"
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const APP_SERVER_NOTIFICATION_METHODS = [
|
|
12
|
+
"thread/status/changed",
|
|
13
|
+
"thread/name/updated",
|
|
14
|
+
"thread/tokenUsage/updated",
|
|
15
|
+
"turn/started",
|
|
16
|
+
"turn/completed",
|
|
17
|
+
"item/completed",
|
|
18
|
+
"item/commandExecution/requestApproval",
|
|
19
|
+
"item/fileChange/requestApproval",
|
|
20
|
+
"item/permissions/requestApproval",
|
|
21
|
+
"item/tool/requestUserInput",
|
|
22
|
+
"item/tool/call",
|
|
23
|
+
"mcpServer/elicitation/request"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const APP_SERVER_LIVE_PATCH_NOTIFICATION_METHODS = [
|
|
27
|
+
"thread/status/changed",
|
|
28
|
+
"thread/name/updated",
|
|
29
|
+
"thread/tokenUsage/updated",
|
|
30
|
+
"turn/started",
|
|
31
|
+
"turn/completed",
|
|
32
|
+
"turn/plan/updated",
|
|
33
|
+
"turn/diff/updated",
|
|
34
|
+
"item/started",
|
|
35
|
+
"item/completed",
|
|
36
|
+
"item/agentMessage/delta",
|
|
37
|
+
"item/plan/delta",
|
|
38
|
+
"item/reasoning/summaryTextDelta",
|
|
39
|
+
"item/reasoning/textDelta",
|
|
40
|
+
"item/fileChange/outputDelta",
|
|
41
|
+
"thread/compacted"
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const APP_SERVER_SERVER_REQUEST_METHODS = [
|
|
45
|
+
"account/chatgptAuthTokens/refresh",
|
|
46
|
+
"item/commandExecution/requestApproval",
|
|
47
|
+
"item/fileChange/requestApproval",
|
|
48
|
+
"item/permissions/requestApproval",
|
|
49
|
+
"item/tool/requestUserInput",
|
|
50
|
+
"mcpServer/elicitation/request",
|
|
51
|
+
"item/tool/call"
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export const APP_SERVER_ITEM_TYPES = [
|
|
55
|
+
"userMessage",
|
|
56
|
+
"agentMessage",
|
|
57
|
+
"commandExecution",
|
|
58
|
+
"reasoning",
|
|
59
|
+
"contextCompaction",
|
|
60
|
+
"mcpToolCall",
|
|
61
|
+
"dynamicToolCall",
|
|
62
|
+
"collabToolCall",
|
|
63
|
+
"fileChange"
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export const APP_SERVER_DRIFT_RUNBOOK_PATH = ".maintainer/ops/app-server-drift-runbook.md";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCodexAppServerBridge } from "./codex-app-server-client.mjs";
|
|
2
|
+
import { createFakeCodexAppServerBridge } from "./fake-codex-app-server-bridge.mjs";
|
|
3
|
+
|
|
4
|
+
export function createAppServerState() {
|
|
5
|
+
return {
|
|
6
|
+
lastControlEvent: null,
|
|
7
|
+
lastInteraction: null,
|
|
8
|
+
lastSelectionEvent: null,
|
|
9
|
+
lastSurfaceEvent: null,
|
|
10
|
+
lastWrite: null
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createLiveState({ cwd = process.cwd() } = {}) {
|
|
15
|
+
return {
|
|
16
|
+
agentRoomByThreadId: {},
|
|
17
|
+
companionByThreadId: {},
|
|
18
|
+
controlLease: null,
|
|
19
|
+
interactionFlow: null,
|
|
20
|
+
lastError: null,
|
|
21
|
+
lastSyncAt: null,
|
|
22
|
+
pendingInteraction: null,
|
|
23
|
+
selectedProjectCwd: cwd,
|
|
24
|
+
selectionSource: "remote",
|
|
25
|
+
surfacePresenceByClientId: {},
|
|
26
|
+
selectedThreadId: null,
|
|
27
|
+
selectedThreadSnapshot: null,
|
|
28
|
+
threads: [],
|
|
29
|
+
turnDiff: null,
|
|
30
|
+
turnOriginsByThreadId: {},
|
|
31
|
+
watcherConnected: false,
|
|
32
|
+
writeLock: null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createCodexRuntime({
|
|
37
|
+
binaryPath,
|
|
38
|
+
cwd = process.cwd(),
|
|
39
|
+
listenUrl,
|
|
40
|
+
useFakeAppServer = false,
|
|
41
|
+
fakeSendDelayMs = 0
|
|
42
|
+
} = {}) {
|
|
43
|
+
const codexAppServer = useFakeAppServer
|
|
44
|
+
? createFakeCodexAppServerBridge({
|
|
45
|
+
binaryPath,
|
|
46
|
+
cwd,
|
|
47
|
+
listenUrl,
|
|
48
|
+
sendDelayMs: fakeSendDelayMs
|
|
49
|
+
})
|
|
50
|
+
: createCodexAppServerBridge({
|
|
51
|
+
binaryPath,
|
|
52
|
+
listenUrl
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
appServerState: createAppServerState(),
|
|
57
|
+
codexAppServer,
|
|
58
|
+
liveState: createLiveState({ cwd })
|
|
59
|
+
};
|
|
60
|
+
}
|