lark-coding-assistant 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // src/daemon/server.ts
2
- import { appendFile, chmod as chmod3, open as open2, readFile as readFile2, rm } from "fs/promises";
2
+ import { appendFile, chmod as chmod3, open as open2, readFile as readFile3, rm } from "fs/promises";
3
3
  import { createServer } from "net";
4
4
 
5
5
  // src/platform/process.ts
@@ -92,6 +92,14 @@ async function writeJsonAtomic(path, value, mode = 384) {
92
92
  await chmod(path, mode);
93
93
  }
94
94
 
95
+ // src/agents/types.ts
96
+ var AGENT_IDS = ["codex", "traex", "claude"];
97
+ function normalizeAgentId(value) {
98
+ if (value === "trae-cli") return "traex";
99
+ if (value === "claude-code") return "claude";
100
+ return AGENT_IDS.includes(value) ? value : void 0;
101
+ }
102
+
95
103
  // src/core/store.ts
96
104
  var AppStore = class {
97
105
  constructor(paths2) {
@@ -108,8 +116,21 @@ var AppStore = class {
108
116
  chmod2(this.paths.logsDir, 448)
109
117
  ]);
110
118
  }
111
- loadConfig() {
112
- return readJson(this.paths.config);
119
+ async loadConfig() {
120
+ const config = await readJson(this.paths.config);
121
+ if (!config) return void 0;
122
+ const binaries = config.agentBinaries ?? {};
123
+ return {
124
+ tenant: config.tenant,
125
+ appId: config.appId,
126
+ tmuxBinary: config.tmuxBinary,
127
+ agentBinaries: {
128
+ codex: binaries.codex ?? "codex",
129
+ traex: binaries.traex ?? binaries["trae-cli"] ?? "trae-cli",
130
+ claude: binaries.claude ?? binaries["claude-code"] ?? "claude"
131
+ },
132
+ pollIntervalMs: config.pollIntervalMs
133
+ };
113
134
  }
114
135
  saveConfig(config) {
115
136
  return writeJsonAtomic(this.paths.config, config);
@@ -121,7 +142,13 @@ var AppStore = class {
121
142
  return writeJsonAtomic(this.paths.secrets, secrets);
122
143
  }
123
144
  async loadState() {
124
- return await readJson(this.paths.state) ?? emptyState();
145
+ const state = await readJson(this.paths.state);
146
+ if (!state) return emptyState();
147
+ const sessions = Object.fromEntries(Object.entries(state.sessions ?? {}).flatMap(([id, session]) => {
148
+ const agent = normalizeAgentId(session.agent);
149
+ return agent ? [[id, { ...session, agent }]] : [];
150
+ }));
151
+ return { ...state, sessions };
125
152
  }
126
153
  saveState(state) {
127
154
  return writeJsonAtomic(this.paths.state, state);
@@ -171,6 +198,246 @@ function serializeAppError(error) {
171
198
  ...Object.keys(errorContext).length > 0 ? { errorContext } : {}
172
199
  };
173
200
  }
201
+ function systemErrorCode(error) {
202
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
203
+ }
204
+
205
+ // src/session/start-request.ts
206
+ import { stat } from "fs/promises";
207
+ import { isAbsolute } from "path";
208
+ async function validateStartSessionRequest(request) {
209
+ if (!validSessionId(request.sessionId)) {
210
+ throw new AppError(
211
+ "INVALID_SESSION_NAME",
212
+ "session name must use letters, digits, underscore, or dash",
213
+ { sessionId: request.sessionId }
214
+ );
215
+ }
216
+ if (!isAbsolute(request.cwd)) {
217
+ throw new AppError("INVALID_CWD", "working directory must be absolute", { cwd: request.cwd });
218
+ }
219
+ const info = await stat(request.cwd).catch((error) => {
220
+ throw new AppError("INVALID_CWD", `working directory is unavailable: ${request.cwd}`, { cwd: request.cwd }, { cause: error });
221
+ });
222
+ if (!info.isDirectory()) {
223
+ throw new AppError("INVALID_CWD", `working directory is not a directory: ${request.cwd}`, { cwd: request.cwd });
224
+ }
225
+ if (request.resume?.mode === "session" && !request.resume.sessionId.trim()) {
226
+ throw new AppError("INVALID_RESUME", "resume session id must not be empty", {
227
+ reason: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u65F6\u5FC5\u987B\u63D0\u4F9B session ID"
228
+ });
229
+ }
230
+ return request;
231
+ }
232
+ function validSessionId(value) {
233
+ return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
234
+ }
235
+
236
+ // src/session/startup-failure.ts
237
+ function sessionStartupFailure(result2, fallback) {
238
+ if (result2.ok || result2.errorCode !== "AGENT_EXITED_DURING_STARTUP") return void 0;
239
+ const context = result2.errorContext ?? {};
240
+ return {
241
+ sessionId: typeof context.sessionId === "string" ? context.sessionId : fallback.sessionId,
242
+ agent: fallback.agent,
243
+ ...typeof context.exitStatus === "number" ? { exitStatus: context.exitStatus } : {},
244
+ terminalExcerpt: typeof context.terminalExcerpt === "string" && context.terminalExcerpt.trim() ? context.terminalExcerpt : "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002"
245
+ };
246
+ }
247
+
248
+ // src/session/reconciler.ts
249
+ var SessionReconciler = class {
250
+ constructor(tmux, sessionPrefix = "lark-coding-assistant", missingThreshold = 3, log = async () => void 0, resolveAgentVersion = async () => "unknown") {
251
+ this.tmux = tmux;
252
+ this.sessionPrefix = sessionPrefix;
253
+ this.missingThreshold = missingThreshold;
254
+ this.log = log;
255
+ this.resolveAgentVersion = resolveAgentVersion;
256
+ }
257
+ tmux;
258
+ sessionPrefix;
259
+ missingThreshold;
260
+ log;
261
+ resolveAgentVersion;
262
+ misses = /* @__PURE__ */ new Map();
263
+ async reconcile(input, discover = false) {
264
+ const sessions = { ...input.sessions };
265
+ let changed = false;
266
+ for (const [id, session] of Object.entries(sessions)) {
267
+ const result2 = await this.confirm(session);
268
+ if (result2.status === "unavailable") {
269
+ await this.log(`tmux inspection unavailable for ${id}: ${errorMessage2(result2.error)}`);
270
+ continue;
271
+ }
272
+ if (result2.status === "live") {
273
+ this.misses.delete(id);
274
+ if (result2.pane.paneId !== session.paneId || result2.pane.sessionName !== session.sessionName) {
275
+ sessions[id] = { ...session, paneId: result2.pane.paneId, sessionName: result2.pane.sessionName, updatedAt: Date.now() };
276
+ changed = true;
277
+ }
278
+ continue;
279
+ }
280
+ const misses = (this.misses.get(id) ?? 0) + 1;
281
+ this.misses.set(id, misses);
282
+ if (misses < this.missingThreshold) continue;
283
+ delete sessions[id];
284
+ this.misses.delete(id);
285
+ changed = true;
286
+ }
287
+ if (discover) {
288
+ changed = await this.discover(sessions) || changed;
289
+ }
290
+ const activeSessionId = input.activeSessionId && sessions[input.activeSessionId] ? input.activeSessionId : Object.keys(sessions)[0];
291
+ if (activeSessionId !== input.activeSessionId) changed = true;
292
+ const removedActive = input.activeSessionId && !sessions[input.activeSessionId] ? input.sessions?.[input.activeSessionId] : void 0;
293
+ const state = changed ? { ...input, sessions, activeSessionId, updatedAt: Date.now() } : input;
294
+ return { state, liveSessions: Object.values(sessions), removedActive, changed };
295
+ }
296
+ async confirm(session) {
297
+ const direct = await this.tmux.inspectStatus(session.paneId);
298
+ if (direct.status === "live" || direct.status === "unavailable") return direct;
299
+ const byName = await this.tmux.inspectSession(session.sessionName);
300
+ if (byName.status === "live" || byName.status === "unavailable") return byName;
301
+ return byName.status === "dead" ? byName : direct;
302
+ }
303
+ async discover(sessions) {
304
+ let panes;
305
+ try {
306
+ panes = await this.tmux.listSessions(`${this.sessionPrefix}-`);
307
+ } catch (error) {
308
+ await this.log(`tmux session discovery unavailable: ${errorMessage2(error)}`);
309
+ return false;
310
+ }
311
+ let changed = false;
312
+ const seen = /* @__PURE__ */ new Set();
313
+ for (const pane of panes) {
314
+ if (seen.has(pane.sessionName) || pane.dead) continue;
315
+ seen.add(pane.sessionName);
316
+ const registered = Object.values(sessions).find((session) => session.sessionName === pane.sessionName);
317
+ if (registered) {
318
+ if (registered.paneId !== pane.paneId) {
319
+ sessions[registered.id] = { ...registered, paneId: pane.paneId, updatedAt: Date.now() };
320
+ changed = true;
321
+ }
322
+ if (!await this.tmux.readMetadata(pane.sessionName)) {
323
+ await this.writeMetadata(pane, registered).catch((error) => this.log(
324
+ `failed to backfill tmux metadata for ${registered.id}: ${errorMessage2(error)}`
325
+ ));
326
+ }
327
+ continue;
328
+ }
329
+ const metadata = await this.tmux.readMetadata(pane.sessionName);
330
+ const recovered = metadata ? this.fromMetadata(pane, metadata) : await this.fromLegacy(pane);
331
+ if (!recovered || sessions[recovered.id]) continue;
332
+ if (!metadata) {
333
+ try {
334
+ await this.writeMetadata(pane, recovered);
335
+ } catch (error) {
336
+ await this.log(`failed to persist recovered tmux metadata for ${recovered.id}: ${errorMessage2(error)}`);
337
+ continue;
338
+ }
339
+ }
340
+ sessions[recovered.id] = recovered;
341
+ this.misses.delete(recovered.id);
342
+ changed = true;
343
+ await this.log(`recovered managed session ${recovered.id} from tmux`);
344
+ }
345
+ return changed;
346
+ }
347
+ fromMetadata(pane, metadata) {
348
+ if (!validSessionId(metadata.sessionId) || pane.sessionName !== `${this.sessionPrefix}-${metadata.sessionId}`) return void 0;
349
+ return {
350
+ id: metadata.sessionId,
351
+ agent: metadata.agent,
352
+ sessionName: pane.sessionName,
353
+ paneId: pane.paneId,
354
+ cwd: metadata.cwd,
355
+ agentVersion: metadata.agentVersion,
356
+ agentSessionId: metadata.agentSessionId,
357
+ updatedAt: Date.now()
358
+ };
359
+ }
360
+ async fromLegacy(pane) {
361
+ const id = pane.sessionName.startsWith(`${this.sessionPrefix}-`) ? pane.sessionName.slice(this.sessionPrefix.length + 1) : "";
362
+ const agent = inferLegacyAgent(pane);
363
+ if (!validSessionId(id) || !agent) return void 0;
364
+ const agentVersion = await this.resolveAgentVersion(agent).catch(() => "unknown");
365
+ return {
366
+ id,
367
+ agent,
368
+ sessionName: pane.sessionName,
369
+ paneId: pane.paneId,
370
+ cwd: pane.cwd,
371
+ agentVersion,
372
+ updatedAt: Date.now()
373
+ };
374
+ }
375
+ writeMetadata(pane, session) {
376
+ return this.tmux.writeMetadata(pane.sessionName, {
377
+ managed: true,
378
+ sessionId: session.id,
379
+ agent: session.agent,
380
+ cwd: session.cwd,
381
+ agentVersion: session.agentVersion,
382
+ agentSessionId: session.agentSessionId
383
+ });
384
+ }
385
+ };
386
+ function inferLegacyAgent(pane) {
387
+ const command = `${pane.startCommand}
388
+ ${pane.currentCommand}`;
389
+ if (/(?:^|[\s/'"])(?:trae-cli|traex)(?:[\s/'"]|$)/i.test(command)) return "traex";
390
+ if (/(?:^|[\s/'"])claude(?:[\s/'"]|$)/i.test(command)) return "claude";
391
+ if (/(?:^|[\s/'"])codex(?:[\s/'"]|$)/i.test(command)) return "codex";
392
+ return void 0;
393
+ }
394
+ function errorMessage2(error) {
395
+ return error instanceof Error ? error.message : String(error);
396
+ }
397
+
398
+ // src/session/native-session.ts
399
+ import { readdir, readFile as readFile2 } from "fs/promises";
400
+ import { homedir } from "os";
401
+ import { join } from "path";
402
+ var UUID = "[0-9a-fA-F-]{32,36}";
403
+ async function resolveNativeAgentSessionId(agent, pid, home = homedir()) {
404
+ if (!Number.isInteger(pid) || pid <= 0) return void 0;
405
+ if (agent === "traex") {
406
+ const peer = await resolveTraexPeer(pid, home);
407
+ if (peer) return peer;
408
+ return matchPath(
409
+ await processOpenFiles(pid),
410
+ new RegExp(`/\\.trae/cli/sessions/.+/rollout-[^/]+-(${UUID})\\.jsonl(?:\\.lock)?$`)
411
+ );
412
+ }
413
+ const openFiles = await processOpenFiles(pid);
414
+ if (agent === "codex") return matchPath(openFiles, new RegExp(`/\\.codex/thread-writer-locks/(${UUID})\\.lock$`));
415
+ return matchPath(openFiles, new RegExp(`/\\.claude/projects/[^/]+/(${UUID})\\.jsonl$`));
416
+ }
417
+ async function resolveTraexPeer(pid, home) {
418
+ const directory = join(home, ".trae", "cli", "session-peers");
419
+ const entries = await readdir(directory).catch(() => []);
420
+ for (const entry of entries) {
421
+ if (!entry.endsWith(".json")) continue;
422
+ const value = await readFile2(join(directory, entry), "utf8").then(JSON.parse).catch(() => void 0);
423
+ if (!value || typeof value !== "object") continue;
424
+ const peer = value;
425
+ if (peer.pid === pid && typeof peer.threadId === "string" && peer.threadId) return peer.threadId;
426
+ }
427
+ return void 0;
428
+ }
429
+ async function processOpenFiles(pid) {
430
+ const result2 = await runFile("lsof", ["-Fn", "-p", String(pid)], { timeoutMs: 3e3 }).catch(() => void 0);
431
+ if (!result2) return [];
432
+ return result2.stdout.split("\n").filter((line) => line.startsWith("n")).map((line) => line.slice(1));
433
+ }
434
+ function matchPath(paths2, pattern) {
435
+ for (const path of paths2) {
436
+ const match = path.match(pattern);
437
+ if (match?.[1]) return match[1];
438
+ }
439
+ return void 0;
440
+ }
174
441
 
175
442
  // src/tmux/controller.ts
176
443
  import { randomBytes as randomBytes2 } from "crypto";
@@ -189,7 +456,15 @@ function shellQuote(value) {
189
456
  }
190
457
 
191
458
  // src/tmux/controller.ts
192
- var PANE_FORMAT = "#{session_name} #{pane_id} #{pane_pid} #{pane_current_command} #{pane_current_path} #{pane_dead} #{cursor_x} #{cursor_y}";
459
+ var PANE_FORMAT = "#{session_name} #{pane_id} #{pane_pid} #{pane_start_command} #{pane_current_command} #{pane_current_path} #{pane_dead} #{pane_dead_status} #{cursor_x} #{cursor_y}";
460
+ var METADATA_OPTIONS = {
461
+ managed: "@lca-managed",
462
+ sessionId: "@lca-session-id",
463
+ agent: "@lca-agent",
464
+ cwd: "@lca-cwd",
465
+ agentVersion: "@lca-agent-version",
466
+ agentSessionId: "@lca-agent-session-id"
467
+ };
193
468
  var TmuxController = class {
194
469
  constructor(binary = "tmux") {
195
470
  this.binary = binary;
@@ -223,7 +498,7 @@ var TmuxController = class {
223
498
  options.binary,
224
499
  ...options.args ?? []
225
500
  ].map(shellQuote).join(" ");
226
- await runFile(this.binary, [
501
+ const createArgs = [
227
502
  "new-session",
228
503
  "-d",
229
504
  "-s",
@@ -235,7 +510,11 @@ var TmuxController = class {
235
510
  "-y",
236
511
  "40",
237
512
  command
238
- ]);
513
+ ];
514
+ if (options.preserveOnExit) {
515
+ createArgs.push(";", "set-option", "-w", "-t", `=${options.sessionName}:`, "remain-on-exit", "on");
516
+ }
517
+ await runFile(this.binary, createArgs);
239
518
  const pane = await this.findBySession(options.sessionName);
240
519
  if (!pane) throw new Error("tmux created a session without a discoverable pane");
241
520
  return pane;
@@ -259,6 +538,10 @@ var TmuxController = class {
259
538
  return stdout.split("\n").map(parsePane).find(Boolean);
260
539
  }
261
540
  async inspect(paneId) {
541
+ const result2 = await this.inspectStatus(paneId);
542
+ return result2.status === "live" || result2.status === "dead" ? result2.pane : void 0;
543
+ }
544
+ async inspectStatus(paneId) {
262
545
  assertSafeTmuxTarget(paneId);
263
546
  try {
264
547
  const { stdout } = await runFile(this.binary, [
@@ -268,11 +551,65 @@ var TmuxController = class {
268
551
  paneId,
269
552
  PANE_FORMAT
270
553
  ]);
271
- return parsePane(stdout.trim());
272
- } catch {
273
- return void 0;
554
+ if (!stdout.trim()) return { status: "missing" };
555
+ const pane = parsePane(stdout.trim());
556
+ if (!pane) return { status: "unavailable", error: new Error("invalid tmux pane response") };
557
+ return pane.dead ? { status: "dead", pane } : { status: "live", pane };
558
+ } catch (error) {
559
+ return tmuxTargetMissing(error) ? { status: "missing" } : { status: "unavailable", error };
560
+ }
561
+ }
562
+ async inspectSession(sessionName) {
563
+ try {
564
+ const pane = await this.findBySession(sessionName);
565
+ if (!pane) return { status: "missing" };
566
+ return pane.dead ? { status: "dead", pane } : { status: "live", pane };
567
+ } catch (error) {
568
+ return tmuxTargetMissing(error) ? { status: "missing" } : { status: "unavailable", error };
274
569
  }
275
570
  }
571
+ async listSessions(prefix) {
572
+ const { stdout } = await runFile(this.binary, ["list-panes", "-a", "-F", PANE_FORMAT]);
573
+ return stdout.split("\n").map(parsePane).filter((pane) => Boolean(pane?.sessionName.startsWith(prefix)));
574
+ }
575
+ async writeMetadata(sessionName, metadata) {
576
+ const values = {
577
+ managed: "1",
578
+ sessionId: metadata.sessionId,
579
+ agent: metadata.agent,
580
+ cwd: metadata.cwd,
581
+ agentVersion: metadata.agentVersion,
582
+ agentSessionId: metadata.agentSessionId
583
+ };
584
+ for (const [key, option2] of Object.entries(METADATA_OPTIONS)) {
585
+ const value = values[key];
586
+ if (value === void 0) {
587
+ await runFile(this.binary, ["set-option", "-u", "-t", sessionName, option2]).catch(() => void 0);
588
+ } else {
589
+ await runFile(this.binary, ["set-option", "-t", sessionName, option2, value]);
590
+ }
591
+ }
592
+ }
593
+ async readMetadata(sessionName) {
594
+ const values = {};
595
+ for (const [key, option2] of Object.entries(METADATA_OPTIONS)) {
596
+ const result2 = await runFile(this.binary, ["show-options", "-t", sessionName, "-v", option2]).catch(() => void 0);
597
+ if (!result2) {
598
+ if (key === "agentSessionId") continue;
599
+ return void 0;
600
+ }
601
+ values[key] = result2.stdout.trim();
602
+ }
603
+ if (values.managed !== "1" || !values.sessionId || !values.cwd || !values.agentVersion || values.agent !== "codex" && values.agent !== "traex" && values.agent !== "claude") return void 0;
604
+ return {
605
+ managed: true,
606
+ sessionId: values.sessionId,
607
+ agent: values.agent,
608
+ cwd: values.cwd,
609
+ agentVersion: values.agentVersion,
610
+ agentSessionId: values.agentSessionId || void 0
611
+ };
612
+ }
276
613
  async capture(paneId, lines = 200) {
277
614
  assertSafeTmuxTarget(paneId);
278
615
  const { stdout } = await runFile(this.binary, [
@@ -287,6 +624,16 @@ var TmuxController = class {
287
624
  ]);
288
625
  return stdout;
289
626
  }
627
+ async preserveOnExit(sessionName, enabled) {
628
+ await runFile(this.binary, [
629
+ "set-option",
630
+ "-w",
631
+ "-t",
632
+ `=${sessionName}:`,
633
+ "remain-on-exit",
634
+ enabled ? "on" : "off"
635
+ ]);
636
+ }
290
637
  sendText(paneId, input, submit = true) {
291
638
  assertSafeTmuxTarget(paneId);
292
639
  const text = sanitizeRemoteInput(input);
@@ -306,25 +653,47 @@ var TmuxController = class {
306
653
  }
307
654
  async sendKey(paneId, key) {
308
655
  assertSafeTmuxTarget(paneId);
309
- if (!/^(Enter|Escape|Space|Tab|BSpace|Up|Down|Left|Right|C-c|C-u|C-k|C-Enter|[yandpcq1-9])$/.test(key)) {
656
+ if (!/^(Enter|Escape|Space|Tab|BSpace|Up|Down|Left|Right|PPage|NPage|C-c|C-u|C-k|C-Enter|[yandpcq1-9])$/.test(key)) {
310
657
  throw new Error(`unsupported tmux key: ${key}`);
311
658
  }
312
659
  await runFile(this.binary, ["send-keys", "-t", paneId, key]);
313
660
  }
314
661
  async killSession(sessionName) {
315
- await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`]);
662
+ try {
663
+ await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`]);
664
+ } catch (error) {
665
+ if (!tmuxTargetMissing(error)) throw error;
666
+ }
316
667
  }
317
668
  };
318
669
  function displaySessionId(sessionName) {
319
670
  return sessionName.replace(/^lark-coding-assistant-/, "");
320
671
  }
321
672
  function parsePane(line) {
322
- const [sessionName, paneId, pidText, currentCommand, cwd, deadText, cursorXText, cursorYText] = line.trim().split(" ");
673
+ const [sessionName, paneId, pidText, startCommand, currentCommand, cwd, deadText, exitStatusText, cursorXText, cursorYText] = line.trim().split(" ");
323
674
  const pid = Number(pidText);
324
675
  const cursorX = Number(cursorXText);
325
676
  const cursorY = Number(cursorYText);
326
- if (!sessionName || !paneId || !Number.isInteger(pid) || !currentCommand || !cwd || !Number.isInteger(cursorX) || !Number.isInteger(cursorY)) return void 0;
327
- return { sessionName, paneId, pid, currentCommand, cwd, dead: deadText === "1", cursorX, cursorY };
677
+ if (!sessionName || !paneId || !Number.isInteger(pid) || !startCommand || !currentCommand || !cwd && deadText !== "1" || !Number.isInteger(cursorX) || !Number.isInteger(cursorY)) return void 0;
678
+ const exitStatus = exitStatusText === "" ? void 0 : Number(exitStatusText);
679
+ return {
680
+ sessionName,
681
+ paneId,
682
+ pid,
683
+ startCommand,
684
+ currentCommand,
685
+ cwd: cwd || "/",
686
+ dead: deadText === "1",
687
+ exitStatus: Number.isInteger(exitStatus) ? exitStatus : void 0,
688
+ cursorX,
689
+ cursorY
690
+ };
691
+ }
692
+ function tmuxTargetMissing(error) {
693
+ const message = error instanceof Error ? error.message : String(error);
694
+ const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr) : "";
695
+ return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found/i.test(`${message}
696
+ ${stderr}`);
328
697
  }
329
698
 
330
699
  // src/screen/detector.ts
@@ -348,12 +717,12 @@ var CODEX_DIALECT = {
348
717
  };
349
718
  var TRAE_DIALECT = {
350
719
  ...CODEX_DIALECT,
351
- id: "trae-cli",
720
+ id: "traex",
352
721
  headerPatterns: [...commonHeaders, /^\s*Question\s+\d+\/\d+/i],
353
722
  customInputControls: [/^(?:other|none of the above|add notes)$/i]
354
723
  };
355
724
  var CLAUDE_DIALECT = {
356
- id: "claude-code",
725
+ id: "claude",
357
726
  headerPatterns: [
358
727
  ...commonHeaders,
359
728
  /^\s*(?:←\s*)?[☐☑☒]\s+.+?(?:\s+✔\s+Submit\s*→)?\s*$/i,
@@ -701,11 +1070,18 @@ function claudeResumeArgs(resume) {
701
1070
  // src/agents/stop-hook.ts
702
1071
  function codexStyleStopHookArgs(command) {
703
1072
  const hook = `{hooks=[{type="command",command=${JSON.stringify(command)},timeout=5}]}`;
704
- return ["--dangerously-bypass-hook-trust", "-c", `hooks.Stop=[${hook}]`];
1073
+ return [
1074
+ "--dangerously-bypass-hook-trust",
1075
+ "-c",
1076
+ `hooks.SessionStart=[${hook}]`,
1077
+ "-c",
1078
+ `hooks.Stop=[${hook}]`
1079
+ ];
705
1080
  }
706
1081
  function claudeStopHookArgs(command) {
707
1082
  return ["--settings", JSON.stringify({
708
1083
  hooks: {
1084
+ SessionStart: [{ hooks: [{ type: "command", command, timeout: 5 }] }],
709
1085
  Stop: [{ hooks: [{ type: "command", command, timeout: 5 }] }]
710
1086
  }
711
1087
  })];
@@ -714,7 +1090,7 @@ function claudeStopHookArgs(command) {
714
1090
  // src/agents/codex.ts
715
1091
  var codexAdapter = {
716
1092
  id: "codex",
717
- displayName: "Codex",
1093
+ displayName: "codex",
718
1094
  groupOrder: 10,
719
1095
  binary: (config) => config.agentBinaries.codex,
720
1096
  versionArgs: ["--version"],
@@ -727,10 +1103,10 @@ var codexAdapter = {
727
1103
 
728
1104
  // src/agents/trae-cli.ts
729
1105
  var traeCliAdapter = {
730
- id: "trae-cli",
731
- displayName: "Trae CLI",
1106
+ id: "traex",
1107
+ displayName: "traex",
732
1108
  groupOrder: 20,
733
- binary: (config) => config.agentBinaries["trae-cli"],
1109
+ binary: (config) => config.agentBinaries.traex,
734
1110
  versionArgs: ["--version"],
735
1111
  buildLaunchArgs: ({ resume, stopHookCommand }) => [
736
1112
  ...codexStyleStopHookArgs(stopHookCommand),
@@ -741,10 +1117,10 @@ var traeCliAdapter = {
741
1117
 
742
1118
  // src/agents/claude-code.ts
743
1119
  var claudeCodeAdapter = {
744
- id: "claude-code",
745
- displayName: "Claude Code",
1120
+ id: "claude",
1121
+ displayName: "claude",
746
1122
  groupOrder: 30,
747
- binary: (config) => config.agentBinaries["claude-code"],
1123
+ binary: (config) => config.agentBinaries.claude,
748
1124
  versionArgs: ["--version"],
749
1125
  buildLaunchArgs: ({ resume, stopHookCommand }) => [
750
1126
  ...claudeStopHookArgs(stopHookCommand),
@@ -753,9 +1129,6 @@ var claudeCodeAdapter = {
753
1129
  detectScreen: detectClaudeScreen
754
1130
  };
755
1131
 
756
- // src/agents/types.ts
757
- var AGENT_IDS = ["codex", "trae-cli", "claude-code"];
758
-
759
1132
  // src/agents/registry.ts
760
1133
  var adapters = /* @__PURE__ */ new Map([
761
1134
  [codexAdapter.id, codexAdapter],
@@ -771,10 +1144,15 @@ function listAgentAdapters() {
771
1144
  return [...adapters.values()].sort((left, right) => left.groupOrder - right.groupOrder);
772
1145
  }
773
1146
  function isAgentId(value) {
774
- return AGENT_IDS.includes(value);
1147
+ return normalizeAgentId(value) === value;
775
1148
  }
776
1149
 
777
1150
  // src/agents/stop-event.ts
1151
+ function validSessionStartCandidate(value) {
1152
+ if (!value || typeof value !== "object") return false;
1153
+ const candidate = value;
1154
+ return typeof candidate.sessionId === "string" && candidate.sessionId.length > 0 && (candidate.agent === "codex" || candidate.agent === "traex" || candidate.agent === "claude") && typeof candidate.agentSessionId === "string" && candidate.agentSessionId.length > 0 && typeof candidate.cwd === "string" && candidate.cwd.length > 0;
1155
+ }
778
1156
  function validTurnCompleteCandidate(value) {
779
1157
  if (!value || typeof value !== "object") return false;
780
1158
  const candidate = value;
@@ -852,11 +1230,10 @@ function withoutSignature(value) {
852
1230
  function isSignedAction(value) {
853
1231
  if (!value || typeof value !== "object") return false;
854
1232
  const item = value;
855
- return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "manual") && (item.kind !== "choice" || item.interactionKind === "approval" || item.interactionKind === "question" || item.interactionKind === "choice") && (item.kind !== "manual" || typeof item.sessionId === "string" && (item.manualMode === "explicit" || item.manualMode === "fallback")) && typeof item.agent === "string" && isAgentId(item.agent) && typeof item.action === "string" && typeof item.paneId === "string" && typeof item.fingerprint === "string" && typeof item.chatId === "string" && typeof item.nonce === "string" && typeof item.expiresAt === "number" && typeof item.sig === "string";
1233
+ return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "session-stop" || item.kind === "session-create" || item.kind === "session-start-error" || item.kind === "startup-conflict" || item.kind === "resume-picker" || item.kind === "manual") && (item.kind !== "choice" || item.interactionKind === "approval" || item.interactionKind === "question" || item.interactionKind === "choice") && (item.kind !== "manual" || typeof item.sessionId === "string" && (item.manualMode === "explicit" || item.manualMode === "fallback")) && (item.kind !== "session-stop" && item.kind !== "session-start-error" && item.kind !== "resume-picker" && item.kind !== "startup-conflict" || typeof item.sessionId === "string") && typeof item.agent === "string" && isAgentId(item.agent) && typeof item.action === "string" && typeof item.paneId === "string" && typeof item.fingerprint === "string" && typeof item.chatId === "string" && typeof item.nonce === "string" && typeof item.expiresAt === "number" && typeof item.sig === "string";
856
1234
  }
857
1235
 
858
1236
  // src/lark/cards.ts
859
- import { homedir } from "os";
860
1237
  function choiceCard(chatId, paneId, screen, signer, agent = "codex") {
861
1238
  if (!screen.interaction) throw new Error("choice card requires a structured interaction");
862
1239
  const interaction = screen.interaction;
@@ -1011,37 +1388,37 @@ function interactionInputCard(agent, label) {
1011
1388
  var MANUAL_TEXT_FIELD = "manual_text";
1012
1389
  var MANUAL_TYPE_ACTION = "type";
1013
1390
  var MANUAL_SUBMIT_ACTION = "submit";
1014
- function manualControlCard(chatId, view, signer) {
1015
- const state = view.state ?? "active";
1016
- const mode = view.mode ?? "fallback";
1017
- const agentName = getAgentAdapter(view.session.agent).displayName;
1018
- const fingerprint = view.screen.fingerprint;
1391
+ function manualControlCard(chatId, view2, signer) {
1392
+ const state = view2.state ?? "active";
1393
+ const mode = view2.mode ?? "fallback";
1394
+ const agentName = getAgentAdapter(view2.session.agent).displayName;
1395
+ const fingerprint = view2.screen.fingerprint;
1019
1396
  const sign = (action) => signer.sign({
1020
1397
  kind: "manual",
1021
- sessionId: view.session.id,
1398
+ sessionId: view2.session.id,
1022
1399
  manualMode: mode,
1023
- agent: view.session.agent,
1400
+ agent: view2.session.agent,
1024
1401
  action,
1025
- paneId: view.session.paneId,
1402
+ paneId: view2.session.paneId,
1026
1403
  fingerprint,
1027
1404
  chatId
1028
1405
  }, 10 * 6e4);
1029
- const status = state === "recovered" ? "\u2705 \u5DF2\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\uFF0C\u8BF7\u4F7F\u7528\u65B0\u53D1\u9001\u7684\u8BED\u4E49\u5316\u64CD\u4F5C\u5361\u3002" : state === "exited" ? "\u2705 \u5DF2\u9000\u51FA\u624B\u52A8\u9065\u63A7\u6A21\u5F0F\uFF0C\u672C\u5730 Agent \u548C tmux \u4ECD\u5728\u8FD0\u884C\u3002" : state === "stale" ? "\u26A0\uFE0F \u7EC8\u7AEF\u753B\u9762\u5DF2\u53D8\u5316\uFF0C\u65E7\u64CD\u4F5C\u672A\u6267\u884C\uFF1B\u8BF7\u786E\u8BA4\u6700\u65B0\u753B\u9762\u540E\u91CD\u8BD5\u3002" : state === "error" ? `\u26A0\uFE0F ${escapeMarkdown(view.notice ?? "\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u6700\u65B0\u753B\u9762\u3002")}` : mode === "explicit" ? "\u26A0\uFE0F \u5DF2\u9501\u5B9A\u624B\u52A8\u9065\u63A7\uFF1B\u5373\u4F7F\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\uFF0C\u4E5F\u4F1A\u7EE7\u7EED\u7531\u4F60\u76F4\u63A5\u64CD\u4F5C\u7EC8\u7AEF\u3002" : "\u26A0\uFE0F \u5F53\u524D\u4E3A\u624B\u52A8\u9065\u63A7\u6A21\u5F0F\uFF0C\u6240\u6709\u64CD\u4F5C\u90FD\u4E0D\u4F1A\u8FDB\u884C\u8BED\u4E49\u5B89\u5168\u5224\u65AD\u3002";
1406
+ const status = state === "recovered" ? "\u2705 \u5DF2\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\uFF0C\u8BF7\u4F7F\u7528\u65B0\u53D1\u9001\u7684\u8BED\u4E49\u5316\u64CD\u4F5C\u5361\u3002" : state === "exited" ? "\u2705 \u5DF2\u9000\u51FA\u624B\u52A8\u9065\u63A7\u6A21\u5F0F\uFF0C\u672C\u5730 Agent \u548C tmux \u4ECD\u5728\u8FD0\u884C\u3002" : state === "stale" ? "\u26A0\uFE0F \u7EC8\u7AEF\u753B\u9762\u5DF2\u53D8\u5316\uFF0C\u65E7\u64CD\u4F5C\u672A\u6267\u884C\uFF1B\u8BF7\u786E\u8BA4\u6700\u65B0\u753B\u9762\u540E\u91CD\u8BD5\u3002" : state === "error" ? `\u26A0\uFE0F ${escapeMarkdown(view2.notice ?? "\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u6700\u65B0\u753B\u9762\u3002")}` : mode === "explicit" ? "\u26A0\uFE0F \u5DF2\u9501\u5B9A\u624B\u52A8\u9065\u63A7\uFF1B\u5373\u4F7F\u6062\u590D\u7ED3\u6784\u5316\u8BC6\u522B\uFF0C\u4E5F\u4F1A\u7EE7\u7EED\u7531\u4F60\u76F4\u63A5\u64CD\u4F5C\u7EC8\u7AEF\u3002" : "\u26A0\uFE0F \u5F53\u524D\u4E3A\u624B\u52A8\u9065\u63A7\u6A21\u5F0F\uFF0C\u6240\u6709\u64CD\u4F5C\u90FD\u4E0D\u4F1A\u8FDB\u884C\u8BED\u4E49\u5B89\u5168\u5224\u65AD\u3002";
1030
1407
  const metadata = [
1031
- `**Session** \`${escapeInlineCode(view.session.id)}\` \xB7 **Agent** ${escapeMarkdown(agentName)}`,
1032
- `**\u72B6\u6001** \`${view.screen.state}\` \xB7 **\u91C7\u96C6\u65F6\u95F4** ${formatHandledAt(view.capturedAt)}`,
1033
- view.lastOperation ? `**\u6700\u8FD1\u64CD\u4F5C** ${escapeMarkdown(view.lastOperation)}` : void 0,
1034
- view.notice && state !== "error" ? escapeMarkdown(view.notice) : void 0
1408
+ `**Session** \`${escapeInlineCode(view2.session.id)}\` \xB7 **Agent** ${escapeMarkdown(agentName)}`,
1409
+ `**\u72B6\u6001** \`${view2.screen.state}\` \xB7 **\u91C7\u96C6\u65F6\u95F4** ${formatHandledAt(view2.capturedAt)}`,
1410
+ view2.lastOperation ? `**\u6700\u8FD1\u64CD\u4F5C** ${escapeMarkdown(view2.lastOperation)}` : void 0,
1411
+ view2.notice && state !== "error" ? escapeMarkdown(view2.notice) : void 0
1035
1412
  ].filter(Boolean).join("\n");
1036
1413
  const elements = [
1037
1414
  { tag: "markdown", content: status },
1038
1415
  { tag: "markdown", content: metadata },
1039
1416
  { tag: "markdown", content: `\`\`\`text
1040
- ${escapeFence(view.output).slice(-5e3)}
1417
+ ${escapeFence(view2.output).slice(-5e3)}
1041
1418
  \`\`\`` }
1042
1419
  ];
1043
1420
  if (state === "recovered" || state === "exited") {
1044
- return cardElements(`${view.session.id} \xB7 ${agentName} \u624B\u52A8\u9065\u63A7`, elements, state === "recovered" ? "green" : "grey");
1421
+ return cardElements(`${view2.session.id} \xB7 ${agentName} \u624B\u52A8\u9065\u63A7`, elements, state === "recovered" ? "green" : "grey");
1045
1422
  }
1046
1423
  elements.push(
1047
1424
  manualButtonRow([
@@ -1086,7 +1463,7 @@ ${escapeFence(view.output).slice(-5e3)}
1086
1463
  manualButton("\u7ED3\u675F\u9065\u63A7", sign("exit"))
1087
1464
  ])
1088
1465
  );
1089
- return cardElements(`${view.session.id} \xB7 ${agentName} \u624B\u52A8\u9065\u63A7`, elements, "orange");
1466
+ return cardElements(`${view2.session.id} \xB7 ${agentName} \u624B\u52A8\u9065\u63A7`, elements, "orange");
1090
1467
  }
1091
1468
  function manualButton(label, value, type = "default") {
1092
1469
  return {
@@ -1142,7 +1519,7 @@ function statusCard(status) {
1142
1519
  }
1143
1520
  const adapter = getAgentAdapter(session.agent);
1144
1521
  const visual = statusVisual(status);
1145
- const path = formatDisplayPath(session.cwd);
1522
+ const path = session.cwd;
1146
1523
  return {
1147
1524
  schema: "2.0",
1148
1525
  config: { update_multi: true },
@@ -1181,7 +1558,7 @@ function statusCard(status) {
1181
1558
  }
1182
1559
  };
1183
1560
  }
1184
- function sessionPickerCard(chatId, sessions, activeSessionId, signer) {
1561
+ function sessionPickerCard(chatId, sessions, activeSessionId, signer, confirmingStopSessionId, allowCreate = true) {
1185
1562
  const groups = listAgentAdapters().map((adapter) => ({ adapter, sessions: sessions.filter((session) => session.agent === adapter.id) })).filter(({ sessions: group }) => group.length > 0);
1186
1563
  const elements = groups.flatMap(({ adapter, sessions: group }, groupIndex) => [
1187
1564
  ...groupIndex > 0 ? [{ tag: "hr", margin: "6px 0px" }] : [],
@@ -1191,8 +1568,37 @@ function sessionPickerCard(chatId, sessions, activeSessionId, signer) {
1191
1568
  text_size: "heading",
1192
1569
  margin: "2px 0px 0px 0px"
1193
1570
  },
1194
- ...group.map((session) => sessionCard(chatId, session, session.id === activeSessionId, signer))
1571
+ ...group.map((session) => sessionCard(
1572
+ chatId,
1573
+ session,
1574
+ session.id === activeSessionId,
1575
+ signer,
1576
+ confirmingStopSessionId === session.id
1577
+ ))
1195
1578
  ]);
1579
+ if (elements.length > 0) elements.push({ tag: "hr", margin: "8px 0px" });
1580
+ else elements.push({ tag: "markdown", content: "\u6682\u65E0\u53EF\u8FDE\u63A5\u7684 Coding Session\uFF0C\u53EF\u4EE5\u76F4\u63A5\u65B0\u5EFA\u4E00\u4E2A\u3002" });
1581
+ elements.push(allowCreate ? {
1582
+ tag: "button",
1583
+ text: { tag: "plain_text", content: "\uFF0B \u65B0\u5EFA Session" },
1584
+ type: "primary",
1585
+ width: "fill",
1586
+ size: "medium",
1587
+ behaviors: [{
1588
+ type: "callback",
1589
+ value: signer.sign({
1590
+ kind: "session-create",
1591
+ agent: "codex",
1592
+ action: "open",
1593
+ paneId: "",
1594
+ fingerprint: "create",
1595
+ chatId
1596
+ }, 10 * 6e4)
1597
+ }]
1598
+ } : {
1599
+ tag: "markdown",
1600
+ content: "\u2705 \u65B0\u5EFA Session \u8868\u5355\u5DF2\u53D1\u9001\uFF1B\u4ECD\u53EF\u67E5\u770B\u6216\u64CD\u4F5C\u4E0A\u65B9 Session\u3002"
1601
+ });
1196
1602
  return {
1197
1603
  schema: "2.0",
1198
1604
  config: { update_multi: true },
@@ -1200,37 +1606,139 @@ function sessionPickerCard(chatId, sessions, activeSessionId, signer) {
1200
1606
  body: {
1201
1607
  vertical_spacing: "10px",
1202
1608
  padding: "12px",
1203
- elements: elements.length > 0 ? elements : [{ tag: "markdown", content: "\u6682\u65E0\u53EF\u8FDE\u63A5\u7684 Coding Session\u3002" }]
1609
+ elements
1204
1610
  }
1205
1611
  };
1206
1612
  }
1207
- function sessionCard(chatId, session, active, signer) {
1613
+ var SESSION_CREATE_SUBMIT_ACTION = "session_create_submit";
1614
+ var SESSION_CREATE_NAME_FIELD = "session_name";
1615
+ var SESSION_CREATE_AGENT_FIELD = "session_agent";
1616
+ var SESSION_CREATE_CWD_FIELD = "session_cwd";
1617
+ var SESSION_CREATE_RESUME_FIELD = "session_resume";
1618
+ function sessionCreateCard() {
1619
+ return cardElements("\u65B0\u5EFA Coding Session", [
1620
+ { tag: "markdown", content: "\u5728\u672C\u673A\u53D7\u7BA1 tmux \u4E2D\u542F\u52A8\u4E00\u4E2A\u65B0\u4F1A\u8BDD\uFF1B\u521B\u5EFA\u6210\u529F\u540E\u4F1A\u81EA\u52A8\u8FDE\u63A5\u3002" },
1621
+ {
1622
+ tag: "form",
1623
+ name: "session_create_form",
1624
+ direction: "vertical",
1625
+ vertical_spacing: "10px",
1626
+ elements: [
1627
+ {
1628
+ tag: "input",
1629
+ name: SESSION_CREATE_NAME_FIELD,
1630
+ required: true,
1631
+ placeholder: { tag: "plain_text", content: "Session \u540D\u79F0\uFF0C\u4F8B\u5982 helix" },
1632
+ label: { tag: "plain_text", content: "Session \u540D\u79F0" }
1633
+ },
1634
+ {
1635
+ tag: "select_static",
1636
+ name: SESSION_CREATE_AGENT_FIELD,
1637
+ required: true,
1638
+ placeholder: { tag: "plain_text", content: "\u9009\u62E9 Agent" },
1639
+ initial_option: "codex",
1640
+ options: listAgentAdapters().map((adapter) => ({
1641
+ text: { tag: "plain_text", content: adapter.displayName },
1642
+ value: adapter.id
1643
+ }))
1644
+ },
1645
+ {
1646
+ tag: "input",
1647
+ name: SESSION_CREATE_CWD_FIELD,
1648
+ required: true,
1649
+ placeholder: { tag: "plain_text", content: "/absolute/path/to/project" },
1650
+ label: { tag: "plain_text", content: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u5FC5\u987B\u4E3A\u7EDD\u5BF9\u8DEF\u5F84\uFF09" }
1651
+ },
1652
+ {
1653
+ tag: "select_static",
1654
+ name: SESSION_CREATE_RESUME_FIELD,
1655
+ placeholder: { tag: "plain_text", content: "\u9009\u62E9\u542F\u52A8\u65B9\u5F0F" },
1656
+ initial_option: "new",
1657
+ options: [
1658
+ { text: { tag: "plain_text", content: "\u65B0\u4F1A\u8BDD" }, value: "new" },
1659
+ { text: { tag: "plain_text", content: "\u6253\u5F00\u539F\u751F Resume Picker" }, value: "picker" }
1660
+ ]
1661
+ },
1662
+ {
1663
+ tag: "button",
1664
+ name: SESSION_CREATE_SUBMIT_ACTION,
1665
+ text: { tag: "plain_text", content: "\u542F\u52A8\u5E76\u8FDE\u63A5" },
1666
+ type: "primary",
1667
+ width: "fill",
1668
+ size: "medium",
1669
+ form_action_type: "submit"
1670
+ }
1671
+ ]
1672
+ }
1673
+ ]);
1674
+ }
1675
+ function sessionCreateResultCard(success, content, session) {
1676
+ const details = session ? `
1677
+
1678
+ **Session** \`${escapeInlineCode(session.id)}\`
1679
+ **Agent** ${escapeMarkdown(getAgentAdapter(session.agent).displayName)}
1680
+ **\u5DE5\u4F5C\u76EE\u5F55** \`${escapeInlineCode(session.cwd)}\`` : "";
1681
+ return cardElements(
1682
+ success ? "Session \u5DF2\u542F\u52A8" : "Session \u542F\u52A8\u5931\u8D25",
1683
+ [{ tag: "markdown", content: `${success ? "\u2705" : "\u26A0\uFE0F"} ${escapeMarkdown(content)}${details}` }],
1684
+ success ? "green" : "red"
1685
+ );
1686
+ }
1687
+ function sessionCreateProgressCard() {
1688
+ return cardElements("\u6B63\u5728\u542F\u52A8 Session", [
1689
+ { tag: "markdown", content: "\u23F3 \u6B63\u5728\u521B\u5EFA tmux pane\uFF0C\u5E76\u786E\u8BA4 Agent \u548C\u539F\u751F Session \u72B6\u6001\u2026" }
1690
+ ], "blue");
1691
+ }
1692
+ function sessionCreateOpenedCard() {
1693
+ return cardElements("\u65B0\u5EFA\u8868\u5355\u5DF2\u6253\u5F00", [{
1694
+ tag: "markdown",
1695
+ content: "\u2705 \u8BF7\u5728\u6700\u65B0\u53D1\u9001\u7684\u201C\u65B0\u5EFA Coding Session\u201D\u5361\u7247\u4E2D\u7EE7\u7EED\u64CD\u4F5C\u3002\n\n\u5982\u9700\u67E5\u770B Session\uFF0C\u8BF7\u53D1\u9001 `/sessions`\u3002"
1696
+ }], "grey");
1697
+ }
1698
+ function sessionCreateFailureCard(chatId, content, signer) {
1699
+ return cardElements("Session \u542F\u52A8\u5931\u8D25", [
1700
+ { tag: "markdown", content: `\u26A0\uFE0F ${escapeMarkdown(content)}` },
1701
+ ...sessionFailureActions(chatId, "", "codex", signer)
1702
+ ], "red");
1703
+ }
1704
+ function sessionStartupFailureCard(chatId, failure, signer) {
1705
+ const exitStatus = failure.exitStatus === void 0 ? "\u672A\u77E5" : String(failure.exitStatus);
1706
+ return cardElements("Session \u542F\u52A8\u5931\u8D25", [
1707
+ {
1708
+ tag: "markdown",
1709
+ content: `\u26A0\uFE0F **Session** \`${escapeInlineCode(failure.sessionId)}\`
1710
+ **Agent** ${escapeMarkdown(getAgentAdapter(failure.agent).displayName)}
1711
+ **\u9000\u51FA\u7801** \`${exitStatus}\``
1712
+ },
1713
+ { tag: "markdown", content: `**\u539F\u59CB\u9519\u8BEF**
1714
+
1715
+ \`\`\`text
1716
+ ${escapeFence(failure.terminalExcerpt)}
1717
+ \`\`\`` },
1718
+ ...sessionFailureActions(chatId, failure.sessionId, failure.agent, signer)
1719
+ ], "red");
1720
+ }
1721
+ function sessionFailureActions(chatId, sessionId, agent, signer) {
1722
+ const common = {
1723
+ kind: "session-start-error",
1724
+ sessionId,
1725
+ agent,
1726
+ paneId: "",
1727
+ fingerprint: "startup-error",
1728
+ chatId
1729
+ };
1730
+ return [
1731
+ actionButton("\u65B0\u5EFA Session", "primary", signer.sign({ ...common, action: "create" }, 10 * 6e4)),
1732
+ actionButton("\u67E5\u770B Sessions", "default", signer.sign({ ...common, action: "sessions" }, 10 * 6e4))
1733
+ ];
1734
+ }
1735
+ function sessionCard(chatId, session, active, signer, confirmingStop = false) {
1208
1736
  const content = [
1209
1737
  active ? `**${escapeMarkdown(session.id)}** <text_tag color='green'>\u25CF \u5F53\u524D\u8FDE\u63A5</text_tag>` : `**${escapeMarkdown(session.id)}**`,
1210
- `\u{1F4C1} \`${escapeInlineCode(formatDisplayPath(session.cwd))}\``
1738
+ `\u{1F4C1} \`${escapeInlineCode(session.cwd)}\``
1211
1739
  ].join("\n\n");
1212
1740
  const elements = [{ tag: "markdown", content }];
1213
- if (!active) {
1214
- elements.push({
1215
- tag: "button",
1216
- text: { tag: "plain_text", content: `\u8FDE\u63A5 ${session.id}` },
1217
- type: "primary",
1218
- width: "fill",
1219
- size: "medium",
1220
- margin: "2px 0px 0px 0px",
1221
- behaviors: [{
1222
- type: "callback",
1223
- value: signer.sign({
1224
- kind: "session",
1225
- agent: session.agent,
1226
- action: session.id,
1227
- paneId: session.paneId,
1228
- fingerprint: String(session.updatedAt),
1229
- chatId
1230
- })
1231
- }]
1232
- });
1233
- }
1741
+ elements.push(confirmingStop ? sessionStopConfirmation(chatId, session, signer) : sessionActions(chatId, session, active, signer));
1234
1742
  const contentColumn = {
1235
1743
  tag: "column",
1236
1744
  width: "weighted",
@@ -1254,15 +1762,143 @@ function sessionCard(chatId, session, active, signer) {
1254
1762
  }, contentColumn] : [contentColumn]
1255
1763
  };
1256
1764
  }
1257
- function formatDisplayPath(path, home = homedir(), maxLength = 44) {
1258
- const display = path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
1259
- if (display.length <= maxLength) return display;
1260
- const segments = display.split("/").filter(Boolean);
1261
- return segments.length <= 2 ? display : `\u2026/${segments.slice(-2).join("/")}`;
1765
+ function sessionActions(chatId, session, active, signer) {
1766
+ const buttons = [];
1767
+ if (!active) buttons.push(actionButton(`\u8FDE\u63A5 ${session.id}`, "primary", signer.sign({
1768
+ kind: "session",
1769
+ agent: session.agent,
1770
+ action: session.id,
1771
+ paneId: session.paneId,
1772
+ fingerprint: String(session.updatedAt),
1773
+ chatId
1774
+ })));
1775
+ buttons.push(actionButton("\u5173\u95ED", "danger", signer.sign({
1776
+ kind: "session-stop",
1777
+ sessionId: session.id,
1778
+ agent: session.agent,
1779
+ action: "request",
1780
+ paneId: session.paneId,
1781
+ fingerprint: String(session.updatedAt),
1782
+ chatId
1783
+ })));
1784
+ return buttonRow(buttons);
1785
+ }
1786
+ function sessionStopConfirmation(chatId, session, signer) {
1787
+ const common = {
1788
+ kind: "session-stop",
1789
+ sessionId: session.id,
1790
+ agent: session.agent,
1791
+ paneId: session.paneId,
1792
+ fingerprint: String(session.updatedAt),
1793
+ chatId
1794
+ };
1795
+ return {
1796
+ tag: "column_set",
1797
+ flex_mode: "none",
1798
+ columns: [{
1799
+ tag: "column",
1800
+ width: "weighted",
1801
+ weight: 1,
1802
+ background_style: "grey",
1803
+ padding: "8px 10px",
1804
+ vertical_spacing: "6px",
1805
+ elements: [
1806
+ { tag: "markdown", content: `\u26A0\uFE0F \u786E\u8BA4\u5173\u95ED **${escapeMarkdown(session.id)}**\uFF1FAgent \u548C tmux session \u5C06\u9000\u51FA\u3002` },
1807
+ buttonRow([
1808
+ actionButton("\u786E\u8BA4\u5173\u95ED", "danger", signer.sign({ ...common, action: "confirm" })),
1809
+ actionButton("\u53D6\u6D88", "default", signer.sign({ ...common, action: "cancel" }))
1810
+ ])
1811
+ ]
1812
+ }]
1813
+ };
1814
+ }
1815
+ function resumePickerCard(chatId, session, picker, signer) {
1816
+ const common = {
1817
+ kind: "resume-picker",
1818
+ sessionId: session.id,
1819
+ agent: session.agent,
1820
+ paneId: session.paneId,
1821
+ fingerprint: picker.fingerprint,
1822
+ chatId
1823
+ };
1824
+ const optionElements = picker.options.map((option2) => ({
1825
+ tag: "column_set",
1826
+ flex_mode: "none",
1827
+ columns: [{
1828
+ tag: "column",
1829
+ width: "weighted",
1830
+ weight: 1,
1831
+ background_style: "grey",
1832
+ padding: "9px 10px",
1833
+ vertical_spacing: "5px",
1834
+ elements: [
1835
+ { tag: "markdown", content: `${option2.selected ? "\u25CF" : "\u25CB"} **${escapeMarkdown(option2.label)}**${option2.detail ? `
1836
+ ${escapeMarkdown(option2.detail)}` : ""}` },
1837
+ actionButton("\u6062\u590D\u6B64 Session", option2.selected ? "primary" : "default", signer.sign({ ...common, action: `select:${option2.id}` }, 10 * 6e4))
1838
+ ]
1839
+ }]
1840
+ }));
1841
+ const navigation = [
1842
+ ...picker.canPrevious ? [actionButton("\u4E0A\u4E00\u9875", "default", signer.sign({ ...common, action: "previous" }, 10 * 6e4))] : [],
1843
+ actionButton("\u5237\u65B0", "default", signer.sign({ ...common, action: "refresh" }, 10 * 6e4)),
1844
+ ...picker.canNext ? [actionButton("\u4E0B\u4E00\u9875", "default", signer.sign({ ...common, action: "next" }, 10 * 6e4))] : [],
1845
+ actionButton("\u53D6\u6D88", "danger", signer.sign({ ...common, action: "cancel" }, 10 * 6e4))
1846
+ ];
1847
+ return cardElements(
1848
+ "\u9009\u62E9\u8981\u6062\u590D\u7684 Session",
1849
+ [
1850
+ { tag: "markdown", content: `**${escapeMarkdown(session.id)} \xB7 ${escapeMarkdown(getAgentAdapter(session.agent).displayName)}**${picker.position && picker.total ? ` \xB7 \u5F53\u524D ${picker.position}/${picker.total}` : ""}` },
1851
+ ...optionElements,
1852
+ buttonRow(navigation)
1853
+ ],
1854
+ "yellow"
1855
+ );
1856
+ }
1857
+ function startupConflictCard(chatId, requested, owner, signer) {
1858
+ const common = {
1859
+ kind: "startup-conflict",
1860
+ sessionId: requested.id,
1861
+ agent: requested.agent,
1862
+ paneId: owner.paneId,
1863
+ fingerprint: String(owner.updatedAt),
1864
+ chatId
1865
+ };
1866
+ return cardElements("Session \u5DF2\u7531\u5176\u4ED6\u8FDE\u63A5\u5360\u7528", [
1867
+ {
1868
+ tag: "markdown",
1869
+ content: `\u8981\u6062\u590D\u7684 **${escapeMarkdown(requested.agent)}** \u539F\u751F Session \u5DF2\u7531 LCA Session **${escapeMarkdown(owner.id)}** \u8FDE\u63A5\u3002
1870
+
1871
+ **\u8BF7\u6C42\u540D\u79F0** \`${escapeInlineCode(requested.id)}\`
1872
+ **\u5DE5\u4F5C\u76EE\u5F55** \`${escapeInlineCode(requested.cwd)}\``
1873
+ },
1874
+ buttonRow([
1875
+ actionButton(`\u8FDE\u63A5 ${owner.id}`, "primary", signer.sign({ ...common, action: "connect" }, 10 * 6e4)),
1876
+ actionButton("\u542F\u52A8\u65B0\u4F1A\u8BDD", "default", signer.sign({ ...common, action: "new" }, 10 * 6e4)),
1877
+ actionButton("\u53D6\u6D88", "danger", signer.sign({ ...common, action: "cancel" }, 10 * 6e4))
1878
+ ])
1879
+ ], "yellow");
1880
+ }
1881
+ function actionButton(text, type, value) {
1882
+ return {
1883
+ tag: "button",
1884
+ text: { tag: "plain_text", content: text },
1885
+ type,
1886
+ size: "medium",
1887
+ behaviors: [{ type: "callback", value }]
1888
+ };
1889
+ }
1890
+ function buttonRow(buttons) {
1891
+ return {
1892
+ tag: "column_set",
1893
+ flex_mode: "none",
1894
+ horizontal_spacing: "8px",
1895
+ margin: "2px 0px 0px 0px",
1896
+ columns: buttons.map((button) => ({ tag: "column", width: "weighted", weight: 1, elements: [button] }))
1897
+ };
1262
1898
  }
1263
1899
  function agentMarker(agent) {
1264
1900
  if (agent === "codex") return "\u{1F535}";
1265
- if (agent === "trae-cli") return "\u{1F7E3}";
1901
+ if (agent === "traex") return "\u{1F7E3}";
1266
1902
  return "\u{1F7E0}";
1267
1903
  }
1268
1904
  function statusColumn(title, value) {
@@ -1300,6 +1936,12 @@ function handledActionCard(action, result2, handledAt = /* @__PURE__ */ new Date
1300
1936
  ].join("\n");
1301
1937
  return card(title, content, [], "green");
1302
1938
  }
1939
+ function expiredActionCard() {
1940
+ return cardElements("\u5361\u7247\u5DF2\u5931\u6548", [{
1941
+ tag: "markdown",
1942
+ content: "\u26A0\uFE0F \u6B64\u5361\u7247\u6216\u6309\u94AE\u5DF2\u8FC7\u671F\u3001\u5DF2\u5904\u7406\uFF0C\u6216 bridge daemon \u5DF2\u91CD\u542F\u3002\n\n\u8BF7\u91CD\u65B0\u53D1\u9001\u5BF9\u5E94\u547D\u4EE4\u83B7\u53D6\u6700\u65B0\u5361\u7247\uFF1BSession \u76F8\u5173\u64CD\u4F5C\u53EF\u53D1\u9001 `/sessions`\u3002"
1943
+ }], "grey");
1944
+ }
1303
1945
  function card(title, content, buttons, template = "blue") {
1304
1946
  return cardElements(title, [{ tag: "markdown", content }, ...buttons], template);
1305
1947
  }
@@ -1372,21 +2014,36 @@ var LarkGateway = class {
1372
2014
  });
1373
2015
  this.channel.on("message", async (message) => this.handler.onMessage(message));
1374
2016
  this.channel.on("cardAction", async (event) => {
2017
+ console.error(`[lca] card action received: tag=${event.action.tag ?? "unknown"} name=${event.action.name ?? "-"} message=${event.messageId}`);
1375
2018
  const mappedFormAction = event.action.formValue && event.action.name ? this.formActions.get(event.messageId)?.get(event.action.name) : void 0;
1376
2019
  const action = this.signer.verify(event.action.value, event.chatId) ?? (mappedFormAction ? this.signer.verify(mappedFormAction, event.chatId) : void 0);
1377
- if (!action) return { toast: { type: "error", content: "\u64CD\u4F5C\u5DF2\u5931\u6548\uFF0C\u8BF7\u7B49\u5F85\u65B0\u5361\u7247\u3002" } };
1378
- if (event.action.formValue || action.kind === "manual") {
2020
+ if (!action) {
1379
2021
  if (this.formActionsInFlight.has(event.messageId)) {
1380
- return { toast: { type: "warning", content: action.kind === "manual" ? "\u7EC8\u7AEF\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002" : "\u7B54\u6848\u6B63\u5728\u63D0\u4EA4\uFF0C\u8BF7\u7A0D\u5019\u3002" } };
2022
+ return { toast: { type: "warning", content: "\u64CD\u4F5C\u6B63\u5728\u5904\u7406\u4E2D\uFF0C\u8BF7\u7A0D\u5019\u3002" } };
2023
+ }
2024
+ console.error(`[lca] card action rejected: signature or mapping invalid for message=${event.messageId}`);
2025
+ void this.markExpiredCard(event);
2026
+ return { toast: { type: "error", content: "\u5361\u7247\u6216\u6309\u94AE\u5DF2\u5931\u6548\uFF0C\u8BF7\u83B7\u53D6\u6700\u65B0\u5361\u7247\u3002" } };
2027
+ }
2028
+ if (event.action.formValue || action.kind === "manual" || action.kind === "session-create" || action.kind === "startup-conflict" || action.kind === "resume-picker" || action.kind === "session-stop" || action.kind === "session-start-error") {
2029
+ if (this.formActionsInFlight.has(event.messageId)) {
2030
+ return { toast: { type: "warning", content: inFlightToast(action) } };
1381
2031
  }
1382
2032
  this.formActionsInFlight.add(event.messageId);
1383
2033
  void this.handleDeferredCardAction(event, action).finally(() => {
1384
2034
  this.formActionsInFlight.delete(event.messageId);
1385
2035
  });
1386
- return { toast: { type: "success", content: action.kind === "manual" ? "\u6B63\u5728\u64CD\u4F5C\u672C\u5730\u7EC8\u7AEF\u2026" : "\u6B63\u5728\u63D0\u4EA4\u5230\u672C\u5730\u7EC8\u7AEF\u2026" } };
2036
+ return { toast: { type: "success", content: progressToast(action) } };
1387
2037
  }
1388
2038
  const result2 = await this.handler.onAction(event, action);
1389
2039
  if (result2.type === "error") return { toast: result2 };
2040
+ if (result2.type === "session-create-form") {
2041
+ this.rememberSessionCreateFormAction(event.messageId, event.chatId);
2042
+ return {
2043
+ toast: { type: "success", content: result2.content },
2044
+ card: { type: "raw", data: sessionCreateCard() }
2045
+ };
2046
+ }
1390
2047
  if (result2.type === "manual") {
1391
2048
  this.rememberManualFormActions(event.messageId, event.chatId, result2.view);
1392
2049
  return {
@@ -1401,10 +2058,47 @@ var LarkGateway = class {
1401
2058
  card: { type: "raw", data: choiceCard(event.chatId, result2.paneId, result2.screen, this.signer, result2.agent) }
1402
2059
  };
1403
2060
  }
1404
- if (result2.type === "awaiting-input") {
2061
+ if (result2.type === "awaiting-input") {
2062
+ return {
2063
+ toast: { type: "success", content: result2.content },
2064
+ card: { type: "raw", data: interactionInputCard(result2.agent, result2.label) }
2065
+ };
2066
+ }
2067
+ if (result2.type === "session-created") {
2068
+ this.formActions.delete(event.messageId);
2069
+ return {
2070
+ toast: { type: "success", content: result2.content },
2071
+ card: { type: "raw", data: sessionCreateResultCard(true, result2.content, result2.session) }
2072
+ };
2073
+ }
2074
+ if (result2.type === "session-start-failed") {
2075
+ return {
2076
+ toast: { type: "error", content: result2.content },
2077
+ card: { type: "raw", data: sessionStartupFailureCard(event.chatId, result2.failure, this.signer) }
2078
+ };
2079
+ }
2080
+ if (result2.type === "resume-picker") {
2081
+ return {
2082
+ toast: { type: "success", content: result2.content },
2083
+ card: { type: "raw", data: resumePickerCard(event.chatId, result2.session, result2.picker, this.signer) }
2084
+ };
2085
+ }
2086
+ if (result2.type === "startup-conflict") {
2087
+ return {
2088
+ toast: { type: "warning", content: result2.content },
2089
+ card: { type: "raw", data: startupConflictCard(event.chatId, requestSession(result2.request), result2.owner, this.signer) }
2090
+ };
2091
+ }
2092
+ if (result2.type === "session-picker") {
1405
2093
  return {
1406
2094
  toast: { type: "success", content: result2.content },
1407
- card: { type: "raw", data: interactionInputCard(result2.agent, result2.label) }
2095
+ card: { type: "raw", data: sessionPickerCard(
2096
+ event.chatId,
2097
+ result2.sessions,
2098
+ result2.activeSessionId,
2099
+ this.signer,
2100
+ result2.confirmingStopSessionId
2101
+ ) }
1408
2102
  };
1409
2103
  }
1410
2104
  return {
@@ -1419,12 +2113,82 @@ var LarkGateway = class {
1419
2113
  formActions = /* @__PURE__ */ new Map();
1420
2114
  formActionsInFlight = /* @__PURE__ */ new Set();
1421
2115
  async handleDeferredCardAction(event, action) {
2116
+ let initialResumePicker;
1422
2117
  try {
2118
+ if (action.kind === "session-create" && action.action === "submit") {
2119
+ await this.updateCardAfterAction(event.messageId, sessionCreateProgressCard());
2120
+ }
1423
2121
  const result2 = await this.handler.onAction(event, action);
1424
2122
  if (result2.type === "error") {
2123
+ if (action.kind === "session-create") {
2124
+ await this.updateCardAfterAction(
2125
+ event.messageId,
2126
+ sessionCreateFailureCard(event.chatId, result2.content, this.signer)
2127
+ );
2128
+ this.formActions.delete(event.messageId);
2129
+ return;
2130
+ }
1425
2131
  await this.channel.send(event.chatId, { text: `\u5361\u7247\u64CD\u4F5C\u672A\u5B8C\u6210\uFF1A${result2.content}` });
1426
2132
  return;
1427
2133
  }
2134
+ if (result2.type === "session-created") {
2135
+ await this.updateCardAfterAction(event.messageId, sessionCreateResultCard(true, result2.content, result2.session));
2136
+ this.formActions.delete(event.messageId);
2137
+ return;
2138
+ }
2139
+ if (result2.type === "session-start-failed") {
2140
+ await this.updateCardAfterAction(
2141
+ event.messageId,
2142
+ sessionStartupFailureCard(event.chatId, result2.failure, this.signer)
2143
+ );
2144
+ this.formActions.delete(event.messageId);
2145
+ return;
2146
+ }
2147
+ if (result2.type === "resume-picker") {
2148
+ if (action.kind === "session-create") initialResumePicker = result2.session;
2149
+ await this.updateCardAfterAction(event.messageId, resumePickerCard(event.chatId, result2.session, result2.picker, this.signer));
2150
+ return;
2151
+ }
2152
+ if (result2.type === "startup-conflict") {
2153
+ await this.updateCardAfterAction(
2154
+ event.messageId,
2155
+ startupConflictCard(event.chatId, requestSession(result2.request), result2.owner, this.signer)
2156
+ );
2157
+ return;
2158
+ }
2159
+ if (result2.type === "session-picker") {
2160
+ if (action.kind === "session-start-error") {
2161
+ await this.channel.send(event.chatId, { card: sessionPickerCard(
2162
+ event.chatId,
2163
+ result2.sessions,
2164
+ result2.activeSessionId,
2165
+ this.signer,
2166
+ result2.confirmingStopSessionId
2167
+ ) });
2168
+ return;
2169
+ }
2170
+ await this.updateCardAfterAction(event.messageId, sessionPickerCard(
2171
+ event.chatId,
2172
+ result2.sessions,
2173
+ result2.activeSessionId,
2174
+ this.signer,
2175
+ result2.confirmingStopSessionId
2176
+ ));
2177
+ return;
2178
+ }
2179
+ if (result2.type === "session-create-form") {
2180
+ const sent = await this.channel.send(event.chatId, { card: sessionCreateCard() });
2181
+ this.rememberSessionCreateFormAction(sent.messageId, event.chatId);
2182
+ const sourceCard = action.kind === "session-create" && result2.sessions ? sessionPickerCard(event.chatId, result2.sessions, result2.activeSessionId, this.signer, void 0, false) : sessionCreateOpenedCard();
2183
+ await this.updateCardAfterAction(event.messageId, sourceCard).catch(async (error) => {
2184
+ const detail = cardErrorDetail(error);
2185
+ console.error(`[lca] failed to retire session-create source card: message=${event.messageId} detail=${detail}`);
2186
+ await this.channel.send(event.chatId, {
2187
+ text: "\u65B0\u5EFA\u8868\u5355\u5DF2\u53D1\u9001\uFF0C\u4F46\u539F\u5361\u7247\u72B6\u6001\u66F4\u65B0\u5931\u8D25\uFF1B\u8BF7\u4F7F\u7528\u6700\u65B0\u7684\u201C\u65B0\u5EFA Coding Session\u201D\u5361\u7247\u7EE7\u7EED\u64CD\u4F5C\u3002"
2188
+ }).catch(() => void 0);
2189
+ });
2190
+ return;
2191
+ }
1428
2192
  if (result2.type === "refresh") {
1429
2193
  await this.updateCardAfterAction(
1430
2194
  event.messageId,
@@ -1444,8 +2208,17 @@ var LarkGateway = class {
1444
2208
  }
1445
2209
  await this.updateCardAfterAction(event.messageId, handledActionCard(action, result2.content));
1446
2210
  } catch (error) {
1447
- const detail = error instanceof Error ? error.message : String(error);
1448
- await this.channel.send(event.chatId, { text: `\u5361\u7247\u64CD\u4F5C\u540C\u6B65\u5931\u8D25\uFF1A${detail}` }).catch(() => void 0);
2211
+ const detail = cardErrorDetail(error);
2212
+ if (initialResumePicker) {
2213
+ await this.handler.onResumePickerDeliveryFailure?.(initialResumePicker).catch((cleanupError) => {
2214
+ console.error(`[lca] resume picker rollback failed: session=${initialResumePicker?.id} detail=${cardErrorDetail(cleanupError)}`);
2215
+ });
2216
+ }
2217
+ if (action.kind === "session-create") this.formActions.delete(event.messageId);
2218
+ console.error(`[lca] deferred card action failed: kind=${action.kind} action=${action.action} message=${event.messageId} detail=${detail}`);
2219
+ await this.channel.send(event.chatId, {
2220
+ text: action.kind === "session-create" ? `\u65B0\u5EFA Session \u8868\u5355\u6253\u5F00\u5931\u8D25\uFF1A${detail}\u3002\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\uFF0C\u6216\u76F4\u63A5\u4F7F\u7528 /start \u547D\u4EE4\u3002` : `\u5361\u7247\u64CD\u4F5C\u540C\u6B65\u5931\u8D25\uFF1A${detail}`
2221
+ }).catch(() => void 0);
1449
2222
  }
1450
2223
  }
1451
2224
  async updateCardAfterAction(messageId, card2) {
@@ -1463,6 +2236,17 @@ var LarkGateway = class {
1463
2236
  }
1464
2237
  throw lastError;
1465
2238
  }
2239
+ async markExpiredCard(event) {
2240
+ try {
2241
+ await this.updateCardAfterAction(event.messageId, expiredActionCard());
2242
+ } catch (error) {
2243
+ const detail = cardErrorDetail(error);
2244
+ console.error(`[lca] failed to mark expired card: message=${event.messageId} detail=${detail}`);
2245
+ await this.channel.send(event.chatId, {
2246
+ text: "\u5361\u7247\u6216\u6309\u94AE\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u5BF9\u5E94\u547D\u4EE4\u83B7\u53D6\u6700\u65B0\u5361\u7247\uFF1BSession \u76F8\u5173\u64CD\u4F5C\u53EF\u53D1\u9001 /sessions\u3002"
2247
+ }).catch(() => void 0);
2248
+ }
2249
+ }
1466
2250
  connect() {
1467
2251
  return this.channel.connect();
1468
2252
  }
@@ -1488,23 +2272,23 @@ var LarkGateway = class {
1488
2272
  await this.channel.updateCard(messageId, handledActionCard(action, content));
1489
2273
  this.formActions.delete(messageId);
1490
2274
  }
1491
- async sendManual(chatId, view) {
1492
- const result2 = await this.channel.send(chatId, { card: manualControlCard(chatId, view, this.signer) });
1493
- this.rememberManualFormActions(result2.messageId, chatId, view);
2275
+ async sendManual(chatId, view2) {
2276
+ const result2 = await this.channel.send(chatId, { card: manualControlCard(chatId, view2, this.signer) });
2277
+ this.rememberManualFormActions(result2.messageId, chatId, view2);
1494
2278
  return result2;
1495
2279
  }
1496
- rememberManualFormActions(messageId, chatId, view) {
1497
- if (view.state === "recovered" || view.state === "exited") {
2280
+ rememberManualFormActions(messageId, chatId, view2) {
2281
+ if (view2.state === "recovered" || view2.state === "exited") {
1498
2282
  this.formActions.delete(messageId);
1499
2283
  return;
1500
2284
  }
1501
2285
  const common = {
1502
2286
  kind: "manual",
1503
- sessionId: view.session.id,
1504
- manualMode: view.mode ?? "fallback",
1505
- agent: view.session.agent,
1506
- paneId: view.session.paneId,
1507
- fingerprint: view.screen.fingerprint,
2287
+ sessionId: view2.session.id,
2288
+ manualMode: view2.mode ?? "fallback",
2289
+ agent: view2.session.agent,
2290
+ paneId: view2.session.paneId,
2291
+ fingerprint: view2.screen.fingerprint,
1508
2292
  chatId
1509
2293
  };
1510
2294
  this.formActions.set(messageId, /* @__PURE__ */ new Map([
@@ -1512,6 +2296,19 @@ var LarkGateway = class {
1512
2296
  [MANUAL_SUBMIT_ACTION, this.signer.sign({ ...common, action: MANUAL_SUBMIT_ACTION }, 10 * 6e4)]
1513
2297
  ]));
1514
2298
  }
2299
+ rememberSessionCreateFormAction(messageId, chatId) {
2300
+ this.formActions.set(messageId, /* @__PURE__ */ new Map([[
2301
+ SESSION_CREATE_SUBMIT_ACTION,
2302
+ this.signer.sign({
2303
+ kind: "session-create",
2304
+ agent: "codex",
2305
+ action: "submit",
2306
+ paneId: "",
2307
+ fingerprint: "create",
2308
+ chatId
2309
+ }, 10 * 6e4)
2310
+ ]]));
2311
+ }
1515
2312
  rememberFormActions(messageId, chatId, paneId, screen, agent) {
1516
2313
  if (screen.interaction?.semantics?.activation !== "toggle") {
1517
2314
  this.formActions.delete(messageId);
@@ -1540,10 +2337,27 @@ var LarkGateway = class {
1540
2337
  sendSessionPicker(chatId, sessions, activeSessionId) {
1541
2338
  return this.channel.send(chatId, { card: sessionPickerCard(chatId, sessions, activeSessionId, this.signer) });
1542
2339
  }
2340
+ sendResumePicker(chatId, session, picker) {
2341
+ return this.channel.send(chatId, { card: resumePickerCard(chatId, session, picker, this.signer) });
2342
+ }
2343
+ sendStartupConflict(chatId, request, owner) {
2344
+ return this.channel.send(chatId, { card: startupConflictCard(chatId, requestSession(request), owner, this.signer) });
2345
+ }
2346
+ async sendSessionCreate(chatId) {
2347
+ const result2 = await this.channel.send(chatId, { card: sessionCreateCard() });
2348
+ this.rememberSessionCreateFormAction(result2.messageId, chatId);
2349
+ return result2;
2350
+ }
2351
+ sendSessionStartupFailure(chatId, failure) {
2352
+ return this.channel.send(chatId, { card: sessionStartupFailureCard(chatId, failure, this.signer) });
2353
+ }
1543
2354
  sendStopConfirmation(chatId, paneId, fingerprint, agent) {
1544
2355
  return this.channel.send(chatId, { card: stopCard(chatId, paneId, fingerprint, this.signer, agent) });
1545
2356
  }
1546
2357
  };
2358
+ function requestSession(request) {
2359
+ return { id: request.sessionId, agent: request.agent, cwd: request.cwd };
2360
+ }
1547
2361
  function cardActionLocked(error) {
1548
2362
  if (typeof error === "string") return /card action is lock/i.test(error);
1549
2363
  if (error instanceof Error && /card action is lock/i.test(error.message)) return true;
@@ -1551,6 +2365,234 @@ function cardActionLocked(error) {
1551
2365
  const record = error;
1552
2366
  return ["message", "msg", "cause"].some((key) => cardActionLocked(record[key]));
1553
2367
  }
2368
+ function progressToast(action) {
2369
+ if (action.kind === "manual") return "\u6B63\u5728\u64CD\u4F5C\u672C\u5730\u7EC8\u7AEF\u2026";
2370
+ if (action.kind === "session-create") return action.action === "open" ? "\u6B63\u5728\u6253\u5F00\u65B0\u5EFA\u8868\u5355\u2026" : "\u6B63\u5728\u542F\u52A8 Session\u2026";
2371
+ if (action.kind === "resume-picker") return action.action.startsWith("select:") ? "\u6B63\u5728\u6062\u590D Session\u2026" : "\u6B63\u5728\u66F4\u65B0 Resume Picker\u2026";
2372
+ if (action.kind === "startup-conflict") return action.action === "new" ? "\u6B63\u5728\u542F\u52A8\u65B0 Session\u2026" : "\u6B63\u5728\u5904\u7406 Session \u51B2\u7A81\u2026";
2373
+ if (action.kind === "session-stop") return "\u6B63\u5728\u5904\u7406 Session\u2026";
2374
+ if (action.kind === "session-start-error") return action.action === "create" ? "\u6B63\u5728\u6253\u5F00\u65B0\u5EFA\u8868\u5355\u2026" : "\u6B63\u5728\u6253\u5F00 Sessions\u2026";
2375
+ return "\u6B63\u5728\u63D0\u4EA4\u5230\u672C\u5730\u7EC8\u7AEF\u2026";
2376
+ }
2377
+ function inFlightToast(action) {
2378
+ if (action.kind === "manual") return "\u7EC8\u7AEF\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002";
2379
+ if (action.kind === "session-create") return action.action === "open" ? "\u6B63\u5728\u6253\u5F00\u65B0\u5EFA\u8868\u5355\uFF0C\u8BF7\u7A0D\u5019\u3002" : "Session \u6B63\u5728\u542F\u52A8\uFF0C\u8BF7\u7A0D\u5019\u3002";
2380
+ if (action.kind === "resume-picker") return "Resume Picker \u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002";
2381
+ if (action.kind === "startup-conflict") return "Session \u51B2\u7A81\u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002";
2382
+ if (action.kind === "session-stop") return "Session \u64CD\u4F5C\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002";
2383
+ if (action.kind === "session-start-error") return "\u6B63\u5728\u5904\u7406\u542F\u52A8\u5931\u8D25\u64CD\u4F5C\uFF0C\u8BF7\u7A0D\u5019\u3002";
2384
+ return "\u7B54\u6848\u6B63\u5728\u63D0\u4EA4\uFF0C\u8BF7\u7A0D\u5019\u3002";
2385
+ }
2386
+ function cardErrorDetail(error) {
2387
+ const response = error && typeof error === "object" ? error.response : void 0;
2388
+ const data = response && typeof response === "object" ? response.data : void 0;
2389
+ const record = data && typeof data === "object" ? data : void 0;
2390
+ const code = typeof record?.code === "number" || typeof record?.code === "string" ? String(record.code) : void 0;
2391
+ const message = typeof record?.msg === "string" ? record.msg : error instanceof Error ? error.message : String(error);
2392
+ const sanitized = message.replace(/(?:authorization\s*:\s*bearer|bearer)\s+[^\s,'"\]}]+/gi, "Bearer [REDACTED]").replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 1200);
2393
+ return code ? `code=${code} ${sanitized}` : sanitized;
2394
+ }
2395
+
2396
+ // src/lark/start-command.ts
2397
+ function parseStartCommand(input) {
2398
+ const tokens = tokenize(input);
2399
+ if (tokens[0] !== "/start") throw invalid("\u547D\u4EE4\u5FC5\u987B\u4EE5 /start \u5F00\u5934");
2400
+ const sessionId = tokens[1];
2401
+ if (!sessionId || sessionId.startsWith("--")) throw invalid("\u8BF7\u63D0\u4F9B session \u540D\u79F0");
2402
+ let agentValue;
2403
+ let cwd;
2404
+ let resume;
2405
+ for (let index = 2; index < tokens.length; index += 1) {
2406
+ const token = tokens[index];
2407
+ if (token === "--agent") {
2408
+ if (agentValue !== void 0) throw invalid("--agent \u4E0D\u80FD\u91CD\u590D");
2409
+ agentValue = requiredValue(tokens, ++index, "--agent");
2410
+ continue;
2411
+ }
2412
+ if (token === "--cwd") {
2413
+ if (cwd !== void 0) throw invalid("--cwd \u4E0D\u80FD\u91CD\u590D");
2414
+ cwd = requiredValue(tokens, ++index, "--cwd");
2415
+ continue;
2416
+ }
2417
+ if (token === "--resume-last") {
2418
+ throw invalid("\u98DE\u4E66\u7AEF\u4E0D\u652F\u6301 --resume-last\uFF0C\u8BF7\u4F7F\u7528 --resume \u6253\u5F00\u539F\u751F Resume Picker");
2419
+ }
2420
+ if (token === "--resume") {
2421
+ if (resume) throw invalid("\u53EA\u80FD\u9009\u62E9\u4E00\u79CD\u6062\u590D\u65B9\u5F0F");
2422
+ const candidate = tokens[index + 1];
2423
+ if (candidate && !candidate.startsWith("--")) {
2424
+ throw invalid("\u98DE\u4E66\u7AEF\u4E0D\u652F\u6301\u8F93\u5165\u5386\u53F2 Session ID\uFF0C\u8BF7\u4F7F\u7528 --resume \u6253\u5F00\u539F\u751F Resume Picker");
2425
+ }
2426
+ resume = { mode: "picker" };
2427
+ continue;
2428
+ }
2429
+ throw invalid(`\u65E0\u6CD5\u8BC6\u522B\u53C2\u6570\uFF1A${token}`);
2430
+ }
2431
+ const agent = agentValue ? normalizeAgentId(agentValue) : void 0;
2432
+ if (!agentValue) throw invalid("\u8BF7\u63D0\u4F9B --agent");
2433
+ if (!agent) throw invalid(`\u4E0D\u652F\u6301\u7684 agent\uFF1A${agentValue}\uFF1B\u53EF\u9009 codex\u3001traex\u3001claude`);
2434
+ if (!cwd) throw invalid("\u8BF7\u63D0\u4F9B --cwd");
2435
+ return { sessionId, agent, cwd, resume };
2436
+ }
2437
+ function tokenize(input) {
2438
+ const tokens = [];
2439
+ let current = "";
2440
+ let quote;
2441
+ let escaped = false;
2442
+ const push = () => {
2443
+ if (current) tokens.push(current);
2444
+ current = "";
2445
+ };
2446
+ for (const char of input.trim()) {
2447
+ if (escaped) {
2448
+ current += char;
2449
+ escaped = false;
2450
+ } else if (char === "\\" && quote !== "'") {
2451
+ escaped = true;
2452
+ } else if (quote) {
2453
+ if (char === quote) quote = void 0;
2454
+ else current += char;
2455
+ } else if (char === "'" || char === '"') {
2456
+ quote = char;
2457
+ } else if (/\s/.test(char)) {
2458
+ push();
2459
+ } else {
2460
+ current += char;
2461
+ }
2462
+ }
2463
+ if (escaped) current += "\\";
2464
+ if (quote) throw invalid("\u5F15\u53F7\u6CA1\u6709\u95ED\u5408");
2465
+ push();
2466
+ return tokens;
2467
+ }
2468
+ function requiredValue(tokens, index, option2) {
2469
+ const value = tokens[index];
2470
+ if (!value || value.startsWith("--")) throw invalid(`${option2} \u7F3A\u5C11\u53C2\u6570\u503C`);
2471
+ return value;
2472
+ }
2473
+ function invalid(reason) {
2474
+ return new AppError("INVALID_OPTIONS", reason, { reason });
2475
+ }
2476
+
2477
+ // src/screen/resume-picker.ts
2478
+ import { createHash as createHash2 } from "crypto";
2479
+ function parseResumePicker(raw, agent) {
2480
+ const normalized = normalizeScreen(raw);
2481
+ return agent === "claude" ? parseClaudePicker(normalized, agent) : parseCodexStylePicker(normalized, agent);
2482
+ }
2483
+ function parseCodexStylePicker(normalized, agent) {
2484
+ if (!/^\s*Resume a previous session\s*$/mi.test(normalized)) return void 0;
2485
+ const lines = normalized.split("\n");
2486
+ const start = lines.findIndex((line) => /Type to search/i.test(line));
2487
+ const end = lines.findIndex((line, index) => index > start && /enter\s+resume/i.test(line));
2488
+ if (start < 0 || end < 0) return void 0;
2489
+ const options = [];
2490
+ for (const line of lines.slice(start + 1, end)) {
2491
+ const match = line.match(/^\s*(❯)?\s*(\d+\s*(?:m|h|d|w|mo|y)\s+ago)\s+(.+?)\s*$/i);
2492
+ if (!match?.[3]) continue;
2493
+ const label = match[3].trim();
2494
+ options.push(option(options.length, label, match[2]?.replace(/\s+/g, ""), Boolean(match[1])));
2495
+ }
2496
+ if (options.length === 0) return void 0;
2497
+ const positionMatch = normalized.match(/(\d+)\s*\/\s*(\d+)\s*·\s*(\d+)%/);
2498
+ return view(
2499
+ agent,
2500
+ normalized,
2501
+ options,
2502
+ positionMatch ? Number(positionMatch[1]) : void 0,
2503
+ positionMatch ? Number(positionMatch[2]) : void 0
2504
+ );
2505
+ }
2506
+ function parseClaudePicker(normalized, agent) {
2507
+ const header = normalized.match(/^\s*Resume session(?:\s*\((\d+)\s+of\s+(\d+)\))?\s*$/mi);
2508
+ if (!header) return void 0;
2509
+ const lines = normalized.split("\n");
2510
+ const start = lines.findIndex((line) => /⌕\s*Search|Search…/i.test(line));
2511
+ const end = lines.findIndex((line, index) => index > start && /Ctrl\+A to show all projects/i.test(line));
2512
+ if (start < 0 || end < 0) return void 0;
2513
+ const options = [];
2514
+ const body = lines.slice(start + 1, end);
2515
+ const navigation = {
2516
+ canPrevious: body.some((line) => /^\s*↑/.test(line)),
2517
+ canNext: body.some((line) => /^\s*↓/.test(line))
2518
+ };
2519
+ for (let index = 0; index < body.length - 1; index += 1) {
2520
+ const title = body[index] ?? "";
2521
+ const detail = body[index + 1] ?? "";
2522
+ if (!/\b(?:minute|hour|day|week|month|year)s? ago\s*·/i.test(detail)) continue;
2523
+ const selected = /^\s*❯/.test(title);
2524
+ const label = title.replace(/^\s*[❯↓↑]?\s*/, "").trim();
2525
+ if (!label || /^[↓↑]/.test(title.trim())) continue;
2526
+ options.push(option(options.length, label, detail.trim(), selected));
2527
+ index += 1;
2528
+ }
2529
+ if (options.length === 0) return void 0;
2530
+ return view(
2531
+ agent,
2532
+ normalized,
2533
+ options,
2534
+ header[1] ? Number(header[1]) : void 0,
2535
+ header[2] ? Number(header[2]) : void 0,
2536
+ navigation
2537
+ );
2538
+ }
2539
+ function option(visibleIndex, label, detail, selected) {
2540
+ const id = createHash2("sha256").update(`${visibleIndex}
2541
+ ${label}
2542
+ ${detail ?? ""}`).digest("hex").slice(0, 16);
2543
+ return { id, label, detail, selected, visibleIndex };
2544
+ }
2545
+ function view(agent, normalized, options, position, total, navigation) {
2546
+ const selectedIndex = Math.max(0, options.findIndex((candidate) => candidate.selected));
2547
+ const hasHiddenOptions = position !== void 0 && total !== void 0 && total > options.length;
2548
+ return {
2549
+ agent,
2550
+ fingerprint: createHash2("sha256").update(normalized).digest("hex"),
2551
+ options,
2552
+ selectedIndex,
2553
+ position,
2554
+ total,
2555
+ canPrevious: navigation?.canPrevious || hasHiddenOptions && position > 1,
2556
+ canNext: navigation?.canNext || hasHiddenOptions && position < total
2557
+ };
2558
+ }
2559
+
2560
+ // src/terminal/startup-error.ts
2561
+ var MAX_LINES = 20;
2562
+ var MAX_CHARS = 2e3;
2563
+ var EMPTY_EXCERPT = "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002";
2564
+ function startupTerminalExcerpt(raw) {
2565
+ const sanitized = redactSecrets(stripTerminalControls(raw));
2566
+ const lines = sanitized.split("\n").map(cleanLine).filter((line) => line !== void 0);
2567
+ while (lines[0] === "") lines.shift();
2568
+ while (lines.at(-1) === "") lines.pop();
2569
+ if (lines.length === 0) return EMPTY_EXCERPT;
2570
+ const errorIndex = lines.findIndex((line) => /\b(?:error|failed|fatal)\b/i.test(line));
2571
+ const selected = (errorIndex >= 0 ? lines.slice(errorIndex) : lines.slice(-MAX_LINES)).slice(0, MAX_LINES);
2572
+ let excerpt = selected.join("\n").trim();
2573
+ if (!excerpt) return EMPTY_EXCERPT;
2574
+ const lineTruncated = errorIndex >= 0 ? lines.length - errorIndex > MAX_LINES : lines.length > MAX_LINES;
2575
+ const charTruncated = excerpt.length > MAX_CHARS;
2576
+ if (charTruncated) excerpt = excerpt.slice(0, MAX_CHARS).trimEnd();
2577
+ if (lineTruncated || charTruncated) excerpt = `${excerpt}
2578
+ \u2026 \u8F93\u51FA\u5DF2\u622A\u65AD`;
2579
+ return excerpt;
2580
+ }
2581
+ function stripTerminalControls(value) {
2582
+ return value.replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/g, "").replace(/\u001B(?:[@-_][0-?]*[ -/]*[@-~]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/\r/g, "\n").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001A\u001C-\u001F\u007F]/g, "");
2583
+ }
2584
+ function cleanLine(value) {
2585
+ const line = value.replace(/[ \t]+$/g, "").trimStart();
2586
+ if (/^Pane is dead(?:\s|\(|$)/i.test(line)) return void 0;
2587
+ if (/^[╭╮╰╯┌┐└┘─━═│┃┊┋┄┅┈┉┼├┤┬┴┿╋+\-_=\s]+$/.test(line)) return void 0;
2588
+ return line.replace(/^[│┃]\s?/, "").replace(/\s?[│┃]$/, "").trimEnd();
2589
+ }
2590
+ function redactSecrets(value) {
2591
+ return value.replace(/\b(authorization\s*[:=]\s*)(?:Bearer\s+)?[^\s,;]+/gi, "$1[REDACTED]").replace(/\b(Bearer)\s+[^\s,'"\]}]+/gi, "$1 [REDACTED]").replace(
2592
+ /\b(api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|app[_-]?secret|cookie)\b(\s*[:=]\s*)([^\s,;]+)/gi,
2593
+ "$1$2[REDACTED]"
2594
+ ).replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret|signature)=)[^&#\s]+/gi, "$1[REDACTED]");
2595
+ }
1554
2596
 
1555
2597
  // src/daemon/server.ts
1556
2598
  var AssistantDaemon = class {
@@ -1573,6 +2615,7 @@ var AssistantDaemon = class {
1573
2615
  config;
1574
2616
  state;
1575
2617
  tmux;
2618
+ reconciler;
1576
2619
  screen;
1577
2620
  previousScreen;
1578
2621
  completedEvents = /* @__PURE__ */ new Set();
@@ -1589,6 +2632,9 @@ var AssistantDaemon = class {
1589
2632
  unresolvedCandidate;
1590
2633
  unresolvedNotified = /* @__PURE__ */ new Set();
1591
2634
  closedManualCards = /* @__PURE__ */ new Set();
2635
+ pendingAgentSessionClaims = /* @__PURE__ */ new Map();
2636
+ pendingResumePickers = /* @__PURE__ */ new Map();
2637
+ pendingStartupConflicts = /* @__PURE__ */ new Map();
1592
2638
  pendingInteractionInput;
1593
2639
  attachAttempts = /* @__PURE__ */ new Map();
1594
2640
  server = createServer((socket) => this.handleSocket(socket));
@@ -1599,11 +2645,21 @@ var AssistantDaemon = class {
1599
2645
  this.config = config;
1600
2646
  this.state = await this.store.loadState();
1601
2647
  this.tmux = new TmuxController(config.tmuxBinary);
2648
+ this.reconciler = new SessionReconciler(
2649
+ this.tmux,
2650
+ this.sessionName,
2651
+ 3,
2652
+ (message) => this.log(message),
2653
+ async (agent) => {
2654
+ const adapter = getAgentAdapter(agent);
2655
+ return (await runFile(adapter.binary(this.config), [...adapter.versionArgs])).stdout.trim();
2656
+ }
2657
+ );
1602
2658
  const secrets = await this.store.loadSecrets();
1603
2659
  if (!secrets) throw new Error("missing secrets; run lark-coding-assistant init again");
1604
2660
  await this.acquireRuntimeFiles();
1605
2661
  try {
1606
- await this.reconcileSessions();
2662
+ await this.reconcileSessions(true);
1607
2663
  await rm(this.paths.socket, { force: true });
1608
2664
  await new Promise((resolve, reject) => {
1609
2665
  this.server.once("error", reject);
@@ -1612,9 +2668,11 @@ var AssistantDaemon = class {
1612
2668
  await chmod3(this.paths.socket, 384);
1613
2669
  this.gateway = this.gatewayFactory(config, secrets, {
1614
2670
  onMessage: (message) => this.onLarkMessage(message),
1615
- onAction: (event, action) => this.onLarkAction(event, action)
2671
+ onAction: (event, action) => this.onLarkAction(event, action),
2672
+ onResumePickerDeliveryFailure: (session) => this.handleResumePickerDeliveryFailure(session)
1616
2673
  });
1617
2674
  await this.gateway.connect();
2675
+ await this.refreshNativeAgentSessionClaims();
1618
2676
  this.schedulePoll();
1619
2677
  await this.log("daemon started");
1620
2678
  } catch (error) {
@@ -1645,7 +2703,7 @@ var AssistantDaemon = class {
1645
2703
  return;
1646
2704
  } catch (error) {
1647
2705
  if (!isAlreadyExists(error)) throw error;
1648
- const existingPid = Number.parseInt(await readFile2(this.paths.pid, "utf8").catch(() => ""), 10);
2706
+ const existingPid = Number.parseInt(await readFile3(this.paths.pid, "utf8").catch(() => ""), 10);
1649
2707
  if (Number.isInteger(existingPid) && processIsAlive(existingPid)) {
1650
2708
  throw new Error(`daemon is already running with PID ${existingPid}`);
1651
2709
  }
@@ -1656,7 +2714,7 @@ var AssistantDaemon = class {
1656
2714
  }
1657
2715
  async releaseRuntimeFiles() {
1658
2716
  if (!this.ownsRuntimeFiles) return;
1659
- const ownerPid = Number.parseInt(await readFile2(this.paths.pid, "utf8").catch(() => ""), 10);
2717
+ const ownerPid = Number.parseInt(await readFile3(this.paths.pid, "utf8").catch(() => ""), 10);
1660
2718
  if (ownerPid === process.pid) {
1661
2719
  await Promise.all([rm(this.paths.socket, { force: true }), rm(this.paths.pid, { force: true })]);
1662
2720
  }
@@ -1707,20 +2765,21 @@ var AssistantDaemon = class {
1707
2765
  return this.rotateBindCode();
1708
2766
  case "resetOwner":
1709
2767
  return this.resetOwner();
2768
+ case "agentSessionStarted":
2769
+ return this.handleAgentSessionStarted(request.candidate);
1710
2770
  case "turnComplete":
1711
2771
  return this.handleTurnComplete(request.candidate);
1712
2772
  }
1713
2773
  }
1714
2774
  async startSession(sessionId, cwd, agentId, resume) {
1715
- if (!validSessionId(sessionId)) {
1716
- return fail(new AppError(
1717
- "INVALID_SESSION_NAME",
1718
- "session name must use letters, digits, underscore, or dash",
1719
- { sessionId }
1720
- ));
2775
+ try {
2776
+ await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
2777
+ } catch (error) {
2778
+ return fail(error);
1721
2779
  }
2780
+ await this.reconcileSessions(true);
1722
2781
  const existing = this.state.sessions?.[sessionId];
1723
- if (existing && await this.tmux.inspect(existing.paneId)) {
2782
+ if (existing) {
1724
2783
  return fail(new AppError(
1725
2784
  "SESSION_EXISTS",
1726
2785
  `managed coding-agent session is already running: ${sessionId}`,
@@ -1736,21 +2795,33 @@ var AssistantDaemon = class {
1736
2795
  }
1737
2796
  const adapter = getAgentAdapter(agentId);
1738
2797
  const binary = adapter.binary(this.config);
1739
- const agentVersion = (await runFile(binary, [...adapter.versionArgs])).stdout.trim();
1740
- const tmuxSessionName = existing?.sessionName ?? `${this.sessionName}-${sessionId}`;
1741
- const pane = await this.tmux.create({
1742
- sessionName: tmuxSessionName,
1743
- cwd,
1744
- binary,
1745
- args: adapter.buildLaunchArgs({
1746
- resume,
1747
- stopHookCommand: this.stopHookCommand
1748
- }),
1749
- env: {
1750
- LARK_CODING_ASSISTANT_SOCKET: this.paths.socket,
1751
- LARK_CODING_ASSISTANT_SESSION_ID: sessionId
1752
- }
1753
- });
2798
+ let agentVersion;
2799
+ try {
2800
+ agentVersion = (await runFile(binary, [...adapter.versionArgs])).stdout.trim();
2801
+ } catch (error) {
2802
+ return fail(systemErrorCode(error) === "ENOENT" ? new AppError("BINARY_NOT_FOUND", `command not found: ${binary}`, { binary }, { cause: error }) : new AppError("START_FAILED", `failed to inspect agent binary: ${binary}`, { sessionId }, { cause: error }));
2803
+ }
2804
+ const tmuxSessionName = `${this.sessionName}-${sessionId}`;
2805
+ let pane;
2806
+ try {
2807
+ pane = await this.tmux.create({
2808
+ sessionName: tmuxSessionName,
2809
+ cwd,
2810
+ binary,
2811
+ args: adapter.buildLaunchArgs({
2812
+ resume,
2813
+ stopHookCommand: this.stopHookCommand
2814
+ }),
2815
+ env: {
2816
+ LARK_CODING_ASSISTANT_SOCKET: this.paths.socket,
2817
+ LARK_CODING_ASSISTANT_SESSION_ID: sessionId,
2818
+ LARK_CODING_ASSISTANT_AGENT: agentId
2819
+ },
2820
+ preserveOnExit: true
2821
+ });
2822
+ } catch (error) {
2823
+ return fail(isAppError(error) ? error : new AppError("START_FAILED", "failed to create tmux session", { sessionId }, { cause: error }));
2824
+ }
1754
2825
  const binding = this.createSessionBinding();
1755
2826
  const session = {
1756
2827
  id: sessionId,
@@ -1761,7 +2832,33 @@ var AssistantDaemon = class {
1761
2832
  agentVersion,
1762
2833
  updatedAt: Date.now()
1763
2834
  };
1764
- this.state = {
2835
+ const pendingClaim = this.pendingAgentSessionClaims.get(sessionId);
2836
+ if (pendingClaim?.agent === agentId) {
2837
+ const owner = this.findAgentSessionOwner(agentId, pendingClaim.agentSessionId, sessionId);
2838
+ if (owner) {
2839
+ this.pendingAgentSessionClaims.delete(sessionId);
2840
+ await this.tmux.killSession(pane.sessionName).catch(() => void 0);
2841
+ return fail(agentSessionInUse(sessionId, owner.id));
2842
+ }
2843
+ session.agentSessionId = pendingClaim.agentSessionId;
2844
+ this.pendingAgentSessionClaims.delete(sessionId);
2845
+ }
2846
+ try {
2847
+ await this.tmux.writeMetadata(pane.sessionName, {
2848
+ managed: true,
2849
+ sessionId,
2850
+ agent: agentId,
2851
+ cwd,
2852
+ agentVersion,
2853
+ agentSessionId: session.agentSessionId
2854
+ });
2855
+ } catch (error) {
2856
+ await this.tmux.killSession(pane.sessionName).catch((cleanupError) => this.log(
2857
+ `failed to clean session ${sessionId} after metadata error: ${errorMessage3(cleanupError)}`
2858
+ ));
2859
+ return fail(new AppError("START_FAILED", "failed to persist tmux session metadata", { sessionId }, { cause: error }));
2860
+ }
2861
+ const nextState = {
1765
2862
  ...this.state,
1766
2863
  sessions: { ...this.state.sessions, [sessionId]: session },
1767
2864
  activeSessionId: this.state.activeSessionId ?? sessionId,
@@ -1770,8 +2867,45 @@ var AssistantDaemon = class {
1770
2867
  bindCodeExpiresAt: binding.mode === "code" ? Date.now() + 10 * 6e4 : void 0,
1771
2868
  updatedAt: Date.now()
1772
2869
  };
1773
- await this.store.saveState(this.state);
1774
- await this.poll();
2870
+ try {
2871
+ await this.store.saveState(nextState);
2872
+ } catch (error) {
2873
+ await this.tmux.killSession(pane.sessionName).catch((cleanupError) => this.log(
2874
+ `failed to clean session ${sessionId} after state error: ${errorMessage3(cleanupError)}`
2875
+ ));
2876
+ return fail(new AppError("START_FAILED", "failed to persist session state", { sessionId }, { cause: error }));
2877
+ }
2878
+ this.state = nextState;
2879
+ const lateClaim = this.pendingAgentSessionClaims.get(sessionId);
2880
+ if (lateClaim) {
2881
+ const claimed = await this.handleAgentSessionStarted(lateClaim);
2882
+ if (!claimed.ok) {
2883
+ await this.stopSession(sessionId).catch(() => void 0);
2884
+ return claimed;
2885
+ }
2886
+ }
2887
+ if (resume && resume.mode !== "picker") {
2888
+ const initialClaim = await this.waitForInitialAgentSessionClaim(sessionId, pane.pid);
2889
+ if (!initialClaim.ok) {
2890
+ await this.stopSession(sessionId).catch(() => void 0);
2891
+ return initialClaim;
2892
+ }
2893
+ } else if (!resume) {
2894
+ const stable = await this.waitForStartupStability(session, 500);
2895
+ if (!stable.ok) {
2896
+ await this.stopSession(sessionId).catch(() => void 0);
2897
+ return stable;
2898
+ }
2899
+ }
2900
+ if (resume?.mode !== "picker") {
2901
+ await this.tmux.preserveOnExit(session.sessionName, false).catch((error) => this.log(
2902
+ `failed to disable startup preservation for ${sessionId}: ${errorMessage3(error)}`
2903
+ ));
2904
+ }
2905
+ await this.log(
2906
+ `session created: session=${session.id} agent=${session.agent} pane=${session.paneId} active=${this.state.activeSessionId === session.id}`
2907
+ );
2908
+ await this.poll().catch((error) => this.log(`initial poll failed for ${sessionId}: ${errorMessage3(error)}`));
1775
2909
  return { ok: true, value: { pane, session, binding, active: this.state.activeSessionId === sessionId } };
1776
2910
  }
1777
2911
  createSessionBinding() {
@@ -1818,6 +2952,40 @@ var AssistantDaemon = class {
1818
2952
  }
1819
2953
  }
1820
2954
  if (message.senderId !== this.state.ownerOpenId || message.chatId !== this.state.boundChatId) return;
2955
+ if (text === "/start") {
2956
+ try {
2957
+ await this.gateway?.sendSessionCreate(message.chatId);
2958
+ } catch (error) {
2959
+ await this.log(`session create card failed: ${errorMessage3(error)}`);
2960
+ await this.gateway?.sendText(message.chatId, "\u65B0\u5EFA Session \u8868\u5355\u53D1\u9001\u5931\u8D25\u3002\u8BF7\u4F7F\u7528 /start <name> --agent <agent> --cwd <\u7EDD\u5BF9\u8DEF\u5F84>\u3002");
2961
+ }
2962
+ return;
2963
+ }
2964
+ if (text.startsWith("/start ")) {
2965
+ let request;
2966
+ try {
2967
+ request = parseStartCommand(text);
2968
+ } catch (error) {
2969
+ await this.gateway?.sendText(message.chatId, remoteError(fail(error)));
2970
+ return;
2971
+ }
2972
+ const result3 = await this.startRemoteSession(request);
2973
+ if (!result3.ok) {
2974
+ const failure = sessionStartupFailure(result3.error, request);
2975
+ if (failure) await this.gateway?.sendSessionStartupFailure(message.chatId, failure);
2976
+ else await this.gateway?.sendText(message.chatId, remoteError(result3.error));
2977
+ } else if (result3.state === "picker") {
2978
+ try {
2979
+ await this.gateway?.sendResumePicker(message.chatId, result3.session, result3.picker);
2980
+ } catch (error) {
2981
+ await this.handleResumePickerDeliveryFailure(result3.session);
2982
+ await this.log(`resume picker notification failed: session=${result3.session.id} error=${errorMessage3(error)}`);
2983
+ await this.gateway?.sendText(message.chatId, "Resume Picker \u5361\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u4E34\u65F6 Session \u5DF2\u6E05\u7406\uFF0C\u8BF7\u91CD\u8BD5\u3002");
2984
+ }
2985
+ } else if (result3.state === "conflict") await this.gateway?.sendStartupConflict(message.chatId, result3.request, result3.owner);
2986
+ else await this.gateway?.sendText(message.chatId, remoteStartSuccess(result3.session));
2987
+ return;
2988
+ }
1821
2989
  const tailMatch = text.match(/^\/tail(?:\s+(\d+))?$/);
1822
2990
  if (tailMatch) {
1823
2991
  const lines = tailMatch[1] ? Number(tailMatch[1]) : 80;
@@ -1825,7 +2993,7 @@ var AssistantDaemon = class {
1825
2993
  await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
1826
2994
  return;
1827
2995
  }
1828
- const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorMessage2(error)}`);
2996
+ const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorMessage3(error)}`);
1829
2997
  const session = this.activeSession();
1830
2998
  const metadata = session ? `**${session.id} \xB7 ${getAgentAdapter(session.agent).displayName}** \xB7 \u72B6\u6001 \`${this.screen?.state ?? "unknown"}\` \xB7 ${manualTimestamp()}` : "**\u5F53\u524D\u6CA1\u6709 active session**";
1831
2999
  await this.gateway?.sendMarkdown(message.chatId, `${metadata}
@@ -1841,13 +3009,13 @@ ${escapeFence2(output).slice(-6800)}
1841
3009
  }
1842
3010
  if (text === "/manual") {
1843
3011
  await this.poll();
1844
- const view = this.currentManualView();
1845
- if (!view) await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
3012
+ const view2 = this.currentManualView();
3013
+ if (!view2) await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
1846
3014
  else {
1847
3015
  try {
1848
- await this.gateway?.sendManual(message.chatId, view);
3016
+ await this.gateway?.sendManual(message.chatId, view2);
1849
3017
  } catch (error) {
1850
- await this.log(`manual card failed: ${errorMessage2(error)}`);
3018
+ await this.log(`manual card failed: ${errorMessage3(error)}`);
1851
3019
  await this.gateway?.sendText(
1852
3020
  message.chatId,
1853
3021
  "\u624B\u52A8\u9065\u63A7\u5361\u53D1\u9001\u5931\u8D25\u3002\u53EF\u4F7F\u7528 /tail 120 \u67E5\u770B\u7EC8\u7AEF\uFF0C\u6216\u4F7F\u7528 /key\u3001/type\u3001/submit \u64CD\u4F5C\u3002"
@@ -1885,7 +3053,7 @@ ${escapeFence2(output).slice(-6800)}
1885
3053
  return;
1886
3054
  }
1887
3055
  if (text === "/sessions") {
1888
- const sessions = await this.reconcileSessions();
3056
+ const sessions = await this.reconcileSessions(true);
1889
3057
  try {
1890
3058
  await this.gateway?.sendSessionPicker(
1891
3059
  message.chatId,
@@ -1893,7 +3061,7 @@ ${escapeFence2(output).slice(-6800)}
1893
3061
  this.state.activeSessionId
1894
3062
  );
1895
3063
  } catch (error) {
1896
- await this.log(`session picker notification failed: ${errorMessage2(error)}`);
3064
+ await this.log(`session picker notification failed: ${errorMessage3(error)}`);
1897
3065
  await this.gateway?.sendText(
1898
3066
  message.chatId,
1899
3067
  "Session \u9009\u62E9\u5361\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u4E5F\u53EF\u4EE5\u53D1\u9001 /use <session \u540D\u79F0> \u8FDB\u884C\u5207\u6362\u3002"
@@ -2048,6 +3216,41 @@ ${escapeFence2(output).slice(-6800)}
2048
3216
  const result3 = await this.useSession(target.id);
2049
3217
  return result3.ok ? { type: "success", content: `\u5DF2\u8FDE\u63A5\u5230 ${target.id}` } : { type: "error", content: result3.error };
2050
3218
  }
3219
+ if (action.kind === "session-stop") return this.handleSessionStopAction(action);
3220
+ if (action.kind === "session-start-error") {
3221
+ if (action.action === "create") return { type: "session-create-form", content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002" };
3222
+ if (action.action === "sessions") {
3223
+ await this.reconcileSessions(true);
3224
+ return this.sessionPickerActionResult("\u5DF2\u53D1\u9001\u6700\u65B0 Sessions\u3002");
3225
+ }
3226
+ return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u5931\u8D25\u5361\u7247\u64CD\u4F5C\u3002" };
3227
+ }
3228
+ if (action.kind === "startup-conflict") return this.handleStartupConflictAction(action);
3229
+ if (action.kind === "resume-picker") return this.handleResumePickerAction(action);
3230
+ if (action.kind === "session-create") {
3231
+ if (action.action === "open") {
3232
+ return {
3233
+ type: "session-create-form",
3234
+ content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
3235
+ sessions: Object.values(this.state.sessions ?? {}),
3236
+ activeSessionId: this.state.activeSessionId
3237
+ };
3238
+ }
3239
+ if (action.action !== "submit" || !event.action.formValue) {
3240
+ return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u8868\u5355\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
3241
+ }
3242
+ const request = startRequestFromForm(event.action.formValue);
3243
+ if (!request.ok) return { type: "error", content: request.error };
3244
+ const result3 = await this.startRemoteSession(request.value);
3245
+ if (!result3.ok) return larkStartupError(result3.error, request.value);
3246
+ if (result3.state === "picker") {
3247
+ return { type: "resume-picker", content: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5386\u53F2 Session\u3002", session: result3.session, picker: result3.picker };
3248
+ }
3249
+ if (result3.state === "conflict") {
3250
+ return { type: "startup-conflict", content: "\u76EE\u6807\u539F\u751F Session \u5DF2\u7531\u73B0\u6709 LCA Session \u8FDE\u63A5\u3002", request: result3.request, owner: result3.owner };
3251
+ }
3252
+ return { type: "session-created", content: remoteStartSuccess(result3.session), session: result3.session };
3253
+ }
2051
3254
  if (action.kind === "manual") {
2052
3255
  return this.withInteractionNotificationsSuppressed(() => this.handleManualAction(action, event));
2053
3256
  }
@@ -2112,7 +3315,7 @@ ${escapeFence2(output).slice(-6800)}
2112
3315
  return {
2113
3316
  type: "manual",
2114
3317
  content: "\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\u3002",
2115
- view: this.manualView(session, current2, "error", errorMessage2(error), operation, action.manualMode)
3318
+ view: this.manualView(session, current2, "error", errorMessage3(error), operation, action.manualMode)
2116
3319
  };
2117
3320
  }
2118
3321
  const current = this.screen ?? screen;
@@ -2130,6 +3333,142 @@ ${escapeFence2(output).slice(-6800)}
2130
3333
  view: this.manualView(session, current, "active", void 0, operation, action.manualMode)
2131
3334
  };
2132
3335
  }
3336
+ async handleSessionStopAction(action) {
3337
+ const sessionId = action.sessionId ?? "";
3338
+ const target = this.state.sessions?.[sessionId];
3339
+ if (!target || target.agent !== action.agent || target.paneId !== action.paneId || String(target.updatedAt) !== action.fingerprint) {
3340
+ return { type: "error", content: "\u76EE\u6807 session \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
3341
+ }
3342
+ if (action.action === "request") {
3343
+ return this.sessionPickerActionResult(`\u8BF7\u786E\u8BA4\u662F\u5426\u5173\u95ED ${target.id}\u3002`, target.id);
3344
+ }
3345
+ if (action.action === "cancel") {
3346
+ return this.sessionPickerActionResult(`\u5DF2\u53D6\u6D88\u5173\u95ED ${target.id}\u3002`);
3347
+ }
3348
+ if (action.action !== "confirm") return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u5173\u95ED\u64CD\u4F5C\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
3349
+ const stopped = await this.stopSession(target.id);
3350
+ if (!stopped.ok) return { type: "error", content: remoteError(stopped) };
3351
+ return this.sessionPickerActionResult(`\u5DF2\u5173\u95ED ${target.id}\u3002`);
3352
+ }
3353
+ async handleStartupConflictAction(action) {
3354
+ const requestedSessionId = action.sessionId ?? "";
3355
+ const pending = this.pendingStartupConflicts.get(requestedSessionId);
3356
+ const owner = pending ? this.state.sessions?.[pending.ownerSessionId] : void 0;
3357
+ if (!pending || !owner || pending.request.agent !== action.agent || owner.paneId !== action.paneId || String(owner.updatedAt) !== action.fingerprint) {
3358
+ return { type: "error", content: "Session \u51B2\u7A81\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA Session\u3002" };
3359
+ }
3360
+ if (action.action === "cancel") {
3361
+ this.pendingStartupConflicts.delete(requestedSessionId);
3362
+ return { type: "success", content: `\u5DF2\u53D6\u6D88\u521B\u5EFA ${requestedSessionId}\u3002` };
3363
+ }
3364
+ if (action.action === "connect") {
3365
+ const selected = await this.useSession(owner.id);
3366
+ if (!selected.ok) return { type: "error", content: remoteError(selected) };
3367
+ this.pendingStartupConflicts.delete(requestedSessionId);
3368
+ return { type: "session-created", content: `\u5DF2\u8FDE\u63A5\u73B0\u6709 session\u300C${owner.id}\u300D\u3002`, session: owner };
3369
+ }
3370
+ if (action.action !== "new") return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u51B2\u7A81\u5904\u7406\u64CD\u4F5C\u3002" };
3371
+ this.pendingStartupConflicts.delete(requestedSessionId);
3372
+ const result2 = await this.startRemoteSession({ ...pending.request, resume: void 0 });
3373
+ if (!result2.ok) return larkStartupError(result2.error, pending.request);
3374
+ if (result2.state !== "ready") return { type: "error", content: "\u542F\u52A8\u65B0\u4F1A\u8BDD\u65F6\u51FA\u73B0\u4E86\u610F\u5916\u6062\u590D\u72B6\u6001\uFF0C\u8BF7\u91CD\u8BD5\u3002" };
3375
+ return { type: "session-created", content: remoteStartSuccess(result2.session), session: result2.session };
3376
+ }
3377
+ sessionPickerActionResult(content, confirmingStopSessionId) {
3378
+ return {
3379
+ type: "session-picker",
3380
+ content,
3381
+ sessions: Object.values(this.state.sessions ?? {}),
3382
+ activeSessionId: this.state.activeSessionId,
3383
+ confirmingStopSessionId
3384
+ };
3385
+ }
3386
+ async handleResumePickerAction(action) {
3387
+ const sessionId = action.sessionId ?? "";
3388
+ const request = this.pendingResumePickers.get(sessionId);
3389
+ const session = this.state.sessions?.[sessionId];
3390
+ if (!request || !session || session.agent !== action.agent || session.paneId !== action.paneId) {
3391
+ return { type: "error", content: "Resume Picker \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA Session\u3002" };
3392
+ }
3393
+ if (action.action === "cancel") {
3394
+ this.pendingResumePickers.delete(sessionId);
3395
+ const stopped = await this.stopSession(sessionId);
3396
+ return stopped.ok ? { type: "success", content: `\u5DF2\u53D6\u6D88\u521B\u5EFA ${sessionId}\u3002` } : { type: "error", content: remoteError(stopped) };
3397
+ }
3398
+ let picker = await this.readResumePicker(session);
3399
+ if (!picker) {
3400
+ this.pendingResumePickers.delete(sessionId);
3401
+ return { type: "error", content: "\u539F\u751F Resume Picker \u5F53\u524D\u65E0\u6CD5\u5B89\u5168\u8BC6\u522B\uFF0C\u5DF2\u5141\u8BB8\u624B\u52A8\u9065\u63A7\u515C\u5E95\uFF1B\u8BF7\u53D1\u9001 /manual\u3002" };
3402
+ }
3403
+ if (action.action !== "refresh" && picker.fingerprint !== action.fingerprint) {
3404
+ return { type: "resume-picker", content: "Picker \u5DF2\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u4E3A\u6700\u65B0\u5185\u5BB9\u3002", session, picker };
3405
+ }
3406
+ if (action.action === "refresh") {
3407
+ return { type: "resume-picker", content: "\u5DF2\u5237\u65B0 Resume Picker\u3002", session, picker };
3408
+ }
3409
+ if (action.action === "previous" || action.action === "next") {
3410
+ await this.tmux.sendKey(session.paneId, action.action === "previous" ? "PPage" : "NPage");
3411
+ picker = await this.waitForResumePicker(session, picker.fingerprint) ?? picker;
3412
+ return { type: "resume-picker", content: "\u5DF2\u5207\u6362 Picker \u9875\u9762\u3002", session, picker };
3413
+ }
3414
+ const optionId = action.action.startsWith("select:") ? action.action.slice("select:".length) : "";
3415
+ const option2 = picker.options.find((candidate) => candidate.id === optionId);
3416
+ if (!option2) return { type: "resume-picker", content: "\u6240\u9009\u9879\u5DF2\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u3002", session, picker };
3417
+ const delta = option2.visibleIndex - picker.selectedIndex;
3418
+ const key = delta < 0 ? "Up" : "Down";
3419
+ for (let step = 0; step < Math.abs(delta); step += 1) await this.tmux.sendKey(session.paneId, key);
3420
+ const pane = await this.tmux.inspect(session.paneId);
3421
+ if (!pane || pane.dead) {
3422
+ this.pendingResumePickers.delete(sessionId);
3423
+ return larkStartupError(fail(await this.startupExitedError(session, pane)), request);
3424
+ }
3425
+ await this.tmux.sendKey(session.paneId, "Enter");
3426
+ this.pendingResumePickers.delete(sessionId);
3427
+ const claimed = await this.waitForInitialAgentSessionClaim(sessionId, pane.pid);
3428
+ if (!claimed.ok) {
3429
+ await this.stopSession(sessionId).catch(() => void 0);
3430
+ if (claimed.errorCode === "AGENT_SESSION_IN_USE") {
3431
+ const ownerSessionId = typeof claimed.errorContext?.ownerSessionId === "string" ? claimed.errorContext.ownerSessionId : void 0;
3432
+ const owner = ownerSessionId ? this.state.sessions?.[ownerSessionId] : void 0;
3433
+ if (owner) {
3434
+ this.pendingStartupConflicts.set(sessionId, { request, ownerSessionId: owner.id });
3435
+ return {
3436
+ type: "startup-conflict",
3437
+ content: "\u6240\u9009\u539F\u751F Session \u5DF2\u7531\u73B0\u6709 LCA Session \u8FDE\u63A5\u3002",
3438
+ request,
3439
+ owner
3440
+ };
3441
+ }
3442
+ }
3443
+ return larkStartupError(claimed, request);
3444
+ }
3445
+ await this.tmux.preserveOnExit(session.sessionName, false).catch(() => void 0);
3446
+ const selected = await this.useSession(sessionId);
3447
+ if (!selected.ok) return { type: "error", content: remoteError(selected) };
3448
+ return { type: "session-created", content: remoteStartSuccess(session), session };
3449
+ }
3450
+ async readResumePicker(session) {
3451
+ const pane = await this.tmux.inspect(session.paneId);
3452
+ if (!pane || pane.dead) return void 0;
3453
+ const raw = await this.tmux.capture(session.paneId, 120).catch(() => "");
3454
+ return parseResumePicker(raw, session.agent);
3455
+ }
3456
+ async handleResumePickerDeliveryFailure(candidate) {
3457
+ const current = this.state.sessions?.[candidate.id];
3458
+ if (!current || current.paneId !== candidate.paneId || !this.pendingResumePickers.has(candidate.id)) return;
3459
+ await this.log(`resume picker delivery failed; rolling back provisional session: session=${candidate.id} pane=${candidate.paneId}`);
3460
+ await this.stopSession(candidate.id);
3461
+ }
3462
+ async waitForResumePicker(session, previousFingerprint, timeoutMs = 2500) {
3463
+ const deadline = Date.now() + timeoutMs;
3464
+ let latest;
3465
+ while (Date.now() < deadline) {
3466
+ latest = await this.readResumePicker(session);
3467
+ if (latest && (!previousFingerprint || latest.fingerprint !== previousFingerprint)) return latest;
3468
+ await new Promise((resolve) => setTimeout(resolve, 100));
3469
+ }
3470
+ return latest;
3471
+ }
2133
3472
  currentManualView() {
2134
3473
  const session = this.activeSession();
2135
3474
  if (!session || !this.screen || this.screen.state === "exited") return void 0;
@@ -2176,7 +3515,7 @@ ${escapeFence2(output).slice(-6800)}
2176
3515
  ${escapeFence2(output).slice(-6500)}
2177
3516
  \`\`\``);
2178
3517
  } catch (error) {
2179
- await this.gateway?.sendText(chatId, `\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF1A${errorMessage2(error)}`);
3518
+ await this.gateway?.sendText(chatId, `\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF1A${errorMessage3(error)}`);
2180
3519
  }
2181
3520
  }
2182
3521
  async handleChoiceAction(action, session, event) {
@@ -2457,6 +3796,11 @@ ${escapeFence2(output).slice(-6500)}
2457
3796
  }
2458
3797
  const sessions = { ...this.state.sessions };
2459
3798
  delete sessions[sessionId];
3799
+ this.pendingResumePickers.delete(sessionId);
3800
+ this.pendingStartupConflicts.delete(sessionId);
3801
+ for (const [pendingId, conflict] of this.pendingStartupConflicts) {
3802
+ if (conflict.ownerSessionId === sessionId) this.pendingStartupConflicts.delete(pendingId);
3803
+ }
2460
3804
  const wasActive = this.state.activeSessionId === sessionId;
2461
3805
  const activeSessionId = wasActive ? Object.keys(sessions)[0] : this.state.activeSessionId;
2462
3806
  this.state = {
@@ -2515,7 +3859,7 @@ ${escapeFence2(output).slice(-6500)}
2515
3859
  }
2516
3860
  async runScheduledPoll() {
2517
3861
  const operation = this.poll().catch(async (error) => {
2518
- if (!this.closing) await this.log(`poll failed: ${errorMessage2(error)}`);
3862
+ if (!this.closing) await this.log(`poll failed: ${errorMessage3(error)}`);
2519
3863
  });
2520
3864
  this.pollInFlight = operation;
2521
3865
  try {
@@ -2549,6 +3893,7 @@ ${escapeFence2(output).slice(-6500)}
2549
3893
  await this.maybeNotifyCompletion();
2550
3894
  await this.flushPending();
2551
3895
  await this.reconcileSessions();
3896
+ await this.refreshNativeAgentSessionClaims();
2552
3897
  }
2553
3898
  async notifyTransition() {
2554
3899
  const current = this.screen;
@@ -2569,13 +3914,13 @@ ${escapeFence2(output).slice(-6500)}
2569
3914
  await this.gateway.sendText(this.state.boundChatId, `${agentName}/tmux pane \u5DF2\u9000\u51FA\u3002`);
2570
3915
  }
2571
3916
  } catch (error) {
2572
- await this.log(`notification failed: ${errorMessage2(error)}`);
3917
+ await this.log(`notification failed: ${errorMessage3(error)}`);
2573
3918
  }
2574
3919
  }
2575
3920
  async maybeNotifyUnresolved() {
2576
3921
  const screen = this.screen;
2577
3922
  const session = this.activeSession();
2578
- if (this.interactionNotificationsSuppressed > 0 || !screen || !session || !this.gateway || !this.state.boundChatId || screen.state !== "input" && screen.state !== "unknown" || safeStructuredInteraction(screen)) {
3923
+ if (this.interactionNotificationsSuppressed > 0 || !screen || !session || !this.gateway || !this.state.boundChatId || this.pendingResumePickers?.has(session.id) || screen.state !== "input" && screen.state !== "unknown" || safeStructuredInteraction(screen)) {
2579
3924
  this.unresolvedCandidate = void 0;
2580
3925
  return;
2581
3926
  }
@@ -2591,16 +3936,30 @@ ${escapeFence2(output).slice(-6500)}
2591
3936
  this.state.boundChatId,
2592
3937
  this.manualView(session, screen, "active", "\u5F53\u524D\u7EC8\u7AEF\u4EA4\u4E92\u65E0\u6CD5\u5B89\u5168\u8BC6\u522B\uFF0C\u5DF2\u81EA\u52A8\u8FDB\u5165\u624B\u52A8\u9065\u63A7\u515C\u5E95\u3002")
2593
3938
  );
3939
+ await this.log(
3940
+ `manual fallback sent: session=${session.id} agent=${session.agent} pane=${session.paneId} state=${screen.state} fingerprint=${screen.fingerprint}`
3941
+ );
2594
3942
  } catch (error) {
2595
- await this.log(`manual fallback card failed: ${errorMessage2(error)}`);
3943
+ await this.log(`manual fallback card failed: ${errorMessage3(error)}`);
2596
3944
  await this.gateway.sendText(
2597
3945
  this.state.boundChatId,
2598
3946
  "\u5F53\u524D\u7EC8\u7AEF\u4EA4\u4E92\u65E0\u6CD5\u5B89\u5168\u8BC6\u522B\uFF0C\u4E14\u624B\u52A8\u9065\u63A7\u5361\u53D1\u9001\u5931\u8D25\u3002\u53EF\u4F7F\u7528 /tail 120\u3001/key\u3001/type \u6216 /submit \u5904\u7406\u3002"
2599
- ).catch((sendError) => this.log(`manual fallback text failed: ${errorMessage2(sendError)}`));
3947
+ ).catch((sendError) => this.log(`manual fallback text failed: ${errorMessage3(sendError)}`));
2600
3948
  }
2601
3949
  }
2602
3950
  async handleTurnComplete(candidate) {
2603
3951
  if (!validTurnCompleteCandidate(candidate)) return { ok: false, error: "invalid turn-complete candidate" };
3952
+ const completingSession = this.state.sessions?.[candidate.sessionId];
3953
+ if (completingSession) {
3954
+ const claimed = await this.handleAgentSessionStarted({
3955
+ sessionId: candidate.sessionId,
3956
+ agent: completingSession.agent,
3957
+ agentSessionId: candidate.agentSessionId,
3958
+ cwd: candidate.cwd,
3959
+ source: "Stop"
3960
+ });
3961
+ if (!claimed.ok) return claimed;
3962
+ }
2604
3963
  const eventKey = `${candidate.agentSessionId}:${candidate.eventId}`;
2605
3964
  if (this.completedEvents.has(eventKey)) return { ok: true };
2606
3965
  remember(this.completedEvents, eventKey, 256);
@@ -2610,6 +3969,142 @@ ${escapeFence2(output).slice(-6500)}
2610
3969
  this.pendingCompletionAt = Date.now();
2611
3970
  return { ok: true };
2612
3971
  }
3972
+ async handleAgentSessionStarted(candidate) {
3973
+ if (!validSessionStartCandidate(candidate)) return { ok: false, error: "invalid agent-session candidate" };
3974
+ const session = this.state.sessions?.[candidate.sessionId];
3975
+ if (session && session.agent !== candidate.agent) {
3976
+ return { ok: false, error: "agent-session candidate does not match managed session" };
3977
+ }
3978
+ const owner = this.findAgentSessionOwner(candidate.agent, candidate.agentSessionId, candidate.sessionId);
3979
+ if (owner) {
3980
+ this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
3981
+ await this.log(
3982
+ `agent session conflict: session=${candidate.sessionId} agent=${candidate.agent} agentSession=${candidate.agentSessionId} owner=${owner.id}`
3983
+ );
3984
+ setTimeout(() => void this.rejectDuplicateAgentSession(candidate, owner), 100);
3985
+ return fail(agentSessionInUse(candidate.sessionId, owner.id));
3986
+ }
3987
+ if (!session) {
3988
+ this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
3989
+ return { ok: true };
3990
+ }
3991
+ if (session.agentSessionId === candidate.agentSessionId) return { ok: true };
3992
+ const updated = { ...session, agentSessionId: candidate.agentSessionId, updatedAt: Date.now() };
3993
+ this.state = {
3994
+ ...this.state,
3995
+ sessions: { ...this.state.sessions, [session.id]: updated },
3996
+ updatedAt: Date.now()
3997
+ };
3998
+ this.pendingAgentSessionClaims.delete(session.id);
3999
+ await this.store.saveState(this.state);
4000
+ await this.tmux.writeMetadata(session.sessionName, {
4001
+ managed: true,
4002
+ sessionId: session.id,
4003
+ agent: session.agent,
4004
+ cwd: session.cwd,
4005
+ agentVersion: session.agentVersion,
4006
+ agentSessionId: candidate.agentSessionId
4007
+ }).catch((error) => this.log(`failed to persist agent session claim for ${session.id}: ${errorMessage3(error)}`));
4008
+ await this.log(
4009
+ `agent session claimed: session=${session.id} agent=${session.agent} agentSession=${candidate.agentSessionId}`
4010
+ );
4011
+ return { ok: true };
4012
+ }
4013
+ async refreshNativeAgentSessionClaims() {
4014
+ for (const session of Object.values(this.state.sessions ?? {})) {
4015
+ if (session.agentSessionId) continue;
4016
+ const pane = await this.tmux.inspect(session.paneId);
4017
+ if (!pane || pane.dead) continue;
4018
+ const agentSessionId = await resolveNativeAgentSessionId(session.agent, pane.pid).catch((error) => {
4019
+ void this.log(`failed to resolve native agent session for ${session.id}: ${errorMessage3(error)}`);
4020
+ return void 0;
4021
+ });
4022
+ if (!agentSessionId) continue;
4023
+ await this.handleAgentSessionStarted({
4024
+ sessionId: session.id,
4025
+ agent: session.agent,
4026
+ agentSessionId,
4027
+ cwd: session.cwd,
4028
+ source: "runtime-discovery"
4029
+ });
4030
+ }
4031
+ }
4032
+ async waitForInitialAgentSessionClaim(sessionId, panePid) {
4033
+ const deadline = Date.now() + 3500;
4034
+ while (Date.now() < deadline) {
4035
+ const session2 = this.state.sessions?.[sessionId];
4036
+ if (!session2) return { ok: false, error: `session disappeared during startup: ${sessionId}` };
4037
+ if (session2.agentSessionId) return { ok: true };
4038
+ const pane2 = await this.tmux.inspect(session2.paneId);
4039
+ if (!pane2 || pane2.dead) return fail(await this.startupExitedError(session2, pane2));
4040
+ const agentSessionId = await resolveNativeAgentSessionId(session2.agent, panePid).catch(() => void 0);
4041
+ if (agentSessionId) {
4042
+ return this.handleAgentSessionStarted({
4043
+ sessionId,
4044
+ agent: session2.agent,
4045
+ agentSessionId,
4046
+ cwd: session2.cwd,
4047
+ source: "startup-discovery"
4048
+ });
4049
+ }
4050
+ await new Promise((resolve) => setTimeout(resolve, 100));
4051
+ }
4052
+ const session = this.state.sessions?.[sessionId];
4053
+ if (!session) return fail(new AppError("SESSION_NOT_FOUND", `session disappeared during startup: ${sessionId}`, { sessionId }));
4054
+ const pane = await this.tmux.inspect(session.paneId);
4055
+ if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4056
+ if (session.agentSessionId || this.pendingAgentSessionClaims.has(sessionId)) return { ok: true };
4057
+ return fail(new AppError(
4058
+ "AGENT_IDENTITY_TIMEOUT",
4059
+ `unable to identify resumed native session: ${sessionId}`,
4060
+ { sessionId, agent: session.agent }
4061
+ ));
4062
+ }
4063
+ async startupExitedError(session, pane) {
4064
+ const terminalTail = await this.tmux.capture(session.paneId, 40).then((output) => tailScreen(output, 40).slice(-3e3)).catch(() => "");
4065
+ return new AppError(
4066
+ "AGENT_EXITED_DURING_STARTUP",
4067
+ `agent exited during startup: ${session.id}`,
4068
+ {
4069
+ sessionId: session.id,
4070
+ agent: session.agent,
4071
+ terminalExcerpt: startupTerminalExcerpt(terminalTail),
4072
+ exitStatus: pane?.exitStatus
4073
+ }
4074
+ );
4075
+ }
4076
+ async waitForStartupStability(session, durationMs) {
4077
+ const deadline = Date.now() + durationMs;
4078
+ while (Date.now() < deadline) {
4079
+ const pane = await this.tmux.inspect(session.paneId);
4080
+ if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4081
+ await new Promise((resolve) => setTimeout(resolve, 80));
4082
+ }
4083
+ return { ok: true };
4084
+ }
4085
+ findAgentSessionOwner(agent, agentSessionId, excludeSessionId) {
4086
+ const persisted = Object.values(this.state.sessions ?? {}).find((candidate) => candidate.id !== excludeSessionId && candidate.agent === agent && candidate.agentSessionId === agentSessionId);
4087
+ if (persisted) return persisted;
4088
+ const pending = [...this.pendingAgentSessionClaims.values()].find((candidate) => candidate.sessionId !== excludeSessionId && candidate.agent === agent && candidate.agentSessionId === agentSessionId);
4089
+ return pending ? { id: pending.sessionId } : void 0;
4090
+ }
4091
+ async rejectDuplicateAgentSession(candidate, owner) {
4092
+ const duplicate = this.state.sessions?.[candidate.sessionId];
4093
+ if (duplicate) await this.stopSession(duplicate.id).catch((error) => this.log(
4094
+ `failed to stop duplicate agent session ${duplicate.id}: ${errorMessage3(error)}`
4095
+ ));
4096
+ else {
4097
+ const sessionName = `${this.sessionName}-${candidate.sessionId}`;
4098
+ if (await this.tmux.hasSession(sessionName)) await this.tmux.killSession(sessionName).catch(() => void 0);
4099
+ }
4100
+ this.pendingAgentSessionClaims.delete(candidate.sessionId);
4101
+ if (this.state.boundChatId) {
4102
+ await this.gateway?.sendText(
4103
+ this.state.boundChatId,
4104
+ `${candidate.agent} \u539F\u751F session \u5DF2\u7531 LCA session\u300C${owner.id}\u300D\u8FDE\u63A5\uFF1B\u5DF2\u505C\u6B62\u91CD\u590D\u521B\u5EFA\u7684\u300C${candidate.sessionId}\u300D\u3002\u8BF7\u7528 /sessions \u8FDE\u63A5\u300C${owner.id}\u300D\u3002`
4105
+ ).catch((error) => this.log(`agent session conflict notification failed: ${errorMessage3(error)}`));
4106
+ }
4107
+ }
2613
4108
  updateScreen(next) {
2614
4109
  if (this.screen?.fingerprint !== next.fingerprint || this.screen.state !== next.state) {
2615
4110
  this.outputStableSince = Date.now();
@@ -2661,31 +4156,29 @@ ${output}`
2661
4156
  activeSession() {
2662
4157
  return this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
2663
4158
  }
2664
- async reconcileSessions() {
2665
- const sessions = Object.values(this.state.sessions ?? {});
2666
- const inspections = await Promise.all(sessions.map(async (session) => ({
2667
- session,
2668
- pane: await this.tmux.inspect(session.paneId)
2669
- })));
2670
- const liveSessions = inspections.filter(({ pane }) => pane && !pane.dead).map(({ session }) => session);
2671
- const liveById = Object.fromEntries(liveSessions.map((session) => [session.id, session]));
2672
- const activeSessionId = this.state.activeSessionId && liveById[this.state.activeSessionId] ? this.state.activeSessionId : liveSessions[0]?.id;
2673
- const sessionsChanged = liveSessions.length !== sessions.length;
2674
- const activeChanged = activeSessionId !== this.state.activeSessionId;
2675
- if (!sessionsChanged && !activeChanged) return liveSessions;
2676
- const removedActive = this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
2677
- if (activeChanged && removedActive && !liveById[removedActive.id] && this.previousScreen?.state !== "exited" && this.state.boundChatId) {
4159
+ async reconcileSessions(discover = false) {
4160
+ const previousActive = this.state.activeSessionId;
4161
+ const result2 = await this.reconciler.reconcile(this.state, discover);
4162
+ if (!result2.changed) return result2.liveSessions;
4163
+ const activeChanged = result2.state.activeSessionId !== previousActive;
4164
+ if (result2.removedActive) {
4165
+ await this.log(
4166
+ `session removed: session=${result2.removedActive.id} agent=${result2.removedActive.agent} pane=${result2.removedActive.paneId} wasActive=true nextActive=${result2.state.activeSessionId ?? "-"}`
4167
+ );
4168
+ }
4169
+ if (activeChanged && result2.removedActive && this.previousScreen?.state !== "exited" && this.state.boundChatId) {
2678
4170
  await this.gateway?.sendText(
2679
4171
  this.state.boundChatId,
2680
- `${getAgentAdapter(removedActive.agent).displayName}/tmux pane \u5DF2\u9000\u51FA\u3002`
2681
- ).catch((error) => this.log(`exit notification failed: ${errorMessage2(error)}`));
4172
+ `${getAgentAdapter(result2.removedActive.agent).displayName}/tmux pane \u5DF2\u9000\u51FA\u3002`
4173
+ ).catch((error) => this.log(`exit notification failed: ${errorMessage3(error)}`));
4174
+ }
4175
+ this.state = result2.state;
4176
+ for (const sessionId of this.pendingResumePickers.keys()) {
4177
+ if (!this.state.sessions?.[sessionId]) this.pendingResumePickers.delete(sessionId);
4178
+ }
4179
+ for (const [sessionId, conflict] of this.pendingStartupConflicts) {
4180
+ if (!this.state.sessions?.[conflict.ownerSessionId]) this.pendingStartupConflicts.delete(sessionId);
2682
4181
  }
2683
- this.state = {
2684
- ...this.state,
2685
- sessions: liveById,
2686
- activeSessionId,
2687
- updatedAt: Date.now()
2688
- };
2689
4182
  if (activeChanged) {
2690
4183
  this.screen = void 0;
2691
4184
  this.previousScreen = void 0;
@@ -2697,11 +4190,13 @@ ${output}`
2697
4190
  this.unresolvedNotified.clear();
2698
4191
  }
2699
4192
  await this.store.saveState(this.state);
2700
- return liveSessions;
4193
+ return result2.liveSessions;
2701
4194
  }
2702
4195
  async useSession(sessionId) {
2703
4196
  const session = this.state.sessions?.[sessionId];
2704
- if (!session || !await this.tmux.inspect(session.paneId)) return { ok: false, error: `unknown or stopped session: ${sessionId}` };
4197
+ if (!session) return fail(new AppError("SESSION_NOT_FOUND", `unknown session: ${sessionId}`, { sessionId }));
4198
+ const pane = await this.tmux.inspect(session.paneId);
4199
+ if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
2705
4200
  this.state = { ...this.state, activeSessionId: sessionId, updatedAt: Date.now() };
2706
4201
  this.screen = void 0;
2707
4202
  this.previousScreen = void 0;
@@ -2713,8 +4208,44 @@ ${output}`
2713
4208
  this.unresolvedNotified.clear();
2714
4209
  await this.store.saveState(this.state);
2715
4210
  await this.poll();
4211
+ await this.log(`session activated: session=${session.id} agent=${session.agent} pane=${session.paneId}`);
2716
4212
  return { ok: true, value: session };
2717
4213
  }
4214
+ async startRemoteSession(request) {
4215
+ await this.log(
4216
+ `remote session create requested: session=${request.sessionId} agent=${request.agent} resume=${request.resume?.mode ?? "new"}`
4217
+ );
4218
+ const started = await this.startSession(request.sessionId, request.cwd, request.agent, request.resume);
4219
+ if (!started.ok) {
4220
+ await this.log(`remote session create failed: session=${request.sessionId} code=${started.errorCode ?? "UNKNOWN"}`);
4221
+ if (started.errorCode === "AGENT_SESSION_IN_USE") {
4222
+ const ownerSessionId = typeof started.errorContext?.ownerSessionId === "string" ? started.errorContext.ownerSessionId : void 0;
4223
+ const owner = ownerSessionId ? this.state.sessions?.[ownerSessionId] : void 0;
4224
+ if (owner) {
4225
+ this.pendingStartupConflicts.set(request.sessionId, { request, ownerSessionId: owner.id });
4226
+ return { ok: true, state: "conflict", request, owner };
4227
+ }
4228
+ }
4229
+ return { ok: false, error: started };
4230
+ }
4231
+ const selected = await this.useSession(request.sessionId);
4232
+ if (!selected.ok) {
4233
+ await this.log(`remote session activation failed: session=${request.sessionId} code=${selected.errorCode ?? "UNKNOWN"}`);
4234
+ return { ok: false, error: selected };
4235
+ }
4236
+ const session = selected.value;
4237
+ if (request.resume?.mode === "picker") {
4238
+ const picker = await this.waitForResumePicker(session);
4239
+ if (picker) {
4240
+ this.pendingResumePickers.set(session.id, request);
4241
+ return { ok: true, state: "picker", session, picker };
4242
+ }
4243
+ await this.tmux.preserveOnExit(session.sessionName, false).catch((error) => this.log(
4244
+ `failed to disable remain-on-exit after resume picker fallback: session=${session.id} error=${errorMessage3(error)}`
4245
+ ));
4246
+ }
4247
+ return { ok: true, state: "ready", session };
4248
+ }
2718
4249
  async log(message) {
2719
4250
  await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
2720
4251
  `, { mode: 384 });
@@ -2723,7 +4254,14 @@ ${output}`
2723
4254
  function fail(error) {
2724
4255
  return { ok: false, ...serializeAppError(error) };
2725
4256
  }
2726
- function errorMessage2(error) {
4257
+ function agentSessionInUse(sessionId, ownerSessionId) {
4258
+ return new AppError(
4259
+ "AGENT_SESSION_IN_USE",
4260
+ `agent session is already managed by ${ownerSessionId}`,
4261
+ { sessionId, ownerSessionId }
4262
+ );
4263
+ }
4264
+ function errorMessage3(error) {
2727
4265
  return error instanceof Error ? error.message : String(error);
2728
4266
  }
2729
4267
  function isAlreadyExists(error) {
@@ -2737,9 +4275,6 @@ function processIsAlive(pid) {
2737
4275
  return error instanceof Error && "code" in error && error.code === "EPERM";
2738
4276
  }
2739
4277
  }
2740
- function validSessionId(value) {
2741
- return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
2742
- }
2743
4278
  function remember(values, value, limit) {
2744
4279
  values.add(value);
2745
4280
  while (values.size > limit) {
@@ -2748,6 +4283,76 @@ function remember(values, value, limit) {
2748
4283
  values.delete(oldest);
2749
4284
  }
2750
4285
  }
4286
+ function startRequestFromForm(values) {
4287
+ const sessionId = formString(values[SESSION_CREATE_NAME_FIELD]);
4288
+ const agentValue = formString(values[SESSION_CREATE_AGENT_FIELD]);
4289
+ const cwd = formString(values[SESSION_CREATE_CWD_FIELD]);
4290
+ const resumeMode = formString(values[SESSION_CREATE_RESUME_FIELD]) || "new";
4291
+ if (!sessionId) return { ok: false, error: "\u8BF7\u586B\u5199 Session \u540D\u79F0\u3002" };
4292
+ const agent = normalizeAgentId(agentValue);
4293
+ if (!agent) return { ok: false, error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
4294
+ if (!cwd) return { ok: false, error: "\u8BF7\u586B\u5199\u7EDD\u5BF9\u5DE5\u4F5C\u76EE\u5F55\u3002" };
4295
+ let resume;
4296
+ if (resumeMode === "last") {
4297
+ return { ok: false, error: "\u98DE\u4E66\u5DF2\u4E0D\u518D\u652F\u6301\u201C\u6062\u590D\u4E0A\u6B21\u4F1A\u8BDD\u201D\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u8868\u5355\u5E76\u4F7F\u7528 Resume Picker\u3002" };
4298
+ }
4299
+ if (resumeMode === "picker") resume = { mode: "picker" };
4300
+ else if (resumeMode !== "new") {
4301
+ return { ok: false, error: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
4302
+ }
4303
+ return { ok: true, value: { sessionId, agent, cwd, resume } };
4304
+ }
4305
+ function formString(value) {
4306
+ if (typeof value === "string") return value.trim();
4307
+ if (value && typeof value === "object" && typeof value.value === "string") {
4308
+ return value.value.trim();
4309
+ }
4310
+ return "";
4311
+ }
4312
+ function remoteStartSuccess(session) {
4313
+ return `\u5DF2\u542F\u52A8\u5E76\u8FDE\u63A5 ${getAgentAdapter(session.agent).displayName} session\u300C${session.id}\u300D\u3002`;
4314
+ }
4315
+ function larkStartupError(result2, request) {
4316
+ const failure = sessionStartupFailure(result2, request);
4317
+ return failure ? { type: "session-start-failed", content: `${failure.agent} \u542F\u52A8\u5931\u8D25\u3002`, failure } : { type: "error", content: remoteError(result2) };
4318
+ }
4319
+ function remoteError(result2) {
4320
+ if (result2.ok) return "\u64CD\u4F5C\u5DF2\u5B8C\u6210\u3002";
4321
+ const context = result2.errorContext ?? {};
4322
+ const sessionId = typeof context.sessionId === "string" ? context.sessionId : "\u8BE5\u540D\u79F0";
4323
+ switch (result2.errorCode) {
4324
+ case "SESSION_EXISTS":
4325
+ return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 session \u5DF2\u5728\u8FD0\u884C\u3002\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0\uFF0C\u6216\u7528 /sessions \u8FDE\u63A5\u73B0\u6709 session\u3002`;
4326
+ case "AGENT_SESSION_IN_USE": {
4327
+ const ownerSessionId = typeof context.ownerSessionId === "string" ? context.ownerSessionId : "\u73B0\u6709 session";
4328
+ return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 Agent \u539F\u751F session \u5DF2\u7531 LCA session\u300C${ownerSessionId}\u300D\u8FDE\u63A5\u3002\u8BF7\u7528 /sessions \u8FDE\u63A5\u73B0\u6709 session\u3002`;
4329
+ }
4330
+ case "AGENT_EXITED_DURING_STARTUP": {
4331
+ const agent = typeof context.agent === "string" ? context.agent : "Agent";
4332
+ const exitStatus = typeof context.exitStatus === "number" ? `\uFF08\u9000\u51FA\u7801 ${context.exitStatus}\uFF09` : "";
4333
+ const excerpt = typeof context.terminalExcerpt === "string" ? context.terminalExcerpt : "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002";
4334
+ return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A${agent} \u542F\u52A8\u540E\u7ACB\u5373\u9000\u51FA${exitStatus}\u3002
4335
+
4336
+ \u539F\u59CB\u9519\u8BEF\uFF1A
4337
+ ${excerpt}`;
4338
+ }
4339
+ case "AGENT_IDENTITY_TIMEOUT":
4340
+ return `\u65E0\u6CD5\u6062\u590D session\u300C${sessionId}\u300D\uFF1AAgent \u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u672A\u80FD\u786E\u8BA4\u539F\u751F session ID\u3002\u8BF7\u91CD\u8BD5\u6216\u542F\u52A8\u65B0\u4F1A\u8BDD\u3002`;
4341
+ case "INVALID_SESSION_NAME":
4342
+ return "Session \u540D\u79F0\u65E0\u6548\uFF1A\u53EA\u80FD\u5305\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u77ED\u6A2A\u7EBF\uFF0C\u957F\u5EA6\u4E3A 1\u201340 \u4E2A\u5B57\u7B26\u3002";
4343
+ case "INVALID_CWD":
4344
+ return `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u53EF\u7528\uFF1A${typeof context.cwd === "string" ? context.cwd : "\u8BF7\u586B\u5199\u672C\u673A\u5B58\u5728\u7684\u7EDD\u5BF9\u8DEF\u5F84"}\u3002`;
4345
+ case "BINARY_NOT_FOUND":
4346
+ return `\u627E\u4E0D\u5230 Agent \u547D\u4EE4\uFF1A${typeof context.binary === "string" ? context.binary : "\u8BF7\u68C0\u67E5\u5B89\u88C5\u4E0E PATH"}\u3002`;
4347
+ case "INVALID_OPTIONS":
4348
+ case "INVALID_RESUME":
4349
+ return typeof context.reason === "string" ? context.reason : "\u542F\u52A8\u53C2\u6570\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002";
4350
+ case "START_FAILED":
4351
+ return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\u3002\u8BF7\u68C0\u67E5\u5DE5\u4F5C\u76EE\u5F55\u3001Agent \u5B89\u88C5\uFF0C\u6216\u5728\u672C\u673A\u8FD0\u884C lca logs \u67E5\u770B\u65E5\u5FD7\u3002`;
4352
+ default:
4353
+ return "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u53EF\u5728\u672C\u673A\u8FD0\u884C lca status \u548C lca logs \u68C0\u67E5\u3002";
4354
+ }
4355
+ }
2751
4356
  function escapeFence2(value) {
2752
4357
  return value.replace(/```/g, "``\\`");
2753
4358
  }
@@ -2794,27 +4399,27 @@ function manualTimestamp() {
2794
4399
 
2795
4400
  // src/core/paths.ts
2796
4401
  import { homedir as homedir2 } from "os";
2797
- import { join } from "path";
4402
+ import { join as join2 } from "path";
2798
4403
  function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
2799
- const base = root || join(homedir2(), ".lark-coding-assistant");
4404
+ const base = root || join2(homedir2(), ".lark-coding-assistant");
2800
4405
  return {
2801
4406
  root: base,
2802
- config: join(base, "config.json"),
2803
- secrets: join(base, "secrets.json"),
2804
- state: join(base, "state.json"),
2805
- logsDir: join(base, "logs"),
2806
- logFile: join(base, "logs", "assistant.log"),
2807
- runtimeDir: join(base, "runtime"),
2808
- socket: join(base, "runtime", "daemon.sock"),
2809
- pid: join(base, "runtime", "daemon.pid")
4407
+ config: join2(base, "config.json"),
4408
+ secrets: join2(base, "secrets.json"),
4409
+ state: join2(base, "state.json"),
4410
+ logsDir: join2(base, "logs"),
4411
+ logFile: join2(base, "logs", "assistant.log"),
4412
+ runtimeDir: join2(base, "runtime"),
4413
+ socket: join2(base, "runtime", "daemon.sock"),
4414
+ pid: join2(base, "runtime", "daemon.pid")
2810
4415
  };
2811
4416
  }
2812
4417
 
2813
4418
  // src/daemon-entry.ts
2814
- import { readFile as readFile3 } from "fs/promises";
4419
+ import { readFile as readFile4 } from "fs/promises";
2815
4420
  var paths = resolveAppPaths();
2816
4421
  var packageInfo = JSON.parse(
2817
- await readFile3(new URL("../package.json", import.meta.url), "utf8")
4422
+ await readFile4(new URL("../package.json", import.meta.url), "utf8")
2818
4423
  );
2819
4424
  var daemon = new AssistantDaemon(
2820
4425
  new AppStore(paths),