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/memory/domain.js
CHANGED
|
@@ -1,61 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The memory's pure rules, on plain data: the option, a record's shape, each part's status, the split line and its revisions,
|
|
3
|
-
* its reads and pin, the retention and how a refresh keeps the parts the roster has forgotten. Imports nothing of the plugin's but `kernel/` and `platform/json.ts`.
|
|
4
|
-
*/
|
|
5
1
|
import { STATUSES } from "../kernel/protocol.js";
|
|
6
2
|
import { obj } from "../platform/json.js";
|
|
7
|
-
/** How many unpinned records a project keeps: those with a lesson first, then the newest by `movedAt`. */
|
|
8
3
|
export const MAX_RECORDS = 30;
|
|
9
|
-
/** Unpinned records that last moved longer ago than this are dropped when the plugin loads. */
|
|
10
4
|
export const RETENTION_MS = 90 * 24 * 60 * 60_000;
|
|
11
|
-
/** A record's `split` while its top has stated no split line. */
|
|
12
5
|
export const NOT_STATED = "not stated";
|
|
13
|
-
/** The most characters a lesson holds: courier_note refuses what is longer, and a record written by hand is clipped when read. */
|
|
14
6
|
export const LESSON_MAX = 300;
|
|
15
|
-
/**
|
|
16
|
-
* How a part stands: its last report's status, how its wait ended without one (`failed`, `interrupted`), `stopped` by
|
|
17
|
-
* courier_stop, `gone` before it settled, `stalled` on a message it will not take, or `out`: it owes a report.
|
|
18
|
-
*/
|
|
19
7
|
export const PART_STATUSES = [...STATUSES, "interrupted", "stopped", "gone", "stalled", "out"];
|
|
20
8
|
export const isPartStatus = (value) => typeof value === "string" && PART_STATUSES.includes(value);
|
|
21
|
-
/** Whether a part has settled, as every part of a finished tree has: it neither owes a report nor is stalled. */
|
|
22
9
|
export const isSettled = (status) => status !== "out" && status !== "stalled";
|
|
23
|
-
/** How many revisions of the split line a record keeps, the newest. */
|
|
24
10
|
export const MAX_REVISIONS = 3;
|
|
25
|
-
/** The `memory` option: whether records are kept and listed; on unless `false`. */
|
|
26
11
|
export function readMemory(options) {
|
|
27
12
|
const value = obj(options).memory;
|
|
28
13
|
if (value === undefined || typeof value === "boolean")
|
|
29
14
|
return { memory: value !== false, problems: [] };
|
|
30
15
|
return { memory: true, problems: [`memory must be true or false, not ${JSON.stringify(value)}; using true`] };
|
|
31
16
|
}
|
|
32
|
-
/** A line beginning with `split:` after any markdown (whitespace, emphasis, heading, quote and list marks). */
|
|
33
17
|
const STARTS_SPLIT = /^[\s*_#>`-]*split:/i;
|
|
34
|
-
/** A line holding `split:` as a word. */
|
|
35
18
|
const HOLDS_SPLIT = /\bsplit:/i;
|
|
36
|
-
/** The split line a text states, as the real-model checker reads it: a line beginning with `split:` after any markdown, else one holding it. */
|
|
37
19
|
export function splitLineOf(text) {
|
|
38
20
|
const lines = text.split("\n");
|
|
39
21
|
return (lines.find((line) => STARTS_SPLIT.test(line)) ?? lines.find((line) => HOLDS_SPLIT.test(line)))?.trim();
|
|
40
22
|
}
|
|
41
|
-
/** Whether a split line is stated, beginning with `split:` after any markdown, not a passing mention nor `not stated`. */
|
|
42
23
|
export const isStated = (line) => STARTS_SPLIT.test(line);
|
|
43
|
-
/** The record's `split` once `text` is read: kept once stated; a mention gives way to the first stated line; `not stated` to either. */
|
|
44
24
|
export function splitOf(recorded, text) {
|
|
45
25
|
if (isStated(recorded))
|
|
46
26
|
return recorded;
|
|
47
27
|
const found = splitLineOf(text);
|
|
48
28
|
return found !== undefined && (recorded === NOT_STATED || isStated(found)) ? found : recorded;
|
|
49
29
|
}
|
|
50
|
-
/** Every line of a text that begins with `split:` after any markdown, trimmed: a passing mention of a split is none. */
|
|
51
30
|
export const splitLinesOf = (text) => text
|
|
52
31
|
.split("\n")
|
|
53
32
|
.filter((line) => STARTS_SPLIT.test(line))
|
|
54
33
|
.map((line) => line.trim());
|
|
55
|
-
/**
|
|
56
|
-
* The lines after the one recorded last: after its reply, else (compacted, or unread by an overlapping refresh) those stated since its
|
|
57
|
-
* time; without a revision, after `split`'s first statement; all when neither is found.
|
|
58
|
-
*/
|
|
59
34
|
function statedSince(previous, stated) {
|
|
60
35
|
const last = previous.revisions?.at(-1);
|
|
61
36
|
const words = (line) => splitProse(line.text);
|
|
@@ -68,10 +43,6 @@ function statedSince(previous, stated) {
|
|
|
68
43
|
return stated.filter((line) => line.at !== undefined && line.at > last.at);
|
|
69
44
|
return stated.slice(stated.findLastIndex((line) => words(line) === splitProse(last.text)) + 1);
|
|
70
45
|
}
|
|
71
|
-
/**
|
|
72
|
-
* The revisions once the split lines `stated` are read at a refresh at `at` with `parts` parts: those `statedSince` the line recorded
|
|
73
|
-
* last, each appended when its words differ from the line before, so a re-read appends nothing.
|
|
74
|
-
*/
|
|
75
46
|
export function revise(previous, stated, at, parts) {
|
|
76
47
|
const recorded = splitProse(previous.revisions?.at(-1)?.text ?? previous.split);
|
|
77
48
|
const { added } = statedSince(previous, stated).reduce(({ last, added }, line) => {
|
|
@@ -81,28 +52,18 @@ export function revise(previous, stated, at, parts) {
|
|
|
81
52
|
const revisions = [...(previous.revisions ?? []), ...added].slice(-MAX_REVISIONS);
|
|
82
53
|
return revisions.length ? revisions : undefined;
|
|
83
54
|
}
|
|
84
|
-
/**
|
|
85
|
-
* The split and revisions once the stated line `found` replaces a mention: when `found` is among the revisions (recorded beside the
|
|
86
|
-
* mention before #294), the first of them was stated first and is `split`, the rest stay; else `found` is, beside them all.
|
|
87
|
-
*/
|
|
88
55
|
export function replacingMention(found, revisions) {
|
|
89
56
|
const [first, ...rest] = revisions ?? [];
|
|
90
57
|
if (!first || !revisions.some((one) => splitProse(one.text) === splitProse(found)))
|
|
91
58
|
return { split: found, ...(first ? { revisions } : {}) };
|
|
92
59
|
return { split: first.text, ...(rest.length ? { revisions: rest } : {}) };
|
|
93
60
|
}
|
|
94
|
-
/** Heading, quote and list marks before a line's words, each with the space markdown puts after it, unlike the `#` of `#282`. */
|
|
95
61
|
const BLOCK_MARKS = /^(?:\s|#+\s|>\s|[-+*]\s|\d+[.)]\s)*/;
|
|
96
|
-
/** A bold or bold italic span within a line, opening at a word's start and closing at its end, unlike the asterisks of `2**3` or a `**` beside a slash. */
|
|
97
62
|
const BOLD = /(?<=^|[\s(["'])(\*\*\*?)([^\s*/][^*]*?)(?<![\s(/])\1(?=$|[\s.,;:!?)\]"'])/gu;
|
|
98
|
-
/** An italic span opens only before a letter, digit, quote or code span, which tells it from the globs of `*.ts and *.md` or `src/*.ts`. */
|
|
99
63
|
const ITALIC = /(?<=^|[\s(["'])\*([\p{L}\p{N}\0"'][^*]*?)(?<![\s(/])\*(?=$|[\s.,;:!?)\]"'])/gu;
|
|
100
|
-
/** A label the line opens with in underscores or a code span (`_split:_`, `` `split:` ``), which its colon tells from `__init__`. */
|
|
101
64
|
const LABEL = /^(__?|`)([^\s_`][^_`]*:)\1(?=\s)/;
|
|
102
|
-
/** A code span, whose asterisks are its words', and the stand-in it is masked by while emphasis is taken off. */
|
|
103
65
|
const CODE_SPAN = /`[^`]*`/g;
|
|
104
66
|
const MASKED = /\0(\d+)\0/g;
|
|
105
|
-
/** Marks taken off a line they wrap whole, with what its words must open with; `*` may recur inside as a glob's, the others not at all. */
|
|
106
67
|
const STARS = [
|
|
107
68
|
["***", /[^\s*/]/],
|
|
108
69
|
["**", /[^\s*/]/],
|
|
@@ -123,7 +84,6 @@ const unwrapped = (line, marks) => {
|
|
|
123
84
|
const found = marks.find(wraps);
|
|
124
85
|
return found ? inner(found[0]).trim() : line;
|
|
125
86
|
};
|
|
126
|
-
/** A split line as the listing shows it: one line, without its block marks, the asterisks wrapping it or a span in it, nor an underscore or code span wrapping it. */
|
|
127
87
|
export function splitProse(split) {
|
|
128
88
|
const code = [];
|
|
129
89
|
const masked = oneLine(split.replaceAll("\0", ""))
|
|
@@ -133,7 +93,6 @@ export function splitProse(split) {
|
|
|
133
93
|
const prose = unwrapped(masked, STARS).replace(BOLD, "$2").replace(ITALIC, "$1");
|
|
134
94
|
return unwrapped(prose.replace(MASKED, (_, i) => code[Number(i)]), WRAPS);
|
|
135
95
|
}
|
|
136
|
-
/** A part's status by `Standing`; one whose report state is not known is still out. */
|
|
137
96
|
export function partStatus({ stopped, stalled, report }) {
|
|
138
97
|
if (stopped)
|
|
139
98
|
return "stopped";
|
|
@@ -145,65 +104,50 @@ export function partStatus({ stopped, stalled, report }) {
|
|
|
145
104
|
return report.ended;
|
|
146
105
|
return report.status ?? "done";
|
|
147
106
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
107
|
+
export function subagentStatus({ running, stalled, outcome }) {
|
|
108
|
+
if (stalled)
|
|
109
|
+
return "stalled";
|
|
110
|
+
if (running)
|
|
111
|
+
return "out";
|
|
112
|
+
return outcome === "failed" || outcome === "interrupted" ? outcome : "done";
|
|
113
|
+
}
|
|
152
114
|
export function unreadPart(read, last, gone) {
|
|
153
115
|
const settledLast = last && isSettled(last.status) ? last.status : "gone";
|
|
154
116
|
const status = !gone || isSettled(read.status) ? read.status : settledLast;
|
|
155
117
|
const { title, duration, cost } = last ?? read;
|
|
156
118
|
return { ...read, title, status, ...(duration !== undefined ? { duration } : {}), ...(cost !== undefined ? { cost } : {}) };
|
|
157
119
|
}
|
|
158
|
-
/**
|
|
159
|
-
* The parts of a refreshed record: those recorded before, in their order, each as `current` has it (read now, or found
|
|
160
|
-
* gone), else as last recorded, then those first seen now, in the order the tree is drawn.
|
|
161
|
-
*/
|
|
162
120
|
export function mergeParts(previous, current) {
|
|
163
121
|
const now = new Map(current.map((part) => [part.sessionID, part]));
|
|
164
122
|
const kept = previous.map((part) => now.get(part.sessionID) ?? part);
|
|
165
123
|
const known = new Set(previous.map((part) => part.sessionID));
|
|
166
124
|
return [...kept, ...current.filter((part) => !known.has(part.sessionID))];
|
|
167
125
|
}
|
|
168
|
-
/** `recalled` after one more call at `at`, the top's first call noted when `byTop`. */
|
|
169
126
|
export function noteRecall(recalled, byTop, at) {
|
|
170
127
|
const calls = (recalled?.calls ?? 0) + 1;
|
|
171
128
|
const first = recalled?.at ?? (byTop ? at : undefined);
|
|
172
129
|
return first === undefined ? { calls } : { at: first, calls };
|
|
173
130
|
}
|
|
174
|
-
/** A text as the one line a lesson is: whitespace and line breaks collapsed to spaces, trimmed. */
|
|
175
131
|
export const oneLine = (text) => text.replace(/\s+/g, " ").trim();
|
|
176
|
-
/**
|
|
177
|
-
* A hand-written lesson's text as one line, clipped at `LESSON_MAX`; `clipped` is the length it had when it was clipped.
|
|
178
|
-
* courier_note does not clip: it refuses a line over `LESSON_MAX` (`note.ts`).
|
|
179
|
-
*/
|
|
180
132
|
export function lessonText(text) {
|
|
181
133
|
const line = oneLine(text);
|
|
182
134
|
if (line.length <= LESSON_MAX)
|
|
183
135
|
return { text: line };
|
|
184
136
|
return { text: line.slice(0, LESSON_MAX).trimEnd(), clipped: line.length };
|
|
185
137
|
}
|
|
186
|
-
/** One reader tree's `reads` after courier_recall listed the record or gave it in full at `at`. */
|
|
187
138
|
export function noteRead(reads, kind, at) {
|
|
188
139
|
return { shown: (reads?.shown ?? 0) + (kind === "shown" ? 1 : 0), read: (reads?.read ?? 0) + (kind === "read" ? 1 : 0), at: Math.max(reads?.at ?? 0, at) };
|
|
189
140
|
}
|
|
190
|
-
/** The reads of every reader tree as one: the counts summed, `at` the latest; none without a reader. */
|
|
191
141
|
export function sumReads(reads) {
|
|
192
142
|
if (!reads.length)
|
|
193
143
|
return undefined;
|
|
194
144
|
return reads.reduce((sum, one) => ({ shown: sum.shown + one.shown, read: sum.read + one.read, at: Math.max(sum.at, one.at) }));
|
|
195
145
|
}
|
|
196
|
-
/**
|
|
197
|
-
* The records a project keeps at `now`: every pinned one, whatever its age, and of the rest the first `MAX_RECORDS` by
|
|
198
|
-
* whether each has a lesson, then by `movedAt`, none older than `RETENTION_MS`; the rest are dropped.
|
|
199
|
-
*/
|
|
200
146
|
export function keep(records, now) {
|
|
201
147
|
const pinned = records.filter((record) => record.pinned);
|
|
202
|
-
// Aged out first, so that a stale record takes no slot from a live one behind it in the order.
|
|
203
148
|
const live = records.filter((record) => !record.pinned && now - record.movedAt <= RETENTION_MS);
|
|
204
149
|
const ordered = live.toSorted((a, b) => Number(Boolean(b.lesson)) - Number(Boolean(a.lesson)) || b.movedAt - a.movedAt);
|
|
205
150
|
const kept = [...pinned, ...ordered.slice(0, MAX_RECORDS)];
|
|
206
151
|
return { kept, dropped: records.filter((record) => !kept.includes(record)) };
|
|
207
152
|
}
|
|
208
|
-
/** Records as courier_recall and the memory view list them: the pinned first, then by when each last moved, newest first. */
|
|
209
153
|
export const pinnedFirst = (records) => records.toSorted((a, b) => Number(Boolean(b.pinned)) - Number(Boolean(a.pinned)) || b.movedAt - a.movedAt);
|
package/dist/memory/events.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { EventModule, EventPorts } from "../platform/events.js";
|
|
2
2
|
import type { MemoryState, RefreshPorts } from "./ports.js";
|
|
3
|
-
/** OpenCode's `session.execution.*` events, as the memory reads them. */
|
|
4
3
|
interface Execution {
|
|
5
4
|
readonly id: string;
|
|
6
5
|
readonly created?: number;
|
|
@@ -9,18 +8,8 @@ interface Execution {
|
|
|
9
8
|
readonly reason?: string;
|
|
10
9
|
};
|
|
11
10
|
}
|
|
12
|
-
/** The ports the event module runs on: the watcher's, which every hub-v2 member joins with. */
|
|
13
11
|
export type MemoryEventPorts = RefreshPorts & EventPorts;
|
|
14
|
-
/**
|
|
15
|
-
* The top of the tree a session is in: by the reverse index for a spawned one, else the session itself when it called
|
|
16
|
-
* courier_recall or has started sessions; undefined for a session in no tree.
|
|
17
|
-
*/
|
|
18
12
|
export declare function topOf(ports: Pick<RefreshPorts, "storage">, state: Pick<MemoryState, "tops">, sessionID: string): Promise<string | undefined>;
|
|
19
|
-
/** A turn ended in a tree: its record is written again, once however many subscriptions see the event. Returns the top refreshed. */
|
|
20
13
|
export declare function noteTurnEnd(ports: RefreshPorts, state: MemoryState, event: Execution): Promise<string | undefined>;
|
|
21
|
-
/**
|
|
22
|
-
* Keeps each tree's record current: after the tree's and the person's notices of a turn's end have gone out, and the
|
|
23
|
-
* report state it settled is written, the record of the tree it belongs to is rebuilt. A failure is logged, never thrown.
|
|
24
|
-
*/
|
|
25
14
|
export declare function memoryEvents(state: MemoryState): EventModule<MemoryEventPorts>;
|
|
26
15
|
export {};
|
package/dist/memory/events.js
CHANGED
|
@@ -3,10 +3,6 @@ import { childrenOf, endsIdle, indexedLineage } from "../tree/index.js";
|
|
|
3
3
|
import { refresh } from "./record.js";
|
|
4
4
|
const TURN_END_TYPES = ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"];
|
|
5
5
|
const TURN_ENDS = new Set(TURN_END_TYPES);
|
|
6
|
-
/**
|
|
7
|
-
* The top of the tree a session is in: by the reverse index for a spawned one, else the session itself when it called
|
|
8
|
-
* courier_recall or has started sessions; undefined for a session in no tree.
|
|
9
|
-
*/
|
|
10
6
|
export async function topOf(ports, state, sessionID) {
|
|
11
7
|
const chain = await indexedLineage(ports.storage, sessionID);
|
|
12
8
|
if (chain.length)
|
|
@@ -15,16 +11,13 @@ export async function topOf(ports, state, sessionID) {
|
|
|
15
11
|
return sessionID;
|
|
16
12
|
return (await childrenOf(ports.storage, sessionID)).length ? sessionID : undefined;
|
|
17
13
|
}
|
|
18
|
-
/** A turn ended in a tree: its record is written again, once however many subscriptions see the event. Returns the top refreshed. */
|
|
19
14
|
export async function noteTurnEnd(ports, state, event) {
|
|
20
15
|
const top = await topOf(ports, state, event.data.sessionID);
|
|
21
16
|
if (top === undefined)
|
|
22
17
|
return undefined;
|
|
23
|
-
//
|
|
18
|
+
// a plain permission decline also reports "shutdown" (api-notes)
|
|
24
19
|
if (event.data.reason === "shutdown" && !endsIdle(await ports.session.context({ sessionID: event.data.sessionID }).catch(() => [])))
|
|
25
20
|
return undefined;
|
|
26
|
-
// Claimed after the lookup, so other sessions' turns do not crowd `seen`; released on a failure, as a write is
|
|
27
|
-
// the same twice, so the hub's other subscription may still make it.
|
|
28
21
|
if (!claim(state.seen, event.id))
|
|
29
22
|
return undefined;
|
|
30
23
|
try {
|
|
@@ -36,10 +29,6 @@ export async function noteTurnEnd(ports, state, event) {
|
|
|
36
29
|
}
|
|
37
30
|
return top;
|
|
38
31
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Keeps each tree's record current: after the tree's and the person's notices of a turn's end have gone out, and the
|
|
41
|
-
* report state it settled is written, the record of the tree it belongs to is rebuilt. A failure is logged, never thrown.
|
|
42
|
-
*/
|
|
43
32
|
export function memoryEvents(state) {
|
|
44
33
|
return {
|
|
45
34
|
types: TURN_END_TYPES,
|
package/dist/memory/forget.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { MemoryPorts } from "./ports.js";
|
|
2
|
-
/** What forgetting did: the record is gone, only its lesson is, or there was nothing to forget. */
|
|
3
2
|
export type Forgotten = "record" | "lesson" | "none";
|
|
4
|
-
/**
|
|
5
|
-
* Forgets the record of the tree under `sessionID` in the ports' project, as the memory view's `x` does, or with
|
|
6
|
-
* `lesson` only its lesson, writing the record back without it. Whatever the memory option says: it only removes.
|
|
7
|
-
*/
|
|
8
3
|
export declare function forget(ports: Pick<MemoryPorts, "storage" | "projectID">, sessionID: string, { lesson }?: {
|
|
9
4
|
lesson?: boolean | undefined;
|
|
10
5
|
}): Promise<Forgotten>;
|
package/dist/memory/forget.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { memoryStore } from "./store.js";
|
|
2
|
-
/**
|
|
3
|
-
* Forgets the record of the tree under `sessionID` in the ports' project, as the memory view's `x` does, or with
|
|
4
|
-
* `lesson` only its lesson, writing the record back without it. Whatever the memory option says: it only removes.
|
|
5
|
-
*/
|
|
6
2
|
export async function forget(ports, sessionID, { lesson = false } = {}) {
|
|
7
3
|
const store = memoryStore(ports.storage);
|
|
8
4
|
const record = await store.get(ports.projectID, sessionID);
|
package/dist/memory/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// What the rest of the plugin uses of the memory: its tools, event module, RPC, option and sweep for `src/index.ts`,
|
|
2
|
-
// and the record's types.
|
|
3
1
|
export { LESSON_MAX, MAX_RECORDS, NOT_STATED, readMemory, RETENTION_MS } from "./domain.js";
|
|
4
2
|
export { memoryEvents } from "./events.js";
|
|
5
3
|
export { lessonsOf } from "./lessons.js";
|
package/dist/memory/lessons.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { type Entry } from "../tree/index.js";
|
|
2
2
|
import type { MemoryPorts } from "./ports.js";
|
|
3
|
-
/**
|
|
4
|
-
* The `lessons` entry of this project, as `courier_spawn` writes it to a spawned session before its prompt and to a root at
|
|
5
|
-
* its first spawn: none with the memory option off, or while no record of the project holds a lesson.
|
|
6
|
-
*/
|
|
7
3
|
export declare function lessonsOf(ports: MemoryPorts): Promise<ReadonlyArray<Entry>>;
|
|
8
|
-
/**
|
|
9
|
-
* Puts the project's lessons entry again, once a lesson was written, on the writer's tree's top, the top's current children
|
|
10
|
-
* below `maxDepth` and the writer, each only where it differs from the entry stored; a failure is logged. Returns the sessions put.
|
|
11
|
-
*/
|
|
12
4
|
export declare function relesson(ports: MemoryPorts, writerID: string, top: string): Promise<string[]>;
|
package/dist/memory/lessons.js
CHANGED
|
@@ -3,20 +3,12 @@ import { childrenOf } from "../tree/index.js";
|
|
|
3
3
|
import { pinnedFirst } from "./domain.js";
|
|
4
4
|
import { memoryStore } from "./store.js";
|
|
5
5
|
import { LESSONS_KEY, lessonsEntry } from "./text.js";
|
|
6
|
-
/**
|
|
7
|
-
* The `lessons` entry of this project, as `courier_spawn` writes it to a spawned session before its prompt and to a root at
|
|
8
|
-
* its first spawn: none with the memory option off, or while no record of the project holds a lesson.
|
|
9
|
-
*/
|
|
10
6
|
export async function lessonsOf(ports) {
|
|
11
7
|
if (!ports.memory)
|
|
12
8
|
return [];
|
|
13
9
|
const value = lessonsEntry(pinnedFirst(await memoryStore(ports.storage).ofProject(ports.projectID)));
|
|
14
10
|
return value === undefined ? [] : [{ key: LESSONS_KEY, value }];
|
|
15
11
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Puts the project's lessons entry again, once a lesson was written, on the writer's tree's top, the top's current children
|
|
18
|
-
* below `maxDepth` and the writer, each only where it differs from the entry stored; a failure is logged. Returns the sessions put.
|
|
19
|
-
*/
|
|
20
12
|
export async function relesson(ports, writerID, top) {
|
|
21
13
|
const entries = ports.session.instructions?.entry;
|
|
22
14
|
if (!entries)
|
|
@@ -45,7 +37,6 @@ export async function relesson(ports, writerID, top) {
|
|
|
45
37
|
return true;
|
|
46
38
|
}
|
|
47
39
|
catch (error) {
|
|
48
|
-
// A child on the roster that OpenCode no longer knows is a normal path, not one to log.
|
|
49
40
|
if (!isNotFound(error))
|
|
50
41
|
ports.log(`courier_note: the lessons entry of ${sessionID} could not be written: ${String(error)}`);
|
|
51
42
|
return false;
|
package/dist/memory/note.d.ts
CHANGED
|
@@ -3,10 +3,6 @@ import type { MemoryPorts } from "./ports.js";
|
|
|
3
3
|
export interface NoteInput {
|
|
4
4
|
readonly text: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Why courier_note wrote nothing: the option is off, the caller is a leaf, its text is empty or longer than `LESSON_MAX`
|
|
8
|
-
* (`length` is what it had as one line), or its tree has no record to write on.
|
|
9
|
-
*/
|
|
10
6
|
export type Refusal = {
|
|
11
7
|
readonly kind: "off";
|
|
12
8
|
} | {
|
|
@@ -22,7 +18,6 @@ export type Refusal = {
|
|
|
22
18
|
readonly kind: "unrecorded";
|
|
23
19
|
readonly top: string;
|
|
24
20
|
};
|
|
25
|
-
/** What courier_note gives: the lesson written on the caller's tree's record, with the one it replaced, or why it was not. */
|
|
26
21
|
export type Noted = {
|
|
27
22
|
readonly kind: "noted";
|
|
28
23
|
readonly projectID: string;
|
|
@@ -30,8 +25,4 @@ export type Noted = {
|
|
|
30
25
|
readonly lesson: Lesson;
|
|
31
26
|
readonly replaced?: Lesson;
|
|
32
27
|
} | Refusal;
|
|
33
|
-
/**
|
|
34
|
-
* courier_note: writes `text`, as one line of at most `LESSON_MAX` characters (longer is refused, never clipped), as the lesson
|
|
35
|
-
* on the record of the caller's own tree in this project, replacing the one there; the top is the top of the caller's lineage, or itself.
|
|
36
|
-
*/
|
|
37
28
|
export declare function note(ports: MemoryPorts, callerID: string, input: NoteInput): Promise<Noted>;
|
package/dist/memory/note.js
CHANGED
|
@@ -2,10 +2,6 @@ import { lineage } from "../tree/index.js";
|
|
|
2
2
|
import { LESSON_MAX, oneLine } from "./domain.js";
|
|
3
3
|
import { relesson } from "./lessons.js";
|
|
4
4
|
import { memoryStore } from "./store.js";
|
|
5
|
-
/**
|
|
6
|
-
* courier_note: writes `text`, as one line of at most `LESSON_MAX` characters (longer is refused, never clipped), as the lesson
|
|
7
|
-
* on the record of the caller's own tree in this project, replacing the one there; the top is the top of the caller's lineage, or itself.
|
|
8
|
-
*/
|
|
9
5
|
export async function note(ports, callerID, input) {
|
|
10
6
|
if (!ports.memory)
|
|
11
7
|
return { kind: "off" };
|
|
@@ -26,7 +22,6 @@ export async function note(ports, callerID, input) {
|
|
|
26
22
|
return { kind: "unrecorded", top };
|
|
27
23
|
const lesson = { text, at: ports.now(), ...(depth ? { depth } : {}) };
|
|
28
24
|
await store.put(projectID, top, { ...record, lesson });
|
|
29
|
-
// The lessons entry of the top, its children and the writer moves with it; the next tree's sessions get it at their spawn.
|
|
30
25
|
await relesson(ports, callerID, top);
|
|
31
26
|
return { kind: "noted", projectID, top, lesson, ...(record.lesson ? { replaced: record.lesson } : {}) };
|
|
32
27
|
}
|
package/dist/memory/pin.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import type { MemoryPorts } from "./ports.js";
|
|
2
|
-
/** What pinning did: the record is pinned, unpinned, or there is no record to pin. */
|
|
3
2
|
export type PinOutcome = "pinned" | "unpinned" | "none";
|
|
4
|
-
/**
|
|
5
|
-
* Pins the record of the tree under `sessionID` in the ports' project, as the memory view's `p` does, or with `pinned`
|
|
6
|
-
* false unpins it; a record pinned already keeps when it was. Whatever the memory option says: a pin is the person's.
|
|
7
|
-
*/
|
|
8
3
|
export declare function pin(ports: Pick<MemoryPorts, "storage" | "projectID" | "now">, sessionID: string, pinned: boolean): Promise<PinOutcome>;
|
package/dist/memory/pin.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { memoryStore } from "./store.js";
|
|
2
|
-
/**
|
|
3
|
-
* Pins the record of the tree under `sessionID` in the ports' project, as the memory view's `p` does, or with `pinned`
|
|
4
|
-
* false unpins it; a record pinned already keeps when it was. Whatever the memory option says: a pin is the person's.
|
|
5
|
-
*/
|
|
6
2
|
export async function pin(ports, sessionID, pinned) {
|
|
7
3
|
const store = memoryStore(ports.storage);
|
|
8
4
|
const record = await store.get(ports.projectID, sessionID);
|
package/dist/memory/ports.d.ts
CHANGED
|
@@ -1,55 +1,35 @@
|
|
|
1
|
-
/** The memory's ports: what refreshing a record and courier_recall need of the plugin context, and what the process shares. */
|
|
2
1
|
import type { Plugin } from "@opencode/plugin";
|
|
3
2
|
import type { Storage } from "../platform/storage.js";
|
|
4
|
-
import type { Instructions, ModelWindow } from "../tree/index.js";
|
|
3
|
+
import type { Instructions, Lister, ModelWindow } from "../tree/index.js";
|
|
5
4
|
import type { Pinned, ReadKind, Reads, StoredRecord, TreeRecord } from "./domain.js";
|
|
6
5
|
export type { Lesson, PartRecord, PartStatus, Pinned, ReadKind, Reads, Recalled, Revision, StoredRecord, TreeRecord } from "./domain.js";
|
|
7
6
|
type Context = Plugin.Context;
|
|
8
|
-
/** What refreshing a record runs on: every hub-v2 member's watch ports have these, so the event module runs through any. */
|
|
9
7
|
export interface RefreshPorts {
|
|
10
8
|
readonly storage: Storage;
|
|
11
|
-
readonly session: Pick<Context["session"], "get" | "context"
|
|
9
|
+
readonly session: Pick<Context["session"], "get" | "context"> & Lister;
|
|
12
10
|
readonly now: () => number;
|
|
13
11
|
readonly log: (message: string) => void;
|
|
14
12
|
}
|
|
15
|
-
/** What courier_recall and courier_note run on: one instance's ports. */
|
|
16
13
|
export interface MemoryPorts extends RefreshPorts {
|
|
17
|
-
/** With `instructions`, the keyed session entries, which courier_note puts the lessons entry through; missing on a host before 2.2.0. */
|
|
18
14
|
readonly session: RefreshPorts["session"] & Instructions;
|
|
19
|
-
/** The project of the plugin's location, whose records the tools read and write. */
|
|
20
15
|
readonly projectID: string;
|
|
21
|
-
/** The `memory` option: off, and the tools write and list nothing. */
|
|
22
16
|
readonly memory: boolean;
|
|
23
|
-
/** The tree's `maxDepth` limit: a session at it is a leaf, which courier_note refuses. */
|
|
24
17
|
readonly maxDepth: number;
|
|
25
18
|
}
|
|
26
|
-
/** What every plugin instance in the process shares of the memory, under a fixed key outside the hub. */
|
|
27
19
|
export interface MemoryState {
|
|
28
|
-
/** The context windows of the models known, by `<providerID>/<modelID>`, filled as each instance loads. */
|
|
29
20
|
readonly windows: Map<string, ModelWindow>;
|
|
30
|
-
/** The directories of the locations whose instance has memory off: no record is written for a tree whose top runs there. */
|
|
31
21
|
readonly off: Set<string>;
|
|
32
|
-
/** The top sessions that called courier_recall, whose turn ends refresh their record before they have started a session. */
|
|
33
22
|
readonly tops: Set<string>;
|
|
34
|
-
/** The turn ends recorded, so the hub's two subscriptions refresh once. */
|
|
35
23
|
readonly seen: Set<string>;
|
|
36
24
|
}
|
|
37
|
-
/** The records, by project and top session, each read with its reads and pin, which lie beside it and are written apart. */
|
|
38
25
|
export interface MemoryStore {
|
|
39
26
|
get(projectID: string, sessionID: string): Promise<TreeRecord | undefined>;
|
|
40
|
-
/** Writes the record's facts; its `reads` and `pinned` are left out, as nothing but `noteRead` and `pin` write them. */
|
|
41
27
|
put(projectID: string, sessionID: string, record: TreeRecord): Promise<void>;
|
|
42
|
-
/** Removes the record with its reads and pin. */
|
|
43
28
|
remove(projectID: string, sessionID: string): Promise<void>;
|
|
44
|
-
/** Counts a listing or a full read of the record by the tree under `by` at `at`; gives that tree's reads of it. */
|
|
45
29
|
noteRead(projectID: string, sessionID: string, by: string, kind: ReadKind, at: number): Promise<Reads>;
|
|
46
|
-
/** Pins the record, or with `undefined` unpins it. */
|
|
47
30
|
pin(projectID: string, sessionID: string, pinned: Pinned | undefined): Promise<void>;
|
|
48
|
-
/** A project's records, in key order. */
|
|
49
31
|
ofProject(projectID: string): Promise<StoredRecord[]>;
|
|
50
|
-
/** Every project's records. */
|
|
51
32
|
all(): Promise<StoredRecord[]>;
|
|
52
|
-
/** The trees with reads or a pin but no record, left by a record removed while it was read or pinned. */
|
|
53
33
|
strays(): Promise<Array<{
|
|
54
34
|
projectID: string;
|
|
55
35
|
sessionID: string;
|
package/dist/memory/recall.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { MemoryPorts, MemoryState } from "./ports.js";
|
|
|
3
3
|
export interface RecallInput {
|
|
4
4
|
readonly sessionID?: string;
|
|
5
5
|
}
|
|
6
|
-
/** What courier_recall gives: the option is off, the project's records with the caller's tree named, one record in full, or none under that session. */
|
|
7
6
|
export type Recall = {
|
|
8
7
|
readonly kind: "off";
|
|
9
8
|
} | {
|
|
@@ -20,8 +19,4 @@ export type Recall = {
|
|
|
20
19
|
readonly projectID: string;
|
|
21
20
|
readonly sessionID: string;
|
|
22
21
|
};
|
|
23
|
-
/**
|
|
24
|
-
* courier_recall: notes the call on the caller's tree, then lists the project's records, pinned first and then newest first,
|
|
25
|
-
* or gives the one under `sessionID` in full, each counted on the other trees' records; with the memory option off, nothing is written or read.
|
|
26
|
-
*/
|
|
27
22
|
export declare function recall(ports: MemoryPorts, state: Pick<MemoryState, "tops">, callerID: string, input: RecallInput): Promise<Recall>;
|
package/dist/memory/recall.js
CHANGED
|
@@ -3,11 +3,9 @@ import { finite, millis } from "../platform/json.js";
|
|
|
3
3
|
import { lineage } from "../tree/index.js";
|
|
4
4
|
import { NOT_STATED, noteRead, noteRecall, pinnedFirst } from "./domain.js";
|
|
5
5
|
import { memoryStore } from "./store.js";
|
|
6
|
-
/** The top of the caller's tree: the top of its lineage, or itself. */
|
|
7
6
|
async function topOfCaller(ports, callerID) {
|
|
8
7
|
return (await lineage(ports.storage, callerID)).at(-1)?.parentID ?? callerID;
|
|
9
8
|
}
|
|
10
|
-
/** Notes the call on the caller's tree's record, which is written now if the tree has none yet, so the call is remembered before any session is started. */
|
|
11
9
|
async function note(ports, top, byTop) {
|
|
12
10
|
const store = memoryStore(ports.storage);
|
|
13
11
|
const now = ports.now();
|
|
@@ -26,10 +24,6 @@ async function note(ports, top, byTop) {
|
|
|
26
24
|
parts: [],
|
|
27
25
|
});
|
|
28
26
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Counts, on each record of another tree, that this call listed it or gave it in full, best effort: a count lost is
|
|
31
|
-
* logged, never the call's failure. Gives the reads of the reader's tree on each, in order, or undefined where it failed.
|
|
32
|
-
*/
|
|
33
27
|
function count(ports, top, kind, sessionIDs) {
|
|
34
28
|
const store = memoryStore(ports.storage);
|
|
35
29
|
const now = ports.now();
|
|
@@ -38,10 +32,6 @@ function count(ports, top, kind, sessionIDs) {
|
|
|
38
32
|
return undefined;
|
|
39
33
|
})));
|
|
40
34
|
}
|
|
41
|
-
/**
|
|
42
|
-
* courier_recall: notes the call on the caller's tree, then lists the project's records, pinned first and then newest first,
|
|
43
|
-
* or gives the one under `sessionID` in full, each counted on the other trees' records; with the memory option off, nothing is written or read.
|
|
44
|
-
*/
|
|
45
35
|
export async function recall(ports, state, callerID, input) {
|
|
46
36
|
if (!ports.memory)
|
|
47
37
|
return { kind: "off" };
|
|
@@ -50,13 +40,11 @@ export async function recall(ports, state, callerID, input) {
|
|
|
50
40
|
addBounded(state.tops, top, SEEN_MAX);
|
|
51
41
|
const store = memoryStore(ports.storage);
|
|
52
42
|
const { projectID } = ports;
|
|
53
|
-
// Some models send null, or an empty string, for an optional field they leave out.
|
|
54
43
|
const wanted = typeof input.sessionID === "string" ? input.sessionID.trim() : "";
|
|
55
44
|
if (wanted) {
|
|
56
45
|
const record = await store.get(projectID, wanted);
|
|
57
46
|
if (!record)
|
|
58
47
|
return { kind: "missing", projectID, sessionID: wanted };
|
|
59
|
-
// Another tree's record is given with this read added to the reads just read; the caller's own is not counted.
|
|
60
48
|
const [counted] = wanted === top ? [undefined] : await count(ports, top, "read", [wanted]);
|
|
61
49
|
const reads = counted ? noteRead(record.reads, "read", counted.at) : record.reads;
|
|
62
50
|
return { kind: "record", projectID, record: { ...record, ...(reads ? { reads } : {}), projectID, sessionID: wanted } };
|
package/dist/memory/record.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
import { type TreeRecord } from "./domain.js";
|
|
2
2
|
import type { MemoryState, RefreshPorts } from "./ports.js";
|
|
3
|
-
/**
|
|
4
|
-
* Writes the record of the tree under `top` as it stands at `at`, after the turn of `ended`; a part the roster has forgotten
|
|
5
|
-
* keeps its last recorded state, unless found gone before it settled. Nothing for a top unknown to OpenCode or with memory off.
|
|
6
|
-
*/
|
|
7
3
|
export declare function refresh(ports: RefreshPorts, state: Pick<MemoryState, "windows" | "off">, top: string, at: number, ended?: string): Promise<TreeRecord | undefined>;
|