@songsid/agend 2.1.2-beta.43 → 2.1.2-beta.45
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.js +1 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +27 -0
- package/dist/daemon.js +80 -2
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +10 -1
- package/dist/fleet-manager.js +82 -3
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +8 -0
- package/dist/instance-lifecycle.js +41 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +60 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -236,6 +236,8 @@ export class FleetManager {
|
|
|
236
236
|
instanceActivity = new Map();
|
|
237
237
|
/** instanceName → tail of deliveries waiting for its IPC to come back. */
|
|
238
238
|
ipcWaitTails = new Map();
|
|
239
|
+
/** instanceName → restart currently executing; concurrent callers join it. */
|
|
240
|
+
restartsInFlight = new Map();
|
|
239
241
|
// Last user message delivered to each instance — used to react ✅ on completion.
|
|
240
242
|
lastInboundMsg = new Map();
|
|
241
243
|
topicArchiver;
|
|
@@ -588,7 +590,25 @@ export class FleetManager {
|
|
|
588
590
|
getInstanceStatus(name) {
|
|
589
591
|
if (this.lifecycle.isPaused(name))
|
|
590
592
|
return "paused";
|
|
593
|
+
const daemon = this.lifecycle.daemons.get(name);
|
|
591
594
|
const processStatus = this.instanceProcessStatus.get(name);
|
|
595
|
+
// IPC can be disconnected during a respawn, so the event which announces
|
|
596
|
+
// the new live pane may be missed. The in-process daemon is authoritative
|
|
597
|
+
// in that case; do not leave a stale `crashed` cache masking an instance
|
|
598
|
+
// that has already recovered and can answer messages.
|
|
599
|
+
const daemonStatus = daemon?.getProcessStatus?.();
|
|
600
|
+
if (daemonStatus === "running") {
|
|
601
|
+
if (processStatus)
|
|
602
|
+
this.instanceProcessStatus.delete(name);
|
|
603
|
+
// A recovered in-process daemon is also the authority for clearing a
|
|
604
|
+
// marker left by a crash-loop/reconnect race. This keeps standalone
|
|
605
|
+
// `agend ls` from seeing the old marker after the next API outage.
|
|
606
|
+
try {
|
|
607
|
+
unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
|
|
608
|
+
}
|
|
609
|
+
catch { /* absent */ }
|
|
610
|
+
return "running";
|
|
611
|
+
}
|
|
592
612
|
if (processStatus)
|
|
593
613
|
return processStatus;
|
|
594
614
|
const pidPath = join(this.getInstanceDir(name), "daemon.pid");
|
|
@@ -670,6 +690,13 @@ export class FleetManager {
|
|
|
670
690
|
cacheInstanceProcessStatus(name, status) {
|
|
671
691
|
if (status === "running") {
|
|
672
692
|
this.instanceProcessStatus.delete(name);
|
|
693
|
+
// A prior crash-loop marker is one-shot. Successful respawn is the
|
|
694
|
+
// authoritative recovery signal even when the marker outlived an IPC
|
|
695
|
+
// disconnect and was not consumed by a new Daemon constructor.
|
|
696
|
+
try {
|
|
697
|
+
unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
|
|
698
|
+
}
|
|
699
|
+
catch { /* absent */ }
|
|
673
700
|
return;
|
|
674
701
|
}
|
|
675
702
|
if (status !== "crashed" && status !== "stopped")
|
|
@@ -901,11 +928,41 @@ export class FleetManager {
|
|
|
901
928
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
902
929
|
await this.startClassicInstance(instanceName, this.classicChannels.getBackendByInstance(instanceName, this.fleetConfig?.defaults?.backend), this.classicChannels.getPreTaskCommand(channel.channelId, channel.adapterId), this.classicChannels.getModel(channel.channelId, channel.adapterId, this.fleetConfig?.defaults?.model), this.classicChannels.getAutoPauseAfter(channel.channelId, channel.adapterId, this.fleetConfig?.defaults?.auto_pause_after));
|
|
903
930
|
}
|
|
904
|
-
async startInstance(name, config, topicMode, kind = "fleet-topic"
|
|
931
|
+
async startInstance(name, config, topicMode, kind = "fleet-topic",
|
|
932
|
+
/**
|
|
933
|
+
* Explicit starts (CLI/API) may resume a paused or failed daemon. Startup
|
|
934
|
+
* and reconcile calls leave this false so a persisted pause remains paused
|
|
935
|
+
* across a fleet restart.
|
|
936
|
+
*/
|
|
937
|
+
resumePaused = false) {
|
|
938
|
+
if (resumePaused && this.lifecycle.isPaused(name)) {
|
|
939
|
+
await this.lifecycle.wake(name, 30_000);
|
|
940
|
+
// A successful wake clears the persisted pause marker and produces a
|
|
941
|
+
// fresh instance_state snapshot. Drop any stale process error left by a
|
|
942
|
+
// pre-pause crash so /api/fleet and `agend ls` converge on running.
|
|
943
|
+
this.instanceProcessStatus.delete(name);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
905
946
|
if (this.lifecycle.isPaused(name)) {
|
|
906
947
|
this.logger.info({ name }, "Persisted paused instance — skipping startup");
|
|
907
948
|
return;
|
|
908
949
|
}
|
|
950
|
+
if (this.lifecycle.daemons.has(name)) {
|
|
951
|
+
// A crash-loop daemon remains in the lifecycle map so its health monitor
|
|
952
|
+
// can expose the failure. The old start path treated that object as
|
|
953
|
+
// already running and merely deleted the process-status cache, leaving a
|
|
954
|
+
// dead pane (and crash marker) behind. An explicit start is a recovery
|
|
955
|
+
// request: tear down the failed daemon and build a fresh one.
|
|
956
|
+
if (resumePaused) {
|
|
957
|
+
const status = this.getInstanceStatus(name);
|
|
958
|
+
if (status === "crashed" || status === "stopped") {
|
|
959
|
+
await this.restartSingleInstance(name);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
this.logger.info({ name }, "Instance already running, skipping");
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
909
966
|
if (config.general_topic) {
|
|
910
967
|
// antigravity (agy) does not read MCP instructions — fleet context and
|
|
911
968
|
// routing instructions are not injected, so it cannot act as a dispatcher.
|
|
@@ -916,12 +973,19 @@ export class FleetManager {
|
|
|
916
973
|
}
|
|
917
974
|
this.ensureGeneralInstructions(config.working_directory, config.backend);
|
|
918
975
|
}
|
|
919
|
-
this.instanceProcessStatus.delete(name);
|
|
920
976
|
await this.lifecycle.start(name, config, topicMode, {
|
|
921
977
|
kind,
|
|
922
978
|
backend: config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code",
|
|
923
979
|
model: this.resolveInstanceModel(name).display,
|
|
924
980
|
});
|
|
981
|
+
// Only clear a stale process status after a real start succeeded. Clearing
|
|
982
|
+
// it before lifecycle.start() can turn a crash-loop daemon's dead pane into
|
|
983
|
+
// a falsely running instance when lifecycle.start() returns early.
|
|
984
|
+
this.instanceProcessStatus.delete(name);
|
|
985
|
+
try {
|
|
986
|
+
unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
|
|
987
|
+
}
|
|
988
|
+
catch { /* consumed or absent */ }
|
|
925
989
|
// Auto-connect IPC — daemon.start() ensures socket is ready before resolving
|
|
926
990
|
await this.connectIpcToInstance(name);
|
|
927
991
|
}
|
|
@@ -1030,6 +1094,21 @@ export class FleetManager {
|
|
|
1030
1094
|
}
|
|
1031
1095
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
1032
1096
|
async restartSingleInstance(name, opts) {
|
|
1097
|
+
// One restart at a time per instance. Multiple sources can ask concurrently
|
|
1098
|
+
// (MCP revival, /restart, pty_error, model failover); a second stop/start
|
|
1099
|
+
// interleaved with the first tears down the window the first just created.
|
|
1100
|
+
// Later callers join the in-flight restart instead — its opts win.
|
|
1101
|
+
const inFlight = this.restartsInFlight.get(name);
|
|
1102
|
+
if (inFlight) {
|
|
1103
|
+
this.logger.info({ name }, "restartSingleInstance: joining the restart already in flight");
|
|
1104
|
+
return inFlight;
|
|
1105
|
+
}
|
|
1106
|
+
const run = this.doRestartSingleInstance(name, opts)
|
|
1107
|
+
.finally(() => this.restartsInFlight.delete(name));
|
|
1108
|
+
this.restartsInFlight.set(name, run);
|
|
1109
|
+
return run;
|
|
1110
|
+
}
|
|
1111
|
+
async doRestartSingleInstance(name, opts) {
|
|
1033
1112
|
if (this.configPath) {
|
|
1034
1113
|
this.loadConfig(this.configPath);
|
|
1035
1114
|
this.routing.rebuild(this.fleetConfig);
|
|
@@ -6862,7 +6941,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
6862
6941
|
(async () => {
|
|
6863
6942
|
try {
|
|
6864
6943
|
const topicMode = this.fleetConfig?.channel?.mode === "topic";
|
|
6865
|
-
await this.startInstance(name, config, topicMode ?? false);
|
|
6944
|
+
await this.startInstance(name, config, topicMode ?? false, "fleet-topic", true);
|
|
6866
6945
|
this.emitSseEvent("status", this.getUiStatus());
|
|
6867
6946
|
res.writeHead(200);
|
|
6868
6947
|
res.end(JSON.stringify({ ok: true }));
|