@cortexkit/opencode-magic-context 0.44.0 → 0.44.2
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/README.md +6 -0
- package/dist/agents/dreamer.d.ts +4 -6
- package/dist/agents/dreamer.d.ts.map +1 -1
- package/dist/agents/magic-context-prompt.d.ts +2 -0
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
- package/dist/config/live-child-output-cap.d.ts +18 -0
- package/dist/config/live-child-output-cap.d.ts.map +1 -0
- package/dist/config/schema/magic-context.d.ts +64 -11
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-48f7np9d.js → conflict-warning-hook-1msyvb51.js} +6 -6
- package/dist/{derive-budgets-8kgevxmx.js → derive-budgets-gd2270s0.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +11 -0
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts +2 -0
- package/dist/features/magic-context/dreamer/docs-proposals.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manual-summary.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +5 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts +2 -0
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
- package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +7 -2
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +2 -2
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +8 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +5 -0
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +18 -2
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +12 -0
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +3 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/token-budget.d.ts +33 -0
- package/dist/features/magic-context/dreamer/token-budget.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts +15 -0
- package/dist/features/magic-context/dreamer/verify-diff.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify-gate.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -2
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts +4 -0
- package/dist/features/magic-context/dreamer/verify-read-cap.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/verify.d.ts +5 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity-cache.d.ts +6 -0
- package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +15 -22
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -0
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/verification-paths.d.ts +1 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +5 -0
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-activity.d.ts +10 -0
- package/dist/features/magic-context/session-activity.d.ts.map +1 -0
- package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
- package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
- package/dist/features/magic-context/session-project-backfill.d.ts +4 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts +5 -0
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts +1 -0
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts +1 -1
- package/dist/features/magic-context/smart-notes/storage.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/types.d.ts +2 -0
- package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts +4 -1
- package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/embed-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-payloads.d.ts +1 -0
- package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts +6 -4
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +16 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts +7 -0
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +1 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-5f4qsnaq.js → index-18tjha8g.js} +2 -2
- package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
- package/dist/{index-tjq5jztd.js → index-3bygzk2d.js} +1 -1
- package/dist/{index-cj5qe2nw.js → index-3x9caerx.js} +8 -6
- package/dist/index-40s1xz22.js +125 -0
- package/dist/{index-pye48m79.js → index-egkfvhms.js} +130 -42
- package/dist/{index-2zf5fv89.js → index-esxfcz5z.js} +171 -12
- package/dist/index-gtzcmv90.js +7260 -0
- package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
- package/dist/{index-v6wsttqt.js → index-kr5qehbd.js} +1 -1
- package/dist/{index-dx0xnd4q.js → index-kwe03yde.js} +1 -1
- package/dist/{index-c906f3ff.js → index-r9vkp4n9.js} +4 -4
- package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
- package/dist/{index-01ft0h3p.js → index-stb5fg12.js} +2 -2
- package/dist/{index-rdey8znw.js → index-ttdn47aq.js} +35260 -32540
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15123 -23504
- package/dist/{message-time-backfill-rm9wmcem.js → message-time-backfill-q374fxc5.js} +3 -3
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -9
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +1 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-ktz5zt70.js → read-session-chunk-wmvgnd24.js} +6 -4
- package/dist/{recomp-orchestrator-ahekzt0s.js → recomp-orchestrator-rk40ff15.js} +10 -9
- package/dist/{safe-notification-target-6w0esgd6.js → safe-notification-target-50gm9c58.js} +3 -3
- package/dist/{send-session-notification-w7xbnrsm.js → send-session-notification-6xsrcxnp.js} +5 -5
- package/dist/shared/cache-ttl-display.d.ts +3 -1
- package/dist/shared/cache-ttl-display.d.ts.map +1 -1
- package/dist/shared/harness.d.ts +11 -0
- package/dist/shared/harness.d.ts.map +1 -1
- package/dist/shared/model-cache-ttl.d.ts +9 -0
- package/dist/shared/model-cache-ttl.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +1 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +7 -7
- package/dist/shared/prompt-async-transport.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +1 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +21 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +12 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/status-view.d.ts +1 -1
- package/dist/shared/status-view.d.ts.map +1 -1
- package/dist/shared/subagent-runner.d.ts +3 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +5 -0
- package/dist/shared/user-facing-codes.d.ts.map +1 -1
- package/dist/{task-config-mgf5nsh3.js → task-config-2bm68628.js} +1 -1
- package/dist/tools/ctx-expand/render.d.ts.map +1 -1
- package/dist/transform-postprocess-phase-t7xgdhdf.js +36 -0
- package/dist/v2/hidden-completion.d.ts +2 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +1 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +11 -0
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -0
- package/dist/v2/hooks/session-project-backfill.d.ts +1 -1
- package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -1
- package/dist/v2/hooks/storage-gate.d.ts +11 -5
- package/dist/v2/hooks/storage-gate.d.ts.map +1 -1
- package/dist/v2/host-service.d.ts +2 -1
- package/dist/v2/host-service.d.ts.map +1 -1
- package/dist/v2/server.js +95347 -93236
- package/dist/v2/store-reader.d.ts +2 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/wrapup-orchestrator-h8tr10c6.js +19 -0
- package/package.json +1 -1
- package/src/agents/dreamer.ts +4 -6
- package/src/agents/hidden-agent-registrations.ts +3 -3
- package/src/agents/magic-context-prompt.test.ts +125 -0
- package/src/agents/magic-context-prompt.ts +12 -7
- package/src/agents/permissions.test.ts +3 -4
- package/src/config/latch-permanence-guard.test.ts +9 -0
- package/src/config/live-child-output-cap.test.ts +101 -0
- package/src/config/live-child-output-cap.ts +74 -0
- package/src/config/live-run-config.test.ts +24 -11
- package/src/config/schema/magic-context.test.ts +16 -1
- package/src/config/schema/magic-context.ts +27 -10
- package/src/features/magic-context/dreamer/task-registry.ts +13 -0
- package/src/shared/cache-ttl-display.ts +14 -20
- package/src/shared/harness.ts +14 -0
- package/src/shared/logger.test.ts +1 -0
- package/src/shared/model-cache-ttl.test.ts +40 -0
- package/src/shared/model-cache-ttl.ts +58 -0
- package/src/shared/model-suggestion-retry.ts +9 -2
- package/src/shared/prompt-async-transport.test.ts +254 -1
- package/src/shared/prompt-async-transport.ts +153 -44
- package/src/shared/prompt-surface-a1-golden.md +13 -13
- package/src/shared/rpc-async-probes.test.ts +230 -0
- package/src/shared/rpc-server.ts +6 -4
- package/src/shared/rpc-types.ts +1 -1
- package/src/shared/rpc-utils.test.ts +27 -1
- package/src/shared/rpc-utils.ts +161 -9
- package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +94 -0
- package/src/shared/sqlite-pass-scope.test.ts +182 -0
- package/src/shared/sqlite-transaction-mode-guard.test.ts +42 -10
- package/src/shared/sqlite-write-locker-test-support.ts +1 -0
- package/src/shared/sqlite.ts +142 -56
- package/src/shared/status-view.ts +1 -1
- package/src/shared/storage-busy.test.ts +95 -0
- package/src/shared/subagent-runner.ts +4 -0
- package/src/shared/user-facing-codes.ts +6 -0
- package/src/shared/windows-hidden-spawns.test.ts +132 -0
- package/src/shared/write-transaction-attribution.test.ts +1 -0
- package/dist/wrapup-orchestrator-9srk0kjf.js +0 -18
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
|
|
6
|
+
const root = resolve(import.meta.dir, "../../../..");
|
|
7
|
+
const shared = "packages/plugin/src/shared/sqlite.ts";
|
|
8
|
+
function files(dir: string): string[] {
|
|
9
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
10
|
+
const path = join(dir, entry.name);
|
|
11
|
+
if (entry.isDirectory()) return files(path);
|
|
12
|
+
return /\.tsx?$/.test(path) && !/\.test\.|test-support|\.d\.ts$/.test(path) ? [path] : [];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function scan(
|
|
17
|
+
path: string,
|
|
18
|
+
text: string,
|
|
19
|
+
): { violations: string[]; transactions: number; literals: number } {
|
|
20
|
+
const violations: string[] = [];
|
|
21
|
+
let transactions = 0;
|
|
22
|
+
let literals = 0;
|
|
23
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
|
24
|
+
const visit = (node: ts.Node) => {
|
|
25
|
+
if (
|
|
26
|
+
ts.isCallExpression(node) &&
|
|
27
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
28
|
+
node.expression.name.text === "transaction"
|
|
29
|
+
)
|
|
30
|
+
transactions++;
|
|
31
|
+
if (
|
|
32
|
+
path !== shared &&
|
|
33
|
+
ts.isImportDeclaration(node) &&
|
|
34
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
35
|
+
!node.importClause?.isTypeOnly &&
|
|
36
|
+
/^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
|
|
37
|
+
) {
|
|
38
|
+
violations.push(`${path}: native SQLite import bypasses shared acquisition`);
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
path !== shared &&
|
|
42
|
+
ts.isExportDeclaration(node) &&
|
|
43
|
+
!node.isTypeOnly &&
|
|
44
|
+
node.moduleSpecifier &&
|
|
45
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
46
|
+
/^(bun:sqlite|node:sqlite|better-sqlite3)$/.test(node.moduleSpecifier.text)
|
|
47
|
+
) {
|
|
48
|
+
violations.push(`${path}: native SQLite re-export bypasses shared acquisition`);
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
path !== shared &&
|
|
52
|
+
ts.isCallExpression(node) &&
|
|
53
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
54
|
+
(ts.isIdentifier(node.expression) && node.expression.text === "require")) &&
|
|
55
|
+
/(?:bun:sqlite|node:sqlite|better-sqlite3)/.test(
|
|
56
|
+
node.arguments.map((arg) => arg.getText(source)).join(""),
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
violations.push(`${path}: dynamic native SQLite import bypasses shared acquisition`);
|
|
60
|
+
}
|
|
61
|
+
const sqlText = ts.isStringLiteralLike(node)
|
|
62
|
+
? node.text
|
|
63
|
+
: ts.isTemplateExpression(node)
|
|
64
|
+
? node.head.text + node.templateSpans.map((span) => span.literal.text).join("")
|
|
65
|
+
: undefined;
|
|
66
|
+
if (
|
|
67
|
+
sqlText !== undefined &&
|
|
68
|
+
/\bBEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*(?:;|$)/i.test(sqlText)
|
|
69
|
+
) {
|
|
70
|
+
literals++;
|
|
71
|
+
if (path !== shared) {
|
|
72
|
+
const call = node.parent;
|
|
73
|
+
const routed =
|
|
74
|
+
!ts.isTemplateExpression(node) &&
|
|
75
|
+
ts.isCallExpression(call) &&
|
|
76
|
+
ts.isPropertyAccessExpression(call.expression) &&
|
|
77
|
+
call.expression.name.text === "exec" &&
|
|
78
|
+
/^\s*BEGIN\s+(IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sqlText);
|
|
79
|
+
if (!routed)
|
|
80
|
+
violations.push(
|
|
81
|
+
`${path}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}: BEGIN acquisition must be a standalone shared Database.exec call`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
ts.forEachChild(node, visit);
|
|
86
|
+
};
|
|
87
|
+
visit(source);
|
|
88
|
+
return { violations, transactions, literals };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test("all production transaction acquisition routes through the shared SQLite runtime", () => {
|
|
92
|
+
const scans = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"]
|
|
93
|
+
.flatMap((directory) => files(join(root, directory)))
|
|
94
|
+
.map((path) => scan(relative(root, path), readFileSync(path, "utf8")));
|
|
95
|
+
expect(scans.reduce((sum, scan) => sum + scan.transactions, 0)).toBeGreaterThan(50);
|
|
96
|
+
expect(scans.reduce((sum, scan) => sum + scan.literals, 0)).toBeGreaterThan(20);
|
|
97
|
+
expect(scans.flatMap((scan) => scan.violations)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("acquisition fence rejects raw native transactions and unrouted BEGIN shapes", () => {
|
|
101
|
+
expect(
|
|
102
|
+
scan(
|
|
103
|
+
"new-writer.ts",
|
|
104
|
+
'import { Database } from "bun:sqlite"; new Database(":memory:").transaction(() => write())();',
|
|
105
|
+
).violations,
|
|
106
|
+
).toHaveLength(1);
|
|
107
|
+
expect(
|
|
108
|
+
scan(
|
|
109
|
+
"new-writer.ts",
|
|
110
|
+
'const { DatabaseSync } = require("node:sqlite"); new DatabaseSync(":memory:").transaction(() => write())();',
|
|
111
|
+
).violations,
|
|
112
|
+
).toHaveLength(1);
|
|
113
|
+
expect(scan("new-writer.ts", 'export { Database } from "bun:sqlite";').violations).toHaveLength(
|
|
114
|
+
1,
|
|
115
|
+
);
|
|
116
|
+
expect(scan("new-writer.ts", 'db.prepare("BEGIN IMMEDIATE").run();').violations).toHaveLength(
|
|
117
|
+
1,
|
|
118
|
+
);
|
|
119
|
+
expect(
|
|
120
|
+
scan("new-writer.ts", 'db.exec("BEGIN IMMEDIATE; INSERT INTO result VALUES (1)");')
|
|
121
|
+
.violations,
|
|
122
|
+
).toHaveLength(1);
|
|
123
|
+
expect(scan("new-writer.ts", "db.exec(`BEGIN IMMEDIATE; ${extra}`);").violations).toHaveLength(
|
|
124
|
+
1,
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
scan(
|
|
128
|
+
"new-writer.ts",
|
|
129
|
+
'import { Database } from "./shared/sqlite"; db.exec("BEGIN IMMEDIATE"); db.transaction(() => write()).immediate();',
|
|
130
|
+
).violations,
|
|
131
|
+
).toEqual([]);
|
|
132
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Database, withPrivilegedWriter, withSqliteTransformPass } from "./sqlite";
|
|
6
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
7
|
+
|
|
8
|
+
for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
9
|
+
test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
|
|
11
|
+
const path = join(dir, "context.db");
|
|
12
|
+
const db = new Database(path);
|
|
13
|
+
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT)");
|
|
14
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
15
|
+
let calls = 0;
|
|
16
|
+
const write = () => {
|
|
17
|
+
calls++;
|
|
18
|
+
db.prepare("INSERT INTO result VALUES (?)").run("once");
|
|
19
|
+
return "managed";
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
withSqliteTransformPass(() => {
|
|
23
|
+
if (mode === "literal") {
|
|
24
|
+
db.exec("BEGIN IMMEDIATE");
|
|
25
|
+
write();
|
|
26
|
+
db.exec("COMMIT");
|
|
27
|
+
} else {
|
|
28
|
+
const transaction = db.transaction(write);
|
|
29
|
+
expect(mode === "default" ? transaction() : transaction[mode]()).toBe(
|
|
30
|
+
"managed",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
expect(calls).toBe(1);
|
|
35
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "once" }]);
|
|
36
|
+
} finally {
|
|
37
|
+
await locker.exited;
|
|
38
|
+
db.close();
|
|
39
|
+
rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}, 30000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
|
|
45
|
+
const db = new Database(":memory:");
|
|
46
|
+
db.exec("CREATE TABLE result(value TEXT)");
|
|
47
|
+
let calls = 0;
|
|
48
|
+
const transaction = db.transaction(function (this: { prefix: string }, value: string) {
|
|
49
|
+
calls++;
|
|
50
|
+
db.prepare("INSERT INTO result VALUES (?)").run(this.prefix + value);
|
|
51
|
+
db.transaction(() => {
|
|
52
|
+
db.exec("INSERT INTO result VALUES ('inner')");
|
|
53
|
+
}).immediate();
|
|
54
|
+
throw Object.assign(new Error("busy after writes"), { code: "SQLITE_BUSY" });
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
expect(() =>
|
|
58
|
+
withSqliteTransformPass(() => transaction.call({ prefix: "outer-" }, "value")),
|
|
59
|
+
).toThrow("busy after writes");
|
|
60
|
+
expect(calls).toBe(1);
|
|
61
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
|
|
62
|
+
} finally {
|
|
63
|
+
db.close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("exhausted routed acquisition never enters the callback or multiplies privileged retries", () => {
|
|
68
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-routed-exhaustion-"));
|
|
69
|
+
const path = join(dir, "context.db");
|
|
70
|
+
const blocker = new Database(path);
|
|
71
|
+
const db = new Database(path);
|
|
72
|
+
blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
|
|
73
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
74
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
75
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
76
|
+
let callbacks = 0;
|
|
77
|
+
try {
|
|
78
|
+
for (const run of [
|
|
79
|
+
() => db.transaction(() => callbacks++)(),
|
|
80
|
+
() => withPrivilegedWriter(db, () => callbacks++),
|
|
81
|
+
]) {
|
|
82
|
+
wait.mockClear();
|
|
83
|
+
expect(() => withSqliteTransformPass(run)).toThrow("after 3 attempts");
|
|
84
|
+
expect(wait).toHaveBeenCalledTimes(2);
|
|
85
|
+
expect(callbacks).toBe(0);
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
wait.mockRestore();
|
|
89
|
+
blocker.exec("ROLLBACK");
|
|
90
|
+
blocker.close();
|
|
91
|
+
db.close();
|
|
92
|
+
rmSync(dir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
Database,
|
|
7
|
+
withoutSqliteTransformPass,
|
|
8
|
+
withPrivilegedWriter,
|
|
9
|
+
withSqliteTransformPass,
|
|
10
|
+
} from "./sqlite";
|
|
11
|
+
|
|
12
|
+
test("background privileged acquisition attempts once without backoff", () => {
|
|
13
|
+
const db = new Database(":memory:");
|
|
14
|
+
const error = Object.assign(new Error("original busy"), { code: "SQLITE_BUSY" });
|
|
15
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
19
|
+
let callbacks = 0;
|
|
20
|
+
try {
|
|
21
|
+
expect(() => withPrivilegedWriter(db, () => callbacks++)).toThrow("original busy");
|
|
22
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(wait).not.toHaveBeenCalled();
|
|
24
|
+
expect(callbacks).toBe(0);
|
|
25
|
+
} finally {
|
|
26
|
+
exec.mockRestore();
|
|
27
|
+
wait.mockRestore();
|
|
28
|
+
db.close();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("background transaction exhausts one five-second busy timeout", () => {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-background-busy-"));
|
|
34
|
+
const path = join(dir, "context.db");
|
|
35
|
+
const blocker = new Database(path);
|
|
36
|
+
const writer = new Database(path);
|
|
37
|
+
blocker.exec("PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT)");
|
|
38
|
+
writer.exec("PRAGMA busy_timeout=5000");
|
|
39
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
40
|
+
let callbacks = 0;
|
|
41
|
+
try {
|
|
42
|
+
const start = performance.now();
|
|
43
|
+
expect(() => writer.transaction(() => callbacks++)()).toThrow();
|
|
44
|
+
const elapsed = performance.now() - start;
|
|
45
|
+
expect(callbacks).toBe(0);
|
|
46
|
+
expect(elapsed).toBeGreaterThanOrEqual(4500);
|
|
47
|
+
expect(elapsed).toBeLessThan(7500);
|
|
48
|
+
} finally {
|
|
49
|
+
blocker.exec("ROLLBACK");
|
|
50
|
+
blocker.close();
|
|
51
|
+
writer.close();
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}, 30000);
|
|
55
|
+
|
|
56
|
+
test("foreground retry scope crosses awaits, nests and excludes detached maintenance", async () => {
|
|
57
|
+
const db = new Database(":memory:");
|
|
58
|
+
const error = Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
59
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
60
|
+
throw error;
|
|
61
|
+
});
|
|
62
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
63
|
+
const acquire = () =>
|
|
64
|
+
withPrivilegedWriter(db, () => {
|
|
65
|
+
throw new Error("callback must not run");
|
|
66
|
+
});
|
|
67
|
+
let late: Promise<void> | undefined;
|
|
68
|
+
let release!: () => void;
|
|
69
|
+
const gate = new Promise<void>((resolve) => {
|
|
70
|
+
release = resolve;
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await withSqliteTransformPass(async () => {
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
expect(() => withSqliteTransformPass(acquire)).toThrow("after 3 attempts");
|
|
76
|
+
expect(exec).toHaveBeenCalledTimes(3);
|
|
77
|
+
exec.mockClear();
|
|
78
|
+
wait.mockClear();
|
|
79
|
+
withoutSqliteTransformPass(() => {
|
|
80
|
+
expect(acquire).toThrow("busy");
|
|
81
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(wait).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
exec.mockClear();
|
|
85
|
+
expect(acquire).toThrow("after 3 attempts");
|
|
86
|
+
expect(exec).toHaveBeenCalledTimes(3);
|
|
87
|
+
late = (async () => {
|
|
88
|
+
await gate;
|
|
89
|
+
exec.mockClear();
|
|
90
|
+
wait.mockClear();
|
|
91
|
+
expect(acquire).toThrow("busy");
|
|
92
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(wait).not.toHaveBeenCalled();
|
|
94
|
+
})();
|
|
95
|
+
});
|
|
96
|
+
release();
|
|
97
|
+
await late;
|
|
98
|
+
exec.mockClear();
|
|
99
|
+
expect(acquire).toThrow("busy");
|
|
100
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
101
|
+
} finally {
|
|
102
|
+
release();
|
|
103
|
+
exec.mockRestore();
|
|
104
|
+
wait.mockRestore();
|
|
105
|
+
db.close();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("throwing foreground passes cannot leak retry policy to later work", async () => {
|
|
110
|
+
const db = new Database(":memory:");
|
|
111
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
112
|
+
throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
113
|
+
});
|
|
114
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
115
|
+
try {
|
|
116
|
+
await expect(
|
|
117
|
+
withSqliteTransformPass(async () => {
|
|
118
|
+
throw new Error("pass failed");
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toThrow("pass failed");
|
|
121
|
+
expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("busy");
|
|
122
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(wait).not.toHaveBeenCalled();
|
|
124
|
+
} finally {
|
|
125
|
+
exec.mockRestore();
|
|
126
|
+
wait.mockRestore();
|
|
127
|
+
db.close();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("explicit deferred reads on a writable handle do not contend with the writer", () => {
|
|
132
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-deferred-read-"));
|
|
133
|
+
const path = join(dir, "context.db");
|
|
134
|
+
const writer = new Database(path);
|
|
135
|
+
const reader = new Database(path);
|
|
136
|
+
writer.exec(
|
|
137
|
+
"PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); INSERT INTO result VALUES ('committed')",
|
|
138
|
+
);
|
|
139
|
+
reader.exec("PRAGMA busy_timeout=0");
|
|
140
|
+
writer.exec("BEGIN IMMEDIATE");
|
|
141
|
+
writer.exec("INSERT INTO result VALUES ('uncommitted')");
|
|
142
|
+
try {
|
|
143
|
+
const rows = withSqliteTransformPass(() =>
|
|
144
|
+
reader.transaction(() => reader.prepare("SELECT value FROM result").all()).deferred(),
|
|
145
|
+
);
|
|
146
|
+
expect(rows).toEqual([{ value: "committed" }]);
|
|
147
|
+
} finally {
|
|
148
|
+
writer.exec("ROLLBACK");
|
|
149
|
+
writer.close();
|
|
150
|
+
reader.close();
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("an explicitly started foreground pass owns its lifetime independently", async () => {
|
|
156
|
+
const db = new Database(":memory:");
|
|
157
|
+
const exec = spyOn(db, "exec").mockImplementation(() => {
|
|
158
|
+
throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
159
|
+
});
|
|
160
|
+
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
161
|
+
let release!: () => void;
|
|
162
|
+
const gate = new Promise<void>((resolve) => {
|
|
163
|
+
release = resolve;
|
|
164
|
+
});
|
|
165
|
+
let child!: Promise<void>;
|
|
166
|
+
try {
|
|
167
|
+
await withSqliteTransformPass(async () => {
|
|
168
|
+
child = withSqliteTransformPass(async () => {
|
|
169
|
+
await gate;
|
|
170
|
+
expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("after 3 attempts");
|
|
171
|
+
expect(exec).toHaveBeenCalledTimes(3);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
release();
|
|
175
|
+
await child;
|
|
176
|
+
} finally {
|
|
177
|
+
release();
|
|
178
|
+
exec.mockRestore();
|
|
179
|
+
wait.mockRestore();
|
|
180
|
+
db.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
@@ -4,14 +4,14 @@ import { join, relative, resolve } from "node:path";
|
|
|
4
4
|
import ts from "typescript";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* starts with no lock. If it reads before it writes, the write must upgrade a read
|
|
7
|
+
* An explicitly deferred transaction starts with no writer lock. If it reads before it writes, the write must upgrade a read
|
|
9
8
|
* snapshot to a write lock, and SQLite never runs the busy handler for that
|
|
10
9
|
* upgrade: under another process's write it fails at once with SQLITE_BUSY, or
|
|
11
10
|
* with SQLITE_BUSY_SNAPSHOT when another commit landed after the read, and
|
|
12
11
|
* busy_timeout never applies. Many OpenCode and Pi processes share one context.db,
|
|
13
|
-
* so
|
|
14
|
-
*
|
|
12
|
+
* so writing transactions must acquire at BEGIN. The shared runtime defaults
|
|
13
|
+
* writable handles to IMMEDIATE; plugin/Pi call sites keep their explicit
|
|
14
|
+
* `.immediate()` / `.exclusive()` declarations as an additional intent fence.
|
|
15
15
|
*
|
|
16
16
|
* Only read-only transactions may stay deferred. Each one is listed here, keyed
|
|
17
17
|
* by file and the nearest named enclosing function, with the reason it never
|
|
@@ -26,7 +26,7 @@ const READ_ONLY_DEFERRED_TRANSACTIONS: Record<string, string> = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
const REPOSITORY_ROOT = resolve(import.meta.dir, "../../../..");
|
|
29
|
-
const SOURCE_ROOTS = ["packages/plugin/src", "packages/pi-plugin/src"];
|
|
29
|
+
const SOURCE_ROOTS = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"];
|
|
30
30
|
const LOCKING_MODES = new Set(["immediate", "exclusive"]);
|
|
31
31
|
|
|
32
32
|
function sourceFiles(directory: string): string[] {
|
|
@@ -56,13 +56,16 @@ function isTransactionCall(node: ts.Node): node is ts.CallExpression {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** True when `node` is the receiver of a called `.immediate()` / `.exclusive()`. */
|
|
59
|
-
function isCalledWithLockingMode(
|
|
59
|
+
function isCalledWithLockingMode(
|
|
60
|
+
node: ts.Node,
|
|
61
|
+
modes: ReadonlySet<string> = LOCKING_MODES,
|
|
62
|
+
): boolean {
|
|
60
63
|
const access = node.parent;
|
|
61
64
|
return (
|
|
62
65
|
access !== undefined &&
|
|
63
66
|
ts.isPropertyAccessExpression(access) &&
|
|
64
67
|
access.expression === node &&
|
|
65
|
-
|
|
68
|
+
modes.has(access.name.text) &&
|
|
66
69
|
access.parent !== undefined &&
|
|
67
70
|
ts.isCallExpression(access.parent) &&
|
|
68
71
|
access.parent.expression === access
|
|
@@ -92,7 +95,11 @@ function enclosingFunctionName(node: ts.Node): string {
|
|
|
92
95
|
* A transaction stored in a variable is locking only when every use of that
|
|
93
96
|
* variable calls `.immediate()` or `.exclusive()` on it.
|
|
94
97
|
*/
|
|
95
|
-
function storedTransactionLocks(
|
|
98
|
+
function storedTransactionLocks(
|
|
99
|
+
call: ts.CallExpression,
|
|
100
|
+
source: ts.SourceFile,
|
|
101
|
+
modes: ReadonlySet<string> = LOCKING_MODES,
|
|
102
|
+
): boolean {
|
|
96
103
|
const declaration = call.parent;
|
|
97
104
|
if (!ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return false;
|
|
98
105
|
const name = declaration.name.text;
|
|
@@ -111,7 +118,7 @@ function storedTransactionLocks(call: ts.CallExpression, source: ts.SourceFile):
|
|
|
111
118
|
ts.forEachChild(node, visit);
|
|
112
119
|
};
|
|
113
120
|
visit(scope);
|
|
114
|
-
return uses.length > 0 && uses.every(isCalledWithLockingMode);
|
|
121
|
+
return uses.length > 0 && uses.every((use) => isCalledWithLockingMode(use, modes));
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
interface TransactionScan {
|
|
@@ -154,8 +161,33 @@ function repositorySources(): Array<{ path: string; text: string }> {
|
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
describe("SQLite transaction lock mode", () => {
|
|
164
|
+
it("declares every audited read-only transaction explicitly deferred", () => {
|
|
165
|
+
const candidates: string[] = [];
|
|
166
|
+
for (const file of repositorySources()) {
|
|
167
|
+
const source = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true);
|
|
168
|
+
const visit = (node: ts.Node): void => {
|
|
169
|
+
if (isTransactionCall(node)) {
|
|
170
|
+
const key = `${file.path}#${enclosingFunctionName(node)}`;
|
|
171
|
+
if (key in READ_ONLY_DEFERRED_TRANSACTIONS) {
|
|
172
|
+
const deferred = new Set(["deferred"]);
|
|
173
|
+
if (
|
|
174
|
+
!isCalledWithLockingMode(node, deferred) &&
|
|
175
|
+
!storedTransactionLocks(node, source, deferred)
|
|
176
|
+
)
|
|
177
|
+
candidates.push(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
ts.forEachChild(node, visit);
|
|
181
|
+
};
|
|
182
|
+
visit(source);
|
|
183
|
+
}
|
|
184
|
+
expect(candidates.sort()).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
157
187
|
it("starts every writing transaction with .immediate() or .exclusive()", () => {
|
|
158
|
-
const { deferredSites, lockingSites } = scanTransactionModes(
|
|
188
|
+
const { deferredSites, lockingSites } = scanTransactionModes(
|
|
189
|
+
repositorySources().filter((file) => !file.path.startsWith("packages/cli/")),
|
|
190
|
+
);
|
|
159
191
|
// The scan must actually see the codebase's transactions.
|
|
160
192
|
expect(lockingSites).toBeGreaterThan(50);
|
|
161
193
|
|
|
@@ -26,6 +26,7 @@ export async function startSqliteWriteLocker(
|
|
|
26
26
|
const child = Bun.spawn(["bun", import.meta.path, dbPath, String(holdMs)], {
|
|
27
27
|
stdout: "pipe",
|
|
28
28
|
stderr: "pipe",
|
|
29
|
+
windowsHide: true,
|
|
29
30
|
});
|
|
30
31
|
const reader = child.stdout.getReader();
|
|
31
32
|
const decoder = new TextDecoder();
|