specpi 0.17.1 → 0.18.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/CHANGELOG.md +8 -0
- package/README.md +2 -2
- package/SECURITY_MODEL.md +5 -1
- package/docs/delegation/README.md +1 -1
- package/docs/delegation/protocol.md +23 -0
- package/extensions/command-guard/index.ts +3 -3
- package/extensions/delegation/core.mjs +26 -3
- package/extensions/delegation/extension.mjs +54 -3
- package/extensions/delegation/presentation.mjs +82 -0
- package/package.json +1 -1
- package/scripts/specpi.mjs +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0 - 2026-09-07
|
|
4
|
+
|
|
5
|
+
- Start Command Guard off in RPC sessions, including new SpecPi Chat conversations. Explicit `/guard guard` and `/guard strict` still enable protection for the current session; the terminal startup chooser is unchanged.
|
|
6
|
+
|
|
7
|
+
- Let installer PATH discovery continue past inaccessible candidates while preserving permission errors for explicit executable paths. Use the pinned repository Pi for test fixtures and a temporary npm cache for package validation, so restricted test accounts do not require personal npm access.
|
|
8
|
+
|
|
9
|
+
- Publish bounded delegate lifecycle metadata through Pi's RPC widget protocol for SpecPi Chat's live panel. Add exact-attempt human cancellation, stop sampling on settlement/shutdown, and keep prior-generation task labels out of replacement sessions. Worker policy, model-facing operations, quotas and provider behavior are unchanged.
|
|
10
|
+
|
|
3
11
|
## 0.17.1 - 2026-09-07
|
|
4
12
|
|
|
5
13
|
- Reduce the npm package by shipping only runtime scripts; remove obsolete plans, research documents and unused assets, and shorten operational guides.
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ specpi doctor
|
|
|
48
48
|
|
|
49
49
|
Preview the changes, confirm the install, then check the setup. Restart Pi when finished.
|
|
50
50
|
|
|
51
|
-
To pin this release, use `npm install --global specpi@0.
|
|
51
|
+
To pin this release, use `npm install --global specpi@0.18.0`.
|
|
52
52
|
|
|
53
53
|
Delegation is enabled at startup. Use `/delegate off` to turn it off.
|
|
54
54
|
|
|
@@ -62,7 +62,7 @@ Chat beside your code, attach files, and switch conversations.
|
|
|
62
62
|
|
|
63
63
|
In VS Code, find **SpecPi Chat** by **tannermidd** in Extensions and install it. Open **SpecPi** in the Activity Bar, then **Connect Pi**.
|
|
64
64
|
|
|
65
|
-
Chat 0.
|
|
65
|
+
Chat 0.4.0 adds live delegate progress and worker Stop controls, plus compact tags for attached files. With SpecPi 0.18.0, new Chat sessions start with Guard off; use `/guard guard` or `/guard strict` to enable it. Update the harness and use **Restart Pi** to load these changes.
|
|
66
66
|
|
|
67
67
|
## Go further
|
|
68
68
|
|
package/SECURITY_MODEL.md
CHANGED
|
@@ -95,6 +95,8 @@ SpecPi is not an operating-system sandbox and does not claim to contain a compro
|
|
|
95
95
|
|
|
96
96
|
## Installer and managed state
|
|
97
97
|
|
|
98
|
+
Installer executable discovery skips inaccessible candidates in an implicit PATH search; it neither changes their permissions nor executes them. Explicit executable paths and unexpected filesystem errors still fail. The existing Windows winget App Execution Alias handling is unchanged.
|
|
99
|
+
|
|
98
100
|
SpecPi is configuration and executable extension code for Pi and runs with the invoking user's permissions. Installing the public npm package adds the `specpi` CLI to npm-managed state but does not run SpecPi installation, download external tools, or mutate Pi state; the package defines no npm install lifecycle script. The installer prints a plan and requires confirmation unless `--yes` is supplied. `plan` is non-mutating. Installation and update merge documented settings leaves, preserve unrelated packages and configuration, and modify AGENTS and shell files only inside SpecPi marker blocks.
|
|
99
101
|
|
|
100
102
|
The canonical npm route is an installer-CLI distribution. Direct `pi install npm:specpi` loads packaged extensions, skills, and themes but bypasses the managed installer, so it does not provide managed instructions, supporting packages, browser runtime acquisition, optional tools, shell integration, backups, or ownership records. Pi-bundled runtime modules are optional peers and are not copied into the tarball. Release automation validates the packed artifact, requires a matching immutable version and release tag, publishes only from a protected environment, and requests npm provenance through short-lived GitHub OIDC credentials.
|
|
@@ -189,7 +191,7 @@ Repository checks, smoke tests, checksums, closed capability validators, and bro
|
|
|
189
191
|
|
|
190
192
|
The optional `vscode/` extension is distributed separately from the SpecPi npm installation. It runs in the VS Code workspace extension host and requires a trusted filesystem workspace before launching Pi or collecting file context. Only application-level settings choose executable paths; the launcher resolves native executables or known adjacent Pi JavaScript entry points and never executes a shell command string. Pi runs in the explicitly selected workspace with its normal installed extensions and provider configuration. VS Code trust does not replace Pi project trust, and the extension does not pass a flag to bypass Pi's trust decisions or Command Guard.
|
|
191
193
|
|
|
192
|
-
The sidebar uses Pi's documented JSONL RPC protocol. Pi owns provider authentication and tool execution; the extension does not read, copy, or manage authentication files. RPC model metadata is projected to display fields. Approvals and selection/input/editor requests are displayed as text and require an exact response for the requesting conversation's active request. Switching the displayed conversation preserves outstanding requests; background requests appear as needing input and never receive automatic approval. Stop, disconnect, runtime session transitions, and expiry cancel the affected conversation's requests. Expired or wrong-conversation responses cannot authorize later requests. In RPC, Command Guard starts
|
|
194
|
+
The sidebar uses Pi's documented JSONL RPC protocol. Pi owns provider authentication and tool execution; the extension does not read, copy, or manage authentication files. RPC model metadata is projected to display fields. Approvals and selection/input/editor requests are displayed as text and require an exact response for the requesting conversation's active request. Switching the displayed conversation preserves outstanding requests; background requests appear as needing input and never receive automatic approval. Stop, disconnect, runtime session transitions, and expiry cancel the affected conversation's requests. Expired or wrong-conversation responses cannot authorize later requests. In RPC, including SpecPi Chat, Command Guard starts off without an initial selection dialog because Pi cannot read dialog responses until its startup handlers return. Explicit `/guard guard` and `/guard strict` commands enable protection for the current session; new sessions start off again. The terminal startup chooser and initialization-failure handling are unchanged. Unsupported terminal-only components are disclosed rather than treated as functional editor controls.
|
|
193
195
|
|
|
194
196
|
Chat allows up to 90 seconds for an initial read-only RPC readiness probe before sending normal requests. This accommodates the 30-second startup fallback in older installed Guard versions without extending later request deadlines. Dialogs arriving before readiness receive cancellation only, never an approval or mode selection; the sidebar explains the limitation and retains a notice after connection. An extension with an unbounded startup dialog can still prevent readiness. Chat times out with recovery guidance instead of bypassing that extension or changing the user's installed harness.
|
|
195
197
|
|
|
@@ -207,6 +209,8 @@ Pi persists chats, including sent image blocks, in a workspace-specific director
|
|
|
207
209
|
|
|
208
210
|
Conversation edits select entries from the active Pi tree, revalidate the selection before mutation, and fork before the chosen user prompt. Exact original text and validated image blocks are captured before forking because Pi's fork response returns text only. The original conversation is retained, restored drafts require another send action, and conversation forks/clones do not restore or checkpoint workspace files. Actions are scoped to the active workspace, connection, and session revision. Export opens an unsaved Markdown document and Copy Conversation writes the visible Markdown to the OS clipboard only on explicit request. Both omit image bytes and represent images as placeholders; they do not reconstruct truncated or hidden history. Usage displays report Pi measurements, preserving unavailable values rather than inventing totals. The separate provider Limits row projects bounded strings from the known Codex Usage and pi-usage status keys, strips terminal/control sequences, and inserts text only. It does not parse numeric quotas, combine budgets with conversation cost, read provider credentials or usage-cache files, or add provider requests. Reports stay with their live connection and are cleared on disconnect, exit, and reconnect; refreshing and cache age remain upstream plugin responsibilities. Opening the details has no RPC or provider side effect. Existing `/usage` notifications and exact confirmation dialogs continue through the normal UI protocol; no automatic reset consumption or terminal-footer emulation is added.
|
|
209
211
|
|
|
212
|
+
The Delegates panel consumes the known `specpi-delegation-v1` RPC widget key, with one versioned JSON record of at most 32 KiB and eight projected worker rows. Public identities, shortened task labels, states, elapsed times, call counts and code-owned diagnostics are text-only; snapshots, full prompts, raw provider errors and child transcripts are not sent through the live widget. A single extension-side timer samples existing counters while slots are occupied; it makes no provider or source requests and is disposed on shutdown/rebinding. Invalidated-generation jobs contribute only to occupied-slot counts, not cross-session task labels; replaced same-generation stopping jobs remain visible until settlement. Chat clears live state on disconnect, exit and runtime session replacement and keeps at most 32 connection-local completion summaries for refresh; it adds no child journal or webview persistence. Existing delegate tool receipts are projected into readable advisory reports without changing their model/session content. Stop actions require a trusted workspace, foreground conversation, matching context and an observed running/queued batch/job/attempt. Pi rechecks that exact attempt before its human command cancels it. Worker Stop does not submit draft text/images, clear parent queues, approve tools, change quotas or claim remote termination. The widget is an integration contract with trusted installed extensions, not protection against a malicious extension in Pi's process.
|
|
213
|
+
|
|
210
214
|
Native change review reads VS Code's Git API, validates selected workspace paths, and opens the corresponding working-tree or staged view through the built-in diff/editor actions. It does not execute a shell command, stage, commit, discard, or restore files. Git integration availability and the filesystem race boundary still apply. The sidebar coordinator keeps independent Pi connections for explicitly opened conversations. Switching conversations or folders only changes the selected view. Stop and Disconnect target the selected process; extension-host shutdown stops all owned processes. User-initiated parallel conversations share their workspace files and are not isolated worktrees. Branching uses Pi's pre-runtime `--fork` to copy into a new owned session before extensions can write, then performs any active-branch or prompt selection there; the original process and session remain unchanged.
|
|
211
215
|
|
|
212
216
|
The extension introduces no network server, telemetry, runtime package dependency, or automatic model request when a view opens. Pi itself may contact its configured services when connected. Tests use synthetic homes, agent directories, providers, workspaces and profiles, plus an installed VS Code extension host; they do not access the user's live Pi credentials or conversations. Live provider availability and billing are outside the offline test evidence.
|
|
@@ -16,7 +16,7 @@ The parent remains the sole writer and verifies workers' evidence. Use delegatio
|
|
|
16
16
|
|
|
17
17
|
Startup enables dispatch only after host, settings and Guard checks; it launches no workers. Preflight may perform Pi-owned authentication/OAuth preparation. Off and safety revocations survive reload/session switches; restarting Pi restores the on default. While off, the tool is absent from model requests.
|
|
18
18
|
|
|
19
|
-
The TUI panel shows worker state, elapsed time and call counts. **Ready for review** requires a parent disposition; **stopping** still occupies a slot. RPC/
|
|
19
|
+
The TUI panel shows worker state, elapsed time and call counts. **Ready for review** requires a parent disposition; **stopping** still occupies a slot. RPC mode additionally publishes bounded, versioned worker metadata for SpecPi Chat's live Delegates panel; print/JSON modes do not mount widgets. Chat shows expandable tasks/metrics, attempt-bound Stop controls, and advisory transcript summaries. Update both Chat and the harness, then restart Pi. Expand tool output for findings and evidence.
|
|
20
20
|
|
|
21
21
|
Model/thinking changes revoke old results and preflight the new route. Unsupported routes pause dispatch; compatible selections resume it unless explicitly off. Guard, session, branch and task/scope changes revoke the current generation. Ordinary conversation advancement does not. Reload/off/on never reset spent process quotas or release unsettled requests. Restart Pi to load changed runtime code or a different working root.
|
|
22
22
|
|
|
@@ -240,6 +240,29 @@ cache. After eviction, those operations are revalidated against current state; t
|
|
|
240
240
|
cannot start inference or restore cancelled jobs. Generation and source checks still
|
|
241
241
|
apply. Neither cache eviction nor failed attempts reset quotas or block cancellation.
|
|
242
242
|
|
|
243
|
+
## RPC display metadata
|
|
244
|
+
|
|
245
|
+
In RPC mode, the extension emits `setWidget` with key `specpi-delegation-v1`
|
|
246
|
+
and a single JSON string in `widgetLines`. Version 1 contains `enabled`, occupied
|
|
247
|
+
`active` slots, `concurrency`, process `calls`/`callLimit`, and at most eight job rows.
|
|
248
|
+
Rows contain batch/job/attempt IDs, mode, state, settlement, call/tool counters,
|
|
249
|
+
elapsed milliseconds, a shortened task label, public provider/model names,
|
|
250
|
+
parent disposition and code-owned diagnostics. No source snapshots, full prompts,
|
|
251
|
+
child transcript or provider query is part of this channel. Sampling is at most
|
|
252
|
+
once a second between lifecycle updates and stops when no slots are occupied.
|
|
253
|
+
Invalidated-generation job identities/tasks are omitted; their unsettled slots still
|
|
254
|
+
count. A replaced batch in the same generation remains visible until settlement.
|
|
255
|
+
Shutdown/rebinding clears the old widget and timer. No new persistent store is added.
|
|
256
|
+
|
|
257
|
+
Chat treats this payload as untrusted, accepts one bounded versioned record, strips
|
|
258
|
+
controls and renders text only. Its Stop button invokes
|
|
259
|
+
`/delegate cancel-worker <batchId> <jobId> <attemptId>` through normal RPC command
|
|
260
|
+
dispatch. This human UI command checks the exact current queued/running attempt
|
|
261
|
+
before cancellation; it cannot cancel a later follow-up under the same job name,
|
|
262
|
+
start inference, raise limits or alter the user's draft/attachments. Parent Stop and
|
|
263
|
+
worker Stop are separate controls. Neither stopping nor settlement proves remote
|
|
264
|
+
termination. Existing model-facing `cancel` requests are unchanged.
|
|
265
|
+
|
|
243
266
|
## Cancellation and lifecycle
|
|
244
267
|
|
|
245
268
|
```json
|
|
@@ -163,7 +163,7 @@ export default function registerCommandGuard(
|
|
|
163
163
|
approvalTimeoutMs?: number;
|
|
164
164
|
} = {},
|
|
165
165
|
): void {
|
|
166
|
-
//
|
|
166
|
+
// The terminal startup chooser falls back to Guard, so it stays short; RPC starts off without a dialog.
|
|
167
167
|
// An approval waits on a person reading severity, category, cwd, affected paths, reason, and alternative;
|
|
168
168
|
// withTimeout cannot cancel the underlying prompt, so a short bound would deny work mid-decision and leave
|
|
169
169
|
// a live selector on screen. Both directions still fail closed, just on a human timescale.
|
|
@@ -230,14 +230,14 @@ export default function registerCommandGuard(
|
|
|
230
230
|
try {
|
|
231
231
|
subscribeGuardState();
|
|
232
232
|
// Pi RPC binds session_start before it starts reading UI responses.
|
|
233
|
-
// Begin
|
|
233
|
+
// Begin off without a startup dialog; /guard remains available
|
|
234
234
|
// for explicit changes once the RPC client is connected.
|
|
235
235
|
const choice = ctx.hasUI && ctx.mode !== "rpc" ? await startupChoice(ctx, startupTimeoutMs) : undefined;
|
|
236
236
|
if (state.generation !== startupGeneration) {
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
let mode: "guard" | "strict" | "off" = "guard";
|
|
240
|
+
let mode: "guard" | "strict" | "off" = ctx.mode === "rpc" ? "off" : "guard";
|
|
241
241
|
if (choice === "Strict") {
|
|
242
242
|
mode = "strict";
|
|
243
243
|
} else if (choice === "Off for this session") {
|
|
@@ -86,8 +86,9 @@ export function createDelegationController({
|
|
|
86
86
|
cost: "unavailable; no invoice cap",
|
|
87
87
|
batches: [...[...history.values()].map((item) => structuredClone(item)), ...[...batches.values()].map(summary)],
|
|
88
88
|
});
|
|
89
|
-
// Presentation
|
|
90
|
-
|
|
89
|
+
// Presentation samples bounded metadata only: no source checks or provider queries.
|
|
90
|
+
// RPC includes terminal rows so a stopped worker does not silently disappear.
|
|
91
|
+
const presentation = ({ includeInactive = false } = {}) => ({
|
|
91
92
|
enabled,
|
|
92
93
|
active,
|
|
93
94
|
concurrency: policy.concurrency,
|
|
@@ -95,9 +96,24 @@ export function createDelegationController({
|
|
|
95
96
|
callLimit: policy.sessionCalls,
|
|
96
97
|
jobs: [...batches.values()].flatMap((batch) =>
|
|
97
98
|
[...batch.jobs.values()]
|
|
98
|
-
.filter((job) =>
|
|
99
|
+
.filter((job) =>
|
|
100
|
+
includeInactive
|
|
101
|
+
? batch.generation === generation
|
|
102
|
+
: job.settling || (!batch.retired && !quiet.has(job.state) && !job.disposition),
|
|
103
|
+
)
|
|
99
104
|
.map((job) => ({
|
|
100
105
|
id: job.spec.id,
|
|
106
|
+
...(includeInactive
|
|
107
|
+
? {
|
|
108
|
+
batchId: batch.id,
|
|
109
|
+
attemptId: job.attemptId,
|
|
110
|
+
task: job.spec.question?.slice(0, 240) || "",
|
|
111
|
+
provider: batch.model?.provider,
|
|
112
|
+
model: batch.model?.id,
|
|
113
|
+
disposition: job.disposition?.decision ?? null,
|
|
114
|
+
error: job.error ?? null,
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
101
117
|
mode: job.mode,
|
|
102
118
|
state: job.state,
|
|
103
119
|
settling: job.settling,
|
|
@@ -395,6 +411,13 @@ export function createDelegationController({
|
|
|
395
411
|
}
|
|
396
412
|
|
|
397
413
|
active -= 1;
|
|
414
|
+
// A replaced batch can still own a stopping worker. Publish
|
|
415
|
+
// its settlement before cleanup removes it, but never expose
|
|
416
|
+
// an invalidated generation to the new context.
|
|
417
|
+
if (batch.retired && batch.generation === generation) {
|
|
418
|
+
changed();
|
|
419
|
+
}
|
|
420
|
+
|
|
398
421
|
cleanupBatch(batch);
|
|
399
422
|
changed();
|
|
400
423
|
pump();
|
|
@@ -2,7 +2,13 @@ import { DelegationError, publicErrorMessage } from "./errors.mjs";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { createDelegationController } from "./core.mjs";
|
|
4
4
|
import { DEFAULT_TIMEOUT_MINUTES, DEFAULT_BUDGET_MULTIPLIER, timeoutLimits, budgetLimits } from "./protocol.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createLivePanel,
|
|
7
|
+
createRpcPanel,
|
|
8
|
+
createToolRenderers,
|
|
9
|
+
readableLimits,
|
|
10
|
+
readableStatus,
|
|
11
|
+
} from "./presentation.mjs";
|
|
6
12
|
|
|
7
13
|
const USAGE =
|
|
8
14
|
"Usage: /delegate [on|off|status|limits|budget [<multiplier>|reset]|timeout [<minutes>|reset]|cancel <batchId>]";
|
|
@@ -154,6 +160,7 @@ export function createDelegationExtension(
|
|
|
154
160
|
};
|
|
155
161
|
|
|
156
162
|
const panel = presentation ? createLivePanel(() => controller.presentation(), presentation) : undefined;
|
|
163
|
+
const rpcPanel = createRpcPanel(() => controller.presentation({ includeInactive: true }));
|
|
157
164
|
|
|
158
165
|
const status = () => ({
|
|
159
166
|
...controller.status(),
|
|
@@ -176,6 +183,7 @@ export function createDelegationExtension(
|
|
|
176
183
|
: undefined,
|
|
177
184
|
);
|
|
178
185
|
panel?.update();
|
|
186
|
+
rpcPanel.update();
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
function invalidate(reason, preserveStartupDefault = false) {
|
|
@@ -207,6 +215,7 @@ export function createDelegationExtension(
|
|
|
207
215
|
|
|
208
216
|
const factory = (pi) => {
|
|
209
217
|
panel?.dispose();
|
|
218
|
+
rpcPanel.dispose();
|
|
210
219
|
detach();
|
|
211
220
|
bindingEpoch += 1;
|
|
212
221
|
const issuedEpoch = bindingEpoch;
|
|
@@ -360,6 +369,7 @@ export function createDelegationExtension(
|
|
|
360
369
|
currentContext = ctx;
|
|
361
370
|
toolsReady = true;
|
|
362
371
|
panel?.bind(ctx);
|
|
372
|
+
rpcPanel.bind(ctx);
|
|
363
373
|
invalidate("session started or resources reloaded", true);
|
|
364
374
|
try {
|
|
365
375
|
loadTimeout();
|
|
@@ -390,6 +400,7 @@ export function createDelegationExtension(
|
|
|
390
400
|
|
|
391
401
|
invalidate("session shutdown");
|
|
392
402
|
panel?.dispose();
|
|
403
|
+
rpcPanel.dispose();
|
|
393
404
|
prepareContext(undefined, true);
|
|
394
405
|
detach();
|
|
395
406
|
currentContext = undefined;
|
|
@@ -443,9 +454,49 @@ export function createDelegationExtension(
|
|
|
443
454
|
}
|
|
444
455
|
|
|
445
456
|
currentContext = ctx;
|
|
446
|
-
const [action = "status", id, extra] = args.trim().split(/\s+/u).filter(Boolean);
|
|
457
|
+
const [action = "status", id, extra, attemptId, ...rest] = args.trim().split(/\s+/u).filter(Boolean);
|
|
447
458
|
try {
|
|
448
|
-
|
|
459
|
+
// Chat binds Stop to an exact observed attempt, never a future
|
|
460
|
+
// follow-up with the same job name. This command starts no model turn.
|
|
461
|
+
if (action === "cancel-worker") {
|
|
462
|
+
const job = controller
|
|
463
|
+
.presentation({ includeInactive: true })
|
|
464
|
+
.jobs.find(
|
|
465
|
+
(candidate) =>
|
|
466
|
+
candidate.batchId === id &&
|
|
467
|
+
candidate.id === extra &&
|
|
468
|
+
candidate.attemptId === attemptId,
|
|
469
|
+
);
|
|
470
|
+
if (!ctx.hasUI || rest.length || !job || !["queued", "running"].includes(job.state)) {
|
|
471
|
+
ctx.ui.notify(
|
|
472
|
+
"That delegate attempt is no longer running. Its state was not changed.",
|
|
473
|
+
"warning",
|
|
474
|
+
);
|
|
475
|
+
updatePresentation();
|
|
476
|
+
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
await controller.execute({
|
|
481
|
+
operation: "cancel",
|
|
482
|
+
requestId: `human-${randomUUID()}`,
|
|
483
|
+
batchId: id,
|
|
484
|
+
jobId: extra,
|
|
485
|
+
});
|
|
486
|
+
ctx.ui.notify(
|
|
487
|
+
"Delegate stop requested. The slot stays occupied until Pi reports settlement; remote termination is not guaranteed.",
|
|
488
|
+
"info",
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
extra ||
|
|
496
|
+
attemptId ||
|
|
497
|
+
rest.length ||
|
|
498
|
+
(id && !["cancel", "timeout", "budget"].includes(action))
|
|
499
|
+
) {
|
|
449
500
|
throw new DelegationError(USAGE);
|
|
450
501
|
}
|
|
451
502
|
|
|
@@ -139,6 +139,88 @@ export function createLivePanel(getView, { truncateToWidth }) {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Versioned display metadata over Pi's supported string-array widget transport.
|
|
143
|
+
// Never send snapshots, full prompts, child transcripts or raw provider diagnostics.
|
|
144
|
+
export function createRpcPanel(getView) {
|
|
145
|
+
const key = "specpi-delegation-v1";
|
|
146
|
+
let context;
|
|
147
|
+
let timer;
|
|
148
|
+
let signature;
|
|
149
|
+
const dispose = () => {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
timer = undefined;
|
|
152
|
+
if (context?.mode === "rpc") {
|
|
153
|
+
try {
|
|
154
|
+
context.ui.setWidget(key, undefined);
|
|
155
|
+
} catch {
|
|
156
|
+
// UI teardown cannot affect worker ownership.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
context = undefined;
|
|
161
|
+
signature = undefined;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const update = () => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
timer = undefined;
|
|
167
|
+
if (context?.mode !== "rpc" || !context.ui?.setWidget) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const view = getView();
|
|
172
|
+
const payload = {
|
|
173
|
+
version: 1,
|
|
174
|
+
enabled: view.enabled,
|
|
175
|
+
active: view.active,
|
|
176
|
+
concurrency: view.concurrency,
|
|
177
|
+
calls: view.calls,
|
|
178
|
+
callLimit: view.callLimit,
|
|
179
|
+
jobs: view.jobs.slice(0, 8).map((job) => ({
|
|
180
|
+
id: job.id,
|
|
181
|
+
batchId: job.batchId,
|
|
182
|
+
attemptId: job.attemptId,
|
|
183
|
+
mode: job.mode,
|
|
184
|
+
state: job.state,
|
|
185
|
+
settling: job.settling,
|
|
186
|
+
calls: job.calls,
|
|
187
|
+
tools: job.tools,
|
|
188
|
+
elapsedMs: job.elapsedMs,
|
|
189
|
+
disposition: job.disposition,
|
|
190
|
+
task: plain(job.task).slice(0, 240),
|
|
191
|
+
provider: plain(job.provider).slice(0, 128),
|
|
192
|
+
model: plain(job.model).slice(0, 128),
|
|
193
|
+
error: job.error ? plain(job.error).slice(0, 512) : null,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
const next = JSON.stringify(payload);
|
|
197
|
+
if (next !== signature) {
|
|
198
|
+
context.ui.setWidget(key, [next]);
|
|
199
|
+
signature = next;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (view.active > 0) {
|
|
203
|
+
timer = setTimeout(() => {
|
|
204
|
+
try {
|
|
205
|
+
update();
|
|
206
|
+
} catch {
|
|
207
|
+
// Optional RPC UI failures never escape the sampling timer.
|
|
208
|
+
}
|
|
209
|
+
}, 1000);
|
|
210
|
+
timer.unref?.();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
update,
|
|
216
|
+
dispose,
|
|
217
|
+
bind(ctx) {
|
|
218
|
+
dispose();
|
|
219
|
+
context = ctx;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
142
224
|
export function readableStatus(state) {
|
|
143
225
|
const mode = state.enabled ? "enabled" : state.updating ? "updating model" : state.requested ? "paused" : "off";
|
|
144
226
|
const lines = [
|
package/package.json
CHANGED
package/scripts/specpi.mjs
CHANGED
|
@@ -172,6 +172,12 @@ function resolveCommand(command, env = process.env) {
|
|
|
172
172
|
return path.resolve(file);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// Like shell PATH lookup, continue past inaccessible candidates.
|
|
176
|
+
// Explicit executable paths must still report permission failures.
|
|
177
|
+
if (!hasPath && (error.code === "EACCES" || error.code === "EPERM")) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
175
181
|
if (error.code !== "ENOENT" && error.code !== "ENOTDIR") {
|
|
176
182
|
throw error;
|
|
177
183
|
}
|