@openacp/cli 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,14 @@ import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
55
55
  var log = createChildLogger({ module: "agent-instance" });
56
56
  function resolveAgentCommand(cmd) {
57
57
  const packageDirs = [
58
- path.resolve(process.cwd(), "node_modules", "@zed-industries", cmd, "dist", "index.js"),
58
+ path.resolve(
59
+ process.cwd(),
60
+ "node_modules",
61
+ "@zed-industries",
62
+ cmd,
63
+ "dist",
64
+ "index.js"
65
+ ),
59
66
  path.resolve(process.cwd(), "node_modules", cmd, "dist", "index.js")
60
67
  ];
61
68
  for (const jsPath of packageDirs) {
@@ -103,19 +110,33 @@ var AgentInstance = class _AgentInstance {
103
110
  constructor(agentName) {
104
111
  this.agentName = agentName;
105
112
  }
106
- static async spawn(agentDef, workingDirectory) {
113
+ static async spawnSubprocess(agentDef, workingDirectory) {
107
114
  const instance = new _AgentInstance(agentDef.name);
108
115
  const resolved = resolveAgentCommand(agentDef.command);
109
- log.debug({ agentName: agentDef.name, command: resolved.command, args: resolved.args }, "Spawning agent");
110
- const spawnStart = Date.now();
111
- instance.child = spawn(resolved.command, [...resolved.args, ...agentDef.args], {
112
- stdio: ["pipe", "pipe", "pipe"],
113
- cwd: workingDirectory,
114
- env: { ...process.env, ...agentDef.env }
115
- });
116
+ log.debug(
117
+ {
118
+ agentName: agentDef.name,
119
+ command: resolved.command,
120
+ args: resolved.args
121
+ },
122
+ "Resolved agent command"
123
+ );
124
+ instance.child = spawn(
125
+ resolved.command,
126
+ [...resolved.args, ...agentDef.args],
127
+ {
128
+ stdio: ["pipe", "pipe", "pipe"],
129
+ cwd: workingDirectory,
130
+ env: { ...process.env, ...agentDef.env }
131
+ }
132
+ );
116
133
  await new Promise((resolve, reject) => {
117
134
  instance.child.on("error", (err) => {
118
- reject(new Error(`Failed to spawn agent "${agentDef.name}": ${err.message}. Is "${agentDef.command}" installed?`));
135
+ reject(
136
+ new Error(
137
+ `Failed to spawn agent "${agentDef.name}": ${err.message}. Is "${agentDef.command}" installed?`
138
+ )
139
+ );
119
140
  });
120
141
  instance.child.on("spawn", () => resolve());
121
142
  });
@@ -125,14 +146,20 @@ var AgentInstance = class _AgentInstance {
125
146
  });
126
147
  const stdinLogger = new Transform({
127
148
  transform(chunk, _enc, cb) {
128
- log.debug({ direction: "send", raw: chunk.toString().trimEnd() }, "ACP raw");
149
+ log.debug(
150
+ { direction: "send", raw: chunk.toString().trimEnd() },
151
+ "ACP raw"
152
+ );
129
153
  cb(null, chunk);
130
154
  }
131
155
  });
132
156
  stdinLogger.pipe(instance.child.stdin);
133
157
  const stdoutLogger = new Transform({
134
158
  transform(chunk, _enc, cb) {
135
- log.debug({ direction: "recv", raw: chunk.toString().trimEnd() }, "ACP raw");
159
+ log.debug(
160
+ { direction: "recv", raw: chunk.toString().trimEnd() },
161
+ "ACP raw"
162
+ );
136
163
  cb(null, chunk);
137
164
  }
138
165
  });
@@ -151,26 +178,82 @@ var AgentInstance = class _AgentInstance {
151
178
  terminal: true
152
179
  }
153
180
  });
154
- const response = await instance.connection.newSession({
155
- cwd: workingDirectory,
156
- mcpServers: []
157
- });
158
- instance.sessionId = response.sessionId;
159
- instance.child.on("exit", (code, signal) => {
160
- log.info({ sessionId: instance.sessionId, exitCode: code, signal }, "Agent process exited");
181
+ return instance;
182
+ }
183
+ setupCrashDetection() {
184
+ this.child.on("exit", (code, signal) => {
185
+ log.info(
186
+ { sessionId: this.sessionId, exitCode: code, signal },
187
+ "Agent process exited"
188
+ );
161
189
  if (code !== 0 && code !== null) {
162
- const stderr = instance.stderrCapture.getLastLines();
163
- instance.onSessionUpdate({
190
+ const stderr = this.stderrCapture.getLastLines();
191
+ this.onSessionUpdate({
164
192
  type: "error",
165
193
  message: `Agent crashed (exit code ${code})
166
194
  ${stderr}`
167
195
  });
168
196
  }
169
197
  });
170
- instance.connection.closed.then(() => {
171
- log.debug({ sessionId: instance.sessionId }, "ACP connection closed");
198
+ this.connection.closed.then(() => {
199
+ log.debug({ sessionId: this.sessionId }, "ACP connection closed");
200
+ });
201
+ }
202
+ static async spawn(agentDef, workingDirectory) {
203
+ log.debug(
204
+ { agentName: agentDef.name, command: agentDef.command },
205
+ "Spawning agent"
206
+ );
207
+ const spawnStart = Date.now();
208
+ const instance = await _AgentInstance.spawnSubprocess(
209
+ agentDef,
210
+ workingDirectory
211
+ );
212
+ const response = await instance.connection.newSession({
213
+ cwd: workingDirectory,
214
+ mcpServers: []
172
215
  });
173
- log.info({ sessionId: response.sessionId, durationMs: Date.now() - spawnStart }, "Agent spawn complete");
216
+ instance.sessionId = response.sessionId;
217
+ instance.setupCrashDetection();
218
+ log.info(
219
+ { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
220
+ "Agent spawn complete"
221
+ );
222
+ return instance;
223
+ }
224
+ static async resume(agentDef, workingDirectory, agentSessionId) {
225
+ log.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
226
+ const spawnStart = Date.now();
227
+ const instance = await _AgentInstance.spawnSubprocess(
228
+ agentDef,
229
+ workingDirectory
230
+ );
231
+ try {
232
+ const response = await instance.connection.unstable_resumeSession({
233
+ sessionId: agentSessionId,
234
+ cwd: workingDirectory
235
+ });
236
+ instance.sessionId = response.sessionId;
237
+ log.info(
238
+ { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
239
+ "Agent resume complete"
240
+ );
241
+ } catch (err) {
242
+ log.warn(
243
+ { err, agentSessionId },
244
+ "Resume failed, falling back to new session"
245
+ );
246
+ const response = await instance.connection.newSession({
247
+ cwd: workingDirectory,
248
+ mcpServers: []
249
+ });
250
+ instance.sessionId = response.sessionId;
251
+ log.info(
252
+ { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
253
+ "Agent fallback spawn complete"
254
+ );
255
+ }
256
+ instance.setupCrashDetection();
174
257
  return instance;
175
258
  }
176
259
  // createClient — implemented in Task 6b
@@ -223,7 +306,10 @@ ${stderr}`
223
306
  };
224
307
  break;
225
308
  case "available_commands_update":
226
- event = { type: "commands_update", commands: update.availableCommands };
309
+ event = {
310
+ type: "commands_update",
311
+ commands: update.availableCommands
312
+ };
227
313
  break;
228
314
  default:
229
315
  return;
@@ -286,8 +372,14 @@ ${stderr}`
286
372
  state.output = state.output.slice(excess);
287
373
  }
288
374
  };
289
- childProcess.stdout?.on("data", (chunk) => appendOutput(chunk.toString()));
290
- childProcess.stderr?.on("data", (chunk) => appendOutput(chunk.toString()));
375
+ childProcess.stdout?.on(
376
+ "data",
377
+ (chunk) => appendOutput(chunk.toString())
378
+ );
379
+ childProcess.stderr?.on(
380
+ "data",
381
+ (chunk) => appendOutput(chunk.toString())
382
+ );
291
383
  childProcess.on("exit", (code, signal) => {
292
384
  state.exitStatus = { exitCode: code, signal };
293
385
  });
@@ -301,7 +393,10 @@ ${stderr}`
301
393
  return {
302
394
  output: state.output,
303
395
  truncated: false,
304
- exitStatus: state.exitStatus ? { exitCode: state.exitStatus.exitCode, signal: state.exitStatus.signal } : void 0
396
+ exitStatus: state.exitStatus ? {
397
+ exitCode: state.exitStatus.exitCode,
398
+ signal: state.exitStatus.signal
399
+ } : void 0
305
400
  };
306
401
  },
307
402
  async waitForTerminalExit(params) {
@@ -310,7 +405,10 @@ ${stderr}`
310
405
  throw new Error(`Terminal not found: ${params.terminalId}`);
311
406
  }
312
407
  if (state.exitStatus !== null) {
313
- return { exitCode: state.exitStatus.exitCode, signal: state.exitStatus.signal };
408
+ return {
409
+ exitCode: state.exitStatus.exitCode,
410
+ signal: state.exitStatus.signal
411
+ };
314
412
  }
315
413
  return new Promise((resolve) => {
316
414
  state.process.on("exit", (code, signal) => {
@@ -381,6 +479,11 @@ var AgentManager = class {
381
479
  if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
382
480
  return AgentInstance.spawn(agentDef, workingDirectory);
383
481
  }
482
+ async resume(agentName, workingDirectory, agentSessionId) {
483
+ const agentDef = this.getAgent(agentName);
484
+ if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
485
+ return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
486
+ }
384
487
  };
385
488
 
386
489
  // src/core/session.ts
@@ -393,6 +496,7 @@ var Session = class {
393
496
  agentName;
394
497
  workingDirectory;
395
498
  agentInstance;
499
+ agentSessionId = "";
396
500
  status = "initializing";
397
501
  name;
398
502
  promptQueue = [];
@@ -426,7 +530,10 @@ var Session = class {
426
530
  this.log.debug("Prompt execution started");
427
531
  try {
428
532
  await this.agentInstance.prompt(text);
429
- this.log.info({ durationMs: Date.now() - promptStart }, "Prompt execution completed");
533
+ this.log.info(
534
+ { durationMs: Date.now() - promptStart },
535
+ "Prompt execution completed"
536
+ );
430
537
  if (!this.name) {
431
538
  await this.autoName();
432
539
  }
@@ -499,10 +606,34 @@ var Session = class {
499
606
  // src/core/session-manager.ts
500
607
  var SessionManager = class {
501
608
  sessions = /* @__PURE__ */ new Map();
609
+ store;
610
+ constructor(store = null) {
611
+ this.store = store;
612
+ }
502
613
  async createSession(channelId, agentName, workingDirectory, agentManager) {
503
614
  const agentInstance = await agentManager.spawn(agentName, workingDirectory);
504
- const session = new Session({ channelId, agentName, workingDirectory, agentInstance });
615
+ const session = new Session({
616
+ channelId,
617
+ agentName,
618
+ workingDirectory,
619
+ agentInstance
620
+ });
505
621
  this.sessions.set(session.id, session);
622
+ session.agentSessionId = session.agentInstance.sessionId;
623
+ if (this.store) {
624
+ await this.store.save({
625
+ sessionId: session.id,
626
+ agentSessionId: session.agentInstance.sessionId,
627
+ agentName: session.agentName,
628
+ workingDir: session.workingDirectory,
629
+ channelId,
630
+ status: session.status,
631
+ createdAt: session.createdAt.toISOString(),
632
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
633
+ name: session.name,
634
+ platform: {}
635
+ });
636
+ }
506
637
  return session;
507
638
  }
508
639
  getSession(sessionId) {
@@ -516,9 +647,44 @@ var SessionManager = class {
516
647
  }
517
648
  return void 0;
518
649
  }
650
+ registerSession(session) {
651
+ this.sessions.set(session.id, session);
652
+ }
653
+ async updateSessionPlatform(sessionId, platform) {
654
+ if (!this.store) return;
655
+ const record = this.store.get(sessionId);
656
+ if (record) {
657
+ await this.store.save({ ...record, platform });
658
+ }
659
+ }
660
+ async updateSessionActivity(sessionId) {
661
+ if (!this.store) return;
662
+ const record = this.store.get(sessionId);
663
+ if (record) {
664
+ await this.store.save({
665
+ ...record,
666
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
667
+ });
668
+ }
669
+ }
670
+ async updateSessionStatus(sessionId, status) {
671
+ if (!this.store) return;
672
+ const record = this.store.get(sessionId);
673
+ if (record) {
674
+ await this.store.save({ ...record, status });
675
+ }
676
+ }
519
677
  async cancelSession(sessionId) {
520
678
  const session = this.sessions.get(sessionId);
521
- if (session) await session.cancel();
679
+ if (session) {
680
+ await session.cancel();
681
+ if (this.store) {
682
+ const record = this.store.get(sessionId);
683
+ if (record) {
684
+ await this.store.save({ ...record, status: "cancelled" });
685
+ }
686
+ }
687
+ }
522
688
  }
523
689
  listSessions(channelId) {
524
690
  const all = Array.from(this.sessions.values());
@@ -526,6 +692,14 @@ var SessionManager = class {
526
692
  return all;
527
693
  }
528
694
  async destroyAll() {
695
+ if (this.store) {
696
+ for (const session of this.sessions.values()) {
697
+ const record = this.store.get(session.id);
698
+ if (record) {
699
+ await this.store.save({ ...record, status: "finished" });
700
+ }
701
+ }
702
+ }
529
703
  for (const session of this.sessions.values()) {
530
704
  await session.destroy();
531
705
  }
@@ -551,6 +725,129 @@ var NotificationManager = class {
551
725
  }
552
726
  };
553
727
 
728
+ // src/core/core.ts
729
+ import path3 from "path";
730
+ import os from "os";
731
+
732
+ // src/core/session-store.ts
733
+ import fs2 from "fs";
734
+ import path2 from "path";
735
+ var log2 = createChildLogger({ module: "session-store" });
736
+ var DEBOUNCE_MS = 2e3;
737
+ var JsonFileSessionStore = class {
738
+ records = /* @__PURE__ */ new Map();
739
+ filePath;
740
+ ttlDays;
741
+ debounceTimer = null;
742
+ cleanupInterval = null;
743
+ flushHandler = null;
744
+ constructor(filePath, ttlDays) {
745
+ this.filePath = filePath;
746
+ this.ttlDays = ttlDays;
747
+ this.load();
748
+ this.cleanup();
749
+ this.cleanupInterval = setInterval(
750
+ () => this.cleanup(),
751
+ 24 * 60 * 60 * 1e3
752
+ );
753
+ this.flushHandler = () => this.flushSync();
754
+ process.on("SIGTERM", this.flushHandler);
755
+ process.on("SIGINT", this.flushHandler);
756
+ process.on("exit", this.flushHandler);
757
+ }
758
+ async save(record) {
759
+ this.records.set(record.sessionId, { ...record });
760
+ this.scheduleDiskWrite();
761
+ }
762
+ get(sessionId) {
763
+ return this.records.get(sessionId);
764
+ }
765
+ findByPlatform(channelId, predicate) {
766
+ for (const record of this.records.values()) {
767
+ if (record.channelId === channelId && predicate(record.platform)) {
768
+ return record;
769
+ }
770
+ }
771
+ return void 0;
772
+ }
773
+ list(channelId) {
774
+ const all = [...this.records.values()];
775
+ if (channelId) return all.filter((r) => r.channelId === channelId);
776
+ return all;
777
+ }
778
+ async remove(sessionId) {
779
+ this.records.delete(sessionId);
780
+ this.scheduleDiskWrite();
781
+ }
782
+ flushSync() {
783
+ if (this.debounceTimer) {
784
+ clearTimeout(this.debounceTimer);
785
+ this.debounceTimer = null;
786
+ }
787
+ const data = {
788
+ version: 1,
789
+ sessions: Object.fromEntries(this.records)
790
+ };
791
+ const dir = path2.dirname(this.filePath);
792
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
793
+ fs2.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
794
+ }
795
+ destroy() {
796
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
797
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
798
+ if (this.flushHandler) {
799
+ process.removeListener("SIGTERM", this.flushHandler);
800
+ process.removeListener("SIGINT", this.flushHandler);
801
+ process.removeListener("exit", this.flushHandler);
802
+ this.flushHandler = null;
803
+ }
804
+ }
805
+ load() {
806
+ if (!fs2.existsSync(this.filePath)) return;
807
+ try {
808
+ const raw = JSON.parse(
809
+ fs2.readFileSync(this.filePath, "utf-8")
810
+ );
811
+ if (raw.version !== 1) {
812
+ log2.warn(
813
+ { version: raw.version },
814
+ "Unknown session store version, skipping load"
815
+ );
816
+ return;
817
+ }
818
+ for (const [id, record] of Object.entries(raw.sessions)) {
819
+ this.records.set(id, record);
820
+ }
821
+ log2.info({ count: this.records.size }, "Loaded session records");
822
+ } catch (err) {
823
+ log2.error({ err }, "Failed to load session store");
824
+ }
825
+ }
826
+ cleanup() {
827
+ const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
828
+ let removed = 0;
829
+ for (const [id, record] of this.records) {
830
+ if (record.status === "active" || record.status === "initializing")
831
+ continue;
832
+ const lastActive = new Date(record.lastActiveAt).getTime();
833
+ if (lastActive < cutoff) {
834
+ this.records.delete(id);
835
+ removed++;
836
+ }
837
+ }
838
+ if (removed > 0) {
839
+ log2.info({ removed }, "Cleaned up expired session records");
840
+ this.scheduleDiskWrite();
841
+ }
842
+ }
843
+ scheduleDiskWrite() {
844
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
845
+ this.debounceTimer = setTimeout(() => {
846
+ this.flushSync();
847
+ }, DEBOUNCE_MS);
848
+ }
849
+ };
850
+
554
851
  // src/tunnel/extract-file-info.ts
555
852
  function extractFileInfo(name, kind, content) {
556
853
  if (!content) return null;
@@ -621,7 +918,7 @@ function parseContent(content) {
621
918
  }
622
919
 
623
920
  // src/core/core.ts
624
- var log2 = createChildLogger({ module: "core" });
921
+ var log3 = createChildLogger({ module: "core" });
625
922
  var OpenACPCore = class {
626
923
  configManager;
627
924
  agentManager;
@@ -629,11 +926,18 @@ var OpenACPCore = class {
629
926
  notificationManager;
630
927
  adapters = /* @__PURE__ */ new Map();
631
928
  tunnelService;
929
+ sessionStore = null;
930
+ resumeLocks = /* @__PURE__ */ new Map();
632
931
  constructor(configManager) {
633
932
  this.configManager = configManager;
634
933
  const config = configManager.get();
635
934
  this.agentManager = new AgentManager(config);
636
- this.sessionManager = new SessionManager();
935
+ const storePath = path3.join(os.homedir(), ".openacp", "sessions.json");
936
+ this.sessionStore = new JsonFileSessionStore(
937
+ storePath,
938
+ config.sessionStore.ttlDays
939
+ );
940
+ this.sessionManager = new SessionManager(this.sessionStore);
637
941
  this.notificationManager = new NotificationManager(this.adapters);
638
942
  }
639
943
  registerAdapter(name, adapter) {
@@ -661,16 +965,33 @@ var OpenACPCore = class {
661
965
  // --- Message Routing ---
662
966
  async handleMessage(message) {
663
967
  const config = this.configManager.get();
664
- log2.debug({ channelId: message.channelId, threadId: message.threadId, userId: message.userId }, "Incoming message");
968
+ log3.debug(
969
+ {
970
+ channelId: message.channelId,
971
+ threadId: message.threadId,
972
+ userId: message.userId
973
+ },
974
+ "Incoming message"
975
+ );
665
976
  if (config.security.allowedUserIds.length > 0) {
666
977
  if (!config.security.allowedUserIds.includes(message.userId)) {
667
- log2.warn({ userId: message.userId }, "Rejected message from unauthorized user");
978
+ log3.warn(
979
+ { userId: message.userId },
980
+ "Rejected message from unauthorized user"
981
+ );
668
982
  return;
669
983
  }
670
984
  }
671
985
  const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
672
986
  if (activeSessions.length >= config.security.maxConcurrentSessions) {
673
- log2.warn({ userId: message.userId, currentCount: activeSessions.length, max: config.security.maxConcurrentSessions }, "Session limit reached");
987
+ log3.warn(
988
+ {
989
+ userId: message.userId,
990
+ currentCount: activeSessions.length,
991
+ max: config.security.maxConcurrentSessions
992
+ },
993
+ "Session limit reached"
994
+ );
674
995
  const adapter = this.adapters.get(message.channelId);
675
996
  if (adapter) {
676
997
  await adapter.sendMessage("system", {
@@ -680,14 +1001,21 @@ var OpenACPCore = class {
680
1001
  }
681
1002
  return;
682
1003
  }
683
- const session = this.sessionManager.getSessionByThread(message.channelId, message.threadId);
1004
+ let session = this.sessionManager.getSessionByThread(
1005
+ message.channelId,
1006
+ message.threadId
1007
+ );
1008
+ if (!session) {
1009
+ session = await this.lazyResume(message) ?? void 0;
1010
+ }
684
1011
  if (!session) return;
1012
+ this.sessionManager.updateSessionActivity(session.id);
685
1013
  await session.enqueuePrompt(message.text);
686
1014
  }
687
1015
  async handleNewSession(channelId, agentName, workspacePath) {
688
1016
  const config = this.configManager.get();
689
1017
  const resolvedAgent = agentName || config.defaultAgent;
690
- log2.info({ channelId, agentName: resolvedAgent }, "New session request");
1018
+ log3.info({ channelId, agentName: resolvedAgent }, "New session request");
691
1019
  const resolvedWorkspace = this.configManager.resolveWorkspace(
692
1020
  workspacePath || config.agents[resolvedAgent]?.workingDirectory
693
1021
  );
@@ -704,7 +1032,10 @@ var OpenACPCore = class {
704
1032
  return session;
705
1033
  }
706
1034
  async handleNewChat(channelId, currentThreadId) {
707
- const currentSession = this.sessionManager.getSessionByThread(channelId, currentThreadId);
1035
+ const currentSession = this.sessionManager.getSessionByThread(
1036
+ channelId,
1037
+ currentThreadId
1038
+ );
708
1039
  if (!currentSession) return null;
709
1040
  return this.handleNewSession(
710
1041
  channelId,
@@ -712,6 +1043,63 @@ var OpenACPCore = class {
712
1043
  currentSession.workingDirectory
713
1044
  );
714
1045
  }
1046
+ // --- Lazy Resume ---
1047
+ async lazyResume(message) {
1048
+ const store = this.sessionStore;
1049
+ if (!store) return null;
1050
+ const lockKey = `${message.channelId}:${message.threadId}`;
1051
+ const existing = this.resumeLocks.get(lockKey);
1052
+ if (existing) return existing;
1053
+ const record = store.findByPlatform(
1054
+ message.channelId,
1055
+ (p) => String(p.topicId) === message.threadId
1056
+ );
1057
+ if (!record) return null;
1058
+ if (record.status === "cancelled" || record.status === "error") return null;
1059
+ const resumePromise = (async () => {
1060
+ try {
1061
+ const agentInstance = await this.agentManager.resume(
1062
+ record.agentName,
1063
+ record.workingDir,
1064
+ record.agentSessionId
1065
+ );
1066
+ const session = new Session({
1067
+ id: record.sessionId,
1068
+ channelId: record.channelId,
1069
+ agentName: record.agentName,
1070
+ workingDirectory: record.workingDir,
1071
+ agentInstance
1072
+ });
1073
+ session.threadId = message.threadId;
1074
+ session.agentSessionId = agentInstance.sessionId;
1075
+ session.status = "active";
1076
+ session.name = record.name;
1077
+ this.sessionManager.registerSession(session);
1078
+ const adapter = this.adapters.get(message.channelId);
1079
+ if (adapter) {
1080
+ this.wireSessionEvents(session, adapter);
1081
+ }
1082
+ await store.save({
1083
+ ...record,
1084
+ agentSessionId: agentInstance.sessionId,
1085
+ status: "active",
1086
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1087
+ });
1088
+ log3.info(
1089
+ { sessionId: session.id, threadId: message.threadId },
1090
+ "Lazy resume successful"
1091
+ );
1092
+ return session;
1093
+ } catch (err) {
1094
+ log3.error({ err, record }, "Lazy resume failed");
1095
+ return null;
1096
+ } finally {
1097
+ this.resumeLocks.delete(lockKey);
1098
+ }
1099
+ })();
1100
+ this.resumeLocks.set(lockKey, resumePromise);
1101
+ return resumePromise;
1102
+ }
715
1103
  // --- Event Wiring ---
716
1104
  toOutgoingMessage(event, session) {
717
1105
  switch (event.type) {
@@ -720,19 +1108,37 @@ var OpenACPCore = class {
720
1108
  case "thought":
721
1109
  return { type: "thought", text: event.content };
722
1110
  case "tool_call": {
723
- const metadata = { id: event.id, kind: event.kind, status: event.status, content: event.content, locations: event.locations };
1111
+ const metadata = {
1112
+ id: event.id,
1113
+ kind: event.kind,
1114
+ status: event.status,
1115
+ content: event.content,
1116
+ locations: event.locations
1117
+ };
724
1118
  this.enrichWithViewerLinks(event, metadata, session);
725
1119
  return { type: "tool_call", text: event.name, metadata };
726
1120
  }
727
1121
  case "tool_update": {
728
- const metadata = { id: event.id, status: event.status, content: event.content };
1122
+ const metadata = {
1123
+ id: event.id,
1124
+ status: event.status,
1125
+ content: event.content
1126
+ };
729
1127
  this.enrichWithViewerLinks(event, metadata, session);
730
1128
  return { type: "tool_update", text: "", metadata };
731
1129
  }
732
1130
  case "plan":
733
1131
  return { type: "plan", text: "", metadata: { entries: event.entries } };
734
1132
  case "usage":
735
- return { type: "usage", text: "", metadata: { tokensUsed: event.tokensUsed, contextSize: event.contextSize, cost: event.cost } };
1133
+ return {
1134
+ type: "usage",
1135
+ text: "",
1136
+ metadata: {
1137
+ tokensUsed: event.tokensUsed,
1138
+ contextSize: event.contextSize,
1139
+ cost: event.cost
1140
+ }
1141
+ };
736
1142
  default:
737
1143
  return { type: "text", text: "" };
738
1144
  }
@@ -741,17 +1147,39 @@ var OpenACPCore = class {
741
1147
  if (!this.tunnelService || !session) return;
742
1148
  const name = "name" in event ? event.name || "" : "";
743
1149
  const kind = "kind" in event ? event.kind : void 0;
744
- log2.debug({ name, kind, status: event.status, hasContent: !!event.content }, "enrichWithViewerLinks: inspecting event");
1150
+ log3.debug(
1151
+ { name, kind, status: event.status, hasContent: !!event.content },
1152
+ "enrichWithViewerLinks: inspecting event"
1153
+ );
745
1154
  const fileInfo = extractFileInfo(name, kind, event.content);
746
1155
  if (!fileInfo) return;
747
- log2.info({ name, kind, filePath: fileInfo.filePath, hasOldContent: !!fileInfo.oldContent }, "enrichWithViewerLinks: extracted file info");
1156
+ log3.info(
1157
+ {
1158
+ name,
1159
+ kind,
1160
+ filePath: fileInfo.filePath,
1161
+ hasOldContent: !!fileInfo.oldContent
1162
+ },
1163
+ "enrichWithViewerLinks: extracted file info"
1164
+ );
748
1165
  const store = this.tunnelService.getStore();
749
1166
  const viewerLinks = {};
750
1167
  if (fileInfo.oldContent) {
751
- const id2 = store.storeDiff(session.id, fileInfo.filePath, fileInfo.oldContent, fileInfo.content, session.workingDirectory);
1168
+ const id2 = store.storeDiff(
1169
+ session.id,
1170
+ fileInfo.filePath,
1171
+ fileInfo.oldContent,
1172
+ fileInfo.content,
1173
+ session.workingDirectory
1174
+ );
752
1175
  if (id2) viewerLinks.diff = this.tunnelService.diffUrl(id2);
753
1176
  }
754
- const id = store.storeFile(session.id, fileInfo.filePath, fileInfo.content, session.workingDirectory);
1177
+ const id = store.storeFile(
1178
+ session.id,
1179
+ fileInfo.filePath,
1180
+ fileInfo.content,
1181
+ session.workingDirectory
1182
+ );
755
1183
  if (id) viewerLinks.file = this.tunnelService.fileUrl(id);
756
1184
  if (Object.keys(viewerLinks).length > 0) {
757
1185
  metadata.viewerLinks = viewerLinks;
@@ -768,12 +1196,19 @@ var OpenACPCore = class {
768
1196
  case "tool_update":
769
1197
  case "plan":
770
1198
  case "usage":
771
- adapter.sendMessage(session.id, this.toOutgoingMessage(event, session));
1199
+ adapter.sendMessage(
1200
+ session.id,
1201
+ this.toOutgoingMessage(event, session)
1202
+ );
772
1203
  break;
773
1204
  case "session_end":
774
1205
  session.status = "finished";
1206
+ this.sessionManager.updateSessionStatus(session.id, "finished");
775
1207
  adapter.cleanupSkillCommands(session.id);
776
- adapter.sendMessage(session.id, { type: "session_end", text: `Done (${event.reason})` });
1208
+ adapter.sendMessage(session.id, {
1209
+ type: "session_end",
1210
+ text: `Done (${event.reason})`
1211
+ });
777
1212
  this.notificationManager.notify(session.channelId, {
778
1213
  sessionId: session.id,
779
1214
  sessionName: session.name,
@@ -782,8 +1217,12 @@ var OpenACPCore = class {
782
1217
  });
783
1218
  break;
784
1219
  case "error":
1220
+ this.sessionManager.updateSessionStatus(session.id, "error");
785
1221
  adapter.cleanupSkillCommands(session.id);
786
- adapter.sendMessage(session.id, { type: "error", text: event.message });
1222
+ adapter.sendMessage(session.id, {
1223
+ type: "error",
1224
+ text: event.message
1225
+ });
787
1226
  this.notificationManager.notify(session.channelId, {
788
1227
  sessionId: session.id,
789
1228
  sessionName: session.name,
@@ -792,7 +1231,7 @@ var OpenACPCore = class {
792
1231
  });
793
1232
  break;
794
1233
  case "commands_update":
795
- log2.debug({ commands: event.commands }, "Commands available");
1234
+ log3.debug({ commands: event.commands }, "Commands available");
796
1235
  adapter.sendSkillCommands(session.id, event.commands);
797
1236
  break;
798
1237
  }
@@ -1106,7 +1545,7 @@ function buildDeepLink(chatId, messageId) {
1106
1545
  // src/adapters/telegram/commands.ts
1107
1546
  import { InlineKeyboard } from "grammy";
1108
1547
  import { nanoid as nanoid2 } from "nanoid";
1109
- var log4 = createChildLogger({ module: "telegram-commands" });
1548
+ var log5 = createChildLogger({ module: "telegram-commands" });
1110
1549
  function setupCommands(bot, core, chatId) {
1111
1550
  bot.command("new", (ctx) => handleNew(ctx, core, chatId));
1112
1551
  bot.command("new_chat", (ctx) => handleNewChat(ctx, core, chatId));
@@ -1161,7 +1600,7 @@ async function handleNew(ctx, core, chatId) {
1161
1600
  const args = matchStr.split(" ").filter(Boolean);
1162
1601
  const agentName = args[0];
1163
1602
  const workspace = args[1];
1164
- log4.info({ userId: ctx.from?.id, agentName }, "New session command");
1603
+ log5.info({ userId: ctx.from?.id, agentName }, "New session command");
1165
1604
  let threadId;
1166
1605
  try {
1167
1606
  const topicName = `\u{1F504} New Session`;
@@ -1176,6 +1615,9 @@ async function handleNew(ctx, core, chatId) {
1176
1615
  workspace
1177
1616
  );
1178
1617
  session.threadId = String(threadId);
1618
+ await core.sessionManager.updateSessionPlatform(session.id, {
1619
+ topicId: threadId
1620
+ });
1179
1621
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
1180
1622
  try {
1181
1623
  await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
@@ -1191,7 +1633,7 @@ async function handleNew(ctx, core, chatId) {
1191
1633
  parse_mode: "HTML"
1192
1634
  }
1193
1635
  );
1194
- session.warmup().catch((err) => log4.error({ err }, "Warm-up error"));
1636
+ session.warmup().catch((err) => log5.error({ err }, "Warm-up error"));
1195
1637
  } catch (err) {
1196
1638
  if (threadId) {
1197
1639
  try {
@@ -1231,6 +1673,9 @@ async function handleNewChat(ctx, core, chatId) {
1231
1673
  parse_mode: "HTML"
1232
1674
  });
1233
1675
  session.threadId = String(newThreadId);
1676
+ await core.sessionManager.updateSessionPlatform(session.id, {
1677
+ topicId: newThreadId
1678
+ });
1234
1679
  await ctx.api.sendMessage(
1235
1680
  chatId,
1236
1681
  `\u2705 New chat (same agent &amp; workspace)
@@ -1241,7 +1686,7 @@ async function handleNewChat(ctx, core, chatId) {
1241
1686
  parse_mode: "HTML"
1242
1687
  }
1243
1688
  );
1244
- session.warmup().catch((err) => log4.error({ err }, "Warm-up error"));
1689
+ session.warmup().catch((err) => log5.error({ err }, "Warm-up error"));
1245
1690
  } catch (err) {
1246
1691
  const message = err instanceof Error ? err.message : String(err);
1247
1692
  await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
@@ -1255,7 +1700,7 @@ async function handleCancel(ctx, core) {
1255
1700
  String(threadId)
1256
1701
  );
1257
1702
  if (session) {
1258
- log4.info({ sessionId: session.id }, "Cancel session command");
1703
+ log5.info({ sessionId: session.id }, "Cancel session command");
1259
1704
  await session.cancel();
1260
1705
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
1261
1706
  }
@@ -1376,7 +1821,7 @@ var STATIC_COMMANDS = [
1376
1821
  // src/adapters/telegram/permissions.ts
1377
1822
  import { InlineKeyboard as InlineKeyboard2 } from "grammy";
1378
1823
  import { nanoid as nanoid3 } from "nanoid";
1379
- var log5 = createChildLogger({ module: "telegram-permissions" });
1824
+ var log6 = createChildLogger({ module: "telegram-permissions" });
1380
1825
  var PermissionHandler = class {
1381
1826
  constructor(bot, chatId, getSession, sendNotification) {
1382
1827
  this.bot = bot;
@@ -1436,7 +1881,7 @@ ${escapeHtml(request.description)}`,
1436
1881
  }
1437
1882
  const session = this.getSession(pending.sessionId);
1438
1883
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
1439
- log5.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
1884
+ log6.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
1440
1885
  if (session?.pendingPermission?.requestId === pending.requestId) {
1441
1886
  session.pendingPermission.resolve(optionId);
1442
1887
  session.pendingPermission = void 0;
@@ -1503,7 +1948,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
1503
1948
  }
1504
1949
 
1505
1950
  // src/adapters/telegram/adapter.ts
1506
- var log6 = createChildLogger({ module: "telegram" });
1951
+ var log7 = createChildLogger({ module: "telegram" });
1507
1952
  var TelegramAdapter = class extends ChannelAdapter {
1508
1953
  bot;
1509
1954
  telegramConfig;
@@ -1521,10 +1966,10 @@ var TelegramAdapter = class extends ChannelAdapter {
1521
1966
  this.telegramConfig = config;
1522
1967
  }
1523
1968
  async start() {
1524
- this.bot = new Bot(this.telegramConfig.botToken);
1969
+ this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch } });
1525
1970
  this.bot.catch((err) => {
1526
1971
  const rootCause = err.error instanceof Error ? err.error : err;
1527
- log6.error({ err: rootCause }, "Telegram bot error");
1972
+ log7.error({ err: rootCause }, "Telegram bot error");
1528
1973
  });
1529
1974
  this.bot.api.config.use((prev, method, payload, signal) => {
1530
1975
  if (method === "getUpdates") {
@@ -1577,7 +2022,7 @@ var TelegramAdapter = class extends ChannelAdapter {
1577
2022
  this.setupRoutes();
1578
2023
  this.bot.start({
1579
2024
  allowed_updates: ["message", "callback_query"],
1580
- onStart: () => log6.info(
2025
+ onStart: () => log7.info(
1581
2026
  { chatId: this.telegramConfig.chatId },
1582
2027
  "Telegram bot started"
1583
2028
  )
@@ -1589,7 +2034,7 @@ var TelegramAdapter = class extends ChannelAdapter {
1589
2034
  this.assistantTopicId
1590
2035
  );
1591
2036
  } catch (err) {
1592
- log6.error({ err }, "Failed to spawn assistant");
2037
+ log7.error({ err }, "Failed to spawn assistant");
1593
2038
  }
1594
2039
  try {
1595
2040
  const config = this.core.configManager.get();
@@ -1610,7 +2055,7 @@ Workspace: <code>${workspace}</code>
1610
2055
  reply_markup: buildMenuKeyboard()
1611
2056
  });
1612
2057
  } catch (err) {
1613
- log6.warn({ err }, "Failed to send welcome message");
2058
+ log7.warn({ err }, "Failed to send welcome message");
1614
2059
  }
1615
2060
  }
1616
2061
  async stop() {
@@ -1618,7 +2063,7 @@ Workspace: <code>${workspace}</code>
1618
2063
  await this.assistantSession.destroy();
1619
2064
  }
1620
2065
  await this.bot.stop();
1621
- log6.info("Telegram bot stopped");
2066
+ log7.info("Telegram bot stopped");
1622
2067
  }
1623
2068
  setupRoutes() {
1624
2069
  this.bot.on("message:text", async (ctx) => {
@@ -1636,7 +2081,7 @@ Workspace: <code>${workspace}</code>
1636
2081
  ctx.replyWithChatAction("typing").catch(() => {
1637
2082
  });
1638
2083
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
1639
- (err) => log6.error({ err }, "Assistant error")
2084
+ (err) => log7.error({ err }, "Assistant error")
1640
2085
  );
1641
2086
  return;
1642
2087
  }
@@ -1647,7 +2092,7 @@ Workspace: <code>${workspace}</code>
1647
2092
  threadId: String(threadId),
1648
2093
  userId: String(ctx.from.id),
1649
2094
  text: ctx.message.text
1650
- }).catch((err) => log6.error({ err }, "handleMessage error"));
2095
+ }).catch((err) => log7.error({ err }, "handleMessage error"));
1651
2096
  });
1652
2097
  }
1653
2098
  // --- ChannelAdapter implementations ---
@@ -1725,7 +2170,9 @@ Workspace: <code>${workspace}</code>
1725
2170
  await this.finalizeDraft(sessionId);
1726
2171
  await this.bot.api.sendMessage(
1727
2172
  this.telegramConfig.chatId,
1728
- formatPlan(content.metadata),
2173
+ formatPlan(
2174
+ content.metadata
2175
+ ),
1729
2176
  {
1730
2177
  message_thread_id: threadId,
1731
2178
  parse_mode: "HTML",
@@ -1737,7 +2184,9 @@ Workspace: <code>${workspace}</code>
1737
2184
  case "usage": {
1738
2185
  await this.bot.api.sendMessage(
1739
2186
  this.telegramConfig.chatId,
1740
- formatUsage(content.metadata),
2187
+ formatUsage(
2188
+ content.metadata
2189
+ ),
1741
2190
  {
1742
2191
  message_thread_id: threadId,
1743
2192
  parse_mode: "HTML",
@@ -1778,7 +2227,7 @@ Workspace: <code>${workspace}</code>
1778
2227
  }
1779
2228
  }
1780
2229
  async sendPermissionRequest(sessionId, request) {
1781
- log6.info({ sessionId, requestId: request.id }, "Permission request sent");
2230
+ log7.info({ sessionId, requestId: request.id }, "Permission request sent");
1782
2231
  const session = this.core.sessionManager.getSession(
1783
2232
  sessionId
1784
2233
  );
@@ -1786,7 +2235,7 @@ Workspace: <code>${workspace}</code>
1786
2235
  await this.permissionHandler.sendPermissionRequest(session, request);
1787
2236
  }
1788
2237
  async sendNotification(notification) {
1789
- log6.info(
2238
+ log7.info(
1790
2239
  { sessionId: notification.sessionId, type: notification.type },
1791
2240
  "Notification sent"
1792
2241
  );
@@ -1812,7 +2261,7 @@ Workspace: <code>${workspace}</code>
1812
2261
  });
1813
2262
  }
1814
2263
  async createSessionThread(sessionId, name) {
1815
- log6.info({ sessionId, name }, "Session topic created");
2264
+ log7.info({ sessionId, name }, "Session topic created");
1816
2265
  return String(
1817
2266
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
1818
2267
  );
@@ -1830,7 +2279,9 @@ Workspace: <code>${workspace}</code>
1830
2279
  );
1831
2280
  }
1832
2281
  async sendSkillCommands(sessionId, commands) {
1833
- const session = this.core.sessionManager.getSession(sessionId);
2282
+ const session = this.core.sessionManager.getSession(
2283
+ sessionId
2284
+ );
1834
2285
  if (!session) return;
1835
2286
  const threadId = Number(session.threadId);
1836
2287
  if (!threadId) return;
@@ -1866,11 +2317,15 @@ Workspace: <code>${workspace}</code>
1866
2317
  }
1867
2318
  );
1868
2319
  this.skillMessages.set(sessionId, msg.message_id);
1869
- await this.bot.api.pinChatMessage(this.telegramConfig.chatId, msg.message_id, {
1870
- disable_notification: true
1871
- });
2320
+ await this.bot.api.pinChatMessage(
2321
+ this.telegramConfig.chatId,
2322
+ msg.message_id,
2323
+ {
2324
+ disable_notification: true
2325
+ }
2326
+ );
1872
2327
  } catch (err) {
1873
- log6.error({ err, sessionId }, "Failed to send skill commands");
2328
+ log7.error({ err, sessionId }, "Failed to send skill commands");
1874
2329
  }
1875
2330
  await this.updateCommandAutocomplete(session.agentName, commands);
1876
2331
  }
@@ -1901,9 +2356,15 @@ Workspace: <code>${workspace}</code>
1901
2356
  await this.bot.api.setMyCommands(all, {
1902
2357
  scope: { type: "chat", chat_id: this.telegramConfig.chatId }
1903
2358
  });
1904
- log6.info({ count: all.length, skills: validSkills.length }, "Updated command autocomplete");
2359
+ log7.info(
2360
+ { count: all.length, skills: validSkills.length },
2361
+ "Updated command autocomplete"
2362
+ );
1905
2363
  } catch (err) {
1906
- log6.error({ err, commands: all }, "Failed to update command autocomplete");
2364
+ log7.error(
2365
+ { err, commands: all },
2366
+ "Failed to update command autocomplete"
2367
+ );
1907
2368
  }
1908
2369
  }
1909
2370
  async finalizeDraft(sessionId) {
@@ -1928,4 +2389,4 @@ export {
1928
2389
  ChannelAdapter,
1929
2390
  TelegramAdapter
1930
2391
  };
1931
- //# sourceMappingURL=chunk-E6BM7RUB.js.map
2392
+ //# sourceMappingURL=chunk-HTXK4NLG.js.map