@songsid/agend 2.1.5-beta.13 → 2.1.5-beta.14
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/kiro.js +24 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/daemon.d.ts +16 -0
- package/dist/daemon.js +27 -0
- 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 +75 -13
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +5 -0
- package/dist/instance-lifecycle.js +12 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +12 -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/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:";
|
|
@@ -6555,6 +6556,41 @@ export class FleetManager {
|
|
|
6555
6556
|
extra: { generalName, promptKind: kind },
|
|
6556
6557
|
});
|
|
6557
6558
|
}
|
|
6559
|
+
/**
|
|
6560
|
+
* Offer a one-tap re-login next to an auth alert.
|
|
6561
|
+
*
|
|
6562
|
+
* The alert already names the remedy in words (`/login <backend>`), which
|
|
6563
|
+
* still leaves the user to retype it somewhere. The button routes into the
|
|
6564
|
+
* same chooser `/login` uses, so pressing it starts the flow in place.
|
|
6565
|
+
*
|
|
6566
|
+
* Backends with no remote login flow (opencode logs in from a terminal) get
|
|
6567
|
+
* no button — the alert's own wording already tells them what to run.
|
|
6568
|
+
*/
|
|
6569
|
+
async offerBackendLogin(targetInstance, backend) {
|
|
6570
|
+
if (!LOGIN_FLOWS[backend])
|
|
6571
|
+
return;
|
|
6572
|
+
const adapterId = this.getInstanceAdapterId(targetInstance);
|
|
6573
|
+
const adapter = this.getAdapterForInstance(targetInstance);
|
|
6574
|
+
const chatId = this.getGroupIdForInstance(targetInstance);
|
|
6575
|
+
const topicId = this.fleetConfig?.instances[targetInstance]?.topic_id;
|
|
6576
|
+
const threadId = topicId != null ? String(topicId) : undefined;
|
|
6577
|
+
if (!adapter || !adapterId || !chatId) {
|
|
6578
|
+
this.logger.warn({ targetInstance, backend, adapterId, chatId }, "Cannot address the re-login button — the alert text still names the command");
|
|
6579
|
+
return;
|
|
6580
|
+
}
|
|
6581
|
+
await this.postNonceButtonPrompt({
|
|
6582
|
+
prefix: LOGIN_CALLBACK_PREFIX,
|
|
6583
|
+
alertType: "login",
|
|
6584
|
+
instanceName: "login",
|
|
6585
|
+
adapter,
|
|
6586
|
+
adapterId,
|
|
6587
|
+
chatId,
|
|
6588
|
+
threadId,
|
|
6589
|
+
message: t("login.offer", backend),
|
|
6590
|
+
choices: [{ action: backend, label: t("login.offer_action", backend) }],
|
|
6591
|
+
expiredText: t("buttons.stale"),
|
|
6592
|
+
});
|
|
6593
|
+
}
|
|
6558
6594
|
/** Consume a General assist button exactly once. */
|
|
6559
6595
|
async handleInteractivePromptAssist(data, callbackAdapterId, receivingAdapter) {
|
|
6560
6596
|
const claimed = this.consumeNonceCallback(INTERACTIVE_ASSIST_CALLBACK_PREFIX, /^interactive-assist:([0-9a-f]+):(confirm|cancel)$/, data, callbackAdapterId, receivingAdapter);
|
|
@@ -7543,11 +7579,15 @@ export class FleetManager {
|
|
|
7543
7579
|
if (cleanupFailed) {
|
|
7544
7580
|
await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
|
|
7545
7581
|
}
|
|
7582
|
+
const send = (text) => chat.adapter.sendText(chat.chatId, text, { threadId: chat.threadId }).catch(() => { });
|
|
7546
7583
|
let text;
|
|
7547
7584
|
if (ok) {
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7585
|
+
// Same order as the web-login path: the login result goes out first,
|
|
7586
|
+
// then the recovery reports its own outcome. Waiting for recovery to
|
|
7587
|
+
// build this message is what made a successful login look hung.
|
|
7588
|
+
await send(t("login.completed", backend));
|
|
7589
|
+
await announcePostLoginRecovery(backend, () => this.recoverBackendInstances(backend), send);
|
|
7590
|
+
return;
|
|
7551
7591
|
}
|
|
7552
7592
|
else if (detail === "cancelled") {
|
|
7553
7593
|
// The cancel command's own reply already announced this — a second
|
|
@@ -7671,31 +7711,53 @@ export class FleetManager {
|
|
|
7671
7711
|
* only re-reads credentials on process start (a paused instance's CLI is
|
|
7672
7712
|
* already dead, so waking it respawns with the new token for free).
|
|
7673
7713
|
*/
|
|
7674
|
-
|
|
7714
|
+
/**
|
|
7715
|
+
* Wake/restart every instance of a backend after a successful re-login.
|
|
7716
|
+
*
|
|
7717
|
+
* Bounded by a wall-clock deadline. This used to be an unbounded sequential
|
|
7718
|
+
* loop, and the caller only built its "login completed" message AFTER it
|
|
7719
|
+
* returned — so with several instances (or one slow restart) the user was
|
|
7720
|
+
* told nothing at all for minutes, concluded the login had hung, and
|
|
7721
|
+
* restarted things by hand. Instances that do not finish in time are NOT
|
|
7722
|
+
* cancelled: they are still coming back, and are reported as pending so the
|
|
7723
|
+
* message can say so instead of implying failure.
|
|
7724
|
+
*/
|
|
7725
|
+
async recoverBackendInstances(backend, deadlineMs = POST_LOGIN_RECOVERY_DEADLINE_MS) {
|
|
7675
7726
|
const woken = [];
|
|
7676
7727
|
const restarted = [];
|
|
7728
|
+
const pending = [];
|
|
7729
|
+
const deadline = Date.now() + deadlineMs;
|
|
7677
7730
|
for (const name of this.configuredBackendInstanceNames()) {
|
|
7678
7731
|
if (this.backendNameOf(name) !== backend)
|
|
7679
7732
|
continue;
|
|
7680
7733
|
const status = this.getInstanceStatus(name);
|
|
7681
|
-
|
|
7734
|
+
if (status !== "paused" && status !== "running")
|
|
7735
|
+
continue;
|
|
7736
|
+
const result = await runBeforeDeadline(async () => {
|
|
7682
7737
|
if (status === "paused") {
|
|
7683
7738
|
if (this.daemons.has(name))
|
|
7684
7739
|
await this.lifecycle.wake(name, 30_000);
|
|
7685
7740
|
else
|
|
7686
7741
|
await this.startPersistedPausedInstance(name);
|
|
7687
|
-
woken.push(name);
|
|
7688
7742
|
}
|
|
7689
|
-
else
|
|
7743
|
+
else {
|
|
7690
7744
|
await this.restartSingleInstance(name);
|
|
7691
|
-
restarted.push(name);
|
|
7692
7745
|
}
|
|
7746
|
+
}, deadline);
|
|
7747
|
+
if (result.status === "fulfilled") {
|
|
7748
|
+
(status === "paused" ? woken : restarted).push(name);
|
|
7693
7749
|
}
|
|
7694
|
-
|
|
7695
|
-
|
|
7750
|
+
else if (result.status === "timeout") {
|
|
7751
|
+
// Out of time: record it and stop waiting, but keep walking the list —
|
|
7752
|
+
// the remaining instances are checked against the same deadline and
|
|
7753
|
+
// fall straight through, so the caller still learns about all of them.
|
|
7754
|
+
pending.push(name);
|
|
7755
|
+
}
|
|
7756
|
+
else {
|
|
7757
|
+
this.logger.warn({ err: result.reason?.message, name, status }, "Post-login recovery failed");
|
|
7696
7758
|
}
|
|
7697
7759
|
}
|
|
7698
|
-
return { woken, restarted };
|
|
7760
|
+
return { woken, restarted, pending };
|
|
7699
7761
|
}
|
|
7700
7762
|
/** Backend chooser button → start that backend's login session. */
|
|
7701
7763
|
async handleLoginBackendSelect(data, callbackAdapterId, receivingAdapter) {
|