@tt-a1i/openpi 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +82 -6
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +138 -130
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { OutputView } from "./domain.ts";
|
|
16
|
+
import { TerminalTextSanitizer } from "../../shared/terminal-text.ts";
|
|
16
17
|
|
|
17
18
|
export class OutputBuffer {
|
|
18
19
|
private chunks: string[] = [];
|
|
20
|
+
private modelSafeChunks: string[] = [];
|
|
19
21
|
/** Bytes currently retained across `chunks`. */
|
|
20
22
|
private retainedBytes = 0;
|
|
23
|
+
private modelSafeRetainedBytes = 0;
|
|
21
24
|
/** Cached join of `chunks`; invalidated on push so 1Hz UI ticks are cheap. */
|
|
22
25
|
private cachedText: string | undefined = "";
|
|
26
|
+
private cachedModelSafeText: string | undefined = "";
|
|
23
27
|
/** Bumped on every push; lets the UI cache derived line layouts. */
|
|
24
28
|
version = 0;
|
|
25
29
|
totalBytes = 0;
|
|
@@ -28,6 +32,7 @@ export class OutputBuffer {
|
|
|
28
32
|
|
|
29
33
|
private readonly maxRetainedBytes: number;
|
|
30
34
|
private readonly spill?: (chunk: string) => unknown;
|
|
35
|
+
private readonly modelSanitizer = new TerminalTextSanitizer();
|
|
31
36
|
|
|
32
37
|
constructor(maxRetainedBytes: number, spill?: (chunk: string) => unknown) {
|
|
33
38
|
this.maxRetainedBytes = maxRetainedBytes;
|
|
@@ -36,6 +41,7 @@ export class OutputBuffer {
|
|
|
36
41
|
|
|
37
42
|
push(chunk: string) {
|
|
38
43
|
if (chunk.length === 0) return true;
|
|
44
|
+
this.retainModelSafeText(this.modelSanitizer.push(chunk));
|
|
39
45
|
let bytes = Buffer.byteLength(chunk, "utf8");
|
|
40
46
|
this.totalBytes += bytes;
|
|
41
47
|
const spillAccepted = this.spill?.(chunk) !== false;
|
|
@@ -74,11 +80,38 @@ export class OutputBuffer {
|
|
|
74
80
|
|
|
75
81
|
view(): OutputView {
|
|
76
82
|
this.cachedText ??= this.chunks.join("");
|
|
83
|
+
this.cachedModelSafeText ??= this.modelSafeChunks.join("");
|
|
77
84
|
return {
|
|
78
85
|
text: this.cachedText,
|
|
86
|
+
modelSafeText: this.cachedModelSafeText,
|
|
79
87
|
totalBytes: this.totalBytes,
|
|
80
88
|
truncatedBytes: this.truncatedBytes,
|
|
81
89
|
spillPath: this.spillPath,
|
|
82
90
|
};
|
|
83
91
|
}
|
|
92
|
+
|
|
93
|
+
private retainModelSafeText(text: string) {
|
|
94
|
+
if (text.length === 0) return;
|
|
95
|
+
let bytes = Buffer.byteLength(text, "utf8");
|
|
96
|
+
if (bytes > this.maxRetainedBytes) {
|
|
97
|
+
this.modelSafeChunks = [];
|
|
98
|
+
this.modelSafeRetainedBytes = 0;
|
|
99
|
+
const raw = Buffer.from(text, "utf8");
|
|
100
|
+
let start = raw.length - this.maxRetainedBytes;
|
|
101
|
+
while (start < raw.length && (raw[start] & 0xc0) === 0x80) start++;
|
|
102
|
+
text = raw.subarray(start).toString("utf8");
|
|
103
|
+
bytes = raw.length - start;
|
|
104
|
+
}
|
|
105
|
+
this.modelSafeChunks.push(text);
|
|
106
|
+
this.modelSafeRetainedBytes += bytes;
|
|
107
|
+
while (
|
|
108
|
+
this.modelSafeRetainedBytes > this.maxRetainedBytes &&
|
|
109
|
+
this.modelSafeChunks.length > 1
|
|
110
|
+
) {
|
|
111
|
+
const evicted = this.modelSafeChunks.shift();
|
|
112
|
+
if (evicted === undefined) break;
|
|
113
|
+
this.modelSafeRetainedBytes -= Buffer.byteLength(evicted, "utf8");
|
|
114
|
+
}
|
|
115
|
+
this.cachedModelSafeText = undefined;
|
|
116
|
+
}
|
|
84
117
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
formatSize,
|
|
7
7
|
truncateTail,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { sanitizeTerminalText } from "../../shared/terminal-text.ts";
|
|
9
10
|
import {
|
|
10
11
|
formatDuration,
|
|
11
12
|
formatElapsed,
|
|
@@ -139,7 +140,7 @@ function outputSection(
|
|
|
139
140
|
maxLines: number,
|
|
140
141
|
) {
|
|
141
142
|
if (view.totalBytes === 0) return `${label}: (empty)`;
|
|
142
|
-
const truncation = truncateTail(view.
|
|
143
|
+
const truncation = truncateTail(view.modelSafeText, {
|
|
143
144
|
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
144
145
|
maxLines: Math.min(maxLines, DEFAULT_MAX_LINES),
|
|
145
146
|
});
|
|
@@ -186,12 +187,15 @@ export function buildTerminalBatchResultMessage(
|
|
|
186
187
|
messages: readonly string[],
|
|
187
188
|
omitted = 0,
|
|
188
189
|
) {
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const sanitizedMessages = messages.map(sanitizeTerminalText);
|
|
191
|
+
if (sanitizedMessages.length === 1 && omitted === 0) {
|
|
192
|
+
return sanitizedMessages[0]!;
|
|
193
|
+
}
|
|
194
|
+
const summaries = sanitizedMessages.map(
|
|
191
195
|
(message) => message.split("\n", 1)[0] || "Background terminal result",
|
|
192
196
|
);
|
|
193
197
|
const header = [
|
|
194
|
-
`${
|
|
198
|
+
`${sanitizedMessages.length} background terminal result${sanitizedMessages.length === 1 ? "" : "s"}:`,
|
|
195
199
|
...summaries.map((summary) => `- ${summary}`),
|
|
196
200
|
omitted > 0
|
|
197
201
|
? `- ${omitted} older result${omitted === 1 ? "" : "s"} omitted from this bounded batch; use bg_list/bg_status for retained details.`
|
|
@@ -206,7 +210,7 @@ export function buildTerminalBatchResultMessage(
|
|
|
206
210
|
`${header}${logsHeader}${truncationMarker}`,
|
|
207
211
|
"utf8",
|
|
208
212
|
);
|
|
209
|
-
const logs = truncateTail(
|
|
213
|
+
const logs = truncateTail(sanitizedMessages.join("\n\n"), {
|
|
210
214
|
maxBytes: Math.max(1, RESULT_BATCH_MAX - fixedBytes),
|
|
211
215
|
maxLines: DEFAULT_MAX_LINES,
|
|
212
216
|
});
|
|
@@ -220,6 +224,10 @@ export function buildKillReport(results: ReadonlyArray<KillResult>) {
|
|
|
220
224
|
if (entry.killed) {
|
|
221
225
|
return `Killed ${entry.id} "${entry.title}" (${entry.exit}).`;
|
|
222
226
|
}
|
|
227
|
+
if (entry.terminationFailed) {
|
|
228
|
+
const detail = entry.errorText ? ` ${entry.errorText}.` : "";
|
|
229
|
+
return `Could not confirm process-tree termination for ${entry.id} "${entry.title}" (${entry.exit}).${detail}`;
|
|
230
|
+
}
|
|
223
231
|
if (entry.wasRunning) {
|
|
224
232
|
// The natural exit won the race with the kill signal.
|
|
225
233
|
return `${entry.id} "${entry.title}" exited on its own before the kill landed (${entry.exit}).`;
|
|
@@ -1,43 +1,66 @@
|
|
|
1
|
+
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
import {
|
|
3
|
+
type CompletionOwner,
|
|
4
|
+
createCompletionInbox,
|
|
5
|
+
} from "../../shared/completion-inbox.ts";
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
|
-
* Deferred one-shot delivery
|
|
3
|
-
* terminal's result is held
|
|
4
|
-
* message or consumed by a tool call (bg_kill /
|
|
5
|
-
* returned the settlement itself.
|
|
6
|
-
* structurally impossible — whoever
|
|
8
|
+
* Deferred one-shot delivery adapter (same semantics as subagents'): a
|
|
9
|
+
* settled terminal's result is held in the shared inbox until it is either
|
|
10
|
+
* drained into a follow-up message or consumed by a tool call (bg_kill /
|
|
11
|
+
* bg_status) that already returned the settlement itself. Stable ids make
|
|
12
|
+
* double delivery structurally impossible — whoever claims first wins.
|
|
7
13
|
*/
|
|
8
|
-
export function createDeferredResultDelivery<T extends { id: string }>(
|
|
9
|
-
|
|
14
|
+
export function createDeferredResultDelivery<T extends { id: string }>(
|
|
15
|
+
options: { readonly owner?: () => CompletionOwner | undefined } = {},
|
|
16
|
+
) {
|
|
17
|
+
const inbox = createCompletionInbox<T>();
|
|
18
|
+
const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
const queue = {
|
|
12
21
|
defer(result: T) {
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
const currentOwner = owner();
|
|
23
|
+
inbox.defer(
|
|
24
|
+
{
|
|
25
|
+
deliveryId: `background:${result.id}`,
|
|
26
|
+
owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
|
|
27
|
+
producer: "background",
|
|
28
|
+
producerId: result.id,
|
|
29
|
+
terminalRef: { kind: "terminal-snapshot", id: result.id },
|
|
30
|
+
wake: "producer-policy",
|
|
31
|
+
payload: result,
|
|
32
|
+
},
|
|
33
|
+
currentOwner,
|
|
34
|
+
);
|
|
35
|
+
return inbox.size();
|
|
15
36
|
},
|
|
16
37
|
consume(ids: Iterable<string>) {
|
|
17
|
-
|
|
38
|
+
inbox.consume("background", ids);
|
|
18
39
|
},
|
|
19
40
|
drain(maxResults = Number.POSITIVE_INFINITY) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
results.push(result);
|
|
24
|
-
pending.delete(id);
|
|
25
|
-
}
|
|
26
|
-
return results;
|
|
41
|
+
return inbox
|
|
42
|
+
.claim(owner(), maxResults)
|
|
43
|
+
.map((envelope) => envelope.payload);
|
|
27
44
|
},
|
|
28
45
|
restore(results: readonly T[]) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
inbox.retryClaimed(
|
|
47
|
+
"background",
|
|
48
|
+
results.map((result) => result.id),
|
|
49
|
+
owner(),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
acknowledge(results: readonly T[]) {
|
|
53
|
+
inbox.acknowledge(results.map((result) => `background:${result.id}`));
|
|
33
54
|
},
|
|
34
55
|
size() {
|
|
35
|
-
return
|
|
56
|
+
return inbox.size();
|
|
36
57
|
},
|
|
37
58
|
clear() {
|
|
38
|
-
|
|
59
|
+
inbox.clear();
|
|
39
60
|
},
|
|
61
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
40
62
|
};
|
|
63
|
+
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
/**
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
type KeybindingsManager,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Key, matchesKey, type EditorComponent } from "@earendil-works/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
BelowEditorNavigationEditor,
|
|
9
|
+
BelowEditorStripState,
|
|
10
|
+
} from "../shared/below-editor-navigation.ts";
|
|
11
|
+
import {
|
|
12
|
+
registerEditorLayer,
|
|
13
|
+
removeEditorLayer,
|
|
14
|
+
} from "../shared/editor-layers.ts";
|
|
15
|
+
|
|
16
|
+
export function shouldClearContext(
|
|
17
|
+
data: string,
|
|
18
|
+
editorText: string,
|
|
19
|
+
isIdle: boolean,
|
|
20
|
+
) {
|
|
21
|
+
return isIdle && editorText.length === 0 && matchesKey(data, Key.ctrl("c"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ClearContextEditor extends BelowEditorNavigationEditor {
|
|
25
|
+
private readonly isIdle: () => boolean;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
base: EditorComponent,
|
|
29
|
+
keybindings: KeybindingsManager,
|
|
30
|
+
isIdle: () => boolean,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
base,
|
|
34
|
+
keybindings,
|
|
35
|
+
new BelowEditorStripState(),
|
|
36
|
+
() => false,
|
|
37
|
+
() => undefined,
|
|
38
|
+
() => undefined,
|
|
39
|
+
);
|
|
40
|
+
this.isIdle = isIdle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override handleInput(data: string) {
|
|
44
|
+
if (shouldClearContext(data, this.getText(), this.isIdle())) {
|
|
45
|
+
// Route through Pi's built-in /new command so session replacement keeps
|
|
46
|
+
// Pi's persistence, cleanup, and transcript lifecycle as the source of truth.
|
|
47
|
+
this.setText("/new");
|
|
48
|
+
this.onSubmit?.("/new");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
super.handleInput(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function installClearContextShortcut(pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
57
|
+
if (ctx.mode !== "tui") return () => {};
|
|
58
|
+
|
|
59
|
+
registerEditorLayer(pi, ctx, {
|
|
60
|
+
id: "clear-context",
|
|
61
|
+
order: 100,
|
|
62
|
+
wrap: (base, _tui, _theme, keybindings) =>
|
|
63
|
+
new ClearContextEditor(base, keybindings, () => ctx.isIdle()),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return () => removeEditorLayer(pi, "clear-context");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function clearContext(pi: ExtensionAPI) {
|
|
70
|
+
let removeShortcut = () => {};
|
|
71
|
+
|
|
72
|
+
pi.on("session_start", (_event, ctx) => {
|
|
73
|
+
removeShortcut();
|
|
74
|
+
removeShortcut = installClearContextShortcut(pi, ctx);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.on("session_shutdown", () => {
|
|
78
|
+
removeShortcut();
|
|
79
|
+
removeShortcut = () => {};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { installClearContextShortcut };
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
|
|
12
12
|
export const MIN_CONTEXT_PIVOT_TOKENS = 30_000;
|
|
13
13
|
const STATUS_KEY = "context-pivot";
|
|
14
|
+
const NOTHING_TO_COMPACT_ERROR = "Nothing to compact (session too small)";
|
|
15
|
+
const NO_DISCARDABLE_HISTORY_MESSAGE =
|
|
16
|
+
"Context pivot could not run: this session has no discardable history to compact. Continue in the current session, or use /sessions to choose another session; to begin cleanly, start a new Session in Pi.";
|
|
14
17
|
|
|
15
18
|
interface PendingPivot {
|
|
16
19
|
brief: string;
|
|
@@ -57,6 +60,13 @@ export function buildPivotSummary(brief: string): string {
|
|
|
57
60
|
].join("\n");
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function formatContextPivotError(error: unknown): string {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
return message === NOTHING_TO_COMPACT_ERROR
|
|
66
|
+
? NO_DISCARDABLE_HISTORY_MESSAGE
|
|
67
|
+
: `Context pivot failed: ${message}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function impossibleKeptId(entries: readonly SessionEntry[]) {
|
|
61
71
|
return `${entries.at(-1)?.id ?? "context-pivot"}-context-pivot-cut`;
|
|
62
72
|
}
|
|
@@ -73,7 +83,7 @@ function validateBrief(brief: string, ctx: ExtensionContext) {
|
|
|
73
83
|
}
|
|
74
84
|
if (tokens < MIN_CONTEXT_PIVOT_TOKENS) {
|
|
75
85
|
throw new Error(
|
|
76
|
-
`Context is only ${Math.round(tokens).toLocaleString()} tokens; use context_pivot once context reaches at least ${MIN_CONTEXT_PIVOT_TOKENS.toLocaleString()} tokens, or /
|
|
86
|
+
`Context is only ${Math.round(tokens).toLocaleString()} tokens; use context_pivot once context reaches at least ${MIN_CONTEXT_PIVOT_TOKENS.toLocaleString()} tokens, or use /sessions to browse or switch an existing session; start a new Session in Pi when a clean session is needed.`,
|
|
77
87
|
);
|
|
78
88
|
}
|
|
79
89
|
}
|
|
@@ -127,7 +137,7 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
127
137
|
name: "context_pivot",
|
|
128
138
|
label: "Context Pivot",
|
|
129
139
|
description:
|
|
130
|
-
"Deliberately replace a long, noisy active context with a concise brief for the next phase while staying in the same Pi session. Use once context is at least 30k tokens and the work is moving between phases such as research → implementation or implementation → review; below 30k it is rejected. Use /
|
|
140
|
+
"Deliberately replace a long, noisy active context with a concise brief for the next phase while staying in the same Pi session. Use once context is at least 30k tokens and the work is moving between phases such as research → implementation or implementation → review; below 30k it is rejected. Use /sessions to browse or switch an existing session; start a new Session in Pi when a genuinely new session is needed.",
|
|
131
141
|
promptSnippet:
|
|
132
142
|
"Compress a long current session into a clean brief before changing phase",
|
|
133
143
|
promptGuidelines: [
|
|
@@ -176,11 +186,11 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
176
186
|
onError: (error) => {
|
|
177
187
|
if (pivotGeneration === generation) pending = undefined;
|
|
178
188
|
clear();
|
|
189
|
+
const message = formatContextPivotError(error);
|
|
179
190
|
if (ctx.hasUI) {
|
|
180
|
-
ctx.ui.notify(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
191
|
+
ctx.ui.notify(message, "error");
|
|
192
|
+
} else {
|
|
193
|
+
console.error(message);
|
|
184
194
|
}
|
|
185
195
|
},
|
|
186
196
|
});
|
package/extensions/cron/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ import type {
|
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import {
|
|
19
19
|
advanceDeliveredJobs,
|
|
20
|
+
CRON_DELIVERY_MAX_BYTES,
|
|
21
|
+
CRON_DELIVERY_MAX_JOBS,
|
|
22
|
+
CRON_MAX_JOBS,
|
|
20
23
|
type CronJob,
|
|
21
24
|
dueJobs,
|
|
22
25
|
formatInterval,
|
|
@@ -41,6 +44,48 @@ const SYSTEM_RUNTIME: CronRuntime = {
|
|
|
41
44
|
},
|
|
42
45
|
};
|
|
43
46
|
|
|
47
|
+
function deliveryMessage(due: readonly CronJob[]) {
|
|
48
|
+
const jobs = due.map((job) => ({
|
|
49
|
+
id: job.id,
|
|
50
|
+
prompt: job.prompt,
|
|
51
|
+
recurring: job.intervalMs !== undefined,
|
|
52
|
+
}));
|
|
53
|
+
return due.length === 1
|
|
54
|
+
? {
|
|
55
|
+
customType: "cron-fire",
|
|
56
|
+
content: `[cron ${jobs[0]!.id} · ${jobs[0]!.recurring ? "recurring" : "once"}]\n${jobs[0]!.prompt}`,
|
|
57
|
+
display: true,
|
|
58
|
+
details: jobs[0]!,
|
|
59
|
+
}
|
|
60
|
+
: {
|
|
61
|
+
customType: "cron-fire",
|
|
62
|
+
content: `${due.length} scheduled prompts are due:\n\n${jobs
|
|
63
|
+
.map(
|
|
64
|
+
(job) =>
|
|
65
|
+
`[cron ${job.id} · ${job.recurring ? "recurring" : "once"}]\n${job.prompt}`,
|
|
66
|
+
)
|
|
67
|
+
.join("\n\n")}`,
|
|
68
|
+
display: true,
|
|
69
|
+
details: { count: jobs.length, jobs },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dueDeliveryBatch(due: readonly CronJob[]) {
|
|
74
|
+
const selected: CronJob[] = [];
|
|
75
|
+
for (const job of due.slice(0, CRON_DELIVERY_MAX_JOBS)) {
|
|
76
|
+
const candidate = [...selected, job];
|
|
77
|
+
const message = deliveryMessage(candidate);
|
|
78
|
+
if (
|
|
79
|
+
new TextEncoder().encode(message.content).byteLength >
|
|
80
|
+
CRON_DELIVERY_MAX_BYTES
|
|
81
|
+
) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
selected.push(job);
|
|
85
|
+
}
|
|
86
|
+
return selected;
|
|
87
|
+
}
|
|
88
|
+
|
|
44
89
|
export default function cron(
|
|
45
90
|
pi: ExtensionAPI,
|
|
46
91
|
runtime: CronRuntime = SYSTEM_RUNTIME,
|
|
@@ -58,30 +103,7 @@ export default function cron(
|
|
|
58
103
|
const fire = (due: readonly CronJob[]) => {
|
|
59
104
|
if (due.length === 0) return true;
|
|
60
105
|
try {
|
|
61
|
-
const
|
|
62
|
-
id: job.id,
|
|
63
|
-
prompt: job.prompt,
|
|
64
|
-
recurring: job.intervalMs !== undefined,
|
|
65
|
-
}));
|
|
66
|
-
const message =
|
|
67
|
-
due.length === 1
|
|
68
|
-
? {
|
|
69
|
-
customType: "cron-fire",
|
|
70
|
-
content: `[cron ${jobs[0]!.id} · ${jobs[0]!.recurring ? "recurring" : "once"}]\n${jobs[0]!.prompt}`,
|
|
71
|
-
display: true,
|
|
72
|
-
details: jobs[0],
|
|
73
|
-
}
|
|
74
|
-
: {
|
|
75
|
-
customType: "cron-fire",
|
|
76
|
-
content: `${due.length} scheduled prompts are due:\n\n${jobs
|
|
77
|
-
.map(
|
|
78
|
-
(job) =>
|
|
79
|
-
`[cron ${job.id} · ${job.recurring ? "recurring" : "once"}]\n${job.prompt}`,
|
|
80
|
-
)
|
|
81
|
-
.join("\n\n")}`,
|
|
82
|
-
display: true,
|
|
83
|
-
details: { count: jobs.length, jobs },
|
|
84
|
-
};
|
|
106
|
+
const message = deliveryMessage(due);
|
|
85
107
|
pi.sendMessage<
|
|
86
108
|
| { id: number; prompt: string; recurring: boolean }
|
|
87
109
|
| {
|
|
@@ -109,9 +131,11 @@ export default function cron(
|
|
|
109
131
|
const now = runtime.now();
|
|
110
132
|
const due = dueJobs(jobs, now);
|
|
111
133
|
if (due.length === 0) return;
|
|
134
|
+
const batch = dueDeliveryBatch(due);
|
|
135
|
+
if (batch.length === 0) return;
|
|
112
136
|
const deliveredIds = new Set<number>();
|
|
113
|
-
if (fire(
|
|
114
|
-
for (const job of
|
|
137
|
+
if (fire(batch)) {
|
|
138
|
+
for (const job of batch) deliveredIds.add(job.id);
|
|
115
139
|
}
|
|
116
140
|
jobs = advanceDeliveredJobs(jobs, deliveredIds, runtime.now());
|
|
117
141
|
if (jobs.length === 0) stopTicker();
|
|
@@ -170,12 +194,29 @@ export default function cron(
|
|
|
170
194
|
return;
|
|
171
195
|
}
|
|
172
196
|
|
|
197
|
+
if (jobs.length >= CRON_MAX_JOBS) {
|
|
198
|
+
ctx.ui.notify(
|
|
199
|
+
`A session can have at most ${CRON_MAX_JOBS} scheduled prompts. Remove one before adding another.`,
|
|
200
|
+
"warning",
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
const intervalMs = parsed.intervalMs!;
|
|
206
|
+
const now = runtime.now();
|
|
207
|
+
const nextRunAt = now + intervalMs;
|
|
208
|
+
if (!Number.isSafeInteger(now) || !Number.isSafeInteger(nextRunAt)) {
|
|
209
|
+
ctx.ui.notify(
|
|
210
|
+
"Scheduled time is too far in the future. Use a shorter duration.",
|
|
211
|
+
"warning",
|
|
212
|
+
);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
174
215
|
const job: CronJob = {
|
|
175
216
|
id: nextId++,
|
|
176
217
|
prompt: parsed.prompt!,
|
|
177
218
|
...(parsed.oneShot ? {} : { intervalMs }),
|
|
178
|
-
nextRunAt
|
|
219
|
+
nextRunAt,
|
|
179
220
|
};
|
|
180
221
|
jobs.push(job);
|
|
181
222
|
startTicker();
|
|
@@ -22,6 +22,9 @@ const UNITS: Record<string, number> = {
|
|
|
22
22
|
|
|
23
23
|
export const MIN_INTERVAL_MS = 30_000;
|
|
24
24
|
export const CRON_PROMPT_MAX_CHARS = 2_000;
|
|
25
|
+
export const CRON_MAX_JOBS = 64;
|
|
26
|
+
export const CRON_DELIVERY_MAX_JOBS = 16;
|
|
27
|
+
export const CRON_DELIVERY_MAX_BYTES = 48 * 1024;
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Parse a duration like `30s`, `5m`, `2h`. Deliberately a small duration
|
|
@@ -33,7 +36,8 @@ export function parseDuration(text: string): number | undefined {
|
|
|
33
36
|
if (!match) return undefined;
|
|
34
37
|
const value = Number(match[1]);
|
|
35
38
|
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
36
|
-
|
|
39
|
+
const durationMs = value * UNITS[match[2].toLowerCase()];
|
|
40
|
+
return Number.isSafeInteger(durationMs) ? durationMs : undefined;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface ParsedCronCommand {
|
|
@@ -81,8 +85,14 @@ export function parseCronCommand(raw: string): ParsedCronCommand {
|
|
|
81
85
|
error: `Minimum interval is ${MIN_INTERVAL_MS / 1000}s (the scheduler polls about that often).`,
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
|
-
const prompt = schedule[3].trim()
|
|
88
|
+
const prompt = schedule[3].trim();
|
|
85
89
|
if (!prompt) return { action: "help", error: "Provide a prompt to run." };
|
|
90
|
+
if (prompt.length > CRON_PROMPT_MAX_CHARS) {
|
|
91
|
+
return {
|
|
92
|
+
action: "help",
|
|
93
|
+
error: `Prompt is too long. Maximum is ${CRON_PROMPT_MAX_CHARS} characters.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
86
96
|
return {
|
|
87
97
|
action: "add",
|
|
88
98
|
intervalMs,
|