@songsid/agend 2.1.6-beta.9 → 2.1.6
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/backend/codex.d.ts +35 -0
- package/dist/backend/codex.js +588 -36
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -0
- package/dist/backend/kiro.js +129 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.d.ts +37 -4
- package/dist/backend/muse.js +150 -61
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +15 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +98 -10
- package/dist/daemon.js +836 -94
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +30 -1
- package/dist/fleet-manager.js +152 -47
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +17 -0
- package/dist/instance-lifecycle.js +118 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +16 -0
- package/dist/locale.js.map +1 -1
- package/dist/muse-usage-relay.d.ts +72 -0
- package/dist/muse-usage-relay.js +431 -0
- package/dist/muse-usage-relay.js.map +1 -0
- package/dist/tmux-manager.d.ts +2 -0
- package/dist/tmux-manager.js +20 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +27 -0
- package/dist/tool-permissions.js +64 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +9 -0
- package/dist/topic-commands.js +36 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/view.html +2 -2
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +86 -9
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +0 -11
- package/dist/usage/usage-api.js +50 -8
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +2 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -292,6 +292,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
292
292
|
private eventLogPruneTimer;
|
|
293
293
|
private logRotateTimer;
|
|
294
294
|
private discordPresenceTimer;
|
|
295
|
+
private discordPresenceEagerTimer;
|
|
296
|
+
private discordPresenceEagerPending;
|
|
295
297
|
private discordPresenceInFlight;
|
|
296
298
|
private static readonly DISCORD_PRESENCE_REFRESH_MS;
|
|
297
299
|
/** Days of event/activity history to keep. */
|
|
@@ -681,6 +683,12 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
681
683
|
startAll(configPath: string): Promise<void>;
|
|
682
684
|
/** Keep Discord profile activity aligned with the same cached usage source as /usage. */
|
|
683
685
|
private startDiscordUsagePresence;
|
|
686
|
+
/**
|
|
687
|
+
* Request one prompt presence refresh after an instance/adapter comes online.
|
|
688
|
+
* Startup can bring several instances up together; a short coalescing window
|
|
689
|
+
* keeps that herd on the existing shared refresh path and one usage fetch.
|
|
690
|
+
*/
|
|
691
|
+
private requestDiscordUsagePresenceRefresh;
|
|
684
692
|
private refreshDiscordUsagePresence;
|
|
685
693
|
/**
|
|
686
694
|
* Delete inbox files older than retentionDays (by mtime). Cleans the shared
|
|
@@ -918,6 +926,19 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
918
926
|
private handleSetDisplayName;
|
|
919
927
|
private handleSetDescription;
|
|
920
928
|
handleScheduleCrudHttp(instance: string, op: string, args: Record<string, unknown>): Promise<unknown>;
|
|
929
|
+
/**
|
|
930
|
+
* The only place an agent's request creates, changes or removes a schedule
|
|
931
|
+
* (#895). The IPC handler (MCP calls, direct channel.sock writes) and the
|
|
932
|
+
* agent endpoint (agent-cli, HTTP agent mode) are adapters over this, so the
|
|
933
|
+
* target check cannot be present on one face and missing on the other — the
|
|
934
|
+
* shape #804 had to close twice.
|
|
935
|
+
*
|
|
936
|
+
* `caller` is the instance the server resolved for the request; any
|
|
937
|
+
* `source` in `args` is ignored, and a schedule's source is always its caller.
|
|
938
|
+
* A refusal is a ToolNotPermittedError: 403 on the agent endpoint, the error
|
|
939
|
+
* of the schedule response on IPC.
|
|
940
|
+
*/
|
|
941
|
+
private performScheduleOp;
|
|
921
942
|
handleDecisionCrudHttp(instance: string, op: string, args: Record<string, unknown>): Promise<unknown>;
|
|
922
943
|
handleTaskCrudHttp(instance: string, args: Record<string, unknown>): Promise<unknown>;
|
|
923
944
|
handleSetDisplayNameHttp(instance: string, name: string): Promise<unknown>;
|
|
@@ -1655,7 +1676,15 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1655
1676
|
private customProviderFor;
|
|
1656
1677
|
/** Ask a backend for its catalog using ONE instance's real config. Never throws. */
|
|
1657
1678
|
private instanceScopedModels;
|
|
1658
|
-
/**
|
|
1679
|
+
/**
|
|
1680
|
+
* Account-wide catalog: probe cache first, live probe when the cache is
|
|
1681
|
+
* missing or older than the same ~1h window `/model` uses (#888).
|
|
1682
|
+
*
|
|
1683
|
+
* Previously this served anything inside the 24h hard TTL, so `list_models`
|
|
1684
|
+
* could answer with a day-old list `/model` had already re-probed past.
|
|
1685
|
+
* The probe runs under the same deadline `/model` uses: a vendor that stops
|
|
1686
|
+
* answering degrades to the previous list, never stalls the tool call.
|
|
1687
|
+
*/
|
|
1659
1688
|
private globalModelCatalog;
|
|
1660
1689
|
/** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
|
|
1661
1690
|
/** Label an effort choice, marking the one currently configured. */
|
package/dist/fleet-manager.js
CHANGED
|
@@ -52,7 +52,7 @@ import { runBeforeDeadline } from "./deadline.js";
|
|
|
52
52
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
53
53
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
54
54
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
55
|
-
import { handleAgentRequest } from "./agent-endpoint.js";
|
|
55
|
+
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
56
56
|
import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
57
57
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
58
58
|
import { validateFleetConfig } from "./config-validator.js";
|
|
@@ -63,7 +63,7 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
|
|
|
63
63
|
import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
|
|
64
64
|
import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
|
|
65
65
|
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
66
|
-
import { mayUseTool, resolveToolSet, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
66
|
+
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
67
67
|
import { decideWebGate, loadOrCreateWebToken, readWebToken } from "./web-auth.js";
|
|
68
68
|
import { fleetLevelDifferences, fleetLevelSignature } from "./fleet-level-config.js";
|
|
69
69
|
import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
|
|
@@ -488,6 +488,8 @@ export class FleetManager {
|
|
|
488
488
|
eventLogPruneTimer = null;
|
|
489
489
|
logRotateTimer = null;
|
|
490
490
|
discordPresenceTimer = null;
|
|
491
|
+
discordPresenceEagerTimer = null;
|
|
492
|
+
discordPresenceEagerPending = false;
|
|
491
493
|
discordPresenceInFlight = null;
|
|
492
494
|
static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
|
|
493
495
|
/** Days of event/activity history to keep. */
|
|
@@ -1839,6 +1841,7 @@ export class FleetManager {
|
|
|
1839
1841
|
catch { /* consumed or absent */ }
|
|
1840
1842
|
// Auto-connect IPC — daemon.start() ensures socket is ready before resolving
|
|
1841
1843
|
await this.connectIpcToInstance(name);
|
|
1844
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
1842
1845
|
}
|
|
1843
1846
|
/** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
|
|
1844
1847
|
async startPersistedPausedInstance(name) {
|
|
@@ -3058,12 +3061,37 @@ export class FleetManager {
|
|
|
3058
3061
|
startDiscordUsagePresence() {
|
|
3059
3062
|
if (this.discordPresenceTimer)
|
|
3060
3063
|
clearInterval(this.discordPresenceTimer);
|
|
3064
|
+
if (this.discordPresenceEagerTimer) {
|
|
3065
|
+
clearTimeout(this.discordPresenceEagerTimer);
|
|
3066
|
+
this.discordPresenceEagerTimer = null;
|
|
3067
|
+
}
|
|
3068
|
+
this.discordPresenceEagerPending = false;
|
|
3061
3069
|
void this.refreshDiscordUsagePresence();
|
|
3062
3070
|
this.discordPresenceTimer = setInterval(() => {
|
|
3063
3071
|
void this.refreshDiscordUsagePresence();
|
|
3064
3072
|
}, FleetManager.DISCORD_PRESENCE_REFRESH_MS);
|
|
3065
3073
|
this.discordPresenceTimer.unref?.();
|
|
3066
3074
|
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Request one prompt presence refresh after an instance/adapter comes online.
|
|
3077
|
+
* Startup can bring several instances up together; a short coalescing window
|
|
3078
|
+
* keeps that herd on the existing shared refresh path and one usage fetch.
|
|
3079
|
+
*/
|
|
3080
|
+
requestDiscordUsagePresenceRefresh() {
|
|
3081
|
+
this.discordPresenceEagerPending = true;
|
|
3082
|
+
// During early fleet startup the interval is not installed yet; its first
|
|
3083
|
+
// refresh in startDiscordUsagePresence already covers all pending starts.
|
|
3084
|
+
if (!this.discordPresenceTimer || this.discordPresenceEagerTimer)
|
|
3085
|
+
return;
|
|
3086
|
+
this.discordPresenceEagerTimer = setTimeout(() => {
|
|
3087
|
+
this.discordPresenceEagerTimer = null;
|
|
3088
|
+
if (!this.discordPresenceEagerPending || !this.discordPresenceTimer)
|
|
3089
|
+
return;
|
|
3090
|
+
this.discordPresenceEagerPending = false;
|
|
3091
|
+
void this.refreshDiscordUsagePresence();
|
|
3092
|
+
}, 50);
|
|
3093
|
+
this.discordPresenceEagerTimer.unref?.();
|
|
3094
|
+
}
|
|
3067
3095
|
refreshDiscordUsagePresence() {
|
|
3068
3096
|
if (this.discordPresenceInFlight)
|
|
3069
3097
|
return this.discordPresenceInFlight;
|
|
@@ -3706,7 +3734,7 @@ export class FleetManager {
|
|
|
3706
3734
|
this.adapter.setChatId(String(fleet.channel.group_id));
|
|
3707
3735
|
}
|
|
3708
3736
|
if (this.discordPresenceTimer && this.adapter.type === "discord") {
|
|
3709
|
-
|
|
3737
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
3710
3738
|
}
|
|
3711
3739
|
this.startTopicCleanupPoller();
|
|
3712
3740
|
// Prune stale external sessions every 5 minutes
|
|
@@ -4051,7 +4079,7 @@ export class FleetManager {
|
|
|
4051
4079
|
adapter.setChatId(String(channelConfig.group_id));
|
|
4052
4080
|
}
|
|
4053
4081
|
if (this.discordPresenceTimer && adapter.type === "discord") {
|
|
4054
|
-
|
|
4082
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
4055
4083
|
}
|
|
4056
4084
|
this.logger.info({ adapterId, type: channelConfig.type }, "Additional adapter started");
|
|
4057
4085
|
}
|
|
@@ -4296,7 +4324,7 @@ export class FleetManager {
|
|
|
4296
4324
|
await adapter.reconnectGateway(previous?.lastError ?? "fleet adapter restart");
|
|
4297
4325
|
this.adapterState.set(id, { status: "connected", retryCount: 0 });
|
|
4298
4326
|
if (this.discordPresenceTimer && adapter.type === "discord") {
|
|
4299
|
-
|
|
4327
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
4300
4328
|
}
|
|
4301
4329
|
this.logger.info({ id }, "Adapter gateway rebuilt successfully");
|
|
4302
4330
|
}
|
|
@@ -4324,7 +4352,7 @@ export class FleetManager {
|
|
|
4324
4352
|
this.logger.info({ id, attempt }, "Adapter restarted successfully");
|
|
4325
4353
|
this.adapterState.set(id, { status: "connected", retryCount: 0 });
|
|
4326
4354
|
if (this.discordPresenceTimer && adapter.type === "discord") {
|
|
4327
|
-
|
|
4355
|
+
this.requestDiscordUsagePresenceRefresh();
|
|
4328
4356
|
}
|
|
4329
4357
|
return;
|
|
4330
4358
|
}
|
|
@@ -5550,36 +5578,18 @@ export class FleetManager {
|
|
|
5550
5578
|
return;
|
|
5551
5579
|
}
|
|
5552
5580
|
try {
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
label: payload.label,
|
|
5566
|
-
timezone: payload.timezone,
|
|
5567
|
-
silent: !!(payload.silent),
|
|
5568
|
-
};
|
|
5569
|
-
result = this.scheduler.create(params);
|
|
5570
|
-
break;
|
|
5571
|
-
}
|
|
5572
|
-
case "fleet_schedule_list":
|
|
5573
|
-
result = this.scheduler.list(payload.target);
|
|
5574
|
-
break;
|
|
5575
|
-
case "fleet_schedule_update":
|
|
5576
|
-
result = this.scheduler.update(payload.id, payload);
|
|
5577
|
-
break;
|
|
5578
|
-
case "fleet_schedule_delete":
|
|
5579
|
-
this.scheduler.delete(payload.id);
|
|
5580
|
-
result = "ok";
|
|
5581
|
-
break;
|
|
5582
|
-
}
|
|
5581
|
+
const op = String(msg.type).replace("fleet_schedule_", "");
|
|
5582
|
+
const result = this.performScheduleOp(instanceName, op, payload, {
|
|
5583
|
+
// The daemon sends its last chat id, which is unset until the instance
|
|
5584
|
+
// has had a chat message — and cross-instance traffic never sets it. So
|
|
5585
|
+
// a worker that only takes delegated tasks, the very instance #895 lets
|
|
5586
|
+
// self-schedule, hit "NOT NULL constraint failed: schedules.reply_chat_id".
|
|
5587
|
+
// No chat means no reply chat, exactly as on the agent endpoint.
|
|
5588
|
+
chatId: meta.chat_id ?? "",
|
|
5589
|
+
threadId: meta.thread_id || null,
|
|
5590
|
+
adapterId: meta.adapter_id || null,
|
|
5591
|
+
silent: !!(payload.silent),
|
|
5592
|
+
});
|
|
5583
5593
|
ipc.send({ type: "fleet_schedule_response", fleetRequestId, result });
|
|
5584
5594
|
}
|
|
5585
5595
|
catch (err) {
|
|
@@ -5715,23 +5725,76 @@ export class FleetManager {
|
|
|
5715
5725
|
async handleScheduleCrudHttp(instance, op, args) {
|
|
5716
5726
|
if (!this.scheduler)
|
|
5717
5727
|
return { error: "Scheduler not available" };
|
|
5728
|
+
if (op !== "create" && op !== "list" && op !== "update" && op !== "delete") {
|
|
5729
|
+
return { error: `Unknown schedule op: ${op}` };
|
|
5730
|
+
}
|
|
5731
|
+
// No bound chat on this path, as before: a schedule made through the agent
|
|
5732
|
+
// endpoint has nowhere of its own to reply.
|
|
5733
|
+
return this.performScheduleOp(instance, op, args, { chatId: "", threadId: null });
|
|
5734
|
+
}
|
|
5735
|
+
/**
|
|
5736
|
+
* The only place an agent's request creates, changes or removes a schedule
|
|
5737
|
+
* (#895). The IPC handler (MCP calls, direct channel.sock writes) and the
|
|
5738
|
+
* agent endpoint (agent-cli, HTTP agent mode) are adapters over this, so the
|
|
5739
|
+
* target check cannot be present on one face and missing on the other — the
|
|
5740
|
+
* shape #804 had to close twice.
|
|
5741
|
+
*
|
|
5742
|
+
* `caller` is the instance the server resolved for the request; any
|
|
5743
|
+
* `source` in `args` is ignored, and a schedule's source is always its caller.
|
|
5744
|
+
* A refusal is a ToolNotPermittedError: 403 on the agent endpoint, the error
|
|
5745
|
+
* of the schedule response on IPC.
|
|
5746
|
+
*/
|
|
5747
|
+
performScheduleOp(caller, op, args, reply) {
|
|
5748
|
+
const scheduler = this.scheduler;
|
|
5749
|
+
// Shape first, so the value the permission decision reads is the value the
|
|
5750
|
+
// scheduler gets. `target` used to be filtered through typeof for the
|
|
5751
|
+
// decision and passed raw to the scheduler, whose `target.startsWith`
|
|
5752
|
+
// then threw a TypeError for `target: 123` (#897). A non-string is refused
|
|
5753
|
+
// — never coerced: turning 123 into "123" would decide an identity
|
|
5754
|
+
// question on a value the caller did not send. `null` means absent, as
|
|
5755
|
+
// omitting it always did, and is removed before the scheduler sees it.
|
|
5756
|
+
if (args.target === null) {
|
|
5757
|
+
args = { ...args };
|
|
5758
|
+
delete args.target;
|
|
5759
|
+
}
|
|
5760
|
+
if (args.target !== undefined && typeof args.target !== "string") {
|
|
5761
|
+
throw new Error(`${op}_schedule: "target" must be an instance name (a string), not ${typeof args.target}.`);
|
|
5762
|
+
}
|
|
5763
|
+
if ((op === "update" || op === "delete") && typeof args.id !== "string") {
|
|
5764
|
+
throw new Error(`${op}_schedule: "id" must be a schedule id (a string) — get one from list_schedules.`);
|
|
5765
|
+
}
|
|
5766
|
+
const requestedTarget = args.target;
|
|
5767
|
+
if (op !== "list") {
|
|
5768
|
+
const profile = resolveToolSet(this.fleetConfig?.instances[caller], caller);
|
|
5769
|
+
const existing = op === "create" ? null : scheduler.get(args.id);
|
|
5770
|
+
const refusal = scheduleOpRefusal(profile, caller, op, { requestedTarget, existing });
|
|
5771
|
+
if (refusal) {
|
|
5772
|
+
this.logger.warn({ instance: caller, profile, op, target: requestedTarget ?? existing?.target, scheduleId: args.id }, "tool-permissions: schedule op refused");
|
|
5773
|
+
throw new ToolNotPermittedError(refusal);
|
|
5774
|
+
}
|
|
5775
|
+
}
|
|
5718
5776
|
switch (op) {
|
|
5719
5777
|
case "create":
|
|
5720
|
-
return
|
|
5778
|
+
return scheduler.create({
|
|
5721
5779
|
cron: args.cron,
|
|
5722
5780
|
at: args.at,
|
|
5723
5781
|
message: args.message,
|
|
5724
|
-
source:
|
|
5725
|
-
|
|
5782
|
+
source: caller,
|
|
5783
|
+
target: requestedTarget || caller,
|
|
5784
|
+
reply_chat_id: reply.chatId,
|
|
5785
|
+
reply_thread_id: reply.threadId,
|
|
5786
|
+
...(reply.adapterId !== undefined ? { reply_adapter_id: reply.adapterId } : {}),
|
|
5726
5787
|
label: args.label,
|
|
5727
5788
|
timezone: args.timezone,
|
|
5789
|
+
...(reply.silent !== undefined ? { silent: reply.silent } : {}),
|
|
5728
5790
|
});
|
|
5729
|
-
case "list":
|
|
5730
|
-
|
|
5791
|
+
case "list":
|
|
5792
|
+
return scheduler.list(requestedTarget);
|
|
5793
|
+
case "update":
|
|
5794
|
+
return scheduler.update(args.id, args);
|
|
5731
5795
|
case "delete":
|
|
5732
|
-
|
|
5796
|
+
scheduler.delete(args.id);
|
|
5733
5797
|
return "ok";
|
|
5734
|
-
default: return { error: `Unknown schedule op: ${op}` };
|
|
5735
5798
|
}
|
|
5736
5799
|
}
|
|
5737
5800
|
async handleDecisionCrudHttp(instance, op, args) {
|
|
@@ -6396,6 +6459,9 @@ export class FleetManager {
|
|
|
6396
6459
|
}
|
|
6397
6460
|
}
|
|
6398
6461
|
}
|
|
6462
|
+
// Capture instance dir BEFORE lifecycle.remove, which deletes the instance
|
|
6463
|
+
// from fleetConfig. (The backend would be unresolvable afterwards.)
|
|
6464
|
+
const instanceDirToClean = this.getInstanceDir(name);
|
|
6399
6465
|
await this.lifecycle.remove(name, authorization);
|
|
6400
6466
|
// Clean up per-instance tracking maps so they don't grow unbounded
|
|
6401
6467
|
// as instances are created and deleted over the lifetime of the fleet.
|
|
@@ -6404,12 +6470,29 @@ export class FleetManager {
|
|
|
6404
6470
|
this.rateLimitWarnedAt.delete(name);
|
|
6405
6471
|
// Clean up statusline watcher + instance directory
|
|
6406
6472
|
this.statuslineWatcher.unwatch(name);
|
|
6473
|
+
// instanceDirToClean and effectiveBackend are captured before lifecycle.remove
|
|
6474
|
+
// (which deletes the instance from fleetConfig).
|
|
6407
6475
|
try {
|
|
6408
|
-
rmSync(
|
|
6476
|
+
rmSync(instanceDirToClean, { recursive: true, force: true });
|
|
6409
6477
|
}
|
|
6410
6478
|
catch (err) {
|
|
6411
6479
|
this.logger.debug({ err, name }, "Instance dir cleanup failed");
|
|
6412
6480
|
}
|
|
6481
|
+
// Remove the codex short home (~/.agend/cx/<hash>/) unconditionally.
|
|
6482
|
+
// (#953). CodexBackend.shortHomeFor is a pure path computation; the existsSync
|
|
6483
|
+
// guard makes this a no-op for instances that never used a short home (non-codex,
|
|
6484
|
+
// or codex instances on a short-enough path). Unconditional also covers instances
|
|
6485
|
+
// whose backend was switched away from codex before deletion — they still have a
|
|
6486
|
+
// short home that would otherwise be inherited by a later same-name instance.
|
|
6487
|
+
try {
|
|
6488
|
+
const { CodexBackend } = await import("./backend/codex.js");
|
|
6489
|
+
const shortHome = CodexBackend.shortHomeFor(instanceDirToClean);
|
|
6490
|
+
if (existsSync(shortHome))
|
|
6491
|
+
rmSync(shortHome, { recursive: true, force: true });
|
|
6492
|
+
}
|
|
6493
|
+
catch (err) {
|
|
6494
|
+
this.logger.debug({ err, name }, "Codex short home cleanup failed");
|
|
6495
|
+
}
|
|
6413
6496
|
}
|
|
6414
6497
|
startStatuslineWatcher(name) {
|
|
6415
6498
|
if (this.lifecycle.isPaused(name))
|
|
@@ -9993,17 +10076,25 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
9993
10076
|
return [];
|
|
9994
10077
|
}
|
|
9995
10078
|
}
|
|
9996
|
-
/**
|
|
10079
|
+
/**
|
|
10080
|
+
* Account-wide catalog: probe cache first, live probe when the cache is
|
|
10081
|
+
* missing or older than the same ~1h window `/model` uses (#888).
|
|
10082
|
+
*
|
|
10083
|
+
* Previously this served anything inside the 24h hard TTL, so `list_models`
|
|
10084
|
+
* could answer with a day-old list `/model` had already re-probed past.
|
|
10085
|
+
* The probe runs under the same deadline `/model` uses: a vendor that stops
|
|
10086
|
+
* answering degrades to the previous list, never stalls the tool call.
|
|
10087
|
+
*/
|
|
9997
10088
|
async globalModelCatalog(backend) {
|
|
9998
10089
|
const cached = this.readCliEnv(backend);
|
|
9999
|
-
if (cached?.models?.length) {
|
|
10090
|
+
if (cached?.models?.length && !this.cliEnvNeedsRefresh(cached)) {
|
|
10000
10091
|
return {
|
|
10001
10092
|
backend, scope: "global", current_model: cached.currentModel ?? null,
|
|
10002
10093
|
models: cached.models, source: "cache",
|
|
10003
10094
|
probed_at: new Date(cached.probedAt).toISOString(),
|
|
10004
10095
|
};
|
|
10005
10096
|
}
|
|
10006
|
-
const env = await this.
|
|
10097
|
+
const env = await this.probeBackendBounded(backend);
|
|
10007
10098
|
if (env?.models?.length) {
|
|
10008
10099
|
return {
|
|
10009
10100
|
backend, scope: "global", current_model: env.currentModel ?? null,
|
|
@@ -10011,6 +10102,15 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
10011
10102
|
probed_at: new Date(env.probedAt).toISOString(),
|
|
10012
10103
|
};
|
|
10013
10104
|
}
|
|
10105
|
+
// Probe failed or timed out: like `/model`, the previous list is still the
|
|
10106
|
+
// best answer — a stale list beats an empty one.
|
|
10107
|
+
if (cached?.models?.length) {
|
|
10108
|
+
return {
|
|
10109
|
+
backend, scope: "global", current_model: cached.currentModel ?? null,
|
|
10110
|
+
models: cached.models, source: "cache",
|
|
10111
|
+
probed_at: new Date(cached.probedAt).toISOString(),
|
|
10112
|
+
};
|
|
10113
|
+
}
|
|
10014
10114
|
// Reported rather than thrown: "we could not enumerate" is a useful answer,
|
|
10015
10115
|
// and the caller can still set a model by name (AgEnD passes it through).
|
|
10016
10116
|
return {
|
|
@@ -10952,6 +11052,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
10952
11052
|
clearInterval(this.discordPresenceTimer);
|
|
10953
11053
|
this.discordPresenceTimer = null;
|
|
10954
11054
|
}
|
|
11055
|
+
if (this.discordPresenceEagerTimer) {
|
|
11056
|
+
clearTimeout(this.discordPresenceEagerTimer);
|
|
11057
|
+
this.discordPresenceEagerTimer = null;
|
|
11058
|
+
}
|
|
11059
|
+
this.discordPresenceEagerPending = false;
|
|
10955
11060
|
// Cancel-button timers were never cleared here. The idle-check interval is not
|
|
10956
11061
|
// unref'd, so it held the event loop open past shutdown and kept retrying
|
|
10957
11062
|
// deletes against an adapter that was already gone.
|