@songsid/agend 2.1.11-beta.1 → 2.1.11-beta.3
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-engine-ledger.d.ts +36 -0
- package/dist/backend/kiro-engine-ledger.js +113 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -0
- package/dist/backend/kiro-kas-store.d.ts +11 -0
- package/dist/backend/kiro-kas-store.js +67 -0
- package/dist/backend/kiro-kas-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +28 -0
- package/dist/backend/kiro-v3-identity.js +213 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -0
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +34 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode-permission.d.ts +49 -0
- package/dist/backend/opencode-permission.js +181 -0
- package/dist/backend/opencode-permission.js.map +1 -0
- package/dist/backend/opencode.d.ts +6 -0
- package/dist/backend/opencode.js +46 -2
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +15 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +26 -25
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +1 -0
- package/dist/channel/ipc-bridge.js +1 -1
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +27 -6
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +99 -0
- package/dist/command-table.js +125 -0
- package/dist/command-table.js.map +1 -0
- package/dist/daemon.d.ts +36 -0
- package/dist/daemon.js +165 -2
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +16 -0
- package/dist/delivery-outbox.js +55 -6
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/e2e-isolation.d.ts +122 -0
- package/dist/e2e-isolation.js +386 -0
- package/dist/e2e-isolation.js.map +1 -0
- package/dist/fleet-manager.d.ts +24 -0
- package/dist/fleet-manager.js +301 -489
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +56 -0
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +14 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +71 -0
- package/dist/kiro-engine-status.js +68 -0
- package/dist/kiro-engine-status.js.map +1 -0
- package/dist/locale.js +16 -4
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.js +10 -0
- package/dist/login-flows.js.map +1 -1
- package/dist/outbound-handlers.js +16 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -0
- package/dist/outbound-schemas.js +3 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +18 -5
- package/dist/paths.js.map +1 -1
- package/dist/tool-permissions.js +4 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +3 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/web-terminal/terminal-input.js +69 -0
- package/dist/ui/web-terminal/terminal.css +6 -0
- package/dist/ui/web-terminal/terminal.html +9 -1
- package/dist/ui/web-terminal/terminal.js +31 -2
- package/dist/update-check.d.ts +40 -2
- package/dist/update-check.js +101 -3
- package/dist/update-check.js.map +1 -1
- package/dist/web-terminal-http.js +1 -0
- package/dist/web-terminal-http.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command table: for every command AgEnD answers, WHERE it applies and WHO may use it.
|
|
3
|
+
*
|
|
4
|
+
* Before this there was no single place to look. A slash command's lock emoji was typed by hand into its
|
|
5
|
+
* description, its gate lived in whichever of two copied handlers dispatched it, and the same command had a
|
|
6
|
+
* different gate in a fleet channel, a ClassicBot channel and as typed text. The audit of #1148 found `/compact`
|
|
7
|
+
* labelled 🔒 and open to everyone, `/stop` open on Discord and admin-only on Telegram, `/save` judged by the wrong
|
|
8
|
+
* kind of admin — all of them the same mistake: the rule and the label were allowed to disagree.
|
|
9
|
+
*
|
|
10
|
+
* This file is pure data plus one pure function, so it can be read, and tested cell by cell, without a client.
|
|
11
|
+
*
|
|
12
|
+
* It sits BEHIND the door in src/slash-authz.ts and never in front of it. The door decides whether the caller may
|
|
13
|
+
* speak here at all (not a DM, an acceptable guild, the access policy of the channel's owner, fail closed). Only
|
|
14
|
+
* then does this ask whether the command applies in this kind of channel and which kind of admin it needs. It can
|
|
15
|
+
* therefore only narrow what the door let through; nothing here is a way around it.
|
|
16
|
+
*
|
|
17
|
+
* Levels
|
|
18
|
+
* - anyone whoever the door admitted (in a fleet channel: someone the access policy hears; in a ClassicBot
|
|
19
|
+
* channel: everyone, as for typed messages)
|
|
20
|
+
* - channel-admin the admin of the channel's own kind: in a fleet channel an explicit fleet admin, in a ClassicBot
|
|
21
|
+
* channel a fleet admin OR a ClassicBot admin (what `isModelAdmin` always meant)
|
|
22
|
+
* - fleet-admin an explicit entry in the INVOKING adapter's `allowed_users`; an empty list means nobody
|
|
23
|
+
* - classic-admin a ClassicBot admin only (`admin_users`)
|
|
24
|
+
* - handler the door has admitted the caller and the table asks nothing more: the command's own handler
|
|
25
|
+
* decides, because the rule is not one level. Only `/start` is like this — an allowlist that is a
|
|
26
|
+
* different list on each platform (see `start` below). A command with this level is not locked.
|
|
27
|
+
*
|
|
28
|
+
* Platforms: the table is read per platform. `scopes` is what the Discord slash dispatcher enforces; `telegram`, where
|
|
29
|
+
* present, holds the cells where Telegram's own handlers gate differently (they are not table-driven yet, and each
|
|
30
|
+
* override is pinned against the real handlers by tests/command-gates-by-platform.test.ts). The audit that produced
|
|
31
|
+
* this table once folded both platforms into one level per command and lost Telegram's side of it; a cell that holds
|
|
32
|
+
* for Discord is not thereby true for Telegram, and "most restrictive wins" is not a way to merge them.
|
|
33
|
+
*
|
|
34
|
+
* Scopes: `fleet` is an instance's own channel, `general` the General dispatcher (also a fleet channel, but some
|
|
35
|
+
* commands differ there), `classic` a ClassicBot channel, `none` a channel AgEnD has no agent in.
|
|
36
|
+
*/
|
|
37
|
+
export type CommandScope = "fleet" | "general" | "classic" | "none";
|
|
38
|
+
export type CommandLevel = "anyone" | "channel-admin" | "fleet-admin" | "classic-admin" | "handler";
|
|
39
|
+
export type Platform = "discord" | "telegram";
|
|
40
|
+
/** A locale key, optionally with arguments (`cmd.admin_required` wants the command name). */
|
|
41
|
+
export type Reply = readonly [key: string, ...args: string[]];
|
|
42
|
+
/**
|
|
43
|
+
* One scope on one platform. `passthrough`: there is no handler for this command here — the typed text is not a
|
|
44
|
+
* command, it goes to the agent like any message (or, in a chat with no agent, is dropped). It is NOT a refusal and
|
|
45
|
+
* grants nothing; it exists so that "Telegram has no such command" is stated instead of being read off Discord's cell.
|
|
46
|
+
*/
|
|
47
|
+
export type PlatformRule = ScopeRule | {
|
|
48
|
+
readonly passthrough: true;
|
|
49
|
+
};
|
|
50
|
+
export type ScopeRule = {
|
|
51
|
+
readonly level: CommandLevel; /** For `handler`: what the handler checks, per platform. */
|
|
52
|
+
readonly note?: string;
|
|
53
|
+
}
|
|
54
|
+
/** The command does not apply in this kind of channel: say so, with this reply, and do nothing. */
|
|
55
|
+
| {
|
|
56
|
+
readonly refuse: Reply;
|
|
57
|
+
};
|
|
58
|
+
export interface CommandSpec {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
/** Appears in the Discord slash menu. */
|
|
61
|
+
readonly slash: boolean;
|
|
62
|
+
/** What the Discord slash dispatcher enforces. */
|
|
63
|
+
readonly scopes: Readonly<Record<CommandScope, ScopeRule>>;
|
|
64
|
+
/** What Telegram's own handlers do, every scope written out (nothing is inherited from `scopes`). */
|
|
65
|
+
readonly telegram: Readonly<Record<CommandScope, PlatformRule>>;
|
|
66
|
+
/** Said when the caller is not at the required level. */
|
|
67
|
+
readonly denied: Reply;
|
|
68
|
+
/** Said instead of `denied` when a fleet-admin command is off because the adapter lists no admins at all. */
|
|
69
|
+
readonly disabled?: Reply;
|
|
70
|
+
}
|
|
71
|
+
export declare const COMMANDS: readonly CommandSpec[];
|
|
72
|
+
export declare function commandSpec(name: string): CommandSpec | undefined;
|
|
73
|
+
/** The rule for one scope on one platform: the Discord column for Discord, the Telegram column for Telegram. */
|
|
74
|
+
export declare function ruleFor(spec: CommandSpec, scope: CommandScope, platform: "discord"): ScopeRule;
|
|
75
|
+
export declare function ruleFor(spec: CommandSpec, scope: CommandScope, platform: Platform): PlatformRule;
|
|
76
|
+
/** True when some scope the command applies in asks for more than `anyone` — what the lock emoji says. */
|
|
77
|
+
export declare function isLocked(spec: CommandSpec): boolean;
|
|
78
|
+
/** The prefix for a slash command's menu description: generated, never typed. Unknown names get none. */
|
|
79
|
+
export declare function slashLock(name: string): string;
|
|
80
|
+
export type FleetAdminState = "ok" | "disabled" | "denied";
|
|
81
|
+
/** What the caller knows about the person; each is asked only if the command's level needs it. */
|
|
82
|
+
export interface CommandChecks {
|
|
83
|
+
fleetAdmin(): FleetAdminState;
|
|
84
|
+
channelAdmin(): boolean;
|
|
85
|
+
classicAdmin(): boolean;
|
|
86
|
+
}
|
|
87
|
+
export type CommandDecision = {
|
|
88
|
+
allow: true;
|
|
89
|
+
} | {
|
|
90
|
+
allow: false;
|
|
91
|
+
reply: Reply;
|
|
92
|
+
};
|
|
93
|
+
/** On Telegram a cell can also be "not a command here" (see `PlatformRule`): nothing to authorize, refuse or run. */
|
|
94
|
+
export type PlatformDecision = CommandDecision | {
|
|
95
|
+
allow: false;
|
|
96
|
+
passthrough: true;
|
|
97
|
+
};
|
|
98
|
+
export declare function decideCommand(spec: CommandSpec, scope: CommandScope, checks: CommandChecks, platform?: "discord"): CommandDecision;
|
|
99
|
+
export declare function decideCommand(spec: CommandSpec, scope: CommandScope, checks: CommandChecks, platform: Platform): PlatformDecision;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const SCOPES = ["fleet", "general", "classic", "none"];
|
|
2
|
+
/** The same level everywhere. */
|
|
3
|
+
const everywhere = (level) => ({ fleet: { level }, general: { level }, classic: { level }, none: { level } });
|
|
4
|
+
/** An agent's own channel (fleet, General, ClassicBot): `level` there, `otherwise` anywhere else. */
|
|
5
|
+
const inAgentChannels = (level, otherwise) => ({ fleet: { level }, general: { level }, classic: { level }, none: { refuse: otherwise } });
|
|
6
|
+
/** ClassicBot channels only: `level` there, `otherwise` everywhere else. */
|
|
7
|
+
const classicOnly = (level, otherwise) => ({ fleet: { refuse: otherwise }, general: { refuse: otherwise }, classic: { level }, none: { refuse: otherwise } });
|
|
8
|
+
const NOT_AUTHORIZED = ["not_authorized"];
|
|
9
|
+
const PERMISSION_DENIED = ["permission.denied"];
|
|
10
|
+
const NO_AGENT = ["classic.no_agent"];
|
|
11
|
+
const NO_AGENT_START = ["classic.no_agent_start"];
|
|
12
|
+
/**
|
|
13
|
+
* The Telegram column, written out for EVERY scope of every command: a Telegram cell is never inherited from Discord,
|
|
14
|
+
* because most of them differ (a command can have no Telegram handler at all). Scopes are the same four: `general` is
|
|
15
|
+
* the General topic, `fleet` an instance's own topic, `classic` a registered ClassicBot chat (private, or a group
|
|
16
|
+
* addressed with `/cmd@bot`), `none` a chat with no agent (an unregistered group or private chat).
|
|
17
|
+
*/
|
|
18
|
+
const PASS = { passthrough: true };
|
|
19
|
+
const ANYONE = { level: "anyone" };
|
|
20
|
+
const FLEET_ADMIN = { level: "fleet-admin" };
|
|
21
|
+
const CLASSIC_ADMIN = { level: "classic-admin" };
|
|
22
|
+
const CHANNEL_ADMIN = { level: "channel-admin" };
|
|
23
|
+
/** A chat with no agent: the command exists on Telegram and checks the caller, then finds nothing to act on — nobody gets it. */
|
|
24
|
+
const NOBODY_NO_AGENT = { refuse: NO_AGENT };
|
|
25
|
+
const NOBODY_NO_AGENT_START = { refuse: NO_AGENT_START };
|
|
26
|
+
const HANDLER = { level: "handler", note: "Telegram private: user allowlist. Telegram group: group allowlist + ClassicBot admin." };
|
|
27
|
+
const tg = (general, fleet, classic, none = PASS) => ({ general, fleet, classic, none });
|
|
28
|
+
/** Only the General topic has a handler: everywhere else the typed text is an ordinary message to the agent. */
|
|
29
|
+
const tgGeneralOnly = (general) => tg(general, PASS, PASS);
|
|
30
|
+
export const COMMANDS = [
|
|
31
|
+
// ── ClassicBot lifecycle ──
|
|
32
|
+
{
|
|
33
|
+
name: "start", slash: true, denied: NOT_AUTHORIZED,
|
|
34
|
+
// Not one level. Discord: the guild allowlist (`isGuildAllowed`) and nothing else — no admin. Telegram private chat:
|
|
35
|
+
// `isUserAllowed`. Telegram group: `isGroupAllowed` AND a ClassicBot admin. The handler enforces each (the table
|
|
36
|
+
// only says where it applies), so the single word "anyone" that stood here was wrong for Telegram groups.
|
|
37
|
+
scopes: {
|
|
38
|
+
fleet: { refuse: ["classic.topic_bound"] }, general: { refuse: ["classic.topic_bound"] }, classic: { refuse: ["classic.already_active"] },
|
|
39
|
+
none: { level: "handler", note: "Discord: guild allowlist. Telegram private: user allowlist. Telegram group: group allowlist + ClassicBot admin." },
|
|
40
|
+
},
|
|
41
|
+
telegram: tg(PASS, PASS, HANDLER, HANDLER),
|
|
42
|
+
},
|
|
43
|
+
// A ClassicBot admin on both platforms. Telegram always required one; Discord used to require nothing ("the guild
|
|
44
|
+
// allowlist is the trust boundary"), then briefly a channel admin, which let a fleet admin stop a channel Telegram
|
|
45
|
+
// would not have let them stop.
|
|
46
|
+
{ name: "stop", slash: true, denied: ["classic.admin_only_stop"], scopes: classicOnly("classic-admin", NO_AGENT), telegram: tg(PASS, PASS, CLASSIC_ADMIN, NOBODY_NO_AGENT) },
|
|
47
|
+
{ name: "chat", slash: true, denied: NOT_AUTHORIZED, scopes: classicOnly("anyone", NO_AGENT_START), telegram: tg(PASS, PASS, ANYONE) },
|
|
48
|
+
// Discord only: on Telegram `/load` is not a command (the text goes to the agent).
|
|
49
|
+
{ name: "load", slash: true, denied: ["admin.required"], scopes: classicOnly("classic-admin", NO_AGENT_START), telegram: tg(PASS, PASS, PASS) },
|
|
50
|
+
// ── Per-agent controls ──
|
|
51
|
+
// Telegram's ClassicBot handler asks `isAdmin` (a ClassicBot admin only; a fleet admin alone is refused), where Discord
|
|
52
|
+
// asks `isModelAdmin` (a fleet admin or a ClassicBot admin).
|
|
53
|
+
{ name: "pause", slash: true, denied: PERMISSION_DENIED, scopes: inAgentChannels("channel-admin", NO_AGENT), telegram: tg(FLEET_ADMIN, FLEET_ADMIN, CLASSIC_ADMIN, NOBODY_NO_AGENT_START) },
|
|
54
|
+
{ name: "wake", slash: true, denied: PERMISSION_DENIED, scopes: inAgentChannels("channel-admin", NO_AGENT), telegram: tg(FLEET_ADMIN, FLEET_ADMIN, CLASSIC_ADMIN, NOBODY_NO_AGENT_START) },
|
|
55
|
+
// Discord had no check at all (the 🔒 was only a label). Telegram: a ClassicBot admin in a ClassicBot chat, and NO
|
|
56
|
+
// check in a fleet topic.
|
|
57
|
+
{ name: "compact", slash: true, denied: ["cmd.admin_required", "/compact"], scopes: inAgentChannels("channel-admin", NO_AGENT), telegram: tg(ANYONE, ANYONE, CLASSIC_ADMIN, NOBODY_NO_AGENT_START) },
|
|
58
|
+
{ name: "clear", slash: true, denied: PERMISSION_DENIED, scopes: inAgentChannels("channel-admin", NO_AGENT), telegram: tg(FLEET_ADMIN, FLEET_ADMIN, CHANNEL_ADMIN, NOBODY_NO_AGENT_START) },
|
|
59
|
+
{ name: "model", slash: true, denied: PERMISSION_DENIED, scopes: inAgentChannels("channel-admin", NO_AGENT), telegram: tg(FLEET_ADMIN, FLEET_ADMIN, CHANNEL_ADMIN, NOBODY_NO_AGENT_START) },
|
|
60
|
+
// Telegram has no ClassicBot /effort handler (the text goes to the agent).
|
|
61
|
+
{ name: "effort", slash: true, denied: PERMISSION_DENIED, scopes: inAgentChannels("channel-admin", NO_AGENT), telegram: tg(FLEET_ADMIN, FLEET_ADMIN, PASS) },
|
|
62
|
+
// Telegram's typed /collab has no check in a fleet topic and no handler in a ClassicBot chat (the text goes to the agent).
|
|
63
|
+
{ name: "collab", slash: true, denied: NOT_AUTHORIZED, scopes: inAgentChannels("channel-admin", NO_AGENT_START), telegram: tg(ANYONE, ANYONE, PASS) },
|
|
64
|
+
// Telegram: a ClassicBot admin in a ClassicBot chat, no check in a fleet topic (same as /compact).
|
|
65
|
+
{ name: "save", slash: true, denied: ["admin.required"], scopes: inAgentChannels("channel-admin", NO_AGENT_START), telegram: tg(ANYONE, ANYONE, CLASSIC_ADMIN, NOBODY_NO_AGENT_START) },
|
|
66
|
+
// Anyone who can talk to the agent may talk to it mid-turn, ask a side question, interrupt it or read its context.
|
|
67
|
+
{ name: "steer", slash: true, denied: NOT_AUTHORIZED, scopes: inAgentChannels("anyone", NO_AGENT), telegram: tg(ANYONE, ANYONE, ANYONE, NOBODY_NO_AGENT_START) },
|
|
68
|
+
{ name: "btw", slash: true, denied: NOT_AUTHORIZED, scopes: inAgentChannels("anyone", NO_AGENT), telegram: tg(ANYONE, ANYONE, ANYONE, NOBODY_NO_AGENT_START) },
|
|
69
|
+
{ name: "cancel", slash: true, denied: NOT_AUTHORIZED, scopes: inAgentChannels("anyone", NO_AGENT), telegram: tg(ANYONE, ANYONE, ANYONE, NOBODY_NO_AGENT_START) },
|
|
70
|
+
{ name: "ctx", slash: true, denied: NOT_AUTHORIZED, scopes: inAgentChannels("anyone", NO_AGENT), telegram: tg(ANYONE, ANYONE, ANYONE, NOBODY_NO_AGENT_START) },
|
|
71
|
+
// ── The fleet, from anywhere ── (on Telegram: only the General topic has these handlers)
|
|
72
|
+
{ name: "status", slash: true, denied: ["cmd.admin_required", "/status"], scopes: everywhere("fleet-admin"), telegram: tgGeneralOnly(FLEET_ADMIN) },
|
|
73
|
+
{ name: "restart", slash: true, denied: NOT_AUTHORIZED, scopes: everywhere("fleet-admin"), telegram: tgGeneralOnly(FLEET_ADMIN) },
|
|
74
|
+
{ name: "login", slash: true, denied: PERMISSION_DENIED, scopes: everywhere("fleet-admin"), telegram: tgGeneralOnly(FLEET_ADMIN) },
|
|
75
|
+
{ name: "update", slash: true, denied: NOT_AUTHORIZED, disabled: ["update.disabled"], scopes: everywhere("fleet-admin"), telegram: tgGeneralOnly(FLEET_ADMIN) },
|
|
76
|
+
{ name: "doctor", slash: true, denied: NOT_AUTHORIZED, scopes: everywhere("fleet-admin"), telegram: tgGeneralOnly(FLEET_ADMIN) },
|
|
77
|
+
{ name: "dashboard", slash: true, denied: NOT_AUTHORIZED, disabled: ["dashboard.disabled"], scopes: everywhere("fleet-admin"), telegram: tgGeneralOnly(FLEET_ADMIN) },
|
|
78
|
+
// Informational. `/tips on|off|advanced on` change settings and are gated by the handler on that argument.
|
|
79
|
+
{ name: "sysinfo", slash: true, denied: NOT_AUTHORIZED, scopes: everywhere("anyone"), telegram: tgGeneralOnly(ANYONE) },
|
|
80
|
+
{ name: "usage", slash: true, denied: NOT_AUTHORIZED, scopes: everywhere("anyone"), telegram: tgGeneralOnly(ANYONE) },
|
|
81
|
+
// On Telegram it also works in an instance's topic.
|
|
82
|
+
{ name: "tips", slash: true, denied: PERMISSION_DENIED, scopes: everywhere("anyone"), telegram: tg(ANYONE, ANYONE, PASS) },
|
|
83
|
+
];
|
|
84
|
+
const BY_NAME = new Map(COMMANDS.map(spec => [spec.name, spec]));
|
|
85
|
+
export function commandSpec(name) {
|
|
86
|
+
return BY_NAME.get(name);
|
|
87
|
+
}
|
|
88
|
+
export function ruleFor(spec, scope, platform) {
|
|
89
|
+
return platform === "telegram" ? spec.telegram[scope] : spec.scopes[scope];
|
|
90
|
+
}
|
|
91
|
+
/** True when some scope the command applies in asks for more than `anyone` — what the lock emoji says. */
|
|
92
|
+
export function isLocked(spec) {
|
|
93
|
+
return SCOPES.some(scope => {
|
|
94
|
+
const rule = spec.scopes[scope];
|
|
95
|
+
return "level" in rule && rule.level !== "anyone" && rule.level !== "handler";
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** The prefix for a slash command's menu description: generated, never typed. Unknown names get none. */
|
|
99
|
+
export function slashLock(name) {
|
|
100
|
+
const spec = BY_NAME.get(name);
|
|
101
|
+
return spec && isLocked(spec) ? "🔒 " : "";
|
|
102
|
+
}
|
|
103
|
+
export function decideCommand(spec, scope, checks, platform = "discord") {
|
|
104
|
+
const rule = ruleFor(spec, scope, platform);
|
|
105
|
+
if ("passthrough" in rule)
|
|
106
|
+
return { allow: false, passthrough: true };
|
|
107
|
+
if ("refuse" in rule)
|
|
108
|
+
return { allow: false, reply: rule.refuse };
|
|
109
|
+
switch (rule.level) {
|
|
110
|
+
case "anyone":
|
|
111
|
+
case "handler":
|
|
112
|
+
return { allow: true };
|
|
113
|
+
case "channel-admin":
|
|
114
|
+
return checks.channelAdmin() ? { allow: true } : { allow: false, reply: spec.denied };
|
|
115
|
+
case "classic-admin":
|
|
116
|
+
return checks.classicAdmin() ? { allow: true } : { allow: false, reply: spec.denied };
|
|
117
|
+
case "fleet-admin": {
|
|
118
|
+
const state = checks.fleetAdmin();
|
|
119
|
+
if (state === "ok")
|
|
120
|
+
return { allow: true };
|
|
121
|
+
return { allow: false, reply: state === "disabled" && spec.disabled ? spec.disabled : spec.denied };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=command-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-table.js","sourceRoot":"","sources":["../src/command-table.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -474,6 +474,12 @@ export declare class Daemon extends EventEmitter {
|
|
|
474
474
|
/** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
|
|
475
475
|
private dialogParkedKey;
|
|
476
476
|
private dialogParkedReported;
|
|
477
|
+
/**
|
|
478
|
+
* The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
|
|
479
|
+
* still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
|
|
480
|
+
* dialog on screen at every poll and ignores nothing.
|
|
481
|
+
*/
|
|
482
|
+
private dialogAnswers;
|
|
477
483
|
/** Current stdin-owning runtime dialog, independent from execution state. */
|
|
478
484
|
private inputBlockedDialogKey;
|
|
479
485
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
@@ -699,6 +705,13 @@ export declare class Daemon extends EventEmitter {
|
|
|
699
705
|
private mcpDeathDeferredForPid;
|
|
700
706
|
/** Prevent in-flight monitor callbacks from re-arming after a pause. */
|
|
701
707
|
private runtimeMonitorsFrozen;
|
|
708
|
+
/**
|
|
709
|
+
* Bumped by every stop / pause / startup abort (they all freeze the monitors). A launch that has to
|
|
710
|
+
* await something before it touches the pane (a backend's `prepareLaunch`) remembers the value it
|
|
711
|
+
* started under and gives up if it moved: a stopped instance must not get a window, a config or a
|
|
712
|
+
* token from a launch that was already in the air. A wake that STARTS after the freeze is unaffected.
|
|
713
|
+
*/
|
|
714
|
+
private launchFenceEpoch;
|
|
702
715
|
private errorWaitingForRecovery;
|
|
703
716
|
private errorDetectedAt;
|
|
704
717
|
private errorRecoveryDeadlineAt;
|
|
@@ -1325,6 +1338,29 @@ export declare class Daemon extends EventEmitter {
|
|
|
1325
1338
|
* within a bound and then fail loudly.
|
|
1326
1339
|
*/
|
|
1327
1340
|
private probeBlockingDialog;
|
|
1341
|
+
/** What identifies the request on screen: the backend's own identity when it has one, else the whole screen verbatim. */
|
|
1342
|
+
private static screenOf;
|
|
1343
|
+
private static answerKey;
|
|
1344
|
+
/**
|
|
1345
|
+
* The episode being counted is over: its count and its report. Its parked clock ends too (`clearParked`) only when the
|
|
1346
|
+
* dialog is gone or a different request is positively identified — not on a difference nobody can explain.
|
|
1347
|
+
*/
|
|
1348
|
+
private endDialogEpisode;
|
|
1349
|
+
/**
|
|
1350
|
+
* A poll that did not complete an answer-and-look (lock busy, keys not sent, nothing to press) breaks "in a row" for
|
|
1351
|
+
* this dialog; if the dialog on screen is a DIFFERENT one, the episode being counted belongs to a screen that is gone.
|
|
1352
|
+
*/
|
|
1353
|
+
private breakAnswerStreak;
|
|
1354
|
+
/**
|
|
1355
|
+
* One answer of the runtime scanner to a dialog, and whether the SAME dialog (same kind, same content) was still
|
|
1356
|
+
* on screen right after it.
|
|
1357
|
+
* An answer that took ends the episode (what shows next is a new dialog, so the parked clock restarts too);
|
|
1358
|
+
* DIALOG_ANSWER_IGNORED_MAX in a row that did not is reported ONCE per episode, for ANY backend's dialog —
|
|
1359
|
+
* not only the ones that hold deliveries — so a human is told in about a quarter of a minute instead of the
|
|
1360
|
+
* instance sitting there until a hang detector (and only with pending work) notices. The event carries the
|
|
1361
|
+
* dialog's static description and a count, never the pane: a pane can hold anything the agent printed.
|
|
1362
|
+
*/
|
|
1363
|
+
private noteDialogAnswer;
|
|
1328
1364
|
/**
|
|
1329
1365
|
* Remember how long a blocking dialog has been on screen. Once it has
|
|
1330
1366
|
* outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
|
package/dist/daemon.js
CHANGED
|
@@ -447,6 +447,18 @@ const STARTUP_DIALOG_POLL_MS = 500;
|
|
|
447
447
|
const STARTUP_DIALOG_BUDGET_MS = 30_000;
|
|
448
448
|
/** A blocking dialog still on screen this long after first being seen is reported for a human. */
|
|
449
449
|
const DIALOG_PARKED_NOTIFY_MS = 60_000;
|
|
450
|
+
/** After a runtime dialog's keys, how long the pane gets to repaint before it is read again to see whether they took. */
|
|
451
|
+
const DIALOG_ANSWER_SETTLE_MS = 300;
|
|
452
|
+
/** Consecutive answers one runtime dialog ignored (still on screen right after the keys) before it is reported for a human. */
|
|
453
|
+
const DIALOG_ANSWER_IGNORED_MAX = 3;
|
|
454
|
+
/**
|
|
455
|
+
* Once a dialog has been reported as ignoring the answer, how long the scanner waits between answers to the SAME request.
|
|
456
|
+
* Not a stop: requests that merely look alike (several queued permission requests for one directory) cannot be told
|
|
457
|
+
* apart from an ignored answer, and each still needs its answer — they get it, slowly — while a dialog that really
|
|
458
|
+
* ignores the keys is pressed far less. A different request, the dialog going away, or a new spawn restores the
|
|
459
|
+
* normal pace (and a fresh count).
|
|
460
|
+
*/
|
|
461
|
+
const DIALOG_ANSWER_BACKOFF_MS = 30_000;
|
|
450
462
|
/** How many "dialog painted just before the write → wait → retry" rounds a delivery tolerates. */
|
|
451
463
|
const LATE_DIALOG_WRITE_ROUNDS = 3;
|
|
452
464
|
/** How many times a delivery redoes itself when a spawn starts between its settle wait and its pane write. */
|
|
@@ -1024,6 +1036,12 @@ export class Daemon extends EventEmitter {
|
|
|
1024
1036
|
/** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
|
|
1025
1037
|
dialogParkedKey = null;
|
|
1026
1038
|
dialogParkedReported = false;
|
|
1039
|
+
/**
|
|
1040
|
+
* The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
|
|
1041
|
+
* still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
|
|
1042
|
+
* dialog on screen at every poll and ignores nothing.
|
|
1043
|
+
*/
|
|
1044
|
+
dialogAnswers = null;
|
|
1027
1045
|
/** Current stdin-owning runtime dialog, independent from execution state. */
|
|
1028
1046
|
inputBlockedDialogKey = null;
|
|
1029
1047
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
@@ -1260,6 +1278,13 @@ export class Daemon extends EventEmitter {
|
|
|
1260
1278
|
mcpDeathDeferredForPid = null;
|
|
1261
1279
|
/** Prevent in-flight monitor callbacks from re-arming after a pause. */
|
|
1262
1280
|
runtimeMonitorsFrozen = false;
|
|
1281
|
+
/**
|
|
1282
|
+
* Bumped by every stop / pause / startup abort (they all freeze the monitors). A launch that has to
|
|
1283
|
+
* await something before it touches the pane (a backend's `prepareLaunch`) remembers the value it
|
|
1284
|
+
* started under and gives up if it moved: a stopped instance must not get a window, a config or a
|
|
1285
|
+
* token from a launch that was already in the air. A wake that STARTS after the freeze is unaffected.
|
|
1286
|
+
*/
|
|
1287
|
+
launchFenceEpoch = 0;
|
|
1263
1288
|
errorWaitingForRecovery = false; // true = error detected, waiting for ready pattern
|
|
1264
1289
|
errorDetectedAt = 0;
|
|
1265
1290
|
errorRecoveryDeadlineAt = 0;
|
|
@@ -2654,7 +2679,7 @@ export class Daemon extends EventEmitter {
|
|
|
2654
2679
|
return false;
|
|
2655
2680
|
this.healthCheckPaused = true;
|
|
2656
2681
|
this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
|
|
2657
|
-
this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.");
|
|
2682
|
+
this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.", undefined, "cli_unsupported");
|
|
2658
2683
|
return true;
|
|
2659
2684
|
}
|
|
2660
2685
|
/**
|
|
@@ -2670,12 +2695,13 @@ export class Daemon extends EventEmitter {
|
|
|
2670
2695
|
* `crash_loop` already had this treatment; this is the same bridge for the other
|
|
2671
2696
|
* four, carrying a human-readable cause and the operator's next step.
|
|
2672
2697
|
*/
|
|
2673
|
-
emitSupervisionEnded(reason, remedy, exitCode) {
|
|
2698
|
+
emitSupervisionEnded(reason, remedy, exitCode, cause) {
|
|
2674
2699
|
this.emit("supervision_ended", {
|
|
2675
2700
|
name: this.name,
|
|
2676
2701
|
reason,
|
|
2677
2702
|
remedy,
|
|
2678
2703
|
...(exitCode !== undefined ? { exitCode } : {}),
|
|
2704
|
+
...(cause ? { cause } : {}),
|
|
2679
2705
|
});
|
|
2680
2706
|
}
|
|
2681
2707
|
setProcessStatus(status) {
|
|
@@ -2725,11 +2751,18 @@ export class Daemon extends EventEmitter {
|
|
|
2725
2751
|
this.errorMonitorTimer = setInterval(async () => {
|
|
2726
2752
|
if (!this.tmux || this.spawning)
|
|
2727
2753
|
return;
|
|
2754
|
+
// This poll belongs to the spawn and the monitors it started under. Every await below can outlive them (a stop,
|
|
2755
|
+
// a pause, a respawn); a stale poll must not touch state a newer one owns, so it checks before committing.
|
|
2756
|
+
const pollSpawn = this.spawnGeneration;
|
|
2757
|
+
const pollFence = this.launchFenceEpoch;
|
|
2758
|
+
const stale = () => pollSpawn !== this.spawnGeneration || pollFence !== this.launchFenceEpoch;
|
|
2728
2759
|
try {
|
|
2729
2760
|
const alive = await this.tmux.isWindowAlive();
|
|
2730
2761
|
if (!alive)
|
|
2731
2762
|
return;
|
|
2732
2763
|
const pane = await this.tmux.capturePane();
|
|
2764
|
+
if (stale())
|
|
2765
|
+
return;
|
|
2733
2766
|
const inputBlockedDialog = this.updateInputBlockedState(pane, dialogs);
|
|
2734
2767
|
// A sign-in screen that appears AFTER the startup scan ended (first run:
|
|
2735
2768
|
// theme → login can take longer than its budget) is the same auth
|
|
@@ -2769,6 +2802,7 @@ export class Daemon extends EventEmitter {
|
|
|
2769
2802
|
// on). Runtime table only: startup patterns are loose on purpose and
|
|
2770
2803
|
// must never send keys into an ordinary transcript.
|
|
2771
2804
|
let blockingSeen = null;
|
|
2805
|
+
let answersBackedOff = false;
|
|
2772
2806
|
for (const dialog of dialogs) {
|
|
2773
2807
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
2774
2808
|
continue;
|
|
@@ -2777,6 +2811,20 @@ export class Daemon extends EventEmitter {
|
|
|
2777
2811
|
blockingSeen = dialog;
|
|
2778
2812
|
if (dialog.holdOnly)
|
|
2779
2813
|
break; // recognised, deliberately not answered; trackDialogParked reports it
|
|
2814
|
+
// Reported as ignoring the daemon: the SAME request (the identity the count was made on — verbatim, the
|
|
2815
|
+
// backend's own when it names one) is answered only once per DIALOG_ANSWER_BACKOFF_MS. Anything else — another
|
|
2816
|
+
// request, an unexplained difference, the dialog gone — is not held back here; the answer path below ends or
|
|
2817
|
+
// keeps the episode by its own rules.
|
|
2818
|
+
const episode = this.dialogAnswers;
|
|
2819
|
+
// A wall clock that moved BACKWARDS since the last answer (NTP step, VM resume) gives a negative elapsed time:
|
|
2820
|
+
// the backoff has expired, not "not started" — held to it, the answers would stop until the clock caught up.
|
|
2821
|
+
const sinceLastAnswer = episode ? Date.now() - episode.lastAnswerAt : 0;
|
|
2822
|
+
if (episode?.reported && episode.key === Daemon.answerKey(dialog)
|
|
2823
|
+
&& episode.screen === Daemon.screenOf(dialog, pane)
|
|
2824
|
+
&& sinceLastAnswer >= 0 && sinceLastAnswer < DIALOG_ANSWER_BACKOFF_MS) {
|
|
2825
|
+
answersBackedOff = true;
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2780
2828
|
const autoKey = dialog.autoResolutionKey;
|
|
2781
2829
|
if (dialog.verifyAfterKeys && autoKey
|
|
2782
2830
|
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
@@ -2796,10 +2844,23 @@ export class Daemon extends EventEmitter {
|
|
|
2796
2844
|
// while a delivery was finishing. A stale danger menu must never
|
|
2797
2845
|
// receive a blind key sequence.
|
|
2798
2846
|
const currentPane = await this.tmux.capturePane();
|
|
2847
|
+
// Every await below can outlive the spawn / monitors this poll started under: after each one, before
|
|
2848
|
+
// any state is touched or any further key is sent, `stale()` (fixed at the poll's start) is asked again.
|
|
2849
|
+
if (stale())
|
|
2850
|
+
return;
|
|
2799
2851
|
if (!Daemon.dialogMatches(dialog, currentPane)) {
|
|
2800
2852
|
this.updateInputBlockedState(currentPane, dialogs);
|
|
2853
|
+
this.endDialogEpisode(); // somebody (or the CLI) already cleared it: that screen is over
|
|
2801
2854
|
return;
|
|
2802
2855
|
}
|
|
2856
|
+
// What is being answered: the request's identity when the backend can name it (verbatim, without whatever
|
|
2857
|
+
// ticks around it), else the whole screen.
|
|
2858
|
+
const before = Daemon.screenOf(dialog, currentPane);
|
|
2859
|
+
// Another request since the last answer: the count and the report belong to that one. Its parked clock goes
|
|
2860
|
+
// too only when the backend vouches for the identity — an unrecognised difference (a timer, a spinner) is
|
|
2861
|
+
// not evidence that the old request was answered, and the one-minute fallback must keep running.
|
|
2862
|
+
if (this.dialogAnswers?.key === Daemon.answerKey(dialog) && this.dialogAnswers.screen !== before)
|
|
2863
|
+
this.endDialogEpisode(dialog.requestIdentity !== undefined);
|
|
2803
2864
|
if (dialog.verifyAfterKeys && autoKey
|
|
2804
2865
|
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
2805
2866
|
&& this.autoResolvedDialogKey === autoKey)
|
|
@@ -2818,17 +2879,24 @@ export class Daemon extends EventEmitter {
|
|
|
2818
2879
|
else {
|
|
2819
2880
|
sent = await this.tmux.pasteText(key, this.systemPasteOptions());
|
|
2820
2881
|
}
|
|
2882
|
+
if (stale())
|
|
2883
|
+
return;
|
|
2821
2884
|
if (!sent) {
|
|
2822
2885
|
if (dialog.verifyAfterKeys && autoKey) {
|
|
2823
2886
|
this.autoResolvedDialogGeneration = 0;
|
|
2824
2887
|
this.autoResolvedDialogKey = null;
|
|
2825
2888
|
}
|
|
2889
|
+
this.breakAnswerStreak(dialog);
|
|
2826
2890
|
return;
|
|
2827
2891
|
}
|
|
2828
2892
|
await new Promise(r => setTimeout(r, 200));
|
|
2893
|
+
if (stale())
|
|
2894
|
+
return;
|
|
2829
2895
|
}
|
|
2830
2896
|
if (dialog.verifyAfterKeys) {
|
|
2831
2897
|
const afterKeysPane = await this.tmux.capturePane();
|
|
2898
|
+
if (stale())
|
|
2899
|
+
return;
|
|
2832
2900
|
const dialogStillActive = dialog.inputBlocked
|
|
2833
2901
|
? dialogs.some(candidate => candidate.inputBlocked && Daemon.dialogMatches(candidate, afterKeysPane))
|
|
2834
2902
|
: Daemon.dialogMatches(dialog, afterKeysPane);
|
|
@@ -2844,8 +2912,27 @@ export class Daemon extends EventEmitter {
|
|
|
2844
2912
|
resolved = true;
|
|
2845
2913
|
}
|
|
2846
2914
|
}
|
|
2915
|
+
else if (dialog.keys.length > 0) {
|
|
2916
|
+
// Did the keys take? Read the pane again. The same request still there is an answer that did nothing. A request
|
|
2917
|
+
// the backend names as DIFFERENT (the next queued one), or no dialog at all, means it took. Without a
|
|
2918
|
+
// backend-named identity any difference on the screen (a ticking timer) proves nothing either way: nothing
|
|
2919
|
+
// is reported and the parked clock is left alone. Two requests that look exactly alike cannot be told apart.
|
|
2920
|
+
await new Promise(r => setTimeout(r, DIALOG_ANSWER_SETTLE_MS));
|
|
2921
|
+
const afterPane = await this.tmux.capturePane();
|
|
2922
|
+
if (stale())
|
|
2923
|
+
return; // (nothing was touched since the last check: only a read)
|
|
2924
|
+
const stillThere = Daemon.dialogMatches(dialog, afterPane);
|
|
2925
|
+
const same = stillThere && Daemon.screenOf(dialog, afterPane) === before;
|
|
2926
|
+
this.noteDialogAnswer(dialog, same ? "ignored" : (!stillThere || dialog.requestIdentity) ? "took" : "unproven", before);
|
|
2927
|
+
}
|
|
2928
|
+
else {
|
|
2929
|
+
this.breakAnswerStreak(dialog);
|
|
2930
|
+
}
|
|
2847
2931
|
});
|
|
2932
|
+
if (stale())
|
|
2933
|
+
return; // the answer-and-look outlived its spawn / monitors: it commits nothing
|
|
2848
2934
|
if (!dismissed) {
|
|
2935
|
+
this.breakAnswerStreak(dialog);
|
|
2849
2936
|
this.logger.info({ dialog: dialog.description }, "Dialog dismissal deferred — pane write in flight");
|
|
2850
2937
|
}
|
|
2851
2938
|
else if (dialog.verifyAfterKeys && !resolved) {
|
|
@@ -2854,6 +2941,11 @@ export class Daemon extends EventEmitter {
|
|
|
2854
2941
|
this.trackDialogParked(blockingSeen);
|
|
2855
2942
|
return; // Dialog handled (or deliberately deferred): skip error checks this cycle
|
|
2856
2943
|
}
|
|
2944
|
+
// Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
|
|
2945
|
+
// backoff): unless it is the one backing off, the screen that was being answered is gone, and the next dialog is
|
|
2946
|
+
// a new one.
|
|
2947
|
+
if (!answersBackedOff)
|
|
2948
|
+
this.dialogAnswers = null;
|
|
2857
2949
|
this.trackDialogParked(blockingSeen);
|
|
2858
2950
|
if (blockingSeen)
|
|
2859
2951
|
return; // held dialog: skip error checks this cycle
|
|
@@ -4649,6 +4741,7 @@ export class Daemon extends EventEmitter {
|
|
|
4649
4741
|
/** Stop every runtime poller/watcher while preserving IPC and daemon state. */
|
|
4650
4742
|
freezeRuntimeMonitors() {
|
|
4651
4743
|
this.runtimeMonitorsFrozen = true;
|
|
4744
|
+
this.launchFenceEpoch++;
|
|
4652
4745
|
if (this.healthCheckTimer) {
|
|
4653
4746
|
clearTimeout(this.healthCheckTimer);
|
|
4654
4747
|
this.healthCheckTimer = null;
|
|
@@ -5716,6 +5809,64 @@ export class Daemon extends EventEmitter {
|
|
|
5716
5809
|
this.trackDialogParked(null);
|
|
5717
5810
|
return { state: "clear" };
|
|
5718
5811
|
}
|
|
5812
|
+
/** What identifies the request on screen: the backend's own identity when it has one, else the whole screen verbatim. */
|
|
5813
|
+
static screenOf(dialog, pane) {
|
|
5814
|
+
return dialog.requestIdentity?.(pane) ?? pane;
|
|
5815
|
+
}
|
|
5816
|
+
static answerKey(dialog) {
|
|
5817
|
+
return `${Daemon.dialogKey(dialog)}\0${dialog.description}`;
|
|
5818
|
+
}
|
|
5819
|
+
/**
|
|
5820
|
+
* The episode being counted is over: its count and its report. Its parked clock ends too (`clearParked`) only when the
|
|
5821
|
+
* dialog is gone or a different request is positively identified — not on a difference nobody can explain.
|
|
5822
|
+
*/
|
|
5823
|
+
endDialogEpisode(clearParked = true) {
|
|
5824
|
+
this.dialogAnswers = null;
|
|
5825
|
+
if (clearParked)
|
|
5826
|
+
this.trackDialogParked(null);
|
|
5827
|
+
}
|
|
5828
|
+
/**
|
|
5829
|
+
* A poll that did not complete an answer-and-look (lock busy, keys not sent, nothing to press) breaks "in a row" for
|
|
5830
|
+
* this dialog; if the dialog on screen is a DIFFERENT one, the episode being counted belongs to a screen that is gone.
|
|
5831
|
+
*/
|
|
5832
|
+
breakAnswerStreak(dialog) {
|
|
5833
|
+
if (!this.dialogAnswers)
|
|
5834
|
+
return;
|
|
5835
|
+
if (this.dialogAnswers.key === Daemon.answerKey(dialog))
|
|
5836
|
+
this.dialogAnswers.ignored = 0;
|
|
5837
|
+
else
|
|
5838
|
+
this.endDialogEpisode();
|
|
5839
|
+
}
|
|
5840
|
+
/**
|
|
5841
|
+
* One answer of the runtime scanner to a dialog, and whether the SAME dialog (same kind, same content) was still
|
|
5842
|
+
* on screen right after it.
|
|
5843
|
+
* An answer that took ends the episode (what shows next is a new dialog, so the parked clock restarts too);
|
|
5844
|
+
* DIALOG_ANSWER_IGNORED_MAX in a row that did not is reported ONCE per episode, for ANY backend's dialog —
|
|
5845
|
+
* not only the ones that hold deliveries — so a human is told in about a quarter of a minute instead of the
|
|
5846
|
+
* instance sitting there until a hang detector (and only with pending work) notices. The event carries the
|
|
5847
|
+
* dialog's static description and a count, never the pane: a pane can hold anything the agent printed.
|
|
5848
|
+
*/
|
|
5849
|
+
noteDialogAnswer(dialog, outcome, screen) {
|
|
5850
|
+
const key = Daemon.answerKey(dialog);
|
|
5851
|
+
if (!this.dialogAnswers || this.dialogAnswers.key !== key)
|
|
5852
|
+
this.dialogAnswers = { key, ignored: 0, reported: false, screen, lastAnswerAt: 0 }; // the exact screen being counted: every answer in the episode saw this same one
|
|
5853
|
+
const episode = this.dialogAnswers;
|
|
5854
|
+
if (outcome !== "ignored") {
|
|
5855
|
+
this.endDialogEpisode(outcome === "took");
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
episode.ignored++;
|
|
5859
|
+
episode.lastAnswerAt = Date.now();
|
|
5860
|
+
if (episode.ignored < DIALOG_ANSWER_IGNORED_MAX || episode.reported)
|
|
5861
|
+
return;
|
|
5862
|
+
episode.reported = true;
|
|
5863
|
+
// The time-based "parked" report would say the same thing again a minute later.
|
|
5864
|
+
if (this.dialogParkedKey === Daemon.dialogKey(dialog))
|
|
5865
|
+
this.dialogParkedReported = true;
|
|
5866
|
+
const holdsDeliveries = dialog.blocksDelivery === true || dialog.holdOnly === true;
|
|
5867
|
+
this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
|
|
5868
|
+
this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
|
|
5869
|
+
}
|
|
5719
5870
|
/**
|
|
5720
5871
|
* Remember how long a blocking dialog has been on screen. Once it has
|
|
5721
5872
|
* outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
|
|
@@ -7596,6 +7747,7 @@ export class Daemon extends EventEmitter {
|
|
|
7596
7747
|
* into exactly the window this exists to close.
|
|
7597
7748
|
*/
|
|
7598
7749
|
beginSpawn() {
|
|
7750
|
+
this.dialogAnswers = null;
|
|
7599
7751
|
// A relaunch settles any outstanding quit watch: the CLI came back.
|
|
7600
7752
|
this.clearQuitRelaunchWatch();
|
|
7601
7753
|
// A restarted CLI has no trustworthy turn edge for the process it replaced.
|
|
@@ -8171,6 +8323,7 @@ export class Daemon extends EventEmitter {
|
|
|
8171
8323
|
catch { /* non-fatal */ }
|
|
8172
8324
|
}
|
|
8173
8325
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
8326
|
+
const launchFence = this.launchFenceEpoch;
|
|
8174
8327
|
const backendConfig = this.buildBackendConfig();
|
|
8175
8328
|
// Compare freshly-built instructions against the last value the agent was
|
|
8176
8329
|
// told about. Computed for ALL backends (not gated by
|
|
@@ -8206,6 +8359,16 @@ export class Daemon extends EventEmitter {
|
|
|
8206
8359
|
this.pendingInstructionsUpdate = backendConfig.instructions;
|
|
8207
8360
|
}
|
|
8208
8361
|
}
|
|
8362
|
+
// A backend that has to ask its CLI something before the launch command exists (OpenCode: does
|
|
8363
|
+
// this binary take --auto?) does it here, off the event loop, bounded, and never fails the launch.
|
|
8364
|
+
try {
|
|
8365
|
+
await this.backend.prepareLaunch?.();
|
|
8366
|
+
}
|
|
8367
|
+
catch { /* unknown capability → the backend's conservative form */ }
|
|
8368
|
+
// Thrown, not `false`: a false verdict reads as "the CLI failed to start" and sends the startup path
|
|
8369
|
+
// into its resume-retry / set-the-session-aside handling, for an instance somebody just stopped.
|
|
8370
|
+
if (launchFence !== this.launchFenceEpoch)
|
|
8371
|
+
throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
|
|
8209
8372
|
this.backend.writeConfig(backendConfig);
|
|
8210
8373
|
this.backend.preTrust?.(this.config.working_directory);
|
|
8211
8374
|
// Resolve working directory (e.g. symlink for hidden paths)
|