jeopi-cli 16.2.24 → 16.2.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.
@@ -8,7 +8,7 @@ Controls the background subagents you spawned with `task`. Results deliver thems
8
8
  - **`cancel`**: Stop subagents that have hung, stalled, or are no longer needed. Requires `id`/`ids`. Works whether the subagent is still on its first run or was later resumed.
9
9
  - **`steer`**: Send a message to a specific subagent (`id` + `message`). Delivered as steering to a running subagent, or wakes an idle/parked one with a real turn. Add `pause: true` to also ask it to stop at its next safe boundary after handling the message.
10
10
  - **`pause`**: Ask a running subagent to stop at its next safe boundary and go idle. Non-running subagents are a no-op — nothing to pause.
11
- - **`resume`**: Wake a subagent with a follow-up. A `running` or `queued` subagent is a no-op (nothing to resume). Any other subagent needs `message` — it has no pending work to continue on its own, but its session and full context are still alive, so a follow-up message continues it exactly where it left off.
11
+ - **`resume`**: Wake a subagent with a follow-up. A `running` or `queued` subagent is a no-op (nothing to resume). A `paused` subagent (you asked it to stop) resumes even without `message` — it continues with a default "keep going" nudge, since you already know why it stopped. Any other subagent (`completed`/`failed`/`cancelled` on its own) needs an explicit `message` — it has no known pending work, but its session and full context are still alive, so a follow-up message continues it exactly where it left off.
12
12
 
13
13
  # Filtering Output
14
14
 
@@ -138,10 +138,16 @@ export function buildSessionContext(
138
138
  };
139
139
  }
140
140
 
141
- // Walk from leaf to root, collecting path
141
+ // Walk from leaf to root, collecting path. `seen` guards against a corrupt/
142
+ // cyclic parentId chain (e.g. A.parentId === B, B.parentId === A) looping
143
+ // forever and growing `path` unbounded — the same OOM class the sibling
144
+ // walks in session-manager.ts (`pathTo`) and session-loader.ts
145
+ // (`collectActiveBranchIds`) already guard against.
142
146
  const path: SessionEntry[] = [];
147
+ const seen = new Set<string>();
143
148
  let current: SessionEntry | undefined = leaf;
144
- while (current) {
149
+ while (current && !seen.has(current.id)) {
150
+ seen.add(current.id);
145
151
  path.push(current);
146
152
  current = current.parentId ? byId.get(current.parentId) : undefined;
147
153
  }
@@ -211,6 +211,7 @@ function parseShakeMode(args: string): ShakeMode | { error: string } {
211
211
  const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
212
212
  {
213
213
  name: "settings",
214
+ priority: 40,
214
215
  description: "Open settings menu",
215
216
  handleTui: (_command, runtime) => {
216
217
  runtime.ctx.showSettingsSelector();
@@ -265,6 +266,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
265
266
  },
266
267
  {
267
268
  name: "goal",
269
+ priority: 84,
268
270
  description: "Toggle goal mode (persistent autonomous objective for this session)",
269
271
  subcommands: [
270
272
  { name: "set", description: "Set or replace the goal", usage: "<objective>" },
@@ -558,6 +560,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
558
560
  },
559
561
  {
560
562
  name: "export",
563
+ priority: 50,
561
564
  description: "Export session to HTML file",
562
565
  inlineHint: "[path]",
563
566
  allowArgs: true,
@@ -585,6 +588,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
585
588
  },
586
589
  {
587
590
  name: "dump",
591
+ priority: 54,
588
592
  description: "Copy session transcript to clipboard (and write LLM request JSON to tmp)",
589
593
  acpDescription: "Return full transcript as plain text, with LLM request JSON path",
590
594
  allowArgs: true,
@@ -854,6 +858,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
854
858
  },
855
859
  {
856
860
  name: "copy",
861
+ priority: 55,
857
862
  description: "Pick text or code from the conversation to copy",
858
863
  allowArgs: true,
859
864
  handleTui: async (command, runtime) => {
@@ -928,6 +933,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
928
933
  },
929
934
  {
930
935
  name: "session",
936
+ priority: 88,
931
937
  description: "Session management commands",
932
938
  acpDescription: "Show session information",
933
939
  acpInputHint: "info|delete",
@@ -1344,6 +1350,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1344
1350
  },
1345
1351
  {
1346
1352
  name: "new",
1353
+ priority: 96,
1347
1354
  description: "Start a new session",
1348
1355
  handleTui: async (_command, runtime) => {
1349
1356
  runtime.ctx.editor.setText("");
@@ -1381,6 +1388,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1381
1388
  },
1382
1389
  {
1383
1390
  name: "compact",
1391
+ priority: 72,
1384
1392
  description: "Manually compact the session context",
1385
1393
  acpDescription: "Compact the conversation",
1386
1394
  subcommands: COMPACT_MODES.map(mode => ({
@@ -1467,6 +1475,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1467
1475
  },
1468
1476
  {
1469
1477
  name: "resume",
1478
+ priority: 92,
1470
1479
  description: "Resume a different session",
1471
1480
  inlineHint: "[session id]",
1472
1481
  allowArgs: true,
@@ -1525,6 +1534,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1525
1534
  },
1526
1535
  {
1527
1536
  name: "retry",
1537
+ priority: 70,
1528
1538
  description: "Retry the last failed agent turn",
1529
1539
  handleTui: async (_command, runtime) => {
1530
1540
  const didRetry = await runtime.ctx.session.retry();
@@ -16,6 +16,8 @@ export interface BuiltinSlashCommand {
16
16
  name: string;
17
17
  aliases?: string[];
18
18
  description: string;
19
+ /** Ranking boost for the empty-prefix `/` menu and tied fuzzy matches (higher shows first); flows straight through to `jeopi-tui`'s `SlashCommand.priority`. Commands without one default to 0. */
20
+ priority?: number;
19
21
  /** Subcommands for dropdown completion (e.g. /mcp add, /mcp list). */
20
22
  subcommands?: SubcommandDef[];
21
23
  /** Static inline hint when command takes a simple argument (no subcommands). */
@@ -186,6 +186,26 @@ describe("SubagentTool", () => {
186
186
  const afterResume = await tool.execute("c4", { action: "list" });
187
187
  expect(afterResume.details?.subagents[0].status).not.toBe("paused");
188
188
  });
189
+ it("resume without a message succeeds on a paused subagent (GJC parity) but still requires one once terminal", async () => {
190
+ const { resolve } = registerPendingTaskJob(manager, "Main", "Sub1");
191
+ const fakeSession = { deliverIrcMessage: async () => "injected" as const } as unknown as AgentSession;
192
+ registry.register({ id: "Sub1", displayName: "Sub1", kind: "sub", session: fakeSession, status: "running" });
193
+ const tool = new SubagentTool(makeSession({ agentId: "Main", registry, manager }));
194
+
195
+ await tool.execute("c1", { action: "pause", id: "Sub1" });
196
+ resolve("stopped early");
197
+ await manager.getJob("Sub1")?.promise;
198
+ registry.setStatus("Sub1", "idle");
199
+
200
+ // No `message` — a paused subagent has known intent (we asked it to stop),
201
+ // so resume proceeds with a default continuation body instead of throwing.
202
+ const resumed = await tool.execute("c2", { action: "resume", id: "Sub1" });
203
+ expect(resumed.isError).toBeFalsy();
204
+ expect(resumed.details?.receipt?.outcome).toBe("injected");
205
+
206
+ // Once genuinely terminal again (no outstanding pause), resume still needs a message.
207
+ await expect(tool.execute("c3", { action: "resume", id: "Sub1" })).rejects.toThrow(ToolError);
208
+ });
189
209
 
190
210
  it("a subagent that goes idle while running is never mistaken for paused", async () => {
191
211
  const { resolve } = registerPendingTaskJob(manager, "Main", "Sub1");
@@ -76,6 +76,7 @@ const PREVIEW_CHARS = 2_000;
76
76
  const FULL_PREVIEW_CHARS = 12_000;
77
77
  const DEFAULT_PAUSE_DIRECTIVE =
78
78
  "Stop at the next safe boundary and yield. You can be resumed later with more instructions.";
79
+ const DEFAULT_RESUME_MESSAGE = "Continue from where you left off.";
79
80
 
80
81
  export type SubagentStatus = "queued" | "running" | "paused" | "completed" | "failed" | "cancelled" | "not_found";
81
82
 
@@ -522,18 +523,26 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
522
523
  };
523
524
  }
524
525
 
526
+ // GJC parity: a `paused` subagent has saved context it stopped *because
527
+ // we asked it to* — resume can proceed with a default continuation body,
528
+ // the same way GJC's native `resumeSubagent` continues a non-terminal
529
+ // record without requiring a message. A subagent that simply finished on
530
+ // its own (completed/failed/cancelled) has no such known intent, so that
531
+ // case still requires an explicit `message`.
532
+ const isPaused = this.#pausedIds.has(id);
525
533
  const message = params.message?.trim();
526
- if (!message) {
534
+ if (!message && !isPaused) {
527
535
  throw new ToolError(
528
536
  `\`message\` is required to resume "${id}" — it has no pending work to continue on its own.`,
529
537
  );
530
538
  }
539
+ const body = message || DEFAULT_RESUME_MESSAGE;
531
540
  return this.#deliver(
532
541
  manager,
533
542
  registry,
534
543
  senderId,
535
544
  id,
536
- message,
545
+ body,
537
546
  "resume",
538
547
  verbosity,
539
548
  outcome => `Resumed ${id} (${outcome}).`,