lark-coding-assistant 0.2.3 → 0.2.5

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.
@@ -12,6 +12,7 @@ function runFile(file, args, options = {}) {
12
12
  {
13
13
  cwd: options.cwd,
14
14
  timeout: options.timeoutMs ?? 1e4,
15
+ signal: options.signal,
15
16
  encoding: "utf8",
16
17
  maxBuffer: 4 * 1024 * 1024
17
18
  },
@@ -110,8 +111,8 @@ import { isAbsolute, normalize, resolve } from "path";
110
111
  var AppError = class extends Error {
111
112
  code;
112
113
  context;
113
- constructor(code, message, context = {}, options = {}) {
114
- super(message, options);
114
+ constructor(code, message2, context = {}, options = {}) {
115
+ super(message2, options);
115
116
  this.name = "AppError";
116
117
  this.code = code;
117
118
  this.context = context;
@@ -120,6 +121,13 @@ var AppError = class extends Error {
120
121
  function isAppError(error) {
121
122
  return error instanceof AppError;
122
123
  }
124
+ function asAppError(error, code = "UNKNOWN", context = {}) {
125
+ if (isAppError(error)) {
126
+ if (Object.keys(context).length === 0) return error;
127
+ return new AppError(error.code, error.message, { ...context, ...error.context }, { cause: error });
128
+ }
129
+ return new AppError(code, errorMessage(error), context, { cause: error });
130
+ }
123
131
  function errorMessage(error) {
124
132
  return error instanceof Error ? error.message : String(error);
125
133
  }
@@ -293,16 +301,138 @@ function validSessionId(value) {
293
301
 
294
302
  // src/session/startup-failure.ts
295
303
  function sessionStartupFailure(result2, fallback) {
296
- if (result2.ok || result2.errorCode !== "AGENT_EXITED_DURING_STARTUP") return void 0;
304
+ if (result2.ok || result2.errorCode !== "AGENT_EXITED_DURING_STARTUP" && result2.errorCode !== "SESSION_START_TIMEOUT") return void 0;
297
305
  const context = result2.errorContext ?? {};
298
306
  return {
299
307
  sessionId: typeof context.sessionId === "string" ? context.sessionId : fallback.sessionId,
300
308
  agent: fallback.agent,
309
+ reason: result2.errorCode === "SESSION_START_TIMEOUT" ? "timeout" : "exited",
310
+ ...typeof context.cwd === "string" ? { cwd: context.cwd } : {},
311
+ ...typeof context.stage === "string" ? { stage: context.stage } : {},
312
+ ...typeof context.elapsedMs === "number" ? { elapsedMs: context.elapsedMs } : {},
301
313
  ...typeof context.exitStatus === "number" ? { exitStatus: context.exitStatus } : {},
302
314
  terminalExcerpt: typeof context.terminalExcerpt === "string" && context.terminalExcerpt.trim() ? context.terminalExcerpt : "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002"
303
315
  };
304
316
  }
305
317
 
318
+ // src/session/start-coordinator.ts
319
+ import { randomUUID } from "crypto";
320
+ var SessionStartCoordinator = class {
321
+ constructor(timeoutMs = 3e4, log = async () => void 0) {
322
+ this.timeoutMs = timeoutMs;
323
+ this.log = log;
324
+ }
325
+ timeoutMs;
326
+ log;
327
+ active = /* @__PURE__ */ new Map();
328
+ async run(descriptor, execute, cleanup) {
329
+ const existing = this.active.get(descriptor.sessionId);
330
+ if (existing) {
331
+ return {
332
+ ok: false,
333
+ error: new AppError("SESSION_STARTING", `session is already starting: ${descriptor.sessionId}`, {
334
+ sessionId: descriptor.sessionId,
335
+ agent: descriptor.agent,
336
+ cwd: descriptor.cwd,
337
+ startId: existing.startId,
338
+ elapsedMs: Date.now() - existing.startedAt
339
+ })
340
+ };
341
+ }
342
+ const startId = randomUUID();
343
+ const startedAt = Date.now();
344
+ const deadline = startedAt + this.timeoutMs;
345
+ const controller = new AbortController();
346
+ let currentStage = "initializing";
347
+ const fields = logFields({ ...descriptor, startId });
348
+ const context = {
349
+ ...descriptor,
350
+ startId,
351
+ startedAt,
352
+ deadline,
353
+ signal: controller.signal,
354
+ remainingMs: () => Math.max(1, deadline - Date.now()),
355
+ stage: async (name, operation2) => {
356
+ if (controller.signal.aborted || Date.now() >= deadline) {
357
+ throw timeoutError(descriptor, startId, name, startedAt, this.timeoutMs);
358
+ }
359
+ currentStage = name;
360
+ const stageStartedAt = Date.now();
361
+ await this.record(`session start stage requested: ${fields} stage=${name}`);
362
+ try {
363
+ const value = await operation2();
364
+ await this.record(`session start stage succeeded: ${fields} stage=${name} elapsedMs=${Date.now() - stageStartedAt}`);
365
+ return value;
366
+ } catch (error) {
367
+ const normalized = controller.signal.aborted || Date.now() >= deadline ? timeoutError(descriptor, startId, name, startedAt, this.timeoutMs) : asAppError(error, "START_FAILED", startErrorContext(descriptor, startId, name, startedAt));
368
+ await this.record(`session start stage failed: ${fields} stage=${name} code=${normalized.code} elapsedMs=${Date.now() - stageStartedAt}`);
369
+ throw normalized;
370
+ } finally {
371
+ if (currentStage === name) currentStage = "finalizing";
372
+ }
373
+ }
374
+ };
375
+ this.active.set(descriptor.sessionId, { startId, startedAt });
376
+ await this.record(`session start requested: ${fields}`);
377
+ let timer;
378
+ const timeout = new Promise((_resolve, reject) => {
379
+ timer = setTimeout(() => {
380
+ const error = timeoutError(descriptor, startId, currentStage, startedAt, this.timeoutMs);
381
+ controller.abort(error);
382
+ reject(error);
383
+ }, this.timeoutMs);
384
+ });
385
+ const operation = execute(context);
386
+ try {
387
+ const value = await Promise.race([operation, timeout]);
388
+ await this.record(`session start succeeded: ${fields} elapsedMs=${Date.now() - startedAt}`);
389
+ return { ok: true, value };
390
+ } catch (error) {
391
+ const normalized = controller.signal.aborted || Date.now() >= deadline ? timeoutError(descriptor, startId, errorStage(error), startedAt, this.timeoutMs) : asAppError(error, "START_FAILED", startErrorContext(descriptor, startId, errorStage(error), startedAt));
392
+ if (!controller.signal.aborted) controller.abort(normalized);
393
+ await operation.catch(() => void 0);
394
+ await cleanup(context, normalized).catch(async (cleanupError) => {
395
+ await this.record(`session start cleanup failed: ${fields} code=${normalized.code} detail=${message(cleanupError)}`);
396
+ });
397
+ await this.record(`${normalized.code === "SESSION_START_TIMEOUT" ? "session start timed out" : "session start failed"}: ${fields} code=${normalized.code} stage=${String(normalized.context.stage ?? "unknown")} elapsedMs=${Date.now() - startedAt}`);
398
+ return { ok: false, error: normalized };
399
+ } finally {
400
+ if (timer) clearTimeout(timer);
401
+ this.active.delete(descriptor.sessionId);
402
+ }
403
+ }
404
+ async record(message2) {
405
+ await this.log(message2).catch(() => void 0);
406
+ }
407
+ };
408
+ function timeoutError(descriptor, startId, stage, startedAt, timeoutMs) {
409
+ return new AppError("SESSION_START_TIMEOUT", `session start exceeded ${timeoutMs} ms: ${descriptor.sessionId}`, {
410
+ ...startErrorContext(descriptor, startId, stage, startedAt),
411
+ timeoutMs
412
+ });
413
+ }
414
+ function startErrorContext(descriptor, startId, stage, startedAt) {
415
+ return {
416
+ sessionId: descriptor.sessionId,
417
+ agent: descriptor.agent,
418
+ cwd: descriptor.cwd,
419
+ resume: descriptor.resume?.mode ?? "new",
420
+ source: descriptor.source,
421
+ startId,
422
+ stage,
423
+ elapsedMs: Date.now() - startedAt
424
+ };
425
+ }
426
+ function errorStage(error) {
427
+ return error instanceof AppError && typeof error.context.stage === "string" ? error.context.stage : "unknown";
428
+ }
429
+ function logFields(descriptor) {
430
+ return `startId=${descriptor.startId} source=${descriptor.source} session=${descriptor.sessionId} agent=${descriptor.agent} cwd=${JSON.stringify(descriptor.cwd)} resume=${descriptor.resume?.mode ?? "new"}`;
431
+ }
432
+ function message(error) {
433
+ return error instanceof Error ? error.message : String(error);
434
+ }
435
+
306
436
  // src/session/reconciler.ts
307
437
  var SessionReconciler = class {
308
438
  constructor(tmux, sessionPrefix = "lark-coding-assistant", missingThreshold = 3, log = async () => void 0, resolveAgentVersion = async () => "unknown") {
@@ -318,11 +448,12 @@ var SessionReconciler = class {
318
448
  log;
319
449
  resolveAgentVersion;
320
450
  misses = /* @__PURE__ */ new Map();
321
- async reconcile(input, discover = false) {
451
+ async reconcile(input, discover = false, signal) {
322
452
  const sessions = { ...input.sessions };
323
453
  let changed = false;
324
454
  for (const [id, session] of Object.entries(sessions)) {
325
- const result2 = await this.confirm(session);
455
+ if (signal?.aborted) throw signal.reason;
456
+ const result2 = await this.confirm(session, signal);
326
457
  if (result2.status === "unavailable") {
327
458
  await this.log(`tmux inspection unavailable for ${id}: ${errorMessage2(result2.error)}`);
328
459
  continue;
@@ -336,7 +467,7 @@ var SessionReconciler = class {
336
467
  continue;
337
468
  }
338
469
  if (result2.status === "dead") {
339
- await this.tmux.killSession(result2.pane.sessionName).catch((error) => this.log(
470
+ await this.tmux.killSession(result2.pane.sessionName, signal).catch((error) => this.log(
340
471
  `failed to clean dead tmux session ${result2.pane.sessionName}: ${errorMessage2(error)}`
341
472
  ));
342
473
  delete sessions[id];
@@ -352,7 +483,7 @@ var SessionReconciler = class {
352
483
  changed = true;
353
484
  }
354
485
  if (discover) {
355
- changed = await this.discover(sessions) || changed;
486
+ changed = await this.discover(sessions, signal) || changed;
356
487
  }
357
488
  const activeSessionId = input.activeSessionId && sessions[input.activeSessionId] ? input.activeSessionId : Object.keys(sessions)[0];
358
489
  if (activeSessionId !== input.activeSessionId) changed = true;
@@ -360,17 +491,17 @@ var SessionReconciler = class {
360
491
  const state = changed ? { ...input, sessions, activeSessionId, updatedAt: Date.now() } : input;
361
492
  return { state, liveSessions: Object.values(sessions), removedActive, changed };
362
493
  }
363
- async confirm(session) {
364
- const direct = await this.tmux.inspectStatus(session.paneId);
494
+ async confirm(session, signal) {
495
+ const direct = await this.tmux.inspectStatus(session.paneId, signal);
365
496
  if (direct.status === "live" || direct.status === "unavailable") return direct;
366
- const byName = await this.tmux.inspectSession(session.sessionName);
497
+ const byName = await this.tmux.inspectSession(session.sessionName, signal);
367
498
  if (byName.status === "live" || byName.status === "unavailable") return byName;
368
499
  return byName.status === "dead" ? byName : direct;
369
500
  }
370
- async discover(sessions) {
501
+ async discover(sessions, signal) {
371
502
  let panes;
372
503
  try {
373
- panes = await this.tmux.listSessions(`${this.sessionPrefix}-`);
504
+ panes = await this.tmux.listSessions(`${this.sessionPrefix}-`, signal);
374
505
  } catch (error) {
375
506
  await this.log(`tmux session discovery unavailable: ${errorMessage2(error)}`);
376
507
  return false;
@@ -379,11 +510,12 @@ var SessionReconciler = class {
379
510
  const seen = /* @__PURE__ */ new Set();
380
511
  const liveSessionNames = new Set(panes.filter((pane) => !pane.dead).map((pane) => pane.sessionName));
381
512
  for (const pane of panes) {
513
+ if (signal?.aborted) throw signal.reason;
382
514
  if (seen.has(pane.sessionName)) continue;
383
515
  seen.add(pane.sessionName);
384
516
  if (pane.dead) {
385
517
  if (!liveSessionNames.has(pane.sessionName)) {
386
- await this.tmux.killSession(pane.sessionName).catch((error) => this.log(
518
+ await this.tmux.killSession(pane.sessionName, signal).catch((error) => this.log(
387
519
  `failed to clean orphaned dead tmux session ${pane.sessionName}: ${errorMessage2(error)}`
388
520
  ));
389
521
  }
@@ -395,19 +527,19 @@ var SessionReconciler = class {
395
527
  sessions[registered.id] = { ...registered, paneId: pane.paneId, updatedAt: Date.now() };
396
528
  changed = true;
397
529
  }
398
- if (!await this.tmux.readMetadata(pane.sessionName)) {
399
- await this.writeMetadata(pane, registered).catch((error) => this.log(
530
+ if (!await this.tmux.readMetadata(pane.sessionName, signal)) {
531
+ await this.writeMetadata(pane, registered, signal).catch((error) => this.log(
400
532
  `failed to backfill tmux metadata for ${registered.id}: ${errorMessage2(error)}`
401
533
  ));
402
534
  }
403
535
  continue;
404
536
  }
405
- const metadata = await this.tmux.readMetadata(pane.sessionName);
406
- const recovered = metadata ? this.fromMetadata(pane, metadata) : await this.fromLegacy(pane);
537
+ const metadata = await this.tmux.readMetadata(pane.sessionName, signal);
538
+ const recovered = metadata ? this.fromMetadata(pane, metadata) : await this.fromLegacy(pane, signal);
407
539
  if (!recovered || sessions[recovered.id]) continue;
408
540
  if (!metadata) {
409
541
  try {
410
- await this.writeMetadata(pane, recovered);
542
+ await this.writeMetadata(pane, recovered, signal);
411
543
  } catch (error) {
412
544
  await this.log(`failed to persist recovered tmux metadata for ${recovered.id}: ${errorMessage2(error)}`);
413
545
  continue;
@@ -433,11 +565,11 @@ var SessionReconciler = class {
433
565
  updatedAt: Date.now()
434
566
  };
435
567
  }
436
- async fromLegacy(pane) {
568
+ async fromLegacy(pane, signal) {
437
569
  const id = pane.sessionName.startsWith(`${this.sessionPrefix}-`) ? pane.sessionName.slice(this.sessionPrefix.length + 1) : "";
438
570
  const agent = inferLegacyAgent(pane);
439
571
  if (!validSessionId(id) || !agent) return void 0;
440
- const agentVersion = await this.resolveAgentVersion(agent).catch(() => "unknown");
572
+ const agentVersion = await this.resolveAgentVersion(agent, signal).catch(() => "unknown");
441
573
  return {
442
574
  id,
443
575
  agent,
@@ -448,7 +580,7 @@ var SessionReconciler = class {
448
580
  updatedAt: Date.now()
449
581
  };
450
582
  }
451
- writeMetadata(pane, session) {
583
+ writeMetadata(pane, session, signal) {
452
584
  return this.tmux.writeMetadata(pane.sessionName, {
453
585
  managed: true,
454
586
  sessionId: session.id,
@@ -456,7 +588,7 @@ var SessionReconciler = class {
456
588
  cwd: session.cwd,
457
589
  agentVersion: session.agentVersion,
458
590
  agentSessionId: session.agentSessionId
459
- });
591
+ }, signal);
460
592
  }
461
593
  };
462
594
  function inferLegacyAgent(pane) {
@@ -476,17 +608,17 @@ import { readdir, readFile as readFile2 } from "fs/promises";
476
608
  import { homedir as homedir2 } from "os";
477
609
  import { join } from "path";
478
610
  var UUID = "[0-9a-fA-F-]{32,36}";
479
- async function resolveNativeAgentSessionId(agent, pid, home = homedir2()) {
611
+ async function resolveNativeAgentSessionId(agent, pid, home = homedir2(), signal) {
480
612
  if (!Number.isInteger(pid) || pid <= 0) return void 0;
481
613
  if (agent === "traex") {
482
614
  const peer = await resolveTraexPeer(pid, home);
483
615
  if (peer) return peer;
484
616
  return matchPath(
485
- await processOpenFiles(pid),
617
+ await processOpenFiles(pid, signal),
486
618
  new RegExp(`/\\.trae/cli/sessions/.+/rollout-[^/]+-(${UUID})\\.jsonl(?:\\.lock)?$`)
487
619
  );
488
620
  }
489
- const openFiles = await processOpenFiles(pid);
621
+ const openFiles = await processOpenFiles(pid, signal);
490
622
  if (agent === "codex") return matchPath(openFiles, new RegExp(`/\\.codex/thread-writer-locks/(${UUID})\\.lock$`));
491
623
  return matchPath(openFiles, new RegExp(`/\\.claude/projects/[^/]+/(${UUID})\\.jsonl$`));
492
624
  }
@@ -502,8 +634,8 @@ async function resolveTraexPeer(pid, home) {
502
634
  }
503
635
  return void 0;
504
636
  }
505
- async function processOpenFiles(pid) {
506
- const result2 = await runFile("lsof", ["-Fn", "-p", String(pid)], { timeoutMs: 3e3 }).catch(() => void 0);
637
+ async function processOpenFiles(pid, signal) {
638
+ const result2 = await runFile("lsof", ["-Fn", "-p", String(pid)], { timeoutMs: 3e3, signal }).catch(() => void 0);
507
639
  if (!result2) return [];
508
640
  return result2.stdout.split("\n").filter((line) => line.startsWith("n")).map((line) => line.slice(1));
509
641
  }
@@ -558,7 +690,7 @@ var TmuxController = class {
558
690
  { sessionId: options.sessionName }
559
691
  );
560
692
  }
561
- if (await this.hasSession(options.sessionName)) {
693
+ if (await this.hasSession(options.sessionName, options.signal)) {
562
694
  throw new AppError(
563
695
  "SESSION_EXISTS",
564
696
  `tmux session already exists: ${options.sessionName}`,
@@ -590,34 +722,34 @@ var TmuxController = class {
590
722
  if (options.preserveOnExit) {
591
723
  createArgs.push(";", "set-option", "-w", "-t", `=${options.sessionName}:`, "remain-on-exit", "on");
592
724
  }
593
- await runFile(this.binary, createArgs);
594
- const pane = await this.findBySession(options.sessionName);
725
+ await runFile(this.binary, createArgs, { signal: options.signal });
726
+ const pane = await this.findBySession(options.sessionName, options.signal);
595
727
  if (!pane) throw new Error("tmux created a session without a discoverable pane");
596
728
  return pane;
597
729
  }
598
- async hasSession(sessionName) {
730
+ async hasSession(sessionName, signal) {
599
731
  try {
600
- await runFile(this.binary, ["has-session", "-t", `=${sessionName}`]);
732
+ await runFile(this.binary, ["has-session", "-t", `=${sessionName}`], { signal });
601
733
  return true;
602
734
  } catch {
603
735
  return false;
604
736
  }
605
737
  }
606
- async findBySession(sessionName) {
738
+ async findBySession(sessionName, signal) {
607
739
  const { stdout } = await runFile(this.binary, [
608
740
  "list-panes",
609
741
  "-t",
610
742
  `=${sessionName}`,
611
743
  "-F",
612
744
  PANE_FORMAT
613
- ]);
745
+ ], { signal });
614
746
  return stdout.split("\n").map(parsePane).find(Boolean);
615
747
  }
616
- async inspect(paneId) {
617
- const result2 = await this.inspectStatus(paneId);
748
+ async inspect(paneId, signal) {
749
+ const result2 = await this.inspectStatus(paneId, signal);
618
750
  return result2.status === "live" || result2.status === "dead" ? result2.pane : void 0;
619
751
  }
620
- async inspectStatus(paneId) {
752
+ async inspectStatus(paneId, signal) {
621
753
  assertSafeTmuxTarget(paneId);
622
754
  try {
623
755
  const { stdout } = await runFile(this.binary, [
@@ -626,7 +758,7 @@ var TmuxController = class {
626
758
  "-t",
627
759
  paneId,
628
760
  PANE_FORMAT
629
- ]);
761
+ ], { signal });
630
762
  if (!stdout.trim()) return { status: "missing" };
631
763
  const pane = parsePane(stdout.trim());
632
764
  if (!pane) return { status: "unavailable", error: new Error("invalid tmux pane response") };
@@ -635,20 +767,20 @@ var TmuxController = class {
635
767
  return tmuxTargetMissing(error) ? { status: "missing" } : { status: "unavailable", error };
636
768
  }
637
769
  }
638
- async inspectSession(sessionName) {
770
+ async inspectSession(sessionName, signal) {
639
771
  try {
640
- const pane = await this.findBySession(sessionName);
772
+ const pane = await this.findBySession(sessionName, signal);
641
773
  if (!pane) return { status: "missing" };
642
774
  return pane.dead ? { status: "dead", pane } : { status: "live", pane };
643
775
  } catch (error) {
644
776
  return tmuxTargetMissing(error) ? { status: "missing" } : { status: "unavailable", error };
645
777
  }
646
778
  }
647
- async listSessions(prefix) {
648
- const { stdout } = await runFile(this.binary, ["list-panes", "-a", "-F", PANE_FORMAT]);
779
+ async listSessions(prefix, signal) {
780
+ const { stdout } = await runFile(this.binary, ["list-panes", "-a", "-F", PANE_FORMAT], { signal });
649
781
  return stdout.split("\n").map(parsePane).filter((pane) => Boolean(pane?.sessionName.startsWith(prefix)));
650
782
  }
651
- async writeMetadata(sessionName, metadata) {
783
+ async writeMetadata(sessionName, metadata, signal) {
652
784
  const values = {
653
785
  managed: "1",
654
786
  sessionId: metadata.sessionId,
@@ -660,16 +792,16 @@ var TmuxController = class {
660
792
  for (const [key, option2] of Object.entries(METADATA_OPTIONS)) {
661
793
  const value = values[key];
662
794
  if (value === void 0) {
663
- await runFile(this.binary, ["set-option", "-u", "-t", sessionName, option2]).catch(() => void 0);
795
+ await runFile(this.binary, ["set-option", "-u", "-t", sessionName, option2], { signal }).catch(() => void 0);
664
796
  } else {
665
- await runFile(this.binary, ["set-option", "-t", sessionName, option2, value]);
797
+ await runFile(this.binary, ["set-option", "-t", sessionName, option2, value], { signal });
666
798
  }
667
799
  }
668
800
  }
669
- async readMetadata(sessionName) {
801
+ async readMetadata(sessionName, signal) {
670
802
  const values = {};
671
803
  for (const [key, option2] of Object.entries(METADATA_OPTIONS)) {
672
- const result2 = await runFile(this.binary, ["show-options", "-t", sessionName, "-v", option2]).catch(() => void 0);
804
+ const result2 = await runFile(this.binary, ["show-options", "-t", sessionName, "-v", option2], { signal }).catch(() => void 0);
673
805
  if (!result2) {
674
806
  if (key === "agentSessionId") continue;
675
807
  return void 0;
@@ -686,7 +818,7 @@ var TmuxController = class {
686
818
  agentSessionId: values.agentSessionId || void 0
687
819
  };
688
820
  }
689
- async capture(paneId, lines = 200) {
821
+ async capture(paneId, lines = 200, signal) {
690
822
  assertSafeTmuxTarget(paneId);
691
823
  const { stdout } = await runFile(this.binary, [
692
824
  "capture-pane",
@@ -697,10 +829,10 @@ var TmuxController = class {
697
829
  paneId,
698
830
  "-S",
699
831
  `-${Math.max(1, lines)}`
700
- ]);
832
+ ], { signal });
701
833
  return stdout;
702
834
  }
703
- async preserveOnExit(sessionName, enabled) {
835
+ async preserveOnExit(sessionName, enabled, signal) {
704
836
  await runFile(this.binary, [
705
837
  "set-option",
706
838
  "-w",
@@ -708,7 +840,7 @@ var TmuxController = class {
708
840
  `=${sessionName}:`,
709
841
  "remain-on-exit",
710
842
  enabled ? "on" : "off"
711
- ]);
843
+ ], { signal });
712
844
  }
713
845
  sendText(paneId, input, submit = true) {
714
846
  assertSafeTmuxTarget(paneId);
@@ -727,16 +859,16 @@ var TmuxController = class {
727
859
  this.writes = this.writes.then(operation, operation);
728
860
  return this.writes;
729
861
  }
730
- async sendKey(paneId, key) {
862
+ async sendKey(paneId, key, signal) {
731
863
  assertSafeTmuxTarget(paneId);
732
864
  if (!/^(Enter|Escape|Space|Tab|BSpace|Up|Down|Left|Right|PPage|NPage|C-c|C-u|C-k|C-Enter|[yandpcq1-9])$/.test(key)) {
733
865
  throw new Error(`unsupported tmux key: ${key}`);
734
866
  }
735
- await runFile(this.binary, ["send-keys", "-t", paneId, key]);
867
+ await runFile(this.binary, ["send-keys", "-t", paneId, key], { signal });
736
868
  }
737
- async killSession(sessionName) {
869
+ async killSession(sessionName, signal) {
738
870
  try {
739
- await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`]);
871
+ await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`], { signal });
740
872
  } catch (error) {
741
873
  if (!tmuxTargetMissing(error)) throw error;
742
874
  }
@@ -766,9 +898,9 @@ function parsePane(line) {
766
898
  };
767
899
  }
768
900
  function tmuxTargetMissing(error) {
769
- const message = error instanceof Error ? error.message : String(error);
901
+ const message2 = error instanceof Error ? error.message : String(error);
770
902
  const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr) : "";
771
- return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found|no server running/i.test(`${message}
903
+ return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found|no server running/i.test(`${message2}
772
904
  ${stderr}`);
773
905
  }
774
906
 
@@ -1823,6 +1955,25 @@ function sessionCreateFailureCard(chatId, content, signer) {
1823
1955
  ], "red");
1824
1956
  }
1825
1957
  function sessionStartupFailureCard(chatId, failure, signer) {
1958
+ if (failure.reason === "timeout") {
1959
+ const details = [
1960
+ `\u26A0\uFE0F **Session** \`${escapeInlineCode(failure.sessionId)}\``,
1961
+ `**Agent** ${escapeMarkdown(getAgentAdapter(failure.agent).displayName)}`,
1962
+ `**\u5DE5\u4F5C\u76EE\u5F55** \`${escapeInlineCode(failure.cwd ?? "\u672A\u77E5")}\``,
1963
+ `**\u7ED3\u679C** \u542F\u52A8\u8D85\u8FC7 30 \u79D2\uFF0C\u5DF2\u53D6\u6D88\u5E76\u6E05\u7406`,
1964
+ `**\u8D85\u65F6\u9636\u6BB5** \`${escapeInlineCode(failure.stage ?? "unknown")}\``
1965
+ ].join("\n");
1966
+ const output = failure.terminalExcerpt.trim() ? [{ tag: "markdown", content: `**\u6700\u8FD1\u7EC8\u7AEF\u8F93\u51FA**
1967
+
1968
+ \`\`\`text
1969
+ ${escapeFence(failure.terminalExcerpt)}
1970
+ \`\`\`` }] : [];
1971
+ return cardElements("Session \u542F\u52A8\u5931\u8D25", [
1972
+ { tag: "markdown", content: details },
1973
+ ...output,
1974
+ ...sessionFailureActions(chatId, failure.sessionId, failure.agent, signer)
1975
+ ], "red");
1976
+ }
1826
1977
  const exitStatus = failure.exitStatus === void 0 ? "\u672A\u77E5" : String(failure.exitStatus);
1827
1978
  return cardElements("Session \u542F\u52A8\u5931\u8D25", [
1828
1979
  {
@@ -2133,7 +2284,7 @@ var LarkGateway = class {
2133
2284
  httpTimeoutMs: 3e4,
2134
2285
  respectProxyEnv: true
2135
2286
  });
2136
- this.channel.on("message", async (message) => this.handler.onMessage(message));
2287
+ this.channel.on("message", async (message2) => this.handler.onMessage(message2));
2137
2288
  this.channel.on("cardAction", async (event) => {
2138
2289
  console.error(`[lca] card action received: tag=${event.action.tag ?? "unknown"} name=${event.action.name ?? "-"} message=${event.messageId}`);
2139
2290
  const mappedFormAction = event.action.formValue && event.action.name ? this.formActions.get(event.messageId)?.get(event.action.name) : void 0;
@@ -2383,22 +2534,22 @@ var LarkGateway = class {
2383
2534
  await Promise.all([...chats].map((chatId) => this.clearProcessing(chatId)));
2384
2535
  await this.channel.disconnect();
2385
2536
  }
2386
- async startProcessing(message) {
2387
- await this.clearProcessing(message.chatId);
2388
- const generation = this.processingGenerations.get(message.chatId) ?? 0;
2537
+ async startProcessing(message2) {
2538
+ await this.clearProcessing(message2.chatId);
2539
+ const generation = this.processingGenerations.get(message2.chatId) ?? 0;
2389
2540
  try {
2390
- const reactionId = await this.channel.addReaction(message.messageId, "Typing");
2391
- if (this.processingGenerations.get(message.chatId) !== generation) {
2392
- await this.removeProcessingReaction(message.messageId, reactionId);
2541
+ const reactionId = await this.channel.addReaction(message2.messageId, "Typing");
2542
+ if (this.processingGenerations.get(message2.chatId) !== generation) {
2543
+ await this.removeProcessingReaction(message2.messageId, reactionId);
2393
2544
  return;
2394
2545
  }
2395
2546
  const timer = setTimeout(() => {
2396
- void this.clearProcessing(message.chatId, generation);
2547
+ void this.clearProcessing(message2.chatId, generation);
2397
2548
  }, 10 * 6e4);
2398
2549
  timer.unref?.();
2399
- this.processingReactions.set(message.chatId, { messageId: message.messageId, reactionId, timer, generation });
2550
+ this.processingReactions.set(message2.chatId, { messageId: message2.messageId, reactionId, timer, generation });
2400
2551
  } catch (error) {
2401
- console.error(`[lca] failed to add processing reaction: chat=${message.chatId} message=${message.messageId} detail=${cardErrorDetail(error)}`);
2552
+ console.error(`[lca] failed to add processing reaction: chat=${message2.chatId} message=${message2.messageId} detail=${cardErrorDetail(error)}`);
2402
2553
  }
2403
2554
  }
2404
2555
  sendText(chatId, text) {
@@ -2561,8 +2712,8 @@ function cardErrorDetail(error) {
2561
2712
  const data = response && typeof response === "object" ? response.data : void 0;
2562
2713
  const record = data && typeof data === "object" ? data : void 0;
2563
2714
  const code = typeof record?.code === "number" || typeof record?.code === "string" ? String(record.code) : void 0;
2564
- const message = typeof record?.msg === "string" ? record.msg : error instanceof Error ? error.message : String(error);
2565
- const sanitized = message.replace(/(?:authorization\s*:\s*bearer|bearer)\s+[^\s,'"\]}]+/gi, "Bearer [REDACTED]").replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 1200);
2715
+ const message2 = typeof record?.msg === "string" ? record.msg : error instanceof Error ? error.message : String(error);
2716
+ const sanitized = message2.replace(/(?:authorization\s*:\s*bearer|bearer)\s+[^\s,'"\]}]+/gi, "Bearer [REDACTED]").replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 1200);
2566
2717
  return code ? `code=${code} ${sanitized}` : sanitized;
2567
2718
  }
2568
2719
 
@@ -2915,7 +3066,7 @@ var WorkspaceSnapshotStore = class {
2915
3066
 
2916
3067
  // src/daemon/server.ts
2917
3068
  var AssistantDaemon = class {
2918
- constructor(store, paths2, gatewayFactory = (config, secrets, handler) => new LarkGateway(config, secrets, handler), sessionName = "lark-coding-assistant", stopHookCommand = "lark-coding-assistant-hook", completionQuietMs = 2500, appVersion = "dev") {
3069
+ constructor(store, paths2, gatewayFactory = (config, secrets, handler) => new LarkGateway(config, secrets, handler), sessionName = "lark-coding-assistant", stopHookCommand = "lark-coding-assistant-hook", completionQuietMs = 2500, appVersion = "dev", sessionStartTimeoutMs = 3e4) {
2919
3070
  this.store = store;
2920
3071
  this.paths = paths2;
2921
3072
  this.gatewayFactory = gatewayFactory;
@@ -2923,6 +3074,7 @@ var AssistantDaemon = class {
2923
3074
  this.stopHookCommand = stopHookCommand;
2924
3075
  this.completionQuietMs = completionQuietMs;
2925
3076
  this.appVersion = appVersion;
3077
+ this.sessionStarts = new SessionStartCoordinator(sessionStartTimeoutMs, (message2) => this.log(message2));
2926
3078
  }
2927
3079
  store;
2928
3080
  paths;
@@ -2955,6 +3107,8 @@ var AssistantDaemon = class {
2955
3107
  pendingResumePickers = /* @__PURE__ */ new Map();
2956
3108
  pendingStartupConflicts = /* @__PURE__ */ new Map();
2957
3109
  workspaceSnapshots = new WorkspaceSnapshotStore();
3110
+ sessionStarts;
3111
+ startStateWrites = Promise.resolve();
2958
3112
  pendingInteractionInput;
2959
3113
  attachAttempts = /* @__PURE__ */ new Map();
2960
3114
  server = createServer((socket) => this.handleSocket(socket));
@@ -2969,10 +3123,10 @@ var AssistantDaemon = class {
2969
3123
  this.tmux,
2970
3124
  this.sessionName,
2971
3125
  3,
2972
- (message) => this.log(message),
2973
- async (agent) => {
3126
+ (message2) => this.log(message2),
3127
+ async (agent, signal) => {
2974
3128
  const adapter = getAgentAdapter(agent);
2975
- return (await runFile(adapter.binary(this.config), [...adapter.versionArgs])).stdout.trim();
3129
+ return (await runFile(adapter.binary(this.config), [...adapter.versionArgs], { signal })).stdout.trim();
2976
3130
  }
2977
3131
  );
2978
3132
  const secrets = await this.store.loadSecrets();
@@ -2987,7 +3141,7 @@ var AssistantDaemon = class {
2987
3141
  });
2988
3142
  await chmod3(this.paths.socket, 384);
2989
3143
  this.gateway = this.gatewayFactory(config, secrets, {
2990
- onMessage: (message) => this.onLarkMessage(message),
3144
+ onMessage: (message2) => this.onLarkMessage(message2),
2991
3145
  onAction: (event, action) => this.onLarkAction(event, action),
2992
3146
  onResumePickerDeliveryFailure: (session) => this.handleResumePickerDeliveryFailure(session)
2993
3147
  });
@@ -3091,14 +3245,36 @@ var AssistantDaemon = class {
3091
3245
  return this.handleTurnComplete(request.candidate);
3092
3246
  }
3093
3247
  }
3094
- async startSession(sessionId, cwd, agentId, resume) {
3248
+ async startSession(sessionId, cwd, agentId, resume, source = "cli") {
3095
3249
  try {
3096
3250
  const validated = await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
3097
3251
  cwd = validated.cwd;
3098
3252
  } catch (error) {
3099
3253
  return fail(error);
3100
3254
  }
3101
- await this.reconcileSessions(true);
3255
+ let createdSessionName;
3256
+ const outcome = await this.sessionStarts.run(
3257
+ { sessionId, cwd, agent: agentId, resume, source },
3258
+ async (context) => {
3259
+ const result2 = await this.startSessionCore(
3260
+ sessionId,
3261
+ cwd,
3262
+ agentId,
3263
+ resume,
3264
+ context,
3265
+ (sessionName) => {
3266
+ createdSessionName = sessionName;
3267
+ }
3268
+ );
3269
+ if (!result2.ok) throw daemonResultAppError(result2);
3270
+ return result2.value;
3271
+ },
3272
+ async (_context, error) => this.cleanupStartTransaction(sessionId, createdSessionName, error)
3273
+ );
3274
+ return outcome.ok ? { ok: true, value: outcome.value } : fail(outcome.error);
3275
+ }
3276
+ async startSessionCore(sessionId, cwd, agentId, resume, context, created) {
3277
+ await context.stage("reconcile", () => this.reconcileSessions(true, context.signal));
3102
3278
  const existing = this.state.sessions?.[sessionId];
3103
3279
  if (existing) {
3104
3280
  return fail(new AppError(
@@ -3118,14 +3294,17 @@ var AssistantDaemon = class {
3118
3294
  const binary = adapter.binary(this.config);
3119
3295
  let agentVersion;
3120
3296
  try {
3121
- agentVersion = (await runFile(binary, [...adapter.versionArgs])).stdout.trim();
3297
+ agentVersion = (await context.stage("agent-version", () => runFile(binary, [...adapter.versionArgs], {
3298
+ timeoutMs: context.remainingMs(),
3299
+ signal: context.signal
3300
+ }))).stdout.trim();
3122
3301
  } catch (error) {
3123
- 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 }));
3302
+ return fail(isAppError(error) ? error : 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 }));
3124
3303
  }
3125
3304
  const tmuxSessionName = `${this.sessionName}-${sessionId}`;
3126
3305
  let pane;
3127
3306
  try {
3128
- pane = await this.tmux.create({
3307
+ pane = await context.stage("tmux-create", () => this.tmux.create({
3129
3308
  sessionName: tmuxSessionName,
3130
3309
  cwd,
3131
3310
  binary,
@@ -3138,8 +3317,10 @@ var AssistantDaemon = class {
3138
3317
  LARK_CODING_ASSISTANT_SESSION_ID: sessionId,
3139
3318
  LARK_CODING_ASSISTANT_AGENT: agentId
3140
3319
  },
3141
- preserveOnExit: true
3142
- });
3320
+ preserveOnExit: true,
3321
+ signal: context.signal
3322
+ }));
3323
+ created(pane.sessionName);
3143
3324
  } catch (error) {
3144
3325
  return fail(isAppError(error) ? error : new AppError("START_FAILED", "failed to create tmux session", { sessionId }, { cause: error }));
3145
3326
  }
@@ -3154,81 +3335,128 @@ var AssistantDaemon = class {
3154
3335
  updatedAt: Date.now()
3155
3336
  };
3156
3337
  const pendingClaim = this.pendingAgentSessionClaims.get(sessionId);
3157
- if (pendingClaim?.agent === agentId) {
3158
- const owner = this.findAgentSessionOwner(agentId, pendingClaim.agentSessionId, sessionId);
3159
- if (owner) {
3160
- this.pendingAgentSessionClaims.delete(sessionId);
3161
- await this.tmux.killSession(pane.sessionName).catch(() => void 0);
3162
- return fail(agentSessionInUse(sessionId, owner.id));
3163
- }
3164
- session.agentSessionId = pendingClaim.agentSessionId;
3165
- this.pendingAgentSessionClaims.delete(sessionId);
3166
- }
3167
- try {
3168
- await this.tmux.writeMetadata(pane.sessionName, {
3169
- managed: true,
3170
- sessionId,
3171
- agent: agentId,
3172
- cwd,
3173
- agentVersion,
3174
- agentSessionId: session.agentSessionId
3175
- });
3176
- } catch (error) {
3177
- await this.tmux.killSession(pane.sessionName).catch((cleanupError) => this.log(
3178
- `failed to clean session ${sessionId} after metadata error: ${errorMessage3(cleanupError)}`
3179
- ));
3180
- return fail(new AppError("START_FAILED", "failed to persist tmux session metadata", { sessionId }, { cause: error }));
3181
- }
3182
- const nextState = {
3183
- ...this.state,
3184
- sessions: { ...this.state.sessions, [sessionId]: session },
3185
- activeSessionId: this.state.activeSessionId ?? sessionId,
3186
- boundChatId: binding.mode === "reused" ? this.state.boundChatId : void 0,
3187
- bindCodeHash: binding.mode === "code" ? hashBindCode(binding.bindCode) : void 0,
3188
- bindCodeExpiresAt: binding.mode === "code" ? Date.now() + 10 * 6e4 : void 0,
3189
- updatedAt: Date.now()
3190
- };
3191
- try {
3192
- await this.store.saveState(nextState);
3193
- } catch (error) {
3194
- await this.tmux.killSession(pane.sessionName).catch((cleanupError) => this.log(
3195
- `failed to clean session ${sessionId} after state error: ${errorMessage3(cleanupError)}`
3196
- ));
3197
- return fail(new AppError("START_FAILED", "failed to persist session state", { sessionId }, { cause: error }));
3198
- }
3199
- this.state = nextState;
3200
- const lateClaim = this.pendingAgentSessionClaims.get(sessionId);
3201
- if (lateClaim) {
3202
- const claimed = await this.handleAgentSessionStarted(lateClaim);
3203
- if (!claimed.ok) {
3204
- await this.stopSession(sessionId).catch(() => void 0);
3205
- return claimed;
3206
- }
3338
+ if (pendingClaim) {
3339
+ const claimed = this.claimStartingAgentSession(session, pendingClaim);
3340
+ if (!claimed.ok) return claimed;
3207
3341
  }
3208
3342
  if (resume && resume.mode !== "picker") {
3209
- const initialClaim = await this.waitForInitialAgentSessionClaim(sessionId, pane.pid);
3343
+ const initialClaim = await context.stage(
3344
+ "agent-identity",
3345
+ () => this.waitForInitialAgentSessionClaim(session, pane.pid, context.signal)
3346
+ );
3210
3347
  if (!initialClaim.ok) {
3211
- await this.stopSession(sessionId).catch(() => void 0);
3212
3348
  return initialClaim;
3213
3349
  }
3214
- } else if (!resume) {
3215
- const stable = await this.waitForStartupStability(session, 500);
3350
+ } else {
3351
+ const stable = await context.stage(
3352
+ "startup-stability",
3353
+ () => this.waitForStartupStability(session, 500, context.signal)
3354
+ );
3216
3355
  if (!stable.ok) {
3217
- await this.stopSession(sessionId).catch(() => void 0);
3218
3356
  return stable;
3219
3357
  }
3220
3358
  }
3359
+ const resumePicker = resume?.mode === "picker" && context.source === "lark" ? await context.stage("resume-picker", async () => {
3360
+ const picker = await this.waitForResumePicker(
3361
+ session,
3362
+ void 0,
3363
+ context.remainingMs(),
3364
+ context.signal
3365
+ );
3366
+ if (!picker) {
3367
+ throw new AppError("START_FAILED", "agent resume picker did not become available", {
3368
+ sessionId,
3369
+ agent: agentId
3370
+ });
3371
+ }
3372
+ return picker;
3373
+ }) : void 0;
3221
3374
  if (resume?.mode !== "picker") {
3222
- await this.tmux.preserveOnExit(session.sessionName, false).catch((error) => this.log(
3375
+ await this.tmux.preserveOnExit(session.sessionName, false, context.signal).catch((error) => this.log(
3223
3376
  `failed to disable startup preservation for ${sessionId}: ${errorMessage3(error)}`
3224
3377
  ));
3225
3378
  await this.rememberSessionWorkspace(session.cwd);
3226
3379
  }
3380
+ const lateClaim = this.pendingAgentSessionClaims.get(sessionId);
3381
+ if (lateClaim) {
3382
+ const claimed = this.claimStartingAgentSession(session, lateClaim);
3383
+ if (!claimed.ok) return claimed;
3384
+ }
3385
+ try {
3386
+ await context.stage("metadata", () => this.tmux.writeMetadata(pane.sessionName, {
3387
+ managed: true,
3388
+ sessionId,
3389
+ agent: agentId,
3390
+ cwd,
3391
+ agentVersion,
3392
+ agentSessionId: session.agentSessionId
3393
+ }, context.signal));
3394
+ await context.stage("state", () => this.commitStartedSession(session, binding));
3395
+ } catch (error) {
3396
+ return fail(isAppError(error) ? error : new AppError("START_FAILED", "failed to persist completed session", { sessionId }, { cause: error }));
3397
+ }
3398
+ const postCommitClaim = this.pendingAgentSessionClaims.get(sessionId);
3399
+ if (postCommitClaim) {
3400
+ const claimed = await this.handleAgentSessionStarted(postCommitClaim);
3401
+ if (!claimed.ok) return claimed;
3402
+ }
3227
3403
  await this.log(
3228
3404
  `session created: session=${session.id} agent=${session.agent} pane=${session.paneId} active=${this.state.activeSessionId === session.id}`
3229
3405
  );
3230
- await this.poll().catch((error) => this.log(`initial poll failed for ${sessionId}: ${errorMessage3(error)}`));
3231
- return { ok: true, value: { pane, session, binding, active: this.state.activeSessionId === sessionId } };
3406
+ return {
3407
+ ok: true,
3408
+ value: { pane, session, binding, active: this.state.activeSessionId === sessionId, resumePicker }
3409
+ };
3410
+ }
3411
+ async cleanupStartTransaction(sessionId, createdSessionName, error) {
3412
+ const session = this.state.sessions?.[sessionId];
3413
+ const ownsSession = Boolean(createdSessionName && session?.sessionName === createdSessionName);
3414
+ const createdPane = createdSessionName ? await this.tmux.findBySession(createdSessionName, AbortSignal.timeout(750)).catch(() => void 0) : void 0;
3415
+ const paneId = ownsSession ? session?.paneId : createdPane?.paneId;
3416
+ if (paneId && !error.context.terminalExcerpt) {
3417
+ const terminalExcerpt = await this.tmux.capture(paneId, 40, AbortSignal.timeout(1e3)).then((output) => startupTerminalExcerpt(tailScreen(output, 40).slice(-3e3))).catch(() => "");
3418
+ if (terminalExcerpt) error.context.terminalExcerpt = terminalExcerpt;
3419
+ }
3420
+ if (session && ownsSession) {
3421
+ await this.stopSession(sessionId, AbortSignal.timeout(2e3)).catch((cleanupError) => this.log(
3422
+ `session start state cleanup failed: session=${sessionId} detail=${errorMessage3(cleanupError)}`
3423
+ ));
3424
+ if (this.state.sessions?.[sessionId]?.sessionName === createdSessionName) {
3425
+ await this.forgetSessionState(sessionId).catch((cleanupError) => this.log(
3426
+ `session start forced state cleanup failed: session=${sessionId} detail=${errorMessage3(cleanupError)}`
3427
+ ));
3428
+ }
3429
+ } else if (createdSessionName) {
3430
+ await this.tmux.killSession(createdSessionName, AbortSignal.timeout(2e3)).catch((cleanupError) => this.log(
3431
+ `session start tmux cleanup failed: session=${sessionId} detail=${errorMessage3(cleanupError)}`
3432
+ ));
3433
+ }
3434
+ this.pendingAgentSessionClaims.delete(sessionId);
3435
+ this.pendingResumePickers.delete(sessionId);
3436
+ this.pendingStartupConflicts.delete(sessionId);
3437
+ }
3438
+ commitStartedSession(session, binding) {
3439
+ const commit = async () => {
3440
+ if (this.state.sessions?.[session.id]) {
3441
+ throw new AppError("SESSION_EXISTS", `managed coding-agent session is already running: ${session.id}`, {
3442
+ sessionId: session.id
3443
+ });
3444
+ }
3445
+ const nextState = {
3446
+ ...this.state,
3447
+ sessions: { ...this.state.sessions, [session.id]: session },
3448
+ activeSessionId: this.state.activeSessionId ?? session.id,
3449
+ boundChatId: binding.mode === "reused" ? this.state.boundChatId : void 0,
3450
+ bindCodeHash: binding.mode === "code" ? hashBindCode(binding.bindCode) : void 0,
3451
+ bindCodeExpiresAt: binding.mode === "code" ? Date.now() + 10 * 6e4 : void 0,
3452
+ updatedAt: Date.now()
3453
+ };
3454
+ await this.store.saveState(nextState);
3455
+ this.state = nextState;
3456
+ };
3457
+ const pending = this.startStateWrites.then(commit, commit);
3458
+ this.startStateWrites = pending.catch(() => void 0);
3459
+ return pending;
3232
3460
  }
3233
3461
  createSessionBinding() {
3234
3462
  if (this.state.ownerOpenId && this.state.boundChatId && !this.state.autoBindDisabled) {
@@ -3291,26 +3519,26 @@ var AssistantDaemon = class {
3291
3519
  await this.tmux.sendText(session.paneId, text);
3292
3520
  return { ok: true };
3293
3521
  }
3294
- async onLarkMessage(message) {
3295
- if (message.chatType !== "p2p" || message.senderIsBot || message.senderType === "bot") return;
3296
- const text = message.content.trim();
3522
+ async onLarkMessage(message2) {
3523
+ if (message2.chatType !== "p2p" || message2.senderIsBot || message2.senderType === "bot") return;
3524
+ const text = message2.content.trim();
3297
3525
  const attachCode = text.match(/^\/attach\s+([^\s]+)\s*$/)?.[1];
3298
3526
  if (!this.state.boundChatId) {
3299
- if (this.canAutoBind(message)) {
3300
- await this.bindChat(message, true);
3527
+ if (this.canAutoBind(message2)) {
3528
+ await this.bindChat(message2, true);
3301
3529
  } else {
3302
3530
  if (!attachCode) return;
3303
- await this.handleAttach(message, attachCode);
3531
+ await this.handleAttach(message2, attachCode);
3304
3532
  return;
3305
3533
  }
3306
3534
  }
3307
- if (message.senderId !== this.state.ownerOpenId || message.chatId !== this.state.boundChatId) return;
3535
+ if (message2.senderId !== this.state.ownerOpenId || message2.chatId !== this.state.boundChatId) return;
3308
3536
  if (text === "/start") {
3309
3537
  try {
3310
- await this.gateway?.sendSessionCreate(message.chatId, await this.createSessionWorkspaceView(message.chatId));
3538
+ await this.gateway?.sendSessionCreate(message2.chatId, await this.createSessionWorkspaceView(message2.chatId));
3311
3539
  } catch (error) {
3312
3540
  await this.log(`session create card failed: ${errorMessage3(error)}`);
3313
- 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");
3541
+ await this.gateway?.sendText(message2.chatId, "\u65B0\u5EFA Session \u8868\u5355\u53D1\u9001\u5931\u8D25\u3002\u8BF7\u4F7F\u7528 /start <name> --agent <agent> --cwd <\u7EDD\u5BF9\u8DEF\u5F84>\u3002");
3314
3542
  }
3315
3543
  return;
3316
3544
  }
@@ -3319,37 +3547,37 @@ var AssistantDaemon = class {
3319
3547
  try {
3320
3548
  request = parseStartCommand(text);
3321
3549
  } catch (error) {
3322
- await this.gateway?.sendText(message.chatId, remoteError(fail(error)));
3550
+ await this.gateway?.sendText(message2.chatId, remoteError(fail(error)));
3323
3551
  return;
3324
3552
  }
3325
3553
  const result3 = await this.startRemoteSession(request);
3326
3554
  if (!result3.ok) {
3327
3555
  const failure = sessionStartupFailure(result3.error, request);
3328
- if (failure) await this.gateway?.sendSessionStartupFailure(message.chatId, failure);
3329
- else await this.gateway?.sendText(message.chatId, remoteError(result3.error));
3556
+ if (failure) await this.gateway?.sendSessionStartupFailure(message2.chatId, failure);
3557
+ else await this.gateway?.sendText(message2.chatId, remoteError(result3.error));
3330
3558
  } else if (result3.state === "picker") {
3331
3559
  try {
3332
- await this.gateway?.sendResumePicker(message.chatId, result3.session, result3.picker);
3560
+ await this.gateway?.sendResumePicker(message2.chatId, result3.session, result3.picker);
3333
3561
  } catch (error) {
3334
3562
  await this.handleResumePickerDeliveryFailure(result3.session);
3335
3563
  await this.log(`resume picker notification failed: session=${result3.session.id} error=${errorMessage3(error)}`);
3336
- 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");
3564
+ await this.gateway?.sendText(message2.chatId, "Resume Picker \u5361\u7247\u53D1\u9001\u5931\u8D25\uFF0C\u4E34\u65F6 Session \u5DF2\u6E05\u7406\uFF0C\u8BF7\u91CD\u8BD5\u3002");
3337
3565
  }
3338
- } else if (result3.state === "conflict") await this.gateway?.sendStartupConflict(message.chatId, result3.request, result3.owner);
3339
- else await this.gateway?.sendText(message.chatId, remoteStartSuccess(result3.session));
3566
+ } else if (result3.state === "conflict") await this.gateway?.sendStartupConflict(message2.chatId, result3.request, result3.owner);
3567
+ else await this.gateway?.sendText(message2.chatId, remoteStartSuccess(result3.session));
3340
3568
  return;
3341
3569
  }
3342
3570
  const tailMatch = text.match(/^\/tail(?:\s+(\d+))?$/);
3343
3571
  if (tailMatch) {
3344
3572
  const lines = tailMatch[1] ? Number(tailMatch[1]) : 80;
3345
3573
  if (!Number.isInteger(lines) || lines < 20 || lines > 300) {
3346
- await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
3574
+ await this.gateway?.sendText(message2.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
3347
3575
  return;
3348
3576
  }
3349
3577
  const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorMessage3(error)}`);
3350
3578
  const session = this.activeSession();
3351
3579
  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**";
3352
- await this.gateway?.sendMarkdown(message.chatId, `${metadata}
3580
+ await this.gateway?.sendMarkdown(message2.chatId, `${metadata}
3353
3581
 
3354
3582
  \`\`\`text
3355
3583
  ${escapeFence2(output).slice(-6800)}
@@ -3357,20 +3585,20 @@ ${escapeFence2(output).slice(-6800)}
3357
3585
  return;
3358
3586
  }
3359
3587
  if (text.startsWith("/tail")) {
3360
- await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
3588
+ await this.gateway?.sendText(message2.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
3361
3589
  return;
3362
3590
  }
3363
3591
  if (text === "/manual") {
3364
3592
  await this.poll();
3365
3593
  const view2 = this.currentManualView();
3366
- if (!view2) await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
3594
+ if (!view2) await this.gateway?.sendText(message2.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
3367
3595
  else {
3368
3596
  try {
3369
- await this.gateway?.sendManual(message.chatId, view2);
3597
+ await this.gateway?.sendManual(message2.chatId, view2);
3370
3598
  } catch (error) {
3371
3599
  await this.log(`manual card failed: ${errorMessage3(error)}`);
3372
3600
  await this.gateway?.sendText(
3373
- message.chatId,
3601
+ message2.chatId,
3374
3602
  "\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"
3375
3603
  );
3376
3604
  }
@@ -3379,44 +3607,44 @@ ${escapeFence2(output).slice(-6800)}
3379
3607
  }
3380
3608
  const manualKey = text.match(/^\/key\s+(up|down|left|right|enter|esc|tab|space|backspace|ctrl-c)$/i)?.[1];
3381
3609
  if (manualKey) {
3382
- await this.executeManualCommand(message.chatId, `\u6309\u952E ${manualKey}`, async (session) => {
3610
+ await this.executeManualCommand(message2.chatId, `\u6309\u952E ${manualKey}`, async (session) => {
3383
3611
  await this.tmux.sendKey(session.paneId, manualTmuxKey(manualKey));
3384
3612
  });
3385
3613
  return;
3386
3614
  }
3387
3615
  if (text.startsWith("/key")) {
3388
- await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/key up|down|left|right|enter|esc|tab|space|backspace|ctrl-c");
3616
+ await this.gateway?.sendText(message2.chatId, "\u7528\u6CD5\uFF1A/key up|down|left|right|enter|esc|tab|space|backspace|ctrl-c");
3389
3617
  return;
3390
3618
  }
3391
3619
  const typeMatch = text.match(/^\/(type|submit)\s+([\s\S]+)$/);
3392
3620
  if (typeMatch?.[1] && typeMatch[2]?.trim()) {
3393
3621
  const submit = typeMatch[1] === "submit";
3394
- await this.executeManualCommand(message.chatId, submit ? "\u8F93\u5165\u5E76\u63D0\u4EA4" : "\u4EC5\u8F93\u5165", async (session) => {
3622
+ await this.executeManualCommand(message2.chatId, submit ? "\u8F93\u5165\u5E76\u63D0\u4EA4" : "\u4EC5\u8F93\u5165", async (session) => {
3395
3623
  await this.tmux.sendText(session.paneId, typeMatch[2], submit);
3396
3624
  });
3397
3625
  return;
3398
3626
  }
3399
3627
  if (text === "/type" || text === "/submit") {
3400
- await this.gateway?.sendText(message.chatId, `\u7528\u6CD5\uFF1A${text} <\u6587\u672C>`);
3628
+ await this.gateway?.sendText(message2.chatId, `\u7528\u6CD5\uFF1A${text} <\u6587\u672C>`);
3401
3629
  return;
3402
3630
  }
3403
3631
  if (text === "/status") {
3404
3632
  const status = await this.runtimeStatus();
3405
- await this.gateway?.sendStatus(message.chatId, status);
3633
+ await this.gateway?.sendStatus(message2.chatId, status);
3406
3634
  return;
3407
3635
  }
3408
3636
  if (text === "/sessions") {
3409
3637
  const sessions = await this.reconcileSessions(true);
3410
3638
  try {
3411
3639
  await this.gateway?.sendSessionPicker(
3412
- message.chatId,
3640
+ message2.chatId,
3413
3641
  sessions,
3414
3642
  this.state.activeSessionId
3415
3643
  );
3416
3644
  } catch (error) {
3417
3645
  await this.log(`session picker notification failed: ${errorMessage3(error)}`);
3418
3646
  await this.gateway?.sendText(
3419
- message.chatId,
3647
+ message2.chatId,
3420
3648
  "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"
3421
3649
  );
3422
3650
  }
@@ -3428,7 +3656,7 @@ ${escapeFence2(output).slice(-6800)}
3428
3656
  const target = this.state.sessions?.[useSessionId];
3429
3657
  const reply = result3.ok ? `\u5DF2\u8FDE\u63A5\u5230 ${target ? getAgentAdapter(target.agent).displayName : "coding agent"} session\uFF1A${useSessionId}` : `\u5207\u6362\u5931\u8D25\uFF1A${result3.error}`;
3430
3658
  await this.gateway?.sendText(
3431
- message.chatId,
3659
+ message2.chatId,
3432
3660
  reply
3433
3661
  );
3434
3662
  return;
@@ -3445,16 +3673,16 @@ ${escapeFence2(output).slice(-6800)}
3445
3673
  updatedAt: Date.now()
3446
3674
  };
3447
3675
  await this.store.saveState(this.state);
3448
- await this.gateway?.sendText(message.chatId, "\u5DF2\u89E3\u9664\u672C\u6B21\u98DE\u4E66\u7ED1\u5B9A\uFF1Bcoding agent \u548C tmux \u4ECD\u5728\u8FD0\u884C\u3002");
3676
+ await this.gateway?.sendText(message2.chatId, "\u5DF2\u89E3\u9664\u672C\u6B21\u98DE\u4E66\u7ED1\u5B9A\uFF1Bcoding agent \u548C tmux \u4ECD\u5728\u8FD0\u884C\u3002");
3449
3677
  return;
3450
3678
  }
3451
3679
  if (text === "/stop") {
3452
3680
  await this.poll();
3453
3681
  const session = this.activeSession();
3454
3682
  if (!session || !this.screen) {
3455
- await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u505C\u6B62\u7684 coding agent \u4F1A\u8BDD\u3002");
3683
+ await this.gateway?.sendText(message2.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u505C\u6B62\u7684 coding agent \u4F1A\u8BDD\u3002");
3456
3684
  } else {
3457
- await this.gateway?.sendStopConfirmation(message.chatId, session.paneId, this.screen.fingerprint, session.agent);
3685
+ await this.gateway?.sendStopConfirmation(message2.chatId, session.paneId, this.screen.fingerprint, session.agent);
3458
3686
  }
3459
3687
  return;
3460
3688
  }
@@ -3463,15 +3691,15 @@ ${escapeFence2(output).slice(-6800)}
3463
3691
  const session = this.activeSession();
3464
3692
  if (!session || session.id !== pendingInput.sessionId) {
3465
3693
  this.pendingInteractionInput = void 0;
3466
- await this.gateway?.sendText(message.chatId, "\u4EA4\u4E92\u4F1A\u8BDD\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u64CD\u4F5C\u3002");
3694
+ await this.gateway?.sendText(message2.chatId, "\u4EA4\u4E92\u4F1A\u8BDD\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u64CD\u4F5C\u3002");
3467
3695
  return;
3468
3696
  }
3469
- await this.tmux.sendText(session.paneId, message.content, pendingInput.submitOnInput);
3697
+ await this.tmux.sendText(session.paneId, message2.content, pendingInput.submitOnInput);
3470
3698
  this.pendingInteractionInput = void 0;
3471
3699
  if (pendingInput.submitOnInput) {
3472
3700
  await this.waitForInteractionChange(pendingInput.interactionId);
3473
3701
  if (this.screen?.interaction?.interactionId === pendingInput.interactionId) {
3474
- await this.gateway?.sendText(message.chatId, "\u8865\u5145\u5185\u5BB9\u5DF2\u8F93\u5165\uFF0C\u4F46\u7EC8\u7AEF\u4ECD\u505C\u7559\u5728\u539F\u95EE\u9898\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002");
3702
+ await this.gateway?.sendText(message2.chatId, "\u8865\u5145\u5185\u5BB9\u5DF2\u8F93\u5165\uFF0C\u4F46\u7EC8\u7AEF\u4ECD\u505C\u7559\u5728\u539F\u95EE\u9898\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002");
3475
3703
  return;
3476
3704
  }
3477
3705
  const content = `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`;
@@ -3481,12 +3709,12 @@ ${escapeFence2(output).slice(-6800)}
3481
3709
  }
3482
3710
  if (pendingInput.role === "custom-input") {
3483
3711
  const refreshed = await this.withInteractionNotificationsSuppressed(async () => {
3484
- await this.waitForCustomInputValue(pendingInput.interactionId, pendingInput.controlId, message.content);
3712
+ await this.waitForCustomInputValue(pendingInput.interactionId, pendingInput.controlId, message2.content);
3485
3713
  const current = this.screen;
3486
3714
  if (current?.interaction?.semantics && (current.interaction.actionConfidence ?? 0) >= 0.85) {
3487
3715
  await this.gateway?.updateChoice(
3488
3716
  pendingInput.cardMessageId,
3489
- message.chatId,
3717
+ message2.chatId,
3490
3718
  session.paneId,
3491
3719
  current,
3492
3720
  session.agent
@@ -3497,43 +3725,43 @@ ${escapeFence2(output).slice(-6800)}
3497
3725
  });
3498
3726
  if (refreshed) return;
3499
3727
  }
3500
- await this.gateway?.sendText(message.chatId, `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`);
3728
+ await this.gateway?.sendText(message2.chatId, `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`);
3501
3729
  return;
3502
3730
  }
3503
- await this.gateway?.startProcessing(message);
3731
+ await this.gateway?.startProcessing(message2);
3504
3732
  await this.poll();
3505
3733
  if (this.shouldQueueMessage()) {
3506
3734
  if (this.pendingMessages.length >= 100) {
3507
- await this.gateway?.sendText(message.chatId, "\u5F85\u53D1\u9001\u961F\u5217\u5DF2\u6EE1\uFF0C\u8BF7\u5148\u5904\u7406\u5F53\u524D\u7EC8\u7AEF\u72B6\u6001\u3002");
3735
+ await this.gateway?.sendText(message2.chatId, "\u5F85\u53D1\u9001\u961F\u5217\u5DF2\u6EE1\uFF0C\u8BF7\u5148\u5904\u7406\u5F53\u524D\u7EC8\u7AEF\u72B6\u6001\u3002");
3508
3736
  return;
3509
3737
  }
3510
- this.pendingMessages.push(message.content);
3511
- await this.gateway?.sendText(message.chatId, `\u5F53\u524D\u7EC8\u7AEF\u6682\u4E0D\u53EF\u5B89\u5168\u5199\u5165\uFF0C\u6D88\u606F\u5DF2\u6392\u961F\uFF08${this.pendingMessages.length} \u6761\uFF09\u3002`);
3738
+ this.pendingMessages.push(message2.content);
3739
+ await this.gateway?.sendText(message2.chatId, `\u5F53\u524D\u7EC8\u7AEF\u6682\u4E0D\u53EF\u5B89\u5168\u5199\u5165\uFF0C\u6D88\u606F\u5DF2\u6392\u961F\uFF08${this.pendingMessages.length} \u6761\uFF09\u3002`);
3512
3740
  return;
3513
3741
  }
3514
- const result2 = await this.send(message.content);
3515
- if (!result2.ok) await this.gateway?.sendText(message.chatId, `\u672A\u53D1\u9001\uFF1A${result2.error}`);
3742
+ const result2 = await this.send(message2.content);
3743
+ if (!result2.ok) await this.gateway?.sendText(message2.chatId, `\u672A\u53D1\u9001\uFF1A${result2.error}`);
3516
3744
  }
3517
- async handleAttach(message, code) {
3518
- if (this.state.ownerOpenId && message.senderId !== this.state.ownerOpenId) return;
3519
- if (!this.allowAttachAttempt(message.senderId)) return;
3745
+ async handleAttach(message2, code) {
3746
+ if (this.state.ownerOpenId && message2.senderId !== this.state.ownerOpenId) return;
3747
+ if (!this.allowAttachAttempt(message2.senderId)) return;
3520
3748
  const valid = Boolean(
3521
3749
  this.state.bindCodeHash && this.state.bindCodeExpiresAt && this.state.bindCodeExpiresAt >= Date.now() && verifyBindCode(code, this.state.bindCodeHash)
3522
3750
  );
3523
3751
  if (!valid) {
3524
- await this.gateway?.sendText(message.chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u7ED1\u5B9A\u7801\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\u3002");
3752
+ await this.gateway?.sendText(message2.chatId, "\u7ED1\u5B9A\u5931\u8D25\uFF1A\u7ED1\u5B9A\u7801\u65E0\u6548\u6216\u5DF2\u8FC7\u671F\u3002");
3525
3753
  return;
3526
3754
  }
3527
- await this.bindChat(message, false);
3755
+ await this.bindChat(message2, false);
3528
3756
  }
3529
- canAutoBind(message) {
3530
- return !this.state.autoBindDisabled && Boolean(this.state.ownerOpenId) && message.senderId === this.state.ownerOpenId;
3757
+ canAutoBind(message2) {
3758
+ return !this.state.autoBindDisabled && Boolean(this.state.ownerOpenId) && message2.senderId === this.state.ownerOpenId;
3531
3759
  }
3532
- async bindChat(message, automatic) {
3760
+ async bindChat(message2, automatic) {
3533
3761
  this.state = {
3534
3762
  ...this.state,
3535
- ownerOpenId: this.state.ownerOpenId ?? message.senderId,
3536
- boundChatId: message.chatId,
3763
+ ownerOpenId: this.state.ownerOpenId ?? message2.senderId,
3764
+ boundChatId: message2.chatId,
3537
3765
  autoBindDisabled: false,
3538
3766
  bindCodeHash: void 0,
3539
3767
  bindCodeExpiresAt: void 0,
@@ -3541,7 +3769,7 @@ ${escapeFence2(output).slice(-6800)}
3541
3769
  };
3542
3770
  await this.store.saveState(this.state);
3543
3771
  await this.gateway?.sendText(
3544
- message.chatId,
3772
+ message2.chatId,
3545
3773
  automatic ? "\u5DF2\u81EA\u52A8\u8FDE\u63A5\u5F53\u524D coding agent \u4F1A\u8BDD\u3002\u4E4B\u540E\u76F4\u63A5\u53D1\u9001\u666E\u901A\u6D88\u606F\u5373\u53EF\u3002" : "\u7ED1\u5B9A\u6210\u529F\u3002\u4E4B\u540E\u7684\u666E\u901A\u6D88\u606F\u4F1A\u53D1\u9001\u5230\u5F53\u524D tmux \u4E2D\u7684 coding agent\uFF1B\u53EF\u7528 /tail\u3001/status\u3001/sessions\u3001/detach\u3001/stop\u3002"
3546
3774
  );
3547
3775
  this.previousScreen = void 0;
@@ -3810,21 +4038,42 @@ ${escapeFence2(output).slice(-6800)}
3810
4038
  const optionId = action.action.startsWith("select:") ? action.action.slice("select:".length) : "";
3811
4039
  const option2 = picker.options.find((candidate) => candidate.id === optionId);
3812
4040
  if (!option2) return { type: "resume-picker", content: "\u6240\u9009\u9879\u5DF2\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u3002", session, picker };
3813
- const delta = option2.visibleIndex - picker.selectedIndex;
3814
- const key = delta < 0 ? "Up" : "Down";
3815
- for (let step = 0; step < Math.abs(delta); step += 1) await this.tmux.sendKey(session.paneId, key);
3816
- const pane = await this.tmux.inspect(session.paneId);
3817
- if (!pane || pane.dead) {
3818
- this.pendingResumePickers.delete(sessionId);
3819
- return larkStartupError(fail(await this.startupExitedError(session, pane)), request);
3820
- }
3821
- await this.tmux.sendKey(session.paneId, "Enter");
3822
- this.pendingResumePickers.delete(sessionId);
3823
- const claimed = await this.waitForInitialAgentSessionClaim(sessionId, pane.pid);
3824
- if (!claimed.ok) {
3825
- await this.stopSession(sessionId).catch(() => void 0);
3826
- if (claimed.errorCode === "AGENT_SESSION_IN_USE") {
3827
- const ownerSessionId = typeof claimed.errorContext?.ownerSessionId === "string" ? claimed.errorContext.ownerSessionId : void 0;
4041
+ const restored = await this.sessionStarts.run(
4042
+ { sessionId, agent: session.agent, cwd: session.cwd, resume: request.resume, source: "resume-picker" },
4043
+ async (context) => {
4044
+ const restoringSession = { ...session };
4045
+ const delta = option2.visibleIndex - picker.selectedIndex;
4046
+ const key = delta < 0 ? "Up" : "Down";
4047
+ await context.stage("resume-selection", async () => {
4048
+ for (let step = 0; step < Math.abs(delta); step += 1) {
4049
+ await this.tmux.sendKey(session.paneId, key, context.signal);
4050
+ }
4051
+ const pane = await this.tmux.inspect(session.paneId, context.signal);
4052
+ if (!pane || pane.dead) throw await this.startupExitedError(session, pane);
4053
+ await this.tmux.sendKey(session.paneId, "Enter", context.signal);
4054
+ this.pendingResumePickers.delete(sessionId);
4055
+ const claimed = await this.waitForInitialAgentSessionClaim(restoringSession, pane.pid, context.signal);
4056
+ if (!claimed.ok) throw daemonResultAppError(claimed);
4057
+ if (restoringSession.agentSessionId) {
4058
+ const persisted = await this.handleAgentSessionStarted({
4059
+ sessionId,
4060
+ agent: session.agent,
4061
+ agentSessionId: restoringSession.agentSessionId,
4062
+ cwd: session.cwd,
4063
+ source: "resume-picker"
4064
+ });
4065
+ if (!persisted.ok) throw daemonResultAppError(persisted);
4066
+ }
4067
+ await this.tmux.preserveOnExit(session.sessionName, false, context.signal);
4068
+ });
4069
+ return session;
4070
+ },
4071
+ async (_context, error) => this.cleanupStartTransaction(sessionId, session.sessionName, error)
4072
+ );
4073
+ if (!restored.ok) {
4074
+ const failed = fail(restored.error);
4075
+ if (restored.error.code === "AGENT_SESSION_IN_USE") {
4076
+ const ownerSessionId = typeof restored.error.context.ownerSessionId === "string" ? restored.error.context.ownerSessionId : void 0;
3828
4077
  const owner = ownerSessionId ? this.state.sessions?.[ownerSessionId] : void 0;
3829
4078
  if (owner) {
3830
4079
  this.pendingStartupConflicts.set(sessionId, { request, ownerSessionId: owner.id });
@@ -3836,18 +4085,17 @@ ${escapeFence2(output).slice(-6800)}
3836
4085
  };
3837
4086
  }
3838
4087
  }
3839
- return larkStartupError(claimed, request);
4088
+ return larkStartupError(failed, request);
3840
4089
  }
3841
- await this.tmux.preserveOnExit(session.sessionName, false).catch(() => void 0);
3842
4090
  const selected = await this.useSession(sessionId);
3843
4091
  if (!selected.ok) return { type: "error", content: remoteError(selected) };
3844
4092
  await this.rememberSessionWorkspace(session.cwd);
3845
4093
  return { type: "session-created", content: remoteStartSuccess(session), session };
3846
4094
  }
3847
- async readResumePicker(session) {
3848
- const pane = await this.tmux.inspect(session.paneId);
4095
+ async readResumePicker(session, signal) {
4096
+ const pane = await this.tmux.inspect(session.paneId, signal);
3849
4097
  if (!pane || pane.dead) return void 0;
3850
- const raw = await this.tmux.capture(session.paneId, 120).catch(() => "");
4098
+ const raw = await this.tmux.capture(session.paneId, 120, signal).catch(() => "");
3851
4099
  return parseResumePicker(raw, session.agent);
3852
4100
  }
3853
4101
  async handleResumePickerDeliveryFailure(candidate) {
@@ -3856,13 +4104,14 @@ ${escapeFence2(output).slice(-6800)}
3856
4104
  await this.log(`resume picker delivery failed; rolling back provisional session: session=${candidate.id} pane=${candidate.paneId}`);
3857
4105
  await this.stopSession(candidate.id);
3858
4106
  }
3859
- async waitForResumePicker(session, previousFingerprint, timeoutMs = 2500) {
4107
+ async waitForResumePicker(session, previousFingerprint, timeoutMs = 2500, signal) {
3860
4108
  const deadline = Date.now() + timeoutMs;
3861
4109
  let latest;
3862
4110
  while (Date.now() < deadline) {
3863
- latest = await this.readResumePicker(session);
4111
+ if (signal?.aborted) throw signal.reason;
4112
+ latest = await this.readResumePicker(session, signal);
3864
4113
  if (latest && (!previousFingerprint || latest.fingerprint !== previousFingerprint)) return latest;
3865
- await new Promise((resolve2) => setTimeout(resolve2, 100));
4114
+ await abortableDelay(100, signal);
3866
4115
  }
3867
4116
  return latest;
3868
4117
  }
@@ -4087,13 +4336,25 @@ ${escapeFence2(output).slice(-6500)}
4087
4336
  if (focusedIndex === targetIndex) return { ok: true };
4088
4337
  const direction = targetIndex > focusedIndex ? "Down" : "Up";
4089
4338
  await this.tmux.sendKey(paneId, direction);
4090
- await new Promise((resolve2) => setTimeout(resolve2, 40));
4091
- await this.poll();
4092
- const nextFocused = this.screen?.actions.findIndex(({ focused }) => focused) ?? -1;
4093
- if (nextFocused === focusedIndex) return { ok: false, error: "terminal focus did not move as expected" };
4339
+ const moved = await this.waitForFocusMove(interactionId, focusedIndex);
4340
+ if (!moved.ok) return moved;
4094
4341
  }
4095
4342
  return { ok: false, error: "terminal focus navigation exceeded its safe step limit" };
4096
4343
  }
4344
+ async waitForFocusMove(interactionId, previousIndex) {
4345
+ const deadline = Date.now() + 1200;
4346
+ while (Date.now() < deadline) {
4347
+ await new Promise((resolve2) => setTimeout(resolve2, 60));
4348
+ await this.poll();
4349
+ const interaction = this.screen?.interaction;
4350
+ if (!interaction || interaction.interactionId !== interactionId) {
4351
+ return { ok: false, error: "interaction changed while navigating; refusing action" };
4352
+ }
4353
+ const focusedIndex = this.screen?.actions.findIndex(({ focused }) => focused) ?? -1;
4354
+ if (focusedIndex !== -1 && focusedIndex !== previousIndex) return { ok: true };
4355
+ }
4356
+ return { ok: false, error: "terminal focus did not move before the navigation timeout" };
4357
+ }
4097
4358
  async waitForInteractionChange(interactionId) {
4098
4359
  const deadline = Date.now() + 1500;
4099
4360
  while (Date.now() < deadline) {
@@ -4180,7 +4441,7 @@ ${escapeFence2(output).slice(-6500)}
4180
4441
  }
4181
4442
  return { ok: true, committed: false };
4182
4443
  }
4183
- async stopSession(sessionId = this.state.activeSessionId) {
4444
+ async stopSession(sessionId = this.state.activeSessionId, signal) {
4184
4445
  if (!sessionId) {
4185
4446
  return fail(new AppError("SESSION_NOT_FOUND", "no active managed session", { sessionId: "default" }));
4186
4447
  }
@@ -4188,9 +4449,11 @@ ${escapeFence2(output).slice(-6500)}
4188
4449
  if (!session) {
4189
4450
  return fail(new AppError("SESSION_NOT_FOUND", `unknown session: ${sessionId}`, { sessionId }));
4190
4451
  }
4191
- if (await this.tmux.hasSession(session.sessionName)) {
4192
- await this.tmux.killSession(session.sessionName);
4193
- }
4452
+ await this.tmux.killSession(session.sessionName, signal);
4453
+ await this.forgetSessionState(sessionId);
4454
+ return { ok: true };
4455
+ }
4456
+ async forgetSessionState(sessionId) {
4194
4457
  const sessions = { ...this.state.sessions };
4195
4458
  delete sessions[sessionId];
4196
4459
  this.pendingResumePickers.delete(sessionId);
@@ -4217,7 +4480,6 @@ ${escapeFence2(output).slice(-6500)}
4217
4480
  this.unresolvedNotified.clear();
4218
4481
  }
4219
4482
  await this.store.saveState(this.state);
4220
- return { ok: true };
4221
4483
  }
4222
4484
  async resetOwner() {
4223
4485
  this.pendingMessages.length = 0;
@@ -4426,37 +4688,75 @@ ${escapeFence2(output).slice(-6500)}
4426
4688
  });
4427
4689
  }
4428
4690
  }
4429
- async waitForInitialAgentSessionClaim(sessionId, panePid) {
4691
+ async waitForInitialAgentSessionClaim(session, panePid, signal) {
4430
4692
  const deadline = Date.now() + 3500;
4431
4693
  while (Date.now() < deadline) {
4432
- const session2 = this.state.sessions?.[sessionId];
4433
- if (!session2) return { ok: false, error: `session disappeared during startup: ${sessionId}` };
4434
- if (session2.agentSessionId) return { ok: true };
4435
- const pane2 = await this.tmux.inspect(session2.paneId);
4436
- if (!pane2 || pane2.dead) return fail(await this.startupExitedError(session2, pane2));
4437
- const agentSessionId = await resolveNativeAgentSessionId(session2.agent, panePid).catch(() => void 0);
4694
+ if (session.agentSessionId) return { ok: true };
4695
+ const committedClaim2 = this.state.sessions?.[session.id]?.agentSessionId;
4696
+ if (committedClaim2) {
4697
+ session.agentSessionId = committedClaim2;
4698
+ return { ok: true };
4699
+ }
4700
+ const pending2 = this.pendingAgentSessionClaims.get(session.id);
4701
+ if (pending2) {
4702
+ const claimed = this.claimStartingAgentSession(session, pending2);
4703
+ if (!claimed.ok || session.agentSessionId) return claimed;
4704
+ }
4705
+ if (signal?.aborted) throw signal.reason;
4706
+ const pane2 = await this.tmux.inspect(session.paneId, signal);
4707
+ if (signal?.aborted) throw signal.reason;
4708
+ if (!pane2 || pane2.dead) return fail(await this.startupExitedError(session, pane2));
4709
+ const agentSessionId = await resolveNativeAgentSessionId(
4710
+ session.agent,
4711
+ panePid,
4712
+ void 0,
4713
+ signal
4714
+ ).catch(() => void 0);
4438
4715
  if (agentSessionId) {
4439
- return this.handleAgentSessionStarted({
4440
- sessionId,
4441
- agent: session2.agent,
4716
+ return this.claimStartingAgentSession(session, {
4717
+ sessionId: session.id,
4718
+ agent: session.agent,
4442
4719
  agentSessionId,
4443
- cwd: session2.cwd,
4720
+ cwd: session.cwd,
4444
4721
  source: "startup-discovery"
4445
4722
  });
4446
4723
  }
4447
- await new Promise((resolve2) => setTimeout(resolve2, 100));
4724
+ await abortableDelay(100, signal);
4448
4725
  }
4449
- const session = this.state.sessions?.[sessionId];
4450
- if (!session) return fail(new AppError("SESSION_NOT_FOUND", `session disappeared during startup: ${sessionId}`, { sessionId }));
4451
- const pane = await this.tmux.inspect(session.paneId);
4726
+ const pane = await this.tmux.inspect(session.paneId, signal);
4727
+ if (signal?.aborted) throw signal.reason;
4452
4728
  if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4453
- if (session.agentSessionId || this.pendingAgentSessionClaims.has(sessionId)) return { ok: true };
4729
+ const pending = this.pendingAgentSessionClaims.get(session.id);
4730
+ if (pending) return this.claimStartingAgentSession(session, pending);
4731
+ const committedClaim = this.state.sessions?.[session.id]?.agentSessionId;
4732
+ if (committedClaim) {
4733
+ session.agentSessionId = committedClaim;
4734
+ return { ok: true };
4735
+ }
4736
+ if (session.agentSessionId) return { ok: true };
4454
4737
  return fail(new AppError(
4455
4738
  "AGENT_IDENTITY_TIMEOUT",
4456
- `unable to identify resumed native session: ${sessionId}`,
4457
- { sessionId, agent: session.agent }
4739
+ `unable to identify resumed native session: ${session.id}`,
4740
+ { sessionId: session.id, agent: session.agent }
4458
4741
  ));
4459
4742
  }
4743
+ claimStartingAgentSession(session, candidate) {
4744
+ if (candidate.agent !== session.agent) {
4745
+ return fail(new AppError("START_FAILED", "agent-session candidate does not match starting session", {
4746
+ sessionId: session.id,
4747
+ agent: session.agent
4748
+ }));
4749
+ }
4750
+ const owner = this.findAgentSessionOwner(candidate.agent, candidate.agentSessionId, session.id);
4751
+ if (owner) {
4752
+ this.pendingAgentSessionClaims.delete(session.id);
4753
+ return fail(agentSessionInUse(session.id, owner.id));
4754
+ }
4755
+ session.agentSessionId = candidate.agentSessionId;
4756
+ session.updatedAt = Date.now();
4757
+ this.pendingAgentSessionClaims.delete(session.id);
4758
+ return { ok: true };
4759
+ }
4460
4760
  async startupExitedError(session, pane) {
4461
4761
  const terminalTail = await this.tmux.capture(session.paneId, 40).then((output) => tailScreen(output, 40).slice(-3e3)).catch(() => "");
4462
4762
  return new AppError(
@@ -4470,12 +4770,14 @@ ${escapeFence2(output).slice(-6500)}
4470
4770
  }
4471
4771
  );
4472
4772
  }
4473
- async waitForStartupStability(session, durationMs) {
4773
+ async waitForStartupStability(session, durationMs, signal) {
4474
4774
  const deadline = Date.now() + durationMs;
4475
4775
  while (Date.now() < deadline) {
4476
- const pane = await this.tmux.inspect(session.paneId);
4776
+ if (signal?.aborted) throw signal.reason;
4777
+ const pane = await this.tmux.inspect(session.paneId, signal);
4778
+ if (signal?.aborted) throw signal.reason;
4477
4779
  if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4478
- await new Promise((resolve2) => setTimeout(resolve2, 80));
4780
+ await abortableDelay(80, signal);
4479
4781
  }
4480
4782
  return { ok: true };
4481
4783
  }
@@ -4538,13 +4840,13 @@ ${output}`
4538
4840
  const session = this.activeSession();
4539
4841
  if (!session || this.shouldQueueMessage()) return;
4540
4842
  while (this.pendingMessages.length > 0 && !this.shouldQueueMessage()) {
4541
- const message = this.pendingMessages[0];
4542
- if (!message) {
4843
+ const message2 = this.pendingMessages[0];
4844
+ if (!message2) {
4543
4845
  this.pendingMessages.shift();
4544
4846
  continue;
4545
4847
  }
4546
4848
  this.clearPendingCompletion();
4547
- await this.tmux.sendText(session.paneId, message);
4849
+ await this.tmux.sendText(session.paneId, message2);
4548
4850
  this.pendingMessages.shift();
4549
4851
  }
4550
4852
  if (this.pendingMessages.length === 0 && this.state.boundChatId) {
@@ -4553,9 +4855,9 @@ ${output}`
4553
4855
  activeSession() {
4554
4856
  return this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
4555
4857
  }
4556
- async reconcileSessions(discover = false) {
4858
+ async reconcileSessions(discover = false, signal) {
4557
4859
  const previousActive = this.state.activeSessionId;
4558
- const result2 = await this.reconciler.reconcile(this.state, discover);
4860
+ const result2 = await this.reconciler.reconcile(this.state, discover, signal);
4559
4861
  if (!result2.changed) return result2.liveSessions;
4560
4862
  const activeChanged = result2.state.activeSessionId !== previousActive;
4561
4863
  if (result2.removedActive) {
@@ -4612,7 +4914,7 @@ ${output}`
4612
4914
  await this.log(
4613
4915
  `remote session create requested: session=${request.sessionId} agent=${request.agent} resume=${request.resume?.mode ?? "new"}`
4614
4916
  );
4615
- const started = await this.startSession(request.sessionId, request.cwd, request.agent, request.resume);
4917
+ const started = await this.startSession(request.sessionId, request.cwd, request.agent, request.resume, "lark");
4616
4918
  if (!started.ok) {
4617
4919
  await this.log(`remote session create failed: session=${request.sessionId} code=${started.errorCode ?? "UNKNOWN"}`);
4618
4920
  if (started.errorCode === "AGENT_SESSION_IN_USE") {
@@ -4632,7 +4934,7 @@ ${output}`
4632
4934
  }
4633
4935
  const session = selected.value;
4634
4936
  if (request.resume?.mode === "picker") {
4635
- const picker = await this.waitForResumePicker(session);
4937
+ const picker = started.value.resumePicker;
4636
4938
  if (picker) {
4637
4939
  this.pendingResumePickers.set(session.id, request);
4638
4940
  return { ok: true, state: "picker", session, picker };
@@ -4704,14 +5006,36 @@ ${output}`
4704
5006
  `failed to persist recent workspace: cwd=${cwd} error=${errorMessage3(error)}`
4705
5007
  ));
4706
5008
  }
4707
- async log(message) {
4708
- await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
5009
+ async log(message2) {
5010
+ await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message2}
4709
5011
  `, { mode: 384 });
4710
5012
  }
4711
5013
  };
4712
5014
  function fail(error) {
4713
5015
  return { ok: false, ...serializeAppError(error) };
4714
5016
  }
5017
+ function daemonResultAppError(result2) {
5018
+ return new AppError(
5019
+ result2.errorCode ?? "START_FAILED",
5020
+ result2.error,
5021
+ result2.errorContext ?? {}
5022
+ );
5023
+ }
5024
+ function abortableDelay(ms, signal) {
5025
+ if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
5026
+ if (signal.aborted) return Promise.reject(signal.reason);
5027
+ return new Promise((resolve2, reject) => {
5028
+ const timer = setTimeout(() => {
5029
+ signal.removeEventListener("abort", aborted);
5030
+ resolve2();
5031
+ }, ms);
5032
+ const aborted = () => {
5033
+ clearTimeout(timer);
5034
+ reject(signal.reason);
5035
+ };
5036
+ signal.addEventListener("abort", aborted, { once: true });
5037
+ });
5038
+ }
4715
5039
  function agentSessionInUse(sessionId, ownerSessionId) {
4716
5040
  return new AppError(
4717
5041
  "AGENT_SESSION_IN_USE",
@@ -4786,6 +5110,21 @@ function remoteError(result2) {
4786
5110
  switch (result2.errorCode) {
4787
5111
  case "SESSION_EXISTS":
4788
5112
  return context.source === "tmux" ? `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u68C0\u6D4B\u5230\u540C\u540D tmux \u4F1A\u8BDD\uFF0C\u4F46\u5B83\u672A\u767B\u8BB0\u4E3A\u53EF\u8FDE\u63A5\u7684 LCA session\u3002\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0\uFF0C\u6216\u5728\u672C\u673A\u68C0\u67E5 tmux \u4F1A\u8BDD\u3002` : `\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`;
5113
+ case "SESSION_STARTING":
5114
+ return `session\u300C${sessionId}\u300D\u6B63\u5728\u542F\u52A8\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u5B8C\u6210\u540E\u518D\u8BD5\u3002`;
5115
+ case "SESSION_START_TIMEOUT": {
5116
+ const agent = typeof context.agent === "string" ? context.agent : "Agent";
5117
+ const cwd = typeof context.cwd === "string" ? context.cwd : "\u672A\u77E5\u76EE\u5F55";
5118
+ const stage = typeof context.stage === "string" ? context.stage : "unknown";
5119
+ const excerpt = typeof context.terminalExcerpt === "string" && context.terminalExcerpt.trim() ? `
5120
+
5121
+ \u6700\u8FD1\u7EC8\u7AEF\u8F93\u51FA\uFF1A
5122
+ ${context.terminalExcerpt}` : "";
5123
+ return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A${agent} \u542F\u52A8\u8D85\u8FC7 30 \u79D2\uFF0C\u5DF2\u53D6\u6D88\u5E76\u6E05\u7406\u3002
5124
+
5125
+ \u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}
5126
+ \u8D85\u65F6\u9636\u6BB5\uFF1A${stage}${excerpt}`;
5127
+ }
4789
5128
  case "AGENT_SESSION_IN_USE": {
4790
5129
  const ownerSessionId = typeof context.ownerSessionId === "string" ? context.ownerSessionId : "\u73B0\u6709 session";
4791
5130
  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`;