opencode-courier 0.8.0 → 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.
@@ -1,11 +1,13 @@
1
1
  import { finite, isNotFound, millis } from "../platform/json.js";
2
2
  import { busy, contextOf, reportState, stalledAmong, subtree } from "../tree/index.js";
3
- import { mergeParts, NOT_STATED, partStatus, splitLineOf } from "./domain.js";
3
+ import { isSettled, isStated, mergeParts, NOT_STATED, partStatus, replacingMention, revise, splitLinesOf, splitOf, unreadPart } from "./domain.js";
4
4
  import { memoryStore } from "./store.js";
5
- /** The text of a session's replies, in order, for its split line. */
6
- const repliesOf = (messages) => messages
7
- .flatMap((message) => (message.type === "assistant" ? message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])) : []))
8
- .join("\n");
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 })));
9
11
  /** How many courier_spawn calls of a session failed, refused by a limit mostly. */
10
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);
11
13
  /** A part's context use and refused spawns, as its messages show them. */
@@ -19,14 +21,16 @@ const keptUse = (last) => ({
19
21
  ...(last?.context !== undefined ? { context: last.context } : {}),
20
22
  ...(last?.refused ? { refused: last.refused } : {}),
21
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) }));
22
26
  /**
23
27
  * One session of the tree as its record keeps it, without its use when its messages were not read (only when its turn
24
- * `ended` or it is `known` to no record yet), and whether it runs now.
28
+ * `ended` or it is `known` to no record yet), without a duration when its info was not, and whether it runs now.
25
29
  */
26
30
  async function part(ports, windows, { entry, depth }, stalled, at, known, ended) {
27
31
  const { sessionID } = entry;
28
- const [info, use, report] = await Promise.all([
29
- ports.session.get({ sessionID }).catch(() => undefined),
32
+ const [{ info, gone }, use, report] = await Promise.all([
33
+ infoOf(ports, sessionID),
30
34
  ended || !known ? ports.session.context({ sessionID }).then((messages) => useOf(messages, windows), () => undefined) : undefined,
31
35
  reportState(ports.storage, sessionID).catch(() => undefined),
32
36
  ]);
@@ -41,14 +45,20 @@ async function part(ports, windows, { entry, depth }, stalled, at, known, ended)
41
45
  isolated: entry.isolated,
42
46
  ...(entry.group ? { group: entry.group } : {}),
43
47
  status: partStatus({ stopped: report?.stopped ?? false, stalled, report }),
44
- duration: Math.max(0, end - created),
48
+ ...(info ? { duration: Math.max(0, end - created) } : {}),
45
49
  ...(cost !== undefined ? { cost } : {}),
46
50
  };
47
- return { record, use, running };
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] : [])));
48
58
  }
49
59
  /**
50
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
51
- * keeps its last recorded state. Nothing for a top unknown to OpenCode or in a location with memory off; returns the record.
61
+ * keeps its last recorded state, unless found gone before it settled. Nothing for a top unknown to OpenCode or with memory off.
52
62
  */
53
63
  export async function refresh(ports, state, top, at, ended) {
54
64
  let info;
@@ -64,25 +74,47 @@ export async function refresh(ports, state, top, at, ended) {
64
74
  return undefined;
65
75
  const store = memoryStore(ports.storage);
66
76
  const [previous, { nodes }] = await Promise.all([store.get(info.projectID, top), subtree(ports.storage, top)]);
67
- // The split line is in the top's own replies, so only its own turn end can change one stated already.
68
- const replies = ended === top || (previous?.split ?? NOT_STATED) === NOT_STATED ? ports.session.context({ sessionID: top }).then(repliesOf, () => "") : "";
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
+ : [];
69
83
  const stalled = await stalledAmong(ports, nodes.map((node) => node.entry.sessionID));
70
84
  const windows = [...state.windows.values()];
71
85
  const known = new Set(previous?.parts.map((one) => one.sessionID));
72
- const read = await Promise.all(nodes.map((node) => part(ports, windows, node, stalled.has(node.entry.sessionID), at, known.has(node.entry.sessionID), node.entry.sessionID === ended)));
73
- const split = splitLineOf(await replies);
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;
74
91
  // Read again just before the write: a courier_recall or courier_note call, or another turn end's refresh, wrote meanwhile.
75
92
  const latest = (await store.get(info.projectID, top).catch(() => previous)) ?? previous;
76
93
  const last = new Map(latest?.parts.map((one) => [one.sessionID, one]));
77
- const parts = mergeParts(latest?.parts ?? [], read.map(({ record, use }) => ({ ...record, ...(use ?? keptUse(last.get(record.sessionID))) })));
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]);
78
100
  const recalled = latest?.recalled ?? previous?.recalled;
79
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);
80
111
  const record = {
81
112
  startedAt: finite(millis(info.time.created)) ?? previous?.startedAt ?? at,
82
113
  movedAt: at,
83
114
  title: info.title ?? previous?.title ?? "",
84
- split: split ?? latest?.split ?? NOT_STATED,
85
- finished: !busy(info.time) && !read.some(({ running }) => running) && parts.every((one) => one.status !== "out" && one.status !== "stalled"),
115
+ split,
116
+ ...(revisions ? { revisions } : {}),
117
+ finished: !busy(info.time) && !read.some(({ running }) => running) && parts.every((one) => isSettled(one.status)),
86
118
  ...(recalled ? { recalled } : {}),
87
119
  ...(lesson ? { lesson } : {}),
88
120
  parts,
@@ -1,13 +1,15 @@
1
1
  import type { Plugin } from "@opencode/plugin";
2
2
  import type { MemoryPorts } from "./ports.js";
3
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">;
4
6
  /**
5
- * The records of the ports' project, newest first, as the memory view lists them; nothing, and `memory` false, with the
6
- * option off. Read from the store alone: unlike courier_recall, a look from the terminal is noted on no record.
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.
7
9
  */
8
- export declare function records(ports: Pick<MemoryPorts, "storage" | "projectID" | "memory">): Promise<Records>;
10
+ export declare function records(ports: Pick<MemoryRpcPorts, "storage" | "projectID" | "memory">): Promise<Records>;
9
11
  /** Serves the memory's RPC at the plugin's location, after the tree's and the person's; disposed with the plugin. */
10
- export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "register">, ports: Pick<MemoryPorts, "storage" | "projectID" | "memory">): Promise<import("@opencode/plugin/promise/rpc").RpcRegistration<{
12
+ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "register">, ports: MemoryRpcPorts): Promise<import("@opencode/plugin/promise/rpc").RpcRegistration<{
11
13
  readonly id: "courier-memory";
12
14
  readonly methods: {
13
15
  readonly records: {
@@ -19,12 +21,12 @@ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "regi
19
21
  readonly sessionID: string;
20
22
  readonly split: string;
21
23
  readonly title: string;
22
- readonly movedAt: number;
23
24
  readonly startedAt: number;
25
+ readonly movedAt: number;
24
26
  readonly finished: boolean;
25
27
  readonly parts: readonly {
26
28
  readonly sessionID: string;
27
- readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "out" | "stopped" | "stalled";
29
+ readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "out" | "stopped" | "gone" | "stalled";
28
30
  readonly title: string;
29
31
  readonly isolated: boolean;
30
32
  readonly depth: number;
@@ -43,6 +45,14 @@ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "regi
43
45
  readonly text: string;
44
46
  readonly depth?: number | undefined;
45
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;
46
56
  }[];
47
57
  }, {
48
58
  readonly memory: boolean;
@@ -51,12 +61,12 @@ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "regi
51
61
  readonly sessionID: string;
52
62
  readonly split: string;
53
63
  readonly title: string;
54
- readonly movedAt: number;
55
64
  readonly startedAt: number;
65
+ readonly movedAt: number;
56
66
  readonly finished: boolean;
57
67
  readonly parts: readonly {
58
68
  readonly sessionID: string;
59
- readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "out" | "stopped" | "stalled";
69
+ readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "out" | "stopped" | "gone" | "stalled";
60
70
  readonly title: string;
61
71
  readonly isolated: boolean;
62
72
  readonly depth: number;
@@ -75,6 +85,14 @@ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "regi
75
85
  readonly text: string;
76
86
  readonly depth?: number | undefined;
77
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;
78
96
  }[];
79
97
  }> & import("effect/Schema").Struct<{
80
98
  readonly memory: import("effect/Schema").Boolean;
@@ -95,13 +113,21 @@ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "regi
95
113
  readonly at: import("effect/Schema").Number;
96
114
  readonly depth: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
97
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
+ }>>;
98
124
  readonly parts: import("effect/Schema").$Array<import("effect/Schema").Struct<{
99
125
  readonly sessionID: import("effect/Schema").String;
100
126
  readonly title: import("effect/Schema").String;
101
127
  readonly depth: import("effect/Schema").Int;
102
128
  readonly isolated: import("effect/Schema").Boolean;
103
129
  readonly group: import("effect/Schema").optionalKey<import("effect/Schema").String>;
104
- readonly status: import("effect/Schema").Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
130
+ readonly status: import("effect/Schema").Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"]>;
105
131
  readonly duration: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
106
132
  readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
107
133
  readonly context: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
@@ -129,6 +155,25 @@ export declare function registerMemoryRpc(rpc: Pick<Plugin.Context["rpc"], "regi
129
155
  readonly outcome: import("effect/Schema").Literals<readonly ["record", "lesson", "none"]>;
130
156
  }>;
131
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
+ };
132
177
  };
133
178
  readonly events: {};
134
179
  }>>;
@@ -1,23 +1,25 @@
1
- import { newestFirst } from "./domain.js";
1
+ import { pinnedFirst } from "./domain.js";
2
2
  import { forget } from "./forget.js";
3
+ import { pin } from "./pin.js";
3
4
  import { memoryRpc } from "./rpc.js";
4
5
  import { memoryStore } from "./store.js";
5
- /** A stored record as the RPC gives it: without its project, which the listing names once. */
6
- const toRecord = ({ projectID: _project, ...record }) => record;
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;
7
8
  /**
8
- * The records of the ports' project, newest first, as the memory view lists them; nothing, and `memory` false, with the
9
- * option off. Read from the store alone: unlike courier_recall, a look from the terminal is noted on no record.
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.
10
11
  */
11
12
  export async function records(ports) {
12
13
  const { projectID } = ports;
13
14
  if (!ports.memory)
14
15
  return { memory: false, projectID, records: [] };
15
- return { memory: true, projectID, records: newestFirst(await memoryStore(ports.storage).ofProject(projectID)).map(toRecord) };
16
+ return { memory: true, projectID, records: pinnedFirst(await memoryStore(ports.storage).ofProject(projectID)).map(toRecord) };
16
17
  }
17
18
  /** Serves the memory's RPC at the plugin's location, after the tree's and the person's; disposed with the plugin. */
18
19
  export function registerMemoryRpc(rpc, ports) {
19
20
  return rpc.register(memoryRpc, {
20
21
  records: () => records(ports),
21
22
  forget: async ({ sessionID, lesson }) => ({ outcome: await forget(ports, sessionID, { lesson: lesson === true }) }),
23
+ pin: async ({ sessionID, pinned }) => ({ outcome: await pin(ports, sessionID, pinned) }),
22
24
  });
23
25
  }
@@ -1,6 +1,6 @@
1
1
  import { Schema } from "effect";
2
2
  /** A part's statuses, as `memory/domain.ts`'s `PART_STATUSES` has them; a unit test keeps the two equal. */
3
- export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
3
+ export declare const PART_STATUSES: readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"];
4
4
  /** One session started in a tree, as its record keeps it: durations in milliseconds, context use a share of the model's window. */
5
5
  declare const Part: Schema.Struct<{
6
6
  readonly sessionID: Schema.String;
@@ -9,7 +9,7 @@ declare const Part: Schema.Struct<{
9
9
  readonly depth: Schema.Int;
10
10
  readonly isolated: Schema.Boolean;
11
11
  readonly group: Schema.optionalKey<Schema.String>;
12
- readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
12
+ readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"]>;
13
13
  readonly duration: Schema.optionalKey<Schema.Number>;
14
14
  readonly cost: Schema.optionalKey<Schema.Number>;
15
15
  readonly context: Schema.optionalKey<Schema.Number>;
@@ -38,6 +38,16 @@ declare const Record: Schema.Struct<{
38
38
  readonly at: Schema.Number;
39
39
  readonly depth: Schema.optionalKey<Schema.Int>;
40
40
  }>>;
41
+ /** How often other trees' courier_recall calls listed the record and gave it in full, and when one last did. */
42
+ readonly reads: Schema.optionalKey<Schema.Struct<{
43
+ readonly shown: Schema.Int;
44
+ readonly read: Schema.Int;
45
+ readonly at: Schema.Number;
46
+ }>>;
47
+ /** The person's pin, when; a pinned record is listed first and never swept. */
48
+ readonly pinned: Schema.optionalKey<Schema.Struct<{
49
+ readonly at: Schema.Number;
50
+ }>>;
41
51
  readonly parts: Schema.$Array<Schema.Struct<{
42
52
  readonly sessionID: Schema.String;
43
53
  readonly title: Schema.String;
@@ -45,14 +55,14 @@ declare const Record: Schema.Struct<{
45
55
  readonly depth: Schema.Int;
46
56
  readonly isolated: Schema.Boolean;
47
57
  readonly group: Schema.optionalKey<Schema.String>;
48
- readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
58
+ readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"]>;
49
59
  readonly duration: Schema.optionalKey<Schema.Number>;
50
60
  readonly cost: Schema.optionalKey<Schema.Number>;
51
61
  readonly context: Schema.optionalKey<Schema.Number>;
52
62
  readonly refused: Schema.optionalKey<Schema.Int>;
53
63
  }>>;
54
64
  }>;
55
- /** The records of the project of the location asked, newest first; none, and `memory` false, when the option is off there. */
65
+ /** The records of the project of the location asked, pinned first and then newest first; none, and `memory` false, when the option is off there. */
56
66
  declare const Records: Schema.Struct<{
57
67
  readonly memory: Schema.Boolean;
58
68
  readonly projectID: Schema.String;
@@ -72,6 +82,16 @@ declare const Records: Schema.Struct<{
72
82
  readonly at: Schema.Number;
73
83
  readonly depth: Schema.optionalKey<Schema.Int>;
74
84
  }>>;
85
+ /** How often other trees' courier_recall calls listed the record and gave it in full, and when one last did. */
86
+ readonly reads: Schema.optionalKey<Schema.Struct<{
87
+ readonly shown: Schema.Int;
88
+ readonly read: Schema.Int;
89
+ readonly at: Schema.Number;
90
+ }>>;
91
+ /** The person's pin, when; a pinned record is listed first and never swept. */
92
+ readonly pinned: Schema.optionalKey<Schema.Struct<{
93
+ readonly at: Schema.Number;
94
+ }>>;
75
95
  readonly parts: Schema.$Array<Schema.Struct<{
76
96
  readonly sessionID: Schema.String;
77
97
  readonly title: Schema.String;
@@ -79,7 +99,7 @@ declare const Records: Schema.Struct<{
79
99
  readonly depth: Schema.Int;
80
100
  readonly isolated: Schema.Boolean;
81
101
  readonly group: Schema.optionalKey<Schema.String>;
82
- readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
102
+ readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"]>;
83
103
  readonly duration: Schema.optionalKey<Schema.Number>;
84
104
  readonly cost: Schema.optionalKey<Schema.Number>;
85
105
  readonly context: Schema.optionalKey<Schema.Number>;
@@ -91,11 +111,17 @@ declare const Records: Schema.Struct<{
91
111
  declare const Forgotten: Schema.Struct<{
92
112
  readonly outcome: Schema.Literals<readonly ["record", "lesson", "none"]>;
93
113
  }>;
114
+ /** What a pin did: pinned the record, unpinned it, or found no record to pin. */
115
+ declare const Pinned: Schema.Struct<{
116
+ readonly outcome: Schema.Literals<readonly ["pinned", "unpinned", "none"]>;
117
+ }>;
94
118
  export type MemoryRecord = typeof Record.Type;
95
119
  export type MemoryPart = typeof Part.Type;
96
120
  export type MemoryLesson = typeof Lesson.Type;
121
+ export type MemoryReads = NonNullable<MemoryRecord["reads"]>;
97
122
  export type Records = typeof Records.Type;
98
123
  export type Forgotten = typeof Forgotten.Type;
124
+ export type Pinned = typeof Pinned.Type;
99
125
  export declare const memoryRpc: {
100
126
  readonly id: "courier-memory";
101
127
  readonly methods: {
@@ -108,7 +134,7 @@ export declare const memoryRpc: {
108
134
  readonly sessionID: string;
109
135
  readonly title: string;
110
136
  readonly parts: readonly {
111
- readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "stalled" | "out";
137
+ readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "gone" | "stalled" | "out";
112
138
  readonly sessionID: string;
113
139
  readonly title: string;
114
140
  readonly depth: number;
@@ -132,6 +158,14 @@ export declare const memoryRpc: {
132
158
  readonly at: number;
133
159
  readonly depth?: number | undefined;
134
160
  } | undefined;
161
+ readonly reads?: {
162
+ readonly shown: number;
163
+ readonly read: number;
164
+ readonly at: number;
165
+ } | undefined;
166
+ readonly pinned?: {
167
+ readonly at: number;
168
+ } | undefined;
135
169
  }[];
136
170
  }, {
137
171
  readonly memory: boolean;
@@ -140,7 +174,7 @@ export declare const memoryRpc: {
140
174
  readonly sessionID: string;
141
175
  readonly title: string;
142
176
  readonly parts: readonly {
143
- readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "stalled" | "out";
177
+ readonly status: "done" | "partial" | "blocked" | "failed" | "interrupted" | "stopped" | "gone" | "stalled" | "out";
144
178
  readonly sessionID: string;
145
179
  readonly title: string;
146
180
  readonly depth: number;
@@ -164,6 +198,14 @@ export declare const memoryRpc: {
164
198
  readonly at: number;
165
199
  readonly depth?: number | undefined;
166
200
  } | undefined;
201
+ readonly reads?: {
202
+ readonly shown: number;
203
+ readonly read: number;
204
+ readonly at: number;
205
+ } | undefined;
206
+ readonly pinned?: {
207
+ readonly at: number;
208
+ } | undefined;
167
209
  }[];
168
210
  }> & Schema.Struct<{
169
211
  readonly memory: Schema.Boolean;
@@ -184,6 +226,16 @@ export declare const memoryRpc: {
184
226
  readonly at: Schema.Number;
185
227
  readonly depth: Schema.optionalKey<Schema.Int>;
186
228
  }>>;
229
+ /** How often other trees' courier_recall calls listed the record and gave it in full, and when one last did. */
230
+ readonly reads: Schema.optionalKey<Schema.Struct<{
231
+ readonly shown: Schema.Int;
232
+ readonly read: Schema.Int;
233
+ readonly at: Schema.Number;
234
+ }>>;
235
+ /** The person's pin, when; a pinned record is listed first and never swept. */
236
+ readonly pinned: Schema.optionalKey<Schema.Struct<{
237
+ readonly at: Schema.Number;
238
+ }>>;
187
239
  readonly parts: Schema.$Array<Schema.Struct<{
188
240
  readonly sessionID: Schema.String;
189
241
  readonly title: Schema.String;
@@ -191,7 +243,7 @@ export declare const memoryRpc: {
191
243
  readonly depth: Schema.Int;
192
244
  readonly isolated: Schema.Boolean;
193
245
  readonly group: Schema.optionalKey<Schema.String>;
194
- readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"]>;
246
+ readonly status: Schema.Literals<readonly ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"]>;
195
247
  readonly duration: Schema.optionalKey<Schema.Number>;
196
248
  readonly cost: Schema.optionalKey<Schema.Number>;
197
249
  readonly context: Schema.optionalKey<Schema.Number>;
@@ -220,6 +272,26 @@ export declare const memoryRpc: {
220
272
  readonly outcome: Schema.Literals<readonly ["record", "lesson", "none"]>;
221
273
  }>;
222
274
  };
275
+ /** Pins the record of the tree under `sessionID` in the project asked, or with `pinned` false unpins it. */
276
+ readonly pin: {
277
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
278
+ readonly sessionID: string;
279
+ readonly pinned: boolean;
280
+ }, {
281
+ readonly sessionID: string;
282
+ readonly pinned: boolean;
283
+ }> & Schema.Struct<{
284
+ readonly sessionID: Schema.String;
285
+ readonly pinned: Schema.Boolean;
286
+ }>;
287
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
288
+ readonly outcome: "pinned" | "none" | "unpinned";
289
+ }, {
290
+ readonly outcome: "pinned" | "none" | "unpinned";
291
+ }> & Schema.Struct<{
292
+ readonly outcome: Schema.Literals<readonly ["pinned", "unpinned", "none"]>;
293
+ }>;
294
+ };
223
295
  };
224
296
  readonly events: {};
225
297
  };
@@ -6,7 +6,7 @@ import { Rpc } from "@opencode/plugin/rpc";
6
6
  import { Schema } from "effect";
7
7
  const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
8
8
  /** A part's statuses, as `memory/domain.ts`'s `PART_STATUSES` has them; a unit test keeps the two equal. */
9
- export const PART_STATUSES = ["done", "partial", "blocked", "failed", "interrupted", "stopped", "stalled", "out"];
9
+ export const PART_STATUSES = ["done", "partial", "blocked", "failed", "interrupted", "stopped", "gone", "stalled", "out"];
10
10
  /** One session started in a tree, as its record keeps it: durations in milliseconds, context use a share of the model's window. */
11
11
  const Part = Schema.Struct({
12
12
  sessionID: Schema.String,
@@ -33,9 +33,13 @@ const Record = Schema.Struct({
33
33
  finished: Schema.Boolean,
34
34
  recalled: Schema.optionalKey(Schema.Struct({ at: Schema.optionalKey(Schema.Number), calls: Count })),
35
35
  lesson: Schema.optionalKey(Lesson),
36
+ /** How often other trees' courier_recall calls listed the record and gave it in full, and when one last did. */
37
+ reads: Schema.optionalKey(Schema.Struct({ shown: Count, read: Count, at: Schema.Number })),
38
+ /** The person's pin, when; a pinned record is listed first and never swept. */
39
+ pinned: Schema.optionalKey(Schema.Struct({ at: Schema.Number })),
36
40
  parts: Schema.Array(Part),
37
41
  });
38
- /** The records of the project of the location asked, newest first; none, and `memory` false, when the option is off there. */
42
+ /** The records of the project of the location asked, pinned first and then newest first; none, and `memory` false, when the option is off there. */
39
43
  const Records = Schema.Struct({
40
44
  memory: Schema.Boolean,
41
45
  projectID: Schema.String,
@@ -43,6 +47,8 @@ const Records = Schema.Struct({
43
47
  });
44
48
  /** What a forget did: removed the record, dropped only its lesson, or found nothing to forget. */
45
49
  const Forgotten = Schema.Struct({ outcome: Schema.Literals(["record", "lesson", "none"]) });
50
+ /** What a pin did: pinned the record, unpinned it, or found no record to pin. */
51
+ const Pinned = Schema.Struct({ outcome: Schema.Literals(["pinned", "unpinned", "none"]) });
46
52
  export const memoryRpc = Rpc.define({
47
53
  id: "courier-memory",
48
54
  methods: {
@@ -55,6 +61,11 @@ export const memoryRpc = Rpc.define({
55
61
  input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, lesson: Schema.optionalKey(Schema.Boolean) })),
56
62
  output: Schema.toStandardSchemaV1(Forgotten),
57
63
  },
64
+ /** Pins the record of the tree under `sessionID` in the project asked, or with `pinned` false unpins it. */
65
+ pin: {
66
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, pinned: Schema.Boolean })),
67
+ output: Schema.toStandardSchemaV1(Pinned),
68
+ },
58
69
  },
59
70
  events: {},
60
71
  });
@@ -4,7 +4,11 @@ import type { MemoryStore } from "./ports.js";
4
4
  export declare const recordKey: (projectID: string, sessionID: string) => string;
5
5
  /** The key prefix of a project's records. */
6
6
  export declare const projectPrefix: (projectID: string) => string;
7
- /** A record as stored, or undefined for a value that is not one. */
7
+ /** The person's pin on a record, `{ at }`; the key's presence pins. */
8
+ export declare const pinKey: (projectID: string, sessionID: string) => string;
9
+ /** The reads of a record by the tree under `by`, `{ shown, read, at }`, written only by that tree's sessions. */
10
+ export declare const readsKey: (projectID: string, sessionID: string, by: string) => string;
11
+ /** A record as stored, or undefined for a value that is not one; its reads and pin lie beside it (`beside`). */
8
12
  export declare function readRecord(value: unknown): TreeRecord | undefined;
9
13
  /** The `MemoryStore` over the plugin's storage. */
10
14
  export declare function memoryStore(storage: Storage): MemoryStore;