opencode-courier 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +119 -136
  2. package/dist/index.js +23 -4
  3. package/dist/kernel/protocol.d.ts +2 -0
  4. package/dist/kernel/protocol.js +9 -1
  5. package/dist/memory/domain.d.ts +188 -0
  6. package/dist/memory/domain.js +209 -0
  7. package/dist/memory/events.d.ts +26 -0
  8. package/dist/memory/events.js +46 -0
  9. package/dist/memory/forget.d.ts +10 -0
  10. package/dist/memory/forget.js +20 -0
  11. package/dist/memory/index.d.ts +11 -0
  12. package/dist/memory/index.js +12 -0
  13. package/dist/memory/note.d.ts +37 -0
  14. package/dist/memory/note.js +29 -0
  15. package/dist/memory/pin.d.ts +8 -0
  16. package/dist/memory/pin.js +13 -0
  17. package/dist/memory/ports.d.ts +55 -0
  18. package/dist/memory/ports.js +1 -0
  19. package/dist/memory/recall.d.ts +27 -0
  20. package/dist/memory/recall.js +67 -0
  21. package/dist/memory/record.d.ts +7 -0
  22. package/dist/memory/record.js +124 -0
  23. package/dist/memory/rpc-handlers.d.ts +179 -0
  24. package/dist/memory/rpc-handlers.js +25 -0
  25. package/dist/memory/rpc.d.ts +299 -0
  26. package/dist/memory/rpc.js +71 -0
  27. package/dist/memory/store.d.ts +14 -0
  28. package/dist/memory/store.js +174 -0
  29. package/dist/memory/sweep.d.ts +7 -0
  30. package/dist/memory/sweep.js +16 -0
  31. package/dist/memory/text.d.ts +35 -0
  32. package/dist/memory/text.js +116 -0
  33. package/dist/memory/tools.d.ts +16 -0
  34. package/dist/memory/tools.js +78 -0
  35. package/dist/memory/tui.d.ts +82 -0
  36. package/dist/person/rpc.d.ts +4 -4
  37. package/dist/platform/json.d.ts +1 -0
  38. package/dist/platform/json.js +1 -0
  39. package/dist/rpc.d.ts +1 -0
  40. package/dist/rpc.js +1 -0
  41. package/dist/skill.js +10 -3
  42. package/dist/tree/context.d.ts +2 -2
  43. package/dist/tree/context.js +4 -3
  44. package/dist/tree/index.d.ts +6 -5
  45. package/dist/tree/index.js +5 -4
  46. package/dist/tree/mascot.js +1 -1
  47. package/dist/tree/ports.d.ts +4 -2
  48. package/dist/tree/roster.d.ts +17 -0
  49. package/dist/tree/roster.js +24 -0
  50. package/dist/tree/rpc-handlers.d.ts +20 -20
  51. package/dist/tree/rpc.d.ts +20 -20
  52. package/dist/tree/status.d.ts +5 -5
  53. package/dist/tree/subtree.d.ts +2 -2
  54. package/dist/tree/text.js +2 -1
  55. package/dist/tree/tui.d.ts +11 -3
  56. package/dist/tui.js +612 -123
  57. package/package.json +1 -1
@@ -0,0 +1,55 @@
1
+ /** The memory's ports: what refreshing a record and courier_recall need of the plugin context, and what the process shares. */
2
+ import type { Plugin } from "@opencode/plugin";
3
+ import type { Storage } from "../platform/storage.js";
4
+ import type { ModelWindow } from "../tree/index.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
+ 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
+ export interface RefreshPorts {
10
+ readonly storage: Storage;
11
+ readonly session: Pick<Context["session"], "get" | "context">;
12
+ readonly now: () => number;
13
+ readonly log: (message: string) => void;
14
+ }
15
+ /** What courier_recall and courier_note run on: one instance's ports. */
16
+ export interface MemoryPorts extends RefreshPorts {
17
+ /** The project of the plugin's location, whose records the tools read and write. */
18
+ readonly projectID: string;
19
+ /** The `memory` option: off, and the tools write and list nothing. */
20
+ readonly memory: boolean;
21
+ /** The tree's `maxDepth` limit: a session at it is a leaf, which courier_note refuses. */
22
+ readonly maxDepth: number;
23
+ }
24
+ /** What every plugin instance in the process shares of the memory, under a fixed key outside the hub. */
25
+ export interface MemoryState {
26
+ /** The context windows of the models known, by `<providerID>/<modelID>`, filled as each instance loads. */
27
+ readonly windows: Map<string, ModelWindow>;
28
+ /** The directories of the locations whose instance has memory off: no record is written for a tree whose top runs there. */
29
+ readonly off: Set<string>;
30
+ /** The top sessions that called courier_recall, whose turn ends refresh their record before they have started a session. */
31
+ readonly tops: Set<string>;
32
+ /** The turn ends recorded, so the hub's two subscriptions refresh once. */
33
+ readonly seen: Set<string>;
34
+ }
35
+ /** The records, by project and top session, each read with its reads and pin, which lie beside it and are written apart. */
36
+ export interface MemoryStore {
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. */
39
+ put(projectID: string, sessionID: string, record: TreeRecord): Promise<void>;
40
+ /** Removes the record with its reads and pin. */
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>;
46
+ /** A project's records, in key order. */
47
+ ofProject(projectID: string): Promise<StoredRecord[]>;
48
+ /** Every project's records. */
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
+ }>>;
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { type StoredRecord } from "./domain.js";
2
+ import type { MemoryPorts, MemoryState } from "./ports.js";
3
+ export interface RecallInput {
4
+ readonly sessionID?: string;
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
+ export type Recall = {
8
+ readonly kind: "off";
9
+ } | {
10
+ readonly kind: "list";
11
+ readonly projectID: string;
12
+ readonly top: string;
13
+ readonly records: ReadonlyArray<StoredRecord>;
14
+ } | {
15
+ readonly kind: "record";
16
+ readonly projectID: string;
17
+ readonly record: StoredRecord;
18
+ } | {
19
+ readonly kind: "missing";
20
+ readonly projectID: string;
21
+ readonly sessionID: string;
22
+ };
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
+ export declare function recall(ports: MemoryPorts, state: Pick<MemoryState, "tops">, callerID: string, input: RecallInput): Promise<Recall>;
@@ -0,0 +1,67 @@
1
+ import { addBounded, SEEN_MAX } from "../platform/bounded.js";
2
+ import { finite, millis } from "../platform/json.js";
3
+ import { lineage } from "../tree/index.js";
4
+ import { NOT_STATED, noteRead, noteRecall, pinnedFirst } from "./domain.js";
5
+ import { memoryStore } from "./store.js";
6
+ /** The top of the caller's tree: the top of its lineage, or itself. */
7
+ async function topOfCaller(ports, callerID) {
8
+ return (await lineage(ports.storage, callerID)).at(-1)?.parentID ?? callerID;
9
+ }
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
+ async function note(ports, top, byTop) {
12
+ const store = memoryStore(ports.storage);
13
+ const now = ports.now();
14
+ const previous = await store.get(ports.projectID, top);
15
+ const recalled = noteRecall(previous?.recalled, byTop, now);
16
+ if (previous)
17
+ return store.put(ports.projectID, top, { ...previous, recalled });
18
+ const info = await ports.session.get({ sessionID: top }).catch(() => undefined);
19
+ await store.put(ports.projectID, top, {
20
+ startedAt: finite(millis(info?.time.created)) ?? now,
21
+ movedAt: now,
22
+ title: info?.title ?? "",
23
+ split: NOT_STATED,
24
+ finished: false,
25
+ recalled,
26
+ parts: [],
27
+ });
28
+ }
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
+ 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.
44
+ */
45
+ export async function recall(ports, state, callerID, input) {
46
+ if (!ports.memory)
47
+ return { kind: "off" };
48
+ const top = await topOfCaller(ports, callerID);
49
+ await note(ports, top, top === callerID);
50
+ addBounded(state.tops, top, SEEN_MAX);
51
+ const store = memoryStore(ports.storage);
52
+ const { projectID } = ports;
53
+ // Some models send null, or an empty string, for an optional field they leave out.
54
+ const wanted = typeof input.sessionID === "string" ? input.sessionID.trim() : "";
55
+ if (wanted) {
56
+ const record = await store.get(projectID, wanted);
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 } };
63
+ }
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 };
67
+ }
@@ -0,0 +1,7 @@
1
+ import { type TreeRecord } from "./domain.js";
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
+ export declare function refresh(ports: RefreshPorts, state: Pick<MemoryState, "windows" | "off">, top: string, at: number, ended?: string): Promise<TreeRecord | undefined>;
@@ -0,0 +1,124 @@
1
+ import { finite, isNotFound, millis } from "../platform/json.js";
2
+ import { busy, contextOf, reportState, stalledAmong, subtree } from "../tree/index.js";
3
+ import { isSettled, isStated, mergeParts, NOT_STATED, partStatus, replacingMention, revise, splitLinesOf, splitOf, unreadPart } from "./domain.js";
4
+ import { memoryStore } from "./store.js";
5
+ /** A session's replies, in order, each with when it was created, for its split lines; only the `finished` ones when asked. */
6
+ const repliesOf = (messages, finished) => messages.flatMap((message) => message.type === "assistant" && (!finished || message.time?.completed !== undefined)
7
+ ? [{ text: message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"), at: finite(millis(message.time?.created)) }]
8
+ : []);
9
+ /** The lines in the replies that begin with `split:`, each with its reply's time. */
10
+ const statedIn = (replies) => replies.flatMap(({ text, at }) => splitLinesOf(text).map((line) => (at === undefined ? { text: line } : { text: line, at })));
11
+ /** How many courier_spawn calls of a session failed, refused by a limit mostly. */
12
+ const refusedIn = (messages) => messages.reduce((n, message) => (message.type === "assistant" ? n + message.content.filter((part) => part.type === "tool" && part.name === "courier_spawn" && part.state.status === "error").length : n), 0);
13
+ /** A part's context use and refused spawns, as its messages show them. */
14
+ function useOf(messages, windows) {
15
+ const context = contextOf(messages, windows);
16
+ const refused = refusedIn(messages);
17
+ return { ...(context !== undefined ? { context: Math.round(context * 100) / 100 } : {}), ...(refused ? { refused } : {}) };
18
+ }
19
+ /** A part's use as last recorded. */
20
+ const keptUse = (last) => ({
21
+ ...(last?.context !== undefined ? { context: last.context } : {}),
22
+ ...(last?.refused ? { refused: last.refused } : {}),
23
+ });
24
+ /** A session's info, or `gone` when OpenCode no longer knows it; neither when it could not be read for another reason. */
25
+ const infoOf = (ports, sessionID) => ports.session.get({ sessionID }).then((info) => ({ info, gone: false }), (error) => ({ gone: isNotFound(error) }));
26
+ /**
27
+ * One session of the tree as its record keeps it, without its use when its messages were not read (only when its turn
28
+ * `ended` or it is `known` to no record yet), without a duration when its info was not, and whether it runs now.
29
+ */
30
+ async function part(ports, windows, { entry, depth }, stalled, at, known, ended) {
31
+ const { sessionID } = entry;
32
+ const [{ info, gone }, use, report] = await Promise.all([
33
+ infoOf(ports, sessionID),
34
+ ended || !known ? ports.session.context({ sessionID }).then((messages) => useOf(messages, windows), () => undefined) : undefined,
35
+ reportState(ports.storage, sessionID).catch(() => undefined),
36
+ ]);
37
+ const running = info !== undefined && busy(info.time);
38
+ const created = finite(millis(info?.time.created)) ?? entry.createdAt;
39
+ const end = info && !running && info.time.idle !== undefined ? millis(info.time.idle) : at;
40
+ const cost = finite(info?.cost);
41
+ const record = {
42
+ sessionID,
43
+ title: info?.title ?? entry.title,
44
+ depth,
45
+ isolated: entry.isolated,
46
+ ...(entry.group ? { group: entry.group } : {}),
47
+ status: partStatus({ stopped: report?.stopped ?? false, stalled, report }),
48
+ ...(info ? { duration: Math.max(0, end - created) } : {}),
49
+ ...(cost !== undefined ? { cost } : {}),
50
+ };
51
+ return { record, use, running, unread: info === undefined, gone };
52
+ }
53
+ /** Of the parts last recorded, those the roster has forgotten before they settled whose session OpenCode no longer knows. */
54
+ async function goneSince(ports, parts, onRoster) {
55
+ const unsettled = parts.filter((one) => !onRoster.has(one.sessionID) && !isSettled(one.status));
56
+ const looked = await Promise.all(unsettled.map(async ({ sessionID }) => ({ sessionID, ...(await infoOf(ports, sessionID)) })));
57
+ return new Set(looked.flatMap((one) => (one.gone ? [one.sessionID] : [])));
58
+ }
59
+ /**
60
+ * Writes the record of the tree under `top` as it stands at `at`, after the turn of `ended`; a part the roster has forgotten
61
+ * keeps its last recorded state, unless found gone before it settled. Nothing for a top unknown to OpenCode or with memory off.
62
+ */
63
+ export async function refresh(ports, state, top, at, ended) {
64
+ let info;
65
+ try {
66
+ info = await ports.session.get({ sessionID: top });
67
+ }
68
+ catch (error) {
69
+ if (isNotFound(error))
70
+ return undefined;
71
+ throw error;
72
+ }
73
+ if (state.off.has(info.location.directory))
74
+ return undefined;
75
+ const store = memoryStore(ports.storage);
76
+ const [previous, { nodes }] = await Promise.all([store.get(info.projectID, top), subtree(ports.storage, top)]);
77
+ // The split lines are in the top's own replies, so only its own turn end can revise one stated already; another's reads
78
+ // them while none is stated, and only the replies finished, never a line the top is still writing.
79
+ const own = ended === top;
80
+ const replies = own || !isStated(previous?.split ?? NOT_STATED)
81
+ ? ports.session.context({ sessionID: top }).then((messages) => repliesOf(messages, !own), () => [])
82
+ : [];
83
+ const stalled = await stalledAmong(ports, nodes.map((node) => node.entry.sessionID));
84
+ const windows = [...state.windows.values()];
85
+ const known = new Set(previous?.parts.map((one) => one.sessionID));
86
+ const [read, lost] = await Promise.all([
87
+ Promise.all(nodes.map((node) => part(ports, windows, node, stalled.has(node.entry.sessionID), at, known.has(node.entry.sessionID), node.entry.sessionID === ended))),
88
+ goneSince(ports, previous?.parts ?? [], new Set(nodes.map((node) => node.entry.sessionID))),
89
+ ]);
90
+ const said = await replies;
91
+ // Read again just before the write: a courier_recall or courier_note call, or another turn end's refresh, wrote meanwhile.
92
+ const latest = (await store.get(info.projectID, top).catch(() => previous)) ?? previous;
93
+ const last = new Map(latest?.parts.map((one) => [one.sessionID, one]));
94
+ const current = read.map(({ record, use, unread, gone }) => {
95
+ const before = last.get(record.sessionID);
96
+ return { ...(unread ? unreadPart(record, before, gone) : record), ...(use ?? keptUse(before)) };
97
+ });
98
+ const forgotten = (latest?.parts ?? []).filter((one) => lost.has(one.sessionID)).map((one) => unreadPart(one, one, true));
99
+ const parts = mergeParts(latest?.parts ?? [], [...current, ...forgotten]);
100
+ const recalled = latest?.recalled ?? previous?.recalled;
101
+ const lesson = latest?.lesson ?? previous?.lesson;
102
+ // The first stated line stays even when compaction has dropped its reply; a later one is a revision.
103
+ const first = latest?.split ?? NOT_STATED;
104
+ const found = splitOf(first, said.map((reply) => reply.text).join("\n"));
105
+ const stated = statedIn(said);
106
+ const replaced = found !== first;
107
+ const { split, revisions: recorded } = replaced ? replacingMention(found, latest?.revisions) : { split: first, revisions: latest?.revisions };
108
+ // The line that replaced a mention is the first stated, so beside revisions already recorded it is not one of them too.
109
+ const since = replaced && recorded ? stated.slice(1) : stated;
110
+ const revisions = split === NOT_STATED ? undefined : revise({ split, revisions: recorded }, since, at, parts.length);
111
+ const record = {
112
+ startedAt: finite(millis(info.time.created)) ?? previous?.startedAt ?? at,
113
+ movedAt: at,
114
+ title: info.title ?? previous?.title ?? "",
115
+ split,
116
+ ...(revisions ? { revisions } : {}),
117
+ finished: !busy(info.time) && !read.some(({ running }) => running) && parts.every((one) => isSettled(one.status)),
118
+ ...(recalled ? { recalled } : {}),
119
+ ...(lesson ? { lesson } : {}),
120
+ parts,
121
+ };
122
+ await store.put(info.projectID, top, record);
123
+ return record;
124
+ }
@@ -0,0 +1,179 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import type { MemoryPorts } from "./ports.js";
3
+ import { type Records } from "./rpc.js";
4
+ /** What the memory's RPC runs on: one instance's ports. */
5
+ export type MemoryRpcPorts = Pick<MemoryPorts, "storage" | "projectID" | "memory" | "now">;
6
+ /**
7
+ * The records of the ports' project, pinned first and then newest first, as the memory view lists them; nothing, and
8
+ * `memory` false, with the option off. Read from the store alone: unlike courier_recall, a look from the terminal is noted on no record.
9
+ */
10
+ export declare function records(ports: Pick<MemoryRpcPorts, "storage" | "projectID" | "memory">): Promise<Records>;
11
+ /** Serves the memory's RPC at the plugin's location, after the tree's and the person's; disposed with the plugin. */
12
+ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "register">, ports: MemoryRpcPorts): Promise<import("@opencode/plugin/promise/rpc").RpcRegistration<{
13
+ readonly id: "courier-memory";
14
+ readonly methods: {
15
+ readonly records: {
16
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & import("effect/Schema").Struct<{}>;
17
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
18
+ readonly memory: boolean;
19
+ readonly projectID: string;
20
+ readonly records: readonly {
21
+ readonly sessionID: string;
22
+ readonly split: string;
23
+ readonly title: string;
24
+ readonly startedAt: number;
25
+ readonly movedAt: number;
26
+ readonly finished: boolean;
27
+ readonly parts: readonly {
28
+ readonly sessionID: string;
29
+ readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "out" | "stopped" | "gone" | "stalled";
30
+ readonly title: string;
31
+ readonly isolated: boolean;
32
+ readonly depth: number;
33
+ readonly context?: number | undefined;
34
+ readonly group?: string | undefined;
35
+ readonly cost?: number | undefined;
36
+ readonly refused?: number | undefined;
37
+ readonly duration?: number | undefined;
38
+ }[];
39
+ readonly recalled?: {
40
+ readonly calls: number;
41
+ readonly at?: number | undefined;
42
+ } | undefined;
43
+ readonly lesson?: {
44
+ readonly at: number;
45
+ readonly text: string;
46
+ readonly depth?: number | undefined;
47
+ } | undefined;
48
+ readonly reads?: {
49
+ readonly shown: number;
50
+ readonly read: number;
51
+ readonly at: number;
52
+ } | undefined;
53
+ readonly pinned?: {
54
+ readonly at: number;
55
+ } | undefined;
56
+ }[];
57
+ }, {
58
+ readonly memory: boolean;
59
+ readonly projectID: string;
60
+ readonly records: readonly {
61
+ readonly sessionID: string;
62
+ readonly split: string;
63
+ readonly title: string;
64
+ readonly startedAt: number;
65
+ readonly movedAt: number;
66
+ readonly finished: boolean;
67
+ readonly parts: readonly {
68
+ readonly sessionID: string;
69
+ readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "out" | "stopped" | "gone" | "stalled";
70
+ readonly title: string;
71
+ readonly isolated: boolean;
72
+ readonly depth: number;
73
+ readonly context?: number | undefined;
74
+ readonly group?: string | undefined;
75
+ readonly cost?: number | undefined;
76
+ readonly refused?: number | undefined;
77
+ readonly duration?: number | undefined;
78
+ }[];
79
+ readonly recalled?: {
80
+ readonly calls: number;
81
+ readonly at?: number | undefined;
82
+ } | undefined;
83
+ readonly lesson?: {
84
+ readonly at: number;
85
+ readonly text: string;
86
+ readonly depth?: number | undefined;
87
+ } | undefined;
88
+ readonly reads?: {
89
+ readonly shown: number;
90
+ readonly read: number;
91
+ readonly at: number;
92
+ } | undefined;
93
+ readonly pinned?: {
94
+ readonly at: number;
95
+ } | undefined;
96
+ }[];
97
+ }> & import("effect/Schema").Struct<{
98
+ readonly memory: import("effect/Schema").Boolean;
99
+ readonly projectID: import("effect/Schema").String;
100
+ readonly records: import("effect/Schema").$Array<import("effect/Schema").Struct<{
101
+ readonly sessionID: import("effect/Schema").String;
102
+ readonly startedAt: import("effect/Schema").Number;
103
+ readonly movedAt: import("effect/Schema").Number;
104
+ readonly title: import("effect/Schema").String;
105
+ readonly split: import("effect/Schema").String;
106
+ readonly finished: import("effect/Schema").Boolean;
107
+ readonly recalled: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
108
+ readonly at: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
109
+ readonly calls: import("effect/Schema").Int;
110
+ }>>;
111
+ readonly lesson: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
112
+ readonly text: import("effect/Schema").String;
113
+ readonly at: import("effect/Schema").Number;
114
+ readonly depth: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
115
+ }>>;
116
+ readonly reads: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
117
+ readonly shown: import("effect/Schema").Int;
118
+ readonly read: import("effect/Schema").Int;
119
+ readonly at: import("effect/Schema").Number;
120
+ }>>;
121
+ readonly pinned: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
122
+ readonly at: import("effect/Schema").Number;
123
+ }>>;
124
+ readonly parts: import("effect/Schema").$Array<import("effect/Schema").Struct<{
125
+ readonly sessionID: import("effect/Schema").String;
126
+ readonly title: import("effect/Schema").String;
127
+ readonly depth: import("effect/Schema").Int;
128
+ readonly isolated: import("effect/Schema").Boolean;
129
+ readonly group: import("effect/Schema").optionalKey<import("effect/Schema").String>;
130
+ readonly status: import("effect/Schema").Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"]>;
131
+ readonly duration: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
132
+ readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
133
+ readonly context: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
134
+ readonly refused: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
135
+ }>>;
136
+ }>>;
137
+ }>;
138
+ };
139
+ readonly forget: {
140
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
141
+ readonly sessionID: string;
142
+ readonly lesson?: boolean | undefined;
143
+ }, {
144
+ readonly sessionID: string;
145
+ readonly lesson?: boolean | undefined;
146
+ }> & import("effect/Schema").Struct<{
147
+ readonly sessionID: import("effect/Schema").String;
148
+ readonly lesson: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
149
+ }>;
150
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
151
+ readonly outcome: "record" | "lesson" | "none";
152
+ }, {
153
+ readonly outcome: "record" | "lesson" | "none";
154
+ }> & import("effect/Schema").Struct<{
155
+ readonly outcome: import("effect/Schema").Literals<readonly ["record", "lesson", "none"]>;
156
+ }>;
157
+ };
158
+ readonly pin: {
159
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
160
+ readonly sessionID: string;
161
+ readonly pinned: boolean;
162
+ }, {
163
+ readonly sessionID: string;
164
+ readonly pinned: boolean;
165
+ }> & import("effect/Schema").Struct<{
166
+ readonly sessionID: import("effect/Schema").String;
167
+ readonly pinned: import("effect/Schema").Boolean;
168
+ }>;
169
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
170
+ readonly outcome: "pinned" | "unpinned" | "none";
171
+ }, {
172
+ readonly outcome: "pinned" | "unpinned" | "none";
173
+ }> & import("effect/Schema").Struct<{
174
+ readonly outcome: import("effect/Schema").Literals<readonly ["pinned", "unpinned", "none"]>;
175
+ }>;
176
+ };
177
+ };
178
+ readonly events: {};
179
+ }>>;
@@ -0,0 +1,25 @@
1
+ import { pinnedFirst } from "./domain.js";
2
+ import { forget } from "./forget.js";
3
+ import { pin } from "./pin.js";
4
+ import { memoryRpc } from "./rpc.js";
5
+ import { memoryStore } from "./store.js";
6
+ /** A stored record as the RPC gives it: without its project, which the listing names once, nor the split line's revisions, which its schema lacks. */
7
+ const toRecord = ({ projectID: _project, revisions: _revisions, ...record }) => record;
8
+ /**
9
+ * The records of the ports' project, pinned first and then newest first, as the memory view lists them; nothing, and
10
+ * `memory` false, with the option off. Read from the store alone: unlike courier_recall, a look from the terminal is noted on no record.
11
+ */
12
+ export async function records(ports) {
13
+ const { projectID } = ports;
14
+ if (!ports.memory)
15
+ return { memory: false, projectID, records: [] };
16
+ return { memory: true, projectID, records: pinnedFirst(await memoryStore(ports.storage).ofProject(projectID)).map(toRecord) };
17
+ }
18
+ /** Serves the memory's RPC at the plugin's location, after the tree's and the person's; disposed with the plugin. */
19
+ export function registerMemoryRpc(rpc, ports) {
20
+ return rpc.register(memoryRpc, {
21
+ records: () => records(ports),
22
+ forget: async ({ sessionID, lesson }) => ({ outcome: await forget(ports, sessionID, { lesson: lesson === true }) }),
23
+ pin: async ({ sessionID, pinned }) => ({ outcome: await pin(ports, sessionID, pinned) }),
24
+ });
25
+ }