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,549 @@
1
+ export function createLiveTranscriptStateService({
2
+ liveState,
3
+ mapThreadItemToCompanionEntry,
4
+ nowIso = () => new Date().toISOString(),
5
+ getDefaultCwd = () => process.cwd(),
6
+ extractNotificationDelta = (params = {}) => (
7
+ params.delta ??
8
+ params.textDelta ??
9
+ params.outputDelta ??
10
+ params.output ??
11
+ params.chunk ??
12
+ ""
13
+ ),
14
+ visibleTranscriptLimit = 120
15
+ } = {}) {
16
+ function mergeThreadSummary(threadId, patch = {}) {
17
+ if (!threadId) {
18
+ return;
19
+ }
20
+
21
+ liveState.threads = liveState.threads.map((thread) => (
22
+ thread.id === threadId
23
+ ? {
24
+ ...thread,
25
+ ...patch,
26
+ id: thread.id
27
+ }
28
+ : thread
29
+ ));
30
+ }
31
+
32
+ function ensureLiveSelectedSnapshot(threadId, cwd = null) {
33
+ const current = liveState.selectedThreadSnapshot;
34
+ if (current?.thread?.id === threadId) {
35
+ return current;
36
+ }
37
+
38
+ const summary = liveState.threads.find((thread) => thread.id === threadId) || null;
39
+ const nextSnapshot = {
40
+ thread: {
41
+ activeTurnId: null,
42
+ activeTurnStatus: null,
43
+ cwd: cwd || summary?.cwd || getDefaultCwd(),
44
+ id: threadId,
45
+ lastTurnId: null,
46
+ lastTurnStatus: null,
47
+ name: summary?.name || null,
48
+ path: null,
49
+ preview: summary?.preview || null,
50
+ source: summary?.source || null,
51
+ status: summary?.status || null,
52
+ updatedAt: summary?.updatedAt || nowIso()
53
+ },
54
+ transcript: [],
55
+ transcriptCount: 0
56
+ };
57
+
58
+ liveState.selectedThreadSnapshot = nextSnapshot;
59
+ return nextSnapshot;
60
+ }
61
+
62
+ function commitLiveSelectedSnapshot(snapshot) {
63
+ liveState.selectedThreadSnapshot = snapshot;
64
+ liveState.lastSyncAt = nowIso();
65
+ liveState.lastError = null;
66
+ }
67
+
68
+ function clampTranscriptEntries(entries) {
69
+ return entries.slice(-visibleTranscriptLimit);
70
+ }
71
+
72
+ function upsertTranscriptEntry(snapshot, entry) {
73
+ const transcript = Array.isArray(snapshot?.transcript) ? [...snapshot.transcript] : [];
74
+ const transcriptCountBase = Number.isFinite(snapshot?.transcriptCount)
75
+ ? snapshot.transcriptCount
76
+ : transcript.length;
77
+ const nextEntry = {
78
+ ...entry
79
+ };
80
+ const index = nextEntry.itemId
81
+ ? transcript.findIndex((existing) => existing.itemId && existing.itemId === nextEntry.itemId)
82
+ : -1;
83
+
84
+ if (index >= 0) {
85
+ transcript[index] = {
86
+ ...transcript[index],
87
+ ...nextEntry,
88
+ itemId: transcript[index].itemId || nextEntry.itemId || null,
89
+ text: nextEntry.text ?? transcript[index].text,
90
+ timestamp: nextEntry.timestamp || transcript[index].timestamp || null
91
+ };
92
+ return {
93
+ ...snapshot,
94
+ transcript: clampTranscriptEntries(transcript),
95
+ transcriptCount: Math.max(transcriptCountBase, transcript.length)
96
+ };
97
+ }
98
+
99
+ transcript.push(nextEntry);
100
+ return {
101
+ ...snapshot,
102
+ transcript: clampTranscriptEntries(transcript),
103
+ transcriptCount: Math.max(transcriptCountBase + 1, transcript.length)
104
+ };
105
+ }
106
+
107
+ function updateTranscriptEntryByItemId(snapshot, itemId, updater) {
108
+ if (!itemId) {
109
+ return snapshot;
110
+ }
111
+
112
+ const transcript = Array.isArray(snapshot?.transcript) ? [...snapshot.transcript] : [];
113
+ const index = transcript.findIndex((entry) => entry.itemId && entry.itemId === itemId);
114
+ if (index < 0) {
115
+ return snapshot;
116
+ }
117
+
118
+ const updated = updater(transcript[index]);
119
+ if (!updated) {
120
+ return snapshot;
121
+ }
122
+
123
+ transcript[index] = updated;
124
+ return {
125
+ ...snapshot,
126
+ transcript: clampTranscriptEntries(transcript),
127
+ transcriptCount: Number.isFinite(snapshot?.transcriptCount) ? snapshot.transcriptCount : transcript.length
128
+ };
129
+ }
130
+
131
+ function applyTranscriptItemUpdate({ threadId, cwd, turnId, item, timestamp = nowIso() }) {
132
+ if (!threadId || !item) {
133
+ return false;
134
+ }
135
+
136
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
137
+ const nextSnapshot = upsertTranscriptEntry(
138
+ {
139
+ ...snapshot,
140
+ thread: {
141
+ ...snapshot.thread,
142
+ cwd: cwd || snapshot.thread?.cwd || null,
143
+ lastTurnId: turnId || snapshot.thread?.lastTurnId || null,
144
+ updatedAt: timestamp
145
+ }
146
+ },
147
+ mapThreadItemToCompanionEntry(item, {
148
+ id: turnId || snapshot.thread?.lastTurnId || null,
149
+ startedAt: timestamp,
150
+ updatedAt: timestamp
151
+ })
152
+ );
153
+
154
+ commitLiveSelectedSnapshot(nextSnapshot);
155
+ return true;
156
+ }
157
+
158
+ function appendToTranscriptItem({ threadId, cwd, turnId, itemId, defaults, appendText, timestamp = nowIso() }) {
159
+ if (!threadId || !itemId || !appendText) {
160
+ return false;
161
+ }
162
+
163
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
164
+ const existing = (snapshot.transcript || []).find((entry) => entry.itemId === itemId) || null;
165
+ const baseEntry = existing || {
166
+ itemId,
167
+ kind: defaults.kind,
168
+ phase: defaults.phase || null,
169
+ role: defaults.role,
170
+ text: "",
171
+ timestamp,
172
+ turnId: turnId || snapshot.thread?.lastTurnId || null
173
+ };
174
+
175
+ const nextEntry = {
176
+ ...baseEntry,
177
+ ...defaults,
178
+ itemId,
179
+ text: `${baseEntry.text || ""}${appendText}`,
180
+ timestamp,
181
+ turnId: turnId || baseEntry.turnId || null
182
+ };
183
+
184
+ const nextSnapshot = upsertTranscriptEntry(
185
+ {
186
+ ...snapshot,
187
+ thread: {
188
+ ...snapshot.thread,
189
+ cwd: cwd || snapshot.thread?.cwd || null,
190
+ lastTurnId: turnId || snapshot.thread?.lastTurnId || null,
191
+ updatedAt: timestamp
192
+ }
193
+ },
194
+ nextEntry
195
+ );
196
+
197
+ commitLiveSelectedSnapshot(nextSnapshot);
198
+ return true;
199
+ }
200
+
201
+ function appendCommandOutputDelta({ threadId, cwd, turnId, itemId, delta, timestamp = nowIso() }) {
202
+ if (!threadId || !itemId || !delta) {
203
+ return false;
204
+ }
205
+
206
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
207
+ const nextSnapshot = updateTranscriptEntryByItemId(snapshot, itemId, (entry) => {
208
+ const needsSeparator = entry.text && !entry.text.includes("\n");
209
+ return {
210
+ ...entry,
211
+ text: `${entry.text || ""}${needsSeparator ? "\n" : ""}${delta}`,
212
+ timestamp,
213
+ turnId: turnId || entry.turnId || null
214
+ };
215
+ });
216
+
217
+ if (nextSnapshot === snapshot) {
218
+ return false;
219
+ }
220
+
221
+ commitLiveSelectedSnapshot({
222
+ ...nextSnapshot,
223
+ thread: {
224
+ ...nextSnapshot.thread,
225
+ updatedAt: timestamp
226
+ }
227
+ });
228
+ return true;
229
+ }
230
+
231
+ function appendFileChangeOutputDelta({ threadId, cwd, turnId, itemId, delta, timestamp = nowIso() }) {
232
+ if (!threadId || !itemId || !delta) {
233
+ return false;
234
+ }
235
+
236
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
237
+ const nextSnapshot = updateTranscriptEntryByItemId(snapshot, itemId, (entry) => {
238
+ const separator = entry.text && !entry.text.endsWith("\n") ? "\n" : "";
239
+ return {
240
+ ...entry,
241
+ text: `${entry.text || ""}${separator}${delta}`,
242
+ timestamp,
243
+ turnId: turnId || entry.turnId || null
244
+ };
245
+ });
246
+
247
+ if (nextSnapshot === snapshot) {
248
+ return false;
249
+ }
250
+
251
+ commitLiveSelectedSnapshot({
252
+ ...nextSnapshot,
253
+ thread: {
254
+ ...nextSnapshot.thread,
255
+ updatedAt: timestamp
256
+ }
257
+ });
258
+ return true;
259
+ }
260
+
261
+ function applyTurnPlanUpdate({ threadId, cwd, turnId, explanation = null, plan = null, timestamp = nowIso() }) {
262
+ if (!threadId) {
263
+ return false;
264
+ }
265
+
266
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
267
+ const nextThread = {
268
+ ...snapshot.thread,
269
+ cwd: cwd || snapshot.thread?.cwd || null,
270
+ lastTurnId: turnId || snapshot.thread?.lastTurnId || null,
271
+ livePlan: Array.isArray(plan) ? plan : snapshot.thread?.livePlan || null,
272
+ planExplanation: explanation ?? snapshot.thread?.planExplanation ?? null,
273
+ updatedAt: timestamp
274
+ };
275
+
276
+ commitLiveSelectedSnapshot({
277
+ ...snapshot,
278
+ thread: nextThread
279
+ });
280
+ return true;
281
+ }
282
+
283
+ function applyThreadTokenUsageUpdate({ threadId, cwd, tokenUsage, timestamp = nowIso() }) {
284
+ if (!threadId || !tokenUsage) {
285
+ return false;
286
+ }
287
+
288
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
289
+ commitLiveSelectedSnapshot({
290
+ ...snapshot,
291
+ thread: {
292
+ ...snapshot.thread,
293
+ cwd: cwd || snapshot.thread?.cwd || null,
294
+ tokenUsage,
295
+ updatedAt: timestamp
296
+ }
297
+ });
298
+ return true;
299
+ }
300
+
301
+ function applyTurnLifecycleUpdate({ threadId, cwd, turn, timestamp = nowIso() }) {
302
+ if (!threadId) {
303
+ return false;
304
+ }
305
+
306
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
307
+ const status = turn?.status || null;
308
+ const nextThread = {
309
+ ...snapshot.thread,
310
+ activeTurnId: status === "inProgress" ? turn?.id || snapshot.thread?.activeTurnId || null : null,
311
+ activeTurnStatus: status === "inProgress" ? status : null,
312
+ cwd: cwd || turn?.cwd || snapshot.thread?.cwd || null,
313
+ id: threadId,
314
+ lastTurnId: turn?.id || snapshot.thread?.lastTurnId || null,
315
+ lastTurnStatus: status || snapshot.thread?.lastTurnStatus || null,
316
+ status: status === "inProgress" ? "inProgress" : snapshot.thread?.status || null,
317
+ updatedAt: turn?.updatedAt || turn?.startedAt || timestamp
318
+ };
319
+
320
+ if (status && status !== "inProgress") {
321
+ nextThread.status = status;
322
+ }
323
+
324
+ commitLiveSelectedSnapshot({
325
+ ...snapshot,
326
+ thread: nextThread
327
+ });
328
+ mergeThreadSummary(threadId, {
329
+ cwd: nextThread.cwd,
330
+ status: nextThread.status,
331
+ updatedAt: nextThread.updatedAt
332
+ });
333
+ return true;
334
+ }
335
+
336
+ function applyThreadNameUpdate({ threadId, cwd, name, timestamp = nowIso() }) {
337
+ if (!threadId || !name) {
338
+ return false;
339
+ }
340
+
341
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
342
+ commitLiveSelectedSnapshot({
343
+ ...snapshot,
344
+ thread: {
345
+ ...snapshot.thread,
346
+ name,
347
+ updatedAt: timestamp
348
+ }
349
+ });
350
+ mergeThreadSummary(threadId, {
351
+ name,
352
+ updatedAt: timestamp
353
+ });
354
+ return true;
355
+ }
356
+
357
+ function applyThreadStatusUpdate({ threadId, cwd, status, timestamp = nowIso() }) {
358
+ if (!threadId) {
359
+ return false;
360
+ }
361
+
362
+ const snapshot = ensureLiveSelectedSnapshot(threadId, cwd);
363
+ commitLiveSelectedSnapshot({
364
+ ...snapshot,
365
+ thread: {
366
+ ...snapshot.thread,
367
+ status: status || snapshot.thread?.status || null,
368
+ updatedAt: timestamp
369
+ }
370
+ });
371
+ mergeThreadSummary(threadId, {
372
+ status: status || null,
373
+ updatedAt: timestamp
374
+ });
375
+ return true;
376
+ }
377
+
378
+ function applyWatcherNotification(message, { threadId, cwd }) {
379
+ const params = message.params || {};
380
+ const eventThreadId = params.threadId || threadId;
381
+ const eventTurnId = params.turnId || params.turn?.id || null;
382
+ const timestamp = nowIso();
383
+
384
+ if (eventThreadId !== liveState.selectedThreadId) {
385
+ return false;
386
+ }
387
+
388
+ switch (message.method) {
389
+ case "turn/started":
390
+ return applyTurnLifecycleUpdate({
391
+ cwd,
392
+ threadId: eventThreadId,
393
+ timestamp,
394
+ turn: params.turn || { id: eventTurnId, status: "inProgress" }
395
+ });
396
+ case "turn/completed":
397
+ return applyTurnLifecycleUpdate({
398
+ cwd,
399
+ threadId: eventThreadId,
400
+ timestamp,
401
+ turn: params.turn || { id: eventTurnId, status: "completed" }
402
+ });
403
+ case "item/started":
404
+ case "item/completed":
405
+ return applyTranscriptItemUpdate({
406
+ cwd,
407
+ item: params.item,
408
+ threadId: eventThreadId,
409
+ timestamp,
410
+ turnId: eventTurnId
411
+ });
412
+ case "item/agentMessage/delta":
413
+ return appendToTranscriptItem({
414
+ appendText: extractNotificationDelta(params),
415
+ cwd,
416
+ defaults: {
417
+ kind: "message",
418
+ phase: null,
419
+ role: "assistant"
420
+ },
421
+ itemId: params.itemId || null,
422
+ threadId: eventThreadId,
423
+ timestamp,
424
+ turnId: eventTurnId
425
+ });
426
+ case "item/plan/delta":
427
+ return appendToTranscriptItem({
428
+ appendText: extractNotificationDelta(params),
429
+ cwd,
430
+ defaults: {
431
+ kind: "plan",
432
+ phase: null,
433
+ role: "system"
434
+ },
435
+ itemId: params.itemId || null,
436
+ threadId: eventThreadId,
437
+ timestamp,
438
+ turnId: eventTurnId
439
+ });
440
+ case "item/reasoning/summaryTextDelta":
441
+ case "item/reasoning/textDelta":
442
+ return appendToTranscriptItem({
443
+ appendText: extractNotificationDelta(params),
444
+ cwd,
445
+ defaults: {
446
+ kind: "reasoning",
447
+ phase: null,
448
+ role: "system"
449
+ },
450
+ itemId: params.itemId || null,
451
+ threadId: eventThreadId,
452
+ timestamp,
453
+ turnId: eventTurnId
454
+ });
455
+ case "item/reasoning/summaryPartAdded":
456
+ return appendToTranscriptItem({
457
+ appendText: "\n\n",
458
+ cwd,
459
+ defaults: {
460
+ kind: "reasoning",
461
+ phase: null,
462
+ role: "system"
463
+ },
464
+ itemId: params.itemId || null,
465
+ threadId: eventThreadId,
466
+ timestamp,
467
+ turnId: eventTurnId
468
+ });
469
+ case "item/commandExecution/outputDelta":
470
+ return appendCommandOutputDelta({
471
+ cwd,
472
+ delta: extractNotificationDelta(params),
473
+ itemId: params.itemId || null,
474
+ threadId: eventThreadId,
475
+ timestamp,
476
+ turnId: eventTurnId
477
+ });
478
+ case "item/fileChange/outputDelta":
479
+ return appendFileChangeOutputDelta({
480
+ cwd,
481
+ delta: extractNotificationDelta(params),
482
+ itemId: params.itemId || null,
483
+ threadId: eventThreadId,
484
+ timestamp,
485
+ turnId: eventTurnId
486
+ });
487
+ case "turn/plan/updated":
488
+ return applyTurnPlanUpdate({
489
+ cwd,
490
+ explanation: params.explanation || null,
491
+ plan: params.plan || null,
492
+ threadId: eventThreadId,
493
+ timestamp,
494
+ turnId: eventTurnId
495
+ });
496
+ case "thread/tokenUsage/updated":
497
+ return applyThreadTokenUsageUpdate({
498
+ cwd,
499
+ threadId: eventThreadId,
500
+ timestamp,
501
+ tokenUsage: params.tokenUsage || params.usage || params.thread?.tokenUsage || null
502
+ });
503
+ case "thread/compacted":
504
+ return applyTranscriptItemUpdate({
505
+ cwd,
506
+ item: {
507
+ id: params.itemId || `thread-compacted-${eventTurnId || timestamp}`,
508
+ type: "contextCompaction"
509
+ },
510
+ threadId: eventThreadId,
511
+ timestamp,
512
+ turnId: eventTurnId
513
+ });
514
+ case "thread/name/updated":
515
+ return applyThreadNameUpdate({
516
+ cwd,
517
+ name: params.name || params.thread?.name || null,
518
+ threadId: eventThreadId,
519
+ timestamp
520
+ });
521
+ case "thread/status/changed":
522
+ return applyThreadStatusUpdate({
523
+ cwd,
524
+ status: params.status || params.thread?.status || null,
525
+ threadId: eventThreadId,
526
+ timestamp
527
+ });
528
+ default:
529
+ return false;
530
+ }
531
+ }
532
+
533
+ return {
534
+ appendCommandOutputDelta,
535
+ appendFileChangeOutputDelta,
536
+ appendToTranscriptItem,
537
+ applyThreadNameUpdate,
538
+ applyThreadStatusUpdate,
539
+ applyThreadTokenUsageUpdate,
540
+ applyTranscriptItemUpdate,
541
+ applyTurnLifecycleUpdate,
542
+ applyTurnPlanUpdate,
543
+ applyWatcherNotification,
544
+ commitLiveSelectedSnapshot,
545
+ ensureLiveSelectedSnapshot,
546
+ mergeThreadSummary,
547
+ upsertTranscriptEntry
548
+ };
549
+ }
@@ -0,0 +1,39 @@
1
+ const KIB = 1024;
2
+
3
+ export const MOBILE_NETWORK_PROFILES = {
4
+ "weak-mobile": {
5
+ downstreamBytesPerSecond: 48 * KIB,
6
+ dropSseAfterMs: null,
7
+ jitterMs: 40,
8
+ requestDelayMs: 180,
9
+ responseDelayMs: 180,
10
+ upstreamBytesPerSecond: 32 * KIB
11
+ },
12
+ "weak-mobile-reconnect": {
13
+ downstreamBytesPerSecond: 48 * KIB,
14
+ dropSseAfterMs: 3200,
15
+ jitterMs: 50,
16
+ requestDelayMs: 220,
17
+ responseDelayMs: 220,
18
+ upstreamBytesPerSecond: 32 * KIB
19
+ }
20
+ };
21
+
22
+ export function resolveMobileNetworkProfile(name = "") {
23
+ const key = String(name || "").trim().toLowerCase();
24
+ if (!key) {
25
+ return null;
26
+ }
27
+ return MOBILE_NETWORK_PROFILES[key] ? { name: key, ...MOBILE_NETWORK_PROFILES[key] } : null;
28
+ }
29
+
30
+ export function withNetworkJitter(baseMs, jitterMs = 0) {
31
+ const base = Math.max(0, Number(baseMs) || 0);
32
+ const jitter = Math.max(0, Number(jitterMs) || 0);
33
+ if (jitter === 0) {
34
+ return base;
35
+ }
36
+ const offset = Math.round((Math.random() * (jitter * 2)) - jitter);
37
+ return Math.max(0, base + offset);
38
+ }
39
+
@@ -0,0 +1,62 @@
1
+ export function createMockCodexAdapter(store) {
2
+ let heartbeat = null;
3
+
4
+ function scheduleFollowUp(command) {
5
+ if (command.type === "send_text") {
6
+ setTimeout(() => {
7
+ store.applyCommand({
8
+ type: "simulate_assistant_turn",
9
+ source: "mock-adapter",
10
+ text: `Mock adapter reply: "${command.text.trim()}" was accepted. A real Codex adapter would now focus the app, submit the text, and stream the resulting turn back into the companion.`
11
+ });
12
+ }, 700);
13
+ return;
14
+ }
15
+
16
+ if (command.type === "approve") {
17
+ setTimeout(() => {
18
+ store.applyCommand({
19
+ type: "simulate_assistant_turn",
20
+ source: "mock-adapter",
21
+ text: "Approval path validated. The next native spike can wire this action to a real gated tool call or session step."
22
+ });
23
+ }, 500);
24
+ return;
25
+ }
26
+
27
+ if (command.type === "set_strategy") {
28
+ setTimeout(() => {
29
+ store.applyCommand({
30
+ type: "simulate_assistant_turn",
31
+ source: "mock-adapter",
32
+ text: "Capability ladder updated. This is where a real adapter would announce what semantic read and write paths are currently healthy."
33
+ });
34
+ }, 350);
35
+ }
36
+ }
37
+
38
+ function start() {
39
+ heartbeat = setInterval(() => {
40
+ const snapshot = store.getState();
41
+ const mode = snapshot.session.strategy.label;
42
+ store.publishTranscript(
43
+ "system",
44
+ "status",
45
+ `Heartbeat: host is still live in ${mode}. Transport is ${snapshot.session.transportLabel}.`
46
+ );
47
+ }, 45000);
48
+ }
49
+
50
+ function stop() {
51
+ if (heartbeat) {
52
+ clearInterval(heartbeat);
53
+ heartbeat = null;
54
+ }
55
+ }
56
+
57
+ return {
58
+ start,
59
+ stop,
60
+ scheduleFollowUp
61
+ };
62
+ }