@songsid/agend 2.1.2-beta.42 → 2.1.2-beta.44
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/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +4 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +7 -1
- package/dist/fleet-manager.js +70 -4
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +9 -0
- package/dist/general-instance.js +11 -0
- package/dist/general-instance.js.map +1 -0
- package/dist/instance-lifecycle.js +6 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +65 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/topic-commands.js +8 -0
- package/dist/topic-commands.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -50,6 +50,7 @@ import { validateFleetConfig } from "./config-validator.js";
|
|
|
50
50
|
import { readLastInboundAt } from "./daemon.js";
|
|
51
51
|
import { clearPausedMarker } from "./pause-marker.js";
|
|
52
52
|
import { releaseProcessFleetLock } from "./fleet-lock.js";
|
|
53
|
+
import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
|
|
53
54
|
import { getTmuxSession } from "./config.js";
|
|
54
55
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
55
56
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -587,7 +588,25 @@ export class FleetManager {
|
|
|
587
588
|
getInstanceStatus(name) {
|
|
588
589
|
if (this.lifecycle.isPaused(name))
|
|
589
590
|
return "paused";
|
|
591
|
+
const daemon = this.lifecycle.daemons.get(name);
|
|
590
592
|
const processStatus = this.instanceProcessStatus.get(name);
|
|
593
|
+
// IPC can be disconnected during a respawn, so the event which announces
|
|
594
|
+
// the new live pane may be missed. The in-process daemon is authoritative
|
|
595
|
+
// in that case; do not leave a stale `crashed` cache masking an instance
|
|
596
|
+
// that has already recovered and can answer messages.
|
|
597
|
+
const daemonStatus = daemon?.getProcessStatus?.();
|
|
598
|
+
if (daemonStatus === "running") {
|
|
599
|
+
if (processStatus)
|
|
600
|
+
this.instanceProcessStatus.delete(name);
|
|
601
|
+
// A recovered in-process daemon is also the authority for clearing a
|
|
602
|
+
// marker left by a crash-loop/reconnect race. This keeps standalone
|
|
603
|
+
// `agend ls` from seeing the old marker after the next API outage.
|
|
604
|
+
try {
|
|
605
|
+
unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
|
|
606
|
+
}
|
|
607
|
+
catch { /* absent */ }
|
|
608
|
+
return "running";
|
|
609
|
+
}
|
|
591
610
|
if (processStatus)
|
|
592
611
|
return processStatus;
|
|
593
612
|
const pidPath = join(this.getInstanceDir(name), "daemon.pid");
|
|
@@ -669,6 +688,13 @@ export class FleetManager {
|
|
|
669
688
|
cacheInstanceProcessStatus(name, status) {
|
|
670
689
|
if (status === "running") {
|
|
671
690
|
this.instanceProcessStatus.delete(name);
|
|
691
|
+
// A prior crash-loop marker is one-shot. Successful respawn is the
|
|
692
|
+
// authoritative recovery signal even when the marker outlived an IPC
|
|
693
|
+
// disconnect and was not consumed by a new Daemon constructor.
|
|
694
|
+
try {
|
|
695
|
+
unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
|
|
696
|
+
}
|
|
697
|
+
catch { /* absent */ }
|
|
672
698
|
return;
|
|
673
699
|
}
|
|
674
700
|
if (status !== "crashed" && status !== "stopped")
|
|
@@ -704,7 +730,7 @@ export class FleetManager {
|
|
|
704
730
|
const victims = selectLruEvictions(warm, cap, {
|
|
705
731
|
exclude,
|
|
706
732
|
isEvicting: name => this.warmCapEvicting.has(name),
|
|
707
|
-
isGeneral: name => this.fleetConfig
|
|
733
|
+
isGeneral: name => isGeneralInstance(this.fleetConfig, name),
|
|
708
734
|
isIdle: name => this.getInstanceExecutionState(name) === "idle",
|
|
709
735
|
lastInboundAt: name => readLastInboundAt(this.getInstanceDir(name)) ?? 0,
|
|
710
736
|
});
|
|
@@ -878,6 +904,9 @@ export class FleetManager {
|
|
|
878
904
|
this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
|
|
879
905
|
return "awake";
|
|
880
906
|
}
|
|
907
|
+
if (isGeneralInstance(this.fleetConfig, name)) {
|
|
908
|
+
throw new Error(GENERAL_PAUSE_ERROR);
|
|
909
|
+
}
|
|
881
910
|
await this.lifecycle.pause(name);
|
|
882
911
|
return this.lifecycle.isPaused(name) ? "paused" : "not_idle";
|
|
883
912
|
}
|
|
@@ -897,11 +926,41 @@ export class FleetManager {
|
|
|
897
926
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
898
927
|
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));
|
|
899
928
|
}
|
|
900
|
-
async startInstance(name, config, topicMode, kind = "fleet-topic"
|
|
929
|
+
async startInstance(name, config, topicMode, kind = "fleet-topic",
|
|
930
|
+
/**
|
|
931
|
+
* Explicit starts (CLI/API) may resume a paused or failed daemon. Startup
|
|
932
|
+
* and reconcile calls leave this false so a persisted pause remains paused
|
|
933
|
+
* across a fleet restart.
|
|
934
|
+
*/
|
|
935
|
+
resumePaused = false) {
|
|
936
|
+
if (resumePaused && this.lifecycle.isPaused(name)) {
|
|
937
|
+
await this.lifecycle.wake(name, 30_000);
|
|
938
|
+
// A successful wake clears the persisted pause marker and produces a
|
|
939
|
+
// fresh instance_state snapshot. Drop any stale process error left by a
|
|
940
|
+
// pre-pause crash so /api/fleet and `agend ls` converge on running.
|
|
941
|
+
this.instanceProcessStatus.delete(name);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
901
944
|
if (this.lifecycle.isPaused(name)) {
|
|
902
945
|
this.logger.info({ name }, "Persisted paused instance — skipping startup");
|
|
903
946
|
return;
|
|
904
947
|
}
|
|
948
|
+
if (this.lifecycle.daemons.has(name)) {
|
|
949
|
+
// A crash-loop daemon remains in the lifecycle map so its health monitor
|
|
950
|
+
// can expose the failure. The old start path treated that object as
|
|
951
|
+
// already running and merely deleted the process-status cache, leaving a
|
|
952
|
+
// dead pane (and crash marker) behind. An explicit start is a recovery
|
|
953
|
+
// request: tear down the failed daemon and build a fresh one.
|
|
954
|
+
if (resumePaused) {
|
|
955
|
+
const status = this.getInstanceStatus(name);
|
|
956
|
+
if (status === "crashed" || status === "stopped") {
|
|
957
|
+
await this.restartSingleInstance(name);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
this.logger.info({ name }, "Instance already running, skipping");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
905
964
|
if (config.general_topic) {
|
|
906
965
|
// antigravity (agy) does not read MCP instructions — fleet context and
|
|
907
966
|
// routing instructions are not injected, so it cannot act as a dispatcher.
|
|
@@ -912,12 +971,19 @@ export class FleetManager {
|
|
|
912
971
|
}
|
|
913
972
|
this.ensureGeneralInstructions(config.working_directory, config.backend);
|
|
914
973
|
}
|
|
915
|
-
this.instanceProcessStatus.delete(name);
|
|
916
974
|
await this.lifecycle.start(name, config, topicMode, {
|
|
917
975
|
kind,
|
|
918
976
|
backend: config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code",
|
|
919
977
|
model: this.resolveInstanceModel(name).display,
|
|
920
978
|
});
|
|
979
|
+
// Only clear a stale process status after a real start succeeded. Clearing
|
|
980
|
+
// it before lifecycle.start() can turn a crash-loop daemon's dead pane into
|
|
981
|
+
// a falsely running instance when lifecycle.start() returns early.
|
|
982
|
+
this.instanceProcessStatus.delete(name);
|
|
983
|
+
try {
|
|
984
|
+
unlinkSync(join(this.getInstanceDir(name), "crash-state.json"));
|
|
985
|
+
}
|
|
986
|
+
catch { /* consumed or absent */ }
|
|
921
987
|
// Auto-connect IPC — daemon.start() ensures socket is ready before resolving
|
|
922
988
|
await this.connectIpcToInstance(name);
|
|
923
989
|
}
|
|
@@ -6858,7 +6924,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
6858
6924
|
(async () => {
|
|
6859
6925
|
try {
|
|
6860
6926
|
const topicMode = this.fleetConfig?.channel?.mode === "topic";
|
|
6861
|
-
await this.startInstance(name, config, topicMode ?? false);
|
|
6927
|
+
await this.startInstance(name, config, topicMode ?? false, "fleet-topic", true);
|
|
6862
6928
|
this.emitSseEvent("status", this.getUiStatus());
|
|
6863
6929
|
res.writeHead(200);
|
|
6864
6930
|
res.end(JSON.stringify({ ok: true }));
|