opencode-courier 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -193
- package/dist/index.d.ts +4 -8
- package/dist/index.js +66 -85
- package/dist/kernel/protocol.d.ts +20 -0
- package/dist/kernel/protocol.js +36 -0
- package/dist/kernel/tool.d.ts +5 -0
- package/dist/kernel/tool.js +5 -0
- package/dist/person/events.d.ts +23 -0
- package/dist/person/events.js +113 -0
- package/dist/person/form.d.ts +26 -0
- package/dist/person/form.js +64 -0
- package/dist/person/index.d.ts +17 -0
- package/dist/person/index.js +5 -0
- package/dist/{relay.d.ts → person/permission.d.ts} +29 -25
- package/dist/{relay.js → person/permission.js} +11 -9
- package/dist/person/ports.d.ts +58 -0
- package/dist/person/ports.js +2 -0
- package/dist/{question → person/question}/answer.d.ts +33 -4
- package/dist/{question → person/question}/answer.js +49 -17
- package/dist/person/question/index.d.ts +7 -0
- package/dist/{question → person/question}/index.js +3 -3
- package/dist/person/question/lifecycle.d.ts +43 -0
- package/dist/{question → person/question}/lifecycle.js +10 -19
- package/dist/{question → person/question}/pure.d.ts +4 -6
- package/dist/{question → person/question}/pure.js +3 -4
- package/dist/{question → person/question}/relay.d.ts +1 -1
- package/dist/{question → person/question}/relay.js +27 -35
- package/dist/{question → person/question}/runtime.d.ts +2 -3
- package/dist/{question → person/question}/runtime.js +2 -3
- package/dist/{question → person/question}/shared.d.ts +13 -3
- package/dist/person/question/shared.js +52 -0
- package/dist/person/question/types.d.ts +121 -0
- package/dist/person/question/types.js +1 -0
- package/dist/person/rpc-handlers.d.ts +160 -0
- package/dist/person/rpc-handlers.js +62 -0
- package/dist/person/rpc.d.ts +227 -0
- package/dist/person/rpc.js +70 -0
- package/dist/person/text.d.ts +68 -0
- package/dist/{notices.js → person/text.js} +53 -190
- package/dist/person/tools.d.ts +20 -0
- package/dist/person/tools.js +55 -0
- package/dist/person/tui.d.ts +46 -0
- package/dist/platform/bounded.d.ts +12 -0
- package/dist/{bounded.js → platform/bounded.js} +6 -4
- package/dist/platform/events.d.ts +34 -0
- package/dist/platform/events.js +121 -0
- package/dist/platform/hub.d.ts +195 -0
- package/dist/{hub.js → platform/hub.js} +78 -119
- package/dist/platform/json.d.ts +8 -0
- package/dist/platform/json.js +15 -0
- package/dist/platform/version.d.ts +16 -0
- package/dist/{version.js → platform/version.js} +5 -11
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.js +3 -0
- package/dist/scheduling/index.d.ts +6 -0
- package/dist/scheduling/index.js +5 -0
- package/dist/{later.d.ts → scheduling/later.d.ts} +13 -9
- package/dist/{later.js → scheduling/later.js} +18 -9
- package/dist/scheduling/text.d.ts +6 -0
- package/dist/scheduling/text.js +10 -0
- package/dist/scheduling/tools.d.ts +14 -0
- package/dist/scheduling/tools.js +48 -0
- package/dist/skill.d.ts +17 -0
- package/dist/skill.js +62 -0
- package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +5 -26
- package/dist/{cleanup.js → tree/cleanup.js} +20 -15
- package/dist/tree/context.d.ts +15 -0
- package/dist/tree/context.js +34 -0
- package/dist/tree/domain.d.ts +101 -0
- package/dist/tree/domain.js +68 -0
- package/dist/tree/events.d.ts +71 -0
- package/dist/tree/events.js +204 -0
- package/dist/tree/forget.d.ts +24 -0
- package/dist/tree/forget.js +59 -0
- package/dist/tree/group.d.ts +82 -0
- package/dist/tree/group.js +203 -0
- package/dist/tree/index.d.ts +10 -0
- package/dist/tree/index.js +11 -0
- package/dist/tree/limits.d.ts +10 -0
- package/dist/tree/limits.js +60 -0
- package/dist/tree/panel.d.ts +13 -0
- package/dist/tree/panel.js +75 -0
- package/dist/tree/ports.d.ts +276 -0
- package/dist/tree/ports.js +1 -0
- package/dist/tree/report.d.ts +23 -0
- package/dist/tree/report.js +33 -0
- package/dist/tree/roster.d.ts +27 -0
- package/dist/tree/roster.js +62 -0
- package/dist/tree/rpc-handlers.d.ts +201 -0
- package/dist/tree/rpc-handlers.js +20 -0
- package/dist/tree/rpc.d.ts +344 -0
- package/dist/tree/rpc.js +83 -0
- package/dist/tree/send.d.ts +14 -0
- package/dist/tree/send.js +99 -0
- package/dist/tree/spawn.d.ts +21 -0
- package/dist/tree/spawn.js +173 -0
- package/dist/tree/status.d.ts +128 -0
- package/dist/tree/status.js +62 -0
- package/dist/tree/stop.d.ts +33 -0
- package/dist/tree/stop.js +116 -0
- package/dist/tree/store.d.ts +24 -0
- package/dist/tree/store.js +264 -0
- package/dist/tree/subtree.d.ts +101 -0
- package/dist/tree/subtree.js +60 -0
- package/dist/tree/summary.d.ts +7 -0
- package/dist/tree/summary.js +35 -0
- package/dist/tree/text.d.ts +77 -0
- package/dist/tree/text.js +287 -0
- package/dist/tree/tools.d.ts +54 -0
- package/dist/tree/tools.js +167 -0
- package/dist/tree/tui.d.ts +53 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +1219 -0
- package/dist/webhook/index.d.ts +5 -0
- package/dist/webhook/index.js +3 -0
- package/dist/{webhook.d.ts → webhook/receiver.d.ts} +25 -36
- package/dist/{webhook.js → webhook/receiver.js} +66 -67
- package/dist/webhook/subscriptions.d.ts +29 -0
- package/dist/webhook/subscriptions.js +52 -0
- package/dist/webhook/text.d.ts +23 -0
- package/dist/webhook/text.js +82 -0
- package/dist/webhook/tools.d.ts +19 -0
- package/dist/webhook/tools.js +46 -0
- package/package.json +18 -7
- package/dist/bounded.d.ts +0 -10
- package/dist/courier.d.ts +0 -95
- package/dist/courier.js +0 -131
- package/dist/hub.d.ts +0 -325
- package/dist/json.d.ts +0 -4
- package/dist/json.js +0 -4
- package/dist/notices.d.ts +0 -246
- package/dist/question/index.d.ts +0 -6
- package/dist/question/lifecycle.d.ts +0 -52
- package/dist/question/shared.js +0 -24
- package/dist/roster.d.ts +0 -81
- package/dist/roster.js +0 -175
- package/dist/tools.d.ts +0 -74
- package/dist/tools.js +0 -228
- package/dist/version.d.ts +0 -22
- package/dist/watch.d.ts +0 -61
- package/dist/watch.js +0 -289
- /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
- /package/dist/{storage.js → platform/storage.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -3,77 +3,44 @@ import { Plugin as EffectPlugin } from "@opencode/plugin/effect";
|
|
|
3
3
|
import { fromPromise } from "@opencode/plugin/promise/adapter";
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
-
import { hub,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
6
|
+
import { watchFromHub } from "./platform/events.js";
|
|
7
|
+
import { hub, openInProcess, permissions as domains, sharedState } from "./platform/hub.js";
|
|
8
|
+
import { builtVersions, versionNotice } from "./platform/version.js";
|
|
9
|
+
import { addSchedulingTools, cancelScheduled, dueJob, isScheduled } from "./scheduling/index.js";
|
|
10
|
+
import { addPersonTools, asksPermission, asksQuestion, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
|
|
11
|
+
import { addSkill } from "./skill.js";
|
|
12
|
+
import { addTreeTools, deliverReleased, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
|
|
13
|
+
import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
|
|
14
|
+
/** The spawns under way in the process, outside the hub, so spawns in other locations, copies and hub versions count them. */
|
|
15
|
+
const gate = sharedState("spawning", () => ({ reserved: new Set(), turn: Promise.resolve() }));
|
|
16
|
+
/** The type of each item enqueued for a spawned session, outside the hub like the gate. */
|
|
17
|
+
const inbox = sharedState("inbox", () => new Map());
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
* stops it. Requests are served with any live instance's ports; sessions and storage are shared.
|
|
19
|
-
* A receiver that could not listen is dropped, so the next instance to load tries again, and a new
|
|
20
|
-
* one waits for the previous one to finish closing, as on a plugin reload.
|
|
19
|
+
* What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
|
|
20
|
+
* due `courier_later` messages, then released join groups. Every hub-v2 member's `later` is its LaterPorts.
|
|
21
21
|
*/
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.then((server) => {
|
|
32
|
-
log(`courier webhook: listening on http://${config.host}:${server.address().port}`);
|
|
33
|
-
return server;
|
|
34
|
-
}, (error) => {
|
|
35
|
-
log(`courier webhook: cannot listen on ${config.host}:${config.port}: ${String(error)}`);
|
|
36
|
-
if (receivers.current === receiver)
|
|
37
|
-
receivers.current = undefined;
|
|
38
|
-
return undefined;
|
|
39
|
-
}),
|
|
40
|
-
};
|
|
41
|
-
receivers.current = receiver;
|
|
42
|
-
return receiver;
|
|
43
|
-
}
|
|
44
|
-
function joinReceiver(config, ports) {
|
|
45
|
-
const receiver = receivers.current ?? startReceiver(config, ports.log);
|
|
46
|
-
if (!sameSettings(receiver.config, config))
|
|
47
|
-
ports.log(`courier webhook: already running on ${receiver.config.host}:${receiver.config.port} with other settings ` +
|
|
48
|
-
"(port, host, secret or maxBytes); this location's are ignored. Set the webhook option once, in the global config.");
|
|
49
|
-
receiver.instances.add(ports);
|
|
50
|
-
return async () => {
|
|
51
|
-
receiver.instances.delete(ports);
|
|
52
|
-
if (receiver.instances.size > 0 || receivers.current !== receiver)
|
|
53
|
-
return;
|
|
54
|
-
receivers.current = undefined;
|
|
55
|
-
const closing = receiver.server.then((server) => new Promise((resolve) => {
|
|
56
|
-
if (!server)
|
|
57
|
-
return resolve();
|
|
58
|
-
server.closeAllConnections();
|
|
59
|
-
server.close(() => resolve());
|
|
60
|
-
}));
|
|
61
|
-
receivers.closing = closing;
|
|
62
|
-
await closing;
|
|
63
|
-
if (receivers.closing === closing)
|
|
64
|
-
receivers.closing = undefined;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
// One set of handled events for every instance in the process, since the hub's two subscriptions,
|
|
68
|
-
// and a copy of another hub version or one from before the hub, are sent the same events, and of
|
|
69
|
-
// the permission requests sessions were told about. The scheduler's claim set is the hub's
|
|
70
|
-
// own business: the hub runs the scheduler once for its members.
|
|
22
|
+
const jobs = [
|
|
23
|
+
dueJob,
|
|
24
|
+
(owner, claimed) => deliverReleased(owner.later, claimed),
|
|
25
|
+
];
|
|
26
|
+
const { join, track, nudge } = openInProcess(jobs);
|
|
27
|
+
/** The permission domains of the loaded instances: every member's is its location's, whichever copy joined it. */
|
|
28
|
+
const permissions = domains;
|
|
29
|
+
// One set of handled events and of told permission requests for every instance in the process, since
|
|
30
|
+
// several subscriptions and copies see the same events. The scheduler's claim set is the hub's own.
|
|
71
31
|
const { watched } = hub;
|
|
72
|
-
const watchState = {
|
|
32
|
+
const watchState = { seen: watched.seen, inbox };
|
|
33
|
+
const personState = { ...watched, forms: hub.forms };
|
|
73
34
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
35
|
+
* What a session waits on outside the tree, for the silent-turn rule, cheapest first: a request of its
|
|
36
|
+
* own to the person, then a scheduled message or a webhook it subscribed to since its last prompt.
|
|
37
|
+
*/
|
|
38
|
+
const outside = waitsOn(asksPermission, asksQuestion, together(isScheduled, subscribedSince));
|
|
39
|
+
/** What the hub's subscriptions do with each event of OpenCode's, module by module, in this order. */
|
|
40
|
+
const eventModules = [treeEvents(watchState, outside), personEvents(personState)];
|
|
41
|
+
/**
|
|
42
|
+
* The courier tools and webhook receiver, as a promise plugin joining the hub that runs the scheduler
|
|
43
|
+
* and the watcher; its member carries the location by which the question relay finds its ports.
|
|
77
44
|
*/
|
|
78
45
|
export const courier = () => Plugin.define({
|
|
79
46
|
id: "courier",
|
|
@@ -98,7 +65,18 @@ export const courier = () => Plugin.define({
|
|
|
98
65
|
newID: () => `question_${randomUUID()}`,
|
|
99
66
|
log,
|
|
100
67
|
};
|
|
68
|
+
const { limits, problems } = readLimits(ctx.options);
|
|
69
|
+
const { nativeChildren, problems: nativeProblems } = readNativeChildren(ctx.options);
|
|
70
|
+
for (const problem of [...problems, ...nativeProblems])
|
|
71
|
+
log(`courier: ${problem}`);
|
|
72
|
+
const roles = new Map();
|
|
101
73
|
const ports = {
|
|
74
|
+
limits,
|
|
75
|
+
nativeChildren,
|
|
76
|
+
gate,
|
|
77
|
+
roles,
|
|
78
|
+
nudge,
|
|
79
|
+
log,
|
|
102
80
|
session: ctx.session,
|
|
103
81
|
agent: ctx.agent,
|
|
104
82
|
worktree: ctx.worktree,
|
|
@@ -107,6 +85,7 @@ export const courier = () => Plugin.define({
|
|
|
107
85
|
projectID: ctx.location.project.id,
|
|
108
86
|
now: Date.now,
|
|
109
87
|
head: headOf,
|
|
88
|
+
dirty: dirtyOf,
|
|
110
89
|
pending: async (sessionID) => {
|
|
111
90
|
const [requests, questions] = await Promise.all([
|
|
112
91
|
pendingOf(permissions(), sessionID),
|
|
@@ -132,17 +111,21 @@ export const courier = () => Plugin.define({
|
|
|
132
111
|
catch (error) {
|
|
133
112
|
later.log(`courier webhook: not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
134
113
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
answer: answerPorts,
|
|
140
|
-
|
|
141
|
-
hooks,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await
|
|
114
|
+
const stopPorts = { courier: ports, cancelScheduled: (sessionID) => cancelScheduled(later, sessionID), cleanup: cleanupPorts };
|
|
115
|
+
// The tools in a fixed order: the tree's, then the person's, the scheduled messages' and the webhooks'.
|
|
116
|
+
await ctx.tool.transform((tools) => {
|
|
117
|
+
addTreeTools(tools, { courier: ports, cleanup: cleanupPorts, stop: stopPorts });
|
|
118
|
+
addPersonTools(tools, { answer: answerPorts, questions: questionPorts, waiting: watched.waiting });
|
|
119
|
+
addSchedulingTools(tools, later);
|
|
120
|
+
addWebhookTools(tools, { hooks, receiving });
|
|
121
|
+
});
|
|
122
|
+
await ctx.skill.transform(addSkill);
|
|
123
|
+
// What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
|
|
124
|
+
await Promise.resolve()
|
|
125
|
+
.then(() => registerTreeRpc(ctx.rpc, stopPorts))
|
|
126
|
+
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions, maxTotal: limits.maxTotal }))
|
|
127
|
+
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
128
|
+
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, log }, event));
|
|
146
129
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
|
147
130
|
const leaveRelay = joinRelay(questionPorts);
|
|
148
131
|
const noticed = noticeCutOff(questionPorts).catch((error) => questionPorts.log(`courier question: stored questions: ${String(error)}`));
|
|
@@ -153,9 +136,10 @@ export const courier = () => Plugin.define({
|
|
|
153
136
|
permissions,
|
|
154
137
|
now: questionPorts.now,
|
|
155
138
|
log: later.log,
|
|
139
|
+
nudge,
|
|
156
140
|
};
|
|
157
|
-
// The hub's subscriptions to OpenCode's events, one and a standby, start with this copy's
|
|
158
|
-
watchFromHub(hub,
|
|
141
|
+
// The hub's subscriptions to OpenCode's events, one and a standby, start with this copy's modules.
|
|
142
|
+
watchFromHub(hub, eventModules);
|
|
159
143
|
const member = {
|
|
160
144
|
directory: ctx.location.directory,
|
|
161
145
|
location: ctx.location,
|
|
@@ -181,11 +165,8 @@ export const courier = () => Plugin.define({
|
|
|
181
165
|
},
|
|
182
166
|
});
|
|
183
167
|
/**
|
|
184
|
-
* The plugin OpenCode loads
|
|
185
|
-
*
|
|
186
|
-
* interrupted, so a child's question could not be withdrawn once its parent answered. Everything
|
|
187
|
-
* else is the promise plugin above, run through the plugin package's own adapter, as OpenCode runs
|
|
188
|
-
* a promise plugin.
|
|
168
|
+
* The plugin OpenCode loads: an Effect plugin, since the question relay wraps OpenCode's question tool
|
|
169
|
+
* and only the Effect API makes its call interruptible; the rest is the promise plugin, adapted.
|
|
189
170
|
*/
|
|
190
171
|
export default EffectPlugin.define({
|
|
191
172
|
id: "courier",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every module of the plugin shares of the protocol with the models: the envelope a message arrives in,
|
|
3
|
+
* the phrase that ends a turn, the statuses of a report, the split line and the skill's id, and how outside
|
|
4
|
+
* text is clipped and defused.
|
|
5
|
+
*/
|
|
6
|
+
/** Closes a tool result after which the caller most likely has nothing left to do. */
|
|
7
|
+
export declare const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
|
|
8
|
+
/** The statuses a report carries: the task is done, partly done, waits on the parent, or failed. */
|
|
9
|
+
export declare const STATUSES: readonly ["done", "partial", "blocked", "failed"];
|
|
10
|
+
export type Status = (typeof STATUSES)[number];
|
|
11
|
+
export declare const isStatus: (value: unknown) => value is Status;
|
|
12
|
+
/** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
|
|
13
|
+
export declare const SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part)";
|
|
14
|
+
export declare const SKILL_ID = "courier-orchestrate";
|
|
15
|
+
export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
|
|
16
|
+
export declare function clipText(text: string, max?: number): string;
|
|
17
|
+
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
18
|
+
export declare function defuse(text: string): string;
|
|
19
|
+
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
20
|
+
export declare function describeFailure(tool: string, error: unknown): Error;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every module of the plugin shares of the protocol with the models: the envelope a message arrives in,
|
|
3
|
+
* the phrase that ends a turn, the statuses of a report, the split line and the skill's id, and how outside
|
|
4
|
+
* text is clipped and defused.
|
|
5
|
+
*/
|
|
6
|
+
/** Closes a tool result after which the caller most likely has nothing left to do. */
|
|
7
|
+
export const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
|
|
8
|
+
/** The statuses a report carries: the task is done, partly done, waits on the parent, or failed. */
|
|
9
|
+
export const STATUSES = ["done", "partial", "blocked", "failed"];
|
|
10
|
+
export const isStatus = (value) => typeof value === "string" && STATUSES.includes(value);
|
|
11
|
+
/** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
|
|
12
|
+
export const SPLIT_LINE = "split: no, because …; considered: <the parts you weighed> or split: <n> parts: … (one phrase per part)";
|
|
13
|
+
export const SKILL_ID = "courier-orchestrate";
|
|
14
|
+
export function envelope(from, message, attributes = {}) {
|
|
15
|
+
const extra = Object.entries(attributes)
|
|
16
|
+
.map(([name, value]) => ` ${name}="${value}"`)
|
|
17
|
+
.join("");
|
|
18
|
+
return `<courier from="${from}"${extra}>\n${message}\n</courier>`;
|
|
19
|
+
}
|
|
20
|
+
/** Longest piece of free text (a review body, a generic payload) copied into a delivered message. */
|
|
21
|
+
const MAX_TEXT = 1500;
|
|
22
|
+
export function clipText(text, max = MAX_TEXT) {
|
|
23
|
+
return text.length > max ? `${text.slice(0, max)}… [${text.length - max} more characters]` : text;
|
|
24
|
+
}
|
|
25
|
+
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
26
|
+
export function defuse(text) {
|
|
27
|
+
return text.replace(/<(\/?)(courier)/gi, "<$1$2");
|
|
28
|
+
}
|
|
29
|
+
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
|
30
|
+
export function describeFailure(tool, error) {
|
|
31
|
+
const tagged = error;
|
|
32
|
+
const message = (typeof tagged?.message === "string" && tagged.message) ||
|
|
33
|
+
[tagged?._tag, tagged?.sessionID].filter((item) => typeof item === "string").join(" ") ||
|
|
34
|
+
String(error);
|
|
35
|
+
return new Error(`${tool} failed: ${message}`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin";
|
|
2
|
+
/** The editor OpenCode hands a tool transform. */
|
|
3
|
+
export type ToolEditor = Parameters<Parameters<Plugin.Context["tool"]["transform"]>[0]>[0];
|
|
4
|
+
/** A `.catch` handler that rethrows a failure of `tool` as the error the model reads. */
|
|
5
|
+
export declare const rethrow: (tool: string) => (error: unknown) => never;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EventModule } from "../platform/events.js";
|
|
2
|
+
import { type PermissionAsked, type PermissionReplied } from "./permission.js";
|
|
3
|
+
import { type PersonPorts, type PersonState } from "./ports.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tells the session at the top, the parent of a child or the session a chain of children started
|
|
6
|
+
* from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
|
|
7
|
+
*/
|
|
8
|
+
export declare function reportAsked(ports: PersonPorts, state: PersonState, event: PermissionAsked): Promise<string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Tells the session told about a request that it was answered some other way, so it does not pass on
|
|
11
|
+
* a stale question; requests answered through courier_answer no longer wait and are skipped.
|
|
12
|
+
*/
|
|
13
|
+
export declare function reportReplied(ports: PersonPorts, state: PersonState, event: PermissionReplied): Promise<string[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Relays the requests that spawned sessions already wait on, in every loaded location, which the
|
|
16
|
+
* event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function relayPending(ports: PersonPorts, state: PersonState): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Tells the top session of a spawned child's pending permission or form, relays the requests already
|
|
21
|
+
* pending on every (re)subscribe, and tells the question relay when events are followed and when not.
|
|
22
|
+
*/
|
|
23
|
+
export declare function personEvents(state: PersonState): EventModule<PersonPorts>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { envelope } from "../kernel/protocol.js";
|
|
2
|
+
import { claim } from "../platform/bounded.js";
|
|
3
|
+
import { allEntries, lineage, nativeRootOf } from "../tree/index.js";
|
|
4
|
+
import { reportForm, reportFormSettled } from "./form.js";
|
|
5
|
+
import { listEverywhere } from "./permission.js";
|
|
6
|
+
import { topOf } from "./ports.js";
|
|
7
|
+
import { eventsFollowed, eventsLeft, formShown, formsMayHaveBeenMissed, locationClosing } from "./question/index.js";
|
|
8
|
+
import { permissionNotice, permissionSettledNotice } from "./text.js";
|
|
9
|
+
/**
|
|
10
|
+
* Tells the session at the top, the parent of a child or the session a chain of children started
|
|
11
|
+
* from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
|
|
12
|
+
*/
|
|
13
|
+
export async function reportAsked(ports, state, event) {
|
|
14
|
+
if (!claim(state.seen, event.id))
|
|
15
|
+
return [];
|
|
16
|
+
const request = event.data;
|
|
17
|
+
const chain = await lineage(ports.storage, request.sessionID);
|
|
18
|
+
// Claimed only now, after the lookup: a request answered meanwhile is in `answered`, and one
|
|
19
|
+
// already relayed, from the event or from the pending list, is in `waiting`.
|
|
20
|
+
if (!chain.length || state.answered.has(request.id) || !claim(state.waiting, request.id))
|
|
21
|
+
return [];
|
|
22
|
+
const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
|
|
23
|
+
// A native child's request shows as OpenCode's own prompt in its native root's view: when that is the top's, it is not asked again.
|
|
24
|
+
const native = nativeRootOf(chain) === topOf(chain);
|
|
25
|
+
await ports.session.synthetic({
|
|
26
|
+
sessionID: topOf(chain),
|
|
27
|
+
text: envelope(request.sessionID, permissionNotice(chain[0].title, request, startedBy, native), {
|
|
28
|
+
asks: "permission",
|
|
29
|
+
request: request.id,
|
|
30
|
+
...(native ? { shown: "native" } : {}),
|
|
31
|
+
}),
|
|
32
|
+
description: `Session ${request.sessionID} asks for permission`,
|
|
33
|
+
metadata: { source: "courier", from: request.sessionID, asks: "permission", requestID: request.id, ...(native ? { shown: "native" } : {}) },
|
|
34
|
+
delivery: "steer",
|
|
35
|
+
});
|
|
36
|
+
return [topOf(chain)];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Tells the session told about a request that it was answered some other way, so it does not pass on
|
|
40
|
+
* a stale question; requests answered through courier_answer no longer wait and are skipped.
|
|
41
|
+
*/
|
|
42
|
+
export async function reportReplied(ports, state, event) {
|
|
43
|
+
if (!claim(state.seen, event.id))
|
|
44
|
+
return [];
|
|
45
|
+
const { sessionID, requestID, reply } = event.data;
|
|
46
|
+
if (!state.waiting.delete(requestID)) {
|
|
47
|
+
claim(state.answered, requestID);
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const chain = await lineage(ports.storage, sessionID);
|
|
51
|
+
if (!chain.length)
|
|
52
|
+
return [];
|
|
53
|
+
await ports.session.synthetic({
|
|
54
|
+
sessionID: topOf(chain),
|
|
55
|
+
text: envelope(sessionID, permissionSettledNotice(chain[0].title, requestID, reply), { answered: reply, request: requestID }),
|
|
56
|
+
description: `Session ${sessionID} no longer asks for permission`,
|
|
57
|
+
metadata: { source: "courier", from: sessionID, answered: reply, requestID },
|
|
58
|
+
delivery: "steer",
|
|
59
|
+
});
|
|
60
|
+
return [topOf(chain)];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Relays the requests that spawned sessions already wait on, in every loaded location, which the
|
|
64
|
+
* event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
|
|
65
|
+
*/
|
|
66
|
+
export async function relayPending(ports, state) {
|
|
67
|
+
const sessions = new Set((await allEntries(ports.storage)).map((entry) => entry.sessionID));
|
|
68
|
+
const domains = [...ports.permissions()];
|
|
69
|
+
// Each request is claimed before its notice goes out, so relaying them all at once tells each
|
|
70
|
+
// once, however many locations list it; a location that cannot be read lists none.
|
|
71
|
+
const relay = async (sessionID) => {
|
|
72
|
+
const listed = (await listEverywhere(domains, sessionID)).flatMap((found) => found.requests);
|
|
73
|
+
await Promise.all(listed.map((request) => reportAsked(ports, state, { id: `pending:${request.id}`, data: request })));
|
|
74
|
+
};
|
|
75
|
+
await Promise.all([...sessions].map(relay));
|
|
76
|
+
}
|
|
77
|
+
/** The events of what a spawned session asks the person: permission requests, forms, and locations closing on them. */
|
|
78
|
+
function routePerson(ports, state, event) {
|
|
79
|
+
if (event.type === "permission.asked")
|
|
80
|
+
return reportAsked(ports, state, event);
|
|
81
|
+
if (event.type === "permission.replied")
|
|
82
|
+
return reportReplied(ports, state, event);
|
|
83
|
+
if (event.type === "form.created") {
|
|
84
|
+
// Not claimed: the hub's other subscription, or another copy's watcher, may resolve the same
|
|
85
|
+
// waiting call, which is harmless.
|
|
86
|
+
formShown(event);
|
|
87
|
+
return reportForm(ports, state, event);
|
|
88
|
+
}
|
|
89
|
+
if (event.type === "form.replied" || event.type === "form.cancelled")
|
|
90
|
+
return reportFormSettled(ports, state, event);
|
|
91
|
+
// Not claimed either: a location closing withdraws its forms, which must not pass for dismissals.
|
|
92
|
+
// The event's location is optional; without one, the shutdown counts for every location.
|
|
93
|
+
if (event.type === "location.shutdown") {
|
|
94
|
+
const directory = event.location?.directory;
|
|
95
|
+
locationClosing(ports.now(), typeof directory === "string" ? directory : undefined);
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Tells the top session of a spawned child's pending permission or form, relays the requests already
|
|
101
|
+
* pending on every (re)subscribe, and tells the question relay when events are followed and when not.
|
|
102
|
+
*/
|
|
103
|
+
export function personEvents(state) {
|
|
104
|
+
return {
|
|
105
|
+
handle: (ports, event) => routePerson(ports, state, event),
|
|
106
|
+
// Question forms shown while no instance followed the events were not seen: released now,
|
|
107
|
+
// and again on the first event, by when the stream is surely connected.
|
|
108
|
+
followed: () => (eventsFollowed() ? formsMayHaveBeenMissed : undefined),
|
|
109
|
+
// Alongside the new subscription; a request both relays see is relayed once.
|
|
110
|
+
subscribed: (ports) => relayPending(ports, state).catch((error) => ports.log(`courier watch: could not relay pending requests: ${String(error)}`)),
|
|
111
|
+
left: eventsLeft,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Form, type PersonPorts, type PersonState } from "./ports.js";
|
|
2
|
+
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
3
|
+
export declare const QUESTION_FORM = "question";
|
|
4
|
+
/** The part of OpenCode's `form.created` event the notice is made from. */
|
|
5
|
+
export interface FormCreated {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly data: {
|
|
8
|
+
readonly form: Form;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** OpenCode's `form.replied` or `form.cancelled` event: the form is no longer shown. */
|
|
12
|
+
export interface FormSettled {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly data: {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly sessionID: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tells the top session about a form OpenCode shows in a spawned session, such as web search asking
|
|
22
|
+
* for its provider, which only the person can answer; question forms are left to the question relay.
|
|
23
|
+
*/
|
|
24
|
+
export declare function reportForm(ports: PersonPorts, state: PersonState, event: FormCreated): Promise<string[]>;
|
|
25
|
+
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
26
|
+
export declare function reportFormSettled(ports: PersonPorts, state: PersonState, event: FormSettled): Promise<string[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { envelope } from "../kernel/protocol.js";
|
|
2
|
+
import { claim, SEEN_MAX, setBounded } from "../platform/bounded.js";
|
|
3
|
+
import { lineage, nativeRootOf } from "../tree/index.js";
|
|
4
|
+
import { topOf } from "./ports.js";
|
|
5
|
+
import { formNotice, formSettledNotice, kindOf } from "./text.js";
|
|
6
|
+
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
7
|
+
export const QUESTION_FORM = "question";
|
|
8
|
+
/**
|
|
9
|
+
* Tells the top session about a form OpenCode shows in a spawned session, such as web search asking
|
|
10
|
+
* for its provider, which only the person can answer; question forms are left to the question relay.
|
|
11
|
+
*/
|
|
12
|
+
export async function reportForm(ports, state, event) {
|
|
13
|
+
if (!claim(state.seen, event.id))
|
|
14
|
+
return [];
|
|
15
|
+
const form = event.data.form;
|
|
16
|
+
if (kindOf(form) === QUESTION_FORM)
|
|
17
|
+
return [];
|
|
18
|
+
const chain = await lineage(ports.storage, form.sessionID);
|
|
19
|
+
// As with a permission request, claimed after the lookup, so a form settled meanwhile is not told.
|
|
20
|
+
if (!chain.length || state.forms.settled.has(form.id) || state.forms.told.has(form.id))
|
|
21
|
+
return [];
|
|
22
|
+
const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
|
|
23
|
+
const kind = kindOf(form);
|
|
24
|
+
const telling = ports.session.synthetic({
|
|
25
|
+
sessionID: topOf(chain),
|
|
26
|
+
text: envelope(form.sessionID, formNotice(chain[0].title, form, startedBy, nativeRootOf(chain), topOf(chain)), {
|
|
27
|
+
asks: "form",
|
|
28
|
+
form: form.id,
|
|
29
|
+
...(kind ? { kind } : {}),
|
|
30
|
+
}),
|
|
31
|
+
description: `Session ${form.sessionID} shows a form`,
|
|
32
|
+
metadata: { source: "courier", from: form.sessionID, asks: "form", formID: form.id, ...(kind ? { kind } : {}) },
|
|
33
|
+
delivery: "steer",
|
|
34
|
+
});
|
|
35
|
+
setBounded(state.forms.told, form.id, telling, SEEN_MAX);
|
|
36
|
+
await telling;
|
|
37
|
+
return [topOf(chain)];
|
|
38
|
+
}
|
|
39
|
+
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
40
|
+
export async function reportFormSettled(ports, state, event) {
|
|
41
|
+
if (!claim(state.seen, event.id))
|
|
42
|
+
return [];
|
|
43
|
+
const { id, sessionID } = event.data;
|
|
44
|
+
const telling = state.forms.told.get(id);
|
|
45
|
+
if (!telling) {
|
|
46
|
+
claim(state.forms.settled, id);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
state.forms.told.delete(id);
|
|
50
|
+
const settled = event.type === "form.replied" ? "answered" : "cancelled";
|
|
51
|
+
// After the notice that the form is shown, which another instance may still be sending, so the
|
|
52
|
+
// settling never arrives first; and not at all when that notice did not go out.
|
|
53
|
+
const [chain, told] = await Promise.all([lineage(ports.storage, sessionID), telling.then(() => true, () => false)]);
|
|
54
|
+
if (!chain.length || !told)
|
|
55
|
+
return [];
|
|
56
|
+
await ports.session.synthetic({
|
|
57
|
+
sessionID: topOf(chain),
|
|
58
|
+
text: envelope(sessionID, formSettledNotice(chain[0].title, id, settled), { settled, form: id }),
|
|
59
|
+
description: `Session ${sessionID} no longer shows a form`,
|
|
60
|
+
metadata: { source: "courier", from: sessionID, settled, formID: id },
|
|
61
|
+
delivery: "steer",
|
|
62
|
+
});
|
|
63
|
+
return [topOf(chain)];
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PendingPrompt, WatchPorts } from "../tree/index.js";
|
|
2
|
+
import type { PersonPorts } from "./ports.js";
|
|
3
|
+
import type { Prompt } from "./question/index.js";
|
|
4
|
+
export { personEvents } from "./events.js";
|
|
5
|
+
export { asksPermission, pendingOf } from "./permission.js";
|
|
6
|
+
export type { AnswerPorts, Permissions, Waiting } from "./permission.js";
|
|
7
|
+
export type { PersonPorts, PersonState } from "./ports.js";
|
|
8
|
+
export { registerPersonRpc } from "./rpc-handlers.js";
|
|
9
|
+
export { asksQuestion, joinRelay, noticeCutOff, pendingQuestions, portsAt, relayQuestions } from "./question/index.js";
|
|
10
|
+
export type { QuestionPorts } from "./question/index.js";
|
|
11
|
+
export { addPersonTools } from "./tools.js";
|
|
12
|
+
export type { PersonToolPorts } from "./tools.js";
|
|
13
|
+
type Satisfies<T extends U, U> = T;
|
|
14
|
+
/** The tree declares the question shape its `Pending` needs; this fails to compile once `Prompt` and it differ. */
|
|
15
|
+
export type PromptMatchesPending = [Satisfies<Prompt, PendingPrompt>, Satisfies<PendingPrompt, Prompt>];
|
|
16
|
+
/** The watcher's ports are the ones the person events run on; this fails to compile once `WatchPorts` stops satisfying `PersonPorts`. */
|
|
17
|
+
export type WatchPortsArePersonPorts = Satisfies<WatchPorts, PersonPorts>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { personEvents } from "./events.js";
|
|
2
|
+
export { asksPermission, pendingOf } from "./permission.js";
|
|
3
|
+
export { registerPersonRpc } from "./rpc-handlers.js";
|
|
4
|
+
export { asksQuestion, joinRelay, noticeCutOff, pendingQuestions, portsAt, relayQuestions } from "./question/index.js";
|
|
5
|
+
export { addPersonTools } from "./tools.js";
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin";
|
|
2
|
-
import { type Pending } from "
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { type Pending, type RosterStorage } from "../tree/index.js";
|
|
3
|
+
/** The answers to a permission request, as OpenCode's own prompt offers them. */
|
|
4
|
+
export declare const REPLIES: readonly ["once", "always", "reject"];
|
|
5
|
+
export type Reply = (typeof REPLIES)[number];
|
|
6
|
+
/** A permission request, as OpenCode's `permission.asked` event carries it. */
|
|
7
|
+
export interface PermissionRequest {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly sessionID: string;
|
|
10
|
+
readonly action: string;
|
|
11
|
+
readonly resources: ReadonlyArray<string>;
|
|
12
|
+
readonly save?: ReadonlyArray<string>;
|
|
13
|
+
readonly message?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What answering a permission request did; `answered` is false when no location loaded here held the
|
|
17
|
+
* request: answered already, no longer waited for, or waiting in another server on the same data dir.
|
|
18
|
+
*/
|
|
19
|
+
export interface PermissionAnswered {
|
|
20
|
+
readonly sessionID: string;
|
|
21
|
+
readonly requestID: string;
|
|
22
|
+
readonly reply: Reply;
|
|
23
|
+
readonly answered: boolean;
|
|
24
|
+
}
|
|
5
25
|
type Context = Plugin.Context;
|
|
6
26
|
/** One location's pending permission requests. OpenCode keeps them per location, so an isolated child's are in its worktree's. */
|
|
7
27
|
export type Permissions = Pick<Context["permission"], "list" | "reply">;
|
|
@@ -44,31 +64,15 @@ export declare function listEverywhere<D extends Pick<Permissions, "list">>(perm
|
|
|
44
64
|
domain: D;
|
|
45
65
|
requests: import("@opencode/client").PermissionRequest[];
|
|
46
66
|
})[]>;
|
|
67
|
+
/** Whether a session waits on a permission request of its own, in any loaded location. */
|
|
68
|
+
export declare function asksPermission(ports: {
|
|
69
|
+
readonly permissions: () => Iterable<Pick<Permissions, "list">>;
|
|
70
|
+
}, sessionID: string): Promise<boolean>;
|
|
47
71
|
/**
|
|
48
|
-
* Answers a permission request of a session started, directly or through others, from the
|
|
49
|
-
*
|
|
50
|
-
* started with courier_spawn cannot approve what its own children ask. `answered` is false when
|
|
51
|
-
* no location loaded in this server holds the request, which may then wait in another server.
|
|
72
|
+
* Answers a permission request of a session started, directly or through others, from the caller,
|
|
73
|
+
* which must be the top session; `answered` is false when no location loaded here holds the request.
|
|
52
74
|
*/
|
|
53
75
|
export declare function answer(ports: AnswerPorts, waiting: Waiting, callerID: string, input: AnswerInput): Promise<PermissionAnswered>;
|
|
54
76
|
/** The requests a session waits on, in whichever location holds them, for courier_status. */
|
|
55
77
|
export declare function pendingOf(permissions: Iterable<Permissions>, sessionID: string): Promise<Pending[]>;
|
|
56
|
-
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
57
|
-
export declare const QUESTION_FORM = "question";
|
|
58
|
-
/** The part of OpenCode's `form.created` event the notice is made from. */
|
|
59
|
-
export interface FormCreated {
|
|
60
|
-
readonly id: string;
|
|
61
|
-
readonly data: {
|
|
62
|
-
readonly form: Form;
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
/** OpenCode's `form.replied` or `form.cancelled` event: the form is no longer shown. */
|
|
66
|
-
export interface FormSettled {
|
|
67
|
-
readonly id: string;
|
|
68
|
-
readonly type: string;
|
|
69
|
-
readonly data: {
|
|
70
|
-
readonly id: string;
|
|
71
|
-
readonly sessionID: string;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
78
|
export {};
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { describeFailure } from "
|
|
2
|
-
import { REJECTED
|
|
3
|
-
import { answeringTop } from "
|
|
1
|
+
import { describeFailure } from "../kernel/protocol.js";
|
|
2
|
+
import { REJECTED } from "./text.js";
|
|
3
|
+
import { answeringTop } from "../tree/index.js";
|
|
4
|
+
/** The answers to a permission request, as OpenCode's own prompt offers them. */
|
|
5
|
+
export const REPLIES = ["once", "always", "reject"];
|
|
4
6
|
/** A session's pending requests in every location, with the domain holding each; a location that cannot be read gives its error. */
|
|
5
7
|
export function listEverywhere(permissions, sessionID) {
|
|
6
8
|
return Promise.all([...permissions].map((domain) => domain.list({ sessionID }).then((requests) => ({ domain, requests: requests.filter((request) => request.sessionID === sessionID) }), (error) => ({ domain, requests: [], error }))));
|
|
7
9
|
}
|
|
10
|
+
/** Whether a session waits on a permission request of its own, in any loaded location. */
|
|
11
|
+
export async function asksPermission(ports, sessionID) {
|
|
12
|
+
return (await listEverywhere([...ports.permissions()], sessionID)).some((found) => found.requests.length > 0);
|
|
13
|
+
}
|
|
8
14
|
/** The location holding a pending request, and the request; undefined when none does, and an error when one could not be read. */
|
|
9
15
|
async function locate(ports, sessionID, requestID) {
|
|
10
16
|
const listed = await listEverywhere(ports.permissions(), sessionID);
|
|
@@ -19,10 +25,8 @@ async function locate(ports, sessionID, requestID) {
|
|
|
19
25
|
return undefined;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
22
|
-
* Answers a permission request of a session started, directly or through others, from the
|
|
23
|
-
*
|
|
24
|
-
* started with courier_spawn cannot approve what its own children ask. `answered` is false when
|
|
25
|
-
* no location loaded in this server holds the request, which may then wait in another server.
|
|
28
|
+
* Answers a permission request of a session started, directly or through others, from the caller,
|
|
29
|
+
* which must be the top session; `answered` is false when no location loaded here holds the request.
|
|
26
30
|
*/
|
|
27
31
|
export async function answer(ports, waiting, callerID, input) {
|
|
28
32
|
const reply = input.reply;
|
|
@@ -66,5 +70,3 @@ export async function pendingOf(permissions, sessionID) {
|
|
|
66
70
|
});
|
|
67
71
|
return [...found.values()];
|
|
68
72
|
}
|
|
69
|
-
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
70
|
-
export const QUESTION_FORM = "question";
|