@cortexkit/opencode-magic-context 0.44.1 → 0.44.3
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/config/live-child-output-cap.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +63 -10
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-d14ah73v.js → conflict-warning-hook-hmpzxep6.js} +3 -3
- package/dist/{derive-budgets-gd2270s0.js → derive-budgets-vjmvh6pz.js} +3 -1
- 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/classify.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/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +1 -1
- package/dist/features/magic-context/dreamer/lease.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/refresh-primers.d.ts +5 -0
- 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 +11 -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/message-index-async.d.ts +1 -1
- package/dist/features/magic-context/message-index-async.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-project-backfill.d.ts +4 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-storage.d.ts +5 -5
- package/dist/features/magic-context/session-project-storage.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-embedding-measurements.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 +2 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.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-types.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-types.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/derive-budgets.d.ts +4 -0
- package/dist/hooks/magic-context/derive-budgets.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/historian-host-runner.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/lkg-persist.d.ts +1 -4
- package/dist/hooks/magic-context/lkg-persist.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/producer-window-guard.d.ts +4 -1
- package/dist/hooks/magic-context/producer-window-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts +13 -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.map +1 -1
- package/dist/{index-a9vbtdzv.js → index-33q7s9y2.js} +1 -1
- package/dist/{index-cz88kw0n.js → index-4an2p8qa.js} +183 -39
- package/dist/{index-d107az47.js → index-638hf49n.js} +1 -1
- package/dist/{index-stb5fg12.js → index-9y5p0d50.js} +31 -1
- package/dist/{index-s6b7h12g.js → index-dgg5y7er.js} +73 -2
- package/dist/{index-6w9p73jz.js → index-jqcbs2k1.js} +10 -0
- package/dist/{index-0rp6pj54.js → index-nf9sk8sh.js} +7 -5
- package/dist/index-q5p1aqvv.js +7260 -0
- package/dist/index-rdsaxnf2.js +125 -0
- package/dist/{index-jtdrvmcn.js → index-yhm60r53.js} +37544 -37072
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8402 -14429
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap.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-fkdv2g5b.js → read-session-chunk-n072mcq4.js} +2 -2
- package/dist/{recomp-orchestrator-k4h5n0p3.js → recomp-orchestrator-19tr6zya.js} +6 -5
- package/dist/{send-session-notification-g2k03zyn.js → send-session-notification-04hvd54c.js} +2 -2
- 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-utils.d.ts +21 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/run-token-log.d.ts +13 -0
- package/dist/shared/run-token-log.d.ts.map +1 -0
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +19 -0
- package/dist/shared/sqlite.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/transform-postprocess-phase-z2mfc5w1.js +36 -0
- package/dist/v2/hidden-completion.d.ts +9 -1
- 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/hidden-terminal-error.d.ts +8 -0
- package/dist/v2/hooks/hidden-terminal-error.d.ts.map +1 -0
- 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/hooks/tools.d.ts +1 -1
- package/dist/v2/hooks/tools.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 +24948 -22659
- package/dist/v2/store-reader.d.ts +2 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/{wrapup-orchestrator-35y56636.js → wrapup-orchestrator-sy4m56a7.js} +7 -6
- package/package.json +1 -1
- package/src/agents/dreamer.ts +4 -6
- package/src/agents/hidden-agent-registrations.ts +3 -3
- package/src/agents/permissions.test.ts +3 -4
- package/src/config/latch-permanence-guard.test.ts +13 -0
- package/src/config/live-child-output-cap.test.ts +26 -0
- package/src/config/live-child-output-cap.ts +6 -2
- package/src/config/schema/magic-context.ts +21 -6
- package/src/features/magic-context/dreamer/task-registry.ts +13 -0
- package/src/shared/data-path.test.ts +1 -1
- package/src/shared/logger.test.ts +1 -0
- package/src/shared/model-suggestion-retry.ts +9 -2
- package/src/shared/models-dev-cache.test.ts +30 -1
- package/src/shared/prompt-async-transport.test.ts +254 -1
- package/src/shared/prompt-async-transport.ts +153 -44
- package/src/shared/rpc-async-probes.test.ts +230 -0
- package/src/shared/rpc-server.ts +6 -4
- package/src/shared/rpc-utils.test.ts +27 -1
- package/src/shared/rpc-utils.ts +161 -9
- package/src/shared/run-token-log.test.ts +65 -0
- package/src/shared/run-token-log.ts +39 -0
- package/src/shared/sqlite-acquisition-fence.test.ts +132 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +102 -0
- package/src/shared/sqlite-pass-scope.test.ts +153 -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 +245 -56
- package/src/shared/storage-busy.test.ts +72 -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 +14 -11
|
@@ -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,102 @@
|
|
|
1
|
+
import { expect, 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
|
+
withAsyncPrivilegedWriter,
|
|
8
|
+
withPrivilegedWriter,
|
|
9
|
+
withSqliteTransformPass,
|
|
10
|
+
} from "./sqlite";
|
|
11
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
12
|
+
|
|
13
|
+
for (const mode of ["default", "immediate", "exclusive", "literal"] as const) {
|
|
14
|
+
test(`shared SQLite retries ${mode} acquisition before running any writes`, async () => {
|
|
15
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-routed-acquisition-"));
|
|
16
|
+
const path = join(dir, "context.db");
|
|
17
|
+
const db = new Database(path);
|
|
18
|
+
db.exec(
|
|
19
|
+
"PRAGMA journal_mode=WAL; PRAGMA busy_timeout=50; CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
20
|
+
);
|
|
21
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
22
|
+
let calls = 0;
|
|
23
|
+
const write = () => {
|
|
24
|
+
calls++;
|
|
25
|
+
db.prepare("INSERT INTO result VALUES (?)").run("once");
|
|
26
|
+
return "managed";
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
await withSqliteTransformPass(async () => {
|
|
30
|
+
await withAsyncPrivilegedWriter(db, () => undefined);
|
|
31
|
+
if (mode === "literal") {
|
|
32
|
+
db.exec("BEGIN IMMEDIATE");
|
|
33
|
+
write();
|
|
34
|
+
db.exec("COMMIT");
|
|
35
|
+
} else {
|
|
36
|
+
const transaction = db.transaction(write);
|
|
37
|
+
expect(mode === "default" ? transaction() : transaction[mode]()).toBe(
|
|
38
|
+
"managed",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
expect(calls).toBe(1);
|
|
43
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "once" }]);
|
|
44
|
+
} finally {
|
|
45
|
+
await locker.exited;
|
|
46
|
+
db.close();
|
|
47
|
+
rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
}, 30000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("routed transactions preserve nesting, receiver, arguments and rollback without callback retries", () => {
|
|
53
|
+
const db = new Database(":memory:");
|
|
54
|
+
db.exec(
|
|
55
|
+
"CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
56
|
+
);
|
|
57
|
+
let calls = 0;
|
|
58
|
+
const transaction = db.transaction(function (this: { prefix: string }, value: string) {
|
|
59
|
+
calls++;
|
|
60
|
+
db.prepare("INSERT INTO result VALUES (?)").run(this.prefix + value);
|
|
61
|
+
db.transaction(() => {
|
|
62
|
+
db.exec("INSERT INTO result VALUES ('inner')");
|
|
63
|
+
}).immediate();
|
|
64
|
+
throw Object.assign(new Error("busy after writes"), { code: "SQLITE_BUSY" });
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
expect(() =>
|
|
68
|
+
withSqliteTransformPass(() => transaction.call({ prefix: "outer-" }, "value")),
|
|
69
|
+
).toThrow("busy after writes");
|
|
70
|
+
expect(calls).toBe(1);
|
|
71
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
|
|
72
|
+
} finally {
|
|
73
|
+
db.close();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("exhausted routed acquisition never enters the callback or multiplies privileged retries", () => {
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-routed-exhaustion-"));
|
|
79
|
+
const path = join(dir, "context.db");
|
|
80
|
+
const blocker = new Database(path);
|
|
81
|
+
const db = new Database(path);
|
|
82
|
+
blocker.exec(
|
|
83
|
+
"PRAGMA journal_mode=WAL; CREATE TABLE result(value TEXT); CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
84
|
+
);
|
|
85
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
86
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
87
|
+
let callbacks = 0;
|
|
88
|
+
try {
|
|
89
|
+
for (const run of [
|
|
90
|
+
() => db.transaction(() => callbacks++)(),
|
|
91
|
+
() => withPrivilegedWriter(db, () => callbacks++),
|
|
92
|
+
]) {
|
|
93
|
+
expect(() => withSqliteTransformPass(run)).toThrow("acquisition remained busy");
|
|
94
|
+
expect(callbacks).toBe(0);
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
blocker.exec("ROLLBACK");
|
|
98
|
+
blocker.close();
|
|
99
|
+
db.close();
|
|
100
|
+
rmSync(dir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { expect, 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
|
+
beginSqliteWriterAsync,
|
|
7
|
+
Database,
|
|
8
|
+
isTransientSqliteError,
|
|
9
|
+
SqliteAcquisitionBusyError,
|
|
10
|
+
withAsyncPrivilegedWriter,
|
|
11
|
+
withPrivilegedWriter,
|
|
12
|
+
withSqliteBackgroundWriter,
|
|
13
|
+
withSqliteTransformPass,
|
|
14
|
+
} from "./sqlite";
|
|
15
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
16
|
+
|
|
17
|
+
function fixture() {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-busy-yield-"));
|
|
19
|
+
const path = join(dir, "context.db");
|
|
20
|
+
const db = new Database(path);
|
|
21
|
+
db.exec(
|
|
22
|
+
"PRAGMA journal_mode=WAL; CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
23
|
+
);
|
|
24
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
25
|
+
return {
|
|
26
|
+
db,
|
|
27
|
+
path,
|
|
28
|
+
close: () => {
|
|
29
|
+
db.close();
|
|
30
|
+
rmSync(dir, { recursive: true, force: true });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("tool-style transaction outside a pass waits for a brief lock", async () => {
|
|
36
|
+
const { db, path, close } = fixture();
|
|
37
|
+
const locker = await startSqliteWriteLocker(path, 300);
|
|
38
|
+
try {
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
db.transaction(() =>
|
|
41
|
+
db.prepare("INSERT INTO context_privilege_state(id, enabled) VALUES (1, 0)").run(),
|
|
42
|
+
).immediate();
|
|
43
|
+
expect(performance.now() - start).toBeGreaterThanOrEqual(250);
|
|
44
|
+
expect(db.prepare("SELECT COUNT(*) AS count FROM context_privilege_state").get()).toEqual({
|
|
45
|
+
count: 1,
|
|
46
|
+
});
|
|
47
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
48
|
+
} finally {
|
|
49
|
+
await locker.exited;
|
|
50
|
+
close();
|
|
51
|
+
}
|
|
52
|
+
}, 30000);
|
|
53
|
+
|
|
54
|
+
test("named background acquisition gives up quickly and leaves work for retry", () => {
|
|
55
|
+
const { db, path, close } = fixture();
|
|
56
|
+
const blocker = new Database(path);
|
|
57
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
58
|
+
let calls = 0;
|
|
59
|
+
let retryQueued = false;
|
|
60
|
+
try {
|
|
61
|
+
const start = performance.now();
|
|
62
|
+
try {
|
|
63
|
+
withSqliteBackgroundWriter(() => db.transaction(() => calls++)());
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!isTransientSqliteError(error)) throw error;
|
|
66
|
+
retryQueued = true;
|
|
67
|
+
}
|
|
68
|
+
expect(performance.now() - start).toBeLessThan(250);
|
|
69
|
+
expect(retryQueued).toBe(true);
|
|
70
|
+
expect(calls).toBe(0);
|
|
71
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
72
|
+
} finally {
|
|
73
|
+
blocker.exec("ROLLBACK");
|
|
74
|
+
blocker.close();
|
|
75
|
+
close();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("foreground admission keeps the loop ticking while a separate writer holds the lock", async () => {
|
|
80
|
+
const { db, path, close } = fixture();
|
|
81
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
82
|
+
let timerFired = false;
|
|
83
|
+
let calls = 0;
|
|
84
|
+
try {
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
timerFired = true;
|
|
87
|
+
}, 100);
|
|
88
|
+
await withSqliteTransformPass(() =>
|
|
89
|
+
withAsyncPrivilegedWriter(db, () => {
|
|
90
|
+
calls++;
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
expect(timerFired).toBe(true);
|
|
94
|
+
expect(calls).toBe(1);
|
|
95
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
96
|
+
} finally {
|
|
97
|
+
await locker.exited;
|
|
98
|
+
close();
|
|
99
|
+
}
|
|
100
|
+
}, 30000);
|
|
101
|
+
|
|
102
|
+
test("a foreground in-pass writer has bounded 250ms tolerance and restores its timeout", () => {
|
|
103
|
+
const { db, path, close } = fixture();
|
|
104
|
+
const blocker = new Database(path);
|
|
105
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
106
|
+
try {
|
|
107
|
+
const start = performance.now();
|
|
108
|
+
expect(() =>
|
|
109
|
+
withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined)),
|
|
110
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
111
|
+
expect(performance.now() - start).toBeGreaterThanOrEqual(200);
|
|
112
|
+
expect(performance.now() - start).toBeLessThan(400);
|
|
113
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
114
|
+
} finally {
|
|
115
|
+
blocker.exec("ROLLBACK");
|
|
116
|
+
blocker.close();
|
|
117
|
+
close();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("ready background publication retries on a timer and retains its write", async () => {
|
|
122
|
+
const { db, path, close } = fixture();
|
|
123
|
+
const locker = await startSqliteWriteLocker(path, 650);
|
|
124
|
+
let ticked = false;
|
|
125
|
+
try {
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
ticked = true;
|
|
128
|
+
}, 100);
|
|
129
|
+
await beginSqliteWriterAsync(db, "historian-publish");
|
|
130
|
+
db.prepare("INSERT INTO context_privilege_state(id, enabled) VALUES (1, 0)").run();
|
|
131
|
+
db.exec("COMMIT");
|
|
132
|
+
expect(ticked).toBe(true);
|
|
133
|
+
expect(db.prepare("SELECT COUNT(*) AS count FROM context_privilege_state").get()).toEqual({
|
|
134
|
+
count: 1,
|
|
135
|
+
});
|
|
136
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
137
|
+
} finally {
|
|
138
|
+
await locker.exited;
|
|
139
|
+
close();
|
|
140
|
+
}
|
|
141
|
+
}, 30000);
|
|
142
|
+
|
|
143
|
+
test("an in-pass BEGIN tolerates a sibling writer that releases within 250ms", async () => {
|
|
144
|
+
const { db, path, close } = fixture();
|
|
145
|
+
const locker = await startSqliteWriteLocker(path, 150);
|
|
146
|
+
try {
|
|
147
|
+
withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined));
|
|
148
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
149
|
+
} finally {
|
|
150
|
+
await locker.exited;
|
|
151
|
+
close();
|
|
152
|
+
}
|
|
153
|
+
}, 30000);
|
|
@@ -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();
|