opencode-courier 0.8.0 → 0.10.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 +19 -10
- package/dist/kernel/protocol.d.ts +6 -0
- package/dist/kernel/protocol.js +13 -1
- package/dist/memory/domain.d.ts +82 -13
- package/dist/memory/domain.js +139 -14
- package/dist/memory/index.d.ts +3 -2
- package/dist/memory/index.js +1 -0
- package/dist/memory/pin.d.ts +8 -0
- package/dist/memory/pin.js +13 -0
- package/dist/memory/ports.d.ts +14 -3
- package/dist/memory/recall.d.ts +2 -2
- package/dist/memory/recall.js +24 -5
- package/dist/memory/record.d.ts +1 -1
- package/dist/memory/record.js +50 -18
- package/dist/memory/rpc-handlers.d.ts +54 -9
- package/dist/memory/rpc-handlers.js +8 -6
- package/dist/memory/rpc.d.ts +80 -8
- package/dist/memory/rpc.js +13 -2
- package/dist/memory/store.d.ts +5 -1
- package/dist/memory/store.js +99 -15
- package/dist/memory/sweep.d.ts +4 -1
- package/dist/memory/sweep.js +6 -2
- package/dist/memory/text.d.ts +12 -4
- package/dist/memory/text.js +33 -12
- package/dist/memory/tools.js +2 -2
- package/dist/memory/tui.d.ts +14 -5
- package/dist/rpc.d.ts +1 -1
- package/dist/skill.d.ts +4 -1
- package/dist/skill.js +25 -16
- package/dist/tree/mascot.js +1 -1
- package/dist/tree/rpc-handlers.d.ts +4 -4
- package/dist/tree/rpc.d.ts +4 -4
- package/dist/tree/text.js +11 -8
- package/dist/tui.js +80 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ version than the pinned one writes one line to the server log
|
|
|
108
108
|
<!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
|
|
109
109
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
110
110
|
|---|---|
|
|
111
|
-
| 0.7.3, 0.8.0 | 2.0.32 |
|
|
111
|
+
| 0.7.3, 0.8.0, 0.9.0, 0.10.0 | 2.0.32 |
|
|
112
112
|
| 0.7.2 | 2.0.31 |
|
|
113
113
|
| 0.7.1 | 2.0.30 |
|
|
114
114
|
<!-- compat-table:end -->
|
|
@@ -246,13 +246,21 @@ Each spawned session's brief says when to split its task and when to do it itsel
|
|
|
246
246
|
## Memory
|
|
247
247
|
|
|
248
248
|
The plugin keeps **one record per session tree** in the project, from facts it already has: when the
|
|
249
|
-
tree started and last moved, the top session's title and `split:` line,
|
|
249
|
+
tree started and last moved, the top session's title and first `split:` line, with up to 3 later ones
|
|
250
|
+
that read otherwise (when each was stated, and after how many parts), and for each session started
|
|
250
251
|
in the tree its title, depth, isolation, join group, status, duration, cost, context use and spawns
|
|
251
|
-
refused; never a report's body or artifacts.
|
|
252
|
+
refused; never a report's body or artifacts. A session deleted since keeps its last status and
|
|
253
|
+
duration, or reads `gone` when it had not settled, so it never holds its tree at `not finished`.
|
|
254
|
+
The one thing a model writes into it is **the tree's
|
|
252
255
|
lesson**, with `courier_note`: one line of at most 300 characters, from the session that verified the
|
|
253
|
-
tree, with the job it comes from and the reason. `courier_recall` reads the records back
|
|
254
|
-
|
|
255
|
-
the
|
|
256
|
+
tree, with the job it comes from and the reason. `courier_recall` reads the records back, each line
|
|
257
|
+
showing the split line as prose (its markdown marks dropped, the asterisks of its globs kept) cut at a
|
|
258
|
+
word, then the latest later one (`then, after 1 part: split: 2 parts: …`), and saying how long the tree ran and how long ago it last moved; the `courier-orchestrate` skill asks for
|
|
259
|
+
one call before the split decision and one `courier_note` before the top replies. Each record counts
|
|
260
|
+
how often other trees' `courier_recall` calls listed it and opened it in full, and when last, so the
|
|
261
|
+
person can tell a lesson still read from one nobody reads; a record the person pins in the terminal
|
|
262
|
+
is listed first and never swept. Each project keeps 30 unpinned records, those with a lesson first,
|
|
263
|
+
and none older than 90 days. `"memory":
|
|
256
264
|
false` in the plugin's `options` turns it off ([more](docs/reference.md#memory)).
|
|
257
265
|
|
|
258
266
|
## In the terminal
|
|
@@ -292,10 +300,11 @@ permission prompt or form is shown in the session, OpenCode hides the footer, so
|
|
|
292
300
|
`ctrl+x k`.
|
|
293
301
|
|
|
294
302
|
`/courier memory` (or the palette's "Courier: memory") lists the project's [tree records](#memory),
|
|
295
|
-
|
|
296
|
-
`enter` shows one in full; `
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
pinned first, then newest first grouped by the day each tree last moved, one row each with how often
|
|
304
|
+
other trees read it and its lesson under it. `enter` shows one in full; `p` pins or unpins the record,
|
|
305
|
+
and a pinned one is listed first and never swept; `x` forgets the record under the row, or in a
|
|
306
|
+
record on its lesson's rows only the lesson, after a confirm; `esc` goes back and closes. The search
|
|
307
|
+
takes every letter but `x` and `p` ([more](docs/reference.md#the-memory-view)).
|
|
299
308
|
|
|
300
309
|
## Native child sessions
|
|
301
310
|
|
|
@@ -11,9 +11,15 @@ export type Status = (typeof STATUSES)[number];
|
|
|
11
11
|
export declare const isStatus: (value: unknown) => value is Status;
|
|
12
12
|
/** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
|
|
13
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
|
+
/** The third form, for the root alone: it hands a heavy task whole to one session, to keep its own context. */
|
|
15
|
+
export declare const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
|
|
16
|
+
/** `SPLIT_LINE` with the root's third form: the root's pointer offers it, a child's brief and role part never do. */
|
|
17
|
+
export declare const ROOT_SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part) or split: 1 part: <the task>, to keep my context";
|
|
14
18
|
export declare const SKILL_ID = "courier-orchestrate";
|
|
15
19
|
export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
|
|
16
20
|
export declare function clipText(text: string, max?: number): string;
|
|
21
|
+
/** `clipText` cut at the last space within `max` instead, so no word is cut in two, unless that keeps less than half of `max`. */
|
|
22
|
+
export declare function clipWords(text: string, max: number): string;
|
|
17
23
|
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
18
24
|
export declare function defuse(text: string): string;
|
|
19
25
|
/** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
|
package/dist/kernel/protocol.js
CHANGED
|
@@ -10,6 +10,10 @@ export const STATUSES = ["done", "partial", "blocked", "failed"];
|
|
|
10
10
|
export const isStatus = (value) => typeof value === "string" && STATUSES.includes(value);
|
|
11
11
|
/** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
|
|
12
12
|
export const SPLIT_LINE = "split: no, because …; considered: <the parts you weighed> or split: <n> parts: … (one phrase per part)";
|
|
13
|
+
/** The third form, for the root alone: it hands a heavy task whole to one session, to keep its own context. */
|
|
14
|
+
export const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
|
|
15
|
+
/** `SPLIT_LINE` with the root's third form: the root's pointer offers it, a child's brief and role part never do. */
|
|
16
|
+
export const ROOT_SPLIT_LINE = `${SPLIT_LINE} or ${ONE_PART_LINE}`;
|
|
13
17
|
export const SKILL_ID = "courier-orchestrate";
|
|
14
18
|
export function envelope(from, message, attributes = {}) {
|
|
15
19
|
const extra = Object.entries(attributes)
|
|
@@ -19,8 +23,16 @@ export function envelope(from, message, attributes = {}) {
|
|
|
19
23
|
}
|
|
20
24
|
/** Longest piece of free text (a review body, a generic payload) copied into a delivered message. */
|
|
21
25
|
const MAX_TEXT = 1500;
|
|
26
|
+
const clipped = (text, kept) => `${kept}… [${text.length - kept.length} more characters]`;
|
|
22
27
|
export function clipText(text, max = MAX_TEXT) {
|
|
23
|
-
return text.length > max ?
|
|
28
|
+
return text.length > max ? clipped(text, text.slice(0, max)) : text;
|
|
29
|
+
}
|
|
30
|
+
/** `clipText` cut at the last space within `max` instead, so no word is cut in two, unless that keeps less than half of `max`. */
|
|
31
|
+
export function clipWords(text, max) {
|
|
32
|
+
if (text.length <= max)
|
|
33
|
+
return text;
|
|
34
|
+
const space = text.lastIndexOf(" ", max);
|
|
35
|
+
return clipped(text, (space >= max / 2 ? text.slice(0, space) : text.slice(0, max)).trimEnd());
|
|
24
36
|
}
|
|
25
37
|
/** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
|
|
26
38
|
export function defuse(text) {
|
package/dist/memory/domain.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The memory's pure rules, on plain data: the option, a record's shape, each part's status, the split line,
|
|
3
|
-
* retention and how a refresh keeps the parts the roster has forgotten. Imports nothing of the plugin's but `kernel/` and `platform/json.ts`.
|
|
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
4
|
*/
|
|
5
5
|
import { type Status } from "../kernel/protocol.js";
|
|
6
|
-
/** How many records a project keeps, the newest by `movedAt`. */
|
|
6
|
+
/** How many unpinned records a project keeps: those with a lesson first, then the newest by `movedAt`. */
|
|
7
7
|
export declare const MAX_RECORDS = 30;
|
|
8
|
-
/**
|
|
8
|
+
/** Unpinned records that last moved longer ago than this are dropped when the plugin loads. */
|
|
9
9
|
export declare const RETENTION_MS: number;
|
|
10
10
|
/** A record's `split` while its top has stated no split line. */
|
|
11
11
|
export declare const NOT_STATED = "not stated";
|
|
@@ -13,11 +13,13 @@ export declare const NOT_STATED = "not stated";
|
|
|
13
13
|
export declare const LESSON_MAX = 300;
|
|
14
14
|
/**
|
|
15
15
|
* How a part stands: its last report's status, how its wait ended without one (`failed`, `interrupted`), `stopped` by
|
|
16
|
-
* courier_stop, `stalled` on a message it will not take, or `out`: it owes a report
|
|
16
|
+
* courier_stop, `gone` before it settled, `stalled` on a message it will not take, or `out`: it owes a report.
|
|
17
17
|
*/
|
|
18
|
-
export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
|
|
18
|
+
export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"];
|
|
19
19
|
export type PartStatus = (typeof PART_STATUSES)[number];
|
|
20
20
|
export declare const isPartStatus: (value: unknown) => value is PartStatus;
|
|
21
|
+
/** Whether a part has settled, as every part of a finished tree has: it neither owes a report nor is stalled. */
|
|
22
|
+
export declare const isSettled: (status: PartStatus) => status is "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "gone";
|
|
21
23
|
/** One session started in a tree, as its record keeps it; `duration` is in milliseconds, `context` a share of its model's window. */
|
|
22
24
|
export interface PartRecord {
|
|
23
25
|
readonly sessionID: string;
|
|
@@ -49,6 +51,29 @@ export interface Lesson {
|
|
|
49
51
|
readonly at: number;
|
|
50
52
|
readonly depth?: number;
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* How often other trees' courier_recall calls listed the record (`shown`) or gave it in full (`read`), and when one last
|
|
56
|
+
* did (`at`). Kept per reader tree and summed when read; a listing to the tree's own sessions counts nothing.
|
|
57
|
+
*/
|
|
58
|
+
export interface Reads {
|
|
59
|
+
readonly shown: number;
|
|
60
|
+
readonly read: number;
|
|
61
|
+
readonly at: number;
|
|
62
|
+
}
|
|
63
|
+
/** What a courier_recall call did with a record: listed it, or gave it in full. */
|
|
64
|
+
export type ReadKind = "shown" | "read";
|
|
65
|
+
/** The person's pin, set in the memory view: when. A pinned record is listed first and never swept. */
|
|
66
|
+
export interface Pinned {
|
|
67
|
+
readonly at: number;
|
|
68
|
+
}
|
|
69
|
+
/** How many revisions of the split line a record keeps, the newest. */
|
|
70
|
+
export declare const MAX_REVISIONS = 3;
|
|
71
|
+
/** A later split line the top stated, worded otherwise than the one before it: when, and how many parts the tree had then. */
|
|
72
|
+
export interface Revision {
|
|
73
|
+
readonly text: string;
|
|
74
|
+
readonly at: number;
|
|
75
|
+
readonly parts: number;
|
|
76
|
+
}
|
|
52
77
|
/** What is remembered of one tree: its top session and each session started in it, never a report's body or artifacts. */
|
|
53
78
|
export interface TreeRecord {
|
|
54
79
|
/** When the top session was created, epoch milliseconds. */
|
|
@@ -56,13 +81,22 @@ export interface TreeRecord {
|
|
|
56
81
|
/** When a turn in the tree last ended, or the record was first written. */
|
|
57
82
|
readonly movedAt: number;
|
|
58
83
|
readonly title: string;
|
|
59
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The top's first split line, as `e2e/real-model-recursive.mjs` reads it, or `not stated`; a stated line takes precedence over a
|
|
86
|
+
* mention, which it replaces once, and is never replaced.
|
|
87
|
+
*/
|
|
60
88
|
readonly split: string;
|
|
89
|
+
/** Absent until the top states a split line worded otherwise than `split`; newest last, at most `MAX_REVISIONS`. */
|
|
90
|
+
readonly revisions?: ReadonlyArray<Revision>;
|
|
61
91
|
/** Whether, at the last refresh, no session in the tree ran and every part had settled: none was out or stalled. */
|
|
62
92
|
readonly finished: boolean;
|
|
63
93
|
readonly recalled?: Recalled;
|
|
64
94
|
/** Absent until a session of the tree that is not a leaf wrote one; a second note replaces the first. */
|
|
65
95
|
readonly lesson?: Lesson;
|
|
96
|
+
/** Absent until another tree's courier_recall listed or read the record; kept beside the record, never written with it. */
|
|
97
|
+
readonly reads?: Reads;
|
|
98
|
+
/** Absent until the person pinned the record in the memory view; kept beside the record, never written with it. */
|
|
99
|
+
readonly pinned?: Pinned;
|
|
66
100
|
readonly parts: ReadonlyArray<PartRecord>;
|
|
67
101
|
}
|
|
68
102
|
/** A record with where it is kept: its project and its top session. */
|
|
@@ -77,6 +111,29 @@ export declare function readMemory(options: unknown): {
|
|
|
77
111
|
};
|
|
78
112
|
/** 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. */
|
|
79
113
|
export declare function splitLineOf(text: string): string | undefined;
|
|
114
|
+
/** Whether a split line is stated, beginning with `split:` after any markdown, not a passing mention nor `not stated`. */
|
|
115
|
+
export declare const isStated: (line: string) => boolean;
|
|
116
|
+
/** The record's `split` once `text` is read: kept once stated; a mention gives way to the first stated line; `not stated` to either. */
|
|
117
|
+
export declare function splitOf(recorded: string, text: string): string;
|
|
118
|
+
/** Every line of a text that begins with `split:` after any markdown, trimmed: a passing mention of a split is none. */
|
|
119
|
+
export declare const splitLinesOf: (text: string) => string[];
|
|
120
|
+
/** A split line the top's replies state, with when its reply was created when the message says. */
|
|
121
|
+
export interface Stated {
|
|
122
|
+
readonly text: string;
|
|
123
|
+
readonly at?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The revisions once the split lines `stated` are read at a refresh at `at` with `parts` parts: those `statedSince` the line recorded
|
|
127
|
+
* last, each appended when its words differ from the line before, so a re-read appends nothing.
|
|
128
|
+
*/
|
|
129
|
+
export declare function revise(previous: Pick<TreeRecord, "split" | "revisions">, stated: ReadonlyArray<Stated>, at: number, parts: number): Revision[] | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* The split and revisions once the stated line `found` replaces a mention: when `found` is among the revisions (recorded beside the
|
|
132
|
+
* mention before #294), the first of them was stated first and is `split`, the rest stay; else `found` is, beside them all.
|
|
133
|
+
*/
|
|
134
|
+
export declare function replacingMention(found: string, revisions: ReadonlyArray<Revision> | undefined): Pick<TreeRecord, "split" | "revisions">;
|
|
135
|
+
/** 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. */
|
|
136
|
+
export declare function splitProse(split: string): string;
|
|
80
137
|
/** What a part's status is read from: whether courier_stop stopped it, whether it is stalled, and its report state. */
|
|
81
138
|
export interface Standing {
|
|
82
139
|
readonly stopped: boolean;
|
|
@@ -91,8 +148,13 @@ export interface Standing {
|
|
|
91
148
|
/** A part's status by `Standing`; one whose report state is not known is still out. */
|
|
92
149
|
export declare function partStatus({ stopped, stalled, report }: Standing): PartStatus;
|
|
93
150
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
151
|
+
* A part whose session could not be read, from what was read of it and its last record: its title, duration and cost as
|
|
152
|
+
* last recorded; once OpenCode no longer knows it (`gone`), settled as read, else as last recorded, else `gone`.
|
|
153
|
+
*/
|
|
154
|
+
export declare function unreadPart(read: PartRecord, last: PartRecord | undefined, gone: boolean): PartRecord;
|
|
155
|
+
/**
|
|
156
|
+
* The parts of a refreshed record: those recorded before, in their order, each as `current` has it (read now, or found
|
|
157
|
+
* gone), else as last recorded, then those first seen now, in the order the tree is drawn.
|
|
96
158
|
*/
|
|
97
159
|
export declare function mergeParts(previous: ReadonlyArray<PartRecord>, current: ReadonlyArray<PartRecord>): PartRecord[];
|
|
98
160
|
/** `recalled` after one more call at `at`, the top's first call noted when `byTop`. */
|
|
@@ -110,10 +172,17 @@ export declare function lessonText(text: string): {
|
|
|
110
172
|
text: string;
|
|
111
173
|
clipped: number;
|
|
112
174
|
};
|
|
113
|
-
/**
|
|
114
|
-
export declare function
|
|
175
|
+
/** One reader tree's `reads` after courier_recall listed the record or gave it in full at `at`. */
|
|
176
|
+
export declare function noteRead(reads: Reads | undefined, kind: ReadKind, at: number): Reads;
|
|
177
|
+
/** The reads of every reader tree as one: the counts summed, `at` the latest; none without a reader. */
|
|
178
|
+
export declare function sumReads(reads: ReadonlyArray<Reads>): Reads | undefined;
|
|
179
|
+
/**
|
|
180
|
+
* The records a project keeps at `now`: every pinned one, whatever its age, and of the rest the first `MAX_RECORDS` by
|
|
181
|
+
* whether each has a lesson, then by `movedAt`, none older than `RETENTION_MS`; the rest are dropped.
|
|
182
|
+
*/
|
|
183
|
+
export declare function keep<T extends Pick<StoredRecord, "movedAt" | "lesson" | "pinned">>(records: ReadonlyArray<T>, now: number): {
|
|
115
184
|
kept: T[];
|
|
116
185
|
dropped: T[];
|
|
117
186
|
};
|
|
118
|
-
/** Records
|
|
119
|
-
export declare const
|
|
187
|
+
/** Records as courier_recall and the memory view list them: the pinned first, then by when each last moved, newest first. */
|
|
188
|
+
export declare const pinnedFirst: <T extends Pick<StoredRecord, "movedAt" | "pinned">>(records: ReadonlyArray<T>) => T[];
|
package/dist/memory/domain.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The memory's pure rules, on plain data: the option, a record's shape, each part's status, the split line,
|
|
3
|
-
* retention and how a refresh keeps the parts the roster has forgotten. Imports nothing of the plugin's but `kernel/` and `platform/json.ts`.
|
|
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
4
|
*/
|
|
5
5
|
import { STATUSES } from "../kernel/protocol.js";
|
|
6
6
|
import { obj } from "../platform/json.js";
|
|
7
|
-
/** How many records a project keeps, the newest by `movedAt`. */
|
|
7
|
+
/** How many unpinned records a project keeps: those with a lesson first, then the newest by `movedAt`. */
|
|
8
8
|
export const MAX_RECORDS = 30;
|
|
9
|
-
/**
|
|
9
|
+
/** Unpinned records that last moved longer ago than this are dropped when the plugin loads. */
|
|
10
10
|
export const RETENTION_MS = 90 * 24 * 60 * 60_000;
|
|
11
11
|
/** A record's `split` while its top has stated no split line. */
|
|
12
12
|
export const NOT_STATED = "not stated";
|
|
@@ -14,10 +14,14 @@ export const NOT_STATED = "not stated";
|
|
|
14
14
|
export const LESSON_MAX = 300;
|
|
15
15
|
/**
|
|
16
16
|
* How a part stands: its last report's status, how its wait ended without one (`failed`, `interrupted`), `stopped` by
|
|
17
|
-
* courier_stop, `stalled` on a message it will not take, or `out`: it owes a report
|
|
17
|
+
* courier_stop, `gone` before it settled, `stalled` on a message it will not take, or `out`: it owes a report.
|
|
18
18
|
*/
|
|
19
|
-
export const PART_STATUSES = [...STATUSES, "interrupted", "stopped", "stalled", "out"];
|
|
19
|
+
export const PART_STATUSES = [...STATUSES, "interrupted", "stopped", "gone", "stalled", "out"];
|
|
20
20
|
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
|
+
export const isSettled = (status) => status !== "out" && status !== "stalled";
|
|
23
|
+
/** How many revisions of the split line a record keeps, the newest. */
|
|
24
|
+
export const MAX_REVISIONS = 3;
|
|
21
25
|
/** The `memory` option: whether records are kept and listed; on unless `false`. */
|
|
22
26
|
export function readMemory(options) {
|
|
23
27
|
const value = obj(options).memory;
|
|
@@ -34,6 +38,101 @@ export function splitLineOf(text) {
|
|
|
34
38
|
const lines = text.split("\n");
|
|
35
39
|
return (lines.find((line) => STARTS_SPLIT.test(line)) ?? lines.find((line) => HOLDS_SPLIT.test(line)))?.trim();
|
|
36
40
|
}
|
|
41
|
+
/** Whether a split line is stated, beginning with `split:` after any markdown, not a passing mention nor `not stated`. */
|
|
42
|
+
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
|
+
export function splitOf(recorded, text) {
|
|
45
|
+
if (isStated(recorded))
|
|
46
|
+
return recorded;
|
|
47
|
+
const found = splitLineOf(text);
|
|
48
|
+
return found !== undefined && (recorded === NOT_STATED || isStated(found)) ? found : recorded;
|
|
49
|
+
}
|
|
50
|
+
/** Every line of a text that begins with `split:` after any markdown, trimmed: a passing mention of a split is none. */
|
|
51
|
+
export const splitLinesOf = (text) => text
|
|
52
|
+
.split("\n")
|
|
53
|
+
.filter((line) => STARTS_SPLIT.test(line))
|
|
54
|
+
.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
|
+
function statedSince(previous, stated) {
|
|
60
|
+
const last = previous.revisions?.at(-1);
|
|
61
|
+
const words = (line) => splitProse(line.text);
|
|
62
|
+
if (!last)
|
|
63
|
+
return stated.slice(stated.findIndex((line) => words(line) === splitProse(previous.split)) + 1);
|
|
64
|
+
const inReply = stated.findLastIndex((line) => words(line) === splitProse(last.text) && line.at === last.at);
|
|
65
|
+
if (inReply >= 0)
|
|
66
|
+
return stated.slice(inReply + 1);
|
|
67
|
+
if (stated.some((line) => line.at !== undefined))
|
|
68
|
+
return stated.filter((line) => line.at !== undefined && line.at > last.at);
|
|
69
|
+
return stated.slice(stated.findLastIndex((line) => words(line) === splitProse(last.text)) + 1);
|
|
70
|
+
}
|
|
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
|
+
export function revise(previous, stated, at, parts) {
|
|
76
|
+
const recorded = splitProse(previous.revisions?.at(-1)?.text ?? previous.split);
|
|
77
|
+
const { added } = statedSince(previous, stated).reduce(({ last, added }, line) => {
|
|
78
|
+
const words = splitProse(line.text);
|
|
79
|
+
return words === last ? { last, added } : { last: words, added: [...added, { text: line.text, at: line.at ?? at, parts }] };
|
|
80
|
+
}, { last: recorded, added: [] });
|
|
81
|
+
const revisions = [...(previous.revisions ?? []), ...added].slice(-MAX_REVISIONS);
|
|
82
|
+
return revisions.length ? revisions : undefined;
|
|
83
|
+
}
|
|
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
|
+
export function replacingMention(found, revisions) {
|
|
89
|
+
const [first, ...rest] = revisions ?? [];
|
|
90
|
+
if (!first || !revisions.some((one) => splitProse(one.text) === splitProse(found)))
|
|
91
|
+
return { split: found, ...(first ? { revisions } : {}) };
|
|
92
|
+
return { split: first.text, ...(rest.length ? { revisions: rest } : {}) };
|
|
93
|
+
}
|
|
94
|
+
/** Heading, quote and list marks before a line's words, each with the space markdown puts after it, unlike the `#` of `#282`. */
|
|
95
|
+
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
|
+
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
|
+
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
|
+
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
|
+
const CODE_SPAN = /`[^`]*`/g;
|
|
104
|
+
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
|
+
const STARS = [
|
|
107
|
+
["***", /[^\s*/]/],
|
|
108
|
+
["**", /[^\s*/]/],
|
|
109
|
+
["*", /[\p{L}\p{N}\0"']/u],
|
|
110
|
+
];
|
|
111
|
+
const WRAPS = [
|
|
112
|
+
["__", /[^\s_]/],
|
|
113
|
+
["_", /[^\s_]/],
|
|
114
|
+
["`", /[^\s`]/],
|
|
115
|
+
];
|
|
116
|
+
const unwrapped = (line, marks) => {
|
|
117
|
+
const inner = (mark) => line.slice(mark.length, -mark.length);
|
|
118
|
+
const wraps = ([mark, opens]) => {
|
|
119
|
+
const words = inner(mark);
|
|
120
|
+
const apart = mark === "*" ? !words.includes("* ") && !words.includes(" *") : !words.includes(mark);
|
|
121
|
+
return line.length > 2 * mark.length && line.startsWith(mark) && line.endsWith(mark) && opens.test(words[0]) && !/[\s(/]/.test(words.at(-1)) && apart;
|
|
122
|
+
};
|
|
123
|
+
const found = marks.find(wraps);
|
|
124
|
+
return found ? inner(found[0]).trim() : line;
|
|
125
|
+
};
|
|
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
|
+
export function splitProse(split) {
|
|
128
|
+
const code = [];
|
|
129
|
+
const masked = oneLine(split.replaceAll("\0", ""))
|
|
130
|
+
.replace(BLOCK_MARKS, "")
|
|
131
|
+
.replace(LABEL, "$2")
|
|
132
|
+
.replace(CODE_SPAN, (span) => `\0${code.push(span) - 1}\0`);
|
|
133
|
+
const prose = unwrapped(masked, STARS).replace(BOLD, "$2").replace(ITALIC, "$1");
|
|
134
|
+
return unwrapped(prose.replace(MASKED, (_, i) => code[Number(i)]), WRAPS);
|
|
135
|
+
}
|
|
37
136
|
/** A part's status by `Standing`; one whose report state is not known is still out. */
|
|
38
137
|
export function partStatus({ stopped, stalled, report }) {
|
|
39
138
|
if (stopped)
|
|
@@ -47,8 +146,18 @@ export function partStatus({ stopped, stalled, report }) {
|
|
|
47
146
|
return report.status ?? "done";
|
|
48
147
|
}
|
|
49
148
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
149
|
+
* A part whose session could not be read, from what was read of it and its last record: its title, duration and cost as
|
|
150
|
+
* last recorded; once OpenCode no longer knows it (`gone`), settled as read, else as last recorded, else `gone`.
|
|
151
|
+
*/
|
|
152
|
+
export function unreadPart(read, last, gone) {
|
|
153
|
+
const settledLast = last && isSettled(last.status) ? last.status : "gone";
|
|
154
|
+
const status = !gone || isSettled(read.status) ? read.status : settledLast;
|
|
155
|
+
const { title, duration, cost } = last ?? read;
|
|
156
|
+
return { ...read, title, status, ...(duration !== undefined ? { duration } : {}), ...(cost !== undefined ? { cost } : {}) };
|
|
157
|
+
}
|
|
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.
|
|
52
161
|
*/
|
|
53
162
|
export function mergeParts(previous, current) {
|
|
54
163
|
const now = new Map(current.map((part) => [part.sessionID, part]));
|
|
@@ -74,11 +183,27 @@ export function lessonText(text) {
|
|
|
74
183
|
return { text: line };
|
|
75
184
|
return { text: line.slice(0, LESSON_MAX).trimEnd(), clipped: line.length };
|
|
76
185
|
}
|
|
77
|
-
/**
|
|
186
|
+
/** One reader tree's `reads` after courier_recall listed the record or gave it in full at `at`. */
|
|
187
|
+
export function noteRead(reads, kind, at) {
|
|
188
|
+
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
|
+
}
|
|
190
|
+
/** The reads of every reader tree as one: the counts summed, `at` the latest; none without a reader. */
|
|
191
|
+
export function sumReads(reads) {
|
|
192
|
+
if (!reads.length)
|
|
193
|
+
return undefined;
|
|
194
|
+
return reads.reduce((sum, one) => ({ shown: sum.shown + one.shown, read: sum.read + one.read, at: Math.max(sum.at, one.at) }));
|
|
195
|
+
}
|
|
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
|
+
*/
|
|
78
200
|
export function keep(records, now) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
201
|
+
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
|
+
const live = records.filter((record) => !record.pinned && now - record.movedAt <= RETENTION_MS);
|
|
204
|
+
const ordered = live.toSorted((a, b) => Number(Boolean(b.lesson)) - Number(Boolean(a.lesson)) || b.movedAt - a.movedAt);
|
|
205
|
+
const kept = [...pinned, ...ordered.slice(0, MAX_RECORDS)];
|
|
206
|
+
return { kept, dropped: records.filter((record) => !kept.includes(record)) };
|
|
82
207
|
}
|
|
83
|
-
/** Records
|
|
84
|
-
export const
|
|
208
|
+
/** Records as courier_recall and the memory view list them: the pinned first, then by when each last moved, newest first. */
|
|
209
|
+
export const pinnedFirst = (records) => records.toSorted((a, b) => Number(Boolean(b.pinned)) - Number(Boolean(a.pinned)) || b.movedAt - a.movedAt);
|
package/dist/memory/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { LESSON_MAX, MAX_RECORDS, NOT_STATED, readMemory, RETENTION_MS } from "./domain.js";
|
|
2
2
|
export { memoryEvents, type MemoryEventPorts } from "./events.js";
|
|
3
3
|
export { note, type Noted, type NoteInput } from "./note.js";
|
|
4
|
-
export
|
|
4
|
+
export { pin, type PinOutcome } from "./pin.js";
|
|
5
|
+
export type { Lesson, MemoryPorts, MemoryState, PartRecord, PartStatus, Pinned, Reads, Recalled, RefreshPorts, Revision, StoredRecord, TreeRecord } from "./ports.js";
|
|
5
6
|
export { recall, type Recall, type RecallInput } from "./recall.js";
|
|
6
7
|
export { refresh } from "./record.js";
|
|
7
|
-
export { registerMemoryRpc } from "./rpc-handlers.js";
|
|
8
|
+
export { registerMemoryRpc, type MemoryRpcPorts } from "./rpc-handlers.js";
|
|
8
9
|
export { sweepRecords } from "./sweep.js";
|
|
9
10
|
export { NOTE_TOOL, RECALL_TOOL } from "./text.js";
|
|
10
11
|
export { addMemoryTools, type MemoryToolPorts } from "./tools.js";
|
package/dist/memory/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
export { LESSON_MAX, MAX_RECORDS, NOT_STATED, readMemory, RETENTION_MS } from "./domain.js";
|
|
4
4
|
export { memoryEvents } from "./events.js";
|
|
5
5
|
export { note } from "./note.js";
|
|
6
|
+
export { pin } from "./pin.js";
|
|
6
7
|
export { recall } from "./recall.js";
|
|
7
8
|
export { refresh } from "./record.js";
|
|
8
9
|
export { registerMemoryRpc } from "./rpc-handlers.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MemoryPorts } from "./ports.js";
|
|
2
|
+
/** What pinning did: the record is pinned, unpinned, or there is no record to pin. */
|
|
3
|
+
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
|
+
export declare function pin(ports: Pick<MemoryPorts, "storage" | "projectID" | "now">, sessionID: string, pinned: boolean): Promise<PinOutcome>;
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
export async function pin(ports, sessionID, pinned) {
|
|
7
|
+
const store = memoryStore(ports.storage);
|
|
8
|
+
const record = await store.get(ports.projectID, sessionID);
|
|
9
|
+
if (!record)
|
|
10
|
+
return "none";
|
|
11
|
+
await store.pin(ports.projectID, sessionID, pinned ? (record.pinned ?? { at: ports.now() }) : undefined);
|
|
12
|
+
return pinned ? "pinned" : "unpinned";
|
|
13
|
+
}
|
package/dist/memory/ports.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import type { Plugin } from "@opencode/plugin";
|
|
3
3
|
import type { Storage } from "../platform/storage.js";
|
|
4
4
|
import type { ModelWindow } from "../tree/index.js";
|
|
5
|
-
import type { StoredRecord, TreeRecord } from "./domain.js";
|
|
6
|
-
export type { Lesson, PartRecord, PartStatus, Recalled, StoredRecord, TreeRecord } from "./domain.js";
|
|
5
|
+
import type { Pinned, ReadKind, Reads, StoredRecord, TreeRecord } from "./domain.js";
|
|
6
|
+
export type { Lesson, PartRecord, PartStatus, Pinned, ReadKind, Reads, Recalled, Revision, StoredRecord, TreeRecord } from "./domain.js";
|
|
7
7
|
type Context = Plugin.Context;
|
|
8
8
|
/** What refreshing a record runs on: every hub-v2 member's watch ports have these, so the event module runs through any. */
|
|
9
9
|
export interface RefreshPorts {
|
|
@@ -32,13 +32,24 @@ export interface MemoryState {
|
|
|
32
32
|
/** The turn ends recorded, so the hub's two subscriptions refresh once. */
|
|
33
33
|
readonly seen: Set<string>;
|
|
34
34
|
}
|
|
35
|
-
/** The records, by project and top session. */
|
|
35
|
+
/** The records, by project and top session, each read with its reads and pin, which lie beside it and are written apart. */
|
|
36
36
|
export interface MemoryStore {
|
|
37
37
|
get(projectID: string, sessionID: string): Promise<TreeRecord | undefined>;
|
|
38
|
+
/** Writes the record's facts; its `reads` and `pinned` are left out, as nothing but `noteRead` and `pin` write them. */
|
|
38
39
|
put(projectID: string, sessionID: string, record: TreeRecord): Promise<void>;
|
|
40
|
+
/** Removes the record with its reads and pin. */
|
|
39
41
|
remove(projectID: string, sessionID: string): Promise<void>;
|
|
42
|
+
/** Counts a listing or a full read of the record by the tree under `by` at `at`; gives that tree's reads of it. */
|
|
43
|
+
noteRead(projectID: string, sessionID: string, by: string, kind: ReadKind, at: number): Promise<Reads>;
|
|
44
|
+
/** Pins the record, or with `undefined` unpins it. */
|
|
45
|
+
pin(projectID: string, sessionID: string, pinned: Pinned | undefined): Promise<void>;
|
|
40
46
|
/** A project's records, in key order. */
|
|
41
47
|
ofProject(projectID: string): Promise<StoredRecord[]>;
|
|
42
48
|
/** Every project's records. */
|
|
43
49
|
all(): Promise<StoredRecord[]>;
|
|
50
|
+
/** The trees with reads or a pin but no record, left by a record removed while it was read or pinned. */
|
|
51
|
+
strays(): Promise<Array<{
|
|
52
|
+
projectID: string;
|
|
53
|
+
sessionID: string;
|
|
54
|
+
}>>;
|
|
44
55
|
}
|
package/dist/memory/recall.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type Recall = {
|
|
|
21
21
|
readonly sessionID: string;
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* courier_recall: notes the call on the caller's tree, then lists the project's records
|
|
25
|
-
* under `sessionID` in full
|
|
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
26
|
*/
|
|
27
27
|
export declare function recall(ports: MemoryPorts, state: Pick<MemoryState, "tops">, callerID: string, input: RecallInput): Promise<Recall>;
|
package/dist/memory/recall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { addBounded, SEEN_MAX } from "../platform/bounded.js";
|
|
2
2
|
import { finite, millis } from "../platform/json.js";
|
|
3
3
|
import { lineage } from "../tree/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { NOT_STATED, noteRead, noteRecall, pinnedFirst } from "./domain.js";
|
|
5
5
|
import { memoryStore } from "./store.js";
|
|
6
6
|
/** The top of the caller's tree: the top of its lineage, or itself. */
|
|
7
7
|
async function topOfCaller(ports, callerID) {
|
|
@@ -27,8 +27,20 @@ async function note(ports, top, byTop) {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
+
function count(ports, top, kind, sessionIDs) {
|
|
34
|
+
const store = memoryStore(ports.storage);
|
|
35
|
+
const now = ports.now();
|
|
36
|
+
return Promise.all(sessionIDs.map((sessionID) => store.noteRead(ports.projectID, sessionID, top, kind, now).catch((error) => {
|
|
37
|
+
ports.log(`courier memory: could not count the ${kind === "shown" ? "listing" : "read"} of the record under ${sessionID}: ${String(error)}`);
|
|
38
|
+
return undefined;
|
|
39
|
+
})));
|
|
40
|
+
}
|
|
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.
|
|
32
44
|
*/
|
|
33
45
|
export async function recall(ports, state, callerID, input) {
|
|
34
46
|
if (!ports.memory)
|
|
@@ -42,7 +54,14 @@ export async function recall(ports, state, callerID, input) {
|
|
|
42
54
|
const wanted = typeof input.sessionID === "string" ? input.sessionID.trim() : "";
|
|
43
55
|
if (wanted) {
|
|
44
56
|
const record = await store.get(projectID, wanted);
|
|
45
|
-
|
|
57
|
+
if (!record)
|
|
58
|
+
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
|
+
const [counted] = wanted === top ? [undefined] : await count(ports, top, "read", [wanted]);
|
|
61
|
+
const reads = counted ? noteRead(record.reads, "read", counted.at) : record.reads;
|
|
62
|
+
return { kind: "record", projectID, record: { ...record, ...(reads ? { reads } : {}), projectID, sessionID: wanted } };
|
|
46
63
|
}
|
|
47
|
-
|
|
64
|
+
const records = pinnedFirst(await store.ofProject(projectID));
|
|
65
|
+
await count(ports, top, "shown", records.flatMap((record) => (record.sessionID === top ? [] : [record.sessionID])));
|
|
66
|
+
return { kind: "list", projectID, top, records };
|
|
48
67
|
}
|
package/dist/memory/record.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { type TreeRecord } from "./domain.js";
|
|
|
2
2
|
import type { MemoryState, RefreshPorts } from "./ports.js";
|
|
3
3
|
/**
|
|
4
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. Nothing for a top unknown to OpenCode or
|
|
5
|
+
* keeps its last recorded state, unless found gone before it settled. Nothing for a top unknown to OpenCode or with memory off.
|
|
6
6
|
*/
|
|
7
7
|
export declare function refresh(ports: RefreshPorts, state: Pick<MemoryState, "windows" | "off">, top: string, at: number, ended?: string): Promise<TreeRecord | undefined>;
|