@retaw106/mmc-oc 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +12 -1
- package/dist/config/schema/magic-context.d.ts +33 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config-init.js +12 -1
- package/dist/{context-wt7g4f8k.js → context-c72brxe5.js} +717 -206
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
- package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
- package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
- package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
- package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +4 -0
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +57 -2
- package/dist/features/magic-context/transform-decision-log.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/apply-operations.d.ts +3 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts +4 -0
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
- package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-trigger.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/degraded-pass-refusal.d.ts +2 -0
- package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
- package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
- package/dist/hooks/magic-context/edit-marker.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/pending-drop-eligibility.d.ts +14 -0
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts +2 -0
- package/dist/hooks/magic-context/pending-materialization.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/render-options.d.ts +19 -0
- package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
- 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/system-prompt-hash.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 +8 -4
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.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-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +2 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +0 -7
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
- package/dist/{index-3gj1ak4c.js → index-27ddyx39.js} +1 -1
- package/dist/{index-z2w1eq3y.js → index-48e9znrc.js} +491 -197
- package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
- package/dist/{index-czba8tw3.js → index-dwh99z66.js} +33 -10
- package/dist/{index-kw21z5pe.js → index-kahkxcqw.js} +7005 -6903
- package/dist/{index-a5mw0xp3.js → index-r7s7d350.js} +1 -1
- package/dist/{index-18yrysyj.js → index-ykq24x1n.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{message-time-backfill-sykevdax.js → message-time-backfill-j96x1sab.js} +1 -1
- package/dist/model-resolution-cvyrrcjw.js +8 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-863d6c06.js → read-session-chunk-t8bg6wkz.js} +3 -3
- package/dist/{recomp-orchestrator-wrjy3qym.js → recomp-orchestrator-k760qy1n.js} +6 -6
- package/dist/{send-session-notification-cz6ev088.js → send-session-notification-njjf3b4s.js} +3 -3
- package/dist/shared/model-suggestion-retry.d.ts +21 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +5 -0
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
- package/dist/shared/reclaim-distance.d.ts +33 -0
- package/dist/shared/reclaim-distance.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +2 -0
- package/dist/shared/sqlite.d.ts.map +1 -1
- 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/user-facing-codes.d.ts +2 -2
- package/dist/v2/hidden-completion.d.ts +20 -1
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +22 -1
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/server.js +8312 -7354
- package/dist/v2/store-reader.d.ts +27 -1
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/web/main.js +34 -10
- package/dist/{wrapup-orchestrator-bmybc5p5.js → wrapup-orchestrator-b2gswatj.js} +6 -6
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +4 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/schema/magic-context.test.ts +50 -0
- package/src/config/schema/magic-context.ts +34 -1
- package/src/shared/model-suggestion-retry.ts +5 -1
- package/src/shared/prompt-surface-runtime.ts +12 -0
- package/src/shared/reclaim-distance.test.ts +111 -0
- package/src/shared/reclaim-distance.ts +91 -0
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +237 -1
- package/src/shared/sqlite.ts +55 -14
- 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/user-facing-codes.ts +2 -2
- package/dist/model-resolution-wazv26n0.js +0 -6
package/src/shared/sqlite.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
42
|
+
import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
|
|
43
43
|
import { statSync } from "node:fs";
|
|
44
44
|
// Type import only — runtime is loaded dynamically below. @types/better-sqlite3
|
|
45
45
|
// has the richest definitions and is a structural superset of the API surface
|
|
@@ -47,6 +47,16 @@ import { statSync } from "node:fs";
|
|
|
47
47
|
// node:sqlite at runtime (both expose prepare/run/get/all/exec/close).
|
|
48
48
|
import type BetterSqlite3 from "better-sqlite3";
|
|
49
49
|
|
|
50
|
+
// Capture the root before creating request-local AsyncLocalStorage scopes.
|
|
51
|
+
// Shared workers and timers must not inherit callbacks that reject stale
|
|
52
|
+
// database writes: those callbacks can retain an obsolete request's inputs.
|
|
53
|
+
const resourceRoot = new AsyncResource("magic-context:root");
|
|
54
|
+
|
|
55
|
+
/** Create host-owned resources without inheriting request-local state. */
|
|
56
|
+
export function inRootAsyncContext<T>(operation: () => T): T {
|
|
57
|
+
return resourceRoot.runInAsyncScope(operation);
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
/**
|
|
51
61
|
* Slow-write reporting is injected rather than imported: this module is also
|
|
52
62
|
* executed directly by Node (the `node:sqlite` smoke in CI, Pi, Desktop), and
|
|
@@ -419,7 +429,9 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
419
429
|
!/^\s*(?:SELECT|EXPLAIN|PRAGMA|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql)
|
|
420
430
|
) {
|
|
421
431
|
try {
|
|
422
|
-
|
|
432
|
+
// exec can commit a script prefix before BUSY. Run it once;
|
|
433
|
+
// only prepared statements are safe to probe and retry.
|
|
434
|
+
return withAutocommitTimeout(db, () => nativeExec(sql), false);
|
|
423
435
|
} finally {
|
|
424
436
|
if (mayChangeTagIdentity(sql)) tagIdentityWriteGeneration += 1;
|
|
425
437
|
}
|
|
@@ -586,7 +598,9 @@ function trackSqliteConnection(
|
|
|
586
598
|
|
|
587
599
|
const TrackedDatabase = new Proxy(DatabaseImpl, {
|
|
588
600
|
construct(target, args) {
|
|
589
|
-
const db =
|
|
601
|
+
const db = inRootAsyncContext(
|
|
602
|
+
() => Reflect.construct(target, args, target) as BetterSqlite3.Database,
|
|
603
|
+
);
|
|
590
604
|
return trackSqliteConnection(db, args[0], args[1]);
|
|
591
605
|
},
|
|
592
606
|
}) as typeof BetterSqlite3;
|
|
@@ -999,33 +1013,60 @@ function foregroundWaitLease() {
|
|
|
999
1013
|
: undefined;
|
|
1000
1014
|
}
|
|
1001
1015
|
|
|
1002
|
-
/**
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
|
|
1016
|
+
/** Probe a prepared statement's writer lock without charging uncontended work
|
|
1017
|
+
* against the foreground lock-wait budget. An exec script may commit some of
|
|
1018
|
+
* its statements before failing, so it cannot be retried without duplicating
|
|
1019
|
+
* writes; conservatively charge its single attempt's full execution time. */
|
|
1020
|
+
function withAutocommitTimeout<T>(db: Database, operation: () => T, singleStatement = true): T {
|
|
1007
1021
|
assertTransformWrite();
|
|
1008
1022
|
const lease = foregroundWaitLease();
|
|
1009
1023
|
if (isInTransaction(db)) return operation();
|
|
1010
1024
|
const scoped = lease || backgroundWriterScope.getStore();
|
|
1011
1025
|
const previous = scoped ? pragmaNumber(db, "busy_timeout") : null;
|
|
1012
|
-
if (scoped)
|
|
1013
|
-
db.exec(
|
|
1014
|
-
`PRAGMA busy_timeout=${lease ? Math.floor(Math.min(lease.remainingWaitMs, lease.guard?.remainingMs() ?? Infinity)) : SHORT_BUSY_TIMEOUT_MS}`,
|
|
1015
|
-
);
|
|
1016
1026
|
const started = performance.now();
|
|
1017
1027
|
let completed = false;
|
|
1018
|
-
|
|
1028
|
+
const execute = () => {
|
|
1019
1029
|
const result = operation();
|
|
1020
1030
|
completed = true;
|
|
1021
1031
|
return result;
|
|
1032
|
+
};
|
|
1033
|
+
try {
|
|
1034
|
+
if (!scoped) return execute();
|
|
1035
|
+
if (singleStatement) {
|
|
1036
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
1037
|
+
try {
|
|
1038
|
+
return execute();
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (!isTransientSqliteError(error) || isInTransaction(db)) throw error;
|
|
1041
|
+
}
|
|
1042
|
+
assertTransformWrite();
|
|
1043
|
+
}
|
|
1044
|
+
const timeout = Math.floor(
|
|
1045
|
+
Math.min(
|
|
1046
|
+
lease?.remainingWaitMs ?? SHORT_BUSY_TIMEOUT_MS,
|
|
1047
|
+
lease?.guard?.remainingMs() ?? Infinity,
|
|
1048
|
+
),
|
|
1049
|
+
);
|
|
1050
|
+
db.exec(`PRAGMA busy_timeout=${timeout}`);
|
|
1051
|
+
const waitStarted = performance.now();
|
|
1052
|
+
try {
|
|
1053
|
+
return execute();
|
|
1054
|
+
} finally {
|
|
1055
|
+
// Deduct the prepared statement's contended retry or the exec
|
|
1056
|
+
// script's execution from the remaining foreground wait budget;
|
|
1057
|
+
// the native API cannot separate lock waiting from execution.
|
|
1058
|
+
if (lease)
|
|
1059
|
+
lease.remainingWaitMs = Math.max(
|
|
1060
|
+
0,
|
|
1061
|
+
lease.remainingWaitMs - (performance.now() - waitStarted),
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1022
1064
|
} catch (error) {
|
|
1023
1065
|
if (lease && isTransientSqliteError(error))
|
|
1024
1066
|
throw new SqliteAcquisitionBusyError(error, "autocommit");
|
|
1025
1067
|
throw error;
|
|
1026
1068
|
} finally {
|
|
1027
1069
|
const elapsed = performance.now() - started;
|
|
1028
|
-
if (lease) lease.remainingWaitMs = Math.max(0, lease.remainingWaitMs - elapsed);
|
|
1029
1070
|
if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
|
|
1030
1071
|
// Native autocommit does not expose the instant its lock was acquired.
|
|
1031
1072
|
// Label the upper bound so a waiting statement is not blamed as a holder.
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { jsonHashSink, stableStringify, stableStringifyHash } from "./stable-json";
|
|
3
|
+
import { piJsonHash, TailHygieneJsonMemo } from "./tail-hygiene-json";
|
|
4
|
+
|
|
5
|
+
function hash(text: string): string {
|
|
6
|
+
const sink = jsonHashSink("excluded\0");
|
|
7
|
+
sink.write(text);
|
|
8
|
+
return sink.digest();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function piStringify(value: unknown): string {
|
|
12
|
+
const seen = new Set<object>();
|
|
13
|
+
try {
|
|
14
|
+
return (
|
|
15
|
+
JSON.stringify(value, (_key, current) => {
|
|
16
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
17
|
+
return current;
|
|
18
|
+
if (seen.has(current)) return "[Circular]";
|
|
19
|
+
seen.add(current);
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
Object.entries(current).sort(([a], [b]) => a.localeCompare(b)),
|
|
22
|
+
);
|
|
23
|
+
}) ?? ""
|
|
24
|
+
);
|
|
25
|
+
} catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("streamed excluded fingerprints equal both original serializers on randomized values", () => {
|
|
31
|
+
let state = 704;
|
|
32
|
+
const random = (n: number): number => {
|
|
33
|
+
state = (Math.imul(state, 1664525) + 1013904223) | 0;
|
|
34
|
+
return (state >>> 0) % n;
|
|
35
|
+
};
|
|
36
|
+
const leaves = [
|
|
37
|
+
undefined,
|
|
38
|
+
null,
|
|
39
|
+
-0,
|
|
40
|
+
NaN,
|
|
41
|
+
Infinity,
|
|
42
|
+
1e-7,
|
|
43
|
+
1e23,
|
|
44
|
+
false,
|
|
45
|
+
"\ud800",
|
|
46
|
+
"é😀\n\u2028",
|
|
47
|
+
'"\\',
|
|
48
|
+
];
|
|
49
|
+
const generate = (depth: number): unknown => {
|
|
50
|
+
if (!depth || random(3) === 0) return leaves[random(leaves.length)];
|
|
51
|
+
if (random(2)) {
|
|
52
|
+
const array = new Array(4);
|
|
53
|
+
array[0] = generate(depth - 1);
|
|
54
|
+
array[2] = undefined;
|
|
55
|
+
array[3] = generate(depth - 1);
|
|
56
|
+
return array;
|
|
57
|
+
}
|
|
58
|
+
const value: Record<string, unknown> = {};
|
|
59
|
+
const keys = ["10", "2", "Z", "ä", "a", "e\u0301", "é"];
|
|
60
|
+
for (let i = 0; i < 4; i++) value[keys[random(keys.length)]] = generate(depth - 1);
|
|
61
|
+
if (random(5) === 0) value.self = value;
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
const shared = { a: undefined, z: "shared" };
|
|
65
|
+
const cycle: unknown[] = [];
|
|
66
|
+
cycle.push(cycle);
|
|
67
|
+
const samples: unknown[] = [
|
|
68
|
+
cycle,
|
|
69
|
+
{ a: shared, b: shared },
|
|
70
|
+
1n,
|
|
71
|
+
new Date(0),
|
|
72
|
+
{ toJSON: () => "date" },
|
|
73
|
+
];
|
|
74
|
+
for (let i = 0; i < 2_000; i++) samples.push(generate(4));
|
|
75
|
+
for (const value of samples) {
|
|
76
|
+
try {
|
|
77
|
+
const expected = stableStringify(value);
|
|
78
|
+
expect(stableStringifyHash(value, "excluded\0")).toBe(hash(expected));
|
|
79
|
+
} catch {
|
|
80
|
+
expect(() => stableStringifyHash(value, "excluded\0")).toThrow();
|
|
81
|
+
}
|
|
82
|
+
expect(piJsonHash(value, () => piStringify(value))).toBe(hash(piStringify(value)));
|
|
83
|
+
}
|
|
84
|
+
console.log(`excluded fingerprint differential: ${samples.length} values per serializer`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("part memo catches in-place edits, alias changes, key order and accessor values", () => {
|
|
88
|
+
const memo = new TailHygieneJsonMemo();
|
|
89
|
+
let calls = 0;
|
|
90
|
+
const get = (value: unknown): string => {
|
|
91
|
+
memo.beginPass();
|
|
92
|
+
return memo.get("part", value, () => {
|
|
93
|
+
calls++;
|
|
94
|
+
return stableStringify(value);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const shared = { value: "first" };
|
|
98
|
+
const value = { a: shared, b: shared };
|
|
99
|
+
expect(get(value)).toBe(get(structuredClone(value)));
|
|
100
|
+
expect(calls).toBe(1);
|
|
101
|
+
shared.value = "other";
|
|
102
|
+
expect(get(value)).toContain("other");
|
|
103
|
+
expect(calls).toBe(2);
|
|
104
|
+
value.b = { value: "other" };
|
|
105
|
+
expect(get(value)).not.toContain("Circular");
|
|
106
|
+
expect(calls).toBe(3);
|
|
107
|
+
expect(get({ b: value.b, a: value.a })).toBe(get(value));
|
|
108
|
+
expect(calls).toBe(5);
|
|
109
|
+
let changing = 0;
|
|
110
|
+
const accessor = {
|
|
111
|
+
get value() {
|
|
112
|
+
return ++changing;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
expect(get(accessor)).not.toBe(get(accessor));
|
|
116
|
+
expect(calls).toBe(7);
|
|
117
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { jsonHashSink } from "./stable-json";
|
|
2
|
+
|
|
3
|
+
const MAX_MEMO_BYTES = 64 * 1024 * 1024;
|
|
4
|
+
const MAX_MEMO_ENTRIES = 10_000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A bounded, process-local memo for individual parts and selected tool fields.
|
|
8
|
+
* Detached snapshots catch in-place host edits; reference equality alone cannot.
|
|
9
|
+
* Key collisions between sessions only evict entries, never imply equality.
|
|
10
|
+
*/
|
|
11
|
+
export class TailHygieneJsonMemo {
|
|
12
|
+
private entries = new Map<string, { snapshot: unknown; result: string; bytes: number }>();
|
|
13
|
+
private bytes = 0;
|
|
14
|
+
private passText = new WeakMap<object, string>();
|
|
15
|
+
private passHash = new WeakMap<object, string>();
|
|
16
|
+
|
|
17
|
+
beginPass(): void {
|
|
18
|
+
this.passText = new WeakMap();
|
|
19
|
+
this.passHash = new WeakMap();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(key: string, value: unknown, compute: () => string, fingerprint = false): string {
|
|
23
|
+
const pass = fingerprint ? this.passHash : this.passText;
|
|
24
|
+
const object = value !== null && typeof value === "object" ? value : undefined;
|
|
25
|
+
const local = object ? pass.get(object) : undefined;
|
|
26
|
+
if (local !== undefined) return local;
|
|
27
|
+
const previous = this.entries.get(key);
|
|
28
|
+
if (previous && sameJson(previous.snapshot, value)) {
|
|
29
|
+
if (object) pass.set(object, previous.result);
|
|
30
|
+
return previous.result;
|
|
31
|
+
}
|
|
32
|
+
const result = compute();
|
|
33
|
+
if (previous) {
|
|
34
|
+
this.bytes -= previous.bytes;
|
|
35
|
+
this.entries.delete(key);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const copy = copyJson(value);
|
|
39
|
+
const bytes = copy.bytes + (key.length + result.length) * 2;
|
|
40
|
+
if (bytes <= MAX_MEMO_BYTES) {
|
|
41
|
+
this.entries.set(key, { snapshot: copy.value, result, bytes });
|
|
42
|
+
this.bytes += bytes;
|
|
43
|
+
if (object) pass.set(object, result);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Accessors and exotic objects retain their original serialization behavior.
|
|
47
|
+
}
|
|
48
|
+
while (this.bytes > MAX_MEMO_BYTES || this.entries.size > MAX_MEMO_ENTRIES) {
|
|
49
|
+
const oldest = this.entries.entries().next().value;
|
|
50
|
+
if (!oldest) break;
|
|
51
|
+
this.bytes -= oldest[1].bytes;
|
|
52
|
+
this.entries.delete(oldest[0]);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function copyJson(value: unknown): { value: unknown; bytes: number } {
|
|
59
|
+
const seen = new Map<object, unknown>();
|
|
60
|
+
let bytes = 0;
|
|
61
|
+
const copy = (current: unknown): unknown => {
|
|
62
|
+
bytes += 16;
|
|
63
|
+
if (typeof current === "string") bytes += current.length * 2;
|
|
64
|
+
if (bytes > MAX_MEMO_BYTES) throw new Error("memo capacity");
|
|
65
|
+
if (current === null || typeof current !== "object") {
|
|
66
|
+
if (["function", "symbol", "bigint"].includes(typeof current))
|
|
67
|
+
throw new Error("exotic");
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
if (seen.has(current)) return seen.get(current);
|
|
71
|
+
const prototype = Object.getPrototypeOf(current);
|
|
72
|
+
if (!Array.isArray(current) && prototype !== Object.prototype && prototype !== null)
|
|
73
|
+
throw new Error("exotic");
|
|
74
|
+
const result = Array.isArray(current)
|
|
75
|
+
? new Array(current.length)
|
|
76
|
+
: Object.create(prototype);
|
|
77
|
+
seen.set(current, result);
|
|
78
|
+
for (const key of Object.keys(current)) {
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
80
|
+
if (!descriptor || !("value" in descriptor)) throw new Error("accessor");
|
|
81
|
+
bytes += key.length * 2 + 16;
|
|
82
|
+
if (key === "__proto__") {
|
|
83
|
+
Object.defineProperty(result, key, {
|
|
84
|
+
value: copy(descriptor.value),
|
|
85
|
+
enumerable: true,
|
|
86
|
+
writable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
});
|
|
89
|
+
} else result[key] = copy(descriptor.value);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
return { value: copy(value), bytes };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sameJson(before: unknown, after: unknown): boolean {
|
|
97
|
+
const forward = new Map<object, object>();
|
|
98
|
+
const reverse = new Set<object>();
|
|
99
|
+
const same = (left: unknown, right: unknown): boolean => {
|
|
100
|
+
if (
|
|
101
|
+
left === null ||
|
|
102
|
+
right === null ||
|
|
103
|
+
typeof left !== "object" ||
|
|
104
|
+
typeof right !== "object"
|
|
105
|
+
)
|
|
106
|
+
return Object.is(left, right);
|
|
107
|
+
if (forward.has(left)) return forward.get(left) === right;
|
|
108
|
+
if (reverse.has(right) || Object.getPrototypeOf(left) !== Object.getPrototypeOf(right))
|
|
109
|
+
return false;
|
|
110
|
+
forward.set(left, right);
|
|
111
|
+
reverse.add(right);
|
|
112
|
+
if (Array.isArray(left) && (!Array.isArray(right) || left.length !== right.length))
|
|
113
|
+
return false;
|
|
114
|
+
const keys = Object.keys(left);
|
|
115
|
+
const otherKeys = Object.keys(right);
|
|
116
|
+
if (keys.length !== otherKeys.length) return false;
|
|
117
|
+
for (let index = 0; index < keys.length; index++) {
|
|
118
|
+
const key = keys[index];
|
|
119
|
+
// Pi sorts with localeCompare, which can tie distinct Unicode spellings.
|
|
120
|
+
// Preserve insertion order too, because a tied sort keeps that order.
|
|
121
|
+
if (key !== otherKeys[index]) return false;
|
|
122
|
+
const descriptor = Object.getOwnPropertyDescriptor(right, key);
|
|
123
|
+
if (
|
|
124
|
+
!descriptor ||
|
|
125
|
+
!("value" in descriptor) ||
|
|
126
|
+
!same((left as Record<string, unknown>)[key], descriptor.value)
|
|
127
|
+
)
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
};
|
|
132
|
+
return same(before, after);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fingerprint excluded Pi content without retaining its whole JSON string.
|
|
137
|
+
* Match Pi's native JSON replacer: localeCompare key order, omitted undefined
|
|
138
|
+
* object fields, null array holes, and a marker for repeated non-array objects.
|
|
139
|
+
* Unsupported objects and array cycles use the original serializer; serialization
|
|
140
|
+
* errors produce the fingerprint of an empty string, as they did before.
|
|
141
|
+
*/
|
|
142
|
+
export function piJsonHash(value: unknown, fallback: () => string): string {
|
|
143
|
+
const sink = jsonHashSink("excluded\0");
|
|
144
|
+
try {
|
|
145
|
+
const seen = new Set<object>();
|
|
146
|
+
const arrays = new Set<object>();
|
|
147
|
+
const write = (current: unknown): void => {
|
|
148
|
+
if (current === null || typeof current !== "object") {
|
|
149
|
+
if (["function", "symbol", "bigint"].includes(typeof current))
|
|
150
|
+
throw new Error("exotic");
|
|
151
|
+
sink.write(JSON.stringify(current) ?? "");
|
|
152
|
+
} else if (Array.isArray(current)) {
|
|
153
|
+
if (
|
|
154
|
+
Object.getPrototypeOf(current) !== Array.prototype ||
|
|
155
|
+
Object.hasOwn(current, "toJSON")
|
|
156
|
+
)
|
|
157
|
+
throw new Error("exotic array");
|
|
158
|
+
if (arrays.has(current)) throw new Error("array cycle");
|
|
159
|
+
arrays.add(current);
|
|
160
|
+
sink.write("[");
|
|
161
|
+
for (let index = 0; index < current.length; index++) {
|
|
162
|
+
if (index) sink.write(",");
|
|
163
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, index);
|
|
164
|
+
if (descriptor && !("value" in descriptor)) throw new Error("accessor");
|
|
165
|
+
if (!descriptor && index in current) throw new Error("inherited array element");
|
|
166
|
+
write(descriptor?.value === undefined ? null : descriptor.value);
|
|
167
|
+
}
|
|
168
|
+
sink.write("]");
|
|
169
|
+
arrays.delete(current);
|
|
170
|
+
} else if (seen.has(current)) {
|
|
171
|
+
sink.write('"[Circular]"');
|
|
172
|
+
} else {
|
|
173
|
+
const prototype = Object.getPrototypeOf(current);
|
|
174
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error("exotic");
|
|
175
|
+
seen.add(current);
|
|
176
|
+
const entries = Object.keys(current).map((key): [string, unknown] => {
|
|
177
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
178
|
+
if (!descriptor || !("value" in descriptor)) throw new Error("accessor");
|
|
179
|
+
return [key, descriptor.value];
|
|
180
|
+
});
|
|
181
|
+
const sorted = Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)));
|
|
182
|
+
if (
|
|
183
|
+
entries.length <= 16 &&
|
|
184
|
+
entries.every(
|
|
185
|
+
([key, child]) =>
|
|
186
|
+
key.length <= 4096 &&
|
|
187
|
+
(child === null ||
|
|
188
|
+
child === undefined ||
|
|
189
|
+
typeof child === "number" ||
|
|
190
|
+
typeof child === "boolean" ||
|
|
191
|
+
(typeof child === "string" && child.length <= 4096)),
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
sink.write(JSON.stringify(sorted));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
sink.write("{");
|
|
198
|
+
let first = true;
|
|
199
|
+
for (const [key, child] of Object.entries(sorted)) {
|
|
200
|
+
if (child === undefined) continue;
|
|
201
|
+
if (!first) sink.write(",");
|
|
202
|
+
first = false;
|
|
203
|
+
sink.write(JSON.stringify(key));
|
|
204
|
+
sink.write(":");
|
|
205
|
+
write(child);
|
|
206
|
+
}
|
|
207
|
+
sink.write("}");
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
write(value);
|
|
211
|
+
return sink.digest();
|
|
212
|
+
} catch {
|
|
213
|
+
const failed = jsonHashSink("excluded\0");
|
|
214
|
+
failed.write(fallback());
|
|
215
|
+
return failed.digest();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -18,8 +18,8 @@ export const USER_FACING_FAILURES = {
|
|
|
18
18
|
history_boundary_unresolved: {
|
|
19
19
|
code: "MC-H04",
|
|
20
20
|
sentence:
|
|
21
|
-
"This request was not sent:
|
|
22
|
-
action: "
|
|
21
|
+
"This request was not sent: Magic Context could not safely place the history summary boundary in the loaded conversation, and the untrimmed request is larger than the model's context window.",
|
|
22
|
+
action: "Reopen this session in its current project directory, then run /mmc-recomp to rebuild the history summary. A model with a larger context window can also help.",
|
|
23
23
|
},
|
|
24
24
|
history_over_window_unmanaged: {
|
|
25
25
|
code: "MC-H06",
|