@gajae-code/coding-agent 0.12.21 → 0.13.0
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 +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- package/src/web/search/index.ts +3 -3
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export const SDK_STATE_VERSION = 1;
|
|
2
2
|
|
|
3
3
|
// The session-index snapshot carries its own format version, independent of the shared
|
|
4
|
-
// SDK_STATE_VERSION used by discovery, the lifecycle ledger, and per-event records.
|
|
5
|
-
// snapshots relax the on-disk schema to allow gaps in indexSeq (compaction
|
|
6
|
-
// sessions and superseded heartbeats), so an older broker must
|
|
7
|
-
// fall back to a truncated log.
|
|
8
|
-
|
|
4
|
+
// SDK_STATE_VERSION used by discovery, the lifecycle ledger, and per-event records.
|
|
5
|
+
// Version 2 snapshots relax the on-disk schema to allow gaps in indexSeq (compaction
|
|
6
|
+
// drops terminal+dead sessions and superseded heartbeats), so an older broker must
|
|
7
|
+
// hard-fail rather than silently fall back to a truncated log. Version 3 snapshots may
|
|
8
|
+
// additionally carry composite-identity events (hostIncarnation, activity checkpoints,
|
|
9
|
+
// session_deleted tombstones) and retention-compacted sessions; v2 (and v1) snapshots
|
|
10
|
+
// remain readable. Bumping only this constant keeps the broker protocol untouched.
|
|
11
|
+
export const SESSION_INDEX_SNAPSHOT_VERSION = 3;
|
|
9
12
|
|
|
10
13
|
export class UnsupportedStateVersionError extends Error {
|
|
11
14
|
readonly code = "unsupported_state_version";
|
|
@@ -32,10 +35,11 @@ export function assertSupportedStateVersion(file: string, value: unknown): void
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
// Fences the session-index snapshot format independently of SDK_STATE_VERSION. An older
|
|
36
|
-
// (whose SESSION_INDEX_SNAPSHOT_VERSION is
|
|
37
|
-
// snapshot and refuses to start; a current broker accepts
|
|
38
|
-
// (version 1
|
|
38
|
+
// Fences the session-index snapshot format independently of SDK_STATE_VERSION. An older
|
|
39
|
+
// broker (whose SESSION_INDEX_SNAPSHOT_VERSION is below 3) trips assertSupportedStateVersion
|
|
40
|
+
// on a version-3 snapshot and refuses to start; a current broker accepts the legacy
|
|
41
|
+
// contiguous format (version 1), the gapped version-2 format, and the version-3 format
|
|
42
|
+
// with composite-identity events.
|
|
39
43
|
export function assertSupportedSnapshotVersion(file: string, value: unknown): void {
|
|
40
44
|
if (!value || typeof value !== "object") return;
|
|
41
45
|
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
@@ -15,6 +15,9 @@ const BROKER_OPERATIONS = new Set([
|
|
|
15
15
|
"session.close",
|
|
16
16
|
"session.delete",
|
|
17
17
|
"broker.shutdown",
|
|
18
|
+
"elevation.issue",
|
|
19
|
+
"elevation.status",
|
|
20
|
+
"session.control",
|
|
18
21
|
]);
|
|
19
22
|
type RequestInput = Record<string, unknown>;
|
|
20
23
|
type BrokerRequest = {
|
|
@@ -23,6 +26,7 @@ type BrokerRequest = {
|
|
|
23
26
|
operation?: string;
|
|
24
27
|
input?: RequestInput;
|
|
25
28
|
idempotencyKey?: string;
|
|
29
|
+
elevationRequestId?: string;
|
|
26
30
|
};
|
|
27
31
|
function digest(value: string): Buffer {
|
|
28
32
|
return createHash("sha256").update(value).digest();
|
|
@@ -134,6 +138,10 @@ export class BrokerTransport {
|
|
|
134
138
|
sendError(socket, frame.id, "invalid_input", "idempotencyKey must be a string");
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
141
|
+
if (frame.elevationRequestId !== undefined && typeof frame.elevationRequestId !== "string") {
|
|
142
|
+
sendError(socket, frame.id, "invalid_input", "elevationRequestId must be a string");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
137
145
|
if (frame.operation === "broker.shutdown") {
|
|
138
146
|
const action = brokerShutdownSendAction(
|
|
139
147
|
send(socket, { type: "broker_response", id: frame.id, ok: true, result: { accepted: true } }),
|
|
@@ -145,7 +153,12 @@ export class BrokerTransport {
|
|
|
145
153
|
return;
|
|
146
154
|
}
|
|
147
155
|
try {
|
|
148
|
-
const result = await this.#broker.handleRequest(
|
|
156
|
+
const result = await this.#broker.handleRequest(
|
|
157
|
+
frame.operation,
|
|
158
|
+
frame.input,
|
|
159
|
+
frame.idempotencyKey,
|
|
160
|
+
frame.elevationRequestId,
|
|
161
|
+
);
|
|
149
162
|
send(socket, { type: "broker_response", id: frame.id, ...result });
|
|
150
163
|
} catch {
|
|
151
164
|
sendError(socket, frame.id, "unavailable", "broker request failed");
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
24
24
|
import { execFile } from "node:child_process";
|
|
25
25
|
import * as crypto from "node:crypto";
|
|
26
26
|
import * as fs from "node:fs";
|
|
27
|
+
import * as fsPromises from "node:fs/promises";
|
|
27
28
|
import * as os from "node:os";
|
|
28
29
|
import * as path from "node:path";
|
|
29
30
|
import { promisify } from "node:util";
|
|
@@ -81,9 +82,11 @@ import {
|
|
|
81
82
|
import { acpFinalTextFromMessage } from "../acp/final-text";
|
|
82
83
|
import { ensureBroker } from "../broker/ensure";
|
|
83
84
|
import { publishSessionHostRuntimeEvidence, type SessionHostRuntimePublication } from "../broker/lifecycle";
|
|
85
|
+
import { processIncarnation } from "../broker/process-incarnation";
|
|
84
86
|
import { SessionIndex } from "../broker/session-index";
|
|
87
|
+
import { elevationAuthorityPath, verifyElevationCapability } from "../elevation/capability";
|
|
85
88
|
import { createSdkSurfaceFactory, type SessionSdkHost, SessionSdkSessionRuntime, shouldHostSdk } from "../host";
|
|
86
|
-
import { type ControlSurface, dispatchControl } from "../host/control";
|
|
89
|
+
import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "../host/control";
|
|
87
90
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
|
|
88
91
|
import type { SdkFrame } from "../host/types";
|
|
89
92
|
import {
|
|
@@ -2384,6 +2387,7 @@ function sdkControlSurface(
|
|
|
2384
2387
|
settings?: Settings,
|
|
2385
2388
|
configOverrides: Map<string, unknown> = new Map(),
|
|
2386
2389
|
configRevision: { current: number } = { current: 0 },
|
|
2390
|
+
elevationAuthorityToken?: string,
|
|
2387
2391
|
abortOwnedPrompt: (
|
|
2388
2392
|
connectionId: string | undefined,
|
|
2389
2393
|
) => Promise<{ aborted: true; disposition: "cancelled" | "already_terminal" | "idle" }> = async () => ({
|
|
@@ -2683,6 +2687,9 @@ function sdkControlSurface(
|
|
|
2683
2687
|
cancelPendingPreflights(): void;
|
|
2684
2688
|
cancelPendingPreflightsForConnection(connectionId: string): void;
|
|
2685
2689
|
} = {
|
|
2690
|
+
authorizeElevationClaim: (sdkId, input, capability) =>
|
|
2691
|
+
elevationAuthorityToken !== undefined &&
|
|
2692
|
+
verifyElevationCapability(elevationAuthorityToken, capability, sdkId, input),
|
|
2686
2693
|
prompt: (text, images, clientRef) =>
|
|
2687
2694
|
submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore(), clientRef, true),
|
|
2688
2695
|
steer: text => sendSteer(text),
|
|
@@ -2717,7 +2724,7 @@ function sdkControlSurface(
|
|
|
2717
2724
|
pending.completeDirect();
|
|
2718
2725
|
return { resolved: true };
|
|
2719
2726
|
},
|
|
2720
|
-
answerGate: async (id, response, expectedSessionId, idempotencyKey) => {
|
|
2727
|
+
answerGate: async (id, response, expectedSessionId, idempotencyKey, _elevationRequestId) => {
|
|
2721
2728
|
if (!acceptGateResolution())
|
|
2722
2729
|
throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
|
|
2723
2730
|
if (expectedSessionId === undefined) auditMissingExpectedSessionId("workflow.gate_answer");
|
|
@@ -2792,7 +2799,7 @@ function sdkControlSurface(
|
|
|
2792
2799
|
code: "terminal_uncertain",
|
|
2793
2800
|
});
|
|
2794
2801
|
},
|
|
2795
|
-
approvePlan: async (id, choice, expectedSessionId) => {
|
|
2802
|
+
approvePlan: async (id, choice, expectedSessionId, _elevationRequestId) => {
|
|
2796
2803
|
if (!acceptGateResolution())
|
|
2797
2804
|
throw Object.assign(new Error("Workflow plan is no longer answerable."), { code: "resource_gone" });
|
|
2798
2805
|
if (expectedSessionId === undefined) auditMissingExpectedSessionId("workflow.plan_approve");
|
|
@@ -3917,6 +3924,11 @@ export function createNotificationsExtension(
|
|
|
3917
3924
|
// build information and required capability while lifecycle startup can settle
|
|
3918
3925
|
// a structured failure instead of leaving the lifecycle caller pending.
|
|
3919
3926
|
const token = resolveToken();
|
|
3927
|
+
const elevationAuthorityToken = crypto.randomBytes(32).toString("base64url");
|
|
3928
|
+
const elevationAuthorityFile = elevationAuthorityPath(endpointStateRoot, id);
|
|
3929
|
+
await fsPromises.mkdir(path.dirname(elevationAuthorityFile), { recursive: true, mode: 0o700 });
|
|
3930
|
+
await Bun.write(elevationAuthorityFile, `${JSON.stringify({ version: 1, token: elevationAuthorityToken })}\n`);
|
|
3931
|
+
await fsPromises.chmod(elevationAuthorityFile, 0o600);
|
|
3920
3932
|
let server: NotificationServer;
|
|
3921
3933
|
try {
|
|
3922
3934
|
const { NotificationServer, nativeBuildInfo } = sdkBusNatives();
|
|
@@ -4599,6 +4611,7 @@ export function createNotificationsExtension(
|
|
|
4599
4611
|
settings,
|
|
4600
4612
|
configOverrides,
|
|
4601
4613
|
configRevision,
|
|
4614
|
+
elevationAuthorityToken,
|
|
4602
4615
|
async connectionId => {
|
|
4603
4616
|
const active = [...promptSubmissions.entries()].find(
|
|
4604
4617
|
([, submission]) => submission.connectionId === connectionId && !submission.terminal,
|
|
@@ -4882,15 +4895,7 @@ export function createNotificationsExtension(
|
|
|
4882
4895
|
dispatchControl(
|
|
4883
4896
|
controlSurface,
|
|
4884
4897
|
OPERATIONS.find(row => row.kind === "control" && row.sdkId === operation),
|
|
4885
|
-
|
|
4886
|
-
id: requestId,
|
|
4887
|
-
operation,
|
|
4888
|
-
input: request.input,
|
|
4889
|
-
expectedRevision:
|
|
4890
|
-
typeof request.expectedRevision === "string" ? request.expectedRevision : undefined,
|
|
4891
|
-
idempotencyKey: typeof request.idempotencyKey === "string" ? request.idempotencyKey : undefined,
|
|
4892
|
-
confirm: request.confirm === true,
|
|
4893
|
-
},
|
|
4898
|
+
controlRequestFromFrame(request as Record<string, unknown>),
|
|
4894
4899
|
),
|
|
4895
4900
|
);
|
|
4896
4901
|
|
|
@@ -4902,6 +4907,16 @@ export function createNotificationsExtension(
|
|
|
4902
4907
|
},
|
|
4903
4908
|
query: async (connectionId, frame) => {
|
|
4904
4909
|
const request = frame as { id?: unknown; query?: unknown; input?: unknown; cursor?: unknown };
|
|
4910
|
+
// D1: a non-string top-level cursor is rejected, never silently
|
|
4911
|
+
// dropped into a fresh-snapshot query (parity with the loopback
|
|
4912
|
+
// session-runtime adapter).
|
|
4913
|
+
if (request.cursor !== undefined && typeof request.cursor !== "string")
|
|
4914
|
+
return {
|
|
4915
|
+
type: "query_response",
|
|
4916
|
+
id: typeof request.id === "string" ? request.id : undefined,
|
|
4917
|
+
ok: false,
|
|
4918
|
+
error: { code: "invalid_input", message: "cursor must be a non-empty string" },
|
|
4919
|
+
};
|
|
4905
4920
|
const response = await queryHandlers.dispatch({
|
|
4906
4921
|
id: typeof request.id === "string" ? request.id : undefined,
|
|
4907
4922
|
query: typeof request.query === "string" ? request.query : "",
|
|
@@ -5608,6 +5623,7 @@ export function createNotificationsExtension(
|
|
|
5608
5623
|
throwIfLifecycleStopped();
|
|
5609
5624
|
const locator = { repo: path.resolve(ctx.cwd), stateRoot: endpointStateRoot };
|
|
5610
5625
|
const endpointMtimeMs = fs.statSync(path.join(endpointStateRoot, "sdk", `${id}.json`)).mtimeMs;
|
|
5626
|
+
const hostProcessIncarnation = processIncarnation(process.pid);
|
|
5611
5627
|
await host.registerWithBroker({
|
|
5612
5628
|
// The endpoint is written before registration. Its exact mtime
|
|
5613
5629
|
// binds this index generation to that discovery record.
|
|
@@ -5617,6 +5633,11 @@ export function createNotificationsExtension(
|
|
|
5617
5633
|
...input,
|
|
5618
5634
|
locator,
|
|
5619
5635
|
pid: process.pid,
|
|
5636
|
+
// A pid alone cannot survive its own reuse, and the marker that
|
|
5637
|
+
// binds it lives in the workspace state root this host can outlive.
|
|
5638
|
+
// Publishing the incarnation into broker-owned storage is what keeps
|
|
5639
|
+
// teardown identity provable after that workspace is gone.
|
|
5640
|
+
...(hostProcessIncarnation ? { processIncarnation: hostProcessIncarnation } : {}),
|
|
5620
5641
|
endpointMtimeMs,
|
|
5621
5642
|
...(lifecycleRequestId ? { lifecycleRequestId } : {}),
|
|
5622
5643
|
});
|
|
@@ -5810,7 +5831,7 @@ export function createNotificationsExtension(
|
|
|
5810
5831
|
logger.warn(`notifications: failed to start server: ${String(e)}`);
|
|
5811
5832
|
const result = failLifecycleStartup("failed", e);
|
|
5812
5833
|
finishStartup(result);
|
|
5813
|
-
let suppressExtensionError =
|
|
5834
|
+
let suppressExtensionError = result.failure?.message === "Lifecycle SDK startup was cancelled.";
|
|
5814
5835
|
let stopped = false;
|
|
5815
5836
|
try {
|
|
5816
5837
|
stopped = await stopSession(id, "session", runtime);
|
|
@@ -195,7 +195,21 @@ export function createKindAwareReconciliation(
|
|
|
195
195
|
if (!correlation) return;
|
|
196
196
|
await queueMutation(candidate => {
|
|
197
197
|
const record = candidate.get(keyOf(kind, correlation));
|
|
198
|
-
if (!record
|
|
198
|
+
if (!record) return { value: undefined, changed: false };
|
|
199
|
+
if (record.terminalAt !== undefined) {
|
|
200
|
+
// The terminal is claimed by one delivery path while the reason arrives on
|
|
201
|
+
// another, so ordering is not the caller's to control. A late agent_failed
|
|
202
|
+
// enriches the settled record instead of being dropped; it must not resurrect
|
|
203
|
+
// it, so status, terminalAt, retention order, and the clientRef index stay
|
|
204
|
+
// as-is (cleanupRecords is intentionally not re-run). First reason wins: a
|
|
205
|
+
// late generic frame never overwrites a specific one. Persisted so the reason
|
|
206
|
+
// survives reconnect/restart reconciliation.
|
|
207
|
+
if (frame.type === "agent_failed" && record.error === undefined) {
|
|
208
|
+
record.error = sanitizePromptFailure(frame.error);
|
|
209
|
+
return { value: undefined, changed: true };
|
|
210
|
+
}
|
|
211
|
+
return { value: undefined, changed: false };
|
|
212
|
+
}
|
|
199
213
|
if (frame.type === "agent_start") {
|
|
200
214
|
if (record.status !== "accepted") return { value: undefined, changed: false };
|
|
201
215
|
record.status = "in_flight";
|
|
@@ -278,7 +292,12 @@ export function createKindAwareReconciliation(
|
|
|
278
292
|
terminalAt: record.terminalAt as number,
|
|
279
293
|
...(record.outcome !== undefined ? { outcome: record.outcome } : {}),
|
|
280
294
|
};
|
|
281
|
-
if (record.status === "terminal_ok")
|
|
295
|
+
if (record.status === "terminal_ok")
|
|
296
|
+
return {
|
|
297
|
+
status: "terminal_ok",
|
|
298
|
+
...terminal,
|
|
299
|
+
...(record.error !== undefined ? { error: record.error } : {}),
|
|
300
|
+
};
|
|
282
301
|
return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
|
|
283
302
|
};
|
|
284
303
|
|
|
@@ -602,6 +602,13 @@ export interface DaemonHealth {
|
|
|
602
602
|
stopped: boolean;
|
|
603
603
|
heartbeatAt: number | undefined;
|
|
604
604
|
heartbeatAgeMs: number | undefined;
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Session endpoints the live owner reported an OPEN socket to in its latest
|
|
608
|
+
* matching heartbeat sidecar. `undefined` means the owner never published the
|
|
609
|
+
* field (older daemon, no stable owner tag, no matching sidecar): unknown, not zero.
|
|
610
|
+
*/
|
|
611
|
+
attachedEndpoints: number | undefined;
|
|
605
612
|
generation: number | undefined;
|
|
606
613
|
currentGeneration: number;
|
|
607
614
|
generationRelation: DaemonGenerationRelation;
|
|
@@ -822,7 +829,13 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
822
829
|
});
|
|
823
830
|
} catch {
|
|
824
831
|
daemonStateUnreadable = true;
|
|
825
|
-
snapshot = {
|
|
832
|
+
snapshot = {
|
|
833
|
+
ownerTag: null,
|
|
834
|
+
effectiveHeartbeatAt: undefined,
|
|
835
|
+
legacyEmbedded: false,
|
|
836
|
+
attachedEndpoints: undefined,
|
|
837
|
+
state: undefined,
|
|
838
|
+
};
|
|
826
839
|
}
|
|
827
840
|
const state = snapshot.state ? parseDaemonState(JSON.stringify(snapshot.state)) : undefined;
|
|
828
841
|
const heartbeatAt = finiteNonNegativeNumber(snapshot.effectiveHeartbeatAt);
|
|
@@ -840,6 +853,7 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
840
853
|
stopped: state?.stoppedAt !== undefined,
|
|
841
854
|
heartbeatAt,
|
|
842
855
|
heartbeatAgeMs: heartbeatAt === undefined ? undefined : displayHeartbeatAgeMs(now, heartbeatAt),
|
|
856
|
+
attachedEndpoints: snapshot.attachedEndpoints,
|
|
843
857
|
generation: state?.generation,
|
|
844
858
|
currentGeneration: DAEMON_GENERATION,
|
|
845
859
|
generationRelation: daemonGenerationRelation(state),
|
|
@@ -863,6 +877,19 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
863
877
|
level: "warn",
|
|
864
878
|
detail: "a live daemon owns a different bot token or chat id",
|
|
865
879
|
});
|
|
880
|
+
} else if (telegramConfigured && !daemon.stopped && daemon.generationRelation !== "current") {
|
|
881
|
+
// Liveness is not serviceability. `isCurrentCompatibleOwner` refuses to attach
|
|
882
|
+
// a session to an owner whose generation is not this build's, and
|
|
883
|
+
// `acquireDaemonOwnership` then answers `provisional`, which `ensureTelegramDaemonRunning`
|
|
884
|
+
// reports as `blocked_identity`: no transport is ever attached and no
|
|
885
|
+
// notification is ever published, while the owner keeps heartbeating. Reporting
|
|
886
|
+
// that as OK is what makes the outage invisible; the generation is inventory
|
|
887
|
+
// metadata that never forces a reload on its own, so the operator has to.
|
|
888
|
+
checks.push({
|
|
889
|
+
name: "daemon",
|
|
890
|
+
level: "warn",
|
|
891
|
+
detail: `daemon pid ${daemon.pid} serves generation ${daemon.generation ?? "unknown"} but this build attaches only to generation ${daemon.currentGeneration}; it cannot attach a session transport — run \`gjc daemon reload\``,
|
|
892
|
+
});
|
|
866
893
|
} else {
|
|
867
894
|
checks.push({ name: "daemon", level: "ok", detail: `daemon pid ${daemon.pid} alive with a fresh heartbeat` });
|
|
868
895
|
}
|
|
@@ -931,6 +958,25 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
931
958
|
"No local notification endpoint for this working directory. In this GJC terminal run /notify on; if it does not report notifications enabled, start a new local GJC session. Do not re-pair Telegram.",
|
|
932
959
|
});
|
|
933
960
|
}
|
|
961
|
+
// Endpoint files are registrations, not deliveries: a live owner that
|
|
962
|
+
// reports zero OPEN sockets cannot deliver to any of them. An owner that
|
|
963
|
+
// never published a count is unknown, and unknown is never treated as zero.
|
|
964
|
+
if (
|
|
965
|
+
telegramConfigured &&
|
|
966
|
+
daemon.present &&
|
|
967
|
+
daemon.alive &&
|
|
968
|
+
daemon.heartbeatFresh &&
|
|
969
|
+
daemon.identityMatches &&
|
|
970
|
+
!daemon.stopped &&
|
|
971
|
+
endpoints.total > 0 &&
|
|
972
|
+
daemon.attachedEndpoints === 0
|
|
973
|
+
) {
|
|
974
|
+
checks.push({
|
|
975
|
+
name: "endpoint_attachment",
|
|
976
|
+
level: "warn",
|
|
977
|
+
detail: `${endpoints.total} endpoint file(s) registered but daemon pid ${daemon.pid} reports 0 attached session(s); the daemon cannot deliver to any registered endpoint`,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
934
980
|
}
|
|
935
981
|
|
|
936
982
|
// Optional selected REST-only reachability probe.
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* Session-scoped durable retention (kind-aware store) is provided by reconciliation-store.ts (#3032).
|
|
17
17
|
* - The clientRef index is session-runtime scoped; a ref conflicts only while
|
|
18
18
|
* retained and must never be reused as a retry mechanism.
|
|
19
|
-
* - Terminal transitions settle once:
|
|
19
|
+
* - Terminal transitions settle once: first terminal outcome wins. A late
|
|
20
|
+
* `agent_failed` may still attach its sanitized reason to an already-terminal
|
|
21
|
+
* record that has none, but never changes status, terminalAt, or retention.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure } from "../prompt-failure";
|
|
@@ -67,6 +69,8 @@ export type TurnPromptReconciliation =
|
|
|
67
69
|
acceptedAt: number;
|
|
68
70
|
startedAt?: number;
|
|
69
71
|
terminalAt: number;
|
|
72
|
+
/** Present when a late `agent_failed` supplied the only failure reason. */
|
|
73
|
+
error?: { code: string; message: string };
|
|
70
74
|
}
|
|
71
75
|
| {
|
|
72
76
|
status: "failed";
|
|
@@ -184,7 +188,17 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
|
|
|
184
188
|
) => {
|
|
185
189
|
if (!correlation) return;
|
|
186
190
|
const record = records.get(keyOf(correlation));
|
|
187
|
-
if (!record
|
|
191
|
+
if (!record) return;
|
|
192
|
+
if (record.terminalAt !== undefined) {
|
|
193
|
+
// The terminal is claimed by one path while the reason arrives on another,
|
|
194
|
+
// so ordering is not the caller's to control. A late agent_failed enriches
|
|
195
|
+
// the settled record instead of being dropped; it must not resurrect it,
|
|
196
|
+
// so status, terminalAt, retention order, and clientRefIndex stay as-is.
|
|
197
|
+
// First reason wins: a late generic frame never overwrites a specific one.
|
|
198
|
+
if (frame.type === "agent_failed" && record.error === undefined)
|
|
199
|
+
record.error = sanitizePromptFailure(frame.error);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
188
202
|
if (frame.type === "agent_start") {
|
|
189
203
|
if (record.status === "accepted") {
|
|
190
204
|
record.status = "in_flight";
|
|
@@ -227,7 +241,12 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
|
|
|
227
241
|
...(record.startedAt !== undefined ? { startedAt: record.startedAt } : {}),
|
|
228
242
|
terminalAt: record.terminalAt as number,
|
|
229
243
|
};
|
|
230
|
-
if (record.status === "terminal_ok")
|
|
244
|
+
if (record.status === "terminal_ok")
|
|
245
|
+
return {
|
|
246
|
+
status: "terminal_ok",
|
|
247
|
+
...terminal,
|
|
248
|
+
...(record.error !== undefined ? { error: record.error } : {}),
|
|
249
|
+
};
|
|
231
250
|
return { status: "failed", ...terminal, error: record.error ?? sanitizePromptFailure(undefined) };
|
|
232
251
|
};
|
|
233
252
|
|
|
@@ -93,8 +93,11 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
93
93
|
* later session from creating a topic or replaying notifications. Generation
|
|
94
94
|
* 58 preserves that invariant during durable-fence load promotion and restores
|
|
95
95
|
* the exact grace deadline when a failed archive publication rolls back.
|
|
96
|
+
* Generation 59 publishes the owner's attached OPEN-socket count in the
|
|
97
|
+
* heartbeat sidecar so `gjc notify health` can report a live daemon that is
|
|
98
|
+
* attached to no registered endpoint (#4128).
|
|
96
99
|
*/
|
|
97
|
-
export const DAEMON_GENERATION =
|
|
100
|
+
export const DAEMON_GENERATION = 59;
|
|
98
101
|
|
|
99
102
|
/**
|
|
100
103
|
* Serving-compatibility boundary for daemon lifecycle requests. Epoch 5
|
|
@@ -1217,8 +1217,19 @@ type OwnerHeartbeatSidecar = {
|
|
|
1217
1217
|
ownerId: string;
|
|
1218
1218
|
acquisitionId: string;
|
|
1219
1219
|
heartbeatAt: number;
|
|
1220
|
+
/**
|
|
1221
|
+
* Session endpoints this owner had an OPEN WebSocket to when the heartbeat was
|
|
1222
|
+
* published. Optional: a sidecar written by an older daemon omits it and must
|
|
1223
|
+
* read back as unknown, never as zero.
|
|
1224
|
+
*/
|
|
1225
|
+
attachedEndpoints?: number;
|
|
1220
1226
|
};
|
|
1221
1227
|
|
|
1228
|
+
/** Attachment counts are advisory observations, so only safe non-negative integers are published or trusted. */
|
|
1229
|
+
function validAttachedEndpointCount(value: unknown): value is number {
|
|
1230
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1222
1233
|
function ownerTagFromState(
|
|
1223
1234
|
state: DaemonState | undefined,
|
|
1224
1235
|
): Pick<OwnerHeartbeatSidecar, "pid" | "incarnation" | "ownerId" | "acquisitionId"> | undefined {
|
|
@@ -1250,6 +1261,7 @@ function sidecarMatchesOwnerTag(
|
|
|
1250
1261
|
typeof sidecar.ownerId === "string" &&
|
|
1251
1262
|
typeof sidecar.acquisitionId === "string" &&
|
|
1252
1263
|
Number.isSafeInteger(sidecar.heartbeatAt) &&
|
|
1264
|
+
(sidecar.attachedEndpoints === undefined || validAttachedEndpointCount(sidecar.attachedEndpoints)) &&
|
|
1253
1265
|
sidecar.pid === tag.pid &&
|
|
1254
1266
|
sidecar.incarnation === tag.incarnation &&
|
|
1255
1267
|
sidecar.ownerId === tag.ownerId &&
|
|
@@ -1277,6 +1289,12 @@ export interface OwnerFreshnessSnapshot {
|
|
|
1277
1289
|
ownerTag: Pick<OwnerHeartbeatSidecar, "pid" | "incarnation" | "ownerId" | "acquisitionId"> | null;
|
|
1278
1290
|
effectiveHeartbeatAt: number | undefined;
|
|
1279
1291
|
legacyEmbedded: boolean;
|
|
1292
|
+
/**
|
|
1293
|
+
* Session endpoints the current owner reported as attached in its latest matching
|
|
1294
|
+
* heartbeat sidecar. `undefined` means the owner never published the field (older
|
|
1295
|
+
* daemon, no stable owner tag, or no matching sidecar) — that is unknown, not zero.
|
|
1296
|
+
*/
|
|
1297
|
+
attachedEndpoints: number | undefined;
|
|
1280
1298
|
state: DaemonState | undefined;
|
|
1281
1299
|
}
|
|
1282
1300
|
|
|
@@ -1293,7 +1311,14 @@ export async function readOwnerFreshnessSnapshot(input: {
|
|
|
1293
1311
|
const tag = ownerTagFromState(state);
|
|
1294
1312
|
const lock = await readOwnershipLock(fsImpl, paths.lock);
|
|
1295
1313
|
const ownerTag = tag && ownershipLockMatchesState(lock, state) ? tag : null;
|
|
1296
|
-
if (legacyEmbedded)
|
|
1314
|
+
if (legacyEmbedded)
|
|
1315
|
+
return {
|
|
1316
|
+
ownerTag,
|
|
1317
|
+
effectiveHeartbeatAt: state?.heartbeatAt,
|
|
1318
|
+
legacyEmbedded,
|
|
1319
|
+
attachedEndpoints: undefined,
|
|
1320
|
+
state,
|
|
1321
|
+
};
|
|
1297
1322
|
const sidecar = await readSidecarLenient(fsImpl, paths.heartbeat);
|
|
1298
1323
|
const rereadState = await readJson<DaemonState>(fsImpl, paths.state);
|
|
1299
1324
|
const rereadLock = await readOwnershipLock(fsImpl, paths.lock);
|
|
@@ -1301,15 +1326,20 @@ export async function readOwnerFreshnessSnapshot(input: {
|
|
|
1301
1326
|
const stableTag = rereadTag && ownershipLockMatchesState(rereadLock, rereadState) ? rereadTag : null;
|
|
1302
1327
|
const stable =
|
|
1303
1328
|
ownerTag && stableTag && sidecarMatchesOwnerTag({ ...ownerTag, heartbeatAt: 0 }, stableTag) ? stableTag : null;
|
|
1329
|
+
const matchingSidecar = stable && sidecarMatchesOwnerTag(sidecar, stable) ? sidecar : undefined;
|
|
1304
1330
|
const effectiveHeartbeatAt = stable
|
|
1305
|
-
?
|
|
1306
|
-
? Math.max(rereadState?.heartbeatAt ?? 0,
|
|
1331
|
+
? matchingSidecar
|
|
1332
|
+
? Math.max(rereadState?.heartbeatAt ?? 0, matchingSidecar.heartbeatAt)
|
|
1307
1333
|
: rereadState?.heartbeatAt
|
|
1308
1334
|
: undefined;
|
|
1335
|
+
const attachedEndpoints = validAttachedEndpointCount(matchingSidecar?.attachedEndpoints)
|
|
1336
|
+
? matchingSidecar.attachedEndpoints
|
|
1337
|
+
: undefined;
|
|
1309
1338
|
return {
|
|
1310
1339
|
ownerTag: stable,
|
|
1311
1340
|
effectiveHeartbeatAt,
|
|
1312
1341
|
legacyEmbedded: false,
|
|
1342
|
+
attachedEndpoints,
|
|
1313
1343
|
state: rereadState,
|
|
1314
1344
|
};
|
|
1315
1345
|
}
|
|
@@ -1323,6 +1353,12 @@ export async function renewOwnerHeartbeatSidecar(input: {
|
|
|
1323
1353
|
now?: () => number;
|
|
1324
1354
|
pid?: number;
|
|
1325
1355
|
pidIncarnation?: (pid: number) => string | undefined;
|
|
1356
|
+
/**
|
|
1357
|
+
* Session endpoints the caller currently holds an OPEN WebSocket to. Omitted by
|
|
1358
|
+
* non-daemon callers (bootstrap/proof paths) so their sidecar reports unknown
|
|
1359
|
+
* rather than falsely claiming zero attachments.
|
|
1360
|
+
*/
|
|
1361
|
+
attachedEndpoints?: number;
|
|
1326
1362
|
}): Promise<boolean> {
|
|
1327
1363
|
const fsImpl = input.fs ?? nodeFs;
|
|
1328
1364
|
const paths = daemonPaths(input.settings.getAgentDir());
|
|
@@ -1347,6 +1383,7 @@ export async function renewOwnerHeartbeatSidecar(input: {
|
|
|
1347
1383
|
ownerId: input.ownerId,
|
|
1348
1384
|
acquisitionId,
|
|
1349
1385
|
heartbeatAt: (input.now ?? Date.now)(),
|
|
1386
|
+
...(validAttachedEndpointCount(input.attachedEndpoints) ? { attachedEndpoints: input.attachedEndpoints } : {}),
|
|
1350
1387
|
};
|
|
1351
1388
|
const tmp = `${paths.heartbeat}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1352
1389
|
await fsImpl.writeFile(tmp, `${JSON.stringify(sidecar, null, 2)}\n`, { mode: 0o600 });
|
|
@@ -9665,6 +9702,18 @@ export class TelegramNotificationDaemon {
|
|
|
9665
9702
|
private stopFlushTimer(): void {
|
|
9666
9703
|
this.runtime.stopInterval("telegram-flush");
|
|
9667
9704
|
}
|
|
9705
|
+
/**
|
|
9706
|
+
* Sessions this owner can actually deliver to right now. Only `WebSocket.OPEN`
|
|
9707
|
+
* counts: a CONNECTING socket is constructed but cannot carry a frame, and
|
|
9708
|
+
* CLOSING/CLOSED ones are gone. Every delivery path in this daemon refuses to
|
|
9709
|
+
* send unless `readyState === WebSocket.OPEN`, so OPEN is exactly the state
|
|
9710
|
+
* that means a notification can leave this process for that session.
|
|
9711
|
+
*/
|
|
9712
|
+
private attachedEndpointCount(): number {
|
|
9713
|
+
let attached = 0;
|
|
9714
|
+
for (const session of this.sessions.values()) if (session.ws.readyState === WebSocket.OPEN) attached += 1;
|
|
9715
|
+
return attached;
|
|
9716
|
+
}
|
|
9668
9717
|
private async renewOwnershipHeartbeat(): Promise<boolean> {
|
|
9669
9718
|
return renewOwnerHeartbeatSidecar({
|
|
9670
9719
|
settings: this.opts.settings,
|
|
@@ -9674,6 +9723,7 @@ export class TelegramNotificationDaemon {
|
|
|
9674
9723
|
now: this.opts.now,
|
|
9675
9724
|
pid: this.opts.pid ?? process.pid,
|
|
9676
9725
|
pidIncarnation: this.opts.pidIncarnation,
|
|
9726
|
+
attachedEndpoints: this.attachedEndpointCount(),
|
|
9677
9727
|
});
|
|
9678
9728
|
}
|
|
9679
9729
|
|
|
@@ -12347,6 +12397,7 @@ export class TelegramNotificationDaemon {
|
|
|
12347
12397
|
now: this.opts.now,
|
|
12348
12398
|
pid: this.opts.pid ?? process.pid,
|
|
12349
12399
|
pidIncarnation: this.opts.pidIncarnation,
|
|
12400
|
+
attachedEndpoints: this.attachedEndpointCount(),
|
|
12350
12401
|
}))
|
|
12351
12402
|
)
|
|
12352
12403
|
break;
|