@cortexkit/opencode-magic-context 0.49.8 → 0.49.9

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 (105) hide show
  1. package/dist/agents/hidden-agent-registrations.d.ts +5 -3
  2. package/dist/agents/hidden-agent-registrations.d.ts.map +1 -1
  3. package/dist/auto-search-worker.js +1 -1
  4. package/dist/config/schema/agent-overrides.d.ts +13 -13
  5. package/dist/config/schema/magic-context.d.ts +500 -500
  6. package/dist/{context-x0ccfv0f.js → context-ey3ap3cc.js} +29 -24
  7. package/dist/features/magic-context/primer-clustering.d.ts.map +1 -1
  8. package/dist/features/magic-context/smart-notes/compiler-prompt.d.ts +1 -1
  9. package/dist/features/magic-context/smart-notes/compiler-prompt.d.ts.map +1 -1
  10. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  11. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
  12. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
  13. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  14. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
  15. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  16. package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
  17. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  18. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  19. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/provider-visible-parts.d.ts +10 -4
  21. package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
  23. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
  25. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
  26. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +5 -1
  27. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
  29. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
  31. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  33. package/dist/{index-b4gcb3rk.js → index-0x9cn9w1.js} +2 -2
  34. package/dist/{index-ydjgz36f.js → index-16vwdyy5.js} +1 -1
  35. package/dist/{index-xf6k8szp.js → index-21919gcq.js} +3 -3
  36. package/dist/{index-hxrvx6cy.js → index-3403gbzj.js} +129 -103
  37. package/dist/{index-gzg9ekc1.js → index-6k86es2p.js} +129 -15
  38. package/dist/{index-8kk7yh6m.js → index-9jvab89d.js} +1 -1
  39. package/dist/{index-qgp5vnt4.js → index-a865mj02.js} +1 -1
  40. package/dist/{index-xpn05jxq.js → index-f8cn4xqa.js} +287 -58
  41. package/dist/{index-2rw244rh.js → index-ffa8x2as.js} +4 -4
  42. package/dist/{index-bd9cjab3.js → index-g7jkrz0x.js} +35 -15
  43. package/dist/{index-pjgzdezw.js → index-nenz4gpb.js} +3 -3
  44. package/dist/{index-zf9nydbd.js → index-swgpe402.js} +4 -4
  45. package/dist/{index-9g1c4bt9.js → index-wpwpn7nj.js} +88 -8
  46. package/dist/{index-w1ejb9qr.js → index-x57ep4fp.js} +33 -19
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +50 -46
  49. package/dist/{message-index-zs882h8e.js → message-index-6r1x9cqh.js} +5 -5
  50. package/dist/{message-index-bounded-hxjp56qk.js → message-index-bounded-fskdyc81.js} +5 -5
  51. package/dist/migration-worker.js +4 -4
  52. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  53. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  54. package/dist/raw-ordinal-warmup-worker.js +3 -3
  55. package/dist/{read-session-chunk-t9r7r0sg.js → read-session-chunk-zgpvs423.js} +4 -4
  56. package/dist/{recomp-orchestrator-gr36zvd2.js → recomp-orchestrator-77xc8wpe.js} +10 -10
  57. package/dist/{search-qjen1pmz.js → search-72fkjwk2.js} +8 -8
  58. package/dist/shared/hidden-zen-read.d.ts +24 -0
  59. package/dist/shared/hidden-zen-read.d.ts.map +1 -0
  60. package/dist/shared/reclaim-distance.d.ts +33 -0
  61. package/dist/shared/reclaim-distance.d.ts.map +1 -0
  62. package/dist/shared/stable-json.d.ts +7 -0
  63. package/dist/shared/stable-json.d.ts.map +1 -1
  64. package/dist/shared/tail-hygiene-json.d.ts +22 -0
  65. package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
  66. package/dist/shared/transcript.d.ts +5 -0
  67. package/dist/shared/transcript.d.ts.map +1 -1
  68. package/dist/{storage-tags-a513rkj3.js → storage-tags-35m4gms2.js} +2 -2
  69. package/dist/{transform-postprocess-phase-yv463b7y.js → transform-postprocess-phase-r8d406j0.js} +10 -10
  70. package/dist/v2/auto-search-worker.js +1 -1
  71. package/dist/v2/{context-wd82v9z6.js → context-z2yagwy4.js} +168 -35
  72. package/dist/v2/hidden-completion.d.ts.map +1 -1
  73. package/dist/v2/hooks/hidden-child.d.ts +2 -2
  74. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  75. package/dist/v2/{message-index-yd94vt9n.js → message-index-9ezq6wze.js} +5 -5
  76. package/dist/v2/{message-index-bounded-madg2kfr.js → message-index-bounded-rqc603f2.js} +5 -5
  77. package/dist/v2/migration-worker.js +4 -4
  78. package/dist/v2/raw-ordinal-warmup-worker.js +3 -3
  79. package/dist/v2/{read-session-chunk-7qs791f3.js → read-session-chunk-7f87tg2q.js} +4 -4
  80. package/dist/v2/{recomp-orchestrator-ee91r9s6.js → recomp-orchestrator-s5tdpsbs.js} +9 -9
  81. package/dist/v2/{search-pn15rr52.js → search-2td1b1wy.js} +8 -8
  82. package/dist/v2/{server-08k4zkfk.js → server-23znwjqk.js} +4 -4
  83. package/dist/v2/{server-y5g32dhq.js → server-2hxypdx5.js} +4 -4
  84. package/dist/v2/{server-yak3sxya.js → server-4zwx20yg.js} +1 -1
  85. package/dist/v2/{server-shme41gj.js → server-89fcbe37.js} +35 -15
  86. package/dist/v2/{server-q360kdb8.js → server-c9w52qss.js} +30 -13
  87. package/dist/v2/{server-4gz0xkrc.js → server-f27he3j1.js} +1 -1
  88. package/dist/v2/{server-xj4mewze.js → server-gc8x2xdb.js} +66 -8
  89. package/dist/v2/{server-jjsf29aq.js → server-kmapq3v0.js} +286 -57
  90. package/dist/v2/{server-adfte03x.js → server-xqf2d2mg.js} +1 -1
  91. package/dist/v2/{server-j3dm74ey.js → server-z0rjemg1.js} +2 -2
  92. package/dist/v2/server.js +2 -2
  93. package/dist/v2/{storage-tags-spa92sgj.js → storage-tags-c4zfcxv4.js} +2 -2
  94. package/dist/v2/{wrapup-orchestrator-5wxy1ggv.js → wrapup-orchestrator-wg8dmmm9.js} +9 -9
  95. package/dist/{wrapup-orchestrator-gv08c6f9.js → wrapup-orchestrator-b1z99yg5.js} +11 -11
  96. package/package.json +1 -1
  97. package/src/agents/hidden-agent-registrations.ts +22 -4
  98. package/src/shared/hidden-zen-read.test.ts +99 -0
  99. package/src/shared/hidden-zen-read.ts +88 -0
  100. package/src/shared/reclaim-distance.test.ts +111 -0
  101. package/src/shared/reclaim-distance.ts +91 -0
  102. package/src/shared/stable-json.ts +56 -14
  103. package/src/shared/tail-hygiene-json.test.ts +117 -0
  104. package/src/shared/tail-hygiene-json.ts +217 -0
  105. package/src/shared/transcript.ts +7 -0
@@ -19,7 +19,7 @@ import {
19
19
  registerActiveCompartmentRun,
20
20
  setRecompStarting2,
21
21
  setRecompTerminal2
22
- } from "./server-q360kdb8.js";
22
+ } from "./server-c9w52qss.js";
23
23
  import"./server-s6jqh8bj.js";
24
24
  import"./server-988cqjme.js";
25
25
  import {
@@ -27,7 +27,7 @@ import {
27
27
  } from "./server-wafzvyhg.js";
28
28
  import"./server-63ha7f0f.js";
29
29
  import"./server-9a6bwc12.js";
30
- import"./server-j3dm74ey.js";
30
+ import"./server-z0rjemg1.js";
31
31
  import {
32
32
  getWrapupInProgressState,
33
33
  acquireWrapupInProgress,
@@ -36,23 +36,23 @@ import {
36
36
  getHistorianFailureState,
37
37
  clearEmergencyRecovery,
38
38
  prepareRawSessionOrdinals2
39
- } from "./server-xj4mewze.js";
40
- import"./server-shme41gj.js";
41
- import"./server-08k4zkfk.js";
39
+ } from "./server-gc8x2xdb.js";
40
+ import"./server-89fcbe37.js";
41
+ import"./server-23znwjqk.js";
42
42
  import"./server-1zx87sdm.js";
43
- import"./server-jjsf29aq.js";
43
+ import"./server-kmapq3v0.js";
44
44
  import"./server-7e087rvk.js";
45
45
  import"./server-51779zs2.js";
46
46
  import"./server-bx59ezg6.js";
47
47
  import"./server-2ra6pwf3.js";
48
48
  import"./server-zrwaj29a.js";
49
- import"./server-adfte03x.js";
49
+ import"./server-xqf2d2mg.js";
50
50
  import"./server-fr0wbhdn.js";
51
- import"./server-4gz0xkrc.js";
51
+ import"./server-f27he3j1.js";
52
52
  import"./server-njnx39q2.js";
53
53
  import"./server-4easnf78.js";
54
54
  import"./server-fb3zxn06.js";
55
- import"./server-yak3sxya.js";
55
+ import"./server-4zwx20yg.js";
56
56
  import {
57
57
  pushNotification2
58
58
  } from "./server-x8a7jmkh.js";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  runManagedWrapup2
3
- } from "./index-pjgzdezw.js";
4
- import"./index-xf6k8szp.js";
5
- import"./index-hxrvx6cy.js";
3
+ } from "./index-nenz4gpb.js";
4
+ import"./index-21919gcq.js";
5
+ import"./index-3403gbzj.js";
6
6
  import"./index-s178h9z8.js";
7
- import"./index-ydjgz36f.js";
8
- import"./index-b4gcb3rk.js";
7
+ import"./index-16vwdyy5.js";
8
+ import"./index-0x9cn9w1.js";
9
9
  import"./index-sd3egckz.js";
10
10
  import"./index-a43xq1pg.js";
11
11
  import"./index-n78avt2p.js";
@@ -19,22 +19,22 @@ import"./index-x8a7jmkh.js";
19
19
  import"./index-a4b0gbyg.js";
20
20
  import"./index-bmwfzz28.js";
21
21
  import"./index-kvw6xew3.js";
22
- import"./index-zf9nydbd.js";
23
- import"./index-qgp5vnt4.js";
22
+ import"./index-swgpe402.js";
23
+ import"./index-a865mj02.js";
24
24
  import"./index-fb3zxn06.js";
25
25
  import"./index-fr0wbhdn.js";
26
26
  import"./index-0jz47061.js";
27
27
  import"./index-mp2kghkv.js";
28
28
  import"./index-q28hsbpg.js";
29
- import"./index-9g1c4bt9.js";
30
- import"./index-xpn05jxq.js";
29
+ import"./index-wpwpn7nj.js";
30
+ import"./index-f8cn4xqa.js";
31
31
  import"./index-1zx87sdm.js";
32
32
  import"./index-q76br5zn.js";
33
33
  import"./index-angx1s0k.js";
34
34
  import"./index-v0p37hm2.js";
35
35
  import"./index-w28y7d0z.js";
36
- import"./index-8kk7yh6m.js";
37
- import"./index-bd9cjab3.js";
36
+ import"./index-9jvab89d.js";
37
+ import"./index-g7jkrz0x.js";
38
38
  import"./index-58e3y90n.js";
39
39
  import"./index-150zz6fh.js";
40
40
  import"./index-gjvjh960.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.49.8",
3
+ "version": "0.49.9",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -1,3 +1,4 @@
1
+ import { isToolFreeHiddenAgent, ZEN_READ_AGENT_SUFFIX } from "../shared/hidden-zen-read";
1
2
  import { buildAllowOnlyPermission } from "./permissions";
2
3
 
3
4
  /**
@@ -77,8 +78,8 @@ export interface HiddenAgentRegistration {
77
78
  * Hoisted function declaration: returns the hidden-agent registrations with
78
79
  * INLINE id / allow-list / step-cap literals (see {@link HiddenAgentRegistration}
79
80
  * for why these must not come from module-level `var` consts). Prompts and
80
- * computed overrides are passed in by the caller. Historians transform only
81
- * the text supplied in their prompt and never receive tools.
81
+ * computed overrides are passed in by the caller. The separate Zen carriers
82
+ * advertise read for provider compatibility; execution is refused by session.
82
83
  */
83
84
  export function buildHiddenAgentRegistrations(args: {
84
85
  dreamerPrompt: string | undefined;
@@ -88,7 +89,7 @@ export function buildHiddenAgentRegistrations(args: {
88
89
  historianEditorPrompt: string | undefined;
89
90
  dreamerOverrides?: Record<string, unknown>;
90
91
  historianOverrides?: Record<string, unknown>;
91
- /** Legacy config input retained for compatibility; historians always have zero tools. */
92
+ /** Legacy config input retained for compatibility; it cannot grant historian tools. */
92
93
  historianDisallowed: readonly string[];
93
94
  }): HiddenAgentRegistration[] {
94
95
  return [
@@ -278,6 +279,19 @@ export function buildHiddenAgentRegistrations(args: {
278
279
  ];
279
280
  }
280
281
 
282
+ /** Alternate carriers preserve the original registrations for all other providers. */
283
+ export function buildZenReadAgentRegistrations(
284
+ registrations: HiddenAgentRegistration[],
285
+ ): HiddenAgentRegistration[] {
286
+ return registrations
287
+ .filter((registration) => isToolFreeHiddenAgent(registration.id))
288
+ .map((registration) => ({
289
+ ...registration,
290
+ id: `${registration.id}${ZEN_READ_AGENT_SUFFIX}`,
291
+ allowedTools: ["read"],
292
+ }));
293
+ }
294
+
281
295
  /**
282
296
  * Build a hidden-agent config with a deny-everything-by-default permission
283
297
  * baseline and a hard tool-iteration ceiling. User overrides may lower
@@ -333,7 +347,11 @@ export function buildHiddenAgentConfig(
333
347
  // The plugin appends named Task routing denies after the general wildcard.
334
348
  // Seed an explicit Task deny so that routing also re-emits a final wildcard
335
349
  // deny for Task itself; otherwise OpenCode exposes a callable-but-refused tool.
336
- if (lockPermissions && agentLabel?.startsWith("historian") && allowedTools.length === 0) {
350
+ if (
351
+ lockPermissions &&
352
+ ((agentLabel?.startsWith("historian") && allowedTools.length === 0) ||
353
+ agentLabel?.endsWith(ZEN_READ_AGENT_SUFFIX))
354
+ ) {
337
355
  basePermission.task = "deny";
338
356
  }
339
357
  return {
@@ -0,0 +1,99 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ needsBlockedRead,
4
+ rememberV1HiddenRun,
5
+ shapeV1HiddenMessage,
6
+ withBlockedReadInstruction,
7
+ } from "./hidden-zen-read";
8
+
9
+ for (const [agent, task, sentence] of [
10
+ [
11
+ "historian",
12
+ "initial",
13
+ "Summarize only the supplied conversation and memory context. Do not call tools.",
14
+ ],
15
+ [
16
+ "historian-recomp",
17
+ "repair",
18
+ "Summarize only the supplied conversation and memory context. Do not call tools.",
19
+ ],
20
+ [
21
+ "historian-editor",
22
+ "fallback",
23
+ "Summarize only the supplied conversation and memory context. Do not call tools.",
24
+ ],
25
+ [
26
+ "dreamer-classifier",
27
+ "classify-memories",
28
+ "Classify only the supplied memory context. Do not call tools.",
29
+ ],
30
+ [
31
+ "dreamer-classifier",
32
+ "compress-cues",
33
+ "Compress only the supplied memory cues. Do not call tools.",
34
+ ],
35
+ [
36
+ "smart-note-compiler",
37
+ "compile",
38
+ "Compile only the supplied conditions and memory context. Do not call tools.",
39
+ ],
40
+ [
41
+ "dreamer-reviewer",
42
+ "review-user-memories",
43
+ "Review only the supplied observations and memory context. Do not call tools.",
44
+ ],
45
+ ]) {
46
+ test(`Zen compatibility is provider-scoped and prompt-guided for ${agent}/${task}`, () => {
47
+ expect(needsBlockedRead(agent!, "opencode")).toBe(true);
48
+ for (const provider of ["anthropic", "openai", "opencode-zen", undefined])
49
+ expect(needsBlockedRead(agent!, provider)).toBe(false);
50
+ expect(
51
+ withBlockedReadInstruction("original prompt\n", { agent: agent!, metadata: { task } }),
52
+ ).toBe(`original prompt\n\n\n${sentence}`);
53
+ expect(withBlockedReadInstruction(sentence!, { agent: agent!, metadata: { task } })).toBe(
54
+ sentence!,
55
+ );
56
+ });
57
+ }
58
+
59
+ test("genuine-tool agents and user agents never receive the compatibility read", () => {
60
+ for (const agent of [
61
+ "dreamer",
62
+ "dreamer-docs",
63
+ "dreamer-memory-mapper",
64
+ "dreamer-primer-investigator",
65
+ "dreamer-retrospective",
66
+ "build",
67
+ "custom",
68
+ ])
69
+ expect(needsBlockedRead(agent, "opencode")).toBe(false);
70
+ });
71
+
72
+ test("v1 uses the host-resolved default model only on a registered hidden session", () => {
73
+ const message = { agent: "historian", model: { providerID: "opencode" }, system: "original" };
74
+ shapeV1HiddenMessage("user", message);
75
+ expect(message).toEqual({
76
+ agent: "historian",
77
+ model: { providerID: "opencode" },
78
+ system: "original",
79
+ });
80
+ rememberV1HiddenRun("child", {
81
+ agent: "historian",
82
+ kind: "historian",
83
+ system: "original",
84
+ title: "test",
85
+ directory: "/project",
86
+ timeoutMs: 1000,
87
+ });
88
+ try {
89
+ shapeV1HiddenMessage("child", message);
90
+ expect(message.agent).toBe("historian-zen-blocked-read");
91
+ expect(message.system).toBe(
92
+ "original\n\nSummarize only the supplied conversation and memory context. Do not call tools.",
93
+ );
94
+ shapeV1HiddenMessage("child", message);
95
+ expect(message.system.match(/Do not call tools\./g)).toHaveLength(1);
96
+ } finally {
97
+ rememberV1HiddenRun("child", undefined);
98
+ }
99
+ });
@@ -0,0 +1,88 @@
1
+ import type { HiddenRunIdentity } from "../hooks/magic-context/compartment-runner-types";
2
+
3
+ /** These agents transform supplied text; agents that inspect code or memories keep their existing tools. */
4
+ export function isToolFreeHiddenAgent(agent: string): boolean {
5
+ return [
6
+ "historian",
7
+ "historian-recomp",
8
+ "historian-editor",
9
+ "dreamer-classifier",
10
+ "dreamer-reviewer",
11
+ "smart-note-compiler",
12
+ ].includes(agent);
13
+ }
14
+
15
+ export const TOOL_USE_DISABLED = "tool use is disabled for this task";
16
+ export const ZEN_READ_AGENT_SUFFIX = "-zen-blocked-read";
17
+
18
+ export function needsBlockedRead(agent: string, providerID: string | undefined): boolean {
19
+ return providerID === "opencode" && isToolFreeHiddenAgent(agent);
20
+ }
21
+
22
+ export function blockedReadInstruction(run: Pick<HiddenRunIdentity, "agent" | "metadata">): string {
23
+ if (run.agent.startsWith("historian"))
24
+ return "Summarize only the supplied conversation and memory context. Do not call tools.";
25
+ if (run.metadata?.task === "compress-cues")
26
+ return "Compress only the supplied memory cues. Do not call tools.";
27
+ if (run.agent === "smart-note-compiler")
28
+ return "Compile only the supplied conditions and memory context. Do not call tools.";
29
+ if (run.agent === "dreamer-reviewer")
30
+ return "Review only the supplied observations and memory context. Do not call tools.";
31
+ return "Classify only the supplied memory context. Do not call tools.";
32
+ }
33
+
34
+ export function withBlockedReadInstruction(
35
+ system: string,
36
+ run: Parameters<typeof blockedReadInstruction>[0],
37
+ ): string {
38
+ const instruction = blockedReadInstruction(run);
39
+ return system.endsWith(instruction) ? system : `${system}${system ? "\n\n" : ""}${instruction}`;
40
+ }
41
+
42
+ // Only sessions opened by Magic Context's hidden executor can have tool calls
43
+ // blocked in OpenCode 1. Agent names and user permissions never select user sessions.
44
+ const blockedSessions = new Set<string>();
45
+ const v1Runs = new Map<string, HiddenRunIdentity>();
46
+
47
+ export function rememberV1HiddenRun(sessionID: string, run: HiddenRunIdentity | undefined): void {
48
+ if (run) v1Runs.set(sessionID, run);
49
+ else {
50
+ v1Runs.delete(sessionID);
51
+ setBlockedReadSession(sessionID, false);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * OpenCode 1 calls chat.message after selecting the request's actual model.
57
+ * Select the read-bearing carrier there so agent defaults and fallback models
58
+ * cannot accidentally apply Zen's compatibility surface to another provider.
59
+ */
60
+ export function shapeV1HiddenMessage(
61
+ sessionID: string,
62
+ message: {
63
+ agent?: string;
64
+ model?: { providerID: string };
65
+ system?: string;
66
+ },
67
+ ): void {
68
+ const run = v1Runs.get(sessionID);
69
+ if (!run) return;
70
+ const agent = message.agent ?? run.agent;
71
+ const base = agent.endsWith(ZEN_READ_AGENT_SUFFIX)
72
+ ? agent.slice(0, -ZEN_READ_AGENT_SUFFIX.length)
73
+ : agent;
74
+ const enabled = needsBlockedRead(base, message.model?.providerID);
75
+ setBlockedReadSession(sessionID, enabled);
76
+ if (!enabled) return;
77
+ message.agent = `${base}${ZEN_READ_AGENT_SUFFIX}`;
78
+ message.system = withBlockedReadInstruction(message.system ?? "", run);
79
+ }
80
+
81
+ export function setBlockedReadSession(sessionID: string, enabled: boolean): void {
82
+ if (enabled) blockedSessions.add(sessionID);
83
+ else blockedSessions.delete(sessionID);
84
+ }
85
+
86
+ export function blockedReadSessionRefusal(sessionID: string): string | undefined {
87
+ return blockedSessions.has(sessionID) ? TOOL_USE_DISABLED : undefined;
88
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ farBehindToolBookmark,
4
+ isReclaimUserTurn,
5
+ type ReclaimDistanceEntry,
6
+ } from "./reclaim-distance";
7
+
8
+ const output = (tag: number, tokens = 1000): ReclaimDistanceEntry => ({
9
+ toolOutputTag: tag,
10
+ tokens,
11
+ });
12
+ const turn = (): ReclaimDistanceEntry => ({ userTurn: true, tokens: 10 });
13
+
14
+ describe("reclaim distance", () => {
15
+ it("repeated rebuilds cannot age the current task", () => {
16
+ const entries = [turn(), ...Array.from({ length: 28 }, (_, i) => output(i + 1))];
17
+ for (const count of [2, 3, 5]) {
18
+ let bookmark = 0;
19
+ for (let pass = 0; pass < count + 1; pass++) {
20
+ expect(bookmark).toBe(0);
21
+ bookmark = Math.max(bookmark, farBehindToolBookmark(entries, 200_000));
22
+ }
23
+ }
24
+ });
25
+
26
+ it("three newer user turns establish a bookmark, not two", () => {
27
+ const entries = [turn(), output(1), turn(), output(2), turn()];
28
+ expect(farBehindToolBookmark(entries, 200_000)).toBe(0);
29
+ expect(farBehindToolBookmark([...entries, turn()], 200_000)).toBe(1);
30
+ });
31
+
32
+ it("single-turn tool loop ages at exactly a quarter of the usable window", () => {
33
+ const entries = [turn(), output(1), output(2, 49_999)];
34
+ expect(farBehindToolBookmark(entries, 200_000)).toBe(0);
35
+ expect(farBehindToolBookmark([...entries, { tokens: 1 }], 200_000)).toBe(1);
36
+ expect(farBehindToolBookmark([turn(), output(1, 100_000)], 200_000)).toBe(0);
37
+ });
38
+
39
+ it("multipart outputs never age from their own trailing chunks", () => {
40
+ expect(farBehindToolBookmark([output(1), output(1, 60_000)], 200_000)).toBe(0);
41
+ });
42
+
43
+ it("unknown windows disable only token distance", () => {
44
+ expect(farBehindToolBookmark([output(1), output(2, 99_999)], 0)).toBe(0);
45
+ expect(farBehindToolBookmark([output(1), turn(), turn(), turn()], 0)).toBe(1);
46
+ });
47
+
48
+ it("excludes synthetic host wakes, MC reminders, heads and result carriers", () => {
49
+ expect(
50
+ isReclaimUserTurn({
51
+ role: "user",
52
+ parts: [
53
+ {
54
+ kind: "text",
55
+ synthetic: true,
56
+ text: "<system-reminder>Background task finished.</system-reminder>",
57
+ },
58
+ ],
59
+ }),
60
+ ).toBe(false);
61
+ expect(
62
+ isReclaimUserTurn({
63
+ role: "user",
64
+ parts: [
65
+ {
66
+ kind: "text",
67
+ text: "<system-reminder>Your next step: call ctx_reduce on the outputs you've already used (5).</system-reminder>",
68
+ },
69
+ ],
70
+ }),
71
+ ).toBe(false);
72
+ expect(
73
+ isReclaimUserTurn({
74
+ role: "user",
75
+ syntheticHead: true,
76
+ parts: [{ kind: "text", text: "history" }],
77
+ }),
78
+ ).toBe(false);
79
+ expect(
80
+ isReclaimUserTurn({ role: "user", parts: [{ kind: "tool_result", text: "output" }] }),
81
+ ).toBe(false);
82
+ expect(
83
+ isReclaimUserTurn({
84
+ role: "user",
85
+ parts: [
86
+ { kind: "tool_result", text: "output" },
87
+ { kind: "text", text: "continue" },
88
+ ],
89
+ }),
90
+ ).toBe(true);
91
+ });
92
+
93
+ it("counts an authored part even after synthetic or ignored parts", () => {
94
+ for (const provenance of [{ synthetic: true }, { ignored: true }]) {
95
+ expect(
96
+ isReclaimUserTurn({ role: "user", parts: [{ kind: "image", ...provenance }] }),
97
+ ).toBe(false);
98
+ expect(
99
+ isReclaimUserTurn({
100
+ role: "user",
101
+ parts: [{ kind: "text", text: "notice", ...provenance }, { kind: "image" }],
102
+ }),
103
+ ).toBe(true);
104
+ }
105
+ });
106
+
107
+ it("a young lower-tag result holds back distant higher tags", () => {
108
+ expect(farBehindToolBookmark([output(2), output(3, 50_000), output(1)], 200_000)).toBe(0);
109
+ expect(farBehindToolBookmark([output(1), output(3, 50_000), output(2)], 200_000)).toBe(1);
110
+ });
111
+ });
@@ -0,0 +1,91 @@
1
+ export const RECLAIM_DISTANCE_USER_TURNS = 3;
2
+ export const RECLAIM_DISTANCE_WINDOW_FRACTION = 0.25;
3
+
4
+ export interface ReclaimDistanceEntry {
5
+ /** Local content tokens, excluding Magic Context's history and reduction/note reminders. */
6
+ tokens: number;
7
+ /** One event per authored user-role message, after any folded tool results. */
8
+ userTurn?: boolean;
9
+ /** Durable identifier of the tool invocation whose result ends at this entry. */
10
+ toolOutputTag?: number;
11
+ }
12
+
13
+ /** Only authored content starts a user turn; host control notices do not. */
14
+ export function isReclaimUserTurn(args: {
15
+ role?: string;
16
+ syntheticHead?: boolean;
17
+ synthetic?: boolean;
18
+ ignored?: boolean;
19
+ parts: readonly { kind: string; text?: string; synthetic?: boolean; ignored?: boolean }[];
20
+ }): boolean {
21
+ return (
22
+ args.role === "user" &&
23
+ !args.syntheticHead &&
24
+ !args.synthetic &&
25
+ !args.ignored &&
26
+ args.parts.some((part) => {
27
+ if (part.synthetic || part.ignored) return false;
28
+ if (part.kind !== "text" && part.kind !== "image" && part.kind !== "file") return false;
29
+ if (part.kind !== "text") return true;
30
+ return stripReclaimMcText(part.text ?? "").trim().length > 0;
31
+ })
32
+ );
33
+ }
34
+
35
+ /** Remove Magic Context's reduction/note reminders, not generic host wake notices. */
36
+ export function stripReclaimMcText(text: string): string {
37
+ return text
38
+ .replace(/^§\d+§\s*/, "")
39
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, (reminder) =>
40
+ /^(?:Your next step: call ctx_reduce on the outputs you've already used|Still unstamped:|Housekeeping: .* are reclaimable\. Stamp each output with ctx_reduce|.* are reclaimable\. Make a ctx_reduce pass now|.* are still unstamped\. Call ctx_reduce now)/.test(
41
+ reminder.slice("<system-reminder>".length).trimStart(),
42
+ )
43
+ ? ""
44
+ : reminder,
45
+ )
46
+ .replace(/<instruction name="deferred_notes">[\s\S]*?<\/instruction>/g, "");
47
+ }
48
+
49
+ /**
50
+ * A free cache rebuild is not evidence that the model has moved on. An output
51
+ * is far behind only after three newer user turns or a quarter-window of newer
52
+ * content. Return a prefix of qualifying tags; callers drop against the PREVIOUS
53
+ * durable bookmark before advancing it, so distance must survive two rebuilds.
54
+ */
55
+ export function farBehindToolBookmark(
56
+ entries: readonly ReclaimDistanceEntry[],
57
+ usableWindow: number,
58
+ ): number {
59
+ let newerTurns = 0;
60
+ let newerTokens = 0;
61
+ const farTags: number[] = [];
62
+ let firstYoungTag = Infinity;
63
+ const seenOutputs = new Set<number>();
64
+ const tokenDistance =
65
+ usableWindow > 0 && Number.isFinite(usableWindow)
66
+ ? usableWindow * RECLAIM_DISTANCE_WINDOW_FRACTION
67
+ : Infinity;
68
+ for (let index = entries.length - 1; index >= 0; index--) {
69
+ const entry = entries[index];
70
+ if (!entry) continue;
71
+ const tag = entry.toolOutputTag;
72
+ // Multi-part outputs are positioned at their last result, not their call
73
+ // or first chunk: an output must never count its own remaining payload.
74
+ if (tag !== undefined && !seenOutputs.has(tag)) {
75
+ seenOutputs.add(tag);
76
+ if (newerTurns >= RECLAIM_DISTANCE_USER_TURNS || newerTokens >= tokenDistance) {
77
+ farTags.push(tag);
78
+ } else {
79
+ firstYoungTag = Math.min(firstYoungTag, tag);
80
+ }
81
+ }
82
+ if (entry.userTurn) newerTurns++;
83
+ if (Number.isFinite(entry.tokens) && entry.tokens > 0) newerTokens += entry.tokens;
84
+ }
85
+ // Tags follow call order, not result arrival. A late lower-tag sibling must
86
+ // hold the prefix even when a higher-tag result is already distant.
87
+ return farTags.reduce(
88
+ (bookmark, tag) => (tag < firstYoungTag ? Math.max(bookmark, tag) : bookmark),
89
+ 0,
90
+ );
91
+ }
@@ -22,33 +22,33 @@ export function stableStringify(value: unknown, seen = new WeakSet<object>()): s
22
22
  // value's text once per enclosing level, so a large value N levels deep cost
23
23
  // N copies of itself.
24
24
  const pieces: string[] = [];
25
- writeStable(value, seen, pieces);
25
+ writeStable(value, seen, (piece) => pieces.push(piece));
26
26
  return pieces.join("");
27
27
  }
28
28
 
29
- function writeStable(value: unknown, seen: WeakSet<object>, pieces: string[]): void {
29
+ function writeStable(value: unknown, seen: WeakSet<object>, write: (piece: string) => void): void {
30
30
  if (value === undefined) {
31
- pieces.push("undefined");
31
+ write("undefined");
32
32
  return;
33
33
  }
34
34
  if (value === null || typeof value !== "object") {
35
- pieces.push(JSON.stringify(value) ?? String(value));
35
+ write(JSON.stringify(value) ?? String(value));
36
36
  return;
37
37
  }
38
38
  // Any object met a second time, shared or cyclic, is written as "[Circular]".
39
39
  if (seen.has(value)) {
40
- pieces.push('"[Circular]"');
40
+ write('"[Circular]"');
41
41
  return;
42
42
  }
43
43
  seen.add(value);
44
44
  if (Array.isArray(value)) {
45
- pieces.push("[");
45
+ write("[");
46
46
  for (let index = 0; index < value.length; index += 1) {
47
- if (index > 0) pieces.push(",");
47
+ if (index > 0) write(",");
48
48
  // A hole writes nothing between its commas, as joining a mapped array did.
49
- if (index in value) writeStable(value[index], seen, pieces);
49
+ if (index in value) writeStable(value[index], seen, write);
50
50
  }
51
- pieces.push("]");
51
+ write("]");
52
52
  return;
53
53
  }
54
54
  // Code-point sort (NOT localeCompare). Stable across runtimes/locales.
@@ -57,11 +57,53 @@ function writeStable(value: unknown, seen: WeakSet<object>, pieces: string[]): v
57
57
  if (a > b) return 1;
58
58
  return 0;
59
59
  });
60
- pieces.push("{");
60
+ // Native JSON can emit a small scalar-only object in one operation. Larger
61
+ // containers still stream; undefined and integer keys need the writer's
62
+ // non-native spelling/order. Bound chunks so hashing never builds a large JSON string.
63
+ if (
64
+ entries.length > 0 &&
65
+ entries.length <= 16 &&
66
+ entries.every(
67
+ ([key, child]) =>
68
+ key.length <= 4096 &&
69
+ !/^(0|[1-9]\d*)$/.test(key) &&
70
+ (child === null ||
71
+ typeof child === "number" ||
72
+ typeof child === "boolean" ||
73
+ (typeof child === "string" && child.length <= 4096)),
74
+ )
75
+ ) {
76
+ write(JSON.stringify(Object.fromEntries(entries)));
77
+ return;
78
+ }
79
+ write("{");
61
80
  entries.forEach(([key, child], index) => {
62
- if (index > 0) pieces.push(",");
63
- pieces.push(JSON.stringify(key), ":");
64
- writeStable(child, seen, pieces);
81
+ if (index > 0) write(",");
82
+ write(JSON.stringify(key));
83
+ write(":");
84
+ writeStable(child, seen, write);
65
85
  });
66
- pieces.push("}");
86
+ write("}");
87
+ }
88
+
89
+ /** The existing UTF-16 FNV fingerprint, without retaining the serialized JSON. */
90
+ export function stableStringifyHash(value: unknown, prefix = ""): string {
91
+ const sink = jsonHashSink(prefix);
92
+ writeStable(value, new WeakSet(), sink.write);
93
+ return sink.digest();
94
+ }
95
+
96
+ /** Hash the same code units as the content memo, including lone surrogates. */
97
+ export function jsonHashSink(prefix = ""): {
98
+ write: (piece: string) => void;
99
+ digest: () => string;
100
+ } {
101
+ let hash = 0x811c9dc5;
102
+ const write = (piece: string): void => {
103
+ for (let index = 0; index < piece.length; index++) {
104
+ hash = Math.imul(hash ^ piece.charCodeAt(index), 0x01000193);
105
+ }
106
+ };
107
+ write(prefix);
108
+ return { write, digest: () => (hash >>> 0).toString(16).padStart(8, "0") };
67
109
  }