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
|
@@ -1,230 +1,202 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* system-reminder extension
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* system-reminder extension, the renderer of the record-once request path
|
|
3
|
+
* (lib/reminders.ts has the placement contract, extensions/ledger the design).
|
|
4
|
+
* It owns the live reminder state every other extension feeds over
|
|
5
|
+
* `one-code:system-reminder`: the `first-prepend` stack and the open sticky
|
|
6
|
+
* states. A one-shot is persisted when emitted, as a `one-code:pending` entry,
|
|
7
|
+
* and delivered from the ledger's outbox.
|
|
8
|
+
*
|
|
9
|
+
* Its `context` hook runs right after the ledger's, which has cut the request
|
|
10
|
+
* to its tail and published the plan. It renders onto that tail only: the
|
|
11
|
+
* stack when the request opens an epoch, the sticky states, the pending
|
|
12
|
+
* one-shots, and the blocks capability owners add on RENDER_CHANNEL (live
|
|
13
|
+
* minus what the epoch told). It writes into the plan which pending items the
|
|
14
|
+
* request carries and what its new blocks tell, and the ledger records both
|
|
15
|
+
* with the request. Nothing is snapshotted or restored: everything above the
|
|
16
|
+
* tail is the record.
|
|
9
17
|
*
|
|
10
18
|
* On a model that takes a mid-conversation system message (lib/system-role.ts),
|
|
11
|
-
* its `before_provider_request` hook
|
|
12
|
-
* first user message into one system message after it
|
|
13
|
-
*
|
|
19
|
+
* its `before_provider_request` hook lifts the stack's session-fact blocks off
|
|
20
|
+
* the first user message into one system message after it (an epoch's request
|
|
21
|
+
* only), and shapes the tail's `<total_tokens>` lines (lib/turn-budget-layout.ts).
|
|
14
22
|
*/
|
|
15
23
|
|
|
24
|
+
import { randomUUID } from "node:crypto";
|
|
16
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
26
|
import { CLEAR_AT_BETA, MID_CONVERSATION_SYSTEM_BETA, reseatMessageMark, withBetas } from "../lib/anthropic-payload.ts";
|
|
18
|
-
import {
|
|
19
|
-
appendReminderBlocks,
|
|
20
|
-
framedReminderText,
|
|
21
|
-
injectReminders,
|
|
22
|
-
movesToSystemRole,
|
|
23
|
-
openingUserAnchor,
|
|
24
|
-
REMINDER_CHANNEL,
|
|
25
|
-
ReminderQueue,
|
|
26
|
-
type ReminderEntry,
|
|
27
|
-
type ReminderPayload,
|
|
28
|
-
tailAnchor,
|
|
29
|
-
} from "../lib/reminders.ts";
|
|
30
|
-
import {
|
|
31
|
-
CONTEXT_BASELINE_CHANNEL, CONTEXT_RESTORE_CHANNEL, CONTEXT_STACK_ENTRY, CONTEXT_STATE_ENTRY, CONTEXT_FACTS_REFRESH_CHANNEL,
|
|
32
|
-
contextStackOnBranch, RESTORED_STACK_KEYS, LIVE_CONTEXT_KEYS,
|
|
33
|
-
type ContextBaseline, type ContextRestoreRequest, type ContextStackSnapshot, type ContextFactsRefresh,
|
|
34
|
-
} from "../lib/context-stack.ts";
|
|
35
|
-
import { CONTEXT_FACT_KEYS, DATE_CHANGE_KEY, RESUME_FACTS_KEY } from "../lib/context-facts.ts";
|
|
36
|
-
import { claudeFamily } from "../lib/model-tier.ts";
|
|
37
27
|
import { emptyToolResultContent } from "../lib/empty-tool-result.ts";
|
|
28
|
+
import { foldBranch } from "../lib/ledger/fold.ts";
|
|
29
|
+
import { LEDGER_PLAN_CHANNEL, type RequestPlan } from "../lib/ledger/plan.ts";
|
|
30
|
+
import { type EntryLike, makeRecord, type PendingRecordData, RECORD, stateOnBranch } from "../lib/ledger/records.ts";
|
|
31
|
+
import { recordState } from "../lib/ledger/state.ts";
|
|
32
|
+
import { contentHash } from "../lib/ledger/wire.ts";
|
|
33
|
+
import { claudeFamily } from "../lib/model-tier.ts";
|
|
34
|
+
import { extensionVersion } from "../lib/package-version.ts";
|
|
35
|
+
import { LiveReminders, type OneShot, REMINDER_CHANNEL, RENDER_CHANNEL, type RenderRequest, renderReminders, type ReminderPayload, stickyFact } from "../lib/reminders.ts";
|
|
36
|
+
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
38
37
|
import { instructionRole, messagesKey, type MovedBlock, systemRoleLayout, wireShape, withSystemRoleContext } from "../lib/system-role.ts";
|
|
39
38
|
import { resolveCountdowns, withTurnBudgetMessages } from "../lib/turn-budget-layout.ts";
|
|
40
39
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
/** Whether requests carry the `<total_tokens>` budget lines (`CC_TOTAL_TOKENS=0` turns them off). */
|
|
41
|
+
const budgetMarkers = () => process.env.CC_TOTAL_TOKENS !== "0";
|
|
42
|
+
|
|
43
|
+
/** A pending entry as a one-shot to place. */
|
|
44
|
+
function oneShotOf(item: PendingRecordData): OneShot {
|
|
45
|
+
return {
|
|
46
|
+
id: item.id,
|
|
47
|
+
text: item.text,
|
|
48
|
+
placement: item.placement ?? "last-append",
|
|
49
|
+
...(item.raw ? { raw: true } : {}),
|
|
50
|
+
...(item.toolCallId ? { toolCallId: item.toolCallId } : {}),
|
|
51
|
+
...(item.promptTimestamp !== undefined ? { promptTimestamp: item.promptTimestamp } : {}),
|
|
52
|
+
...(item.once ? { once: true } : {}),
|
|
53
|
+
...(item.orTail ? { orTail: true } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The `one-code:state` key prefix of a sticky state's recorded start. */
|
|
58
|
+
const STICKY_START_PREFIX = "sticky-start:";
|
|
49
59
|
|
|
50
60
|
export default function systemReminderExtension(pi: ExtensionAPI) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
82
|
-
const branch = contextStackOnBranch(ctx.sessionManager.getBranch());
|
|
83
|
-
stored = branch !== undefined;
|
|
84
|
-
lastState = branch ? JSON.stringify({ version: 1, sticky: branch.sticky, pinned: branch.pinned ?? [], baselines: branch.baselines }) : "";
|
|
85
|
-
lastStack = branch ? JSON.stringify(branch.stack) : "";
|
|
86
|
-
comparedQueue = -1;
|
|
87
|
-
if (branch) reminderQueue.restoreBranch(branch, CONTEXT_FACT_KEYS);
|
|
88
|
-
});
|
|
61
|
+
const alive = sessionAlive(pi);
|
|
62
|
+
const by = extensionVersion();
|
|
63
|
+
const live = new LiveReminders();
|
|
64
|
+
/** The plan the ledger published for the request being rendered. */
|
|
65
|
+
let plan: RequestPlan | undefined;
|
|
66
|
+
/** One-shots emitted before the session started (an extension's load): persisted at session_start. */
|
|
67
|
+
let early: ReminderPayload[] | undefined = [];
|
|
68
|
+
/** Without the ledger (a test harness, a session that does not load it): the pending ids this instance delivered. */
|
|
69
|
+
const deliveredHere = new Set<string>();
|
|
70
|
+
|
|
71
|
+
const persist = (payload: ReminderPayload) => {
|
|
72
|
+
const data = makeRecord("pending", by, {
|
|
73
|
+
id: randomUUID(),
|
|
74
|
+
text: payload.text as string,
|
|
75
|
+
placement: payload.placement === "user-prepend" ? "user-prepend" : "last-append",
|
|
76
|
+
...(payload.key ? { key: payload.key } : {}),
|
|
77
|
+
...(payload.raw ? { raw: true } : {}),
|
|
78
|
+
...(payload.toolCallId ? { toolCallId: payload.toolCallId } : {}),
|
|
79
|
+
...(payload.promptTimestamp !== undefined ? { promptTimestamp: payload.promptTimestamp } : {}),
|
|
80
|
+
...(payload.once ? { once: true } : {}),
|
|
81
|
+
...(payload.orTail ? { orTail: true } : {}),
|
|
82
|
+
...(payload.untilCompaction ? { untilCompaction: true } : {}),
|
|
83
|
+
...(payload.observations?.length ? { observations: payload.observations } : {}),
|
|
84
|
+
...(payload.notified?.length ? { notified: payload.notified } : {}),
|
|
85
|
+
});
|
|
86
|
+
pi.appendEntry(RECORD.pending, data);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** The session's branch: the sticky states' starts it recorded. */
|
|
90
|
+
let branchOf: (() => EntryLike[]) | undefined;
|
|
89
91
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
92
|
+
* When each sticky state started, as the branch recorded it. An owner that
|
|
93
|
+
* runs again (a resume, /reload, /tree) emits its state without a start, so
|
|
94
|
+
* the renderer takes the branch's start for the same key and text: an epoch
|
|
95
|
+
* that renders the conversation again (a model switch) then places the block
|
|
96
|
+
* where the original requests did, and its first delivery stays told. A
|
|
97
|
+
* removal records the run's end (null), so nothing carries past it.
|
|
93
98
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/** `capturedAt`: the queue version when `stack` was taken. Unchanged since the last comparison, the snapshot is too. */
|
|
98
|
-
const persistSnapshot = (stack: ReminderEntry[] = reminderQueue.persistentEntries("first-prepend"), capturedAt = reminderQueue.version) => {
|
|
99
|
-
const version = reminderQueue.version;
|
|
100
|
-
if (stored && capturedAt === version && version === comparedQueue && baselineChanges === comparedBaselines) return;
|
|
101
|
-
comparedQueue = capturedAt === version ? version : -1;
|
|
102
|
-
comparedBaselines = baselineChanges;
|
|
103
|
-
const state = { version: 1 as const, sticky: reminderQueue.persistentEntries("sticky-append"), pinned: reminderQueue.persistentPins(), baselines };
|
|
104
|
-
const serialized = JSON.stringify(state);
|
|
105
|
-
const serializedStack = JSON.stringify(stack);
|
|
106
|
-
if (!stored) {
|
|
107
|
-
pi.appendEntry(CONTEXT_STACK_ENTRY, structuredClone({ ...state, stack }));
|
|
108
|
-
stored = true;
|
|
109
|
-
} else if (serialized !== lastState || serializedStack !== lastStack) {
|
|
110
|
-
// Compaction and explicit capability switches start a new prefix.
|
|
111
|
-
pi.appendEntry(CONTEXT_STATE_ENTRY, structuredClone({ ...state, ...(serializedStack !== lastStack ? { stack } : {}) }));
|
|
112
|
-
}
|
|
113
|
-
lastState = serialized;
|
|
114
|
-
lastStack = serializedStack;
|
|
99
|
+
const stickyStart = (key: string): { since: number; hash: string } | undefined => {
|
|
100
|
+
const stored = branchOf ? (stateOnBranch(branchOf(), `${STICKY_START_PREFIX}${key}`) as { since?: unknown; hash?: unknown } | undefined) : undefined;
|
|
101
|
+
return typeof stored?.since === "number" && typeof stored.hash === "string" ? { since: stored.since, hash: stored.hash } : undefined;
|
|
115
102
|
};
|
|
116
|
-
pi.events.on(CONTEXT_FACTS_REFRESH_CHANNEL, (data) => {
|
|
117
|
-
const refresh = data as ContextFactsRefresh;
|
|
118
|
-
reminderQueue.replaceFirstPrepend(CONTEXT_FACT_KEYS, refresh.entries);
|
|
119
|
-
reminderQueue.cancelPending([DATE_CHANGE_KEY, RESUME_FACTS_KEY]);
|
|
120
|
-
baselines["claude-context"] = structuredClone(refresh.baseline);
|
|
121
|
-
baselineChanges++;
|
|
122
|
-
// /compact may be the last action before exit. Persist now, not just on
|
|
123
|
-
// the next request, so a resume cannot revive the pre-compaction facts.
|
|
124
|
-
persistSnapshot();
|
|
125
|
-
});
|
|
126
|
-
/** The blocks the last `context` pass placed for the system message; the payload hook moves them. */
|
|
127
|
-
let moved: MovedBlock[] = [];
|
|
128
|
-
/** The tool results' countdowns the last `context` pass lifted, by call id (lib/turn-budget-layout.ts). */
|
|
129
|
-
let countdowns = new Map<string, number>();
|
|
130
103
|
|
|
131
104
|
pi.events.on(REMINDER_CHANNEL, (data) => {
|
|
132
|
-
|
|
105
|
+
let payload = data as ReminderPayload | undefined;
|
|
133
106
|
if (!payload) return;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
once: payload.once,
|
|
148
|
-
toolCallId: payload.toolCallId,
|
|
149
|
-
});
|
|
107
|
+
const sticky = payload.placement === "sticky-append" && !payload.remove && typeof payload.text === "string";
|
|
108
|
+
const key = payload.key ?? payload.text ?? "";
|
|
109
|
+
const recorded = sticky ? stickyStart(key) : undefined;
|
|
110
|
+
if (sticky && payload.since === undefined && recorded?.hash === contentHash(payload.text)) payload = { ...payload, since: recorded.since };
|
|
111
|
+
// A removal ends the state's run: switched on again, even with the same
|
|
112
|
+
// text, it is a new state that rides the switching call's result, not the
|
|
113
|
+
// old run's start, whose first delivery the branch already told.
|
|
114
|
+
if (payload.remove && payload.key && branchOf && alive() && stickyStart(payload.key)) recordState(pi, `${STICKY_START_PREFIX}${payload.key}`, null);
|
|
115
|
+
const applied = live.apply(payload);
|
|
116
|
+
if (sticky && applied === "live" && branchOf && alive()) {
|
|
117
|
+
const entry = live.stickyEntries().find((state) => state.key === key);
|
|
118
|
+
const hash = entry ? contentHash(entry.text) : undefined;
|
|
119
|
+
if (entry && entry.since > 0 && (recorded?.since !== entry.since || recorded?.hash !== hash)) recordState(pi, `${STICKY_START_PREFIX}${key}`, { since: entry.since, hash });
|
|
150
120
|
}
|
|
121
|
+
if (applied !== "one-shot") return;
|
|
122
|
+
if (early) early.push(payload);
|
|
123
|
+
else if (alive()) persist(payload);
|
|
124
|
+
});
|
|
125
|
+
pi.on("session_start", (_event, ctx) => {
|
|
126
|
+
const sessionManager = ctx?.sessionManager;
|
|
127
|
+
branchOf = sessionManager ? () => sessionManager.getBranch() as EntryLike[] : undefined;
|
|
128
|
+
const queued = early ?? [];
|
|
129
|
+
early = undefined;
|
|
130
|
+
for (const payload of queued) persist(payload);
|
|
131
|
+
});
|
|
132
|
+
pi.events.on(LEDGER_PLAN_CHANNEL, (data) => {
|
|
133
|
+
plan = data as RequestPlan;
|
|
151
134
|
});
|
|
152
135
|
|
|
153
|
-
// A
|
|
154
|
-
//
|
|
155
|
-
// re-caches the message (a transient block would vanish on the next request
|
|
156
|
-
// and cost the result and everything after it). Runs before the hooks
|
|
157
|
-
// extension's PostToolUse in load order, so a hook that replaces the whole
|
|
158
|
-
// result would drop these appended blocks — the hooks extension re-attaches
|
|
159
|
-
// the trailing reminder run when it applies a replacement (review M3,
|
|
160
|
-
// applyPostToolUseOutcome / isAppendedReminderText).
|
|
136
|
+
// A tool's empty result is normalized before anything else reads it. Read's
|
|
137
|
+
// raw bytes stay for file-tracker's empty-file warning.
|
|
161
138
|
pi.on("tool_result", (event) => {
|
|
162
|
-
// Normalize the tool's result before reminders make an empty result look
|
|
163
|
-
// nonempty. Read's raw bytes stay for file-tracker's empty-file warning.
|
|
164
139
|
const replacement = emptyToolResultContent(event.content, event.toolName);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const content = replacement ?? event.content;
|
|
168
|
-
return {
|
|
169
|
-
content: entries.length ? appendReminderBlocks(content, entries) : content,
|
|
170
|
-
...(event.structuredContent !== undefined ? { structuredContent: event.structuredContent } : {}),
|
|
171
|
-
};
|
|
140
|
+
if (!replacement) return;
|
|
141
|
+
return { content: replacement, ...(event.structuredContent !== undefined ? { structuredContent: event.structuredContent } : {}) };
|
|
172
142
|
});
|
|
173
143
|
|
|
174
|
-
/** The
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!messages.some((m) => ANCHOR_ROLES.has(m.role))) return messages;
|
|
181
|
-
// A one-shot that arrived after the last tool result was stored (a mode
|
|
182
|
-
// change while idle, a deferred-tool miss whose call had no tool_result
|
|
183
|
-
// hook) rides this request's tail — and stays pinned there afterwards.
|
|
184
|
-
if (reminderQueue.hasPendingOneShots) {
|
|
185
|
-
const anchor = tailAnchor(messages);
|
|
186
|
-
if (anchor) reminderQueue.pin(anchor, "last-append", messages);
|
|
187
|
-
}
|
|
188
|
-
// A local-command breadcrumb rides the prompt that opens a request, before
|
|
189
|
-
// the user's text (Claude Code's placement), and stays there. Mid-turn —
|
|
190
|
-
// the request ends in a tool result — it waits for the next prompt.
|
|
191
|
-
if (reminderQueue.hasPending("user-prepend")) {
|
|
192
|
-
const anchor = openingUserAnchor(messages);
|
|
193
|
-
if (anchor) reminderQueue.pin(anchor, "user-prepend");
|
|
194
|
-
}
|
|
195
|
-
// Capture first-prepend before draining (one may be next-turn), then save
|
|
196
|
-
// after drain resolves sticky anchors. Store logical entries before layout,
|
|
197
|
-
// including systemRoleOnly blocks even on a model that cannot carry them.
|
|
198
|
-
reminderQueue.finishRestore();
|
|
199
|
-
const capturedAt = reminderQueue.version;
|
|
200
|
-
const stack = reminderQueue.persistentEntries("first-prepend");
|
|
201
|
-
const drained = reminderQueue.drain(messages);
|
|
202
|
-
persistSnapshot(stack, capturedAt);
|
|
203
|
-
// A block meant only for the system message is left out where there is none.
|
|
204
|
-
const reminders = drained.filter((entry) => layout || !entry.systemRoleOnly);
|
|
205
|
-
if (layout) {
|
|
206
|
-
moved = reminders
|
|
207
|
-
.filter((entry) => entry.placement === "first-prepend" && movesToSystemRole(entry.order))
|
|
208
|
-
.sort((a, b) => a.order - b.order)
|
|
209
|
-
.map((entry) => ({ framed: framedReminderText(entry), inner: entry.text }));
|
|
210
|
-
}
|
|
211
|
-
return injectReminders(messages, reminders);
|
|
212
|
-
};
|
|
144
|
+
/** The stack blocks the last `context` pass placed for the system message; the payload hook moves them. */
|
|
145
|
+
let moved: MovedBlock[] = [];
|
|
146
|
+
/** Whether the last `context` pass rendered an epoch: only then does the first user message carry the stack. */
|
|
147
|
+
let epochRequest = true;
|
|
148
|
+
/** The tool results' countdowns the last `context` pass lifted, by call id (lib/turn-budget-layout.ts). */
|
|
149
|
+
let countdowns = new Map<string, number>();
|
|
213
150
|
|
|
214
151
|
pi.on("context", (event, ctx) => {
|
|
215
|
-
|
|
216
|
-
|
|
152
|
+
const current = plan;
|
|
153
|
+
plan = undefined;
|
|
217
154
|
const layout = systemRoleLayout(ctx.model);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
155
|
+
const epoch = current?.epoch ?? true;
|
|
156
|
+
const fold = current?.fold ?? foldBranch(ctx.sessionManager.getBranch() as EntryLike[]);
|
|
157
|
+
|
|
158
|
+
// Owners add nothing on an epoch (the live stack is rendered), so the told maps need no reset here.
|
|
159
|
+
const request: RenderRequest = { epoch, told: fold.told, modes: fold.modes, toldBlocks: fold.blocks, model: ctx.model, blocks: [] };
|
|
160
|
+
pi.events.emit(RENDER_CHANNEL, request);
|
|
161
|
+
const oneShots: OneShot[] = [
|
|
162
|
+
...fold.pending.filter((item) => !deliveredHere.has(item.id)).map(oneShotOf),
|
|
163
|
+
...request.blocks.map((block) => ({ text: block.text, placement: "last-append" as const, ...(block.facts ? { facts: block.facts } : {}) })),
|
|
164
|
+
];
|
|
165
|
+
const stack = live.stackEntries();
|
|
166
|
+
// A sticky state's first delivery is told, so a failed request's revokes and an epoch renders it again.
|
|
167
|
+
const toldBlocks = epoch ? new Map<string, string>() : fold.blocks;
|
|
168
|
+
const sticky = live.stickyEntries().map((entry) => {
|
|
169
|
+
const fact = stickyFact(entry);
|
|
170
|
+
return { ...entry, fact, delivered: toldBlocks.get(fact.key) === fact.hash };
|
|
171
|
+
});
|
|
172
|
+
const rendered = renderReminders(event.messages, { epoch, layout: layout !== undefined, stack, sticky, oneShots });
|
|
173
|
+
const stickyFacts = sticky.filter((entry) => !entry.delivered && rendered.stickyPlaced.includes(entry.key)).map((entry) => entry.fact);
|
|
174
|
+
|
|
175
|
+
const consumed = rendered.placed.flatMap((shot) => (shot.id ? [shot.id] : []));
|
|
176
|
+
// Each fact with the text that carries it: a repair that drops the text drops the fact.
|
|
177
|
+
const carried = [
|
|
178
|
+
...(epoch && rendered.stackPlaced ? stack.filter((entry) => layout !== undefined || !entry.systemRoleOnly).map((entry) => ({ text: entry.text, facts: entry.facts ?? [] })) : []),
|
|
179
|
+
...rendered.placed.map((shot) => ({ text: shot.text, facts: shot.facts ?? [] })),
|
|
180
|
+
...sticky.filter((entry) => stickyFacts.includes(entry.fact)).map((entry) => ({ text: entry.text, facts: [entry.fact] })),
|
|
181
|
+
].filter((block) => block.facts.length > 0);
|
|
182
|
+
if (current) {
|
|
183
|
+
current.consumed.push(...consumed);
|
|
184
|
+
current.carried.push(...carried);
|
|
185
|
+
} else {
|
|
186
|
+
for (const id of consumed) deliveredHere.add(id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
moved = rendered.moved;
|
|
190
|
+
epochRequest = epoch;
|
|
191
|
+
// Each tool result's <total_tokens> countdown, lifted for a system message
|
|
192
|
+
// or framed, while the result's blocks are still separate (lib/turn-budget-layout.ts).
|
|
222
193
|
// With the budget off no result carries one, so a tool's own such line stays its output.
|
|
223
|
-
|
|
224
|
-
const resolved =
|
|
194
|
+
countdowns = new Map();
|
|
195
|
+
const resolved = budgetMarkers() && ctx.model && wireShape(ctx.model.api) ? resolveCountdowns(rendered.messages, layout !== undefined) : undefined;
|
|
225
196
|
if (resolved) countdowns = resolved.left;
|
|
226
|
-
const out = resolved?.messages ?? messages;
|
|
227
|
-
|
|
197
|
+
const out = resolved?.messages ?? rendered.messages;
|
|
198
|
+
const changed = out.length !== event.messages.length || out.some((message, i) => message !== event.messages[i]);
|
|
199
|
+
return changed ? { messages: out } : undefined;
|
|
228
200
|
});
|
|
229
201
|
|
|
230
202
|
pi.on("before_provider_request", (event, ctx) => {
|
|
@@ -239,14 +211,14 @@ export default function systemReminderExtension(pi: ExtensionAPI) {
|
|
|
239
211
|
// gets its clear_at nudge after each tool result.
|
|
240
212
|
const firstParty = model.provider === "anthropic";
|
|
241
213
|
const nudge = layout === "anthropic" && firstParty && claudeFamily(model.id) === "fable";
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (payload === event.payload) return undefined;
|
|
245
|
-
if (shape === "anthropic") {
|
|
214
|
+
payload = withTurnBudgetMessages(payload, { shape, systemRole: layout !== undefined, role, left: countdowns, nudge, markers: budgetMarkers(), stackCarrier: epochRequest }) ?? payload;
|
|
215
|
+
if (payload !== event.payload && shape === "anthropic") {
|
|
246
216
|
const key = messagesKey(shape);
|
|
247
217
|
payload = { ...payload, [key]: reseatMessageMark(payload[key] as Array<{ role?: unknown; content?: unknown }>) };
|
|
248
218
|
}
|
|
249
|
-
|
|
250
|
-
|
|
219
|
+
// The betas go on every request of a model that takes the layout: an
|
|
220
|
+
// earlier request's system messages are part of every later one.
|
|
221
|
+
if (layout === "anthropic" && firstParty) payload = withBetas(payload, nudge ? [MID_CONVERSATION_SYSTEM_BETA, CLEAR_AT_BETA] : [MID_CONVERSATION_SYSTEM_BETA]);
|
|
222
|
+
return payload === event.payload ? undefined : payload;
|
|
251
223
|
});
|
|
252
224
|
}
|