llm-wiki-kit 0.2.7 → 0.2.9
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/README.md +5 -1
- package/docs/integrations/claude-code.md +2 -1
- package/docs/integrations/codex.md +3 -1
- package/docs/manual.md +7 -3
- package/docs/operations.md +3 -1
- package/docs/security.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/package.json +1 -1
- package/src/compact-capture.js +292 -0
- package/src/hook.js +28 -9
- package/src/install.js +43 -10
- package/src/state.js +16 -0
package/README.md
CHANGED
|
@@ -86,10 +86,12 @@ Use Claude Code or Codex normally.
|
|
|
86
86
|
|
|
87
87
|
The installed hooks:
|
|
88
88
|
|
|
89
|
-
- inject functional compact context at session start, instructions loaded, prompt submit
|
|
89
|
+
- inject functional compact context at session start, instructions loaded, and prompt submit. The hook still uses `wiki/memory.md`, `wiki/index.md`, relevant wiki search results, maintenance signals, update status, and any compact recovery packet; it formats only the useful parts so user-visible hook context does not look like a raw debug dump.
|
|
90
90
|
- remove Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces at session start so `llm-wiki/` remains the active wiki implementation
|
|
91
91
|
- record small redacted raw event envelopes and per-turn state
|
|
92
92
|
- capture decision points, debugging findings, changed files, and verification notes
|
|
93
|
+
- before compaction, classify the current turn and save a redacted checkpoint for meaningful or durable work; durable candidates also get a maintenance queue item
|
|
94
|
+
- after compaction, store the redacted compact summary only; if pre-compact preservation failed, prepare a recovery packet for the next legal model-visible context hook
|
|
93
95
|
- allow tool calls to proceed without secret/PII-based hook blocking
|
|
94
96
|
- update `llm-wiki/outputs/questions/YYYY-MM-DD-live-qa.md` only for meaningful work turns
|
|
95
97
|
- avoid automatic `wiki/queries/` and `wiki/decisions/` promotion in the default answer-first mode
|
|
@@ -100,6 +102,7 @@ The installed hooks:
|
|
|
100
102
|
|
|
101
103
|
If you need to think about saving every answer manually, the setup has failed.
|
|
102
104
|
If wiki maintenance delays the actual answer, the setup is being used wrong. The default capture mode is `LLM_WIKI_KIT_CAPTURE_MODE=answer-first`; the old eager query/decision capture path remains only as deprecated compatibility mode via `LLM_WIKI_KIT_CAPTURE_MODE=legacy-eager`.
|
|
105
|
+
Pre-compact preservation always lets compaction proceed. `LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT=off` suppresses failure warnings; `limited` and `soft` both emit a non-blocking warning if checkpoint storage fails. The hook reads only a bounded transcript tail controlled by `LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES`; the raw transcript path and authentication values are redacted before any checkpoint or recovery packet is written.
|
|
103
106
|
|
|
104
107
|
## Operational Commands
|
|
105
108
|
|
|
@@ -171,6 +174,7 @@ llm-wiki hook claude Stop
|
|
|
171
174
|
## Security Defaults
|
|
172
175
|
|
|
173
176
|
- Full raw transcript capture is disabled by default.
|
|
177
|
+
- PreCompact may read a small bounded transcript tail to create a redacted checkpoint, but it does not store the full transcript or raw `transcript_path`.
|
|
174
178
|
- Tool calls are not blocked only because inputs look sensitive.
|
|
175
179
|
- Authentication values such as tokens, passwords, and private keys are redacted before durable summaries are written.
|
|
176
180
|
- Hook payloads are stored only as redacted event envelopes.
|
|
@@ -44,11 +44,12 @@ The hook records redacted turn summaries but does not deny tool calls only becau
|
|
|
44
44
|
|
|
45
45
|
At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, performs a cached npm update notice check for npm installs, then injects functional compact context. The context still uses `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, relevant wiki/search state, operating rules, maintenance signals, passive runtime update status, and managed-template cleanup notes; the hook formats those signals so they are usable if shown in the Claude Code UI. At `UserPromptSubmit`, it recovers stale turn state, searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful functional compact context set. Update notice cache is scoped by npm command, and maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
|
|
46
46
|
|
|
47
|
-
`PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact`
|
|
47
|
+
`PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` classifies the current turn before compaction: simple turns record only a context note, meaningful work writes a live Q&A checkpoint, and durable candidates write both a checkpoint and a maintenance queue item. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event. `PostCompact` stores the redacted compact summary as a context note and prepares any pending recovery packet without returning model-visible context directly. In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` and `SessionEnd` append live Q&A only for meaningful work turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record or document durable knowledge and no durable wiki update is detected, `Stop`/`SessionEnd` queue a pending maintenance item for agent review. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
|
|
48
48
|
|
|
49
49
|
Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic managed-template refresh behavior.
|
|
50
50
|
Set `LLM_WIKI_KIT_UPDATE_NOTICE=0` only while suppressing the cached passive runtime update status.
|
|
51
51
|
Set `LLM_WIKI_KIT_CAPTURE_MODE=legacy-eager` only as deprecated compatibility mode for the old eager query/decision capture behavior.
|
|
52
|
+
Set `LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT=off` to suppress pre-compact failure warnings; `limited` and `soft` both keep compaction moving and emit a non-blocking warning when checkpoint/queue preservation fails. `LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES` controls the bounded tail size.
|
|
52
53
|
|
|
53
54
|
After installation or update, run:
|
|
54
55
|
|
|
@@ -33,7 +33,8 @@ Expected behavior:
|
|
|
33
33
|
- `UserPromptSubmit` recovers stale turn state, searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful functional compact context set. Update notice cache is scoped by npm command, and maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
|
|
34
34
|
- `PreToolUse` records redacted tool summaries without blocking tool calls.
|
|
35
35
|
- `PostToolUse` records redacted tool summaries in a turn buffer.
|
|
36
|
-
- `PreCompact`
|
|
36
|
+
- `PreCompact` classifies the current turn before compaction. Simple turns record only a context note; meaningful work writes a live Q&A checkpoint; durable candidates write both a checkpoint and a maintenance queue item. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event.
|
|
37
|
+
- `PostCompact` stores the redacted compact summary as a context note and prepares any pending compact recovery packet. It does not return `hookSpecificOutput.additionalContext`, because Codex `PostCompact` only supports common output fields.
|
|
37
38
|
- In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` appends live Q&A only for meaningful work turns and does not auto-create `wiki/queries/` or `wiki/decisions/`.
|
|
38
39
|
- If the user explicitly asked to record or document durable knowledge and no durable wiki update is detected, `Stop` queues a pending maintenance item for agent review.
|
|
39
40
|
- `Stop` clears the per-session turn buffer after recording. `SubagentStop` leaves the parent turn buffer available for the final stop event.
|
|
@@ -43,6 +44,7 @@ Hook payloads are stored as small redacted event envelopes rather than full tran
|
|
|
43
44
|
Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic managed-template refresh behavior.
|
|
44
45
|
Set `LLM_WIKI_KIT_UPDATE_NOTICE=0` only while suppressing the cached passive runtime update status.
|
|
45
46
|
Set `LLM_WIKI_KIT_CAPTURE_MODE=legacy-eager` only as deprecated compatibility mode for the old eager query/decision capture behavior.
|
|
47
|
+
Set `LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT=off` to suppress pre-compact failure warnings; `limited` and `soft` both keep compaction moving and emit a non-blocking warning when checkpoint/queue preservation fails. `LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES` controls the bounded tail size.
|
|
46
48
|
|
|
47
49
|
Run these after install:
|
|
48
50
|
|
package/docs/manual.md
CHANGED
|
@@ -62,7 +62,8 @@ llm-wiki/
|
|
|
62
62
|
|
|
63
63
|
설치된 hook은 다음 일을 자동으로 수행한다.
|
|
64
64
|
|
|
65
|
-
- session start,
|
|
65
|
+
- session start, instructions loaded, prompt submit 시점에 functional compact context를 주입한다. `wiki/memory.md`, `wiki/index.md`, 관련 wiki 검색 결과, maintenance signal, update status, compact recovery packet은 계속 사용하되 사용자 화면에 보일 수 있는 hook context는 필요한 정보 중심으로 정제한다.
|
|
66
|
+
- pre compact 시점에는 현재 turn을 분류하고, simple turn은 context note만 남기며, meaningful/durable turn은 redacted live Q&A checkpoint와 필요한 maintenance queue 후보를 남긴다. 저장 실패 시에도 compact는 진행시키고, 중요한 내용만 recovery packet으로 준비한다.
|
|
66
67
|
- prompt/tool/result summary를 redaction한 뒤 turn buffer에 기록한다.
|
|
67
68
|
- 의미 있는 작업 turn만 `outputs/questions/YYYY-MM-DD-live-qa.md`에 live Q&A로 남긴다.
|
|
68
69
|
- 기본 answer-first mode에서는 `wiki/queries/`와 `wiki/decisions/`를 매 turn 자동 생성하지 않는다.
|
|
@@ -384,9 +385,11 @@ Claude Code handled events:
|
|
|
384
385
|
- `Stop`
|
|
385
386
|
- `SessionEnd`
|
|
386
387
|
|
|
387
|
-
Hook payload는 full transcript가 아니라 작은 redacted event envelope다. context output도 field별 redaction을 거친다.
|
|
388
|
+
Hook payload는 full transcript가 아니라 작은 redacted event envelope다. context output도 field별 redaction을 거친다. `PreCompact`는 checkpoint 생성을 위해 작은 bounded transcript tail만 읽을 수 있고, 저장 전 인증값과 raw `transcript_path`를 redaction한다.
|
|
388
389
|
|
|
389
|
-
Context를 반환하는 Codex/Claude hook event는 기능을 제거하지 않는다. memory hot index, navigation index, relevant wiki hits, link expansion, maintenance signal, passive runtime update status
|
|
390
|
+
Context를 반환하는 Codex/Claude hook event는 기능을 제거하지 않는다. memory hot index, navigation index, relevant wiki hits, link expansion, maintenance signal, passive runtime update status, compact recovery packet을 계속 사용할 수 있고, 사용자 화면에 보일 수 있는 `additionalContext`만 functional compact context로 정제한다. `PostCompact`는 compact summary 저장과 recovery packet 준비만 수행하고 model-visible context를 직접 반환하지 않는다. `llm-wiki context` CLI의 full debug 출력은 이 hook formatting 정책과 별도로 유지한다.
|
|
391
|
+
|
|
392
|
+
Pre-compact 보존은 compact를 block하지 않는다. `LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT=off`는 실패 warning을 억제하고, `limited`/`soft`는 checkpoint/queue 저장 실패 시 non-blocking warning만 남긴다. `LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES`로 transcript tail byte 한도를 조정한다.
|
|
390
393
|
|
|
391
394
|
## Security Defaults
|
|
392
395
|
|
|
@@ -396,6 +399,7 @@ Context를 반환하는 Codex/Claude hook event는 기능을 제거하지 않는
|
|
|
396
399
|
- token, password, bearer credential, private key, `.env` 원문 같은 authentication value는 durable summary에 쓰기 전 redaction한다.
|
|
397
400
|
- phone number, email, date, business identifier는 local work context로 유용할 수 있어 기본 보존한다.
|
|
398
401
|
- full raw transcript capture는 기본 기능이 아니다.
|
|
402
|
+
- `PreCompact` checkpoint는 bounded redacted transcript tail만 사용할 수 있으며 full transcript와 raw `transcript_path`는 저장하지 않는다.
|
|
399
403
|
- `llm-wiki lint`는 wiki 안의 secret-like content를 error로 보고한다.
|
|
400
404
|
|
|
401
405
|
민감한 raw command output, log, screenshot, transcript를 저장해야 할 때는 먼저 redaction한다. 인증값은 wiki, report, live Q&A, command history에 남기지 않는다.
|
package/docs/operations.md
CHANGED
|
@@ -143,7 +143,9 @@ After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working
|
|
|
143
143
|
|
|
144
144
|
Daily use should be Claude Code/Codex first. The user should not need to run a chain of `llm-wiki` commands while working. Hooks inject context automatically, but the current user answer takes priority over wiki cleanup. The active agent updates durable wiki pages when reusable project knowledge appears and the turn's importance or user consent justifies persistence. Hook context policy is function-first: memory, search, maintenance, and update signals remain available, while user-visible context is formatted as functional compact context instead of a raw dump.
|
|
145
145
|
|
|
146
|
-
In the default `LLM_WIKI_KIT_CAPTURE_MODE=answer-first` mode, `Stop` and `SessionEnd` append live Q&A only for meaningful work turns. They do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked for recording/documentation and no durable wiki update is detected, a pending cleanup candidate is written to `llm-wiki/outputs/maintenance/queue.md`. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete. `SessionStart` injects a one-item queue summary; `UserPromptSubmit` injects a soft reminder only when the prompt is wiki/maintenance related or matches a queue topic. This is a recovery and reminder layer, not a full transcript capture path.
|
|
146
|
+
In the default `LLM_WIKI_KIT_CAPTURE_MODE=answer-first` mode, `Stop` and `SessionEnd` append live Q&A only for meaningful work turns. They do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked for recording/documentation and no durable wiki update is detected, a pending cleanup candidate is written to `llm-wiki/outputs/maintenance/queue.md`. `PreCompact` performs the same answer-first classification before context compaction: simple turns get only a context note, archive-worthy turns get a live Q&A checkpoint, and durable candidates get a checkpoint plus queue item. If checkpoint storage fails, compaction still proceeds and the hook prepares an important-only compact recovery packet for the next legal context-injection event. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete. `SessionStart` injects a one-item queue summary; `UserPromptSubmit` injects a soft reminder only when the prompt is wiki/maintenance related or matches a queue topic. This is a recovery and reminder layer, not a full transcript capture path.
|
|
147
|
+
|
|
148
|
+
Pre-compact preservation defaults to `LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT=limited`, but compaction is never blocked by llm-wiki-kit. `limited` and `soft` emit non-blocking failure warnings, and `off` suppresses failure output. `LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES` controls the small bounded transcript tail used for checkpoint context. Authentication values and the raw transcript path are redacted before storage.
|
|
147
149
|
|
|
148
150
|
`LLM_WIKI_KIT_CAPTURE_MODE=legacy-eager` keeps the old eager live Q&A/query/decision/maintenance behavior for compatibility only. New projects should rely on the answer-first default.
|
|
149
151
|
|
package/docs/security.md
CHANGED
|
@@ -13,4 +13,4 @@ Before writing durable summaries, the runtime redacts authentication values such
|
|
|
13
13
|
|
|
14
14
|
Manual and hook context output also runs through redaction before returning excerpts or search hits. `llm-wiki lint` reports remaining secret-like wiki content as an error so it can be removed or rewritten before it becomes reusable project memory.
|
|
15
15
|
|
|
16
|
-
Hook payloads are stored as small event envelopes, not full raw transcripts. Full transcript capture is intentionally not implemented as a default. If a project needs
|
|
16
|
+
Hook payloads are stored as small event envelopes, not full raw transcripts. Full transcript capture is intentionally not implemented as a default. `PreCompact` may read a small bounded transcript tail for a redacted checkpoint, but it does not store the raw transcript path or full transcript. If a project needs raw transcript capture, add a project-local policy and a redaction path first.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -242,7 +242,7 @@ If the queue is always empty during ordinary Q&A, that is normal. If you expecte
|
|
|
242
242
|
|
|
243
243
|
The hook does not block tool calls only because inputs look sensitive. Durable summaries redact authentication values before writing, while ordinary work context such as dates, phone numbers, emails, and business identifiers is preserved by default.
|
|
244
244
|
|
|
245
|
-
Hook payloads are stored as small redacted event envelopes rather than full transcripts. Manual and hook context output is redacted before wiki excerpts or search hits are returned.
|
|
245
|
+
Hook payloads are stored as small redacted event envelopes rather than full transcripts. `PreCompact` checkpoints may include a bounded redacted transcript tail, but the full transcript and raw `transcript_path` are not stored. Manual and hook context output is redacted before wiki excerpts or search hits are returned.
|
|
246
246
|
|
|
247
247
|
## Duplicate Pages Appear
|
|
248
248
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { open, stat } from 'fs/promises';
|
|
2
|
+
import { relative } from 'path';
|
|
3
|
+
import { classifyTurn } from './capture-policy.js';
|
|
4
|
+
import { recordMaintenanceForEntry } from './maintenance.js';
|
|
5
|
+
import { appendContextNote, appendLiveQa, appendWikiLog } from './project.js';
|
|
6
|
+
import { redactText, summarizeForStorage } from './redaction.js';
|
|
7
|
+
import { buildEntryFromState, clearCompactRecovery, readCompactRecovery, writeCompactRecovery } from './state.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PRECOMPACT_TAIL_BYTES = 32 * 1024;
|
|
10
|
+
const MAX_PRECOMPACT_TAIL_BYTES = 256 * 1024;
|
|
11
|
+
const ENFORCEMENT_MODES = new Set(['limited', 'soft', 'off']);
|
|
12
|
+
|
|
13
|
+
export function preCompactEnforcementMode(env = process.env) {
|
|
14
|
+
const value = String(env.LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT || 'limited').toLowerCase();
|
|
15
|
+
return ENFORCEMENT_MODES.has(value) ? value : 'limited';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function preCompactTranscriptTailBytes(env = process.env) {
|
|
19
|
+
const raw = env.LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES;
|
|
20
|
+
if (raw === undefined || raw === '') return DEFAULT_PRECOMPACT_TAIL_BYTES;
|
|
21
|
+
const parsed = Number(raw);
|
|
22
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_PRECOMPACT_TAIL_BYTES;
|
|
23
|
+
return Math.min(Math.floor(parsed), MAX_PRECOMPACT_TAIL_BYTES);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function compactSummaryText(payload) {
|
|
27
|
+
return summarizeForStorage(
|
|
28
|
+
payload.compact_summary ||
|
|
29
|
+
payload.compactSummary ||
|
|
30
|
+
payload.summary ||
|
|
31
|
+
payload.message ||
|
|
32
|
+
'',
|
|
33
|
+
4000
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transcriptPath(payload) {
|
|
38
|
+
return payload.transcript_path || payload.transcriptPath || '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function redactTranscriptPath(text, path) {
|
|
42
|
+
let output = String(text || '');
|
|
43
|
+
if (path) output = output.split(path).join('[REDACTED:transcript-path]');
|
|
44
|
+
output = output.replace(/("transcript_path"\s*:\s*)"[^"]*"/gi, '$1"[REDACTED:transcript-path]"');
|
|
45
|
+
output = output.replace(/('transcript_path'\s*:\s*)'[^']*'/gi, "$1'[REDACTED:transcript-path]'");
|
|
46
|
+
output = output.replace(/\btranscript_path\s*=\s*[^\s"'`]+/gi, 'transcript_path=[REDACTED:transcript-path]');
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readTranscriptTail(path, maxBytes) {
|
|
51
|
+
if (!path) return { ok: false, reason: 'missing-transcript-path', text: '' };
|
|
52
|
+
if (maxBytes <= 0) return { ok: false, reason: 'transcript-tail-disabled', text: '' };
|
|
53
|
+
|
|
54
|
+
let handle = null;
|
|
55
|
+
try {
|
|
56
|
+
const info = await stat(path);
|
|
57
|
+
if (!info.isFile()) return { ok: false, reason: 'transcript-not-file', text: '' };
|
|
58
|
+
const length = Math.min(info.size, maxBytes);
|
|
59
|
+
const start = Math.max(0, info.size - length);
|
|
60
|
+
const buffer = Buffer.alloc(length);
|
|
61
|
+
handle = await open(path, 'r');
|
|
62
|
+
await handle.read(buffer, 0, length, start);
|
|
63
|
+
const text = summarizeForStorage(redactTranscriptPath(buffer.toString('utf8'), path), 4000);
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
text,
|
|
67
|
+
bytesRead: length,
|
|
68
|
+
truncated: info.size > maxBytes,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
reason: summarizeForStorage(error?.code || error?.message || 'read-failed', 200),
|
|
74
|
+
text: '',
|
|
75
|
+
};
|
|
76
|
+
} finally {
|
|
77
|
+
await handle?.close().catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendToField(current, addition) {
|
|
82
|
+
const existing = String(current || '').trim();
|
|
83
|
+
if (!existing || existing === '(not captured)') return addition;
|
|
84
|
+
return `${existing}\n${addition}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function entryWithTranscriptTail(entry, tail) {
|
|
88
|
+
const next = { ...entry };
|
|
89
|
+
if (tail.ok && tail.text) {
|
|
90
|
+
const marker = tail.truncated
|
|
91
|
+
? 'PreCompact transcript tail (redacted bounded excerpt; earlier transcript omitted):'
|
|
92
|
+
: 'PreCompact transcript tail (redacted bounded excerpt):';
|
|
93
|
+
next.result = appendToField(next.result, `${marker}\n${tail.text}`);
|
|
94
|
+
next.followUp = appendToField(
|
|
95
|
+
next.followUp,
|
|
96
|
+
'Compact checkpoint includes only a bounded redacted transcript tail, not the raw transcript path or full transcript.'
|
|
97
|
+
);
|
|
98
|
+
} else if (tail.reason && tail.reason !== 'missing-transcript-path') {
|
|
99
|
+
next.followUp = appendToField(
|
|
100
|
+
next.followUp,
|
|
101
|
+
`Transcript tail unavailable during PreCompact (${tail.reason}); checkpoint used turn state only.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return next;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function captured(value) {
|
|
108
|
+
const text = String(value || '').trim();
|
|
109
|
+
return text && text !== '(not captured)' ? text : '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sanitizeFailureText(projectRoot, value) {
|
|
113
|
+
let text = String(value || '');
|
|
114
|
+
if (projectRoot) text = text.split(projectRoot).join('[REDACTED:project-root]');
|
|
115
|
+
return summarizeForStorage(text, 1200);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizeFailures(projectRoot, failures) {
|
|
119
|
+
return failures.map((failure) => sanitizeFailureText(projectRoot, failure));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatRecoveryContext(recovery) {
|
|
123
|
+
const entry = recovery?.entry || {};
|
|
124
|
+
const lines = [
|
|
125
|
+
'LLM Wiki compact recovery:',
|
|
126
|
+
'- PreCompact preservation could not finish before compaction. Use this fallback context only for important lost details.',
|
|
127
|
+
`- classification: ${recovery?.classification || 'unknown'}`,
|
|
128
|
+
];
|
|
129
|
+
if (recovery?.postCompactSummary) {
|
|
130
|
+
lines.push(`- compact summary: ${summarizeForStorage(recovery.postCompactSummary, 600)}`);
|
|
131
|
+
}
|
|
132
|
+
if (recovery?.failures) {
|
|
133
|
+
lines.push(`- preservation failure: ${summarizeForStorage(recovery.failures, 600)}`);
|
|
134
|
+
}
|
|
135
|
+
const fields = [
|
|
136
|
+
['question', 'question', 900],
|
|
137
|
+
['work', 'work', 1200],
|
|
138
|
+
['result', 'result', 1400],
|
|
139
|
+
['changedFiles', 'changed files', 700],
|
|
140
|
+
['verification', 'verification', 700],
|
|
141
|
+
['followUp', 'follow-up', 700],
|
|
142
|
+
];
|
|
143
|
+
for (const [key, label, limit] of fields) {
|
|
144
|
+
const value = captured(entry[key]);
|
|
145
|
+
if (value) lines.push(`\n${label}:\n${summarizeForStorage(value, limit)}`);
|
|
146
|
+
}
|
|
147
|
+
return summarizeForStorage(lines.join('\n'), 5000);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function recordCompactRecoveryFailure(projectRoot, payload, entry, classification, failures) {
|
|
151
|
+
const recovery = {
|
|
152
|
+
created_at: new Date().toISOString(),
|
|
153
|
+
updated_at: new Date().toISOString(),
|
|
154
|
+
classification: classification.kind,
|
|
155
|
+
failures: summarizeForStorage(failures.join('; '), 1200),
|
|
156
|
+
entry: {
|
|
157
|
+
topic: summarizeForStorage(entry.topic, 300),
|
|
158
|
+
question: summarizeForStorage(entry.question, 1000),
|
|
159
|
+
work: summarizeForStorage(entry.work, 1600),
|
|
160
|
+
result: summarizeForStorage(entry.result, 1800),
|
|
161
|
+
changedFiles: summarizeForStorage(entry.changedFiles, 900),
|
|
162
|
+
verification: summarizeForStorage(entry.verification, 900),
|
|
163
|
+
followUp: summarizeForStorage(entry.followUp, 900),
|
|
164
|
+
firstTimestamp: entry.firstTimestamp,
|
|
165
|
+
session: entry.session,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
recovery.context = formatRecoveryContext(recovery);
|
|
169
|
+
await writeCompactRecovery(projectRoot, payload, recovery);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function failureOutput(message, mode) {
|
|
173
|
+
if (mode === 'off') return {};
|
|
174
|
+
const reason = summarizeForStorage(message, 1200);
|
|
175
|
+
return {
|
|
176
|
+
systemMessage: `LLM Wiki PreCompact preservation warning: ${reason}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function handlePreCompactCapture(projectRoot, provider, eventName, payload) {
|
|
181
|
+
const mode = preCompactEnforcementMode();
|
|
182
|
+
const failures = [];
|
|
183
|
+
const tail = await readTranscriptTail(transcriptPath(payload), preCompactTranscriptTailBytes());
|
|
184
|
+
const stateEntry = await buildEntryFromState(projectRoot, payload, payload.last_assistant_message || payload.response || '');
|
|
185
|
+
const entry = entryWithTranscriptTail(stateEntry, tail);
|
|
186
|
+
const classification = classifyTurn(entry, eventName);
|
|
187
|
+
|
|
188
|
+
await appendContextNote(
|
|
189
|
+
projectRoot,
|
|
190
|
+
eventName,
|
|
191
|
+
[
|
|
192
|
+
`PreCompact classification: ${classification.kind}.`,
|
|
193
|
+
tail.ok ? `Transcript tail bytes read: ${tail.bytesRead}.` : `Transcript tail: ${tail.reason || 'not available'}.`,
|
|
194
|
+
'Turn state is intentionally kept for the later Stop/SessionEnd hook.',
|
|
195
|
+
].join('\n')
|
|
196
|
+
).catch((error) => {
|
|
197
|
+
failures.push(`context note failed: ${error?.message || error}`);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let liveQaPath = null;
|
|
201
|
+
if (classification.archive) {
|
|
202
|
+
const checkpointEntry = {
|
|
203
|
+
...entry,
|
|
204
|
+
followUp: appendToField(
|
|
205
|
+
entry.followUp,
|
|
206
|
+
classification.suggestDurable
|
|
207
|
+
? 'PreCompact saved this checkpoint and queued durable wiki review; merge reusable facts into existing wiki pages after the current answer.'
|
|
208
|
+
: 'PreCompact saved this checkpoint before context compaction; reusable facts should be merged only when appropriate.'
|
|
209
|
+
),
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
liveQaPath = await appendLiveQa(projectRoot, checkpointEntry);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
failures.push(`live Q&A checkpoint failed: ${error?.message || error}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (liveQaPath && ['suggest-durable', 'explicit-durable'].includes(classification.kind)) {
|
|
219
|
+
try {
|
|
220
|
+
const result = await recordMaintenanceForEntry(projectRoot, entry, {
|
|
221
|
+
source: liveQaPath,
|
|
222
|
+
eventName,
|
|
223
|
+
reason: 'PreCompact durable candidate checkpoint needs review after context compaction.',
|
|
224
|
+
});
|
|
225
|
+
if (result.created === false && result.reason) {
|
|
226
|
+
failures.push(`maintenance queue failed: ${result.reason}`);
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
failures.push(`maintenance queue failed: ${error?.message || error}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (liveQaPath) {
|
|
234
|
+
await appendWikiLog(
|
|
235
|
+
projectRoot,
|
|
236
|
+
`precompact checkpoint; archive=${relative(projectRoot, liveQaPath)}; classification=${classification.kind}`
|
|
237
|
+
).catch((error) => {
|
|
238
|
+
failures.push(`wiki log failed: ${error?.message || error}`);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (failures.length > 0) {
|
|
243
|
+
let safeFailures = sanitizeFailures(projectRoot, failures);
|
|
244
|
+
if (classification.archive) {
|
|
245
|
+
try {
|
|
246
|
+
await recordCompactRecoveryFailure(projectRoot, payload, entry, classification, safeFailures);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
failures.push(`compact recovery marker failed: ${error?.message || error}`);
|
|
249
|
+
safeFailures = sanitizeFailures(projectRoot, failures);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return failureOutput(`LLM Wiki PreCompact preservation did not complete. ${safeFailures.join('; ')}`, mode);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function recordPostCompactSummary(projectRoot, eventName, payload) {
|
|
259
|
+
const summary = compactSummaryText(payload);
|
|
260
|
+
await appendContextNote(
|
|
261
|
+
projectRoot,
|
|
262
|
+
eventName,
|
|
263
|
+
summary ? `Compact summary:\n${redactText(summary, 4000)}` : 'Compact summary was not provided.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function finalizeCompactRecovery(projectRoot, payload) {
|
|
268
|
+
const recovery = await readCompactRecovery(projectRoot, payload);
|
|
269
|
+
if (!recovery) return null;
|
|
270
|
+
const summary = compactSummaryText(payload);
|
|
271
|
+
const next = {
|
|
272
|
+
...recovery,
|
|
273
|
+
updated_at: new Date().toISOString(),
|
|
274
|
+
postCompactSummary: summary || recovery.postCompactSummary || '',
|
|
275
|
+
ready: true,
|
|
276
|
+
};
|
|
277
|
+
next.context = formatRecoveryContext(next);
|
|
278
|
+
await writeCompactRecovery(projectRoot, payload, next);
|
|
279
|
+
await appendContextNote(
|
|
280
|
+
projectRoot,
|
|
281
|
+
'PostCompact',
|
|
282
|
+
'Prepared compact recovery packet for the next legal model-visible hook context.'
|
|
283
|
+
).catch(() => {});
|
|
284
|
+
return next;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function consumeCompactRecoveryContext(projectRoot, payload) {
|
|
288
|
+
const recovery = await readCompactRecovery(projectRoot, payload);
|
|
289
|
+
if (!recovery) return '';
|
|
290
|
+
await clearCompactRecovery(projectRoot, payload);
|
|
291
|
+
return recovery.context || formatRecoveryContext(recovery);
|
|
292
|
+
}
|
package/src/hook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findProjectRoot } from './fs-utils.js';
|
|
2
2
|
import { classifyTurn, formatDurableCaptureGuidance, hasDetectedDurableWikiChange, isLegacyEagerCaptureMode } from './capture-policy.js';
|
|
3
|
-
import { bootstrapProject,
|
|
3
|
+
import { bootstrapProject, appendLiveQa, appendSessionEnvelope, appendWikiLog, buildContextBrief, writeDecisionPage, writeQueryPage } from './project.js';
|
|
4
|
+
import { consumeCompactRecoveryContext, finalizeCompactRecovery, handlePreCompactCapture, recordPostCompactSummary } from './compact-capture.js';
|
|
4
5
|
import { recoverStaleTurnStates, recordMaintenanceForEntry } from './maintenance.js';
|
|
5
6
|
import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
|
|
6
7
|
import { recordProject } from './projects.js';
|
|
@@ -34,8 +35,16 @@ function toolSummary(payload) {
|
|
|
34
35
|
return `${toolName}: ${summarizeForStorage(input, 1200)}`;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function
|
|
38
|
+
function supportsAdditionalContext(provider, eventName) {
|
|
39
|
+
const normalized = String(provider || '').toLowerCase();
|
|
40
|
+
if (normalized === 'codex') return ['SessionStart', 'UserPromptSubmit'].includes(eventName);
|
|
41
|
+
if (normalized === 'claude') return ['SessionStart', 'InstructionsLoaded', 'UserPromptSubmit'].includes(eventName);
|
|
42
|
+
return ['SessionStart', 'InstructionsLoaded', 'UserPromptSubmit'].includes(eventName);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function contextOutput(provider, eventName, context) {
|
|
38
46
|
if (!context) return {};
|
|
47
|
+
if (!supportsAdditionalContext(provider, eventName)) return {};
|
|
39
48
|
return {
|
|
40
49
|
hookSpecificOutput: {
|
|
41
50
|
hookEventName: eventName,
|
|
@@ -128,21 +137,30 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded') {
|
|
131
|
-
const
|
|
132
|
-
|
|
140
|
+
const recovery = supportsAdditionalContext(provider, eventName)
|
|
141
|
+
? await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '')
|
|
142
|
+
: '';
|
|
143
|
+
const context = await hookContext(
|
|
144
|
+
projectRoot,
|
|
145
|
+
eventName,
|
|
146
|
+
[recovery, await buildContextBrief(projectRoot, 'SessionStart')].filter(Boolean).join('\n\n'),
|
|
147
|
+
payload
|
|
148
|
+
);
|
|
149
|
+
return contextOutput(provider, eventName, context);
|
|
133
150
|
}
|
|
134
151
|
|
|
135
152
|
if (eventName === 'UserPromptSubmit') {
|
|
136
153
|
const prompt = promptText(payload);
|
|
137
154
|
await rememberQuestion(projectRoot, payload, prompt);
|
|
138
155
|
const guidance = formatDurableCaptureGuidance(prompt);
|
|
156
|
+
const recovery = await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '');
|
|
139
157
|
const context = await hookContext(
|
|
140
158
|
projectRoot,
|
|
141
159
|
eventName,
|
|
142
|
-
[await buildContextBrief(projectRoot, eventName, prompt), guidance].filter(Boolean).join('\n\n'),
|
|
160
|
+
[recovery, await buildContextBrief(projectRoot, eventName, prompt), guidance].filter(Boolean).join('\n\n'),
|
|
143
161
|
payload
|
|
144
162
|
);
|
|
145
|
-
return contextOutput(eventName, context);
|
|
163
|
+
return contextOutput(provider, eventName, context);
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
if (eventName === 'PreToolUse') {
|
|
@@ -156,11 +174,12 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
if (eventName === 'PreCompact' || eventName === 'PostCompact') {
|
|
159
|
-
await appendContextNote(projectRoot, eventName, 'Compaction lifecycle event captured by llm-wiki-kit.');
|
|
160
177
|
if (eventName === 'PostCompact') {
|
|
161
|
-
|
|
178
|
+
await recordPostCompactSummary(projectRoot, eventName, payload);
|
|
179
|
+
await finalizeCompactRecovery(projectRoot, payload).catch(() => {});
|
|
180
|
+
return {};
|
|
162
181
|
}
|
|
163
|
-
return
|
|
182
|
+
return handlePreCompactCapture(projectRoot, provider, eventName, payload);
|
|
164
183
|
}
|
|
165
184
|
|
|
166
185
|
if (eventName === 'Stop' || eventName === 'SubagentStop' || eventName === 'SessionEnd') {
|
package/src/install.js
CHANGED
|
@@ -19,6 +19,8 @@ import { bootstrapProject } from './project.js';
|
|
|
19
19
|
import { recordProject } from './projects.js';
|
|
20
20
|
import { binPath, detectInstallSource, packageRoot, runtimeVersion } from './version.js';
|
|
21
21
|
|
|
22
|
+
const SESSION_START_MATCHER = 'startup|resume|clear|compact';
|
|
23
|
+
|
|
22
24
|
export function hookCommand(provider, eventName, options = {}) {
|
|
23
25
|
return commandForNodeScript(binPath, ['hook', provider, eventName], options);
|
|
24
26
|
}
|
|
@@ -125,11 +127,23 @@ async function reconcileWindowsCommand() {
|
|
|
125
127
|
|
|
126
128
|
function addHook(hooks, eventName, command, options = {}) {
|
|
127
129
|
hooks[eventName] = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
|
|
128
|
-
const
|
|
130
|
+
const existing = hooks[eventName].find((entry) => (
|
|
129
131
|
Array.isArray(entry?.hooks) &&
|
|
130
132
|
entry.hooks.some((hook) => hook?.type === 'command' && hook?.command === command)
|
|
131
133
|
));
|
|
132
|
-
if (
|
|
134
|
+
if (existing) {
|
|
135
|
+
let changed = false;
|
|
136
|
+
if (options.matcher && existing.matcher !== options.matcher) {
|
|
137
|
+
existing.matcher = options.matcher;
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
const hook = existing.hooks.find((item) => item?.type === 'command' && item?.command === command);
|
|
141
|
+
if (hook && options.commandWindows && hook.commandWindows !== options.commandWindows) {
|
|
142
|
+
hook.commandWindows = options.commandWindows;
|
|
143
|
+
changed = true;
|
|
144
|
+
}
|
|
145
|
+
return changed;
|
|
146
|
+
}
|
|
133
147
|
const entry = {
|
|
134
148
|
hooks: [
|
|
135
149
|
{
|
|
@@ -186,12 +200,21 @@ function unsupportedKitClaudeEvents(hooks, supportedEvents) {
|
|
|
186
200
|
.sort();
|
|
187
201
|
}
|
|
188
202
|
|
|
189
|
-
function
|
|
190
|
-
return
|
|
203
|
+
function expectedMatcher(eventName) {
|
|
204
|
+
return eventName === 'SessionStart' ? SESSION_START_MATCHER : undefined;
|
|
191
205
|
}
|
|
192
206
|
|
|
193
|
-
function
|
|
194
|
-
return
|
|
207
|
+
function hookCommandMatches(hook, command) {
|
|
208
|
+
return hook?.command === command || hook?.commandWindows === command;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function hookEventIsCurrent(entries, eventName, command) {
|
|
212
|
+
const matcher = expectedMatcher(eventName);
|
|
213
|
+
return Array.isArray(entries) && entries.some((entry) => (
|
|
214
|
+
(!matcher || entry.matcher === matcher) &&
|
|
215
|
+
Array.isArray(entry?.hooks) &&
|
|
216
|
+
entry.hooks.some((hook) => hook?.type === 'command' && hookCommandMatches(hook, command))
|
|
217
|
+
));
|
|
195
218
|
}
|
|
196
219
|
|
|
197
220
|
export async function install(options = {}) {
|
|
@@ -221,7 +244,7 @@ export async function install(options = {}) {
|
|
|
221
244
|
changed.push('codex:replaced');
|
|
222
245
|
}
|
|
223
246
|
for (const eventName of CODEX_EVENTS) {
|
|
224
|
-
const matcher = eventName
|
|
247
|
+
const matcher = expectedMatcher(eventName);
|
|
225
248
|
const command = hookCommand('codex', eventName, { platform });
|
|
226
249
|
const commandWindows = isWindows({ platform }) ? hookCommand('codex', eventName, { platform: 'win32' }) : undefined;
|
|
227
250
|
if (addHook(codex.hooks, eventName, command, { matcher, commandWindows })) {
|
|
@@ -251,7 +274,7 @@ export async function install(options = {}) {
|
|
|
251
274
|
changed.push(...removedUnsupported.map((eventName) => `claude:removed-unsupported:${eventName}`));
|
|
252
275
|
}
|
|
253
276
|
for (const eventName of claudeEvents) {
|
|
254
|
-
const matcher = eventName
|
|
277
|
+
const matcher = expectedMatcher(eventName);
|
|
255
278
|
if (addHook(claude.hooks, eventName, hookCommand('claude', eventName, { platform }), { matcher })) {
|
|
256
279
|
claudeChanged = true;
|
|
257
280
|
changed.push(`claude:${eventName}`);
|
|
@@ -312,9 +335,18 @@ export async function status(options = {}) {
|
|
|
312
335
|
const claude = await readJson(claudeSettingsPath, {});
|
|
313
336
|
const claudeDetection = detectClaudeVersion();
|
|
314
337
|
const claudeEvents = supportedClaudeEvents(claudeDetection);
|
|
315
|
-
const
|
|
338
|
+
const codexMissingEvents = CODEX_EVENTS.filter((eventName) => !hookEventIsCurrent(
|
|
339
|
+
codex.hooks?.[eventName] || [],
|
|
340
|
+
eventName,
|
|
341
|
+
hookCommand('codex', eventName, { platform })
|
|
342
|
+
));
|
|
343
|
+
const claudeMissingEvents = claudeEvents.filter((eventName) => !hookEventIsCurrent(
|
|
344
|
+
claude.hooks?.[eventName] || [],
|
|
345
|
+
eventName,
|
|
346
|
+
hookCommand('claude', eventName, { platform })
|
|
347
|
+
));
|
|
316
348
|
const claudeUnsupportedKitEvents = unsupportedKitClaudeEvents(claude.hooks || {}, claudeEvents);
|
|
317
|
-
const codexInstalled =
|
|
349
|
+
const codexInstalled = codexMissingEvents.length === 0;
|
|
318
350
|
const claudeInstalled = claudeMissingEvents.length === 0 && claudeUnsupportedKitEvents.length === 0;
|
|
319
351
|
const discoveredCommandPaths = await findCommandPaths('llm-wiki', { platform });
|
|
320
352
|
const commandPath = discoveredCommandPaths[0] || null;
|
|
@@ -344,6 +376,7 @@ export async function status(options = {}) {
|
|
|
344
376
|
claudeVersion: claudeDetection.version || 'unknown',
|
|
345
377
|
claudeModernHooks: claudeDetection.modern,
|
|
346
378
|
claudeSupportedEvents: claudeEvents,
|
|
379
|
+
codexMissingEvents,
|
|
347
380
|
claudeMissingEvents,
|
|
348
381
|
claudeUnsupportedKitEvents,
|
|
349
382
|
hooksCurrent: codexInstalled && claudeInstalled,
|
package/src/state.js
CHANGED
|
@@ -38,6 +38,22 @@ export async function clearTurnState(projectRoot, payload) {
|
|
|
38
38
|
await unlink(statePath(projectRoot, payload)).catch(() => {});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function compactRecoveryPath(projectRoot, payload) {
|
|
42
|
+
return join(kitDataDir(), 'compact-recovery', `${sessionKey(projectRoot, payload)}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function readCompactRecovery(projectRoot, payload) {
|
|
46
|
+
return readJson(compactRecoveryPath(projectRoot, payload), null);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeCompactRecovery(projectRoot, payload, recovery) {
|
|
50
|
+
await writeJson(compactRecoveryPath(projectRoot, payload), recovery);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function clearCompactRecovery(projectRoot, payload) {
|
|
54
|
+
await unlink(compactRecoveryPath(projectRoot, payload)).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
export async function rememberQuestion(projectRoot, payload, prompt) {
|
|
42
58
|
const state = await readTurnState(projectRoot, payload);
|
|
43
59
|
const clean = summarizeForStorage(prompt, 3000);
|