@timurproko/a1 0.1.8-dev.459 → 0.1.8-dev.467
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/dist/contracts/owned-ui/model.d.ts +2 -0
- package/dist/contracts/owned-ui/validation.js +3 -0
- package/dist/integrations/pi/components/shell-footer-status.js +32 -8
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -0
- package/dist/integrations/pi/engine/adapter.js +42 -4
- package/dist/integrations/pi/engine/compaction-progress.d.ts +28 -0
- package/dist/integrations/pi/engine/compaction-progress.js +96 -0
- package/dist/integrations/pi/engine/conformance.d.ts +1 -1
- package/dist/integrations/pi/engine/conformance.js +3 -3
- package/dist/integrations/pi/engine/session-integration.d.ts +25 -3
- package/dist/integrations/pi/engine/session-integration.js +93 -8
- package/dist/integrations/pi/session-ui/session-shell-root.js +28 -1
- package/dist/integrations/pi/session-ui/session-shell.js +3 -39
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/docs/local-worktree-cleanup.md +16 -0
- package/package.json +1 -1
|
@@ -126,6 +126,8 @@ export interface OwnedUiFooterView {
|
|
|
126
126
|
export interface OwnedUiStatusView {
|
|
127
127
|
readonly title: string;
|
|
128
128
|
readonly workingMessage: string | null;
|
|
129
|
+
/** Estimated progress of the shown work state as an integer percent below 100, when the engine can measure it. */
|
|
130
|
+
readonly workingProgress?: number | null;
|
|
129
131
|
readonly diagnostics: readonly string[];
|
|
130
132
|
readonly badges: readonly string[];
|
|
131
133
|
readonly usage?: OwnedUiUsageView;
|
|
@@ -347,6 +347,9 @@ export function assertOwnedUiEditorState(editor) {
|
|
|
347
347
|
export function assertOwnedUiStatusView(status) {
|
|
348
348
|
assertBoundedText(status.title, "owned-UI status title", MAX_LABEL_LENGTH);
|
|
349
349
|
assertOptionalText(status.workingMessage, "owned-UI working message", MAX_MESSAGE_LENGTH);
|
|
350
|
+
if (status.workingProgress !== undefined && status.workingProgress !== null) {
|
|
351
|
+
assertIntegerInRange(status.workingProgress, 0, 99, "owned-UI working progress");
|
|
352
|
+
}
|
|
350
353
|
assertCollection(status.diagnostics, "owned-UI status diagnostics", MAX_STATUS_DIAGNOSTICS);
|
|
351
354
|
for (const diagnostic of status.diagnostics) {
|
|
352
355
|
assertBoundedText(diagnostic, "owned-UI status diagnostic", MAX_MESSAGE_LENGTH);
|
|
@@ -75,19 +75,29 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
|
|
|
75
75
|
const statusUi = createTuiFacade(runtime ?? { getColumns: () => 80, getRows: () => 24, requestRender() { } });
|
|
76
76
|
let workingOverride;
|
|
77
77
|
let outputPad = PINNED_PI_LAYOUT.outputPad;
|
|
78
|
+
let progressPresentation = "pinned";
|
|
78
79
|
let placement = statusPlacement(view, workingOverride);
|
|
79
|
-
|
|
80
|
-
let
|
|
80
|
+
const liveStatusText = () => formatProgressStatus(liveWorkingText(view, workingOverride, progressPresentation));
|
|
81
|
+
let component = statusComponent(view, statusUi, outputPad, liveStatusText, placement);
|
|
82
|
+
let signature = statusSignature(view, workingOverride, outputPad, placement, progressPresentation);
|
|
81
83
|
const rebuild = () => {
|
|
82
84
|
const nextPlacement = statusPlacement(view, workingOverride);
|
|
83
|
-
const nextSignature = statusSignature(view, workingOverride, outputPad, nextPlacement);
|
|
85
|
+
const nextSignature = statusSignature(view, workingOverride, outputPad, nextPlacement, progressPresentation);
|
|
84
86
|
if (nextSignature === signature)
|
|
85
87
|
return;
|
|
88
|
+
// Performance: progress ticks change only the live message; the spinner keeps its frame and timer.
|
|
89
|
+
if (placement === "live" && nextPlacement === "live" && component instanceof WorkingStatusIndicator
|
|
90
|
+
&& statusSignature(view, workingOverride, outputPad, nextPlacement, progressPresentation, false)
|
|
91
|
+
=== statusSignature(view, workingOverride, outputPad, placement, progressPresentation, false)) {
|
|
92
|
+
component.setMessage(liveStatusText());
|
|
93
|
+
signature = nextSignature;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
86
96
|
if (component !== undefined && "dispose" in component && typeof component.dispose === "function")
|
|
87
97
|
component.dispose();
|
|
88
98
|
placement = nextPlacement;
|
|
89
99
|
signature = nextSignature;
|
|
90
|
-
component = statusComponent(view, statusUi,
|
|
100
|
+
component = statusComponent(view, statusUi, outputPad, liveStatusText, placement);
|
|
91
101
|
};
|
|
92
102
|
return {
|
|
93
103
|
render: width => component?.render(width) ?? [],
|
|
@@ -107,6 +117,10 @@ export function createPiShellStatus(view, formatProgressStatus, runtime) {
|
|
|
107
117
|
outputPad = padding;
|
|
108
118
|
rebuild();
|
|
109
119
|
},
|
|
120
|
+
setProgressPresentation(presentation) {
|
|
121
|
+
progressPresentation = presentation;
|
|
122
|
+
rebuild();
|
|
123
|
+
},
|
|
110
124
|
dispose() {
|
|
111
125
|
if (component !== undefined && "dispose" in component && typeof component.dispose === "function")
|
|
112
126
|
component.dispose();
|
|
@@ -145,9 +159,18 @@ function statusPlacement(view, workingOverride) {
|
|
|
145
159
|
return "dock";
|
|
146
160
|
return view.status.workingMessage === null ? "hidden" : "dock";
|
|
147
161
|
}
|
|
148
|
-
|
|
162
|
+
// Rationale: the engine publishes the semantic working word and a separate measured percent; bare A1
|
|
163
|
+
// composes them here, while the pinned route keeps the bare word for comparison with Pi.
|
|
164
|
+
function liveWorkingText(view, workingOverride, progressPresentation) {
|
|
165
|
+
const message = workingOverride ?? view.status.workingMessage ?? "Working";
|
|
166
|
+
const progress = view.status.workingProgress;
|
|
167
|
+
return workingOverride === undefined && progressPresentation === "custom-viewport" && typeof progress === "number"
|
|
168
|
+
? `${message} (${progress}%)`
|
|
169
|
+
: message;
|
|
170
|
+
}
|
|
171
|
+
function statusComponent(view, ui, outputPad, liveStatusText, placement) {
|
|
149
172
|
if (placement === "live") {
|
|
150
|
-
return new WorkingStatusIndicator(ui,
|
|
173
|
+
return new WorkingStatusIndicator(ui, liveStatusText());
|
|
151
174
|
}
|
|
152
175
|
if (placement === "dock") {
|
|
153
176
|
if (view.lifecycle === "failed") {
|
|
@@ -157,8 +180,9 @@ function statusComponent(view, ui, workingOverride, outputPad, formatProgressSta
|
|
|
157
180
|
}
|
|
158
181
|
return undefined;
|
|
159
182
|
}
|
|
160
|
-
function statusSignature(view, workingOverride, outputPad, placement) {
|
|
161
|
-
|
|
183
|
+
function statusSignature(view, workingOverride, outputPad, placement, progressPresentation, withMessage = true) {
|
|
184
|
+
const message = withMessage ? `${workingOverride ?? ""}\u0000${view.status.workingMessage ?? ""}\u0000${view.status.workingProgress ?? ""}` : "";
|
|
185
|
+
return `${placement}\u0000${outputPad}\u0000${view.lifecycle}\u0000${progressPresentation}\u0000${message}\u0000${view.status.diagnostics.at(-1) ?? ""}`;
|
|
162
186
|
}
|
|
163
187
|
function queuedInputText(submissions, presentation) {
|
|
164
188
|
if (submissions.length === 0)
|
|
@@ -79,6 +79,8 @@ export type PiShellStatusPlacement = "live" | "dock" | "hidden";
|
|
|
79
79
|
export interface PiShellStatusPort extends PiShellViewComponentPort {
|
|
80
80
|
setWorkingOverride(message: string | undefined): void;
|
|
81
81
|
setOutputPad(padding: 0 | 1): void;
|
|
82
|
+
/** Bare A1 shows engine-measured progress beside the working word; the pinned route keeps the bare word. */
|
|
83
|
+
setProgressPresentation(presentation: "pinned" | "custom-viewport"): void;
|
|
82
84
|
/** Semantic row placement; callers must not inspect rendered text. */
|
|
83
85
|
placement(): PiShellStatusPlacement;
|
|
84
86
|
renderDock(width: number): readonly string[];
|
|
@@ -14,6 +14,7 @@ import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACK
|
|
|
14
14
|
import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
|
|
15
15
|
import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
16
16
|
import { PiSessionCommandIntegration } from "./session-integration.js";
|
|
17
|
+
import { observeCompactionProgress } from "./compaction-progress.js";
|
|
17
18
|
import { PiSettingsIntegration } from "./settings-integration.js";
|
|
18
19
|
import { PendingEngineDelivery } from "./pending-delivery.js";
|
|
19
20
|
/** Explicit flush failure when required delivery was interrupted rather than completed. */
|
|
@@ -147,6 +148,7 @@ export class PiEngineAdapter {
|
|
|
147
148
|
#assistantResponseSequence = 0;
|
|
148
149
|
#statusKind = null;
|
|
149
150
|
#sessionCommands;
|
|
151
|
+
#compactionProgress = null;
|
|
150
152
|
#gitBranch = null;
|
|
151
153
|
#extensionUi;
|
|
152
154
|
#extensionShutdown;
|
|
@@ -1059,6 +1061,7 @@ export class PiEngineAdapter {
|
|
|
1059
1061
|
clearQueuedWorkflows() {
|
|
1060
1062
|
const session = this.#requireWorkflowSession();
|
|
1061
1063
|
const result = session.clearQueue?.();
|
|
1064
|
+
this.#sessionCommands?.forgetQueuedImages();
|
|
1062
1065
|
if (!isRecord(result))
|
|
1063
1066
|
return [];
|
|
1064
1067
|
return [...readStringArray(result.steering), ...readStringArray(result.followUp)];
|
|
@@ -1257,6 +1260,8 @@ export class PiEngineAdapter {
|
|
|
1257
1260
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopping", reason: null });
|
|
1258
1261
|
this.#unsubscribe?.();
|
|
1259
1262
|
this.#unsubscribe = undefined;
|
|
1263
|
+
this.#compactionProgress?.dispose();
|
|
1264
|
+
this.#compactionProgress = null;
|
|
1260
1265
|
await this.#runtime?.dispose();
|
|
1261
1266
|
this.#lifecycle = "stopped";
|
|
1262
1267
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopped", reason: null });
|
|
@@ -2016,6 +2021,11 @@ export class PiEngineAdapter {
|
|
|
2016
2021
|
this.#activeCommandIds = [];
|
|
2017
2022
|
this.#completedCommands.clear();
|
|
2018
2023
|
this.#sessionCommands = new PiSessionCommandIntegration(session);
|
|
2024
|
+
this.#compactionProgress?.dispose();
|
|
2025
|
+
this.#compactionProgress = observeCompactionProgress(session, percent => {
|
|
2026
|
+
if (this.#session === session && !this.#disposed)
|
|
2027
|
+
this.#publishCompactionProgress(percent);
|
|
2028
|
+
});
|
|
2019
2029
|
this.#editor = {
|
|
2020
2030
|
text: "",
|
|
2021
2031
|
queuedSubmissions: [],
|
|
@@ -2024,7 +2034,7 @@ export class PiEngineAdapter {
|
|
|
2024
2034
|
historyRevision: this.#editor.historyRevision + 1,
|
|
2025
2035
|
submitEnabled: true,
|
|
2026
2036
|
};
|
|
2027
|
-
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
2037
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null, badges: [] };
|
|
2028
2038
|
this.#statusKind = null;
|
|
2029
2039
|
this.#agentRunActive = false;
|
|
2030
2040
|
this.#agentRunSequence = 0;
|
|
@@ -2115,7 +2125,7 @@ export class PiEngineAdapter {
|
|
|
2115
2125
|
const wasBusy = this.#lifecycle === "busy";
|
|
2116
2126
|
this.#statusKind = kind;
|
|
2117
2127
|
this.#lifecycle = "busy";
|
|
2118
|
-
this.#status = { ...this.#status, workingMessage: message };
|
|
2128
|
+
this.#status = { ...this.#status, workingMessage: message, workingProgress: null };
|
|
2119
2129
|
if (!wasBusy)
|
|
2120
2130
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
2121
2131
|
this.#emitEvent({ type: "status", status: this.#status });
|
|
@@ -2133,10 +2143,32 @@ export class PiEngineAdapter {
|
|
|
2133
2143
|
#leaveWorkStates() {
|
|
2134
2144
|
this.#statusKind = null;
|
|
2135
2145
|
this.#lifecycle = "ready";
|
|
2136
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
2146
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
2137
2147
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
2138
2148
|
this.#emitEvent({ type: "status", status: this.#status });
|
|
2139
2149
|
}
|
|
2150
|
+
// Invariant: progress belongs to the compaction state only and is published when the integer changes.
|
|
2151
|
+
#publishCompactionProgress(percent) {
|
|
2152
|
+
if (this.#statusKind !== "compaction" || this.#status.workingProgress === percent)
|
|
2153
|
+
return;
|
|
2154
|
+
this.#status = { ...this.#status, workingProgress: percent };
|
|
2155
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
2156
|
+
}
|
|
2157
|
+
// Rationale: a manual compaction ends with an idle session, so the engine would never consume
|
|
2158
|
+
// the messages queued during it; automatic compaction returns to a run or a pending prompt.
|
|
2159
|
+
#deliverQueuedAfterCompaction() {
|
|
2160
|
+
const commands = this.#sessionCommands;
|
|
2161
|
+
const generation = this.#sessionGeneration;
|
|
2162
|
+
if (commands === undefined)
|
|
2163
|
+
return;
|
|
2164
|
+
const report = (error) => {
|
|
2165
|
+
if (generation !== this.#sessionGeneration || this.#disposed)
|
|
2166
|
+
return;
|
|
2167
|
+
this.#addDiagnostic("error", "engine-command", `Queued input could not be sent after compaction: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
2168
|
+
this.#emitView();
|
|
2169
|
+
};
|
|
2170
|
+
commands.deliverQueuedAfterCompaction(report).catch(report);
|
|
2171
|
+
}
|
|
2140
2172
|
#handlePiEvent(event) {
|
|
2141
2173
|
try {
|
|
2142
2174
|
this.#applyPiEvent(event);
|
|
@@ -2286,9 +2318,13 @@ export class PiEngineAdapter {
|
|
|
2286
2318
|
return;
|
|
2287
2319
|
case "compaction_start":
|
|
2288
2320
|
this.#enterWorkState("compaction", "Compacting");
|
|
2321
|
+
this.#compactionProgress?.begin();
|
|
2289
2322
|
return;
|
|
2290
2323
|
case "compaction_end":
|
|
2324
|
+
this.#compactionProgress?.end();
|
|
2291
2325
|
this.#endWorkState("compaction");
|
|
2326
|
+
if (event.reason === "manual")
|
|
2327
|
+
this.#deliverQueuedAfterCompaction();
|
|
2292
2328
|
return;
|
|
2293
2329
|
case "thinking_level_changed":
|
|
2294
2330
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -2797,10 +2833,12 @@ export class PiEngineAdapter {
|
|
|
2797
2833
|
this.#admissionStopped = !canResume;
|
|
2798
2834
|
this.#unsubscribe?.();
|
|
2799
2835
|
this.#unsubscribe = undefined;
|
|
2836
|
+
this.#compactionProgress?.dispose();
|
|
2837
|
+
this.#compactionProgress = null;
|
|
2800
2838
|
++this.#sessionGeneration;
|
|
2801
2839
|
this.#agentRunActive = false;
|
|
2802
2840
|
this.#statusKind = null;
|
|
2803
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
2841
|
+
this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
|
|
2804
2842
|
this.#lifecycle = this.#disposed ? "stopped" : canResume ? "ready" : "failed";
|
|
2805
2843
|
this.#editor = { ...this.#editor, submitEnabled: canResume && !this.#disposed };
|
|
2806
2844
|
this.#viewRevision += 1;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Expected summary size for a first compaction, when no previous summary on the branch can serve as the estimate. */
|
|
2
|
+
export declare const DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS = 4000;
|
|
3
|
+
/** The public session surface compaction progress reads: the agent's stream function and the branch entries. */
|
|
4
|
+
export interface CompactionProgressSession {
|
|
5
|
+
readonly agent?: {
|
|
6
|
+
streamFunction?: unknown;
|
|
7
|
+
} | undefined;
|
|
8
|
+
readonly sessionManager?: {
|
|
9
|
+
getBranch?(): readonly unknown[];
|
|
10
|
+
} | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface CompactionProgressObserver {
|
|
13
|
+
/** Marks `compaction_start`: resets the streamed count and captures the expected summary size. */
|
|
14
|
+
begin(): void;
|
|
15
|
+
/** Marks `compaction_end`: later stream chunks are ignored. */
|
|
16
|
+
end(): void;
|
|
17
|
+
/** Restores the original stream function; the observer reports nothing afterwards. */
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Observes the summarization stream of a compaction through the session agent's public stream
|
|
22
|
+
* function and reports an estimated integer percent. Pi never iterates that stream itself (it
|
|
23
|
+
* reads only its final result), so the observer iterates it in the background between
|
|
24
|
+
* `begin()` and `end()` and returns the same stream object to Pi. Outside that window the
|
|
25
|
+
* wrapper is a pass-through. Returns null when the agent exposes no callable stream function,
|
|
26
|
+
* in which case compaction proceeds without progress.
|
|
27
|
+
*/
|
|
28
|
+
export declare function observeCompactionProgress(session: CompactionProgressSession, onProgress: (percent: number) => void): CompactionProgressObserver | null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Expected summary size for a first compaction, when no previous summary on the branch can serve as the estimate. */
|
|
2
|
+
export const DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS = 4000;
|
|
3
|
+
/** The percent shown while a compaction is still running never reaches this value. */
|
|
4
|
+
const MAX_RUNNING_PERCENT = 99;
|
|
5
|
+
/**
|
|
6
|
+
* Observes the summarization stream of a compaction through the session agent's public stream
|
|
7
|
+
* function and reports an estimated integer percent. Pi never iterates that stream itself (it
|
|
8
|
+
* reads only its final result), so the observer iterates it in the background between
|
|
9
|
+
* `begin()` and `end()` and returns the same stream object to Pi. Outside that window the
|
|
10
|
+
* wrapper is a pass-through. Returns null when the agent exposes no callable stream function,
|
|
11
|
+
* in which case compaction proceeds without progress.
|
|
12
|
+
*/
|
|
13
|
+
export function observeCompactionProgress(session, onProgress) {
|
|
14
|
+
const agent = session.agent;
|
|
15
|
+
if (agent === undefined || typeof agent.streamFunction !== "function")
|
|
16
|
+
return null;
|
|
17
|
+
const original = agent.streamFunction;
|
|
18
|
+
let active = false;
|
|
19
|
+
let disposed = false;
|
|
20
|
+
let generation = 0;
|
|
21
|
+
let streamed = 0;
|
|
22
|
+
let expected = DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
|
|
23
|
+
let reported = null;
|
|
24
|
+
const report = () => {
|
|
25
|
+
const percent = Math.min(MAX_RUNNING_PERCENT, Math.floor((100 * streamed) / expected));
|
|
26
|
+
if (percent === reported)
|
|
27
|
+
return;
|
|
28
|
+
reported = percent;
|
|
29
|
+
onProgress(percent);
|
|
30
|
+
};
|
|
31
|
+
const wrapped = async (model, context, options) => {
|
|
32
|
+
const stream = await original(model, context, options);
|
|
33
|
+
if (!active || disposed)
|
|
34
|
+
return stream;
|
|
35
|
+
const observed = generation;
|
|
36
|
+
void (async () => {
|
|
37
|
+
report();
|
|
38
|
+
// Invariant: observation never affects the request; a stream that cannot be iterated ends observation only.
|
|
39
|
+
try {
|
|
40
|
+
for await (const event of stream) {
|
|
41
|
+
if (observed !== generation || !active)
|
|
42
|
+
return;
|
|
43
|
+
if (event.type === "text_delta") {
|
|
44
|
+
streamed += event.delta.length;
|
|
45
|
+
report();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
return stream;
|
|
54
|
+
};
|
|
55
|
+
agent.streamFunction = wrapped;
|
|
56
|
+
return {
|
|
57
|
+
begin() {
|
|
58
|
+
generation += 1;
|
|
59
|
+
streamed = 0;
|
|
60
|
+
reported = null;
|
|
61
|
+
expected = latestCompactionSummaryLength(session) ?? DEFAULT_EXPECTED_COMPACTION_SUMMARY_CHARS;
|
|
62
|
+
active = true;
|
|
63
|
+
},
|
|
64
|
+
end() {
|
|
65
|
+
active = false;
|
|
66
|
+
generation += 1;
|
|
67
|
+
},
|
|
68
|
+
dispose() {
|
|
69
|
+
if (disposed)
|
|
70
|
+
return;
|
|
71
|
+
disposed = true;
|
|
72
|
+
active = false;
|
|
73
|
+
generation += 1;
|
|
74
|
+
if (agent.streamFunction === wrapped)
|
|
75
|
+
agent.streamFunction = original;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function latestCompactionSummaryLength(session) {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = session.sessionManager?.getBranch?.() ?? [];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
88
|
+
const entry = entries[index];
|
|
89
|
+
if (typeof entry !== "object" || entry === null)
|
|
90
|
+
continue;
|
|
91
|
+
const { type, summary } = entry;
|
|
92
|
+
if (type === "compaction" && typeof summary === "string" && summary.length > 0)
|
|
93
|
+
return summary.length;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
@@ -6,7 +6,7 @@ export interface PiCapabilityConformanceResult {
|
|
|
6
6
|
export declare const REQUIRED_PI_CAPABILITY_OPERATIONS: Readonly<{
|
|
7
7
|
readonly "public-exports": readonly ["services.create", "session.create", "runtime.create"];
|
|
8
8
|
readonly "session-lifecycle": readonly ["session.new", "session.resume", "session.rebind", "session.dispose"];
|
|
9
|
-
readonly "commands-events": readonly ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
9
|
+
readonly "commands-events": readonly ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
10
10
|
readonly "models-authentication": readonly ["models.list", "models.refresh", "models.completeSimple", "auth.status", "auth.login", "auth.logout", "auth.cancel"];
|
|
11
11
|
readonly settings: readonly ["settings.read", "settings.write", "settings.flush"];
|
|
12
12
|
readonly "resources-extensions": readonly ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"];
|
|
@@ -8,7 +8,7 @@ import { createWindowsNulCleanupExtension } from "./windows-filesystem-hygiene.j
|
|
|
8
8
|
export const REQUIRED_PI_CAPABILITY_OPERATIONS = Object.freeze({
|
|
9
9
|
"public-exports": ["services.create", "session.create", "runtime.create"],
|
|
10
10
|
"session-lifecycle": ["session.new", "session.resume", "session.rebind", "session.dispose"],
|
|
11
|
-
"commands-events": ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"],
|
|
11
|
+
"commands-events": ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"],
|
|
12
12
|
"models-authentication": ["models.list", "models.refresh", "models.completeSimple", "auth.status", "auth.login", "auth.logout", "auth.cancel"],
|
|
13
13
|
settings: ["settings.read", "settings.write", "settings.flush"],
|
|
14
14
|
"resources-extensions": ["resources.discover", "extensions.inline", "extensions.bind", "extensions.reload", "renderers.invoke"],
|
|
@@ -85,7 +85,7 @@ export async function runPiUpgradeConformance() {
|
|
|
85
85
|
});
|
|
86
86
|
const session = created.session;
|
|
87
87
|
sessionId = session.sessionId;
|
|
88
|
-
requireMethods(session, "session commands", ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"]);
|
|
88
|
+
requireMethods(session, "session commands", ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"]);
|
|
89
89
|
requireMethods(services.modelRuntime, "models/authentication", ["getModels", "getModel", "completeSimple", "checkAuth", "login", "logout", "refresh"]);
|
|
90
90
|
requireMethods(services.settingsManager, "settings", ["getGlobalSettings", "getProjectSettings", "flush"]);
|
|
91
91
|
requireMethods(services.resourceLoader, "resources/extensions", ["getExtensions", "getSkills", "getPrompts", "getThemes", "reload"]);
|
|
@@ -96,7 +96,7 @@ export async function runPiUpgradeConformance() {
|
|
|
96
96
|
catch (error) {
|
|
97
97
|
throw new PiUpgradeConformanceError("session", error);
|
|
98
98
|
}
|
|
99
|
-
const commandSurface = ["prompt", "steer", "followUp", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
99
|
+
const commandSurface = ["prompt", "steer", "followUp", "clearQueue", "abort", "compact", "setModel", "setThinkingLevel", "subscribe", "dispose"];
|
|
100
100
|
const capabilities = Object.entries(REQUIRED_PI_CAPABILITY_OPERATIONS)
|
|
101
101
|
.map(([capabilityName, operations]) => capability(capabilityName, operations));
|
|
102
102
|
validatePiCapabilityResults(VERSION, capabilities);
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
import type { AgentSession, AgentSessionEvent, PromptOptions } from "../startup-public.js";
|
|
2
2
|
import { type AgentCommandOutcome, type AgentEvent } from "../../../contracts/agent-engine/index.js";
|
|
3
|
+
type PiPromptImages = NonNullable<PromptOptions["images"]>;
|
|
3
4
|
export interface PiDocumentedSessionCommands {
|
|
4
5
|
readonly isStreaming: AgentSession["isStreaming"];
|
|
5
6
|
readonly isRetrying: AgentSession["isRetrying"];
|
|
6
7
|
readonly isCompacting: AgentSession["isCompacting"];
|
|
7
8
|
prompt(text: string, options?: Parameters<AgentSession["prompt"]>[1]): Promise<void>;
|
|
8
|
-
steer(text: string): Promise<void>;
|
|
9
|
-
followUp(text: string): Promise<void>;
|
|
9
|
+
steer(text: string, images?: PiPromptImages): Promise<void>;
|
|
10
|
+
followUp(text: string, images?: PiPromptImages): Promise<void>;
|
|
10
11
|
abort(): Promise<void>;
|
|
11
12
|
abortRetry(): void;
|
|
12
13
|
abortCompaction(): void;
|
|
13
14
|
compact(customInstructions?: Parameters<AgentSession["compact"]>[0]): Promise<unknown>;
|
|
15
|
+
clearQueue(): {
|
|
16
|
+
readonly steering: readonly string[];
|
|
17
|
+
readonly followUp: readonly string[];
|
|
18
|
+
};
|
|
14
19
|
executeBash?(command: string, onChunk: unknown, options: {
|
|
15
20
|
readonly excludeFromContext: boolean;
|
|
16
21
|
}): Promise<unknown>;
|
|
22
|
+
/** Optional: extension commands cannot be queued, so they run immediately during compaction as in pinned Pi. */
|
|
23
|
+
readonly extensionRunner?: {
|
|
24
|
+
getCommand(name: string): unknown;
|
|
25
|
+
} | undefined;
|
|
17
26
|
}
|
|
18
27
|
export type PiSessionCommand = {
|
|
19
28
|
readonly type: "prompt" | "steer" | "follow-up";
|
|
20
29
|
readonly text: string;
|
|
21
|
-
readonly images?:
|
|
30
|
+
readonly images?: PiPromptImages;
|
|
22
31
|
} | {
|
|
23
32
|
readonly type: "abort" | "retry" | "compact";
|
|
24
33
|
} | {
|
|
@@ -37,9 +46,22 @@ export declare class PiSessionCommandIntegration {
|
|
|
37
46
|
private readonly session;
|
|
38
47
|
constructor(session: PiDocumentedSessionCommands);
|
|
39
48
|
execute(command: PiSessionCommand): Promise<PiSessionCommandResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Delivers the messages queued during a manual compaction. The session is idle when a manual
|
|
51
|
+
* compaction ends, so nothing would consume the queue: the first message starts one run
|
|
52
|
+
* through the ordinary prompt path with its mode, and the rest are re-queued for that run,
|
|
53
|
+
* whose initial poll injects them in order. Automatic compaction needs no delivery: its
|
|
54
|
+
* continuing run or pending prompt consumes the queue itself. The run is not awaited; when
|
|
55
|
+
* the start is refused before the message is accepted, the queue is restored and the error
|
|
56
|
+
* reported through `onFailure`.
|
|
57
|
+
*/
|
|
58
|
+
deliverQueuedAfterCompaction(onFailure: (error: unknown) => void): Promise<void>;
|
|
59
|
+
/** Forgets the attachments kept for queued messages; the queue itself was cleared by the caller. */
|
|
60
|
+
forgetQueuedImages(): void;
|
|
40
61
|
}
|
|
41
62
|
export interface PiOrderedEventIntegration {
|
|
42
63
|
dispose(): void;
|
|
43
64
|
}
|
|
44
65
|
export declare function subscribeToPiSessionEvents(session: Pick<AgentSession, "subscribe">, sessionId: string, emit: (event: AgentEvent) => void, malformed: (diagnostic: string) => void): PiOrderedEventIntegration;
|
|
45
66
|
export declare function convertPiSessionEvent(event: AgentSessionEvent, sessionId: string, sequence: number): AgentEvent | null;
|
|
67
|
+
export {};
|
|
@@ -3,6 +3,9 @@ import { AGENT_ENGINE_CONTRACT_VERSION, } from "../../../contracts/agent-engine/
|
|
|
3
3
|
export class PiSessionCommandIntegration {
|
|
4
4
|
session;
|
|
5
5
|
#lastPrompt = null;
|
|
6
|
+
// Rationale: Pi's queue restore returns text only, so the attachments of messages queued
|
|
7
|
+
// during compaction are kept here until the queue is delivered or cleared.
|
|
8
|
+
#queuedImages = [];
|
|
6
9
|
constructor(session) {
|
|
7
10
|
this.session = session;
|
|
8
11
|
}
|
|
@@ -18,22 +21,24 @@ export class PiSessionCommandIntegration {
|
|
|
18
21
|
});
|
|
19
22
|
return { outcome: "completed" };
|
|
20
23
|
case "steer":
|
|
24
|
+
case "follow-up": {
|
|
21
25
|
this.#lastPrompt = command.text;
|
|
26
|
+
const mode = command.type === "steer" ? "steer" : "followUp";
|
|
27
|
+
if (this.session.isCompacting && !this.#isExtensionCommand(command.text)) {
|
|
28
|
+
// Compatibility: match interactive Pi: prompt() refuses input during manual compaction,
|
|
29
|
+
// while the engine queue accepts it at any time and delivers it when compaction ends.
|
|
30
|
+
await this.#queue(mode, command.text, command.images);
|
|
31
|
+
return { outcome: "completed" };
|
|
32
|
+
}
|
|
22
33
|
// Compatibility: match interactive Pi: prompt() owns template/extension expansion and
|
|
23
34
|
// turns the accepted steering message into the visible user row while
|
|
24
35
|
// later messages remain in the pending queue.
|
|
25
36
|
await this.session.prompt(command.text, {
|
|
26
|
-
streamingBehavior:
|
|
27
|
-
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
28
|
-
});
|
|
29
|
-
return { outcome: "completed" };
|
|
30
|
-
case "follow-up":
|
|
31
|
-
this.#lastPrompt = command.text;
|
|
32
|
-
await this.session.prompt(command.text, {
|
|
33
|
-
streamingBehavior: "followUp",
|
|
37
|
+
streamingBehavior: mode,
|
|
34
38
|
...(command.images === undefined ? {} : { images: [...command.images] }),
|
|
35
39
|
});
|
|
36
40
|
return { outcome: "completed" };
|
|
41
|
+
}
|
|
37
42
|
case "abort":
|
|
38
43
|
if (this.session.isRetrying)
|
|
39
44
|
this.session.abortRetry();
|
|
@@ -61,6 +66,86 @@ export class PiSessionCommandIntegration {
|
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Delivers the messages queued during a manual compaction. The session is idle when a manual
|
|
71
|
+
* compaction ends, so nothing would consume the queue: the first message starts one run
|
|
72
|
+
* through the ordinary prompt path with its mode, and the rest are re-queued for that run,
|
|
73
|
+
* whose initial poll injects them in order. Automatic compaction needs no delivery: its
|
|
74
|
+
* continuing run or pending prompt consumes the queue itself. The run is not awaited; when
|
|
75
|
+
* the start is refused before the message is accepted, the queue is restored and the error
|
|
76
|
+
* reported through `onFailure`.
|
|
77
|
+
*/
|
|
78
|
+
async deliverQueuedAfterCompaction(onFailure) {
|
|
79
|
+
if (this.session.isStreaming || this.session.isCompacting)
|
|
80
|
+
return;
|
|
81
|
+
const { steering, followUp } = this.session.clearQueue();
|
|
82
|
+
const queued = [
|
|
83
|
+
...steering.map(text => ({ mode: "steer", text, images: this.#takeImages(text) })),
|
|
84
|
+
...followUp.map(text => ({ mode: "followUp", text, images: this.#takeImages(text) })),
|
|
85
|
+
];
|
|
86
|
+
this.#queuedImages = [];
|
|
87
|
+
const [first, ...rest] = queued;
|
|
88
|
+
if (first === undefined)
|
|
89
|
+
return;
|
|
90
|
+
this.#lastPrompt = first.text;
|
|
91
|
+
let accepted = false;
|
|
92
|
+
const started = this.session.prompt(first.text, {
|
|
93
|
+
streamingBehavior: first.mode,
|
|
94
|
+
preflightResult: success => { accepted = success; },
|
|
95
|
+
...(first.images === undefined ? {} : { images: [...first.images] }),
|
|
96
|
+
});
|
|
97
|
+
for (const item of rest)
|
|
98
|
+
await this.#queue(item.mode, item.text, item.images);
|
|
99
|
+
started.catch(async (error) => {
|
|
100
|
+
// Security: a run that reached the provider is never resent; only a refused start restores.
|
|
101
|
+
try {
|
|
102
|
+
if (!accepted)
|
|
103
|
+
await this.#restoreQueue(queued);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
onFailure(error);
|
|
107
|
+
}
|
|
108
|
+
}).catch(() => undefined);
|
|
109
|
+
}
|
|
110
|
+
/** Forgets the attachments kept for queued messages; the queue itself was cleared by the caller. */
|
|
111
|
+
forgetQueuedImages() {
|
|
112
|
+
this.#queuedImages = [];
|
|
113
|
+
}
|
|
114
|
+
// Rationale: the rest were re-queued before the refused start surfaced, so the queue is rebuilt
|
|
115
|
+
// in the original order; anything queued in between keeps its place after it.
|
|
116
|
+
async #restoreQueue(queued) {
|
|
117
|
+
const { steering, followUp } = this.session.clearQueue();
|
|
118
|
+
const restored = new Set(queued.map(item => item.text));
|
|
119
|
+
const extra = [
|
|
120
|
+
...steering.filter(text => !restored.has(text)).map(text => ({ mode: "steer", text, images: this.#takeImages(text) })),
|
|
121
|
+
...followUp.filter(text => !restored.has(text)).map(text => ({ mode: "followUp", text, images: this.#takeImages(text) })),
|
|
122
|
+
];
|
|
123
|
+
this.#queuedImages = [];
|
|
124
|
+
for (const item of [...queued, ...extra])
|
|
125
|
+
await this.#queue(item.mode, item.text, item.images);
|
|
126
|
+
}
|
|
127
|
+
async #queue(mode, text, images) {
|
|
128
|
+
if (images !== undefined && images.length > 0)
|
|
129
|
+
this.#queuedImages.push({ text, images: [...images] });
|
|
130
|
+
if (mode === "steer")
|
|
131
|
+
await this.session.steer(text, images === undefined ? undefined : [...images]);
|
|
132
|
+
else
|
|
133
|
+
await this.session.followUp(text, images === undefined ? undefined : [...images]);
|
|
134
|
+
}
|
|
135
|
+
#takeImages(text) {
|
|
136
|
+
const index = this.#queuedImages.findIndex(item => item.text === text);
|
|
137
|
+
if (index === -1)
|
|
138
|
+
return undefined;
|
|
139
|
+
const [item] = this.#queuedImages.splice(index, 1);
|
|
140
|
+
return item?.images;
|
|
141
|
+
}
|
|
142
|
+
#isExtensionCommand(text) {
|
|
143
|
+
if (!text.startsWith("/"))
|
|
144
|
+
return false;
|
|
145
|
+
const spaceIndex = text.indexOf(" ");
|
|
146
|
+
const name = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
147
|
+
return this.session.extensionRunner?.getCommand(name) !== undefined;
|
|
148
|
+
}
|
|
64
149
|
}
|
|
65
150
|
export function subscribeToPiSessionEvents(session, sessionId, emit, malformed) {
|
|
66
151
|
let sequence = 0;
|
|
@@ -59,6 +59,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
59
59
|
#workflowStatusAnchors = new Map();
|
|
60
60
|
#workflowStatusMessages = new Map();
|
|
61
61
|
#lastWorkflowStatusId;
|
|
62
|
+
// Invariant: the notice is dock chrome, never transcript content; the custom viewport alone uses it.
|
|
63
|
+
#dockNotice;
|
|
62
64
|
#inputSurface;
|
|
63
65
|
#inputSurfaceCoordination = "editor";
|
|
64
66
|
#dockInputReuseEnabled;
|
|
@@ -103,6 +105,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
103
105
|
});
|
|
104
106
|
this.resources = createPiShellLoadedResources(startup.resources ?? [], startup.expanded ?? false);
|
|
105
107
|
this.#status = createPiShellStatus(view, progressStatusText, handlers);
|
|
108
|
+
this.#status.setProgressPresentation(this.#customViewport ? "custom-viewport" : "pinned");
|
|
106
109
|
this.#footer = createPiShellFooter(this.#viewWithExtensionStatuses(view), cwd, this.#customViewport ? "a1" : "pi");
|
|
107
110
|
this.#queued = createPiQueuedInputStatus(view.editor.queuedSubmissions, this.#customViewport ? "custom-viewport" : "pinned");
|
|
108
111
|
this.editor = createPiShellEditor({
|
|
@@ -340,6 +343,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
340
343
|
this.#documentLayouts.clear();
|
|
341
344
|
}
|
|
342
345
|
#mountTranscript(block) {
|
|
346
|
+
// Invariant: new content dismisses the notice; a revision of a mounted block keeps it.
|
|
347
|
+
this.#dismissDockNotice();
|
|
343
348
|
const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers, this.#submittedPromptComposer, this.#outputPad, !this.#thinkingVisible, this.#mermaidRenderingMode, this.#showImages, this.#imageWidthCells, this.#imageAssets, { ...this.#componentRuntime, changed: () => {
|
|
344
349
|
if (this.#transcript.get(block.id) !== created)
|
|
345
350
|
return;
|
|
@@ -521,7 +526,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
521
526
|
#renderDockLayout(width) {
|
|
522
527
|
const queued = this.#customViewport ? [] : this.#renderQueued(width);
|
|
523
528
|
const statusRows = this.#customViewport ? this.#status.renderDock(width) : this.#renderStatus(width);
|
|
524
|
-
const transientRows = [...queued, ...statusRows];
|
|
529
|
+
const transientRows = [...queued, ...statusRows, ...this.#renderDockNotice(width)];
|
|
525
530
|
const aboveWidgets = this.#renderWidgets("aboveEditor", width);
|
|
526
531
|
const input = this.#inputSurface.render(width);
|
|
527
532
|
// Invariant: pointer rows describe the body, not the autocomplete block now preceding it.
|
|
@@ -535,6 +540,17 @@ export class OwnedUiSessionShellRoot {
|
|
|
535
540
|
inputRows: body?.rowCount ?? input.length,
|
|
536
541
|
};
|
|
537
542
|
}
|
|
543
|
+
#renderDockNotice(width) {
|
|
544
|
+
if (this.#dockNotice === undefined)
|
|
545
|
+
return [];
|
|
546
|
+
return ["", ...renderPiShellStatusText(this.#dockNotice, width, this.#outputPad)];
|
|
547
|
+
}
|
|
548
|
+
#dismissDockNotice() {
|
|
549
|
+
if (this.#dockNotice === undefined)
|
|
550
|
+
return;
|
|
551
|
+
this.#dockNotice = undefined;
|
|
552
|
+
this.#invalidateChrome();
|
|
553
|
+
}
|
|
538
554
|
#renderQueued(width) {
|
|
539
555
|
return this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
|
|
540
556
|
}
|
|
@@ -733,6 +749,14 @@ export class OwnedUiSessionShellRoot {
|
|
|
733
749
|
return rows.join("\n");
|
|
734
750
|
}
|
|
735
751
|
appendWorkflowStatus(message) {
|
|
752
|
+
// Rationale: bare A1 acknowledges commands in one dock notice above the editor, where the
|
|
753
|
+
// reader's eye already is, instead of a transcript row that opens an empty session at the
|
|
754
|
+
// top-left or sinks into a long feed. The pinned route keeps Pi's chat placement.
|
|
755
|
+
if (this.#customViewport) {
|
|
756
|
+
this.#dockNotice = message;
|
|
757
|
+
this.#invalidateChrome();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
736
760
|
const previousId = this.#lastWorkflowStatusId;
|
|
737
761
|
if (previousId !== undefined
|
|
738
762
|
&& this.#transcriptOrder.at(-1) === previousId
|
|
@@ -872,6 +896,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
872
896
|
this.invalidate();
|
|
873
897
|
}
|
|
874
898
|
resetWorkflowPresentation() {
|
|
899
|
+
this.#dockNotice = undefined;
|
|
875
900
|
for (const id of this.#workflowStatusAnchors.keys()) {
|
|
876
901
|
this.#transcript.get(id)?.dispose?.();
|
|
877
902
|
this.#transcript.delete(id);
|
|
@@ -1056,6 +1081,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
1056
1081
|
this.#transcriptOrder = order;
|
|
1057
1082
|
}
|
|
1058
1083
|
#appendAnchoredWorkflowComponent(render, dispose) {
|
|
1084
|
+
// Invariant: transcript-bound workflow output supersedes a pending acknowledgement.
|
|
1085
|
+
this.#dismissDockNotice();
|
|
1059
1086
|
this.#workflowTranscriptSequence += 1;
|
|
1060
1087
|
const id = `workflow-status-${this.#workflowTranscriptSequence}`;
|
|
1061
1088
|
const component = {
|
|
@@ -66,7 +66,6 @@ export class OwnedUiSessionShell {
|
|
|
66
66
|
#showImages = true;
|
|
67
67
|
#imageWidthCells = 80;
|
|
68
68
|
#fullscreenExitOutput = "transcript";
|
|
69
|
-
#compactionQueue = [];
|
|
70
69
|
#waitingImages = new Map();
|
|
71
70
|
#lastClearTime = 0;
|
|
72
71
|
#lastEscapeTime = 0;
|
|
@@ -494,8 +493,6 @@ export class OwnedUiSessionShell {
|
|
|
494
493
|
model: event.model,
|
|
495
494
|
});
|
|
496
495
|
}
|
|
497
|
-
if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
|
|
498
|
-
void this.#flushCompactionQueue();
|
|
499
496
|
if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
|
|
500
497
|
this.#settleStoppedLifecycle();
|
|
501
498
|
});
|
|
@@ -642,18 +639,8 @@ export class OwnedUiSessionShell {
|
|
|
642
639
|
}
|
|
643
640
|
}
|
|
644
641
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
this.#compactionQueue.push({
|
|
648
|
-
text: input,
|
|
649
|
-
draft: displayInput,
|
|
650
|
-
type: "steer",
|
|
651
|
-
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
652
|
-
});
|
|
653
|
-
this.root.appendWorkflowResult({ command: "compact", outcome: "completed", message: `Queued during compaction: ${input}` });
|
|
654
|
-
this.runtime.requestRender();
|
|
655
|
-
return { outcome: "completed", diagnostic: null };
|
|
656
|
-
}
|
|
642
|
+
// Compatibility: match interactive Pi: input during compaction is queued steering; the engine
|
|
643
|
+
// shows it in the pending rows and delivers it when compaction ends.
|
|
657
644
|
const type = this.view().lifecycle === "busy" ? "steer" : "prompt";
|
|
658
645
|
this.#rememberInput(displayInput, type);
|
|
659
646
|
this.root.resumeViewportFollowing();
|
|
@@ -772,15 +759,6 @@ export class OwnedUiSessionShell {
|
|
|
772
759
|
if (this.root.editor.getText() === draft)
|
|
773
760
|
this.root.editor.setText("");
|
|
774
761
|
this.root.resumeViewportFollowing();
|
|
775
|
-
if (this.view().status.workingMessage?.startsWith("Compacting") === true) {
|
|
776
|
-
this.#compactionQueue.push({
|
|
777
|
-
text,
|
|
778
|
-
draft: displayInput,
|
|
779
|
-
type: "follow-up",
|
|
780
|
-
...(prepared.images.length === 0 ? {} : { images: prepared.images }),
|
|
781
|
-
});
|
|
782
|
-
return { outcome: "completed", diagnostic: null };
|
|
783
|
-
}
|
|
784
762
|
return this.#execute({
|
|
785
763
|
type: "follow-up",
|
|
786
764
|
correlationId: this.#correlation("follow-up"),
|
|
@@ -790,9 +768,8 @@ export class OwnedUiSessionShell {
|
|
|
790
768
|
}, displayInput);
|
|
791
769
|
}
|
|
792
770
|
restoreQueuedInput() {
|
|
793
|
-
const queued = [...this.#waitingImages.keys(), ...this.backend.clearQueuedWorkflows()
|
|
771
|
+
const queued = [...this.#waitingImages.keys(), ...this.backend.clearQueuedWorkflows()];
|
|
794
772
|
this.#cancelWaitingImages();
|
|
795
|
-
this.#compactionQueue = [];
|
|
796
773
|
if (queued.length === 0)
|
|
797
774
|
return;
|
|
798
775
|
this.root.editor.setText(queued.join("\n"));
|
|
@@ -1773,19 +1750,6 @@ export class OwnedUiSessionShell {
|
|
|
1773
1750
|
this.root.setInputSurface(null);
|
|
1774
1751
|
this.runtime.requestRender();
|
|
1775
1752
|
}
|
|
1776
|
-
async #flushCompactionQueue() {
|
|
1777
|
-
const queued = this.#compactionQueue;
|
|
1778
|
-
this.#compactionQueue = [];
|
|
1779
|
-
for (const item of queued) {
|
|
1780
|
-
await this.#execute({
|
|
1781
|
-
type: item.type,
|
|
1782
|
-
correlationId: this.#correlation(`compaction-${item.type}`),
|
|
1783
|
-
sessionId: this.backend.sessionId,
|
|
1784
|
-
text: item.text,
|
|
1785
|
-
...(item.images === undefined ? {} : { images: item.images }),
|
|
1786
|
-
}, item.draft);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
1753
|
async #execute(command, draft) {
|
|
1790
1754
|
if (draft === undefined)
|
|
1791
1755
|
return this.backend.execute(command);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-17T18:17:47.242Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-17T18:17:37.589Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-17T18:18:40.058Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "bb9636bcbdcbd48220ad6beb4934afc7b07c369e0abe1906d9dacc45a67ec675",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -22,6 +22,20 @@ node scripts/governance/local-worktree-cleanup.mjs sweep --repo D:/Git/a1
|
|
|
22
22
|
|
|
23
23
|
`sweep` evaluates every released registration in one pass under the queue limits (100 registrations, 500 remote requests, a durable round-robin cursor) with a 180-second elapsed budget, since each merged candidate costs three evidence loads, and completes each candidate whose PR is verified merged using exactly the `complete` safeguards. It needs no `enable` and starts no process: its authority is each candidate's release. A candidate whose PR is open, draft, or not yet finalized reports `pending`; a candidate whose PR closed without merge reports `awaiting-discard` and is never touched; blockers report their exact reason. An old stop sentinel does not prevent a sweep, but `disable` run while a sweep is executing stops it before its next destructive step. If another session holds the mutation lock the sweep reports `mutation-busy` and the agent relays it as deferred rather than waiting. The JSON report carries a `lines` array with one relayable line per candidate and pruned branch, for example `#458 close-absent-and-skewed-cleanup: removed [worktree-removed, local-ref-removed]`. Pending or blocked results never delay the new delivery.
|
|
24
24
|
|
|
25
|
+
### Nothing left to remove
|
|
26
|
+
|
|
27
|
+
A released entry whose evidence can never verify (for example `delivery-content-drift` after a stale-base merge, or `source-association` for a PR without an `openspec-implementation` fence) would otherwise be re-verified and reported `blocked` on every sweep. When such an entry's worktree path is gone, Git holds no live row for it (its own dangling row is retired), and its local topic ref is absent, the sweep and `complete` ask GitHub only whether the pull request merged into `develop` in this repository and then mark the journal `done` with `retired-nothing-left` and the original evidence reason. Nothing is deleted; a present path, row, or ref keeps the entry blocked, an unmerged PR keeps it `pending` or `awaiting-discard`, a deferred failure such as `remote-budget` is never treated as unverifiable, and preview retires nothing. For an all-absent entry whose PR is not merged, the maintainer runs the explicit form, which reads nothing from GitHub and deletes nothing:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node scripts/governance/local-worktree-cleanup.mjs forget --repo D:/Git/a1 --id REGISTRATION_ID --confirm-nothing-left
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`forget` refuses without the flag, for an owned or deleting entry, and while the path, a live Git row, or the ref still exists (`something-remains`). `status` shows both notes under `completion`.
|
|
34
|
+
|
|
35
|
+
### Keep the base current before merge
|
|
36
|
+
|
|
37
|
+
The `develop` ruleset requires the pull request head to be up to date with `develop`. When another delivery merges first, the PR shows `BEHIND`: merge `origin/develop` into the branch (or use "Update branch"), push, let the `OpenSpec finalization` workflow re-finalize with digests of the merged bytes, wait for validation, and hand off again. Merging a stale finalized head is what produced `delivery-content-drift` for #461; with the strict policy GitHub refuses that merge.
|
|
38
|
+
|
|
25
39
|
### Accepted ancestry
|
|
26
40
|
|
|
27
41
|
Since the `OpenSpec finalization` workflow pushes its commit onto the PR branch after the agent's last push, the registered head is normally one commit behind the merged head. Cleanup therefore accepts a registered head, live worktree HEAD, or local topic-ref tip that equals the merged PR head or is one of its ancestors on GitHub, verified with the compare API: every commit reachable from such a tip is reachable from a head the maintainer accepted, so nothing is lost. A tip that holds a commit outside the merged head, or a commit GitHub does not know, still blocks with `candidate-head-association`, `worktree-identity-changed`, or `local-ref-advanced`, and the branch attachment must still be the registered one. Ref deletion reads the tip immediately before deleting, requires it to be accepted, and uses it as the compare-and-delete expectation. Ancestry of `develop` is never used, because the repository squash-merges.
|
|
@@ -162,6 +176,8 @@ State, journals, stop controls, and execution reports live in `<git-common-dir>/
|
|
|
162
176
|
- `unmanaged`: no local registration; no automatic adoption.
|
|
163
177
|
- `removed`: worktree and eligible local-ref operations were verified.
|
|
164
178
|
- `already-absent`: a completed journal's path/ref are still absent.
|
|
179
|
+
- `retired`: evidence was unverifiable but nothing remained to delete and the PR is merged; the journal is complete with `retired-nothing-left`.
|
|
180
|
+
- `forgotten`: the maintainer explicitly closed an all-absent entry with `forget`.
|
|
165
181
|
- `partial`: a destructive step began but all cleanup could not be verified; the same command resumes it.
|
|
166
182
|
- `deferred`: a bounded pass or concurrent mutation owner prevented evaluation.
|
|
167
183
|
|
package/package.json
CHANGED