lark-coding-assistant 0.2.6 → 0.2.8

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.
@@ -450,6 +450,7 @@ var SessionReconciler = class {
450
450
  misses = /* @__PURE__ */ new Map();
451
451
  async reconcile(input, discover = false, signal) {
452
452
  const sessions = { ...input.sessions };
453
+ const removedSessions = [];
453
454
  let changed = false;
454
455
  for (const [id, session] of Object.entries(sessions)) {
455
456
  if (signal?.aborted) throw signal.reason;
@@ -467,9 +468,14 @@ var SessionReconciler = class {
467
468
  continue;
468
469
  }
469
470
  if (result2.status === "dead") {
471
+ const terminalOutput = await this.tmux.capture(result2.pane.paneId, 80, signal).catch((error) => {
472
+ void this.log(`failed to capture dead tmux pane ${result2.pane.paneId}: ${errorMessage2(error)}`);
473
+ return "";
474
+ });
470
475
  await this.tmux.killSession(result2.pane.sessionName, signal).catch((error) => this.log(
471
476
  `failed to clean dead tmux session ${result2.pane.sessionName}: ${errorMessage2(error)}`
472
477
  ));
478
+ removedSessions.push({ session, pane: result2.pane, terminalOutput });
473
479
  delete sessions[id];
474
480
  this.misses.delete(id);
475
481
  changed = true;
@@ -478,6 +484,7 @@ var SessionReconciler = class {
478
484
  const misses = (this.misses.get(id) ?? 0) + 1;
479
485
  this.misses.set(id, misses);
480
486
  if (misses < this.missingThreshold) continue;
487
+ removedSessions.push({ session });
481
488
  delete sessions[id];
482
489
  this.misses.delete(id);
483
490
  changed = true;
@@ -489,7 +496,7 @@ var SessionReconciler = class {
489
496
  if (activeSessionId !== input.activeSessionId) changed = true;
490
497
  const removedActive = input.activeSessionId && !sessions[input.activeSessionId] ? input.sessions?.[input.activeSessionId] : void 0;
491
498
  const state = changed ? { ...input, sessions, activeSessionId, updatedAt: Date.now() } : input;
492
- return { state, liveSessions: Object.values(sessions), removedActive, changed };
499
+ return { state, liveSessions: Object.values(sessions), removedActive, removedSessions, changed };
493
500
  }
494
501
  async confirm(session, signal) {
495
502
  const direct = await this.tmux.inspectStatus(session.paneId, signal);
@@ -3222,6 +3229,10 @@ var AssistantDaemon = class {
3222
3229
  }
3223
3230
  case "start":
3224
3231
  return this.startSession(request.sessionId, request.cwd, request.agent, request.resume);
3232
+ case "reconcile": {
3233
+ await this.reconcileSessions(true);
3234
+ return { ok: true };
3235
+ }
3225
3236
  case "status":
3226
3237
  return { ok: true, value: await this.runtimeStatus(request.sessionId) };
3227
3238
  case "tail":
@@ -3333,11 +3344,6 @@ var AssistantDaemon = class {
3333
3344
  agentVersion,
3334
3345
  updatedAt: Date.now()
3335
3346
  };
3336
- const pendingClaim = this.pendingAgentSessionClaims.get(sessionId);
3337
- if (pendingClaim) {
3338
- const claimed = this.claimStartingAgentSession(session, pendingClaim);
3339
- if (!claimed.ok) return claimed;
3340
- }
3341
3347
  if (resume && resume.mode !== "picker") {
3342
3348
  const initialClaim = await context.stage(
3343
3349
  "agent-identity",
@@ -3376,11 +3382,6 @@ var AssistantDaemon = class {
3376
3382
  ));
3377
3383
  await this.rememberSessionWorkspace(session.cwd);
3378
3384
  }
3379
- const lateClaim = this.pendingAgentSessionClaims.get(sessionId);
3380
- if (lateClaim) {
3381
- const claimed = this.claimStartingAgentSession(session, lateClaim);
3382
- if (!claimed.ok) return claimed;
3383
- }
3384
3385
  try {
3385
3386
  await context.stage("metadata", () => this.tmux.writeMetadata(pane.sessionName, {
3386
3387
  managed: true,
@@ -3391,6 +3392,7 @@ var AssistantDaemon = class {
3391
3392
  agentSessionId: session.agentSessionId
3392
3393
  }, context.signal));
3393
3394
  await context.stage("state", () => this.commitStartedSession(session, binding));
3395
+ await this.clearSessionExitEvent(sessionId, context.startedAt);
3394
3396
  } catch (error) {
3395
3397
  return fail(isAppError(error) ? error : new AppError("START_FAILED", "failed to persist completed session", { sessionId }, { cause: error }));
3396
3398
  }
@@ -4480,6 +4482,26 @@ ${escapeFence2(output).slice(-6500)}
4480
4482
  }
4481
4483
  await this.store.saveState(this.state);
4482
4484
  }
4485
+ async clearSessionExitEvent(sessionId, before) {
4486
+ const event = this.state.recentSessionExits?.[sessionId];
4487
+ if (!event || event.occurredAt >= before) return;
4488
+ const recentSessionExits = { ...this.state.recentSessionExits };
4489
+ delete recentSessionExits[sessionId];
4490
+ this.state = { ...this.state, recentSessionExits, updatedAt: Date.now() };
4491
+ await this.store.saveState(this.state);
4492
+ }
4493
+ async rememberSessionExitEvent(event) {
4494
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
4495
+ const recentSessionExits = Object.fromEntries(Object.entries(this.state.recentSessionExits ?? {}).filter(([, value]) => value.occurredAt >= cutoff));
4496
+ recentSessionExits[event.sessionId] = event;
4497
+ const retained = Object.entries(recentSessionExits).sort(([, left], [, right]) => right.occurredAt - left.occurredAt).slice(0, 20);
4498
+ this.state = {
4499
+ ...this.state,
4500
+ recentSessionExits: Object.fromEntries(retained),
4501
+ updatedAt: Date.now()
4502
+ };
4503
+ await this.store.saveState(this.state);
4504
+ }
4483
4505
  async resetOwner() {
4484
4506
  this.pendingMessages.length = 0;
4485
4507
  this.pendingInteractionInput = void 0;
@@ -4633,20 +4655,56 @@ ${escapeFence2(output).slice(-6500)}
4633
4655
  if (session && session.agent !== candidate.agent) {
4634
4656
  return { ok: false, error: "agent-session candidate does not match managed session" };
4635
4657
  }
4636
- const owner = this.findAgentSessionOwner(candidate.agent, candidate.agentSessionId, candidate.sessionId);
4637
- if (owner) {
4658
+ if (!session) {
4638
4659
  this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
4639
4660
  await this.log(
4640
- `agent session conflict: session=${candidate.sessionId} agent=${candidate.agent} agentSession=${candidate.agentSessionId} owner=${owner.id}`
4661
+ `agent session hook candidate awaiting managed session: session=${candidate.sessionId} agent=${candidate.agent} agentSession=${candidate.agentSessionId}`
4641
4662
  );
4642
- setTimeout(() => void this.rejectDuplicateAgentSession(candidate, owner), 100);
4643
- return fail(agentSessionInUse(candidate.sessionId, owner.id));
4663
+ return { ok: true };
4644
4664
  }
4645
- if (!session) {
4646
- this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
4665
+ if (session.agentSessionId) {
4666
+ this.pendingAgentSessionClaims.delete(session.id);
4667
+ if (session.agentSessionId !== candidate.agentSessionId) {
4668
+ await this.log(
4669
+ `ignored agent session hook candidate: session=${session.id} agent=${session.agent} hookSession=${candidate.agentSessionId} confirmedSession=${session.agentSessionId}`
4670
+ );
4671
+ }
4647
4672
  return { ok: true };
4648
4673
  }
4649
- if (session.agentSessionId === candidate.agentSessionId) return { ok: true };
4674
+ this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
4675
+ const agentSessionId = await this.resolvePaneNativeAgentSessionId(session);
4676
+ if (!agentSessionId) return { ok: true };
4677
+ if (agentSessionId !== candidate.agentSessionId) {
4678
+ await this.log(
4679
+ `ignored stale agent session hook candidate: session=${session.id} agent=${session.agent} hookSession=${candidate.agentSessionId} pidSession=${agentSessionId}`
4680
+ );
4681
+ }
4682
+ const confirmed = { ...candidate, agentSessionId, source: "pid-confirmed" };
4683
+ const owner = this.findAgentSessionOwner(session.agent, agentSessionId, session.id);
4684
+ if (owner) {
4685
+ await this.log(
4686
+ `confirmed agent session conflict: session=${session.id} agent=${session.agent} agentSession=${agentSessionId} owner=${owner.id}`
4687
+ );
4688
+ await this.rejectDuplicateAgentSession(confirmed, owner);
4689
+ return fail(agentSessionInUse(session.id, owner.id, agentSessionId));
4690
+ }
4691
+ return this.persistConfirmedAgentSessionClaim(session, confirmed);
4692
+ }
4693
+ async resolvePaneNativeAgentSessionId(session) {
4694
+ let pane;
4695
+ try {
4696
+ pane = await this.tmux.inspect(session.paneId);
4697
+ } catch (error) {
4698
+ void this.log(`failed to inspect pane for native session: session=${session.id} error=${errorMessage3(error)}`);
4699
+ return void 0;
4700
+ }
4701
+ if (!pane || pane.dead) return void 0;
4702
+ return resolveNativeAgentSessionId(session.agent, pane.pid).catch((error) => {
4703
+ void this.log(`failed to resolve native agent session for ${session.id}: ${errorMessage3(error)}`);
4704
+ return void 0;
4705
+ });
4706
+ }
4707
+ async persistConfirmedAgentSessionClaim(session, candidate) {
4650
4708
  const updated = { ...session, agentSessionId: candidate.agentSessionId, updatedAt: Date.now() };
4651
4709
  this.state = {
4652
4710
  ...this.state,
@@ -4671,12 +4729,7 @@ ${escapeFence2(output).slice(-6500)}
4671
4729
  async refreshNativeAgentSessionClaims() {
4672
4730
  for (const session of Object.values(this.state.sessions ?? {})) {
4673
4731
  if (session.agentSessionId) continue;
4674
- const pane = await this.tmux.inspect(session.paneId);
4675
- if (!pane || pane.dead) continue;
4676
- const agentSessionId = await resolveNativeAgentSessionId(session.agent, pane.pid).catch((error) => {
4677
- void this.log(`failed to resolve native agent session for ${session.id}: ${errorMessage3(error)}`);
4678
- return void 0;
4679
- });
4732
+ const agentSessionId = await this.resolvePaneNativeAgentSessionId(session);
4680
4733
  if (!agentSessionId) continue;
4681
4734
  await this.handleAgentSessionStarted({
4682
4735
  sessionId: session.id,
@@ -4696,11 +4749,6 @@ ${escapeFence2(output).slice(-6500)}
4696
4749
  session.agentSessionId = committedClaim2;
4697
4750
  return { ok: true };
4698
4751
  }
4699
- const pending2 = this.pendingAgentSessionClaims.get(session.id);
4700
- if (pending2) {
4701
- const claimed = this.claimStartingAgentSession(session, pending2);
4702
- if (!claimed.ok || session.agentSessionId) return claimed;
4703
- }
4704
4752
  if (signal?.aborted) throw signal.reason;
4705
4753
  const pane2 = await this.tmux.inspect(session.paneId, signal);
4706
4754
  if (signal?.aborted) throw signal.reason;
@@ -4725,8 +4773,6 @@ ${escapeFence2(output).slice(-6500)}
4725
4773
  const pane = await this.tmux.inspect(session.paneId, signal);
4726
4774
  if (signal?.aborted) throw signal.reason;
4727
4775
  if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4728
- const pending = this.pendingAgentSessionClaims.get(session.id);
4729
- if (pending) return this.claimStartingAgentSession(session, pending);
4730
4776
  const committedClaim = this.state.sessions?.[session.id]?.agentSessionId;
4731
4777
  if (committedClaim) {
4732
4778
  session.agentSessionId = committedClaim;
@@ -4749,7 +4795,7 @@ ${escapeFence2(output).slice(-6500)}
4749
4795
  const owner = this.findAgentSessionOwner(candidate.agent, candidate.agentSessionId, session.id);
4750
4796
  if (owner) {
4751
4797
  this.pendingAgentSessionClaims.delete(session.id);
4752
- return fail(agentSessionInUse(session.id, owner.id));
4798
+ return fail(agentSessionInUse(session.id, owner.id, candidate.agentSessionId));
4753
4799
  }
4754
4800
  session.agentSessionId = candidate.agentSessionId;
4755
4801
  session.updatedAt = Date.now();
@@ -4787,6 +4833,14 @@ ${escapeFence2(output).slice(-6500)}
4787
4833
  return pending ? { id: pending.sessionId } : void 0;
4788
4834
  }
4789
4835
  async rejectDuplicateAgentSession(candidate, owner) {
4836
+ await this.rememberSessionExitEvent({
4837
+ sessionId: candidate.sessionId,
4838
+ agent: candidate.agent,
4839
+ reason: "agent-session-conflict",
4840
+ agentSessionId: candidate.agentSessionId,
4841
+ ownerSessionId: owner.id,
4842
+ occurredAt: Date.now()
4843
+ });
4790
4844
  const duplicate = this.state.sessions?.[candidate.sessionId];
4791
4845
  if (duplicate) await this.stopSession(duplicate.id).catch((error) => this.log(
4792
4846
  `failed to stop duplicate agent session ${duplicate.id}: ${errorMessage3(error)}`
@@ -4799,7 +4853,7 @@ ${escapeFence2(output).slice(-6500)}
4799
4853
  if (this.state.boundChatId) {
4800
4854
  await this.gateway?.sendText(
4801
4855
  this.state.boundChatId,
4802
- `${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`
4856
+ `${candidate.agent} \u539F\u751F session\u300C${candidate.agentSessionId}\u300D\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`
4803
4857
  ).catch((error) => this.log(`agent session conflict notification failed: ${errorMessage3(error)}`));
4804
4858
  }
4805
4859
  }
@@ -4871,6 +4925,22 @@ ${output}`
4871
4925
  ).catch((error) => this.log(`exit notification failed: ${errorMessage3(error)}`));
4872
4926
  }
4873
4927
  this.state = result2.state;
4928
+ for (const removed of result2.removedSessions) {
4929
+ const exitStatus = removed.pane?.exitStatus;
4930
+ if (exitStatus === void 0 || exitStatus === 0) continue;
4931
+ const terminalExcerpt = startupTerminalExcerpt(tailScreen(removed.terminalOutput ?? "", 80).slice(-3e3));
4932
+ await this.rememberSessionExitEvent({
4933
+ sessionId: removed.session.id,
4934
+ agent: removed.session.agent,
4935
+ reason: "agent-exited",
4936
+ exitStatus,
4937
+ terminalExcerpt: terminalExcerpt || void 0,
4938
+ occurredAt: Date.now()
4939
+ });
4940
+ await this.log(
4941
+ `agent exited: session=${removed.session.id} agent=${removed.session.agent} exit=${exitStatus}`
4942
+ );
4943
+ }
4874
4944
  for (const sessionId of this.pendingResumePickers.keys()) {
4875
4945
  if (!this.state.sessions?.[sessionId]) this.pendingResumePickers.delete(sessionId);
4876
4946
  }
@@ -5035,11 +5105,11 @@ function abortableDelay(ms, signal) {
5035
5105
  signal.addEventListener("abort", aborted, { once: true });
5036
5106
  });
5037
5107
  }
5038
- function agentSessionInUse(sessionId, ownerSessionId) {
5108
+ function agentSessionInUse(sessionId, ownerSessionId, agentSessionId) {
5039
5109
  return new AppError(
5040
5110
  "AGENT_SESSION_IN_USE",
5041
5111
  `agent session is already managed by ${ownerSessionId}`,
5042
- { sessionId, ownerSessionId }
5112
+ { sessionId, ownerSessionId, agentSessionId }
5043
5113
  );
5044
5114
  }
5045
5115
  function errorMessage3(error) {