Skip to content

Commit 1899d76

Browse files
committed
docs(session): clarify note continuity protocol
1 parent 3086e6a commit 1899d76

3 files changed

Lines changed: 140 additions & 34 deletions

File tree

opencode.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/services/session-mcp-runtime.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
SESSION_NOTES_READ_DESCRIPTION,
1313
SESSION_NOTES_WRITE_DESCRIPTION,
1414
SESSION_SEARCH_BASELINE_DESCRIPTION,
15+
SESSION_SEARCH_STRENGTHENED_DESCRIPTION,
1516
} from "./session-mcp-runtime.ts";
1617
import type { SessionExecutor } from "./session-executor.ts";
1718
import {
@@ -752,6 +753,71 @@ describe("session-mcp-runtime", () => {
752753
}
753754
});
754755

756+
it("pins the cross-tool continuity protocol language in shipped descriptions", () => {
757+
const search = SESSION_SEARCH_BASELINE_DESCRIPTION.toLowerCase();
758+
const strengthened = SESSION_SEARCH_STRENGTHENED_DESCRIPTION.toLowerCase();
759+
const read = SESSION_NOTES_READ_DESCRIPTION.toLowerCase();
760+
const write = SESSION_NOTES_WRITE_DESCRIPTION.toLowerCase();
761+
762+
// session_search should bias agents toward search-first recall, especially
763+
// at the start of a new session or after compaction, and should explicitly
764+
// chain to session_notes_read for note hits.
765+
assertStringIncludes(search, "first");
766+
assertStringIncludes(search, "after compaction");
767+
assertStringIncludes(search, "session_notes_read");
768+
assertStringIncludes(search, "session_notes_write");
769+
770+
// The strengthened overlay (used on new sessions and post-compaction turns)
771+
// must keep the strong recommendation and still chain to session_notes_read.
772+
assertStringIncludes(strengthened, "new session");
773+
assertStringIncludes(strengthened, "post-compaction");
774+
assertStringIncludes(strengthened, "strongly recommended");
775+
assertStringIncludes(strengthened, "session_search");
776+
assertStringIncludes(strengthened, "session_notes_read");
777+
778+
// session_notes_read should reinforce that it is the second step after
779+
// session_search and that new-session/post-compaction are recall moments.
780+
// It must also tell agents that progress updates use non-empty text so
781+
// they don't accidentally delete via empty-text replace.
782+
assertStringIncludes(read, "session_search");
783+
assertStringIncludes(read, "after compaction");
784+
assertStringIncludes(read, "non-empty");
785+
// Pin the empty-text deletion footgun warning on the read description so
786+
// future edits cannot silently drop it. Use small lowercase-normalized
787+
// fragments rather than exact sentence/casing.
788+
assertStringIncludes(read, "empty `text`");
789+
assertStringIncludes(read, "delete");
790+
assertStringIncludes(read, "fully");
791+
assertStringIncludes(read, "complete");
792+
793+
// session_notes_write should encode the full lifecycle protocol:
794+
// start -> search/read existing then create with checklist;
795+
// sub-task done -> upsert; stop mid-task -> update before reporting;
796+
// ~75% context counts as stopping mid-task; complete -> clear (delete)
797+
// only when fully done, before reporting back.
798+
for (
799+
const phrase of [
800+
// Step 1: search before creating, then create checklist.
801+
"session_search",
802+
"session_notes_read",
803+
"checklist",
804+
// Step 2: upsert with id.
805+
"replace: <id>",
806+
"non-empty",
807+
// Step 3: stop mid-task / approaching context limit.
808+
"mid-task",
809+
"before reporting back",
810+
"75%",
811+
// Step 4: clear only when fully complete; clearing == delete.
812+
"fully",
813+
"complete",
814+
"empty `text`",
815+
]
816+
) {
817+
assertStringIncludes(write, phrase.toLowerCase());
818+
}
819+
});
820+
755821
it("executes the full note action contract through the runtime", async () => {
756822
const redis = new RedisClient({ endpoint: "redis://unused" });
757823
const runtime = createSessionMcpRuntime({

src/services/session-mcp-runtime.ts

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,39 @@ const SEARCH_RESULT_CREATED_AT_FALLBACK = "1970-01-01T00:00:00.000Z";
4545

4646
export const SESSION_NOTES_WRITE_DESCRIPTION = [
4747
"Pin working context as a session note so it survives topic switches, long tool",
48-
"loops, and compaction. Use this BEFORE drifting away from important context:",
48+
"loops, and compaction. Notes are the primary continuity surface — write or",
49+
"update one BEFORE you stop, hand back to the user, or risk losing context.",
50+
"",
51+
"Required lifecycle (follow this protocol — do not skip steps):",
52+
"",
53+
"1. **Starting a new task** → first run `session_search` and",
54+
" `session_notes_read` on relevant note hits to check whether an existing",
55+
" note already covers this work. If one does, upsert it (see step 2). If",
56+
" none exists, create a new note with a short description and a markdown",
57+
" checklist of the planned sub-tasks.",
58+
"2. **Finishing a sub-task** → upsert the same note (call this tool with",
59+
" non-empty `text` and `replace: <id>`) to check off the completed item.",
60+
"3. **Stopping mid-task** (blocker, finding, awaiting user input) → upsert",
61+
" the note with current progress and findings BEFORE reporting back to the",
62+
" user. Approaching ~75% context usage counts as stopping mid-task: upsert",
63+
" with full detail so a post-compaction agent can resume without",
64+
" re-derivation.",
65+
"4. **Completing the task** → ONLY when the task is fully done, clear the",
66+
" note BEFORE reporting back. Clearing means deleting it: call this tool",
67+
' with empty `text` and `replace: <id>` (or `replace: "*"` with empty text',
68+
" to clear all notes for this scope). Do not clear a note for a paused,",
69+
" blocked, or partially complete task — upsert per step 3 instead.",
70+
"",
71+
"Also write/update a note:",
4972
"",
50-
"- Before switching to a different topic or task",
5173
"- After a user correction changes your assumptions",
52-
"- When a small task stalls and work shifts elsewhere",
74+
"- Before switching to a different topic or task",
5375
"- During long tool-calling sequences where key state lives only in your context",
54-
"- Before compaction is likely (many messages into a session)",
5576
"",
5677
"Do NOT use this for ephemeral state that will be irrelevant within a few turns",
5778
"(e.g., intermediate variable values, transient build errors you are about to",
58-
"fix, or scratchpad reasoning). Notes are for context you need to survive",
59-
"across topic switches or compaction — not for every observation.",
79+
"fix, or scratchpad reasoning). Notes are for context that must survive topic",
80+
"switches, compaction, or a fresh agent picking up the work.",
6081
"",
6182
"Accepts `text` (markdown body) and optional `replace`:",
6283
"",
@@ -70,59 +91,84 @@ export const SESSION_NOTES_WRITE_DESCRIPTION = [
7091
'- omit `replace` to create a new note and return `{ action: "created", id }`',
7192
"",
7293
"Always rely on the returned `action` instead of inferring the outcome from the",
73-
"inputs alone.",
94+
"inputs alone. Capture the returned `id` so subsequent updates upsert the same",
95+
"note rather than creating duplicates.",
7496
"",
75-
"Prefer concise markdown with headings, bullets, and short code snippets:",
97+
"Prefer concise markdown with a heading and a checklist:",
7698
"",
7799
" ## Current Task: Fix Redis TTL bug",
78100
" - **File:** `src/services/redis-client.ts`",
79101
" - **Root cause:** TTL not refreshed on read",
80-
" - **Next step:** Add EXPIRE call after GET in `refreshEntry()`",
102+
" - **Progress:**",
103+
" - [x] Reproduce on staging",
104+
" - [x] Identify missing EXPIRE in `refreshEntry()`",
105+
" - [ ] Add regression test",
106+
" - [ ] Land fix",
81107
" - **User correction:** Use seconds not milliseconds for TTL",
82108
].join("\n");
83109

84110
export const SESSION_NOTES_READ_DESCRIPTION = [
85-
"Reopen exact pinned note text instead of reconstructing it from memory. Use this",
86-
"when you resume an interrupted topic, need the exact wording of a pinned user",
87-
"instruction, or want to verify what you previously noted before acting on it.",
111+
"Reopen exact pinned note text instead of reconstructing it from memory. This",
112+
"is the second step of the recall protocol: after `session_search` surfaces a",
113+
"matching note hit, call `session_notes_read` with that note `id` to load the",
114+
"full body before acting.",
115+
"",
116+
"Call this tool whenever you need authoritative pinned context, especially:",
117+
"",
118+
"- At the start of a new session, after compaction, or when resuming an",
119+
" interrupted task — these are the highest-priority recall moments.",
120+
'- When `session_search` returns a `type: "note"` hit relevant to your task.',
121+
"- When you need the exact wording of a pinned user instruction, plan, or",
122+
" checklist before acting on it.",
88123
"",
89124
"If `id` is provided, returns that single note as",
90125
"`{ note: { id, text, created_at, updated_at } }`; when the id does not exist,",
91126
"returns `{ note: null }`.",
92127
"",
93128
"Always prefer reading a pinned note over reciting its contents from recall —",
94-
"notes are the source of truth for intentionally preserved context.",
129+
"notes are the source of truth for intentionally preserved context. After",
130+
"reading, update the note as you make progress by calling",
131+
"`session_notes_write` with non-empty `text` and `replace: <id>` (passing",
132+
"empty `text` would delete the note — only do that when the task is fully",
133+
"complete).",
95134
].join("\n");
96135

97136
export const SESSION_SEARCH_BASELINE_DESCRIPTION = [
98-
"Search local indexed content for the current root session. This is the default",
99-
"recall path — use it FIRST when you need prior context, especially:",
137+
"Search local indexed content for the current root session. This is the FIRST",
138+
"step of the recall protocol — run a `session_search` BEFORE doing other work",
139+
"whenever prior context may exist, especially:",
100140
"",
101-
"- At the start of a new session or after compaction",
102-
"- When resuming a topic you worked on earlier",
103-
"- Before re-solving a problem that may already have a solution in session history",
104-
"- To check whether pinned session notes already contain the context you need",
141+
"- At the start of a new session or immediately after compaction (highest",
142+
" priority — pinned notes and prior decisions may not be in working memory).",
143+
"- When resuming a topic you worked on earlier in this or a sibling session.",
144+
"- Before re-solving a problem that may already have a solution in session",
145+
" history, or contradicting an earlier decision.",
146+
"- To check whether pinned session notes already contain the context you need.",
105147
"",
106148
'Results may include indexed memory content (type: "memory") and, when pinned',
107149
'session notes exist, matching notes (type: "note"). Note results include',
108150
'`id`, `root_session_id`, `scope: "local" | "project"`, `created_at`, and',
109-
"`updated_at` — use `session_notes_read` with the note `id` to reopen the full",
110-
"note text. Not every query will return note results; notes only appear when they",
111-
"match the search query and the session has pinned notes.",
151+
"`updated_at` — when a note hit is relevant, immediately call",
152+
"`session_notes_read` with that `id` to load the full note text. Do not",
153+
"paraphrase a note from its snippet. Not every query returns note results;",
154+
"notes only appear when they match the query and the session has pinned notes.",
112155
"",
113-
"Prefer session_search over reconstructing context from scratch. If search",
114-
"returns relevant note hits, read the note before duplicating its contents.",
156+
"Prefer `session_search` over reconstructing context from scratch. The full",
157+
"continuity protocol is: (1) `session_search` to discover, (2)",
158+
"`session_notes_read` for relevant note hits, (3) `session_notes_write` to",
159+
"pin or update progress before stopping or reporting back.",
115160
].join("\n");
116161

117162
export const SESSION_SEARCH_STRENGTHENED_DESCRIPTION =
118163
SESSION_SEARCH_BASELINE_DESCRIPTION +
119164
"\n\n" +
120165
[
121166
"⚠️ This is a new session or a post-compaction turn. Prior context may have been",
122-
"summarized or is not yet in your working memory. STRONGLY RECOMMENDED: run a",
123-
"session_search query before starting work to recover earlier decisions, pinned",
124-
"notes, and task state. This avoids re-solving problems or contradicting earlier",
125-
"decisions that survived compaction.",
167+
"summarized or is not yet in your working memory. STRONGLY RECOMMENDED before",
168+
"doing anything else: run `session_search` to recover earlier decisions, pinned",
169+
"notes, and task state, then call `session_notes_read` on any relevant note",
170+
"hits to load their exact text. This avoids re-solving problems, contradicting",
171+
"earlier decisions, or duplicating notes that already track the work.",
126172
].join("\n");
127173

128174
type PluginToolArgs = Parameters<typeof tool>[0]["args"];

0 commit comments

Comments
 (0)