@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.
- package/LICENSE +4 -0
- package/README.md +50 -17
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +33 -0
- package/dist/agent/approval.js +40 -5
- package/dist/agent/compaction.d.ts +2 -0
- package/dist/agent/compaction.js +60 -17
- package/dist/agent/guards.d.ts +16 -16
- package/dist/agent/guards.js +978 -24
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +20 -0
- package/dist/agent/process-tree.js +81 -10
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +165 -38
- package/dist/agent/registry.d.ts +3 -1
- package/dist/agent/registry.js +168 -11
- package/dist/agent/runner.d.ts +21 -0
- package/dist/agent/runner.js +44 -5
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +14 -0
- package/dist/agent/shell-session.js +251 -40
- package/dist/agent/tool-schemas.js +26 -7
- package/dist/agent/tools/append_file.js +3 -1
- package/dist/agent/tools/apply_patch.js +44 -4
- package/dist/agent/tools/copy_file.js +26 -3
- package/dist/agent/tools/delete_file.js +22 -3
- package/dist/agent/tools/grep.js +35 -4
- package/dist/agent/tools/make_dir.js +3 -1
- package/dist/agent/tools/move_path.d.ts +7 -0
- package/dist/agent/tools/move_path.js +31 -5
- package/dist/agent/tools/read_file.js +63 -3
- package/dist/agent/tools/read_many.js +21 -3
- package/dist/agent/tools/readers.d.ts +53 -1
- package/dist/agent/tools/readers.js +236 -26
- package/dist/agent/tools/replace_in_file.js +38 -5
- package/dist/agent/tools/shell.js +2 -2
- package/dist/agent/tools/types.d.ts +26 -0
- package/dist/agent/tools/validate.d.ts +15 -2
- package/dist/agent/tools/validate.js +66 -3
- package/dist/agent/tools/write_file.js +3 -1
- package/dist/app.d.ts +2 -1
- package/dist/app.js +526 -85
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +397 -337
- package/dist/commands/about.js +1 -1
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +12 -0
- package/dist/commands/compact.js +14 -2
- package/dist/commands/context.js +5 -4
- package/dist/commands/index.js +2 -0
- package/dist/commands/jobs.js +3 -2
- package/dist/commands/kill.js +3 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +15 -15
- package/dist/commands/model.d.ts +1 -13
- package/dist/commands/model.js +6 -38
- package/dist/commands/provider.d.ts +1 -15
- package/dist/commands/provider.js +4 -22
- package/dist/commands/skills.js +34 -4
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/config/store.d.ts +21 -0
- package/dist/config/store.js +47 -9
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +41 -2
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +19 -0
- package/dist/lib/diagnostics.js +32 -6
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/openaiCompatible.d.ts +24 -2
- package/dist/providers/openaiCompatible.js +20 -6
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/session/log.d.ts +14 -2
- package/dist/session/log.js +56 -27
- package/dist/session/projection.d.ts +34 -1
- package/dist/session/projection.js +315 -46
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +15 -0
- package/dist/ui/StatusBar.js +1 -1
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +58 -11
- package/src/agent/compaction.ts +62 -17
- package/src/agent/guards.ts +1049 -25
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +105 -23
- package/src/agent/protocol.ts +187 -39
- package/src/agent/registry.ts +186 -14
- package/src/agent/runner.ts +50 -7
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +265 -41
- package/src/agent/tool-schemas.ts +26 -7
- package/src/agent/tools/append_file.ts +3 -1
- package/src/agent/tools/apply_patch.ts +45 -4
- package/src/agent/tools/copy_file.ts +27 -3
- package/src/agent/tools/delete_file.ts +22 -3
- package/src/agent/tools/grep.ts +36 -4
- package/src/agent/tools/make_dir.ts +3 -1
- package/src/agent/tools/move_path.ts +38 -5
- package/src/agent/tools/read_file.ts +64 -3
- package/src/agent/tools/read_many.ts +21 -3
- package/src/agent/tools/readers.ts +289 -26
- package/src/agent/tools/replace_in_file.ts +39 -5
- package/src/agent/tools/shell.ts +2 -2
- package/src/agent/tools/types.ts +43 -1
- package/src/agent/tools/validate.ts +73 -4
- package/src/agent/tools/write_file.ts +3 -1
- package/src/app.ts +533 -82
- package/src/{cli.ts → cli-main.ts} +34 -6
- package/src/commands/about.ts +1 -1
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +14 -2
- package/src/commands/context.ts +5 -4
- package/src/commands/index.ts +2 -0
- package/src/commands/jobs.ts +3 -2
- package/src/commands/kill.ts +3 -1
- package/src/commands/login.ts +15 -15
- package/src/commands/model.ts +6 -38
- package/src/commands/provider.ts +4 -22
- package/src/commands/skills.ts +34 -4
- package/src/commands/threads.ts +28 -0
- package/src/config/store.ts +47 -9
- package/src/config/trust.ts +45 -2
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +32 -6
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/openaiCompatible.ts +47 -10
- package/src/providers/spec.ts +42 -0
- package/src/session/log.ts +70 -27
- package/src/session/projection.ts +360 -47
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +16 -1
- package/src/ui/StatusBar.ts +1 -1
- package/tsconfig.json +1 -0
- /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
|
+
}
|
package/dist/lib/markdown.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
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
|
|
71
|
+
Object.assign(headers, buildHeaders(spec, opts.apiKey));
|
|
59
72
|
if (req.clientRequestId)
|
|
60
73
|
headers["idempotency-key"] = req.clientRequestId;
|
|
61
|
-
return await fetch(
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
package/dist/session/log.d.ts
CHANGED
|
@@ -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;
|
package/dist/session/log.js
CHANGED
|
@@ -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,
|
|
14
|
-
import {
|
|
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({
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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;
|