@songsid/agend 2.1.5-beta.13 → 2.1.5-beta.15
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/claude-code.d.ts +1 -0
- package/dist/backend/claude-code.js +1 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/kiro.js +24 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +25 -9
- package/dist/daemon.js +312 -96
- package/dist/daemon.js.map +1 -1
- package/dist/deadline.d.ts +23 -0
- package/dist/deadline.js +29 -0
- package/dist/deadline.js.map +1 -0
- package/dist/fleet-manager.d.ts +23 -0
- package/dist/fleet-manager.js +78 -14
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +5 -0
- package/dist/instance-lifecycle.js +42 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +18 -2
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +31 -4
- package/dist/login-controller.js +52 -4
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +29 -0
- package/dist/login-flows.js +48 -1
- package/dist/login-flows.js.map +1 -1
- package/dist/restart-progress.js +3 -21
- package/dist/restart-progress.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +41 -0
- package/dist/turn-reply-guard.js +77 -0
- package/dist/turn-reply-guard.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run work under a wall-clock deadline without cancelling it.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here aborts anything — a tmux spawn or an HTTP call keeps going after
|
|
5
|
+
* its deadline passes; the caller simply stops waiting and can say so. That is
|
|
6
|
+
* the point: a step with no deadline cannot report anything while it hangs, so
|
|
7
|
+
* the user sees silence and assumes the whole operation is stuck (#722, and the
|
|
8
|
+
* post-login recovery that silently swallowed its own success message).
|
|
9
|
+
*
|
|
10
|
+
* Rejections are reported rather than thrown, so one failed step never takes
|
|
11
|
+
* the surrounding loop down with it, and are always observed — an abandoned
|
|
12
|
+
* promise that rejects later must not surface as an unhandled rejection.
|
|
13
|
+
*/
|
|
14
|
+
export type DeadlineResult<T> = {
|
|
15
|
+
status: "fulfilled";
|
|
16
|
+
value: T;
|
|
17
|
+
} | {
|
|
18
|
+
status: "rejected";
|
|
19
|
+
reason: unknown;
|
|
20
|
+
} | {
|
|
21
|
+
status: "timeout";
|
|
22
|
+
};
|
|
23
|
+
export declare function runBeforeDeadline<T>(start: () => Promise<T>, deadline: number): Promise<DeadlineResult<T>>;
|
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Largest delay setTimeout honours; anything above is clamped to 1ms by Node. */
|
|
2
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
3
|
+
export async function runBeforeDeadline(start, deadline) {
|
|
4
|
+
const remaining = deadline - Date.now();
|
|
5
|
+
if (remaining <= 0)
|
|
6
|
+
return { status: "timeout" };
|
|
7
|
+
let work;
|
|
8
|
+
try {
|
|
9
|
+
work = start();
|
|
10
|
+
}
|
|
11
|
+
catch (reason) {
|
|
12
|
+
// A synchronous throw from `start` is a rejection like any other.
|
|
13
|
+
return { status: "rejected", reason };
|
|
14
|
+
}
|
|
15
|
+
let timer;
|
|
16
|
+
const timeout = new Promise(resolve => {
|
|
17
|
+
// Node clamps a delay above the 32-bit maximum to 1ms — so a very distant
|
|
18
|
+
// deadline would fire IMMEDIATELY and report every step as timed out, the
|
|
19
|
+
// exact opposite of what the caller asked for. Cap instead.
|
|
20
|
+
timer = setTimeout(() => resolve({ status: "timeout" }), Math.min(remaining, MAX_TIMER_MS));
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
});
|
|
23
|
+
const settled = work.then(value => ({ status: "fulfilled", value }), reason => ({ status: "rejected", reason }));
|
|
24
|
+
const result = await Promise.race([settled, timeout]);
|
|
25
|
+
if (timer)
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=deadline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadline.js","sourceRoot":"","sources":["../src/deadline.ts"],"names":[],"mappings":"AAkBA,kFAAkF;AAClF,MAAM,YAAY,GAAG,aAAa,CAAC;AAEnC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAI,KAAuB,EAAE,QAAgB;IAClF,MAAM,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACxC,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACjD,IAAI,IAAgB,CAAC;IACrB,IAAI,CAAC;QACH,IAAI,GAAG,KAAK,EAAE,CAAC;IACjB,CAAC;IAAC,OAAO,MAAM,EAAE,CAAC;QAChB,kEAAkE;QAClE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IACxC,CAAC;IACD,IAAI,KAAgD,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAoB,OAAO,CAAC,EAAE;QACvD,0EAA0E;QAC1E,0EAA0E;QAC1E,4DAA4D;QAC5D,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;QAC5F,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CACvB,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EACzC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAC3C,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,IAAI,KAAK;QAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface DeliveryOptions {
|
|
|
66
66
|
/** Test/operational override; normal deliveries use the 60 second backstop. */
|
|
67
67
|
idleTimeoutMs?: number;
|
|
68
68
|
}
|
|
69
|
+
export declare const LOGIN_CALLBACK_PREFIX = "login:";
|
|
69
70
|
/**
|
|
70
71
|
* Upper bound on a live probe driven by `/model`.
|
|
71
72
|
*
|
|
@@ -928,6 +929,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
928
929
|
* contain usernames or secret-adjacent text and stays in daemon.log.
|
|
929
930
|
*/
|
|
930
931
|
notifyInteractivePrompt(instanceName: string, kind: string): Promise<void>;
|
|
932
|
+
/**
|
|
933
|
+
* Offer a one-tap re-login next to an auth alert.
|
|
934
|
+
*
|
|
935
|
+
* The alert already names the remedy in words (`/login <backend>`), which
|
|
936
|
+
* still leaves the user to retype it somewhere. The button routes into the
|
|
937
|
+
* same chooser `/login` uses, so pressing it starts the flow in place.
|
|
938
|
+
*
|
|
939
|
+
* Backends with no remote login flow (opencode logs in from a terminal) get
|
|
940
|
+
* no button — the alert's own wording already tells them what to run.
|
|
941
|
+
*/
|
|
942
|
+
offerBackendLogin(targetInstance: string, backend: string): Promise<void>;
|
|
931
943
|
/** Consume a General assist button exactly once. */
|
|
932
944
|
private handleInteractivePromptAssist;
|
|
933
945
|
/** Send a "🛑 Cancel" button to the instance's topic/channel after delivery. */
|
|
@@ -1188,6 +1200,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1188
1200
|
* only re-reads credentials on process start (a paused instance's CLI is
|
|
1189
1201
|
* already dead, so waking it respawns with the new token for free).
|
|
1190
1202
|
*/
|
|
1203
|
+
/**
|
|
1204
|
+
* Wake/restart every instance of a backend after a successful re-login.
|
|
1205
|
+
*
|
|
1206
|
+
* Bounded by a wall-clock deadline. This used to be an unbounded sequential
|
|
1207
|
+
* loop, and the caller only built its "login completed" message AFTER it
|
|
1208
|
+
* returned — so with several instances (or one slow restart) the user was
|
|
1209
|
+
* told nothing at all for minutes, concluded the login had hung, and
|
|
1210
|
+
* restarted things by hand. Instances that do not finish in time are NOT
|
|
1211
|
+
* cancelled: they are still coming back, and are reported as pending so the
|
|
1212
|
+
* message can say so instead of implying failure.
|
|
1213
|
+
*/
|
|
1191
1214
|
private recoverBackendInstances;
|
|
1192
1215
|
/** Backend chooser button → start that backend's login session. */
|
|
1193
1216
|
private handleLoginBackendSelect;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -48,7 +48,8 @@ import { handleViewRequest, isViewPath } from "./view-api.js";
|
|
|
48
48
|
import { handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
49
49
|
import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES, checkAuthStatus } from "./login-flows.js";
|
|
50
50
|
import { LoginSession } from "./login-manager.js";
|
|
51
|
-
import { LoginController, LOGIN_TOKEN_RESEND_PREFIX } from "./login-controller.js";
|
|
51
|
+
import { LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOGIN_RECOVERY_DEADLINE_MS, announcePostLoginRecovery } from "./login-controller.js";
|
|
52
|
+
import { runBeforeDeadline } from "./deadline.js";
|
|
52
53
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
53
54
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
54
55
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
@@ -219,7 +220,7 @@ const HANG_CALLBACK_PREFIX = "hang:";
|
|
|
219
220
|
const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
220
221
|
const TIP_DISMISS_CALLBACK_PREFIX = "tip-dismiss:";
|
|
221
222
|
const TIP_UNLOCK_CALLBACK_PREFIX = "tip-unlock:";
|
|
222
|
-
const LOGIN_CALLBACK_PREFIX = "login:";
|
|
223
|
+
export const LOGIN_CALLBACK_PREFIX = "login:";
|
|
223
224
|
const INSTALL_CALLBACK_PREFIX = "install-select:";
|
|
224
225
|
const CLASSIC_APPROVE_CALLBACK_PREFIX = "classic-approve:";
|
|
225
226
|
const LOGIN_MENU_CALLBACK_PREFIX = "login-menu:";
|
|
@@ -4785,7 +4786,9 @@ export class FleetManager {
|
|
|
4785
4786
|
// The adapter resolves only after the platform POST returns. Keep
|
|
4786
4787
|
// outward-facing logs/cancel state on the same confirmation boundary:
|
|
4787
4788
|
// a routed-but-failed reply is not a delivered reply.
|
|
4788
|
-
|
|
4789
|
+
// `statusOnly` is daemon-owned envelope metadata, not an MCP argument:
|
|
4790
|
+
// an agent cannot invent it to suppress the normal completion marker.
|
|
4791
|
+
if (!error && result != null && msg.statusOnly !== true) {
|
|
4789
4792
|
try {
|
|
4790
4793
|
this.afterReplyRouted(instanceName, args, senderSessionName);
|
|
4791
4794
|
}
|
|
@@ -6555,6 +6558,41 @@ export class FleetManager {
|
|
|
6555
6558
|
extra: { generalName, promptKind: kind },
|
|
6556
6559
|
});
|
|
6557
6560
|
}
|
|
6561
|
+
/**
|
|
6562
|
+
* Offer a one-tap re-login next to an auth alert.
|
|
6563
|
+
*
|
|
6564
|
+
* The alert already names the remedy in words (`/login <backend>`), which
|
|
6565
|
+
* still leaves the user to retype it somewhere. The button routes into the
|
|
6566
|
+
* same chooser `/login` uses, so pressing it starts the flow in place.
|
|
6567
|
+
*
|
|
6568
|
+
* Backends with no remote login flow (opencode logs in from a terminal) get
|
|
6569
|
+
* no button — the alert's own wording already tells them what to run.
|
|
6570
|
+
*/
|
|
6571
|
+
async offerBackendLogin(targetInstance, backend) {
|
|
6572
|
+
if (!LOGIN_FLOWS[backend])
|
|
6573
|
+
return;
|
|
6574
|
+
const adapterId = this.getInstanceAdapterId(targetInstance);
|
|
6575
|
+
const adapter = this.getAdapterForInstance(targetInstance);
|
|
6576
|
+
const chatId = this.getGroupIdForInstance(targetInstance);
|
|
6577
|
+
const topicId = this.fleetConfig?.instances[targetInstance]?.topic_id;
|
|
6578
|
+
const threadId = topicId != null ? String(topicId) : undefined;
|
|
6579
|
+
if (!adapter || !adapterId || !chatId) {
|
|
6580
|
+
this.logger.warn({ targetInstance, backend, adapterId, chatId }, "Cannot address the re-login button — the alert text still names the command");
|
|
6581
|
+
return;
|
|
6582
|
+
}
|
|
6583
|
+
await this.postNonceButtonPrompt({
|
|
6584
|
+
prefix: LOGIN_CALLBACK_PREFIX,
|
|
6585
|
+
alertType: "login",
|
|
6586
|
+
instanceName: "login",
|
|
6587
|
+
adapter,
|
|
6588
|
+
adapterId,
|
|
6589
|
+
chatId,
|
|
6590
|
+
threadId,
|
|
6591
|
+
message: t("login.offer", backend),
|
|
6592
|
+
choices: [{ action: backend, label: t("login.offer_action", backend) }],
|
|
6593
|
+
expiredText: t("buttons.stale"),
|
|
6594
|
+
});
|
|
6595
|
+
}
|
|
6558
6596
|
/** Consume a General assist button exactly once. */
|
|
6559
6597
|
async handleInteractivePromptAssist(data, callbackAdapterId, receivingAdapter) {
|
|
6560
6598
|
const claimed = this.consumeNonceCallback(INTERACTIVE_ASSIST_CALLBACK_PREFIX, /^interactive-assist:([0-9a-f]+):(confirm|cancel)$/, data, callbackAdapterId, receivingAdapter);
|
|
@@ -7543,11 +7581,15 @@ export class FleetManager {
|
|
|
7543
7581
|
if (cleanupFailed) {
|
|
7544
7582
|
await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
|
|
7545
7583
|
}
|
|
7584
|
+
const send = (text) => chat.adapter.sendText(chat.chatId, text, { threadId: chat.threadId }).catch(() => { });
|
|
7546
7585
|
let text;
|
|
7547
7586
|
if (ok) {
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7587
|
+
// Same order as the web-login path: the login result goes out first,
|
|
7588
|
+
// then the recovery reports its own outcome. Waiting for recovery to
|
|
7589
|
+
// build this message is what made a successful login look hung.
|
|
7590
|
+
await send(t("login.completed", backend));
|
|
7591
|
+
await announcePostLoginRecovery(backend, () => this.recoverBackendInstances(backend), send);
|
|
7592
|
+
return;
|
|
7551
7593
|
}
|
|
7552
7594
|
else if (detail === "cancelled") {
|
|
7553
7595
|
// The cancel command's own reply already announced this — a second
|
|
@@ -7671,31 +7713,53 @@ export class FleetManager {
|
|
|
7671
7713
|
* only re-reads credentials on process start (a paused instance's CLI is
|
|
7672
7714
|
* already dead, so waking it respawns with the new token for free).
|
|
7673
7715
|
*/
|
|
7674
|
-
|
|
7716
|
+
/**
|
|
7717
|
+
* Wake/restart every instance of a backend after a successful re-login.
|
|
7718
|
+
*
|
|
7719
|
+
* Bounded by a wall-clock deadline. This used to be an unbounded sequential
|
|
7720
|
+
* loop, and the caller only built its "login completed" message AFTER it
|
|
7721
|
+
* returned — so with several instances (or one slow restart) the user was
|
|
7722
|
+
* told nothing at all for minutes, concluded the login had hung, and
|
|
7723
|
+
* restarted things by hand. Instances that do not finish in time are NOT
|
|
7724
|
+
* cancelled: they are still coming back, and are reported as pending so the
|
|
7725
|
+
* message can say so instead of implying failure.
|
|
7726
|
+
*/
|
|
7727
|
+
async recoverBackendInstances(backend, deadlineMs = POST_LOGIN_RECOVERY_DEADLINE_MS) {
|
|
7675
7728
|
const woken = [];
|
|
7676
7729
|
const restarted = [];
|
|
7730
|
+
const pending = [];
|
|
7731
|
+
const deadline = Date.now() + deadlineMs;
|
|
7677
7732
|
for (const name of this.configuredBackendInstanceNames()) {
|
|
7678
7733
|
if (this.backendNameOf(name) !== backend)
|
|
7679
7734
|
continue;
|
|
7680
7735
|
const status = this.getInstanceStatus(name);
|
|
7681
|
-
|
|
7736
|
+
if (status !== "paused" && status !== "running")
|
|
7737
|
+
continue;
|
|
7738
|
+
const result = await runBeforeDeadline(async () => {
|
|
7682
7739
|
if (status === "paused") {
|
|
7683
7740
|
if (this.daemons.has(name))
|
|
7684
7741
|
await this.lifecycle.wake(name, 30_000);
|
|
7685
7742
|
else
|
|
7686
7743
|
await this.startPersistedPausedInstance(name);
|
|
7687
|
-
woken.push(name);
|
|
7688
7744
|
}
|
|
7689
|
-
else
|
|
7745
|
+
else {
|
|
7690
7746
|
await this.restartSingleInstance(name);
|
|
7691
|
-
restarted.push(name);
|
|
7692
7747
|
}
|
|
7748
|
+
}, deadline);
|
|
7749
|
+
if (result.status === "fulfilled") {
|
|
7750
|
+
(status === "paused" ? woken : restarted).push(name);
|
|
7693
7751
|
}
|
|
7694
|
-
|
|
7695
|
-
|
|
7752
|
+
else if (result.status === "timeout") {
|
|
7753
|
+
// Out of time: record it and stop waiting, but keep walking the list —
|
|
7754
|
+
// the remaining instances are checked against the same deadline and
|
|
7755
|
+
// fall straight through, so the caller still learns about all of them.
|
|
7756
|
+
pending.push(name);
|
|
7757
|
+
}
|
|
7758
|
+
else {
|
|
7759
|
+
this.logger.warn({ err: result.reason?.message, name, status }, "Post-login recovery failed");
|
|
7696
7760
|
}
|
|
7697
7761
|
}
|
|
7698
|
-
return { woken, restarted };
|
|
7762
|
+
return { woken, restarted, pending };
|
|
7699
7763
|
}
|
|
7700
7764
|
/** Backend chooser button → start that backend's login session. */
|
|
7701
7765
|
async handleLoginBackendSelect(data, callbackAdapterId, receivingAdapter) {
|