recallnest 2.3.0 → 2.6.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 (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,628 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ import { LLMClient } from "../llm-client.js";
8
+ import {
9
+ buildPivotLlmConfig,
10
+ buildPivotRequestProfile,
11
+ buildStratifiedSample,
12
+ loadFrozenBundle,
13
+ main as pivotMain,
14
+ PIPELINE_VERSION,
15
+ PIVOT_MODEL,
16
+ pivotRequestProfileHash,
17
+ PROMPT_VERSION,
18
+ selectionIdentityHash,
19
+ writeFrozenBundle,
20
+ type SessionMeta,
21
+ } from "../../scripts/pivot-distill.js";
22
+
23
+ interface CapturedRequest {
24
+ method: string;
25
+ url: string;
26
+ body: unknown;
27
+ }
28
+
29
+ interface LoopbackStub {
30
+ baseURL: string;
31
+ requests: CapturedRequest[];
32
+ close: () => Promise<void>;
33
+ }
34
+
35
+ interface PlannedResponse {
36
+ body: Record<string, unknown>;
37
+ statusCode?: number;
38
+ headers?: Record<string, string>;
39
+ }
40
+
41
+ async function startLoopbackStub(
42
+ responseBody: Record<string, unknown> | PlannedResponse[],
43
+ statusCode = 200,
44
+ responseHeaders: Record<string, string> = {},
45
+ ): Promise<LoopbackStub> {
46
+ const requests: CapturedRequest[] = [];
47
+ const server = createServer(async (request, response) => {
48
+ try {
49
+ const chunks: Buffer[] = [];
50
+ for await (const chunk of request) {
51
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
52
+ }
53
+ const body: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8"));
54
+ requests.push({
55
+ method: request.method ?? "",
56
+ url: request.url ?? "",
57
+ body,
58
+ });
59
+ const planned = Array.isArray(responseBody)
60
+ ? responseBody[Math.min(requests.length - 1, responseBody.length - 1)]
61
+ : { body: responseBody, statusCode, headers: responseHeaders };
62
+ response.writeHead(planned.statusCode ?? 200, {
63
+ "content-type": "application/json",
64
+ connection: "close",
65
+ ...(planned.headers ?? {}),
66
+ });
67
+ response.end(JSON.stringify(planned.body));
68
+ } catch (error) {
69
+ response.writeHead(500, { "content-type": "text/plain", connection: "close" });
70
+ response.end(error instanceof Error ? error.message : "loopback stub error");
71
+ }
72
+ });
73
+ await new Promise<void>((resolve, reject) => {
74
+ const onError = (error: Error): void => reject(error);
75
+ server.once("error", onError);
76
+ server.listen(0, "127.0.0.1", () => {
77
+ server.off("error", onError);
78
+ resolve();
79
+ });
80
+ });
81
+ const address = server.address();
82
+ if (!address || typeof address === "string") {
83
+ server.close();
84
+ throw new Error("Loopback stub did not receive a TCP address");
85
+ }
86
+
87
+ return {
88
+ baseURL: `http://127.0.0.1:${address.port}/v1`,
89
+ requests,
90
+ close: async () => {
91
+ await new Promise<void>((resolve, reject) => {
92
+ server.close((error) => error ? reject(error) : resolve());
93
+ server.closeAllConnections();
94
+ });
95
+ },
96
+ };
97
+ }
98
+
99
+ function completionResponse(options: {
100
+ content: string | null;
101
+ finishReason: "stop" | "length";
102
+ model: string;
103
+ usage?: Record<string, unknown>;
104
+ }): Record<string, unknown> {
105
+ return {
106
+ id: "chatcmpl-loopback",
107
+ object: "chat.completion",
108
+ created: 0,
109
+ model: options.model,
110
+ choices: [{
111
+ index: 0,
112
+ message: { role: "assistant", content: options.content },
113
+ logprobs: null,
114
+ finish_reason: options.finishReason,
115
+ }],
116
+ ...(options.usage ? { usage: options.usage } : {}),
117
+ };
118
+ }
119
+
120
+ describe("LLMClient pivot detailed chat", () => {
121
+ it("sends the exact Qwen-compatible request body and returns normalized metadata", async () => {
122
+ const stub = await startLoopbackStub(completionResponse({
123
+ content: " {\"ok\":true} ",
124
+ finishReason: "stop",
125
+ model: "qwen-response-snapshot",
126
+ usage: {
127
+ prompt_tokens: 120,
128
+ completion_tokens: 18,
129
+ total_tokens: 138,
130
+ prompt_tokens_details: { cached_tokens: 12 },
131
+ completion_tokens_details: { reasoning_tokens: 3 },
132
+ },
133
+ }));
134
+ try {
135
+ const unsafeBaseURL = stub.baseURL.replace("http://", "http://user:password@") +
136
+ "?token=secret#fragment";
137
+ const profile = buildPivotRequestProfile(unsafeBaseURL);
138
+ const client = new LLMClient(buildPivotLlmConfig({
139
+ apiKey: "loopback-test-key",
140
+ model: "qwen-request-snapshot",
141
+ baseURL: unsafeBaseURL,
142
+ }, profile, 1_000));
143
+
144
+ const result = await client.chatLongDetailed("system prompt", "user prompt", {
145
+ temperature: 0.05,
146
+ enableThinking: false,
147
+ responseFormat: { type: "json_object" },
148
+ tokenLimit: { parameter: "max_tokens", value: 321 },
149
+ });
150
+
151
+ expect(stub.requests).toHaveLength(1);
152
+ expect(stub.requests[0]).toEqual({
153
+ method: "POST",
154
+ url: "/v1/chat/completions",
155
+ body: {
156
+ model: PIVOT_MODEL,
157
+ messages: [
158
+ { role: "system", content: "system prompt" },
159
+ { role: "user", content: "user prompt" },
160
+ ],
161
+ temperature: 0.05,
162
+ max_tokens: 321,
163
+ response_format: { type: "json_object" },
164
+ enable_thinking: false,
165
+ },
166
+ });
167
+ expect(result).toEqual({
168
+ content: "{\"ok\":true}",
169
+ finishReason: "stop",
170
+ responseModel: "qwen-response-snapshot",
171
+ usage: {
172
+ promptTokens: 120,
173
+ completionTokens: 18,
174
+ totalTokens: 138,
175
+ cachedPromptTokens: 12,
176
+ reasoningTokens: 3,
177
+ },
178
+ });
179
+ } finally {
180
+ await stub.close();
181
+ }
182
+ });
183
+
184
+ it("disables hidden SDK retries so one outer attempt sends one HTTP request", async () => {
185
+ const stub = await startLoopbackStub({
186
+ error: { message: "temporary failure", type: "server_error" },
187
+ }, 500, { "retry-after": "0" });
188
+ try {
189
+ const client = new LLMClient({
190
+ apiKey: "loopback-test-key",
191
+ model: "qwen-request-snapshot",
192
+ baseURL: stub.baseURL,
193
+ timeoutMs: 1_000,
194
+ maxRetries: 0,
195
+ });
196
+ await expect(client.chatLongDetailed("system", "user")).rejects.toThrow();
197
+ expect(stub.requests).toHaveLength(1);
198
+ } finally {
199
+ await stub.close();
200
+ }
201
+ });
202
+
203
+ it("supports max_completion_tokens and exposes a length finish reason", async () => {
204
+ const stub = await startLoopbackStub(completionResponse({
205
+ content: "{\"partial\":true}",
206
+ finishReason: "length",
207
+ model: "qwen-response-snapshot",
208
+ }));
209
+ try {
210
+ const client = new LLMClient({
211
+ apiKey: "loopback-test-key",
212
+ model: "qwen-request-snapshot",
213
+ baseURL: stub.baseURL,
214
+ timeoutMs: 1_000,
215
+ });
216
+
217
+ const result = await client.chatLongDetailed("system", "user", {
218
+ tokenLimit: { parameter: "max_completion_tokens", value: 456 },
219
+ });
220
+
221
+ expect(stub.requests[0].body).toEqual({
222
+ model: "qwen-request-snapshot",
223
+ messages: [
224
+ { role: "system", content: "system" },
225
+ { role: "user", content: "user" },
226
+ ],
227
+ temperature: 0.1,
228
+ max_completion_tokens: 456,
229
+ });
230
+ expect(result).toEqual({
231
+ content: "{\"partial\":true}",
232
+ finishReason: "length",
233
+ responseModel: "qwen-response-snapshot",
234
+ usage: null,
235
+ });
236
+ } finally {
237
+ await stub.close();
238
+ }
239
+ });
240
+
241
+ it("keeps chatLong as a string-or-null max_tokens wrapper", async () => {
242
+ const stub = await startLoopbackStub(completionResponse({
243
+ content: " legacy content ",
244
+ finishReason: "stop",
245
+ model: "legacy-response-model",
246
+ }));
247
+ try {
248
+ const client = new LLMClient({
249
+ apiKey: "loopback-test-key",
250
+ model: "legacy-request-model",
251
+ baseURL: stub.baseURL,
252
+ timeoutMs: 1_000,
253
+ temperature: 0.25,
254
+ });
255
+
256
+ const result: string | null = await client.chatLong("legacy system", "legacy user", 77);
257
+
258
+ expect(result).toBe("legacy content");
259
+ expect(stub.requests[0].body).toEqual({
260
+ model: "legacy-request-model",
261
+ messages: [
262
+ { role: "system", content: "legacy system" },
263
+ { role: "user", content: "legacy user" },
264
+ ],
265
+ temperature: 0.25,
266
+ max_tokens: 77,
267
+ });
268
+ } finally {
269
+ await stub.close();
270
+ }
271
+ });
272
+
273
+ it("runs authorized transport only from the frozen bundle with bounded retries and complete audit identity", async () => {
274
+ const invalidUsage = { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 };
275
+ const successUsage = { prompt_tokens: 11, completion_tokens: 3, total_tokens: 14 };
276
+ const success = completionResponse({
277
+ content: JSON.stringify({ hasPivot: false, candidates: [] }),
278
+ finishReason: "stop",
279
+ model: PIVOT_MODEL,
280
+ usage: successUsage,
281
+ });
282
+ const stub = await startLoopbackStub([
283
+ {
284
+ statusCode: 429,
285
+ headers: { "retry-after": "0" },
286
+ body: { error: { message: "rate limited", type: "rate_limit_error" } },
287
+ },
288
+ {
289
+ body: completionResponse({
290
+ content: "not-json",
291
+ finishReason: "stop",
292
+ model: PIVOT_MODEL,
293
+ usage: invalidUsage,
294
+ }),
295
+ },
296
+ { body: success },
297
+ ]);
298
+ const root = mkdtempSync(join(tmpdir(), "pivot-transport-loopback-"));
299
+ const sourcePath = join(root, "source.jsonl");
300
+ writeFileSync(sourcePath, "{}\n");
301
+ const sourceStat = statSync(sourcePath, { bigint: true });
302
+ const profile = buildPivotRequestProfile(stub.baseURL, 600);
303
+ const descriptor = writeFrozenBundle(root, [{
304
+ session: {
305
+ key: "codex:loopback-session",
306
+ sessionId: "loopback-session",
307
+ rawHarness: "codex",
308
+ harness: "codex",
309
+ parserKind: "codex-rollout",
310
+ project: "test",
311
+ path: sourcePath,
312
+ date: "2026-08-03",
313
+ sizeBytes: Number(sourceStat.size),
314
+ mtimeNs: sourceStat.mtimeNs.toString(),
315
+ origin: "deja",
316
+ fingerprint: "placeholder",
317
+ } satisfies SessionMeta,
318
+ sample: buildStratifiedSample([
319
+ { role: "user", text: "请保留可以回查的出处" },
320
+ { role: "assistant", text: "已经记录出处" },
321
+ ], 600),
322
+ }], profile, pivotRequestProfileHash(profile));
323
+ const bundleDir = join(root, "input-bundle");
324
+ const loaded = loadFrozenBundle(bundleDir, descriptor.bundleHash, descriptor.requestProfileHash);
325
+ const entry = loaded.entries[0];
326
+ const configPath = join(root, "config.json");
327
+ writeFileSync(configPath, JSON.stringify({
328
+ llm: { apiKey: "loopback-test-key", model: "qwen-turbo", baseURL: stub.baseURL },
329
+ }));
330
+ const outputDir = join(root, "transport-output");
331
+ mkdirSync(outputDir, { mode: 0o755 });
332
+ const dejaMarker = join(root, "deja-was-called");
333
+ const fakeDeja = join(root, "must-not-run-deja.sh");
334
+ writeFileSync(fakeDeja, `#!/bin/sh\ntouch "${dejaMarker}"\nexit 99\n`, { mode: 0o700 });
335
+ chmodSync(fakeDeja, 0o700);
336
+ const args = [
337
+ "--mode", "transport",
338
+ "--input-bundle", bundleDir,
339
+ "--bundle-hash", descriptor.bundleHash,
340
+ "--request-profile-hash", descriptor.requestProfileHash,
341
+ "--session-key", entry.session.key,
342
+ "--output-dir", outputDir,
343
+ "--deja-bin", fakeDeja,
344
+ "--allow-external-llm",
345
+ ];
346
+ const previousConfig = process.env.LOCAL_MEMORY_CONFIG;
347
+ process.env.LOCAL_MEMORY_CONFIG = configPath;
348
+ try {
349
+ await pivotMain(args);
350
+ expect(stub.requests).toHaveLength(3);
351
+ expect(stub.requests.every((request) => request.url === "/v1/chat/completions")).toBe(true);
352
+ expect(existsSync(dejaMarker)).toBe(false);
353
+ const resultPath = join(outputDir, "results", `${entry.session.fingerprint}.json`);
354
+ const result = JSON.parse(readFileSync(resultPath, "utf8")) as {
355
+ mode: string;
356
+ selectionHash: string;
357
+ outboundSampleSha256: string;
358
+ status: string;
359
+ attempts: number;
360
+ responseModel: string;
361
+ usage: { promptTokens: number; completionTokens: number; totalTokens: number };
362
+ attemptErrors: string[];
363
+ };
364
+ expect(result).toMatchObject({
365
+ mode: "transport",
366
+ outboundSampleSha256: entry.sampleSha256,
367
+ status: "ok",
368
+ attempts: 3,
369
+ responseModel: PIVOT_MODEL,
370
+ usage: { promptTokens: 21, completionTokens: 5, totalTokens: 26 },
371
+ });
372
+ expect(result.selectionHash).toHaveLength(64);
373
+ expect(result.attemptErrors).toHaveLength(2);
374
+ const invalid = JSON.parse(
375
+ readFileSync(join(outputDir, "invalid", `${entry.session.fingerprint}.json`), "utf8"),
376
+ ) as { attempts: Array<{ usage?: { promptTokens: number; completionTokens: number; totalTokens: number } }> };
377
+ expect(invalid.attempts).toHaveLength(2);
378
+ expect(invalid.attempts[1].usage).toEqual({
379
+ promptTokens: 10, completionTokens: 2, totalTokens: 12,
380
+ });
381
+ const summary = JSON.parse(readFileSync(join(outputDir, "summary.json"), "utf8")) as Record<string, unknown>;
382
+ const reportState = JSON.parse(
383
+ readFileSync(join(outputDir, "report-state.json"), "utf8"),
384
+ ) as Record<string, unknown>;
385
+ expect(summary).toMatchObject({
386
+ reportStatus: "complete",
387
+ mode: "transport",
388
+ selectionHash: result.selectionHash,
389
+ manifestSessions: 1,
390
+ resultSessions: 1,
391
+ usage: { promptTokens: 21, completionTokens: 5, totalTokens: 26 },
392
+ attemptAudit: {
393
+ validLedgers: 1,
394
+ invalidLedgers: 0,
395
+ startedAttempts: 3,
396
+ inFlightAttempts: 0,
397
+ recordedUsage: { promptTokens: 21, completionTokens: 5, totalTokens: 26 },
398
+ },
399
+ });
400
+ expect(reportState).toMatchObject({
401
+ status: "complete", mode: "transport", selectionHash: result.selectionHash,
402
+ });
403
+ expect(statSync(outputDir).mode & 0o777).toBe(0o700);
404
+ expect(statSync(join(outputDir, "results")).mode & 0o777).toBe(0o700);
405
+ expect(statSync(join(outputDir, "attempts")).mode & 0o777).toBe(0o700);
406
+ expect(statSync(resultPath).mode & 0o777).toBe(0o600);
407
+ expect(statSync(join(outputDir, "attempts", `${entry.session.fingerprint}.json`)).mode & 0o777)
408
+ .toBe(0o600);
409
+ expect(statSync(join(outputDir, "summary.json")).mode & 0o777).toBe(0o600);
410
+
411
+ await pivotMain(args);
412
+ expect(stub.requests).toHaveLength(3);
413
+ result.responseModel = "tampered-model";
414
+ writeFileSync(resultPath, `${JSON.stringify(result)}\n`);
415
+ await expect(pivotMain(args)).rejects.toThrow("failed strict identity validation");
416
+ expect(stub.requests).toHaveLength(3);
417
+ expect(JSON.parse(readFileSync(join(outputDir, "report-state.json"), "utf8")))
418
+ .toMatchObject({ status: "partial" });
419
+ expect(JSON.parse(readFileSync(join(outputDir, "summary.json"), "utf8")))
420
+ .toMatchObject({ reportStatus: "partial", resultSessions: 0 });
421
+ } finally {
422
+ if (previousConfig === undefined) delete process.env.LOCAL_MEMORY_CONFIG;
423
+ else process.env.LOCAL_MEMORY_CONFIG = previousConfig;
424
+ await stub.close();
425
+ }
426
+ }, 15_000);
427
+
428
+ it("preserves empty-content usage and refuses to reset a failed run's three-attempt budget", async () => {
429
+ const usage = [
430
+ { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
431
+ { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
432
+ { prompt_tokens: 6, completion_tokens: 3, total_tokens: 9 },
433
+ ];
434
+ const stub = await startLoopbackStub(usage.map((attemptUsage, index) => ({
435
+ body: completionResponse({
436
+ content: index === 0 ? null : "not-json",
437
+ finishReason: "stop",
438
+ model: PIVOT_MODEL,
439
+ usage: attemptUsage,
440
+ }),
441
+ })));
442
+ const root = mkdtempSync(join(tmpdir(), "pivot-failed-resume-loopback-"));
443
+ const sourcePath = join(root, "source.jsonl");
444
+ writeFileSync(sourcePath, "{}\n");
445
+ const sourceStat = statSync(sourcePath, { bigint: true });
446
+ const profile = buildPivotRequestProfile(stub.baseURL, 600);
447
+ const descriptor = writeFrozenBundle(root, [{
448
+ session: {
449
+ key: "codex:failed-resume-session",
450
+ sessionId: "failed-resume-session",
451
+ rawHarness: "codex",
452
+ harness: "codex",
453
+ parserKind: "codex-rollout",
454
+ project: "test",
455
+ path: sourcePath,
456
+ date: "2026-08-03",
457
+ sizeBytes: Number(sourceStat.size),
458
+ mtimeNs: sourceStat.mtimeNs.toString(),
459
+ origin: "deja",
460
+ fingerprint: "placeholder",
461
+ } satisfies SessionMeta,
462
+ sample: buildStratifiedSample([
463
+ { role: "user", text: "请保留可以回查的出处" },
464
+ { role: "assistant", text: "已经记录出处" },
465
+ ], 600),
466
+ }], profile, pivotRequestProfileHash(profile));
467
+ const bundleDir = join(root, "input-bundle");
468
+ const entry = loadFrozenBundle(
469
+ bundleDir, descriptor.bundleHash, descriptor.requestProfileHash,
470
+ ).entries[0];
471
+ const configPath = join(root, "config.json");
472
+ writeFileSync(configPath, JSON.stringify({
473
+ llm: { apiKey: "loopback-test-key", model: "qwen-turbo", baseURL: stub.baseURL },
474
+ }));
475
+ const outputDir = join(root, "transport-output");
476
+ const args = [
477
+ "--mode", "transport",
478
+ "--input-bundle", bundleDir,
479
+ "--bundle-hash", descriptor.bundleHash,
480
+ "--request-profile-hash", descriptor.requestProfileHash,
481
+ "--session-key", entry.session.key,
482
+ "--output-dir", outputDir,
483
+ "--allow-external-llm",
484
+ ];
485
+ const previousConfig = process.env.LOCAL_MEMORY_CONFIG;
486
+ process.env.LOCAL_MEMORY_CONFIG = configPath;
487
+ try {
488
+ await expect(pivotMain(args)).rejects.toThrow("Stopped after 3 failed attempt(s)");
489
+ expect(stub.requests).toHaveLength(3);
490
+ const resultPath = join(outputDir, "results", `${entry.session.fingerprint}.json`);
491
+ const resultBeforeResume = readFileSync(resultPath, "utf8");
492
+ const result = JSON.parse(resultBeforeResume) as {
493
+ status: string;
494
+ attempts: number;
495
+ usage: { promptTokens: number; completionTokens: number; totalTokens: number };
496
+ attemptErrors: string[];
497
+ };
498
+ expect(result).toMatchObject({
499
+ status: "invalid",
500
+ attempts: 3,
501
+ usage: { promptTokens: 15, completionTokens: 6, totalTokens: 21 },
502
+ });
503
+ expect(result.attemptErrors).toHaveLength(3);
504
+ const invalid = JSON.parse(
505
+ readFileSync(join(outputDir, "invalid", `${entry.session.fingerprint}.json`), "utf8"),
506
+ ) as {
507
+ recovered: boolean;
508
+ attempts: Array<{
509
+ responseModel?: string;
510
+ finishReason?: string | null;
511
+ usage?: { promptTokens: number; completionTokens: number; totalTokens: number };
512
+ }>;
513
+ };
514
+ expect(invalid.recovered).toBeFalse();
515
+ expect(invalid.attempts).toHaveLength(3);
516
+ expect(invalid.attempts[0]).toMatchObject({
517
+ responseModel: PIVOT_MODEL,
518
+ finishReason: "stop",
519
+ usage: { promptTokens: 4, completionTokens: 1, totalTokens: 5 },
520
+ });
521
+
522
+ await expect(pivotMain(args)).rejects.toThrow("already records 3 attempt(s)");
523
+ expect(stub.requests).toHaveLength(3);
524
+ expect(readFileSync(resultPath, "utf8")).toBe(resultBeforeResume);
525
+ } finally {
526
+ if (previousConfig === undefined) delete process.env.LOCAL_MEMORY_CONFIG;
527
+ else process.env.LOCAL_MEMORY_CONFIG = previousConfig;
528
+ await stub.close();
529
+ }
530
+ }, 15_000);
531
+
532
+ it("refuses a crash-marked in-flight attempt without sending another request", async () => {
533
+ const stub = await startLoopbackStub(completionResponse({
534
+ content: JSON.stringify({ hasPivot: false, candidates: [] }),
535
+ finishReason: "stop",
536
+ model: PIVOT_MODEL,
537
+ }));
538
+ const root = mkdtempSync(join(tmpdir(), "pivot-in-flight-loopback-"));
539
+ const sourcePath = join(root, "source.jsonl");
540
+ writeFileSync(sourcePath, "{}\n");
541
+ const sourceStat = statSync(sourcePath, { bigint: true });
542
+ const profile = buildPivotRequestProfile(stub.baseURL, 600);
543
+ const descriptor = writeFrozenBundle(root, [{
544
+ session: {
545
+ key: "codex:in-flight-session",
546
+ sessionId: "in-flight-session",
547
+ rawHarness: "codex",
548
+ harness: "codex",
549
+ parserKind: "codex-rollout",
550
+ project: "test",
551
+ path: sourcePath,
552
+ date: "2026-08-03",
553
+ sizeBytes: Number(sourceStat.size),
554
+ mtimeNs: sourceStat.mtimeNs.toString(),
555
+ origin: "deja",
556
+ fingerprint: "placeholder",
557
+ } satisfies SessionMeta,
558
+ sample: buildStratifiedSample([
559
+ { role: "user", text: "崩溃后不要偷偷重发请求" },
560
+ { role: "assistant", text: "会先保留请求预算记录" },
561
+ ], 600),
562
+ }], profile, pivotRequestProfileHash(profile));
563
+ const bundleDir = join(root, "input-bundle");
564
+ const entry = loadFrozenBundle(
565
+ bundleDir, descriptor.bundleHash, descriptor.requestProfileHash,
566
+ ).entries[0];
567
+ const configPath = join(root, "config.json");
568
+ writeFileSync(configPath, JSON.stringify({
569
+ llm: { apiKey: "loopback-test-key", model: "qwen-turbo", baseURL: stub.baseURL },
570
+ }));
571
+ const outputDir = join(root, "transport-output");
572
+ const attemptsDir = join(outputDir, "attempts");
573
+ mkdirSync(attemptsDir, { recursive: true, mode: 0o700 });
574
+ const selectionHash = selectionIdentityHash("transport", [entry]);
575
+ writeFileSync(join(attemptsDir, `${entry.session.fingerprint}.json`), `${JSON.stringify({
576
+ schemaVersion: 1,
577
+ mode: "transport",
578
+ selectionHash,
579
+ outboundSampleSha256: entry.sampleSha256,
580
+ promptVersion: PROMPT_VERSION,
581
+ pipelineVersion: PIPELINE_VERSION,
582
+ model: PIVOT_MODEL,
583
+ requestProfileHash: descriptor.requestProfileHash,
584
+ bundleHash: descriptor.bundleHash,
585
+ session: entry.session,
586
+ maxSessionAttempts: profile.retryPolicy.maxSessionAttempts,
587
+ attempts: [{
588
+ attempt: 1,
589
+ state: "in-flight",
590
+ startedAt: "2026-08-03T00:00:00.000Z",
591
+ }],
592
+ updatedAt: "2026-08-03T00:00:00.000Z",
593
+ }, null, 2)}\n`, { mode: 0o600 });
594
+ const args = [
595
+ "--mode", "transport",
596
+ "--input-bundle", bundleDir,
597
+ "--bundle-hash", descriptor.bundleHash,
598
+ "--request-profile-hash", descriptor.requestProfileHash,
599
+ "--session-key", entry.session.key,
600
+ "--output-dir", outputDir,
601
+ "--allow-external-llm",
602
+ ];
603
+ const previousConfig = process.env.LOCAL_MEMORY_CONFIG;
604
+ process.env.LOCAL_MEMORY_CONFIG = configPath;
605
+ try {
606
+ await expect(pivotMain(args)).rejects.toThrow("incomplete attempt ledger");
607
+ expect(stub.requests).toHaveLength(0);
608
+ expect(JSON.parse(readFileSync(join(outputDir, "report-state.json"), "utf8")))
609
+ .toMatchObject({ status: "partial" });
610
+ expect(JSON.parse(readFileSync(join(outputDir, "summary.json"), "utf8")))
611
+ .toMatchObject({
612
+ reportStatus: "partial",
613
+ resultSessions: 0,
614
+ attemptAudit: {
615
+ validLedgers: 1,
616
+ invalidLedgers: 0,
617
+ startedAttempts: 1,
618
+ inFlightAttempts: 1,
619
+ recordedUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
620
+ },
621
+ });
622
+ } finally {
623
+ if (previousConfig === undefined) delete process.env.LOCAL_MEMORY_CONFIG;
624
+ else process.env.LOCAL_MEMORY_CONFIG = previousConfig;
625
+ await stub.close();
626
+ }
627
+ }, 15_000);
628
+ });