Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4de1b70
chore: update devlog for v2.2.19 closeout
lidge-jun Aug 12, 2026
7a53052
fix(officecli): preflight Windows release assets (#309)
parkjs101 Aug 12, 2026
5178f69
[agent] docs: plan Slack thread delivery fix
lidge-jun Aug 12, 2026
5691917
[agent] docs: harden Slack delivery plan
lidge-jun Aug 12, 2026
f3a065b
[agent] docs: complete Slack delivery audit gates
lidge-jun Aug 12, 2026
3e42781
[agent] docs: validate persisted Slack targets
lidge-jun Aug 12, 2026
5d22b2f
[agent] fix(slack): preserve active thread delivery
lidge-jun Aug 12, 2026
4ef0bc5
[agent] test(prompt): preserve active channel contract
lidge-jun Aug 12, 2026
2e83b63
chore: update officecli for PowerShell batch diagnostics (#313)
parkjs101 Aug 12, 2026
4547fde
chore: update officecli for CSV import persistence (#301) (#314)
parkjs101 Aug 12, 2026
bd0b4c2
fix(manager): launch JS instances through Node on Windows (#318)
parkjs101 Aug 12, 2026
b46a585
chore: update devlog ref for the Slack conversation-context research …
lidge-jun Aug 12, 2026
7f41389
fix(control): allow Computer Use on Windows with the window-scoped AP…
lidge-jun Aug 12, 2026
09245ec
docs(control): carry the Windows Computer Use contract into skills an…
lidge-jun Aug 12, 2026
9f7429d
chore: update devlog ref for the Windows Computer Use plan (#308)
lidge-jun Aug 12, 2026
641e243
fix(slack): expose active conversation context (#315) (#319)
parkjs101 Aug 12, 2026
f9bed5d
test(slack): characterize identity cache behaviors the suite left unp…
lidge-jun Aug 12, 2026
0754977
feat(slack): add the enrichment-cache concurrency primitive
lidge-jun Aug 12, 2026
ffb55e6
refactor(slack): rewire identity onto the enrichment-cache primitive
lidge-jun Aug 12, 2026
ad9a9c0
chore: devlog checkpoint
lidge-jun Aug 12, 2026
7b82550
fix(settings): let the CLI status notice settle instead of sticking (…
lidge-jun Aug 12, 2026
896ff95
chore: update devlog ref for the #312 polling plan
lidge-jun Aug 12, 2026
37dac3a
chore: update officecli for resident lock warning (#320)
parkjs101 Aug 12, 2026
64df977
feat(slack): resolve conversation and thread context (#315, #317)
lidge-jun Aug 12, 2026
2434edc
fix(slack): pass the failing key to classifyFailure instead of shared…
lidge-jun Aug 12, 2026
26cb315
fix(slack): per-method suppression and rate budgets, bounded thread c…
lidge-jun Aug 12, 2026
6fae527
test(slack): make the warn-latch test a real oracle
lidge-jun Aug 12, 2026
7f0c655
fix(windows): stop shipping BOM-less PowerShell and tell agents the s…
lidge-jun Aug 12, 2026
c94bcd9
chore: update devlog ref for the WP15 Windows shell plan
lidge-jun Aug 12, 2026
7b67ca4
fix(slack): keep the loop alive while awaiting the retry backoff
lidge-jun Aug 12, 2026
9fda428
feat(slack): inject conversation context into the agent prompt (#315,…
lidge-jun Aug 12, 2026
a104a67
fix(slack): give a thread's earlier messages to an agent joining mid-…
lidge-jun Aug 12, 2026
ca2426f
feat(memory): remember where the host toolchain actually lives (#299)
lidge-jun Aug 12, 2026
d78ecbd
chore: update devlog ref for the WP16 toolchain plan
lidge-jun Aug 12, 2026
93d3213
chore: update devlog ref for the Slack conversation-context unit
lidge-jun Aug 12, 2026
b5b34bf
fix(slack): preserve active prefetch claims under cap
parkjs101 Aug 12, 2026
42cc74e
fix(slack): make 'already handled' as durable as 'still to handle' (#…
lidge-jun Aug 12, 2026
99f244a
chore: update devlog ref for the WP20 Slack durability plan
lidge-jun Aug 12, 2026
7400e95
Merge remote-tracking branch 'origin/dev' into codex/issue-317-slack-…
parkjs101 Aug 12, 2026
c9da60c
fix(slack): route the dedupe failure logs through the credential masker
lidge-jun Aug 12, 2026
4afc972
Merge remote-tracking branch 'origin/dev' into codex/issue-317-slack-…
parkjs101 Aug 12, 2026
8fbfa94
docs: document native Windows log ownership (#324)
parkjs101 Aug 12, 2026
ae7f055
fix(slack): release prefetch claims before handoff
parkjs101 Aug 12, 2026
9f5c346
Merge remote-tracking branch 'origin/dev' into codex/issue-317-slack-…
parkjs101 Aug 12, 2026
4cf9868
test(install): scope native Windows README guard
parkjs101 Aug 12, 2026
de921f0
Merge pull request #325 from lidge-jun/codex/issue-317-slack-context
parkjs101 Aug 12, 2026
58aa131
fix(security): reject NTFS ADS paths and fold Windows path identity
lidge-jun Aug 12, 2026
53aaed4
fix(security): drop case folding that over-authorized on case-sensiti…
lidge-jun Aug 12, 2026
d113c0f
fix(process): unified OwnedProcess lifetime across 44 .kill() callsites
lidge-jun Aug 12, 2026
e66ba0e
feat(messaging): shared delivery-error taxonomy and channel capabilities
lidge-jun Aug 12, 2026
98bd4aa
feat(core): Windows shell taxonomy with pwsh/PowerShell/GitBash/cmd d…
lidge-jun Aug 12, 2026
aa7f31f
feat(messaging): shared draft-stream with latest-wins coalescing
lidge-jun Aug 12, 2026
6c8d1c3
feat(telegram): durable update offset with frontier bootstrap
lidge-jun Aug 12, 2026
5b4d5c7
feat(discord): gateway lifecycle supervisor with generation fencing
lidge-jun Aug 12, 2026
11217cd
feat(discord): per-route REST rate-limit scheduler with bucket union
lidge-jun Aug 12, 2026
1b26631
test: update source-regex assertions for the custom poller contract
lidge-jun Aug 12, 2026
e3a0ed3
chore: sync line counts after wp7-wp10 implementation
lidge-jun Aug 12, 2026
057e9f3
chore: bump v2.2.19
lidge-jun Aug 12, 2026
d8a17d1
chore: bump v2.2.20
lidge-jun Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ git add devlog && git commit -m "chore: update devlog ref" && git push
- Recent non-strict hotspots: explicit `/continue`, workflow helper slash commands (`/plan` as PABCD P compatibility guide, `/interview`, `/deliberate`, `/planaudit`, `/review`, `/search`, `/goal`, `/goalplan`, `/team`, `/task`, `/fork`, `/gd`; forward PABCD transitions require `cli-jaw orchestrate <phase> --attest '{"from","to","did",...}'`), pre-prompt context hooks (`context-hooks.json`, `cli-jaw hooks`), bounded local search contract (narrow-path Grep/rg; external search via active search skill), Telegram Hub P0–P4 (`structure/telegram.md`, `/api/dashboard/telegram-hub`), goal pause gate continuation suppression (`goal_pause_gate_pending`), `tests/run.mts` programmatic test driver, `/goal plan` and `/goalplan` store user direction as `planHint` and require `/goal refine` before checkpoints; agent pause first-tap state is exposed as derived `pauseGate` on status/API surfaces while persisted status remains `active`; bounded automation is `/goal run ...`, not top-level `/autopilot`), Codex App clean-install default with opt-in migration for existing settings, bounded child-backed nullable CLI status, read-only OpenCodex root-URL/live-health diagnostics, Pi top-level `pi --mode rpc` runtime with isolated `PI_CODING_AGENT_DIR` profiles, AGY `-p` print-mode runtime with capability-probed optional `--model` (observed in AGY 1.0.12), Grok weekly quota via `~/.grok/auth.json` + Grok Build billing gRPC-web before legacy monthly fallback, SSE-first `GET /api/events` event channel with WebSocket fallback, bounded tool-log sanitizer, worker progress query/watch, canonical `/api/channel/send`, heartbeat `every`/`cron` schedules, browser runtime diagnostics/session lifecycle, Electron Node sidecar packaging, private active `k-writing` routing for Korean promotional/content writing, canonical platform classification via `src/core/platform-kind.ts` (`windows-native|wsl|linux|darwin|other`; `process.platform` decides first and `WSLENV` is never a WSL signal), and `npm run gate:all`.
- Standalone lifecycle is home-scoped: `jaw --home <path> service stop|restart [--port N]` verifies `<JAW_HOME>/jaw.pid.json` before signalling; registered launchd/systemd instances delegate to their native manager. Never recommend killing every Node process.
- Slack connection environment variables own their matching fields at runtime. Settings exposes only variable names and conservatively locks connection editing/reset while any are present; CLI setup refuses mixed input. Generic settings writes reject only env-owned paths, and persistence strips only those fields so env values never enter `settings.json` or erase unrelated file-backed credentials.
- Slack-triggered Boss turns receive `channel_id` and parent `thread_ts` in the per-turn user prompt regardless of multi-session state; agents must use that explicit context for Slack lookup/send APIs instead of parsing session labels.
- Optimization/score-maximization goals follow the optimization-loop discipline (LOOP-PHASE-DEATH/CONTINUITY/CANDIDATE-ANCHOR/INSTANCE-CHECK + GATE-ORACLE-VALIDITY):
classify candidate changes, ban a class after 3 consecutive discards, force evaluator-gate work on repeated D-phase deaths.
Canonical: dev-pabcd §10, dev-testing §9.5; injected via orchestration template and goal continuation.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This repository is a Node.js ESM orchestration runtime for boss/employee dispatc
- Gemini full-access runs use `--skip-trust --approval-mode yolo` on both fresh and resume sessions.
- `/api/channel/send` is the canonical outbound Telegram/Discord/Slack delivery endpoint.
- Slack connection environment variables own their matching fields at runtime: `GET /api/settings` reports `slackEnvironmentVariables` while redacting values, Settings and CLI setup conservatively refuse connection editing while any are present, generic `PUT`s reject only env-owned paths, and persistence strips only those fields so environment values never enter `settings.json` or erase unrelated file-backed credentials. Full `POST /api/settings/slack/reset` still returns `409` while any connection env variable exists.
- Slack-triggered Boss turns receive `channel_id` and parent `thread_ts` in the per-turn user prompt regardless of multi-session state; agents use that explicit context for Slack lookup/send APIs instead of parsing session labels.
- Heartbeat schedules support `{ kind: "every", minutes }` and `{ kind: "cron", cron, timeZone? }`.
- Tool logs are capped by `src/shared/tool-log-sanitize.ts` before SSE/WebSocket, `agent_done`, and orchestration snapshot delivery. Web UI delivery is SSE-first through `GET /api/events`, with WebSocket as the legacy fallback dispatcher.
- Employee worker progress is query-first via `jaw worker status [agent]`, watchable via `jaw worker watch [agent]` or `jaw dispatch --watch`, memory-only for current plus previous completed run, and safe-summary only with thinking detail hidden.
Expand Down
97 changes: 94 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,97 @@ wsl.exe -d Ubuntu -- bash -lc "jaw dashboard"

</details>

<details>
<summary><b>Native Windows (PowerShell beta)</b> — detached server logs</summary>

`jaw serve` writes to the stdout and stderr streams it inherits. It does not
create or open `serve.out.log`, and native Windows does not have a registered
`jaw service` logging backend. PowerShell's
`Start-Process -RedirectStandardOutput/-RedirectStandardError` creates or
truncates its target files on every launch.

Run the redirection inside a child PowerShell process instead. This example
appends operator-owned logs under `<JAW_HOME>\logs`:

```powershell
$jawHome = 'C:\jaw\worker-a'
$port = 3458
$logDir = Join-Path $jawHome 'logs'
$outLog = Join-Path $logDir 'serve.out.log'
$errLog = Join-Path $logDir 'serve.err.log'
New-Item -ItemType Directory -Force -Path $logDir -ErrorAction Stop | Out-Null
foreach ($path in @($outLog, $errLog)) {
# OpenOrCreate preserves existing content while proving that the child can append.
$probe = [IO.File]::Open($path, 'OpenOrCreate', 'Write', 'ReadWrite')
$probe.Dispose()
}

$jaw = (Get-Command jaw.cmd -ErrorAction Stop).Source
$childCommand = "& '$jaw' --home '$jawHome' serve --port $port --no-open 1>> '$outLog' 2>> '$errLog'"
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($childCommand))
Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile', '-EncodedCommand', $encoded -WindowStyle Hidden | Out-Null
```

Read each stream from a separate PowerShell terminal (`Get-Content -Wait`
occupies its terminal). These commands use explicit paths because variables
from the launch terminal are not available in a new PowerShell session:

```powershell
# Terminal 1
Get-Content -LiteralPath 'C:\jaw\worker-a\logs\serve.out.log' -Tail 100 -Wait

# Terminal 2
Get-Content -LiteralPath 'C:\jaw\worker-a\logs\serve.err.log' -Tail 100 -Wait
```

Lifecycle commands are home-scoped and verify `<JAW_HOME>\jaw.pid.json`
before signalling:

```powershell
& $jaw --home $jawHome service stop --port $port
& $jaw --home $jawHome service restart --port $port
```

A standalone `service restart` safely relaunches the instance detached, but
cannot recreate the operator's file redirection. To preserve file capture,
`stop`, optionally rotate the closed logs, and run the launch block again:

```powershell
$pidFile = Join-Path $jawHome 'jaw.pid.json'
$serverProcess = $null
if (Test-Path -LiteralPath $pidFile -PathType Leaf) {
$record = Get-Content -LiteralPath $pidFile -Raw -ErrorAction Stop | ConvertFrom-Json
$serverProcess = Get-Process -Id ([int]$record.pid) -ErrorAction SilentlyContinue
}

& $jaw --home $jawHome service stop --port $port
if ($LASTEXITCODE -ne 0) {
throw "jaw service stop failed with exit code $LASTEXITCODE"
}
if ($serverProcess) {
try {
if (-not $serverProcess.WaitForExit(5000)) {
throw "jaw serve pid $($serverProcess.Id) did not exit within 5000ms"
}
} finally {
$serverProcess.Dispose()
}
}

$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
foreach ($path in @($outLog, $errLog)) {
if (Test-Path -LiteralPath $path) {
Move-Item -LiteralPath $path -Destination "$path.$stamp" -ErrorAction Stop
}
}
# Run the Start-Process launch block above again.
```

Do not use `Get-Process node | Stop-Process`; it can terminate unrelated
cli-jaw instances and AI runtime processes.

</details>

<details>
<summary><b>Fresh-machine evidence</b> — maintainer release check</summary>

Expand Down Expand Up @@ -485,7 +576,7 @@ jaw skill list # see what's available
| **Web-AI vendors** | `jaw browser web-ai --vendor chatgpt\|gemini\|grok` with session lifecycle, diagnostics, source-audit/answer-artifact support, and ChatGPT code-mode zip recovery |
| **Diagram Skill** | Generate SVG diagrams and interactive visualizations, rendered inline in chat |

Computer Use lets you control any macOS app — Finder, Safari, System Settings, Xcode — through natural language. Point it at your localhost dev server in Safari and you get a full visual testing loop.
Computer Use lets you control desktop apps — Finder, Safari, System Settings, Xcode on macOS; any window on Windows — through natural language. Point it at your localhost dev server in a browser and you get a full visual testing loop. The two hosts expose different APIs (macOS is app-scoped, Windows is window-scoped), and the `desktop-control` skill routes between them.

---

Expand Down Expand Up @@ -514,7 +605,7 @@ Same capabilities as Telegram — text, files, commands. Channel/thread routing,

### Slack

Socket Mode bot with the same shared command catalog — mentions, DMs, slash commands, file/image relay, thread replies.
Socket Mode bot with the same shared command catalog — mentions, DMs, slash commands, file/image relay, thread replies. Each Slack-triggered agent turn receives the current conversation ID and parent thread timestamp explicitly, so history/member lookups and targeted replies do not depend on parsing an internal session label or enabling multi-session.

<details>
<summary>Setup (guided wizard)</summary>
Expand Down Expand Up @@ -690,7 +781,7 @@ Architecture details: [ARCHITECTURE.md](docs/ARCHITECTURE.md) · Pre-prompt cont
| Browser commands fail | Install Chrome/Chromium. Run `jaw browser start` first |
| Employee dispatch hangs | Run `jaw employee list`, ensure the employee CLI is authenticated (`jaw doctor`), then retry with `jaw dispatch --watch` |
| Employee dispatch returns non-JSON or HTML | The server may be stale or missing the route. Run `npm run build` or restart the manager/dashboard process. |
| Computer Use not working | macOS only. Codex CLI required. Check Automation permission in System Settings |
| Computer Use not working | macOS or Windows; Codex CLI required. macOS: check Automation permission in System Settings. Windows: run calls inside `node_repl` and keep the Codex desktop app running in the logged-on session — an empty `list_windows()` means you are not on the pipe, not that no windows are open |

---

Expand Down
2 changes: 1 addition & 1 deletion devlog
23 changes: 22 additions & 1 deletion electron/src/main/lib/terminal/shell-discovery.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { existsSync } from 'node:fs';
import {
detectWindowsShell,
windowsGitBashPaths,
type WindowsShellKind,
} from '../../../../../src/core/windows-shell.js';

const SHELLS: Record<string, string[]> = {
darwin: ['/bin/zsh', '/bin/bash', '/bin/sh'],
linux: ['/bin/bash', '/bin/zsh', '/bin/sh'],
win32: ['powershell.exe', 'cmd.exe'],
};

function windowsShellExecutable(kind: WindowsShellKind): string {
switch (kind) {
case 'pwsh7':
return 'pwsh.exe';
case 'powershell5':
return 'powershell.exe';
case 'gitbash':
return windowsGitBashPaths(process.env).find(existsSync) ?? 'bash.exe';
case 'cmd':
case 'unknown':
return process.env['ComSpec'] || process.env['COMSPEC'] || 'cmd.exe';
}
}

export function discoverShell(): string {
if (process.platform === 'win32') {
return windowsShellExecutable(detectWindowsShell());
}
const envShell = process.env.SHELL;
if (envShell && existsSync(envShell)) return envShell;
const candidates = SHELLS[process.platform] ?? ['/bin/sh'];
Expand Down
2 changes: 1 addition & 1 deletion officecli
Submodule officecli updated 55 files
+1 −0 README.md
+1 −1 docs/hwp-source-inventory.md
+5 −0 docs/providers/rhwp-sidecar-contract.md
+65 −0 install.ps1
+31 −3 install.sh
+1 −0 officecli.slnx
+9 −1 scripts/gate.sh
+34 −0 scripts/integration-checks.json
+12 −2 scripts/ledger-checks.json
+121 −0 scripts/verify-integration-merge.py
+32 −4 src/officecli/CommandBuilder.Batch.cs
+180 −0 src/officecli/CommandBuilder.Convert.cs
+74 −0 src/officecli/CommandBuilder.Get.Hwp.cs
+3 −0 src/officecli/CommandBuilder.GetQuery.cs
+29 −4 src/officecli/CommandBuilder.Import.cs
+4 −0 src/officecli/CommandBuilder.NativeOps.cs
+4 −1 src/officecli/CommandBuilder.View.HwpNative.cs
+23 −12 src/officecli/CommandBuilder.View.HwpRoute.cs
+41 −8 src/officecli/CommandBuilder.View.cs
+68 −4 src/officecli/CommandBuilder.Watch.cs
+12 −0 src/officecli/CommandBuilder.cs
+1 −0 src/officecli/Core/SkillInstaller.cs
+32 −1 src/officecli/Core/TemplateMerger.cs
+5 −0 src/officecli/Handlers/Excel/ExcelHandler.Helpers.Sheet.cs
+7 −1 src/officecli/Handlers/Excel/ExcelHandler.Import.cs
+16 −1 src/officecli/Handlers/Hwp/HwpRuntimeProbe.cs
+153 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Forms.cs
+290 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Html.cs
+167 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Issues.cs
+190 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Markdown.cs
+97 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Objects.cs
+135 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.OutlineStats.cs
+31 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Styles.cs
+143 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Tables.cs
+137 −0 src/officecli/Handlers/Hwpx/HwpxHandler.View.Text.cs
+1 −1,271 src/officecli/Handlers/Hwpx/HwpxHandler.View.cs
+3 −3 src/officecli/McpServer.cs
+49 −8 src/officecli/ResidentServer.cs
+171 −22 src/rhwp-field-bridge/Cargo.lock
+1 −1 src/rhwp-field-bridge/Cargo.toml
+15 −0 src/rhwp-field-bridge/src/ops_native_header_footer.rs
+3 −0 src/rhwp-field-bridge/src/ops_native_objects.rs
+4 −0 src/rhwp-field-bridge/src/ops_native_support.rs
+11 −0 src/rhwp-field-bridge/src/ops_native_text.rs
+57 −0 tests/OfficeCli.Tests/BatchPowerShellDiagnosticsTests.cs
+96 −0 tests/OfficeCli.Tests/ConvertCommandTests.cs
+96 −0 tests/OfficeCli.Tests/ExcelImportPersistenceTests.cs
+42 −0 tests/OfficeCli.Tests/Hwp/HwpBridgeNativeOpTests.cs
+48 −28 tests/OfficeCli.Tests/Hwp/HwpBridgeSidecarTestSupport.cs
+32 −10 tests/OfficeCli.Tests/Hwp/HwpBridgeTableScanTests.cs
+37 −0 tests/OfficeCli.Tests/Hwp/HwpProviderRouteTests.cs
+7 −0 tests/OfficeCli.Tests/Hwp/HwpRhwpSurfaceParityTests.cs
+40 −0 tests/OfficeCli.Tests/InstallerSecurityTests.cs
+20 −0 tests/OfficeCli.Tests/ResidentLockWarningTests.cs
+74 −2 tests/fixtures/common/rhwp-surface-parity.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cli-jaw",
"version": "2.2.18",
"version": "2.2.20",
"description": "Personal AI assistant powered by Pi, Antigravity, AI-E, Claude, Claude E, Codex, Codex App, Cursor, Grok, Kiro, OpenCode, and Copilot — Web, Terminal, Telegram, and Discord interfaces with 107 built-in skills",
"type": "module",
"keywords": [
Expand Down
84 changes: 84 additions & 0 deletions public/manager/src/settings/cli-status-polling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// #312: the Settings panel used to read /api/cli-status once, so a probe that
// was still running when the page mounted left "상태 확인 중" on screen forever.
//
// The server cannot fix this on its own: CliStatusCache is demand-driven and
// has no timer (src/cli/cli-status.ts), so a snapshot only advances when
// somebody reads it again. The UI has to be that somebody — but bounded, since
// a read can fork a worker that runs real CLI probes.

export type CliStatusProbeState = 'checking' | 'fresh' | 'stale' | 'failing';

export type PollableCliStatus = {
probeState?: CliStatusProbeState;
/** Server-provided backoff deadline, present while probeState is `failing`. */
nextRetryAt?: number;
};

/** Floor for the first delay: a GET can fork a worker running real CLI probes. */
export const CLI_STATUS_MIN_DELAY_MS = 1_000;
export const CLI_STATUS_MAX_DELAY_MS = 8_000;

/**
* The worker itself may legitimately run for 60s
* (WORKER_OUTER_TIMEOUT_MS in src/cli/cli-status-worker.ts). Give it that plus
* room for one observing read, or a healthy slow host gets reported as a
* timeout.
*/
export const CLI_STATUS_POLL_HORIZON_MS = 90_000;
export const CLI_STATUS_MAX_ATTEMPTS = 24;

/**
* `failing` is deliberately NOT terminal. The cache restarts probing on the
* first read after its backoff expires, so a UI that stops on `failing` would
* simply swap one permanent notice for another and never observe the recovery.
*/
export function shouldPollCliStatus(
snapshot: Record<string, PollableCliStatus> | null | undefined,
cli: string | null | undefined,
): boolean {
if (!snapshot || !cli) return false;
const state = snapshot[cli]?.probeState;
if (!state) return false;
return state === 'checking' || state === 'failing';
}

/** Gentle backoff between the floor and the ceiling. */
export function nextCliStatusPollDelay(attempt: number): number {
const step = Number.isFinite(attempt) && attempt > 0 ? Math.floor(attempt) : 0;
const delay = CLI_STATUS_MIN_DELAY_MS * 2 ** Math.min(step, 8);
return Math.min(Math.max(delay, CLI_STATUS_MIN_DELAY_MS), CLI_STATUS_MAX_DELAY_MS);
}

export type PollSchedule =
| { kind: 'stop' }
| { kind: 'exhausted' }
| { kind: 'wait'; delayMs: number };

/**
* Decides the next move. Two independent bounds, neither of which resets on
* server responses: a wall-clock deadline and a cap on real requests. Waiting
* out a server backoff must NOT consume an attempt, or a host that is merely
* backing off would be declared timed-out without ever being asked again.
*/
export function planCliStatusPoll(input: {
snapshot: Record<string, PollableCliStatus> | null | undefined;
cli: string | null | undefined;
attempts: number;
now: number;
deadline: number;
}): PollSchedule {
const { snapshot, cli, attempts, now, deadline } = input;
if (!shouldPollCliStatus(snapshot, cli)) return { kind: 'stop' };
if (now >= deadline) return { kind: 'exhausted' };
if (attempts >= CLI_STATUS_MAX_ATTEMPTS) return { kind: 'exhausted' };

const backoff = snapshot?.[cli!]?.nextRetryAt;
const earliest = typeof backoff === 'number' && backoff > now
? backoff
: now + nextCliStatusPollDelay(attempts);
// A backoff reaching past the deadline means the answer will not arrive in
// time: wait until the deadline and report exhaustion there rather than
// firing a doomed request or arming an unbounded timer.
const target = Math.min(earliest, deadline);
return { kind: 'wait', delayMs: Math.max(target - now, 0) };
}
Loading
Loading