@reticlehq/server 3.3.0 → 3.5.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/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* anybody could lose. When it does shut down, the MCP proxy reconnects and `ensureDaemon` spawns a
|
|
20
20
|
* fresh one on the next call, which is a path already exercised hundreds of times a day.
|
|
21
21
|
*/
|
|
22
|
+
import { STATUS_HOLD_MS } from '@reticlehq/core';
|
|
22
23
|
/** Set the first time any tool is dispatched through runTool — the chokepoint every path shares. */
|
|
23
24
|
let servedToolCall = false;
|
|
24
25
|
export function noteToolCall() {
|
|
@@ -27,9 +28,30 @@ export function noteToolCall() {
|
|
|
27
28
|
export function everServedToolCall() {
|
|
28
29
|
return servedToolCall;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* How long one hold from a setup run keeps the daemon from idling out. `init` renews it on a timer
|
|
33
|
+
* well inside this window for as long as it runs, so a run that dies stops holding within it.
|
|
34
|
+
*/
|
|
35
|
+
export const SETUP_HOLD_MS = STATUS_HOLD_MS;
|
|
36
|
+
/**
|
|
37
|
+
* When a setup run last said it is waiting on this daemon — see STATUS_HOLD_QUERY.
|
|
38
|
+
*
|
|
39
|
+
* `init` starts the daemon and can then wait on the app longer than the idle grace: a cold desktop
|
|
40
|
+
* build, a slow first compile. The daemon counted none of that as activity, idled out
|
|
41
|
+
* (`reticle_daemon_idle_exit`) while init was still waiting for the app to dial it, and the app came
|
|
42
|
+
* up to a closed port.
|
|
43
|
+
*/
|
|
44
|
+
let setupHeldAt;
|
|
45
|
+
export function noteSetupHold(now) {
|
|
46
|
+
setupHeldAt = now;
|
|
47
|
+
}
|
|
48
|
+
function setupHolding(now) {
|
|
49
|
+
return undefined !== setupHeldAt && now - setupHeldAt < SETUP_HOLD_MS;
|
|
50
|
+
}
|
|
30
51
|
/** Tests only. */
|
|
31
52
|
export function resetDaemonUsefulness() {
|
|
32
53
|
servedToolCall = false;
|
|
54
|
+
setupHeldAt = undefined;
|
|
33
55
|
}
|
|
34
56
|
/**
|
|
35
57
|
* True when the daemon has done nothing for anyone and has nothing to lose by exiting.
|
|
@@ -49,13 +71,14 @@ export function isUselessDaemon(facts) {
|
|
|
49
71
|
* clause the shutdown watcher could never fire on the many daemons that spawn in a directory with
|
|
50
72
|
* no app and sit there for the length of an editor session.
|
|
51
73
|
*/
|
|
52
|
-
export function buildIdlePredicate(agentConnected, sessions, pool) {
|
|
53
|
-
return () =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
export function buildIdlePredicate(agentConnected, sessions, pool, now = () => Date.now()) {
|
|
75
|
+
return () => !setupHolding(now()) &&
|
|
76
|
+
(!agentConnected() ||
|
|
77
|
+
isUselessDaemon({
|
|
78
|
+
servedToolCall: everServedToolCall(),
|
|
79
|
+
everConnected: sessions.everConnected(),
|
|
80
|
+
activeLeases: pool.activeCount(),
|
|
81
|
+
})) &&
|
|
59
82
|
0 === sessions.count() &&
|
|
60
83
|
0 === pool.activeCount();
|
|
61
84
|
}
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ## Why `fixed-pending-release` is an accepted answer
|
|
10
10
|
*
|
|
11
|
-
* Work
|
|
12
|
-
* ships. The label is
|
|
13
|
-
*
|
|
14
|
-
* when they close things, only to say so.
|
|
11
|
+
* Work that is already ON the release branch is legitimately still open; the issue closes when the
|
|
12
|
+
* version ships. The label is that fact. It is not a fact about an open pull request. Applying it
|
|
13
|
+
* there is what told contributors a fix had shipped when the pull request was still unmerged.
|
|
15
14
|
*
|
|
16
15
|
* Pure. The git log and the GitHub lookup belong to the caller, which is what makes every branch
|
|
17
16
|
* below testable without a network or a repo.
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ## Why `fixed-pending-release` is an accepted answer
|
|
10
10
|
*
|
|
11
|
-
* Work
|
|
12
|
-
* ships. The label is
|
|
13
|
-
*
|
|
14
|
-
* when they close things, only to say so.
|
|
11
|
+
* Work that is already ON the release branch is legitimately still open; the issue closes when the
|
|
12
|
+
* version ships. The label is that fact. It is not a fact about an open pull request. Applying it
|
|
13
|
+
* there is what told contributors a fix had shipped when the pull request was still unmerged.
|
|
15
14
|
*
|
|
16
15
|
* Pure. The git log and the GitHub lookup belong to the caller, which is what makes every branch
|
|
17
16
|
* below testable without a network or a repo.
|
|
@@ -61,9 +60,10 @@ export function staleIssues(claimed, issues) {
|
|
|
61
60
|
/** The message the guard prints. Held here so the check and its tests cannot describe it differently. */
|
|
62
61
|
export function staleIssueReport(stale) {
|
|
63
62
|
const list = stale.map((n) => `#${String(n)}`).join(', ');
|
|
63
|
+
const pronoun = 1 === stale.length ? 'it' : 'them';
|
|
64
64
|
return (`${list} ${1 === stale.length ? 'is' : 'are'} still OPEN, and a commit on this branch says it ` +
|
|
65
|
-
`closes ${
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
65
|
+
`closes ${pronoun}. Anyone browsing the tracker reads that as work nobody has started. Close ` +
|
|
66
|
+
`${pronoun} when this merges. Do not apply \`${PENDING_RELEASE_LABEL}\` from an open pull ` +
|
|
67
|
+
`request: that label means the fix is already on the release branch and waiting for the version ` +
|
|
68
|
+
`to ship. Applying it earlier is what sent contributors away from work that was not merged.`);
|
|
69
69
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect proof for a run with no system browser to lean on: `init --no-open`, CI, a container,
|
|
3
|
+
* an agent, or a launcher that failed.
|
|
4
|
+
*
|
|
5
|
+
* Opens the app in a lease on the daemon at `port`, over the MCP transport every daemon serves --
|
|
6
|
+
* the same route `reticle verify` takes, so there is no second browser stack and no second daemon
|
|
7
|
+
* fighting this one for the port.
|
|
8
|
+
*/
|
|
9
|
+
/** A lease this run opened, and the way to hand it back; or why it could not be opened. */
|
|
10
|
+
type LeaseOutcome = {
|
|
11
|
+
readonly sessionId: string;
|
|
12
|
+
readonly release: () => Promise<void>;
|
|
13
|
+
} | {
|
|
14
|
+
readonly failed: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function openLeaseFor(port: number, url: string, token: string | undefined): Promise<LeaseOutcome>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connect proof for a run with no system browser to lean on: `init --no-open`, CI, a container,
|
|
3
|
+
* an agent, or a launcher that failed.
|
|
4
|
+
*
|
|
5
|
+
* Opens the app in a lease on the daemon at `port`, over the MCP transport every daemon serves --
|
|
6
|
+
* the same route `reticle verify` takes, so there is no second browser stack and no second daemon
|
|
7
|
+
* fighting this one for the port.
|
|
8
|
+
*/
|
|
9
|
+
import { acquireLease, connectOverSse, endpointFor, releaseLease, } from '../../cli/adhoc-verdict.js';
|
|
10
|
+
const reason = (error) => (error instanceof Error ? error.message : String(error));
|
|
11
|
+
export async function openLeaseFor(port, url, token) {
|
|
12
|
+
let caller;
|
|
13
|
+
try {
|
|
14
|
+
caller = await connectOverSse(endpointFor(port, token));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return { failed: `could not reach the daemon on port ${String(port)}: ${reason(error)}` };
|
|
18
|
+
}
|
|
19
|
+
const close = () => caller.close().catch(() => undefined);
|
|
20
|
+
try {
|
|
21
|
+
const got = await acquireLease(caller, url);
|
|
22
|
+
if ('failed' in got) {
|
|
23
|
+
await close();
|
|
24
|
+
return { failed: got.failed.join(' ') };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
sessionId: got.leased,
|
|
28
|
+
release: async () => {
|
|
29
|
+
await releaseLease(caller, got.leased);
|
|
30
|
+
await close();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
await close();
|
|
36
|
+
return { failed: reason(error) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `init` leaves in the state home about a dev server it handed over: the log, its one rotated
|
|
3
|
+
* predecessor, and a record of where the server is.
|
|
4
|
+
*
|
|
5
|
+
* All keyed on the app directory, beside the daemon logs and so outside the project: no bundler
|
|
6
|
+
* watcher ever sees them change (a `.reticle/` write inside the app once put Vite into a continuous
|
|
7
|
+
* full-reload loop), and two projects never share a file.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
export declare function devServerLogName(cwd: string): string;
|
|
11
|
+
/** Where a handed-over server is: enough to find it again and to prove it is still that server. */
|
|
12
|
+
declare const HandedOverSchema: z.ZodObject<{
|
|
13
|
+
pid: z.ZodNumber;
|
|
14
|
+
url: z.ZodString;
|
|
15
|
+
appDir: z.ZodString;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
url: string;
|
|
18
|
+
pid: number;
|
|
19
|
+
appDir: string;
|
|
20
|
+
}, {
|
|
21
|
+
url: string;
|
|
22
|
+
pid: number;
|
|
23
|
+
appDir: string;
|
|
24
|
+
}>;
|
|
25
|
+
export type HandedOver = z.infer<typeof HandedOverSchema>;
|
|
26
|
+
/**
|
|
27
|
+
* Written when `init` leaves a server running, so the next `init` can attach to it.
|
|
28
|
+
*
|
|
29
|
+
* Only Vite announces itself (through its plugin). Re-running init while a Next or Angular server
|
|
30
|
+
* it had started was still up spawned a second `dev`, which failed with "port 3000 is already in use
|
|
31
|
+
* — something else is serving there" — the something being the server init itself had left.
|
|
32
|
+
*/
|
|
33
|
+
export declare function recordHandOver(dir: string, record: HandedOver): void;
|
|
34
|
+
/**
|
|
35
|
+
* The url of the server `init` handed over for `appDir`, when that process is alive AND the url
|
|
36
|
+
* still answers — both, because a recycled pid proves nothing and a url answering proves only that
|
|
37
|
+
* SOMETHING is there. Undefined otherwise, and the caller starts one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function handedOverUrl(dir: string, appDir: string, deps: {
|
|
40
|
+
alive: (pid: number) => boolean;
|
|
41
|
+
answers: (url: string) => Promise<boolean>;
|
|
42
|
+
}): Promise<string | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Stop the server `init` handed over for `appDir`, and forget it. True when one was stopped.
|
|
45
|
+
*
|
|
46
|
+
* For a port move: Next reads the daemon url into `NEXT_PUBLIC_RETICLE_URL` once, when `next dev`
|
|
47
|
+
* starts, so attaching to the running server after `init --port <new>` left the page dialling the
|
|
48
|
+
* old port — which the same run had just stopped. Only a server this record proves init started is
|
|
49
|
+
* touched; one the user started is theirs to restart.
|
|
50
|
+
*/
|
|
51
|
+
export declare function stopHandedOver(dir: string, appDir: string, deps: {
|
|
52
|
+
alive: (pid: number) => boolean;
|
|
53
|
+
kill: (pid: number) => void;
|
|
54
|
+
}): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Remove what servers that are gone left behind: their logs, rotated logs and records.
|
|
57
|
+
*
|
|
58
|
+
* Every run wrote a log per project and nothing ever deleted one, so the state home kept a file for
|
|
59
|
+
* every app `init` had ever started. A record whose pid is dead takes its logs with it; a log with no
|
|
60
|
+
* record at all goes once it is old. `keep` is the app about to start, whose files are replaced
|
|
61
|
+
* anyway.
|
|
62
|
+
*/
|
|
63
|
+
export declare function sweepDeadDevServers(dir: string, deps: {
|
|
64
|
+
alive: (pid: number) => boolean;
|
|
65
|
+
now: number;
|
|
66
|
+
keep?: string;
|
|
67
|
+
}): void;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `init` leaves in the state home about a dev server it handed over: the log, its one rotated
|
|
3
|
+
* predecessor, and a record of where the server is.
|
|
4
|
+
*
|
|
5
|
+
* All keyed on the app directory, beside the daemon logs and so outside the project: no bundler
|
|
6
|
+
* watcher ever sees them change (a `.reticle/` write inside the app once put Vite into a continuous
|
|
7
|
+
* full-reload loop), and two projects never share a file.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { ROTATED_SUFFIX } from './dev-server-log-cap.js';
|
|
14
|
+
const DEV_SERVER_PREFIX = 'dev-server-';
|
|
15
|
+
const LOG_SUFFIX = '.log';
|
|
16
|
+
const RECORD_SUFFIX = '.json';
|
|
17
|
+
/**
|
|
18
|
+
* A log with no record is from a build before records existed, so there is no pid to ask. Anything
|
|
19
|
+
* this old is from a server long gone; a live one that went this quiet loses a file it was not using.
|
|
20
|
+
*/
|
|
21
|
+
const UNRECORDED_LOG_MAX_AGE_MS = 24 * 60 * 60_000;
|
|
22
|
+
const OWN_FILE = /^dev-server-([0-9a-f]{12})\.(?:log|log\.1|json)$/;
|
|
23
|
+
function keyOf(cwd) {
|
|
24
|
+
return createHash('sha256').update(cwd).digest('hex').slice(0, 12);
|
|
25
|
+
}
|
|
26
|
+
export function devServerLogName(cwd) {
|
|
27
|
+
return `${DEV_SERVER_PREFIX}${keyOf(cwd)}${LOG_SUFFIX}`;
|
|
28
|
+
}
|
|
29
|
+
function recordName(cwd) {
|
|
30
|
+
return `${DEV_SERVER_PREFIX}${keyOf(cwd)}${RECORD_SUFFIX}`;
|
|
31
|
+
}
|
|
32
|
+
/** Where a handed-over server is: enough to find it again and to prove it is still that server. */
|
|
33
|
+
const HandedOverSchema = z.object({
|
|
34
|
+
pid: z.number().int().positive(),
|
|
35
|
+
url: z.string().min(1),
|
|
36
|
+
appDir: z.string().min(1),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Written when `init` leaves a server running, so the next `init` can attach to it.
|
|
40
|
+
*
|
|
41
|
+
* Only Vite announces itself (through its plugin). Re-running init while a Next or Angular server
|
|
42
|
+
* it had started was still up spawned a second `dev`, which failed with "port 3000 is already in use
|
|
43
|
+
* — something else is serving there" — the something being the server init itself had left.
|
|
44
|
+
*/
|
|
45
|
+
export function recordHandOver(dir, record) {
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(join(dir, recordName(record.appDir)), JSON.stringify(record));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Without the record the next run starts a server as it always did. Never fail a handover on it.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function readRecord(path) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = HandedOverSchema.safeParse(JSON.parse(readFileSync(path, 'utf8')));
|
|
56
|
+
return parsed.success ? parsed.data : undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The url of the server `init` handed over for `appDir`, when that process is alive AND the url
|
|
64
|
+
* still answers — both, because a recycled pid proves nothing and a url answering proves only that
|
|
65
|
+
* SOMETHING is there. Undefined otherwise, and the caller starts one.
|
|
66
|
+
*/
|
|
67
|
+
export async function handedOverUrl(dir, appDir, deps) {
|
|
68
|
+
const record = readRecord(join(dir, recordName(appDir)));
|
|
69
|
+
if (undefined === record || record.appDir !== appDir || !deps.alive(record.pid))
|
|
70
|
+
return undefined;
|
|
71
|
+
return (await deps.answers(record.url)) ? record.url : undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stop the server `init` handed over for `appDir`, and forget it. True when one was stopped.
|
|
75
|
+
*
|
|
76
|
+
* For a port move: Next reads the daemon url into `NEXT_PUBLIC_RETICLE_URL` once, when `next dev`
|
|
77
|
+
* starts, so attaching to the running server after `init --port <new>` left the page dialling the
|
|
78
|
+
* old port — which the same run had just stopped. Only a server this record proves init started is
|
|
79
|
+
* touched; one the user started is theirs to restart.
|
|
80
|
+
*/
|
|
81
|
+
export function stopHandedOver(dir, appDir, deps) {
|
|
82
|
+
const path = join(dir, recordName(appDir));
|
|
83
|
+
const record = readRecord(path);
|
|
84
|
+
if (undefined === record || record.appDir !== appDir || !deps.alive(record.pid))
|
|
85
|
+
return false;
|
|
86
|
+
deps.kill(record.pid);
|
|
87
|
+
rmSync(path, { force: true });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Remove what servers that are gone left behind: their logs, rotated logs and records.
|
|
92
|
+
*
|
|
93
|
+
* Every run wrote a log per project and nothing ever deleted one, so the state home kept a file for
|
|
94
|
+
* every app `init` had ever started. A record whose pid is dead takes its logs with it; a log with no
|
|
95
|
+
* record at all goes once it is old. `keep` is the app about to start, whose files are replaced
|
|
96
|
+
* anyway.
|
|
97
|
+
*/
|
|
98
|
+
export function sweepDeadDevServers(dir, deps) {
|
|
99
|
+
let files;
|
|
100
|
+
try {
|
|
101
|
+
files = readdirSync(dir);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const keepKey = undefined === deps.keep ? undefined : keyOf(deps.keep);
|
|
107
|
+
// Exactly the names this module writes, so nothing else in the state home can match.
|
|
108
|
+
const keys = new Set(files.map((f) => OWN_FILE.exec(f)?.[1]).filter((k) => undefined !== k));
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
if (key === keepKey)
|
|
111
|
+
continue;
|
|
112
|
+
const log = join(dir, `${DEV_SERVER_PREFIX}${key}${LOG_SUFFIX}`);
|
|
113
|
+
const recordPath = join(dir, `${DEV_SERVER_PREFIX}${key}${RECORD_SUFFIX}`);
|
|
114
|
+
const record = readRecord(recordPath);
|
|
115
|
+
const dead = undefined === record
|
|
116
|
+
? deps.now - lastWrittenAt(log) > UNRECORDED_LOG_MAX_AGE_MS
|
|
117
|
+
: !deps.alive(record.pid);
|
|
118
|
+
if (!dead)
|
|
119
|
+
continue;
|
|
120
|
+
for (const path of [log, `${log}${ROTATED_SUFFIX}`, recordPath])
|
|
121
|
+
rmSync(path, { force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function lastWrittenAt(path) {
|
|
125
|
+
try {
|
|
126
|
+
return statSync(path).mtimeMs;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server's log, with a ceiling — and the small process that enforces it.
|
|
3
|
+
*
|
|
4
|
+
* The handed-over dev server wrote straight into a file descriptor, which is what keeps it alive once
|
|
5
|
+
* `init` exits (a pipe whose reader has gone is an EPIPE on the next write). But a descriptor nobody
|
|
6
|
+
* reads has no ceiling: an upstream echo loop — TanStack devtools and Vite 8 forwarding each other's
|
|
7
|
+
* console lines — grew `~/.reticle/dev-server-<hash>.log` to 2.9 GB in about five minutes.
|
|
8
|
+
*
|
|
9
|
+
* So the server's output goes through a pipe again, and the reader on the other end is THIS script,
|
|
10
|
+
* run as its own process in the server's process group rather than inside `init`. It lives exactly
|
|
11
|
+
* as long as the server's output does: it exits when the pipes close, never before, so the EPIPE
|
|
12
|
+
* that killed handed-over servers cannot come back. It ignores the signals a group stop sends, for
|
|
13
|
+
* the same reason — the server gets them too, and may still be writing on its way out.
|
|
14
|
+
*
|
|
15
|
+
* The bound is rotation, not truncation: past the cap the file is renamed to `<log>.1` (replacing
|
|
16
|
+
* the previous one) and a fresh file started, so disk use stays under twice the cap while the most
|
|
17
|
+
* recent output — the part anybody reads — is always there.
|
|
18
|
+
*
|
|
19
|
+
* Why not truncate from the daemon on a timer: the daemon idles out on its own schedule and a
|
|
20
|
+
* handed-over server does not, so the one bound that matters would stop being enforced exactly when
|
|
21
|
+
* nobody is watching. A reader that shares the server's lifetime cannot be outlived by it.
|
|
22
|
+
*/
|
|
23
|
+
/** Per file. The log and its one rotated predecessor together never exceed twice this. */
|
|
24
|
+
export declare const DEV_SERVER_LOG_CAP_BYTES: number;
|
|
25
|
+
/** The single predecessor a rotation keeps. */
|
|
26
|
+
export declare const ROTATED_SUFFIX = ".1";
|
|
27
|
+
/** Appends to `path`, rotating it to `path.1` whenever the next write would pass `capBytes`. */
|
|
28
|
+
export declare class BoundedLog {
|
|
29
|
+
#private;
|
|
30
|
+
private readonly path;
|
|
31
|
+
private readonly capBytes;
|
|
32
|
+
constructor(path: string, capBytes?: number);
|
|
33
|
+
write(chunk: Buffer): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server's log, with a ceiling — and the small process that enforces it.
|
|
3
|
+
*
|
|
4
|
+
* The handed-over dev server wrote straight into a file descriptor, which is what keeps it alive once
|
|
5
|
+
* `init` exits (a pipe whose reader has gone is an EPIPE on the next write). But a descriptor nobody
|
|
6
|
+
* reads has no ceiling: an upstream echo loop — TanStack devtools and Vite 8 forwarding each other's
|
|
7
|
+
* console lines — grew `~/.reticle/dev-server-<hash>.log` to 2.9 GB in about five minutes.
|
|
8
|
+
*
|
|
9
|
+
* So the server's output goes through a pipe again, and the reader on the other end is THIS script,
|
|
10
|
+
* run as its own process in the server's process group rather than inside `init`. It lives exactly
|
|
11
|
+
* as long as the server's output does: it exits when the pipes close, never before, so the EPIPE
|
|
12
|
+
* that killed handed-over servers cannot come back. It ignores the signals a group stop sends, for
|
|
13
|
+
* the same reason — the server gets them too, and may still be writing on its way out.
|
|
14
|
+
*
|
|
15
|
+
* The bound is rotation, not truncation: past the cap the file is renamed to `<log>.1` (replacing
|
|
16
|
+
* the previous one) and a fresh file started, so disk use stays under twice the cap while the most
|
|
17
|
+
* recent output — the part anybody reads — is always there.
|
|
18
|
+
*
|
|
19
|
+
* Why not truncate from the daemon on a timer: the daemon idles out on its own schedule and a
|
|
20
|
+
* handed-over server does not, so the one bound that matters would stop being enforced exactly when
|
|
21
|
+
* nobody is watching. A reader that shares the server's lifetime cannot be outlived by it.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import { closeSync, openSync, renameSync, writeSync } from 'node:fs';
|
|
25
|
+
import { pathToFileURL } from 'node:url';
|
|
26
|
+
/** Per file. The log and its one rotated predecessor together never exceed twice this. */
|
|
27
|
+
export const DEV_SERVER_LOG_CAP_BYTES = 8 * 1024 * 1024;
|
|
28
|
+
const APPEND = 'a';
|
|
29
|
+
const TRUNCATE = 'w';
|
|
30
|
+
/** The single predecessor a rotation keeps. */
|
|
31
|
+
export const ROTATED_SUFFIX = '.1';
|
|
32
|
+
/** Appends to `path`, rotating it to `path.1` whenever the next write would pass `capBytes`. */
|
|
33
|
+
export class BoundedLog {
|
|
34
|
+
path;
|
|
35
|
+
capBytes;
|
|
36
|
+
/** Undefined only between a rotation's close and a reopen that failed; the next write retries. */
|
|
37
|
+
#fd;
|
|
38
|
+
#size = 0;
|
|
39
|
+
constructor(path, capBytes = DEV_SERVER_LOG_CAP_BYTES) {
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.capBytes = capBytes;
|
|
42
|
+
this.#fd = openSync(path, APPEND);
|
|
43
|
+
}
|
|
44
|
+
write(chunk) {
|
|
45
|
+
try {
|
|
46
|
+
if (this.#fd === undefined || (0 < this.#size && this.#size + chunk.length > this.capBytes)) {
|
|
47
|
+
this.#rotate();
|
|
48
|
+
}
|
|
49
|
+
const fd = this.#fd;
|
|
50
|
+
if (fd === undefined)
|
|
51
|
+
return;
|
|
52
|
+
// A single chunk bigger than the cap is kept only in its tail: it is still bounded.
|
|
53
|
+
const kept = chunk.length > this.capBytes ? chunk.subarray(chunk.length - this.capBytes) : chunk;
|
|
54
|
+
writeSync(fd, kept);
|
|
55
|
+
this.#size += kept.length;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Disk full, a removed state dir: the output is lost, the server is not. Never throw here.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
close() {
|
|
62
|
+
const fd = this.#fd;
|
|
63
|
+
this.#fd = undefined;
|
|
64
|
+
if (fd === undefined)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* already closed */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Close, rename to the predecessor, reopen. A rename that fails (the rotated name is taken, or on
|
|
75
|
+
* Windows something holds the file) truncates in place instead: the predecessor is lost, the bound
|
|
76
|
+
* and the newest output are not. It used to leave the descriptor closed, and every later write
|
|
77
|
+
* vanished inside `write`'s catch.
|
|
78
|
+
*/
|
|
79
|
+
#rotate() {
|
|
80
|
+
this.close();
|
|
81
|
+
let flags = APPEND;
|
|
82
|
+
try {
|
|
83
|
+
renameSync(this.path, `${this.path}${ROTATED_SUFFIX}`);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
flags = TRUNCATE;
|
|
87
|
+
}
|
|
88
|
+
this.#fd = openSync(this.path, flags);
|
|
89
|
+
this.#size = 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Arguments are `<log path> <cap bytes> <shell command>`, the way `OwnedDevServer` spawns this. */
|
|
93
|
+
function main(argv) {
|
|
94
|
+
const [logPath, cap, command] = argv;
|
|
95
|
+
if (undefined === logPath || undefined === command)
|
|
96
|
+
process.exit(2);
|
|
97
|
+
const log = new BoundedLog(logPath, Number(cap) || DEV_SERVER_LOG_CAP_BYTES);
|
|
98
|
+
// Stopping the group signals both of us. The server decides when it is done; this outlives it.
|
|
99
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
|
|
100
|
+
process.on(signal, () => { });
|
|
101
|
+
const child = spawn(command, {
|
|
102
|
+
shell: true,
|
|
103
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
+
env: process.env,
|
|
105
|
+
});
|
|
106
|
+
child.stdout.on('data', (chunk) => log.write(chunk));
|
|
107
|
+
child.stderr.on('data', (chunk) => log.write(chunk));
|
|
108
|
+
// `close`, not `exit`: every byte the server wrote is read before this process goes.
|
|
109
|
+
child.on('close', (code) => {
|
|
110
|
+
log.close();
|
|
111
|
+
process.exit(code ?? 1);
|
|
112
|
+
});
|
|
113
|
+
child.on('error', () => {
|
|
114
|
+
log.close();
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const entry = process.argv[1];
|
|
119
|
+
if (undefined !== entry && import.meta.url === pathToFileURL(entry).href) {
|
|
120
|
+
main(process.argv.slice(2));
|
|
121
|
+
}
|
|
@@ -25,6 +25,8 @@ export interface WaitFacts {
|
|
|
25
25
|
readonly quietForMs: number;
|
|
26
26
|
/** Milliseconds since the wait began. */
|
|
27
27
|
readonly elapsedMs: number;
|
|
28
|
+
/** Explicit caller limit; otherwise a busy build keeps the default ceiling. */
|
|
29
|
+
readonly budgetMs?: number | undefined;
|
|
28
30
|
/**
|
|
29
31
|
* How long silence may mean "still starting" on THIS machine. Defaults to QUIET_MEANS_HUNG_MS.
|
|
30
32
|
*
|
|
@@ -38,7 +38,7 @@ export function judgeWait(facts) {
|
|
|
38
38
|
return WaitVerdict.READY;
|
|
39
39
|
if (facts.launcherExited)
|
|
40
40
|
return WaitVerdict.DEAD;
|
|
41
|
-
if (facts.elapsedMs >= WAIT_CEILING_MS)
|
|
41
|
+
if (facts.elapsedMs >= (facts.budgetMs ?? WAIT_CEILING_MS))
|
|
42
42
|
return WaitVerdict.HUNG;
|
|
43
43
|
// Output IS progress. Silence is what distinguishes a build from a wedge.
|
|
44
44
|
return facts.quietForMs >= (facts.quietMeansHungMs ?? QUIET_MEANS_HUNG_MS)
|
|
@@ -8,12 +8,35 @@
|
|
|
8
8
|
* dial and setup ends with "the SDK is in the page and never dialled the bridge" — the SDK being
|
|
9
9
|
* blamed for the absence of the thing it was dialling.
|
|
10
10
|
*
|
|
11
|
-
* Idempotent by construction:
|
|
12
|
-
*
|
|
11
|
+
* Idempotent by construction: a daemon that speaks our wire contract is adopted. One that does NOT
|
|
12
|
+
* is not usable, however healthy its `/status`: an older Reticle turns away every hello from the page
|
|
13
|
+
* init just instrumented ("bridge refused the connection: invalid message"), and init used to adopt
|
|
14
|
+
* it anyway, wait out its budget, and blame the page. So a skewed daemon nobody is using is replaced
|
|
15
|
+
* (stopped by its LISTENER, never `lsof -ti`, which also kills the agent's MCP proxy), and one with
|
|
16
|
+
* live sessions is left alone with the exact command to run once they are done.
|
|
17
|
+
*
|
|
18
|
+
* A stranger on the port that is not a Reticle daemon at all is refused before this runs; see
|
|
19
|
+
* `bridge-port.ts`.
|
|
13
20
|
*/
|
|
21
|
+
import type { DaemonRegistryEntry } from '@reticlehq/core/artifacts';
|
|
14
22
|
interface EnsureDaemonDeps {
|
|
23
|
+
/**
|
|
24
|
+
* Whether a daemon's `/status` body says it speaks a different wire contract from this build.
|
|
25
|
+
* Injected: the judge is `daemonSkew` in cli-launch, the same rule `reticle mcp` warns with.
|
|
26
|
+
*/
|
|
27
|
+
readonly skewed: (status: unknown) => boolean;
|
|
15
28
|
readonly usable: (port: number) => Promise<boolean>;
|
|
29
|
+
/** The daemon's `/status` body, or undefined. */
|
|
30
|
+
readonly status: (port: number) => Promise<unknown>;
|
|
31
|
+
/** Stop the daemon on the port. True once the port is free. */
|
|
32
|
+
readonly replace: (port: number) => Promise<boolean>;
|
|
16
33
|
readonly spawn: (port: number) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* A live process already owns this port's pid file — a daemon mid-start that this spawn lost the
|
|
36
|
+
* lock to. That is exactly what `spawn` returning false means on a fresh HOME where the editor's
|
|
37
|
+
* `reticle mcp` got there first, and it is a reason to wait, not to give up.
|
|
38
|
+
*/
|
|
39
|
+
readonly starting: (port: number) => boolean;
|
|
17
40
|
readonly waitReady: (port: number) => Promise<unknown>;
|
|
18
41
|
readonly scriptPath: string | undefined;
|
|
19
42
|
}
|
|
@@ -21,10 +44,48 @@ export declare const EnsureDaemon: {
|
|
|
21
44
|
/** Something usable was already there. */
|
|
22
45
|
readonly ADOPTED: "adopted";
|
|
23
46
|
readonly STARTED: "started";
|
|
47
|
+
/** A daemon on another wire contract, serving nobody, was stopped and ours started in its place. */
|
|
48
|
+
readonly REPLACED: "replaced";
|
|
49
|
+
/** A daemon on another wire contract is serving live sessions, so it was left alone. */
|
|
50
|
+
readonly SKEWED_IN_USE: "skewed-in-use";
|
|
24
51
|
/** Could not be started, and the caller has to say so rather than blame the page. */
|
|
25
52
|
readonly UNAVAILABLE: "unavailable";
|
|
26
53
|
};
|
|
27
54
|
export type EnsureDaemon = (typeof EnsureDaemon)[keyof typeof EnsureDaemon];
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
interface EnsureDaemonResult {
|
|
56
|
+
readonly state: EnsureDaemon;
|
|
57
|
+
/** One line for the person watching, when the state needs one. */
|
|
58
|
+
readonly message?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function ensureDaemon(port: number, deps: EnsureDaemonDeps): Promise<EnsureDaemonResult>;
|
|
61
|
+
interface RetireDeps {
|
|
62
|
+
/** Every LIVE daemon in the registry. */
|
|
63
|
+
readonly registered: () => readonly DaemonRegistryEntry[];
|
|
64
|
+
readonly status: (port: number) => Promise<unknown>;
|
|
65
|
+
/** Stop the daemon on the port by its listener. True once the port is free. */
|
|
66
|
+
readonly stop: (port: number) => Promise<boolean>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Stop this project's daemons on any port but `port`, the one init is about to use. Returns the
|
|
70
|
+
* ports stopped.
|
|
71
|
+
*
|
|
72
|
+
* `init --port <new>` started a daemon on the new port and left the old one serving: the build
|
|
73
|
+
* plugin found both, dialled the old one, and init — waiting on the new one — exited 1 with
|
|
74
|
+
* "connected to a DIFFERENT Reticle daemon". A daemon another project's tab is still on is left
|
|
75
|
+
* alone, because stopping it would take that app down to fix this one. Stopped by its listener
|
|
76
|
+
* (`runKill` without force), never `lsof -ti`, which also kills the agent's MCP proxy.
|
|
77
|
+
*/
|
|
78
|
+
export declare function retireMovedDaemons(projectId: string | undefined, port: number, deps: RetireDeps): Promise<number[]>;
|
|
79
|
+
/** The real registry, `/status` and listener-only stop. */
|
|
80
|
+
export declare function nodeRetireDeps(): RetireDeps;
|
|
81
|
+
/**
|
|
82
|
+
* Hold the daemon on `port` until the returned release is called, by renewing a hold on its
|
|
83
|
+
* `/status` well inside the hold window.
|
|
84
|
+
*
|
|
85
|
+
* `init` starts the daemon and can then wait on the app for longer than the idle grace — a cold
|
|
86
|
+
* desktop build, a slow first compile. None of that counted as activity, and the daemon init had
|
|
87
|
+
* just started idled out (`reticle_daemon_idle_exit`) while init was still waiting on it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function holdDaemon(port: number): () => void;
|
|
90
|
+
export declare function nodeEnsureDaemonDeps(skewed: (status: unknown) => boolean): EnsureDaemonDeps;
|
|
30
91
|
export {};
|