@parall/codex-agent 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,515 @@
1
+ import { spawn } from "node:child_process";
2
+ import { execSync } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import * as fs from "node:fs";
5
+ import { normalizeApprovalPolicy, normalizeSandbox } from "./config.js";
6
+ import { EventMapper } from "./event-mapping.js";
7
+ import { JsonRpcStdioClient } from "./jsonrpc-client.js";
8
+ /**
9
+ * Bridge driver backed by `codex app-server --listen stdio://`.
10
+ *
11
+ * Lifecycle:
12
+ * - bridge startup: spawn one `codex app-server` subprocess, do the
13
+ * `initialize` / `initialized` handshake once, keep the pipe open
14
+ * - per Parall dispatch: `thread/start` (first time) or `thread/resume`,
15
+ * then `turn/start`; forward server notifications until `turn/completed`
16
+ * - fork-on-busy: `thread/fork` creates a disposable sibling thread
17
+ *
18
+ * Concurrency: agent-core routes one dispatch per sessionKey at a time, so
19
+ * main + fork can interleave turns on the same stdio pipe. We route
20
+ * notifications by threadId the server stamps on every item/turn event.
21
+ */
22
+ export class CodexAppServerAdapter {
23
+ opts;
24
+ client = null;
25
+ proc = null;
26
+ initialized = false;
27
+ startPromise = null;
28
+ activeTurns = new Map();
29
+ resumedThreadIds = new Set();
30
+ stopping = false;
31
+ /**
32
+ * Store an active turn sink keyed by threadId. If a sink already exists for
33
+ * the same threadId, log a warning and fail the existing sink — this
34
+ * shouldn't happen in practice (agent-core serialises per sessionKey, and
35
+ * thread/fork returns a distinct id) but the failure mode of a silent
36
+ * replacement would be a perpetually hung dispatch generator, which is
37
+ * hard to debug.
38
+ */
39
+ setActiveTurn(threadId, sink, log) {
40
+ const existing = this.activeTurns.get(threadId);
41
+ if (existing) {
42
+ (log ?? this.opts.log)?.warn?.(`codex-agent: thread ${threadId} already had an active turn; failing the previous dispatch`);
43
+ existing.push({ kind: "error", message: `thread ${threadId} replaced by concurrent turn` });
44
+ existing.close();
45
+ }
46
+ this.activeTurns.set(threadId, sink);
47
+ }
48
+ constructor(opts) {
49
+ this.opts = opts;
50
+ }
51
+ async *dispatch({ bodyForAgent, sessionKey, context }) {
52
+ await this.ensureStarted(context.log);
53
+ // After ensureStarted resolves the subprocess could still die before we
54
+ // capture the client (handleSubprocessClose nulls this.client). Yield a
55
+ // clean error event instead of relying on a non-null assertion that would
56
+ // throw a TypeError on the next sendRequest call.
57
+ const client = this.client;
58
+ if (!client) {
59
+ yield { type: "error", message: "Codex app-server not available (subprocess died during dispatch start)" };
60
+ return;
61
+ }
62
+ const log = this.opts.log ?? context.log;
63
+ const isMainSession = this.opts.sessionManager.isMain(sessionKey);
64
+ let threadId = this.opts.sessionManager.getThreadId(sessionKey);
65
+ if (!threadId) {
66
+ try {
67
+ threadId = await this.openThread(client, { resumeId: undefined });
68
+ this.opts.sessionManager.recordThreadId(sessionKey, threadId);
69
+ // Mark this freshly-started thread as already live in the current
70
+ // app-server process. Without this, the second dispatch after a cold
71
+ // start would enter the thread/resume branch below for a thread this
72
+ // process just created — and if the server treats resume-of-just-
73
+ // created-thread as an attach-to-detached-thread operation, it could
74
+ // fail the resume path and replace the thread, losing the first turn.
75
+ this.resumedThreadIds.add(threadId);
76
+ }
77
+ catch (err) {
78
+ yield { type: "error", message: `Codex thread/start failed: ${errToString(err)}` };
79
+ return;
80
+ }
81
+ }
82
+ else if (isMainSession && !this.resumedThreadIds.has(threadId)) {
83
+ // We have a persisted threadId from a previous bridge run — resume it.
84
+ // Mirror the deferred-clear pattern from the turn/start retry below: only
85
+ // discard the persisted id once a fresh-thread start has actually
86
+ // succeeded, so a transient resume failure (network/timeout/upstream
87
+ // hiccup) doesn't permanently throw away the prior conversation context.
88
+ try {
89
+ threadId = await this.openThread(client, { resumeId: threadId });
90
+ this.opts.sessionManager.recordThreadId(sessionKey, threadId);
91
+ this.resumedThreadIds.add(threadId);
92
+ }
93
+ catch (err) {
94
+ log?.warn?.(`codex-agent: thread/resume failed (${errToString(err)}); attempting one-shot fresh-thread start`);
95
+ let freshThreadId;
96
+ try {
97
+ freshThreadId = await this.openThread(client, { resumeId: undefined });
98
+ }
99
+ catch (innerErr) {
100
+ yield {
101
+ type: "error",
102
+ message: `Codex thread/start failed after resume error (persisted thread retained): ${errToString(innerErr)}`,
103
+ };
104
+ return;
105
+ }
106
+ this.opts.sessionManager.clearMainThread();
107
+ this.opts.sessionManager.recordThreadId(sessionKey, freshThreadId);
108
+ this.resumedThreadIds.add(freshThreadId);
109
+ threadId = freshThreadId;
110
+ }
111
+ }
112
+ const sink = new TurnSink();
113
+ this.setActiveTurn(threadId, sink, log);
114
+ const groupKey = randomUUID();
115
+ let sawTurnEnd = false;
116
+ try {
117
+ // Start the turn. If the stored threadId is dead, we retry once with a fresh thread.
118
+ let turnStartResult;
119
+ try {
120
+ turnStartResult = await client.sendRequest("turn/start", {
121
+ threadId,
122
+ input: buildTurnInput(bodyForAgent),
123
+ });
124
+ }
125
+ catch (err) {
126
+ const message = errToString(err);
127
+ // For the main session we attempt one fresh-thread retry. Codex CLI's
128
+ // wording for stale threads shifts across versions (not found / unknown
129
+ // / invalid / expired / gone / ...), so we don't gate the retry on a
130
+ // regex. But we only commit to discarding the persisted threadId after
131
+ // the retry succeeds — if the failure was transient (network, timeout,
132
+ // upstream model hiccup), keeping the old id lets the next dispatch
133
+ // resume normally instead of permanently losing conversation context.
134
+ // Fork sessions don't retry: agent-core spawns a fresh fork next trigger.
135
+ if (!isMainSession) {
136
+ yield { type: "error", message: `Codex turn/start failed: ${message}` };
137
+ return;
138
+ }
139
+ log?.warn?.(`codex-agent: turn/start on thread ${threadId} failed (${message}); attempting one-shot fresh-thread retry`);
140
+ this.activeTurns.delete(threadId);
141
+ let freshThreadId;
142
+ try {
143
+ freshThreadId = await this.openThread(client, { resumeId: undefined });
144
+ }
145
+ catch (createErr) {
146
+ yield { type: "error", message: `Codex turn/start failed; could not create replacement thread: ${errToString(createErr)}` };
147
+ return;
148
+ }
149
+ this.setActiveTurn(freshThreadId, sink, log);
150
+ try {
151
+ turnStartResult = await client.sendRequest("turn/start", {
152
+ threadId: freshThreadId,
153
+ input: buildTurnInput(bodyForAgent),
154
+ });
155
+ }
156
+ catch (retryErr) {
157
+ // Retry also failed — likely transient or systemic, not a stale-thread
158
+ // issue. Don't clobber the persisted threadId; next dispatch will try
159
+ // resume again.
160
+ this.activeTurns.delete(freshThreadId);
161
+ yield { type: "error", message: `Codex turn/start failed after retry: ${errToString(retryErr)}` };
162
+ return;
163
+ }
164
+ // Retry accepted — the original thread really was unusable. Now safe
165
+ // to discard the old persisted id and persist the fresh one.
166
+ this.opts.sessionManager.clearMainThread();
167
+ this.opts.sessionManager.recordThreadId(sessionKey, freshThreadId);
168
+ this.resumedThreadIds.add(freshThreadId);
169
+ threadId = freshThreadId;
170
+ }
171
+ void turnStartResult; // turnId comes back here but isn't needed — we key by threadId
172
+ while (true) {
173
+ const envelope = await sink.next();
174
+ if (envelope.kind === "turn_end") {
175
+ sawTurnEnd = true;
176
+ break;
177
+ }
178
+ if (envelope.kind === "error") {
179
+ yield { type: "error", message: envelope.message };
180
+ continue;
181
+ }
182
+ const event = envelope.event;
183
+ if (event.type === "error") {
184
+ yield event;
185
+ continue;
186
+ }
187
+ if (event.type === "text") {
188
+ // Layer 0 symmetric output contract: Codex's plain text is never
189
+ // projected as a chat message. Outbound messages must come from the
190
+ // agent explicitly invoking `@parall/cli messages send` / `dm` via
191
+ // the shell/exec tool. Text events are still yielded so agent-core
192
+ // records them as suppressed session steps for audit.
193
+ yield { ...event, project: false, groupKey };
194
+ continue;
195
+ }
196
+ yield { ...event, groupKey };
197
+ }
198
+ }
199
+ finally {
200
+ this.activeTurns.delete(threadId);
201
+ if (!sawTurnEnd) {
202
+ // Defensive: if we bailed early, make sure we leave no dangling sink.
203
+ sink.close();
204
+ }
205
+ }
206
+ }
207
+ async forkSession({ sessionKey: parentSessionKey }) {
208
+ const client = this.client;
209
+ if (!client)
210
+ return null;
211
+ const parentThreadId = this.opts.sessionManager.getThreadId(parentSessionKey);
212
+ if (!parentThreadId)
213
+ return null;
214
+ const handle = this.opts.sessionManager.createForkSessionKey();
215
+ try {
216
+ const result = await client.sendRequest("thread/fork", {
217
+ threadId: parentThreadId,
218
+ ephemeral: true,
219
+ });
220
+ const forkedThreadId = extractThreadId(result);
221
+ if (!forkedThreadId) {
222
+ this.opts.log?.warn?.("codex-agent: thread/fork returned no thread id");
223
+ return null;
224
+ }
225
+ this.opts.sessionManager.recordThreadId(handle.sessionKey, forkedThreadId);
226
+ return handle;
227
+ }
228
+ catch (err) {
229
+ return this.logForkFailure(err);
230
+ }
231
+ }
232
+ cleanupFork({ fork }) {
233
+ this.opts.sessionManager.cleanupFork(fork.sessionKey);
234
+ }
235
+ logForkFailure(err) {
236
+ this.opts.log?.warn?.(`codex-agent: thread/fork failed: ${errToString(err)}`);
237
+ return null;
238
+ }
239
+ async stop() {
240
+ this.stopping = true;
241
+ const proc = this.proc;
242
+ const client = this.client;
243
+ this.proc = null;
244
+ this.client = null;
245
+ this.initialized = false;
246
+ if (client)
247
+ client.dispose(new Error("adapter stopped"));
248
+ if (proc && proc.exitCode === null && proc.signalCode === null) {
249
+ proc.kill("SIGTERM");
250
+ }
251
+ }
252
+ async ensureStarted(log) {
253
+ if (this.initialized && this.client)
254
+ return;
255
+ if (this.startPromise)
256
+ return this.startPromise;
257
+ this.startPromise = this.doStart(log);
258
+ try {
259
+ await this.startPromise;
260
+ }
261
+ finally {
262
+ // Keep startPromise set only while actively starting; reset once resolved
263
+ // (or rejected) so the next ensureStarted() after a subprocess death can
264
+ // restart cleanly.
265
+ this.startPromise = null;
266
+ }
267
+ }
268
+ async doStart(log) {
269
+ // Reset graceful-stop flag in case the adapter is being restarted after a
270
+ // previous stop() — otherwise handleSubprocessClose would mislabel the next
271
+ // unexpected exit as "during graceful stop".
272
+ this.stopping = false;
273
+ ensureGitRepo(this.opts.workspaceDir);
274
+ // Only steer Codex's own state via CODEX_HOME. Leaving HOME untouched
275
+ // preserves the user's real dotfiles for any subprocess Codex spawns
276
+ // (git/ssh/npm/etc.). Earlier versions also rewrote HOME, which broke
277
+ // tooling for users who set a custom CODEX_HOME pointing somewhere
278
+ // other than their shell home.
279
+ const env = {
280
+ ...process.env,
281
+ CODEX_HOME: this.opts.codexHome,
282
+ FORCE_COLOR: "0",
283
+ NO_COLOR: "1",
284
+ };
285
+ const args = ["app-server", "--listen", "stdio://"];
286
+ (log ?? this.opts.log)?.info?.(`codex-agent: spawning ${this.opts.codexBin} ${args.join(" ")}`);
287
+ const proc = spawn(this.opts.codexBin, args, {
288
+ cwd: this.opts.workspaceDir,
289
+ env,
290
+ stdio: ["pipe", "pipe", "pipe"],
291
+ });
292
+ proc.stderr.setEncoding("utf8");
293
+ proc.stderr.on("data", (chunk) => {
294
+ (log ?? this.opts.log)?.warn?.(`codex-agent[stderr]: ${chunk.trim()}`);
295
+ });
296
+ // Don't publish proc/client until the initialize handshake succeeds. If
297
+ // initialize rejects (timeout, malformed handshake, app-server crash on
298
+ // start), we kill the half-spawned subprocess and clear local refs here
299
+ // — leaving them on `this` would leak the orphan process and cause the
300
+ // next ensureStarted() to spawn a second app-server on top of it.
301
+ const client = new JsonRpcStdioClient(proc);
302
+ client.setNotificationHandler((method, params) => this.routeNotification(method, params));
303
+ proc.once("close", (code, signal) => this.handleSubprocessClose(proc, code, signal, log));
304
+ proc.once("error", (err) => this.handleSubprocessClose(proc, null, null, log, err));
305
+ try {
306
+ await client.sendRequest("initialize", {
307
+ clientInfo: { name: "parall-codex-agent", version: "1" },
308
+ capabilities: { experimentalApi: false },
309
+ });
310
+ client.sendNotification("initialized", {});
311
+ }
312
+ catch (err) {
313
+ client.dispose(err instanceof Error ? err : new Error(String(err)));
314
+ if (proc.exitCode === null && proc.signalCode === null)
315
+ proc.kill("SIGTERM");
316
+ throw err;
317
+ }
318
+ this.proc = proc;
319
+ this.client = client;
320
+ this.initialized = true;
321
+ }
322
+ handleSubprocessClose(proc, code, signal, log, err) {
323
+ // Ignore close/error callbacks from a stale subprocess instance: the
324
+ // adapter may already have spawned a fresh app-server (e.g. after a
325
+ // recovered handshake failure), and we don't want a dying old child to
326
+ // tear down the new healthy one.
327
+ if (this.proc !== null && this.proc !== proc)
328
+ return;
329
+ const reason = err
330
+ ? `spawn error: ${err.message}`
331
+ : `exited (code=${code ?? "null"}${signal ? `, signal=${signal}` : ""})`;
332
+ const logger = log ?? this.opts.log;
333
+ if (this.stopping) {
334
+ logger?.info?.(`codex-agent: app-server subprocess ${reason} during graceful stop`);
335
+ }
336
+ else {
337
+ logger?.warn?.(`codex-agent: app-server subprocess ${reason}; resetting adapter`);
338
+ }
339
+ this.client?.dispose(err ?? new Error(`app-server ${reason}`));
340
+ for (const activeSink of this.activeTurns.values()) {
341
+ activeSink.push({ kind: "error", message: `Codex app-server ${reason}` });
342
+ activeSink.close();
343
+ }
344
+ this.activeTurns.clear();
345
+ this.resumedThreadIds.clear();
346
+ this.client = null;
347
+ this.proc = null;
348
+ this.initialized = false;
349
+ }
350
+ async openThread(client, opts) {
351
+ // Per the app-server protocol, `thread/resume` accepts the same
352
+ // configuration overrides as `thread/start` (sandbox, approvalPolicy,
353
+ // model, reasoningEffort). Passing them on resume lets the user change
354
+ // PRLL_CODEX_* env vars and have the bridge pick them up on the next
355
+ // restart instead of being stuck on the values baked into the persisted
356
+ // thread. Sandbox / approval values are normalised to the camelCase enum
357
+ // the protocol expects (`workspaceWrite`, `onRequest`, etc.), so users
358
+ // can supply either CLI-style or protocol-style env values.
359
+ const commonParams = {
360
+ approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
361
+ sandbox: normalizeSandbox(this.opts.sandbox),
362
+ };
363
+ if (this.opts.model)
364
+ commonParams.model = this.opts.model;
365
+ if (this.opts.reasoningEffort) {
366
+ // The app-server JSON-RPC surface uses camelCase for overrides —
367
+ // `modelReasoningEffort` parallels `approvalPolicy` / `sandbox` on the
368
+ // top-level params. The corresponding config.toml key is
369
+ // `model_reasoning_effort` (snake_case), but the nested `config` on
370
+ // `thread/start` / `thread/resume` takes the camelCase form.
371
+ commonParams.config = { modelReasoningEffort: this.opts.reasoningEffort };
372
+ }
373
+ const method = opts.resumeId ? "thread/resume" : "thread/start";
374
+ const params = opts.resumeId
375
+ ? { threadId: opts.resumeId, ...commonParams }
376
+ : { cwd: this.opts.workspaceDir, ...commonParams };
377
+ const result = await client.sendRequest(method, params);
378
+ const threadId = extractThreadId(result) ?? opts.resumeId;
379
+ if (!threadId) {
380
+ throw new Error(`${method} returned no thread id`);
381
+ }
382
+ return threadId;
383
+ }
384
+ routeNotification(method, params) {
385
+ const threadId = extractThreadIdFromNotification(params);
386
+ if (!threadId) {
387
+ // Surface server-initiated generic errors to every active turn.
388
+ if (method === "error") {
389
+ const msg = params?.message ?? "Codex app-server error";
390
+ for (const sink of this.activeTurns.values()) {
391
+ sink.push({ kind: "error", message: String(msg) });
392
+ }
393
+ }
394
+ return;
395
+ }
396
+ const sink = this.activeTurns.get(threadId);
397
+ if (!sink)
398
+ return;
399
+ // For turn/completed we still run the mapper first — it emits a
400
+ // RuntimeEvent error if `turn.status === "failed"`. Enqueue those events
401
+ // before the turn_end sentinel so the dispatch loop can yield them.
402
+ for (const event of sink.mapper.map(method, params)) {
403
+ sink.push({ kind: "runtime", event });
404
+ }
405
+ if (method === "turn/completed") {
406
+ sink.push({ kind: "turn_end", threadId });
407
+ }
408
+ }
409
+ }
410
+ /** Per-turn buffered sink backed by an unbounded promise queue. */
411
+ class TurnSink {
412
+ mapper = new EventMapper();
413
+ queue = [];
414
+ resolver = null;
415
+ closed = false;
416
+ push(envelope) {
417
+ if (this.closed)
418
+ return;
419
+ if (this.resolver) {
420
+ const r = this.resolver;
421
+ this.resolver = null;
422
+ r(envelope);
423
+ return;
424
+ }
425
+ this.queue.push(envelope);
426
+ }
427
+ next() {
428
+ // Drain any queued envelopes first, even after close(). Otherwise a final
429
+ // error envelope enqueued right before close() (e.g. by
430
+ // handleSubprocessClose) is silently dropped because the consumer would
431
+ // see turn_end before it.
432
+ const pending = this.queue.shift();
433
+ if (pending)
434
+ return Promise.resolve(pending);
435
+ if (this.closed) {
436
+ return Promise.resolve({ kind: "turn_end" });
437
+ }
438
+ return new Promise((resolve) => {
439
+ this.resolver = resolve;
440
+ });
441
+ }
442
+ close() {
443
+ this.closed = true;
444
+ const r = this.resolver;
445
+ this.resolver = null;
446
+ r?.({ kind: "turn_end" });
447
+ }
448
+ }
449
+ /**
450
+ * Codex app-server's `turn/start` expects `input` as an array of content
451
+ * items (each `{ type: "text", text: "..." }` or similar), not a raw string.
452
+ * Wrap the event body so the protocol contract is honoured — sending a bare
453
+ * string has worked historically via undocumented coercion but isn't stable.
454
+ */
455
+ function buildTurnInput(body) {
456
+ return [{ type: "text", text: body }];
457
+ }
458
+ function ensureGitRepo(workingDirectory) {
459
+ fs.mkdirSync(workingDirectory, { recursive: true });
460
+ // Only `git init` if the workspace isn't already inside any git repo. A
461
+ // bare existsSync(.git) check would miss the common case of a user pointing
462
+ // PRLL_CODEX_WORKSPACE_DIR at a subdirectory of their existing project,
463
+ // and silently creating a nested repo there would mangle their layout.
464
+ try {
465
+ execSync("git rev-parse --is-inside-work-tree", { cwd: workingDirectory, stdio: "pipe" });
466
+ return;
467
+ }
468
+ catch {
469
+ // Not inside a repo — fall through to init.
470
+ }
471
+ const env = {
472
+ ...process.env,
473
+ GIT_AUTHOR_NAME: "parall-codex-agent",
474
+ GIT_AUTHOR_EMAIL: "agent@parall.local",
475
+ GIT_COMMITTER_NAME: "parall-codex-agent",
476
+ GIT_COMMITTER_EMAIL: "agent@parall.local",
477
+ };
478
+ try {
479
+ execSync("git init", { cwd: workingDirectory, stdio: "pipe", env });
480
+ execSync("git commit --allow-empty -m init", { cwd: workingDirectory, stdio: "pipe", env });
481
+ }
482
+ catch {
483
+ // Non-fatal: codex app-server may still accept a bare directory. Let it raise at turn time.
484
+ }
485
+ }
486
+ function extractThreadId(result) {
487
+ if (!result || typeof result !== "object")
488
+ return undefined;
489
+ const r = result;
490
+ if (typeof r.threadId === "string")
491
+ return r.threadId;
492
+ const thread = r.thread;
493
+ if (thread && typeof thread.id === "string")
494
+ return thread.id;
495
+ return undefined;
496
+ }
497
+ function extractThreadIdFromNotification(params) {
498
+ if (!params || typeof params !== "object")
499
+ return undefined;
500
+ const p = params;
501
+ if (typeof p.threadId === "string")
502
+ return p.threadId;
503
+ const thread = p.thread;
504
+ if (thread && typeof thread.id === "string")
505
+ return thread.id;
506
+ const meta = (p._meta ?? p.meta);
507
+ if (meta && typeof meta.threadId === "string")
508
+ return meta.threadId;
509
+ return undefined;
510
+ }
511
+ function errToString(err) {
512
+ if (err instanceof Error)
513
+ return err.message;
514
+ return String(err);
515
+ }
@@ -0,0 +1,36 @@
1
+ import type { RuntimeEvent } from "@parall/agent-core";
2
+ /**
3
+ * Translation layer between `codex app-server` JSON-RPC notifications and
4
+ * `@parall/agent-core` RuntimeEvents. The adapter owns one of these per
5
+ * in-flight turn.
6
+ *
7
+ * Delta policy: Codex emits streaming `item/*\/delta` chunks plus a final
8
+ * `item/completed` with the aggregated text. We drop all `*_delta`
9
+ * notifications entirely and only emit the aggregated `item/completed`
10
+ * text — symmetric with the Claude bridge, which similarly projects the
11
+ * assistant's final message as one event.
12
+ *
13
+ * Output contract: every `text` event is emitted with `project: false`.
14
+ * Under the symmetric Layer 0 contract (see
15
+ * `docs/engineering-design/agent-dm-loop-prevention.md`), plain text is
16
+ * recorded as a suppressed session step for audit but never surfaces as a
17
+ * chat message. Outbound messages happen exclusively via the agent
18
+ * explicitly invoking `@parall/cli messages send` / `dm`.
19
+ */
20
+ export declare class EventMapper {
21
+ private readonly toolCallStart;
22
+ /**
23
+ * Map a single server notification to zero or more RuntimeEvents.
24
+ * Return `null` for notifications that have no chat surface.
25
+ */
26
+ map(method: string, params: unknown): RuntimeEvent[];
27
+ private mapItem;
28
+ /**
29
+ * Compute the durationMs for a tool call completion, preferring the server's
30
+ * value and falling back to the locally measured interval. Always clears the
31
+ * tracked start timestamp so long-lived bridges don't accumulate stale
32
+ * entries when the server routinely supplies durationMs.
33
+ */
34
+ private resolveDuration;
35
+ }
36
+ //# sourceMappingURL=event-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-mapping.d.ts","sourceRoot":"","sources":["../src/event-mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAE3D;;;OAGG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,YAAY,EAAE;IA8CpD,OAAO,CAAC,OAAO;IA4If;;;;;OAKG;IACH,OAAO,CAAC,eAAe;CASxB"}