agent-sh 0.14.7 → 0.14.9

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.
Files changed (54) hide show
  1. package/dist/agent/agent-loop.d.ts +0 -4
  2. package/dist/agent/agent-loop.js +8 -166
  3. package/dist/agent/entry-format.d.ts +5 -0
  4. package/dist/agent/entry-format.js +9 -0
  5. package/dist/agent/extensions/rolling-history/constants.d.ts +1 -0
  6. package/dist/agent/extensions/rolling-history/constants.js +1 -0
  7. package/dist/agent/extensions/rolling-history/index.d.ts +4 -0
  8. package/dist/agent/extensions/rolling-history/index.js +203 -0
  9. package/dist/agent/extensions/rolling-history/recall.d.ts +4 -0
  10. package/dist/agent/extensions/rolling-history/recall.js +122 -0
  11. package/dist/agent/extensions/rolling-history/strategy.d.ts +70 -0
  12. package/dist/agent/extensions/rolling-history/strategy.js +336 -0
  13. package/dist/agent/host-types.d.ts +0 -3
  14. package/dist/agent/index.js +46 -5
  15. package/dist/agent/live-view.d.ts +57 -0
  16. package/dist/agent/live-view.js +238 -0
  17. package/dist/agent/llm-client.d.ts +1 -0
  18. package/dist/agent/llm-client.js +1 -1
  19. package/dist/agent/session-store.d.ts +90 -0
  20. package/dist/agent/session-store.js +288 -0
  21. package/dist/agent/store.d.ts +74 -0
  22. package/dist/agent/store.js +284 -0
  23. package/dist/agent/subagent.js +2 -2
  24. package/dist/agent/tool-protocol.d.ts +11 -11
  25. package/dist/cli/auth/discover.js +18 -1
  26. package/dist/cli/index.js +4 -2
  27. package/dist/core/index.d.ts +0 -1
  28. package/dist/core/index.js +0 -1
  29. package/dist/core/settings.d.ts +5 -1
  30. package/dist/core/settings.js +62 -1
  31. package/dist/extensions/index.d.ts +1 -0
  32. package/dist/shell/events.d.ts +1 -0
  33. package/dist/shell/input-handler.js +4 -0
  34. package/dist/shell/strategies/bash.js +6 -2
  35. package/dist/shell/tui-renderer.js +5 -2
  36. package/dist/utils/diff-renderer.js +9 -7
  37. package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
  38. package/examples/extensions/ashi/package.json +2 -2
  39. package/examples/extensions/ashi/src/capture.ts +1 -1
  40. package/examples/extensions/ashi/src/cli.ts +3 -4
  41. package/examples/extensions/ashi/src/compaction.ts +6 -2
  42. package/examples/extensions/ashi/src/frontend.ts +13 -10
  43. package/examples/extensions/ashi/src/multi-session-store.ts +35 -12
  44. package/examples/extensions/ashi/src/session-commands.ts +1 -1
  45. package/examples/extensions/ashi/src/user-shell-intents.ts +17 -0
  46. package/examples/extensions/ollama.ts +3 -2
  47. package/examples/extensions/opencode-provider.ts +1 -2
  48. package/examples/extensions/zai-coding-plan.ts +1 -2
  49. package/package.json +13 -1
  50. package/dist/agent/conversation-state.d.ts +0 -142
  51. package/dist/agent/conversation-state.js +0 -788
  52. package/dist/agent/history-file.d.ts +0 -81
  53. package/dist/agent/history-file.js +0 -271
  54. package/examples/extensions/ashi/src/session-store.ts +0 -363
@@ -1,81 +0,0 @@
1
- import { type NuclearEntry } from "./nuclear-form.js";
2
- export interface HistoryAdapter {
3
- append(entries: NuclearEntry[]): Promise<void>;
4
- readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
5
- search(query: string): Promise<{
6
- entry: NuclearEntry;
7
- line: string;
8
- }[]>;
9
- findBySeq(seq: number): Promise<NuclearEntry | null>;
10
- /** Walk parent pointers from a leaf back to the root. Tree-aware adapters only. */
11
- getBranch?(leafSeq: number): Promise<NuclearEntry[]>;
12
- /** Return every entry, including sibling branches. Tree-aware adapters only. */
13
- getTree?(): Promise<NuclearEntry[]>;
14
- /** Move the active leaf for the next append. Tree-aware adapters only. */
15
- setLeaf?(seq: number): void;
16
- }
17
- export declare class InMemoryHistory implements HistoryAdapter {
18
- private entries;
19
- constructor(initial?: NuclearEntry[]);
20
- append(entries: NuclearEntry[]): Promise<void>;
21
- readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
22
- search(query: string): Promise<{
23
- entry: NuclearEntry;
24
- line: string;
25
- }[]>;
26
- findBySeq(seq: number): Promise<NuclearEntry | null>;
27
- }
28
- export declare class NoopHistory implements HistoryAdapter {
29
- append(): Promise<void>;
30
- readRecent(): Promise<NuclearEntry[]>;
31
- search(): Promise<{
32
- entry: NuclearEntry;
33
- line: string;
34
- }[]>;
35
- findBySeq(): Promise<NuclearEntry | null>;
36
- }
37
- export declare class HistoryFile implements HistoryAdapter {
38
- readonly instanceId: string;
39
- private filePath;
40
- private lockPath;
41
- constructor(opts?: {
42
- filePath?: string;
43
- instanceId?: string;
44
- });
45
- /**
46
- * Append entries atomically. Uses O_APPEND for concurrency safety.
47
- * Triggers truncation check after writing.
48
- */
49
- append(entries: NuclearEntry[]): Promise<void>;
50
- /**
51
- * Read the most recent N entries from the history file, filtered.
52
- * Read-only tool calls (read_file, grep, glob, ls) are excluded so
53
- * the returned entries are all meaningful conversation turns.
54
- */
55
- readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
56
- /**
57
- * Search history entries by regex/keyword, scanning the file from the
58
- * end. Caps at ~20 MB of content to bound cost on 100 MB history files.
59
- */
60
- search(query: string): Promise<{
61
- entry: NuclearEntry;
62
- line: string;
63
- }[]>;
64
- /** Find a single entry by sequence number, streaming from the file end. */
65
- findBySeq(seq: number): Promise<NuclearEntry | null>;
66
- getSize(): Promise<number>;
67
- /**
68
- * Yield lines from the file in reverse order (newest-first). Buffers
69
- * pre-first-newline bytes across chunks to stitch lines that straddle
70
- * a boundary; carries raw bytes (not strings) so UTF-8 characters split
71
- * by a chunk boundary are never decoded mid-codepoint.
72
- */
73
- private streamReverseLines;
74
- /**
75
- * Truncate from the front if file exceeds historyMaxBytes.
76
- * Uses a lock file for the rewrite operation.
77
- */
78
- private maybeTruncate;
79
- private acquireLock;
80
- private releaseLock;
81
- }
@@ -1,271 +0,0 @@
1
- /**
2
- * Persistent history file — append-only JSONL at ~/.agent-sh/history.
3
- *
4
- * Multiple agent-sh instances can write concurrently — each line is under
5
- * PIPE_BUF so O_APPEND writes are atomic. Only truncation (which rewrites
6
- * the file) uses a lock file for safety.
7
- */
8
- import * as fs from "node:fs/promises";
9
- import * as fss from "node:fs";
10
- import * as path from "node:path";
11
- import * as crypto from "node:crypto";
12
- import { CONFIG_DIR, getSettings } from "../core/settings.js";
13
- import { serializeEntry, deserializeEntry, isReadOnly, compileSearchRegex, matchEntry, } from "./nuclear-form.js";
14
- const HISTORY_PATH = path.join(CONFIG_DIR, "history");
15
- const LOCK_STALE_MS = 10_000; // consider lock stale after 10s
16
- export class InMemoryHistory {
17
- entries;
18
- constructor(initial = []) {
19
- this.entries = [...initial];
20
- }
21
- async append(entries) {
22
- this.entries.push(...entries);
23
- }
24
- async readRecent(maxEntries) {
25
- const filtered = this.entries.filter((e) => !isReadOnly(e));
26
- return maxEntries ? filtered.slice(-maxEntries) : filtered;
27
- }
28
- async search(query) {
29
- if (!query.trim())
30
- return [];
31
- const re = compileSearchRegex(query);
32
- const out = [];
33
- for (let i = this.entries.length - 1; i >= 0; i--) {
34
- const m = matchEntry(this.entries[i], re);
35
- if (m)
36
- out.push(m);
37
- }
38
- return out;
39
- }
40
- async findBySeq(seq) {
41
- return this.entries.find((e) => e.seq === seq) ?? null;
42
- }
43
- }
44
- export class NoopHistory {
45
- async append() { }
46
- async readRecent() { return []; }
47
- async search() { return []; }
48
- async findBySeq() { return null; }
49
- }
50
- export class HistoryFile {
51
- instanceId;
52
- filePath;
53
- lockPath;
54
- constructor(opts) {
55
- this.filePath = opts?.filePath ?? HISTORY_PATH;
56
- this.lockPath = this.filePath + ".lock";
57
- this.instanceId = opts?.instanceId ?? crypto.randomBytes(2).toString("hex");
58
- // Custom paths may target a dir that doesn't exist yet; create sync so
59
- // the first append() can't race with the mkdir.
60
- try {
61
- fss.mkdirSync(path.dirname(this.filePath), { recursive: true });
62
- }
63
- catch { /* ignore */ }
64
- }
65
- /**
66
- * Append entries atomically. Uses O_APPEND for concurrency safety.
67
- * Triggers truncation check after writing.
68
- */
69
- async append(entries) {
70
- if (entries.length === 0)
71
- return;
72
- const lines = entries.map((e) => serializeEntry(e) + "\n").join("");
73
- await fs.appendFile(this.filePath, lines, { flag: "a" });
74
- await this.maybeTruncate();
75
- }
76
- /**
77
- * Read the most recent N entries from the history file, filtered.
78
- * Read-only tool calls (read_file, grep, glob, ls) are excluded so
79
- * the returned entries are all meaningful conversation turns.
80
- */
81
- async readRecent(maxEntries) {
82
- maxEntries ??= getSettings().historyStartupEntries;
83
- const want = maxEntries * 3 + 10;
84
- const recent = []; // newest-first
85
- for await (const line of this.streamReverseLines()) {
86
- const entry = deserializeEntry(line);
87
- if (entry && !isReadOnly(entry))
88
- recent.push(entry);
89
- if (recent.length >= want)
90
- break;
91
- }
92
- // Caller expects oldest-to-newest order.
93
- return recent.reverse().slice(-maxEntries);
94
- }
95
- /**
96
- * Search history entries by regex/keyword, scanning the file from the
97
- * end. Caps at ~20 MB of content to bound cost on 100 MB history files.
98
- */
99
- async search(query) {
100
- if (!query.trim())
101
- return [];
102
- const regex = compileSearchRegex(query);
103
- const budgetBytes = 20 * 1024 * 1024;
104
- let scanned = 0;
105
- const results = [];
106
- for await (const line of this.streamReverseLines()) {
107
- scanned += line.length + 1;
108
- if (scanned > budgetBytes)
109
- break;
110
- const entry = deserializeEntry(line);
111
- if (!entry)
112
- continue;
113
- const m = matchEntry(entry, regex);
114
- if (m)
115
- results.push(m);
116
- }
117
- return results;
118
- }
119
- /** Find a single entry by sequence number, streaming from the file end. */
120
- async findBySeq(seq) {
121
- for await (const line of this.streamReverseLines()) {
122
- const entry = deserializeEntry(line);
123
- if (entry && entry.seq === seq)
124
- return entry;
125
- }
126
- return null;
127
- }
128
- async getSize() {
129
- try {
130
- const stat = await fs.stat(this.filePath);
131
- return stat.size;
132
- }
133
- catch {
134
- return 0;
135
- }
136
- }
137
- /**
138
- * Yield lines from the file in reverse order (newest-first). Buffers
139
- * pre-first-newline bytes across chunks to stitch lines that straddle
140
- * a boundary; carries raw bytes (not strings) so UTF-8 characters split
141
- * by a chunk boundary are never decoded mid-codepoint.
142
- */
143
- async *streamReverseLines(chunkBytes = 1 << 20) {
144
- let handle;
145
- let fileSize;
146
- try {
147
- const stat = await fs.stat(this.filePath);
148
- fileSize = stat.size;
149
- if (fileSize === 0)
150
- return;
151
- handle = await fs.open(this.filePath, "r");
152
- }
153
- catch {
154
- return;
155
- }
156
- try {
157
- let position = fileSize;
158
- let pending = Buffer.alloc(0);
159
- while (position > 0) {
160
- const readSize = Math.min(chunkBytes, position);
161
- position -= readSize;
162
- const buf = Buffer.alloc(readSize);
163
- await handle.read(buf, 0, readSize, position);
164
- // pending: start-bytes of a line whose first \n lives in this chunk.
165
- const combined = Buffer.concat([buf, pending]);
166
- const newlineIdxs = [];
167
- for (let i = 0; i < combined.length; i++) {
168
- if (combined[i] === 0x0A)
169
- newlineIdxs.push(i);
170
- }
171
- if (newlineIdxs.length === 0) {
172
- pending = combined;
173
- continue;
174
- }
175
- const firstNl = newlineIdxs[0];
176
- const lastNl = newlineIdxs[newlineIdxs.length - 1];
177
- // Post-last-\n: a line straddling into the later chunk (completed
178
- // here because `pending` was appended at the end of `combined`).
179
- const trailing = combined.subarray(lastNl + 1);
180
- if (trailing.length > 0)
181
- yield trailing.toString("utf-8");
182
- for (let i = newlineIdxs.length - 1; i >= 1; i--) {
183
- const seg = combined.subarray(newlineIdxs[i - 1] + 1, newlineIdxs[i]);
184
- if (seg.length > 0)
185
- yield seg.toString("utf-8");
186
- }
187
- // Pre-first-\n: partial if there's more file to the left, else complete.
188
- const leading = combined.subarray(0, firstNl);
189
- if (position === 0) {
190
- if (leading.length > 0)
191
- yield leading.toString("utf-8");
192
- pending = Buffer.alloc(0);
193
- }
194
- else {
195
- pending = leading;
196
- }
197
- }
198
- if (pending.length > 0)
199
- yield pending.toString("utf-8");
200
- }
201
- finally {
202
- await handle.close();
203
- }
204
- }
205
- // ── Truncation ──────────────────────────────────────────────────
206
- /**
207
- * Truncate from the front if file exceeds historyMaxBytes.
208
- * Uses a lock file for the rewrite operation.
209
- */
210
- async maybeTruncate() {
211
- const maxBytes = getSettings().historyMaxBytes;
212
- const size = await this.getSize();
213
- // Only truncate when significantly over (150%) to avoid frequent rewrites
214
- if (size <= maxBytes * 1.5)
215
- return;
216
- const acquired = await this.acquireLock();
217
- if (!acquired)
218
- return; // another process is truncating
219
- try {
220
- let content;
221
- try {
222
- content = await fs.readFile(this.filePath, "utf-8");
223
- }
224
- catch {
225
- return;
226
- }
227
- const lines = content.split("\n").filter(Boolean);
228
- // Drop oldest lines until under maxBytes
229
- let totalBytes = Buffer.byteLength(content, "utf-8");
230
- let dropCount = 0;
231
- while (totalBytes > maxBytes && dropCount < lines.length - 1) {
232
- totalBytes -= Buffer.byteLength(lines[dropCount] + "\n", "utf-8");
233
- dropCount++;
234
- }
235
- if (dropCount === 0)
236
- return;
237
- const remaining = lines.slice(dropCount).join("\n") + "\n";
238
- // Atomic rewrite: write temp → rename
239
- const tmpPath = this.filePath + ".tmp." + process.pid;
240
- await fs.writeFile(tmpPath, remaining);
241
- await fs.rename(tmpPath, this.filePath);
242
- }
243
- finally {
244
- await this.releaseLock();
245
- }
246
- }
247
- async acquireLock() {
248
- try {
249
- // Check for stale lock
250
- try {
251
- const stat = await fs.stat(this.lockPath);
252
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
253
- await fs.unlink(this.lockPath).catch(() => { });
254
- }
255
- }
256
- catch {
257
- // Lock doesn't exist — good
258
- }
259
- // O_EXCL ensures atomicity
260
- const fd = await fs.open(this.lockPath, fss.constants.O_CREAT | fss.constants.O_EXCL | fss.constants.O_WRONLY);
261
- await fd.close();
262
- return true;
263
- }
264
- catch {
265
- return false; // lock held by another process
266
- }
267
- }
268
- async releaseLock() {
269
- await fs.unlink(this.lockPath).catch(() => { });
270
- }
271
- }