@oar-sh/oar 0.9.19 → 0.9.21
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/.github/extensions/web-relay/polling/polling-loop.mjs +2 -26
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +2 -26
- package/CHANGELOG.md +529 -0
- package/README.md +14 -14
- package/bin/oar.js +392 -98
- package/package.json +4 -2
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +60 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +258 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +22 -1
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +1 -0
- package/server/claude-worker/claude-session-process.mjs +10 -0
- package/server/claude-worker/claude-session-worker-main.mjs +8 -1
- package/server/claude-worker/claude-turn-publisher.mjs +163 -8
- package/server/claude-worker/claude-usage-totals.mjs +58 -0
- package/server/copilot-worker/copilot-plan-board.mjs +4 -7
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +86 -3
- package/server/copilot-worker/copilot-sdk-session-process.mjs +131 -5
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -1
- package/server/cursor-worker/cursor-session-worker.mjs +12 -1
- package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +18 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
- package/server/grok-worker/grok-background-tasks.mjs +9 -0
- package/server/grok-worker/grok-session-worker.mjs +12 -1
- package/server/grok-worker/grok-turn-runner.mjs +47 -6
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/migrations/0001-add-structured-answer.mjs +5 -7
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +5 -7
- package/server/migrations/0003-push-subscriptions.mjs +5 -7
- package/server/migrations/0004-steer-settle-markers.mjs +5 -7
- package/server/migrations/0005-remote-relays.mjs +5 -7
- package/server/migrations/0006-workspace-root-settings.mjs +5 -7
- package/server/migrations/0007-claude-cloud.mjs +5 -7
- package/server/migrations/0008-message-pins.mjs +5 -7
- package/server/migrations/0009-subagent-timeline.mjs +5 -7
- package/server/migrations/0010-subagent-timeline-pruning.mjs +5 -7
- package/server/migrations/0011-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0013-previews.mjs +5 -7
- package/server/migrations/0014-conversation-groups.mjs +5 -7
- package/server/migrations/0015-conversation-group-position.mjs +5 -7
- package/server/migrations/0016-share-access-log.mjs +5 -7
- package/server/migrations/0017-conversation-history-indexes.mjs +5 -7
- package/server/migrations/0018-background-task-rows.mjs +5 -7
- package/server/migrations/0019-relay-event-subscription-options.mjs +5 -7
- package/server/migrations/0020-conversation-may-create-sessions.mjs +5 -7
- package/server/migrations/0021-conversation-group-agent-marks.mjs +5 -7
- package/server/migrations/0022-relay-schedules.mjs +5 -7
- package/server/migrations/0023-relay-asks.mjs +5 -7
- package/server/migrations/0024-deepseek-usage-totals.mjs +5 -7
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +5 -7
- package/server/migrations/0026-deepseek-usage-served-models.mjs +5 -7
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +5 -7
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +5 -7
- package/server/migrations/0029-deepseek-balance-rises.mjs +5 -7
- package/server/migrations/0030-deepseek-usage-actuals.mjs +5 -7
- package/server/migrations/0031-queue-provider.mjs +5 -7
- package/server/migrations/0032-runtime-session-billing-switch.mjs +5 -7
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +5 -7
- package/server/migrations/0034-claude-api-usage-totals.mjs +5 -7
- package/server/migrations/0035-conversation-plan-only.mjs +87 -0
- package/server/migrations/0036-token-usage.mjs +267 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +106 -3
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/api-client.js +6 -3
- package/server/public/app/ask-user-view.js +20 -6
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +611 -70
- package/server/public/app/chat-actions-menu-state.mjs +139 -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-control-state.mjs +12 -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 +16 -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 +20 -2
- 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 +234 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +50 -2
- package/server/public/app/inbox-view.mjs +141 -5
- package/server/public/app/journal-view.js +311 -58
- 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/plan-board-shared.mjs +49 -0
- package/server/public/app/plan-usage-view.mjs +1 -1
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/relay-board-inline.mjs +4 -1
- package/server/public/app/relay-board-view.js +38 -10
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +42 -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/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +250 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +61 -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 +15 -4
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/subagent-viewer.mjs +27 -2
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -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 +2258 -1585
- package/server/public/sw.js +8 -0
- package/server/relay-heap-guard.mjs +14 -1
- package/server/relay-self-restart.mjs +83 -2
- package/server/relay-tools.md +5 -3
- package/server/repositories/message-repository.mjs +54 -1
- package/server/repositories/remote-relay-repository.mjs +10 -0
- package/server/repositories/session-repository.mjs +25 -1
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +144 -1
- package/server/routes/relay-ask-routes.mjs +138 -8
- package/server/routes/relay-board-routes.mjs +55 -40
- package/server/routes/sessions-routes.mjs +144 -14
- package/server/server-runtime.mjs +322 -51
- package/server/services/about-service.mjs +81 -53
- package/server/services/cache-rebuild-service.mjs +7 -0
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/conversation-preferences-service.mjs +31 -0
- package/server/services/database-file.mjs +413 -0
- package/server/services/db-backup-service.mjs +809 -0
- package/server/services/deepseek-usage-import.mjs +6 -9
- package/server/services/delete-archive-service.mjs +3 -0
- package/server/services/earlier-database-service.mjs +69 -126
- package/server/services/oar-cli-helpers.mjs +214 -12
- package/server/services/oar-cli-menu.mjs +1 -0
- package/server/services/oar-state-migration-service.mjs +33 -13
- package/server/services/package-data-rescue.mjs +4 -2
- package/server/services/plan-usage-claude-api.mjs +14 -15
- package/server/services/plan-usage-contract.mjs +6 -2
- package/server/services/plan-usage-deepseek.mjs +1 -1
- package/server/services/plan-usage-tokens.mjs +248 -0
- package/server/services/push-dispatch-service.mjs +134 -9
- package/server/services/relay-cli-launcher-service.mjs +3 -0
- package/server/services/relay-event-service.mjs +30 -0
- package/server/services/relay-question-context.mjs +11 -0
- package/server/services/relay-restart-picture.mjs +67 -22
- package/server/services/remote-inbox-watch-service.mjs +204 -0
- package/server/services/remote-relay-caller-context.mjs +80 -7
- package/server/services/remote-relay-dispatcher.mjs +101 -19
- package/server/services/restart-resume-service.mjs +5 -2
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-launch-service.mjs +12 -3
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-stop-service.mjs +27 -6
- package/server/services/session-worker-websocket-service.mjs +40 -16
- package/server/services/token-usage-service.mjs +430 -0
- package/server/services/update-install-service.mjs +13 -0
- package/server/services/usage-limit-pause-service.mjs +8 -1
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/server/services/worker-scope.mjs +77 -0
- package/shared/build-id.mjs +128 -0
- package/shared/plan-board-actions.mjs +130 -0
- package/shared/plan-only.mjs +43 -0
- package/shared/relay-asks.mjs +3 -0
- package/shared/relay-events.mjs +38 -4
- package/shared/remote-relay-contract.mjs +66 -11
- package/shared/turn-token-usage.mjs +144 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +23 -0
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -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 { resolveBuildId } from '../../shared/build-id.mjs';
|
|
23
24
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
24
25
|
import {
|
|
25
26
|
createRunnerLinkBridge,
|
|
@@ -52,6 +53,10 @@ async function main() {
|
|
|
52
53
|
|
|
53
54
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
54
55
|
|| path.resolve(__dirname, '..', 'config.json');
|
|
56
|
+
// The code this process started on, read once from the repo root and kept
|
|
57
|
+
// for its whole life: the relay recycles a worker whose build differs from
|
|
58
|
+
// its own, and a tree that moves on under us must not change our answer.
|
|
59
|
+
const buildId = resolveBuildId(path.resolve(__dirname, '..', '..')).id;
|
|
55
60
|
// Two reads of the same small file. Collapsing them to one would mean
|
|
56
61
|
// re-implementing `resolveRelayServerUrl`'s env override, host
|
|
57
62
|
// normalisation and port defaulting inside this worker — a live contract
|
|
@@ -129,6 +134,7 @@ async function main() {
|
|
|
129
134
|
let heartbeatTimer = null;
|
|
130
135
|
heartbeat = createHeartbeatController({
|
|
131
136
|
api,
|
|
137
|
+
buildId,
|
|
132
138
|
pollMs: HEARTBEAT_MS,
|
|
133
139
|
getSessionReady: () => true,
|
|
134
140
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
@@ -158,6 +164,7 @@ async function main() {
|
|
|
158
164
|
serverUrl,
|
|
159
165
|
token,
|
|
160
166
|
dbg,
|
|
167
|
+
buildId,
|
|
161
168
|
getSessionReady: () => true,
|
|
162
169
|
getSessionId: () => sdkSessionId,
|
|
163
170
|
getPid: () => process.pid,
|
|
@@ -238,7 +245,7 @@ async function main() {
|
|
|
238
245
|
getActiveQueueMessageIds: () => turnRunner.getActiveQueueMessageIds(),
|
|
239
246
|
});
|
|
240
247
|
|
|
241
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
248
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
242
249
|
heartbeat.startHeartbeat();
|
|
243
250
|
wsLink.start();
|
|
244
251
|
}
|
|
@@ -13,10 +13,12 @@ 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 { 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';
|
|
18
19
|
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
19
20
|
import { createCursorTurnRunner } from './cursor-turn-runner.mjs';
|
|
21
|
+
import { buildCursorSteeringState } from './cursor-worker-steering.mjs';
|
|
20
22
|
import { resolveCursorSessionStoreDir } from '../services/cursor-session-root-service.mjs';
|
|
21
23
|
|
|
22
24
|
// Windows console launch: the launcher names the worker log, this copies
|
|
@@ -48,6 +50,10 @@ async function main() {
|
|
|
48
50
|
|
|
49
51
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
50
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;
|
|
51
57
|
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
52
58
|
const token = loadTokenFromConfig(configPath);
|
|
53
59
|
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
@@ -93,17 +99,22 @@ async function main() {
|
|
|
93
99
|
let heartbeatTimer = null;
|
|
94
100
|
const heartbeat = createHeartbeatController({
|
|
95
101
|
api,
|
|
102
|
+
buildId,
|
|
96
103
|
pollMs: HEARTBEAT_MS,
|
|
97
104
|
getSessionReady: () => true,
|
|
98
105
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
99
106
|
setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
|
|
100
107
|
getActiveQueueMessageId: () => turnRunner.getActiveQueueMessageId(),
|
|
108
|
+
// No steering here; the snapshot tells the relay when it may replace
|
|
109
|
+
// this worker: between turns (the agent handle is rebuilt on demand).
|
|
110
|
+
getSteeringState: () => buildCursorSteeringState(turnRunner),
|
|
101
111
|
});
|
|
102
112
|
|
|
103
113
|
const wsLink = createWorkerWebSocketLink({
|
|
104
114
|
serverUrl,
|
|
105
115
|
token,
|
|
106
116
|
dbg,
|
|
117
|
+
buildId,
|
|
107
118
|
getSessionReady: () => true,
|
|
108
119
|
getSessionId: () => sdkSessionId,
|
|
109
120
|
getPid: () => process.pid,
|
|
@@ -146,7 +157,7 @@ async function main() {
|
|
|
146
157
|
getActiveQueueMessageIds: () => [turnRunner.getActiveQueueAttempt()],
|
|
147
158
|
});
|
|
148
159
|
|
|
149
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
160
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
150
161
|
heartbeat.startHeartbeat();
|
|
151
162
|
wsLink.start();
|
|
152
163
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { buildCursorUserMessage } from './cursor-attachments.mjs';
|
|
2
3
|
import { buildCursorContextUsage } from './cursor-context-usage.mjs';
|
|
3
4
|
import { createSdkMessageNormalizer } from './sdk-message-normalizer.mjs';
|
|
@@ -23,6 +24,7 @@ import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instr
|
|
|
23
24
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
24
25
|
import { resolveFallbackContextLimitTokens } from '../../shared/context-window-fallbacks.mjs';
|
|
25
26
|
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
27
|
+
import { PLAN_BOARD_ACTIONS } from '../../shared/plan-board-actions.mjs';
|
|
26
28
|
import { explainLaunchFailure, missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
27
29
|
|
|
28
30
|
// Stale-auth recovery budget per turn: expiry is routine on long-lived
|
|
@@ -34,12 +36,6 @@ const MAX_STALE_AUTH_RETRIES = 2;
|
|
|
34
36
|
// exchanged auth goes stale around the 4h mark while the API key stays valid.
|
|
35
37
|
const STALE_HANDLE_IDLE_MS = 30 * 60_000;
|
|
36
38
|
|
|
37
|
-
const PLAN_BOARD_ACTIONS = [
|
|
38
|
-
{ id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
|
|
39
|
-
{ id: 'interactive', label: 'Stop here and prompt myself', mode: 'agent' },
|
|
40
|
-
{ id: 'exit_only', label: 'Stop here', mode: 'agent' },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
39
|
// The Cursor SDK's SendOptions carries only model/mode/callbacks — there is
|
|
44
40
|
// no per-turn instruction channel — so ask/autopilot steering rides on the
|
|
45
41
|
// user message text, mirroring the Copilot extension's prompt-context
|
|
@@ -447,6 +443,45 @@ export function createCursorTurnRunner({
|
|
|
447
443
|
});
|
|
448
444
|
}
|
|
449
445
|
|
|
446
|
+
// A process without the row's attempt id names its run once (POST
|
|
447
|
+
// /api/turn-token-usage): a turn requeued onto a restarted worker is a
|
|
448
|
+
// second run of it.
|
|
449
|
+
const tokenRunId = randomUUID();
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Ship the turn's tokens to the token booking: the SDK's usage
|
|
453
|
+
* for the turn, all its model calls together, under the turn's model.
|
|
454
|
+
* Cursor counts fresh input apart from the cache (cursor-context-usage.mjs
|
|
455
|
+
* adds the three up). No cost: the SDK prices no single turn. A field the
|
|
456
|
+
* SDK left out stays out, and the relay keeps it unknown. Fire-and-forget:
|
|
457
|
+
* it never throws and the turn does not wait on it.
|
|
458
|
+
*/
|
|
459
|
+
async function publishTurnTokens(message, state, model, normalizer) {
|
|
460
|
+
try {
|
|
461
|
+
const usage = state.lastUsage || normalizer?.lastUsage || null;
|
|
462
|
+
const turnModel = state.responseModel || model || '';
|
|
463
|
+
if (!usage || typeof usage !== 'object' || !turnModel || !message?.id) return;
|
|
464
|
+
await api('POST', '/api/turn-token-usage', {
|
|
465
|
+
conversationId: message.conversationId,
|
|
466
|
+
turnId: String(message.id),
|
|
467
|
+
runId: message.attemptId || tokenRunId,
|
|
468
|
+
provider: 'cursor',
|
|
469
|
+
...(agentHandle?.agentId ? { nativeSessionId: agentHandle.agentId } : {}),
|
|
470
|
+
at: new Date().toISOString(),
|
|
471
|
+
models: [{
|
|
472
|
+
model: turnModel,
|
|
473
|
+
inputTokens: usage.inputTokens,
|
|
474
|
+
outputTokens: usage.outputTokens,
|
|
475
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
476
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
477
|
+
reasoningTokens: usage.reasoningTokens,
|
|
478
|
+
}],
|
|
479
|
+
});
|
|
480
|
+
} catch (error) {
|
|
481
|
+
dbg('turn token publish failed', error?.message || String(error));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
450
485
|
/**
|
|
451
486
|
* Report the agent's cumulative billed usage so the relay can book this
|
|
452
487
|
* turn's increase against the configured monthly allowance. The model is
|
|
@@ -704,6 +739,7 @@ export function createCursorTurnRunner({
|
|
|
704
739
|
// (the auth retry swaps it), so a discarded attempt's usage is not
|
|
705
740
|
// republished over the retry's.
|
|
706
741
|
await publishContextUsage(message, state, model, normalizer);
|
|
742
|
+
void publishTurnTokens(message, state, model, normalizer);
|
|
707
743
|
await publishPlanUsage(message, state, model);
|
|
708
744
|
}
|
|
709
745
|
|
|
@@ -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
|
@@ -40,6 +40,8 @@ import { migrateQueueProvider } from './migrations/0031-queue-provider.mjs';
|
|
|
40
40
|
import { migrateRuntimeSessionBillingSwitch } from './migrations/0032-runtime-session-billing-switch.mjs';
|
|
41
41
|
import { migrateClaudeApiUsageCheckpoints } from './migrations/0033-claude-api-usage-checkpoints.mjs';
|
|
42
42
|
import { migrateClaudeApiUsageTotals } from './migrations/0034-claude-api-usage-totals.mjs';
|
|
43
|
+
import { migrateConversationPlanOnly } from './migrations/0035-conversation-plan-only.mjs';
|
|
44
|
+
import { migrateTokenUsage } from './migrations/0036-token-usage.mjs';
|
|
43
45
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
44
46
|
|
|
45
47
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -1059,6 +1061,22 @@ try {
|
|
|
1059
1061
|
console.warn(`[claude-api-usage] migration 0034 failed; retrying on next boot: ${error?.message || error}`);
|
|
1060
1062
|
}
|
|
1061
1063
|
|
|
1064
|
+
// A plan-only conversation, handled like 0020.
|
|
1065
|
+
try {
|
|
1066
|
+
migrateConversationPlanOnly(db);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
console.warn(`[plan-only] migration 0035 failed; retrying on next boot: ${error?.message || error}`);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Tokens per turn and per day for every provider, with a one-time
|
|
1072
|
+
// copy of the Claude API and DeepSeek days. After 0025 and 0034: a missing
|
|
1073
|
+
// source throws here, so it is logged and retried next boot like 0034.
|
|
1074
|
+
try {
|
|
1075
|
+
migrateTokenUsage(db);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
console.warn(`[token-usage] migration 0036 failed; retrying on next boot: ${error?.message || error}`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1062
1080
|
const conversationColumns = db.prepare(`PRAGMA table_info(conversations)`).all().map((c) => c.name);
|
|
1063
1081
|
if (!conversationColumns.includes('archived')) {
|
|
1064
1082
|
db.exec(`ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0`);
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
// (api-docs.deepseek.com, quick_start/agent_integrations/claude_code).
|
|
36
36
|
|
|
37
37
|
import { deepseekAnthropicBaseUrl } from '../../shared/deepseek-api.mjs';
|
|
38
|
+
import { estimateCost } from '../../shared/deepseek-pricing.mjs';
|
|
38
39
|
import { isHostClaudeSessionVariable } from '../../shared/host-claude-session-env.mjs';
|
|
39
40
|
import { CLAUDE_CLI_KEY_CHAT_SETTINGS, claudeCliPinnedEnv } from '../claude-worker/claude-cli-pinned-env.mjs';
|
|
40
41
|
import {
|
|
@@ -262,6 +263,19 @@ export function createDeepSeekClaudeBackend({ env = process.env } = {}) {
|
|
|
262
263
|
// relay's model ids (relayModel); the relay books what each report adds.
|
|
263
264
|
// Claude Code's `total_cost_usd` is at Claude's prices and is not sent.
|
|
264
265
|
usageRoute: '/api/deepseek-plan-usage',
|
|
266
|
+
// A turn's tokens (POST /api/turn-token-usage) are priced at DeepSeek's
|
|
267
|
+
// own rates for the time, as the card's estimate is: cache writes and
|
|
268
|
+
// fresh input are both misses. Null for a model the table has no price for.
|
|
269
|
+
turnTokenCost: (entry, at) => {
|
|
270
|
+
const costUsd = estimateCost({
|
|
271
|
+
model: entry.model,
|
|
272
|
+
at,
|
|
273
|
+
hitTokens: entry.cacheReadTokens,
|
|
274
|
+
missTokens: entry.inputTokens + entry.cacheWriteTokens,
|
|
275
|
+
outputTokens: entry.outputTokens,
|
|
276
|
+
});
|
|
277
|
+
return { costUsd, costSource: costUsd === null ? null : 'oar-deepseek-table' };
|
|
278
|
+
},
|
|
265
279
|
|
|
266
280
|
// A Claude id is not a model of DeepSeek's to request, only one it may
|
|
267
281
|
// have served (relayModel).
|
|
@@ -235,6 +235,15 @@ 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. */
|
|
@@ -12,10 +12,12 @@ 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 { resolveBuildId } from '../../shared/build-id.mjs';
|
|
15
16
|
import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
|
|
16
17
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
17
18
|
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
18
19
|
import { createGrokTurnRunner } from './grok-turn-runner.mjs';
|
|
20
|
+
import { buildGrokSteeringState } from './grok-worker-steering.mjs';
|
|
19
21
|
import { createGrokTaskPublisher } from './grok-background-tasks.mjs';
|
|
20
22
|
|
|
21
23
|
// Windows console launch: the launcher names the worker log, this copies
|
|
@@ -47,6 +49,10 @@ async function main() {
|
|
|
47
49
|
|
|
48
50
|
const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
|
|
49
51
|
|| path.resolve(__dirname, '..', 'config.json');
|
|
52
|
+
// The code this process started on, read once from the repo root and kept
|
|
53
|
+
// for its whole life: the relay recycles a worker whose build differs from
|
|
54
|
+
// its own, and a tree that moves on under us must not change our answer.
|
|
55
|
+
const buildId = resolveBuildId(path.resolve(__dirname, '..', '..')).id;
|
|
50
56
|
const serverUrl = resolveRelayServerUrl({ configPath });
|
|
51
57
|
const token = loadTokenFromConfig(configPath);
|
|
52
58
|
const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
|
|
@@ -96,6 +102,7 @@ async function main() {
|
|
|
96
102
|
let heartbeatTimer = null;
|
|
97
103
|
heartbeat = createHeartbeatController({
|
|
98
104
|
api,
|
|
105
|
+
buildId,
|
|
99
106
|
pollMs: HEARTBEAT_MS,
|
|
100
107
|
getSessionReady: () => true,
|
|
101
108
|
getHeartbeatTimer: () => heartbeatTimer,
|
|
@@ -106,12 +113,16 @@ async function main() {
|
|
|
106
113
|
getBackgroundTaskIds: () => turnRunner.backgroundTaskIds(),
|
|
107
114
|
onResyncBackgroundTasks: () => turnRunner.resyncBackgroundTasks(),
|
|
108
115
|
onAnswered: () => turnRunner.flushKeptTaskSettles(),
|
|
116
|
+
// No steering here; the snapshot tells the relay when it may replace
|
|
117
|
+
// this worker: no turn, and no open terminal (they end with the worker).
|
|
118
|
+
getSteeringState: () => buildGrokSteeringState(turnRunner),
|
|
109
119
|
});
|
|
110
120
|
|
|
111
121
|
const wsLink = createWorkerWebSocketLink({
|
|
112
122
|
serverUrl,
|
|
113
123
|
token,
|
|
114
124
|
dbg,
|
|
125
|
+
buildId,
|
|
115
126
|
getSessionReady: () => true,
|
|
116
127
|
// A relay that just (re)started learns of the terminals from the digest
|
|
117
128
|
// on the next beat; ask for it now.
|
|
@@ -171,7 +182,7 @@ async function main() {
|
|
|
171
182
|
getActiveQueueMessageIds: () => [turnRunner.getActiveQueueAttempt()],
|
|
172
183
|
});
|
|
173
184
|
|
|
174
|
-
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
|
|
185
|
+
dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'} build=${buildId || 'unknown'}`);
|
|
175
186
|
heartbeat.startHeartbeat();
|
|
176
187
|
wsLink.start();
|
|
177
188
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import {
|
|
2
3
|
createGrokAgentHandle,
|
|
3
4
|
startGrokTurn,
|
|
@@ -11,15 +12,10 @@ import { createPreviewInstructionsProvider } from '../../shared/preview-instruct
|
|
|
11
12
|
import { fetchRemoteRelayInflight } from '../../shared/remote-relay-tool-core.mjs';
|
|
12
13
|
import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
|
|
13
14
|
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
15
|
+
import { PLAN_BOARD_ACTIONS } from '../../shared/plan-board-actions.mjs';
|
|
14
16
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
15
17
|
import { explainLaunchFailure, missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
16
18
|
|
|
17
|
-
const PLAN_BOARD_ACTIONS = [
|
|
18
|
-
{ id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
|
|
19
|
-
{ id: 'interactive', label: 'Stop here and prompt myself', mode: 'agent' },
|
|
20
|
-
{ id: 'exit_only', label: 'Stop here', mode: 'agent' },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
19
|
// Grok has no exit-plan tool, so the only board source is the plan-mode
|
|
24
20
|
// text-shape fallback; the payload matches the Claude/Cursor workers'.
|
|
25
21
|
export function buildGrokPlanReadyBoardPayload({
|
|
@@ -352,6 +348,47 @@ export function createGrokTurnRunner({
|
|
|
352
348
|
});
|
|
353
349
|
}
|
|
354
350
|
|
|
351
|
+
// A process without the row's attempt id names its run once (POST
|
|
352
|
+
// /api/turn-token-usage): a turn requeued onto a restarted worker is a
|
|
353
|
+
// second run of it.
|
|
354
|
+
const tokenRunId = randomUUID();
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Ship the turn's tokens to the token booking: per model where
|
|
358
|
+
* the agent breaks them down, else the turn's totals under its model, with
|
|
359
|
+
* the agent's own cost. Grok counts fresh input apart from the cache
|
|
360
|
+
* (grok-context-usage.mjs adds the three up). Advisory, fire-and-forget.
|
|
361
|
+
*/
|
|
362
|
+
async function publishTurnTokens(message, state, model) {
|
|
363
|
+
const usage = state.turnUsage;
|
|
364
|
+
if (!usage || !message?.id) return;
|
|
365
|
+
const entry = (source, entryModel) => ({
|
|
366
|
+
model: entryModel,
|
|
367
|
+
inputTokens: source.inputTokens,
|
|
368
|
+
outputTokens: source.outputTokens,
|
|
369
|
+
cacheWriteTokens: source.cacheCreationTokens,
|
|
370
|
+
cacheReadTokens: source.cachedReadTokens,
|
|
371
|
+
reasoningTokens: source.reasoningTokens,
|
|
372
|
+
...(source.costUsd !== null && source.costUsd !== undefined ? { costUsd: source.costUsd, costSource: 'provider' } : {}),
|
|
373
|
+
});
|
|
374
|
+
const turnModel = state.responseModel || usage.modelId || model || '';
|
|
375
|
+
const models = Array.isArray(usage.modelUsage) && usage.modelUsage.length
|
|
376
|
+
? usage.modelUsage.map((item) => entry(item, item.model))
|
|
377
|
+
: (turnModel ? [entry(usage, turnModel)] : []);
|
|
378
|
+
if (!models.length) return;
|
|
379
|
+
await api('POST', '/api/turn-token-usage', {
|
|
380
|
+
conversationId: message.conversationId,
|
|
381
|
+
turnId: String(message.id),
|
|
382
|
+
runId: message.attemptId || tokenRunId,
|
|
383
|
+
provider: 'grok',
|
|
384
|
+
...(sdkSessionId ? { nativeSessionId: sdkSessionId } : {}),
|
|
385
|
+
at: usage.capturedAt || new Date().toISOString(),
|
|
386
|
+
models,
|
|
387
|
+
}).catch((error) => {
|
|
388
|
+
dbg('turn token publish failed', error?.message || String(error));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
355
392
|
/**
|
|
356
393
|
* Ship per-turn token/cost totals for the Check Usage Grok card. Advisory:
|
|
357
394
|
* failures never disturb the turn reply.
|
|
@@ -521,6 +558,7 @@ export function createGrokTurnRunner({
|
|
|
521
558
|
// awaits made it false.
|
|
522
559
|
void publishContextUsage(message, state, model).catch(() => {});
|
|
523
560
|
void publishPlanUsage(message, state).catch(() => {});
|
|
561
|
+
void publishTurnTokens(message, state, model).catch(() => {});
|
|
524
562
|
}
|
|
525
563
|
|
|
526
564
|
if (aborted) {
|
|
@@ -629,6 +667,9 @@ export function createGrokTurnRunner({
|
|
|
629
667
|
// The terminals as background tasks: the heartbeat's digest and resync,
|
|
630
668
|
// the panel's Stop, kept settle notices.
|
|
631
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,
|
|
632
673
|
resyncBackgroundTasks: () => terminalTasks?.resync(),
|
|
633
674
|
stopBackgroundTask: (taskId) => terminalTasks?.stop(taskId) || false,
|
|
634
675
|
flushKeptTaskSettles: () => terminalTasks?.flushKeptSettles(),
|
|
@@ -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
|
+
}
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* Safe to run repeatedly (idempotent) and against a live database — ADD COLUMN
|
|
13
13
|
* only takes a brief write lock and new columns default to NULL.
|
|
14
14
|
*
|
|
15
|
-
* Usage: node server/migrations/0001-add-structured-answer.mjs [path/to/
|
|
15
|
+
* Usage: node server/migrations/0001-add-structured-answer.mjs [path/to/oar.db]
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import process from 'process';
|
|
21
21
|
import Database from 'better-sqlite3';
|
|
22
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
22
23
|
|
|
23
24
|
const NEW_COLUMNS = [
|
|
24
25
|
{ name: 'structured_answer', ddl: 'ALTER TABLE relay_questions ADD COLUMN structured_answer TEXT' },
|
|
@@ -56,15 +57,12 @@ export function migrate(dbPath) {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
function defaultDbPath() {
|
|
60
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
61
|
-
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
65
61
|
if (isMain) {
|
|
66
|
-
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
67
62
|
try {
|
|
63
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
64
|
+
// name before 0.9.20); never a new one.
|
|
65
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
68
66
|
const result = migrate(dbPath);
|
|
69
67
|
const applied = result.applied.length ? result.applied.join(', ') : '(none)';
|
|
70
68
|
const skipped = result.skipped.length ? result.skipped.join(', ') : '(none)';
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Idempotent: re-running once path_key exists is a no-op.
|
|
20
20
|
*
|
|
21
|
-
* Usage: node server/migrations/0002-recent-workspace-roots-path-key.mjs [path/to/
|
|
21
|
+
* Usage: node server/migrations/0002-recent-workspace-roots-path-key.mjs [path/to/oar.db]
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
@@ -27,6 +27,7 @@ import process from 'process';
|
|
|
27
27
|
import Database from 'better-sqlite3';
|
|
28
28
|
|
|
29
29
|
import { normalizeWorkspaceRootKey } from '../services/workspace-root-path-policy.mjs';
|
|
30
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Rebuild against an already-open database handle.
|
|
@@ -97,15 +98,12 @@ export function migrate(dbPath, options = {}) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function defaultDbPath() {
|
|
101
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
102
|
-
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
106
102
|
if (isMain) {
|
|
107
|
-
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
108
103
|
try {
|
|
104
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
105
|
+
// name before 0.9.20); never a new one.
|
|
106
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
109
107
|
const result = migrate(dbPath);
|
|
110
108
|
console.log(`Migration 0002 on ${dbPath}`);
|
|
111
109
|
if (result.applied) {
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* databases; this module exists so existing databases upgrade at startup and
|
|
15
15
|
* so the migration can be run offline against a db file.
|
|
16
16
|
*
|
|
17
|
-
* Usage: node server/migrations/0003-push-subscriptions.mjs [path/to/
|
|
17
|
+
* Usage: node server/migrations/0003-push-subscriptions.mjs [path/to/oar.db]
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import process from 'process';
|
|
23
23
|
import Database from 'better-sqlite3';
|
|
24
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
24
25
|
|
|
25
26
|
export const PUSH_SUBSCRIPTIONS_SCHEMA = `
|
|
26
27
|
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
@@ -67,15 +68,12 @@ export function migrate(dbPath) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function defaultDbPath() {
|
|
71
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
72
|
-
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
71
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
76
72
|
if (isMain) {
|
|
77
|
-
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
78
73
|
try {
|
|
74
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
75
|
+
// name before 0.9.20); never a new one.
|
|
76
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
79
77
|
const result = migrate(dbPath);
|
|
80
78
|
console.log(`Migration 0003 on ${dbPath}`);
|
|
81
79
|
console.log(result.applied ? ' created push_subscriptions' : ` skipped: ${result.reason}`);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Idempotent: the column is added once, the backfill only fills NULLs, and the
|
|
21
21
|
* kind rewrite matches nothing once applied. Safe to run on every boot.
|
|
22
22
|
*
|
|
23
|
-
* Usage: node server/migrations/0004-steer-settle-markers.mjs [path/to/
|
|
23
|
+
* Usage: node server/migrations/0004-steer-settle-markers.mjs [path/to/oar.db]
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { fileURLToPath } from 'url';
|
|
@@ -29,6 +29,7 @@ import process from 'process';
|
|
|
29
29
|
import Database from 'better-sqlite3';
|
|
30
30
|
|
|
31
31
|
import { STEER_FOLDED_TEXT } from '../../shared/steer-settle-markers.mjs';
|
|
32
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
32
33
|
|
|
33
34
|
function tableExists(db, name) {
|
|
34
35
|
return !!db
|
|
@@ -97,15 +98,12 @@ export function migrate(dbPath) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function defaultDbPath() {
|
|
101
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
102
|
-
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
106
102
|
if (isMain) {
|
|
107
|
-
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
108
103
|
try {
|
|
104
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
105
|
+
// name before 0.9.20); never a new one.
|
|
106
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
109
107
|
const result = migrate(dbPath);
|
|
110
108
|
console.log(`Migration 0004 on ${dbPath}`);
|
|
111
109
|
console.log(result.applied
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Additive and idempotent. Safe to run on every boot.
|
|
17
17
|
*
|
|
18
|
-
* Usage: node server/migrations/0005-remote-relays.mjs [path/to/
|
|
18
|
+
* Usage: node server/migrations/0005-remote-relays.mjs [path/to/oar.db]
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
import path from 'path';
|
|
23
23
|
import process from 'process';
|
|
24
24
|
import Database from 'better-sqlite3';
|
|
25
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
25
26
|
|
|
26
27
|
function tableExists(db, name) {
|
|
27
28
|
return !!db
|
|
@@ -68,15 +69,12 @@ export function migrate(dbPath) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function defaultDbPath() {
|
|
72
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
73
|
-
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
72
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
77
73
|
if (isMain) {
|
|
78
|
-
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
79
74
|
try {
|
|
75
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
76
|
+
// name before 0.9.20); never a new one.
|
|
77
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
80
78
|
const result = migrate(dbPath);
|
|
81
79
|
console.log(`Migration 0005 on ${dbPath}`);
|
|
82
80
|
console.log(result.applied
|