@zhuxixi/pi-agent-board 0.7.0 → 0.8.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 +26 -0
- package/README.md +6 -3
- package/VERIFY.md +2 -1
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -1
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +115 -0
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +469 -0
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +92 -0
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +771 -0
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +299 -0
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +609 -0
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +145 -0
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +224 -0
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +341 -0
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +875 -0
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +116 -0
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +517 -0
- package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +114 -0
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +120 -0
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +146 -0
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +116 -0
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +84 -0
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +92 -0
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +123 -0
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +204 -0
- package/package.json +3 -2
- package/runner/job-runner.mjs +8 -7
- package/runner/pty-runner.mjs +616 -27
- package/runner/state-coordinator.mjs +43 -17
- package/runner/state-runner.mjs +6 -5
- package/scripts/run-perf-gate.mjs +40 -0
- package/src/commands/agent-board.ts +8 -8
- package/src/commands/attach-flow.ts +5 -5
- package/src/core/control-protocol.mjs +482 -0
- package/src/core/editor-state-reporter.mjs +11 -1
- package/src/core/foreground-preview-cache.mjs +117 -0
- package/src/core/host-protocol.mjs +24 -0
- package/src/core/locks.mjs +68 -14
- package/src/core/paths.mjs +35 -3
- package/src/core/pid.mjs +32 -1
- package/src/core/pty-attach-jiggle-controller.mjs +27 -3
- package/src/core/pty-attach-reconnect.mjs +13 -6
- package/src/core/pty-attach-render.mjs +20 -0
- package/src/core/state-commands.mjs +88 -6
- package/src/core/status-consistency.mjs +98 -0
- package/src/core/store.mjs +59 -13
- package/src/core/terminal-attach-client.mjs +803 -0
- package/src/core/terminal-attach-protocol.mjs +252 -0
- package/src/core/terminal-model.mjs +222 -0
- package/src/core/terminal-snapshot.mjs +440 -0
- package/src/index.ts +12 -4
- package/src/runtime/service.mjs +284 -16
- package/src/ui/dashboard.ts +41 -33
- package/src/ui/pty-attach.ts +236 -72
- package/src/core/pty-input.mjs +0 -47
package/src/core/store.mjs
CHANGED
|
@@ -8,9 +8,9 @@ import { atomicWriteJson, ensureDir, readJson } from "./atomic.mjs";
|
|
|
8
8
|
import { sameHostOwner } from "./host-coordination.mjs";
|
|
9
9
|
import { tryAcquireOwnedViewLock } from "./locks.mjs";
|
|
10
10
|
import * as P from "./paths.mjs";
|
|
11
|
-
import { isAlive } from "./pid.mjs";
|
|
11
|
+
import { currentProcessIdentity, isAlive } from "./pid.mjs";
|
|
12
12
|
import { readCodeRefs, summarizeCodeRefs } from "./code-refs-store.mjs";
|
|
13
|
-
import { readDiagnosticSummary } from "./diagnostics.mjs";
|
|
13
|
+
import { appendDiagnostic, readDiagnosticSummary } from "./diagnostics.mjs";
|
|
14
14
|
import { readEvidence, summarizeEvidence } from "./evidence.mjs";
|
|
15
15
|
import { readFollowUpQueue, summarizeFollowUpQueue } from "./follow-up-queue.mjs";
|
|
16
16
|
import { readSteering, summarizeSteering } from "./steering.mjs";
|
|
@@ -141,6 +141,39 @@ function hostClaimActive(host) {
|
|
|
141
141
|
return Boolean(host && (host.state === "starting" || host.state === "alive" || host.state === "stopping"));
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Views with an unrecovered host-meta contention report on record. The
|
|
146
|
+
* heartbeat path calls updateOwnedHost once per second, so without this a
|
|
147
|
+
* sustained contention episode would flood diagnostics.jsonl (issue #112).
|
|
148
|
+
*/
|
|
149
|
+
const hostMetaContentionReported = new Set();
|
|
150
|
+
|
|
151
|
+
/** Test hook: clear the per-process contention report throttle. */
|
|
152
|
+
export function clearHostMetaThrottleForTests() {
|
|
153
|
+
hostMetaContentionReported.clear();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Best-effort single warn per view per contention episode; diagnostics must
|
|
158
|
+
* never break the caller (appendDiagnostic throws on fs failure).
|
|
159
|
+
*/
|
|
160
|
+
function reportHostMetaContention(root, viewId, code, message, details) {
|
|
161
|
+
if (hostMetaContentionReported.has(viewId)) return;
|
|
162
|
+
hostMetaContentionReported.add(viewId);
|
|
163
|
+
try {
|
|
164
|
+
appendDiagnostic(root, viewId, { source: "store", level: "warn", code, message, details });
|
|
165
|
+
} catch { /* best effort */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Identity stamped on host-meta acquisitions so a holder that dies mid-hold
|
|
170
|
+
* leaves a reclaimable record (issue #112).
|
|
171
|
+
* @returns {{pid: number, startToken: string|null}}
|
|
172
|
+
*/
|
|
173
|
+
function hostMetaIdentity() {
|
|
174
|
+
return currentProcessIdentity();
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
/**
|
|
145
178
|
* Atomically create the provisional `starting` host record for a new instance.
|
|
146
179
|
* The ONLY entry point allowed to move "no claim / reclaimable terminal state"
|
|
@@ -149,13 +182,23 @@ function hostClaimActive(host) {
|
|
|
149
182
|
* ready/stop fields so no stale owner data survives the handover.
|
|
150
183
|
* @param {string} root
|
|
151
184
|
* @param {Partial<HostStatus> & { viewId: string, instanceId: string }} provisionalHost
|
|
152
|
-
* @param {{ heldStartLease?: unknown }} [opts]
|
|
153
|
-
*
|
|
185
|
+
* @param {{ heldStartLease?: unknown, lockImpl?: typeof tryAcquireOwnedViewLock }} [opts]
|
|
186
|
+
* heldStartLease is reserved for host-start lease nesting; lockImpl injects
|
|
187
|
+
* the host-meta acquisition for deterministic contention/identity tests.
|
|
154
188
|
* @returns {{ claimed: boolean, host: HostStatus|null }}
|
|
155
189
|
*/
|
|
156
190
|
export function claimHost(root, provisionalHost, opts = {}) {
|
|
157
|
-
const
|
|
158
|
-
|
|
191
|
+
const acquireHostMeta = opts.lockImpl ?? tryAcquireOwnedViewLock;
|
|
192
|
+
const lock = acquireHostMeta(root, provisionalHost.viewId, "host-meta", { identity: hostMetaIdentity() });
|
|
193
|
+
if (!lock.acquired) {
|
|
194
|
+
// busy is ordinary millisecond-scale contention; blocked (identity-less
|
|
195
|
+
// holder) is the orphan-lock signature worth a diagnostic (issue #112).
|
|
196
|
+
if (lock.reason === "blocked") {
|
|
197
|
+
reportHostMetaContention(root, provisionalHost.viewId, "host_meta_claim_contended", "host-meta lease blocked; host claim not established", { reason: lock.reason });
|
|
198
|
+
}
|
|
199
|
+
return { claimed: false, host: null };
|
|
200
|
+
}
|
|
201
|
+
hostMetaContentionReported.delete(provisionalHost.viewId);
|
|
159
202
|
try {
|
|
160
203
|
const existing = readHost(root, provisionalHost.viewId);
|
|
161
204
|
if (hostClaimActive(existing)) return { claimed: false, host: existing };
|
|
@@ -213,12 +256,11 @@ export function claimHost(root, provisionalHost, opts = {}) {
|
|
|
213
256
|
* few milliseconds, but a one-shot acquire can land inside that window and
|
|
214
257
|
* return busy — a revoke or recovery write that silently no-ops is a real
|
|
215
258
|
* reliability bug, not just a test race. Both `busy` (live owner) and
|
|
216
|
-
* `blocked` (identity-less
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* not-updated — never as ownership loss.
|
|
259
|
+
* `blocked` (identity-less holder) are transient here: retry a few times with
|
|
260
|
+
* a short synchronous sleep before giving up, and record a warn diagnostic on
|
|
261
|
+
* a sustained episode (issue #112). Acquisitions stamp a full process identity
|
|
262
|
+
* (issue #112), so a holder that dies mid-hold leaves a reclaimable record;
|
|
263
|
+
* legacy identity-less residue is reclaimed past the orphan age gate.
|
|
222
264
|
*/
|
|
223
265
|
const UPDATE_LOCK_BUSY_ATTEMPTS = 3;
|
|
224
266
|
const UPDATE_LOCK_BUSY_SLEEP_MS = 20;
|
|
@@ -243,17 +285,21 @@ const UPDATE_LOCK_BUSY_SLEEP_MS = 20;
|
|
|
243
285
|
export function updateOwnedHost(root, viewId, expectedInstanceId, mutate, opts = {}) {
|
|
244
286
|
const acquireHostMeta = opts.lockImpl ?? tryAcquireOwnedViewLock;
|
|
245
287
|
let lock;
|
|
288
|
+
let lastReason = null;
|
|
246
289
|
for (let attempt = 0; ; attempt++) {
|
|
247
|
-
lock = acquireHostMeta(root, viewId, "host-meta");
|
|
290
|
+
lock = acquireHostMeta(root, viewId, "host-meta", { identity: hostMetaIdentity() });
|
|
248
291
|
if (lock.acquired) break;
|
|
292
|
+
lastReason = lock.reason;
|
|
249
293
|
// busy and blocked are both millisecond-scale holds for host-meta;
|
|
250
294
|
// neither is ownership information — only the fenced read below is.
|
|
251
295
|
if (attempt >= UPDATE_LOCK_BUSY_ATTEMPTS - 1) {
|
|
296
|
+
reportHostMetaContention(root, viewId, "host_meta_lease_contended", "host-meta lease contended; fenced write not applied", { attempts: UPDATE_LOCK_BUSY_ATTEMPTS, lastReason });
|
|
252
297
|
return { updated: false, ownerChanged: false, host: null };
|
|
253
298
|
}
|
|
254
299
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, UPDATE_LOCK_BUSY_SLEEP_MS);
|
|
255
300
|
}
|
|
256
301
|
try {
|
|
302
|
+
hostMetaContentionReported.delete(viewId);
|
|
257
303
|
const host = readHost(root, viewId);
|
|
258
304
|
if (!host || !sameHostOwner(host, expectedInstanceId)) {
|
|
259
305
|
return { updated: false, ownerChanged: true, host };
|