@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -3
  3. package/VERIFY.md +2 -1
  4. package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +3 -1
  5. package/docs/superpowers/plans/2026-09-10-reader-consistency.md +115 -0
  6. package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +469 -0
  7. package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +92 -0
  8. package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +771 -0
  9. package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +299 -0
  10. package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +609 -0
  11. package/docs/superpowers/plans/2026-09-14-terminal-model.md +145 -0
  12. package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +224 -0
  13. package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +341 -0
  14. package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +875 -0
  15. package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +116 -0
  16. package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +517 -0
  17. package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +114 -0
  18. package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +120 -0
  19. package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +146 -0
  20. package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +116 -0
  21. package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +84 -0
  22. package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +92 -0
  23. package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +123 -0
  24. package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +204 -0
  25. package/package.json +3 -2
  26. package/runner/job-runner.mjs +8 -7
  27. package/runner/pty-runner.mjs +616 -27
  28. package/runner/state-coordinator.mjs +43 -17
  29. package/runner/state-runner.mjs +6 -5
  30. package/scripts/run-perf-gate.mjs +40 -0
  31. package/src/commands/agent-board.ts +8 -8
  32. package/src/commands/attach-flow.ts +5 -5
  33. package/src/core/control-protocol.mjs +482 -0
  34. package/src/core/editor-state-reporter.mjs +11 -1
  35. package/src/core/foreground-preview-cache.mjs +117 -0
  36. package/src/core/host-protocol.mjs +24 -0
  37. package/src/core/locks.mjs +68 -14
  38. package/src/core/paths.mjs +35 -3
  39. package/src/core/pid.mjs +32 -1
  40. package/src/core/pty-attach-jiggle-controller.mjs +27 -3
  41. package/src/core/pty-attach-reconnect.mjs +13 -6
  42. package/src/core/pty-attach-render.mjs +20 -0
  43. package/src/core/state-commands.mjs +88 -6
  44. package/src/core/status-consistency.mjs +98 -0
  45. package/src/core/store.mjs +59 -13
  46. package/src/core/terminal-attach-client.mjs +803 -0
  47. package/src/core/terminal-attach-protocol.mjs +252 -0
  48. package/src/core/terminal-model.mjs +222 -0
  49. package/src/core/terminal-snapshot.mjs +440 -0
  50. package/src/index.ts +12 -4
  51. package/src/runtime/service.mjs +284 -16
  52. package/src/ui/dashboard.ts +41 -33
  53. package/src/ui/pty-attach.ts +236 -72
  54. package/src/core/pty-input.mjs +0 -47
@@ -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] reserved for host-start lease nesting;
153
- * host-meta is always acquired independently here (short critical section).
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 lock = tryAcquireOwnedViewLock(root, provisionalHost.viewId, "host-meta");
158
- if (!lock.acquired) return { claimed: false, host: null };
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 short hold — updateOwnedHost itself acquires
217
- * host-meta without a reclaimable identity, so concurrent fenced writes look
218
- * blocked to each other) are transient here: retry a few times with a short
219
- * synchronous sleep before giving up. A genuinely orphaned host-meta lock
220
- * (holder SIGKILLed mid-hold) survives the window and surfaces as retryable
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 };