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,453 @@
|
|
|
1
|
+
export function createCompanionStateService({
|
|
2
|
+
ADVISORY_PARTICIPANT_IDS = [],
|
|
3
|
+
COMPANION_WAKEUP_LIMIT,
|
|
4
|
+
COMPANION_WAKEUP_SNOOZE_MS,
|
|
5
|
+
COMPANION_WAKEUP_STALE_MS,
|
|
6
|
+
COMPANION_WAKEUP_VISIBLE_MS,
|
|
7
|
+
buildParticipant,
|
|
8
|
+
liveState,
|
|
9
|
+
nowIso
|
|
10
|
+
}) {
|
|
11
|
+
function defaultAdvisoryState(id) {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
lastWakeAt: null,
|
|
15
|
+
metaLabel: "dormant",
|
|
16
|
+
state: "dormant",
|
|
17
|
+
wakeKey: null,
|
|
18
|
+
wakeKind: null
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function advisoryMetaLabel(state, wakeKind = null) {
|
|
23
|
+
if (state === "ready") {
|
|
24
|
+
return wakeKind === "summary" ? "summary ready" : "review ready";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return "dormant";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isCompanionWakeupVisible(notice) {
|
|
31
|
+
return notice?.status === "ready";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeCompanionThreadState(state, { now = Date.now() } = {}) {
|
|
35
|
+
const baseState = state || {
|
|
36
|
+
advisories: Object.fromEntries(ADVISORY_PARTICIPANT_IDS.map((advisorId) => [advisorId, defaultAdvisoryState(advisorId)])),
|
|
37
|
+
wakeups: []
|
|
38
|
+
};
|
|
39
|
+
const currentWakeups = Array.isArray(baseState.wakeups) ? baseState.wakeups : [];
|
|
40
|
+
let changed = !Array.isArray(baseState.wakeups);
|
|
41
|
+
const nextWakeups = [];
|
|
42
|
+
|
|
43
|
+
for (const rawNotice of currentWakeups) {
|
|
44
|
+
const advisorId = ADVISORY_PARTICIPANT_IDS.includes(rawNotice?.advisorId) ? rawNotice.advisorId : null;
|
|
45
|
+
const key = String(rawNotice?.key || "").trim();
|
|
46
|
+
const atMs = new Date(rawNotice?.timestamp || 0).getTime();
|
|
47
|
+
if (!advisorId || !key || !atMs || now - atMs > COMPANION_WAKEUP_STALE_MS) {
|
|
48
|
+
changed = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let nextNotice = {
|
|
53
|
+
...rawNotice,
|
|
54
|
+
advisorId,
|
|
55
|
+
key,
|
|
56
|
+
status: rawNotice?.status || "ready"
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (nextNotice.status === "snoozed") {
|
|
60
|
+
const snoozeUntilMs = new Date(nextNotice.snoozeUntil || 0).getTime();
|
|
61
|
+
if (!snoozeUntilMs || now >= snoozeUntilMs) {
|
|
62
|
+
nextNotice = {
|
|
63
|
+
...nextNotice,
|
|
64
|
+
snoozeUntil: null,
|
|
65
|
+
status: "ready",
|
|
66
|
+
timestamp: nowIso()
|
|
67
|
+
};
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const visibleAtMs = new Date(nextNotice.timestamp || 0).getTime();
|
|
73
|
+
if (nextNotice.status === "ready" && visibleAtMs && now - visibleAtMs > COMPANION_WAKEUP_VISIBLE_MS) {
|
|
74
|
+
nextNotice = {
|
|
75
|
+
...nextNotice,
|
|
76
|
+
expiredAt: nextNotice.expiredAt || nowIso(),
|
|
77
|
+
status: "expired"
|
|
78
|
+
};
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
nextWakeups.push(nextNotice);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
nextWakeups.sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime());
|
|
86
|
+
if (nextWakeups.length > COMPANION_WAKEUP_LIMIT) {
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const limitedWakeups = nextWakeups.slice(0, COMPANION_WAKEUP_LIMIT);
|
|
91
|
+
const nextAdvisories = {};
|
|
92
|
+
for (const advisorId of ADVISORY_PARTICIPANT_IDS) {
|
|
93
|
+
const current = baseState.advisories?.[advisorId] || defaultAdvisoryState(advisorId);
|
|
94
|
+
const readyWakeup = limitedWakeups.find((notice) => notice.advisorId === advisorId && isCompanionWakeupVisible(notice));
|
|
95
|
+
if (readyWakeup) {
|
|
96
|
+
nextAdvisories[advisorId] = {
|
|
97
|
+
...current,
|
|
98
|
+
lastWakeAt: readyWakeup.timestamp,
|
|
99
|
+
metaLabel: advisoryMetaLabel("ready", readyWakeup.wakeKind),
|
|
100
|
+
state: "ready",
|
|
101
|
+
wakeKey: readyWakeup.key,
|
|
102
|
+
wakeKind: readyWakeup.wakeKind || null
|
|
103
|
+
};
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
nextAdvisories[advisorId] = {
|
|
108
|
+
...current,
|
|
109
|
+
metaLabel: "dormant",
|
|
110
|
+
state: "dormant",
|
|
111
|
+
wakeKey: null,
|
|
112
|
+
wakeKind: null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const nextState = {
|
|
117
|
+
advisories: nextAdvisories,
|
|
118
|
+
wakeups: limitedWakeups
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (!changed) {
|
|
122
|
+
changed = JSON.stringify({
|
|
123
|
+
advisories: baseState.advisories || {},
|
|
124
|
+
wakeups: currentWakeups
|
|
125
|
+
}) !== JSON.stringify(nextState);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
changed,
|
|
130
|
+
nextState
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureThreadCompanionState(threadId) {
|
|
135
|
+
const id = String(threadId || "").trim();
|
|
136
|
+
if (!id) {
|
|
137
|
+
return {
|
|
138
|
+
advisories: Object.fromEntries(ADVISORY_PARTICIPANT_IDS.map((advisorId) => [advisorId, defaultAdvisoryState(advisorId)])),
|
|
139
|
+
wakeups: []
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const existing = liveState.companionByThreadId[id];
|
|
144
|
+
if (existing) {
|
|
145
|
+
for (const advisorId of ADVISORY_PARTICIPANT_IDS) {
|
|
146
|
+
if (!existing.advisories[advisorId]) {
|
|
147
|
+
existing.advisories[advisorId] = defaultAdvisoryState(advisorId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
existing.wakeups = Array.isArray(existing.wakeups) ? existing.wakeups : [];
|
|
151
|
+
return existing;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const created = {
|
|
155
|
+
advisories: Object.fromEntries(ADVISORY_PARTICIPANT_IDS.map((advisorId) => [advisorId, defaultAdvisoryState(advisorId)])),
|
|
156
|
+
wakeups: []
|
|
157
|
+
};
|
|
158
|
+
liveState.companionByThreadId = {
|
|
159
|
+
...liveState.companionByThreadId,
|
|
160
|
+
[id]: created
|
|
161
|
+
};
|
|
162
|
+
return created;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setThreadCompanionState(threadId, state) {
|
|
166
|
+
const id = String(threadId || "").trim();
|
|
167
|
+
if (!id) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
liveState.companionByThreadId = {
|
|
172
|
+
...liveState.companionByThreadId,
|
|
173
|
+
[id]: state
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pruneCompanionWakeupsForThread(threadId, { now = Date.now() } = {}) {
|
|
178
|
+
const id = String(threadId || "").trim();
|
|
179
|
+
if (!id) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const state = liveState.companionByThreadId[id];
|
|
184
|
+
if (!state) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { changed, nextState } = normalizeCompanionThreadState(state, { now });
|
|
189
|
+
if (changed) {
|
|
190
|
+
setThreadCompanionState(id, nextState);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return changed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function pruneAllCompanionWakeups({ now = Date.now() } = {}) {
|
|
197
|
+
let changed = false;
|
|
198
|
+
for (const threadId of Object.keys(liveState.companionByThreadId || {})) {
|
|
199
|
+
if (pruneCompanionWakeupsForThread(threadId, { now })) {
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return changed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resetCompanionWakeups(threadId, { preserveLastWake = true } = {}) {
|
|
207
|
+
const id = String(threadId || "").trim();
|
|
208
|
+
if (!id) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const state = ensureThreadCompanionState(id);
|
|
213
|
+
const hadWakeups = Boolean(state.wakeups.length);
|
|
214
|
+
let changed = hadWakeups;
|
|
215
|
+
const advisories = { ...state.advisories };
|
|
216
|
+
|
|
217
|
+
for (const advisorId of ADVISORY_PARTICIPANT_IDS) {
|
|
218
|
+
const current = advisories[advisorId] || defaultAdvisoryState(advisorId);
|
|
219
|
+
if (current.state !== "dormant" || current.metaLabel !== "dormant" || current.wakeKey || current.wakeKind) {
|
|
220
|
+
advisories[advisorId] = {
|
|
221
|
+
...current,
|
|
222
|
+
metaLabel: "dormant",
|
|
223
|
+
state: "dormant",
|
|
224
|
+
wakeKey: null,
|
|
225
|
+
wakeKind: null,
|
|
226
|
+
...(preserveLastWake ? {} : { lastWakeAt: null })
|
|
227
|
+
};
|
|
228
|
+
changed = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!changed) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setThreadCompanionState(id, {
|
|
237
|
+
advisories,
|
|
238
|
+
wakeups: []
|
|
239
|
+
});
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function queueCompanionWakeup({
|
|
244
|
+
allowDuringPending = false,
|
|
245
|
+
advisorId,
|
|
246
|
+
text,
|
|
247
|
+
threadId,
|
|
248
|
+
timestamp = nowIso(),
|
|
249
|
+
turnId = null,
|
|
250
|
+
wakeKey,
|
|
251
|
+
wakeKind = "review"
|
|
252
|
+
} = {}) {
|
|
253
|
+
const id = String(threadId || "").trim();
|
|
254
|
+
const advisor = ADVISORY_PARTICIPANT_IDS.includes(advisorId) ? advisorId : null;
|
|
255
|
+
const body = String(text || "").trim();
|
|
256
|
+
const key = String(wakeKey || "").trim();
|
|
257
|
+
if (!id || !advisor || !body || !key) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const pending = liveState.pendingInteraction || null;
|
|
262
|
+
if (!allowDuringPending && pending?.threadId && pending.threadId === id) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pruneCompanionWakeupsForThread(id);
|
|
267
|
+
const state = ensureThreadCompanionState(id);
|
|
268
|
+
const existing = state.wakeups.find((notice) => notice.key === key);
|
|
269
|
+
if (existing) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const nextWakeups = [
|
|
274
|
+
{
|
|
275
|
+
advisorId: advisor,
|
|
276
|
+
id: `companion-${advisor}-${turnId || Date.now()}`,
|
|
277
|
+
key,
|
|
278
|
+
kind: "commentary",
|
|
279
|
+
note: "advisory wakeup",
|
|
280
|
+
role: "assistant",
|
|
281
|
+
status: "ready",
|
|
282
|
+
text: body,
|
|
283
|
+
timestamp,
|
|
284
|
+
turnId,
|
|
285
|
+
wakeKind
|
|
286
|
+
},
|
|
287
|
+
...state.wakeups.filter((notice) => notice.advisorId !== advisor)
|
|
288
|
+
];
|
|
289
|
+
const { nextState } = normalizeCompanionThreadState({
|
|
290
|
+
advisories: {
|
|
291
|
+
...state.advisories
|
|
292
|
+
},
|
|
293
|
+
wakeups: nextWakeups
|
|
294
|
+
});
|
|
295
|
+
setThreadCompanionState(id, nextState);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function companionActionMessage({ action, advisorId, snoozeUntil = null } = {}) {
|
|
300
|
+
const advisorLabel = advisorId === "gemini" ? "Gemini" : advisorId === "oracle" ? "Oracle" : "Advisor";
|
|
301
|
+
if (action === "summon") {
|
|
302
|
+
return advisorId === "oracle" ? "Oracle review ready." : "Gemini recap ready.";
|
|
303
|
+
}
|
|
304
|
+
if (action === "snooze") {
|
|
305
|
+
return `${advisorLabel} reminder snoozed until ${new Date(snoozeUntil || Date.now()).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}.`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return `${advisorLabel} reminder dismissed.`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function summonCompanionWakeup({ advisorId = "", threadId = null } = {}) {
|
|
312
|
+
const nextThreadId = threadId || liveState.selectedThreadId || liveState.selectedThreadSnapshot?.thread?.id || null;
|
|
313
|
+
if (!nextThreadId) {
|
|
314
|
+
throw new Error("Select a live session before waking an advisor.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const normalizedAdvisorId = String(advisorId || "").trim().toLowerCase();
|
|
318
|
+
if (!ADVISORY_PARTICIPANT_IDS.includes(normalizedAdvisorId)) {
|
|
319
|
+
throw new Error(`Unsupported advisor: ${advisorId}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const wakeKind = normalizedAdvisorId === "oracle" ? "review" : "summary";
|
|
323
|
+
const queued = queueCompanionWakeup({
|
|
324
|
+
advisorId: normalizedAdvisorId,
|
|
325
|
+
allowDuringPending: true,
|
|
326
|
+
text:
|
|
327
|
+
wakeKind === "review"
|
|
328
|
+
? "Review ready: Oracle can stage a quick risk review draft for this channel."
|
|
329
|
+
: "Summary ready: Gemini can stage a quick recap draft for this channel.",
|
|
330
|
+
threadId: nextThreadId,
|
|
331
|
+
turnId: null,
|
|
332
|
+
wakeKey: `manual-${normalizedAdvisorId}:${Date.now()}`,
|
|
333
|
+
wakeKind
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!queued) {
|
|
337
|
+
throw new Error("Could not stage the advisor wakeup.");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
action: "summon",
|
|
342
|
+
advisorId: normalizedAdvisorId,
|
|
343
|
+
message: companionActionMessage({ action: "summon", advisorId: normalizedAdvisorId }),
|
|
344
|
+
wakeKind
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function applyCompanionWakeupAction({ action, threadId, wakeKey } = {}) {
|
|
349
|
+
const id = String(threadId || "").trim();
|
|
350
|
+
const key = String(wakeKey || "").trim();
|
|
351
|
+
if (!id || !key) {
|
|
352
|
+
throw new Error("A live advisory notice is required.");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const normalizedAction = action === "snooze" ? "snooze" : action === "dismiss" ? "dismiss" : "";
|
|
356
|
+
if (!normalizedAction) {
|
|
357
|
+
throw new Error(`Unsupported companion action: ${action}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
pruneCompanionWakeupsForThread(id);
|
|
361
|
+
const state = ensureThreadCompanionState(id);
|
|
362
|
+
const existing = state.wakeups.find((notice) => notice.key === key);
|
|
363
|
+
if (!existing || !isCompanionWakeupVisible(existing)) {
|
|
364
|
+
throw new Error("That advisory notice is no longer active.");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const nextWakeup =
|
|
368
|
+
normalizedAction === "snooze"
|
|
369
|
+
? {
|
|
370
|
+
...existing,
|
|
371
|
+
snoozeUntil: new Date(Date.now() + COMPANION_WAKEUP_SNOOZE_MS).toISOString(),
|
|
372
|
+
status: "snoozed"
|
|
373
|
+
}
|
|
374
|
+
: {
|
|
375
|
+
...existing,
|
|
376
|
+
dismissedAt: nowIso(),
|
|
377
|
+
snoozeUntil: null,
|
|
378
|
+
status: "dismissed"
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const nextWakeups = state.wakeups.map((notice) => (notice.key === key ? nextWakeup : notice));
|
|
382
|
+
const { nextState } = normalizeCompanionThreadState({
|
|
383
|
+
advisories: {
|
|
384
|
+
...state.advisories
|
|
385
|
+
},
|
|
386
|
+
wakeups: nextWakeups
|
|
387
|
+
});
|
|
388
|
+
setThreadCompanionState(id, nextState);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
action: normalizedAction,
|
|
392
|
+
advisorId: existing.advisorId,
|
|
393
|
+
message: companionActionMessage({
|
|
394
|
+
action: normalizedAction,
|
|
395
|
+
advisorId: existing.advisorId,
|
|
396
|
+
snoozeUntil: nextWakeup.snoozeUntil || null
|
|
397
|
+
}),
|
|
398
|
+
snoozeUntil: nextWakeup.snoozeUntil || null
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildSelectedCompanionState(threadId = liveState.selectedThreadId || null) {
|
|
403
|
+
const id = String(threadId || "").trim();
|
|
404
|
+
if (!id) {
|
|
405
|
+
return {
|
|
406
|
+
advisories: ADVISORY_PARTICIPANT_IDS.map((advisorId) => defaultAdvisoryState(advisorId)),
|
|
407
|
+
wakeups: []
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
pruneCompanionWakeupsForThread(id);
|
|
412
|
+
const state = liveState.companionByThreadId[id] || ensureThreadCompanionState(id);
|
|
413
|
+
const visibleWakeups = (state.wakeups || []).filter((notice) => isCompanionWakeupVisible(notice));
|
|
414
|
+
return {
|
|
415
|
+
advisories: ADVISORY_PARTICIPANT_IDS.map((advisorId) => ({
|
|
416
|
+
...(state.advisories[advisorId] || defaultAdvisoryState(advisorId)),
|
|
417
|
+
id: advisorId,
|
|
418
|
+
label: advisorId
|
|
419
|
+
})),
|
|
420
|
+
wakeups: visibleWakeups.map((notice) => ({
|
|
421
|
+
...notice,
|
|
422
|
+
actions: [
|
|
423
|
+
{ action: "snooze", busyLabel: "Later...", label: "Later" },
|
|
424
|
+
{ action: "dismiss", busyLabel: "Dismissing...", label: "Dismiss" }
|
|
425
|
+
],
|
|
426
|
+
lane: notice.advisorId,
|
|
427
|
+
origin: notice.advisorId,
|
|
428
|
+
participant: buildParticipant(notice.advisorId, {
|
|
429
|
+
metaLabel: advisoryMetaLabel("ready", notice.wakeKind),
|
|
430
|
+
state: "ready",
|
|
431
|
+
wakeKind: notice.wakeKind
|
|
432
|
+
})
|
|
433
|
+
}))
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
advisoryMetaLabel,
|
|
439
|
+
applyCompanionWakeupAction,
|
|
440
|
+
buildSelectedCompanionState,
|
|
441
|
+
companionActionMessage,
|
|
442
|
+
defaultAdvisoryState,
|
|
443
|
+
ensureThreadCompanionState,
|
|
444
|
+
isCompanionWakeupVisible,
|
|
445
|
+
normalizeCompanionThreadState,
|
|
446
|
+
pruneAllCompanionWakeups,
|
|
447
|
+
pruneCompanionWakeupsForThread,
|
|
448
|
+
queueCompanionWakeup,
|
|
449
|
+
resetCompanionWakeups,
|
|
450
|
+
setThreadCompanionState,
|
|
451
|
+
summonCompanionWakeup
|
|
452
|
+
};
|
|
453
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
export function createControlLeaseService({
|
|
2
|
+
broadcast = () => {},
|
|
3
|
+
buildLivePayload = () => ({}),
|
|
4
|
+
clearControlLeaseState,
|
|
5
|
+
defaultTtlMs,
|
|
6
|
+
ensureRemoteControlLeaseState,
|
|
7
|
+
getControlLeaseForThreadState,
|
|
8
|
+
liveState,
|
|
9
|
+
nowMs = () => Date.now(),
|
|
10
|
+
recordControlEvent = () => {},
|
|
11
|
+
renewControlLeaseState,
|
|
12
|
+
setControlLeaseState,
|
|
13
|
+
setTimeoutFn = setTimeout,
|
|
14
|
+
clearTimeoutFn = clearTimeout
|
|
15
|
+
} = {}) {
|
|
16
|
+
let controlLeaseTimer = null;
|
|
17
|
+
|
|
18
|
+
function clearControlLease({
|
|
19
|
+
actor = "system",
|
|
20
|
+
actorClientId = null,
|
|
21
|
+
broadcastUpdate = false,
|
|
22
|
+
cause = "manual",
|
|
23
|
+
recordEvent = false,
|
|
24
|
+
threadId = null
|
|
25
|
+
} = {}) {
|
|
26
|
+
if (threadId && liveState.controlLease?.threadId && liveState.controlLease.threadId !== threadId) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (controlLeaseTimer) {
|
|
31
|
+
clearTimeoutFn(controlLeaseTimer);
|
|
32
|
+
controlLeaseTimer = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!liveState.controlLease) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const previousLease = liveState.controlLease;
|
|
40
|
+
liveState.controlLease = clearControlLeaseState(liveState.controlLease, { threadId, now: nowMs() });
|
|
41
|
+
if (liveState.controlLease) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (recordEvent) {
|
|
46
|
+
recordControlEvent({
|
|
47
|
+
action: "release",
|
|
48
|
+
actor,
|
|
49
|
+
actorClientId,
|
|
50
|
+
cause,
|
|
51
|
+
owner: previousLease.owner || null,
|
|
52
|
+
ownerClientId: previousLease.ownerClientId || null,
|
|
53
|
+
ownerLabel: previousLease.ownerLabel || null,
|
|
54
|
+
reason: previousLease.reason || null,
|
|
55
|
+
source: previousLease.source || null,
|
|
56
|
+
threadId: previousLease.threadId || threadId || null
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (broadcastUpdate) {
|
|
61
|
+
broadcast("live", buildLivePayload());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function scheduleControlLeaseExpiry() {
|
|
66
|
+
if (controlLeaseTimer) {
|
|
67
|
+
clearTimeoutFn(controlLeaseTimer);
|
|
68
|
+
controlLeaseTimer = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lease = liveState.controlLease;
|
|
72
|
+
if (!lease?.expiresAt) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const delay = new Date(lease.expiresAt).getTime() - nowMs();
|
|
77
|
+
if (delay <= 0) {
|
|
78
|
+
clearControlLease({
|
|
79
|
+
actor: "system",
|
|
80
|
+
broadcastUpdate: true,
|
|
81
|
+
cause: "expired",
|
|
82
|
+
recordEvent: true,
|
|
83
|
+
threadId: lease.threadId
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
controlLeaseTimer = setTimeoutFn(() => {
|
|
89
|
+
controlLeaseTimer = null;
|
|
90
|
+
if (liveState.controlLease?.threadId === lease.threadId && liveState.controlLease?.expiresAt === lease.expiresAt) {
|
|
91
|
+
clearControlLease({
|
|
92
|
+
actor: "system",
|
|
93
|
+
broadcastUpdate: true,
|
|
94
|
+
cause: "expired",
|
|
95
|
+
recordEvent: true,
|
|
96
|
+
threadId: lease.threadId
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}, delay);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setControlLease({
|
|
103
|
+
clientId = null,
|
|
104
|
+
owner = "remote",
|
|
105
|
+
reason = "compose",
|
|
106
|
+
source = "remote",
|
|
107
|
+
threadId = liveState.selectedThreadId || null,
|
|
108
|
+
ttlMs
|
|
109
|
+
} = {}) {
|
|
110
|
+
liveState.controlLease = setControlLeaseState({
|
|
111
|
+
clientId,
|
|
112
|
+
now: nowMs(),
|
|
113
|
+
owner,
|
|
114
|
+
reason,
|
|
115
|
+
source,
|
|
116
|
+
threadId,
|
|
117
|
+
ttlMs: ttlMs ?? defaultTtlMs
|
|
118
|
+
});
|
|
119
|
+
scheduleControlLeaseExpiry();
|
|
120
|
+
return liveState.controlLease;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getControlLeaseForThread(threadId = null) {
|
|
124
|
+
const lease = getControlLeaseForThreadState(liveState.controlLease, threadId, { now: nowMs() });
|
|
125
|
+
if (!lease && liveState.controlLease?.expiresAt && new Date(liveState.controlLease.expiresAt).getTime() <= nowMs()) {
|
|
126
|
+
clearControlLease({ threadId: liveState.controlLease.threadId });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lease;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renewControlLease({
|
|
134
|
+
clientId = null,
|
|
135
|
+
owner = null,
|
|
136
|
+
reason = null,
|
|
137
|
+
source = null,
|
|
138
|
+
threadId = liveState.selectedThreadId || null,
|
|
139
|
+
ttlMs
|
|
140
|
+
} = {}) {
|
|
141
|
+
liveState.controlLease = renewControlLeaseState({
|
|
142
|
+
clientId,
|
|
143
|
+
lease: getControlLeaseForThread(threadId),
|
|
144
|
+
now: nowMs(),
|
|
145
|
+
owner,
|
|
146
|
+
reason,
|
|
147
|
+
source,
|
|
148
|
+
threadId,
|
|
149
|
+
ttlMs: ttlMs ?? defaultTtlMs
|
|
150
|
+
});
|
|
151
|
+
scheduleControlLeaseExpiry();
|
|
152
|
+
return liveState.controlLease;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getControlLeaseForSelectedThread() {
|
|
156
|
+
return getControlLeaseForThread(liveState.selectedThreadId || null);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureRemoteControlLease(threadId, source = "remote", clientId = null, ttlMs) {
|
|
160
|
+
liveState.controlLease = ensureRemoteControlLeaseState({
|
|
161
|
+
clientId,
|
|
162
|
+
lease: liveState.controlLease,
|
|
163
|
+
now: nowMs(),
|
|
164
|
+
source,
|
|
165
|
+
threadId,
|
|
166
|
+
ttlMs: ttlMs ?? defaultTtlMs
|
|
167
|
+
});
|
|
168
|
+
scheduleControlLeaseExpiry();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
clearControlLease,
|
|
173
|
+
ensureRemoteControlLease,
|
|
174
|
+
getControlLeaseForSelectedThread,
|
|
175
|
+
getControlLeaseForThread,
|
|
176
|
+
renewControlLease,
|
|
177
|
+
scheduleControlLeaseExpiry,
|
|
178
|
+
setControlLease
|
|
179
|
+
};
|
|
180
|
+
}
|