opencode-courier 0.15.0 → 0.16.0
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/README.md +1 -1
- package/dist/person/text.d.ts +1 -1
- package/dist/person/text.js +1 -1
- package/dist/person/tools.js +3 -2
- package/dist/scheduling/later.js +1 -1
- package/dist/scheduling/text.js +2 -2
- package/dist/scheduling/tools.js +3 -3
- package/dist/skill.js +7 -6
- package/dist/tree/events.js +24 -5
- package/dist/tree/limits.js +13 -6
- package/dist/tree/nudge.d.ts +2 -0
- package/dist/tree/report.d.ts +3 -1
- package/dist/tree/report.js +5 -2
- package/dist/tree/stop.js +18 -6
- package/dist/tree/text.d.ts +2 -0
- package/dist/tree/text.js +34 -13
- package/dist/tree/tools.js +8 -7
- package/dist/tui.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ name, without a version: OpenCode offers updates only for an entry without an ex
|
|
|
85
85
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
86
86
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
87
87
|
|---|---|
|
|
88
|
-
| 0.14.0, 0.15.0 | 2.2.6 |
|
|
88
|
+
| 0.14.0, 0.15.0, 0.16.0 | 2.2.6 |
|
|
89
89
|
| 0.13.7, 0.13.8 | 2.2.5 |
|
|
90
90
|
| 0.13.6 | 2.2.4 |
|
|
91
91
|
<!-- compat-table:end -->
|
package/dist/person/text.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Asked, Elsewhere, Form, Outcome, PermissionAnswered, PermissionRequest, Prompt, QuestionAnswered, Reply, Withdrawal } from "./ports.js";
|
|
2
2
|
export type AnswerResult = PermissionAnswered | QuestionAnswered;
|
|
3
3
|
export declare function answerText(result: AnswerResult): string;
|
|
4
|
-
export declare const REJECTED = "Refused by the session
|
|
4
|
+
export declare const REJECTED = "Refused by the person or the top session of the tree. Do without it, or report back why you cannot.";
|
|
5
5
|
export declare function refusedText(sessionID: string, requestID: string, reason: string): string;
|
|
6
6
|
export declare const origin: (startedBy?: string) => string;
|
|
7
7
|
export declare const STAYS_QUIET = "and then it does not report back on its own: if it stays quiet, message it with courier_send to have it carry on.";
|
package/dist/person/text.js
CHANGED
|
@@ -18,7 +18,7 @@ export function answerText(result) {
|
|
|
18
18
|
return `Passed the answers to question ${result.requestID} on to ${result.sessionID}${how}; it carries on and reports back with courier_send. ${END_TURN}`;
|
|
19
19
|
}
|
|
20
20
|
// a bare rejection ends the child's turn (api-notes)
|
|
21
|
-
export const REJECTED = "Refused by the session
|
|
21
|
+
export const REJECTED = "Refused by the person or the top session of the tree. Do without it, or report back why you cannot.";
|
|
22
22
|
export function refusedText(sessionID, requestID, reason) {
|
|
23
23
|
return `not allowed in your session (${reason}). The refusal was passed on with that reason to request ${requestID} of ${sessionID}, so its call fails and it carries on; it reports back with courier_send. Tell the person, and do not propose it again unless they ask.`;
|
|
24
24
|
}
|
package/dist/person/tools.js
CHANGED
|
@@ -48,8 +48,9 @@ export function addPersonTools(tools, ports) {
|
|
|
48
48
|
"working with in your session, as its own permission prompt for what that session asked, under your permission " +
|
|
49
49
|
"rules, and the call returns once they have answered; what they allow is passed on, and a refusal with a reason " +
|
|
50
50
|
"fails this call and is passed on with the reason. Do not ask them in your reply or with your question tool first. " +
|
|
51
|
-
"For a question,
|
|
52
|
-
"
|
|
51
|
+
"For a question, the question tool call the notice asks for passes the person's answer on by itself; use answers " +
|
|
52
|
+
"only when they answered some other way, or the notice or that call's result tells you to, and never answer for " +
|
|
53
|
+
"them. The session carries on once it has the answer.",
|
|
53
54
|
input: AnswerInput,
|
|
54
55
|
execute: async (input, context) => {
|
|
55
56
|
const call = { messageID: context.messageID, callID: context.id };
|
package/dist/scheduling/later.js
CHANGED
|
@@ -76,7 +76,7 @@ export async function deliverDue(ports, claimed) {
|
|
|
76
76
|
sessionID: due.sessionID,
|
|
77
77
|
text: envelope(due.from, due.message, { scheduled: new Date(due.createdAt).toISOString() }),
|
|
78
78
|
description: `Scheduled message from ${due.from}`,
|
|
79
|
-
metadata: { source: "courier", from: due.from, scheduled: due.id },
|
|
79
|
+
metadata: { source: "courier", from: due.from, scheduled: due.id, scheduledAt: due.createdAt },
|
|
80
80
|
delivery: "queue",
|
|
81
81
|
})
|
|
82
82
|
.catch((error) => ports.log(`courier_later ${due.id} to ${due.sessionID} not delivered: ${String(error)}`));
|
package/dist/scheduling/text.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function laterText(entry, fireAt, own) {
|
|
2
2
|
const next = own
|
|
3
3
|
? "It arrives when due, after your current turn if one is running, so do not wait for it: once nothing " +
|
|
4
|
-
"else is left to do now, end your turn by replying without calling more tools.
|
|
5
|
-
"
|
|
4
|
+
"else is left to do now, end your turn by replying without calling more tools. Once the last report " +
|
|
5
|
+
"it covers arrives, cancel it with courier_cancel."
|
|
6
6
|
: "Cancel it with courier_cancel if it is no longer needed.";
|
|
7
7
|
return `Scheduled ${entry.id} for ${fireAt}, to ${entry.sessionID}. ${next}`;
|
|
8
8
|
}
|
package/dist/scheduling/tools.js
CHANGED
|
@@ -18,9 +18,9 @@ export function addSchedulingTools(tools, later) {
|
|
|
18
18
|
name: "courier_later",
|
|
19
19
|
options: { codemode: false },
|
|
20
20
|
description: "Schedule a message for a session (this one by default), delivered when due and waking it if idle. " +
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"when
|
|
21
|
+
"For a long task or batch of sessions you start, use it as a safety net: schedule one check-in per group, and " +
|
|
22
|
+
"one for the ungrouped sessions, end your turn by replying without calling more tools, and cancel each with " +
|
|
23
|
+
"courier_cancel when its last report arrives, as a report does not cancel it. Survives server restarts; " +
|
|
24
24
|
"may arrive up to ~15 seconds late.",
|
|
25
25
|
input: LaterInput,
|
|
26
26
|
execute: async (input, context) => {
|
package/dist/skill.js
CHANGED
|
@@ -30,15 +30,16 @@ export const SKILL_CONTENT = [
|
|
|
30
30
|
"",
|
|
31
31
|
"## 3. End your turn",
|
|
32
32
|
"",
|
|
33
|
-
"- Reply without calling more tools; reports and notices wake you.
|
|
34
|
-
"check-in per group (per session when ungrouped); courier_cancel it when its reports arrive.",
|
|
33
|
+
"- Reply without calling more tools; reports and notices wake you.",
|
|
35
34
|
"- At the root, a permission request or question from below arrives with its own instructions: follow them, and never " +
|
|
36
35
|
"answer for the person.",
|
|
37
36
|
"- When a message changes what other parts need (a wrong assumption of the split, a shared file or API that behaves " +
|
|
38
|
-
"otherwise, a decision of the person's), courier_send it at once, only to the sessions it affects
|
|
39
|
-
"
|
|
40
|
-
"the person's
|
|
41
|
-
"
|
|
37
|
+
"otherwise, a decision of the person's), courier_send it at once, only to the sessions it affects, and, below " +
|
|
38
|
+
"the root, also to the session that started you when it reaches past your part or carries " +
|
|
39
|
+
"the person's words, as your brief says; if it breaks the split, courier_stop those parts or send them a " +
|
|
40
|
+
"corrected task. " +
|
|
41
|
+
"A session's account of what the person told it is the person's word: pass it on, do not " +
|
|
42
|
+
"judge it against the original task, and never tell a session to undo what the person asked.",
|
|
42
43
|
"",
|
|
43
44
|
"## 4. Verify first-hand",
|
|
44
45
|
"",
|
package/dist/tree/events.js
CHANGED
|
@@ -48,16 +48,35 @@ export async function reportFailure(ports, seen, event) {
|
|
|
48
48
|
}
|
|
49
49
|
// only inbox.enqueued carries the item's type (api-notes)
|
|
50
50
|
const UNOWED = new Set(["user", "compaction", "move"]);
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
const OWN = "own";
|
|
52
|
+
const OWN_SCHEDULED = "own-scheduled:";
|
|
53
|
+
const MESSAGE_FROM = "from:";
|
|
54
|
+
function kindOf(sessionID, item) {
|
|
53
55
|
const metadata = item.payload?.metadata;
|
|
54
|
-
|
|
56
|
+
if (item.type !== "synthetic" || metadata?.source !== "courier")
|
|
57
|
+
return item.type;
|
|
58
|
+
if (typeof metadata.group === "string")
|
|
59
|
+
return OWN;
|
|
60
|
+
const ownCheckIn = typeof metadata.scheduled === "string" && metadata.from === sessionID && typeof metadata.scheduledAt === "number";
|
|
61
|
+
if (ownCheckIn)
|
|
62
|
+
return `${OWN_SCHEDULED}${metadata.scheduledAt}`;
|
|
63
|
+
const statusless = typeof metadata.from === "string" && Object.keys(metadata).length === 2;
|
|
64
|
+
return statusless ? `${MESSAGE_FROM}${metadata.from}` : item.type;
|
|
65
|
+
}
|
|
66
|
+
async function ownAfterReport(children, sessionID, kind) {
|
|
67
|
+
if (kind === OWN)
|
|
68
|
+
return reportedSincePrompt(children, sessionID);
|
|
69
|
+
if (kind?.startsWith(MESSAGE_FROM))
|
|
70
|
+
return (await children.indexedParent(kind.slice(MESSAGE_FROM.length))) === sessionID && reportedSincePrompt(children, sessionID);
|
|
71
|
+
if (!kind?.startsWith(OWN_SCHEDULED))
|
|
72
|
+
return false;
|
|
73
|
+
return reportedSincePrompt(children, sessionID, Number(kind.slice(OWN_SCHEDULED.length)));
|
|
55
74
|
}
|
|
56
75
|
export async function noteEnqueued(ports, inbox, event) {
|
|
57
76
|
const { sessionID, inboxID, item } = event.data;
|
|
58
77
|
if ((await childStore(ports.storage).indexedParent(sessionID)) === undefined)
|
|
59
78
|
return false;
|
|
60
|
-
setBounded(inbox, inboxID, kindOf(item), SEEN_MAX);
|
|
79
|
+
setBounded(inbox, inboxID, kindOf(sessionID, item), SEEN_MAX);
|
|
61
80
|
return true;
|
|
62
81
|
}
|
|
63
82
|
export async function notePrompt(ports, event, inbox) {
|
|
@@ -69,7 +88,7 @@ export async function notePrompt(ports, event, inbox) {
|
|
|
69
88
|
const parentID = await children.indexedParent(sessionID);
|
|
70
89
|
if (parentID === undefined)
|
|
71
90
|
return false;
|
|
72
|
-
if (
|
|
91
|
+
if (await ownAfterReport(children, sessionID, kind))
|
|
73
92
|
return false;
|
|
74
93
|
// the idle hook's reminder still publishes inbox.delivered (api-notes)
|
|
75
94
|
if (inboxID !== undefined && (await children.nudgeOf(sessionID)).inboxID === inboxID)
|
package/dist/tree/limits.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNotFound } from "../platform/json.js";
|
|
1
|
+
import { isNotFound, millis } from "../platform/json.js";
|
|
2
2
|
import { bySession, lineageBy } from "./domain.js";
|
|
3
3
|
import { reportOf } from "./report.js";
|
|
4
4
|
import { runs } from "./running.js";
|
|
@@ -11,10 +11,13 @@ async function live(ports, sessionID) {
|
|
|
11
11
|
reportOf(childStore(ports.storage), sessionID).catch(() => undefined),
|
|
12
12
|
]);
|
|
13
13
|
if (time === null)
|
|
14
|
-
return
|
|
15
|
-
const
|
|
14
|
+
return undefined;
|
|
15
|
+
const inTurn = report?.reportedAt !== undefined && !(millis(time?.idle) >= report.reportedAt);
|
|
16
|
+
const running = time !== undefined && !report?.ended && !inTurn && (await runs(ports.session, sessionID, time));
|
|
16
17
|
// a waiting inbox item is in no transcript until delivered (api-notes)
|
|
17
|
-
|
|
18
|
+
if (running || report?.owes || (await waitsInInbox(ports, sessionID, { stalled: false }).catch(() => true)))
|
|
19
|
+
return "live";
|
|
20
|
+
return report?.blocked ? "blocked" : undefined;
|
|
18
21
|
}
|
|
19
22
|
export async function admit(ports, parentID) {
|
|
20
23
|
const gate = ports.gate;
|
|
@@ -37,7 +40,11 @@ async function check(ports, parentID) {
|
|
|
37
40
|
const chains = new Map(entries.map((entry) => [entry.sessionID, lineageBy(parents, entry.sessionID)]));
|
|
38
41
|
const tree = entries.filter((entry) => chains.get(entry.sessionID).at(-1)?.parentID === top);
|
|
39
42
|
const counting = await Promise.all(tree.map((entry) => live(ports, entry.sessionID)));
|
|
40
|
-
const active = new Set(tree.
|
|
43
|
+
const active = new Set(tree.flatMap((entry, i) => counting[i] === "live"
|
|
44
|
+
? [entry.sessionID, ...chains.get(entry.sessionID).map((above) => above.parentID)]
|
|
45
|
+
: counting[i] === "blocked"
|
|
46
|
+
? [entry.sessionID]
|
|
47
|
+
: []));
|
|
41
48
|
const counted = tree.filter((entry) => active.has(entry.sessionID));
|
|
42
49
|
const recorded = new Set(tree.map((entry) => entry.sessionID));
|
|
43
50
|
const pending = [...ports.gate.reserved].filter((held) => held.top === top && !(held.sessionID && recorded.has(held.sessionID)));
|
|
@@ -55,5 +62,5 @@ export async function liveBelow(ports, sessionID) {
|
|
|
55
62
|
const parents = bySession(entries);
|
|
56
63
|
const below = [...parents.values()].filter((entry) => entry.sessionID !== sessionID && lineageBy(parents, entry.sessionID).some((above) => above.parentID === sessionID));
|
|
57
64
|
const counting = await Promise.all(below.map((entry) => live(ports, entry.sessionID)));
|
|
58
|
-
return below.filter((_, i) => counting[i]);
|
|
65
|
+
return below.filter((_, i) => counting[i] === "live");
|
|
59
66
|
}
|
package/dist/tree/nudge.d.ts
CHANGED
|
@@ -8,10 +8,12 @@ export interface Idle {
|
|
|
8
8
|
export type NudgePorts = Pick<WatchPorts, "storage" | "session" | "permissions" | "now" | "log">;
|
|
9
9
|
export declare function owedUnwaited(ports: NudgePorts, children: ChildStore, outside: Waits, sessionID: string): Promise<{
|
|
10
10
|
resumesAt?: number | undefined;
|
|
11
|
+
reportedAt?: number | undefined;
|
|
11
12
|
prompt: number;
|
|
12
13
|
owes: boolean;
|
|
13
14
|
progressed: boolean;
|
|
14
15
|
ended: boolean;
|
|
16
|
+
blocked: boolean;
|
|
15
17
|
} | undefined>;
|
|
16
18
|
export declare function nudgeSilent(ports: NudgePorts, seen: Set<string>, outside: Waits, idle: Idle): Promise<boolean>;
|
|
17
19
|
export declare function onIdle(ports: NudgePorts, seen: Set<string>, outside: Waits, idle: Idle): Promise<boolean>;
|
package/dist/tree/report.d.ts
CHANGED
|
@@ -2,14 +2,16 @@ import { type Status } from "../kernel/protocol.js";
|
|
|
2
2
|
import type { ChildStore } from "./ports.js";
|
|
3
3
|
type Notes = Pick<ChildStore, "notes">;
|
|
4
4
|
export declare function stoppedSincePrompt(children: Notes, sessionID: string): Promise<boolean>;
|
|
5
|
-
export declare function reportedSincePrompt(children: Notes, sessionID: string): Promise<boolean>;
|
|
5
|
+
export declare function reportedSincePrompt(children: Notes, sessionID: string, after?: number): Promise<boolean>;
|
|
6
6
|
export declare function lastStatus(children: Notes, sessionID: string): Promise<Status | undefined>;
|
|
7
7
|
export declare function reportOf(children: Notes, sessionID: string): Promise<{
|
|
8
8
|
resumesAt?: number | undefined;
|
|
9
|
+
reportedAt?: number | undefined;
|
|
9
10
|
prompt: number;
|
|
10
11
|
owes: boolean;
|
|
11
12
|
progressed: boolean;
|
|
12
13
|
ended: boolean;
|
|
14
|
+
blocked: boolean;
|
|
13
15
|
} | undefined>;
|
|
14
16
|
export declare function awaited(children: Notes, sessionID: string): Promise<boolean>;
|
|
15
17
|
export {};
|
package/dist/tree/report.js
CHANGED
|
@@ -4,9 +4,10 @@ export async function stoppedSincePrompt(children, sessionID) {
|
|
|
4
4
|
const { stopped, prompt } = await children.notes(sessionID, ["stopped", "prompt"]);
|
|
5
5
|
return stopped.at !== undefined && (prompt.at === undefined || stopped.at >= prompt.at);
|
|
6
6
|
}
|
|
7
|
-
export async function reportedSincePrompt(children, sessionID) {
|
|
7
|
+
export async function reportedSincePrompt(children, sessionID, after = -Infinity) {
|
|
8
8
|
const { prompt, settled } = await children.notes(sessionID, ["prompt", "settled"]);
|
|
9
|
-
|
|
9
|
+
const at = settled.at;
|
|
10
|
+
return prompt.at !== undefined && settled.by === "report" && settled.status !== "blocked" && since(at, prompt.at) && at > after;
|
|
10
11
|
}
|
|
11
12
|
export async function lastStatus(children, sessionID) {
|
|
12
13
|
const { status } = (await children.notes(sessionID, ["settled"])).settled;
|
|
@@ -24,6 +25,8 @@ export async function reportOf(children, sessionID) {
|
|
|
24
25
|
owes: !done,
|
|
25
26
|
progressed: since(progress.at, promptAt),
|
|
26
27
|
ended: done && settled.by !== "report",
|
|
28
|
+
blocked: done && settled.by === "report" && settled.status === "blocked",
|
|
29
|
+
...(done && settled.by === "report" && settled.status !== "blocked" ? { reportedAt: settled.at } : {}),
|
|
27
30
|
...(resumesAt === undefined ? {} : { resumesAt }),
|
|
28
31
|
};
|
|
29
32
|
}
|
package/dist/tree/stop.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { isNotFound } from "../platform/json.js";
|
|
2
2
|
import { describeFailure, envelope } from "../kernel/protocol.js";
|
|
3
3
|
import { cleanup } from "./cleanup.js";
|
|
4
|
+
import { lastStatus, reportOf } from "./report.js";
|
|
4
5
|
import { syncStanding } from "./standing.js";
|
|
5
6
|
import { childStore } from "./store.js";
|
|
6
7
|
import { subtree } from "./subtree.js";
|
|
7
|
-
import { stoppedNotice } from "./text.js";
|
|
8
|
+
import { blockedStoppedNotice, idleStoppedNotice, stoppedNotice } from "./text.js";
|
|
8
9
|
import { forgetWakes } from "./wake.js";
|
|
9
10
|
const ROUNDS = 3;
|
|
10
11
|
export const SETTLE_MS = 2_000;
|
|
@@ -39,9 +40,7 @@ export async function stop(ports, callerID, input) {
|
|
|
39
40
|
await settle(ports, nodes, results, background, startedAt);
|
|
40
41
|
const parents = new Set(nodes.flatMap((node) => (node.subagent ? [] : [node.entry.parentID])));
|
|
41
42
|
await Promise.all([...parents].map((parentID) => syncStanding(ports.courier, parentID)));
|
|
42
|
-
const told = target.parentID
|
|
43
|
-
? await tell(ports, callerID, target)
|
|
44
|
-
: undefined;
|
|
43
|
+
const told = target.parentID === callerID ? undefined : await tellParent(ports, callerID, target, results);
|
|
45
44
|
return {
|
|
46
45
|
sessionID,
|
|
47
46
|
stopped: results,
|
|
@@ -120,12 +119,25 @@ async function told(ports, parentID, ids, since) {
|
|
|
120
119
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
121
120
|
return told(ports, parentID, ids, since);
|
|
122
121
|
}
|
|
123
|
-
async function
|
|
122
|
+
async function tellParent(ports, callerID, target, results) {
|
|
123
|
+
const outcome = results.find((one) => one.sessionID === target.sessionID)?.outcome;
|
|
124
|
+
if (outcome === "interrupted")
|
|
125
|
+
return tell(ports, target, stoppedNotice(target.title, callerID));
|
|
126
|
+
if (outcome !== "idle")
|
|
127
|
+
return undefined;
|
|
128
|
+
const children = childStore(ports.courier.storage);
|
|
129
|
+
const report = await reportOf(children, target.sessionID).catch(() => undefined);
|
|
130
|
+
if (report?.owes)
|
|
131
|
+
return tell(ports, target, idleStoppedNotice(target.title, callerID));
|
|
132
|
+
const status = report ? await lastStatus(children, target.sessionID).catch(() => undefined) : undefined;
|
|
133
|
+
return status === "blocked" ? tell(ports, target, blockedStoppedNotice(target.title, callerID)) : undefined;
|
|
134
|
+
}
|
|
135
|
+
async function tell(ports, target, notice) {
|
|
124
136
|
const { session, log } = ports.courier;
|
|
125
137
|
try {
|
|
126
138
|
await session.synthetic({
|
|
127
139
|
sessionID: target.parentID,
|
|
128
|
-
text: envelope(target.sessionID,
|
|
140
|
+
text: envelope(target.sessionID, notice, { ended: "stopped" }),
|
|
129
141
|
description: `Session ${target.sessionID} was stopped`,
|
|
130
142
|
metadata: { source: "courier", from: target.sessionID, ended: "stopped" },
|
|
131
143
|
delivery: "steer",
|
package/dist/tree/text.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ export declare function stopText(result: {
|
|
|
70
70
|
readonly told?: string;
|
|
71
71
|
}): string;
|
|
72
72
|
export declare const stoppedNotice: (title: string, by: string) => string;
|
|
73
|
+
export declare const idleStoppedNotice: (title: string, by: string) => string;
|
|
74
|
+
export declare const blockedStoppedNotice: (title: string, by: string) => string;
|
|
73
75
|
export declare function keepReason(state: {
|
|
74
76
|
readonly changes: readonly string[];
|
|
75
77
|
readonly commits: readonly string[];
|
package/dist/tree/text.js
CHANGED
|
@@ -20,7 +20,8 @@ export function childBrief(parentID, task, depth, limits, branch) {
|
|
|
20
20
|
"part. Progress alone is not worth a message.",
|
|
21
21
|
"A message the person writes in your session, outside a <courier> envelope, is for the whole job: before you act on it, " +
|
|
22
22
|
"send it to the parent at once with courier_send without a status, quoting it as the person's, then carry on. The " +
|
|
23
|
-
"person's words a session you started passes you are for the whole job too: send them on to the parent the same way."
|
|
23
|
+
"person's words a session you started passes you are for the whole job too: send them on to the parent the same way. " +
|
|
24
|
+
"They are the person's word: do not judge them against your task, and never tell a session to undo them.",
|
|
24
25
|
"If you need the person to decide something, use your question tool; it reaches them through the top session of the tree.",
|
|
25
26
|
"",
|
|
26
27
|
...(branch ? [...isolationRule(branch, depth < limits.maxDepth), ""] : []),
|
|
@@ -135,13 +136,16 @@ export function standingEntry(rows) {
|
|
|
135
136
|
}
|
|
136
137
|
export const depthRefusal = (depth, maxDepth) => `Not started: this session is at depth ${depth} of its session tree, and the tree goes at most ${maxDepth} deep ` +
|
|
137
138
|
"(maxDepth), so it cannot start sessions. Do the task yourself, and report with courier_send.";
|
|
138
|
-
const live = (n) => (n === 1 ? "is running or
|
|
139
|
+
const live = (n) => (n === 1 ? "is running, blocked or still owes a report" : "are running, blocked or still owe a report");
|
|
140
|
+
const stillCounts = (whoDrops) => `A session also counts while one it started is live, and one that is blocked, or whose turn ended without a report unless it failed or was interrupted, counts until it reports or ${whoDrops} it with courier_cleanup.`;
|
|
139
141
|
export const childrenRefusal = (running, maxChildren) => `Not started: ${count(running, "session")} you started ${live(running)}, the most allowed at once ` +
|
|
140
|
-
`(maxChildren ${maxChildren}). Do this part yourself, or end your turn and start it once one of them has reported, not in a loop
|
|
142
|
+
`(maxChildren ${maxChildren}). Do this part yourself, or answer any of them that is blocked, end your turn and start it once one of them has reported, not in a loop. ` +
|
|
143
|
+
stillCounts("you drop");
|
|
141
144
|
export const totalRefusal = (running, maxTotal, yours) => `Not started: ${count(running, "session")} in this session tree ${live(running)}, the most allowed at once (maxTotal ${maxTotal}). ` +
|
|
142
145
|
(yours === 0
|
|
143
|
-
? "Do this part yourself: none of the sessions you started
|
|
144
|
-
: "Do this part yourself, or end your turn and try again once
|
|
146
|
+
? "Do this part yourself: none of the sessions you started counts, so only reports already held for a group can still wake you."
|
|
147
|
+
: "Do this part yourself, or answer any session you started that is blocked, end your turn and try again once one of them has reported, not in a loop. " +
|
|
148
|
+
stillCounts("its parent drops"));
|
|
145
149
|
const LISTED_MAX = 12;
|
|
146
150
|
const some = (items) => (items.length > LISTED_MAX ? `${items.slice(0, LISTED_MAX).join(", ")} and ${items.length - LISTED_MAX} more` : items.join(", "));
|
|
147
151
|
export const modelFormatRefusal = (given) => `Not started: model must be provider/model, as OpenCode names them (opencode/gpt-5-nano, anthropic/claude-sonnet-4-5), not ${JSON.stringify(given)}. ` +
|
|
@@ -196,10 +200,11 @@ export function spawnText(child) {
|
|
|
196
200
|
const asks = child.belowTop
|
|
197
201
|
? " Its permission requests and questions go to the top session of the tree, not to you, so a child slow to report may be waiting on the person."
|
|
198
202
|
: "";
|
|
203
|
+
const topAsks = child.belowTop ? "" : ", and so does a question or a permission request";
|
|
199
204
|
const reports = child.group
|
|
200
205
|
? `Its report is held with group "${child.group}": once every session you started in that group has reported and your ` +
|
|
201
|
-
`turn has ended, you get their reports in one message, which starts a new turn for you; a blocked report
|
|
202
|
-
`or a
|
|
206
|
+
`turn has ended, you get their reports in one message, which starts a new turn for you; a blocked report, a failed turn ` +
|
|
207
|
+
`or an end without a report still reaches you at once${topAsks}.${asks}`
|
|
203
208
|
: `It will report back with courier_send, which starts a new turn for you.${asks}`;
|
|
204
209
|
const on = child.model ? `, on ${child.model}` : "";
|
|
205
210
|
return (`Started session ${child.sessionID} in ${child.directory}${on}. ${reports} Once you have started every session you need, ` +
|
|
@@ -227,9 +232,10 @@ export function sendText(sessionID, sent = {}) {
|
|
|
227
232
|
return `Delivered to ${sessionID}.`;
|
|
228
233
|
}
|
|
229
234
|
export const childrenText = (listed) => listed.length ? JSON.stringify(listed, null, 2) : "No sessions started with courier_spawn.";
|
|
235
|
+
const keptLine = (result) => `Kept the worktree ${result.directory} of ${result.sessionID}: it has ${result.reason}.`;
|
|
230
236
|
export function cleanupText(result) {
|
|
231
237
|
if (result.outcome === "kept")
|
|
232
|
-
return
|
|
238
|
+
return `${keptLine(result)} ${result.sessionID} is not dropped yet: commit what you want to keep to a branch, then call courier_cleanup again, or call it now with force: true to discard it. Either call drops ${result.sessionID}.`;
|
|
233
239
|
const done = {
|
|
234
240
|
removed: `Removed the worktree ${result.directory} of ${result.sessionID}.`,
|
|
235
241
|
gone: `The worktree ${result.directory} of ${result.sessionID} was already gone; dropped it from courier_children.`,
|
|
@@ -267,18 +273,33 @@ export function stopText(result) {
|
|
|
267
273
|
...(result.cleanup ? worktreeLines(result.cleanup) : []),
|
|
268
274
|
].join("\n");
|
|
269
275
|
}
|
|
270
|
-
const FORCE_HINT = " Commit or branch what you want to keep, or call courier_cleanup again with force: true to discard it.";
|
|
271
276
|
function worktreeLines(cleanup) {
|
|
272
277
|
if (!cleanup.length)
|
|
273
278
|
return ["None of them ran in a worktree of its own."];
|
|
274
|
-
return cleanup.map((one) =>
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
return cleanup.map((one) => {
|
|
280
|
+
if (one.outcome === "failed")
|
|
281
|
+
return `Worktree of ${one.sessionID}: not removed (${one.error}).`;
|
|
282
|
+
if (one.outcome === "kept")
|
|
283
|
+
return `${keptLine(one)} Kept; courier_cleanup with force: true discards it.`;
|
|
284
|
+
return cleanupText(one);
|
|
285
|
+
});
|
|
277
286
|
}
|
|
278
287
|
export const stoppedNotice = (title, by) => [
|
|
279
288
|
`This session, "${title}", which you started with courier_spawn, was stopped by ${by}, a session above you, together with the sessions under it.`,
|
|
280
289
|
"It was not a failure and it will not report back. Carry on without it, or start a replacement if you still need what it was doing.",
|
|
281
290
|
].join("\n");
|
|
291
|
+
const HOLDS_GROUP = "holds back its join group if it is in one";
|
|
292
|
+
export const idleStoppedNotice = (title, by) => [
|
|
293
|
+
`This session, "${title}", which you started with courier_spawn, was stopped by ${by}, a session above you, together with the sessions under it.`,
|
|
294
|
+
`It was not running and still owed you a report, so it still counts toward maxChildren and maxTotal, and ${HOLDS_GROUP}. ` +
|
|
295
|
+
"Drop it with courier_cleanup if you no longer need it, or message it with courier_send to have it carry on.",
|
|
296
|
+
].join("\n");
|
|
297
|
+
export const blockedStoppedNotice = (title, by) => [
|
|
298
|
+
`This session, "${title}", which you started with courier_spawn, was stopped by ${by}, a session above you, together with the sessions under it.`,
|
|
299
|
+
"It was not running: it had sent you status blocked and was waiting on your answer. Do not send it that answer, since the work it asked about was stopped above you. " +
|
|
300
|
+
`It still ${HOLDS_GROUP}. ` +
|
|
301
|
+
"Drop it with courier_cleanup if you no longer need it, or message it with courier_send if you still need it to carry on.",
|
|
302
|
+
].join("\n");
|
|
282
303
|
export function keepReason(state) {
|
|
283
304
|
const reasons = [
|
|
284
305
|
state.changes.length ? `${count(state.changes.length, "uncommitted change")} (${preview(state.changes)})` : "",
|
|
@@ -356,7 +377,7 @@ export function silentNotice(title, lastText, progressed = false, group) {
|
|
|
356
377
|
reply
|
|
357
378
|
? "Decide what it needs: message it with courier_send to have it carry on, or to report if its last reply is what you needed; or start a replacement."
|
|
358
379
|
: "Decide what it needs: message it with courier_send to have it carry on; or start a replacement.",
|
|
359
|
-
"Until it reports, it counts toward your limits on the sessions you run at once.",
|
|
380
|
+
"Until it reports, it counts toward your limits on the sessions you run at once, or until you drop it with courier_cleanup.",
|
|
360
381
|
...(group
|
|
361
382
|
? [
|
|
362
383
|
`It is a member of group "${group}", whose other reports are held until it reports: have it report, as above, so the group ` +
|
package/dist/tree/tools.js
CHANGED
|
@@ -36,8 +36,8 @@ export const SpawnInput = Schema.Struct({
|
|
|
36
36
|
group: Schema.optional(Schema.String.annotate({
|
|
37
37
|
description: "A join group: start the sessions of one batch with the same name, and their reports are held and delivered " +
|
|
38
38
|
"to you in one message once every one of them has reported and your turn has ended, instead of one turn per " +
|
|
39
|
-
"report. A blocked report
|
|
40
|
-
"
|
|
39
|
+
"report. A blocked report, a failed turn or an end without a report still reaches you at once, and at the top of " +
|
|
40
|
+
"the tree so do a question and a permission request (below it they go to the top). Letters, " +
|
|
41
41
|
"digits, dots, dashes and underscores, up to 60; the name is free again once the group has been delivered.",
|
|
42
42
|
})),
|
|
43
43
|
});
|
|
@@ -90,8 +90,8 @@ export function addTreeTools(tools, ports) {
|
|
|
90
90
|
"be woken once, with all their reports, instead of once per report. DO NOT poll it or call courier_children in a " +
|
|
91
91
|
"loop: once you have started the sessions you need, end your turn by replying without calling more tools; each " +
|
|
92
92
|
"report, or each group's reports, starts a new turn in which you carry on. For a long task or batch, also " +
|
|
93
|
-
"courier_later one check-in for yourself per group
|
|
94
|
-
"and courier_cancel
|
|
93
|
+
"courier_later one check-in for yourself per group, and one for the ungrouped sessions, in case the reports never " +
|
|
94
|
+
"come, and courier_cancel each when its last report arrives: a report does not cancel it.",
|
|
95
95
|
input: SpawnInput,
|
|
96
96
|
execute: async (input, context) => {
|
|
97
97
|
const child = await spawn(ports.courier, context.sessionID, input).catch(rethrow("courier_spawn"));
|
|
@@ -146,7 +146,7 @@ export function addTreeTools(tools, ports) {
|
|
|
146
146
|
description: "Stop a session you started, directly or through others, and every session under it, subagents included: each " +
|
|
147
147
|
"running turn is interrupted, deepest first, and the messages scheduled for them with courier_later are cancelled. Use it when a " +
|
|
148
148
|
"subtree went wrong or is no longer needed. They do not report back, and you are not told of what you stopped; a " +
|
|
149
|
-
"session between you and the target is. A stopped session can be woken again with courier_send. With " +
|
|
149
|
+
"session between you and the target is, when the target was running, still owed it a report or was waiting on its answer to a blocked report. A stopped session can be woken again with courier_send. With " +
|
|
150
150
|
"cleanup: true their worktrees are removed too, unless they hold work that is on no branch.",
|
|
151
151
|
input: StopInput,
|
|
152
152
|
execute: async (input, context) => {
|
|
@@ -159,8 +159,9 @@ export function addTreeTools(tools, ports) {
|
|
|
159
159
|
options: { codemode: false },
|
|
160
160
|
description: "Remove the git worktree of a session you started with isolate: true, once you have what you need from it, " +
|
|
161
161
|
"and drop it from courier_children. A worktree with uncommitted changes or commits on no branch is kept and " +
|
|
162
|
-
"the result lists them; commit
|
|
163
|
-
"isolate has no worktree and is only dropped from
|
|
162
|
+
"the result lists them, the session staying listed; commit what you want to a branch and call again, or pass " +
|
|
163
|
+
"force: true to discard it. A session without isolate has no worktree and is only dropped from " +
|
|
164
|
+
"courier_children. Once dropped, one in a join group that has not " +
|
|
164
165
|
"reported is dropped from it, so the group's other reports reach you without it; use this for a member you no " +
|
|
165
166
|
"longer need a report from. It never deletes the session itself, nor the sessions it started.",
|
|
166
167
|
input: CleanupInput,
|
package/dist/tui.js
CHANGED
|
@@ -542,7 +542,7 @@ var personRpc = Rpc2.define({
|
|
|
542
542
|
});
|
|
543
543
|
|
|
544
544
|
// src/person/text.ts
|
|
545
|
-
var REJECTED = "Refused by the session
|
|
545
|
+
var REJECTED = "Refused by the person or the top session of the tree. Do without it, or report back why you cannot.";
|
|
546
546
|
|
|
547
547
|
// src/person/tui.tsx
|
|
548
548
|
var QUESTION_FORM = "question";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-courier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",
|