@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.
- package/dist/agents/hidden-agent-registrations.d.ts +5 -3
- package/dist/agents/hidden-agent-registrations.d.ts.map +1 -1
- package/dist/auto-search-worker.js +1 -1
- package/dist/config/schema/agent-overrides.d.ts +13 -13
- package/dist/config/schema/magic-context.d.ts +500 -500
- package/dist/{context-x0ccfv0f.js → context-ey3ap3cc.js} +29 -24
- package/dist/features/magic-context/primer-clustering.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler-prompt.d.ts +1 -1
- package/dist/features/magic-context/smart-notes/compiler-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
- package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/dropped-input-guard.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/provider-visible-parts.d.ts +10 -4
- package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
- package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +5 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-b4gcb3rk.js → index-0x9cn9w1.js} +2 -2
- package/dist/{index-ydjgz36f.js → index-16vwdyy5.js} +1 -1
- package/dist/{index-xf6k8szp.js → index-21919gcq.js} +3 -3
- package/dist/{index-hxrvx6cy.js → index-3403gbzj.js} +129 -103
- package/dist/{index-gzg9ekc1.js → index-6k86es2p.js} +129 -15
- package/dist/{index-8kk7yh6m.js → index-9jvab89d.js} +1 -1
- package/dist/{index-qgp5vnt4.js → index-a865mj02.js} +1 -1
- package/dist/{index-xpn05jxq.js → index-f8cn4xqa.js} +287 -58
- package/dist/{index-2rw244rh.js → index-ffa8x2as.js} +4 -4
- package/dist/{index-bd9cjab3.js → index-g7jkrz0x.js} +35 -15
- package/dist/{index-pjgzdezw.js → index-nenz4gpb.js} +3 -3
- package/dist/{index-zf9nydbd.js → index-swgpe402.js} +4 -4
- package/dist/{index-9g1c4bt9.js → index-wpwpn7nj.js} +88 -8
- package/dist/{index-w1ejb9qr.js → index-x57ep4fp.js} +33 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -46
- package/dist/{message-index-zs882h8e.js → message-index-6r1x9cqh.js} +5 -5
- package/dist/{message-index-bounded-hxjp56qk.js → message-index-bounded-fskdyc81.js} +5 -5
- package/dist/migration-worker.js +4 -4
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/raw-ordinal-warmup-worker.js +3 -3
- package/dist/{read-session-chunk-t9r7r0sg.js → read-session-chunk-zgpvs423.js} +4 -4
- package/dist/{recomp-orchestrator-gr36zvd2.js → recomp-orchestrator-77xc8wpe.js} +10 -10
- package/dist/{search-qjen1pmz.js → search-72fkjwk2.js} +8 -8
- package/dist/shared/hidden-zen-read.d.ts +24 -0
- package/dist/shared/hidden-zen-read.d.ts.map +1 -0
- package/dist/shared/reclaim-distance.d.ts +33 -0
- package/dist/shared/reclaim-distance.d.ts.map +1 -0
- package/dist/shared/stable-json.d.ts +7 -0
- package/dist/shared/stable-json.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-json.d.ts +22 -0
- package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
- package/dist/shared/transcript.d.ts +5 -0
- package/dist/shared/transcript.d.ts.map +1 -1
- package/dist/{storage-tags-a513rkj3.js → storage-tags-35m4gms2.js} +2 -2
- package/dist/{transform-postprocess-phase-yv463b7y.js → transform-postprocess-phase-r8d406j0.js} +10 -10
- package/dist/v2/auto-search-worker.js +1 -1
- package/dist/v2/{context-wd82v9z6.js → context-z2yagwy4.js} +168 -35
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +2 -2
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/{message-index-yd94vt9n.js → message-index-9ezq6wze.js} +5 -5
- package/dist/v2/{message-index-bounded-madg2kfr.js → message-index-bounded-rqc603f2.js} +5 -5
- package/dist/v2/migration-worker.js +4 -4
- package/dist/v2/raw-ordinal-warmup-worker.js +3 -3
- package/dist/v2/{read-session-chunk-7qs791f3.js → read-session-chunk-7f87tg2q.js} +4 -4
- package/dist/v2/{recomp-orchestrator-ee91r9s6.js → recomp-orchestrator-s5tdpsbs.js} +9 -9
- package/dist/v2/{search-pn15rr52.js → search-2td1b1wy.js} +8 -8
- package/dist/v2/{server-08k4zkfk.js → server-23znwjqk.js} +4 -4
- package/dist/v2/{server-y5g32dhq.js → server-2hxypdx5.js} +4 -4
- package/dist/v2/{server-yak3sxya.js → server-4zwx20yg.js} +1 -1
- package/dist/v2/{server-shme41gj.js → server-89fcbe37.js} +35 -15
- package/dist/v2/{server-q360kdb8.js → server-c9w52qss.js} +30 -13
- package/dist/v2/{server-4gz0xkrc.js → server-f27he3j1.js} +1 -1
- package/dist/v2/{server-xj4mewze.js → server-gc8x2xdb.js} +66 -8
- package/dist/v2/{server-jjsf29aq.js → server-kmapq3v0.js} +286 -57
- package/dist/v2/{server-adfte03x.js → server-xqf2d2mg.js} +1 -1
- package/dist/v2/{server-j3dm74ey.js → server-z0rjemg1.js} +2 -2
- package/dist/v2/server.js +2 -2
- package/dist/v2/{storage-tags-spa92sgj.js → storage-tags-c4zfcxv4.js} +2 -2
- package/dist/v2/{wrapup-orchestrator-5wxy1ggv.js → wrapup-orchestrator-wg8dmmm9.js} +9 -9
- package/dist/{wrapup-orchestrator-gv08c6f9.js → wrapup-orchestrator-b1z99yg5.js} +11 -11
- package/package.json +1 -1
- package/src/agents/hidden-agent-registrations.ts +22 -4
- package/src/shared/hidden-zen-read.test.ts +99 -0
- package/src/shared/hidden-zen-read.ts +88 -0
- package/src/shared/reclaim-distance.test.ts +111 -0
- package/src/shared/reclaim-distance.ts +91 -0
- package/src/shared/stable-json.ts +56 -14
- package/src/shared/tail-hygiene-json.test.ts +117 -0
- package/src/shared/tail-hygiene-json.ts +217 -0
- package/src/shared/transcript.ts +7 -0
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
registerActiveCompartmentRun,
|
|
20
20
|
setRecompStarting2,
|
|
21
21
|
setRecompTerminal2
|
|
22
|
-
} from "./server-
|
|
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-
|
|
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-
|
|
40
|
-
import"./server-
|
|
41
|
-
import"./server-
|
|
39
|
+
} from "./server-gc8x2xdb.js";
|
|
40
|
+
import"./server-89fcbe37.js";
|
|
41
|
+
import"./server-23znwjqk.js";
|
|
42
42
|
import"./server-1zx87sdm.js";
|
|
43
|
-
import"./server-
|
|
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-
|
|
49
|
+
import"./server-xqf2d2mg.js";
|
|
50
50
|
import"./server-fr0wbhdn.js";
|
|
51
|
-
import"./server-
|
|
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-
|
|
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-
|
|
4
|
-
import"./index-
|
|
5
|
-
import"./index-
|
|
3
|
+
} from "./index-nenz4gpb.js";
|
|
4
|
+
import"./index-21919gcq.js";
|
|
5
|
+
import"./index-3403gbzj.js";
|
|
6
6
|
import"./index-s178h9z8.js";
|
|
7
|
-
import"./index-
|
|
8
|
-
import"./index-
|
|
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-
|
|
23
|
-
import"./index-
|
|
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-
|
|
30
|
-
import"./index-
|
|
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-
|
|
37
|
-
import"./index-
|
|
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,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.
|
|
81
|
-
*
|
|
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;
|
|
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 (
|
|
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>,
|
|
29
|
+
function writeStable(value: unknown, seen: WeakSet<object>, write: (piece: string) => void): void {
|
|
30
30
|
if (value === undefined) {
|
|
31
|
-
|
|
31
|
+
write("undefined");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
if (value === null || typeof value !== "object") {
|
|
35
|
-
|
|
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
|
-
|
|
40
|
+
write('"[Circular]"');
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
seen.add(value);
|
|
44
44
|
if (Array.isArray(value)) {
|
|
45
|
-
|
|
45
|
+
write("[");
|
|
46
46
|
for (let index = 0; index < value.length; index += 1) {
|
|
47
|
-
if (index > 0)
|
|
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,
|
|
49
|
+
if (index in value) writeStable(value[index], seen, write);
|
|
50
50
|
}
|
|
51
|
-
|
|
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
|
-
|
|
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)
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
if (index > 0) write(",");
|
|
82
|
+
write(JSON.stringify(key));
|
|
83
|
+
write(":");
|
|
84
|
+
writeStable(child, seen, write);
|
|
65
85
|
});
|
|
66
|
-
|
|
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
|
}
|