@zenera/cli 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
@@ -1,14 +1,18 @@
1
- import { readProjectConfig } from '@zenera/neo';
1
+ import { readProjectConfig, sandboxLimits, } from '@zenera/neo';
2
2
  import { parse } from "../args.js";
3
3
  import { resolveBuild } from "../image.js";
4
- import { engineDisk, ensurePodmanReady, ownedContainers, podmanStatus, removeContainers, } from "../podman.js";
4
+ import { engineDisk, ensurePodmanReady, idleContainers, ownedContainers, podmanStatus, removeContainers, sessionOwners, } from "../podman.js";
5
5
  import { current as currentProject, dirSize, isProjectDir, open as openProject, Registry, sessionIds, } from "../projects.js";
6
- import { ago, bold, bytes, dim, green, json, note, red, table, usageError, write, writeAll, yellow, } from "../term.js";
6
+ import { ago, bold, bytes, count, dim, green, json, note, red, table, usageError, write, writeAll, yellow, } from "../term.js";
7
7
  const USAGE = 'zen sandbox [status|up|pull|clean|disk] [options]';
8
8
  /** Enough to see the pattern; the rest are a number. */
9
9
  const LISTED = 6;
10
10
  /** Under the labels, which is where the eye already is. */
11
11
  const INDENT = ' '.repeat(11);
12
+ /** `agents.yaml` states memory in MiB; `bytes` prints bytes. */
13
+ const MIB = 1024 * 1024;
14
+ /** Below this many free locks, `status` says so: a batch can spend that in minutes. */
15
+ const FEW_LOCKS = 256;
12
16
  // ---------------------------------------------------------------------------
13
17
  // The container engine, on its own
14
18
  //
@@ -31,6 +35,8 @@ export const sandbox = {
31
35
  '',
32
36
  ' --project <name|dir> Which project the image and containers belong to.',
33
37
  ' --image <ref> Use this image instead of the project\u2019s.',
38
+ ' --idle With clean: only what no live run is using — stopped ones,',
39
+ ' and running ones whose run was killed before it cleaned up.',
34
40
  '',
35
41
  'Without --project this is the project you are standing in, and standing',
36
42
  'nowhere is an answer: `status` then reports the engine and every container',
@@ -43,6 +49,8 @@ export const sandbox = {
43
49
  const { values, positionals } = parse(ctx.args, {
44
50
  project: { type: 'string' },
45
51
  image: { type: 'string' },
52
+ stopped: { type: 'boolean' },
53
+ idle: { type: 'boolean' },
46
54
  }, USAGE);
47
55
  const what = positionals[0] ?? 'status';
48
56
  if (!['status', 'up', 'pull', 'clean', 'disk'].includes(what)) {
@@ -68,7 +76,7 @@ export const sandbox = {
68
76
  }
69
77
  return up(image, build, true, ctx.json, true);
70
78
  case 'clean':
71
- return clean(ctx.json);
79
+ return clean(ctx.json, Boolean(values.idle || values.stopped));
72
80
  case 'disk':
73
81
  return disk(ctx.json);
74
82
  }
@@ -97,6 +105,7 @@ async function projectSandbox(cwd, values) {
97
105
  name: found.name,
98
106
  image: build?.tag ?? config.sandbox?.image,
99
107
  build,
108
+ limits: widest(config),
100
109
  };
101
110
  }
102
111
  catch {
@@ -113,12 +122,16 @@ async function status(project, image, asJson) {
113
122
  // a report about one project must not list another's — or the faker's,
114
123
  // which wears the same label and belongs to no project at all.
115
124
  const containers = project ? ofProject(project.dir, all) : all;
125
+ const idle = all.length > 0 ? idleContainers(all, await sessionOwners()) : [];
116
126
  if (asJson) {
117
127
  json({
118
128
  ...found,
119
129
  project: project?.name ?? null,
120
130
  dockerfile: build?.dockerfile ?? null,
131
+ limits: project?.limits ?? null,
132
+ fits: fits(found.capacity?.memory, project?.limits?.memory) ?? null,
121
133
  containers,
134
+ idle: idle.map((c) => c.name),
122
135
  });
123
136
  return;
124
137
  }
@@ -128,7 +141,17 @@ async function status(project, image, asJson) {
128
141
  const state = found.machine.starting ? yellow('starting') : mark(found.machine.running);
129
142
  write(`${bold('machine')} ${found.machine.name} ${state}`);
130
143
  }
144
+ if (found.connection) {
145
+ // A concurrency limit wearing a connection string: `ssh://` opens one
146
+ // connection per call and sshd refuses them past ten, which stays
147
+ // invisible until a batch is wide enough to lose tasks at random.
148
+ const how = found.connection.shared
149
+ ? dim('· one shared connection, no limit on parallel calls')
150
+ : yellow('· a new ssh connection per call — about 10 at a time');
151
+ write(`${bold('connection')} ${dim(found.connection.uri)} ${how}`);
152
+ }
131
153
  write(`${bold('responds')} ${mark(found.ready)}`);
154
+ writeAll(limitLines(found, project?.limits));
132
155
  if (project) {
133
156
  write(`${bold('project')} ${project.name} ${dim(project.dir)}`);
134
157
  }
@@ -138,12 +161,69 @@ async function status(project, image, asJson) {
138
161
  if (build) {
139
162
  write(`${bold('dockerfile')} ${dim(build.dockerfile)}`);
140
163
  }
141
- writeAll(containerLines(containers, project?.name));
164
+ writeAll(lockLines(found.freeLocks, idle.length));
165
+ writeAll(containerLines(containers, idle, project?.name, all.length));
142
166
  if (!found.installed || !found.ready) {
143
167
  note('');
144
168
  note(dim('run `zen sandbox up` to fix what can be fixed.'));
145
169
  }
146
170
  }
171
+ /**
172
+ * The hungriest container this project can start. An agent may override the
173
+ * project's `sandbox:` block, and it is the largest of them that decides
174
+ * whether a batch fits — reporting the project's own figure would understate
175
+ * the machine by however much an agent asked for on top.
176
+ *
177
+ * The overrides are merged onto the project's block rather than read alone,
178
+ * because that is what `SandboxPool.for` does when it builds the container: an
179
+ * agent that names only `memory:` still runs with the project's `cpus:`.
180
+ */
181
+ function widest(config) {
182
+ const base = config.sandbox ?? {};
183
+ return config.agents
184
+ .map((a) => sandboxLimits(a.sandbox ? { ...base, ...a.sandbox } : base))
185
+ .reduce((most, one) => (one.memory > most.memory ? one : most), sandboxLimits(base));
186
+ }
187
+ /**
188
+ * How many containers of this size the engine could hold at once.
189
+ *
190
+ * Nothing enforces it — `--memory` caps one container and the kernel kills
191
+ * whatever overruns the sum, so this is what the machine can honour, not a
192
+ * gate anything checks. It is worth printing because neither figure gives it
193
+ * alone: off Linux the memory is the *machine's*, which has nothing to do with
194
+ * the host, so a few containers at the 4096 MiB default can be past it while
195
+ * the laptop sits nearly empty.
196
+ */
197
+ function fits(capacity, container) {
198
+ if (!capacity || !container) {
199
+ return undefined;
200
+ }
201
+ return Math.max(0, Math.floor(capacity / container));
202
+ }
203
+ /**
204
+ * Two ceilings, in the order they bite: what the engine has, and what one
205
+ * container is permitted to take of it.
206
+ */
207
+ function limitLines(found, limits) {
208
+ const lines = [];
209
+ const where = found.machine ? 'machine' : 'host';
210
+ if (found.capacity) {
211
+ const { memory, swap, cpus } = found.capacity;
212
+ const swapped = swap > 0 ? `${bytes(swap * MIB)} swap` : 'no swap';
213
+ lines.push(`${bold('capacity')} ${bytes(memory * MIB)} ${dim(`· ${swapped} · ${cpus} cpus · the ${where}'s, not the host's`)}`);
214
+ }
215
+ if (limits) {
216
+ const room = fits(found.capacity?.memory, limits.memory);
217
+ const how = limits.declared ? limits.hardening : `${limits.hardening} defaults`;
218
+ const tail = room === undefined
219
+ ? how
220
+ : room > 0
221
+ ? `· ${how} · the ${where} has room for ${room}`
222
+ : `· ${how} · more than the ${where} has`;
223
+ lines.push(`${bold('container')} ${bytes(limits.memory * MIB)} ${dim(`· ${limits.cpus} cpus ${tail}`)}`);
224
+ }
225
+ return lines;
226
+ }
147
227
  /**
148
228
  * The containers this project's sessions made. A container carries the session
149
229
  * id that made it in a label, and a session id is a directory name under the
@@ -154,6 +234,22 @@ function ofProject(dir, containers) {
154
234
  const sessions = new Set(sessionIds(dir));
155
235
  return containers.filter((c) => c.key !== undefined && sessions.has(c.key));
156
236
  }
237
+ /**
238
+ * Every container the engine holds takes one of its locks, stopped or not, and
239
+ * at zero nothing can be created — for any project. Said only when it is close.
240
+ */
241
+ function lockLines(free, idle) {
242
+ if (free === undefined || free >= FEW_LOCKS) {
243
+ return [];
244
+ }
245
+ const left = free === 0 ? red('none free') : yellow(`${free} free`);
246
+ return [
247
+ `${bold('locks')} ${left} ${dim('· one per container, stopped ones included')}`,
248
+ ...(idle > 0
249
+ ? [`${INDENT}${dim(`${idle} no live run is using — zen sandbox clean --idle`)}`]
250
+ : []),
251
+ ];
252
+ }
157
253
  /**
158
254
  * One per line rather than one long line, because there is normally more than
159
255
  * one and the interesting part — how old, and whether anything is still up —
@@ -164,21 +260,31 @@ function ofProject(dir, containers) {
164
260
  * stopped ones behind. `scope` names the project these belong to, and saying
165
261
  * so is half the answer — the other half is that there are more elsewhere.
166
262
  */
167
- function containerLines(containers, scope) {
263
+ function containerLines(containers, idle, scope, total = containers.length) {
168
264
  const tail = scope
169
265
  ? `one per session in ${scope}, kept by \`persist: true\` — all of them: zen sandbox disk`
170
266
  : 'one per session, kept by `persist: true` — see: zen sandbox disk';
267
+ const elsewhere = total > containers.length ? ` · ${total} on the machine` : '';
171
268
  if (containers.length === 0) {
172
- return [`${bold('containers')} ${dim(scope ? `none in ${scope}` : 'none')}`];
269
+ return [`${bold('containers')} ${dim(scope ? `none in ${scope}${elsewhere}` : 'none')}`];
173
270
  }
271
+ // Running, but the run that started it is gone: nothing will ever stop it.
272
+ const orphans = new Set(idle.filter((c) => c.state === 'running').map((c) => c.name));
174
273
  const running = containers.filter((c) => c.state === 'running').length;
175
- const head = `${bold('containers')} ${containers.length} ${dim(running ? `· ${running} running` : '· none running')}`;
274
+ const lost = containers.filter((c) => orphans.has(c.name)).length;
275
+ const head = `${bold('containers')} ${containers.length} ${dim((running ? `· ${running} running` : '· none running') +
276
+ (lost ? `, ${lost} of them orphaned` : '') +
277
+ elsewhere)}`;
176
278
  const rows = containers
177
279
  .slice(0, LISTED)
178
280
  .map((c) => [
179
281
  INDENT.slice(2),
180
282
  c.name,
181
- c.state === 'running' ? green('running') : dim(c.state),
283
+ orphans.has(c.name)
284
+ ? yellow('orphaned')
285
+ : c.state === 'running'
286
+ ? green('running')
287
+ : dim(c.state),
182
288
  dim(ago(c.createdAt)),
183
289
  ]);
184
290
  const rest = containers.length - LISTED;
@@ -187,6 +293,9 @@ function containerLines(containers, scope) {
187
293
  ...table(rows),
188
294
  ...(rest > 0 ? [`${INDENT}${dim(`+${rest} more`)}`] : []),
189
295
  `${INDENT}${dim(tail)}`,
296
+ ...(lost
297
+ ? [`${INDENT}${dim('orphaned: its run was killed — zen sandbox clean --idle')}`]
298
+ : []),
190
299
  ];
191
300
  }
192
301
  async function up(image, build, yes, asJson, rebuild = false) {
@@ -197,8 +306,9 @@ async function up(image, build, yes, asJson, rebuild = false) {
197
306
  }
198
307
  write(`${green('ready')}${image ? ` ${dim(image)}` : ''}`);
199
308
  }
200
- async function clean(asJson) {
201
- const containers = await ownedContainers(undefined, undefined, { sizes: true });
309
+ async function clean(asJson, idle) {
310
+ const owned = await ownedContainers(undefined, undefined, { sizes: true });
311
+ const containers = idle ? idleContainers(owned, await sessionOwners()) : owned;
202
312
  const names = containers.map((c) => c.name);
203
313
  const freed = containers.reduce((n, c) => n + (c.size ?? 0), 0);
204
314
  await removeContainers(names);
@@ -236,10 +346,30 @@ async function disk(asJson) {
236
346
  write('');
237
347
  }
238
348
  writeAll(projectRows(projects, loose));
239
- if (usage && usage.images.reclaimable > 0) {
349
+ const idle = idleContainers(containers, await sessionOwners());
350
+ const live = containers.length - idle.length;
351
+ const sessions = projects.reduce((n, p) => n + p.files, 0);
352
+ const hints = [
353
+ ...(idle.length > 0
354
+ ? [`zen sandbox clean --idle ${count(idle.length, 'container')} no live run is using`]
355
+ : []),
356
+ ...(live > 0
357
+ ? [`zen sandbox clean also the ${live} in use, cutting off whatever runs there`]
358
+ : []),
359
+ ...(usage && usage.images.reclaimable > 0
360
+ ? [`podman image prune -a ${bytes(usage.images.reclaimable)} of unused images`]
361
+ : []),
362
+ // The ON DISK column is the bulk of the report and no command above touches it.
363
+ ...(sessions > 0
364
+ ? [
365
+ `${bytes(sessions)} ON DISK is project directories on this host, not the engine: ` +
366
+ 'removing containers frees none of it',
367
+ ]
368
+ : []),
369
+ ];
370
+ if (hints.length > 0) {
240
371
  write('');
241
- write(dim(`zen sandbox clean every container above`));
242
- write(dim(`podman image prune -a ${bytes(usage.images.reclaimable)} of unused images`));
372
+ writeAll(hints.map((h) => dim(h)));
243
373
  }
244
374
  }
245
375
  function engineRows(usage) {
@@ -311,13 +441,16 @@ function projectRows(projects, loose) {
311
441
  // Containers whose session directory is gone, and faker's, which are
312
442
  // labelled the same way and belong to no project at all.
313
443
  const size = loose.reduce((n, c) => n + (c.size ?? 0), 0);
444
+ const fakers = loose.filter((c) => c.key?.startsWith('faker-')).length;
314
445
  rows.push([
315
446
  dim('(unclaimed)'),
316
447
  dim('—'),
317
448
  dim('—'),
318
449
  dim(String(loose.length)),
319
450
  dim(bytes(size)),
320
- dim('no session owns these'),
451
+ dim(fakers === loose.length
452
+ ? `zen faker ${fakers === 1 ? 'server' : 'servers'}, no project`
453
+ : 'no session owns these'),
321
454
  ]);
322
455
  }
323
456
  const total = (pick) => projects.reduce((n, p) => n + pick(p), 0);
package/dist/engine.d.ts CHANGED
@@ -12,10 +12,23 @@ export interface EngineOptions {
12
12
  image?: string;
13
13
  /** another directory to remember into — `--memory` */
14
14
  memoryDir?: string;
15
+ /** recall only: nothing is written and several runs may share one graph */
16
+ memoryReadOnly?: boolean;
15
17
  /** whether credentials reach the sandbox — `--no-keys` sets this false */
16
18
  keys?: boolean;
17
19
  /** answer the sandbox's install question without asking — `--yes` */
18
20
  yes?: boolean;
21
+ /**
22
+ * Hold back the warnings about the project rather than the run. A batch
23
+ * opens one project a hundred times, and a hundred copies of the same
24
+ * complaint is a worse report than one.
25
+ */
26
+ quiet?: boolean;
27
+ /**
28
+ * The session will never be resumed, so its containers are removed on close
29
+ * whatever `persist:` says. A stopped one would only hold an engine lock.
30
+ */
31
+ disposable?: boolean;
19
32
  }
20
33
  export interface Engine {
21
34
  project: AgentProject;
@@ -27,6 +40,8 @@ export interface Engine {
27
40
  /** whether the agent was denied every writing tool */
28
41
  readOnly: boolean;
29
42
  session: SessionPaths;
43
+ /** the graph this session remembers into, when any agent has one */
44
+ memory?: string;
30
45
  /** the session's accumulated state, when it has one */
31
46
  state?: AgentState;
32
47
  /** the containers this session may start; present whether or not it does */
@@ -56,7 +71,16 @@ export interface RunOutcome {
56
71
  durationMs: number;
57
72
  /** where the report landed, when one could be rendered */
58
73
  report?: string;
74
+ /** where the Mermaid graph landed, when one could be emitted */
75
+ graph?: string;
59
76
  }
77
+ /**
78
+ * What one finished run looks like to a program: `zen run --json` prints it,
79
+ * and a batch writes one beside every item. The same function on purpose — a
80
+ * batch whose items were shaped differently would make every consumer of a
81
+ * single run a special case.
82
+ */
83
+ export declare function envelope(engine: Engine, outcome: RunOutcome): Record<string, unknown>;
60
84
  /**
61
85
  * One turn. The session's state decides whether this starts a run or continues
62
86
  * one — which is the whole of what `resume` used to be, and why it is not a
package/dist/engine.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AgentRunner, FilePayloadStore, SANDBOX_MOUNT, SKILLS_MOUNT, Workspace, assertState, buildRunReport, exaTools, lastText, loadProject, readProjectConfig, renderReportHtml, sandboxTools, turns, workspaceTools, } from '@zenera/neo';
1
+ import { AgentRunner, FilePayloadStore, SANDBOX_MOUNT, SKILLS_MOUNT, Workspace, assertState, buildRunReport, exaTools, fileTools, lastText, loadProject, memoryDir, readProjectConfig, renderReportHtml, sandboxTools, turns, } from '@zenera/neo';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { writeFile } from 'node:fs/promises';
4
4
  import { resolve } from 'node:path';
@@ -8,8 +8,9 @@ import { readJson, writeJson } from "./home.js";
8
8
  import { KeyStore, assertUsable } from "./keys.js";
9
9
  import { projectMounts } from "./projects.js";
10
10
  import { buildSandbox, preflight, teardown, usesSandbox } from "./sandbox.js";
11
- import { acquire, createRun, readSessionMeta, writeSessionMeta, } from "./session.js";
11
+ import { acquire, createRun, readSessionMeta, writeRunMeta, writeSessionMeta, } from "./session.js";
12
12
  import { CliError, EXIT, invalidError, warn } from "./term.js";
13
+ import { traceMermaid } from "./trace.js";
13
14
  /**
14
15
  * Wires the library to a session directory. Every store is file-backed and
15
16
  * rooted inside `.data/`, so a session is self-contained: copy the directory
@@ -27,7 +28,9 @@ export async function open(opts) {
27
28
  // build: one SDK's words about one model, when the useful answer is which
28
29
  // of the project's models are reachable and which are not.
29
30
  for (const issue of auditModels(opts.project.dir, keys)) {
30
- warn(describeIssue(issue));
31
+ if (!opts.quiet) {
32
+ warn(describeIssue(issue));
33
+ }
31
34
  }
32
35
  const meta = await readSessionMeta(opts.session);
33
36
  const workspace = resolve(meta.workspace);
@@ -40,13 +43,15 @@ export async function open(opts) {
40
43
  let sandbox;
41
44
  let project;
42
45
  let files;
46
+ let memory;
43
47
  try {
44
48
  const { root, config } = readProjectConfig(opts.project.dir);
45
49
  // Naming a directory declares the graph, but it cannot bind anyone to
46
50
  // it — so say when the run is pointed at a memory no agent will touch.
47
- if (opts.memoryDir && !config.agents.some((a) => a.memory)) {
51
+ if (opts.memoryDir && !opts.quiet && !config.agents.some((a) => a.memory)) {
48
52
  warn('--memory names a directory no agent uses — none has `memory: true`');
49
53
  }
54
+ memory = memoryDir(root, config, opts.memoryDir);
50
55
  // Assets and the skill catalog are mounted for both, under one name.
51
56
  const mounts = projectMounts(root, config, opts.memoryDir);
52
57
  sandbox = buildSandbox({
@@ -59,6 +64,7 @@ export async function open(opts) {
59
64
  keys: opts.keys,
60
65
  env: env?.names,
61
66
  mounts,
67
+ ...(opts.disposable ? { persist: false } : {}),
62
68
  });
63
69
  const workspaceOptions = {
64
70
  root: workspace,
@@ -74,7 +80,7 @@ export async function open(opts) {
74
80
  // Both groups are pointed at one directory, so both are told the
75
81
  // one name for it: whatever `run_command` prints a path as,
76
82
  // `read_file` accepts.
77
- ...workspaceTools(workspaceOptions),
83
+ ...fileTools(workspaceOptions),
78
84
  ...sandboxTools(sandbox.pool),
79
85
  // Registered whether or not a key exists: the credential is
80
86
  // read when a tool is called, so a project that names `exa:*`
@@ -84,6 +90,7 @@ export async function open(opts) {
84
90
  ],
85
91
  skillsAt: SKILLS_MOUNT,
86
92
  memoryDir: opts.memoryDir,
93
+ memoryReadOnly: opts.memoryReadOnly,
87
94
  payloads,
88
95
  resolveFile: (path) => files.within(path),
89
96
  });
@@ -118,6 +125,7 @@ export async function open(opts) {
118
125
  workspaceMount: sandbox.spec.workdir ?? SANDBOX_MOUNT,
119
126
  readOnly: Boolean(opts.readOnly),
120
127
  session: opts.session,
128
+ memory,
121
129
  state,
122
130
  sandbox,
123
131
  lock,
@@ -151,6 +159,33 @@ export function mounts(engine) {
151
159
  })),
152
160
  ];
153
161
  }
162
+ /**
163
+ * What one finished run looks like to a program: `zen run --json` prints it,
164
+ * and a batch writes one beside every item. The same function on purpose — a
165
+ * batch whose items were shaped differently would make every consumer of a
166
+ * single run a special case.
167
+ */
168
+ export function envelope(engine, outcome) {
169
+ return {
170
+ session: { id: engine.session.id, dir: engine.session.dir },
171
+ run: {
172
+ id: outcome.run.id,
173
+ dir: outcome.run.dir,
174
+ input: outcome.run.input,
175
+ output: outcome.run.output,
176
+ state: outcome.run.state,
177
+ meta: outcome.run.meta,
178
+ ...(outcome.report ? { report: outcome.report } : {}),
179
+ ...(outcome.graph ? { graph: outcome.graph } : {}),
180
+ },
181
+ mounts: mounts(engine),
182
+ agent: outcome.result.agent,
183
+ stopReason: outcome.result.stopReason,
184
+ durationMs: outcome.durationMs,
185
+ usage: outcome.result.usage,
186
+ output: outcome.text,
187
+ };
188
+ }
154
189
  /**
155
190
  * One turn. The session's state decides whether this starts a run or continues
156
191
  * one — which is the whole of what `resume` used to be, and why it is not a
@@ -205,7 +240,24 @@ async function record(engine, input, result, startedAt, durationMs) {
205
240
  catch {
206
241
  // A report is a convenience. Losing it must not lose the run.
207
242
  }
208
- writeJson(run.meta, {
243
+ // The same trajectory for the other reader. `report.html` is for a person;
244
+ // this is what `zen inspect graph` prints, written down while the state is
245
+ // already in hand — a batch of a hundred items is otherwise a hundred more
246
+ // commands to run before anything can be read.
247
+ let graph;
248
+ try {
249
+ await writeFile(run.graph, traceMermaid(result.state, {
250
+ runId: run.id,
251
+ dir: run.dir,
252
+ workspace: engine.workspace,
253
+ memory: engine.memory,
254
+ }), 'utf8');
255
+ graph = run.graph;
256
+ }
257
+ catch {
258
+ // Same rule as the report: a view of the run is not the run.
259
+ }
260
+ writeRunMeta(run, {
209
261
  version: 1,
210
262
  id: run.id,
211
263
  session: session.id,
@@ -217,9 +269,10 @@ async function record(engine, input, result, startedAt, durationMs) {
217
269
  turns: turns(result.state),
218
270
  usage: result.usage,
219
271
  workspace: engine.workspace,
272
+ memory: engine.memory,
220
273
  error: result.state.error,
221
- }, 0o644);
222
- return { run, result, text, durationMs, report };
274
+ });
275
+ return { run, result, text, durationMs, report, graph };
223
276
  }
224
277
  function asText(input) {
225
278
  if (typeof input === 'string') {
@@ -0,0 +1,59 @@
1
+ import { type AgentEvent } from '@zenera/neo';
2
+ export type EventBody = {
3
+ type: 'llm';
4
+ model: string;
5
+ in: number;
6
+ cached: number;
7
+ out: number;
8
+ reasoning: number;
9
+ ms?: number;
10
+ } | {
11
+ type: 'tool';
12
+ phase: 'start';
13
+ id: string;
14
+ name: string;
15
+ subject: string;
16
+ } | {
17
+ type: 'tool';
18
+ phase: 'end';
19
+ id: string;
20
+ name: string;
21
+ ok: boolean;
22
+ ms?: number;
23
+ } | {
24
+ type: 'say';
25
+ text: string;
26
+ };
27
+ export type EventLine = EventBody & {
28
+ t: string;
29
+ };
30
+ /** A line that cannot be written is dropped: watching a run must not break it. */
31
+ export declare function eventLog(path: string): (body: EventBody) => void;
32
+ /** For `zen run`: the runtime's own events, folded to the shared lines. */
33
+ export declare function agentEventLog(path: string): (event: AgentEvent) => void;
34
+ /** One line of copilot's JSONL, as much of it as is read here. */
35
+ export interface CopilotEvent {
36
+ type: string;
37
+ data?: Record<string, unknown>;
38
+ }
39
+ /** A model call read off copilot's span file - see `meta/tokens.ts`. */
40
+ export interface CopilotCall {
41
+ model: string;
42
+ usage: {
43
+ inputTokens: number;
44
+ cachedInputTokens: number;
45
+ outputTokens: number;
46
+ reasoningTokens: number;
47
+ };
48
+ durationMs: number;
49
+ }
50
+ /**
51
+ * For `zen meta run`: copilot's events and spans, folded to the same lines.
52
+ * Its JSONL has no token counts, so `llm` lines come from the spans instead,
53
+ * about a second after each call.
54
+ */
55
+ export declare function copilotEventLog(path: string): {
56
+ event: (event: CopilotEvent) => void;
57
+ call: (call: CopilotCall) => void;
58
+ };
59
+ //# sourceMappingURL=eventlog.d.ts.map