atom-agent 1.3.0 → 1.5.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 +62 -0
- package/README.md +220 -224
- package/dist/App.js +922 -341
- package/dist/adapters.js +127 -14
- package/dist/agent/goal-evaluator.js +3 -0
- package/dist/agent/loop.js +211 -430
- package/dist/agent/tool-pipeline.js +398 -0
- package/dist/agent/turn-events.js +12 -0
- package/dist/cli.js +57 -8
- package/dist/compact.js +72 -8
- package/dist/config.js +19 -0
- package/dist/context-manager.js +6 -2
- package/dist/extensions.js +6 -0
- package/dist/file-diffs.js +108 -0
- package/dist/kilo.js +1 -1
- package/dist/local-discovery.js +2 -2
- package/dist/media.js +276 -0
- package/dist/overflow.js +140 -0
- package/dist/policy.js +8 -0
- package/dist/scheduler.js +38 -9
- package/dist/session-revert.js +125 -0
- package/dist/sessions.js +101 -0
- package/dist/snapshots.js +69 -0
- package/dist/system.js +2 -89
- package/dist/telemetry.js +26 -1
- package/dist/todos.js +241 -0
- package/dist/tools/filesystem.js +102 -22
- package/dist/tools/registry.js +184 -45
- package/dist/tools/ripgrep.js +7 -6
- package/dist/tools/search.js +172 -17
- package/dist/tools/shared.js +6 -0
- package/dist/tools.js +7 -39
- package/dist/ui/diff-panel.js +5 -5
- package/dist/ui/diff-view.js +16 -7
- package/dist/ui/diff.js +73 -51
- package/dist/ui/errors.js +20 -6
- package/dist/ui/input.js +24 -20
- package/dist/ui/live-tail.js +36 -1
- package/dist/ui/markdown.js +9 -4
- package/dist/ui/modals.js +6 -4
- package/dist/ui/paint-scheduler.js +120 -0
- package/dist/ui/palette.js +4 -2
- package/dist/ui/pickers.js +4 -1
- package/dist/ui/side-by-side.js +88 -27
- package/dist/ui/status-bar.js +63 -8
- package/dist/ui/stream-store.js +7 -0
- package/dist/ui/theme.js +23 -1
- package/dist/ui/todo-panel.js +5 -2
- package/dist/ui/tool-inspector.js +33 -4
- package/dist/ui/transcript.js +9 -6
- package/dist/web/events.js +93 -0
- package/dist/web/runtime.js +790 -0
- package/dist/web/server.js +570 -0
- package/dist/web/ui/app.js +1925 -0
- package/dist/web/ui/index.html +135 -0
- package/dist/web/ui/styles.css +515 -0
- package/dist/zen.js +115 -4
- package/documentation/cli.md +5 -5
- package/documentation/configuration.md +11 -6
- package/documentation/development.md +4 -3
- package/documentation/extensions.md +1 -1
- package/documentation/goals.md +1 -1
- package/documentation/index.md +4 -4
- package/documentation/providers.md +2 -3
- package/documentation/skills.md +3 -3
- package/documentation/tools.md +8 -3
- package/documentation/troubleshooting.md +1 -1
- package/examples/extensions/01-audit-gate.js +2 -2
- package/examples/extensions/02-notes-tool.js +2 -2
- package/examples/extensions/03-custom-command.js +2 -2
- package/package.json +3 -2
package/dist/scheduler.js
CHANGED
|
@@ -218,6 +218,33 @@ export function canonicalFileKey(rawPath, cwd = process.cwd()) {
|
|
|
218
218
|
}
|
|
219
219
|
return abs;
|
|
220
220
|
}
|
|
221
|
+
/** Capture the mutable-registry inputs planBatches needs (see above). */
|
|
222
|
+
export function captureSchedulerSnapshot() {
|
|
223
|
+
const names = toolNames();
|
|
224
|
+
const modes = {};
|
|
225
|
+
for (const name of names) {
|
|
226
|
+
try {
|
|
227
|
+
const mode = toolExecutionMode(name);
|
|
228
|
+
if (mode !== undefined)
|
|
229
|
+
modes[name] = mode;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// A failing mode read plans as "no hint" — fail safe, as before.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
toolNames: [...names],
|
|
237
|
+
customToolNames: names.filter((name) => {
|
|
238
|
+
try {
|
|
239
|
+
return isCustomTool(name);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}),
|
|
245
|
+
executionModes: modes,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
221
248
|
// Partition one assistant message's tool_calls into commit batches,
|
|
222
249
|
// preserving program order: consecutive batchable calls form one batch; any
|
|
223
250
|
// serial-only call closes the batch and runs as a strict serial singleton.
|
|
@@ -228,7 +255,14 @@ export function canonicalFileKey(rawPath, cwd = process.cwd()) {
|
|
|
228
255
|
// order always holds. Reads never split on each other. A later batch never
|
|
229
256
|
// moves ahead of an earlier serial call, and batches never span the block
|
|
230
257
|
// boundary.
|
|
231
|
-
|
|
258
|
+
//
|
|
259
|
+
// The optional snapshot (see captureSchedulerSnapshot) freezes the mutable
|
|
260
|
+
// registry inputs for the whole block; absent, the snapshot is captured live
|
|
261
|
+
// once up front — planning never re-reads the live registry mid-block.
|
|
262
|
+
export function planBatches(calls, snapshot) {
|
|
263
|
+
const reg = snapshot ?? captureSchedulerSnapshot();
|
|
264
|
+
const knownTools = new Set(reg.toolNames);
|
|
265
|
+
const customTools = new Set(reg.customToolNames);
|
|
232
266
|
const batches = [];
|
|
233
267
|
let open = [];
|
|
234
268
|
// Canonical file keys of the open batch ("read" and/or "write" per key).
|
|
@@ -255,12 +289,7 @@ export function planBatches(calls) {
|
|
|
255
289
|
// exactly as before, one call per batch).
|
|
256
290
|
if (calls.some((call) => {
|
|
257
291
|
const name = typeof call?.function?.name === "string" ? call.function.name : "";
|
|
258
|
-
|
|
259
|
-
return toolExecutionMode(name) === "sequential";
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
292
|
+
return reg.executionModes[name] === "sequential";
|
|
264
293
|
})) {
|
|
265
294
|
return calls.map((call) => [{ call, parsed: lenientParse(call), parallelKey: null }]);
|
|
266
295
|
}
|
|
@@ -293,13 +322,13 @@ export function planBatches(calls) {
|
|
|
293
322
|
// like the old allowlist-miss). Batch planning is never corrupted by
|
|
294
323
|
// what it cannot see; validation still runs in the loop, where failures
|
|
295
324
|
// become inline-error results.
|
|
296
|
-
if (
|
|
325
|
+
if (customTools.has(name)) {
|
|
297
326
|
singleton(call, parsed);
|
|
298
327
|
continue;
|
|
299
328
|
}
|
|
300
329
|
// Missing metadata fails safe to serial (never batch the unknown).
|
|
301
330
|
const meta = TOOL_EFFECTS[name];
|
|
302
|
-
if (malformed || !meta || !
|
|
331
|
+
if (malformed || !meta || !knownTools.has(name)) {
|
|
303
332
|
singleton(call, parsed);
|
|
304
333
|
continue;
|
|
305
334
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Session-scoped revert (ticket 08): "undo that bad turn".
|
|
2
|
+
//
|
|
3
|
+
// Composition only — no new snapshot system. Every piece already exists:
|
|
4
|
+
//
|
|
5
|
+
// - src/snapshots.ts: getCheckpoint (lookup), restoreCheckpointFiles
|
|
6
|
+
// (byte-exact, hash-verified file restore), conversationCutIndex (the
|
|
7
|
+
// turn-boundary cut shared by /rewind and forkSession), listCheckpoints
|
|
8
|
+
// (no-snapshot detection).
|
|
9
|
+
// - src/sessions.ts: getSession (read), updateSession (atomic persist of the
|
|
10
|
+
// truncated record).
|
|
11
|
+
//
|
|
12
|
+
// Ordering is the atomicity story: file restore runs BEFORE the session
|
|
13
|
+
// record is touched, so a failed restore (hash mismatch, unreadable
|
|
14
|
+
// snapshot, unknown checkpoint) leaves the session file byte-identical —
|
|
15
|
+
// nothing was written yet. If the record persist then fails (disk error,
|
|
16
|
+
// record vanished mid-flight), the just-restored files are rolled back to
|
|
17
|
+
// their pre-revert bytes best-effort and a clear error is returned; the
|
|
18
|
+
// session file itself is untouched either way (updateSession only writes on
|
|
19
|
+
// success, atomically).
|
|
20
|
+
//
|
|
21
|
+
// Forked branches are separate records (ticket 07): this writes exactly one
|
|
22
|
+
// session file and never touches any other session or fork.
|
|
23
|
+
//
|
|
24
|
+
// Import budget: node:fs (promises only) + ./sessions.js + ./snapshots.js.
|
|
25
|
+
// Never touches compact/overflow/config/context-manager, loop, todos,
|
|
26
|
+
// file-diffs, legacy save, tool execution/registry, permissions/policy,
|
|
27
|
+
// skills, or provider adapters.
|
|
28
|
+
import { promises as fsp } from "node:fs";
|
|
29
|
+
import { getSession, updateSession } from "./sessions.js";
|
|
30
|
+
import { conversationCutIndex, getCheckpoint, listCheckpoints, restoreCheckpointFiles, } from "./snapshots.js";
|
|
31
|
+
async function backupDisk(files) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
try {
|
|
35
|
+
out.push({ abs: f.abs, existed: true, bytes: await fsp.readFile(f.abs) });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
out.push({ abs: f.abs, existed: false, bytes: null });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
async function rollbackDisk(backups) {
|
|
44
|
+
for (const b of backups) {
|
|
45
|
+
try {
|
|
46
|
+
if (!b.existed || b.bytes === null) {
|
|
47
|
+
await fsp.rm(b.abs, { force: true });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
await fsp.writeFile(b.abs, b.bytes);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Best-effort: the session record is already byte-identical; a
|
|
55
|
+
// failed file rollback is reported, never thrown.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function assistantHasToolCalls(m) {
|
|
60
|
+
return m.role === "assistant" && m.tool_calls !== undefined;
|
|
61
|
+
}
|
|
62
|
+
// Revert one session's conversation AND files to a checkpoint captured
|
|
63
|
+
// earlier in its lineage. Returns a success with the persisted session and
|
|
64
|
+
// a confirmation line, or a failure with a clear error — failures never
|
|
65
|
+
// write the session file.
|
|
66
|
+
export async function revertSessionToCheckpoint(sessionId, checkpointId, home) {
|
|
67
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
68
|
+
return { ok: false, error: "revert failed: missing session id — nothing was changed" };
|
|
69
|
+
}
|
|
70
|
+
if (typeof checkpointId !== "string" || checkpointId.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: "revert failed: missing checkpoint — nothing was changed. " +
|
|
74
|
+
"Pick a checkpoint from /rewind (every write/edit auto-snapshots).",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const session = getSession(sessionId, home);
|
|
78
|
+
if (!session) {
|
|
79
|
+
return { ok: false, error: "revert failed: unknown session — nothing was changed" };
|
|
80
|
+
}
|
|
81
|
+
const cp = getCheckpoint(checkpointId);
|
|
82
|
+
if (!cp) {
|
|
83
|
+
if (listCheckpoints().length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: "no snapshots recorded — nothing to revert and nothing was changed. " +
|
|
87
|
+
"Every write/edit auto-snapshots; make a file edit, then pick a checkpoint via /rewind.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { ok: false, error: "revert failed: checkpoint no longer available — nothing was changed" };
|
|
91
|
+
}
|
|
92
|
+
const backups = await backupDisk(cp.files);
|
|
93
|
+
// Files first: a failed restore returns before the record is touched,
|
|
94
|
+
// so the session file stays byte-identical.
|
|
95
|
+
const filesMsg = await restoreCheckpointFiles(cp.id);
|
|
96
|
+
if (filesMsg.startsWith("Error:")) {
|
|
97
|
+
return { ok: false, error: `${filesMsg} — session left exactly as it was` };
|
|
98
|
+
}
|
|
99
|
+
// Turn-boundary cut (the /rewind + forkSession rule, reused — never
|
|
100
|
+
// reimplemented): drops the whole turn containing the mark so
|
|
101
|
+
// assistant/tool_call pairing can never split. Marks clamp to the live
|
|
102
|
+
// lengths, so a checkpoint from a longer lineage can only shrink.
|
|
103
|
+
const historyCut = conversationCutIndex(session.history.map((m) => ({ role: m.role, hasToolCalls: assistantHasToolCalls(m) })), cp.historyLength, 1);
|
|
104
|
+
const turnsCut = conversationCutIndex(session.turns.map((t) => ({ role: t.role })), cp.turnsLength, 0);
|
|
105
|
+
const droppedMessages = session.history.length - historyCut;
|
|
106
|
+
const next = updateSession(sessionId, {
|
|
107
|
+
history: session.history.slice(0, historyCut),
|
|
108
|
+
turns: session.turns.slice(0, turnsCut),
|
|
109
|
+
}, home);
|
|
110
|
+
if (!next) {
|
|
111
|
+
await rollbackDisk(backups);
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: "revert failed: could not save the rewound session (record rejected or vanished) — " +
|
|
115
|
+
"files were restored to their pre-revert state and the session is unchanged",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
session: next,
|
|
121
|
+
message: `(reverted "${session.title}" to checkpoint #${cp.seq} "${cp.label}" — ` +
|
|
122
|
+
`dropped ${droppedMessages} message(s), restored ${cp.files.length} file(s); ` +
|
|
123
|
+
`shell side effects were never snapshotted and are unchanged)`,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/sessions.js
CHANGED
|
@@ -355,6 +355,107 @@ export function getSession(id, home) {
|
|
|
355
355
|
export function loadSession(id, home) {
|
|
356
356
|
return getSession(id, home);
|
|
357
357
|
}
|
|
358
|
+
// Turn-boundary cut for forks (ticket 07): the /rewind precedent's rule
|
|
359
|
+
// (conversationCutIndex in snapshots.ts) — drop the whole turn containing
|
|
360
|
+
// the mark so assistant/tool_call pairing can never split. Duplicated
|
|
361
|
+
// locally (not imported) to honor this module's import budget (node:fs,
|
|
362
|
+
// node:path, node:crypto, ./auth.js, ./goal.js only).
|
|
363
|
+
function forkCutIndex(messages, mark, keepFirst) {
|
|
364
|
+
const len = messages.length;
|
|
365
|
+
const floor = Math.max(0, Math.floor(keepFirst));
|
|
366
|
+
if (len <= floor)
|
|
367
|
+
return len;
|
|
368
|
+
const m = Math.max(floor, Math.min(Number.isFinite(mark) ? Math.floor(mark) : len, len));
|
|
369
|
+
let turnStart = -1;
|
|
370
|
+
for (let i = m - 1; i >= floor; i--) {
|
|
371
|
+
if (messages[i]?.role === "user") {
|
|
372
|
+
turnStart = i;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (turnStart === -1)
|
|
377
|
+
return floor;
|
|
378
|
+
for (let i = turnStart; i < m; i++) {
|
|
379
|
+
const msg = messages[i];
|
|
380
|
+
if (msg !== undefined &&
|
|
381
|
+
msg.role === "assistant" &&
|
|
382
|
+
msg.hasToolCalls !== true) {
|
|
383
|
+
return m;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return turnStart;
|
|
387
|
+
}
|
|
388
|
+
// JSON deep copy for forked payloads (history/turns/metadata): the fork's
|
|
389
|
+
// file is independent on disk either way, but a deep copy also keeps the
|
|
390
|
+
// two in-memory records from aliasing nested objects. Falls back to the
|
|
391
|
+
// original reference when the value is not JSON-serializable (records are
|
|
392
|
+
// JSON-file shaped, so this path is defensive only).
|
|
393
|
+
function deepCopyJson(value) {
|
|
394
|
+
try {
|
|
395
|
+
const text = JSON.stringify(value);
|
|
396
|
+
if (text === undefined)
|
|
397
|
+
return value;
|
|
398
|
+
return JSON.parse(text);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Fork a session at a message (ticket 07): "try another approach from here".
|
|
405
|
+
// Reads the source record and persists a brand-new session (fresh stable id
|
|
406
|
+
// from the same scheme as createSession, createdAt/updatedAt = now) holding
|
|
407
|
+
// the source's history up to atMessageIndex and nothing after, cut at a turn
|
|
408
|
+
// boundary via forkCutIndex. atMessageIndex counts history messages to keep
|
|
409
|
+
// (checkpoint-length semantics, like cp.historyLength in the rewind path);
|
|
410
|
+
// omitted/NaN/Infinity forks at the tip, out-of-range values clamp.
|
|
411
|
+
// The display transcript (turns) is sliced at the analogous boundary (the
|
|
412
|
+
// system prompt at history[0] has no turns counterpart, hence the offset).
|
|
413
|
+
// goal + metadata (todos/filediffs/extension keys) ride over opaquely so the
|
|
414
|
+
// branch continues with full context; usageTotals resets to null so the new
|
|
415
|
+
// branch accrues its own spend. provider/model/effort/mode/cwd are carried.
|
|
416
|
+
// The source file is only read, never written; the active pointer is never
|
|
417
|
+
// touched. Returns null when the source id is missing/unknown. Disk errors
|
|
418
|
+
// from the fork write propagate to the caller.
|
|
419
|
+
export function forkSession(sourceId, atMessageIndex, home) {
|
|
420
|
+
if (typeof sourceId !== "string" || sourceId.length === 0)
|
|
421
|
+
return null;
|
|
422
|
+
const source = getSession(sourceId, home);
|
|
423
|
+
if (!source)
|
|
424
|
+
return null;
|
|
425
|
+
const mark = atMessageIndex === undefined ? source.history.length : atMessageIndex;
|
|
426
|
+
const historyCut = forkCutIndex(source.history.map((m) => ({
|
|
427
|
+
role: m.role,
|
|
428
|
+
hasToolCalls: m.role === "assistant" &&
|
|
429
|
+
m.tool_calls !== undefined,
|
|
430
|
+
})), mark, 1);
|
|
431
|
+
const systemOffset = source.history[0]?.role === "system" ? 1 : 0;
|
|
432
|
+
const turnsMark = Math.max(0, Math.min(historyCut - systemOffset, source.turns.length));
|
|
433
|
+
const turnsCut = forkCutIndex(source.turns.map((t) => ({
|
|
434
|
+
role: t.role,
|
|
435
|
+
hasToolCalls: t.tool_calls !== undefined,
|
|
436
|
+
})), turnsMark, 0);
|
|
437
|
+
const at = new Date().toISOString();
|
|
438
|
+
const forked = {
|
|
439
|
+
id: newSessionId(),
|
|
440
|
+
title: `${source.title} (fork)`,
|
|
441
|
+
createdAt: at,
|
|
442
|
+
updatedAt: at,
|
|
443
|
+
cwd: source.cwd,
|
|
444
|
+
provider: source.provider,
|
|
445
|
+
model: source.model,
|
|
446
|
+
effort: source.effort,
|
|
447
|
+
mode: source.mode,
|
|
448
|
+
usageTotals: null,
|
|
449
|
+
// Opaque carry-over (tolerantly validated — a trashed goal reads as
|
|
450
|
+
// no-goal, never throws), same posture as createSession.
|
|
451
|
+
goal: serializeGoalForPersist(restoreGoalFromPersist(source.goal)),
|
|
452
|
+
history: deepCopyJson(source.history.slice(0, historyCut)),
|
|
453
|
+
turns: deepCopyJson(source.turns.slice(0, turnsCut)),
|
|
454
|
+
metadata: deepCopyJson({ ...source.metadata }),
|
|
455
|
+
};
|
|
456
|
+
persistSession(forked, home);
|
|
457
|
+
return forked;
|
|
458
|
+
}
|
|
358
459
|
export function listSessions(home) {
|
|
359
460
|
let entries;
|
|
360
461
|
try {
|
package/dist/snapshots.js
CHANGED
|
@@ -119,6 +119,34 @@ function newCheckpointId(nextSeq) {
|
|
|
119
119
|
return `${Date.now().toString(36)}-${nextSeq.toString(36)}${randomBytes(3).toString("hex")}`;
|
|
120
120
|
}
|
|
121
121
|
async function readPrior(abs) {
|
|
122
|
+
let st;
|
|
123
|
+
try {
|
|
124
|
+
const s = await fsp.stat(abs);
|
|
125
|
+
st = s;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return { abs, existed: false, hash: null, bytes: null, overflowPath: null };
|
|
129
|
+
}
|
|
130
|
+
if (!st.isFile())
|
|
131
|
+
return { abs, existed: false, hash: null, bytes: null, overflowPath: null };
|
|
132
|
+
// Large files spill via STREAMING copy (constant memory): a full readFile
|
|
133
|
+
// just to decide the file is too big would itself OOM on GB inputs.
|
|
134
|
+
if (st.size > SNAPSHOT_OVERFLOW_BYTES) {
|
|
135
|
+
try {
|
|
136
|
+
pruneStaleSnapshotOverflow();
|
|
137
|
+
const dir = snapshotDir();
|
|
138
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
139
|
+
const name = `snapshot-${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}.bin`;
|
|
140
|
+
const file = path.join(dir, name);
|
|
141
|
+
const hash = await streamCopyWithHash(abs, file);
|
|
142
|
+
return { abs, existed: true, hash, bytes: null, overflowPath: file };
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Streaming failed (disk/perm) — fall through to the bounded read
|
|
146
|
+
// below, which keeps small files restorable; a huge file here can
|
|
147
|
+
// still press memory, but only when the disk path already failed.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
122
150
|
let bytes = null;
|
|
123
151
|
try {
|
|
124
152
|
bytes = await fsp.readFile(abs);
|
|
@@ -143,6 +171,47 @@ async function readPrior(abs) {
|
|
|
143
171
|
}
|
|
144
172
|
return { abs, existed: true, hash, bytes, overflowPath: null };
|
|
145
173
|
}
|
|
174
|
+
// Constant-memory file copy that hashes while streaming: the hash covers
|
|
175
|
+
// exactly the bytes landed on disk (restore re-verifies against it).
|
|
176
|
+
function streamCopyWithHash(src, dest) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const hash = createHash("sha256");
|
|
179
|
+
let settled = false;
|
|
180
|
+
const fail = (e) => {
|
|
181
|
+
if (settled)
|
|
182
|
+
return;
|
|
183
|
+
settled = true;
|
|
184
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
185
|
+
};
|
|
186
|
+
let rs;
|
|
187
|
+
let ws;
|
|
188
|
+
try {
|
|
189
|
+
rs = fs.createReadStream(src);
|
|
190
|
+
ws = fs.createWriteStream(dest);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
fail(e);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
rs.on("error", fail);
|
|
197
|
+
ws.on("error", fail);
|
|
198
|
+
rs.on("data", (chunk) => {
|
|
199
|
+
hash.update(chunk);
|
|
200
|
+
});
|
|
201
|
+
ws.on("finish", () => {
|
|
202
|
+
if (settled)
|
|
203
|
+
return;
|
|
204
|
+
settled = true;
|
|
205
|
+
try {
|
|
206
|
+
resolve(hash.digest("hex"));
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
rs.pipe(ws);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
146
215
|
function pushCheckpoint(label, files, marks) {
|
|
147
216
|
seq += 1;
|
|
148
217
|
const cp = {
|
package/dist/system.js
CHANGED
|
@@ -13,93 +13,6 @@
|
|
|
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
|
-
"## 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."
|
|
16
|
+
"You are ATOM, An AI coding agent created by beast-ofcourse (Bhavin bogam).",
|
|
17
|
+
"Tools: read, write, edit, grep, glob, bash, bash_output, webfetch, websearch, ask_question, todowrite, todo_get, todo_update, update_goal (goal turns only).",
|
|
105
18
|
].join("\n");
|
package/dist/telemetry.js
CHANGED
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
53
53
|
import * as path from "node:path";
|
|
54
54
|
import { randomUUID } from "node:crypto";
|
|
55
|
+
import { fileURLToPath } from "node:url";
|
|
55
56
|
import { scrubSecrets } from "./policy.js";
|
|
56
57
|
import { atomDir } from "./auth.js";
|
|
57
58
|
export const TELEMETRY_VERSION = 1;
|
|
@@ -578,6 +579,30 @@ export function summarizeTelemetry(sessions) {
|
|
|
578
579
|
.sort((a, b) => b.calls - a.calls || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
579
580
|
return agg;
|
|
580
581
|
}
|
|
582
|
+
// App version stamped onto session records (Temp-session gap: every record
|
|
583
|
+
// carried atomVersion:null, so traces from different builds were
|
|
584
|
+
// indistinguishable). Resolved once from the package manifest beside the
|
|
585
|
+
// source tree — dist/ mirrors src/, so ../package.json holds in both
|
|
586
|
+
// layouts. Cached, never throws: null when unreadable keeps today's shape.
|
|
587
|
+
let cachedAtomVersion;
|
|
588
|
+
export function resolveAtomVersion() {
|
|
589
|
+
if (cachedAtomVersion !== undefined)
|
|
590
|
+
return cachedAtomVersion;
|
|
591
|
+
try {
|
|
592
|
+
const here = fileURLToPath(import.meta.url);
|
|
593
|
+
const raw = readFileSync(path.join(path.dirname(here), "..", "package.json"), "utf8");
|
|
594
|
+
const v = JSON.parse(raw).version;
|
|
595
|
+
cachedAtomVersion = typeof v === "string" && v.length > 0 ? v : null;
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
cachedAtomVersion = null;
|
|
599
|
+
}
|
|
600
|
+
return cachedAtomVersion;
|
|
601
|
+
}
|
|
602
|
+
// Test seam: drop the cached manifest read (production never calls this).
|
|
603
|
+
export function resetAtomVersion() {
|
|
604
|
+
cachedAtomVersion = undefined;
|
|
605
|
+
}
|
|
581
606
|
// In-memory trace for one session plus atomic turn-boundary persistence.
|
|
582
607
|
// Every public method is safe to call with null/undefined turn ids and never
|
|
583
608
|
// throws; when disabled, all record methods are no-ops.
|
|
@@ -607,7 +632,7 @@ export class TelemetryRecorder {
|
|
|
607
632
|
sessionId: this.sessionId,
|
|
608
633
|
startedAt: toIso(startedMs),
|
|
609
634
|
endedAt: null,
|
|
610
|
-
atomVersion: opts.atomVersion
|
|
635
|
+
atomVersion: opts.atomVersion !== undefined ? opts.atomVersion : resolveAtomVersion(),
|
|
611
636
|
project: opts.project !== undefined ? opts.project : projectBasename(),
|
|
612
637
|
provider: opts.provider ?? "unknown",
|
|
613
638
|
model: opts.model ?? "unknown",
|