@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.3
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/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +10 -7
- package/dist/agent-endpoint.js +21 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +141 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +2 -2
- package/dist/auth-api.js +4 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +14 -2
- package/dist/backend/kiro.js +84 -9
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +4 -2
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -1
- package/dist/channel/adapters/discord.js +23 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +19 -1
- package/dist/channel/adapters/telegram.js +60 -9
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +23 -12
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.js +9 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +14 -2
- package/dist/cli.js +55 -16
- package/dist/cli.js.map +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +49 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +27 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +48 -0
- package/dist/daemon.js +250 -22
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +17 -2
- package/dist/delivery-outbox.js +13 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-manager.d.ts +63 -1
- package/dist/fleet-manager.js +314 -20
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +9 -6
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +11 -2
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +3 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +10 -7
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +36 -26
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +28 -15
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +10 -2
- package/dist/outbound-schemas.js +15 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +7 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/service-activation.js +4 -3
- package/dist/service-activation.js.map +1 -1
- package/dist/settings-api.js +71 -21
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +70 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +26 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- package/dist/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-monitor.d.ts +8 -0
- package/dist/transcript-monitor.js +26 -2
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +54 -0
- package/dist/transcript-sources.js +188 -27
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-render.js +12 -0
- package/dist/ui/chat-store.js +1 -1
- package/dist/ui/chat-strings.js +12 -12
- package/dist/ui/chat-thread.js +23 -4
- package/dist/ui/panel-chat.js +16 -3
- package/dist/ui/panel-needs.js +5 -2
- package/dist/ui/panel-settings.js +129 -31
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +44 -4
- package/dist/ui/shared/app-i18n.js +37 -15
- package/dist/ui/shared/app-prefs.js +249 -0
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +17 -4
- package/dist/ui/shared/app-store.js +3 -1
- package/dist/ui/shared/app.css +55 -0
- package/dist/ui/shared/app.js +6 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +169 -32
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +13 -4
- package/dist/ui/shared/view-strings.js +22 -4
- package/dist/ui/signin.html +1 -1
- package/dist/update-install.d.ts +15 -1
- package/dist/update-install.js +6 -0
- package/dist/update-install.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +21 -1
- package/dist/web-api.js +157 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +7 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-prefs.d.ts +63 -0
- package/dist/web-prefs.js +263 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-session.d.ts +36 -1
- package/dist/web-session.js +53 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +21 -0
- package/dist/web-settings-patch.js +74 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AGENT_WHATS_NEW, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
|
|
1
2
|
import { join, dirname, basename, resolve } from "node:path";
|
|
2
3
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -77,10 +78,18 @@ const REPLY_GUARD_IDLE_CONFIRM_MS = 60_000;
|
|
|
77
78
|
* down like a turn the transcript cannot place — never a recovery.
|
|
78
79
|
*/
|
|
79
80
|
const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
|
|
81
|
+
/**
|
|
82
|
+
* #1641: an open turn in the CLI's transcript holds the instance busy (no paste, whatever the pane shows) only while the
|
|
83
|
+
* transcript is still moving: a turn whose end was missed must not pin the instance busy forever. A live turn writes
|
|
84
|
+
* often; a long silent tool run shows its own busy row on the pane.
|
|
85
|
+
*/
|
|
86
|
+
export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
|
|
80
87
|
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.";
|
|
81
88
|
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
82
|
-
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
|
|
83
|
-
function transcriptMarkerEvidence(kind) {
|
|
89
|
+
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
|
|
90
|
+
function transcriptMarkerEvidence(kind, rawPaste = false) {
|
|
91
|
+
if (rawPaste)
|
|
92
|
+
return "transcript-turn-user-entry-raw-paste-text";
|
|
84
93
|
return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
|
|
85
94
|
}
|
|
86
95
|
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
|
|
@@ -472,6 +481,16 @@ const POST_ENTER_PROOF_WINDOW_MS = 3_000;
|
|
|
472
481
|
/** A final bounded observation before an ambiguous Codex submit can be classified. */
|
|
473
482
|
const CODEX_LATE_PROOF_MS = 5_000;
|
|
474
483
|
const POST_ENTER_PROOF_POLL_MS = 250;
|
|
484
|
+
/** #1647: a raw paste a pane with no transcript showed taken — its text left the input row (or the CLI's own busy marker
|
|
485
|
+
* replaced it), with output after the Enter. */
|
|
486
|
+
const RAW_PASTE_LEFT_INPUT = "raw-paste-left-input-after-output";
|
|
487
|
+
/**
|
|
488
|
+
* #1647/#1663 P1-2: backends whose transcript AgEnD holds for this instance alone, so a raw paste's exact text in it is
|
|
489
|
+
* this paste's turn. claude-code: the path is its own statusline.json's transcript_path (written by this instance's
|
|
490
|
+
* CLI). Not codex: its rollout is chosen by working directory, which another instance can share, and AgEnD holds no
|
|
491
|
+
* session id to bind it — so a codex raw paste the pane could not prove stays uncertain.
|
|
492
|
+
*/
|
|
493
|
+
const RAW_TEXT_TRANSCRIPT_BACKENDS = ["claude-code"];
|
|
475
494
|
/**
|
|
476
495
|
* A readerless CLI's delivery is proven by its own transcript, which lags the pane by a flush. Only a delta that was
|
|
477
496
|
* read and carried no marker for this long is held against the delivery — a late flush must not raise a false ⚠️.
|
|
@@ -1237,6 +1256,11 @@ export class Daemon extends EventEmitter {
|
|
|
1237
1256
|
turnReplyGuard = new TurnReplyGuard();
|
|
1238
1257
|
/** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
|
|
1239
1258
|
transcriptTurns = new TranscriptTurnLedger();
|
|
1259
|
+
/** #1641: when this launch's transcript monitor last read anything (a turn boundary, a tool, assistant text); performance.now(). */
|
|
1260
|
+
transcriptActivityAt = 0;
|
|
1261
|
+
/** #1641: the launch and composer a stuck pane's idle evidence is about, and since when (performance.now()); see recoverStuck. */
|
|
1262
|
+
stuckRecoveryKey = null;
|
|
1263
|
+
stuckRecoverySince = 0;
|
|
1240
1264
|
/** #1510: since when (monotonic) this generation has been held as running in the transcript. */
|
|
1241
1265
|
transcriptRunningSince = null;
|
|
1242
1266
|
/** Prevent a visible stale XML fragment from being recovered on later turns. */
|
|
@@ -1388,6 +1412,8 @@ export class Daemon extends EventEmitter {
|
|
|
1388
1412
|
recentPastes = [];
|
|
1389
1413
|
pendingInstructionsUpdate;
|
|
1390
1414
|
pendingInstructionsNotice = false;
|
|
1415
|
+
/** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
|
|
1416
|
+
pendingWhatsNew = null;
|
|
1391
1417
|
// Whether the warmup steering-reload notice should be injected after spawn.
|
|
1392
1418
|
// Set in trySpawn by comparing the freshly-built instructions against the
|
|
1393
1419
|
// last value the agent was told about (prev-instructions). Skipped when
|
|
@@ -1856,8 +1882,14 @@ export class Daemon extends EventEmitter {
|
|
|
1856
1882
|
this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
|
|
1857
1883
|
return;
|
|
1858
1884
|
}
|
|
1885
|
+
// #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
|
|
1886
|
+
if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
|
|
1887
|
+
this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1859
1890
|
this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
|
|
1860
|
-
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof
|
|
1891
|
+
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
|
|
1892
|
+
: "positive submission proof", verdict);
|
|
1861
1893
|
}
|
|
1862
1894
|
/**
|
|
1863
1895
|
* Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
|
|
@@ -1873,23 +1905,28 @@ export class Daemon extends EventEmitter {
|
|
|
1873
1905
|
* TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
|
|
1874
1906
|
* only ends the wait, falling back to today's outcome.
|
|
1875
1907
|
*/
|
|
1876
|
-
async proveDeliveryFromTranscript(delivery, checkpoint) {
|
|
1908
|
+
async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
|
|
1877
1909
|
const spawn = this.spawnGeneration;
|
|
1878
1910
|
const fence = this.launchFenceEpoch;
|
|
1879
1911
|
let last = "unavailable";
|
|
1880
1912
|
let outcome = "delivered";
|
|
1881
1913
|
let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
1914
|
+
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
1915
|
+
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
1916
|
+
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
1917
|
+
let found = false;
|
|
1882
1918
|
try {
|
|
1883
1919
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
1884
1920
|
if (this.deliveryWritesStopping)
|
|
1885
1921
|
return; // reconciliation owns the row now
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1922
|
+
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
1923
|
+
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
1924
|
+
found = true;
|
|
1888
1925
|
outcome = "delivered";
|
|
1889
|
-
evidence = transcriptMarkerEvidence(
|
|
1926
|
+
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
1890
1927
|
break;
|
|
1891
1928
|
}
|
|
1892
|
-
last =
|
|
1929
|
+
last = look;
|
|
1893
1930
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
1894
1931
|
last = "unavailable";
|
|
1895
1932
|
break;
|
|
@@ -1909,6 +1946,11 @@ export class Daemon extends EventEmitter {
|
|
|
1909
1946
|
catch (err) {
|
|
1910
1947
|
this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
|
|
1911
1948
|
}
|
|
1949
|
+
if (rawPaste && !found) {
|
|
1950
|
+
outcome = "uncertain";
|
|
1951
|
+
evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
|
|
1952
|
+
this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
|
|
1953
|
+
}
|
|
1912
1954
|
// The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
|
|
1913
1955
|
// promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
|
|
1914
1956
|
// Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
|
|
@@ -3719,7 +3761,18 @@ export class Daemon extends EventEmitter {
|
|
|
3719
3761
|
if (delivered)
|
|
3720
3762
|
this.finishDurableSubmission(durableAttempt ?? null, verdict);
|
|
3721
3763
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
3722
|
-
|
|
3764
|
+
const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
|
|
3765
|
+
const checkpoint = verdict.transcriptCheckpoint;
|
|
3766
|
+
if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
|
|
3767
|
+
// #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
|
|
3768
|
+
// than the pane window sometimes. Wait for it there (bounded), off the paste lock.
|
|
3769
|
+
this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
|
|
3770
|
+
void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
|
|
3771
|
+
}
|
|
3772
|
+
else {
|
|
3773
|
+
this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
|
|
3774
|
+
this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
|
|
3775
|
+
}
|
|
3723
3776
|
}
|
|
3724
3777
|
else if (durableAttempt && verdict.durableBeginCommitted) {
|
|
3725
3778
|
this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
|
|
@@ -5041,9 +5094,68 @@ export class Daemon extends EventEmitter {
|
|
|
5041
5094
|
buildTranscriptMonitor(source) {
|
|
5042
5095
|
const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
|
|
5043
5096
|
this.transcriptTurns.reset();
|
|
5044
|
-
|
|
5097
|
+
this.transcriptActivityAt = 0;
|
|
5098
|
+
// Monotonic: a wall-clock correction must neither end nor extend the open-turn hold (#1641 R1).
|
|
5099
|
+
const touch = () => { this.transcriptActivityAt = performance.now(); };
|
|
5100
|
+
monitor.on("turns", (turns) => { this.transcriptTurns.observe(turns, performance.now()); touch(); });
|
|
5101
|
+
// #1641 R2: a re-baselined source starts over, and so does what it said (an open turn read before is withdrawn).
|
|
5102
|
+
monitor.on("reset", () => { if (this.transcriptMonitor === monitor) {
|
|
5103
|
+
this.transcriptTurns.reset();
|
|
5104
|
+
this.transcriptActivityAt = 0;
|
|
5105
|
+
} });
|
|
5106
|
+
for (const event of ["tool_use", "tool_result", "assistant_text"])
|
|
5107
|
+
monitor.on(event, touch);
|
|
5045
5108
|
return monitor;
|
|
5046
5109
|
}
|
|
5110
|
+
/**
|
|
5111
|
+
* #1641: what the CLI's own transcript (codex's rollout) says about its turn. `open`: a turn started and has not ended,
|
|
5112
|
+
* and the transcript moved within TRANSCRIPT_OPEN_TURN_QUIET_MS. `closed`: every turn read since this launch's monitor
|
|
5113
|
+
* began has ended. `unknown`: not a transcript backend, nothing followed, or an open turn gone quiet (a missed end).
|
|
5114
|
+
*/
|
|
5115
|
+
transcriptTurnState(now = performance.now()) {
|
|
5116
|
+
if (this.backend?.turnEndFromTranscript !== true || this.transcriptMonitor?.following() !== true)
|
|
5117
|
+
return "unknown";
|
|
5118
|
+
// #1641 R2: an empty ledger after a baseline that could not see the current turn's start is not "closed".
|
|
5119
|
+
if (this.transcriptMonitor.turnStateKnown?.() !== true)
|
|
5120
|
+
return "unknown";
|
|
5121
|
+
const since = this.transcriptTurns.openSince();
|
|
5122
|
+
if (since === null)
|
|
5123
|
+
return "closed";
|
|
5124
|
+
return now - Math.max(since, this.transcriptActivityAt) < TRANSCRIPT_OPEN_TURN_QUIET_MS ? "open" : "unknown";
|
|
5125
|
+
}
|
|
5126
|
+
/**
|
|
5127
|
+
* #1641: a stuck pane that is in fact an idle CLI (a footer this build cannot read) must not stay stuck for hours with
|
|
5128
|
+
* every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
|
|
5129
|
+
* or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
|
|
5130
|
+
* ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
|
|
5131
|
+
*/
|
|
5132
|
+
recoverStuck(pane, snapshot, changeAt, turn) {
|
|
5133
|
+
const machine = this.instanceStateMachine;
|
|
5134
|
+
const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
|
|
5135
|
+
// The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
|
|
5136
|
+
if (!machine || !idleComposer || turn === "open" || this.inputTransientInPane(pane)) {
|
|
5137
|
+
this.stuckRecoveryKey = null;
|
|
5138
|
+
return snapshot;
|
|
5139
|
+
}
|
|
5140
|
+
// The evidence is about this launch and this screen. Once it holds, it keeps holding — a still pane reads stuck
|
|
5141
|
+
// again at the next sweep (its footer is still unreadable) and must not start the window over (#1641 R1).
|
|
5142
|
+
const mono = performance.now();
|
|
5143
|
+
const key = `${this.spawnGeneration}:${this.launchAttempt}\n${pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "")}`;
|
|
5144
|
+
if (key !== this.stuckRecoveryKey) {
|
|
5145
|
+
this.stuckRecoveryKey = key;
|
|
5146
|
+
this.stuckRecoverySince = mono;
|
|
5147
|
+
}
|
|
5148
|
+
if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
|
|
5149
|
+
return snapshot;
|
|
5150
|
+
// A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
|
|
5151
|
+
// recovered again. Only the edge into idle is news.
|
|
5152
|
+
const fields = { evidence: turn === "closed" ? "transcript_no_open_turn" : "stable_empty_composer", unchangedForMs: snapshot.unchangedForMs };
|
|
5153
|
+
if (this.instanceState !== "idle")
|
|
5154
|
+
this.logger.info(fields, "Stuck instance recovered — the CLI shows its idle composer and is not running a turn (#1641)");
|
|
5155
|
+
else
|
|
5156
|
+
this.logger.debug(fields, "Still idle on its unreadable footer — the stuck reading was recovered again (#1641)");
|
|
5157
|
+
return machine.observe(pane, Date.now(), { settled: true, changeAt, forceReady: true });
|
|
5158
|
+
}
|
|
5047
5159
|
replyCompletionGuardEnabled() {
|
|
5048
5160
|
return this.config.reply_completion_guard !== false
|
|
5049
5161
|
&& this.backend?.replyCompletionGuard === true;
|
|
@@ -5425,7 +5537,9 @@ export class Daemon extends EventEmitter {
|
|
|
5425
5537
|
// a pair every ~1.6s for as long as the repaint lasted (#1116). Real
|
|
5426
5538
|
// work cannot hide behind this: it changes the layout, the next
|
|
5427
5539
|
// output arms another probe, and the debounce capture still fires.
|
|
5428
|
-
|
|
5540
|
+
// #1641: never while the transcript shows a turn running — that idle-looking screen is mid-turn.
|
|
5541
|
+
if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
|
|
5542
|
+
&& this.transcriptTurnState() !== "open")
|
|
5429
5543
|
return;
|
|
5430
5544
|
// A second burst overtook the probe. Do not bless its stale capture as
|
|
5431
5545
|
// idle: continuous output is the safer working signal, while the next
|
|
@@ -5458,6 +5572,10 @@ export class Daemon extends EventEmitter {
|
|
|
5458
5572
|
// decisions about *now*. The old double-observe expressed that by calling
|
|
5459
5573
|
// observe twice, which silently disabled the "content moved" branch.
|
|
5460
5574
|
const structuredPeriodicIdle = this.backend?.isPeriodicRedrawIdlePane;
|
|
5575
|
+
// #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
|
|
5576
|
+
// hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
|
|
5577
|
+
const transcriptTurn = this.transcriptTurnState();
|
|
5578
|
+
const transcriptBusy = transcriptTurn === "open";
|
|
5461
5579
|
let snapshot;
|
|
5462
5580
|
if (deliveryFallback !== null) {
|
|
5463
5581
|
// A missing footer cannot fall through to a broad ready match. All
|
|
@@ -5466,7 +5584,7 @@ export class Daemon extends EventEmitter {
|
|
|
5466
5584
|
settled: true,
|
|
5467
5585
|
changeAt: observedChangeAt,
|
|
5468
5586
|
forceReady: deliveryFallback,
|
|
5469
|
-
forceBusy: !deliveryFallback || paneActivity !== null,
|
|
5587
|
+
forceBusy: !deliveryFallback || paneActivity !== null || transcriptBusy,
|
|
5470
5588
|
});
|
|
5471
5589
|
}
|
|
5472
5590
|
else if (structuredPeriodicIdle && reason === "output_probe") {
|
|
@@ -5488,6 +5606,7 @@ export class Daemon extends EventEmitter {
|
|
|
5488
5606
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5489
5607
|
settled: true,
|
|
5490
5608
|
changeAt: observedChangeAt,
|
|
5609
|
+
forceBusy: transcriptBusy,
|
|
5491
5610
|
});
|
|
5492
5611
|
}
|
|
5493
5612
|
}
|
|
@@ -5521,6 +5640,7 @@ export class Daemon extends EventEmitter {
|
|
|
5521
5640
|
settled: true,
|
|
5522
5641
|
changeAt: observedChangeAt,
|
|
5523
5642
|
forceReady: true,
|
|
5643
|
+
forceBusy: transcriptBusy,
|
|
5524
5644
|
});
|
|
5525
5645
|
}
|
|
5526
5646
|
else {
|
|
@@ -5569,6 +5689,7 @@ export class Daemon extends EventEmitter {
|
|
|
5569
5689
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5570
5690
|
settled: true,
|
|
5571
5691
|
changeAt: observedChangeAt,
|
|
5692
|
+
forceBusy: transcriptBusy,
|
|
5572
5693
|
});
|
|
5573
5694
|
}
|
|
5574
5695
|
else {
|
|
@@ -5586,9 +5707,11 @@ export class Daemon extends EventEmitter {
|
|
|
5586
5707
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5587
5708
|
settled,
|
|
5588
5709
|
changeAt: observedChangeAt,
|
|
5589
|
-
forceBusy: paneActivity !== null,
|
|
5710
|
+
forceBusy: paneActivity !== null || transcriptBusy,
|
|
5590
5711
|
});
|
|
5591
5712
|
}
|
|
5713
|
+
if (snapshot.state === "stuck")
|
|
5714
|
+
snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
|
|
5592
5715
|
this.applyInstanceStateSnapshot(snapshot, pane);
|
|
5593
5716
|
// Backends without a transcript feed can still say what they are doing, if
|
|
5594
5717
|
// their TUI names it. Free-riding on a capture that already happened: no
|
|
@@ -6011,12 +6134,14 @@ export class Daemon extends EventEmitter {
|
|
|
6011
6134
|
* here — a steered message must read EXACTLY like a queued one to the
|
|
6012
6135
|
* agent, or the two drift apart in what the agent is told about replying.
|
|
6013
6136
|
*/
|
|
6137
|
+
/** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
|
|
6138
|
+
agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
|
|
6014
6139
|
formatInboundMessage(content, meta) {
|
|
6015
6140
|
const user = meta.user || "unknown";
|
|
6016
6141
|
const fromInstance = meta.from_instance;
|
|
6017
6142
|
let formatted;
|
|
6018
6143
|
if (fromInstance) {
|
|
6019
|
-
formatted = formatCrossInstanceInboundMessage(content, meta);
|
|
6144
|
+
formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
|
|
6020
6145
|
}
|
|
6021
6146
|
else {
|
|
6022
6147
|
const via = meta.source ? ` via ${meta.source}` : "";
|
|
@@ -6029,7 +6154,10 @@ export class Daemon extends EventEmitter {
|
|
|
6029
6154
|
formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
|
|
6030
6155
|
}
|
|
6031
6156
|
formatted += renderHandoffMetadata(meta);
|
|
6032
|
-
|
|
6157
|
+
// #1650: a CLI-mode agent has no reply tool — it answers with the agend-agent command.
|
|
6158
|
+
formatted += this.agentMode() === "cli"
|
|
6159
|
+
? '\n(Reply with agend-agent reply "…" — do NOT respond with direct text)'
|
|
6160
|
+
: "\n(Reply using the reply tool — do NOT respond with direct text)";
|
|
6033
6161
|
}
|
|
6034
6162
|
if (meta.reply_to_text) {
|
|
6035
6163
|
formatted += `\n(reply_to: "${meta.reply_to_text}")`;
|
|
@@ -6291,6 +6419,23 @@ export class Daemon extends EventEmitter {
|
|
|
6291
6419
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6292
6420
|
return;
|
|
6293
6421
|
}
|
|
6422
|
+
const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
6423
|
+
if (whatsNew) {
|
|
6424
|
+
// #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
|
|
6425
|
+
// instance nobody writes to. Recorded only once pasted: a failed or cancelled paste stays pending.
|
|
6426
|
+
const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch });
|
|
6427
|
+
if (pasted && this.pendingWhatsNew === whatsNew) {
|
|
6428
|
+
this.pendingWhatsNew = null;
|
|
6429
|
+
try {
|
|
6430
|
+
recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
|
|
6431
|
+
}
|
|
6432
|
+
catch (err) {
|
|
6433
|
+
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6437
|
+
return;
|
|
6438
|
+
}
|
|
6294
6439
|
const status = channelStatus(meta);
|
|
6295
6440
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
6296
6441
|
// the previous turn's tool list.
|
|
@@ -7663,6 +7808,33 @@ export class Daemon extends EventEmitter {
|
|
|
7663
7808
|
this.logger.warn("Could not read the pane after Enter — not confirming the submission");
|
|
7664
7809
|
return sawOutput && residue === "absent";
|
|
7665
7810
|
}
|
|
7811
|
+
/**
|
|
7812
|
+
* #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
|
|
7813
|
+
* CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
|
|
7814
|
+
* own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
|
|
7815
|
+
* pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
|
|
7816
|
+
*
|
|
7817
|
+
* Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
|
|
7818
|
+
* post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
|
|
7819
|
+
* caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
|
|
7820
|
+
* or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
|
|
7821
|
+
*/
|
|
7822
|
+
async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
|
|
7823
|
+
const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
|
|
7824
|
+
const live = () => current() && performance.now() <= deadline;
|
|
7825
|
+
for (;;) {
|
|
7826
|
+
if (!live())
|
|
7827
|
+
return null;
|
|
7828
|
+
if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
|
|
7829
|
+
const residue = await this.pasteResidue(formatted);
|
|
7830
|
+
if (!live())
|
|
7831
|
+
return null;
|
|
7832
|
+
if (residue === "absent")
|
|
7833
|
+
return RAW_PASTE_LEFT_INPUT;
|
|
7834
|
+
}
|
|
7835
|
+
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7666
7838
|
/** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
|
|
7667
7839
|
async kiroSubmissionEvidence(windowId, formatted, signature) {
|
|
7668
7840
|
if (!this.tmux)
|
|
@@ -7975,9 +8147,11 @@ export class Daemon extends EventEmitter {
|
|
|
7975
8147
|
offset: attemptEvidence.transcriptOffset,
|
|
7976
8148
|
};
|
|
7977
8149
|
// From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
|
|
7978
|
-
// reply, or still be painting the paste) — this delivery's exact marker only.
|
|
7979
|
-
|
|
7980
|
-
|
|
8150
|
+
// reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
|
|
8151
|
+
// A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
|
|
8152
|
+
if (["claude-code", "codex"].includes(attemptEvidence.backend)
|
|
8153
|
+
&& (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
|
|
8154
|
+
signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
|
|
7981
8155
|
}
|
|
7982
8156
|
}
|
|
7983
8157
|
}
|
|
@@ -8068,6 +8242,7 @@ export class Daemon extends EventEmitter {
|
|
|
8068
8242
|
steerEnterDeadline = deadline;
|
|
8069
8243
|
}
|
|
8070
8244
|
let enterAt = Date.now();
|
|
8245
|
+
const enterMono = performance.now();
|
|
8071
8246
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
8072
8247
|
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
|
|
8073
8248
|
if (steerComposerProof && !steerCurrent()) {
|
|
@@ -8313,12 +8488,20 @@ export class Daemon extends EventEmitter {
|
|
|
8313
8488
|
return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
|
|
8314
8489
|
}
|
|
8315
8490
|
}
|
|
8316
|
-
if (!submitted) {
|
|
8317
|
-
|
|
8491
|
+
if (!submitted && rawPaste) {
|
|
8492
|
+
// #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
|
|
8493
|
+
// paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
|
|
8494
|
+
// never a second Enter (a raw command must not run twice).
|
|
8495
|
+
const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
|
|
8496
|
+
if (!taken) {
|
|
8318
8497
|
verdict.phase = "raw-paste-submit-proof";
|
|
8319
8498
|
verdict.proof = "no-positive-acceptance-proof";
|
|
8320
8499
|
return false;
|
|
8321
8500
|
}
|
|
8501
|
+
verdict.proof = taken;
|
|
8502
|
+
submitted = true;
|
|
8503
|
+
}
|
|
8504
|
+
if (!submitted) {
|
|
8322
8505
|
// Legacy system pastes have no unique envelope id. Preserve their
|
|
8323
8506
|
// existing bounded retry until they can be tied to a specific turn.
|
|
8324
8507
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
@@ -8631,10 +8814,10 @@ export class Daemon extends EventEmitter {
|
|
|
8631
8814
|
* A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
|
|
8632
8815
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
8633
8816
|
*/
|
|
8634
|
-
transcriptProofFor(checkpoint,
|
|
8817
|
+
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
8635
8818
|
const launchAtWrite = this.launchFenceEpoch;
|
|
8636
8819
|
return {
|
|
8637
|
-
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend,
|
|
8820
|
+
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
8638
8821
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
8639
8822
|
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
|
|
8640
8823
|
onProof,
|
|
@@ -10426,6 +10609,49 @@ export class Daemon extends EventEmitter {
|
|
|
10426
10609
|
}
|
|
10427
10610
|
catch { /* non-fatal */ }
|
|
10428
10611
|
}
|
|
10612
|
+
/**
|
|
10613
|
+
* #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
|
|
10614
|
+
* and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
|
|
10615
|
+
* entries recorded meanwhile are dropped from it; none left → nothing.
|
|
10616
|
+
*/
|
|
10617
|
+
currentWhatsNew(pending) {
|
|
10618
|
+
const record = readWhatsNewRecord(this.instanceDir);
|
|
10619
|
+
if (record === "unreadable") {
|
|
10620
|
+
this.pendingWhatsNew = null;
|
|
10621
|
+
return null;
|
|
10622
|
+
}
|
|
10623
|
+
const known = new Set(record?.announced ?? []);
|
|
10624
|
+
const ids = pending.ids.filter(id => !known.has(id));
|
|
10625
|
+
if (!ids.length) {
|
|
10626
|
+
this.pendingWhatsNew = null;
|
|
10627
|
+
return null;
|
|
10628
|
+
}
|
|
10629
|
+
if (ids.length === pending.ids.length)
|
|
10630
|
+
return pending;
|
|
10631
|
+
const narrowed = { notice: whatsNewNotice(pending.version, AGENT_WHATS_NEW.filter(e => ids.includes(e.id))), ids, version: pending.version };
|
|
10632
|
+
this.pendingWhatsNew = narrowed;
|
|
10633
|
+
return narrowed;
|
|
10634
|
+
}
|
|
10635
|
+
/** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
|
|
10636
|
+
prepareWhatsNew(fresh, version = agendVersion()) {
|
|
10637
|
+
try {
|
|
10638
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version });
|
|
10639
|
+
if (!plan) {
|
|
10640
|
+
this.pendingWhatsNew = null;
|
|
10641
|
+
return;
|
|
10642
|
+
}
|
|
10643
|
+
if ("silent" in plan) {
|
|
10644
|
+
recordWhatsNew(this.instanceDir, plan.silent, plan.version);
|
|
10645
|
+
this.pendingWhatsNew = null;
|
|
10646
|
+
return;
|
|
10647
|
+
}
|
|
10648
|
+
this.pendingWhatsNew = plan;
|
|
10649
|
+
this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
10650
|
+
}
|
|
10651
|
+
catch (err) {
|
|
10652
|
+
this.logger.debug({ err }, "what's-new not prepared");
|
|
10653
|
+
}
|
|
10654
|
+
}
|
|
10429
10655
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
10430
10656
|
this.startupAdmission?.();
|
|
10431
10657
|
const launchFence = this.launchFenceEpoch;
|
|
@@ -10435,6 +10661,8 @@ export class Daemon extends EventEmitter {
|
|
|
10435
10661
|
// instructionsReloadedOnResume) so the warmup steering-reload can be
|
|
10436
10662
|
// skipped when nothing changed.
|
|
10437
10663
|
this.lastBuiltInstructions = backendConfig.instructions ?? "";
|
|
10664
|
+
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
10665
|
+
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
10438
10666
|
{
|
|
10439
10667
|
const prevFile = join(this.instanceDir, "prev-instructions");
|
|
10440
10668
|
let prev = "";
|