@axiom-lattice/cli-a2a 0.1.5 → 0.1.7

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.
Files changed (65) hide show
  1. package/.turbo/turbo-build.log +34 -32
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +85 -2
  4. package/__tests__/opencode-executor.test.ts +631 -31
  5. package/__tests__/session-store-path.test.ts +40 -0
  6. package/dist/{chunk-NQIDRU47.mjs → chunk-54SKDK3D.mjs} +78 -32
  7. package/dist/chunk-54SKDK3D.mjs.map +1 -0
  8. package/dist/chunk-BSNQOP2W.mjs +350 -0
  9. package/dist/chunk-BSNQOP2W.mjs.map +1 -0
  10. package/dist/chunk-DRFFQJII.mjs +195 -0
  11. package/dist/chunk-DRFFQJII.mjs.map +1 -0
  12. package/dist/chunk-URJDRXJW.mjs +378 -0
  13. package/dist/chunk-URJDRXJW.mjs.map +1 -0
  14. package/dist/{chunk-G7AGL2QA.mjs → chunk-WZB7FBSX.mjs} +23 -5
  15. package/dist/chunk-WZB7FBSX.mjs.map +1 -0
  16. package/dist/{chunk-LXL47XMZ.mjs → chunk-YGUPSPZO.mjs} +9 -1
  17. package/dist/chunk-YGUPSPZO.mjs.map +1 -0
  18. package/dist/{chunk-35NFMGMS.mjs → chunk-ZP3JNJQJ.mjs} +3 -3
  19. package/dist/chunk-ZP3JNJQJ.mjs.map +1 -0
  20. package/dist/cli.js +769 -156
  21. package/dist/cli.js.map +1 -1
  22. package/dist/cli.mjs +7 -6
  23. package/dist/cli.mjs.map +1 -1
  24. package/dist/executor-LLE5VEFJ.mjs +12 -0
  25. package/dist/executor-LO7P3KUW.mjs +12 -0
  26. package/dist/{executor-RVBGAWUF.mjs → executor-OOLGGXHS.mjs} +4 -3
  27. package/dist/{executors-QIIKBUMJ.mjs → executors-AQHOPGSR.mjs} +2 -2
  28. package/dist/index.d.mts +92 -21
  29. package/dist/index.d.ts +92 -21
  30. package/dist/index.js +731 -123
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +15 -7
  33. package/package.json +10 -3
  34. package/runtime/cli-path.cjs +3 -0
  35. package/runtime/cli-path.d.ts +1 -0
  36. package/runtime/cli-path.mjs +5 -0
  37. package/src/config/defaults.ts +3 -1
  38. package/src/config/types.ts +8 -3
  39. package/src/executors/claude/client.ts +44 -14
  40. package/src/executors/claude/executor.ts +65 -30
  41. package/src/executors/codex/client.ts +239 -16
  42. package/src/executors/codex/executor.ts +70 -37
  43. package/src/executors/events.ts +15 -1
  44. package/src/executors/index.ts +13 -6
  45. package/src/executors/opencode/client.ts +192 -18
  46. package/src/executors/opencode/executor.ts +65 -30
  47. package/src/index.ts +8 -0
  48. package/src/local-state.ts +53 -0
  49. package/src/server/agent-card.ts +1 -0
  50. package/src/server/index.ts +22 -7
  51. package/src/session-store.ts +262 -0
  52. package/dist/chunk-35NFMGMS.mjs.map +0 -1
  53. package/dist/chunk-G7AGL2QA.mjs.map +0 -1
  54. package/dist/chunk-LXL47XMZ.mjs.map +0 -1
  55. package/dist/chunk-NQIDRU47.mjs.map +0 -1
  56. package/dist/chunk-VSZ3DACI.mjs +0 -179
  57. package/dist/chunk-VSZ3DACI.mjs.map +0 -1
  58. package/dist/chunk-WNCDOYZS.mjs +0 -187
  59. package/dist/chunk-WNCDOYZS.mjs.map +0 -1
  60. package/dist/executor-OWPVDUXH.mjs +0 -11
  61. package/dist/executor-XWHWUVQ3.mjs +0 -11
  62. /package/dist/{executor-OWPVDUXH.mjs.map → executor-LLE5VEFJ.mjs.map} +0 -0
  63. /package/dist/{executor-RVBGAWUF.mjs.map → executor-LO7P3KUW.mjs.map} +0 -0
  64. /package/dist/{executor-XWHWUVQ3.mjs.map → executor-OOLGGXHS.mjs.map} +0 -0
  65. /package/dist/{executors-QIIKBUMJ.mjs.map → executors-AQHOPGSR.mjs.map} +0 -0
package/dist/cli.js CHANGED
@@ -102,13 +102,13 @@ function registerExecutor(provider, factory) {
102
102
  function getExecutor(provider) {
103
103
  return registry.get(provider);
104
104
  }
105
- function createExecutor(config) {
105
+ function createExecutor(config, sessionStore) {
106
106
  const factory = registry.get(config.provider);
107
107
  if (!factory) {
108
108
  const available = Array.from(registry.keys()).join(", ");
109
109
  throw new Error(`Unknown provider: ${config.provider}. Available: ${available}`);
110
110
  }
111
- return factory(config);
111
+ return factory(config, sessionStore);
112
112
  }
113
113
  var import_sdk, import_uuid, registry;
114
114
  var init_executors = __esm({
@@ -120,20 +120,211 @@ var init_executors = __esm({
120
120
  }
121
121
  });
122
122
 
123
+ // src/session-store.ts
124
+ function resolveSessionStorePath(options) {
125
+ if (options.configuredPath && options.configuredPath.length > 0) {
126
+ return options.configuredPath;
127
+ }
128
+ return (0, import_node_path2.join)(
129
+ options.storeDir ?? DEFAULT_STORE_DIR,
130
+ `sessions-${options.provider}-${options.port}.json`
131
+ );
132
+ }
133
+ function generateContextId() {
134
+ return (0, import_node_crypto.randomUUID)();
135
+ }
136
+ var import_node_path2, import_node_os, import_node_fs2, import_node_crypto, log3, DEFAULT_STORE_DIR, DEFAULT_STORE_PATH, DEFAULT_CLEANUP_INTERVAL_MS, SessionBindingStore;
137
+ var init_session_store = __esm({
138
+ "src/session-store.ts"() {
139
+ "use strict";
140
+ import_node_path2 = require("path");
141
+ import_node_os = require("os");
142
+ import_node_fs2 = require("fs");
143
+ import_node_crypto = require("crypto");
144
+ init_logger();
145
+ log3 = logger.child("session-store");
146
+ DEFAULT_STORE_DIR = (0, import_node_path2.join)(
147
+ (0, import_node_os.homedir)(),
148
+ "Library",
149
+ "Application Support",
150
+ "axiom-lattice",
151
+ "cli-a2a"
152
+ );
153
+ DEFAULT_STORE_PATH = (0, import_node_path2.join)(DEFAULT_STORE_DIR, "sessions.json");
154
+ DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
155
+ SessionBindingStore = class {
156
+ constructor(opts) {
157
+ this.cache = /* @__PURE__ */ new Map();
158
+ this.cleanupTimer = null;
159
+ this.dirty = false;
160
+ this.flushTimeout = null;
161
+ this.storePath = opts?.storePath && opts.storePath.length > 0 ? opts.storePath : DEFAULT_STORE_PATH;
162
+ this.persist = opts?.persist ?? true;
163
+ this.loadFromFile();
164
+ this.startCleanup(opts?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS);
165
+ }
166
+ // ── Public API ────────────────────────────────────────────────────────
167
+ get(contextId, provider) {
168
+ const key = this.makeKey(contextId, provider);
169
+ const binding = this.cache.get(key);
170
+ if (!binding) return null;
171
+ if (this.isExpired(binding)) {
172
+ this.cache.delete(key);
173
+ this.markDirty();
174
+ return null;
175
+ }
176
+ return binding;
177
+ }
178
+ set(binding) {
179
+ const key = this.makeKey(binding.contextId, binding.provider);
180
+ this.cache.set(key, binding);
181
+ this.markDirty();
182
+ }
183
+ delete(contextId, provider) {
184
+ const key = this.makeKey(contextId, provider);
185
+ const existed = this.cache.delete(key);
186
+ if (existed) this.markDirty();
187
+ return existed;
188
+ }
189
+ clearActiveTask(contextId, provider) {
190
+ const binding = this.get(contextId, provider);
191
+ if (binding) {
192
+ binding.activeTaskId = void 0;
193
+ binding.activeTaskState = void 0;
194
+ binding.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
195
+ this.markDirty();
196
+ }
197
+ }
198
+ /**
199
+ * Remove all expired entries.
200
+ * Called automatically on the cleanup interval; call manually before shutdown.
201
+ */
202
+ cleanup() {
203
+ let removed = 0;
204
+ for (const [key, binding] of this.cache) {
205
+ if (this.isExpired(binding)) {
206
+ this.cache.delete(key);
207
+ removed++;
208
+ }
209
+ }
210
+ if (removed > 0) {
211
+ this.markDirty();
212
+ log3.info("TTL cleanup completed", { removed, remaining: this.cache.size });
213
+ }
214
+ return removed;
215
+ }
216
+ /**
217
+ * Graceful shutdown: stop cleanup timer, flush pending writes.
218
+ */
219
+ shutdown() {
220
+ if (this.cleanupTimer) {
221
+ clearInterval(this.cleanupTimer);
222
+ this.cleanupTimer = null;
223
+ }
224
+ if (this.flushTimeout) {
225
+ clearTimeout(this.flushTimeout);
226
+ this.flushTimeout = null;
227
+ }
228
+ this.flushToFile();
229
+ log3.info("Session store shut down", { bindings: this.cache.size });
230
+ }
231
+ /** For testing: return the number of cached bindings. */
232
+ get size() {
233
+ return this.cache.size;
234
+ }
235
+ // ── Internal ──────────────────────────────────────────────────────────
236
+ makeKey(contextId, provider) {
237
+ return `${provider}:${contextId}`;
238
+ }
239
+ isExpired(binding) {
240
+ if (!binding.expiresAt) return false;
241
+ return Date.now() >= new Date(binding.expiresAt).getTime();
242
+ }
243
+ startCleanup(intervalMs) {
244
+ this.cleanupTimer = setInterval(() => this.cleanup(), intervalMs);
245
+ this.cleanupTimer.unref();
246
+ }
247
+ // ── Persistence ───────────────────────────────────────────────────────
248
+ markDirty() {
249
+ if (!this.persist) return;
250
+ this.dirty = true;
251
+ if (!this.flushTimeout) {
252
+ this.flushTimeout = setTimeout(() => {
253
+ this.flushToFile();
254
+ }, 1e3);
255
+ }
256
+ }
257
+ flushToFile() {
258
+ this.flushTimeout = null;
259
+ if (!this.persist || !this.dirty) return;
260
+ this.dirty = false;
261
+ this.writeAtomically();
262
+ }
263
+ writeAtomically() {
264
+ const tmpPath = this.storePath + ".tmp";
265
+ const data = {
266
+ version: 1,
267
+ bindings: Array.from(this.cache.values())
268
+ };
269
+ try {
270
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(this.storePath), { recursive: true });
271
+ (0, import_node_fs2.writeFileSync)(tmpPath, JSON.stringify(data, null, 2), "utf-8");
272
+ (0, import_node_fs2.renameSync)(tmpPath, this.storePath);
273
+ } catch (err) {
274
+ log3.error("Failed to write session store", { error: err.message });
275
+ try {
276
+ (0, import_node_fs2.unlinkSync)(tmpPath);
277
+ } catch {
278
+ }
279
+ }
280
+ }
281
+ loadFromFile() {
282
+ if (!this.persist || !(0, import_node_fs2.existsSync)(this.storePath)) return;
283
+ try {
284
+ const raw = (0, import_node_fs2.readFileSync)(this.storePath, "utf-8");
285
+ const data = JSON.parse(raw);
286
+ if (!data || !Array.isArray(data.bindings)) {
287
+ throw new Error("Invalid store format: bindings array missing");
288
+ }
289
+ let loaded = 0;
290
+ let expired = 0;
291
+ for (const b of data.bindings) {
292
+ if (!b.provider || !b.contextId || !b.sessionId) continue;
293
+ if (this.isExpired(b)) {
294
+ expired++;
295
+ continue;
296
+ }
297
+ const key = this.makeKey(b.contextId, b.provider);
298
+ this.cache.set(key, b);
299
+ loaded++;
300
+ }
301
+ log3.info("Session store loaded", { path: this.storePath, loaded, expired });
302
+ } catch (err) {
303
+ log3.error("Failed to load session store \u2014 starting fresh", {
304
+ path: this.storePath,
305
+ error: err.message
306
+ });
307
+ this.cache.clear();
308
+ this.writeAtomically();
309
+ }
310
+ }
311
+ };
312
+ }
313
+ });
314
+
123
315
  // src/executors/opencode/client.ts
124
- var import_node_child_process, log4, OpenCodeClient;
316
+ var import_node_child_process, log5, OpenCodeClient;
125
317
  var init_client = __esm({
126
318
  "src/executors/opencode/client.ts"() {
127
319
  "use strict";
128
320
  import_node_child_process = require("child_process");
129
321
  init_logger();
130
- log4 = logger.child("opencode:client");
322
+ log5 = logger.child("opencode:client");
131
323
  OpenCodeClient = class {
132
324
  constructor(config) {
133
325
  this.config = config;
134
326
  this.currentChild = null;
135
327
  }
136
- /** Abort the currently running child process (if any). */
137
328
  abort() {
138
329
  if (this.currentChild) {
139
330
  this.currentChild.kill("SIGTERM");
@@ -146,14 +337,117 @@ var init_client = __esm({
146
337
  }
147
338
  }
148
339
  /**
149
- * Execute a prompt via the OpenCode CLI and return the response text.
340
+ * Execute a prompt via the OpenCode CLI.
341
+ *
342
+ * @param prompt - The user prompt text.
343
+ * @param sessionId - If provided, continues the session with `--session`.
344
+ * Otherwise creates a new session using `--format json` to capture the
345
+ * session ID.
346
+ * @param workingDirectory - Optional per-request working directory override.
347
+ * Falls back to config.workdir if not provided.
150
348
  */
151
- async execute(prompt) {
349
+ async execute(prompt, sessionId, workingDirectory) {
350
+ if (sessionId) {
351
+ const text = await this.runResume(prompt, sessionId, workingDirectory);
352
+ return { text, sessionId };
353
+ }
354
+ return this.runNew(prompt, workingDirectory);
355
+ }
356
+ resolveWorkdir(override) {
357
+ return override || this.config.workdir || process.cwd();
358
+ }
359
+ // ── New session (first turn) ──────────────────────────────────────────
360
+ async runNew(prompt, workingDirectory) {
361
+ const cliPath = this.config.cliPath || "opencode";
362
+ const timeout = this.config.timeout ?? 6e5;
363
+ const MAX_PROMPT = 1e5;
364
+ if (prompt.length > MAX_PROMPT) {
365
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);
366
+ }
367
+ const env = {
368
+ PATH: process.env.PATH ?? "",
369
+ HOME: process.env.HOME ?? ""
370
+ };
371
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
372
+ if (process.env[key]) env[key] = process.env[key];
373
+ }
374
+ for (const key of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENCODE_SERVER_PASSWORD", "OPENCODE_SERVER_USERNAME"]) {
375
+ if (process.env[key]) env[key] = process.env[key];
376
+ }
377
+ const args = ["run", "--format", "json"];
378
+ if (this.config.model) {
379
+ args.push("--model", this.config.model);
380
+ }
381
+ if (this.config.agent) {
382
+ args.push("--agent", this.config.agent);
383
+ }
384
+ if (this.config.attachUrl) {
385
+ args.push("--attach", this.config.attachUrl);
386
+ }
387
+ args.push("--dangerously-skip-permissions", prompt);
388
+ const workdir = this.resolveWorkdir(workingDirectory);
389
+ log5.info("Spawning opencode (new session, json)", { cliPath, workdir });
390
+ return new Promise((resolve3, reject) => {
391
+ const child = (0, import_node_child_process.spawn)(cliPath, args, {
392
+ cwd: workdir,
393
+ env,
394
+ stdio: ["ignore", "pipe", "pipe"]
395
+ });
396
+ this.currentChild = child;
397
+ let stdout = "";
398
+ let stderr = "";
399
+ const MAX_OUTPUT = 1e7;
400
+ const timer = setTimeout(() => {
401
+ child.kill("SIGTERM");
402
+ const forceTimer = setTimeout(() => {
403
+ if (child.exitCode === null) child.kill("SIGKILL");
404
+ }, 5e3);
405
+ child.on("close", () => clearTimeout(forceTimer));
406
+ reject(new Error(`OpenCode execution timed out after ${timeout}ms`));
407
+ }, timeout);
408
+ child.stdout?.on("data", (chunk) => {
409
+ stdout += chunk.toString();
410
+ if (stdout.length > MAX_OUTPUT) {
411
+ child.kill("SIGTERM");
412
+ setTimeout(() => {
413
+ if (child.exitCode === null) child.kill("SIGKILL");
414
+ }, 5e3);
415
+ }
416
+ });
417
+ child.stderr?.on("data", (chunk) => {
418
+ stderr += chunk.toString();
419
+ log5.debug("OpenCode stderr", { text: chunk.toString().trim() });
420
+ });
421
+ child.on("close", (code) => {
422
+ clearTimeout(timer);
423
+ this.currentChild = null;
424
+ if (code !== 0) {
425
+ const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
426
+ log5.warn("OpenCode non-zero exit", { code, stderr: errMsg });
427
+ reject(new Error(errMsg));
428
+ return;
429
+ }
430
+ try {
431
+ const { text, sessionId } = this.parseJsonEvents(stdout);
432
+ resolve3({ text, sessionId });
433
+ } catch (err) {
434
+ reject(new Error(`Failed to parse OpenCode JSON output: ${err.message}`));
435
+ }
436
+ });
437
+ child.on("error", (err) => {
438
+ clearTimeout(timer);
439
+ log5.error("OpenCode spawn failed", { error: err.message });
440
+ reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
441
+ });
442
+ });
443
+ }
444
+ // ── Resume session ────────────────────────────────────────────────────
445
+ async runResume(prompt, sessionId, workingDirectory) {
152
446
  const cliPath = this.config.cliPath || "opencode";
153
447
  const timeout = this.config.timeout ?? 6e5;
154
448
  const MAX_PROMPT = 1e5;
155
449
  if (prompt.length > MAX_PROMPT) {
156
- throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages or use a provider with HTTP API transport.`);
450
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);
157
451
  }
158
452
  const env = {
159
453
  PATH: process.env.PATH ?? "",
@@ -165,7 +459,7 @@ var init_client = __esm({
165
459
  for (const key of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENCODE_SERVER_PASSWORD", "OPENCODE_SERVER_USERNAME"]) {
166
460
  if (process.env[key]) env[key] = process.env[key];
167
461
  }
168
- const args = ["run", prompt];
462
+ const args = ["run", "--session", sessionId];
169
463
  if (this.config.model) {
170
464
  args.push("--model", this.config.model);
171
465
  }
@@ -175,12 +469,12 @@ var init_client = __esm({
175
469
  if (this.config.attachUrl) {
176
470
  args.push("--attach", this.config.attachUrl);
177
471
  }
178
- args.push("--format", "default");
179
- args.push("--dangerously-skip-permissions");
180
- log4.info("Spawning opencode", { cliPath, workdir: this.config.workdir, model: this.config.model });
472
+ args.push("--format", "default", "--dangerously-skip-permissions", prompt);
473
+ const workdir = this.resolveWorkdir(workingDirectory);
474
+ log5.info("Spawning opencode (resume)", { sessionId, workdir });
181
475
  return new Promise((resolve3, reject) => {
182
476
  const child = (0, import_node_child_process.spawn)(cliPath, args, {
183
- cwd: this.config.workdir || process.cwd(),
477
+ cwd: workdir,
184
478
  env,
185
479
  stdio: ["ignore", "pipe", "pipe"]
186
480
  });
@@ -207,7 +501,7 @@ var init_client = __esm({
207
501
  });
208
502
  child.stderr?.on("data", (chunk) => {
209
503
  stderr += chunk.toString();
210
- log4.debug("OpenCode stderr", { text: chunk.toString().trim() });
504
+ log5.debug("OpenCode stderr", { text: chunk.toString().trim() });
211
505
  });
212
506
  child.on("close", (code) => {
213
507
  clearTimeout(timer);
@@ -216,22 +510,66 @@ var init_client = __esm({
216
510
  resolve3(stdout.trim());
217
511
  } else {
218
512
  const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
219
- log4.warn("OpenCode non-zero exit", { code, stderr: errMsg });
513
+ log5.warn("OpenCode non-zero exit", { code, stderr: errMsg });
220
514
  reject(new Error(errMsg));
221
515
  }
222
516
  });
223
517
  child.on("error", (err) => {
224
518
  clearTimeout(timer);
225
- log4.error("OpenCode spawn failed", { error: err.message });
519
+ log5.error("OpenCode spawn failed", { error: err.message });
226
520
  reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
227
521
  });
228
522
  });
229
523
  }
524
+ // ── Parser ────────────────────────────────────────────────────────────
525
+ /**
526
+ * Parse newline-delimited JSON events from OpenCode's `--format json` output.
527
+ * Extracts the sessionID from the first event that contains it, and
528
+ * accumulates text content from relevant events.
529
+ */
530
+ parseJsonEvents(stdout) {
531
+ const lines = stdout.split("\n").filter((l) => l.trim());
532
+ let sessionId = "";
533
+ const textParts = [];
534
+ for (const line of lines) {
535
+ let event;
536
+ try {
537
+ event = JSON.parse(line);
538
+ } catch {
539
+ continue;
540
+ }
541
+ if (!sessionId && event.sessionID) {
542
+ sessionId = event.sessionID;
543
+ }
544
+ if (event.type === "error") continue;
545
+ const text = event.text || event.content || event.message;
546
+ if (text) {
547
+ textParts.push(text);
548
+ }
549
+ }
550
+ if (!sessionId) {
551
+ return {
552
+ text: textParts.join("\n").trim() || stdout.trim(),
553
+ sessionId: ""
554
+ };
555
+ }
556
+ return {
557
+ text: textParts.join("\n").trim() || "No response from OpenCode.",
558
+ sessionId
559
+ };
560
+ }
230
561
  };
231
562
  }
232
563
  });
233
564
 
234
565
  // src/executors/events.ts
566
+ function extractText(message) {
567
+ return message.parts.filter((p) => {
568
+ const part = p;
569
+ const text = part.text;
570
+ return text !== void 0 && text !== null;
571
+ }).map((p) => p.text).join("\n");
572
+ }
235
573
  function publishTask(bus, taskId, contextId) {
236
574
  bus.publish({
237
575
  kind: "task",
@@ -280,22 +618,24 @@ __export(executor_exports, {
280
618
  OpenCodeExecutor: () => OpenCodeExecutor,
281
619
  createOpenCodeExecutor: () => createOpenCodeExecutor
282
620
  });
283
- function createOpenCodeExecutor(config) {
284
- return new OpenCodeExecutor(config);
621
+ function createOpenCodeExecutor(config, sessionStore) {
622
+ return new OpenCodeExecutor(config, sessionStore);
285
623
  }
286
- var log5, OpenCodeExecutor;
624
+ var log6, OpenCodeExecutor;
287
625
  var init_executor = __esm({
288
626
  "src/executors/opencode/executor.ts"() {
289
627
  "use strict";
628
+ init_session_store();
290
629
  init_logger();
291
630
  init_client();
292
631
  init_events();
293
- log5 = logger.child("opencode:executor");
632
+ log6 = logger.child("opencode:executor");
294
633
  OpenCodeExecutor = class {
295
- constructor(config) {
634
+ constructor(config, sessionStore) {
296
635
  this.client = null;
297
636
  this.initialized = false;
298
637
  this.config = config;
638
+ this.sessionStore = sessionStore;
299
639
  }
300
640
  async initialize() {
301
641
  if (this.initialized) return;
@@ -309,62 +649,81 @@ var init_executor = __esm({
309
649
  timeout: this.config.timeouts.prompt ?? 6e5
310
650
  });
311
651
  this.initialized = true;
312
- log5.info("Executor initialized", { workdir: oc.projectDirectory, model: oc.model });
652
+ log6.info("Executor initialized", { workdir: oc.projectDirectory, model: oc.model });
313
653
  }
314
654
  async shutdown() {
315
655
  this.client = null;
316
656
  this.initialized = false;
317
- log5.info("Executor shut down");
657
+ log6.info("Executor shut down");
318
658
  }
319
659
  async execute(ctx, bus) {
320
- const { taskId, contextId, userMessage, task } = ctx;
660
+ const { taskId, userMessage } = ctx;
321
661
  await this.initialize();
662
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
663
+ let contextId = ctx.contextId;
664
+ if (!contextId) {
665
+ contextId = generateContextId();
666
+ log6.info("Generated new contextId", { contextId, taskId });
667
+ }
668
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "opencode") : null;
669
+ const sessionId = binding?.sessionId;
322
670
  try {
323
- if (!task) {
671
+ const existingTask = ctx.task;
672
+ if (!existingTask) {
324
673
  publishTask(bus, taskId, contextId);
325
674
  publishStatus(bus, taskId, contextId, "submitted");
326
675
  }
327
- publishStatus(bus, taskId, contextId, "working", "Processing request...");
328
- const promptText = this.extractText(userMessage);
329
- log5.info("Sending prompt to OpenCode", { taskId, len: promptText.length });
330
- const response = await this.client.execute(promptText);
331
- const finalText = response || "No response from OpenCode.";
332
- publishFinalArtifact(bus, taskId, contextId, finalText);
676
+ publishStatus(bus, taskId, contextId, "working");
677
+ const promptText = extractText(userMessage);
678
+ const userMsg = userMessage;
679
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
680
+ log6.info("Sending prompt to OpenCode", {
681
+ taskId,
682
+ contextId,
683
+ sessionId: sessionId || "(new)",
684
+ len: promptText.length,
685
+ workingDirectory: workingDirectory || "(default)"
686
+ });
687
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
688
+ this.sessionStore.set({
689
+ provider: "opencode",
690
+ contextId,
691
+ sessionId: result.sessionId,
692
+ activeTaskId: void 0,
693
+ activeTaskState: void 0,
694
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
695
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
696
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
697
+ });
698
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from OpenCode.");
333
699
  publishStatus(bus, taskId, contextId, "completed", void 0, true);
334
700
  bus.finished();
335
- log5.info("Task completed", { taskId, len: finalText.length });
701
+ log6.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
336
702
  } catch (error) {
337
703
  const msg = error.message ?? String(error);
338
- log5.error("Execution failed", { taskId, error: msg });
704
+ log6.error("Execution failed", { taskId, contextId, error: msg });
339
705
  publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
340
706
  bus.finished();
341
707
  }
342
708
  }
343
709
  async cancelTask(taskId, bus) {
344
- log5.info("Cancel requested for OpenCode task", { taskId });
710
+ log6.info("Cancel requested for OpenCode task", { taskId });
345
711
  this.client?.abort();
346
712
  publishStatus(bus, taskId, "", "canceled", "OpenCode task cancelled", true);
347
713
  bus.finished();
348
714
  }
349
- extractText(message) {
350
- return message.parts.filter((p) => {
351
- const part = p;
352
- const text = part.text;
353
- return text !== void 0 && text !== null;
354
- }).map((p) => p.text).join("\n");
355
- }
356
715
  };
357
716
  }
358
717
  });
359
718
 
360
719
  // src/executors/codex/client.ts
361
- var import_node_child_process2, log6, CodexClient;
720
+ var import_node_child_process2, log7, CodexClient;
362
721
  var init_client2 = __esm({
363
722
  "src/executors/codex/client.ts"() {
364
723
  "use strict";
365
724
  import_node_child_process2 = require("child_process");
366
725
  init_logger();
367
- log6 = logger.child("codex:client");
726
+ log7 = logger.child("codex:client");
368
727
  CodexClient = class {
369
728
  constructor(config) {
370
729
  this.config = config;
@@ -382,9 +741,24 @@ var init_client2 = __esm({
382
741
  }
383
742
  }
384
743
  /**
385
- * Execute a prompt via the Codex CLI and return the response text.
744
+ * Execute a prompt via the Codex CLI.
745
+ *
746
+ * @param prompt - The user prompt text.
747
+ * @param sessionId - If provided, resumes an existing session.
748
+ * Otherwise creates a new session and extracts the thread_id from JSONL.
749
+ * @param workingDirectory - Optional per-request working directory override.
386
750
  */
387
- async execute(prompt) {
751
+ async execute(prompt, sessionId, workingDirectory) {
752
+ if (sessionId) {
753
+ return this.executeResume(prompt, sessionId, workingDirectory);
754
+ }
755
+ return this.executeNew(prompt, workingDirectory);
756
+ }
757
+ resolveWorkdir(override) {
758
+ return override || this.config.workdir || process.cwd();
759
+ }
760
+ // ── New session (first turn) ──────────────────────────────────────────
761
+ async executeNew(prompt, workingDirectory) {
388
762
  const cliPath = this.config.cliPath || "codex";
389
763
  const timeout = this.config.timeout ?? 3e5;
390
764
  const MAX_PROMPT = 1e5;
@@ -401,15 +775,20 @@ var init_client2 = __esm({
401
775
  if (this.config.apiKey) {
402
776
  env["OPENAI_API_KEY"] = this.config.apiKey;
403
777
  }
404
- const args = [];
778
+ const args = ["exec", "--json"];
405
779
  if (this.config.model) {
406
780
  args.push("--model", this.config.model);
407
781
  }
408
- args.push(prompt);
409
- log6.info("Spawning codex", { cliPath, workdir: this.config.workdir, model: this.config.model });
782
+ args.push(
783
+ "--dangerously-bypass-approvals-and-sandbox",
784
+ "--skip-git-repo-check",
785
+ prompt
786
+ );
787
+ const workdir = this.resolveWorkdir(workingDirectory);
788
+ log7.info("Spawning codex (new session)", { cliPath, workdir, model: this.config.model });
410
789
  return new Promise((resolve3, reject) => {
411
790
  const child = (0, import_node_child_process2.spawn)(cliPath, args, {
412
- cwd: this.config.workdir || process.cwd(),
791
+ cwd: workdir,
413
792
  env,
414
793
  stdio: ["ignore", "pipe", "pipe"]
415
794
  });
@@ -436,26 +815,182 @@ var init_client2 = __esm({
436
815
  });
437
816
  child.stderr?.on("data", (chunk) => {
438
817
  stderr += chunk.toString();
439
- log6.debug("Codex stderr", { text: chunk.toString().trim() });
818
+ log7.debug("Codex stderr", { text: chunk.toString().trim() });
440
819
  });
441
820
  child.on("close", (code) => {
442
821
  clearTimeout(timer);
443
822
  this.currentChild = null;
444
- if (code === 0) {
445
- resolve3(stdout.trim());
446
- } else {
823
+ if (code !== 0) {
824
+ const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
825
+ log7.warn("Codex non-zero exit", { code, stderr: errMsg });
826
+ reject(new Error(errMsg));
827
+ return;
828
+ }
829
+ try {
830
+ const { text, sessionId } = this.parseJsonl(stdout);
831
+ resolve3({ text, sessionId });
832
+ } catch (err) {
833
+ reject(new Error(`Failed to parse Codex JSONL output: ${err.message}`));
834
+ }
835
+ });
836
+ child.on("error", (err) => {
837
+ clearTimeout(timer);
838
+ log7.error("Codex spawn failed", { error: err.message });
839
+ reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
840
+ });
841
+ });
842
+ }
843
+ // ── Resume session (subsequent turns) ─────────────────────────────────
844
+ async executeResume(prompt, sessionId, workingDirectory) {
845
+ const cliPath = this.config.cliPath || "codex";
846
+ const timeout = this.config.timeout ?? 3e5;
847
+ const MAX_PROMPT = 1e5;
848
+ if (prompt.length > MAX_PROMPT) {
849
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
850
+ }
851
+ const env = {
852
+ PATH: process.env.PATH ?? "",
853
+ HOME: process.env.HOME ?? ""
854
+ };
855
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
856
+ if (process.env[key]) env[key] = process.env[key];
857
+ }
858
+ if (this.config.apiKey) {
859
+ env["OPENAI_API_KEY"] = this.config.apiKey;
860
+ }
861
+ const args = ["exec", "resume", sessionId];
862
+ if (this.config.model) {
863
+ args.push("--model", this.config.model);
864
+ }
865
+ args.push(
866
+ "--dangerously-bypass-approvals-and-sandbox",
867
+ "--skip-git-repo-check",
868
+ prompt
869
+ );
870
+ const workdir = this.resolveWorkdir(workingDirectory);
871
+ log7.info("Spawning codex (resume)", { sessionId, workdir });
872
+ return new Promise((resolve3, reject) => {
873
+ const child = (0, import_node_child_process2.spawn)(cliPath, args, {
874
+ cwd: workdir,
875
+ env,
876
+ stdio: ["ignore", "pipe", "pipe"]
877
+ });
878
+ this.currentChild = child;
879
+ let stdout = "";
880
+ let stderr = "";
881
+ const MAX_OUTPUT = 1e7;
882
+ const timer = setTimeout(() => {
883
+ child.kill("SIGTERM");
884
+ const forceTimer = setTimeout(() => {
885
+ if (child.exitCode === null) child.kill("SIGKILL");
886
+ }, 5e3);
887
+ child.on("close", () => clearTimeout(forceTimer));
888
+ reject(new Error(`Codex execution timed out after ${timeout}ms`));
889
+ }, timeout);
890
+ child.stdout?.on("data", (chunk) => {
891
+ stdout += chunk.toString();
892
+ if (stdout.length > MAX_OUTPUT) {
893
+ child.kill("SIGTERM");
894
+ setTimeout(() => {
895
+ if (child.exitCode === null) child.kill("SIGKILL");
896
+ }, 5e3);
897
+ }
898
+ });
899
+ child.stderr?.on("data", (chunk) => {
900
+ stderr += chunk.toString();
901
+ log7.debug("Codex stderr", { text: chunk.toString().trim() });
902
+ });
903
+ child.on("close", (code) => {
904
+ clearTimeout(timer);
905
+ this.currentChild = null;
906
+ if (code !== 0) {
447
907
  const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
448
- log6.warn("Codex non-zero exit", { code, stderr: errMsg });
908
+ log7.warn("Codex non-zero exit", { code, stderr: errMsg });
449
909
  reject(new Error(errMsg));
910
+ return;
450
911
  }
912
+ const text = this.extractResumeText(stdout);
913
+ resolve3({ text, sessionId });
451
914
  });
452
915
  child.on("error", (err) => {
453
916
  clearTimeout(timer);
454
- log6.error("Codex spawn failed", { error: err.message });
917
+ log7.error("Codex spawn failed", { error: err.message });
455
918
  reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
456
919
  });
457
920
  });
458
921
  }
922
+ // ── Parsers ───────────────────────────────────────────────────────────
923
+ /**
924
+ * Parse JSONL output to extract session thread_id and agent response text.
925
+ */
926
+ parseJsonl(stdout) {
927
+ const lines = stdout.split("\n").filter((l) => l.trim());
928
+ let sessionId = "";
929
+ const textParts = [];
930
+ for (const line of lines) {
931
+ let event;
932
+ try {
933
+ event = JSON.parse(line);
934
+ } catch {
935
+ continue;
936
+ }
937
+ if (event.type === "thread.started" && event.thread_id) {
938
+ sessionId = event.thread_id;
939
+ }
940
+ if (event.type === "item.completed") {
941
+ const item = event.item;
942
+ if (item && item.type === "agent_message" && item.text) {
943
+ textParts.push(item.text);
944
+ }
945
+ }
946
+ }
947
+ if (!sessionId) {
948
+ throw new Error("No thread_id found in Codex JSONL output");
949
+ }
950
+ return {
951
+ text: textParts.join("\n").trim() || "No response from Codex.",
952
+ sessionId
953
+ };
954
+ }
955
+ /**
956
+ * Extract the agent response text from the resume mode output.
957
+ *
958
+ * The resume output includes a CLI header followed by the conversation.
959
+ * We extract everything after the last "codex" marker line as the
960
+ * latest agent response.
961
+ */
962
+ extractResumeText(stdout) {
963
+ const lines = stdout.split("\n");
964
+ let responseStart = -1;
965
+ for (let i = 0; i < lines.length; i++) {
966
+ if (lines[i].trim() === "codex") {
967
+ responseStart = i;
968
+ }
969
+ }
970
+ if (responseStart >= 0 && responseStart + 1 < lines.length) {
971
+ return lines.slice(responseStart + 1).join("\n").trim();
972
+ }
973
+ let skipHeader = true;
974
+ const resultLines = [];
975
+ for (const line of lines) {
976
+ const trimmed = line.trim();
977
+ if (skipHeader) {
978
+ if (trimmed === "--------" || trimmed === "codex") {
979
+ skipHeader = false;
980
+ }
981
+ continue;
982
+ }
983
+ if (trimmed === "user" || trimmed === "codex") {
984
+ continue;
985
+ }
986
+ if (trimmed.startsWith("tokens used")) {
987
+ if (resultLines.length > 0) resultLines.pop();
988
+ break;
989
+ }
990
+ resultLines.push(line);
991
+ }
992
+ return resultLines.join("\n").trim() || stdout.trim();
993
+ }
459
994
  };
460
995
  }
461
996
  });
@@ -466,22 +1001,24 @@ __export(executor_exports2, {
466
1001
  CodexExecutor: () => CodexExecutor,
467
1002
  createCodexExecutor: () => createCodexExecutor
468
1003
  });
469
- function createCodexExecutor(config) {
470
- return new CodexExecutor(config);
1004
+ function createCodexExecutor(config, sessionStore) {
1005
+ return new CodexExecutor(config, sessionStore);
471
1006
  }
472
- var log7, CodexExecutor;
1007
+ var log8, CodexExecutor;
473
1008
  var init_executor2 = __esm({
474
1009
  "src/executors/codex/executor.ts"() {
475
1010
  "use strict";
1011
+ init_session_store();
476
1012
  init_logger();
477
1013
  init_client2();
478
1014
  init_events();
479
- log7 = logger.child("codex:executor");
1015
+ log8 = logger.child("codex:executor");
480
1016
  CodexExecutor = class {
481
- constructor(config) {
1017
+ constructor(config, sessionStore) {
482
1018
  this.client = null;
483
1019
  this.initialized = false;
484
1020
  this.config = config;
1021
+ this.sessionStore = sessionStore;
485
1022
  }
486
1023
  async initialize() {
487
1024
  if (this.initialized) return;
@@ -494,63 +1031,82 @@ var init_executor2 = __esm({
494
1031
  timeout: this.config.timeouts.prompt ?? 6e5
495
1032
  });
496
1033
  this.initialized = true;
497
- log7.info("Executor initialized", { cliPath: cx.cliPath, workdir: cx.workdir });
1034
+ log8.info("Executor initialized", { cliPath: cx.cliPath, workdir: cx.workdir });
498
1035
  }
499
1036
  async shutdown() {
500
1037
  this.client = null;
501
1038
  this.initialized = false;
502
- log7.info("Executor shut down");
1039
+ log8.info("Executor shut down");
503
1040
  }
504
1041
  async execute(ctx, bus) {
505
- const { taskId, contextId, userMessage, task } = ctx;
1042
+ const { taskId, userMessage } = ctx;
506
1043
  await this.initialize();
1044
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
1045
+ let contextId = ctx.contextId;
1046
+ if (!contextId) {
1047
+ contextId = generateContextId();
1048
+ log8.info("Generated new contextId", { contextId, taskId });
1049
+ }
1050
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "codex") : null;
1051
+ const sessionId = binding?.sessionId;
507
1052
  try {
508
- if (!task) {
1053
+ const existingTask = ctx.task;
1054
+ if (!existingTask) {
509
1055
  publishTask(bus, taskId, contextId);
510
1056
  publishStatus(bus, taskId, contextId, "submitted");
511
1057
  }
512
- publishStatus(bus, taskId, contextId, "working", "Processing request...");
513
- const promptText = this.extractText(userMessage);
514
- log7.info("Sending prompt to Codex", { taskId, len: promptText.length });
515
- const response = await this.client.execute(promptText);
516
- const finalText = response || "No response from Codex.";
517
- publishFinalArtifact(bus, taskId, contextId, finalText);
1058
+ publishStatus(bus, taskId, contextId, "working");
1059
+ const promptText = extractText(userMessage);
1060
+ const userMsg = userMessage;
1061
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
1062
+ log8.info("Sending prompt to Codex", {
1063
+ taskId,
1064
+ contextId,
1065
+ sessionId: sessionId || "(new)",
1066
+ len: promptText.length,
1067
+ workingDirectory: workingDirectory || "(default)"
1068
+ });
1069
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
1070
+ this.sessionStore.set({
1071
+ provider: "codex",
1072
+ contextId,
1073
+ sessionId: result.sessionId,
1074
+ activeTaskId: void 0,
1075
+ activeTaskState: void 0,
1076
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1077
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1078
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
1079
+ });
1080
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Codex.");
518
1081
  publishStatus(bus, taskId, contextId, "completed", void 0, true);
519
1082
  bus.finished();
520
- log7.info("Task completed", { taskId, len: finalText.length });
1083
+ log8.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
521
1084
  } catch (error) {
522
1085
  const msg = error.message ?? String(error);
523
- log7.error("Execution failed", { taskId, error: msg });
1086
+ log8.error("Execution failed", { taskId, contextId, error: msg });
524
1087
  publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
525
1088
  bus.finished();
526
1089
  }
527
1090
  }
528
1091
  async cancelTask(taskId, bus) {
529
- log7.info("Cancel requested for Codex task", { taskId });
1092
+ log8.info("Cancel requested for Codex task", { taskId });
530
1093
  this.client?.abort();
531
1094
  publishStatus(bus, taskId, "", "canceled", "Codex task cancelled", true);
532
1095
  bus.finished();
533
1096
  }
534
- // ── Helpers ─────────────────────────────────────────────────────────────
535
- extractText(message) {
536
- return message.parts.filter((p) => {
537
- const part = p;
538
- const text = part.text;
539
- return text !== void 0 && text !== null;
540
- }).map((p) => p.text).join("\n");
541
- }
542
1097
  };
543
1098
  }
544
1099
  });
545
1100
 
546
1101
  // src/executors/claude/client.ts
547
- var import_node_child_process3, log8, ClaudeClient;
1102
+ var import_node_child_process3, import_uuid3, log9, ClaudeClient;
548
1103
  var init_client3 = __esm({
549
1104
  "src/executors/claude/client.ts"() {
550
1105
  "use strict";
551
1106
  import_node_child_process3 = require("child_process");
1107
+ import_uuid3 = require("uuid");
552
1108
  init_logger();
553
- log8 = logger.child("claude:client");
1109
+ log9 = logger.child("claude:client");
554
1110
  ClaudeClient = class {
555
1111
  constructor(config) {
556
1112
  this.config = config;
@@ -568,14 +1124,31 @@ var init_client3 = __esm({
568
1124
  }
569
1125
  }
570
1126
  /**
571
- * Execute a prompt via the Claude Code CLI and return the response text.
1127
+ * Execute a prompt via the Claude Code CLI.
1128
+ *
1129
+ * @param prompt - The user prompt text.
1130
+ * @param sessionId - If provided, resumes the session with `--resume`.
1131
+ * Otherwise creates a new session with `--session-id <new-uuid>`.
1132
+ * @param workingDirectory - Optional per-request working directory override.
1133
+ * @returns The response text and the session ID (new or existing).
572
1134
  */
573
- async execute(prompt) {
1135
+ async execute(prompt, sessionId, workingDirectory) {
1136
+ if (sessionId) {
1137
+ const text2 = await this.run(["-p", "--resume", sessionId, prompt], workingDirectory);
1138
+ return { text: text2, sessionId };
1139
+ }
1140
+ const newSessionId = (0, import_uuid3.v4)();
1141
+ const text = await this.run(["-p", "--session-id", newSessionId, prompt], workingDirectory);
1142
+ return { text, sessionId: newSessionId };
1143
+ }
1144
+ // ── Internal ──────────────────────────────────────────────────────────
1145
+ async run(args, workingDirectory) {
574
1146
  const cliPath = this.config.cliPath || "claude";
575
1147
  const timeout = this.config.timeout ?? 3e5;
576
1148
  const MAX_PROMPT = 1e5;
577
- if (prompt.length > MAX_PROMPT) {
578
- throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
1149
+ const promptArg = args[args.length - 1];
1150
+ if (promptArg && promptArg.length > MAX_PROMPT) {
1151
+ throw new Error(`Prompt too large (${promptArg.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
579
1152
  }
580
1153
  const env = {
581
1154
  PATH: process.env.PATH ?? "",
@@ -587,15 +1160,19 @@ var init_client3 = __esm({
587
1160
  if (this.config.apiKey) {
588
1161
  env["ANTHROPIC_API_KEY"] = this.config.apiKey;
589
1162
  }
590
- const args = ["-p"];
1163
+ const fullArgs = [];
1164
+ for (const arg of args.slice(0, -1)) {
1165
+ fullArgs.push(arg);
1166
+ }
591
1167
  if (this.config.model) {
592
- args.push("--model", this.config.model);
1168
+ fullArgs.push("--model", this.config.model);
593
1169
  }
594
- args.push(prompt);
595
- log8.info("Spawning claude", { cliPath, workdir: this.config.workdir, model: this.config.model });
1170
+ fullArgs.push(promptArg);
1171
+ const workdir = workingDirectory || this.config.workdir || process.cwd();
1172
+ log9.info("Spawning claude", { cliPath, workdir, model: this.config.model });
596
1173
  return new Promise((resolve3, reject) => {
597
- const child = (0, import_node_child_process3.spawn)(cliPath, args, {
598
- cwd: this.config.workdir || process.cwd(),
1174
+ const child = (0, import_node_child_process3.spawn)(cliPath, fullArgs, {
1175
+ cwd: workdir,
599
1176
  env,
600
1177
  stdio: ["ignore", "pipe", "pipe"]
601
1178
  });
@@ -622,7 +1199,7 @@ var init_client3 = __esm({
622
1199
  });
623
1200
  child.stderr?.on("data", (chunk) => {
624
1201
  stderr += chunk.toString();
625
- log8.debug("Claude stderr", { text: chunk.toString().trim() });
1202
+ log9.debug("Claude stderr", { text: chunk.toString().trim() });
626
1203
  });
627
1204
  child.on("close", (code) => {
628
1205
  clearTimeout(timer);
@@ -631,13 +1208,13 @@ var init_client3 = __esm({
631
1208
  resolve3(stdout.trim());
632
1209
  } else {
633
1210
  const errMsg = stderr.trim() || stdout.trim() || `Claude Code exited with code ${code}`;
634
- log8.warn("Claude non-zero exit", { code, stderr: errMsg });
1211
+ log9.warn("Claude non-zero exit", { code, stderr: errMsg });
635
1212
  reject(new Error(errMsg));
636
1213
  }
637
1214
  });
638
1215
  child.on("error", (err) => {
639
1216
  clearTimeout(timer);
640
- log8.error("Claude spawn failed", { error: err.message });
1217
+ log9.error("Claude spawn failed", { error: err.message });
641
1218
  reject(new Error(`Failed to start claude: ${err.message}. Install: npm i -g @anthropic-ai/claude-code`));
642
1219
  });
643
1220
  });
@@ -652,22 +1229,24 @@ __export(executor_exports3, {
652
1229
  ClaudeExecutor: () => ClaudeExecutor,
653
1230
  createClaudeExecutor: () => createClaudeExecutor
654
1231
  });
655
- function createClaudeExecutor(config) {
656
- return new ClaudeExecutor(config);
1232
+ function createClaudeExecutor(config, sessionStore) {
1233
+ return new ClaudeExecutor(config, sessionStore);
657
1234
  }
658
- var log9, ClaudeExecutor;
1235
+ var log10, ClaudeExecutor;
659
1236
  var init_executor3 = __esm({
660
1237
  "src/executors/claude/executor.ts"() {
661
1238
  "use strict";
1239
+ init_session_store();
662
1240
  init_logger();
663
1241
  init_client3();
664
1242
  init_events();
665
- log9 = logger.child("claude:executor");
1243
+ log10 = logger.child("claude:executor");
666
1244
  ClaudeExecutor = class {
667
- constructor(config) {
1245
+ constructor(config, sessionStore) {
668
1246
  this.client = null;
669
1247
  this.initialized = false;
670
1248
  this.config = config;
1249
+ this.sessionStore = sessionStore;
671
1250
  }
672
1251
  async initialize() {
673
1252
  if (this.initialized) return;
@@ -680,50 +1259,69 @@ var init_executor3 = __esm({
680
1259
  timeout: this.config.timeouts.prompt ?? 6e5
681
1260
  });
682
1261
  this.initialized = true;
683
- log9.info("Executor initialized", { cliPath: cc.cliPath, workdir: cc.workdir });
1262
+ log10.info("Executor initialized", { cliPath: cc.cliPath, workdir: cc.workdir });
684
1263
  }
685
1264
  async shutdown() {
686
1265
  this.client = null;
687
1266
  this.initialized = false;
688
- log9.info("Executor shut down");
1267
+ log10.info("Executor shut down");
689
1268
  }
690
1269
  async execute(ctx, bus) {
691
- const { taskId, contextId, userMessage, task } = ctx;
1270
+ const { taskId, userMessage } = ctx;
692
1271
  await this.initialize();
1272
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
1273
+ let contextId = ctx.contextId;
1274
+ if (!contextId) {
1275
+ contextId = generateContextId();
1276
+ log10.info("Generated new contextId", { contextId, taskId });
1277
+ }
1278
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "claude") : null;
1279
+ const sessionId = binding?.sessionId;
693
1280
  try {
694
- if (!task) {
1281
+ const existingTask = ctx.task;
1282
+ if (!existingTask) {
695
1283
  publishTask(bus, taskId, contextId);
696
1284
  publishStatus(bus, taskId, contextId, "submitted");
697
1285
  }
698
- publishStatus(bus, taskId, contextId, "working", "Processing request...");
699
- const promptText = this.extractText(userMessage);
700
- log9.info("Sending prompt to Claude Code", { taskId, len: promptText.length });
701
- const response = await this.client.execute(promptText);
702
- const finalText = response || "No response from Claude Code.";
703
- publishFinalArtifact(bus, taskId, contextId, finalText);
1286
+ publishStatus(bus, taskId, contextId, "working");
1287
+ const promptText = extractText(userMessage);
1288
+ const userMsg = userMessage;
1289
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
1290
+ log10.info("Sending prompt to Claude Code", {
1291
+ taskId,
1292
+ contextId,
1293
+ sessionId: sessionId || "(new)",
1294
+ len: promptText.length,
1295
+ workingDirectory: workingDirectory || "(default)"
1296
+ });
1297
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
1298
+ this.sessionStore.set({
1299
+ provider: "claude",
1300
+ contextId,
1301
+ sessionId: result.sessionId,
1302
+ activeTaskId: void 0,
1303
+ activeTaskState: void 0,
1304
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1305
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1306
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
1307
+ });
1308
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Claude Code.");
704
1309
  publishStatus(bus, taskId, contextId, "completed", void 0, true);
705
1310
  bus.finished();
706
- log9.info("Task completed", { taskId, len: finalText.length });
1311
+ log10.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
707
1312
  } catch (error) {
708
1313
  const msg = error.message ?? String(error);
709
- log9.error("Execution failed", { taskId, error: msg });
1314
+ log10.error("Execution failed", { taskId, contextId, error: msg });
710
1315
  publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
711
1316
  bus.finished();
712
1317
  }
713
1318
  }
714
1319
  async cancelTask(taskId, bus) {
715
- log9.info("Cancel requested for Claude Code task", { taskId });
1320
+ log10.info("Cancel requested for Claude Code task", { taskId });
716
1321
  this.client?.abort();
717
1322
  publishStatus(bus, taskId, "", "canceled", "Claude Code task cancelled", true);
718
1323
  bus.finished();
719
1324
  }
720
- extractText(message) {
721
- return message.parts.filter((p) => {
722
- const part = p;
723
- const text = part.text;
724
- return text !== void 0 && text !== null;
725
- }).map((p) => p.text).join("\n");
726
- }
727
1325
  };
728
1326
  }
729
1327
  });
@@ -733,14 +1331,14 @@ var bridge_exports = {};
733
1331
  __export(bridge_exports, {
734
1332
  BridgeClient: () => BridgeClient
735
1333
  });
736
- var import_node_net, import_node_crypto, log10, SimpleWs, BridgeClient;
1334
+ var import_node_net, import_node_crypto2, log11, SimpleWs, BridgeClient;
737
1335
  var init_bridge = __esm({
738
1336
  "src/bridge.ts"() {
739
1337
  "use strict";
740
1338
  import_node_net = require("net");
741
- import_node_crypto = require("crypto");
1339
+ import_node_crypto2 = require("crypto");
742
1340
  init_logger();
743
- log10 = logger.child("bridge");
1341
+ log11 = logger.child("bridge");
744
1342
  SimpleWs = class {
745
1343
  constructor() {
746
1344
  this.socket = null;
@@ -753,7 +1351,7 @@ var init_bridge = __esm({
753
1351
  const host = url.hostname;
754
1352
  const port = parseInt(url.port) || (url.protocol === "wss:" ? 443 : 80);
755
1353
  const path = url.pathname + url.search;
756
- const key = (0, import_node_crypto.randomBytes)(16).toString("base64");
1354
+ const key = (0, import_node_crypto2.randomBytes)(16).toString("base64");
757
1355
  const req = (0, import_node_net.connect)({ host, port }, () => {
758
1356
  const headers = [
759
1357
  `GET ${path} HTTP/1.1`,
@@ -847,7 +1445,7 @@ var init_bridge = __esm({
847
1445
  if (!this.socket) return;
848
1446
  const payload = Buffer.from(data, "utf-8");
849
1447
  const len = payload.length;
850
- const maskKey = (0, import_node_crypto.randomBytes)(4);
1448
+ const maskKey = (0, import_node_crypto2.randomBytes)(4);
851
1449
  const masked = Buffer.alloc(len);
852
1450
  for (let i = 0; i < len; i++) {
853
1451
  masked[i] = payload[i] ^ maskKey[i % 4];
@@ -899,7 +1497,7 @@ var init_bridge = __esm({
899
1497
  this.ws = new SimpleWs();
900
1498
  let firstConnect = true;
901
1499
  this.ws.onopen = () => {
902
- log10.info("Bridge connected");
1500
+ log11.info("Bridge connected");
903
1501
  this.ws.send(JSON.stringify({
904
1502
  type: "agent:register",
905
1503
  agentId: this.config.agentId,
@@ -914,7 +1512,7 @@ var init_bridge = __esm({
914
1512
  return;
915
1513
  }
916
1514
  if (msg.type === "agent:registered") {
917
- log10.info("Agent registered via bridge", { agentId: this.config.agentId });
1515
+ log11.info("Agent registered via bridge", { agentId: this.config.agentId });
918
1516
  if (firstConnect) {
919
1517
  firstConnect = false;
920
1518
  resolve3();
@@ -926,7 +1524,7 @@ var init_bridge = __esm({
926
1524
  }
927
1525
  };
928
1526
  this.ws.onclose = () => {
929
- log10.warn("Bridge disconnected, reconnecting...");
1527
+ log11.warn("Bridge disconnected, reconnecting...");
930
1528
  if (this.running) {
931
1529
  this.reconnectTimer = setTimeout(
932
1530
  () => this.start().catch(() => {
@@ -936,13 +1534,13 @@ var init_bridge = __esm({
936
1534
  }
937
1535
  };
938
1536
  this.ws.onerror = (err) => {
939
- log10.error("Bridge error", { error: err.message });
1537
+ log11.error("Bridge error", { error: err.message });
940
1538
  if (firstConnect) {
941
1539
  firstConnect = false;
942
1540
  reject(err);
943
1541
  }
944
1542
  };
945
- log10.info("Connecting bridge", { url: this.config.bridgeUrl });
1543
+ log11.info("Connecting bridge", { url: this.config.bridgeUrl });
946
1544
  this.ws.connect(this.config.bridgeUrl, this.config.apiKey);
947
1545
  });
948
1546
  }
@@ -957,7 +1555,7 @@ var init_bridge = __esm({
957
1555
  }
958
1556
  this.ws?.close();
959
1557
  this.ws = null;
960
- log10.info("Bridge stopped");
1558
+ log11.info("Bridge stopped");
961
1559
  }
962
1560
  // ── Internal ────────────────────────────────────────────────────────────
963
1561
  async handleHttpRequest(msg) {
@@ -991,7 +1589,7 @@ var init_bridge = __esm({
991
1589
  body: responseBody
992
1590
  }));
993
1591
  } catch (err) {
994
- log10.error("Proxy request failed", { url, error: err.message });
1592
+ log11.error("Proxy request failed", { url, error: err.message });
995
1593
  this.ws?.send(JSON.stringify({
996
1594
  type: "http:error",
997
1595
  requestId,
@@ -1005,7 +1603,7 @@ var init_bridge = __esm({
1005
1603
 
1006
1604
  // src/cli.ts
1007
1605
  var import_node_util = require("util");
1008
- var import_node_path2 = require("path");
1606
+ var import_node_path3 = require("path");
1009
1607
 
1010
1608
  // src/config/loader.ts
1011
1609
  var import_node_fs = require("fs");
@@ -1051,6 +1649,8 @@ var DEFAULTS = Object.freeze({
1051
1649
  session: {
1052
1650
  titlePrefix: "A2A Session",
1053
1651
  reuseByContext: true,
1652
+ persist: true,
1653
+ storePath: "",
1054
1654
  ttl: 36e5,
1055
1655
  cleanupInterval: 3e5
1056
1656
  },
@@ -1069,7 +1669,7 @@ var DEFAULTS = Object.freeze({
1069
1669
  level: "info"
1070
1670
  },
1071
1671
  opencode: {
1072
- baseUrl: "http://localhost:4096",
1672
+ baseUrl: "",
1073
1673
  projectDirectory: "",
1074
1674
  model: "",
1075
1675
  agent: "",
@@ -1195,7 +1795,8 @@ function buildAgentCard(config) {
1195
1795
  skills,
1196
1796
  capabilities: {
1197
1797
  streaming: agentCard.streaming ?? true,
1198
- pushNotifications: agentCard.pushNotifications ?? false
1798
+ pushNotifications: agentCard.pushNotifications ?? false,
1799
+ stateTransitionHistory: true
1199
1800
  },
1200
1801
  // Additional interfaces advertised to orchestrators
1201
1802
  additionalInterfaces: [
@@ -1209,15 +1810,26 @@ function buildAgentCard(config) {
1209
1810
 
1210
1811
  // src/server/index.ts
1211
1812
  init_executors();
1813
+ init_session_store();
1212
1814
  init_logger();
1213
- var log3 = logger.child("server");
1815
+ var log4 = logger.child("server");
1214
1816
  async function createA2AServer(config) {
1215
1817
  const srv = config.server;
1216
1818
  const port = srv.port ?? 3e3;
1217
1819
  const hostname = srv.hostname ?? "0.0.0.0";
1218
1820
  const advertiseHost = srv.advertiseHost ?? "localhost";
1219
1821
  const advertiseProto = srv.advertiseProtocol ?? "http";
1220
- const executor = createExecutor(config);
1822
+ const sessionConf = config.session;
1823
+ const sessionStore = new SessionBindingStore({
1824
+ persist: sessionConf.persist ?? true,
1825
+ storePath: resolveSessionStorePath({
1826
+ configuredPath: sessionConf.storePath,
1827
+ provider: config.provider,
1828
+ port
1829
+ }),
1830
+ cleanupIntervalMs: sessionConf.cleanupInterval
1831
+ });
1832
+ const executor = createExecutor(config, sessionStore);
1221
1833
  await executor.initialize();
1222
1834
  const agentCard = buildAgentCard(config);
1223
1835
  const taskStore = new import_server.InMemoryTaskStore();
@@ -1249,7 +1861,7 @@ async function createA2AServer(config) {
1249
1861
  app.use("/a2a/jsonrpc", (0, import_express2.jsonRpcHandler)({ requestHandler, userBuilder: import_express2.UserBuilder.noAuthentication }));
1250
1862
  app.use("/a2a/rest", (0, import_express2.restHandler)({ requestHandler, userBuilder: import_express2.UserBuilder.noAuthentication }));
1251
1863
  const httpServer = app.listen(port, hostname, () => {
1252
- log3.info("A2A server started", { bind: hostname, port, proto: advertiseProto });
1864
+ log4.info("A2A server started", { bind: hostname, port, proto: advertiseProto });
1253
1865
  const banner = [
1254
1866
  "",
1255
1867
  "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
@@ -1273,14 +1885,15 @@ async function createA2AServer(config) {
1273
1885
  async shutdown() {
1274
1886
  httpServer.close();
1275
1887
  await executor.shutdown();
1276
- log3.info("Server shut down");
1888
+ sessionStore.shutdown();
1889
+ log4.info("Server shut down");
1277
1890
  }
1278
1891
  };
1279
1892
  }
1280
1893
 
1281
1894
  // src/cli.ts
1282
1895
  init_logger();
1283
- var log11 = logger.child("cli");
1896
+ var log12 = logger.child("cli");
1284
1897
  var PKG_VERSION = "0.1.0";
1285
1898
  function printUsage() {
1286
1899
  console.log(`
@@ -1431,7 +2044,7 @@ async function registerWithGateway(registerUrl, registerKey, config, agentCardUr
1431
2044
  };
1432
2045
  try {
1433
2046
  const url = registerUrl.replace(/\/$/, "") + "/api/assistants";
1434
- log11.info("Registering with gateway", { url, agentCardUrl });
2047
+ log12.info("Registering with gateway", { url, agentCardUrl });
1435
2048
  const resp = await fetch(url, {
1436
2049
  method: "POST",
1437
2050
  headers: {
@@ -1442,24 +2055,24 @@ async function registerWithGateway(registerUrl, registerKey, config, agentCardUr
1442
2055
  });
1443
2056
  if (!resp.ok) {
1444
2057
  const text = await resp.text().catch(() => "");
1445
- log11.error("Gateway registration failed", { status: resp.status, body: text });
2058
+ log12.error("Gateway registration failed", { status: resp.status, body: text });
1446
2059
  return null;
1447
2060
  }
1448
2061
  const result = await resp.json();
1449
2062
  const assistantId = result.data?.id ?? body.graphDefinition.key;
1450
- log11.info("Registered with gateway", { assistantId, agentCardUrl });
2063
+ log12.info("Registered with gateway", { assistantId, agentCardUrl });
1451
2064
  console.log(`Registered as agent: ${assistantId}`);
1452
2065
  console.log(`Agent card: ${agentCardUrl}`);
1453
2066
  return { assistantId, agentCardUrl };
1454
2067
  } catch (err) {
1455
- log11.error("Gateway registration error", { error: err.message });
2068
+ log12.error("Gateway registration error", { error: err.message });
1456
2069
  return null;
1457
2070
  }
1458
2071
  }
1459
2072
  async function unregisterFromGateway(registerUrl, registerKey, assistantId) {
1460
2073
  try {
1461
2074
  const url = `${registerUrl.replace(/\/$/, "")}/api/assistants/${assistantId}`;
1462
- log11.info("Unregistering from gateway", { assistantId });
2075
+ log12.info("Unregistering from gateway", { assistantId });
1463
2076
  const resp = await fetch(url, {
1464
2077
  method: "DELETE",
1465
2078
  headers: {
@@ -1467,19 +2080,19 @@ async function unregisterFromGateway(registerUrl, registerKey, assistantId) {
1467
2080
  }
1468
2081
  });
1469
2082
  if (!resp.ok && resp.status !== 404) {
1470
- log11.warn("Gateway unregistration failed", { status: resp.status });
2083
+ log12.warn("Gateway unregistration failed", { status: resp.status });
1471
2084
  } else {
1472
- log11.info("Unregistered from gateway", { assistantId });
2085
+ log12.info("Unregistered from gateway", { assistantId });
1473
2086
  }
1474
2087
  } catch (err) {
1475
- log11.warn("Gateway unregistration error", { error: err.message });
2088
+ log12.warn("Gateway unregistration error", { error: err.message });
1476
2089
  }
1477
2090
  }
1478
2091
  async function main() {
1479
2092
  const { configPath, overrides, registerUrl, registerKey, unregisterOnShutdown, bridgeUrl, bridgeKey } = parseCliArgs();
1480
2093
  const config = resolveConfig(configPath, overrides);
1481
2094
  if (configPath) {
1482
- config.configDir = (0, import_node_path2.dirname)((0, import_node_path2.resolve)(configPath));
2095
+ config.configDir = (0, import_node_path3.dirname)((0, import_node_path3.resolve)(configPath));
1483
2096
  }
1484
2097
  const levelStr = config.logging.level ?? "info";
1485
2098
  const levelMap = {
@@ -1490,9 +2103,9 @@ async function main() {
1490
2103
  };
1491
2104
  logger.setLevel(levelMap[levelStr] ?? 20 /* INFO */);
1492
2105
  if (!configPath) {
1493
- log11.info("No --config provided \u2014 running with defaults + CLI args.");
2106
+ log12.info("No --config provided \u2014 running with defaults + CLI args.");
1494
2107
  }
1495
- log11.info("Starting CLI A2A Gateway", {
2108
+ log12.info("Starting CLI A2A Gateway", {
1496
2109
  provider: config.provider,
1497
2110
  config: configPath ?? "(defaults)",
1498
2111
  agent: config.agentCard.name,
@@ -1524,10 +2137,10 @@ async function main() {
1524
2137
  localBaseUrl: `http://localhost:${listenPort}`
1525
2138
  });
1526
2139
  bridgeClient.start().then(() => {
1527
- log11.info("Bridge connected \u2014 agent reachable via gateway proxy");
2140
+ log12.info("Bridge connected \u2014 agent reachable via gateway proxy");
1528
2141
  console.log(`Bridge: ws://gateway \u2192 localhost:${listenPort}`);
1529
2142
  }).catch((err) => {
1530
- log11.error("Bridge connection failed", { error: err.message });
2143
+ log12.error("Bridge connection failed", { error: err.message });
1531
2144
  });
1532
2145
  }
1533
2146
  let registeredAssistantId = null;
@@ -1544,7 +2157,7 @@ async function main() {
1544
2157
  }
1545
2158
  }
1546
2159
  const shutdown = async (signal) => {
1547
- log11.info(`${signal} received, shutting down...`);
2160
+ log12.info(`${signal} received, shutting down...`);
1548
2161
  bridgeClient?.stop();
1549
2162
  if (registeredAssistantId && registerUrl && registerKey && unregisterOnShutdown) {
1550
2163
  await unregisterFromGateway(registerUrl, registerKey, registeredAssistantId);
@@ -1556,7 +2169,7 @@ async function main() {
1556
2169
  process.on("SIGTERM", () => shutdown("SIGTERM"));
1557
2170
  }
1558
2171
  main().catch((err) => {
1559
- log11.error("Fatal error", { error: err.message, stack: err.stack });
2172
+ log12.error("Fatal error", { error: err.message, stack: err.stack });
1560
2173
  process.exit(1);
1561
2174
  });
1562
2175
  //# sourceMappingURL=cli.js.map