@oar-sh/oar 0.9.12 → 0.9.14
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/extension.mjs +9 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +15 -1
- package/.github/extensions/web-relay/skills/question-bridge.mjs +4 -1
- package/CHANGELOG.md +508 -0
- package/README.md +12 -12
- package/bin/oar.js +155 -94
- package/package.json +5 -5
- package/server/README.md +479 -157
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
- package/server/claude-worker/claude-session-process.mjs +92 -15
- package/server/claude-worker/claude-turn-publisher.mjs +20 -1
- package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
- package/server/claude-worker/task-output-discovery.mjs +28 -2
- package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-prompt-context.mjs +3 -1
- package/server/copilot-worker/copilot-question-bridge.mjs +11 -6
- package/server/copilot-worker/copilot-sdk-adapter.mjs +11 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +24 -1
- package/server/cursor-worker/cursor-ask-user-tool.mjs +1 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +22 -7
- package/server/cursor-worker/cursor-session-worker.mjs +9 -2
- package/server/cursor-worker/cursor-turn-runner.mjs +18 -3
- package/server/db-schema.mjs +10 -0
- package/server/grok-worker/acp-host-services.mjs +6 -3
- package/server/grok-worker/grok-background-tasks.mjs +7 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +13 -3
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +60 -4
- package/server/public/app/ask-user-view.js +29 -4
- package/server/public/app/background-tasks-view.mjs +56 -7
- package/server/public/app/bootstrap.js +7 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-provider-indicator.mjs +18 -8
- package/server/public/app/conversation-view.js +33 -9
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +9 -3
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/question-answer-rule.mjs +15 -0
- package/server/public/app/question-multi-select.mjs +9 -2
- package/server/public/app/relay-event-view.mjs +5 -1
- package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +52 -0
- package/server/public/app/socket-handlers.js +1 -1
- package/server/public/app/task-console-render.mjs +171 -7
- package/server/public/app/task-console.mjs +111 -11
- package/server/public/app/task-output-text.mjs +15 -6
- package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
- package/server/public/app/transcript-jump-bottom.mjs +20 -0
- package/server/public/index.html +76 -5
- package/server/relay-exit-codes.mjs +6 -0
- package/server/relay-self-restart.mjs +6 -1
- package/server/relay-tools.md +17 -9
- package/server/repositories/message-repository.mjs +32 -28
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/repositories/subagent-timeline-statements.mjs +2 -0
- package/server/routes/ask-user-routes.mjs +41 -7
- package/server/routes/messages-routes.mjs +220 -37
- package/server/routes/preview-routes.mjs +73 -7
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +79 -5
- package/server/server-runtime.mjs +335 -218
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +240 -13
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/cursor-session-root-service.mjs +70 -6
- package/server/services/data-dir-resolution.mjs +208 -19
- package/server/services/earlier-database-service.mjs +631 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/oar-cli-helpers.mjs +6 -1
- package/server/services/oar-state-layout.mjs +107 -0
- package/server/services/oar-state-migration-service.mjs +387 -96
- package/server/services/package-data-rescue-preinstall.mjs +22 -0
- package/server/services/package-data-rescue.mjs +137 -0
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/preview-registry-service.mjs +3 -2
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +42 -8
- package/server/services/relay-event-service.mjs +210 -14
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +387 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/relay-singleton-guard.mjs +300 -36
- package/server/services/remote-relay-caller-context.mjs +7 -2
- package/server/services/remote-relay-dispatcher.mjs +217 -28
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +52 -15
- package/server/services/session-worker-launch-service.mjs +31 -1
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/stale-turn-recovery.mjs +99 -0
- package/server/services/subagent-timeline-service.mjs +4 -1
- package/server/services/task-output-service.mjs +217 -18
- package/server/services/windows-autostart-service.mjs +329 -31
- package/server/services/windows-boot-autostart-service.mjs +188 -60
- package/shared/preview-instructions.mjs +17 -12
- package/shared/preview-tool-core.mjs +32 -14
- package/shared/question-answer-rule.mjs +63 -0
- package/shared/relay-events.mjs +106 -3
- package/shared/remote-relay-contract.mjs +32 -15
- package/shared/subagent-timeline.mjs +4 -0
- package/shared/task-output-text.mjs +196 -83
- package/shared/windows-system-tools.mjs +25 -0
- package/shared/worker-runtime/config-loader.mjs +54 -8
- package/shared/worker-runtime/prompt-context.mjs +3 -1
- package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
- package/shared/worker-runtime/working-directory.mjs +65 -0
- package/server/uploads/image_input.md +0 -540
|
@@ -80,7 +80,9 @@ export function createCopilotPromptContextBuilder({
|
|
|
80
80
|
if (includeInstructions) {
|
|
81
81
|
// Advisory: a preview lookup that fails costs the block, never the turn.
|
|
82
82
|
const previewBlock = typeof getPreviewInstructions === 'function'
|
|
83
|
-
? await Promise.resolve()
|
|
83
|
+
? await Promise.resolve()
|
|
84
|
+
.then(() => getPreviewInstructions({ conversationId: String(message?.conversationId || '') }))
|
|
85
|
+
.catch(() => '')
|
|
84
86
|
: '';
|
|
85
87
|
instructions = applyMediaEmbedInstructions(applyPreviewInstructions(toolInstructions, previewBlock));
|
|
86
88
|
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { createAskUserBridge } from '../../shared/ask-user-bridge.mjs';
|
|
26
26
|
import { createRelayQuestion } from '../../shared/question-wait.mjs';
|
|
27
27
|
import { looksLikeMultiSelectQuestion } from '../../shared/question-multi-select.mjs';
|
|
28
|
+
import { readApprovalAnswer } from '../../shared/question-answer-rule.mjs';
|
|
28
29
|
import {
|
|
29
30
|
DEFAULT_QUESTION_TIMEOUT_MS,
|
|
30
31
|
QUESTION_TIMEOUT_CONTINUATION_TEXT,
|
|
@@ -262,9 +263,10 @@ export function createCopilotQuestionBridge({
|
|
|
262
263
|
/**
|
|
263
264
|
* `onPermissionRequest` in ask mode → an approve/deny card.
|
|
264
265
|
*
|
|
265
|
-
* Freeform is allowed and anything
|
|
266
|
-
* so a human can type *why* they are
|
|
267
|
-
* feedback instead of a bare refusal.
|
|
266
|
+
* Freeform is allowed and anything but the approve choice (by the shared
|
|
267
|
+
* rule: trimmed, any case) denies — so a human can type *why* they are
|
|
268
|
+
* refusing and the model receives it as feedback instead of a bare refusal.
|
|
269
|
+
* The card's rationale says so, since a typed "yes" refuses too.
|
|
268
270
|
*/
|
|
269
271
|
async function askToolApproval(request, { signal, message } = {}) {
|
|
270
272
|
const description = describePermissionRequest(request);
|
|
@@ -273,21 +275,24 @@ export function createCopilotQuestionBridge({
|
|
|
273
275
|
choices: [PERMISSION_APPROVE_CHOICE, PERMISSION_DENY_CHOICE],
|
|
274
276
|
allowFreeform: true,
|
|
275
277
|
source: 'onPermissionRequest',
|
|
276
|
-
rationale: 'Copilot requested permission to run a tool in ask mode.',
|
|
278
|
+
rationale: 'Copilot requested permission to run a tool in ask mode. Approve runs it; Deny or a typed reply refuses it, and a typed reply tells Copilot why.',
|
|
277
279
|
extra: {
|
|
278
280
|
requestId: String(request?.requestId || '') || undefined,
|
|
279
281
|
permissionKind: String(request?.kind || '') || undefined,
|
|
280
282
|
},
|
|
281
283
|
}, { signal, message });
|
|
282
284
|
const answer = String(result?.answer ?? '').trim();
|
|
283
|
-
const
|
|
285
|
+
const decision = result?.timedOut
|
|
286
|
+
? null
|
|
287
|
+
: readApprovalAnswer(answer, { approve: PERMISSION_APPROVE_CHOICE, deny: PERMISSION_DENY_CHOICE });
|
|
288
|
+
const approved = decision === 'approve';
|
|
284
289
|
// The denial note is composed HERE rather than in the permission handler so
|
|
285
290
|
// the handler never needs to know the card's choice labels — that would be
|
|
286
291
|
// an import cycle (this module already imports the request describer from
|
|
287
292
|
// the adapter). A freeform denial carries the human's own reason; a plain
|
|
288
293
|
// "Deny" click does not, so echoing the button label back at the model as
|
|
289
294
|
// though it were an explanation is avoided.
|
|
290
|
-
const feedback = answer &&
|
|
295
|
+
const feedback = answer && decision !== 'deny'
|
|
291
296
|
? answer
|
|
292
297
|
: 'The user declined this action.';
|
|
293
298
|
return { approved, answer, feedback, timedOut: result?.timedOut === true, description };
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from './copilot-model-switch.mjs';
|
|
33
33
|
import { copilotRuntimeEntry } from '../copilot-sdk-runtime.mjs';
|
|
34
34
|
import { describeTurnStall, isTurnStalledError } from '../../shared/worker-runtime/turn-liveness.mjs';
|
|
35
|
+
import { WORKING_DIRECTORY_MISSING_CODE, isWorkingDirectoryMissingError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
35
36
|
|
|
36
37
|
const VERSION_DIR_RE = /^\d+\.\d+\.\d+$/;
|
|
37
38
|
|
|
@@ -510,6 +511,16 @@ export const COPILOT_TURN_CONTINUE_GUIDANCE =
|
|
|
510
511
|
*/
|
|
511
512
|
export function classifyCopilotTurnException(error) {
|
|
512
513
|
const detail = String(error?.message || error || '').trim() || 'unknown error';
|
|
514
|
+
// The conversation's folder is gone; the message names it.
|
|
515
|
+
if (isWorkingDirectoryMissingError(error)) {
|
|
516
|
+
return {
|
|
517
|
+
code: WORKING_DIRECTORY_MISSING_CODE,
|
|
518
|
+
stableCode: `copilot.${WORKING_DIRECTORY_MISSING_CODE}`,
|
|
519
|
+
text: detail,
|
|
520
|
+
guidance: error.guidance,
|
|
521
|
+
detail,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
513
524
|
// An unconfirmed explicit model selection (audit #13). The error's own
|
|
514
525
|
// message already names requested vs actual model, so it IS the user-facing
|
|
515
526
|
// text; the stable code is what lets the UI (and tests) key on it.
|
|
@@ -124,6 +124,7 @@ import { createTaskOutputSpool } from '../../shared/worker-runtime/task-output-s
|
|
|
124
124
|
import { createShellOutputMirror } from './copilot-shell-output-mirror.mjs';
|
|
125
125
|
import { isTransientProgressText, settleForTask, shellExitFromEvent } from './copilot-task-settle.mjs';
|
|
126
126
|
import { createKeptPosts } from '../../shared/worker-runtime/kept-posts.mjs';
|
|
127
|
+
import { explainLaunchFailure, missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
127
128
|
|
|
128
129
|
// How long the runtime may sit with no session activity before the worker
|
|
129
130
|
// closes it. The worker process itself stays up and reconnects lazily on the
|
|
@@ -369,6 +370,9 @@ export function createCopilotSdkSessionRunner({
|
|
|
369
370
|
// module imports the real SDK (that lives in copilot-sdk-adapter.mjs).
|
|
370
371
|
resolvePathsImpl = resolveCopilotSdkPaths,
|
|
371
372
|
startClientImpl = startCopilotClient,
|
|
373
|
+
// The error a runtime started in `cwd` fails with up front, or null
|
|
374
|
+
// (shared/worker-runtime/working-directory.mjs).
|
|
375
|
+
checkWorkingDirectoryImpl = missingWorkingDirectoryError,
|
|
372
376
|
createNormalizerImpl = createCopilotEventNormalizer,
|
|
373
377
|
buildMessageOptionsImpl = buildCopilotMessageOptions,
|
|
374
378
|
// BYOK: `COPILOT_PROVIDER_*` in this worker's env become
|
|
@@ -1567,6 +1571,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
1567
1571
|
taskId: shell.shellId,
|
|
1568
1572
|
taskType: 'local_bash',
|
|
1569
1573
|
description: shell.description || 'Detached shell',
|
|
1574
|
+
...(shell.command ? { command: shell.command } : {}),
|
|
1570
1575
|
startedAt: shell.startedAt || null,
|
|
1571
1576
|
stoppable: false,
|
|
1572
1577
|
}));
|
|
@@ -1587,6 +1592,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
1587
1592
|
taskId: task.id,
|
|
1588
1593
|
taskType: 'local_bash',
|
|
1589
1594
|
description: task.description || task.command || 'Background shell',
|
|
1595
|
+
...shellCommandField(task),
|
|
1590
1596
|
startedAt,
|
|
1591
1597
|
stoppable: true,
|
|
1592
1598
|
...(task.progressSummary ? { summary: task.progressSummary } : {}),
|
|
@@ -1616,6 +1622,12 @@ export function createCopilotSdkSessionRunner({
|
|
|
1616
1622
|
return cards;
|
|
1617
1623
|
}
|
|
1618
1624
|
|
|
1625
|
+
/** A registry shell's command line, bounded (the relay scrubs, then cuts it to 2,000). */
|
|
1626
|
+
function shellCommandField(task) {
|
|
1627
|
+
const command = typeof task?.command === 'string' ? task.command.trim().slice(0, 8000) : '';
|
|
1628
|
+
return command ? { command } : {};
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1619
1631
|
/** Where the task console reads a shell's output: its own log, or the spool mirror. */
|
|
1620
1632
|
function shellOutputFields(task) {
|
|
1621
1633
|
const logPath = String(task.logPath || '').trim();
|
|
@@ -1869,6 +1881,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
1869
1881
|
postTaskSettle(task.id, {
|
|
1870
1882
|
...settleForTask(finished, { exitCode: shell ? (shellExitCodes.get(task.id) ?? null) : null }),
|
|
1871
1883
|
description: task.description || task.displayName || task.command || (shell ? 'Background shell' : 'Background agent'),
|
|
1884
|
+
...(shell ? shellCommandField(task) : {}),
|
|
1872
1885
|
taskType: shell ? 'local_bash' : 'local_agent',
|
|
1873
1886
|
startedAt: Date.parse(String(task.startedAt || '')) || null,
|
|
1874
1887
|
...(task.toolCallId ? { toolUseId: String(task.toolCallId) } : {}),
|
|
@@ -2135,6 +2148,7 @@ export function createCopilotSdkSessionRunner({
|
|
|
2135
2148
|
postTaskSettle(shell.shellId, {
|
|
2136
2149
|
...settleForTask({ type: 'shell', status: '' }, { exitCode: shellExitCodes.get(shell.shellId) ?? null }),
|
|
2137
2150
|
description: shell.description || 'Detached shell',
|
|
2151
|
+
...(shell.command ? { command: shell.command } : {}),
|
|
2138
2152
|
taskType: 'local_bash',
|
|
2139
2153
|
startedAt: shell.startedAt || null,
|
|
2140
2154
|
});
|
|
@@ -2612,8 +2626,17 @@ export function createCopilotSdkSessionRunner({
|
|
|
2612
2626
|
if (client) return client;
|
|
2613
2627
|
if (starting) return starting;
|
|
2614
2628
|
starting = (async () => {
|
|
2629
|
+
// The runtime is spawned in the conversation's folder: a folder that is
|
|
2630
|
+
// gone fails the turn with its name, not with the spawn's ENOENT.
|
|
2631
|
+
const missingFolder = checkWorkingDirectoryImpl(cwd);
|
|
2632
|
+
if (missingFolder) throw missingFolder;
|
|
2615
2633
|
sdkPaths = resolvePathsImpl({ env });
|
|
2616
|
-
|
|
2634
|
+
let started;
|
|
2635
|
+
try {
|
|
2636
|
+
started = await startClientImpl({ paths: sdkPaths, cwd, clientName, logLevel, dbg });
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
throw explainLaunchFailure(error, cwd, { check: checkWorkingDirectoryImpl });
|
|
2639
|
+
}
|
|
2617
2640
|
client = started.client;
|
|
2618
2641
|
detachRuntimeExit = observeRuntimeExit(client, handleRuntimeExit);
|
|
2619
2642
|
runtimeTree = watchRuntimeTree(client);
|
|
@@ -10,7 +10,7 @@ export const ASK_USER_TOOL_DESCRIPTION =
|
|
|
10
10
|
+ 'This is the ONLY way to ask the user anything: a question written as plain '
|
|
11
11
|
+ 'reply text is NOT shown interactively and ends the turn unanswered. Call '
|
|
12
12
|
+ 'this tool whenever user input would materially change the result, before '
|
|
13
|
-
+ 'making assumptions. Provide 2-
|
|
13
|
+
+ 'making assumptions. Provide 2-6 concise options per question; the user can '
|
|
14
14
|
+ 'always answer in free text instead. This tool blocks until the user '
|
|
15
15
|
+ 'responds; on timeout it returns an instruction to continue under the '
|
|
16
16
|
+ 'current relay mode.';
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isTurnStalledError,
|
|
9
9
|
resolveTurnStallWindows,
|
|
10
10
|
} from '../../shared/worker-runtime/turn-liveness.mjs';
|
|
11
|
+
import { WORKING_DIRECTORY_MISSING_CODE, isWorkingDirectoryMissingError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The only module in the repo that imports `@cursor/sdk` (and its
|
|
@@ -422,6 +423,19 @@ export function classifyCursorError(error) {
|
|
|
422
423
|
const message = String(error?.message || error);
|
|
423
424
|
const isRetryable = error?.isRetryable === true;
|
|
424
425
|
const name = String(error?.name || '');
|
|
426
|
+
// The conversation's folder is gone; the message names it.
|
|
427
|
+
if (isWorkingDirectoryMissingError(error)) {
|
|
428
|
+
return {
|
|
429
|
+
isAuth: false,
|
|
430
|
+
isBusy: false,
|
|
431
|
+
isRetryable: false,
|
|
432
|
+
code: WORKING_DIRECTORY_MISSING_CODE,
|
|
433
|
+
stableCode: `cursor.${WORKING_DIRECTORY_MISSING_CODE}`,
|
|
434
|
+
message,
|
|
435
|
+
text: message,
|
|
436
|
+
guidance: error.guidance,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
425
439
|
if (isTurnStalledError(error)) {
|
|
426
440
|
return {
|
|
427
441
|
isAuth: false,
|
|
@@ -611,12 +625,13 @@ const modelParameterDefsCache = new Map();
|
|
|
611
625
|
|
|
612
626
|
/**
|
|
613
627
|
* Map the composer's reasoning effort onto the model params Cursor expects on
|
|
614
|
-
* `agent.send`. Effort lives in a per-model parameter named 'effort'
|
|
615
|
-
* 'reasoning'
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
628
|
+
* `agent.send`. Effort lives in a per-model parameter named 'effort',
|
|
629
|
+
* 'reasoning' or 'reasoning_effort', consulted in that order ('extra-high' is
|
|
630
|
+
* Cursor's spelling of 'xhigh'); models that also expose a boolean 'thinking'
|
|
631
|
+
* parameter only honor the higher tiers with thinking enabled, so an explicit
|
|
632
|
+
* effort turns thinking on and 'none' turns it off. Best-effort like
|
|
633
|
+
* readModelContextWindow: null on any failure or when the model has no
|
|
634
|
+
* matching parameter, which sends the model's default variant.
|
|
620
635
|
*/
|
|
621
636
|
export async function resolveCursorReasoningParams({
|
|
622
637
|
apiKey,
|
|
@@ -651,7 +666,7 @@ export async function resolveCursorReasoningParams({
|
|
|
651
666
|
const paramsById = new Map(
|
|
652
667
|
parameters.map((param) => [String(param?.id || '').trim().toLowerCase(), param]),
|
|
653
668
|
);
|
|
654
|
-
const effortParam = paramsById.get('effort') || paramsById.get('reasoning');
|
|
669
|
+
const effortParam = paramsById.get('effort') || paramsById.get('reasoning') || paramsById.get('reasoning_effort');
|
|
655
670
|
const hasThinkingParam = paramsById.has('thinking');
|
|
656
671
|
const effortValues = new Set(
|
|
657
672
|
(Array.isArray(effortParam?.values) ? effortParam.values : [])
|
|
@@ -17,6 +17,7 @@ import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-fil
|
|
|
17
17
|
import { createControlPoller } from '../../shared/control-poller.mjs';
|
|
18
18
|
import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
|
|
19
19
|
import { createCursorTurnRunner } from './cursor-turn-runner.mjs';
|
|
20
|
+
import { resolveCursorSessionStoreDir } from '../services/cursor-session-root-service.mjs';
|
|
20
21
|
|
|
21
22
|
// Windows console launch: the launcher names the worker log, this copies
|
|
22
23
|
// stdout/stderr into it. A no-op wherever the launcher redirects instead.
|
|
@@ -57,8 +58,14 @@ async function main() {
|
|
|
57
58
|
// renewal hint, which is more visible to the user than a dead worker.
|
|
58
59
|
console.warn('cursor-session-worker: CURSOR_API_KEY is not set; turns will fail until it is provided');
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
// `$CURSOR_AGENT_STORE_DIR`, else `cursor-agents` in the relay's data dir:
|
|
62
|
+
// the same folder the relay's Session root looks in.
|
|
63
|
+
const storeDir = resolveCursorSessionStoreDir({
|
|
64
|
+
env: process.env,
|
|
65
|
+
serverDir: path.resolve(__dirname, '..'),
|
|
66
|
+
configPath,
|
|
67
|
+
sdkSessionId,
|
|
68
|
+
});
|
|
62
69
|
|
|
63
70
|
const api = createApiClient({
|
|
64
71
|
serverUrl,
|
|
@@ -23,6 +23,7 @@ import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instr
|
|
|
23
23
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
24
24
|
import { resolveFallbackContextLimitTokens } from '../../shared/context-window-fallbacks.mjs';
|
|
25
25
|
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
26
|
+
import { explainLaunchFailure, missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
26
27
|
|
|
27
28
|
// Stale-auth recovery budget per turn: expiry is routine on long-lived
|
|
28
29
|
// handles, and one retry proved too small in practice (an orchestrator turn
|
|
@@ -122,6 +123,9 @@ export function createCursorTurnRunner({
|
|
|
122
123
|
resolveModelParamsImpl = resolveCursorReasoningParams,
|
|
123
124
|
createNormalizerImpl = createSdkMessageNormalizer,
|
|
124
125
|
buildUserMessageImpl = buildCursorUserMessage,
|
|
126
|
+
// The error an agent started in `cwd` fails with up front, or null
|
|
127
|
+
// (shared/worker-runtime/working-directory.mjs).
|
|
128
|
+
checkWorkingDirectoryImpl = missingWorkingDirectoryError,
|
|
125
129
|
// Whether agent handles carry the relay's `remote_relay` tool
|
|
126
130
|
// (shared/remote-relay-tool-core.mjs). Default: ask the relay.
|
|
127
131
|
remoteRelayToolGate = null,
|
|
@@ -174,7 +178,9 @@ export function createCursorTurnRunner({
|
|
|
174
178
|
// plain refusal the model can relay instead of a failed turn.
|
|
175
179
|
const previewTool = createPreviewTool({
|
|
176
180
|
api,
|
|
177
|
-
|
|
181
|
+
// Between turns, the worker's own session (as remote_relay below): list
|
|
182
|
+
// and close are scoped to a conversation and refused without one.
|
|
183
|
+
getConversationId: () => activeMessage?.conversationId || sdkSessionId || '',
|
|
178
184
|
dbg,
|
|
179
185
|
});
|
|
180
186
|
const customTools = {
|
|
@@ -301,6 +307,10 @@ export function createCursorTurnRunner({
|
|
|
301
307
|
agentHandle = null;
|
|
302
308
|
}
|
|
303
309
|
if (!agentHandle) {
|
|
310
|
+
// The agent works in the conversation's folder: a folder that is gone
|
|
311
|
+
// fails the turn with its name (and is no reason to drop the agent).
|
|
312
|
+
const missingFolder = checkWorkingDirectoryImpl(cwd);
|
|
313
|
+
if (missingFolder) throw missingFolder;
|
|
304
314
|
const durableId = String(message.cursorAgentId || '').trim() || cursorAgentId;
|
|
305
315
|
const agents = buildCursorSubagentAgents(rosterModels);
|
|
306
316
|
await remoteRelayGate.ready();
|
|
@@ -309,11 +319,15 @@ export function createCursorTurnRunner({
|
|
|
309
319
|
try {
|
|
310
320
|
agentHandle = await createAgentHandleImpl({ ...options, agentId: durableId });
|
|
311
321
|
} catch (error) {
|
|
312
|
-
if (!durableId) throw error;
|
|
322
|
+
if (!durableId) throw explainLaunchFailure(error, cwd, { check: checkWorkingDirectoryImpl });
|
|
313
323
|
// The durable agent may have been pruned server-side; fall back to a
|
|
314
324
|
// fresh agent rather than wedging the conversation.
|
|
315
325
|
dbg('cursor agent resume failed, creating fresh agent', error?.message || String(error));
|
|
316
|
-
|
|
326
|
+
try {
|
|
327
|
+
agentHandle = await createAgentHandleImpl({ ...options, agentId: '' });
|
|
328
|
+
} catch (freshError) {
|
|
329
|
+
throw explainLaunchFailure(freshError, cwd, { check: checkWorkingDirectoryImpl });
|
|
330
|
+
}
|
|
317
331
|
}
|
|
318
332
|
}
|
|
319
333
|
// Only recorded once a handle exists: a create that threw leaves the
|
|
@@ -787,6 +801,7 @@ export function createCursorTurnRunner({
|
|
|
787
801
|
// A stall is told in the user's terms, and with its own advice: the
|
|
788
802
|
// run was cancelled, so sending the message again is all it takes.
|
|
789
803
|
message: classified.text || classified.message,
|
|
804
|
+
...(classified.guidance ? { guidance: classified.guidance } : {}),
|
|
790
805
|
...(classified.isStalled ? { guidance: 'Send the message again to retry.' } : {}),
|
|
791
806
|
failedAt: new Date().toISOString(),
|
|
792
807
|
queueMessageId: String(message.id || '') || null,
|
package/server/db-schema.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { migrateConversationGroups } from './migrations/0014-conversation-groups
|
|
|
23
23
|
import { migrateConversationGroupPosition } from './migrations/0015-conversation-group-position.mjs';
|
|
24
24
|
import { migrateShareAccessLog } from './migrations/0016-share-access-log.mjs';
|
|
25
25
|
import { migrateConversationHistoryIndexes } from './migrations/0017-conversation-history-indexes.mjs';
|
|
26
|
+
import { migrateBackgroundTaskRows } from './migrations/0018-background-task-rows.mjs';
|
|
26
27
|
import { migrateImageConversationSchema } from './repositories/image-conversation-repository.mjs';
|
|
27
28
|
|
|
28
29
|
// Mirrors DEFAULT_RELAY_MODE in server-runtime.mjs; used only by the one-time
|
|
@@ -845,6 +846,15 @@ try {
|
|
|
845
846
|
console.warn(`[history] migration 0017 failed; retrying on next boot: ${error?.message || error}`);
|
|
846
847
|
}
|
|
847
848
|
|
|
849
|
+
// The task console's rows across a restart: one new table. The task store
|
|
850
|
+
// checks for it and keeps its rows in memory only without it, so a failure is
|
|
851
|
+
// logged and retried next boot like 0017.
|
|
852
|
+
try {
|
|
853
|
+
migrateBackgroundTaskRows(db);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
console.warn(`[background-tasks] migration 0018 failed; retrying on next boot: ${error?.message || error}`);
|
|
856
|
+
}
|
|
857
|
+
|
|
848
858
|
const runtimeSessionColumns = db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((c) => c.name);
|
|
849
859
|
if (runtimeSessionColumns.length) {
|
|
850
860
|
if (!runtimeSessionColumns.includes('strategy')) {
|
|
@@ -37,6 +37,9 @@ export function createAcpHostServices({
|
|
|
37
37
|
platform = process.platform,
|
|
38
38
|
spawnImpl = spawn,
|
|
39
39
|
spawnSyncImpl = spawnSync,
|
|
40
|
+
// Full path of a program Windows ships (taskkill, Windows PowerShell), or
|
|
41
|
+
// its bare name when it is not where Windows keeps it.
|
|
42
|
+
systemToolPath = windowsSystemToolPath,
|
|
40
43
|
fsImpl = fs,
|
|
41
44
|
// Told about every terminal's life (grok-background-tasks.mjs publishes
|
|
42
45
|
// them as background tasks): `started`, `output`, `killed`, `released`,
|
|
@@ -65,9 +68,9 @@ export function createAcpHostServices({
|
|
|
65
68
|
timeout: 10_000,
|
|
66
69
|
stdio: 'ignore',
|
|
67
70
|
});
|
|
68
|
-
cachedWindowsShell = probe && probe.status === 0 ? 'pwsh' :
|
|
71
|
+
cachedWindowsShell = probe && probe.status === 0 ? 'pwsh' : systemToolPath('powershell');
|
|
69
72
|
} catch {
|
|
70
|
-
cachedWindowsShell =
|
|
73
|
+
cachedWindowsShell = systemToolPath('powershell');
|
|
71
74
|
}
|
|
72
75
|
return cachedWindowsShell;
|
|
73
76
|
}
|
|
@@ -210,7 +213,7 @@ export function createAcpHostServices({
|
|
|
210
213
|
if (platform === 'win32' && entry.proc.pid) {
|
|
211
214
|
// proc.kill() only reaches the shell; shells leave grandchildren behind.
|
|
212
215
|
try {
|
|
213
|
-
spawnImpl(
|
|
216
|
+
spawnImpl(systemToolPath('taskkill'), ['/pid', String(entry.proc.pid), '/t', '/f'], {
|
|
214
217
|
stdio: 'ignore',
|
|
215
218
|
windowsHide: true,
|
|
216
219
|
});
|
|
@@ -15,6 +15,8 @@ import { createKeptPosts } from '../../shared/worker-runtime/kept-posts.mjs';
|
|
|
15
15
|
import { createTaskOutputSpool } from '../../shared/worker-runtime/task-output-spool.mjs';
|
|
16
16
|
|
|
17
17
|
const MAX_DESCRIPTION_CHARS = 500;
|
|
18
|
+
// A bound on the post: the relay scrubs the command, then cuts it to 2,000.
|
|
19
|
+
const MAX_COMMAND_CHARS = 8000;
|
|
18
20
|
const SPOOL_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
|
|
19
21
|
// Ended terminals remembered for a late exit after their release.
|
|
20
22
|
const MAX_TRACKED_TERMINALS = 200;
|
|
@@ -68,6 +70,7 @@ export function createGrokTaskPublisher({
|
|
|
68
70
|
taskId: entry.taskId,
|
|
69
71
|
taskType: 'local_bash',
|
|
70
72
|
description: entry.description,
|
|
73
|
+
...(entry.command ? { command: entry.command } : {}),
|
|
71
74
|
startedAt: entry.startedAt,
|
|
72
75
|
stoppable: true,
|
|
73
76
|
...(outputPath ? { outputPath, outputKind: 'spool' } : {}),
|
|
@@ -117,6 +120,9 @@ export function createGrokTaskPublisher({
|
|
|
117
120
|
const entry = {
|
|
118
121
|
taskId,
|
|
119
122
|
description: String(command || '').trim().slice(0, MAX_DESCRIPTION_CHARS) || 'Terminal',
|
|
123
|
+
// The whole command line (the description is cut shorter), for the
|
|
124
|
+
// console's `| tail` marker.
|
|
125
|
+
command: String(command || '').trim().slice(0, MAX_COMMAND_CHARS),
|
|
120
126
|
startedAt,
|
|
121
127
|
kill,
|
|
122
128
|
live: false,
|
|
@@ -196,6 +202,7 @@ export function createGrokTaskPublisher({
|
|
|
196
202
|
status,
|
|
197
203
|
summary: exitCode !== null ? `exit code ${exitCode}` : (signal ? `killed by ${signal}` : null),
|
|
198
204
|
description: entry.description,
|
|
205
|
+
...(entry.command ? { command: entry.command } : {}),
|
|
199
206
|
taskType: 'local_bash',
|
|
200
207
|
startedAt: entry.startedAt,
|
|
201
208
|
// Absent rather than null: a killed process has no exit code.
|
|
@@ -6,12 +6,26 @@ import { AcpClient, extractGrokModelsFromInitialize, pickAllowOnceOptionId } fro
|
|
|
6
6
|
import { createAcpHostServices } from './acp-host-services.mjs';
|
|
7
7
|
import { createSdkMessageNormalizer } from './sdk-message-normalizer.mjs';
|
|
8
8
|
import { describeTurnStall, isTurnStalledError } from '../../shared/worker-runtime/turn-liveness.mjs';
|
|
9
|
+
import { WORKING_DIRECTORY_MISSING_CODE, isWorkingDirectoryMissingError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
9
10
|
|
|
10
11
|
export { extractGrokModelsFromInitialize };
|
|
11
12
|
|
|
12
13
|
export function classifyGrokError(error) {
|
|
13
14
|
const message = String(error?.message || error || '').trim();
|
|
14
15
|
const lower = message.toLowerCase();
|
|
16
|
+
// First, and above all before the ENOENT branch: a spawn in a folder that
|
|
17
|
+
// is gone fails with ENOENT too, and the runner puts it down to the folder,
|
|
18
|
+
// whose path may hold any of the words matched below.
|
|
19
|
+
if (isWorkingDirectoryMissingError(error)) {
|
|
20
|
+
return {
|
|
21
|
+
code: `grok.${WORKING_DIRECTORY_MISSING_CODE}`,
|
|
22
|
+
message,
|
|
23
|
+
text: message,
|
|
24
|
+
guidance: error.guidance,
|
|
25
|
+
isAuth: false,
|
|
26
|
+
isBusy: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
15
29
|
// No bare 'auth' token: it would match unrelated words ("author…") in
|
|
16
30
|
// arbitrary agent error text and misclassify them as login failures.
|
|
17
31
|
if (
|
|
@@ -12,6 +12,7 @@ import { fetchRemoteRelayInflight } from '../../shared/remote-relay-tool-core.mj
|
|
|
12
12
|
import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
|
|
13
13
|
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
14
14
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
15
|
+
import { explainLaunchFailure, missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
15
16
|
|
|
16
17
|
const PLAN_BOARD_ACTIONS = [
|
|
17
18
|
{ id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
|
|
@@ -64,6 +65,7 @@ function buildGrokTerminalError(classified, message) {
|
|
|
64
65
|
code: classified.code,
|
|
65
66
|
stableCode: classified.code,
|
|
66
67
|
message: classified.text || classified.message,
|
|
68
|
+
...(classified.guidance ? { guidance: classified.guidance } : {}),
|
|
67
69
|
// A stalled turn was cancelled and its agent is restarted for the next
|
|
68
70
|
// message, so sending the message again is all it takes.
|
|
69
71
|
...(classified.isStalled ? { guidance: 'Send the message again to retry.' } : {}),
|
|
@@ -115,6 +117,9 @@ export function createGrokTurnRunner({
|
|
|
115
117
|
createAgentHandleImpl = createGrokAgentHandle,
|
|
116
118
|
startGrokTurnImpl = startGrokTurn,
|
|
117
119
|
classifyErrorImpl = classifyGrokError,
|
|
120
|
+
// The error a Grok CLI started in `cwd` fails with up front, or null
|
|
121
|
+
// (shared/worker-runtime/working-directory.mjs).
|
|
122
|
+
checkWorkingDirectoryImpl = missingWorkingDirectoryError,
|
|
118
123
|
getPreviewInstructions = createPreviewInstructionsProvider({ api }),
|
|
119
124
|
env = process.env,
|
|
120
125
|
nodePath = process.execPath,
|
|
@@ -200,6 +205,11 @@ export function createGrokTurnRunner({
|
|
|
200
205
|
|
|
201
206
|
async function ensureAgentHandle(message, model) {
|
|
202
207
|
if (agentHandle) return agentHandle;
|
|
208
|
+
// The CLI is spawned in the conversation's folder; a folder that is gone
|
|
209
|
+
// fails with ENOENT, which would otherwise read as a CLI that is not
|
|
210
|
+
// installed (classifyGrokError).
|
|
211
|
+
const missingFolder = checkWorkingDirectoryImpl(cwd);
|
|
212
|
+
if (missingFolder) throw missingFolder;
|
|
203
213
|
const resumeId = String(message.grokNativeSessionId || grokNativeSessionId || '').trim();
|
|
204
214
|
agentHandle = await createAgentHandleImpl({
|
|
205
215
|
command,
|
|
@@ -215,7 +225,7 @@ export function createGrokTurnRunner({
|
|
|
215
225
|
? { AcpClientImpl: withAcpMcpServers(AcpClient, acpMcpServers), mcpServers: acpMcpServers }
|
|
216
226
|
: {}),
|
|
217
227
|
dbg,
|
|
218
|
-
});
|
|
228
|
+
}).catch((error) => { throw explainLaunchFailure(error, cwd, { check: checkWorkingDirectoryImpl }); });
|
|
219
229
|
if (agentHandle.sessionId) {
|
|
220
230
|
await persistNativeSessionId(message, agentHandle.sessionId);
|
|
221
231
|
}
|
|
@@ -427,7 +437,7 @@ export function createGrokTurnRunner({
|
|
|
427
437
|
const previewBlock = previewInstructionsSent || acpMcpServers.length
|
|
428
438
|
? ''
|
|
429
439
|
: await Promise.resolve()
|
|
430
|
-
.then(() => getPreviewInstructions?.())
|
|
440
|
+
.then(() => getPreviewInstructions?.({ conversationId: message.conversationId || sdkSessionId || '' }))
|
|
431
441
|
.catch(() => '');
|
|
432
442
|
const mediaBlock = mediaInstructionsSent ? '' : renderMediaEmbedInstructionBlock();
|
|
433
443
|
const promptText = [previewBlock, mediaBlock, modeNudge, userText].filter(Boolean).join('\n\n');
|
|
@@ -594,7 +604,7 @@ export function createGrokTurnRunner({
|
|
|
594
604
|
: classified.isStalled
|
|
595
605
|
? classified.text
|
|
596
606
|
|| `System note: the Grok turn stalled and was stopped (${classified.message}). Send a new message to retry.`
|
|
597
|
-
: `System note: the Grok turn failed (${classified.message}).`;
|
|
607
|
+
: classified.text || `System note: the Grok turn failed (${classified.message}).`;
|
|
598
608
|
await publishResponse(message, {
|
|
599
609
|
text: noteText,
|
|
600
610
|
model: message.providerModel || defaultModel || null,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: the task console's rows outlive a relay restart.
|
|
5
|
+
*
|
|
6
|
+
* `background_task_rows` holds what the relay keeps of a background task
|
|
7
|
+
* that has a console (a shell or Monitor task naming its output file), per
|
|
8
|
+
* store key (the id the worker posts under, its SDK session id) and task:
|
|
9
|
+
*
|
|
10
|
+
* - `state` 'live': a task of the worker's last set. After a restart it is
|
|
11
|
+
* held back until the worker reports again: the worker's set decides which
|
|
12
|
+
* are still running, and those of a worker that does not report are marked
|
|
13
|
+
* ended, never shown as running again.
|
|
14
|
+
* - `state` 'ended': the dimmed row the panel keeps for an hour after the end,
|
|
15
|
+
* with its status and exit code. `ended_at` (epoch ms) is what the hour
|
|
16
|
+
* counts from.
|
|
17
|
+
* - `row_json` the task row as the store holds it, without the health
|
|
18
|
+
* reading (the poller reads the file again).
|
|
19
|
+
*
|
|
20
|
+
* Rows go when the hour is up, when the conversation is deleted, and live
|
|
21
|
+
* rows when the worker's set no longer names them.
|
|
22
|
+
*
|
|
23
|
+
* Additive and idempotent. Safe to run on every boot. An older build never
|
|
24
|
+
* reads the table; its consoles start empty after a restart as before.
|
|
25
|
+
*
|
|
26
|
+
* Usage: node server/migrations/0018-background-task-rows.mjs [path/to/copilot.db]
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import path from 'path';
|
|
31
|
+
import process from 'process';
|
|
32
|
+
import Database from 'better-sqlite3';
|
|
33
|
+
|
|
34
|
+
export const BACKGROUND_TASK_ROWS_COLUMNS = Object.freeze(['store_key', 'task_id', 'state', 'row_json', 'ended_at', 'updated_at']);
|
|
35
|
+
|
|
36
|
+
function tableExists(db, name) {
|
|
37
|
+
return !!db
|
|
38
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
39
|
+
.get(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Why the table cannot be used (a table of that name with other columns), or ''. */
|
|
43
|
+
export function backgroundTaskRowsTableProblem(db) {
|
|
44
|
+
if (!tableExists(db, 'background_task_rows')) return 'no background_task_rows table';
|
|
45
|
+
const present = new Set(db.prepare(`PRAGMA table_info(background_task_rows)`).all().map((column) => column.name));
|
|
46
|
+
const missing = BACKGROUND_TASK_ROWS_COLUMNS.filter((name) => !present.has(name));
|
|
47
|
+
return missing.length ? `background_task_rows lacks ${missing.join(', ')}` : '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function migrateBackgroundTaskRows(db) {
|
|
51
|
+
let tablesAdded = 0;
|
|
52
|
+
if (!tableExists(db, 'background_task_rows')) {
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE background_task_rows (
|
|
55
|
+
store_key TEXT NOT NULL,
|
|
56
|
+
task_id TEXT NOT NULL,
|
|
57
|
+
state TEXT NOT NULL CHECK (state IN ('live', 'ended')),
|
|
58
|
+
row_json TEXT NOT NULL,
|
|
59
|
+
ended_at INTEGER,
|
|
60
|
+
updated_at INTEGER NOT NULL,
|
|
61
|
+
PRIMARY KEY (store_key, task_id)
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
tablesAdded += 1;
|
|
65
|
+
}
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_background_task_rows_ended
|
|
68
|
+
ON background_task_rows(state, ended_at)
|
|
69
|
+
`);
|
|
70
|
+
return { applied: tablesAdded > 0, tablesAdded };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function defaultDbPath() {
|
|
74
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function migrate(dbPath, options = {}) {
|
|
79
|
+
const db = new Database(dbPath, options);
|
|
80
|
+
try {
|
|
81
|
+
return migrateBackgroundTaskRows(db);
|
|
82
|
+
} finally {
|
|
83
|
+
db.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
88
|
+
if (isMain) {
|
|
89
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
90
|
+
try {
|
|
91
|
+
const result = migrate(dbPath);
|
|
92
|
+
console.log(`Migration 0018 on ${dbPath}`);
|
|
93
|
+
console.log(result.applied
|
|
94
|
+
? ` background task rows: tables added ${result.tablesAdded}`
|
|
95
|
+
: ' skipped: nothing to change');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Migration 0018 failed: ${err?.message || err}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|