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,12 +1,18 @@
1
1
  import { finite, num, obj, str } from "../platform/json.js";
2
2
  import { scanEntries } from "../platform/storage.js";
3
- import { isPartStatus, lessonText, NOT_STATED } from "./domain.js";
4
- // The memory's storage: the only file that knows its key and layout. A record is read leniently, as one written by
5
- // hand with `e2e/kv.ts set` is: what is missing takes a default, a part without a session id is left out.
3
+ import { isPartStatus, lessonText, MAX_REVISIONS, NOT_STATED, noteRead, sumReads, } from "./domain.js";
4
+ // The memory's storage: the only file that knows its keys and layout. A record is read leniently, as one written by hand
5
+ // with `e2e/kv.ts set` is; beside it, under its key and each with its own writer, lie the person's pin and each reader tree's reads.
6
6
  const PREFIX = "memory/";
7
7
  export const recordKey = (projectID, sessionID) => `${PREFIX}${projectID}/${sessionID}`;
8
8
  /** The key prefix of a project's records. */
9
9
  export const projectPrefix = (projectID) => `${PREFIX}${projectID}/`;
10
+ /** The key prefix of what lies beside a record: its pin and its reads. */
11
+ const besidePrefix = (projectID, sessionID) => `${recordKey(projectID, sessionID)}/`;
12
+ /** The person's pin on a record, `{ at }`; the key's presence pins. */
13
+ export const pinKey = (projectID, sessionID) => `${besidePrefix(projectID, sessionID)}pin`;
14
+ /** The reads of a record by the tree under `by`, `{ shown, read, at }`, written only by that tree's sessions. */
15
+ export const readsKey = (projectID, sessionID, by) => `${besidePrefix(projectID, sessionID)}reads/${by}`;
10
16
  function readPart(value) {
11
17
  const given = obj(value);
12
18
  const sessionID = str(given.sessionID);
@@ -47,7 +53,20 @@ function readLesson(value, movedAt) {
47
53
  const depth = num(given.depth);
48
54
  return { text, at: num(given.at) ?? movedAt, ...(depth !== undefined && Number.isInteger(depth) && depth > 0 ? { depth } : {}) };
49
55
  }
50
- /** A record as stored, or undefined for a value that is not one. */
56
+ /** The revisions as `{ text, at, parts }`, the newest `MAX_REVISIONS`, leaving out an entry without a text, a time or a count of parts. */
57
+ function readRevisions(value) {
58
+ if (!Array.isArray(value))
59
+ return [];
60
+ const revisions = value.flatMap((entry) => {
61
+ const given = obj(entry);
62
+ const text = str(given.text)?.trim();
63
+ const at = num(given.at);
64
+ const parts = num(given.parts);
65
+ return text && at !== undefined && parts !== undefined && parts >= 0 ? [{ text, at, parts }] : [];
66
+ });
67
+ return revisions.slice(-MAX_REVISIONS);
68
+ }
69
+ /** A record as stored, or undefined for a value that is not one; its reads and pin lie beside it (`beside`). */
51
70
  export function readRecord(value) {
52
71
  const given = obj(value);
53
72
  if (!Array.isArray(given.parts))
@@ -55,36 +74,101 @@ export function readRecord(value) {
55
74
  const recalled = readRecalled(given.recalled);
56
75
  const movedAt = num(given.movedAt) ?? num(given.startedAt) ?? 0;
57
76
  const lesson = readLesson(given.lesson, movedAt);
77
+ const revisions = readRevisions(given.revisions);
58
78
  return {
59
79
  startedAt: num(given.startedAt) ?? 0,
60
80
  movedAt,
61
81
  title: str(given.title) ?? "",
62
82
  split: str(given.split) ?? NOT_STATED,
83
+ ...(revisions.length ? { revisions } : {}),
63
84
  finished: given.finished === true,
64
85
  ...(recalled ? { recalled } : {}),
65
86
  ...(lesson ? { lesson } : {}),
66
87
  parts: given.parts.map(readPart).filter((part) => part !== undefined),
67
88
  };
68
89
  }
90
+ /** One tree's reads as `{ shown, read, at }`; a count missing is 0, an entry without a time is none. */
91
+ function readReads(value) {
92
+ const given = obj(value);
93
+ const at = num(given.at);
94
+ if (at === undefined)
95
+ return undefined;
96
+ const count = (field) => Math.max(0, Math.floor(num(given[field]) ?? 0));
97
+ return { shown: count("shown"), read: count("read"), at };
98
+ }
99
+ /** The pin as `{ at }`: any value under the key pins, dated 0 when it says no time. */
100
+ const readPinned = (value) => ({ at: num(obj(value).at) ?? 0 });
101
+ /** A record with what lies beside it: its readers' reads as one, and its pin. */
102
+ function beside(record, { pinned, reads }) {
103
+ const { parts, ...facts } = record;
104
+ const summed = sumReads(reads);
105
+ return { ...facts, ...(summed ? { reads: summed } : {}), ...(pinned ? { pinned } : {}), parts };
106
+ }
107
+ /** The key's path under `memory/`, split: `[projectID, sessionID]` a record, then `pin`, or `reads` and the reader. */
108
+ const pathOf = (prefix, key) => `${prefix.slice(PREFIX.length)}${key}`.split("/");
109
+ /** Sorts a scan's entries by record: the record's value, when the scan held it, and what lies beside it. */
110
+ function group(prefix, entries) {
111
+ const groups = new Map();
112
+ const of = (projectID, sessionID) => {
113
+ const id = `${projectID}/${sessionID}`;
114
+ const found = groups.get(id) ?? { beside: { reads: [] } };
115
+ groups.set(id, found);
116
+ return found;
117
+ };
118
+ for (const { key, value } of entries) {
119
+ const [projectID, sessionID, kind, by] = pathOf(prefix, key);
120
+ if (!projectID || !sessionID)
121
+ continue;
122
+ if (kind === undefined)
123
+ of(projectID, sessionID).record = value;
124
+ else if (kind === "pin" && by === undefined)
125
+ of(projectID, sessionID).beside.pinned = readPinned(value);
126
+ else if (kind === "reads" && by) {
127
+ const reads = readReads(value);
128
+ if (reads)
129
+ of(projectID, sessionID).beside.reads.push(reads);
130
+ }
131
+ }
132
+ return [...groups].map(([id, found]) => {
133
+ const [projectID, sessionID] = id.split("/");
134
+ return { projectID, sessionID, ...found };
135
+ });
136
+ }
69
137
  /** The `MemoryStore` over the plugin's storage. */
70
138
  export function memoryStore(storage) {
71
- const under = async (prefix) => {
72
- const entries = await scanEntries(storage, prefix);
73
- return entries.flatMap(({ key, value }) => {
74
- const parts = `${prefix.slice(PREFIX.length)}${key}`.split("/");
75
- const record = parts.length === 2 ? readRecord(value) : undefined;
76
- return record ? [{ ...record, projectID: parts[0], sessionID: parts[1] }] : [];
77
- });
78
- };
139
+ const scanned = async (prefix) => group(prefix, await scanEntries(storage, prefix));
140
+ const under = async (prefix) => (await scanned(prefix)).flatMap(({ projectID, sessionID, record: value, beside: others }) => {
141
+ const record = readRecord(value);
142
+ return record ? [{ ...beside(record, others), projectID, sessionID }] : [];
143
+ });
79
144
  return {
80
- get: async (projectID, sessionID) => readRecord(await storage.get(recordKey(projectID, sessionID))),
81
- async put(projectID, sessionID, record) {
145
+ async get(projectID, sessionID) {
146
+ const [value, others] = await Promise.all([storage.get(recordKey(projectID, sessionID)), scanned(besidePrefix(projectID, sessionID))]);
147
+ const record = readRecord(value);
148
+ return record && beside(record, others[0]?.beside ?? { reads: [] });
149
+ },
150
+ // What lies beside the record is never written with it, so no refresh or note can write over a pin or a count.
151
+ async put(projectID, sessionID, { reads: _reads, pinned: _pinned, ...record }) {
82
152
  await storage.set(recordKey(projectID, sessionID), { ...record, parts: record.parts.map((part) => ({ ...part })) });
83
153
  },
84
154
  async remove(projectID, sessionID) {
85
- await storage.remove(recordKey(projectID, sessionID));
155
+ const others = await scanEntries(storage, besidePrefix(projectID, sessionID));
156
+ await Promise.all([storage.remove(recordKey(projectID, sessionID)), ...others.map(({ key }) => storage.remove(`${besidePrefix(projectID, sessionID)}${key}`))]);
157
+ },
158
+ async noteRead(projectID, sessionID, by, kind, at) {
159
+ const key = readsKey(projectID, sessionID, by);
160
+ const reads = noteRead(readReads(await storage.get(key)), kind, at);
161
+ await storage.set(key, { ...reads });
162
+ return reads;
163
+ },
164
+ async pin(projectID, sessionID, pinned) {
165
+ if (pinned)
166
+ await storage.set(pinKey(projectID, sessionID), { ...pinned });
167
+ else
168
+ await storage.remove(pinKey(projectID, sessionID));
86
169
  },
87
170
  ofProject: (projectID) => under(projectPrefix(projectID)),
88
171
  all: () => under(PREFIX),
172
+ strays: async () => (await scanned(PREFIX)).filter(({ record }) => record === undefined).map(({ projectID, sessionID }) => ({ projectID, sessionID })),
89
173
  };
90
174
  }
@@ -1,4 +1,7 @@
1
1
  import type { Storage } from "../platform/storage.js";
2
2
  import { type StoredRecord } from "./domain.js";
3
- /** On loading: drops, per project, the records past `MAX_RECORDS` or `RETENTION_MS` at `now`. Returns those dropped. */
3
+ /**
4
+ * On loading: drops, per project, the records `keep` does not at `now`, the pinned never among them, and the reads or
5
+ * pin left beside a record removed meanwhile. Returns the records dropped.
6
+ */
4
7
  export declare function sweepRecords(storage: Storage, now: number): Promise<StoredRecord[]>;
@@ -1,12 +1,16 @@
1
1
  import { keep } from "./domain.js";
2
2
  import { memoryStore } from "./store.js";
3
- /** On loading: drops, per project, the records past `MAX_RECORDS` or `RETENTION_MS` at `now`. Returns those dropped. */
3
+ /**
4
+ * On loading: drops, per project, the records `keep` does not at `now`, the pinned never among them, and the reads or
5
+ * pin left beside a record removed meanwhile. Returns the records dropped.
6
+ */
4
7
  export async function sweepRecords(storage, now) {
5
8
  const store = memoryStore(storage);
6
9
  const byProject = new Map();
7
10
  for (const record of await store.all())
8
11
  byProject.set(record.projectID, [...(byProject.get(record.projectID) ?? []), record]);
9
12
  const dropped = [...byProject.values()].flatMap((records) => keep(records, now).dropped);
10
- await Promise.all(dropped.map((record) => store.remove(record.projectID, record.sessionID)));
13
+ const strays = await store.strays();
14
+ await Promise.all([...dropped, ...strays].map((record) => store.remove(record.projectID, record.sessionID)));
11
15
  return dropped;
12
16
  }
@@ -5,14 +5,22 @@ export declare const RECALL_TOOL = "courier_recall";
5
5
  export declare const NOTE_TOOL = "courier_note";
6
6
  /** A duration in milliseconds as a model reads it: seconds under half a minute, else whole minutes, else hours and minutes. */
7
7
  export declare function duration(ms: number): string;
8
+ /** How long ago a record last moved: a duration under a day, else whole days, which a record kept for months is read in. */
9
+ export declare function age(ms: number): string;
8
10
  /** A lesson as the listing and the note's result show it: dated, and from which depth when a session below the top wrote it. */
9
11
  export declare function lessonLine(lesson: Lesson): string;
10
- /** One record as the listing shows it: its top, when it started, its title, its split line, its parts, how long it ran and what it cost. */
11
- export declare function recordLine(record: StoredRecord, top: string): string;
12
+ /**
13
+ * One record as the listing shows it: its top, when it started, its title, its split line and its latest revision as prose,
14
+ * its parts, how long it ran, how long ago it last moved, what it cost, and whether the person pinned it.
15
+ */
16
+ export declare function recordLine(record: StoredRecord, top: string, now: number): string;
12
17
  /** What courier_recall says when the memory option is off. */
13
18
  export declare const RECALL_OFF = "Memory is off: the plugin's memory option is false, so no record of a session tree is kept or listed in this project. Decide from the task alone.";
14
- /** The listing: every record of the project, newest first, the caller's own tree marked. */
15
- export declare function recallListText(records: ReadonlyArray<StoredRecord>, top: string): string;
19
+ /**
20
+ * The listing: every record of the project, the pinned first and then newest first, the caller's own tree marked, each
21
+ * aged against `now`.
22
+ */
23
+ export declare function recallListText(records: ReadonlyArray<StoredRecord>, top: string, now: number): string;
16
24
  /** One record in full, as JSON: durations in milliseconds, times in epoch milliseconds, context use as a share of the model's window. */
17
25
  export declare function recallRecordText(record: StoredRecord): string;
18
26
  export declare const recallMissingText: (sessionID: string) => string;
@@ -1,11 +1,12 @@
1
1
  /** The memory's model-facing texts: what courier_recall and courier_note say. */
2
- import { clipText } from "../kernel/protocol.js";
3
- import { LESSON_MAX, oneLine, PART_STATUSES } from "./domain.js";
2
+ import { clipText, clipWords } from "../kernel/protocol.js";
3
+ import { LESSON_MAX, oneLine, PART_STATUSES, splitProse } from "./domain.js";
4
4
  /** The tool's name, which the skill and the role part name too. */
5
5
  export const RECALL_TOOL = "courier_recall";
6
6
  /** The lesson tool's name, which the skill's last step names. */
7
7
  export const NOTE_TOOL = "courier_note";
8
8
  const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
9
+ const DAY_MS = 86_400_000;
9
10
  /** A duration in milliseconds as a model reads it: seconds under half a minute, else whole minutes, else hours and minutes. */
10
11
  export function duration(ms) {
11
12
  const minutes = Math.round(ms / 60_000);
@@ -15,6 +16,11 @@ export function duration(ms) {
15
16
  return `${minutes} min`;
16
17
  return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
17
18
  }
19
+ /** How long ago a record last moved: a duration under a day, else whole days, which a record kept for months is read in. */
20
+ export function age(ms) {
21
+ const days = Math.floor(ms / DAY_MS);
22
+ return days < 1 ? duration(ms) : count(days, "day");
23
+ }
18
24
  /** How a listing counts the parts of a record: by status, in the order of `PART_STATUSES`, plus how many were isolated and how many spawns were refused. */
19
25
  function partsLine(parts) {
20
26
  if (!parts.length)
@@ -39,32 +45,47 @@ export function lessonLine(lesson) {
39
45
  const from = lesson.depth === undefined ? "" : `, from depth ${lesson.depth}`;
40
46
  return `lesson (${day(lesson.at)}${from}): "${clipText(oneLine(lesson.text), LESSON_MAX)}"`;
41
47
  }
42
- /** One record as the listing shows it: its top, when it started, its title, its split line, its parts, how long it ran and what it cost. */
43
- export function recordLine(record, top) {
48
+ /**
49
+ * One record as the listing shows it: its top, when it started, its title, its split line and its latest revision as prose,
50
+ * its parts, how long it ran, how long ago it last moved, what it cost, and whether the person pinned it.
51
+ */
52
+ export function recordLine(record, top, now) {
44
53
  const cost = record.parts.reduce((sum, part) => sum + (part.cost ?? 0), 0);
45
54
  const costed = record.parts.some((part) => part.cost !== undefined);
55
+ const revised = record.revisions?.at(-1);
46
56
  return [
47
57
  `- ${record.sessionID}${record.sessionID === top ? " (this tree)" : ""} ${day(record.startedAt)}: "${clipText(oneLine(record.title), 60)}"`,
48
- clipText(oneLine(record.split), 160),
58
+ clipWords(splitProse(record.split), 160),
59
+ ...(revised ? [`then, after ${count(revised.parts, "part")}: ${clipWords(splitProse(revised.text), 160)}`] : []),
49
60
  partsLine(record.parts),
50
- duration(record.movedAt - record.startedAt),
61
+ `ran ${duration(record.movedAt - record.startedAt)}`,
62
+ `last moved ${age(now - record.movedAt)} ago`,
51
63
  ...(costed ? [`$${cost.toFixed(2)}`] : []),
52
64
  record.finished ? "finished" : "not finished",
65
+ ...(record.pinned ? ["pinned"] : []),
53
66
  ...(record.recalled ? [`recalled${record.recalled.at === undefined ? " by a part" : ""}`] : []),
54
67
  ...(record.lesson ? [lessonLine(record.lesson)] : []),
55
68
  ].join(" · ");
56
69
  }
70
+ /** What the listing says of pinned records, when it holds one. */
71
+ const PINNED = "A record marked \"pinned\" is one the person chose to keep and listed first: weigh it before the rest.";
57
72
  /** What courier_recall says when the memory option is off. */
58
73
  export const RECALL_OFF = "Memory is off: the plugin's memory option is false, so no record of a session tree is kept or listed in this project. Decide from the task alone.";
59
- /** The listing: every record of the project, newest first, the caller's own tree marked. */
60
- export function recallListText(records, top) {
74
+ /**
75
+ * The listing: every record of the project, the pinned first and then newest first, the caller's own tree marked, each
76
+ * aged against `now`.
77
+ */
78
+ export function recallListText(records, top, now) {
61
79
  const others = records.filter((record) => record.sessionID !== top);
80
+ const pinned = records.some((record) => record.pinned);
62
81
  const intro = others.length
63
- ? `${count(others.length, "earlier tree")} recorded in this project, newest first; weigh how they were split and how their parts went before you decide. ` +
64
- "A record holds what the plugin saw, not the work, and a lesson what its orchestrator concluded, next to the facts that bear it out: " +
65
- "courier_recall with a sessionID gives one in full, with each part's status, duration, cost and context use."
82
+ ? `${count(others.length, "earlier tree")} recorded in this project, ${pinned ? "pinned first, then newest first" : "newest first"}; weigh how they were split and how their parts went before you decide. ` +
83
+ "A record holds what the plugin saw, not the work, and a lesson what its orchestrator concluded, next to the facts that bear it out. " +
84
+ "The facts are as they stood when the tree last moved: \"not finished\" with an old \"last moved\" is a stale record, not a tree running now. " +
85
+ "courier_recall with a sessionID gives one in full, with each part's status, duration, cost and context use." +
86
+ (pinned ? ` ${PINNED}` : "")
66
87
  : "No earlier tree is recorded in this project: a tree's record is written by a courier_recall call and as each turn in it ends, and kept 90 days. Decide from the task alone.";
67
- return [intro, ...records.map((record) => recordLine(record, top))].join("\n");
88
+ return [intro, ...records.map((record) => recordLine(record, top, now))].join("\n");
68
89
  }
69
90
  /** One record in full, as JSON: durations in milliseconds, times in epoch milliseconds, context use as a share of the model's window. */
70
91
  export function recallRecordText(record) {
@@ -21,7 +21,7 @@ export function addMemoryTools(tools, ports) {
21
21
  name: RECALL_TOOL,
22
22
  options: { codemode: false },
23
23
  description: "Look once at what earlier session trees in this project did, before you decide whether and how to split a task: " +
24
- "one line per tree, newest first, with how its top split the task, how each part went (its status, how long it ran, " +
24
+ "one line per tree, those the person pinned first and then newest first, with how its top split the task, how each part went (its status, how long it ran, " +
25
25
  "what it cost, how full its context got) and whether a spawn was refused; with sessionID, one tree's record in full. " +
26
26
  "Records hold no report bodies or files. Call it once at the start, not in a loop, and not to check on sessions you " +
27
27
  "started: their reports wake you.",
@@ -32,7 +32,7 @@ export function addMemoryTools(tools, ports) {
32
32
  case "off":
33
33
  return { content: RECALL_OFF, metadata: { memory: false } };
34
34
  case "list":
35
- return { content: recallListText(result.records, result.top), metadata: { projectID: result.projectID, top: result.top, records: result.records } };
35
+ return { content: recallListText(result.records, result.top, ports.memory.now()), metadata: { projectID: result.projectID, top: result.top, records: result.records } };
36
36
  case "record":
37
37
  return { content: recallRecordText(result.record), metadata: { projectID: result.projectID, record: result.record } };
38
38
  case "missing":
@@ -1,10 +1,10 @@
1
1
  import type { Plugin } from "@opencode/plugin/tui";
2
- import { type Forgotten, type MemoryLesson, type MemoryPart, type MemoryRecord } from "./rpc.js";
2
+ import { type Forgotten, type MemoryLesson, type MemoryPart, type MemoryReads, type MemoryRecord, type Pinned } from "./rpc.js";
3
3
  type Context = Plugin.Context;
4
4
  /** The command that opens the memory view: `/courier memory`, or the palette. */
5
5
  export declare const MEMORY_COMMAND = "courier.memory";
6
6
  /** The memory view's line of the `/courier` command's usage. */
7
- export declare const MEMORY_USAGE = "/courier memory lists this project's tree records, to read or forget";
7
+ export declare const MEMORY_USAGE = "/courier memory lists this project's tree records, to read, pin or forget";
8
8
  /** What the view says, in a toast, with the memory option off in the project asked. */
9
9
  export declare const MEMORY_OFF = "Memory is off in this project: the plugin's memory option is false, so no record of a session tree is kept or listed here.";
10
10
  /** What the view says, in a toast, when the project has no record: when none was written yet, or the last was forgotten. */
@@ -15,6 +15,8 @@ export declare const LESSON_LINE_MAX = 160;
15
15
  export declare const ROW_TITLE_MAX = 46;
16
16
  /** The group of the records moved on the current local day, over the others' local dates, as the host's Sessions dialog has it. */
17
17
  export declare const TODAY = "Today";
18
+ /** The group of the records the person pinned, above the days, shown only while there is one. */
19
+ export declare const PINNED = "Pinned";
18
20
  /** The widest a row of the lesson in a record in full is; a longer lesson wraps over rows. */
19
21
  export declare const LESSON_ROW = 90;
20
22
  /** A record's name, as its rows and confirms show it: its title on one line, else its top session. */
@@ -25,11 +27,16 @@ export declare function partsCounted(parts: ReadonlyArray<MemoryPart>): string;
25
27
  export declare function costOf(record: MemoryRecord): number | undefined;
26
28
  /** A list row's title: the tree's title, its top session's id without one; the open session's own tree says so. */
27
29
  export declare function recordTitle(record: MemoryRecord, open?: string): string;
28
- /** A list row's group: `Today` for a tree last moved on the local day of `now`, else that local date; the RPC's order, by the last move, keeps the groups newest first. */
30
+ /**
31
+ * A list row's group: `Pinned` for a pinned record, `Today` for a tree last moved on the local day of `now`, else that
32
+ * local date; the RPC's order, pinned first and then by the last move, keeps the groups in that order.
33
+ */
29
34
  export declare function recordCategory(record: MemoryRecord, now?: number): string;
35
+ /** How often other trees' courier_recall calls listed the record and opened it in full: "shown 7 times, opened once"; "shown 7 times" alone when none opened it. */
36
+ export declare function readsLine(reads: MemoryReads): string;
30
37
  /** A lesson as the view shows it: dated on the local day, from which depth when a session below the top wrote it, and its text. */
31
38
  export declare function lessonHeading(lesson: MemoryLesson): string;
32
- /** A list row's description: its parts by status, how long the tree ran, what its parts cost, and that it is not finished when it is not. */
39
+ /** A list row's description: its parts by status, how long the tree ran, what its parts cost, that it is not finished when it is not, and how often other trees read it. */
33
40
  export declare function recordDescription(record: MemoryRecord): string;
34
41
  /** The lines under a list row: the lesson, dated and from which depth, clipped; none without a lesson. */
35
42
  export declare function recordDetails(record: MemoryRecord): string[];
@@ -63,9 +70,11 @@ export declare function detailOptions(record: MemoryRecord): Array<{
63
70
  export declare function forgetMessage(record: MemoryRecord, lesson: boolean): string;
64
71
  /** The toast after a forget, as the server reports it. */
65
72
  export declare function forgottenLine(record: MemoryRecord, outcome: Forgotten["outcome"]): string;
73
+ /** The toast after a pin, as the server reports it. */
74
+ export declare function pinnedLine(record: MemoryRecord, outcome: Pinned["outcome"]): string;
66
75
  /**
67
76
  * Adds the memory view: the project's records (the open session's location, else the TUI's, else the server's default) under
68
- * `Today` and the local days they last moved; `enter` shows one in full, `x` forgets one or its lesson after a confirm, `esc` goes back and closes.
77
+ * `Pinned`, `Today` and the days they last moved; `enter` shows one in full, `p` pins or unpins one, `x` forgets one or its lesson after a confirm.
69
78
  */
70
79
  export declare function addMemoryView(ctx: Context): {
71
80
  open: () => Promise<void>;
package/dist/rpc.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { treeRpc, type Row, type Stopped, type Summary, type Tree, type TreeRpc, type Wait } from "./tree/rpc.js";
2
2
  export { personRpc, type Answered, type Asks, type PersonRpc } from "./person/rpc.js";
3
- export { memoryRpc, type Forgotten as MemoryForgotten, type MemoryLesson, type MemoryPart, type MemoryRecord, type MemoryRpc, type Records } from "./memory/rpc.js";
3
+ export { memoryRpc, type Forgotten as MemoryForgotten, type MemoryLesson, type MemoryPart, type MemoryReads, type MemoryRecord, type MemoryRpc, type Pinned as MemoryPinned, type Records } from "./memory/rpc.js";
@@ -95,7 +95,7 @@ export function digby(rise, { top = 6, eyes = "open", slump = false, shift = 0,
95
95
  span(head + 2, 8, "k", (column) => !eyeColumns.includes(column), lean(2));
96
96
  span(head + 4, 2, "a", undefined, lean(4));
97
97
  span(10, SIZE, ".", (column) => column >= 6 && column <= 9);
98
- span(10, top, mound, (column) => column >= 6 && column <= 9, shift);
98
+ span(10, top, mound, (column) => column + shift >= 6 && column + shift <= 9, shift);
99
99
  span(11, 10, mound, undefined, shift);
100
100
  span(12, 12, mound, undefined, shift);
101
101
  for (const row of [13, 14, 15])
@@ -60,7 +60,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
60
60
  readonly isolated: boolean;
61
61
  readonly parentID: string;
62
62
  readonly created: number;
63
- readonly state: "stalled" | "running" | "idle" | "gone" | "waiting";
63
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
64
64
  readonly waits: readonly ({
65
65
  readonly type: "permission";
66
66
  readonly action: string;
@@ -102,7 +102,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
102
102
  readonly isolated: boolean;
103
103
  readonly parentID: string;
104
104
  readonly created: number;
105
- readonly state: "stalled" | "running" | "idle" | "gone" | "waiting";
105
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
106
106
  readonly waits: readonly ({
107
107
  readonly type: "permission";
108
108
  readonly action: string;
@@ -229,12 +229,12 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
229
229
  readonly by: import("effect/Schema").String;
230
230
  }>;
231
231
  readonly output: import("effect/StandardSchema").StandardSchemaV1<{
232
- readonly outcome: "removed" | "gone" | "forgotten";
232
+ readonly outcome: "gone" | "removed" | "forgotten";
233
233
  } | {
234
234
  readonly outcome: "kept";
235
235
  readonly reason: string;
236
236
  }, {
237
- readonly outcome: "removed" | "gone" | "forgotten";
237
+ readonly outcome: "gone" | "removed" | "forgotten";
238
238
  } | {
239
239
  readonly outcome: "kept";
240
240
  readonly reason: string;
@@ -227,7 +227,7 @@ export declare const treeRpc: {
227
227
  readonly isolated: boolean;
228
228
  readonly parentID: string;
229
229
  readonly created: number;
230
- readonly state: "stalled" | "running" | "idle" | "gone" | "waiting";
230
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
231
231
  readonly waits: readonly ({
232
232
  readonly type: "permission";
233
233
  readonly action: string;
@@ -269,7 +269,7 @@ export declare const treeRpc: {
269
269
  readonly isolated: boolean;
270
270
  readonly parentID: string;
271
271
  readonly created: number;
272
- readonly state: "stalled" | "running" | "idle" | "gone" | "waiting";
272
+ readonly state: "gone" | "stalled" | "running" | "idle" | "waiting";
273
273
  readonly waits: readonly ({
274
274
  readonly type: "permission";
275
275
  readonly action: string;
@@ -415,12 +415,12 @@ export declare const treeRpc: {
415
415
  readonly by: Schema.String;
416
416
  }>;
417
417
  readonly output: import("effect/StandardSchema").StandardSchemaV1<{
418
- readonly outcome: "removed" | "gone" | "forgotten";
418
+ readonly outcome: "gone" | "removed" | "forgotten";
419
419
  } | {
420
420
  readonly outcome: "kept";
421
421
  readonly reason: string;
422
422
  }, {
423
- readonly outcome: "removed" | "gone" | "forgotten";
423
+ readonly outcome: "gone" | "removed" | "forgotten";
424
424
  } | {
425
425
  readonly outcome: "kept";
426
426
  readonly reason: string;