@bridge_gpt/mcp-server 0.2.18 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +1 -1
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +1 -0
- package/build/conductor/bridge-api-client.js +7 -7
- package/build/conductor/cli.js +65 -12
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/epic-reconcile.js +9 -1
- package/build/conductor/epic-runtime.js +403 -43
- package/build/conductor/epic-state.js +7 -0
- package/build/conductor/errors.js +115 -3
- package/build/conductor/event-accessors.js +28 -10
- package/build/conductor/merge-ledger.js +6 -4
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1884 -6917
- package/build/doctor.js +8 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +11222 -8544
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +25 -93
- package/build/start-tickets-prereqs.js +152 -1
- package/build/start-tickets.js +96 -158
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +8 -4
- package/public/css/main.min.css +39 -0
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +2 -1
package/build/conductor/tools.js
CHANGED
|
@@ -11,9 +11,14 @@
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
|
|
13
13
|
import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
|
|
14
|
-
import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage,
|
|
14
|
+
import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, } from "./store.js";
|
|
15
15
|
import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
|
|
16
16
|
import { waitForDoneGate, resolveDispatchRunIdForBinding } from "./pr-ci-producer.js";
|
|
17
|
+
// BAPI-527: worker-facing ledger operations run through the conductor CLI
|
|
18
|
+
// subprocess (under the captured conductor Node), NOT the in-process SQLite store,
|
|
19
|
+
// so the worker Node never loads the `better-sqlite3` native binary. `store.ts`'s
|
|
20
|
+
// `checkWorkerMessages` is intentionally NOT imported here anymore.
|
|
21
|
+
import { checkWorkerMessagesViaCli, emitConductorEventIfNewViaCli } from "./worker-ledger-cli.js";
|
|
17
22
|
import { resolveConductorBridgeApiAccess, fetchEpicRunState, ConductorBridgeApiError } from "./bridge-api-client.js";
|
|
18
23
|
/** Build a Zod enum from the semantic taxonomy so arbitrary types are rejected up front. */
|
|
19
24
|
export function buildEventTypeZodEnum() {
|
|
@@ -37,7 +42,10 @@ function jsonResult(value) {
|
|
|
37
42
|
/**
|
|
38
43
|
* Wrap a tool handler so any thrown value becomes a sanitized structured JSON
|
|
39
44
|
* error envelope (validation -> 400, store-busy -> 503, otherwise 500). Raw
|
|
40
|
-
* input data, stack traces, and secret material are never echoed back.
|
|
45
|
+
* input data, stack traces, and secret material are never echoed back. Any
|
|
46
|
+
* allowlisted structured `details` on the envelope (e.g. the BAPI-526
|
|
47
|
+
* LEDGER_NATIVE_MODULE_LOAD_FAILED node_version/ABI/module fields) is preserved
|
|
48
|
+
* through {@link jsonResult}'s pretty serialization unchanged.
|
|
41
49
|
*/
|
|
42
50
|
export function withConductorToolErrorHandling(handler) {
|
|
43
51
|
return async (args) => {
|
|
@@ -249,7 +257,17 @@ function registerWaitForDoneGateTool(registerTool) {
|
|
|
249
257
|
timeoutMs: args.timeout_ms,
|
|
250
258
|
pollIntervalMs: args.poll_interval_ms,
|
|
251
259
|
worktreePath: args.worktree_path,
|
|
252
|
-
}, {
|
|
260
|
+
}, {
|
|
261
|
+
resolveRunId: resolveDispatchRunIdForBinding,
|
|
262
|
+
// BAPI-527: route the gate's ENTIRE dedup+emit through the conductor CLI
|
|
263
|
+
// subprocess. Injecting `emitIfNew` (not just the write sink) replaces the
|
|
264
|
+
// in-process `emitConductorEventIfNew`, whose `eventAlreadyExists` poll
|
|
265
|
+
// pre-check would load better-sqlite3 in the worker Node. The dedupe key +
|
|
266
|
+
// deterministic id are derived purely and the id is forwarded to the CLI,
|
|
267
|
+
// so dedup happens server-side on the events.id UNIQUE constraint — the
|
|
268
|
+
// worker path performs NO in-process ledger read or write.
|
|
269
|
+
emitIfNew: (input, dimensions) => emitConductorEventIfNewViaCli(input, dimensions),
|
|
270
|
+
});
|
|
253
271
|
return jsonResult({
|
|
254
272
|
gate_met: result.gate_met,
|
|
255
273
|
timed_out: result.timed_out,
|
|
@@ -338,9 +356,13 @@ function registerCheckMessagesTool(registerTool) {
|
|
|
338
356
|
if (runId.trim().length === 0 || workerId.trim().length === 0) {
|
|
339
357
|
throw new ConductorValidationError("Conductor worker identity is unavailable: provide run_id + worker_id, or set BAPI_CONDUCTOR_RUN_ID and BAPI_CONDUCTOR_WORKER_ID.");
|
|
340
358
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
359
|
+
// BAPI-527: read+ack through the conductor CLI subprocess (captured
|
|
360
|
+
// conductor Node), NOT the in-process store, so the worker Node never loads
|
|
361
|
+
// better-sqlite3. A missing/invalid CONDUCTOR_NODE_PATH surfaces as the
|
|
362
|
+
// typed LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE envelope via the wrapper below.
|
|
363
|
+
const result = await checkWorkerMessagesViaCli({
|
|
364
|
+
runId,
|
|
365
|
+
workerId,
|
|
344
366
|
limit: args.limit,
|
|
345
367
|
});
|
|
346
368
|
return jsonResult(result);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side conductor-ledger subprocess boundary (BAPI-527, durable execution).
|
|
3
|
+
*
|
|
4
|
+
* CORE INVARIANT: only the CAPTURED CONDUCTOR NODE touches the `better-sqlite3`
|
|
5
|
+
* native binary. A dispatched worker's Node process must NEVER import or load it.
|
|
6
|
+
* Every worker-facing ledger operation (`check_messages`, and the
|
|
7
|
+
* `wait_for_done_gate` event emit) therefore runs through this module, which
|
|
8
|
+
* shells out to the packaged `conductor-bin.js` CLI executed by the conductor
|
|
9
|
+
* Node runtime captured at dispatch time in `CONDUCTOR_NODE_PATH`. Conductor-OWNED
|
|
10
|
+
* processes (the CLI itself, the supervisor/epic runtimes) keep calling
|
|
11
|
+
* `store.ts` directly — they legitimately own the native load.
|
|
12
|
+
*
|
|
13
|
+
* Hard-fail contract: when `CONDUCTOR_NODE_PATH` is missing/invalid or
|
|
14
|
+
* `BAPI_CONDUCTOR_CLI_FILE` is absent, this boundary throws a typed
|
|
15
|
+
* {@link ConductorLedgerSubprocessRuntimeError} rather than silently falling back
|
|
16
|
+
* to the worker's own `process.execPath`. Falling back would re-introduce the
|
|
17
|
+
* native load into the worker Node — the exact failure mode this slice removes.
|
|
18
|
+
*
|
|
19
|
+
* Security: argv is always a fixed LIST (`execFile`, never `shell: true`, never a
|
|
20
|
+
* command string). Event JSON payloads cross via STDIN (`--data-json-stdin`), so
|
|
21
|
+
* raw payloads/secrets never enter the process argument list. Every failure is
|
|
22
|
+
* converted to a sanitized typed error — no raw caught error, path, argv, stdout,
|
|
23
|
+
* stderr, or credential value is ever attached.
|
|
24
|
+
*/
|
|
25
|
+
import * as nodeChildProcess from "node:child_process";
|
|
26
|
+
import { isAbsolute as pathIsAbsolute } from "node:path";
|
|
27
|
+
import { ConductorLedgerSubprocessRuntimeError, } from "./errors.js";
|
|
28
|
+
import { makeProducerDedupeKey, makeStableProducerEventId, } from "./producer-ledger.js";
|
|
29
|
+
/** The env key that carries the captured conductor Node executable path. */
|
|
30
|
+
export const CONDUCTOR_NODE_PATH_ENV = "CONDUCTOR_NODE_PATH";
|
|
31
|
+
/** The (pre-existing) env key that carries the packaged `conductor-bin.js` path. */
|
|
32
|
+
export const BAPI_CONDUCTOR_CLI_FILE_ENV = "BAPI_CONDUCTOR_CLI_FILE";
|
|
33
|
+
/**
|
|
34
|
+
* Bounded stdout buffer for a conductor CLI response. Conductor JSON responses
|
|
35
|
+
* (message batches / emit summaries) are small; 8 MiB is generous headroom while
|
|
36
|
+
* still capping a runaway subprocess.
|
|
37
|
+
*/
|
|
38
|
+
export const WORKER_LEDGER_CLI_MAX_BUFFER = 8 * 1024 * 1024;
|
|
39
|
+
/**
|
|
40
|
+
* Hard wall-clock cap for a single conductor CLI subprocess. A hung
|
|
41
|
+
* `conductor-bin.js` must never hang `check_messages` / `wait_for_done_gate`
|
|
42
|
+
* indefinitely; `execFile`'s `timeout` kills the child and surfaces the callback
|
|
43
|
+
* error (mapped to a sanitized `spawn_failed`). Generous relative to a fast local
|
|
44
|
+
* ledger read/write, but bounded.
|
|
45
|
+
*/
|
|
46
|
+
export const WORKER_LEDGER_CLI_TIMEOUT_MS = 30_000;
|
|
47
|
+
const defaultExecFile = (file, args, options, callback) =>
|
|
48
|
+
// The narrowed signature (utf-8 string stdout/stderr + list argv, no shell) is a
|
|
49
|
+
// safe structural subset of Node's execFile overloads.
|
|
50
|
+
nodeChildProcess.execFile(file, args, options, callback);
|
|
51
|
+
function nonEmpty(value) {
|
|
52
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate and resolve the worker-ledger subprocess runtime from the environment.
|
|
56
|
+
*
|
|
57
|
+
* `CONDUCTOR_NODE_PATH` must be set, non-empty, and an ABSOLUTE path (a relative
|
|
58
|
+
* value like `node` / `./node` is rejected as `invalid` — it must never resolve
|
|
59
|
+
* against `PATH` or the worker cwd). `BAPI_CONDUCTOR_CLI_FILE` must be set and
|
|
60
|
+
* non-empty. On any failure this throws a typed
|
|
61
|
+
* {@link ConductorLedgerSubprocessRuntimeError}; it NEVER falls back to
|
|
62
|
+
* `process.execPath`.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveWorkerLedgerCliRuntime(deps = {}) {
|
|
65
|
+
const env = deps.env ?? process.env;
|
|
66
|
+
const isAbsolute = deps.isAbsolute ?? pathIsAbsolute;
|
|
67
|
+
const nodePathRaw = env[CONDUCTOR_NODE_PATH_ENV];
|
|
68
|
+
if (!nonEmpty(nodePathRaw)) {
|
|
69
|
+
throw new ConductorLedgerSubprocessRuntimeError("missing", CONDUCTOR_NODE_PATH_ENV);
|
|
70
|
+
}
|
|
71
|
+
const nodePath = nodePathRaw.trim();
|
|
72
|
+
if (!isAbsolute(nodePath)) {
|
|
73
|
+
throw new ConductorLedgerSubprocessRuntimeError("invalid", CONDUCTOR_NODE_PATH_ENV);
|
|
74
|
+
}
|
|
75
|
+
const cliFileRaw = env[BAPI_CONDUCTOR_CLI_FILE_ENV];
|
|
76
|
+
if (!nonEmpty(cliFileRaw)) {
|
|
77
|
+
throw new ConductorLedgerSubprocessRuntimeError("cli_missing", BAPI_CONDUCTOR_CLI_FILE_ENV);
|
|
78
|
+
}
|
|
79
|
+
return { nodePath, cliFile: cliFileRaw.trim() };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Execute one conductor CLI subcommand under the captured conductor Node and
|
|
83
|
+
* resolve its raw stdout. argv is a fixed LIST — `[cliFile, ...subcommandArgs]` —
|
|
84
|
+
* spawned via `execFile` with `shell` disabled. An optional `stdin` payload is
|
|
85
|
+
* written to the child's stdin (used for `--data-json-stdin`) so raw payloads
|
|
86
|
+
* never enter argv. Any spawn error or abnormal exit is converted to a sanitized
|
|
87
|
+
* {@link ConductorLedgerSubprocessRuntimeError} — the raw error, stderr, and
|
|
88
|
+
* stdout are discarded so no path/secret leaks.
|
|
89
|
+
*/
|
|
90
|
+
export function execConductorCli(subcommandArgs, stdin, deps = {}) {
|
|
91
|
+
const runtime = resolveWorkerLedgerCliRuntime(deps);
|
|
92
|
+
const execFile = deps.execFile ?? defaultExecFile;
|
|
93
|
+
const maxBuffer = deps.maxBuffer ?? WORKER_LEDGER_CLI_MAX_BUFFER;
|
|
94
|
+
const timeout = deps.timeout ?? WORKER_LEDGER_CLI_TIMEOUT_MS;
|
|
95
|
+
const argv = [runtime.cliFile, ...subcommandArgs];
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
let child;
|
|
98
|
+
try {
|
|
99
|
+
child = execFile(runtime.nodePath, argv,
|
|
100
|
+
// A `timeout` bounds a hung child; the kill surfaces as the callback error
|
|
101
|
+
// (mapped below to a sanitized spawn_failed) rather than an infinite hang.
|
|
102
|
+
{ maxBuffer, encoding: "utf8", timeout }, (error, stdout) => {
|
|
103
|
+
if (error) {
|
|
104
|
+
// A non-zero exit, spawn failure, or timeout kill — never surface the
|
|
105
|
+
// raw error text (it can carry paths/secrets). Fixed reason only.
|
|
106
|
+
reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed", CONDUCTOR_NODE_PATH_ENV));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
resolve(stdout);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
reject(new ConductorLedgerSubprocessRuntimeError("spawn_failed", CONDUCTOR_NODE_PATH_ENV));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (stdin !== undefined) {
|
|
117
|
+
// A writable-stream 'error' (e.g. EPIPE when the child exits early) is emitted
|
|
118
|
+
// ASYNCHRONOUSLY and, without a listener, becomes an uncaught exception that
|
|
119
|
+
// crashes the whole worker process. Attach a no-op handler so the failure is
|
|
120
|
+
// absorbed here; the exec callback still surfaces the underlying failure as a
|
|
121
|
+
// sanitized spawn_failed. Never echo the raw error.
|
|
122
|
+
child.stdin?.on("error", () => {
|
|
123
|
+
/* swallowed: the exec callback owns the failure signal */
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
child.stdin?.write(stdin);
|
|
127
|
+
child.stdin?.end();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// A synchronous write failure surfaces through the exec callback as a spawn
|
|
131
|
+
// error; do not throw here (and do not echo the raw failure).
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Parse compact CLI JSON from stdout. Malformed output becomes a sanitized typed
|
|
138
|
+
* {@link ConductorLedgerSubprocessRuntimeError} (`malformed_stdout`) — the raw
|
|
139
|
+
* stdout is never attached to the thrown error.
|
|
140
|
+
*/
|
|
141
|
+
export function parseCliJsonStdout(stdout) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(stdout);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new ConductorLedgerSubprocessRuntimeError("malformed_stdout", CONDUCTOR_NODE_PATH_ENV);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Read + ACK a worker's pending relay messages through the conductor CLI, i.e.
|
|
151
|
+
* `conductor-bin.js check-messages --run-id <id> --worker-id <id> [--limit <n>] --json`.
|
|
152
|
+
* Returns the parsed {@link CheckWorkerMessagesResult}. Never touches the local
|
|
153
|
+
* SQLite store from the worker Node.
|
|
154
|
+
*/
|
|
155
|
+
export async function checkWorkerMessagesViaCli(input, deps = {}) {
|
|
156
|
+
const args = ["check-messages", "--run-id", input.runId, "--worker-id", input.workerId];
|
|
157
|
+
if (input.limit !== undefined) {
|
|
158
|
+
args.push("--limit", String(input.limit));
|
|
159
|
+
}
|
|
160
|
+
args.push("--json");
|
|
161
|
+
const stdout = await execConductorCli(args, undefined, deps);
|
|
162
|
+
return parseCliJsonStdout(stdout);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Build the `emit-event` envelope argv (everything EXCEPT the normalized `data`
|
|
166
|
+
* object, which crosses via stdin through `--data-json-stdin`). Mirrors the
|
|
167
|
+
* claude-hook emit contract so raw payloads/secrets never reach the argument list.
|
|
168
|
+
*/
|
|
169
|
+
export function buildEmitEventArgs(event) {
|
|
170
|
+
const args = ["emit-event", "--type", event.type, "--source", event.source];
|
|
171
|
+
// Forward the deterministic event id so the conductor process dedups server-side
|
|
172
|
+
// on the `events.id` UNIQUE constraint — this is what lets the worker skip the
|
|
173
|
+
// in-process `eventAlreadyExists` poll (which would load better-sqlite3).
|
|
174
|
+
if (nonEmpty(event.id ?? undefined))
|
|
175
|
+
args.push("--id", event.id);
|
|
176
|
+
if (nonEmpty(event.subject ?? undefined))
|
|
177
|
+
args.push("--subject", event.subject);
|
|
178
|
+
if (nonEmpty(event.run_id ?? undefined))
|
|
179
|
+
args.push("--run-id", event.run_id);
|
|
180
|
+
if (nonEmpty(event.worker_id ?? undefined))
|
|
181
|
+
args.push("--worker-id", event.worker_id);
|
|
182
|
+
if (nonEmpty(event.producer ?? undefined))
|
|
183
|
+
args.push("--producer", event.producer);
|
|
184
|
+
if (nonEmpty(event.observed_via ?? undefined)) {
|
|
185
|
+
args.push("--observed-via", event.observed_via);
|
|
186
|
+
}
|
|
187
|
+
if (event.schema_version !== undefined) {
|
|
188
|
+
args.push("--schema-version", String(event.schema_version));
|
|
189
|
+
}
|
|
190
|
+
if (nonEmpty(event.time ?? undefined))
|
|
191
|
+
args.push("--time", event.time);
|
|
192
|
+
if (typeof event.confidence === "number") {
|
|
193
|
+
args.push("--confidence", String(event.confidence));
|
|
194
|
+
}
|
|
195
|
+
args.push("--data-json-stdin", "--json");
|
|
196
|
+
return args;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Emit one conductor event through the CLI, i.e.
|
|
200
|
+
* `conductor-bin.js emit-event ... --data-json-stdin --json`, passing the
|
|
201
|
+
* normalized `data` object over STDIN rather than argv. Returns the parsed CLI
|
|
202
|
+
* JSON result. Never touches the local SQLite store from the worker Node.
|
|
203
|
+
*/
|
|
204
|
+
export async function emitConductorEventViaCli(event, deps = {}) {
|
|
205
|
+
const args = buildEmitEventArgs(event);
|
|
206
|
+
const stdinPayload = JSON.stringify(event.data ?? {});
|
|
207
|
+
const stdout = await execConductorCli(args, stdinPayload, deps);
|
|
208
|
+
return parseCliJsonStdout(stdout);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Dedup-aware event emit through the CLI, with NO in-process ledger access.
|
|
212
|
+
*
|
|
213
|
+
* This is the worker gate path's replacement for the in-process
|
|
214
|
+
* {@link emitConductorEventIfNew}: the dedupe key + deterministic event id are
|
|
215
|
+
* derived PURELY (hashing only — no store read), the id is forwarded to the CLI
|
|
216
|
+
* (`emit-event --id`), and the conductor process performs the dedup server-side on
|
|
217
|
+
* the `events.id` UNIQUE constraint. Because the pre-check `eventAlreadyExists`
|
|
218
|
+
* poll is gone, a dispatched worker calling `wait_for_done_gate` never loads
|
|
219
|
+
* `better-sqlite3` — closing the exact gap BAPI-527 targets. A conductor-side
|
|
220
|
+
* duplicate is reported back as `{ ok: false, reason: "duplicate" }`.
|
|
221
|
+
*/
|
|
222
|
+
export async function emitConductorEventIfNewViaCli(input, dimensions, deps = {}) {
|
|
223
|
+
const dedupeKey = makeProducerDedupeKey(dimensions);
|
|
224
|
+
const eventId = makeStableProducerEventId(dedupeKey);
|
|
225
|
+
// Embed the dedupe key under data.details (mirrors emitConductorEventIfNew) so a
|
|
226
|
+
// later ledger scan can still recognize the event, without disturbing other
|
|
227
|
+
// normalized details the caller supplied.
|
|
228
|
+
const existingData = input.data ?? {};
|
|
229
|
+
const existingDetails = existingData.details && typeof existingData.details === "object" && !Array.isArray(existingData.details)
|
|
230
|
+
? existingData.details
|
|
231
|
+
: {};
|
|
232
|
+
const data = {
|
|
233
|
+
...existingData,
|
|
234
|
+
details: { ...existingDetails, dedupe_key: dedupeKey },
|
|
235
|
+
};
|
|
236
|
+
const result = (await emitConductorEventViaCli({ ...input, id: eventId, data }, deps));
|
|
237
|
+
// The conductor CLI reports a UNIQUE-constraint collision as a structured
|
|
238
|
+
// duplicate rather than an error — treat that as a no-op dedup, everything else
|
|
239
|
+
// as a successful emit of the deterministic id.
|
|
240
|
+
if (result && result.ok === false && result.reason === "duplicate") {
|
|
241
|
+
return { emitted: false, reason: "duplicate" };
|
|
242
|
+
}
|
|
243
|
+
return { emitted: true, event_id: eventId };
|
|
244
|
+
}
|