@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
|
@@ -18,6 +18,18 @@ export interface ProjectionFingerprint {
|
|
|
18
18
|
compactionSource: "local" | "server" | "none";
|
|
19
19
|
}
|
|
20
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
|
+
|
|
21
33
|
export interface Projection {
|
|
22
34
|
version: 1;
|
|
23
35
|
kind: "projection";
|
|
@@ -28,6 +40,17 @@ export interface Projection {
|
|
|
28
40
|
messages: ChatMessage[];
|
|
29
41
|
activeFiles: ActiveFile[];
|
|
30
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;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
export interface ResumeState {
|
|
@@ -40,8 +63,9 @@ export interface ResumeState {
|
|
|
40
63
|
|
|
41
64
|
const READ_ONLY_TOOLS = new Set(["read_file", "read_many", "grep", "list_files", "stat_path", "pwd"]);
|
|
42
65
|
const IDEMPOTENT_TOOLS = new Set(["write_file"]);
|
|
43
|
-
const MAX_MESSAGE_CHARS =
|
|
44
|
-
const MAX_PROJECTION_BYTES =
|
|
66
|
+
const MAX_MESSAGE_CHARS = 8_000;
|
|
67
|
+
const MAX_PROJECTION_BYTES = 200_000;
|
|
68
|
+
const TOOL_FOLD_CHARS = 600;
|
|
45
69
|
|
|
46
70
|
export function unpairedToolNotice(tool: string): string {
|
|
47
71
|
if (READ_ONLY_TOOLS.has(tool))
|
|
@@ -73,8 +97,21 @@ export function sessionMetaFromFacts(facts: any[]): SessionMeta {
|
|
|
73
97
|
let cwd = "";
|
|
74
98
|
let providerId = "";
|
|
75
99
|
let model = "";
|
|
100
|
+
let sawStart = false;
|
|
76
101
|
for (const fact of facts) {
|
|
77
|
-
|
|
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") {
|
|
78
115
|
if (typeof fact.cwd === "string" && fact.cwd)
|
|
79
116
|
cwd = fact.cwd;
|
|
80
117
|
if (typeof fact.providerId === "string" && fact.providerId)
|
|
@@ -86,6 +123,14 @@ export function sessionMetaFromFacts(facts: any[]): SessionMeta {
|
|
|
86
123
|
return { cwd, providerId, model };
|
|
87
124
|
}
|
|
88
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
|
+
|
|
89
134
|
export function sessionMetaFromFile(file: string): SessionMeta {
|
|
90
135
|
return sessionMetaFromFacts(readSession(file));
|
|
91
136
|
}
|
|
@@ -132,6 +177,10 @@ export function factsToMessages(facts: any[]): ChatMessage[] {
|
|
|
132
177
|
else if (fact.type === "tool_call") {
|
|
133
178
|
calls.set(String(fact.id ?? ""), fact.args ?? { tool: fact.tool });
|
|
134
179
|
}
|
|
180
|
+
else if (fact.type === "notice") {
|
|
181
|
+
if (typeof fact.content === "string" && fact.content)
|
|
182
|
+
messages.push({ role: "system", content: fact.content });
|
|
183
|
+
}
|
|
135
184
|
else if (fact.type === "tool_result") {
|
|
136
185
|
const id = String(fact.id ?? "");
|
|
137
186
|
const call = calls.get(id) ?? { tool: fact.tool ?? "unknown" };
|
|
@@ -147,6 +196,11 @@ export function factsToMessages(facts: any[]): ChatMessage[] {
|
|
|
147
196
|
});
|
|
148
197
|
}
|
|
149
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
|
+
}
|
|
150
204
|
}
|
|
151
205
|
flush();
|
|
152
206
|
if (messages.length === 0)
|
|
@@ -186,10 +240,98 @@ function applySummary(messages: ChatMessage[], facts: any[]): ChatMessage[] {
|
|
|
186
240
|
return [marker, ...tail];
|
|
187
241
|
}
|
|
188
242
|
catch {
|
|
189
|
-
return
|
|
243
|
+
return [
|
|
244
|
+
...messages,
|
|
245
|
+
{ role: "system", content: "[projection] Summary file could not be read; resumed without the summary marker." },
|
|
246
|
+
];
|
|
190
247
|
}
|
|
191
248
|
}
|
|
192
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
|
+
|
|
193
335
|
export function buildProjection(facts: any[], metaOverride?: Partial<SessionMeta>): Projection {
|
|
194
336
|
const detected = sessionMetaFromFacts(facts);
|
|
195
337
|
const meta: SessionMeta = {
|
|
@@ -197,13 +339,17 @@ export function buildProjection(facts: any[], metaOverride?: Partial<SessionMeta
|
|
|
197
339
|
providerId: metaOverride?.providerId || detected.providerId,
|
|
198
340
|
model: metaOverride?.model || detected.model,
|
|
199
341
|
};
|
|
200
|
-
const
|
|
201
|
-
const
|
|
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);
|
|
202
346
|
let activeFiles: ActiveFile[] = [];
|
|
203
347
|
let lastFactSeq = 0;
|
|
204
348
|
for (const fact of facts) {
|
|
205
349
|
if (typeof fact?.seq === "number" && fact.seq > lastFactSeq)
|
|
206
350
|
lastFactSeq = fact.seq;
|
|
351
|
+
}
|
|
352
|
+
for (const fact of threadFacts) {
|
|
207
353
|
if (fact?.type === "checkpoint" && Array.isArray(fact.activeFiles))
|
|
208
354
|
activeFiles = fact.activeFiles;
|
|
209
355
|
}
|
|
@@ -221,44 +367,172 @@ export function buildProjection(facts: any[], metaOverride?: Partial<SessionMeta
|
|
|
221
367
|
return projection;
|
|
222
368
|
}
|
|
223
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
|
+
|
|
224
467
|
function clampMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
225
|
-
const list = messages.map((message) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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.`,
|
|
234
495
|
};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
list.shift();
|
|
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
|
+
}
|
|
241
501
|
}
|
|
242
502
|
return list;
|
|
243
503
|
}
|
|
244
504
|
|
|
245
|
-
export function
|
|
505
|
+
export function readProjectionResult(file: string): ProjectionReadResult {
|
|
506
|
+
let raw: any;
|
|
246
507
|
try {
|
|
247
|
-
|
|
248
|
-
if (!raw || raw.version !== 1 || !Array.isArray(raw.messages) || !raw.fingerprint)
|
|
249
|
-
return undefined;
|
|
250
|
-
return raw as Projection;
|
|
508
|
+
raw = JSON.parse(readFileSync(projectionPath(file), "utf8"));
|
|
251
509
|
}
|
|
252
|
-
catch {
|
|
253
|
-
|
|
510
|
+
catch (err: any) {
|
|
511
|
+
if (err?.code === "ENOENT")
|
|
512
|
+
return { status: "missing" };
|
|
513
|
+
return { status: "corrupt" };
|
|
254
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 };
|
|
255
524
|
}
|
|
256
525
|
|
|
257
|
-
export function
|
|
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 {
|
|
258
532
|
const target = projectionPath(file);
|
|
259
533
|
const tmp = target + "." + process.pid + ".tmp";
|
|
260
534
|
try {
|
|
261
|
-
writeFileSync(tmp, JSON.stringify(projection, null, 2), "utf8");
|
|
535
|
+
writeFileSync(tmp, JSON.stringify(withManifest(projection, facts), null, 2), "utf8");
|
|
262
536
|
renameSync(tmp, target);
|
|
263
537
|
}
|
|
264
538
|
catch (err) {
|
|
@@ -274,35 +548,74 @@ export function writeProjection(file: string, projection: Projection): void {
|
|
|
274
548
|
|
|
275
549
|
export function resumeSession(file: string, currentCwd = process.cwd()): ResumeState {
|
|
276
550
|
const facts = readSession(file);
|
|
551
|
+
const hasFacts = facts.length > 0;
|
|
277
552
|
const detected = sessionMetaFromFacts(facts);
|
|
553
|
+
const read = readProjectionResult(file);
|
|
554
|
+
const stored = read.projection;
|
|
278
555
|
const meta: SessionMeta = {
|
|
279
|
-
cwd: detected.cwd || currentCwd,
|
|
280
|
-
providerId: detected.providerId,
|
|
281
|
-
model: detected.model,
|
|
556
|
+
cwd: detected.cwd || stored?.cwd || currentCwd,
|
|
557
|
+
providerId: detected.providerId || stored?.providerId || "",
|
|
558
|
+
model: detected.model || stored?.model || "",
|
|
282
559
|
};
|
|
283
|
-
const expected = projectionFingerprint(
|
|
284
|
-
|
|
560
|
+
const expected = projectionFingerprint(
|
|
561
|
+
meta,
|
|
562
|
+
hasFacts ? compactionSourceFromFacts(factsAfterClear(facts)) : (stored?.fingerprint.compactionSource ?? "none"),
|
|
563
|
+
);
|
|
285
564
|
const warnings: string[] = [];
|
|
286
|
-
let projection: Projection;
|
|
565
|
+
let projection: Projection | undefined;
|
|
287
566
|
let rebuilt = false;
|
|
288
|
-
|
|
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) {
|
|
289
575
|
projection = stored;
|
|
576
|
+
if (read.status === "legacy")
|
|
577
|
+
warnings.push("Projection has no manifest; integrity could not be verified.");
|
|
290
578
|
}
|
|
291
579
|
else {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (
|
|
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)
|
|
295
587
|
warnings.push("Projection fingerprint changed (tools/model/cwd); rebuilt from facts.");
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|
|
300
600
|
}
|
|
301
|
-
|
|
302
|
-
|
|
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.");
|
|
303
611
|
}
|
|
304
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 };
|
|
305
618
|
if (detected.cwd && detected.cwd !== currentCwd)
|
|
306
619
|
warnings.push(`Session was recorded in ${detected.cwd}; current directory is ${currentCwd}.`);
|
|
307
|
-
return { projection, messages:
|
|
620
|
+
return { projection: finalProjection, messages: finalProjection.messages, meta, warnings, rebuilt };
|
|
308
621
|
}
|