jeopi-cli 16.2.24 → 16.2.26

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.
@@ -13,7 +13,7 @@ Look where the agent is NOT — bring the angle they skipped, NEVER re-run reaso
13
13
  Offer that view before they sink work into the wrong direction.
14
14
 
15
15
  <workflow>
16
- You receive the agent's transcript incrementally, including their thoughts.
16
+ You receive the agent's transcript incrementally — their messages, tool calls, and results.
17
17
  Use the tools this session grants you to verify suspicions — by default read-only lookup (`read`, `grep`, `glob`); operators may extend the grant via `WATCHDOG.yml`. Advising is your primary channel; touch mutating tools (when granted) only when a verify step genuinely needs them.
18
18
  Keep exploration lean:
19
19
  - 2–3 tool calls per advise.
@@ -1,7 +1,6 @@
1
1
  <system-notice type="interrupted-thinking">
2
- Your previous turn was interrupted while you were thinking.
3
- - You MUST treat the preserved reasoning as internal continuity context.
4
- - You MUST continue the user's task from the relevant unfinished point.
2
+ Your previous turn was cut off before you finished. Below is your unfinished internal work state — use it silently to resume; do not quote, repeat, or restate it back.
3
+ - Continue the user's task from the relevant unfinished point.
5
4
  ------
6
5
  {{reasoning}}
7
6
  </system-notice>
@@ -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
 
@@ -1593,6 +1593,14 @@ export class AgentSession {
1593
1593
  #retryAttempt = 0;
1594
1594
  /** Rung-4 classifier-refusal resend streak (post fallback-ladder). */
1595
1595
  #refusalBackoff: { startedAt: number; attempt: number } | undefined = undefined;
1596
+ /**
1597
+ * One-shot latch: `reasoning_extraction` classifier refusals are payload-shape-bound
1598
+ * (see advisor/runtime.ts `#includeThinking` degrade) — resending the identical batch
1599
+ * predictably re-refuses. Set the first time `#handleRetryableError` strips unsigned
1600
+ * thinking/redactedThinking from active context and resends on the same model; never
1601
+ * reset, mirroring the advisor's permanent-for-the-session degrade.
1602
+ */
1603
+ #reasoningExtractionDegraded = false;
1596
1604
  #retryPromise: Promise<void> | undefined = undefined;
1597
1605
  #retryResolve: (() => void) | undefined = undefined;
1598
1606
  #activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
@@ -12681,6 +12689,36 @@ export class AgentSession {
12681
12689
  const stopType = message.stopDetails?.type;
12682
12690
  return stopType === "refusal" || stopType === "sensitive";
12683
12691
  }
12692
+ /**
12693
+ * Strip unsigned `thinking` and every `redactedThinking` block from assistant
12694
+ * messages. Used once as a `reasoning_extraction` classifier-refusal degrade:
12695
+ * unlike {@link renderDemotedThinking}'s plain-text fallback (still a plaintext
12696
+ * reasoning replay for dialects outside the Fable/Mythos carve-out), dropping the
12697
+ * block outright removes the trigger instead of reformatting it. Signed thinking
12698
+ * is left untouched — it replays natively via the provider's structured thinking
12699
+ * block and is never demoted to plaintext.
12700
+ * @returns a rewritten array, or `undefined` if no message needed stripping.
12701
+ */
12702
+ #stripUnsignedThinkingForRefusalDegrade(messages: readonly AgentMessage[]): AgentMessage[] | undefined {
12703
+ let changed = false;
12704
+ const next: AgentMessage[] = [];
12705
+ for (const message of messages) {
12706
+ if (message.role !== "assistant") {
12707
+ next.push(message);
12708
+ continue;
12709
+ }
12710
+ const content = message.content.filter(
12711
+ block => block.type !== "redactedThinking" && (block.type !== "thinking" || !!block.thinkingSignature),
12712
+ );
12713
+ if (content.length === message.content.length) {
12714
+ next.push(message);
12715
+ continue;
12716
+ }
12717
+ changed = true;
12718
+ if (content.length > 0) next.push({ ...message, content });
12719
+ }
12720
+ return changed ? next : undefined;
12721
+ }
12684
12722
 
12685
12723
  #getRetryFallbackChains(): RetryFallbackChains {
12686
12724
  const configuredChains = this.settings.get("retry.fallbackChains");
@@ -13099,6 +13137,33 @@ export class AgentSession {
13099
13137
  }
13100
13138
 
13101
13139
  const errorMessage = message.errorMessage || "Unknown error";
13140
+ // `reasoning_extraction` is payload-shape-bound (see advisor/runtime.ts): resending
13141
+ // the identical batch predictably re-refuses, unlike probabilistic classifier flags
13142
+ // where backoff/fallback genuinely help. Degrade once — strip unsigned
13143
+ // thinking/redactedThinking from active context and resend immediately on the same
13144
+ // model — before falling through to the generic fallback-pin / backoff ladder below,
13145
+ // which would otherwise retry the unchanged, re-refusing payload.
13146
+ if (
13147
+ classifierRefusal &&
13148
+ message.stopDetails?.category === "reasoning_extraction" &&
13149
+ !this.#reasoningExtractionDegraded
13150
+ ) {
13151
+ this.#reasoningExtractionDegraded = true;
13152
+ const stripped = this.#stripUnsignedThinkingForRefusalDegrade(this.agent.state.messages);
13153
+ if (stripped) this.agent.replaceMessages(stripped);
13154
+ this.#removeAssistantMessageFromActiveContext(message, "reasoning-extraction-degrade");
13155
+ await this.#emitSessionEvent({
13156
+ type: "auto_retry_start",
13157
+ attempt: this.#retryAttempt,
13158
+ maxAttempts: retrySettings.maxRetries,
13159
+ delayMs: 0,
13160
+ errorMessage,
13161
+ errorId: message.errorId,
13162
+ });
13163
+ this.#scheduleAgentContinue({ delayMs: 1, generation });
13164
+ return true;
13165
+ }
13166
+
13102
13167
  const id = this.#classifyRetryMessage(message);
13103
13168
  const staleOpenAIResponsesReplayError = AIError.is(id, AIError.Flag.StaleResponsesItem);
13104
13169
  const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
@@ -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}).`,