@retaw106/mmc-oc 2.1.2 → 2.2.1
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/agents/memory-maintainer.d.ts +9 -0
- package/dist/agents/memory-maintainer.d.ts.map +1 -1
- package/dist/cli.js +0 -1
- package/dist/config/project-security.d.ts +0 -2
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +0 -5
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +0 -1
- package/dist/{context-gxm0q7tz.js → context-mrvm92n8.js} +3462 -3133
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +0 -6
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +5 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/session-project-backfill.d.ts +1 -1
- package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +8 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +6 -3
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +0 -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-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/default-protection-pressure.test-support.d.ts +37 -0
- package/dist/hooks/magic-context/default-protection-pressure.test-support.d.ts.map +1 -0
- package/dist/hooks/magic-context/drop-stale-reduce-calls.d.ts +4 -0
- package/dist/hooks/magic-context/drop-stale-reduce-calls.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/latest-thinking-recovery.d.ts +61 -0
- package/dist/hooks/magic-context/latest-thinking-recovery.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts +9 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +2 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +8 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +4 -2
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-injection-stripper.d.ts +7 -0
- package/dist/hooks/magic-context/system-injection-stripper.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +5 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +23 -7
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +0 -6
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-ys43j25b.js → index-5b7x80ec.js} +126 -44
- package/dist/{index-cszxqtev.js → index-btp5tdc8.js} +180 -31
- package/dist/{index-pynrjyjy.js → index-g2z2k5m4.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-e4dby3pb.js → read-session-chunk-2xm4sc4y.js} +1 -1
- package/dist/{recomp-orchestrator-xg3jm3s2.js → recomp-orchestrator-frmwj9hz.js} +3 -3
- package/dist/shared/pi-executable.d.ts +1 -1
- package/dist/shared/pi-executable.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +1 -1
- package/dist/shared/subagent-runner.d.ts +22 -1
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/tools/light-descriptions.d.ts +1 -1
- package/dist/tools/light-descriptions.d.ts.map +1 -1
- package/dist/tools/parameter-descriptions.d.ts +1 -3
- package/dist/tools/parameter-descriptions.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +15 -2
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/server.js +3463 -2906
- package/dist/web/main.js +8 -2
- package/dist/{wrapup-orchestrator-9spy01n8.js → wrapup-orchestrator-yknks3ad.js} +3 -3
- package/package.json +1 -1
- package/src/agents/memory-maintainer.ts +12 -0
- package/src/config/index.test.ts +0 -17
- package/src/config/project-security.test.ts +0 -8
- package/src/config/project-security.ts +0 -9
- package/src/config/protected-tools.test.ts +72 -0
- package/src/config/schema/magic-context.test.ts +0 -6
- package/src/config/schema/magic-context.ts +1 -10
- package/src/shared/fold-render-lock-fence.test.ts +84 -0
- package/src/shared/fold-render-lock-fixtures.test.ts +270 -0
- package/src/shared/pi-executable.ts +3 -3
- package/src/shared/rpc-utils.test.ts +4 -5
- package/src/shared/sqlite-640-review.test.ts +184 -0
- package/src/shared/sqlite-context-pragmas.test.ts +105 -0
- package/src/shared/sqlite-writer-diagnostics.test.ts +136 -0
- package/src/shared/subagent-runner.ts +24 -0
- package/src/shared/tag-transcript.test.ts +55 -0
- package/src/shared/tag-transcript.ts +6 -0
- package/src/shared/write-transaction-attribution.test.ts +4 -0
- package/src/shared/write-transaction-timing.test.ts +39 -0
- package/dist/hooks/magic-context/historian-state-file.d.ts +0 -43
- package/dist/hooks/magic-context/historian-state-file.d.ts.map +0 -1
- package/dist/shared/auto-update-provenance.d.ts +0 -9
- package/dist/shared/auto-update-provenance.d.ts.map +0 -1
- package/src/shared/auto-update-provenance.ts +0 -42
|
@@ -64,8 +64,8 @@ const OMP_PI_ARC_COMMAND =
|
|
|
64
64
|
"C:\\Users\\qiiks\\.bun\\bin\\omp.exe C:\\Users\\qiiks\\AppData\\Roaming\\npm\\node_modules\\@oh-my-pi\\pi-coding-agent\\dist\\cli.js";
|
|
65
65
|
const BUN_PI_CHILD_COMMAND =
|
|
66
66
|
"C:\\Users\\qiiks\\.bun\\bin\\bun.exe C:\\Users\\qiiks\\AppData\\Roaming\\npm\\node_modules\\@oh-my-pi\\pi-coding-agent\\dist\\cli.js";
|
|
67
|
-
//
|
|
68
|
-
//
|
|
67
|
+
// Bare Pi and OMP images are insufficient to verify a harness without a Pi arc;
|
|
68
|
+
// these discovery fixtures use bare `pi.exe` for that shared negative case.
|
|
69
69
|
const BARE_PI_COMMAND = "C:\\Users\\qiiks\\.bun\\bin\\pi.exe";
|
|
70
70
|
|
|
71
71
|
function padWmi(value: number, width = 2): string {
|
|
@@ -735,9 +735,7 @@ describe("proving a discovery record stale from the record's own start time", ()
|
|
|
735
735
|
});
|
|
736
736
|
|
|
737
737
|
test("an unreadable start time with an image no host can run is a reused PID", () => {
|
|
738
|
-
|
|
739
|
-
// removed (T-003), so an OMP image cannot be an mmc RPC host.
|
|
740
|
-
for (const imageName of ["svchost.exe", "WinAutomation.UserAgent.exe", "omp.exe"]) {
|
|
738
|
+
for (const imageName of ["svchost.exe", "WinAutomation.UserAgent.exe"]) {
|
|
741
739
|
expect(
|
|
742
740
|
isPidIdentityPlausible(windowsRecord(RECORDED), {
|
|
743
741
|
startTime: null,
|
|
@@ -753,6 +751,7 @@ describe("proving a discovery record stale from the record's own start time", ()
|
|
|
753
751
|
"opencode.exe",
|
|
754
752
|
"OpenChamber.exe",
|
|
755
753
|
"pi.exe",
|
|
754
|
+
"omp.exe",
|
|
756
755
|
"bun.exe",
|
|
757
756
|
"node.exe",
|
|
758
757
|
"ck-mc.exe",
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { Database, SqliteAcquisitionBusyError, withAsyncPrivilegedWriter } from "./sqlite";
|
|
3
|
+
|
|
4
|
+
function fixture() {
|
|
5
|
+
const db = new Database(":memory:");
|
|
6
|
+
db.exec("CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)");
|
|
7
|
+
db.exec("CREATE TABLE writes(value INTEGER)");
|
|
8
|
+
return db;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("review: shared async admission rolls back callback busy without retrying side effects", async () => {
|
|
12
|
+
const db = fixture();
|
|
13
|
+
let callbacks = 0;
|
|
14
|
+
let recoveries = 0;
|
|
15
|
+
try {
|
|
16
|
+
await expect(
|
|
17
|
+
withAsyncPrivilegedWriter(
|
|
18
|
+
db,
|
|
19
|
+
() => {
|
|
20
|
+
callbacks++;
|
|
21
|
+
db.prepare("INSERT INTO writes VALUES (1)").run();
|
|
22
|
+
throw new SqliteAcquisitionBusyError(new Error("nested acquisition busy"));
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
beforeRetry: () => {
|
|
26
|
+
recoveries++;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
),
|
|
30
|
+
).rejects.toThrow("SQLite writer acquisition remained busy");
|
|
31
|
+
expect(callbacks).toBe(1);
|
|
32
|
+
expect(recoveries).toBe(0);
|
|
33
|
+
expect(db.prepare("SELECT * FROM writes").all()).toEqual([]);
|
|
34
|
+
expect(db.inTransaction).toBe(false);
|
|
35
|
+
} finally {
|
|
36
|
+
db.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("review: default OpenCode empty admission retries BEGIN only and preserves output bytes", async () => {
|
|
41
|
+
const db = fixture();
|
|
42
|
+
// Capture the currently routed exec; delegate through the original
|
|
43
|
+
// reference and restore it on exit (writer routing stays intact).
|
|
44
|
+
const originalExec = db.exec;
|
|
45
|
+
let attempts = 0;
|
|
46
|
+
db.exec = (sql) => {
|
|
47
|
+
if (sql === "BEGIN IMMEDIATE" && ++attempts === 1)
|
|
48
|
+
throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
49
|
+
return originalExec.call(db, sql);
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const output = [{ role: "user", content: "§1§ managed input" }];
|
|
53
|
+
const before = JSON.stringify(output);
|
|
54
|
+
await withAsyncPrivilegedWriter(db, () => undefined);
|
|
55
|
+
expect(attempts).toBe(2);
|
|
56
|
+
expect(JSON.stringify(output)).toBe(before);
|
|
57
|
+
expect(db.inTransaction).toBe(false);
|
|
58
|
+
} finally {
|
|
59
|
+
db.exec = originalExec;
|
|
60
|
+
db.close();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("review: COMMIT busy after empty admission is surfaced once and transaction is released", async () => {
|
|
65
|
+
const db = fixture();
|
|
66
|
+
const originalExec = db.exec;
|
|
67
|
+
let callbacks = 0;
|
|
68
|
+
let commits = 0;
|
|
69
|
+
db.exec = (sql) => {
|
|
70
|
+
if (sql === "COMMIT") {
|
|
71
|
+
commits++;
|
|
72
|
+
// Controlled fault injection (not a real rollback-journal lock):
|
|
73
|
+
// verifies the writer surfaces the COMMIT rejection exactly once,
|
|
74
|
+
// never replays the callback, and releases the transaction.
|
|
75
|
+
throw Object.assign(new Error("reader holds rollback-journal lock"), {
|
|
76
|
+
code: "SQLITE_BUSY",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return originalExec.call(db, sql);
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
await expect(
|
|
83
|
+
withAsyncPrivilegedWriter(db, () => {
|
|
84
|
+
callbacks++;
|
|
85
|
+
}),
|
|
86
|
+
).rejects.toThrow("reader holds rollback-journal lock");
|
|
87
|
+
expect(callbacks).toBe(1);
|
|
88
|
+
expect(commits).toBe(1);
|
|
89
|
+
expect(db.inTransaction).toBe(false);
|
|
90
|
+
} finally {
|
|
91
|
+
db.exec = originalExec;
|
|
92
|
+
db.close();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Negative control for the COMMIT fault above: with the injection removed,
|
|
97
|
+
// the same empty admission resolves and commits normally.
|
|
98
|
+
test("review: uninstrumented commit resolves normally after empty admission", async () => {
|
|
99
|
+
const db = fixture();
|
|
100
|
+
let callbacks = 0;
|
|
101
|
+
try {
|
|
102
|
+
await withAsyncPrivilegedWriter(db, () => {
|
|
103
|
+
callbacks++;
|
|
104
|
+
});
|
|
105
|
+
expect(callbacks).toBe(1);
|
|
106
|
+
expect(db.inTransaction).toBe(false);
|
|
107
|
+
} finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("review: three busy admissions still deliver the callback result exactly once", async () => {
|
|
113
|
+
const db = fixture();
|
|
114
|
+
const originalExec = db.exec;
|
|
115
|
+
let attempts = 0;
|
|
116
|
+
let callbacks = 0;
|
|
117
|
+
let recoveries = 0;
|
|
118
|
+
db.exec = (sql) => {
|
|
119
|
+
if (sql === "BEGIN IMMEDIATE" && ++attempts <= 3)
|
|
120
|
+
throw Object.assign(new Error("busy"), { code: "SQLITE_BUSY" });
|
|
121
|
+
return originalExec.call(db, sql);
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
const payload = { messages: [{ role: "user", content: "unchanged" }] };
|
|
125
|
+
const before = JSON.stringify(payload);
|
|
126
|
+
const result = await withAsyncPrivilegedWriter(
|
|
127
|
+
db,
|
|
128
|
+
() => {
|
|
129
|
+
callbacks++;
|
|
130
|
+
db.prepare("INSERT INTO writes VALUES (1)").run();
|
|
131
|
+
return payload;
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
budgetMs: 10_000,
|
|
135
|
+
// Count-only observer: never adds side effects between retries.
|
|
136
|
+
beforeRetry: () => {
|
|
137
|
+
recoveries++;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
expect(attempts).toBe(4);
|
|
142
|
+
expect(callbacks).toBe(1);
|
|
143
|
+
expect(recoveries).toBe(3);
|
|
144
|
+
expect(JSON.stringify(result)).toBe(before);
|
|
145
|
+
expect(db.prepare("SELECT * FROM writes").all()).toEqual([{ value: 1 }]);
|
|
146
|
+
expect(db.inTransaction).toBe(false);
|
|
147
|
+
} finally {
|
|
148
|
+
db.exec = originalExec;
|
|
149
|
+
db.close();
|
|
150
|
+
}
|
|
151
|
+
}, 10_000);
|
|
152
|
+
|
|
153
|
+
// Control: no admission contention — one attempt, one callback, same delivery.
|
|
154
|
+
test("review: uninstrumented admission delivers the callback result on the first attempt", async () => {
|
|
155
|
+
const db = fixture();
|
|
156
|
+
const originalExec = db.exec;
|
|
157
|
+
let admissions = 0;
|
|
158
|
+
db.exec = (sql) => {
|
|
159
|
+
if (sql === "BEGIN IMMEDIATE") admissions++;
|
|
160
|
+
return originalExec.call(db, sql);
|
|
161
|
+
};
|
|
162
|
+
let callbacks = 0;
|
|
163
|
+
try {
|
|
164
|
+
const payload = { messages: [{ role: "user", content: "unchanged" }] };
|
|
165
|
+
const before = JSON.stringify(payload);
|
|
166
|
+
const result = await withAsyncPrivilegedWriter(
|
|
167
|
+
db,
|
|
168
|
+
() => {
|
|
169
|
+
callbacks++;
|
|
170
|
+
db.prepare("INSERT INTO writes VALUES (1)").run();
|
|
171
|
+
return payload;
|
|
172
|
+
},
|
|
173
|
+
{ budgetMs: 10_000 },
|
|
174
|
+
);
|
|
175
|
+
expect(JSON.stringify(result)).toBe(before);
|
|
176
|
+
expect(admissions).toBe(1);
|
|
177
|
+
expect(callbacks).toBe(1);
|
|
178
|
+
expect(db.prepare("SELECT * FROM writes").all()).toEqual([{ value: 1 }]);
|
|
179
|
+
expect(db.inTransaction).toBe(false);
|
|
180
|
+
} finally {
|
|
181
|
+
db.exec = originalExec;
|
|
182
|
+
db.close();
|
|
183
|
+
}
|
|
184
|
+
}, 10_000);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
closeDatabase,
|
|
7
|
+
openDatabase,
|
|
8
|
+
openDatabaseAsync,
|
|
9
|
+
} from "../features/magic-context/storage-db";
|
|
10
|
+
import { Database } from "./sqlite";
|
|
11
|
+
import { configureContextDatabasePragmas } from "./sqlite-context-pragmas";
|
|
12
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
13
|
+
|
|
14
|
+
const roots: string[] = [];
|
|
15
|
+
function fixturePath(): string {
|
|
16
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "mc-context-pragmas-"));
|
|
17
|
+
roots.push(root);
|
|
18
|
+
return join(root, "context.db");
|
|
19
|
+
}
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
closeDatabase();
|
|
22
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("context database durability", () => {
|
|
26
|
+
it("fresh synchronous open uses WAL and synchronous=NORMAL", () => {
|
|
27
|
+
const db = openDatabase(fixturePath());
|
|
28
|
+
expect(db?.prepare("PRAGMA journal_mode").get()).toEqual({ journal_mode: "wal" });
|
|
29
|
+
expect(db?.prepare("PRAGMA synchronous").get()).toEqual({ synchronous: 1 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("fresh async open uses WAL and synchronous=NORMAL", async () => {
|
|
33
|
+
const db = await openDatabaseAsync({ dbPath: fixturePath() });
|
|
34
|
+
expect(db?.prepare("PRAGMA journal_mode").get()).toEqual({ journal_mode: "wal" });
|
|
35
|
+
expect(db?.prepare("PRAGMA synchronous").get()).toEqual({ synchronous: 1 });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("refuses NORMAL when SQLite cannot activate WAL", () => {
|
|
39
|
+
const db = new Database(fixturePath());
|
|
40
|
+
const prepare = db.prepare.bind(db);
|
|
41
|
+
const spy = spyOn(db, "prepare").mockImplementation((sql: string) =>
|
|
42
|
+
prepare(sql === "PRAGMA journal_mode=WAL" ? "PRAGMA journal_mode" : sql),
|
|
43
|
+
);
|
|
44
|
+
try {
|
|
45
|
+
expect(() => configureContextDatabasePragmas(db)).toThrow("requires WAL");
|
|
46
|
+
expect(prepare("PRAGMA synchronous").get()).toEqual({ synchronous: 2 });
|
|
47
|
+
} finally {
|
|
48
|
+
spy.mockRestore();
|
|
49
|
+
db.close();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("read-only inspection leaves a rollback-journal store unchanged", () => {
|
|
54
|
+
const path = fixturePath();
|
|
55
|
+
const writer = new Database(path);
|
|
56
|
+
writer.exec("CREATE TABLE probe(id INTEGER)");
|
|
57
|
+
writer.close();
|
|
58
|
+
const bytes = readFileSync(path);
|
|
59
|
+
const reader = new Database(path, { readonly: true });
|
|
60
|
+
try {
|
|
61
|
+
configureContextDatabasePragmas(reader, true);
|
|
62
|
+
expect(reader.prepare("PRAGMA journal_mode").get()).toEqual({ journal_mode: "delete" });
|
|
63
|
+
expect(reader.prepare("PRAGMA synchronous").get()).toEqual({ synchronous: 2 });
|
|
64
|
+
} finally {
|
|
65
|
+
reader.close();
|
|
66
|
+
}
|
|
67
|
+
expect(readFileSync(path)).toEqual(bytes);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("an independently reopened WAL writer receives the connection-local policy", () => {
|
|
71
|
+
const path = fixturePath();
|
|
72
|
+
const first = new Database(path);
|
|
73
|
+
configureContextDatabasePragmas(first);
|
|
74
|
+
first.close();
|
|
75
|
+
const reopened = new Database(path);
|
|
76
|
+
try {
|
|
77
|
+
// Bun defaults WAL to NORMAL; Node defaults it to FULL. Exercise an
|
|
78
|
+
// explicit FULL connection so the assertion is portable to both.
|
|
79
|
+
reopened.exec("PRAGMA synchronous=FULL");
|
|
80
|
+
expect(reopened.prepare("PRAGMA synchronous").get()).toEqual({ synchronous: 2 });
|
|
81
|
+
configureContextDatabasePragmas(reopened);
|
|
82
|
+
expect(reopened.prepare("PRAGMA synchronous").get()).toEqual({ synchronous: 1 });
|
|
83
|
+
} finally {
|
|
84
|
+
reopened.close();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("WAL configuration releases its read cursor before a sibling commits", () => {
|
|
89
|
+
const path = fixturePath();
|
|
90
|
+
const db = new Database(path);
|
|
91
|
+
const sibling = new Database(path);
|
|
92
|
+
try {
|
|
93
|
+
configureContextDatabasePragmas(db);
|
|
94
|
+
db.exec(
|
|
95
|
+
"CREATE TABLE probe(id INTEGER PRIMARY KEY, value INTEGER); INSERT INTO probe VALUES (1, 0)",
|
|
96
|
+
);
|
|
97
|
+
sibling.exec("UPDATE probe SET value = 1 WHERE id = 1");
|
|
98
|
+
db.exec("UPDATE probe SET value = 2 WHERE id = 1");
|
|
99
|
+
expect(db.prepare("SELECT value FROM probe").get()).toEqual({ value: 2 });
|
|
100
|
+
} finally {
|
|
101
|
+
db.close();
|
|
102
|
+
sibling.close();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createTestTempDirFromPath } from "./test-temp-dir";
|
|
6
|
+
|
|
7
|
+
// The scenario runs in a child process with NODE_ENV=production because the
|
|
8
|
+
// shared logger is silent under test. It imports storage-db for its side effect
|
|
9
|
+
// of registering the real log sink, then drives three privileged writes and one
|
|
10
|
+
// background writer through two connections on a throwaway database:
|
|
11
|
+
// wait - another connection holds the lock for 300 ms, the hold is short
|
|
12
|
+
// hold - the lock comes at once, the operation keeps it for 300 ms
|
|
13
|
+
// fast - neither crosses the threshold, so nothing is logged
|
|
14
|
+
// async - beginSqliteWriterAsync acquires at once and holds for 300 ms
|
|
15
|
+
const scenario = `
|
|
16
|
+
await import(${JSON.stringify(new URL("../features/magic-context/storage-db.ts", import.meta.url).href)});
|
|
17
|
+
const sqlite = await import(${JSON.stringify(new URL("./sqlite.ts", import.meta.url).href)});
|
|
18
|
+
const logger = await import(${JSON.stringify(new URL("./logger.ts", import.meta.url).href)});
|
|
19
|
+
const dbPath = process.env.SCENARIO_DB_PATH;
|
|
20
|
+
const holder = new sqlite.Database(dbPath);
|
|
21
|
+
holder.exec("PRAGMA journal_mode=WAL; CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER)");
|
|
22
|
+
const writer = new sqlite.Database(dbPath);
|
|
23
|
+
const spin = (ms) => { const end = performance.now() + ms; while (performance.now() < end) {} };
|
|
24
|
+
|
|
25
|
+
holder.exec("BEGIN IMMEDIATE");
|
|
26
|
+
setTimeout(() => holder.exec("COMMIT"), 300);
|
|
27
|
+
await sqlite.withAsyncPrivilegedWriter(writer, () => undefined);
|
|
28
|
+
|
|
29
|
+
await sqlite.withAsyncPrivilegedWriter(writer, () => spin(300));
|
|
30
|
+
|
|
31
|
+
await sqlite.withAsyncPrivilegedWriter(writer, () => undefined);
|
|
32
|
+
|
|
33
|
+
await sqlite.beginSqliteWriterAsync(writer, "historian-publish");
|
|
34
|
+
spin(300);
|
|
35
|
+
writer.exec("COMMIT");
|
|
36
|
+
|
|
37
|
+
logger.flushLogger();
|
|
38
|
+
writer.close();
|
|
39
|
+
holder.close();
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
let root: string | null = null;
|
|
43
|
+
let logLines: string[] = [];
|
|
44
|
+
let consoleOutput = "";
|
|
45
|
+
|
|
46
|
+
function field(line: string, name: string): number {
|
|
47
|
+
const value = new RegExp(`\\b${name}=(\\d+)`).exec(line)?.[1];
|
|
48
|
+
if (value === undefined) throw new Error(`${name} missing from: ${line}`);
|
|
49
|
+
return Number(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("sqlite writer diagnostics", () => {
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
root = createTestTempDirFromPath(join(tmpdir(), "mc-sqlite-writer-diagnostics-"));
|
|
55
|
+
const logPath = join(root, "magic-context.log");
|
|
56
|
+
const env: Record<string, string> = {
|
|
57
|
+
...(process.env as Record<string, string>),
|
|
58
|
+
NODE_ENV: "production",
|
|
59
|
+
MMC_LOG_PATH: logPath,
|
|
60
|
+
MMC_STORAGE_DIR: join(root, "storage"),
|
|
61
|
+
SCENARIO_DB_PATH: join(root, "writer.db"),
|
|
62
|
+
};
|
|
63
|
+
for (const key of [
|
|
64
|
+
"HOME",
|
|
65
|
+
"OPENCODE_CONFIG_DIR",
|
|
66
|
+
"XDG_DATA_HOME",
|
|
67
|
+
"XDG_CONFIG_HOME",
|
|
68
|
+
"XDG_STATE_HOME",
|
|
69
|
+
"XDG_CACHE_HOME",
|
|
70
|
+
]) {
|
|
71
|
+
env[key] = join(root, key);
|
|
72
|
+
mkdirSync(env[key]);
|
|
73
|
+
}
|
|
74
|
+
const child = Bun.spawn({
|
|
75
|
+
windowsHide: true,
|
|
76
|
+
cmd: ["bun", "--eval", scenario],
|
|
77
|
+
cwd: import.meta.dir,
|
|
78
|
+
env,
|
|
79
|
+
stdout: "pipe",
|
|
80
|
+
stderr: "pipe",
|
|
81
|
+
});
|
|
82
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
83
|
+
child.exited,
|
|
84
|
+
new Response(child.stdout).text(),
|
|
85
|
+
new Response(child.stderr).text(),
|
|
86
|
+
]);
|
|
87
|
+
expect(exitCode, stderr).toBe(0);
|
|
88
|
+
consoleOutput = stdout + stderr;
|
|
89
|
+
logLines = existsSync(logPath)
|
|
90
|
+
? readFileSync(logPath, "utf8")
|
|
91
|
+
.split("\n")
|
|
92
|
+
.filter((line) => line.includes("sqlite writer"))
|
|
93
|
+
: [];
|
|
94
|
+
}, 30_000);
|
|
95
|
+
|
|
96
|
+
afterAll(() => {
|
|
97
|
+
if (root) rmSync(root, { recursive: true, force: true });
|
|
98
|
+
root = null;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("nothing reaches the host console", () => {
|
|
102
|
+
expect(consoleOutput).not.toContain("[magic-context]");
|
|
103
|
+
expect(consoleOutput).not.toContain("sqlite writer");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a writer that waited for the lock reports the wait, not its short hold", () => {
|
|
107
|
+
const waited = logLines.find((line) => line.includes("attempts=2"));
|
|
108
|
+
if (!waited) throw new Error(`no retried acquisition logged:\n${logLines.join("\n")}`);
|
|
109
|
+
expect(waited).toContain("[magic-context] sqlite writer site=privileged_writer");
|
|
110
|
+
expect(waited).toContain("lane=foreground");
|
|
111
|
+
expect(waited).toContain("outcome=committed");
|
|
112
|
+
expect(field(waited, "acquire_ms")).toBeGreaterThanOrEqual(250);
|
|
113
|
+
expect(field(waited, "hold_ms")).toBeLessThan(250);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a writer that held the lock reports the hold, not its quick acquisition", () => {
|
|
117
|
+
const held = logLines.filter(
|
|
118
|
+
(line) => line.includes("site=privileged_writer") && line.includes("attempts=1"),
|
|
119
|
+
);
|
|
120
|
+
expect(held).toHaveLength(1);
|
|
121
|
+
expect(field(held[0], "acquire_ms")).toBeLessThan(250);
|
|
122
|
+
expect(field(held[0], "hold_ms")).toBeGreaterThanOrEqual(300);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("an async background writer reports its hold under its own site", () => {
|
|
126
|
+
const held = logLines.filter((line) => line.includes("site=historian-publish"));
|
|
127
|
+
expect(held).toHaveLength(1);
|
|
128
|
+
expect(held[0]).toContain("lane=background");
|
|
129
|
+
expect(field(held[0], "acquire_ms")).toBeLessThan(250);
|
|
130
|
+
expect(field(held[0], "hold_ms")).toBeGreaterThanOrEqual(300);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("a fast writer stays out of the log", () => {
|
|
134
|
+
expect(logLines.filter((line) => line.includes("site=privileged_writer"))).toHaveLength(2);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -31,6 +31,17 @@
|
|
|
31
31
|
|
|
32
32
|
import type { ModelInput } from "./model-resolution";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Hard ceiling on historian carrier steps per invocation (upstream 1c8526be
|
|
36
|
+
* parity: `hidden-agent-registrations` caps historian/recomp/editor at 4 —
|
|
37
|
+
* each pass emits one XML answer, so a small fixed cap bounds a runaway
|
|
38
|
+
* zero-tool loop; a per-request output cap or a timeout cannot bound step
|
|
39
|
+
* count). Shared by both harnesses: the OpenCode registration and executor
|
|
40
|
+
* safeguard clamp to it, and the Pi runner enforces it against assistant
|
|
41
|
+
* steps in the child's event stream.
|
|
42
|
+
*/
|
|
43
|
+
export const HISTORIAN_STEP_CAP = 4;
|
|
44
|
+
|
|
34
45
|
/**
|
|
35
46
|
* Configuration for one subagent invocation.
|
|
36
47
|
*
|
|
@@ -84,6 +95,16 @@ export interface SubagentRunOptions {
|
|
|
84
95
|
*/
|
|
85
96
|
thinkingLevel?: string | undefined;
|
|
86
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Pi only: hard cap on assistant steps for this child run (kill with
|
|
100
|
+
* `step_limit` past it). OpenCode ignores this field — its step caps ride
|
|
101
|
+
* the host agent registration plus the hidden-child executor wiring.
|
|
102
|
+
* Absent on Pi → the historian default (`HISTORIAN_STEP_CAP`); used by
|
|
103
|
+
* the Pi maintenance runner (stage A fixed cap, stage B per-round
|
|
104
|
+
* remaining-budget cap).
|
|
105
|
+
*/
|
|
106
|
+
stepCap?: number | undefined;
|
|
107
|
+
|
|
87
108
|
/**
|
|
88
109
|
* Optional progress callback. The runner invokes it for milestone events
|
|
89
110
|
* during the run: spawn, first event received, terminal stop reason
|
|
@@ -158,6 +179,8 @@ export type SubagentProgressEvent =
|
|
|
158
179
|
* - `reason`: failure category, one of:
|
|
159
180
|
* - `"invalid_prompt"`: a known zero-tool child was given no system prompt
|
|
160
181
|
* - `"timeout"`: hit `timeoutMs` before the child finished
|
|
182
|
+
* - `"step_limit"`: the child exceeded its per-agent step cap
|
|
183
|
+
* (historian; `HISTORIAN_STEP_CAP`)
|
|
161
184
|
* - `"abort"`: caller's `signal` was triggered
|
|
162
185
|
* - `"model_failed"`: every configured model + fallback returned an error
|
|
163
186
|
* - `"truncated"`: child stopped because model output hit length limits
|
|
@@ -203,6 +226,7 @@ export type SubagentRunResult =
|
|
|
203
226
|
| "invalid_prompt"
|
|
204
227
|
| "timeout"
|
|
205
228
|
| "abort"
|
|
229
|
+
| "step_limit"
|
|
206
230
|
| "model_failed"
|
|
207
231
|
| "truncated"
|
|
208
232
|
| "spawn_failed"
|
|
@@ -177,6 +177,61 @@ class FakeDb {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
describe("tagTranscript tool aggregation", () => {
|
|
180
|
+
it("exposes exact part references in mutationParts for aggregate, text, and tool targets", () => {
|
|
181
|
+
const tagger = new FakeTagger();
|
|
182
|
+
const firstUse = new TestPart("tool_use", "read:32", '{"file":"long-a"}');
|
|
183
|
+
const firstResult = new TestPart("tool_result", "read:32", "r1");
|
|
184
|
+
const textPart = new TestPart("text", undefined as unknown as string, "plain text");
|
|
185
|
+
const toolPart = new TestPart("tool_use", "read:77", '{"file":"other"}');
|
|
186
|
+
const transcript: Transcript = {
|
|
187
|
+
harness: "pi",
|
|
188
|
+
messages: [
|
|
189
|
+
{ info: { id: "assistant-1", role: "assistant" }, parts: [firstUse, textPart] },
|
|
190
|
+
{ info: { id: "user-1", role: "user" }, parts: [firstResult] },
|
|
191
|
+
{ info: { id: "assistant-2", role: "assistant" }, parts: [toolPart] },
|
|
192
|
+
],
|
|
193
|
+
commit() {},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const { targets } = tagTranscript(
|
|
197
|
+
"session-1",
|
|
198
|
+
transcript,
|
|
199
|
+
tagger,
|
|
200
|
+
new FakeDb() as unknown as ContextDatabase,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Aggregate target: the projection carries the live part objects and
|
|
204
|
+
// the owner-scoped message info stubs, in occurrence order.
|
|
205
|
+
const aggregateTag = tagger.getToolTag("session-1", "read:32", "assistant-1");
|
|
206
|
+
const aggregate = targets.get(aggregateTag ?? -1);
|
|
207
|
+
expect(aggregate?.mutationParts).toEqual([
|
|
208
|
+
{
|
|
209
|
+
message: { info: { id: "assistant-1", role: "assistant" }, parts: [] },
|
|
210
|
+
part: firstUse,
|
|
211
|
+
},
|
|
212
|
+
{ message: { info: { id: "user-1", role: "user" }, parts: [] }, part: firstResult },
|
|
213
|
+
]);
|
|
214
|
+
expect(aggregate?.mutationParts?.[0]?.part).toBe(firstUse);
|
|
215
|
+
expect(aggregate?.mutationParts?.[1]?.part).toBe(firstResult);
|
|
216
|
+
|
|
217
|
+
// Text and single-tool targets: exact single-part projection.
|
|
218
|
+
let textTarget: { mutationParts?: unknown } | undefined;
|
|
219
|
+
let toolTarget: { mutationParts?: unknown } | undefined;
|
|
220
|
+
for (const target of targets.values()) {
|
|
221
|
+
const parts = (target as { mutationParts?: Array<{ part: unknown }> }).mutationParts;
|
|
222
|
+
if (parts?.length === 1 && parts[0].part === textPart) textTarget = target;
|
|
223
|
+
if (parts?.length === 1 && parts[0].part === toolPart) toolTarget = target;
|
|
224
|
+
}
|
|
225
|
+
expect(textTarget).toBeDefined();
|
|
226
|
+
expect(toolTarget).toBeDefined();
|
|
227
|
+
expect(
|
|
228
|
+
(textTarget as { mutationParts: Array<{ part: unknown }> }).mutationParts[0].part,
|
|
229
|
+
).toBe(textPart);
|
|
230
|
+
expect(
|
|
231
|
+
(toolTarget as { mutationParts: Array<{ part: unknown }> }).mutationParts[0].part,
|
|
232
|
+
).toBe(toolPart);
|
|
233
|
+
});
|
|
234
|
+
|
|
180
235
|
it("keeps repeated callIds in separate owner-scoped aggregate targets", () => {
|
|
181
236
|
const tagger = new FakeTagger();
|
|
182
237
|
const firstUse = new TestPart("tool_use", "read:32", '{"file":"long-a"}');
|
|
@@ -1157,6 +1157,10 @@ function buildAggregateTarget(
|
|
|
1157
1157
|
};
|
|
1158
1158
|
|
|
1159
1159
|
return {
|
|
1160
|
+
mutationParts: occurrences.map((occ) => ({
|
|
1161
|
+
message: { info: occ.message.info, parts: [] },
|
|
1162
|
+
part: occ.part,
|
|
1163
|
+
})),
|
|
1160
1164
|
measureReclaim(skeleton) {
|
|
1161
1165
|
let beforeTools = 0;
|
|
1162
1166
|
let afterTools = 0;
|
|
@@ -1319,6 +1323,7 @@ function buildTextTarget(
|
|
|
1319
1323
|
textPrefix: string,
|
|
1320
1324
|
): TagTarget {
|
|
1321
1325
|
return {
|
|
1326
|
+
mutationParts: [{ message: { info: message.info, parts: [] }, part }],
|
|
1322
1327
|
textPrefix,
|
|
1323
1328
|
setContent(content: string): boolean {
|
|
1324
1329
|
return part.setText(content);
|
|
@@ -1351,6 +1356,7 @@ function buildToolTarget(
|
|
|
1351
1356
|
tagId: number,
|
|
1352
1357
|
): TagTarget {
|
|
1353
1358
|
return {
|
|
1359
|
+
mutationParts: [{ message: { info: message.info, parts: [] }, part }],
|
|
1354
1360
|
setContent(content: string): boolean {
|
|
1355
1361
|
return setToolContentOrText(part, content);
|
|
1356
1362
|
},
|
|
@@ -87,6 +87,10 @@ const nonBeginCoveredSites: Array<{ path: string; site: string }> = [
|
|
|
87
87
|
path: "packages/plugin/src/features/magic-context/message-fts-rowid-map.ts",
|
|
88
88
|
site: "message_fts_rowid_backfill",
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
path: "packages/plugin/src/hooks/magic-context/lkg-persist.ts",
|
|
92
|
+
site: "lkg_stale_gc",
|
|
93
|
+
},
|
|
90
94
|
];
|
|
91
95
|
|
|
92
96
|
let scenarioRoot: string | null = null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
|
+
import * as logger from "./logger";
|
|
3
|
+
import { logSlowWriteTransaction } from "./write-transaction-timing";
|
|
4
|
+
|
|
5
|
+
test("slow-write diagnostics distinguish preparation from held rendering and writes", () => {
|
|
6
|
+
const log = spyOn(logger, "log").mockImplementation(() => {});
|
|
7
|
+
try {
|
|
8
|
+
logSlowWriteTransaction("fold", 100, 1000, 1400, {
|
|
9
|
+
pre_metadata: 1,
|
|
10
|
+
pre_onFoldPrepare: 200,
|
|
11
|
+
staleCheck: 5,
|
|
12
|
+
m1Render: 100,
|
|
13
|
+
persistCachedM0: 600,
|
|
14
|
+
sessionMeta: 5,
|
|
15
|
+
onFoldCommit: 10,
|
|
16
|
+
commit: 580,
|
|
17
|
+
});
|
|
18
|
+
expect(log.mock.calls).toEqual([
|
|
19
|
+
[
|
|
20
|
+
"[magic-context] slow write transaction: site=fold held=1300.0ms pre_metadata=1.0ms pre_onFoldPrepare=200.0ms staleCheck=5.0ms m1Render=100.0ms persistCachedM0=600.0ms sessionMeta=5.0ms onFoldCommit=10.0ms commit=580.0ms",
|
|
21
|
+
],
|
|
22
|
+
]);
|
|
23
|
+
logSlowWriteTransaction("fast", 100, 1000, 101);
|
|
24
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
25
|
+
} finally {
|
|
26
|
+
log.mockRestore();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("step diagnostics cannot make a committed write appear to fail", () => {
|
|
31
|
+
const log = spyOn(logger, "log").mockImplementation(() => {
|
|
32
|
+
throw new Error("logging failed");
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
expect(() => logSlowWriteTransaction("fold", 0, 1000, 2000, { commit: 10 })).not.toThrow();
|
|
36
|
+
} finally {
|
|
37
|
+
log.mockRestore();
|
|
38
|
+
}
|
|
39
|
+
});
|