@gajae-code/coding-agent 0.13.0 → 0.13.1
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 +18 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +84 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -2
- package/src/sdk/bus/telegram-daemon.ts +131 -29
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +0 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -162,6 +162,7 @@ type BrokerSession = {
|
|
|
162
162
|
sessionId: string;
|
|
163
163
|
locator?: { repo?: string };
|
|
164
164
|
live?: boolean;
|
|
165
|
+
terminal?: boolean;
|
|
165
166
|
endpointGeneration?: number;
|
|
166
167
|
endpointMtimeMs?: number;
|
|
167
168
|
title?: string;
|
|
@@ -252,7 +253,11 @@ export function paginateAcpSessions(
|
|
|
252
253
|
(value): value is BrokerSession & { locator: { repo: string } } =>
|
|
253
254
|
typeof value?.sessionId === "string" && typeof value.locator?.repo === "string",
|
|
254
255
|
)
|
|
255
|
-
.filter(value => !cwd || value.locator.repo === cwd)
|
|
256
|
+
.filter(value => !cwd || value.locator.repo === cwd)
|
|
257
|
+
// DR-1 retains closed/unregistered rows in the broker index for inspect
|
|
258
|
+
// and offline tail; ACP's session/list advertises only loadable sessions,
|
|
259
|
+
// so terminal rows are excluded here (matching the pre-DR-1 wire shape).
|
|
260
|
+
.filter(value => value.terminal !== true);
|
|
256
261
|
const sessions = filtered.slice(offset, offset + SESSION_PAGE_SIZE).map(value => {
|
|
257
262
|
const metadata = sessionMetadata.get(value.sessionId);
|
|
258
263
|
const updatedAt =
|
|
@@ -1245,7 +1250,10 @@ export class AcpAgent implements Agent {
|
|
|
1245
1250
|
if (
|
|
1246
1251
|
typeof candidate?.sessionId !== "string" ||
|
|
1247
1252
|
typeof candidate.locator?.repo !== "string" ||
|
|
1248
|
-
path.resolve(candidate.locator.repo) !== path.resolve(params.cwd)
|
|
1253
|
+
path.resolve(candidate.locator.repo) !== path.resolve(params.cwd) ||
|
|
1254
|
+
// A DR-1 terminal row is not a loadable session: adopting its cwd
|
|
1255
|
+
// here would re-arm teardown authority for a host that is gone.
|
|
1256
|
+
candidate.terminal === true
|
|
1249
1257
|
)
|
|
1250
1258
|
continue;
|
|
1251
1259
|
if (discovered.has(candidate.sessionId))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
|
-
import { getSupportedEfforts, type Model, modelsAreEqual } from "@gajae-code/ai/core";
|
|
2
|
+
import { getSupportedEfforts, type Model, modelSupportsServiceTier, modelsAreEqual } from "@gajae-code/ai/core";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
5
5
|
fuzzyFilter,
|
|
@@ -365,8 +365,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
365
365
|
#currentThinkingLevel?: ThinkingLevel;
|
|
366
366
|
#activeModelProfile?: string;
|
|
367
367
|
#configuredDefaultChain?: readonly string[];
|
|
368
|
-
#isFastForProvider: (provider?: string) => boolean = () => false;
|
|
369
|
-
#isFastForSubagentProvider: (provider?: string) => boolean = () => false;
|
|
368
|
+
#isFastForProvider: (provider?: string, supportsServiceTier?: boolean) => boolean = () => false;
|
|
369
|
+
#isFastForSubagentProvider: (provider?: string, supportsServiceTier?: boolean) => boolean = () => false;
|
|
370
370
|
#isCurrentModelFastModeActive: () => boolean = () => false;
|
|
371
371
|
#pendingActionItem?: ModelItem | CanonicalModelItem;
|
|
372
372
|
#selectedActionIndex: number = 0;
|
|
@@ -374,6 +374,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
374
374
|
#selectedThinkingIndex: number = 0;
|
|
375
375
|
#assignmentState: "idle" | "assigning" = "idle";
|
|
376
376
|
#closeAfterAssignment = false;
|
|
377
|
+
#unsubscribeCatalogChanged: () => void = () => {};
|
|
378
|
+
#disposed = false;
|
|
377
379
|
|
|
378
380
|
// Preset landing state
|
|
379
381
|
#viewMode: ModelSelectorViewMode = "presets";
|
|
@@ -405,8 +407,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
405
407
|
temporaryOnly?: boolean;
|
|
406
408
|
initialSearchInput?: string;
|
|
407
409
|
sessionId?: string;
|
|
408
|
-
isFastForProvider?: (provider?: string) => boolean;
|
|
409
|
-
isFastForSubagentProvider?: (provider?: string) => boolean;
|
|
410
|
+
isFastForProvider?: (provider?: string, supportsServiceTier?: boolean) => boolean;
|
|
411
|
+
isFastForSubagentProvider?: (provider?: string, supportsServiceTier?: boolean) => boolean;
|
|
410
412
|
isCurrentModelFastModeActive?: () => boolean;
|
|
411
413
|
currentThinkingLevel?: ThinkingLevel;
|
|
412
414
|
activeModelProfile?: string;
|
|
@@ -434,7 +436,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
434
436
|
// session's effective predicate so an auto-disabled provider shows no glyph.
|
|
435
437
|
this.#isCurrentModelFastModeActive =
|
|
436
438
|
options?.isCurrentModelFastModeActive ??
|
|
437
|
-
(() =>
|
|
439
|
+
(() =>
|
|
440
|
+
this.#currentModel
|
|
441
|
+
? this.#isFastForProvider(this.#currentModel.provider, modelSupportsServiceTier(this.#currentModel))
|
|
442
|
+
: false);
|
|
438
443
|
const initialSearchInput = options?.initialSearchInput;
|
|
439
444
|
this.#viewMode = this.#temporaryOnly || initialSearchInput || scopedModels.length > 0 ? "models" : "presets";
|
|
440
445
|
|
|
@@ -479,6 +484,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
479
484
|
// Add bottom border
|
|
480
485
|
this.addChild(new DynamicBorder());
|
|
481
486
|
|
|
487
|
+
if (typeof this.#modelRegistry.onCatalogChanged === "function") {
|
|
488
|
+
this.#unsubscribeCatalogChanged = this.#modelRegistry.onCatalogChanged(() => {
|
|
489
|
+
if (this.#disposed) return;
|
|
490
|
+
if (this.#refreshCatalogView()) this.#tui.requestRender();
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
482
494
|
// Load models and do initial render
|
|
483
495
|
this.#loadModels().then(() => {
|
|
484
496
|
this.#buildProviderTabs();
|
|
@@ -504,6 +516,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
504
516
|
});
|
|
505
517
|
}
|
|
506
518
|
|
|
519
|
+
override dispose(): void {
|
|
520
|
+
if (this.#disposed) return;
|
|
521
|
+
this.#disposed = true;
|
|
522
|
+
this.#unsubscribeCatalogChanged();
|
|
523
|
+
super.dispose();
|
|
524
|
+
}
|
|
525
|
+
|
|
507
526
|
#isActiveDefaultFallback(): boolean {
|
|
508
527
|
if (!this.#currentModel) return false;
|
|
509
528
|
const configuredChain =
|
|
@@ -1705,11 +1724,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
1705
1724
|
// other modelRoles rows show pure intent.
|
|
1706
1725
|
const isSubagentRole = roleInfo.settingsPath === "task.agentModelOverrides";
|
|
1707
1726
|
const isCurrentRow = this.#currentModel !== undefined && modelsAreEqual(this.#currentModel, item.model);
|
|
1727
|
+
const supportsServiceTier = modelSupportsServiceTier(assigned.model);
|
|
1708
1728
|
const roleFast = isSubagentRole
|
|
1709
|
-
? this.#isFastForSubagentProvider(assigned.model.provider)
|
|
1729
|
+
? this.#isFastForSubagentProvider(assigned.model.provider, supportsServiceTier)
|
|
1710
1730
|
: isCurrentRow
|
|
1711
1731
|
? this.#isCurrentModelFastModeActive()
|
|
1712
|
-
: this.#isFastForProvider(assigned.model.provider);
|
|
1732
|
+
: this.#isFastForProvider(assigned.model.provider, supportsServiceTier);
|
|
1713
1733
|
if (roleFast && isCurrentRow && !isSubagentRole) {
|
|
1714
1734
|
currentModelEffectiveGlyphRendered = true;
|
|
1715
1735
|
}
|
|
@@ -145,7 +145,7 @@ export class PlanModeController {
|
|
|
145
145
|
await this.ctx.session.abort({ timeoutMs: ABORT_TIMEOUT_MS });
|
|
146
146
|
if (this.#previousTools?.length) await this.ctx.session.setActiveToolsByName(this.#previousTools);
|
|
147
147
|
if (this.#providerSessionScope && !this.ctx.session.isStreaming) {
|
|
148
|
-
if (this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope))
|
|
148
|
+
if (await this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope))
|
|
149
149
|
this.#providerSessionScope = undefined;
|
|
150
150
|
} else if (this.#previousModelState) {
|
|
151
151
|
const previous = this.#previousModelState;
|
|
@@ -187,7 +187,7 @@ export class PlanModeController {
|
|
|
187
187
|
});
|
|
188
188
|
} catch (error) {
|
|
189
189
|
if (this.#providerSessionScope)
|
|
190
|
-
this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope);
|
|
190
|
+
await this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope);
|
|
191
191
|
this.#providerSessionScope = undefined;
|
|
192
192
|
this.ctx.showWarning(
|
|
193
193
|
`Failed to switch model after streaming: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -284,7 +284,7 @@ export class PlanModeController {
|
|
|
284
284
|
});
|
|
285
285
|
} catch (error) {
|
|
286
286
|
if (this.#providerSessionScope)
|
|
287
|
-
this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope);
|
|
287
|
+
await this.ctx.session.restoreTemporaryProviderSessionScope(this.#providerSessionScope);
|
|
288
288
|
this.#providerSessionScope = undefined;
|
|
289
289
|
this.ctx.showWarning(
|
|
290
290
|
`Failed to switch to plan model for plan mode: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -2476,8 +2476,10 @@ export class SelectorController {
|
|
|
2476
2476
|
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
2477
2477
|
activeModelProfile:
|
|
2478
2478
|
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
2479
|
-
isFastForProvider: provider =>
|
|
2480
|
-
|
|
2479
|
+
isFastForProvider: (provider, supportsServiceTier) =>
|
|
2480
|
+
this.ctx.session.isFastForProvider(provider, supportsServiceTier),
|
|
2481
|
+
isFastForSubagentProvider: (provider, supportsServiceTier) =>
|
|
2482
|
+
this.ctx.session.isFastForSubagentProvider(provider, supportsServiceTier),
|
|
2481
2483
|
isCurrentModelFastModeActive: () => this.ctx.session.isFastModeActive(),
|
|
2482
2484
|
},
|
|
2483
2485
|
);
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
* drain it, so a chatty child whose stdout is never read can still block on a
|
|
29
29
|
* full pipe. That draining is the adopter's responsibility.
|
|
30
30
|
*/
|
|
31
|
+
import * as fs from "node:fs";
|
|
31
32
|
import { logger, postmortem, ptree } from "@gajae-code/utils";
|
|
33
|
+
import { type LinuxProcPidProbeResult, probeLinuxProcPidSync } from "../gjc-runtime/linux-proc";
|
|
32
34
|
|
|
33
35
|
const DEFAULT_GRACEFUL_MS = 2_000;
|
|
34
36
|
// Hard cap for how long `dispose()` waits after SIGKILL before giving up so a
|
|
@@ -69,6 +71,63 @@ function groupAlive(pgid: number): boolean {
|
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** Whether an unreadable `/proc` entry might still describe a running process. */
|
|
75
|
+
export function procEntryMayStillBeRunning(error: unknown): boolean {
|
|
76
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
77
|
+
return code !== "ENOENT" && code !== "ESRCH";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether a process-group leader still has the spawn-time identity we own. */
|
|
81
|
+
export function groupLeaderIdentityMatches(
|
|
82
|
+
expectedStartTime: string | undefined,
|
|
83
|
+
leader: LinuxProcPidProbeResult,
|
|
84
|
+
): boolean {
|
|
85
|
+
// An absent leader cannot be a recycled leader. Its group may still contain
|
|
86
|
+
// descendants that we own, including when the root exits before we can read
|
|
87
|
+
// its start time immediately after spawn.
|
|
88
|
+
if (leader.kind === "absent") return true;
|
|
89
|
+
return expectedStartTime !== undefined && leader.kind === "live" && leader.startTime === expectedStartTime;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether a POSIX process group still has a *running* member. Zombies are
|
|
94
|
+
* inert: they execute no code and can only be reaped by their parent, which
|
|
95
|
+
* the owned process cannot control. Teardown must not wait on an external
|
|
96
|
+
* reaper, so disposal considers a group terminated once every member is dead
|
|
97
|
+
* or a zombie. On Linux this inspects `/proc`; elsewhere it conservatively
|
|
98
|
+
* falls back to {@link groupAlive} (zombies count as alive), so teardown there
|
|
99
|
+
* relies on the bounded SIGTERM/SIGKILL windows rather than zombie detection.
|
|
100
|
+
*/
|
|
101
|
+
function groupHasRunningMembers(pgid: number): boolean {
|
|
102
|
+
if (!groupAlive(pgid)) return false;
|
|
103
|
+
if (process.platform !== "linux") return true;
|
|
104
|
+
try {
|
|
105
|
+
for (const entry of fs.readdirSync("/proc")) {
|
|
106
|
+
if (entry.length === 0 || entry.charCodeAt(0) < 0x30 || entry.charCodeAt(0) > 0x39) continue;
|
|
107
|
+
let stat: string;
|
|
108
|
+
try {
|
|
109
|
+
stat = fs.readFileSync(`/proc/${entry}/stat`, "utf8");
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Entries disappearing during enumeration are expected. Every other
|
|
112
|
+
// failure leaves the member's state unknowable, so fail closed.
|
|
113
|
+
if (!procEntryMayStillBeRunning(error)) continue;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Format: pid (comm) state ppid pgrp session tty ...; comm may contain
|
|
117
|
+
// spaces or parens, so parse everything after the last ')'.
|
|
118
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
119
|
+
const state = fields[0] ?? "";
|
|
120
|
+
if (Number(fields[2]) === pgid && state !== "Z" && state !== "X") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
} catch {
|
|
126
|
+
// /proc unavailable or unreadable: keep the conservative behavior.
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
72
131
|
/** Options for {@link spawnOwnedProcess}. */
|
|
73
132
|
export interface SpawnOwnedOptions {
|
|
74
133
|
cwd?: string;
|
|
@@ -97,6 +156,14 @@ export interface AwaitExitResult {
|
|
|
97
156
|
code: number | null;
|
|
98
157
|
}
|
|
99
158
|
|
|
159
|
+
/** The observed outcome of an owned-process teardown attempt. */
|
|
160
|
+
export type OwnedProcessTeardownStatus = "terminated" | "still_running" | "identity_unverified";
|
|
161
|
+
|
|
162
|
+
/** Result of {@link OwnedProcess.dispose}. */
|
|
163
|
+
export interface OwnedProcessTeardownResult {
|
|
164
|
+
status: OwnedProcessTeardownStatus;
|
|
165
|
+
}
|
|
166
|
+
|
|
100
167
|
/** A spawned child process owned by the runtime with guaranteed teardown. */
|
|
101
168
|
export interface OwnedProcess {
|
|
102
169
|
readonly child: ptree.ChildProcess;
|
|
@@ -116,7 +183,7 @@ export interface OwnedProcess {
|
|
|
116
183
|
* abort listener and deregisters from the live-owner set only after teardown
|
|
117
184
|
* has completed. Repeated/concurrent calls return the same in-flight promise.
|
|
118
185
|
*/
|
|
119
|
-
dispose(): Promise<
|
|
186
|
+
dispose(): Promise<OwnedProcessTeardownResult>;
|
|
120
187
|
}
|
|
121
188
|
|
|
122
189
|
const liveOwners = new Set<OwnedProcess>();
|
|
@@ -155,9 +222,10 @@ export function spawnOwnedProcess(cmd: string[], opts: SpawnOwnedOptions = {}):
|
|
|
155
222
|
// On POSIX with `detached`, the child is its own process-group leader, so the
|
|
156
223
|
// group id equals its pid. `undefined` => single-process (Windows/opt-out).
|
|
157
224
|
const pgid = useGroup ? child.pid : undefined;
|
|
158
|
-
|
|
225
|
+
const groupLeader = pgid === undefined ? undefined : probeLinuxProcPidSync(pgid);
|
|
226
|
+
const groupLeaderStartTime = groupLeader?.kind === "live" ? groupLeader.startTime : undefined;
|
|
159
227
|
let disposed = false;
|
|
160
|
-
let disposePromise: Promise<
|
|
228
|
+
let disposePromise: Promise<OwnedProcessTeardownResult> | undefined;
|
|
161
229
|
let deregistered = false;
|
|
162
230
|
// Terminal once teardown/reconciliation has confirmed the group is gone. A
|
|
163
231
|
// late dispose() must then be a true no-op and never re-probe a pgid the OS
|
|
@@ -172,14 +240,28 @@ export function spawnOwnedProcess(cmd: string[], opts: SpawnOwnedOptions = {}):
|
|
|
172
240
|
}
|
|
173
241
|
};
|
|
174
242
|
|
|
175
|
-
const deregister = (): void => {
|
|
243
|
+
const deregister = (terminal = true): void => {
|
|
176
244
|
if (deregistered) return;
|
|
177
245
|
deregistered = true;
|
|
178
|
-
terminated = true;
|
|
246
|
+
if (terminal) terminated = true;
|
|
179
247
|
liveOwners.delete(owner);
|
|
180
248
|
removeAbort();
|
|
181
249
|
};
|
|
182
250
|
|
|
251
|
+
// Identity verification exists to stop a recycled pgid from being signalled.
|
|
252
|
+
// It is only decidable where the leader's spawn-time identity is readable
|
|
253
|
+
// (`/proc`). Elsewhere the group id is still ours for the lifetime of the
|
|
254
|
+
// owned child, so an unverifiable probe must not be reported as a mismatch —
|
|
255
|
+
// doing so skips SIGTERM/SIGKILL entirely and leaks the whole child tree.
|
|
256
|
+
const groupIdentityMatches = (): boolean => {
|
|
257
|
+
if (pgid === undefined) return false;
|
|
258
|
+
if (process.platform !== "linux") return true;
|
|
259
|
+
const leader = probeLinuxProcPidSync(pgid);
|
|
260
|
+
// Once the original leader has exited, its group can still own descendants.
|
|
261
|
+
// A replacement group has a leader at this pid, whose start time must match.
|
|
262
|
+
return groupLeaderIdentityMatches(groupLeaderStartTime, leader);
|
|
263
|
+
};
|
|
264
|
+
|
|
183
265
|
const signalTree = (signal: NodeJS.Signals): void => {
|
|
184
266
|
const pid = child.pid;
|
|
185
267
|
if (pid === undefined) return;
|
|
@@ -232,51 +314,68 @@ export function spawnOwnedProcess(cmd: string[], opts: SpawnOwnedOptions = {}):
|
|
|
232
314
|
if (timer) clearTimeout(timer);
|
|
233
315
|
}
|
|
234
316
|
},
|
|
235
|
-
dispose(): Promise<
|
|
317
|
+
dispose(): Promise<OwnedProcessTeardownResult> {
|
|
236
318
|
// Already terminal (e.g. clean drain reconciled and deregistered):
|
|
237
319
|
// never re-probe the pgid; treat dispose as a settled no-op.
|
|
238
320
|
if (terminated) {
|
|
239
321
|
disposed = true;
|
|
240
|
-
if (!disposePromise) disposePromise = Promise.resolve();
|
|
322
|
+
if (!disposePromise) disposePromise = Promise.resolve({ status: "terminated" });
|
|
241
323
|
return disposePromise;
|
|
242
324
|
}
|
|
243
325
|
if (disposePromise) return disposePromise;
|
|
244
326
|
disposed = true;
|
|
245
327
|
removeAbort();
|
|
246
|
-
|
|
328
|
+
const result = (status: OwnedProcessTeardownStatus): OwnedProcessTeardownResult => ({ status });
|
|
329
|
+
disposePromise = (async (): Promise<OwnedProcessTeardownResult> => {
|
|
247
330
|
try {
|
|
248
331
|
if (pgid !== undefined) {
|
|
249
|
-
// Group ownership:
|
|
250
|
-
//
|
|
251
|
-
|
|
332
|
+
// Group ownership: terminate every *running* member, even if the
|
|
333
|
+
// root has already exited (it may have backgrounded children).
|
|
334
|
+
// Liveness is measured by running members, not raw group
|
|
335
|
+
// existence: SIGTERM'd descendants become zombies that only their
|
|
336
|
+
// parent (often PID 1) can reap, and teardown must not burn its
|
|
337
|
+
// grace window waiting on an external reaper.
|
|
338
|
+
if (!groupAlive(pgid)) return result("terminated");
|
|
339
|
+
if (!groupIdentityMatches()) return result("identity_unverified");
|
|
252
340
|
signalTree("SIGTERM");
|
|
253
|
-
if (await pollUntil(() => !
|
|
341
|
+
if (await pollUntil(() => !groupHasRunningMembers(pgid), gracefulMs)) return result("terminated");
|
|
342
|
+
if (!groupIdentityMatches()) return result("identity_unverified");
|
|
254
343
|
signalTree("SIGKILL");
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
return;
|
|
344
|
+
if (await pollUntil(() => !groupHasRunningMembers(pgid), SIGKILL_REAP_CAP_MS))
|
|
345
|
+
return result("terminated");
|
|
346
|
+
logger.warn("owned process group still running after SIGKILL", {
|
|
347
|
+
name: opts.name,
|
|
348
|
+
pgid,
|
|
349
|
+
});
|
|
350
|
+
return result("still_running");
|
|
262
351
|
}
|
|
263
352
|
// Single-process fallback (Windows / processGroup:false).
|
|
264
|
-
if (child.exitCode !== null) return;
|
|
353
|
+
if (child.exitCode !== null) return result("terminated");
|
|
265
354
|
signalTree("SIGTERM");
|
|
266
|
-
if ((await owner.awaitExit({ timeoutMs: gracefulMs })).exited) return;
|
|
355
|
+
if ((await owner.awaitExit({ timeoutMs: gracefulMs })).exited) return result("terminated");
|
|
267
356
|
signalTree("SIGKILL");
|
|
268
|
-
await owner.awaitExit({ timeoutMs: SIGKILL_REAP_CAP_MS })
|
|
357
|
+
return (await owner.awaitExit({ timeoutMs: SIGKILL_REAP_CAP_MS })).exited
|
|
358
|
+
? result("terminated")
|
|
359
|
+
: result("still_running");
|
|
269
360
|
} catch (err) {
|
|
270
361
|
logger.warn("owned process dispose failed", {
|
|
271
362
|
name: opts.name,
|
|
272
363
|
error: err instanceof Error ? err.message : String(err),
|
|
273
364
|
});
|
|
274
|
-
|
|
275
|
-
// FIX: deregister only after teardown has completed so a postmortem
|
|
276
|
-
// firing mid-grace still sees the owner and awaits this dispose.
|
|
277
|
-
deregister();
|
|
365
|
+
return result("still_running");
|
|
278
366
|
}
|
|
279
|
-
})()
|
|
367
|
+
})().then(res => {
|
|
368
|
+
// `identity_unverified` and `still_running` leave the owner registered
|
|
369
|
+
// so a later postmortem or caller retry can make another bounded attempt.
|
|
370
|
+
// Only a confirmed `terminated` closes the book.
|
|
371
|
+
if (res.status === "terminated") {
|
|
372
|
+
deregister(true);
|
|
373
|
+
} else {
|
|
374
|
+
disposePromise = undefined;
|
|
375
|
+
disposed = false;
|
|
376
|
+
}
|
|
377
|
+
return res;
|
|
378
|
+
});
|
|
280
379
|
return disposePromise;
|
|
281
380
|
},
|
|
282
381
|
};
|
|
@@ -89,10 +89,10 @@ export class StdioTransport implements MCPTransport {
|
|
|
89
89
|
* Start the subprocess and begin reading.
|
|
90
90
|
*/
|
|
91
91
|
async connect(): Promise<void> {
|
|
92
|
-
if (this.#closePromise) {
|
|
93
|
-
throw new MCPExpectedFailure();
|
|
94
|
-
}
|
|
95
92
|
if (this.#connected) return;
|
|
93
|
+
if (this.#closePromise || this.#process) {
|
|
94
|
+
throw new MCPExpectedFailure(new Error("MCP stdio child teardown is incomplete"));
|
|
95
|
+
}
|
|
96
96
|
|
|
97
97
|
const args = this.config.args ?? [];
|
|
98
98
|
const env = this.config.noInheritEnv
|
|
@@ -244,7 +244,9 @@ export class StdioTransport implements MCPTransport {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
#handleClose(failure?: MCPExpectedFailure): void {
|
|
247
|
-
void this.#closeInternal(true, failure)
|
|
247
|
+
void this.#closeInternal(true, failure).catch(error => {
|
|
248
|
+
this.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
249
|
+
});
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
async request<T = unknown>(
|
|
@@ -374,11 +376,14 @@ export class StdioTransport implements MCPTransport {
|
|
|
374
376
|
|
|
375
377
|
const stdin = this.#getStdin();
|
|
376
378
|
const process = this.#process;
|
|
377
|
-
this.#process = null;
|
|
378
379
|
if (process) {
|
|
379
380
|
stdin?.end();
|
|
380
|
-
await process.dispose()
|
|
381
|
+
const teardown = await process.dispose();
|
|
381
382
|
await process.awaitExit({ timeoutMs: CLOSE_WAIT_MS }).catch(() => ({ exited: false, code: null }));
|
|
383
|
+
if (teardown.status !== "terminated") {
|
|
384
|
+
throw new MCPExpectedFailure(new Error(`stdio child teardown ${teardown.status}`));
|
|
385
|
+
}
|
|
386
|
+
this.#process = null;
|
|
382
387
|
}
|
|
383
388
|
|
|
384
389
|
if (!fromReadLoop && this.#readLoop) {
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
3
|
-
import { SdkClient, SdkClientError, type SdkFrame } from "../client";
|
|
3
|
+
import { SdkClient, SdkClientError, type SdkFrame, type SdkSentRecord } from "../client";
|
|
4
4
|
import { assertReverseResponseFrame, ReverseLeaseError } from "../host/reverse-leases";
|
|
5
5
|
import { validateAdapterControl, validateAdapterSecretFields } from "../protocol/adapter-validation";
|
|
6
6
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
@@ -224,16 +224,24 @@ export class AcpSdkAdapter {
|
|
|
224
224
|
if (isLifecycleOperation(operation) && !idempotencyKey)
|
|
225
225
|
throw new AcpSdkAdapterError("invalid_input", "idempotencyKey is required for lifecycle operations.");
|
|
226
226
|
// The broker may hold a startup in its admission queue before the readiness
|
|
227
|
-
// clock
|
|
228
|
-
// it on readiness alone times out requests the broker is still running. A
|
|
229
|
-
// request that named no readiness budget is queued for the default one, so it
|
|
230
|
-
// needs the same extension rather than the client's generic request deadline.
|
|
227
|
+
// clock starts, so the caller deadline covers the queue wait too.
|
|
231
228
|
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
try {
|
|
230
|
+
return await this.#client.global(operation, input, {
|
|
231
|
+
idempotencyKey,
|
|
232
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (
|
|
236
|
+
isLifecycleOperation(operation) &&
|
|
237
|
+
error instanceof SdkClientError &&
|
|
238
|
+
error.code === "uncertain_after_send" &&
|
|
239
|
+
error.details &&
|
|
240
|
+
typeof error.details === "object"
|
|
241
|
+
)
|
|
242
|
+
return await this.#client.lookupLifecycle(error.details as SdkSentRecord);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
async sdkControl(params: { operation: string; input?: JsonObject }): Promise<unknown> {
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
readBrokerDiscovery,
|
|
46
46
|
redactBrokerDiscovery,
|
|
47
47
|
} from "./discovery";
|
|
48
|
-
import { deriveIdempotencyIdentity } from "./identity";
|
|
48
|
+
import { deriveIdempotencyIdentity, deriveLegacyTargetIdentity } from "./identity";
|
|
49
49
|
import { canonicalDeleteLocatorPath, executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
50
50
|
import {
|
|
51
51
|
type LifecycleDurableEffectsReceipt,
|
|
@@ -237,6 +237,14 @@ function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
|
237
237
|
return response.error.code === "terminal_uncertain" ? "terminal_uncertain" : "terminal_error";
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
const LIFECYCLE_OPERATIONS = new Set([
|
|
241
|
+
"session.create",
|
|
242
|
+
"session.fork",
|
|
243
|
+
"session.resume",
|
|
244
|
+
"session.close",
|
|
245
|
+
"session.delete",
|
|
246
|
+
]);
|
|
247
|
+
|
|
240
248
|
type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
241
249
|
|
|
242
250
|
type SessionListCursor = {
|
|
@@ -457,7 +465,7 @@ function lifecycleTarget(operation: string, input: Record<string, unknown>): unk
|
|
|
457
465
|
case "session.resume":
|
|
458
466
|
case "session.close":
|
|
459
467
|
case "session.delete":
|
|
460
|
-
return { sessionId: id };
|
|
468
|
+
return { root, sessionId: id };
|
|
461
469
|
default:
|
|
462
470
|
return { operation, root, sessionId: id };
|
|
463
471
|
}
|
|
@@ -1411,6 +1419,7 @@ export class Broker {
|
|
|
1411
1419
|
elevationRequestId?: string,
|
|
1412
1420
|
): Promise<BrokerResponse> {
|
|
1413
1421
|
if (this.#stopping) return error("broker_restarting", "broker is stopping");
|
|
1422
|
+
const fingerprint = JSON.stringify({ operation, input: JSON.parse(JSON.stringify(input)) });
|
|
1414
1423
|
const normalization = normalizeBrokerInput(operation, input);
|
|
1415
1424
|
if (isBrokerResponse(normalization)) return normalization;
|
|
1416
1425
|
const approvedInput = input;
|
|
@@ -1454,6 +1463,32 @@ export class Broker {
|
|
|
1454
1463
|
if (operation === "elevation.issue") return this.#elevationIssueRequest(input);
|
|
1455
1464
|
if (operation === "elevation.status") return this.#elevationStatusRequest(input);
|
|
1456
1465
|
if (operation === "session.control") return this.#sessionControlRequest(input);
|
|
1466
|
+
if (operation === "broker.lookup_lifecycle") {
|
|
1467
|
+
const requestedOperation = typeof input.operation === "string" ? input.operation : undefined;
|
|
1468
|
+
const fingerprint = typeof input.fingerprint === "string" ? input.fingerprint : undefined;
|
|
1469
|
+
if (!idempotencyKey || !requestedOperation || !fingerprint)
|
|
1470
|
+
return error("invalid_input", "operation, idempotencyKey, and fingerprint are required");
|
|
1471
|
+
if (!LIFECYCLE_OPERATIONS.has(requestedOperation))
|
|
1472
|
+
return error("not_found", "lifecycle operation was not found");
|
|
1473
|
+
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
|
|
1474
|
+
const entry = this.ledger.get(identity);
|
|
1475
|
+
if (!entry) return error("not_found", "lifecycle operation was not found");
|
|
1476
|
+
if (entry.fingerprint !== fingerprint)
|
|
1477
|
+
return error("idempotency_conflict", "lifecycle request fingerprint differs");
|
|
1478
|
+
if (entry.state === "terminal_uncertain")
|
|
1479
|
+
return {
|
|
1480
|
+
ok: false,
|
|
1481
|
+
error: { code: "terminal_uncertain", message: "lifecycle outcome is still uncertain" },
|
|
1482
|
+
};
|
|
1483
|
+
// Reconcile to the terminal original response contract: callers expect
|
|
1484
|
+
// the BrokerResponse they would have received, not a lookup envelope.
|
|
1485
|
+
return (
|
|
1486
|
+
(entry.response as BrokerResponse) ?? {
|
|
1487
|
+
ok: false,
|
|
1488
|
+
error: { code: "terminal_uncertain", message: "lifecycle outcome has no recorded response" },
|
|
1489
|
+
}
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1457
1492
|
if (!idempotencyKey) return error("invalid_input", "idempotencyKey is required for lifecycle operations");
|
|
1458
1493
|
// Elevation gate (F1.3): allowlisted raw operations execute only behind a
|
|
1459
1494
|
// broker-owned single-use grant. Validation is fail-closed and happens
|
|
@@ -1504,7 +1539,20 @@ export class Broker {
|
|
|
1504
1539
|
const target = createHash("sha256")
|
|
1505
1540
|
.update(canonicalJson(lifecycleTarget(operation, input)))
|
|
1506
1541
|
.digest("hex");
|
|
1507
|
-
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey
|
|
1542
|
+
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey);
|
|
1543
|
+
const operationKey = `${operation}\0${idempotencyKey}`;
|
|
1544
|
+
const legacyIdentity = await deriveLegacyTargetIdentity(
|
|
1545
|
+
this.settings.agentDir,
|
|
1546
|
+
operation,
|
|
1547
|
+
idempotencyKey,
|
|
1548
|
+
target,
|
|
1549
|
+
);
|
|
1550
|
+
if (!this.ledger.get(identity)) {
|
|
1551
|
+
if (await this.ledger.migrateIdentity(legacyIdentity, identity, { operationKey, fingerprint })) {
|
|
1552
|
+
// Exact target-derived legacy identity migrated without granting new authority.
|
|
1553
|
+
} else if (this.ledger.findByOperationKey(operationKey) || this.ledger.hasLegacyIdentity())
|
|
1554
|
+
return error("idempotency_conflict", "legacy lifecycle request has an ambiguous target");
|
|
1555
|
+
}
|
|
1508
1556
|
let reconstructedDeleteCleanup: BrokerCleanupEvidence | undefined;
|
|
1509
1557
|
if (operation === "session.delete" && input.cwd === undefined && input.sessionPath === undefined) {
|
|
1510
1558
|
const entry = this.ledger.get(identity);
|
|
@@ -1570,7 +1618,10 @@ export class Broker {
|
|
|
1570
1618
|
await prev;
|
|
1571
1619
|
try {
|
|
1572
1620
|
const beforeBegin = this.ledger.get(identity);
|
|
1573
|
-
const begun = await this.ledger.begin(identity, requestHash
|
|
1621
|
+
const begun = await this.ledger.begin(identity, requestHash, {
|
|
1622
|
+
operationKey,
|
|
1623
|
+
fingerprint,
|
|
1624
|
+
});
|
|
1574
1625
|
if (begun.kind === "replay") {
|
|
1575
1626
|
const replay = begun.entry.response as BrokerResponse;
|
|
1576
1627
|
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { type BrokerDiscovery, brokerProcessIncarnation, readBrokerDiscovery } from "./discovery";
|
|
7
7
|
import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
|
|
8
|
+
import { BrokerStartupError, clearBrokerStartupFailureMarker, readBrokerStartupFailureMarker } from "./startup-failure";
|
|
8
9
|
export interface EnsureBrokerSettings {
|
|
9
10
|
agentDir: string;
|
|
10
11
|
heartbeatTtlMs?: number;
|
|
@@ -301,6 +302,8 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
301
302
|
|
|
302
303
|
const command = resolveSdkInternalSpawnCommand("broker-internal");
|
|
303
304
|
const spawnLog = await openBrokerSpawnLog(settings.agentDir);
|
|
305
|
+
// A stale marker must never be misattributed to this spawn; clear it first.
|
|
306
|
+
await clearBrokerStartupFailureMarker(settings.agentDir);
|
|
304
307
|
try {
|
|
305
308
|
const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
|
|
306
309
|
detached: true,
|
|
@@ -358,16 +361,16 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
358
361
|
}
|
|
359
362
|
}
|
|
360
363
|
const spawnLogTail = exitedBeforeDiscovery && spawnLog ? await readBrokerSpawnLogTail(spawnLog.path) : "";
|
|
364
|
+
const marker = await readBrokerStartupFailureMarker(settings.agentDir);
|
|
361
365
|
const failure = spawnError
|
|
362
366
|
? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
|
|
363
367
|
: exitedBeforeDiscovery
|
|
364
|
-
? new
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
)
|
|
368
|
+
? new BrokerStartupError({
|
|
369
|
+
exitCode: child.exitCode,
|
|
370
|
+
signal: child.signalCode,
|
|
371
|
+
reason: marker?.reason ?? "Detached SDK broker exited before publishing discovery.",
|
|
372
|
+
stderrExcerpt: spawnLogTail.length > 0 ? spawnLogTail : undefined,
|
|
373
|
+
})
|
|
371
374
|
: discoveryError
|
|
372
375
|
? discoveryError
|
|
373
376
|
: new Error("Timed out waiting for detached SDK broker discovery.");
|
|
@@ -40,6 +40,16 @@ export async function getBrokerIdentityKey(agentDir: string): Promise<string> {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export async function deriveIdempotencyIdentity(
|
|
43
|
+
agentDir: string,
|
|
44
|
+
operation: string,
|
|
45
|
+
callerKey: string,
|
|
46
|
+
_protocolVersionOrLegacyTargetHash?: string,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
const key = await getBrokerIdentityKey(agentDir);
|
|
49
|
+
return createHmac("sha256", Buffer.from(key, "hex")).update(`3|${operation}|${callerKey}`).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function deriveLegacyTargetIdentity(
|
|
43
53
|
agentDir: string,
|
|
44
54
|
operation: string,
|
|
45
55
|
callerKey: string,
|