@songsid/agend 2.3.4-beta.6 → 2.3.4-beta.8
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/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.d.ts +3 -0
- package/dist/backend/kiro.js +57 -4
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +6 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/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.d.ts +40 -0
- package/dist/daemon.js +354 -15
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-manager.d.ts +33 -7
- package/dist/fleet-manager.js +196 -67
- 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/concepts/SKILL.md +390 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +38 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/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 +32 -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/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +6 -0
- package/dist/needs-you.js +8 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/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/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.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/panel-needs.js +4 -3
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +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.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;
|
|
@@ -878,6 +887,7 @@ export class FleetManager {
|
|
|
878
887
|
startupRetryNotices = new Map();
|
|
879
888
|
/** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
|
|
880
889
|
unsupportedCliNotices = new Map();
|
|
890
|
+
kiroCompatibilityHolds = new Map();
|
|
881
891
|
kiroIncompatNotices = new Map();
|
|
882
892
|
/** Backoff between automatic startup retries; the last step repeats while the backend is down. */
|
|
883
893
|
static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
|
@@ -1026,6 +1036,7 @@ export class FleetManager {
|
|
|
1026
1036
|
watchdogTimer = null;
|
|
1027
1037
|
startupMemoryDeadline = null;
|
|
1028
1038
|
eventLoopWatch = null;
|
|
1039
|
+
stallReporter = null;
|
|
1029
1040
|
startedAt = 0;
|
|
1030
1041
|
stormOpenNotifyTimer = null;
|
|
1031
1042
|
memoryLogAt = null;
|
|
@@ -1069,6 +1080,37 @@ export class FleetManager {
|
|
|
1069
1080
|
webChatRemovals = new Map();
|
|
1070
1081
|
/** #1774: a removal under way — its replies' fate waits for its outcome (success: theirs no more; failure: theirs). */
|
|
1071
1082
|
webChatRemovalsPending = new Map();
|
|
1083
|
+
/**
|
|
1084
|
+
* #1490 row 27: "still the same lifecycle of `name`" — its lifecycle epoch, moved on synchronously by every stop,
|
|
1085
|
+
* restart and removal (the map is never pruned, so a same-name replacement has a later one). Taken when a button
|
|
1086
|
+
* or menu is claimed or offered, asked after every await before acting by name.
|
|
1087
|
+
*/
|
|
1088
|
+
/** #1490 row 27: an instance as the chat list names it — its display name where it has one, else its name. */
|
|
1089
|
+
shownInstanceName(name) {
|
|
1090
|
+
try {
|
|
1091
|
+
const classic = this.classicChannels?.getAll().find(ch => ch.instanceName === name) ?? null;
|
|
1092
|
+
return resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic, profile: profileIdentities(this.dataDir).get(name) }).display_name || name;
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
return name;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
lifecyclePin(name) {
|
|
1099
|
+
// Fail closed: a lifecycle that cannot say its epoch holds nothing (and never stops a prompt from being posted).
|
|
1100
|
+
let epoch;
|
|
1101
|
+
try {
|
|
1102
|
+
epoch = this.lifecycle?.epochOf(name);
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return () => false;
|
|
1106
|
+
}
|
|
1107
|
+
return () => { try {
|
|
1108
|
+
return !this.shuttingDown && this.lifecycle?.epochOf(name) === epoch;
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
return false;
|
|
1112
|
+
} };
|
|
1113
|
+
}
|
|
1072
1114
|
webChatGeneration(name) { return this.webChatGenerations.get(name) ?? 0; }
|
|
1073
1115
|
/** #1774: who an instance's reply is from, taken when it is sent: the name's generation and its daemon then. */
|
|
1074
1116
|
webChatIdentity(name) { return { generation: this.webChatGeneration(name), daemon: this.daemons.get(name) }; }
|
|
@@ -5641,6 +5683,23 @@ export class FleetManager {
|
|
|
5641
5683
|
if (resumePaused)
|
|
5642
5684
|
this.lifecycle.markActivitySeedNow(name);
|
|
5643
5685
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5686
|
+
// #1490 row 67: off-loop resolve before skill/instruction publication
|
|
5687
|
+
// constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
|
|
5688
|
+
// ownership first: a Stop during this wait finishes and bumps the epoch,
|
|
5689
|
+
// and the inner lifecycle check would otherwise capture the new epoch and
|
|
5690
|
+
// accept the stale start. Lifecycle start looks up again before its own
|
|
5691
|
+
// createBackend; the probe cache makes that cheap.
|
|
5692
|
+
const epoch = this.lifecycle.epochOf(name);
|
|
5693
|
+
const stopGen = this.explicitStopGeneration.get(name) ?? 0;
|
|
5694
|
+
const resolved = await resolveBackendBinary(backend);
|
|
5695
|
+
execution?.assert();
|
|
5696
|
+
if (this.lifecycle.epochOf(name) !== epoch
|
|
5697
|
+
|| (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
|
|
5698
|
+
throw new SupersededStartError(name);
|
|
5699
|
+
}
|
|
5700
|
+
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
5701
|
+
if (binary)
|
|
5702
|
+
noteResolvedBinary(binary, resolved ?? binary);
|
|
5644
5703
|
if (config.general_topic) {
|
|
5645
5704
|
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
5646
5705
|
}
|
|
@@ -6097,6 +6156,17 @@ export class FleetManager {
|
|
|
6097
6156
|
this.queueKiroIncompatNotice(name, err.message);
|
|
6098
6157
|
return true;
|
|
6099
6158
|
}
|
|
6159
|
+
/** No pane is needed for a compatibility refusal to remain in Needs you. */
|
|
6160
|
+
setKiroCompatibilityHold(name, message) {
|
|
6161
|
+
const holds = this.kiroCompatibilityHolds ??= new Map();
|
|
6162
|
+
if ((message === null && !holds.has(name)) || (message !== null && holds.get(name)?.message === message))
|
|
6163
|
+
return;
|
|
6164
|
+
if (message === null)
|
|
6165
|
+
holds.delete(name);
|
|
6166
|
+
else
|
|
6167
|
+
holds.set(name, { message, since: Date.now() });
|
|
6168
|
+
this.needsYou?.poke();
|
|
6169
|
+
}
|
|
6100
6170
|
/**
|
|
6101
6171
|
* LifecycleContext: a kiro instance the installed kiro-cli cannot run as
|
|
6102
6172
|
* configured (#1109) — at start, or at a respawn after kiro-cli replaced
|
|
@@ -6291,6 +6361,8 @@ export class FleetManager {
|
|
|
6291
6361
|
}
|
|
6292
6362
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
6293
6363
|
async restartSingleInstance(name, opts) {
|
|
6364
|
+
if (opts?.ownerCurrent && !opts.ownerCurrent())
|
|
6365
|
+
throw new RestartSupersededError(this.shownInstanceName(name));
|
|
6294
6366
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
6295
6367
|
if (migration && migration.token !== opts?.kiroMigration)
|
|
6296
6368
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -7591,7 +7663,7 @@ export class FleetManager {
|
|
|
7591
7663
|
return { kind: "refused" };
|
|
7592
7664
|
if (device.tier === "guest")
|
|
7593
7665
|
return this.requestGuestDeviceCode(device, input, token);
|
|
7594
|
-
const paused = this.webLoginCodes.pausedForMs();
|
|
7666
|
+
const paused = this.webLoginCodes.pausedForMs(input.exposureId ?? "local");
|
|
7595
7667
|
if (paused > 0)
|
|
7596
7668
|
return { kind: "limited", retryAfterMs: paused };
|
|
7597
7669
|
const admitted = this.codeRequestLimiter.admit(device.idHash);
|
|
@@ -8076,6 +8148,9 @@ export class FleetManager {
|
|
|
8076
8148
|
this.watchdogTimer = startSdWatchdog();
|
|
8077
8149
|
this.eventLoopWatch?.stop();
|
|
8078
8150
|
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
8151
|
+
// #3009: the event-loop watch runs on the main thread; this one does not, so a stall that never ends is still named.
|
|
8152
|
+
this.stallReporter?.stop();
|
|
8153
|
+
this.stallReporter = startStallReporter({ logFile: join(this.dataDir, "daemon.log") });
|
|
8079
8154
|
try {
|
|
8080
8155
|
await this.startAllWithinDeadline(configPath, deadline);
|
|
8081
8156
|
}
|
|
@@ -8089,6 +8164,8 @@ export class FleetManager {
|
|
|
8089
8164
|
this.watchdogTimer = null;
|
|
8090
8165
|
this.eventLoopWatch?.stop();
|
|
8091
8166
|
this.eventLoopWatch = null;
|
|
8167
|
+
this.stallReporter?.stop();
|
|
8168
|
+
this.stallReporter = null;
|
|
8092
8169
|
}
|
|
8093
8170
|
}
|
|
8094
8171
|
}
|
|
@@ -8356,6 +8433,10 @@ export class FleetManager {
|
|
|
8356
8433
|
const generalDir = join(getAgendHome(), name);
|
|
8357
8434
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
8358
8435
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
8436
|
+
const resolved = await resolveBackendBinary(backendName);
|
|
8437
|
+
const binary = BACKEND_INSTALLATION_INFO[backendName]?.binary;
|
|
8438
|
+
if (binary)
|
|
8439
|
+
noteResolvedBinary(binary, resolved ?? binary);
|
|
8359
8440
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
8360
8441
|
fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
|
|
8361
8442
|
generalsCreated = true;
|
|
@@ -12866,6 +12947,9 @@ export class FleetManager {
|
|
|
12866
12947
|
await this.lifecycle.remove(name, authorization, execution, transition);
|
|
12867
12948
|
else
|
|
12868
12949
|
await this.lifecycle.remove(name, authorization);
|
|
12950
|
+
// #1490 row 27: its hang/exit offers were for the instance now gone (stopInstance clears them; removal stops
|
|
12951
|
+
// through the lifecycle directly). The offers' own lifecycle pins refuse them too.
|
|
12952
|
+
this.clearNoncePromptsForInstance(name);
|
|
12869
12953
|
// #1774: removed — its replies still copying their files are no one's to show, replaced or not (a failed removal
|
|
12870
12954
|
// throws above: its replies stay its own).
|
|
12871
12955
|
this.webChatRemovals.set(name, { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}) });
|
|
@@ -14540,6 +14624,7 @@ export class FleetManager {
|
|
|
14540
14624
|
threadId: opts.threadId,
|
|
14541
14625
|
expiredText: opts.expiredText,
|
|
14542
14626
|
createdAt: Date.now(),
|
|
14627
|
+
lifecycleCurrent: this.lifecyclePin(opts.instanceName),
|
|
14543
14628
|
...opts.extra,
|
|
14544
14629
|
};
|
|
14545
14630
|
entry.timer = setTimeout(() => {
|
|
@@ -15062,7 +15147,8 @@ export class FleetManager {
|
|
|
15062
15147
|
out.push({
|
|
15063
15148
|
name, state: p.state ?? undefined,
|
|
15064
15149
|
interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
|
|
15065
|
-
interactionSummary: p.interaction_summary ?? null,
|
|
15150
|
+
interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
|
|
15151
|
+
kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
|
|
15066
15152
|
pauseReason, pausedAt,
|
|
15067
15153
|
crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
|
|
15068
15154
|
signIn: pauseReason === "auth" ? this.signInFor(name) : null,
|
|
@@ -16253,21 +16339,27 @@ export class FleetManager {
|
|
|
16253
16339
|
if (claimed === "consumed")
|
|
16254
16340
|
return true;
|
|
16255
16341
|
const { entry: pending, action } = claimed;
|
|
16342
|
+
// #1490 row 27: the lifecycle the button was offered for — a replacement since (removal does not clear prompts) is stale.
|
|
16343
|
+
const current = pending.lifecycleCurrent ?? this.lifecyclePin(pending.instanceName);
|
|
16256
16344
|
this.eventLog?.insert(pending.instanceName, oom ? "session_oom_action" : "normal_exit_action", { action, userId: data.userId });
|
|
16257
16345
|
if (action === "ignore") {
|
|
16258
16346
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.ignored", pending.instanceName));
|
|
16259
16347
|
return true;
|
|
16260
16348
|
}
|
|
16349
|
+
if (!current()) {
|
|
16350
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("restart.superseded", this.shownInstanceName(pending.instanceName)));
|
|
16351
|
+
return true;
|
|
16352
|
+
}
|
|
16261
16353
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.restarting", pending.instanceName));
|
|
16262
16354
|
try {
|
|
16263
|
-
await this.restartSingleInstance(pending.instanceName, { explicit: true, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
16355
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ownerCurrent: current, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
16264
16356
|
...(pending.confirmationCurrent ? { recoveryCurrent: pending.confirmationCurrent } : {}) } : {}) });
|
|
16265
16357
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("exit.restarted", pending.instanceName), pending.threadId);
|
|
16266
16358
|
}
|
|
16267
16359
|
catch (err) {
|
|
16268
16360
|
const message = err instanceof Error ? err.message : String(err);
|
|
16269
16361
|
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"));
|
|
16362
|
+
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
16363
|
}
|
|
16272
16364
|
return true;
|
|
16273
16365
|
}
|
|
@@ -17379,7 +17471,10 @@ export class FleetManager {
|
|
|
17379
17471
|
return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
|
|
17380
17472
|
dataDir: this.dataDir, lock: this.loginWindow,
|
|
17381
17473
|
current: owner => this.browserSignInOwnerCurrent(owner),
|
|
17382
|
-
installed: async () =>
|
|
17474
|
+
installed: async () => {
|
|
17475
|
+
const found = await Promise.all(ONBOARDING_BACKENDS.map(async (backend) => (await this.isCliInstalledOffLoop(backend)) ? backend : null));
|
|
17476
|
+
return found.filter((backend) => backend !== null);
|
|
17477
|
+
},
|
|
17383
17478
|
checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
|
|
17384
17479
|
checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
|
|
17385
17480
|
transport: () => createStructuredSignInTransport(),
|
|
@@ -17483,13 +17578,13 @@ export class FleetManager {
|
|
|
17483
17578
|
* #2233: `/login` for a dashboard requester. The same controller and checks; a CLI that is not installed is refused
|
|
17484
17579
|
* with its install command (chat's install-first runs a terminal of its own, which the dashboard does not offer).
|
|
17485
17580
|
*/
|
|
17486
|
-
startWebDashboardLogin(backendArg, chat, opts) {
|
|
17581
|
+
async startWebDashboardLogin(backendArg, chat, opts) {
|
|
17487
17582
|
const wanted = LOGIN_BACKEND_ALIASES[backendArg.toLowerCase()] ?? backendArg.toLowerCase();
|
|
17488
17583
|
if (isRemovedBackend(wanted))
|
|
17489
|
-
return
|
|
17584
|
+
return removedBackendMessage(wanted);
|
|
17490
17585
|
const info = BACKEND_INSTALLATION_INFO[wanted];
|
|
17491
|
-
if (info && LOGIN_FLOWS[wanted]?.remoteLogin !== "unsupported" && !this.
|
|
17492
|
-
return
|
|
17586
|
+
if (info && LOGIN_FLOWS[wanted]?.remoteLogin !== "unsupported" && !(await this.isCliInstalledOffLoop(wanted))) {
|
|
17587
|
+
return t("login.web_dashboard_not_installed", wanted, info.install);
|
|
17493
17588
|
}
|
|
17494
17589
|
if (!opts.skipAuthCheck)
|
|
17495
17590
|
this.recordLoginFlow(wanted, "login", chat.userId);
|
|
@@ -17537,8 +17632,10 @@ export class FleetManager {
|
|
|
17537
17632
|
? removedBackendMessage(backend)
|
|
17538
17633
|
: !installed.has(backend)
|
|
17539
17634
|
? (BACKEND_INSTALLATION_INFO[backend] ? t("login.install_by_name", backend) : t("login.install_on_host", backend))
|
|
17635
|
+
// #2981: one short line in the bare /login list — the full refusal (why, and what to run) is `/login <it>`'s;
|
|
17636
|
+
// a bare /login used to post Antigravity's whole paragraph to someone who never asked about it.
|
|
17540
17637
|
: flow?.remoteLogin === "unsupported"
|
|
17541
|
-
? t("login.
|
|
17638
|
+
? t("login.host_only", flow.command)
|
|
17542
17639
|
: backend === "opencode" ? t("login.unsupported", backend) : t("login.no_remote_flow", backend)}`).join("\n");
|
|
17543
17640
|
await chat.adapter.sendText(chat.chatId, guidance, { threadId: chat.threadId })
|
|
17544
17641
|
.catch(err => this.logger.warn({ err }, "Failed to post unsupported login guidance"));
|
|
@@ -17981,9 +18078,14 @@ export class FleetManager {
|
|
|
17981
18078
|
// session only judges that verdict; it does not read the output for URLs or prompts.
|
|
17982
18079
|
// M-2281 (#2980): no terminal to ask on, low priority, stoppable as a whole, the user's own $SHELL — see
|
|
17983
18080
|
// unattended-install.ts. A prompt nobody can answer fails at once instead of waiting out the timeout.
|
|
18081
|
+
const shell = (await loginShell()) ?? null;
|
|
18082
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
18083
|
+
this.loginWindow.release(claim);
|
|
18084
|
+
return t("login.web_shutting_down");
|
|
18085
|
+
}
|
|
17984
18086
|
const flow = {
|
|
17985
18087
|
backend,
|
|
17986
|
-
command: unattendedInstallCommand(info.unattended ?? info.install),
|
|
18088
|
+
command: unattendedInstallCommand(info.unattended ?? info.install, shell),
|
|
17987
18089
|
successPattern: /$^/,
|
|
17988
18090
|
timeoutMs: this.installTimeoutMs,
|
|
17989
18091
|
};
|
|
@@ -18027,7 +18129,9 @@ export class FleetManager {
|
|
|
18027
18129
|
return;
|
|
18028
18130
|
// The installer may only have added the binary to a profile PATH; a
|
|
18029
18131
|
// fresh login shell sees that, the fleet process's PATH may not.
|
|
18030
|
-
|
|
18132
|
+
// #3009 (leader, cid-3017-3029-overlap): both lookups off the fleet's main thread — the login shell's own check,
|
|
18133
|
+
// then the installer's own directories through backend-availability's check (a killable child tests each one).
|
|
18134
|
+
const installedAt = (await this.locateBinaryOnLoginShell(info.binary)) ?? (await this.locateInstalledBackend(backend));
|
|
18031
18135
|
if (handoff.cancelled || this.installHandoff !== handoff)
|
|
18032
18136
|
return;
|
|
18033
18137
|
if (!installedAt) {
|
|
@@ -18119,38 +18223,27 @@ export class FleetManager {
|
|
|
18119
18223
|
*/
|
|
18120
18224
|
async locateBinaryOnLoginShell(binary) {
|
|
18121
18225
|
try {
|
|
18122
|
-
|
|
18226
|
+
// #3009: the executable-file check runs inside the same login shell (a killable child), never as accessSync/statSync
|
|
18227
|
+
// on the fleet's thread — a path on a filesystem that stopped answering held the whole fleet there.
|
|
18228
|
+
const result = await runOnLoginShell(`p=$(command -v ${binary}) && [ -f "$p" ] && [ -x "$p" ] && printf '%s\n' "$p"`);
|
|
18123
18229
|
if (result.status !== 0)
|
|
18124
18230
|
return null;
|
|
18125
18231
|
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;
|
|
18232
|
+
return isAbsolute(path) ? path : null;
|
|
18130
18233
|
}
|
|
18131
18234
|
catch {
|
|
18132
18235
|
return null;
|
|
18133
18236
|
}
|
|
18134
18237
|
}
|
|
18135
18238
|
/**
|
|
18136
|
-
* #1092: the binary in one of the directories its own installer installs to,
|
|
18137
|
-
*
|
|
18138
|
-
*
|
|
18139
|
-
*
|
|
18239
|
+
* #1092: the binary in one of the directories its own installer installs to, when no login shell can see it (the
|
|
18240
|
+
* installer recorded its PATH in an interactive-only rc file) — only those directories (#1092). #3009: through
|
|
18241
|
+
* backend-availability's install-directory check (the one resolveBackendBinary uses): one killable child, never
|
|
18242
|
+
* accessSync/statSync on the fleet's thread. An executable file counts, a symlink to one too (codex's launcher is one).
|
|
18140
18243
|
*/
|
|
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;
|
|
18244
|
+
async locateInstalledBackend(backend) {
|
|
18245
|
+
const info = BACKEND_INSTALLATION_INFO[backend];
|
|
18246
|
+
return info ? (await findExecutableIn(info.binary, info.binDirs?.(process.env, homedir()) ?? [])) ?? null : null;
|
|
18154
18247
|
}
|
|
18155
18248
|
/** Put an installed binary's directory on the fleet's PATH (#1059), once. */
|
|
18156
18249
|
adoptBinaryDirectory(binaryPath, backend) {
|
|
@@ -18466,20 +18559,26 @@ export class FleetManager {
|
|
|
18466
18559
|
if (claimed === "consumed")
|
|
18467
18560
|
return true;
|
|
18468
18561
|
const { entry: pending, action } = claimed;
|
|
18562
|
+
// #1490 row 27: the lifecycle the button was offered for — a replacement since (removal does not clear prompts) is stale.
|
|
18563
|
+
const current = pending.lifecycleCurrent ?? this.lifecyclePin(pending.instanceName);
|
|
18469
18564
|
this.eventLog?.insert(pending.instanceName, "hang_action", { action, userId: data.userId });
|
|
18470
18565
|
if (action === "wait") {
|
|
18471
18566
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.waiting", pending.instanceName));
|
|
18472
18567
|
return true;
|
|
18473
18568
|
}
|
|
18569
|
+
if (!current()) {
|
|
18570
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("restart.superseded", this.shownInstanceName(pending.instanceName)));
|
|
18571
|
+
return true;
|
|
18572
|
+
}
|
|
18474
18573
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.restarting", pending.instanceName));
|
|
18475
18574
|
try {
|
|
18476
|
-
await this.restartSingleInstance(pending.instanceName, { explicit: true });
|
|
18575
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ownerCurrent: current });
|
|
18477
18576
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("hang.restarted", pending.instanceName), pending.threadId);
|
|
18478
18577
|
}
|
|
18479
18578
|
catch (err) {
|
|
18480
18579
|
const message = err instanceof Error ? err.message : String(err);
|
|
18481
18580
|
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"));
|
|
18581
|
+
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
18582
|
}
|
|
18484
18583
|
return true;
|
|
18485
18584
|
}
|
|
@@ -18555,6 +18654,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18555
18654
|
resolveKnowledgeWorkDir(workDir, backend, instanceName) {
|
|
18556
18655
|
// Backend resolution may create/normalize the real cwd. Instructions and
|
|
18557
18656
|
// skills must land where the CLI actually runs, not an assumed path.
|
|
18657
|
+
// Caller pins resolveBackendBinary first so createBackend does not `which`.
|
|
18558
18658
|
try {
|
|
18559
18659
|
const resolved = createBackend(backend, join(getAgendHome(), "cli-env"))
|
|
18560
18660
|
.resolveWorkingDirectory?.(workDir, instanceName);
|
|
@@ -19877,7 +19977,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19877
19977
|
}));
|
|
19878
19978
|
const timer = setTimeout(() => this.pendingEffortSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19879
19979
|
timer.unref?.();
|
|
19880
|
-
this.pendingEffortSelects.set(nonce, { instanceName: name, userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond });
|
|
19980
|
+
this.pendingEffortSelects.set(nonce, { instanceName: name, instanceCurrent: this.lifecyclePin(name), userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond });
|
|
19881
19981
|
try {
|
|
19882
19982
|
await data.respondChoices(t("effort.menu", this.effortMenuHeader(name)), choices);
|
|
19883
19983
|
}
|
|
@@ -19912,7 +20012,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19912
20012
|
}
|
|
19913
20013
|
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19914
20014
|
timer.unref?.();
|
|
19915
|
-
this.pendingEffortSelects.set(nonce, { instanceName, userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20015
|
+
this.pendingEffortSelects.set(nonce, { instanceName, instanceCurrent: this.lifecyclePin(instanceName), userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
19916
20016
|
try {
|
|
19917
20017
|
const menuMessageId = await adapter.promptUser(chatId, t("effort.menu", this.effortMenuHeader(instanceName)), choices, { threadId });
|
|
19918
20018
|
const pending = this.pendingEffortSelects.get(nonce);
|
|
@@ -19937,6 +20037,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19937
20037
|
const pending = this.pendingEffortSelects.get(match[1]);
|
|
19938
20038
|
if (!pending)
|
|
19939
20039
|
return true;
|
|
20040
|
+
// #1490 row 27: the menu was for one lifecycle of the instance; stopped, restarted or replaced since, it is stale.
|
|
20041
|
+
if (!pending.instanceCurrent()) {
|
|
20042
|
+
this.pendingEffortSelects.delete(match[1]);
|
|
20043
|
+
clearTimeout(pending.timer);
|
|
20044
|
+
data.ack?.(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName)));
|
|
20045
|
+
await pending.respond(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))).catch(() => { });
|
|
20046
|
+
return true;
|
|
20047
|
+
}
|
|
19940
20048
|
// The admin who opened the menu, through the adapter that posted it, in the same channel — and still an admin
|
|
19941
20049
|
// when they click (#754 audit): the menu lives for a minute, and a click carries its own callback data.
|
|
19942
20050
|
const cbChannel = data.threadId ?? data.chatId;
|
|
@@ -19979,17 +20087,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19979
20087
|
let result;
|
|
19980
20088
|
try {
|
|
19981
20089
|
// 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
|
-
|
|
20090
|
+
result = !pending.instanceCurrent() ? t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))
|
|
20091
|
+
: this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)
|
|
20092
|
+
? await withSpawnHoldReply(async (text) => {
|
|
20093
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
20094
|
+
if (progressMsgId)
|
|
20095
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressMsgId, text, pending.adapterThreadId);
|
|
20096
|
+
else
|
|
20097
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
20098
|
+
}
|
|
19987
20099
|
else
|
|
19988
|
-
await pending.
|
|
19989
|
-
}
|
|
19990
|
-
else
|
|
19991
|
-
await pending.respond(text);
|
|
19992
|
-
}, () => this.applyEffort(pending.instanceName, level)) : t("menu.click_stale");
|
|
20100
|
+
await pending.respond(text);
|
|
20101
|
+
}, () => this.applyEffort(pending.instanceName, level, { current: pending.instanceCurrent })) : t("menu.click_stale");
|
|
19993
20102
|
}
|
|
19994
20103
|
catch (err) {
|
|
19995
20104
|
this.logger.error({ err, instance: pending.instanceName, level }, "Effort switch failed");
|
|
@@ -20046,7 +20155,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20046
20155
|
const choices = this.modelMenuChoices(name, nonce, options, currentModel);
|
|
20047
20156
|
const timer = setTimeout(() => this.pendingModelSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
20048
20157
|
timer.unref?.();
|
|
20049
|
-
this.pendingModelSelects.set(nonce, { instanceName: name, model: "", userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond, respondChoices: data.respondChoices });
|
|
20158
|
+
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
20159
|
try {
|
|
20051
20160
|
await data.respondChoices(t("model.menu", `**${currentDisplay}**`), choices);
|
|
20052
20161
|
}
|
|
@@ -20082,7 +20191,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20082
20191
|
}
|
|
20083
20192
|
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
20084
20193
|
timer.unref?.();
|
|
20085
|
-
this.pendingModelSelects.set(nonce, { instanceName, model: "", userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20194
|
+
this.pendingModelSelects.set(nonce, { instanceName, instanceCurrent: this.lifecyclePin(instanceName), model: "", userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20086
20195
|
try {
|
|
20087
20196
|
const menuMessageId = await adapter.promptUser(chatId, t("model.menu", currentDisplay), choices, { threadId });
|
|
20088
20197
|
const pending = this.pendingModelSelects.get(nonce);
|
|
@@ -20255,6 +20364,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20255
20364
|
const pending = this.pendingModelSelects.get(match[1]);
|
|
20256
20365
|
if (!pending)
|
|
20257
20366
|
return true;
|
|
20367
|
+
// #1490 row 27: the menu was for one lifecycle of the instance; stopped, restarted or replaced since, it is stale.
|
|
20368
|
+
if (!pending.instanceCurrent()) {
|
|
20369
|
+
this.pendingModelSelects.delete(match[1]);
|
|
20370
|
+
clearTimeout(pending.timer);
|
|
20371
|
+
data.ack?.(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName)));
|
|
20372
|
+
await pending.respond(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))).catch(() => { });
|
|
20373
|
+
return true;
|
|
20374
|
+
}
|
|
20258
20375
|
// The admin who opened the menu, through the adapter that posted it, in the same channel — and still an admin
|
|
20259
20376
|
// when they click (#754 audit): the menu lives for a minute, and a click carries its own callback data.
|
|
20260
20377
|
const cbChannel = data.threadId ?? data.chatId;
|
|
@@ -20315,17 +20432,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20315
20432
|
let result;
|
|
20316
20433
|
try {
|
|
20317
20434
|
// 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
|
-
|
|
20435
|
+
result = !pending.instanceCurrent() ? t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))
|
|
20436
|
+
: this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)
|
|
20437
|
+
? await withSpawnHoldReply(async (text) => {
|
|
20438
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
20439
|
+
if (progressMsgId)
|
|
20440
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressMsgId, text, pending.adapterThreadId);
|
|
20441
|
+
else
|
|
20442
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
20443
|
+
}
|
|
20323
20444
|
else
|
|
20324
|
-
await pending.
|
|
20325
|
-
}
|
|
20326
|
-
else
|
|
20327
|
-
await pending.respond(text);
|
|
20328
|
-
}, () => this.applyModel(pending.instanceName, model)) : t("menu.click_stale");
|
|
20445
|
+
await pending.respond(text);
|
|
20446
|
+
}, () => this.applyModel(pending.instanceName, model, { current: pending.instanceCurrent })) : t("menu.click_stale");
|
|
20329
20447
|
}
|
|
20330
20448
|
catch (err) {
|
|
20331
20449
|
this.logger.error({ err, instance: pending.instanceName, model }, "Model switch failed");
|
|
@@ -20418,7 +20536,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20418
20536
|
* restart backends only read it at launch, so it is persisted and the
|
|
20419
20537
|
* instance respawns.
|
|
20420
20538
|
*/
|
|
20421
|
-
async applyEffort(instanceName, requested) {
|
|
20539
|
+
async applyEffort(instanceName, requested, opts) {
|
|
20540
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
20541
|
+
if (opts?.current && !opts.current())
|
|
20542
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20422
20543
|
const level = requested.trim().toLowerCase();
|
|
20423
20544
|
const backendName = this.backendNameForInstance(instanceName);
|
|
20424
20545
|
let strategy = "unsupported";
|
|
@@ -20453,10 +20574,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20453
20574
|
this.pasteRawToClassicInstance(instanceName, `/effort ${applied}`);
|
|
20454
20575
|
return `${warn}${t("effort.runtime_success", instanceName, applied)}`;
|
|
20455
20576
|
}
|
|
20456
|
-
await this.restartSingleInstance(instanceName);
|
|
20577
|
+
await this.restartSingleInstance(instanceName, { ownerCurrent: opts?.current });
|
|
20457
20578
|
return `${warn}${t("effort.restart_success", instanceName, applied)}`;
|
|
20458
20579
|
}
|
|
20459
|
-
async applyModel(instanceName, model) {
|
|
20580
|
+
async applyModel(instanceName, model, opts) {
|
|
20581
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first, after the runtime switch's
|
|
20582
|
+
// await, and fenced into the restart.
|
|
20583
|
+
if (opts?.current && !opts.current())
|
|
20584
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20460
20585
|
// Reject model names with newlines or control characters: they would be
|
|
20461
20586
|
// persisted to fleet.yaml and pasted raw into the CLI (P3 from #1490 audit).
|
|
20462
20587
|
if (/[\x00-\x1f\x7f]/.test(model)) {
|
|
@@ -20482,6 +20607,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20482
20607
|
// #1690: claude-code's runtime switch reports what it came to; the model is saved unless claude was told No.
|
|
20483
20608
|
if (strategy === "runtime" && backendName === "claude-code") {
|
|
20484
20609
|
const result = await this.runRuntimeModelSwitch(instanceName, model);
|
|
20610
|
+
if (opts?.current && !opts.current())
|
|
20611
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20485
20612
|
if (result.outcome === "mismatch")
|
|
20486
20613
|
return `${warn}${t("model.switch_mismatch", instanceName, result.shown, model)}`;
|
|
20487
20614
|
if (result.outcome === "not_sent")
|
|
@@ -20502,7 +20629,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20502
20629
|
this.pasteRawToClassicInstance(instanceName, `/model ${model}`);
|
|
20503
20630
|
return `${warn}${t("model.runtime_success", instanceName, model)}${this.effortSuffix(instanceName)}`;
|
|
20504
20631
|
}
|
|
20505
|
-
await this.restartSingleInstance(instanceName);
|
|
20632
|
+
await this.restartSingleInstance(instanceName, { ownerCurrent: opts?.current });
|
|
20506
20633
|
return `${warn}${t("model.restart_success", instanceName, model)}${this.effortSuffix(instanceName)}`;
|
|
20507
20634
|
}
|
|
20508
20635
|
/** Save an instance's model (fleet.yaml, or its ClassicBot channel). False when neither holds it. */
|
|
@@ -21435,6 +21562,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21435
21562
|
}
|
|
21436
21563
|
this.eventLoopWatch?.stop();
|
|
21437
21564
|
this.eventLoopWatch = null;
|
|
21565
|
+
this.stallReporter?.stop();
|
|
21566
|
+
this.stallReporter = null;
|
|
21438
21567
|
// A login/install window is a dedicated tmux server with its own TTL
|
|
21439
21568
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
21440
21569
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|