@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/daemon.js
CHANGED
|
@@ -10,7 +10,8 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
10
10
|
import { EventEmitter } from "node:events";
|
|
11
11
|
import { rotateLogIfNeededAsync } from "./logger.js";
|
|
12
12
|
import { coredumpFilterLaunchPrefix } from "./coredump-filter.js";
|
|
13
|
-
import { observeCliOomIdentity, confirmedCliOom, consecutiveSessionOoms } from "./session-oom.js";
|
|
13
|
+
import { observeCliOomIdentity, confirmedCliOom, consecutiveSessionOoms, describeSelfOom } from "./session-oom.js";
|
|
14
|
+
import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, SessionOomStoppedError, sessionOomStopHint } from "./session-oom-stop.js";
|
|
14
15
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
15
16
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
16
17
|
import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
@@ -58,6 +59,7 @@ import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMa
|
|
|
58
59
|
import { codexLiveRowMatches } from "./backend/codex.js";
|
|
59
60
|
import { codexPackageProblemMessage } from "./backend/codex-package.js";
|
|
60
61
|
import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
|
|
62
|
+
import { MuseBackend } from "./backend/muse.js";
|
|
61
63
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
62
64
|
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
63
65
|
/** #2140: deliveries that may carry an unwritten what's-new notice before it is recorded anyway. */
|
|
@@ -1163,6 +1165,25 @@ export class Daemon extends EventEmitter {
|
|
|
1163
1165
|
crashCount = 0;
|
|
1164
1166
|
cliOomObservation = null;
|
|
1165
1167
|
consecutiveOoms = null;
|
|
1168
|
+
collateralOomRecovery = false;
|
|
1169
|
+
unclearOomStopped = false;
|
|
1170
|
+
sessionOomStopUnpersisted = false;
|
|
1171
|
+
assertSessionOomAdmission() {
|
|
1172
|
+
if (this.sessionOomStopUnpersisted)
|
|
1173
|
+
throw new SessionOomStoppedError(false, { blocked: true, record: null, revision: null, confirmedOom: true });
|
|
1174
|
+
assertNoSessionOomStop(this.instanceDir);
|
|
1175
|
+
}
|
|
1176
|
+
isSessionOomStopped() {
|
|
1177
|
+
return this.sessionOomStopUnpersisted || readSessionOomStop(this.instanceDir).blocked;
|
|
1178
|
+
}
|
|
1179
|
+
isUnclearOomStopped() {
|
|
1180
|
+
return this.unclearOomStopped && this.healthCheckPaused && this.processStatus === "crashed";
|
|
1181
|
+
}
|
|
1182
|
+
stopUnclearOom(exitCode) {
|
|
1183
|
+
this.unclearOomStopped = true;
|
|
1184
|
+
this.healthCheckPaused = true;
|
|
1185
|
+
this.emitSupervisionEnded(t("inst.oom_unclear_stopped"), t("inst.session_oom_remedy"), exitCode, "oom_unclear");
|
|
1186
|
+
}
|
|
1166
1187
|
oomSessionKey() {
|
|
1167
1188
|
// Claude's per-instance statusline owns its ID. Other current getters
|
|
1168
1189
|
// either return null or select by cwd (Muse/Grok): those are not proof
|
|
@@ -1810,6 +1831,12 @@ export class Daemon extends EventEmitter {
|
|
|
1810
1831
|
model: config.model ?? "default",
|
|
1811
1832
|
};
|
|
1812
1833
|
this.logger = rootLogger.child({ instance: name }, { level: config.log_level });
|
|
1834
|
+
if (backend instanceof MuseBackend) {
|
|
1835
|
+
const log = this.logger;
|
|
1836
|
+
backend.setPlaceholderDebug((rec) => {
|
|
1837
|
+
log.debug(rec, "muse empty-composer placeholder classified as empty");
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1813
1840
|
this.tmuxSessionName = getTmuxSession();
|
|
1814
1841
|
this.messageBus = new MessageBus();
|
|
1815
1842
|
this.messageBus.setLogger(this.logger);
|
|
@@ -1822,7 +1849,10 @@ export class Daemon extends EventEmitter {
|
|
|
1822
1849
|
// finished turn can never resume after a later restart. reset() is
|
|
1823
1850
|
// deliberately NOT hooked: stop/spawn abandon the in-memory turn while
|
|
1824
1851
|
// the on-disk interruption must survive for the resume gate.
|
|
1825
|
-
this.turnReplyGuard.onComplete = () =>
|
|
1852
|
+
this.turnReplyGuard.onComplete = () => {
|
|
1853
|
+
clearInFlightTurnMarker(this.instanceDir);
|
|
1854
|
+
this.publishWorkingState("turn_closed");
|
|
1855
|
+
};
|
|
1826
1856
|
}
|
|
1827
1857
|
/** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
|
|
1828
1858
|
setDeliveryOutboxPort(port) {
|
|
@@ -2117,6 +2147,11 @@ export class Daemon extends EventEmitter {
|
|
|
2117
2147
|
* daemon's own state only; it never inspects the pane itself.
|
|
2118
2148
|
*/
|
|
2119
2149
|
notAcceptingReason() {
|
|
2150
|
+
if (this.sessionOomStopUnpersisted)
|
|
2151
|
+
return t("inst.session_oom_hold");
|
|
2152
|
+
const oomStop = readSessionOomStop(this.instanceDir);
|
|
2153
|
+
if (oomStop.blocked)
|
|
2154
|
+
return sessionOomStopHint(oomStop);
|
|
2120
2155
|
if (this.pauseWakeState !== "active")
|
|
2121
2156
|
return t("delivery.hold_state", this.pauseWakeState);
|
|
2122
2157
|
if (this.inputBlockedDialogKey !== null)
|
|
@@ -2167,6 +2202,7 @@ export class Daemon extends EventEmitter {
|
|
|
2167
2202
|
}
|
|
2168
2203
|
}
|
|
2169
2204
|
async startAdmitted() {
|
|
2205
|
+
this.assertSessionOomAdmission();
|
|
2170
2206
|
ensureInstanceDir(this.instanceDir);
|
|
2171
2207
|
writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
|
|
2172
2208
|
this.logger.info(`Starting ${this.name}`);
|
|
@@ -3094,19 +3130,36 @@ export class Daemon extends EventEmitter {
|
|
|
3094
3130
|
return;
|
|
3095
3131
|
this.cliOomObservation = null;
|
|
3096
3132
|
const decisionKey = this.oomSessionKey();
|
|
3097
|
-
this.consecutiveOoms = consecutiveSessionOoms(this.consecutiveOoms, decisionKey, decisionKey === key && oom
|
|
3133
|
+
this.consecutiveOoms = consecutiveSessionOoms(this.consecutiveOoms, decisionKey, decisionKey === key && oom?.attribution === "self");
|
|
3134
|
+
const collateralOom = oom?.attribution === "collateral";
|
|
3135
|
+
if (collateralOom)
|
|
3136
|
+
this.logger.warn({ attribution: oom.attribution, pid: oom.pid }, "OOM victim without self-caused proof — preserving session; waiting for memory recovery");
|
|
3098
3137
|
if (this.consecutiveOoms && this.consecutiveOoms.count >= 2) {
|
|
3099
3138
|
// Stop supervision, not an idle pause: queued work must not auto-wake
|
|
3100
3139
|
// the same session. An explicit restart is the person's retry choice.
|
|
3140
|
+
// No await after the final captured health owner/session check. The
|
|
3141
|
+
// stop is authoritative before observers can see supervision end.
|
|
3142
|
+
try {
|
|
3143
|
+
writeSessionOomStop(this.instanceDir, { owner: healthOwner, sessionKey: decisionKey,
|
|
3144
|
+
configuration: this.getWorkingStateObservation().configuration, stoppedAt: Date.now(),
|
|
3145
|
+
count: this.consecutiveOoms.count, victim: { pid: oom.pid, at: oom.at }, evidence: oom.evidence });
|
|
3146
|
+
}
|
|
3147
|
+
catch {
|
|
3148
|
+
// Storage failure cannot revive this CLI in the current process.
|
|
3149
|
+
// Say explicitly that a fleet restart cannot yet rely on durability.
|
|
3150
|
+
this.sessionOomStopUnpersisted = true;
|
|
3151
|
+
this.logger.error("Could not persist session OOM stop; fix storage before restarting the fleet");
|
|
3152
|
+
}
|
|
3101
3153
|
this.healthCheckPaused = true;
|
|
3102
3154
|
try {
|
|
3103
|
-
this.appendCrashHistory({ exitCode, lastOutput, crashType, reason: "session_oom" });
|
|
3155
|
+
this.appendCrashHistory({ exitCode, lastOutput, crashType, reason: "session_oom", oomEvidence: oom.evidence });
|
|
3104
3156
|
}
|
|
3105
3157
|
catch (error) {
|
|
3106
3158
|
this.logger.warn({ error }, "Could not append session OOM history");
|
|
3107
3159
|
}
|
|
3108
3160
|
this.logger.error({ pid: oom.pid, count: this.consecutiveOoms.count }, "Repeated confirmed CLI OOM — automatic resume stopped; session preserved");
|
|
3109
|
-
this.emitSupervisionEnded(t("inst.session_oom_stopped")
|
|
3161
|
+
this.emitSupervisionEnded(`${t("inst.session_oom_stopped")} (${describeSelfOom(oom.evidence)})`, t("inst.session_oom_remedy")
|
|
3162
|
+
+ (this.sessionOomStopUnpersisted ? ` ${t("inst.session_oom_save_failed")}` : ""), exitCode, "session_oom");
|
|
3110
3163
|
return;
|
|
3111
3164
|
}
|
|
3112
3165
|
// Kill the dead window (remain-on-exit keeps it around) before respawn
|
|
@@ -3116,7 +3169,7 @@ export class Daemon extends EventEmitter {
|
|
|
3116
3169
|
return;
|
|
3117
3170
|
}
|
|
3118
3171
|
// Detect claude-code background session conflict — recover without counting as crash
|
|
3119
|
-
if (lastOutput && (lastOutput.includes("background agent") || lastOutput.includes("Session is currently running"))) {
|
|
3172
|
+
if (!oom && lastOutput && (lastOutput.includes("background agent") || lastOutput.includes("Session is currently running"))) {
|
|
3120
3173
|
if (!this.backgroundSessionRecoveryAttempted) {
|
|
3121
3174
|
this.backgroundSessionRecoveryAttempted = true;
|
|
3122
3175
|
this.logger.warn("Detected lingering background agent session — starting fresh (no resume)");
|
|
@@ -3156,7 +3209,7 @@ export class Daemon extends EventEmitter {
|
|
|
3156
3209
|
// --continue again and crash in the same way → loop. Clear the session id
|
|
3157
3210
|
// and skip resume so the next spawn starts fresh. (skipResume also stops
|
|
3158
3211
|
// saveSessionId below from resurrecting the id from statusline.json.)
|
|
3159
|
-
if (this.paneSaysNoConversation(lastOutput)) {
|
|
3212
|
+
if (!oom && this.paneSaysNoConversation(lastOutput)) {
|
|
3160
3213
|
this.setSessionAside("cli_reported_no_conversation");
|
|
3161
3214
|
this.skipResume = true;
|
|
3162
3215
|
}
|
|
@@ -3177,12 +3230,21 @@ export class Daemon extends EventEmitter {
|
|
|
3177
3230
|
return;
|
|
3178
3231
|
}
|
|
3179
3232
|
// Detect rapid crash: sliding window — 3+ crashes in 5 minutes
|
|
3180
|
-
|
|
3233
|
+
if (!collateralOom)
|
|
3234
|
+
this.crashTimestamps.push(Date.now());
|
|
3235
|
+
else
|
|
3236
|
+
this.crashTimestamps = [];
|
|
3181
3237
|
const crashWindowMs = 5 * 60_000;
|
|
3182
3238
|
this.crashTimestamps = this.crashTimestamps.filter(t => t > Date.now() - crashWindowMs);
|
|
3183
3239
|
if (this.crashTimestamps.length >= 3) {
|
|
3184
3240
|
this.healthCheckPaused = true;
|
|
3185
3241
|
this.logger.error({ crashesInWindow: this.crashTimestamps.length, lastExitCode: exitCode ?? null, lastOutput: paneTail(lastOutput) ?? null }, "3+ crashes in 5 minutes — pausing respawn");
|
|
3242
|
+
// Unknown cause is bounded like an ordinary crash, never a
|
|
3243
|
+
// self-OOM verdict. Preserve the session for the person's choice.
|
|
3244
|
+
if (oom?.attribution === "unknown") {
|
|
3245
|
+
this.stopUnclearOom(exitCode);
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3186
3248
|
// P1: Persist crash state so next process restart skips resume
|
|
3187
3249
|
try {
|
|
3188
3250
|
writeFileSync(join(this.instanceDir, "crash-state.json"), JSON.stringify({
|
|
@@ -3199,18 +3261,23 @@ export class Daemon extends EventEmitter {
|
|
|
3199
3261
|
if (reset_after > 0 && Date.now() - this.lastCrashAt > reset_after) {
|
|
3200
3262
|
this.crashCount = 0;
|
|
3201
3263
|
}
|
|
3202
|
-
|
|
3264
|
+
if (!collateralOom)
|
|
3265
|
+
this.crashCount++;
|
|
3203
3266
|
this.lastCrashAt = Date.now();
|
|
3204
|
-
if (this.crashCount > max_retries) {
|
|
3267
|
+
if (!collateralOom && this.crashCount > max_retries) {
|
|
3205
3268
|
this.logger.error({ crashCount: this.crashCount, maxRetries: max_retries, lastExitCode: exitCode ?? null, lastOutput: paneTail(lastOutput) ?? null }, "Max crash retries exceeded — not respawning");
|
|
3206
3269
|
this.healthCheckPaused = true;
|
|
3270
|
+
if (oom?.attribution === "unknown") {
|
|
3271
|
+
this.stopUnclearOom(exitCode);
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3207
3274
|
this.emitSupervisionEnded(`it crashed ${this.crashCount} times, exceeding restart_policy.max_retries (${max_retries})`, "Check the logs for the cause, then restart it.");
|
|
3208
3275
|
return; // don't schedule next — given up
|
|
3209
3276
|
}
|
|
3210
3277
|
// Calculate backoff delay
|
|
3211
3278
|
const delay = backoff === "exponential"
|
|
3212
|
-
? Math.min(1000 * Math.pow(2, this.crashCount - 1), 60_000)
|
|
3213
|
-
: 1000 * this.crashCount;
|
|
3279
|
+
? Math.min(1000 * Math.pow(2, Math.max(1, this.crashCount) - 1), 60_000)
|
|
3280
|
+
: 1000 * Math.max(1, this.crashCount);
|
|
3214
3281
|
this.logger.warn({ crashCount: this.crashCount, delay }, `${cliLabel} window died — respawning after backoff`);
|
|
3215
3282
|
await new Promise(r => setTimeout(r, delay));
|
|
3216
3283
|
try {
|
|
@@ -3255,7 +3322,14 @@ export class Daemon extends EventEmitter {
|
|
|
3255
3322
|
// Write snapshot before spawn — consumed only if resume fails
|
|
3256
3323
|
this.writeRotationSnapshot("crash");
|
|
3257
3324
|
// Try --resume first; spawnClaudeWindow falls back to fresh session if resume fails
|
|
3258
|
-
|
|
3325
|
+
this.collateralOomRecovery = collateralOom;
|
|
3326
|
+
let resumed;
|
|
3327
|
+
try {
|
|
3328
|
+
resumed = await this.spawnClaudeWindow();
|
|
3329
|
+
}
|
|
3330
|
+
finally {
|
|
3331
|
+
this.collateralOomRecovery = false;
|
|
3332
|
+
}
|
|
3259
3333
|
if (!resumed) {
|
|
3260
3334
|
// Resume failed → fresh session → inject snapshot for context
|
|
3261
3335
|
await this.injectSnapshotMessage();
|
|
@@ -3349,6 +3423,7 @@ export class Daemon extends EventEmitter {
|
|
|
3349
3423
|
* four, carrying a human-readable cause and the operator's next step.
|
|
3350
3424
|
*/
|
|
3351
3425
|
emitSupervisionEnded(reason, remedy, exitCode, cause) {
|
|
3426
|
+
this.publishWorkingState("supervision_ended");
|
|
3352
3427
|
this.emit("supervision_ended", {
|
|
3353
3428
|
name: this.name,
|
|
3354
3429
|
reason,
|
|
@@ -3361,6 +3436,7 @@ export class Daemon extends EventEmitter {
|
|
|
3361
3436
|
if (this.processStatus === status)
|
|
3362
3437
|
return;
|
|
3363
3438
|
this.processStatus = status;
|
|
3439
|
+
this.publishWorkingState("process");
|
|
3364
3440
|
if (status === "running") {
|
|
3365
3441
|
// A successful crash respawn is a new pane generation. It is ready by
|
|
3366
3442
|
// this point, so an error gate inherited from the dead pane must not keep
|
|
@@ -3909,6 +3985,7 @@ export class Daemon extends EventEmitter {
|
|
|
3909
3985
|
this.deliveryEpoch = fleetEpoch === undefined
|
|
3910
3986
|
? this.deliveryEpoch + 1
|
|
3911
3987
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
3988
|
+
this.publishWorkingState("cancelled");
|
|
3912
3989
|
this.logger.info({ deliveryEpoch: this.deliveryEpoch }, "Pending delivery queue cleared by user cancel");
|
|
3913
3990
|
}
|
|
3914
3991
|
/** Board 907c7899: the layout a web /compact's end can be read from on this pane (kiro classic or TUI V2), or null. */
|
|
@@ -4477,6 +4554,7 @@ export class Daemon extends EventEmitter {
|
|
|
4477
4554
|
return true;
|
|
4478
4555
|
}
|
|
4479
4556
|
async stop() {
|
|
4557
|
+
this.publishWorkingState("before_stop");
|
|
4480
4558
|
this.logger.info("Stopping daemon instance");
|
|
4481
4559
|
this.clearQuitRelaunchWatch();
|
|
4482
4560
|
this.fenceDeliveryWritesForStop();
|
|
@@ -4625,6 +4703,31 @@ export class Daemon extends EventEmitter {
|
|
|
4625
4703
|
getInteractionSnapshot() {
|
|
4626
4704
|
return this.interactionObservation.snapshot(this.interactionOwner());
|
|
4627
4705
|
}
|
|
4706
|
+
workingTurnDelivery = null;
|
|
4707
|
+
workingBackgroundObservation = null;
|
|
4708
|
+
/** Pure metadata read: never discovers sessions, captures a pane or reads backend files. */
|
|
4709
|
+
getWorkingStateObservation() {
|
|
4710
|
+
const turn = this.turnReplyGuard.snapshot();
|
|
4711
|
+
const backend = this.config.backend ?? this.backend?.binaryName ?? "unknown";
|
|
4712
|
+
const profile = resolveCredentialProfile(this.config.backend_options?.[backend]);
|
|
4713
|
+
return {
|
|
4714
|
+
owner: this.interactionOwner(), backend,
|
|
4715
|
+
configuration: createHash("sha256").update(JSON.stringify([backend, this.config.working_directory, profile])).digest("hex"),
|
|
4716
|
+
executionState: this.processStatus === "running" ? this.isPaused ? "paused" : this.instanceState : null,
|
|
4717
|
+
processStatus: this.getProcessStatus(), sessionId: null, backendTurnId: null,
|
|
4718
|
+
backgroundWork: this.workingBackgroundObservation,
|
|
4719
|
+
turn: turn ? { generation: turn.generation,
|
|
4720
|
+
deliveryId: this.workingTurnDelivery?.generation === turn.generation ? this.workingTurnDelivery.deliveryId : null,
|
|
4721
|
+
correlationId: turn.target.correlationId ?? null, messageId: turn.target.messageId ?? null,
|
|
4722
|
+
cancelled: turn.cancelledByUser, completionDelivered: turn.completionDelivered } : null,
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
publishWorkingState(reason) {
|
|
4726
|
+
try {
|
|
4727
|
+
this.emit("working_state", { reason, observation: this.getWorkingStateObservation() });
|
|
4728
|
+
}
|
|
4729
|
+
catch { /* Journal observers cannot change daemon behavior. */ }
|
|
4730
|
+
}
|
|
4628
4731
|
/** Key of the last interaction snapshot actually emitted + broadcast (#1219). */
|
|
4629
4732
|
lastPublishedInteractionKey = null;
|
|
4630
4733
|
publishInteraction() {
|
|
@@ -4877,6 +4980,7 @@ export class Daemon extends EventEmitter {
|
|
|
4877
4980
|
this.pauseWakeState = "paused";
|
|
4878
4981
|
this.autoPauseController.markPaused();
|
|
4879
4982
|
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
|
|
4983
|
+
this.publishWorkingState("execution");
|
|
4880
4984
|
this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
|
|
4881
4985
|
this.ipcServer?.broadcast({
|
|
4882
4986
|
type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
|
|
@@ -4902,6 +5006,7 @@ export class Daemon extends EventEmitter {
|
|
|
4902
5006
|
}
|
|
4903
5007
|
/** Respawn the CLI in the preserved window and block until its prompt is ready. */
|
|
4904
5008
|
async wake(timeoutMs = 30_000) {
|
|
5009
|
+
this.assertSessionOomAdmission();
|
|
4905
5010
|
// An explicit wake (e.g. after the user re-logs in) cancels a deferred
|
|
4906
5011
|
// auth pause — otherwise the instance would pause again the moment it idles.
|
|
4907
5012
|
this.pausePending = false;
|
|
@@ -4969,6 +5074,7 @@ export class Daemon extends EventEmitter {
|
|
|
4969
5074
|
this.clearErrorRecoveryGate();
|
|
4970
5075
|
clearPausedMarker(this.instanceDir);
|
|
4971
5076
|
this.resumeRuntimeMonitors();
|
|
5077
|
+
this.publishWorkingState("execution");
|
|
4972
5078
|
this.logger.info("Instance auto-woke");
|
|
4973
5079
|
this.ipcServer?.broadcast({
|
|
4974
5080
|
type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
|
|
@@ -5070,6 +5176,17 @@ export class Daemon extends EventEmitter {
|
|
|
5070
5176
|
}
|
|
5071
5177
|
this.instanceState = snapshot.state;
|
|
5072
5178
|
this.acceptedInstanceSnapshot = { ...snapshot };
|
|
5179
|
+
// Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
|
|
5180
|
+
if (typeof pane === "string" && this.backend?.backgroundWorkRunning) {
|
|
5181
|
+
try {
|
|
5182
|
+
const background = this.backend.backgroundWorkRunning(pane);
|
|
5183
|
+
if (background !== this.workingBackgroundObservation) {
|
|
5184
|
+
this.workingBackgroundObservation = background;
|
|
5185
|
+
this.publishWorkingState("background");
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
catch { /* Observational parsing cannot change accepted execution state. */ }
|
|
5189
|
+
}
|
|
5073
5190
|
// OpenCode creates its session lazily on the first submitted message.
|
|
5074
5191
|
// Waiting until stop/pause to persist that id loses resume state when the
|
|
5075
5192
|
// fleet is SIGKILLed or the host reboots.
|
|
@@ -5114,6 +5231,7 @@ export class Daemon extends EventEmitter {
|
|
|
5114
5231
|
}
|
|
5115
5232
|
if (snapshot.state !== previous) {
|
|
5116
5233
|
this.logStateEdge(previous, snapshot);
|
|
5234
|
+
this.publishWorkingState("execution");
|
|
5117
5235
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
5118
5236
|
this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
5119
5237
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
@@ -5196,6 +5314,8 @@ export class Daemon extends EventEmitter {
|
|
|
5196
5314
|
deliveredText,
|
|
5197
5315
|
deliveredAt,
|
|
5198
5316
|
}, this.pendingWork.lastInboundTimestamp());
|
|
5317
|
+
this.workingTurnDelivery = { generation: this.turnReplyGuard.snapshot().generation, deliveryId: meta.delivery_id || null };
|
|
5318
|
+
this.publishWorkingState("turn_started");
|
|
5199
5319
|
// #1209: persist the in-flight turn with a seam checkpoint. A marker
|
|
5200
5320
|
// that survives a restart is an interrupted turn by construction —
|
|
5201
5321
|
// completion (via the guard's onComplete) and cancel paths delete it.
|
|
@@ -10095,8 +10215,10 @@ export class Daemon extends EventEmitter {
|
|
|
10095
10215
|
// purposes even before the idle edge completes the guard. Without
|
|
10096
10216
|
// this, a restart in the reply-to-idle window would re-inject a
|
|
10097
10217
|
// continuation for an already-answered turn.
|
|
10098
|
-
if (replyAttempt?.completionAction)
|
|
10218
|
+
if (replyAttempt?.completionAction) {
|
|
10099
10219
|
clearInFlightTurnMarker(this.instanceDir);
|
|
10220
|
+
this.publishWorkingState("completion_delivered");
|
|
10221
|
+
}
|
|
10100
10222
|
// #1377: a confirmed completion action (reply/react/edit_message) ends
|
|
10101
10223
|
// the human-facing obligation immediately — the idle edge does not need
|
|
10102
10224
|
// to confirm it. Clear any pending confirmation window so a later
|
|
@@ -10759,6 +10881,9 @@ export class Daemon extends EventEmitter {
|
|
|
10759
10881
|
* into exactly the window this exists to close.
|
|
10760
10882
|
*/
|
|
10761
10883
|
beginSpawn() {
|
|
10884
|
+
this.unclearOomStopped = false;
|
|
10885
|
+
this.publishWorkingState("before_spawn");
|
|
10886
|
+
this.workingBackgroundObservation = null;
|
|
10762
10887
|
this.dialogAnswers = null;
|
|
10763
10888
|
this.capacityNudge = null;
|
|
10764
10889
|
this.lastErrorEpisode = null;
|
|
@@ -10974,6 +11099,7 @@ export class Daemon extends EventEmitter {
|
|
|
10974
11099
|
catch { /* best effort */ }
|
|
10975
11100
|
}
|
|
10976
11101
|
async spawnClaudeWindow() {
|
|
11102
|
+
this.assertSessionOomAdmission();
|
|
10977
11103
|
this.beginSpawn();
|
|
10978
11104
|
let resumedSuccessfully = false;
|
|
10979
11105
|
try {
|
|
@@ -11364,11 +11490,20 @@ export class Daemon extends EventEmitter {
|
|
|
11364
11490
|
* Returns true if CLI is ready, false if it failed or got stuck.
|
|
11365
11491
|
*/
|
|
11366
11492
|
async trySpawn(reuseWindow = false, startupTimeoutMs) {
|
|
11367
|
-
const
|
|
11493
|
+
const recoveryOwner = this.collateralOomRecovery ? this.interactionOwner() : null;
|
|
11494
|
+
const enter = () => {
|
|
11495
|
+
if (recoveryOwner && (!sameInteractionOwner(recoveryOwner, this.interactionOwner())
|
|
11496
|
+
|| this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
|
|
11497
|
+
throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
|
|
11498
|
+
}
|
|
11499
|
+
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs);
|
|
11500
|
+
};
|
|
11501
|
+
const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
|
|
11368
11502
|
instanceName: this.name,
|
|
11369
11503
|
workingDirectory: this.config.working_directory,
|
|
11370
11504
|
reason: reuseWindow ? "wake" : this.lastSpawnAt > 0 ? "recovery" : "startup",
|
|
11371
|
-
|
|
11505
|
+
requireNormalMemory: this.collateralOomRecovery,
|
|
11506
|
+
}, enter);
|
|
11372
11507
|
// #906: a resumed kiro conversation comes back as the agent it was saved under. Every launch path that reaches a
|
|
11373
11508
|
// ready CLI (start, recovery, wake) switches it to this instance's own agent here, while the spawn still holds
|
|
11374
11509
|
// deliveries — and outside the spawn gate, which it does not need.
|
|
@@ -11650,6 +11785,7 @@ export class Daemon extends EventEmitter {
|
|
|
11650
11785
|
}
|
|
11651
11786
|
}
|
|
11652
11787
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
11788
|
+
this.assertSessionOomAdmission();
|
|
11653
11789
|
this.startupAdmission?.();
|
|
11654
11790
|
const launchFence = this.launchFenceEpoch;
|
|
11655
11791
|
const backendConfig = this.buildBackendConfig();
|
|
@@ -11698,6 +11834,7 @@ export class Daemon extends EventEmitter {
|
|
|
11698
11834
|
// Thrown, not `false`: a false verdict reads as "the CLI failed to start" and sends the startup path
|
|
11699
11835
|
// into its resume-retry / set-the-session-aside handling, for an instance somebody just stopped.
|
|
11700
11836
|
this.startupAdmission?.();
|
|
11837
|
+
this.assertSessionOomAdmission();
|
|
11701
11838
|
if (launchFence !== this.launchFenceEpoch)
|
|
11702
11839
|
throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
|
|
11703
11840
|
this.backend.writeConfig(backendConfig);
|
|
@@ -11784,6 +11921,7 @@ export class Daemon extends EventEmitter {
|
|
|
11784
11921
|
}
|
|
11785
11922
|
this.startupAdmission?.();
|
|
11786
11923
|
let windowId;
|
|
11924
|
+
this.assertSessionOomAdmission();
|
|
11787
11925
|
if (reuseWindow) {
|
|
11788
11926
|
this.controlClient?.unregisterWindow(this.tmux.getWindowId());
|
|
11789
11927
|
await this.tmux.respawnWindow(cmd, resolvedCwd);
|
|
@@ -12418,6 +12556,7 @@ export class Daemon extends EventEmitter {
|
|
|
12418
12556
|
crashType: data.crashType,
|
|
12419
12557
|
exitCode: data.exitCode,
|
|
12420
12558
|
reason: data.reason,
|
|
12559
|
+
...(data.oomEvidence ? { oomEvidence: data.oomEvidence } : {}),
|
|
12421
12560
|
lastOutput: data.lastOutput,
|
|
12422
12561
|
crashCount: this.crashCount + 1,
|
|
12423
12562
|
crashesInWindow: this.crashTimestamps.length,
|