@astrosheep/pi-context 0.25.1 → 0.26.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 +88 -7
- package/dist/build-info.json +2 -2
- package/dist/extension.js +616 -370
- package/dist/src/context/boot.d.ts +24 -0
- package/dist/src/context/boot.js +33 -24
- package/dist/src/context/budget.d.ts +9 -0
- package/dist/src/context/budget.js +19 -15
- package/dist/src/context/context-window.d.ts +41 -0
- package/dist/src/context/context-window.js +16 -1
- package/dist/src/context/prompts.d.ts +20 -0
- package/dist/src/context/prompts.js +1 -1
- package/dist/src/context/reset-artifacts.d.ts +26 -0
- package/dist/src/context/reset-artifacts.js +18 -17
- package/dist/src/context/reset-lifecycle.d.ts +89 -0
- package/dist/src/context/reset-lifecycle.js +103 -75
- package/dist/src/context/runtime.d.ts +3 -0
- package/dist/src/context/runtime.js +53 -21
- package/dist/src/context/thresholds.d.ts +33 -0
- package/dist/src/context/thresholds.js +1 -1
- package/dist/src/dream/cli.d.ts +10 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.d.ts +2 -0
- package/dist/src/dream/doctor.js +6 -2
- package/dist/src/dream/gates.d.ts +10 -0
- package/dist/src/dream/git.d.ts +21 -0
- package/dist/src/dream/lock.d.ts +31 -0
- package/dist/src/dream/runner.d.ts +30 -0
- package/dist/src/dream/settings.d.ts +16 -0
- package/dist/src/history/history-tools.d.ts +2 -0
- package/dist/src/history/history.d.ts +57 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.js +4 -4
- package/dist/src/notes/address.d.ts +26 -0
- package/dist/src/notes/address.js +8 -14
- package/dist/src/notes/constants.d.ts +3 -0
- package/dist/src/notes/constants.js +3 -0
- package/dist/src/notes/context.d.ts +10 -0
- package/dist/src/notes/context.js +33 -0
- package/dist/src/notes/frontmatter.d.ts +46 -0
- package/dist/src/notes/frontmatter.js +10 -5
- package/dist/src/notes/index.d.ts +4 -0
- package/dist/src/notes/index.js +2 -0
- package/dist/src/notes/paths.d.ts +21 -0
- package/dist/src/notes/paths.js +72 -76
- package/dist/src/notes/store.d.ts +94 -0
- package/dist/src/notes/store.js +298 -242
- package/dist/src/pi/notes/adapter.d.ts +12 -0
- package/dist/src/pi/notes/adapter.js +39 -0
- package/dist/src/pi/notes/session-replay.d.ts +16 -0
- package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
- package/dist/src/pi/notes/snapshot.d.ts +33 -0
- package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
- package/dist/src/pi/notes/tools.d.ts +2 -0
- package/dist/src/{notes → pi/notes}/tools.js +24 -21
- package/dist/src/protocol.d.ts +41 -0
- package/dist/src/protocol.js +4 -6
- package/dist/src/session-reader.d.ts +5 -0
- package/dist/src/settings.d.ts +6 -0
- package/dist/src/tool-output.d.ts +101 -0
- package/dist/src/tool-schema.d.ts +17 -0
- package/dist/test/agent-loop.test.d.ts +1 -0
- package/dist/test/agent-loop.test.js +318 -19
- package/dist/test/boot.integration.test.d.ts +1 -0
- package/dist/test/boot.integration.test.js +55 -29
- package/dist/test/budget-settings.integration.test.d.ts +1 -0
- package/dist/test/budget-settings.integration.test.js +8 -7
- package/dist/test/doctor.test.d.ts +1 -0
- package/dist/test/doctor.test.js +10 -2
- package/dist/test/dream-skill.test.d.ts +1 -0
- package/dist/test/dream-skill.test.js +69 -0
- package/dist/test/dream.test.d.ts +1 -0
- package/dist/test/helpers/extension.d.ts +115 -0
- package/dist/test/helpers/extension.js +6 -6
- package/dist/test/helpers/notes.d.ts +6 -0
- package/dist/test/helpers/notes.js +13 -0
- package/dist/test/history.integration.test.d.ts +1 -0
- package/dist/test/notes-library.test.d.ts +1 -0
- package/dist/test/notes-library.test.js +111 -0
- package/dist/test/notes.integration.test.d.ts +1 -0
- package/dist/test/notes.integration.test.js +22 -24
- package/dist/test/notes.test.d.ts +1 -0
- package/dist/test/notes.test.js +137 -7
- package/dist/test/reset-lifecycle.test.d.ts +1 -0
- package/dist/test/reset-lifecycle.test.js +142 -85
- package/docs/architecture.md +8 -8
- package/docs/reset-lifecycle.md +63 -79
- package/package.json +35 -2
- package/playbook.md +33 -32
- package/skills/dream/SKILL.md +12 -0
- package/src/context/boot.ts +44 -25
- package/src/context/budget.ts +25 -17
- package/src/context/context-window.ts +16 -1
- package/src/context/prompts.ts +2 -2
- package/src/context/reset-artifacts.ts +26 -24
- package/src/context/reset-lifecycle.ts +117 -111
- package/src/context/runtime.ts +50 -22
- package/src/context/thresholds.ts +1 -1
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +5 -2
- package/src/index.ts +4 -4
- package/src/notes/address.ts +9 -15
- package/src/notes/constants.ts +3 -0
- package/src/notes/context.ts +40 -0
- package/src/notes/frontmatter.ts +18 -12
- package/src/notes/index.ts +22 -0
- package/src/notes/paths.ts +64 -78
- package/src/notes/store.ts +308 -244
- package/src/pi/notes/adapter.ts +44 -0
- package/src/{notes → pi/notes}/session-replay.ts +3 -3
- package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
- package/src/{notes → pi/notes}/tools.ts +25 -23
- package/src/protocol.ts +5 -6
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type NotesSnapshot } from "../pi/notes/snapshot.js";
|
|
3
|
+
export type IncompleteNotesNotifier = (ctx: ExtensionContext, windowId: string, snapshot: NotesSnapshot) => void;
|
|
4
|
+
/** The boot custom message: identity, notes snapshot, and static protocol; never reset prose. */
|
|
5
|
+
export type BootMessage = {
|
|
6
|
+
readonly customType: string;
|
|
7
|
+
readonly content: string;
|
|
8
|
+
readonly display: false;
|
|
9
|
+
readonly details: {
|
|
10
|
+
windowId: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Acquire one notes snapshot and build the boot custom message for a window. All identity
|
|
15
|
+
* fields are captured synchronously before the first filesystem await.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildBootMessage(ctx: ExtensionContext, windowId: string, previousId: string | undefined, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<BootMessage>;
|
|
18
|
+
/** Persist one hidden boot message without triggering a model turn. */
|
|
19
|
+
export declare function sendBoot(pi: ExtensionAPI, boot: BootMessage): void;
|
|
20
|
+
/**
|
|
21
|
+
* Boot entry point for `session_start` / `session_tree`. Ordinary startup ensures one root
|
|
22
|
+
* boot; a reset marker instead asks reset-artifact repair to complete its persisted tail.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ensureBoot(pi: ExtensionAPI, ctx: ExtensionContext, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<void>;
|
package/dist/src/context/boot.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { notesContextFromPi } from "../pi/notes/adapter.js";
|
|
2
|
+
import { loadNotesSnapshot } from "../pi/notes/snapshot.js";
|
|
3
3
|
import { BOOT_TYPE } from "../protocol.js";
|
|
4
4
|
import { renderBootBlock } from "./prompts.js";
|
|
5
|
-
import { currentReset, isWindowBoot, rootWindowId } from "./context-window.js";
|
|
5
|
+
import { currentReset, currentWindowId, isWindowBoot, rootWindowId } from "./context-window.js";
|
|
6
6
|
import { repairResetTail } from "./reset-artifacts.js";
|
|
7
|
-
/** Render the boot block from
|
|
8
|
-
function bootContent(
|
|
9
|
-
return renderBootBlock({
|
|
10
|
-
agentName: agentSlug(ctx),
|
|
11
|
-
modelName: modelSlug(ctx),
|
|
12
|
-
firstWindowId: rootWindowId(ctx.sessionManager.getSessionId()),
|
|
13
|
-
currentWindowId: currentId,
|
|
14
|
-
previousWindowId: previousId,
|
|
15
|
-
notes,
|
|
16
|
-
});
|
|
7
|
+
/** Render the boot block from one pre-await identity and acquired notes snapshot. */
|
|
8
|
+
function bootContent(agentName, modelName, firstWindowId, currentId, previousId, notes) {
|
|
9
|
+
return renderBootBlock({ agentName, modelName, firstWindowId, currentWindowId: currentId, previousWindowId: previousId, notes });
|
|
17
10
|
}
|
|
18
11
|
/**
|
|
19
|
-
* Acquire one notes snapshot and build the boot custom message for a window.
|
|
20
|
-
*
|
|
12
|
+
* Acquire one notes snapshot and build the boot custom message for a window. All identity
|
|
13
|
+
* fields are captured synchronously before the first filesystem await.
|
|
21
14
|
*/
|
|
22
|
-
export function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
export async function buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent = () => true) {
|
|
16
|
+
const identity = notesContextFromPi(ctx);
|
|
17
|
+
const notes = await loadNotesSnapshot(ctx, undefined, identity);
|
|
18
|
+
if (isCurrent())
|
|
19
|
+
notifyIncompleteNotes?.(ctx, windowId, notes);
|
|
20
|
+
return {
|
|
21
|
+
customType: BOOT_TYPE,
|
|
22
|
+
content: bootContent(identity.agent, identity.model, rootWindowId(identity.sessionId), windowId, previousId, notes),
|
|
23
|
+
display: false,
|
|
24
|
+
details: { windowId },
|
|
25
|
+
};
|
|
26
26
|
}
|
|
27
27
|
/** Persist one hidden boot message without triggering a model turn. */
|
|
28
28
|
export function sendBoot(pi, boot) {
|
|
@@ -31,16 +31,25 @@ export function sendBoot(pi, boot) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Boot entry point for `session_start` / `session_tree`. Ordinary startup ensures one root
|
|
33
33
|
* boot; a reset marker instead asks reset-artifact repair to complete its persisted tail.
|
|
34
|
-
* Boot idempotence lives here: an already-projected root boot or a complete reset tail emits nothing.
|
|
35
34
|
*/
|
|
36
|
-
export function ensureBoot(pi, ctx, notifyIncompleteNotes) {
|
|
35
|
+
export async function ensureBoot(pi, ctx, notifyIncompleteNotes, isCurrent = () => true) {
|
|
36
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
37
37
|
const reset = currentReset(ctx);
|
|
38
|
+
const windowId = reset?.data.windowId ?? rootWindowId(sessionId);
|
|
39
|
+
const stillCurrent = () => isCurrent() &&
|
|
40
|
+
ctx.signal?.aborted !== true &&
|
|
41
|
+
ctx.sessionManager.getSessionId() === sessionId &&
|
|
42
|
+
currentWindowId(ctx) === windowId;
|
|
38
43
|
if (reset) {
|
|
39
|
-
repairResetTail(pi, ctx, reset, notifyIncompleteNotes);
|
|
44
|
+
await repairResetTail(pi, ctx, reset, notifyIncompleteNotes, stillCurrent);
|
|
40
45
|
return;
|
|
41
46
|
}
|
|
42
|
-
const windowId = rootWindowId(ctx.sessionManager.getSessionId());
|
|
43
47
|
if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId)))
|
|
44
48
|
return;
|
|
45
|
-
|
|
49
|
+
const boot = await buildBootMessage(ctx, windowId, undefined, notifyIncompleteNotes, stillCurrent);
|
|
50
|
+
if (!stillCurrent())
|
|
51
|
+
return;
|
|
52
|
+
if (ctx.sessionManager.buildSessionProjection().messages.some((message) => isWindowBoot(message, windowId)))
|
|
53
|
+
return;
|
|
54
|
+
sendBoot(pi, boot);
|
|
46
55
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ExtensionAPI, type ExtensionContext, type SessionBoundaryDraft, type SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/** Remaining tokens in the provider's active window, or null without a usable estimate. */
|
|
3
|
+
export declare function remainingTokens(ctx: Pick<ExtensionContext, "sessionManager" | "getContextUsage" | "model">): number | null;
|
|
4
|
+
export declare function registerBudget(pi: ExtensionAPI, isEnabled: () => boolean, settingsManager?: SettingsManager, onCloseOut?: (windowId: string) => void): {
|
|
5
|
+
automaticResetEnabled: (ctx: ExtensionContext) => boolean;
|
|
6
|
+
hardReserveDue: (ctx: ExtensionContext) => boolean;
|
|
7
|
+
consumeTurnEnd: (ctx: ExtensionContext) => SessionBoundaryDraft[];
|
|
8
|
+
clear: () => void;
|
|
9
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@earendil-works/pi-ai";
|
|
2
2
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
3
|
+
import { GUIDANCE_TYPE, WARNING_CONTENT, WARNING_TYPE } from "../protocol.js";
|
|
4
4
|
import { readThresholdSettings } from "./thresholds.js";
|
|
5
5
|
import { currentWindowId, hasWindowMessage, windowUsage } from "./context-window.js";
|
|
6
6
|
import { tokenBudgetGuidance } from "./prompts.js";
|
|
@@ -10,7 +10,7 @@ export function remainingTokens(ctx) {
|
|
|
10
10
|
const usage = windowUsage(ctx);
|
|
11
11
|
return !usage || usage.tokens === null ? null : Math.max(0, usage.contextWindow - usage.tokens);
|
|
12
12
|
}
|
|
13
|
-
export function registerBudget(pi, isEnabled, settingsManager) {
|
|
13
|
+
export function registerBudget(pi, isEnabled, settingsManager, onCloseOut = () => { }) {
|
|
14
14
|
let cachedPolicy;
|
|
15
15
|
const notifiedWarnings = new Set();
|
|
16
16
|
const resolvePolicy = (ctx) => {
|
|
@@ -33,7 +33,7 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
33
33
|
const automaticResetEnabled = (ctx) => {
|
|
34
34
|
return resolvePolicy(ctx).automatic;
|
|
35
35
|
};
|
|
36
|
-
const
|
|
36
|
+
const hardReserveDue = (ctx) => {
|
|
37
37
|
if (!automaticResetEnabled(ctx))
|
|
38
38
|
return false;
|
|
39
39
|
const usage = windowUsage(ctx);
|
|
@@ -49,8 +49,8 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
49
49
|
if (notice.windowId !== windowId || !hasWindowMessage(ctx, notice.customType))
|
|
50
50
|
continue;
|
|
51
51
|
ctx.ui.notify(notice.customType === WARNING_TYPE
|
|
52
|
-
? "pi-context:
|
|
53
|
-
: "pi-context:
|
|
52
|
+
? "pi-context: Context almost full; close out the current memory window."
|
|
53
|
+
: "pi-context: Context running low; checkpoint your notes soon.", "warning");
|
|
54
54
|
}
|
|
55
55
|
pendingNotices = [];
|
|
56
56
|
};
|
|
@@ -72,7 +72,7 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
72
72
|
clearStaged();
|
|
73
73
|
const windowId = currentWindowId(ctx);
|
|
74
74
|
const drafts = staged.filter((draft) => draft !== undefined && draft.windowId === windowId);
|
|
75
|
-
pendingNotices = drafts.map(({ windowId, customType }) => ({ windowId, customType }));
|
|
75
|
+
pendingNotices = drafts.map(({ windowId, customType, remaining }) => ({ windowId, customType, remaining }));
|
|
76
76
|
return drafts.map((draft) => ({
|
|
77
77
|
type: "custom_message",
|
|
78
78
|
customType: draft.customType,
|
|
@@ -88,7 +88,6 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
88
88
|
// lifecycle point that must discard an uncommitted draft before the next prompt.
|
|
89
89
|
// UI notices follow committed reminders. Aborted requests can retry their drafts
|
|
90
90
|
// without showing the same low-budget notification twice.
|
|
91
|
-
pi.on("turn_start", (_event, ctx) => notifyCommittedReminders(ctx));
|
|
92
91
|
pi.on("agent_settled", (_event, ctx) => {
|
|
93
92
|
notifyCommittedReminders(ctx);
|
|
94
93
|
clearStaged();
|
|
@@ -103,13 +102,16 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
103
102
|
return undefined;
|
|
104
103
|
const windowId = currentWindowId(ctx);
|
|
105
104
|
const { reminder, warning } = thresholdsFor(ctx);
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
if (remaining <= warning && automaticResetEnabled(ctx)) {
|
|
106
|
+
// Re-arm close-out on each eligible request. A prior request may have aborted
|
|
107
|
+
// after persisting the warning, so warning deduplication must not own this state.
|
|
108
|
+
onCloseOut(windowId);
|
|
109
|
+
if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId)
|
|
110
|
+
return undefined;
|
|
111
|
+
// The critical close-out starts at reserve + runway, not at the hard reserve.
|
|
110
112
|
pendingGuidance = undefined;
|
|
111
|
-
const content =
|
|
112
|
-
pendingWarning = { windowId, content };
|
|
113
|
+
const content = WARNING_CONTENT;
|
|
114
|
+
pendingWarning = { windowId, content, remaining };
|
|
113
115
|
const warningMessage = {
|
|
114
116
|
role: "custom",
|
|
115
117
|
customType: WARNING_TYPE,
|
|
@@ -119,13 +121,15 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
119
121
|
};
|
|
120
122
|
return { messages: [..._event.messages, warningMessage] };
|
|
121
123
|
}
|
|
124
|
+
if (hasWindowMessage(ctx, WARNING_TYPE) || pendingWarning?.windowId === windowId)
|
|
125
|
+
return undefined;
|
|
122
126
|
if (hasWindowMessage(ctx, GUIDANCE_TYPE) || pendingGuidance?.windowId === windowId)
|
|
123
127
|
return undefined;
|
|
124
128
|
if (remaining <= reminder) {
|
|
125
129
|
// Persist at turn_end, before any reset drafts. A queued sendMessage could
|
|
126
130
|
// otherwise cross the marker and leak the old window's reminder forward.
|
|
127
131
|
const left = Math.max(0, remaining - warning);
|
|
128
|
-
pendingGuidance = { windowId, content: tokenBudgetGuidance(left) };
|
|
132
|
+
pendingGuidance = { windowId, content: tokenBudgetGuidance(left), remaining };
|
|
129
133
|
}
|
|
130
134
|
return undefined;
|
|
131
135
|
});
|
|
@@ -143,7 +147,7 @@ export function registerBudget(pi, isEnabled, settingsManager) {
|
|
|
143
147
|
}));
|
|
144
148
|
return {
|
|
145
149
|
automaticResetEnabled,
|
|
146
|
-
|
|
150
|
+
hardReserveDue,
|
|
147
151
|
consumeTurnEnd,
|
|
148
152
|
clear: () => { clearStaged(); pendingNotices = []; },
|
|
149
153
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type CustomEntry, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { SessionReader } from "../session-reader.js";
|
|
4
|
+
export type WindowMarker = CustomEntry<{
|
|
5
|
+
windowId: string;
|
|
6
|
+
}> & {
|
|
7
|
+
data: {
|
|
8
|
+
windowId: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export declare function isWindowMarker(entry: SessionEntry): entry is WindowMarker;
|
|
12
|
+
/** Only the active branch can supply a window boundary. */
|
|
13
|
+
export declare function currentReset(ctx: SessionReader): WindowMarker | undefined;
|
|
14
|
+
/** A reset is checkpoint-backed only when its marker directly follows a native compaction. */
|
|
15
|
+
export declare function isCheckpointBackedReset(ctx: SessionReader, marker?: WindowMarker | undefined): boolean;
|
|
16
|
+
/** Mint the durable identity of a session's root history window. */
|
|
17
|
+
export declare function rootWindowId(sessionId: string): string;
|
|
18
|
+
/** Persisted messages in the active window, excluding earlier windows on this branch. */
|
|
19
|
+
export declare function hasWindowMessage(ctx: SessionReader, customType: string): boolean;
|
|
20
|
+
/** The root or latest durable marker on the active branch. */
|
|
21
|
+
export declare function currentWindowId(ctx: SessionReader): string;
|
|
22
|
+
/** The durable window active just before the marker: the last earlier marker, else the root window. */
|
|
23
|
+
export declare function previousWindowId(ctx: SessionReader, markerId: string): string;
|
|
24
|
+
/** Match a provider-facing boot message, optionally by window identity. */
|
|
25
|
+
export declare function isWindowBoot(message: AgentMessage, windowId?: string): boolean;
|
|
26
|
+
/** Match a persisted boot entry by raw identity, even when a later edit hides it from projection. */
|
|
27
|
+
export declare function isWindowBootEntry(entry: SessionEntry, windowId: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The durable marker selects a boot message by identity, never by wall-clock time.
|
|
30
|
+
* The boot is the first conversation message of the window. Folding only its prefix
|
|
31
|
+
* preserves later prompt/tool patches in place, including their cacheable ordering.
|
|
32
|
+
*/
|
|
33
|
+
export declare function projectWindow(messages: AgentMessage[], windowId: string): AgentMessage[];
|
|
34
|
+
/**
|
|
35
|
+
* Root windows are not reset boundaries. A forked session can copy a root boot whose
|
|
36
|
+
* details name the source session; refresh that boot in-place in the provider projection
|
|
37
|
+
* while retaining every user/assistant/tool message from the copied root transcript.
|
|
38
|
+
*/
|
|
39
|
+
export declare function projectRootWindow(messages: AgentMessage[], windowId: string): AgentMessage[];
|
|
40
|
+
/** Usage for the selected window, excluding provider usage recorded before its marker. */
|
|
41
|
+
export declare function windowUsage(ctx: Pick<ExtensionContext, "sessionManager" | "getContextUsage" | "model">): import("@earendil-works/pi-coding-agent").ContextUsage | undefined;
|
|
@@ -18,6 +18,16 @@ export function currentReset(ctx) {
|
|
|
18
18
|
}
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
+
/** A reset is checkpoint-backed only when its marker directly follows a native compaction. */
|
|
22
|
+
export function isCheckpointBackedReset(ctx, marker = currentReset(ctx)) {
|
|
23
|
+
if (!marker?.parentId)
|
|
24
|
+
return false;
|
|
25
|
+
const branch = ctx.sessionManager.getBranch();
|
|
26
|
+
const markerIndex = branch.findIndex((entry) => entry.id === marker.id);
|
|
27
|
+
const checkpoint = branch[markerIndex - 1];
|
|
28
|
+
return markerIndex > 0 && checkpoint?.id === marker.parentId && checkpoint.type === "compaction" &&
|
|
29
|
+
checkpoint.summary === "" && checkpoint.firstKeptEntryId === checkpoint.id;
|
|
30
|
+
}
|
|
21
31
|
/** Mint the durable identity of a session's root history window. */
|
|
22
32
|
export function rootWindowId(sessionId) {
|
|
23
33
|
return `pcw:${sessionId.slice(0, 8)}:root`;
|
|
@@ -99,7 +109,12 @@ export function windowUsage(ctx) {
|
|
|
99
109
|
return undefined;
|
|
100
110
|
const windowId = reset.data.windowId;
|
|
101
111
|
try {
|
|
102
|
-
const
|
|
112
|
+
const canonicalMessages = ctx.sessionManager.buildSessionProjection().messages;
|
|
113
|
+
// New resets already cut the durable Pi projection. Marker slicing is retained only
|
|
114
|
+
// for sessions written before native reset checkpoints existed.
|
|
115
|
+
const messages = isCheckpointBackedReset(ctx, reset)
|
|
116
|
+
? canonicalMessages
|
|
117
|
+
: projectWindow(canonicalMessages, windowId);
|
|
103
118
|
const { tokens } = estimateContextTokens(convertToLlm(messages));
|
|
104
119
|
return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
|
|
105
120
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NotesSnapshot } from "../pi/notes/snapshot.js";
|
|
2
|
+
/**
|
|
3
|
+
* Render a static, once-per-window boot block from explicit data. This function does not read
|
|
4
|
+
* notes or call runtime UI APIs; acquisition belongs to loadNotesSnapshot and its caller.
|
|
5
|
+
*/
|
|
6
|
+
export type BootRenderData = {
|
|
7
|
+
readonly agentName: string;
|
|
8
|
+
readonly modelName: string;
|
|
9
|
+
readonly firstWindowId: string;
|
|
10
|
+
readonly currentWindowId: string;
|
|
11
|
+
readonly previousWindowId?: string;
|
|
12
|
+
readonly notes: NotesSnapshot;
|
|
13
|
+
};
|
|
14
|
+
export declare function renderBootBlock(data: BootRenderData): string;
|
|
15
|
+
/**
|
|
16
|
+
* Codex-equivalent low-budget reminder. The measured remaining count is frozen into
|
|
17
|
+
* the text at the crossing that fires it, so each persisted copy is a snapshot true
|
|
18
|
+
* at write time; get_context_remaining remains the live source for the current figure.
|
|
19
|
+
*/
|
|
20
|
+
export declare function tokenBudgetGuidance(remaining: number): string;
|
|
@@ -59,7 +59,7 @@ function notesIndex(snapshot) {
|
|
|
59
59
|
if (recentNotes.length > 0) {
|
|
60
60
|
const lines = [`You find ${recentNotes.length} crumpled note${recentNotes.length === 1 ? "" : "s"} in your pocket (by prefix, most recent first within each: up to ${POCKET_SESSION_LIMIT} from this session, ${POCKET_PROJECT_LIMIT} from @project, ${POCKET_HUMAN_LIMIT} from @human, ${POCKET_AGENT_LIMIT} from @self, ${POCKET_MODEL_LIMIT} from @model). A note's content never appears here, so its name has to say what the note is about:`];
|
|
61
61
|
for (const row of recentNotes) {
|
|
62
|
-
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.
|
|
62
|
+
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updatedAt, snapshot.openedAt)})`);
|
|
63
63
|
}
|
|
64
64
|
sections.push(lines.join("\n"));
|
|
65
65
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, SessionBoundaryDraft, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type WindowMarker } from "./context-window.js";
|
|
3
|
+
import { type IncompleteNotesNotifier } from "./boot.js";
|
|
4
|
+
export type ResetTailState = {
|
|
5
|
+
readonly boot: boolean;
|
|
6
|
+
readonly continuation: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Match the hidden continuation entry that carries the one reset message. */
|
|
9
|
+
export declare function isWindowContinuationEntry(entry: SessionEntry): boolean;
|
|
10
|
+
/** The single continuation sender: the only reset prose persisted for a window. */
|
|
11
|
+
export declare function sendContinuation(pi: ExtensionAPI): void;
|
|
12
|
+
/**
|
|
13
|
+
* The closed, ordered reset shape: retain-none native checkpoint, marker, matching boot,
|
|
14
|
+
* continuation. The boot snapshot is acquired asynchronously after capturing this identity.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildResetDrafts(ctx: ExtensionContext, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<[SessionBoundaryDraft, SessionBoundaryDraft, SessionBoundaryDraft, SessionBoundaryDraft]>;
|
|
17
|
+
/**
|
|
18
|
+
* Inspect the persisted tail of a reset marker. It reports which reset messages are present
|
|
19
|
+
* only while the tail stays repairable: metadata may follow the marker, but real conversation,
|
|
20
|
+
* a foreign message, a later marker, or a misordered/duplicate reset artifact refuses repair.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inspectResetTail(ctx: ExtensionContext, markerId: string, windowId: string): ResetTailState | undefined;
|
|
23
|
+
/** True once the marker's tail already carries its boot and continuation in a valid order. */
|
|
24
|
+
export declare function resetTailCommitted(ctx: ExtensionContext, markerId: string, windowId: string): boolean;
|
|
25
|
+
/** Emit only the reset artifacts an incomplete tail is missing, in the closed order. */
|
|
26
|
+
export declare function repairResetTail(pi: ExtensionAPI, ctx: ExtensionContext, marker: WindowMarker, notifyIncompleteNotes?: IncompleteNotesNotifier, isCurrent?: () => boolean): Promise<void>;
|
|
@@ -11,31 +11,26 @@ export function sendContinuation(pi) {
|
|
|
11
11
|
pi.sendMessage({ customType: CONTINUATION_TYPE, content: CONTINUATION, display: false }, { triggerTurn: false });
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* The closed, ordered reset shape: marker, matching boot,
|
|
15
|
-
*
|
|
14
|
+
* The closed, ordered reset shape: retain-none native checkpoint, marker, matching boot,
|
|
15
|
+
* continuation. The boot snapshot is acquired asynchronously after capturing this identity.
|
|
16
16
|
*/
|
|
17
|
-
export function buildResetDrafts(ctx, notifyIncompleteNotes) {
|
|
18
|
-
const
|
|
17
|
+
export async function buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent = () => true) {
|
|
18
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
19
|
+
const previousId = currentWindowId(ctx);
|
|
20
|
+
const sessionPrefix = sessionId.slice(0, 8);
|
|
19
21
|
const usedWindowIds = new Set(ctx.sessionManager.getBranch().filter(isWindowMarker).map((entry) => entry.data.windowId));
|
|
20
22
|
let windowId;
|
|
21
23
|
do {
|
|
22
24
|
windowId = `pcw:${sessionPrefix}:${randomUUID().slice(0, 8)}`;
|
|
23
25
|
} while (usedWindowIds.has(windowId));
|
|
24
|
-
const boot = buildBootMessage(ctx, windowId,
|
|
26
|
+
const boot = await buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent);
|
|
25
27
|
return [
|
|
28
|
+
{ type: "compaction", summary: "", firstKeptEntryId: null },
|
|
26
29
|
{ type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
|
|
27
30
|
{ type: "custom_message", customType: BOOT_TYPE, content: boot.content, display: false, details: { windowId } },
|
|
28
31
|
{ type: "custom_message", customType: CONTINUATION_TYPE, content: CONTINUATION, display: false },
|
|
29
32
|
];
|
|
30
33
|
}
|
|
31
|
-
/** Persist the marker and send both hidden reset messages; returns the new window id. */
|
|
32
|
-
export function persistManualReset(pi, ctx, notifyIncompleteNotes) {
|
|
33
|
-
const [marker, boot, continuation] = buildResetDrafts(ctx, notifyIncompleteNotes);
|
|
34
|
-
pi.appendEntry(marker.customType, marker.data);
|
|
35
|
-
pi.sendMessage({ customType: boot.customType, content: boot.content, display: boot.display, details: boot.details }, { triggerTurn: false });
|
|
36
|
-
pi.sendMessage({ customType: continuation.customType, content: continuation.content, display: continuation.display }, { triggerTurn: false });
|
|
37
|
-
return boot.details.windowId;
|
|
38
|
-
}
|
|
39
34
|
/**
|
|
40
35
|
* Inspect the persisted tail of a reset marker. It reports which reset messages are present
|
|
41
36
|
* only while the tail stays repairable: metadata may follow the marker, but real conversation,
|
|
@@ -75,12 +70,18 @@ export function resetTailCommitted(ctx, markerId, windowId) {
|
|
|
75
70
|
return tail?.boot === true && tail.continuation === true;
|
|
76
71
|
}
|
|
77
72
|
/** Emit only the reset artifacts an incomplete tail is missing, in the closed order. */
|
|
78
|
-
export function repairResetTail(pi, ctx, marker, notifyIncompleteNotes) {
|
|
73
|
+
export async function repairResetTail(pi, ctx, marker, notifyIncompleteNotes, isCurrent = () => true) {
|
|
79
74
|
const tail = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
80
75
|
if (!tail || (tail.boot && tail.continuation))
|
|
81
76
|
return;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
const boot = tail.boot ? undefined : await buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes, isCurrent);
|
|
78
|
+
if (!isCurrent())
|
|
79
|
+
return;
|
|
80
|
+
const latest = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
81
|
+
if (!latest)
|
|
82
|
+
return;
|
|
83
|
+
if (!latest.boot && boot)
|
|
84
|
+
sendBoot(pi, boot);
|
|
85
|
+
if (!latest.continuation && (latest.boot || boot))
|
|
85
86
|
sendContinuation(pi);
|
|
86
87
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, SessionBoundaryDraft } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
type BudgetOwner = {
|
|
3
|
+
automaticResetEnabled: (ctx: ExtensionContext) => boolean;
|
|
4
|
+
hardReserveDue: (ctx: ExtensionContext) => boolean;
|
|
5
|
+
consumeTurnEnd: (ctx: ExtensionContext) => SessionBoundaryDraft[];
|
|
6
|
+
clear: () => void;
|
|
7
|
+
};
|
|
8
|
+
type ResetOptions = {
|
|
9
|
+
isEnabled: () => boolean;
|
|
10
|
+
budget: BudgetOwner;
|
|
11
|
+
buildReset: (ctx: ExtensionContext, isCurrent: () => boolean) => SessionBoundaryDraft[] | Promise<SessionBoundaryDraft[]>;
|
|
12
|
+
getLifecycleGeneration?: () => number;
|
|
13
|
+
onResetReady?: (ctx: ExtensionContext, drafts: readonly SessionBoundaryDraft[]) => void;
|
|
14
|
+
};
|
|
15
|
+
export type ResetRequestSource = "manual" | "automatic";
|
|
16
|
+
export type ResetRequest = {
|
|
17
|
+
readonly phase: "none";
|
|
18
|
+
} | {
|
|
19
|
+
readonly phase: "close-out";
|
|
20
|
+
readonly windowId: string;
|
|
21
|
+
readonly source: ResetRequestSource;
|
|
22
|
+
} | {
|
|
23
|
+
readonly phase: "tool-requested";
|
|
24
|
+
readonly windowId: string;
|
|
25
|
+
};
|
|
26
|
+
export type ResetOverflowPhase = "idle" | "pending" | "pending-spent" | "spent";
|
|
27
|
+
export interface ResetControlState {
|
|
28
|
+
readonly request: ResetRequest;
|
|
29
|
+
readonly overflow: ResetOverflowPhase;
|
|
30
|
+
}
|
|
31
|
+
export declare function initialResetControl(): ResetControlState;
|
|
32
|
+
/** Facts for a completed turn. Guard values stay lazy to preserve policy resolution order. */
|
|
33
|
+
export interface ResetTurnEndFacts {
|
|
34
|
+
readonly windowId: string;
|
|
35
|
+
readonly aborted: boolean;
|
|
36
|
+
readonly overflow: boolean;
|
|
37
|
+
readonly failed: boolean;
|
|
38
|
+
readonly enabled: boolean;
|
|
39
|
+
readonly queued: boolean;
|
|
40
|
+
readonly automaticResetEnabled: boolean;
|
|
41
|
+
readonly hardReserveDue: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Facts for the pre-settlement boundary; policy guards stay lazy for the same reason. */
|
|
44
|
+
export interface ResetBeforeSettleFacts {
|
|
45
|
+
readonly windowId: string;
|
|
46
|
+
readonly queued: boolean;
|
|
47
|
+
readonly enabled: boolean;
|
|
48
|
+
readonly automaticResetEnabled: boolean;
|
|
49
|
+
readonly aborted: boolean;
|
|
50
|
+
readonly failed: boolean;
|
|
51
|
+
}
|
|
52
|
+
export type ResetControlEvent = {
|
|
53
|
+
readonly type: "close_out";
|
|
54
|
+
readonly windowId: string;
|
|
55
|
+
readonly source: ResetRequestSource;
|
|
56
|
+
} | {
|
|
57
|
+
readonly type: "tool_request";
|
|
58
|
+
readonly windowId: string;
|
|
59
|
+
} | {
|
|
60
|
+
readonly type: "turn_end";
|
|
61
|
+
readonly facts: ResetTurnEndFacts;
|
|
62
|
+
} | {
|
|
63
|
+
readonly type: "before_settle";
|
|
64
|
+
readonly facts: ResetBeforeSettleFacts;
|
|
65
|
+
} | {
|
|
66
|
+
readonly type: "settled";
|
|
67
|
+
} | {
|
|
68
|
+
readonly type: "abort";
|
|
69
|
+
} | {
|
|
70
|
+
readonly type: "clear";
|
|
71
|
+
};
|
|
72
|
+
export type ResetControlEffect = "none" | "close-out-armed" | "already-pending" | "commit-boundary" | "recover-overflow";
|
|
73
|
+
export interface ResetControlResult {
|
|
74
|
+
readonly state: ResetControlState;
|
|
75
|
+
readonly effect: ResetControlEffect;
|
|
76
|
+
}
|
|
77
|
+
/** Pure reset-control transitions: request phases own close-out, tool commit, and fallback. */
|
|
78
|
+
export declare function reduceResetControl(state: ResetControlState, event: ResetControlEvent): ResetControlResult;
|
|
79
|
+
/**
|
|
80
|
+
* Own close-out requests at Pi's public turn and pre-settlement boundaries. Tool-requested
|
|
81
|
+
* resets commit at turn_end; manual and budget close-outs remain armed across note/tool turns
|
|
82
|
+
* and fall back at a successful agent_before_settle. Overflow recovery remains bounded.
|
|
83
|
+
*/
|
|
84
|
+
export declare function registerResetLifecycle(pi: ExtensionAPI, options: ResetOptions): {
|
|
85
|
+
closeOut(windowId: string, source: ResetRequestSource): ResetControlEffect;
|
|
86
|
+
request(windowId: string): "rollover_already_pending" | "rollover_requested";
|
|
87
|
+
clear: () => void;
|
|
88
|
+
};
|
|
89
|
+
export {};
|