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
@@ -5,29 +5,18 @@ import type { Waiting } from "./permission.js";
5
5
  export type { PermissionAnswered, PermissionRequest, Reply } from "./permission.js";
6
6
  export type { Asked, Elsewhere, Outcome, Prompt, QuestionAnswered, Withdrawal } from "./question/index.js";
7
7
  type Context = Plugin.Context;
8
- /** What the person events need of a member's `watch` ports, which are a `WatchPorts` at runtime. */
9
8
  export interface PersonPorts {
10
9
  readonly storage: RosterStorage;
11
10
  readonly session: Pick<Context["session"], "synthetic">;
12
11
  readonly event: Pick<Context["event"], "subscribe">;
13
- /**
14
- * The permission domains of every loaded location, whose pending requests are relayed when the
15
- * watcher (re)subscribes: one watcher serves them all, and an isolated child's are in its worktree's.
16
- */
17
12
  readonly permissions: () => Iterable<Pick<Context["permission"], "list">>;
18
- /** The clock a location shutdown is recorded by: the question relay's, which judges it. */
19
13
  readonly now: () => number;
20
14
  readonly log: (message: string) => void;
21
15
  }
22
- /** The forms spawned sessions and others show, which only the person can answer, by form id; not the question tool's. */
23
16
  export type FormsShown = Map<string, {
24
17
  readonly sessionID: string;
25
18
  readonly title: string;
26
19
  }>;
27
- /**
28
- * What every plugin instance in the process shares of the person events: the handled event ids (the tree's too),
29
- * the requests and forms told about or answered first, and the forms shown now.
30
- */
31
20
  export interface PersonState {
32
21
  readonly seen: Set<string>;
33
22
  readonly waiting: Waiting;
@@ -35,9 +24,7 @@ export interface PersonState {
35
24
  readonly forms: FormsTold;
36
25
  readonly shown: FormsShown;
37
26
  }
38
- /** The session at the top of a lineage: where its permission requests go, since that is where the person is. */
39
27
  export declare const topOf: (chain: RosterEntry[]) => string;
40
- /** One field of an OpenCode form, as much of it as a notice shows. */
41
28
  export interface FormField {
42
29
  readonly key: string;
43
30
  readonly type: string;
@@ -49,12 +36,9 @@ export interface FormField {
49
36
  readonly description?: string;
50
37
  }>;
51
38
  readonly url?: string;
52
- /** Not shown to the person. */
53
39
  readonly hidden?: boolean;
54
- /** Conditions on earlier answers; the field is shown only when they hold. */
55
40
  readonly when?: ReadonlyArray<unknown>;
56
41
  }
57
- /** An OpenCode form, as its `form.created` event carries it. */
58
42
  export interface Form {
59
43
  readonly id: string;
60
44
  readonly sessionID: string;
@@ -1,2 +1 @@
1
- /** The session at the top of a lineage: where its permission requests go, since that is where the person is. */
2
1
  export const topOf = (chain) => chain.at(-1).parentID;
@@ -3,7 +3,6 @@ import type { Answers, Asked, Outcome, Prompt, QuestionPorts, Withdrawal } from
3
3
  import { type CutOff } from "../text.js";
4
4
  import { type RosterStorage } from "../../tree/index.js";
5
5
  import { type QuestionAnswerInput } from "./pure.js";
6
- /** What answering a question did; `answered` is false when it no longer waited. */
7
6
  export type QuestionAnswered = {
8
7
  readonly sessionID: string;
9
8
  readonly requestID: string;
@@ -16,49 +15,22 @@ export type QuestionAnswered = {
16
15
  readonly answers: Answers;
17
16
  };
18
17
  export declare function tell(ports: QuestionPorts, asked: Asked, text: string, attributes: Record<string, string>): Promise<void>;
19
- /** Tells the top session that a question's call was cut off, once per question and process. */
20
18
  export declare function tellCutOff(ports: QuestionPorts, asked: Asked, cutOff: CutOff): Promise<void>;
21
- /**
22
- * What a linked call of the top session is withdrawn with once an answer is passed on, `withdrawal`, and
23
- * whether one was: `linked`, set once it is withdrawn.
24
- */
25
19
  export interface Linked {
26
20
  readonly withdrawal: Withdrawal;
27
21
  linked: boolean;
28
22
  }
29
- /**
30
- * Passes the top session's answer, or the person's dismissal, to a question: as the result of the
31
- * call that waits on it, or as a message when that call was cut off. Undefined when it was settled.
32
- */
33
23
  export declare const deliver: (ports: QuestionPorts, asked: Asked, outcome: Outcome, given?: Linked) => Effect.Effect<"result" | "message" | undefined, unknown>;
34
- /**
35
- * Drops a stored question whose answer went out. If that fails, it is marked answered instead, so
36
- * it is neither listed nor answered again, nor told about after a restart.
37
- */
38
24
  export declare function forget(ports: QuestionPorts, asked: Asked): Promise<void>;
39
- /**
40
- * Answers a question of a session started, directly or through others, from the caller, which must
41
- * be the session at the top, as for permission requests. `answered` is false when it no longer waits.
42
- */
43
25
  export declare function answerQuestion(ports: QuestionPorts, callerID: string, input: QuestionAnswerInput): Promise<QuestionAnswered>;
44
- /**
45
- * Answers a question for the person, from the TUI's inbox, as courier_answer does; the top session hears it once, as
46
- * answered by the person: its linked call is withdrawn, or else, if it was told of it, it is told settled.
47
- */
48
26
  export declare function answerForPerson(ports: QuestionPorts, input: QuestionAnswerInput): Promise<QuestionAnswered>;
49
- /** Whether a session waits on a question of its own, open or cut off and not yet answered. */
50
27
  export declare function asksQuestion(ports: {
51
28
  readonly storage: RosterStorage;
52
29
  }, sessionID: string): Promise<boolean>;
53
- /** The questions a session waits on, for courier_children; `stopped` when its call was cut off. */
54
30
  export declare function pendingQuestions(storage: RosterStorage, sessionID: string): Promise<{
55
31
  type: "question";
56
32
  requestID: string;
57
33
  questions: Prompt[];
58
34
  stopped?: true;
59
35
  }[]>;
60
- /**
61
- * On loading: drops stored questions that are too old, whose session is no longer on a roster, or
62
- * beyond the newest `MAX_STORED`, and tells the top session about the rest, which a restart cut off.
63
- */
64
36
  export declare function noticeCutOff(ports: QuestionPorts): Promise<void>;
@@ -15,12 +15,9 @@ export async function tell(ports, asked, text, attributes) {
15
15
  delivery: "steer",
16
16
  });
17
17
  }
18
- /** Tells the top session that a question's call was cut off, once per question and process. */
19
18
  export async function tellCutOff(ports, asked, cutOff) {
20
- // Not while an answer to it is being passed on, which makes the notice moot.
21
19
  if (shared.noticed.has(asked.requestID) || isPassing(asked.requestID))
22
20
  return;
23
- // Claimed first, so two tellers at once send one notice; given back if it did not go out.
24
21
  claim(shared.noticed, asked.requestID);
25
22
  try {
26
23
  await tell(ports, asked, questionNotice(asked, cutOff), { asks: "question", request: asked.requestID, [cutOff]: "true" });
@@ -31,12 +28,7 @@ export async function tellCutOff(ports, asked, cutOff) {
31
28
  }
32
29
  }
33
30
  const busy = (id) => new Error(`another answer to ${id} is still being passed on; try again in a while.`);
34
- /** This copy's lock per question, kept while an answer to it is passed on or waits its turn. */
35
31
  const locks = new Map();
36
- /**
37
- * Runs `effect` holding the question's lock, once it is taken; fails if that takes more than `ms`.
38
- * Only this copy of the plugin takes the lock: another copy's answer shows in `shared.passing`.
39
- */
40
32
  const holding = (id, ms, effect) => Effect.acquireUseRelease(Effect.sync(() => {
41
33
  const lock = locks.get(id) ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 };
42
34
  lock.users++;
@@ -44,27 +36,19 @@ const holding = (id, ms, effect) => Effect.acquireUseRelease(Effect.sync(() => {
44
36
  return lock;
45
37
  }), (lock) => {
46
38
  const taken = Deferred.makeUnsafe();
47
- // Whichever ends first: the effect, or the wait, which fails only if the lock was not taken by then.
48
39
  const late = Effect.andThen(Effect.sleep(ms), Effect.suspend(() => (Deferred.isDoneUnsafe(taken) ? Effect.never : Effect.fail(busy(id)))));
49
40
  return Effect.raceFirst(lock.semaphore.withPermit(Effect.andThen(Deferred.succeed(taken, undefined), effect)), late);
50
41
  }, (lock) => Effect.sync(() => {
51
42
  if (--lock.users === 0)
52
43
  locks.delete(id);
53
44
  }));
54
- /**
55
- * Passes the top session's answer, or the person's dismissal, to a question: as the result of the
56
- * call that waits on it, or as a message when that call was cut off. Undefined when it was settled.
57
- */
58
45
  export const deliver = (ports, asked, outcome, given) => Effect.suspend(() => {
59
46
  const linked = given ?? { withdrawal: { by: "top", outcome }, linked: false };
60
47
  const id = asked.requestID;
61
48
  const wait = ports.timing.passingWaitMs;
62
- // One answer at a time, and once, per question in this process, since storage cannot be claimed
63
- // atomically: a second waits, going only if the first did not; meanwhile no cut-off notice or link.
64
49
  const since = ports.now();
65
50
  const left = () => wait - (ports.now() - since);
66
51
  const turn = holding(id, wait, Effect.gen(function* () {
67
- // An answer that another copy of the plugin, loaded beside this one, passes on.
68
52
  for (let under = shared.passing.get(id); under; under = shared.passing.get(id)) {
69
53
  if (left() <= 0)
70
54
  return yield* Effect.fail(busy(id));
@@ -74,8 +58,6 @@ export const deliver = (ports, asked, outcome, given) => Effect.suspend(() => {
74
58
  return undefined;
75
59
  const passing = passOn(ports, asked, outcome, linked);
76
60
  shared.passing.set(id, passing);
77
- // Held while it is passed on; one that hangs on a notice that never returns gives way after a
78
- // while, so the question can be answered again, and the child may then be told twice.
79
61
  yield* within(passing, wait).pipe(Effect.ensuring(Effect.sync(() => {
80
62
  if (shared.passing.get(id) === passing)
81
63
  shared.passing.delete(id);
@@ -84,7 +66,6 @@ export const deliver = (ports, asked, outcome, given) => Effect.suspend(() => {
84
66
  }));
85
67
  return Effect.flatMap(turn, (passing) => passing ? Effect.tryPromise({ try: () => passing, catch: (error) => error }) : Effect.succeed(undefined));
86
68
  });
87
- /** Withdraws the top session's call linked to a question, if any, now that it is settled. */
88
69
  function withdraw(known, linked) {
89
70
  if (!known.link)
90
71
  return;
@@ -94,24 +75,18 @@ function withdraw(known, linked) {
94
75
  async function passOn(ports, asked, outcome, linked) {
95
76
  const id = asked.requestID;
96
77
  let known = shared.questions.get(id);
97
- // Taken only if the call ends with it: one already ending some other way, its turn being
98
- // stopped or the person answering in its session, does not.
99
78
  if (known?.call && (await known.call(outcome))) {
100
79
  claim(shared.answered, id);
101
80
  shared.questions.delete(id);
102
- // A call of the top session linked to it, asking the person, is withdrawn: it is settled.
103
81
  withdraw(known, linked);
104
82
  return "result";
105
83
  }
106
- // Its call just ended: whether it was cut off and stays or was settled and is dropped is known once
107
- // settle is through, from the registry, not storage, which lacks a question whose write failed.
108
84
  if (known) {
109
85
  await known.settling;
110
86
  if (shared.questions.get(id) !== known)
111
87
  return undefined;
112
88
  }
113
89
  else {
114
- // Not known to this process: stored by one before it, and not yet loaded here.
115
90
  const stored = (await ports.storage.get(keyOf(id)));
116
91
  if (!stored || stored.answered)
117
92
  return undefined;
@@ -119,18 +94,12 @@ async function passOn(ports, asked, outcome, linked) {
119
94
  shared.questions.set(id, known);
120
95
  }
121
96
  await sendAnswer(ports, asked, outcome);
122
- // Dropped only once the message is out, so a failed send leaves it to be answered again; the
123
- // linked call, if any, goes then too, not before the answer is out.
124
97
  claim(shared.answered, id);
125
98
  shared.questions.delete(id);
126
99
  withdraw(known, linked);
127
100
  await forget(ports, asked);
128
101
  return "message";
129
102
  }
130
- /**
131
- * Drops a stored question whose answer went out. If that fails, it is marked answered instead, so
132
- * it is neither listed nor answered again, nor told about after a restart.
133
- */
134
103
  export async function forget(ports, asked) {
135
104
  try {
136
105
  await ports.storage.remove(keyOf(asked.requestID));
@@ -150,10 +119,6 @@ async function sendAnswer(ports, asked, outcome) {
150
119
  delivery: "steer",
151
120
  });
152
121
  }
153
- /**
154
- * Answers a question of a session started, directly or through others, from the caller, which must
155
- * be the session at the top, as for permission requests. `answered` is false when it no longer waits.
156
- */
157
122
  export async function answerQuestion(ports, callerID, input) {
158
123
  const { sessionID, requestID } = input;
159
124
  await answeringTop(ports.storage, sessionID, callerID, "questions");
@@ -164,10 +129,6 @@ export async function answerQuestion(ports, callerID, input) {
164
129
  const by = await run(deliver(ports, asked, { answers }));
165
130
  return by ? { sessionID, requestID, answered: true, by, answers } : { sessionID, requestID, answered: false };
166
131
  }
167
- /**
168
- * Answers a question for the person, from the TUI's inbox, as courier_answer does; the top session hears it once, as
169
- * answered by the person: its linked call is withdrawn, or else, if it was told of it, it is told settled.
170
- */
171
132
  export async function answerForPerson(ports, input) {
172
133
  const { sessionID, requestID } = input;
173
134
  const known = shared.questions.get(requestID);
@@ -180,17 +141,14 @@ export async function answerForPerson(ports, input) {
180
141
  const by = await run(deliver(ports, asked, { answers }, linked));
181
142
  if (!by)
182
143
  return { sessionID, requestID, answered: false };
183
- // A question whose notice did not go out is not settled with one either; a stored one was told when cut off.
184
144
  const told = !known?.relaying || (await run(within(known.relaying, ports.timing.relayWaitMs))) === true;
185
145
  if (!linked.linked && told)
186
146
  await tell(ports, asked, questionSettledNotice(asked, settled), { answered: "elsewhere", request: requestID }).catch((error) => ports.log(`courier question: could not tell ${asked.top} that ${requestID} is settled: ${String(error)}`));
187
147
  return { sessionID, requestID, answered: true, by, answers };
188
148
  }
189
- /** Whether a session waits on a question of its own, open or cut off and not yet answered. */
190
149
  export async function asksQuestion(ports, sessionID) {
191
150
  return (await pendingQuestions(ports.storage, sessionID)).length > 0;
192
151
  }
193
- /** The questions a session waits on, for courier_children; `stopped` when its call was cut off. */
194
152
  export async function pendingQuestions(storage, sessionID) {
195
153
  const found = new Map();
196
154
  const add = (asked, stopped) => found.set(asked.requestID, {
@@ -207,10 +165,6 @@ export async function pendingQuestions(storage, sessionID) {
207
165
  add(asked, true);
208
166
  return [...found.values()];
209
167
  }
210
- /**
211
- * On loading: drops stored questions that are too old, whose session is no longer on a roster, or
212
- * beyond the newest `MAX_STORED`, and tells the top session about the rest, which a restart cut off.
213
- */
214
168
  export async function noticeCutOff(ports) {
215
169
  const stored = (await scanAll(ports.storage, PREFIX)).sort((a, b) => b.askedAt - a.askedAt);
216
170
  if (!stored.length)
@@ -218,12 +172,9 @@ export async function noticeCutOff(ports) {
218
172
  const roster = await allEntries(ports.storage);
219
173
  const now = ports.now();
220
174
  let kept = 0;
221
- // Which to keep is decided here, newest first; the drops and the notices, one per question, then
222
- // go out together.
223
175
  const work = [];
224
176
  for (const asked of stored) {
225
177
  const known = shared.questions.get(asked.requestID);
226
- // Its call waits, or its top session is asking the person: nothing to tell.
227
178
  if (known?.call || known?.link || known?.settling || shared.passing.has(asked.requestID)) {
228
179
  kept++;
229
180
  continue;
@@ -1,5 +1,3 @@
1
- // The question relay: what the rest of the plugin uses of it. The relay's state is kept in the hub
2
- // (`../../platform/hub.ts`), under a process-wide key of its own; `types.ts` types it.
3
1
  export { answerForPerson, answerQuestion, asksQuestion, noticeCutOff, pendingQuestions } from "./answer.js";
4
2
  export { eventsFollowed, eventsLeft, formShown, formsMayHaveBeenMissed, joinRelay, locationClosing, shutdownReportedAt } from "./lifecycle.js";
5
3
  export { byCodeUnit } from "./pure.js";
@@ -1,26 +1,9 @@
1
1
  import { Effect } from "effect";
2
2
  import type { QuestionPorts } from "./types.js";
3
- /**
4
- * For tests: when the location at `directory` was last reported shutting down, if at all; a shutdown
5
- * reported without a location counts for every directory, and with none given, any location's counts.
6
- */
7
3
  export declare const shutdownReportedAt: (directory?: string) => number | undefined;
8
- /**
9
- * Called for `location.shutdown`: the location at `directory` is closing, which withdraws its open
10
- * forms as if dismissed; with no location it counts for all. `at` is the watcher's clock, epoch ms.
11
- */
12
4
  export declare function locationClosing(at: number, directory?: string): void;
13
- /**
14
- * Whether a dismissal just seen at `directory` was the location closing rather than the person: it
15
- * shut down within the grace, before or after, or this instance unloads in it; unknown matches any.
16
- */
17
5
  export declare const closingSoon: (ports: QuestionPorts, loaded: () => boolean, directory: string | undefined) => Effect.Effect<boolean>;
18
- /**
19
- * Adds an instance's ports to `loaded`, which other copies tell cut-off questions through, until the
20
- * returned function is called. A location loading again forgets the shutdown recorded for it alone.
21
- */
22
6
  export declare function joinRelay(ports: QuestionPorts): () => void;
23
- /** Called for OpenCode's `form.created`: a question call's form is shown, so its question can be relayed. */
24
7
  export declare function formShown(event: {
25
8
  readonly data: {
26
9
  readonly form: {
@@ -29,15 +12,6 @@ export declare function formShown(event: {
29
12
  };
30
13
  };
31
14
  }): void;
32
- /**
33
- * Called when an event stream may have missed forms shown: every call still waiting for its form is
34
- * taken as shown; one still waiting for its permission check is relayed early, the lesser harm.
35
- */
36
15
  export declare function formsMayHaveBeenMissed(): void;
37
- /**
38
- * Called when an instance starts following OpenCode's events: a shown form is missed only while none
39
- * follows, so waiting calls are released; true tells the caller to release again once connected.
40
- */
41
16
  export declare function eventsFollowed(): boolean;
42
- /** Called when an instance's event stream ended or broke. */
43
17
  export declare function eventsLeft(): void;
@@ -1,51 +1,30 @@
1
1
  import { Deferred, Effect, Exit } from "effect";
2
2
  import { shared } from "./shared.js";
3
- /** The key of a shutdown reported without a location: it counts for every location. */
4
3
  const ANYWHERE = "";
5
- /** When the location at `directory` (any location, with none) was last reported shutting down; 0 if never. */
6
4
  const shutdownAt = (directory) => directory === undefined
7
5
  ? Math.max(0, ...shared.shutdowns.values())
8
6
  : Math.max(shared.shutdowns.get(directory) ?? 0, shared.shutdowns.get(ANYWHERE) ?? 0);
9
- /**
10
- * For tests: when the location at `directory` was last reported shutting down, if at all; a shutdown
11
- * reported without a location counts for every directory, and with none given, any location's counts.
12
- */
13
7
  export const shutdownReportedAt = (directory) => shutdownAt(directory) || undefined;
14
- /**
15
- * Called for `location.shutdown`: the location at `directory` is closing, which withdraws its open
16
- * forms as if dismissed; with no location it counts for all. `at` is the watcher's clock, epoch ms.
17
- */
18
8
  export function locationClosing(at, directory) {
19
9
  shared.shutdowns.set(directory ?? ANYWHERE, at);
20
10
  for (const wake of shared.closingWaiters)
21
11
  wake();
22
12
  }
23
- /**
24
- * Whether a dismissal just seen at `directory` was the location closing rather than the person: it
25
- * shut down within the grace, before or after, or this instance unloads in it; unknown matches any.
26
- */
27
13
  export const closingSoon = (ports, loaded, directory) => Effect.suspend(() => {
28
14
  const ms = ports.timing.dismissalGraceMs;
29
15
  const closing = () => !loaded() || ports.now() - shutdownAt(directory) <= ms;
30
16
  if (closing())
31
17
  return Effect.succeed(true);
32
18
  const closed = Deferred.makeUnsafe();
33
- // Woken by any shutdown or unload: only one that answers the question ends the wait.
34
19
  const wake = () => {
35
20
  if (closing())
36
21
  Deferred.doneUnsafe(closed, Exit.succeed(true));
37
22
  };
38
- return Effect.acquireUseRelease(
39
- // Looked at as the waiter is added, so a shutdown cannot come in between unseen.
40
- Effect.sync(() => {
23
+ return Effect.acquireUseRelease(Effect.sync(() => {
41
24
  shared.closingWaiters.add(wake);
42
25
  wake();
43
26
  }), () => (Deferred.isDoneUnsafe(closed) ? Effect.succeed(true) : Effect.raceFirst(Deferred.await(closed), Effect.as(Effect.sleep(ms), false))), () => Effect.sync(() => shared.closingWaiters.delete(wake)));
44
27
  });
45
- /**
46
- * Adds an instance's ports to `loaded`, which other copies tell cut-off questions through, until the
47
- * returned function is called. A location loading again forgets the shutdown recorded for it alone.
48
- */
49
28
  export function joinRelay(ports) {
50
29
  shared.loaded.add(ports);
51
30
  shared.shutdowns.delete(ports.directory);
@@ -55,7 +34,6 @@ export function joinRelay(ports) {
55
34
  wake();
56
35
  };
57
36
  }
58
- /** Called for OpenCode's `form.created`: a question call's form is shown, so its question can be relayed. */
59
37
  export function formShown(event) {
60
38
  const call = event.data.form.metadata?.tool?.id;
61
39
  if (typeof call !== "string")
@@ -64,19 +42,11 @@ export function formShown(event) {
64
42
  shared.shown.get(key)?.();
65
43
  shared.shown.delete(key);
66
44
  }
67
- /**
68
- * Called when an event stream may have missed forms shown: every call still waiting for its form is
69
- * taken as shown; one still waiting for its permission check is relayed early, the lesser harm.
70
- */
71
45
  export function formsMayHaveBeenMissed() {
72
46
  for (const shown of shared.shown.values())
73
47
  shown();
74
48
  shared.shown.clear();
75
49
  }
76
- /**
77
- * Called when an instance starts following OpenCode's events: a shown form is missed only while none
78
- * follows, so waiting calls are released; true tells the caller to release again once connected.
79
- */
80
50
  export function eventsFollowed() {
81
51
  const missed = shared.following === 0 && shared.followed;
82
52
  shared.following++;
@@ -85,7 +55,6 @@ export function eventsFollowed() {
85
55
  formsMayHaveBeenMissed();
86
56
  return missed;
87
57
  }
88
- /** Called when an instance's event stream ended or broke. */
89
58
  export function eventsLeft() {
90
59
  shared.following = Math.max(0, shared.following - 1);
91
60
  }
@@ -5,35 +5,24 @@ type ToolEditor = Parameters<Parameters<EffectPlugin.Context["tool"]["transform"
5
5
  type HostTool = Parameters<Parameters<ToolEditor["update"]>[1]>[0];
6
6
  export type Execute = HostTool["execute"];
7
7
  export type ToolResult = Effect.Success<ReturnType<Execute>>;
8
- /** The questions of a call, without anything the tool does not define. */
9
8
  export declare function promptsOf(input: unknown): Prompt[];
10
9
  export interface QuestionAnswerInput {
11
10
  readonly sessionID: string;
12
11
  readonly requestID: string;
13
- /** One entry per question: a label or the text the person gave, or a list of them. */
14
12
  readonly answers: ReadonlyArray<string | ReadonlyArray<string>>;
15
13
  }
16
- /** The answers in the question tool's shape, checked against the questions. */
17
14
  export declare function normalize(asked: Asked, answers: QuestionAnswerInput["answers"]): Answers;
18
- /** OpenCode's question tool dies with this when the person dismisses the question. */
19
15
  export declare const isDismissal: (cause: Cause.Cause<unknown>) => boolean;
20
- /** How a relayed call ended. */
21
16
  export type CallExit = Exit.Exit<{
22
17
  by: "child";
23
18
  result: ToolResult;
24
19
  } | {
25
20
  by: "top";
26
21
  }, unknown>;
27
- /** How a relayed call that the top session did not answer was settled in the asking session. */
28
22
  export declare function settledIn(exit: CallExit): Elsewhere;
29
- /**
30
- * Orders strings by UTF-16 code units, the default sort's order, stated: it only has to come out the
31
- * same on both sides of a comparison; a locale-aware compare may rank two different labels as equal.
32
- */
33
23
  export declare function byCodeUnit(a: string, b: string): 0 | 1 | -1;
34
24
  export declare const choices: (questions: ReadonlyArray<Prompt>) => string;
35
25
  export declare const wording: (questions: ReadonlyArray<Prompt>) => string;
36
- /** The result of a linked call withdrawn because its question was settled without it. */
37
26
  export declare function withdrawnResult(linked: Question, how: Withdrawal): {
38
27
  output: {
39
28
  answers: Answers;
@@ -1,6 +1,5 @@
1
1
  import { Cause, Exit, Result } from "effect";
2
2
  import { withdrawnText } from "../text.js";
3
- /** The questions of a call, without anything the tool does not define. */
4
3
  export function promptsOf(input) {
5
4
  const questions = input?.questions ?? [];
6
5
  return questions.map((prompt) => ({
@@ -10,7 +9,6 @@ export function promptsOf(input) {
10
9
  ...(prompt.multiple ? { multiple: true } : {}),
11
10
  }));
12
11
  }
13
- /** The answers in the question tool's shape, checked against the questions. */
14
12
  export function normalize(asked, answers) {
15
13
  if (answers.length !== asked.questions.length)
16
14
  throw new Error(`answers needs one entry per question, ${asked.questions.length} in all, not ${answers.length}.`);
@@ -26,9 +24,7 @@ const defectTag = (cause) => {
26
24
  const defect = Cause.findDefect(cause);
27
25
  return Result.isSuccess(defect) ? defect.success?._tag : undefined;
28
26
  };
29
- /** OpenCode's question tool dies with this when the person dismisses the question. */
30
27
  export const isDismissal = (cause) => defectTag(cause) === "QuestionTool.CancelledError";
31
- /** How a relayed call that the top session did not answer was settled in the asking session. */
32
28
  export function settledIn(exit) {
33
29
  if (Exit.isSuccess(exit))
34
30
  return {
@@ -39,19 +35,13 @@ export function settledIn(exit) {
39
35
  return { by: "dismissed" };
40
36
  return { by: "failed", error: String(Cause.squash(exit.cause)) };
41
37
  }
42
- /**
43
- * Orders strings by UTF-16 code units, the default sort's order, stated: it only has to come out the
44
- * same on both sides of a comparison; a locale-aware compare may rank two different labels as equal.
45
- */
46
38
  export function byCodeUnit(a, b) {
47
39
  if (a === b)
48
40
  return 0;
49
41
  return a < b ? -1 : 1;
50
42
  }
51
- // The labels in any order: the answers are labels, so a top session listing them differently still asks the same.
52
43
  export const choices = (questions) => JSON.stringify(questions.map((prompt) => [!!prompt.multiple, prompt.options.map((option) => option.label).sort(byCodeUnit)]));
53
44
  export const wording = (questions) => JSON.stringify(questions.map((prompt) => prompt.question.trim().replace(/\s+/g, " ").toLowerCase()));
54
- /** The answers that count once a linked call's question was settled without it: none unless it was answered. */
55
45
  function withdrawnAnswers(linked, how) {
56
46
  const none = () => linked.questions.map(() => []);
57
47
  switch (how.by) {
@@ -64,7 +54,6 @@ function withdrawnAnswers(linked, how) {
64
54
  return none();
65
55
  }
66
56
  }
67
- /** The result of a linked call withdrawn because its question was settled without it. */
68
57
  export function withdrawnResult(linked, how) {
69
58
  return {
70
59
  output: { answers: withdrawnAnswers(linked, how) },
@@ -1,5 +1,4 @@
1
1
  import type { Plugin as EffectPlugin } from "@opencode/plugin/effect";
2
2
  import { Effect } from "effect";
3
3
  import type { QuestionPorts } from "./types.js";
4
- /** Wraps OpenCode's question tool for the relay. A tool by that id that is missing is left alone. */
5
4
  export declare function relayQuestions(host: Pick<EffectPlugin.Context, "tool" | "location">, ports: () => QuestionPorts | undefined): Effect.Effect<import("@opencode/plugin/effect/registration").Registration, never, import("effect/Scope").Scope>;