@indigoai-us/hq-cli 5.126.0 → 5.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +218 -0
- package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
- package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
- package/dist/command-catalog.generated.d.ts +245 -11
- package/dist/command-catalog.generated.js +323 -11
- package/dist/command-registration-plan.d.ts +1 -0
- package/dist/command-registration-plan.js +11 -2
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/lanes.d.ts +55 -16
- package/dist/commands/lanes.js +1187 -250
- package/dist/commands/mesh.js +254 -19
- package/dist/lib/core-utils/sentry-report.d.ts +2 -0
- package/dist/lib/core-utils/sentry-report.js +47 -7
- package/dist/lib/lanes/accounts.d.ts +67 -7
- package/dist/lib/lanes/accounts.js +233 -43
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +15 -0
- package/dist/lib/lanes/config.js +65 -8
- package/dist/lib/lanes/delivery.d.ts +151 -0
- package/dist/lib/lanes/delivery.js +354 -0
- package/dist/lib/lanes/dropbox.d.ts +96 -0
- package/dist/lib/lanes/dropbox.js +408 -0
- package/dist/lib/lanes/effort.d.ts +50 -0
- package/dist/lib/lanes/effort.js +109 -0
- package/dist/lib/lanes/errors.d.ts +2 -2
- package/dist/lib/lanes/hooks-install.d.ts +81 -0
- package/dist/lib/lanes/hooks-install.js +326 -0
- package/dist/lib/lanes/hq-root.d.ts +19 -0
- package/dist/lib/lanes/hq-root.js +28 -0
- package/dist/lib/lanes/index.d.ts +35 -5
- package/dist/lib/lanes/index.js +19 -4
- package/dist/lib/lanes/liveness.d.ts +7 -1
- package/dist/lib/lanes/liveness.js +64 -5
- package/dist/lib/lanes/machine-identity.d.ts +34 -0
- package/dist/lib/lanes/machine-identity.js +92 -0
- package/dist/lib/lanes/mesh-events.d.ts +109 -0
- package/dist/lib/lanes/mesh-events.js +275 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
- package/dist/lib/lanes/outpost-dispatch.js +90 -0
- package/dist/lib/lanes/questions.d.ts +75 -0
- package/dist/lib/lanes/questions.js +243 -0
- package/dist/lib/lanes/quota.d.ts +95 -0
- package/dist/lib/lanes/quota.js +320 -0
- package/dist/lib/lanes/reconcile-install.d.ts +69 -0
- package/dist/lib/lanes/reconcile-install.js +221 -0
- package/dist/lib/lanes/reconcile.d.ts +102 -0
- package/dist/lib/lanes/reconcile.js +985 -0
- package/dist/lib/lanes/registry.d.ts +18 -5
- package/dist/lib/lanes/registry.js +293 -19
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +7 -3
- package/dist/lib/lanes/store.js +150 -32
- package/dist/lib/lanes/types.d.ts +346 -4
- package/dist/lib/lanes/types.js +138 -5
- package/dist/lib/lanes/watcher-main.d.ts +9 -0
- package/dist/lib/lanes/watcher-main.js +22 -0
- package/dist/lib/lanes/watcher.d.ts +216 -0
- package/dist/lib/lanes/watcher.js +859 -0
- package/dist/lib/mesh/api.d.ts +71 -7
- package/dist/lib/mesh/api.js +167 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
- package/dist/lib/mesh/live/backfill-held.js +66 -21
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +111 -54
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
- package/dist/lib/scan-packages/index.d.ts +2 -0
- package/dist/lib/scan-packages/index.js +1 -1
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/package.json +3 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-lane watcher (spec §5.7).
|
|
3
|
+
*
|
|
4
|
+
* Armed at spawn, detached from the caller. Emits on progress, terminal, and
|
|
5
|
+
* silence (20 minutes with the process still alive). Terminal classification
|
|
6
|
+
* follows spec §5: an envelope decides done/ask/blocked; a process that exits
|
|
7
|
+
* with partial output and no envelope is `interrupted`; silence is never
|
|
8
|
+
* success.
|
|
9
|
+
*
|
|
10
|
+
* Progress has two sources. Output files (worker.out, worker.err,
|
|
11
|
+
* last-message.txt) cover codex and grok, which stream as they work. They do
|
|
12
|
+
* not cover Claude: `claude -p --output-format json` writes nothing until it
|
|
13
|
+
* exits, so a healthy Claude lane used to hit the silence deadline at exactly
|
|
14
|
+
* 20 minutes every time, and a hung one looked identical. The second source
|
|
15
|
+
* is logs/tool-activity.json, which the drain hook rewrites on every
|
|
16
|
+
* PreToolUse call (dropbox.ts). A worker that is working calls tools; a
|
|
17
|
+
* worker that has not called a tool or written output for the whole window
|
|
18
|
+
* is `stalled`, and the watcher says so on the lane record.
|
|
19
|
+
*
|
|
20
|
+
* What this cannot tell apart: a worker hung inside one tool call from a
|
|
21
|
+
* worker running one legitimately long tool call (a 25-minute build); a
|
|
22
|
+
* worker looping on tool calls without making progress from a working one;
|
|
23
|
+
* a model stuck mid-generation with no tool call from one that is writing a
|
|
24
|
+
* long answer. It also depends on the drain hook being registered in the
|
|
25
|
+
* provider home (hooks-install.ts): with no hook there is no touch, and the
|
|
26
|
+
* lane reads as stalled every window, which is the pre-existing behaviour.
|
|
27
|
+
* Distinguishing a hung tool from a slow one needs the provider to report
|
|
28
|
+
* per-call state, which none of the three CLIs expose today. `stalled` is
|
|
29
|
+
* therefore a flag for the senior to look at, never a kill.
|
|
30
|
+
*
|
|
31
|
+
* Queue admission of the next lane when a slot frees is owned by the
|
|
32
|
+
* periodic reconcile pass and is out of scope here.
|
|
33
|
+
*/
|
|
34
|
+
import { type LaneEnvelope, type LaneRecord, type LaneState } from "./types.js";
|
|
35
|
+
import { type QuotaSignal } from "./quota.js";
|
|
36
|
+
import { type LaneMesh } from "./mesh-events.js";
|
|
37
|
+
import { type EnvelopeRepairSpawnInput } from "./spawn.js";
|
|
38
|
+
/** Spec §5.7: no signal for 20 minutes while the process is still alive. */
|
|
39
|
+
export declare const SILENCE_MS: number;
|
|
40
|
+
export declare const WATCHER_POLL_MS = 5000;
|
|
41
|
+
/** Synced last_host_report_at cadence. Reconcile dead-host TTL is 20 minutes. */
|
|
42
|
+
export declare const WATCHER_HEARTBEAT_MS: number;
|
|
43
|
+
/**
|
|
44
|
+
* Cap on logs/watcher.out. A decision line is typically under 300 bytes, so
|
|
45
|
+
* 256 KiB holds hundreds of transitions. A stuck lane cannot fill the disk:
|
|
46
|
+
* once the file would exceed this, the oldest complete lines are dropped.
|
|
47
|
+
*/
|
|
48
|
+
export declare const WATCHER_OUT_MAX_BYTES: number;
|
|
49
|
+
export declare const WATCHER_DECISION_EVENTS: readonly ["liveness_transition", "silence_deadline", "envelope_observed", "envelope_invalid", "envelope_repair", "classification", "terminal_write", "mesh_event", "mesh_event_failed"];
|
|
50
|
+
/** Terminal reason when the envelope failed to parse after the repair turn (spec §4). */
|
|
51
|
+
export declare const ENVELOPE_INVALID_REASON: "envelope_invalid";
|
|
52
|
+
export type WatcherDecisionEvent = (typeof WATCHER_DECISION_EVENTS)[number];
|
|
53
|
+
export declare const WATCHER_SIGNAL_KINDS: readonly ["progress", "terminal", "silence"];
|
|
54
|
+
export type WatcherSignalKind = (typeof WATCHER_SIGNAL_KINDS)[number];
|
|
55
|
+
export interface TerminalClassification {
|
|
56
|
+
state: Extract<LaneState, "done" | "awaiting_input" | "blocked" | "interrupted" | "failed">;
|
|
57
|
+
reason?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface WatcherSignal {
|
|
60
|
+
kind: WatcherSignalKind;
|
|
61
|
+
at: string;
|
|
62
|
+
classification?: TerminalClassification;
|
|
63
|
+
}
|
|
64
|
+
export interface WatcherTickInput {
|
|
65
|
+
nowMs: number;
|
|
66
|
+
processAlive: boolean;
|
|
67
|
+
progressed: boolean;
|
|
68
|
+
lastSignalMs: number;
|
|
69
|
+
envelope: LaneEnvelope | null;
|
|
70
|
+
hasPartialOutput: boolean;
|
|
71
|
+
silenceMs?: number;
|
|
72
|
+
quota?: QuotaSignal | null;
|
|
73
|
+
invalidEnvelope?: {
|
|
74
|
+
errors: string[];
|
|
75
|
+
} | null;
|
|
76
|
+
}
|
|
77
|
+
export interface WatcherTickResult {
|
|
78
|
+
emit?: WatcherSignal;
|
|
79
|
+
nextLastSignalMs: number;
|
|
80
|
+
done: boolean;
|
|
81
|
+
}
|
|
82
|
+
export declare function parseWatcherSignalKind(value: unknown): WatcherSignalKind;
|
|
83
|
+
export declare function classifyLaneExit(input: {
|
|
84
|
+
envelope: LaneEnvelope | null;
|
|
85
|
+
hasPartialOutput: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Set when the worker's own result is a 429 or a usage-limit error.
|
|
88
|
+
* Distinct from a crash: the reason is account_quota_exhausted or
|
|
89
|
+
* account_model_unavailable, not exited_with_partial_output.
|
|
90
|
+
*/
|
|
91
|
+
quota?: QuotaSignal | null;
|
|
92
|
+
/**
|
|
93
|
+
* Set when the run's envelope file exists but does not parse and no
|
|
94
|
+
* repair turn remains. Failed, regardless of partial output: the worker
|
|
95
|
+
* did finish, it just did not honour the contract.
|
|
96
|
+
*/
|
|
97
|
+
invalidEnvelope?: {
|
|
98
|
+
errors: string[];
|
|
99
|
+
} | null;
|
|
100
|
+
}): TerminalClassification;
|
|
101
|
+
export declare function tickWatcher(input: WatcherTickInput): WatcherTickResult;
|
|
102
|
+
export declare function watcherEventsPath(hqRoot: string, laneId: string): string;
|
|
103
|
+
export declare function watcherOutLogPath(hqRoot: string, laneId: string): string;
|
|
104
|
+
export declare function appendCappedLog(filePath: string, line: string, maxBytes?: number): void;
|
|
105
|
+
export declare function writeWatcherDecision(filePath: string, record: Record<string, unknown>, maxBytes?: number): void;
|
|
106
|
+
export declare function watcherLastSignalPath(hqRoot: string, laneId: string): string;
|
|
107
|
+
export declare function appendWatcherSignal(hqRoot: string, laneId: string, signal: WatcherSignal): void;
|
|
108
|
+
/**
|
|
109
|
+
* Slot-free notification. The periodic reconcile pass admits the next queued
|
|
110
|
+
* lane; this watcher must not do it. Intentionally empty.
|
|
111
|
+
*/
|
|
112
|
+
export declare function notifySlotFreed(_input: {
|
|
113
|
+
hqRoot: string;
|
|
114
|
+
provider: LaneRecord["provider"];
|
|
115
|
+
account?: string;
|
|
116
|
+
}): void;
|
|
117
|
+
export interface ApplyTerminalOpts {
|
|
118
|
+
envelope?: LaneEnvelope | null;
|
|
119
|
+
envelopeRef?: string;
|
|
120
|
+
at?: string;
|
|
121
|
+
quota?: QuotaSignal | null;
|
|
122
|
+
}
|
|
123
|
+
export declare function stalledReason(silenceMs: number): string;
|
|
124
|
+
/**
|
|
125
|
+
* The consumer of the silence signal. A silence emit on a running lane sets
|
|
126
|
+
* `stalled` on the record so `hq lanes list` and `show` surface it; the next
|
|
127
|
+
* progress emit clears it. Nothing here changes `state`.
|
|
128
|
+
*/
|
|
129
|
+
export declare function markLaneStalled(hqRoot: string, laneId: string, at: string, reason: string): LaneRecord;
|
|
130
|
+
export declare function clearLaneStalled(hqRoot: string, laneId: string): LaneRecord;
|
|
131
|
+
export declare function applyTerminalToLane(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): LaneRecord;
|
|
132
|
+
/** Terminal write outcome for emitters: only the CAS winner may publish mesh effects. */
|
|
133
|
+
export declare function applyTerminalToLaneWithResult(hqRoot: string, laneId: string, classification: TerminalClassification, opts?: ApplyTerminalOpts): {
|
|
134
|
+
record: LaneRecord;
|
|
135
|
+
changed: boolean;
|
|
136
|
+
};
|
|
137
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
138
|
+
export interface LatestEnvelope {
|
|
139
|
+
envelope: LaneEnvelope;
|
|
140
|
+
ref: string;
|
|
141
|
+
}
|
|
142
|
+
/** The newest envelope file of this run, parsed or not. */
|
|
143
|
+
export interface LatestEnvelopeFile {
|
|
144
|
+
ref: string;
|
|
145
|
+
/** Absolute path of the file. */
|
|
146
|
+
path: string;
|
|
147
|
+
text: string;
|
|
148
|
+
envelope: LaneEnvelope | null;
|
|
149
|
+
/** Parser message when `envelope` is null. */
|
|
150
|
+
error: string | null;
|
|
151
|
+
}
|
|
152
|
+
export declare function readLatestEnvelope(hqRoot: string, laneId: string, minName?: string): LaneEnvelope | null;
|
|
153
|
+
/** Valid-only view over `readLatestEnvelopeFile`; an unparseable file reads as absent. */
|
|
154
|
+
export declare function readLatestEnvelopeRecord(hqRoot: string, laneId: string, minName?: string): LatestEnvelope | null;
|
|
155
|
+
/**
|
|
156
|
+
* `minName` is this run's envelope slot (basename from spawn.json). Envelopes
|
|
157
|
+
* that sort before it belong to an earlier run of the same lane and must not
|
|
158
|
+
* classify this exit: a resumed run that dies before writing its own envelope
|
|
159
|
+
* would otherwise be marked done again from the previous one.
|
|
160
|
+
*/
|
|
161
|
+
export declare function readLatestEnvelopeFile(hqRoot: string, laneId: string, minName?: string): LatestEnvelopeFile | null;
|
|
162
|
+
export interface WatcherLoopDeps {
|
|
163
|
+
sleep?: (ms: number) => Promise<void>;
|
|
164
|
+
now?: () => number;
|
|
165
|
+
isAlive?: (pid: number) => boolean;
|
|
166
|
+
readStarttime?: (pid: number) => number | null;
|
|
167
|
+
pollMs?: number;
|
|
168
|
+
silenceMs?: number;
|
|
169
|
+
watcherOutPath?: string;
|
|
170
|
+
watcherOutMaxBytes?: number;
|
|
171
|
+
/**
|
|
172
|
+
* Mesh seam (spec §8). Default: `createLaneMesh({ interactive: false })`.
|
|
173
|
+
* `null` disables emission entirely (tests that are not about mesh).
|
|
174
|
+
*/
|
|
175
|
+
mesh?: LaneMesh | null;
|
|
176
|
+
/** Override for `providers.<provider>.progress_event_min_interval_ms`. */
|
|
177
|
+
progressEventMinIntervalMs?: number;
|
|
178
|
+
/** Bound a hung mesh request so watcher liveness polling continues. */
|
|
179
|
+
meshRequestTimeoutMs?: number;
|
|
180
|
+
/** Spec §4 repair spawn. Default: `spawnEnvelopeRepair`. */
|
|
181
|
+
spawnRepair?: (hqRoot: string, lane: LaneRecord, input: EnvelopeRepairSpawnInput) => number;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
|
|
185
|
+
* the event name and the error message, and never propagates: the lane's
|
|
186
|
+
* state is decided by the envelope and the process, not by the network.
|
|
187
|
+
*/
|
|
188
|
+
export declare function emitMeshSoft(event: string, logDecision: (record: Record<string, unknown>) => void, at: string, fn: (signal: AbortSignal) => Promise<void>, timeoutMs?: number): Promise<boolean>;
|
|
189
|
+
/** Question ids an ask envelope raised, as `raiseEnvelopeQuestions` mints them. */
|
|
190
|
+
export declare function envelopeQuestionIds(laneId: string, envelopeRef: string, envelope: LaneEnvelope): string[];
|
|
191
|
+
export declare function runWatcherLoop(hqRoot: string, laneId: string, deps?: WatcherLoopDeps): Promise<void>;
|
|
192
|
+
export type EnvelopeRepairDecision = {
|
|
193
|
+
action: "spawned";
|
|
194
|
+
} | {
|
|
195
|
+
action: "give_up";
|
|
196
|
+
errors: string[];
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Spec §4 repair decision for an exited worker whose envelope file does not
|
|
200
|
+
* parse. First failure on a resumable provider with a recorded session id:
|
|
201
|
+
* record `envelope_repair` on the lane and spawn the repair turn. A provider
|
|
202
|
+
* with no session resume gets the same single attempt as a fresh turn. Anything
|
|
203
|
+
* else (already attempted, no session id on a resumable provider, spawn
|
|
204
|
+
* failure): give up with every error collected, and the caller classifies
|
|
205
|
+
* the lane `failed` with reason `envelope_invalid`.
|
|
206
|
+
*/
|
|
207
|
+
export declare function decideEnvelopeRepair(hqRoot: string, laneId: string, latest: LatestEnvelopeFile, nowMs: number, spawnRepair: NonNullable<WatcherLoopDeps["spawnRepair"]>, logDecision: (record: Record<string, unknown>) => void): EnvelopeRepairDecision;
|
|
208
|
+
/** Exported so tests can prove the watcher reads the live record, not spawn env. */
|
|
209
|
+
export declare function laneIdentityFromRecord(hqRoot: string, laneId: string): {
|
|
210
|
+
lane_id: string;
|
|
211
|
+
company: LaneRecord["company"];
|
|
212
|
+
project_id: string;
|
|
213
|
+
story_id: string;
|
|
214
|
+
senior: LaneRecord["senior"];
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=watcher.d.ts.map
|