@datename/fastcode-termux 0.1.6-beta → 0.1.8-beta.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 (165) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +50 -17
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +33 -0
  10. package/dist/agent/approval.js +40 -5
  11. package/dist/agent/compaction.d.ts +2 -0
  12. package/dist/agent/compaction.js +60 -17
  13. package/dist/agent/guards.d.ts +16 -16
  14. package/dist/agent/guards.js +978 -24
  15. package/dist/agent/policy/defaults.d.ts +16 -0
  16. package/dist/agent/policy/defaults.js +17 -0
  17. package/dist/agent/policy/egress.d.ts +32 -0
  18. package/dist/agent/policy/egress.js +280 -0
  19. package/dist/agent/process-tree.d.ts +20 -0
  20. package/dist/agent/process-tree.js +81 -10
  21. package/dist/agent/protocol.d.ts +20 -1
  22. package/dist/agent/protocol.js +165 -38
  23. package/dist/agent/registry.d.ts +3 -1
  24. package/dist/agent/registry.js +168 -11
  25. package/dist/agent/runner.d.ts +21 -0
  26. package/dist/agent/runner.js +44 -5
  27. package/dist/agent/shell-ast.d.ts +11 -0
  28. package/dist/agent/shell-ast.js +204 -0
  29. package/dist/agent/shell-session.d.ts +14 -0
  30. package/dist/agent/shell-session.js +251 -40
  31. package/dist/agent/tool-schemas.js +26 -7
  32. package/dist/agent/tools/append_file.js +3 -1
  33. package/dist/agent/tools/apply_patch.js +44 -4
  34. package/dist/agent/tools/copy_file.js +26 -3
  35. package/dist/agent/tools/delete_file.js +22 -3
  36. package/dist/agent/tools/grep.js +35 -4
  37. package/dist/agent/tools/make_dir.js +3 -1
  38. package/dist/agent/tools/move_path.d.ts +7 -0
  39. package/dist/agent/tools/move_path.js +31 -5
  40. package/dist/agent/tools/read_file.js +63 -3
  41. package/dist/agent/tools/read_many.js +21 -3
  42. package/dist/agent/tools/readers.d.ts +53 -1
  43. package/dist/agent/tools/readers.js +236 -26
  44. package/dist/agent/tools/replace_in_file.js +38 -5
  45. package/dist/agent/tools/shell.js +2 -2
  46. package/dist/agent/tools/types.d.ts +26 -0
  47. package/dist/agent/tools/validate.d.ts +15 -2
  48. package/dist/agent/tools/validate.js +66 -3
  49. package/dist/agent/tools/write_file.js +3 -1
  50. package/dist/app.d.ts +2 -1
  51. package/dist/app.js +526 -85
  52. package/dist/cli-main.js +363 -0
  53. package/dist/cli.js +397 -337
  54. package/dist/commands/about.js +1 -1
  55. package/dist/commands/clear.d.ts +1 -0
  56. package/dist/commands/clear.js +1 -1
  57. package/dist/commands/compact.d.ts +12 -0
  58. package/dist/commands/compact.js +14 -2
  59. package/dist/commands/context.js +5 -4
  60. package/dist/commands/index.js +2 -0
  61. package/dist/commands/jobs.js +3 -2
  62. package/dist/commands/kill.js +3 -1
  63. package/dist/commands/login.d.ts +2 -2
  64. package/dist/commands/login.js +15 -15
  65. package/dist/commands/model.d.ts +1 -13
  66. package/dist/commands/model.js +6 -38
  67. package/dist/commands/provider.d.ts +1 -15
  68. package/dist/commands/provider.js +4 -22
  69. package/dist/commands/skills.js +34 -4
  70. package/dist/commands/threads.d.ts +9 -0
  71. package/dist/commands/threads.js +26 -0
  72. package/dist/config/store.d.ts +21 -0
  73. package/dist/config/store.js +47 -9
  74. package/dist/config/trust.d.ts +9 -0
  75. package/dist/config/trust.js +41 -2
  76. package/dist/headless.d.ts +7 -0
  77. package/dist/headless.js +213 -0
  78. package/dist/index.js +19 -3
  79. package/dist/lib/debug.d.ts +2 -0
  80. package/dist/lib/debug.js +22 -0
  81. package/dist/lib/diagnostics.d.ts +19 -0
  82. package/dist/lib/diagnostics.js +32 -6
  83. package/dist/lib/highlight-subset.d.ts +21 -0
  84. package/dist/lib/highlight-subset.js +88 -0
  85. package/dist/lib/lifecycle.d.ts +5 -0
  86. package/dist/lib/lifecycle.js +62 -0
  87. package/dist/lib/markdown.js +22 -4
  88. package/dist/providers/deepseek.js +1 -1
  89. package/dist/providers/openaiCompatible.d.ts +24 -2
  90. package/dist/providers/openaiCompatible.js +20 -6
  91. package/dist/providers/spec.d.ts +12 -0
  92. package/dist/providers/spec.js +29 -0
  93. package/dist/session/log.d.ts +14 -2
  94. package/dist/session/log.js +56 -27
  95. package/dist/session/projection.d.ts +34 -1
  96. package/dist/session/projection.js +315 -46
  97. package/dist/skills/manager.d.ts +15 -0
  98. package/dist/skills/manager.js +135 -18
  99. package/dist/types.d.ts +15 -0
  100. package/dist/ui/StatusBar.js +1 -1
  101. package/package.json +3 -2
  102. package/src/agent/approval-json.ts +103 -0
  103. package/src/agent/approval-native.ts +168 -0
  104. package/src/agent/approval-store.ts +38 -0
  105. package/src/agent/approval.ts +58 -11
  106. package/src/agent/compaction.ts +62 -17
  107. package/src/agent/guards.ts +1049 -25
  108. package/src/agent/policy/defaults.ts +34 -0
  109. package/src/agent/policy/egress.ts +335 -0
  110. package/src/agent/process-tree.ts +105 -23
  111. package/src/agent/protocol.ts +187 -39
  112. package/src/agent/registry.ts +186 -14
  113. package/src/agent/runner.ts +50 -7
  114. package/src/agent/shell-ast.ts +227 -0
  115. package/src/agent/shell-session.ts +265 -41
  116. package/src/agent/tool-schemas.ts +26 -7
  117. package/src/agent/tools/append_file.ts +3 -1
  118. package/src/agent/tools/apply_patch.ts +45 -4
  119. package/src/agent/tools/copy_file.ts +27 -3
  120. package/src/agent/tools/delete_file.ts +22 -3
  121. package/src/agent/tools/grep.ts +36 -4
  122. package/src/agent/tools/make_dir.ts +3 -1
  123. package/src/agent/tools/move_path.ts +38 -5
  124. package/src/agent/tools/read_file.ts +64 -3
  125. package/src/agent/tools/read_many.ts +21 -3
  126. package/src/agent/tools/readers.ts +289 -26
  127. package/src/agent/tools/replace_in_file.ts +39 -5
  128. package/src/agent/tools/shell.ts +2 -2
  129. package/src/agent/tools/types.ts +43 -1
  130. package/src/agent/tools/validate.ts +73 -4
  131. package/src/agent/tools/write_file.ts +3 -1
  132. package/src/app.ts +533 -82
  133. package/src/{cli.ts → cli-main.ts} +34 -6
  134. package/src/commands/about.ts +1 -1
  135. package/src/commands/clear.ts +1 -1
  136. package/src/commands/compact.ts +14 -2
  137. package/src/commands/context.ts +5 -4
  138. package/src/commands/index.ts +2 -0
  139. package/src/commands/jobs.ts +3 -2
  140. package/src/commands/kill.ts +3 -1
  141. package/src/commands/login.ts +15 -15
  142. package/src/commands/model.ts +6 -38
  143. package/src/commands/provider.ts +4 -22
  144. package/src/commands/skills.ts +34 -4
  145. package/src/commands/threads.ts +28 -0
  146. package/src/config/store.ts +47 -9
  147. package/src/config/trust.ts +45 -2
  148. package/src/headless.ts +225 -0
  149. package/src/index.ts +19 -3
  150. package/src/lib/debug.ts +24 -0
  151. package/src/lib/diagnostics.ts +32 -6
  152. package/src/lib/highlight-subset.ts +93 -0
  153. package/src/lib/lifecycle.ts +69 -0
  154. package/src/lib/markdown.ts +22 -4
  155. package/src/providers/deepseek.ts +1 -1
  156. package/src/providers/openaiCompatible.ts +47 -10
  157. package/src/providers/spec.ts +42 -0
  158. package/src/session/log.ts +70 -27
  159. package/src/session/projection.ts +360 -47
  160. package/src/skills/manager.ts +140 -18
  161. package/src/types/highlight-shims.d.ts +14 -0
  162. package/src/types.ts +16 -1
  163. package/src/ui/StatusBar.ts +1 -1
  164. package/tsconfig.json +1 -0
  165. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -5,8 +5,9 @@ import { toolCatalogHash } from "../agent/registry.js";
5
5
  import { projectionPath, readSession } from "./log.js";
6
6
  const READ_ONLY_TOOLS = new Set(["read_file", "read_many", "grep", "list_files", "stat_path", "pwd"]);
7
7
  const IDEMPOTENT_TOOLS = new Set(["write_file"]);
8
- const MAX_MESSAGE_CHARS = 4000;
9
- const MAX_PROJECTION_BYTES = 19_000;
8
+ const MAX_MESSAGE_CHARS = 8_000;
9
+ const MAX_PROJECTION_BYTES = 200_000;
10
+ const TOOL_FOLD_CHARS = 600;
10
11
  export function unpairedToolNotice(tool) {
11
12
  if (READ_ONLY_TOOLS.has(tool))
12
13
  return "执行中断(只读工具,可安全重试)。";
@@ -33,8 +34,21 @@ export function sessionMetaFromFacts(facts) {
33
34
  let cwd = "";
34
35
  let providerId = "";
35
36
  let model = "";
37
+ let sawStart = false;
36
38
  for (const fact of facts) {
37
- if (fact?.type === "session_start" || fact?.type === "checkpoint") {
39
+ // Only the first session_start defines the session. A resume appends
40
+ // another session_start with the current cwd/model, which must not
41
+ // change the fingerprint of the stored projection.
42
+ if (fact?.type === "session_start" && !sawStart) {
43
+ sawStart = true;
44
+ if (typeof fact.cwd === "string" && fact.cwd)
45
+ cwd = fact.cwd;
46
+ if (typeof fact.providerId === "string" && fact.providerId)
47
+ providerId = fact.providerId;
48
+ if (typeof fact.model === "string" && fact.model)
49
+ model = fact.model;
50
+ }
51
+ else if (fact?.type === "checkpoint") {
38
52
  if (typeof fact.cwd === "string" && fact.cwd)
39
53
  cwd = fact.cwd;
40
54
  if (typeof fact.providerId === "string" && fact.providerId)
@@ -45,6 +59,13 @@ export function sessionMetaFromFacts(facts) {
45
59
  }
46
60
  return { cwd, providerId, model };
47
61
  }
62
+ function factsAfterClear(facts) {
63
+ for (let index = facts.length - 1; index >= 0; index -= 1) {
64
+ if (facts[index]?.type === "cleared")
65
+ return facts.slice(index + 1);
66
+ }
67
+ return facts;
68
+ }
48
69
  export function sessionMetaFromFile(file) {
49
70
  return sessionMetaFromFacts(readSession(file));
50
71
  }
@@ -89,6 +110,10 @@ export function factsToMessages(facts) {
89
110
  else if (fact.type === "tool_call") {
90
111
  calls.set(String(fact.id ?? ""), fact.args ?? { tool: fact.tool });
91
112
  }
113
+ else if (fact.type === "notice") {
114
+ if (typeof fact.content === "string" && fact.content)
115
+ messages.push({ role: "system", content: fact.content });
116
+ }
92
117
  else if (fact.type === "tool_result") {
93
118
  const id = String(fact.id ?? "");
94
119
  const call = calls.get(id) ?? { tool: fact.tool ?? "unknown" };
@@ -104,6 +129,11 @@ export function factsToMessages(facts) {
104
129
  });
105
130
  }
106
131
  }
132
+ else if (fact.type === "egress_denied") {
133
+ const target = fact.target ? ` target=${JSON.stringify(fact.target)}` : "";
134
+ const content = `[egress] ${fact.mode ?? "audit"} rule=${fact.rule ?? "unknown"}${target} would_block=${Boolean(fact.wouldBlock)}\n${fact.reason ?? ""}`;
135
+ messages.push({ role: "system", content });
136
+ }
107
137
  }
108
138
  flush();
109
139
  if (messages.length === 0)
@@ -140,8 +170,90 @@ function applySummary(messages, facts) {
140
170
  return [marker, ...tail];
141
171
  }
142
172
  catch {
143
- return messages;
173
+ return [
174
+ ...messages,
175
+ { role: "system", content: "[projection] Summary file could not be read; resumed without the summary marker." },
176
+ ];
177
+ }
178
+ }
179
+ export function projectionContentHash(projection) {
180
+ const core = {
181
+ version: projection.version,
182
+ kind: projection.kind,
183
+ cwd: projection.cwd,
184
+ providerId: projection.providerId,
185
+ model: projection.model,
186
+ fingerprint: projection.fingerprint,
187
+ messages: projection.messages,
188
+ activeFiles: projection.activeFiles,
189
+ lastFactSeq: projection.lastFactSeq,
190
+ };
191
+ return createHash("sha256").update(JSON.stringify(core)).digest("hex");
192
+ }
193
+ export function factsContentHash(facts) {
194
+ return createHash("sha256").update(JSON.stringify(facts)).digest("hex");
195
+ }
196
+ function lastFactSeqOf(facts) {
197
+ let seq = 0;
198
+ for (const fact of facts) {
199
+ if (typeof fact?.seq === "number" && fact.seq > seq)
200
+ seq = fact.seq;
144
201
  }
202
+ return seq;
203
+ }
204
+ function withManifest(projection, facts) {
205
+ return {
206
+ ...projection,
207
+ manifest: {
208
+ version: 1,
209
+ algorithm: "sha256",
210
+ savedAt: new Date().toISOString(),
211
+ contentHash: projectionContentHash(projection),
212
+ ...(facts ? { sessionHash: factsContentHash(facts), lastFactSeq: lastFactSeqOf(facts) } : {}),
213
+ },
214
+ };
215
+ }
216
+ function emptyProjection(meta) {
217
+ return buildProjection([], meta);
218
+ }
219
+ /**
220
+ * Keep native tool_call/tool_result pairs complete. Orphan tool messages left
221
+ * behind by projection truncation are dropped; missing results are synthesized
222
+ * with the existing "execution unknown" notice so a resumed model never waits
223
+ * for a result that cannot arrive.
224
+ */
225
+ export function pairToolMessages(messages) {
226
+ const out = [];
227
+ const expected = new Map();
228
+ let repaired = false;
229
+ const synthesize = () => {
230
+ for (const [id, tool] of expected)
231
+ out.push({ role: "tool", toolCallId: id, content: unpairedToolNotice(tool) });
232
+ if (expected.size > 0)
233
+ repaired = true;
234
+ expected.clear();
235
+ };
236
+ for (const message of messages) {
237
+ if (message.role === "tool") {
238
+ if (!message.toolCallId || !expected.has(message.toolCallId)) {
239
+ repaired = true;
240
+ continue;
241
+ }
242
+ expected.delete(message.toolCallId);
243
+ out.push(message);
244
+ continue;
245
+ }
246
+ synthesize();
247
+ out.push(message);
248
+ if (message.role === "assistant" && Array.isArray(message.toolCalls)) {
249
+ for (const call of message.toolCalls) {
250
+ if (call?.id)
251
+ expected.set(String(call.id), String(call.name ?? "unknown"));
252
+ }
253
+ }
254
+ }
255
+ synthesize();
256
+ return { messages: out, repaired };
145
257
  }
146
258
  export function buildProjection(facts, metaOverride) {
147
259
  const detected = sessionMetaFromFacts(facts);
@@ -150,13 +262,17 @@ export function buildProjection(facts, metaOverride) {
150
262
  providerId: metaOverride?.providerId || detected.providerId,
151
263
  model: metaOverride?.model || detected.model,
152
264
  };
153
- const messages = clampMessages(applySummary(factsToMessages(facts), facts));
154
- const source = compactionSourceFromFacts(facts);
265
+ const threadFacts = factsAfterClear(facts);
266
+ const folded = foldToolMessages(applySummary(factsToMessages(threadFacts), threadFacts));
267
+ const messages = pairToolMessages(clampMessages(folded)).messages;
268
+ const source = compactionSourceFromFacts(threadFacts);
155
269
  let activeFiles = [];
156
270
  let lastFactSeq = 0;
157
271
  for (const fact of facts) {
158
272
  if (typeof fact?.seq === "number" && fact.seq > lastFactSeq)
159
273
  lastFactSeq = fact.seq;
274
+ }
275
+ for (const fact of threadFacts) {
160
276
  if (fact?.type === "checkpoint" && Array.isArray(fact.activeFiles))
161
277
  activeFiles = fact.activeFiles;
162
278
  }
@@ -173,42 +289,162 @@ export function buildProjection(facts, metaOverride) {
173
289
  };
174
290
  return projection;
175
291
  }
292
+ function clipProjection(text, max) {
293
+ const oneLine = String(text).replace(/\s+/g, " ").trim();
294
+ return oneLine.length > max ? oneLine.slice(0, Math.max(1, max - 1)) + "…" : oneLine;
295
+ }
296
+ function toolTarget(args) {
297
+ if (!args || typeof args !== "object")
298
+ return "";
299
+ const value = args.path ?? args.from ?? args.to ?? args.command ?? args.pattern;
300
+ return typeof value === "string" ? clipProjection(value, 160) : "";
301
+ }
302
+ function firstImportantLine(text) {
303
+ const lines = String(text)
304
+ .split(/\r?\n/)
305
+ .map((line) => line.trim())
306
+ .filter((line) => line && !/^```/.test(line) && line !== "output:");
307
+ const important = lines.find((line) => /error|failed|not found|denied|timed out|exit/i.test(line)) ?? lines[0] ?? "";
308
+ return clipProjection(important, 200);
309
+ }
310
+ /** Keep the edit fact (tool + target + outcome), not the full tool payload. */
311
+ function foldToolMessages(messages) {
312
+ const calls = new Map();
313
+ const out = [];
314
+ for (const message of messages) {
315
+ if (message.role === "assistant" && Array.isArray(message.toolCalls)) {
316
+ for (const call of message.toolCalls) {
317
+ let args = {};
318
+ try {
319
+ args = JSON.parse(call.arguments || "{}");
320
+ }
321
+ catch {
322
+ args = {};
323
+ }
324
+ calls.set(String(call.id), { name: String(call.name ?? "tool"), args });
325
+ }
326
+ out.push(message);
327
+ continue;
328
+ }
329
+ if (message.role === "tool") {
330
+ const call = message.toolCallId ? calls.get(String(message.toolCallId)) : undefined;
331
+ const target = toolTarget(call?.args);
332
+ const status = /error|failed|not found|denied|timed out/i.test(message.content) ? "error" : "ok";
333
+ const detail = firstImportantLine(message.content);
334
+ out.push({
335
+ ...message,
336
+ content: clipProjection(`${call?.name ?? "tool"}${target ? ` ${target}` : ""} → ${status}${detail ? `: ${detail}` : ""}`, TOOL_FOLD_CHARS),
337
+ });
338
+ continue;
339
+ }
340
+ if (message.role === "user" && message.content.startsWith("FastCode executed the requested local tools.")) {
341
+ out.push({ ...message, content: foldLegacyToolResults(message.content) });
342
+ continue;
343
+ }
344
+ out.push(message);
345
+ }
346
+ return out;
347
+ }
348
+ function foldLegacyToolResults(content) {
349
+ const header = "FastCode executed the requested local tools.";
350
+ const body = content.slice(content.indexOf(header) + header.length).trim();
351
+ const blocks = body.split(/\n\n(?=Tool result \d+ \()/);
352
+ const folded = [header];
353
+ for (const block of blocks) {
354
+ const statusMatch = block.match(/^Tool result \d+ \((ok|error)\)/);
355
+ if (!statusMatch)
356
+ continue;
357
+ const callMatch = block.match(/^call:\s*(\{.*\})$/m);
358
+ let call = {};
359
+ try {
360
+ call = callMatch ? JSON.parse(callMatch[1]) : {};
361
+ }
362
+ catch {
363
+ call = {};
364
+ }
365
+ const output = block.includes("output:") ? block.slice(block.indexOf("output:") + "output:".length) : block;
366
+ const target = toolTarget(call);
367
+ const detail = firstImportantLine(output);
368
+ folded.push(`- ${String(call.tool ?? "tool")}${target ? ` ${target}` : ""} → ${statusMatch[1]}${detail ? `: ${detail}` : ""}`);
369
+ }
370
+ return folded.length > 1 ? folded.join("\n") : clipProjection(content, TOOL_FOLD_CHARS);
371
+ }
372
+ function truncateMessageContent(content) {
373
+ if (content.length <= MAX_MESSAGE_CHARS)
374
+ return content;
375
+ const head = Math.floor(MAX_MESSAGE_CHARS * 0.6);
376
+ const tail = Math.max(0, MAX_MESSAGE_CHARS - head);
377
+ return `${content.slice(0, head)}\n[...truncated in projection...]\n${content.slice(-tail)}`;
378
+ }
379
+ function protectedMessage(message) {
380
+ return message.role === "system" && /^\[(context summary|context compacted|projection)/.test(message.content);
381
+ }
176
382
  function clampMessages(messages) {
177
- const list = messages.map((message) => {
178
- const content = message.content.length > MAX_MESSAGE_CHARS
179
- ? message.content.slice(0, MAX_MESSAGE_CHARS) + "\n[...truncated in projection...]"
180
- : message.content;
181
- return {
182
- role: message.role,
183
- content,
184
- ...(message.toolCalls ? { toolCalls: message.toolCalls } : {}),
185
- ...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
383
+ const list = messages.map((message) => ({
384
+ role: message.role,
385
+ content: truncateMessageContent(message.content),
386
+ ...(message.toolCalls ? { toolCalls: message.toolCalls } : {}),
387
+ ...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
388
+ }));
389
+ let dropped = 0;
390
+ const overBudget = () => list.length > 1 && Buffer.byteLength(JSON.stringify(list), "utf8") > MAX_PROJECTION_BYTES;
391
+ const dropOldest = (marker) => {
392
+ for (let index = 0; index < list.length; index += 1) {
393
+ const message = list[index];
394
+ if (index === 0 && message.role === "user")
395
+ continue;
396
+ if (message === marker || protectedMessage(message))
397
+ continue;
398
+ list.splice(index, 1);
399
+ dropped += 1;
400
+ return true;
401
+ }
402
+ return false;
403
+ };
404
+ while (overBudget() && dropOldest())
405
+ ;
406
+ if (dropped > 0) {
407
+ const marker = {
408
+ role: "system",
409
+ content: `[projection] ${dropped} earlier message(s) dropped to fit ${MAX_PROJECTION_BYTES} bytes.`,
186
410
  };
187
- });
188
- while (list.length > 1 && Buffer.byteLength(JSON.stringify(list), "utf8") > MAX_PROJECTION_BYTES) {
189
- if (list.length > 2 && list[0].role === "user")
190
- list.splice(1, 1);
191
- else
192
- list.shift();
411
+ const insertAt = list[0]?.role === "user" ? 1 : 0;
412
+ list.splice(insertAt, 0, marker);
413
+ while (overBudget() && dropOldest(marker)) {
414
+ marker.content = `[projection] ${dropped} earlier message(s) dropped to fit ${MAX_PROJECTION_BYTES} bytes.`;
415
+ }
193
416
  }
194
417
  return list;
195
418
  }
196
- export function readProjection(file) {
419
+ export function readProjectionResult(file) {
420
+ let raw;
197
421
  try {
198
- const raw = JSON.parse(readFileSync(projectionPath(file), "utf8"));
199
- if (!raw || raw.version !== 1 || !Array.isArray(raw.messages) || !raw.fingerprint)
200
- return undefined;
201
- return raw;
422
+ raw = JSON.parse(readFileSync(projectionPath(file), "utf8"));
202
423
  }
203
- catch {
204
- return undefined;
424
+ catch (err) {
425
+ if (err?.code === "ENOENT")
426
+ return { status: "missing" };
427
+ return { status: "corrupt" };
205
428
  }
429
+ if (!raw || raw.version !== 1 || !Array.isArray(raw.messages) || !raw.fingerprint)
430
+ return { status: "corrupt" };
431
+ const projection = raw;
432
+ if (!raw.manifest || typeof raw.manifest.contentHash !== "string")
433
+ return { status: "legacy", projection };
434
+ const actualHash = projectionContentHash(projection);
435
+ if (actualHash !== raw.manifest.contentHash)
436
+ return { status: "hash_mismatch", projection, expectedHash: raw.manifest.contentHash, actualHash };
437
+ return { status: "ok", projection, expectedHash: raw.manifest.contentHash, actualHash };
438
+ }
439
+ export function readProjection(file) {
440
+ const result = readProjectionResult(file);
441
+ return result.status === "ok" || result.status === "legacy" ? result.projection : undefined;
206
442
  }
207
- export function writeProjection(file, projection) {
443
+ export function writeProjection(file, projection, facts) {
208
444
  const target = projectionPath(file);
209
445
  const tmp = target + "." + process.pid + ".tmp";
210
446
  try {
211
- writeFileSync(tmp, JSON.stringify(projection, null, 2), "utf8");
447
+ writeFileSync(tmp, JSON.stringify(withManifest(projection, facts), null, 2), "utf8");
212
448
  renameSync(tmp, target);
213
449
  }
214
450
  catch (err) {
@@ -223,35 +459,68 @@ export function writeProjection(file, projection) {
223
459
  }
224
460
  export function resumeSession(file, currentCwd = process.cwd()) {
225
461
  const facts = readSession(file);
462
+ const hasFacts = facts.length > 0;
226
463
  const detected = sessionMetaFromFacts(facts);
464
+ const read = readProjectionResult(file);
465
+ const stored = read.projection;
227
466
  const meta = {
228
- cwd: detected.cwd || currentCwd,
229
- providerId: detected.providerId,
230
- model: detected.model,
467
+ cwd: detected.cwd || stored?.cwd || currentCwd,
468
+ providerId: detected.providerId || stored?.providerId || "",
469
+ model: detected.model || stored?.model || "",
231
470
  };
232
- const expected = projectionFingerprint(meta, compactionSourceFromFacts(facts));
233
- const stored = readProjection(file);
471
+ const expected = projectionFingerprint(meta, hasFacts ? compactionSourceFromFacts(factsAfterClear(facts)) : (stored?.fingerprint.compactionSource ?? "none"));
234
472
  const warnings = [];
235
473
  let projection;
236
474
  let rebuilt = false;
237
- if (stored && sameFingerprint(stored.fingerprint, expected)) {
475
+ const currentSessionHash = hasFacts ? factsContentHash(facts) : undefined;
476
+ const sessionChanged = hasFacts && Boolean(stored?.manifest?.sessionHash) &&
477
+ stored.manifest.sessionHash !== currentSessionHash;
478
+ const coveredSeq = stored?.manifest?.lastFactSeq ?? stored?.lastFactSeq ?? 0;
479
+ const factsNotCovered = hasFacts && coveredSeq < lastFactSeqOf(facts);
480
+ if (stored && (read.status === "ok" || read.status === "legacy") &&
481
+ sameFingerprint(stored.fingerprint, expected) && !sessionChanged && !factsNotCovered) {
238
482
  projection = stored;
483
+ if (read.status === "legacy")
484
+ warnings.push("Projection has no manifest; integrity could not be verified.");
239
485
  }
240
486
  else {
241
- projection = buildProjection(facts, meta);
242
- rebuilt = true;
243
- if (stored)
487
+ if (read.status === "hash_mismatch")
488
+ warnings.push("Projection manifest hash does not match its contents; ignoring the stored copy.");
489
+ else if (read.status === "corrupt")
490
+ warnings.push("Projection file is corrupt; rebuilding from session facts.");
491
+ else if (sessionChanged || factsNotCovered)
492
+ warnings.push("Session facts changed since the projection was saved; rebuilt from facts.");
493
+ else if (stored)
244
494
  warnings.push("Projection fingerprint changed (tools/model/cwd); rebuilt from facts.");
245
- else
246
- warnings.push("No projection found; rebuilt from facts.");
247
- try {
248
- writeProjection(file, projection);
495
+ if (hasFacts) {
496
+ projection = buildProjection(facts, meta);
497
+ rebuilt = true;
498
+ if (!stored)
499
+ warnings.push("No projection found; rebuilt from facts.");
500
+ try {
501
+ writeProjection(file, projection, facts);
502
+ }
503
+ catch {
504
+ warnings.push("Could not write the projection (filesystem is read-only?); using the in-memory rebuild.");
505
+ }
249
506
  }
250
- catch {
251
- warnings.push("Could not write the projection (filesystem is read-only?); using the in-memory rebuild.");
507
+ else if (stored && read.status !== "hash_mismatch" && read.status !== "corrupt") {
508
+ // The original JSONL was pruned. The verified projection is the
509
+ // only remaining evidence, so use it rather than re-deriving trust
510
+ // from facts that no longer exist.
511
+ projection = stored;
512
+ warnings.push("Session facts were pruned; using the stored projection.");
513
+ }
514
+ else {
515
+ projection = emptyProjection(meta);
516
+ warnings.push("Session content is unavailable or unverifiable; resumed history is empty.");
252
517
  }
253
518
  }
519
+ const repaired = pairToolMessages(projection.messages);
520
+ if (repaired.repaired)
521
+ warnings.push("Tool call/result pairing was repaired during resume.");
522
+ const finalProjection = { ...projection, messages: repaired.messages };
254
523
  if (detected.cwd && detected.cwd !== currentCwd)
255
524
  warnings.push(`Session was recorded in ${detected.cwd}; current directory is ${currentCwd}.`);
256
- return { projection, messages: projection.messages, meta, warnings, rebuilt };
525
+ return { projection: finalProjection, messages: finalProjection.messages, meta, warnings, rebuilt };
257
526
  }
@@ -1,3 +1,17 @@
1
+ export type SkillIntegrity = "ok" | "drift" | "unverified";
2
+ /**
3
+ * Pick the skills registry without treating an empty environment variable as
4
+ * "configured". `??` would keep `""` and make env overrides silently break;
5
+ * explicit null/empty checks keep `""` and `" "` on the config path. There is
6
+ * intentionally no built-in default registry.
7
+ */
8
+ export declare function resolveRegistry(envValue?: string | null, configValue?: string | null): string;
9
+ export declare function hashSkillDirectory(folder: string): string;
10
+ export declare function rebaselineSkills(names?: string[]): Array<{
11
+ name: string;
12
+ ok: boolean;
13
+ warning?: string;
14
+ }>;
1
15
  /** List skills currently installed under ~/.fastcode/skills. */
2
16
  export declare function listInstalledSkills(): any[];
3
17
  /** Look up a skill by name (matches folder name OR frontmatter name). */
@@ -9,6 +23,7 @@ export declare function installSkill(spec: any, opts?: any): Promise<{
9
23
  source: any;
10
24
  path: string;
11
25
  body: any;
26
+ integrity: SkillIntegrity;
12
27
  }>;
13
28
  /** Remove an installed skill by name or folder. */
14
29
  export declare function uninstallSkill(name: any): boolean;