recallnest 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,1178 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { createHash } from "node:crypto";
3
- import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
-
7
- import {
8
- acquireOutputLock,
9
- aggregateDetailedUsage,
10
- agySessionIdFromPath,
11
- assertExternalModeAuthorization,
12
- assertPivotResponseMetadata,
13
- buildPivotRequestProfile,
14
- buildPivotLlmConfig,
15
- buildStratifiedSample,
16
- coalesceTurns,
17
- extractAgyUserText,
18
- extractTurnsFromRecord,
19
- filteredDejaEnvironment,
20
- frozenSampleHash,
21
- geminiJsonTurns,
22
- harnessFamily,
23
- inventoryCompletenessReasons,
24
- isSyntheticSession,
25
- loadFrozenBundle,
26
- loadSessionInventory,
27
- main,
28
- OMISSION_MARKER,
29
- parseCliOptions,
30
- parserKindFor,
31
- redactedResidue,
32
- PIPELINE_VERSION,
33
- PIVOT_MODEL,
34
- pivotRequestProfileHash,
35
- PROMPT_VERSION,
36
- readSessionTurns,
37
- redactForExternalModel,
38
- retryDecision,
39
- resolveBundleSamplePath,
40
- resolveClaudeSessionPath,
41
- safeEndpoint,
42
- selectFrozenEntries,
43
- selectionIdentityHash,
44
- sessionFingerprint,
45
- stableSessionId,
46
- storedResultMatches,
47
- validateJudgeResponse,
48
- writeFrozenBundle,
49
- type FrozenBundleDescriptor,
50
- type SessionMeta,
51
- } from "../../scripts/pivot-distill.js";
52
-
53
- function session(overrides: Partial<SessionMeta> = {}): SessionMeta {
54
- return {
55
- key: "codex:019fc307-9cdb-7c51-812d-7b2d3545c656",
56
- sessionId: "019fc307-9cdb-7c51-812d-7b2d3545c656",
57
- rawHarness: "codex",
58
- harness: "codex",
59
- parserKind: "codex-rollout",
60
- project: "recallnest",
61
- path: "/tmp/rollout-019fc307-9cdb-7c51-812d-7b2d3545c656.jsonl",
62
- date: "2026-08-02",
63
- sizeBytes: 200_000,
64
- mtimeNs: "123456789",
65
- origin: "deja",
66
- fingerprint: "abc",
67
- ...overrides,
68
- };
69
- }
70
-
71
- function sha256(value: string): string {
72
- return createHash("sha256").update(value).digest("hex");
73
- }
74
-
75
- function bundleIdentityHashForTest(input: {
76
- requestProfileHash: string;
77
- manifestSha256: string;
78
- sessions: number;
79
- eligibleSessions: number;
80
- deferredSessions: number;
81
- }): string {
82
- return sha256(JSON.stringify({ schemaVersion: 1, ...input }));
83
- }
84
-
85
- function createBundleFixture(): {
86
- outputDir: string;
87
- bundleDir: string;
88
- descriptor: FrozenBundleDescriptor;
89
- } {
90
- const outputDir = mkdtempSync(join(tmpdir(), "pivot-bundle-"));
91
- const source = join(outputDir, "source.jsonl");
92
- writeFileSync(source, "{}\n");
93
- const sourceStat = statSync(source, { bigint: true });
94
- const profile = buildPivotRequestProfile("https://example.invalid/compatible-mode/v1?ignored=1#fragment", 600);
95
- const profileHash = pivotRequestProfileHash(profile);
96
- const sample = buildStratifiedSample([
97
- { role: "user", text: "以后必须保留可回查证据" },
98
- { role: "assistant", text: "已记录这条规则" },
99
- ], 600);
100
- const descriptor = writeFrozenBundle(outputDir, [{
101
- session: session({
102
- path: source,
103
- sizeBytes: Number(sourceStat.size),
104
- mtimeNs: sourceStat.mtimeNs.toString(),
105
- }),
106
- sample,
107
- }], profile, profileHash);
108
- return { outputDir, bundleDir: join(outputDir, "input-bundle"), descriptor };
109
- }
110
-
111
- describe("pivot-distill harness normalization", () => {
112
- it("reports Gemini and Antigravity as one AGY endpoint", () => {
113
- expect(harnessFamily("claude")).toBe("claude-code");
114
- expect(harnessFamily("codex")).toBe("codex");
115
- expect(harnessFamily("kimi")).toBe("kimi");
116
- expect(harnessFamily("gemini")).toBe("agy");
117
- expect(harnessFamily("antigravity")).toBe("agy");
118
- expect(harnessFamily("grok")).toBeNull();
119
- });
120
-
121
- it("selects the parser from raw harness and file shape", () => {
122
- expect(parserKindFor("gemini", "/x/session.json")).toBe("gemini-json");
123
- expect(parserKindFor("gemini", "/x/transcript_full.jsonl")).toBe("agy-converted");
124
- expect(parserKindFor("antigravity", "/x/transcript.jsonl")).toBe("antigravity-jsonl");
125
- expect(parserKindFor("kimi", "/x/wire.jsonl")).toBe("kimi-wire");
126
- });
127
-
128
- it("extracts the raw UUID from Codex rollout identity", () => {
129
- expect(stableSessionId(
130
- "2026-08-02T23-13-07-019fc309-3111-7d53-b968-d7269eee3959",
131
- "/x/rollout-2026-08-02T23-13-07-019fc309-3111-7d53-b968-d7269eee3959.jsonl",
132
- )).toBe("019fc309-3111-7d53-b968-d7269eee3959");
133
- });
134
-
135
- it("keeps distinct AGY brain session IDs instead of collapsing them to logs", () => {
136
- expect(agySessionIdFromPath("/x/brain/session-a/.system_generated/logs/transcript_full.jsonl")).toBe("session-a");
137
- expect(agySessionIdFromPath("/x/brain/session-b/.system_generated/logs/transcript_full.jsonl")).toBe("session-b");
138
- });
139
-
140
- it("does not accept an unresolved Claude subagent path as a main session", () => {
141
- expect(resolveClaudeSessionPath({
142
- id: "main-session",
143
- harness: "claude",
144
- path: "/definitely-missing/subagents/agent-worker.jsonl",
145
- })).toBeNull();
146
- expect(resolveClaudeSessionPath({
147
- id: "main-session",
148
- harness: "claude",
149
- path: "/x/main-session.jsonl",
150
- })).toBe("/x/main-session.jsonl");
151
- });
152
-
153
- it("resolves both sibling and nested Claude agent files to the main session", () => {
154
- const projectDir = mkdtempSync(join(tmpdir(), "pivot-claude-project-"));
155
- const sessionId = "01234567-89ab-cdef-0123-456789abcdef";
156
- const mainPath = join(projectDir, `${sessionId}.jsonl`);
157
- writeFileSync(mainPath, "\n");
158
- expect(resolveClaudeSessionPath({
159
- id: sessionId,
160
- harness: "claude",
161
- path: join(projectDir, "agent-worker.jsonl"),
162
- })).toBe(mainPath);
163
- const subagentDir = join(projectDir, sessionId, "subagents");
164
- mkdirSync(subagentDir, { recursive: true });
165
- expect(resolveClaudeSessionPath({
166
- id: sessionId,
167
- harness: "claude",
168
- path: join(subagentDir, "agent-worker.jsonl"),
169
- })).toBe(mainPath);
170
- });
171
- });
172
-
173
- describe("pivot-distill transcript adapters", () => {
174
- it("parses Claude user/assistant text and rejects injected context", () => {
175
- expect(extractTurnsFromRecord({
176
- type: "user",
177
- message: { content: [{ type: "text", text: "我决定保留这个规则" }] },
178
- timestamp: "2026-08-02T00:00:00Z",
179
- }, "claude-jsonl")).toEqual([{
180
- role: "user",
181
- text: "我决定保留这个规则",
182
- timestamp: "2026-08-02T00:00:00Z",
183
- }]);
184
- expect(extractTurnsFromRecord({
185
- type: "user",
186
- message: { content: "<system-reminder>ignore previous</system-reminder>" },
187
- }, "claude-jsonl")).toEqual([]);
188
- expect(extractTurnsFromRecord({
189
- type: "user",
190
- message: { content: [{ type: "tool_result", content: "secret" }] },
191
- }, "claude-jsonl")).toEqual([]);
192
- });
193
-
194
- it("parses Codex response_item messages without tool output", () => {
195
- expect(extractTurnsFromRecord({
196
- type: "response_item",
197
- payload: {
198
- type: "message",
199
- role: "user",
200
- content: [{ type: "input_text", text: "这条路不要再走" }],
201
- },
202
- }, "codex-rollout")).toEqual([{ role: "user", text: "这条路不要再走", timestamp: undefined }]);
203
- expect(extractTurnsFromRecord({
204
- type: "response_item",
205
- payload: { type: "function_call_output", output: "ignored" },
206
- }, "codex-rollout")).toEqual([]);
207
- expect(extractTurnsFromRecord({
208
- type: "event_msg",
209
- payload: { type: "user_message", message: "这是 event_msg 里的真实用户输入" },
210
- }, "codex-rollout")).toEqual([{
211
- role: "user",
212
- text: "这是 event_msg 里的真实用户输入",
213
- timestamp: undefined,
214
- }]);
215
- });
216
-
217
- it("parses Kimi user messages and streamed assistant text", () => {
218
- expect(extractTurnsFromRecord({
219
- type: "context.append_message",
220
- message: {
221
- role: "user",
222
- origin: { kind: "user" },
223
- content: [{ type: "text", text: "以后统一用这个" }],
224
- },
225
- }, "kimi-wire")).toEqual([{ role: "user", text: "以后统一用这个", timestamp: undefined }]);
226
- expect(extractTurnsFromRecord({
227
- type: "turn.prompt",
228
- origin: { kind: "user" },
229
- input: [{ type: "text", text: "这是 turn.prompt" }],
230
- time: 1_700_000_000_000,
231
- }, "kimi-wire")).toEqual([{
232
- role: "user",
233
- text: "这是 turn.prompt",
234
- timestamp: "2023-11-14T22:13:20.000Z",
235
- }]);
236
- expect(extractTurnsFromRecord({
237
- type: "context.append_message",
238
- message: {
239
- role: "user",
240
- origin: { kind: "skill_activation" },
241
- content: [{ type: "text", text: "这是系统注入" }],
242
- },
243
- }, "kimi-wire")).toEqual([]);
244
- expect(extractTurnsFromRecord({
245
- type: "context.append_loop_event",
246
- event: { type: "content.part", part: { type: "text", text: "收到" } },
247
- }, "kimi-wire")).toEqual([{ role: "assistant", text: "收到", timestamp: undefined }]);
248
- });
249
-
250
- it("parses Antigravity and Gemini records into the AGY conversation", () => {
251
- expect(extractTurnsFromRecord({
252
- type: "USER_INPUT",
253
- content: "<USER_REQUEST>这是我的规则</USER_REQUEST><ADDITIONAL_METADATA>系统数据</ADDITIONAL_METADATA>",
254
- created_at: "2026-08-01T00:00:00Z",
255
- }, "antigravity-jsonl")).toEqual([{
256
- role: "user",
257
- text: "这是我的规则",
258
- timestamp: "2026-08-01T00:00:00Z",
259
- }]);
260
- expect(geminiJsonTurns(JSON.stringify({
261
- messages: [
262
- { type: "user", timestamp: "t1", content: [{ text: "不要自动删除" }] },
263
- { type: "gemini", timestamp: "t2", content: "明白" },
264
- ],
265
- }))).toEqual([
266
- { role: "user", text: "不要自动删除", timestamp: "t1" },
267
- { role: "assistant", text: "明白", timestamp: "t2" },
268
- ]);
269
- expect(extractAgyUserText(
270
- "保留正文<ADDITIONAL_METADATA>系统数据</ADDITIONAL_METADATA><USER_SETTINGS_CHANGE>设置</USER_SETTINGS_CHANGE>",
271
- )).toBe("保留正文");
272
- expect(extractTurnsFromRecord({
273
- type: "user",
274
- message: { role: "user", content: "转换后的用户内容" },
275
- }, "agy-converted")).toEqual([{
276
- role: "user",
277
- text: "转换后的用户内容",
278
- timestamp: undefined,
279
- }]);
280
- expect(extractTurnsFromRecord({
281
- type: "assistant",
282
- message: { role: "assistant", content: "旧 SQLite 转换后的回答" },
283
- }, "antigravity-jsonl")).toEqual([{
284
- role: "assistant",
285
- text: "旧 SQLite 转换后的回答",
286
- timestamp: undefined,
287
- }]);
288
- });
289
-
290
- it("coalesces streamed assistant parts but keeps user turns separate", () => {
291
- expect(coalesceTurns([
292
- { role: "assistant", text: "前半" },
293
- { role: "assistant", text: "后半" },
294
- { role: "user", text: "下一问" },
295
- ])).toEqual([
296
- { role: "assistant", text: "前半\n后半" },
297
- { role: "user", text: "下一问" },
298
- ]);
299
- });
300
- });
301
-
302
- describe("pivot-distill sampling and validation", () => {
303
- it("covers the beginning, middle, and end of a long session", () => {
304
- const turns = Array.from({ length: 9 }, (_, index) => ({
305
- role: index % 2 === 0 ? "user" as const : "assistant" as const,
306
- text: `turn-${index}-${"x".repeat(90)}`,
307
- }));
308
- const sample = buildStratifiedSample(turns, 400);
309
- expect(sample.text.length).toBeLessThanOrEqual(400);
310
- expect(sample.text).toContain("turn-0-");
311
- expect(sample.text).toContain("turn-4-");
312
- expect(sample.text).toContain("turn-8-");
313
- expect(sample.sampledTurns).toBe(5);
314
- expect(sample.sampledExchanges).toBe(3);
315
- });
316
-
317
- it("redacts a secret before the per-turn cut", () => {
318
- const sample = buildStratifiedSample([{
319
- role: "user",
320
- text: `${"x".repeat(880)} sk-ant-api03-SECRETSECRETSECRET tail`,
321
- }], 2_000);
322
- expect(sample.text).toContain("[REDACTED:anthropic");
323
- expect(sample.text).not.toContain("sk-ant-");
324
- });
325
-
326
- it("keeps complete exchanges verbatim when the 24K-style cap is not reached", () => {
327
- const turns = [
328
- { role: "assistant" as const, text: "开头的 assistant-only 说明" },
329
- { role: "user" as const, text: "连续用户一" },
330
- { role: "user" as const, text: "连续用户二" },
331
- { role: "assistant" as const, text: "回答二之一" },
332
- { role: "assistant" as const, text: "回答二之二" },
333
- { role: "user" as const, text: "结尾尚未回答" },
334
- ];
335
- const sample = buildStratifiedSample(turns, 2_000);
336
- expect(sample.originalTurns).toBe(turns.length);
337
- expect(sample.sampledTurns).toBe(turns.length);
338
- expect(sample.exchanges).toBe(4);
339
- expect(sample.sampledExchanges).toBe(4);
340
- expect(sample.omittedTurns).toBe(0);
341
- expect(sample.text).toContain("助手:回答二之一\n助手:回答二之二");
342
- expect(sample.text).toEndWith("用户:结尾尚未回答");
343
- });
344
-
345
- it("preserves head, centered middle, and tail evidence in long user and assistant turns", () => {
346
- const user = `USER_HEAD_${"u".repeat(180)}_USER_MIDDLE_${"v".repeat(180)}_USER_TAIL`;
347
- const assistant = `ASSIST_HEAD_${"a".repeat(180)}_ASSIST_MIDDLE_${"b".repeat(180)}_ASSIST_TAIL`;
348
- const sample = buildStratifiedSample([
349
- { role: "user", text: user },
350
- { role: "assistant", text: assistant },
351
- ], 340);
352
- expect(sample.text.length).toBeLessThanOrEqual(340);
353
- expect(sample.text).toContain("USER_HEAD_");
354
- expect(sample.text).toContain("USER_MIDDLE_");
355
- expect(sample.text).toContain("USER_TAIL");
356
- expect(sample.text).toContain("ASSIST_HEAD_");
357
- expect(sample.text).toContain("ASSIST_MIDDLE_");
358
- expect(sample.text).toContain("ASSIST_TAIL");
359
- expect(sample.text).toContain(OMISSION_MARKER);
360
- expect(sample.roles.user.truncatedTurns).toBe(1);
361
- expect(sample.roles.assistant.truncatedTurns).toBe(1);
362
- expect(sample.coverageRatio).toBeGreaterThan(0);
363
- expect(sample.coverageRatio).toBeLessThan(1);
364
- });
365
-
366
- it("handles one-role sessions, multiple long turns, boundary rounding, and deterministic output", () => {
367
- const turns = Array.from({ length: 6 }, (_, index) => ({
368
- role: "user" as const,
369
- text: `USER-${index}-HEAD-${"x".repeat(180)}-MID-${index}-${"y".repeat(180)}-TAIL-${index}`,
370
- }));
371
- const first = buildStratifiedSample(turns, 420);
372
- const second = buildStratifiedSample(turns, 420);
373
- expect(second).toEqual(first);
374
- expect(first.text.length).toBeLessThanOrEqual(420);
375
- expect(first.roles.assistant.turns).toBe(0);
376
- expect(first.text).toContain("USER-0-HEAD");
377
- expect(first.text).toContain("TAIL-5");
378
- expect(first.sampledExchanges).toBeLessThan(turns.length);
379
- });
380
-
381
- it("rejects a cap too small for even one minimum exchange", () => {
382
- expect(() => buildStratifiedSample([{ role: "user", text: "x".repeat(200) }], 20))
383
- .toThrow("too small to preserve the required first/last exchanges");
384
- });
385
-
386
- it("redacts common transcript credentials and personal contacts before external LLM use", () => {
387
- const values = [
388
- { text: `DATABASE_URL=postgres://user:${"p".repeat(16)}@db.invalid/app`, secret: "p".repeat(16) },
389
- { text: `Authorization: Basic ${"Q".repeat(24)}`, secret: "Q".repeat(24) },
390
- { text: `Cookie: session=${"c".repeat(24)}`, secret: "c".repeat(24) },
391
- {
392
- text: `token=${"a".repeat(8)}.${"b".repeat(8)}.${"c".repeat(8)}`,
393
- secret: `${"a".repeat(8)}.${"b".repeat(8)}.${"c".repeat(8)}`,
394
- },
395
- { text: "person@example.invalid", secret: "person@example.invalid" },
396
- { text: "13812345678", secret: "13812345678" },
397
- ];
398
- for (const value of values) {
399
- const result = redactForExternalModel(value.text);
400
- expect(result.redacted).toBeGreaterThan(0);
401
- expect(result.text).not.toContain(value.secret);
402
- }
403
- });
404
-
405
- it("accepts grounded candidates and constructs controlled metadata", () => {
406
- const sample = buildStratifiedSample([
407
- { role: "user", text: "这条路以后不要再走" },
408
- { role: "assistant", text: "已记录原因" },
409
- ], 500);
410
- const result = validateJudgeResponse(JSON.stringify({
411
- hasPivot: true,
412
- candidates: [{
413
- kind: "decision",
414
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
415
- anchor: "这条路以后不要再走",
416
- key: "拒绝双真相源",
417
- evidence: ["已记录原因"],
418
- }],
419
- }), sample, session());
420
- expect(result.hasPivot).toBeTrue();
421
- expect(result.candidates[0].canonicalKey).toBe("pivot-decision-拒绝双真相源");
422
- expect(result.candidates[0].proposedScope).toBe("memory:pivot");
423
- expect(result.candidates[0].proposedCategory).toBe("events");
424
- expect(result.candidates[0].tags).toEqual([
425
- "src:019fc307",
426
- "date:2026-08-02",
427
- "harness:codex",
428
- "raw:codex",
429
- ]);
430
- });
431
-
432
- it("redactedResidue ignores placeholder shapes but catches real residue", () => {
433
- // A placeholder's own word shape must not re-match the assignment rule.
434
- expect(redactedResidue("session_id='[REDACTED:sensitive_assignment]'")).toBe(0);
435
- // Placeholder junction with surviving context must not re-match email/uri.
436
- expect(redactedResidue("clone https://[REDACTED:uri_credentials]@gitlab.com/user/repo.git")).toBe(0);
437
- // Genuine residue outside placeholders still counts.
438
- expect(redactedResidue("[REDACTED:email] and mail me at someone@example.com")).toBeGreaterThan(0);
439
- });
440
-
441
- it("grounds anchors and evidence that copy the rendered role labels", () => {
442
- const sample = buildStratifiedSample([
443
- { role: "user", text: "这条路以后不要再走" },
444
- { role: "assistant", text: "已记录原因" },
445
- ], 500);
446
- const result = validateJudgeResponse(JSON.stringify({
447
- hasPivot: true,
448
- candidates: [{
449
- kind: "decision",
450
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
451
- anchor: "用户:这条路以后不要再走",
452
- key: "拒绝双真相源",
453
- evidence: ["助手:已记录原因"],
454
- }],
455
- }), sample, session());
456
- expect(result.hasPivot).toBeTrue();
457
- expect(result.candidates[0].anchor).toBe("这条路以后不要再走");
458
- expect(result.candidates[0].evidence).toEqual(["已记录原因"]);
459
- });
460
-
461
- it("rejects evidence that is only a bare role label", () => {
462
- expect(() => validateJudgeResponse(JSON.stringify({
463
- hasPivot: true,
464
- candidates: [{
465
- kind: "decision",
466
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
467
- anchor: "这条路以后不要再走",
468
- key: "拒绝双真相源",
469
- evidence: ["助手:"],
470
- }],
471
- }), buildStratifiedSample([
472
- { role: "user", text: "这条路以后不要再走" },
473
- { role: "assistant", text: "已记录原因" },
474
- ], 500), session())).toThrow("substantive");
475
- });
476
-
477
- it("keeps verbatim evidence and drops a paraphrased quote without voiding the candidate", () => {
478
- const sample = buildStratifiedSample([
479
- { role: "user", text: "这条路以后不要再走" },
480
- { role: "assistant", text: "已记录原因" },
481
- ], 500);
482
- const result = validateJudgeResponse(JSON.stringify({
483
- hasPivot: true,
484
- candidates: [{
485
- kind: "decision",
486
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
487
- anchor: "这条路以后不要再走",
488
- key: "拒绝双真相源",
489
- evidence: ["已记录原因", "助手当时总结说旧路线会造成双真相源问题"],
490
- }],
491
- }), sample, session());
492
- expect(result.candidates[0].evidence).toEqual(["已记录原因"]);
493
- });
494
-
495
- it("drops a candidate whose stored form degrades below read-back rules", () => {
496
- // "助手:好" passes raw substantive checks but stores as a single char
497
- // after label stripping — the stored-form gate must reject it, and with
498
- // no surviving candidate the response fails as a whole.
499
- expect(() => validateJudgeResponse(JSON.stringify({
500
- hasPivot: true,
501
- candidates: [{
502
- kind: "decision",
503
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
504
- anchor: "这条路以后不要再走",
505
- key: "拒绝双真相源",
506
- evidence: ["助手:好"],
507
- }],
508
- }), buildStratifiedSample([
509
- { role: "user", text: "这条路以后不要再走" },
510
- { role: "assistant", text: "好" },
511
- ], 500), session())).toThrow("requires at least one valid candidate");
512
- });
513
-
514
- it("rejects a candidate when no evidence survives grounding", () => {
515
- expect(() => validateJudgeResponse(JSON.stringify({
516
- hasPivot: true,
517
- candidates: [{
518
- kind: "decision",
519
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
520
- anchor: "这条路以后不要再走",
521
- key: "拒绝双真相源",
522
- evidence: ["助手当时总结说旧路线会造成双真相源问题"],
523
- }],
524
- }), buildStratifiedSample([
525
- { role: "user", text: "这条路以后不要再走" },
526
- { role: "assistant", text: "已记录原因" },
527
- ], 500), session())).toThrow("requires at least one grounded");
528
- });
529
-
530
- it("rejects a case that drops below two grounded evidence quotes", () => {
531
- expect(() => validateJudgeResponse(JSON.stringify({
532
- hasPivot: true,
533
- candidates: [{
534
- kind: "case",
535
- text: "记录了一次问题的解决过程,包含问题定位与验证后的修复方案说明。",
536
- anchor: "这条路以后不要再走",
537
- key: "示例案例",
538
- evidence: ["已记录原因", "这句在输入里根本不存在的概括描述"],
539
- }],
540
- }), buildStratifiedSample([
541
- { role: "user", text: "这条路以后不要再走" },
542
- { role: "assistant", text: "已记录原因" },
543
- ], 500), session())).toThrow("requires at least two grounded");
544
- });
545
-
546
- it("keeps valid candidates when a sibling candidate fails validation", () => {
547
- const sample = buildStratifiedSample([
548
- { role: "user", text: "这条路以后不要再走" },
549
- { role: "assistant", text: "已记录原因" },
550
- ], 500);
551
- const result = validateJudgeResponse(JSON.stringify({
552
- hasPivot: true,
553
- candidates: [
554
- {
555
- kind: "decision",
556
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
557
- anchor: "这条路以后不要再走",
558
- key: "拒绝双真相源",
559
- evidence: ["已记录原因"],
560
- },
561
- {
562
- kind: "case",
563
- text: "记录了一次问题的解决过程,包含问题定位与验证后的修复方案说明。",
564
- anchor: "输入里不存在的锚点句子",
565
- key: "不合格案例",
566
- evidence: ["已记录原因", "这句也不存在于输入之中"],
567
- },
568
- ],
569
- }), sample, session());
570
- expect(result.candidates.length).toBe(1);
571
- expect(result.candidates[0].canonicalKey).toBe("pivot-decision-拒绝双真相源");
572
- });
573
-
574
- it("still fails when no candidate survives validation", () => {
575
- expect(() => validateJudgeResponse(JSON.stringify({
576
- hasPivot: true,
577
- candidates: [{
578
- kind: "decision",
579
- text: "Alice 决定不再采用旧路线,因为它会让同一数据形成两个真相源。",
580
- anchor: "输入里不存在的锚点句子",
581
- key: "拒绝双真相源",
582
- evidence: ["已记录原因"],
583
- }],
584
- }), buildStratifiedSample([
585
- { role: "user", text: "这条路以后不要再走" },
586
- { role: "assistant", text: "已记录原因" },
587
- ], 500), session())).toThrow("requires at least one valid candidate");
588
- });
589
-
590
- it("rejects hallucinated anchors and inconsistent empty verdicts", () => {
591
- expect(() => validateJudgeResponse(JSON.stringify({
592
- hasPivot: true,
593
- candidates: [{
594
- kind: "preference_rule",
595
- text: "Alice 明确要求所有报告都保留证据出处,方便之后重新核对。",
596
- anchor: "输入里并不存在的原话",
597
- key: "保留证据",
598
- }],
599
- }), buildStratifiedSample([{ role: "user", text: "请保留出处" }], 500), session())).toThrow("not grounded");
600
- expect(() => validateJudgeResponse(
601
- JSON.stringify({ hasPivot: true, candidates: [] }),
602
- buildStratifiedSample([{ role: "user", text: "请保留出处" }], 500),
603
- session(),
604
- )).toThrow("requires at least one");
605
- });
606
-
607
- it("enforces evidence counts by pivot kind and forbids quoting omission markers", () => {
608
- const base = {
609
- text: "Alice 明确改变了既有判断,并给出了可以回查的真实原因与结果。",
610
- anchor: "我改主意了",
611
- key: "改变判断",
612
- };
613
- const sample = buildStratifiedSample([
614
- { role: "user", text: "我改主意了" },
615
- { role: "assistant", text: "旧判断失效" },
616
- { role: "assistant", text: "新方案已经验证" },
617
- ], 500);
618
- expect(() => validateJudgeResponse(JSON.stringify({
619
- hasPivot: true,
620
- candidates: [{ kind: "judgment_shift", ...base, evidence: [] }],
621
- }), sample, session())).toThrow("at least one");
622
- expect(() => validateJudgeResponse(JSON.stringify({
623
- hasPivot: true,
624
- candidates: [{ kind: "case", ...base, evidence: ["旧判断失效"] }],
625
- }), sample, session())).toThrow("at least two");
626
- expect(() => validateJudgeResponse(JSON.stringify({
627
- hasPivot: true,
628
- candidates: [{ kind: "preference_rule", ...base, anchor: `我改${OMISSION_MARKER}主意了`, evidence: [] }],
629
- }), buildStratifiedSample([{ role: "user", text: `我改${OMISSION_MARKER}主意了` }], 500), session()))
630
- .toThrow("omission marker");
631
- });
632
-
633
- it("requires distinct substantive evidence grounded within individual turns", () => {
634
- const sample = buildStratifiedSample([
635
- { role: "user", text: "部署时出现连接失败" },
636
- { role: "assistant", text: "先修配置文件" },
637
- { role: "assistant", text: "之后验证服务恢复" },
638
- ], 500);
639
- const candidate = {
640
- kind: "case",
641
- text: "部署连接失败的问题通过修正配置并验证服务恢复得到了解决。",
642
- anchor: "部署时出现连接失败",
643
- key: "部署连接修复",
644
- };
645
- expect(() => validateJudgeResponse(JSON.stringify({
646
- hasPivot: true,
647
- candidates: [{ ...candidate, evidence: ["助手:", "助手:"] }],
648
- }), sample, session())).toThrow(/distinct|substantive/);
649
- expect(() => validateJudgeResponse(JSON.stringify({
650
- hasPivot: true,
651
- candidates: [{ ...candidate, evidence: ["连接失败先修配置文件", "之后验证服务恢复"] }],
652
- }), sample, session())).toThrow("requires at least two grounded");
653
- });
654
-
655
- it("rejects an anchor that appears only in an assistant turn", () => {
656
- expect(() => validateJudgeResponse(JSON.stringify({
657
- hasPivot: true,
658
- candidates: [{
659
- kind: "decision",
660
- text: "Alice 明确决定采用这个方案,并说明了此前方案不再继续使用。",
661
- anchor: "这句话只由助手说过",
662
- key: "助手冒充用户锚点",
663
- }],
664
- }), buildStratifiedSample([
665
- { role: "user", text: "另一句话" },
666
- { role: "assistant", text: "这句话只由助手说过" },
667
- ], 500), session())).toThrow("user turn");
668
- });
669
-
670
- it("rejects sensitive material in a model-proposed canonical key", () => {
671
- expect(() => validateJudgeResponse(JSON.stringify({
672
- hasPivot: true,
673
- candidates: [{
674
- kind: "decision",
675
- text: "Alice 明确决定采用这个方案,并说明了此前方案不再继续使用。",
676
- anchor: "采用这个方案",
677
- key: `token=${"s".repeat(24)}`,
678
- evidence: ["采用这个方案"],
679
- }],
680
- }), buildStratifiedSample([{ role: "user", text: "采用这个方案" }], 500), session()))
681
- .toThrow("sensitive material");
682
- });
683
-
684
- it("round-trips all four validated pivot kinds through strict stored-result checks", () => {
685
- const fixture = createBundleFixture();
686
- const loaded = loadFrozenBundle(
687
- fixture.bundleDir,
688
- fixture.descriptor.bundleHash,
689
- fixture.descriptor.requestProfileHash,
690
- );
691
- const entry = loaded.entries[0];
692
- const selectionHash = selectionIdentityHash("transport", [entry]);
693
- const evidenceByKind = {
694
- judgment_shift: ["已记录这条规则"],
695
- decision: ["已记录这条规则"],
696
- preference_rule: [],
697
- case: ["以后必须保留可回查证据", "已记录这条规则"],
698
- } as const;
699
- for (const kind of ["judgment_shift", "decision", "preference_rule", "case"] as const) {
700
- const judged = validateJudgeResponse(JSON.stringify({
701
- hasPivot: true,
702
- candidates: [{
703
- kind,
704
- text: "用户明确要求以后必须保留可回查证据,最终结果需要能够追溯到真实出处。",
705
- anchor: "以后必须保留可回查证据",
706
- key: `round-trip-${kind}`,
707
- evidence: evidenceByKind[kind],
708
- }],
709
- }), entry.sample, entry.session);
710
- const storedResult = {
711
- schemaVersion: 1,
712
- mode: "transport",
713
- selectionHash,
714
- outboundSampleSha256: entry.sampleSha256,
715
- promptVersion: PROMPT_VERSION,
716
- pipelineVersion: PIPELINE_VERSION,
717
- model: PIVOT_MODEL,
718
- requestProfileHash: fixture.descriptor.requestProfileHash,
719
- bundleHash: fixture.descriptor.bundleHash,
720
- session: entry.session,
721
- status: "ok",
722
- hasPivot: true,
723
- candidates: judged.candidates,
724
- sample: entry.sample.metrics,
725
- responseChars: 100,
726
- attempts: 1,
727
- responseModel: PIVOT_MODEL,
728
- finishReason: "stop",
729
- completedAt: "2026-08-03T00:00:00.000Z",
730
- };
731
- expect(storedResultMatches(
732
- storedResult, entry, "transport", selectionHash, PIVOT_MODEL, fixture.descriptor.bundleHash,
733
- fixture.descriptor.requestProfileHash, 3,
734
- )).toBeTrue();
735
- if (kind === "decision") {
736
- expect(storedResultMatches(
737
- { ...storedResult, session: { ...entry.session, date: "2026-08-04" } },
738
- entry, "transport", selectionHash, PIVOT_MODEL, fixture.descriptor.bundleHash,
739
- fixture.descriptor.requestProfileHash, 3,
740
- )).toBeFalse();
741
- expect(storedResultMatches(
742
- {
743
- ...storedResult,
744
- candidates: [{ ...judged.candidates[0], unexpected: "must-not-pass-through" }],
745
- },
746
- entry, "transport", selectionHash, PIVOT_MODEL, fixture.descriptor.bundleHash,
747
- fixture.descriptor.requestProfileHash, 3,
748
- )).toBeFalse();
749
- }
750
- }
751
- });
752
- });
753
-
754
- describe("pivot-distill request identity and frozen bundle", () => {
755
- it("builds a credential-free fixed-model request profile whose hash covers every request choice", () => {
756
- const endpoint = safeEndpoint("https://user:password@example.invalid//compatible-mode/v1/?token=secret#fragment");
757
- expect(endpoint).toBe("https://example.invalid/compatible-mode/v1");
758
- expect(endpoint).not.toContain("password");
759
- expect(endpoint).not.toContain("token=");
760
- const profile = buildPivotRequestProfile(
761
- "https://user:password@example.invalid/compatible-mode/v1?token=secret#fragment",
762
- 24_000,
763
- );
764
- expect(profile.model).toBe(PIVOT_MODEL);
765
- expect(profile.enableThinking).toBeFalse();
766
- expect(profile.responseFormat).toEqual({ type: "json_object" });
767
- expect(profile.tokenLimit).toEqual({ parameter: "max_tokens", value: 1_800 });
768
- expect(profile.retryPolicy).toEqual({
769
- maxSessionAttempts: 3,
770
- sdkMaxRetries: 0,
771
- backoffMs: [250, 500],
772
- maxRetryAfterMs: 5_000,
773
- retryableHttpStatuses: [408, 409, 429],
774
- retryableServerErrorRange: "500-599",
775
- });
776
- expect(profile.promptHashes.system).toHaveLength(64);
777
- expect(profile.promptHashes.userTemplate).toHaveLength(64);
778
- expect(profile.promptHashes.jsonSchema).toHaveLength(64);
779
- const identity = pivotRequestProfileHash(profile);
780
- expect(pivotRequestProfileHash({ ...profile, temperature: 0.2 })).not.toBe(identity);
781
- expect(pivotRequestProfileHash({
782
- ...profile,
783
- sampling: { ...profile.sampling, omissionMarker: "different" },
784
- })).not.toBe(identity);
785
- expect(pivotRequestProfileHash({
786
- ...profile,
787
- retryPolicy: { ...profile.retryPolicy, sdkMaxRetries: 1 },
788
- })).not.toBe(identity);
789
- expect(buildPivotLlmConfig({
790
- apiKey: "test-key",
791
- model: "qwen-turbo",
792
- baseURL: "https://user:secret@example.invalid/v1?token=hidden#fragment",
793
- }, profile, 12_345)).toMatchObject({
794
- baseURL: "https://example.invalid/compatible-mode/v1",
795
- model: PIVOT_MODEL,
796
- maxRetries: 0,
797
- timeoutMs: 12_345,
798
- });
799
- });
800
-
801
- it("keeps retry cost bounded and aggregates usage from every billed attempt", () => {
802
- const policy = buildPivotRequestProfile("https://example.invalid/v1").retryPolicy;
803
- expect(retryDecision({ status: 429, headers: { "retry-after": "0.01" } }, 1, policy))
804
- .toEqual({ retry: true, delayMs: 10 });
805
- expect(retryDecision({ status: 500 }, 2, policy)).toEqual({ retry: true, delayMs: 500 });
806
- expect(retryDecision({ status: 401 }, 1, policy)).toEqual({ retry: false, delayMs: 0 });
807
- expect(retryDecision({ status: 429 }, 3, policy)).toEqual({ retry: false, delayMs: 0 });
808
- expect(aggregateDetailedUsage([
809
- { promptTokens: 10, completionTokens: 2, totalTokens: 12 },
810
- { promptTokens: 11, completionTokens: 3, totalTokens: 14, cachedPromptTokens: 4 },
811
- ])).toEqual({
812
- promptTokens: 21,
813
- completionTokens: 5,
814
- totalTokens: 26,
815
- cachedPromptTokens: 4,
816
- });
817
- });
818
-
819
- it("makes the external stage and exact outbound sample set part of report identity", () => {
820
- const fixture = createBundleFixture();
821
- const loaded = loadFrozenBundle(
822
- fixture.bundleDir, fixture.descriptor.bundleHash, fixture.descriptor.requestProfileHash,
823
- );
824
- expect(selectionIdentityHash("transport", loaded.entries))
825
- .not.toBe(selectionIdentityHash("regression", loaded.entries));
826
- const changed = structuredClone(loaded.entries);
827
- changed[0].sampleSha256 = "f".repeat(64);
828
- expect(selectionIdentityHash("transport", changed))
829
- .not.toBe(selectionIdentityHash("transport", loaded.entries));
830
- });
831
-
832
- it("writes 0700/0600 bundles and reloads only exact expected identities", () => {
833
- const fixture = createBundleFixture();
834
- expect(statSync(fixture.bundleDir).mode & 0o777).toBe(0o700);
835
- expect(statSync(join(fixture.bundleDir, "bundle.json")).mode & 0o777).toBe(0o600);
836
- const loaded = loadFrozenBundle(
837
- fixture.bundleDir,
838
- fixture.descriptor.bundleHash,
839
- fixture.descriptor.requestProfileHash,
840
- );
841
- expect(loaded.entries).toHaveLength(1);
842
- expect(loaded.entries[0].status).toBe("eligible");
843
- expect(frozenSampleHash(loaded.entries[0].sample)).toBe(loaded.entries[0].sampleSha256);
844
- expect(() => loadFrozenBundle(
845
- fixture.bundleDir,
846
- "wrong-bundle",
847
- fixture.descriptor.requestProfileHash,
848
- )).toThrow("unexpected bundleHash");
849
- expect(() => loadFrozenBundle(
850
- fixture.bundleDir,
851
- fixture.descriptor.bundleHash,
852
- "wrong-profile",
853
- )).toThrow("unexpected requestProfileHash");
854
- });
855
-
856
- it("rejects mutated samples, duplicate session keys, and paths outside the bundle root", () => {
857
- const mutated = createBundleFixture();
858
- const manifestEntry = JSON.parse(readFileSync(join(mutated.bundleDir, "manifest.jsonl"), "utf8")) as {
859
- sampleFile: string;
860
- };
861
- const samplePath = join(mutated.bundleDir, manifestEntry.sampleFile);
862
- const sample = JSON.parse(readFileSync(samplePath, "utf8")) as Record<string, unknown>;
863
- sample.text = `X${String(sample.text).slice(1)}`;
864
- writeFileSync(samplePath, `${JSON.stringify(sample)}\n`);
865
- expect(() => loadFrozenBundle(
866
- mutated.bundleDir,
867
- mutated.descriptor.bundleHash,
868
- mutated.descriptor.requestProfileHash,
869
- )).toThrow("sample grounding projection mismatch");
870
-
871
- const mutatedMetrics = createBundleFixture();
872
- const metricsEntry = JSON.parse(
873
- readFileSync(join(mutatedMetrics.bundleDir, "manifest.jsonl"), "utf8"),
874
- ) as { sampleFile: string };
875
- const metricsPath = join(mutatedMetrics.bundleDir, metricsEntry.sampleFile);
876
- const metricsSample = JSON.parse(readFileSync(metricsPath, "utf8")) as {
877
- metrics: { redactions: number };
878
- };
879
- metricsSample.metrics.redactions += 1;
880
- writeFileSync(metricsPath, `${JSON.stringify(metricsSample)}\n`);
881
- expect(() => loadFrozenBundle(
882
- mutatedMetrics.bundleDir,
883
- mutatedMetrics.descriptor.bundleHash,
884
- mutatedMetrics.descriptor.requestProfileHash,
885
- )).toThrow("sample hash mismatch");
886
-
887
- const duplicate = createBundleFixture();
888
- const originalLine = readFileSync(join(duplicate.bundleDir, "manifest.jsonl"), "utf8").trim();
889
- const duplicatedManifest = `${originalLine}\n${originalLine}\n`;
890
- const descriptor = JSON.parse(readFileSync(join(duplicate.bundleDir, "bundle.json"), "utf8")) as FrozenBundleDescriptor;
891
- descriptor.manifestSha256 = sha256(duplicatedManifest);
892
- descriptor.sessions = 2;
893
- descriptor.eligibleSessions = 2;
894
- descriptor.deferredSessions = 0;
895
- descriptor.bundleHash = bundleIdentityHashForTest({
896
- requestProfileHash: descriptor.requestProfileHash,
897
- manifestSha256: descriptor.manifestSha256,
898
- sessions: descriptor.sessions,
899
- eligibleSessions: descriptor.eligibleSessions,
900
- deferredSessions: descriptor.deferredSessions,
901
- });
902
- writeFileSync(join(duplicate.bundleDir, "manifest.jsonl"), duplicatedManifest);
903
- writeFileSync(join(duplicate.bundleDir, "bundle.json"), `${JSON.stringify(descriptor)}\n`);
904
- expect(() => loadFrozenBundle(
905
- duplicate.bundleDir,
906
- descriptor.bundleHash,
907
- descriptor.requestProfileHash,
908
- )).toThrow("duplicate session key");
909
-
910
- expect(() => resolveBundleSamplePath(duplicate.bundleDir, "/tmp/outside.json"))
911
- .toThrow("must stay relative");
912
- expect(() => resolveBundleSamplePath(duplicate.bundleDir, "samples/../../outside.json"))
913
- .toThrow("must stay relative");
914
- });
915
-
916
- it("rejects a request profile that no longer matches the fixed prompt/body/sampling contract", () => {
917
- const fixture = createBundleFixture();
918
- const descriptor = JSON.parse(readFileSync(join(fixture.bundleDir, "bundle.json"), "utf8")) as FrozenBundleDescriptor;
919
- descriptor.requestProfile.temperature = 0.7;
920
- writeFileSync(join(fixture.bundleDir, "bundle.json"), `${JSON.stringify(descriptor)}\n`);
921
- expect(() => loadFrozenBundle(
922
- fixture.bundleDir,
923
- fixture.descriptor.bundleHash,
924
- fixture.descriptor.requestProfileHash,
925
- )).toThrow("current fixed pivot contract");
926
- });
927
-
928
- it("rejects duplicate keys before bundle creation and keeps assistant-only sessions deferred", () => {
929
- const outputDir = mkdtempSync(join(tmpdir(), "pivot-bundle-duplicate-"));
930
- const profile = buildPivotRequestProfile("https://example.invalid/v1", 500);
931
- const profileHash = pivotRequestProfileHash(profile);
932
- const userSample = buildStratifiedSample([{ role: "user", text: "保留证据" }], 500);
933
- expect(() => writeFrozenBundle(outputDir, [
934
- { session: session(), sample: userSample },
935
- { session: session(), sample: userSample },
936
- ], profile, profileHash)).toThrow("duplicate session key");
937
-
938
- const assistantOnlyDir = mkdtempSync(join(tmpdir(), "pivot-bundle-deferred-"));
939
- const assistantOnly = buildStratifiedSample([{ role: "assistant", text: "只有助手内容" }], 500);
940
- const descriptor = writeFrozenBundle(assistantOnlyDir, [{ session: session(), sample: assistantOnly }], profile, profileHash);
941
- const loaded = loadFrozenBundle(join(assistantOnlyDir, "input-bundle"), descriptor.bundleHash, descriptor.requestProfileHash);
942
- expect(loaded.entries[0].status).toBe("deferred");
943
- expect(loaded.entries[0].reason).toBe("no-parseable-user-text");
944
- });
945
- });
946
-
947
- describe("pivot-distill source enumeration and parse integrity", () => {
948
- it("filters inherited DEJA variables from both inventory subprocesses", () => {
949
- expect(filteredDejaEnvironment({ DEJA_HOME: "/wrong", DEJA_CONFIG: "/wrong/config", KEEP_ME: "yes" }))
950
- .toEqual({ KEEP_ME: "yes" });
951
- });
952
-
953
- it("enumerates four endpoint families plus Claude Desktop and both AGY origins with a fake Deja", () => {
954
- const root = mkdtempSync(join(tmpdir(), "pivot-inventory-"));
955
- const sourceRoot = join(root, "sources");
956
- mkdirSync(sourceRoot, { recursive: true });
957
- const dejaSessions = ([
958
- ["claude", "claude.jsonl"],
959
- ["codex", "codex.jsonl"],
960
- ["kimi", "kimi.jsonl"],
961
- ["gemini", "gemini.json"],
962
- ] as const).map(([harness, name], index) => {
963
- const path = join(sourceRoot, name);
964
- writeFileSync(path, harness === "gemini" ? "{\"messages\":[]}" : "{}\n");
965
- return { id: `deja-${index}-${harness}`, harness, path, project: "test" };
966
- });
967
- const codexArchiveRoot = join(root, "codex-archive");
968
- const claudeDesktopRoot = join(root, "desktop");
969
- const agyConvertedRoot = join(root, "agy", "brain", "session-one", ".system_generated", "logs");
970
- const agyArchiveRoot = join(root, "agy-archive");
971
- for (const directory of [codexArchiveRoot, claudeDesktopRoot, agyConvertedRoot, agyArchiveRoot]) {
972
- mkdirSync(directory, { recursive: true });
973
- }
974
- writeFileSync(join(codexArchiveRoot, "archive.jsonl"), "{}\n");
975
- writeFileSync(join(claudeDesktopRoot, "desktop.jsonl"), "{}\n");
976
- writeFileSync(join(agyConvertedRoot, "transcript_full.jsonl"), "{}\n");
977
- writeFileSync(join(agyArchiveRoot, "legacy.jsonl"), "{}\n");
978
- const payload = JSON.stringify({ schema_version: 2, sessions: dejaSessions });
979
- const dejaBin = join(root, "fake-deja.ts");
980
- writeFileSync(dejaBin, `#!/usr/bin/env bun
981
- if (process.env.DEJA_HOME || process.env.DEJA_CONFIG) process.exit(91);
982
- if (process.argv[2] === "version") console.log("deja 0.16.5");
983
- else console.log(${JSON.stringify(payload)});
984
- `);
985
- chmodSync(dejaBin, 0o700);
986
- const options = parseCliOptions([
987
- "--deja-bin", dejaBin,
988
- "--codex-archive-root", codexArchiveRoot,
989
- "--claude-desktop-root", claudeDesktopRoot,
990
- "--agy-converted-root", join(root, "agy"),
991
- "--agy-archive-root", agyArchiveRoot,
992
- ]);
993
- const previousDejaHome = process.env.DEJA_HOME;
994
- const previousDejaConfig = process.env.DEJA_CONFIG;
995
- process.env.DEJA_HOME = "/must-not-reach-child";
996
- process.env.DEJA_CONFIG = "/must-not-reach-child/config";
997
- try {
998
- const loaded = loadSessionInventory(options);
999
- expect(loaded.missingPaths).toBe(0);
1000
- expect(new Set(loaded.sessions.map((item) => item.harness))).toEqual(new Set(["claude-code", "codex", "kimi", "agy"]));
1001
- expect(loaded.sessions.some((item) => item.origin === "claude-desktop")).toBeTrue();
1002
- expect(loaded.sessions.some((item) => item.origin === "agy-converted")).toBeTrue();
1003
- expect(loaded.sessions.some((item) => item.origin === "agy-archive")).toBeTrue();
1004
- expect(loaded.sessions.some((item) => item.rawHarness === "gemini")).toBeTrue();
1005
- expect(inventoryCompletenessReasons(loaded.sessions, loaded.missingPaths)).toEqual([]);
1006
- expect(inventoryCompletenessReasons(
1007
- loaded.sessions.filter((item) => item.origin !== "claude-desktop"), 0,
1008
- )).toContain("origin=claude-desktop has zero sessions");
1009
- expect(inventoryCompletenessReasons(
1010
- loaded.sessions.filter((item) => item.rawHarness !== "gemini"), 0,
1011
- )).toContain("rawHarness=gemini has zero sessions");
1012
- expect(inventoryCompletenessReasons(loaded.sessions, 1)).toContain("missingPaths=1");
1013
- } finally {
1014
- if (previousDejaHome === undefined) delete process.env.DEJA_HOME;
1015
- else process.env.DEJA_HOME = previousDejaHome;
1016
- if (previousDejaConfig === undefined) delete process.env.DEJA_CONFIG;
1017
- else process.env.DEJA_CONFIG = previousDejaConfig;
1018
- }
1019
- });
1020
-
1021
- it("treats malformed JSONL as a bundle-blocking parse failure", async () => {
1022
- const root = mkdtempSync(join(tmpdir(), "pivot-malformed-"));
1023
- const path = join(root, "bad.jsonl");
1024
- writeFileSync(path, "{\"type\":\"event_msg\",\"payload\":{\"type\":\"user_message\",\"message\":\"ok\"}}\nnot-json\n");
1025
- await expect(readSessionTurns(session({ path, parserKind: "codex-rollout" })))
1026
- .rejects.toThrow("Malformed JSONL at line 2");
1027
- });
1028
- });
1029
-
1030
- describe("pivot-distill external authorization and exact selection", () => {
1031
- const common = [
1032
- "--input-bundle", "/tmp/frozen",
1033
- "--bundle-hash", "bundle-hash",
1034
- "--request-profile-hash", "profile-hash",
1035
- "--allow-external-llm",
1036
- ];
1037
-
1038
- it("requires exact transport, separate regression, and dual full authorization", () => {
1039
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1040
- "--mode", "transport", ...common,
1041
- ]))).toThrow("exactly one");
1042
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1043
- "--mode", "transport", ...common, "--session-key", "codex:one",
1044
- ]))).not.toThrow();
1045
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1046
- "--mode", "transport", ...common,
1047
- "--session-key", "codex:one",
1048
- "--output-dir", "/tmp/frozen/results",
1049
- ]))).toThrow("must not be the frozen input bundle");
1050
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1051
- "--mode", "regression", ...common, "--session-key", "codex:one",
1052
- ]))).toThrow("allow-regression-run");
1053
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1054
- "--mode", "regression", ...common, "--session-key", "codex:one", "--allow-regression-run",
1055
- ]))).not.toThrow();
1056
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1057
- "--mode", "full", ...common,
1058
- ]))).toThrow("allow-full-run");
1059
- expect(() => assertExternalModeAuthorization(parseCliOptions([
1060
- "--mode", "full", ...common, "--allow-full-run",
1061
- ]))).not.toThrow();
1062
- });
1063
-
1064
- it("rejects truncated or wrong-model responses even if their content could parse", () => {
1065
- expect(() => assertPivotResponseMetadata({ finishReason: "length", responseModel: PIVOT_MODEL }))
1066
- .toThrow("finish_reason=length");
1067
- expect(() => assertPivotResponseMetadata({ finishReason: "stop", responseModel: "rolling-alias" }))
1068
- .toThrow("response model mismatch");
1069
- expect(() => assertPivotResponseMetadata({ finishReason: "stop", responseModel: PIVOT_MODEL }))
1070
- .not.toThrow();
1071
- });
1072
-
1073
- it("refuses transport before any live inventory branch when authorization is missing", async () => {
1074
- await expect(main([
1075
- "--mode", "transport",
1076
- "--deja-bin", "/definitely/not/a/deja/binary",
1077
- "--output-dir", join(tmpdir(), "must-not-be-created-by-unauthorized-transport"),
1078
- ])).rejects.toThrow("--allow-external-llm");
1079
- });
1080
-
1081
- it("selects one exact frozen key while full selects every eligible sample", () => {
1082
- const fixture = createBundleFixture();
1083
- const loaded = loadFrozenBundle(
1084
- fixture.bundleDir,
1085
- fixture.descriptor.bundleHash,
1086
- fixture.descriptor.requestProfileHash,
1087
- );
1088
- const transport = parseCliOptions([
1089
- "--mode", "transport",
1090
- "--input-bundle", fixture.bundleDir,
1091
- "--bundle-hash", fixture.descriptor.bundleHash,
1092
- "--request-profile-hash", fixture.descriptor.requestProfileHash,
1093
- "--session-key", loaded.entries[0].session.key,
1094
- "--allow-external-llm",
1095
- ]);
1096
- expect(selectFrozenEntries(loaded, transport).map((entry) => entry.session.key))
1097
- .toEqual([loaded.entries[0].session.key]);
1098
- const full = parseCliOptions([
1099
- "--mode", "full",
1100
- "--input-bundle", fixture.bundleDir,
1101
- "--bundle-hash", fixture.descriptor.bundleHash,
1102
- "--request-profile-hash", fixture.descriptor.requestProfileHash,
1103
- "--allow-external-llm",
1104
- "--allow-full-run",
1105
- ]);
1106
- expect(selectFrozenEntries(loaded, full)).toHaveLength(fixture.descriptor.eligibleSessions);
1107
- });
1108
- });
1109
-
1110
- describe("pivot-distill resume identity and CLI", () => {
1111
- it("changes the stamp when content, prompt, or model changes", () => {
1112
- const base = {
1113
- harness: "codex" as const,
1114
- rawHarness: "codex" as const,
1115
- sessionId: "s1",
1116
- date: "2026-08-02",
1117
- sizeBytes: 100,
1118
- mtimeNs: "1",
1119
- promptVersion: "p1",
1120
- model: "m1",
1121
- };
1122
- const fingerprint = sessionFingerprint(base);
1123
- expect(sessionFingerprint({ ...base, sizeBytes: 101 })).not.toBe(fingerprint);
1124
- expect(sessionFingerprint({ ...base, promptVersion: "p2" })).not.toBe(fingerprint);
1125
- expect(sessionFingerprint({ ...base, model: "m2" })).not.toBe(fingerprint);
1126
- expect(sessionFingerprint({ ...base, rawHarness: "antigravity" })).not.toBe(fingerprint);
1127
- expect(sessionFingerprint({ ...base, date: "2026-08-03" })).not.toBe(fingerprint);
1128
- expect(sessionFingerprint({ ...base, pipelineVersion: "pipeline-v2" })).not.toBe(fingerprint);
1129
- expect(sessionFingerprint({ ...base, sampleSha256: "sample-v2" })).not.toBe(fingerprint);
1130
- expect(sessionFingerprint({ ...base, requestProfileHash: "profile-v2" })).not.toBe(fingerprint);
1131
- });
1132
-
1133
- it("excludes synthetic digest sessions without keyword-deciding pivots", () => {
1134
- expect(isSyntheticSession({ path: "/private/tmp/session-digest-a/x.jsonl", project: "x" })).toBeTrue();
1135
- expect(isSyntheticSession({ path: "/sessions/x.jsonl", project: "digest/a" })).toBeTrue();
1136
- expect(isSyntheticSession({ path: "/sessions/x.jsonl", project: "recallnest" })).toBeFalse();
1137
- });
1138
-
1139
- it("defaults to all non-synthetic sessions and parses frozen execution selectors", () => {
1140
- const defaults = parseCliOptions([]);
1141
- expect(defaults.mode).toBe("inventory");
1142
- expect(defaults.minSize).toBe(1);
1143
- const parsed = parseCliOptions([
1144
- "--mode", "estimate",
1145
- "--min-size", "200000",
1146
- "--limit", "12",
1147
- "--claude-desktop-root", "/tmp/desktop-import",
1148
- "--agy-archive-root", "/tmp/agy-archive",
1149
- ]);
1150
- expect(parsed.mode).toBe("estimate");
1151
- expect(parsed.minSize).toBe(200_000);
1152
- expect(parsed.limit).toBe(12);
1153
- expect(parsed.claudeDesktopRoot).toBe("/tmp/desktop-import");
1154
- expect(parsed.agyArchiveRoot).toBe("/tmp/agy-archive");
1155
- const transport = parseCliOptions([
1156
- "--mode", "transport",
1157
- "--input-bundle", "/tmp/bundle",
1158
- "--bundle-hash", "bundle",
1159
- "--request-profile-hash", "profile",
1160
- "--session-key", "codex:one",
1161
- "--allow-external-llm",
1162
- ]);
1163
- expect(transport.mode).toBe("transport");
1164
- expect(transport.sessionKeys).toEqual(["codex:one"]);
1165
- expect(transport.allowExternalLlm).toBeTrue();
1166
- });
1167
-
1168
- it("rejects concurrent writers to the same output directory", () => {
1169
- const outputDir = mkdtempSync(join(tmpdir(), "pivot-distill-lock-"));
1170
- chmodSync(outputDir, 0o755);
1171
- const release = acquireOutputLock(outputDir);
1172
- expect(statSync(outputDir).mode & 0o777).toBe(0o700);
1173
- expect(() => acquireOutputLock(outputDir)).toThrow("already in use");
1174
- release();
1175
- const releaseAgain = acquireOutputLock(outputDir);
1176
- releaseAgain();
1177
- });
1178
- });