@songsid/agend 2.3.4-beta.5 → 2.3.4-beta.7
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/agend-home.d.ts +13 -0
- package/dist/agend-home.js +42 -0
- package/dist/agend-home.js.map +1 -1
- package/dist/auth-api.js +2 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +88 -3
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +13 -6
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.js +18 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend-availability.d.ts +6 -0
- package/dist/backend-availability.js +13 -5
- package/dist/backend-availability.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +48 -8
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +30 -7
- package/dist/fleet-manager.js +182 -66
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-patch.d.ts +3 -0
- package/dist/fleet-yaml-patch.js +103 -0
- package/dist/fleet-yaml-patch.js.map +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
- package/dist/instance-lifecycle.js +12 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-resume-summary.d.ts +37 -0
- package/dist/kiro-resume-summary.js +85 -0
- package/dist/kiro-resume-summary.js.map +1 -0
- package/dist/locale.js +8 -0
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.js +9 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/reply-buttons-controller.js +10 -0
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/stall-reporter.d.ts +21 -0
- package/dist/stall-reporter.js +82 -0
- package/dist/stall-reporter.js.map +1 -0
- package/dist/tmux-control.d.ts +2 -2
- package/dist/tmux-control.js +4 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +6 -3
- package/dist/tmux-exec-helper.js +115 -53
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +4 -0
- package/dist/tunnel/cloudflared-install.js +20 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +8 -1
- package/dist/tunnel/cloudflared.js +34 -6
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.d.ts +3 -3
- package/dist/tunnel/lease.js +23 -19
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js +6 -7
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/ui/shared/app.css +16 -1
- package/dist/ui/shared/panel-view.js +266 -17
- package/dist/ui/shared/view-strings.js +26 -0
- package/dist/unattended-install.d.ts +1 -1
- package/dist/unattended-install.js +19 -5
- package/dist/unattended-install.js.map +1 -1
- package/dist/view-api.d.ts +15 -0
- package/dist/view-api.js +94 -33
- package/dist/view-api.js.map +1 -1
- package/dist/web-login.d.ts +4 -3
- package/dist/web-login.js +23 -14
- package/dist/web-login.js.map +1 -1
- package/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +20 -8
- package/dist/web-session.js.map +1 -1
- package/launcher/agend-install.cjs +16 -2
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -167,6 +167,11 @@ export interface RestartOpts {
|
|
|
167
167
|
sessionOomRetry?: SessionOomRetry;
|
|
168
168
|
/** Internal pre-invalidation owner fence for an unclear-OOM action. */
|
|
169
169
|
recoveryCurrent?: () => boolean;
|
|
170
|
+
/**
|
|
171
|
+
* #1490 row 27: the lifecycle the caller acts for (lifecyclePin), asked before anything is invalidated. A button or
|
|
172
|
+
* menu offered for one instance never restarts a later one of the same name, or one restarted since.
|
|
173
|
+
*/
|
|
174
|
+
ownerCurrent?: () => boolean;
|
|
170
175
|
carry?: {
|
|
171
176
|
backend: string;
|
|
172
177
|
from: string | null;
|
|
@@ -271,6 +276,11 @@ export type GuestAuditEntry = {
|
|
|
271
276
|
name: string;
|
|
272
277
|
outcome: string;
|
|
273
278
|
};
|
|
279
|
+
/** #1490 row 27: a restart asked for one lifecycle of an instance after it was stopped, restarted or replaced. */
|
|
280
|
+
export declare class RestartSupersededError extends Error {
|
|
281
|
+
/** `shown`: the instance as the chat list names it (shownInstanceName). */
|
|
282
|
+
constructor(shown: string);
|
|
283
|
+
}
|
|
274
284
|
export declare class FleetManager implements FleetContext, LifecycleContext, ArchiverContext, StatuslineWatcherContext, OutboundContext, AgentEndpointContext {
|
|
275
285
|
dataDir: string;
|
|
276
286
|
private static signalTarget;
|
|
@@ -578,6 +588,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
578
588
|
private watchdogTimer;
|
|
579
589
|
private startupMemoryDeadline;
|
|
580
590
|
private eventLoopWatch;
|
|
591
|
+
private stallReporter;
|
|
581
592
|
private startedAt;
|
|
582
593
|
private stormOpenNotifyTimer;
|
|
583
594
|
private memoryLogAt;
|
|
@@ -618,6 +629,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
618
629
|
private readonly webChatRemovals;
|
|
619
630
|
/** #1774: a removal under way — its replies' fate waits for its outcome (success: theirs no more; failure: theirs). */
|
|
620
631
|
private readonly webChatRemovalsPending;
|
|
632
|
+
/**
|
|
633
|
+
* #1490 row 27: "still the same lifecycle of `name`" — its lifecycle epoch, moved on synchronously by every stop,
|
|
634
|
+
* restart and removal (the map is never pruned, so a same-name replacement has a later one). Taken when a button
|
|
635
|
+
* or menu is claimed or offered, asked after every await before acting by name.
|
|
636
|
+
*/
|
|
637
|
+
/** #1490 row 27: an instance as the chat list names it — its display name where it has one, else its name. */
|
|
638
|
+
private shownInstanceName;
|
|
639
|
+
private lifecyclePin;
|
|
621
640
|
private webChatGeneration;
|
|
622
641
|
/** #1774: who an instance's reply is from, taken when it is sent: the name's generation and its daemon then. */
|
|
623
642
|
private webChatIdentity;
|
|
@@ -3668,12 +3687,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
3668
3687
|
*/
|
|
3669
3688
|
private locateBinaryOnLoginShell;
|
|
3670
3689
|
/**
|
|
3671
|
-
* #1092: the binary in one of the directories its own installer installs to,
|
|
3672
|
-
*
|
|
3673
|
-
*
|
|
3674
|
-
*
|
|
3690
|
+
* #1092: the binary in one of the directories its own installer installs to, when no login shell can see it (the
|
|
3691
|
+
* installer recorded its PATH in an interactive-only rc file) — only those directories (#1092). #3009: through
|
|
3692
|
+
* backend-availability's install-directory check (the one resolveBackendBinary uses): one killable child, never
|
|
3693
|
+
* accessSync/statSync on the fleet's thread. An executable file counts, a symlink to one too (codex's launcher is one).
|
|
3675
3694
|
*/
|
|
3676
|
-
private
|
|
3695
|
+
private locateInstalledBackend;
|
|
3677
3696
|
/** Put an installed binary's directory on the fleet's PATH (#1059), once. */
|
|
3678
3697
|
private adoptBinaryDirectory;
|
|
3679
3698
|
/** Discord native `/login` slash — shared by every adapter dispatch block. */
|
|
@@ -4006,8 +4025,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
4006
4025
|
* restart backends only read it at launch, so it is persisted and the
|
|
4007
4026
|
* instance respawns.
|
|
4008
4027
|
*/
|
|
4009
|
-
applyEffort(instanceName: string, requested: string
|
|
4010
|
-
|
|
4028
|
+
applyEffort(instanceName: string, requested: string, opts?: {
|
|
4029
|
+
current?: () => boolean;
|
|
4030
|
+
}): Promise<string>;
|
|
4031
|
+
applyModel(instanceName: string, model: string, opts?: {
|
|
4032
|
+
current?: () => boolean;
|
|
4033
|
+
}): Promise<string>;
|
|
4011
4034
|
/** Save an instance's model (fleet.yaml, or its ClassicBot channel). False when neither holds it. */
|
|
4012
4035
|
private persistInstanceModel;
|
|
4013
4036
|
/**
|
package/dist/fleet-manager.js
CHANGED
|
@@ -27,7 +27,7 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
|
|
|
27
27
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
28
28
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
29
29
|
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
30
|
-
import { unattendedInstallCommand } from "./unattended-install.js";
|
|
30
|
+
import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
|
|
31
31
|
import { InstallStream } from "./install-stream.js";
|
|
32
32
|
import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
|
|
33
33
|
import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
|
|
@@ -39,7 +39,7 @@ import { withinBudget } from "./monotonic-budget.js";
|
|
|
39
39
|
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
40
40
|
import { RuntimeCpuProfiler, ProfileBusyError, profileDuration } from "./runtime-cpu-profile.js";
|
|
41
41
|
import { ProfileControlServer } from "./profile-control.js";
|
|
42
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync,
|
|
42
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
|
|
43
43
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
44
44
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
45
45
|
import { freemem, totalmem, homedir } from "node:os";
|
|
@@ -56,6 +56,7 @@ import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./ki
|
|
|
56
56
|
import { fleetLabel } from "./fleet-label.js";
|
|
57
57
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
58
58
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
59
|
+
import { startStallReporter } from "./stall-reporter.js";
|
|
59
60
|
import { sdNotify, sdNotifyBlocking, setSdNotifyWarn, startSdWatchdog } from "./sd-notify.js";
|
|
60
61
|
import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
|
|
61
62
|
import { readFleetMemory } from "./process-memory.js";
|
|
@@ -74,7 +75,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
|
74
75
|
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
75
76
|
import { EventLog } from "./event-log.js";
|
|
76
77
|
import { binaryProbe } from "./binary-probe.js";
|
|
77
|
-
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
|
|
78
|
+
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
|
|
78
79
|
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
79
80
|
import { OutboxOpenError } from "./outbox-open-error.js";
|
|
80
81
|
import { AdapterWorld } from "./adapter-world.js";
|
|
@@ -627,6 +628,14 @@ function emojiListFilter(opts) {
|
|
|
627
628
|
}
|
|
628
629
|
/** #1920: a guest's scope until phase 2 derives one: nothing. */
|
|
629
630
|
const NO_GUEST_SCOPE = new Set();
|
|
631
|
+
/** #1490 row 27: a restart asked for one lifecycle of an instance after it was stopped, restarted or replaced. */
|
|
632
|
+
export class RestartSupersededError extends Error {
|
|
633
|
+
/** `shown`: the instance as the chat list names it (shownInstanceName). */
|
|
634
|
+
constructor(shown) {
|
|
635
|
+
super(t("restart.superseded", shown));
|
|
636
|
+
this.name = "RestartSupersededError";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
630
639
|
export class FleetManager {
|
|
631
640
|
dataDir;
|
|
632
641
|
static signalTarget = null;
|
|
@@ -1026,6 +1035,7 @@ export class FleetManager {
|
|
|
1026
1035
|
watchdogTimer = null;
|
|
1027
1036
|
startupMemoryDeadline = null;
|
|
1028
1037
|
eventLoopWatch = null;
|
|
1038
|
+
stallReporter = null;
|
|
1029
1039
|
startedAt = 0;
|
|
1030
1040
|
stormOpenNotifyTimer = null;
|
|
1031
1041
|
memoryLogAt = null;
|
|
@@ -1069,6 +1079,37 @@ export class FleetManager {
|
|
|
1069
1079
|
webChatRemovals = new Map();
|
|
1070
1080
|
/** #1774: a removal under way — its replies' fate waits for its outcome (success: theirs no more; failure: theirs). */
|
|
1071
1081
|
webChatRemovalsPending = new Map();
|
|
1082
|
+
/**
|
|
1083
|
+
* #1490 row 27: "still the same lifecycle of `name`" — its lifecycle epoch, moved on synchronously by every stop,
|
|
1084
|
+
* restart and removal (the map is never pruned, so a same-name replacement has a later one). Taken when a button
|
|
1085
|
+
* or menu is claimed or offered, asked after every await before acting by name.
|
|
1086
|
+
*/
|
|
1087
|
+
/** #1490 row 27: an instance as the chat list names it — its display name where it has one, else its name. */
|
|
1088
|
+
shownInstanceName(name) {
|
|
1089
|
+
try {
|
|
1090
|
+
const classic = this.classicChannels?.getAll().find(ch => ch.instanceName === name) ?? null;
|
|
1091
|
+
return resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic, profile: profileIdentities(this.dataDir).get(name) }).display_name || name;
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
return name;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
lifecyclePin(name) {
|
|
1098
|
+
// Fail closed: a lifecycle that cannot say its epoch holds nothing (and never stops a prompt from being posted).
|
|
1099
|
+
let epoch;
|
|
1100
|
+
try {
|
|
1101
|
+
epoch = this.lifecycle?.epochOf(name);
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
return () => false;
|
|
1105
|
+
}
|
|
1106
|
+
return () => { try {
|
|
1107
|
+
return !this.shuttingDown && this.lifecycle?.epochOf(name) === epoch;
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
} };
|
|
1112
|
+
}
|
|
1072
1113
|
webChatGeneration(name) { return this.webChatGenerations.get(name) ?? 0; }
|
|
1073
1114
|
/** #1774: who an instance's reply is from, taken when it is sent: the name's generation and its daemon then. */
|
|
1074
1115
|
webChatIdentity(name) { return { generation: this.webChatGeneration(name), daemon: this.daemons.get(name) }; }
|
|
@@ -5641,6 +5682,23 @@ export class FleetManager {
|
|
|
5641
5682
|
if (resumePaused)
|
|
5642
5683
|
this.lifecycle.markActivitySeedNow(name);
|
|
5643
5684
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5685
|
+
// #1490 row 67: off-loop resolve before skill/instruction publication
|
|
5686
|
+
// constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
|
|
5687
|
+
// ownership first: a Stop during this wait finishes and bumps the epoch,
|
|
5688
|
+
// and the inner lifecycle check would otherwise capture the new epoch and
|
|
5689
|
+
// accept the stale start. Lifecycle start looks up again before its own
|
|
5690
|
+
// createBackend; the probe cache makes that cheap.
|
|
5691
|
+
const epoch = this.lifecycle.epochOf(name);
|
|
5692
|
+
const stopGen = this.explicitStopGeneration.get(name) ?? 0;
|
|
5693
|
+
const resolved = await resolveBackendBinary(backend);
|
|
5694
|
+
execution?.assert();
|
|
5695
|
+
if (this.lifecycle.epochOf(name) !== epoch
|
|
5696
|
+
|| (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
|
|
5697
|
+
throw new SupersededStartError(name);
|
|
5698
|
+
}
|
|
5699
|
+
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
5700
|
+
if (binary)
|
|
5701
|
+
noteResolvedBinary(binary, resolved ?? binary);
|
|
5644
5702
|
if (config.general_topic) {
|
|
5645
5703
|
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
5646
5704
|
}
|
|
@@ -6291,6 +6349,8 @@ export class FleetManager {
|
|
|
6291
6349
|
}
|
|
6292
6350
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
6293
6351
|
async restartSingleInstance(name, opts) {
|
|
6352
|
+
if (opts?.ownerCurrent && !opts.ownerCurrent())
|
|
6353
|
+
throw new RestartSupersededError(this.shownInstanceName(name));
|
|
6294
6354
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
6295
6355
|
if (migration && migration.token !== opts?.kiroMigration)
|
|
6296
6356
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -7591,7 +7651,7 @@ export class FleetManager {
|
|
|
7591
7651
|
return { kind: "refused" };
|
|
7592
7652
|
if (device.tier === "guest")
|
|
7593
7653
|
return this.requestGuestDeviceCode(device, input, token);
|
|
7594
|
-
const paused = this.webLoginCodes.pausedForMs();
|
|
7654
|
+
const paused = this.webLoginCodes.pausedForMs(input.exposureId ?? "local");
|
|
7595
7655
|
if (paused > 0)
|
|
7596
7656
|
return { kind: "limited", retryAfterMs: paused };
|
|
7597
7657
|
const admitted = this.codeRequestLimiter.admit(device.idHash);
|
|
@@ -8076,6 +8136,9 @@ export class FleetManager {
|
|
|
8076
8136
|
this.watchdogTimer = startSdWatchdog();
|
|
8077
8137
|
this.eventLoopWatch?.stop();
|
|
8078
8138
|
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
8139
|
+
// #3009: the event-loop watch runs on the main thread; this one does not, so a stall that never ends is still named.
|
|
8140
|
+
this.stallReporter?.stop();
|
|
8141
|
+
this.stallReporter = startStallReporter({ logFile: join(this.dataDir, "daemon.log") });
|
|
8079
8142
|
try {
|
|
8080
8143
|
await this.startAllWithinDeadline(configPath, deadline);
|
|
8081
8144
|
}
|
|
@@ -8089,6 +8152,8 @@ export class FleetManager {
|
|
|
8089
8152
|
this.watchdogTimer = null;
|
|
8090
8153
|
this.eventLoopWatch?.stop();
|
|
8091
8154
|
this.eventLoopWatch = null;
|
|
8155
|
+
this.stallReporter?.stop();
|
|
8156
|
+
this.stallReporter = null;
|
|
8092
8157
|
}
|
|
8093
8158
|
}
|
|
8094
8159
|
}
|
|
@@ -8356,6 +8421,10 @@ export class FleetManager {
|
|
|
8356
8421
|
const generalDir = join(getAgendHome(), name);
|
|
8357
8422
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
8358
8423
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
8424
|
+
const resolved = await resolveBackendBinary(backendName);
|
|
8425
|
+
const binary = BACKEND_INSTALLATION_INFO[backendName]?.binary;
|
|
8426
|
+
if (binary)
|
|
8427
|
+
noteResolvedBinary(binary, resolved ?? binary);
|
|
8359
8428
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
8360
8429
|
fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
|
|
8361
8430
|
generalsCreated = true;
|
|
@@ -12866,6 +12935,9 @@ export class FleetManager {
|
|
|
12866
12935
|
await this.lifecycle.remove(name, authorization, execution, transition);
|
|
12867
12936
|
else
|
|
12868
12937
|
await this.lifecycle.remove(name, authorization);
|
|
12938
|
+
// #1490 row 27: its hang/exit offers were for the instance now gone (stopInstance clears them; removal stops
|
|
12939
|
+
// through the lifecycle directly). The offers' own lifecycle pins refuse them too.
|
|
12940
|
+
this.clearNoncePromptsForInstance(name);
|
|
12869
12941
|
// #1774: removed — its replies still copying their files are no one's to show, replaced or not (a failed removal
|
|
12870
12942
|
// throws above: its replies stay its own).
|
|
12871
12943
|
this.webChatRemovals.set(name, { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}) });
|
|
@@ -14540,6 +14612,7 @@ export class FleetManager {
|
|
|
14540
14612
|
threadId: opts.threadId,
|
|
14541
14613
|
expiredText: opts.expiredText,
|
|
14542
14614
|
createdAt: Date.now(),
|
|
14615
|
+
lifecycleCurrent: this.lifecyclePin(opts.instanceName),
|
|
14543
14616
|
...opts.extra,
|
|
14544
14617
|
};
|
|
14545
14618
|
entry.timer = setTimeout(() => {
|
|
@@ -16253,21 +16326,27 @@ export class FleetManager {
|
|
|
16253
16326
|
if (claimed === "consumed")
|
|
16254
16327
|
return true;
|
|
16255
16328
|
const { entry: pending, action } = claimed;
|
|
16329
|
+
// #1490 row 27: the lifecycle the button was offered for — a replacement since (removal does not clear prompts) is stale.
|
|
16330
|
+
const current = pending.lifecycleCurrent ?? this.lifecyclePin(pending.instanceName);
|
|
16256
16331
|
this.eventLog?.insert(pending.instanceName, oom ? "session_oom_action" : "normal_exit_action", { action, userId: data.userId });
|
|
16257
16332
|
if (action === "ignore") {
|
|
16258
16333
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.ignored", pending.instanceName));
|
|
16259
16334
|
return true;
|
|
16260
16335
|
}
|
|
16336
|
+
if (!current()) {
|
|
16337
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("restart.superseded", this.shownInstanceName(pending.instanceName)));
|
|
16338
|
+
return true;
|
|
16339
|
+
}
|
|
16261
16340
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.restarting", pending.instanceName));
|
|
16262
16341
|
try {
|
|
16263
|
-
await this.restartSingleInstance(pending.instanceName, { explicit: true, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
16342
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ownerCurrent: current, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
16264
16343
|
...(pending.confirmationCurrent ? { recoveryCurrent: pending.confirmationCurrent } : {}) } : {}) });
|
|
16265
16344
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("exit.restarted", pending.instanceName), pending.threadId);
|
|
16266
16345
|
}
|
|
16267
16346
|
catch (err) {
|
|
16268
16347
|
const message = err instanceof Error ? err.message : String(err);
|
|
16269
16348
|
this.logger.error({ err, instanceName: pending.instanceName }, "Normal-exit restart failed");
|
|
16270
|
-
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("exit.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show normal-exit restart error"));
|
|
16349
|
+
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, err instanceof RestartSupersededError ? message : t("exit.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show normal-exit restart error"));
|
|
16271
16350
|
}
|
|
16272
16351
|
return true;
|
|
16273
16352
|
}
|
|
@@ -17379,7 +17458,10 @@ export class FleetManager {
|
|
|
17379
17458
|
return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
|
|
17380
17459
|
dataDir: this.dataDir, lock: this.loginWindow,
|
|
17381
17460
|
current: owner => this.browserSignInOwnerCurrent(owner),
|
|
17382
|
-
installed: async () =>
|
|
17461
|
+
installed: async () => {
|
|
17462
|
+
const found = await Promise.all(ONBOARDING_BACKENDS.map(async (backend) => (await this.isCliInstalledOffLoop(backend)) ? backend : null));
|
|
17463
|
+
return found.filter((backend) => backend !== null);
|
|
17464
|
+
},
|
|
17383
17465
|
checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
|
|
17384
17466
|
checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
|
|
17385
17467
|
transport: () => createStructuredSignInTransport(),
|
|
@@ -17483,13 +17565,13 @@ export class FleetManager {
|
|
|
17483
17565
|
* #2233: `/login` for a dashboard requester. The same controller and checks; a CLI that is not installed is refused
|
|
17484
17566
|
* with its install command (chat's install-first runs a terminal of its own, which the dashboard does not offer).
|
|
17485
17567
|
*/
|
|
17486
|
-
startWebDashboardLogin(backendArg, chat, opts) {
|
|
17568
|
+
async startWebDashboardLogin(backendArg, chat, opts) {
|
|
17487
17569
|
const wanted = LOGIN_BACKEND_ALIASES[backendArg.toLowerCase()] ?? backendArg.toLowerCase();
|
|
17488
17570
|
if (isRemovedBackend(wanted))
|
|
17489
|
-
return
|
|
17571
|
+
return removedBackendMessage(wanted);
|
|
17490
17572
|
const info = BACKEND_INSTALLATION_INFO[wanted];
|
|
17491
|
-
if (info && LOGIN_FLOWS[wanted]?.remoteLogin !== "unsupported" && !this.
|
|
17492
|
-
return
|
|
17573
|
+
if (info && LOGIN_FLOWS[wanted]?.remoteLogin !== "unsupported" && !(await this.isCliInstalledOffLoop(wanted))) {
|
|
17574
|
+
return t("login.web_dashboard_not_installed", wanted, info.install);
|
|
17493
17575
|
}
|
|
17494
17576
|
if (!opts.skipAuthCheck)
|
|
17495
17577
|
this.recordLoginFlow(wanted, "login", chat.userId);
|
|
@@ -17537,8 +17619,10 @@ export class FleetManager {
|
|
|
17537
17619
|
? removedBackendMessage(backend)
|
|
17538
17620
|
: !installed.has(backend)
|
|
17539
17621
|
? (BACKEND_INSTALLATION_INFO[backend] ? t("login.install_by_name", backend) : t("login.install_on_host", backend))
|
|
17622
|
+
// #2981: one short line in the bare /login list — the full refusal (why, and what to run) is `/login <it>`'s;
|
|
17623
|
+
// a bare /login used to post Antigravity's whole paragraph to someone who never asked about it.
|
|
17540
17624
|
: flow?.remoteLogin === "unsupported"
|
|
17541
|
-
? t("login.
|
|
17625
|
+
? t("login.host_only", flow.command)
|
|
17542
17626
|
: backend === "opencode" ? t("login.unsupported", backend) : t("login.no_remote_flow", backend)}`).join("\n");
|
|
17543
17627
|
await chat.adapter.sendText(chat.chatId, guidance, { threadId: chat.threadId })
|
|
17544
17628
|
.catch(err => this.logger.warn({ err }, "Failed to post unsupported login guidance"));
|
|
@@ -17981,9 +18065,14 @@ export class FleetManager {
|
|
|
17981
18065
|
// session only judges that verdict; it does not read the output for URLs or prompts.
|
|
17982
18066
|
// M-2281 (#2980): no terminal to ask on, low priority, stoppable as a whole, the user's own $SHELL — see
|
|
17983
18067
|
// unattended-install.ts. A prompt nobody can answer fails at once instead of waiting out the timeout.
|
|
18068
|
+
const shell = (await loginShell()) ?? null;
|
|
18069
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
18070
|
+
this.loginWindow.release(claim);
|
|
18071
|
+
return t("login.web_shutting_down");
|
|
18072
|
+
}
|
|
17984
18073
|
const flow = {
|
|
17985
18074
|
backend,
|
|
17986
|
-
command: unattendedInstallCommand(info.unattended ?? info.install),
|
|
18075
|
+
command: unattendedInstallCommand(info.unattended ?? info.install, shell),
|
|
17987
18076
|
successPattern: /$^/,
|
|
17988
18077
|
timeoutMs: this.installTimeoutMs,
|
|
17989
18078
|
};
|
|
@@ -18027,7 +18116,9 @@ export class FleetManager {
|
|
|
18027
18116
|
return;
|
|
18028
18117
|
// The installer may only have added the binary to a profile PATH; a
|
|
18029
18118
|
// fresh login shell sees that, the fleet process's PATH may not.
|
|
18030
|
-
|
|
18119
|
+
// #3009 (leader, cid-3017-3029-overlap): both lookups off the fleet's main thread — the login shell's own check,
|
|
18120
|
+
// then the installer's own directories through backend-availability's check (a killable child tests each one).
|
|
18121
|
+
const installedAt = (await this.locateBinaryOnLoginShell(info.binary)) ?? (await this.locateInstalledBackend(backend));
|
|
18031
18122
|
if (handoff.cancelled || this.installHandoff !== handoff)
|
|
18032
18123
|
return;
|
|
18033
18124
|
if (!installedAt) {
|
|
@@ -18119,38 +18210,27 @@ export class FleetManager {
|
|
|
18119
18210
|
*/
|
|
18120
18211
|
async locateBinaryOnLoginShell(binary) {
|
|
18121
18212
|
try {
|
|
18122
|
-
|
|
18213
|
+
// #3009: the executable-file check runs inside the same login shell (a killable child), never as accessSync/statSync
|
|
18214
|
+
// on the fleet's thread — a path on a filesystem that stopped answering held the whole fleet there.
|
|
18215
|
+
const result = await runOnLoginShell(`p=$(command -v ${binary}) && [ -f "$p" ] && [ -x "$p" ] && printf '%s\n' "$p"`);
|
|
18123
18216
|
if (result.status !== 0)
|
|
18124
18217
|
return null;
|
|
18125
18218
|
const path = result.stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
18126
|
-
|
|
18127
|
-
return null;
|
|
18128
|
-
accessSync(path, fsConstants.X_OK);
|
|
18129
|
-
return statSync(path).isFile() ? path : null;
|
|
18219
|
+
return isAbsolute(path) ? path : null;
|
|
18130
18220
|
}
|
|
18131
18221
|
catch {
|
|
18132
18222
|
return null;
|
|
18133
18223
|
}
|
|
18134
18224
|
}
|
|
18135
18225
|
/**
|
|
18136
|
-
* #1092: the binary in one of the directories its own installer installs to,
|
|
18137
|
-
*
|
|
18138
|
-
*
|
|
18139
|
-
*
|
|
18226
|
+
* #1092: the binary in one of the directories its own installer installs to, when no login shell can see it (the
|
|
18227
|
+
* installer recorded its PATH in an interactive-only rc file) — only those directories (#1092). #3009: through
|
|
18228
|
+
* backend-availability's install-directory check (the one resolveBackendBinary uses): one killable child, never
|
|
18229
|
+
* accessSync/statSync on the fleet's thread. An executable file counts, a symlink to one too (codex's launcher is one).
|
|
18140
18230
|
*/
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
continue;
|
|
18145
|
-
const path = join(dir, info.binary);
|
|
18146
|
-
try {
|
|
18147
|
-
accessSync(path, fsConstants.X_OK);
|
|
18148
|
-
if (statSync(path).isFile())
|
|
18149
|
-
return path;
|
|
18150
|
-
}
|
|
18151
|
-
catch { /* not there */ }
|
|
18152
|
-
}
|
|
18153
|
-
return null;
|
|
18231
|
+
async locateInstalledBackend(backend) {
|
|
18232
|
+
const info = BACKEND_INSTALLATION_INFO[backend];
|
|
18233
|
+
return info ? (await findExecutableIn(info.binary, info.binDirs?.(process.env, homedir()) ?? [])) ?? null : null;
|
|
18154
18234
|
}
|
|
18155
18235
|
/** Put an installed binary's directory on the fleet's PATH (#1059), once. */
|
|
18156
18236
|
adoptBinaryDirectory(binaryPath, backend) {
|
|
@@ -18466,20 +18546,26 @@ export class FleetManager {
|
|
|
18466
18546
|
if (claimed === "consumed")
|
|
18467
18547
|
return true;
|
|
18468
18548
|
const { entry: pending, action } = claimed;
|
|
18549
|
+
// #1490 row 27: the lifecycle the button was offered for — a replacement since (removal does not clear prompts) is stale.
|
|
18550
|
+
const current = pending.lifecycleCurrent ?? this.lifecyclePin(pending.instanceName);
|
|
18469
18551
|
this.eventLog?.insert(pending.instanceName, "hang_action", { action, userId: data.userId });
|
|
18470
18552
|
if (action === "wait") {
|
|
18471
18553
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.waiting", pending.instanceName));
|
|
18472
18554
|
return true;
|
|
18473
18555
|
}
|
|
18556
|
+
if (!current()) {
|
|
18557
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("restart.superseded", this.shownInstanceName(pending.instanceName)));
|
|
18558
|
+
return true;
|
|
18559
|
+
}
|
|
18474
18560
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.restarting", pending.instanceName));
|
|
18475
18561
|
try {
|
|
18476
|
-
await this.restartSingleInstance(pending.instanceName, { explicit: true });
|
|
18562
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ownerCurrent: current });
|
|
18477
18563
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("hang.restarted", pending.instanceName), pending.threadId);
|
|
18478
18564
|
}
|
|
18479
18565
|
catch (err) {
|
|
18480
18566
|
const message = err instanceof Error ? err.message : String(err);
|
|
18481
18567
|
this.logger.error({ err, instanceName: pending.instanceName }, "Hang force-restart failed");
|
|
18482
|
-
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("hang.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show hang restart error"));
|
|
18568
|
+
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, err instanceof RestartSupersededError ? message : t("hang.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show hang restart error"));
|
|
18483
18569
|
}
|
|
18484
18570
|
return true;
|
|
18485
18571
|
}
|
|
@@ -18555,6 +18641,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18555
18641
|
resolveKnowledgeWorkDir(workDir, backend, instanceName) {
|
|
18556
18642
|
// Backend resolution may create/normalize the real cwd. Instructions and
|
|
18557
18643
|
// skills must land where the CLI actually runs, not an assumed path.
|
|
18644
|
+
// Caller pins resolveBackendBinary first so createBackend does not `which`.
|
|
18558
18645
|
try {
|
|
18559
18646
|
const resolved = createBackend(backend, join(getAgendHome(), "cli-env"))
|
|
18560
18647
|
.resolveWorkingDirectory?.(workDir, instanceName);
|
|
@@ -19877,7 +19964,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19877
19964
|
}));
|
|
19878
19965
|
const timer = setTimeout(() => this.pendingEffortSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19879
19966
|
timer.unref?.();
|
|
19880
|
-
this.pendingEffortSelects.set(nonce, { instanceName: name, userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond });
|
|
19967
|
+
this.pendingEffortSelects.set(nonce, { instanceName: name, instanceCurrent: this.lifecyclePin(name), userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond });
|
|
19881
19968
|
try {
|
|
19882
19969
|
await data.respondChoices(t("effort.menu", this.effortMenuHeader(name)), choices);
|
|
19883
19970
|
}
|
|
@@ -19912,7 +19999,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19912
19999
|
}
|
|
19913
20000
|
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19914
20001
|
timer.unref?.();
|
|
19915
|
-
this.pendingEffortSelects.set(nonce, { instanceName, userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20002
|
+
this.pendingEffortSelects.set(nonce, { instanceName, instanceCurrent: this.lifecyclePin(instanceName), userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
19916
20003
|
try {
|
|
19917
20004
|
const menuMessageId = await adapter.promptUser(chatId, t("effort.menu", this.effortMenuHeader(instanceName)), choices, { threadId });
|
|
19918
20005
|
const pending = this.pendingEffortSelects.get(nonce);
|
|
@@ -19937,6 +20024,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19937
20024
|
const pending = this.pendingEffortSelects.get(match[1]);
|
|
19938
20025
|
if (!pending)
|
|
19939
20026
|
return true;
|
|
20027
|
+
// #1490 row 27: the menu was for one lifecycle of the instance; stopped, restarted or replaced since, it is stale.
|
|
20028
|
+
if (!pending.instanceCurrent()) {
|
|
20029
|
+
this.pendingEffortSelects.delete(match[1]);
|
|
20030
|
+
clearTimeout(pending.timer);
|
|
20031
|
+
data.ack?.(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName)));
|
|
20032
|
+
await pending.respond(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))).catch(() => { });
|
|
20033
|
+
return true;
|
|
20034
|
+
}
|
|
19940
20035
|
// The admin who opened the menu, through the adapter that posted it, in the same channel — and still an admin
|
|
19941
20036
|
// when they click (#754 audit): the menu lives for a minute, and a click carries its own callback data.
|
|
19942
20037
|
const cbChannel = data.threadId ?? data.chatId;
|
|
@@ -19979,17 +20074,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19979
20074
|
let result;
|
|
19980
20075
|
try {
|
|
19981
20076
|
// Asked again after the progress edit awaited above: the instance may have moved, or the admin lost admin.
|
|
19982
|
-
result =
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
if (
|
|
19986
|
-
|
|
20077
|
+
result = !pending.instanceCurrent() ? t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))
|
|
20078
|
+
: this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)
|
|
20079
|
+
? await withSpawnHoldReply(async (text) => {
|
|
20080
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
20081
|
+
if (progressMsgId)
|
|
20082
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressMsgId, text, pending.adapterThreadId);
|
|
20083
|
+
else
|
|
20084
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
20085
|
+
}
|
|
19987
20086
|
else
|
|
19988
|
-
await pending.
|
|
19989
|
-
}
|
|
19990
|
-
else
|
|
19991
|
-
await pending.respond(text);
|
|
19992
|
-
}, () => this.applyEffort(pending.instanceName, level)) : t("menu.click_stale");
|
|
20087
|
+
await pending.respond(text);
|
|
20088
|
+
}, () => this.applyEffort(pending.instanceName, level, { current: pending.instanceCurrent })) : t("menu.click_stale");
|
|
19993
20089
|
}
|
|
19994
20090
|
catch (err) {
|
|
19995
20091
|
this.logger.error({ err, instance: pending.instanceName, level }, "Effort switch failed");
|
|
@@ -20046,7 +20142,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20046
20142
|
const choices = this.modelMenuChoices(name, nonce, options, currentModel);
|
|
20047
20143
|
const timer = setTimeout(() => this.pendingModelSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
20048
20144
|
timer.unref?.();
|
|
20049
|
-
this.pendingModelSelects.set(nonce, { instanceName: name, model: "", userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond, respondChoices: data.respondChoices });
|
|
20145
|
+
this.pendingModelSelects.set(nonce, { instanceName: name, instanceCurrent: this.lifecyclePin(name), model: "", userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond, respondChoices: data.respondChoices });
|
|
20050
20146
|
try {
|
|
20051
20147
|
await data.respondChoices(t("model.menu", `**${currentDisplay}**`), choices);
|
|
20052
20148
|
}
|
|
@@ -20082,7 +20178,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20082
20178
|
}
|
|
20083
20179
|
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
20084
20180
|
timer.unref?.();
|
|
20085
|
-
this.pendingModelSelects.set(nonce, { instanceName, model: "", userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20181
|
+
this.pendingModelSelects.set(nonce, { instanceName, instanceCurrent: this.lifecyclePin(instanceName), model: "", userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20086
20182
|
try {
|
|
20087
20183
|
const menuMessageId = await adapter.promptUser(chatId, t("model.menu", currentDisplay), choices, { threadId });
|
|
20088
20184
|
const pending = this.pendingModelSelects.get(nonce);
|
|
@@ -20255,6 +20351,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20255
20351
|
const pending = this.pendingModelSelects.get(match[1]);
|
|
20256
20352
|
if (!pending)
|
|
20257
20353
|
return true;
|
|
20354
|
+
// #1490 row 27: the menu was for one lifecycle of the instance; stopped, restarted or replaced since, it is stale.
|
|
20355
|
+
if (!pending.instanceCurrent()) {
|
|
20356
|
+
this.pendingModelSelects.delete(match[1]);
|
|
20357
|
+
clearTimeout(pending.timer);
|
|
20358
|
+
data.ack?.(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName)));
|
|
20359
|
+
await pending.respond(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))).catch(() => { });
|
|
20360
|
+
return true;
|
|
20361
|
+
}
|
|
20258
20362
|
// The admin who opened the menu, through the adapter that posted it, in the same channel — and still an admin
|
|
20259
20363
|
// when they click (#754 audit): the menu lives for a minute, and a click carries its own callback data.
|
|
20260
20364
|
const cbChannel = data.threadId ?? data.chatId;
|
|
@@ -20315,17 +20419,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20315
20419
|
let result;
|
|
20316
20420
|
try {
|
|
20317
20421
|
// Asked again after the progress edit awaited above: the instance may have moved, or the admin lost admin.
|
|
20318
|
-
result =
|
|
20319
|
-
|
|
20320
|
-
|
|
20321
|
-
if (
|
|
20322
|
-
|
|
20422
|
+
result = !pending.instanceCurrent() ? t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))
|
|
20423
|
+
: this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)
|
|
20424
|
+
? await withSpawnHoldReply(async (text) => {
|
|
20425
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
20426
|
+
if (progressMsgId)
|
|
20427
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressMsgId, text, pending.adapterThreadId);
|
|
20428
|
+
else
|
|
20429
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
20430
|
+
}
|
|
20323
20431
|
else
|
|
20324
|
-
await pending.
|
|
20325
|
-
}
|
|
20326
|
-
else
|
|
20327
|
-
await pending.respond(text);
|
|
20328
|
-
}, () => this.applyModel(pending.instanceName, model)) : t("menu.click_stale");
|
|
20432
|
+
await pending.respond(text);
|
|
20433
|
+
}, () => this.applyModel(pending.instanceName, model, { current: pending.instanceCurrent })) : t("menu.click_stale");
|
|
20329
20434
|
}
|
|
20330
20435
|
catch (err) {
|
|
20331
20436
|
this.logger.error({ err, instance: pending.instanceName, model }, "Model switch failed");
|
|
@@ -20418,7 +20523,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20418
20523
|
* restart backends only read it at launch, so it is persisted and the
|
|
20419
20524
|
* instance respawns.
|
|
20420
20525
|
*/
|
|
20421
|
-
async applyEffort(instanceName, requested) {
|
|
20526
|
+
async applyEffort(instanceName, requested, opts) {
|
|
20527
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
20528
|
+
if (opts?.current && !opts.current())
|
|
20529
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20422
20530
|
const level = requested.trim().toLowerCase();
|
|
20423
20531
|
const backendName = this.backendNameForInstance(instanceName);
|
|
20424
20532
|
let strategy = "unsupported";
|
|
@@ -20453,10 +20561,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20453
20561
|
this.pasteRawToClassicInstance(instanceName, `/effort ${applied}`);
|
|
20454
20562
|
return `${warn}${t("effort.runtime_success", instanceName, applied)}`;
|
|
20455
20563
|
}
|
|
20456
|
-
await this.restartSingleInstance(instanceName);
|
|
20564
|
+
await this.restartSingleInstance(instanceName, { ownerCurrent: opts?.current });
|
|
20457
20565
|
return `${warn}${t("effort.restart_success", instanceName, applied)}`;
|
|
20458
20566
|
}
|
|
20459
|
-
async applyModel(instanceName, model) {
|
|
20567
|
+
async applyModel(instanceName, model, opts) {
|
|
20568
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first, after the runtime switch's
|
|
20569
|
+
// await, and fenced into the restart.
|
|
20570
|
+
if (opts?.current && !opts.current())
|
|
20571
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20460
20572
|
// Reject model names with newlines or control characters: they would be
|
|
20461
20573
|
// persisted to fleet.yaml and pasted raw into the CLI (P3 from #1490 audit).
|
|
20462
20574
|
if (/[\x00-\x1f\x7f]/.test(model)) {
|
|
@@ -20482,6 +20594,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20482
20594
|
// #1690: claude-code's runtime switch reports what it came to; the model is saved unless claude was told No.
|
|
20483
20595
|
if (strategy === "runtime" && backendName === "claude-code") {
|
|
20484
20596
|
const result = await this.runRuntimeModelSwitch(instanceName, model);
|
|
20597
|
+
if (opts?.current && !opts.current())
|
|
20598
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20485
20599
|
if (result.outcome === "mismatch")
|
|
20486
20600
|
return `${warn}${t("model.switch_mismatch", instanceName, result.shown, model)}`;
|
|
20487
20601
|
if (result.outcome === "not_sent")
|
|
@@ -20502,7 +20616,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20502
20616
|
this.pasteRawToClassicInstance(instanceName, `/model ${model}`);
|
|
20503
20617
|
return `${warn}${t("model.runtime_success", instanceName, model)}${this.effortSuffix(instanceName)}`;
|
|
20504
20618
|
}
|
|
20505
|
-
await this.restartSingleInstance(instanceName);
|
|
20619
|
+
await this.restartSingleInstance(instanceName, { ownerCurrent: opts?.current });
|
|
20506
20620
|
return `${warn}${t("model.restart_success", instanceName, model)}${this.effortSuffix(instanceName)}`;
|
|
20507
20621
|
}
|
|
20508
20622
|
/** Save an instance's model (fleet.yaml, or its ClassicBot channel). False when neither holds it. */
|
|
@@ -21435,6 +21549,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21435
21549
|
}
|
|
21436
21550
|
this.eventLoopWatch?.stop();
|
|
21437
21551
|
this.eventLoopWatch = null;
|
|
21552
|
+
this.stallReporter?.stop();
|
|
21553
|
+
this.stallReporter = null;
|
|
21438
21554
|
// A login/install window is a dedicated tmux server with its own TTL
|
|
21439
21555
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
21440
21556
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|