@datename/fastcode-termux 0.1.6 → 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 (303) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +67 -16
  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 +83 -0
  10. package/dist/agent/approval.js +42 -0
  11. package/dist/agent/compaction.d.ts +41 -0
  12. package/dist/agent/compaction.js +270 -0
  13. package/dist/agent/context.d.ts +29 -0
  14. package/dist/agent/context.js +62 -0
  15. package/dist/agent/guards.d.ts +24 -16
  16. package/dist/agent/guards.js +1167 -18
  17. package/dist/agent/ignore.d.ts +11 -0
  18. package/dist/agent/ignore.js +129 -0
  19. package/dist/agent/policy/defaults.d.ts +16 -0
  20. package/dist/agent/policy/defaults.js +17 -0
  21. package/dist/agent/policy/egress.d.ts +32 -0
  22. package/dist/agent/policy/egress.js +280 -0
  23. package/dist/agent/process-tree.d.ts +22 -0
  24. package/dist/agent/process-tree.js +102 -0
  25. package/dist/agent/protocol-mode.d.ts +6 -0
  26. package/dist/agent/protocol-mode.js +51 -0
  27. package/dist/agent/protocol.d.ts +20 -1
  28. package/dist/agent/protocol.js +169 -39
  29. package/dist/agent/registry.d.ts +19 -0
  30. package/dist/agent/registry.js +247 -0
  31. package/dist/agent/runner.d.ts +127 -0
  32. package/dist/agent/runner.js +391 -0
  33. package/dist/agent/shell-ast.d.ts +11 -0
  34. package/dist/agent/shell-ast.js +204 -0
  35. package/dist/agent/shell-session.d.ts +51 -0
  36. package/dist/agent/shell-session.js +611 -0
  37. package/dist/agent/stream.d.ts +29 -0
  38. package/dist/agent/stream.js +103 -0
  39. package/dist/agent/tool-schemas.d.ts +2 -0
  40. package/dist/agent/tool-schemas.js +82 -0
  41. package/dist/agent/tools/append_file.d.ts +2 -0
  42. package/dist/agent/tools/append_file.js +28 -0
  43. package/dist/agent/tools/apply_patch.d.ts +10 -0
  44. package/dist/agent/tools/apply_patch.js +92 -0
  45. package/dist/agent/tools/copy_file.d.ts +2 -0
  46. package/dist/agent/tools/copy_file.js +53 -0
  47. package/dist/agent/tools/delete_file.d.ts +2 -0
  48. package/dist/agent/tools/delete_file.js +43 -0
  49. package/dist/agent/tools/grep.d.ts +2 -0
  50. package/dist/agent/tools/grep.js +54 -0
  51. package/dist/agent/tools/index.d.ts +2 -0
  52. package/dist/agent/tools/index.js +32 -0
  53. package/dist/agent/tools/list_files.d.ts +2 -0
  54. package/dist/agent/tools/list_files.js +17 -0
  55. package/dist/agent/tools/make_dir.d.ts +2 -0
  56. package/dist/agent/tools/make_dir.js +26 -0
  57. package/dist/agent/tools/move_path.d.ts +9 -0
  58. package/dist/agent/tools/move_path.js +73 -0
  59. package/dist/agent/tools/paths.d.ts +7 -0
  60. package/dist/agent/tools/paths.js +84 -0
  61. package/dist/agent/tools/pwd.d.ts +2 -0
  62. package/dist/agent/tools/pwd.js +10 -0
  63. package/dist/agent/tools/read_file.d.ts +2 -0
  64. package/dist/agent/tools/read_file.js +81 -0
  65. package/dist/agent/tools/read_many.d.ts +2 -0
  66. package/dist/agent/tools/read_many.js +43 -0
  67. package/dist/agent/tools/readers.d.ts +58 -0
  68. package/dist/agent/tools/readers.js +426 -0
  69. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  70. package/dist/agent/tools/replace_in_file.js +63 -0
  71. package/dist/agent/tools/shell.d.ts +2 -0
  72. package/dist/agent/tools/shell.js +48 -0
  73. package/dist/agent/tools/stat_path.d.ts +2 -0
  74. package/dist/agent/tools/stat_path.js +31 -0
  75. package/dist/agent/tools/types.d.ts +65 -0
  76. package/dist/agent/tools/types.js +1 -0
  77. package/dist/agent/tools/validate.d.ts +19 -0
  78. package/dist/agent/tools/validate.js +92 -0
  79. package/dist/agent/tools/write_file.d.ts +2 -0
  80. package/dist/agent/tools/write_file.js +28 -0
  81. package/dist/agent/tools.d.ts +3 -1
  82. package/dist/agent/tools.js +2 -377
  83. package/dist/app.d.ts +2 -1
  84. package/dist/app.js +846 -173
  85. package/dist/cli-main.js +363 -0
  86. package/dist/cli.js +444 -333
  87. package/dist/commands/about.js +1 -1
  88. package/dist/commands/attach.d.ts +9 -0
  89. package/dist/commands/attach.js +15 -0
  90. package/dist/commands/bg.d.ts +7 -0
  91. package/dist/commands/bg.js +9 -0
  92. package/dist/commands/cancel.d.ts +10 -0
  93. package/dist/commands/cancel.js +10 -0
  94. package/dist/commands/clear.d.ts +1 -0
  95. package/dist/commands/clear.js +1 -1
  96. package/dist/commands/compact.d.ts +22 -0
  97. package/dist/commands/compact.js +34 -0
  98. package/dist/commands/context.d.ts +7 -0
  99. package/dist/commands/context.js +25 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/expand.d.ts +10 -0
  105. package/dist/commands/expand.js +8 -0
  106. package/dist/commands/index.js +24 -0
  107. package/dist/commands/jobs.d.ts +7 -0
  108. package/dist/commands/jobs.js +17 -0
  109. package/dist/commands/kill.d.ts +7 -0
  110. package/dist/commands/kill.js +13 -0
  111. package/dist/commands/login.d.ts +6 -4
  112. package/dist/commands/login.js +24 -22
  113. package/dist/commands/logout.js +8 -16
  114. package/dist/commands/model.d.ts +1 -0
  115. package/dist/commands/model.js +10 -22
  116. package/dist/commands/provider.d.ts +1 -2
  117. package/dist/commands/provider.js +4 -11
  118. package/dist/commands/skills.js +34 -4
  119. package/dist/commands/steer.d.ts +11 -0
  120. package/dist/commands/steer.js +13 -0
  121. package/dist/commands/threads.d.ts +9 -0
  122. package/dist/commands/threads.js +26 -0
  123. package/dist/commands/whoami.js +6 -5
  124. package/dist/config/store.d.ts +53 -12
  125. package/dist/config/store.js +163 -73
  126. package/dist/config/trust.d.ts +30 -8
  127. package/dist/config/trust.js +139 -14
  128. package/dist/headless.d.ts +7 -0
  129. package/dist/headless.js +213 -0
  130. package/dist/index.js +19 -3
  131. package/dist/lib/debug.d.ts +2 -0
  132. package/dist/lib/debug.js +22 -0
  133. package/dist/lib/diagnostics.d.ts +48 -4
  134. package/dist/lib/diagnostics.js +90 -8
  135. package/dist/lib/highlight-subset.d.ts +21 -0
  136. package/dist/lib/highlight-subset.js +88 -0
  137. package/dist/lib/http.d.ts +11 -1
  138. package/dist/lib/http.js +62 -4
  139. package/dist/lib/lifecycle.d.ts +5 -0
  140. package/dist/lib/lifecycle.js +62 -0
  141. package/dist/lib/markdown.js +22 -4
  142. package/dist/lib/model-meta.d.ts +19 -0
  143. package/dist/lib/model-meta.js +113 -0
  144. package/dist/lib/native-tools.d.ts +12 -0
  145. package/dist/lib/native-tools.js +21 -0
  146. package/dist/lib/net.d.ts +33 -0
  147. package/dist/lib/net.js +168 -0
  148. package/dist/patch/apply.d.ts +15 -0
  149. package/dist/patch/apply.js +172 -0
  150. package/dist/patch/parse.d.ts +29 -0
  151. package/dist/patch/parse.js +112 -0
  152. package/dist/patch/plan.d.ts +49 -0
  153. package/dist/patch/plan.js +273 -0
  154. package/dist/providers/anthropic.d.ts +1 -0
  155. package/dist/providers/anthropic.js +12 -0
  156. package/dist/providers/custom.d.ts +9 -0
  157. package/dist/providers/deepseek.d.ts +10 -0
  158. package/dist/providers/deepseek.js +3 -2
  159. package/dist/providers/google.d.ts +9 -0
  160. package/dist/providers/groq.d.ts +10 -0
  161. package/dist/providers/groq.js +2 -1
  162. package/dist/providers/index.d.ts +10 -201
  163. package/dist/providers/index.js +30 -0
  164. package/dist/providers/mistral.d.ts +10 -0
  165. package/dist/providers/mistral.js +2 -1
  166. package/dist/providers/moonshot.d.ts +10 -0
  167. package/dist/providers/moonshot.js +2 -1
  168. package/dist/providers/ollama.d.ts +9 -0
  169. package/dist/providers/openai.d.ts +10 -0
  170. package/dist/providers/openai.js +2 -1
  171. package/dist/providers/openaiCompatible.d.ts +33 -2
  172. package/dist/providers/openaiCompatible.js +78 -16
  173. package/dist/providers/openrouter.d.ts +10 -0
  174. package/dist/providers/openrouter.js +2 -1
  175. package/dist/providers/qwen.d.ts +10 -0
  176. package/dist/providers/qwen.js +2 -1
  177. package/dist/providers/spec.d.ts +12 -0
  178. package/dist/providers/spec.js +29 -0
  179. package/dist/providers/xai.d.ts +10 -0
  180. package/dist/providers/xai.js +2 -1
  181. package/dist/providers/zhipu.d.ts +10 -0
  182. package/dist/providers/zhipu.js +2 -1
  183. package/dist/session/log.d.ts +49 -14
  184. package/dist/session/log.js +236 -11
  185. package/dist/session/projection.d.ts +76 -0
  186. package/dist/session/projection.js +526 -0
  187. package/dist/skills/manager.d.ts +15 -0
  188. package/dist/skills/manager.js +135 -18
  189. package/dist/types.d.ts +75 -1
  190. package/dist/ui/Message.d.ts +1 -7
  191. package/dist/ui/Message.js +13 -2
  192. package/dist/ui/Picker.d.ts +8 -0
  193. package/dist/ui/Picker.js +24 -0
  194. package/dist/ui/Prompt.d.ts +1 -2
  195. package/dist/ui/Prompt.js +2 -2
  196. package/dist/ui/Spinner.js +11 -5
  197. package/dist/ui/StatusBar.d.ts +2 -1
  198. package/dist/ui/StatusBar.js +6 -5
  199. package/package.json +3 -2
  200. package/src/agent/approval-json.ts +103 -0
  201. package/src/agent/approval-native.ts +168 -0
  202. package/src/agent/approval-store.ts +38 -0
  203. package/src/agent/approval.ts +94 -0
  204. package/src/agent/compaction.ts +321 -0
  205. package/src/agent/context.ts +97 -0
  206. package/src/agent/guards.ts +1239 -19
  207. package/src/agent/ignore.ts +147 -0
  208. package/src/agent/policy/defaults.ts +34 -0
  209. package/src/agent/policy/egress.ts +335 -0
  210. package/src/agent/process-tree.ts +114 -0
  211. package/src/agent/protocol-mode.ts +59 -0
  212. package/src/agent/protocol.ts +191 -40
  213. package/src/agent/registry.ts +270 -0
  214. package/src/agent/runner.ts +486 -0
  215. package/src/agent/shell-ast.ts +227 -0
  216. package/src/agent/shell-session.ts +673 -0
  217. package/src/agent/stream.ts +143 -0
  218. package/src/agent/tool-schemas.ts +86 -0
  219. package/src/agent/tools/append_file.ts +30 -0
  220. package/src/agent/tools/apply_patch.ts +95 -0
  221. package/src/agent/tools/copy_file.ts +56 -0
  222. package/src/agent/tools/delete_file.ts +45 -0
  223. package/src/agent/tools/grep.ts +57 -0
  224. package/src/agent/tools/index.ts +34 -0
  225. package/src/agent/tools/list_files.ts +19 -0
  226. package/src/agent/tools/make_dir.ts +28 -0
  227. package/src/agent/tools/move_path.ts +83 -0
  228. package/src/agent/tools/paths.ts +92 -0
  229. package/src/agent/tools/pwd.ts +12 -0
  230. package/src/agent/tools/read_file.ts +84 -0
  231. package/src/agent/tools/read_many.ts +45 -0
  232. package/src/agent/tools/readers.ts +491 -0
  233. package/src/agent/tools/replace_in_file.ts +66 -0
  234. package/src/agent/tools/shell.ts +53 -0
  235. package/src/agent/tools/stat_path.ts +33 -0
  236. package/src/agent/tools/types.ts +84 -0
  237. package/src/agent/tools/validate.ts +104 -0
  238. package/src/agent/tools/write_file.ts +30 -0
  239. package/src/agent/tools.ts +3 -377
  240. package/src/app.ts +853 -170
  241. package/src/{cli.ts → cli-main.ts} +64 -9
  242. package/src/commands/about.ts +1 -1
  243. package/src/commands/attach.ts +16 -0
  244. package/src/commands/bg.ts +9 -0
  245. package/src/commands/cancel.ts +10 -0
  246. package/src/commands/clear.ts +1 -1
  247. package/src/commands/compact.ts +35 -0
  248. package/src/commands/context.ts +26 -0
  249. package/src/commands/edit.ts +37 -0
  250. package/src/commands/effort.ts +49 -0
  251. package/src/commands/expand.ts +8 -0
  252. package/src/commands/index.ts +24 -0
  253. package/src/commands/jobs.ts +18 -0
  254. package/src/commands/kill.ts +14 -0
  255. package/src/commands/login.ts +24 -22
  256. package/src/commands/logout.ts +8 -16
  257. package/src/commands/model.ts +10 -22
  258. package/src/commands/provider.ts +4 -11
  259. package/src/commands/skills.ts +34 -4
  260. package/src/commands/steer.ts +13 -0
  261. package/src/commands/threads.ts +28 -0
  262. package/src/commands/whoami.ts +6 -5
  263. package/src/config/store.ts +168 -77
  264. package/src/config/trust.ts +181 -22
  265. package/src/headless.ts +225 -0
  266. package/src/index.ts +19 -3
  267. package/src/lib/debug.ts +24 -0
  268. package/src/lib/diagnostics.ts +90 -8
  269. package/src/lib/highlight-subset.ts +93 -0
  270. package/src/lib/http.ts +66 -4
  271. package/src/lib/lifecycle.ts +69 -0
  272. package/src/lib/markdown.ts +22 -4
  273. package/src/lib/model-meta.ts +138 -0
  274. package/src/lib/native-tools.ts +32 -0
  275. package/src/lib/net.ts +204 -0
  276. package/src/patch/apply.ts +194 -0
  277. package/src/patch/parse.ts +131 -0
  278. package/src/patch/plan.ts +317 -0
  279. package/src/providers/anthropic.ts +13 -1
  280. package/src/providers/deepseek.ts +3 -2
  281. package/src/providers/groq.ts +2 -1
  282. package/src/providers/index.ts +32 -1
  283. package/src/providers/mistral.ts +2 -1
  284. package/src/providers/moonshot.ts +2 -1
  285. package/src/providers/openai.ts +2 -1
  286. package/src/providers/openaiCompatible.ts +104 -19
  287. package/src/providers/openrouter.ts +2 -1
  288. package/src/providers/qwen.ts +2 -1
  289. package/src/providers/spec.ts +42 -0
  290. package/src/providers/xai.ts +2 -1
  291. package/src/providers/zhipu.ts +2 -1
  292. package/src/session/log.ts +311 -29
  293. package/src/session/projection.ts +621 -0
  294. package/src/skills/manager.ts +140 -18
  295. package/src/types/highlight-shims.d.ts +14 -0
  296. package/src/types.ts +79 -2
  297. package/src/ui/Message.ts +13 -2
  298. package/src/ui/Picker.ts +24 -0
  299. package/src/ui/Prompt.ts +2 -2
  300. package/src/ui/Spinner.ts +11 -5
  301. package/src/ui/StatusBar.ts +6 -5
  302. package/tsconfig.json +1 -0
  303. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -0,0 +1,621 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import type { ChatMessage } from "../types.js";
4
+ import { formatToolResults } from "../agent/protocol.js";
5
+ import { toolCatalogHash } from "../agent/registry.js";
6
+ import { projectionPath, readSession, type ActiveFile } from "./log.js";
7
+
8
+ export interface SessionMeta {
9
+ cwd: string;
10
+ providerId: string;
11
+ model: string;
12
+ }
13
+
14
+ export interface ProjectionFingerprint {
15
+ toolCatalogHash: string;
16
+ modelId: string;
17
+ cwdDigest: string;
18
+ compactionSource: "local" | "server" | "none";
19
+ }
20
+
21
+ export interface ProjectionManifest {
22
+ version: 1;
23
+ algorithm: "sha256";
24
+ savedAt: string;
25
+ /** Hash of the projection content (messages/meta/files). */
26
+ contentHash: string;
27
+ /** Hash of the source JSONL facts at save time, when available. */
28
+ sessionHash?: string;
29
+ /** Highest fact sequence covered by this projection. */
30
+ lastFactSeq?: number;
31
+ }
32
+
33
+ export interface Projection {
34
+ version: 1;
35
+ kind: "projection";
36
+ cwd: string;
37
+ providerId: string;
38
+ model: string;
39
+ fingerprint: ProjectionFingerprint;
40
+ messages: ChatMessage[];
41
+ activeFiles: ActiveFile[];
42
+ lastFactSeq: number;
43
+ /** Integrity data written at save/prune time; never re-derived from facts. */
44
+ manifest?: ProjectionManifest;
45
+ }
46
+
47
+ export type ProjectionReadStatus = "ok" | "legacy" | "hash_mismatch" | "corrupt" | "missing";
48
+
49
+ export interface ProjectionReadResult {
50
+ status: ProjectionReadStatus;
51
+ projection?: Projection;
52
+ expectedHash?: string;
53
+ actualHash?: string;
54
+ }
55
+
56
+ export interface ResumeState {
57
+ projection: Projection;
58
+ messages: ChatMessage[];
59
+ meta: SessionMeta;
60
+ warnings: string[];
61
+ rebuilt: boolean;
62
+ }
63
+
64
+ const READ_ONLY_TOOLS = new Set(["read_file", "read_many", "grep", "list_files", "stat_path", "pwd"]);
65
+ const IDEMPOTENT_TOOLS = new Set(["write_file"]);
66
+ const MAX_MESSAGE_CHARS = 8_000;
67
+ const MAX_PROJECTION_BYTES = 200_000;
68
+ const TOOL_FOLD_CHARS = 600;
69
+
70
+ export function unpairedToolNotice(tool: string): string {
71
+ if (READ_ONLY_TOOLS.has(tool))
72
+ return "执行中断(只读工具,可安全重试)。";
73
+ if (IDEMPOTENT_TOOLS.has(tool))
74
+ return "执行中断,可安全重试(write_file 为全量写)。";
75
+ return "执行状态未知:进程中断时命令可能已完成。请先验证再操作(stat / git status / git diff),不要直接重做。";
76
+ }
77
+
78
+ export function cwdDigest(cwd: string): string {
79
+ return createHash("sha1").update(cwd).digest("hex").slice(0, 8);
80
+ }
81
+
82
+ export function projectionFingerprint(meta: SessionMeta, compactionSource: ProjectionFingerprint["compactionSource"] = "none"): ProjectionFingerprint {
83
+ return {
84
+ toolCatalogHash: toolCatalogHash(),
85
+ modelId: `${meta.providerId}::${meta.model}`,
86
+ cwdDigest: cwdDigest(meta.cwd),
87
+ compactionSource,
88
+ };
89
+ }
90
+
91
+ export function sameFingerprint(a: ProjectionFingerprint | undefined, b: ProjectionFingerprint): boolean {
92
+ return Boolean(a) && a!.toolCatalogHash === b.toolCatalogHash && a!.modelId === b.modelId &&
93
+ a!.cwdDigest === b.cwdDigest && a!.compactionSource === b.compactionSource;
94
+ }
95
+
96
+ export function sessionMetaFromFacts(facts: any[]): SessionMeta {
97
+ let cwd = "";
98
+ let providerId = "";
99
+ let model = "";
100
+ let sawStart = false;
101
+ for (const fact of facts) {
102
+ // Only the first session_start defines the session. A resume appends
103
+ // another session_start with the current cwd/model, which must not
104
+ // change the fingerprint of the stored projection.
105
+ if (fact?.type === "session_start" && !sawStart) {
106
+ sawStart = true;
107
+ if (typeof fact.cwd === "string" && fact.cwd)
108
+ cwd = fact.cwd;
109
+ if (typeof fact.providerId === "string" && fact.providerId)
110
+ providerId = fact.providerId;
111
+ if (typeof fact.model === "string" && fact.model)
112
+ model = fact.model;
113
+ }
114
+ else if (fact?.type === "checkpoint") {
115
+ if (typeof fact.cwd === "string" && fact.cwd)
116
+ cwd = fact.cwd;
117
+ if (typeof fact.providerId === "string" && fact.providerId)
118
+ providerId = fact.providerId;
119
+ if (typeof fact.model === "string" && fact.model)
120
+ model = fact.model;
121
+ }
122
+ }
123
+ return { cwd, providerId, model };
124
+ }
125
+
126
+ function factsAfterClear(facts: any[]): any[] {
127
+ for (let index = facts.length - 1; index >= 0; index -= 1) {
128
+ if (facts[index]?.type === "cleared")
129
+ return facts.slice(index + 1);
130
+ }
131
+ return facts;
132
+ }
133
+
134
+ export function sessionMetaFromFile(file: string): SessionMeta {
135
+ return sessionMetaFromFacts(readSession(file));
136
+ }
137
+
138
+ function legacyMessages(facts: any[]): ChatMessage[] {
139
+ return facts
140
+ .filter((fact) => fact?.type === "message" && typeof fact.content === "string")
141
+ .map((fact) => ({ role: fact.role, content: fact.content }));
142
+ }
143
+
144
+ export function factsToMessages(facts: any[]): ChatMessage[] {
145
+ const calls = new Map<string, any>();
146
+ const pending: Array<{ call: any; ok: boolean; output: string }> = [];
147
+ const messages: ChatMessage[] = [];
148
+ let nativeTurn = false;
149
+ const flush = () => {
150
+ for (const [id, call] of calls) {
151
+ const output = unpairedToolNotice(String(call?.tool ?? "unknown"));
152
+ if (nativeTurn)
153
+ messages.push({ role: "tool", toolCallId: id, content: output });
154
+ else
155
+ pending.push({ call, ok: false, output });
156
+ }
157
+ calls.clear();
158
+ if (pending.length > 0)
159
+ messages.push({ role: "user", content: formatToolResults(pending.splice(0)) });
160
+ };
161
+ for (const fact of facts) {
162
+ if (!fact)
163
+ continue;
164
+ if (fact.type === "user") {
165
+ flush();
166
+ nativeTurn = false;
167
+ if (typeof fact.content === "string" && fact.content)
168
+ messages.push({ role: "user", content: fact.content });
169
+ }
170
+ else if (fact.type === "assistant_raw") {
171
+ flush();
172
+ const nativeCalls = Array.isArray(fact.toolCalls) ? fact.toolCalls : undefined;
173
+ nativeTurn = Boolean(nativeCalls && nativeCalls.length > 0);
174
+ if (typeof fact.content === "string")
175
+ messages.push({ role: "assistant", content: fact.content, ...(nativeTurn ? { toolCalls: nativeCalls } : {}) });
176
+ }
177
+ else if (fact.type === "tool_call") {
178
+ calls.set(String(fact.id ?? ""), fact.args ?? { tool: fact.tool });
179
+ }
180
+ else if (fact.type === "notice") {
181
+ if (typeof fact.content === "string" && fact.content)
182
+ messages.push({ role: "system", content: fact.content });
183
+ }
184
+ else if (fact.type === "tool_result") {
185
+ const id = String(fact.id ?? "");
186
+ const call = calls.get(id) ?? { tool: fact.tool ?? "unknown" };
187
+ calls.delete(id);
188
+ if (nativeTurn) {
189
+ messages.push({ role: "tool", toolCallId: id, content: String(fact.output ?? "") });
190
+ }
191
+ else {
192
+ pending.push({
193
+ call,
194
+ ok: Boolean(fact.ok),
195
+ output: String(fact.output ?? ""),
196
+ });
197
+ }
198
+ }
199
+ else if (fact.type === "egress_denied") {
200
+ const target = fact.target ? ` target=${JSON.stringify(fact.target)}` : "";
201
+ const content = `[egress] ${fact.mode ?? "audit"} rule=${fact.rule ?? "unknown"}${target} would_block=${Boolean(fact.wouldBlock)}\n${fact.reason ?? ""}`;
202
+ messages.push({ role: "system", content });
203
+ }
204
+ }
205
+ flush();
206
+ if (messages.length === 0)
207
+ return legacyMessages(facts);
208
+ return messages;
209
+ }
210
+
211
+ export function compactionSourceFromFacts(facts: any[]): ProjectionFingerprint["compactionSource"] {
212
+ let source: ProjectionFingerprint["compactionSource"] = "none";
213
+ for (const fact of facts) {
214
+ if (fact?.type === "compacted" && (fact.source === "local" || fact.source === "server"))
215
+ source = fact.source;
216
+ }
217
+ return source;
218
+ }
219
+
220
+ function latestCompaction(facts: any[]): any | undefined {
221
+ for (let i = facts.length - 1; i >= 0; i -= 1) {
222
+ if (facts[i]?.type === "compacted")
223
+ return facts[i];
224
+ }
225
+ return undefined;
226
+ }
227
+
228
+ function applySummary(messages: ChatMessage[], facts: any[]): ChatMessage[] {
229
+ const latest = latestCompaction(facts);
230
+ if (!latest || latest.source !== "local" || typeof latest.summaryRef !== "string")
231
+ return messages;
232
+ try {
233
+ const summary = readFileSync(latest.summaryRef, "utf8");
234
+ const keep = Number(latest.keepMessages);
235
+ const tail = Number.isFinite(keep) && keep > 0 ? messages.slice(-keep) : messages;
236
+ const first = messages[0]?.role === "user" ? messages[0] : undefined;
237
+ const marker: ChatMessage = { role: "system", content: `[context summary]\n${summary}` };
238
+ if (first && tail[0] !== first)
239
+ return [first, marker, ...tail];
240
+ return [marker, ...tail];
241
+ }
242
+ catch {
243
+ return [
244
+ ...messages,
245
+ { role: "system", content: "[projection] Summary file could not be read; resumed without the summary marker." },
246
+ ];
247
+ }
248
+ }
249
+
250
+ export function projectionContentHash(projection: Projection): string {
251
+ const core = {
252
+ version: projection.version,
253
+ kind: projection.kind,
254
+ cwd: projection.cwd,
255
+ providerId: projection.providerId,
256
+ model: projection.model,
257
+ fingerprint: projection.fingerprint,
258
+ messages: projection.messages,
259
+ activeFiles: projection.activeFiles,
260
+ lastFactSeq: projection.lastFactSeq,
261
+ };
262
+ return createHash("sha256").update(JSON.stringify(core)).digest("hex");
263
+ }
264
+
265
+ export function factsContentHash(facts: any[]): string {
266
+ return createHash("sha256").update(JSON.stringify(facts)).digest("hex");
267
+ }
268
+
269
+ function lastFactSeqOf(facts: any[]): number {
270
+ let seq = 0;
271
+ for (const fact of facts) {
272
+ if (typeof fact?.seq === "number" && fact.seq > seq)
273
+ seq = fact.seq;
274
+ }
275
+ return seq;
276
+ }
277
+
278
+ function withManifest(projection: Projection, facts?: any[]): Projection {
279
+ return {
280
+ ...projection,
281
+ manifest: {
282
+ version: 1,
283
+ algorithm: "sha256",
284
+ savedAt: new Date().toISOString(),
285
+ contentHash: projectionContentHash(projection),
286
+ ...(facts ? { sessionHash: factsContentHash(facts), lastFactSeq: lastFactSeqOf(facts) } : {}),
287
+ },
288
+ };
289
+ }
290
+
291
+ function emptyProjection(meta: SessionMeta): Projection {
292
+ return buildProjection([], meta);
293
+ }
294
+
295
+ /**
296
+ * Keep native tool_call/tool_result pairs complete. Orphan tool messages left
297
+ * behind by projection truncation are dropped; missing results are synthesized
298
+ * with the existing "execution unknown" notice so a resumed model never waits
299
+ * for a result that cannot arrive.
300
+ */
301
+ export function pairToolMessages(messages: ChatMessage[]): { messages: ChatMessage[]; repaired: boolean } {
302
+ const out: ChatMessage[] = [];
303
+ const expected = new Map<string, string>();
304
+ let repaired = false;
305
+ const synthesize = () => {
306
+ for (const [id, tool] of expected)
307
+ out.push({ role: "tool", toolCallId: id, content: unpairedToolNotice(tool) });
308
+ if (expected.size > 0)
309
+ repaired = true;
310
+ expected.clear();
311
+ };
312
+ for (const message of messages) {
313
+ if (message.role === "tool") {
314
+ if (!message.toolCallId || !expected.has(message.toolCallId)) {
315
+ repaired = true;
316
+ continue;
317
+ }
318
+ expected.delete(message.toolCallId);
319
+ out.push(message);
320
+ continue;
321
+ }
322
+ synthesize();
323
+ out.push(message);
324
+ if (message.role === "assistant" && Array.isArray(message.toolCalls)) {
325
+ for (const call of message.toolCalls) {
326
+ if (call?.id)
327
+ expected.set(String(call.id), String(call.name ?? "unknown"));
328
+ }
329
+ }
330
+ }
331
+ synthesize();
332
+ return { messages: out, repaired };
333
+ }
334
+
335
+ export function buildProjection(facts: any[], metaOverride?: Partial<SessionMeta>): Projection {
336
+ const detected = sessionMetaFromFacts(facts);
337
+ const meta: SessionMeta = {
338
+ cwd: metaOverride?.cwd || detected.cwd || process.cwd(),
339
+ providerId: metaOverride?.providerId || detected.providerId,
340
+ model: metaOverride?.model || detected.model,
341
+ };
342
+ const threadFacts = factsAfterClear(facts);
343
+ const folded = foldToolMessages(applySummary(factsToMessages(threadFacts), threadFacts));
344
+ const messages = pairToolMessages(clampMessages(folded)).messages;
345
+ const source = compactionSourceFromFacts(threadFacts);
346
+ let activeFiles: ActiveFile[] = [];
347
+ let lastFactSeq = 0;
348
+ for (const fact of facts) {
349
+ if (typeof fact?.seq === "number" && fact.seq > lastFactSeq)
350
+ lastFactSeq = fact.seq;
351
+ }
352
+ for (const fact of threadFacts) {
353
+ if (fact?.type === "checkpoint" && Array.isArray(fact.activeFiles))
354
+ activeFiles = fact.activeFiles;
355
+ }
356
+ const projection: Projection = {
357
+ version: 1,
358
+ kind: "projection",
359
+ cwd: meta.cwd,
360
+ providerId: meta.providerId,
361
+ model: meta.model,
362
+ fingerprint: projectionFingerprint(meta, source),
363
+ messages,
364
+ activeFiles: activeFiles.slice(0, 200),
365
+ lastFactSeq,
366
+ };
367
+ return projection;
368
+ }
369
+
370
+ function clipProjection(text: string, max: number): string {
371
+ const oneLine = String(text).replace(/\s+/g, " ").trim();
372
+ return oneLine.length > max ? oneLine.slice(0, Math.max(1, max - 1)) + "…" : oneLine;
373
+ }
374
+
375
+ function toolTarget(args: any): string {
376
+ if (!args || typeof args !== "object")
377
+ return "";
378
+ const value = args.path ?? args.from ?? args.to ?? args.command ?? args.pattern;
379
+ return typeof value === "string" ? clipProjection(value, 160) : "";
380
+ }
381
+
382
+ function firstImportantLine(text: string): string {
383
+ const lines = String(text)
384
+ .split(/\r?\n/)
385
+ .map((line) => line.trim())
386
+ .filter((line) => line && !/^```/.test(line) && line !== "output:");
387
+ const important = lines.find((line) => /error|failed|not found|denied|timed out|exit/i.test(line)) ?? lines[0] ?? "";
388
+ return clipProjection(important, 200);
389
+ }
390
+
391
+ /** Keep the edit fact (tool + target + outcome), not the full tool payload. */
392
+ function foldToolMessages(messages: ChatMessage[]): ChatMessage[] {
393
+ const calls = new Map<string, { name: string; args: any }>();
394
+ const out: ChatMessage[] = [];
395
+ for (const message of messages) {
396
+ if (message.role === "assistant" && Array.isArray(message.toolCalls)) {
397
+ for (const call of message.toolCalls) {
398
+ let args: any = {};
399
+ try {
400
+ args = JSON.parse(call.arguments || "{}");
401
+ }
402
+ catch {
403
+ args = {};
404
+ }
405
+ calls.set(String(call.id), { name: String(call.name ?? "tool"), args });
406
+ }
407
+ out.push(message);
408
+ continue;
409
+ }
410
+ if (message.role === "tool") {
411
+ const call = message.toolCallId ? calls.get(String(message.toolCallId)) : undefined;
412
+ const target = toolTarget(call?.args);
413
+ const status = /error|failed|not found|denied|timed out/i.test(message.content) ? "error" : "ok";
414
+ const detail = firstImportantLine(message.content);
415
+ out.push({
416
+ ...message,
417
+ content: clipProjection(`${call?.name ?? "tool"}${target ? ` ${target}` : ""} → ${status}${detail ? `: ${detail}` : ""}`, TOOL_FOLD_CHARS),
418
+ });
419
+ continue;
420
+ }
421
+ if (message.role === "user" && message.content.startsWith("FastCode executed the requested local tools.")) {
422
+ out.push({ ...message, content: foldLegacyToolResults(message.content) });
423
+ continue;
424
+ }
425
+ out.push(message);
426
+ }
427
+ return out;
428
+ }
429
+
430
+ function foldLegacyToolResults(content: string): string {
431
+ const header = "FastCode executed the requested local tools.";
432
+ const body = content.slice(content.indexOf(header) + header.length).trim();
433
+ const blocks = body.split(/\n\n(?=Tool result \d+ \()/);
434
+ const folded: string[] = [header];
435
+ for (const block of blocks) {
436
+ const statusMatch = block.match(/^Tool result \d+ \((ok|error)\)/);
437
+ if (!statusMatch)
438
+ continue;
439
+ const callMatch = block.match(/^call:\s*(\{.*\})$/m);
440
+ let call: any = {};
441
+ try {
442
+ call = callMatch ? JSON.parse(callMatch[1]) : {};
443
+ }
444
+ catch {
445
+ call = {};
446
+ }
447
+ const output = block.includes("output:") ? block.slice(block.indexOf("output:") + "output:".length) : block;
448
+ const target = toolTarget(call);
449
+ const detail = firstImportantLine(output);
450
+ folded.push(`- ${String(call.tool ?? "tool")}${target ? ` ${target}` : ""} → ${statusMatch[1]}${detail ? `: ${detail}` : ""}`);
451
+ }
452
+ return folded.length > 1 ? folded.join("\n") : clipProjection(content, TOOL_FOLD_CHARS);
453
+ }
454
+
455
+ function truncateMessageContent(content: string): string {
456
+ if (content.length <= MAX_MESSAGE_CHARS)
457
+ return content;
458
+ const head = Math.floor(MAX_MESSAGE_CHARS * 0.6);
459
+ const tail = Math.max(0, MAX_MESSAGE_CHARS - head);
460
+ return `${content.slice(0, head)}\n[...truncated in projection...]\n${content.slice(-tail)}`;
461
+ }
462
+
463
+ function protectedMessage(message: ChatMessage): boolean {
464
+ return message.role === "system" && /^\[(context summary|context compacted|projection)/.test(message.content);
465
+ }
466
+
467
+ function clampMessages(messages: ChatMessage[]): ChatMessage[] {
468
+ const list = messages.map((message) => ({
469
+ role: message.role,
470
+ content: truncateMessageContent(message.content),
471
+ ...(message.toolCalls ? { toolCalls: message.toolCalls } : {}),
472
+ ...(message.toolCallId ? { toolCallId: message.toolCallId } : {}),
473
+ }));
474
+ let dropped = 0;
475
+ const overBudget = () => list.length > 1 && Buffer.byteLength(JSON.stringify(list), "utf8") > MAX_PROJECTION_BYTES;
476
+ const dropOldest = (marker?: ChatMessage): boolean => {
477
+ for (let index = 0; index < list.length; index += 1) {
478
+ const message = list[index];
479
+ if (index === 0 && message.role === "user")
480
+ continue;
481
+ if (message === marker || protectedMessage(message))
482
+ continue;
483
+ list.splice(index, 1);
484
+ dropped += 1;
485
+ return true;
486
+ }
487
+ return false;
488
+ };
489
+ while (overBudget() && dropOldest())
490
+ ;
491
+ if (dropped > 0) {
492
+ const marker: ChatMessage = {
493
+ role: "system",
494
+ content: `[projection] ${dropped} earlier message(s) dropped to fit ${MAX_PROJECTION_BYTES} bytes.`,
495
+ };
496
+ const insertAt = list[0]?.role === "user" ? 1 : 0;
497
+ list.splice(insertAt, 0, marker);
498
+ while (overBudget() && dropOldest(marker)) {
499
+ marker.content = `[projection] ${dropped} earlier message(s) dropped to fit ${MAX_PROJECTION_BYTES} bytes.`;
500
+ }
501
+ }
502
+ return list;
503
+ }
504
+
505
+ export function readProjectionResult(file: string): ProjectionReadResult {
506
+ let raw: any;
507
+ try {
508
+ raw = JSON.parse(readFileSync(projectionPath(file), "utf8"));
509
+ }
510
+ catch (err: any) {
511
+ if (err?.code === "ENOENT")
512
+ return { status: "missing" };
513
+ return { status: "corrupt" };
514
+ }
515
+ if (!raw || raw.version !== 1 || !Array.isArray(raw.messages) || !raw.fingerprint)
516
+ return { status: "corrupt" };
517
+ const projection = raw as Projection;
518
+ if (!raw.manifest || typeof raw.manifest.contentHash !== "string")
519
+ return { status: "legacy", projection };
520
+ const actualHash = projectionContentHash(projection);
521
+ if (actualHash !== raw.manifest.contentHash)
522
+ return { status: "hash_mismatch", projection, expectedHash: raw.manifest.contentHash, actualHash };
523
+ return { status: "ok", projection, expectedHash: raw.manifest.contentHash, actualHash };
524
+ }
525
+
526
+ export function readProjection(file: string): Projection | undefined {
527
+ const result = readProjectionResult(file);
528
+ return result.status === "ok" || result.status === "legacy" ? result.projection : undefined;
529
+ }
530
+
531
+ export function writeProjection(file: string, projection: Projection, facts?: any[]): void {
532
+ const target = projectionPath(file);
533
+ const tmp = target + "." + process.pid + ".tmp";
534
+ try {
535
+ writeFileSync(tmp, JSON.stringify(withManifest(projection, facts), null, 2), "utf8");
536
+ renameSync(tmp, target);
537
+ }
538
+ catch (err) {
539
+ try {
540
+ rmSync(tmp, { force: true });
541
+ }
542
+ catch {
543
+ // best effort
544
+ }
545
+ throw err;
546
+ }
547
+ }
548
+
549
+ export function resumeSession(file: string, currentCwd = process.cwd()): ResumeState {
550
+ const facts = readSession(file);
551
+ const hasFacts = facts.length > 0;
552
+ const detected = sessionMetaFromFacts(facts);
553
+ const read = readProjectionResult(file);
554
+ const stored = read.projection;
555
+ const meta: SessionMeta = {
556
+ cwd: detected.cwd || stored?.cwd || currentCwd,
557
+ providerId: detected.providerId || stored?.providerId || "",
558
+ model: detected.model || stored?.model || "",
559
+ };
560
+ const expected = projectionFingerprint(
561
+ meta,
562
+ hasFacts ? compactionSourceFromFacts(factsAfterClear(facts)) : (stored?.fingerprint.compactionSource ?? "none"),
563
+ );
564
+ const warnings: string[] = [];
565
+ let projection: Projection | undefined;
566
+ let rebuilt = false;
567
+ const currentSessionHash = hasFacts ? factsContentHash(facts) : undefined;
568
+ const sessionChanged = hasFacts && Boolean(stored?.manifest?.sessionHash) &&
569
+ stored!.manifest!.sessionHash !== currentSessionHash;
570
+ const coveredSeq = stored?.manifest?.lastFactSeq ?? stored?.lastFactSeq ?? 0;
571
+ const factsNotCovered = hasFacts && coveredSeq < lastFactSeqOf(facts);
572
+
573
+ if (stored && (read.status === "ok" || read.status === "legacy") &&
574
+ sameFingerprint(stored.fingerprint, expected) && !sessionChanged && !factsNotCovered) {
575
+ projection = stored;
576
+ if (read.status === "legacy")
577
+ warnings.push("Projection has no manifest; integrity could not be verified.");
578
+ }
579
+ else {
580
+ if (read.status === "hash_mismatch")
581
+ warnings.push("Projection manifest hash does not match its contents; ignoring the stored copy.");
582
+ else if (read.status === "corrupt")
583
+ warnings.push("Projection file is corrupt; rebuilding from session facts.");
584
+ else if (sessionChanged || factsNotCovered)
585
+ warnings.push("Session facts changed since the projection was saved; rebuilt from facts.");
586
+ else if (stored)
587
+ warnings.push("Projection fingerprint changed (tools/model/cwd); rebuilt from facts.");
588
+
589
+ if (hasFacts) {
590
+ projection = buildProjection(facts, meta);
591
+ rebuilt = true;
592
+ if (!stored)
593
+ warnings.push("No projection found; rebuilt from facts.");
594
+ try {
595
+ writeProjection(file, projection, facts);
596
+ }
597
+ catch {
598
+ warnings.push("Could not write the projection (filesystem is read-only?); using the in-memory rebuild.");
599
+ }
600
+ }
601
+ else if (stored && read.status !== "hash_mismatch" && read.status !== "corrupt") {
602
+ // The original JSONL was pruned. The verified projection is the
603
+ // only remaining evidence, so use it rather than re-deriving trust
604
+ // from facts that no longer exist.
605
+ projection = stored;
606
+ warnings.push("Session facts were pruned; using the stored projection.");
607
+ }
608
+ else {
609
+ projection = emptyProjection(meta);
610
+ warnings.push("Session content is unavailable or unverifiable; resumed history is empty.");
611
+ }
612
+ }
613
+
614
+ const repaired = pairToolMessages(projection.messages);
615
+ if (repaired.repaired)
616
+ warnings.push("Tool call/result pairing was repaired during resume.");
617
+ const finalProjection: Projection = { ...projection, messages: repaired.messages };
618
+ if (detected.cwd && detected.cwd !== currentCwd)
619
+ warnings.push(`Session was recorded in ${detected.cwd}; current directory is ${currentCwd}.`);
620
+ return { projection: finalProjection, messages: finalProjection.messages, meta, warnings, rebuilt };
621
+ }