@sema-agent/core 5.1.0 → 5.2.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/CHANGELOG.md +33 -0
- package/dist/agents/roster-store.js +6 -1
- package/dist/bin/sema-tb.js +3 -4
- package/dist/brain/openai.js +3 -5
- package/dist/brain/terminal-cause.d.ts +1 -1
- package/dist/core/hooks.js +8 -3
- package/dist/core/mcp.d.ts +1 -1
- package/dist/core/memory-engine/dual-root.js +2 -1
- package/dist/core/memory-engine/engine.d.ts +4 -0
- package/dist/core/memory-engine/engine.js +25 -6
- package/dist/core/memory-engine/file-backend.d.ts +7 -5
- package/dist/core/memory-engine/file-backend.js +2 -2
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +6 -2
- package/dist/core/memory-engine/layout.js +73 -31
- package/dist/core/memory.js +6 -0
- package/dist/core/protocol-table.d.ts +10 -7
- package/dist/core/protocol-table.js +28 -14
- package/dist/core/runner/prepare-memory.js +4 -4
- package/dist/core/runner/prepare-task.js +30 -14
- package/dist/core/runner/runtask.js +11 -5
- package/dist/core/runner/tool-disclosure.js +6 -1
- package/dist/core/tool-policy.d.ts +1 -0
- package/dist/core/tool-policy.js +12 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/orchestration/run-workflow-tool.js +5 -1
- package/dist/prompt-assembly/assemble.js +0 -1
- package/dist/stores/cc/mailbox-store.js +58 -14
- package/dist/stores/file/file-snapshot-store.d.ts +9 -1
- package/dist/stores/file/file-snapshot-store.js +28 -5
- package/dist/stores/file/fs-atomic.d.ts +4 -1
- package/dist/stores/file/fs-atomic.js +2 -1
- package/dist/stores/file/index.d.ts +10 -3
- package/dist/stores/file/index.js +4 -3
- package/dist/stores/file/mailbox-store.d.ts +5 -0
- package/dist/stores/file/mailbox-store.js +15 -3
- package/dist/stores/file/session-policy-store.d.ts +11 -8
- package/dist/stores/file/session-policy-store.js +21 -4
- package/dist/stores/file/session-store.d.ts +9 -1
- package/dist/stores/file/session-store.js +19 -4
- package/dist/tools/fs/fs-search-tools.js +9 -0
- package/dist/tools/fs/fs-shared.d.ts +1 -1
- package/dist/tools/fs/fs-shared.js +1 -1
- package/dist/tools/todo.js +13 -6
- package/package.json +1 -1
|
@@ -18,7 +18,8 @@ export class FileFileSnapshotStore {
|
|
|
18
18
|
}
|
|
19
19
|
inFlightKey;
|
|
20
20
|
bounds;
|
|
21
|
-
|
|
21
|
+
onCorruptRead;
|
|
22
|
+
constructor(root, bounds, opts) {
|
|
22
23
|
this.base = join(root, "file-snapshots");
|
|
23
24
|
this.blobsDir = join(this.base, "blobs");
|
|
24
25
|
this.manifestsDir = join(this.base, "manifests");
|
|
@@ -30,6 +31,14 @@ export class FileFileSnapshotStore {
|
|
|
30
31
|
maxBytes: bounds?.maxBytes ?? DEFAULT_SNAPSHOT_BOUNDS.maxBytes,
|
|
31
32
|
ignoreDirs: new Set(bounds?.ignoreDirs ?? DEFAULT_SNAPSHOT_BOUNDS.ignoreDirs),
|
|
32
33
|
};
|
|
34
|
+
this.onCorruptRead = opts?.onCorruptRead;
|
|
35
|
+
}
|
|
36
|
+
disclose(path, reason) {
|
|
37
|
+
try {
|
|
38
|
+
this.onCorruptRead?.({ path, reason });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
scopeDir(scope) {
|
|
35
44
|
return join(this.manifestsDir, sanitizeScope(scope));
|
|
@@ -60,9 +69,13 @@ export class FileFileSnapshotStore {
|
|
|
60
69
|
}
|
|
61
70
|
try {
|
|
62
71
|
const parsed = JSON.parse(raw);
|
|
63
|
-
|
|
72
|
+
if (Array.isArray(parsed))
|
|
73
|
+
return new Map(parsed);
|
|
74
|
+
this.disclose(path, "manifest is not a pair array — read as no snapshot");
|
|
75
|
+
return null;
|
|
64
76
|
}
|
|
65
77
|
catch {
|
|
78
|
+
this.disclose(path, "unparseable manifest JSON (torn/zero-byte write?) — read as no snapshot");
|
|
66
79
|
return null;
|
|
67
80
|
}
|
|
68
81
|
}
|
|
@@ -132,6 +145,7 @@ export class FileFileSnapshotStore {
|
|
|
132
145
|
catch (err) {
|
|
133
146
|
if (err.code === "ENOENT")
|
|
134
147
|
return [];
|
|
148
|
+
this.disclose(dir, `manifest scope directory unreadable (${err.code ?? "unknown"}) — listed as no keys`);
|
|
135
149
|
return [];
|
|
136
150
|
}
|
|
137
151
|
return entries
|
|
@@ -242,8 +256,10 @@ export class FileFileSnapshotStore {
|
|
|
242
256
|
catch (err) {
|
|
243
257
|
if (err.code === "ENOENT")
|
|
244
258
|
entries = [];
|
|
245
|
-
else
|
|
259
|
+
else {
|
|
246
260
|
entries = [];
|
|
261
|
+
this.disclose(dir, `manifest scope directory unreadable (${err.code ?? "unknown"}) — the manifest-deletion pass was skipped`);
|
|
262
|
+
}
|
|
247
263
|
}
|
|
248
264
|
for (const file of entries) {
|
|
249
265
|
if (!file.endsWith(".json"))
|
|
@@ -266,8 +282,10 @@ export class FileFileSnapshotStore {
|
|
|
266
282
|
catch (err) {
|
|
267
283
|
if (err.code === "ENOENT")
|
|
268
284
|
scopeDirs = [];
|
|
269
|
-
else
|
|
285
|
+
else {
|
|
286
|
+
this.disclose(this.manifestsDir, `manifest root unreadable (${err.code ?? "unknown"}) — blob GC aborted, every blob kept`);
|
|
270
287
|
return removed;
|
|
288
|
+
}
|
|
271
289
|
}
|
|
272
290
|
for (const scopeName of scopeDirs) {
|
|
273
291
|
const sdir = join(this.manifestsDir, scopeName);
|
|
@@ -279,6 +297,7 @@ export class FileFileSnapshotStore {
|
|
|
279
297
|
if (err.code === "ENOENT")
|
|
280
298
|
continue;
|
|
281
299
|
complete = false;
|
|
300
|
+
this.disclose(sdir, `manifest scope directory unreadable (${err.code ?? "unknown"}) — blob GC aborted, every blob kept`);
|
|
282
301
|
break;
|
|
283
302
|
}
|
|
284
303
|
for (const file of files) {
|
|
@@ -287,6 +306,7 @@ export class FileFileSnapshotStore {
|
|
|
287
306
|
const m = this.readManifest(join(sdir, file));
|
|
288
307
|
if (!m) {
|
|
289
308
|
complete = false;
|
|
309
|
+
this.disclose(join(sdir, file), "manifest unreadable during the live-set scan — blob GC aborted, every blob kept");
|
|
290
310
|
break;
|
|
291
311
|
}
|
|
292
312
|
for (const h of m.values())
|
|
@@ -301,7 +321,10 @@ export class FileFileSnapshotStore {
|
|
|
301
321
|
try {
|
|
302
322
|
blobs = readdirSync(this.blobsDir);
|
|
303
323
|
}
|
|
304
|
-
catch {
|
|
324
|
+
catch (err) {
|
|
325
|
+
if (err.code !== "ENOENT") {
|
|
326
|
+
this.disclose(this.blobsDir, `blob directory unreadable (${err.code ?? "unknown"}) — blob GC skipped, every blob kept`);
|
|
327
|
+
}
|
|
305
328
|
return removed;
|
|
306
329
|
}
|
|
307
330
|
for (const blob of blobs) {
|
|
@@ -4,7 +4,10 @@ export declare function resolveDataRoot(explicit?: string): string;
|
|
|
4
4
|
export declare function writeThenLink(target: string, content: string | Uint8Array): void;
|
|
5
5
|
export declare function ensureDir(dir: string): void;
|
|
6
6
|
export declare function atomicWriteFile(tmpDir: string, target: string, bytes: string): void;
|
|
7
|
-
export declare function readJsonlRecords<T>(path: string
|
|
7
|
+
export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: {
|
|
8
|
+
path: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}) => void): T[];
|
|
8
11
|
export declare function canonicalStoreKey(p: string): string;
|
|
9
12
|
export declare class AppendLog {
|
|
10
13
|
private fd;
|
|
@@ -101,7 +101,7 @@ function basenameOf(p) {
|
|
|
101
101
|
function dirnameOf(p) {
|
|
102
102
|
return dirname(p);
|
|
103
103
|
}
|
|
104
|
-
export function readJsonlRecords(path) {
|
|
104
|
+
export function readJsonlRecords(path, onCorrupt) {
|
|
105
105
|
let raw;
|
|
106
106
|
try {
|
|
107
107
|
raw = readFileSync(path, "utf8");
|
|
@@ -126,6 +126,7 @@ export function readJsonlRecords(path) {
|
|
|
126
126
|
out.push(JSON.parse(line));
|
|
127
127
|
}
|
|
128
128
|
catch {
|
|
129
|
+
onCorrupt?.({ path, reason: `unparseable jsonl record at line ${i + 1} (skipped; the rest of the replay is intact)` });
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
return out;
|
|
@@ -9,10 +9,10 @@ import type { FileSnapshotStore } from "../../core/file-snapshot-store.js";
|
|
|
9
9
|
import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
|
|
10
10
|
export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
11
11
|
export { FileMemoryStore } from "./memory-store.js";
|
|
12
|
-
export { FileSessionRepo } from "./session-store.js";
|
|
12
|
+
export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
|
|
13
13
|
export { FileToolResultStore } from "./tool-result-store.js";
|
|
14
|
-
export { FileSessionPolicyStore } from "./session-policy-store.js";
|
|
15
|
-
export { FileFileSnapshotStore } from "./file-snapshot-store.js";
|
|
14
|
+
export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type SessionPolicyCorruptReadInfo } from "./session-policy-store.js";
|
|
15
|
+
export { FileFileSnapshotStore, type FileFileSnapshotStoreOptions } from "./file-snapshot-store.js";
|
|
16
16
|
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
|
|
17
17
|
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
|
|
18
18
|
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
|
|
@@ -23,6 +23,13 @@ export interface FileStorageBackendOptions {
|
|
|
23
23
|
checkpoint?: FileCheckpointStoreOptions;
|
|
24
24
|
snapshotBounds?: Partial<import("../../core/file-snapshot-store.js").FileSnapshotBounds>;
|
|
25
25
|
embedder?: import("../../core/memory.js").Embedder;
|
|
26
|
+
onCorruptRead?: (info: FileStorageCorruptReadInfo) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface FileStorageCorruptReadInfo {
|
|
29
|
+
path: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
principal?: string;
|
|
26
33
|
}
|
|
27
34
|
export declare class FileStorageBackend {
|
|
28
35
|
readonly root: string;
|
|
@@ -39,7 +39,8 @@ export class FileStorageBackend {
|
|
|
39
39
|
this.lock = new BootLock(join(this.root, "LOCK"));
|
|
40
40
|
this.lock.acquire();
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
42
|
+
const corruptRead = opts.onCorruptRead !== undefined ? { onCorruptRead: opts.onCorruptRead } : undefined;
|
|
43
|
+
const repo = new FileSessionRepo(this.root, corruptRead);
|
|
43
44
|
this.fileSessions = repo;
|
|
44
45
|
this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget" });
|
|
45
46
|
this.sessionStore = this.ttl;
|
|
@@ -48,8 +49,8 @@ export class FileStorageBackend {
|
|
|
48
49
|
this.fileMemory = new FileMemoryStore(this.root, opts.embedder ? { embedder: opts.embedder } : {});
|
|
49
50
|
this.memoryStore = guardedMemoryStore(this.fileMemory, opts.utilityGate);
|
|
50
51
|
this.toolResultStore = new FileToolResultStore(this.root);
|
|
51
|
-
this.sessionPolicyStore = new FileSessionPolicyStore(this.root);
|
|
52
|
-
this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds);
|
|
52
|
+
this.sessionPolicyStore = new FileSessionPolicyStore(this.root, corruptRead);
|
|
53
|
+
this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds, corruptRead);
|
|
53
54
|
this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
|
|
54
55
|
this.workflowJournalStore = this.fileWorkflowJournal;
|
|
55
56
|
this.consolidationLock = createFileConsolidationLock(join(this.root, "consolidation-locks"));
|
|
@@ -2,6 +2,10 @@ import { type MailboxAppendMessage, type MailboxLease, type MailboxStore } from
|
|
|
2
2
|
export interface FileMailboxStoreOptions {
|
|
3
3
|
fsync?: boolean;
|
|
4
4
|
compactEvery?: number;
|
|
5
|
+
onCorruptRead?: (info: {
|
|
6
|
+
path: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}) => void;
|
|
5
9
|
}
|
|
6
10
|
export declare class FileMailboxStore implements MailboxStore {
|
|
7
11
|
private readonly dir;
|
|
@@ -9,6 +13,7 @@ export declare class FileMailboxStore implements MailboxStore {
|
|
|
9
13
|
private readonly fsyncEnabled;
|
|
10
14
|
private readonly compactEvery;
|
|
11
15
|
private readonly touched;
|
|
16
|
+
private readonly discloseCorrupt;
|
|
12
17
|
constructor(root: string, opts?: FileMailboxStoreOptions);
|
|
13
18
|
private boxPath;
|
|
14
19
|
private lockKey;
|
|
@@ -39,9 +39,21 @@ export class FileMailboxStore {
|
|
|
39
39
|
fsyncEnabled;
|
|
40
40
|
compactEvery;
|
|
41
41
|
touched = new Map();
|
|
42
|
+
discloseCorrupt;
|
|
42
43
|
constructor(root, opts = {}) {
|
|
43
44
|
this.fsyncEnabled = opts.fsync !== false;
|
|
44
45
|
this.compactEvery = opts.compactEvery ?? 500;
|
|
46
|
+
const sink = opts.onCorruptRead;
|
|
47
|
+
this.discloseCorrupt =
|
|
48
|
+
sink === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: (info) => {
|
|
51
|
+
try {
|
|
52
|
+
sink(info);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
}
|
|
56
|
+
};
|
|
45
57
|
const lexicalDir = resolve(join(root, "mailboxes"));
|
|
46
58
|
const lexicalTmp = resolve(join(root, "tmp"));
|
|
47
59
|
ensureDir(lexicalDir);
|
|
@@ -67,7 +79,7 @@ export class FileMailboxStore {
|
|
|
67
79
|
return b;
|
|
68
80
|
}
|
|
69
81
|
ensureDir(join(this.dir, sanitizeScope(scope)));
|
|
70
|
-
const events = readJsonlRecords(path);
|
|
82
|
+
const events = readJsonlRecords(path, this.discloseCorrupt);
|
|
71
83
|
const state = { messages: [], nextSeq: 1, log: new AppendLog(path), events: events.length, refs: 1, path };
|
|
72
84
|
for (const ev of events)
|
|
73
85
|
applyEvent(state, ev);
|
|
@@ -145,7 +157,7 @@ export class FileMailboxStore {
|
|
|
145
157
|
return 0;
|
|
146
158
|
return withPathLock(this.lockKey(scope, handle), () => {
|
|
147
159
|
const b = { messages: [], nextSeq: 1 };
|
|
148
|
-
for (const ev of readJsonlRecords(path))
|
|
160
|
+
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
149
161
|
applyEvent(b, ev);
|
|
150
162
|
return b.messages.length;
|
|
151
163
|
});
|
|
@@ -200,7 +212,7 @@ export class FileMailboxStore {
|
|
|
200
212
|
if (sharedBoxes.has(key))
|
|
201
213
|
return false;
|
|
202
214
|
const b = { messages: [], nextSeq: 1 };
|
|
203
|
-
for (const ev of readJsonlRecords(path))
|
|
215
|
+
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
204
216
|
applyEvent(b, ev);
|
|
205
217
|
const newest = newestSentAt(b.messages);
|
|
206
218
|
if (newest === undefined || newest >= now - maxAgeMs)
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "../../core/session-policy-store.js";
|
|
2
|
+
export interface SessionPolicyCorruptReadInfo {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
principal?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FileSessionPolicyStoreOptions {
|
|
9
|
+
onCorruptRead?: (info: SessionPolicyCorruptReadInfo) => void;
|
|
10
|
+
}
|
|
2
11
|
export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
3
12
|
private readonly dir;
|
|
4
13
|
private readonly onCorruptRead;
|
|
5
|
-
constructor(root: string, opts?:
|
|
6
|
-
|
|
7
|
-
sessionId: string;
|
|
8
|
-
principal?: string;
|
|
9
|
-
path: string;
|
|
10
|
-
reason: string;
|
|
11
|
-
}) => void;
|
|
12
|
-
});
|
|
14
|
+
constructor(root: string, opts?: FileSessionPolicyStoreOptions);
|
|
15
|
+
private disclose;
|
|
13
16
|
private discloseCorrupt;
|
|
14
17
|
private pathFor;
|
|
15
18
|
private read;
|
|
@@ -10,13 +10,16 @@ export class FileSessionPolicyStore {
|
|
|
10
10
|
ensureDir(this.dir);
|
|
11
11
|
this.onCorruptRead = opts?.onCorruptRead;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
disclose(info) {
|
|
14
14
|
try {
|
|
15
|
-
this.onCorruptRead?.(
|
|
15
|
+
this.onCorruptRead?.(info);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
discloseCorrupt(sessionId, principal, reason) {
|
|
21
|
+
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
|
|
22
|
+
}
|
|
20
23
|
pathFor(sessionId, principal) {
|
|
21
24
|
const composite = JSON.stringify([sessionId, principal ?? null]);
|
|
22
25
|
return join(this.dir, `${sanitizeScope(composite)}.json`);
|
|
@@ -90,15 +93,29 @@ export class FileSessionPolicyStore {
|
|
|
90
93
|
for (const file of files) {
|
|
91
94
|
if (!file.endsWith(".json"))
|
|
92
95
|
continue;
|
|
96
|
+
const full = join(this.dir, file);
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = readFileSync(full, "utf8");
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err.code !== "ENOENT") {
|
|
103
|
+
this.disclose({ sessionId, path: full, reason: `read failed (non-ENOENT): ${err.message}` });
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
93
107
|
let parsed;
|
|
94
108
|
try {
|
|
95
|
-
parsed = JSON.parse(
|
|
109
|
+
parsed = JSON.parse(raw);
|
|
96
110
|
}
|
|
97
111
|
catch {
|
|
112
|
+
this.disclose({ sessionId, path: full, reason: "unparseable JSON (torn/zero-byte write?)" });
|
|
98
113
|
continue;
|
|
99
114
|
}
|
|
100
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
115
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
116
|
+
this.disclose({ sessionId, path: full, reason: "not a JSON object" });
|
|
101
117
|
continue;
|
|
118
|
+
}
|
|
102
119
|
const r = parsed;
|
|
103
120
|
if (r.__sid !== sessionId)
|
|
104
121
|
continue;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { Session, SessionForkOptions, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
|
|
2
|
+
export interface FileSessionRepoOptions {
|
|
3
|
+
onCorruptRead?: (info: {
|
|
4
|
+
path: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
}) => void;
|
|
7
|
+
}
|
|
2
8
|
export declare class FileSessionRepo implements SessionRepo {
|
|
3
9
|
private readonly dir;
|
|
4
10
|
private readonly tmpDir;
|
|
11
|
+
private readonly onCorruptRead;
|
|
5
12
|
private readonly joined;
|
|
6
|
-
constructor(root: string);
|
|
13
|
+
constructor(root: string, opts?: FileSessionRepoOptions);
|
|
14
|
+
private disclose;
|
|
7
15
|
private pathFor;
|
|
8
16
|
private read;
|
|
9
17
|
private storage;
|
|
@@ -54,12 +54,21 @@ class FileSessionStorage extends BaseSessionStorage {
|
|
|
54
54
|
export class FileSessionRepo {
|
|
55
55
|
dir;
|
|
56
56
|
tmpDir;
|
|
57
|
+
onCorruptRead;
|
|
57
58
|
joined = new Map();
|
|
58
|
-
constructor(root) {
|
|
59
|
+
constructor(root, opts) {
|
|
59
60
|
this.dir = join(root, "sessions");
|
|
60
61
|
this.tmpDir = join(root, "tmp");
|
|
61
62
|
ensureDir(this.dir);
|
|
62
63
|
ensureDir(this.tmpDir);
|
|
64
|
+
this.onCorruptRead = opts?.onCorruptRead;
|
|
65
|
+
}
|
|
66
|
+
disclose(path, reason) {
|
|
67
|
+
try {
|
|
68
|
+
this.onCorruptRead?.({ path, reason });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
pathFor(id) {
|
|
65
74
|
return join(this.dir, `${sanitizePathComponent(id)}${SUFFIX}`);
|
|
@@ -69,7 +78,7 @@ export class FileSessionRepo {
|
|
|
69
78
|
if (!existsSync(path)) {
|
|
70
79
|
throw new SessionError("not_found", `Session not found: ${id}`);
|
|
71
80
|
}
|
|
72
|
-
const lines = readJsonlRecords(path);
|
|
81
|
+
const lines = readJsonlRecords(path, (info) => this.disclose(info.path, info.reason));
|
|
73
82
|
let createdAt = "";
|
|
74
83
|
let forkedFrom;
|
|
75
84
|
const entries = [];
|
|
@@ -132,7 +141,10 @@ export class FileSessionRepo {
|
|
|
132
141
|
try {
|
|
133
142
|
names = readdirSync(this.dir);
|
|
134
143
|
}
|
|
135
|
-
catch {
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err.code !== "ENOENT") {
|
|
146
|
+
this.disclose(this.dir, `session directory unreadable (${err.code ?? "unknown"}) — listed as empty`);
|
|
147
|
+
}
|
|
136
148
|
return [];
|
|
137
149
|
}
|
|
138
150
|
const out = [];
|
|
@@ -144,7 +156,10 @@ export class FileSessionRepo {
|
|
|
144
156
|
const { createdAt, forkedFrom } = this.read(id);
|
|
145
157
|
out.push({ id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}) });
|
|
146
158
|
}
|
|
147
|
-
catch {
|
|
159
|
+
catch (err) {
|
|
160
|
+
if (!(err instanceof SessionError && err.code === "not_found")) {
|
|
161
|
+
this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the listing`);
|
|
162
|
+
}
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
@@ -40,6 +40,15 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
40
40
|
}),
|
|
41
41
|
effect: "read",
|
|
42
42
|
execute: async (args, ctx) => {
|
|
43
|
+
const retired = args;
|
|
44
|
+
for (const [name, repair] of [
|
|
45
|
+
["regex", "the `pattern` is ALWAYS evaluated as a regular expression (ripgrep semantics) — drop the key; to match text literally, escape the regex metacharacters in `pattern`"],
|
|
46
|
+
["max_results", "cap the result count with `head_limit` (0 = unlimited), paginating with `offset`"],
|
|
47
|
+
]) {
|
|
48
|
+
if (retired[name] !== undefined) {
|
|
49
|
+
return errorResult(`Error (Grep): \`${name}\` is not a Grep parameter (retired) — ${repair}. Nothing was searched.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
43
52
|
const a = args;
|
|
44
53
|
for (const [name, value] of [
|
|
45
54
|
["head_limit", a.head_limit],
|
|
@@ -42,7 +42,7 @@ export declare function bashTimeoutCapsSec(caps: {
|
|
|
42
42
|
};
|
|
43
43
|
export declare function bashMaxOutputChars(): number;
|
|
44
44
|
export declare const FILE_PATH_PARAMS: {
|
|
45
|
-
file_path: Type.
|
|
45
|
+
file_path: Type.TString;
|
|
46
46
|
};
|
|
47
47
|
export declare function clipShellOutput(s: string): string;
|
|
48
48
|
export declare function writeShellOverflowFile(env: ExecutionEnv, stdout: string, stderr: string): Promise<string | undefined>;
|
|
@@ -76,7 +76,7 @@ export function bashMaxOutputChars() {
|
|
|
76
76
|
return Math.min(Math.floor(raw), BASH_MAX_OUTPUT_CHARS_CEILING);
|
|
77
77
|
}
|
|
78
78
|
export const FILE_PATH_PARAMS = {
|
|
79
|
-
file_path: Type.
|
|
79
|
+
file_path: Type.String({ description: "File path (within the configured root)." }),
|
|
80
80
|
};
|
|
81
81
|
export function clipShellOutput(s) {
|
|
82
82
|
return clipWithFilePointer(s, bashMaxOutputChars());
|
package/dist/tools/todo.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { errorResult } from "../core/tools.js";
|
|
2
3
|
export function createTodoWriteTool() {
|
|
3
4
|
let todos = [];
|
|
4
5
|
return {
|
|
@@ -6,7 +7,7 @@ export function createTodoWriteTool() {
|
|
|
6
7
|
contract: { contractId: "core.todo_write@1", implementationRevision: "1" },
|
|
7
8
|
description: "Create and update a task list for the current session. The list is rendered to the user as your working plan.\n" +
|
|
8
9
|
"\n" +
|
|
9
|
-
'- Each todo has `content`, `status` ("pending" | "in_progress" | "completed"), and
|
|
10
|
+
'- Each todo has `content`, `status` ("pending" | "in_progress" | "completed"), and `activeForm` (present-tense label shown while in progress).\n' +
|
|
10
11
|
"- Send the full list each call; it replaces the previous one.\n" +
|
|
11
12
|
"- Keep one item `in_progress` at a time and mark it `completed` when done.",
|
|
12
13
|
descriptionClassic: "Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\n" +
|
|
@@ -81,13 +82,19 @@ export function createTodoWriteTool() {
|
|
|
81
82
|
}),
|
|
82
83
|
effect: "idempotent",
|
|
83
84
|
execute: (args) => {
|
|
84
|
-
const shape = (t) => ({
|
|
85
|
-
content: t.content,
|
|
86
|
-
status: t.status,
|
|
87
|
-
activeForm: t.activeForm && t.activeForm.length > 0 ? t.activeForm : t.content,
|
|
88
|
-
});
|
|
85
|
+
const shape = (t) => ({ content: t.content, status: t.status, activeForm: t.activeForm });
|
|
89
86
|
const oldTodos = todos.map(shape);
|
|
90
87
|
const written = args.todos;
|
|
88
|
+
for (let i = 0; i < written.length; i++) {
|
|
89
|
+
const activeForm = written[i].activeForm;
|
|
90
|
+
if (typeof activeForm !== "string" || activeForm.length === 0) {
|
|
91
|
+
const what = activeForm === undefined ? "is missing" : "has an empty or non-string";
|
|
92
|
+
return errorResult(`Error (TodoWrite): todo ${i + 1} of ${written.length} ${what} \`activeForm\` — every todo needs the ` +
|
|
93
|
+
`present-continuous label shown while it runs (e.g. content "Run the tests" → activeForm "Running the ` +
|
|
94
|
+
`tests"), alongside its imperative \`content\`. Nothing was stored; resend the COMPLETE list with the ` +
|
|
95
|
+
`label filled in on every item.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
91
98
|
const allDone = written.every((t) => t.status === "completed");
|
|
92
99
|
todos = allDone ? [] : written;
|
|
93
100
|
const newTodos = written.map(shape);
|