@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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 +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LEGACY FALLBACK (issue #91 phase 6 marking): the shrink-and-hold jiggle is
|
|
3
|
+
* kept only for pre-protocol runners and the AGENT_BOARD_TERMINAL_SNAPSHOT=0
|
|
4
|
+
* kill switch. It is NOT a success condition of the snapshot+subscribe
|
|
5
|
+
* protocol path — the protocol re-baselines the view from runner-owned
|
|
6
|
+
* snapshots (phase 4), and jiggle arming is skipped once the probe resolves
|
|
7
|
+
* to the protocol attach mode. Removal condition: the installed runner fleet
|
|
8
|
+
* is on the snapshot protocol baseline (wire-detectable via hello protocol
|
|
9
|
+
* fields).
|
|
10
|
+
*
|
|
11
|
+
* Injectable orchestration for the attach shrink-and-hold jiggle protocol.
|
|
12
|
+
*
|
|
13
|
+
* Replaces the pulse-pair jiggle (shrink → 200ms → restore) with a
|
|
14
|
+
* shrink-and-hold protocol: on connect we resize the child PTY to a safe
|
|
15
|
+
* alternate size and KEEP it there until the child's own rendering proves it
|
|
16
|
+
* observed the size change (a full clear, \x1b[2J, which pi-tui emits from
|
|
17
|
+
* fullRender(true) whenever widthChanged fires). At normal sizes the alternate
|
|
18
|
+
* size is one column/row smaller; near the minimum it changes only a safe
|
|
19
|
+
* dimension, and at 20x5 no jiggle is attempted. Because there is no
|
|
20
|
+
* "restore" that can cancel the shrink before a clear is seen, event-loop
|
|
21
|
+
* coalescing on either side (outer dashboard timers or the child's
|
|
22
|
+
* SIGWINCH/render throttle) can no longer collapse a jiggle into a
|
|
23
|
+
* net-zero size change.
|
|
24
|
+
*
|
|
25
|
+
* Cold-start (issue #25): if the child TUI starts rendering AFTER we shrink,
|
|
26
|
+
* its first frame baselines at the shrunk size and never clears. The
|
|
27
|
+
* re-arm — the first \x1b[?2026h frame — therefore restores the original
|
|
28
|
+
* size: the now-rendering child sees a width different from its baseline
|
|
29
|
+
* and fullRenders. Guards:
|
|
30
|
+
* G1 NO_FRAME_RESTORE_MS — no TUI frame within 6s → restore (no renderer
|
|
31
|
+
* to trigger; continuing to hold has no purpose). If the TUI boots even
|
|
32
|
+
* later, its first frame re-arms a fresh hold (F1 slow-boot probe), so
|
|
33
|
+
* healing still fires the moment the child actually starts rendering.
|
|
34
|
+
* G2 backoff budget exhausted without a clear → restore (non-pi children,
|
|
35
|
+
* dead sessions).
|
|
36
|
+
* G3 restoreAndStop() — component close/detach restores while the socket
|
|
37
|
+
* is still usable.
|
|
38
|
+
* G4 notifyExternalResize() — a real user resize cancels the hold and
|
|
39
|
+
* adopts the new size.
|
|
40
|
+
* G5 start() restores any previous hold before arming a new one (reconnect).
|
|
41
|
+
* G6 post-restore verify (issue #42): the frameStart fast path restores the
|
|
42
|
+
* hold on the FIRST frame, which may predate the shrink — if the restore
|
|
43
|
+
* then collapses both resizes into a net-zero change at the child, the
|
|
44
|
+
* child renders nothing and never clears. While a frame has been seen
|
|
45
|
+
* and no clear followed, each backoff tick re-arms the shrink (which
|
|
46
|
+
* lands alone and cannot collapse), bounded by the same retry budget.
|
|
47
|
+
*
|
|
48
|
+
* If pi-tui ever drops the 2026h sequence, re-arm degrades to G1: the PTY
|
|
49
|
+
* is restored within 6s — still better than the pre-#10 behavior. At the
|
|
50
|
+
* minimum supported PTY size (20x5), jiggle is disabled because the runner's
|
|
51
|
+
* clamp leaves no valid alternate size.
|
|
52
|
+
*/
|
|
53
|
+
import { resizeJiggleSize } from "./pty-scroll.mjs";
|
|
54
|
+
import {
|
|
55
|
+
advanceRetry,
|
|
56
|
+
createJiggleRetryState,
|
|
57
|
+
feedOutput,
|
|
58
|
+
nextRetryDelay,
|
|
59
|
+
stopRetry,
|
|
60
|
+
} from "./pty-attach-jiggle-retry.mjs";
|
|
61
|
+
|
|
62
|
+
/** Restore the held (shrunk) child PTY when no TUI frame arrives this long. */
|
|
63
|
+
const NO_FRAME_RESTORE_MS = 6000;
|
|
64
|
+
/**
|
|
65
|
+
* Grace window after the frameStart fast-path restore before the chain may
|
|
66
|
+
* re-poke (issue #42). A healthy child proves the restore landed with a
|
|
67
|
+
* fullRender clear within a few hundred ms even on large screens; without the
|
|
68
|
+
* grace a slow-but-healthy clear would trigger a pointless re-shrink flicker.
|
|
69
|
+
*/
|
|
70
|
+
const POST_RESTORE_VERIFY_MS = 900;
|
|
71
|
+
|
|
72
|
+
/** Lifetime cap on runtime desync heals (issue #11): a persistent misdiagnosis
|
|
73
|
+
* must not flicker the screen forever; after this many attempts the backstop
|
|
74
|
+
* stays quiet until the controller is recreated. Consumed on heal() entry. */
|
|
75
|
+
const HEAL_MAX_PER_LIFETIME = 5;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} JiggleRetryControllerDeps
|
|
79
|
+
* @property {(cols: number, rows: number) => void} sendResize - Resize the child PTY.
|
|
80
|
+
* @property {(fn: () => void, ms: number) => unknown} setTimeoutFn - Timer factory.
|
|
81
|
+
* @property {(timer: unknown) => void} clearTimeoutFn - Timer canceller.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {JiggleRetryControllerDeps} deps
|
|
86
|
+
*/
|
|
87
|
+
export function createJiggleRetryController(deps) {
|
|
88
|
+
const { sendResize, setTimeoutFn, clearTimeoutFn } = deps;
|
|
89
|
+
let state = createJiggleRetryState();
|
|
90
|
+
let carry = "";
|
|
91
|
+
let tuiFrameSeen = false;
|
|
92
|
+
/** True while the child PTY is parked at the shrunk size. */
|
|
93
|
+
let held = false;
|
|
94
|
+
/** True once the original size has been sent back (restore is one-shot). */
|
|
95
|
+
let restored = false;
|
|
96
|
+
/** @type {[number, number]} */
|
|
97
|
+
let originalCols = 0;
|
|
98
|
+
let originalRows = 0;
|
|
99
|
+
/** The safe alternate size used while a jiggle hold is active, or null when unsupported. */
|
|
100
|
+
let holdSize = null;
|
|
101
|
+
/** @type {unknown | null} */
|
|
102
|
+
let chainTimer = null;
|
|
103
|
+
/** @type {unknown | null} */
|
|
104
|
+
let g1Timer = null;
|
|
105
|
+
/** Runtime heals spent (issue #11); never reset by start()/restoreAndStop(). */
|
|
106
|
+
let healCount = 0;
|
|
107
|
+
|
|
108
|
+
function clearChainTimer() {
|
|
109
|
+
if (chainTimer === null) return;
|
|
110
|
+
clearTimeoutFn(chainTimer);
|
|
111
|
+
chainTimer = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearG1Timer() {
|
|
115
|
+
if (g1Timer === null) return;
|
|
116
|
+
clearTimeoutFn(g1Timer);
|
|
117
|
+
g1Timer = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clearAllTimers() {
|
|
121
|
+
clearChainTimer();
|
|
122
|
+
clearG1Timer();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Restore the child PTY to the original size exactly once. No-op while
|
|
127
|
+
* not held or after the restore already happened.
|
|
128
|
+
*/
|
|
129
|
+
function restoreIfHeld() {
|
|
130
|
+
if (!held || restored) return;
|
|
131
|
+
sendResize(originalCols, originalRows);
|
|
132
|
+
restored = true;
|
|
133
|
+
held = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One chain tick: advance the backoff budget, then re-arm the hold if the
|
|
138
|
+
* child has a renderer (a TUI frame was seen) but no clear ever proved the
|
|
139
|
+
* last size delta landed and the hold is currently inactive (issue #42:
|
|
140
|
+
* the frameStart fast-path restore can race the shrink into a net-zero
|
|
141
|
+
* size change at the child — its throttled renderer reads back the
|
|
142
|
+
* original size and stays silent forever — so a clear-less restore must
|
|
143
|
+
* be followed by a fresh shrink, which lands alone and cannot collapse).
|
|
144
|
+
* Children that never rendered a frame (shells, cold boots) keep the old
|
|
145
|
+
* pure-countdown behavior and are handled by G1/G2 only.
|
|
146
|
+
*/
|
|
147
|
+
function tickChain() {
|
|
148
|
+
chainTimer = null;
|
|
149
|
+
state = advanceRetry(state);
|
|
150
|
+
ensureHold();
|
|
151
|
+
if (state.clearDetected || state.stopped) return;
|
|
152
|
+
scheduleNextRetry();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ensureHold() {
|
|
156
|
+
if (!tuiFrameSeen || held || state.clearDetected || state.stopped || !holdSize) return;
|
|
157
|
+
sendResize(holdSize.cols, holdSize.rows);
|
|
158
|
+
held = true;
|
|
159
|
+
restored = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function scheduleNextRetry() {
|
|
163
|
+
const delay = nextRetryDelay(state);
|
|
164
|
+
if (delay === null) {
|
|
165
|
+
state = stopRetry(state);
|
|
166
|
+
restoreIfHeld(); // G2
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
chainTimer = setTimeoutFn(tickChain, delay);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function armG1() {
|
|
173
|
+
clearG1Timer();
|
|
174
|
+
g1Timer = setTimeoutFn(() => {
|
|
175
|
+
g1Timer = null;
|
|
176
|
+
if (!tuiFrameSeen) restoreIfHeld(); // G1
|
|
177
|
+
}, NO_FRAME_RESTORE_MS);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Arm a fresh hold for a (re)connected attach at the given PTY size.
|
|
182
|
+
* Restores any previous hold first (G5), then shrinks and holds.
|
|
183
|
+
* @param {number} cols
|
|
184
|
+
* @param {number} rows
|
|
185
|
+
*/
|
|
186
|
+
function start(cols, rows) {
|
|
187
|
+
clearAllTimers();
|
|
188
|
+
if (held) {
|
|
189
|
+
sendResize(originalCols, originalRows); // G5: unwind previous hold
|
|
190
|
+
restored = true;
|
|
191
|
+
held = false;
|
|
192
|
+
}
|
|
193
|
+
state = createJiggleRetryState();
|
|
194
|
+
carry = "";
|
|
195
|
+
tuiFrameSeen = false;
|
|
196
|
+
originalCols = cols;
|
|
197
|
+
originalRows = rows;
|
|
198
|
+
holdSize = resizeJiggleSize(cols, rows);
|
|
199
|
+
sendResize(cols, rows);
|
|
200
|
+
if (!holdSize) {
|
|
201
|
+
// The runner clamps below 20x5, so a smaller request would be a
|
|
202
|
+
// net-zero resize and cannot trigger a redraw. Leave the child at the
|
|
203
|
+
// requested original size and disable this attach's jiggle chain.
|
|
204
|
+
state = stopRetry(state);
|
|
205
|
+
held = false;
|
|
206
|
+
restored = true;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
sendResize(holdSize.cols, holdSize.rows);
|
|
210
|
+
held = true;
|
|
211
|
+
restored = false;
|
|
212
|
+
armG1();
|
|
213
|
+
scheduleNextRetry();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Feed one socket output chunk. A clear wins over the re-arm when both
|
|
218
|
+
* appear in one chunk — but frame cognition is still learned from that
|
|
219
|
+
* chunk: a clear only proves the child redraws, not that it isn't a TUI
|
|
220
|
+
* (a live fullRender chunk bundles a frame start with its clear, issue #11).
|
|
221
|
+
* The first TUI frame restores the held size (the
|
|
222
|
+
* child is now rendering and will fullRender on the width delta) and
|
|
223
|
+
* does NOT reschedule the chain; if G1 already released the hold before
|
|
224
|
+
* the TUI booted, the frame instead re-arms a fresh hold so the running
|
|
225
|
+
* child still sees a width delta (F1 slow-boot probe).
|
|
226
|
+
* Terminal chain states still learn frame cognition from later output
|
|
227
|
+
* (issue #106) — cognition only; re-opening the protocol stays heal()'s job.
|
|
228
|
+
* @param {string} data
|
|
229
|
+
*/
|
|
230
|
+
function feed(data) {
|
|
231
|
+
// Terminal chain states: a clear was seen (chain done) or the chain ended
|
|
232
|
+
// (G2/G3/G4). Output no longer drives the retry protocol — but frame
|
|
233
|
+
// cognition must still be learned from it (issue #106): a TUI whose first
|
|
234
|
+
// frame lands after the chain settled must still open the runtime desync
|
|
235
|
+
// backstop's gate 2 (issue #11), otherwise heal() stays unreachable for the
|
|
236
|
+
// rest of this connection. Cognition only — no timers, no resizes, and no
|
|
237
|
+
// retry-state change: re-opening the protocol is heal()'s job (rate-limited
|
|
238
|
+
// and lifetime-capped), not an output chunk's.
|
|
239
|
+
if (state.clearDetected || state.stopped) {
|
|
240
|
+
if (tuiFrameSeen) return; // latched already — nothing left to learn
|
|
241
|
+
const terminal = feedOutput(state, data, carry);
|
|
242
|
+
carry = terminal.carry; // keep cross-chunk marker detection intact
|
|
243
|
+
if (terminal.frameStartFound) tuiFrameSeen = true;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const result = feedOutput(state, data, carry);
|
|
247
|
+
state = result.state;
|
|
248
|
+
carry = result.carry;
|
|
249
|
+
// Learn frame cognition unconditionally, BEFORE the clear branch: a
|
|
250
|
+
// clear-wins chunk must not swallow it (issue #11), and only the FIRST
|
|
251
|
+
// frame ever seen drives the re-arm/fast-path logic below.
|
|
252
|
+
const firstFrame = result.frameStartFound && !tuiFrameSeen;
|
|
253
|
+
if (result.frameStartFound) tuiFrameSeen = true;
|
|
254
|
+
if (result.clearFound) {
|
|
255
|
+
clearAllTimers();
|
|
256
|
+
restoreIfHeld();
|
|
257
|
+
state = stopRetry({ ...state, clearDetected: true });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (firstFrame) {
|
|
261
|
+
clearG1Timer();
|
|
262
|
+
if (held) {
|
|
263
|
+
restoreIfHeld(); // fast path: child is rendering, width delta now lands
|
|
264
|
+
// …unless that frame was in flight BEFORE the shrink and the restore
|
|
265
|
+
// collapses both into a net-zero change at the child (issue #42).
|
|
266
|
+
// Restart the chain on the post-restore grace window: no clear within
|
|
267
|
+
// POST_RESTORE_VERIFY_MS → tickChain re-arms the hold (ensureHold).
|
|
268
|
+
clearChainTimer();
|
|
269
|
+
chainTimer = setTimeoutFn(tickChain, POST_RESTORE_VERIFY_MS);
|
|
270
|
+
} else {
|
|
271
|
+
// Slow boot: G1 released the hold before the TUI came up, so the
|
|
272
|
+
// child baselined at the original size. Re-arm a fresh hold — its
|
|
273
|
+
// next frame then sees a width delta and fullRenders. Guards: the
|
|
274
|
+
// clear path (primary) and G2 budget exhaustion (chain still
|
|
275
|
+
// ticking). G1 is NOT re-armed: frames are now flowing.
|
|
276
|
+
if (holdSize) sendResize(holdSize.cols, holdSize.rows);
|
|
277
|
+
held = holdSize !== null;
|
|
278
|
+
restored = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Runtime desync backstop (issue #11): re-arm the shrink-and-hold protocol
|
|
285
|
+
* mid-session. Unlike start(), tuiFrameSeen is preserved (the child has
|
|
286
|
+
* rendered), G1 is not armed (frames are flowing), and the budget is
|
|
287
|
+
* lifetime-capped so a misdiagnosis cannot flicker the screen forever.
|
|
288
|
+
* Consumes one budget slot on entry, including the tiny-terminal give-up.
|
|
289
|
+
* @param {number} cols
|
|
290
|
+
* @param {number} rows
|
|
291
|
+
* @returns {boolean} true when a heal hold was armed.
|
|
292
|
+
*/
|
|
293
|
+
function heal(cols, rows) {
|
|
294
|
+
if (healCount >= HEAL_MAX_PER_LIFETIME) return false;
|
|
295
|
+
healCount++;
|
|
296
|
+
clearAllTimers();
|
|
297
|
+
if (held) {
|
|
298
|
+
// Unwind any live hold (e.g. a previous clear-less heal) first.
|
|
299
|
+
sendResize(originalCols, originalRows);
|
|
300
|
+
restored = true;
|
|
301
|
+
held = false;
|
|
302
|
+
}
|
|
303
|
+
state = createJiggleRetryState();
|
|
304
|
+
carry = "";
|
|
305
|
+
originalCols = cols;
|
|
306
|
+
originalRows = rows;
|
|
307
|
+
holdSize = resizeJiggleSize(cols, rows);
|
|
308
|
+
if (!holdSize) {
|
|
309
|
+
state = stopRetry(state);
|
|
310
|
+
held = false;
|
|
311
|
+
restored = true;
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
sendResize(holdSize.cols, holdSize.rows);
|
|
315
|
+
held = true;
|
|
316
|
+
restored = false;
|
|
317
|
+
scheduleNextRetry(); // G2 backoff re-shrinks while a renderer is seen but no clear follows
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Restore the held size (if any) and stop all chain activity. Used by the
|
|
323
|
+
* component on close/detach while the socket is still usable (G3).
|
|
324
|
+
*/
|
|
325
|
+
function restoreAndStop() {
|
|
326
|
+
clearAllTimers();
|
|
327
|
+
restoreIfHeld();
|
|
328
|
+
state = stopRetry(state);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* A real user resize supersedes the hold protocol: cancel all timers,
|
|
333
|
+
* mark the hold void, adopt the new size as the new original, and stop
|
|
334
|
+
* the chain (G4).
|
|
335
|
+
* @param {number} cols
|
|
336
|
+
* @param {number} rows
|
|
337
|
+
*/
|
|
338
|
+
function notifyExternalResize(cols, rows) {
|
|
339
|
+
clearAllTimers();
|
|
340
|
+
held = false;
|
|
341
|
+
restored = false;
|
|
342
|
+
originalCols = cols;
|
|
343
|
+
originalRows = rows;
|
|
344
|
+
holdSize = null;
|
|
345
|
+
state = stopRetry(state);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
start,
|
|
350
|
+
feed,
|
|
351
|
+
heal,
|
|
352
|
+
restoreAndStop,
|
|
353
|
+
notifyExternalResize,
|
|
354
|
+
getState: () => ({ ...state, held, tuiFrameSeen, originalCols, originalRows, holdSize, healCount }),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Pure logic for attach jiggle-retry: detect full-clear, schedule backoff retries. */
|
|
2
|
+
|
|
3
|
+
export const BACKOFF_MS = Object.freeze([120, 500, 1500, 3000, 6000, 10000, 15000, 20000]);
|
|
4
|
+
export const MAX_RETRIES = BACKOFF_MS.length;
|
|
5
|
+
|
|
6
|
+
// Verified empirically in issue #2: a resize jiggle makes the child pi-tui run
|
|
7
|
+
// fullRender(true), whose output begins with a full clear — a controlled test
|
|
8
|
+
// against a live idle session received 2 clear sequences + ~920KB full repaint
|
|
9
|
+
// per jiggle. So \x1b[2J in the socket output is a reliable "jiggle landed"
|
|
10
|
+
// signal. If pi-tui ever changes fullRender to skip the clear, the detector
|
|
11
|
+
// silently degrades to the old one-shot behavior after MAX_RETRIES.
|
|
12
|
+
const FULL_CLEAR = "\x1b[2J";
|
|
13
|
+
// pi-tui begins every frame (including differential frames) with a synchronized
|
|
14
|
+
// output start sequence; boot-time extension output never contains it, so the
|
|
15
|
+
// first occurrence marks "child TUI has started rendering" (issue #10).
|
|
16
|
+
const TUI_FRAME_START = "\x1b[?2026h";
|
|
17
|
+
/** Carry enough bytes to catch either target sequence split at a chunk boundary. */
|
|
18
|
+
const CARRY_LEN = TUI_FRAME_START.length - 1; // 7 bytes
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} JiggleRetryState
|
|
22
|
+
* @property {number} retryIndex - Current retry round (0 = initial, not yet retried).
|
|
23
|
+
* @property {boolean} clearDetected - Whether a full-clear sequence was seen.
|
|
24
|
+
* @property {boolean} stopped - Whether the retry chain has been stopped.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Create initial retry state. */
|
|
28
|
+
export function createJiggleRetryState() {
|
|
29
|
+
return { retryIndex: 0, clearDetected: false, stopped: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Feed PTY output data into the state machine.
|
|
34
|
+
* Returns new state, updated carry buffer, and which target sequences were found.
|
|
35
|
+
* @param {JiggleRetryState} state
|
|
36
|
+
* @param {string} data - New output data chunk.
|
|
37
|
+
* @param {string} carry - Leftover partial escape sequence from previous chunk.
|
|
38
|
+
* @returns {{ state: JiggleRetryState, carry: string, clearFound: boolean, frameStartFound: boolean }}
|
|
39
|
+
*/
|
|
40
|
+
export function feedOutput(state, data, carry) {
|
|
41
|
+
const combined = carry + data;
|
|
42
|
+
const clearFound = hasFullClearSequence(combined);
|
|
43
|
+
const frameStartFound = hasTuiFrameStart(combined);
|
|
44
|
+
// Always keep the tail carry: a clear hit stops the chain (carry irrelevant), and
|
|
45
|
+
// a frame-start hit must still preserve a trailing half-sequence so an immediately
|
|
46
|
+
// following clear split across the next chunk is not missed.
|
|
47
|
+
const newCarry = tailCarry(combined);
|
|
48
|
+
const newState = clearFound ? { ...state, clearDetected: true } : state;
|
|
49
|
+
return { state: newState, carry: newCarry, clearFound, frameStartFound };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the delay before the next retry, or null if no more retries.
|
|
54
|
+
* @param {JiggleRetryState} state
|
|
55
|
+
* @returns {number | null}
|
|
56
|
+
*/
|
|
57
|
+
export function nextRetryDelay(state) {
|
|
58
|
+
if (state.stopped || state.clearDetected) return null;
|
|
59
|
+
if (state.retryIndex >= MAX_RETRIES) return null;
|
|
60
|
+
return BACKOFF_MS[state.retryIndex];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Advance to the next retry round.
|
|
65
|
+
* @param {JiggleRetryState} state
|
|
66
|
+
* @returns {JiggleRetryState}
|
|
67
|
+
*/
|
|
68
|
+
export function advanceRetry(state) {
|
|
69
|
+
return { ...state, retryIndex: state.retryIndex + 1 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Stop the retry chain (attach settled, component closed, etc.).
|
|
74
|
+
* @param {JiggleRetryState} state
|
|
75
|
+
* @returns {JiggleRetryState}
|
|
76
|
+
*/
|
|
77
|
+
export function stopRetry(state) {
|
|
78
|
+
return { ...state, stopped: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if data contains the full-clear escape sequence.
|
|
83
|
+
* @param {string} data
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function hasFullClearSequence(data) {
|
|
87
|
+
return data.includes(FULL_CLEAR);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if data contains the TUI frame-start (synchronized output) sequence.
|
|
92
|
+
* pi-tui begins every frame with \x1b[?2026h; boot-time extension output never
|
|
93
|
+
* contains it, so the first occurrence marks "child TUI has started rendering".
|
|
94
|
+
* @param {string} data
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function hasTuiFrameStart(data) {
|
|
98
|
+
return data.includes(TUI_FRAME_START);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Extract the tail carry for cross-chunk boundary detection. */
|
|
102
|
+
function tailCarry(data) {
|
|
103
|
+
// We only need to carry up to CARRY_LEN bytes to catch a split \x1b[2J or
|
|
104
|
+
// \x1b[?2026h. Find the last \x1b in the tail — everything before it can't be
|
|
105
|
+
// part of a split escape sequence.
|
|
106
|
+
const tail = data.slice(-CARRY_LEN);
|
|
107
|
+
const escIdx = tail.lastIndexOf("\x1b");
|
|
108
|
+
return escIdx >= 0 ? tail.slice(escIdx) : "";
|
|
109
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach reconnect policy (issue #48). The pty-runner can die without sending
|
|
3
|
+
* an exit message (uncaught crash), which previously left the attach view in a
|
|
4
|
+
* 150 ms reconnect loop forever with no way out. Two regimes:
|
|
5
|
+
* - ever connected: a crashed host is unrecoverable quickly — give up after a
|
|
6
|
+
* short window and report "host exited";
|
|
7
|
+
* - never connected: the host may still be cold-starting (service launches it
|
|
8
|
+
* right before attach) — allow a long window before declaring it unreachable.
|
|
9
|
+
*/
|
|
10
|
+
import { resolveTestMs } from "./test-knobs.mjs";
|
|
11
|
+
|
|
12
|
+
/** Give-up delay after a previously-established connection drops (ms). */
|
|
13
|
+
export const ATTACH_RECONNECT_TIMEOUT_MS = 15_000;
|
|
14
|
+
/** Give-up delay while waiting for the initial host connection (ms). */
|
|
15
|
+
export const ATTACH_HOST_START_TIMEOUT_MS = 120_000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Dynamic reader for ATTACH_RECONNECT_TIMEOUT_MS (issue #95 F4): env set after
|
|
19
|
+
* module load must still reach the reconnect policy, so read per evaluation.
|
|
20
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
export function resolveAttachReconnectTimeoutMs(env = process.env) {
|
|
24
|
+
return resolveTestMs(env, "AGENT_BOARD_TEST_ATTACH_RECONNECT_TIMEOUT_MS", ATTACH_RECONNECT_TIMEOUT_MS);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{ everConnected: boolean, disconnectedAt: number|null, connectStartedAt: number, now: number }} input
|
|
29
|
+
* @returns {{ giveUp: boolean, status: string | null }}
|
|
30
|
+
*/
|
|
31
|
+
export function evaluateAttachReconnect({ everConnected, disconnectedAt, connectStartedAt, now }) {
|
|
32
|
+
if (everConnected && disconnectedAt !== null) {
|
|
33
|
+
const elapsed = now - disconnectedAt;
|
|
34
|
+
return elapsed >= resolveAttachReconnectTimeoutMs()
|
|
35
|
+
? { giveUp: true, status: "host exited" }
|
|
36
|
+
: { giveUp: false, status: null };
|
|
37
|
+
}
|
|
38
|
+
const elapsed = now - connectStartedAt;
|
|
39
|
+
return elapsed >= ATTACH_HOST_START_TIMEOUT_MS
|
|
40
|
+
? { giveUp: true, status: "host not reachable" }
|
|
41
|
+
: { giveUp: false, status: null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detach-key policy (issue #91 Phase 6, spec §D1): while the socket is down a
|
|
46
|
+
* key can never reach the child, so treat ← as "leave the view"
|
|
47
|
+
* unconditionally; otherwise detach only when the pushed editorEmpty side
|
|
48
|
+
* channel is exactly true. false and null/undefined mean "forward" — the
|
|
49
|
+
* explicit conservative policy; callers pass `editorEmpty === true` so an
|
|
50
|
+
* unknown state can never arm the detach, and no terminal-buffer heuristic is
|
|
51
|
+
* consulted (the deleted terminal-buffer heuristic family — issues
|
|
52
|
+
* #42/#66/#69/#103 — stays deleted).
|
|
53
|
+
* @param {boolean} connected
|
|
54
|
+
* @param {boolean | null | undefined} editorEmpty
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function shouldEscapeAttach(connected, editorEmpty) {
|
|
58
|
+
// Normalize to a strict boolean: a null/undefined editorEmpty (unknown)
|
|
59
|
+
// forwards — never escapes — and must not leak through the return value.
|
|
60
|
+
return !connected || editorEmpty === true;
|
|
61
|
+
}
|