@oar-sh/oar 0.9.21 → 0.9.23
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 +206 -0
- package/README.md +9 -9
- package/bin/oar.js +28 -0
- package/package.json +13 -14
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +9 -4
- package/server/claude-worker/claude-session-worker-main.mjs +10 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
- package/server/cursor-worker/cursor-session-worker.mjs +9 -1
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +1 -0
- package/server/grok-worker/grok-session-worker.mjs +10 -1
- package/server/grok-worker/grok-turn-runner.mjs +1 -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/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +7 -0
- package/server/public/app/ask-user-view.js +4 -3
- package/server/public/app/bootstrap.js +62 -4
- package/server/public/app/chat-actions-menu-state.mjs +2 -0
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-view.js +306 -49
- package/server/public/app/header-priority-actions.mjs +5 -1
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/inbox-ui.js +70 -79
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +17 -0
- package/server/public/app/live-activity-cap.mjs +103 -0
- 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-thoughts.mjs +15 -1
- package/server/public/app/session-history-ui.js +1 -0
- package/server/public/app/settings-modal.js +6 -0
- package/server/public/app/socket-handlers.js +42 -11
- 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 +8 -6
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/index.html +391 -214
- package/server/repositories/message-repository.mjs +10 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/messages-routes.mjs +46 -34
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +77 -6
- package/server/server-runtime.mjs +199 -171
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/earlier-database-service.mjs +1 -1
- package/server/services/file-serving.mjs +13 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +7 -4
- 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-console-log.mjs +2 -3
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +143 -15
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/update-install-service.mjs +32 -1
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +26 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -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
|
|
@@ -4582,6 +4585,8 @@ export function createClaudeSessionRunner({
|
|
|
4582
4585
|
backgroundTaskIds,
|
|
4583
4586
|
resyncBackgroundTasks,
|
|
4584
4587
|
flushKeptTaskSettles,
|
|
4588
|
+
// Settle notices still kept for the relay: the keepalive stays busy.
|
|
4589
|
+
keptTaskSettleCount: () => keptTaskSettles.size(),
|
|
4585
4590
|
shutdown,
|
|
4586
4591
|
// Test seams / observability.
|
|
4587
4592
|
_getProcess: () => proc,
|
|
@@ -14,6 +14,7 @@ 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';
|
|
17
18
|
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
18
19
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
19
20
|
import { resolveDeliveredAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
|
|
@@ -174,10 +175,16 @@ export async function runClaudeSessionWorker({
|
|
|
174
175
|
getBackgroundTaskIds: () => turnRunner.backgroundTaskIds(),
|
|
175
176
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
176
177
|
onAnswered: () => turnRunner.flushKeptTaskSettles(),
|
|
178
|
+
getKeptPostCount: () => turnRunner.keptTaskSettleCount?.() || 0,
|
|
177
179
|
});
|
|
178
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;
|
|
179
184
|
const wsLink = createWorkerWebSocketLink({
|
|
180
185
|
serverUrl,
|
|
186
|
+
readyRefreshMs: 0,
|
|
187
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
181
188
|
token,
|
|
182
189
|
dbg,
|
|
183
190
|
getSessionReady: () => true,
|
|
@@ -239,6 +246,7 @@ export async function runClaudeSessionWorker({
|
|
|
239
246
|
dbg('unknown worker control ignored', type || '(none)');
|
|
240
247
|
},
|
|
241
248
|
});
|
|
249
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
242
250
|
linkBridge.attach(wsLink);
|
|
243
251
|
|
|
244
252
|
let shuttingDown = false;
|
|
@@ -247,6 +255,7 @@ export async function runClaudeSessionWorker({
|
|
|
247
255
|
shuttingDown = true;
|
|
248
256
|
dbg(`shutting down (${signal})`);
|
|
249
257
|
try { wsLink.stop(); } catch {}
|
|
258
|
+
try { keepalive.stop(); } catch {}
|
|
250
259
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
251
260
|
try { controlPoller.stop(); } catch {}
|
|
252
261
|
// Consumed steers still settling must fail, not be left for dead-worker
|
|
@@ -271,6 +280,6 @@ export async function runClaudeSessionWorker({
|
|
|
271
280
|
});
|
|
272
281
|
|
|
273
282
|
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}${backend ? ` backend=${backend.id}` : ''} build=${buildId || 'unknown'}`);
|
|
274
|
-
|
|
283
|
+
keepalive.start();
|
|
275
284
|
wsLink.start();
|
|
276
285
|
}
|
|
@@ -5133,6 +5133,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
5133
5133
|
resyncBackgroundTasks,
|
|
5134
5134
|
// The relay answered a heartbeat: settle notices kept for it go out.
|
|
5135
5135
|
flushKeptTaskSettles: () => keptTaskSettles.flush(),
|
|
5136
|
+
keptTaskSettleCount: () => keptTaskSettles.size(),
|
|
5136
5137
|
cancelPushedMessage,
|
|
5137
5138
|
// The task panel's per-card Stop (`rpc.tasks.cancel`); also what a
|
|
5138
5139
|
// subagent lane's Stop tries — the runtime tracks sync agents in the same
|
|
@@ -20,6 +20,7 @@ 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';
|
|
23
24
|
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
24
25
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
25
26
|
import {
|
|
@@ -158,10 +159,16 @@ async function main() {
|
|
|
158
159
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
159
160
|
// The relay answered: settle notices it missed go out.
|
|
160
161
|
onAnswered: () => turnRunner.flushKeptTaskSettles?.(),
|
|
162
|
+
getKeptPostCount: () => turnRunner.keptTaskSettleCount?.() || 0,
|
|
161
163
|
});
|
|
162
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;
|
|
163
168
|
const wsLink = createWorkerWebSocketLink({
|
|
164
169
|
serverUrl,
|
|
170
|
+
readyRefreshMs: 0,
|
|
171
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
165
172
|
token,
|
|
166
173
|
dbg,
|
|
167
174
|
buildId,
|
|
@@ -213,11 +220,13 @@ async function main() {
|
|
|
213
220
|
dbg('worker control ignored (unsupported by the SDK worker)', type || '(none)');
|
|
214
221
|
},
|
|
215
222
|
});
|
|
223
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
216
224
|
linkBridge.attach(wsLink);
|
|
217
225
|
|
|
218
226
|
const shutdown = async (signal) => {
|
|
219
227
|
dbg(`shutting down (${signal})`);
|
|
220
228
|
try { wsLink.stop(); } catch {}
|
|
229
|
+
try { keepalive.stop(); } catch {}
|
|
221
230
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
222
231
|
try { controlPoller.stop(); } catch {}
|
|
223
232
|
// Rows whose prompt the runtime consumed and whose settle marker has not
|
|
@@ -246,7 +255,7 @@ async function main() {
|
|
|
246
255
|
});
|
|
247
256
|
|
|
248
257
|
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
249
|
-
|
|
258
|
+
keepalive.start();
|
|
250
259
|
wsLink.start();
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -13,6 +13,7 @@ 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';
|
|
16
17
|
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
17
18
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
18
19
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
@@ -110,8 +111,13 @@ async function main() {
|
|
|
110
111
|
getSteeringState: () => buildCursorSteeringState(turnRunner),
|
|
111
112
|
});
|
|
112
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;
|
|
113
117
|
const wsLink = createWorkerWebSocketLink({
|
|
114
118
|
serverUrl,
|
|
119
|
+
readyRefreshMs: 0,
|
|
120
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
115
121
|
token,
|
|
116
122
|
dbg,
|
|
117
123
|
buildId,
|
|
@@ -128,10 +134,12 @@ async function main() {
|
|
|
128
134
|
}
|
|
129
135
|
},
|
|
130
136
|
});
|
|
137
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
131
138
|
|
|
132
139
|
const shutdown = async (signal) => {
|
|
133
140
|
dbg(`shutting down (${signal})`);
|
|
134
141
|
try { wsLink.stop(); } catch {}
|
|
142
|
+
try { keepalive.stop(); } catch {}
|
|
135
143
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
136
144
|
try { controlPoller.stop(); } catch {}
|
|
137
145
|
// The agent handle owns a SQLite store; close it before exiting — but
|
|
@@ -158,7 +166,7 @@ async function main() {
|
|
|
158
166
|
});
|
|
159
167
|
|
|
160
168
|
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
161
|
-
|
|
169
|
+
keepalive.start();
|
|
162
170
|
wsLink.start();
|
|
163
171
|
}
|
|
164
172
|
|
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`);
|
|
@@ -248,6 +248,7 @@ export function createGrokTaskPublisher({
|
|
|
248
248
|
resync: () => publish({ resync: true }),
|
|
249
249
|
/** The relay answered a heartbeat: settle notices kept for it go out. */
|
|
250
250
|
flushKeptSettles: () => keptSettles.flush(),
|
|
251
|
+
keptSettleCount: () => keptSettles.size(),
|
|
251
252
|
sweep,
|
|
252
253
|
close() {
|
|
253
254
|
clearInterval(sweepTimer);
|
|
@@ -12,6 +12,7 @@ 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';
|
|
15
16
|
import { resolveBuildId } from '../../shared/build-id.mjs';
|
|
16
17
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
17
18
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
@@ -113,13 +114,19 @@ async function main() {
|
|
|
113
114
|
getBackgroundTaskIds: () => turnRunner.backgroundTaskIds(),
|
|
114
115
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
115
116
|
onAnswered: () => turnRunner.flushKeptTaskSettles(),
|
|
117
|
+
getKeptPostCount: () => turnRunner.keptTaskSettleCount?.() || 0,
|
|
116
118
|
// No steering here; the snapshot tells the relay when it may replace
|
|
117
119
|
// this worker: no turn, and no open terminal (they end with the worker).
|
|
118
120
|
getSteeringState: () => buildGrokSteeringState(turnRunner),
|
|
119
121
|
});
|
|
120
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;
|
|
121
126
|
const wsLink = createWorkerWebSocketLink({
|
|
122
127
|
serverUrl,
|
|
128
|
+
readyRefreshMs: 0,
|
|
129
|
+
onRelayFrame: () => keepalive?.nudge(),
|
|
123
130
|
token,
|
|
124
131
|
dbg,
|
|
125
132
|
buildId,
|
|
@@ -154,10 +161,12 @@ async function main() {
|
|
|
154
161
|
dbg('unknown worker control ignored', type || '(none)');
|
|
155
162
|
},
|
|
156
163
|
});
|
|
164
|
+
keepalive = createWorkerKeepalive({ heartbeat, link: wsLink, busyMs: HEARTBEAT_MS });
|
|
157
165
|
|
|
158
166
|
const shutdown = async (signal) => {
|
|
159
167
|
dbg(`shutting down (${signal})`);
|
|
160
168
|
try { wsLink.stop(); } catch {}
|
|
169
|
+
try { keepalive.stop(); } catch {}
|
|
161
170
|
try { heartbeat.stopHeartbeat(); } catch {}
|
|
162
171
|
try { controlPoller.stop(); } catch {}
|
|
163
172
|
try { taskPublisher.close(); } catch {}
|
|
@@ -183,7 +192,7 @@ async function main() {
|
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
186
|
-
|
|
195
|
+
keepalive.start();
|
|
187
196
|
wsLink.start();
|
|
188
197
|
}
|
|
189
198
|
|
|
@@ -673,6 +673,7 @@ export function createGrokTurnRunner({
|
|
|
673
673
|
resyncBackgroundTasks: () => terminalTasks?.resync(),
|
|
674
674
|
stopBackgroundTask: (taskId) => terminalTasks?.stop(taskId) || false,
|
|
675
675
|
flushKeptTaskSettles: () => terminalTasks?.flushKeptSettles(),
|
|
676
|
+
keptTaskSettleCount: () => terminalTasks?.keptSettleCount?.() || 0,
|
|
676
677
|
setTurnCeilingMs(value) {
|
|
677
678
|
const ms = Number(value);
|
|
678
679
|
turnCeilingMs = Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: one order across a turn's thoughts and tool lines.
|
|
5
|
+
*
|
|
6
|
+
* `relay_activity.turn_seq` and `relay_thought.turn_seq`: a number per queue
|
|
7
|
+
* message shared by both tables (repositories/question-repository.mjs
|
|
8
|
+
* nextTurnSeq), given when a row is first written, so the stream view can
|
|
9
|
+
* show a turn's thoughts and tool calls in the order they happened. NULL for
|
|
10
|
+
* every row from before this migration and for rows a session-history
|
|
11
|
+
* rebuild writes; a turn with such a row keeps the grouped layout.
|
|
12
|
+
*
|
|
13
|
+
* Indexes on (queue_message_id, turn_seq), partial (only numbered rows): the
|
|
14
|
+
* next number is a max() over them. Not unique: one counter spans two tables.
|
|
15
|
+
*
|
|
16
|
+
* Not in SCHEMA_SQL (db-schema.mjs): that block runs before the migrations,
|
|
17
|
+
* so an index on the column there would fail on every existing database.
|
|
18
|
+
*
|
|
19
|
+
* Additive and idempotent. Safe to run on every boot; a table that does not
|
|
20
|
+
* exist yet is left alone. Rolling back needs no data change: older code
|
|
21
|
+
* ignores the columns.
|
|
22
|
+
*
|
|
23
|
+
* Usage: node server/migrations/0038-turn-seq.mjs [path/to/oar.db]
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import process from 'process';
|
|
29
|
+
import Database from 'better-sqlite3';
|
|
30
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
31
|
+
|
|
32
|
+
export const TURN_SEQ_TABLES = Object.freeze(['relay_activity', 'relay_thought']);
|
|
33
|
+
|
|
34
|
+
function tableExists(db, name) {
|
|
35
|
+
return !!db
|
|
36
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
37
|
+
.get(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasTurnSeqColumn(db, table) {
|
|
41
|
+
try {
|
|
42
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().some((column) => column.name === 'turn_seq');
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function migrateTurnSeq(db) {
|
|
49
|
+
let columnsAdded = 0;
|
|
50
|
+
let indexesAdded = 0;
|
|
51
|
+
for (const table of TURN_SEQ_TABLES) {
|
|
52
|
+
if (!tableExists(db, table)) continue;
|
|
53
|
+
if (!hasTurnSeqColumn(db, table)) {
|
|
54
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN turn_seq INTEGER`);
|
|
55
|
+
columnsAdded += 1;
|
|
56
|
+
}
|
|
57
|
+
const index = `idx_${table}_queue_turn_seq`;
|
|
58
|
+
const present = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`).get(index);
|
|
59
|
+
if (!present) {
|
|
60
|
+
db.exec(`CREATE INDEX ${index} ON ${table}(queue_message_id, turn_seq) WHERE turn_seq IS NOT NULL`);
|
|
61
|
+
indexesAdded += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { applied: columnsAdded + indexesAdded > 0, columnsAdded, indexesAdded };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function migrate(dbPath, options = {}) {
|
|
68
|
+
const db = new Database(dbPath, options);
|
|
69
|
+
try {
|
|
70
|
+
return migrateTurnSeq(db);
|
|
71
|
+
} finally {
|
|
72
|
+
db.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
77
|
+
if (isMain) {
|
|
78
|
+
try {
|
|
79
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
80
|
+
// name before 0.9.20); never a new one.
|
|
81
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
82
|
+
const result = migrate(dbPath);
|
|
83
|
+
console.log(`Migration 0038 on ${dbPath}`);
|
|
84
|
+
console.log(result.applied
|
|
85
|
+
? ` turn order: columns added ${result.columnsAdded}, indexes added ${result.indexesAdded}`
|
|
86
|
+
: ' skipped: nothing to change');
|
|
87
|
+
process.exit(0);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`Migration 0038 failed: ${err?.message || err}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|