opencode-courier 0.13.8 → 0.14.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.
Files changed (163) hide show
  1. package/README.md +7 -9
  2. package/dist/index.d.ts +0 -8
  3. package/dist/index.js +6 -53
  4. package/dist/kernel/protocol.d.ts +0 -14
  5. package/dist/kernel/protocol.js +1 -15
  6. package/dist/kernel/tool.d.ts +0 -2
  7. package/dist/kernel/tool.js +0 -1
  8. package/dist/memory/domain.d.ts +6 -85
  9. package/dist/memory/domain.js +7 -63
  10. package/dist/memory/events.d.ts +0 -11
  11. package/dist/memory/events.js +1 -12
  12. package/dist/memory/forget.d.ts +0 -5
  13. package/dist/memory/forget.js +0 -4
  14. package/dist/memory/index.js +0 -2
  15. package/dist/memory/lessons.d.ts +0 -8
  16. package/dist/memory/lessons.js +0 -9
  17. package/dist/memory/note.d.ts +0 -9
  18. package/dist/memory/note.js +0 -5
  19. package/dist/memory/pin.d.ts +0 -5
  20. package/dist/memory/pin.js +0 -4
  21. package/dist/memory/ports.d.ts +2 -22
  22. package/dist/memory/recall.d.ts +0 -5
  23. package/dist/memory/recall.js +0 -12
  24. package/dist/memory/record.d.ts +0 -4
  25. package/dist/memory/record.js +8 -31
  26. package/dist/memory/rpc-handlers.d.ts +3 -6
  27. package/dist/memory/rpc-handlers.js +0 -6
  28. package/dist/memory/rpc.d.ts +6 -19
  29. package/dist/memory/rpc.js +1 -16
  30. package/dist/memory/store.d.ts +0 -5
  31. package/dist/memory/store.js +1 -16
  32. package/dist/memory/sweep.d.ts +0 -4
  33. package/dist/memory/sweep.js +0 -4
  34. package/dist/memory/text.d.ts +0 -25
  35. package/dist/memory/text.js +6 -36
  36. package/dist/memory/tools.d.ts +0 -2
  37. package/dist/memory/tools.js +0 -2
  38. package/dist/memory/tui.d.ts +1 -35
  39. package/dist/person/events.d.ts +0 -16
  40. package/dist/person/events.js +3 -30
  41. package/dist/person/form.d.ts +0 -9
  42. package/dist/person/form.js +0 -10
  43. package/dist/person/index.d.ts +0 -2
  44. package/dist/person/permission.d.ts +0 -34
  45. package/dist/person/permission.js +2 -28
  46. package/dist/person/ports.d.ts +0 -16
  47. package/dist/person/ports.js +0 -1
  48. package/dist/person/question/answer.d.ts +0 -28
  49. package/dist/person/question/answer.js +0 -49
  50. package/dist/person/question/index.js +0 -2
  51. package/dist/person/question/lifecycle.d.ts +0 -26
  52. package/dist/person/question/lifecycle.js +1 -32
  53. package/dist/person/question/pure.d.ts +0 -11
  54. package/dist/person/question/pure.js +0 -11
  55. package/dist/person/question/relay.d.ts +0 -1
  56. package/dist/person/question/relay.js +5 -67
  57. package/dist/person/question/runtime.d.ts +0 -14
  58. package/dist/person/question/runtime.js +1 -26
  59. package/dist/person/question/shared.d.ts +0 -17
  60. package/dist/person/question/shared.js +0 -20
  61. package/dist/person/question/types.d.ts +0 -44
  62. package/dist/person/rpc-handlers.d.ts +0 -7
  63. package/dist/person/rpc-handlers.js +0 -6
  64. package/dist/person/rpc.d.ts +0 -20
  65. package/dist/person/rpc.js +0 -16
  66. package/dist/person/text.d.ts +0 -45
  67. package/dist/person/text.js +2 -54
  68. package/dist/person/tools.d.ts +0 -3
  69. package/dist/person/tools.js +1 -3
  70. package/dist/person/tui.d.ts +0 -21
  71. package/dist/platform/bounded.d.ts +0 -8
  72. package/dist/platform/bounded.js +0 -8
  73. package/dist/platform/events.d.ts +0 -18
  74. package/dist/platform/events.js +3 -22
  75. package/dist/platform/hub.d.ts +0 -113
  76. package/dist/platform/hub.js +1 -76
  77. package/dist/platform/json.d.ts +0 -3
  78. package/dist/platform/json.js +1 -3
  79. package/dist/platform/storage.d.ts +0 -7
  80. package/dist/platform/storage.js +0 -6
  81. package/dist/platform/version.d.ts +0 -9
  82. package/dist/platform/version.js +0 -8
  83. package/dist/rpc.js +0 -1
  84. package/dist/scheduling/index.d.ts +0 -1
  85. package/dist/scheduling/index.js +0 -1
  86. package/dist/scheduling/later.d.ts +0 -11
  87. package/dist/scheduling/later.js +0 -14
  88. package/dist/scheduling/text.d.ts +0 -1
  89. package/dist/scheduling/text.js +0 -1
  90. package/dist/scheduling/tools.d.ts +0 -1
  91. package/dist/scheduling/tools.js +1 -5
  92. package/dist/skill.d.ts +1 -9
  93. package/dist/skill.js +4 -12
  94. package/dist/tree/cleanup.d.ts +0 -20
  95. package/dist/tree/cleanup.js +0 -26
  96. package/dist/tree/context.d.ts +0 -5
  97. package/dist/tree/context.js +0 -8
  98. package/dist/tree/domain.d.ts +0 -64
  99. package/dist/tree/domain.js +7 -45
  100. package/dist/tree/events.d.ts +0 -36
  101. package/dist/tree/events.js +4 -49
  102. package/dist/tree/forget.d.ts +0 -15
  103. package/dist/tree/forget.js +0 -18
  104. package/dist/tree/group.d.ts +0 -22
  105. package/dist/tree/group.js +1 -37
  106. package/dist/tree/index.d.ts +2 -2
  107. package/dist/tree/index.js +1 -3
  108. package/dist/tree/limits.d.ts +0 -4
  109. package/dist/tree/limits.js +1 -12
  110. package/dist/tree/mascot.d.ts +0 -25
  111. package/dist/tree/mascot.js +1 -32
  112. package/dist/tree/model.d.ts +0 -5
  113. package/dist/tree/model.js +2 -15
  114. package/dist/tree/nudge.d.ts +0 -11
  115. package/dist/tree/nudge.js +3 -16
  116. package/dist/tree/panel.d.ts +0 -8
  117. package/dist/tree/panel.js +3 -16
  118. package/dist/tree/ports.d.ts +2 -138
  119. package/dist/tree/report.d.ts +0 -10
  120. package/dist/tree/report.js +0 -11
  121. package/dist/tree/roster.d.ts +0 -21
  122. package/dist/tree/roster.js +0 -21
  123. package/dist/tree/rpc-handlers.d.ts +6 -1
  124. package/dist/tree/rpc-handlers.js +0 -5
  125. package/dist/tree/rpc.d.ts +9 -109
  126. package/dist/tree/rpc.js +2 -38
  127. package/dist/tree/running.d.ts +0 -6
  128. package/dist/tree/running.js +2 -10
  129. package/dist/tree/send.d.ts +0 -4
  130. package/dist/tree/send.js +4 -31
  131. package/dist/tree/spawn.d.ts +0 -7
  132. package/dist/tree/spawn.js +6 -47
  133. package/dist/tree/standing.d.ts +0 -8
  134. package/dist/tree/standing.js +1 -11
  135. package/dist/tree/status.d.ts +0 -8
  136. package/dist/tree/status.js +1 -9
  137. package/dist/tree/stop.d.ts +2 -9
  138. package/dist/tree/stop.js +58 -25
  139. package/dist/tree/store.d.ts +0 -8
  140. package/dist/tree/store.js +4 -38
  141. package/dist/tree/subtree.d.ts +8 -26
  142. package/dist/tree/subtree.js +37 -35
  143. package/dist/tree/summary.d.ts +0 -4
  144. package/dist/tree/summary.js +0 -8
  145. package/dist/tree/text.d.ts +1 -44
  146. package/dist/tree/text.js +5 -65
  147. package/dist/tree/tools.d.ts +0 -6
  148. package/dist/tree/tools.js +4 -8
  149. package/dist/tree/tui.d.ts +0 -81
  150. package/dist/tree/wake.d.ts +0 -30
  151. package/dist/tree/wake.js +5 -39
  152. package/dist/tui.js +22 -8
  153. package/package.json +8 -9
  154. package/dist/webhook/index.d.ts +0 -5
  155. package/dist/webhook/index.js +0 -3
  156. package/dist/webhook/receiver.d.ts +0 -80
  157. package/dist/webhook/receiver.js +0 -339
  158. package/dist/webhook/subscriptions.d.ts +0 -29
  159. package/dist/webhook/subscriptions.js +0 -52
  160. package/dist/webhook/text.d.ts +0 -23
  161. package/dist/webhook/text.js +0 -82
  162. package/dist/webhook/tools.d.ts +0 -19
  163. package/dist/webhook/tools.js +0 -46
@@ -10,5 +10,4 @@ export declare const LaterInput: Schema.Struct<{
10
10
  export declare const CancelInput: Schema.Struct<{
11
11
  readonly id: Schema.String;
12
12
  }>;
13
- /** Adds `courier_later` and `courier_cancel`. */
14
13
  export declare function addSchedulingTools(tools: ToolEditor, later: LaterPorts): void;
@@ -4,10 +4,7 @@ import { cancel, schedule } from "./later.js";
4
4
  import { cancelText, laterText } from "./text.js";
5
5
  export const LaterInput = Schema.Struct({
6
6
  message: Schema.String.annotate({ description: "The message to deliver." }),
7
- delayMinutes: Schema.optional(
8
- // Some models send the number as a string ("3", "3.0") whatever the schema says, and resend it
9
- // unchanged after the error, so a string is accepted and schedule reads it as a number.
10
- Schema.Union([Schema.Number, Schema.String]).annotate({
7
+ delayMinutes: Schema.optional(Schema.Union([Schema.Number, Schema.String]).annotate({
11
8
  description: "Deliver this many minutes from now. Give this or at.",
12
9
  })),
13
10
  at: Schema.optional(Schema.String.annotate({ description: "Deliver at this ISO 8601 time. Give this or delayMinutes." })),
@@ -16,7 +13,6 @@ export const LaterInput = Schema.Struct({
16
13
  export const CancelInput = Schema.Struct({
17
14
  id: Schema.String.annotate({ description: "The id courier_later returned." }),
18
15
  });
19
- /** Adds `courier_later` and `courier_cancel`. */
20
16
  export function addSchedulingTools(tools, later) {
21
17
  tools.add({
22
18
  name: "courier_later",
package/dist/skill.d.ts CHANGED
@@ -1,20 +1,12 @@
1
1
  import type { Plugin } from "@opencode/plugin";
2
- /** Why a model loads the skill, listed with every session's available skills. */
3
2
  export declare const SKILL_DESCRIPTION: string;
4
- /**
5
- * The skill's text: how the tools behave, not how to plan or review the parts, which is left to the person's own skills.
6
- * It names no limits, which the role part and the brief give; it repeats the brief's split rule, since the root has no brief.
7
- */
8
3
  export declare const SKILL_CONTENT: string;
9
- /** The editor OpenCode hands a skill transform. */
10
4
  export type SkillEditor = Parameters<Parameters<Plugin.Context["skill"]["transform"]>[0]>[0];
11
- /** The courier-orchestrate skill as OpenCode lists it: its id, name and description, and the text the model loads. */
12
5
  export declare const orchestrateSkill: () => {
13
6
  readonly id: "courier-orchestrate";
14
7
  readonly name: "courier-orchestrate";
15
8
  readonly description: string;
16
- readonly path: "/opencode-courier/courier-orchestrate.md";
9
+ readonly path: "skill://courier-orchestrate/SKILL.md";
17
10
  readonly content: string;
18
11
  };
19
- /** Adds the courier-orchestrate skill; every instance adds the same one, under the same id. */
20
12
  export declare function addSkill(skills: SkillEditor): void;
package/dist/skill.js CHANGED
@@ -1,13 +1,7 @@
1
1
  import { ONE_PART_LINE, SKILL_ID, SPLIT_LINE } from "./kernel/protocol.js";
2
- // The courier-orchestrate skill: the text a session loads with its skill tool.
3
- /** Why a model loads the skill, listed with every session's available skills. */
4
2
  export const SKILL_DESCRIPTION = "How opencode-courier's session tools work. Load before you split a task with courier_spawn: when it has 2 or more " +
5
3
  "independent, substantial parts touching separate files or areas. Starting the parts, waiting without polling, " +
6
4
  "relaying questions and permissions, reading reports, merging and reporting upward.";
7
- /**
8
- * The skill's text: how the tools behave, not how to plan or review the parts, which is left to the person's own skills.
9
- * It names no limits, which the role part and the brief give; it repeats the brief's split rule, since the root has no brief.
10
- */
11
5
  export const SKILL_CONTENT = [
12
6
  "# How opencode-courier's session tools work",
13
7
  "",
@@ -103,8 +97,8 @@ export const SKILL_CONTENT = [
103
97
  "## When a part goes wrong",
104
98
  "",
105
99
  '- A failed turn (failed="...") or a silent end (ended="without-report", quoting the session\'s last reply): message ' +
106
- "the session with courier_send to carry on, to report, or to try again; or start a replacement. Until it reports, " +
107
- "it counts toward the limits.",
100
+ "the session with courier_send to carry on, to report, or to try again; or start a replacement. A silent end counts " +
101
+ "toward the limits until it reports; a failed turn does not, until you message it again.",
108
102
  "- A refusal from courier_spawn naming maxDepth, maxChildren or maxTotal: do that part yourself, or end your turn and " +
109
103
  "start it once a report has arrived. Do not retry it in a loop.",
110
104
  "- Lost track, after a compaction or a restart: one courier_tree shows every session under you with its depth, its " +
@@ -112,11 +106,9 @@ export const SKILL_CONTENT = [
112
106
  "- A subtree that went wrong or is no longer needed: courier_stop its top session; everything under it is stopped " +
113
107
  "too, and nothing in it reports back. cleanup: true removes their worktrees.",
114
108
  ].join("\n");
115
- /** A skill's path is where OpenCode would look for its files; this one has none, so a path of the plugin's own. */
116
- const SKILL_PATH = `/opencode-courier/${SKILL_ID}.md`;
117
- /** The courier-orchestrate skill as OpenCode lists it: its id, name and description, and the text the model loads. */
109
+ // A skill:// path: the read tool serves it from the registry (api-notes)
110
+ const SKILL_PATH = `skill://${SKILL_ID}/SKILL.md`;
118
111
  export const orchestrateSkill = () => ({ id: SKILL_ID, name: SKILL_ID, description: SKILL_DESCRIPTION, path: SKILL_PATH, content: SKILL_CONTENT });
119
- /** Adds the courier-orchestrate skill; every instance adds the same one, under the same id. */
120
112
  export function addSkill(skills) {
121
113
  skills.add(orchestrateSkill());
122
114
  }
@@ -3,31 +3,11 @@ export interface CleanupInput {
3
3
  readonly sessionID: string;
4
4
  readonly force?: boolean;
5
5
  }
6
- /** At most this many changes and commits are returned; the reason still counts them all. */
7
6
  export declare const MAX_LISTED = 50;
8
- /**
9
- * Forgets a child the parent started, removing its worktree if it is isolated. A worktree with uncommitted changes
10
- * or commits found nowhere else is kept unless `force` is set; the result says so.
11
- */
12
7
  export declare function cleanup(ports: CleanupPorts, parentID: string, input: CleanupInput): Promise<CleanupResult>;
13
- /** Names the git to run, by absolute path, overriding the places `GIT_LOCATIONS` lists. */
14
8
  export declare const GIT_ENV = "OPENCODE_COURIER_GIT";
15
- /**
16
- * Where git is looked for, in order: its usual install locations, never `PATH`, so a writable
17
- * directory early in `PATH` cannot put another program in its place.
18
- */
19
9
  export declare const GIT_LOCATIONS: Readonly<Record<"posix" | "win32", readonly string[]>>;
20
- /**
21
- * The git to run: `OPENCODE_COURIER_GIT` when it is an absolute path, otherwise the first of
22
- * `GIT_LOCATIONS` that exists. Throws when there is none.
23
- */
24
10
  export declare function findGit(env?: Readonly<Record<string, string | undefined>>, platform?: NodeJS.Platform, exists?: (path: string) => boolean): string;
25
- /** The commit a worktree is on, or undefined when git cannot tell. */
26
11
  export declare function headOf(directory: string): Promise<string | undefined>;
27
- /** Whether a worktree has uncommitted changes, untracked files included; undefined when git cannot tell. */
28
12
  export declare function dirtyOf(directory: string): Promise<boolean | undefined>;
29
- /**
30
- * Reads a worktree's state with git; undefined when the directory is gone. Commits reachable from
31
- * `base`, the commit the worktree was made from, are not its own work and are not listed.
32
- */
33
13
  export declare function inspectWorktree(directory: string, base?: string): Promise<WorktreeState | undefined>;
@@ -5,18 +5,12 @@ import { forgetDropped } from "./forget.js";
5
5
  import { syncStanding } from "./standing.js";
6
6
  import { childStore } from "./store.js";
7
7
  import { keepReason } from "./text.js";
8
- /** At most this many changes and commits are returned; the reason still counts them all. */
9
8
  export const MAX_LISTED = 50;
10
- /**
11
- * Forgets a child the parent started, removing its worktree if it is isolated. A worktree with uncommitted changes
12
- * or commits found nowhere else is kept unless `force` is set; the result says so.
13
- */
14
9
  export async function cleanup(ports, parentID, input) {
15
10
  const entry = await childStore(ports.storage).entry(parentID, input.sessionID);
16
11
  if (!entry)
17
12
  throw new Error(`${input.sessionID} is not on the courier_children list of ${parentID}.`);
18
13
  const { directory } = entry;
19
- // A member still out is dropped from its group, so the group is released without it; the result names that group.
20
14
  const forget = async () => {
21
15
  const group = await forgetDropped(ports, entry);
22
16
  await syncStanding(ports, parentID);
@@ -24,8 +18,6 @@ export async function cleanup(ports, parentID, input) {
24
18
  };
25
19
  if (!entry.isolated)
26
20
  return { sessionID: input.sessionID, directory, outcome: "forgotten", ...(await forget()) };
27
- // With force the state only decides whether there is anything left to remove, so a worktree git
28
- // can no longer read is still removed.
29
21
  const state = await ports
30
22
  .inspect(directory, entry.base)
31
23
  .catch((error) => (input.force ? { changes: [], commits: [] } : Promise.reject(error)));
@@ -48,26 +40,16 @@ export async function cleanup(ports, parentID, input) {
48
40
  });
49
41
  return { sessionID: input.sessionID, directory, outcome: "removed", ...(await forget()) };
50
42
  }
51
- /** Names the git to run, by absolute path, overriding the places `GIT_LOCATIONS` lists. */
52
43
  export const GIT_ENV = "OPENCODE_COURIER_GIT";
53
- /**
54
- * Where git is looked for, in order: its usual install locations, never `PATH`, so a writable
55
- * directory early in `PATH` cannot put another program in its place.
56
- */
57
44
  export const GIT_LOCATIONS = {
58
45
  posix: [
59
46
  "/usr/bin/git",
60
47
  "/usr/local/bin/git",
61
48
  "/opt/homebrew/bin/git",
62
- // NixOS, and nix-darwin.
63
49
  "/run/current-system/sw/bin/git",
64
50
  ],
65
51
  win32: [String.raw `C:\Program Files\Git\cmd\git.exe`, String.raw `C:\Program Files (x86)\Git\cmd\git.exe`],
66
52
  };
67
- /**
68
- * The git to run: `OPENCODE_COURIER_GIT` when it is an absolute path, otherwise the first of
69
- * `GIT_LOCATIONS` that exists. Throws when there is none.
70
- */
71
53
  export function findGit(env = process.env, platform = process.platform, exists = existsSync) {
72
54
  const named = env[GIT_ENV]?.trim();
73
55
  if (named) {
@@ -84,29 +66,21 @@ export function findGit(env = process.env, platform = process.platform, exists =
84
66
  function git(directory, args) {
85
67
  return new Promise((resolve, reject) => execFile(findGit(), ["-C", directory, ...args], { maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => error ? reject(new Error(`git ${args[0]} in ${directory}: ${stderr.trim() || error.message}`)) : resolve(stdout)));
86
68
  }
87
- /** The commit a worktree is on, or undefined when git cannot tell. */
88
69
  export function headOf(directory) {
89
70
  return git(directory, ["rev-parse", "HEAD"]).then((out) => out.trim() || undefined, () => undefined);
90
71
  }
91
- /** Whether a worktree has uncommitted changes, untracked files included; undefined when git cannot tell. */
92
72
  export function dirtyOf(directory) {
93
73
  return git(directory, ["status", "--porcelain"]).then((out) => out.trim().length > 0, () => undefined);
94
74
  }
95
- /**
96
- * Reads a worktree's state with git; undefined when the directory is gone. Commits reachable from
97
- * `base`, the commit the worktree was made from, are not its own work and are not listed.
98
- */
99
75
  export async function inspectWorktree(directory, base) {
100
76
  if (!existsSync(directory))
101
77
  return undefined;
102
- // An untracked directory is one entry, not every file in it.
103
78
  const status = await git(directory, ["status", "--porcelain=v1", "-z", "--untracked-files=normal"]);
104
79
  const changes = [];
105
80
  const records = status.split("\0").filter(Boolean);
106
81
  for (let i = 0; i < records.length; i++) {
107
82
  const record = records[i];
108
83
  changes.push(record.slice(3));
109
- // A rename or copy is followed by its source path in a record of its own.
110
84
  if ("RC".includes(record[0]) || "RC".includes(record[1]))
111
85
  i++;
112
86
  }
@@ -1,5 +1,4 @@
1
1
  import type { ContextPorts } from "./ports.js";
2
- /** The part of a session context this hook changes. */
3
2
  export interface Shaped {
4
3
  readonly sessionID: string;
5
4
  tools: Record<string, unknown>;
@@ -8,8 +7,4 @@ export interface Shaped {
8
7
  text: string;
9
8
  }>;
10
9
  }
11
- /**
12
- * The `context` hook: hides courier_spawn and the tools named `hiddenFromLeaf` from a session at `maxDepth`, and names a
13
- * session's role in a system part unless its `role` entry does; a session nobody spawned that has started none is pointed to the skill.
14
- */
15
10
  export declare function shapeContext(ports: ContextPorts, event: Shaped): Promise<void>;
@@ -1,14 +1,8 @@
1
1
  import { setBounded } from "../platform/bounded.js";
2
2
  import { childStore } from "./store.js";
3
3
  import { ROLE_KEY, ROLE_PREFIX, rolePart } from "./text.js";
4
- /** How many sessions' roles an instance remembers. */
5
4
  const ROLES_MAX = 1000;
6
- /** A role part, or the role entry's block, already in the system prompt, wherever its part begins. */
7
5
  const ROLE_LINE = new RegExp(`^${ROLE_PREFIX}`, "m");
8
- /**
9
- * The `context` hook: hides courier_spawn and the tools named `hiddenFromLeaf` from a session at `maxDepth`, and names a
10
- * session's role in a system part unless its `role` entry does; a session nobody spawned that has started none is pointed to the skill.
11
- */
12
6
  export async function shapeContext(ports, event) {
13
7
  try {
14
8
  const { depth, entry } = await roleOf(ports, event.sessionID);
@@ -29,7 +23,6 @@ async function roleOf(ports, sessionID) {
29
23
  const known = ports.roles.get(sessionID);
30
24
  if (known !== undefined)
31
25
  return known;
32
- // The reverse index alone: a scan here would run for every session's first request, courier or not.
33
26
  const children = childStore(ports.storage);
34
27
  const chain = await children.indexedLineage(sessionID);
35
28
  const depth = chain.length || ((await children.children(sessionID)).length ? 0 : null);
@@ -37,7 +30,6 @@ async function roleOf(ports, sessionID) {
37
30
  setBounded(ports.roles, sessionID, role, ROLES_MAX);
38
31
  return role;
39
32
  }
40
- /** Whether the session carries the role entry, listed when its role is looked up; one that cannot be read gets the part. */
41
33
  async function hasEntry(ports, sessionID) {
42
34
  if (!ports.instructions)
43
35
  return false;
@@ -1,37 +1,23 @@
1
- /**
2
- * The tree's pure rules, on plain data: the limits, a session's lineage, whether it runs, and a group
3
- * member's standing. Imports nothing of the plugin's but `kernel/` and `platform/json.ts`.
4
- */
5
1
  import type { Plugin } from "@opencode/plugin";
6
2
  import type { Status } from "../kernel/protocol.js";
7
3
  type Context = Plugin.Context;
8
- /** The limits on the tree of sessions courier_spawn builds, from the plugin's options. */
9
4
  export interface Limits {
10
5
  readonly maxDepth: number;
11
6
  readonly maxChildren: number;
12
7
  readonly maxTotal: number;
13
8
  }
14
9
  export declare const DEFAULT_LIMITS: Limits;
15
- /** The limits in the plugin's options; one that is not a positive integer keeps its default and is named in `problems`. */
16
10
  export declare function readLimits(options: unknown): {
17
11
  limits: Limits;
18
12
  problems: string[];
19
13
  };
20
- /** The `nativeChildren` option: whether a child started without isolate or `native` gets OpenCode's own `parentID`; off unless `true`. */
21
14
  export declare function readNativeChildren(options: unknown): {
22
15
  nativeChildren: boolean;
23
16
  problems: string[];
24
17
  };
25
- /** A session's times, as `session.get` gives them. */
26
18
  type Times = Awaited<ReturnType<Context["session"]["get"]>>["time"];
27
- /** A session's times as `busy` reads them; `busy`, when its latest execution started, is stamped since OpenCode 2.2.2. */
28
19
  export type BusyTimes = Pick<Times, "updated" | "idle" | "busy">;
29
- /**
30
- * Whether a session may run by its times: it has never finished a turn, or its latest execution started after the last one ended
31
- * (`busy`; without it `updated`, which a rename, an archive or a waiting inbox item also stamp); only its transcript then tells.
32
- */
33
20
  export declare const busy: (time: BusyTimes) => boolean;
34
- /** A stored message as `turnEnd` reads it: an assistant message's `error` says how its step ended. */
35
21
  type Stored = {
36
22
  readonly type: string;
37
23
  readonly error?: {
@@ -42,50 +28,26 @@ type Stored = {
42
28
  readonly completed?: unknown;
43
29
  };
44
30
  };
45
- /**
46
- * The message a transcript's last turn ended on, unless one runs, past asides made since: an `idle` message, or a step
47
- * interrupted with none after it, as a declined permission or dismissed question ends a turn, recording no `idle` (#326).
48
- */
49
31
  export declare function turnEnd<M extends Stored>(messages: ReadonlyArray<M>): M | undefined;
50
32
  export declare const endsIdle: (messages: ReadonlyArray<Stored>) => boolean;
51
- /** When the turn `end` ended: the session's idle time for an `idle` message, else when its interrupted step ended. */
52
33
  export declare function endedAt(end: Stored, time: BusyTimes): number | undefined;
53
- /**
54
- * Whether a session of a tree read is live, for a cut listing's order: it runs (its turn not failed or interrupted since its last
55
- * prompt), waits on a request or form, or owes a report, but not, unlike for the limits, for a message waiting in its inbox.
56
- */
57
34
  export declare const isLive: (running: boolean | undefined, waits: number, report: {
58
35
  readonly owes: boolean;
59
36
  readonly ended: boolean;
60
37
  } | undefined) => boolean;
61
- /** A session of a tree read, as `cut` takes it: where it is in the tree, when it was started, and whether it is live. */
62
38
  export interface Cuttable {
63
39
  readonly sessionID: string;
64
40
  readonly parentID: string;
65
41
  readonly createdAt: number;
66
42
  readonly live: boolean;
67
43
  }
68
- /**
69
- * Which sessions a listing of at most `max` keeps, of a tree read in the order it is drawn: the live ones first, then
70
- * the newest, each with the sessions above it so the tree still draws; `leftOut` counts the rest.
71
- */
72
44
  export declare function cut<T extends Cuttable>(nodes: ReadonlyArray<T>, max: number): {
73
45
  kept: T[];
74
46
  leftOut: number;
75
47
  };
76
- /**
77
- * Entries older than this are dropped when their parent's roster is read, and on plugin setup,
78
- * except isolated children whose worktree is still there: those stay until courier_cleanup.
79
- */
80
48
  export declare const RETENTION_MS: number;
81
- /** Whether a directory still exists; tests pass a fake. */
82
49
  export type Exists = (directory: string) => boolean;
83
- /**
84
- * Whether an entry is past retention at `now`: recorded more than `RETENTION_MS` before, unless it is an isolated
85
- * child whose worktree exists, which is kept so it can still be cleaned up.
86
- */
87
50
  export declare const isExpired: (entry: RosterEntry, now: number, exists: Exists) => boolean;
88
- /** A session started with courier_spawn, recorded under the session that started it. */
89
51
  export interface RosterEntry {
90
52
  readonly sessionID: string;
91
53
  readonly parentID: string;
@@ -93,49 +55,30 @@ export interface RosterEntry {
93
55
  readonly directory: string;
94
56
  readonly isolated: boolean;
95
57
  readonly createdAt: number;
96
- /** For an isolated child, the directory its worktree was made from; for the record, nothing acts on it. */
97
58
  readonly source?: string;
98
- /** For an isolated child, the project its worktree belongs to; courier_cleanup removes it from there. */
99
59
  readonly project?: string;
100
- /** For an isolated child, the commit its worktree was made from; its own work is what came after. */
101
60
  readonly base?: string;
102
- /** `"status"` when its brief asked for a report with a status; absent for a child an earlier release briefed. */
103
61
  readonly reports?: "status";
104
- /** The join group courier_spawn put it in, whose release holds its report; absent for a child in none. */
105
62
  readonly group?: string;
106
- /** Created with OpenCode's own `parentID` (courier_spawn's `native`, else the `nativeChildren` option), so OpenCode shows its asks in its parent's view. */
107
63
  readonly native?: true;
108
- /** The model it was started on, as `modelName` writes it, when that is not the model its parent was on. */
109
64
  readonly model?: string;
110
65
  }
111
- /** How a child was started, as courier_children and the panel show it: isolated, native when it is, and its model when not its parent's. */
112
66
  export declare function startedAs(entry: RosterEntry): {
113
67
  model?: string | undefined;
114
68
  native?: true | undefined;
115
69
  isolated: boolean;
116
70
  };
117
- /** A model as a session names it: its provider, its id, and its variant, which `"default"` and none both leave to the provider. */
118
71
  export interface ModelRef {
119
72
  readonly providerID: string;
120
73
  readonly id: string;
121
74
  readonly variant?: string;
122
75
  }
123
- /** `provider/model` as `courier_spawn` takes it: one slash, nothing empty on either side. */
124
76
  export declare function parseModelName(text: string): Pick<ModelRef, "providerID" | "id"> | undefined;
125
- /** A model as the roster, the panel and the results name it: `provider/model`, with `#variant` for a variant that is not the default. */
126
77
  export declare function modelName(model: ModelRef): string;
127
- /** `lineage` over roster entries already read. */
128
78
  export declare function lineageIn(entries: ReadonlyArray<RosterEntry>, sessionID: string): RosterEntry[];
129
- /** Roster entries by their session, for `lineageBy`. */
130
79
  export declare function bySession(entries: ReadonlyArray<RosterEntry>): Map<string, RosterEntry>;
131
- /** `lineage` over roster entries already read and mapped by `bySession`. */
132
80
  export declare function lineageBy(bySession: ReadonlyMap<string, RosterEntry>, sessionID: string): RosterEntry[];
133
- /**
134
- * The session whose view OpenCode shows the asks of a lineage's first session in: up its native parents, to the
135
- * first session that is not a native child (an isolated one, or the top). The session itself when it is not native.
136
- */
137
81
  export declare function nativeRootOf(chain: ReadonlyArray<RosterEntry>): string;
138
- /** What a report lists besides its text; `checks` are the commands the session ran and what each gave. */
139
82
  export interface Artifacts {
140
83
  readonly branch?: string;
141
84
  readonly commits?: ReadonlyArray<string>;
@@ -145,21 +88,18 @@ export interface Artifacts {
145
88
  readonly result: string;
146
89
  }>;
147
90
  }
148
- /** A report held with its group: its status, text and artifacts, as courier_send was given them, and when. */
149
91
  export interface HeldReport {
150
92
  readonly at: number;
151
93
  readonly status: Status;
152
94
  readonly message: string;
153
95
  readonly artifacts?: Artifacts;
154
96
  }
155
- /** A member of a group that left it without a report, and why: its turn failed or was interrupted, it was deleted, or courier_cleanup dropped it. */
156
97
  export interface LeftMember {
157
98
  readonly sessionID: string;
158
99
  readonly title: string;
159
100
  readonly by: "failed" | "interrupted" | "deleted" | "dropped";
160
101
  }
161
102
  export declare const isGroupName: (value: unknown) => value is string;
162
- /** A member of a group, as stored: its title for the release notice, and its report or how it left. */
163
103
  export interface Member {
164
104
  readonly title: string;
165
105
  readonly joinedAt: number;
@@ -169,13 +109,9 @@ export interface Member {
169
109
  readonly by: LeftMember["by"];
170
110
  };
171
111
  }
172
- /** What is known of a member's standing: its report is held, still out, or it left, or it is no longer in an open group. */
173
112
  export type Standing = "held" | "out" | LeftMember["by"] | "released";
174
- /** A member's standing from its stored value, or `released` when there is none. */
175
113
  export declare function standingOf(member: Member | undefined): Standing;
176
- /** Whether every member of a group has a held report or has left: released once the parent's turn has ended. */
177
114
  export declare const complete: (members: ReadonlyArray<Member>) => boolean;
178
- /** How long the provider asked to wait after a failed turn, if it did: a positive number of milliseconds, at most `RETRY_MAX_MS`. */
179
115
  export declare function retryDelay(error: {
180
116
  readonly retryAfterMs?: unknown;
181
117
  }): number | undefined;
@@ -1,6 +1,5 @@
1
1
  import { millis, obj } from "../platform/json.js";
2
2
  export const DEFAULT_LIMITS = { maxDepth: 5, maxChildren: 5, maxTotal: 30 };
3
- /** The limits in the plugin's options; one that is not a positive integer keeps its default and is named in `problems`. */
4
3
  export function readLimits(options) {
5
4
  const given = obj(options);
6
5
  const problems = [];
@@ -16,43 +15,27 @@ export function readLimits(options) {
16
15
  const limits = { maxDepth: read("maxDepth"), maxChildren: read("maxChildren"), maxTotal: read("maxTotal") };
17
16
  return { limits, problems };
18
17
  }
19
- /** The `nativeChildren` option: whether a child started without isolate or `native` gets OpenCode's own `parentID`; off unless `true`. */
20
18
  export function readNativeChildren(options) {
21
19
  const value = obj(options).nativeChildren;
22
20
  if (value === undefined || typeof value === "boolean")
23
21
  return { nativeChildren: value === true, problems: [] };
24
22
  return { nativeChildren: false, problems: [`nativeChildren must be true or false, not ${JSON.stringify(value)}; using false`] };
25
23
  }
26
- /**
27
- * Whether a session may run by its times: it has never finished a turn, or its latest execution started after the last one ended
28
- * (`busy`; without it `updated`, which a rename, an archive or a waiting inbox item also stamp); only its transcript then tells.
29
- */
24
+ // time.busy exists since 2.2.2; time.updated is no sign of activity (api-notes)
30
25
  export const busy = (time) => time.idle === undefined || millis(time.busy ?? time.updated) > millis(time.idle);
31
- /** The messages the person's switch of agent or model, move or shell command appends, at rest or mid-turn alike. */
26
+ // a person's switches and shell commands append these, at rest or mid-turn
32
27
  const ASIDES = new Set(["agent-switched", "model-switched", "location-switched", "shell"]);
33
- /**
34
- * The message a transcript's last turn ended on, unless one runs, past asides made since: an `idle` message, or a step
35
- * interrupted with none after it, as a declined permission or dismissed question ends a turn, recording no `idle` (#326).
36
- */
28
+ // a decline or dismissed question ends on an aborted step, no idle (api-notes)
37
29
  export function turnEnd(messages) {
38
30
  const last = messages.findLast((message) => !ASIDES.has(message.type));
39
31
  return last?.type === "idle" || (last?.type === "assistant" && last.error?.type === "aborted") ? last : undefined;
40
32
  }
41
33
  export const endsIdle = (messages) => turnEnd(messages) !== undefined;
42
- /** When the turn `end` ended: the session's idle time for an `idle` message, else when its interrupted step ended. */
43
34
  export function endedAt(end, time) {
44
35
  const at = end.type === "idle" ? millis(time.idle) : millis(end.time?.completed ?? end.time?.created);
45
36
  return Number.isFinite(at) ? at : undefined;
46
37
  }
47
- /**
48
- * Whether a session of a tree read is live, for a cut listing's order: it runs (its turn not failed or interrupted since its last
49
- * prompt), waits on a request or form, or owes a report, but not, unlike for the limits, for a message waiting in its inbox.
50
- */
51
38
  export const isLive = (running, waits, report) => running !== undefined && ((running && !report?.ended) || waits > 0 || (report?.owes ?? false));
52
- /**
53
- * Which sessions a listing of at most `max` keeps, of a tree read in the order it is drawn: the live ones first, then
54
- * the newest, each with the sessions above it so the tree still draws; `leftOut` counts the rest.
55
- */
56
39
  export function cut(nodes, max) {
57
40
  const byID = new Map(nodes.map((node) => [node.sessionID, node]));
58
41
  const ranked = nodes
@@ -70,51 +53,34 @@ export function cut(nodes, max) {
70
53
  }
71
54
  return { kept: nodes.filter((node) => kept.has(node.sessionID)), leftOut: nodes.length - kept.size };
72
55
  }
73
- /**
74
- * Entries older than this are dropped when their parent's roster is read, and on plugin setup,
75
- * except isolated children whose worktree is still there: those stay until courier_cleanup.
76
- */
77
56
  export const RETENTION_MS = 14 * 24 * 60 * 60_000;
78
- /**
79
- * Whether an entry is past retention at `now`: recorded more than `RETENTION_MS` before, unless it is an isolated
80
- * child whose worktree exists, which is kept so it can still be cleaned up.
81
- */
82
57
  export const isExpired = (entry, now, exists) => now - entry.createdAt > RETENTION_MS && !(entry.isolated && exists(entry.directory));
83
- /** How a child was started, as courier_children and the panel show it: isolated, native when it is, and its model when not its parent's. */
84
58
  export function startedAs(entry) {
85
59
  return { isolated: entry.isolated, ...(entry.native ? { native: true } : {}), ...(entry.model ? { model: entry.model } : {}) };
86
60
  }
87
- /** `provider/model` as `courier_spawn` takes it: one slash, nothing empty on either side. */
88
61
  export function parseModelName(text) {
89
62
  const slash = text.indexOf("/");
90
63
  if (slash <= 0 || slash === text.length - 1)
91
64
  return undefined;
92
65
  return { providerID: text.slice(0, slash), id: text.slice(slash + 1) };
93
66
  }
94
- /** A model as the roster, the panel and the results name it: `provider/model`, with `#variant` for a variant that is not the default. */
95
67
  export function modelName(model) {
96
68
  const variant = model.variant && model.variant !== "default" ? `#${model.variant}` : "";
97
69
  return `${model.providerID}/${model.id}${variant}`;
98
70
  }
99
- /** `lineage` over roster entries already read. */
100
71
  export function lineageIn(entries, sessionID) {
101
72
  return lineageBy(bySession(entries), sessionID);
102
73
  }
103
- /** Roster entries by their session, for `lineageBy`. */
104
74
  export function bySession(entries) {
105
75
  return new Map(entries.map((entry) => [entry.sessionID, entry]));
106
76
  }
107
- /** `lineage` over roster entries already read and mapped by `bySession`. */
108
77
  export function lineageBy(bySession, sessionID) {
109
78
  const chain = [];
110
79
  for (let entry = bySession.get(sessionID); entry && !chain.includes(entry); entry = bySession.get(entry.parentID))
111
80
  chain.push(entry);
112
81
  return chain;
113
82
  }
114
- /**
115
- * The session whose view OpenCode shows the asks of a lineage's first session in: up its native parents, to the
116
- * first session that is not a native child (an isolated one, or the top). The session itself when it is not native.
117
- */
83
+ // OpenCode shows a native child's asks in its root's view (api-notes)
118
84
  export function nativeRootOf(chain) {
119
85
  let root = chain[0].sessionID;
120
86
  for (const entry of chain) {
@@ -124,10 +90,8 @@ export function nativeRootOf(chain) {
124
90
  }
125
91
  return root;
126
92
  }
127
- /** What a group may be called: short, and with no `/`, which separates the parts of its keys, or `"`, which closes an attribute. */
128
- const NAME = /^[\w.-]{1,60}$/;
129
- export const isGroupName = (value) => typeof value === "string" && NAME.test(value);
130
- /** A member's standing from its stored value, or `released` when there is none. */
93
+ const GROUP_NAME = /^[\w.-]{1,60}$/;
94
+ export const isGroupName = (value) => typeof value === "string" && GROUP_NAME.test(value);
131
95
  export function standingOf(member) {
132
96
  if (!member)
133
97
  return "released";
@@ -135,11 +99,9 @@ export function standingOf(member) {
135
99
  return "held";
136
100
  return member.left?.by ?? "out";
137
101
  }
138
- /** Whether every member of a group has a held report or has left: released once the parent's turn has ended. */
139
102
  export const complete = (members) => members.every((member) => member.report || member.left);
140
- /** Longer than this is no wait a notice can plan around; OpenCode clamps a header to 2^53 ms, past what a `Date` holds. */
103
+ // OpenCode clamps retry-after to 2^53 ms, past what a Date holds
141
104
  const RETRY_MAX_MS = 30 * 24 * 3_600_000;
142
- /** How long the provider asked to wait after a failed turn, if it did: a positive number of milliseconds, at most `RETRY_MAX_MS`. */
143
105
  export function retryDelay(error) {
144
106
  const wait = error.retryAfterMs;
145
107
  return typeof wait === "number" && Number.isFinite(wait) && wait > 0 && wait <= RETRY_MAX_MS ? wait : undefined;
@@ -1,16 +1,13 @@
1
1
  import type { EventModule } from "../platform/events.js";
2
2
  import type { ExecutionError, Waits, WatchPorts, WatchState } from "./ports.js";
3
- /** The part of OpenCode's `session.execution.failed` event the notice is made from. */
4
3
  export interface ExecutionFailed {
5
4
  readonly id: string;
6
- /** When OpenCode published it, in epoch milliseconds. */
7
5
  readonly created?: number;
8
6
  readonly data: {
9
7
  readonly sessionID: string;
10
8
  readonly error: ExecutionError;
11
9
  };
12
10
  }
13
- /** OpenCode's events that concern one session's turns and inbox: `session.execution.succeeded` and the like. */
14
11
  export interface SessionEvent {
15
12
  readonly id: string;
16
13
  readonly created?: number;
@@ -19,12 +16,7 @@ export interface SessionEvent {
19
16
  readonly reason?: string;
20
17
  };
21
18
  }
22
- /**
23
- * Tells the parent of a child whose turn failed, since a child that cannot reach its model cannot
24
- * call courier_send. Returns the parents told.
25
- */
26
19
  export declare function reportFailure(ports: WatchPorts, seen: Set<string>, event: ExecutionFailed): Promise<string[]>;
27
- /** OpenCode's `session.inbox.enqueued`: the item's type, which its `session.inbox.delivered` does not carry. */
28
20
  export interface InboxEnqueued {
29
21
  readonly data: {
30
22
  readonly sessionID: string;
@@ -34,43 +26,15 @@ export interface InboxEnqueued {
34
26
  };
35
27
  };
36
28
  }
37
- /** Remembers the type of an item enqueued for a spawned session, for `notePrompt`; other sessions cost one read. */
38
29
  export declare function noteEnqueued(ports: WatchPorts, inbox: Map<string, string>, event: InboxEnqueued): Promise<boolean>;
39
- /**
40
- * Notes that a message reached a spawned session, which then owes its parent a report, unless `inbox` knows it for one of
41
- * `UNOWED` (`spawn` notes the task itself); reads the reverse index alone, and claims nothing: the note is the same twice.
42
- */
43
30
  export declare function notePrompt(ports: WatchPorts, event: SessionEvent & {
44
31
  readonly data: {
45
32
  readonly inboxID?: string;
46
33
  };
47
34
  }, inbox?: Map<string, string>): Promise<boolean>;
48
- /**
49
- * Notes that a spawned session's turn was stopped, which takes it out of its group without a report; a
50
- * shutdown's is left to `reportDeclined`, as one stopped between steps resumes on the next start.
51
- */
52
35
  export declare function noteInterrupted(ports: WatchPorts, event: SessionEvent): Promise<boolean>;
53
- /**
54
- * Forgets what a deleted session leaves (report state, groups) and, unless isolated or a parent on the roster, its
55
- * entry. Returns the sessions forgotten.
56
- */
57
36
  export declare function noteDeleted(ports: WatchPorts, event: SessionEvent): Promise<string[]>;
58
- /** A `Waits` asking `suppliers` one after another, cheapest first: the first that waits answers. */
59
37
  export declare const waitsOn: (...suppliers: Waits[]) => Waits;
60
- /** A `Waits` asking `suppliers` at once, as cheap as each other: it waits when one does, and fails when one fails. */
61
- export declare const together: (...suppliers: Waits[]) => Waits;
62
- /**
63
- * Tells the parent of a spawned session whose turn ended without reporting to it since its last
64
- * prompt, unless it waits; finds the session by the reverse index alone. Returns the parents told.
65
- */
66
38
  export declare function reportSilent(ports: WatchPorts, seen: Set<string>, event: SessionEvent, outside: Waits): Promise<string[]>;
67
- /**
68
- * A session's turn that OpenCode reports as a shutdown ended in a plain decline, if its transcript ends on an aborted step: its
69
- * parent is told as for a turn that succeeded without a report. A real shutdown during a step reads the same, and is told too.
70
- */
71
39
  export declare function reportDeclined(ports: WatchPorts, seen: Set<string>, event: SessionEvent, outside: Waits): Promise<string[]>;
72
- /**
73
- * Tells parents of a child's failed or silent turn, unless it waits on what `outside` says, and notes
74
- * its prompts, interruptions and deletion; `after` a turn's end, seals the groups of its session.
75
- */
76
40
  export declare function treeEvents(state: WatchState, outside: Waits): EventModule<WatchPorts>;