@jeffober/thatch 0.1.21 → 0.1.22

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.
@@ -17,7 +17,8 @@ You will be given a JSON payload with:
17
17
  3. Do not save session-specific state, ephemeral debugging details, or info already in CLAUDE.md.
18
18
  4. Apply the durability test before saving: will this still be true and useful in a future session that knows nothing about this branch, commit, or session? If not, skip it. Do not store point-in-time values (migration indices, line numbers, file counts), commit hashes or branch history narratives (git is the source of truth), or anything re-derivable from the codebase faster than recalling it.
19
19
  5. Write for a future session with zero current context. No "we", "our session", "just now".
20
- 6. Your final message must be exactly: "Extraction complete." — do not list or summarize what you saved. The memories are in the store; the parent session does not need a report.
20
+ 6. Call thatch_extraction_done to mark the run complete. Do this even when nothing was worth saving: the parent session holds the buffered interactions until a completion signal arrives, and a memory write is not one on a no-save run.
21
+ 7. Your final message must be exactly: "Extraction complete." — do not list or summarize what you saved. The memories are in the store; the parent session does not need a report.
21
22
 
22
23
  ## What to extract
23
24
 
@@ -44,7 +44,7 @@ Sections, in order: SYNOPSIS, PROBLEM, BACKGROUND (conditional), PROPOSED APPROA
44
44
 
45
45
  **SYNOPSIS, PROBLEM, PROPOSED APPROACH, and ACCEPTANCE CRITERIA are mandatory.** A small ticket gets short sections, not fewer sections. Conditional sections appear only when warranted; omit them when empty. Never emit an empty header.
46
46
 
47
- **Scale the body to the work's conceptual size.** A one-line fix gets a one-line PROBLEM, a one-line PROPOSED APPROACH, and one or two acceptance criteria. A multi-PR feature gets full sections with real depth. The implementer should be able to start work after reading the ticket, but they should not have to read a novel to fix a guard clause.
47
+ **Scale the body to the work's complexity, not its size in files or lines.** Estimate complexity by how much new mental model the implementer must build (see the point-based budgets in Length and style). A one-line fix gets a one-line PROBLEM, a one-line PROPOSED APPROACH, and one or two acceptance criteria. A multi-PR feature gets full sections with real depth. The implementer should be able to start work after reading the ticket, but they should not have to read a novel to fix a guard clause. Most of this skill's guidance exists to tame large tickets; applying it wholesale to a small one produces exactly the bloat the skill warns against.
48
48
 
49
49
  ### SYNOPSIS
50
50
 
@@ -66,11 +66,13 @@ The reader should know what the problem is before they read why it happens.
66
66
 
67
67
  No solution yet. The reader should finish this section agreeing there is something to address, even with zero code knowledge.
68
68
 
69
- Cover three things, each in its own paragraph:
69
+ Cover three things, each in its own paragraph when the ticket has the complexity to warrant it:
70
70
  1. **What is wrong**, concretely. Name the component, the behavior, and the failure mode.
71
71
  2. **Why it matters.** Incident history, risk rating, customer impact, or "preventive: blocks a class of bug we keep almost-hitting."
72
72
  3. **Hazard rating and motivation.** How likely is the bad outcome, and under what conditions? Then answer "why now?": if the risk is low, what motivates this ticket? A prior PR identified it as a follow-up, the API still permits a failure shape, or the next planned change would make it worse. The lead's question is "should I prioritize this?" - give them the answer. _PLAT-135's ticket did this well: it named the cross-wait, then said "why it never fired" and rated the window as once-per-process cold load._
73
73
 
74
+ A small ticket covers all three in a single paragraph; split into one paragraph per topic only when the content earns it.
75
+
74
76
  If the problem is preventive (no current defect), say so plainly. Then say why it is worth fixing despite the low risk.
75
77
 
76
78
  ### BACKGROUND (conditional)
@@ -89,6 +91,8 @@ Omit when the problem is self-contained. A one-line "follow-up from #6838" can l
89
91
 
90
92
  The design concept. Not a full implementation plan. Enough that the implementer knows the direction: "load key material outside the mutex" not "restructure ActiveKey to use cachedKey/publish helpers with a publish function that zeroizes the loser's DEK." The implementer writes the implementation plan; the ticket sets the compass heading.
91
93
 
94
+ Present each mechanism in one short paragraph: what it does, plus the constraint that justifies its existence. Keep a justification when it answers an objection the reader will have (a lock that looks redundant, a retry that looks pointless). Compress failure-mode sagas to their consequence: "a leaked lock blocks every later attempt" carries what a paragraph of driver internals would. Reasons of the form "why not X" (no metric, no kill switch, no singleflight) go in NOTES as declined alternatives.
95
+
92
96
  For investigative tickets, this is "investigate X and propose a solution" rather than a concrete approach. Say so explicitly so the implementer knows the ticket expects analysis before implementation.
93
97
 
94
98
  This is where the ticket diverges most from a PR description. A PR description describes a change that already happened. A ticket proposes one that hasn't. The prose is prescriptive ("the fix should...") rather than descriptive ("this PR moves...").
@@ -105,7 +109,9 @@ Bad criteria are vague:
105
109
  - "improve the locking situation" (untestable)
106
110
  - "restructure ActiveKey" (implementation detail, not a behavior)
107
111
 
108
- If the criterion has a test shape, name it: "SetMaxOpenConns(1) with the connection held by a tx that also resolves the key." This gives the implementer the test design without dictating the implementation.
112
+ If a criterion's test design is non-obvious, name the test shape: "SetMaxOpenConns(1) with the connection held by a tx that also resolves the key." This gives the implementer the test design without dictating the implementation. "Tested explicitly" is not a test shape.
113
+
114
+ Keep the list coarse: three to seven criteria cover most tickets. This section defines "done"; it is not a test inventory. Merge parallel checks that verify the same behavior under different triggers. Drop criteria that restate PROPOSED APPROACH as a checklist - the criterion must add an observable gate, not echo the design.
109
115
 
110
116
  ### RELEASE PLAN (conditional)
111
117
 
@@ -115,7 +121,7 @@ Rollout strategy for tickets that change production behavior. Only when the chan
115
121
  - Deploy sequence (which services, in what order).
116
122
  - Rollback path (what to do if it goes wrong).
117
123
 
118
- A pure internal refactor with no operator-visible behavior change does not need this section. A migration that gates on a backfill completing does.
124
+ A pure internal refactor with no operator-visible behavior change does not need this section. Neither does a change whose rollout is a normal deploy: if the only available bullets say "nothing special happens," omit the section - its absence is the nothing-special signal. A migration that gates on a backfill completing, or a revert that is not free, does need it.
119
125
 
120
126
  ### VALIDATION (conditional)
121
127
 
@@ -129,10 +135,12 @@ Only when prod verification is non-trivial. A pure refactor with good tests does
129
135
 
130
136
  ### DEPENDENCIES (conditional)
131
137
 
132
- Blocking relationships and ordering constraints. "Blocked by PLAT-126" is the tracker field; the _why_ belongs here: "PLAT-125 partitions by location_fingerprint, so it can only run after backfill completes (count == 0), not merely after PLAT-124 merges." That kind of nuance is what prevents a well-intentioned lead from reordering the queue.
138
+ The tracker's blocker links carry the what; this section carries only what links cannot. Write a bullet when there is a _why_ the link cannot express: "PLAT-125 partitions by location_fingerprint, so it can only run after backfill completes (count == 0), not merely after PLAT-124 merges." That kind of nuance is what prevents a well-intentioned lead from reordering the queue. "Blocked by PLAT-126" with nothing to add is the tracker field - set the link there and omit the bullet.
133
139
 
134
140
  Distinguish hard data dependencies (cannot run until X completes) from soft dependencies (should run after X for cleanliness, but technically works independently). Say which kind it is.
135
141
 
142
+ If every bullet would restate a tracker link, omit the section and set the links.
143
+
136
144
  ### NOTES
137
145
 
138
146
  Always a `## NOTES` markdown header. Never a bare `Notes:` line ending a section.
@@ -195,12 +203,24 @@ These are hard rules, not suggestions.
195
203
 
196
204
  ## Length and style
197
205
 
198
- Target: the implementer can start work after one read. The lead can prioritize after reading SYNOPSIS and PROBLEM.
206
+ Target: the implementer can start work after one read. The lead can prioritize after reading SYNOPSIS and PROBLEM. Err on the shorter side.
207
+
208
+ Estimate the ticket's **complexity** (how much new mental model the implementer must build), not its size in files or lines. Complexity drives the word budget the way scrum points drive effort estimates:
209
+
210
+ - **1 point** (one guard, one flag, one constant): the implementer needs to know what to change, not a lesson in the subsystem. ~100-200 words. SYNOPSIS one line, PROBLEM 2-4 sentences (still rate the hazard, even if it is "no current defect"), PROPOSED APPROACH 1-2 sentences, one to three acceptance criteria.
211
+ - **2-3 points** (new function, restructured flow, multi-file refactor): the implementer needs a before/after they cannot infer from the code alone. ~200-400 words. Full mandatory sections; BACKGROUND only when prior work or project context earns it.
212
+ - **5 points** (new feature, migration sequence): the implementer needs a mental model they do not have yet. ~400-600 words. RELEASE PLAN, VALIDATION, and DEPENDENCIES when prod coordination warrants.
213
+ - **8 points** (multi-PR effort, subsystem redesign, constraint-dense work): ~800-1100 words. Complex tickets legitimately need more text; spend it on context and starting state, not on repetition. If PROPOSED APPROACH is longer than the rest of the ticket combined, the ticket is probably a design doc wearing a ticket costume; split it.
199
214
 
200
- Scale the body to the work's conceptual size:
201
- - Small fix (one guard, one flag, one constant): one paragraph per mandatory section, one to three acceptance criteria. ~20-line ceiling.
202
- - Moderate change (new function, restructured flow, multi-file refactor): full sections with real depth. No hard ceiling, but each paragraph earns its place.
203
- - Large change (new feature, multi-PR effort, migration sequence): full sections plus RELEASE PLAN and VALIDATION. May run a full page. If PROPOSED APPROACH is longer than the rest of the ticket combined, the ticket is probably a design doc wearing a ticket costume; split it.
215
+ Estimate by: how many new concepts the implementer must learn, whether the problem and approach have non-obvious mechanics, whether the work spans multiple workflows or systems. Not by files the change will touch or sections present. A mechanical multi-file rename is 1 point. A one-line fix with subtle semantics is 2-3.
216
+
217
+ **When a ticket runs long, take the section-level levers first.** Omit RELEASE PLAN, VALIDATION, DEPENDENCIES, or NOTES when they carry nothing a normal deploy or the tracker links already say. Coarsen ACCEPTANCE CRITERIA. Compress PROPOSED APPROACH failure sagas to their consequences. These levers remove duplication, not knowledge.
218
+
219
+ **Then step up the abstraction ladder.** Do not compress terminology or cut connective tissue to hit a word count. Instead, replace a sequence of mechanism details with the principle that subsumes them.
220
+
221
+ Example: instead of six claims about how a connection pool reuses connections, how session locks are session-scoped, how a lock taken by one statement does not guard the next, and how nothing reports the mismatch, write: "Some Postgres features attach to the connection, not the statement. The pool freely reuses connections between statements, so state set by one statement may not be there for the next, and nothing reports the mismatch." Same understanding, fewer words, no compression. The mechanism details belong in the linked design doc or the code itself, not in the ticket.
222
+
223
+ Brevity means fewer ideas, not compressed claims. If the ticket is still too long after the levers and the ladder, cut the lowest-value claim entirely. Do not cut the connective tissue that makes the remaining claims parse on first read. The knowledge build-up - each section introducing what the next one needs - survives every trim; what goes is redundancy.
204
224
 
205
225
  Inside sections, telegraphic bullet style is fine for criteria and plans: lowercase start, abbreviations (`w/`, `1x`, `~`), parenthetical shorthand. Full sentences for PROBLEM and BACKGROUND prose.
206
226
 
@@ -231,6 +251,8 @@ Read project context when the request, existing ticket, code comments, branch na
231
251
 
232
252
  Use available source material: linked tickets, linked PRs, nearby docs, code comments, commit messages, and project memory. Build a private term map: term -> plain behavior -> source. Do not dump the map into the ticket. Use it to write the shortest useful BACKGROUND.
233
253
 
254
+ When rewriting an existing ticket, mine the old text for facts - harm statements, platform behaviors, traps already discovered - but draft the new description from the sources, not from the old text. Editing the old ticket inherits its phrasing and its habits; the rewrite should stand on the plan docs, the code, and the mined facts.
255
+
234
256
  If a term or dependency controls the work and you cannot resolve it from available context, stop and ask the user. Do not guess. Do not write around the missing definition with vaguer words.
235
257
 
236
258
  Put project context in BACKGROUND only when it helps the implementer start work or helps a lead schedule it correctly. Omit generic backstory.
@@ -389,14 +411,14 @@ That reading alone conveys the shape, scope, and gates of all three tickets.
389
411
  1. **Gather**: read the code the ticket references. Check `git log` for recent changes to the same files. Identify the problem, the prior work, and the proposed direction.
390
412
  2. **Search before create**: search Linear/Jira for likely duplicate tickets. If one exists, surface it before drafting or filing.
391
413
  3. **Codebase comparison**: verify the problem still exists in the current code. If it was already fixed, tell the user. If it was partially mitigated, note the mitigation.
392
- 4. **Read project context for planned work**: if the ticket is part of a larger project, depends on prior work, uses unexplained project terms, or has scheduling dependencies, read the linked tickets, PRs, docs, comments, and memory before drafting. Build a private term map. If a term or dependency controls the work and you cannot resolve it, ask the user.
414
+ 4. **Read project context for planned work**: if the ticket is part of a larger project, depends on prior work, uses unexplained project terms, or has scheduling dependencies, read the linked tickets, PRs, docs, comments, and memory before drafting. Build a private term map. If a term or dependency controls the work and you cannot resolve it, ask the user. When rewriting an existing ticket, mine the old text for facts but draft fresh.
393
415
  5. **Draft PROBLEM first**, in one breath, without looking at code. If you cannot state the harm in three sentences, you do not understand the problem yet. Go back and read.
394
416
  6. **Write BACKGROUND** (if needed): what prior work led here, what was done, what was left behind, and what project context an engineer new to this area needs to start.
395
- 7. **Write PROPOSED APPROACH**: the compass heading, not the map. Prescriptive voice.
396
- 8. **Write ACCEPTANCE CRITERIA**: observable, testable bullets. Name the test shape where relevant.
397
- 9. **Write RELEASE PLAN** (if needed): flags, ordering, rollback.
417
+ 7. **Write PROPOSED APPROACH**: the compass heading, not the map. Prescriptive voice. One short paragraph per mechanism, each with the constraint that justifies it; compress failure sagas to their consequence; move "why not X" reasoning to NOTES.
418
+ 8. **Write ACCEPTANCE CRITERIA**: observable, testable bullets. Coarse: three to seven, merging parallel checks and dropping restatements of the approach. Name the test shape only where the test design is non-obvious.
419
+ 9. **Write RELEASE PLAN** (if needed): flags, ordering, rollback. Omit when the rollout is a normal deploy.
398
420
  10. **Write VALIDATION** (if needed): monitoring, signals, smoke tests.
399
- 11. **Write DEPENDENCIES** (if needed): what blocks this, what this blocks, and why.
421
+ 11. **Write DEPENDENCIES** (if needed): only the why that tracker blocker links cannot express. Omit when the links carry it; set the links instead.
400
422
  12. **Write SYNOPSIS last** - it is a compression of PROBLEM + the core approach.
401
423
  13. **Bold and italicize the save points** in the allowed locations. Bold phrases that name the work (components, mechanisms, behaviors). Italicize phrases that orient to significance (conclusions, payoffs), at most one per paragraph. Apply the scan check.
402
424
  14. **Add NOTES** for non-goals, declined alternatives, and scope limits. Use `## NOTES` header. Omit if empty.
@@ -404,7 +426,7 @@ That reading alone conveys the shape, scope, and gates of all three tickets.
404
426
  16. **Verify links**: check repo file links, external docs, tickets, PRs, and design artifacts where tooling allows. Do not claim an unverified link was verified.
405
427
  17. **Verify prose style and core principle**: check each rule in Prose style. Cut design-narration, authoring-sequence, and buzzwordy abstractions. Verify plain ASCII. Verify prescriptive voice (not descriptive). Verify first-use definitions for subsystem jargon in BACKGROUND or PROBLEM. Verify plain English nouns (no code-domain nouns in prose). Verify conclusion-first in PROBLEM. Verify NOTES bullets are either short pointers or self-contained.
406
428
  18. **Clarity pass**: read the draft as a cold lead skimming a backlog, then as an implementer picking up the work cold. Rewrite before returning it; do not tell the user you performed this pass. Check: does every project-specific term get plain-English meaning before or with the label? Are ambiguous terms qualified or replaced with specific behavior? Does every process word name object/action/effect? Does every contrast name current and proposed behavior? Does every causal sentence show the middle step? Did you preserve connective tissue instead of compressing meaning into noun stacks? If the draft got longer, cut a lower-value claim instead of compressing a high-value one.
407
- 19. **Verify length**: if the body exceeds what the work's conceptual size warrants, cut until each line earns its place. If PROPOSED APPROACH dominates, consider splitting into a design doc plus ticket.
429
+ 19. **Verify length**: estimate the ticket's complexity (see the point-based budgets in Length and style). If the body exceeds the budget, take the section-level levers first (omit conditional sections that duplicate the tracker or a normal deploy, coarsen acceptance criteria, compress approach sagas), then step up the abstraction ladder (replace mechanism sequences with the principle that subsumes them). If still over, cut the lowest-value claim entirely. Do not compress terminology or cut connective tissue to hit a word count. If PROPOSED APPROACH dominates, consider splitting into a design doc plus ticket.
408
430
 
409
431
  ## Anti-patterns
410
432
 
@@ -427,3 +449,7 @@ That reading alone conveys the shape, scope, and gates of all three tickets.
427
449
  - **Conclusion buried at the end.** PROBLEM that builds to its point in the last sentence forces the reader to absorb the explanation before knowing what is being explained. Lead with the problem, then explain why.
428
450
  - **Hazard rating without motivation.** "No incident has hit this shape" answers "how bad?" but not "why prioritize this?" Answer "why now?" so the lead can decide whether to schedule it.
429
451
  - **NOTES bullets that are both wordy and terse.** A bullet that compresses a paragraph of rationale into one dense sentence reads as a private note. Either flag the thing and point to the full explanation, or write a self-contained sentence or two. Not the middle thing.
452
+ - **RELEASE PLAN that argues the release is boring.** If every bullet says the rollout is routine, omit the section. Its absence is the nothing-special signal.
453
+ - **DEPENDENCIES that restate tracker links.** "Blocked by PLAT-126" with nothing to add is the tracker field. Set the link; keep the section only for the why a link cannot express.
454
+ - **Test inventory as acceptance criteria.** A dozen bullets covering every code path is a test plan, not a definition of done. Keep three to seven observable gates; merge parallel checks.
455
+ - **Failure sagas in PROPOSED APPROACH.** Paragraphs of driver internals, pool lifecycle, or library quirks. Compress to the consequence; the implementer discovers the saga at the keyboard.
@@ -28,7 +28,7 @@ file/IPC/CLI contracts ready to graduate into `bun:test` integration tests.
28
28
  | [UC-017](UC-017-buffer-tool-vs-buffer-batch.md) | `buffer-batch` (Claude, `tool_calls[]`+`session_id`) vs `buffer-tool` (Cursor, single+`conversation_id`) |
29
29
  | [UC-018](UC-018-mcp-startup-setup-detection.md) | MCP startup detects missing/broken setup instructions and warns the agent |
30
30
  | [UC-019](UC-019-archived-memory-lifecycle.md) | Archive, search exclusion, update guard, unarchive |
31
- | [UC-020](UC-020-extraction-escalation.md) | Nudge escalation (polite → insistent → ALL-CAPS), buffer persistence, `extraction_done` drain |
31
+ | [UC-020](UC-020-extraction-escalation.md) | Nudge escalation (polite → insistent → ALL-CAPS), buffer persistence, `extraction_done` accept/complete |
32
32
  | [UC-021](UC-021-prediction-autofire.md) | Prediction auto-fire: matcher scores prompt, prediction nudge injected alongside recall |
33
33
  | [UC-022](UC-022-prediction-dedup.md) | Prediction create-on-existing: dedup finds match, links edge without disconfirming |
34
34
  | [UC-023](UC-023-prediction-confidence-model.md) | Bayesian confidence math: create, confirm, soft (weak disconfirm), disconfirm signals |
@@ -18,13 +18,15 @@ client and a synthetic tool call sequence._
18
18
  - The agent loads `thatch-fact-extractor` and saves durable facts via
19
19
  `thatch_memory_remember` — or saves nothing if the activity was routine.
20
20
  - The buffer is **not** drained on nudge delivery — it persists until the agent
21
- writes a memory or calls `thatch_extraction_done`. If the nudge is ignored,
21
+ writes a memory or accepts it by calling `thatch_extraction_done`. Accepting
22
+ quiets the nudge while holding the entries until the extractor completes; a
23
+ child extractor that errors or is deleted requeues them. If the nudge is ignored,
22
24
  the next message carries a repeat nudge, escalating in urgency:
23
25
  - 1st-2nd miss: polite tone
24
26
  - 3rd consecutive miss (missedCount=2): insistent (directive) tone
25
27
  - 4th+ consecutive miss (missedCount>=3): ALL-CAPS tone
26
- The counter resets when the buffer drains (memory write or
27
- `thatch_extraction_done`).
28
+ The counter resets when the buffer drains (memory write) or is accepted
29
+ (`thatch_extraction_done`).
28
30
  - A `thatch_memory_remember` call in a child sub-agent session also drains the
29
31
  parent's buffer (via the `childToParent` Map), so dispatching the
30
32
  fact-extractor as a background task clears the parent's queue.
@@ -1,6 +1,6 @@
1
1
  # UC-020: Extraction nudge escalation and acknowledgment
2
2
 
3
- _Automatable: missedNudges counter + escalation tiers + extraction_done drain
3
+ _Automatable: missedNudges counter + escalation tiers + extraction_done accept
4
4
  are testable with the mock client and a synthetic tool call sequence._
5
5
 
6
6
  **Preconditions**
@@ -27,10 +27,12 @@ are testable with the mock client and a synthetic tool call sequence._
27
27
  persists until a memory is written or `extraction_done` is called. The nudge
28
28
  repeats each turn with escalated tone (and the payload grows if new tool
29
29
  activity adds entries between nudges).
30
- - Step 6: `thatch_extraction_done` drains the buffer and resets the
31
- `missedNudges` counter. The tool returns `"[acknowledged] extraction buffer
32
- drained"`.
33
- - Step 7: no extraction nudge appears (buffer is empty). If the user prompt
30
+ - Step 6: `thatch_extraction_done` in the parent accepts the buffer (moves it
31
+ to a holding area) and resets the `missedNudges` counter. The tool returns
32
+ `"[acknowledged]"`. Held entries drop when the extractor completes (child
33
+ memory write, child `extraction_done`, or child idle); if the child errors
34
+ or is deleted first, the entries requeue and the nudge replays.
35
+ - Step 7: no extraction nudge appears (buffer is accepted). If the user prompt
34
36
  semantically matches existing memories, a recall nudge may appear instead.
35
37
  - Alternative drain path: if a child sub-agent (dispatched via the `task`
36
38
  tool) writes a memory via `thatch_memory_remember`, the parent's buffer is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/extraction.ts CHANGED
@@ -91,6 +91,7 @@ export function buildExtractionPayload(
91
91
  */
92
92
  export class ExtractionPipeline {
93
93
  #buffers = new Map<string, ToolInteraction[]>();
94
+ #accepted = new Map<string, ToolInteraction[]>();
94
95
  #maxBuffer = 20;
95
96
 
96
97
  /** Record a tool execution for later extraction. */
@@ -110,6 +111,46 @@ export class ExtractionPipeline {
110
111
  this.#buffers.delete(sessionID);
111
112
  }
112
113
 
114
+ /**
115
+ * Accept the session's buffer: move it to a holding area so the nudge
116
+ * stops replaying while a dispatched extractor works. AMQP-style accept —
117
+ * the entries are not dropped until a completion signal arrives (the
118
+ * extractor writes a memory, calls extraction_done, or goes idle). If the
119
+ * extractor dies first, requeueAccepted puts them back.
120
+ */
121
+ accept(sessionID: string): void {
122
+ const buf = this.#buffers.get(sessionID) ?? [];
123
+ if (buf.length === 0) return;
124
+ this.#buffers.delete(sessionID);
125
+ const existing = this.#accepted.get(sessionID) ?? [];
126
+ this.#accepted.set(sessionID, [...existing, ...buf]);
127
+ }
128
+
129
+ /** Drop accepted entries: the extractor finished (save or no-save). */
130
+ completeAccepted(sessionID: string): void {
131
+ this.#accepted.delete(sessionID);
132
+ }
133
+
134
+ /**
135
+ * Move accepted entries back to pending: the extractor errored or its
136
+ * session was deleted before completing, so the facts were never
137
+ * processed. Prepended so they replay in original order. Not capped at
138
+ * maxBuffer — a silent drop here would defeat the point of requeueing;
139
+ * the next push trims back to the cap.
140
+ */
141
+ requeueAccepted(sessionID: string): void {
142
+ const accepted = this.#accepted.get(sessionID);
143
+ if (!accepted || accepted.length === 0) return;
144
+ this.#accepted.delete(sessionID);
145
+ const buf = this.#buffers.get(sessionID) ?? [];
146
+ this.#buffers.set(sessionID, [...accepted, ...buf]);
147
+ }
148
+
149
+ /** Returns the session's accepted (held) interactions. Test/introspection. */
150
+ peekAccepted(sessionID: string): ToolInteraction[] {
151
+ return this.#accepted.get(sessionID) ?? [];
152
+ }
153
+
113
154
  /**
114
155
  * Remove entries that were in the snapshot (by reference identity), keeping
115
156
  * any that arrived after the snapshot was taken. Used by the child-parent
package/src/index.ts CHANGED
@@ -129,27 +129,34 @@ export const server: Plugin = async ({ client, worktree }) => {
129
129
  // agent writes a memory, so ignored nudges accumulate and the payload
130
130
  // grows with each missed cycle.
131
131
  //
132
- // A memory_remember call in a child session (sub-agent) also drains
133
- // the parent's buffer — but only the entries that existed at dispatch
134
- // time (the snapshot). Entries from interleaved turns survive so their
135
- // facts aren't silently dropped. Without this, dispatching the
136
- // fact-extractor as a background task writes memories in the child but
137
- // never clears the parent's queue — the nudge replays every turn.
132
+ // Buffer lifecycle is AMQP-style accept/complete, so a failed
133
+ // extractor does not silently drop unprocessed interactions:
134
+ // - thatch_extraction_done in the parent ACCEPTS the buffer: it moves
135
+ // to a holding area and the nudge quiets, but entries are not
136
+ // dropped yet.
137
+ // - COMPLETE drops the held entries. Signals: a memory_remember or
138
+ // extraction_done call in a child session of that parent (the
139
+ // extractor confirming it processed them), or the child going idle
140
+ // (a no-save run that never acks still counts as processed).
141
+ // - REQUEUE returns the held entries to pending. Signals: the child
142
+ // session errors or is deleted before completing.
138
143
  //
139
- // thatch_extraction_done is an explicit acknowledgment tool the model
140
- // calls in the parent session after dispatching. Covers the case where
141
- // the sub-agent errors out or the host doesn't expose parent-child
142
- // session relationships.
144
+ // A memory_remember call in a child session (sub-agent) also drains
145
+ // the parent's pending buffer — but only the entries that existed at
146
+ // dispatch time (the snapshot). Entries from interleaved turns survive
147
+ // so their facts aren't silently dropped.
143
148
  "tool.execute.after": async (input, output) => {
144
149
  if (input.tool === "thatch_memory_remember") {
145
150
  extraction.consume(input.sessionID);
146
151
  missedNudges.delete(input.sessionID);
147
- // If this is a child session, drain only the parent's snapshot entries
148
- // (those present at dispatch time). Interleaved-turn entries survive.
149
- // If no snapshot was recorded (e.g. session.created event never
150
- // arrived), fall back to draining the entire parent buffer.
152
+ // If this is a child session, complete the parent's accepted entries
153
+ // (the extractor confirmed it is alive and saving) and drain the
154
+ // parent's snapshot entries from the pending buffer. If no snapshot
155
+ // was recorded (e.g. session.created event never arrived), fall back
156
+ // to draining the entire parent buffer.
151
157
  const parentID = childToParent.get(input.sessionID);
152
158
  if (parentID) {
159
+ extraction.completeAccepted(parentID);
153
160
  const snapshot = parentSnapshots.get(input.sessionID);
154
161
  if (snapshot) {
155
162
  extraction.consumeSnapshot(parentID, snapshot);
@@ -161,11 +168,21 @@ export const server: Plugin = async ({ client, worktree }) => {
161
168
  }
162
169
  return;
163
170
  }
164
- // Explicit acknowledgment tool drains the buffer without requiring a
165
- // memory write. The model calls this after dispatching the
166
- // fact-extractor to a sub-agent.
171
+ // thatch_extraction_done has two roles depending on the session:
172
+ // - parent, after dispatching the fact-extractor: ACCEPT the buffer
173
+ // (quiet the nudge, hold the entries for completion).
174
+ // - child extractor, at the end of its run: COMPLETE the parent's
175
+ // accepted entries, including no-save runs that write no memory,
176
+ // and drop the child's own buffer (its work is done).
167
177
  if (input.tool === "thatch_extraction_done") {
168
- extraction.consume(input.sessionID);
178
+ const parentID = childToParent.get(input.sessionID);
179
+ if (parentID) {
180
+ extraction.completeAccepted(parentID);
181
+ missedNudges.delete(parentID);
182
+ extraction.consume(input.sessionID);
183
+ } else {
184
+ extraction.accept(input.sessionID);
185
+ }
169
186
  missedNudges.delete(input.sessionID);
170
187
  return;
171
188
  }
@@ -290,7 +307,13 @@ export const server: Plugin = async ({ client, worktree }) => {
290
307
  // snapshots the parent's current buffer — so the child's later
291
308
  // memory_remember drains only those snapshot entries, not the parent's
292
309
  // entire buffer (which may have grown from interleaved turns).
293
- // session.deleted cleans both maps to avoid unbounded growth.
310
+ //
311
+ // The accepted-buffer lifecycle completes or requeues on child lifecycle
312
+ // events: idle means the extractor finished (a no-save run writes no
313
+ // memory but still processed the payload); error or deletion before
314
+ // completion means the facts were never extracted, so the entries go
315
+ // back to pending. session.deleted also cleans both maps to avoid
316
+ // unbounded growth.
294
317
  event: async ({ event }) => {
295
318
  if (event.type === "session.created") {
296
319
  const info = event.properties.info;
@@ -299,9 +322,30 @@ export const server: Plugin = async ({ client, worktree }) => {
299
322
  parentSnapshots.set(info.id, [...extraction.peek(info.parentID)]);
300
323
  }
301
324
  }
325
+ if (event.type === "session.error") {
326
+ const childID = event.properties.sessionID;
327
+ const parentID = childID ? childToParent.get(childID) : undefined;
328
+ if (parentID) extraction.requeueAccepted(parentID);
329
+ return;
330
+ }
331
+ if (event.type === "session.status" && event.properties.status?.type === "idle") {
332
+ const childID = event.properties.sessionID;
333
+ const parentID = childID ? childToParent.get(childID) : undefined;
334
+ if (parentID) {
335
+ extraction.completeAccepted(parentID);
336
+ missedNudges.delete(parentID);
337
+ }
338
+ return;
339
+ }
302
340
  if (event.type === "session.deleted") {
303
- childToParent.delete(event.properties.info.id);
304
- parentSnapshots.delete(event.properties.info.id);
341
+ const id = event.properties.info.id;
342
+ // A child deleted before completing never processed its payload.
343
+ const parentID = childToParent.get(id);
344
+ if (parentID) extraction.requeueAccepted(parentID);
345
+ childToParent.delete(id);
346
+ parentSnapshots.delete(id);
347
+ // A deleted parent takes its accepted entries with it.
348
+ extraction.completeAccepted(id);
305
349
  return;
306
350
  }
307
351
 
package/src/prompts.ts CHANGED
@@ -426,7 +426,9 @@ export function extractionNudge(
426
426
  return `[thatch] ${dispatch} to run the thatch-fact-extractor skill ` +
427
427
  `on ${count} queued tool interaction${plural}. ` +
428
428
  `${acknowledge} ` +
429
- `Then answer the user. ` +
429
+ `This is a system nudge, not user input. After dispatching and acknowledging, ` +
430
+ `stop: do not advance pending work on this nudge's account. If you are ` +
431
+ `waiting for the user to respond, continue waiting. ` +
430
432
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
431
433
  }
432
434
 
package/src/tool-defs.ts CHANGED
@@ -352,29 +352,35 @@ const markCheckedDef: ToolDef = {
352
352
  };
353
353
 
354
354
  /**
355
- * Acknowledge that the extraction nudge has been handled. Drains the
356
- * extraction buffer so the nudge does not replay on the next turn.
355
+ * Extraction-buffer acknowledgment, with AMQP-style accept/complete roles.
357
356
  *
358
- * The actual draining happens in the host's post-tool hook
357
+ * Called in a PARENT session after dispatching the fact-extractor, it accepts
358
+ * the buffer: entries move to a holding area and the nudge quiets, but they
359
+ * are not dropped until the extractor completes. Called in a CHILD extractor
360
+ * at the end of its run, it completes the parent's accepted entries —
361
+ * including no-save runs that write no memory. If the child errors or is
362
+ * deleted before either signal, the host requeues the entries so the facts
363
+ * are not lost.
364
+ *
365
+ * The actual state changes happen in the host's post-tool hook
359
366
  * (tool.execute.after for opencode, PostToolBatch/appendBatch for MCP) —
360
367
  * this tool's execute function is a no-op confirmation. The tool exists so
361
- * the model can call it in the parent session after dispatching the
362
- * fact-extractor to a sub-agent, giving the hook a recognizable tool name
363
- * to key on. Without it, the buffer only drains when a memory_remember
364
- * call lands in the same session (or a tracked child session), which fails
365
- * if the sub-agent errors out or the host doesn't expose parent-child
366
- * session relationships.
368
+ * the model has a recognizable tool name to key on. In the MCP path the
369
+ * file-backed queue is consumed on this call (or on any memory_remember),
370
+ * which is durable across interruption because the queue persists on disk
371
+ * until then.
367
372
  */
368
373
  const extractionDoneDef: ToolDef = {
369
374
  name: "extraction_done",
370
375
  description:
371
- "Acknowledge that the extraction nudge has been dispatched to a sub-agent. " +
372
- "Drains the extraction buffer so the nudge does not replay. " +
373
- "Call this in the parent session after dispatching a background task " +
374
- "to run the thatch-fact-extractor skill.",
376
+ "Acknowledge extraction-buffer work. In a parent session, call after " +
377
+ "dispatching the fact-extractor to a sub-agent: accepts the buffer " +
378
+ "(quiets the nudge) while keeping entries until the extractor completes. " +
379
+ "In the fact-extractor sub-agent, call at the end of the run to mark the " +
380
+ "entries complete, even when nothing was worth saving.",
375
381
  args: {},
376
382
  async execute() {
377
- return "[acknowledged] extraction buffer drained";
383
+ return "[acknowledged]";
378
384
  },
379
385
  };
380
386