@songsid/agend 2.1.6-beta.5 → 2.1.6-beta.7
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/muse.js +27 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +59 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +6 -0
- package/dist/channel/adapters/telegram.js +36 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +18 -0
- package/dist/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/connection-secrets.d.ts +96 -0
- package/dist/connection-secrets.js +25 -0
- package/dist/connection-secrets.js.map +1 -0
- package/dist/daemon.d.ts +29 -0
- package/dist/daemon.js +145 -77
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +180 -0
- package/dist/fleet-manager.js +1008 -9
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +14 -0
- package/dist/locale.js.map +1 -1
- package/dist/provider-probe.d.ts +3 -0
- package/dist/provider-probe.js +12 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/provider-secret-registry.d.ts +98 -0
- package/dist/provider-secret-registry.js +334 -0
- package/dist/provider-secret-registry.js.map +1 -0
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +14 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/secret-store.d.ts +25 -0
- package/dist/secret-store.js +165 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/settings-api.d.ts +101 -0
- package/dist/settings-api.js +404 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/settings.html +171 -6
- package/dist/usage/usage-api.d.ts +12 -0
- package/dist/usage/usage-api.js +39 -0
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -437,7 +437,23 @@ export class BackendUnreachableStartupError extends Error {
|
|
|
437
437
|
/** Bounded wait (under the pane lock) for the prompt to return before retrying a dropped Enter. */
|
|
438
438
|
const STRANDED_RETRY_READY_WAIT_MS = 30_000;
|
|
439
439
|
/** A passive startup phase must clear within this bound before an Enter is sent. */
|
|
440
|
-
|
|
440
|
+
/**
|
|
441
|
+
* How long a passive startup/resume transient may sit WITHOUT REPAINTING before
|
|
442
|
+
* delivery gives up. It is a stall budget, not a total: a CLI that is still
|
|
443
|
+
* painting its resume is making progress, and #826 is what happens when the two
|
|
444
|
+
* are confused — codex's resume is the only declared transient in the fleet
|
|
445
|
+
* (backend/codex.ts), a woken paused codex routinely takes longer than this to
|
|
446
|
+
* come back, and the flat 30s cap turned that into ❌ plus a discarded message
|
|
447
|
+
* on a pane that was seconds from ready.
|
|
448
|
+
*/
|
|
449
|
+
const INPUT_TRANSIENT_STALL_MS = 30_000;
|
|
450
|
+
/**
|
|
451
|
+
* The absolute ceiling for the same wait, so a transient that repaints forever
|
|
452
|
+
* (a spinner on a wedged resume) still ends in an honest failure. Bounded, as
|
|
453
|
+
* every other delivery wait is — just bounded by "no progress", not by a guess
|
|
454
|
+
* at how long a resume takes.
|
|
455
|
+
*/
|
|
456
|
+
const INPUT_TRANSIENT_WAIT_MS = 10 * 60_000;
|
|
441
457
|
const INPUT_TRANSIENT_POLL_MS = 250;
|
|
442
458
|
/** Max "stranded text → submit → wait for prompt → re-check" rounds per delivery; each may send one recovery Enter. */
|
|
443
459
|
const STRANDED_INPUT_MAX_ROUNDS = 3;
|
|
@@ -3834,6 +3850,28 @@ export class Daemon extends EventEmitter {
|
|
|
3834
3850
|
}
|
|
3835
3851
|
return formatted;
|
|
3836
3852
|
}
|
|
3853
|
+
/**
|
|
3854
|
+
* Whether ONE delivery reached a verdict, carried by that delivery.
|
|
3855
|
+
*
|
|
3856
|
+
* `deliverMessage` returns false for two unrelated things: a delivery that
|
|
3857
|
+
* cannot land (window gone, a pane that never came back, an unanswered
|
|
3858
|
+
* dialog) and a delivery that was never attempted (user cancel, a tmux storm,
|
|
3859
|
+
* shutdown). Only the first is a failure to tell the sender about; reporting
|
|
3860
|
+
* the second told a sending agent its message was lost while the fleet was
|
|
3861
|
+
* merely standing down (#826).
|
|
3862
|
+
*
|
|
3863
|
+
* It is per call and not a field on the daemon because deliveries do NOT all
|
|
3864
|
+
* share one lock: an ordinary delivery is serialised on `pasteLock`, a steer
|
|
3865
|
+
* on `steerLock`, and the two run concurrently by design. A field would let a
|
|
3866
|
+
* steer's verdict decide whether a queued message's sender is told its
|
|
3867
|
+
* delivery failed — a false ❌ carrying the wrong correlation id.
|
|
3868
|
+
*/
|
|
3869
|
+
failDelivery(verdict, status) {
|
|
3870
|
+
verdict.reached = true;
|
|
3871
|
+
if (status)
|
|
3872
|
+
this.emit("message_failed", status); // ❌
|
|
3873
|
+
return false;
|
|
3874
|
+
}
|
|
3837
3875
|
/** Tell the fleet when an already-accepted cross-instance pane write failed. */
|
|
3838
3876
|
reportCrossInstanceDeliveryFailure(meta, error) {
|
|
3839
3877
|
if (!meta.from_instance)
|
|
@@ -3889,10 +3927,14 @@ export class Daemon extends EventEmitter {
|
|
|
3889
3927
|
await this.wake();
|
|
3890
3928
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
3891
3929
|
return;
|
|
3892
|
-
|
|
3930
|
+
const verdict = { reached: false };
|
|
3931
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id, verdict })) {
|
|
3893
3932
|
this.markTurnStarted(meta, formatted);
|
|
3894
3933
|
}
|
|
3895
|
-
else if (this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3934
|
+
else if (verdict.reached && this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3935
|
+
// Same rule as the queued path: a steer that never got to try is not a
|
|
3936
|
+
// delivery failure. This holder is the steer's own, which is the point
|
|
3937
|
+
// — it runs on steerLock while a queued delivery runs on pasteLock.
|
|
3896
3938
|
this.reportCrossInstanceDeliveryFailure(meta);
|
|
3897
3939
|
}
|
|
3898
3940
|
}).catch(err => {
|
|
@@ -4022,10 +4064,16 @@ export class Daemon extends EventEmitter {
|
|
|
4022
4064
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
4023
4065
|
// the previous turn's tool list.
|
|
4024
4066
|
this.resetToolProgress();
|
|
4025
|
-
|
|
4067
|
+
const verdict = { reached: false };
|
|
4068
|
+
if (await this.deliverMessage(formatted, status, { deliveryEpoch, submissionId: meta.message_id, verdict })) {
|
|
4026
4069
|
this.markTurnStarted(meta, formatted);
|
|
4027
4070
|
}
|
|
4028
|
-
else if (meta.from_instance &&
|
|
4071
|
+
else if (meta.from_instance && verdict.reached
|
|
4072
|
+
&& this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
4073
|
+
// Only a delivery that reached a verdict is reported. A pane that is
|
|
4074
|
+
// not ready yet, a cancel, a storm hold or a shutdown all return
|
|
4075
|
+
// false without one, and telling the sender its message was lost
|
|
4076
|
+
// there would be the false ❌ of #826 in its other form.
|
|
4029
4077
|
this.reportCrossInstanceDeliveryFailure(meta);
|
|
4030
4078
|
}
|
|
4031
4079
|
}
|
|
@@ -4057,6 +4105,9 @@ export class Daemon extends EventEmitter {
|
|
|
4057
4105
|
* serial in both cases so separate PTY writes can never overlap.
|
|
4058
4106
|
*/
|
|
4059
4107
|
async deliverMessage(formatted, status, opts) {
|
|
4108
|
+
// The caller passes its own holder when it needs the answer; a system paste
|
|
4109
|
+
// that ignores the outcome gets a throwaway.
|
|
4110
|
+
const verdict = opts?.verdict ?? { reached: false };
|
|
4060
4111
|
const cancelled = () => opts?.deliveryEpoch !== undefined
|
|
4061
4112
|
&& !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
|
|
4062
4113
|
if (cancelled() || this.stormWindow?.isStopped())
|
|
@@ -4080,7 +4131,7 @@ export class Daemon extends EventEmitter {
|
|
|
4080
4131
|
await this.waitForSpawnToSettle();
|
|
4081
4132
|
if (cancelled())
|
|
4082
4133
|
return false;
|
|
4083
|
-
if (this.refuseFatalStartupDelivery(status))
|
|
4134
|
+
if (this.refuseFatalStartupDelivery(verdict, status))
|
|
4084
4135
|
return false;
|
|
4085
4136
|
let windowId = this.getWindowId();
|
|
4086
4137
|
const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
|
|
@@ -4122,9 +4173,7 @@ export class Daemon extends EventEmitter {
|
|
|
4122
4173
|
// wedged CLI (where the text would sit unsubmitted and the next message
|
|
4123
4174
|
// would land on top of it) — and instead of holding the queue silently.
|
|
4124
4175
|
this.logger.error("Pane still busy after the idle wait — reporting delivery failure");
|
|
4125
|
-
|
|
4126
|
-
this.emit("message_failed", status); // ❌
|
|
4127
|
-
return false;
|
|
4176
|
+
return this.failDelivery(verdict, status);
|
|
4128
4177
|
}
|
|
4129
4178
|
}
|
|
4130
4179
|
}
|
|
@@ -4145,9 +4194,7 @@ export class Daemon extends EventEmitter {
|
|
|
4145
4194
|
// recovery Enter, so exactly STRANDED_INPUT_MAX_ROUNDS of them go out.
|
|
4146
4195
|
if (round >= STRANDED_INPUT_MAX_ROUNDS) {
|
|
4147
4196
|
this.logger.error({ round }, "Input row still not clear after the stranded-text recovery budget — reporting delivery failure");
|
|
4148
|
-
|
|
4149
|
-
this.emit("message_failed", status); // ❌
|
|
4150
|
-
return false;
|
|
4197
|
+
return this.failDelivery(verdict, status);
|
|
4151
4198
|
}
|
|
4152
4199
|
const stranded = await this.submitStrandedInputIfAny(windowId);
|
|
4153
4200
|
if (cancelled())
|
|
@@ -4156,18 +4203,14 @@ export class Daemon extends EventEmitter {
|
|
|
4156
4203
|
break;
|
|
4157
4204
|
if (stranded === "failed") {
|
|
4158
4205
|
this.logger.error({ round }, "Could not submit the stranded text — reporting delivery failure");
|
|
4159
|
-
|
|
4160
|
-
this.emit("message_failed", status); // ❌
|
|
4161
|
-
return false;
|
|
4206
|
+
return this.failDelivery(verdict, status);
|
|
4162
4207
|
}
|
|
4163
4208
|
const readyAgain = await this.waitForPaneReadyForDelivery(windowId);
|
|
4164
4209
|
if (cancelled())
|
|
4165
4210
|
return false;
|
|
4166
4211
|
if (!readyAgain) {
|
|
4167
4212
|
this.logger.error("Pane never returned to its prompt after submitting stranded input — reporting delivery failure");
|
|
4168
|
-
|
|
4169
|
-
this.emit("message_failed", status); // ❌
|
|
4170
|
-
return false;
|
|
4213
|
+
return this.failDelivery(verdict, status);
|
|
4171
4214
|
}
|
|
4172
4215
|
}
|
|
4173
4216
|
}
|
|
@@ -4179,15 +4222,13 @@ export class Daemon extends EventEmitter {
|
|
|
4179
4222
|
const outcome = await this.paneWriteLock.run(async () => {
|
|
4180
4223
|
if (cancelled())
|
|
4181
4224
|
return false;
|
|
4182
|
-
if (this.refuseFatalStartupDelivery(status))
|
|
4225
|
+
if (this.refuseFatalStartupDelivery(verdict, status))
|
|
4183
4226
|
return false;
|
|
4184
4227
|
// A passive startup phase may paint after the outer readiness probe.
|
|
4185
4228
|
// It clears without input, so waiting under the pane lock cannot starve
|
|
4186
4229
|
// a dialog dismisser and closes the final clear→paste TOCTOU window.
|
|
4187
4230
|
if (!(await this.waitForInputTransientToClear("pre-write"))) {
|
|
4188
|
-
|
|
4189
|
-
this.emit("message_failed", status); // ❌
|
|
4190
|
-
return false;
|
|
4231
|
+
return this.failDelivery(verdict, status);
|
|
4191
4232
|
}
|
|
4192
4233
|
// TOCTOU: the probes above ran outside this lock, and the CLI repaints
|
|
4193
4234
|
// whenever it likes — a resume prompt can be painted between "clear" and
|
|
@@ -4198,7 +4239,7 @@ export class Daemon extends EventEmitter {
|
|
|
4198
4239
|
if (probe.state !== "clear")
|
|
4199
4240
|
return "dialog";
|
|
4200
4241
|
}
|
|
4201
|
-
return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId);
|
|
4242
|
+
return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict);
|
|
4202
4243
|
});
|
|
4203
4244
|
if (outcome !== "dialog")
|
|
4204
4245
|
return outcome;
|
|
@@ -4207,18 +4248,14 @@ export class Daemon extends EventEmitter {
|
|
|
4207
4248
|
// an honest failure rather than a message that never lands.
|
|
4208
4249
|
if (round + 1 >= LATE_DIALOG_WRITE_ROUNDS) {
|
|
4209
4250
|
this.logger.error({ rounds: round + 1 }, "A dialog kept appearing before the pane write — reporting delivery failure");
|
|
4210
|
-
|
|
4211
|
-
this.emit("message_failed", status); // ❌
|
|
4212
|
-
return false;
|
|
4251
|
+
return this.failDelivery(verdict, status);
|
|
4213
4252
|
}
|
|
4214
4253
|
this.logger.info("Dialog appeared before the pane write — waiting for it to clear");
|
|
4215
4254
|
const clear = gateWindowId ? await this.waitForPaneReadyForDelivery(gateWindowId) : false;
|
|
4216
4255
|
if (cancelled())
|
|
4217
4256
|
return false;
|
|
4218
4257
|
if (!clear) {
|
|
4219
|
-
|
|
4220
|
-
this.emit("message_failed", status); // ❌
|
|
4221
|
-
return false;
|
|
4258
|
+
return this.failDelivery(verdict, status);
|
|
4222
4259
|
}
|
|
4223
4260
|
}
|
|
4224
4261
|
}
|
|
@@ -4253,13 +4290,47 @@ export class Daemon extends EventEmitter {
|
|
|
4253
4290
|
return null;
|
|
4254
4291
|
}
|
|
4255
4292
|
/** Capture failure is unknown, never evidence that input is available. */
|
|
4293
|
+
/**
|
|
4294
|
+
* The budget for waiting out a passive transient, measured in progress rather
|
|
4295
|
+
* than in wall clock.
|
|
4296
|
+
*
|
|
4297
|
+
* `observe(pane)` answers "may I keep waiting?". The stall timer restarts
|
|
4298
|
+
* every time the pane text changes, so a resume that is still painting never
|
|
4299
|
+
* runs out; a screen that has not changed for INPUT_TRANSIENT_STALL_MS, or a
|
|
4300
|
+
* transient that outlives the ceiling, does. Both ends stay bounded — the
|
|
4301
|
+
* point of #826 was never that the wait should be unlimited, it was that a
|
|
4302
|
+
* flat 30s could not tell "slow" from "stuck".
|
|
4303
|
+
*/
|
|
4304
|
+
transientProgressBudget(overallDeadline) {
|
|
4305
|
+
const ceiling = Math.min(overallDeadline, Date.now() + INPUT_TRANSIENT_WAIT_MS);
|
|
4306
|
+
let lastPane = null;
|
|
4307
|
+
let stallDeadline = 0;
|
|
4308
|
+
const budget = {
|
|
4309
|
+
stalled: false,
|
|
4310
|
+
observe(pane) {
|
|
4311
|
+
const now = Date.now();
|
|
4312
|
+
if (pane !== lastPane) {
|
|
4313
|
+
lastPane = pane;
|
|
4314
|
+
stallDeadline = now + INPUT_TRANSIENT_STALL_MS;
|
|
4315
|
+
}
|
|
4316
|
+
if (now >= ceiling)
|
|
4317
|
+
return false;
|
|
4318
|
+
budget.stalled = now >= stallDeadline;
|
|
4319
|
+
return !budget.stalled;
|
|
4320
|
+
},
|
|
4321
|
+
};
|
|
4322
|
+
return budget;
|
|
4323
|
+
}
|
|
4256
4324
|
async probeInputTransient() {
|
|
4257
4325
|
if (this.guardedInputTransients().length === 0 || !this.tmux)
|
|
4258
4326
|
return { state: "clear" };
|
|
4259
4327
|
try {
|
|
4260
4328
|
const pane = await this.tmux.capturePane();
|
|
4261
4329
|
const transient = this.inputTransientInPane(pane);
|
|
4262
|
-
|
|
4330
|
+
// The pane text comes back with the verdict: a transient that is still
|
|
4331
|
+
// repainting is still working, and that is the only thing separating a
|
|
4332
|
+
// slow resume from a wedged one.
|
|
4333
|
+
return transient ? { state: "active", transient, pane } : { state: "clear" };
|
|
4263
4334
|
}
|
|
4264
4335
|
catch (err) {
|
|
4265
4336
|
this.logger.debug({ err }, "capture-pane failed during the input-transient probe — pane state unknown");
|
|
@@ -4275,7 +4346,12 @@ export class Daemon extends EventEmitter {
|
|
|
4275
4346
|
const generation = this.inputTransientGuardGeneration;
|
|
4276
4347
|
if (generation === null || generation !== this.spawnGeneration)
|
|
4277
4348
|
return true;
|
|
4278
|
-
|
|
4349
|
+
// Same progress rule as the outer readiness wait, for the same reason: this
|
|
4350
|
+
// runs under the pane write lock, so a flat cap here turned a resume that
|
|
4351
|
+
// repainted one second too late into a discarded message. Holding the lock
|
|
4352
|
+
// while the pane is still painting costs nothing — every other writer is
|
|
4353
|
+
// waiting on the same screen.
|
|
4354
|
+
const budget = this.transientProgressBudget(Date.now() + timeoutMs);
|
|
4279
4355
|
let observedDescription = null;
|
|
4280
4356
|
for (;;) {
|
|
4281
4357
|
if (this.inputTransientGuardGeneration !== generation || this.spawnGeneration !== generation) {
|
|
@@ -4293,12 +4369,18 @@ export class Daemon extends EventEmitter {
|
|
|
4293
4369
|
observedDescription = probe.transient.description;
|
|
4294
4370
|
this.logger.info({ phase, transient: probe.transient.description, generation }, "CLI is still completing startup — waiting before sending Enter");
|
|
4295
4371
|
}
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4372
|
+
// "unknown" (capture-pane failed) has to consume the budget too, or an
|
|
4373
|
+
// unreadable pane would loop forever now that the flat deadline is gone.
|
|
4374
|
+
// It never "changes", so it stalls out on the same rule.
|
|
4375
|
+
if (!budget.observe(probe.state === "active" ? probe.pane : "\u0000unreadable")) {
|
|
4376
|
+
this.logger.error({
|
|
4377
|
+
phase, generation, transient: observedDescription,
|
|
4378
|
+
stalledForMs: budget.stalled ? INPUT_TRANSIENT_STALL_MS : undefined,
|
|
4379
|
+
ceilingMs: timeoutMs,
|
|
4380
|
+
}, "CLI input stayed unavailable — refusing to send Enter");
|
|
4299
4381
|
return false;
|
|
4300
4382
|
}
|
|
4301
|
-
await new Promise(r => setTimeout(r,
|
|
4383
|
+
await new Promise(r => setTimeout(r, INPUT_TRANSIENT_POLL_MS));
|
|
4302
4384
|
}
|
|
4303
4385
|
}
|
|
4304
4386
|
static dialogKey(dialog) {
|
|
@@ -4473,7 +4555,7 @@ export class Daemon extends EventEmitter {
|
|
|
4473
4555
|
const deadline = Date.now() + timeoutMs;
|
|
4474
4556
|
const bottomGated = this.backend?.dropsEnterWhileBusy?.() === true;
|
|
4475
4557
|
let unknownStreak = 0;
|
|
4476
|
-
let
|
|
4558
|
+
let transientBudget = null;
|
|
4477
4559
|
for (;;) {
|
|
4478
4560
|
const remaining = deadline - Date.now();
|
|
4479
4561
|
if (remaining <= 0)
|
|
@@ -4486,15 +4568,19 @@ export class Daemon extends EventEmitter {
|
|
|
4486
4568
|
const transient = await this.probeInputTransient();
|
|
4487
4569
|
if (transient.state === "active") {
|
|
4488
4570
|
unknownStreak = 0;
|
|
4489
|
-
|
|
4490
|
-
if (
|
|
4491
|
-
this.logger.error({
|
|
4571
|
+
transientBudget ||= this.transientProgressBudget(deadline);
|
|
4572
|
+
if (!transientBudget.observe(transient.pane)) {
|
|
4573
|
+
this.logger.error({
|
|
4574
|
+
transient: transient.transient.description,
|
|
4575
|
+
stalledForMs: transientBudget.stalled ? INPUT_TRANSIENT_STALL_MS : undefined,
|
|
4576
|
+
ceilingMs: INPUT_TRANSIENT_WAIT_MS,
|
|
4577
|
+
}, "CLI input stayed unavailable during the delivery-readiness wait");
|
|
4492
4578
|
return false;
|
|
4493
4579
|
}
|
|
4494
4580
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
4495
4581
|
continue;
|
|
4496
4582
|
}
|
|
4497
|
-
|
|
4583
|
+
transientBudget = null;
|
|
4498
4584
|
if (transient.state === "unknown") {
|
|
4499
4585
|
if (++unknownStreak >= DIALOG_PROBE_UNKNOWN_MAX) {
|
|
4500
4586
|
this.logger.error({ probes: unknownStreak }, "Pane stayed unreadable during the input-transient probe — refusing to deliver blind");
|
|
@@ -4640,12 +4726,11 @@ export class Daemon extends EventEmitter {
|
|
|
4640
4726
|
* what to fix, and holding would only build a queue that floods the pane the
|
|
4641
4727
|
* moment the flag clears.
|
|
4642
4728
|
*/
|
|
4643
|
-
refuseFatalStartupDelivery(status) {
|
|
4729
|
+
refuseFatalStartupDelivery(verdict, status) {
|
|
4644
4730
|
if (!this.fatalStartupBlocked)
|
|
4645
4731
|
return false;
|
|
4646
4732
|
this.logger.error("Delivery refused — CLI is parked on a fatal startup screen");
|
|
4647
|
-
|
|
4648
|
-
this.emit("message_failed", status); // ❌
|
|
4733
|
+
this.failDelivery(verdict, status);
|
|
4649
4734
|
return true;
|
|
4650
4735
|
}
|
|
4651
4736
|
/**
|
|
@@ -4724,7 +4809,10 @@ export class Daemon extends EventEmitter {
|
|
|
4724
4809
|
}
|
|
4725
4810
|
return sent;
|
|
4726
4811
|
}
|
|
4727
|
-
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId
|
|
4812
|
+
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
|
|
4813
|
+
// Last and defaulted so the positional callers that ignore the outcome stay
|
|
4814
|
+
// readable; deliverMessage, the only one that reports, always passes its own.
|
|
4815
|
+
verdict = { reached: false }) {
|
|
4728
4816
|
const signature = this.submissionSignature(formatted, submissionId);
|
|
4729
4817
|
let windowId = initialWindowId;
|
|
4730
4818
|
// Bug A: paste with backoff. Transient failures are usually a stale window id
|
|
@@ -4743,9 +4831,9 @@ export class Daemon extends EventEmitter {
|
|
|
4743
4831
|
? "pasteBuffer failed — recovering window and backing off"
|
|
4744
4832
|
: "pasteBuffer failed — non-retryable tmux error");
|
|
4745
4833
|
if (!recoverable) {
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
return
|
|
4834
|
+
// A tmux error the window cannot be recovered from: the text will
|
|
4835
|
+
// never reach this pane, so it is a verdict like the others.
|
|
4836
|
+
return this.failDelivery(verdict, status);
|
|
4749
4837
|
}
|
|
4750
4838
|
windowId = (await this.recoverWindow()) ?? windowId;
|
|
4751
4839
|
if (attempt < maxAttempts)
|
|
@@ -4775,9 +4863,7 @@ export class Daemon extends EventEmitter {
|
|
|
4775
4863
|
}
|
|
4776
4864
|
let enterAt = Date.now();
|
|
4777
4865
|
if (!(await this.sendDeliveryEnter("initial-submit"))) {
|
|
4778
|
-
|
|
4779
|
-
this.emit("message_failed", status); // ❌
|
|
4780
|
-
return false;
|
|
4866
|
+
return this.failDelivery(verdict, status);
|
|
4781
4867
|
}
|
|
4782
4868
|
// Kiro's legacy TUI can swallow Enter while it is still processing a large
|
|
4783
4869
|
// paste — not only during the post-ready redraw (#479): on slower hosts it
|
|
@@ -4860,9 +4946,7 @@ export class Daemon extends EventEmitter {
|
|
|
4860
4946
|
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
4861
4947
|
const strandedAt = Date.now();
|
|
4862
4948
|
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
4863
|
-
|
|
4864
|
-
this.emit("message_failed", status); // ❌
|
|
4865
|
-
return false;
|
|
4949
|
+
return this.failDelivery(verdict, status);
|
|
4866
4950
|
}
|
|
4867
4951
|
const afterEnter = await this.confirmSubmitted(signature, pasteBaseline);
|
|
4868
4952
|
if (afterEnter === "submitted") {
|
|
@@ -4877,9 +4961,7 @@ export class Daemon extends EventEmitter {
|
|
|
4877
4961
|
// message nobody submitted. Output is corroboration; text sitting in
|
|
4878
4962
|
// the input row is disqualifying, and disqualifying evidence wins.
|
|
4879
4963
|
this.logger.error({ afterEnter, strandedAt }, "Stranded message could not be submitted by Enter");
|
|
4880
|
-
|
|
4881
|
-
this.emit("message_failed", status); // ❌
|
|
4882
|
-
return false;
|
|
4964
|
+
return this.failDelivery(verdict, status);
|
|
4883
4965
|
}
|
|
4884
4966
|
// "unproven": nothing of ours is on screen — the paste itself was lost,
|
|
4885
4967
|
// so pasting it again cannot duplicate anything.
|
|
@@ -4888,16 +4970,12 @@ export class Daemon extends EventEmitter {
|
|
|
4888
4970
|
this.logger.error({
|
|
4889
4971
|
tmuxError: this.tmux.getLastPasteError?.() ?? "unknown tmux paste failure",
|
|
4890
4972
|
}, "Idle-gated redelivery paste failed after native-queue silent loss");
|
|
4891
|
-
|
|
4892
|
-
this.emit("message_failed", status); // ❌
|
|
4893
|
-
return false;
|
|
4973
|
+
return this.failDelivery(verdict, status);
|
|
4894
4974
|
}
|
|
4895
4975
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
4896
4976
|
const retryAt = Date.now();
|
|
4897
4977
|
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
|
|
4898
|
-
|
|
4899
|
-
this.emit("message_failed", status); // ❌
|
|
4900
|
-
return false;
|
|
4978
|
+
return this.failDelivery(verdict, status);
|
|
4901
4979
|
}
|
|
4902
4980
|
if (windowId && this.controlClient) {
|
|
4903
4981
|
if (await this.confirmAfterEnter(windowId, retryAt, signature, pasteBaseline, "native-queue-idle-redelivery-retry")) {
|
|
@@ -4912,9 +4990,7 @@ export class Daemon extends EventEmitter {
|
|
|
4912
4990
|
return true;
|
|
4913
4991
|
}
|
|
4914
4992
|
this.logger.error("Idle-gated redelivery also failed after native-queue silent loss");
|
|
4915
|
-
|
|
4916
|
-
this.emit("message_failed", status); // ❌
|
|
4917
|
-
return false;
|
|
4993
|
+
return this.failDelivery(verdict, status);
|
|
4918
4994
|
}
|
|
4919
4995
|
if (windowId && this.controlClient && this.backend?.dropsEnterWhileBusy?.() === true) {
|
|
4920
4996
|
// F2: output after Enter is necessary but not sufficient — the paste
|
|
@@ -4928,9 +5004,7 @@ export class Daemon extends EventEmitter {
|
|
|
4928
5004
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
4929
5005
|
const retryAt = Date.now();
|
|
4930
5006
|
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
|
|
4931
|
-
|
|
4932
|
-
this.emit("message_failed", status); // ❌
|
|
4933
|
-
return false;
|
|
5007
|
+
return this.failDelivery(verdict, status);
|
|
4934
5008
|
}
|
|
4935
5009
|
submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
|
|
4936
5010
|
}
|
|
@@ -4940,9 +5014,7 @@ export class Daemon extends EventEmitter {
|
|
|
4940
5014
|
}
|
|
4941
5015
|
else {
|
|
4942
5016
|
this.logger.error("Message pasted but never submitted (text still in the input row after Enter retry)");
|
|
4943
|
-
|
|
4944
|
-
this.emit("message_failed", status); // ❌
|
|
4945
|
-
return false;
|
|
5017
|
+
return this.failDelivery(verdict, status);
|
|
4946
5018
|
}
|
|
4947
5019
|
}
|
|
4948
5020
|
else if (windowId && this.controlClient) {
|
|
@@ -4967,9 +5039,7 @@ export class Daemon extends EventEmitter {
|
|
|
4967
5039
|
// forever and the next delivery pasted on top — submitting two messages
|
|
4968
5040
|
// as one. Say so instead.
|
|
4969
5041
|
this.logger.error("Message pasted but never submitted (no idle→busy after two Enters)");
|
|
4970
|
-
|
|
4971
|
-
this.emit("message_failed", status); // ❌
|
|
4972
|
-
return false;
|
|
5042
|
+
return this.failDelivery(verdict, status);
|
|
4973
5043
|
}
|
|
4974
5044
|
}
|
|
4975
5045
|
else {
|
|
@@ -4982,9 +5052,7 @@ export class Daemon extends EventEmitter {
|
|
|
4982
5052
|
return true;
|
|
4983
5053
|
}
|
|
4984
5054
|
this.logger.error("Message delivery failed after retries — window not ready");
|
|
4985
|
-
|
|
4986
|
-
this.emit("message_failed", status); // ❌
|
|
4987
|
-
return false;
|
|
5055
|
+
return this.failDelivery(verdict, status);
|
|
4988
5056
|
}
|
|
4989
5057
|
/**
|
|
4990
5058
|
* The single place a pasted message is judged submitted, shared by the
|