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
@@ -6,10 +6,6 @@ import { closingSoon } from "./lifecycle.js";
6
6
  import { choices, isDismissal, normalize, promptsOf, settledIn, withdrawnResult, wording, } from "./pure.js";
7
7
  import { background, run, within } from "./runtime.js";
8
8
  import { anyLoaded, isPassing, keyOf, QUESTION_TOOL, shared, storedOf } from "./shared.js";
9
- /**
10
- * Tells the top session about a question its closing location cut off, after the closing grace of
11
- * `ports`, through any instance still loaded then; without one, the next load tells it.
12
- */
13
9
  function tellLater(ports, question) {
14
10
  const tellNow = Effect.suspend(() => {
15
11
  const later = anyLoaded();
@@ -19,7 +15,6 @@ function tellLater(ports, question) {
19
15
  });
20
16
  void background(Effect.andThen(Effect.sleep(ports.timing.closingGraceMs), tellNow), (error) => ports.log(`courier question: could not tell ${question.top} about ${question.requestID}: ${String(error)}`));
21
17
  }
22
- /** Registers a question, stores it and tells the top session; false when storing or telling failed. */
23
18
  async function relay(ports, question) {
24
19
  shared.questions.set(question.requestID, question);
25
20
  try {
@@ -31,55 +26,38 @@ async function relay(ports, question) {
31
26
  return true;
32
27
  }
33
28
  catch (error) {
34
- // The question stays registered and the call waits on: courier_children lists it, and an answer
35
- // that comes anyway, through a link or courier_answer, still reaches the call.
36
29
  ports.log(`courier question: could not tell ${question.top} about ${question.requestID}: ${String(error)}`);
37
30
  return false;
38
31
  }
39
32
  }
40
- /**
41
- * After a relayed call ended: tidies up, and tells the top session if it was settled without it.
42
- * `loaded` says whether the plugin instance that ran the call is still loaded.
43
- */
44
33
  const settle = (ports, question, exit, loaded) => Effect.gen(function* () {
45
34
  const unloaded = !loaded();
46
- // A dismissal is held from now, alongside the wait below, so the grace is not spent waiting.
47
35
  const dismissed = Exit.isFailure(exit) && isDismissal(exit.cause);
48
36
  const held = dismissed && !unloaded ? yield* Effect.forkChild(closingSoon(ports, loaded, question.directory), { startImmediately: true }) : undefined;
49
- // The top session is told first, for a while only, so what follows neither overtakes the notice
50
- // nor waits forever on one that never returns; `told` is false when it was not told, or not yet.
51
37
  const told = (yield* within(question.relaying, ports.timing.relayWaitMs)) === true;
52
- // Answered by the top session.
53
38
  if (Exit.isSuccess(exit) && exit.value.by === "top") {
54
39
  if (shared.questions.get(question.requestID) === question)
55
40
  shared.questions.delete(question.requestID);
56
41
  return yield* Effect.promise(() => forget(ports, question));
57
42
  }
58
- // Never relayed, or dropped meanwhile.
59
43
  if (shared.questions.get(question.requestID) !== question) {
60
44
  return yield* Effect.promise(() => forget(ports, question));
61
45
  }
62
- // Cut off: the turn was stopped, or the closing location withdrew its forms as if dismissed, so a
63
- // dismissal is held for a following shutdown or unload; the question stays, answered by message.
46
+ // a closing location withdraws its forms as if dismissed (api-notes)
64
47
  const closing = unloaded || !loaded() || (held !== undefined && (yield* Fiber.join(held)));
65
48
  if (Exit.isFailure(exit) && (closing || (!dismissed && Exit.hasInterrupts(exit))))
66
49
  return closing ? tellLater(ports, question) : yield* Effect.promise(() => stopped(ports, question));
67
50
  yield* Effect.promise(() => settledElsewhere(ports, question, settledIn(exit), told));
68
51
  });
69
- /** A relayed call cut off by its stopped turn: the top session is told now, unless it asks the person. */
70
52
  async function stopped(ports, question) {
71
53
  if (!question.link)
72
54
  await tellCutOff(ports, question, "stopped");
73
55
  }
74
- /** A relayed call settled in the asking session: `told` says whether the top session was told about it. */
75
56
  async function settledElsewhere(ports, question, how, told) {
76
- // Its record goes before its registration, so an answer that finds the question gone does not
77
- // find the record still there and send it on.
78
57
  await forget(ports, question);
79
58
  shared.questions.delete(question.requestID);
80
59
  if (question.link)
81
60
  return question.link(how);
82
- // A top session never told about the question is not told that it is settled either.
83
61
  if (!told)
84
62
  return;
85
63
  await tell(ports, question, questionSettledNotice(question, how), {
@@ -87,19 +65,11 @@ async function settledElsewhere(ports, question, how, told) {
87
65
  request: question.requestID,
88
66
  });
89
67
  }
90
- /**
91
- * Starts settling a relayed call that ended, kept on the question until it is through: an answer
92
- * the call did not take waits for it.
93
- */
94
68
  function startSettling(ports, question, exit, loaded) {
95
69
  question.settling = background(settle(ports, question, exit, loaded), (error) => ports.log(`courier question: could not settle ${question.requestID}: ${String(error)}`)).finally(() => {
96
70
  question.settling = undefined;
97
71
  });
98
72
  }
99
- /**
100
- * A spawned session's question call: once its form is shown the top session is told, and whichever
101
- * answer comes first ends the call, the other side's question withdrawn. Other calls run unchanged.
102
- */
103
73
  function asking(ports, directory, original) {
104
74
  return (input, context) => Effect.gen(function* () {
105
75
  const current = ports();
@@ -108,10 +78,8 @@ function asking(ports, directory, original) {
108
78
  return yield* original(input, context);
109
79
  const byTop = Deferred.makeUnsafe();
110
80
  const accepted = Deferred.makeUnsafe();
111
- // For a copy of the plugin that answers through `call`, which takes promises.
112
81
  let acceptancePromise;
113
82
  const acceptance = () => (acceptancePromise ??= Effect.runPromise(Deferred.await(accepted)));
114
- // Handed one outcome only: a second, while the first still ends the call, is not taken.
115
83
  let handed = false;
116
84
  const top = chain.at(-1).parentID;
117
85
  const question = {
@@ -122,7 +90,7 @@ function asking(ports, directory, original) {
122
90
  ...(chain.length > 1 ? { startedBy: chain[0].parentID } : {}),
123
91
  questions: promptsOf(input),
124
92
  askedAt: current.now(),
125
- // OpenCode shows a native child's form in its native root's view: when that is the top's, the person answers there.
93
+ // a native child's form shows in its native root's view (api-notes)
126
94
  ...(nativeRootOf(chain) === top ? { native: true } : {}),
127
95
  directory,
128
96
  call: (outcome) => {
@@ -133,30 +101,22 @@ function asking(ports, directory, original) {
133
101
  return acceptance();
134
102
  },
135
103
  };
136
- // The form is shown only once OpenCode's permission check for the call has passed; a call
137
- // refused there never reaches the top session.
104
+ // form.created comes only once the call's permission check passed
138
105
  const key = `${context.sessionID} ${context.id}`;
139
106
  const loaded = () => ports() === current;
140
107
  const shown = Deferred.makeUnsafe();
141
- // Called as a plain function, by this copy's watcher or another copy's.
142
108
  shared.shown.set(key, () => Deferred.doneUnsafe(shown, Exit.void));
143
109
  const fromTop = Effect.gen(function* () {
144
110
  yield* Deferred.await(shown);
145
- // Started, not waited for: a store or a notice that hangs must not keep the top session's
146
- // answer from the call. Kept on the question, so settle can wait for it.
147
111
  question.relaying = relay(current, question);
148
112
  return { by: "top", outcome: yield* Deferred.await(byTop) };
149
113
  });
150
- const ended = yield* Effect.raceFirst(original(input, context).pipe(Effect.map((result) => ({ by: "child", result }))), fromTop).pipe(
151
- // Not waited for, so storing or a notice that hangs cannot hold the call up.
152
- Effect.onExit((exit) => Effect.sync(() => {
114
+ const ended = yield* Effect.raceFirst(original(input, context).pipe(Effect.map((result) => ({ by: "child", result }))), fromTop).pipe(Effect.onExit((exit) => Effect.sync(() => {
153
115
  shared.shown.delete(key);
154
- // The call no longer waits, whatever settle makes of it: an answer from now on waits for settle.
155
116
  const byTopSession = Exit.isSuccess(exit) && exit.value.by === "top";
156
117
  if (!byTopSession)
157
118
  question.call = undefined;
158
119
  startSettling(current, question, exit, loaded);
159
- // After settling is set, which an answer the call did not take then waits for.
160
120
  Deferred.doneUnsafe(accepted, Exit.succeed(byTopSession));
161
121
  })));
162
122
  if (ended.by === "child")
@@ -175,12 +135,7 @@ function asking(ports, directory, original) {
175
135
  };
176
136
  });
177
137
  }
178
- /** Whether the top session's question call may be linked to a question: not a native child's while its own form is shown. */
179
138
  const linkable = (question, sessionID) => question.top === sessionID && !question.link && !isPassing(question.requestID) && !(question.native && question.call);
180
- /**
181
- * The waiting question a top session's call asks again, the oldest if several: the same question with
182
- * the same choices, in any order. Choices alone would let an unrelated yes-or-no answer a child's.
183
- */
184
139
  function linkFor(sessionID, questions) {
185
140
  const asked = { choices: choices(questions), wording: wording(questions) };
186
141
  return [...shared.questions.values()]
@@ -189,10 +144,6 @@ function linkFor(sessionID, questions) {
189
144
  wording(question.questions) === asked.wording)
190
145
  .sort((a, b) => a.askedAt - b.askedAt)[0];
191
146
  }
192
- /**
193
- * The result of a top session's question call that no waiting question was linked to: when one waits
194
- * with the same choices, reworded or asked earlier, it says how to pass the answers on.
195
- */
196
147
  function unlinkedResult(result, sessionID, questions) {
197
148
  if (typeof result.content !== "string")
198
149
  return result;
@@ -202,10 +153,6 @@ function unlinkedResult(result, sessionID, questions) {
202
153
  return result;
203
154
  return { ...result, content: `${result.content}\n${unlinkedNote(similar)}` };
204
155
  }
205
- /**
206
- * After a linked call failed with its location still loaded: a dismissal by the person dismisses the
207
- * child's question too, or is logged if answered meanwhile; a stop while cut off tells the top.
208
- */
209
156
  const linkedCallFailed = (ports, linked, cause, loaded, directory) => Effect.gen(function* () {
210
157
  if (isDismissal(cause)) {
211
158
  if (yield* closingSoon(ports, loaded, directory))
@@ -216,12 +163,7 @@ const linkedCallFailed = (ports, linked, cause, loaded, directory) => Effect.gen
216
163
  else if (!linked.call && shared.questions.get(linked.requestID) === linked)
217
164
  yield* Effect.promise(() => tellCutOff(ports, linked, "stopped"));
218
165
  });
219
- /** Logs that what happened to a linked call could not be passed on. */
220
166
  const notPassedOn = (ports, linked) => (error) => ports.log(`courier question: could not pass on what happened to ${linked.requestID}: ${String(error)}`);
221
- /**
222
- * The question call of a session that a waiting question was relayed to, asking the person the
223
- * same: what they choose is passed on, and the call is withdrawn if the question is settled first.
224
- */
225
167
  function linking(ports, directory, ask) {
226
168
  return (input, context) => Effect.gen(function* () {
227
169
  const current = ports();
@@ -233,16 +175,14 @@ function linking(ports, directory, ask) {
233
175
  }
234
176
  const loaded = () => ports() === current;
235
177
  const elsewhere = Deferred.makeUnsafe();
236
- // Called as a plain function, by this copy or another one.
237
178
  const settled = (how) => void Deferred.doneUnsafe(elsewhere, Exit.succeed(how));
238
179
  linked.link = settled;
239
180
  const ended = yield* Effect.raceFirst(ask(input, context).pipe(Effect.map((result) => ({ by: "person", result }))), Deferred.await(elsewhere).pipe(Effect.map((how) => ({ by: "elsewhere", how })))).pipe(Effect.onExit((exit) => Effect.sync(() => {
240
181
  if (linked.link === settled)
241
182
  linked.link = undefined;
242
- // Unloaded: OpenCode is closing the location, which withdraws the form; nobody dismissed it.
183
+ // unloaded: OpenCode withdrew the form as it closed the location
243
184
  if (Exit.isSuccess(exit) || !loaded())
244
185
  return;
245
- // Not waited for, as in asking.
246
186
  void background(linkedCallFailed(current, linked, exit.cause, loaded, directory), notPassedOn(current, linked));
247
187
  })));
248
188
  if (ended.by === "elsewhere")
@@ -263,9 +203,7 @@ function linking(ports, directory, ask) {
263
203
  };
264
204
  });
265
205
  }
266
- /** Wraps OpenCode's question tool for the relay. A tool by that id that is missing is left alone. */
267
206
  export function relayQuestions(host, ports) {
268
- // The wrapped tool runs the calls of the host's location: its shutdown is what withdraws their forms.
269
207
  const { directory } = host.location;
270
208
  return host.tool.transform((editor) => {
271
209
  editor.update(QUESTION_TOOL, (tool) => {
@@ -1,20 +1,6 @@
1
1
  import { Clock, Effect } from "effect";
2
- /**
3
- * Runs a relay effect in its own fiber by the relay's clock (`ports.now`): the promise settles as the
4
- * effect ends, rejecting on failure or interruption; interrupting the waiter does not stop the fiber.
5
- */
6
2
  export declare function run<A, E>(effect: Effect.Effect<A, E>): Promise<A>;
7
- /**
8
- * Runs an effect of the relay in the background, as `run` does; the promise resolves once it ended,
9
- * however it ended. A failure is logged with `failed`; an interruption, by a test ending, is not.
10
- */
11
3
  export declare function background(effect: Effect.Effect<unknown, unknown>, failed: (error: unknown) => void): Promise<void>;
12
- /** The promise's value, or undefined once `ms` have passed or it failed. */
13
4
  export declare const within: <T>(promise: Promise<T> | undefined, ms: number) => Effect.Effect<T | undefined>;
14
- /**
15
- * For tests: the clock the relay runs by from now on, a `TestClock` whose time `ports.now` reads,
16
- * or the system clock again with none.
17
- */
18
5
  export declare function useClock(next?: Clock.Clock): void;
19
- /** For tests: interrupts every fiber the relay started, and goes back to the system clock. */
20
6
  export declare function stopRelay(): Promise<void>;
@@ -1,13 +1,6 @@
1
1
  import { Cause, Clock, Duration, Effect, Exit, Fiber, Option, Scope } from "effect";
2
- // What the relay's own fibers run with: this copy's state, not the hub's. A copy of the plugin
3
- // loaded next to this one has its own, and they meet only in the shared fields of the hub.
4
2
  const system = Effect.runSync(Clock.clockWith(Effect.succeed));
5
- /** Largest delay a timer takes; a longer one would fire at once, so it is slept in parts. */
6
3
  const MAX_TIMER_MS = 2_147_483_647;
7
- /**
8
- * The system clock, whose sleeps do not keep the process running: OpenCode exits without waiting
9
- * for a dismissal held, a notice told later or an answer waiting its turn.
10
- */
11
4
  const live = {
12
5
  currentTimeMillisUnsafe: () => system.currentTimeMillisUnsafe(),
13
6
  currentTimeMillis: system.currentTimeMillis,
@@ -25,47 +18,29 @@ function sleep(ms) {
25
18
  const rest = ms > MAX_TIMER_MS ? Effect.suspend(() => sleep(ms - MAX_TIMER_MS)) : Effect.void;
26
19
  return Effect.callback((resume) => {
27
20
  const timer = setTimeout(() => resume(rest), Math.min(ms, MAX_TIMER_MS));
21
+ // unref'd: OpenCode exits without waiting for the relay's timers
28
22
  timer.unref?.();
29
23
  return Effect.sync(() => clearTimeout(timer));
30
24
  });
31
25
  }
32
26
  let clock = live;
33
- /** The fibers the relay starts, which run as long as the process unless a test ends them. */
34
27
  let scope = Scope.makeUnsafe();
35
- /**
36
- * Starts an effect of the relay in a fiber of its own, by the relay's clock, in the relay's scope:
37
- * at once, up to its first wait, as an async function would.
38
- */
39
28
  const start = (effect) => Effect.runFork(Effect.provideService(effect, Clock.Clock, clock)).pipe(Fiber.runIn(scope));
40
- /**
41
- * Runs a relay effect in its own fiber by the relay's clock (`ports.now`): the promise settles as the
42
- * effect ends, rejecting on failure or interruption; interrupting the waiter does not stop the fiber.
43
- */
44
29
  export function run(effect) {
45
30
  return Effect.runPromise(Fiber.join(start(effect)));
46
31
  }
47
- /**
48
- * Runs an effect of the relay in the background, as `run` does; the promise resolves once it ended,
49
- * however it ended. A failure is logged with `failed`; an interruption, by a test ending, is not.
50
- */
51
32
  export function background(effect, failed) {
52
33
  return Effect.runPromise(Fiber.await(start(effect))).then((exit) => {
53
34
  if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause))
54
35
  failed(Cause.squash(exit.cause));
55
36
  });
56
37
  }
57
- /** The promise's value, or undefined once `ms` have passed or it failed. */
58
38
  export const within = (promise, ms) => promise
59
39
  ? Effect.promise(() => promise.then((value) => value, () => undefined)).pipe(Effect.timeoutOption(ms), Effect.map(Option.getOrUndefined))
60
40
  : Effect.succeed(undefined);
61
- /**
62
- * For tests: the clock the relay runs by from now on, a `TestClock` whose time `ports.now` reads,
63
- * or the system clock again with none.
64
- */
65
41
  export function useClock(next) {
66
42
  clock = next ?? live;
67
43
  }
68
- /** For tests: interrupts every fiber the relay started, and goes back to the system clock. */
69
44
  export async function stopRelay() {
70
45
  const ended = scope;
71
46
  scope = Scope.makeUnsafe();
@@ -1,34 +1,17 @@
1
1
  import type { Registry } from "../../platform/storage.js";
2
2
  import type { Asked, Question, QuestionPorts, QuestionState } from "./types.js";
3
- /** OpenCode's own question tool, which the relay wraps. */
4
3
  export declare const QUESTION_TOOL = "question";
5
4
  export declare const PREFIX = "question/";
6
- /** At most this many questions are kept in storage; the oldest go first. */
7
5
  export declare const MAX_STORED = 100;
8
- /** A question as stored; `answered` marks one whose answer went out but that could not be dropped. */
9
6
  export type Stored = Asked & {
10
7
  readonly answered?: true;
11
8
  };
12
9
  export declare const storedOf: ({ directory: _directory, call: _call, link: _link, relaying: _relaying, settling: _settling, ...asked }: Question) => Asked;
13
10
  export declare const isPassing: (requestID: string) => boolean;
14
- /**
15
- * The relay's state in `registry`, the hub's `questions`, with the fields it lacks made: field by
16
- * field, since a copy before the hub may have made it without the fields added later.
17
- */
18
11
  export declare function questionState(registry?: Registry): QuestionState;
19
12
  export declare const shared: QuestionState;
20
- /**
21
- * The ports of an instance still loaded, if any: a member of this copy's hub, or else one that a
22
- * copy of another hub version, or a release before the hub, keeps in `loaded`.
23
- */
24
13
  export declare const anyLoaded: () => QuestionPorts | undefined;
25
- /**
26
- * The question ports of the member that just joined at `location`, while it is loaded: resolved once,
27
- * so a later instance at the same location does not take its place while this one is still loaded.
28
- */
29
14
  export declare function portsAt(location: object): () => QuestionPorts | undefined;
30
15
  export declare const keyOf: (requestID: string) => string;
31
- /** Whether a request id is a question's rather than a permission request's. */
32
16
  export declare const isQuestion: (requestID: string) => boolean;
33
- /** Adds to one of the shared bounded sets. */
34
17
  export declare const claim: (set: Set<string>, value: string) => boolean;
@@ -1,16 +1,10 @@
1
1
  import { addBounded } from "../../platform/bounded.js";
2
2
  import { hub, memberAt, sharedState } from "../../platform/hub.js";
3
- /** OpenCode's own question tool, which the relay wraps. */
4
3
  export const QUESTION_TOOL = "question";
5
4
  export const PREFIX = "question/";
6
- /** At most this many questions are kept in storage; the oldest go first. */
7
5
  export const MAX_STORED = 100;
8
6
  export const storedOf = ({ directory: _directory, call: _call, link: _link, relaying: _relaying, settling: _settling, ...asked }) => asked;
9
7
  export const isPassing = (requestID) => shared.passing.has(requestID) || shared.answered.has(requestID);
10
- /**
11
- * The relay's state in `registry`, the hub's `questions`, with the fields it lacks made: field by
12
- * field, since a copy before the hub may have made it without the fields added later.
13
- */
14
8
  export function questionState(registry) {
15
9
  const state = sharedState("questions", () => ({}), registry);
16
10
  state.questions ??= new Map();
@@ -25,28 +19,14 @@ export function questionState(registry) {
25
19
  state.closingWaiters ??= new Set();
26
20
  return state;
27
21
  }
28
- // Shared by every plugin instance in the process, since a child and the session it asks can be in
29
- // different locations.
30
22
  export const shared = questionState();
31
- /** A member's question ports: every hub-v2 member's `questions` are its instance's, whichever copy joined it. */
32
23
  const portsOf = (member) => member?.questions;
33
- /**
34
- * The ports of an instance still loaded, if any: a member of this copy's hub, or else one that a
35
- * copy of another hub version, or a release before the hub, keeps in `loaded`.
36
- */
37
24
  export const anyLoaded = () => portsOf(hub.members.values().next().value) ?? shared.loaded.values().next().value;
38
- /**
39
- * The question ports of the member that just joined at `location`, while it is loaded: resolved once,
40
- * so a later instance at the same location does not take its place while this one is still loaded.
41
- */
42
25
  export function portsAt(location) {
43
26
  const own = memberAt(location);
44
27
  return () => (own && hub.members.has(own) ? portsOf(own) : undefined);
45
28
  }
46
29
  export const keyOf = (requestID) => `${PREFIX}${requestID}`;
47
- /** Whether a request id is a question's rather than a permission request's. */
48
30
  export const isQuestion = (requestID) => requestID.startsWith("question_");
49
- /** How many noticed and answered questions are remembered. */
50
31
  const REMEMBERED = 1_000;
51
- /** Adds to one of the shared bounded sets. */
52
32
  export const claim = (set, value) => addBounded(set, value, REMEMBERED);
@@ -1,7 +1,6 @@
1
1
  import type { Plugin } from "@opencode/plugin";
2
2
  import type { Storage } from "../../platform/storage.js";
3
3
  type Context = Plugin.Context;
4
- /** One question of OpenCode's question tool, as the asking model wrote it. */
5
4
  export interface Prompt {
6
5
  readonly question: string;
7
6
  readonly header: string;
@@ -11,26 +10,17 @@ export interface Prompt {
11
10
  }>;
12
11
  readonly multiple?: boolean;
13
12
  }
14
- /** The answers to a question call, as the question tool returns them: one list per question. */
15
13
  export type Answers = string[][];
16
- /** A question a spawned session asked with its question tool, stored until it is answered. */
17
14
  export interface Asked {
18
15
  readonly requestID: string;
19
16
  readonly sessionID: string;
20
- /** The session told, at the top of the sessions started with courier_spawn; it alone answers. */
21
17
  readonly top: string;
22
18
  readonly title: string;
23
- /** The session that started the asking one, when that is not the top session itself. */
24
19
  readonly startedBy?: string;
25
20
  readonly questions: ReadonlyArray<Prompt>;
26
21
  readonly askedAt: number;
27
- /** A native child's, whose form OpenCode shows the person in the top session's own view; not linked while shown. */
28
22
  readonly native?: true;
29
23
  }
30
- /**
31
- * How a question was settled without the top session: in the asking session (or, `via` "inbox", by the
32
- * person in the TUI's asks inbox), or not at all.
33
- */
34
24
  export type Elsewhere = {
35
25
  readonly by: "child";
36
26
  readonly answers: Answers;
@@ -41,39 +31,22 @@ export type Elsewhere = {
41
31
  readonly by: "failed";
42
32
  readonly error: string;
43
33
  };
44
- /** What the top session's answer does to a call that waits: answers it, or dismisses it. */
45
34
  export type Outcome = {
46
35
  readonly answers: Answers;
47
36
  } | {
48
37
  readonly dismissed: true;
49
38
  };
50
- /**
51
- * Why a linked call is withdrawn: its question was settled elsewhere, or the top session's outcome
52
- * reached it another way (courier_answer, or a dismissal through an earlier call linked to it).
53
- */
54
39
  export type Withdrawal = Elsewhere | {
55
40
  readonly by: "top";
56
41
  readonly outcome: Outcome;
57
42
  };
58
- /**
59
- * A question this process knows: its call waits (`call`), or was cut off and is answered by message.
60
- * `link` withdraws the top session's call passing the answer on, once settled without that call.
61
- */
62
43
  export interface Question extends Asked {
63
- /** The directory of the location the call runs in, whose shutdown withdraws its form. */
64
44
  directory?: string;
65
- /** Hands the waiting call the top session's outcome; true once the call has ended with it. */
66
45
  call?: (outcome: Outcome) => Promise<boolean>;
67
46
  link?: (how: Withdrawal) => void;
68
- /** Storing the question and telling the top session, while that is under way. */
69
47
  relaying?: Promise<boolean>;
70
- /** Working out what became of it once its call ended without the top session's answer. */
71
48
  settling?: Promise<void>;
72
49
  }
73
- /**
74
- * The relay's timings, carried by its ports: the closing grace before a cut-off question is told, the
75
- * wait for an answer still being passed on, the wait for a notice to go out, and the dismissal hold.
76
- */
77
50
  export interface QuestionTiming {
78
51
  readonly closingGraceMs: number;
79
52
  readonly passingWaitMs: number;
@@ -83,39 +56,22 @@ export interface QuestionTiming {
83
56
  export interface QuestionPorts {
84
57
  readonly storage: Storage;
85
58
  readonly session: Pick<Context["session"], "synthetic">;
86
- /** The directory of the instance's location. */
87
59
  readonly directory: string;
88
- /** The clock, in epoch milliseconds, as OpenCode's location shutdowns are recorded. */
89
60
  readonly now: () => number;
90
61
  readonly timing: QuestionTiming;
91
62
  readonly newID: () => string;
92
63
  readonly log: (message: string) => void;
93
64
  }
94
- /**
95
- * The question relay's state, shared by every instance, as a child and the session it asks can be in
96
- * different locations: open questions, cut-off ones already told, and calls waiting for their form.
97
- */
98
65
  export interface QuestionState {
99
66
  readonly questions: Map<string, Question>;
100
67
  readonly noticed: Set<string>;
101
68
  readonly shown: Map<string, () => void>;
102
- /** The questions whose answer is being passed on, each until that is done, and those whose answer went out. */
103
69
  readonly passing: Map<string, Promise<unknown>>;
104
70
  readonly answered: Set<string>;
105
- /**
106
- * The ports of every loaded instance of every copy. A release before the hub picks one here to tell
107
- * a cut-off question through, so every copy fills it; this copy reads it only for another copy.
108
- */
109
71
  readonly loaded: Set<QuestionPorts>;
110
- /** How many instances follow OpenCode's events now, and whether any has since the process started. */
111
72
  following: number;
112
73
  followed: boolean;
113
- /**
114
- * When OpenCode last reported each location shutting down (`location.shutdown`), by directory
115
- * in epoch milliseconds by the clock of the watcher that saw it; under `""` when the event named no location.
116
- */
117
74
  readonly shutdowns: Map<string, number>;
118
- /** Woken when a location shuts down or an instance unloads: dismissals held to see whether one follows. */
119
75
  readonly closingWaiters: Set<() => void>;
120
76
  }
121
77
  export {};
@@ -2,23 +2,16 @@ import type { Plugin } from "@opencode/plugin";
2
2
  import { type Permissions } from "./permission.js";
3
3
  import { type QuestionPorts } from "./question/index.js";
4
4
  import { type Answered, type Asks } from "./rpc.js";
5
- /** What the person's RPC reads and answers through: the relay's ports, which carry the storage, and every location's permissions. */
6
5
  export interface PersonRpcPorts {
7
6
  readonly questions: QuestionPorts;
8
7
  readonly permissions: () => Iterable<Permissions>;
9
8
  }
10
- /**
11
- * The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
12
- * roster from the top of its lineage down, so each session of the tree sees the same inbox.
13
- */
14
9
  export declare function asks(ports: PersonRpcPorts, sessionID: string): Promise<Asks>;
15
- /** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
16
10
  export declare function answer(ports: PersonRpcPorts, input: {
17
11
  sessionID: string;
18
12
  requestID: string;
19
13
  answers: ReadonlyArray<ReadonlyArray<string>>;
20
14
  }): Promise<Answered>;
21
- /** Serves the person's RPC at the plugin's location; disposed with the plugin. */
22
15
  export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "register">, ports: PersonRpcPorts): Promise<import("@opencode/plugin/promise/rpc").RpcRegistration<{
23
16
  readonly id: "courier-person";
24
17
  readonly methods: {
@@ -2,10 +2,6 @@ import { lineage, subtree } from "../tree/index.js";
2
2
  import { ownRequests, pendingOf } from "./permission.js";
3
3
  import { answerForPerson, pendingQuestions } from "./question/index.js";
4
4
  import { personRpc } from "./rpc.js";
5
- /**
6
- * The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
7
- * roster from the top of its lineage down, so each session of the tree sees the same inbox.
8
- */
9
5
  export async function asks(ports, sessionID) {
10
6
  const { storage } = ports.questions;
11
7
  const top = (await lineage(storage, sessionID)).at(-1)?.parentID ?? sessionID;
@@ -53,7 +49,6 @@ export async function asks(ports, sessionID) {
53
49
  }))),
54
50
  };
55
51
  }
56
- /** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
57
52
  export async function answer(ports, input) {
58
53
  try {
59
54
  const result = await answerForPerson(ports.questions, input);
@@ -63,7 +58,6 @@ export async function answer(ports, input) {
63
58
  return { answered: false, problem: error instanceof Error ? error.message : String(error) };
64
59
  }
65
60
  }
66
- /** Serves the person's RPC at the plugin's location; disposed with the plugin. */
67
61
  export function registerPersonRpc(rpc, ports) {
68
62
  return rpc.register(personRpc, {
69
63
  asks: ({ sessionID }) => asks(ports, sessionID),
@@ -1,28 +1,22 @@
1
1
  import { Schema } from "effect";
2
- /** A session of the tree, as the inbox names where an ask comes from. */
3
2
  declare const Session: Schema.Struct<{
4
3
  readonly sessionID: Schema.String;
5
4
  readonly title: Schema.String;
6
5
  readonly parentID: Schema.String;
7
- /** A child of the tree's top session is at depth 1. */
8
6
  readonly depth: Schema.Int;
9
7
  readonly group: Schema.optionalKey<Schema.String>;
10
8
  }>;
11
- /** A permission request a session of the tree, the top among them, waits on. */
12
9
  declare const Permission: Schema.Struct<{
13
10
  readonly sessionID: Schema.String;
14
11
  readonly requestID: Schema.String;
15
12
  readonly action: Schema.String;
16
13
  readonly resources: Schema.$Array<Schema.String>;
17
- /** What "always" would allow from now on; absent when it is not offered. */
18
14
  readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
19
- /** The request below that allowing this one passes on: the top's courier_answer prompt. */
20
15
  readonly passesOn: Schema.optionalKey<Schema.Struct<{
21
16
  readonly sessionID: Schema.String;
22
17
  readonly requestID: Schema.String;
23
18
  }>>;
24
19
  }>;
25
- /** A question a session of the tree waits on; `stopped` when its call was cut off and the answer goes as a message. */
26
20
  declare const Question: Schema.Struct<{
27
21
  readonly sessionID: Schema.String;
28
22
  readonly requestID: Schema.String;
@@ -37,28 +31,21 @@ declare const Question: Schema.Struct<{
37
31
  }>>;
38
32
  readonly stopped: Schema.optionalKey<Schema.Boolean>;
39
33
  }>;
40
- /** What the sessions of a courier tree wait on the person for, but forms, which the TUI lists itself. */
41
34
  declare const Asks: Schema.Struct<{
42
- /** The session at the top of the tree, which the sessions below were started from. */
43
35
  readonly top: Schema.String;
44
- /** The sessions below the top, in the order a tree is drawn. */
45
36
  readonly sessions: Schema.$Array<Schema.Struct<{
46
37
  readonly sessionID: Schema.String;
47
38
  readonly title: Schema.String;
48
39
  readonly parentID: Schema.String;
49
- /** A child of the tree's top session is at depth 1. */
50
40
  readonly depth: Schema.Int;
51
41
  readonly group: Schema.optionalKey<Schema.String>;
52
42
  }>>;
53
- /** The top session's own requests first, then those of the sessions below. */
54
43
  readonly permissions: Schema.$Array<Schema.Struct<{
55
44
  readonly sessionID: Schema.String;
56
45
  readonly requestID: Schema.String;
57
46
  readonly action: Schema.String;
58
47
  readonly resources: Schema.$Array<Schema.String>;
59
- /** What "always" would allow from now on; absent when it is not offered. */
60
48
  readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
61
- /** The request below that allowing this one passes on: the top's courier_answer prompt. */
62
49
  readonly passesOn: Schema.optionalKey<Schema.Struct<{
63
50
  readonly sessionID: Schema.String;
64
51
  readonly requestID: Schema.String;
@@ -83,7 +70,6 @@ export type Asks = typeof Asks.Type;
83
70
  export type AskedQuestion = typeof Question.Type;
84
71
  export type AskedPermission = typeof Permission.Type;
85
72
  export type TreeSession = typeof Session.Type;
86
- /** What answering a question did; `problem` says why nothing was passed on, when it was refused. */
87
73
  declare const Answered: Schema.Struct<{
88
74
  readonly answered: Schema.Boolean;
89
75
  readonly by: Schema.optionalKey<Schema.Literals<readonly ["result", "message"]>>;
@@ -170,26 +156,20 @@ export declare const personRpc: {
170
156
  readonly stopped?: boolean | undefined;
171
157
  }[];
172
158
  }> & Schema.Struct<{
173
- /** The session at the top of the tree, which the sessions below were started from. */
174
159
  readonly top: Schema.String;
175
- /** The sessions below the top, in the order a tree is drawn. */
176
160
  readonly sessions: Schema.$Array<Schema.Struct<{
177
161
  readonly sessionID: Schema.String;
178
162
  readonly title: Schema.String;
179
163
  readonly parentID: Schema.String;
180
- /** A child of the tree's top session is at depth 1. */
181
164
  readonly depth: Schema.Int;
182
165
  readonly group: Schema.optionalKey<Schema.String>;
183
166
  }>>;
184
- /** The top session's own requests first, then those of the sessions below. */
185
167
  readonly permissions: Schema.$Array<Schema.Struct<{
186
168
  readonly sessionID: Schema.String;
187
169
  readonly requestID: Schema.String;
188
170
  readonly action: Schema.String;
189
171
  readonly resources: Schema.$Array<Schema.String>;
190
- /** What "always" would allow from now on; absent when it is not offered. */
191
172
  readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
192
- /** The request below that allowing this one passes on: the top's courier_answer prompt. */
193
173
  readonly passesOn: Schema.optionalKey<Schema.Struct<{
194
174
  readonly sessionID: Schema.String;
195
175
  readonly requestID: Schema.String;