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.
- package/README.md +7 -9
- package/dist/index.d.ts +0 -8
- package/dist/index.js +6 -53
- package/dist/kernel/protocol.d.ts +0 -14
- package/dist/kernel/protocol.js +1 -15
- package/dist/kernel/tool.d.ts +0 -2
- package/dist/kernel/tool.js +0 -1
- package/dist/memory/domain.d.ts +6 -85
- package/dist/memory/domain.js +7 -63
- package/dist/memory/events.d.ts +0 -11
- package/dist/memory/events.js +1 -12
- package/dist/memory/forget.d.ts +0 -5
- package/dist/memory/forget.js +0 -4
- package/dist/memory/index.js +0 -2
- package/dist/memory/lessons.d.ts +0 -8
- package/dist/memory/lessons.js +0 -9
- package/dist/memory/note.d.ts +0 -9
- package/dist/memory/note.js +0 -5
- package/dist/memory/pin.d.ts +0 -5
- package/dist/memory/pin.js +0 -4
- package/dist/memory/ports.d.ts +2 -22
- package/dist/memory/recall.d.ts +0 -5
- package/dist/memory/recall.js +0 -12
- package/dist/memory/record.d.ts +0 -4
- package/dist/memory/record.js +8 -31
- package/dist/memory/rpc-handlers.d.ts +3 -6
- package/dist/memory/rpc-handlers.js +0 -6
- package/dist/memory/rpc.d.ts +6 -19
- package/dist/memory/rpc.js +1 -16
- package/dist/memory/store.d.ts +0 -5
- package/dist/memory/store.js +1 -16
- package/dist/memory/sweep.d.ts +0 -4
- package/dist/memory/sweep.js +0 -4
- package/dist/memory/text.d.ts +0 -25
- package/dist/memory/text.js +6 -36
- package/dist/memory/tools.d.ts +0 -2
- package/dist/memory/tools.js +0 -2
- package/dist/memory/tui.d.ts +1 -35
- package/dist/person/events.d.ts +0 -16
- package/dist/person/events.js +3 -30
- package/dist/person/form.d.ts +0 -9
- package/dist/person/form.js +0 -10
- package/dist/person/index.d.ts +0 -2
- package/dist/person/permission.d.ts +0 -34
- package/dist/person/permission.js +2 -28
- package/dist/person/ports.d.ts +0 -16
- package/dist/person/ports.js +0 -1
- package/dist/person/question/answer.d.ts +0 -28
- package/dist/person/question/answer.js +0 -49
- package/dist/person/question/index.js +0 -2
- package/dist/person/question/lifecycle.d.ts +0 -26
- package/dist/person/question/lifecycle.js +1 -32
- package/dist/person/question/pure.d.ts +0 -11
- package/dist/person/question/pure.js +0 -11
- package/dist/person/question/relay.d.ts +0 -1
- package/dist/person/question/relay.js +5 -67
- package/dist/person/question/runtime.d.ts +0 -14
- package/dist/person/question/runtime.js +1 -26
- package/dist/person/question/shared.d.ts +0 -17
- package/dist/person/question/shared.js +0 -20
- package/dist/person/question/types.d.ts +0 -44
- package/dist/person/rpc-handlers.d.ts +0 -7
- package/dist/person/rpc-handlers.js +0 -6
- package/dist/person/rpc.d.ts +0 -20
- package/dist/person/rpc.js +0 -16
- package/dist/person/text.d.ts +0 -45
- package/dist/person/text.js +2 -54
- package/dist/person/tools.d.ts +0 -3
- package/dist/person/tools.js +1 -3
- package/dist/person/tui.d.ts +0 -21
- package/dist/platform/bounded.d.ts +0 -8
- package/dist/platform/bounded.js +0 -8
- package/dist/platform/events.d.ts +0 -18
- package/dist/platform/events.js +3 -22
- package/dist/platform/hub.d.ts +0 -113
- package/dist/platform/hub.js +1 -76
- package/dist/platform/json.d.ts +0 -3
- package/dist/platform/json.js +1 -3
- package/dist/platform/storage.d.ts +0 -7
- package/dist/platform/storage.js +0 -6
- package/dist/platform/version.d.ts +0 -9
- package/dist/platform/version.js +0 -8
- package/dist/rpc.js +0 -1
- package/dist/scheduling/index.d.ts +0 -1
- package/dist/scheduling/index.js +0 -1
- package/dist/scheduling/later.d.ts +0 -11
- package/dist/scheduling/later.js +0 -14
- package/dist/scheduling/text.d.ts +0 -1
- package/dist/scheduling/text.js +0 -1
- package/dist/scheduling/tools.d.ts +0 -1
- package/dist/scheduling/tools.js +1 -5
- package/dist/skill.d.ts +1 -9
- package/dist/skill.js +4 -12
- package/dist/tree/cleanup.d.ts +0 -20
- package/dist/tree/cleanup.js +0 -26
- package/dist/tree/context.d.ts +0 -5
- package/dist/tree/context.js +0 -8
- package/dist/tree/domain.d.ts +0 -64
- package/dist/tree/domain.js +7 -45
- package/dist/tree/events.d.ts +0 -36
- package/dist/tree/events.js +4 -49
- package/dist/tree/forget.d.ts +0 -15
- package/dist/tree/forget.js +0 -18
- package/dist/tree/group.d.ts +0 -22
- package/dist/tree/group.js +1 -37
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.js +1 -3
- package/dist/tree/limits.d.ts +0 -4
- package/dist/tree/limits.js +1 -12
- package/dist/tree/mascot.d.ts +0 -25
- package/dist/tree/mascot.js +1 -32
- package/dist/tree/model.d.ts +0 -5
- package/dist/tree/model.js +2 -15
- package/dist/tree/nudge.d.ts +0 -11
- package/dist/tree/nudge.js +3 -16
- package/dist/tree/panel.d.ts +0 -8
- package/dist/tree/panel.js +3 -16
- package/dist/tree/ports.d.ts +2 -138
- package/dist/tree/report.d.ts +0 -10
- package/dist/tree/report.js +0 -11
- package/dist/tree/roster.d.ts +0 -21
- package/dist/tree/roster.js +0 -21
- package/dist/tree/rpc-handlers.d.ts +6 -1
- package/dist/tree/rpc-handlers.js +0 -5
- package/dist/tree/rpc.d.ts +9 -109
- package/dist/tree/rpc.js +2 -38
- package/dist/tree/running.d.ts +0 -6
- package/dist/tree/running.js +2 -10
- package/dist/tree/send.d.ts +0 -4
- package/dist/tree/send.js +4 -31
- package/dist/tree/spawn.d.ts +0 -7
- package/dist/tree/spawn.js +6 -47
- package/dist/tree/standing.d.ts +0 -8
- package/dist/tree/standing.js +1 -11
- package/dist/tree/status.d.ts +0 -8
- package/dist/tree/status.js +1 -9
- package/dist/tree/stop.d.ts +2 -9
- package/dist/tree/stop.js +58 -25
- package/dist/tree/store.d.ts +0 -8
- package/dist/tree/store.js +4 -38
- package/dist/tree/subtree.d.ts +8 -26
- package/dist/tree/subtree.js +37 -35
- package/dist/tree/summary.d.ts +0 -4
- package/dist/tree/summary.js +0 -8
- package/dist/tree/text.d.ts +1 -44
- package/dist/tree/text.js +5 -65
- package/dist/tree/tools.d.ts +0 -6
- package/dist/tree/tools.js +4 -8
- package/dist/tree/tui.d.ts +0 -81
- package/dist/tree/wake.d.ts +0 -30
- package/dist/tree/wake.js +5 -39
- package/dist/tui.js +22 -8
- package/package.json +8 -9
- package/dist/webhook/index.d.ts +0 -5
- package/dist/webhook/index.js +0 -3
- package/dist/webhook/receiver.d.ts +0 -80
- package/dist/webhook/receiver.js +0 -339
- package/dist/webhook/subscriptions.d.ts +0 -29
- package/dist/webhook/subscriptions.js +0 -52
- package/dist/webhook/text.d.ts +0 -23
- package/dist/webhook/text.js +0 -82
- package/dist/webhook/tools.d.ts +0 -19
- package/dist/webhook/tools.js +0 -46
package/dist/person/ports.d.ts
CHANGED
|
@@ -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;
|
package/dist/person/ports.js
CHANGED
|
@@ -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>;
|