one-code-extension 0.5.0 → 0.6.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/README.md +1 -1
- package/docs/guide/differences-from-claude-code.md +9 -6
- package/docs/guide/installation.md +3 -4
- package/docs/guide/troubleshooting.md +1 -1
- package/extensions/auto-mode/denials.ts +3 -2
- package/extensions/background/cron.ts +127 -0
- package/extensions/background/index.ts +135 -47
- package/extensions/background/loop-fire.ts +39 -2
- package/extensions/background/switch-guard.ts +10 -6
- package/extensions/background/wakeup.ts +21 -2
- package/extensions/bash/index.ts +8 -30
- package/extensions/btw/index.ts +32 -18
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-context/index.ts +157 -172
- package/extensions/compaction/index.ts +11 -8
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +15 -14
- package/extensions/context-management/index.ts +11 -28
- package/extensions/effort/index.ts +70 -7
- package/extensions/file-tracker/index.ts +111 -44
- package/extensions/file-tracker/replay.ts +76 -49
- package/extensions/file-tracker/restore.ts +12 -9
- package/extensions/file-tracker/tracker.ts +14 -1
- package/extensions/hooks/index.ts +60 -76
- package/extensions/hooks/prompt-context.ts +59 -0
- package/extensions/hooks/trust.ts +13 -7
- package/extensions/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +17 -1
- package/extensions/lib/agent-loader.ts +3 -2
- package/extensions/lib/anthropic-auth.ts +98 -0
- package/extensions/lib/child-extensions.ts +14 -4
- package/extensions/lib/context-facts.ts +75 -56
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +152 -12
- package/extensions/lib/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +24 -6
- package/extensions/lib/model-catalog/huggingface.json +5 -1
- package/extensions/lib/model-catalog/models-dev.json +10535 -10299
- package/extensions/lib/model-catalog/openrouter.json +191 -393
- package/extensions/lib/notifications.ts +57 -17
- package/extensions/lib/pi-version.ts +25 -8
- package/extensions/lib/plan-mode-channels.ts +29 -7
- package/extensions/lib/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +3 -2
- package/extensions/lib/reminders.ts +327 -621
- package/extensions/lib/request-replay.ts +186 -78
- package/extensions/lib/side-call.ts +11 -0
- package/extensions/lib/subagent-handback.ts +0 -1
- package/extensions/lib/tool-additions.ts +2 -22
- package/extensions/lib/turn-budget-layout.ts +18 -2
- package/extensions/lsp/index.ts +6 -8
- package/extensions/mcp/announce.ts +63 -98
- package/extensions/mcp/index.ts +51 -90
- package/extensions/mcp/trust.ts +13 -6
- package/extensions/model-default/index.ts +86 -19
- package/extensions/permissions/index.ts +157 -48
- package/extensions/permissions/modes.ts +5 -0
- package/extensions/plan-mode/index.ts +142 -45
- package/extensions/plan-mode/run-state.ts +26 -0
- package/extensions/plugins/index.ts +4 -1
- package/extensions/powershell/index.ts +2 -0
- package/extensions/recap/index.ts +11 -14
- package/extensions/search-tools/index.ts +2 -0
- package/extensions/skill/announce.ts +30 -0
- package/extensions/skill/index.ts +65 -48
- package/extensions/skill/invoke.ts +0 -67
- package/extensions/subagents/agents.ts +30 -8
- package/extensions/subagents/fork-cache.ts +9 -3
- package/extensions/subagents/index.ts +180 -107
- package/extensions/subagents/outcome.ts +3 -0
- package/extensions/subagents/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +29 -2
- package/extensions/subagents/runs.ts +5 -0
- package/extensions/system-prompt/index.ts +40 -6
- package/extensions/system-prompt/template.ts +10 -1
- package/extensions/system-reminder/index.ts +174 -202
- package/extensions/tool-search/index.ts +241 -167
- package/extensions/workflow/agent-session.ts +4 -1
- package/extensions/workflow/index.ts +16 -4
- package/extensions/workflow/run-manager.ts +4 -0
- package/extensions/worktree/index.ts +50 -14
- package/package.json +5 -2
- package/extensions/lib/context-stack.ts +0 -81
- package/extensions/subagents/announce.ts +0 -69
- package/extensions/tool-search/announce.ts +0 -58
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* CONTEXT_ORDER.claudeMd, the context block (userEmail + the git snapshot) at
|
|
6
6
|
* CONTEXT_ORDER.context, and the date at CONTEXT_ORDER.date, which on a model
|
|
7
7
|
* that takes a mid-conversation system message rides that message instead
|
|
8
|
-
* (system-reminder, lib/system-role.ts). They
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* (system-reminder, lib/system-role.ts). They are live state: a request that
|
|
9
|
+
* opens an epoch renders them, and every later request in the epoch replays
|
|
10
|
+
* them from the ledger's record (extensions/ledger). The blocks carry ledger
|
|
11
|
+
* facts (lib/context-facts.ts), so the first request of a resumed process says
|
|
12
|
+
* once what changed in the workspace since, and a session that crosses local
|
|
13
|
+
* midnight learns the new date, both on the request's tail.
|
|
11
14
|
*
|
|
12
15
|
* When ONECODE.md files exist, this extension also emits a separate `# oneCodeMd`
|
|
13
16
|
* block at CONTEXT_ORDER.oneCodeMd — after the instructions block, so One
|
|
@@ -41,10 +44,9 @@ import {
|
|
|
41
44
|
import { collectGitStatus, GIT_SNAPSHOT_OWNER_CHANNEL } from "../lib/git-status.ts";
|
|
42
45
|
import { readMemoryIndex } from "../lib/memory-index.ts";
|
|
43
46
|
import { claudeConfigDir, oneCodeStateDir, tryRealpath } from "../lib/paths.ts";
|
|
44
|
-
import { CONTEXT_ORDER, REMINDER_CHANNEL, type
|
|
47
|
+
import { CONTEXT_ORDER, REMINDER_CHANNEL, RENDER_CHANNEL, type RenderRequest, toldBlockFact } from "../lib/reminders.ts";
|
|
45
48
|
import { inContextEntries } from "../lib/compaction-boundary.ts";
|
|
46
|
-
import {
|
|
47
|
-
import { compactionGitStatus, contextFactsBaseline, DATE_CHANGE_KEY, legacyResumeFactsNotice, RESUME_FACTS_KEY, resumeFactsNotice, storedFactsBaseline, type ContextFactsBaseline } from "../lib/context-facts.ts";
|
|
49
|
+
import { compactionGitStatus, contextBlockFacts, contextFingerprints, dateFingerprint, FACT_KEYS, instructionFacts, liveFingerprints, type LiveFingerprints, resumeFactsNotice } from "../lib/context-facts.ts";
|
|
48
50
|
import { resolveToolPath } from "../lib/tool-path.ts";
|
|
49
51
|
import { sessionWorkCwd, WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
50
52
|
import { pathArgument } from "../auto-mode/paths.ts";
|
|
@@ -52,6 +54,9 @@ import { HARNESS_GIT_CONFIG } from "../lib/git.ts";
|
|
|
52
54
|
import { EXTERNAL_INCLUDES_NO, EXTERNAL_INCLUDES_NONE, EXTERNAL_INCLUDES_SETTING, EXTERNAL_INCLUDES_YES, externalIncludesDialog, persistExternalIncludesApproval, readExternalIncludesApproval } from "../lib/claude-external-includes.ts";
|
|
53
55
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
54
56
|
import { consentDialog, installConsentDialogs, startupConsentReady } from "../lib/consent-dialogs.ts";
|
|
57
|
+
import type { ToldFact } from "../lib/ledger/records.ts";
|
|
58
|
+
import { foldSession } from "../lib/ledger/fold-memo.ts";
|
|
59
|
+
import { sessionIdentity } from "../lib/anthropic-auth.ts";
|
|
55
60
|
|
|
56
61
|
const REMINDER_KEY = "claude-context";
|
|
57
62
|
const ONECODE_REMINDER_KEY = "one-code-context";
|
|
@@ -96,12 +101,8 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
96
101
|
pi.events.on(GIT_SNAPSHOT_OWNER_CHANNEL, () => {
|
|
97
102
|
takesGitSnapshot = true;
|
|
98
103
|
});
|
|
99
|
-
/** The date the date
|
|
104
|
+
/** The date the live date block carries. */
|
|
100
105
|
let blockDate = "";
|
|
101
|
-
/** The date the model was last told: the reminder's, or a later date-change notice's. */
|
|
102
|
-
let shownDate = "";
|
|
103
|
-
let hadInstructions = false;
|
|
104
|
-
let hadOneCode = false;
|
|
105
106
|
/**
|
|
106
107
|
* Instruction files in context, by real path: the startup block's files and
|
|
107
108
|
* their imports (`startupShown`, fixed for the session), plus nested files
|
|
@@ -111,8 +112,6 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
111
112
|
let attachedNested = new Set<string>();
|
|
112
113
|
/** Where a nested instruction actually landed: a persisted result or a pinned request tail. */
|
|
113
114
|
const shownByResult = new Map<string, Set<string>>();
|
|
114
|
-
/** Nested files queued on a read's result, not yet sent in a request. */
|
|
115
|
-
const pendingNested = new Map<string, string[]>();
|
|
116
115
|
const remember = (toolCallId: string | undefined, keys: Iterable<string>) => {
|
|
117
116
|
if (!toolCallId) return;
|
|
118
117
|
const shown = shownByResult.get(toolCallId) ?? new Set<string>();
|
|
@@ -156,24 +155,33 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
156
155
|
let approvalPending: Promise<void> | undefined;
|
|
157
156
|
let approvalAbort = new AbortController();
|
|
158
157
|
let sessionGeneration = 0;
|
|
159
|
-
let requestStarted = false;
|
|
160
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The instructions facts while no instructions block renders (no CLAUDE.md,
|
|
161
|
+
* no memory index): they ride the date block, so an epoch still tells the
|
|
162
|
+
* absence and a resume notices a file that appeared since.
|
|
163
|
+
*/
|
|
164
|
+
let absentInstructionFacts: ToldFact[] = [];
|
|
161
165
|
const emitDate = (date: string) => {
|
|
162
166
|
blockDate = date;
|
|
163
|
-
shownDate = date;
|
|
164
167
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
165
168
|
text: dateBlock(date),
|
|
166
|
-
scope: "every-turn",
|
|
167
169
|
key: DATE_REMINDER_KEY,
|
|
168
170
|
placement: "first-prepend",
|
|
169
171
|
order: CONTEXT_ORDER.date,
|
|
172
|
+
facts: [toldBlockFact(FACT_KEYS.date, date), ...absentInstructionFacts],
|
|
170
173
|
});
|
|
171
174
|
};
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
/**
|
|
177
|
+
* The live workspace's fingerprints, taken at this process's first turn, for
|
|
178
|
+
* the resumed-session check, in each directory the epoch's facts may have
|
|
179
|
+
* been read in: the session's own at its start, the entered worktree at a
|
|
180
|
+
* compaction (session_compact reads there).
|
|
181
|
+
*/
|
|
182
|
+
let livePrints: { atStart: LiveFingerprints; atCompaction: () => LiveFingerprints } | undefined;
|
|
183
|
+
/** True until this process's first request inside an epoch has compared the epoch's facts with the live workspace. */
|
|
184
|
+
let resumeCheck = true;
|
|
177
185
|
const readFiles = (cwd: string) => {
|
|
178
186
|
const oneCodeFiles = discoverOneCodeFiles({ cwd, homeOneCodeDir: oneCodeStateDir(), home: os.homedir(), includeExternal });
|
|
179
187
|
return {
|
|
@@ -183,26 +191,21 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
183
191
|
oneCode: buildOneCodeBlock(oneCodeFiles),
|
|
184
192
|
};
|
|
185
193
|
};
|
|
186
|
-
/** What a stored snapshot says the model was shown: its facts, the files its blocks carry, and its dates. */
|
|
187
|
-
const adoptSnapshot = (snapshot: ContextStackSnapshot) => {
|
|
188
|
-
const saved = snapshot.baselines["claude-context"] as { startupShown?: unknown; shownDate?: unknown; facts?: unknown } | undefined;
|
|
189
|
-
facts = storedFactsBaseline(saved?.facts);
|
|
190
|
-
startupShown = new Set(Array.isArray(saved?.startupShown) ? saved.startupShown.filter((p): p is string => typeof p === "string") : []);
|
|
191
|
-
attachedNested = new Set(startupShown);
|
|
192
|
-
blockDate = snapshot.stack.find((entry) => entry.key === DATE_REMINDER_KEY)?.text.match(/^Today's date is (\d{4}-\d{2}-\d{2})\.$/)?.[1] ?? "";
|
|
193
|
-
shownDate = typeof saved?.shownDate === "string" ? saved.shownDate : blockDate;
|
|
194
|
-
hadInstructions = snapshot.stack.some((entry) => entry.key === REMINDER_KEY);
|
|
195
|
-
hadOneCode = snapshot.stack.some((entry) => entry.key === ONECODE_REMINDER_KEY);
|
|
196
|
-
};
|
|
197
194
|
let files: ReturnType<typeof readFiles> | undefined;
|
|
198
195
|
const adoptFiles = (current: ReturnType<typeof readFiles>) => {
|
|
199
196
|
files = current;
|
|
200
|
-
|
|
197
|
+
rule = instructionRule(os.homedir());
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The files message 1 carries (or imports): never attached again on a read.
|
|
201
|
+
* Taken when an epoch renders the blocks; a resumed epoch's are taken from
|
|
202
|
+
* the live files at start, the closest knowledge this process has.
|
|
203
|
+
*/
|
|
204
|
+
const adoptShown = (current: ReturnType<typeof readFiles>) => {
|
|
201
205
|
startupShown = new Set([...current.contextFiles, ...current.oneCodeFiles].flatMap((file) =>
|
|
202
206
|
[file.path, ...(file.imported ?? collectImportedPaths(readOrEmpty(file.path), dirname(file.path), { home: os.homedir() }))],
|
|
203
207
|
).map((path) => tryRealpath(path) ?? path));
|
|
204
|
-
|
|
205
|
-
rule = instructionRule(os.homedir());
|
|
208
|
+
for (const path of startupShown) attachedNested.add(path);
|
|
206
209
|
};
|
|
207
210
|
const snapshotGit = (cwd: string) => {
|
|
208
211
|
const tools = pi.getActiveTools();
|
|
@@ -216,14 +219,56 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
216
219
|
try { persistExternalIncludesApproval(ctx.cwd, os.homedir(), approved); }
|
|
217
220
|
catch (error) { ctx.ui.notify(`Could not remember external CLAUDE.md include approval: ${String(error)}`, "error"); }
|
|
218
221
|
};
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
/**
|
|
223
|
+
* The instructions and ONECODE.md blocks as live state, read in `cwd`. They
|
|
224
|
+
* render when an epoch opens; inside one, message 1 is the record, so a
|
|
225
|
+
* change here (a /config approval, an edited file) shows at the next epoch.
|
|
226
|
+
*/
|
|
227
|
+
const emitInstructions = (current: ReturnType<typeof readFiles>) => {
|
|
228
|
+
const instructions = buildClaudeMdBlock(current);
|
|
229
|
+
if (instructions) {
|
|
230
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
231
|
+
text: instructions,
|
|
232
|
+
key: REMINDER_KEY,
|
|
233
|
+
placement: "first-prepend",
|
|
234
|
+
order: CONTEXT_ORDER.claudeMd,
|
|
235
|
+
facts: instructionFacts(contextFingerprints(current)),
|
|
236
|
+
});
|
|
237
|
+
} else pi.events.emit(REMINDER_CHANNEL, { remove: true, key: REMINDER_KEY });
|
|
238
|
+
const absent = instructions ? [] : instructionFacts(contextFingerprints(current));
|
|
239
|
+
if (JSON.stringify(absent) !== JSON.stringify(absentInstructionFacts)) {
|
|
240
|
+
absentInstructionFacts = absent;
|
|
241
|
+
if (blockDate) emitDate(blockDate);
|
|
242
|
+
}
|
|
243
|
+
// One Code's own instructions ride in a separate block AFTER the instructions
|
|
244
|
+
// block, so they take precedence over CLAUDE.md (higher order = closer to the user text).
|
|
245
|
+
if (current.oneCode) {
|
|
246
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
247
|
+
text: current.oneCode,
|
|
248
|
+
key: ONECODE_REMINDER_KEY,
|
|
249
|
+
placement: "first-prepend",
|
|
250
|
+
order: CONTEXT_ORDER.oneCodeMd,
|
|
251
|
+
});
|
|
252
|
+
} else pi.events.emit(REMINDER_CHANNEL, { remove: true, key: ONECODE_REMINDER_KEY });
|
|
253
|
+
};
|
|
254
|
+
const emitContextBlock = (gitText: string | null, atCompaction: boolean) => {
|
|
255
|
+
const context = buildContextBlock({ email, gitStatus: gitText });
|
|
256
|
+
if (!context) {
|
|
257
|
+
pi.events.emit(REMINDER_CHANNEL, { remove: true, key: CONTEXT_REMINDER_KEY });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
261
|
+
text: context,
|
|
262
|
+
key: CONTEXT_REMINDER_KEY,
|
|
263
|
+
placement: "first-prepend",
|
|
264
|
+
order: CONTEXT_ORDER.context,
|
|
265
|
+
facts: contextBlockFacts(gitStatus ?? null, atCompaction),
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
const refreshInstructions = (cwd: string) => {
|
|
221
269
|
const current = readFiles(cwd);
|
|
222
270
|
adoptFiles(current);
|
|
223
|
-
|
|
224
|
-
if (text) pi.events.emit(REMINDER_CHANNEL, { text, scope: "every-turn", key: REMINDER_KEY, placement: "first-prepend", order: CONTEXT_ORDER.claudeMd });
|
|
225
|
-
if (current.oneCode) pi.events.emit(REMINDER_CHANNEL, { text: current.oneCode, scope: "every-turn", key: ONECODE_REMINDER_KEY, placement: "first-prepend", order: CONTEXT_ORDER.oneCodeMd });
|
|
226
|
-
publishBaseline();
|
|
271
|
+
emitInstructions(current);
|
|
227
272
|
};
|
|
228
273
|
const askApproval = async (ctx: ExtensionContext, paths: string[], afterStartup = false) => {
|
|
229
274
|
const gen = sessionGeneration;
|
|
@@ -233,7 +278,7 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
233
278
|
const choice = await consentDialog(pi.events, () => ctx.ui.select(externalIncludesDialog(paths, os.homedir()), [EXTERNAL_INCLUDES_NO, EXTERNAL_INCLUDES_YES], { signal }), afterStartup);
|
|
234
279
|
if (signal.aborted || gen !== sessionGeneration) return;
|
|
235
280
|
saveApproval(ctx, choice === EXTERNAL_INCLUDES_YES);
|
|
236
|
-
|
|
281
|
+
refreshInstructions(ctx.cwd);
|
|
237
282
|
};
|
|
238
283
|
registerLocalCommand(pi, "config", {
|
|
239
284
|
description: "Configure external CLAUDE.md includes for this project",
|
|
@@ -259,7 +304,7 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
259
304
|
if (picked !== row || gen !== sessionGeneration || approvalAbort.signal.aborted) return;
|
|
260
305
|
if (includeExternal) {
|
|
261
306
|
saveApproval(ctx, false);
|
|
262
|
-
|
|
307
|
+
refreshInstructions(ctx.cwd);
|
|
263
308
|
} else await askApproval(ctx, paths);
|
|
264
309
|
// As in Claude Code, this changes future loads, never retracts text or
|
|
265
310
|
// rewrites the frozen first-message block after a request was sent.
|
|
@@ -281,13 +326,8 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
281
326
|
approvalAbort.abort();
|
|
282
327
|
approvalAbort = new AbortController();
|
|
283
328
|
approvalPending = undefined;
|
|
284
|
-
requestStarted = false;
|
|
285
329
|
const approval = readExternalIncludesApproval(ctx.cwd, os.homedir());
|
|
286
330
|
includeExternal = approval.approved;
|
|
287
|
-
const restored = restoredContext(pi.events);
|
|
288
|
-
pendingResume = restored;
|
|
289
|
-
files = undefined;
|
|
290
|
-
facts = undefined;
|
|
291
331
|
if ((event.reason ?? "startup") === "startup" && ctx.hasUI && !approval.approved && !approval.warningShown) {
|
|
292
332
|
const paths = externalPaths(ctx.cwd);
|
|
293
333
|
if (paths.length) {
|
|
@@ -297,61 +337,23 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
297
337
|
approvalPending = askApproval(ctx, paths, true).catch(() => {});
|
|
298
338
|
}
|
|
299
339
|
}
|
|
300
|
-
if (restored) {
|
|
301
|
-
adoptSnapshot(restored);
|
|
302
|
-
restoreAttachments(ctx);
|
|
303
|
-
rule = instructionRule(os.homedir());
|
|
304
|
-
gitStatus = null; // The stored snapshot wins; do not take another one.
|
|
305
|
-
publishBaseline();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
340
|
// The instructions block carries the instruction files the mode and Claude
|
|
309
341
|
// Code's instructionFiles pick (lib/claude-context.ts instructionRule): by
|
|
310
342
|
// default the CLAUDE.md family, or the project's AGENTS.md files when it
|
|
311
343
|
// has no CLAUDE.md, byte-exact with Claude Code either way. ONECODE.md
|
|
312
|
-
// rides its own higher-precedence block
|
|
313
|
-
|
|
314
|
-
adoptFiles(current);
|
|
315
|
-
const instructions = buildClaudeMdBlock(current);
|
|
316
|
-
if (instructions) {
|
|
317
|
-
pi.events.emit(REMINDER_CHANNEL, {
|
|
318
|
-
text: instructions,
|
|
319
|
-
scope: "every-turn",
|
|
320
|
-
key: REMINDER_KEY,
|
|
321
|
-
placement: "first-prepend",
|
|
322
|
-
order: CONTEXT_ORDER.claudeMd,
|
|
323
|
-
});
|
|
324
|
-
} else if (hadInstructions) {
|
|
325
|
-
pi.events.emit(REMINDER_CHANNEL, { remove: true, key: REMINDER_KEY });
|
|
326
|
-
}
|
|
327
|
-
hadInstructions = instructions !== null;
|
|
344
|
+
// rides its own higher-precedence block.
|
|
345
|
+
refreshInstructions(ctx.cwd);
|
|
328
346
|
email = resolveEmail(ctx.cwd);
|
|
329
|
-
attachedNested = new Set(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// /clear re-fires session_start: the next conversation takes its own snapshot.
|
|
347
|
+
attachedNested = new Set();
|
|
348
|
+
if (files) adoptShown(files);
|
|
349
|
+
restoreAttachments(ctx);
|
|
350
|
+
// /clear and a resume re-fire session_start: this process takes its own snapshot.
|
|
334
351
|
gitStatus = undefined;
|
|
335
|
-
|
|
336
|
-
|
|
352
|
+
livePrints = undefined;
|
|
353
|
+
resumeCheck = true;
|
|
354
|
+
// The user's local date. Inside an epoch the block on message 1 is the
|
|
355
|
+
// record, so a later date rides a notice (the render step below).
|
|
337
356
|
emitDate(localDate());
|
|
338
|
-
|
|
339
|
-
// One Code's own instructions ride in a separate block AFTER the instructions
|
|
340
|
-
// block, so they take precedence over CLAUDE.md (higher order = closer to the user text).
|
|
341
|
-
const oneCode = current.oneCode;
|
|
342
|
-
if (oneCode) {
|
|
343
|
-
pi.events.emit(REMINDER_CHANNEL, {
|
|
344
|
-
text: oneCode,
|
|
345
|
-
scope: "every-turn",
|
|
346
|
-
key: ONECODE_REMINDER_KEY,
|
|
347
|
-
placement: "first-prepend",
|
|
348
|
-
order: CONTEXT_ORDER.oneCodeMd,
|
|
349
|
-
});
|
|
350
|
-
} else if (hadOneCode) {
|
|
351
|
-
pi.events.emit(REMINDER_CHANNEL, { remove: true, key: ONECODE_REMINDER_KEY });
|
|
352
|
-
}
|
|
353
|
-
publishBaseline();
|
|
354
|
-
hadOneCode = oneCode !== null;
|
|
355
357
|
});
|
|
356
358
|
|
|
357
359
|
// Claude Code snapshots git "at the start of the conversation": on the first
|
|
@@ -362,89 +364,79 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
362
364
|
const gen = sessionGeneration;
|
|
363
365
|
await drainApproval();
|
|
364
366
|
if (gen !== sessionGeneration || approvalAbort.signal.aborted) return;
|
|
365
|
-
requestStarted = true;
|
|
366
|
-
// Check only once, just before the first resumed request. No startup git
|
|
367
|
-
// subprocesses, and no changes to the historical first-message blocks.
|
|
368
|
-
if (pendingResume) {
|
|
369
|
-
const restored = pendingResume;
|
|
370
|
-
pendingResume = undefined;
|
|
371
|
-
// Compare in the directory the facts were read in: a resume inside a worktree entered
|
|
372
|
-
// after the session started would otherwise report every path as changed.
|
|
373
|
-
const cwd = facts?.cwd ?? (facts?.atCompaction ? sessionWorkCwd(entered, ctx.cwd) : ctx.cwd);
|
|
374
|
-
const current = readFiles(cwd);
|
|
375
|
-
const liveGit = snapshotGit(cwd);
|
|
376
|
-
const liveFacts = contextFactsBaseline({ ...current, gitStatus: liveGit, atCompaction: false });
|
|
377
|
-
const text = facts ? resumeFactsNotice(facts, liveFacts) : legacyResumeFactsNotice(restored.stack, {
|
|
378
|
-
instructions: buildClaudeMdBlock(current), email: resolveEmail(cwd), gitStatus: liveGit,
|
|
379
|
-
});
|
|
380
|
-
if (text) pi.events.emit(REMINDER_CHANNEL, { key: RESUME_FACTS_KEY, text });
|
|
381
|
-
}
|
|
382
367
|
if (gitStatus !== undefined) return;
|
|
368
|
+
// A process that starts on a compacted branch before the compaction's
|
|
369
|
+
// epoch opened (/compact, then /reload) renders that epoch's message 1:
|
|
370
|
+
// the blocks are the compaction's, not the conversation start's (R3-L11).
|
|
371
|
+
// The fold is shared with tool-search and the ledger: one fold per branch state (lib/ledger/fold-memo.ts).
|
|
372
|
+
const fold = foldSession(ctx.sessionManager, sessionIdentity(pi, ctx.model));
|
|
373
|
+
if (fold.opensEpoch && fold.cause === "compaction") {
|
|
374
|
+
refreshForCompaction(ctx);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
383
377
|
gitStatus = snapshotGit(ctx.cwd);
|
|
384
|
-
if (files)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
order: CONTEXT_ORDER.context,
|
|
394
|
-
});
|
|
378
|
+
if (files) {
|
|
379
|
+
const atStart = liveFingerprints(files, gitStatus, takesGitSnapshot);
|
|
380
|
+
const workCwd = sessionWorkCwd(entered, ctx.cwd);
|
|
381
|
+
// The entered worktree's, read only if the epoch's facts came from a compaction there.
|
|
382
|
+
let inWorktree: LiveFingerprints | undefined;
|
|
383
|
+
const atCompaction = () => (inWorktree ??= liveFingerprints(readFiles(workCwd), snapshotGit(workCwd), takesGitSnapshot));
|
|
384
|
+
livePrints = { atStart, atCompaction: workCwd === ctx.cwd ? () => atStart : atCompaction };
|
|
385
|
+
}
|
|
386
|
+
emitContextBlock(gitStatus, false);
|
|
395
387
|
});
|
|
396
388
|
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
389
|
+
// Inside an epoch, two facts can have moved since its message 1: the date (a
|
|
390
|
+
// session that crosses local midnight learns it, Claude Code's notice), and,
|
|
391
|
+
// on the first request of a resumed process, the workspace the instructions
|
|
392
|
+
// and git blocks describe. Each is said once, on the request's tail.
|
|
393
|
+
pi.events.on(RENDER_CHANNEL, (data) => {
|
|
394
|
+
const request = data as RenderRequest;
|
|
395
|
+
if (request.epoch) {
|
|
396
|
+
// This request renders the live blocks as message 1: with today's date,
|
|
397
|
+
// even when it opens past local midnight (a model switch, a fold).
|
|
398
|
+
resumeCheck = false;
|
|
399
|
+
if (files) adoptShown(files);
|
|
400
|
+
const today = localDate();
|
|
401
|
+
if (blockDate && blockDate !== today) emitDate(today);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
402
404
|
const today = localDate();
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
405
|
+
const toldDate = request.toldBlocks.get(FACT_KEYS.date);
|
|
406
|
+
if (blockDate && toldDate !== undefined && toldDate !== dateFingerprint(today)) {
|
|
407
|
+
request.blocks.push({ text: dateChangeReminder(today), facts: [toldBlockFact(FACT_KEYS.date, today)] });
|
|
408
|
+
// The live block too, so a later epoch (a model switch, a fold) renders today's date on message 1.
|
|
409
|
+
emitDate(today);
|
|
410
|
+
}
|
|
411
|
+
if (!resumeCheck || !livePrints) return;
|
|
412
|
+
const atCompaction = request.modes.get(FACT_KEYS.compaction) ?? false;
|
|
413
|
+
const notice = resumeFactsNotice(request.toldBlocks, atCompaction, atCompaction ? livePrints.atCompaction() : livePrints.atStart);
|
|
414
|
+
// Checked until a request shows the notice told (its facts are the live
|
|
415
|
+
// fingerprints): a failed request revokes them, so the retry says it again.
|
|
416
|
+
if (notice) request.blocks.push(notice);
|
|
417
|
+
else resumeCheck = false;
|
|
418
|
+
});
|
|
409
419
|
|
|
410
|
-
// All successful compactions (manual, automatic and idle) fire this hook.
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
pi.on("session_compact", (_event, ctx) =>
|
|
414
|
-
|
|
420
|
+
// All successful compactions (manual, automatic and idle) fire this hook. The
|
|
421
|
+
// summary opens an epoch, which renders these blocks: refresh them from the
|
|
422
|
+
// workspace now, in the entered worktree if any.
|
|
423
|
+
pi.on("session_compact", (_event, ctx) => refreshForCompaction(ctx));
|
|
424
|
+
function refreshForCompaction(ctx: ExtensionContext) {
|
|
415
425
|
const cwd = sessionWorkCwd(entered, ctx.cwd);
|
|
416
426
|
const current = readFiles(cwd);
|
|
417
427
|
adoptFiles(current);
|
|
428
|
+
emitInstructions(current);
|
|
418
429
|
// Only discarded carriers need reattachment; pi can retain the recent read/tool-result tail.
|
|
419
430
|
keepRetainedAttachments(ctx);
|
|
420
431
|
email = resolveEmail(cwd);
|
|
421
432
|
gitStatus = snapshotGit(cwd);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const add = (key: string, text: string | null, order: number) => {
|
|
426
|
-
if (text) entries.push({ key, text, order, placement: "first-prepend" });
|
|
427
|
-
};
|
|
428
|
-
add(REMINDER_KEY, buildClaudeMdBlock(current), CONTEXT_ORDER.claudeMd);
|
|
429
|
-
add(ONECODE_REMINDER_KEY, current.oneCode, CONTEXT_ORDER.oneCodeMd);
|
|
430
|
-
add(CONTEXT_REMINDER_KEY, buildContextBlock({ email, gitStatus: compactionGitStatus(gitStatus) }), CONTEXT_ORDER.context);
|
|
431
|
-
add(DATE_REMINDER_KEY, dateBlock(blockDate), CONTEXT_ORDER.date);
|
|
432
|
-
pi.events.emit(CONTEXT_FACTS_REFRESH_CHANNEL, { entries, baseline: baseline() });
|
|
433
|
-
});
|
|
433
|
+
emitContextBlock(compactionGitStatus(gitStatus), true);
|
|
434
|
+
emitDate(localDate());
|
|
435
|
+
}
|
|
434
436
|
|
|
435
437
|
// A branch switch leaves the attachments of the branch left behind; the new one may lack them.
|
|
436
|
-
// Its facts are the selected branch's (system-reminder restores its blocks): a branch from
|
|
437
|
-
// before a compaction still shows the session-start facts, and a later compaction or date
|
|
438
|
-
// notice compares against those.
|
|
439
438
|
pi.on("session_tree", (_event, ctx) => {
|
|
440
|
-
const branch = contextStackOnBranch(ctx.sessionManager.getBranch());
|
|
441
|
-
if (branch) {
|
|
442
|
-
adoptSnapshot(branch);
|
|
443
|
-
if (pendingResume) pendingResume = branch;
|
|
444
|
-
publishBaseline();
|
|
445
|
-
}
|
|
446
439
|
restoreAttachments(ctx);
|
|
447
|
-
pendingNested.clear();
|
|
448
440
|
});
|
|
449
441
|
|
|
450
442
|
// A successful read attaches nested instruction files and matching path rules
|
|
@@ -481,16 +473,9 @@ export default function claudeContextExtension(pi: ExtensionAPI) {
|
|
|
481
473
|
}
|
|
482
474
|
pi.events.emit(REMINDER_CHANNEL, { text: nestedInstructionText(file), toolCallId: event.toolCallId });
|
|
483
475
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// before then (the queued text is not persisted) leaves them unshown.
|
|
489
|
-
pi.on("context", (event) => {
|
|
490
|
-
for (const [toolCallId, keys] of pendingNested) {
|
|
491
|
-
if (!event.messages.some((m) => m.role === "toolResult" && m.toolCallId === toolCallId)) continue;
|
|
492
|
-
remember(toolCallId, keys);
|
|
493
|
-
pendingNested.delete(toolCallId);
|
|
494
|
-
}
|
|
476
|
+
// Recorded with the queued text: both are on the branch now (the text as a
|
|
477
|
+
// pending one-shot bound to this result), so a quit before the request
|
|
478
|
+
// keeps them in step, and an abandoned /tree branch takes both away.
|
|
479
|
+
remember(event.toolCallId, shown);
|
|
495
480
|
});
|
|
496
481
|
}
|
|
@@ -57,6 +57,8 @@ import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
|
|
|
57
57
|
import { forcedReasoningLevel } from "../lib/model-policy.ts";
|
|
58
58
|
import { captureRequest, extendPayload, LAST_REQUEST_CHANNEL, LastExchange, lastCovered, type RequestCapture, replayOutputCap } from "../lib/request-replay.ts";
|
|
59
59
|
import { withoutSystemMessages } from "../lib/side-call.ts";
|
|
60
|
+
import { followCoveredMessages } from "../lib/covered-messages.ts";
|
|
61
|
+
import { LEDGER_SENT_CHANNEL, type LedgerSent } from "../lib/ledger/plan.ts";
|
|
60
62
|
import { claudeCodeCut, type CutEntry, doomedMessages } from "./cut.ts";
|
|
61
63
|
import { fitToBudget, replayFits, withoutUsage } from "./fit.ts";
|
|
62
64
|
import { withoutThinking } from "./kept-thinking.ts";
|
|
@@ -110,25 +112,26 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
110
112
|
* (findings §27).
|
|
111
113
|
*/
|
|
112
114
|
let captured: { messages: AgentMessage[]; systemPrompt: string; model: string | undefined } | undefined;
|
|
113
|
-
pi
|
|
114
|
-
captured = { messages
|
|
115
|
+
followCoveredMessages(pi, (messages, ctx) => {
|
|
116
|
+
captured = { messages, systemPrompt: ctx.getSystemPrompt(), model: modelKey(ctx.model) };
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
|
-
* The same request as
|
|
119
|
-
* (lib/request-replay.ts)
|
|
120
|
-
*
|
|
121
|
-
*
|
|
120
|
+
* The same request as it went on the wire, and the reply that answered it
|
|
121
|
+
* (lib/request-replay.ts): the body the ledger's payload hook publishes once
|
|
122
|
+
* it has spliced and validated it, so it is final; it is published for the
|
|
123
|
+
* btw and recap side calls too.
|
|
122
124
|
*/
|
|
123
125
|
const exchange = new LastExchange<AgentMessage & { role: string }>();
|
|
124
126
|
const publish = (capture: ReturnType<typeof captureRequest>) => {
|
|
125
127
|
exchange.setCapture(capture);
|
|
126
128
|
pi.events.emit(LAST_REQUEST_CHANNEL, capture);
|
|
127
129
|
};
|
|
128
|
-
pi.on(
|
|
130
|
+
pi.events.on(LEDGER_SENT_CHANNEL, (data) => {
|
|
131
|
+
const sent = data as LedgerSent;
|
|
129
132
|
// The context this body was built from ends where a fork's own messages start.
|
|
130
133
|
const covers = captured ? lastCovered(captured.messages as { role: string; timestamp?: number }[]) : undefined;
|
|
131
|
-
publish(captureRequest(
|
|
134
|
+
publish(captureRequest(sent.model, sent.payload, covers, sent.sessionId));
|
|
132
135
|
});
|
|
133
136
|
pi.on("message_end", (event) => {
|
|
134
137
|
exchange.noteMessage(event.message as AgentMessage & { role: string });
|
|
@@ -32,17 +32,31 @@ function finite(n: number | null | undefined): number | undefined {
|
|
|
32
32
|
* from; `left` takes the size now and returns what remains of the budget. The
|
|
33
33
|
* value never rises within a turn (as CC's does not), the first known size
|
|
34
34
|
* becomes the baseline when the turn started without an estimate, and an
|
|
35
|
-
* unknown size repeats the last value.
|
|
35
|
+
* unknown size repeats the last value. A compaction inside the turn shrinks
|
|
36
|
+
* the context below the baseline, so `rebase` measures growth from the
|
|
37
|
+
* smaller context on top of what the turn had already spent; without it the
|
|
38
|
+
* countdown stood still until the context regrew past the turn's start.
|
|
36
39
|
*/
|
|
37
40
|
export class TurnBudget {
|
|
38
41
|
private start: number | undefined;
|
|
42
|
+
/** Spent since `start`. */
|
|
39
43
|
private spent = 0;
|
|
44
|
+
/** Spent before the last rebase. */
|
|
45
|
+
private banked = 0;
|
|
40
46
|
|
|
41
47
|
constructor(readonly budget: number) {}
|
|
42
48
|
|
|
43
49
|
startTurn(contextTokens: number | null | undefined): void {
|
|
44
50
|
this.start = finite(contextTokens);
|
|
45
51
|
this.spent = 0;
|
|
52
|
+
this.banked = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Measure from `contextTokens` (or the next known size) on, keeping what the turn spent so far. */
|
|
56
|
+
rebase(contextTokens: number | null | undefined): void {
|
|
57
|
+
this.banked += this.spent;
|
|
58
|
+
this.spent = 0;
|
|
59
|
+
this.start = finite(contextTokens);
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
left(contextTokens: number | null | undefined): number {
|
|
@@ -51,7 +65,7 @@ export class TurnBudget {
|
|
|
51
65
|
if (this.start === undefined) this.start = now;
|
|
52
66
|
this.spent = Math.max(this.spent, now - this.start);
|
|
53
67
|
}
|
|
54
|
-
return Math.max(0, this.budget - this.spent);
|
|
68
|
+
return Math.max(0, this.budget - this.banked - this.spent);
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
71
|
|
|
@@ -9,24 +9,21 @@
|
|
|
9
9
|
* (CONTEXT_ORDER.totalTokens), so it joins the mid-conversation system
|
|
10
10
|
* message where there is one;
|
|
11
11
|
* - every later user message: the constant budget as a raw marker, a `sticky-append`
|
|
12
|
-
* reminder keyed once per session (`since: 0
|
|
13
|
-
*
|
|
12
|
+
* reminder keyed once per session (`since: 0`: every user message a request
|
|
13
|
+
* sends carries it, the first prompt excepted);
|
|
14
14
|
* - every tool result: the countdown, a RAW `last-append` one-shot queued at
|
|
15
|
-
* `tool_result` time and
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* It emits only at runtime, never at load, so the "listeners load first" rule
|
|
21
|
-
* for the bus is not at stake. The block is bound to its call's id: pi runs a
|
|
22
|
-
* parallel batch's `tool_result` hooks concurrently, and unbound, the first
|
|
23
|
-
* result would take every queued countdown and the others none;
|
|
15
|
+
* `tool_result` time and bound to its call's id, so it rides that result on
|
|
16
|
+
* the next request (pi runs a parallel batch's `tool_result` hooks
|
|
17
|
+
* concurrently; unbound, every countdown would land on the batch's last
|
|
18
|
+
* result). The request that carries it records it (extensions/ledger), so no
|
|
19
|
+
* later request re-renders it;
|
|
24
20
|
* - the system prompt, before the gitStatus block — appended by the
|
|
25
21
|
* system-prompt extension from the same pure module.
|
|
26
22
|
*
|
|
27
23
|
* A turn starts at the first `agent_start` after a settle (whoever opened it: a
|
|
28
24
|
* prompt or a harness notification) and its baseline is pi's context estimate
|
|
29
|
-
* at that moment. CC_TOTAL_TOKENS=0 turns
|
|
25
|
+
* at that moment; a compaction rebases it (budget.ts). CC_TOTAL_TOKENS=0 turns
|
|
26
|
+
* all three off.
|
|
30
27
|
*/
|
|
31
28
|
|
|
32
29
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -47,14 +44,12 @@ export default function contextBudgetExtension(pi: ExtensionAPI) {
|
|
|
47
44
|
if (!enabled()) return;
|
|
48
45
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
49
46
|
text: totalTokensBlock(budget.budget),
|
|
50
|
-
scope: "every-turn",
|
|
51
47
|
key: FIRST_PROMPT_KEY,
|
|
52
48
|
placement: "first-prepend",
|
|
53
49
|
order: CONTEXT_ORDER.totalTokens,
|
|
54
50
|
} satisfies ReminderPayload);
|
|
55
51
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
56
52
|
text: totalTokensBlock(budget.budget),
|
|
57
|
-
scope: "every-turn",
|
|
58
53
|
key: USER_MESSAGE_KEY,
|
|
59
54
|
placement: "sticky-append",
|
|
60
55
|
raw: true,
|
|
@@ -72,6 +67,12 @@ export default function contextBudgetExtension(pi: ExtensionAPI) {
|
|
|
72
67
|
pi.on("agent_settled", () => {
|
|
73
68
|
busy = false;
|
|
74
69
|
});
|
|
70
|
+
// A compaction (an auto one mid-turn, an overflow retry) drops the context
|
|
71
|
+
// below the turn's baseline; pi knows its new size only after the next reply,
|
|
72
|
+
// so the first size it reports becomes the baseline.
|
|
73
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
74
|
+
budget.rebase(ctx.getContextUsage()?.tokens);
|
|
75
|
+
});
|
|
75
76
|
|
|
76
77
|
pi.on("tool_result", (event, ctx) => {
|
|
77
78
|
if (!enabled()) return;
|