@gajae-code/coding-agent 0.18.5 → 0.18.6
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 +101 -0
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/sdk/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -1
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -182
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +59 -0
- package/src/commands/update.ts +15 -1
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-profile-activation.ts +70 -4
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +23 -31
- package/src/config/model-resolver.ts +9 -1
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +649 -367
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/sdk/broker/broker.ts +46 -0
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +225 -341
- package/src/sdk/broker/runtime.ts +5 -1
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +34 -1
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +40 -2
- package/src/sdk/host/session-runtime.ts +13 -5
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +7 -1
- package/src/sdk/session.ts +37 -8
- package/src/session/agent-session.ts +114 -39
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +96 -3
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +38 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
|
@@ -965,7 +965,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
965
965
|
if (this.#expanded && !previews?.some(preview => preview.diff)) {
|
|
966
966
|
const editMode = this.#editMode;
|
|
967
967
|
const strategy = editMode ? EDIT_MODE_STRATEGIES[editMode] : undefined;
|
|
968
|
-
const fallback = strategy?.
|
|
968
|
+
const fallback = strategy?.renderStreamingPlaceholder(this.#args, theme);
|
|
969
969
|
if (fallback) context.editStreamingFallback = fallback;
|
|
970
970
|
}
|
|
971
971
|
context.renderDiff = renderDiff;
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -8,6 +8,8 @@ Asks user when you need clarification or input during task execution.
|
|
|
8
8
|
- Use `recommended: <index>` to mark default (0-indexed); " (Recommended)" added automatically
|
|
9
9
|
- Use `questions` for multiple related questions instead of asking one at a time
|
|
10
10
|
- Set `multi: true` on question to allow multiple selections
|
|
11
|
+
- Every question body must contain meaningful, non-whitespace text. Never add empty `metadata`/`ignore` questions.
|
|
12
|
+
- While deep-interview is in the `interviewing` phase, submit exactly one question with the required `deepInterview` metadata on that question. Round 0 needs `round: 0`, `component: "review-topology"`, `dimension: "topology"`, `ambiguity`, and `intent_contract` containing the displayed items and affirmative labels. Later questions need a positive round, component, dimension, and ambiguity. Question prose and `workflowGate` alone cannot record an interview answer. After final-spec persistence transitions to `handoff`, next-workflow/approval choices are ordinary asks without interview round metadata.
|
|
11
13
|
</instruction>
|
|
12
14
|
|
|
13
15
|
<caution>
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -1495,6 +1495,16 @@ export class Broker {
|
|
|
1495
1495
|
discovery: BrokerDiscovery | null = null;
|
|
1496
1496
|
#lock: string;
|
|
1497
1497
|
#owner = randomBytes(12).toString("hex");
|
|
1498
|
+
/** Public publication-incarnation id, fixed at startup; never the endpoint generation. */
|
|
1499
|
+
#diagnosticGeneration = randomBytes(16).toString("hex");
|
|
1500
|
+
/** Trusted build snapshot captured at startup; unknown buildId stays null. */
|
|
1501
|
+
#startupBuild: { packageVersion: string; buildId: string | null } = {
|
|
1502
|
+
packageVersion: packageJson.version,
|
|
1503
|
+
buildId:
|
|
1504
|
+
typeof process.env.GJC_BUILD_ID === "string" && process.env.GJC_BUILD_ID.length > 0
|
|
1505
|
+
? process.env.GJC_BUILD_ID
|
|
1506
|
+
: null,
|
|
1507
|
+
};
|
|
1498
1508
|
#sessionListCursors = new Map<string, SessionListCursor>();
|
|
1499
1509
|
#chains = new Map<string, Promise<void>>();
|
|
1500
1510
|
#spawnInFlight = new Map<string, SpawnInFlight>();
|
|
@@ -3868,6 +3878,10 @@ export class Broker {
|
|
|
3868
3878
|
ownerId: this.#owner,
|
|
3869
3879
|
pid: process.pid,
|
|
3870
3880
|
incarnation,
|
|
3881
|
+
// Observation capability: the startup-fixed generation and the protocol the
|
|
3882
|
+
// broker answers. An old broker simply omits both.
|
|
3883
|
+
diagnosticGeneration: this.#diagnosticGeneration,
|
|
3884
|
+
diagnosticProtocol: 1,
|
|
3871
3885
|
host: "127.0.0.1",
|
|
3872
3886
|
port,
|
|
3873
3887
|
url: `ws://127.0.0.1:${port}`,
|
|
@@ -4728,6 +4742,32 @@ export class Broker {
|
|
|
4728
4742
|
? entry.response
|
|
4729
4743
|
: error("terminal_uncertain", "lifecycle outcome has no recorded response");
|
|
4730
4744
|
}
|
|
4745
|
+
/**
|
|
4746
|
+
* Published diagnostics for observation clients (SPEC "Result contract").
|
|
4747
|
+
*
|
|
4748
|
+
* Everything here is captured once at startup and never recomputed, so a later source
|
|
4749
|
+
* or package change cannot alter what a running broker reports. No token, socket
|
|
4750
|
+
* coordinate, argv or environment value is included; the identity fields are the
|
|
4751
|
+
* internal owner/process/root binding the observer already knows from discovery.
|
|
4752
|
+
*/
|
|
4753
|
+
#publishedDiagnostics(): Promise<BrokerResponse> {
|
|
4754
|
+
// Observation is answered ONLY from an already-owned healthy retained publication.
|
|
4755
|
+
// A broker that owns no publication refuses here and recovers nothing: it does not
|
|
4756
|
+
// publish, ensure, restart or touch the authority to answer.
|
|
4757
|
+
if (this.#publication === null || this.#publicationState !== "healthy-owned") {
|
|
4758
|
+
return Promise.resolve(error("unavailable", "broker publication authority is not owned"));
|
|
4759
|
+
}
|
|
4760
|
+
return Promise.resolve({
|
|
4761
|
+
ok: true,
|
|
4762
|
+
result: {
|
|
4763
|
+
diagnosticProtocol: 1,
|
|
4764
|
+
generation: this.#diagnosticGeneration,
|
|
4765
|
+
build: this.#startupBuild,
|
|
4766
|
+
identity: { ownerId: this.#owner, pid: process.pid, agentRoot: this.settings.agentDir },
|
|
4767
|
+
},
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4731
4771
|
// Wire requests are readiness-gated by BrokerTransport before reaching this dispatcher.
|
|
4732
4772
|
handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse> {
|
|
4733
4773
|
if (operation === "broker.status") return Promise.resolve({ ok: true, result: this.status() });
|
|
@@ -4768,6 +4808,7 @@ export class Broker {
|
|
|
4768
4808
|
const spawnClose = await this.#maybeCloseSpawnChild(input);
|
|
4769
4809
|
if (spawnClose) return spawnClose;
|
|
4770
4810
|
}
|
|
4811
|
+
if (operation === "broker.diagnostics") return this.#publishedDiagnostics();
|
|
4771
4812
|
if (operation === "session.control") return this.#sessionControl(input, idempotencyKey);
|
|
4772
4813
|
if (operation === "session.lookup") {
|
|
4773
4814
|
const lookup = publicLifecycleLookupInput(input);
|
|
@@ -4937,6 +4978,11 @@ export class Broker {
|
|
|
4937
4978
|
...(operation === "session.close" && typeof input.sessionId === "string"
|
|
4938
4979
|
? { intendedSessionId: input.sessionId }
|
|
4939
4980
|
: {}),
|
|
4981
|
+
...(operation === "session.close" &&
|
|
4982
|
+
typeof input.endpointGeneration === "number" &&
|
|
4983
|
+
typeof input.endpointIncarnation === "string"
|
|
4984
|
+
? { closeAuthorityBound: true }
|
|
4985
|
+
: {}),
|
|
4940
4986
|
};
|
|
4941
4987
|
if (!this.ledger.get(identity)) {
|
|
4942
4988
|
const matchingOperation = this.ledger.findAnyByOperationKey(operationKey);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
2
|
import { type BrokerDiscovery, readBrokerDiscovery, readBrokerRestartIntent } from "./discovery";
|
|
3
3
|
import { withBrokerStartupLock } from "./ensure";
|
|
4
|
-
import { observeProcessIncarnation } from "./process-incarnation";
|
|
4
|
+
import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
|
|
5
5
|
import { resolveSdkInternalSpawnCommand } from "./runtime";
|
|
6
6
|
|
|
7
7
|
export interface AuthorizedBrokerSuccessorOptions {
|
|
@@ -61,12 +61,15 @@ export async function launchAuthorizedBrokerSuccessor(
|
|
|
61
61
|
existing.restartRequestId === options.requestId
|
|
62
62
|
)
|
|
63
63
|
return { kind: "adopted" as const, discovery: existing };
|
|
64
|
-
const command = resolveSdkInternalSpawnCommand(
|
|
64
|
+
const command = resolveSdkInternalSpawnCommand(
|
|
65
|
+
process.platform === "win32" ? "broker-internal" : "broker-trampoline-internal",
|
|
66
|
+
);
|
|
65
67
|
let child: ChildProcess;
|
|
68
|
+
const isTrampoline = process.platform !== "win32";
|
|
66
69
|
try {
|
|
67
70
|
child = spawn(command.file, [...command.args, "--agent-dir", options.agentDir], {
|
|
68
71
|
detached: true,
|
|
69
|
-
stdio: "ignore",
|
|
72
|
+
stdio: isTrampoline ? ["ignore", "pipe", "ignore"] : "ignore",
|
|
70
73
|
env: { ...command.env, GJC_BROKER_RESTART_REQUEST: options.requestId },
|
|
71
74
|
...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
|
|
72
75
|
});
|
|
@@ -78,14 +81,44 @@ export async function launchAuthorizedBrokerSuccessor(
|
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
let spawnError: Error | undefined;
|
|
84
|
+
let trampolineOutput = "";
|
|
85
|
+
let trampolineOutputEnded = !isTrampoline;
|
|
86
|
+
if (child.stdout)
|
|
87
|
+
child.stdout
|
|
88
|
+
.setEncoding("utf8")
|
|
89
|
+
.on("data", chunk => (trampolineOutput += chunk))
|
|
90
|
+
.once("end", () => (trampolineOutputEnded = true));
|
|
81
91
|
child.once("error", childError => {
|
|
82
92
|
spawnError = childError;
|
|
83
93
|
});
|
|
84
94
|
child.unref();
|
|
85
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
kind: "spawned" as const,
|
|
97
|
+
child,
|
|
98
|
+
isTrampoline,
|
|
99
|
+
trampolineOutput: () => trampolineOutput,
|
|
100
|
+
trampolineOutputEnded: () => trampolineOutputEnded,
|
|
101
|
+
spawnError: () => spawnError,
|
|
102
|
+
};
|
|
86
103
|
});
|
|
87
104
|
if (spawnOutcome.kind !== "spawned") return spawnOutcome;
|
|
88
105
|
const { child } = spawnOutcome;
|
|
106
|
+
let successorPid: number | undefined;
|
|
107
|
+
let successorIncarnation: string | undefined;
|
|
108
|
+
let trampolineExited = !spawnOutcome.isTrampoline;
|
|
109
|
+
let trampolineExitCode: number | null = null;
|
|
110
|
+
let trampolineSignal: NodeJS.Signals | null = null;
|
|
111
|
+
if (spawnOutcome.isTrampoline)
|
|
112
|
+
child.once("exit", (code, signal) => {
|
|
113
|
+
trampolineExitCode = code;
|
|
114
|
+
trampolineSignal = signal;
|
|
115
|
+
trampolineExited = true;
|
|
116
|
+
});
|
|
117
|
+
if (spawnOutcome.isTrampoline && (child.exitCode !== null || child.signalCode !== null)) {
|
|
118
|
+
trampolineExitCode = child.exitCode;
|
|
119
|
+
trampolineSignal = child.signalCode;
|
|
120
|
+
trampolineExited = true;
|
|
121
|
+
}
|
|
89
122
|
const until = Math.min(Date.now() + Math.max(1, options.deadlineAt - Date.now()), options.deadlineAt);
|
|
90
123
|
for (;;) {
|
|
91
124
|
if (spawnOutcome.spawnError())
|
|
@@ -94,7 +127,34 @@ export async function launchAuthorizedBrokerSuccessor(
|
|
|
94
127
|
reason: "spawn_failed",
|
|
95
128
|
detail: spawnOutcome.spawnError()?.message,
|
|
96
129
|
};
|
|
97
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
130
|
+
if (!spawnOutcome.isTrampoline && (child.exitCode !== null || child.signalCode !== null))
|
|
131
|
+
return { kind: "refused", reason: "spawn_exited_before_publication" };
|
|
132
|
+
if (spawnOutcome.isTrampoline && !successorPid) {
|
|
133
|
+
const report = spawnOutcome.trampolineOutput().trim();
|
|
134
|
+
const match = /^(\d+)\t([^\r\n]+)$/.exec(report);
|
|
135
|
+
if (match) {
|
|
136
|
+
const pid = Number(match[1]);
|
|
137
|
+
const incarnation = match[2];
|
|
138
|
+
if (Number.isSafeInteger(pid) && pid > 0 && isProcessIncarnation(incarnation)) {
|
|
139
|
+
const observed = observeProcessIncarnation(pid);
|
|
140
|
+
if (observed.status === "present" && observed.incarnation === incarnation) {
|
|
141
|
+
successorPid = pid;
|
|
142
|
+
successorIncarnation = incarnation;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (successorPid) {
|
|
148
|
+
const observed = observeProcessIncarnation(successorPid);
|
|
149
|
+
if (
|
|
150
|
+
observed.status === "absent" ||
|
|
151
|
+
(observed.status === "present" && observed.incarnation !== successorIncarnation)
|
|
152
|
+
)
|
|
153
|
+
return { kind: "refused", reason: "spawn_exited_before_publication" };
|
|
154
|
+
}
|
|
155
|
+
if (spawnOutcome.isTrampoline && trampolineExited && spawnOutcome.trampolineOutputEnded() && !successorPid)
|
|
156
|
+
return { kind: "refused", reason: "spawn_exited_before_publication" };
|
|
157
|
+
if (spawnOutcome.isTrampoline && trampolineExited && (trampolineSignal !== null || trampolineExitCode !== 0))
|
|
98
158
|
return { kind: "refused", reason: "spawn_exited_before_publication" };
|
|
99
159
|
const discovered = await readBrokerDiscovery(options.agentDir);
|
|
100
160
|
if (
|
|
@@ -102,7 +162,8 @@ export async function launchAuthorizedBrokerSuccessor(
|
|
|
102
162
|
discovered.packageGeneration === options.packageGeneration &&
|
|
103
163
|
discovered.restartRequestId === options.requestId
|
|
104
164
|
)
|
|
105
|
-
|
|
165
|
+
if (!spawnOutcome.isTrampoline || (trampolineExited && spawnOutcome.trampolineOutputEnded()))
|
|
166
|
+
return { kind: "spawned", discovery: discovered };
|
|
106
167
|
if (Date.now() >= until) return { kind: "refused", reason: "publication_timeout" };
|
|
107
168
|
await Bun.sleep(SUCCESSOR_SPAWN_POLL_MS);
|
|
108
169
|
}
|
|
@@ -386,6 +386,14 @@ export interface BrokerDiscovery {
|
|
|
386
386
|
ownerId: string;
|
|
387
387
|
pid: number;
|
|
388
388
|
incarnation: string;
|
|
389
|
+
/**
|
|
390
|
+
* Public publication-incarnation id for diagnostic observation, fixed at startup.
|
|
391
|
+
* Absent on publications written before the capability existed; an observer then
|
|
392
|
+
* reports `unsupported` instead of synthesizing compatibility.
|
|
393
|
+
*/
|
|
394
|
+
diagnosticGeneration?: string;
|
|
395
|
+
/** Diagnostic protocol the publisher answers; absent means no capability. */
|
|
396
|
+
diagnosticProtocol?: number;
|
|
389
397
|
host: "127.0.0.1";
|
|
390
398
|
port: number;
|
|
391
399
|
url: string;
|
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readBrokerDiscovery,
|
|
15
15
|
readBrokerRestartIntent,
|
|
16
16
|
} from "./discovery";
|
|
17
|
+
import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
|
|
17
18
|
import {
|
|
18
19
|
isSdkInternalRuntimeImagePresent,
|
|
19
20
|
resolveSdkInternalSpawnCommand,
|
|
@@ -369,6 +370,7 @@ interface BrokerOwner {
|
|
|
369
370
|
stop(): Promise<void>;
|
|
370
371
|
canReuse(discovery: BrokerDiscovery | null): boolean;
|
|
371
372
|
markReady(discovery: BrokerDiscovery): boolean;
|
|
373
|
+
setTarget(pid: number, incarnation: string): void;
|
|
372
374
|
}
|
|
373
375
|
type EnsureInitiator = "discovery" | "fixture-lease";
|
|
374
376
|
type EnsureOutcome =
|
|
@@ -453,25 +455,60 @@ async function reapSpawnedBroker(child: ChildProcess, timing: ReapTiming = DEFAU
|
|
|
453
455
|
throw new Error(`Detached SDK broker (pid ${child.pid}) did not exit after SIGKILL during reap.`);
|
|
454
456
|
}
|
|
455
457
|
|
|
458
|
+
async function reapSpawnedBrokerIdentity(
|
|
459
|
+
pid: number,
|
|
460
|
+
incarnation: string,
|
|
461
|
+
timing: ReapTiming = DEFAULT_REAP_TIMING,
|
|
462
|
+
): Promise<void> {
|
|
463
|
+
const matches = (): boolean => {
|
|
464
|
+
const observed = observeProcessIncarnation(pid);
|
|
465
|
+
return observed.status === "present" && observed.incarnation === incarnation;
|
|
466
|
+
};
|
|
467
|
+
const gone = (): boolean => observeProcessIncarnation(pid).status === "absent";
|
|
468
|
+
const signal = (sig: NodeJS.Signals): void => {
|
|
469
|
+
if (!matches()) return;
|
|
470
|
+
try {
|
|
471
|
+
process.kill(pid, sig);
|
|
472
|
+
} catch {
|
|
473
|
+
// already exited between identity check and signal delivery
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
if (gone() || !matches()) return;
|
|
477
|
+
// A reported trampoline has already detached this broker from the retained
|
|
478
|
+
// child handle, so graceful shutdown cannot be observed or reaped reliably.
|
|
479
|
+
// Force termination keeps discovery-timeout cleanup within its caller's
|
|
480
|
+
// bounded deadline while the identity check still prevents PID reuse.
|
|
481
|
+
signal("SIGKILL");
|
|
482
|
+
await Promise.race([waitForProcessAbsence(pid), sleep(timing.killVerifyMs)]);
|
|
483
|
+
if (!gone()) throw new Error(`Detached SDK broker (pid ${pid}) did not exit after SIGKILL during reap.`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function waitForProcessAbsence(pid: number): Promise<void> {
|
|
487
|
+
while (observeProcessIncarnation(pid).status !== "absent") await Bun.sleep(10);
|
|
488
|
+
}
|
|
489
|
+
|
|
456
490
|
function registerBrokerOwner(
|
|
457
491
|
agentDir: string,
|
|
458
492
|
child: ChildProcess,
|
|
459
493
|
timing: ReapTiming = DEFAULT_REAP_TIMING,
|
|
460
494
|
): BrokerOwner {
|
|
461
|
-
|
|
495
|
+
let targetPid = child.pid;
|
|
496
|
+
let targetIncarnation = child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
|
|
462
497
|
let state: "starting" | "ready" | "cleanup-unverified" = "starting";
|
|
463
498
|
const matches = (discovery: BrokerDiscovery | null): boolean =>
|
|
464
499
|
Boolean(
|
|
465
500
|
discovery &&
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
discovery.pid ===
|
|
469
|
-
discovery.incarnation ===
|
|
501
|
+
targetPid !== undefined &&
|
|
502
|
+
targetIncarnation !== undefined &&
|
|
503
|
+
discovery.pid === targetPid &&
|
|
504
|
+
discovery.incarnation === targetIncarnation,
|
|
470
505
|
);
|
|
471
506
|
const owner: BrokerOwner = {
|
|
472
507
|
async stop(): Promise<void> {
|
|
473
508
|
try {
|
|
474
|
-
|
|
509
|
+
if (targetPid !== undefined && targetIncarnation !== undefined && targetPid !== child.pid)
|
|
510
|
+
await reapSpawnedBrokerIdentity(targetPid, targetIncarnation, timing);
|
|
511
|
+
else await reapSpawnedBroker(child, timing);
|
|
475
512
|
} catch (error) {
|
|
476
513
|
state = "cleanup-unverified";
|
|
477
514
|
throw error;
|
|
@@ -486,6 +523,10 @@ function registerBrokerOwner(
|
|
|
486
523
|
state = "ready";
|
|
487
524
|
return true;
|
|
488
525
|
},
|
|
526
|
+
setTarget(pid, incarnation): void {
|
|
527
|
+
targetPid = pid;
|
|
528
|
+
targetIncarnation = incarnation;
|
|
529
|
+
},
|
|
489
530
|
};
|
|
490
531
|
owners.set(agentDir, owner);
|
|
491
532
|
return owner;
|
|
@@ -688,7 +729,11 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
688
729
|
// that finished between our first read and the lock acquisition.
|
|
689
730
|
const discoveredUnderLock = await reconcileBrokerGenerationForStartup(settings, deadline);
|
|
690
731
|
if (discoveredUnderLock) return { kind: "external-discovery", discovery: discoveredUnderLock };
|
|
691
|
-
const command = resolveSdkInternalSpawnCommand(
|
|
732
|
+
const command = resolveSdkInternalSpawnCommand(
|
|
733
|
+
process.platform === "win32" || initiator === "fixture-lease"
|
|
734
|
+
? "broker-internal"
|
|
735
|
+
: "broker-trampoline-internal",
|
|
736
|
+
);
|
|
692
737
|
spawnLog = await openBrokerSpawnLog(settings.agentDir);
|
|
693
738
|
// A stale marker must never be misattributed to this spawn; clear it first.
|
|
694
739
|
await clearBrokerStartupExitRecord(settings.agentDir);
|
|
@@ -696,23 +741,83 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
696
741
|
const childSpawnedAt = Date.now();
|
|
697
742
|
const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
|
|
698
743
|
detached: true,
|
|
699
|
-
stdio: ["ignore", "ignore", spawnLog ? spawnLog.handle.fd : "ignore"],
|
|
744
|
+
stdio: ["ignore", process.platform === "win32" ? "ignore" : "pipe", spawnLog ? spawnLog.handle.fd : "ignore"],
|
|
700
745
|
env: brokerSpawnEnvironment(command, settings.env),
|
|
701
746
|
...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
|
|
702
747
|
});
|
|
748
|
+
const isTrampoline = process.platform !== "win32" && initiator !== "fixture-lease";
|
|
703
749
|
let spawnError: Error | undefined;
|
|
750
|
+
let childExited = false;
|
|
751
|
+
let childExitCode: number | null = null;
|
|
752
|
+
let childSignalCode: NodeJS.Signals | null = null;
|
|
753
|
+
let brokerDeathObserved = false;
|
|
704
754
|
child.once("error", error => {
|
|
705
755
|
spawnError = error;
|
|
706
756
|
});
|
|
707
|
-
|
|
757
|
+
child.once("close", (code, signal) => {
|
|
758
|
+
childExited = true;
|
|
759
|
+
childExitCode = code;
|
|
760
|
+
childSignalCode = signal;
|
|
761
|
+
});
|
|
762
|
+
let trampolineReported = !isTrampoline;
|
|
763
|
+
let spawnedBrokerPid = isTrampoline ? undefined : child.pid;
|
|
764
|
+
let spawnedBrokerIncarnation =
|
|
765
|
+
isTrampoline || child.pid === undefined ? undefined : brokerProcessIncarnation(child.pid);
|
|
708
766
|
const owner = registerBrokerOwner(settings.agentDir, child);
|
|
767
|
+
if (isTrampoline && child.stdout) {
|
|
768
|
+
let output = "";
|
|
769
|
+
child.stdout.on("data", chunk => {
|
|
770
|
+
output += Buffer.from(chunk).toString("utf8");
|
|
771
|
+
const newline = output.indexOf("\n");
|
|
772
|
+
if (newline < 0) return;
|
|
773
|
+
const line = output.slice(0, newline).trim();
|
|
774
|
+
const [rawPid, rawIncarnation] = line?.split("\t") ?? [];
|
|
775
|
+
const pid = rawPid ? Number(rawPid) : NaN;
|
|
776
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return;
|
|
777
|
+
if (isProcessIncarnation(rawIncarnation)) {
|
|
778
|
+
const incarnation = rawIncarnation;
|
|
779
|
+
trampolineReported = true;
|
|
780
|
+
spawnedBrokerPid = pid;
|
|
781
|
+
spawnedBrokerIncarnation = incarnation;
|
|
782
|
+
owner.setTarget(pid, incarnation);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
709
786
|
child.unref();
|
|
710
787
|
// The child holds its own duplicate of the descriptor. Failure to close the
|
|
711
788
|
// parent's diagnostic handle must not discard exact ownership of a live child.
|
|
712
789
|
await spawnLog?.handle.close().catch(() => undefined);
|
|
713
790
|
let discoveryError: unknown;
|
|
714
791
|
while (ensureBrokerTiming.now() < deadline) {
|
|
715
|
-
if (
|
|
792
|
+
if (
|
|
793
|
+
spawnError ||
|
|
794
|
+
(isTrampoline && childExited && (!trampolineReported || childExitCode !== 0 || childSignalCode !== null)) ||
|
|
795
|
+
(!isTrampoline && process.platform === "win32" && (child.exitCode !== null || child.signalCode !== null))
|
|
796
|
+
)
|
|
797
|
+
break;
|
|
798
|
+
if (
|
|
799
|
+
isTrampoline &&
|
|
800
|
+
trampolineReported &&
|
|
801
|
+
spawnedBrokerPid !== undefined &&
|
|
802
|
+
spawnedBrokerIncarnation !== undefined
|
|
803
|
+
) {
|
|
804
|
+
const observed = observeProcessIncarnation(spawnedBrokerPid);
|
|
805
|
+
if (observed.status === "present" && observed.incarnation !== spawnedBrokerIncarnation) {
|
|
806
|
+
brokerDeathObserved = true;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
if (observed.status === "absent") {
|
|
810
|
+
brokerDeathObserved = true;
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// The trampoline's stdout line identifies the real broker. Do not read
|
|
815
|
+
// discovery until that line has been parsed, or a marker from the
|
|
816
|
+
// trampoline could be trusted as the broker's own startup evidence.
|
|
817
|
+
if (isTrampoline && !trampolineReported) {
|
|
818
|
+
await ensureBrokerTiming.sleep(50);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
716
821
|
try {
|
|
717
822
|
const discovered = await readBrokerDiscoveryBeforeDeadline(
|
|
718
823
|
settings.agentDir,
|
|
@@ -724,6 +829,20 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
724
829
|
await ensureBrokerTiming.sleep(50);
|
|
725
830
|
continue;
|
|
726
831
|
}
|
|
832
|
+
if (isTrampoline && !childExited) {
|
|
833
|
+
await ensureBrokerTiming.sleep(50);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (isTrampoline && spawnedBrokerPid !== undefined && spawnedBrokerIncarnation !== undefined) {
|
|
837
|
+
const observed = observeProcessIncarnation(spawnedBrokerPid);
|
|
838
|
+
if (
|
|
839
|
+
observed.status === "absent" ||
|
|
840
|
+
(observed.status === "present" && observed.incarnation !== spawnedBrokerIncarnation)
|
|
841
|
+
) {
|
|
842
|
+
brokerDeathObserved = true;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
727
846
|
if (owner.markReady(discovered)) {
|
|
728
847
|
return initiator === "fixture-lease"
|
|
729
848
|
? { kind: "local-started-fixture", discovery: discovered, owner, child }
|
|
@@ -737,8 +856,13 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
737
856
|
}
|
|
738
857
|
await ensureBrokerTiming.sleep(50);
|
|
739
858
|
}
|
|
740
|
-
const exitedBeforeDiscovery =
|
|
741
|
-
|
|
859
|
+
const exitedBeforeDiscovery =
|
|
860
|
+
(isTrampoline &&
|
|
861
|
+
(!trampolineReported ||
|
|
862
|
+
(childExited && (childExitCode !== 0 || childSignalCode !== null)) ||
|
|
863
|
+
brokerDeathObserved)) ||
|
|
864
|
+
(!isTrampoline && process.platform === "win32" && (child.exitCode !== null || child.signalCode !== null));
|
|
865
|
+
if (!isTrampoline && exitedBeforeDiscovery && childExitCode === 0) {
|
|
742
866
|
// A clean exit means another broker won the ownership lock (two ACP
|
|
743
867
|
// processes racing a cold broker state, e.g. a provider probe and an
|
|
744
868
|
// agent launch). The winner may publish its discovery right after our
|
|
@@ -765,16 +889,16 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
765
889
|
const startupExitRecord = await readBrokerStartupExitRecord(settings.agentDir);
|
|
766
890
|
const trustedMarker =
|
|
767
891
|
marker &&
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
marker.pid ===
|
|
771
|
-
marker.incarnation ===
|
|
892
|
+
spawnedBrokerPid !== undefined &&
|
|
893
|
+
spawnedBrokerIncarnation !== undefined &&
|
|
894
|
+
marker.pid === spawnedBrokerPid &&
|
|
895
|
+
marker.incarnation === spawnedBrokerIncarnation
|
|
772
896
|
? marker
|
|
773
897
|
: undefined;
|
|
774
898
|
const trustedStartupExitRecord =
|
|
775
899
|
startupExitRecord &&
|
|
776
|
-
|
|
777
|
-
startupExitRecord.pid ===
|
|
900
|
+
spawnedBrokerPid !== undefined &&
|
|
901
|
+
startupExitRecord.pid === spawnedBrokerPid &&
|
|
778
902
|
startupExitRecord.writtenAt >= childSpawnedAt
|
|
779
903
|
? startupExitRecord
|
|
780
904
|
: undefined;
|
|
@@ -828,8 +952,20 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
|
|
|
828
952
|
? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
|
|
829
953
|
: exitedBeforeDiscovery
|
|
830
954
|
? new BrokerStartupError({
|
|
831
|
-
exitCode:
|
|
832
|
-
|
|
955
|
+
exitCode: isTrampoline
|
|
956
|
+
? (trustedStartupExitRecord?.exitCode ??
|
|
957
|
+
trustedMarker?.exitCode ??
|
|
958
|
+
childExitCode ??
|
|
959
|
+
child.exitCode ??
|
|
960
|
+
null)
|
|
961
|
+
: (childExitCode ?? child.exitCode ?? trustedStartupExitRecord?.exitCode ?? null),
|
|
962
|
+
signal: isTrampoline
|
|
963
|
+
? (trustedStartupExitRecord?.signal ??
|
|
964
|
+
trustedMarker?.signal ??
|
|
965
|
+
childSignalCode ??
|
|
966
|
+
child.signalCode ??
|
|
967
|
+
null)
|
|
968
|
+
: (childSignalCode ?? child.signalCode ?? trustedStartupExitRecord?.signal ?? null),
|
|
833
969
|
reason:
|
|
834
970
|
brokerStartupExitReason(trustedStartupExitRecord) ?? brokerStartupFailureReason(trustedMarker),
|
|
835
971
|
stderrExcerpt: spawnLogTail.length > 0 ? spawnLogTail : undefined,
|