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
package/src/server.mjs
ADDED
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { open, writeFile } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AGENT_ROOM_MEMBER_IDS,
|
|
9
|
+
defaultAgentRoomState,
|
|
10
|
+
getAgentRoomRetryRound,
|
|
11
|
+
interruptAgentRoomRound,
|
|
12
|
+
normalizeAgentRoomState,
|
|
13
|
+
createAgentRoomMessage,
|
|
14
|
+
setAgentRoomEnabled,
|
|
15
|
+
settleAgentRoomParticipant,
|
|
16
|
+
startAgentRoomRound
|
|
17
|
+
} from "./lib/agent-room-state.mjs";
|
|
18
|
+
import { createAgentRoomContextBuilder } from "./lib/agent-room-context.mjs";
|
|
19
|
+
import { createAgentRoomService } from "./lib/agent-room-service.mjs";
|
|
20
|
+
import { createAgentRoomStore } from "./lib/agent-room-store.mjs";
|
|
21
|
+
import { createAgentRoomRuntime } from "./lib/agent-room-runtime.mjs";
|
|
22
|
+
import { createAttachmentService } from "./lib/attachment-service.mjs";
|
|
23
|
+
import { handleBridgeApiRequest } from "./lib/bridge-api-handler.mjs";
|
|
24
|
+
import { createBridgeStatusBuilder } from "./lib/bridge-status-builder.mjs";
|
|
25
|
+
import {
|
|
26
|
+
ARAZZO_DOC_PATH,
|
|
27
|
+
DISCOVERY_MANIFEST_PATH,
|
|
28
|
+
LLMS_TXT_PATH,
|
|
29
|
+
OPENAPI_DOC_PATH,
|
|
30
|
+
buildArazzoDocument,
|
|
31
|
+
buildLlmsText,
|
|
32
|
+
buildOpenApiDocument,
|
|
33
|
+
buildWellKnownManifest
|
|
34
|
+
} from "./lib/discovery-docs.mjs";
|
|
35
|
+
import {
|
|
36
|
+
buildSessionLogSnapshot,
|
|
37
|
+
mapThreadItemToCompanionEntry,
|
|
38
|
+
mapThreadToCompanionSnapshot,
|
|
39
|
+
pageTranscriptEntries,
|
|
40
|
+
readTranscriptHistoryPageFromSessionLog,
|
|
41
|
+
readTranscriptFromSessionLog
|
|
42
|
+
} from "./lib/codex-app-server-client.mjs";
|
|
43
|
+
import {
|
|
44
|
+
APP_SERVER_LIVE_PATCH_NOTIFICATION_METHODS
|
|
45
|
+
} from "./lib/app-server-contract.mjs";
|
|
46
|
+
import { createCodexRuntime } from "./lib/app-server-runtime.mjs";
|
|
47
|
+
import { createCompanionStateService } from "./lib/companion-state.mjs";
|
|
48
|
+
import { createControlLeaseService } from "./lib/control-lease-service.mjs";
|
|
49
|
+
import { createDebugHarnessService } from "./lib/debug-harness-service.mjs";
|
|
50
|
+
import {
|
|
51
|
+
openThreadInCodex
|
|
52
|
+
} from "./lib/desktop-integration.mjs";
|
|
53
|
+
import { createBridgeRuntimeLifecycleService } from "./lib/bridge-runtime-lifecycle.mjs";
|
|
54
|
+
import { applyLiveControlAction } from "./lib/live-control-state.mjs";
|
|
55
|
+
import { createLivePayloadBuilder } from "./lib/live-payload-builder.mjs";
|
|
56
|
+
import { applyLiveSelectionTransition } from "./lib/live-selection-transition-state.mjs";
|
|
57
|
+
import { createMockCodexAdapter } from "./lib/mock-codex-adapter.mjs";
|
|
58
|
+
import { buildOperatorDiagnostics } from "./lib/operator-diagnostics.mjs";
|
|
59
|
+
import { createRepoChangesService } from "./lib/repo-changes-service.mjs";
|
|
60
|
+
import { createSelectionStateService } from "./lib/selection-state-service.mjs";
|
|
61
|
+
import { createInteractionStateService } from "./lib/interaction-state.mjs";
|
|
62
|
+
import { createInteractionResolutionService } from "./lib/interaction-resolution-service.mjs";
|
|
63
|
+
import { buildInstallPreflight } from "./lib/install-preflight.mjs";
|
|
64
|
+
import { createRuntimeConfig } from "./lib/runtime-config.mjs";
|
|
65
|
+
import { createStaticSurfaceService } from "./lib/static-surface-service.mjs";
|
|
66
|
+
import { canServeSurfaceBootstrap } from "./lib/surface-request-guard.mjs";
|
|
67
|
+
import { createSurfacePresenceService } from "./lib/surface-presence-service.mjs";
|
|
68
|
+
import { createThreadSyncStateService } from "./lib/thread-sync-state.mjs";
|
|
69
|
+
import { createLiveTranscriptStateService } from "./lib/live-transcript-state.mjs";
|
|
70
|
+
import { createWatcherLifecycleService } from "./lib/watcher-lifecycle.mjs";
|
|
71
|
+
import {
|
|
72
|
+
createSurfaceAccessRegistry,
|
|
73
|
+
defaultSurfaceAccessSecretPath,
|
|
74
|
+
loadOrCreateSurfaceAccessSecret
|
|
75
|
+
} from "./lib/surface-access.mjs";
|
|
76
|
+
import {
|
|
77
|
+
applySurfacePresenceUpdate as applySurfacePresenceUpdateState,
|
|
78
|
+
buildSelectedAttachments as buildSelectedAttachmentsState,
|
|
79
|
+
clearControlLease as clearControlLeaseState,
|
|
80
|
+
countSurfacePresence as countSurfacePresenceState,
|
|
81
|
+
ensureRemoteControlLease as ensureRemoteControlLeaseState,
|
|
82
|
+
getControlLeaseForThread as getControlLeaseForThreadState,
|
|
83
|
+
normalizeSurfaceName as normalizeSurfaceNameState,
|
|
84
|
+
pruneStaleSurfacePresence as pruneStaleSurfacePresenceState,
|
|
85
|
+
setControlLease as setControlLeaseState,
|
|
86
|
+
surfaceActorLabel as surfaceActorLabelState,
|
|
87
|
+
renewControlLease as renewControlLeaseState
|
|
88
|
+
} from "./lib/shared-room-state.mjs";
|
|
89
|
+
import { createSessionStore } from "./lib/session-store.mjs";
|
|
90
|
+
import { createSseHub } from "./lib/sse-hub.mjs";
|
|
91
|
+
|
|
92
|
+
const runtimeConfig = createRuntimeConfig({
|
|
93
|
+
cwd: process.cwd(),
|
|
94
|
+
env: process.env,
|
|
95
|
+
importMetaUrl: import.meta.url
|
|
96
|
+
});
|
|
97
|
+
const {
|
|
98
|
+
agentRoomDir,
|
|
99
|
+
appServerListenUrl,
|
|
100
|
+
attachmentDir,
|
|
101
|
+
codexBinaryPath,
|
|
102
|
+
devToolsEnabled,
|
|
103
|
+
exposeHostSurface,
|
|
104
|
+
fakeAgentRoomFailures,
|
|
105
|
+
fakeSendDelayMs,
|
|
106
|
+
host,
|
|
107
|
+
mimeTypes,
|
|
108
|
+
port,
|
|
109
|
+
publicDir,
|
|
110
|
+
runtimeProfile,
|
|
111
|
+
useFakeAgentRoom,
|
|
112
|
+
useFakeAppServer
|
|
113
|
+
} = runtimeConfig;
|
|
114
|
+
const surfaceAccessSecret = await loadOrCreateSurfaceAccessSecret({
|
|
115
|
+
secretPath: defaultSurfaceAccessSecretPath({ cwd: process.cwd() })
|
|
116
|
+
});
|
|
117
|
+
const surfaceAccess = createSurfaceAccessRegistry({
|
|
118
|
+
secret: surfaceAccessSecret
|
|
119
|
+
});
|
|
120
|
+
const store = createSessionStore();
|
|
121
|
+
const mockAdapter = devToolsEnabled ? createMockCodexAdapter(store) : null;
|
|
122
|
+
const agentRoomStore = createAgentRoomStore({
|
|
123
|
+
baseDir: agentRoomDir
|
|
124
|
+
});
|
|
125
|
+
const agentRoomRuntime = createAgentRoomRuntime({
|
|
126
|
+
artifactsDir: agentRoomDir,
|
|
127
|
+
codexBinaryPath,
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
fake: useFakeAgentRoom,
|
|
130
|
+
fakeFailures: fakeAgentRoomFailures
|
|
131
|
+
});
|
|
132
|
+
const { appServerState, codexAppServer, liveState } = createCodexRuntime({
|
|
133
|
+
binaryPath: codexBinaryPath,
|
|
134
|
+
cwd: process.cwd(),
|
|
135
|
+
fakeSendDelayMs,
|
|
136
|
+
listenUrl: appServerListenUrl,
|
|
137
|
+
useFakeAppServer
|
|
138
|
+
});
|
|
139
|
+
const sseHub = createSseHub();
|
|
140
|
+
const CONTROL_LEASE_TTL_MS = 5 * 60 * 1000;
|
|
141
|
+
const SURFACE_PRESENCE_STALE_MS = 45 * 1000;
|
|
142
|
+
const SURFACE_PRESENCE_SWEEP_MS = 15 * 1000;
|
|
143
|
+
const COMPANION_WAKEUP_VISIBLE_MS = 6 * 60 * 1000;
|
|
144
|
+
const COMPANION_WAKEUP_STALE_MS = 20 * 60 * 1000;
|
|
145
|
+
const COMPANION_WAKEUP_SNOOZE_MS = 10 * 60 * 1000;
|
|
146
|
+
const COMPANION_WAKEUP_LIMIT = 4;
|
|
147
|
+
const ADVISORY_PARTICIPANT_IDS = ["oracle", "gemini"];
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if (mockAdapter) {
|
|
151
|
+
mockAdapter.start();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const watchRefreshMethods = new Set(APP_SERVER_LIVE_PATCH_NOTIFICATION_METHODS);
|
|
155
|
+
|
|
156
|
+
const preferredLiveSourceKinds = ["vscode"];
|
|
157
|
+
const fallbackLiveSourceKinds = ["vscode", "cli"];
|
|
158
|
+
const SESSION_LOG_TAIL_BYTES = 1024 * 1024;
|
|
159
|
+
const SELECTED_TRANSCRIPT_PAGE_SIZE = 40;
|
|
160
|
+
const THREAD_PREVIEW_TAIL_BYTES = 64 * 1024;
|
|
161
|
+
const THREAD_PREVIEW_MAX_CHARS = 96;
|
|
162
|
+
const ATTACHMENT_MAX_AGE_MS = 2 * 60 * 60 * 1000;
|
|
163
|
+
const ATTACHMENT_SWEEP_MS = 30 * 60 * 1000;
|
|
164
|
+
const GIT_COMMAND_TIMEOUT_MS = 4000;
|
|
165
|
+
const REPO_CHANGES_CACHE_TTL_MS = 2500;
|
|
166
|
+
const attachmentService = createAttachmentService({
|
|
167
|
+
attachmentDir,
|
|
168
|
+
maxAgeMs: ATTACHMENT_MAX_AGE_MS
|
|
169
|
+
});
|
|
170
|
+
const {
|
|
171
|
+
cleanupAttachmentDir,
|
|
172
|
+
persistImageAttachments
|
|
173
|
+
} = attachmentService;
|
|
174
|
+
const repoChanges = createRepoChangesService({
|
|
175
|
+
cacheTtlMs: REPO_CHANGES_CACHE_TTL_MS,
|
|
176
|
+
gitCommandTimeoutMs: GIT_COMMAND_TIMEOUT_MS,
|
|
177
|
+
sessionLogTailBytes: SESSION_LOG_TAIL_BYTES
|
|
178
|
+
});
|
|
179
|
+
const repoObjectiveCache = new Map();
|
|
180
|
+
function nowIso() {
|
|
181
|
+
return new Date().toISOString();
|
|
182
|
+
}
|
|
183
|
+
const surfacePresenceService = createSurfacePresenceService({
|
|
184
|
+
appServerState,
|
|
185
|
+
applySurfacePresenceUpdateState,
|
|
186
|
+
buildSelectedAttachmentsState,
|
|
187
|
+
countSurfacePresenceState: countSurfacePresenceState,
|
|
188
|
+
defaultStaleMs: SURFACE_PRESENCE_STALE_MS,
|
|
189
|
+
liveState,
|
|
190
|
+
normalizeSurfaceName: normalizeSurfaceNameState,
|
|
191
|
+
nowIso,
|
|
192
|
+
pruneStaleSurfacePresenceState,
|
|
193
|
+
randomId: () => randomUUID()
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const {
|
|
197
|
+
applySurfacePresenceUpdate,
|
|
198
|
+
buildSelectedAttachments,
|
|
199
|
+
countSurfacePresence,
|
|
200
|
+
pruneStaleSurfacePresence,
|
|
201
|
+
recordSurfaceEvent,
|
|
202
|
+
removeSurfacePresence,
|
|
203
|
+
upsertSurfacePresence
|
|
204
|
+
} = surfacePresenceService;
|
|
205
|
+
|
|
206
|
+
function trimInteractionText(value, maxLength = 72) {
|
|
207
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
208
|
+
if (!text) {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function humanizeServerText(value) {
|
|
216
|
+
return String(value || "")
|
|
217
|
+
.replaceAll("_", " ")
|
|
218
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
219
|
+
.replace(/\s+/g, " ")
|
|
220
|
+
.trim()
|
|
221
|
+
.toLowerCase();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sendJson(res, statusCode, payload) {
|
|
225
|
+
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
226
|
+
res.end(JSON.stringify(payload));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function requestBaseUrl(req, fallbackHost = "127.0.0.1", fallbackPort = 4317) {
|
|
230
|
+
const forwardedProto = String(req.headers["x-forwarded-proto"] || "").trim();
|
|
231
|
+
const proto = forwardedProto || "http";
|
|
232
|
+
const forwardedHost = String(req.headers["x-forwarded-host"] || "").trim();
|
|
233
|
+
const hostHeader = String(req.headers.host || "").trim();
|
|
234
|
+
const authority = forwardedHost || hostHeader || `${fallbackHost}:${fallbackPort}`;
|
|
235
|
+
return `${proto}://${authority}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function readJsonBody(req) {
|
|
239
|
+
const chunks = [];
|
|
240
|
+
|
|
241
|
+
for await (const chunk of req) {
|
|
242
|
+
chunks.push(chunk);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
246
|
+
return raw ? JSON.parse(raw) : {};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function runGit(cwd, args) {
|
|
250
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
|
|
251
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
252
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
253
|
+
});
|
|
254
|
+
return stdout;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function invalidateRepoChangesCache(params = {}) {
|
|
258
|
+
return repoChanges.invalidateRepoChangesCache(params);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function getCachedRepoChanges(cwd, options = {}) {
|
|
262
|
+
return repoChanges.getCachedRepoChanges(cwd, options);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildLiveTurnChanges(payload) {
|
|
266
|
+
return repoChanges.buildLiveTurnChanges(payload);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function projectLabel(cwd) {
|
|
270
|
+
const parts = String(cwd || "")
|
|
271
|
+
.split("/")
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
|
|
274
|
+
if (parts.length >= 2) {
|
|
275
|
+
return parts.slice(-2).join("/");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return cwd || "unknown";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function projectLeaf(cwd) {
|
|
282
|
+
const parts = String(cwd || "")
|
|
283
|
+
.split("/")
|
|
284
|
+
.filter(Boolean);
|
|
285
|
+
|
|
286
|
+
return parts.at(-1) || "";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function shortThreadId(value) {
|
|
290
|
+
const id = String(value || "").trim();
|
|
291
|
+
if (!id) {
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function surfaceActorLabel({ surface = "", clientId = null } = {}) {
|
|
299
|
+
return surfaceActorLabelState({ surface, clientId });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function slugifyChannelName(value) {
|
|
303
|
+
const slug = String(value || "")
|
|
304
|
+
.trim()
|
|
305
|
+
.toLowerCase()
|
|
306
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
307
|
+
.replace(/^-+|-+$/g, "");
|
|
308
|
+
|
|
309
|
+
const trimmed = slug.slice(0, 36).replace(/-+$/g, "");
|
|
310
|
+
return trimmed || "session";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function trimTopicText(value, maxLength = 120) {
|
|
314
|
+
const normalized = String(value || "").replace(/\s+/g, " ").trim();
|
|
315
|
+
if (!normalized) {
|
|
316
|
+
return "";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (normalized.length <= maxLength) {
|
|
320
|
+
return normalized;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizePreviewText(value) {
|
|
327
|
+
return String(value || "")
|
|
328
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
329
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
330
|
+
.replace(/\s+/g, " ")
|
|
331
|
+
.trim();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function looksLikeTopicNoise(value) {
|
|
335
|
+
const text = String(value || "").trim();
|
|
336
|
+
if (!text) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (/^\[[^\]]+\]\([^)]+\)$/.test(text)) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return text.startsWith("[$");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isGenericThreadName(name, cwd) {
|
|
348
|
+
const normalized = String(name || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
349
|
+
if (!normalized) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (["session", "untitled", "untitled session", "new session"].includes(normalized)) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function looksLikeChannelLabelCandidate(value) {
|
|
361
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
362
|
+
if (!text || looksLikeTopicNoise(text)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (text.length > 64) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (text.split(/\s+/).length > 9) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (/[?]/.test(text)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function extractCurrentObjective(markdown) {
|
|
382
|
+
const match = String(markdown || "").match(/## Current Objective\s+([\s\S]*?)(?:\n## |\n# |$)/);
|
|
383
|
+
if (!match) {
|
|
384
|
+
return "";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return trimTopicText(match[1].replace(/^- /gm, "").trim(), 120);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function repoObjective(cwd) {
|
|
391
|
+
const summaryPath = path.join(String(cwd || ""), ".agent", "SUMMARY.md");
|
|
392
|
+
if (!cwd || !existsSync(summaryPath)) {
|
|
393
|
+
return "";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const mtimeMs = statSync(summaryPath).mtimeMs;
|
|
398
|
+
const cached = repoObjectiveCache.get(summaryPath);
|
|
399
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
400
|
+
return cached.value;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const value = extractCurrentObjective(readFileSync(summaryPath, "utf8"));
|
|
404
|
+
repoObjectiveCache.set(summaryPath, { mtimeMs, value });
|
|
405
|
+
return value;
|
|
406
|
+
} catch {
|
|
407
|
+
return "";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function bestConversationLabel(snapshot, maxLength = 52) {
|
|
412
|
+
const entry = [...(snapshot?.transcript || [])]
|
|
413
|
+
.reverse()
|
|
414
|
+
.find(
|
|
415
|
+
(candidate) =>
|
|
416
|
+
(candidate?.role === "assistant" || candidate?.role === "user") &&
|
|
417
|
+
candidate?.kind !== "commentary" &&
|
|
418
|
+
looksLikeChannelLabelCandidate(candidate?.text)
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
return entry?.text ? trimTopicText(entry.text, maxLength) : "";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function selectedThreadSummary() {
|
|
425
|
+
if (!liveState.selectedThreadId) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return liveState.threads.find((thread) => thread.id === liveState.selectedThreadId) || null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function bestThreadLabel(thread, snapshot, { selected = false } = {}) {
|
|
433
|
+
const genericName = thread?.name ? trimTopicText(thread.name, 52) : "";
|
|
434
|
+
const meaningfulName =
|
|
435
|
+
thread?.name && !isGenericThreadName(thread.name, thread.cwd)
|
|
436
|
+
? trimTopicText(thread.name, 52)
|
|
437
|
+
: "";
|
|
438
|
+
if (meaningfulName) {
|
|
439
|
+
return meaningfulName;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (selected && genericName) {
|
|
443
|
+
return genericName;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const previewLabel =
|
|
447
|
+
thread?.preview && looksLikeChannelLabelCandidate(thread.preview)
|
|
448
|
+
? trimTopicText(thread.preview, 52)
|
|
449
|
+
: "";
|
|
450
|
+
if (previewLabel) {
|
|
451
|
+
return previewLabel;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const conversationLabel = bestConversationLabel(snapshot, 52);
|
|
455
|
+
if (conversationLabel) {
|
|
456
|
+
return conversationLabel;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (genericName) {
|
|
460
|
+
return genericName;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (selected) {
|
|
464
|
+
return "current session";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const shortId = shortThreadId(thread?.id);
|
|
468
|
+
return shortId ? `session ${shortId}` : "session";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function normalizeLane(source) {
|
|
472
|
+
switch (String(source || "").trim().toLowerCase()) {
|
|
473
|
+
case "remote":
|
|
474
|
+
return "remote";
|
|
475
|
+
case "host":
|
|
476
|
+
case "desktop":
|
|
477
|
+
case "vscode":
|
|
478
|
+
return "desktop";
|
|
479
|
+
case "oracle":
|
|
480
|
+
return "oracle";
|
|
481
|
+
case "gemini":
|
|
482
|
+
return "gemini";
|
|
483
|
+
case "external":
|
|
484
|
+
case "cli":
|
|
485
|
+
return "external";
|
|
486
|
+
default:
|
|
487
|
+
return "";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function buildParticipant(id, overrides = {}) {
|
|
492
|
+
const catalog = {
|
|
493
|
+
codex: {
|
|
494
|
+
capability: "write",
|
|
495
|
+
id: "codex",
|
|
496
|
+
label: "codex",
|
|
497
|
+
lane: "",
|
|
498
|
+
role: "live",
|
|
499
|
+
canAct: true,
|
|
500
|
+
sortOrder: 10,
|
|
501
|
+
token: "accent"
|
|
502
|
+
},
|
|
503
|
+
remote: {
|
|
504
|
+
capability: "write",
|
|
505
|
+
id: "remote",
|
|
506
|
+
label: "remote",
|
|
507
|
+
lane: "remote",
|
|
508
|
+
role: "live",
|
|
509
|
+
canAct: true,
|
|
510
|
+
sortOrder: 20,
|
|
511
|
+
token: "remote"
|
|
512
|
+
},
|
|
513
|
+
desktop: {
|
|
514
|
+
capability: "write",
|
|
515
|
+
id: "desktop",
|
|
516
|
+
label: "desktop",
|
|
517
|
+
lane: "desktop",
|
|
518
|
+
role: "live",
|
|
519
|
+
canAct: true,
|
|
520
|
+
sortOrder: 30,
|
|
521
|
+
token: "desktop"
|
|
522
|
+
},
|
|
523
|
+
nix: {
|
|
524
|
+
capability: "advisory",
|
|
525
|
+
id: "nix",
|
|
526
|
+
label: "nix",
|
|
527
|
+
lane: "nix",
|
|
528
|
+
role: "advisory",
|
|
529
|
+
canAct: false,
|
|
530
|
+
sortOrder: 35,
|
|
531
|
+
token: "nix"
|
|
532
|
+
},
|
|
533
|
+
oracle: {
|
|
534
|
+
capability: "advisory",
|
|
535
|
+
id: "oracle",
|
|
536
|
+
label: "oracle",
|
|
537
|
+
lane: "oracle",
|
|
538
|
+
role: "advisory",
|
|
539
|
+
canAct: false,
|
|
540
|
+
sortOrder: 40,
|
|
541
|
+
token: "oracle"
|
|
542
|
+
},
|
|
543
|
+
gemini: {
|
|
544
|
+
capability: "advisory",
|
|
545
|
+
id: "gemini",
|
|
546
|
+
label: "gemini",
|
|
547
|
+
lane: "gemini",
|
|
548
|
+
role: "advisory",
|
|
549
|
+
canAct: false,
|
|
550
|
+
sortOrder: 50,
|
|
551
|
+
token: "gemini"
|
|
552
|
+
},
|
|
553
|
+
claude: {
|
|
554
|
+
capability: "advisory",
|
|
555
|
+
id: "claude",
|
|
556
|
+
label: "claude",
|
|
557
|
+
lane: "claude",
|
|
558
|
+
role: "advisory",
|
|
559
|
+
canAct: false,
|
|
560
|
+
sortOrder: 55,
|
|
561
|
+
token: "claude"
|
|
562
|
+
},
|
|
563
|
+
spark: {
|
|
564
|
+
capability: "advisory",
|
|
565
|
+
id: "spark",
|
|
566
|
+
label: "spark",
|
|
567
|
+
lane: "spark",
|
|
568
|
+
role: "advisory",
|
|
569
|
+
canAct: false,
|
|
570
|
+
sortOrder: 57,
|
|
571
|
+
token: "spark"
|
|
572
|
+
},
|
|
573
|
+
tools: {
|
|
574
|
+
capability: "observe",
|
|
575
|
+
id: "tools",
|
|
576
|
+
label: "tools",
|
|
577
|
+
lane: "",
|
|
578
|
+
role: "tool",
|
|
579
|
+
canAct: false,
|
|
580
|
+
sortOrder: 60,
|
|
581
|
+
token: "tool"
|
|
582
|
+
},
|
|
583
|
+
updates: {
|
|
584
|
+
capability: "observe",
|
|
585
|
+
id: "updates",
|
|
586
|
+
label: "updates",
|
|
587
|
+
lane: "",
|
|
588
|
+
role: "system",
|
|
589
|
+
canAct: false,
|
|
590
|
+
sortOrder: 70,
|
|
591
|
+
token: "updates"
|
|
592
|
+
},
|
|
593
|
+
system: {
|
|
594
|
+
capability: "observe",
|
|
595
|
+
id: "system",
|
|
596
|
+
label: "system",
|
|
597
|
+
lane: "",
|
|
598
|
+
role: "system",
|
|
599
|
+
canAct: false,
|
|
600
|
+
sortOrder: 80,
|
|
601
|
+
token: "system"
|
|
602
|
+
},
|
|
603
|
+
user: {
|
|
604
|
+
capability: "write",
|
|
605
|
+
id: "user",
|
|
606
|
+
label: "user",
|
|
607
|
+
lane: "",
|
|
608
|
+
role: "live",
|
|
609
|
+
canAct: true,
|
|
610
|
+
sortOrder: 90,
|
|
611
|
+
token: "user"
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
...(catalog[id] || catalog.user),
|
|
617
|
+
...overrides
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const companionState = createCompanionStateService({
|
|
622
|
+
ADVISORY_PARTICIPANT_IDS,
|
|
623
|
+
COMPANION_WAKEUP_LIMIT,
|
|
624
|
+
COMPANION_WAKEUP_SNOOZE_MS,
|
|
625
|
+
COMPANION_WAKEUP_STALE_MS,
|
|
626
|
+
COMPANION_WAKEUP_VISIBLE_MS,
|
|
627
|
+
buildParticipant,
|
|
628
|
+
liveState,
|
|
629
|
+
nowIso
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const {
|
|
633
|
+
applyCompanionWakeupAction,
|
|
634
|
+
buildSelectedCompanionState,
|
|
635
|
+
defaultAdvisoryState,
|
|
636
|
+
pruneAllCompanionWakeups,
|
|
637
|
+
queueCompanionWakeup,
|
|
638
|
+
resetCompanionWakeups,
|
|
639
|
+
summonCompanionWakeup
|
|
640
|
+
} = companionState;
|
|
641
|
+
|
|
642
|
+
function advisoryParticipantForThread(advisorId, threadId) {
|
|
643
|
+
const selected = buildSelectedCompanionState(threadId);
|
|
644
|
+
const advisory = selected.advisories.find((entry) => entry.id === advisorId) || defaultAdvisoryState(advisorId);
|
|
645
|
+
return buildParticipant(advisorId, {
|
|
646
|
+
lastWakeAt: advisory.lastWakeAt || null,
|
|
647
|
+
metaLabel: advisory.metaLabel || "dormant",
|
|
648
|
+
state: advisory.state || "dormant",
|
|
649
|
+
wakeKind: advisory.wakeKind || null
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const interactionState = createInteractionStateService({
|
|
654
|
+
appServerState,
|
|
655
|
+
liveState,
|
|
656
|
+
nowIso,
|
|
657
|
+
trimInteractionText
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const {
|
|
661
|
+
beginInteractionFlow,
|
|
662
|
+
clearInteractionFlow,
|
|
663
|
+
getLastControlEventForSelectedThread,
|
|
664
|
+
getLastInteractionForSelectedThread,
|
|
665
|
+
getLastSelectionEventForSelectedThread,
|
|
666
|
+
getLastSurfaceEventForSelectedThread,
|
|
667
|
+
getLastWriteForSelectedThread,
|
|
668
|
+
getPendingInteractionForSelectedThread,
|
|
669
|
+
interactionKindLabel,
|
|
670
|
+
interactionRequestSummary,
|
|
671
|
+
mapPendingInteraction,
|
|
672
|
+
summarizeNotificationInteraction
|
|
673
|
+
} = interactionState;
|
|
674
|
+
|
|
675
|
+
const buildBridgeStatus = createBridgeStatusBuilder({
|
|
676
|
+
appServerState,
|
|
677
|
+
buildOperatorDiagnostics,
|
|
678
|
+
buildSelectedAttachments,
|
|
679
|
+
codexAppServer,
|
|
680
|
+
devToolsEnabled,
|
|
681
|
+
getControlLeaseForSelectedThread: (...args) => getControlLeaseForSelectedThread(...args),
|
|
682
|
+
getLastControlEventForSelectedThread,
|
|
683
|
+
getLastInteractionForSelectedThread,
|
|
684
|
+
getLastSelectionEventForSelectedThread,
|
|
685
|
+
getLastSurfaceEventForSelectedThread,
|
|
686
|
+
getLastWriteForSelectedThread,
|
|
687
|
+
liveState,
|
|
688
|
+
runtimeProfile
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
let buildAgentRoomContextMarkdown = () => "";
|
|
692
|
+
let buildLivePayload = () => ({});
|
|
693
|
+
const agentRoomService = createAgentRoomService({
|
|
694
|
+
buildParticipant,
|
|
695
|
+
broadcast,
|
|
696
|
+
codexAppServer,
|
|
697
|
+
defaultAgentRoomState,
|
|
698
|
+
getBuildAgentRoomContextMarkdown: () => buildAgentRoomContextMarkdown,
|
|
699
|
+
getLivePayload: () => buildLivePayload(),
|
|
700
|
+
getAgentRoomRetryRound,
|
|
701
|
+
interruptAgentRoomRound,
|
|
702
|
+
liveState,
|
|
703
|
+
mapThreadToCompanionSnapshot,
|
|
704
|
+
normalizeAgentRoomState,
|
|
705
|
+
nowIso,
|
|
706
|
+
persistState: async (target, value, options = {}) => {
|
|
707
|
+
if (options.raw) {
|
|
708
|
+
await writeFile(target, value, "utf8");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
await agentRoomStore.save(target, value);
|
|
712
|
+
},
|
|
713
|
+
randomId: () => randomUUID(),
|
|
714
|
+
runtime: agentRoomRuntime,
|
|
715
|
+
setAgentRoomEnabled,
|
|
716
|
+
settleAgentRoomParticipant,
|
|
717
|
+
startAgentRoomRound,
|
|
718
|
+
store: agentRoomStore
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const {
|
|
722
|
+
buildSelectedAgentRoomState,
|
|
723
|
+
loadThreadAgentRoomState,
|
|
724
|
+
updateAgentRoom
|
|
725
|
+
} = agentRoomService;
|
|
726
|
+
|
|
727
|
+
const livePayloadBuilder = createLivePayloadBuilder({
|
|
728
|
+
advisoryParticipantForThread,
|
|
729
|
+
ADVISORY_PARTICIPANT_IDS,
|
|
730
|
+
bestConversationLabel,
|
|
731
|
+
bestThreadLabel,
|
|
732
|
+
buildBridgeStatus,
|
|
733
|
+
buildParticipant,
|
|
734
|
+
buildSelectedAgentRoomState,
|
|
735
|
+
buildSelectedAttachments,
|
|
736
|
+
buildSelectedCompanionState,
|
|
737
|
+
getPendingInteractionForSelectedThread,
|
|
738
|
+
liveState,
|
|
739
|
+
looksLikeTopicNoise,
|
|
740
|
+
normalizeLane,
|
|
741
|
+
projectLabel,
|
|
742
|
+
pruneAllCompanionWakeups,
|
|
743
|
+
pruneStaleSurfacePresence,
|
|
744
|
+
repoObjective,
|
|
745
|
+
selectedThreadSummary,
|
|
746
|
+
slugifyChannelName,
|
|
747
|
+
summarizeThread,
|
|
748
|
+
trimTopicText
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const {
|
|
752
|
+
applyTurnOrigins,
|
|
753
|
+
buildChannelTopic,
|
|
754
|
+
buildParticipants,
|
|
755
|
+
buildSelectedChannel,
|
|
756
|
+
decorateSnapshot,
|
|
757
|
+
inferEntryLane,
|
|
758
|
+
participantForEntry
|
|
759
|
+
} = livePayloadBuilder;
|
|
760
|
+
buildLivePayload = livePayloadBuilder.buildLivePayload;
|
|
761
|
+
|
|
762
|
+
const liveTranscriptState = createLiveTranscriptStateService({
|
|
763
|
+
extractNotificationDelta: (params = {}) => (
|
|
764
|
+
params.delta ??
|
|
765
|
+
params.textDelta ??
|
|
766
|
+
params.outputDelta ??
|
|
767
|
+
params.output ??
|
|
768
|
+
params.chunk ??
|
|
769
|
+
""
|
|
770
|
+
),
|
|
771
|
+
getDefaultCwd: () => liveState.selectedProjectCwd || process.cwd(),
|
|
772
|
+
liveState,
|
|
773
|
+
mapThreadItemToCompanionEntry,
|
|
774
|
+
nowIso,
|
|
775
|
+
visibleTranscriptLimit: SELECTED_TRANSCRIPT_PAGE_SIZE
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const { applyWatcherNotification } = liveTranscriptState;
|
|
779
|
+
|
|
780
|
+
({ buildAgentRoomContextMarkdown } = createAgentRoomContextBuilder({
|
|
781
|
+
buildSelectedChannel,
|
|
782
|
+
decorateSnapshot,
|
|
783
|
+
nowIso,
|
|
784
|
+
trimTopicText
|
|
785
|
+
}));
|
|
786
|
+
|
|
787
|
+
function recordControlEvent({
|
|
788
|
+
action,
|
|
789
|
+
actor = "system",
|
|
790
|
+
actorClientId = null,
|
|
791
|
+
cause = "manual",
|
|
792
|
+
owner = null,
|
|
793
|
+
ownerClientId = null,
|
|
794
|
+
ownerLabel = null,
|
|
795
|
+
reason = null,
|
|
796
|
+
source = null,
|
|
797
|
+
threadId = null
|
|
798
|
+
} = {}) {
|
|
799
|
+
if (!action || !threadId) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
appServerState.lastControlEvent = {
|
|
804
|
+
action,
|
|
805
|
+
actor,
|
|
806
|
+
actorClientId: actorClientId ? String(actorClientId).trim() : null,
|
|
807
|
+
actorLabel: surfaceActorLabel({ surface: actor, clientId: actorClientId }),
|
|
808
|
+
at: nowIso(),
|
|
809
|
+
cause,
|
|
810
|
+
id: randomUUID(),
|
|
811
|
+
owner,
|
|
812
|
+
ownerClientId: ownerClientId ? String(ownerClientId).trim() : null,
|
|
813
|
+
ownerLabel: ownerLabel || surfaceActorLabel({ surface: owner || source, clientId: ownerClientId }),
|
|
814
|
+
reason,
|
|
815
|
+
source,
|
|
816
|
+
threadId
|
|
817
|
+
};
|
|
818
|
+
return appServerState.lastControlEvent;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function maybeWakeCompanionForTurnCompletion({ threadId, turnId = null } = {}) {
|
|
822
|
+
const diff = liveState.turnDiff?.threadId === threadId ? String(liveState.turnDiff.diff || "").trim() : "";
|
|
823
|
+
if (!threadId || !diff) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return queueCompanionWakeup({
|
|
828
|
+
advisorId: "oracle",
|
|
829
|
+
text: "Review ready: the latest turn settled with live file changes. Oracle can do a quick risk pass if you want a second opinion.",
|
|
830
|
+
threadId,
|
|
831
|
+
turnId,
|
|
832
|
+
wakeKey: `oracle-review:${turnId || threadId}`,
|
|
833
|
+
wakeKind: "review"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function maybeWakeCompanionForCompaction({ threadId, turnId = null } = {}) {
|
|
838
|
+
if (!threadId) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return queueCompanionWakeup({
|
|
843
|
+
advisorId: "gemini",
|
|
844
|
+
text: "Summary ready: context compacted on this channel. Gemini can give you a quick continuity brief if you want one.",
|
|
845
|
+
threadId,
|
|
846
|
+
turnId,
|
|
847
|
+
wakeKey: `gemini-summary:${turnId || threadId}`,
|
|
848
|
+
wakeKind: "summary"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function maybeWakeCompanionForInteractionResolution({ interaction = null, threadId = null } = {}) {
|
|
853
|
+
const nextThreadId = threadId || interaction?.threadId || null;
|
|
854
|
+
if (!nextThreadId) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const kindLabel = interaction?.kindLabel
|
|
859
|
+
? humanizeServerText(interaction.kindLabel)
|
|
860
|
+
: humanizeServerText(interaction?.kind || "interaction");
|
|
861
|
+
const wakeKeySuffix =
|
|
862
|
+
interaction?.requestId ||
|
|
863
|
+
interaction?.at ||
|
|
864
|
+
interaction?.turnId ||
|
|
865
|
+
nextThreadId;
|
|
866
|
+
return queueCompanionWakeup({
|
|
867
|
+
advisorId: "gemini",
|
|
868
|
+
text: `Summary ready: the last ${kindLabel} step settled. Gemini can give you a quick recap if you want one.`,
|
|
869
|
+
threadId: nextThreadId,
|
|
870
|
+
turnId: interaction?.turnId || null,
|
|
871
|
+
wakeKey: `gemini-interaction:${interaction?.kind || "interaction"}:${wakeKeySuffix}`,
|
|
872
|
+
wakeKind: "summary"
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const controlLeaseService = createControlLeaseService({
|
|
877
|
+
broadcast,
|
|
878
|
+
buildLivePayload,
|
|
879
|
+
clearControlLeaseState,
|
|
880
|
+
defaultTtlMs: CONTROL_LEASE_TTL_MS,
|
|
881
|
+
ensureRemoteControlLeaseState,
|
|
882
|
+
getControlLeaseForThreadState,
|
|
883
|
+
liveState,
|
|
884
|
+
recordControlEvent,
|
|
885
|
+
renewControlLeaseState,
|
|
886
|
+
setControlLeaseState
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
const {
|
|
890
|
+
clearControlLease,
|
|
891
|
+
ensureRemoteControlLease,
|
|
892
|
+
getControlLeaseForSelectedThread,
|
|
893
|
+
getControlLeaseForThread,
|
|
894
|
+
renewControlLease,
|
|
895
|
+
scheduleControlLeaseExpiry,
|
|
896
|
+
setControlLease
|
|
897
|
+
} = controlLeaseService;
|
|
898
|
+
const staticSurfaceService = createStaticSurfaceService({
|
|
899
|
+
exposeHostSurface,
|
|
900
|
+
issueSurfaceBootstrap: (surface) => surfaceAccess.issueBootstrap(surface),
|
|
901
|
+
mimeTypes,
|
|
902
|
+
publicDir,
|
|
903
|
+
sendJson
|
|
904
|
+
});
|
|
905
|
+
const { serveStatic } = staticSurfaceService;
|
|
906
|
+
|
|
907
|
+
function broadcast(event, payload) {
|
|
908
|
+
sseHub.broadcast(event, payload);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function requireSurfaceCapability(req, url, capability) {
|
|
912
|
+
return surfaceAccess.requireCapability({
|
|
913
|
+
capability,
|
|
914
|
+
headers: req.headers,
|
|
915
|
+
searchParams: url.searchParams
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function resolveSurfaceAccess(req, url) {
|
|
920
|
+
return surfaceAccess.resolve({
|
|
921
|
+
headers: req.headers,
|
|
922
|
+
searchParams: url.searchParams
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function accessClientId(access) {
|
|
927
|
+
return String(access?.clientId || "").trim() || null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function errorStatusCode(error, fallback = 500) {
|
|
931
|
+
const code = Number(error?.statusCode || error?.status || fallback);
|
|
932
|
+
return Number.isFinite(code) && code > 0 ? code : fallback;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function summarizeThread(thread) {
|
|
936
|
+
const channelLabel = bestThreadLabel(thread, null);
|
|
937
|
+
const openingPreview = deriveOpeningPreview(thread);
|
|
938
|
+
const latestPreview = deriveLatestPreview(thread);
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
channelLabel,
|
|
942
|
+
channelSlug: `#${slugifyChannelName(channelLabel)}`,
|
|
943
|
+
cwd: thread.cwd || null,
|
|
944
|
+
id: thread.id,
|
|
945
|
+
name: thread.name || null,
|
|
946
|
+
openingPreview,
|
|
947
|
+
preview: latestPreview || trimTopicText(normalizePreviewText(thread.preview), THREAD_PREVIEW_MAX_CHARS) || null,
|
|
948
|
+
serverLabel: projectLabel(thread.cwd || ""),
|
|
949
|
+
source: thread.source || null,
|
|
950
|
+
status: thread.status || null,
|
|
951
|
+
updatedAt: thread.updatedAt || null
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function deriveOpeningPreview(thread) {
|
|
956
|
+
if (!thread || !Array.isArray(thread.turns) || thread.turns.length === 0) {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const snapshot = mapThreadToCompanionSnapshot(thread, { limit: null });
|
|
961
|
+
const opener = snapshot?.transcript?.find((entry) => String(entry?.text || "").trim().length > 0) || null;
|
|
962
|
+
const text = normalizePreviewText(opener?.text || "");
|
|
963
|
+
if (!text) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
return trimTopicText(text, THREAD_PREVIEW_MAX_CHARS);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function deriveLatestPreview(thread) {
|
|
970
|
+
let transcript = [];
|
|
971
|
+
|
|
972
|
+
if (thread && Array.isArray(thread.turns) && thread.turns.length > 0) {
|
|
973
|
+
transcript = mapThreadToCompanionSnapshot(thread, { limit: null }).transcript || [];
|
|
974
|
+
} else if (thread?.path) {
|
|
975
|
+
transcript = readTranscriptFromSessionLog(thread.path, {
|
|
976
|
+
limit: 24,
|
|
977
|
+
maxBytes: THREAD_PREVIEW_TAIL_BYTES
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (!transcript.length) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const latest =
|
|
986
|
+
[...transcript]
|
|
987
|
+
.reverse()
|
|
988
|
+
.find((entry) => (
|
|
989
|
+
(entry?.role === "assistant" || entry?.role === "user") &&
|
|
990
|
+
entry?.kind !== "commentary" &&
|
|
991
|
+
normalizePreviewText(entry?.text || "") &&
|
|
992
|
+
!looksLikeTopicNoise(normalizePreviewText(entry?.text || ""))
|
|
993
|
+
)) || null;
|
|
994
|
+
const text = normalizePreviewText(latest?.text || "");
|
|
995
|
+
if (!text) {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
return trimTopicText(text, THREAD_PREVIEW_MAX_CHARS);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function rememberTurnOrigin(threadId, turnId, source) {
|
|
1002
|
+
if (!threadId || !turnId || !source) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const current = liveState.turnOriginsByThreadId[threadId] || {};
|
|
1007
|
+
const next = {
|
|
1008
|
+
...current,
|
|
1009
|
+
[turnId]: source
|
|
1010
|
+
};
|
|
1011
|
+
const keys = Object.keys(next);
|
|
1012
|
+
|
|
1013
|
+
if (keys.length > 80) {
|
|
1014
|
+
const trimmed = {};
|
|
1015
|
+
for (const key of keys.slice(-80)) {
|
|
1016
|
+
trimmed[key] = next[key];
|
|
1017
|
+
}
|
|
1018
|
+
liveState.turnOriginsByThreadId[threadId] = trimmed;
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
liveState.turnOriginsByThreadId[threadId] = next;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function buildSelectedThreadSnapshotFromLog(thread, { limit = SELECTED_TRANSCRIPT_PAGE_SIZE } = {}) {
|
|
1026
|
+
const transcriptLimit = Math.max(1, Number.parseInt(limit, 10) || SELECTED_TRANSCRIPT_PAGE_SIZE);
|
|
1027
|
+
const snapshot = buildLightweightSelectedThreadSnapshotFromLog(thread, {
|
|
1028
|
+
limit: transcriptLimit
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
if (Array.isArray(thread?.turns) && thread.turns.length > 0) {
|
|
1032
|
+
const fullSnapshot = mapThreadToCompanionSnapshot(thread, { limit: transcriptLimit });
|
|
1033
|
+
if (fullSnapshot.transcriptCount > snapshot.transcriptCount) {
|
|
1034
|
+
return fullSnapshot;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (snapshot.transcriptCount >= transcriptLimit) {
|
|
1039
|
+
return snapshot;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const fullThread = await codexAppServer.readThread(thread.id, true);
|
|
1043
|
+
if (!fullThread) {
|
|
1044
|
+
return snapshot;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const fullSnapshot = mapThreadToCompanionSnapshot(fullThread, { limit: transcriptLimit });
|
|
1048
|
+
return fullSnapshot.transcriptCount > snapshot.transcriptCount ? fullSnapshot : snapshot;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function buildLightweightSelectedThreadSnapshotFromLog(thread, { limit = SELECTED_TRANSCRIPT_PAGE_SIZE } = {}) {
|
|
1052
|
+
const transcriptLimit = Math.max(1, Number.parseInt(limit, 10) || SELECTED_TRANSCRIPT_PAGE_SIZE);
|
|
1053
|
+
if (Array.isArray(thread?.turns) && thread.turns.length > 0) {
|
|
1054
|
+
return mapThreadToCompanionSnapshot(thread, { limit: transcriptLimit });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return buildSessionLogSnapshot(thread, {
|
|
1058
|
+
limit: transcriptLimit,
|
|
1059
|
+
maxBytes: SESSION_LOG_TAIL_BYTES
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function selectedThreadSnapshotNeedsDeepHydration(snapshot, {
|
|
1064
|
+
limit = SELECTED_TRANSCRIPT_PAGE_SIZE,
|
|
1065
|
+
thread = null
|
|
1066
|
+
} = {}) {
|
|
1067
|
+
const transcriptLimit = Math.max(1, Number.parseInt(limit, 10) || SELECTED_TRANSCRIPT_PAGE_SIZE);
|
|
1068
|
+
if (Array.isArray(thread?.turns) && thread.turns.length > 0) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return Number(snapshot?.transcriptCount || 0) < transcriptLimit;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function buildTranscriptHistoryPageFromThread(thread, {
|
|
1076
|
+
beforeIndex = null,
|
|
1077
|
+
limit = 40,
|
|
1078
|
+
visibleCount = null
|
|
1079
|
+
} = {}) {
|
|
1080
|
+
const transcript = mapThreadToCompanionSnapshot(thread, { limit: null }).transcript || [];
|
|
1081
|
+
return pageTranscriptEntries(transcript, {
|
|
1082
|
+
beforeIndex,
|
|
1083
|
+
limit,
|
|
1084
|
+
visibleCount
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function loadTranscriptHistoryPage({
|
|
1089
|
+
beforeIndex = null,
|
|
1090
|
+
limit = 40,
|
|
1091
|
+
threadId,
|
|
1092
|
+
visibleCount = null
|
|
1093
|
+
} = {}) {
|
|
1094
|
+
if (!threadId) {
|
|
1095
|
+
throw new Error("threadId is required");
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const thread =
|
|
1099
|
+
(liveState.selectedThreadSnapshot?.thread?.id === threadId
|
|
1100
|
+
? liveState.selectedThreadSnapshot.thread
|
|
1101
|
+
: liveState.threads.find((entry) => entry.id === threadId)) ||
|
|
1102
|
+
await codexAppServer.readThread(threadId, false);
|
|
1103
|
+
|
|
1104
|
+
if (!thread) {
|
|
1105
|
+
throw Object.assign(new Error(`Thread ${threadId} not found.`), { statusCode: 404 });
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const logPage = readTranscriptHistoryPageFromSessionLog(thread.path, {
|
|
1109
|
+
beforeIndex,
|
|
1110
|
+
limit,
|
|
1111
|
+
visibleCount
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
if (Array.isArray(thread.turns) && thread.turns.length > 0) {
|
|
1115
|
+
const fullPage = buildTranscriptHistoryPageFromThread(thread, {
|
|
1116
|
+
beforeIndex,
|
|
1117
|
+
limit,
|
|
1118
|
+
visibleCount
|
|
1119
|
+
});
|
|
1120
|
+
if (fullPage.totalCount > logPage.totalCount) {
|
|
1121
|
+
return fullPage;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const fullThread = await codexAppServer.readThread(threadId, true);
|
|
1126
|
+
if (!fullThread) {
|
|
1127
|
+
return logPage;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const fullPage = buildTranscriptHistoryPageFromThread(fullThread, {
|
|
1131
|
+
beforeIndex,
|
|
1132
|
+
limit,
|
|
1133
|
+
visibleCount
|
|
1134
|
+
});
|
|
1135
|
+
return fullPage.totalCount > logPage.totalCount ? fullPage : logPage;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const threadSyncState = createThreadSyncStateService({
|
|
1139
|
+
broadcast,
|
|
1140
|
+
buildLivePayload,
|
|
1141
|
+
buildLightweightSelectedThreadSnapshot: buildLightweightSelectedThreadSnapshotFromLog,
|
|
1142
|
+
buildSelectedThreadSnapshot: buildSelectedThreadSnapshotFromLog,
|
|
1143
|
+
clearControlLease,
|
|
1144
|
+
codexAppServer,
|
|
1145
|
+
fallbackLiveSourceKinds,
|
|
1146
|
+
liveState,
|
|
1147
|
+
loadThreadAgentRoomState,
|
|
1148
|
+
mapThreadToCompanionSnapshot,
|
|
1149
|
+
nowIso,
|
|
1150
|
+
preferredLiveSourceKinds,
|
|
1151
|
+
processCwd: () => process.cwd(),
|
|
1152
|
+
snapshotNeedsDeepHydration: selectedThreadSnapshotNeedsDeepHydration,
|
|
1153
|
+
selectedTranscriptLimit: SELECTED_TRANSCRIPT_PAGE_SIZE,
|
|
1154
|
+
readSelectedThread: (threadId) => codexAppServer.readThread(threadId, false),
|
|
1155
|
+
summarizeThread
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
const {
|
|
1159
|
+
createThreadSelectionState,
|
|
1160
|
+
mergeSelectedThreadSnapshot,
|
|
1161
|
+
prewarmThreadSnapshots,
|
|
1162
|
+
refreshLiveState,
|
|
1163
|
+
refreshSelectedThreadSnapshot,
|
|
1164
|
+
refreshThreads
|
|
1165
|
+
} = threadSyncState;
|
|
1166
|
+
const watcherLifecycle = createWatcherLifecycleService({
|
|
1167
|
+
appServerState,
|
|
1168
|
+
applyWatcherNotification,
|
|
1169
|
+
beginInteractionFlow,
|
|
1170
|
+
broadcast,
|
|
1171
|
+
buildLivePayload,
|
|
1172
|
+
clearInteractionFlow,
|
|
1173
|
+
codexAppServer,
|
|
1174
|
+
invalidateRepoChangesCache,
|
|
1175
|
+
liveState,
|
|
1176
|
+
mapPendingInteraction,
|
|
1177
|
+
maybeWakeCompanionForCompaction,
|
|
1178
|
+
maybeWakeCompanionForInteractionResolution,
|
|
1179
|
+
maybeWakeCompanionForTurnCompletion,
|
|
1180
|
+
nowIso,
|
|
1181
|
+
refreshSelectedThreadSnapshot,
|
|
1182
|
+
rememberTurnOrigin,
|
|
1183
|
+
resetCompanionWakeups,
|
|
1184
|
+
summarizeNotificationInteraction,
|
|
1185
|
+
watchRefreshMethods
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const {
|
|
1189
|
+
clearWatcher,
|
|
1190
|
+
getWatcherController,
|
|
1191
|
+
hasWatcherController,
|
|
1192
|
+
restartWatcher,
|
|
1193
|
+
scheduleSnapshotRefresh
|
|
1194
|
+
} = watcherLifecycle;
|
|
1195
|
+
const selectionStateService = createSelectionStateService({
|
|
1196
|
+
appServerState,
|
|
1197
|
+
applyLiveSelectionTransition,
|
|
1198
|
+
bestThreadLabel,
|
|
1199
|
+
broadcast,
|
|
1200
|
+
buildLivePayload,
|
|
1201
|
+
createThreadSelectionState,
|
|
1202
|
+
getPendingInteractionForSelectedThread,
|
|
1203
|
+
liveState,
|
|
1204
|
+
nowIso,
|
|
1205
|
+
nowMs: () => Date.now(),
|
|
1206
|
+
projectLabel,
|
|
1207
|
+
randomId: () => randomUUID(),
|
|
1208
|
+
refreshSelectedThreadSnapshot,
|
|
1209
|
+
refreshThreads,
|
|
1210
|
+
restartWatcher,
|
|
1211
|
+
scheduleControlLeaseExpiry,
|
|
1212
|
+
shortThreadId,
|
|
1213
|
+
slugifyChannelName,
|
|
1214
|
+
surfaceActorLabel
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
const {
|
|
1218
|
+
createThreadSelection,
|
|
1219
|
+
setSelection
|
|
1220
|
+
} = selectionStateService;
|
|
1221
|
+
const debugHarnessService = createDebugHarnessService({
|
|
1222
|
+
ADVISORY_PARTICIPANT_IDS,
|
|
1223
|
+
appServerState,
|
|
1224
|
+
broadcast,
|
|
1225
|
+
buildLivePayload,
|
|
1226
|
+
getDefaultCwd: () => process.cwd(),
|
|
1227
|
+
liveState,
|
|
1228
|
+
nowIso,
|
|
1229
|
+
nowMs: () => Date.now(),
|
|
1230
|
+
queueCompanionWakeup
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
const {
|
|
1234
|
+
clearDebugPendingInteraction,
|
|
1235
|
+
setDebugCompanionWakeup,
|
|
1236
|
+
setDebugPendingInteraction
|
|
1237
|
+
} = debugHarnessService;
|
|
1238
|
+
|
|
1239
|
+
const interactionResolutionService = createInteractionResolutionService({
|
|
1240
|
+
appServerState,
|
|
1241
|
+
broadcast,
|
|
1242
|
+
buildLivePayload,
|
|
1243
|
+
controlLeaseTtlMs: CONTROL_LEASE_TTL_MS,
|
|
1244
|
+
ensureRemoteControlLease,
|
|
1245
|
+
getWatcherController,
|
|
1246
|
+
hasWatcherController,
|
|
1247
|
+
liveState,
|
|
1248
|
+
maybeWakeCompanionForInteractionResolution,
|
|
1249
|
+
nowIso,
|
|
1250
|
+
setControlLease
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const {
|
|
1254
|
+
resolvePendingInteraction
|
|
1255
|
+
} = interactionResolutionService;
|
|
1256
|
+
|
|
1257
|
+
const bridgeRuntimeLifecycle = createBridgeRuntimeLifecycleService({
|
|
1258
|
+
broadcast,
|
|
1259
|
+
buildLivePayload,
|
|
1260
|
+
cleanupAttachmentDir,
|
|
1261
|
+
codexAppServer,
|
|
1262
|
+
liveState,
|
|
1263
|
+
prewarmThreadSnapshots,
|
|
1264
|
+
refreshSelectedThreadSnapshot,
|
|
1265
|
+
refreshThreads,
|
|
1266
|
+
restartWatcher,
|
|
1267
|
+
scheduleSnapshotRefresh
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
const {
|
|
1271
|
+
bootstrapLiveState,
|
|
1272
|
+
interruptSelectedThread
|
|
1273
|
+
} = bridgeRuntimeLifecycle;
|
|
1274
|
+
|
|
1275
|
+
function streamState(req, res) {
|
|
1276
|
+
sseHub.open(res, [
|
|
1277
|
+
{ event: "snapshot", payload: store.getState() },
|
|
1278
|
+
{ event: "live", payload: buildLivePayload() }
|
|
1279
|
+
], {
|
|
1280
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1281
|
+
"Content-Type": "text/event-stream; charset=utf-8"
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
req.on("close", () => {
|
|
1285
|
+
sseHub.close(res);
|
|
1286
|
+
res.end();
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
store.subscribe((snapshot) => {
|
|
1291
|
+
broadcast("snapshot", snapshot);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
setInterval(() => {
|
|
1295
|
+
const surfacesChanged = pruneStaleSurfacePresence();
|
|
1296
|
+
const companionChanged = pruneAllCompanionWakeups();
|
|
1297
|
+
if (surfacesChanged || companionChanged) {
|
|
1298
|
+
broadcast("live", buildLivePayload());
|
|
1299
|
+
}
|
|
1300
|
+
}, SURFACE_PRESENCE_SWEEP_MS);
|
|
1301
|
+
|
|
1302
|
+
const server = createServer(async (req, res) => {
|
|
1303
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
1304
|
+
try {
|
|
1305
|
+
const handled = await handleBridgeApiRequest({
|
|
1306
|
+
req,
|
|
1307
|
+
res,
|
|
1308
|
+
url,
|
|
1309
|
+
deps: {
|
|
1310
|
+
CONTROL_LEASE_TTL_MS,
|
|
1311
|
+
accessClientId,
|
|
1312
|
+
appServerState,
|
|
1313
|
+
applyCompanionWakeupAction,
|
|
1314
|
+
applyLiveControlAction,
|
|
1315
|
+
applySurfacePresenceUpdate,
|
|
1316
|
+
broadcast,
|
|
1317
|
+
buildBridgeStatus,
|
|
1318
|
+
buildInstallPreflight,
|
|
1319
|
+
buildLivePayload,
|
|
1320
|
+
buildLiveTurnChanges,
|
|
1321
|
+
buildSelectedThreadSnapshot: buildSelectedThreadSnapshotFromLog,
|
|
1322
|
+
canServeSurfaceBootstrap,
|
|
1323
|
+
clearDebugPendingInteraction,
|
|
1324
|
+
codexAppServer,
|
|
1325
|
+
createThreadSelection,
|
|
1326
|
+
decorateSnapshot,
|
|
1327
|
+
devToolsEnabled,
|
|
1328
|
+
exposeHostSurface,
|
|
1329
|
+
ensureRemoteControlLease,
|
|
1330
|
+
errorStatusCode,
|
|
1331
|
+
getCachedRepoChanges,
|
|
1332
|
+
getControlLeaseForThread,
|
|
1333
|
+
interruptSelectedThread,
|
|
1334
|
+
invalidateRepoChangesCache,
|
|
1335
|
+
issueSurfaceBootstrap: (surface) => surfaceAccess.issueBootstrap(surface),
|
|
1336
|
+
liveState,
|
|
1337
|
+
loadTranscriptHistoryPage,
|
|
1338
|
+
cwd: process.cwd(),
|
|
1339
|
+
mapThreadToCompanionSnapshot,
|
|
1340
|
+
mergeSelectedThreadSnapshot,
|
|
1341
|
+
mockAdapter,
|
|
1342
|
+
nowIso,
|
|
1343
|
+
openThreadInCodex,
|
|
1344
|
+
persistImageAttachments,
|
|
1345
|
+
readJsonBody,
|
|
1346
|
+
recordControlEvent,
|
|
1347
|
+
refreshLiveState,
|
|
1348
|
+
refreshThreads,
|
|
1349
|
+
rememberTurnOrigin,
|
|
1350
|
+
requireSurfaceCapability,
|
|
1351
|
+
resolvePendingInteraction,
|
|
1352
|
+
resolveSurfaceAccess,
|
|
1353
|
+
restartWatcher,
|
|
1354
|
+
hasWatcherController,
|
|
1355
|
+
scheduleSnapshotRefresh,
|
|
1356
|
+
scheduleControlLeaseExpiry,
|
|
1357
|
+
sendJson,
|
|
1358
|
+
setDebugCompanionWakeup,
|
|
1359
|
+
setDebugPendingInteraction,
|
|
1360
|
+
setSelection,
|
|
1361
|
+
store,
|
|
1362
|
+
streamState,
|
|
1363
|
+
summonCompanionWakeup,
|
|
1364
|
+
updateAgentRoom,
|
|
1365
|
+
runtimeConfig
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
if (handled) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (req.method === "GET") {
|
|
1373
|
+
const baseUrl = requestBaseUrl(req, host === "0.0.0.0" ? "127.0.0.1" : host, port);
|
|
1374
|
+
|
|
1375
|
+
if (url.pathname === DISCOVERY_MANIFEST_PATH) {
|
|
1376
|
+
sendJson(res, 200, buildWellKnownManifest({ baseUrl }));
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (url.pathname === OPENAPI_DOC_PATH) {
|
|
1381
|
+
sendJson(res, 200, buildOpenApiDocument({ baseUrl }));
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (url.pathname === ARAZZO_DOC_PATH) {
|
|
1386
|
+
res.writeHead(200, {
|
|
1387
|
+
"Cache-Control": "no-store, max-age=0",
|
|
1388
|
+
"Content-Type": "application/vnd.oai.workflows+json; charset=utf-8",
|
|
1389
|
+
Pragma: "no-cache"
|
|
1390
|
+
});
|
|
1391
|
+
res.end(JSON.stringify(buildArazzoDocument({ baseUrl })));
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (url.pathname === LLMS_TXT_PATH) {
|
|
1396
|
+
res.writeHead(200, {
|
|
1397
|
+
"Cache-Control": "no-store, max-age=0",
|
|
1398
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1399
|
+
Pragma: "no-cache"
|
|
1400
|
+
});
|
|
1401
|
+
res.end(buildLlmsText({ baseUrl }));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
await serveStatic(req, res, url.pathname);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
sendJson(res, 405, { error: "Method not allowed" });
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, state: buildLivePayload() });
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
const attachmentSweepTimer = setInterval(() => {
|
|
1416
|
+
void cleanupAttachmentDir();
|
|
1417
|
+
}, ATTACHMENT_SWEEP_MS);
|
|
1418
|
+
attachmentSweepTimer.unref?.();
|
|
1419
|
+
|
|
1420
|
+
server.listen(port, host, () => {
|
|
1421
|
+
const address = server.address();
|
|
1422
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
1423
|
+
const printableHost =
|
|
1424
|
+
host === "0.0.0.0"
|
|
1425
|
+
? "0.0.0.0 (all interfaces; 127.0.0.1 also works)"
|
|
1426
|
+
: host;
|
|
1427
|
+
const localOpenHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
1428
|
+
const baseUrl = `http://${localOpenHost}:${actualPort}`;
|
|
1429
|
+
console.log(`Dextunnel MVP listening on http://${printableHost}:${actualPort}`);
|
|
1430
|
+
console.log(`Remote: ${baseUrl}/`);
|
|
1431
|
+
console.log(`Legacy remote path: ${baseUrl}/remote.html`);
|
|
1432
|
+
console.log("Preflight: npm run doctor");
|
|
1433
|
+
if (host === "127.0.0.1") {
|
|
1434
|
+
console.log("Phone or tablet access: npm run start:network");
|
|
1435
|
+
}
|
|
1436
|
+
void bootstrapLiveState();
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1440
|
+
process.on(signal, async () => {
|
|
1441
|
+
clearWatcher();
|
|
1442
|
+
mockAdapter?.stop();
|
|
1443
|
+
await codexAppServer.dispose();
|
|
1444
|
+
process.exit(0);
|
|
1445
|
+
});
|
|
1446
|
+
}
|