@songsid/agend 2.3.4-beta.7 → 2.3.4-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/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex.js +8 -3
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +39 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +4 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +43 -0
- package/dist/daemon.js +361 -17
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +85 -4
- package/dist/fleet-manager.js +375 -30
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +390 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-lifecycle.d.ts +8 -2
- package/dist/instance-lifecycle.js +37 -6
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/locale.js +47 -1
- package/dist/locale.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +6 -0
- package/dist/needs-you.js +8 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.js +3 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.js +5 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +12 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +2 -0
- package/dist/ui/panel-needs.js +4 -3
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app.css +15 -0
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-strings.js +16 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +8 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -33,14 +33,14 @@ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
|
33
33
|
import { readAgentFile } from "./agent-file-read.js";
|
|
34
34
|
import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
|
|
35
35
|
import { LOGIN_FLOWS } from "./login-flows.js";
|
|
36
|
-
import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
|
|
36
|
+
import { ProgressAccumulator, isSearchProgram, summarizeProgress } from "./tool-progress.js";
|
|
37
37
|
import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
|
|
38
38
|
import { isCrossInstanceVisibility } from "./cross-instance-notice.js";
|
|
39
39
|
import { ContextGuardian } from "./context-guardian.js";
|
|
40
40
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
41
41
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
42
42
|
import { MessageBus } from "./channel/message-bus.js";
|
|
43
|
-
import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
43
|
+
import { shellQuote, KiroCompatibilityError, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
44
44
|
import { getTmuxSession } from "./config.js";
|
|
45
45
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
46
46
|
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
@@ -60,7 +60,8 @@ import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
|
60
60
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
61
61
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
62
62
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
63
|
-
import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
63
|
+
import { markerConsumed, scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
64
|
+
import { QUEUED_HANDOFF_MAX_REDELIVERIES, QueuedHandoffStore } from "./queued-handoffs.js";
|
|
64
65
|
import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
65
66
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
66
67
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
@@ -151,6 +152,8 @@ export const HANDOFF_RECHECK_MS = 1_000;
|
|
|
151
152
|
export const WAIT_STOP_POLL_MS = 250;
|
|
152
153
|
/** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
|
|
153
154
|
export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
|
|
155
|
+
/** #3030: a queued hand-off's watch outlives a long turn (the record outlives the watch in any case). */
|
|
156
|
+
export const QUEUED_HANDOFF_WATCH_LIFETIME_MS = 12 * 60 * 60_000;
|
|
154
157
|
/**
|
|
155
158
|
* An instance publishing more than this many execution-state edges inside the
|
|
156
159
|
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
@@ -343,7 +346,8 @@ export function shellCommandLabel(command) {
|
|
|
343
346
|
if (!program)
|
|
344
347
|
return "";
|
|
345
348
|
const isBareWord = (t) => !!t && /^[A-Za-z][A-Za-z0-9_-]*$/.test(t);
|
|
346
|
-
|
|
349
|
+
// A search program's next word is what it searches for, not a subcommand: `grep <token>` keeps only `grep`.
|
|
350
|
+
const label = isBareWord(tokens[1]) && !isSearchProgram(program) ? `${program} ${tokens[1]}` : program;
|
|
347
351
|
return label.length > 40 ? `${label.slice(0, 39)}…` : label;
|
|
348
352
|
}
|
|
349
353
|
export class PaneStateMachine {
|
|
@@ -1580,6 +1584,12 @@ export class Daemon extends EventEmitter {
|
|
|
1580
1584
|
pendingWhatsNew = null;
|
|
1581
1585
|
pasteQueueDepth = 0;
|
|
1582
1586
|
/** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
|
|
1587
|
+
/** #3030: people's messages a CLI never took (or that a restart left unverifiable), told once per batch. */
|
|
1588
|
+
unsettledHandoffs = { "not-taken": [], unverifiable: [] };
|
|
1589
|
+
unsettledHandoffsTimer = null;
|
|
1590
|
+
/** #3030: since when (monotonic) the CLI has been idle without a break; null while it is not. */
|
|
1591
|
+
instanceIdleSinceMono = null;
|
|
1592
|
+
queuedHandoffStore = null;
|
|
1583
1593
|
cancelledHumans = [];
|
|
1584
1594
|
cancelledHumansTimer = null;
|
|
1585
1595
|
firstDeliveryDelay = new FirstDeliveryDelay();
|
|
@@ -1991,6 +2001,9 @@ export class Daemon extends EventEmitter {
|
|
|
1991
2001
|
this.publishWorkingState("turn_closed");
|
|
1992
2002
|
};
|
|
1993
2003
|
}
|
|
2004
|
+
/** Register each issued HTTP credential with the same-process lifecycle owner. */
|
|
2005
|
+
agentCredentialIssuer;
|
|
2006
|
+
setAgentCredentialIssuer(issue) { this.agentCredentialIssuer = issue; }
|
|
1994
2007
|
/** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
|
|
1995
2008
|
setDeliveryOutboxPort(port) {
|
|
1996
2009
|
this.deliveryOutbox = port;
|
|
@@ -2127,6 +2140,243 @@ export class Daemon extends EventEmitter {
|
|
|
2127
2140
|
logger: this.logger,
|
|
2128
2141
|
});
|
|
2129
2142
|
}
|
|
2143
|
+
handoffStore() {
|
|
2144
|
+
return this.queuedHandoffStore ??= new QueuedHandoffStore(this.instanceDir);
|
|
2145
|
+
}
|
|
2146
|
+
/** #3030: the CLI that holds a queued hand-off; a record is settled only by a later one. */
|
|
2147
|
+
handoffHolder() { return `${this.bootId}:${this.spawnGeneration}`; }
|
|
2148
|
+
/**
|
|
2149
|
+
* #3030: before the paste. A hand-off into a busy Claude Code / Codex queue gets its record — the transcript
|
|
2150
|
+
* checkpoint before the paste, the unique leading marker and what a redelivery needs — or, for a redelivery written
|
|
2151
|
+
* as an ordinary idle submission, its record moves its checkpoint here, so a crash right after this paste is still
|
|
2152
|
+
* judged from the transcript that will show it. A write that did not keep a record runs as before.
|
|
2153
|
+
*/
|
|
2154
|
+
async recordQueuedHandoff(formatted, handingOff, verdict, durableAttempt, humanVerificationId) {
|
|
2155
|
+
const track = verdict.trackHandoff;
|
|
2156
|
+
if (!track)
|
|
2157
|
+
return;
|
|
2158
|
+
if (!handingOff && !track.id) {
|
|
2159
|
+
// An earlier attempt of this delivery was a hand-off; this one is an idle submission.
|
|
2160
|
+
if (verdict.handoff)
|
|
2161
|
+
this.dropQueuedHandoff(verdict);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const backend = this.config.backend ?? this.backend?.binaryName ?? "";
|
|
2165
|
+
if (!["claude-code", "codex"].includes(backend))
|
|
2166
|
+
return;
|
|
2167
|
+
const marker = durableAttempt ? `[agend-delivery-id:${durableAttempt.deliveryId}]`
|
|
2168
|
+
: humanVerificationId ? `[agend-human-id:${humanVerificationId}]` : null;
|
|
2169
|
+
if (!marker || !formatted.startsWith(marker))
|
|
2170
|
+
return;
|
|
2171
|
+
let checkpoint = verdict.transcriptCheckpoint ?? null;
|
|
2172
|
+
if (!checkpoint) {
|
|
2173
|
+
try {
|
|
2174
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2175
|
+
}
|
|
2176
|
+
catch (err) {
|
|
2177
|
+
this.logger.debug({ err }, "Could not checkpoint the transcript before a queued hand-off");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!checkpoint)
|
|
2181
|
+
return; // nothing could judge it later: it runs as before (✅ at the hand-off)
|
|
2182
|
+
const entry = {
|
|
2183
|
+
id: track.id ?? randomUUID(), marker, backend, path: checkpoint.path, offset: checkpoint.offset,
|
|
2184
|
+
content: track.content, meta: track.meta, holder: this.handoffHolder(), createdAt: Date.now(),
|
|
2185
|
+
redeliveries: track.redeliveries ?? 0,
|
|
2186
|
+
};
|
|
2187
|
+
try {
|
|
2188
|
+
const dropped = this.handoffStore().put(entry);
|
|
2189
|
+
if (handingOff)
|
|
2190
|
+
verdict.handoff = entry;
|
|
2191
|
+
for (const old of dropped)
|
|
2192
|
+
this.logger.warn({ handoff: old.id, createdAt: old.createdAt }, "Queued hand-off record dropped: too many on record");
|
|
2193
|
+
}
|
|
2194
|
+
catch (err) {
|
|
2195
|
+
this.logger.warn({ err }, "Could not record a queued hand-off; it runs as before (✅ at the hand-off)");
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* #3030: after pushChannelMessage's write. A hand-off on record whose paste went out is watched (✅ when a turn
|
|
2200
|
+
* takes it); one that was never written goes. A redelivery that ended as an ordinary submission is done; one that a
|
|
2201
|
+
* cancel dropped goes too (a cancel is the person's); one stopped before its write stays for the next CLI.
|
|
2202
|
+
*/
|
|
2203
|
+
afterTrackedWrite(meta, verdict, sent, deliveryEpoch) {
|
|
2204
|
+
try {
|
|
2205
|
+
// A stale epoch with writes still allowed is a cancel (the person's); a stop fences writes too, and keeps records.
|
|
2206
|
+
const cancelled = !this.isDeliveryEpochCurrent(deliveryEpoch) && !this.deliveryWritesStopping;
|
|
2207
|
+
if (verdict.handoff) {
|
|
2208
|
+
if (sent || verdict.paneWriteStarted)
|
|
2209
|
+
this.watchQueuedHandoff(verdict.handoff);
|
|
2210
|
+
else if (!verdict.trackHandoff?.id || cancelled)
|
|
2211
|
+
this.dropQueuedHandoff(verdict);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const id = meta.handoff_redelivery;
|
|
2215
|
+
if (!id)
|
|
2216
|
+
return;
|
|
2217
|
+
if (sent || verdict.paneWriteStarted || cancelled) {
|
|
2218
|
+
this.handoffStore().remove(id);
|
|
2219
|
+
this.logger.info({ handoff: id, sent, proof: verdict.proof ?? null }, "Queued hand-off redelivered as an ordinary submission");
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
catch (err) {
|
|
2223
|
+
this.logger.warn({ err }, "Could not update the queued hand-off record after its write");
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
redeliveryStillOurs(meta) {
|
|
2227
|
+
const holder = this.handoffHolder();
|
|
2228
|
+
if (meta.handoff_holder !== holder)
|
|
2229
|
+
return false;
|
|
2230
|
+
try {
|
|
2231
|
+
return this.handoffStore().get(meta.handoff_redelivery)?.holder === holder;
|
|
2232
|
+
}
|
|
2233
|
+
catch {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
dropQueuedHandoff(verdict) {
|
|
2238
|
+
const entry = verdict.handoff;
|
|
2239
|
+
verdict.handoff = undefined;
|
|
2240
|
+
if (!entry)
|
|
2241
|
+
return;
|
|
2242
|
+
try {
|
|
2243
|
+
this.handoffStore().remove(entry.id);
|
|
2244
|
+
}
|
|
2245
|
+
catch (err) {
|
|
2246
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a queued hand-off record");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* #3030: the hand-off's CLI is the current one: watch its transcript for the turn that takes it (✅ then), and for an
|
|
2251
|
+
* idle CLI that never did (reported). A stop ends the watch; the record stays for the next CLI to settle.
|
|
2252
|
+
*/
|
|
2253
|
+
watchQueuedHandoff(entry) {
|
|
2254
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2255
|
+
const started = consumedWatches.start({
|
|
2256
|
+
deliveryId: `handoff:${entry.id}`,
|
|
2257
|
+
attemptNo: entry.redeliveries,
|
|
2258
|
+
backend: entry.backend,
|
|
2259
|
+
path: entry.path,
|
|
2260
|
+
offset: entry.offset,
|
|
2261
|
+
marker: entry.marker,
|
|
2262
|
+
// A cancel or stop moves the epoch: the watch ends quietly (a cancel cleared the record, a stop leaves it).
|
|
2263
|
+
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch
|
|
2264
|
+
&& epoch === this.deliveryEpoch,
|
|
2265
|
+
consumed: (via) => this.settleQueuedHandoff(entry, "taken", `transcript:${via}`),
|
|
2266
|
+
idleForMs: () => this.instanceIdleSinceMono === null ? null : performance.now() - this.instanceIdleSinceMono,
|
|
2267
|
+
notTaken: () => this.settleQueuedHandoff(entry, "not-taken", "transcript:absent-while-idle"),
|
|
2268
|
+
// A queued message waits for the running turn, however long that is.
|
|
2269
|
+
lifetimeMs: QUEUED_HANDOFF_WATCH_LIFETIME_MS,
|
|
2270
|
+
logger: this.logger,
|
|
2271
|
+
});
|
|
2272
|
+
if (!started) {
|
|
2273
|
+
// No watch (the fleet's cap): ✅ as before; the record still lets a restart judge it.
|
|
2274
|
+
const status = channelStatus(entry.meta);
|
|
2275
|
+
if (status)
|
|
2276
|
+
this.emit("message_confirmed", status);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/** #3030: a record's end — taken (✅), or never taken / not judgeable (❌, told once per batch). */
|
|
2280
|
+
settleQueuedHandoff(entry, outcome, proof) {
|
|
2281
|
+
try {
|
|
2282
|
+
this.handoffStore().remove(entry.id);
|
|
2283
|
+
}
|
|
2284
|
+
catch (err) {
|
|
2285
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a settled queued hand-off record");
|
|
2286
|
+
}
|
|
2287
|
+
const meta = entry.meta;
|
|
2288
|
+
const status = channelStatus(meta);
|
|
2289
|
+
if (outcome === "taken") {
|
|
2290
|
+
this.logger.info({ handoff: entry.id, proof, redeliveries: entry.redeliveries }, "Queued hand-off taken by the CLI");
|
|
2291
|
+
if (status)
|
|
2292
|
+
this.emit("message_confirmed", status);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
this.logger.warn({ handoff: entry.id, outcome, proof, redeliveries: entry.redeliveries }, "Queued hand-off was not taken by the CLI");
|
|
2296
|
+
if (meta.from_instance) {
|
|
2297
|
+
this.reportCrossInstanceDeliveryFailure(meta, { reached: true, phase: "handoff-not-consumed", proof: outcome === "not-taken" ? "not-taken" : "unverifiable" });
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!status)
|
|
2301
|
+
return;
|
|
2302
|
+
this.unsettledHandoffs[outcome].push({ chatId: status.chatId, messageId: status.messageId, ...(status.threadId ? { threadId: status.threadId } : {}) });
|
|
2303
|
+
if (this.unsettledHandoffsTimer)
|
|
2304
|
+
clearTimeout(this.unsettledHandoffsTimer);
|
|
2305
|
+
this.unsettledHandoffsTimer = setTimeout(() => {
|
|
2306
|
+
this.unsettledHandoffsTimer = null;
|
|
2307
|
+
for (const reason of ["not-taken", "unverifiable"]) {
|
|
2308
|
+
const messages = this.unsettledHandoffs[reason].splice(0);
|
|
2309
|
+
if (messages.length)
|
|
2310
|
+
this.emit("human_handoffs_unsettled", { reason, messages });
|
|
2311
|
+
}
|
|
2312
|
+
}, CANCELLED_HUMANS_NOTICE_MS);
|
|
2313
|
+
this.unsettledHandoffsTimer.unref?.();
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* #3030: right after a CLI came up — the records left by CLIs that are gone. Their queue went with them, so each is
|
|
2317
|
+
* judged by its own transcript from its checkpoint: taken there (✅, nothing pasted); proven not taken (the whole
|
|
2318
|
+
* delta read, the marker absent or only enqueued) → redelivered once, with the same marker, through the ordinary
|
|
2319
|
+
* queue; anything that cannot be judged → never pasted, reported. Ownership is evidence: never pasted twice.
|
|
2320
|
+
*/
|
|
2321
|
+
async reconcileQueuedHandoffs() {
|
|
2322
|
+
let entries;
|
|
2323
|
+
try {
|
|
2324
|
+
entries = this.handoffStore().list().filter(e => e.holder !== this.handoffHolder());
|
|
2325
|
+
}
|
|
2326
|
+
catch (err) {
|
|
2327
|
+
this.logger.warn({ err }, "Could not read the queued hand-off records");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (!entries.length)
|
|
2331
|
+
return;
|
|
2332
|
+
// Captured before any await: a reconciliation that outlives its CLI can never claim the next CLI's holder.
|
|
2333
|
+
const holder = this.handoffHolder();
|
|
2334
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2335
|
+
const fresh = () => spawn === this.spawnGeneration && fence === this.launchFenceEpoch && epoch === this.deliveryEpoch
|
|
2336
|
+
&& !this.deliveryWritesStopping;
|
|
2337
|
+
let redelivered = 0, taken = 0, unverifiable = 0;
|
|
2338
|
+
for (const entry of entries) {
|
|
2339
|
+
const kind = await scanTranscriptForDeliveryMarker(entry.path, entry.offset, entry.backend, { marker: entry.marker });
|
|
2340
|
+
// This CLI is gone already, or a cancel came: the next CLI settles them (a cancel cleared them).
|
|
2341
|
+
if (!fresh())
|
|
2342
|
+
return;
|
|
2343
|
+
// …and the record is still the one that was read (another reconciliation or a cancel did not change it meanwhile).
|
|
2344
|
+
let current;
|
|
2345
|
+
try {
|
|
2346
|
+
current = this.handoffStore().get(entry.id);
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
current = undefined;
|
|
2350
|
+
}
|
|
2351
|
+
if (!current || current.holder !== entry.holder || current.redeliveries !== entry.redeliveries)
|
|
2352
|
+
continue;
|
|
2353
|
+
if (markerConsumed(kind)) {
|
|
2354
|
+
taken++;
|
|
2355
|
+
this.settleQueuedHandoff(entry, "taken", `restart-transcript:${kind}`);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
if (kind === "unavailable" || entry.redeliveries >= QUEUED_HANDOFF_MAX_REDELIVERIES) {
|
|
2359
|
+
unverifiable++;
|
|
2360
|
+
this.settleQueuedHandoff(entry, "unverifiable", kind === "unavailable" ? "restart-transcript:unavailable" : "redelivery-limit");
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
// Proven not taken: the CLI that queued it is gone and its transcript, read to the end, never took it.
|
|
2364
|
+
const next = { ...entry, holder, redeliveries: entry.redeliveries + 1 };
|
|
2365
|
+
try {
|
|
2366
|
+
this.handoffStore().put(next);
|
|
2367
|
+
}
|
|
2368
|
+
catch (err) {
|
|
2369
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not update a queued hand-off record; not redelivered now");
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
redelivered++;
|
|
2373
|
+
// The person's receipt was settled by the first write: the redelivery carries its marker, not its receipt binding.
|
|
2374
|
+
const { human_receipt_id: _r, human_owner_key: _o, human_daemon_owner: _d, human_wire_sha256: _w, ...meta } = entry.meta;
|
|
2375
|
+
this.pushChannelMessage(entry.content, { ...meta, handoff_redelivery: entry.id,
|
|
2376
|
+
handoff_redeliveries: String(next.redeliveries), handoff_holder: next.holder }, undefined, epoch);
|
|
2377
|
+
}
|
|
2378
|
+
this.logger.info({ instance: this.name, redelivered, taken, unverifiable, proof: "transcript" }, "Queued hand-offs left by the previous CLI settled");
|
|
2379
|
+
}
|
|
2130
2380
|
finishDurableSubmission(delivery, verdict) {
|
|
2131
2381
|
// Legacy backends may accept a pane write without enough pane evidence to
|
|
2132
2382
|
// claim submission. Keep that row inspectable instead of recording a false
|
|
@@ -2635,6 +2885,8 @@ export class Daemon extends EventEmitter {
|
|
|
2635
2885
|
this.startErrorMonitor();
|
|
2636
2886
|
}
|
|
2637
2887
|
this.startInstanceStateMonitor();
|
|
2888
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
2889
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
2638
2890
|
this.logger.info(`${this.name} ready`);
|
|
2639
2891
|
}
|
|
2640
2892
|
/**
|
|
@@ -3109,8 +3361,6 @@ export class Daemon extends EventEmitter {
|
|
|
3109
3361
|
scheduleNext();
|
|
3110
3362
|
return;
|
|
3111
3363
|
}
|
|
3112
|
-
// The CLI owns the MCP server process, so the daemon can only observe it.
|
|
3113
|
-
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3114
3364
|
// Human-readable backend label for logs (e.g. "claude", "kiro-cli")
|
|
3115
3365
|
const cliLabel = this.backend?.binaryName ?? "CLI";
|
|
3116
3366
|
const healthTmux = this.tmux;
|
|
@@ -3131,6 +3381,9 @@ export class Daemon extends EventEmitter {
|
|
|
3131
3381
|
if (discardStaleHealth())
|
|
3132
3382
|
return;
|
|
3133
3383
|
if (paneStatus?.alive) {
|
|
3384
|
+
// A CLI shutdown also closes its MCP children. Revive tools only for
|
|
3385
|
+
// a positively live pane, never while its exit is being classified.
|
|
3386
|
+
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3134
3387
|
// Capture only a known primary while it lives. No extra tmux call: PID comes
|
|
3135
3388
|
// from this same status read. Fast/unobserved deaths remain ordinary crashes.
|
|
3136
3389
|
const key = this.oomSessionKey();
|
|
@@ -3177,6 +3430,29 @@ export class Daemon extends EventEmitter {
|
|
|
3177
3430
|
return;
|
|
3178
3431
|
}
|
|
3179
3432
|
}
|
|
3433
|
+
// tmux marks the PTY dead when its fd closes, before SIGCHLD can fill
|
|
3434
|
+
// pane_dead_status. Claude /exit (and any other CLI) can hit this gap.
|
|
3435
|
+
// Give the status four bounded re-probes; missing is never exit zero.
|
|
3436
|
+
if (paneStatus && !paneStatus.alive && paneStatus.exitCode === undefined) {
|
|
3437
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
3438
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
3439
|
+
if (discardStaleHealth())
|
|
3440
|
+
return;
|
|
3441
|
+
const observed = await healthTmux.getPaneStatus();
|
|
3442
|
+
if (discardStaleHealth())
|
|
3443
|
+
return;
|
|
3444
|
+
if (observed)
|
|
3445
|
+
paneStatus = observed;
|
|
3446
|
+
if (paneStatus.alive || paneStatus.exitCode !== undefined)
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3449
|
+
if (paneStatus.alive) {
|
|
3450
|
+
this.windowQueryFailureTicks = 0;
|
|
3451
|
+
this.stormWindow?.noteWindowAlive(this.name);
|
|
3452
|
+
scheduleNext();
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3180
3456
|
// paneStatus === null → window gone entirely (e.g. tmux server crash)
|
|
3181
3457
|
// paneStatus.alive === false → pane dead, exit code available
|
|
3182
3458
|
const exitCode = paneStatus?.exitCode;
|
|
@@ -3360,6 +3636,8 @@ export class Daemon extends EventEmitter {
|
|
|
3360
3636
|
try {
|
|
3361
3637
|
await this.spawnClaudeWindow();
|
|
3362
3638
|
this.setProcessStatus("running");
|
|
3639
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3640
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3363
3641
|
this.logger.info("Recovered from background session conflict");
|
|
3364
3642
|
this.emit("crash_respawn", this.name);
|
|
3365
3643
|
}
|
|
@@ -3519,6 +3797,8 @@ export class Daemon extends EventEmitter {
|
|
|
3519
3797
|
catch { /* may not exist */ }
|
|
3520
3798
|
}
|
|
3521
3799
|
this.setProcessStatus("running");
|
|
3800
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3801
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3522
3802
|
this.logger.info({ resumed }, `Respawned ${cliLabel} window after crash`);
|
|
3523
3803
|
this.emit("crash_respawn", this.name);
|
|
3524
3804
|
// Emit the per-instance audit event while the storm is still active
|
|
@@ -4215,6 +4495,14 @@ export class Daemon extends EventEmitter {
|
|
|
4215
4495
|
// (Completion paths clear via the guard's onComplete; a cancel with no
|
|
4216
4496
|
// later idle edge would otherwise leave the marker behind.)
|
|
4217
4497
|
clearInFlightTurnMarker(this.instanceDir);
|
|
4498
|
+
// #3030: what the person cancelled is not kept for a redelivery (nor reported as not taken).
|
|
4499
|
+
try {
|
|
4500
|
+
for (const entry of this.handoffStore().list())
|
|
4501
|
+
this.handoffStore().remove(entry.id);
|
|
4502
|
+
}
|
|
4503
|
+
catch (err) {
|
|
4504
|
+
this.logger.warn({ err }, "Could not clear queued hand-off records on cancel");
|
|
4505
|
+
}
|
|
4218
4506
|
this.deliveryEpoch = fleetEpoch === undefined
|
|
4219
4507
|
? this.deliveryEpoch + 1
|
|
4220
4508
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
@@ -5511,6 +5799,10 @@ export class Daemon extends EventEmitter {
|
|
|
5511
5799
|
if (snapshot.state === "working" && this.recoveryBootSettled
|
|
5512
5800
|
&& this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
|
|
5513
5801
|
this.recoveryNativeWork = true;
|
|
5802
|
+
if (snapshot.state !== "idle")
|
|
5803
|
+
this.instanceIdleSinceMono = null;
|
|
5804
|
+
else if (this.instanceState !== "idle" || this.instanceIdleSinceMono === null)
|
|
5805
|
+
this.instanceIdleSinceMono = performance.now();
|
|
5514
5806
|
this.instanceState = snapshot.state;
|
|
5515
5807
|
this.acceptedInstanceSnapshot = { ...snapshot };
|
|
5516
5808
|
// Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
|
|
@@ -5622,7 +5914,7 @@ export class Daemon extends EventEmitter {
|
|
|
5622
5914
|
* message can arrive mid-turn and sit queued, and marking at arrival would let
|
|
5623
5915
|
* the PREVIOUS turn's idle edge consume (and reset) the new turn's state.
|
|
5624
5916
|
*/
|
|
5625
|
-
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch) {
|
|
5917
|
+
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch, queuedHandoff = false) {
|
|
5626
5918
|
// Cancel may have interrupted an in-flight paste before its promise settled.
|
|
5627
5919
|
// Its late success must not re-arm the turn that the user just cancelled.
|
|
5628
5920
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
@@ -5670,7 +5962,9 @@ export class Daemon extends EventEmitter {
|
|
|
5670
5962
|
// during the continuation boots clean instead of chaining resumes.
|
|
5671
5963
|
// #2544: a reply that settled before this arm already answered the person —
|
|
5672
5964
|
// writing a marker would re-inject a continuation for a finished turn.
|
|
5673
|
-
|
|
5965
|
+
// #3030: a message only queued in the busy CLI has not started a turn: the interrupted turn is still the running one,
|
|
5966
|
+
// and a queued message lost to a restart is the hand-off record's to redeliver, not a continuation's.
|
|
5967
|
+
if (!meta.resumedContinuationOf && !armed.completionDelivered && !queuedHandoff) {
|
|
5674
5968
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5675
5969
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5676
5970
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -7075,10 +7369,12 @@ export class Daemon extends EventEmitter {
|
|
|
7075
7369
|
// Command name only — never the arguments. See shellCommandLabel.
|
|
7076
7370
|
if (name === "Bash")
|
|
7077
7371
|
return `$ ${shellCommandLabel(String(inp.command ?? ""))}`;
|
|
7372
|
+
// The tool name only — never the search pattern: it can be a literal secret someone is looking for, and this label
|
|
7373
|
+
// reaches the channel's progress line.
|
|
7078
7374
|
if (name === "Glob")
|
|
7079
|
-
return
|
|
7375
|
+
return "Glob";
|
|
7080
7376
|
if (name === "Grep")
|
|
7081
|
-
return
|
|
7377
|
+
return "Grep";
|
|
7082
7378
|
if (name === "Agent")
|
|
7083
7379
|
return "Agent (subagent)";
|
|
7084
7380
|
if (name.startsWith("mcp__agend__"))
|
|
@@ -7449,6 +7745,13 @@ export class Daemon extends EventEmitter {
|
|
|
7449
7745
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7450
7746
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
7451
7747
|
this.deferDurableDelivery(meta, "delivery epoch changed while queued");
|
|
7748
|
+
// #3030: a cancel is the person's — a queued redelivery it dropped is not redelivered later.
|
|
7749
|
+
if (meta.handoff_redelivery && !this.deliveryWritesStopping) {
|
|
7750
|
+
try {
|
|
7751
|
+
this.handoffStore().remove(meta.handoff_redelivery);
|
|
7752
|
+
}
|
|
7753
|
+
catch { /* the next CLI judges it */ }
|
|
7754
|
+
}
|
|
7452
7755
|
return;
|
|
7453
7756
|
}
|
|
7454
7757
|
if (this.config.pre_task_command) {
|
|
@@ -7459,13 +7762,26 @@ export class Daemon extends EventEmitter {
|
|
|
7459
7762
|
// #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
|
|
7460
7763
|
// (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
|
|
7461
7764
|
// byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
|
|
7462
|
-
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id
|
|
7765
|
+
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id && !meta.handoff_redelivery
|
|
7766
|
+
? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7463
7767
|
const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
|
|
7464
7768
|
const status = channelStatus(meta);
|
|
7465
7769
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
7466
7770
|
// the previous turn's tool list.
|
|
7467
7771
|
this.resetToolProgress();
|
|
7468
7772
|
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7773
|
+
// #3030: a hand-off into the CLI's own queue stays on record until a turn takes it (redeliveries included).
|
|
7774
|
+
if (!content.startsWith("/raw ") || meta.from_instance) {
|
|
7775
|
+
const { handoff_redelivery: redeliveryOf, handoff_redeliveries: redeliveries, handoff_holder: _holder, ...kept } = meta;
|
|
7776
|
+
verdict.trackHandoff = { content, meta: kept,
|
|
7777
|
+
...(redeliveryOf ? { id: redeliveryOf, redeliveries: Number(redeliveries) || 0 } : {}) };
|
|
7778
|
+
}
|
|
7779
|
+
// #3030: a redelivery is written only by the CLI it was issued for, and only while its record still says so —
|
|
7780
|
+
// one queued for a CLI that was replaced meanwhile is that next CLI's reconciliation's to judge again.
|
|
7781
|
+
if (meta.handoff_redelivery && !this.redeliveryStillOurs(meta)) {
|
|
7782
|
+
this.logger.info({ handoff: meta.handoff_redelivery }, "Queued hand-off redelivery dropped: its CLI was replaced before the write");
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7469
7785
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
7470
7786
|
if (durableAttempt === false) {
|
|
7471
7787
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -7488,10 +7804,11 @@ export class Daemon extends EventEmitter {
|
|
|
7488
7804
|
if (whatsNew && (sent || verdict.paneWriteStarted))
|
|
7489
7805
|
this.recordWhatsNewSent(whatsNew);
|
|
7490
7806
|
}
|
|
7807
|
+
this.afterTrackedWrite(meta, verdict, !!sent, deliveryEpoch);
|
|
7491
7808
|
if (sent) {
|
|
7492
7809
|
delivered = true;
|
|
7493
7810
|
this.finishDurableSubmission(durable, verdict);
|
|
7494
|
-
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7811
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch, !!verdict.handoff);
|
|
7495
7812
|
}
|
|
7496
7813
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
7497
7814
|
this.finishDurableDelivery(durable, "uncertain", `${verdict.phase ?? "post-submit"}:${verdict.proof ?? "unproven"}`, verdict);
|
|
@@ -9830,6 +10147,17 @@ export class Daemon extends EventEmitter {
|
|
|
9830
10147
|
}
|
|
9831
10148
|
}
|
|
9832
10149
|
}
|
|
10150
|
+
// #3030: a hand-off into a busy CLI's own queue is on record before its paste (write-ahead): that queue lives only
|
|
10151
|
+
// in the CLI process, and a stop before a turn takes it would lose it without a trace.
|
|
10152
|
+
if (verdict.trackHandoff && !rawPaste) {
|
|
10153
|
+
await this.recordQueuedHandoff(formatted, handingOffToNativeQueue, verdict, durableAttempt, humanVerificationId);
|
|
10154
|
+
if (stillCurrent && !stillCurrent()) {
|
|
10155
|
+
if (!verdict.trackHandoff.id)
|
|
10156
|
+
this.dropQueuedHandoff(verdict);
|
|
10157
|
+
verdict.fenced = !verdict.durableBeginCommitted;
|
|
10158
|
+
return false;
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
9833
10161
|
if (lookBeforePaste) {
|
|
9834
10162
|
await lookBeforePaste();
|
|
9835
10163
|
// The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
|
|
@@ -10066,8 +10394,9 @@ export class Daemon extends EventEmitter {
|
|
|
10066
10394
|
}
|
|
10067
10395
|
const proof = await this.confirmSubmitted(signature, pasteBaseline);
|
|
10068
10396
|
if (proof === "submitted") {
|
|
10069
|
-
|
|
10070
|
-
|
|
10397
|
+
// ✅ native queue accepted — unless it is on record (#3030): then ✅ waits for the turn that takes it.
|
|
10398
|
+
if (status && !verdict.handoff)
|
|
10399
|
+
this.emit("message_confirmed", status);
|
|
10071
10400
|
return true;
|
|
10072
10401
|
}
|
|
10073
10402
|
if (proof === "unverifiable") {
|
|
@@ -10099,8 +10428,8 @@ export class Daemon extends EventEmitter {
|
|
|
10099
10428
|
this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
|
|
10100
10429
|
verdict.phase = "best-effort-submission";
|
|
10101
10430
|
verdict.proof = "unverified";
|
|
10102
|
-
if (status)
|
|
10103
|
-
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10431
|
+
if (status && !verdict.handoff)
|
|
10432
|
+
this.emit("message_confirmed", status); // ✅ (best-effort; on record: when taken, #3030)
|
|
10104
10433
|
return true;
|
|
10105
10434
|
}
|
|
10106
10435
|
// raw_paste has no unique marker and is not allowed to re-paste or
|
|
@@ -10115,6 +10444,10 @@ export class Daemon extends EventEmitter {
|
|
|
10115
10444
|
// confirmation ladder. "stranded" is the case that used to be a silent
|
|
10116
10445
|
// ✅ — the text was visible, so the old check called it delivered.
|
|
10117
10446
|
this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
|
|
10447
|
+
// #3030: what follows is an idle submission (or a failure the caller reports): no longer a queued hand-off.
|
|
10448
|
+
if (verdict.handoff && !verdict.trackHandoff?.id)
|
|
10449
|
+
this.dropQueuedHandoff(verdict);
|
|
10450
|
+
verdict.handoff = undefined;
|
|
10118
10451
|
if (windowId && this.controlClient && !(await this.controlClient.waitUntilIdle(windowId))) {
|
|
10119
10452
|
verdict.phase = "native-queue-idle-wait";
|
|
10120
10453
|
verdict.proof = "idle-wait-failed";
|
|
@@ -12513,7 +12846,12 @@ export class Daemon extends EventEmitter {
|
|
|
12513
12846
|
|| this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
|
|
12514
12847
|
throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
|
|
12515
12848
|
}
|
|
12516
|
-
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs)
|
|
12849
|
+
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs).catch(err => {
|
|
12850
|
+
// Includes an in-place binary replacement during asynchronous launch preparation.
|
|
12851
|
+
if (err instanceof KiroCompatibilityError)
|
|
12852
|
+
this.emit("kiro_compatibility_hold", err.message);
|
|
12853
|
+
throw err;
|
|
12854
|
+
});
|
|
12517
12855
|
};
|
|
12518
12856
|
const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
|
|
12519
12857
|
instanceName: this.name,
|
|
@@ -12736,6 +13074,11 @@ export class Daemon extends EventEmitter {
|
|
|
12736
13074
|
this.startupAdmission?.();
|
|
12737
13075
|
const launchFence = this.launchFenceEpoch;
|
|
12738
13076
|
const backendConfig = this.buildBackendConfig();
|
|
13077
|
+
// A compatibility refusal is not a failed resume: no configuration, pin, history or pane may change.
|
|
13078
|
+
if (this.backend?.assertLaunchSupported) {
|
|
13079
|
+
this.backend.assertLaunchSupported(backendConfig);
|
|
13080
|
+
this.emit("kiro_compatibility_hold", null);
|
|
13081
|
+
}
|
|
12739
13082
|
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
12740
13083
|
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
12741
13084
|
// Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
|
|
@@ -12772,6 +13115,7 @@ export class Daemon extends EventEmitter {
|
|
|
12772
13115
|
const agentTokenPath = join(this.instanceDir, "agent.token");
|
|
12773
13116
|
const agentToken = randomBytes(32).toString("hex");
|
|
12774
13117
|
const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
|
|
13118
|
+
this.agentCredentialIssuer?.(agentToken);
|
|
12775
13119
|
if (!tokenWrite.ok) {
|
|
12776
13120
|
// Do not fail the spawn over it — an instance that cannot start is worse
|
|
12777
13121
|
// than one whose token is readable. But say so, loudly: the whole point of
|