@gajae-code/coding-agent 0.11.9 → 0.11.11
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/CHANGELOG.md +56 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +11 -2
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +11 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +39 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +31 -7
- package/src/config/model-registry.ts +4 -2
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +222 -27
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +1191 -164
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +24 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +290 -132
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +173 -127
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +152 -42
- package/src/task/index.ts +2 -0
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +25 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/resource-gc.ts +496 -120
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
|
@@ -67,7 +67,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
|
|
|
67
67
|
idleTimeoutMs: 60_000,
|
|
68
68
|
rich: { enabled: true },
|
|
69
69
|
richDraft: { enabled: false },
|
|
70
|
-
toolActivity: { enabled:
|
|
70
|
+
toolActivity: { enabled: false },
|
|
71
71
|
topics: { nameTemplate: undefined },
|
|
72
72
|
btw: { enabled: true },
|
|
73
73
|
streaming: { enabled: true },
|
|
@@ -102,7 +102,7 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
|
|
|
102
102
|
idleTimeoutMs: 60_000,
|
|
103
103
|
rich: { enabled: true },
|
|
104
104
|
richDraft: { enabled: false },
|
|
105
|
-
toolActivity: { enabled:
|
|
105
|
+
toolActivity: { enabled: false },
|
|
106
106
|
topics: { nameTemplate: undefined },
|
|
107
107
|
btw: { enabled: true },
|
|
108
108
|
streaming: { enabled: true },
|
|
@@ -34,11 +34,12 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
34
34
|
* concrete detached quarantine plus a proven-absent canonical lock pathname —
|
|
35
35
|
* when deleting an observed owner-lock lease. Generation 12 refreshes retained
|
|
36
36
|
* native path and process authority semantics. Generation 13 rejects special
|
|
37
|
-
* files before retained native authority opens.
|
|
37
|
+
* files before retained native authority opens. Generation 14 reloads shared
|
|
38
|
+
* chat daemons after notification configuration parsing changes.
|
|
38
39
|
*/
|
|
39
40
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
40
|
-
discord:
|
|
41
|
-
slack:
|
|
41
|
+
discord: 14,
|
|
42
|
+
slack: 14,
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
package/src/sdk/bus/config.ts
CHANGED
|
@@ -170,7 +170,7 @@ export function parseNotificationSettingsSnapshot(rawConfig?: unknown): Notifica
|
|
|
170
170
|
enabled: notificationSettingsBoolean(richDraft.enabled, false),
|
|
171
171
|
},
|
|
172
172
|
toolActivity: {
|
|
173
|
-
enabled: notificationSettingsBoolean(toolActivity.enabled,
|
|
173
|
+
enabled: notificationSettingsBoolean(toolActivity.enabled, false),
|
|
174
174
|
},
|
|
175
175
|
streaming: {
|
|
176
176
|
enabled: notificationSettingsBoolean(streaming.enabled, true),
|
|
@@ -12,6 +12,7 @@ export interface DaemonPaths {
|
|
|
12
12
|
dir: string;
|
|
13
13
|
lock: string;
|
|
14
14
|
state: string;
|
|
15
|
+
heartbeat: string;
|
|
15
16
|
ownership: string;
|
|
16
17
|
roots: string;
|
|
17
18
|
steal: string;
|
|
@@ -26,6 +27,7 @@ export function daemonPaths(agentDir: string): DaemonPaths {
|
|
|
26
27
|
lock: path.join(dir, "telegram-daemon.lock"),
|
|
27
28
|
state: path.join(dir, "telegram-daemon.state.json"),
|
|
28
29
|
ownership: path.join(dir, "telegram-daemon.ownership"),
|
|
30
|
+
heartbeat: path.join(dir, "telegram-daemon.heartbeat.json"),
|
|
29
31
|
roots: path.join(dir, "telegram-daemon.roots.json"),
|
|
30
32
|
steal: path.join(dir, "telegram-daemon.steal"),
|
|
31
33
|
aliases: path.join(dir, "telegram-callback-aliases.json"),
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -31,7 +31,8 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
|
31
31
|
import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
33
33
|
import { logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
34
|
-
import { projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
34
|
+
import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
35
|
+
import { isAuthenticated, kNoAuth } from "../../config/model-registry";
|
|
35
36
|
import { Settings } from "../../config/settings";
|
|
36
37
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
37
38
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
@@ -54,6 +55,7 @@ import type {
|
|
|
54
55
|
AskSettlementResult,
|
|
55
56
|
} from "../../tools";
|
|
56
57
|
import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../../tools/ask-answer-registry";
|
|
58
|
+
import { acpFinalTextFromMessage } from "../acp/final-text";
|
|
57
59
|
import { ensureBroker } from "../broker/ensure";
|
|
58
60
|
import { SessionIndex } from "../broker/session-index";
|
|
59
61
|
import { SessionSdkHost, shouldHostSdk } from "../host";
|
|
@@ -89,6 +91,8 @@ import {
|
|
|
89
91
|
type EnsureDaemonResult,
|
|
90
92
|
endpointAuthorityDigest,
|
|
91
93
|
ensureTelegramDaemonRunningDetailed,
|
|
94
|
+
type RegisterNotificationRootResult,
|
|
95
|
+
unregisterNotificationRoot,
|
|
92
96
|
} from "./telegram-daemon";
|
|
93
97
|
|
|
94
98
|
// ===========================================================================
|
|
@@ -909,6 +913,8 @@ interface SessionRuntime {
|
|
|
909
913
|
hostStopped: boolean;
|
|
910
914
|
serverStopped: boolean;
|
|
911
915
|
brokerRegistrationReleased: boolean;
|
|
916
|
+
/** Managed Telegram root registration released during terminal teardown. */
|
|
917
|
+
notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken?: string };
|
|
912
918
|
verbosity: "lean" | "verbose";
|
|
913
919
|
sessionTag: string;
|
|
914
920
|
/** Whether the agent loop is currently running (drives the typing indicator). */
|
|
@@ -931,7 +937,13 @@ interface SessionRuntime {
|
|
|
931
937
|
emitPromptLifecycle: (
|
|
932
938
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
933
939
|
frame:
|
|
934
|
-
| {
|
|
940
|
+
| {
|
|
941
|
+
type: "agent_start" | "agent_end";
|
|
942
|
+
sessionId: string;
|
|
943
|
+
commandId?: string;
|
|
944
|
+
turnId?: string;
|
|
945
|
+
finalText?: string;
|
|
946
|
+
}
|
|
935
947
|
| {
|
|
936
948
|
type: "agent_failed";
|
|
937
949
|
sessionId: string;
|
|
@@ -971,7 +983,10 @@ interface SessionRuntime {
|
|
|
971
983
|
/** SDK control frames received during provisional ownership; replayed only after stable activation. */
|
|
972
984
|
deferredInboundControls: Array<() => void>;
|
|
973
985
|
/** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
|
|
974
|
-
inFlightTools: Map<
|
|
986
|
+
inFlightTools: Map<
|
|
987
|
+
string,
|
|
988
|
+
{ toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
|
|
989
|
+
>;
|
|
975
990
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
976
991
|
cancelPostmortemCleanup: () => void;
|
|
977
992
|
/** Disposes side-turn resources when their owning logical session becomes unavailable. */
|
|
@@ -1101,7 +1116,7 @@ const defaultConfig: NotificationConfig = {
|
|
|
1101
1116
|
idleTimeoutMs: 60_000,
|
|
1102
1117
|
rich: { enabled: true },
|
|
1103
1118
|
richDraft: { enabled: false },
|
|
1104
|
-
toolActivity: { enabled:
|
|
1119
|
+
toolActivity: { enabled: false },
|
|
1105
1120
|
streaming: { enabled: true },
|
|
1106
1121
|
topics: {},
|
|
1107
1122
|
btw: { enabled: true },
|
|
@@ -1694,6 +1709,7 @@ const UNINSTALLED_CONTROL_OPERATIONS = new Set(["auth.login", "host_tools.regist
|
|
|
1694
1709
|
|
|
1695
1710
|
const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
|
|
1696
1711
|
"model.cycle": "cycleModel",
|
|
1712
|
+
"model.profile.set": "setModelProfile",
|
|
1697
1713
|
"thinking.cycle": "cycleThinkingLevel",
|
|
1698
1714
|
"queue.steering_mode.set": "setQueueMode",
|
|
1699
1715
|
"queue.follow_up_mode.set": "setQueueMode",
|
|
@@ -1858,8 +1874,26 @@ function sdkQuerySurface(
|
|
|
1858
1874
|
const currentThinkingLevel = api.getThinkingLevel();
|
|
1859
1875
|
return projectQ10Models({ models, currentModel, currentThinkingLevel });
|
|
1860
1876
|
},
|
|
1861
|
-
getModelProfiles: () =>
|
|
1862
|
-
|
|
1877
|
+
getModelProfiles: async () => {
|
|
1878
|
+
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
1879
|
+
const catalog = projectModelProfileCatalog(profiles, ctx.modelRegistry.getError());
|
|
1880
|
+
const providers = new Set([...profiles.values()].flatMap(profile => profile.requiredProviders));
|
|
1881
|
+
const authenticatedProviders = new Set<string>();
|
|
1882
|
+
await Promise.all(
|
|
1883
|
+
[...providers].map(async provider => {
|
|
1884
|
+
try {
|
|
1885
|
+
const credential = await ctx.modelRegistry.getApiKeyForProvider(provider, id);
|
|
1886
|
+
if (credential === kNoAuth || isAuthenticated(credential)) authenticatedProviders.add(provider);
|
|
1887
|
+
} catch {
|
|
1888
|
+
// A provider whose credential state cannot be read is not currently configurable.
|
|
1889
|
+
}
|
|
1890
|
+
}),
|
|
1891
|
+
);
|
|
1892
|
+
return catalog.map(item => ({
|
|
1893
|
+
...item,
|
|
1894
|
+
available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
|
|
1895
|
+
}));
|
|
1896
|
+
},
|
|
1863
1897
|
getSkillState: () => ctx.getSkillState(),
|
|
1864
1898
|
getGates: () => {
|
|
1865
1899
|
const workflowGate = ctx.workflowGate;
|
|
@@ -2329,6 +2363,14 @@ function sdkControlSurface(
|
|
|
2329
2363
|
);
|
|
2330
2364
|
return typed("model.set", { id: `${model.provider}/${model.id}`, thinkingLevel });
|
|
2331
2365
|
},
|
|
2366
|
+
setModelProfile: async id => {
|
|
2367
|
+
if (!bindings.has("setModelProfile") || !ctx.setModelProfile)
|
|
2368
|
+
return unavailable("model.profile.set", "no model-profile activation seam is installed")();
|
|
2369
|
+
if (!id) throw Object.assign(new Error("model.profile.set requires a profile id."), { code: "invalid_input" });
|
|
2370
|
+
if (!ctx.modelRegistry.getModelProfile(id))
|
|
2371
|
+
throw Object.assign(new Error(`Model profile ${id} was not found.`), { code: "invalid_input" });
|
|
2372
|
+
return { changed: await ctx.setModelProfile(id), id };
|
|
2373
|
+
},
|
|
2332
2374
|
cycleModel: async () => {
|
|
2333
2375
|
if (!bindings.has("cycleModel"))
|
|
2334
2376
|
return unavailable("model.cycle", "no session model-cycle seam is installed")();
|
|
@@ -2832,6 +2874,7 @@ export function createNotificationsExtension(
|
|
|
2832
2874
|
settings: Settings;
|
|
2833
2875
|
cwd: string;
|
|
2834
2876
|
sessionId: string;
|
|
2877
|
+
onRegistered?: (registration: RegisterNotificationRootResult) => void;
|
|
2835
2878
|
}) => Promise<EnsureDaemonResult>;
|
|
2836
2879
|
ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
|
|
2837
2880
|
/** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
|
|
@@ -2873,13 +2916,24 @@ export function createNotificationsExtension(
|
|
|
2873
2916
|
settings: Settings,
|
|
2874
2917
|
cwd: string,
|
|
2875
2918
|
id: string,
|
|
2919
|
+
onRegistered?: (registrationToken: string) => void,
|
|
2876
2920
|
): Promise<"ready" | "blocked_identity"> {
|
|
2877
2921
|
if (options.ensureTelegramDaemon) {
|
|
2878
|
-
return (await options.ensureTelegramDaemon({
|
|
2922
|
+
return (await options.ensureTelegramDaemon({
|
|
2923
|
+
settings,
|
|
2924
|
+
cwd,
|
|
2925
|
+
sessionId: id,
|
|
2926
|
+
onRegistered: registration => onRegistered?.(registration.token),
|
|
2927
|
+
})) === "blocked"
|
|
2879
2928
|
? "blocked_identity"
|
|
2880
2929
|
: "ready";
|
|
2881
2930
|
}
|
|
2882
|
-
return (await ensureTelegramDaemonRunningDetailed({
|
|
2931
|
+
return (await ensureTelegramDaemonRunningDetailed({
|
|
2932
|
+
settings,
|
|
2933
|
+
cwd,
|
|
2934
|
+
sessionId: id,
|
|
2935
|
+
onRegistered: registration => onRegistered?.(registration.token),
|
|
2936
|
+
})) === "blocked_identity"
|
|
2883
2937
|
? "blocked_identity"
|
|
2884
2938
|
: "ready";
|
|
2885
2939
|
}
|
|
@@ -2888,9 +2942,10 @@ export function createNotificationsExtension(
|
|
|
2888
2942
|
cfg: NotificationConfig,
|
|
2889
2943
|
cwd: string,
|
|
2890
2944
|
id: string,
|
|
2945
|
+
onRegistered?: (registrationToken: string) => void,
|
|
2891
2946
|
): Promise<boolean> {
|
|
2892
2947
|
if (isTelegramConfigured(cfg)) {
|
|
2893
|
-
if ((await ensureTelegramOwner(settings, cwd, id)) === "blocked_identity") return false;
|
|
2948
|
+
if ((await ensureTelegramOwner(settings, cwd, id, onRegistered)) === "blocked_identity") return false;
|
|
2894
2949
|
}
|
|
2895
2950
|
await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
|
|
2896
2951
|
return true;
|
|
@@ -3040,6 +3095,20 @@ export function createNotificationsExtension(
|
|
|
3040
3095
|
hostStopped: rt.hostStopped && rt.serverStopped,
|
|
3041
3096
|
brokerRegistrationReleased: rt.brokerRegistrationReleased,
|
|
3042
3097
|
});
|
|
3098
|
+
if (rt.notificationRootRegistration) {
|
|
3099
|
+
try {
|
|
3100
|
+
await unregisterNotificationRoot({
|
|
3101
|
+
settings: rt.notificationRootRegistration.settings,
|
|
3102
|
+
cwd: rt.notificationRootRegistration.cwd,
|
|
3103
|
+
sessionId: id,
|
|
3104
|
+
registrationToken: rt.notificationRootRegistration.registrationToken,
|
|
3105
|
+
});
|
|
3106
|
+
rt.notificationRootRegistration = undefined;
|
|
3107
|
+
} catch (e) {
|
|
3108
|
+
ownerReleaseFailures.push(e);
|
|
3109
|
+
logger.warn(`notifications: Telegram root unregister failed: ${String(e)}`);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3043
3112
|
if (ownerReleaseFailures.length > 0) {
|
|
3044
3113
|
cleanupRetries.set(id, rt);
|
|
3045
3114
|
throw new AggregateError(ownerReleaseFailures, `SDK notification runtime ${id} owner release failed.`);
|
|
@@ -3188,7 +3257,13 @@ export function createNotificationsExtension(
|
|
|
3188
3257
|
const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
|
|
3189
3258
|
`${correlation.commandId}:${correlation.turnId}`;
|
|
3190
3259
|
type PromptLifecycleFrame =
|
|
3191
|
-
| {
|
|
3260
|
+
| {
|
|
3261
|
+
type: "agent_start" | "agent_end";
|
|
3262
|
+
sessionId: string;
|
|
3263
|
+
commandId?: string;
|
|
3264
|
+
turnId?: string;
|
|
3265
|
+
finalText?: string;
|
|
3266
|
+
}
|
|
3192
3267
|
| {
|
|
3193
3268
|
type: "agent_failed";
|
|
3194
3269
|
sessionId: string;
|
|
@@ -3638,9 +3713,13 @@ export function createNotificationsExtension(
|
|
|
3638
3713
|
emitPromptLifecycle,
|
|
3639
3714
|
emitPromptEvent,
|
|
3640
3715
|
pendingInbound: new Set<number>(),
|
|
3641
|
-
inFlightTools: new Map<
|
|
3716
|
+
inFlightTools: new Map<
|
|
3717
|
+
string,
|
|
3718
|
+
{ toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
|
|
3719
|
+
>(),
|
|
3642
3720
|
deferredGatePresentations: [],
|
|
3643
3721
|
deferredInboundControls: [],
|
|
3722
|
+
notificationRootRegistration: undefined,
|
|
3644
3723
|
};
|
|
3645
3724
|
const initializedRuntime = runtime;
|
|
3646
3725
|
runtimes.set(id, initializedRuntime);
|
|
@@ -4071,7 +4150,7 @@ export function createNotificationsExtension(
|
|
|
4071
4150
|
}
|
|
4072
4151
|
if (typeof inbound.redact === "boolean") {
|
|
4073
4152
|
if (inbound.redact && !runtime.committedRedact) {
|
|
4074
|
-
terminalizeInFlightTools(runtime, runtime.id, "
|
|
4153
|
+
terminalizeInFlightTools(runtime, runtime.id, "cancelled");
|
|
4075
4154
|
}
|
|
4076
4155
|
runtime.committedRedact = inbound.redact;
|
|
4077
4156
|
runtime.redact = inbound.redact;
|
|
@@ -4130,12 +4209,19 @@ export function createNotificationsExtension(
|
|
|
4130
4209
|
}
|
|
4131
4210
|
if (notificationsEnabledForSession && settingsAvailable && settings) {
|
|
4132
4211
|
try {
|
|
4133
|
-
|
|
4212
|
+
let registrationToken: string | undefined;
|
|
4213
|
+
if (
|
|
4214
|
+
!(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
|
|
4215
|
+
registrationToken = token;
|
|
4216
|
+
}))
|
|
4217
|
+
) {
|
|
4134
4218
|
const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
|
|
4135
4219
|
finishStartup(result);
|
|
4136
4220
|
await cleanupAbandonedStartup();
|
|
4137
4221
|
return result;
|
|
4138
4222
|
}
|
|
4223
|
+
if (isTelegramConfigured(cfg))
|
|
4224
|
+
runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
|
|
4139
4225
|
} catch (error) {
|
|
4140
4226
|
const result = failLifecycleStartup("failed", error);
|
|
4141
4227
|
finishStartup(result);
|
|
@@ -4411,8 +4497,15 @@ export function createNotificationsExtension(
|
|
|
4411
4497
|
runtime.redact = true;
|
|
4412
4498
|
runtime.verbosity = "lean";
|
|
4413
4499
|
runtime.stream = false;
|
|
4500
|
+
for (const [toolCallId, tool] of runtime.inFlightTools) {
|
|
4501
|
+
runtime.inFlightTools.set(toolCallId, {
|
|
4502
|
+
toolName: tool.toolName,
|
|
4503
|
+
...(tool.pendingPhase ? { pendingPhase: tool.pendingPhase } : {}),
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4414
4506
|
return;
|
|
4415
4507
|
}
|
|
4508
|
+
const wasPolicySuspended = runtime.policySuspended;
|
|
4416
4509
|
const redactionEnabled = policy.redact && !runtime.committedRedact;
|
|
4417
4510
|
runtime.policyGeneration++;
|
|
4418
4511
|
runtime.committedRedact = policy.redact;
|
|
@@ -4420,7 +4513,8 @@ export function createNotificationsExtension(
|
|
|
4420
4513
|
runtime.redact = policy.redact;
|
|
4421
4514
|
runtime.verbosity = policy.verbosity;
|
|
4422
4515
|
runtime.stream = policy.stream;
|
|
4423
|
-
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "
|
|
4516
|
+
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "cancelled", true);
|
|
4517
|
+
else if (wasPolicySuspended && !policy.redact) settleProvisionalToolTerminals(runtime, runtime.id);
|
|
4424
4518
|
},
|
|
4425
4519
|
activate: binding => {
|
|
4426
4520
|
const runtime = runtimes.get(binding.sessionId);
|
|
@@ -4434,7 +4528,23 @@ export function createNotificationsExtension(
|
|
|
4434
4528
|
const { settings, settingsAvailable } = resolveSettings(options.settings);
|
|
4435
4529
|
if (!settingsAvailable || !settings) return "blocked_identity";
|
|
4436
4530
|
try {
|
|
4437
|
-
|
|
4531
|
+
let registrationToken: string | undefined;
|
|
4532
|
+
const result = await ensureTelegramOwner(settings, binding.cwd, binding.sessionId, token => {
|
|
4533
|
+
registrationToken = token;
|
|
4534
|
+
});
|
|
4535
|
+
const runtime = runtimes.get(binding.sessionId);
|
|
4536
|
+
const configured = isTelegramConfigured(resolveSettings(options.settings).cfg);
|
|
4537
|
+
if (result === "ready" && runtime && !runtime.stopping && configured) {
|
|
4538
|
+
runtime.notificationRootRegistration = { settings, cwd: binding.cwd, registrationToken };
|
|
4539
|
+
} else if (registrationToken !== undefined) {
|
|
4540
|
+
await unregisterNotificationRoot({
|
|
4541
|
+
settings,
|
|
4542
|
+
cwd: binding.cwd,
|
|
4543
|
+
sessionId: binding.sessionId,
|
|
4544
|
+
registrationToken,
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
return result;
|
|
4438
4548
|
} catch {
|
|
4439
4549
|
return "failed";
|
|
4440
4550
|
}
|
|
@@ -4647,8 +4757,22 @@ export function createNotificationsExtension(
|
|
|
4647
4757
|
await rotateSessionAuthority(event, ctx, false);
|
|
4648
4758
|
});
|
|
4649
4759
|
|
|
4650
|
-
const terminalizeInFlightTools = (
|
|
4651
|
-
|
|
4760
|
+
const terminalizeInFlightTools = (
|
|
4761
|
+
rt: SessionRuntime,
|
|
4762
|
+
id: string,
|
|
4763
|
+
phase: "cancelled" | "failed",
|
|
4764
|
+
allowSafeRedactedFrame = false,
|
|
4765
|
+
): void => {
|
|
4766
|
+
if (rt.policySuspended && !allowSafeRedactedFrame) {
|
|
4767
|
+
for (const [toolCallId, tool] of rt.inFlightTools) {
|
|
4768
|
+
rt.inFlightTools.set(toolCallId, {
|
|
4769
|
+
toolName: tool.toolName,
|
|
4770
|
+
pendingPhase: tool.pendingPhase ?? phase,
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4775
|
+
if (rt.notificationsActive && (!rt.redact || allowSafeRedactedFrame)) {
|
|
4652
4776
|
for (const [toolCallId, { toolName }] of rt.inFlightTools) {
|
|
4653
4777
|
try {
|
|
4654
4778
|
pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
|
|
@@ -4660,6 +4784,27 @@ export function createNotificationsExtension(
|
|
|
4660
4784
|
rt.inFlightTools.clear();
|
|
4661
4785
|
};
|
|
4662
4786
|
|
|
4787
|
+
const settleProvisionalToolTerminals = (rt: SessionRuntime, id: string): void => {
|
|
4788
|
+
for (const [toolCallId, tool] of rt.inFlightTools) {
|
|
4789
|
+
if (!tool.pendingPhase) continue;
|
|
4790
|
+
try {
|
|
4791
|
+
if (rt.notificationsActive && !rt.redact) {
|
|
4792
|
+
pushSessionFrame(rt, {
|
|
4793
|
+
type: "tool_activity",
|
|
4794
|
+
sessionId: id,
|
|
4795
|
+
toolCallId,
|
|
4796
|
+
toolName: tool.toolName,
|
|
4797
|
+
phase: tool.pendingPhase,
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
} catch (e) {
|
|
4801
|
+
logger.warn(`notifications: provisional tool_activity settlement failed: ${String(e)}`);
|
|
4802
|
+
} finally {
|
|
4803
|
+
rt.inFlightTools.delete(toolCallId);
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
};
|
|
4807
|
+
|
|
4663
4808
|
const resetTurnStreamState = (rt: SessionRuntime): void => {
|
|
4664
4809
|
rt.currentTurnText = undefined;
|
|
4665
4810
|
rt.preAskFlushedText = undefined;
|
|
@@ -4796,14 +4941,28 @@ export function createNotificationsExtension(
|
|
|
4796
4941
|
});
|
|
4797
4942
|
else {
|
|
4798
4943
|
rt.notePromptReconciliation(correlation, { type: "agent_end" });
|
|
4799
|
-
if (rt.recordPromptTerminal(correlation))
|
|
4800
|
-
|
|
4944
|
+
if (rt.recordPromptTerminal(correlation)) {
|
|
4945
|
+
const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
|
|
4946
|
+
message =>
|
|
4947
|
+
message &&
|
|
4948
|
+
typeof message === "object" &&
|
|
4949
|
+
(message as { role?: unknown }).role === "assistant" &&
|
|
4950
|
+
(message as { stopReason?: unknown }).stopReason === "stop",
|
|
4951
|
+
);
|
|
4952
|
+
const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
|
|
4953
|
+
rt.emitPromptLifecycle(correlation, {
|
|
4954
|
+
type: "agent_end",
|
|
4955
|
+
sessionId: id,
|
|
4956
|
+
...correlation,
|
|
4957
|
+
...(finalText ? { finalText } : {}),
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4801
4960
|
}
|
|
4802
4961
|
} else {
|
|
4803
4962
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
4804
4963
|
}
|
|
4805
4964
|
rt.activePromptCorrelation = undefined;
|
|
4806
|
-
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "
|
|
4965
|
+
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
|
|
4807
4966
|
try {
|
|
4808
4967
|
pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
|
|
4809
4968
|
} catch (e) {
|
|
@@ -4921,14 +5080,13 @@ export function createNotificationsExtension(
|
|
|
4921
5080
|
// path and ordered before it — unlike message_update, which is queued async),
|
|
4922
5081
|
// then flushed here before the ask tool's execute calls registerAsk.
|
|
4923
5082
|
api.on("tool_execution_start", (event, ctx) => {
|
|
5083
|
+
const id = sessionId(ctx);
|
|
5084
|
+
const rt = runtimes.get(id);
|
|
5085
|
+
rt?.emitPromptEvent(event);
|
|
4924
5086
|
if (event.toolName === "ask") {
|
|
4925
|
-
const id = sessionId(ctx);
|
|
4926
|
-
const rt = runtimes.get(id);
|
|
4927
5087
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4928
5088
|
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
4929
5089
|
}
|
|
4930
|
-
const id = sessionId(ctx);
|
|
4931
|
-
const rt = runtimes.get(id);
|
|
4932
5090
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4933
5091
|
rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
|
|
4934
5092
|
try {
|
|
@@ -4944,12 +5102,32 @@ export function createNotificationsExtension(
|
|
|
4944
5102
|
}
|
|
4945
5103
|
});
|
|
4946
5104
|
|
|
5105
|
+
api.on("tool_execution_update", (event, ctx) => {
|
|
5106
|
+
const rt = runtimes.get(sessionId(ctx));
|
|
5107
|
+
rt?.emitPromptEvent(event);
|
|
5108
|
+
});
|
|
5109
|
+
|
|
4947
5110
|
api.on("tool_execution_end", (event, ctx) => {
|
|
4948
5111
|
const id = sessionId(ctx);
|
|
4949
5112
|
const rt = runtimes.get(id);
|
|
4950
5113
|
if (!rt) return;
|
|
5114
|
+
rt.emitPromptEvent(event);
|
|
4951
5115
|
const inFlight = rt.inFlightTools.get(event.toolCallId);
|
|
4952
|
-
if (!
|
|
5116
|
+
if (!inFlight) return;
|
|
5117
|
+
if (!rt.notificationsActive) {
|
|
5118
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
5119
|
+
return;
|
|
5120
|
+
}
|
|
5121
|
+
if (rt.policySuspended) {
|
|
5122
|
+
if (!inFlight.pendingPhase) {
|
|
5123
|
+
rt.inFlightTools.set(event.toolCallId, {
|
|
5124
|
+
toolName: inFlight.toolName,
|
|
5125
|
+
pendingPhase: event.isError ? "failed" : "completed",
|
|
5126
|
+
});
|
|
5127
|
+
}
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5130
|
+
if (rt.redact) {
|
|
4953
5131
|
rt.inFlightTools.delete(event.toolCallId);
|
|
4954
5132
|
return;
|
|
4955
5133
|
}
|
|
@@ -5117,10 +5295,22 @@ export function createNotificationsExtension(
|
|
|
5117
5295
|
await Promise.allSettled([...branchStartupTasks]);
|
|
5118
5296
|
const id = sessionId(ctx);
|
|
5119
5297
|
const rt = runtimes.get(id);
|
|
5120
|
-
if (rt) terminalizeInFlightTools(rt, id, "
|
|
5298
|
+
if (rt) terminalizeInFlightTools(rt, id, "cancelled");
|
|
5299
|
+
// Startup is only genuinely in flight when a `sessionStartPromises` entry
|
|
5300
|
+
// exists. Once startup has settled, the host is broker-visible and its
|
|
5301
|
+
// post-start `reconcileCurrentSession` may already have minted a
|
|
5302
|
+
// replacement notification-root token whose unregister is still awaiting
|
|
5303
|
+
// its file lock and atomic registry write. Returning before that settles
|
|
5304
|
+
// leaves a stale `sessions[id]` row that the retained older token is
|
|
5305
|
+
// correctly fenced from removing, so shutdown must join it.
|
|
5306
|
+
const startupWasPending = sessionStartPromises.has(id);
|
|
5121
5307
|
const controllerStop =
|
|
5122
5308
|
typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
|
|
5123
|
-
|
|
5309
|
+
const settledControllerStop = controllerStop.catch(error => {
|
|
5310
|
+
logger.warn(`notifications: controller shutdown failed: ${String(error)}`);
|
|
5311
|
+
return false;
|
|
5312
|
+
});
|
|
5313
|
+
if (startupWasPending) void settledControllerStop;
|
|
5124
5314
|
try {
|
|
5125
5315
|
await stopSession(id);
|
|
5126
5316
|
} catch (error) {
|
|
@@ -5131,5 +5321,10 @@ export function createNotificationsExtension(
|
|
|
5131
5321
|
// error severity (matching the postmortem cleanup precedent).
|
|
5132
5322
|
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
5133
5323
|
}
|
|
5324
|
+
// Keep shutdown nonblocking only while native startup is genuinely
|
|
5325
|
+
// pending (the `/notify on` path); otherwise await the controller queue so
|
|
5326
|
+
// completed-start reconciliation and its replacement-token cleanup are
|
|
5327
|
+
// joined before lifecycle shutdown returns.
|
|
5328
|
+
if (!startupWasPending) await settledControllerStop;
|
|
5134
5329
|
});
|
|
5135
5330
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
tokenFingerprint,
|
|
29
29
|
} from "./config";
|
|
30
30
|
import { type DaemonPaths, daemonPaths, HEARTBEAT_TTL_MS } from "./daemon-paths";
|
|
31
|
+
import { type OwnerFreshnessSnapshot, readOwnerFreshnessSnapshot, type TelegramDaemonFs } from "./telegram-daemon";
|
|
31
32
|
import { DAEMON_GENERATION } from "./telegram-daemon-contract";
|
|
32
33
|
|
|
33
34
|
const DEFAULT_API_BASE = "https://api.telegram.org";
|
|
@@ -578,10 +579,21 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
578
579
|
checks.push({ name: "config", level: "ok", detail: "enabled with at least one configured adapter" });
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
// Daemon ownership state (offline;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
// Daemon ownership state (offline; corrupt or unreadable state degrades to a
|
|
583
|
+
// warning instead of making the health command itself fail).
|
|
584
|
+
let daemonStateUnreadable = false;
|
|
585
|
+
let snapshot: OwnerFreshnessSnapshot;
|
|
586
|
+
try {
|
|
587
|
+
snapshot = await readOwnerFreshnessSnapshot({
|
|
588
|
+
settings: opts.settings,
|
|
589
|
+
fs: fs as unknown as TelegramDaemonFs,
|
|
590
|
+
});
|
|
591
|
+
} catch {
|
|
592
|
+
daemonStateUnreadable = true;
|
|
593
|
+
snapshot = { ownerTag: null, effectiveHeartbeatAt: undefined, legacyEmbedded: false, state: undefined };
|
|
594
|
+
}
|
|
595
|
+
const state = snapshot.state ? parseDaemonState(JSON.stringify(snapshot.state)) : undefined;
|
|
596
|
+
const heartbeatAt = finiteNonNegativeNumber(snapshot.effectiveHeartbeatAt);
|
|
585
597
|
const daemon: DaemonHealth = {
|
|
586
598
|
present: Boolean(state),
|
|
587
599
|
ownerId: state?.ownerId,
|
|
@@ -600,7 +612,9 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
600
612
|
currentGeneration: DAEMON_GENERATION,
|
|
601
613
|
generationRelation: daemonGenerationRelation(state),
|
|
602
614
|
};
|
|
603
|
-
if (
|
|
615
|
+
if (daemonStateUnreadable) {
|
|
616
|
+
checks.push({ name: "daemon", level: "warn", detail: "daemon ownership record is corrupt or unreadable" });
|
|
617
|
+
} else if (!state) {
|
|
604
618
|
checks.push({ name: "daemon", level: "ok", detail: "no daemon ownership record (none running)" });
|
|
605
619
|
} else if (!daemon.alive) {
|
|
606
620
|
checks.push({
|
|
@@ -74,7 +74,7 @@ export class NotificationOperatorRuntime {
|
|
|
74
74
|
#stopRequested = false;
|
|
75
75
|
#activeAbort: AbortController | undefined;
|
|
76
76
|
#intervals = new Map<string, OperatorIntervalHandle>();
|
|
77
|
-
#exclusive = new
|
|
77
|
+
#exclusive = new Map<string, Promise<void>>();
|
|
78
78
|
|
|
79
79
|
#deps: NotificationOperatorTimerDeps;
|
|
80
80
|
|
|
@@ -130,7 +130,7 @@ export class NotificationOperatorRuntime {
|
|
|
130
130
|
|
|
131
131
|
stopInterval(name: string): void {
|
|
132
132
|
const timer = this.#intervals.get(name);
|
|
133
|
-
if (
|
|
133
|
+
if (timer === undefined) return;
|
|
134
134
|
const clearIntervalImpl = this.#deps.clearIntervalImpl ?? clearInterval;
|
|
135
135
|
clearIntervalImpl(timer);
|
|
136
136
|
this.#intervals.delete(name);
|
|
@@ -142,14 +142,40 @@ export class NotificationOperatorRuntime {
|
|
|
142
142
|
|
|
143
143
|
async runExclusive(name: string, fn: () => Promise<void>): Promise<void> {
|
|
144
144
|
if (this.#exclusive.has(name)) return;
|
|
145
|
-
|
|
145
|
+
const completion = Promise.withResolvers<void>();
|
|
146
|
+
this.#exclusive.set(name, completion.promise);
|
|
146
147
|
try {
|
|
147
|
-
|
|
148
|
+
try {
|
|
149
|
+
void fn().then(completion.resolve, completion.reject);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
completion.reject(error);
|
|
152
|
+
}
|
|
153
|
+
await completion.promise;
|
|
148
154
|
} finally {
|
|
149
|
-
this.#exclusive.delete(name);
|
|
155
|
+
if (this.#exclusive.get(name) === completion.promise) this.#exclusive.delete(name);
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
159
|
+
async joinExclusive(name: string, timeoutMs: number): Promise<boolean> {
|
|
160
|
+
const completion = this.#exclusive.get(name);
|
|
161
|
+
if (!completion) return true;
|
|
162
|
+
const setTimeoutImpl = this.#deps.setTimeoutImpl ?? setTimeout;
|
|
163
|
+
const clearTimeoutImpl = this.#deps.clearTimeoutImpl ?? clearTimeout;
|
|
164
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
165
|
+
const timer = setTimeoutImpl(() => resolve(false), timeoutMs);
|
|
166
|
+
void completion.then(
|
|
167
|
+
() => {
|
|
168
|
+
clearTimeoutImpl(timer);
|
|
169
|
+
resolve(true);
|
|
170
|
+
},
|
|
171
|
+
() => {
|
|
172
|
+
clearTimeoutImpl(timer);
|
|
173
|
+
resolve(true);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
return await promise;
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
154
180
|
return new Promise<void>(resolve => {
|
|
155
181
|
if (signal?.aborted) return resolve();
|
|
@@ -116,6 +116,14 @@ export class RateLimitPool<T = unknown> {
|
|
|
116
116
|
return total;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Whether any queued item currently matches the predicate. */
|
|
120
|
+
someQueued(predicate: (item: RateLimitItem<T>) => boolean): boolean {
|
|
121
|
+
for (const queue of this.lanes.values()) {
|
|
122
|
+
if (queue.some(entry => predicate(entry.item))) return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
/** Current available token count (after refill at `now`). */
|
|
120
128
|
availableTokens(nowMs: number = this.now()): number {
|
|
121
129
|
this.refill(nowMs);
|