@songsid/agend 2.1.5-beta.12 → 2.1.5-beta.13
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/codex.d.ts +23 -0
- package/dist/backend/codex.js +43 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.js +6 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +37 -4
- package/dist/daemon.js +280 -30
- package/dist/daemon.js.map +1 -1
- package/dist/pane-input-residue.d.ts +19 -0
- package/dist/pane-input-residue.js +23 -0
- package/dist/pane-input-residue.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -23,7 +23,7 @@ import { writeSecretFile } from "./secret-file.js";
|
|
|
23
23
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
24
24
|
import { buildFleetInstructions } from "./instructions.js";
|
|
25
25
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
26
|
-
import { bottomRowIsReady, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
26
|
+
import { bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
27
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
28
|
const __dirname = dirname(__filename);
|
|
29
29
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -386,6 +386,18 @@ export class PendingWorkTracker {
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
const NORMAL_ENTER_SETTLE_MS = 500;
|
|
389
|
+
/** Attempts to read the pre-paste pane before a delivery gives up on a baseline. */
|
|
390
|
+
const BASELINE_CAPTURE_ATTEMPTS = 3;
|
|
391
|
+
const BASELINE_CAPTURE_RETRY_MS = 150;
|
|
392
|
+
/** Occurrences of `needle` in `haystack` (plain text, no regex semantics). */
|
|
393
|
+
function countOccurrences(haystack, needle) {
|
|
394
|
+
if (!needle)
|
|
395
|
+
return 0;
|
|
396
|
+
let n = 0;
|
|
397
|
+
for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length))
|
|
398
|
+
n++;
|
|
399
|
+
return n;
|
|
400
|
+
}
|
|
389
401
|
/** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
|
|
390
402
|
const BOTTOM_READY_POLL_MS = 250;
|
|
391
403
|
/** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
|
|
@@ -3539,7 +3551,7 @@ export class Daemon extends EventEmitter {
|
|
|
3539
3551
|
await this.wake();
|
|
3540
3552
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
3541
3553
|
return;
|
|
3542
|
-
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch })) {
|
|
3554
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id })) {
|
|
3543
3555
|
this.markTurnStarted(meta, formatted);
|
|
3544
3556
|
}
|
|
3545
3557
|
else if (this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
@@ -3577,7 +3589,7 @@ export class Daemon extends EventEmitter {
|
|
|
3577
3589
|
await this.wake();
|
|
3578
3590
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
3579
3591
|
return;
|
|
3580
|
-
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch })) {
|
|
3592
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id })) {
|
|
3581
3593
|
this.markTurnStarted(meta, formatted);
|
|
3582
3594
|
}
|
|
3583
3595
|
}).catch(err => {
|
|
@@ -3673,7 +3685,7 @@ export class Daemon extends EventEmitter {
|
|
|
3673
3685
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
3674
3686
|
// the previous turn's tool list.
|
|
3675
3687
|
this.resetToolProgress();
|
|
3676
|
-
if (await this.deliverMessage(formatted, status, { deliveryEpoch })) {
|
|
3688
|
+
if (await this.deliverMessage(formatted, status, { deliveryEpoch, submissionId: meta.message_id })) {
|
|
3677
3689
|
this.markTurnStarted(meta, formatted);
|
|
3678
3690
|
}
|
|
3679
3691
|
else if (meta.from_instance && this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
@@ -3841,7 +3853,7 @@ export class Daemon extends EventEmitter {
|
|
|
3841
3853
|
if (probe.state !== "clear")
|
|
3842
3854
|
return "dialog";
|
|
3843
3855
|
}
|
|
3844
|
-
return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status);
|
|
3856
|
+
return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId);
|
|
3845
3857
|
});
|
|
3846
3858
|
if (outcome !== "dialog")
|
|
3847
3859
|
return outcome;
|
|
@@ -4250,13 +4262,17 @@ export class Daemon extends EventEmitter {
|
|
|
4250
4262
|
}
|
|
4251
4263
|
return sent;
|
|
4252
4264
|
}
|
|
4253
|
-
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status) {
|
|
4265
|
+
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId) {
|
|
4266
|
+
const signature = this.submissionSignature(formatted, submissionId);
|
|
4254
4267
|
let windowId = initialWindowId;
|
|
4255
4268
|
// Bug A: paste with backoff. Transient failures are usually a stale window id
|
|
4256
4269
|
// after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
|
|
4257
4270
|
const maxAttempts = 3;
|
|
4258
4271
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4259
4272
|
const pasteStartedAt = Date.now();
|
|
4273
|
+
// Read the pane BEFORE writing to it, so the submission check can require
|
|
4274
|
+
// evidence this paste ADDED rather than evidence that was already there.
|
|
4275
|
+
const pasteBaseline = await this.capturePaneEvidence(signature);
|
|
4260
4276
|
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
4261
4277
|
if (!pasted) {
|
|
4262
4278
|
const tmuxError = this.tmux.getLastPasteError?.() ?? "unknown tmux paste failure";
|
|
@@ -4338,16 +4354,70 @@ export class Daemon extends EventEmitter {
|
|
|
4338
4354
|
// can silently swallow it). Idle submissions keep the swallowed-Enter path.
|
|
4339
4355
|
if (handingOffToNativeQueue) {
|
|
4340
4356
|
await new Promise(r => setTimeout(r, NATIVE_QUEUE_PASTE_VERIFY_MS));
|
|
4341
|
-
|
|
4357
|
+
const proof = await this.confirmSubmitted(signature, pasteBaseline);
|
|
4358
|
+
if (proof === "submitted") {
|
|
4342
4359
|
if (status)
|
|
4343
4360
|
this.emit("message_confirmed", status); // ✅ native queue accepted
|
|
4344
4361
|
return true;
|
|
4345
4362
|
}
|
|
4346
|
-
|
|
4347
|
-
|
|
4363
|
+
if (proof === "unverifiable") {
|
|
4364
|
+
// Our text is on the pane but cannot be shown to have left the input
|
|
4365
|
+
// row — either the backend exposes no input row (only /steer reaches
|
|
4366
|
+
// this path on such a backend) or the pre-paste pane was unreadable.
|
|
4367
|
+
// Re-pasting on that would deliver the message twice, so accept as
|
|
4368
|
+
// this path always has, and record that it is unproven.
|
|
4369
|
+
this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
|
|
4370
|
+
if (status)
|
|
4371
|
+
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
4372
|
+
return true;
|
|
4373
|
+
}
|
|
4374
|
+
// Not submitted (or not provably submitted): fall back once to the
|
|
4375
|
+
// normal idle-gated path, which is the only one with a full
|
|
4376
|
+
// confirmation ladder. "stranded" is the case that used to be a silent
|
|
4377
|
+
// ✅ — the text was visible, so the old check called it delivered.
|
|
4378
|
+
this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
|
|
4348
4379
|
if (windowId && this.controlClient) {
|
|
4349
4380
|
await this.controlClient.waitUntilIdle(windowId);
|
|
4350
4381
|
}
|
|
4382
|
+
// Re-read before doing anything: the CLI had a whole turn to flush its
|
|
4383
|
+
// queue while we waited, and the right recovery differs per outcome.
|
|
4384
|
+
// Re-pasting a payload that is STILL in the input row appends it to
|
|
4385
|
+
// itself — paste-buffer writes at the cursor — and the next Enter then
|
|
4386
|
+
// submits the message twice over. That is the trap this fix would walk
|
|
4387
|
+
// into if it treated "not confirmed" as one state.
|
|
4388
|
+
const settled = await this.confirmSubmitted(signature, pasteBaseline);
|
|
4389
|
+
if (settled === "submitted") {
|
|
4390
|
+
this.logger.info("Native-queue message submitted itself while we waited for idle");
|
|
4391
|
+
if (status)
|
|
4392
|
+
this.emit("message_confirmed", status); // ✅
|
|
4393
|
+
return true;
|
|
4394
|
+
}
|
|
4395
|
+
if (settled === "stranded") {
|
|
4396
|
+
// The text is already there in full: it needs submitting, not sending
|
|
4397
|
+
// again. The prompt is back now, so an Enter can land.
|
|
4398
|
+
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
4399
|
+
const strandedAt = Date.now();
|
|
4400
|
+
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
4401
|
+
if (status)
|
|
4402
|
+
this.emit("message_failed", status); // ❌
|
|
4403
|
+
return false;
|
|
4404
|
+
}
|
|
4405
|
+
const afterEnter = await this.confirmSubmitted(signature, pasteBaseline);
|
|
4406
|
+
const turnStarted = windowId && this.controlClient
|
|
4407
|
+
? await this.confirmBusyAfterEnter(windowId, strandedAt)
|
|
4408
|
+
: false;
|
|
4409
|
+
if (afterEnter === "submitted" || turnStarted) {
|
|
4410
|
+
if (status)
|
|
4411
|
+
this.emit("message_confirmed", status); // ✅
|
|
4412
|
+
return true;
|
|
4413
|
+
}
|
|
4414
|
+
this.logger.error("Stranded message could not be submitted by Enter");
|
|
4415
|
+
if (status)
|
|
4416
|
+
this.emit("message_failed", status); // ❌
|
|
4417
|
+
return false;
|
|
4418
|
+
}
|
|
4419
|
+
// "unproven": nothing of ours is on screen — the paste itself was lost,
|
|
4420
|
+
// so pasting it again cannot duplicate anything.
|
|
4351
4421
|
const repasted = await this.tmux.pasteBuffer(formatted);
|
|
4352
4422
|
if (!repasted) {
|
|
4353
4423
|
this.logger.error({
|
|
@@ -4382,7 +4452,7 @@ export class Daemon extends EventEmitter {
|
|
|
4382
4452
|
return true;
|
|
4383
4453
|
}
|
|
4384
4454
|
}
|
|
4385
|
-
else if (await this.
|
|
4455
|
+
else if (await this.confirmSubmitted(signature, pasteBaseline) === "submitted") {
|
|
4386
4456
|
if (status)
|
|
4387
4457
|
this.emit("message_confirmed", status); // ✅
|
|
4388
4458
|
return true;
|
|
@@ -4463,31 +4533,195 @@ export class Daemon extends EventEmitter {
|
|
|
4463
4533
|
return false;
|
|
4464
4534
|
}
|
|
4465
4535
|
/**
|
|
4466
|
-
*
|
|
4467
|
-
*
|
|
4468
|
-
*
|
|
4536
|
+
* The single place a pasted message is judged submitted, shared by the
|
|
4537
|
+
* native-queue handoff, the idle-gated redelivery and the system pastes.
|
|
4538
|
+
*
|
|
4539
|
+
* It replaces three divergent weak checks, the weakest of which accepted "the
|
|
4540
|
+
* pasted text is visible somewhere in the pane". That one was satisfied BY
|
|
4541
|
+
* the failure it was meant to catch: text stranded in the input row is on
|
|
4542
|
+
* screen, so a message nobody submitted was confirmed ✅ with no warning
|
|
4543
|
+
* anywhere. Evidence is weighed in order of what it can prove, and the
|
|
4544
|
+
* disqualifying evidence is checked first.
|
|
4469
4545
|
*/
|
|
4470
|
-
async
|
|
4546
|
+
async confirmSubmitted(signature, baseline) {
|
|
4471
4547
|
if (!this.tmux)
|
|
4472
|
-
return
|
|
4548
|
+
return "unproven";
|
|
4549
|
+
let pane;
|
|
4473
4550
|
try {
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4551
|
+
pane = await this.tmux.capturePane();
|
|
4552
|
+
}
|
|
4553
|
+
catch {
|
|
4554
|
+
return "unproven";
|
|
4555
|
+
}
|
|
4556
|
+
// Without a way to tell the input row from the transcript, "the text is on
|
|
4557
|
+
// screen" cannot distinguish submitted from stranded — that ambiguity IS
|
|
4558
|
+
// the bug. Such a backend gets an explicit verdict rather than a guess:
|
|
4559
|
+
// "unverifiable" (something of ours appeared, but not provably out of the
|
|
4560
|
+
// input row) or "unproven" (nothing of ours appeared, so the paste itself
|
|
4561
|
+
// was lost). The caller keeps that backend's existing best-effort handling
|
|
4562
|
+
// for the first and recovers on the second, which is what it did before —
|
|
4563
|
+
// the fix here is for backends that CAN be read, not a new guess for those
|
|
4564
|
+
// that cannot.
|
|
4565
|
+
const prompt = this.backend?.getBottomReadyPattern?.();
|
|
4566
|
+
if (!prompt) {
|
|
4567
|
+
const seen = this.paneEvidence(pane, signature);
|
|
4568
|
+
if (signature.unique && seen.payload > 0)
|
|
4569
|
+
return "unverifiable";
|
|
4570
|
+
return seen.payload > (baseline?.payload ?? Infinity) || seen.queued > (baseline?.queued ?? Infinity)
|
|
4571
|
+
? "unverifiable"
|
|
4572
|
+
: "unproven";
|
|
4573
|
+
}
|
|
4574
|
+
const after = this.paneEvidence(pane, signature);
|
|
4575
|
+
// 1. Disqualifying evidence, checked FIRST and never overridden by the
|
|
4576
|
+
// corroborating evidence below: our text is sitting in the input row, so
|
|
4577
|
+
// it was not submitted — whatever else is on screen.
|
|
4578
|
+
//
|
|
4579
|
+
// It must be OUR text. A unique signature settles that on its own; a
|
|
4580
|
+
// body-derived one cannot, so it is only attributed to this delivery
|
|
4581
|
+
// when the input row did not already show it before we pasted.
|
|
4582
|
+
// Otherwise an older stranded message with the same opening would be
|
|
4583
|
+
// read as ours, we would press Enter to "recover" it, and the turn IT
|
|
4584
|
+
// starts would confirm a message that never reached the pane.
|
|
4585
|
+
if (after.strandedInput && (signature.unique || baseline?.strandedInput === false))
|
|
4586
|
+
return "stranded";
|
|
4587
|
+
// 2. Positive evidence. A unique signature needs no baseline: no earlier
|
|
4588
|
+
// message can carry this delivery's message_id, so finding it outside
|
|
4589
|
+
// the input row is proof in itself — which also means a momentary
|
|
4590
|
+
// failure to read the pane BEFORE pasting cannot turn a delivered
|
|
4591
|
+
// message into a re-paste.
|
|
4592
|
+
if (signature.unique && after.payload > 0)
|
|
4593
|
+
return "submitted";
|
|
4594
|
+
// 3. Otherwise the evidence must be NEW relative to the pane as it was
|
|
4595
|
+
// before we pasted: a queue marker left by an earlier message, or an
|
|
4596
|
+
// older transcript entry that opens the same way, are on screen either
|
|
4597
|
+
// way and would otherwise confirm a paste that never landed.
|
|
4598
|
+
if (baseline) {
|
|
4599
|
+
if (after.queued > baseline.queued)
|
|
4600
|
+
return "submitted";
|
|
4601
|
+
if (after.payload > baseline.payload)
|
|
4602
|
+
return "submitted";
|
|
4603
|
+
// 4. Nothing new of ours anywhere: the paste was swallowed by a redraw.
|
|
4604
|
+
return "unproven";
|
|
4605
|
+
}
|
|
4606
|
+
// No baseline and nothing that identifies this delivery on its own. "The
|
|
4607
|
+
// paste was lost" is a guess, not a finding, and acting on it re-pastes a
|
|
4608
|
+
// message that may well have been submitted — so say the evidence is
|
|
4609
|
+
// missing instead of inventing a verdict.
|
|
4610
|
+
this.logger.warn("Could not read the pane before pasting — this delivery cannot be verified either way");
|
|
4611
|
+
return "unverifiable";
|
|
4612
|
+
}
|
|
4613
|
+
/**
|
|
4614
|
+
* What the pane currently shows of a given message. Taken once before the
|
|
4615
|
+
* paste and once after, so confirmSubmitted can require a NEW marker or a NEW
|
|
4616
|
+
* echo rather than accepting whatever was already there.
|
|
4617
|
+
*/
|
|
4618
|
+
async capturePaneEvidence(signature) {
|
|
4619
|
+
if (!this.tmux)
|
|
4620
|
+
return null;
|
|
4621
|
+
// Bounded re-probe: a capture that fails here costs the delivery its only
|
|
4622
|
+
// "before" picture, and a momentary failure should not decide anything.
|
|
4623
|
+
for (let attempt = 1; attempt <= BASELINE_CAPTURE_ATTEMPTS; attempt++) {
|
|
4624
|
+
try {
|
|
4625
|
+
const pane = await this.tmux.capturePane();
|
|
4626
|
+
const evidence = this.paneEvidence(pane, signature);
|
|
4627
|
+
const prompt = this.backend?.getBottomReadyPattern?.();
|
|
4628
|
+
if (prompt && strandedAgendMessageInInput(pane, prompt)) {
|
|
4629
|
+
// Whatever we paste now lands after it, and one Enter submits both as
|
|
4630
|
+
// a single message. Nothing here can undo that; saying so beats
|
|
4631
|
+
// letting two messages silently merge.
|
|
4632
|
+
this.logger.warn("An unsubmitted message is already in the input row — this delivery will be appended to it");
|
|
4633
|
+
}
|
|
4634
|
+
return evidence;
|
|
4481
4635
|
}
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4636
|
+
catch {
|
|
4637
|
+
if (attempt < BASELINE_CAPTURE_ATTEMPTS)
|
|
4638
|
+
await new Promise(r => setTimeout(r, BASELINE_CAPTURE_RETRY_MS));
|
|
4485
4639
|
}
|
|
4640
|
+
}
|
|
4641
|
+
// Still unreadable: confirmSubmitted falls back to evidence that stands on
|
|
4642
|
+
// its own (a unique message_id) and refuses to guess without it.
|
|
4643
|
+
this.logger.warn("Could not read the pane before pasting — no baseline for this delivery");
|
|
4644
|
+
return null;
|
|
4645
|
+
}
|
|
4646
|
+
paneEvidence(pane, signature) {
|
|
4647
|
+
const marker = this.backend?.getQueuedInputMarker?.();
|
|
4648
|
+
const prompt = this.backend?.getBottomReadyPattern?.();
|
|
4649
|
+
const input = prompt ? inputAreaText(pane, prompt) : null;
|
|
4650
|
+
return {
|
|
4651
|
+
queued: marker ? pane.split(/\r?\n/).filter(row => marker.test(row)).length : 0,
|
|
4652
|
+
payload: countOccurrences(pane.replace(/\s+/g, ""), signature.value),
|
|
4653
|
+
strandedInput: input != null && inputShowsPastedText(input, signature.value),
|
|
4654
|
+
};
|
|
4655
|
+
}
|
|
4656
|
+
/**
|
|
4657
|
+
* What identifies THIS message on screen. The routing envelope carries a
|
|
4658
|
+
* message_id that no earlier transcript entry can share, so prefer it: the
|
|
4659
|
+
* body alone is not distinctive enough — short repeated instructions ("carry
|
|
4660
|
+
* on", a status ping) recur verbatim, and an older copy would vouch for a
|
|
4661
|
+
* paste that never landed. System pastes carry no envelope and fall back to
|
|
4662
|
+
* the body signature, which the baseline comparison still ties to this paste.
|
|
4663
|
+
*/
|
|
4664
|
+
submissionSignature(formatted, trustedId) {
|
|
4665
|
+
// The id must come from the message's own metadata, never from scanning the
|
|
4666
|
+
// rendered text: agents and users discuss message ids in the body all the
|
|
4667
|
+
// time ("check message_id: abc"), and a body-scanned value would match an
|
|
4668
|
+
// older transcript entry quoting the same thing — then be trusted as unique
|
|
4669
|
+
// and confirm a paste that never landed. Only the value AgEnD itself put in
|
|
4670
|
+
// the handoff metadata identifies THIS delivery.
|
|
4671
|
+
const id = trustedId?.trim();
|
|
4672
|
+
if (id)
|
|
4673
|
+
return { value: `message_id:${id}`, unique: true };
|
|
4674
|
+
return { value: pastedTextSignature(formatted), unique: false };
|
|
4675
|
+
}
|
|
4676
|
+
/**
|
|
4677
|
+
* Paste + submit + confirm for the SYSTEM messages that used to go out via
|
|
4678
|
+
* tmux.pasteText: one Enter (two for queue-less backends) and no verification
|
|
4679
|
+
* at all, so a swallowed Enter left the notice sitting in the input row where
|
|
4680
|
+
* the next delivery submitted both as one message. Shares confirmSubmitted
|
|
4681
|
+
* with the delivery path — having one primitive is the point.
|
|
4682
|
+
*
|
|
4683
|
+
* Best effort by design: these notices must never fail a delivery or throw.
|
|
4684
|
+
* The caller already holds paneWriteLock.
|
|
4685
|
+
*/
|
|
4686
|
+
async submitSystemPaste(text, label) {
|
|
4687
|
+
if (!this.tmux)
|
|
4688
|
+
return false;
|
|
4689
|
+
const signature = this.submissionSignature(text);
|
|
4690
|
+
const baseline = await this.capturePaneEvidence(signature);
|
|
4691
|
+
if (!(await this.tmux.pasteBuffer(text))) {
|
|
4692
|
+
this.logger.warn({ label }, "System paste failed to reach the pane");
|
|
4486
4693
|
return false;
|
|
4487
4694
|
}
|
|
4488
|
-
|
|
4695
|
+
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
4696
|
+
if (!(await this.sendDeliveryEnter(label)))
|
|
4489
4697
|
return false;
|
|
4698
|
+
let proof = await this.confirmSubmitted(signature, baseline);
|
|
4699
|
+
if (proof === "unverifiable") {
|
|
4700
|
+
// No input row to read: this backend gets exactly what the old pasteText
|
|
4701
|
+
// path gave it, including the unconditional second Enter for queue-less
|
|
4702
|
+
// TUIs that swallow the first. Narrowing that to one Enter on the grounds
|
|
4703
|
+
// that "the text is visible" would be the very inference this change
|
|
4704
|
+
// exists to remove — visible text is also what a strand looks like.
|
|
4705
|
+
if (this.systemPasteOptions().retryEnter) {
|
|
4706
|
+
await new Promise(r => setTimeout(r, 1_000));
|
|
4707
|
+
await this.sendDeliveryEnter(`${label}-defensive-retry`);
|
|
4708
|
+
}
|
|
4709
|
+
return true; // best effort, exactly as before — nothing here is verified
|
|
4710
|
+
}
|
|
4711
|
+
if (proof !== "submitted") {
|
|
4712
|
+
// A bare Enter is a no-op at an empty prompt, so this is safe even if the
|
|
4713
|
+
// first one did land; when the text is still in the input row it is the
|
|
4714
|
+
// submit it never got. Re-pasting would append the text to itself.
|
|
4715
|
+
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
4716
|
+
if (!(await this.sendDeliveryEnter(`${label}-retry`)))
|
|
4717
|
+
return false;
|
|
4718
|
+
proof = await this.confirmSubmitted(signature, baseline);
|
|
4490
4719
|
}
|
|
4720
|
+
if (proof !== "submitted") {
|
|
4721
|
+
this.logger.warn({ label, proof }, "System paste may not have been submitted");
|
|
4722
|
+
return false;
|
|
4723
|
+
}
|
|
4724
|
+
return true;
|
|
4491
4725
|
}
|
|
4492
4726
|
/** Re-resolve this instance's tmux window by name (stale id after crash/respawn). */
|
|
4493
4727
|
async recoverWindow() {
|
|
@@ -4969,7 +5203,16 @@ export class Daemon extends EventEmitter {
|
|
|
4969
5203
|
try {
|
|
4970
5204
|
// Messages can arrive during a restart and be queued on pasteLock before the
|
|
4971
5205
|
// snapshot lands; both write to the pane, so both go through the same lock.
|
|
4972
|
-
|
|
5206
|
+
const restoreNotice = `[system:session-snapshot]\n${snapshot}\n\nThis is a background context restore — do NOT reply to or acknowledge this message. Simply resume normal operation when the next user or instance message arrives.`;
|
|
5207
|
+
const injected = await this.paneWriteLock.run(() => this.submitSystemPaste(restoreNotice, "session-snapshot-restore"));
|
|
5208
|
+
if (!injected) {
|
|
5209
|
+
// rotation-state.json was deleted when the prompt was built, so there is
|
|
5210
|
+
// nothing left to retry from: the restore is gone either way. Say so —
|
|
5211
|
+
// reporting it as injected is how a lost context restore stays invisible.
|
|
5212
|
+
this.logger.error("Session snapshot could not be submitted — session continues without context");
|
|
5213
|
+
this.emit("snapshot_failed", this.name);
|
|
5214
|
+
return;
|
|
5215
|
+
}
|
|
4973
5216
|
this.logger.info("Injected session snapshot as first message");
|
|
4974
5217
|
this.emit("snapshot_injected", this.name);
|
|
4975
5218
|
}
|
|
@@ -5391,9 +5634,16 @@ export class Daemon extends EventEmitter {
|
|
|
5391
5634
|
// This path only runs when pasteQueueDepth > 0 — i.e. exactly when a real
|
|
5392
5635
|
// delivery is already in flight or queued. Without the lock the notice and
|
|
5393
5636
|
// that delivery race into the same pane.
|
|
5394
|
-
await this.paneWriteLock.run(
|
|
5395
|
-
|
|
5396
|
-
|
|
5637
|
+
const told = await this.paneWriteLock.run(() => this.submitSystemPaste(buildInstructionReloadNotice(this.backend?.binaryName ?? "unknown", this.name, this.instanceDir), "instruction-reload-notice"));
|
|
5638
|
+
if (!told) {
|
|
5639
|
+
// Recording the new instructions here would mark the agent as told about
|
|
5640
|
+
// a notice it never received, and every later restart would then skip
|
|
5641
|
+
// the reload. Leave the snapshot behind and hand it to the deferred
|
|
5642
|
+
// notice, which fires on the next real message.
|
|
5643
|
+
this.logger.warn("Instruction reload notice not confirmed — deferring it to the next message");
|
|
5644
|
+
this.pendingInstructionsNotice = true;
|
|
5645
|
+
return;
|
|
5646
|
+
}
|
|
5397
5647
|
// Record the value the agent has now been told about so the next
|
|
5398
5648
|
// unchanged restart skips the reload.
|
|
5399
5649
|
try {
|