opencode-courier 0.13.8 → 0.15.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 +7 -54
  4. package/dist/kernel/protocol.d.ts +3 -17
  5. package/dist/kernel/protocol.js +3 -17
  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 +1 -9
  16. package/dist/memory/lessons.js +2 -11
  17. package/dist/memory/note.d.ts +2 -11
  18. package/dist/memory/note.js +5 -11
  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 +1 -26
  35. package/dist/memory/text.js +8 -38
  36. package/dist/memory/tools.d.ts +0 -2
  37. package/dist/memory/tools.js +4 -6
  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 +3 -4
  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 +6 -8
  92. package/dist/skill.d.ts +1 -9
  93. package/dist/skill.js +40 -97
  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 +17 -13
  98. package/dist/tree/domain.d.ts +0 -64
  99. package/dist/tree/domain.js +7 -45
  100. package/dist/tree/events.d.ts +5 -36
  101. package/dist/tree/events.js +23 -55
  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 +6 -39
  106. package/dist/tree/index.d.ts +2 -2
  107. package/dist/tree/index.js +1 -3
  108. package/dist/tree/limits.d.ts +1 -4
  109. package/dist/tree/limits.js +9 -13
  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 +5 -139
  119. package/dist/tree/report.d.ts +1 -10
  120. package/dist/tree/report.js +4 -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 +1 -7
  132. package/dist/tree/spawn.js +15 -56
  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 +4 -46
  146. package/dist/tree/text.js +44 -83
  147. package/dist/tree/tools.d.ts +0 -6
  148. package/dist/tree/tools.js +15 -15
  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 +24 -10
  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
@@ -1,195 +1,82 @@
1
1
  import { type Registry, type Storage } from "./storage.js";
2
- /**
3
- * The plugin's process-wide state on `globalThis`, shared by every loaded copy; only this module puts
4
- * it there. `HUB_VERSION`, the fixed claim keys and version skew: docs/maintainers/architecture.md, § Several copies in one process.
5
- */
6
2
  export declare const HUB_VERSION = 2;
7
- /** The key of the hub; a copy that finds another version there keeps its own under `${HUB_KEY}@<version>`. */
8
3
  export declare const HUB_KEY = "opencode-courier.hub";
9
- /** The ports a member's scheduler keeps the owner key through: what every member's `later` ports have. */
10
4
  export interface TickPorts {
11
5
  readonly storage: Pick<Storage, "get" | "set" | "remove">;
12
6
  readonly now: () => number;
13
7
  }
14
- /**
15
- * The events handled, and the permission requests a session was told about or that were answered
16
- * first: claim sets shared by every subscription and copy, so each event is handled once.
17
- */
18
8
  export interface Watched {
19
9
  readonly seen: Set<string>;
20
10
  readonly waiting: Set<string>;
21
11
  readonly answered: Set<string>;
22
12
  }
23
- /** The forms sessions were told about, kept apart from the permission requests. */
24
13
  export interface FormsTold {
25
- /** Forms still shown whose notice went out, or is going out: the notice's delivery. */
26
14
  readonly told: Map<string, Promise<unknown>>;
27
- /** Forms settled before anyone was told, so a notice whose roster lookup was overtaken is not sent. */
28
15
  readonly settled: Set<string>;
29
16
  }
30
- /**
31
- * A loaded plugin instance: its location's directory and permission domain, and the ports a job
32
- * run once for the whole process would run it through.
33
- */
34
17
  export interface Member {
35
18
  readonly directory: string;
36
- /**
37
- * The location OpenCode set the instance up with, the very object the instance's Effect half is
38
- * handed too, which finds its member by it (`portsAt`). A member of a copy before it has none.
39
- */
40
19
  readonly location?: object;
41
- /** The location's permission domain, kept in `locations`. */
42
20
  readonly permission: unknown;
43
- /** The ports the scheduler's tick runs through: the owner key's, and the tick jobs'. */
44
21
  readonly later: TickPorts;
45
- /** The ports the hub's subscriptions to OpenCode's events run through. */
46
22
  readonly watch: unknown;
47
- /** The question relay's ports. */
48
23
  readonly questions: unknown;
49
24
  readonly log: (message: string) => void;
50
25
  }
51
- /**
52
- * A tick under way: the member it runs through, when it started, by the wall clock every member's
53
- * `now` reads (the next ticks compare it with another member's), and its end.
54
- */
55
26
  export interface Tick {
56
27
  readonly member: Member;
57
28
  readonly since: number;
58
- /** Settles once the tick has ended, the owner key released if its loop stopped meanwhile; never rejects. */
59
29
  readonly done: Promise<void>;
60
30
  }
61
- /**
62
- * The scheduler's loop, one per hub: it runs while the hub has members, and each tick runs the jobs
63
- * registered with `open` through the ports of the member that joined first and is still loaded.
64
- */
65
31
  export interface Scheduler {
66
- /** The loop's interval, while it runs. */
67
32
  timer?: unknown;
68
- /**
69
- * The tick under way, whichever copy started it: for a minute the key is renewed for it and due
70
- * ticks skip; past it, one whose member left holds nothing up. Older copies have only `ticking`.
71
- */
72
33
  running?: Tick;
73
- /**
74
- * The member the tick under way runs through, if any, set and cleared with `running`, for a copy
75
- * that reads only this: a tick that falls due meanwhile is skipped, unless that member has left.
76
- */
77
34
  ticking?: Member;
78
- /** One tick, by the copy of the plugin that joined last, so a copy loaded after an update runs its own code. */
79
35
  tick?: () => Promise<void>;
80
- /** Whether a nudge came since the tick under way started, whichever copy's, so it ticks once more when done. */
81
36
  nudged?: boolean;
82
37
  }
83
- /** How the hub starts and stops the scheduler's loop, and waits before re-reading the owner key; tests pass their own. */
84
38
  export interface Timers {
85
39
  readonly every: (run: () => void, ms: number) => unknown;
86
40
  readonly stop: (timer: unknown) => void;
87
41
  readonly wait: (ms: number) => Promise<void>;
88
42
  }
89
- /** How often the hub's scheduler ticks: what a job delivers, a `courier_later` message, can be this late. */
90
43
  export declare const TICK_MS = 15000;
91
- /**
92
- * The storage key, shared by every server on one data directory, naming the server whose scheduler
93
- * delivers `courier_later` messages: `{ server, at }`, its id and when it last wrote (epoch ms).
94
- */
95
44
  export declare const OWNER_KEY = "scheduler/owner";
96
- /** How long the owner key holds without being renewed; the owner renews it every tick, four times as often. */
97
45
  export declare const OWNER_EXPIRY_MS: number;
98
- /**
99
- * How old the owner's own key may be for it to renew the key and deliver at once. An older one,
100
- * which it may no longer hold by the time it writes, is taken again like a free one, with the wait.
101
- */
102
46
  export declare const OWNER_RENEW_MS: number;
103
- /** The wait between writing the owner key and reading it back, at least `OWNER_WAIT_MS` and up to twice that. */
104
47
  export declare const OWNER_WAIT_MS = 500;
105
- /**
106
- * This server's id in the owner key: the same for every copy of the plugin in the process, and
107
- * different in every other process, a restart of this one included.
108
- */
109
48
  export declare const SERVER: string;
110
- /**
111
- * How long an unloading instance waits for the work it leaves behind: what it started itself, a tick
112
- * or a notice under way through it, and on the last leave the owner key's release.
113
- */
114
49
  export declare const LEAVE_MS = 2000;
115
- /** How many subscriptions to OpenCode's events a hub keeps, each through another member: one, and a standby. */
116
50
  export declare const WATCHERS = 2;
117
- /** One of the hub's subscriptions to OpenCode's events: the member whose ports it runs through, and how it ends. */
118
51
  export interface Watcher {
119
52
  readonly member: Member;
120
53
  readonly stop: AbortController;
121
- /** Settles once the subscription has ended, after `stop`: after the event it is handling, not the stream's close. */
122
54
  readonly done: Promise<void>;
123
55
  }
124
56
  export interface Hub {
125
57
  readonly version: typeof HUB_VERSION;
126
- /** The instances loaded now, in the order they joined. */
127
58
  readonly members: Set<Member>;
128
59
  readonly scheduler: Scheduler;
129
- /**
130
- * The hub's subscriptions to OpenCode's events, each through another member: two while two members
131
- * are loaded, so one is always connected when the other's member leaves.
132
- */
133
60
  readonly watchers: Watcher[];
134
- /**
135
- * Starts a subscription through a member, by the copy of the plugin that set it last; until one
136
- * has, the hub follows no events.
137
- */
138
61
  subscribe?: (member: Member) => Watcher;
139
- /** The ids of the `courier_later` messages being delivered. */
140
62
  readonly claimed: Set<string>;
141
- /** The events handled, and the permission requests a session was told about or that were answered first. */
142
63
  readonly watched: Watched;
143
- /** The forms sessions were told about, and those settled first. */
144
64
  readonly forms: FormsTold;
145
- /**
146
- * The permission domain of every loaded instance, by instance. OpenCode keeps a request where its
147
- * session runs, so a request of an isolated child is answered through the instance loaded in its worktree.
148
- */
149
65
  readonly locations: Map<object, unknown>;
150
- /** The webhook receiver's state: the object every copy shares under the key `receiver`. */
151
66
  readonly receivers: object;
152
- /** The question relay's state: the object every copy shares under the key `questions`. */
153
67
  readonly questions: object;
154
- /** Whether the hub of another version found under `HUB_KEY` has been logged. */
155
68
  skewLogged: boolean;
156
69
  }
157
- /**
158
- * A job each tick runs, in its turn, through the first loaded member, which may be another copy's:
159
- * it uses only the ports every hub-v2 member has. `claimed` is the hub's claim set; a throw ends the tick.
160
- */
161
70
  export type TickJob = (owner: Member, claimed: Set<string>) => Promise<void>;
162
- /** A hub, as `open` finds or makes it, and how instances join it. */
163
71
  export interface Opened {
164
72
  readonly hub: Hub;
165
- /**
166
- * Adds a loaded instance to the hub and `locations`; returns its leave. The first join starts the
167
- * scheduler and watchers, the last leave stops them; a leave waits `LEAVE_MS` for the member's work.
168
- */
169
73
  readonly join: (member: Member) => () => Promise<void>;
170
- /** Hands the hub work a member started, which its leave waits for; the member's own background work. */
171
74
  readonly track: (member: Member, work: Promise<unknown>) => void;
172
- /**
173
- * Runs a tick now, or once the tick under way has ended, so what has just fallen due (a released
174
- * group) is delivered at once rather than at the next tick; nothing while no instance is loaded.
175
- */
176
75
  readonly nudge: () => void;
177
76
  }
178
- /**
179
- * The value under `opencode-courier.<name>` every copy in the process shares, made by `create` the first
180
- * time; another version, or a copy before the hub, may have made it, so its shape is kept for good.
181
- */
182
77
  export declare function sharedState<T>(name: string, create: () => T, registry?: Registry): T;
183
- /**
184
- * This copy's hub in `registry`, and how its instances join it. Its ticks run `jobs` in order, kept in
185
- * this copy's tick, which the hub runs once this copy has joined last, never in the hub itself.
186
- */
187
78
  export declare function open(registry: Registry, jobs: readonly TickJob[], timers?: Timers, server?: string): Opened;
188
- /** This copy's hub, in the process. */
189
79
  export declare const hub: Hub;
190
- /** Opens this copy's hub in the process, with its tick jobs; see `open`. */
191
80
  export declare const openInProcess: (jobs: readonly TickJob[]) => Opened;
192
- /** The member of this copy's hub set up with `location` that joined last, while it is loaded. */
193
81
  export declare function memberAt(location: object): Member | undefined;
194
- /** The permission domains of the loaded instances. */
195
82
  export declare const permissions: () => MapIterator<unknown>;
@@ -2,38 +2,18 @@ import { randomInt } from "node:crypto";
2
2
  import { hostname } from "node:os";
3
3
  import { num, obj, str } from "./json.js";
4
4
  import { processWide } from "./storage.js";
5
- /**
6
- * The plugin's process-wide state on `globalThis`, shared by every loaded copy; only this module puts
7
- * it there. `HUB_VERSION`, the fixed claim keys and version skew: docs/maintainers/architecture.md, § Several copies in one process.
8
- */
9
5
  export const HUB_VERSION = 2;
10
- /** The key of the hub; a copy that finds another version there keeps its own under `${HUB_KEY}@<version>`. */
11
6
  export const HUB_KEY = "opencode-courier.hub";
12
7
  const realTimers = {
13
8
  every: (run, ms) => setInterval(run, ms),
14
9
  stop: (timer) => clearInterval(timer),
15
10
  wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
16
11
  };
17
- /** How often the hub's scheduler ticks: what a job delivers, a `courier_later` message, can be this late. */
18
12
  export const TICK_MS = 15_000;
19
- /**
20
- * The storage key, shared by every server on one data directory, naming the server whose scheduler
21
- * delivers `courier_later` messages: `{ server, at }`, its id and when it last wrote (epoch ms).
22
- */
23
13
  export const OWNER_KEY = "scheduler/owner";
24
- /** How long the owner key holds without being renewed; the owner renews it every tick, four times as often. */
25
14
  export const OWNER_EXPIRY_MS = 4 * TICK_MS;
26
- /**
27
- * How old the owner's own key may be for it to renew the key and deliver at once. An older one,
28
- * which it may no longer hold by the time it writes, is taken again like a free one, with the wait.
29
- */
30
15
  export const OWNER_RENEW_MS = OWNER_EXPIRY_MS / 2;
31
- /** The wait between writing the owner key and reading it back, at least `OWNER_WAIT_MS` and up to twice that. */
32
16
  export const OWNER_WAIT_MS = 500;
33
- /**
34
- * This server's id in the owner key: the same for every copy of the plugin in the process, and
35
- * different in every other process, a restart of this one included.
36
- */
37
17
  export const SERVER = `${hostname()}:${process.pid}:${performance.timeOrigin}`;
38
18
  async function readOwner(ports) {
39
19
  const value = obj(await ports.storage.get(OWNER_KEY));
@@ -41,26 +21,13 @@ async function readOwner(ports) {
41
21
  const at = num(value.at);
42
22
  return server !== undefined && at !== undefined ? { server, at } : undefined;
43
23
  }
44
- /**
45
- * How long an unloading instance waits for the work it leaves behind: what it started itself, a tick
46
- * or a notice under way through it, and on the last leave the owner key's release.
47
- */
24
+ // OpenCode waits up to 2 s for an unloading instance (architecture)
48
25
  export const LEAVE_MS = 2_000;
49
- /** Whether a key written at `at` still holds at `now`; one from the future, after the clock went back, holds as long. */
50
26
  const holds = (at, now, ms) => Math.abs(now - at) < ms;
51
- /** How many subscriptions to OpenCode's events a hub keeps, each through another member: one, and a standby. */
52
27
  export const WATCHERS = 2;
53
- /**
54
- * The value under `opencode-courier.<name>` every copy in the process shares, made by `create` the first
55
- * time; another version, or a copy before the hub, may have made it, so its shape is kept for good.
56
- */
57
28
  export function sharedState(name, create, registry = globalThis) {
58
29
  return processWide(`opencode-courier.${name}`, create, registry);
59
30
  }
60
- /**
61
- * Finds or makes this copy's hub in `registry`: the one under `HUB_KEY`, or its own under a versioned
62
- * key when another version holds that; its claim sets are the pre-hub keys', made when missing.
63
- */
64
31
  function find(registry) {
65
32
  const shared = (name, create) => sharedState(name, create, registry);
66
33
  const create = () => ({
@@ -73,25 +40,15 @@ function find(registry) {
73
40
  forms: shared("forms", () => ({ told: new Map(), settled: new Set() })),
74
41
  locations: shared("locations", () => new Map()),
75
42
  receivers: shared("receiver", () => ({})),
76
- // Filled in by `person/question/`, field by field, since a copy before the hub may have made it without them.
77
43
  questions: shared("questions", () => ({})),
78
44
  skewLogged: false,
79
45
  });
80
46
  const found = registry[Symbol.for(HUB_KEY)];
81
- // The version of the hub of another copy found under `HUB_KEY`, if one is there.
82
47
  const skew = found != null && found.version !== HUB_VERSION ? { version: found.version } : undefined;
83
48
  return { hub: processWide(skew ? `${HUB_KEY}@${HUB_VERSION}` : HUB_KEY, create, registry), skew };
84
49
  }
85
- /**
86
- * This copy's hub in `registry`, and how its instances join it. Its ticks run `jobs` in order, kept in
87
- * this copy's tick, which the hub runs once this copy has joined last, never in the hub itself.
88
- */
89
50
  export function open(registry, jobs, timers = realTimers, server = SERVER) {
90
51
  const { hub, skew } = find(registry);
91
- /**
92
- * Whether this server holds the owner key, taking or renewing it through `ports`. A free, expired
93
- * or stalled key is written and read back after a random wait: the last of two writers holds it.
94
- */
95
52
  const own = async (ports) => {
96
53
  const held = await readOwner(ports);
97
54
  const now = ports.now();
@@ -103,34 +60,23 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
103
60
  await timers.wait(randomInt(OWNER_WAIT_MS, 2 * OWNER_WAIT_MS));
104
61
  return (await readOwner(ports))?.server === server;
105
62
  };
106
- /** Whether `held` is this server's key, recent enough to renew without reading it back. */
107
63
  const fresh = (held, now) => held?.server === server && holds(held.at, now, OWNER_RENEW_MS);
108
- /** Renews the owner key if this server holds it fresh: what a tick does, for a tick still delivering. */
109
64
  const renew = async (ports) => {
110
65
  const now = ports.now();
111
66
  if (fresh(await readOwner(ports), now))
112
67
  await ports.storage.set(OWNER_KEY, { server, at: now });
113
68
  };
114
- /** Whether a hub of another version in the process has instances, so its loop, under the same server id, still holds the key. */
115
69
  const othersRunning = () => Object.getOwnPropertySymbols(registry).some((key) => {
116
70
  const other = registry[key];
117
71
  const name = key.description ?? "";
118
72
  return other !== hub && (name === HUB_KEY || name.startsWith(`${HUB_KEY}@`)) && Number(other?.members?.size) > 0;
119
73
  });
120
- /**
121
- * Removes the owner key if this server holds it, so another takes over at its next tick: once the
122
- * loop has stopped, no tick is under way and no other hub version here runs a loop under this id.
123
- */
124
74
  const release = async (ports) => {
125
75
  const scheduler = hub.scheduler;
126
76
  const idle = () => scheduler.timer === undefined && !scheduler.running && !scheduler.ticking && !othersRunning();
127
77
  if (idle() && (await readOwner(ports))?.server === server && idle())
128
78
  await ports.storage.remove(OWNER_KEY);
129
79
  };
130
- /**
131
- * Whether the tick under way still holds the next up: within a minute of its start it does; past
132
- * that it is taken to hang, holding up only while its member is loaded. `recent` says which.
133
- */
134
80
  const busy = () => {
135
81
  const { running, ticking } = hub.scheduler;
136
82
  if (!running)
@@ -140,7 +86,6 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
140
86
  return { busy: true, recent: true };
141
87
  return { busy: hub.members.has(running.member), recent: false };
142
88
  };
143
- /** `busy`, renewing the key for a recent tick under way, as each tick falling due meanwhile does. */
144
89
  const underWay = () => {
145
90
  const state = busy();
146
91
  const first = hub.members.values().next().value;
@@ -148,8 +93,6 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
148
93
  void renew(first.later).catch((error) => first.log(`courier_later scheduler: ${String(error)}`));
149
94
  return state.busy;
150
95
  };
151
- // One tick, through the first loaded member, by the server holding the owner key. The claim on each
152
- // delivery stays, as a second line of defence against a pre-hub copy running its own interval.
153
96
  const tick = async () => {
154
97
  const scheduler = hub.scheduler;
155
98
  if (underWay())
@@ -170,8 +113,6 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
170
113
  const run = { member: owner, since, done: new Promise((resolve) => (end = resolve)) };
171
114
  scheduler.running = run;
172
115
  scheduler.ticking = owner;
173
- // `done` settles however the tick ends, and the key is given back, a log that throws included:
174
- // leaves wait for it. A log that throws is not logged again; there is nowhere left to tell.
175
116
  const tell = (message) => {
176
117
  try {
177
118
  owner.log(message);
@@ -188,21 +129,17 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
188
129
  tell(`courier_later scheduler: ${String(error)}`);
189
130
  }
190
131
  finally {
191
- // A tick past its minute may have been followed by another, whose record stays.
192
132
  if (scheduler.running === run) {
193
133
  scheduler.running = undefined;
194
134
  scheduler.ticking = undefined;
195
135
  }
196
136
  }
197
- // A loop stopped meanwhile, its members gone, gives back the key this tick may have written.
198
137
  if (scheduler.timer === undefined)
199
138
  await release(owner.later).catch((error) => tell(`courier_later scheduler: owner key not released: ${String(error)}`));
200
139
  end();
201
140
  if (scheduler.nudged && scheduler.timer !== undefined)
202
141
  void scheduler.tick?.();
203
142
  };
204
- // Through the hub's tick, the copy loaded last's, like the loop's; this copy's only before one joined. A
205
- // tick under way runs once more when it ends, so nothing is started here meanwhile.
206
143
  const nudge = () => {
207
144
  const scheduler = hub.scheduler;
208
145
  if (scheduler.timer === undefined)
@@ -211,8 +148,6 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
211
148
  if (!busy().busy)
212
149
  void (scheduler.tick ?? tick)();
213
150
  };
214
- // Subscriptions through the earliest members without one, the longest loaded, until there are
215
- // `WATCHERS`; never through `leaving`.
216
151
  const watch = (leaving) => {
217
152
  const watching = new Set(hub.watchers.map((watcher) => watcher.member));
218
153
  for (const member of hub.members) {
@@ -222,7 +157,6 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
222
157
  hub.watchers.push(hub.subscribe(member));
223
158
  }
224
159
  };
225
- // The work each member of this copy started and handed over, until it settles.
226
160
  const work = new WeakMap();
227
161
  const track = (member, started) => {
228
162
  const set = work.get(member) ?? new Set();
@@ -248,13 +182,9 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
248
182
  return () => {
249
183
  hub.members.delete(member);
250
184
  hub.locations.delete(member);
251
- // What this member leaves behind: the work it handed over, and a tick under way through it,
252
- // which keeps using its storage and session until it ends.
253
185
  const behind = [...(work.get(member) ?? [])];
254
186
  if (scheduler.running?.member === member)
255
187
  behind.push(scheduler.running.done);
256
- // The other subscription, already connected, follows the events meanwhile; this one is replaced
257
- // before it ends, after its current event, so the relay's watcher count never drops to none.
258
188
  const index = hub.watchers.findIndex((watcher) => watcher.member === member);
259
189
  if (index >= 0) {
260
190
  const [leaving] = hub.watchers.splice(index, 1);
@@ -265,7 +195,6 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
265
195
  if (hub.members.size === 0 && scheduler.timer !== undefined) {
266
196
  timers.stop(scheduler.timer);
267
197
  scheduler.timer = undefined;
268
- // After the tick under way, whichever copy started it, which may be writing the key.
269
198
  behind.push((async () => {
270
199
  await scheduler.running?.done;
271
200
  await release(member.later);
@@ -283,11 +212,8 @@ export function open(registry, jobs, timers = realTimers, server = SERVER) {
283
212
  nudge,
284
213
  };
285
214
  }
286
- /** This copy's hub, in the process. */
287
215
  export const hub = find(globalThis).hub;
288
- /** Opens this copy's hub in the process, with its tick jobs; see `open`. */
289
216
  export const openInProcess = (jobs) => open(globalThis, jobs);
290
- /** The member of this copy's hub set up with `location` that joined last, while it is loaded. */
291
217
  export function memberAt(location) {
292
218
  let found;
293
219
  for (const member of hub.members)
@@ -295,5 +221,4 @@ export function memberAt(location) {
295
221
  found = member;
296
222
  return found;
297
223
  }
298
- /** The permission domains of the loaded instances. */
299
224
  export const permissions = () => hub.locations.values();
@@ -1,9 +1,6 @@
1
- /** Readers for untyped JSON, such as a webhook payload or a plugin option: the value when it has the type, else nothing. */
2
1
  export declare const str: (value: unknown) => string | undefined;
3
2
  export declare const num: (value: unknown) => number | undefined;
4
3
  export declare const finite: (value: unknown) => number | undefined;
5
4
  export declare const obj: (value: unknown) => Record<string, any>;
6
- /** Epoch milliseconds from a session time, a `DateTime` in OpenCode's API and a number in tests; NaN for none. */
7
5
  export declare function millis(value: unknown): number;
8
- /** Whether an error of OpenCode's says that what it names, such as a session, does not exist (`Session.NotFoundError`). */
9
6
  export declare const isNotFound: (error: unknown) => boolean;
@@ -1,9 +1,8 @@
1
- /** Readers for untyped JSON, such as a webhook payload or a plugin option: the value when it has the type, else nothing. */
2
1
  export const str = (value) => (typeof value === "string" ? value : undefined);
3
2
  export const num = (value) => (typeof value === "number" && Number.isInteger(value) ? value : undefined);
4
3
  export const finite = (value) => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
5
4
  export const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
6
- /** Epoch milliseconds from a session time, a `DateTime` in OpenCode's API and a number in tests; NaN for none. */
5
+ // OpenCode's session times are DateTimes; tests pass numbers
7
6
  export function millis(value) {
8
7
  if (typeof value === "number")
9
8
  return value;
@@ -12,5 +11,4 @@ export function millis(value) {
12
11
  const epoch = obj(value).epochMilliseconds;
13
12
  return typeof epoch === "number" ? epoch : Number.NaN;
14
13
  }
15
- /** Whether an error of OpenCode's says that what it names, such as a session, does not exist (`Session.NotFoundError`). */
16
14
  export const isNotFound = (error) => str(obj(error)._tag)?.includes("NotFound") ?? false;
@@ -1,18 +1,11 @@
1
1
  import type { Plugin } from "@opencode/plugin";
2
2
  type Context = Plugin.Context;
3
3
  export type Storage = Pick<Context["storage"], "get" | "set" | "remove" | "scan">;
4
- /** Every value stored under `prefix`, following the scan's pages. */
5
4
  export declare function scanAll<T>(storage: Pick<Storage, "scan">, prefix: string): Promise<T[]>;
6
- /** Every key under `prefix`, without the prefix, with its value, following the scan's pages. */
7
5
  export declare function scanEntries<T>(storage: Pick<Storage, "scan">, prefix: string): Promise<{
8
6
  key: string;
9
7
  value: T;
10
8
  }[]>;
11
- /** Where process-wide values are kept: `globalThis`, or a stand-in for it in tests. */
12
9
  export type Registry = Record<symbol, unknown>;
13
- /**
14
- * The one value the whole process keeps under `Symbol.for(key)`, made by `create` the first time,
15
- * so every instance of the plugin, and every copy of it loaded later, gets the same one.
16
- */
17
10
  export declare function processWide<T>(key: string, create: () => T, registry?: Registry): T;
18
11
  export {};
@@ -1,8 +1,6 @@
1
- /** Every value stored under `prefix`, following the scan's pages. */
2
1
  export async function scanAll(storage, prefix) {
3
2
  return (await scanEntries(storage, prefix)).map((entry) => entry.value);
4
3
  }
5
- /** Every key under `prefix`, without the prefix, with its value, following the scan's pages. */
6
4
  export async function scanEntries(storage, prefix) {
7
5
  const entries = [];
8
6
  let after;
@@ -14,10 +12,6 @@ export async function scanEntries(storage, prefix) {
14
12
  } while (after);
15
13
  return entries;
16
14
  }
17
- /**
18
- * The one value the whole process keeps under `Symbol.for(key)`, made by `create` the first time,
19
- * so every instance of the plugin, and every copy of it loaded later, gets the same one.
20
- */
21
15
  export function processWide(key, create, registry = globalThis) {
22
16
  const symbol = Symbol.for(key);
23
17
  registry[symbol] ??= create();
@@ -1,16 +1,7 @@
1
1
  import type { App } from "@opencode/plugin/app";
2
- /** The versions a build of the plugin carries: its own, and the OpenCode it was built and tested against. */
3
2
  export interface BuiltVersions {
4
3
  readonly plugin: string;
5
4
  readonly opencode: string;
6
5
  }
7
- /**
8
- * Reads from the package's own package.json, next to `dist/` as to `src/`: `version` and the exact
9
- * `@opencode/plugin` devDependency version, which the peer repeats. Throws if either is missing.
10
- */
11
6
  export declare function builtVersions(): BuiltVersions;
12
- /**
13
- * The server-log line for an OpenCode other than the plugin was built against, undefined when it is;
14
- * logged once per load, never in a tool result. A context without a version is still told the pin.
15
- */
16
7
  export declare function versionNotice(built: BuiltVersions, app: Partial<Pick<App, "version" | "channel">> | undefined): string | undefined;
@@ -1,9 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  const VERSION_TABLE = "https://github.com/ivopogace/opencode-courier/blob/main/docs/reference.md#the-opencode-version";
3
- /**
4
- * Reads from the package's own package.json, next to `dist/` as to `src/`: `version` and the exact
5
- * `@opencode/plugin` devDependency version, which the peer repeats. Throws if either is missing.
6
- */
7
3
  export function builtVersions() {
8
4
  const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
9
5
  const plugin = pkg.version;
@@ -14,10 +10,6 @@ export function builtVersions() {
14
10
  throw new Error("package.json pins no @opencode/plugin under devDependencies");
15
11
  return { plugin, opencode };
16
12
  }
17
- /**
18
- * The server-log line for an OpenCode other than the plugin was built against, undefined when it is;
19
- * logged once per load, never in a tool result. A context without a version is still told the pin.
20
- */
21
13
  export function versionNotice(built, app) {
22
14
  const version = typeof app?.version === "string" ? app.version : undefined;
23
15
  if (version === built.opencode)
package/dist/rpc.js CHANGED
@@ -1,4 +1,3 @@
1
- // The package's `rpc` entrypoint: the courier's RPC contracts, for a client that calls them.
2
1
  export { treeRpc } from "./tree/rpc.js";
3
2
  export { personRpc } from "./person/rpc.js";
4
3
  export { memoryRpc } from "./memory/rpc.js";
@@ -2,5 +2,4 @@ import type { TickJob } from "../platform/hub.js";
2
2
  export { cancel, cancelScheduled, deliverDue, isScheduled, schedule, scheduledFor } from "./later.js";
3
3
  export type { LaterEntry, LaterInput, LaterPorts } from "./later.js";
4
4
  export { addSchedulingTools } from "./tools.js";
5
- /** The scheduler's tick job: due messages, through the first loaded member, whose `later` is every hub-v2 member's LaterPorts. */
6
5
  export declare const dueJob: TickJob;
@@ -1,5 +1,4 @@
1
1
  import { deliverDue } from "./later.js";
2
2
  export { cancel, cancelScheduled, deliverDue, isScheduled, schedule, scheduledFor } from "./later.js";
3
3
  export { addSchedulingTools } from "./tools.js";
4
- /** The scheduler's tick job: due messages, through the first loaded member, whose `later` is every hub-v2 member's LaterPorts. */
5
4
  export const dueJob = (owner, claimed) => deliverDue(owner.later, claimed);
@@ -11,7 +11,6 @@ export interface LaterEntry {
11
11
  }
12
12
  export interface LaterPorts {
13
13
  readonly storage: Storage;
14
- /** `get` for the release of a join group, which waits for its parent's turn to end; `get` and `context` for a missed wake (`ctx.session`). */
15
14
  readonly session: Pick<Context["session"], "synthetic" | "get" | "context">;
16
15
  readonly now: () => number;
17
16
  readonly newID: () => string;
@@ -19,26 +18,16 @@ export interface LaterPorts {
19
18
  }
20
19
  export interface LaterInput {
21
20
  readonly message: string;
22
- /** A number, or a string holding one, which some models send instead. */
23
21
  readonly delayMinutes?: number | string;
24
22
  readonly at?: string;
25
23
  readonly sessionID?: string;
26
24
  }
27
- /** Stores a message for later delivery; the scheduler in `deliverDue` sends it once it is due. */
28
25
  export declare function schedule(ports: LaterPorts, from: string, input: LaterInput): Promise<LaterEntry>;
29
- /** Drops a pending message; false when there was none, e.g. it was already delivered. */
30
26
  export declare function cancel(ports: LaterPorts, id: string): Promise<boolean>;
31
- /** The messages scheduled for a session that have not been delivered yet. */
32
27
  export declare function scheduledFor(storage: Pick<Storage, "scan">, sessionID: string): Promise<LaterEntry[]>;
33
- /** Cancels every message scheduled for a session; the ids of those cancelled. */
34
28
  export declare function cancelScheduled(ports: LaterPorts, sessionID: string): Promise<string[]>;
35
- /** Whether a session waits on a scheduled message of its own. */
36
29
  export declare function isScheduled(ports: {
37
30
  readonly storage: Pick<Storage, "scan">;
38
31
  }, sessionID: string): Promise<boolean>;
39
- /**
40
- * Delivers every due message once: an id is claimed synchronously in the shared `claimed`, re-read,
41
- * delivered and only then removed; a crash in between delivers it again after a restart, not never.
42
- */
43
32
  export declare function deliverDue(ports: LaterPorts, claimed: Set<string>): Promise<void>;
44
33
  export {};
@@ -1,12 +1,7 @@
1
1
  import { envelope } from "../kernel/protocol.js";
2
2
  import { scanAll } from "../platform/storage.js";
3
3
  const PREFIX = "later/";
4
- /** The latest time a Date can hold; a message due after it could never be shown or delivered. */
5
4
  const LATEST = 8.64e15;
6
- /**
7
- * A plain decimal number of minutes, the only string form of delayMinutes taken ("0x10" is not),
8
- * once trimmed of the whitespace `\s` matches.
9
- */
10
5
  const DECIMAL = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
11
6
  function minutesOf(given) {
12
7
  if (typeof given !== "string")
@@ -34,7 +29,6 @@ function fireTime(now, input) {
34
29
  throw new Error(`at is in the past: ${input.at}`);
35
30
  return at;
36
31
  }
37
- /** Stores a message for later delivery; the scheduler in `deliverDue` sends it once it is due. */
38
32
  export async function schedule(ports, from, input) {
39
33
  const now = ports.now();
40
34
  const entry = {
@@ -48,7 +42,6 @@ export async function schedule(ports, from, input) {
48
42
  await ports.storage.set(PREFIX + entry.id, { ...entry });
49
43
  return entry;
50
44
  }
51
- /** Drops a pending message; false when there was none, e.g. it was already delivered. */
52
45
  export async function cancel(ports, id) {
53
46
  if ((await ports.storage.get(PREFIX + id)) === undefined)
54
47
  return false;
@@ -58,24 +51,17 @@ export async function cancel(ports, id) {
58
51
  function pending(ports) {
59
52
  return scanAll(ports.storage, PREFIX);
60
53
  }
61
- /** The messages scheduled for a session that have not been delivered yet. */
62
54
  export async function scheduledFor(storage, sessionID) {
63
55
  return (await pending({ storage: storage })).filter((entry) => entry.sessionID === sessionID);
64
56
  }
65
- /** Cancels every message scheduled for a session; the ids of those cancelled. */
66
57
  export async function cancelScheduled(ports, sessionID) {
67
58
  const scheduled = await scheduledFor(ports.storage, sessionID);
68
59
  const dropped = await Promise.all(scheduled.map(async (later) => ((await cancel(ports, later.id)) ? later.id : undefined)));
69
60
  return dropped.filter((id) => id !== undefined);
70
61
  }
71
- /** Whether a session waits on a scheduled message of its own. */
72
62
  export async function isScheduled(ports, sessionID) {
73
63
  return (await scheduledFor(ports.storage, sessionID)).length > 0;
74
64
  }
75
- /**
76
- * Delivers every due message once: an id is claimed synchronously in the shared `claimed`, re-read,
77
- * delivered and only then removed; a crash in between delivers it again after a restart, not never.
78
- */
79
65
  export async function deliverDue(ports, claimed) {
80
66
  const now = ports.now();
81
67
  for (const due of (await pending(ports)).filter((entry) => entry.fireAt <= now)) {
@@ -1,4 +1,3 @@
1
- /** `own` when the message goes to the session that scheduled it. */
2
1
  export declare function laterText(entry: {
3
2
  readonly id: string;
4
3
  readonly sessionID: string;
@@ -1,4 +1,3 @@
1
- /** `own` when the message goes to the session that scheduled it. */
2
1
  export function laterText(entry, fireAt, own) {
3
2
  const next = own
4
3
  ? "It arrives when due, after your current turn if one is running, so do not wait for it: once nothing " +