@ai-ecoverse/slicc-agent 3.20.2 → 3.21.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/README.md CHANGED
@@ -104,11 +104,11 @@ const agent = await owner.connect();
104
104
  app.model = { ...createKernelModel({ kernel, root }), ...createAgentModel(agent, { storage: localStorage }) };
105
105
  ```
106
106
 
107
- `createAgentModel()` returns the `agent` and `settings` ports. Settings keeps the UI preferences in `storage`; model and thinking belong to the conversation, and accounts and models come from the worker.
107
+ `createAgentModel()` returns the `agent`, `settings`, `tray`, `sprinkles`, `memory` and `changes` ports. Settings keeps the UI preferences in `storage`; model and thinking belong to the conversation, and accounts and models come from the worker.
108
108
 
109
109
  The adapter keeps the replicated transcript and maps it on every update. User, assistant and compaction entries become messages; tool results fill in the tool calls they answer; the generation in flight shows as a streaming message; retries and running compactions show as system messages. A failed generation ends in an error card whose action fits the error: credential errors (an invalid or missing key, 401/403, not authorized) open settings, a model the account can't use offers the model picker, and anything else offers Retry. A content-filter stop is different: every later request resends the content that tripped the filter, so Retry, and any new message, would be stopped again. Content-filter stops are Bedrock's `content_filtered` and `guardrail_intervened`, Anthropic's `sensitive` and refusals, and OpenAI's `content_filter`. Their card leads with "The model's content filter stopped this reply." and offers **Drop the last turn** (spectrum ≥ 1.12.0). `AgentControl.rewind(messageId)` finds the user turn before that message and forks the cone's conversation at the entry before it (`Conversation.fork`, so `pi.agent` comes back as it was there). It points the cone at the fork and returns the turn's prompt, which spectrum puts back into the composer. Forking the very first turn creates a fresh conversation with the same model and thinking level. The filtered branch stays in storage, off the active path. The fork starts with a `slicc.rewound` entry that shows as a "Rewound 1 turn" notice, and clicking the card of an earlier filtered turn rewinds that turn.
110
110
 
111
- Each cone's active conversation is in the session document `slicc.agents` (the root until the first rewind). The host's transcript follows it across forks. Rewinding is refused while the cone itself is running or has queued input, because a fork starts with an empty inbox, but never because its scoops are busy (see Cones and scoops). A background compaction still running on the old branch finishes there. A fork also starts with fresh `pi.usage`. Credential and model errors lead with a plain sentence ("Bedrock rejected the API key.", "This model isn't available with the current account.") and keep the provider's raw error as `detail`, which spectrum (≥ 1.11.0) shows underneath. `busy()` and `queue()` read durable's live and inbox documents. Steering is the default (#4): a send or a steer goes out with `whenBusy: 'steer'`, so while a run is going it joins after the current tool round, and a `queue` send is a follow-up. User messages carry `delivered` from the host's deliveries; entries the host didn't record (from before a reload) count as steered when they follow a tool result. Cones and scoops are below; the freezer and questions come later. Licks show as spectrum's lick cards (below). slicc-spectrum is an optional peer dependency (≥ 1.27.2, where `AgentPort.freeze` is optional), used for types only, so package managers don't install it next to the agent.
111
+ Each cone's active conversation is in the session document `slicc.agents` (the root until the first rewind). The host's transcript follows it across forks. Rewinding is refused while the cone itself is running or has queued input, because a fork starts with an empty inbox, but never because its scoops are busy (see Cones and scoops). A background compaction still running on the old branch finishes there. A fork also starts with fresh `pi.usage`. Credential and model errors lead with a plain sentence ("Bedrock rejected the API key.", "This model isn't available with the current account.") and keep the provider's raw error as `detail`, which spectrum (≥ 1.11.0) shows underneath. `busy()` and `queue()` read durable's live and inbox documents. Steering is the default (#4): a send or a steer goes out with `whenBusy: 'steer'`, so while a run is going it joins after the current tool round, and a `queue` send is a follow-up. User messages carry `delivered` from the host's deliveries; entries the host didn't record (from before a reload) count as steered when they follow a tool result. Cones and scoops are below; the freezer and questions come later. Licks show as spectrum's lick cards (below). slicc-spectrum is an optional peer dependency (≥ 1.37.0, which has `ChangesPort` with copyable commands in its empty state), used for types only, so package managers don't install it next to the agent.
112
112
 
113
113
  Each tool call shows its input in the way that suits it:
114
114
  - `bash`: its command, with a timeout as the card's `meta`;
@@ -303,6 +303,26 @@ pi durable keeps every conversation and entry in the session database, and has n
303
303
  - **The `freezer` command** is the same script as `agent`: `freezer list`, `freezer show <id>` (the chat as Markdown, which replaces v6's `transcript-export`) and `freezer thaw <id>`. The gelatiere's pass reads frozen chats with it.
304
304
  - **The panel:** the transcript service adds `frozen`, and `AgentControl` adds `freeze`, `newChat`, `thaw` and `discard`.
305
305
 
306
+ ### Changes
307
+
308
+ pi has no git integration and no changes list; pi coding-agent leaves that to the user's git. So the Changes surface is git, run in the kernel, and works only when git is installed and only in git repos.
309
+
310
+ - **What's listed:** each repo's unstaged work, the working tree against the index (`git --no-optional-locks status --porcelain=v1 -z --untracked-files=all`).
311
+ - Untracked files show as added, and a rename shows as a deleted plus an added file.
312
+ - `before` is the index version (`git show :<path>`) and `after` is the working file. Binary files and files over 1 MB get null for both, which spectrum shows as "no diff".
313
+ - At most 200 files per repo are listed. There's no store of our own, and `agentId` is null, because git doesn't know which agent wrote a file.
314
+ - **Without git,** `unavailable()` says git isn't installed and how to get it (`pnpm add -g @ai-ecoverse/wasm-git`). **With git but no repo,** it says to `git init` a folder under /home or clone one with slicc-node or the extension connected; cloning over HTTPS through the page alone fails. Seven doesn't preinstall git; the user or the agent installs it with that command.
315
+ - **The `system` section** has a Git line. With `git` on PATH, it says local repositories work and that clone, fetch and push need slicc-node or the extension. Without it, it gives the agent `pnpm add -g @ai-ecoverse/wasm-git` to install it when needed. Seven doesn't preinstall git (Lars, 2026-10-09). The install line also names the other tested packages that aren't preinstalled: `@ai-ecoverse/wasi-esbuild` (esbuild) and `@ai-ecoverse/wasi-typescript` (tsc).
316
+ - **Which repos:**
317
+ - the repo of each live agent's cwd (`git rev-parse --show-toplevel`), with the active cone's first;
318
+ - plus a scan of /home and each /mnt/<name>, three levels deep and at most 200 directories per level, skipping node_modules and dot-directories.
319
+ - Repos under /tmp and /var/lib/slicc are left out.
320
+ - **When it scans:** lazily, when the surface first asks, then on file events in /home, /mnt and /scoops and when the agents change, debounced by 500 ms, with at most one scan running and one trailing scan queued, so an `unzip` or `pnpm install` costs two scans. Events inside a known repo's .git count only for its index, HEAD and refs. A new .git, or an overflow, rescans for repos.
321
+ - **Accept** stages the file (`git add -A -- <path>`), so it leaves the list; committing stays the user's job.
322
+ - **Revert** discards the unstaged change: `git restore -- <path>`, or deleting an untracked file. Spectrum asks first with its negative confirm, because this can't be undone.
323
+ - **Rewind** leaves files alone. When the dropped turn had a write, edit, bash or codemode call, its notice adds "Changed files stay; use Changes or git to revert them."
324
+ - **The port:** the transcript service adds `changes` (`{ unavailable, changes }`), and `AgentControl` adds `changesOpen`, `changeAccept` and `changeRevert`. `createAgentModel()` returns `changes`, spectrum's `ChangesPort`, with `repo` on each change.
325
+
306
326
  ### Sprinkles
307
327
 
308
328
  A sprinkle is a small HTML panel next to the chat. pi has nothing for this; it's SLICC's UI, rendered by slicc-spectrum (≥ 1.23) through its `SprinklePort`.
@@ -331,6 +351,10 @@ v6's `open`, `reload`, `close`, `send` and `chat` answer with what to do instead
331
351
 
332
352
  The built-in `sprinkles` skill teaches the files, the bridge and the command.
333
353
 
354
+ ## The kernel
355
+
356
+ `@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.23.0 <2`): the agent only calls `attachKernel`, the client end of the page kernel's port, so it must match the kernel the page runs. Seven chooses that version: slicc-bios pins the certified kernel next to the agent in `src/packages/agent/package.json`, and pnpm fills the peer from that pin. The devDependency is the lowest certified kernel (1.23.0), and the unit and integration tests run on it. A second CI job reruns the integration tests on the newest certified kernel (`NEWEST_CERTIFIED_KERNEL` in `.github/workflows/ci.yml`, 1.26.2 today), so both ends of the range are tested; it moves by hand, with each certification. If the port protocol ever breaks within 1.x, a patch release narrows the upper bound. Renovate opens PRs for the kernel, `wasi-pnpm`, the `wasm-*` packages and `slicc-cdp` but never merges them, because they move only after the homescoop coordinator certifies them.
357
+
334
358
  ## Patched dependencies
335
359
 
336
360
  Fixes to pi stay in this repository. [`patches/patches.json`](patches/patches.json) lists each one with its `kind`, `package`, `patchedVersion`, `reason`, `removeWhen` and `verify` command, and an optional `marker` (a file in the package and a string it must contain):
@@ -0,0 +1,89 @@
1
+ const MAX_TEXT = 1024 * 1024;
2
+ const MAX_FILES = 200;
3
+ const SCAN_DEPTH = 3;
4
+ const SCAN_ENTRIES = 200;
5
+ const NO_GIT = "Changes needs git, and git isn't installed. Install it with `pnpm add -g @ai-ecoverse/wasm-git`, then run `git init` in a folder under /home, or clone with slicc-node or the extension connected.";
6
+ const NO_REPO = "Changes lists the changes in git repositories, and there is none here yet. Run `git init` in a folder under /home, or clone one with slicc-node or the extension connected.";
7
+ const UNMERGED = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"];
8
+ function parseStatus(out) {
9
+ const parts = out.split("\0");
10
+ const found = [];
11
+ for (let i = 0; i < parts.length; i++) {
12
+ const part = parts[i];
13
+ if (part.length < 4) continue;
14
+ const index = part[0];
15
+ const tree = part[1];
16
+ const path = part.slice(3);
17
+ if (index === "R" || index === "C") i++;
18
+ if (UNMERGED.includes(index + tree)) found.push({ path, status: "modified", kind: "conflict" });
19
+ else if (index === "?" && tree === "?")
20
+ found.push({ path, status: "added", kind: "untracked" });
21
+ else if (tree === "D") found.push({ path, status: "deleted", kind: "tracked" });
22
+ else if (tree === "A") found.push({ path, status: "added", kind: "tracked" });
23
+ else if (tree !== " " && tree !== "!")
24
+ found.push({ path, status: "modified", kind: "tracked" });
25
+ }
26
+ return found;
27
+ }
28
+ function textOf(bytes) {
29
+ if (bytes.length > MAX_TEXT || bytes.includes(0)) return null;
30
+ try {
31
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function shown(text) {
37
+ return text.length > MAX_TEXT || text.includes("\0") || text.includes("\uFFFD") ? null : text;
38
+ }
39
+ function scanned(name) {
40
+ return name !== "node_modules" && !name.startsWith(".");
41
+ }
42
+ function within(path, root) {
43
+ return root === "/" || path === root || path.startsWith(`${root}/`);
44
+ }
45
+ function relative(path, root) {
46
+ return root === "/" ? path.slice(1) : path.slice(root.length + 1);
47
+ }
48
+ function joined(root, rel) {
49
+ return root === "/" ? `/${rel}` : `${root}/${rel}`;
50
+ }
51
+ const quote = (word) => `'${word.replaceAll("'", "'\\''")}'`;
52
+ async function run(env, command, cwd, context) {
53
+ let out = "";
54
+ let err = "";
55
+ const ran = await env.exec(
56
+ typeof command === "string" ? command : command.map(quote).join(" "),
57
+ {
58
+ cwd,
59
+ onOutput: (text, _context, info) => {
60
+ if (info.stream === "stdout") out += text;
61
+ else err += text;
62
+ }
63
+ },
64
+ context
65
+ );
66
+ return ran.ok ? { code: ran.value.exitCode, out, err } : { code: -1, out, err: ran.error.message };
67
+ }
68
+ function failure(ran, what) {
69
+ return new Error(ran.err.trim() || ran.out.trim() || `${what} failed`);
70
+ }
71
+ export {
72
+ MAX_FILES,
73
+ MAX_TEXT,
74
+ NO_GIT,
75
+ NO_REPO,
76
+ SCAN_DEPTH,
77
+ SCAN_ENTRIES,
78
+ failure,
79
+ joined,
80
+ parseStatus,
81
+ quote,
82
+ relative,
83
+ run,
84
+ scanned,
85
+ shown,
86
+ textOf,
87
+ within
88
+ };
89
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/changes/git.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\n\nexport const MAX_TEXT = 1024 * 1024;\nexport const MAX_FILES = 200;\nexport const SCAN_DEPTH = 3;\nexport const SCAN_ENTRIES = 200;\nexport const NO_GIT =\n \"Changes needs git, and git isn't installed. Install it with `pnpm add -g @ai-ecoverse/wasm-git`, then run `git init` in a folder under /home, or clone with slicc-node or the extension connected.\";\nexport const NO_REPO =\n 'Changes lists the changes in git repositories, and there is none here yet. Run `git init` in a folder under /home, or clone one with slicc-node or the extension connected.';\n\nexport type ChangeStatus = 'added' | 'modified' | 'deleted';\nexport type ChangeKind = 'tracked' | 'untracked' | 'conflict';\n\nconst UNMERGED = ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'];\n\nexport interface FileChange {\n path: string;\n repo: string;\n status: ChangeStatus;\n kind: ChangeKind;\n before: string | null;\n after: string | null;\n}\n\nexport interface ChangesView {\n unavailable: string | null;\n changes: FileChange[];\n}\n\nexport interface Pending {\n path: string;\n status: ChangeStatus;\n kind: ChangeKind;\n}\n\nexport function parseStatus(out: string): Pending[] {\n const parts = out.split('\\0');\n const found: Pending[] = [];\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i] as string;\n if (part.length < 4) continue;\n const index = part[0] as string;\n const tree = part[1] as string;\n const path = part.slice(3);\n if (index === 'R' || index === 'C') i++;\n if (UNMERGED.includes(index + tree)) found.push({ path, status: 'modified', kind: 'conflict' });\n else if (index === '?' && tree === '?')\n found.push({ path, status: 'added', kind: 'untracked' });\n else if (tree === 'D') found.push({ path, status: 'deleted', kind: 'tracked' });\n else if (tree === 'A') found.push({ path, status: 'added', kind: 'tracked' });\n else if (tree !== ' ' && tree !== '!')\n found.push({ path, status: 'modified', kind: 'tracked' });\n }\n return found;\n}\n\nexport function textOf(bytes: Uint8Array): string | null {\n if (bytes.length > MAX_TEXT || bytes.includes(0)) return null;\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n } catch {\n return null;\n }\n}\n\nexport function shown(text: string): string | null {\n return text.length > MAX_TEXT || text.includes('\\0') || text.includes('\uFFFD') ? null : text;\n}\n\nexport function scanned(name: string): boolean {\n return name !== 'node_modules' && !name.startsWith('.');\n}\n\nexport function within(path: string, root: string): boolean {\n return root === '/' || path === root || path.startsWith(`${root}/`);\n}\n\nexport function relative(path: string, root: string): string {\n return root === '/' ? path.slice(1) : path.slice(root.length + 1);\n}\n\nexport function joined(root: string, rel: string): string {\n return root === '/' ? `/${rel}` : `${root}/${rel}`;\n}\n\nexport type Ran = { code: number; out: string; err: string };\n\nexport const quote = (word: string): string => `'${word.replaceAll(\"'\", \"'\\\\''\")}'`;\n\nexport async function run(\n env: Pick<ExecutionEnv, 'exec'>,\n command: string | readonly string[],\n cwd: string,\n context: Context\n): Promise<Ran> {\n let out = '';\n let err = '';\n const ran = await env.exec(\n typeof command === 'string' ? command : command.map(quote).join(' '),\n {\n cwd,\n onOutput: (text, _context, info) => {\n if (info.stream === 'stdout') out += text;\n else err += text;\n },\n },\n context\n );\n return ran.ok\n ? { code: ran.value.exitCode, out, err }\n : { code: -1, out, err: ran.error.message };\n}\n\nexport function failure(ran: Ran, what: string): Error {\n return new Error(ran.err.trim() || ran.out.trim() || `${what} failed`);\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,230 @@
1
+ import { replicatedState } from "@earendil-works/chord";
2
+ import { HOME } from "../kernel/env.js";
3
+ import {
4
+ failure,
5
+ joined,
6
+ MAX_FILES,
7
+ NO_GIT,
8
+ NO_REPO,
9
+ parseStatus,
10
+ relative,
11
+ run,
12
+ SCAN_DEPTH,
13
+ SCAN_ENTRIES,
14
+ scanned,
15
+ shown,
16
+ textOf,
17
+ within
18
+ } from "./git.js";
19
+ const CHANGES_MS = 500;
20
+ const WATCHED = [HOME, "/mnt", "/scoops"];
21
+ const EXCLUDED = ["/tmp", "/var/lib/slicc"];
22
+ async function walk(env, dir, depth, out, context) {
23
+ const listed = await env.listDir(dir, context);
24
+ if (!listed.ok) return;
25
+ if (listed.value.some((entry) => entry.name === ".git")) {
26
+ out.add(dir);
27
+ return;
28
+ }
29
+ if (depth >= SCAN_DEPTH) return;
30
+ const dirs = listed.value.filter((entry) => entry.kind === "directory" && scanned(entry.name)).slice(0, SCAN_ENTRIES);
31
+ for (const entry of dirs) await walk(env, entry.path, depth + 1, out, context);
32
+ }
33
+ async function cwds(agents, context) {
34
+ const state = agents.state();
35
+ const ids = [
36
+ state.active,
37
+ ...Object.keys(state.cones).filter((id) => id !== state.active),
38
+ ...Object.entries(state.scoops).filter(([, scoop]) => !scoop.gone && !scoop.dropped && !scoop.frozen).map(([id]) => id)
39
+ ];
40
+ const out = [];
41
+ for (const id of ids) {
42
+ const conversation = await agents.conversation(id, context);
43
+ if (conversation) out.push((await conversation.agent(context)).cwd ?? HOME);
44
+ }
45
+ return out;
46
+ }
47
+ async function discover(options, context) {
48
+ const { env } = options;
49
+ const found = [];
50
+ for (const cwd of await cwds(options.agents, context)) {
51
+ const top = await run(env, ["git", "rev-parse", "--show-toplevel"], cwd, context);
52
+ if (top.code === 0 && top.out.trim()) found.push(top.out.trim());
53
+ }
54
+ const walked = /* @__PURE__ */ new Set();
55
+ await walk(env, HOME, 0, walked, context);
56
+ const mounts = await env.listDir("/mnt", context);
57
+ if (mounts.ok) {
58
+ for (const mount of mounts.value)
59
+ if (mount.kind === "directory") await walk(env, mount.path, 0, walked, context);
60
+ }
61
+ const ordered = [...found, ...[...walked].sort()];
62
+ return [...new Set(ordered)].filter(
63
+ (root) => !EXCLUDED.some((excluded) => within(root, excluded))
64
+ );
65
+ }
66
+ async function sideOf(env, repo, pending, context) {
67
+ const path = joined(repo, pending.path);
68
+ const before = pending.status === "added" ? null : await run(env, ["git", "show", `:${pending.path}`], repo, context).then(
69
+ (ran) => ran.code === 0 ? shown(ran.out) : null
70
+ );
71
+ const read = pending.status === "deleted" ? null : await env.readBinaryFile(path, context);
72
+ const after = read?.ok ? textOf(read.value) : null;
73
+ const opaque = pending.status !== "added" && before === null || pending.status !== "deleted" && after === null;
74
+ return { path, before: opaque ? null : before, after: opaque ? null : after };
75
+ }
76
+ async function statusOf(env, repo, context) {
77
+ const ran = await run(
78
+ env,
79
+ [
80
+ "git",
81
+ "--no-optional-locks",
82
+ "status",
83
+ "--porcelain=v1",
84
+ "-z",
85
+ "--untracked-files=all",
86
+ "--ignore-submodules=all"
87
+ ],
88
+ repo,
89
+ context
90
+ );
91
+ if (ran.code !== 0) return null;
92
+ const out = [];
93
+ for (const pending of parseStatus(ran.out).slice(0, MAX_FILES)) {
94
+ const side = await sideOf(env, repo, pending, context);
95
+ out.push({ ...side, repo, status: pending.status, kind: pending.kind });
96
+ }
97
+ return out.sort((a, b) => a.path.localeCompare(b.path));
98
+ }
99
+ const STATE = ["index", "HEAD", "refs"];
100
+ function noise(path, repos) {
101
+ return repos.some((repo) => {
102
+ const git = joined(repo, ".git");
103
+ return within(path, git) && path !== git && !STATE.includes(relative(path, git).split("/")[0]);
104
+ });
105
+ }
106
+ function rediscovers(paths, repos) {
107
+ return paths.some((path) => {
108
+ const parts = path.split("/");
109
+ const git = parts.indexOf(".git");
110
+ if (git > 0) return !repos.includes(parts.slice(0, git).join("/") || "/");
111
+ if (parts.some((part) => part && !scanned(part))) return false;
112
+ return !repos.some((repo) => within(path, repo));
113
+ });
114
+ }
115
+ function attachChanges(options) {
116
+ const { env } = options;
117
+ const view = replicatedState({ unavailable: null, changes: [] });
118
+ let repos = null;
119
+ let opened = null;
120
+ let watcher;
121
+ let stop = () => void 0;
122
+ let timer;
123
+ let lock = Promise.resolve();
124
+ const serial = (work) => {
125
+ const next = lock.then(work, work);
126
+ lock = next.catch(() => void 0);
127
+ return next;
128
+ };
129
+ const scan = async (context) => {
130
+ const git = await run(env, "command -v git", HOME, context);
131
+ if (git.code !== 0) {
132
+ view.replace(context, { unavailable: NO_GIT, changes: [] });
133
+ return;
134
+ }
135
+ repos ??= await discover(options, context);
136
+ const changes = [];
137
+ const live = [];
138
+ for (const repo of repos) {
139
+ const found = await statusOf(env, repo, context);
140
+ if (!found) continue;
141
+ live.push(repo);
142
+ changes.push(...found);
143
+ }
144
+ repos = live;
145
+ view.replace(context, { unavailable: live.length ? null : NO_REPO, changes });
146
+ };
147
+ const refresh = (context) => serial(() => scan(context));
148
+ let running = false;
149
+ let again = false;
150
+ const kick = async (context) => {
151
+ if (running) {
152
+ again = true;
153
+ return;
154
+ }
155
+ running = true;
156
+ do {
157
+ again = false;
158
+ await refresh(context).catch(() => void 0);
159
+ } while (again);
160
+ running = false;
161
+ };
162
+ const later = (context) => {
163
+ clearTimeout(timer);
164
+ timer = setTimeout(() => void kick(context), options.reloadMs);
165
+ };
166
+ const find = async (path, context) => {
167
+ await refresh(context);
168
+ const change = view.value.changes.find((item) => item.path === path);
169
+ if (!change) throw new Error(`${path} has no pending change`);
170
+ return change;
171
+ };
172
+ const start = async (context) => {
173
+ await refresh(context);
174
+ const watched = await env.watch(
175
+ WATCHED.map((path) => ({ path, recursive: true, exclude: { names: ["node_modules"] } })),
176
+ (change) => {
177
+ const known = repos ?? [];
178
+ const paths = "paths" in change ? change.paths.filter((path) => !noise(path, known)) : null;
179
+ if (paths?.length === 0) return;
180
+ if (!paths || rediscovers(paths, known)) repos = null;
181
+ later(context);
182
+ },
183
+ context
184
+ );
185
+ watcher = watched.ok ? watched.value : void 0;
186
+ stop = options.agents.onChange(() => {
187
+ repos = null;
188
+ later(context);
189
+ });
190
+ };
191
+ return {
192
+ view,
193
+ open(context) {
194
+ opened ??= start(context);
195
+ return opened;
196
+ },
197
+ async accept(path, context) {
198
+ const change = await find(path, context);
199
+ const rel = relative(change.path, change.repo);
200
+ const ran = await run(env, ["git", "add", "-A", "--", rel], change.repo, context);
201
+ if (ran.code !== 0) throw failure(ran, `git add ${rel}`);
202
+ await refresh(context);
203
+ },
204
+ async revert(path, context) {
205
+ const change = await find(path, context);
206
+ const rel = relative(change.path, change.repo);
207
+ if (change.kind === "conflict")
208
+ throw new Error(`${change.path} has a merge conflict; resolve it with git first.`);
209
+ if (change.kind === "untracked") {
210
+ const removed = await env.remove(change.path, { force: true }, context);
211
+ if (!removed.ok) throw removed.error;
212
+ } else {
213
+ const ran = await run(env, ["git", "restore", "--", rel], change.repo, context);
214
+ if (ran.code !== 0) throw failure(ran, `git restore ${rel}`);
215
+ }
216
+ await refresh(context);
217
+ },
218
+ refresh,
219
+ async close(context) {
220
+ clearTimeout(timer);
221
+ stop();
222
+ await watcher?.close(context);
223
+ }
224
+ };
225
+ }
226
+ export {
227
+ CHANGES_MS,
228
+ attachChanges
229
+ };
230
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/changes/index.ts"],
4
+ "sourcesContent": ["import { type Context, type MutableReplicatedState, replicatedState } from '@earendil-works/chord';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Agents } from '../agents.ts';\nimport { HOME } from '../kernel/env.ts';\nimport {\n type ChangesView,\n type FileChange,\n failure,\n joined,\n MAX_FILES,\n NO_GIT,\n NO_REPO,\n type Pending,\n parseStatus,\n relative,\n run,\n SCAN_DEPTH,\n SCAN_ENTRIES,\n scanned,\n shown,\n textOf,\n within,\n} from './git.ts';\n\nexport interface ChangesRuntime {\n readonly view: MutableReplicatedState<ChangesView>;\n open(context: Context): Promise<void>;\n accept(path: string, context: Context): Promise<void>;\n revert(path: string, context: Context): Promise<void>;\n refresh(context: Context): Promise<void>;\n close(context: Context): Promise<void>;\n}\n\nexport const CHANGES_MS = 500;\n\nexport interface ChangesOptions {\n env: ExecutionEnv;\n agents: Agents;\n reloadMs: number;\n}\n\nconst WATCHED = [HOME, '/mnt', '/scoops'];\nconst EXCLUDED = ['/tmp', '/var/lib/slicc'];\n\ntype Env = ChangesOptions['env'];\n\nasync function walk(env: Env, dir: string, depth: number, out: Set<string>, context: Context) {\n const listed = await env.listDir(dir, context);\n if (!listed.ok) return;\n if (listed.value.some((entry) => entry.name === '.git')) {\n out.add(dir);\n return;\n }\n if (depth >= SCAN_DEPTH) return;\n const dirs = listed.value\n .filter((entry) => entry.kind === 'directory' && scanned(entry.name))\n .slice(0, SCAN_ENTRIES);\n for (const entry of dirs) await walk(env, entry.path, depth + 1, out, context);\n}\n\nasync function cwds(agents: Agents, context: Context): Promise<string[]> {\n const state = agents.state();\n const ids = [\n state.active,\n ...Object.keys(state.cones).filter((id) => id !== state.active),\n ...Object.entries(state.scoops)\n .filter(([, scoop]) => !scoop.gone && !scoop.dropped && !scoop.frozen)\n .map(([id]) => id),\n ];\n const out: string[] = [];\n for (const id of ids) {\n const conversation = await agents.conversation(id, context);\n if (conversation) out.push((await conversation.agent(context)).cwd ?? HOME);\n }\n return out;\n}\n\nasync function discover(options: ChangesOptions, context: Context): Promise<string[]> {\n const { env } = options;\n const found: string[] = [];\n for (const cwd of await cwds(options.agents, context)) {\n const top = await run(env, ['git', 'rev-parse', '--show-toplevel'], cwd, context);\n if (top.code === 0 && top.out.trim()) found.push(top.out.trim());\n }\n const walked = new Set<string>();\n await walk(env, HOME, 0, walked, context);\n const mounts = await env.listDir('/mnt', context);\n if (mounts.ok)\n for (const mount of mounts.value)\n if (mount.kind === 'directory') await walk(env, mount.path, 0, walked, context);\n const ordered = [...found, ...[...walked].sort()];\n return [...new Set(ordered)].filter(\n (root) => !EXCLUDED.some((excluded) => within(root, excluded))\n );\n}\n\nasync function sideOf(env: Env, repo: string, pending: Pending, context: Context) {\n const path = joined(repo, pending.path);\n const before =\n pending.status === 'added'\n ? null\n : await run(env, ['git', 'show', `:${pending.path}`], repo, context).then((ran) =>\n ran.code === 0 ? shown(ran.out) : null\n );\n const read = pending.status === 'deleted' ? null : await env.readBinaryFile(path, context);\n const after = read?.ok ? textOf(read.value) : null;\n const opaque =\n (pending.status !== 'added' && before === null) ||\n (pending.status !== 'deleted' && after === null);\n return { path, before: opaque ? null : before, after: opaque ? null : after };\n}\n\nasync function statusOf(env: Env, repo: string, context: Context): Promise<FileChange[] | null> {\n const ran = await run(\n env,\n [\n 'git',\n '--no-optional-locks',\n 'status',\n '--porcelain=v1',\n '-z',\n '--untracked-files=all',\n '--ignore-submodules=all',\n ],\n repo,\n context\n );\n if (ran.code !== 0) return null;\n const out: FileChange[] = [];\n for (const pending of parseStatus(ran.out).slice(0, MAX_FILES)) {\n const side = await sideOf(env, repo, pending, context);\n out.push({ ...side, repo, status: pending.status, kind: pending.kind });\n }\n return out.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nconst STATE = ['index', 'HEAD', 'refs'];\n\nfunction noise(path: string, repos: readonly string[]): boolean {\n return repos.some((repo) => {\n const git = joined(repo, '.git');\n return (\n within(path, git) &&\n path !== git &&\n !STATE.includes(relative(path, git).split('/')[0] as string)\n );\n });\n}\n\nfunction rediscovers(paths: readonly string[], repos: readonly string[]): boolean {\n return paths.some((path) => {\n const parts = path.split('/');\n const git = parts.indexOf('.git');\n if (git > 0) return !repos.includes(parts.slice(0, git).join('/') || '/');\n if (parts.some((part) => part && !scanned(part))) return false;\n return !repos.some((repo) => within(path, repo));\n });\n}\n\nexport function attachChanges(options: ChangesOptions): ChangesRuntime {\n const { env } = options;\n const view = replicatedState<ChangesView>({ unavailable: null, changes: [] });\n let repos: string[] | null = null;\n let opened: Promise<void> | null = null;\n let watcher: FileWatcher | undefined;\n let stop = () => undefined as void;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(work: () => Promise<T>): Promise<T> => {\n const next = lock.then(work, work);\n lock = next.catch(() => undefined);\n return next;\n };\n const scan = async (context: Context) => {\n const git = await run(env, 'command -v git', HOME, context);\n if (git.code !== 0) {\n view.replace(context, { unavailable: NO_GIT, changes: [] });\n return;\n }\n repos ??= await discover(options, context);\n const changes: FileChange[] = [];\n const live: string[] = [];\n for (const repo of repos) {\n const found = await statusOf(env, repo, context);\n if (!found) continue;\n live.push(repo);\n changes.push(...found);\n }\n repos = live;\n view.replace(context, { unavailable: live.length ? null : NO_REPO, changes });\n };\n const refresh = (context: Context) => serial(() => scan(context));\n let running = false;\n let again = false;\n const kick = async (context: Context) => {\n if (running) {\n again = true;\n return;\n }\n running = true;\n do {\n again = false;\n await refresh(context).catch(() => undefined);\n } while (again);\n running = false;\n };\n const later = (context: Context) => {\n clearTimeout(timer);\n timer = setTimeout(() => void kick(context), options.reloadMs);\n };\n const find = async (path: string, context: Context) => {\n await refresh(context);\n const change = view.value.changes.find((item) => item.path === path);\n if (!change) throw new Error(`${path} has no pending change`);\n return change;\n };\n const start = async (context: Context) => {\n await refresh(context);\n const watched = await env.watch(\n WATCHED.map((path) => ({ path, recursive: true, exclude: { names: ['node_modules'] } })),\n (change) => {\n const known = repos ?? [];\n const paths = 'paths' in change ? change.paths.filter((path) => !noise(path, known)) : null;\n if (paths?.length === 0) return;\n if (!paths || rediscovers(paths, known)) repos = null;\n later(context);\n },\n context\n );\n watcher = watched.ok ? watched.value : undefined;\n stop = options.agents.onChange(() => {\n repos = null;\n later(context);\n });\n };\n return {\n view,\n open(context) {\n opened ??= start(context);\n return opened;\n },\n async accept(path, context) {\n const change = await find(path, context);\n const rel = relative(change.path, change.repo);\n const ran = await run(env, ['git', 'add', '-A', '--', rel], change.repo, context);\n if (ran.code !== 0) throw failure(ran, `git add ${rel}`);\n await refresh(context);\n },\n async revert(path, context) {\n const change = await find(path, context);\n const rel = relative(change.path, change.repo);\n if (change.kind === 'conflict')\n throw new Error(`${change.path} has a merge conflict; resolve it with git first.`);\n if (change.kind === 'untracked') {\n const removed = await env.remove(change.path, { force: true }, context);\n if (!removed.ok) throw removed.error;\n } else {\n const ran = await run(env, ['git', 'restore', '--', rel], change.repo, context);\n if (ran.code !== 0) throw failure(ran, `git restore ${rel}`);\n }\n await refresh(context);\n },\n refresh,\n async close(context) {\n clearTimeout(timer);\n stop();\n await watcher?.close(context);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/client.js CHANGED
@@ -47,7 +47,8 @@ async function connectAgent(endpoint, options = {}) {
47
47
  sprinkles,
48
48
  memories,
49
49
  memoryScopes,
50
- frozen
50
+ frozen,
51
+ changes
51
52
  } = session.use(AgentTranscript);
52
53
  const settings = session.use(AgentSettings);
53
54
  await session.ready(context);
@@ -63,6 +64,7 @@ async function connectAgent(endpoint, options = {}) {
63
64
  memories,
64
65
  memoryScopes,
65
66
  frozen,
67
+ changes,
66
68
  settings,
67
69
  async prompt(text, whenBusy = "followUp") {
68
70
  const sent = await control.send({ text, whenBusy, requestId: null }, context);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/client.ts"],
4
- "sourcesContent": ["import {\n type Context,\n createRemoteServiceBinding,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { Client, createClientServiceTransport } from '@earendil-works/pi-client';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n type Command,\n type Delivered,\n type SendMode,\n} from './services.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { PortChannel, type PortEndpoint } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly settings: AgentSettings;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n close(): Promise<void>;\n}\n\nfunction attached(client: Client): Promise<void> {\n return new Promise((resolve) => {\n const stop = client.onAttachmentChange(() => {\n stop();\n resolve();\n });\n });\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const channel = new PortChannel(endpoint);\n const serverId = await channel.hello;\n const client = await Client.connect({ serverId, transportFactory: channel.transport });\n const server = createRemoteServiceBinding({\n services: [AgentSessions],\n transport: createClientServiceTransport(client, () => ({ serverId })),\n bound: true,\n });\n const ready = attached(client);\n await server.use(AgentSessions).attach(AGENT_SESSION, context);\n await ready;\n const session = createRemoteServiceBinding({\n services: [AgentControl, AgentTranscript, AgentSettings],\n transport: createClientServiceTransport(client, () => client.attachment),\n bound: true,\n });\n const control = session.use(AgentControl);\n const {\n state: transcript,\n deliveries,\n agents,\n views,\n commands,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n } = session.use(AgentTranscript);\n const settings = session.use(AgentSettings);\n await session.ready(context);\n return {\n serverId,\n control,\n transcript,\n deliveries,\n agents,\n views,\n commands,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n settings,\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n await server.use(AgentSessions).detach(context);\n await session.dispose(context);\n await server.dispose(context);\n await client.dispose();\n },\n };\n}\n"],
5
- "mappings": "AAAA;AAAA,EAEE;AAAA,OAEK;AACP,SAAS,0BAA0B;AACnC,SAAS,QAAQ,oCAAoC;AAIrD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAIK;AAEP,SAAS,mBAAsC;AAmB/C,SAAS,SAAS,QAA+B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,OAAO,OAAO,mBAAmB,MAAM;AAC3C,WAAK;AACL,cAAQ;AAAA,IACV,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAsB,aACpB,UACA,UAAiC,CAAC,GACR;AAC1B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,IAAI,YAAY,QAAQ;AACxC,QAAM,WAAW,MAAM,QAAQ;AAC/B,QAAM,SAAS,MAAM,OAAO,QAAQ,EAAE,UAAU,kBAAkB,QAAQ,UAAU,CAAC;AACrF,QAAM,SAAS,2BAA2B;AAAA,IACxC,UAAU,CAAC,aAAa;AAAA,IACxB,WAAW,6BAA6B,QAAQ,OAAO,EAAE,SAAS,EAAE;AAAA,IACpE,OAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,SAAS,MAAM;AAC7B,QAAM,OAAO,IAAI,aAAa,EAAE,OAAO,eAAe,OAAO;AAC7D,QAAM;AACN,QAAM,UAAU,2BAA2B;AAAA,IACzC,UAAU,CAAC,cAAc,iBAAiB,aAAa;AAAA,IACvD,WAAW,6BAA6B,QAAQ,MAAM,OAAO,UAAU;AAAA,IACvE,OAAO;AAAA,EACT,CAAC;AACD,QAAM,UAAU,QAAQ,IAAI,YAAY;AACxC,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,QAAQ,IAAI,eAAe;AAC/B,QAAM,WAAW,QAAQ,IAAI,aAAa;AAC1C,QAAM,QAAQ,MAAM,OAAO;AAC3B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,OAAO,MAAM,WAAW,YAAY;AACxC,YAAM,OAAO,MAAM,QAAQ,KAAK,EAAE,MAAM,UAAU,WAAW,KAAK,GAAG,OAAO;AAC5E,UAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,OAAO,EAAE;AAC/E,YAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,cAAc,OAAO;AAC7D,UAAI,QAAQ,WAAW,OAAQ,OAAM,IAAI,MAAM,eAAe,QAAQ,MAAM,EAAE;AAC9E,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,MAAM,QAAQ;AACZ,YAAM,OAAO,IAAI,aAAa,EAAE,OAAO,OAAO;AAC9C,YAAM,QAAQ,QAAQ,OAAO;AAC7B,YAAM,OAAO,QAAQ,OAAO;AAC5B,YAAM,OAAO,QAAQ;AAAA,IACvB;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import {\n type Context,\n createRemoteServiceBinding,\n type ReplicatedState,\n} from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { Client, createClientServiceTransport } from '@earendil-works/pi-client';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { ChangesView } from './changes/git.ts';\nimport type { FrozenCone } from './freezer/index.ts';\nimport type { MemoryEntry, MemoryScope } from './memory/format.ts';\nimport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentSettings,\n type AgentsSummary,\n AgentTranscript,\n type Command,\n type Delivered,\n type SendMode,\n} from './services.ts';\nimport type { Sprinkle } from './sprinkles/kind.ts';\nimport { PortChannel, type PortEndpoint } from './wire.ts';\n\nexport interface AgentConnection {\n readonly serverId: string;\n readonly control: AgentControl;\n readonly transcript: ReplicatedState<ConversationView>;\n readonly deliveries: ReplicatedState<Record<string, Delivered>>;\n readonly agents?: ReplicatedState<AgentsSummary>;\n readonly views?: ReplicatedState<Record<string, ConversationView>>;\n readonly commands?: ReplicatedState<Command[]>;\n readonly sprinkles?: ReplicatedState<Sprinkle[]>;\n readonly memories?: ReplicatedState<MemoryEntry[]>;\n readonly memoryScopes?: ReplicatedState<MemoryScope[]>;\n readonly frozen?: ReplicatedState<FrozenCone[]>;\n readonly changes?: ReplicatedState<ChangesView>;\n readonly settings: AgentSettings;\n prompt(text: string, whenBusy?: SendMode): Promise<string>;\n close(): Promise<void>;\n}\n\nfunction attached(client: Client): Promise<void> {\n return new Promise((resolve) => {\n const stop = client.onAttachmentChange(() => {\n stop();\n resolve();\n });\n });\n}\n\nexport async function connectAgent(\n endpoint: PortEndpoint,\n options: { context?: Context } = {}\n): Promise<AgentConnection> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const channel = new PortChannel(endpoint);\n const serverId = await channel.hello;\n const client = await Client.connect({ serverId, transportFactory: channel.transport });\n const server = createRemoteServiceBinding({\n services: [AgentSessions],\n transport: createClientServiceTransport(client, () => ({ serverId })),\n bound: true,\n });\n const ready = attached(client);\n await server.use(AgentSessions).attach(AGENT_SESSION, context);\n await ready;\n const session = createRemoteServiceBinding({\n services: [AgentControl, AgentTranscript, AgentSettings],\n transport: createClientServiceTransport(client, () => client.attachment),\n bound: true,\n });\n const control = session.use(AgentControl);\n const {\n state: transcript,\n deliveries,\n agents,\n views,\n commands,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n changes,\n } = session.use(AgentTranscript);\n const settings = session.use(AgentSettings);\n await session.ready(context);\n return {\n serverId,\n control,\n transcript,\n deliveries,\n agents,\n views,\n commands,\n sprinkles,\n memories,\n memoryScopes,\n frozen,\n changes,\n settings,\n async prompt(text, whenBusy = 'followUp') {\n const sent = await control.send({ text, whenBusy, requestId: null }, context);\n if (!sent.accepted) throw new Error(`${sent.error.code}: ${sent.error.message}`);\n const settled = await control.wait(sent.submissionId, context);\n if (settled.status !== 'done') throw new Error(`unanswered: ${settled.reason}`);\n return settled.text;\n },\n async close() {\n await server.use(AgentSessions).detach(context);\n await session.dispose(context);\n await server.dispose(context);\n await client.dispose();\n },\n };\n}\n"],
5
+ "mappings": "AAAA;AAAA,EAEE;AAAA,OAEK;AACP,SAAS,0BAA0B;AACnC,SAAS,QAAQ,oCAAoC;AAKrD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAIK;AAEP,SAAS,mBAAsC;AAoB/C,SAAS,SAAS,QAA+B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,OAAO,OAAO,mBAAmB,MAAM;AAC3C,WAAK;AACL,cAAQ;AAAA,IACV,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAsB,aACpB,UACA,UAAiC,CAAC,GACR;AAC1B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,IAAI,YAAY,QAAQ;AACxC,QAAM,WAAW,MAAM,QAAQ;AAC/B,QAAM,SAAS,MAAM,OAAO,QAAQ,EAAE,UAAU,kBAAkB,QAAQ,UAAU,CAAC;AACrF,QAAM,SAAS,2BAA2B;AAAA,IACxC,UAAU,CAAC,aAAa;AAAA,IACxB,WAAW,6BAA6B,QAAQ,OAAO,EAAE,SAAS,EAAE;AAAA,IACpE,OAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,SAAS,MAAM;AAC7B,QAAM,OAAO,IAAI,aAAa,EAAE,OAAO,eAAe,OAAO;AAC7D,QAAM;AACN,QAAM,UAAU,2BAA2B;AAAA,IACzC,UAAU,CAAC,cAAc,iBAAiB,aAAa;AAAA,IACvD,WAAW,6BAA6B,QAAQ,MAAM,OAAO,UAAU;AAAA,IACvE,OAAO;AAAA,EACT,CAAC;AACD,QAAM,UAAU,QAAQ,IAAI,YAAY;AACxC,QAAM;AAAA,IACJ,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,QAAQ,IAAI,eAAe;AAC/B,QAAM,WAAW,QAAQ,IAAI,aAAa;AAC1C,QAAM,QAAQ,MAAM,OAAO;AAC3B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,OAAO,MAAM,WAAW,YAAY;AACxC,YAAM,OAAO,MAAM,QAAQ,KAAK,EAAE,MAAM,UAAU,WAAW,KAAK,GAAG,OAAO;AAC5E,UAAI,CAAC,KAAK,SAAU,OAAM,IAAI,MAAM,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,OAAO,EAAE;AAC/E,YAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,cAAc,OAAO;AAC7D,UAAI,QAAQ,WAAW,OAAQ,OAAM,IAAI,MAAM,eAAe,QAAQ,MAAM,EAAE;AAC9E,aAAO,QAAQ;AAAA,IACjB;AAAA,IACA,MAAM,QAAQ;AACZ,YAAM,OAAO,IAAI,aAAa,EAAE,OAAO,OAAO;AAC9C,YAAM,QAAQ,QAAQ,OAAO;AAC7B,YAAM,OAAO,QAAQ,OAAO;AAC5B,YAAM,OAAO,QAAQ;AAAA,IACvB;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/dist/control.js CHANGED
@@ -30,6 +30,21 @@ function freezerControl(freezer) {
30
30
  })
31
31
  };
32
32
  }
33
+ function changesControl(changes) {
34
+ const needed = () => {
35
+ if (!changes) throw new Error("This agent tracks no changes.");
36
+ return changes;
37
+ };
38
+ const done = (path, run) => created(async () => {
39
+ await run(needed());
40
+ return path;
41
+ });
42
+ return {
43
+ changesOpen: async (context) => changes?.open(context),
44
+ changeAccept: (path, context) => done(path, (runtime) => runtime.accept(path, context)),
45
+ changeRevert: (path, context) => done(path, (runtime) => runtime.revert(path, context))
46
+ };
47
+ }
33
48
  function memoryControl(memory) {
34
49
  return {
35
50
  async memorySave(draft, context) {
@@ -77,6 +92,16 @@ function cones(target) {
77
92
  }
78
93
  };
79
94
  }
95
+ const WRITERS = ["write", "edit", "bash", "codemode"];
96
+ function writesIn(entries) {
97
+ return entries.some(
98
+ (entry) => (entry.model ?? []).some(
99
+ (message) => Array.isArray(message.content) && message.content.some(
100
+ (block) => block.type === "toolCall" && WRITERS.includes(String(block.name))
101
+ )
102
+ )
103
+ );
104
+ }
80
105
  function lastTurn(view, messageId) {
81
106
  const { entries } = view;
82
107
  const target = entries.findIndex((entry) => `e${entry.id}` === messageId);
@@ -84,7 +109,11 @@ function lastTurn(view, messageId) {
84
109
  const turn = entries.slice(0, target).findLastIndex((entry) => entry.kind === "pi.user");
85
110
  if (turn < 0) return null;
86
111
  const user = entries[turn];
87
- return { text: answerText(user.model?.[0]?.content), at: entries[turn - 1]?.id ?? null };
112
+ return {
113
+ text: answerText(user.model?.[0]?.content),
114
+ at: entries[turn - 1]?.id ?? null,
115
+ files: writesIn(entries.slice(turn))
116
+ };
88
117
  }
89
118
  function busy(view) {
90
119
  const live = view.docs["pi.live"];
@@ -126,8 +155,9 @@ async function rewindAgent(deps, agentId, messageId, context) {
126
155
  if ("reason" in found) return found;
127
156
  const fork = await forkAt(harness, conversation, found, context);
128
157
  const changed = scoops && agents && !scoop ? await scoops.scoops.rewound(agents.activeCone(), fork, context) : { stopped: [], restored: [] };
158
+ const files = found.turn.files;
129
159
  await fork.submit(
130
- { type: "write", entry: { kind: "slicc.rewound", data: { turns: 1, ...changed } } },
160
+ { type: "write", entry: { kind: "slicc.rewound", data: { turns: 1, ...changed, files } } },
131
161
  context
132
162
  );
133
163
  if (scoop && agents)
@@ -137,7 +167,7 @@ async function rewindAgent(deps, agentId, messageId, context) {
137
167
  else await cone.switchCone(fork, context);
138
168
  return { done: true, text: found.turn.text, reason: null };
139
169
  }
140
- function createAgentControl(harness, target, deliveries, licks, scoops, skills, sprinkles, memory, freezer) {
170
+ function createAgentControl(harness, target, deliveries, licks, scoops, skills, sprinkles, memory, freezer, changes) {
141
171
  const cone = cones(target);
142
172
  const current = () => cone.cone();
143
173
  const agents = target.agents;
@@ -199,6 +229,7 @@ function createAgentControl(harness, target, deliveries, licks, scoops, skills,
199
229
  ...sprinkleControl(sprinkles),
200
230
  ...memoryControl(memory),
201
231
  ...freezerControl(freezer),
232
+ ...changesControl(changes),
202
233
  async unqueue(agentId, id, context) {
203
234
  return withdraw(await conversationFor(agentId, context), id, context);
204
235
  },