@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.
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
package/dist/web/main.js CHANGED
@@ -1086,7 +1086,7 @@ init_logger();
1086
1086
  init_logger();
1087
1087
 
1088
1088
  // src/shared/pi-executable.ts
1089
- var PI_IMAGE_NAMES = new Set(["pi", "pi.cmd"]);
1089
+ var PI_IMAGE_NAMES = new Set(["pi", "pi.cmd", "omp", "oh-my-pi"]);
1090
1090
 
1091
1091
  // src/shared/rpc-utils.ts
1092
1092
  var ownRpcServerInstanceIds = new Set;
@@ -4781,6 +4781,11 @@ var MEMORY_MAINTAINER_MANIFEST_CONTRACT = [
4781
4781
  // src/v2/hooks/hidden-child.ts
4782
4782
  init_tag_content_primitives();
4783
4783
  init_logger();
4784
+
4785
+ // src/shared/subagent-runner.ts
4786
+ var HISTORIAN_STEP_CAP = 4;
4787
+
4788
+ // src/v2/hooks/hidden-child.ts
4784
4789
  var HIDDEN_HISTORIAN_AGENT = "historian";
4785
4790
  var AGENT_TOOL_ALLOWLIST = {
4786
4791
  [HIDDEN_HISTORIAN_AGENT]: [],
@@ -4788,7 +4793,8 @@ var AGENT_TOOL_ALLOWLIST = {
4788
4793
  [MEMORY_MAINTAINER_B_AGENT]: MEMORY_MAINTAINER_B_ALLOWED_TOOLS
4789
4794
  };
4790
4795
  var AGENT_STEP_CAPS = {
4791
- [MEMORY_MAINTAINER_A_AGENT]: MEMORY_MAINTAINER_A_STEP_CAP
4796
+ [MEMORY_MAINTAINER_A_AGENT]: MEMORY_MAINTAINER_A_STEP_CAP,
4797
+ [HIDDEN_HISTORIAN_AGENT]: HISTORIAN_STEP_CAP
4792
4798
  };
4793
4799
 
4794
4800
  // src/v2/hidden-child-record.ts
@@ -21,10 +21,10 @@ import {
21
21
  getActiveCompartmentRun,
22
22
  markActiveCompartmentRunPublished,
23
23
  registerActiveCompartmentRun
24
- } from "./index-ys43j25b.js";
24
+ } from "./index-5b7x80ec.js";
25
25
  import"./index-1j34a56j.js";
26
26
  import"./index-x37vg474.js";
27
- import"./index-cszxqtev.js";
27
+ import"./index-btp5tdc8.js";
28
28
  import"./index-ramz5kkm.js";
29
29
  import {
30
30
  pushNotification2
@@ -37,7 +37,7 @@ import {
37
37
  import {
38
38
  setRecompStarting2,
39
39
  setRecompTerminal2
40
- } from "./index-pynrjyjy.js";
40
+ } from "./index-g2z2k5m4.js";
41
41
  import"./index-33gprt9g.js";
42
42
 
43
43
  // src/hooks/magic-context/wrapup-orchestrator.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retaw106/mmc-oc",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -40,6 +40,18 @@ export const MEMORY_MAINTAINER_B_ALLOWED_TOOLS = ["read", "grep", "glob"] as con
40
40
  */
41
41
  export const MEMORY_MAINTAINER_A_STEP_CAP = 4;
42
42
 
43
+ /**
44
+ * The next attempt's physical cap: the budget minus what earlier rounds of
45
+ * this agent already consumed, floored at 1 — even a fully consumed budget
46
+ * still owes the child one apply to emit its final manifest.
47
+ *
48
+ * Lives here (next to the constants) so the OC2 adapter and the Pi
49
+ * maintenance runner share one definition (moved from oc2-adapter.ts).
50
+ */
51
+ export function resolveMaintenanceStepCap(baseSteps: number, consumedSteps: number): number {
52
+ return Math.max(1, baseSteps - consumedSteps);
53
+ }
54
+
43
55
  /** Stage A system prompt: identity + the hard rules that bound a zero-tool screen. */
44
56
  export const MEMORY_MAINTAINER_A_SYSTEM_PROMPT = [
45
57
  "You are the memory-maintenance stage A screener: a hidden background agent that",
@@ -689,12 +689,6 @@ describe("loadPluginConfig — variable expansion scope", () => {
689
689
  });
690
690
 
691
691
  describe("loadPluginConfig — user-only settings", () => {
692
- it("allows user config to disable auto_update", () => {
693
- const result = loadWithUserConfig(JSON.stringify({ auto_update: false }));
694
-
695
- expect(result.auto_update).toBe(false);
696
- });
697
-
698
692
  it("allows user config to opt in to an exact home project", () => {
699
693
  const result = loadWithUserConfig(JSON.stringify({ allow_home_project: true }));
700
694
 
@@ -711,17 +705,6 @@ describe("loadPluginConfig — user-only settings", () => {
711
705
  expect(result.configWarnings?.join("\n")).toContain("Ignoring allow_home_project");
712
706
  });
713
707
 
714
- it("prevents project config from overriding user auto_update", () => {
715
- const result = loadWithUserAndProjectConfig(
716
- JSON.stringify({ auto_update: true, enabled: true }),
717
- JSON.stringify({ auto_update: false, enabled: false }),
718
- );
719
-
720
- expect(result.auto_update).toBe(true);
721
- expect(result.enabled).toBe(false);
722
- expect(result.configWarnings?.join("\n")).toContain("Ignoring auto_update");
723
- });
724
-
725
708
  it("keeps historian model selection user-owned when project config tries to override it", () => {
726
709
  const result = loadWithUserAndProjectConfig(
727
710
  JSON.stringify({
@@ -23,14 +23,6 @@ describe("stripUnsafeProjectConfigFields", () => {
23
23
  expect(warnings.join("\n")).toContain("Ignoring profiles from project config");
24
24
  });
25
25
 
26
- it("strips auto_update from project config", () => {
27
- const raw: Record<string, unknown> = { auto_update: false, historian: { two_pass: true } };
28
- const warnings = stripUnsafeProjectConfigFields(raw);
29
- expect("auto_update" in raw).toBe(false);
30
- expect(raw.historian).toEqual({ two_pass: true });
31
- expect(warnings.some((w) => w.includes("auto_update"))).toBe(true);
32
- });
33
-
34
26
  it("strips fail_closed_blocking from project config (user-tier only)", () => {
35
27
  const raw: Record<string, unknown> = {
36
28
  fail_closed_blocking: false,
@@ -296,8 +296,6 @@ function makeProjectThresholdWarning(field: string, reason: string): string {
296
296
  * - `profiles` — profile definitions choose hidden-agent models and must stay
297
297
  * in trusted user config; a repository may select a named profile but cannot
298
298
  * supply its contents.
299
- * - `auto_update` — a repo must not suppress plugin self-updates (which can
300
- * carry security fixes).
301
299
  * - `fail_closed_blocking` — a repo must not un-block (or force-block) the
302
300
  * loud inoperability gate; only the user may restore silent degrade.
303
301
  * - `debug_rpc` — a repo must not enable process heap capture or memory diagnostics.
@@ -341,13 +339,6 @@ export function stripUnsafeProjectConfigFields(projectRaw: Record<string, unknow
341
339
  );
342
340
  }
343
341
 
344
- if ("auto_update" in projectRaw) {
345
- delete projectRaw.auto_update;
346
- warnings.push(
347
- "Ignoring auto_update from project config (security: this setting only honors user-level config).",
348
- );
349
- }
350
-
351
342
  if ("fail_closed_blocking" in projectRaw) {
352
343
  delete projectRaw.fail_closed_blocking;
353
344
  warnings.push(
@@ -0,0 +1,72 @@
1
+ import { expect, it } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { protectedToolTagNumbers } from "../features/magic-context/reclaim-protection";
6
+ import { createTestTempDirFromPath } from "../shared/test-temp-dir";
7
+ import { loadPluginConfigDetailed } from ".";
8
+ import { MagicContextConfigSchema } from "./schema/magic-context";
9
+
10
+ it("protected_tools defaults, validation, active-only ordinals and zero override", () => {
11
+ expect(MagicContextConfigSchema.parse({}).protected_tools).toEqual({
12
+ todowrite: 1,
13
+ mmc_reduce: 3,
14
+ });
15
+ for (const value of [-1, 1.5, "2", null])
16
+ expect(
17
+ MagicContextConfigSchema.safeParse({ protected_tools: { custom: value } }).success,
18
+ ).toBe(false);
19
+ const tags = [1, 2, 3, 4, 5].map((tagNumber) => ({
20
+ tagNumber,
21
+ toolName: tagNumber % 2 ? "MCP_CUSTOM" : "custom",
22
+ type: "tool",
23
+ status: tagNumber === 5 ? "compacted" : tagNumber === 4 ? "dropped" : "active",
24
+ }));
25
+ expect([...protectedToolTagNumbers(tags, { custom: 2 })]).toEqual([3, 2]);
26
+ expect([...protectedToolTagNumbers(tags, { custom: 0 })]).toEqual([]);
27
+ });
28
+
29
+ it("protected_tools does not treat inherited object keys as configured tools", () => {
30
+ const tags = ["constructor", "__proto__"].map((toolName, i) => ({
31
+ tagNumber: i + 1,
32
+ toolName,
33
+ type: "tool",
34
+ status: "active",
35
+ }));
36
+ expect([...protectedToolTagNumbers(tags)]).toEqual([]);
37
+ expect([...protectedToolTagNumbers(tags, { constructor: 1 })]).toEqual([1]);
38
+ });
39
+
40
+ it("protected_tools merges normalized project keys over user and shipped defaults", () => {
41
+ const dir = createTestTempDirFromPath(join(tmpdir(), "protected-tools-config-"));
42
+ const old = process.env.XDG_CONFIG_HOME;
43
+ const oldConfigDir = process.env.OPENCODE_CONFIG_DIR;
44
+ try {
45
+ delete process.env.OPENCODE_CONFIG_DIR;
46
+ const project = join(dir, "project");
47
+ // Fork layout: user config at <XDG_CONFIG_HOME>/opencode/mmc.jsonc,
48
+ // project config at <project>/.mmc/mmc.jsonc.
49
+ mkdirSync(join(dir, "opencode"), { recursive: true });
50
+ mkdirSync(join(project, ".mmc"), { recursive: true });
51
+ writeFileSync(
52
+ join(dir, "opencode/mmc.jsonc"),
53
+ JSON.stringify({ protected_tools: { MCP_CUSTOM: 3, todowrite: 0 } }),
54
+ );
55
+ writeFileSync(
56
+ join(project, ".mmc/mmc.jsonc"),
57
+ JSON.stringify({ protected_tools: { custom: 2, CTX_REDUCE: 1 } }),
58
+ );
59
+ process.env.XDG_CONFIG_HOME = dir;
60
+ expect(loadPluginConfigDetailed(project).config.protected_tools).toEqual({
61
+ custom: 2,
62
+ todowrite: 0,
63
+ mmc_reduce: 1,
64
+ });
65
+ } finally {
66
+ if (old === undefined) delete process.env.XDG_CONFIG_HOME;
67
+ else process.env.XDG_CONFIG_HOME = old;
68
+ if (oldConfigDir === undefined) delete process.env.OPENCODE_CONFIG_DIR;
69
+ else process.env.OPENCODE_CONFIG_DIR = oldConfigDir;
70
+ rmSync(dir, { recursive: true, force: true });
71
+ }
72
+ });
@@ -84,7 +84,6 @@ describe("MagicContextConfigSchema", () => {
84
84
  enabled: true,
85
85
  allow_home_project: false,
86
86
  fail_closed_blocking: true,
87
- auto_update: false,
88
87
  toast_duration_ms: 5000,
89
88
  cache_ttl: "10m",
90
89
  prompt_surface: { default: "full" },
@@ -181,11 +180,6 @@ describe("MagicContextConfigSchema", () => {
181
180
  expect(result.historian?.disable).toBe(true);
182
181
  });
183
182
 
184
- it("accepts optional auto_update user preference", () => {
185
- expect(MagicContextConfigSchema.parse({ auto_update: false }).auto_update).toBe(false);
186
- expect(MagicContextConfigSchema.parse({ auto_update: true }).auto_update).toBe(true);
187
- });
188
-
189
183
  it("accepts an explicitly configured Pi subagent extension allowlist", () => {
190
184
  expect(
191
185
  MagicContextConfigSchema.parse({
@@ -249,10 +249,6 @@ export interface MagicContextConfig {
249
249
  enabled: boolean;
250
250
  /** User-level setting that lets a session in the canonical home directory use project memory. */
251
251
  allow_home_project: boolean;
252
- /** DEPRECATED no-op: the auto-update check was removed (2026-10-05) — the package is
253
- * private-registry-only, so public npm checks always failed. The key is still accepted
254
- * (USER config only; project configs stripped) so existing configs keep parsing. */
255
- auto_update?: boolean;
256
252
  /** Output language for generated Magic Context prose. USER config only. */
257
253
  language?: string;
258
254
  /** Active user-owned model profile after user/project resolution. */
@@ -436,12 +432,7 @@ export const MagicContextConfigSchema = z.object({
436
432
  .describe(
437
433
  "Allow Magic Context sessions launched from the exact canonical home directory. A non-git home uses its deterministic dir: identity; a home repository uses its git: identity. USER-LEVEL ONLY: project config is ignored. The home identity is excluded from registry seed exports, never resolves descendants by containment, and cannot join a workspace.",
438
434
  ),
439
- auto_update: z
440
- .boolean()
441
- .optional()
442
- .describe(
443
- "DEPRECATED no-op: the automatic update check was removed (the package is private-registry-only, so public npm checks always 404). The key is still accepted so existing configs keep parsing; upgrade via the host's own plugin update channel.",
444
- ),
435
+
445
436
  language: z
446
437
  .string()
447
438
  .trim()
@@ -0,0 +1,84 @@
1
+ import { expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import ts from "typescript";
5
+
6
+ const root = resolve(import.meta.dir, "../../../../");
7
+
8
+ for (const [host, path, functions] of [
9
+ [
10
+ "OpenCode",
11
+ "packages/plugin/src/hooks/magic-context/inject-compartments.ts",
12
+ ["materializeM0", "softRefreshCachedM1"],
13
+ ],
14
+ [
15
+ "Pi",
16
+ "packages/pi-plugin/src/inject-compartments-pi.ts",
17
+ ["materializeM0Pi", "softRefreshCachedM1Pi"],
18
+ ],
19
+ ] as const) {
20
+ test(`${host} fold and refresh render m[1] inside their writer section`, () => {
21
+ const source = ts.createSourceFile(
22
+ path,
23
+ readFileSync(resolve(root, path), "utf8"),
24
+ ts.ScriptTarget.Latest,
25
+ true,
26
+ );
27
+ const inspected: string[] = [];
28
+ const violations: string[] = [];
29
+ function visit(node: ts.Node): void {
30
+ if (
31
+ ts.isFunctionDeclaration(node) &&
32
+ node.name &&
33
+ functions.some((name) => name === node.name?.text)
34
+ ) {
35
+ inspected.push(node.name.text);
36
+ const calls: ts.CallExpression[] = [];
37
+ function collect(child: ts.Node): void {
38
+ if (ts.isCallExpression(child)) calls.push(child);
39
+ ts.forEachChild(child, collect);
40
+ }
41
+ collect(node);
42
+ const admission = calls.find(
43
+ (call) =>
44
+ call.expression.getText(source).endsWith(".exec") &&
45
+ call.arguments[0]?.getText(source) === '"BEGIN IMMEDIATE"',
46
+ );
47
+ expect(admission).toBeDefined();
48
+ const commit = calls.find(
49
+ (call) =>
50
+ admission &&
51
+ call.pos > admission.pos &&
52
+ call.expression.getText(source).endsWith(".exec") &&
53
+ call.arguments[0]?.getText(source) === '"COMMIT"',
54
+ );
55
+ expect(commit).toBeDefined();
56
+ const renders = calls.filter((call) =>
57
+ /^renderM1(?:Pi)?WithMetadata$/.test(call.expression.getText(source)),
58
+ );
59
+ expect(renders).toHaveLength(1);
60
+ for (const render of renders) {
61
+ if (
62
+ !admission ||
63
+ !commit ||
64
+ render.pos < admission.pos ||
65
+ render.pos > commit.pos
66
+ )
67
+ violations.push(`${node.name.text}: m[1] render outside writer`);
68
+ }
69
+ for (const call of calls) {
70
+ if (
71
+ call.expression.getText(source).endsWith("onFoldPrepare") &&
72
+ admission &&
73
+ call.pos > admission.pos
74
+ )
75
+ violations.push(`${node.name.text}: wire preparation inside writer`);
76
+ }
77
+ }
78
+ ts.forEachChild(node, visit);
79
+ }
80
+ visit(source);
81
+ expect(inspected.sort()).toEqual([...functions].sort());
82
+ expect(violations).toEqual([]);
83
+ });
84
+ }
@@ -0,0 +1,270 @@
1
+ import { expect, test } from "bun:test";
2
+ import { rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { appendCompartments } from "../features/magic-context/compartment-storage";
6
+ import {
7
+ insertMemory,
8
+ setMemoryClassification,
9
+ updateMemorySeenCount,
10
+ updateMemoryVerification,
11
+ } from "../features/magic-context/memory/storage-memory";
12
+ import {
13
+ recordIndexedMessageTime,
14
+ recordMessageFtsRowid,
15
+ } from "../features/magic-context/message-fts-rowid-map";
16
+ import { runMigrations } from "../features/magic-context/migrations";
17
+ import { getOrCreateSessionMeta } from "../features/magic-context/storage";
18
+ import { initializeDatabase } from "../features/magic-context/storage-db";
19
+ import { renderMemoryBlockV2 } from "../hooks/magic-context/inject-compartments";
20
+ import { estimateTokens } from "../hooks/magic-context/read-session-formatting";
21
+ import { Database } from "./sqlite";
22
+ import { createTestTempDirFromPath } from "./test-temp-dir";
23
+
24
+ // Fork policy (P2-fold-runtime): upstream's "recovered compartment ranges"
25
+ // template depends on the compartment_history_versions bookkeeping table,
26
+ // which upstream added deliberately and this fork chose NOT to adopt
27
+ // (P4:64 / oc-core correction 24). The template is intentionally not ported
28
+ // and no assertion was weakened to compensate.
29
+
30
+ export interface LockedDeltaFixture {
31
+ db: Database;
32
+ sibling: Database;
33
+ directory: string;
34
+ sessionId: string;
35
+ projectPath: string;
36
+ close: () => void;
37
+ }
38
+
39
+ export function createLockedDeltaFixture(
40
+ label: string,
41
+ open?: (path: string) => Database,
42
+ ): LockedDeltaFixture {
43
+ const directory = createTestTempDirFromPath(join(tmpdir(), `mc-locked-delta-${label}-`));
44
+ const path = join(directory, "context.db");
45
+ const db = open ? open(path) : new Database(path);
46
+ if (!open) {
47
+ initializeDatabase(db);
48
+ runMigrations(db);
49
+ }
50
+ db.exec("PRAGMA journal_mode=WAL");
51
+ const sibling = new Database(path);
52
+ const sessionId = label;
53
+ const projectPath = "git:locked-delta";
54
+ getOrCreateSessionMeta(db, sessionId);
55
+ return {
56
+ db,
57
+ sibling,
58
+ directory,
59
+ sessionId,
60
+ projectPath,
61
+ close: () => {
62
+ sibling.close();
63
+ db.close();
64
+ rmSync(directory, { recursive: true, force: true });
65
+ },
66
+ };
67
+ }
68
+ export interface LockedDeltaOptions {
69
+ memoryBudget?: number;
70
+ temporalAwareness?: boolean;
71
+ }
72
+ interface LockedDeltaDriver<Markers> {
73
+ host: string;
74
+ create: (label: string) => LockedDeltaFixture;
75
+ materialize: (fixture: LockedDeltaFixture, options: LockedDeltaOptions) => Markers;
76
+ refresh: (
77
+ fixture: LockedDeltaFixture,
78
+ options: LockedDeltaOptions,
79
+ beforeAdmission: () => void,
80
+ ) => void;
81
+ render: (fixture: LockedDeltaFixture, options: LockedDeltaOptions, markers: Markers) => string;
82
+ temporalHeadings?: boolean;
83
+ /**
84
+ * OpenCode's refresh exposes a production admission callback
85
+ * (beforeCacheCommitForTest). Pi has no such field and this fork adds none:
86
+ * when false, the sibling write is interposed by wrapping the routed
87
+ * db.exec and running it immediately BEFORE the first delegated
88
+ * "BEGIN IMMEDIATE" of the refresh call.
89
+ */
90
+ usesAdmissionCallback?: boolean;
91
+ }
92
+ function compartment() {
93
+ return {
94
+ sequence: 0,
95
+ startMessage: 1,
96
+ endMessage: 2,
97
+ startMessageId: "a",
98
+ endMessageId: "b",
99
+ title: "locked history",
100
+ content: "locked history",
101
+ p1: "locked history",
102
+ p2: "summary",
103
+ p3: "outcome",
104
+ p4: "anchor",
105
+ legacy: 0,
106
+ };
107
+ }
108
+ function persistedDelta(f: LockedDeltaFixture): string {
109
+ return getOrCreateSessionMeta(f.db, f.sessionId).cachedM1Bytes?.toString("utf8") ?? "";
110
+ }
111
+ function refreshAtAdmission<Markers>(
112
+ driver: LockedDeltaDriver<Markers>,
113
+ f: LockedDeltaFixture,
114
+ options: LockedDeltaOptions,
115
+ write: () => void,
116
+ ) {
117
+ let admissions = 0;
118
+ let hooks = 0;
119
+ const usesCallback = driver.usesAdmissionCallback !== false;
120
+ // Capture the currently routed exec (writer accounting intact); delegate
121
+ // through the original reference and restore that same reference on exit —
122
+ // never bypass the routing with a raw native binding.
123
+ const originalExec = f.db.exec;
124
+ f.db.exec = (sql) => {
125
+ if (sql === "BEGIN IMMEDIATE") {
126
+ admissions++;
127
+ if (!usesCallback && admissions === 1 && hooks === 0) {
128
+ hooks++;
129
+ write();
130
+ }
131
+ }
132
+ return originalExec.call(f.db, sql);
133
+ };
134
+ try {
135
+ driver.refresh(
136
+ f,
137
+ options,
138
+ usesCallback
139
+ ? () => {
140
+ expect(admissions).toBe(0);
141
+ hooks++;
142
+ write();
143
+ }
144
+ : () => {},
145
+ );
146
+ expect(hooks).toBe(1);
147
+ // Exactly one writer section per refresh for BOTH variants: the
148
+ // interposed write must hit the refresh's own admission, not an
149
+ // unrelated earlier transaction.
150
+ expect(admissions).toBe(1);
151
+ } finally {
152
+ f.db.exec = originalExec;
153
+ }
154
+ }
155
+
156
+ /** These fixtures use real sibling writes, then compare persisted bytes to an independent live render. */
157
+ export function registerLockedDeltaRegressions<Markers>(driver: LockedDeltaDriver<Markers>): void {
158
+ for (const change of ["importance", "reinforcement", "verification"] as const) {
159
+ test(`${driver.host} m[1] reads memory ${change} under its writer`, () => {
160
+ const f = driver.create(`${driver.host}-${change}`);
161
+ try {
162
+ const sample = insertMemory(f.db, {
163
+ projectPath: "git:budget-only",
164
+ category: "ARCHITECTURE",
165
+ content: "candidate A ".repeat(40),
166
+ });
167
+ const options = {
168
+ memoryBudget:
169
+ 4 * (estimateTokens(renderMemoryBlockV2([sample], "new-memories")) + 2),
170
+ };
171
+ const markers = driver.materialize(f, options);
172
+ const a = insertMemory(f.db, {
173
+ projectPath: f.projectPath,
174
+ category: "ARCHITECTURE",
175
+ content: "candidate A ".repeat(40),
176
+ });
177
+ const b = insertMemory(f.db, {
178
+ projectPath: f.projectPath,
179
+ category: "ARCHITECTURE",
180
+ content: "candidate B ".repeat(40),
181
+ });
182
+ setMemoryClassification(f.db, a.id, { importance: 50 });
183
+ setMemoryClassification(f.db, b.id, { importance: 50 });
184
+ f.db
185
+ .prepare(
186
+ "UPDATE memories SET last_seen_at = CASE WHEN id = ? THEN 1000 ELSE 0 END, verified_at = NULL WHERE id IN (?, ?)",
187
+ )
188
+ .run(a.id, a.id, b.id);
189
+ const before = driver.render(f, options, markers);
190
+ expect(before).toContain("candidate A");
191
+ expect(before).not.toContain("candidate B");
192
+ refreshAtAdmission(driver, f, options, () => {
193
+ if (change === "importance")
194
+ setMemoryClassification(f.sibling, b.id, { importance: 100 });
195
+ else if (change === "reinforcement") updateMemorySeenCount(f.sibling, b.id);
196
+ else updateMemoryVerification(f.sibling, b.id, "verified");
197
+ });
198
+ const fresh = driver.render(f, options, markers);
199
+ expect(fresh).toContain("candidate B");
200
+ expect(fresh).not.toContain("candidate A");
201
+ expect(persistedDelta(f)).toBe(fresh);
202
+ expect(persistedDelta(f)).not.toBe(before);
203
+ } finally {
204
+ f.close();
205
+ }
206
+ });
207
+ }
208
+
209
+ test(`${driver.host} m[1] uses the baseline cutoff for a newly expired additive memory`, () => {
210
+ const f = driver.create(`${driver.host}-expiry`);
211
+ const realNow = Date.now;
212
+ let now = 1000;
213
+ Date.now = () => now;
214
+ try {
215
+ const options = {};
216
+ const markers = driver.materialize(f, options);
217
+ now = 2000;
218
+ const before = driver.render(f, options, markers);
219
+ expect(before).not.toContain("eligible at the baseline cutoff");
220
+ refreshAtAdmission(driver, f, options, () => {
221
+ insertMemory(f.sibling, {
222
+ projectPath: f.projectPath,
223
+ category: "ARCHITECTURE",
224
+ content: "Memory eligible at the baseline cutoff",
225
+ expiresAt: 2500,
226
+ });
227
+ now = 3000;
228
+ });
229
+ const fresh = driver.render(f, options, markers);
230
+ expect(fresh).toContain("eligible at the baseline cutoff");
231
+ expect(persistedDelta(f)).toBe(fresh);
232
+ } finally {
233
+ Date.now = realNow;
234
+ f.close();
235
+ }
236
+ });
237
+
238
+ if (driver.temporalHeadings) {
239
+ test(`${driver.host} m[1] reads indexed temporal headings under its writer`, () => {
240
+ const f = driver.create(`${driver.host}-dates`);
241
+ const originalXdgDataHome = process.env.XDG_DATA_HOME;
242
+ process.env.XDG_DATA_HOME = f.directory;
243
+ try {
244
+ const options = { temporalAwareness: true };
245
+ const markers = driver.materialize(f, options);
246
+ appendCompartments(f.db, f.sessionId, [compartment()]);
247
+ for (const ordinal of [1, 2])
248
+ recordMessageFtsRowid(f.db, f.sessionId, ordinal, ordinal);
249
+ const before = driver.render(f, options, markers);
250
+ expect(before).not.toContain("2025-01-01");
251
+ refreshAtAdmission(driver, f, options, () => {
252
+ for (const ordinal of [1, 2])
253
+ recordIndexedMessageTime(
254
+ f.sibling,
255
+ f.sessionId,
256
+ ordinal,
257
+ Date.UTC(2025, 0, 1),
258
+ );
259
+ });
260
+ const fresh = driver.render(f, options, markers);
261
+ expect(fresh).toContain("2025-01-01");
262
+ expect(persistedDelta(f)).toBe(fresh);
263
+ } finally {
264
+ if (originalXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
265
+ else process.env.XDG_DATA_HOME = originalXdgDataHome;
266
+ f.close();
267
+ }
268
+ });
269
+ }
270
+ }
@@ -1,7 +1,7 @@
1
- export type PiExecutableHarnessKind = "pi";
1
+ export type PiExecutableHarnessKind = "pi" | "omp";
2
2
 
3
3
  /** Executable images recognized as Pi-compatible hosts. */
4
- export const PI_IMAGE_NAMES = new Set(["pi", "pi.cmd"]);
4
+ export const PI_IMAGE_NAMES = new Set(["pi", "pi.cmd", "omp", "oh-my-pi"]);
5
5
 
6
6
  /** Classify a process title or executable path using the shared Pi image vocabulary. */
7
7
  export function piHarnessKindFromExecutable(
@@ -16,5 +16,5 @@ export function piHarnessKindFromExecutable(
16
16
  ?.toLowerCase()
17
17
  .replace(/\.(?:exe|cmd)$/, "");
18
18
  if (!executable || !PI_IMAGE_NAMES.has(executable)) return undefined;
19
- return "pi";
19
+ return executable === "pi" ? "pi" : "omp";
20
20
  }