@astrosheep/pi-context 0.25.2 → 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 +613 -367
- 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 +16 -12
- 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 +309 -10
- 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 +19 -11
- 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
|
@@ -24,6 +24,16 @@ export function currentReset(ctx: SessionReader): WindowMarker | undefined {
|
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** A reset is checkpoint-backed only when its marker directly follows a native compaction. */
|
|
28
|
+
export function isCheckpointBackedReset(ctx: SessionReader, marker = currentReset(ctx)): boolean {
|
|
29
|
+
if (!marker?.parentId) return false;
|
|
30
|
+
const branch = ctx.sessionManager.getBranch();
|
|
31
|
+
const markerIndex = branch.findIndex((entry) => entry.id === marker.id);
|
|
32
|
+
const checkpoint = branch[markerIndex - 1];
|
|
33
|
+
return markerIndex > 0 && checkpoint?.id === marker.parentId && checkpoint.type === "compaction" &&
|
|
34
|
+
checkpoint.summary === "" && checkpoint.firstKeptEntryId === checkpoint.id;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
/** Mint the durable identity of a session's root history window. */
|
|
28
38
|
export function rootWindowId(sessionId: string): string {
|
|
29
39
|
return `pcw:${sessionId.slice(0, 8)}:root`;
|
|
@@ -106,7 +116,12 @@ export function windowUsage(ctx: Pick<ExtensionContext, "sessionManager" | "getC
|
|
|
106
116
|
if (!contextWindow) return undefined;
|
|
107
117
|
const windowId = reset.data.windowId;
|
|
108
118
|
try {
|
|
109
|
-
const
|
|
119
|
+
const canonicalMessages = ctx.sessionManager.buildSessionProjection().messages;
|
|
120
|
+
// New resets already cut the durable Pi projection. Marker slicing is retained only
|
|
121
|
+
// for sessions written before native reset checkpoints existed.
|
|
122
|
+
const messages: AgentMessage[] = isCheckpointBackedReset(ctx, reset)
|
|
123
|
+
? canonicalMessages
|
|
124
|
+
: projectWindow(canonicalMessages, windowId);
|
|
110
125
|
const { tokens } = estimateContextTokens(convertToLlm(messages));
|
|
111
126
|
return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
|
|
112
127
|
} catch {
|
package/src/context/prompts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NotesHome, NotesSnapshot } from "../notes/
|
|
1
|
+
import type { NotesHome, NotesSnapshot } from "../pi/notes/snapshot.js";
|
|
2
2
|
import { CONTEXT_WINDOW_OPEN_TAG, CONTEXT_WINDOW_CLOSE_TAG, POCKET_AGENT_LIMIT, POCKET_HUMAN_LIMIT, POCKET_MODEL_LIMIT, POCKET_PROJECT_LIMIT, POCKET_SESSION_LIMIT, PROTOCOL_BLOCK, GUIDANCE_OPEN_TAG, GUIDANCE_CLOSE_TAG } from "../protocol.js";
|
|
3
3
|
|
|
4
4
|
/** Codex-style <context_window> identity block: the resolved agent and model names plus first/current/previous window ids. */
|
|
@@ -62,7 +62,7 @@ function notesIndex(snapshot: NotesSnapshot): string {
|
|
|
62
62
|
if (recentNotes.length > 0) {
|
|
63
63
|
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:`];
|
|
64
64
|
for (const row of recentNotes) {
|
|
65
|
-
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.
|
|
65
|
+
lines.push(`- ${row.address} (${row.body.split("\n").length} lines, ${row.sizeBytes} UTF-8 bytes, updated ${relativeTime(row.meta.updatedAt, snapshot.openedAt)})`);
|
|
66
66
|
}
|
|
67
67
|
sections.push(lines.join("\n"));
|
|
68
68
|
}
|
|
@@ -20,11 +20,17 @@ export function sendContinuation(pi: ExtensionAPI): void {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* The closed, ordered reset shape: marker, matching boot,
|
|
24
|
-
*
|
|
23
|
+
* The closed, ordered reset shape: retain-none native checkpoint, marker, matching boot,
|
|
24
|
+
* continuation. The boot snapshot is acquired asynchronously after capturing this identity.
|
|
25
25
|
*/
|
|
26
|
-
export function buildResetDrafts(
|
|
27
|
-
|
|
26
|
+
export async function buildResetDrafts(
|
|
27
|
+
ctx: ExtensionContext,
|
|
28
|
+
notifyIncompleteNotes?: IncompleteNotesNotifier,
|
|
29
|
+
isCurrent: () => boolean = () => true,
|
|
30
|
+
): Promise<[SessionBoundaryDraft, SessionBoundaryDraft, SessionBoundaryDraft, SessionBoundaryDraft]> {
|
|
31
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
32
|
+
const previousId = currentWindowId(ctx);
|
|
33
|
+
const sessionPrefix = sessionId.slice(0, 8);
|
|
28
34
|
const usedWindowIds = new Set(
|
|
29
35
|
ctx.sessionManager.getBranch().filter(isWindowMarker).map((entry) => entry.data.windowId),
|
|
30
36
|
);
|
|
@@ -32,27 +38,13 @@ export function buildResetDrafts(ctx: ExtensionContext, notifyIncompleteNotes?:
|
|
|
32
38
|
do {
|
|
33
39
|
windowId = `pcw:${sessionPrefix}:${randomUUID().slice(0, 8)}`;
|
|
34
40
|
} while (usedWindowIds.has(windowId));
|
|
35
|
-
const boot = buildBootMessage(ctx, windowId,
|
|
41
|
+
const boot = await buildBootMessage(ctx, windowId, previousId, notifyIncompleteNotes, isCurrent);
|
|
36
42
|
return [
|
|
43
|
+
{ type: "compaction", summary: "", firstKeptEntryId: null },
|
|
37
44
|
{ type: "custom", customType: RESET_MARKER_TYPE, data: { windowId } },
|
|
38
45
|
{ type: "custom_message", customType: BOOT_TYPE, content: boot.content, display: false, details: { windowId } },
|
|
39
46
|
{ type: "custom_message", customType: CONTINUATION_TYPE, content: CONTINUATION, display: false },
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Persist the marker and send both hidden reset messages; returns the new window id. */
|
|
44
|
-
export function persistManualReset(pi: ExtensionAPI, ctx: ExtensionContext, notifyIncompleteNotes?: IncompleteNotesNotifier): string {
|
|
45
|
-
const [marker, boot, continuation] = buildResetDrafts(ctx, notifyIncompleteNotes);
|
|
46
|
-
pi.appendEntry(marker.customType, marker.data);
|
|
47
|
-
pi.sendMessage(
|
|
48
|
-
{ customType: boot.customType, content: boot.content, display: boot.display, details: boot.details },
|
|
49
|
-
{ triggerTurn: false },
|
|
50
|
-
);
|
|
51
|
-
pi.sendMessage(
|
|
52
|
-
{ customType: continuation.customType, content: continuation.content, display: continuation.display },
|
|
53
|
-
{ triggerTurn: false },
|
|
54
|
-
);
|
|
55
|
-
return boot.details.windowId;
|
|
47
|
+
];
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
/**
|
|
@@ -93,9 +85,19 @@ export function resetTailCommitted(ctx: ExtensionContext, markerId: string, wind
|
|
|
93
85
|
}
|
|
94
86
|
|
|
95
87
|
/** Emit only the reset artifacts an incomplete tail is missing, in the closed order. */
|
|
96
|
-
export function repairResetTail(
|
|
88
|
+
export async function repairResetTail(
|
|
89
|
+
pi: ExtensionAPI,
|
|
90
|
+
ctx: ExtensionContext,
|
|
91
|
+
marker: WindowMarker,
|
|
92
|
+
notifyIncompleteNotes?: IncompleteNotesNotifier,
|
|
93
|
+
isCurrent: () => boolean = () => true,
|
|
94
|
+
): Promise<void> {
|
|
97
95
|
const tail = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
98
96
|
if (!tail || (tail.boot && tail.continuation)) return;
|
|
99
|
-
|
|
100
|
-
if (!
|
|
97
|
+
const boot = tail.boot ? undefined : await buildBootMessage(ctx, marker.data.windowId, previousWindowId(ctx, marker.id), notifyIncompleteNotes, isCurrent);
|
|
98
|
+
if (!isCurrent()) return;
|
|
99
|
+
const latest = inspectResetTail(ctx, marker.id, marker.data.windowId);
|
|
100
|
+
if (!latest) return;
|
|
101
|
+
if (!latest.boot && boot) sendBoot(pi, boot);
|
|
102
|
+
if (!latest.continuation && (latest.boot || boot)) sendContinuation(pi);
|
|
101
103
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { isContextOverflow, isRecoverableLength } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { AgentBeforeSettleEvent, ExtensionAPI, ExtensionContext, SessionBoundaryDraft } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { currentReset } from "./context-window.js";
|
|
4
|
+
import { currentReset, currentWindowId } from "./context-window.js";
|
|
5
5
|
|
|
6
6
|
type BudgetOwner = {
|
|
7
7
|
automaticResetEnabled: (ctx: ExtensionContext) => boolean;
|
|
8
|
-
|
|
8
|
+
hardReserveDue: (ctx: ExtensionContext) => boolean;
|
|
9
9
|
consumeTurnEnd: (ctx: ExtensionContext) => SessionBoundaryDraft[];
|
|
10
10
|
clear: () => void;
|
|
11
11
|
};
|
|
@@ -13,7 +13,9 @@ type BudgetOwner = {
|
|
|
13
13
|
type ResetOptions = {
|
|
14
14
|
isEnabled: () => boolean;
|
|
15
15
|
budget: BudgetOwner;
|
|
16
|
-
buildReset: (ctx: ExtensionContext) => SessionBoundaryDraft[]
|
|
16
|
+
buildReset: (ctx: ExtensionContext, isCurrent: () => boolean) => SessionBoundaryDraft[] | Promise<SessionBoundaryDraft[]>;
|
|
17
|
+
getLifecycleGeneration?: () => number;
|
|
18
|
+
onResetReady?: (ctx: ExtensionContext, drafts: readonly SessionBoundaryDraft[]) => void;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
function isAbort(message: AgentMessage, outcome: string | undefined, ctx: ExtensionContext): boolean {
|
|
@@ -26,79 +28,59 @@ function isOverflowLike(message: AgentMessage, ctx: ExtensionContext): boolean {
|
|
|
26
28
|
(ctx.model !== undefined && isRecoverableLength(message, ctx.model.maxTokens));
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* requests deduplicate while one is pending.
|
|
35
|
-
* - `overflow` is the bounded provider-overflow recovery chain:
|
|
36
|
-
* - "idle" no overflow failure is pending; recovery is available for a later chain.
|
|
37
|
-
* - "pending" an overflow failure is pending; recovery is still available.
|
|
38
|
-
* - "pending-spent" an overflow failure is pending, but recovery was already spent this chain.
|
|
39
|
-
* - "spent" recovery was spent and the failure is no longer pending.
|
|
40
|
-
*
|
|
41
|
-
* All eight combinations of the two axes are reachable (a request may arrive while an overflow
|
|
42
|
-
* chain is pending, spent, or settled), and each has a defined transition. There is no state
|
|
43
|
-
* whose fields silently contradict one another.
|
|
44
|
-
*/
|
|
45
|
-
export type ResetRequestPhase = "none" | "explicit";
|
|
31
|
+
export type ResetRequestSource = "manual" | "automatic";
|
|
32
|
+
export type ResetRequest =
|
|
33
|
+
| { readonly phase: "none" }
|
|
34
|
+
| { readonly phase: "close-out"; readonly windowId: string; readonly source: ResetRequestSource }
|
|
35
|
+
| { readonly phase: "tool-requested"; readonly windowId: string };
|
|
46
36
|
export type ResetOverflowPhase = "idle" | "pending" | "pending-spent" | "spent";
|
|
47
37
|
|
|
48
38
|
export interface ResetControlState {
|
|
49
|
-
readonly request:
|
|
39
|
+
readonly request: ResetRequest;
|
|
50
40
|
readonly overflow: ResetOverflowPhase;
|
|
51
41
|
}
|
|
52
42
|
|
|
43
|
+
const NO_REQUEST: ResetRequest = { phase: "none" };
|
|
44
|
+
|
|
53
45
|
export function initialResetControl(): ResetControlState {
|
|
54
|
-
return { request:
|
|
46
|
+
return { request: NO_REQUEST, overflow: "idle" };
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
/**
|
|
58
|
-
* Facts for a completed turn. The three `get`-supplied fields are guards the adapter resolves
|
|
59
|
-
* lazily: the reducer reads each one only in the branch where the previous adapter consulted it,
|
|
60
|
-
* so policy resolution and pending-message probes keep their original call ordering.
|
|
61
|
-
*/
|
|
49
|
+
/** Facts for a completed turn. Guard values stay lazy to preserve policy resolution order. */
|
|
62
50
|
export interface ResetTurnEndFacts {
|
|
51
|
+
readonly windowId: string;
|
|
63
52
|
readonly aborted: boolean;
|
|
64
53
|
readonly overflow: boolean;
|
|
65
54
|
readonly failed: boolean;
|
|
66
55
|
readonly enabled: boolean;
|
|
67
56
|
readonly queued: boolean;
|
|
68
57
|
readonly automaticResetEnabled: boolean;
|
|
69
|
-
readonly
|
|
58
|
+
readonly hardReserveDue: boolean;
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
/** Facts for the pre-settlement boundary; policy guards stay lazy for the same reason. */
|
|
73
62
|
export interface ResetBeforeSettleFacts {
|
|
63
|
+
readonly windowId: string;
|
|
74
64
|
readonly queued: boolean;
|
|
75
65
|
readonly enabled: boolean;
|
|
76
66
|
readonly automaticResetEnabled: boolean;
|
|
77
67
|
readonly aborted: boolean;
|
|
68
|
+
readonly failed: boolean;
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
export type ResetControlEvent =
|
|
81
|
-
| { readonly type: "
|
|
72
|
+
| { readonly type: "close_out"; readonly windowId: string; readonly source: ResetRequestSource }
|
|
73
|
+
| { readonly type: "tool_request"; readonly windowId: string }
|
|
82
74
|
| { readonly type: "turn_end"; readonly facts: ResetTurnEndFacts }
|
|
83
75
|
| { readonly type: "before_settle"; readonly facts: ResetBeforeSettleFacts }
|
|
84
76
|
| { readonly type: "settled" }
|
|
85
77
|
| { readonly type: "abort" }
|
|
86
78
|
| { readonly type: "clear" };
|
|
87
79
|
|
|
88
|
-
/**
|
|
89
|
-
* The requested effect of a transition. Effects are named, not performed: marker/boot/
|
|
90
|
-
* continuation writes, continuation requests, and UI notices stay in the adapter.
|
|
91
|
-
*
|
|
92
|
-
* - "none" keep any return value to the drafts already collected for this event.
|
|
93
|
-
* - "requested" a new explicit reset request was recorded.
|
|
94
|
-
* - "already-requested" a request was already pending and was deduplicated.
|
|
95
|
-
* - "commit-boundary" build and commit the reset boundary now (turn_end).
|
|
96
|
-
* - "recover-overflow" build and commit the one bounded overflow recovery now (settle).
|
|
97
|
-
*/
|
|
98
80
|
export type ResetControlEffect =
|
|
99
81
|
| "none"
|
|
100
|
-
| "
|
|
101
|
-
| "already-
|
|
82
|
+
| "close-out-armed"
|
|
83
|
+
| "already-pending"
|
|
102
84
|
| "commit-boundary"
|
|
103
85
|
| "recover-overflow";
|
|
104
86
|
|
|
@@ -107,59 +89,77 @@ export interface ResetControlResult {
|
|
|
107
89
|
readonly effect: ResetControlEffect;
|
|
108
90
|
}
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*/
|
|
92
|
+
function requestForWindow(request: ResetRequest, windowId: string): ResetRequest {
|
|
93
|
+
return request.phase !== "none" && request.windowId === windowId ? request : NO_REQUEST;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pure reset-control transitions: request phases own close-out, tool commit, and fallback. */
|
|
115
97
|
export function reduceResetControl(state: ResetControlState, event: ResetControlEvent): ResetControlResult {
|
|
116
98
|
switch (event.type) {
|
|
117
|
-
case "
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
case "close_out": {
|
|
100
|
+
const request = state.request;
|
|
101
|
+
if (request.phase === "tool-requested" && request.windowId === event.windowId) {
|
|
102
|
+
return { state, effect: "already-pending" };
|
|
103
|
+
}
|
|
104
|
+
if (request.phase === "close-out" && request.windowId === event.windowId) {
|
|
105
|
+
if (request.source === "manual" || event.source === "automatic") return { state, effect: "already-pending" };
|
|
106
|
+
return { state: { ...state, request: { ...request, source: "manual" } }, effect: "close-out-armed" };
|
|
107
|
+
}
|
|
108
|
+
return { state: { ...state, request: { phase: "close-out", windowId: event.windowId, source: event.source } }, effect: "close-out-armed" };
|
|
109
|
+
}
|
|
110
|
+
case "tool_request": {
|
|
111
|
+
const request = state.request;
|
|
112
|
+
if (request.phase === "tool-requested" && request.windowId === event.windowId) return { state, effect: "already-pending" };
|
|
113
|
+
return { state: { ...state, request: { phase: "tool-requested", windowId: event.windowId } }, effect: "close-out-armed" };
|
|
120
114
|
}
|
|
121
115
|
case "turn_end": {
|
|
122
116
|
const facts = event.facts;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
const request: ResetRequestPhase = "none";
|
|
126
|
-
if (facts.aborted) {
|
|
127
|
-
// An aborted turn drops the whole boundary: no explicit request, no overflow chain.
|
|
128
|
-
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
129
|
-
}
|
|
117
|
+
const request = requestForWindow(state.request, facts.windowId);
|
|
118
|
+
if (facts.aborted) return { state: initialResetControl(), effect: "none" };
|
|
130
119
|
if (facts.overflow) {
|
|
131
|
-
// A failure that Pi may recover natively: arm (or re-arm) the bounded settle path.
|
|
132
|
-
// A queued message, disabled mode, or disabled automatic reset leaves it disarmed.
|
|
133
120
|
const pending = !facts.queued && facts.enabled && facts.automaticResetEnabled;
|
|
134
121
|
const spent = state.overflow === "pending-spent" || state.overflow === "spent";
|
|
135
122
|
const overflow: ResetOverflowPhase = pending
|
|
136
123
|
? (spent ? "pending-spent" : "pending")
|
|
137
124
|
: (spent ? "spent" : "idle");
|
|
138
|
-
return { state: { request, overflow }, effect: "none" };
|
|
125
|
+
return { state: { request: NO_REQUEST, overflow }, effect: "none" };
|
|
126
|
+
}
|
|
127
|
+
if (facts.failed) return { state: { request: NO_REQUEST, overflow: state.overflow }, effect: "none" };
|
|
128
|
+
if (!facts.enabled) {
|
|
129
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "none" };
|
|
130
|
+
}
|
|
131
|
+
// A direct tool request commits after the whole tool batch. The budget cutoff is
|
|
132
|
+
// a separate hard-reserve safety path; ordinary close-out waits for settlement.
|
|
133
|
+
if (request.phase === "tool-requested" || facts.hardReserveDue) {
|
|
134
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
|
|
139
135
|
}
|
|
140
|
-
|
|
141
|
-
// error leaves the armed overflow chain untouched for the settle boundary.
|
|
142
|
-
const overflow: ResetOverflowPhase = facts.failed ? state.overflow : "idle";
|
|
143
|
-
if (!facts.enabled || facts.failed) return { state: { request, overflow }, effect: "none" };
|
|
144
|
-
// Explicit and threshold resets both commit at turn_end, after incoming and budget drafts.
|
|
145
|
-
const commit = requested || facts.thresholdDue;
|
|
146
|
-
return { state: { request, overflow }, effect: commit ? "commit-boundary" : "none" };
|
|
136
|
+
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
147
137
|
}
|
|
148
138
|
case "before_settle": {
|
|
149
139
|
const facts = event.facts;
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
140
|
+
if (facts.aborted) return { state: initialResetControl(), effect: "none" };
|
|
141
|
+
const request = requestForWindow(state.request, facts.windowId);
|
|
142
|
+
if (state.overflow === "pending" || state.overflow === "pending-spent") {
|
|
143
|
+
if (facts.queued) {
|
|
144
|
+
return { state: { ...state, request: facts.failed ? NO_REQUEST : request }, effect: "none" };
|
|
145
|
+
}
|
|
146
|
+
const spent = state.overflow === "pending-spent";
|
|
147
|
+
if (!facts.enabled || !facts.automaticResetEnabled) {
|
|
148
|
+
return { state: { request: facts.failed ? NO_REQUEST : request, overflow: spent ? "spent" : "idle" }, effect: "none" };
|
|
149
|
+
}
|
|
150
|
+
return { state: { request: NO_REQUEST, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
|
|
156
151
|
}
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
if (facts.failed || !facts.enabled || request.phase !== "close-out") {
|
|
153
|
+
return { state: { ...state, request: NO_REQUEST }, effect: "none" };
|
|
154
|
+
}
|
|
155
|
+
if (facts.queued) return { state: { ...state, request }, effect: "none" };
|
|
156
|
+
if (request.source === "automatic" && !facts.automaticResetEnabled) {
|
|
157
|
+
return { state: { ...state, request: NO_REQUEST }, effect: "none" };
|
|
158
|
+
}
|
|
159
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
|
|
159
160
|
}
|
|
160
161
|
case "settled":
|
|
161
|
-
|
|
162
|
-
return { state: { ...state, overflow: "idle" }, effect: "none" };
|
|
162
|
+
return { state: initialResetControl(), effect: "none" };
|
|
163
163
|
case "abort":
|
|
164
164
|
case "clear":
|
|
165
165
|
return { state: initialResetControl(), effect: "none" };
|
|
@@ -167,71 +167,76 @@ export function reduceResetControl(state: ResetControlState, event: ResetControl
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
-
* Own
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* native recovery attempt has been cancelled.
|
|
174
|
-
*
|
|
175
|
-
* This function is the effect adapter: it captures Pi events, translates them into pure
|
|
176
|
-
* reset-control transitions, and performs the resulting marker/boot/continuation writes and
|
|
177
|
-
* notifications. The decision of what to do lives entirely in `reduceResetControl`.
|
|
170
|
+
* Own close-out requests at Pi's public turn and pre-settlement boundaries. Tool-requested
|
|
171
|
+
* resets commit at turn_end; manual and budget close-outs remain armed across note/tool turns
|
|
172
|
+
* and fall back at a successful agent_before_settle. Overflow recovery remains bounded.
|
|
178
173
|
*/
|
|
179
174
|
export function registerResetLifecycle(pi: ExtensionAPI, options: ResetOptions) {
|
|
180
175
|
let sessionActive = true;
|
|
176
|
+
let lifecycleGeneration = 0;
|
|
181
177
|
let control = initialResetControl();
|
|
182
178
|
|
|
183
179
|
const clear = () => { control = reduceResetControl(control, { type: "clear" }).state; };
|
|
184
|
-
const resetBoundaryResult = (entries: SessionBoundaryDraft[], ctx: ExtensionContext) => {
|
|
180
|
+
const resetBoundaryResult = async (entries: SessionBoundaryDraft[], ctx: ExtensionContext) => {
|
|
181
|
+
const generation = lifecycleGeneration;
|
|
182
|
+
const outerGeneration = options.getLifecycleGeneration?.();
|
|
183
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
184
|
+
const windowId = currentWindowId(ctx);
|
|
185
|
+
const isCurrent = () => sessionActive && lifecycleGeneration === generation &&
|
|
186
|
+
(outerGeneration === undefined || options.getLifecycleGeneration?.() === outerGeneration) && options.isEnabled() &&
|
|
187
|
+
ctx.signal?.aborted !== true && ctx.sessionManager.getSessionId() === sessionId && currentWindowId(ctx) === windowId;
|
|
188
|
+
let resetDrafts: SessionBoundaryDraft[];
|
|
185
189
|
try {
|
|
186
|
-
|
|
190
|
+
resetDrafts = await options.buildReset(ctx, isCurrent);
|
|
187
191
|
} catch (error) {
|
|
188
|
-
ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
|
|
189
|
-
// The incoming drafts and budget drafts are already valid work from this
|
|
190
|
-
// boundary. Preserve them, but do not claim a continuation when reset
|
|
191
|
-
// construction failed.
|
|
192
|
+
if (isCurrent()) ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
|
|
192
193
|
return entries.length > 0 ? { entries } : undefined;
|
|
193
194
|
}
|
|
195
|
+
if (!isCurrent()) return entries.length > 0 ? { entries } : undefined;
|
|
196
|
+
options.onResetReady?.(ctx, resetDrafts);
|
|
197
|
+
return { entries: [...entries, ...resetDrafts], continue: true as const };
|
|
194
198
|
};
|
|
195
199
|
|
|
196
|
-
pi.on("turn_end", (event, ctx) => {
|
|
200
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
197
201
|
if (!sessionActive) return undefined;
|
|
198
202
|
const aborted = isAbort(event.message, event.outcome, ctx);
|
|
199
203
|
const stagedBudgetEntries = options.budget.consumeTurnEnd(ctx);
|
|
200
|
-
|
|
201
|
-
// drains its instance-local staging, so aborts and disabled mode cannot commit it.
|
|
202
|
-
const budgetEntries = options.isEnabled() && !aborted ? stagedBudgetEntries : [];
|
|
204
|
+
const budgetEntries = options.isEnabled() && !aborted && event.outcome !== "error" ? stagedBudgetEntries : [];
|
|
203
205
|
const entries = [...(event.entries ?? []), ...budgetEntries];
|
|
204
206
|
const decision = reduceResetControl(control, {
|
|
205
207
|
type: "turn_end",
|
|
206
208
|
facts: {
|
|
209
|
+
windowId: currentWindowId(ctx),
|
|
207
210
|
aborted,
|
|
208
211
|
overflow: aborted ? false : isOverflowLike(event.message, ctx),
|
|
209
212
|
failed: event.outcome === "error",
|
|
210
213
|
enabled: options.isEnabled(),
|
|
211
214
|
get queued() { return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages(); },
|
|
212
215
|
get automaticResetEnabled() { return options.budget.automaticResetEnabled(ctx); },
|
|
213
|
-
get
|
|
216
|
+
get hardReserveDue() { return options.budget.hardReserveDue(ctx); },
|
|
214
217
|
},
|
|
215
218
|
});
|
|
216
219
|
control = decision.state;
|
|
217
|
-
if (decision.effect === "commit-boundary") return resetBoundaryResult(entries, ctx);
|
|
220
|
+
if (decision.effect === "commit-boundary") return await resetBoundaryResult(entries, ctx);
|
|
218
221
|
return entries.length > 0 ? { entries } : undefined;
|
|
219
222
|
});
|
|
220
223
|
|
|
221
|
-
pi.on("agent_before_settle", (event: AgentBeforeSettleEvent, ctx) => {
|
|
224
|
+
pi.on("agent_before_settle", async (event: AgentBeforeSettleEvent, ctx) => {
|
|
222
225
|
if (!sessionActive) return undefined;
|
|
223
226
|
const decision = reduceResetControl(control, {
|
|
224
227
|
type: "before_settle",
|
|
225
228
|
facts: {
|
|
229
|
+
windowId: currentWindowId(ctx),
|
|
226
230
|
get queued() { return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages(); },
|
|
227
231
|
get enabled() { return options.isEnabled(); },
|
|
228
232
|
get automaticResetEnabled() { return options.budget.automaticResetEnabled(ctx); },
|
|
229
|
-
|
|
233
|
+
aborted: event.outcome === "aborted" || ctx.signal?.aborted === true,
|
|
234
|
+
failed: event.outcome === "error",
|
|
230
235
|
},
|
|
231
236
|
});
|
|
232
237
|
control = decision.state;
|
|
233
|
-
if (decision.effect !== "recover-overflow") return undefined;
|
|
234
|
-
return resetBoundaryResult(event.entries, ctx);
|
|
238
|
+
if (decision.effect !== "commit-boundary" && decision.effect !== "recover-overflow") return undefined;
|
|
239
|
+
return await resetBoundaryResult(event.entries, ctx);
|
|
235
240
|
});
|
|
236
241
|
|
|
237
242
|
pi.on("session_before_compact", (event, ctx) => {
|
|
@@ -242,30 +247,31 @@ export function registerResetLifecycle(pi: ExtensionAPI, options: ResetOptions)
|
|
|
242
247
|
if (event.reason === "manual") {
|
|
243
248
|
ctx.ui.notify("pi-context: /compact is disabled while context windows are active; use /wipe-memory to start a fresh window.", "warning");
|
|
244
249
|
}
|
|
245
|
-
// Native compaction is cancelled here. Threshold resets are decided solely from
|
|
246
|
-
// completed-turn usage at turn_end, never from canonical pre-request history.
|
|
247
250
|
return { cancel: true };
|
|
248
251
|
}
|
|
249
252
|
return undefined;
|
|
250
253
|
});
|
|
251
254
|
|
|
252
255
|
pi.on("agent_end", (_event, ctx) => {
|
|
253
|
-
if (ctx.signal?.aborted)
|
|
256
|
+
if (ctx.signal?.aborted) control = reduceResetControl(control, { type: "abort" }).state;
|
|
254
257
|
});
|
|
255
258
|
pi.on("agent_settled", () => {
|
|
256
|
-
// A failed recovery chain is bounded to one reset/retry. Once Pi settles, a later
|
|
257
|
-
// user prompt starts a new chain; successful continuations clear this earlier.
|
|
258
259
|
control = reduceResetControl(control, { type: "settled" }).state;
|
|
259
260
|
});
|
|
260
|
-
pi.on("session_start", () => { clear(); sessionActive = true; });
|
|
261
|
-
pi.on("session_tree", clear);
|
|
262
|
-
pi.on("session_shutdown", () => { clear(); options.budget.clear(); sessionActive = false; });
|
|
261
|
+
pi.on("session_start", () => { lifecycleGeneration++; clear(); sessionActive = true; });
|
|
262
|
+
pi.on("session_tree", () => { lifecycleGeneration++; clear(); });
|
|
263
|
+
pi.on("session_shutdown", () => { lifecycleGeneration++; clear(); options.budget.clear(); sessionActive = false; });
|
|
263
264
|
|
|
264
265
|
return {
|
|
265
|
-
|
|
266
|
-
const decision = reduceResetControl(control, { type: "
|
|
266
|
+
closeOut(windowId: string, source: ResetRequestSource) {
|
|
267
|
+
const decision = reduceResetControl(control, { type: "close_out", windowId, source });
|
|
268
|
+
control = decision.state;
|
|
269
|
+
return decision.effect;
|
|
270
|
+
},
|
|
271
|
+
request(windowId: string) {
|
|
272
|
+
const decision = reduceResetControl(control, { type: "tool_request", windowId });
|
|
267
273
|
control = decision.state;
|
|
268
|
-
return decision.effect === "already-
|
|
274
|
+
return decision.effect === "already-pending" ? "rollover_already_pending" : "rollover_requested";
|
|
269
275
|
},
|
|
270
276
|
clear,
|
|
271
277
|
};
|