@songsid/agend 2.3.0 → 2.3.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/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/backend/muse.d.ts +20 -2
- package/dist/backend/muse.js +154 -16
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -1
- package/dist/channel/adapters/telegram.js +3 -35
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +2 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +13 -0
- package/dist/daemon.js +155 -16
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +11 -0
- package/dist/delivery-outbox.js +47 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -1
- package/dist/fleet-lock.d.ts +6 -0
- package/dist/fleet-lock.js +19 -0
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +34 -2
- package/dist/fleet-manager.js +264 -39
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +3 -2
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -2
- package/dist/instance-lifecycle.d.ts +1 -0
- package/dist/instance-lifecycle.js +11 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +24 -2
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -0
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.js +1 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -2
- package/dist/outbound-handlers.js +5 -5
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/search/search-index.d.ts +10 -2
- package/dist/search/search-index.js +17 -4
- package/dist/search/search-index.js.map +1 -1
- package/dist/service-installer.d.ts +23 -1
- package/dist/service-installer.js +40 -11
- package/dist/service-installer.js.map +1 -1
- package/dist/session-oom-stop.d.ts +45 -0
- package/dist/session-oom-stop.js +259 -0
- package/dist/session-oom-stop.js.map +1 -0
- package/dist/session-oom.d.ts +13 -0
- package/dist/session-oom.js +119 -7
- package/dist/session-oom.js.map +1 -1
- package/dist/settings-api.d.ts +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/spawn-gate.d.ts +2 -0
- package/dist/spawn-gate.js +17 -4
- package/dist/spawn-gate.js.map +1 -1
- package/dist/start-attempt.d.ts +11 -1
- package/dist/start-attempt.js +16 -5
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +37 -0
- package/dist/start-failure-chat.js +142 -0
- package/dist/start-failure-chat.js.map +1 -0
- package/dist/start-failure.d.ts +24 -0
- package/dist/start-failure.js +68 -1
- package/dist/start-failure.js.map +1 -1
- package/dist/telegram-api-root.d.ts +1 -0
- package/dist/telegram-api-root.js +40 -0
- package/dist/telegram-api-root.js.map +1 -0
- package/dist/types.d.ts +3 -2
- package/dist/ui/panel-chat.js +19 -6
- package/dist/ui/panel-log.js +3 -3
- package/dist/ui/panel-settings.js +8 -1
- package/dist/ui/settings-strings.js +6 -0
- package/dist/ui/shared/app-shell.js +21 -4
- package/dist/ui/shared/app.css +9 -3
- package/dist/ui/shared/usage-dialog.js +21 -11
- package/dist/ui/shared/view-strings.js +2 -0
- package/dist/web-api.d.ts +1 -1
- package/dist/web-api.js +1 -1
- package/dist/web-api.js.map +1 -1
- package/dist/working-state-journal.d.ts +68 -0
- package/dist/working-state-journal.js +93 -0
- package/dist/working-state-journal.js.map +1 -0
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -217,6 +217,8 @@ import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
|
217
217
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
218
218
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
219
219
|
import { presentationState, interactionSummary, sameInteractionOwner } from "./interaction-observation.js";
|
|
220
|
+
import { describeSelfOom } from "./session-oom.js";
|
|
221
|
+
import { assertNoSessionOomStop, readSessionOomStop, captureSessionOomRetry, clearSessionOomStopForRetry, SessionOomStoppedError, sessionOomStopHint } from "./session-oom-stop.js";
|
|
220
222
|
import { idleGateTimeoutLog } from "./idle-gate-log.js";
|
|
221
223
|
import { DEFAULT_STUCK_TIMEOUT_MS, readLastInboundAt } from "./daemon.js";
|
|
222
224
|
import { clearPausedMarker, readPausedAt, readPauseReason, writePausedMarker } from "./pause-marker.js";
|
|
@@ -460,6 +462,7 @@ const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
|
460
462
|
const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
|
|
461
463
|
const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
|
|
462
464
|
const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
|
|
465
|
+
const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
|
|
463
466
|
/** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
|
|
464
467
|
const NEEDS_PAUSE_TTL_MS = 10_000;
|
|
465
468
|
const HANG_CALLBACK_PREFIX = "hang:";
|
|
@@ -471,7 +474,7 @@ const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
|
471
474
|
* group's approval, tips, and the per-user /model and /effort menus (which are not nonce entries at all).
|
|
472
475
|
*/
|
|
473
476
|
const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
|
|
474
|
-
HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
|
|
477
|
+
HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, OOM_RETRY_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
|
|
475
478
|
]);
|
|
476
479
|
/**
|
|
477
480
|
* Where a reply goes on a fleet with no chat platform (web track C4): it is "sent" by being shown in the
|
|
@@ -759,6 +762,8 @@ export class FleetManager {
|
|
|
759
762
|
* backend outage during a post-update herd). instanceName → pending retry.
|
|
760
763
|
*/
|
|
761
764
|
startupRetries = new Map();
|
|
765
|
+
/** #2484: instances whose automatic startup retry is running now (between its timer and its outcome). */
|
|
766
|
+
startupRetriesRunning = new Set();
|
|
762
767
|
/** Bumped by every explicit stop (incl. the stop half of a restart); fences the outage hand-off. */
|
|
763
768
|
explicitStopGeneration = new Map();
|
|
764
769
|
/** instanceName → outage hand-off currently executing; a repeat joins it. */
|
|
@@ -1792,7 +1797,7 @@ export class FleetManager {
|
|
|
1792
1797
|
...(web && by.guest ? { web_guest: "1" } : {}), // #1920: "[user:… via web (guest), id:…]", as /ui/send
|
|
1793
1798
|
...reactions.meta,
|
|
1794
1799
|
},
|
|
1795
|
-
}, by.humanOrigin, button.label)) !== false;
|
|
1800
|
+
}, by.humanOrigin, button.label, this.startRetryHoldFor(set.instance, inbound))) !== false;
|
|
1796
1801
|
}
|
|
1797
1802
|
finally {
|
|
1798
1803
|
// Both false and throw reopen the choice in the controller. Finish the
|
|
@@ -1841,8 +1846,18 @@ export class FleetManager {
|
|
|
1841
1846
|
}
|
|
1842
1847
|
this.deliveryOutbox = outbox;
|
|
1843
1848
|
this.deliveryOutboxRecovered = true;
|
|
1849
|
+
outbox.on("working_references", (names) => {
|
|
1850
|
+
for (const name of names)
|
|
1851
|
+
this.observeWorkingState(name, "obligations");
|
|
1852
|
+
});
|
|
1844
1853
|
outbox.on("admitted", () => { this.scheduleDeliveryOutboxPump(); this.wakeCoordinator?.kick(); });
|
|
1845
1854
|
outbox.on("state", (event) => {
|
|
1855
|
+
if (event.deliveryId) {
|
|
1856
|
+
const row = outbox.get(event.deliveryId);
|
|
1857
|
+
if (row)
|
|
1858
|
+
for (const name of new Set([row.sourceInstance, row.targetInstance]))
|
|
1859
|
+
this.observeWorkingState(name, "obligations");
|
|
1860
|
+
}
|
|
1846
1861
|
this.scheduleDeliveryOutboxPump();
|
|
1847
1862
|
this.wakeCoordinator?.kick();
|
|
1848
1863
|
if (event.deliveryId && event.state === "delivered") {
|
|
@@ -2001,8 +2016,19 @@ export class FleetManager {
|
|
|
2001
2016
|
: item),
|
|
2002
2017
|
};
|
|
2003
2018
|
}
|
|
2019
|
+
/** A terminal OOM stop is not an auto-wakeable pause, including after a fleet restart. */
|
|
2020
|
+
sessionOomStopped(name) {
|
|
2021
|
+
return this.daemons.get(name)?.isSessionOomStopped?.() === true || readSessionOomStop(this.getInstanceDir(name)).blocked;
|
|
2022
|
+
}
|
|
2023
|
+
sessionOomStopReason(name) {
|
|
2024
|
+
const state = readSessionOomStop(this.getInstanceDir(name));
|
|
2025
|
+
return state.blocked ? sessionOomStopHint(state)
|
|
2026
|
+
: this.daemons.get(name)?.notAcceptingReason?.() ?? sessionOomStopHint();
|
|
2027
|
+
}
|
|
2004
2028
|
/** #1641: why a target is not taking queued work right now, or null when nothing known holds it. */
|
|
2005
2029
|
deliveryWaitingReason(target) {
|
|
2030
|
+
if (this.sessionOomStopped(target))
|
|
2031
|
+
return this.sessionOomStopReason(target);
|
|
2006
2032
|
if (this.lifecycle.isPaused(target))
|
|
2007
2033
|
return t("delivery.hold_state", "paused");
|
|
2008
2034
|
return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
|
|
@@ -2015,6 +2041,9 @@ export class FleetManager {
|
|
|
2015
2041
|
}
|
|
2016
2042
|
/** Called after a replacement Daemon object is installed in lifecycle.daemons. */
|
|
2017
2043
|
onDaemonReady(name, daemonBootId) {
|
|
2044
|
+
const daemon = this.daemons.get(name);
|
|
2045
|
+
if (daemon?.bootId === daemonBootId)
|
|
2046
|
+
this.listenForWorkingState(name, daemon);
|
|
2018
2047
|
const outbox = this.deliveryOutbox;
|
|
2019
2048
|
if (!outbox)
|
|
2020
2049
|
return;
|
|
@@ -2038,6 +2067,82 @@ export class FleetManager {
|
|
|
2038
2067
|
}
|
|
2039
2068
|
this.scheduleDeliveryOutboxPump();
|
|
2040
2069
|
}
|
|
2070
|
+
workingStateListeners;
|
|
2071
|
+
workingStateErrors;
|
|
2072
|
+
/** Attach only to the published owner. Nothing here sends or resumes work. */
|
|
2073
|
+
listenForWorkingState(name, daemon) {
|
|
2074
|
+
if (typeof daemon.getWorkingStateObservation !== "function")
|
|
2075
|
+
return;
|
|
2076
|
+
this.workingStateListeners ??= new WeakSet();
|
|
2077
|
+
if (this.workingStateListeners.has(daemon))
|
|
2078
|
+
return;
|
|
2079
|
+
this.workingStateListeners.add(daemon);
|
|
2080
|
+
daemon.on("working_state", (event) => {
|
|
2081
|
+
try {
|
|
2082
|
+
if (this.daemons.get(name) !== daemon || !sameInteractionOwner(event.observation.owner, daemon.getInteractionSnapshot().owner))
|
|
2083
|
+
return;
|
|
2084
|
+
this.observeWorkingState(name, event.reason, event.observation);
|
|
2085
|
+
}
|
|
2086
|
+
catch { /* A malformed/stale observation cannot affect the daemon. */ }
|
|
2087
|
+
});
|
|
2088
|
+
this.observeWorkingState(name, "published");
|
|
2089
|
+
}
|
|
2090
|
+
observeWorkingState(name, reason, supplied) {
|
|
2091
|
+
const outbox = this.deliveryOutbox, daemon = this.daemons.get(name);
|
|
2092
|
+
if (!outbox || !daemon || typeof daemon.getWorkingStateObservation !== "function")
|
|
2093
|
+
return;
|
|
2094
|
+
try {
|
|
2095
|
+
const observation = supplied ?? daemon.getWorkingStateObservation();
|
|
2096
|
+
if (!sameInteractionOwner(observation.owner, daemon.getInteractionSnapshot().owner))
|
|
2097
|
+
return;
|
|
2098
|
+
const references = [];
|
|
2099
|
+
const unavailable = [];
|
|
2100
|
+
let totalReferences = 0;
|
|
2101
|
+
const board = this.scheduler?.db.board;
|
|
2102
|
+
if (!board)
|
|
2103
|
+
unavailable.push("tasks");
|
|
2104
|
+
else
|
|
2105
|
+
try {
|
|
2106
|
+
const tasks = board.listCards({ assignee: name, status: ["claimed", "blocked", "review"], limit: 16 });
|
|
2107
|
+
totalReferences += tasks.total;
|
|
2108
|
+
for (const card of tasks.cards)
|
|
2109
|
+
references.push({ kind: "task", id: card.uuid, version: card.version,
|
|
2110
|
+
status: card.status });
|
|
2111
|
+
}
|
|
2112
|
+
catch {
|
|
2113
|
+
unavailable.push("tasks");
|
|
2114
|
+
}
|
|
2115
|
+
try {
|
|
2116
|
+
const obligations = outbox.workingObligations(name);
|
|
2117
|
+
references.push(...obligations.references);
|
|
2118
|
+
totalReferences += obligations.total;
|
|
2119
|
+
}
|
|
2120
|
+
catch {
|
|
2121
|
+
unavailable.push("obligations");
|
|
2122
|
+
}
|
|
2123
|
+
const entry = outbox.observeWorkingState(name, { version: 1, reason, observedAt: Date.now(), observation,
|
|
2124
|
+
lastActivityAt: this.lastActivity.get(name) ?? null, references, totalReferences, unavailable });
|
|
2125
|
+
if (!entry)
|
|
2126
|
+
return;
|
|
2127
|
+
const state = entry.current.observation.executionState ?? "unknown";
|
|
2128
|
+
const counts = { tasks: entry.current.references.filter(r => r.kind === "task").length,
|
|
2129
|
+
owed: entry.current.references.filter(r => r.kind === "owed").length,
|
|
2130
|
+
waiting: entry.current.references.filter(r => r.kind === "waiting").length,
|
|
2131
|
+
background: entry.current.observation.backgroundWork,
|
|
2132
|
+
total: entry.current.totalReferences, unavailable: entry.current.unavailable };
|
|
2133
|
+
this.logger.info({ name, state, reason, revision: entry.revision, ...counts }, "Working-state journal observed");
|
|
2134
|
+
this.eventLog?.insert(name, "working_state", { state, reason, revision: entry.revision, ...counts,
|
|
2135
|
+
message: `Observed ${state}; tasks ${counts.tasks}, replies owed ${counts.owed}, waiting ${counts.waiting}; ${counts.total} references${counts.background === true ? "; background work running" : ""}${unavailable.length ? "; some sources unavailable" : ""}. Journal only; no recovery action.` });
|
|
2136
|
+
this.emitSseEvent("steps_tick", { instance: name });
|
|
2137
|
+
}
|
|
2138
|
+
catch {
|
|
2139
|
+
this.workingStateErrors ??= new Set();
|
|
2140
|
+
if (!this.workingStateErrors.has(name)) {
|
|
2141
|
+
this.workingStateErrors.add(name);
|
|
2142
|
+
this.logger.warn({ name }, "Could not persist working-state observation; delivery and restart behavior unchanged");
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2041
2146
|
/**
|
|
2042
2147
|
* #926: the two reply-obligation safety nets. Idle owners with an unanswered
|
|
2043
2148
|
* request get one reminder (after the grace); requesters get one overdue
|
|
@@ -3725,7 +3830,7 @@ export class FleetManager {
|
|
|
3725
3830
|
finish(false);
|
|
3726
3831
|
});
|
|
3727
3832
|
}
|
|
3728
|
-
async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent) {
|
|
3833
|
+
async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold) {
|
|
3729
3834
|
if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
3730
3835
|
return false;
|
|
3731
3836
|
await this.holdDeliveryForStorm(instanceName, deliveryEpoch);
|
|
@@ -3764,7 +3869,7 @@ export class FleetManager {
|
|
|
3764
3869
|
const log = idleGateTimeoutLog(instanceName, timeoutMs, this.instanceStateCache.get(instanceName), Date.now());
|
|
3765
3870
|
this.logger[log.level](log.fields, log.message);
|
|
3766
3871
|
}
|
|
3767
|
-
const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent);
|
|
3872
|
+
const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
|
|
3768
3873
|
if (!sent)
|
|
3769
3874
|
return false;
|
|
3770
3875
|
if (this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent)) {
|
|
@@ -3793,8 +3898,11 @@ export class FleetManager {
|
|
|
3793
3898
|
* Ordering is preserved by serialising behind any waiter already queued for this
|
|
3794
3899
|
* instance, *including* when the socket happens to be up: otherwise a message
|
|
3795
3900
|
* arriving after the reconnect could overtake one that has been waiting for it.
|
|
3901
|
+
*
|
|
3902
|
+
* `onStartRetryHold` (#2484, a person's message): an instance in its own start-retry backoff is waited for until that
|
|
3903
|
+
* retry brings it up, however long the backoff, and fails only once the retries give up.
|
|
3796
3904
|
*/
|
|
3797
|
-
async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent) {
|
|
3905
|
+
async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
|
|
3798
3906
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
3799
3907
|
return false;
|
|
3800
3908
|
const queued = this.ipcWaitTails.get(instanceName);
|
|
@@ -3808,7 +3916,7 @@ export class FleetManager {
|
|
|
3808
3916
|
.then(() => {
|
|
3809
3917
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
3810
3918
|
return false;
|
|
3811
|
-
return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent);
|
|
3919
|
+
return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
|
|
3812
3920
|
});
|
|
3813
3921
|
// The chain stores a settled-either-way promise so one failed delivery cannot
|
|
3814
3922
|
// wedge every later one, and so `queued` above is safe to await unguarded.
|
|
@@ -3826,9 +3934,10 @@ export class FleetManager {
|
|
|
3826
3934
|
}
|
|
3827
3935
|
}
|
|
3828
3936
|
/** Poll for the instance's IPC to come back, then send. Throws if it does not. */
|
|
3829
|
-
async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent) {
|
|
3830
|
-
|
|
3937
|
+
async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
|
|
3938
|
+
let deadline = Date.now() + IPC_RECONNECT_GRACE_MS;
|
|
3831
3939
|
let warned = false;
|
|
3940
|
+
let held = false;
|
|
3832
3941
|
for (;;) {
|
|
3833
3942
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
3834
3943
|
return false;
|
|
@@ -3837,6 +3946,22 @@ export class FleetManager {
|
|
|
3837
3946
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3838
3947
|
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
3839
3948
|
return true;
|
|
3949
|
+
// #2484: while the instance waits for (or runs) its own start retry, the grace runs from now — so the message is
|
|
3950
|
+
// delivered once a retry brings it up, and fails only after the retries stop (they gave up, or a person stopped
|
|
3951
|
+
// or paused it) and a last grace passes without it.
|
|
3952
|
+
if (onStartRetryHold && this.inStartRetryBackoff(instanceName)) {
|
|
3953
|
+
deadline = Math.max(deadline, Date.now() + IPC_RECONNECT_GRACE_MS);
|
|
3954
|
+
if (!held) {
|
|
3955
|
+
held = true;
|
|
3956
|
+
this.logger.info({ instanceName, retry: this.startupRetries.get(instanceName)?.attempt }, "Instance is waiting for its start retry — holding the message until it is up");
|
|
3957
|
+
try {
|
|
3958
|
+
onStartRetryHold();
|
|
3959
|
+
}
|
|
3960
|
+
catch (err) {
|
|
3961
|
+
this.logger.debug({ err, instanceName }, "Start-retry hold notice failed");
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3840
3965
|
if (Date.now() >= deadline) {
|
|
3841
3966
|
throw new Error(`Instance '${instanceName}' IPC is unavailable`);
|
|
3842
3967
|
}
|
|
@@ -4222,6 +4347,18 @@ export class FleetManager {
|
|
|
4222
4347
|
this.deliveryOutbox?.markHumanReceiptUncertain(id, instanceName);
|
|
4223
4348
|
}
|
|
4224
4349
|
}
|
|
4350
|
+
/**
|
|
4351
|
+
* #2484: a person's message waits for its instance's own start retry (DeliveryOptions.onStartRetryHold), shown as
|
|
4352
|
+
* queued on the message meanwhile. A ClassicBot room message (no `msg`) has no delivery-status reactions: it only waits.
|
|
4353
|
+
*/
|
|
4354
|
+
startRetryHoldFor(instanceName, msg) {
|
|
4355
|
+
return {
|
|
4356
|
+
onStartRetryHold: () => {
|
|
4357
|
+
if (msg?.chatId && msg.messageId)
|
|
4358
|
+
this.reactMessageStatus(instanceName, msg.chatId, msg.messageId, "queued", msg.threadId || undefined);
|
|
4359
|
+
},
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4225
4362
|
/** Synchronous steer/btw handoff, using the same evidence and owner binding. */
|
|
4226
4363
|
sendHumanControl(instanceName, payload, origin, originalBody) {
|
|
4227
4364
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
@@ -4262,7 +4399,7 @@ export class FleetManager {
|
|
|
4262
4399
|
return false;
|
|
4263
4400
|
this.enforceWarmCap(instanceName); // woke one → evict a different LRU idle if over cap
|
|
4264
4401
|
}
|
|
4265
|
-
const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent);
|
|
4402
|
+
const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold);
|
|
4266
4403
|
if (!sent)
|
|
4267
4404
|
return false;
|
|
4268
4405
|
// A cross-instance item arriving before the daemon observes this turn as
|
|
@@ -4273,7 +4410,7 @@ export class FleetManager {
|
|
|
4273
4410
|
return true;
|
|
4274
4411
|
}
|
|
4275
4412
|
const previous = this.idleGatedDeliveryTails.get(instanceName) ?? Promise.resolve();
|
|
4276
|
-
const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent));
|
|
4413
|
+
const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold));
|
|
4277
4414
|
this.idleGatedDeliveryTails.set(instanceName, delivery);
|
|
4278
4415
|
try {
|
|
4279
4416
|
return await delivery;
|
|
@@ -4730,7 +4867,7 @@ export class FleetManager {
|
|
|
4730
4867
|
mode: target => this.deliveryWorkerMode(target),
|
|
4731
4868
|
available: () => !this.shuttingDown && this.deliveryOutbox?.isOpen === true,
|
|
4732
4869
|
listPending: () => this.deliveryOutbox?.listPending() ?? [],
|
|
4733
|
-
isPaused: target => this.lifecycle.isPaused(target),
|
|
4870
|
+
isPaused: target => !this.sessionOomStopped(target) && this.lifecycle.isPaused(target),
|
|
4734
4871
|
pauseReason: target => readPauseReason(this.getInstanceDir(target)),
|
|
4735
4872
|
isRestarting: target => this.isInstanceRestarting(target),
|
|
4736
4873
|
wake: async (target) => {
|
|
@@ -4742,7 +4879,7 @@ export class FleetManager {
|
|
|
4742
4879
|
warmCap: () => this.fleetConfig?.defaults?.warm_cap ?? 0,
|
|
4743
4880
|
warmOverflow: () => this.fleetConfig?.defaults?.warm_overflow ?? DEFAULT_WARM_OVERFLOW,
|
|
4744
4881
|
wokeAt: target => this.lifecycle.wokeAtFor(target),
|
|
4745
|
-
notAcceptingReason: target => this.daemons.get(target)?.notAcceptingReason?.() ?? null,
|
|
4882
|
+
notAcceptingReason: target => this.sessionOomStopped(target) ? this.sessionOomStopReason(target) : this.daemons.get(target)?.notAcceptingReason?.() ?? null,
|
|
4746
4883
|
notifyTarget: (target, text) => { this.notifyInstanceTopic(target, text); },
|
|
4747
4884
|
notifySender: (source, text) => { this.notifyInstanceTopic(source, text); },
|
|
4748
4885
|
kickPump: () => this.scheduleDeliveryOutboxPump(),
|
|
@@ -4797,7 +4934,12 @@ export class FleetManager {
|
|
|
4797
4934
|
* is exactly the old lifecycle wake; otherwise it goes through the shared
|
|
4798
4935
|
* slot admission and joins any wake already in flight for the target.
|
|
4799
4936
|
*/
|
|
4800
|
-
async explicitWake(name, timeoutMs = 30_000) {
|
|
4937
|
+
async explicitWake(name, timeoutMs = 30_000, retryStoppedSession = false) {
|
|
4938
|
+
if (this.sessionOomStopped(name)) {
|
|
4939
|
+
if (!retryStoppedSession)
|
|
4940
|
+
throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
|
|
4941
|
+
return this.restartSingleInstance(name, { explicit: true });
|
|
4942
|
+
}
|
|
4801
4943
|
const token = await this.acquireWakeSlot(name);
|
|
4802
4944
|
try {
|
|
4803
4945
|
await this.lifecycle.wake(name, timeoutMs);
|
|
@@ -4818,7 +4960,7 @@ export class FleetManager {
|
|
|
4818
4960
|
}
|
|
4819
4961
|
async changeInstancePauseState(name, action) {
|
|
4820
4962
|
if (action === "wake") {
|
|
4821
|
-
await this.explicitWake(name, 30_000);
|
|
4963
|
+
await this.explicitWake(name, 30_000, true);
|
|
4822
4964
|
this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
|
|
4823
4965
|
return "awake";
|
|
4824
4966
|
}
|
|
@@ -4994,6 +5136,11 @@ export class FleetManager {
|
|
|
4994
5136
|
/** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
|
|
4995
5137
|
transition, execution) {
|
|
4996
5138
|
execution?.assert();
|
|
5139
|
+
if (this.sessionOomStopped(name)) {
|
|
5140
|
+
if (resumePaused)
|
|
5141
|
+
return this.restartSingleInstance(name, { explicit: true });
|
|
5142
|
+
throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
|
|
5143
|
+
}
|
|
4997
5144
|
// CLI single-instance cold starts bypass startAll. Start diagnostics before
|
|
4998
5145
|
// any lifecycle/wake work; a shutdown must not revive the stopped sampler.
|
|
4999
5146
|
if (!this.shuttingDown)
|
|
@@ -5002,7 +5149,7 @@ export class FleetManager {
|
|
|
5002
5149
|
// into a running instance — harmless, but noisy — or race this start).
|
|
5003
5150
|
this.cancelStartupRetry(name);
|
|
5004
5151
|
if (resumePaused && this.lifecycle.isPaused(name)) {
|
|
5005
|
-
await this.explicitWake(name, 30_000);
|
|
5152
|
+
await this.explicitWake(name, 30_000, true);
|
|
5006
5153
|
// A successful wake clears the persisted pause marker and produces a
|
|
5007
5154
|
// fresh instance_state snapshot. Drop any stale process error left by a
|
|
5008
5155
|
// pre-pause crash so /api/fleet and `agend ls` converge on running.
|
|
@@ -5022,7 +5169,7 @@ export class FleetManager {
|
|
|
5022
5169
|
if (resumePaused) {
|
|
5023
5170
|
const status = this.getInstanceStatus(name);
|
|
5024
5171
|
if (status === "crashed" || status === "stopped") {
|
|
5025
|
-
await this.restartSingleInstance(name);
|
|
5172
|
+
await this.restartSingleInstance(name, { explicit: true });
|
|
5026
5173
|
return;
|
|
5027
5174
|
}
|
|
5028
5175
|
}
|
|
@@ -5197,6 +5344,8 @@ export class FleetManager {
|
|
|
5197
5344
|
// outcome. Retrying would start an instance the operator just stopped.
|
|
5198
5345
|
if (err instanceof SupersededStartError)
|
|
5199
5346
|
return false;
|
|
5347
|
+
if (err instanceof SessionOomStoppedError)
|
|
5348
|
+
await this.notifySessionOomStop(name);
|
|
5200
5349
|
if (this.stopOnUnsupportedCli(name, err, what))
|
|
5201
5350
|
return false;
|
|
5202
5351
|
this.logger.error({ err, name }, `Failed to start ${what}`);
|
|
@@ -5217,6 +5366,11 @@ export class FleetManager {
|
|
|
5217
5366
|
scheduleStartupRetry(name, attempt) {
|
|
5218
5367
|
if (this.shuttingDown)
|
|
5219
5368
|
return;
|
|
5369
|
+
if (this.sessionOomStopped(name)) {
|
|
5370
|
+
this.instanceProcessStatus.set(name, "stopped");
|
|
5371
|
+
this.logger.warn({ name }, this.sessionOomStopReason(name));
|
|
5372
|
+
return;
|
|
5373
|
+
}
|
|
5220
5374
|
if (this.startupRetries.has(name))
|
|
5221
5375
|
return;
|
|
5222
5376
|
const backoff = FleetManager.STARTUP_RETRY_BACKOFF_MS;
|
|
@@ -5244,6 +5398,12 @@ export class FleetManager {
|
|
|
5244
5398
|
this.startupRetries.delete(name);
|
|
5245
5399
|
this.logger.info({ name }, "Pending automatic startup retry cancelled");
|
|
5246
5400
|
}
|
|
5401
|
+
/** #2484: the instance's own start retry is scheduled or running, and no person paused it (a stop cancels the retry). */
|
|
5402
|
+
inStartRetryBackoff(name) {
|
|
5403
|
+
if (this.shuttingDown || this.lifecycle.isPaused(name))
|
|
5404
|
+
return false;
|
|
5405
|
+
return this.startupRetries.has(name) || this.startupRetriesRunning.has(name);
|
|
5406
|
+
}
|
|
5247
5407
|
/** Pending automatic retry, if any (status display / tests). */
|
|
5248
5408
|
pendingStartupRetry(name) {
|
|
5249
5409
|
const pending = this.startupRetries.get(name);
|
|
@@ -5294,6 +5454,8 @@ export class FleetManager {
|
|
|
5294
5454
|
this.startupRetries.delete(name);
|
|
5295
5455
|
if (this.shuttingDown)
|
|
5296
5456
|
return;
|
|
5457
|
+
if (this.sessionOomStopped(name))
|
|
5458
|
+
return;
|
|
5297
5459
|
if (!this.isConfiguredInstance(name))
|
|
5298
5460
|
return; // removed from fleet.yaml / classic channel meanwhile
|
|
5299
5461
|
if (this.lifecycle.isPaused(name) || this.daemons.has(name))
|
|
@@ -5309,6 +5471,7 @@ export class FleetManager {
|
|
|
5309
5471
|
}
|
|
5310
5472
|
const topicMode = this.fleetConfig?.channel?.mode === "topic"
|
|
5311
5473
|
|| !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
|
|
5474
|
+
this.startupRetriesRunning.add(name);
|
|
5312
5475
|
try {
|
|
5313
5476
|
await this.spawnGate.run({
|
|
5314
5477
|
instanceName: name,
|
|
@@ -5330,6 +5493,9 @@ export class FleetManager {
|
|
|
5330
5493
|
this.logger.error({ err, name, attempt: attempt + 1 }, "Automatic startup retry failed");
|
|
5331
5494
|
this.scheduleStartupRetry(name, attempt + 1);
|
|
5332
5495
|
}
|
|
5496
|
+
finally {
|
|
5497
|
+
this.startupRetriesRunning.delete(name);
|
|
5498
|
+
}
|
|
5333
5499
|
}
|
|
5334
5500
|
/** Fleet-topic (fleet.yaml) or ClassicBot (classic channel) — both are retryable; anything else is gone. */
|
|
5335
5501
|
isConfiguredInstance(name) {
|
|
@@ -5655,12 +5821,29 @@ export class FleetManager {
|
|
|
5655
5821
|
}
|
|
5656
5822
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
5657
5823
|
async restartSingleInstance(name, opts) {
|
|
5824
|
+
if (opts?.recoveryCurrent && !opts.recoveryCurrent())
|
|
5825
|
+
throw new Error(t("exit.expired", name));
|
|
5826
|
+
const retry = opts?.explicit ? opts.sessionOomRetry ?? captureSessionOomRetry(this.getInstanceDir(name)) : undefined;
|
|
5827
|
+
if (opts?.sessionOomRetry) {
|
|
5828
|
+
const current = captureSessionOomRetry(this.getInstanceDir(name));
|
|
5829
|
+
// An unclear-cause stop intentionally has no tombstone. Only its live
|
|
5830
|
+
// launch fence can authorize an absent-marker receipt; positive stops
|
|
5831
|
+
// still require the offered durable revision.
|
|
5832
|
+
if ((!retry?.revision && !opts.recoveryCurrent) || current.revision !== retry?.revision || current.legacyRevision !== retry?.legacyRevision)
|
|
5833
|
+
throw new SessionOomStoppedError(true);
|
|
5834
|
+
}
|
|
5835
|
+
if (!opts?.explicit && this.sessionOomStopped(name))
|
|
5836
|
+
throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
|
|
5658
5837
|
// One restart at a time per instance. Multiple sources can ask concurrently
|
|
5659
5838
|
// (MCP revival, /restart, pty_error, model failover); a second stop/start
|
|
5660
5839
|
// interleaved with the first tears down the window the first just created.
|
|
5661
5840
|
// Later callers join the in-flight restart instead — its opts win.
|
|
5662
5841
|
const inFlight = this.restartsInFlight.get(name);
|
|
5663
5842
|
if (inFlight) {
|
|
5843
|
+
// Do not let a late explicit caller clear a stop belonging to the
|
|
5844
|
+
// automatic flight it would join. That action must be retried afterwards.
|
|
5845
|
+
if (retry && this.sessionOomStopped(name))
|
|
5846
|
+
throw new SessionOomStoppedError(true);
|
|
5664
5847
|
this.logger.info({ name }, "restartSingleInstance: joining the restart already in flight");
|
|
5665
5848
|
// #1753: a credential-profile switch joining it still needs its conversation carried in that restart's stop →
|
|
5666
5849
|
// start gap: handed to the restart while it has not reached the gap (and carries nothing else); otherwise not
|
|
@@ -5687,7 +5870,7 @@ export class FleetManager {
|
|
|
5687
5870
|
workingDirectory,
|
|
5688
5871
|
reason: "restart",
|
|
5689
5872
|
stage: "lifecycle",
|
|
5690
|
-
}, () => this.doRestartSingleInstance(name, opts, transition)))
|
|
5873
|
+
}, () => this.doRestartSingleInstance(name, opts, transition, retry)))
|
|
5691
5874
|
.finally(() => {
|
|
5692
5875
|
this.restartsInFlight.delete(name);
|
|
5693
5876
|
if (this.restartGaps.get(name) === gap)
|
|
@@ -5728,7 +5911,7 @@ export class FleetManager {
|
|
|
5728
5911
|
this.lifecycle.clearActivitySeedNow(name);
|
|
5729
5912
|
}
|
|
5730
5913
|
}
|
|
5731
|
-
async doRestartSingleInstance(name, opts, transition) {
|
|
5914
|
+
async doRestartSingleInstance(name, opts, transition, retry) {
|
|
5732
5915
|
if (this.configPath) {
|
|
5733
5916
|
this.loadConfig(this.configPath);
|
|
5734
5917
|
this.routing.rebuild(this.fleetConfig);
|
|
@@ -5746,7 +5929,7 @@ export class FleetManager {
|
|
|
5746
5929
|
if (!wasPaused)
|
|
5747
5930
|
this.restartsHoldingSlot.add(name);
|
|
5748
5931
|
try {
|
|
5749
|
-
await this.doRestartWithSlot(name, opts, transition, config, wasPaused);
|
|
5932
|
+
await this.doRestartWithSlot(name, opts, transition, config, wasPaused, retry);
|
|
5750
5933
|
// An explicit restart of a paused instance is an operator wake (§1.6):
|
|
5751
5934
|
// its success lifts the coordinator's park/backoff (the level stays).
|
|
5752
5935
|
// Automatic restarts never do.
|
|
@@ -5758,7 +5941,7 @@ export class FleetManager {
|
|
|
5758
5941
|
this.releaseWakeSlot(name, slot);
|
|
5759
5942
|
}
|
|
5760
5943
|
}
|
|
5761
|
-
async doRestartWithSlot(name, opts, transition, config, wasPaused) {
|
|
5944
|
+
async doRestartWithSlot(name, opts, transition, config, wasPaused, retry) {
|
|
5762
5945
|
const pausedAt = wasPaused ? (this.lifecycle.getLastPausedAt(name) ?? readPausedAt(this.getInstanceDir(name))) : null;
|
|
5763
5946
|
const pauseReason = wasPaused ? readPauseReason(this.getInstanceDir(name)) : null;
|
|
5764
5947
|
// #1753: the directory the CLI being stopped was launched in — its conversation is under that directory's key.
|
|
@@ -5766,6 +5949,12 @@ export class FleetManager {
|
|
|
5766
5949
|
// Between the stop and the start: the old CLI's transcript is complete, the new one has not looked yet.
|
|
5767
5950
|
/** `launching`: the login the start will use — read only when a switch is to be carried. */
|
|
5768
5951
|
const betweenStopAndStart = async (launching) => {
|
|
5952
|
+
// Clearing is synchronous, after the old daemon is stopped and inside
|
|
5953
|
+
// this action's serialized transition; an unseen later stop is retained.
|
|
5954
|
+
if (retry)
|
|
5955
|
+
clearSessionOomStopForRetry(this.getInstanceDir(name), retry);
|
|
5956
|
+
else
|
|
5957
|
+
assertNoSessionOomStop(this.getInstanceDir(name));
|
|
5769
5958
|
// #1753: this restart's own carry, or one a caller joined it with before this point (restartSingleInstance).
|
|
5770
5959
|
const gap = this.restartGaps.get(name);
|
|
5771
5960
|
const carry = gap?.carry ?? opts?.carry;
|
|
@@ -9702,7 +9891,7 @@ export class FleetManager {
|
|
|
9702
9891
|
...generalReactions.meta,
|
|
9703
9892
|
...extraMeta,
|
|
9704
9893
|
},
|
|
9705
|
-
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text);
|
|
9894
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text, this.startRetryHoldFor(generalInstance, msg));
|
|
9706
9895
|
generalReactions.consume();
|
|
9707
9896
|
this.afterUserInboundDelivered(generalInstance, msg);
|
|
9708
9897
|
this.logger.info(`${msg.username} → ${generalInstance}: ${(text ?? "").slice(0, 100)}`);
|
|
@@ -9798,7 +9987,7 @@ export class FleetManager {
|
|
|
9798
9987
|
...reactions.meta,
|
|
9799
9988
|
...extraMeta,
|
|
9800
9989
|
},
|
|
9801
|
-
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text);
|
|
9990
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text, this.startRetryHoldFor(instanceName, msg));
|
|
9802
9991
|
// Only after the message actually went out. A failed delivery keeps the
|
|
9803
9992
|
// reactions queued for the retry / the next message.
|
|
9804
9993
|
reactions.consume();
|
|
@@ -15021,14 +15210,42 @@ export class FleetManager {
|
|
|
15021
15210
|
/** A clean exit is intentional from the CLI's perspective, but often not from
|
|
15022
15211
|
* the operator's. Keep the instance notice passive and put the action in the
|
|
15023
15212
|
* same-world General topic where an administrator can make the choice. */
|
|
15024
|
-
async
|
|
15025
|
-
this.
|
|
15213
|
+
async notifySessionOomStop(instanceName, unclear = false) {
|
|
15214
|
+
await this.notifyNormalExit(instanceName, unclear ? "unclear" : true);
|
|
15215
|
+
}
|
|
15216
|
+
async notifyNormalExit(instanceName, oom = false) {
|
|
15217
|
+
const daemon = oom === "unclear" ? this.daemons.get(instanceName) : undefined;
|
|
15218
|
+
const owner = daemon ? { ...daemon.getInteractionSnapshot().owner } : undefined;
|
|
15219
|
+
const current = daemon && owner ? () => this.daemons.get(instanceName) === daemon
|
|
15220
|
+
&& daemon.isUnclearOomStopped() && sameInteractionOwner(owner, daemon.getInteractionSnapshot().owner) : undefined;
|
|
15221
|
+
if (oom === "unclear" && !current?.())
|
|
15222
|
+
return;
|
|
15223
|
+
const state = oom ? readSessionOomStop(this.getInstanceDir(instanceName)) : null;
|
|
15224
|
+
if (oom === "unclear" && state?.blocked)
|
|
15225
|
+
return;
|
|
15226
|
+
const proof = state?.blocked ? state.record?.evidence ?? state.evidence : undefined;
|
|
15227
|
+
if (oom === true && !proof)
|
|
15228
|
+
return;
|
|
15229
|
+
const sessionOomRetry = oom ? captureSessionOomRetry(this.getInstanceDir(instanceName)) : undefined;
|
|
15230
|
+
if (oom && [...this.pendingNonceButtons.values()].some(p => p.prefix === OOM_RETRY_CALLBACK_PREFIX
|
|
15231
|
+
&& p.instanceName === instanceName && p.sessionOomRetry?.revision === sessionOomRetry?.revision
|
|
15232
|
+
&& (oom !== "unclear" || p.confirmationCurrent?.() === true)))
|
|
15233
|
+
return;
|
|
15234
|
+
const prefix = oom ? OOM_RETRY_CALLBACK_PREFIX : EXIT_RESTART_CALLBACK_PREFIX;
|
|
15235
|
+
const message = oom === "unclear" ? t("inst.oom_unclear_choice", instanceName) : oom && proof
|
|
15236
|
+
? t("inst.session_oom_choice", instanceName, describeSelfOom(proof))
|
|
15237
|
+
: t("exit.general_notice", placeRef({ kind: "instance", name: instanceName }, instanceName));
|
|
15238
|
+
const choices = oom
|
|
15239
|
+
? [{ action: "resume", label: t("inst.session_oom_resume") }, { action: "fresh", label: t("inst.session_oom_fresh") }]
|
|
15240
|
+
: [{ action: "restart", label: t("exit.restart") }, { action: "ignore", label: t("exit.ignore") }];
|
|
15241
|
+
const extra = sessionOomRetry ? { sessionOomRetry, ...(current ? { confirmationCurrent: current } : {}) } : undefined;
|
|
15242
|
+
this.notifyInstanceTopic(instanceName, (oom ? message : t("exit.instance_notice", instanceName)) + placeSuffix({ kind: "general" }, "General"));
|
|
15026
15243
|
const web = this.webOnlyPromptPlace();
|
|
15027
15244
|
if (web) {
|
|
15028
15245
|
await this.postNonceButtonPrompt({
|
|
15029
|
-
prefix
|
|
15030
|
-
message
|
|
15031
|
-
choices
|
|
15246
|
+
prefix, alertType: "exit_restart", instanceName, ...web,
|
|
15247
|
+
message, extra,
|
|
15248
|
+
choices,
|
|
15032
15249
|
expiredText: t("exit.expired", instanceName),
|
|
15033
15250
|
});
|
|
15034
15251
|
return;
|
|
@@ -15049,37 +15266,36 @@ export class FleetManager {
|
|
|
15049
15266
|
return;
|
|
15050
15267
|
}
|
|
15051
15268
|
await this.postNonceButtonPrompt({
|
|
15052
|
-
prefix
|
|
15269
|
+
prefix,
|
|
15053
15270
|
alertType: "exit_restart",
|
|
15054
15271
|
instanceName,
|
|
15055
15272
|
adapter,
|
|
15056
15273
|
adapterId,
|
|
15057
15274
|
chatId,
|
|
15058
15275
|
threadId,
|
|
15059
|
-
message
|
|
15060
|
-
choices
|
|
15061
|
-
{ action: "restart", label: t("exit.restart") },
|
|
15062
|
-
{ action: "ignore", label: t("exit.ignore") },
|
|
15063
|
-
],
|
|
15276
|
+
message, extra,
|
|
15277
|
+
choices,
|
|
15064
15278
|
expiredText: t("exit.expired", instanceName),
|
|
15065
15279
|
});
|
|
15066
15280
|
}
|
|
15067
15281
|
/** Consume a clean-exit Restart/Ignore button exactly once. */
|
|
15068
15282
|
async handleExitRestartPrompt(data, callbackAdapterId, receivingAdapter) {
|
|
15069
|
-
const
|
|
15283
|
+
const oom = data.callbackData.startsWith(OOM_RETRY_CALLBACK_PREFIX);
|
|
15284
|
+
const claimed = this.consumeNonceCallback(oom ? OOM_RETRY_CALLBACK_PREFIX : EXIT_RESTART_CALLBACK_PREFIX, oom ? /^oom-retry:([0-9a-f]+):(resume|fresh)$/ : /^exit-restart:([0-9a-f]+):(restart|ignore)$/, data, callbackAdapterId, receivingAdapter);
|
|
15070
15285
|
if (claimed === null)
|
|
15071
15286
|
return false;
|
|
15072
15287
|
if (claimed === "consumed")
|
|
15073
15288
|
return true;
|
|
15074
15289
|
const { entry: pending, action } = claimed;
|
|
15075
|
-
this.eventLog?.insert(pending.instanceName, "normal_exit_action", { action, userId: data.userId });
|
|
15290
|
+
this.eventLog?.insert(pending.instanceName, oom ? "session_oom_action" : "normal_exit_action", { action, userId: data.userId });
|
|
15076
15291
|
if (action === "ignore") {
|
|
15077
15292
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.ignored", pending.instanceName));
|
|
15078
15293
|
return true;
|
|
15079
15294
|
}
|
|
15080
15295
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.restarting", pending.instanceName));
|
|
15081
15296
|
try {
|
|
15082
|
-
await this.restartSingleInstance(pending.instanceName
|
|
15297
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
15298
|
+
...(pending.confirmationCurrent ? { recoveryCurrent: pending.confirmationCurrent } : {}) } : {}) });
|
|
15083
15299
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("exit.restarted", pending.instanceName), pending.threadId);
|
|
15084
15300
|
}
|
|
15085
15301
|
catch (err) {
|
|
@@ -16910,6 +17126,10 @@ export class FleetManager {
|
|
|
16910
17126
|
log: (level, message, extra) => this.logger[level](extra ?? {}, message),
|
|
16911
17127
|
});
|
|
16912
17128
|
db.board.onChange(({ rev, ids, projects, wakes }) => {
|
|
17129
|
+
// Refresh both old and new owners on reassignment; wakes are not an ownership index.
|
|
17130
|
+
if (ids.length)
|
|
17131
|
+
for (const name of this.daemons.keys())
|
|
17132
|
+
this.observeWorkingState(name, "tasks");
|
|
16913
17133
|
this.emitSseEvent("tasks", { rev, ids, projects });
|
|
16914
17134
|
waker.note(wakes);
|
|
16915
17135
|
// step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
|
|
@@ -17008,7 +17228,7 @@ export class FleetManager {
|
|
|
17008
17228
|
}
|
|
17009
17229
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.restarting", pending.instanceName));
|
|
17010
17230
|
try {
|
|
17011
|
-
await this.restartSingleInstance(pending.instanceName);
|
|
17231
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true });
|
|
17012
17232
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("hang.restarted", pending.instanceName), pending.threadId);
|
|
17013
17233
|
}
|
|
17014
17234
|
catch (err) {
|
|
@@ -17780,7 +18000,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
17780
18000
|
content: fullText,
|
|
17781
18001
|
targetSession: instanceName,
|
|
17782
18002
|
meta,
|
|
17783
|
-
}, msg.humanOrigin, msg.humanOriginalBody ?? text);
|
|
18003
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? text, this.startRetryHoldFor(instanceName));
|
|
17784
18004
|
reactions.consume();
|
|
17785
18005
|
}
|
|
17786
18006
|
catch (err) {
|
|
@@ -19738,7 +19958,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19738
19958
|
// also sees the channel's messages.
|
|
19739
19959
|
if (adapterId)
|
|
19740
19960
|
this.bindInstanceAdapter(instanceName, adapterId);
|
|
19741
|
-
|
|
19961
|
+
if (this.sessionOomStopped(instanceName)) {
|
|
19962
|
+
await this.restartSingleInstance(instanceName, { explicit: true });
|
|
19963
|
+
}
|
|
19964
|
+
else {
|
|
19965
|
+
await this.startClassicInstance(instanceName, classicChannels.getBackend(channelId, adapterId, this.fleetConfig?.defaults?.backend), classicChannels.getPreTaskCommand(channelId, adapterId), classicChannels.getModel(channelId, adapterId, this.fleetConfig?.defaults?.model), classicChannels.getAutoPauseAfter(channelId, adapterId, this.fleetConfig?.defaults?.auto_pause_after));
|
|
19966
|
+
}
|
|
19742
19967
|
this.reregisterClassicChannels();
|
|
19743
19968
|
// Auto-enable collab for Discord classic channels (TG uses @mention directly without collab mode)
|
|
19744
19969
|
if (guildId && !classicChannels.isCollab(channelId, adapterId)) {
|