@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/index.js CHANGED
@@ -34,16 +34,19 @@ __export(index_exports, {
34
34
  CodexExecutor: () => CodexExecutor,
35
35
  LogLevel: () => LogLevel,
36
36
  OpenCodeExecutor: () => OpenCodeExecutor,
37
+ SessionBindingStore: () => SessionBindingStore,
37
38
  createA2AServer: () => createA2AServer,
38
39
  createClaudeExecutor: () => createClaudeExecutor,
39
40
  createCodexExecutor: () => createCodexExecutor,
40
41
  createExecutor: () => createExecutor,
41
42
  createOpenCodeExecutor: () => createOpenCodeExecutor,
43
+ generateContextId: () => generateContextId,
42
44
  getExecutor: () => getExecutor,
43
45
  loadConfigFile: () => loadConfigFile,
44
46
  logger: () => logger,
45
47
  registerExecutor: () => registerExecutor,
46
- resolveConfig: () => resolveConfig
48
+ resolveConfig: () => resolveConfig,
49
+ resolveSessionStorePath: () => resolveSessionStorePath
47
50
  });
48
51
  module.exports = __toCommonJS(index_exports);
49
52
 
@@ -91,6 +94,8 @@ var DEFAULTS = Object.freeze({
91
94
  session: {
92
95
  titlePrefix: "A2A Session",
93
96
  reuseByContext: true,
97
+ persist: true,
98
+ storePath: "",
94
99
  ttl: 36e5,
95
100
  cleanupInterval: 3e5
96
101
  },
@@ -109,7 +114,7 @@ var DEFAULTS = Object.freeze({
109
114
  level: "info"
110
115
  },
111
116
  opencode: {
112
- baseUrl: "http://localhost:4096",
117
+ baseUrl: "",
113
118
  projectDirectory: "",
114
119
  model: "",
115
120
  agent: "",
@@ -284,7 +289,8 @@ function buildAgentCard(config) {
284
289
  skills,
285
290
  capabilities: {
286
291
  streaming: agentCard.streaming ?? true,
287
- pushNotifications: agentCard.pushNotifications ?? false
292
+ pushNotifications: agentCard.pushNotifications ?? false,
293
+ stateTransitionHistory: true
288
294
  },
289
295
  // Additional interfaces advertised to orchestrators
290
296
  additionalInterfaces: [
@@ -306,24 +312,219 @@ function registerExecutor(provider, factory) {
306
312
  function getExecutor(provider) {
307
313
  return registry.get(provider);
308
314
  }
309
- function createExecutor(config) {
315
+ function createExecutor(config, sessionStore) {
310
316
  const factory = registry.get(config.provider);
311
317
  if (!factory) {
312
318
  const available = Array.from(registry.keys()).join(", ");
313
319
  throw new Error(`Unknown provider: ${config.provider}. Available: ${available}`);
314
320
  }
315
- return factory(config);
321
+ return factory(config, sessionStore);
322
+ }
323
+
324
+ // src/session-store.ts
325
+ var import_node_path2 = require("path");
326
+ var import_node_os = require("os");
327
+ var import_node_fs2 = require("fs");
328
+ var import_node_crypto = require("crypto");
329
+ var log3 = logger.child("session-store");
330
+ var DEFAULT_STORE_DIR = (0, import_node_path2.join)(
331
+ (0, import_node_os.homedir)(),
332
+ "Library",
333
+ "Application Support",
334
+ "axiom-lattice",
335
+ "cli-a2a"
336
+ );
337
+ var DEFAULT_STORE_PATH = (0, import_node_path2.join)(DEFAULT_STORE_DIR, "sessions.json");
338
+ var DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
339
+ function resolveSessionStorePath(options) {
340
+ if (options.configuredPath && options.configuredPath.length > 0) {
341
+ return options.configuredPath;
342
+ }
343
+ return (0, import_node_path2.join)(
344
+ options.storeDir ?? DEFAULT_STORE_DIR,
345
+ `sessions-${options.provider}-${options.port}.json`
346
+ );
347
+ }
348
+ var SessionBindingStore = class {
349
+ constructor(opts) {
350
+ this.cache = /* @__PURE__ */ new Map();
351
+ this.cleanupTimer = null;
352
+ this.dirty = false;
353
+ this.flushTimeout = null;
354
+ this.storePath = opts?.storePath && opts.storePath.length > 0 ? opts.storePath : DEFAULT_STORE_PATH;
355
+ this.persist = opts?.persist ?? true;
356
+ this.loadFromFile();
357
+ this.startCleanup(opts?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS);
358
+ }
359
+ // ── Public API ────────────────────────────────────────────────────────
360
+ get(contextId, provider) {
361
+ const key = this.makeKey(contextId, provider);
362
+ const binding = this.cache.get(key);
363
+ if (!binding) return null;
364
+ if (this.isExpired(binding)) {
365
+ this.cache.delete(key);
366
+ this.markDirty();
367
+ return null;
368
+ }
369
+ return binding;
370
+ }
371
+ set(binding) {
372
+ const key = this.makeKey(binding.contextId, binding.provider);
373
+ this.cache.set(key, binding);
374
+ this.markDirty();
375
+ }
376
+ delete(contextId, provider) {
377
+ const key = this.makeKey(contextId, provider);
378
+ const existed = this.cache.delete(key);
379
+ if (existed) this.markDirty();
380
+ return existed;
381
+ }
382
+ clearActiveTask(contextId, provider) {
383
+ const binding = this.get(contextId, provider);
384
+ if (binding) {
385
+ binding.activeTaskId = void 0;
386
+ binding.activeTaskState = void 0;
387
+ binding.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
388
+ this.markDirty();
389
+ }
390
+ }
391
+ /**
392
+ * Remove all expired entries.
393
+ * Called automatically on the cleanup interval; call manually before shutdown.
394
+ */
395
+ cleanup() {
396
+ let removed = 0;
397
+ for (const [key, binding] of this.cache) {
398
+ if (this.isExpired(binding)) {
399
+ this.cache.delete(key);
400
+ removed++;
401
+ }
402
+ }
403
+ if (removed > 0) {
404
+ this.markDirty();
405
+ log3.info("TTL cleanup completed", { removed, remaining: this.cache.size });
406
+ }
407
+ return removed;
408
+ }
409
+ /**
410
+ * Graceful shutdown: stop cleanup timer, flush pending writes.
411
+ */
412
+ shutdown() {
413
+ if (this.cleanupTimer) {
414
+ clearInterval(this.cleanupTimer);
415
+ this.cleanupTimer = null;
416
+ }
417
+ if (this.flushTimeout) {
418
+ clearTimeout(this.flushTimeout);
419
+ this.flushTimeout = null;
420
+ }
421
+ this.flushToFile();
422
+ log3.info("Session store shut down", { bindings: this.cache.size });
423
+ }
424
+ /** For testing: return the number of cached bindings. */
425
+ get size() {
426
+ return this.cache.size;
427
+ }
428
+ // ── Internal ──────────────────────────────────────────────────────────
429
+ makeKey(contextId, provider) {
430
+ return `${provider}:${contextId}`;
431
+ }
432
+ isExpired(binding) {
433
+ if (!binding.expiresAt) return false;
434
+ return Date.now() >= new Date(binding.expiresAt).getTime();
435
+ }
436
+ startCleanup(intervalMs) {
437
+ this.cleanupTimer = setInterval(() => this.cleanup(), intervalMs);
438
+ this.cleanupTimer.unref();
439
+ }
440
+ // ── Persistence ───────────────────────────────────────────────────────
441
+ markDirty() {
442
+ if (!this.persist) return;
443
+ this.dirty = true;
444
+ if (!this.flushTimeout) {
445
+ this.flushTimeout = setTimeout(() => {
446
+ this.flushToFile();
447
+ }, 1e3);
448
+ }
449
+ }
450
+ flushToFile() {
451
+ this.flushTimeout = null;
452
+ if (!this.persist || !this.dirty) return;
453
+ this.dirty = false;
454
+ this.writeAtomically();
455
+ }
456
+ writeAtomically() {
457
+ const tmpPath = this.storePath + ".tmp";
458
+ const data = {
459
+ version: 1,
460
+ bindings: Array.from(this.cache.values())
461
+ };
462
+ try {
463
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(this.storePath), { recursive: true });
464
+ (0, import_node_fs2.writeFileSync)(tmpPath, JSON.stringify(data, null, 2), "utf-8");
465
+ (0, import_node_fs2.renameSync)(tmpPath, this.storePath);
466
+ } catch (err) {
467
+ log3.error("Failed to write session store", { error: err.message });
468
+ try {
469
+ (0, import_node_fs2.unlinkSync)(tmpPath);
470
+ } catch {
471
+ }
472
+ }
473
+ }
474
+ loadFromFile() {
475
+ if (!this.persist || !(0, import_node_fs2.existsSync)(this.storePath)) return;
476
+ try {
477
+ const raw = (0, import_node_fs2.readFileSync)(this.storePath, "utf-8");
478
+ const data = JSON.parse(raw);
479
+ if (!data || !Array.isArray(data.bindings)) {
480
+ throw new Error("Invalid store format: bindings array missing");
481
+ }
482
+ let loaded = 0;
483
+ let expired = 0;
484
+ for (const b of data.bindings) {
485
+ if (!b.provider || !b.contextId || !b.sessionId) continue;
486
+ if (this.isExpired(b)) {
487
+ expired++;
488
+ continue;
489
+ }
490
+ const key = this.makeKey(b.contextId, b.provider);
491
+ this.cache.set(key, b);
492
+ loaded++;
493
+ }
494
+ log3.info("Session store loaded", { path: this.storePath, loaded, expired });
495
+ } catch (err) {
496
+ log3.error("Failed to load session store \u2014 starting fresh", {
497
+ path: this.storePath,
498
+ error: err.message
499
+ });
500
+ this.cache.clear();
501
+ this.writeAtomically();
502
+ }
503
+ }
504
+ };
505
+ function generateContextId() {
506
+ return (0, import_node_crypto.randomUUID)();
316
507
  }
317
508
 
318
509
  // src/server/index.ts
319
- var log3 = logger.child("server");
510
+ var log4 = logger.child("server");
320
511
  async function createA2AServer(config) {
321
512
  const srv = config.server;
322
513
  const port = srv.port ?? 3e3;
323
514
  const hostname = srv.hostname ?? "0.0.0.0";
324
515
  const advertiseHost = srv.advertiseHost ?? "localhost";
325
516
  const advertiseProto = srv.advertiseProtocol ?? "http";
326
- const executor = createExecutor(config);
517
+ const sessionConf = config.session;
518
+ const sessionStore = new SessionBindingStore({
519
+ persist: sessionConf.persist ?? true,
520
+ storePath: resolveSessionStorePath({
521
+ configuredPath: sessionConf.storePath,
522
+ provider: config.provider,
523
+ port
524
+ }),
525
+ cleanupIntervalMs: sessionConf.cleanupInterval
526
+ });
527
+ const executor = createExecutor(config, sessionStore);
327
528
  await executor.initialize();
328
529
  const agentCard = buildAgentCard(config);
329
530
  const taskStore = new import_server.InMemoryTaskStore();
@@ -355,7 +556,7 @@ async function createA2AServer(config) {
355
556
  app.use("/a2a/jsonrpc", (0, import_express2.jsonRpcHandler)({ requestHandler, userBuilder: import_express2.UserBuilder.noAuthentication }));
356
557
  app.use("/a2a/rest", (0, import_express2.restHandler)({ requestHandler, userBuilder: import_express2.UserBuilder.noAuthentication }));
357
558
  const httpServer = app.listen(port, hostname, () => {
358
- log3.info("A2A server started", { bind: hostname, port, proto: advertiseProto });
559
+ log4.info("A2A server started", { bind: hostname, port, proto: advertiseProto });
359
560
  const banner = [
360
561
  "",
361
562
  "\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",
@@ -379,20 +580,20 @@ async function createA2AServer(config) {
379
580
  async shutdown() {
380
581
  httpServer.close();
381
582
  await executor.shutdown();
382
- log3.info("Server shut down");
583
+ sessionStore.shutdown();
584
+ log4.info("Server shut down");
383
585
  }
384
586
  };
385
587
  }
386
588
 
387
589
  // src/executors/opencode/client.ts
388
590
  var import_node_child_process = require("child_process");
389
- var log4 = logger.child("opencode:client");
591
+ var log5 = logger.child("opencode:client");
390
592
  var OpenCodeClient = class {
391
593
  constructor(config) {
392
594
  this.config = config;
393
595
  this.currentChild = null;
394
596
  }
395
- /** Abort the currently running child process (if any). */
396
597
  abort() {
397
598
  if (this.currentChild) {
398
599
  this.currentChild.kill("SIGTERM");
@@ -405,14 +606,32 @@ var OpenCodeClient = class {
405
606
  }
406
607
  }
407
608
  /**
408
- * Execute a prompt via the OpenCode CLI and return the response text.
609
+ * Execute a prompt via the OpenCode CLI.
610
+ *
611
+ * @param prompt - The user prompt text.
612
+ * @param sessionId - If provided, continues the session with `--session`.
613
+ * Otherwise creates a new session using `--format json` to capture the
614
+ * session ID.
615
+ * @param workingDirectory - Optional per-request working directory override.
616
+ * Falls back to config.workdir if not provided.
409
617
  */
410
- async execute(prompt) {
618
+ async execute(prompt, sessionId, workingDirectory) {
619
+ if (sessionId) {
620
+ const text = await this.runResume(prompt, sessionId, workingDirectory);
621
+ return { text, sessionId };
622
+ }
623
+ return this.runNew(prompt, workingDirectory);
624
+ }
625
+ resolveWorkdir(override) {
626
+ return override || this.config.workdir || process.cwd();
627
+ }
628
+ // ── New session (first turn) ──────────────────────────────────────────
629
+ async runNew(prompt, workingDirectory) {
411
630
  const cliPath = this.config.cliPath || "opencode";
412
631
  const timeout = this.config.timeout ?? 6e5;
413
632
  const MAX_PROMPT = 1e5;
414
633
  if (prompt.length > MAX_PROMPT) {
415
- throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages or use a provider with HTTP API transport.`);
634
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);
416
635
  }
417
636
  const env = {
418
637
  PATH: process.env.PATH ?? "",
@@ -424,7 +643,7 @@ var OpenCodeClient = class {
424
643
  for (const key of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENCODE_SERVER_PASSWORD", "OPENCODE_SERVER_USERNAME"]) {
425
644
  if (process.env[key]) env[key] = process.env[key];
426
645
  }
427
- const args = ["run", prompt];
646
+ const args = ["run", "--format", "json"];
428
647
  if (this.config.model) {
429
648
  args.push("--model", this.config.model);
430
649
  }
@@ -434,12 +653,12 @@ var OpenCodeClient = class {
434
653
  if (this.config.attachUrl) {
435
654
  args.push("--attach", this.config.attachUrl);
436
655
  }
437
- args.push("--format", "default");
438
- args.push("--dangerously-skip-permissions");
439
- log4.info("Spawning opencode", { cliPath, workdir: this.config.workdir, model: this.config.model });
656
+ args.push("--dangerously-skip-permissions", prompt);
657
+ const workdir = this.resolveWorkdir(workingDirectory);
658
+ log5.info("Spawning opencode (new session, json)", { cliPath, workdir });
440
659
  return new Promise((resolve2, reject) => {
441
660
  const child = (0, import_node_child_process.spawn)(cliPath, args, {
442
- cwd: this.config.workdir || process.cwd(),
661
+ cwd: workdir,
443
662
  env,
444
663
  stdio: ["ignore", "pipe", "pipe"]
445
664
  });
@@ -466,7 +685,92 @@ var OpenCodeClient = class {
466
685
  });
467
686
  child.stderr?.on("data", (chunk) => {
468
687
  stderr += chunk.toString();
469
- log4.debug("OpenCode stderr", { text: chunk.toString().trim() });
688
+ log5.debug("OpenCode stderr", { text: chunk.toString().trim() });
689
+ });
690
+ child.on("close", (code) => {
691
+ clearTimeout(timer);
692
+ this.currentChild = null;
693
+ if (code !== 0) {
694
+ const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
695
+ log5.warn("OpenCode non-zero exit", { code, stderr: errMsg });
696
+ reject(new Error(errMsg));
697
+ return;
698
+ }
699
+ try {
700
+ const { text, sessionId } = this.parseJsonEvents(stdout);
701
+ resolve2({ text, sessionId });
702
+ } catch (err) {
703
+ reject(new Error(`Failed to parse OpenCode JSON output: ${err.message}`));
704
+ }
705
+ });
706
+ child.on("error", (err) => {
707
+ clearTimeout(timer);
708
+ log5.error("OpenCode spawn failed", { error: err.message });
709
+ reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
710
+ });
711
+ });
712
+ }
713
+ // ── Resume session ────────────────────────────────────────────────────
714
+ async runResume(prompt, sessionId, workingDirectory) {
715
+ const cliPath = this.config.cliPath || "opencode";
716
+ const timeout = this.config.timeout ?? 6e5;
717
+ const MAX_PROMPT = 1e5;
718
+ if (prompt.length > MAX_PROMPT) {
719
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}).`);
720
+ }
721
+ const env = {
722
+ PATH: process.env.PATH ?? "",
723
+ HOME: process.env.HOME ?? ""
724
+ };
725
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
726
+ if (process.env[key]) env[key] = process.env[key];
727
+ }
728
+ for (const key of ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENCODE_SERVER_PASSWORD", "OPENCODE_SERVER_USERNAME"]) {
729
+ if (process.env[key]) env[key] = process.env[key];
730
+ }
731
+ const args = ["run", "--session", sessionId];
732
+ if (this.config.model) {
733
+ args.push("--model", this.config.model);
734
+ }
735
+ if (this.config.agent) {
736
+ args.push("--agent", this.config.agent);
737
+ }
738
+ if (this.config.attachUrl) {
739
+ args.push("--attach", this.config.attachUrl);
740
+ }
741
+ args.push("--format", "default", "--dangerously-skip-permissions", prompt);
742
+ const workdir = this.resolveWorkdir(workingDirectory);
743
+ log5.info("Spawning opencode (resume)", { sessionId, workdir });
744
+ return new Promise((resolve2, reject) => {
745
+ const child = (0, import_node_child_process.spawn)(cliPath, args, {
746
+ cwd: workdir,
747
+ env,
748
+ stdio: ["ignore", "pipe", "pipe"]
749
+ });
750
+ this.currentChild = child;
751
+ let stdout = "";
752
+ let stderr = "";
753
+ const MAX_OUTPUT = 1e7;
754
+ const timer = setTimeout(() => {
755
+ child.kill("SIGTERM");
756
+ const forceTimer = setTimeout(() => {
757
+ if (child.exitCode === null) child.kill("SIGKILL");
758
+ }, 5e3);
759
+ child.on("close", () => clearTimeout(forceTimer));
760
+ reject(new Error(`OpenCode execution timed out after ${timeout}ms`));
761
+ }, timeout);
762
+ child.stdout?.on("data", (chunk) => {
763
+ stdout += chunk.toString();
764
+ if (stdout.length > MAX_OUTPUT) {
765
+ child.kill("SIGTERM");
766
+ setTimeout(() => {
767
+ if (child.exitCode === null) child.kill("SIGKILL");
768
+ }, 5e3);
769
+ }
770
+ });
771
+ child.stderr?.on("data", (chunk) => {
772
+ stderr += chunk.toString();
773
+ log5.debug("OpenCode stderr", { text: chunk.toString().trim() });
470
774
  });
471
775
  child.on("close", (code) => {
472
776
  clearTimeout(timer);
@@ -475,21 +779,65 @@ var OpenCodeClient = class {
475
779
  resolve2(stdout.trim());
476
780
  } else {
477
781
  const errMsg = stderr.trim() || stdout.trim() || `OpenCode exited with code ${code}`;
478
- log4.warn("OpenCode non-zero exit", { code, stderr: errMsg });
782
+ log5.warn("OpenCode non-zero exit", { code, stderr: errMsg });
479
783
  reject(new Error(errMsg));
480
784
  }
481
785
  });
482
786
  child.on("error", (err) => {
483
787
  clearTimeout(timer);
484
- log4.error("OpenCode spawn failed", { error: err.message });
788
+ log5.error("OpenCode spawn failed", { error: err.message });
485
789
  reject(new Error(`Failed to start opencode: ${err.message}. Is it installed?`));
486
790
  });
487
791
  });
488
792
  }
793
+ // ── Parser ────────────────────────────────────────────────────────────
794
+ /**
795
+ * Parse newline-delimited JSON events from OpenCode's `--format json` output.
796
+ * Extracts the sessionID from the first event that contains it, and
797
+ * accumulates text content from relevant events.
798
+ */
799
+ parseJsonEvents(stdout) {
800
+ const lines = stdout.split("\n").filter((l) => l.trim());
801
+ let sessionId = "";
802
+ const textParts = [];
803
+ for (const line of lines) {
804
+ let event;
805
+ try {
806
+ event = JSON.parse(line);
807
+ } catch {
808
+ continue;
809
+ }
810
+ if (!sessionId && event.sessionID) {
811
+ sessionId = event.sessionID;
812
+ }
813
+ if (event.type === "error") continue;
814
+ const text = event.text || event.content || event.message;
815
+ if (text) {
816
+ textParts.push(text);
817
+ }
818
+ }
819
+ if (!sessionId) {
820
+ return {
821
+ text: textParts.join("\n").trim() || stdout.trim(),
822
+ sessionId: ""
823
+ };
824
+ }
825
+ return {
826
+ text: textParts.join("\n").trim() || "No response from OpenCode.",
827
+ sessionId
828
+ };
829
+ }
489
830
  };
490
831
 
491
832
  // src/executors/events.ts
492
833
  var import_uuid2 = require("uuid");
834
+ function extractText(message) {
835
+ return message.parts.filter((p) => {
836
+ const part = p;
837
+ const text = part.text;
838
+ return text !== void 0 && text !== null;
839
+ }).map((p) => p.text).join("\n");
840
+ }
493
841
  function publishTask(bus, taskId, contextId) {
494
842
  bus.publish({
495
843
  kind: "task",
@@ -526,12 +874,13 @@ function publishFinalArtifact(bus, taskId, contextId, text) {
526
874
  }
527
875
 
528
876
  // src/executors/opencode/executor.ts
529
- var log5 = logger.child("opencode:executor");
877
+ var log6 = logger.child("opencode:executor");
530
878
  var OpenCodeExecutor = class {
531
- constructor(config) {
879
+ constructor(config, sessionStore) {
532
880
  this.client = null;
533
881
  this.initialized = false;
534
882
  this.config = config;
883
+ this.sessionStore = sessionStore;
535
884
  }
536
885
  async initialize() {
537
886
  if (this.initialized) return;
@@ -545,58 +894,77 @@ var OpenCodeExecutor = class {
545
894
  timeout: this.config.timeouts.prompt ?? 6e5
546
895
  });
547
896
  this.initialized = true;
548
- log5.info("Executor initialized", { workdir: oc.projectDirectory, model: oc.model });
897
+ log6.info("Executor initialized", { workdir: oc.projectDirectory, model: oc.model });
549
898
  }
550
899
  async shutdown() {
551
900
  this.client = null;
552
901
  this.initialized = false;
553
- log5.info("Executor shut down");
902
+ log6.info("Executor shut down");
554
903
  }
555
904
  async execute(ctx, bus) {
556
- const { taskId, contextId, userMessage, task } = ctx;
905
+ const { taskId, userMessage } = ctx;
557
906
  await this.initialize();
907
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
908
+ let contextId = ctx.contextId;
909
+ if (!contextId) {
910
+ contextId = generateContextId();
911
+ log6.info("Generated new contextId", { contextId, taskId });
912
+ }
913
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "opencode") : null;
914
+ const sessionId = binding?.sessionId;
558
915
  try {
559
- if (!task) {
916
+ const existingTask = ctx.task;
917
+ if (!existingTask) {
560
918
  publishTask(bus, taskId, contextId);
561
919
  publishStatus(bus, taskId, contextId, "submitted");
562
920
  }
563
- publishStatus(bus, taskId, contextId, "working", "Processing request...");
564
- const promptText = this.extractText(userMessage);
565
- log5.info("Sending prompt to OpenCode", { taskId, len: promptText.length });
566
- const response = await this.client.execute(promptText);
567
- const finalText = response || "No response from OpenCode.";
568
- publishFinalArtifact(bus, taskId, contextId, finalText);
921
+ publishStatus(bus, taskId, contextId, "working");
922
+ const promptText = extractText(userMessage);
923
+ const userMsg = userMessage;
924
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
925
+ log6.info("Sending prompt to OpenCode", {
926
+ taskId,
927
+ contextId,
928
+ sessionId: sessionId || "(new)",
929
+ len: promptText.length,
930
+ workingDirectory: workingDirectory || "(default)"
931
+ });
932
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
933
+ this.sessionStore.set({
934
+ provider: "opencode",
935
+ contextId,
936
+ sessionId: result.sessionId,
937
+ activeTaskId: void 0,
938
+ activeTaskState: void 0,
939
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
940
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
941
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
942
+ });
943
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from OpenCode.");
569
944
  publishStatus(bus, taskId, contextId, "completed", void 0, true);
570
945
  bus.finished();
571
- log5.info("Task completed", { taskId, len: finalText.length });
946
+ log6.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
572
947
  } catch (error) {
573
948
  const msg = error.message ?? String(error);
574
- log5.error("Execution failed", { taskId, error: msg });
949
+ log6.error("Execution failed", { taskId, contextId, error: msg });
575
950
  publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
576
951
  bus.finished();
577
952
  }
578
953
  }
579
954
  async cancelTask(taskId, bus) {
580
- log5.info("Cancel requested for OpenCode task", { taskId });
955
+ log6.info("Cancel requested for OpenCode task", { taskId });
581
956
  this.client?.abort();
582
957
  publishStatus(bus, taskId, "", "canceled", "OpenCode task cancelled", true);
583
958
  bus.finished();
584
959
  }
585
- extractText(message) {
586
- return message.parts.filter((p) => {
587
- const part = p;
588
- const text = part.text;
589
- return text !== void 0 && text !== null;
590
- }).map((p) => p.text).join("\n");
591
- }
592
960
  };
593
- function createOpenCodeExecutor(config) {
594
- return new OpenCodeExecutor(config);
961
+ function createOpenCodeExecutor(config, sessionStore) {
962
+ return new OpenCodeExecutor(config, sessionStore);
595
963
  }
596
964
 
597
965
  // src/executors/codex/client.ts
598
966
  var import_node_child_process2 = require("child_process");
599
- var log6 = logger.child("codex:client");
967
+ var log7 = logger.child("codex:client");
600
968
  var CodexClient = class {
601
969
  constructor(config) {
602
970
  this.config = config;
@@ -614,9 +982,24 @@ var CodexClient = class {
614
982
  }
615
983
  }
616
984
  /**
617
- * Execute a prompt via the Codex CLI and return the response text.
985
+ * Execute a prompt via the Codex CLI.
986
+ *
987
+ * @param prompt - The user prompt text.
988
+ * @param sessionId - If provided, resumes an existing session.
989
+ * Otherwise creates a new session and extracts the thread_id from JSONL.
990
+ * @param workingDirectory - Optional per-request working directory override.
618
991
  */
619
- async execute(prompt) {
992
+ async execute(prompt, sessionId, workingDirectory) {
993
+ if (sessionId) {
994
+ return this.executeResume(prompt, sessionId, workingDirectory);
995
+ }
996
+ return this.executeNew(prompt, workingDirectory);
997
+ }
998
+ resolveWorkdir(override) {
999
+ return override || this.config.workdir || process.cwd();
1000
+ }
1001
+ // ── New session (first turn) ──────────────────────────────────────────
1002
+ async executeNew(prompt, workingDirectory) {
620
1003
  const cliPath = this.config.cliPath || "codex";
621
1004
  const timeout = this.config.timeout ?? 3e5;
622
1005
  const MAX_PROMPT = 1e5;
@@ -633,15 +1016,20 @@ var CodexClient = class {
633
1016
  if (this.config.apiKey) {
634
1017
  env["OPENAI_API_KEY"] = this.config.apiKey;
635
1018
  }
636
- const args = [];
1019
+ const args = ["exec", "--json"];
637
1020
  if (this.config.model) {
638
1021
  args.push("--model", this.config.model);
639
1022
  }
640
- args.push(prompt);
641
- log6.info("Spawning codex", { cliPath, workdir: this.config.workdir, model: this.config.model });
1023
+ args.push(
1024
+ "--dangerously-bypass-approvals-and-sandbox",
1025
+ "--skip-git-repo-check",
1026
+ prompt
1027
+ );
1028
+ const workdir = this.resolveWorkdir(workingDirectory);
1029
+ log7.info("Spawning codex (new session)", { cliPath, workdir, model: this.config.model });
642
1030
  return new Promise((resolve2, reject) => {
643
1031
  const child = (0, import_node_child_process2.spawn)(cliPath, args, {
644
- cwd: this.config.workdir || process.cwd(),
1032
+ cwd: workdir,
645
1033
  env,
646
1034
  stdio: ["ignore", "pipe", "pipe"]
647
1035
  });
@@ -668,35 +1056,192 @@ var CodexClient = class {
668
1056
  });
669
1057
  child.stderr?.on("data", (chunk) => {
670
1058
  stderr += chunk.toString();
671
- log6.debug("Codex stderr", { text: chunk.toString().trim() });
1059
+ log7.debug("Codex stderr", { text: chunk.toString().trim() });
672
1060
  });
673
1061
  child.on("close", (code) => {
674
1062
  clearTimeout(timer);
675
1063
  this.currentChild = null;
676
- if (code === 0) {
677
- resolve2(stdout.trim());
678
- } else {
1064
+ if (code !== 0) {
1065
+ const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
1066
+ log7.warn("Codex non-zero exit", { code, stderr: errMsg });
1067
+ reject(new Error(errMsg));
1068
+ return;
1069
+ }
1070
+ try {
1071
+ const { text, sessionId } = this.parseJsonl(stdout);
1072
+ resolve2({ text, sessionId });
1073
+ } catch (err) {
1074
+ reject(new Error(`Failed to parse Codex JSONL output: ${err.message}`));
1075
+ }
1076
+ });
1077
+ child.on("error", (err) => {
1078
+ clearTimeout(timer);
1079
+ log7.error("Codex spawn failed", { error: err.message });
1080
+ reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
1081
+ });
1082
+ });
1083
+ }
1084
+ // ── Resume session (subsequent turns) ─────────────────────────────────
1085
+ async executeResume(prompt, sessionId, workingDirectory) {
1086
+ const cliPath = this.config.cliPath || "codex";
1087
+ const timeout = this.config.timeout ?? 3e5;
1088
+ const MAX_PROMPT = 1e5;
1089
+ if (prompt.length > MAX_PROMPT) {
1090
+ throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
1091
+ }
1092
+ const env = {
1093
+ PATH: process.env.PATH ?? "",
1094
+ HOME: process.env.HOME ?? ""
1095
+ };
1096
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"]) {
1097
+ if (process.env[key]) env[key] = process.env[key];
1098
+ }
1099
+ if (this.config.apiKey) {
1100
+ env["OPENAI_API_KEY"] = this.config.apiKey;
1101
+ }
1102
+ const args = ["exec", "resume", sessionId];
1103
+ if (this.config.model) {
1104
+ args.push("--model", this.config.model);
1105
+ }
1106
+ args.push(
1107
+ "--dangerously-bypass-approvals-and-sandbox",
1108
+ "--skip-git-repo-check",
1109
+ prompt
1110
+ );
1111
+ const workdir = this.resolveWorkdir(workingDirectory);
1112
+ log7.info("Spawning codex (resume)", { sessionId, workdir });
1113
+ return new Promise((resolve2, reject) => {
1114
+ const child = (0, import_node_child_process2.spawn)(cliPath, args, {
1115
+ cwd: workdir,
1116
+ env,
1117
+ stdio: ["ignore", "pipe", "pipe"]
1118
+ });
1119
+ this.currentChild = child;
1120
+ let stdout = "";
1121
+ let stderr = "";
1122
+ const MAX_OUTPUT = 1e7;
1123
+ const timer = setTimeout(() => {
1124
+ child.kill("SIGTERM");
1125
+ const forceTimer = setTimeout(() => {
1126
+ if (child.exitCode === null) child.kill("SIGKILL");
1127
+ }, 5e3);
1128
+ child.on("close", () => clearTimeout(forceTimer));
1129
+ reject(new Error(`Codex execution timed out after ${timeout}ms`));
1130
+ }, timeout);
1131
+ child.stdout?.on("data", (chunk) => {
1132
+ stdout += chunk.toString();
1133
+ if (stdout.length > MAX_OUTPUT) {
1134
+ child.kill("SIGTERM");
1135
+ setTimeout(() => {
1136
+ if (child.exitCode === null) child.kill("SIGKILL");
1137
+ }, 5e3);
1138
+ }
1139
+ });
1140
+ child.stderr?.on("data", (chunk) => {
1141
+ stderr += chunk.toString();
1142
+ log7.debug("Codex stderr", { text: chunk.toString().trim() });
1143
+ });
1144
+ child.on("close", (code) => {
1145
+ clearTimeout(timer);
1146
+ this.currentChild = null;
1147
+ if (code !== 0) {
679
1148
  const errMsg = stderr.trim() || stdout.trim() || `Codex exited with code ${code}`;
680
- log6.warn("Codex non-zero exit", { code, stderr: errMsg });
1149
+ log7.warn("Codex non-zero exit", { code, stderr: errMsg });
681
1150
  reject(new Error(errMsg));
1151
+ return;
682
1152
  }
1153
+ const text = this.extractResumeText(stdout);
1154
+ resolve2({ text, sessionId });
683
1155
  });
684
1156
  child.on("error", (err) => {
685
1157
  clearTimeout(timer);
686
- log6.error("Codex spawn failed", { error: err.message });
1158
+ log7.error("Codex spawn failed", { error: err.message });
687
1159
  reject(new Error(`Failed to start codex: ${err.message}. Is it installed? (npm i -g @openai/codex)`));
688
1160
  });
689
1161
  });
690
1162
  }
1163
+ // ── Parsers ───────────────────────────────────────────────────────────
1164
+ /**
1165
+ * Parse JSONL output to extract session thread_id and agent response text.
1166
+ */
1167
+ parseJsonl(stdout) {
1168
+ const lines = stdout.split("\n").filter((l) => l.trim());
1169
+ let sessionId = "";
1170
+ const textParts = [];
1171
+ for (const line of lines) {
1172
+ let event;
1173
+ try {
1174
+ event = JSON.parse(line);
1175
+ } catch {
1176
+ continue;
1177
+ }
1178
+ if (event.type === "thread.started" && event.thread_id) {
1179
+ sessionId = event.thread_id;
1180
+ }
1181
+ if (event.type === "item.completed") {
1182
+ const item = event.item;
1183
+ if (item && item.type === "agent_message" && item.text) {
1184
+ textParts.push(item.text);
1185
+ }
1186
+ }
1187
+ }
1188
+ if (!sessionId) {
1189
+ throw new Error("No thread_id found in Codex JSONL output");
1190
+ }
1191
+ return {
1192
+ text: textParts.join("\n").trim() || "No response from Codex.",
1193
+ sessionId
1194
+ };
1195
+ }
1196
+ /**
1197
+ * Extract the agent response text from the resume mode output.
1198
+ *
1199
+ * The resume output includes a CLI header followed by the conversation.
1200
+ * We extract everything after the last "codex" marker line as the
1201
+ * latest agent response.
1202
+ */
1203
+ extractResumeText(stdout) {
1204
+ const lines = stdout.split("\n");
1205
+ let responseStart = -1;
1206
+ for (let i = 0; i < lines.length; i++) {
1207
+ if (lines[i].trim() === "codex") {
1208
+ responseStart = i;
1209
+ }
1210
+ }
1211
+ if (responseStart >= 0 && responseStart + 1 < lines.length) {
1212
+ return lines.slice(responseStart + 1).join("\n").trim();
1213
+ }
1214
+ let skipHeader = true;
1215
+ const resultLines = [];
1216
+ for (const line of lines) {
1217
+ const trimmed = line.trim();
1218
+ if (skipHeader) {
1219
+ if (trimmed === "--------" || trimmed === "codex") {
1220
+ skipHeader = false;
1221
+ }
1222
+ continue;
1223
+ }
1224
+ if (trimmed === "user" || trimmed === "codex") {
1225
+ continue;
1226
+ }
1227
+ if (trimmed.startsWith("tokens used")) {
1228
+ if (resultLines.length > 0) resultLines.pop();
1229
+ break;
1230
+ }
1231
+ resultLines.push(line);
1232
+ }
1233
+ return resultLines.join("\n").trim() || stdout.trim();
1234
+ }
691
1235
  };
692
1236
 
693
1237
  // src/executors/codex/executor.ts
694
- var log7 = logger.child("codex:executor");
1238
+ var log8 = logger.child("codex:executor");
695
1239
  var CodexExecutor = class {
696
- constructor(config) {
1240
+ constructor(config, sessionStore) {
697
1241
  this.client = null;
698
1242
  this.initialized = false;
699
1243
  this.config = config;
1244
+ this.sessionStore = sessionStore;
700
1245
  }
701
1246
  async initialize() {
702
1247
  if (this.initialized) return;
@@ -709,59 +1254,78 @@ var CodexExecutor = class {
709
1254
  timeout: this.config.timeouts.prompt ?? 6e5
710
1255
  });
711
1256
  this.initialized = true;
712
- log7.info("Executor initialized", { cliPath: cx.cliPath, workdir: cx.workdir });
1257
+ log8.info("Executor initialized", { cliPath: cx.cliPath, workdir: cx.workdir });
713
1258
  }
714
1259
  async shutdown() {
715
1260
  this.client = null;
716
1261
  this.initialized = false;
717
- log7.info("Executor shut down");
1262
+ log8.info("Executor shut down");
718
1263
  }
719
1264
  async execute(ctx, bus) {
720
- const { taskId, contextId, userMessage, task } = ctx;
1265
+ const { taskId, userMessage } = ctx;
721
1266
  await this.initialize();
1267
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
1268
+ let contextId = ctx.contextId;
1269
+ if (!contextId) {
1270
+ contextId = generateContextId();
1271
+ log8.info("Generated new contextId", { contextId, taskId });
1272
+ }
1273
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "codex") : null;
1274
+ const sessionId = binding?.sessionId;
722
1275
  try {
723
- if (!task) {
1276
+ const existingTask = ctx.task;
1277
+ if (!existingTask) {
724
1278
  publishTask(bus, taskId, contextId);
725
1279
  publishStatus(bus, taskId, contextId, "submitted");
726
1280
  }
727
- publishStatus(bus, taskId, contextId, "working", "Processing request...");
728
- const promptText = this.extractText(userMessage);
729
- log7.info("Sending prompt to Codex", { taskId, len: promptText.length });
730
- const response = await this.client.execute(promptText);
731
- const finalText = response || "No response from Codex.";
732
- publishFinalArtifact(bus, taskId, contextId, finalText);
1281
+ publishStatus(bus, taskId, contextId, "working");
1282
+ const promptText = extractText(userMessage);
1283
+ const userMsg = userMessage;
1284
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
1285
+ log8.info("Sending prompt to Codex", {
1286
+ taskId,
1287
+ contextId,
1288
+ sessionId: sessionId || "(new)",
1289
+ len: promptText.length,
1290
+ workingDirectory: workingDirectory || "(default)"
1291
+ });
1292
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
1293
+ this.sessionStore.set({
1294
+ provider: "codex",
1295
+ contextId,
1296
+ sessionId: result.sessionId,
1297
+ activeTaskId: void 0,
1298
+ activeTaskState: void 0,
1299
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1300
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1301
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
1302
+ });
1303
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Codex.");
733
1304
  publishStatus(bus, taskId, contextId, "completed", void 0, true);
734
1305
  bus.finished();
735
- log7.info("Task completed", { taskId, len: finalText.length });
1306
+ log8.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
736
1307
  } catch (error) {
737
1308
  const msg = error.message ?? String(error);
738
- log7.error("Execution failed", { taskId, error: msg });
1309
+ log8.error("Execution failed", { taskId, contextId, error: msg });
739
1310
  publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
740
1311
  bus.finished();
741
1312
  }
742
1313
  }
743
1314
  async cancelTask(taskId, bus) {
744
- log7.info("Cancel requested for Codex task", { taskId });
1315
+ log8.info("Cancel requested for Codex task", { taskId });
745
1316
  this.client?.abort();
746
1317
  publishStatus(bus, taskId, "", "canceled", "Codex task cancelled", true);
747
1318
  bus.finished();
748
1319
  }
749
- // ── Helpers ─────────────────────────────────────────────────────────────
750
- extractText(message) {
751
- return message.parts.filter((p) => {
752
- const part = p;
753
- const text = part.text;
754
- return text !== void 0 && text !== null;
755
- }).map((p) => p.text).join("\n");
756
- }
757
1320
  };
758
- function createCodexExecutor(config) {
759
- return new CodexExecutor(config);
1321
+ function createCodexExecutor(config, sessionStore) {
1322
+ return new CodexExecutor(config, sessionStore);
760
1323
  }
761
1324
 
762
1325
  // src/executors/claude/client.ts
763
1326
  var import_node_child_process3 = require("child_process");
764
- var log8 = logger.child("claude:client");
1327
+ var import_uuid3 = require("uuid");
1328
+ var log9 = logger.child("claude:client");
765
1329
  var ClaudeClient = class {
766
1330
  constructor(config) {
767
1331
  this.config = config;
@@ -779,14 +1343,31 @@ var ClaudeClient = class {
779
1343
  }
780
1344
  }
781
1345
  /**
782
- * Execute a prompt via the Claude Code CLI and return the response text.
1346
+ * Execute a prompt via the Claude Code CLI.
1347
+ *
1348
+ * @param prompt - The user prompt text.
1349
+ * @param sessionId - If provided, resumes the session with `--resume`.
1350
+ * Otherwise creates a new session with `--session-id <new-uuid>`.
1351
+ * @param workingDirectory - Optional per-request working directory override.
1352
+ * @returns The response text and the session ID (new or existing).
783
1353
  */
784
- async execute(prompt) {
1354
+ async execute(prompt, sessionId, workingDirectory) {
1355
+ if (sessionId) {
1356
+ const text2 = await this.run(["-p", "--resume", sessionId, prompt], workingDirectory);
1357
+ return { text: text2, sessionId };
1358
+ }
1359
+ const newSessionId = (0, import_uuid3.v4)();
1360
+ const text = await this.run(["-p", "--session-id", newSessionId, prompt], workingDirectory);
1361
+ return { text, sessionId: newSessionId };
1362
+ }
1363
+ // ── Internal ──────────────────────────────────────────────────────────
1364
+ async run(args, workingDirectory) {
785
1365
  const cliPath = this.config.cliPath || "claude";
786
1366
  const timeout = this.config.timeout ?? 3e5;
787
1367
  const MAX_PROMPT = 1e5;
788
- if (prompt.length > MAX_PROMPT) {
789
- throw new Error(`Prompt too large (${prompt.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
1368
+ const promptArg = args[args.length - 1];
1369
+ if (promptArg && promptArg.length > MAX_PROMPT) {
1370
+ throw new Error(`Prompt too large (${promptArg.length} chars, max ${MAX_PROMPT}). Split into smaller messages.`);
790
1371
  }
791
1372
  const env = {
792
1373
  PATH: process.env.PATH ?? "",
@@ -798,15 +1379,19 @@ var ClaudeClient = class {
798
1379
  if (this.config.apiKey) {
799
1380
  env["ANTHROPIC_API_KEY"] = this.config.apiKey;
800
1381
  }
801
- const args = ["-p"];
1382
+ const fullArgs = [];
1383
+ for (const arg of args.slice(0, -1)) {
1384
+ fullArgs.push(arg);
1385
+ }
802
1386
  if (this.config.model) {
803
- args.push("--model", this.config.model);
1387
+ fullArgs.push("--model", this.config.model);
804
1388
  }
805
- args.push(prompt);
806
- log8.info("Spawning claude", { cliPath, workdir: this.config.workdir, model: this.config.model });
1389
+ fullArgs.push(promptArg);
1390
+ const workdir = workingDirectory || this.config.workdir || process.cwd();
1391
+ log9.info("Spawning claude", { cliPath, workdir, model: this.config.model });
807
1392
  return new Promise((resolve2, reject) => {
808
- const child = (0, import_node_child_process3.spawn)(cliPath, args, {
809
- cwd: this.config.workdir || process.cwd(),
1393
+ const child = (0, import_node_child_process3.spawn)(cliPath, fullArgs, {
1394
+ cwd: workdir,
810
1395
  env,
811
1396
  stdio: ["ignore", "pipe", "pipe"]
812
1397
  });
@@ -833,7 +1418,7 @@ var ClaudeClient = class {
833
1418
  });
834
1419
  child.stderr?.on("data", (chunk) => {
835
1420
  stderr += chunk.toString();
836
- log8.debug("Claude stderr", { text: chunk.toString().trim() });
1421
+ log9.debug("Claude stderr", { text: chunk.toString().trim() });
837
1422
  });
838
1423
  child.on("close", (code) => {
839
1424
  clearTimeout(timer);
@@ -842,13 +1427,13 @@ var ClaudeClient = class {
842
1427
  resolve2(stdout.trim());
843
1428
  } else {
844
1429
  const errMsg = stderr.trim() || stdout.trim() || `Claude Code exited with code ${code}`;
845
- log8.warn("Claude non-zero exit", { code, stderr: errMsg });
1430
+ log9.warn("Claude non-zero exit", { code, stderr: errMsg });
846
1431
  reject(new Error(errMsg));
847
1432
  }
848
1433
  });
849
1434
  child.on("error", (err) => {
850
1435
  clearTimeout(timer);
851
- log8.error("Claude spawn failed", { error: err.message });
1436
+ log9.error("Claude spawn failed", { error: err.message });
852
1437
  reject(new Error(`Failed to start claude: ${err.message}. Install: npm i -g @anthropic-ai/claude-code`));
853
1438
  });
854
1439
  });
@@ -856,12 +1441,13 @@ var ClaudeClient = class {
856
1441
  };
857
1442
 
858
1443
  // src/executors/claude/executor.ts
859
- var log9 = logger.child("claude:executor");
1444
+ var log10 = logger.child("claude:executor");
860
1445
  var ClaudeExecutor = class {
861
- constructor(config) {
1446
+ constructor(config, sessionStore) {
862
1447
  this.client = null;
863
1448
  this.initialized = false;
864
1449
  this.config = config;
1450
+ this.sessionStore = sessionStore;
865
1451
  }
866
1452
  async initialize() {
867
1453
  if (this.initialized) return;
@@ -874,53 +1460,72 @@ var ClaudeExecutor = class {
874
1460
  timeout: this.config.timeouts.prompt ?? 6e5
875
1461
  });
876
1462
  this.initialized = true;
877
- log9.info("Executor initialized", { cliPath: cc.cliPath, workdir: cc.workdir });
1463
+ log10.info("Executor initialized", { cliPath: cc.cliPath, workdir: cc.workdir });
878
1464
  }
879
1465
  async shutdown() {
880
1466
  this.client = null;
881
1467
  this.initialized = false;
882
- log9.info("Executor shut down");
1468
+ log10.info("Executor shut down");
883
1469
  }
884
1470
  async execute(ctx, bus) {
885
- const { taskId, contextId, userMessage, task } = ctx;
1471
+ const { taskId, userMessage } = ctx;
886
1472
  await this.initialize();
1473
+ const reuseByContext = this.config.session?.reuseByContext ?? true;
1474
+ let contextId = ctx.contextId;
1475
+ if (!contextId) {
1476
+ contextId = generateContextId();
1477
+ log10.info("Generated new contextId", { contextId, taskId });
1478
+ }
1479
+ const binding = reuseByContext ? this.sessionStore.get(contextId, "claude") : null;
1480
+ const sessionId = binding?.sessionId;
887
1481
  try {
888
- if (!task) {
1482
+ const existingTask = ctx.task;
1483
+ if (!existingTask) {
889
1484
  publishTask(bus, taskId, contextId);
890
1485
  publishStatus(bus, taskId, contextId, "submitted");
891
1486
  }
892
- publishStatus(bus, taskId, contextId, "working", "Processing request...");
893
- const promptText = this.extractText(userMessage);
894
- log9.info("Sending prompt to Claude Code", { taskId, len: promptText.length });
895
- const response = await this.client.execute(promptText);
896
- const finalText = response || "No response from Claude Code.";
897
- publishFinalArtifact(bus, taskId, contextId, finalText);
1487
+ publishStatus(bus, taskId, contextId, "working");
1488
+ const promptText = extractText(userMessage);
1489
+ const userMsg = userMessage;
1490
+ const workingDirectory = userMsg.metadata && typeof userMsg.metadata === "object" ? userMsg.metadata.workingDirectory : void 0;
1491
+ log10.info("Sending prompt to Claude Code", {
1492
+ taskId,
1493
+ contextId,
1494
+ sessionId: sessionId || "(new)",
1495
+ len: promptText.length,
1496
+ workingDirectory: workingDirectory || "(default)"
1497
+ });
1498
+ const result = await this.client.execute(promptText, sessionId, workingDirectory);
1499
+ this.sessionStore.set({
1500
+ provider: "claude",
1501
+ contextId,
1502
+ sessionId: result.sessionId,
1503
+ activeTaskId: void 0,
1504
+ activeTaskState: void 0,
1505
+ createdAt: binding?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1506
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1507
+ expiresAt: new Date(Date.now() + (this.config.session?.ttl ?? 24 * 60 * 60 * 1e3)).toISOString()
1508
+ });
1509
+ publishFinalArtifact(bus, taskId, contextId, result.text || "No response from Claude Code.");
898
1510
  publishStatus(bus, taskId, contextId, "completed", void 0, true);
899
1511
  bus.finished();
900
- log9.info("Task completed", { taskId, len: finalText.length });
1512
+ log10.info("Task completed", { taskId, contextId, sessionId: result.sessionId });
901
1513
  } catch (error) {
902
1514
  const msg = error.message ?? String(error);
903
- log9.error("Execution failed", { taskId, error: msg });
1515
+ log10.error("Execution failed", { taskId, contextId, error: msg });
904
1516
  publishStatus(bus, taskId, contextId, "failed", `Error: ${msg}`, true);
905
1517
  bus.finished();
906
1518
  }
907
1519
  }
908
1520
  async cancelTask(taskId, bus) {
909
- log9.info("Cancel requested for Claude Code task", { taskId });
1521
+ log10.info("Cancel requested for Claude Code task", { taskId });
910
1522
  this.client?.abort();
911
1523
  publishStatus(bus, taskId, "", "canceled", "Claude Code task cancelled", true);
912
1524
  bus.finished();
913
1525
  }
914
- extractText(message) {
915
- return message.parts.filter((p) => {
916
- const part = p;
917
- const text = part.text;
918
- return text !== void 0 && text !== null;
919
- }).map((p) => p.text).join("\n");
920
- }
921
1526
  };
922
- function createClaudeExecutor(config) {
923
- return new ClaudeExecutor(config);
1527
+ function createClaudeExecutor(config, sessionStore) {
1528
+ return new ClaudeExecutor(config, sessionStore);
924
1529
  }
925
1530
  // Annotate the CommonJS export names for ESM import in node:
926
1531
  0 && (module.exports = {
@@ -928,15 +1533,18 @@ function createClaudeExecutor(config) {
928
1533
  CodexExecutor,
929
1534
  LogLevel,
930
1535
  OpenCodeExecutor,
1536
+ SessionBindingStore,
931
1537
  createA2AServer,
932
1538
  createClaudeExecutor,
933
1539
  createCodexExecutor,
934
1540
  createExecutor,
935
1541
  createOpenCodeExecutor,
1542
+ generateContextId,
936
1543
  getExecutor,
937
1544
  loadConfigFile,
938
1545
  logger,
939
1546
  registerExecutor,
940
- resolveConfig
1547
+ resolveConfig,
1548
+ resolveSessionStorePath
941
1549
  });
942
1550
  //# sourceMappingURL=index.js.map