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.
- package/CHANGELOG.md +8 -0
- package/dist/cli.js +2637 -2637
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/package.json +12 -12
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/prompts/tools/subagent.md +1 -1
- package/src/session/session-context.ts +8 -2
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/slash-commands/types.ts +2 -0
- package/src/tools/subagent.test.ts +20 -0
- package/src/tools/subagent.ts +11 -2
|
@@ -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).
|
|
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");
|
package/src/tools/subagent.ts
CHANGED
|
@@ -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
|
-
|
|
545
|
+
body,
|
|
537
546
|
"resume",
|
|
538
547
|
verbosity,
|
|
539
548
|
outcome => `Resumed ${id} (${outcome}).`,
|