@songsid/agend 2.1.12-beta.7 → 2.1.12-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 +34 -1
- package/dist/backend/claude-code.js +123 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +21 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +4 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/cli.js +22 -7
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +1 -1
- package/dist/command-table.js +7 -1
- package/dist/command-table.js.map +1 -1
- package/dist/daemon.d.ts +43 -2
- package/dist/daemon.js +274 -72
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -1
- package/dist/fleet-manager.d.ts +32 -2
- package/dist/fleet-manager.js +213 -6
- package/dist/fleet-manager.js.map +1 -1
- package/dist/kiro-db-reader.d.ts +65 -0
- package/dist/kiro-db-reader.js +258 -0
- package/dist/kiro-db-reader.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +56 -0
- package/dist/kiro-transcript-lane.js +141 -0
- package/dist/kiro-transcript-lane.js.map +1 -0
- package/dist/kiro-transcript-worker.d.ts +1 -0
- package/dist/kiro-transcript-worker.js +28 -0
- package/dist/kiro-transcript-worker.js.map +1 -0
- package/dist/locale.js +24 -0
- package/dist/locale.js.map +1 -1
- package/dist/pane-input-residue.d.ts +2 -0
- package/dist/pane-input-residue.js +5 -1
- package/dist/pane-input-residue.js.map +1 -1
- package/dist/profile-control.d.ts +20 -0
- package/dist/profile-control.js +308 -0
- package/dist/profile-control.js.map +1 -0
- package/dist/runtime-cpu-profile.d.ts +37 -0
- package/dist/runtime-cpu-profile.js +123 -0
- package/dist/runtime-cpu-profile.js.map +1 -0
- package/dist/service-installer.d.ts +6 -0
- package/dist/service-installer.js +18 -2
- package/dist/service-installer.js.map +1 -1
- package/dist/topic-commands.d.ts +12 -1
- package/dist/topic-commands.js +33 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +28 -0
- package/dist/transcript-delta-reader.js +148 -0
- package/dist/transcript-delta-reader.js.map +1 -0
- package/dist/transcript-monitor.d.ts +2 -0
- package/dist/transcript-monitor.js +19 -3
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +12 -47
- package/dist/transcript-sources.js +54 -235
- package/dist/transcript-sources.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -30,13 +30,14 @@ import { writeSecretFile } from "./secret-file.js";
|
|
|
30
30
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
31
31
|
import { buildFleetInstructions } from "./instructions.js";
|
|
32
32
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
33
|
-
import { bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
33
|
+
import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
34
34
|
import { TurnReplyGuard } from "./turn-reply-guard.js";
|
|
35
35
|
import { t } from "./locale.js";
|
|
36
36
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
37
37
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
38
38
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
39
39
|
import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
40
|
+
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
40
41
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
41
42
|
import { codexLiveRowMatches } from "./backend/codex.js";
|
|
42
43
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
@@ -68,6 +69,10 @@ const REPLY_DROP_WARNING_COOLDOWN_MS = 5 * 60_000;
|
|
|
68
69
|
const REPLY_GUARD_IDLE_CONFIRM_MS = 60_000;
|
|
69
70
|
const REPLY_RECOVERY_PROMPT = "[system:reply-required] The previous human-facing turn ended without a successfully delivered reply. Do not redo the work. React with an emoji or use the reply tool exactly once now to send the user a concise conclusion. If no substantive answer is needed, a brief react is sufficient. If you already replied to this message, do nothing. Do not reply to this system instruction except through the react or reply tool.";
|
|
70
71
|
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
72
|
+
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758). */
|
|
73
|
+
function transcriptMarkerEvidence(kind) {
|
|
74
|
+
return kind === "user" ? "transcript-marker" : "transcript-marker-queued";
|
|
75
|
+
}
|
|
71
76
|
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir) {
|
|
72
77
|
const source = binaryName === "codex" || binaryName === "grok" || binaryName === "muse"
|
|
73
78
|
? "AGENTS.md"
|
|
@@ -1765,7 +1770,8 @@ export class Daemon extends EventEmitter {
|
|
|
1765
1770
|
this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
|
|
1766
1771
|
return;
|
|
1767
1772
|
}
|
|
1768
|
-
this.finishDurableDelivery(delivery, "delivered",
|
|
1773
|
+
this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
|
|
1774
|
+
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof)}` : "positive submission proof");
|
|
1769
1775
|
}
|
|
1770
1776
|
/**
|
|
1771
1777
|
* Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
|
|
@@ -1794,7 +1800,7 @@ export class Daemon extends EventEmitter {
|
|
|
1794
1800
|
const found = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, delivery.deliveryId);
|
|
1795
1801
|
if (found === "user" || found === "queued") {
|
|
1796
1802
|
outcome = "delivered";
|
|
1797
|
-
evidence = found
|
|
1803
|
+
evidence = transcriptMarkerEvidence(found);
|
|
1798
1804
|
break;
|
|
1799
1805
|
}
|
|
1800
1806
|
last = found;
|
|
@@ -2073,6 +2079,15 @@ export class Daemon extends EventEmitter {
|
|
|
2073
2079
|
// conversation store via a pluggable source. Backends with no known
|
|
2074
2080
|
// source stay inert exactly as before.
|
|
2075
2081
|
this.transcriptMonitor = new TranscriptMonitor(this.instanceDir, this.logger, createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore()));
|
|
2082
|
+
const transcriptMonitor = this.transcriptMonitor;
|
|
2083
|
+
const transcriptFence = this.launchFenceEpoch;
|
|
2084
|
+
// Baseline in the isolate before this daemon accepts new work. A
|
|
2085
|
+
// stopped/replaced launch may never re-arm polling after this await.
|
|
2086
|
+
await transcriptMonitor.initialize();
|
|
2087
|
+
if (this.startupAborted || this.launchFenceEpoch !== transcriptFence || this.transcriptMonitor !== transcriptMonitor) {
|
|
2088
|
+
transcriptMonitor.stop();
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2076
2091
|
// 5. Wire transcript events
|
|
2077
2092
|
const ackIfPending = () => {
|
|
2078
2093
|
if (!this.pendingAckMessage || !this.adapter)
|
|
@@ -2843,7 +2858,13 @@ export class Daemon extends EventEmitter {
|
|
|
2843
2858
|
// was just stopped or paused (#1160 review).
|
|
2844
2859
|
if (this.runtimeMonitorsFrozen || this.healthCheckPaused)
|
|
2845
2860
|
return;
|
|
2846
|
-
|
|
2861
|
+
// Reset is synchronous for file sources, but Kiro's baseline is
|
|
2862
|
+
// read off-thread. Finish it before starting the replacement CLI,
|
|
2863
|
+
// otherwise its first work could become part of that baseline.
|
|
2864
|
+
if (!await this.resetTranscriptBeforeAdmission())
|
|
2865
|
+
return;
|
|
2866
|
+
if (this.runtimeMonitorsFrozen || this.healthCheckPaused)
|
|
2867
|
+
return;
|
|
2847
2868
|
// Kill orphan MCP server from the crashed CLI session.
|
|
2848
2869
|
// MCP server writes its PID to channel.mcp.pid on startup.
|
|
2849
2870
|
try {
|
|
@@ -4252,50 +4273,77 @@ export class Daemon extends EventEmitter {
|
|
|
4252
4273
|
// concurrency coordination, first output + idle is `budgetMs`, the dialog
|
|
4253
4274
|
// scan is STARTUP_DIALOG_BUDGET_MS — so the outcome is always trySpawn's
|
|
4254
4275
|
// own verdict. A soft timer only makes a slow wake visible in the log.
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4276
|
+
const wakeFence = this.launchFenceEpoch;
|
|
4277
|
+
const wakeGeneration = this.spawnGeneration;
|
|
4278
|
+
const isCurrent = () => !this.startupAborted && this.launchFenceEpoch === wakeFence
|
|
4279
|
+
&& this.spawnGeneration === wakeGeneration && this.pauseWakeState === "waking";
|
|
4280
|
+
// The shared transition covers baseline initialization as well as spawn.
|
|
4281
|
+
// Every wake().then(deliver) caller must wait for the same admission edge.
|
|
4282
|
+
const transition = (async () => {
|
|
4260
4283
|
try {
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4284
|
+
await this.autoPauseController.wakeOnDeliver(async () => {
|
|
4285
|
+
const slow = setTimeout(() => {
|
|
4286
|
+
this.logger.warn({ budgetMs }, "Wake is exceeding its budget — still waiting for the spawn (it cannot be cancelled)");
|
|
4287
|
+
}, budgetMs + STARTUP_DIALOG_BUDGET_MS);
|
|
4288
|
+
slow.unref?.();
|
|
4289
|
+
try {
|
|
4290
|
+
const ready = await this.trySpawn(true, budgetMs);
|
|
4291
|
+
if (!ready)
|
|
4292
|
+
throw new Error(`Wake failed: the CLI did not become ready (budget ${budgetMs}ms)`);
|
|
4293
|
+
}
|
|
4294
|
+
finally {
|
|
4295
|
+
clearTimeout(slow);
|
|
4296
|
+
}
|
|
4297
|
+
});
|
|
4298
|
+
if (!isCurrent())
|
|
4299
|
+
throw new Error("Wake cancelled: the launch was superseded");
|
|
4300
|
+
if (!await this.resetTranscriptBeforeAdmission() || !isCurrent()) {
|
|
4301
|
+
throw new Error("Wake cancelled: transcript initialization was superseded");
|
|
4302
|
+
}
|
|
4303
|
+
this.pauseWakeState = "active";
|
|
4304
|
+
this.healthCheckPaused = false;
|
|
4305
|
+
this.pauseRequested = false;
|
|
4306
|
+
// trySpawn resolved only after the new CLI reached its ready prompt.
|
|
4307
|
+
// Discard any recovery gate retained while monitors were frozen.
|
|
4308
|
+
this.clearErrorRecoveryGate();
|
|
4309
|
+
clearPausedMarker(this.instanceDir);
|
|
4310
|
+
this.resumeRuntimeMonitors();
|
|
4311
|
+
this.logger.info("Instance auto-woke");
|
|
4312
|
+
this.ipcServer?.broadcast({
|
|
4313
|
+
type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
|
|
4314
|
+
});
|
|
4315
|
+
this.emit("auto_woke", { name: this.name });
|
|
4316
|
+
}
|
|
4317
|
+
catch (err) {
|
|
4318
|
+
if (isCurrent()) {
|
|
4319
|
+
this.pauseWakeState = "paused";
|
|
4320
|
+
this.healthCheckPaused = true;
|
|
4321
|
+
}
|
|
4322
|
+
this.logger.error({ err: err.message }, "Instance wake failed");
|
|
4323
|
+
throw err;
|
|
4264
4324
|
}
|
|
4265
4325
|
finally {
|
|
4266
|
-
|
|
4326
|
+
this.endSpawn();
|
|
4267
4327
|
}
|
|
4268
|
-
});
|
|
4328
|
+
})();
|
|
4269
4329
|
this.pauseWakeTransition = transition;
|
|
4270
4330
|
try {
|
|
4271
4331
|
await transition;
|
|
4272
|
-
this.pauseWakeState = "active";
|
|
4273
|
-
this.healthCheckPaused = false;
|
|
4274
|
-
this.pauseRequested = false;
|
|
4275
|
-
// trySpawn resolved only after the new CLI reached its ready prompt.
|
|
4276
|
-
// Discard any recovery gate retained while monitors were frozen.
|
|
4277
|
-
this.clearErrorRecoveryGate();
|
|
4278
|
-
clearPausedMarker(this.instanceDir);
|
|
4279
|
-
this.transcriptMonitor?.resetOffset();
|
|
4280
|
-
this.resumeRuntimeMonitors();
|
|
4281
|
-
this.logger.info("Instance auto-woke");
|
|
4282
|
-
this.ipcServer?.broadcast({
|
|
4283
|
-
type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
|
|
4284
|
-
});
|
|
4285
|
-
this.emit("auto_woke", { name: this.name });
|
|
4286
|
-
}
|
|
4287
|
-
catch (err) {
|
|
4288
|
-
this.pauseWakeState = "paused";
|
|
4289
|
-
this.healthCheckPaused = true;
|
|
4290
|
-
this.logger.error({ err: err.message }, "Instance wake failed");
|
|
4291
|
-
throw err;
|
|
4292
4332
|
}
|
|
4293
4333
|
finally {
|
|
4294
|
-
this.endSpawn();
|
|
4295
4334
|
if (this.pauseWakeTransition === transition)
|
|
4296
4335
|
this.pauseWakeTransition = null;
|
|
4297
4336
|
}
|
|
4298
4337
|
}
|
|
4338
|
+
/** A reset baseline must settle before a launch admits new work. */
|
|
4339
|
+
async resetTranscriptBeforeAdmission() {
|
|
4340
|
+
const monitor = this.transcriptMonitor;
|
|
4341
|
+
const fence = this.launchFenceEpoch, generation = this.spawnGeneration;
|
|
4342
|
+
monitor?.resetOffset();
|
|
4343
|
+
await monitor?.initialize();
|
|
4344
|
+
return !this.startupAborted && this.launchFenceEpoch === fence
|
|
4345
|
+
&& this.spawnGeneration === generation && this.transcriptMonitor === monitor;
|
|
4346
|
+
}
|
|
4299
4347
|
/**
|
|
4300
4348
|
* One line per edge, until the instance is flapping. The edges themselves are
|
|
4301
4349
|
* still published to every observer; only the log is limited, so the next
|
|
@@ -6143,7 +6191,7 @@ export class Daemon extends EventEmitter {
|
|
|
6143
6191
|
const canHandOff = (supportsQueuedInput || opts?.steer)
|
|
6144
6192
|
&& readiness === "busy"
|
|
6145
6193
|
&& (await this.probeBlockingDialog()).state === "clear"
|
|
6146
|
-
&& await this.hasPositiveDeliveryInput();
|
|
6194
|
+
&& await this.hasPositiveDeliveryInput(true);
|
|
6147
6195
|
if (canHandOff) {
|
|
6148
6196
|
// Native queue (codex), or an explicit /steer: hand the complete
|
|
6149
6197
|
// paste+Enter transaction to the busy CLI now. For steer this is the
|
|
@@ -6240,7 +6288,7 @@ export class Daemon extends EventEmitter {
|
|
|
6240
6288
|
if (probe.state !== "clear")
|
|
6241
6289
|
return "dialog";
|
|
6242
6290
|
}
|
|
6243
|
-
if (!(await this.hasPositiveDeliveryInput()))
|
|
6291
|
+
if (!(await this.hasPositiveDeliveryInput(handingOffToNativeQueue)))
|
|
6244
6292
|
return "dialog";
|
|
6245
6293
|
// #829: a CLI that restores a cancelled prompt into its input box would
|
|
6246
6294
|
// have this message pasted onto it and both submitted as one. Clear it
|
|
@@ -6262,6 +6310,9 @@ export class Daemon extends EventEmitter {
|
|
|
6262
6310
|
if (residue !== "clear")
|
|
6263
6311
|
return this.failDelivery(verdict, status, "input-residue", residue);
|
|
6264
6312
|
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
|
|
6313
|
+
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
6314
|
+
if (written === "handoff-box-unread")
|
|
6315
|
+
return "dialog";
|
|
6265
6316
|
// Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
|
|
6266
6317
|
return written === false && verdict.fenced ? stale() : written;
|
|
6267
6318
|
});
|
|
@@ -6809,8 +6860,26 @@ export class Daemon extends EventEmitter {
|
|
|
6809
6860
|
}
|
|
6810
6861
|
return true;
|
|
6811
6862
|
}
|
|
6812
|
-
/**
|
|
6813
|
-
|
|
6863
|
+
/**
|
|
6864
|
+
* Codex can paint a prompt before the TTY enters raw mode. Both are required.
|
|
6865
|
+
*
|
|
6866
|
+
* A hand-off into a busy pane (native queue or steer) pastes and presses Enter at once, so a backend with a structural
|
|
6867
|
+
* box reader must see its box on a fresh capture first: a pane whose box cannot be read — a modal the dialog table
|
|
6868
|
+
* does not know, a frame the reader refuses, a failed capture — is not one to type into (#1169 review). Asked
|
|
6869
|
+
* before the hand-off is chosen and again under the pane lock, right before the write; false waits for readiness.
|
|
6870
|
+
*/
|
|
6871
|
+
async hasPositiveDeliveryInput(handOff = false) {
|
|
6872
|
+
if (handOff && this.backend?.readInputRow) {
|
|
6873
|
+
if (!this.tmux)
|
|
6874
|
+
return false;
|
|
6875
|
+
try {
|
|
6876
|
+
if (this.backend.readInputRow(await this.tmux.capturePane()) === null)
|
|
6877
|
+
return false;
|
|
6878
|
+
}
|
|
6879
|
+
catch {
|
|
6880
|
+
return false;
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6814
6883
|
if (!this.needsStartupInputProof())
|
|
6815
6884
|
return true;
|
|
6816
6885
|
const check = this.backend?.isDeliveryInputReadyPane;
|
|
@@ -7494,6 +7563,15 @@ export class Daemon extends EventEmitter {
|
|
|
7494
7563
|
verdict.fenced = !verdict.durableBeginCommitted;
|
|
7495
7564
|
return false;
|
|
7496
7565
|
}
|
|
7566
|
+
// A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
|
|
7567
|
+
// before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
|
|
7568
|
+
// the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
|
|
7569
|
+
// nothing was written and the attempt ends like a paste that failed.
|
|
7570
|
+
if (handingOffToNativeQueue && this.backend?.readInputRow && !pasteBaseline?.inputReadable) {
|
|
7571
|
+
if (!verdict.durableBeginCommitted)
|
|
7572
|
+
return "handoff-box-unread";
|
|
7573
|
+
return this.failDelivery(verdict, status, "paste", "handoff-input-unreadable");
|
|
7574
|
+
}
|
|
7497
7575
|
// Readiness, idle-gate, dialog, spawn and pane-lock waits have all ended.
|
|
7498
7576
|
// Commit the submission fence at the last possible point before the
|
|
7499
7577
|
// first side effect; a crash during those waits remains safely replayable.
|
|
@@ -7523,6 +7601,11 @@ export class Daemon extends EventEmitter {
|
|
|
7523
7601
|
path: attemptEvidence.transcriptPath,
|
|
7524
7602
|
offset: attemptEvidence.transcriptOffset,
|
|
7525
7603
|
};
|
|
7604
|
+
// From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
|
|
7605
|
+
// reply, or still be painting the paste) — this delivery's exact marker only.
|
|
7606
|
+
if (["claude-code", "codex"].includes(attemptEvidence.backend)) {
|
|
7607
|
+
signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
|
|
7608
|
+
}
|
|
7526
7609
|
}
|
|
7527
7610
|
}
|
|
7528
7611
|
if (rawPaste)
|
|
@@ -7700,7 +7783,7 @@ export class Daemon extends EventEmitter {
|
|
|
7700
7783
|
// The recovery Enter can be accepted before Codex paints its new
|
|
7701
7784
|
// transcript. One immediate capture is not a failure verdict: give
|
|
7702
7785
|
// that echo/queue a bounded chance to appear, without re-pasting.
|
|
7703
|
-
const afterEnter = this.
|
|
7786
|
+
const afterEnter = this.structuredInputEvidence()
|
|
7704
7787
|
? await this.lateCodexSubmissionProof(signature, pasteBaseline, true)
|
|
7705
7788
|
: await this.confirmSubmitted(signature, pasteBaseline);
|
|
7706
7789
|
if (afterEnter === "submitted") {
|
|
@@ -7714,8 +7797,8 @@ export class Daemon extends EventEmitter {
|
|
|
7714
7797
|
// another turn's output — and treating that as proof re-confirms a
|
|
7715
7798
|
// message nobody submitted. Output is corroboration; text sitting in
|
|
7716
7799
|
// the input row is disqualifying, and disqualifying evidence wins.
|
|
7717
|
-
if (this.
|
|
7718
|
-
this.logger.warn({ phase: "native-queue-submit", proof: afterEnter, strandedAt },
|
|
7800
|
+
if (this.structuredInputEvidence() && afterEnter !== "stranded") {
|
|
7801
|
+
this.logger.warn({ phase: "native-queue-submit", proof: afterEnter, strandedAt }, `${this.cliLabel()} recovery Enter outcome uncertain — no hard failure or duplicate paste`);
|
|
7719
7802
|
verdict.phase = "native-queue-submit";
|
|
7720
7803
|
verdict.proof = afterEnter;
|
|
7721
7804
|
return false;
|
|
@@ -7723,11 +7806,15 @@ export class Daemon extends EventEmitter {
|
|
|
7723
7806
|
this.logger.error({ afterEnter, strandedAt }, "Stranded message could not be submitted by Enter");
|
|
7724
7807
|
return this.failDelivery(verdict, status, "native-queue-submit", afterEnter);
|
|
7725
7808
|
}
|
|
7726
|
-
|
|
7809
|
+
// A structural box reader (claude-code, #1200) is in the same position as Codex once the turn has run: the
|
|
7810
|
+
// message may have been taken and its echo scrolled out, the queue drained, the box empty — or the capture
|
|
7811
|
+
// failed. None of that proves the paste was lost, so nothing short of positive evidence is a reason to paste
|
|
7812
|
+
// again (#1353 review); "unverifiable" used to, and could submit the message twice.
|
|
7813
|
+
if (this.backend?.isDeliveryInputReadyPane || this.backend?.readInputRow) {
|
|
7727
7814
|
// In Codex a missing viewport echo is inconclusive, not a proof of
|
|
7728
7815
|
// loss. Another paste could run the same request twice. Leave the
|
|
7729
7816
|
// already-pasted delivery at 👀 and let the next observation decide.
|
|
7730
|
-
this.logger.warn({ phase: "native-queue-proof", proof: settled },
|
|
7817
|
+
this.logger.warn({ phase: "native-queue-proof", proof: settled }, `${this.cliLabel()} native-queue outcome uncertain — not re-pasting`);
|
|
7731
7818
|
verdict.phase = "native-queue-proof";
|
|
7732
7819
|
verdict.proof = settled;
|
|
7733
7820
|
return false;
|
|
@@ -7824,7 +7911,7 @@ export class Daemon extends EventEmitter {
|
|
|
7824
7911
|
verdict.proof = "output-edge";
|
|
7825
7912
|
}
|
|
7826
7913
|
else {
|
|
7827
|
-
const proof = this.
|
|
7914
|
+
const proof = this.structuredInputEvidence()
|
|
7828
7915
|
? await this.lateCodexSubmissionProof(signature, pasteBaseline)
|
|
7829
7916
|
: await this.confirmSubmitted(signature, pasteBaseline);
|
|
7830
7917
|
if (proof === "submitted") {
|
|
@@ -7832,12 +7919,12 @@ export class Daemon extends EventEmitter {
|
|
|
7832
7919
|
this.emit("message_confirmed", status);
|
|
7833
7920
|
return true;
|
|
7834
7921
|
}
|
|
7835
|
-
if (this.
|
|
7922
|
+
if (this.structuredInputEvidence() && proof !== "stranded") {
|
|
7836
7923
|
// This is the observed #910 race: the CLI processed the message
|
|
7837
7924
|
// although its echo had not appeared within the proof window. A
|
|
7838
7925
|
// missing viewport signature cannot establish non-delivery; keep
|
|
7839
7926
|
// 👀 and never emit the sender's hard ❌ or re-paste blindly.
|
|
7840
|
-
this.logger.warn({ phase: "post-submit-proof", proof },
|
|
7927
|
+
this.logger.warn({ phase: "post-submit-proof", proof }, `${this.cliLabel()} delivery outcome uncertain — no hard failure or duplicate paste`);
|
|
7841
7928
|
verdict.phase = "post-submit-proof";
|
|
7842
7929
|
verdict.proof = proof;
|
|
7843
7930
|
return false;
|
|
@@ -7910,7 +7997,7 @@ export class Daemon extends EventEmitter {
|
|
|
7910
7997
|
return dialogClear && this.spawnGeneration === spawnGeneration && this.getWindowId() === windowId;
|
|
7911
7998
|
}
|
|
7912
7999
|
canProveSubmission() {
|
|
7913
|
-
return
|
|
8000
|
+
return this.readsInput();
|
|
7914
8001
|
}
|
|
7915
8002
|
/** Positive submission retires the startup transient guard for this spawn. */
|
|
7916
8003
|
submittedProof() {
|
|
@@ -7947,18 +8034,22 @@ export class Daemon extends EventEmitter {
|
|
|
7947
8034
|
}
|
|
7948
8035
|
return busy;
|
|
7949
8036
|
}
|
|
7950
|
-
if (this.
|
|
8037
|
+
if (this.structuredInputEvidence()) {
|
|
7951
8038
|
// Codex's first post-wake redraw may hide the echo for a few seconds.
|
|
7952
8039
|
// Absence from a viewport is NOT proof the paste was lost, so only a
|
|
7953
8040
|
// positively identified strand authorizes another Enter. Never re-paste
|
|
7954
8041
|
// here: the CLI may already be processing the unique message_id.
|
|
7955
8042
|
let proof = "unproven";
|
|
7956
8043
|
const firstDeadline = Date.now() + POST_ENTER_PROOF_WINDOW_MS;
|
|
8044
|
+
// A structural box reader (claude-code) sees its box still holding the paste for a moment after an Enter it did
|
|
8045
|
+
// take — Claude paints the submit late (captured live on 2.1.293: ~40 captures in a row). So for it a strand
|
|
8046
|
+
// proves a swallowed Enter only once it outlasts the window; an early one is polled through. Codex keeps its rule.
|
|
8047
|
+
const strandMustPersist = !!this.backend?.readInputRow;
|
|
7957
8048
|
for (;;) {
|
|
7958
8049
|
proof = await this.confirmSubmitted(signature, baseline);
|
|
7959
8050
|
if (proof === "submitted")
|
|
7960
8051
|
return true;
|
|
7961
|
-
if (proof === "stranded" || Date.now() >= firstDeadline)
|
|
8052
|
+
if ((proof === "stranded" && !strandMustPersist) || Date.now() >= firstDeadline)
|
|
7962
8053
|
break;
|
|
7963
8054
|
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
7964
8055
|
}
|
|
@@ -8008,13 +8099,65 @@ export class Daemon extends EventEmitter {
|
|
|
8008
8099
|
async confirmSubmitted(signature, baseline) {
|
|
8009
8100
|
if (!this.tmux)
|
|
8010
8101
|
return "unproven";
|
|
8011
|
-
let pane;
|
|
8102
|
+
let pane = null;
|
|
8012
8103
|
try {
|
|
8013
8104
|
pane = await this.tmux.capturePane();
|
|
8014
8105
|
}
|
|
8015
|
-
catch {
|
|
8016
|
-
|
|
8017
|
-
|
|
8106
|
+
catch { /* the transcript may still say */ }
|
|
8107
|
+
const onPane = pane === null ? "unproven" : this.judgeSubmission(pane, signature, baseline);
|
|
8108
|
+
if (onPane === "submitted")
|
|
8109
|
+
return onPane;
|
|
8110
|
+
const t = signature.transcript;
|
|
8111
|
+
const found = await this.transcriptLook(signature);
|
|
8112
|
+
// One synchronous stretch from here: the fence is asked after the last await, and only then is the hit kept and the
|
|
8113
|
+
// spawn's guard retired — a stop or respawn that lands between the look and this line leaves both untouched.
|
|
8114
|
+
if (!found || !t?.current())
|
|
8115
|
+
return onPane;
|
|
8116
|
+
if (!t.provenBy) {
|
|
8117
|
+
t.provenBy = found;
|
|
8118
|
+
t.onProof?.(found);
|
|
8119
|
+
}
|
|
8120
|
+
return this.submittedProof();
|
|
8121
|
+
}
|
|
8122
|
+
/**
|
|
8123
|
+
* The CLI's transcript, from this attempt's checkpoint, holds this delivery's exact marker: a user entry (read) or
|
|
8124
|
+
* one it queued (taken into its queue). Either is the CLI having taken the
|
|
8125
|
+
* message, so it outranks the pane — including a box that still shows the paste a moment after Enter (Claude paints
|
|
8126
|
+
* the submit late, captured live on 2.1.293), which on its own reads as a strand. Positive only; never a downgrade.
|
|
8127
|
+
*/
|
|
8128
|
+
async transcriptLook(signature) {
|
|
8129
|
+
const t = signature.transcript;
|
|
8130
|
+
if (!t || !t.current())
|
|
8131
|
+
return null;
|
|
8132
|
+
if (t.provenBy)
|
|
8133
|
+
return t.provenBy;
|
|
8134
|
+
const found = await t.reader.look();
|
|
8135
|
+
// No side effect here: the caller keeps the hit and retires the guard only after its own fence check.
|
|
8136
|
+
return found === "user" || found === "queued" ? found : null;
|
|
8137
|
+
}
|
|
8138
|
+
/**
|
|
8139
|
+
* A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
|
|
8140
|
+
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
8141
|
+
*/
|
|
8142
|
+
transcriptProofFor(checkpoint, deliveryId, spawnAtWrite, stillCurrent, onProof) {
|
|
8143
|
+
const launchAtWrite = this.launchFenceEpoch;
|
|
8144
|
+
return {
|
|
8145
|
+
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, deliveryId),
|
|
8146
|
+
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
8147
|
+
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
|
|
8148
|
+
onProof,
|
|
8149
|
+
};
|
|
8150
|
+
}
|
|
8151
|
+
/** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
|
|
8152
|
+
cliLabel() {
|
|
8153
|
+
return this.config.backend ?? this.backend?.binaryName ?? "CLI";
|
|
8154
|
+
}
|
|
8155
|
+
/**
|
|
8156
|
+
* The verdict one snapshot supports — the live viewport, or (for a unique signature) the scrollback a late proof
|
|
8157
|
+
* reads. One judge for both, so a history read cannot vouch for what the live one refuses: an unreadable box, or our
|
|
8158
|
+
* text still in it (#1353 review).
|
|
8159
|
+
*/
|
|
8160
|
+
judgeSubmission(pane, signature, baseline) {
|
|
8018
8161
|
// Without a way to tell the input row from the transcript, "the text is on
|
|
8019
8162
|
// screen" cannot distinguish submitted from stranded — that ambiguity IS
|
|
8020
8163
|
// the bug. Such a backend gets an explicit verdict rather than a guess:
|
|
@@ -8024,8 +8167,7 @@ export class Daemon extends EventEmitter {
|
|
|
8024
8167
|
// for the first and recovers on the second, which is what it did before —
|
|
8025
8168
|
// the fix here is for backends that CAN be read, not a new guess for those
|
|
8026
8169
|
// that cannot.
|
|
8027
|
-
|
|
8028
|
-
if (!prompt) {
|
|
8170
|
+
if (!this.readsInput()) {
|
|
8029
8171
|
const seen = this.paneEvidence(pane, signature);
|
|
8030
8172
|
if (signature.unique && seen.payload > 0)
|
|
8031
8173
|
return "unverifiable";
|
|
@@ -8042,6 +8184,11 @@ export class Daemon extends EventEmitter {
|
|
|
8042
8184
|
&& !this.backend.getBusyPattern?.()?.test(pane)) {
|
|
8043
8185
|
return after.payload > 0 ? "unverifiable" : "unproven";
|
|
8044
8186
|
}
|
|
8187
|
+
// A structural reader (claude-code) that finds no input box on this screen — a dialog in front of it, a layout it
|
|
8188
|
+
// does not know — cannot place our text on either side of the box: the same verdicts as a readerless backend.
|
|
8189
|
+
if (this.backend?.readInputRow && !after.inputReadable) {
|
|
8190
|
+
return after.payload > 0 ? "unverifiable" : "unproven";
|
|
8191
|
+
}
|
|
8045
8192
|
// 1. Disqualifying evidence, checked FIRST and never overridden by the
|
|
8046
8193
|
// corroborating evidence below: our text is sitting in the input row, so
|
|
8047
8194
|
// it was not submitted — whatever else is on screen.
|
|
@@ -8052,8 +8199,22 @@ export class Daemon extends EventEmitter {
|
|
|
8052
8199
|
// Otherwise an older stranded message with the same opening would be
|
|
8053
8200
|
// read as ours, we would press Enter to "recover" it, and the turn IT
|
|
8054
8201
|
// starts would confirm a message that never reached the pane.
|
|
8055
|
-
|
|
8202
|
+
//
|
|
8203
|
+
// For a structural box reader (claude-code), "did not already show it" needs a baseline whose box was READ: an
|
|
8204
|
+
// unreadable one (a dialog, a redraw the reader refused) says nothing about what the box held, and counting it as
|
|
8205
|
+
// empty would claim an older message as ours and press Enter on it (#1353 review). The prompt-row backends (codex,
|
|
8206
|
+
// kiro) keep their existing attribution.
|
|
8207
|
+
const attributable = baseline != null
|
|
8208
|
+
&& (!this.backend?.readInputRow || (baseline.inputReadable && after.inputReadable));
|
|
8209
|
+
if (after.strandedInput && (signature.unique || (attributable && baseline.strandedInput === false)))
|
|
8210
|
+
return "stranded";
|
|
8211
|
+
// A paste the CLI shows collapsed carries no signature at all. One the box did not hold before we pasted is
|
|
8212
|
+
// ours, and it is still in the box — whatever echo or output is on screen (#1200).
|
|
8213
|
+
if (attributable && after.collapsedPastes > baseline.collapsedPastes)
|
|
8056
8214
|
return "stranded";
|
|
8215
|
+
// Something sits in the box that cannot be attributed either way. Only a unique signature seen outside the box
|
|
8216
|
+
// (below) can still prove this delivery; nothing else on screen may.
|
|
8217
|
+
const unattributedResidue = !attributable && (after.strandedInput || after.collapsedPastes > 0);
|
|
8057
8218
|
// 2. Positive evidence. A unique signature needs no baseline: no earlier
|
|
8058
8219
|
// message can carry this delivery's message_id, so finding it outside
|
|
8059
8220
|
// the input row is proof in itself — which also means a momentary
|
|
@@ -8061,12 +8222,16 @@ export class Daemon extends EventEmitter {
|
|
|
8061
8222
|
// message into a re-paste.
|
|
8062
8223
|
if (signature.unique && after.payload > 0)
|
|
8063
8224
|
return this.submittedProof();
|
|
8225
|
+
if (unattributedResidue)
|
|
8226
|
+
return "unverifiable";
|
|
8064
8227
|
// 3. Otherwise the evidence must be NEW relative to the pane as it was
|
|
8065
8228
|
// before we pasted: a queue marker left by an earlier message, or an
|
|
8066
8229
|
// older transcript entry that opens the same way, are on screen either
|
|
8067
8230
|
// way and would otherwise confirm a paste that never landed.
|
|
8068
8231
|
if (baseline) {
|
|
8069
|
-
|
|
8232
|
+
// A queue is new only against a box that was read before the paste (a reader backend): an unreadable baseline
|
|
8233
|
+
// says nothing about whether the queue was already there.
|
|
8234
|
+
if (after.queued > baseline.queued && (!this.backend?.readInputRow || baseline.inputReadable))
|
|
8070
8235
|
return this.submittedProof();
|
|
8071
8236
|
if (after.payload > baseline.payload)
|
|
8072
8237
|
return this.submittedProof();
|
|
@@ -8094,12 +8259,10 @@ export class Daemon extends EventEmitter {
|
|
|
8094
8259
|
return proof;
|
|
8095
8260
|
if (signature.unique && this.tmux?.capturePaneWithHistory) {
|
|
8096
8261
|
try {
|
|
8262
|
+
// Scrollback can hold the echo the viewport lost — but it is judged exactly like the viewport: a box that
|
|
8263
|
+
// cannot be read, or that still holds our text, is never upgraded by an echo further up.
|
|
8097
8264
|
const history = await this.tmux.capturePaneWithHistory(300);
|
|
8098
|
-
|
|
8099
|
-
const known = !this.backend?.isDeliveryInputReadyPane
|
|
8100
|
-
|| this.backend.isDeliveryInputReadyPane(history)
|
|
8101
|
-
|| this.backend.getBusyPattern?.()?.test(history);
|
|
8102
|
-
if (known && seen.payload > 0 && !seen.strandedInput)
|
|
8265
|
+
if (this.judgeSubmission(history, signature, baseline) === "submitted")
|
|
8103
8266
|
return this.submittedProof();
|
|
8104
8267
|
}
|
|
8105
8268
|
catch { /* a failed history read proves neither delivery nor loss */ }
|
|
@@ -8123,9 +8286,8 @@ export class Daemon extends EventEmitter {
|
|
|
8123
8286
|
try {
|
|
8124
8287
|
const pane = await this.tmux.capturePane();
|
|
8125
8288
|
const evidence = this.paneEvidence(pane, signature);
|
|
8126
|
-
const
|
|
8127
|
-
if (
|
|
8128
|
-
&& strandedAgendMessageInInput(pane, prompt)) {
|
|
8289
|
+
const input = this.inputRegion(pane);
|
|
8290
|
+
if (input && agendMessageInInput(input.text)) {
|
|
8129
8291
|
// Whatever we paste now lands after it, and one Enter submits both as
|
|
8130
8292
|
// a single message. Nothing here can undo that; saying so beats
|
|
8131
8293
|
// letting two messages silently merge.
|
|
@@ -8144,16 +8306,45 @@ export class Daemon extends EventEmitter {
|
|
|
8144
8306
|
return null;
|
|
8145
8307
|
}
|
|
8146
8308
|
paneEvidence(pane, signature) {
|
|
8147
|
-
const
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8309
|
+
const input = this.inputRegion(pane);
|
|
8310
|
+
// A structural box reader vouches for the queue itself (InputBox.queued): a marker's words elsewhere on the pane —
|
|
8311
|
+
// a reply quoting it, an old block — are not a queue (#1169 review). Other backends count their marker rows.
|
|
8312
|
+
const marker = this.backend?.readInputRow ? null : this.backend?.getQueuedInputMarker?.();
|
|
8151
8313
|
return {
|
|
8152
|
-
queued:
|
|
8314
|
+
queued: this.backend?.readInputRow
|
|
8315
|
+
? (input?.queued ? 1 : 0)
|
|
8316
|
+
: marker ? pane.split(/\r?\n/).filter(row => marker.test(row)).length : 0,
|
|
8153
8317
|
payload: countOccurrences(pane.replace(/\s+/g, ""), signature.value),
|
|
8154
|
-
strandedInput: input != null && inputShowsPastedText(input, signature.value),
|
|
8318
|
+
strandedInput: input != null && inputShowsPastedText(input.text, signature.value),
|
|
8319
|
+
collapsedPastes: input?.collapsedPastes ?? 0,
|
|
8320
|
+
inputReadable: input != null,
|
|
8155
8321
|
};
|
|
8156
8322
|
}
|
|
8323
|
+
/**
|
|
8324
|
+
* The input box as it stands on this screen: the backend's structural reader when it has one (claude-code, #1200),
|
|
8325
|
+
* else the prompt-row heuristic (codex — only on a screen it vouches for — and kiro). null: no box to read here.
|
|
8326
|
+
*/
|
|
8327
|
+
inputRegion(pane) {
|
|
8328
|
+
if (this.backend?.readInputRow)
|
|
8329
|
+
return this.backend.readInputRow(pane);
|
|
8330
|
+
const prompt = this.backend?.getBottomReadyPattern?.();
|
|
8331
|
+
if (!prompt || (this.backend?.isDeliveryInputReadyPane && !this.backend.isDeliveryInputReadyPane(pane)))
|
|
8332
|
+
return null;
|
|
8333
|
+
const text = inputAreaText(pane, prompt);
|
|
8334
|
+
return text == null ? null : { text, collapsedPastes: 0 };
|
|
8335
|
+
}
|
|
8336
|
+
/** Whether this backend's input box can be read at all (a given screen may still have none). */
|
|
8337
|
+
readsInput() {
|
|
8338
|
+
return !!this.backend?.readInputRow || !!this.backend?.getBottomReadyPattern?.();
|
|
8339
|
+
}
|
|
8340
|
+
/**
|
|
8341
|
+
* Backends whose pane evidence is structured (codex's input/footer pair, a structural box reader): there, our text
|
|
8342
|
+
* missing from the viewport is inconclusive — an echo can arrive late or scroll away — and never a loss to re-paste
|
|
8343
|
+
* over. Only text positively seen in the box (a strand) authorizes another Enter.
|
|
8344
|
+
*/
|
|
8345
|
+
structuredInputEvidence() {
|
|
8346
|
+
return !!this.backend?.isDeliveryInputReadyPane || !!this.backend?.readInputRow;
|
|
8347
|
+
}
|
|
8157
8348
|
/**
|
|
8158
8349
|
* What identifies THIS message on screen. The routing envelope carries a
|
|
8159
8350
|
* message_id that no earlier transcript entry can share, so prefer it: the
|
|
@@ -8220,6 +8411,17 @@ export class Daemon extends EventEmitter {
|
|
|
8220
8411
|
if (!(await this.sendDeliveryEnter(label, guard?.current)))
|
|
8221
8412
|
return false;
|
|
8222
8413
|
let proof = await this.confirmSubmitted(signature, baseline);
|
|
8414
|
+
// A structural box reader sees the paste in the box until the CLI digests the Enter (claude-code repaints a moment
|
|
8415
|
+
// later): give that a bounded window before calling it stranded or lost, as the delivery ladder does.
|
|
8416
|
+
if (this.backend?.readInputRow) {
|
|
8417
|
+
const deadline = Date.now() + POST_ENTER_PROOF_WINDOW_MS;
|
|
8418
|
+
while (proof !== "submitted" && Date.now() < deadline) {
|
|
8419
|
+
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
8420
|
+
if (guard && !guard.current())
|
|
8421
|
+
return false;
|
|
8422
|
+
proof = await this.confirmSubmitted(signature, baseline);
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8223
8425
|
if (proof === "unverifiable") {
|
|
8224
8426
|
if (this.backend?.isDeliveryInputReadyPane) {
|
|
8225
8427
|
// A Codex screen without a structurally current input/footer pair is
|