@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,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level store operations over the agent-board layout (see paths.mjs).
|
|
3
|
+
* Roster/meta/state/status read+write, row listing, and view creation/recovery.
|
|
4
|
+
* Used by the extension, the runner, and tests. Pure node, no Pi imports.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { atomicWriteJson, ensureDir, readJson } from "./atomic.mjs";
|
|
8
|
+
import { sameHostOwner } from "./host-coordination.mjs";
|
|
9
|
+
import { tryAcquireOwnedViewLock } from "./locks.mjs";
|
|
10
|
+
import * as P from "./paths.mjs";
|
|
11
|
+
import { currentProcessIdentity, isAlive } from "./pid.mjs";
|
|
12
|
+
import { readCodeRefs, summarizeCodeRefs } from "./code-refs-store.mjs";
|
|
13
|
+
import { appendDiagnostic, readDiagnosticSummary } from "./diagnostics.mjs";
|
|
14
|
+
import { readEvidence, summarizeEvidence } from "./evidence.mjs";
|
|
15
|
+
import { readFollowUpQueue, summarizeFollowUpQueue } from "./follow-up-queue.mjs";
|
|
16
|
+
import { readSteering, summarizeSteering } from "./steering.mjs";
|
|
17
|
+
|
|
18
|
+
/** @typedef {import("./types.mjs").Roster} Roster */
|
|
19
|
+
/** @typedef {import("./types.mjs").ViewMeta} ViewMeta */
|
|
20
|
+
/** @typedef {import("./types.mjs").ViewState} ViewState */
|
|
21
|
+
/** @typedef {import("./types.mjs").RunStatus} RunStatus */
|
|
22
|
+
/** @typedef {import("./types.mjs").HostStatus} HostStatus */
|
|
23
|
+
/** @typedef {import("./types.mjs").LaunchPrefs} LaunchPrefs */
|
|
24
|
+
|
|
25
|
+
const META_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
// ---- roster ---------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** @param {string} root @returns {Roster} */
|
|
30
|
+
export function readRoster(root) {
|
|
31
|
+
return readJson(P.rosterPath(root), { version: 1, views: [] });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {string} root @param {Roster} roster */
|
|
35
|
+
export function writeRoster(root, roster) {
|
|
36
|
+
atomicWriteJson(P.rosterPath(root), roster);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string} root @param {string} viewId */
|
|
40
|
+
export function addToRoster(root, viewId) {
|
|
41
|
+
const roster = readRoster(root);
|
|
42
|
+
if (!roster.views.includes(viewId)) {
|
|
43
|
+
roster.views.push(viewId);
|
|
44
|
+
writeRoster(root, roster);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {string} root @param {string} viewId */
|
|
49
|
+
export function removeFromRoster(root, viewId) {
|
|
50
|
+
const roster = readRoster(root);
|
|
51
|
+
const next = roster.views.filter((v) => v !== viewId);
|
|
52
|
+
if (next.length !== roster.views.length) {
|
|
53
|
+
roster.version = roster.version ?? 1;
|
|
54
|
+
writeRoster(root, { version: roster.version, views: next });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {string} root @returns {LaunchPrefs} */
|
|
59
|
+
export function readLaunchPrefs(root) {
|
|
60
|
+
return readJson(P.launchPrefsPath(root), {
|
|
61
|
+
version: 1,
|
|
62
|
+
cwd: null,
|
|
63
|
+
model: null,
|
|
64
|
+
thinkingLevel: null,
|
|
65
|
+
screenLogRetentionDays: null,
|
|
66
|
+
screenLogMaxSize: null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {string} root @param {Partial<LaunchPrefs>} prefs */
|
|
71
|
+
export function writeLaunchPrefs(root, prefs) {
|
|
72
|
+
atomicWriteJson(P.launchPrefsPath(root), {
|
|
73
|
+
version: 1,
|
|
74
|
+
cwd: prefs.cwd ?? null,
|
|
75
|
+
model: prefs.model ?? null,
|
|
76
|
+
thinkingLevel: prefs.thinkingLevel ?? null,
|
|
77
|
+
screenLogRetentionDays: prefs.screenLogRetentionDays ?? null,
|
|
78
|
+
screenLogMaxSize: prefs.screenLogMaxSize ?? null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- meta / state / status ------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/** @param {string} root @param {string} viewId @returns {ViewMeta|null} */
|
|
85
|
+
export function readMeta(root, viewId) {
|
|
86
|
+
return readJson(P.metaPath(root, viewId), /** @type {ViewMeta|null} */ (null));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {string} root @param {ViewMeta} meta */
|
|
90
|
+
export function writeMeta(root, meta) {
|
|
91
|
+
meta.updatedAt = Date.now();
|
|
92
|
+
atomicWriteJson(P.metaPath(root, meta.id), meta);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {string} root @param {string} viewId @returns {ViewState|null} */
|
|
96
|
+
export function readState(root, viewId) {
|
|
97
|
+
return readJson(P.statePath(root, viewId), /** @type {ViewState|null} */ (null));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {string} root @param {ViewState} state */
|
|
101
|
+
export function writeState(root, state) {
|
|
102
|
+
atomicWriteJson(P.statePath(root, state.viewId), state);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {string} root @param {string} viewId @param {string} runId @returns {RunStatus|null} */
|
|
106
|
+
export function readStatus(root, viewId, runId) {
|
|
107
|
+
return readJson(P.statusPath(root, viewId, runId), /** @type {RunStatus|null} */ (null));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {string} root @param {RunStatus} status */
|
|
111
|
+
export function writeStatus(root, status) {
|
|
112
|
+
atomicWriteJson(P.statusPath(root, status.viewId, status.runId), status);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {string} root @param {string} viewId @returns {HostStatus|null} */
|
|
116
|
+
export function readHost(root, viewId) {
|
|
117
|
+
return readJson(P.hostPath(root, viewId), /** @type {HostStatus|null} */ (null));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @param {string} root @param {HostStatus} host */
|
|
121
|
+
export function writeHost(root, host) {
|
|
122
|
+
atomicWriteJson(P.hostPath(root, host.viewId), host);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} root
|
|
127
|
+
* @param {string} viewId
|
|
128
|
+
* @param {number|null} pid
|
|
129
|
+
* @param {{ instanceId?: string, identity?: object|null }} [extra] merged into the mirror record
|
|
130
|
+
*/
|
|
131
|
+
export function writeHostPid(root, viewId, pid, extra) {
|
|
132
|
+
atomicWriteJson(P.hostPidPath(root, viewId), { pid, at: Date.now(), ...(extra ?? {}) });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Host states that represent an unreplaced claim: no replacement may be
|
|
137
|
+
* started while a host record is in one of these states (issue #70).
|
|
138
|
+
* @param {HostStatus|null} host
|
|
139
|
+
*/
|
|
140
|
+
function hostClaimActive(host) {
|
|
141
|
+
return Boolean(host && (host.state === "starting" || host.state === "alive" || host.state === "stopping"));
|
|
142
|
+
}
|
|
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
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Atomically create the provisional `starting` host record for a new instance.
|
|
179
|
+
* The ONLY entry point allowed to move "no claim / reclaimable terminal state"
|
|
180
|
+
* into `starting` (issue #70). Refuses when an active claim exists or the
|
|
181
|
+
* host-meta lease is contended; the fresh record always nulls runner/child/
|
|
182
|
+
* ready/stop fields so no stale owner data survives the handover.
|
|
183
|
+
* @param {string} root
|
|
184
|
+
* @param {Partial<HostStatus> & { viewId: string, instanceId: string }} provisionalHost
|
|
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.
|
|
188
|
+
* @returns {{ claimed: boolean, host: HostStatus|null }}
|
|
189
|
+
*/
|
|
190
|
+
export function claimHost(root, provisionalHost, opts = {}) {
|
|
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);
|
|
202
|
+
try {
|
|
203
|
+
const existing = readHost(root, provisionalHost.viewId);
|
|
204
|
+
if (hostClaimActive(existing)) return { claimed: false, host: existing };
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
/** @type {HostStatus} */
|
|
207
|
+
const record = {
|
|
208
|
+
version: 1,
|
|
209
|
+
viewId: provisionalHost.viewId,
|
|
210
|
+
mode: "pty",
|
|
211
|
+
instanceId: provisionalHost.instanceId,
|
|
212
|
+
configPath: provisionalHost.configPath ?? null,
|
|
213
|
+
socketPath: provisionalHost.socketPath ?? null,
|
|
214
|
+
claimAt: provisionalHost.claimAt ?? now,
|
|
215
|
+
claimPid: provisionalHost.claimPid ?? null,
|
|
216
|
+
claimIdentity: provisionalHost.claimIdentity ?? null,
|
|
217
|
+
// A fresh claim owns nothing yet: force-clear every field a superseded
|
|
218
|
+
// instance might have left behind.
|
|
219
|
+
runnerPid: null,
|
|
220
|
+
runnerIdentity: null,
|
|
221
|
+
runnerSpawnedAt: null,
|
|
222
|
+
childPid: null,
|
|
223
|
+
childIdentity: null,
|
|
224
|
+
childSpawnedAt: null,
|
|
225
|
+
readyAt: null,
|
|
226
|
+
stopRequestedAt: null,
|
|
227
|
+
revokeToken: null,
|
|
228
|
+
stopReason: null,
|
|
229
|
+
state: "starting",
|
|
230
|
+
startedAt: provisionalHost.claimAt ?? now,
|
|
231
|
+
lastSeenAt: now,
|
|
232
|
+
endedAt: null,
|
|
233
|
+
exitCode: null,
|
|
234
|
+
error: null,
|
|
235
|
+
cols: provisionalHost.cols ?? 120,
|
|
236
|
+
rows: provisionalHost.rows ?? 36,
|
|
237
|
+
attachedClients: 0,
|
|
238
|
+
};
|
|
239
|
+
writeHost(root, record);
|
|
240
|
+
// Mirror is best-effort; host.json is the authority for the new protocol.
|
|
241
|
+
try {
|
|
242
|
+
writeHostPid(root, provisionalHost.viewId, record.claimPid, {
|
|
243
|
+
instanceId: record.instanceId,
|
|
244
|
+
identity: record.claimIdentity,
|
|
245
|
+
});
|
|
246
|
+
} catch { /* best effort */ }
|
|
247
|
+
return { claimed: true, host: record };
|
|
248
|
+
} finally {
|
|
249
|
+
lock.lease.release();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Bounded contention retry for owner-fenced host writes (issue #70, PR #84 CI
|
|
255
|
+
* wave 2). Heartbeat/client-merge writes hold the host-meta lease for only a
|
|
256
|
+
* few milliseconds, but a one-shot acquire can land inside that window and
|
|
257
|
+
* return busy — a revoke or recovery write that silently no-ops is a real
|
|
258
|
+
* reliability bug, not just a test race. Both `busy` (live owner) and
|
|
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.
|
|
264
|
+
*/
|
|
265
|
+
const UPDATE_LOCK_BUSY_ATTEMPTS = 3;
|
|
266
|
+
const UPDATE_LOCK_BUSY_SLEEP_MS = 20;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Owner-fenced compare-and-write for host.json. Re-reads the view's record
|
|
270
|
+
* under the host-meta lease and only applies `mutate` while it still belongs
|
|
271
|
+
* to `expectedInstanceId`; a superseded instance's late heartbeat/crash/exit
|
|
272
|
+
* writes are rejected with `ownerChanged: true` and the current host (issue
|
|
273
|
+
* #70). Lease contention that survives the bounded retry returns not-updated
|
|
274
|
+
* with `ownerChanged: false` — callers treat that as retryable (heartbeats
|
|
275
|
+
* simply retry next tick; revokes/recovery re-attempt).
|
|
276
|
+
* @param {string} root
|
|
277
|
+
* @param {string} viewId
|
|
278
|
+
* @param {string} expectedInstanceId
|
|
279
|
+
* @param {(host: HostStatus) => HostStatus} mutate returns a new record
|
|
280
|
+
* @param {{ heldStartLease?: unknown, lockImpl?: typeof tryAcquireOwnedViewLock }} [opts]
|
|
281
|
+
* heldStartLease is reserved for host-start lease nesting; lockImpl injects
|
|
282
|
+
* the host-meta acquisition for deterministic contention tests.
|
|
283
|
+
* @returns {{ updated: boolean, ownerChanged: boolean, host: HostStatus|null }}
|
|
284
|
+
*/
|
|
285
|
+
export function updateOwnedHost(root, viewId, expectedInstanceId, mutate, opts = {}) {
|
|
286
|
+
const acquireHostMeta = opts.lockImpl ?? tryAcquireOwnedViewLock;
|
|
287
|
+
let lock;
|
|
288
|
+
let lastReason = null;
|
|
289
|
+
for (let attempt = 0; ; attempt++) {
|
|
290
|
+
lock = acquireHostMeta(root, viewId, "host-meta", { identity: hostMetaIdentity() });
|
|
291
|
+
if (lock.acquired) break;
|
|
292
|
+
lastReason = lock.reason;
|
|
293
|
+
// busy and blocked are both millisecond-scale holds for host-meta;
|
|
294
|
+
// neither is ownership information — only the fenced read below is.
|
|
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 });
|
|
297
|
+
return { updated: false, ownerChanged: false, host: null };
|
|
298
|
+
}
|
|
299
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, UPDATE_LOCK_BUSY_SLEEP_MS);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
hostMetaContentionReported.delete(viewId);
|
|
303
|
+
const host = readHost(root, viewId);
|
|
304
|
+
if (!host || !sameHostOwner(host, expectedInstanceId)) {
|
|
305
|
+
return { updated: false, ownerChanged: true, host };
|
|
306
|
+
}
|
|
307
|
+
const next = mutate(host);
|
|
308
|
+
// The mutate result must still belong to the expected instance: a buggy or
|
|
309
|
+
// hijacked callback that swaps the owner token is never written (issue #70).
|
|
310
|
+
if (!sameHostOwner(next, expectedInstanceId)) {
|
|
311
|
+
return { updated: false, ownerChanged: true, host };
|
|
312
|
+
}
|
|
313
|
+
writeHost(root, next);
|
|
314
|
+
try {
|
|
315
|
+
writeHostPid(root, viewId, next.runnerPid ?? null, {
|
|
316
|
+
instanceId: next.instanceId,
|
|
317
|
+
identity: next.runnerIdentity ?? null,
|
|
318
|
+
});
|
|
319
|
+
} catch { /* best effort */ }
|
|
320
|
+
return { updated: true, ownerChanged: false, host: next };
|
|
321
|
+
} finally {
|
|
322
|
+
lock.lease.release();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** @param {string} root @param {string} viewId @returns {number|null} */
|
|
327
|
+
export function readHostPid(root, viewId) {
|
|
328
|
+
return readJson(P.hostPidPath(root, viewId), { pid: null }).pid ?? null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** @param {string} root @param {string} viewId @param {string} runId @param {number|null} pid */
|
|
332
|
+
export function writePid(root, viewId, runId, pid) {
|
|
333
|
+
atomicWriteJson(P.pidPath(root, viewId, runId), { pid, at: Date.now() });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** @param {string} root @param {string} viewId @param {string} runId @returns {number|null} */
|
|
337
|
+
export function readPid(root, viewId, runId) {
|
|
338
|
+
return readJson(P.pidPath(root, viewId, runId), { pid: null }).pid ?? null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---- listing / loading ----------------------------------------------------
|
|
342
|
+
|
|
343
|
+
/** @param {string} root @param {string} viewId @returns {string[]} run ids, newest first by mtime. */
|
|
344
|
+
export function listRunIds(root, viewId) {
|
|
345
|
+
const dir = P.runsDir(root, viewId);
|
|
346
|
+
if (!existsSync(dir)) return [];
|
|
347
|
+
try {
|
|
348
|
+
return readdirSync(dir)
|
|
349
|
+
.filter((name) => {
|
|
350
|
+
try {
|
|
351
|
+
return statSync(P.runDir(root, viewId, name)).isDirectory();
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
.sort((a, b) => mtime(P.runDir(root, viewId, b)) - mtime(P.runDir(root, viewId, a)));
|
|
357
|
+
} catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** @param {string} dir */
|
|
363
|
+
function mtime(dir) {
|
|
364
|
+
try {
|
|
365
|
+
return statSync(dir).mtimeMs;
|
|
366
|
+
} catch {
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A merged dashboard row: meta + derived state. `state` may be null for a never-run row.
|
|
373
|
+
* @typedef {Object} Row
|
|
374
|
+
* @property {ViewMeta} meta
|
|
375
|
+
* @property {ViewState|null} state
|
|
376
|
+
* @property {boolean} alive Whether the row's current run pid/foreground activity is alive.
|
|
377
|
+
* @property {boolean} hostAlive Whether a PTY host/socket is alive and attachable.
|
|
378
|
+
* @property {boolean} hostActive Whether an unreplaced host claim exists (starting/alive/stopping).
|
|
379
|
+
* @property {boolean} hostReady Whether the host claims readiness (alive + readyAt); metadata hint only.
|
|
380
|
+
* @property {HostStatus|null} host
|
|
381
|
+
* @property {import("./types.mjs").ReviewSummary} [review]
|
|
382
|
+
* @property {import("./types.mjs").DiagnosticSummary} [diagnostics]
|
|
383
|
+
* @property {import("./types.mjs").FollowUpSummary} [followUps]
|
|
384
|
+
* @property {import("./types.mjs").SteeringSummary} [steering]
|
|
385
|
+
* @property {import("./types.mjs").CodeRefsSummary} [codeRefs]
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
/** @param {string} root @param {string} viewId @returns {Row|null} */
|
|
389
|
+
export function loadRow(root, viewId) {
|
|
390
|
+
const meta = readMeta(root, viewId);
|
|
391
|
+
if (!meta) return null;
|
|
392
|
+
const state = readState(root, viewId);
|
|
393
|
+
const summaries = readViewArtifactSummaries(root, viewId);
|
|
394
|
+
const enrichedState = state ? { ...state, ...summaries } : state;
|
|
395
|
+
let alive = false;
|
|
396
|
+
// True when a detached runner pid record exists for the current run. The
|
|
397
|
+
// processState mirror below exists for the foreground path, which has no
|
|
398
|
+
// runner pid to poll — it must never override a RECORDED pid: an
|
|
399
|
+
// un-finalized dead runner is the row's truth, and trusting the mirror
|
|
400
|
+
// here makes reconcile() skip the row forever (issue #153, A10).
|
|
401
|
+
let runnerPidRecorded = false;
|
|
402
|
+
if (enrichedState?.currentRunId) {
|
|
403
|
+
const pid = readPid(root, viewId, enrichedState.currentRunId);
|
|
404
|
+
runnerPidRecorded = pid != null;
|
|
405
|
+
alive = isAlive(pid);
|
|
406
|
+
}
|
|
407
|
+
// A managed session can also be active in the foreground after the user attaches
|
|
408
|
+
// and types a follow-up. In that path there is no detached runner pid for us to
|
|
409
|
+
// poll, but foreground extension events mirror processState into state.json.
|
|
410
|
+
if (!alive && !runnerPidRecorded && enrichedState?.processState === "alive") alive = true;
|
|
411
|
+
const host = readHost(root, viewId);
|
|
412
|
+
// New-protocol records carry an explicit runnerPid (null until spawn
|
|
413
|
+
// confirms); only legacy records without the property fall back to the
|
|
414
|
+
// host-pid mirror (issue #70).
|
|
415
|
+
const hostPid = host && Object.hasOwn(host, "runnerPid") ? host.runnerPid : readHostPid(root, viewId);
|
|
416
|
+
const hostAlive = Boolean(host && (host.state === "alive" || host.state === "starting") && isAlive(hostPid));
|
|
417
|
+
const hostActive = Boolean(host && (host.state === "starting" || host.state === "alive" || host.state === "stopping"));
|
|
418
|
+
const hostReady = Boolean(hostActive && host.state === "alive" && host.readyAt != null);
|
|
419
|
+
return { meta, state: enrichedState, alive, hostAlive, hostActive, hostReady, host, ...summaries };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** @param {string} root @param {string} viewId */
|
|
423
|
+
export function readViewArtifactSummaries(root, viewId) {
|
|
424
|
+
const review = summarizeEvidence(readEvidence(root, viewId));
|
|
425
|
+
const diagnostics = readDiagnosticSummary(root, viewId);
|
|
426
|
+
const followUps = summarizeFollowUpQueue(readFollowUpQueue(root, viewId));
|
|
427
|
+
const steering = summarizeSteering(readSteering(root, viewId));
|
|
428
|
+
const codeRefs = summarizeCodeRefs(readCodeRefs(root, viewId));
|
|
429
|
+
return { review, diagnostics, followUps, steering, codeRefs };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Load every non-archived row referenced by the roster.
|
|
434
|
+
* @param {string} root
|
|
435
|
+
* @param {{ includeArchived?: boolean }} [opts]
|
|
436
|
+
* @returns {Row[]}
|
|
437
|
+
*/
|
|
438
|
+
export function listRows(root, opts = {}) {
|
|
439
|
+
const roster = readRoster(root);
|
|
440
|
+
/** @type {Row[]} */
|
|
441
|
+
const rows = [];
|
|
442
|
+
for (const viewId of roster.views) {
|
|
443
|
+
// Archived short-circuit: archived rows are invisible on the dashboard, so
|
|
444
|
+
// never pay for their artifact files (state/evidence/host/diagnostics...).
|
|
445
|
+
// meta.json is the single authoritative source of the archived flag.
|
|
446
|
+
const meta = readMeta(root, viewId);
|
|
447
|
+
if (!meta) continue;
|
|
448
|
+
if (meta.archived && !opts.includeArchived) continue;
|
|
449
|
+
const row = loadRow(root, viewId);
|
|
450
|
+
if (!row) continue;
|
|
451
|
+
if (row.meta.archived && !opts.includeArchived) continue;
|
|
452
|
+
rows.push(row);
|
|
453
|
+
}
|
|
454
|
+
return rows;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---- creation -------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create a new managed view (row) and persist meta + roster + an initial queued state.
|
|
461
|
+
* Does not launch anything — the caller launches a run afterward.
|
|
462
|
+
* @param {string} root
|
|
463
|
+
* @param {{
|
|
464
|
+
* id: string, name: string, cwd: string, repoCwd?: string, repoRoot?: string|null,
|
|
465
|
+
* worktreeMode?: import("./types.mjs").WorktreeMode, worktreePath?: string|null,
|
|
466
|
+
* defaultModel?: string|null,
|
|
467
|
+
* defaultThinking?: "off"|"minimal"|"low"|"medium"|"high"|"xhigh"|null,
|
|
468
|
+
* writeCapable?: boolean,
|
|
469
|
+
* sessionFile?: string,
|
|
470
|
+
* }} opts
|
|
471
|
+
* @returns {ViewMeta}
|
|
472
|
+
*/
|
|
473
|
+
export function createView(root, opts) {
|
|
474
|
+
ensureDir(P.sessionsDir(root));
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
/** @type {ViewMeta} */
|
|
477
|
+
const meta = {
|
|
478
|
+
version: META_VERSION,
|
|
479
|
+
id: opts.id,
|
|
480
|
+
name: opts.name,
|
|
481
|
+
cwd: opts.cwd,
|
|
482
|
+
repoCwd: opts.repoCwd ?? opts.cwd,
|
|
483
|
+
repoRoot: opts.repoRoot ?? null,
|
|
484
|
+
sessionFile: opts.sessionFile ?? P.sessionFilePath(root, opts.id),
|
|
485
|
+
createdAt: now,
|
|
486
|
+
updatedAt: now,
|
|
487
|
+
pinned: false,
|
|
488
|
+
kind: "pi-session",
|
|
489
|
+
defaultModel: opts.defaultModel ?? null,
|
|
490
|
+
defaultThinking: opts.defaultThinking ?? null,
|
|
491
|
+
worktreeMode: opts.worktreeMode ?? "off",
|
|
492
|
+
worktreePath: opts.worktreePath ?? null,
|
|
493
|
+
writeCapable: opts.writeCapable ?? true,
|
|
494
|
+
archived: false,
|
|
495
|
+
source: "agent-board",
|
|
496
|
+
};
|
|
497
|
+
ensureDir(P.viewDir(root, meta.id));
|
|
498
|
+
writeMeta(root, meta);
|
|
499
|
+
/** @type {ViewState} */
|
|
500
|
+
const state = {
|
|
501
|
+
version: 1,
|
|
502
|
+
viewId: meta.id,
|
|
503
|
+
currentRunId: null,
|
|
504
|
+
semanticState: "queued",
|
|
505
|
+
processState: "exited",
|
|
506
|
+
summary: "Queued",
|
|
507
|
+
lastActivityAt: now,
|
|
508
|
+
updatedAt: now,
|
|
509
|
+
needsInput: false,
|
|
510
|
+
hasError: false,
|
|
511
|
+
latestAssistantPreview: "",
|
|
512
|
+
latestTool: null,
|
|
513
|
+
question: null,
|
|
514
|
+
pendingQuestions: [],
|
|
515
|
+
error: null,
|
|
516
|
+
lastVisitedAt: null,
|
|
517
|
+
lastAgentActivityAt: null,
|
|
518
|
+
autoState: null,
|
|
519
|
+
};
|
|
520
|
+
writeState(root, state);
|
|
521
|
+
addToRoster(root, meta.id);
|
|
522
|
+
return meta;
|
|
523
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl+Z from the board or an attached session. Pi only handles it at its own prompt. Sent on to a
|
|
3
|
+
* session it would stop that session's screen for good: nothing there can be stopped or resumed,
|
|
4
|
+
* since a session has no shell. Stops only this Pi, as sessions run in their own process groups.
|
|
5
|
+
*
|
|
6
|
+
* The same steps as Pi's own, which an extension can't call. On Windows `app.suspend` has no key,
|
|
7
|
+
* so this isn't reached.
|
|
8
|
+
*
|
|
9
|
+
* @param {{ start(): void, stop(): void, requestRender(force?: boolean): void }} tui
|
|
10
|
+
* @param {{ beforeStop?: () => void, afterResume?: () => void }} [hooks]
|
|
11
|
+
*/
|
|
12
|
+
export function suspendToShell(tui, hooks = {}) {
|
|
13
|
+
// Without a handle holding the event loop open, Node could exit on `fg` before SIGCONT arrives.
|
|
14
|
+
const keepAlive = setInterval(() => {}, 2 ** 30);
|
|
15
|
+
// Ctrl+C at the shell mustn't kill what's in the background.
|
|
16
|
+
const ignoreSigint = () => {};
|
|
17
|
+
process.on("SIGINT", ignoreSigint);
|
|
18
|
+
const finish = () => {
|
|
19
|
+
clearInterval(keepAlive);
|
|
20
|
+
process.removeListener("SIGINT", ignoreSigint);
|
|
21
|
+
};
|
|
22
|
+
process.once("SIGCONT", () => {
|
|
23
|
+
finish();
|
|
24
|
+
tui.start();
|
|
25
|
+
hooks.afterResume?.();
|
|
26
|
+
tui.requestRender(true);
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
hooks.beforeStop?.();
|
|
30
|
+
tui.stop();
|
|
31
|
+
process.kill(0, "SIGTSTP");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
finish();
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|