omnirush 0.8.6 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/assets/CHANGELOG.md +120 -0
  2. package/assets/extensions/omnirush/agents-lib.ts +501 -63
  3. package/assets/extensions/omnirush/agents.ts +140 -115
  4. package/assets/extensions/omnirush/bgshell-lib.ts +432 -0
  5. package/assets/extensions/omnirush/bgshell.ts +392 -0
  6. package/assets/extensions/omnirush/capture/workspace-collector.ts +86 -22
  7. package/assets/extensions/omnirush/collector.ts +275 -43
  8. package/assets/extensions/omnirush/commands.ts +2 -0
  9. package/assets/extensions/omnirush/deliveries.ts +145 -0
  10. package/assets/extensions/omnirush/guard/UPSTREAM +2 -0
  11. package/assets/extensions/omnirush/guard/git-command-policy.ts +885 -0
  12. package/assets/extensions/omnirush/guard-lib.ts +230 -0
  13. package/assets/extensions/omnirush/guard.ts +340 -0
  14. package/assets/extensions/omnirush/index.ts +32 -6
  15. package/assets/extensions/omnirush/mcp.ts +103 -8
  16. package/assets/extensions/omnirush/memory-lib.ts +516 -0
  17. package/assets/extensions/omnirush/pi-engine.ts +115 -3
  18. package/assets/extensions/omnirush/plan-lib.ts +38 -9
  19. package/assets/extensions/omnirush/plan.ts +85 -30
  20. package/assets/extensions/omnirush/sota.ts +52 -4
  21. package/assets/extensions/omnirush/status-lib.ts +3 -0
  22. package/assets/extensions/omnirush/subagent-marker.ts +21 -0
  23. package/assets/extensions/omnirush/subagents-lib.ts +623 -0
  24. package/assets/extensions/omnirush/subagents.ts +305 -0
  25. package/assets/extensions/omnirush/swarm-lib.ts +142 -0
  26. package/assets/extensions/omnirush/swarm.ts +95 -0
  27. package/assets/extensions/omnirush/voice/capture.ts +502 -0
  28. package/assets/extensions/omnirush/voice/core/UPSTREAM +16 -0
  29. package/assets/extensions/omnirush/voice/core/file-source.ts +70 -0
  30. package/assets/extensions/omnirush/voice/core/index.ts +21 -0
  31. package/assets/extensions/omnirush/voice/core/keyterms.ts +117 -0
  32. package/assets/extensions/omnirush/voice/core/resample.ts +63 -0
  33. package/assets/extensions/omnirush/voice/core/segmenter.ts +231 -0
  34. package/assets/extensions/omnirush/voice/core/session.ts +403 -0
  35. package/assets/extensions/omnirush/voice/core/text.ts +81 -0
  36. package/assets/extensions/omnirush/voice/core/transcriber.ts +135 -0
  37. package/assets/extensions/omnirush/voice/core/types.ts +102 -0
  38. package/assets/extensions/omnirush/voice/core/wav.ts +95 -0
  39. package/assets/extensions/omnirush/voice/keys.ts +435 -0
  40. package/assets/extensions/omnirush/voice/kitty.ts +64 -0
  41. package/assets/extensions/omnirush/voice/pvrecorder-worker.cjs +43 -0
  42. package/assets/extensions/omnirush/voice/settings.ts +67 -0
  43. package/assets/extensions/omnirush/voice.ts +838 -0
  44. package/assets/extensions/omnirush/yolo-lib.ts +80 -0
  45. package/assets/extensions/omnirush/yolo.ts +85 -0
  46. package/package.json +7 -3
  47. package/scripts/brand-engine.js +526 -0
  48. package/scripts/build-all-packages.py +29 -1
  49. package/scripts/smoke-packages.py +33 -1
  50. package/src/bin.js +232 -33
  51. package/src/compat.js +272 -0
  52. package/src/lib.js +64 -0
  53. package/src/sessions.js +222 -0
  54. package/scripts/patch-pi-branding.js +0 -251
@@ -4,7 +4,11 @@
4
4
  // the SAME workspace:
5
5
  // - role presets: code-searcher (read-only exploration),
6
6
  // researcher-web (web_fetch research), general-worker (full tools)
7
- // - parallel execution, every task at once unless max_parallel throttles
7
+ // - parallel execution, every task at once unless max_parallel throttles;
8
+ // no count cap, but memory-aware admission (memory-lib.ts): a child
9
+ // starts while the machine keeps enough RAM free and the agents' process
10
+ // tree stays under its budget, the rest wait ("queued: waiting for
11
+ // memory") and start as memory frees up
8
12
  // - no wall-clock limit: a child runs until it is done. Only an explicit
9
13
  // timeout_minutes, the inactivity watchdog (no output at all for
10
14
  // OMNIRUSH_AGENT_INACTIVITY_MINUTES, default 30, longer while one of
@@ -20,7 +24,8 @@
20
24
  // running children
21
25
  //
22
26
  // Tracing is free: every child writes its own session JSONL under
23
- // <agentDir>/sessions/<cwd-slug>/ (children run without --no-session)
27
+ // <session dir>/subagents/ (children run without --no-session; that
28
+ // folder keeps them out of /resume and --continue)
24
29
  // under a session id the parent picks, and the parent session's
25
30
  // collector records each child's conversation as a sub-agent of the
26
31
  // turn (session.child events with parent and depth, as the desktop
@@ -29,6 +34,11 @@
29
34
  // stopped and recorded as interrupted. Children get
30
35
  // OMNIRUSH_PARENT_SESSION, so a child process uploads nothing of its own.
31
36
  //
37
+ // Nesting: a sub-agent may start sub-agents of its own, up to
38
+ // MAX_SUBAGENT_DEPTH (3) layers below the main session (the desktop's
39
+ // OMNIRUSH_SUBAGENT_DEPTH). Each child gets OMNIRUSH_AGENT_DEPTH (its layer)
40
+ // and a note saying it; layer 3 has no spawn_agents.
41
+ //
32
42
  // `/agents` lists this session's subagents (running + finished);
33
43
  // `/agents cancel <id|all>` stops running ones.
34
44
 
@@ -36,6 +46,8 @@ import { StringEnum } from "@earendil-works/pi-ai";
36
46
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
37
47
  import { Type } from "typebox";
38
48
 
49
+ import { deliveryHub } from "./deliveries";
50
+ import { subagentTasks } from "./subagents";
39
51
  import {
40
52
  AGENT_ROLES,
41
53
  AGENTS_HOOK,
@@ -44,15 +56,25 @@ import {
44
56
  type AgentRunner,
45
57
  type AgentsHook,
46
58
  type AgentRecord,
59
+ agentDepth,
60
+ canDelegate,
47
61
  childInactivityMs,
48
62
  renderAgentStatus,
63
+ subagentLayerNote,
49
64
  renderChildResults,
50
65
  renderDelivery,
51
- modelSelectionFromContext,
52
66
  runChildAgent,
67
+ childModelFallback,
68
+ subagentSessionDir,
53
69
  type ChildResult,
54
70
  type ChildTask,
55
71
  } from "./agents-lib";
72
+ import { SWARM_GUIDELINE } from "./swarm-lib";
73
+ import { omniDir } from "./auth";
74
+ import { MemoryAdmission, readMemorySettings } from "./memory-lib";
75
+
76
+ /** Blocking spawn_agents: progress updates at most this often (ms). */
77
+ const PROGRESS_UPDATE_MS = 500;
56
78
 
57
79
  /** A timeout_minutes below this is taken for a placeholder, not a limit. */
58
80
  const MIN_TIMEOUT_MINUTES = 1;
@@ -68,7 +90,7 @@ const SpawnAgentsParams = Type.Object({
68
90
  }),
69
91
  task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
70
92
  model: Type.Optional(Type.String({
71
- description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "gpt-6-sol:high"). Omit (or leave empty) to inherit the parent session\'s current model and effort',
93
+ description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "meta-muse-spark", "muse-spark-1.1:low", "gpt-6-astra", "gpt-6-sol:high", "gpt-5.6-sol"). Omit (or leave empty) for the sub-agent model the user picked with /subagents, else the parent session\'s current model and effort',
72
94
  })),
73
95
  }),
74
96
  { description: "Tasks to delegate; they all run in parallel", minItems: 1 },
@@ -106,21 +128,28 @@ function renderSome(records: AgentRecord[], unknown: string[] = []): string {
106
128
  return parts.join("\n\n") || "No matching sub-agents.";
107
129
  }
108
130
 
109
- export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {}) {
131
+ export default function (pi: ExtensionAPI, options: { run?: AgentRunner; admission?: MemoryAdmission | null; env?: NodeJS.ProcessEnv } = {}) {
132
+ // Sub-agents nest up to MAX_SUBAGENT_DEPTH layers below the main session,
133
+ // as in the desktop app: every sub-agent is told its layer, and the last
134
+ // layer gets no spawn_agents (nor the tools that manage sub-agents).
135
+ const depth = agentDepth(options.env ?? process.env);
136
+ if (depth > 0) {
137
+ pi.on("before_agent_start", async (event: any) => {
138
+ const base = typeof event?.systemPrompt === "string" ? event.systemPrompt : "";
139
+ return { systemPrompt: `${base}\n\n${subagentLayerNote(depth)}` };
140
+ });
141
+ }
142
+ if (!canDelegate(depth)) return;
143
+
110
144
  const watchdogMinutes = Math.round(childInactivityMs() / 60_000);
111
- /** The session this runtime is on (deliveries go to it). */
112
- let currentSession = "";
113
- let cwd = process.cwd();
114
- let inheritedModel = "";
145
+ const hub = deliveryHub(pi);
146
+ /** The last context seen (the TUI status bar for running agents). */
115
147
  let lastContext: any = null;
116
148
  let statusTimer: ReturnType<typeof setTimeout> | null = null;
117
- /** An agent run is under way (one-shot runs deliver through the settle loop otherwise). */
118
- let busy = false;
119
- let oneShot = false;
120
149
 
121
150
  function publishStatus(): void {
122
151
  statusTimer = null;
123
- const session = currentSession;
152
+ const session = hub.currentSession;
124
153
  if (!session || !lastContext?.ui?.setStatus) return;
125
154
  const records = manager.list(session);
126
155
  if (records.length > 0) lastContext.ui.setStatus("omnirush-agents", agentStatusSummary(records));
@@ -132,40 +161,60 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
132
161
  statusTimer.unref?.();
133
162
  }
134
163
 
135
- const send = (records: AgentRecord[]) => {
136
- pi.sendMessage(
137
- {
138
- customType: DELIVERY_TYPE,
139
- content: renderDelivery(records),
140
- display: true,
141
- details: { results: records.map(resultOf) },
142
- },
143
- // Busy: into the running turn at its next step. Idle: a new turn.
144
- { triggerTurn: true, deliverAs: "steer" },
145
- );
164
+ // Memory-aware admission of children; thresholds from the environment or
165
+ // settings.json "subagentMemory" (memory-lib.ts). Built on first use.
166
+ let admission: MemoryAdmission | null = null;
167
+ const admissionOf = (): MemoryAdmission => {
168
+ admission ??= options.admission ?? new MemoryAdmission({ settings: readMemorySettings(omniDir()) });
169
+ return admission;
146
170
  };
147
-
148
- const deliver = () => {
149
- if (!currentSession) return;
150
- // One-shot and between runs: the agent_settled loop delivers.
151
- if (oneShot && !busy) {
152
- return;
153
- }
154
- for (const group of manager.takeDeliverable(currentSession)) send(group.records);
171
+ const lazyAdmission = {
172
+ check: () => admissionOf().check(),
173
+ started: (key: object, pid?: number | null) => admissionOf().started(key, pid ?? null),
174
+ spawned: (key: object, pid: number | null | undefined) => admissionOf().spawned(key, pid),
175
+ finished: (key: object) => admissionOf().finished(key),
155
176
  };
156
177
 
178
+ /** Tell the user when children wait for memory (a warning when it is critically low). */
179
+ function memoryWait(info: { critical: boolean; detail: string; queued: number } | null): void {
180
+ scheduleStatus();
181
+ if (!info) return;
182
+ const text = info.critical
183
+ ? `omnirush: ${info.detail}. Not starting more sub-agents until memory frees up (${info.queued} queued); running ones continue.`
184
+ : `omnirush: ${info.queued} sub-agent${info.queued === 1 ? "" : "s"} queued: waiting for memory (${info.detail}).`;
185
+ if (lastContext?.hasUI && lastContext?.ui?.notify) lastContext.ui.notify(text, info.critical ? "warning" : "info");
186
+ // Headless: the top agent says it on stderr (a sub-agent's stderr only reaches its parent).
187
+ else if (info.critical || !String(process.env.OMNIRUSH_PARENT_SESSION ?? "").trim()) process.stderr.write(`${text}\n`);
188
+ }
189
+
157
190
  const manager = new AgentManager({
158
191
  run: options.run ?? ((task, options) => runChildAgent(task, {
159
- cwd: options.cwd ?? cwd,
192
+ cwd: options.cwd ?? hub.cwd,
160
193
  parentSessionId: options.parentSessionId,
161
194
  sessionId: options.sessionId,
195
+ ...(options.sessionDir ? { sessionDir: options.sessionDir } : {}),
162
196
  signal: options.signal,
163
197
  onActivity: options.onActivity,
198
+ ...(options.onSpawn ? { onSpawn: options.onSpawn } : {}),
199
+ extraEnv: admissionOf().childEnv(),
164
200
  ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
165
201
  })),
166
202
  // After the settle's own bookkeeping (waiters took theirs).
167
- onDeliverable: () => queueMicrotask(deliver),
203
+ onDeliverable: () => hub.notify(),
168
204
  onChanged: scheduleStatus,
205
+ admission: options.admission === null ? null : lazyAdmission,
206
+ onMemoryWait: memoryWait,
207
+ });
208
+
209
+ hub.addSource({
210
+ hasPending: (session) => manager.hasPending(session),
211
+ // One message per finished batch (or per child with notify "each").
212
+ take: (session) => manager.takeDeliverable(session).map(({ records }) => ({
213
+ customType: DELIVERY_TYPE,
214
+ content: renderDelivery(records),
215
+ details: { results: records.map(resultOf) },
216
+ })),
217
+ changed: () => manager.changed(),
169
218
  });
170
219
 
171
220
  const hook: AgentsHook = {
@@ -177,17 +226,8 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
177
226
  (globalThis as any)[AGENTS_HOOK] = hook;
178
227
 
179
228
  const sessionOf = (ctx: any): string => {
180
- const id = String(ctx?.sessionManager?.getSessionId?.() ?? "");
181
- if (id && id !== currentSession) {
182
- currentSession = id;
183
- inheritedModel = "";
184
- }
185
229
  lastContext = ctx ?? lastContext;
186
- const selected = modelSelectionFromContext(ctx);
187
- if (selected) inheritedModel = selected;
188
- if (ctx?.cwd) cwd = String(ctx.cwd);
189
- if (ctx?.mode === "print" || ctx?.mode === "json") oneShot = true;
190
- return id;
230
+ return hub.sessionOf(ctx);
191
231
  };
192
232
 
193
233
  pi.registerTool({
@@ -212,6 +252,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
212
252
  "Short fan-outs whose results you need right away (quick searches, lookups): the default blocking call.",
213
253
  "Long work (implementation chunks, builds, test suites, long research — anything that may take many minutes) while you have other things to do: spawn_agents with wait:false, then keep working; the results arrive on their own as a message, so do not poll agents_status in a loop. Call agents_wait only when you have nothing else to do and need the results before going on.",
214
254
  "Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
255
+ SWARM_GUIDELINE,
215
256
  ],
216
257
  parameters: SpawnAgentsParams,
217
258
 
@@ -222,16 +263,22 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
222
263
  if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
223
264
  throw new Error("invalid tasks: every entry needs a non-empty task string");
224
265
  }
225
- // An omitted model inherits the live parent selection. Explicit task
226
- // models remain available for deliberate mixed-model swarms.
266
+ // A task's own model[:effort] wins; an omitted (or empty) one takes
267
+ // the /subagents pick, else the parent session's current model and
268
+ // effort (resolved below by subagentTasks).
227
269
  const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
228
270
  tasks.push({
229
271
  role: raw.role,
230
272
  task: raw.task.trim(),
231
- ...((requestedModel || inheritedModel) ? { model: requestedModel || inheritedModel } : {}),
273
+ ...(requestedModel ? { model: requestedModel } : {}),
232
274
  });
233
275
  }
234
276
  if (tasks.length === 0) throw new Error("no tasks given");
277
+ // The model and effort each child runs on (subagents.ts): the task's
278
+ // own model[:effort], else the /subagents pick, else the parent
279
+ // session's current model and effort; the main model when a pick is
280
+ // unavailable.
281
+ const resolvedTasks = subagentTasks(pi, ctx, tasks);
235
282
  if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
236
283
  const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
237
284
  // Only a deliberate limit counts: models fill optional numbers with
@@ -246,8 +293,10 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
246
293
  ? Math.floor(params.max_parallel)
247
294
  : undefined;
248
295
  const background = params.wait === false;
296
+ let progressTimer: ReturnType<typeof setTimeout> | null = null;
297
+ let finished = false;
249
298
 
250
- const dispatched = manager.dispatch(parentSessionId, tasks, {
299
+ const dispatched = manager.dispatch(parentSessionId, resolvedTasks, {
251
300
  background,
252
301
  concurrency,
253
302
  notify: params.notify === "each" ? "each" : "batch",
@@ -260,18 +309,31 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
260
309
  });
261
310
  },
262
311
  onChanged: () => {
263
- if (background) return;
264
- onUpdate?.({
265
- content: [{ type: "text", text: agentStatusSummary(manager.list(parentSessionId)) }],
266
- details: { results: manager.list(parentSessionId).filter((candidate) => candidate.result).map(resultOf) },
267
- });
312
+ // Every sign of life of every child lands here: the progress update
313
+ // (which carries the finished results) goes out at most every
314
+ // PROGRESS_UPDATE_MS, not once per output chunk.
315
+ if (background || progressTimer) return;
316
+ progressTimer = setTimeout(() => {
317
+ progressTimer = null;
318
+ if (finished) return;
319
+ onUpdate?.({
320
+ content: [{ type: "text", text: agentStatusSummary(manager.list(parentSessionId)) }],
321
+ details: { results: manager.list(parentSessionId).filter((candidate) => candidate.result).map(resultOf) },
322
+ });
323
+ }, PROGRESS_UPDATE_MS);
324
+ progressTimer.unref?.();
268
325
  },
269
326
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
270
327
  cwd: workdir,
328
+ // Children's sessions stay out of the user's /resume list.
329
+ sessionDir: subagentSessionDir(ctx?.sessionManager?.getSessionDir?.()),
271
330
  });
272
331
 
273
332
  if (background) {
274
- const lines = dispatched.records.map((record) => `- ${record.id}: ${record.role}${record.model ? ` [${record.model}]` : ""} — ${record.task.length > 100 ? `${record.task.slice(0, 99)}…` : record.task}`);
333
+ const lines = dispatched.records.map((record, index) => {
334
+ const note = childModelFallback(resolvedTasks[index])?.note;
335
+ return `- ${record.id}: ${record.role}${record.model ? ` [${record.model}${record.effort ? ` · ${record.effort}` : ""}]` : ""} — ${record.task.length > 100 ? `${record.task.slice(0, 99)}…` : record.task}${note ? ` (${note})` : ""}`;
336
+ });
275
337
  return {
276
338
  content: [{
277
339
  type: "text",
@@ -288,20 +350,27 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
288
350
  details: {
289
351
  background: true,
290
352
  batch: dispatched.batch,
291
- results: dispatched.records.map((record) => ({
292
- agent_id: record.id,
293
- role: record.role,
294
- task: record.task,
295
- session_id: record.sessionId,
296
- ...(record.model ? { model: record.model } : {}),
297
- parallel_limit: record.parallelLimit,
298
- status: "running",
299
- })),
353
+ results: dispatched.records.map((record, index) => {
354
+ const modelFallback = childModelFallback(resolvedTasks[index]);
355
+ return {
356
+ agent_id: record.id,
357
+ role: record.role,
358
+ task: record.task,
359
+ session_id: record.sessionId,
360
+ ...(record.model ? { model: record.model } : {}),
361
+ ...(record.effort ? { effort: record.effort } : {}),
362
+ ...(modelFallback ? { model_fallback: modelFallback } : {}),
363
+ parallel_limit: record.parallelLimit,
364
+ status: "running",
365
+ };
366
+ }),
300
367
  },
301
368
  };
302
369
  }
303
370
 
304
371
  await dispatched.done;
372
+ finished = true;
373
+ if (progressTimer) clearTimeout(progressTimer);
305
374
  const records = dispatched.records;
306
375
  return {
307
376
  content: [{ type: "text", text: renderChildResults(records.map((record) => record.result as ChildResult), records.map((record) => record.id)) }],
@@ -404,55 +473,6 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
404
473
  },
405
474
  });
406
475
 
407
- pi.on("session_start", (_event: any, ctx: any) => {
408
- sessionOf(ctx);
409
- });
410
-
411
- pi.on("agent_start", (_event: any, ctx: any) => {
412
- sessionOf(ctx);
413
- busy = true;
414
- });
415
-
416
- /**
417
- * One-shot runs (print/json mode) end when the agent settles: background
418
- * children still out would be lost. The run waits for them here and
419
- * continues with their results (a new run nested in this settle, whose
420
- * own settle resumes this one when everything is done).
421
- */
422
- let resumeOuter: (() => void) | null = null;
423
- pi.on("agent_settled", async (_event: any, ctx: any) => {
424
- busy = false;
425
- const session = sessionOf(ctx);
426
- if (!oneShot) {
427
- // Anything that finished right as the run ended.
428
- deliver();
429
- return;
430
- }
431
- const outer = resumeOuter;
432
- resumeOuter = null;
433
- try {
434
- for (;;) {
435
- if (typeof ctx?.isIdle === "function" && !ctx.isIdle()) {
436
- // A run started (a delivery): its settle resumes this one.
437
- await new Promise<void>((resolve) => { resumeOuter = resolve; });
438
- break;
439
- }
440
- if (!session || !manager.hasPending(session)) break;
441
- const groups = manager.takeDeliverable(session);
442
- if (groups.length === 0) {
443
- await manager.changed();
444
- continue;
445
- }
446
- busy = true;
447
- const records = groups.flatMap((group) => group.records);
448
- send(records);
449
- if (typeof ctx?.isIdle === "function" && ctx.isIdle()) busy = false;
450
- }
451
- } finally {
452
- outer?.();
453
- }
454
- });
455
-
456
476
  pi.on("session_shutdown", async () => {
457
477
  // Children the session ends under are stopped (the collector, when it
458
478
  // runs, already did this before its last capture).
@@ -460,7 +480,7 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
460
480
  });
461
481
 
462
482
  pi.registerCommand("agents", {
463
- description: "List this session's subagents (running and finished); /agents cancel <id|all> stops running ones",
483
+ description: "List this session's subagents (running and finished) and the memory they may use; /agents cancel <id|all> stops running ones; /agents memory shows the memory admission",
464
484
  handler: async (args: string, ctx: any) => {
465
485
  const session = sessionOf(ctx);
466
486
  const words = String(args ?? "").trim().split(/\s+/).filter(Boolean);
@@ -472,11 +492,16 @@ export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {})
472
492
  return;
473
493
  }
474
494
  const records = manager.list(session);
495
+ const memory = options.admission === null ? "" : `\n${admissionOf().describe()}`;
496
+ if (words[0] === "memory") {
497
+ ctx?.ui?.notify?.(memory.trim() || "Memory admission is off.");
498
+ return;
499
+ }
475
500
  if (records.length === 0) {
476
- ctx?.ui?.notify?.("No subagents have been spawned in this session yet.");
501
+ ctx?.ui?.notify?.(`No subagents have been spawned in this session yet.${memory}`);
477
502
  return;
478
503
  }
479
- ctx?.ui?.notify?.(renderAgentStatus(records));
504
+ ctx?.ui?.notify?.(`${renderAgentStatus(records)}${memory}`);
480
505
  },
481
506
  });
482
507
  }