@cortexkit/opencode-magic-context 0.44.2 → 0.44.4
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/dist/{conflict-warning-hook-1msyvb51.js → conflict-warning-hook-hmpzxep6.js} +3 -3
- package/dist/{derive-budgets-gd2270s0.js → derive-budgets-ncwn1jk9.js} +4 -2
- package/dist/features/magic-context/dreamer/classify.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/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts +1 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +8 -4
- package/dist/features/magic-context/memory/project-identity.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.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/storage-db.d.ts +6 -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/v22-deferred-backfill.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts +12 -1
- package/dist/hooks/magic-context/compartment-render-epoch.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-types.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-types.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/drop-stale-reduce-calls.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +7 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +5 -0
- package/dist/hooks/magic-context/inject-compartments.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/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/read-session-chunk.d.ts +4 -2
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +2 -2
- package/dist/hooks/magic-context/read-session-raw.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 +6 -0
- package/dist/hooks/magic-context/storage-busy-refusal.d.ts.map +1 -1
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-r9vkp4n9.js → index-33q7s9y2.js} +1 -1
- package/dist/{index-egkfvhms.js → index-4an2p8qa.js} +80 -14
- package/dist/{index-18tjha8g.js → index-638hf49n.js} +1 -1
- package/dist/{index-gtzcmv90.js → index-mrc0twf3.js} +12 -6
- package/dist/{index-stb5fg12.js → index-msqr3x6m.js} +32 -2
- package/dist/{index-y5145x8k.js → index-n96ag4rk.js} +15 -1
- package/dist/{index-esxfcz5z.js → index-qm36k34q.js} +56 -13
- package/dist/{index-ttdn47aq.js → index-sqp1hz9v.js} +311 -82
- package/dist/{index-40s1xz22.js → index-wprsye9w.js} +1 -1
- package/dist/{index-3x9caerx.js → index-zx7pmr39.js} +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +530 -250
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-wmvgnd24.js → read-session-chunk-339hg90t.js} +3 -3
- package/dist/{recomp-orchestrator-rk40ff15.js → recomp-orchestrator-0ers6smx.js} +7 -7
- package/dist/{send-session-notification-6xsrcxnp.js → send-session-notification-04hvd54c.js} +2 -2
- package/dist/shared/rpc-utils.d.ts +7 -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.d.ts +7 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/transcript.d.ts +2 -0
- package/dist/shared/transcript.d.ts.map +1 -1
- package/dist/shared/user-answer.d.ts +4 -0
- package/dist/shared/user-answer.d.ts.map +1 -0
- package/dist/{transform-postprocess-phase-t7xgdhdf.js → transform-postprocess-phase-ttdkrm6r.js} +7 -7
- package/dist/v2/hidden-completion.d.ts +7 -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-terminal-error.d.ts +8 -0
- package/dist/v2/hooks/hidden-terminal-error.d.ts.map +1 -0
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts +1 -1
- package/dist/v2/hooks/store.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/server.js +916 -221
- package/dist/v2/store-reader.d.ts +2 -1
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/{wrapup-orchestrator-h8tr10c6.js → wrapup-orchestrator-x0jvvrqn.js} +8 -8
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +4 -0
- package/src/shared/data-path.test.ts +1 -1
- package/src/shared/models-dev-cache.test.ts +30 -1
- package/src/shared/rpc-utils.test.ts +27 -2
- package/src/shared/rpc-utils.ts +44 -15
- package/src/shared/run-token-log.test.ts +65 -0
- package/src/shared/run-token-log.ts +39 -0
- package/src/shared/sqlite-acquisition-routing.test.ts +19 -11
- package/src/shared/sqlite-pass-scope.test.ts +115 -144
- package/src/shared/sqlite.ts +123 -20
- package/src/shared/storage-busy.test.ts +34 -57
- package/src/shared/tag-transcript.ts +4 -1
- package/src/shared/transcript.ts +3 -0
- package/src/shared/user-answer.test.ts +40 -0
- package/src/shared/user-answer.ts +17 -0
- package/src/shared/write-transaction-attribution.test.ts +13 -11
|
@@ -1,182 +1,153 @@
|
|
|
1
|
-
import { expect,
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
|
+
beginSqliteWriterAsync,
|
|
6
7
|
Database,
|
|
7
|
-
|
|
8
|
+
isTransientSqliteError,
|
|
9
|
+
SqliteAcquisitionBusyError,
|
|
10
|
+
withAsyncPrivilegedWriter,
|
|
8
11
|
withPrivilegedWriter,
|
|
12
|
+
withSqliteBackgroundWriter,
|
|
9
13
|
withSqliteTransformPass,
|
|
10
14
|
} from "./sqlite";
|
|
15
|
+
import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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);
|
|
20
38
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 });
|
|
25
48
|
} finally {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
db.close();
|
|
49
|
+
await locker.exited;
|
|
50
|
+
close();
|
|
29
51
|
}
|
|
30
|
-
});
|
|
52
|
+
}, 30000);
|
|
31
53
|
|
|
32
|
-
test("background
|
|
33
|
-
const
|
|
34
|
-
const path = join(dir, "context.db");
|
|
54
|
+
test("named background acquisition gives up quickly and leaves work for retry", () => {
|
|
55
|
+
const { db, path, close } = fixture();
|
|
35
56
|
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
57
|
blocker.exec("BEGIN IMMEDIATE");
|
|
40
|
-
let
|
|
58
|
+
let calls = 0;
|
|
59
|
+
let retryQueued = false;
|
|
41
60
|
try {
|
|
42
61
|
const start = performance.now();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 });
|
|
48
72
|
} finally {
|
|
49
73
|
blocker.exec("ROLLBACK");
|
|
50
74
|
blocker.close();
|
|
51
|
-
|
|
52
|
-
rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
close();
|
|
53
76
|
}
|
|
54
|
-
}
|
|
77
|
+
});
|
|
55
78
|
|
|
56
|
-
test("foreground
|
|
57
|
-
const db =
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
});
|
|
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;
|
|
72
84
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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);
|
|
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 });
|
|
101
96
|
} finally {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
wait.mockRestore();
|
|
105
|
-
db.close();
|
|
97
|
+
await locker.exited;
|
|
98
|
+
close();
|
|
106
99
|
}
|
|
107
|
-
});
|
|
100
|
+
}, 30000);
|
|
108
101
|
|
|
109
|
-
test("
|
|
110
|
-
const db =
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
const wait = spyOn(Atomics, "wait").mockReturnValue("timed-out");
|
|
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");
|
|
115
106
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
expect(()
|
|
122
|
-
expect(
|
|
123
|
-
expect(wait).not.toHaveBeenCalled();
|
|
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 });
|
|
124
114
|
} finally {
|
|
125
|
-
exec
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
blocker.exec("ROLLBACK");
|
|
116
|
+
blocker.close();
|
|
117
|
+
close();
|
|
128
118
|
}
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
test("
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
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')");
|
|
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;
|
|
142
125
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
146
|
-
|
|
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 });
|
|
147
137
|
} finally {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
reader.close();
|
|
151
|
-
rmSync(dir, { recursive: true, force: true });
|
|
138
|
+
await locker.exited;
|
|
139
|
+
close();
|
|
152
140
|
}
|
|
153
|
-
});
|
|
141
|
+
}, 30000);
|
|
154
142
|
|
|
155
|
-
test("an
|
|
156
|
-
const db =
|
|
157
|
-
const
|
|
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>;
|
|
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);
|
|
166
146
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await gate;
|
|
170
|
-
expect(() => withPrivilegedWriter(db, () => undefined)).toThrow("after 3 attempts");
|
|
171
|
-
expect(exec).toHaveBeenCalledTimes(3);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
release();
|
|
175
|
-
await child;
|
|
147
|
+
withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined));
|
|
148
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
176
149
|
} finally {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
wait.mockRestore();
|
|
180
|
-
db.close();
|
|
150
|
+
await locker.exited;
|
|
151
|
+
close();
|
|
181
152
|
}
|
|
182
|
-
});
|
|
153
|
+
}, 30000);
|
package/src/shared/sqlite.ts
CHANGED
|
@@ -224,7 +224,9 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
224
224
|
writable: true,
|
|
225
225
|
value: (sql: string) => {
|
|
226
226
|
if (/^\s*BEGIN\s+(?:IMMEDIATE|EXCLUSIVE)(?:\s+TRANSACTION)?\s*;?\s*$/i.test(sql)) {
|
|
227
|
-
|
|
227
|
+
if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore())
|
|
228
|
+
acquireShort(db, () => nativeExec(sql), sql.trim());
|
|
229
|
+
else nativeExec(sql);
|
|
228
230
|
return db;
|
|
229
231
|
}
|
|
230
232
|
return nativeExec(sql);
|
|
@@ -489,6 +491,13 @@ export type Statement = BetterSqlite3.Statement<unknown[], unknown>;
|
|
|
489
491
|
|
|
490
492
|
const privilegeDepth = new WeakMap<Database, number>();
|
|
491
493
|
const transformPassScope = new AsyncLocalStorage<{ active: boolean } | undefined>();
|
|
494
|
+
const admissionScope = new AsyncLocalStorage<boolean>();
|
|
495
|
+
const backgroundWriterScope = new AsyncLocalStorage<boolean>();
|
|
496
|
+
|
|
497
|
+
/** Opt in only a writer that can safely defer its work after a busy acquisition. */
|
|
498
|
+
export function withSqliteBackgroundWriter<T>(operation: () => T): T {
|
|
499
|
+
return backgroundWriterScope.run(true, operation);
|
|
500
|
+
}
|
|
492
501
|
|
|
493
502
|
/** Only an awaited foreground transform may spend the extra acquisition budget.
|
|
494
503
|
* The mutable lease also expires for detached descendants when that pass ends. */
|
|
@@ -530,35 +539,123 @@ export class SqliteAcquisitionBusyError extends Error {
|
|
|
530
539
|
readonly code = "SQLITE_BUSY";
|
|
531
540
|
readonly stage: string;
|
|
532
541
|
constructor(cause: unknown, stage = "BEGIN IMMEDIATE") {
|
|
533
|
-
super("SQLite writer acquisition remained busy
|
|
542
|
+
super("SQLite writer acquisition remained busy", { cause });
|
|
534
543
|
this.name = "SqliteAcquisitionBusyError";
|
|
535
544
|
this.stage = stage;
|
|
536
545
|
}
|
|
537
546
|
}
|
|
538
547
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
548
|
+
const SHORT_BUSY_TIMEOUT_MS = 25;
|
|
549
|
+
const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 250;
|
|
550
|
+
const FOREGROUND_ACQUISITION_BUDGET_MS = 16_500;
|
|
551
|
+
|
|
552
|
+
/** The connection is never handed back to another caller with a shortened timeout. */
|
|
553
|
+
function acquireShort(db: Database, acquire: () => unknown, site: string): void {
|
|
554
|
+
const started = performance.now();
|
|
555
|
+
const previous = pragmaNumber(db, "busy_timeout");
|
|
556
|
+
let acquired = false;
|
|
557
|
+
try {
|
|
558
|
+
// Async admission retries after yielding, so each of its BEGIN attempts
|
|
559
|
+
// stays short. An ordinary BEGIN inside a transform pass cannot yield;
|
|
560
|
+
// give that single attempt enough time for a brief writer to finish.
|
|
561
|
+
const timeout =
|
|
562
|
+
transformPassScope.getStore()?.active &&
|
|
563
|
+
!admissionScope.getStore() &&
|
|
564
|
+
!backgroundWriterScope.getStore()
|
|
565
|
+
? FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS
|
|
566
|
+
: SHORT_BUSY_TIMEOUT_MS;
|
|
567
|
+
db.exec(`PRAGMA busy_timeout=${timeout}`);
|
|
544
568
|
acquire();
|
|
545
|
-
|
|
569
|
+
acquired = true;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (transformPassScope.getStore()?.active && isTransientSqliteError(error))
|
|
572
|
+
throw new SqliteAcquisitionBusyError(error, site);
|
|
573
|
+
throw error;
|
|
574
|
+
} finally {
|
|
575
|
+
if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
|
|
576
|
+
const elapsed = performance.now() - started;
|
|
577
|
+
if (elapsed >= 250)
|
|
578
|
+
console.warn(
|
|
579
|
+
`[${new Date().toISOString()}] [magic-context] sqlite acquisition site=${site} lane=${transformPassScope.getStore()?.active ? "foreground" : "background"} elapsed=${Math.round(elapsed)}ms attempts=1 outcome=${acquired ? "acquired" : "busy"}`,
|
|
580
|
+
);
|
|
546
581
|
}
|
|
547
|
-
|
|
548
|
-
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Begin an immediate transaction with bounded asynchronous retries on writer contention.
|
|
585
|
+
* Callers must recheck any lease or source snapshot after this function returns. */
|
|
586
|
+
export async function beginSqliteWriterAsync(db: Database, site: string): Promise<void> {
|
|
587
|
+
const started = performance.now();
|
|
588
|
+
let attempts = 0;
|
|
589
|
+
for (;;) {
|
|
590
|
+
attempts++;
|
|
549
591
|
try {
|
|
550
|
-
|
|
592
|
+
withSqliteBackgroundWriter(() => db.exec("BEGIN IMMEDIATE"));
|
|
593
|
+
const elapsed = performance.now() - started;
|
|
594
|
+
if (elapsed >= 250)
|
|
595
|
+
console.warn(
|
|
596
|
+
`[${new Date().toISOString()}] [magic-context] sqlite acquisition site=${site} lane=background elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=acquired`,
|
|
597
|
+
);
|
|
551
598
|
return;
|
|
552
599
|
} catch (error) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
600
|
+
if (!isTransientSqliteError(error)) throw error;
|
|
601
|
+
const elapsed = performance.now() - started;
|
|
602
|
+
if (elapsed >= FOREGROUND_ACQUISITION_BUDGET_MS) {
|
|
603
|
+
console.warn(
|
|
604
|
+
`[${new Date().toISOString()}] [magic-context] sqlite acquisition site=${site} lane=background elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=busy`,
|
|
605
|
+
);
|
|
557
606
|
throw error;
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
607
|
+
}
|
|
608
|
+
await new Promise<void>((resolve) =>
|
|
609
|
+
setTimeout(
|
|
610
|
+
resolve,
|
|
611
|
+
Math.min(
|
|
612
|
+
attempts === 1 ? 500 : 1000,
|
|
613
|
+
FOREGROUND_ACQUISITION_BUDGET_MS - elapsed,
|
|
614
|
+
),
|
|
615
|
+
),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Retry admission before any turn work, yielding between short lock attempts. */
|
|
622
|
+
export async function withAsyncPrivilegedWriter<T>(db: Database, operation: () => T): Promise<T> {
|
|
623
|
+
const started = performance.now();
|
|
624
|
+
let attempts = 0;
|
|
625
|
+
for (;;) {
|
|
626
|
+
attempts++;
|
|
627
|
+
try {
|
|
628
|
+
const result = admissionScope.run(true, () =>
|
|
629
|
+
withSqliteTransformPass(() => withPrivilegedWriter(db, operation)),
|
|
630
|
+
);
|
|
631
|
+
const elapsed = performance.now() - started;
|
|
632
|
+
if (elapsed >= 250)
|
|
633
|
+
console.warn(
|
|
634
|
+
`[${new Date().toISOString()}] [magic-context] sqlite acquisition site=privileged_writer lane=foreground elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=acquired`,
|
|
635
|
+
);
|
|
636
|
+
return result;
|
|
637
|
+
} catch (error) {
|
|
638
|
+
// A callback failure can occur after writes; only retry a failed BEGIN.
|
|
639
|
+
if (!(error instanceof SqliteAcquisitionBusyError)) throw error;
|
|
640
|
+
const elapsed = performance.now() - started;
|
|
641
|
+
if (elapsed >= FOREGROUND_ACQUISITION_BUDGET_MS) {
|
|
642
|
+
if (elapsed >= 250)
|
|
643
|
+
console.warn(
|
|
644
|
+
`[${new Date().toISOString()}] [magic-context] sqlite acquisition site=privileged_writer lane=foreground elapsed=${Math.round(elapsed)}ms attempts=${attempts} outcome=busy`,
|
|
645
|
+
);
|
|
646
|
+
throw error instanceof SqliteAcquisitionBusyError
|
|
647
|
+
? error
|
|
648
|
+
: new SqliteAcquisitionBusyError(error);
|
|
649
|
+
}
|
|
650
|
+
await new Promise<void>((resolve) =>
|
|
651
|
+
setTimeout(
|
|
652
|
+
resolve,
|
|
653
|
+
Math.min(
|
|
654
|
+
attempts === 1 ? 500 : 1000,
|
|
655
|
+
FOREGROUND_ACQUISITION_BUDGET_MS - elapsed,
|
|
656
|
+
),
|
|
657
|
+
),
|
|
658
|
+
);
|
|
562
659
|
}
|
|
563
660
|
}
|
|
564
661
|
}
|
|
@@ -587,7 +684,13 @@ export function withPrivilegedWriter<T>(db: Database, operation: () => T): T {
|
|
|
587
684
|
if (nested) {
|
|
588
685
|
db.exec(`SAVEPOINT ${savepoint}`);
|
|
589
686
|
} else {
|
|
590
|
-
|
|
687
|
+
try {
|
|
688
|
+
db.exec("BEGIN IMMEDIATE");
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (transformPassScope.getStore()?.active && isTransientSqliteError(error))
|
|
691
|
+
throw new SqliteAcquisitionBusyError(error);
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
591
694
|
}
|
|
592
695
|
privilegeDepth.set(db, previousDepth + 1);
|
|
593
696
|
try {
|
|
@@ -1,70 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import {
|
|
3
6
|
Database,
|
|
4
7
|
isTransientSqliteError,
|
|
5
|
-
|
|
6
|
-
withPrivilegedWriter
|
|
8
|
+
withAsyncPrivilegedWriter,
|
|
9
|
+
withPrivilegedWriter,
|
|
7
10
|
} from "./sqlite";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
db.exec(
|
|
21
|
-
"CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER); CREATE TABLE result(value TEXT)",
|
|
22
|
-
);
|
|
23
|
-
const exec = db.exec.bind(db);
|
|
24
|
-
let attempts = 0;
|
|
25
|
-
let callbacks = 0;
|
|
26
|
-
const delays: number[] = [];
|
|
27
|
-
wait.mockImplementation((_array, _index, _value, timeout) => {
|
|
28
|
-
delays.push(timeout!);
|
|
29
|
-
return "timed-out";
|
|
30
|
-
});
|
|
31
|
-
const intercepted = spyOn(db, "exec").mockImplementation((sql) => {
|
|
32
|
-
if (sql === "BEGIN IMMEDIATE" && ++attempts < clearsOn)
|
|
33
|
-
throw Object.assign(new Error("locked"), { code: "SQLITE_BUSY" });
|
|
34
|
-
return exec(sql);
|
|
35
|
-
});
|
|
36
|
-
try {
|
|
37
|
-
const result = withPrivilegedWriter(db, () => {
|
|
38
|
-
callbacks++;
|
|
39
|
-
db.prepare("INSERT INTO result VALUES (?)").run("managed");
|
|
40
|
-
return JSON.stringify(db.prepare("SELECT * FROM result").all());
|
|
41
|
-
});
|
|
42
|
-
expect(result).toBe('[{"value":"managed"}]');
|
|
43
|
-
expect(attempts).toBe(clearsOn);
|
|
44
|
-
expect(callbacks).toBe(1);
|
|
45
|
-
expect(delays).toEqual([500, 1000].slice(0, clearsOn - 1));
|
|
46
|
-
} finally {
|
|
47
|
-
intercepted.mockRestore();
|
|
48
|
-
db.close();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
test("exhausted acquisition never invokes callback", () => {
|
|
53
|
-
const db = new Database(":memory:");
|
|
12
|
+
describe("writer acquisition", () => {
|
|
13
|
+
test("exhausted async admission preserves the foreground budget and never invokes the callback", async () => {
|
|
14
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-writer-budget-"));
|
|
15
|
+
const path = join(dir, "context.db");
|
|
16
|
+
const blocker = new Database(path);
|
|
17
|
+
blocker.exec(
|
|
18
|
+
"CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)",
|
|
19
|
+
);
|
|
20
|
+
const db = new Database(path);
|
|
21
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
22
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
54
23
|
let calls = 0;
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
});
|
|
24
|
+
let ticks = 0;
|
|
25
|
+
const timer = setInterval(() => ticks++, 50);
|
|
26
|
+
const started = performance.now();
|
|
59
27
|
try {
|
|
60
|
-
expect((
|
|
61
|
-
|
|
28
|
+
await expect(withAsyncPrivilegedWriter(db, () => calls++)).rejects.toThrow(
|
|
29
|
+
"acquisition remained busy",
|
|
30
|
+
);
|
|
31
|
+
expect(performance.now() - started).toBeGreaterThanOrEqual(16_500);
|
|
32
|
+
expect(performance.now() - started).toBeLessThan(17_500);
|
|
33
|
+
expect(ticks).toBeGreaterThan(100);
|
|
62
34
|
expect(calls).toBe(0);
|
|
35
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
|
|
63
36
|
} finally {
|
|
64
|
-
|
|
37
|
+
clearInterval(timer);
|
|
38
|
+
blocker.exec("ROLLBACK");
|
|
39
|
+
blocker.close();
|
|
65
40
|
db.close();
|
|
41
|
+
rmSync(dir, { recursive: true, force: true });
|
|
66
42
|
}
|
|
67
|
-
});
|
|
43
|
+
}, 30000);
|
|
44
|
+
|
|
68
45
|
test("busy callback rolls back without retrying mutations", () => {
|
|
69
46
|
const db = new Database(":memory:");
|
|
70
47
|
db.exec(
|
|
@@ -81,11 +58,11 @@ describe("writer acquisition backoff", () => {
|
|
|
81
58
|
).toThrow("after acquisition");
|
|
82
59
|
expect(calls).toBe(1);
|
|
83
60
|
expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
|
|
84
|
-
expect(wait).not.toHaveBeenCalled();
|
|
85
61
|
} finally {
|
|
86
62
|
db.close();
|
|
87
63
|
}
|
|
88
64
|
});
|
|
65
|
+
|
|
89
66
|
test("recognizes Bun extended and Node numeric contention codes only", () => {
|
|
90
67
|
expect(isTransientSqliteError({ code: "SQLITE_BUSY_SNAPSHOT" })).toBe(true);
|
|
91
68
|
expect(isTransientSqliteError({ code: "ERR_SQLITE_ERROR", errcode: 5 })).toBe(true);
|
|
@@ -1275,7 +1275,7 @@ function buildAggregateTarget(
|
|
|
1275
1275
|
return any ? "truncated" : "absent";
|
|
1276
1276
|
},
|
|
1277
1277
|
// Open invocations still belong to the current turn; only complete arcs reclaim.
|
|
1278
|
-
canDrop: complete,
|
|
1278
|
+
canDrop: () => complete() && !occurrences.some((occ) => occ.part.hasUserAnswer?.()),
|
|
1279
1279
|
requiresToolArcSkeleton,
|
|
1280
1280
|
// Non-mutating read of the invocation input (the tool_use occurrence
|
|
1281
1281
|
// carries the arguments). Used by smart-drops supersession selection.
|
|
@@ -1375,6 +1375,9 @@ function buildToolTarget(
|
|
|
1375
1375
|
// drop() replaces an invocation part's arguments with a marker, so a
|
|
1376
1376
|
// new drop of one keeps its real arguments instead.
|
|
1377
1377
|
cannotRemove: () => part.setToolInput !== undefined,
|
|
1378
|
+
// Without a paired tool call, an answer marker can forbid automatic dropping,
|
|
1379
|
+
// but must not make ordinary results newly eligible for automatic reclaim.
|
|
1380
|
+
...(part.hasUserAnswer?.() ? { canDrop: () => false } : {}),
|
|
1378
1381
|
inputStringBytes(): number | null {
|
|
1379
1382
|
return part.setToolInput ? toolInputStringBytes(part.getToolInput?.() ?? null) : 0;
|
|
1380
1383
|
},
|
package/src/shared/transcript.ts
CHANGED
|
@@ -78,6 +78,9 @@ export interface TranscriptPart {
|
|
|
78
78
|
/** Discriminator for filter logic. Stable across mutations. */
|
|
79
79
|
readonly kind: TranscriptPartKind;
|
|
80
80
|
|
|
81
|
+
/** True when the host marks this tool result as carrying a user's answer. */
|
|
82
|
+
hasUserAnswer?(): boolean;
|
|
83
|
+
|
|
81
84
|
/**
|
|
82
85
|
* Best-effort identifier for cross-pass tracking. May be:
|
|
83
86
|
* - OpenCode part ID (e.g. "prt_..."), stable across passes.
|