@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.
- package/artifacts/skills/thatch-fact-extractor.md +2 -1
- package/artifacts/skills/ticket-description.md +42 -16
- package/docs/qa/use-cases/README.md +1 -1
- package/docs/qa/use-cases/UC-003-extraction-nudge.md +5 -3
- package/docs/qa/use-cases/UC-020-extraction-escalation.md +7 -5
- package/package.json +1 -1
- package/src/extraction.ts +41 -0
- package/src/index.ts +65 -21
- package/src/prompts.ts +3 -1
- package/src/tool-defs.ts +20 -14
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
|
|
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
|
|
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):
|
|
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**:
|
|
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`
|
|
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
|
|
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
|
|
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`
|
|
31
|
-
`missedNudges` counter. The tool returns
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
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
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
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,
|
|
148
|
-
// (
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
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
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"
|
|
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]
|
|
383
|
+
return "[acknowledged]";
|
|
378
384
|
},
|
|
379
385
|
};
|
|
380
386
|
|