@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
|
@@ -18,6 +18,7 @@ import { createClaudeSessionRootResolver } from '../services/claude-session-root
|
|
|
18
18
|
import {
|
|
19
19
|
startClaudeSession,
|
|
20
20
|
createCanUseTool,
|
|
21
|
+
isPersonOnlyAsk,
|
|
21
22
|
readContextUsage,
|
|
22
23
|
readPlanUsage,
|
|
23
24
|
normalizeClaudeEffort,
|
|
@@ -2354,9 +2355,10 @@ export function createClaudeSessionRunner({
|
|
|
2354
2355
|
// nothing else is left: the cap exists to bound a retry storm's chatter,
|
|
2355
2356
|
// while a structured entry carries transcript geometry no later line can
|
|
2356
2357
|
// restore (a compaction boundary buffered between turns, then 50 task
|
|
2357
|
-
// notifications, used to lose its break row silently). The
|
|
2358
|
-
// capRelayActivityEntries protects structured rows too
|
|
2359
|
-
// prose
|
|
2358
|
+
// notifications, used to lose its break row silently). The relay's
|
|
2359
|
+
// capRelayActivityEntries protects structured rows too and keeps the
|
|
2360
|
+
// newest prose of each thread; here too the newest lines are the
|
|
2361
|
+
// informative ones.
|
|
2360
2362
|
while (proc.pendingActivities.length > MAX_PENDING_ACTIVITIES) {
|
|
2361
2363
|
const proseIndex = proc.pendingActivities.findIndex((carried) => typeof carried === 'string');
|
|
2362
2364
|
proc.pendingActivities.splice(proseIndex === -1 ? 0 : proseIndex, 1);
|
|
@@ -3806,8 +3808,9 @@ export function createClaudeSessionRunner({
|
|
|
3806
3808
|
// denied. Register a continuation turn first: it is born processing,
|
|
3807
3809
|
// gives the card a real queue row, and the flow's eventual top-level
|
|
3808
3810
|
// result (or stream end) closes it like any other continuation.
|
|
3811
|
+
// A person-only safety ask is a question card too.
|
|
3809
3812
|
if (
|
|
3810
|
-
toolName === 'AskUserQuestion'
|
|
3813
|
+
(toolName === 'AskUserQuestion' || isPersonOnlyAsk(options))
|
|
3811
3814
|
&& proc === processRef
|
|
3812
3815
|
&& !processRef.activeCtx
|
|
3813
3816
|
&& !processRef.pendingDelivered.length
|
|
@@ -4250,11 +4253,17 @@ export function createClaudeSessionRunner({
|
|
|
4250
4253
|
// `releasable`: the relay may replace this worker now (a billing swap):
|
|
4251
4254
|
// no CLI process and no delivery on its way in, or a process
|
|
4252
4255
|
// canReleaseForRespawn would let go of.
|
|
4256
|
+
// `cliAlive`: the CLI child is still up. The relay's recycle replaces a
|
|
4257
|
+
// stale worker only once it is releasable AND its CLI is gone; a
|
|
4258
|
+
// releasable worker whose CLI is up sits in the idle window, where a
|
|
4259
|
+
// scheduled job may still wake it, and is left to its own idle-out. A
|
|
4260
|
+
// closing process is not gone yet, so it still counts as alive.
|
|
4253
4261
|
if (!proc || proc.closing || proc.aborted) {
|
|
4254
4262
|
const admitting = admittingDeliveries > 0;
|
|
4255
4263
|
return {
|
|
4256
4264
|
turnActive: false, canSteer: false, holdReason: null, messageId: null, supported: true, autoCompactWindow: null,
|
|
4257
4265
|
releasable: !admitting, releaseHold: admitting ? 'turn' : null, backgroundTaskCount: 0,
|
|
4266
|
+
cliAlive: Boolean(proc),
|
|
4258
4267
|
};
|
|
4259
4268
|
}
|
|
4260
4269
|
const ctx = proc.activeCtx || null;
|
|
@@ -4293,6 +4302,7 @@ export function createClaudeSessionRunner({
|
|
|
4293
4302
|
releasable: !hold,
|
|
4294
4303
|
releaseHold: hold || null,
|
|
4295
4304
|
backgroundTaskCount: proc.liveTasks.size,
|
|
4305
|
+
cliAlive: true,
|
|
4296
4306
|
};
|
|
4297
4307
|
}
|
|
4298
4308
|
|
|
@@ -4575,6 +4585,8 @@ export function createClaudeSessionRunner({
|
|
|
4575
4585
|
backgroundTaskIds,
|
|
4576
4586
|
resyncBackgroundTasks,
|
|
4577
4587
|
flushKeptTaskSettles,
|
|
4588
|
+
// Settle notices still kept for the relay: the keepalive stays busy.
|
|
4589
|
+
keptTaskSettleCount: () => keptTaskSettles.size(),
|
|
4578
4590
|
shutdown,
|
|
4579
4591
|
// Test seams / observability.
|
|
4580
4592
|
_getProcess: () => proc,
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
|
|
15
15
|
import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
|
|
16
16
|
import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
|
|
17
|
+
import { createWorkerKeepalive } from '../../shared/worker-runtime/worker-keepalive.mjs';
|
|
18
|
+
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
17
19
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
18
20
|
import { resolveDeliveredAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
|
|
19
21
|
import { resolveDeliveredThinking } from '../../shared/claude-thinking.mjs';
|
|
@@ -59,6 +61,10 @@ export async function runClaudeSessionWorker({
|
|
|
59
61
|
|
|
60
62
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
61
63
|
|| path.resolve(__dirname, '..', 'config.json');
|
|
64
|
+
// The code this process started on, read once from the repo root and kept
|
|
65
|
+
// for its whole life: the relay recycles a worker whose build differs from
|
|
66
|
+
// its own, and a tree that moves on under us must not change our answer.
|
|
67
|
+
const buildId = resolveBuildId(path.resolve(__dirname, '..', '..')).id;
|
|
62
68
|
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
63
69
|
const token = loadTokenFromConfig(configPath);
|
|
64
70
|
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
@@ -143,6 +149,7 @@ export async function runClaudeSessionWorker({
|
|
|
143
149
|
let heartbeatTimer = null;
|
|
144
150
|
heartbeat = createHeartbeatController({
|
|
145
151
|
api,
|
|
152
|
+
buildId,
|
|
146
153
|
// What this worker runs on: the relay hands it only rows of that billing.
|
|
147
154
|
workerKind: turnRunner.workerKind,
|
|
148
155
|
pollMs: HEARTBEAT_MS,
|
|
@@ -168,10 +175,16 @@ export async function runClaudeSessionWorker({
|
|
|
168
175
|
getBackgroundTaskIds: () => turnRunner.backgroundTaskIds(),
|
|
169
176
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
170
177
|
onAnswered: () => turnRunner.flushKeptTaskSettles(),
|
|
178
|
+
getKeptPostCount: () => turnRunner.keptTaskSettleCount?.() || 0,
|
|
171
179
|
});
|
|
172
180
|
|
|
181
|
+
// One timer for the heartbeat and the socket's readiness refresh: 10 s
|
|
182
|
+
// while working, 30 s while idle on a relay that allows it.
|
|
183
|
+
let keepalive = null;
|
|
173
184
|
const wsLink = createWorkerWebSocketLink({
|
|
174
185
|
serverUrl,
|
|
186
|
+
readyRefreshMs: 0,
|
|
187
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
175
188
|
token,
|
|
176
189
|
dbg,
|
|
177
190
|
getSessionReady: () => true,
|
|
@@ -191,6 +204,7 @@ export async function runClaudeSessionWorker({
|
|
|
191
204
|
getSessionId: () => sdkSessionId,
|
|
192
205
|
getPid: () => process.pid,
|
|
193
206
|
workerKind: turnRunner.workerKind,
|
|
207
|
+
buildId,
|
|
194
208
|
onDeliver: async (pending, reason) => {
|
|
195
209
|
dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
|
|
196
210
|
const deliveredTimeout = Number(pending?.settings?.backgroundTaskTimeoutMs);
|
|
@@ -232,6 +246,7 @@ export async function runClaudeSessionWorker({
|
|
|
232
246
|
dbg('unknown worker control ignored', type || '(none)');
|
|
233
247
|
},
|
|
234
248
|
});
|
|
249
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
235
250
|
linkBridge.attach(wsLink);
|
|
236
251
|
|
|
237
252
|
let shuttingDown = false;
|
|
@@ -240,6 +255,7 @@ export async function runClaudeSessionWorker({
|
|
|
240
255
|
shuttingDown = true;
|
|
241
256
|
dbg(`shutting down (${signal})`);
|
|
242
257
|
try { wsLink.stop(); } catch {}
|
|
258
|
+
try { keepalive.stop(); } catch {}
|
|
243
259
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
244
260
|
try { controlPoller.stop(); } catch {}
|
|
245
261
|
// Consumed steers still settling must fail, not be left for dead-worker
|
|
@@ -263,7 +279,7 @@ export async function runClaudeSessionWorker({
|
|
|
263
279
|
onBeforeExit: () => { try { turnRunner.shutdown({ graceful: false }); } catch {} },
|
|
264
280
|
});
|
|
265
281
|
|
|
266
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}${backend ? ` backend=${backend.id}` : ''}`);
|
|
267
|
-
|
|
282
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}${backend ? ` backend=${backend.id}` : ''} build=${buildId || 'unknown'}`);
|
|
283
|
+
keepalive.start();
|
|
268
284
|
wsLink.start();
|
|
269
285
|
}
|
|
@@ -709,6 +709,12 @@ export function createCopilotSdkSessionRunner({
|
|
|
709
709
|
// card), so they hold the stall watchdog and the idle shutdown too — but
|
|
710
710
|
// not steering: nobody is answering a card here.
|
|
711
711
|
let pendingRelayToolCalls = 0;
|
|
712
|
+
// Deliveries `handlePendingPayload` is still working on. Most become (or
|
|
713
|
+
// steer into) `activeTurn` at once, but a steer that waits on the turn's
|
|
714
|
+
// send, its row or the relay context can find the turn gone and open its
|
|
715
|
+
// own afterwards: in that gap no turn exists, and the worker must still
|
|
716
|
+
// not read as replaceable (`releaseHold`).
|
|
717
|
+
let admittingDeliveries = 0;
|
|
712
718
|
// `relay_tool` is registered only while this relay has paired remotes.
|
|
713
719
|
// Asked once when the worker starts; a session build waits for that first
|
|
714
720
|
// answer (it is async anyway), and every runtime stop asks again so the
|
|
@@ -4739,7 +4745,62 @@ export function createCopilotSdkSessionRunner({
|
|
|
4739
4745
|
const canSteer = canAcceptSteering();
|
|
4740
4746
|
const holdReason = turnActive && !canSteer ? steeringHoldReason(turn) : null;
|
|
4741
4747
|
const messageId = String(turn?.currentOwner?.message?.id || turn?.message?.id || '').trim() || null;
|
|
4742
|
-
|
|
4748
|
+
// `releasable`: the relay may stop this worker and launch a fresh one now
|
|
4749
|
+
// (its recycle after a deploy), the same fields the Claude worker sends.
|
|
4750
|
+
const hold = releaseHold();
|
|
4751
|
+
return {
|
|
4752
|
+
turnActive,
|
|
4753
|
+
canSteer,
|
|
4754
|
+
holdReason,
|
|
4755
|
+
messageId,
|
|
4756
|
+
supported: true,
|
|
4757
|
+
cancellableIds: cancellableIds(),
|
|
4758
|
+
releasable: !hold,
|
|
4759
|
+
releaseHold: hold || null,
|
|
4760
|
+
// As the task panel counts them: with the registry it is the truth, and
|
|
4761
|
+
// the event-scraped shell set would count its shells a second time.
|
|
4762
|
+
backgroundTaskCount: sessionRpc.tasks ? backgroundTasks.size : backgroundShells.size(),
|
|
4763
|
+
// The runtime process (the CLI child) is up: a client or a session
|
|
4764
|
+
// handle exists until `stopRuntime` clears both. The relay's recycle
|
|
4765
|
+
// replaces a stale worker only once it is releasable AND this is false;
|
|
4766
|
+
// releasable with the runtime up is the idle window, left to the idle
|
|
4767
|
+
// shutdown.
|
|
4768
|
+
cliAlive: Boolean(client || session),
|
|
4769
|
+
};
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
/**
|
|
4773
|
+
* What keeps this worker from being replaced now ('' when nothing does):
|
|
4774
|
+
* the idle shutdown's gates (`evaluateLifecycle`) and the runtime tree's
|
|
4775
|
+
* busy check, named with the relay's hold words. Stopping the worker ends
|
|
4776
|
+
* the runtime, and with it the turn, the card a human is answering, a
|
|
4777
|
+
* compaction, and every background shell and agent.
|
|
4778
|
+
*
|
|
4779
|
+
* - 'question': a question card or an ask-mode approval is open;
|
|
4780
|
+
* - 'compaction': the runtime is compacting the session;
|
|
4781
|
+
* - 'turn': a turn is running or still publishing its rows, a `relay_tool`
|
|
4782
|
+
* call is out, an image row is on the relay's image routes, or a
|
|
4783
|
+
* delivery is on its way in;
|
|
4784
|
+
* - 'background': a background shell or agent is live, a settled shell's
|
|
4785
|
+
* continuation is due (`continuationGraceMs`), a registry read is due
|
|
4786
|
+
* or under way, or the task registry has failed to answer: in the last
|
|
4787
|
+
* two "nothing is running" is not known (`backgroundWorkHoldsRuntime`
|
|
4788
|
+
* holds the runtime on the same grounds).
|
|
4789
|
+
*/
|
|
4790
|
+
function releaseHold() {
|
|
4791
|
+
if (pendingHumanRequests > 0) return 'question';
|
|
4792
|
+
if (isCompacting()) return 'compaction';
|
|
4793
|
+
if (
|
|
4794
|
+
activeTurn || publishingTurns.size > 0 || pendingRelayToolCalls > 0
|
|
4795
|
+
|| directImageMessages.size > 0 || admittingDeliveries > 0
|
|
4796
|
+
) return 'turn';
|
|
4797
|
+
if (backgroundShells.size() > 0 || backgroundTasks.size > 0) return 'background';
|
|
4798
|
+
if (continuationDueSince && Date.now() - continuationDueSince < continuationGraceMs) return 'background';
|
|
4799
|
+
// A read of the registry that is debounced, queued or on its way may
|
|
4800
|
+
// still find a live agent or shell the last answer did not have.
|
|
4801
|
+
if (sessionRpc.tasks && (taskRefreshTimer || taskRefreshQueued > 0 || taskRefreshInFlight)) return 'background';
|
|
4802
|
+
if (sessionRpc.tasks && taskRefreshFailures > 0 && taskRefreshFailures < MAX_TASK_REFRESH_FAILURES) return 'background';
|
|
4803
|
+
return '';
|
|
4743
4804
|
}
|
|
4744
4805
|
|
|
4745
4806
|
/**
|
|
@@ -4883,6 +4944,18 @@ export function createCopilotSdkSessionRunner({
|
|
|
4883
4944
|
}
|
|
4884
4945
|
|
|
4885
4946
|
async function handlePendingPayload(pending) {
|
|
4947
|
+
// A payload without a message is refused at once, as it always was: it
|
|
4948
|
+
// admits nothing.
|
|
4949
|
+
if (!pending?.message) return false;
|
|
4950
|
+
admittingDeliveries += 1;
|
|
4951
|
+
try {
|
|
4952
|
+
return await admitPendingPayload(pending);
|
|
4953
|
+
} finally {
|
|
4954
|
+
admittingDeliveries -= 1;
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
async function admitPendingPayload(pending) {
|
|
4886
4959
|
const message = pending?.message || null;
|
|
4887
4960
|
if (!message) return false;
|
|
4888
4961
|
// An image model is no chat model: its row goes to the relay's image
|
|
@@ -5060,6 +5133,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
5060
5133
|
resyncBackgroundTasks,
|
|
5061
5134
|
// The relay answered a heartbeat: settle notices kept for it go out.
|
|
5062
5135
|
flushKeptTaskSettles: () => keptTaskSettles.flush(),
|
|
5136
|
+
keptTaskSettleCount: () => keptTaskSettles.size(),
|
|
5063
5137
|
cancelPushedMessage,
|
|
5064
5138
|
// The task panel's per-card Stop (`rpc.tasks.cancel`); also what a
|
|
5065
5139
|
// subagent lane's Stop tries — the runtime tracks sync agents in the same
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
|
|
21
21
|
import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
|
|
22
22
|
import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
|
|
23
|
+
import { createWorkerKeepalive } from '../../shared/worker-runtime/worker-keepalive.mjs';
|
|
24
|
+
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
23
25
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
24
26
|
import {
|
|
25
27
|
createRunnerLinkBridge,
|
|
@@ -52,6 +54,10 @@ async function main() {
|
|
|
52
54
|
|
|
53
55
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
54
56
|
|| path.resolve(__dirname, '..', 'config.json');
|
|
57
|
+
// The code this process started on, read once from the repo root and kept
|
|
58
|
+
// for its whole life: the relay recycles a worker whose build differs from
|
|
59
|
+
// its own, and a tree that moves on under us must not change our answer.
|
|
60
|
+
const buildId = resolveBuildId(path.resolve(__dirname, '..', '..')).id;
|
|
55
61
|
// Two reads of the same small file. Collapsing them to one would mean
|
|
56
62
|
// re-implementing `resolveRelayServerUrl`'s env override, host
|
|
57
63
|
// normalisation and port defaulting inside this worker — a live contract
|
|
@@ -129,6 +135,7 @@ async function main() {
|
|
|
129
135
|
let heartbeatTimer = null;
|
|
130
136
|
heartbeat = createHeartbeatController({
|
|
131
137
|
api,
|
|
138
|
+
buildId,
|
|
132
139
|
pollMs: HEARTBEAT_MS,
|
|
133
140
|
getSessionReady: () => true,
|
|
134
141
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
@@ -152,12 +159,19 @@ async function main() {
|
|
|
152
159
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
153
160
|
// The relay answered: settle notices it missed go out.
|
|
154
161
|
onAnswered: () => turnRunner.flushKeptTaskSettles?.(),
|
|
162
|
+
getKeptPostCount: () => turnRunner.keptTaskSettleCount?.() || 0,
|
|
155
163
|
});
|
|
156
164
|
|
|
165
|
+
// One timer for the heartbeat and the socket's readiness refresh: 10 s
|
|
166
|
+
// while working, 30 s while idle on a relay that allows it.
|
|
167
|
+
let keepalive = null;
|
|
157
168
|
const wsLink = createWorkerWebSocketLink({
|
|
158
169
|
serverUrl,
|
|
170
|
+
readyRefreshMs: 0,
|
|
171
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
159
172
|
token,
|
|
160
173
|
dbg,
|
|
174
|
+
buildId,
|
|
161
175
|
getSessionReady: () => true,
|
|
162
176
|
getSessionId: () => sdkSessionId,
|
|
163
177
|
getPid: () => process.pid,
|
|
@@ -206,11 +220,13 @@ async function main() {
|
|
|
206
220
|
dbg('worker control ignored (unsupported by the SDK worker)', type || '(none)');
|
|
207
221
|
},
|
|
208
222
|
});
|
|
223
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
209
224
|
linkBridge.attach(wsLink);
|
|
210
225
|
|
|
211
226
|
const shutdown = async (signal) => {
|
|
212
227
|
dbg(`shutting down (${signal})`);
|
|
213
228
|
try { wsLink.stop(); } catch {}
|
|
229
|
+
try { keepalive.stop(); } catch {}
|
|
214
230
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
215
231
|
try { controlPoller.stop(); } catch {}
|
|
216
232
|
// Rows whose prompt the runtime consumed and whose settle marker has not
|
|
@@ -238,8 +254,8 @@ async function main() {
|
|
|
238
254
|
getActiveQueueMessageIds: () => turnRunner.getActiveQueueMessageIds(),
|
|
239
255
|
});
|
|
240
256
|
|
|
241
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
242
|
-
|
|
257
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
258
|
+
keepalive.start();
|
|
243
259
|
wsLink.start();
|
|
244
260
|
}
|
|
245
261
|
|
|
@@ -13,10 +13,13 @@ import {
|
|
|
13
13
|
import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
|
|
14
14
|
import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
|
|
15
15
|
import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
|
|
16
|
+
import { createWorkerKeepalive } from '../../shared/worker-runtime/worker-keepalive.mjs';
|
|
17
|
+
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
16
18
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
17
19
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
18
20
|
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
19
21
|
import { createCursorTurnRunner } from './cursor-turn-runner.mjs';
|
|
22
|
+
import { buildCursorSteeringState } from './cursor-worker-steering.mjs';
|
|
20
23
|
import { resolveCursorSessionStoreDir } from '../services/cursor-session-root-service.mjs';
|
|
21
24
|
|
|
22
25
|
// Windows console launch: the launcher names the worker log, this copies
|
|
@@ -48,6 +51,10 @@ async function main() {
|
|
|
48
51
|
|
|
49
52
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
50
53
|
|| path.resolve(__dirname, '..', 'config.json');
|
|
54
|
+
// The code this process started on, read once from the repo root and kept
|
|
55
|
+
// for its whole life: the relay recycles a worker whose build differs from
|
|
56
|
+
// its own, and a tree that moves on under us must not change our answer.
|
|
57
|
+
const buildId = resolveBuildId(path.resolve(__dirname, '..', '..')).id;
|
|
51
58
|
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
52
59
|
const token = loadTokenFromConfig(configPath);
|
|
53
60
|
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
@@ -93,17 +100,27 @@ async function main() {
|
|
|
93
100
|
let heartbeatTimer = null;
|
|
94
101
|
const heartbeat = createHeartbeatController({
|
|
95
102
|
api,
|
|
103
|
+
buildId,
|
|
96
104
|
pollMs: HEARTBEAT_MS,
|
|
97
105
|
getSessionReady: () => true,
|
|
98
106
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
99
107
|
setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
|
|
100
108
|
getActiveQueueMessageId: () => turnRunner.getActiveQueueMessageId(),
|
|
109
|
+
// No steering here; the snapshot tells the relay when it may replace
|
|
110
|
+
// this worker: between turns (the agent handle is rebuilt on demand).
|
|
111
|
+
getSteeringState: () => buildCursorSteeringState(turnRunner),
|
|
101
112
|
});
|
|
102
113
|
|
|
114
|
+
// One timer for the heartbeat and the socket's readiness refresh: 10 s
|
|
115
|
+
// while working, 30 s while idle on a relay that allows it.
|
|
116
|
+
let keepalive = null;
|
|
103
117
|
const wsLink = createWorkerWebSocketLink({
|
|
104
118
|
serverUrl,
|
|
119
|
+
readyRefreshMs: 0,
|
|
120
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
105
121
|
token,
|
|
106
122
|
dbg,
|
|
123
|
+
buildId,
|
|
107
124
|
getSessionReady: () => true,
|
|
108
125
|
getSessionId: () => sdkSessionId,
|
|
109
126
|
getPid: () => process.pid,
|
|
@@ -117,10 +134,12 @@ async function main() {
|
|
|
117
134
|
}
|
|
118
135
|
},
|
|
119
136
|
});
|
|
137
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
120
138
|
|
|
121
139
|
const shutdown = async (signal) => {
|
|
122
140
|
dbg(`shutting down (${signal})`);
|
|
123
141
|
try { wsLink.stop(); } catch {}
|
|
142
|
+
try { keepalive.stop(); } catch {}
|
|
124
143
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
125
144
|
try { controlPoller.stop(); } catch {}
|
|
126
145
|
// The agent handle owns a SQLite store; close it before exiting — but
|
|
@@ -146,8 +165,8 @@ async function main() {
|
|
|
146
165
|
getActiveQueueMessageIds: () => [turnRunner.getActiveQueueAttempt()],
|
|
147
166
|
});
|
|
148
167
|
|
|
149
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
150
|
-
|
|
168
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
169
|
+
keepalive.start();
|
|
151
170
|
wsLink.start();
|
|
152
171
|
}
|
|
153
172
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cursor worker's heartbeat steering snapshot, from its turn runner
|
|
3
|
+
* (cursor-turn-runner.mjs). A module of its own so it can be tested: the
|
|
4
|
+
* worker main runs at import.
|
|
5
|
+
*
|
|
6
|
+
* Only a turn holds the release. The Cursor SDK tells the runner of no work
|
|
7
|
+
* that outlives a run: whether a sub-agent or a background shell can keep
|
|
8
|
+
* running after the run's result is not something its types say, and the
|
|
9
|
+
* runner sees neither of them, so there is no background count to send. The
|
|
10
|
+
* relay's quiet rule (10 minutes without a delivery or a turn end) covers
|
|
11
|
+
* what the SDK does not say.
|
|
12
|
+
*/
|
|
13
|
+
import { buildSimpleSteeringState } from '../../shared/worker-runtime/simple-steering-state.mjs';
|
|
14
|
+
|
|
15
|
+
export function buildCursorSteeringState(turnRunner) {
|
|
16
|
+
return buildSimpleSteeringState({
|
|
17
|
+
activeAttempt: turnRunner.getActiveQueueAttempt(),
|
|
18
|
+
turnActive: turnRunner.isTurnActive(),
|
|
19
|
+
});
|
|
20
|
+
}
|
package/server/db-schema.mjs
CHANGED
|
@@ -42,6 +42,8 @@ import { migrateClaudeApiUsageCheckpoints } from './migrations/0033-claude-api-u
|
|
|
42
42
|
import { migrateClaudeApiUsageTotals } from './migrations/0034-claude-api-usage-totals.mjs';
|
|
43
43
|
import { migrateConversationPlanOnly } from './migrations/0035-conversation-plan-only.mjs';
|
|
44
44
|
import { migrateTokenUsage } from './migrations/0036-token-usage.mjs';
|
|
45
|
+
import { migrateRelayAsksQueueId } from './migrations/0037-relay-asks-queue-id.mjs';
|
|
46
|
+
import { migrateTurnSeq } from './migrations/0038-turn-seq.mjs';
|
|
45
47
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
46
48
|
|
|
47
49
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -1077,6 +1079,23 @@ try {
|
|
|
1077
1079
|
console.warn(`[token-usage] migration 0036 failed; retrying on next boot: ${error?.message || error}`);
|
|
1078
1080
|
}
|
|
1079
1081
|
|
|
1082
|
+
// The turn an ask was made in: one column on 0023's table. The ask service
|
|
1083
|
+
// works without it, so a failure is logged and retried next boot like 0023.
|
|
1084
|
+
try {
|
|
1085
|
+
migrateRelayAsksQueueId(db);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
console.warn(`[relay-asks] migration 0037 failed; retrying on next boot: ${error?.message || error}`);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// One order across a turn's thoughts and tool lines (the stream view). The
|
|
1091
|
+
// repository checks for the column and works without it (no numbers, the
|
|
1092
|
+
// grouped layout), so a failure is logged and retried next boot.
|
|
1093
|
+
try {
|
|
1094
|
+
migrateTurnSeq(db);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
console.warn(`[turn-seq] migration 0038 failed; retrying on next boot: ${error?.message || error}`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1080
1099
|
const conversationColumns = db.prepare(`PRAGMA table_info(conversations)`).all().map((c) => c.name);
|
|
1081
1100
|
if (!conversationColumns.includes('archived')) {
|
|
1082
1101
|
db.exec(`ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0`);
|
|
@@ -235,10 +235,20 @@ export function createGrokTaskPublisher({
|
|
|
235
235
|
stop,
|
|
236
236
|
/** The ids of the live terminals, for the heartbeat's digest. */
|
|
237
237
|
backgroundTaskIds: () => liveEntries().map((entry) => entry.taskId),
|
|
238
|
+
/**
|
|
239
|
+
* Terminals whose process runs and that the agent has not released, from
|
|
240
|
+
* the moment they start: a superset of the live ids, which wait out the
|
|
241
|
+
* publish grace first. What may die with the worker, so the relay must
|
|
242
|
+
* not replace it while any is open (the steering snapshot's count); a
|
|
243
|
+
* `sleep 600` started a second before a recycle is as live as one the
|
|
244
|
+
* panel already shows.
|
|
245
|
+
*/
|
|
246
|
+
openTerminalCount: () => [...terminals.values()].filter((entry) => !entry.exited && !entry.released).length,
|
|
238
247
|
/** The relay asked for the set again (heartbeat answer). */
|
|
239
248
|
resync: () => publish({ resync: true }),
|
|
240
249
|
/** The relay answered a heartbeat: settle notices kept for it go out. */
|
|
241
250
|
flushKeptSettles: () => keptSettles.flush(),
|
|
251
|
+
keptSettleCount: () => keptSettles.size(),
|
|
242
252
|
sweep,
|
|
243
253
|
close() {
|
|
244
254
|
clearInterval(sweepTimer);
|
|
@@ -12,10 +12,13 @@ import {
|
|
|
12
12
|
import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
|
|
13
13
|
import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
|
|
14
14
|
import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
|
|
15
|
+
import { createWorkerKeepalive } from '../../shared/worker-runtime/worker-keepalive.mjs';
|
|
16
|
+
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
15
17
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
16
18
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
17
19
|
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
18
20
|
import { createGrokTurnRunner } from './grok-turn-runner.mjs';
|
|
21
|
+
import { buildGrokSteeringState } from './grok-worker-steering.mjs';
|
|
19
22
|
import { createGrokTaskPublisher } from './grok-background-tasks.mjs';
|
|
20
23
|
|
|
21
24
|
// Windows console launch: the launcher names the worker log, this copies
|
|
@@ -47,6 +50,10 @@ async function main() {
|
|
|
47
50
|
|
|
48
51
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
49
52
|
|| path.resolve(__dirname, '..', 'config.json');
|
|
53
|
+
// The code this process started on, read once from the repo root and kept
|
|
54
|
+
// for its whole life: the relay recycles a worker whose build differs from
|
|
55
|
+
// its own, and a tree that moves on under us must not change our answer.
|
|
56
|
+
const buildId = resolveBuildId(path.resolve(__dirname, '..', '..')).id;
|
|
50
57
|
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
51
58
|
const token = loadTokenFromConfig(configPath);
|
|
52
59
|
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
@@ -96,6 +103,7 @@ async function main() {
|
|
|
96
103
|
let heartbeatTimer = null;
|
|
97
104
|
heartbeat = createHeartbeatController({
|
|
98
105
|
api,
|
|
106
|
+
buildId,
|
|
99
107
|
pollMs: HEARTBEAT_MS,
|
|
100
108
|
getSessionReady: () => true,
|
|
101
109
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
@@ -106,12 +114,22 @@ async function main() {
|
|
|
106
114
|
getBackgroundTaskIds: () => turnRunner.backgroundTaskIds(),
|
|
107
115
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
108
116
|
onAnswered: () => turnRunner.flushKeptTaskSettles(),
|
|
117
|
+
getKeptPostCount: () => turnRunner.keptTaskSettleCount?.() || 0,
|
|
118
|
+
// No steering here; the snapshot tells the relay when it may replace
|
|
119
|
+
// this worker: no turn, and no open terminal (they end with the worker).
|
|
120
|
+
getSteeringState: () => buildGrokSteeringState(turnRunner),
|
|
109
121
|
});
|
|
110
122
|
|
|
123
|
+
// One timer for the heartbeat and the socket's readiness refresh: 10 s
|
|
124
|
+
// while working, 30 s while idle on a relay that allows it.
|
|
125
|
+
let keepalive = null;
|
|
111
126
|
const wsLink = createWorkerWebSocketLink({
|
|
112
127
|
serverUrl,
|
|
128
|
+
readyRefreshMs: 0,
|
|
129
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
113
130
|
token,
|
|
114
131
|
dbg,
|
|
132
|
+
buildId,
|
|
115
133
|
getSessionReady: () => true,
|
|
116
134
|
// A relay that just (re)started learns of the terminals from the digest
|
|
117
135
|
// on the next beat; ask for it now.
|
|
@@ -143,10 +161,12 @@ async function main() {
|
|
|
143
161
|
dbg('unknown worker control ignored', type || '(none)');
|
|
144
162
|
},
|
|
145
163
|
});
|
|
164
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
146
165
|
|
|
147
166
|
const shutdown = async (signal) => {
|
|
148
167
|
dbg(`shutting down (${signal})`);
|
|
149
168
|
try { wsLink.stop(); } catch {}
|
|
169
|
+
try { keepalive.stop(); } catch {}
|
|
150
170
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
151
171
|
try { controlPoller.stop(); } catch {}
|
|
152
172
|
try { taskPublisher.close(); } catch {}
|
|
@@ -171,8 +191,8 @@ async function main() {
|
|
|
171
191
|
getActiveQueueMessageIds: () => [turnRunner.getActiveQueueAttempt()],
|
|
172
192
|
});
|
|
173
193
|
|
|
174
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
175
|
-
|
|
194
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
195
|
+
keepalive.start();
|
|
176
196
|
wsLink.start();
|
|
177
197
|
}
|
|
178
198
|
|
|
@@ -667,9 +667,13 @@ export function createGrokTurnRunner({
|
|
|
667
667
|
// The terminals as background tasks: the heartbeat's digest and resync,
|
|
668
668
|
// the panel's Stop, kept settle notices.
|
|
669
669
|
backgroundTaskIds: () => terminalTasks?.backgroundTaskIds() || [],
|
|
670
|
+
// Every terminal that runs and is not released, published or not yet:
|
|
671
|
+
// the steering snapshot's background count (grok-worker-steering.mjs).
|
|
672
|
+
openTerminalCount: () => terminalTasks?.openTerminalCount?.() || 0,
|
|
670
673
|
resyncBackgroundTasks: () => terminalTasks?.resync(),
|
|
671
674
|
stopBackgroundTask: (taskId) => terminalTasks?.stop(taskId) || false,
|
|
672
675
|
flushKeptTaskSettles: () => terminalTasks?.flushKeptSettles(),
|
|
676
|
+
keptTaskSettleCount: () => terminalTasks?.keptSettleCount?.() || 0,
|
|
673
677
|
setTurnCeilingMs(value) {
|
|
674
678
|
const ms = Number(value);
|
|
675
679
|
turnCeilingMs = Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Grok worker's heartbeat steering snapshot, from its turn runner
|
|
3
|
+
* (grok-turn-runner.mjs). A module of its own so it can be tested: the worker
|
|
4
|
+
* main runs at import.
|
|
5
|
+
*
|
|
6
|
+
* A turn holds the release, then any open terminal: it dies with the worker.
|
|
7
|
+
* Counted from the terminal's start (`openTerminalCount`), not from when it
|
|
8
|
+
* becomes a panel row (`backgroundTaskIds`, after a 2 s grace), so a command
|
|
9
|
+
* started just before a recycle sweep still holds it.
|
|
10
|
+
*/
|
|
11
|
+
import { buildSimpleSteeringState } from '../../shared/worker-runtime/simple-steering-state.mjs';
|
|
12
|
+
|
|
13
|
+
export function buildGrokSteeringState(turnRunner) {
|
|
14
|
+
const open = Number(turnRunner.openTerminalCount?.()) || 0;
|
|
15
|
+
// The live ids are open terminals too; the larger count wins should the two
|
|
16
|
+
// ever disagree.
|
|
17
|
+
const listed = turnRunner.backgroundTaskIds?.().length || 0;
|
|
18
|
+
return buildSimpleSteeringState({
|
|
19
|
+
activeAttempt: turnRunner.getActiveQueueAttempt(),
|
|
20
|
+
turnActive: turnRunner.isTurnActive(),
|
|
21
|
+
backgroundTaskCount: Math.max(open, listed),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: the turn an ask was made in.
|
|
5
|
+
*
|
|
6
|
+
* `relay_asks.queue_id`: the queue row of the turn that was live in the
|
|
7
|
+
* asking conversation when its agent asked (services/relay-ask-service.mjs
|
|
8
|
+
* create), so the browser can show the ask in that turn's bubble. NULL for
|
|
9
|
+
* an ask made on behalf of another session, one a paired relay's agent made
|
|
10
|
+
* here, a mirror, and every ask from before this migration. No index: it is
|
|
11
|
+
* read with the row, never looked up by.
|
|
12
|
+
*
|
|
13
|
+
* Not one of RELAY_ASK_COLUMNS (0023): the ask service works without it and
|
|
14
|
+
* then stores nothing.
|
|
15
|
+
*
|
|
16
|
+
* Additive and idempotent. Safe to run on every boot; nothing to do while
|
|
17
|
+
* there is no relay_asks table (0023 not run yet). Rolling back needs no
|
|
18
|
+
* data change: older code ignores the column.
|
|
19
|
+
*
|
|
20
|
+
* Usage: node server/migrations/0037-relay-asks-queue-id.mjs [path/to/oar.db]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
import process from 'process';
|
|
26
|
+
import Database from 'better-sqlite3';
|
|
27
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
28
|
+
|
|
29
|
+
function tableExists(db, name) {
|
|
30
|
+
return !!db
|
|
31
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
32
|
+
.get(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function migrateRelayAsksQueueId(db) {
|
|
36
|
+
if (!tableExists(db, 'relay_asks')) return { applied: false, columnsAdded: 0 };
|
|
37
|
+
const present = new Set(db.prepare(`PRAGMA table_info(relay_asks)`).all().map((column) => column.name));
|
|
38
|
+
if (present.has('queue_id')) return { applied: false, columnsAdded: 0 };
|
|
39
|
+
db.exec(`ALTER TABLE relay_asks ADD COLUMN queue_id TEXT`);
|
|
40
|
+
return { applied: true, columnsAdded: 1 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function migrate(dbPath, options = {}) {
|
|
44
|
+
const db = new Database(dbPath, options);
|
|
45
|
+
try {
|
|
46
|
+
return migrateRelayAsksQueueId(db);
|
|
47
|
+
} finally {
|
|
48
|
+
db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
53
|
+
if (isMain) {
|
|
54
|
+
try {
|
|
55
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
56
|
+
// name before 0.9.20); never a new one.
|
|
57
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
58
|
+
const result = migrate(dbPath);
|
|
59
|
+
console.log(`Migration 0037 on ${dbPath}`);
|
|
60
|
+
console.log(result.applied ? ` relay asks: columns added ${result.columnsAdded}` : ' skipped: nothing to change');
|
|
61
|
+
process.exit(0);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(`Migration 0037 failed: ${err?.message || err}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|