@ai-ecoverse/slicc-agent 3.22.0 → 3.24.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 +29 -16
- package/dist/agents.js.map +2 -2
- package/dist/freezer/index.js +255 -71
- package/dist/freezer/index.js.map +3 -3
- package/dist/host.js +3 -0
- package/dist/host.js.map +2 -2
- package/dist/spectrum/agent-port.js +10 -2
- package/dist/spectrum/agent-port.js.map +2 -2
- package/dist/spectrum/messages.js +13 -0
- package/dist/spectrum/messages.js.map +2 -2
- package/dist/system.js +1 -1
- package/dist/system.js.map +1 -1
- package/dist/types/agents.d.ts +2 -0
- package/dist/types/freezer/index.d.ts +12 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ app.model = { ...createKernelModel({ kernel, root }), ...createAgentModel(agent,
|
|
|
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.
|
|
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.39.0, whose Freezer shows every conversation with its kind and Open, Thaw and Remove), 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`;
|
|
@@ -235,7 +235,6 @@ The model can write a short JavaScript program that calls its tools, with the `c
|
|
|
235
235
|
- **Isolation.** Seven is cross-origin isolated, so pi-codemode's `SharedArrayBuffer` interrupt works.
|
|
236
236
|
- **Overrides.** `runAgentWorker` takes `codemodeWasm` and `codemodeWorker` to override both, for bundled hosts.
|
|
237
237
|
|
|
238
|
-
||||||| parent of d578592 (wip: sprinkles)
|
|
239
238
|
### Memory
|
|
240
239
|
|
|
241
240
|
pi has no memory store or memory tool; its coding agent loads context files, and [pi-subagents](https://github.com/nicobailon/pi-subagents) gives a role a `MEMORY.md`. Both are ported here over `ExecutionEnv`.
|
|
@@ -285,24 +284,38 @@ pi core has no browser tooling, and Mario's pi-skills `browser-tools` launches i
|
|
|
285
284
|
|
|
286
285
|
### The freezer
|
|
287
286
|
|
|
288
|
-
pi durable keeps every conversation and entry in the session database, and has no archive flag and no delete.
|
|
287
|
+
pi durable keeps every conversation and entry in the session database, and has no archive flag and no delete (#75). The `slicc.agents` registry is the index of which conversations exist and what they are, so the Freezer lists from it, as Lars put it: the list of sessions in the database.
|
|
289
288
|
|
|
290
|
-
- **
|
|
289
|
+
- **The list** (`frozen` on the transcript service): every cone, frozen cone, scoop and `agent` run the registry knows.
|
|
290
|
+
- Each row has its id, name (names needn't be unique), title (the first user line), model, message count and last activity.
|
|
291
|
+
- It also has `kind` (`cone`, `scoop` or `agent`), `live` and `thawedAs`.
|
|
292
|
+
- Live rows come first, then the newest. A stopped conversation's numbers are read once and cached.
|
|
293
|
+
- **New conversation** (spectrum's `clear` and `/clear`, Lars's W9) gives the same cone, with its id and name, a fresh conversation with the same configuration, when the chat isn't empty. The old conversation becomes a frozen row. On a scoop, `clear` is a plain reset.
|
|
294
|
+
- **Delete cone** (the Freezer's freeze):
|
|
291
295
|
- its run and its scoops' runs stop;
|
|
292
|
-
- it moves from `cones` to `frozen`
|
|
296
|
+
- it moves from `cones` to `frozen` with its live scoops marked frozen, keeping their transcripts and workspaces;
|
|
293
297
|
- if it was active, another cone is selected, and if it was the last one, a fresh cone takes its name.
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
- **
|
|
297
|
-
- **
|
|
298
|
-
- **
|
|
298
|
+
- 14b's memory extraction runs first, and one background call (with `memory.extractModel` or the cone's model) gives it a short title.
|
|
299
|
+
- **Thaw:**
|
|
300
|
+
- **A frozen cone** comes back under its old id when that's free (otherwise a new `cone-<n>`), keeping its name, with its scoops unfrozen.
|
|
301
|
+
- **A live row** just opens: it's selected.
|
|
302
|
+
- **A stopped scoop or `agent` run comes back as a cone, as a fork:** a conversation's task ownership can't change, so `Conversation.fork` at its last entry gives an ownerless conversation with the cone's tools and instructions, cwd `/home`, and the scoop's model and thinking level. It becomes `cone-<n>` with the scoop's name, and the scoop's row records `thawedAs`.
|
|
303
|
+
- The thawed cone's first entry is a `slicc.thawed` line the model sees and spectrum shows as a notice: whose conversation it was, and whether its workspace is still there or was deleted when it stopped.
|
|
304
|
+
- **Remove** (the Freezer's delete, `discard`):
|
|
305
|
+
- on a frozen cone, it removes the record, marks its scoops gone and deletes their folders;
|
|
306
|
+
- on a stopped scoop or `agent` run, it sets `removed` (a tombstone in the registry, so the row never returns) and deletes its folder.
|
|
307
|
+
- The history stays in the session database. Live rows can't be removed.
|
|
299
308
|
- **While frozen:**
|
|
300
|
-
-
|
|
301
|
-
- cron fires, watches and webhooks aimed at
|
|
309
|
+
- a frozen cone's scoops aren't listed by `agent list`, and `agent send|wait|stop` on one says it is frozen with its cone;
|
|
310
|
+
- cron fires, watches and webhooks aimed at it are skipped, and the active cone gets one `warn` lick per source naming what was skipped;
|
|
302
311
|
- sprinkles it owned send to the active cone;
|
|
303
312
|
- it can't be rewound.
|
|
304
|
-
- **The `freezer` command** is the same script as `agent`:
|
|
305
|
-
-
|
|
313
|
+
- **The `freezer` command** is the same script as `agent`:
|
|
314
|
+
- `freezer list` shows every conversation with its kind, and `(live)` on live ones;
|
|
315
|
+
- `freezer show <id>` shows a conversation as Markdown, which replaces v6's `transcript-export`;
|
|
316
|
+
- `freezer thaw <id>` thaws it.
|
|
317
|
+
- The gelatiere's pass reads frozen chats with it.
|
|
318
|
+
- **The panel:** `AgentControl` has `freeze` (Delete cone), `newChat` (New conversation), `thaw` and `discard` (Remove).
|
|
306
319
|
|
|
307
320
|
### Changes
|
|
308
321
|
|
|
@@ -313,7 +326,7 @@ pi has no git integration and no changes list; pi coding-agent leaves that to th
|
|
|
313
326
|
- `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".
|
|
314
327
|
- 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.
|
|
315
328
|
- **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.
|
|
316
|
-
- **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), `@ai-ecoverse/wasi-typescript` (tsc)
|
|
329
|
+
- **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), `@ai-ecoverse/wasi-typescript` (tsc), `@ai-ecoverse/wasi-biome` (biome), for PDFs `@ai-ecoverse/wasm-poppler` (pdftotext and friends) and `@ai-ecoverse/wasm-qpdf` (qpdf), and for Python `@ai-ecoverse/wasix-python` (python3, pip) with `@ai-ecoverse/wasix-uv-shim@0.1.0-1` (a uv-compatible shim, pinned while the package is new; only `uv run` uses the venv until kernel#168).
|
|
317
330
|
- **Which repos:**
|
|
318
331
|
- the repo of each live agent's cwd (`git rev-parse --show-toplevel`), with the active cone's first;
|
|
319
332
|
- 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.
|
|
@@ -354,7 +367,7 @@ The built-in `sprinkles` skill teaches the files, the bridge and the command.
|
|
|
354
367
|
|
|
355
368
|
## The kernel
|
|
356
369
|
|
|
357
|
-
`@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.
|
|
370
|
+
`@ai-ecoverse/slicc-kernel` is a peer dependency (`>=1.26.3 <2`; older kernels have an IPv6-to-loopback security bug): 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 the range allows (1.26.3), 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`). Today the floor is 1.26.3 and the newest certified kernel 1.26.6; the second job moves by hand to each newer certified kernel, so both ends of the range are tested. 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.
|
|
358
371
|
|
|
359
372
|
## Patched dependencies
|
|
360
373
|
|
package/dist/agents.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/agents.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n type ConversationId,\n defineDoc,\n type Harness,\n type Tx,\n} from '@earendil-works/pi-durable';\nimport { ConeDoc } from './cone.ts';\nimport type { RoleMemory } from './memory/store.ts';\n\nexport const FIRST_CONE = 'cone';\n\nexport type ConeRecord = { name: string; conversation: number };\n\nexport type Mark = { in: number; at: number };\n\nexport type ScoopRecord = {\n name: string;\n folder: string;\n cone: string;\n conversation: number;\n anchor: number;\n role: string | null;\n memory?: RoleMemory;\n origin?: string;\n context?: { project: boolean; global: boolean };\n kind: 'async' | 'sync';\n parent: string;\n depth: number;\n roots: { write: string[]; read: string[] } | null;\n created: Mark | null;\n dropped: Mark | null;\n gone: boolean;\n frozen?: string;\n};\n\nexport type FrozenRecord = {\n cone: string;\n name: string;\n conversation: number;\n title: string;\n model: string;\n messages: number;\n frozenAt: number;\n scoops: string[];\n};\n\nexport function live(record: ScoopRecord): boolean {\n return record.dropped === null && !record.gone && !record.frozen;\n}\n\nexport type AgentsState = {\n cones: Record<string, ConeRecord>;\n active: string;\n next: number;\n scoops: Record<string, ScoopRecord>;\n frozen?: Record<string, FrozenRecord>;\n frozenNext?: number;\n};\n\nexport const AgentsDoc = defineDoc<AgentsState>({\n kind: 'slicc.agents',\n version: 1,\n scope: 'session',\n initial: () => ({ cones: {}, active: FIRST_CONE, next: 2, scoops: {} }),\n});\n\nexport const SCOOP_PREFIX = 'scoop:';\n\nexport function scoopId(folder: string): string {\n return `${SCOOP_PREFIX}${folder}`;\n}\n\nexport function slug(name: string): string {\n const cleaned = name\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[-._]+|[-._]+$/g, '')\n .slice(0, 48);\n return cleaned || 'scoop';\n}\n\nexport function freeFolder(state: Pick<AgentsState, 'scoops'>, name: string): string {\n const base = slug(name);\n if (!state.scoops[scoopId(base)]) return base;\n for (let n = 2; ; n++) if (!state.scoops[scoopId(`${base}-${n}`)]) return `${base}-${n}`;\n}\n\nexport interface Agents {\n state(): Readonly<AgentsState>;\n cone(): Conversation;\n activeCone(): string;\n conversation(agentId: string, context: Context): Promise<Conversation | undefined>;\n selectCone(id: string, context: Context): Promise<void>;\n createCone(name: string, context: Context): Promise<string>;\n switchCone(conversation: Conversation, context: Context): Promise<void>;\n setConversation(coneId: string, conversation: Conversation, context: Context): Promise<void>;\n update<T>(change: (tx: Tx, state: AgentsState) => T | Promise<T>, context: Context): Promise<T>;\n onChange(listener: (state: Readonly<AgentsState>) => void): () => void;\n onCone(listener: (conversation: Conversation) => void | Promise<void>): () => void;\n}\n\nasync function initial(harness: Harness, root: Conversation, context: Context) {\n return harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n if (!doc.cones[FIRST_CONE]) {\n const legacy = (await tx.doc(ConeDoc)).conversation;\n doc.cones[FIRST_CONE] = { name: 'sliccy', conversation: legacy ?? root.id };\n }\n if (!doc.cones[doc.active]) doc.active = FIRST_CONE;\n return JSON.parse(JSON.stringify(doc)) as AgentsState;\n }, context);\n}\n\nexport async function openAgents(\n harness: Harness,\n root: Conversation,\n context: Context\n): Promise<Agents> {\n let state = await initial(harness, root, context);\n const handles = new Map<number, Conversation>([[root.id, root]]);\n const handle = async (id: number, using: Context) => {\n const known = handles.get(id);\n if (known) return known;\n const found = await harness.conversation(id as ConversationId, using);\n if (found) handles.set(id, found);\n return found;\n };\n let cone =\n (await handle((state.cones[state.active] as ConeRecord).conversation, context)) ?? root;\n const changes = new Set<(state: Readonly<AgentsState>) => void>();\n const cones = new Set<(conversation: Conversation) => void | Promise<void>>();\n const notifyCone = async (next: Conversation) => {\n cone = next;\n await Promise.all([...cones].map((listener) => listener(next)));\n };\n const update: Agents['update'] = async (change, using) => {\n const [result, next] = await harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n const value = await change(tx, doc as AgentsState);\n return [value, JSON.parse(JSON.stringify(doc)) as AgentsState] as const;\n }, using);\n state = next;\n for (const listener of changes) listener(state);\n return result;\n };\n const agents: Agents = {\n state: () => state,\n cone: () => cone,\n activeCone: () => state.active,\n async conversation(agentId, using) {\n const id = state.cones[agentId]?.conversation ?? state.scoops[agentId]?.conversation;\n return id === undefined ? undefined : handle(id, using);\n },\n async selectCone(id, using) {\n const record = state.cones[id];\n if (!record) throw new Error(`no cone ${id}`);\n if (state.active === id) return;\n await update((_tx, doc) => {\n doc.active = id;\n }, using);\n await notifyCone((await handle(record.conversation, using)) as Conversation);\n },\n async createCone(name, using) {\n const wanted = name.trim();\n if (!wanted) throw new Error('A cone needs a name.');\n if (Object.values(state.cones).some((record) => record.name === wanted))\n throw new Error(`A cone named ${wanted} already exists.`);\n const conversation = await harness.createConversation(\n {\n ownership: { kind: 'ownerless' },\n agent: { model: (await cone.agent(using)).model ?? null },\n },\n using\n );\n handles.set(conversation.id, conversation);\n return update((_tx, doc) => {\n const id = `cone-${doc.next}`;\n doc.next += 1;\n doc.cones[id] = { name: wanted, conversation: conversation.id };\n return id;\n }, using);\n },\n switchCone(conversation, using) {\n return agents.setConversation(state.active, conversation, using);\n },\n async setConversation(coneId, conversation, using) {\n handles.set(conversation.id, conversation);\n await update(async (tx, doc) => {\n (doc.cones[coneId] as ConeRecord).conversation = conversation.id;\n if (coneId === doc.active) (await tx.doc(ConeDoc)).conversation = conversation.id;\n }, using);\n if (coneId === state.active) await notifyCone(conversation);\n },\n update,\n onChange(listener) {\n changes.add(listener);\n return () => changes.delete(listener);\n },\n onCone(listener) {\n cones.add(listener);\n return () => cones.delete(listener);\n },\n };\n return agents;\n}\n"],
|
|
5
|
-
"mappings": "AACA;AAAA,EAGE;AAAA,OAGK;AACP,SAAS,eAAe;AAGjB,MAAM,aAAa;
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n type ConversationId,\n defineDoc,\n type Harness,\n type Tx,\n} from '@earendil-works/pi-durable';\nimport { ConeDoc } from './cone.ts';\nimport type { RoleMemory } from './memory/store.ts';\n\nexport const FIRST_CONE = 'cone';\n\nexport type ConeRecord = { name: string; conversation: number };\n\nexport type Mark = { in: number; at: number };\n\nexport type ScoopRecord = {\n name: string;\n folder: string;\n cone: string;\n conversation: number;\n anchor: number;\n role: string | null;\n memory?: RoleMemory;\n origin?: string;\n context?: { project: boolean; global: boolean };\n kind: 'async' | 'sync';\n parent: string;\n depth: number;\n roots: { write: string[]; read: string[] } | null;\n created: Mark | null;\n dropped: Mark | null;\n gone: boolean;\n frozen?: string;\n removed?: boolean;\n thawedAs?: string;\n};\n\nexport type FrozenRecord = {\n cone: string;\n name: string;\n conversation: number;\n title: string;\n model: string;\n messages: number;\n frozenAt: number;\n scoops: string[];\n};\n\nexport function live(record: ScoopRecord): boolean {\n return record.dropped === null && !record.gone && !record.frozen;\n}\n\nexport type AgentsState = {\n cones: Record<string, ConeRecord>;\n active: string;\n next: number;\n scoops: Record<string, ScoopRecord>;\n frozen?: Record<string, FrozenRecord>;\n frozenNext?: number;\n};\n\nexport const AgentsDoc = defineDoc<AgentsState>({\n kind: 'slicc.agents',\n version: 1,\n scope: 'session',\n initial: () => ({ cones: {}, active: FIRST_CONE, next: 2, scoops: {} }),\n});\n\nexport const SCOOP_PREFIX = 'scoop:';\n\nexport function scoopId(folder: string): string {\n return `${SCOOP_PREFIX}${folder}`;\n}\n\nexport function slug(name: string): string {\n const cleaned = name\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[-._]+|[-._]+$/g, '')\n .slice(0, 48);\n return cleaned || 'scoop';\n}\n\nexport function freeFolder(state: Pick<AgentsState, 'scoops'>, name: string): string {\n const base = slug(name);\n if (!state.scoops[scoopId(base)]) return base;\n for (let n = 2; ; n++) if (!state.scoops[scoopId(`${base}-${n}`)]) return `${base}-${n}`;\n}\n\nexport interface Agents {\n state(): Readonly<AgentsState>;\n cone(): Conversation;\n activeCone(): string;\n conversation(agentId: string, context: Context): Promise<Conversation | undefined>;\n selectCone(id: string, context: Context): Promise<void>;\n createCone(name: string, context: Context): Promise<string>;\n switchCone(conversation: Conversation, context: Context): Promise<void>;\n setConversation(coneId: string, conversation: Conversation, context: Context): Promise<void>;\n update<T>(change: (tx: Tx, state: AgentsState) => T | Promise<T>, context: Context): Promise<T>;\n onChange(listener: (state: Readonly<AgentsState>) => void): () => void;\n onCone(listener: (conversation: Conversation) => void | Promise<void>): () => void;\n}\n\nasync function initial(harness: Harness, root: Conversation, context: Context) {\n return harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n if (!doc.cones[FIRST_CONE]) {\n const legacy = (await tx.doc(ConeDoc)).conversation;\n doc.cones[FIRST_CONE] = { name: 'sliccy', conversation: legacy ?? root.id };\n }\n if (!doc.cones[doc.active]) doc.active = FIRST_CONE;\n return JSON.parse(JSON.stringify(doc)) as AgentsState;\n }, context);\n}\n\nexport async function openAgents(\n harness: Harness,\n root: Conversation,\n context: Context\n): Promise<Agents> {\n let state = await initial(harness, root, context);\n const handles = new Map<number, Conversation>([[root.id, root]]);\n const handle = async (id: number, using: Context) => {\n const known = handles.get(id);\n if (known) return known;\n const found = await harness.conversation(id as ConversationId, using);\n if (found) handles.set(id, found);\n return found;\n };\n let cone =\n (await handle((state.cones[state.active] as ConeRecord).conversation, context)) ?? root;\n const changes = new Set<(state: Readonly<AgentsState>) => void>();\n const cones = new Set<(conversation: Conversation) => void | Promise<void>>();\n const notifyCone = async (next: Conversation) => {\n cone = next;\n await Promise.all([...cones].map((listener) => listener(next)));\n };\n const update: Agents['update'] = async (change, using) => {\n const [result, next] = await harness.commit(async (tx) => {\n const doc = await tx.doc(AgentsDoc);\n const value = await change(tx, doc as AgentsState);\n return [value, JSON.parse(JSON.stringify(doc)) as AgentsState] as const;\n }, using);\n state = next;\n for (const listener of changes) listener(state);\n return result;\n };\n const agents: Agents = {\n state: () => state,\n cone: () => cone,\n activeCone: () => state.active,\n async conversation(agentId, using) {\n const id = state.cones[agentId]?.conversation ?? state.scoops[agentId]?.conversation;\n return id === undefined ? undefined : handle(id, using);\n },\n async selectCone(id, using) {\n const record = state.cones[id];\n if (!record) throw new Error(`no cone ${id}`);\n if (state.active === id) return;\n await update((_tx, doc) => {\n doc.active = id;\n }, using);\n await notifyCone((await handle(record.conversation, using)) as Conversation);\n },\n async createCone(name, using) {\n const wanted = name.trim();\n if (!wanted) throw new Error('A cone needs a name.');\n if (Object.values(state.cones).some((record) => record.name === wanted))\n throw new Error(`A cone named ${wanted} already exists.`);\n const conversation = await harness.createConversation(\n {\n ownership: { kind: 'ownerless' },\n agent: { model: (await cone.agent(using)).model ?? null },\n },\n using\n );\n handles.set(conversation.id, conversation);\n return update((_tx, doc) => {\n const id = `cone-${doc.next}`;\n doc.next += 1;\n doc.cones[id] = { name: wanted, conversation: conversation.id };\n return id;\n }, using);\n },\n switchCone(conversation, using) {\n return agents.setConversation(state.active, conversation, using);\n },\n async setConversation(coneId, conversation, using) {\n handles.set(conversation.id, conversation);\n await update(async (tx, doc) => {\n (doc.cones[coneId] as ConeRecord).conversation = conversation.id;\n if (coneId === doc.active) (await tx.doc(ConeDoc)).conversation = conversation.id;\n }, using);\n if (coneId === state.active) await notifyCone(conversation);\n },\n update,\n onChange(listener) {\n changes.add(listener);\n return () => changes.delete(listener);\n },\n onCone(listener) {\n cones.add(listener);\n return () => cones.delete(listener);\n },\n };\n return agents;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/freezer/index.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { replicatedState } from "@earendil-works/chord";
|
|
2
2
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
3
3
|
import { answerText } from "../agent.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
live
|
|
6
|
+
} from "../agents.js";
|
|
7
|
+
import { HOME } from "../kernel/env.js";
|
|
8
|
+
import { SCOOPS_ROOT, workspace } from "../scoops/service.js";
|
|
9
|
+
const THAWED_KIND = "slicc.thawed";
|
|
6
10
|
const USAGE = `usage:
|
|
7
|
-
freezer list every
|
|
8
|
-
freezer show <id> a
|
|
9
|
-
freezer thaw <id>
|
|
11
|
+
freezer list every conversation: id, kind, name, title, messages, when
|
|
12
|
+
freezer show <id> a conversation as Markdown
|
|
13
|
+
freezer thaw <id> continue a stopped conversation as a cone
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Delete a cone, or start a new conversation, from the chat toolbar or the command palette.
|
|
12
16
|
`;
|
|
13
17
|
const TITLE_LENGTH = 60;
|
|
18
|
+
const TOUCH_MS = 500;
|
|
14
19
|
function textOf(entry) {
|
|
15
20
|
return answerText(entry.model?.[0]?.content).trim();
|
|
16
21
|
}
|
|
@@ -32,7 +37,7 @@ function markdown(record, entries) {
|
|
|
32
37
|
const out = [
|
|
33
38
|
`# ${record.title}`,
|
|
34
39
|
"",
|
|
35
|
-
`${record.name} \xB7 ${record.model} \xB7
|
|
40
|
+
`${record.name} \xB7 ${record.model} \xB7 ${new Date(record.frozenAt).toISOString()}`
|
|
36
41
|
];
|
|
37
42
|
for (const entry of entries) {
|
|
38
43
|
if (entry.kind === "pi.user" && textOf(entry)) out.push("", "## User", "", textOf(entry));
|
|
@@ -47,22 +52,79 @@ function markdown(record, entries) {
|
|
|
47
52
|
return `${out.join("\n")}
|
|
48
53
|
`;
|
|
49
54
|
}
|
|
50
|
-
function
|
|
51
|
-
return
|
|
52
|
-
id,
|
|
53
|
-
name: record.name,
|
|
54
|
-
title: record.title,
|
|
55
|
-
model: record.model,
|
|
56
|
-
messages: record.messages,
|
|
57
|
-
frozenAt: record.frozenAt
|
|
58
|
-
})).sort((a, b) => b.frozenAt - a.frozenAt);
|
|
55
|
+
function counted(entries) {
|
|
56
|
+
return entries.filter((entry) => entry.kind === "pi.user" || entry.kind === "pi.assistant").length;
|
|
59
57
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
function lastAt(entries) {
|
|
59
|
+
let at = 0;
|
|
60
|
+
for (const entry of entries)
|
|
61
|
+
for (const message of entry.model ?? []) {
|
|
62
|
+
const stamp = message.timestamp;
|
|
63
|
+
if (typeof stamp === "number" && stamp > at) at = stamp;
|
|
64
|
+
}
|
|
65
|
+
return at;
|
|
66
|
+
}
|
|
67
|
+
async function statsOf(harness, id, context) {
|
|
68
|
+
const conversation = await harness.conversation(id, context);
|
|
69
|
+
if (!conversation) return { title: firstLine([]), model: "", messages: 0, frozenAt: 0 };
|
|
70
|
+
const view = await conversation.context(context);
|
|
71
|
+
const agent = await conversation.agent(context);
|
|
72
|
+
return {
|
|
73
|
+
title: firstLine(view.entries),
|
|
74
|
+
model: modelName(agent.model),
|
|
75
|
+
messages: counted(view.entries),
|
|
76
|
+
frozenAt: lastAt(view.entries)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const kindOf = (scoop) => scoop.kind === "sync" ? "agent" : "scoop";
|
|
80
|
+
async function rows(harness, state, cache, context) {
|
|
81
|
+
const stats = async (conversation, stopped) => {
|
|
82
|
+
const cached = stopped ? cache.get(conversation) : void 0;
|
|
83
|
+
if (cached) return cached;
|
|
84
|
+
const found = await statsOf(harness, conversation, context);
|
|
85
|
+
if (stopped) cache.set(conversation, found);
|
|
86
|
+
return found;
|
|
87
|
+
};
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const [id, cone] of Object.entries(state.cones))
|
|
90
|
+
out.push({
|
|
91
|
+
id,
|
|
92
|
+
name: cone.name,
|
|
93
|
+
...await stats(cone.conversation, false),
|
|
94
|
+
kind: "cone",
|
|
95
|
+
live: true,
|
|
96
|
+
thawedAs: null
|
|
97
|
+
});
|
|
98
|
+
for (const [id, record] of Object.entries(state.frozen ?? {}))
|
|
99
|
+
out.push({
|
|
100
|
+
id,
|
|
101
|
+
name: record.name,
|
|
102
|
+
title: record.title,
|
|
103
|
+
model: record.model,
|
|
104
|
+
messages: record.messages,
|
|
105
|
+
frozenAt: record.frozenAt,
|
|
106
|
+
kind: "cone",
|
|
107
|
+
live: false,
|
|
108
|
+
thawedAs: null
|
|
109
|
+
});
|
|
110
|
+
for (const [id, scoop] of Object.entries(state.scoops)) {
|
|
111
|
+
if (scoop.removed) continue;
|
|
112
|
+
const running = live(scoop);
|
|
113
|
+
out.push({
|
|
114
|
+
id,
|
|
115
|
+
name: scoop.name,
|
|
116
|
+
...await stats(scoop.conversation, !running),
|
|
117
|
+
kind: kindOf(scoop),
|
|
118
|
+
live: running,
|
|
119
|
+
thawedAs: scoop.thawedAs ?? null
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out.sort((a, b) => Number(b.live) - Number(a.live) || b.frozenAt - a.frozenAt);
|
|
123
|
+
}
|
|
124
|
+
function thawedNote(kind, name, folder, kept) {
|
|
125
|
+
const what = kind === "agent" ? "agent run" : kind;
|
|
126
|
+
const place = kept ? `Its workspace ${folder} is still there.` : `Its workspace ${folder} was deleted when it stopped.`;
|
|
127
|
+
return `This was ${what} ${name}'s conversation, thawed as a cone. ${place} You now work in ${HOME} as a cone, with a cone's tools.`;
|
|
66
128
|
}
|
|
67
129
|
async function snapshot(conversation, name, now, context) {
|
|
68
130
|
const view = await conversation.context(context);
|
|
@@ -74,9 +136,7 @@ async function snapshot(conversation, name, now, context) {
|
|
|
74
136
|
conversation: conversation.id,
|
|
75
137
|
title: firstLine(view.entries),
|
|
76
138
|
model: modelName(agent.model),
|
|
77
|
-
messages: view.entries
|
|
78
|
-
(entry) => entry.kind === "pi.user" || entry.kind === "pi.assistant"
|
|
79
|
-
).length,
|
|
139
|
+
messages: counted(view.entries),
|
|
80
140
|
frozenAt: now
|
|
81
141
|
}
|
|
82
142
|
};
|
|
@@ -95,25 +155,34 @@ function frozenTarget(state, target) {
|
|
|
95
155
|
}
|
|
96
156
|
async function command(options, runtime, argv, context) {
|
|
97
157
|
const verb = argv[0] ?? "help";
|
|
158
|
+
const listed = await runtime.refresh();
|
|
98
159
|
if (verb === "list") {
|
|
99
|
-
const
|
|
100
|
-
(
|
|
160
|
+
const lines = listed.map(
|
|
161
|
+
(item2) => `${item2.id} ${item2.kind}${item2.live ? " (live)" : ""} ${item2.name} ${item2.title} ${item2.messages} messages ${new Date(item2.frozenAt).toISOString()}`
|
|
101
162
|
);
|
|
102
|
-
return { code: 0, out:
|
|
103
|
-
` : "
|
|
163
|
+
return { code: 0, out: lines.length ? `${lines.join("\n")}
|
|
164
|
+
` : "no conversations\n" };
|
|
104
165
|
}
|
|
105
166
|
if (verb !== "show" && verb !== "thaw")
|
|
106
167
|
return { code: verb === "help" || verb === "--help" ? 0 : 2, out: USAGE };
|
|
107
168
|
const id = argv[1] ?? "";
|
|
108
|
-
const
|
|
109
|
-
if (!
|
|
169
|
+
const item = listed.find((row) => row.id === id);
|
|
170
|
+
if (!item) return { code: 1, out: `freezer: there is no conversation ${id}
|
|
171
|
+
` };
|
|
172
|
+
if (verb === "thaw") {
|
|
173
|
+
if (item.live) return { code: 1, out: `freezer: ${item.name} is live; it needs no thawing
|
|
110
174
|
` };
|
|
111
|
-
if (verb === "thaw")
|
|
112
175
|
return { code: 0, out: `thawed ${id} as ${await runtime.thaw(id, context)}
|
|
113
176
|
` };
|
|
114
|
-
|
|
177
|
+
}
|
|
178
|
+
const state = options.agents.state();
|
|
179
|
+
const conversationId = state.frozen?.[id]?.conversation ?? state.cones[id]?.conversation ?? state.scoops[id]?.conversation;
|
|
180
|
+
const conversation = await options.harness.conversation(
|
|
181
|
+
conversationId,
|
|
182
|
+
context
|
|
183
|
+
);
|
|
115
184
|
const entries = conversation ? (await conversation.context(context)).entries : [];
|
|
116
|
-
return { code: 0, out: markdown(
|
|
185
|
+
return { code: 0, out: markdown(item, entries) };
|
|
117
186
|
}
|
|
118
187
|
async function freshLike(harness, conversation, context) {
|
|
119
188
|
const agent = await conversation.agent(context);
|
|
@@ -132,11 +201,140 @@ async function freshLike(harness, conversation, context) {
|
|
|
132
201
|
context
|
|
133
202
|
);
|
|
134
203
|
}
|
|
204
|
+
async function activeCone(agents, context) {
|
|
205
|
+
const conversation = await agents.conversation(agents.state().active, context);
|
|
206
|
+
if (!conversation) throw new Error("There is no active cone.");
|
|
207
|
+
return conversation;
|
|
208
|
+
}
|
|
209
|
+
async function thawScoop(options, id, scoop, context) {
|
|
210
|
+
const { harness, agents, env, now } = options;
|
|
211
|
+
if (scoop.removed) throw new Error(`There is no conversation ${id}.`);
|
|
212
|
+
if (live(scoop)) throw new Error(`${scoop.name} is still running; open it instead.`);
|
|
213
|
+
const conversation = await harness.conversation(scoop.conversation, context);
|
|
214
|
+
if (!conversation) throw new Error(`There is no conversation ${id}.`);
|
|
215
|
+
const last = (await conversation.context(context)).entries.at(-1);
|
|
216
|
+
const agent = await conversation.agent(context);
|
|
217
|
+
const cone = await (await activeCone(agents, context)).agent(context);
|
|
218
|
+
const created = {
|
|
219
|
+
ownership: { kind: "ownerless" },
|
|
220
|
+
agent: {
|
|
221
|
+
model: agent.model,
|
|
222
|
+
thinkingLevel: agent.thinkingLevel,
|
|
223
|
+
extensions: cone.extensions,
|
|
224
|
+
tools: cone.tools,
|
|
225
|
+
instructions: cone.instructions ?? null,
|
|
226
|
+
cwd: HOME
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const fork = last ? await conversation.fork(last.id, created, context) : await harness.createConversation(created, context);
|
|
230
|
+
const folder = workspace(scoop.folder);
|
|
231
|
+
const exists = await env.exists(folder, context);
|
|
232
|
+
const kept = exists.ok && exists.value;
|
|
233
|
+
await fork.submit(
|
|
234
|
+
{
|
|
235
|
+
type: "write",
|
|
236
|
+
entry: {
|
|
237
|
+
kind: THAWED_KIND,
|
|
238
|
+
data: { scoop: id, name: scoop.name, kind: kindOf(scoop), workspace: folder, kept },
|
|
239
|
+
model: [
|
|
240
|
+
{
|
|
241
|
+
role: "user",
|
|
242
|
+
content: thawedNote(kindOf(scoop), scoop.name, folder, kept),
|
|
243
|
+
timestamp: now()
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
context
|
|
249
|
+
);
|
|
250
|
+
const coneId = await agents.update((_tx, doc) => {
|
|
251
|
+
const target = `cone-${doc.next}`;
|
|
252
|
+
doc.next += 1;
|
|
253
|
+
doc.cones[target] = { name: scoop.name, conversation: fork.id };
|
|
254
|
+
doc.scoops[id].thawedAs = target;
|
|
255
|
+
return target;
|
|
256
|
+
}, context);
|
|
257
|
+
await agents.selectCone(coneId, context);
|
|
258
|
+
return coneId;
|
|
259
|
+
}
|
|
260
|
+
async function removeScoop(options, id, scoop, context) {
|
|
261
|
+
const { agents, env } = options;
|
|
262
|
+
if (live(scoop)) throw new Error(`${scoop.name} is still running; stop it first.`);
|
|
263
|
+
await agents.update((_tx, doc) => {
|
|
264
|
+
const entry = doc.scoops[id];
|
|
265
|
+
entry.removed = true;
|
|
266
|
+
entry.gone = true;
|
|
267
|
+
delete entry.frozen;
|
|
268
|
+
}, context);
|
|
269
|
+
await env.remove(`${SCOOPS_ROOT}/${scoop.folder}`, { recursive: true, force: true }, context);
|
|
270
|
+
}
|
|
271
|
+
function listing(harness, agents) {
|
|
272
|
+
const frozen = replicatedState([]);
|
|
273
|
+
const cache = /* @__PURE__ */ new Map();
|
|
274
|
+
let lock = Promise.resolve([]);
|
|
275
|
+
const refresh = () => {
|
|
276
|
+
lock = lock.catch(() => []).then(async () => {
|
|
277
|
+
const listed = await rows(harness, agents.state(), cache, BACKGROUND_CONTEXT);
|
|
278
|
+
frozen.replace(BACKGROUND_CONTEXT, listed);
|
|
279
|
+
return listed;
|
|
280
|
+
});
|
|
281
|
+
return lock;
|
|
282
|
+
};
|
|
283
|
+
const quiet = () => void refresh().catch(() => void 0);
|
|
284
|
+
quiet();
|
|
285
|
+
const stop = agents.onChange(quiet);
|
|
286
|
+
let timer;
|
|
287
|
+
const touch = () => {
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
timer = setTimeout(quiet, TOUCH_MS);
|
|
290
|
+
};
|
|
291
|
+
const off = () => {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
stop();
|
|
294
|
+
};
|
|
295
|
+
return { frozen, refresh, touch, off };
|
|
296
|
+
}
|
|
297
|
+
async function thawFrozen(agents, id, context) {
|
|
298
|
+
const coneId = await agents.update((_tx, doc) => {
|
|
299
|
+
const record = doc.frozen?.[id];
|
|
300
|
+
if (!record) throw new Error(`There is no frozen chat ${id}.`);
|
|
301
|
+
const reuse = !doc.cones[record.cone];
|
|
302
|
+
const target = reuse ? record.cone : `cone-${doc.next}`;
|
|
303
|
+
if (!reuse) doc.next += 1;
|
|
304
|
+
doc.cones[target] = { name: record.name, conversation: record.conversation };
|
|
305
|
+
for (const scoop of record.scoops) {
|
|
306
|
+
const entry = doc.scoops[scoop];
|
|
307
|
+
if (!entry) continue;
|
|
308
|
+
delete entry.frozen;
|
|
309
|
+
entry.cone = target;
|
|
310
|
+
}
|
|
311
|
+
delete doc.frozen?.[id];
|
|
312
|
+
return target;
|
|
313
|
+
}, context);
|
|
314
|
+
await agents.selectCone(coneId, context);
|
|
315
|
+
return coneId;
|
|
316
|
+
}
|
|
317
|
+
async function discardFrozen(options, id, context) {
|
|
318
|
+
const { agents, env } = options;
|
|
319
|
+
const record = agents.state().frozen?.[id];
|
|
320
|
+
if (!record) throw new Error(`There is no frozen chat ${id}.`);
|
|
321
|
+
const folders = record.scoops.map((scoop) => agents.state().scoops[scoop]?.folder).filter((folder) => Boolean(folder));
|
|
322
|
+
await agents.update((_tx, doc) => {
|
|
323
|
+
for (const scoop of record.scoops) {
|
|
324
|
+
const entry = doc.scoops[scoop];
|
|
325
|
+
if (!entry) continue;
|
|
326
|
+
delete entry.frozen;
|
|
327
|
+
entry.gone = true;
|
|
328
|
+
}
|
|
329
|
+
delete doc.frozen?.[id];
|
|
330
|
+
}, context);
|
|
331
|
+
for (const folder of folders)
|
|
332
|
+
await env.remove(`${SCOOPS_ROOT}/${folder}`, { recursive: true, force: true }, context);
|
|
333
|
+
}
|
|
135
334
|
function attachFreezer(options) {
|
|
136
|
-
const { harness, agents
|
|
335
|
+
const { harness, agents } = options;
|
|
137
336
|
const now = options.now ?? Date.now;
|
|
138
|
-
const frozen =
|
|
139
|
-
const off = agents.onChange((state) => frozen.replace(BACKGROUND_CONTEXT, list(state)));
|
|
337
|
+
const { frozen, refresh, touch, off } = listing(harness, agents);
|
|
140
338
|
const conversationOf = async (agentId, context) => {
|
|
141
339
|
const conversation = await agents.conversation(agentId, context);
|
|
142
340
|
if (!conversation) throw new Error(`There is no cone ${agentId}.`);
|
|
@@ -230,41 +428,25 @@ function attachFreezer(options) {
|
|
|
230
428
|
return id;
|
|
231
429
|
},
|
|
232
430
|
async thaw(id, context) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const entry = doc.scoops[scoop];
|
|
242
|
-
if (!entry) continue;
|
|
243
|
-
delete entry.frozen;
|
|
244
|
-
entry.cone = target;
|
|
245
|
-
}
|
|
246
|
-
delete doc.frozen?.[id];
|
|
247
|
-
return target;
|
|
248
|
-
}, context);
|
|
249
|
-
await agents.selectCone(coneId, context);
|
|
250
|
-
return coneId;
|
|
431
|
+
const state = agents.state();
|
|
432
|
+
if (state.cones[id]) {
|
|
433
|
+
await agents.selectCone(id, context);
|
|
434
|
+
return id;
|
|
435
|
+
}
|
|
436
|
+
const scoop = state.scoops[id];
|
|
437
|
+
if (scoop) return thawScoop({ ...options, now }, id, scoop, context);
|
|
438
|
+
return thawFrozen(agents, id, context);
|
|
251
439
|
},
|
|
252
440
|
async discard(id, context) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (!entry) continue;
|
|
260
|
-
delete entry.frozen;
|
|
261
|
-
entry.gone = true;
|
|
262
|
-
}
|
|
263
|
-
delete doc.frozen?.[id];
|
|
264
|
-
}, context);
|
|
265
|
-
for (const folder of folders)
|
|
266
|
-
await env.remove(`${SCOOPS_ROOT}/${folder}`, { recursive: true, force: true }, context);
|
|
441
|
+
const state = agents.state();
|
|
442
|
+
const scoop = state.scoops[id];
|
|
443
|
+
if (scoop) return removeScoop(options, id, scoop, context);
|
|
444
|
+
const cone = state.cones[id];
|
|
445
|
+
if (cone) throw new Error(`${cone.name} is live; delete the cone first.`);
|
|
446
|
+
return discardFrozen(options, id, context);
|
|
267
447
|
},
|
|
448
|
+
refresh,
|
|
449
|
+
touch,
|
|
268
450
|
frozenTarget: (target) => frozenTarget(agents.state(), target),
|
|
269
451
|
command: (argv, _caller, context) => command(options, runtime, argv, context),
|
|
270
452
|
close: () => off()
|
|
@@ -272,12 +454,14 @@ function attachFreezer(options) {
|
|
|
272
454
|
return runtime;
|
|
273
455
|
}
|
|
274
456
|
export {
|
|
457
|
+
THAWED_KIND,
|
|
458
|
+
TOUCH_MS,
|
|
275
459
|
USAGE,
|
|
276
460
|
attachFreezer,
|
|
277
461
|
firstLine,
|
|
278
|
-
freeName,
|
|
279
462
|
frozenTarget,
|
|
280
463
|
markdown,
|
|
281
|
-
modelName
|
|
464
|
+
modelName,
|
|
465
|
+
thawedNote
|
|
282
466
|
};
|
|
283
467
|
//# sourceMappingURL=index.js.map
|