@songsid/agend 2.1.6-beta.14 → 2.1.6-beta.15
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/daemon.d.ts +4 -0
- package/dist/daemon.js +147 -4
- package/dist/daemon.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.d.ts
CHANGED
|
@@ -1143,6 +1143,10 @@ export declare class Daemon extends EventEmitter {
|
|
|
1143
1143
|
* (dropped) Enter — and an unreadable pane is not a verified absence either.
|
|
1144
1144
|
*/
|
|
1145
1145
|
private confirmSubmittedAfterEnter;
|
|
1146
|
+
/** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
|
|
1147
|
+
private kiroSubmissionEvidence;
|
|
1148
|
+
/** Observe the Kiro paste outside paneWriteLock; a long agent turn must not block dialog handling. */
|
|
1149
|
+
private observeKiroSubmission;
|
|
1146
1150
|
/**
|
|
1147
1151
|
* While the CLI is parked on a fatal startup screen (see fatalStartupBlocked),
|
|
1148
1152
|
* no pane write may happen: paste+Enter into the corrupt-config modal would
|
package/dist/daemon.js
CHANGED
|
@@ -449,6 +449,10 @@ export class BackendUnreachableStartupError extends Error {
|
|
|
449
449
|
}
|
|
450
450
|
/** Bounded wait (under the pane lock) for the prompt to return before retrying a dropped Enter. */
|
|
451
451
|
const STRANDED_RETRY_READY_WAIT_MS = 30_000;
|
|
452
|
+
/** A pasted Kiro message may be processing for longer than the retry window. */
|
|
453
|
+
const KIRO_SUBMISSION_OBSERVE_MS = 10 * 60_000;
|
|
454
|
+
const KIRO_SUBMISSION_POLL_MS = 1_000;
|
|
455
|
+
const KIRO_SUBMISSION_UNREADABLE_POLLS = 40;
|
|
452
456
|
/** A passive startup phase must clear within this bound before an Enter is sent. */
|
|
453
457
|
/**
|
|
454
458
|
* How long a passive startup/resume transient may sit WITHOUT REPAINTING before
|
|
@@ -4244,6 +4248,8 @@ export class Daemon extends EventEmitter {
|
|
|
4244
4248
|
// A false return means the cap expired with a spawn STILL running — the
|
|
4245
4249
|
// generation captured below is in-progress evidence, not settled evidence,
|
|
4246
4250
|
// so the critical section must still back out (see settledClean).
|
|
4251
|
+
if (this.spawnSettled && status)
|
|
4252
|
+
this.emit("message_queued", status);
|
|
4247
4253
|
const settledClean = await this.waitForSpawnToSettle();
|
|
4248
4254
|
// Everything captured below (window id, readiness verdicts) belongs to
|
|
4249
4255
|
// this spawn generation. A spawn that starts afterwards is detected inside
|
|
@@ -4385,6 +4391,9 @@ export class Daemon extends EventEmitter {
|
|
|
4385
4391
|
}
|
|
4386
4392
|
return this.deliverMessage(formatted, status, { ...opts, spawnRetry: retries });
|
|
4387
4393
|
}
|
|
4394
|
+
if (typeof outcome === "object") {
|
|
4395
|
+
return this.observeKiroSubmission(formatted, outcome, status, verdict, cancelled);
|
|
4396
|
+
}
|
|
4388
4397
|
if (outcome !== "dialog")
|
|
4389
4398
|
return outcome;
|
|
4390
4399
|
// Wait OUTSIDE the lock (holding it would starve the runtime dismisser),
|
|
@@ -4913,6 +4922,126 @@ export class Daemon extends EventEmitter {
|
|
|
4913
4922
|
this.logger.warn("Could not read the pane after Enter — not confirming the submission");
|
|
4914
4923
|
return sawOutput && residue === "absent";
|
|
4915
4924
|
}
|
|
4925
|
+
/** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
|
|
4926
|
+
async kiroSubmissionEvidence(windowId, formatted, signature) {
|
|
4927
|
+
if (!this.tmux)
|
|
4928
|
+
return "unreadable";
|
|
4929
|
+
let pane;
|
|
4930
|
+
try {
|
|
4931
|
+
pane = await this.tmux.capturePane();
|
|
4932
|
+
}
|
|
4933
|
+
catch {
|
|
4934
|
+
return "unreadable";
|
|
4935
|
+
}
|
|
4936
|
+
const prompt = this.backend?.getBottomReadyPattern?.();
|
|
4937
|
+
if (prompt && bottomRowIsReady(pane, prompt)
|
|
4938
|
+
&& inputShowsPastedText(inputAreaText(pane, prompt) ?? "", signature.value))
|
|
4939
|
+
return "stranded";
|
|
4940
|
+
if (prompt && pasteLeftInInput(pane, prompt, formatted))
|
|
4941
|
+
return "stranded";
|
|
4942
|
+
// During generation the same text can still be typeahead for an older
|
|
4943
|
+
// turn. A history hit is safe only after this pane returns to its prompt.
|
|
4944
|
+
if (!signature.unique || !this.tmux.capturePaneWithHistory
|
|
4945
|
+
|| !(await this.isPaneReadyForDelivery(windowId)))
|
|
4946
|
+
return "unknown";
|
|
4947
|
+
try {
|
|
4948
|
+
const history = await this.tmux.capturePaneWithHistory(300);
|
|
4949
|
+
// History includes the CURRENT input row. The earlier viewport capture
|
|
4950
|
+
// and readiness probe may belong to an older frame: Kiro can finish a
|
|
4951
|
+
// turn and expose our unsent typeahead between those reads. Judge both
|
|
4952
|
+
// readiness and input residue on this same, final snapshot before its
|
|
4953
|
+
// message id can count as a submitted transcript echo.
|
|
4954
|
+
if (!prompt || !bottomRowIsReady(history, prompt))
|
|
4955
|
+
return "unknown";
|
|
4956
|
+
if (inputShowsPastedText(inputAreaText(history, prompt) ?? "", signature.value)
|
|
4957
|
+
|| pasteLeftInInput(history, prompt, formatted))
|
|
4958
|
+
return "stranded";
|
|
4959
|
+
if (countOccurrences(history.replace(/\s+/g, ""), signature.value) > 0)
|
|
4960
|
+
return "submitted";
|
|
4961
|
+
}
|
|
4962
|
+
catch { /* missing history cannot establish a verdict */ }
|
|
4963
|
+
return "unknown";
|
|
4964
|
+
}
|
|
4965
|
+
/** Observe the Kiro paste outside paneWriteLock; a long agent turn must not block dialog handling. */
|
|
4966
|
+
async observeKiroSubmission(formatted, pending, status, verdict, cancelled) {
|
|
4967
|
+
const deadline = Date.now() + KIRO_SUBMISSION_OBSERVE_MS;
|
|
4968
|
+
let retriedStrand = false;
|
|
4969
|
+
let unreadable = 0;
|
|
4970
|
+
const current = () => !cancelled()
|
|
4971
|
+
&& !this.spawning
|
|
4972
|
+
&& this.spawnGeneration === pending.spawnGeneration
|
|
4973
|
+
&& this.getWindowId() === pending.windowId
|
|
4974
|
+
&& this.tmux?.getWindowId() === pending.windowId;
|
|
4975
|
+
for (;;) {
|
|
4976
|
+
if (!current())
|
|
4977
|
+
return false;
|
|
4978
|
+
const evidence = await this.kiroSubmissionEvidence(pending.windowId, formatted, pending.signature);
|
|
4979
|
+
if (!current())
|
|
4980
|
+
return false;
|
|
4981
|
+
if (evidence === "submitted") {
|
|
4982
|
+
if (status)
|
|
4983
|
+
this.emit("message_confirmed", status);
|
|
4984
|
+
return true;
|
|
4985
|
+
}
|
|
4986
|
+
unreadable = evidence === "unreadable" ? unreadable + 1 : 0;
|
|
4987
|
+
if (unreadable >= KIRO_SUBMISSION_UNREADABLE_POLLS) {
|
|
4988
|
+
return this.failDelivery(verdict, status, "post-submit-proof", "pane-unreadable");
|
|
4989
|
+
}
|
|
4990
|
+
if (evidence === "stranded" && await this.isPaneReadyForDelivery(pending.windowId)) {
|
|
4991
|
+
if (!current())
|
|
4992
|
+
return false;
|
|
4993
|
+
if (retriedStrand)
|
|
4994
|
+
return this.failDelivery(verdict, status, "stranded-text-retry", "stranded");
|
|
4995
|
+
const retry = await this.paneWriteLock.run(async () => {
|
|
4996
|
+
if (!current())
|
|
4997
|
+
return "cancelled";
|
|
4998
|
+
const fresh = await this.kiroSubmissionEvidence(pending.windowId, formatted, pending.signature);
|
|
4999
|
+
if (!current())
|
|
5000
|
+
return "cancelled";
|
|
5001
|
+
if (fresh === "submitted")
|
|
5002
|
+
return "submitted";
|
|
5003
|
+
if (fresh !== "stranded" || !(await this.isPaneReadyForDelivery(pending.windowId)))
|
|
5004
|
+
return "changed";
|
|
5005
|
+
if (!current())
|
|
5006
|
+
return "cancelled";
|
|
5007
|
+
const sent = await this.sendDeliveryEnter("stranded-text-retry", current);
|
|
5008
|
+
if (!current())
|
|
5009
|
+
return "cancelled";
|
|
5010
|
+
return sent ? "sent" : "failed";
|
|
5011
|
+
});
|
|
5012
|
+
if (!current())
|
|
5013
|
+
return false;
|
|
5014
|
+
if (retry === "cancelled")
|
|
5015
|
+
return false;
|
|
5016
|
+
if (retry === "submitted") {
|
|
5017
|
+
if (status)
|
|
5018
|
+
this.emit("message_confirmed", status);
|
|
5019
|
+
return true;
|
|
5020
|
+
}
|
|
5021
|
+
if (retry === "failed")
|
|
5022
|
+
return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
|
|
5023
|
+
if (retry === "sent") {
|
|
5024
|
+
retriedStrand = true;
|
|
5025
|
+
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_WINDOW_MS));
|
|
5026
|
+
continue;
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
const alive = await this.tmux?.isWindowAlive().catch(() => false);
|
|
5030
|
+
if (!current())
|
|
5031
|
+
return false;
|
|
5032
|
+
if (!alive)
|
|
5033
|
+
return this.failDelivery(verdict, status, "post-submit-proof", "window-gone");
|
|
5034
|
+
if (Date.now() >= deadline) {
|
|
5035
|
+
// A live, busy pane and no positive strand are still ambiguous. The
|
|
5036
|
+
// hang detector owns hung CLI recovery; elapsed time alone is not ❌.
|
|
5037
|
+
this.logger.warn("Kiro submission remains unproven; keeping the delivery queued without a false failure");
|
|
5038
|
+
verdict.phase = "post-submit-proof";
|
|
5039
|
+
verdict.proof = "unproven";
|
|
5040
|
+
return false;
|
|
5041
|
+
}
|
|
5042
|
+
await new Promise(r => setTimeout(r, KIRO_SUBMISSION_POLL_MS));
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
4916
5045
|
/**
|
|
4917
5046
|
* While the CLI is parked on a fatal startup screen (see fatalStartupBlocked),
|
|
4918
5047
|
* no pane write may happen: paste+Enter into the corrupt-config modal would
|
|
@@ -4992,9 +5121,13 @@ export class Daemon extends EventEmitter {
|
|
|
4992
5121
|
* precisely so the lock's scope is visible at the call site rather than being
|
|
4993
5122
|
* an invariant maintained by comments.
|
|
4994
5123
|
*/
|
|
4995
|
-
async sendDeliveryEnter(phase) {
|
|
5124
|
+
async sendDeliveryEnter(phase, stillCurrent) {
|
|
4996
5125
|
if (!(await this.waitForInputTransientToClear(phase)))
|
|
4997
5126
|
return false;
|
|
5127
|
+
// A recovery Enter may have waited for a transient while cancel or spawn
|
|
5128
|
+
// replaced the delivery. Check at the last point before the tmux write.
|
|
5129
|
+
if (stillCurrent && !stillCurrent())
|
|
5130
|
+
return false;
|
|
4998
5131
|
const sent = await this.tmux.sendSpecialKey("Enter");
|
|
4999
5132
|
if (!sent) {
|
|
5000
5133
|
this.logger.error({
|
|
@@ -5219,10 +5352,20 @@ export class Daemon extends EventEmitter {
|
|
|
5219
5352
|
// F2: output after Enter is necessary but not sufficient — the paste
|
|
5220
5353
|
// must also have LEFT the input row.
|
|
5221
5354
|
let submitted = await this.confirmSubmittedAfterEnter(windowId, enterAt, formatted);
|
|
5355
|
+
if (!submitted && signature.unique) {
|
|
5356
|
+
submitted = await this.kiroSubmissionEvidence(windowId, formatted, signature) === "submitted";
|
|
5357
|
+
if (!submitted) {
|
|
5358
|
+
// No output edge is not proof of a lost message. Leave the pane
|
|
5359
|
+
// lock before waiting through a long Kiro turn, so runtime dialog
|
|
5360
|
+
// handling can still make progress.
|
|
5361
|
+
if (status)
|
|
5362
|
+
this.emit("message_queued", status);
|
|
5363
|
+
return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5222
5366
|
if (!submitted) {
|
|
5223
|
-
//
|
|
5224
|
-
//
|
|
5225
|
-
// bounded, since we hold the pane lock here.
|
|
5367
|
+
// Legacy system pastes have no unique envelope id. Preserve their
|
|
5368
|
+
// existing bounded retry until they can be tied to a specific turn.
|
|
5226
5369
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
5227
5370
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
5228
5371
|
const retryAt = Date.now();
|