@songsid/agend 2.1.6-beta.7 → 2.1.6-beta.9
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/dist/backend/claude-code.d.ts +25 -0
- package/dist/backend/claude-code.js +103 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +17 -0
- package/dist/backend/codex.js +31 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +37 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/tool-router.js +20 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +101 -3
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +38 -1
- package/dist/fleet-manager.js +138 -34
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +6 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/usage/providers.d.ts +8 -6
- package/dist/usage/providers.js +83 -9
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +32 -16
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -896,6 +896,11 @@ export class Daemon extends EventEmitter {
|
|
|
896
896
|
/** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
|
|
897
897
|
dialogParkedKey = null;
|
|
898
898
|
dialogParkedReported = false;
|
|
899
|
+
/** Current stdin-owning runtime dialog, independent from execution state. */
|
|
900
|
+
inputBlockedDialogKey = null;
|
|
901
|
+
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
902
|
+
autoResolvedDialogKey = null;
|
|
903
|
+
autoResolvedDialogGeneration = 0;
|
|
899
904
|
backgroundSessionRecoveryAttempted = false;
|
|
900
905
|
/** Whether the last spawn started a fresh session (not resumed). */
|
|
901
906
|
isNewSession = false;
|
|
@@ -2186,6 +2191,7 @@ export class Daemon extends EventEmitter {
|
|
|
2186
2191
|
if (!alive)
|
|
2187
2192
|
return;
|
|
2188
2193
|
const pane = await this.tmux.capturePane();
|
|
2194
|
+
const inputBlockedDialog = this.updateInputBlockedState(pane, dialogs);
|
|
2189
2195
|
const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
|
|
2190
2196
|
if (interactivePrompt) {
|
|
2191
2197
|
this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
|
|
@@ -2202,7 +2208,7 @@ export class Daemon extends EventEmitter {
|
|
|
2202
2208
|
const hasPendingWork = this.pendingWork.hasPendingWork();
|
|
2203
2209
|
if (!hasPendingWork)
|
|
2204
2210
|
this.blockingProcessDetector.reset();
|
|
2205
|
-
const blockingProcess = hasPendingWork
|
|
2211
|
+
const blockingProcess = hasPendingWork && !inputBlockedDialog
|
|
2206
2212
|
? this.blockingProcessDetector.observe(pane, paneActivity, Date.now())
|
|
2207
2213
|
: null;
|
|
2208
2214
|
if (blockingProcess) {
|
|
@@ -2224,27 +2230,80 @@ export class Daemon extends EventEmitter {
|
|
|
2224
2230
|
blockingSeen = dialog;
|
|
2225
2231
|
if (dialog.holdOnly)
|
|
2226
2232
|
break; // recognised, deliberately not answered; trackDialogParked reports it
|
|
2233
|
+
const autoKey = dialog.autoResolutionKey;
|
|
2234
|
+
if (dialog.verifyAfterKeys && autoKey
|
|
2235
|
+
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
2236
|
+
&& this.autoResolvedDialogKey === autoKey) {
|
|
2237
|
+
// A previous poll sent the safety choice but the CLI has not
|
|
2238
|
+
// repainted yet. Never send another Enter into the same screen.
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2227
2241
|
// These keys go straight into the pane. Sent while a message delivery is
|
|
2228
2242
|
// mid-transaction, an `Escape` wipes the pasted text and an `Enter`
|
|
2229
2243
|
// submits it half-composed — the user sees a message that vanished. Skip
|
|
2230
2244
|
// (not queue) when the pane is busy: this poller runs every 5s, and the
|
|
2231
2245
|
// dialog will still be on screen next tick.
|
|
2246
|
+
let resolved = false;
|
|
2232
2247
|
const dismissed = await this.paneWriteLock.tryRun(async () => {
|
|
2248
|
+
// Re-read under the lock: the first capture may have gone stale
|
|
2249
|
+
// while a delivery was finishing. A stale danger menu must never
|
|
2250
|
+
// receive a blind key sequence.
|
|
2251
|
+
const currentPane = await this.tmux.capturePane();
|
|
2252
|
+
if (!Daemon.dialogMatches(dialog, currentPane)) {
|
|
2253
|
+
this.updateInputBlockedState(currentPane, dialogs);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (dialog.verifyAfterKeys && autoKey
|
|
2257
|
+
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
2258
|
+
&& this.autoResolvedDialogKey === autoKey)
|
|
2259
|
+
return;
|
|
2260
|
+
if (dialog.verifyAfterKeys && autoKey) {
|
|
2261
|
+
this.autoResolvedDialogGeneration = this.spawnGeneration;
|
|
2262
|
+
this.autoResolvedDialogKey = autoKey;
|
|
2263
|
+
}
|
|
2233
2264
|
this.logger.info(`Auto-dismissing runtime dialog: ${dialog.description}`);
|
|
2234
2265
|
const SPECIAL_KEYS = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
2235
2266
|
for (const key of dialog.keys) {
|
|
2267
|
+
let sent = false;
|
|
2236
2268
|
if (SPECIAL_KEYS.has(key)) {
|
|
2237
|
-
await this.tmux.sendSpecialKey(key);
|
|
2269
|
+
sent = await this.tmux.sendSpecialKey(key);
|
|
2238
2270
|
}
|
|
2239
2271
|
else {
|
|
2240
|
-
await this.tmux.pasteText(key, this.systemPasteOptions());
|
|
2272
|
+
sent = await this.tmux.pasteText(key, this.systemPasteOptions());
|
|
2273
|
+
}
|
|
2274
|
+
if (!sent) {
|
|
2275
|
+
if (dialog.verifyAfterKeys && autoKey) {
|
|
2276
|
+
this.autoResolvedDialogGeneration = 0;
|
|
2277
|
+
this.autoResolvedDialogKey = null;
|
|
2278
|
+
}
|
|
2279
|
+
return;
|
|
2241
2280
|
}
|
|
2242
2281
|
await new Promise(r => setTimeout(r, 200));
|
|
2243
2282
|
}
|
|
2283
|
+
if (dialog.verifyAfterKeys) {
|
|
2284
|
+
const afterKeysPane = await this.tmux.capturePane();
|
|
2285
|
+
const dialogStillActive = dialog.inputBlocked
|
|
2286
|
+
? dialogs.some(candidate => candidate.inputBlocked && Daemon.dialogMatches(candidate, afterKeysPane))
|
|
2287
|
+
: Daemon.dialogMatches(dialog, afterKeysPane);
|
|
2288
|
+
if (dialogStillActive) {
|
|
2289
|
+
this.logger.warn({ dialog: dialog.description }, "Runtime dialog remained after its safety choice");
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
this.updateInputBlockedState(afterKeysPane, dialogs);
|
|
2293
|
+
if (dialog.postDismissNotice) {
|
|
2294
|
+
resolved = await this.submitSystemPaste(dialog.postDismissNotice.text, dialog.postDismissNotice.label);
|
|
2295
|
+
}
|
|
2296
|
+
else {
|
|
2297
|
+
resolved = true;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2244
2300
|
});
|
|
2245
2301
|
if (!dismissed) {
|
|
2246
2302
|
this.logger.info({ dialog: dialog.description }, "Dialog dismissal deferred — pane write in flight");
|
|
2247
2303
|
}
|
|
2304
|
+
else if (dialog.verifyAfterKeys && !resolved) {
|
|
2305
|
+
this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
|
|
2306
|
+
}
|
|
2248
2307
|
this.trackDialogParked(blockingSeen);
|
|
2249
2308
|
return; // Dialog handled (or deliberately deferred): skip error checks this cycle
|
|
2250
2309
|
}
|
|
@@ -2842,6 +2901,15 @@ export class Daemon extends EventEmitter {
|
|
|
2842
2901
|
}
|
|
2843
2902
|
applyInstanceStateSnapshot(snapshot, pane) {
|
|
2844
2903
|
const previous = this.instanceState;
|
|
2904
|
+
// A live safety menu owns stdin. It may still contain Claude's persistent
|
|
2905
|
+
// `❯` ready marker, but that is not an idle turn and must not clear pending
|
|
2906
|
+
// work, retire Cancel, arm auto-pause, or enter the generic stuck path.
|
|
2907
|
+
// Keep the public execution state unchanged until the menu disappears; the
|
|
2908
|
+
// dedicated input_blocked event carries the reason to observers.
|
|
2909
|
+
if (this.inputBlockedDialogKey !== null) {
|
|
2910
|
+
this.logger.debug({ dialog: this.inputBlockedDialogKey }, "Suppressing execution-state edge while CLI input is blocked by a dialog");
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2845
2913
|
this.instanceState = snapshot.state;
|
|
2846
2914
|
// OpenCode creates its session lazily on the first submitted message.
|
|
2847
2915
|
// Waiting until stop/pause to persist that id loses resume state when the
|
|
@@ -3315,6 +3383,7 @@ export class Daemon extends EventEmitter {
|
|
|
3315
3383
|
const captureStartedAt = Date.now();
|
|
3316
3384
|
try {
|
|
3317
3385
|
const pane = await this.tmux.capturePane();
|
|
3386
|
+
this.updateInputBlockedState(pane);
|
|
3318
3387
|
// Output received while capture-pane was in flight makes this snapshot
|
|
3319
3388
|
// stale. Its output handler has already armed a new debounce.
|
|
3320
3389
|
const outputMovedDuringCapture = (expectedOutputAt > 0 && this.instanceStateLastOutputAt > expectedOutputAt)
|
|
@@ -3551,6 +3620,10 @@ export class Daemon extends EventEmitter {
|
|
|
3551
3620
|
this.instanceStateSafetyListener = null;
|
|
3552
3621
|
}
|
|
3553
3622
|
handleStuckTransition(pane, snapshot, readyPattern) {
|
|
3623
|
+
if (this.inputBlockedDialogKey !== null) {
|
|
3624
|
+
this.logger.debug({ dialog: this.inputBlockedDialogKey }, "Suppressing stuck notification while CLI input is blocked by a dialog");
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3554
3627
|
const deterministicReadyPattern = new RegExp(readyPattern.source, readyPattern.flags.replace(/[gy]/g, ""));
|
|
3555
3628
|
const diagnostic = {
|
|
3556
3629
|
backend: this.backend?.binaryName ?? this.config.backend ?? "unknown",
|
|
@@ -4388,8 +4461,33 @@ export class Daemon extends EventEmitter {
|
|
|
4388
4461
|
}
|
|
4389
4462
|
/** A dialog counts only when it is the CURRENT interactive region, if the backend can tell; else by pattern. */
|
|
4390
4463
|
static dialogMatches(dialog, pane) {
|
|
4464
|
+
dialog.pattern.lastIndex = 0;
|
|
4391
4465
|
return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
|
|
4392
4466
|
}
|
|
4467
|
+
/** Refresh the separate stdin-blocked observation from one pane capture. */
|
|
4468
|
+
updateInputBlockedState(pane, dialogs = this.backend?.getRuntimeDialogs?.() ?? []) {
|
|
4469
|
+
const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
|
|
4470
|
+
const nextKey = active ? Daemon.dialogKey(active) : null;
|
|
4471
|
+
if (nextKey !== this.inputBlockedDialogKey) {
|
|
4472
|
+
this.inputBlockedDialogKey = nextKey;
|
|
4473
|
+
this.emit("input_blocked", {
|
|
4474
|
+
name: this.name,
|
|
4475
|
+
blocked: active !== null,
|
|
4476
|
+
description: active?.description,
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
if (!active && this.autoResolvedDialogGeneration === this.spawnGeneration) {
|
|
4480
|
+
// The old screen is gone. A later dangerous command in the same spawn
|
|
4481
|
+
// must get its own one-shot answer.
|
|
4482
|
+
this.autoResolvedDialogKey = null;
|
|
4483
|
+
this.autoResolvedDialogGeneration = 0;
|
|
4484
|
+
}
|
|
4485
|
+
return active;
|
|
4486
|
+
}
|
|
4487
|
+
/** Whether a runtime prompt currently owns the pane's stdin. */
|
|
4488
|
+
isInputBlocked() {
|
|
4489
|
+
return this.inputBlockedDialogKey !== null;
|
|
4490
|
+
}
|
|
4393
4491
|
/**
|
|
4394
4492
|
* Tri-state liveness. tmux's own isWindowAlive folds every query failure
|
|
4395
4493
|
* into `false`, so a transient tmux hiccup would read as "the CLI died" —
|