atom-agent 1.1.0 → 1.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 +31 -0
- package/README.md +5 -4
- package/dist/App.js +738 -79
- package/dist/adapters.js +30 -8
- package/dist/agent/gates.js +14 -1
- package/dist/agent/loop-guard.js +11 -13
- package/dist/agent/loop.js +212 -69
- package/dist/agent/normalize.js +9 -2
- package/dist/cli.js +16 -2
- package/dist/compact.js +128 -2
- package/dist/env-block.js +43 -5
- package/dist/scheduler.js +101 -21
- package/dist/sessions.js +524 -0
- package/dist/system.js +89 -13
- package/dist/tools/dir-cache.js +7 -0
- package/dist/tools/filesystem.js +3 -2
- package/dist/tools/registry.js +1 -0
- package/dist/tools/ripgrep.js +256 -0
- package/dist/tools/search.js +119 -58
- package/dist/tools/shared.js +39 -0
- package/dist/tools/shell.js +7 -5
- package/dist/tools/web.js +6 -6
- package/dist/tools.js +1 -0
- package/dist/ui/diff-view.js +7 -2
- package/dist/ui/live-host.js +18 -0
- package/dist/ui/live-tail.js +9 -3
- package/dist/ui/markdown.js +26 -2
- package/dist/ui/palette.js +2 -0
- package/dist/ui/side-by-side.js +2 -2
- package/dist/ui/status-host.js +22 -0
- package/dist/ui/stream-store.js +48 -0
- package/dist/ui/tool-inspector.js +7 -1
- package/dist/ui/transcript.js +92 -38
- package/dist/zen.js +66 -13
- package/package.json +1 -1
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
// Durable multi-session store for ATOM.
|
|
2
|
+
//
|
|
3
|
+
// One JSON file per session under ~/.atom/sessions/<id>.json (ATOM_HOME
|
|
4
|
+
// override honored via auth.ts's atomDir) plus a plaintext pointer file
|
|
5
|
+
// ~/.atom/sessions/active holding the active session id.
|
|
6
|
+
//
|
|
7
|
+
// This module never touches the legacy single-file save owned by
|
|
8
|
+
// src/session.ts (~/.atom/session.json) — that file stays exactly as-is.
|
|
9
|
+
//
|
|
10
|
+
// Conventions (mirroring session.ts / auth.ts):
|
|
11
|
+
// - Writes are atomic (temp file + rename, mkdir -p) so a kill mid-write
|
|
12
|
+
// can never leave a half-written record; no .tmp leftovers on failure.
|
|
13
|
+
// - 0600 POSIX perms, best-effort on Windows (never throws for chmod).
|
|
14
|
+
// - Loads never throw: missing -> null, malformed -> null, and listings
|
|
15
|
+
// silently skip corrupt files.
|
|
16
|
+
// - provider/model/effort/mode are opaque carried fields. The literal
|
|
17
|
+
// defaults below are documented here on purpose — this module must NOT
|
|
18
|
+
// depend on DEFAULT_PROVIDER from zen.js (avoid coupling) and must NEVER
|
|
19
|
+
// import LLM clients.
|
|
20
|
+
// - No transient UI state (scroll, cursor, picker, queue) is stored.
|
|
21
|
+
//
|
|
22
|
+
// Import budget: value imports are node:fs, node:path, node:crypto and
|
|
23
|
+
// ./auth.js only (type-only imports from ./zen.js / ./providers.js are
|
|
24
|
+
// erased at compile, so the runtime DAG stays acyclic and React-free).
|
|
25
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
26
|
+
import { randomUUID } from "node:crypto";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { atomDir } from "./auth.js";
|
|
29
|
+
// Literal defaults for new sessions (opaque carried fields — see header).
|
|
30
|
+
export const SESSION_DEFAULT_PROVIDER = "opencode-zen";
|
|
31
|
+
export const SESSION_DEFAULT_MODEL = "";
|
|
32
|
+
export const SESSION_DEFAULT_EFFORT = "default";
|
|
33
|
+
export const SESSION_DEFAULT_MODE = "normal";
|
|
34
|
+
export const SESSIONS_DIRNAME = "sessions";
|
|
35
|
+
export const ACTIVE_FILENAME = "active";
|
|
36
|
+
export function sessionsDir(home) {
|
|
37
|
+
return path.join(atomDir(home), SESSIONS_DIRNAME);
|
|
38
|
+
}
|
|
39
|
+
export function sessionFilePath(id, home) {
|
|
40
|
+
return path.join(sessionsDir(home), `${id}.json`);
|
|
41
|
+
}
|
|
42
|
+
export function activeFilePath(home) {
|
|
43
|
+
return path.join(sessionsDir(home), ACTIVE_FILENAME);
|
|
44
|
+
}
|
|
45
|
+
const MONTHS = [
|
|
46
|
+
"January",
|
|
47
|
+
"February",
|
|
48
|
+
"March",
|
|
49
|
+
"April",
|
|
50
|
+
"May",
|
|
51
|
+
"June",
|
|
52
|
+
"July",
|
|
53
|
+
"August",
|
|
54
|
+
"September",
|
|
55
|
+
"October",
|
|
56
|
+
"November",
|
|
57
|
+
"December",
|
|
58
|
+
];
|
|
59
|
+
// Default display title: local date+time like "September 9, 2026 20:41:32"
|
|
60
|
+
// (long English month, unpadded day, 24h zero-padded HH:MM:SS local time).
|
|
61
|
+
// createdAt stays a separate ISO string on the record.
|
|
62
|
+
export function formatSessionTitle(date = new Date()) {
|
|
63
|
+
const d = date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
|
|
64
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
65
|
+
return (`${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()} ` +
|
|
66
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`);
|
|
67
|
+
}
|
|
68
|
+
function newSessionId() {
|
|
69
|
+
return `ses_${randomUUID().replace(/-/g, "")}`;
|
|
70
|
+
}
|
|
71
|
+
function toISODate(now) {
|
|
72
|
+
if (now === undefined)
|
|
73
|
+
return new Date().toISOString();
|
|
74
|
+
const d = now instanceof Date ? now : new Date(now);
|
|
75
|
+
return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString();
|
|
76
|
+
}
|
|
77
|
+
function safeCwd() {
|
|
78
|
+
try {
|
|
79
|
+
return process.cwd();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isRecord(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
function isNonEmptyString(value) {
|
|
89
|
+
return typeof value === "string" && value.length > 0;
|
|
90
|
+
}
|
|
91
|
+
function isValidDateString(value) {
|
|
92
|
+
return (typeof value === "string" &&
|
|
93
|
+
value.length > 0 &&
|
|
94
|
+
!Number.isNaN(Date.parse(value)));
|
|
95
|
+
}
|
|
96
|
+
// Atomic write: temp file + rename, 0600 POSIX best-effort. Cleans up the
|
|
97
|
+
// temp file when the write/rename fails so no .tmp leftovers remain. Disk
|
|
98
|
+
// errors propagate to the caller.
|
|
99
|
+
function writeFileAtomic(finalPath, content) {
|
|
100
|
+
mkdirSync(path.dirname(finalPath), { recursive: true });
|
|
101
|
+
const tmpPath = `${finalPath}.tmp.${process.pid}`;
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(tmpPath, content, "utf8");
|
|
104
|
+
try {
|
|
105
|
+
chmodSync(tmpPath, 0o600);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// best-effort on Windows; ignore
|
|
109
|
+
}
|
|
110
|
+
renameSync(tmpPath, finalPath);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
try {
|
|
114
|
+
if (existsSync(tmpPath))
|
|
115
|
+
unlinkSync(tmpPath);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// cleanup best-effort; report the original failure
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function validateUsageTotals(value) {
|
|
124
|
+
if (value === null || value === undefined)
|
|
125
|
+
return null;
|
|
126
|
+
if (!isRecord(value))
|
|
127
|
+
return null;
|
|
128
|
+
const out = {};
|
|
129
|
+
for (const key of [
|
|
130
|
+
"prompt_tokens",
|
|
131
|
+
"completion_tokens",
|
|
132
|
+
"total_tokens",
|
|
133
|
+
"cacheReadTokens",
|
|
134
|
+
"cacheWriteTokens",
|
|
135
|
+
]) {
|
|
136
|
+
const v = value[key];
|
|
137
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
|
|
138
|
+
out[key] = Math.floor(v);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function validateToolCall(value) {
|
|
144
|
+
if (!isRecord(value))
|
|
145
|
+
return false;
|
|
146
|
+
if (!isNonEmptyString(value["id"]))
|
|
147
|
+
return false;
|
|
148
|
+
const fn = value["function"];
|
|
149
|
+
if (!isRecord(fn))
|
|
150
|
+
return false;
|
|
151
|
+
if (typeof fn["name"] !== "string" || fn["name"].length === 0)
|
|
152
|
+
return false;
|
|
153
|
+
if (typeof fn["arguments"] !== "string")
|
|
154
|
+
return false;
|
|
155
|
+
if (value["type"] !== undefined && typeof value["type"] !== "string") {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
function validateChatMessage(value) {
|
|
161
|
+
if (!isRecord(value))
|
|
162
|
+
return false;
|
|
163
|
+
const role = value["role"];
|
|
164
|
+
if (role === "system" || role === "user") {
|
|
165
|
+
return typeof value["content"] === "string";
|
|
166
|
+
}
|
|
167
|
+
if (role === "assistant") {
|
|
168
|
+
const content = value["content"];
|
|
169
|
+
if (content !== undefined &&
|
|
170
|
+
content !== null &&
|
|
171
|
+
typeof content !== "string") {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const calls = value["tool_calls"];
|
|
175
|
+
if (calls !== undefined) {
|
|
176
|
+
if (!Array.isArray(calls) || calls.length === 0)
|
|
177
|
+
return false;
|
|
178
|
+
for (const c of calls) {
|
|
179
|
+
if (!validateToolCall(c))
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
if (role === "tool") {
|
|
186
|
+
return (isNonEmptyString(value["tool_call_id"]) &&
|
|
187
|
+
typeof value["content"] === "string");
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
function validateTurn(value) {
|
|
192
|
+
if (!isRecord(value))
|
|
193
|
+
return false;
|
|
194
|
+
const role = value["role"];
|
|
195
|
+
if (role !== "user" && role !== "assistant" && role !== "tool")
|
|
196
|
+
return false;
|
|
197
|
+
if (typeof value["content"] !== "string")
|
|
198
|
+
return false;
|
|
199
|
+
if (value["error"] !== undefined && typeof value["error"] !== "boolean") {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (value["thinking"] !== undefined &&
|
|
203
|
+
typeof value["thinking"] !== "boolean") {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// Strict-enough read validation: bad shape -> null (caller treats the file
|
|
209
|
+
// as missing/corrupt, never throws). Unknown extra keys are ignored.
|
|
210
|
+
// provider/model/effort/mode stay opaque (typeof string only — any id,
|
|
211
|
+
// including "", round-trips) so this store never couples to LLM clients.
|
|
212
|
+
function validateSessionRecord(data) {
|
|
213
|
+
if (!isRecord(data))
|
|
214
|
+
return null;
|
|
215
|
+
if (!isNonEmptyString(data["id"]))
|
|
216
|
+
return null;
|
|
217
|
+
if (typeof data["title"] !== "string" || data["title"].trim().length === 0) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (!isValidDateString(data["createdAt"]))
|
|
221
|
+
return null;
|
|
222
|
+
if (!isValidDateString(data["updatedAt"]))
|
|
223
|
+
return null;
|
|
224
|
+
if (typeof data["cwd"] !== "string")
|
|
225
|
+
return null;
|
|
226
|
+
if (typeof data["provider"] !== "string")
|
|
227
|
+
return null;
|
|
228
|
+
if (typeof data["model"] !== "string")
|
|
229
|
+
return null;
|
|
230
|
+
if (typeof data["effort"] !== "string")
|
|
231
|
+
return null;
|
|
232
|
+
if (typeof data["mode"] !== "string")
|
|
233
|
+
return null;
|
|
234
|
+
const history = data["history"];
|
|
235
|
+
if (!Array.isArray(history))
|
|
236
|
+
return null;
|
|
237
|
+
for (const m of history) {
|
|
238
|
+
if (!validateChatMessage(m))
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const turns = data["turns"];
|
|
242
|
+
if (!Array.isArray(turns))
|
|
243
|
+
return null;
|
|
244
|
+
for (const t of turns) {
|
|
245
|
+
if (!validateTurn(t))
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const metadata = data["metadata"];
|
|
249
|
+
return {
|
|
250
|
+
id: data["id"],
|
|
251
|
+
title: data["title"],
|
|
252
|
+
createdAt: data["createdAt"],
|
|
253
|
+
updatedAt: data["updatedAt"],
|
|
254
|
+
cwd: data["cwd"],
|
|
255
|
+
provider: data["provider"],
|
|
256
|
+
model: data["model"],
|
|
257
|
+
effort: data["effort"],
|
|
258
|
+
mode: data["mode"],
|
|
259
|
+
usageTotals: validateUsageTotals(data["usageTotals"]),
|
|
260
|
+
history: history,
|
|
261
|
+
turns: turns,
|
|
262
|
+
metadata: isRecord(metadata) ? { ...metadata } : {},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function readSessionFile(filePath) {
|
|
266
|
+
let raw;
|
|
267
|
+
try {
|
|
268
|
+
if (!existsSync(filePath))
|
|
269
|
+
return null;
|
|
270
|
+
raw = readFileSync(filePath, "utf8");
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
let data;
|
|
276
|
+
try {
|
|
277
|
+
data = JSON.parse(raw);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return validateSessionRecord(data);
|
|
283
|
+
}
|
|
284
|
+
function persistSession(session, home) {
|
|
285
|
+
writeFileAtomic(sessionFilePath(session.id, home), JSON.stringify(session, null, 2) + "\n");
|
|
286
|
+
}
|
|
287
|
+
export function createSession(opts = {}, home) {
|
|
288
|
+
const at = toISODate(opts.now);
|
|
289
|
+
let id = typeof opts.id === "string" && opts.id.length > 0 ? opts.id : newSessionId();
|
|
290
|
+
// Never silently overwrite: an explicit id that already exists (caller
|
|
291
|
+
// retry / collision) falls back to a fresh id, so ids stay unique. The
|
|
292
|
+
// check-then-write races only across processes (last-writer-wins, the
|
|
293
|
+
// documented store posture); each write itself stays atomic.
|
|
294
|
+
if (id === opts.id && opts.id) {
|
|
295
|
+
let exists = false;
|
|
296
|
+
try {
|
|
297
|
+
exists = existsSync(sessionFilePath(id, home));
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
exists = false;
|
|
301
|
+
}
|
|
302
|
+
if (exists)
|
|
303
|
+
id = newSessionId();
|
|
304
|
+
}
|
|
305
|
+
const rawTitle = typeof opts.title === "string" ? opts.title.trim() : "";
|
|
306
|
+
const session = {
|
|
307
|
+
id,
|
|
308
|
+
title: rawTitle.length > 0 ? rawTitle : formatSessionTitle(new Date(at)),
|
|
309
|
+
createdAt: at,
|
|
310
|
+
updatedAt: at,
|
|
311
|
+
cwd: typeof opts.cwd === "string" ? opts.cwd : safeCwd(),
|
|
312
|
+
provider: opts.provider ?? SESSION_DEFAULT_PROVIDER,
|
|
313
|
+
model: opts.model ?? SESSION_DEFAULT_MODEL,
|
|
314
|
+
effort: opts.effort ?? SESSION_DEFAULT_EFFORT,
|
|
315
|
+
mode: opts.mode ?? SESSION_DEFAULT_MODE,
|
|
316
|
+
usageTotals: opts.usageTotals === undefined || opts.usageTotals === null
|
|
317
|
+
? null
|
|
318
|
+
: validateUsageTotals(opts.usageTotals),
|
|
319
|
+
history: (opts.history ?? []).map((m) => ({ ...m })),
|
|
320
|
+
turns: (opts.turns ?? []).map((t) => ({ ...t })),
|
|
321
|
+
metadata: isRecord(opts.metadata) ? { ...opts.metadata } : {},
|
|
322
|
+
};
|
|
323
|
+
persistSession(session, home);
|
|
324
|
+
// First session wins the active pointer; later creates leave it alone.
|
|
325
|
+
if (getActiveSessionId(home) === null) {
|
|
326
|
+
setActiveSession(session.id, home);
|
|
327
|
+
}
|
|
328
|
+
return session;
|
|
329
|
+
}
|
|
330
|
+
export function getSession(id, home) {
|
|
331
|
+
if (typeof id !== "string" || id.length === 0)
|
|
332
|
+
return null;
|
|
333
|
+
return readSessionFile(sessionFilePath(id, home));
|
|
334
|
+
}
|
|
335
|
+
// Alias-safe full-record read.
|
|
336
|
+
export function loadSession(id, home) {
|
|
337
|
+
return getSession(id, home);
|
|
338
|
+
}
|
|
339
|
+
export function listSessions(home) {
|
|
340
|
+
let entries;
|
|
341
|
+
try {
|
|
342
|
+
entries = readdirSync(sessionsDir(home));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
const out = [];
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
if (!entry.endsWith(".json"))
|
|
350
|
+
continue;
|
|
351
|
+
const session = readSessionFile(path.join(sessionsDir(home), entry));
|
|
352
|
+
if (session)
|
|
353
|
+
out.push(session);
|
|
354
|
+
}
|
|
355
|
+
out.sort((a, b) => {
|
|
356
|
+
const updated = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
|
|
357
|
+
if (updated !== 0)
|
|
358
|
+
return updated;
|
|
359
|
+
const created = Date.parse(b.createdAt) - Date.parse(a.createdAt);
|
|
360
|
+
if (created !== 0)
|
|
361
|
+
return created;
|
|
362
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
363
|
+
});
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
export function updateSession(id, patch, home) {
|
|
367
|
+
const current = getSession(id, home);
|
|
368
|
+
if (!current || !isRecord(patch))
|
|
369
|
+
return null;
|
|
370
|
+
const { id: _droppedId, createdAt: _droppedCreatedAt, ...rest } = patch;
|
|
371
|
+
void _droppedId;
|
|
372
|
+
void _droppedCreatedAt;
|
|
373
|
+
const candidate = {
|
|
374
|
+
...current,
|
|
375
|
+
...rest,
|
|
376
|
+
id: current.id,
|
|
377
|
+
createdAt: current.createdAt,
|
|
378
|
+
updatedAt: typeof rest["updatedAt"] === "string" &&
|
|
379
|
+
isValidDateString(rest["updatedAt"])
|
|
380
|
+
? rest["updatedAt"]
|
|
381
|
+
: new Date().toISOString(),
|
|
382
|
+
};
|
|
383
|
+
if (typeof candidate["title"] === "string") {
|
|
384
|
+
candidate["title"] = candidate["title"].trim();
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(candidate["history"])) {
|
|
387
|
+
candidate["history"] = candidate["history"].map((m) => isRecord(m) ? { ...m } : m);
|
|
388
|
+
}
|
|
389
|
+
if (Array.isArray(candidate["turns"])) {
|
|
390
|
+
candidate["turns"] = candidate["turns"].map((t) => isRecord(t) ? { ...t } : t);
|
|
391
|
+
}
|
|
392
|
+
if (candidate["metadata"] !== undefined &&
|
|
393
|
+
!isRecord(candidate["metadata"])) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const valid = validateSessionRecord(candidate);
|
|
397
|
+
if (!valid)
|
|
398
|
+
return null;
|
|
399
|
+
persistSession(valid, home);
|
|
400
|
+
return valid;
|
|
401
|
+
}
|
|
402
|
+
export function renameSession(id, title, home) {
|
|
403
|
+
if (typeof title !== "string" || title.trim().length === 0)
|
|
404
|
+
return null;
|
|
405
|
+
const current = getSession(id, home);
|
|
406
|
+
if (!current)
|
|
407
|
+
return null;
|
|
408
|
+
const next = {
|
|
409
|
+
...current,
|
|
410
|
+
title: title.trim(),
|
|
411
|
+
updatedAt: new Date().toISOString(),
|
|
412
|
+
};
|
|
413
|
+
persistSession(next, home);
|
|
414
|
+
return next;
|
|
415
|
+
}
|
|
416
|
+
export function deleteSession(id, home) {
|
|
417
|
+
if (typeof id !== "string" || id.length === 0)
|
|
418
|
+
return false;
|
|
419
|
+
const filePath = sessionFilePath(id, home);
|
|
420
|
+
try {
|
|
421
|
+
if (!existsSync(filePath))
|
|
422
|
+
return false;
|
|
423
|
+
unlinkSync(filePath);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
// Clear the active pointer only when it pointed at the deleted session.
|
|
429
|
+
try {
|
|
430
|
+
if (getActiveSessionId(home) === id)
|
|
431
|
+
setActiveSession(null, home);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// never throws; active cleanup is best-effort
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
// Full-record overwrite. The stored id/createdAt win when a record already
|
|
439
|
+
// exists on disk — id/createdAt can never be mutated through save.
|
|
440
|
+
// updatedAt always bumps to now.
|
|
441
|
+
export function saveSession(session, home) {
|
|
442
|
+
if (!session || !isNonEmptyString(session.id)) {
|
|
443
|
+
throw new Error("saveSession: session.id must be a non-empty string");
|
|
444
|
+
}
|
|
445
|
+
const disk = getSession(session.id, home);
|
|
446
|
+
const candidate = {
|
|
447
|
+
...session,
|
|
448
|
+
id: disk ? disk.id : session.id,
|
|
449
|
+
createdAt: disk ? disk.createdAt : session.createdAt,
|
|
450
|
+
updatedAt: new Date().toISOString(),
|
|
451
|
+
};
|
|
452
|
+
const valid = validateSessionRecord(candidate);
|
|
453
|
+
if (!valid) {
|
|
454
|
+
throw new Error("saveSession: session record failed validation");
|
|
455
|
+
}
|
|
456
|
+
persistSession(valid, home);
|
|
457
|
+
return valid;
|
|
458
|
+
}
|
|
459
|
+
// Bump updatedAt to now (runtime message/assistant/tool mutations). The
|
|
460
|
+
// caller mutates content via updateSession/saveSession; touch only refreshes
|
|
461
|
+
// the recency marker so listings sort correctly.
|
|
462
|
+
export function touchSession(id, home) {
|
|
463
|
+
const current = getSession(id, home);
|
|
464
|
+
if (!current)
|
|
465
|
+
return null;
|
|
466
|
+
const next = { ...current, updatedAt: new Date().toISOString() };
|
|
467
|
+
persistSession(next, home);
|
|
468
|
+
return next;
|
|
469
|
+
}
|
|
470
|
+
// null clears the pointer. Unknown ids are ignored (active unchanged).
|
|
471
|
+
// Never throws.
|
|
472
|
+
export function setActiveSession(id, home) {
|
|
473
|
+
try {
|
|
474
|
+
const activePath = activeFilePath(home);
|
|
475
|
+
if (id === null) {
|
|
476
|
+
try {
|
|
477
|
+
if (existsSync(activePath))
|
|
478
|
+
unlinkSync(activePath);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// best-effort clear; ignore
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (typeof id !== "string" || id.length === 0)
|
|
486
|
+
return;
|
|
487
|
+
if (!getSession(id, home))
|
|
488
|
+
return;
|
|
489
|
+
writeFileAtomic(activePath, id);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// never throws; ignore
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
export function getActiveSessionId(home) {
|
|
496
|
+
try {
|
|
497
|
+
const activePath = activeFilePath(home);
|
|
498
|
+
if (!existsSync(activePath))
|
|
499
|
+
return null;
|
|
500
|
+
const raw = readFileSync(activePath, "utf8").trim();
|
|
501
|
+
return raw.length > 0 ? raw : null;
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
export function getActiveSession(home) {
|
|
508
|
+
const id = getActiveSessionId(home);
|
|
509
|
+
if (!id)
|
|
510
|
+
return null;
|
|
511
|
+
return getSession(id, home);
|
|
512
|
+
}
|
|
513
|
+
// Return the active session when it still exists on disk, else create (and
|
|
514
|
+
// activate, when nothing is set) a new one from opts.
|
|
515
|
+
export function ensureActiveSession(opts = {}, home) {
|
|
516
|
+
const active = getActiveSession(home);
|
|
517
|
+
if (active)
|
|
518
|
+
return active;
|
|
519
|
+
const created = createSession(opts, home);
|
|
520
|
+
// createSession only claims the pointer when none is set; a dangling
|
|
521
|
+
// pointer must be re-pointed at the replacement session.
|
|
522
|
+
setActiveSession(created.id, home);
|
|
523
|
+
return created;
|
|
524
|
+
}
|
package/dist/system.js
CHANGED
|
@@ -13,17 +13,93 @@
|
|
|
13
13
|
// arrive as `Error: ...` text inside the result (invalid args say how to
|
|
14
14
|
// fix; a denial means replan, never retry).
|
|
15
15
|
export const SYSTEM_PROMPT = [
|
|
16
|
-
"You are ATOM,
|
|
17
|
-
"",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"",
|
|
28
|
-
"
|
|
16
|
+
"You are ATOM, an autonomous AI coding agent created by beast-ofcourse (Bhavin).",
|
|
17
|
+
"Your job is to solve software-engineering tasks accurately, efficiently, and with minimal unnecessary changes.",
|
|
18
|
+
"",
|
|
19
|
+
"## Core Loop",
|
|
20
|
+
"For every task, continuously follow:",
|
|
21
|
+
"UNDERSTAND → EXPLORE → PLAN → EXECUTE → VERIFY → COMPLETE",
|
|
22
|
+
"",
|
|
23
|
+
"Do not stop merely because the code was changed. A task is complete only when the result has been verified or a concrete blocker has been established with evidence.",
|
|
24
|
+
"",
|
|
25
|
+
"## Understand",
|
|
26
|
+
"- Identify the user's actual goal, constraints, and acceptance criteria.",
|
|
27
|
+
"- Resolve ambiguity from the repository before asking questions when the answer can be discovered with tools.",
|
|
28
|
+
"- Do not assume repository structure, APIs, behavior, or configuration. Inspect them.",
|
|
29
|
+
"",
|
|
30
|
+
"## Explore",
|
|
31
|
+
"- Read relevant files before modifying them.",
|
|
32
|
+
"- Search the repository before creating new code.",
|
|
33
|
+
"- Trace existing implementations, call sites, types, configuration, and tests.",
|
|
34
|
+
"- Prefer understanding existing architecture over introducing parallel implementations.",
|
|
35
|
+
"- For unfamiliar code, inspect enough surrounding context to understand how it actually works.",
|
|
36
|
+
"",
|
|
37
|
+
"## Plan",
|
|
38
|
+
"- For non-trivial tasks, create a concise ordered todo list before implementation.",
|
|
39
|
+
"- Keep exactly one todo in progress at a time.",
|
|
40
|
+
"- Mark todos complete immediately after their work is actually finished.",
|
|
41
|
+
"- Adapt the plan when exploration or verification reveals new information.",
|
|
42
|
+
"- Do not create unnecessary work just to satisfy the plan.",
|
|
43
|
+
"",
|
|
44
|
+
"## Execute",
|
|
45
|
+
"- Make the smallest correct change that solves the underlying problem.",
|
|
46
|
+
"- Preserve existing architecture, conventions, APIs, and behavior unless the task requires changing them.",
|
|
47
|
+
"- Reuse existing utilities, abstractions, and patterns before introducing new ones.",
|
|
48
|
+
"- Fix root causes rather than symptoms.",
|
|
49
|
+
"- Handle relevant errors, edge cases, race conditions, and failure paths.",
|
|
50
|
+
"- Avoid speculative features, unnecessary refactors, and unrelated formatting changes.",
|
|
51
|
+
"- Remove dead code or obsolete logic when your change makes it unnecessary.",
|
|
52
|
+
"",
|
|
53
|
+
"## Tool Strategy",
|
|
54
|
+
"- Tools are your source of truth for the repository and environment.",
|
|
55
|
+
"- Never claim something is true when it has not been established by tool output.",
|
|
56
|
+
"- Batch independent reads, searches, inspections, and other safe operations whenever possible.",
|
|
57
|
+
"- Prefer parallel tool execution over sequential calls when operations have no dependencies.",
|
|
58
|
+
"- Do not parallelize operations that depend on each other's results or could conflict.",
|
|
59
|
+
"- After each tool result, determine what information it provides, what remains unknown, and what action has the highest value next.",
|
|
60
|
+
"- Avoid repeatedly reading the same information unless the repository changed or verification requires it.",
|
|
61
|
+
"",
|
|
62
|
+
"## Verification",
|
|
63
|
+
"- Verify behavior after implementation.",
|
|
64
|
+
"- Run the most relevant tests, typechecks, linters, builds, or targeted checks available.",
|
|
65
|
+
"- Prefer targeted verification first, then broader verification when appropriate.",
|
|
66
|
+
"- Inspect failures instead of blindly retrying.",
|
|
67
|
+
"- If a test, command, or check fails because of your change, fix it before declaring completion.",
|
|
68
|
+
"- Do not declare success based solely on compilation if runtime behavior remains unverified.",
|
|
69
|
+
"- Do not end with an unverified summary.",
|
|
70
|
+
"",
|
|
71
|
+
"## Failure Recovery",
|
|
72
|
+
"- Tool failures are information, not reasons to stop.",
|
|
73
|
+
"- Tools return failures as text such as `Error: ...`; read the error carefully and adapt.",
|
|
74
|
+
"- Invalid arguments: correct the arguments using the tool's feedback.",
|
|
75
|
+
"- Permission or capability denial: replan using an available approach.",
|
|
76
|
+
"- Environment failure: determine whether the failure is caused by ATOM, the repository, or the environment.",
|
|
77
|
+
"- Never repeat the same failed action without changing the underlying cause.",
|
|
78
|
+
"- If progress is impossible, report the exact blocker and the evidence that proves it.",
|
|
79
|
+
"",
|
|
80
|
+
"## Efficiency",
|
|
81
|
+
"- Minimize unnecessary tool calls, context usage, latency, and duplicated work.",
|
|
82
|
+
"- Prefer high-information actions that answer multiple questions at once.",
|
|
83
|
+
"- Use repository search and targeted inspection instead of reading large unrelated files.",
|
|
84
|
+
"- Keep tool chains moving: when one result enables several independent next actions, perform them together.",
|
|
85
|
+
"- Do not waste time narrating internal reasoning to the user.",
|
|
86
|
+
"",
|
|
87
|
+
"## Code Quality",
|
|
88
|
+
"- Favor simple, readable, maintainable code.",
|
|
89
|
+
"- Follow the repository's existing style rather than imposing a personal style.",
|
|
90
|
+
"- Keep abstractions proportional to the problem.",
|
|
91
|
+
"- Avoid duplicated logic.",
|
|
92
|
+
"- Keep types accurate and explicit where they improve correctness.",
|
|
93
|
+
"- Preserve backwards compatibility unless breaking behavior is explicitly required.",
|
|
94
|
+
"- Consider security, performance, concurrency, resource cleanup, and error handling when relevant.",
|
|
95
|
+
"",
|
|
96
|
+
"## Completion Contract",
|
|
97
|
+
"Before finishing, confirm:",
|
|
98
|
+
"1. The requested behavior was implemented.",
|
|
99
|
+
"2. Relevant existing behavior was preserved.",
|
|
100
|
+
"3. The implementation is internally consistent with the repository.",
|
|
101
|
+
"4. Appropriate verification was performed.",
|
|
102
|
+
"5. Remaining failures or limitations are explicitly identified.",
|
|
103
|
+
"",
|
|
104
|
+
"Your final response should be concise and factual: summarize what changed, what was verified, and any remaining blocker."
|
|
29
105
|
].join("\n");
|
package/dist/tools/dir-cache.js
CHANGED
|
@@ -76,6 +76,13 @@ function toCwdRel(absDir, cwd, dirRel) {
|
|
|
76
76
|
return dirRel;
|
|
77
77
|
return `${prefix}/${dirRel}`;
|
|
78
78
|
}
|
|
79
|
+
// Public for the ripgrep adapter: rg emits dir-relative paths, but the
|
|
80
|
+
// enumerated contract (and therefore outputs) is cwd-relative — which may
|
|
81
|
+
// climb out of the tree (`../../..`) when the search dir sits outside cwd.
|
|
82
|
+
// Exported so both paths share the one mapping (never duplicated logic).
|
|
83
|
+
export function rgRelToCwdRel(absDir, cwd, dirRel) {
|
|
84
|
+
return toCwdRel(absDir, cwd, dirRel);
|
|
85
|
+
}
|
|
79
86
|
function filterSkipped(relPaths) {
|
|
80
87
|
return relPaths.filter((rel) => {
|
|
81
88
|
if (!rel)
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -7,7 +7,7 @@ import { contentHash, fingerprintKey, readFingerprints } from "./fingerprints.js
|
|
|
7
7
|
import { appendOverflow } from "./overflow.js";
|
|
8
8
|
import { getCachedRead, invalidatePath, normalizeReadWindow, setCachedRead } from "./read-cache.js";
|
|
9
9
|
import { invalidateListingsForFile } from "./dir-cache.js";
|
|
10
|
-
import { err, invalidCall, READ_CHAR_CAP, resolveSandbox } from "./shared.js";
|
|
10
|
+
import { err, invalidCall, READ_CHAR_CAP, resolveSandbox, truncateHead } from "./shared.js";
|
|
11
11
|
// offset/limit are 1-based line numbers. Output capped at ~64KB.
|
|
12
12
|
export async function readTool(args, cwd = process.cwd()) {
|
|
13
13
|
try {
|
|
@@ -58,7 +58,8 @@ export async function readTool(args, cwd = process.cwd()) {
|
|
|
58
58
|
let out = window.map((line, i) => `${offset + i}: ${line}`).join("\n");
|
|
59
59
|
if (out.length > READ_CHAR_CAP) {
|
|
60
60
|
const full = out;
|
|
61
|
-
|
|
61
|
+
const t = truncateHead(full, READ_CHAR_CAP, "\n[truncated: output exceeded 64KB]");
|
|
62
|
+
out = appendOverflow(t.head, t.note, "file output", full);
|
|
62
63
|
}
|
|
63
64
|
try {
|
|
64
65
|
const statInfo = { mtimeMs: st.mtimeMs ?? 0, size: st.size ?? 0 };
|
package/dist/tools/registry.js
CHANGED
|
@@ -574,6 +574,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
574
574
|
"runInBackground=true for servers/watchers/slow builds, then poll with bash_output. " +
|
|
575
575
|
"WHEN NOT to use: never for reading/writing/searching files; never destructive or exfiltrating without explicit user approval; " +
|
|
576
576
|
"don't assume a TTY. " +
|
|
577
|
+
"Shell is cmd.exe on Windows (use dir; quote paths containing spaces) and POSIX sh elsewhere — the env block names it; never probe with ls/pwd/whoami. " +
|
|
577
578
|
"Foreground returns JSON {exitCode, stdout, stderr, timedOut, ...} (streams truncate with pointers). " +
|
|
578
579
|
"Background returns {backgroundTaskId, ...} immediately; the process keeps running detached. " +
|
|
579
580
|
"PRIVILEGED: no sandbox beyond cwd+timeout.",
|