@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isContextOverflow, isRecoverableLength } from "@earendil-works/pi-ai";
|
|
2
|
-
import { currentReset } from "./context-window.js";
|
|
2
|
+
import { currentReset, currentWindowId } from "./context-window.js";
|
|
3
3
|
function isAbort(message, outcome, ctx) {
|
|
4
4
|
return outcome === "aborted" || (message.role === "assistant" && message.stopReason === "aborted") || ctx.signal?.aborted === true;
|
|
5
5
|
}
|
|
@@ -9,139 +9,166 @@ function isOverflowLike(message, ctx) {
|
|
|
9
9
|
return isContextOverflow(message, ctx.model?.contextWindow) ||
|
|
10
10
|
(ctx.model !== undefined && isRecoverableLength(message, ctx.model.maxTokens));
|
|
11
11
|
}
|
|
12
|
+
const NO_REQUEST = { phase: "none" };
|
|
12
13
|
export function initialResetControl() {
|
|
13
|
-
return { request:
|
|
14
|
+
return { request: NO_REQUEST, overflow: "idle" };
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
16
|
+
function requestForWindow(request, windowId) {
|
|
17
|
+
return request.phase !== "none" && request.windowId === windowId ? request : NO_REQUEST;
|
|
18
|
+
}
|
|
19
|
+
/** Pure reset-control transitions: request phases own close-out, tool commit, and fallback. */
|
|
20
20
|
export function reduceResetControl(state, event) {
|
|
21
21
|
switch (event.type) {
|
|
22
|
-
case "
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
case "close_out": {
|
|
23
|
+
const request = state.request;
|
|
24
|
+
if (request.phase === "tool-requested" && request.windowId === event.windowId) {
|
|
25
|
+
return { state, effect: "already-pending" };
|
|
26
|
+
}
|
|
27
|
+
if (request.phase === "close-out" && request.windowId === event.windowId) {
|
|
28
|
+
if (request.source === "manual" || event.source === "automatic")
|
|
29
|
+
return { state, effect: "already-pending" };
|
|
30
|
+
return { state: { ...state, request: { ...request, source: "manual" } }, effect: "close-out-armed" };
|
|
31
|
+
}
|
|
32
|
+
return { state: { ...state, request: { phase: "close-out", windowId: event.windowId, source: event.source } }, effect: "close-out-armed" };
|
|
33
|
+
}
|
|
34
|
+
case "tool_request": {
|
|
35
|
+
const request = state.request;
|
|
36
|
+
if (request.phase === "tool-requested" && request.windowId === event.windowId)
|
|
37
|
+
return { state, effect: "already-pending" };
|
|
38
|
+
return { state: { ...state, request: { phase: "tool-requested", windowId: event.windowId } }, effect: "close-out-armed" };
|
|
26
39
|
}
|
|
27
40
|
case "turn_end": {
|
|
28
41
|
const facts = event.facts;
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (facts.aborted) {
|
|
33
|
-
// An aborted turn drops the whole boundary: no explicit request, no overflow chain.
|
|
34
|
-
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
35
|
-
}
|
|
42
|
+
const request = requestForWindow(state.request, facts.windowId);
|
|
43
|
+
if (facts.aborted)
|
|
44
|
+
return { state: initialResetControl(), effect: "none" };
|
|
36
45
|
if (facts.overflow) {
|
|
37
|
-
// A failure that Pi may recover natively: arm (or re-arm) the bounded settle path.
|
|
38
|
-
// A queued message, disabled mode, or disabled automatic reset leaves it disarmed.
|
|
39
46
|
const pending = !facts.queued && facts.enabled && facts.automaticResetEnabled;
|
|
40
47
|
const spent = state.overflow === "pending-spent" || state.overflow === "spent";
|
|
41
48
|
const overflow = pending
|
|
42
49
|
? (spent ? "pending-spent" : "pending")
|
|
43
50
|
: (spent ? "spent" : "idle");
|
|
44
|
-
return { state: { request, overflow }, effect: "none" };
|
|
51
|
+
return { state: { request: NO_REQUEST, overflow }, effect: "none" };
|
|
52
|
+
}
|
|
53
|
+
if (facts.failed)
|
|
54
|
+
return { state: { request: NO_REQUEST, overflow: state.overflow }, effect: "none" };
|
|
55
|
+
if (!facts.enabled) {
|
|
56
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "none" };
|
|
45
57
|
}
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const commit = requested || facts.thresholdDue;
|
|
53
|
-
return { state: { request, overflow }, effect: commit ? "commit-boundary" : "none" };
|
|
58
|
+
// A direct tool request commits after the whole tool batch. The budget cutoff is
|
|
59
|
+
// a separate hard-reserve safety path; ordinary close-out waits for settlement.
|
|
60
|
+
if (request.phase === "tool-requested" || facts.hardReserveDue) {
|
|
61
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
|
|
62
|
+
}
|
|
63
|
+
return { state: { request, overflow: "idle" }, effect: "none" };
|
|
54
64
|
}
|
|
55
65
|
case "before_settle": {
|
|
56
66
|
const facts = event.facts;
|
|
57
|
-
if (
|
|
58
|
-
return { state, effect: "none" };
|
|
59
|
-
|
|
67
|
+
if (facts.aborted)
|
|
68
|
+
return { state: initialResetControl(), effect: "none" };
|
|
69
|
+
const request = requestForWindow(state.request, facts.windowId);
|
|
70
|
+
if (state.overflow === "pending" || state.overflow === "pending-spent") {
|
|
71
|
+
if (facts.queued) {
|
|
72
|
+
return { state: { ...state, request: facts.failed ? NO_REQUEST : request }, effect: "none" };
|
|
73
|
+
}
|
|
74
|
+
const spent = state.overflow === "pending-spent";
|
|
75
|
+
if (!facts.enabled || !facts.automaticResetEnabled) {
|
|
76
|
+
return { state: { request: facts.failed ? NO_REQUEST : request, overflow: spent ? "spent" : "idle" }, effect: "none" };
|
|
77
|
+
}
|
|
78
|
+
return { state: { request: NO_REQUEST, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
|
|
79
|
+
}
|
|
80
|
+
if (facts.failed || !facts.enabled || request.phase !== "close-out") {
|
|
81
|
+
return { state: { ...state, request: NO_REQUEST }, effect: "none" };
|
|
82
|
+
}
|
|
60
83
|
if (facts.queued)
|
|
61
|
-
return { state, effect: "none" };
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return { state: { ...state, overflow: spent ? "spent" : "idle" }, effect: "none" };
|
|
84
|
+
return { state: { ...state, request }, effect: "none" };
|
|
85
|
+
if (request.source === "automatic" && !facts.automaticResetEnabled) {
|
|
86
|
+
return { state: { ...state, request: NO_REQUEST }, effect: "none" };
|
|
65
87
|
}
|
|
66
|
-
|
|
67
|
-
return { state: { ...state, overflow: "spent" }, effect: spent ? "none" : "recover-overflow" };
|
|
88
|
+
return { state: { request: NO_REQUEST, overflow: "idle" }, effect: "commit-boundary" };
|
|
68
89
|
}
|
|
69
90
|
case "settled":
|
|
70
|
-
|
|
71
|
-
return { state: { ...state, overflow: "idle" }, effect: "none" };
|
|
91
|
+
return { state: initialResetControl(), effect: "none" };
|
|
72
92
|
case "abort":
|
|
73
93
|
case "clear":
|
|
74
94
|
return { state: initialResetControl(), effect: "none" };
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
/**
|
|
78
|
-
* Own
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* native recovery attempt has been cancelled.
|
|
82
|
-
*
|
|
83
|
-
* This function is the effect adapter: it captures Pi events, translates them into pure
|
|
84
|
-
* reset-control transitions, and performs the resulting marker/boot/continuation writes and
|
|
85
|
-
* notifications. The decision of what to do lives entirely in `reduceResetControl`.
|
|
98
|
+
* Own close-out requests at Pi's public turn and pre-settlement boundaries. Tool-requested
|
|
99
|
+
* resets commit at turn_end; manual and budget close-outs remain armed across note/tool turns
|
|
100
|
+
* and fall back at a successful agent_before_settle. Overflow recovery remains bounded.
|
|
86
101
|
*/
|
|
87
102
|
export function registerResetLifecycle(pi, options) {
|
|
88
103
|
let sessionActive = true;
|
|
104
|
+
let lifecycleGeneration = 0;
|
|
89
105
|
let control = initialResetControl();
|
|
90
106
|
const clear = () => { control = reduceResetControl(control, { type: "clear" }).state; };
|
|
91
|
-
const resetBoundaryResult = (entries, ctx) => {
|
|
107
|
+
const resetBoundaryResult = async (entries, ctx) => {
|
|
108
|
+
const generation = lifecycleGeneration;
|
|
109
|
+
const outerGeneration = options.getLifecycleGeneration?.();
|
|
110
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
111
|
+
const windowId = currentWindowId(ctx);
|
|
112
|
+
const isCurrent = () => sessionActive && lifecycleGeneration === generation &&
|
|
113
|
+
(outerGeneration === undefined || options.getLifecycleGeneration?.() === outerGeneration) && options.isEnabled() &&
|
|
114
|
+
ctx.signal?.aborted !== true && ctx.sessionManager.getSessionId() === sessionId && currentWindowId(ctx) === windowId;
|
|
115
|
+
let resetDrafts;
|
|
92
116
|
try {
|
|
93
|
-
|
|
117
|
+
resetDrafts = await options.buildReset(ctx, isCurrent);
|
|
94
118
|
}
|
|
95
119
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// boundary. Preserve them, but do not claim a continuation when reset
|
|
99
|
-
// construction failed.
|
|
120
|
+
if (isCurrent())
|
|
121
|
+
ctx.ui.notify(`pi-context: could not build reset (${String(error)}).`, "warning");
|
|
100
122
|
return entries.length > 0 ? { entries } : undefined;
|
|
101
123
|
}
|
|
124
|
+
if (!isCurrent())
|
|
125
|
+
return entries.length > 0 ? { entries } : undefined;
|
|
126
|
+
options.onResetReady?.(ctx, resetDrafts);
|
|
127
|
+
return { entries: [...entries, ...resetDrafts], continue: true };
|
|
102
128
|
};
|
|
103
|
-
pi.on("turn_end", (event, ctx) => {
|
|
129
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
104
130
|
if (!sessionActive)
|
|
105
131
|
return undefined;
|
|
106
132
|
const aborted = isAbort(event.message, event.outcome, ctx);
|
|
107
133
|
const stagedBudgetEntries = options.budget.consumeTurnEnd(ctx);
|
|
108
|
-
|
|
109
|
-
// drains its instance-local staging, so aborts and disabled mode cannot commit it.
|
|
110
|
-
const budgetEntries = options.isEnabled() && !aborted ? stagedBudgetEntries : [];
|
|
134
|
+
const budgetEntries = options.isEnabled() && !aborted && event.outcome !== "error" ? stagedBudgetEntries : [];
|
|
111
135
|
const entries = [...(event.entries ?? []), ...budgetEntries];
|
|
112
136
|
const decision = reduceResetControl(control, {
|
|
113
137
|
type: "turn_end",
|
|
114
138
|
facts: {
|
|
139
|
+
windowId: currentWindowId(ctx),
|
|
115
140
|
aborted,
|
|
116
141
|
overflow: aborted ? false : isOverflowLike(event.message, ctx),
|
|
117
142
|
failed: event.outcome === "error",
|
|
118
143
|
enabled: options.isEnabled(),
|
|
119
144
|
get queued() { return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages(); },
|
|
120
145
|
get automaticResetEnabled() { return options.budget.automaticResetEnabled(ctx); },
|
|
121
|
-
get
|
|
146
|
+
get hardReserveDue() { return options.budget.hardReserveDue(ctx); },
|
|
122
147
|
},
|
|
123
148
|
});
|
|
124
149
|
control = decision.state;
|
|
125
150
|
if (decision.effect === "commit-boundary")
|
|
126
|
-
return resetBoundaryResult(entries, ctx);
|
|
151
|
+
return await resetBoundaryResult(entries, ctx);
|
|
127
152
|
return entries.length > 0 ? { entries } : undefined;
|
|
128
153
|
});
|
|
129
|
-
pi.on("agent_before_settle", (event, ctx) => {
|
|
154
|
+
pi.on("agent_before_settle", async (event, ctx) => {
|
|
130
155
|
if (!sessionActive)
|
|
131
156
|
return undefined;
|
|
132
157
|
const decision = reduceResetControl(control, {
|
|
133
158
|
type: "before_settle",
|
|
134
159
|
facts: {
|
|
160
|
+
windowId: currentWindowId(ctx),
|
|
135
161
|
get queued() { return event.context.pendingMessages.length > 0 || ctx.hasPendingMessages(); },
|
|
136
162
|
get enabled() { return options.isEnabled(); },
|
|
137
163
|
get automaticResetEnabled() { return options.budget.automaticResetEnabled(ctx); },
|
|
138
|
-
|
|
164
|
+
aborted: event.outcome === "aborted" || ctx.signal?.aborted === true,
|
|
165
|
+
failed: event.outcome === "error",
|
|
139
166
|
},
|
|
140
167
|
});
|
|
141
168
|
control = decision.state;
|
|
142
|
-
if (decision.effect !== "recover-overflow")
|
|
169
|
+
if (decision.effect !== "commit-boundary" && decision.effect !== "recover-overflow")
|
|
143
170
|
return undefined;
|
|
144
|
-
return resetBoundaryResult(event.entries, ctx);
|
|
171
|
+
return await resetBoundaryResult(event.entries, ctx);
|
|
145
172
|
});
|
|
146
173
|
pi.on("session_before_compact", (event, ctx) => {
|
|
147
174
|
if (!sessionActive)
|
|
@@ -153,29 +180,30 @@ export function registerResetLifecycle(pi, options) {
|
|
|
153
180
|
if (event.reason === "manual") {
|
|
154
181
|
ctx.ui.notify("pi-context: /compact is disabled while context windows are active; use /wipe-memory to start a fresh window.", "warning");
|
|
155
182
|
}
|
|
156
|
-
// Native compaction is cancelled here. Threshold resets are decided solely from
|
|
157
|
-
// completed-turn usage at turn_end, never from canonical pre-request history.
|
|
158
183
|
return { cancel: true };
|
|
159
184
|
}
|
|
160
185
|
return undefined;
|
|
161
186
|
});
|
|
162
187
|
pi.on("agent_end", (_event, ctx) => {
|
|
163
188
|
if (ctx.signal?.aborted)
|
|
164
|
-
|
|
189
|
+
control = reduceResetControl(control, { type: "abort" }).state;
|
|
165
190
|
});
|
|
166
191
|
pi.on("agent_settled", () => {
|
|
167
|
-
// A failed recovery chain is bounded to one reset/retry. Once Pi settles, a later
|
|
168
|
-
// user prompt starts a new chain; successful continuations clear this earlier.
|
|
169
192
|
control = reduceResetControl(control, { type: "settled" }).state;
|
|
170
193
|
});
|
|
171
|
-
pi.on("session_start", () => { clear(); sessionActive = true; });
|
|
172
|
-
pi.on("session_tree", clear);
|
|
173
|
-
pi.on("session_shutdown", () => { clear(); options.budget.clear(); sessionActive = false; });
|
|
194
|
+
pi.on("session_start", () => { lifecycleGeneration++; clear(); sessionActive = true; });
|
|
195
|
+
pi.on("session_tree", () => { lifecycleGeneration++; clear(); });
|
|
196
|
+
pi.on("session_shutdown", () => { lifecycleGeneration++; clear(); options.budget.clear(); sessionActive = false; });
|
|
174
197
|
return {
|
|
175
|
-
|
|
176
|
-
const decision = reduceResetControl(control, { type: "
|
|
198
|
+
closeOut(windowId, source) {
|
|
199
|
+
const decision = reduceResetControl(control, { type: "close_out", windowId, source });
|
|
200
|
+
control = decision.state;
|
|
201
|
+
return decision.effect;
|
|
202
|
+
},
|
|
203
|
+
request(windowId) {
|
|
204
|
+
const decision = reduceResetControl(control, { type: "tool_request", windowId });
|
|
177
205
|
control = decision.state;
|
|
178
|
-
return decision.effect === "already-
|
|
206
|
+
return decision.effect === "already-pending" ? "rollover_already_pending" : "rollover_requested";
|
|
179
207
|
},
|
|
180
208
|
clear,
|
|
181
209
|
};
|
|
@@ -2,10 +2,11 @@ import { getCurrentSystemMessage, Type } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { VERSION, defineTool } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { registerBudget } from "./budget.js";
|
|
4
4
|
import { output } from "../tool-output.js";
|
|
5
|
-
import { migrateLegacyHomes } from "../notes/
|
|
6
|
-
import { currentReset, isWindowMarker, projectRootWindow, projectWindow, rootWindowId } from "./context-window.js";
|
|
5
|
+
import { migrateLegacyHomes } from "../pi/notes/adapter.js";
|
|
6
|
+
import { currentReset, currentWindowId, isCheckpointBackedReset, isWindowBoot, isWindowMarker, projectRootWindow, projectWindow, rootWindowId } from "./context-window.js";
|
|
7
7
|
import { registerResetLifecycle } from "./reset-lifecycle.js";
|
|
8
|
-
import { buildResetDrafts,
|
|
8
|
+
import { buildResetDrafts, resetTailCommitted } from "./reset-artifacts.js";
|
|
9
|
+
import { BOOT_TYPE, WARNING_CONTENT, WARNING_TYPE } from "../protocol.js";
|
|
9
10
|
import { ensureBoot } from "./boot.js";
|
|
10
11
|
// The bundle captures its identity; direct source loads must not claim a built hash.
|
|
11
12
|
const buildLabel = typeof __PI_CONTEXT_BUILD__ === "undefined"
|
|
@@ -17,6 +18,7 @@ function branchHasWindowMarker(ctx, fromId) {
|
|
|
17
18
|
/** Register the context-window runtime and its context-owned commands/tools. */
|
|
18
19
|
export function registerContext(pi, settingsManager) {
|
|
19
20
|
let enabled = true;
|
|
21
|
+
let lifecycleGeneration = 0;
|
|
20
22
|
let missingBootNotice;
|
|
21
23
|
const incompleteNotesNotified = new Set();
|
|
22
24
|
const pendingResetNotices = new Set();
|
|
@@ -30,7 +32,7 @@ export function registerContext(pi, settingsManager) {
|
|
|
30
32
|
const branch = ctx.sessionManager.getBranch();
|
|
31
33
|
for (const windowId of pendingResetNotices) {
|
|
32
34
|
const marker = branch.find((entry) => isWindowMarker(entry) && entry.data.windowId === windowId);
|
|
33
|
-
if (!marker || !resetTailCommitted(ctx, marker.id, windowId))
|
|
35
|
+
if (!marker || !isCheckpointBackedReset(ctx, marker) || !resetTailCommitted(ctx, marker.id, windowId))
|
|
34
36
|
continue;
|
|
35
37
|
pendingResetNotices.delete(windowId);
|
|
36
38
|
ctx.ui.notify(`pi-context: memory cleared · ${windowId}`, "info");
|
|
@@ -51,19 +53,25 @@ export function registerContext(pi, settingsManager) {
|
|
|
51
53
|
const migrationWarning = migrateLegacyHomes();
|
|
52
54
|
if (migrationWarning)
|
|
53
55
|
console.warn(`pi-context: ${migrationWarning}`);
|
|
54
|
-
const budget = registerBudget(pi, () => enabled, settingsManager);
|
|
55
|
-
pi.on("session_start", (_event, ctx) => {
|
|
56
|
+
const budget = registerBudget(pi, () => enabled, settingsManager, (windowId) => resets.closeOut(windowId, "automatic"));
|
|
57
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
58
|
+
const generation = ++lifecycleGeneration;
|
|
56
59
|
if (!enabled)
|
|
57
60
|
return;
|
|
58
61
|
missingBootNotice = undefined;
|
|
59
62
|
pendingResetNotices.clear();
|
|
60
|
-
ensureBoot(pi, ctx, notifyIncompleteNotes);
|
|
63
|
+
await ensureBoot(pi, ctx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
|
|
61
64
|
});
|
|
62
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
65
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
66
|
+
const generation = ++lifecycleGeneration;
|
|
63
67
|
missingBootNotice = undefined;
|
|
64
68
|
pendingResetNotices.clear();
|
|
65
69
|
if (enabled)
|
|
66
|
-
ensureBoot(pi, ctx, notifyIncompleteNotes);
|
|
70
|
+
await ensureBoot(pi, ctx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
|
|
71
|
+
});
|
|
72
|
+
pi.on("session_shutdown", () => {
|
|
73
|
+
lifecycleGeneration++;
|
|
74
|
+
pendingResetNotices.clear();
|
|
67
75
|
});
|
|
68
76
|
// Pi's branch summarizer receives raw entries and bypasses context_with_system. Do not
|
|
69
77
|
// let a summary of a reset branch smuggle erased history back into the destination.
|
|
@@ -81,7 +89,12 @@ export function registerContext(pi, settingsManager) {
|
|
|
81
89
|
const reset = currentReset(ctx);
|
|
82
90
|
const windowId = reset?.data.windowId ?? rootWindowId(ctx.sessionManager.getSessionId());
|
|
83
91
|
try {
|
|
84
|
-
|
|
92
|
+
if (reset) {
|
|
93
|
+
if (!event.messages.some((message) => isWindowBoot(message, windowId)))
|
|
94
|
+
throw new Error(`Missing boot for context window ${windowId}`);
|
|
95
|
+
return { messages: isCheckpointBackedReset(ctx, reset) ? event.messages : projectWindow(event.messages, windowId) };
|
|
96
|
+
}
|
|
97
|
+
return { messages: projectRootWindow(event.messages, windowId) };
|
|
85
98
|
}
|
|
86
99
|
catch (error) {
|
|
87
100
|
if (missingBootNotice !== windowId) {
|
|
@@ -100,10 +113,13 @@ export function registerContext(pi, settingsManager) {
|
|
|
100
113
|
const arg = args.trim().toLowerCase();
|
|
101
114
|
if (arg === "on") {
|
|
102
115
|
enabled = true;
|
|
103
|
-
|
|
116
|
+
const generation = ++lifecycleGeneration;
|
|
117
|
+
await ensureBoot(pi, cmdCtx, notifyIncompleteNotes, () => generation === lifecycleGeneration && enabled);
|
|
104
118
|
}
|
|
105
119
|
else if (arg === "off") {
|
|
106
120
|
enabled = false;
|
|
121
|
+
lifecycleGeneration++;
|
|
122
|
+
pendingResetNotices.clear();
|
|
107
123
|
budget.clear();
|
|
108
124
|
resets.clear();
|
|
109
125
|
}
|
|
@@ -115,17 +131,28 @@ export function registerContext(pi, settingsManager) {
|
|
|
115
131
|
},
|
|
116
132
|
});
|
|
117
133
|
pi.registerCommand("wipe-memory", {
|
|
118
|
-
description: "
|
|
134
|
+
description: "Ask the agent to close out its notes, then start a fresh context window",
|
|
119
135
|
handler: async (_args, cmdCtx) => {
|
|
120
136
|
if (!enabled) {
|
|
121
137
|
cmdCtx.ui.notify("pi-context: /wipe-memory requires /pi-context on.", "error");
|
|
122
138
|
return;
|
|
123
139
|
}
|
|
140
|
+
const requestedWindowId = currentWindowId(cmdCtx);
|
|
124
141
|
await cmdCtx.waitForIdle();
|
|
125
|
-
if (!enabled)
|
|
142
|
+
if (!enabled || currentWindowId(cmdCtx) !== requestedWindowId)
|
|
143
|
+
return;
|
|
144
|
+
const armed = resets.closeOut(requestedWindowId, "manual");
|
|
145
|
+
if (armed === "already-pending")
|
|
126
146
|
return;
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
try {
|
|
148
|
+
pi.sendMessage({ customType: WARNING_TYPE, content: WARNING_CONTENT, display: false }, { triggerTurn: true });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
resets.clear();
|
|
152
|
+
cmdCtx.ui.notify(`pi-context: could not start manual close-out (${String(error)}).`, "error");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await cmdCtx.waitForIdle();
|
|
129
156
|
},
|
|
130
157
|
});
|
|
131
158
|
pi.registerTool(defineTool({
|
|
@@ -133,18 +160,23 @@ export function registerContext(pi, settingsManager) {
|
|
|
133
160
|
label: "Wipe memory",
|
|
134
161
|
description: "Wipe your in-context memory and start a fresh context window. Your session, notes, and history survive.",
|
|
135
162
|
parameters: Type.Object({}, { additionalProperties: false }),
|
|
136
|
-
async execute() {
|
|
163
|
+
async execute(_id, _params, _signal, _update, ctx) {
|
|
137
164
|
if (!enabled)
|
|
138
165
|
return output({ error: "pi-context is off (/pi-context on to enable)" });
|
|
139
|
-
return output({ status: resets.request() }, undefined, true);
|
|
166
|
+
return output({ status: resets.request(currentWindowId(ctx)) }, undefined, true);
|
|
140
167
|
},
|
|
141
168
|
}));
|
|
142
169
|
const resets = registerResetLifecycle(pi, {
|
|
143
170
|
isEnabled: () => enabled,
|
|
144
|
-
buildReset: (ctx) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
171
|
+
buildReset: (ctx, isCurrent) => buildResetDrafts(ctx, notifyIncompleteNotes, isCurrent),
|
|
172
|
+
getLifecycleGeneration: () => lifecycleGeneration,
|
|
173
|
+
onResetReady: (_ctx, drafts) => {
|
|
174
|
+
const boot = drafts.find((draft) => draft.type === "custom_message" && draft.customType === BOOT_TYPE);
|
|
175
|
+
if (boot?.type === "custom_message" && boot.details && typeof boot.details === "object") {
|
|
176
|
+
const windowId = boot.details.windowId;
|
|
177
|
+
if (typeof windowId === "string")
|
|
178
|
+
pendingResetNotices.add(windowId);
|
|
179
|
+
}
|
|
148
180
|
},
|
|
149
181
|
budget,
|
|
150
182
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SettingsManager, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type PiContextSettings } from "../settings.js";
|
|
3
|
+
export type ResolvedThresholds = {
|
|
4
|
+
reminder: number;
|
|
5
|
+
reserve: number;
|
|
6
|
+
warning: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Pure derivation of the thresholds from Pi's reserve: the reminder fires at reserve
|
|
10
|
+
* plus the pi-context margin, the warning steer at reserve plus WARNING_RUNWAY_TOKENS.
|
|
11
|
+
* An invalid margin degrades to the default and reports one warning. Automatic
|
|
12
|
+
* threshold/overflow handling is represented by reset lifecycle boundary drafts;
|
|
13
|
+
* no compaction summary is generated.
|
|
14
|
+
*/
|
|
15
|
+
export declare function deriveThresholds(reserveTokens: number, margins: PiContextSettings): {
|
|
16
|
+
thresholds: ResolvedThresholds;
|
|
17
|
+
warnings: string[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Read the active compaction reserve, enablement, and pi-context margin settings. This
|
|
21
|
+
* function deliberately has no cache: the budget owner supplies the invocation-scoped
|
|
22
|
+
* cache so two live piContext instances cannot share mutable policy state.
|
|
23
|
+
*/
|
|
24
|
+
export type ThresholdSettingsResolution = {
|
|
25
|
+
thresholds: ResolvedThresholds;
|
|
26
|
+
automatic: boolean;
|
|
27
|
+
warnings: string[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Resolve policy from either the explicitly supplied SDK authority or Pi's default
|
|
31
|
+
* file-backed settings. The caller owns diagnostics and any lifecycle caching.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readThresholdSettings(ctx: ExtensionContext, settingsManager?: SettingsManager): ThresholdSettingsResolution;
|
|
@@ -23,7 +23,7 @@ export function deriveThresholds(reserveTokens, margins) {
|
|
|
23
23
|
else {
|
|
24
24
|
const parsed = validMargin(margins.reminderMarginTokens);
|
|
25
25
|
if (parsed === undefined) {
|
|
26
|
-
warnings.push(`pi-context: ${reminderKey} must be a positive integer; using default
|
|
26
|
+
warnings.push(`pi-context: ${reminderKey} must be a positive integer; using the default reminder margin.`);
|
|
27
27
|
reminderMargin = DEFAULT_REMINDER_MARGIN_TOKENS;
|
|
28
28
|
}
|
|
29
29
|
else
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runDreamer, type DreamerSessionFactory } from "./runner.js";
|
|
3
|
+
import { type DreamerSetting } from "./settings.js";
|
|
4
|
+
/** Injection seams used by tests; production uses the defaults. */
|
|
5
|
+
export type DreamDependencies = {
|
|
6
|
+
sessionFactory?: DreamerSessionFactory;
|
|
7
|
+
dreamerSettings?: (cwd?: string) => DreamerSetting;
|
|
8
|
+
runDreamer?: typeof runDreamer;
|
|
9
|
+
};
|
|
10
|
+
export declare function main(argv?: string[], deps?: DreamDependencies): Promise<number>;
|
package/dist/src/dream/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadPlaybook, runDreamer } from "./runner.js";
|
|
|
8
8
|
import { gitCommit } from "./git.js";
|
|
9
9
|
import { readDreamerSettings } from "./settings.js";
|
|
10
10
|
import { doctor } from "./doctor.js";
|
|
11
|
-
import { notesRoot } from "../notes/
|
|
11
|
+
import { notesRoot } from "../pi/notes/adapter.js";
|
|
12
12
|
function args(argv) { const out = {}; for (let i = 0; i < argv.length; i++) {
|
|
13
13
|
const a = argv[i];
|
|
14
14
|
if (a === "--force" || a === "--help")
|
package/dist/src/dream/doctor.js
CHANGED
|
@@ -37,11 +37,15 @@ export function doctor(home) {
|
|
|
37
37
|
report(path, `duplicate metadata key ${field[1]}; keep one value`);
|
|
38
38
|
fields.set(field[1], field[2].replace(/^(["'])(.*)\1$/, "$2"));
|
|
39
39
|
}
|
|
40
|
-
for (const [key, valid] of Object.entries({ origin: /^(user|self|external)$/, status: /^(active|superseded|pending|archived)$/, stale: /^(true|false)$/,
|
|
40
|
+
for (const [key, valid] of Object.entries({ origin: /^(user|self|external)$/, status: /^(active|superseded|pending|archived)$/, stale: /^(true|false)$/, accessCount: /^\d+$/ })) {
|
|
41
41
|
if (!valid.test(fields.get(key) ?? ""))
|
|
42
42
|
report(path, `missing/invalid ${key}; repair frontmatter`);
|
|
43
43
|
}
|
|
44
|
-
for (const
|
|
44
|
+
for (const [legacy, current] of [["created_at", "createdAt"], ["updated_at", "updatedAt"], ["last_accessed", "lastAccessed"], ["access_count", "accessCount"], ["source_window", "sourceWindow"], ["recurrence_count", "recurrenceCount"], ["recurrence_windows", "recurrenceWindows"]]) {
|
|
45
|
+
if (fields.has(legacy))
|
|
46
|
+
report(path, `legacy metadata key ${legacy}; manually migrate to ${current}`);
|
|
47
|
+
}
|
|
48
|
+
for (const key of ["createdAt", "updatedAt", "lastAccessed"]) {
|
|
45
49
|
const value = fields.get(key);
|
|
46
50
|
if (!value || !Number.isFinite(Date.parse(value)))
|
|
47
51
|
report(path, `missing/invalid ${key}; use an ISO timestamp`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type GateResult = {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
reason: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* The scheduler reads the last-run sidecar, not the lock: the lock's lifetime says
|
|
7
|
+
* nothing about when the last dream ran, while the sidecar records exactly that.
|
|
8
|
+
*/
|
|
9
|
+
export declare function timeGate(stampPath: string, minHours: number, now?: number): GateResult;
|
|
10
|
+
export declare function materialGate(home: string, sinceMtime: number, minSessions: number): GateResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcome of one audit commit. `ok: true` always carries a real `commit` snapshot;
|
|
3
|
+
* `empty: true` only means no files changed (an empty baseline commit or no commit was
|
|
4
|
+
* needed). `ok: false` means the audit layer could not guarantee a snapshot.
|
|
5
|
+
*/
|
|
6
|
+
export type AuditResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
commit: string;
|
|
9
|
+
empty: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Git audit layer for a dream run: one commit before (baseline) and one after (dream),
|
|
16
|
+
* so the human gate reviews `git show` instead of trusting a report, and rollback is
|
|
17
|
+
* `git revert`. The caller decides how loud a failure is; this function only reports it.
|
|
18
|
+
* A clean tree on an established repository commits nothing; a repository with no HEAD
|
|
19
|
+
* gets an empty baseline commit, because an audit run with no snapshot is not a success.
|
|
20
|
+
*/
|
|
21
|
+
export declare function gitCommit(home: string, message: string): AuditResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type LockState = {
|
|
2
|
+
path: string;
|
|
3
|
+
held: boolean;
|
|
4
|
+
reason?: string;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
/** mtime of the last-run sidecar before this run took the lock; failLock restores it. */
|
|
7
|
+
priorStampMtime?: number;
|
|
8
|
+
/** Random identity written into the lock file; cleanup only removes the lock it wrote. */
|
|
9
|
+
token?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The scheduler's last-run timestamp lives in a sidecar beside the lock, never in the
|
|
13
|
+
* lock file itself: acquiring, releasing or cleaning up the lock touches only the PID
|
|
14
|
+
* marker, so lock lifecycle does not destroy the timestamp the time gate reads.
|
|
15
|
+
*/
|
|
16
|
+
export declare function lastRunPath(lockPath: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Acquire the dream lock with Git-style exclusive existence locking: one O_CREAT|O_EXCL
|
|
19
|
+
* creation. An existing path refuses acquisition regardless of its contents, PID, or age,
|
|
20
|
+
* and is never read for permission, replaced, or removed. There is no automatic stale
|
|
21
|
+
* recovery; a crash-left lock is human cleanup after confirming no dream is running.
|
|
22
|
+
*/
|
|
23
|
+
export declare function acquireLock(path: string): LockState;
|
|
24
|
+
/** Release only the lock this run acquired. Idempotent: repeated cleanup does nothing. */
|
|
25
|
+
export declare function releaseLock(lock: LockState): void;
|
|
26
|
+
/**
|
|
27
|
+
* A failed run must not advance the scheduler: restore the previous timestamp, or remove
|
|
28
|
+
* the one this run wrote when there was none. Only this run's own marker is removed, and
|
|
29
|
+
* the state is marked released so a later cleanup attempt is harmless.
|
|
30
|
+
*/
|
|
31
|
+
export declare function failLock(lock: LockState): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AgentSession, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export type DreamWrite = {
|
|
3
|
+
tool: "write" | "edit";
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
export type DreamResult = {
|
|
7
|
+
report: string;
|
|
8
|
+
writes: DreamWrite[];
|
|
9
|
+
error?: string;
|
|
10
|
+
};
|
|
11
|
+
export type DreamerSession = Pick<AgentSession, "prompt" | "subscribe" | "dispose">;
|
|
12
|
+
export type DreamerSessionFactory = (options: {
|
|
13
|
+
cwd: string;
|
|
14
|
+
modelPattern?: string;
|
|
15
|
+
tools: string[];
|
|
16
|
+
}) => Promise<DreamerSession>;
|
|
17
|
+
export declare const DREAMER_TOOLS: string[];
|
|
18
|
+
/** The only custom definitions in the dream session replace the two built-ins with jailed versions. */
|
|
19
|
+
export declare function dreamerWriteToolDefinitions(notesHome: string): ToolDefinition<any, any, any>[];
|
|
20
|
+
export declare const defaultDreamerSessionFactory: DreamerSessionFactory;
|
|
21
|
+
/**
|
|
22
|
+
* Run one dream turn. A dreamer failure is returned as `error` together with the partial
|
|
23
|
+
* writes observed so far, so the caller can record partial state instead of losing it;
|
|
24
|
+
* only a failure to even start the session throws.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runDreamer(playbook: string, cwd: string, options?: {
|
|
27
|
+
modelPattern?: string;
|
|
28
|
+
sessionFactory?: DreamerSessionFactory;
|
|
29
|
+
}): Promise<DreamResult>;
|
|
30
|
+
export declare function loadPlaybook(path: string): string;
|