@retaw106/mmc-oc 2.1.2 → 2.2.0

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.
Files changed (98) hide show
  1. package/dist/agents/memory-maintainer.d.ts +9 -0
  2. package/dist/agents/memory-maintainer.d.ts.map +1 -1
  3. package/dist/cli.js +0 -1
  4. package/dist/config/project-security.d.ts +0 -2
  5. package/dist/config/project-security.d.ts.map +1 -1
  6. package/dist/config/schema/magic-context.d.ts +0 -5
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config-init.js +0 -1
  9. package/dist/{context-gxm0q7tz.js → context-mrvm92n8.js} +3462 -3133
  10. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +0 -6
  11. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
  12. package/dist/features/magic-context/migrations.d.ts +5 -0
  13. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  14. package/dist/features/magic-context/session-project-backfill.d.ts +1 -1
  15. package/dist/features/magic-context/session-project-backfill.d.ts.map +1 -1
  16. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  17. package/dist/features/magic-context/storage-db.d.ts +8 -0
  18. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage.d.ts +1 -1
  21. package/dist/features/magic-context/storage.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/cache-busting-signals.d.ts +6 -3
  23. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +0 -1
  25. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/default-protection-pressure.test-support.d.ts +37 -0
  29. package/dist/hooks/magic-context/default-protection-pressure.test-support.d.ts.map +1 -0
  30. package/dist/hooks/magic-context/drop-stale-reduce-calls.d.ts +4 -0
  31. package/dist/hooks/magic-context/drop-stale-reduce-calls.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/latest-thinking-recovery.d.ts +61 -0
  34. package/dist/hooks/magic-context/latest-thinking-recovery.d.ts.map +1 -0
  35. package/dist/hooks/magic-context/lkg-replay.d.ts +9 -0
  36. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/lkg-slot.d.ts +2 -0
  38. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/read-session-chunk.d.ts +8 -1
  40. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/strip-content.d.ts +4 -2
  42. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/system-injection-stripper.d.ts +7 -0
  44. package/dist/hooks/magic-context/system-injection-stripper.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/tool-drop-target.d.ts +5 -0
  47. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +23 -7
  49. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/transform.d.ts +0 -6
  51. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  52. package/dist/{index-ys43j25b.js → index-5b7x80ec.js} +126 -44
  53. package/dist/{index-cszxqtev.js → index-btp5tdc8.js} +180 -31
  54. package/dist/{index-pynrjyjy.js → index-g2z2k5m4.js} +1 -1
  55. package/dist/index.js +1 -1
  56. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  57. package/dist/{read-session-chunk-e4dby3pb.js → read-session-chunk-2xm4sc4y.js} +1 -1
  58. package/dist/{recomp-orchestrator-xg3jm3s2.js → recomp-orchestrator-frmwj9hz.js} +3 -3
  59. package/dist/shared/pi-executable.d.ts +1 -1
  60. package/dist/shared/pi-executable.d.ts.map +1 -1
  61. package/dist/shared/prompt-surface-runtime.d.ts +1 -1
  62. package/dist/shared/subagent-runner.d.ts +22 -1
  63. package/dist/shared/subagent-runner.d.ts.map +1 -1
  64. package/dist/tools/light-descriptions.d.ts +1 -1
  65. package/dist/tools/light-descriptions.d.ts.map +1 -1
  66. package/dist/tools/parameter-descriptions.d.ts +1 -3
  67. package/dist/tools/parameter-descriptions.d.ts.map +1 -1
  68. package/dist/v2/hooks/context.d.ts.map +1 -1
  69. package/dist/v2/hooks/hidden-child.d.ts +15 -2
  70. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  71. package/dist/v2/server.js +3463 -2906
  72. package/dist/web/main.js +8 -2
  73. package/dist/{wrapup-orchestrator-9spy01n8.js → wrapup-orchestrator-yknks3ad.js} +3 -3
  74. package/package.json +1 -1
  75. package/src/agents/memory-maintainer.ts +12 -0
  76. package/src/config/index.test.ts +0 -17
  77. package/src/config/project-security.test.ts +0 -8
  78. package/src/config/project-security.ts +0 -9
  79. package/src/config/protected-tools.test.ts +72 -0
  80. package/src/config/schema/magic-context.test.ts +0 -6
  81. package/src/config/schema/magic-context.ts +1 -10
  82. package/src/shared/fold-render-lock-fence.test.ts +84 -0
  83. package/src/shared/fold-render-lock-fixtures.test.ts +270 -0
  84. package/src/shared/pi-executable.ts +3 -3
  85. package/src/shared/rpc-utils.test.ts +4 -5
  86. package/src/shared/sqlite-640-review.test.ts +184 -0
  87. package/src/shared/sqlite-context-pragmas.test.ts +105 -0
  88. package/src/shared/sqlite-writer-diagnostics.test.ts +136 -0
  89. package/src/shared/subagent-runner.ts +24 -0
  90. package/src/shared/tag-transcript.test.ts +55 -0
  91. package/src/shared/tag-transcript.ts +6 -0
  92. package/src/shared/write-transaction-attribution.test.ts +4 -0
  93. package/src/shared/write-transaction-timing.test.ts +39 -0
  94. package/dist/hooks/magic-context/historian-state-file.d.ts +0 -43
  95. package/dist/hooks/magic-context/historian-state-file.d.ts.map +0 -1
  96. package/dist/shared/auto-update-provenance.d.ts +0 -9
  97. package/dist/shared/auto-update-provenance.d.ts.map +0 -1
  98. 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
- // fork trim: OMP harness support is removed (T-003); bare `pi.exe` stands in
68
- // for the "image-only, no Pi arc" cases upstream covered with bare `omp.exe`.
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
- // fork trim: bare `omp.exe` joins this list — OMP harness support is
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
+ });