@hwip/cli 0.0.1 → 0.1.1
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 +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CollectResult } from './listener.js';
|
|
2
|
+
export interface InventoryReply {
|
|
3
|
+
id: number;
|
|
4
|
+
result?: CollectResult;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface InventoryOffload {
|
|
8
|
+
/** Resolve one inventory sweep. Rejects when a working worker fails mid-sweep. */
|
|
9
|
+
collect: () => Promise<CollectResult>;
|
|
10
|
+
/** Stop the worker. Safe to call more than once. */
|
|
11
|
+
close: () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Longer than any sane sweep (measured ~3s at 15 sessions), shorter than the
|
|
14
|
+
* run of cycles a stuck worker would otherwise cost: the caller gates sweeps
|
|
15
|
+
* one-at-a-time, so this bounds how long a hang can hold the inventory. */
|
|
16
|
+
export declare const COLLECT_TIMEOUT_MS = 20000;
|
|
17
|
+
/**
|
|
18
|
+
* Consecutive timed-out sweeps before the offload stops using workers.
|
|
19
|
+
*
|
|
20
|
+
* A sweep that overruns is terminated, and the next one starts a fresh worker
|
|
21
|
+
* from nothing — so on a host where a cold sweep cannot finish inside the
|
|
22
|
+
* timeout, retrying forever means the daemon never reports at all. Reporting
|
|
23
|
+
* slowly beats not reporting, and in-thread is exactly "slowly". Three in a row
|
|
24
|
+
* rather than one, so a single slow moment (a load spike, a machine waking) does
|
|
25
|
+
* not cost the offload straight away; the streak resets on any answered sweep.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_CONSECUTIVE_TIMEOUTS = 3;
|
|
28
|
+
/**
|
|
29
|
+
* How long timed-out sweeps run in-thread before a worker is tried again.
|
|
30
|
+
*
|
|
31
|
+
* This used to be forever, which inverted the whole point of the offload: a
|
|
32
|
+
* load spike long enough to overrun three sweeps left the daemon running the
|
|
33
|
+
* blocking sweep on its own event loop for the rest of its life. Measured
|
|
34
|
+
* 2026-09-18 on this Mac — after the switch, a `setInterval(5_000)` fired at a
|
|
35
|
+
* median of 28.5 s, the WebSocket read loop went with it, and the heartbeat
|
|
36
|
+
* killed a healthy socket every 1-3 minutes.
|
|
37
|
+
*
|
|
38
|
+
* The load that overruns a sweep passes; the give-up must pass with it. Five
|
|
39
|
+
* minutes is long enough that a busy stretch is not spent respawning workers
|
|
40
|
+
* into the same wall, short enough that the degraded state is measured in
|
|
41
|
+
* minutes rather than days.
|
|
42
|
+
*/
|
|
43
|
+
export declare const IN_THREAD_COOLDOWN_MS: number;
|
|
44
|
+
export declare const startInventoryOffload: (inThread: () => CollectResult, log: (msg: string) => void, workerPath?: string,
|
|
45
|
+
/** Overridable so a test can exercise the timeout path without waiting 20s. */
|
|
46
|
+
timeoutMs?: number,
|
|
47
|
+
/** Overridable for the same reason as `timeoutMs`. */
|
|
48
|
+
cooldownMs?: number) => InventoryOffload;
|
|
49
|
+
//# sourceMappingURL=inventory-offload.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startInventoryOffload = exports.IN_THREAD_COOLDOWN_MS = exports.MAX_CONSECUTIVE_TIMEOUTS = exports.COLLECT_TIMEOUT_MS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Runs the tmux session inventory on a worker thread so the daemon's event
|
|
6
|
+
* loop stays free while `tmux` child processes are waited on.
|
|
7
|
+
*
|
|
8
|
+
* Why a thread and not async spawns: the inventory is a chain of synchronous
|
|
9
|
+
* `spawnSync` calls — one `list-sessions` plus `display-message` and
|
|
10
|
+
* `capture-pane` per session — and with 15 sessions a single sweep was measured
|
|
11
|
+
* at 3s on the main thread (57% of daemon wall time, measured 2026-08-26).
|
|
12
|
+
* Every one of those seconds the WebSocket was neither read nor written, so
|
|
13
|
+
* stream subscribes arrived late, frames left late, pongs were missed and leases
|
|
14
|
+
* lapsed. Moving the same synchronous code onto a worker keeps every tmux call
|
|
15
|
+
* exactly as it was — including the tests that stub `spawnSync` — and only
|
|
16
|
+
* changes which thread waits.
|
|
17
|
+
*
|
|
18
|
+
* The worker is optional. A worker that fails before it has ever answered
|
|
19
|
+
* (missing file, module error, immediate exit) is given up on: pending and
|
|
20
|
+
* future sweeps run in-thread as before, so a broken worker degrades to the old
|
|
21
|
+
* behaviour instead of an empty session list. A worker that dies after having
|
|
22
|
+
* worked is treated as transient — that sweep fails (the caller skips the
|
|
23
|
+
* cycle) and the next `collect()` starts a fresh one.
|
|
24
|
+
*
|
|
25
|
+
* A worker we killed ourselves is neither. `terminate()` (on timeout, on close)
|
|
26
|
+
* exits with code 1, indistinguishable from a crash, so the exit code cannot be
|
|
27
|
+
* read — what separates the two is whether that worker still owed an answer,
|
|
28
|
+
* which is why `Live` carries its own `owed` ids rather than sharing `pending`.
|
|
29
|
+
* Getting this wrong is expensive in one direction only: a timeout misread as
|
|
30
|
+
* "died before ever answering" is permanent, and it fires exactly when the host
|
|
31
|
+
* is slow enough for a sweep to overrun, which is when the offload matters most.
|
|
32
|
+
*/
|
|
33
|
+
const path_1 = require("path");
|
|
34
|
+
const worker_threads_1 = require("worker_threads");
|
|
35
|
+
/** Longer than any sane sweep (measured ~3s at 15 sessions), shorter than the
|
|
36
|
+
* run of cycles a stuck worker would otherwise cost: the caller gates sweeps
|
|
37
|
+
* one-at-a-time, so this bounds how long a hang can hold the inventory. */
|
|
38
|
+
exports.COLLECT_TIMEOUT_MS = 20_000;
|
|
39
|
+
/**
|
|
40
|
+
* Consecutive timed-out sweeps before the offload stops using workers.
|
|
41
|
+
*
|
|
42
|
+
* A sweep that overruns is terminated, and the next one starts a fresh worker
|
|
43
|
+
* from nothing — so on a host where a cold sweep cannot finish inside the
|
|
44
|
+
* timeout, retrying forever means the daemon never reports at all. Reporting
|
|
45
|
+
* slowly beats not reporting, and in-thread is exactly "slowly". Three in a row
|
|
46
|
+
* rather than one, so a single slow moment (a load spike, a machine waking) does
|
|
47
|
+
* not cost the offload straight away; the streak resets on any answered sweep.
|
|
48
|
+
*/
|
|
49
|
+
exports.MAX_CONSECUTIVE_TIMEOUTS = 3;
|
|
50
|
+
/**
|
|
51
|
+
* How long timed-out sweeps run in-thread before a worker is tried again.
|
|
52
|
+
*
|
|
53
|
+
* This used to be forever, which inverted the whole point of the offload: a
|
|
54
|
+
* load spike long enough to overrun three sweeps left the daemon running the
|
|
55
|
+
* blocking sweep on its own event loop for the rest of its life. Measured
|
|
56
|
+
* 2026-09-18 on this Mac — after the switch, a `setInterval(5_000)` fired at a
|
|
57
|
+
* median of 28.5 s, the WebSocket read loop went with it, and the heartbeat
|
|
58
|
+
* killed a healthy socket every 1-3 minutes.
|
|
59
|
+
*
|
|
60
|
+
* The load that overruns a sweep passes; the give-up must pass with it. Five
|
|
61
|
+
* minutes is long enough that a busy stretch is not spent respawning workers
|
|
62
|
+
* into the same wall, short enough that the degraded state is measured in
|
|
63
|
+
* minutes rather than days.
|
|
64
|
+
*/
|
|
65
|
+
exports.IN_THREAD_COOLDOWN_MS = 5 * 60_000;
|
|
66
|
+
const startInventoryOffload = (inThread, log, workerPath = (0, path_1.join)(__dirname, 'inventory-worker.js'),
|
|
67
|
+
/** Overridable so a test can exercise the timeout path without waiting 20s. */
|
|
68
|
+
timeoutMs = exports.COLLECT_TIMEOUT_MS,
|
|
69
|
+
/** Overridable for the same reason as `timeoutMs`. */
|
|
70
|
+
cooldownMs = exports.IN_THREAD_COOLDOWN_MS) => {
|
|
71
|
+
let worker = null;
|
|
72
|
+
let closed = false;
|
|
73
|
+
/** Set once a worker has answered; a failure before that is not transient. */
|
|
74
|
+
let everReplied = false;
|
|
75
|
+
/** Set when workers are given up on for good — a worker that never worked. */
|
|
76
|
+
let inThreadOnly = false;
|
|
77
|
+
/** While `Date.now()` is under this, timed-out sweeps run in-thread instead. */
|
|
78
|
+
let inThreadUntil = 0;
|
|
79
|
+
let nextId = 0;
|
|
80
|
+
/** Timed-out sweeps since the last answered one. */
|
|
81
|
+
let consecutiveTimeouts = 0;
|
|
82
|
+
const pending = new Map();
|
|
83
|
+
const settleAll = (outcome) => {
|
|
84
|
+
for (const [id, p] of pending) {
|
|
85
|
+
clearTimeout(p.timer);
|
|
86
|
+
pending.delete(id);
|
|
87
|
+
outcome(p);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const onFailure = (what) => {
|
|
91
|
+
if (closed)
|
|
92
|
+
return settleAll((p) => p.reject(new Error('inventory offload closed')));
|
|
93
|
+
if (everReplied)
|
|
94
|
+
return settleAll((p) => p.reject(new Error(what)));
|
|
95
|
+
inThreadOnly = true;
|
|
96
|
+
log(`! ${what} before its first sweep — running the inventory in-thread from now on`);
|
|
97
|
+
settleAll((p) => {
|
|
98
|
+
try {
|
|
99
|
+
p.resolve(inThread());
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
p.reject(err instanceof Error ? err : new Error(String(err)));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const spawn = () => {
|
|
107
|
+
const w = new worker_threads_1.Worker(workerPath);
|
|
108
|
+
// Never keep the process alive on the worker's account: the daemon's
|
|
109
|
+
// own socket and timers decide when it exits.
|
|
110
|
+
w.unref();
|
|
111
|
+
const live = { w, owed: new Set() };
|
|
112
|
+
w.on('message', (msg) => {
|
|
113
|
+
live.owed.delete(msg.id);
|
|
114
|
+
const p = pending.get(msg.id);
|
|
115
|
+
if (!p)
|
|
116
|
+
return;
|
|
117
|
+
pending.delete(msg.id);
|
|
118
|
+
clearTimeout(p.timer);
|
|
119
|
+
everReplied = true;
|
|
120
|
+
consecutiveTimeouts = 0;
|
|
121
|
+
if (msg.error !== undefined)
|
|
122
|
+
p.reject(new Error(msg.error));
|
|
123
|
+
else
|
|
124
|
+
p.resolve(msg.result);
|
|
125
|
+
});
|
|
126
|
+
w.on('error', (err) => {
|
|
127
|
+
if (worker === live)
|
|
128
|
+
worker = null;
|
|
129
|
+
// Reported here, so the exit that follows is this same death and not
|
|
130
|
+
// a second one. An id leaves `owed` the moment its sweep is settled,
|
|
131
|
+
// whatever settled it — that is the invariant the exit handler reads.
|
|
132
|
+
live.owed.clear();
|
|
133
|
+
onFailure(`inventory worker error (${err instanceof Error ? err.message : String(err)})`);
|
|
134
|
+
});
|
|
135
|
+
w.on('exit', (code) => {
|
|
136
|
+
if (worker === live)
|
|
137
|
+
worker = null;
|
|
138
|
+
// Did THIS worker still owe an answer? `terminate()` exits with code
|
|
139
|
+
// 1 exactly like a crash, so the code says nothing; what separates a
|
|
140
|
+
// death we caused from one we did not is whether the sweep it was
|
|
141
|
+
// holding had already been settled. Asking the shared `pending` map
|
|
142
|
+
// instead judged a terminated worker against the NEXT worker's sweep
|
|
143
|
+
// — the timeout path nulls `worker`, so the following poll spawns a
|
|
144
|
+
// replacement and registers its id a second before this event lands.
|
|
145
|
+
// A routine timeout then read as "died before ever answering", which
|
|
146
|
+
// is permanent: `everReplied` is false, so the offload never
|
|
147
|
+
// recovers for the life of the daemon.
|
|
148
|
+
if (live.owed.size > 0)
|
|
149
|
+
onFailure(`inventory worker exited (${code})`);
|
|
150
|
+
});
|
|
151
|
+
return live;
|
|
152
|
+
};
|
|
153
|
+
const collect = () => {
|
|
154
|
+
if (closed || inThreadOnly)
|
|
155
|
+
return Promise.resolve(inThread());
|
|
156
|
+
if (inThreadUntil > 0) {
|
|
157
|
+
if (Date.now() < inThreadUntil)
|
|
158
|
+
return Promise.resolve(inThread());
|
|
159
|
+
inThreadUntil = 0;
|
|
160
|
+
// One strike, not three. The first degrade spends three sweeps
|
|
161
|
+
// finding out the host is too slow, and each of those costs a whole
|
|
162
|
+
// `timeoutMs` with no inventory at all (the caller skips the cycle
|
|
163
|
+
// and gates the next sweep behind this one). Paying that again on
|
|
164
|
+
// every cooldown expiry, on a host that is still loaded, is the
|
|
165
|
+
// "not reporting" this module exists to avoid.
|
|
166
|
+
consecutiveTimeouts = exports.MAX_CONSECUTIVE_TIMEOUTS - 1;
|
|
167
|
+
log('inventory: trying the worker again');
|
|
168
|
+
}
|
|
169
|
+
worker ??= spawn();
|
|
170
|
+
const live = worker;
|
|
171
|
+
const id = ++nextId;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
pending.delete(id);
|
|
175
|
+
// Settled here, so the terminate below is a death we caused and
|
|
176
|
+
// this worker's exit must not be reported as a failure.
|
|
177
|
+
live.owed.delete(id);
|
|
178
|
+
consecutiveTimeouts += 1;
|
|
179
|
+
if (consecutiveTimeouts >= exports.MAX_CONSECUTIVE_TIMEOUTS && !inThreadOnly && inThreadUntil === 0) {
|
|
180
|
+
inThreadUntil = Date.now() + cooldownMs;
|
|
181
|
+
const held = cooldownMs >= 60_000 ? `${Math.round(cooldownMs / 60_000)} min` : `${Math.round(cooldownMs / 1_000)}s`;
|
|
182
|
+
log(`! inventory sweep timed out ${consecutiveTimeouts}× in a row — running the inventory in-thread for ${held}`);
|
|
183
|
+
}
|
|
184
|
+
reject(new Error(`inventory sweep exceeded ${timeoutMs / 1000}s`));
|
|
185
|
+
// A stuck worker is not coming back; the next call spawns a fresh one.
|
|
186
|
+
if (worker === live)
|
|
187
|
+
worker = null;
|
|
188
|
+
void live.w.terminate();
|
|
189
|
+
}, timeoutMs);
|
|
190
|
+
timer.unref();
|
|
191
|
+
pending.set(id, { resolve, reject, timer });
|
|
192
|
+
live.owed.add(id);
|
|
193
|
+
live.w.postMessage(id);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const close = () => {
|
|
197
|
+
closed = true;
|
|
198
|
+
settleAll((p) => p.reject(new Error('inventory offload closed')));
|
|
199
|
+
const live = worker;
|
|
200
|
+
worker = null;
|
|
201
|
+
if (!live)
|
|
202
|
+
return;
|
|
203
|
+
// Settled above; clear what it owed for the same reason the timeout does.
|
|
204
|
+
live.owed.clear();
|
|
205
|
+
void live.w.terminate();
|
|
206
|
+
};
|
|
207
|
+
return { collect, close };
|
|
208
|
+
};
|
|
209
|
+
exports.startInventoryOffload = startInventoryOffload;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Worker-thread entry for the tmux session inventory (see inventory-offload.ts).
|
|
5
|
+
* One sweep per request; the sweep itself is the unchanged synchronous
|
|
6
|
+
* `collectSessionsVerbose` — it only runs on this thread now.
|
|
7
|
+
*/
|
|
8
|
+
const worker_threads_1 = require("worker_threads");
|
|
9
|
+
const agent_rules_fetch_js_1 = require("./agent-rules-fetch.js");
|
|
10
|
+
const listener_js_1 = require("./listener.js");
|
|
11
|
+
const port = worker_threads_1.parentPort;
|
|
12
|
+
if (!port)
|
|
13
|
+
throw new Error('inventory-worker must be started as a worker thread');
|
|
14
|
+
port.on('message', (id) => {
|
|
15
|
+
let reply;
|
|
16
|
+
try {
|
|
17
|
+
// Own module instance: pick up the OTA rules the main thread cached, else
|
|
18
|
+
// every non-claude session reports `unknown` (see syncManifestFromCache).
|
|
19
|
+
// Inside the try: a throw here must become an error reply, not a timeout.
|
|
20
|
+
(0, agent_rules_fetch_js_1.syncManifestFromCache)();
|
|
21
|
+
reply = { id, result: (0, listener_js_1.collectSessionsVerbose)() };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
reply = { id, error: err instanceof Error ? err.message : String(err) };
|
|
25
|
+
}
|
|
26
|
+
port.postMessage(reply);
|
|
27
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type JSONPath } from 'jsonc-parser';
|
|
2
|
+
/**
|
|
3
|
+
* An agent's settings file read as JSONC — comments and trailing commas allowed,
|
|
4
|
+
* the way the agents themselves read them — and changed by small edits to its
|
|
5
|
+
* text, so what the user wrote around hwip's entries (comments, their ordering,
|
|
6
|
+
* their indentation) stays exactly as it was. `jsonc-parser` (Microsoft, the
|
|
7
|
+
* parser VS Code uses for its own settings) does the parsing and the edits;
|
|
8
|
+
* this file only fixes the conventions hwip needs on top of it.
|
|
9
|
+
*
|
|
10
|
+
* Plain JSON is a subset, so a file without comments is read and edited the
|
|
11
|
+
* same way. A file with a syntax error is not read at all (`JsoncSyntaxError`):
|
|
12
|
+
* editing around a broken part could write something worse.
|
|
13
|
+
*
|
|
14
|
+
* Every edit is checked: the text after it must read back as exactly the value
|
|
15
|
+
* the edit was meant to produce. The library gets that wrong in places — removing
|
|
16
|
+
* an item of an array whose last two items share a line joins them into one or
|
|
17
|
+
* breaks the brackets — and a settings file that agents read must never be left
|
|
18
|
+
* like that. A file with
|
|
19
|
+
* no comments is then written whole from the intended value (nothing of the
|
|
20
|
+
* user's is lost); one with comments is left unchanged and the edit throws
|
|
21
|
+
* (`JsoncEditError`).
|
|
22
|
+
*/
|
|
23
|
+
export declare class JsoncSyntaxError extends Error {
|
|
24
|
+
constructor();
|
|
25
|
+
}
|
|
26
|
+
/** Valid JSONC, but the value is a list, a string, `null` — not a settings object. */
|
|
27
|
+
export declare class JsoncNotObjectError extends Error {
|
|
28
|
+
constructor();
|
|
29
|
+
}
|
|
30
|
+
/** An edit the library could not apply correctly to a file whose comments a rewrite would lose. */
|
|
31
|
+
export declare class JsoncEditError extends Error {
|
|
32
|
+
constructor();
|
|
33
|
+
}
|
|
34
|
+
/** The value of a JSONC text, or `undefined` when it has a syntax error. An empty text is `{}`. */
|
|
35
|
+
export declare const parseJsonc: (text: string) => unknown;
|
|
36
|
+
export declare class JsoncDoc {
|
|
37
|
+
private readonly bom;
|
|
38
|
+
private body;
|
|
39
|
+
private readonly formatting;
|
|
40
|
+
/** What the file held when it was read (null: there was no file), for `replaceFile` to check before it writes. */
|
|
41
|
+
readonly expected: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* `text` is a settings file's content; throws `JsoncSyntaxError`, or `JsoncNotObjectError` when it is valid but not an object.
|
|
44
|
+
* `existed` is false for a file that was not there (`text` is then empty).
|
|
45
|
+
*/
|
|
46
|
+
constructor(text: string, existed?: boolean);
|
|
47
|
+
/** The text without an entry that shares its line (a compact array or object), or null when a comment sits in the way. */
|
|
48
|
+
private withoutInline;
|
|
49
|
+
/** Would removing the entry at `path` take a comment with it (one inside the entry, or at the end of its last line)? */
|
|
50
|
+
removalTakesComment(path: JSONPath): boolean;
|
|
51
|
+
/** Does the text hold a comment? (Not one inside a string: the scanner knows tokens.) */
|
|
52
|
+
hasComments(): boolean;
|
|
53
|
+
/** Every comment in `text` with where it starts, in order. */
|
|
54
|
+
private static commentsIn;
|
|
55
|
+
/**
|
|
56
|
+
* Did `next` lose a comment of the current text that was not inside `allowed`
|
|
57
|
+
* (a range of the current text: what the edit may take with it)? The library
|
|
58
|
+
* drops comments around a removed entry it has no right to; a copy of the same
|
|
59
|
+
* text elsewhere does not stand in for a lost one.
|
|
60
|
+
*/
|
|
61
|
+
private losesComment;
|
|
62
|
+
/** Take `next` as the new text if it reads back as `expected` and loses no comment but those in `allowed`; else see the class comment. */
|
|
63
|
+
private commit;
|
|
64
|
+
/** The current value, read again after every edit. */
|
|
65
|
+
get data(): Record<string, unknown>;
|
|
66
|
+
/** Set `path` to `value`, creating what is missing on the way. */
|
|
67
|
+
set(path: JSONPath, value: unknown): void;
|
|
68
|
+
/** Remove the property or array item at `path`. A comment right next to it goes with it. */
|
|
69
|
+
remove(path: JSONPath): void;
|
|
70
|
+
/**
|
|
71
|
+
* What removing the entry at `path` may take with it: the entry itself and the
|
|
72
|
+
* comment at the end of its last line — not one above it, nor one after the
|
|
73
|
+
* closing bracket of its container, which are not this entry's.
|
|
74
|
+
*/
|
|
75
|
+
private removalZone;
|
|
76
|
+
/**
|
|
77
|
+
* The text without the lines of the entry at `path`, when it stands on lines of
|
|
78
|
+
* its own: its own comment and comma go with it, and so does the comma before it
|
|
79
|
+
* when it was the last one. A comment on another line, or on the line of the
|
|
80
|
+
* entry before, stays — the library would take the comment next to the entry
|
|
81
|
+
* along. An entry that shares its line with others goes through `withoutInline`.
|
|
82
|
+
*/
|
|
83
|
+
private withoutLines;
|
|
84
|
+
/** Add `value` at the end of the array at `path`, after the comment on the last item's line. */
|
|
85
|
+
append(path: JSONPath, value: unknown): void;
|
|
86
|
+
/**
|
|
87
|
+
* The library's insert can leave a comment of the user's on the line of the new
|
|
88
|
+
* entry, and an uninstall would take it away with the entry (it is the entry's
|
|
89
|
+
* "own" comment by position). A new entry must have no comment in its removal
|
|
90
|
+
* zone; otherwise fail now, naming the file, and leave the text alone.
|
|
91
|
+
*/
|
|
92
|
+
private refuseAdoptedComment;
|
|
93
|
+
/** `value` as JSON text in the file's own indentation unit. */
|
|
94
|
+
private render;
|
|
95
|
+
/**
|
|
96
|
+
* The text with `childText` put after the last child of an object or array, on a
|
|
97
|
+
* line of its own at the last child's indentation; null for an empty container
|
|
98
|
+
* or one whose last child shares its line with something else (the caller falls
|
|
99
|
+
* back). The library's insert would re-indent the siblings and move a comment at
|
|
100
|
+
* the end of the last child's line onto the new child.
|
|
101
|
+
*/
|
|
102
|
+
private insertLast;
|
|
103
|
+
/** The file's new text: its byte order mark kept, one line ending at the end. */
|
|
104
|
+
text(): string;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=jsonc-doc.d.ts.map
|