@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
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Process-level shutdown guards.
3
+ *
4
+ * The TUI already calls shutdownShells() from its React cleanup, but fatal
5
+ * errors and SIGTERM/SIGHUP can bypass that path. These guards are best-effort
6
+ * and idempotent; they do not turn FastCode into a kernel-level sandbox.
7
+ */
8
+ import { shutdownShells } from "../agent/shell-session.js";
9
+ let installed = false;
10
+ let cleaned = false;
11
+ export function cleanupProcesses() {
12
+ if (cleaned)
13
+ return;
14
+ cleaned = true;
15
+ try {
16
+ shutdownShells();
17
+ }
18
+ catch {
19
+ // Best effort: never mask the original failure.
20
+ }
21
+ }
22
+ export function installProcessGuards(options = {}) {
23
+ if (installed)
24
+ return () => undefined;
25
+ installed = true;
26
+ const onSignal = (signal) => {
27
+ process.stderr.write(`[fastcode] ${signal} received; shutting down.\n`);
28
+ cleanupProcesses();
29
+ process.exit(signal === "SIGINT" ? 130 : 143);
30
+ };
31
+ const onFatal = (error) => {
32
+ const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
33
+ process.stderr.write(`[fastcode] fatal: ${detail}\n`);
34
+ cleanupProcesses();
35
+ options.onFatal?.(error);
36
+ process.exit(1);
37
+ };
38
+ const signals = ["SIGTERM", "SIGHUP"];
39
+ for (const signal of signals) {
40
+ try {
41
+ process.on(signal, onSignal);
42
+ }
43
+ catch {
44
+ // Some platforms do not expose every signal.
45
+ }
46
+ }
47
+ process.on("uncaughtException", onFatal);
48
+ process.on("unhandledRejection", onFatal);
49
+ return () => {
50
+ for (const signal of signals) {
51
+ try {
52
+ process.removeListener(signal, onSignal);
53
+ }
54
+ catch {
55
+ // Ignore unsupported signals.
56
+ }
57
+ }
58
+ process.removeListener("uncaughtException", onFatal);
59
+ process.removeListener("unhandledRejection", onFatal);
60
+ installed = false;
61
+ };
62
+ }
@@ -6,8 +6,10 @@
6
6
  * markdown parser. The result is a fully-formatted ANSI string we can
7
7
  * print directly inside an Ink <Text>.
8
8
  */
9
+ import chalk from "chalk";
9
10
  import { marked } from "marked";
10
11
  import { markedTerminal } from "marked-terminal";
12
+ import { supportsHighlightLanguage } from "./highlight-subset.js";
11
13
  let configured = false;
12
14
  function configure() {
13
15
  if (configured)
@@ -15,16 +17,32 @@ function configure() {
15
17
  marked.use(markedTerminal({
16
18
  reflowText: false,
17
19
  tab: 2,
20
+ // marked-terminal calls this only when highlighting fails; keep the
21
+ // cold-language fallback as plain text instead of colored source.
22
+ code: (text) => text,
18
23
  }));
19
24
  configured = true;
20
25
  }
26
+ function unsupportedFenceLanguages(md) {
27
+ const missing = new Set();
28
+ for (const match of md.matchAll(/(?:^|\n)\s*(?:`{3,}|~{3,})([^\n]*)/g)) {
29
+ const info = (match[1] ?? "").trim();
30
+ if (!info)
31
+ continue;
32
+ const language = info.split(/\s+/)[0] ?? "";
33
+ if (language && !supportsHighlightLanguage(language))
34
+ missing.add(language);
35
+ }
36
+ return Array.from(missing);
37
+ }
21
38
  export function renderMarkdown(md) {
22
39
  configure();
23
40
  try {
24
- const out = marked.parse(md);
25
- // marked-terminal sometimes appends a stray trailing newline; trim only
26
- // trailing whitespace so we don't lose meaningful blank lines.
27
- return out.replace(/\s+$/, "");
41
+ const out = marked.parse(md).replace(/\s+$/, "");
42
+ const missing = unsupportedFenceLanguages(String(md));
43
+ if (missing.length === 0)
44
+ return out;
45
+ return out + "\n\n" + chalk.dim(`[fastcode] no syntax highlighting for ${missing.join(", ")}; rendered as plain text.`);
28
46
  }
29
47
  catch {
30
48
  return md;
@@ -9,7 +9,7 @@ export const deepseekProvider = {
9
9
  label: "DeepSeek",
10
10
  authEnv: ["DEEPSEEK_API_KEY"],
11
11
  baseUrl: BASE,
12
- defaultModel: "deepseek-v4-pro",
12
+ defaultModel: "deepseek-flash",
13
13
  listModels: (apiKey) => listModelsHttp({ baseUrl: BASE, apiKey, staticModels: STATIC }),
14
14
  stream: (req, apiKey) => streamChatCompletion(req, { baseUrl: BASE, apiKey, staticModels: STATIC, reasoning: "openai" }),
15
15
  supportsReasoning: () => true,
@@ -1,4 +1,26 @@
1
- export declare function streamChatCompletion(req: any, opts: any): AsyncGenerator<{
1
+ import { type RequestSpec } from "./spec.js";
2
+ export interface ModelInfo {
3
+ id: string;
4
+ label?: string;
5
+ [key: string]: any;
6
+ }
7
+ export interface StreamOptions {
8
+ baseUrl: string;
9
+ apiKey?: string;
10
+ chatPath?: string;
11
+ modelsPath?: string | null;
12
+ query?: Record<string, string>;
13
+ pathParams?: Record<string, string>;
14
+ authHeaderName?: string;
15
+ authPrefix?: string;
16
+ staticModels?: ModelInfo[];
17
+ extraHeaders?: Record<string, string>;
18
+ /** Legacy auth prefix, e.g. "Bearer". Prefer authPrefix for new callers. */
19
+ authHeader?: string;
20
+ reasoning?: "openai" | "openrouter" | "none" | string;
21
+ }
22
+ export declare function toRequestSpec(opts: StreamOptions): RequestSpec;
23
+ export declare function streamChatCompletion(req: any, opts: StreamOptions): AsyncGenerator<{
2
24
  delta: string;
3
25
  done: boolean;
4
26
  reasoning?: undefined;
@@ -23,4 +45,4 @@ export declare function streamChatCompletion(req: any, opts: any): AsyncGenerato
23
45
  reasoning?: undefined;
24
46
  toolCalls?: undefined;
25
47
  }, void, unknown>;
26
- export declare function listModelsHttp(opts: any): Promise<any>;
48
+ export declare function listModelsHttp(opts: StreamOptions): Promise<any>;
@@ -1,5 +1,17 @@
1
1
  import { fetchJson, streamSSE } from "../lib/http.js";
2
2
  import { finalizeToolCalls, mergeToolCallDelta } from "../lib/native-tools.js";
3
+ import { buildHeaders, buildRequestUrl } from "./spec.js";
4
+ export function toRequestSpec(opts) {
5
+ return {
6
+ baseUrl: trim(opts.baseUrl),
7
+ chatPath: opts.chatPath,
8
+ modelsPath: opts.modelsPath,
9
+ query: opts.query,
10
+ pathParams: opts.pathParams,
11
+ authHeaderName: opts.authHeaderName,
12
+ authPrefix: opts.authPrefix ?? (opts.authHeader !== undefined ? `${opts.authHeader} ` : undefined),
13
+ };
14
+ }
3
15
  /** Build a chat-completions body in the classic or the newer (compat) dialect. */
4
16
  function buildBody(req, compat, opts, includeReasoning = true) {
5
17
  const messages = [];
@@ -48,6 +60,7 @@ function buildBody(req, compat, opts, includeReasoning = true) {
48
60
  }
49
61
  export async function* streamChatCompletion(req, opts) {
50
62
  const hasReasoning = Boolean(req.reasoningEffort && req.reasoningEffort !== "auto" && opts.reasoning && opts.reasoning !== "none");
63
+ const spec = toRequestSpec(opts);
51
64
  const send = async (compat, includeReasoning = hasReasoning) => {
52
65
  const headers = {
53
66
  "content-type": "application/json",
@@ -55,10 +68,10 @@ export async function* streamChatCompletion(req, opts) {
55
68
  ...(opts.extraHeaders ?? {}),
56
69
  };
57
70
  if (opts.apiKey)
58
- headers["authorization"] = (opts.authHeader ?? "Bearer") + " " + opts.apiKey;
71
+ Object.assign(headers, buildHeaders(spec, opts.apiKey));
59
72
  if (req.clientRequestId)
60
73
  headers["idempotency-key"] = req.clientRequestId;
61
- return await fetch(trim(opts.baseUrl) + "/chat/completions", {
74
+ return await fetch(buildRequestUrl(spec, opts.chatPath ?? "/chat/completions"), {
62
75
  method: "POST",
63
76
  headers,
64
77
  body: JSON.stringify(buildBody(req, compat, opts, includeReasoning)),
@@ -130,13 +143,14 @@ export async function* streamChatCompletion(req, opts) {
130
143
  }
131
144
  export async function listModelsHttp(opts) {
132
145
  const staticModels = opts.staticModels ?? [];
146
+ if (opts.modelsPath === null)
147
+ return staticModels;
133
148
  if (!opts.apiKey && staticModels.length)
134
149
  return staticModels;
135
150
  try {
136
- const headers = {};
137
- if (opts.apiKey)
138
- headers["authorization"] = (opts.authHeader ?? "Bearer") + " " + opts.apiKey;
139
- const json = await fetchJson(trim(opts.baseUrl) + "/models", { headers });
151
+ const spec = toRequestSpec(opts);
152
+ const headers = opts.apiKey ? buildHeaders(spec, opts.apiKey) : {};
153
+ const json = await fetchJson(buildRequestUrl(spec, opts.modelsPath ?? "/models"), { headers });
140
154
  const ids = (json.data ?? [])
141
155
  .map((m) => m.id)
142
156
  .filter((id) => typeof id === "string" && id.length > 0);
@@ -0,0 +1,12 @@
1
+ export interface RequestSpec {
2
+ baseUrl: string;
3
+ chatPath?: string;
4
+ modelsPath?: string | null;
5
+ query?: Record<string, string>;
6
+ pathParams?: Record<string, string>;
7
+ authHeaderName?: string;
8
+ authPrefix?: string;
9
+ }
10
+ export declare function resolveBaseUrl(spec: RequestSpec): string;
11
+ export declare function buildRequestUrl(spec: RequestSpec, path: string, params?: Record<string, string>): string;
12
+ export declare function buildHeaders(spec: RequestSpec, apiKey: string, extra?: Record<string, string>): Record<string, string>;
@@ -0,0 +1,29 @@
1
+ const ENV_MAP = {
2
+ region: "AWS_REGION",
3
+ account_id: "CLOUDFLARE_ACCOUNT_ID",
4
+ project_id: "GOOGLE_CLOUD_PROJECT",
5
+ deployment: "AZURE_OPENAI_DEPLOYMENT",
6
+ };
7
+ export function resolveBaseUrl(spec) {
8
+ return spec.baseUrl.replace(/\{(\w+)\}/g, (_, key) => {
9
+ const envName = ENV_MAP[key];
10
+ const value = envName ? process.env[envName] : undefined;
11
+ if (!value)
12
+ throw new Error(`Missing ${envName ?? key} for placeholder {${key}}`);
13
+ return value;
14
+ });
15
+ }
16
+ export function buildRequestUrl(spec, path, params) {
17
+ const base = resolveBaseUrl(spec);
18
+ const merged = { ...spec.pathParams, ...params };
19
+ const resolvedPath = path.replace(/\{(\w+)\}/g, (_, key) => merged[key] ?? `{${key}}`);
20
+ const query = spec.query ? "?" + new URLSearchParams(spec.query).toString() : "";
21
+ return base + resolvedPath + query;
22
+ }
23
+ export function buildHeaders(spec, apiKey, extra) {
24
+ const headers = {
25
+ [spec.authHeaderName ?? "authorization"]: (spec.authPrefix ?? "Bearer ") + apiKey,
26
+ ...extra,
27
+ };
28
+ return headers;
29
+ }
@@ -14,24 +14,36 @@ export interface SessionSummary {
14
14
  messages: number;
15
15
  lastTs: number;
16
16
  }
17
+ /** Reserved slot for conversation compaction at the exit save point. */
18
+ export interface SessionCompactionSlot {
19
+ source?: "local" | "server" | "none";
20
+ summaryRef?: string | null;
21
+ pending?: boolean;
22
+ }
23
+ export interface SessionCloseInfo {
24
+ reason?: "exit" | "signal" | "fatal" | "close";
25
+ compaction?: SessionCompactionSlot;
26
+ }
17
27
  export declare function sessionCacheLimitBytes(): number;
18
28
  export declare function loggingEnabled(): boolean;
19
29
  export declare function projectionPath(file: string): string;
20
30
  export declare function fsyncMode(): "on" | "off" | "auto";
21
31
  export declare function fsyncEnabled(type?: string): boolean;
32
+ /** Summary files referenced by older compacted facts in this session. */
33
+ export declare function staleSummaryRefs(file: string): string[];
22
34
  export declare function createSessionLog(meta?: Record<string, any>): {
23
35
  id: string;
24
36
  file: string;
25
37
  append: (entry: Record<string, any>) => number;
26
38
  count(): number;
27
- close(): void;
39
+ close(info?: SessionCloseInfo): void;
28
40
  };
29
41
  export declare function openSessionLog(file: string, meta?: Record<string, any>): {
30
42
  id: string;
31
43
  file: string;
32
44
  append: (entry: Record<string, any>) => number;
33
45
  count(): number;
34
- close(): void;
46
+ close(info?: SessionCloseInfo): void;
35
47
  };
36
48
  export declare function listSessions(): {
37
49
  id: string;
@@ -10,9 +10,10 @@
10
10
  * FASTCODE_SESSION_CACHE_MB=N sessions cache cap in MB (default 256)
11
11
  * FASTCODE_FSYNC=0|1 force checkpoint fsync off/on
12
12
  */
13
- import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
14
- import { basename, join } from "node:path";
13
+ import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, writeSync } from "node:fs";
14
+ import { join } from "node:path";
15
15
  import { getPaths } from "../config/paths.js";
16
+ import { buildProjection, writeProjection } from "./projection.js";
16
17
  const NL = String.fromCharCode(10);
17
18
  export function sessionCacheLimitBytes() {
18
19
  const mb = Number(process.env["FASTCODE_SESSION_CACHE_MB"]);
@@ -63,6 +64,23 @@ function writeLine(file, line, type) {
63
64
  closeSync(fd);
64
65
  }
65
66
  }
67
+ function fsyncPath(file) {
68
+ if (!existsSync(file))
69
+ return;
70
+ try {
71
+ const fd = openSync(file, "r");
72
+ try {
73
+ fsyncSync(fd);
74
+ }
75
+ finally {
76
+ closeSync(fd);
77
+ }
78
+ }
79
+ catch {
80
+ // FUSE / read-only filesystems may reject fsync; the append is still
81
+ // best effort and the caller already caught close-level failures.
82
+ }
83
+ }
66
84
  function countLines(file) {
67
85
  if (!existsSync(file))
68
86
  return 0;
@@ -101,10 +119,25 @@ function openLog(id, file, meta) {
101
119
  count() {
102
120
  return seq;
103
121
  },
104
- close() {
105
- append({ type: "session_end" });
122
+ close(info = {}) {
123
+ append({
124
+ type: "session_end",
125
+ reason: info.reason ?? "close",
126
+ // Reserved extension point: future compaction can fill this
127
+ // slot without changing the session_end event shape.
128
+ compaction: {
129
+ source: info.compaction?.source ?? "none",
130
+ summaryRef: info.compaction?.summaryRef ?? null,
131
+ pending: info.compaction?.pending ?? false,
132
+ },
133
+ });
106
134
  closed = true;
107
135
  try {
136
+ // Snapshot + manifest are written while the facts still exist;
137
+ // pruning later keeps this verified copy.
138
+ ensureProjection(file);
139
+ fsyncPath(file);
140
+ fsyncPath(projectionPath(file));
108
141
  pruneSessions();
109
142
  }
110
143
  catch {
@@ -113,6 +146,21 @@ function openLog(id, file, meta) {
113
146
  },
114
147
  };
115
148
  }
149
+ /** Summary files referenced by older compacted facts in this session. */
150
+ export function staleSummaryRefs(file) {
151
+ let facts;
152
+ try {
153
+ facts = readSession(file);
154
+ }
155
+ catch {
156
+ return [];
157
+ }
158
+ const compacted = facts.filter((fact) => fact?.type === "compacted");
159
+ const latest = compacted.at(-1)?.summaryRef;
160
+ return Array.from(new Set(compacted
161
+ .map((fact) => fact.summaryRef)
162
+ .filter((ref) => typeof ref === "string" && ref.length > 0 && ref !== latest)));
163
+ }
116
164
  export function createSessionLog(meta = {}) {
117
165
  const { sessions } = getPaths();
118
166
  mkdirSync(sessions, { recursive: true });
@@ -287,32 +335,13 @@ export function latestUnclosedSession() {
287
335
  return findUnclosedSessions()[0];
288
336
  }
289
337
  function ensureProjection(file) {
290
- const target = projectionPath(file);
291
- if (existsSync(target))
292
- return;
338
+ // Always refresh from the facts that still exist: a projection written at
339
+ // an earlier checkpoint may be stale by the time we close or prune. If the
340
+ // JSONL is already gone, leave the verified projection untouched.
293
341
  const entries = readSession(file);
294
342
  if (entries.length === 0)
295
343
  return;
296
- const summary = summarize(basename(file).replace(/[.]jsonl$/, ""), file, entries);
297
- const lastUser = [...entries].reverse().find((entry) => entry.type === "message" && entry.role === "user");
298
- const lastAssistant = [...entries].reverse().find((entry) => entry.type === "message" && entry.role === "assistant");
299
- const payload = {
300
- version: 1,
301
- kind: "pruned_session",
302
- source: basename(file),
303
- createdAt: new Date().toISOString(),
304
- summary: {
305
- cwd: summary.cwd,
306
- providerId: summary.providerId,
307
- model: summary.model,
308
- turns: summary.turns,
309
- messages: summary.messages,
310
- activeFiles: summary.activeFiles.slice(0, 200),
311
- lastUser: typeof lastUser?.content === "string" ? lastUser.content.slice(0, 2000) : "",
312
- lastAssistant: typeof lastAssistant?.content === "string" ? lastAssistant.content.slice(0, 2000) : "",
313
- },
314
- };
315
- writeFileSync(target, JSON.stringify(payload, null, 2), "utf8");
344
+ writeProjection(file, buildProjection(entries), entries);
316
345
  }
317
346
  export function pruneSessions(limit = sessionCacheLimitBytes()) {
318
347
  const all = listSessions();
@@ -11,6 +11,17 @@ export interface ProjectionFingerprint {
11
11
  cwdDigest: string;
12
12
  compactionSource: "local" | "server" | "none";
13
13
  }
14
+ export interface ProjectionManifest {
15
+ version: 1;
16
+ algorithm: "sha256";
17
+ savedAt: string;
18
+ /** Hash of the projection content (messages/meta/files). */
19
+ contentHash: string;
20
+ /** Hash of the source JSONL facts at save time, when available. */
21
+ sessionHash?: string;
22
+ /** Highest fact sequence covered by this projection. */
23
+ lastFactSeq?: number;
24
+ }
14
25
  export interface Projection {
15
26
  version: 1;
16
27
  kind: "projection";
@@ -21,6 +32,15 @@ export interface Projection {
21
32
  messages: ChatMessage[];
22
33
  activeFiles: ActiveFile[];
23
34
  lastFactSeq: number;
35
+ /** Integrity data written at save/prune time; never re-derived from facts. */
36
+ manifest?: ProjectionManifest;
37
+ }
38
+ export type ProjectionReadStatus = "ok" | "legacy" | "hash_mismatch" | "corrupt" | "missing";
39
+ export interface ProjectionReadResult {
40
+ status: ProjectionReadStatus;
41
+ projection?: Projection;
42
+ expectedHash?: string;
43
+ actualHash?: string;
24
44
  }
25
45
  export interface ResumeState {
26
46
  projection: Projection;
@@ -37,7 +57,20 @@ export declare function sessionMetaFromFacts(facts: any[]): SessionMeta;
37
57
  export declare function sessionMetaFromFile(file: string): SessionMeta;
38
58
  export declare function factsToMessages(facts: any[]): ChatMessage[];
39
59
  export declare function compactionSourceFromFacts(facts: any[]): ProjectionFingerprint["compactionSource"];
60
+ export declare function projectionContentHash(projection: Projection): string;
61
+ export declare function factsContentHash(facts: any[]): string;
62
+ /**
63
+ * Keep native tool_call/tool_result pairs complete. Orphan tool messages left
64
+ * behind by projection truncation are dropped; missing results are synthesized
65
+ * with the existing "execution unknown" notice so a resumed model never waits
66
+ * for a result that cannot arrive.
67
+ */
68
+ export declare function pairToolMessages(messages: ChatMessage[]): {
69
+ messages: ChatMessage[];
70
+ repaired: boolean;
71
+ };
40
72
  export declare function buildProjection(facts: any[], metaOverride?: Partial<SessionMeta>): Projection;
73
+ export declare function readProjectionResult(file: string): ProjectionReadResult;
41
74
  export declare function readProjection(file: string): Projection | undefined;
42
- export declare function writeProjection(file: string, projection: Projection): void;
75
+ export declare function writeProjection(file: string, projection: Projection, facts?: any[]): void;
43
76
  export declare function resumeSession(file: string, currentCwd?: string): ResumeState;