@oar-sh/oar 0.9.20 → 0.9.22
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/CHANGELOG.md +325 -0
- package/README.md +13 -13
- package/bin/oar.js +2 -2
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +31 -2
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +16 -4
- package/server/claude-worker/claude-session-worker-main.mjs +18 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +75 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +18 -2
- package/server/cursor-worker/cursor-session-worker.mjs +21 -2
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +10 -0
- package/server/grok-worker/grok-session-worker.mjs +22 -2
- package/server/grok-worker/grok-turn-runner.mjs +4 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +19 -0
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +11 -2
- package/server/public/app/ask-user-view.js +24 -9
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +618 -72
- package/server/public/app/chat-actions-menu-state.mjs +141 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +309 -49
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +238 -0
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +88 -81
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +328 -58
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +38 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +251 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +67 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +47 -13
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +35 -8
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2603 -1767
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +33 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +92 -15
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +117 -12
- package/server/server-runtime.mjs +367 -170
- package/server/services/about-service.mjs +22 -53
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +84 -18
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +183 -31
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/build-id.mjs +128 -0
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +31 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +44 -10
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// What the relay does when its queue may hold work for a worker: hand rows to
|
|
4
|
+
// the workers that are ready (the socket service's notify), make sure every
|
|
5
|
+
// session with a pending row has a worker (the prime), and keep the prime's
|
|
6
|
+
// maintenance pass armed while rows wait. The wakes come from the queue's
|
|
7
|
+
// own writes (queue-wake-hook.mjs), from a retry time coming due, from gates
|
|
8
|
+
// that lift, and from the backstop poll.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The prime: every session that owns a pending row gets its worker started
|
|
12
|
+
* (rows in retry backoff included, so the worker is warm when the backoff
|
|
13
|
+
* matures; rows held at the usage limit are not listed until their reset).
|
|
14
|
+
*/
|
|
15
|
+
export function createPendingWorkerPrimer({
|
|
16
|
+
isEnabled = () => true,
|
|
17
|
+
listPendingOwnerSessionIds,
|
|
18
|
+
isKillBlocked = () => false,
|
|
19
|
+
// A session whose rows were failed instead of launched (a Claude API chat
|
|
20
|
+
// the key cannot serve): true to skip its launch.
|
|
21
|
+
failRowsWithoutLaunch = () => false,
|
|
22
|
+
ensureWorker,
|
|
23
|
+
// Rows were found: the maintenance pass keeps retrying until none is.
|
|
24
|
+
onRowsWaiting = () => {},
|
|
25
|
+
warn = () => {},
|
|
26
|
+
limit = 25,
|
|
27
|
+
} = {}) {
|
|
28
|
+
return async function prime(reason = 'pending-worker-prime') {
|
|
29
|
+
if (!isEnabled()) return 0;
|
|
30
|
+
const rows = listPendingOwnerSessionIds(limit) || [];
|
|
31
|
+
if (rows.length) onRowsWaiting();
|
|
32
|
+
let requested = 0;
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const sdkSessionId = String(row?.sdk_session_id || '').trim();
|
|
35
|
+
if (!sdkSessionId) continue;
|
|
36
|
+
if (isKillBlocked(sdkSessionId) === true) continue;
|
|
37
|
+
try {
|
|
38
|
+
// Per session: a throw here costs this session its launch, never the
|
|
39
|
+
// rest of the pass.
|
|
40
|
+
if (failRowsWithoutLaunch(sdkSessionId)) continue;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
warn(`WORKER PRIME skipped session=${sdkSessionId.slice(0, 8)} err=${error?.message || error}`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
requested += 1;
|
|
46
|
+
try {
|
|
47
|
+
const result = await ensureWorker(sdkSessionId);
|
|
48
|
+
if (!result?.ok) {
|
|
49
|
+
warn(`WORKER PRIME blocked session=${sdkSessionId.slice(0, 8)} reason=${String(result?.error || reason || 'pending-worker-prime')}`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
warn(`WORKER PRIME failed session=${sdkSessionId.slice(0, 8)} reason=${String(reason || 'pending-worker-prime')} err=${error?.message || error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return requested;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The wake. `wake(reason)` hands rows out at once, primes, and arms the
|
|
61
|
+
* maintenance pass; `wake(reason, { prime: false })` leaves the prime to that
|
|
62
|
+
* pass (a queue write: the send route launches its own worker, and a write
|
|
63
|
+
* burst must not run a prime per write). Primes never overlap: one asked for
|
|
64
|
+
* while one runs runs once after it. `primeFromBackstop` is the poll's share:
|
|
65
|
+
* a prime at most every `backstopPrimeGapMs`.
|
|
66
|
+
*/
|
|
67
|
+
export function createQueueWaker({
|
|
68
|
+
prime,
|
|
69
|
+
notify = () => {},
|
|
70
|
+
armPrime = () => {},
|
|
71
|
+
isReady = () => true,
|
|
72
|
+
backstopPrimeGapMs = 5_000,
|
|
73
|
+
now = () => Date.now(),
|
|
74
|
+
warn = () => {},
|
|
75
|
+
} = {}) {
|
|
76
|
+
let primeRunning = false;
|
|
77
|
+
let primeAgain = null;
|
|
78
|
+
let lastBackstopPrimeAt = -Infinity;
|
|
79
|
+
|
|
80
|
+
async function runPrime(reason) {
|
|
81
|
+
if (primeRunning) {
|
|
82
|
+
primeAgain = reason;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
primeRunning = true;
|
|
86
|
+
try {
|
|
87
|
+
let next = reason;
|
|
88
|
+
while (next) {
|
|
89
|
+
primeAgain = null;
|
|
90
|
+
try {
|
|
91
|
+
await prime(next);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
warn(`WORKER PRIME pass failed reason=${next} err=${error?.message || error}`);
|
|
94
|
+
}
|
|
95
|
+
next = primeAgain;
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
primeRunning = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function wake(reason = 'queue-wake', { prime: withPrime = true } = {}) {
|
|
103
|
+
if (!isReady()) return false;
|
|
104
|
+
try { armPrime(); } catch {}
|
|
105
|
+
try { notify(reason); } catch (error) {
|
|
106
|
+
warn(`queue notify failed reason=${reason} err=${error?.message || error}`);
|
|
107
|
+
}
|
|
108
|
+
if (withPrime) void runPrime(reason);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function primeFromBackstop(reason = 'backstop') {
|
|
113
|
+
if (!isReady()) return false;
|
|
114
|
+
try { armPrime(); } catch {}
|
|
115
|
+
const at = now();
|
|
116
|
+
if (at - lastBackstopPrimeAt < backstopPrimeGapMs) return false;
|
|
117
|
+
lastBackstopPrimeAt = at;
|
|
118
|
+
void runPrime(reason);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
wake,
|
|
124
|
+
primeFromBackstop,
|
|
125
|
+
status: () => ({ primeRunning, primeQueued: primeAgain !== null }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -108,6 +108,15 @@ export function createRelayAskService({
|
|
|
108
108
|
}
|
|
109
109
|
})();
|
|
110
110
|
const available = !problem;
|
|
111
|
+
// The turn an ask was made in (migration 0037); without the column asks
|
|
112
|
+
// still work and store none.
|
|
113
|
+
const hasQueueId = available && (() => {
|
|
114
|
+
try {
|
|
115
|
+
return db.prepare(`PRAGMA table_info(relay_asks)`).all().some((column) => column.name === 'queue_id');
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
111
120
|
|
|
112
121
|
const sql = available ? {
|
|
113
122
|
insert: db.prepare(`
|
|
@@ -116,6 +125,12 @@ export function createRelayAskService({
|
|
|
116
125
|
question, options_json, recommendation, detail, urgency, status, created_at, updated_at)
|
|
117
126
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
|
118
127
|
`),
|
|
128
|
+
insertWithQueue: hasQueueId ? db.prepare(`
|
|
129
|
+
INSERT INTO relay_asks
|
|
130
|
+
(id, relay_id, remote_id, conversation_id, conversation_title, created_by, creator_json,
|
|
131
|
+
question, options_json, recommendation, detail, urgency, status, created_at, updated_at, queue_id)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)
|
|
133
|
+
`) : null,
|
|
119
134
|
get: db.prepare(`SELECT * FROM relay_asks WHERE id = ?`),
|
|
120
135
|
getMirror: db.prepare(`SELECT * FROM relay_asks WHERE relay_id = ? AND remote_id = ?`),
|
|
121
136
|
// The Inbox: this relay's asks, pending and closed since.
|
|
@@ -131,6 +146,17 @@ export function createRelayAskService({
|
|
|
131
146
|
AND (status = 'pending' OR (status <> 'pending' AND updated_at >= ?))
|
|
132
147
|
ORDER BY created_at DESC, rowid DESC LIMIT ?
|
|
133
148
|
`),
|
|
149
|
+
// A session's own cards: the asks in it kept here, none it only asked
|
|
150
|
+
// elsewhere and no mirrors.
|
|
151
|
+
listIn: db.prepare(`
|
|
152
|
+
SELECT * FROM relay_asks
|
|
153
|
+
WHERE conversation_id = ? AND relay_id = 'self'
|
|
154
|
+
AND (status = 'pending' OR (status <> 'pending' AND updated_at >= ?))
|
|
155
|
+
ORDER BY created_at DESC, rowid DESC LIMIT ?
|
|
156
|
+
`),
|
|
157
|
+
// What carryIn and carryBy are about to move.
|
|
158
|
+
listCarryIn: db.prepare(`SELECT id FROM relay_asks WHERE relay_id = 'self' AND conversation_id = ? AND status = 'pending'`),
|
|
159
|
+
listCarryBy: db.prepare(`SELECT id FROM relay_asks WHERE created_by = ? AND status = 'pending'`),
|
|
134
160
|
countPendingHere: db.prepare(`SELECT COUNT(*) AS n FROM relay_asks WHERE relay_id = 'self' AND status = 'pending'`),
|
|
135
161
|
countPendingIn: db.prepare(`SELECT COUNT(*) AS n FROM relay_asks WHERE relay_id = 'self' AND conversation_id = ? AND status = 'pending'`),
|
|
136
162
|
countPendingBy: db.prepare(`SELECT COUNT(*) AS n FROM relay_asks WHERE created_by = ? AND status = 'pending'`),
|
|
@@ -228,6 +254,9 @@ export function createRelayAskService({
|
|
|
228
254
|
? { relay: creator.relayName || null, conversationId: creator.conversationId || null, title: creator.conversationTitle || null }
|
|
229
255
|
: (toText(row.created_by) ? { conversationId: row.created_by, title: toText(asker?.title) || null } : null),
|
|
230
256
|
onBehalf: creator ? true : (!!toText(row.created_by) && row.created_by !== row.conversation_id),
|
|
257
|
+
// The queue row of the turn that asked (null: on behalf, a paired
|
|
258
|
+
// relay's agent, or older); a mirror has none.
|
|
259
|
+
...(mirror ? {} : { queueId: row.queue_id || null }),
|
|
231
260
|
createdAt: row.created_at,
|
|
232
261
|
updatedAt: row.updated_at,
|
|
233
262
|
};
|
|
@@ -259,9 +288,10 @@ export function createRelayAskService({
|
|
|
259
288
|
/**
|
|
260
289
|
* Stores an ask about `conversationId` (a session of this relay). The asker
|
|
261
290
|
* is `createdBy` (a conversation here: the same one, or a lead asking for
|
|
262
|
-
* another) or `creator` (a paired relay's agent).
|
|
291
|
+
* another) or `creator` (a paired relay's agent). `queueId`: the queue row
|
|
292
|
+
* of the turn that asked, when it ran in this session. Returns the ask.
|
|
263
293
|
*/
|
|
264
|
-
function create({ conversationId, createdBy = null, creator = null, ...input } = {}) {
|
|
294
|
+
function create({ conversationId, createdBy = null, creator = null, queueId = null, ...input } = {}) {
|
|
265
295
|
requireAvailable();
|
|
266
296
|
const session = conversation(conversationId);
|
|
267
297
|
if (!isLive(session)) {
|
|
@@ -281,7 +311,7 @@ export function createRelayAskService({
|
|
|
281
311
|
const { question, options, recommendation, detail, urgency } = checked.value;
|
|
282
312
|
const id = newId();
|
|
283
313
|
const at = isoNow();
|
|
284
|
-
|
|
314
|
+
const values = [
|
|
285
315
|
id,
|
|
286
316
|
SELF,
|
|
287
317
|
null,
|
|
@@ -296,7 +326,9 @@ export function createRelayAskService({
|
|
|
296
326
|
urgency,
|
|
297
327
|
at,
|
|
298
328
|
at,
|
|
299
|
-
|
|
329
|
+
];
|
|
330
|
+
if (sql.insertWithQueue) sql.insertWithQueue.run(...values, clip(queueId, 200) || null);
|
|
331
|
+
else sql.insert.run(...values);
|
|
300
332
|
const row = sql.get.get(id);
|
|
301
333
|
const ask = shape(row);
|
|
302
334
|
log(`${id} in ${shortId(session.id)} by ${remoteCreator ? `relay ${shortId(remoteCreator.relayId)}` : shortId(asker)}${urgency === 'now' ? ' (now)' : ''}`);
|
|
@@ -335,6 +367,20 @@ export function createRelayAskService({
|
|
|
335
367
|
return [...rows.filter((row) => row.status === 'pending'), ...rows.filter((row) => row.status !== 'pending')].map(shape);
|
|
336
368
|
}
|
|
337
369
|
|
|
370
|
+
/**
|
|
371
|
+
* A session's cards: the asks in it kept on this relay (not those it only
|
|
372
|
+
* asked elsewhere, no mirrors), pending first, then those closed in the
|
|
373
|
+
* last answeredKeepMs.
|
|
374
|
+
*/
|
|
375
|
+
function listIn(conversationId) {
|
|
376
|
+
if (!available) return [];
|
|
377
|
+
const id = toText(conversationId);
|
|
378
|
+
if (!id) return [];
|
|
379
|
+
const since = new Date(now() - RELAY_ASK_LIMITS.answeredKeepMs).toISOString();
|
|
380
|
+
const rows = sql.listIn.all(id, since, RELAY_ASK_LIMITS.listMax);
|
|
381
|
+
return [...rows.filter((row) => row.status === 'pending'), ...rows.filter((row) => row.status !== 'pending')].map(shape);
|
|
382
|
+
}
|
|
383
|
+
|
|
338
384
|
/** Who is told the outcome of a row here: its session and the conversation that asked. */
|
|
339
385
|
function tell(row) {
|
|
340
386
|
const ask = shape(row);
|
|
@@ -429,7 +475,19 @@ export function createRelayAskService({
|
|
|
429
475
|
const from = toText(sourceId);
|
|
430
476
|
const to = toText(targetId);
|
|
431
477
|
if (!from || !to || from === to) return 0;
|
|
432
|
-
|
|
478
|
+
// The rows both updates touch, each once (a self-ask in and by the
|
|
479
|
+
// source is in both), so their cards follow to the successor.
|
|
480
|
+
const touched = new Set();
|
|
481
|
+
const moved = db.transaction(() => {
|
|
482
|
+
for (const row of sql.listCarryIn.all(from)) touched.add(row.id);
|
|
483
|
+
for (const row of sql.listCarryBy.all(from)) touched.add(row.id);
|
|
484
|
+
return sql.carryIn.run(to, from).changes + sql.carryBy.run(to, from).changes;
|
|
485
|
+
})();
|
|
486
|
+
for (const id of touched) {
|
|
487
|
+
const row = sql.get.get(id);
|
|
488
|
+
// A mirror's card lives on the paired relay: nothing to show here.
|
|
489
|
+
if (row && row.relay_id === SELF) changed(row);
|
|
490
|
+
}
|
|
433
491
|
if (moved) log(`conv=${shortId(from)} compacted into ${shortId(to)}: ${moved} question${moved === 1 ? '' : 's'} moved`);
|
|
434
492
|
return moved;
|
|
435
493
|
}
|
|
@@ -586,6 +644,7 @@ export function createRelayAskService({
|
|
|
586
644
|
get,
|
|
587
645
|
listHere,
|
|
588
646
|
listOf,
|
|
647
|
+
listIn,
|
|
589
648
|
pendingCount,
|
|
590
649
|
answer,
|
|
591
650
|
dismiss,
|
|
@@ -107,7 +107,7 @@ import {
|
|
|
107
107
|
relayEventQuietWindowEnd,
|
|
108
108
|
relayEventTypesTake,
|
|
109
109
|
} from '../../shared/relay-events.mjs';
|
|
110
|
-
import { REMOTE_RELAY_APPROVAL_SOURCE } from '../../shared/relay-asks.mjs';
|
|
110
|
+
import { REMOTE_RELAY_APPROVAL_SOURCE, isPersonOnlyQuestion } from '../../shared/relay-asks.mjs';
|
|
111
111
|
|
|
112
112
|
export const RELAY_EVENT_SUBSCRIPTIONS_SOCKET_EVENT = 'relay_event_subscriptions';
|
|
113
113
|
export const RELAY_EVENT_SELF_RELAY_ID = 'self';
|
|
@@ -1770,6 +1770,7 @@ export function createRelayEventService({
|
|
|
1770
1770
|
prompt: 'text',
|
|
1771
1771
|
questionId: 'text',
|
|
1772
1772
|
approvalCard: 'bool',
|
|
1773
|
+
personOnly: 'bool',
|
|
1773
1774
|
// question_answered
|
|
1774
1775
|
answer: 'text',
|
|
1775
1776
|
choice: 'text',
|
|
@@ -2316,6 +2317,8 @@ export function createRelayEventService({
|
|
|
2316
2317
|
options,
|
|
2317
2318
|
questionId: toText(question.id),
|
|
2318
2319
|
...(toText(context?.source) === REMOTE_RELAY_APPROVAL_SOURCE ? { approvalCard: true } : {}),
|
|
2320
|
+
// A Claude session's Safety check: the user's to answer.
|
|
2321
|
+
...(isPersonOnlyQuestion(context) ? { personOnly: true } : {}),
|
|
2319
2322
|
// A question that does not block (relay_tool ask).
|
|
2320
2323
|
...(toText(question.kind) === 'ask' ? { kind: 'ask' } : {}),
|
|
2321
2324
|
});
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
isMissingRelayEventRoute,
|
|
45
45
|
relayWithoutCrossRelayEventsText,
|
|
46
46
|
} from './relay-event-link-service.mjs';
|
|
47
|
-
import { RELAY_ASK_KIND, relayQuestionKind } from '../../shared/relay-asks.mjs';
|
|
47
|
+
import { RELAY_ASK_KIND, isPersonOnlyQuestion, relayQuestionKind } from '../../shared/relay-asks.mjs';
|
|
48
48
|
import { RELAY_ASK_REMOTE_PATHS, isMissingRelayAskRoute, relayWithoutAsksText } from './relay-ask-link-service.mjs';
|
|
49
49
|
|
|
50
50
|
// The one place a `relay_tool` tool call is decided and carried out (plan
|
|
@@ -147,6 +147,9 @@ const CATALOG_PROVIDER_LABELS = Object.freeze({
|
|
|
147
147
|
});
|
|
148
148
|
// Worker statuses that mean "working on something right now" (scope active).
|
|
149
149
|
const WORKING_WORKER_STATUSES = new Set(['starting', 'processing']);
|
|
150
|
+
// A worker process that is up; the one a session's `worker` field names when
|
|
151
|
+
// the registry holds more than one entry for its conversation.
|
|
152
|
+
const LIVE_WORKER_STATUSES = new Set(['starting', 'ready', 'processing']);
|
|
150
153
|
// Terminal turn failures are stored as ordinary assistant rows; these are the
|
|
151
154
|
// texts the relay writes for them (buildTerminalFailureTextForChat and the two
|
|
152
155
|
// recovery-limit paths in messages-routes / server-runtime). Each is the whole
|
|
@@ -194,6 +197,11 @@ function shortId(value) {
|
|
|
194
197
|
return toText(value).slice(0, 8) || '-';
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
/** A session item's `worker` field, left out when the relay has no worker registered for it. */
|
|
201
|
+
function workerField(worker) {
|
|
202
|
+
return worker ? { worker: { ...worker } } : {};
|
|
203
|
+
}
|
|
204
|
+
|
|
197
205
|
function nonNegativeInt(value) {
|
|
198
206
|
const number = Number(value);
|
|
199
207
|
return Number.isInteger(number) && number >= 0 ? number : 0;
|
|
@@ -457,11 +465,13 @@ function relayTarget(relay) {
|
|
|
457
465
|
/**
|
|
458
466
|
* The question card's view of a relay question: what the agent needs to answer
|
|
459
467
|
* it. Null for the remote's own remote-relay approval cards, which only its
|
|
460
|
-
* user may answer (the remote hides them from us as well)
|
|
468
|
+
* user may answer (the remote hides them from us as well), and for a Claude
|
|
469
|
+
* session's Safety check card, which only the user may answer.
|
|
461
470
|
*/
|
|
462
471
|
function mapPendingQuestion(question) {
|
|
463
472
|
if (!isPlainObject(question) || !toText(question.id)) return null;
|
|
464
473
|
if (isRemoteRelayApprovalQuestion(question)) return null;
|
|
474
|
+
if (isPersonOnlyQuestion(question.context)) return null;
|
|
465
475
|
const kind = relayQuestionKind(question);
|
|
466
476
|
if (kind === RELAY_ASK_KIND) {
|
|
467
477
|
// A question that does not block: the user's to answer, the session works on.
|
|
@@ -1494,17 +1504,42 @@ export function createRemoteRelayDispatcher({
|
|
|
1494
1504
|
};
|
|
1495
1505
|
}
|
|
1496
1506
|
|
|
1497
|
-
|
|
1507
|
+
/**
|
|
1508
|
+
* The target relay's session workers, from its /api/status: `working`, the
|
|
1509
|
+
* conversations whose worker is starting or running a turn, and
|
|
1510
|
+
* `workerFor(conversationId)`, that conversation's `worker` field (kind,
|
|
1511
|
+
* build id, and whether it runs other code than that relay, which the relay
|
|
1512
|
+
* judges against its own build id and recycles once idle), or null. A relay
|
|
1513
|
+
* that predates build ids gives null build ids and never `stale`; one that
|
|
1514
|
+
* cannot be read gives nothing.
|
|
1515
|
+
*/
|
|
1516
|
+
async function sessionWorkers(ctx) {
|
|
1517
|
+
const working = new Set();
|
|
1518
|
+
const byConversation = new Map();
|
|
1498
1519
|
try {
|
|
1499
1520
|
const status = await call(ctx, 'GET', '/api/status');
|
|
1500
1521
|
const workers = Array.isArray(status?.sessionWorker?.workers) ? status.sessionWorker.workers : [];
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
.
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1522
|
+
for (const worker of workers) {
|
|
1523
|
+
const conversationId = toText(worker?.conversationId);
|
|
1524
|
+
if (!conversationId) continue;
|
|
1525
|
+
const workerStatus = toText(worker?.status).toLowerCase();
|
|
1526
|
+
if (WORKING_WORKER_STATUSES.has(workerStatus)) working.add(conversationId);
|
|
1527
|
+
const known = byConversation.get(conversationId);
|
|
1528
|
+
if (known && (known.live || !LIVE_WORKER_STATUSES.has(workerStatus))) continue;
|
|
1529
|
+
byConversation.set(conversationId, {
|
|
1530
|
+
live: LIVE_WORKER_STATUSES.has(workerStatus),
|
|
1531
|
+
field: {
|
|
1532
|
+
kind: toText(worker?.kind) || null,
|
|
1533
|
+
buildId: toText(worker?.buildId) || null,
|
|
1534
|
+
stale: worker?.stale === true,
|
|
1535
|
+
},
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
} catch {}
|
|
1539
|
+
return {
|
|
1540
|
+
working,
|
|
1541
|
+
workerFor: (conversationId) => byConversation.get(toText(conversationId))?.field || null,
|
|
1542
|
+
};
|
|
1508
1543
|
}
|
|
1509
1544
|
|
|
1510
1545
|
/** The session group of a listed row: id, display name and the row's role. */
|
|
@@ -1521,7 +1556,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1521
1556
|
};
|
|
1522
1557
|
}
|
|
1523
1558
|
|
|
1524
|
-
function mapSession(row,
|
|
1559
|
+
function mapSession(row, workers, ctx = null, groups = null) {
|
|
1525
1560
|
const id = toText(row?.id);
|
|
1526
1561
|
const via = toText(row?.origin?.relayName);
|
|
1527
1562
|
// A session an agent of that relay created there, and from which session.
|
|
@@ -1531,7 +1566,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1531
1566
|
title: toText(row?.title),
|
|
1532
1567
|
provider: toText(row?.runtimeProviderType) || 'github',
|
|
1533
1568
|
model: toText(row?.runtimeProviderModel) || toText(row?.runtimeModel) || toText(row?.preferredModel) || null,
|
|
1534
|
-
active: row?.activeTurn === true || !!
|
|
1569
|
+
active: row?.activeTurn === true || !!workers?.working.has(id) || isBusyState(row?.sessionState),
|
|
1535
1570
|
...sessionActivityFields(row),
|
|
1536
1571
|
updatedAt: row?.updatedAt || null,
|
|
1537
1572
|
messageCount: nonNegativeInt(row?.messageCount),
|
|
@@ -1623,10 +1658,12 @@ export function createRemoteRelayDispatcher({
|
|
|
1623
1658
|
cursor = decodeCursor(args.cursor, ['beforeConversationId', 'beforeUpdatedAt', 'beforeGroupId']);
|
|
1624
1659
|
if (!cursor) throw invalidInput('cursor is not a nextCursor from list_sessions');
|
|
1625
1660
|
}
|
|
1626
|
-
|
|
1661
|
+
// The active scope filters on the workers, so it reads them first; the
|
|
1662
|
+
// others read them once the list is known, for the `worker` fields only.
|
|
1663
|
+
const workers = scope === 'active' ? await sessionWorkers(ctx) : null;
|
|
1627
1664
|
const filtering = !!query || scope === 'active';
|
|
1628
1665
|
const keep = (row) => (!query || toText(row?.title).toLowerCase().includes(query))
|
|
1629
|
-
&& (scope !== 'active' || row?.activeTurn === true ||
|
|
1666
|
+
&& (scope !== 'active' || row?.activeTurn === true || workers.working.has(toText(row?.id)) || isBusyState(row?.sessionState));
|
|
1630
1667
|
|
|
1631
1668
|
const sessions = [];
|
|
1632
1669
|
const seen = new Set();
|
|
@@ -1649,7 +1686,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1649
1686
|
// A group served again whole (its cursor row vanished) repeats rows.
|
|
1650
1687
|
if (seen.has(toText(row?.id))) continue;
|
|
1651
1688
|
seen.add(toText(row?.id));
|
|
1652
|
-
if (keep(row)) sessions.push(mapSession(row,
|
|
1689
|
+
if (keep(row)) sessions.push(mapSession(row, workers, ctx, groups));
|
|
1653
1690
|
if (sessions.length >= limit) break;
|
|
1654
1691
|
}
|
|
1655
1692
|
scanned += consumed;
|
|
@@ -1667,6 +1704,12 @@ export function createRemoteRelayDispatcher({
|
|
|
1667
1704
|
hasMore = consumed < rows.length || response?.pageInfo?.hasMore === true;
|
|
1668
1705
|
if (sessions.length >= limit || !hasMore || !rows.length) break;
|
|
1669
1706
|
}
|
|
1707
|
+
// Each session's worker process, while the relay has one registered for it.
|
|
1708
|
+
const workerInfo = workers || (sessions.length ? await sessionWorkers(ctx) : null);
|
|
1709
|
+
for (let index = 0; index < sessions.length; index += 1) {
|
|
1710
|
+
const worker = workerInfo?.workerFor(sessions[index].id);
|
|
1711
|
+
if (worker) sessions[index] = { ...sessions[index], ...workerField(worker) };
|
|
1712
|
+
}
|
|
1670
1713
|
|
|
1671
1714
|
const nextCursor = scope === 'all' && hasMore && cursor ? encodeCursor(cursor) : null;
|
|
1672
1715
|
const note = filtering && hasMore && sessions.length < limit && scope !== 'all'
|
|
@@ -1716,7 +1759,9 @@ export function createRemoteRelayDispatcher({
|
|
|
1716
1759
|
budget -= text.length;
|
|
1717
1760
|
const role = toText(row.role) || 'assistant';
|
|
1718
1761
|
const via = toText(row?.origin?.relayName);
|
|
1719
|
-
|
|
1762
|
+
// The newest lines, as the progress path reads them: the relay keeps the
|
|
1763
|
+
// newest per thread now, so the first ones are not the start anyway.
|
|
1764
|
+
const activities = includeActivity && role === 'assistant' ? activityTexts(row.activities, { fromEnd: true }) : [];
|
|
1720
1765
|
out.unshift({
|
|
1721
1766
|
id: toText(row.id),
|
|
1722
1767
|
role,
|
|
@@ -1749,7 +1794,12 @@ export function createRemoteRelayDispatcher({
|
|
|
1749
1794
|
query: { limit: args.last, beforeMessageId: before?.beforeMessageId, beforeTimestamp: before?.beforeTimestamp, groupMembers: 1 },
|
|
1750
1795
|
});
|
|
1751
1796
|
const conversationId = toText(conversation?.id) || args.session;
|
|
1752
|
-
|
|
1797
|
+
// Neither read rejects: a relay that cannot say leaves the fields out.
|
|
1798
|
+
const [pendingQuestions, workers] = await Promise.all([
|
|
1799
|
+
loadPendingQuestions(ctx, conversationId, { includeAsks: true }),
|
|
1800
|
+
sessionWorkers(ctx),
|
|
1801
|
+
]);
|
|
1802
|
+
const worker = workers.workerFor(conversationId);
|
|
1753
1803
|
const { messages, omitted } = shapeMessages(conversation?.messages, {
|
|
1754
1804
|
maxChars: args.max_chars,
|
|
1755
1805
|
includeActivity: args.include_activity,
|
|
@@ -1798,6 +1848,7 @@ export function createRemoteRelayDispatcher({
|
|
|
1798
1848
|
...(via ? { via } : {}),
|
|
1799
1849
|
...(conversation?.mayCreateSessions === true ? { mayCreateSessions: true } : {}),
|
|
1800
1850
|
...(conversation?.planOnly === true ? { planOnly: true } : {}),
|
|
1851
|
+
...workerField(worker),
|
|
1801
1852
|
...(group ? { group } : {}),
|
|
1802
1853
|
openUrl: remoteConversationUrl(ctx.relay.url, conversationId) || null,
|
|
1803
1854
|
},
|
|
@@ -2547,6 +2598,11 @@ export function createRemoteRelayDispatcher({
|
|
|
2547
2598
|
`Question ${args.question_id} was not found on relay "${ctx.relay.name}".`);
|
|
2548
2599
|
}
|
|
2549
2600
|
const session = toText(question.conversationId) || null;
|
|
2601
|
+
if (isPersonOnlyQuestion(question.context)) {
|
|
2602
|
+
throw new DispatchFailure(403, REMOTE_RELAY_ERROR_CODES.forbidden,
|
|
2603
|
+
`Question ${args.question_id} is a Safety check: Claude Code marks the call as person-only, so this card is the user's to answer, never an agent's. Leave it to the user.`,
|
|
2604
|
+
{ session });
|
|
2605
|
+
}
|
|
2550
2606
|
if (ctx.relay.local && session && isOwnConversation(ctx, session)) {
|
|
2551
2607
|
throw new DispatchFailure(400, REMOTE_RELAY_ERROR_CODES.ownSession,
|
|
2552
2608
|
`Question ${args.question_id} belongs to this conversation itself; an agent cannot answer its own question. It is the user's to answer.`,
|
|
@@ -3415,7 +3471,17 @@ export function createRemoteRelayDispatcher({
|
|
|
3415
3471
|
};
|
|
3416
3472
|
}
|
|
3417
3473
|
const session = askSession(ctx, args);
|
|
3418
|
-
const ask = relayAskCall(() => service.create({
|
|
3474
|
+
const ask = relayAskCall(() => service.create({
|
|
3475
|
+
conversationId: session,
|
|
3476
|
+
createdBy: ctx.conversationId,
|
|
3477
|
+
...askInput(args),
|
|
3478
|
+
// The queue id means "the turn that was live when the ask was made": a
|
|
3479
|
+
// background sub-agent asking after its parent turn ended may record
|
|
3480
|
+
// the user's next, unrelated turn (accepted; there is no worker
|
|
3481
|
+
// attribution). An ask on behalf of another session stores null: the
|
|
3482
|
+
// asking turn lives in another conversation.
|
|
3483
|
+
queueId: session === ctx.conversationId ? (ctx.caller?.processingRowId || null) : null,
|
|
3484
|
+
}));
|
|
3419
3485
|
const forOther = session !== ctx.conversationId;
|
|
3420
3486
|
return {
|
|
3421
3487
|
summary: `ask → ${ctx.relay.name}${forOther ? ` session ${shortId(session)}` : ''}: question ${ask.id} pending${ask.urgency === 'now' ? ' (now)' : ''}`,
|
|
@@ -83,7 +83,7 @@ export function resolveBootReportingWindowMs(env = process.env, log = () => {})
|
|
|
83
83
|
export const UNREPORTED_BACKGROUND_TASK_ID = 'unreported-background-work';
|
|
84
84
|
// It stays until the worker reports, and a worker on older code never does
|
|
85
85
|
// for a task that prints nothing: the way out is the session's Kill session.
|
|
86
|
-
export const UNREPORTED_BACKGROUND_TASK_DESCRIPTION = 'background work, details not reported since the relay restarted (Kill session in its
|
|
86
|
+
export const UNREPORTED_BACKGROUND_TASK_DESCRIPTION = 'background work, details not reported since the relay restarted (Kill session in its ︙ menu ends it)';
|
|
87
87
|
// A `running` background run row that its worker's task set does not list
|
|
88
88
|
// and nothing has updated for this long is not running work (see
|
|
89
89
|
// `runListedByWorker`).
|
|
@@ -367,6 +367,9 @@ export function createSessionHistoryRefreshService({
|
|
|
367
367
|
timestamp,
|
|
368
368
|
activity.subagentRunId,
|
|
369
369
|
activity.metadata ? JSON.stringify(activity.metadata) : null,
|
|
370
|
+
// turn_seq: an imported turn has no order of its own; it keeps the
|
|
371
|
+
// grouped layout.
|
|
372
|
+
null,
|
|
370
373
|
);
|
|
371
374
|
}
|
|
372
375
|
const thoughts = ensureArray(message?.thoughts)
|
|
@@ -386,6 +389,7 @@ export function createSessionHistoryRefreshService({
|
|
|
386
389
|
thought.done ? 1 : 0,
|
|
387
390
|
timestamp,
|
|
388
391
|
thought.subagentRunId,
|
|
392
|
+
null, // turn_seq, as for the activity rows above
|
|
389
393
|
);
|
|
390
394
|
}
|
|
391
395
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-session launch mutex, one for the whole relay process, keyed by
|
|
3
|
+
* sdkSessionId.
|
|
4
|
+
*
|
|
5
|
+
* What runs under it: the Change CWD relaunch, the launch route and the
|
|
6
|
+
* delete and archive routes (sessions-routes.mjs), the worker recycle's
|
|
7
|
+
* recycle, archived stop and launch retry (worker-recycle-service.mjs), and
|
|
8
|
+
* the archived sweep at relay start (sweepArchivedWorkers there). What does
|
|
9
|
+
* not: the Claude billing swap, which keeps the old worker away from the
|
|
10
|
+
* rows by its own delivery fence; the send path's ensureWorker; and the kill
|
|
11
|
+
* route (messages-routes.mjs), so a Kill that lands in a recycle's window
|
|
12
|
+
* between its stop and its launch (about 3 s) can look undone, because the
|
|
13
|
+
* launch starts a fresh idle worker. No turn is cut by that. Taking the
|
|
14
|
+
* mutex in those places is a follow-up.
|
|
15
|
+
*
|
|
16
|
+
* It used to be built inside the sessions routes' registration, where nothing
|
|
17
|
+
* outside the routes could take it, so a recycle could stop a worker while a
|
|
18
|
+
* relaunch was half done. A module singleton is the one place both import.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createKeyedMutex } from './keyed-mutex-service.mjs';
|
|
22
|
+
|
|
23
|
+
export const sessionLaunchMutex = createKeyedMutex();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { normalizeBuildId } from '../../shared/build-id.mjs';
|
|
4
|
+
|
|
3
5
|
function normalizeSessionId(value) {
|
|
4
6
|
const text = String(value || '').trim();
|
|
5
7
|
return text || null;
|
|
@@ -141,6 +143,12 @@ export function normalizeWorkerSteeringSnapshot(raw) {
|
|
|
141
143
|
const tasks = Number(raw.backgroundTaskCount);
|
|
142
144
|
snapshot.backgroundTaskCount = Number.isInteger(tasks) && tasks > 0 ? Math.min(tasks, 999) : 0;
|
|
143
145
|
}
|
|
146
|
+
// Whether the worker's CLI child still runs. The recycle replaces a worker
|
|
147
|
+
// only once that child is gone, because a live CLI may hold scheduled jobs
|
|
148
|
+
// of its own (CronCreate, ScheduleWakeup) that a stop would lose. Only a
|
|
149
|
+
// worker that reports it carries the key: an absent key means "cannot say",
|
|
150
|
+
// which the recycle treats differently from false.
|
|
151
|
+
if (typeof raw.cliAlive === 'boolean') snapshot.cliAlive = raw.cliAlive;
|
|
144
152
|
return snapshot;
|
|
145
153
|
}
|
|
146
154
|
|
|
@@ -160,6 +168,12 @@ function sanitizeState(raw = null) {
|
|
|
160
168
|
lastError: String(base.lastError || '').trim() || null,
|
|
161
169
|
steering: normalizeWorkerSteeringSnapshot(base.steering),
|
|
162
170
|
kind: normalizeWorkerKind(base.kind),
|
|
171
|
+
// The build id of the code the worker process started on, as it stated
|
|
172
|
+
// it; null when it did not say or said something malformed.
|
|
173
|
+
buildId: normalizeBuildId(base.buildId),
|
|
174
|
+
// When the relay first heard of the current pid: the process's age as far
|
|
175
|
+
// as the relay can tell, without asking the OS. Null without a pid.
|
|
176
|
+
startedAt: normalizeTimestamp(base.startedAt, null),
|
|
163
177
|
updatedAt: normalizeTimestamp(base.updatedAt, nowIso),
|
|
164
178
|
createdAt,
|
|
165
179
|
};
|
|
@@ -266,11 +280,33 @@ export function createSessionWorkerRegistry() {
|
|
|
266
280
|
// heartbeated — indefinitely, if none ever did.
|
|
267
281
|
// The kind is the same: a process's own word. An upsert that does not
|
|
268
282
|
// state it keeps the known one, unless the process it described is gone.
|
|
283
|
+
// The build id and the start time follow the same rule.
|
|
269
284
|
const statesKind = Object.prototype.hasOwnProperty.call(rawState, 'kind');
|
|
285
|
+
const statesBuildId = Object.prototype.hasOwnProperty.call(rawState, 'buildId');
|
|
286
|
+
const statesStartedAt = Object.prototype.hasOwnProperty.call(rawState, 'startedAt');
|
|
270
287
|
if (!statesKind) next.kind = existing?.kind || null;
|
|
288
|
+
if (!statesBuildId) next.buildId = existing?.buildId || null;
|
|
289
|
+
if (!statesStartedAt) next.startedAt = existing?.startedAt || null;
|
|
271
290
|
if (existing && isWorkerReplacedOrGone(existing, next)) {
|
|
272
291
|
next.steering = null;
|
|
273
292
|
if (!statesKind) next.kind = null;
|
|
293
|
+
if (!statesBuildId) next.buildId = null;
|
|
294
|
+
if (!statesStartedAt) next.startedAt = null;
|
|
295
|
+
}
|
|
296
|
+
// A pid the entry did not have before is the first the relay hears of
|
|
297
|
+
// that process, so its start time is now. Callers spread the old entry
|
|
298
|
+
// into the upsert that brings the new pid, so a stated startedAt counts
|
|
299
|
+
// only when it is not the old process's own.
|
|
300
|
+
// The build id is that process's own word as well, and it arrives with its
|
|
301
|
+
// hello or heartbeat, not with the upsert that brings the pid. A build id
|
|
302
|
+
// equal to the old entry's is the old process's, spread in, and would make
|
|
303
|
+
// a recycled worker read as the old code (or an old worker as current)
|
|
304
|
+
// until the new one spoke; it is dropped. A different one was stated for
|
|
305
|
+
// this process and stays.
|
|
306
|
+
if (next.pid && next.pid !== existing?.pid) {
|
|
307
|
+
const statedOwnStart = statesStartedAt && next.startedAt && next.startedAt !== existing?.startedAt;
|
|
308
|
+
if (!statedOwnStart) next.startedAt = new Date().toISOString();
|
|
309
|
+
if (next.buildId && next.buildId === existing?.buildId) next.buildId = null;
|
|
274
310
|
}
|
|
275
311
|
clearIndexesForSession(existing);
|
|
276
312
|
bySession.set(state.sdkSessionId, next);
|