@songsid/agend 2.3.1 → 2.3.2-beta.1
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/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +16 -10
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +7 -1
- package/dist/authority/session-authorizations.js +6 -4
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +19 -6
- package/dist/authority/web-authority.js +22 -8
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +33 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/config-validator.js +40 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +34 -3
- package/dist/daemon.js +227 -46
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +12 -1
- package/dist/delivery-outbox.js +67 -15
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +9 -0
- package/dist/doctor.js.map +1 -1
- package/dist/event-log.d.ts +5 -0
- package/dist/event-log.js +13 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-manager.d.ts +140 -0
- package/dist/fleet-manager.js +414 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +1 -0
- package/dist/fleet-process-handlers.js +9 -1
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +106 -55
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +107 -56
- package/dist/guest-seen-people.d.ts +38 -0
- package/dist/guest-seen-people.js +78 -0
- package/dist/guest-seen-people.js.map +1 -0
- package/dist/instance-config-impact.js +4 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -0
- package/dist/instance-lifecycle.js +16 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +38 -10
- package/dist/locale.js.map +1 -1
- package/dist/mac-auto-restore.d.ts +117 -0
- package/dist/mac-auto-restore.js +294 -0
- package/dist/mac-auto-restore.js.map +1 -0
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +9 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/settings-api.d.ts +26 -0
- package/dist/settings-api.js +107 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +37 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +5 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/start-attempt.d.ts +9 -5
- package/dist/start-attempt.js +27 -8
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +4 -0
- package/dist/start-failure-chat.js +23 -6
- package/dist/start-failure-chat.js.map +1 -1
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +8 -0
- package/dist/start-failure-notice.js.map +1 -1
- package/dist/start-failure.d.ts +25 -0
- package/dist/start-failure.js +75 -2
- package/dist/start-failure.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +25 -0
- package/dist/turn-reply-guard.js +111 -12
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/ui/backend-signin.js +5 -2
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-effects.js +4 -4
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +2 -2
- package/dist/ui/first-run.js +2 -2
- package/dist/ui/fleet-canvas-view.js +114 -0
- package/dist/ui/fleet-canvas.js +15 -77
- package/dist/ui/fleet-office-editor.js +80 -5
- package/dist/ui/fleet-office-layout.js +197 -0
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-pack.d.ts +39 -0
- package/dist/ui/fleet-office-pack.js +240 -29
- package/dist/ui/fleet-office-skin.js +130 -21
- package/dist/ui/fleet-office-store.js +75 -4
- package/dist/ui/fleet-office-view.js +154 -0
- package/dist/ui/fleet-office-zones.js +95 -33
- package/dist/ui/fleet-office.js +530 -94
- package/dist/ui/fleet-usage.js +7 -6
- package/dist/ui/panel-chat.js +1 -1
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-settings.js +578 -163
- package/dist/ui/search-dialog.js +26 -7
- package/dist/ui/settings-appearance.js +82 -29
- package/dist/ui/settings-apply.js +42 -7
- package/dist/ui/settings-dialogs.js +68 -22
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +244 -78
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +2 -0
- package/dist/ui/settings-registry.js +142 -0
- package/dist/ui/settings-strings.js +102 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +14 -0
- package/dist/ui/shared/app-nav.js +10 -5
- package/dist/ui/shared/app-prefs.js +80 -2
- package/dist/ui/shared/app-route.js +39 -5
- package/dist/ui/shared/app-shell.js +59 -4
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +242 -17
- package/dist/ui/shared/app.js +3 -0
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/identity-color.js +4 -4
- package/dist/ui/shared/signin.js +2 -2
- package/dist/ui/shared/ui-dialog.js +6 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/web-authorize.js +13 -12
- package/dist/web-api.d.ts +8 -2
- package/dist/web-api.js +160 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +1 -1
- package/dist/web-experimental.js +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-mod-agent.js +2 -2
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-office-skins.d.ts +108 -0
- package/dist/web-office-skins.js +298 -0
- package/dist/web-office-skins.js.map +1 -0
- package/dist/web-office-store.d.ts +8 -2
- package/dist/web-office-store.js +37 -7
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +6 -0
- package/dist/web-settings-patch.js +61 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -2
- package/dist/web-shell-routes.js +17 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/working-recovery.d.ts +104 -0
- package/dist/working-recovery.js +263 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +3 -1
- package/dist/working-state-journal.js +15 -2
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -46,7 +46,7 @@ import { buildFleetInstructions } from "./instructions.js";
|
|
|
46
46
|
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
47
47
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
48
48
|
import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
49
|
-
import { TurnReplyGuard } from "./turn-reply-guard.js";
|
|
49
|
+
import { TurnReplyGuard, formatReplyDropDetection, turnExpectsPersonReply } from "./turn-reply-guard.js";
|
|
50
50
|
import { t } from "./locale.js";
|
|
51
51
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
52
52
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
@@ -1245,6 +1245,8 @@ export class Daemon extends EventEmitter {
|
|
|
1245
1245
|
* skipResume — the #1209 gate reads this, never the resettable flag.
|
|
1246
1246
|
*/
|
|
1247
1247
|
bootSkippedResume = false;
|
|
1248
|
+
recoveryCrashLoopFresh = false;
|
|
1249
|
+
recoveryFreshChoice = false;
|
|
1248
1250
|
startupAborted = false;
|
|
1249
1251
|
/** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
|
|
1250
1252
|
dialogParkedSince = 0;
|
|
@@ -2203,6 +2205,7 @@ export class Daemon extends EventEmitter {
|
|
|
2203
2205
|
}
|
|
2204
2206
|
async startAdmitted() {
|
|
2205
2207
|
this.assertSessionOomAdmission();
|
|
2208
|
+
this.recoveryBootWorkPending = true;
|
|
2206
2209
|
ensureInstanceDir(this.instanceDir);
|
|
2207
2210
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
2208
2211
|
this.logger.info(`Starting ${this.name}`);
|
|
@@ -2211,6 +2214,8 @@ export class Daemon extends EventEmitter {
|
|
|
2211
2214
|
// even when the JSON is corrupt so it cannot remain forever.
|
|
2212
2215
|
const crashStatePath = join(this.instanceDir, "crash-state.json");
|
|
2213
2216
|
this.bootSkippedResume = false;
|
|
2217
|
+
this.recoveryCrashLoopFresh = false;
|
|
2218
|
+
this.recoveryFreshChoice = false;
|
|
2214
2219
|
try {
|
|
2215
2220
|
if (existsSync(crashStatePath)) {
|
|
2216
2221
|
const content = readFileSync(crashStatePath, "utf-8");
|
|
@@ -2219,6 +2224,10 @@ export class Daemon extends EventEmitter {
|
|
|
2219
2224
|
if (state?.resumeDisabled === true) {
|
|
2220
2225
|
this.skipResume = true;
|
|
2221
2226
|
this.bootSkippedResume = true;
|
|
2227
|
+
this.recoveryCrashLoopFresh = (state.reason === undefined || state.reason === "crash_loop")
|
|
2228
|
+
&& Number.isInteger(state.crashesInWindow) && state.crashesInWindow >= 3
|
|
2229
|
+
&& Number.isFinite(state.lastCrashAt) && state.lastCrashAt > 0;
|
|
2230
|
+
this.recoveryFreshChoice = !this.recoveryCrashLoopFresh;
|
|
2222
2231
|
this.logger.warn("Previous crash loop detected — starting without resume");
|
|
2223
2232
|
}
|
|
2224
2233
|
}
|
|
@@ -2367,12 +2376,19 @@ export class Daemon extends EventEmitter {
|
|
|
2367
2376
|
// #1209: conditional continuation for the turn interrupted by the last
|
|
2368
2377
|
// restart, if the gates allow. Detached like the warmup notice; the gate
|
|
2369
2378
|
// consumes its one-shot marker first and never throws into boot.
|
|
2370
|
-
|
|
2379
|
+
const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2371
2380
|
// Warmup: wait for CLI idle, then trigger steering reload — but only when
|
|
2372
2381
|
// the instructions actually changed since the agent last saw them.
|
|
2373
2382
|
// Skipping the no-op reload saves 10-30s of agent time on every restart
|
|
2374
2383
|
// where instructions are unchanged.
|
|
2375
|
-
|
|
2384
|
+
const warmup = this.runWarmupInstructionNotice();
|
|
2385
|
+
const recoveryOwner = JSON.stringify(this.interactionOwner());
|
|
2386
|
+
void Promise.allSettled([legacyResume, warmup]).then(() => {
|
|
2387
|
+
if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
|
|
2388
|
+
this.recoveryBootWorkPending = false;
|
|
2389
|
+
this.recoveryBootSettled = true;
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2376
2392
|
if (!this.config.lightweight) {
|
|
2377
2393
|
// 3. Pipe-pane for prompt detection. Rotate first so a ballooned log from a
|
|
2378
2394
|
// previous stuck splash (hundreds of MB of ANSI frames) is truncated before
|
|
@@ -3974,6 +3990,7 @@ export class Daemon extends EventEmitter {
|
|
|
3974
3990
|
clearPendingDeliveries(fleetEpoch) {
|
|
3975
3991
|
// FleetManager calls this synchronously before sendEscape. Cancel only the
|
|
3976
3992
|
// current human turn; leave already-forwarded tool requests to settle.
|
|
3993
|
+
this.recoveryCancelled = true;
|
|
3977
3994
|
this.turnReplyGuard.cancelByUser();
|
|
3978
3995
|
// #1241: a cancelled turn never confirms — its next edge completes it as
|
|
3979
3996
|
// intentional. Drop a pending confirmation so it cannot promote afterwards.
|
|
@@ -3985,6 +4002,7 @@ export class Daemon extends EventEmitter {
|
|
|
3985
4002
|
this.deliveryEpoch = fleetEpoch === undefined
|
|
3986
4003
|
? this.deliveryEpoch + 1
|
|
3987
4004
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
4005
|
+
this.cancelCapacityNudge("the user cancelled");
|
|
3988
4006
|
this.publishWorkingState("cancelled");
|
|
3989
4007
|
this.logger.info({ deliveryEpoch: this.deliveryEpoch }, "Pending delivery queue cleared by user cancel");
|
|
3990
4008
|
}
|
|
@@ -4703,6 +4721,88 @@ export class Daemon extends EventEmitter {
|
|
|
4703
4721
|
getInteractionSnapshot() {
|
|
4704
4722
|
return this.interactionObservation.snapshot(this.interactionOwner());
|
|
4705
4723
|
}
|
|
4724
|
+
ownedWorkingSession = null;
|
|
4725
|
+
recoveryBootSettled = false;
|
|
4726
|
+
recoveryBootWorkPending = false;
|
|
4727
|
+
recoveryLegacyCovered = false;
|
|
4728
|
+
recoveryNativeWork = false;
|
|
4729
|
+
recoveryContinuationId = null;
|
|
4730
|
+
recoveryCancelled = false;
|
|
4731
|
+
workingRecoveryInputOwner = null;
|
|
4732
|
+
noteLegacyRestartReminder() { this.recoveryLegacyCovered = true; }
|
|
4733
|
+
getWorkingRecoveryState() {
|
|
4734
|
+
const observation = this.getWorkingStateObservation();
|
|
4735
|
+
const owner = JSON.stringify(observation.owner), ownsInput = this.workingRecoveryInputOwner === owner;
|
|
4736
|
+
return { owner, configuration: observation.configuration,
|
|
4737
|
+
sessionId: this.ownedWorkingSession, idle: (ownsInput || this.instanceState === "idle") && this.pasteQueueDepth === 0,
|
|
4738
|
+
paused: this.isPaused, stopped: this.isSessionOomStopped(), legacyPending: !this.recoveryBootSettled,
|
|
4739
|
+
legacyCovered: this.recoveryLegacyCovered, nativeWork: !ownsInput && this.recoveryNativeWork,
|
|
4740
|
+
freshChoice: this.recoveryFreshChoice || this.isNewSession && !this.bootSkippedResume, crashLoopFresh: this.recoveryCrashLoopFresh,
|
|
4741
|
+
cancelled: this.recoveryCancelled, background: this.workingBackgroundObservation };
|
|
4742
|
+
}
|
|
4743
|
+
/** Local system work: no user turn, channel reply target, wake or replay of an original request. */
|
|
4744
|
+
async deliverWorkingRecovery(content, delivery, admitted) {
|
|
4745
|
+
const tmux = this.tmux, owner = JSON.stringify(this.interactionOwner()), epoch = this.deliveryEpoch;
|
|
4746
|
+
const current = () => this.tmux === tmux && owner === JSON.stringify(this.interactionOwner())
|
|
4747
|
+
&& this.deliveryEpoch === epoch && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen
|
|
4748
|
+
&& !this.spawning && !this.startupAborted && !this.isPaused && !this.healthCheckPaused
|
|
4749
|
+
&& this.processStatus === "running" && this.pasteQueueDepth === 0
|
|
4750
|
+
&& (this.workingRecoveryInputOwner === owner || this.instanceState === "idle" && !this.recoveryNativeWork)
|
|
4751
|
+
&& this.recoveryBootSettled && !this.recoveryCancelled && !this.isSessionOomStopped() && admitted();
|
|
4752
|
+
let begun = false;
|
|
4753
|
+
try {
|
|
4754
|
+
await this.paneWriteLock.run(async () => {
|
|
4755
|
+
if (!tmux || !current())
|
|
4756
|
+
return;
|
|
4757
|
+
const pane = await tmux.capturePane();
|
|
4758
|
+
const input = this.backend?.readInputRow?.(pane);
|
|
4759
|
+
if (!current() || !input || input.text !== "" || input.collapsedPastes !== 0 || input.queued
|
|
4760
|
+
|| !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
|
|
4761
|
+
|| this.backend?.backgroundWorkRunning?.(pane) === true)
|
|
4762
|
+
return;
|
|
4763
|
+
if (await tmux.getPaneInputMode?.() !== "raw" || !current())
|
|
4764
|
+
return;
|
|
4765
|
+
const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
|
|
4766
|
+
if (!current())
|
|
4767
|
+
return;
|
|
4768
|
+
const signature = this.submissionSignature(content), baseline = this.paneEvidence(pane, signature);
|
|
4769
|
+
const attempt = { ...delivery, contentSha256: deliveryContentDigest(content) };
|
|
4770
|
+
if (!this.beginDurableDelivery(attempt, evidence))
|
|
4771
|
+
return;
|
|
4772
|
+
begun = true;
|
|
4773
|
+
// Own PTY input immediately produces a working observation. It must not
|
|
4774
|
+
// cancel this same fenced write; external queue/cancel/owner guards stay.
|
|
4775
|
+
this.workingRecoveryInputOwner = owner;
|
|
4776
|
+
// Conservatively commit possible input BEFORE paste, not merely before Enter.
|
|
4777
|
+
// A process death at any later boundary must never replay this continuation.
|
|
4778
|
+
if (!this.deliveryOutbox?.markEnterStarted(delivery.deliveryId, this.bootId, delivery.attemptNo))
|
|
4779
|
+
return;
|
|
4780
|
+
if (!current() || !await tmux.pasteBuffer(content, { guard: current }) || !current())
|
|
4781
|
+
return;
|
|
4782
|
+
await new Promise(resolve => setTimeout(resolve, NORMAL_ENTER_SETTLE_MS));
|
|
4783
|
+
if (!current() || !await this.sendDeliveryEnterAfterAvailability("working-recovery", current, attempt))
|
|
4784
|
+
return;
|
|
4785
|
+
const proof = await this.confirmSubmitted(signature, baseline);
|
|
4786
|
+
const committed = this.deliveryOutbox.complete(delivery.deliveryId, this.bootId, delivery.attemptNo, proof === "submitted" ? "delivered" : "uncertain", "local continuation Enter attempted once; no replay");
|
|
4787
|
+
if (committed && proof === "submitted" && current()) {
|
|
4788
|
+
this.recoveryContinuationId = delivery.deliveryId;
|
|
4789
|
+
this.publishWorkingState("execution");
|
|
4790
|
+
}
|
|
4791
|
+
});
|
|
4792
|
+
}
|
|
4793
|
+
catch { /* The durable fence below owns ambiguity; never retry a begun write. */ }
|
|
4794
|
+
finally {
|
|
4795
|
+
if (this.workingRecoveryInputOwner === owner) {
|
|
4796
|
+
this.workingRecoveryInputOwner = null;
|
|
4797
|
+
if (this.tmux === tmux && owner === JSON.stringify(this.interactionOwner()))
|
|
4798
|
+
this.recoveryNativeWork = true;
|
|
4799
|
+
}
|
|
4800
|
+
if (begun)
|
|
4801
|
+
this.deliveryOutbox?.complete(delivery.deliveryId, this.bootId, delivery.attemptNo, "uncertain", "local continuation may have reached input; not replayed");
|
|
4802
|
+
else
|
|
4803
|
+
this.deliveryOutbox?.retryBeforeBegin(delivery.deliveryId, this.bootId, delivery.attemptNo, "working recovery prerequisite changed before input", 5_000);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4706
4806
|
workingTurnDelivery = null;
|
|
4707
4807
|
workingBackgroundObservation = null;
|
|
4708
4808
|
/** Pure metadata read: never discovers sessions, captures a pane or reads backend files. */
|
|
@@ -4714,7 +4814,8 @@ export class Daemon extends EventEmitter {
|
|
|
4714
4814
|
owner: this.interactionOwner(), backend,
|
|
4715
4815
|
configuration: createHash("sha256").update(JSON.stringify([backend, this.config.working_directory, profile])).digest("hex"),
|
|
4716
4816
|
executionState: this.processStatus === "running" ? this.isPaused ? "paused" : this.instanceState : null,
|
|
4717
|
-
processStatus: this.getProcessStatus(), sessionId:
|
|
4817
|
+
processStatus: this.getProcessStatus(), sessionId: this.ownedWorkingSession, backendTurnId: null,
|
|
4818
|
+
...(this.recoveryContinuationId ? { recoveryDeliveryId: this.recoveryContinuationId } : {}),
|
|
4718
4819
|
backgroundWork: this.workingBackgroundObservation,
|
|
4719
4820
|
turn: turn ? { generation: turn.generation,
|
|
4720
4821
|
deliveryId: this.workingTurnDelivery?.generation === turn.generation ? this.workingTurnDelivery.deliveryId : null,
|
|
@@ -5174,6 +5275,9 @@ export class Daemon extends EventEmitter {
|
|
|
5174
5275
|
if (unblockedTurn && unblockedTurn.phase === "awaiting")
|
|
5175
5276
|
this.maybeProxyReplyOnTurnEnd(pane);
|
|
5176
5277
|
}
|
|
5278
|
+
if (snapshot.state === "working" && this.recoveryBootSettled
|
|
5279
|
+
&& this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
|
|
5280
|
+
this.recoveryNativeWork = true;
|
|
5177
5281
|
this.instanceState = snapshot.state;
|
|
5178
5282
|
this.acceptedInstanceSnapshot = { ...snapshot };
|
|
5179
5283
|
// Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
|
|
@@ -5290,14 +5394,21 @@ export class Daemon extends EventEmitter {
|
|
|
5290
5394
|
// Its late success must not re-arm the turn that the user just cancelled.
|
|
5291
5395
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
5292
5396
|
return;
|
|
5293
|
-
// Channel turns
|
|
5294
|
-
//
|
|
5295
|
-
//
|
|
5296
|
-
//
|
|
5297
|
-
//
|
|
5298
|
-
// web-only sink), but it is a person waiting for an answer all the
|
|
5299
|
-
|
|
5397
|
+
// Channel turns started by a person. Cross-instance, schedules (silent or
|
|
5398
|
+
// not), and AgEnD system notices must not expect a reply to a person (#2544):
|
|
5399
|
+
// a proxy or recovery notice would land in the last USER topic, or flag a
|
|
5400
|
+
// heartbeat that was told not to report. A web message on a fleet with no
|
|
5401
|
+
// chat platform has no chat_id (its reply goes to the web chat through the
|
|
5402
|
+
// fleet's web-only sink), but it is a person waiting for an answer all the
|
|
5403
|
+
// same: the guard arms for it (alpha.2).
|
|
5404
|
+
if (!turnExpectsPersonReply(meta)) {
|
|
5405
|
+
this.logger.debug({
|
|
5406
|
+
source: meta.source || null,
|
|
5407
|
+
user: meta.user || null,
|
|
5408
|
+
from_instance: meta.from_instance || null,
|
|
5409
|
+
}, "Reply guard skipped — turn was not started by a person");
|
|
5300
5410
|
return;
|
|
5411
|
+
}
|
|
5301
5412
|
// The last non-empty line of what we pasted: everything on screen after it
|
|
5302
5413
|
// is the agent's own output.
|
|
5303
5414
|
const inboundMarker = deliveredText.split(/\r?\n/).map(l => l.trim()).filter(Boolean).pop();
|
|
@@ -5314,7 +5425,8 @@ export class Daemon extends EventEmitter {
|
|
|
5314
5425
|
deliveredText,
|
|
5315
5426
|
deliveredAt,
|
|
5316
5427
|
}, this.pendingWork.lastInboundTimestamp());
|
|
5317
|
-
|
|
5428
|
+
const armed = this.turnReplyGuard.snapshot();
|
|
5429
|
+
this.workingTurnDelivery = { generation: armed.generation, deliveryId: meta.delivery_id || null };
|
|
5318
5430
|
this.publishWorkingState("turn_started");
|
|
5319
5431
|
// #1209: persist the in-flight turn with a seam checkpoint. A marker
|
|
5320
5432
|
// that survives a restart is an interrupted turn by construction —
|
|
@@ -5323,7 +5435,9 @@ export class Daemon extends EventEmitter {
|
|
|
5323
5435
|
// the interruption. Reads are windowed/bounded (see turn-resume.ts).
|
|
5324
5436
|
// A resumed continuation never re-arms (P3a): episode-once means a crash
|
|
5325
5437
|
// during the continuation boots clean instead of chaining resumes.
|
|
5326
|
-
|
|
5438
|
+
// #2544: a reply that settled before this arm already answered the person —
|
|
5439
|
+
// writing a marker would re-inject a continuation for a finished turn.
|
|
5440
|
+
if (!meta.resumedContinuationOf && !armed.completionDelivered) {
|
|
5327
5441
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5328
5442
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5329
5443
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -5371,6 +5485,7 @@ export class Daemon extends EventEmitter {
|
|
|
5371
5485
|
const marker = consumeInFlightTurnMarker(this.instanceDir);
|
|
5372
5486
|
if (!marker)
|
|
5373
5487
|
return "none";
|
|
5488
|
+
this.recoveryLegacyCovered = true;
|
|
5374
5489
|
// P1 fence: capture before any await. A cancel (deliveryEpoch),
|
|
5375
5490
|
// respawn (spawnGeneration) or stop/pause (launchFenceEpoch) during the
|
|
5376
5491
|
// grace hold must stale this gate — the push below rechecks, and the
|
|
@@ -5737,7 +5852,7 @@ export class Daemon extends EventEmitter {
|
|
|
5737
5852
|
reason,
|
|
5738
5853
|
recoveryStarted: true,
|
|
5739
5854
|
});
|
|
5740
|
-
|
|
5855
|
+
this.deliverReplyDropNotice("inst.reply_drop_retrying", "replydrop", "Reply-drop status", turn, reason, false);
|
|
5741
5856
|
this.queueReplyRecoveryPrompt(turn);
|
|
5742
5857
|
}
|
|
5743
5858
|
reportReplyDropWithoutRetry(turn, reason) {
|
|
@@ -5748,7 +5863,7 @@ export class Daemon extends EventEmitter {
|
|
|
5748
5863
|
reason,
|
|
5749
5864
|
recoveryStarted: false,
|
|
5750
5865
|
});
|
|
5751
|
-
|
|
5866
|
+
this.deliverReplyDropNotice("inst.reply_drop_unknown", "replydrop", "Unconfirmed reply status", turn, reason, true);
|
|
5752
5867
|
}
|
|
5753
5868
|
queueReplyRecoveryPrompt(turn) {
|
|
5754
5869
|
const deliveryEpoch = this.deliveryEpoch;
|
|
@@ -5841,12 +5956,20 @@ export class Daemon extends EventEmitter {
|
|
|
5841
5956
|
generation: turn.generation,
|
|
5842
5957
|
reason,
|
|
5843
5958
|
});
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5959
|
+
this.deliverReplyDropNotice("inst.reply_drop_unrecovered", "replydropwarn", "Reply-drop recovery warning", turn, reason, true);
|
|
5960
|
+
}
|
|
5961
|
+
/** Channel notice for a reply-drop. `rateLimit` shares the per-instance cooldown (#2544). */
|
|
5962
|
+
deliverReplyDropNotice(key, kind, title, turn, reason, rateLimit) {
|
|
5963
|
+
if (rateLimit) {
|
|
5964
|
+
const now = Date.now();
|
|
5965
|
+
if (this.lastReplyDropWarningAt !== 0
|
|
5966
|
+
&& now - this.lastReplyDropWarningAt < REPLY_DROP_WARNING_COOLDOWN_MS) {
|
|
5967
|
+
this.logger.debug({ reason, kind }, "Reply-drop channel notice rate-limited");
|
|
5968
|
+
return;
|
|
5969
|
+
}
|
|
5970
|
+
this.lastReplyDropWarningAt = now;
|
|
5971
|
+
}
|
|
5972
|
+
void this.deliverDaemonReply(t(key, formatReplyDropDetection(turn, reason)), kind, title, turn.target, true);
|
|
5850
5973
|
}
|
|
5851
5974
|
/** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
|
|
5852
5975
|
async sendProxyReply(pane, target) {
|
|
@@ -6444,6 +6567,7 @@ export class Daemon extends EventEmitter {
|
|
|
6444
6567
|
}
|
|
6445
6568
|
/** Stop every runtime poller/watcher while preserving IPC and daemon state. */
|
|
6446
6569
|
freezeRuntimeMonitors() {
|
|
6570
|
+
this.cancelCapacityNudge("the instance was stopped, paused or respawned");
|
|
6447
6571
|
this.runtimeMonitorsFrozen = true;
|
|
6448
6572
|
this.launchFenceEpoch++;
|
|
6449
6573
|
this.interactionObservation.reset(this.interactionOwner());
|
|
@@ -6556,6 +6680,10 @@ export class Daemon extends EventEmitter {
|
|
|
6556
6680
|
this.config.warm_cap = update.warm_cap;
|
|
6557
6681
|
else if (update.warm_cap === null)
|
|
6558
6682
|
delete this.config.warm_cap;
|
|
6683
|
+
if (typeof update.resume_interrupted_work === "boolean")
|
|
6684
|
+
this.config.resume_interrupted_work = update.resume_interrupted_work;
|
|
6685
|
+
else if (update.resume_interrupted_work === null)
|
|
6686
|
+
delete this.config.resume_interrupted_work;
|
|
6559
6687
|
const nextAutoPause = typeof update.auto_pause_after === "number"
|
|
6560
6688
|
&& Number.isFinite(update.auto_pause_after)
|
|
6561
6689
|
&& update.auto_pause_after >= 0
|
|
@@ -7828,7 +7956,8 @@ export class Daemon extends EventEmitter {
|
|
|
7828
7956
|
/**
|
|
7829
7957
|
* Arm the "keep going" nudge for the error occurrence just reported (`pattern` must be the one the lifecycle was
|
|
7830
7958
|
* handed). The nudge is sent once, `delayMs` from now on the MONOTONIC clock (a wall-clock step changes nothing),
|
|
7831
|
-
* and only if
|
|
7959
|
+
* and only if the failed turn still owns an empty idle composer — see {@link tickCapacityNudge}.
|
|
7960
|
+
* Cosmetic footer redraws are not new work. Returns false when there is nothing to arm.
|
|
7832
7961
|
*/
|
|
7833
7962
|
armCapacityNudge(pattern, delayMs) {
|
|
7834
7963
|
const episode = this.lastErrorEpisode;
|
|
@@ -7847,9 +7976,12 @@ export class Daemon extends EventEmitter {
|
|
|
7847
7976
|
// lookahead on the counting pattern: whole-pane dedup would max at 1 and miss later hits. See #1215.)
|
|
7848
7977
|
if (!codexLiveRowMatches(episode.pane, pattern))
|
|
7849
7978
|
return false;
|
|
7979
|
+
this.cancelCapacityNudge("the instance has work of its own");
|
|
7850
7980
|
this.capacityNudge = {
|
|
7851
7981
|
pane: episode.pane, pattern, spawn: episode.spawn, fence: episode.fence,
|
|
7852
7982
|
epoch: this.deliveryEpoch, dueAt: performance.now() + delayMs,
|
|
7983
|
+
episode,
|
|
7984
|
+
promiseTarget: null,
|
|
7853
7985
|
};
|
|
7854
7986
|
this.logger.info({ delayMs }, "Model-capacity nudge armed");
|
|
7855
7987
|
return true;
|
|
@@ -7864,7 +7996,7 @@ export class Daemon extends EventEmitter {
|
|
|
7864
7996
|
const nudge = this.capacityNudge, episode = this.lastErrorEpisode;
|
|
7865
7997
|
const tmux = this.tmux, attempt = this.launchAttempt, boot = this.bootId;
|
|
7866
7998
|
this.cancelCapacityNudge("retries exhausted");
|
|
7867
|
-
if (!nudge || !episode || !tmux)
|
|
7999
|
+
if (!nudge || !episode || nudge.episode !== episode || !tmux)
|
|
7868
8000
|
return null;
|
|
7869
8001
|
const window = tmux.getWindowId?.();
|
|
7870
8002
|
const current = () => this.tmux === tmux && tmux.getWindowId?.() === window
|
|
@@ -7872,9 +8004,7 @@ export class Daemon extends EventEmitter {
|
|
|
7872
8004
|
&& episode === this.lastErrorEpisode && this.isDeliveryEpochCurrent(nudge.epoch)
|
|
7873
8005
|
&& !this.startupAborted && !this.spawning && !this.isPaused
|
|
7874
8006
|
&& this.getProcessStatus() === "running" && this.instanceState === "idle" && this.pasteQueueDepth === 0;
|
|
7875
|
-
const accepts = (pane) =>
|
|
7876
|
-
&& this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
|
|
7877
|
-
&& codexLiveRowMatches(pane, nudge.pattern) && !this.backend?.backgroundWorkRunning?.(pane);
|
|
8007
|
+
const accepts = (pane) => this.capacityNudgePaneCurrent(nudge, pane);
|
|
7878
8008
|
const revalidate = () => this.paneWriteLock.run(async () => {
|
|
7879
8009
|
if (!current())
|
|
7880
8010
|
return false;
|
|
@@ -7896,10 +8026,35 @@ export class Daemon extends EventEmitter {
|
|
|
7896
8026
|
}
|
|
7897
8027
|
/** Drop an armed nudge (the user stopped / paused the instance, or the lifecycle gave up on it). */
|
|
7898
8028
|
cancelCapacityNudge(reason) {
|
|
7899
|
-
|
|
8029
|
+
const nudge = this.capacityNudge;
|
|
8030
|
+
if (!nudge)
|
|
7900
8031
|
return;
|
|
7901
8032
|
this.capacityNudge = null;
|
|
7902
8033
|
this.logger.info({ reason }, "Model-capacity nudge cancelled");
|
|
8034
|
+
this.reportCapacityNudgeCancelled(nudge, reason);
|
|
8035
|
+
}
|
|
8036
|
+
/** Only a dispatched channel promise needs a later cancellation notice. */
|
|
8037
|
+
markCapacityNudgePromised(target) {
|
|
8038
|
+
if (this.capacityNudge)
|
|
8039
|
+
this.capacityNudge.promiseTarget = target;
|
|
8040
|
+
}
|
|
8041
|
+
reportCapacityNudgeCancelled(nudge, reason) {
|
|
8042
|
+
const target = nudge.promiseTarget;
|
|
8043
|
+
if (!target)
|
|
8044
|
+
return;
|
|
8045
|
+
nudge.promiseTarget = null;
|
|
8046
|
+
this.emit("capacity_nudge_cancelled", { name: this.name, reason, target });
|
|
8047
|
+
}
|
|
8048
|
+
/**
|
|
8049
|
+
* A failed turn must still be the last live transcript item, with no new
|
|
8050
|
+
* error occurrence, draft, queue, busy status or background work. Codex's
|
|
8051
|
+
* own periodic-redraw idle classifier recognises its empty composer/footer;
|
|
8052
|
+
* never infer inactivity from byte equality or a cached idle state alone.
|
|
8053
|
+
*/
|
|
8054
|
+
capacityNudgePaneCurrent(nudge, pane) {
|
|
8055
|
+
return this.paneAuthoritativelyIdle(pane) && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
|
|
8056
|
+
&& !this.backend?.backgroundWorkRunning?.(pane) && codexLiveRowMatches(pane, nudge.pattern)
|
|
8057
|
+
&& Daemon.countOccurrences(nudge.pattern, pane) === Daemon.countOccurrences(nudge.pattern, nudge.pane);
|
|
7903
8058
|
}
|
|
7904
8059
|
/** The first live composer of a spawn fixes what is history (see {@link nudgeBaselines}). */
|
|
7905
8060
|
takeNudgeBaselines(pane, patterns) {
|
|
@@ -7922,8 +8077,8 @@ export class Daemon extends EventEmitter {
|
|
|
7922
8077
|
}
|
|
7923
8078
|
/**
|
|
7924
8079
|
* One monitor poll's look at an armed nudge. It is CANCELLED — never postponed — when anything about the instance
|
|
7925
|
-
* moved: a stop / pause / respawn (spawn generation or monitor epoch), a user cancel (delivery epoch), the pane
|
|
7926
|
-
* longer
|
|
8080
|
+
* moved: a stop / pause / respawn (spawn generation or monitor epoch), a user cancel (delivery epoch), the pane
|
|
8081
|
+
* no longer belonging to the failed turn (new output, a draft, another dialog), the
|
|
7927
8082
|
* agent working, or a message waiting for delivery. Only when it is due and still untouched is it fired — once — and
|
|
7928
8083
|
* the injection re-verifies everything under the pane-write lock.
|
|
7929
8084
|
*/
|
|
@@ -7933,10 +8088,11 @@ export class Daemon extends EventEmitter {
|
|
|
7933
8088
|
return;
|
|
7934
8089
|
const gone = nudge.spawn !== this.spawnGeneration || nudge.fence !== this.launchFenceEpoch ? "the instance was stopped, paused or respawned"
|
|
7935
8090
|
: !this.isDeliveryEpochCurrent(nudge.epoch) ? "the user cancelled"
|
|
7936
|
-
:
|
|
7937
|
-
:
|
|
8091
|
+
: nudge.episode !== this.lastErrorEpisode ? "the instance has work of its own"
|
|
8092
|
+
: this.isPaused || this.healthCheckPaused || this.getProcessStatus() === "stopped" ? "the instance is not running"
|
|
7938
8093
|
: this.instanceState === "working" || this.pasteQueueDepth > 0 ? "the instance has work of its own"
|
|
7939
|
-
:
|
|
8094
|
+
: !this.capacityNudgePaneCurrent(nudge, pane) ? "the failed turn is no longer idle with an empty input"
|
|
8095
|
+
: null;
|
|
7940
8096
|
if (gone) {
|
|
7941
8097
|
this.cancelCapacityNudge(gone);
|
|
7942
8098
|
return;
|
|
@@ -7944,29 +8100,38 @@ export class Daemon extends EventEmitter {
|
|
|
7944
8100
|
if (performance.now() < nudge.dueAt)
|
|
7945
8101
|
return;
|
|
7946
8102
|
this.capacityNudge = null; // fired: once per episode
|
|
7947
|
-
void this.sendCapacityNudge(nudge).catch(err =>
|
|
8103
|
+
void this.sendCapacityNudge(nudge).catch(err => {
|
|
8104
|
+
this.logger.warn({ err: err.message }, "Model-capacity nudge failed");
|
|
8105
|
+
this.reportCapacityNudgeCancelled(nudge, "the retry could not be submitted");
|
|
8106
|
+
});
|
|
7948
8107
|
}
|
|
7949
8108
|
async sendCapacityNudge(nudge) {
|
|
7950
8109
|
// Everything the nudge was armed under, asked again after EVERY await that precedes a write — inside the paste
|
|
7951
8110
|
// primitive too (its capture, its paste, its pause, its Enter, its retry): once any of it is false nothing more is
|
|
7952
8111
|
// written. A message queued behind the lock, or the agent starting a turn, is "work of its own" like at the poll.
|
|
7953
8112
|
const current = () => nudge.spawn === this.spawnGeneration && nudge.fence === this.launchFenceEpoch
|
|
8113
|
+
&& nudge.episode === this.lastErrorEpisode
|
|
7954
8114
|
&& this.isDeliveryEpochCurrent(nudge.epoch) && !this.isPaused && !this.healthCheckPaused
|
|
7955
8115
|
&& !this.startupAborted && this.getProcessStatus() !== "stopped"
|
|
7956
8116
|
&& this.pasteQueueDepth === 0 && this.instanceState !== "working";
|
|
7957
8117
|
// Exclusive with every delivery: nothing can paste a turn between the verdict below and the nudge.
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
8118
|
+
let sent = false;
|
|
8119
|
+
try {
|
|
8120
|
+
await this.paneWriteLock.run(async () => {
|
|
8121
|
+
if (!this.tmux)
|
|
8122
|
+
return;
|
|
8123
|
+
// ONE capture decides failed-turn ownership and empty/idle input.
|
|
8124
|
+
sent = await this.submitSystemPaste(t("inst.codex_capacity_nudge_text"), "capacity-continue", {
|
|
8125
|
+
current,
|
|
8126
|
+
accept: pane => this.capacityNudgePaneCurrent(nudge, pane),
|
|
8127
|
+
});
|
|
8128
|
+
this.logger.info({ sent }, "Model-capacity nudge attempted");
|
|
7967
8129
|
});
|
|
7968
|
-
|
|
7969
|
-
|
|
8130
|
+
}
|
|
8131
|
+
finally {
|
|
8132
|
+
if (!sent)
|
|
8133
|
+
this.reportCapacityNudgeCancelled(nudge, "the retry could not be submitted");
|
|
8134
|
+
}
|
|
7970
8135
|
}
|
|
7971
8136
|
/** What identifies the request on screen: the backend's own identity when it has one, else the whole screen verbatim. */
|
|
7972
8137
|
static screenOf(dialog, pane) {
|
|
@@ -10233,7 +10398,9 @@ export class Daemon extends EventEmitter {
|
|
|
10233
10398
|
// false and must not clear the window either.
|
|
10234
10399
|
if (replyAttempt?.completionAction) {
|
|
10235
10400
|
const snap = this.turnReplyGuard.snapshot();
|
|
10236
|
-
|
|
10401
|
+
// generation 0 is a pre-arm token applied to the live generation (#2544).
|
|
10402
|
+
if (snap && (snap.generation === replyAttempt.generation || replyAttempt.generation === 0)
|
|
10403
|
+
&& snap.phase === "awaiting"
|
|
10237
10404
|
&& snap.completionDelivered && !snap.cancelledByUser) {
|
|
10238
10405
|
this.clearReplyGuardConfirm();
|
|
10239
10406
|
}
|
|
@@ -10883,9 +11050,14 @@ export class Daemon extends EventEmitter {
|
|
|
10883
11050
|
beginSpawn() {
|
|
10884
11051
|
this.unclearOomStopped = false;
|
|
10885
11052
|
this.publishWorkingState("before_spawn");
|
|
11053
|
+
this.recoveryContinuationId = null;
|
|
10886
11054
|
this.workingBackgroundObservation = null;
|
|
11055
|
+
this.ownedWorkingSession = null;
|
|
11056
|
+
this.recoveryBootSettled = false;
|
|
11057
|
+
this.recoveryNativeWork = false;
|
|
11058
|
+
this.recoveryCancelled = false;
|
|
10887
11059
|
this.dialogAnswers = null;
|
|
10888
|
-
this.
|
|
11060
|
+
this.cancelCapacityNudge("the instance was stopped, paused or respawned");
|
|
10889
11061
|
this.lastErrorEpisode = null;
|
|
10890
11062
|
this.nudgeBaselines = null;
|
|
10891
11063
|
// A relaunch settles any outstanding quit watch: the CLI came back.
|
|
@@ -10916,6 +11088,8 @@ export class Daemon extends EventEmitter {
|
|
|
10916
11088
|
this.spawnDepth = Math.max(0, this.spawnDepth - 1);
|
|
10917
11089
|
if (this.spawnDepth > 0)
|
|
10918
11090
|
return; // an enclosing spawn is still running
|
|
11091
|
+
if (!this.recoveryBootWorkPending)
|
|
11092
|
+
this.recoveryBootSettled = true;
|
|
10919
11093
|
this.spawning = false;
|
|
10920
11094
|
const resolve = this.resolveSpawnSettled;
|
|
10921
11095
|
this.spawnSettled = null;
|
|
@@ -12369,6 +12543,13 @@ export class Daemon extends EventEmitter {
|
|
|
12369
12543
|
const sid = this.backend?.getSessionId();
|
|
12370
12544
|
if (!sid)
|
|
12371
12545
|
return;
|
|
12546
|
+
if ((this.config.backend ?? "claude-code") === "claude-code" && Buffer.byteLength(sid) <= 256
|
|
12547
|
+
&& !/[\u0000-\u001f\u007f]/.test(sid) && this.ownedWorkingSession !== sid) {
|
|
12548
|
+
this.ownedWorkingSession = sid;
|
|
12549
|
+
// Reuse this existing owned checkpoint. An idle instance may have no
|
|
12550
|
+
// later state edge before an OOM; its journal must not stay unknown.
|
|
12551
|
+
this.publishWorkingState("process");
|
|
12552
|
+
}
|
|
12372
12553
|
const path = join(this.instanceDir, "session-id");
|
|
12373
12554
|
// Idle observations happen after turns. Avoid needless writes (and mtime
|
|
12374
12555
|
// churn) while still updating the marker when /new changes the session.
|