opencode-clm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +35 -0
- package/NOTICE +41 -0
- package/README.md +294 -0
- package/index.ts +332 -0
- package/package.json +28 -0
- package/skills/clm-context/SKILL.md +166 -0
- package/src/budget.ts +304 -0
- package/src/clm.ts +725 -0
- package/src/compact.ts +73 -0
- package/src/context-document.ts +659 -0
- package/src/continuity.ts +722 -0
- package/src/mirror-guard.ts +95 -0
- package/src/mirror-store.ts +154 -0
- package/src/observation.ts +155 -0
- package/src/opencode.ts +339 -0
- package/src/overflow.ts +212 -0
- package/src/policy.ts +60 -0
- package/src/presentation.ts +127 -0
- package/src/projection.ts +146 -0
- package/src/settings.ts +189 -0
- package/src/state.ts +241 -0
- package/src/steering.ts +54 -0
- package/src/types.ts +102 -0
- package/steering/house-brief.md +43 -0
package/src/state.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Adapted from pi-clm src/state.ts (MIT, Copyright 2026 Emanuel Casco).
|
|
2
|
+
//
|
|
3
|
+
// Pi appends the state as custom session entries and reconstructs the latest one per
|
|
4
|
+
// branch. OpenCode gives plugins no session store and has no branches, so the whole state
|
|
5
|
+
// lives in one `state.json` in the session directory, replaced atomically on each save.
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { open, readFile, rename, rm } from "node:fs/promises";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import type { ProjectionCheckpoint } from "./projection.ts";
|
|
11
|
+
import type { ContextEditTrace, LiveContextMessage } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export const STATE_FILE = "state.json";
|
|
14
|
+
export const STATE_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
export type OutcomeKind = "applied" | "rejected" | "reset" | "compacted";
|
|
17
|
+
|
|
18
|
+
export interface LiveContextOutcome {
|
|
19
|
+
kind: OutcomeKind;
|
|
20
|
+
message: string;
|
|
21
|
+
beforeEstimate?: number;
|
|
22
|
+
afterEstimate?: number;
|
|
23
|
+
estimateUnit?: "characters" | "tokens";
|
|
24
|
+
at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LiveContextState {
|
|
28
|
+
version: 1;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
/** Increases on every accepted edit and every reset. */
|
|
31
|
+
revision: number;
|
|
32
|
+
checkpoint?: ProjectionCheckpoint;
|
|
33
|
+
lastOutcome?: LiveContextOutcome;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoadedState {
|
|
37
|
+
state: LiveContextState;
|
|
38
|
+
/** Set when a state file existed but could not be used; the state is then initial. */
|
|
39
|
+
warning?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SOURCE_KINDS = new Set(["kept", "edited", "removed", "restored", "normalized"]);
|
|
43
|
+
const OUTCOME_KINDS = new Set(["applied", "rejected", "reset", "compacted"]);
|
|
44
|
+
|
|
45
|
+
export function initialLiveContextState(): LiveContextState {
|
|
46
|
+
return { version: 1, enabled: true, revision: 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function statePath(directory: string): string {
|
|
50
|
+
return join(directory, STATE_FILE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isCount(value: unknown): value is number {
|
|
58
|
+
return Number.isInteger(value) && (value as number) >= 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isEstimateUnit(value: unknown): boolean {
|
|
62
|
+
return value === "characters" || value === "tokens";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Content the unflattener can turn into OpenCode parts: absent, a string, or typed parts. */
|
|
66
|
+
function isMessageContent(value: unknown): boolean {
|
|
67
|
+
if (value === undefined || typeof value === "string") return true;
|
|
68
|
+
return Array.isArray(value) && value.every((part) => isObject(part) && typeof part.type === "string");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isMessageArray(value: unknown): value is LiveContextMessage[] {
|
|
72
|
+
return (
|
|
73
|
+
Array.isArray(value) &&
|
|
74
|
+
value.every((message) => isObject(message) && typeof message.role === "string" && isMessageContent(message.content))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isContextEditTrace(value: unknown): value is ContextEditTrace {
|
|
79
|
+
if (!isObject(value)) return false;
|
|
80
|
+
const trace = value as Partial<ContextEditTrace>;
|
|
81
|
+
if (
|
|
82
|
+
trace.version !== 1 ||
|
|
83
|
+
!isCount(trace.sourceRevision) ||
|
|
84
|
+
!isCount(trace.sourceMessageCount) ||
|
|
85
|
+
!isCount(trace.outputMessageCount) ||
|
|
86
|
+
!Array.isArray(trace.sources) ||
|
|
87
|
+
!Array.isArray(trace.additions)
|
|
88
|
+
) return false;
|
|
89
|
+
const sourceCount = trace.sourceMessageCount;
|
|
90
|
+
const outputCount = trace.outputMessageCount;
|
|
91
|
+
const isOutputIndex = (index: unknown) => isCount(index) && index < outputCount;
|
|
92
|
+
return (
|
|
93
|
+
trace.sources.every((item) =>
|
|
94
|
+
isObject(item) &&
|
|
95
|
+
isCount(item.sourceIndex) &&
|
|
96
|
+
item.sourceIndex < sourceCount &&
|
|
97
|
+
SOURCE_KINDS.has(item.kind) &&
|
|
98
|
+
(item.outputIndex === undefined || isOutputIndex(item.outputIndex)),
|
|
99
|
+
) &&
|
|
100
|
+
trace.additions.every((item) => isObject(item) && item.kind === "added" && isOutputIndex(item.outputIndex))
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isProjectionCheckpoint(value: unknown): value is ProjectionCheckpoint {
|
|
105
|
+
if (!isObject(value)) return false;
|
|
106
|
+
const checkpoint = value as Partial<ProjectionCheckpoint>;
|
|
107
|
+
return (
|
|
108
|
+
checkpoint.version === 1 &&
|
|
109
|
+
isCount(checkpoint.revision) &&
|
|
110
|
+
isCount(checkpoint.sourceMessageCount) &&
|
|
111
|
+
Array.isArray(checkpoint.sourceIds) &&
|
|
112
|
+
checkpoint.sourceIds.length === checkpoint.sourceMessageCount &&
|
|
113
|
+
checkpoint.sourceIds.every((id) => typeof id === "string") &&
|
|
114
|
+
typeof checkpoint.sourceDigest === "string" &&
|
|
115
|
+
/^[a-f0-9]{64}$/.test(checkpoint.sourceDigest) &&
|
|
116
|
+
isMessageArray(checkpoint.projectedMessages) &&
|
|
117
|
+
Number.isFinite(checkpoint.beforeEstimate) &&
|
|
118
|
+
Number.isFinite(checkpoint.afterEstimate) &&
|
|
119
|
+
isEstimateUnit(checkpoint.estimateUnit) &&
|
|
120
|
+
typeof checkpoint.createdAt === "string" &&
|
|
121
|
+
(checkpoint.editTrace === undefined ||
|
|
122
|
+
(isContextEditTrace(checkpoint.editTrace) &&
|
|
123
|
+
checkpoint.editTrace.sourceMessageCount === checkpoint.sourceMessageCount &&
|
|
124
|
+
checkpoint.editTrace.outputMessageCount === checkpoint.projectedMessages.length))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isOutcome(value: unknown): value is LiveContextOutcome {
|
|
129
|
+
if (!isObject(value)) return false;
|
|
130
|
+
return (
|
|
131
|
+
OUTCOME_KINDS.has(value.kind as string) &&
|
|
132
|
+
typeof value.message === "string" &&
|
|
133
|
+
typeof value.at === "string" &&
|
|
134
|
+
(value.beforeEstimate === undefined || Number.isFinite(value.beforeEstimate)) &&
|
|
135
|
+
(value.afterEstimate === undefined || Number.isFinite(value.afterEstimate)) &&
|
|
136
|
+
(value.estimateUnit === undefined || isEstimateUnit(value.estimateUnit))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function isLiveContextState(value: unknown): value is LiveContextState {
|
|
141
|
+
if (!isObject(value)) return false;
|
|
142
|
+
const state = value as Partial<LiveContextState>;
|
|
143
|
+
return (
|
|
144
|
+
state.version === STATE_VERSION &&
|
|
145
|
+
typeof state.enabled === "boolean" &&
|
|
146
|
+
isCount(state.revision) &&
|
|
147
|
+
// Accept writes the checkpoint and the state with the same revision; reset moves past it.
|
|
148
|
+
(state.checkpoint === undefined ||
|
|
149
|
+
(isProjectionCheckpoint(state.checkpoint) && state.checkpoint.revision === state.revision)) &&
|
|
150
|
+
(state.lastOutcome === undefined || isOutcome(state.lastOutcome))
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Drop the checkpoint and advance the revision past it, keeping `enabled`. */
|
|
155
|
+
export function resetProjectionState(
|
|
156
|
+
state: LiveContextState,
|
|
157
|
+
message: string,
|
|
158
|
+
at = new Date().toISOString(),
|
|
159
|
+
): LiveContextState {
|
|
160
|
+
return {
|
|
161
|
+
version: 1,
|
|
162
|
+
enabled: state.enabled,
|
|
163
|
+
revision: state.revision + 1,
|
|
164
|
+
lastOutcome: { kind: "reset", message, at },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Read `state.json` from the session directory. A missing file is a new session. An
|
|
170
|
+
* unreadable, malformed or foreign-version file yields the initial state and a warning;
|
|
171
|
+
* this function never throws.
|
|
172
|
+
*/
|
|
173
|
+
export async function loadLiveContextState(directory: string): Promise<LoadedState> {
|
|
174
|
+
const path = statePath(directory);
|
|
175
|
+
let text: string;
|
|
176
|
+
try {
|
|
177
|
+
text = await readFile(path, "utf8");
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return { state: initialLiveContextState() };
|
|
180
|
+
return { state: initialLiveContextState(), warning: `Could not read ${path}: ${describe(error)}; starting clean.` };
|
|
181
|
+
}
|
|
182
|
+
let parsed: unknown;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(text);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return { state: initialLiveContextState(), warning: `${path} is not valid JSON (${describe(error)}); starting clean.` };
|
|
187
|
+
}
|
|
188
|
+
if (isObject(parsed) && parsed.version !== STATE_VERSION) {
|
|
189
|
+
return {
|
|
190
|
+
state: initialLiveContextState(),
|
|
191
|
+
warning: `${path} has unsupported version ${JSON.stringify(parsed.version)}; starting clean.`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (!isLiveContextState(parsed)) {
|
|
195
|
+
return { state: initialLiveContextState(), warning: `${path} has an invalid shape; starting clean.` };
|
|
196
|
+
}
|
|
197
|
+
return { state: parsed };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Write `state.json` atomically with mode 0600: temp file, fsync, rename, then fsync the
|
|
202
|
+
* directory (best effort). Concurrent saves are not ordered: the last rename wins, so an
|
|
203
|
+
* older state can replace a newer one. Callers must serialize saves per session.
|
|
204
|
+
*/
|
|
205
|
+
export async function saveLiveContextState(directory: string, state: LiveContextState): Promise<void> {
|
|
206
|
+
const path = statePath(directory);
|
|
207
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
208
|
+
const text = `${JSON.stringify(state)}\n`;
|
|
209
|
+
try {
|
|
210
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
211
|
+
try {
|
|
212
|
+
await handle.writeFile(text, "utf8");
|
|
213
|
+
await handle.sync();
|
|
214
|
+
} finally {
|
|
215
|
+
await handle.close();
|
|
216
|
+
}
|
|
217
|
+
await rename(temporary, path);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
await syncDirectory(dirname(path));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Persist the rename. Some platforms refuse to open or fsync a directory; ignore that. */
|
|
226
|
+
async function syncDirectory(directory: string): Promise<void> {
|
|
227
|
+
try {
|
|
228
|
+
const handle = await open(directory, "r");
|
|
229
|
+
try {
|
|
230
|
+
await handle.sync();
|
|
231
|
+
} finally {
|
|
232
|
+
await handle.close();
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Best effort.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function describe(error: unknown): string {
|
|
240
|
+
return error instanceof Error ? error.message : String(error);
|
|
241
|
+
}
|
package/src/steering.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Adapted from pi-clm src/steering.ts (MIT, Copyright 2026 Emanuel Casco).
|
|
2
|
+
/**
|
|
3
|
+
* Steering document: the one place where context-management *strategy* may enter the
|
|
4
|
+
* prompt. The harness itself only states the editing protocol; a steering file tells the
|
|
5
|
+
* model when to compact, what to keep, how to structure notes, and so on. It is
|
|
6
|
+
* swappable so experiments can compare "no strategy" against a paper-style brief or an
|
|
7
|
+
* evolved skill, and it is hashed so runs can be attributed to an exact document.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { basename, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
export interface SteeringDocument {
|
|
15
|
+
path: string;
|
|
16
|
+
name: string;
|
|
17
|
+
text: string;
|
|
18
|
+
/** First 12 hex chars of the SHA-256 of the file bytes (matches `sha256sum`). */
|
|
19
|
+
hash: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function steeringPathFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
|
|
23
|
+
const raw = env.CLM_STEERING?.trim();
|
|
24
|
+
if (!raw || raw === "none" || raw === "off") return undefined;
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadSteeringDocument(path: string): SteeringDocument {
|
|
29
|
+
const absolute = resolve(path);
|
|
30
|
+
let raw: Buffer;
|
|
31
|
+
try {
|
|
32
|
+
raw = readFileSync(absolute);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`steering: cannot read ${absolute}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
const text = raw.toString("utf8").trim();
|
|
37
|
+
if (!text) throw new Error(`steering: document is empty: ${absolute}`);
|
|
38
|
+
return {
|
|
39
|
+
path: absolute,
|
|
40
|
+
name: basename(absolute),
|
|
41
|
+
text,
|
|
42
|
+
// Hash of the file bytes, so it matches `sha256sum <file>`.
|
|
43
|
+
hash: createHash("sha256").update(raw).digest("hex").slice(0, 12),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** System-prompt section appended after the editing protocol. */
|
|
48
|
+
export function steeringPromptSection(doc: SteeringDocument): string {
|
|
49
|
+
return `## Context-management guidance (${doc.name})\n\n${doc.text}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function steeringStatusLine(doc: SteeringDocument | undefined): string {
|
|
53
|
+
return doc ? `steering: ${doc.name} (sha256sum ${doc.hash}…) · ${doc.path}` : "steering: none (protocol only)";
|
|
54
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Adapted from pi-clm src/types.ts (MIT, Copyright 2026 Emanuel Casco).
|
|
2
|
+
|
|
3
|
+
export type LiveContextMessage = {
|
|
4
|
+
role: string;
|
|
5
|
+
content?: unknown;
|
|
6
|
+
timestamp?: number;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface DocumentBlock {
|
|
11
|
+
index: number;
|
|
12
|
+
id: string;
|
|
13
|
+
role: string;
|
|
14
|
+
protected: boolean;
|
|
15
|
+
header: string;
|
|
16
|
+
body: string;
|
|
17
|
+
source: LiveContextMessage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ContextDocumentSnapshot {
|
|
21
|
+
version: 1;
|
|
22
|
+
revision: number;
|
|
23
|
+
baselineDigest: string;
|
|
24
|
+
/** Per-document framing nonce. Derived from the baseline digest, or from a stable seed. */
|
|
25
|
+
documentId: string;
|
|
26
|
+
/** True when the nonce is seed-based and bodies were escaped (see renderContextDocument). */
|
|
27
|
+
stableDocument?: boolean;
|
|
28
|
+
text: string;
|
|
29
|
+
messages: LiveContextMessage[];
|
|
30
|
+
blocks: DocumentBlock[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the transform rendered for one request; consumed once before the next request. */
|
|
34
|
+
export interface TurnBaseline {
|
|
35
|
+
rawMessages: LiveContextMessage[];
|
|
36
|
+
effectiveMessages: LiveContextMessage[];
|
|
37
|
+
snapshot: ContextDocumentSnapshot;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ApplyDocumentOptions {
|
|
41
|
+
/** CLM permits reordered blocks and new IDs prefixed with new-. */
|
|
42
|
+
editingMode?: "conservative" | "clm";
|
|
43
|
+
/**
|
|
44
|
+
* Edit gate for edits that grow the context. "fit": growth is accepted while the result
|
|
45
|
+
* stays within `limit`; "shrink": growth is rejected; "none": no size check.
|
|
46
|
+
* Edits that do not grow are always accepted. Default: "fit" in CLM mode, else "shrink".
|
|
47
|
+
*/
|
|
48
|
+
gate?: "fit" | "shrink" | "none";
|
|
49
|
+
/**
|
|
50
|
+
* Limit the fit gate measures against, in the unit of `estimate` (characters by default).
|
|
51
|
+
* Without a positive finite limit the fit gate accepts every edit, like "none".
|
|
52
|
+
*/
|
|
53
|
+
limit?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The task statement is pinned outside the mirror, so a headerless rewrite replaces
|
|
56
|
+
* every block (instead of keeping the first user turn of the document).
|
|
57
|
+
*/
|
|
58
|
+
taskPinned?: boolean;
|
|
59
|
+
/** Estimator for the shrink gate. Defaults to the rendered-character estimate. */
|
|
60
|
+
estimate?: (messages: LiveContextMessage[]) => number;
|
|
61
|
+
/** Unit named in gate diagnostics. Defaults to "characters". */
|
|
62
|
+
estimateUnit?: "characters" | "tokens";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ContextEditSourceKind = "kept" | "edited" | "removed" | "restored" | "normalized";
|
|
66
|
+
|
|
67
|
+
export interface ContextEditSourceTrace {
|
|
68
|
+
/** Zero-based index in the exact effective context rendered to the mirror. */
|
|
69
|
+
sourceIndex: number;
|
|
70
|
+
/** Zero-based index in the accepted projected context; absent when removed. */
|
|
71
|
+
outputIndex?: number;
|
|
72
|
+
kind: ContextEditSourceKind;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ContextEditAdditionTrace {
|
|
76
|
+
/** Zero-based index in the accepted projected context. */
|
|
77
|
+
outputIndex: number;
|
|
78
|
+
kind: "added";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Compact provenance for reconstructing one accepted mirror edit without duplicating message bodies. */
|
|
82
|
+
export interface ContextEditTrace {
|
|
83
|
+
version: 1;
|
|
84
|
+
sourceRevision: number;
|
|
85
|
+
sourceMessageCount: number;
|
|
86
|
+
outputMessageCount: number;
|
|
87
|
+
sources: ContextEditSourceTrace[];
|
|
88
|
+
additions: ContextEditAdditionTrace[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ApplyDocumentResult {
|
|
92
|
+
accepted: boolean;
|
|
93
|
+
changed: boolean;
|
|
94
|
+
messages: LiveContextMessage[];
|
|
95
|
+
/** Sizes in the gate's estimate unit (characters by default, or the injected estimator's unit). */
|
|
96
|
+
beforeEstimate: number;
|
|
97
|
+
afterEstimate: number;
|
|
98
|
+
savingsEstimate: number;
|
|
99
|
+
diagnostics: string[];
|
|
100
|
+
editTrace?: ContextEditTrace;
|
|
101
|
+
reason?: string;
|
|
102
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
You manage your own context. The file named in "Editable context" is the exact
|
|
2
|
+
conversation you will see on your next request; whatever you write there is what you
|
|
3
|
+
will remember. Use it deliberately.
|
|
4
|
+
|
|
5
|
+
When to act
|
|
6
|
+
- Watch the [CLM BUDGET] notices; they state the estimated size of the next request and
|
|
7
|
+
the provider-reported size of the previous one. Edit before you reach the reserve, not
|
|
8
|
+
after: one turn with several large tool results can jump past the budget.
|
|
9
|
+
- Prefer one larger batched edit over many small ones. Each accepted edit changes the
|
|
10
|
+
request prefix, so everything after the edit point is re-processed by the provider.
|
|
11
|
+
|
|
12
|
+
What to keep
|
|
13
|
+
- The task statement, the current plan, decisions and their reasons, verified facts with
|
|
14
|
+
their sources (file paths, ids, commands), dead ends so you do not retry them, and the
|
|
15
|
+
next action.
|
|
16
|
+
- Anything you will need verbatim (exact values, keys, quotes). Either keep it in the
|
|
17
|
+
context, or offload it to a file and keep the path plus a one-line index; re-read on
|
|
18
|
+
demand.
|
|
19
|
+
|
|
20
|
+
What to drop or shrink
|
|
21
|
+
- Raw tool output you have already extracted the facts from: replace the block body with a
|
|
22
|
+
one-line summary of what the command was and what you learned.
|
|
23
|
+
- Superseded exploration, duplicated content, your own scratch reasoning once its
|
|
24
|
+
conclusion is recorded.
|
|
25
|
+
|
|
26
|
+
How to edit
|
|
27
|
+
- Read the first line of the mirror right before you write and copy it unchanged; then
|
|
28
|
+
list the `[[CTX_TURN ...]]` header lines to get block ids. Do not print whole bodies.
|
|
29
|
+
- Edit bodies in place with a small Python script (re.sub on the block body); keep every
|
|
30
|
+
header line you retain. Delete a block by removing its header and body together.
|
|
31
|
+
- To add a durable note, insert a block whose id starts with `new-` (for example
|
|
32
|
+
`id=new-tracker`) with a role label such as `notes`. Never invent numeric ids; ids of
|
|
33
|
+
existing blocks come only from the current headers. Update the note in place afterwards
|
|
34
|
+
instead of appending new copies.
|
|
35
|
+
- If you want to replace everything with a summary, you may write the file as plain text
|
|
36
|
+
without any headers: it becomes one notes block after the original task statement. Use
|
|
37
|
+
this sparingly; you lose everything else.
|
|
38
|
+
- Tool results that were withheld by the overflow guard can be re-read from the file path
|
|
39
|
+
in their note; read only the part you need (offset/limit or grep), and record the fact
|
|
40
|
+
you extracted before moving on.
|
|
41
|
+
- Keep a compact tracker block near the top (status, done/todo, key facts, next step) and
|
|
42
|
+
maintain it every few turns.
|
|
43
|
+
- Never fabricate content in a summary; when unsure, keep the exact text or offload it.
|