lark-coding-assistant 0.2.2 → 0.2.4

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.
@@ -5,13 +5,14 @@ import { createServer } from "net";
5
5
  // src/platform/process.ts
6
6
  import { execFile, spawn } from "child_process";
7
7
  function runFile(file, args, options = {}) {
8
- return new Promise((resolve, reject) => {
8
+ return new Promise((resolve2, reject) => {
9
9
  execFile(
10
10
  file,
11
11
  [...args],
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
  },
@@ -20,13 +21,13 @@ function runFile(file, args, options = {}) {
20
21
  reject(Object.assign(error, { stdout, stderr }));
21
22
  return;
22
23
  }
23
- resolve({ stdout, stderr });
24
+ resolve2({ stdout, stderr });
24
25
  }
25
26
  );
26
27
  });
27
28
  }
28
29
  function runFileWithInput(file, args, input) {
29
- return new Promise((resolve, reject) => {
30
+ return new Promise((resolve2, reject) => {
30
31
  const child = spawn(file, [...args], { stdio: ["pipe", "pipe", "pipe"] });
31
32
  let stdout = "";
32
33
  let stderr = "";
@@ -36,7 +37,7 @@ function runFileWithInput(file, args, input) {
36
37
  child.stderr.on("data", (chunk) => stderr += chunk);
37
38
  child.once("error", reject);
38
39
  child.once("exit", (code, signal) => {
39
- if (code === 0) resolve({ stdout, stderr });
40
+ if (code === 0) resolve2({ stdout, stderr });
40
41
  else reject(new Error(`${file} exited with ${code ?? signal}: ${stderr.trim()}`));
41
42
  });
42
43
  child.stdin.end(input, "utf8");
@@ -100,6 +101,88 @@ function normalizeAgentId(value) {
100
101
  return AGENT_IDS.includes(value) ? value : void 0;
101
102
  }
102
103
 
104
+ // src/workspace/path.ts
105
+ import { constants } from "fs";
106
+ import { access, stat } from "fs/promises";
107
+ import { homedir } from "os";
108
+ import { isAbsolute, normalize, resolve } from "path";
109
+
110
+ // src/core/errors.ts
111
+ var AppError = class extends Error {
112
+ code;
113
+ context;
114
+ constructor(code, message2, context = {}, options = {}) {
115
+ super(message2, options);
116
+ this.name = "AppError";
117
+ this.code = code;
118
+ this.context = context;
119
+ }
120
+ };
121
+ function isAppError(error) {
122
+ return error instanceof AppError;
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
+ }
131
+ function errorMessage(error) {
132
+ return error instanceof Error ? error.message : String(error);
133
+ }
134
+ function serializeAppError(error) {
135
+ if (!isAppError(error)) return { error: errorMessage(error) };
136
+ const errorContext = Object.fromEntries(
137
+ Object.entries(error.context).filter(([, value]) => value !== void 0)
138
+ );
139
+ return {
140
+ error: error.message,
141
+ errorCode: error.code,
142
+ ...Object.keys(errorContext).length > 0 ? { errorContext } : {}
143
+ };
144
+ }
145
+ function systemErrorCode(error) {
146
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
147
+ }
148
+
149
+ // src/workspace/path.ts
150
+ function normalizeWorkspacePath(input, home = homedir()) {
151
+ const value = input.trim();
152
+ if (!value) throw invalidCwd(input, "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
153
+ let expanded = value;
154
+ if (value === "~") expanded = home;
155
+ else if (value.startsWith("~/")) expanded = resolve(home, value.slice(2));
156
+ else if (value.startsWith("~")) throw invalidCwd(input, "\u4E0D\u652F\u6301 ~other-user\uFF0C\u8BF7\u4F7F\u7528 ~ \u6216 ~/\u76EE\u5F55");
157
+ if (!isAbsolute(expanded)) throw invalidCwd(input, "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u6216\u4F7F\u7528 ~/\u76EE\u5F55");
158
+ return normalize(expanded);
159
+ }
160
+ async function validateWorkspaceDirectory(input, home = homedir()) {
161
+ const cwd = normalizeWorkspacePath(input, home);
162
+ let info;
163
+ try {
164
+ info = await stat(cwd);
165
+ } catch (error) {
166
+ const reason = systemErrorCode(error) === "EACCES" ? `\u65E0\u6743\u8BBF\u95EE\u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}` : `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u7528\uFF1A${cwd}`;
167
+ throw invalidCwd(cwd, reason, error);
168
+ }
169
+ if (!info.isDirectory()) throw invalidCwd(cwd, `\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\u5939\uFF1A${cwd}`);
170
+ try {
171
+ await access(cwd, constants.R_OK | constants.X_OK);
172
+ } catch (error) {
173
+ throw invalidCwd(cwd, `\u65E0\u6743\u8FDB\u5165\u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}`, error);
174
+ }
175
+ return cwd;
176
+ }
177
+ function normalizeWorkspaceRoots(values, home = homedir()) {
178
+ const unique = /* @__PURE__ */ new Set();
179
+ for (const value of values) unique.add(normalizeWorkspacePath(value, home));
180
+ return [...unique];
181
+ }
182
+ function invalidCwd(cwd, reason, cause) {
183
+ return new AppError("INVALID_CWD", reason, { cwd, reason }, cause === void 0 ? {} : { cause });
184
+ }
185
+
103
186
  // src/core/store.ts
104
187
  var AppStore = class {
105
188
  constructor(paths2) {
@@ -129,11 +212,15 @@ var AppStore = class {
129
212
  traex: binaries.traex ?? binaries["trae-cli"] ?? "trae-cli",
130
213
  claude: binaries.claude ?? binaries["claude-code"] ?? "claude"
131
214
  },
132
- pollIntervalMs: config.pollIntervalMs
215
+ pollIntervalMs: config.pollIntervalMs,
216
+ workspaceRoots: normalizeWorkspaceRoots(config.workspaceRoots ?? [])
133
217
  };
134
218
  }
135
219
  saveConfig(config) {
136
- return writeJsonAtomic(this.paths.config, config);
220
+ return writeJsonAtomic(this.paths.config, {
221
+ ...config,
222
+ workspaceRoots: normalizeWorkspaceRoots(config.workspaceRoots)
223
+ });
137
224
  }
138
225
  loadSecrets() {
139
226
  return readJson(this.paths.secrets);
@@ -148,12 +235,33 @@ var AppStore = class {
148
235
  const agent = normalizeAgentId(session.agent);
149
236
  return agent ? [[id, { ...session, agent }]] : [];
150
237
  }));
151
- return { ...state, sessions };
238
+ const recentWorkspaces = await normalizeRecentWorkspaces(state.recentWorkspaces ?? []);
239
+ return { ...state, sessions, recentWorkspaces };
152
240
  }
153
241
  saveState(state) {
154
242
  return writeJsonAtomic(this.paths.state, state);
155
243
  }
156
244
  };
245
+ async function normalizeRecentWorkspaces(values) {
246
+ const unique = /* @__PURE__ */ new Set();
247
+ const result2 = [];
248
+ const ordered = [...values].sort((left, right) => right.lastUsedAt - left.lastUsedAt);
249
+ for (const value of ordered) {
250
+ if (result2.length >= 30) break;
251
+ if (!Number.isFinite(value.lastUsedAt)) continue;
252
+ let cwd;
253
+ try {
254
+ cwd = normalizeWorkspacePath(value.cwd);
255
+ await validateWorkspaceDirectory(cwd);
256
+ } catch {
257
+ continue;
258
+ }
259
+ if (unique.has(cwd)) continue;
260
+ unique.add(cwd);
261
+ result2.push({ cwd, lastUsedAt: value.lastUsedAt });
262
+ }
263
+ return result2;
264
+ }
157
265
  function createBindCode() {
158
266
  return randomBytes(16).toString("base64url");
159
267
  }
@@ -170,41 +278,7 @@ function verifyBindCode(code, encoded) {
170
278
  return actual.length === expected.length && timingSafeEqual(actual, expected);
171
279
  }
172
280
 
173
- // src/core/errors.ts
174
- var AppError = class extends Error {
175
- code;
176
- context;
177
- constructor(code, message, context = {}, options = {}) {
178
- super(message, options);
179
- this.name = "AppError";
180
- this.code = code;
181
- this.context = context;
182
- }
183
- };
184
- function isAppError(error) {
185
- return error instanceof AppError;
186
- }
187
- function errorMessage(error) {
188
- return error instanceof Error ? error.message : String(error);
189
- }
190
- function serializeAppError(error) {
191
- if (!isAppError(error)) return { error: errorMessage(error) };
192
- const errorContext = Object.fromEntries(
193
- Object.entries(error.context).filter(([, value]) => value !== void 0)
194
- );
195
- return {
196
- error: error.message,
197
- errorCode: error.code,
198
- ...Object.keys(errorContext).length > 0 ? { errorContext } : {}
199
- };
200
- }
201
- function systemErrorCode(error) {
202
- return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
203
- }
204
-
205
281
  // src/session/start-request.ts
206
- import { stat } from "fs/promises";
207
- import { isAbsolute } from "path";
208
282
  async function validateStartSessionRequest(request) {
209
283
  if (!validSessionId(request.sessionId)) {
210
284
  throw new AppError(
@@ -213,21 +287,13 @@ async function validateStartSessionRequest(request) {
213
287
  { sessionId: request.sessionId }
214
288
  );
215
289
  }
216
- if (!isAbsolute(request.cwd)) {
217
- throw new AppError("INVALID_CWD", "working directory must be absolute", { cwd: request.cwd });
218
- }
219
- const info = await stat(request.cwd).catch((error) => {
220
- throw new AppError("INVALID_CWD", `working directory is unavailable: ${request.cwd}`, { cwd: request.cwd }, { cause: error });
221
- });
222
- if (!info.isDirectory()) {
223
- throw new AppError("INVALID_CWD", `working directory is not a directory: ${request.cwd}`, { cwd: request.cwd });
224
- }
290
+ const cwd = await validateWorkspaceDirectory(request.cwd);
225
291
  if (request.resume?.mode === "session" && !request.resume.sessionId.trim()) {
226
292
  throw new AppError("INVALID_RESUME", "resume session id must not be empty", {
227
293
  reason: "\u6062\u590D\u5386\u53F2\u4F1A\u8BDD\u65F6\u5FC5\u987B\u63D0\u4F9B session ID"
228
294
  });
229
295
  }
230
- return request;
296
+ return { ...request, cwd };
231
297
  }
232
298
  function validSessionId(value) {
233
299
  return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
@@ -235,16 +301,138 @@ function validSessionId(value) {
235
301
 
236
302
  // src/session/startup-failure.ts
237
303
  function sessionStartupFailure(result2, fallback) {
238
- 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;
239
305
  const context = result2.errorContext ?? {};
240
306
  return {
241
307
  sessionId: typeof context.sessionId === "string" ? context.sessionId : fallback.sessionId,
242
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 } : {},
243
313
  ...typeof context.exitStatus === "number" ? { exitStatus: context.exitStatus } : {},
244
314
  terminalExcerpt: typeof context.terminalExcerpt === "string" && context.terminalExcerpt.trim() ? context.terminalExcerpt : "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002"
245
315
  };
246
316
  }
247
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
+
248
436
  // src/session/reconciler.ts
249
437
  var SessionReconciler = class {
250
438
  constructor(tmux, sessionPrefix = "lark-coding-assistant", missingThreshold = 3, log = async () => void 0, resolveAgentVersion = async () => "unknown") {
@@ -260,11 +448,12 @@ var SessionReconciler = class {
260
448
  log;
261
449
  resolveAgentVersion;
262
450
  misses = /* @__PURE__ */ new Map();
263
- async reconcile(input, discover = false) {
451
+ async reconcile(input, discover = false, signal) {
264
452
  const sessions = { ...input.sessions };
265
453
  let changed = false;
266
454
  for (const [id, session] of Object.entries(sessions)) {
267
- const result2 = await this.confirm(session);
455
+ if (signal?.aborted) throw signal.reason;
456
+ const result2 = await this.confirm(session, signal);
268
457
  if (result2.status === "unavailable") {
269
458
  await this.log(`tmux inspection unavailable for ${id}: ${errorMessage2(result2.error)}`);
270
459
  continue;
@@ -277,6 +466,15 @@ var SessionReconciler = class {
277
466
  }
278
467
  continue;
279
468
  }
469
+ if (result2.status === "dead") {
470
+ await this.tmux.killSession(result2.pane.sessionName, signal).catch((error) => this.log(
471
+ `failed to clean dead tmux session ${result2.pane.sessionName}: ${errorMessage2(error)}`
472
+ ));
473
+ delete sessions[id];
474
+ this.misses.delete(id);
475
+ changed = true;
476
+ continue;
477
+ }
280
478
  const misses = (this.misses.get(id) ?? 0) + 1;
281
479
  this.misses.set(id, misses);
282
480
  if (misses < this.missingThreshold) continue;
@@ -285,7 +483,7 @@ var SessionReconciler = class {
285
483
  changed = true;
286
484
  }
287
485
  if (discover) {
288
- changed = await this.discover(sessions) || changed;
486
+ changed = await this.discover(sessions, signal) || changed;
289
487
  }
290
488
  const activeSessionId = input.activeSessionId && sessions[input.activeSessionId] ? input.activeSessionId : Object.keys(sessions)[0];
291
489
  if (activeSessionId !== input.activeSessionId) changed = true;
@@ -293,45 +491,55 @@ var SessionReconciler = class {
293
491
  const state = changed ? { ...input, sessions, activeSessionId, updatedAt: Date.now() } : input;
294
492
  return { state, liveSessions: Object.values(sessions), removedActive, changed };
295
493
  }
296
- async confirm(session) {
297
- const direct = await this.tmux.inspectStatus(session.paneId);
494
+ async confirm(session, signal) {
495
+ const direct = await this.tmux.inspectStatus(session.paneId, signal);
298
496
  if (direct.status === "live" || direct.status === "unavailable") return direct;
299
- const byName = await this.tmux.inspectSession(session.sessionName);
497
+ const byName = await this.tmux.inspectSession(session.sessionName, signal);
300
498
  if (byName.status === "live" || byName.status === "unavailable") return byName;
301
499
  return byName.status === "dead" ? byName : direct;
302
500
  }
303
- async discover(sessions) {
501
+ async discover(sessions, signal) {
304
502
  let panes;
305
503
  try {
306
- panes = await this.tmux.listSessions(`${this.sessionPrefix}-`);
504
+ panes = await this.tmux.listSessions(`${this.sessionPrefix}-`, signal);
307
505
  } catch (error) {
308
506
  await this.log(`tmux session discovery unavailable: ${errorMessage2(error)}`);
309
507
  return false;
310
508
  }
311
509
  let changed = false;
312
510
  const seen = /* @__PURE__ */ new Set();
511
+ const liveSessionNames = new Set(panes.filter((pane) => !pane.dead).map((pane) => pane.sessionName));
313
512
  for (const pane of panes) {
314
- if (seen.has(pane.sessionName) || pane.dead) continue;
513
+ if (signal?.aborted) throw signal.reason;
514
+ if (seen.has(pane.sessionName)) continue;
315
515
  seen.add(pane.sessionName);
516
+ if (pane.dead) {
517
+ if (!liveSessionNames.has(pane.sessionName)) {
518
+ await this.tmux.killSession(pane.sessionName, signal).catch((error) => this.log(
519
+ `failed to clean orphaned dead tmux session ${pane.sessionName}: ${errorMessage2(error)}`
520
+ ));
521
+ }
522
+ continue;
523
+ }
316
524
  const registered = Object.values(sessions).find((session) => session.sessionName === pane.sessionName);
317
525
  if (registered) {
318
526
  if (registered.paneId !== pane.paneId) {
319
527
  sessions[registered.id] = { ...registered, paneId: pane.paneId, updatedAt: Date.now() };
320
528
  changed = true;
321
529
  }
322
- if (!await this.tmux.readMetadata(pane.sessionName)) {
323
- 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(
324
532
  `failed to backfill tmux metadata for ${registered.id}: ${errorMessage2(error)}`
325
533
  ));
326
534
  }
327
535
  continue;
328
536
  }
329
- const metadata = await this.tmux.readMetadata(pane.sessionName);
330
- 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);
331
539
  if (!recovered || sessions[recovered.id]) continue;
332
540
  if (!metadata) {
333
541
  try {
334
- await this.writeMetadata(pane, recovered);
542
+ await this.writeMetadata(pane, recovered, signal);
335
543
  } catch (error) {
336
544
  await this.log(`failed to persist recovered tmux metadata for ${recovered.id}: ${errorMessage2(error)}`);
337
545
  continue;
@@ -357,11 +565,11 @@ var SessionReconciler = class {
357
565
  updatedAt: Date.now()
358
566
  };
359
567
  }
360
- async fromLegacy(pane) {
568
+ async fromLegacy(pane, signal) {
361
569
  const id = pane.sessionName.startsWith(`${this.sessionPrefix}-`) ? pane.sessionName.slice(this.sessionPrefix.length + 1) : "";
362
570
  const agent = inferLegacyAgent(pane);
363
571
  if (!validSessionId(id) || !agent) return void 0;
364
- const agentVersion = await this.resolveAgentVersion(agent).catch(() => "unknown");
572
+ const agentVersion = await this.resolveAgentVersion(agent, signal).catch(() => "unknown");
365
573
  return {
366
574
  id,
367
575
  agent,
@@ -372,7 +580,7 @@ var SessionReconciler = class {
372
580
  updatedAt: Date.now()
373
581
  };
374
582
  }
375
- writeMetadata(pane, session) {
583
+ writeMetadata(pane, session, signal) {
376
584
  return this.tmux.writeMetadata(pane.sessionName, {
377
585
  managed: true,
378
586
  sessionId: session.id,
@@ -380,7 +588,7 @@ var SessionReconciler = class {
380
588
  cwd: session.cwd,
381
589
  agentVersion: session.agentVersion,
382
590
  agentSessionId: session.agentSessionId
383
- });
591
+ }, signal);
384
592
  }
385
593
  };
386
594
  function inferLegacyAgent(pane) {
@@ -397,20 +605,20 @@ function errorMessage2(error) {
397
605
 
398
606
  // src/session/native-session.ts
399
607
  import { readdir, readFile as readFile2 } from "fs/promises";
400
- import { homedir } from "os";
608
+ import { homedir as homedir2 } from "os";
401
609
  import { join } from "path";
402
610
  var UUID = "[0-9a-fA-F-]{32,36}";
403
- async function resolveNativeAgentSessionId(agent, pid, home = homedir()) {
611
+ async function resolveNativeAgentSessionId(agent, pid, home = homedir2(), signal) {
404
612
  if (!Number.isInteger(pid) || pid <= 0) return void 0;
405
613
  if (agent === "traex") {
406
614
  const peer = await resolveTraexPeer(pid, home);
407
615
  if (peer) return peer;
408
616
  return matchPath(
409
- await processOpenFiles(pid),
617
+ await processOpenFiles(pid, signal),
410
618
  new RegExp(`/\\.trae/cli/sessions/.+/rollout-[^/]+-(${UUID})\\.jsonl(?:\\.lock)?$`)
411
619
  );
412
620
  }
413
- const openFiles = await processOpenFiles(pid);
621
+ const openFiles = await processOpenFiles(pid, signal);
414
622
  if (agent === "codex") return matchPath(openFiles, new RegExp(`/\\.codex/thread-writer-locks/(${UUID})\\.lock$`));
415
623
  return matchPath(openFiles, new RegExp(`/\\.claude/projects/[^/]+/(${UUID})\\.jsonl$`));
416
624
  }
@@ -426,8 +634,8 @@ async function resolveTraexPeer(pid, home) {
426
634
  }
427
635
  return void 0;
428
636
  }
429
- async function processOpenFiles(pid) {
430
- 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);
431
639
  if (!result2) return [];
432
640
  return result2.stdout.split("\n").filter((line) => line.startsWith("n")).map((line) => line.slice(1));
433
641
  }
@@ -482,11 +690,11 @@ var TmuxController = class {
482
690
  { sessionId: options.sessionName }
483
691
  );
484
692
  }
485
- if (await this.hasSession(options.sessionName)) {
693
+ if (await this.hasSession(options.sessionName, options.signal)) {
486
694
  throw new AppError(
487
695
  "SESSION_EXISTS",
488
696
  `tmux session already exists: ${options.sessionName}`,
489
- { sessionId: displaySessionId(options.sessionName) }
697
+ { sessionId: displaySessionId(options.sessionName), source: "tmux" }
490
698
  );
491
699
  }
492
700
  const environment = Object.entries(options.env ?? {}).map(([key, value]) => {
@@ -514,34 +722,34 @@ var TmuxController = class {
514
722
  if (options.preserveOnExit) {
515
723
  createArgs.push(";", "set-option", "-w", "-t", `=${options.sessionName}:`, "remain-on-exit", "on");
516
724
  }
517
- await runFile(this.binary, createArgs);
518
- 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);
519
727
  if (!pane) throw new Error("tmux created a session without a discoverable pane");
520
728
  return pane;
521
729
  }
522
- async hasSession(sessionName) {
730
+ async hasSession(sessionName, signal) {
523
731
  try {
524
- await runFile(this.binary, ["has-session", "-t", `=${sessionName}`]);
732
+ await runFile(this.binary, ["has-session", "-t", `=${sessionName}`], { signal });
525
733
  return true;
526
734
  } catch {
527
735
  return false;
528
736
  }
529
737
  }
530
- async findBySession(sessionName) {
738
+ async findBySession(sessionName, signal) {
531
739
  const { stdout } = await runFile(this.binary, [
532
740
  "list-panes",
533
741
  "-t",
534
742
  `=${sessionName}`,
535
743
  "-F",
536
744
  PANE_FORMAT
537
- ]);
745
+ ], { signal });
538
746
  return stdout.split("\n").map(parsePane).find(Boolean);
539
747
  }
540
- async inspect(paneId) {
541
- const result2 = await this.inspectStatus(paneId);
748
+ async inspect(paneId, signal) {
749
+ const result2 = await this.inspectStatus(paneId, signal);
542
750
  return result2.status === "live" || result2.status === "dead" ? result2.pane : void 0;
543
751
  }
544
- async inspectStatus(paneId) {
752
+ async inspectStatus(paneId, signal) {
545
753
  assertSafeTmuxTarget(paneId);
546
754
  try {
547
755
  const { stdout } = await runFile(this.binary, [
@@ -550,7 +758,7 @@ var TmuxController = class {
550
758
  "-t",
551
759
  paneId,
552
760
  PANE_FORMAT
553
- ]);
761
+ ], { signal });
554
762
  if (!stdout.trim()) return { status: "missing" };
555
763
  const pane = parsePane(stdout.trim());
556
764
  if (!pane) return { status: "unavailable", error: new Error("invalid tmux pane response") };
@@ -559,20 +767,20 @@ var TmuxController = class {
559
767
  return tmuxTargetMissing(error) ? { status: "missing" } : { status: "unavailable", error };
560
768
  }
561
769
  }
562
- async inspectSession(sessionName) {
770
+ async inspectSession(sessionName, signal) {
563
771
  try {
564
- const pane = await this.findBySession(sessionName);
772
+ const pane = await this.findBySession(sessionName, signal);
565
773
  if (!pane) return { status: "missing" };
566
774
  return pane.dead ? { status: "dead", pane } : { status: "live", pane };
567
775
  } catch (error) {
568
776
  return tmuxTargetMissing(error) ? { status: "missing" } : { status: "unavailable", error };
569
777
  }
570
778
  }
571
- async listSessions(prefix) {
572
- 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 });
573
781
  return stdout.split("\n").map(parsePane).filter((pane) => Boolean(pane?.sessionName.startsWith(prefix)));
574
782
  }
575
- async writeMetadata(sessionName, metadata) {
783
+ async writeMetadata(sessionName, metadata, signal) {
576
784
  const values = {
577
785
  managed: "1",
578
786
  sessionId: metadata.sessionId,
@@ -584,16 +792,16 @@ var TmuxController = class {
584
792
  for (const [key, option2] of Object.entries(METADATA_OPTIONS)) {
585
793
  const value = values[key];
586
794
  if (value === void 0) {
587
- 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);
588
796
  } else {
589
- await runFile(this.binary, ["set-option", "-t", sessionName, option2, value]);
797
+ await runFile(this.binary, ["set-option", "-t", sessionName, option2, value], { signal });
590
798
  }
591
799
  }
592
800
  }
593
- async readMetadata(sessionName) {
801
+ async readMetadata(sessionName, signal) {
594
802
  const values = {};
595
803
  for (const [key, option2] of Object.entries(METADATA_OPTIONS)) {
596
- const result2 = await runFile(this.binary, ["show-options", "-t", sessionName, "-v", option2]).catch(() => void 0);
804
+ const result2 = await runFile(this.binary, ["show-options", "-t", sessionName, "-v", option2], { signal }).catch(() => void 0);
597
805
  if (!result2) {
598
806
  if (key === "agentSessionId") continue;
599
807
  return void 0;
@@ -610,7 +818,7 @@ var TmuxController = class {
610
818
  agentSessionId: values.agentSessionId || void 0
611
819
  };
612
820
  }
613
- async capture(paneId, lines = 200) {
821
+ async capture(paneId, lines = 200, signal) {
614
822
  assertSafeTmuxTarget(paneId);
615
823
  const { stdout } = await runFile(this.binary, [
616
824
  "capture-pane",
@@ -621,10 +829,10 @@ var TmuxController = class {
621
829
  paneId,
622
830
  "-S",
623
831
  `-${Math.max(1, lines)}`
624
- ]);
832
+ ], { signal });
625
833
  return stdout;
626
834
  }
627
- async preserveOnExit(sessionName, enabled) {
835
+ async preserveOnExit(sessionName, enabled, signal) {
628
836
  await runFile(this.binary, [
629
837
  "set-option",
630
838
  "-w",
@@ -632,7 +840,7 @@ var TmuxController = class {
632
840
  `=${sessionName}:`,
633
841
  "remain-on-exit",
634
842
  enabled ? "on" : "off"
635
- ]);
843
+ ], { signal });
636
844
  }
637
845
  sendText(paneId, input, submit = true) {
638
846
  assertSafeTmuxTarget(paneId);
@@ -651,16 +859,16 @@ var TmuxController = class {
651
859
  this.writes = this.writes.then(operation, operation);
652
860
  return this.writes;
653
861
  }
654
- async sendKey(paneId, key) {
862
+ async sendKey(paneId, key, signal) {
655
863
  assertSafeTmuxTarget(paneId);
656
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)) {
657
865
  throw new Error(`unsupported tmux key: ${key}`);
658
866
  }
659
- await runFile(this.binary, ["send-keys", "-t", paneId, key]);
867
+ await runFile(this.binary, ["send-keys", "-t", paneId, key], { signal });
660
868
  }
661
- async killSession(sessionName) {
869
+ async killSession(sessionName, signal) {
662
870
  try {
663
- await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`]);
871
+ await runFile(this.binary, ["kill-session", "-t", `=${sessionName}`], { signal });
664
872
  } catch (error) {
665
873
  if (!tmuxTargetMissing(error)) throw error;
666
874
  }
@@ -690,9 +898,9 @@ function parsePane(line) {
690
898
  };
691
899
  }
692
900
  function tmuxTargetMissing(error) {
693
- const message = error instanceof Error ? error.message : String(error);
901
+ const message2 = error instanceof Error ? error.message : String(error);
694
902
  const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr) : "";
695
- return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found/i.test(`${message}
903
+ return /can't find (?:pane|session|window)|no such (?:pane|session|window)|(?:pane|session|window) not found|no server running/i.test(`${message2}
696
904
  ${stderr}`);
697
905
  }
698
906
 
@@ -1207,6 +1415,8 @@ var ActionSigner = class {
1207
1415
  value.interactionKind ?? null,
1208
1416
  value.sessionId ?? null,
1209
1417
  value.manualMode ?? null,
1418
+ value.snapshotId ?? null,
1419
+ value.page ?? null,
1210
1420
  value.agent,
1211
1421
  value.action,
1212
1422
  value.paneId,
@@ -1230,7 +1440,12 @@ function withoutSignature(value) {
1230
1440
  function isSignedAction(value) {
1231
1441
  if (!value || typeof value !== "object") return false;
1232
1442
  const item = value;
1233
- return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "session-stop" || item.kind === "session-create" || item.kind === "session-start-error" || item.kind === "startup-conflict" || item.kind === "resume-picker" || item.kind === "manual") && (item.kind !== "choice" || item.interactionKind === "approval" || item.interactionKind === "question" || item.interactionKind === "choice") && (item.kind !== "manual" || typeof item.sessionId === "string" && (item.manualMode === "explicit" || item.manualMode === "fallback")) && (item.kind !== "session-stop" && item.kind !== "session-start-error" && item.kind !== "resume-picker" && item.kind !== "startup-conflict" || typeof item.sessionId === "string") && typeof item.agent === "string" && isAgentId(item.agent) && typeof item.action === "string" && typeof item.paneId === "string" && typeof item.fingerprint === "string" && typeof item.chatId === "string" && typeof item.nonce === "string" && typeof item.expiresAt === "number" && typeof item.sig === "string";
1443
+ return item.v === 1 && (item.kind === "choice" || item.kind === "stop" || item.kind === "session" || item.kind === "session-stop" || item.kind === "session-create" || item.kind === "session-start-error" || item.kind === "startup-conflict" || item.kind === "resume-picker" || item.kind === "manual") && (item.kind !== "choice" || item.interactionKind === "approval" || item.interactionKind === "question" || item.interactionKind === "choice") && (item.kind !== "manual" || typeof item.sessionId === "string" && (item.manualMode === "explicit" || item.manualMode === "fallback")) && (item.snapshotId === void 0 || typeof item.snapshotId === "string") && (item.page === void 0 || typeof item.page === "number" && Number.isInteger(item.page) && item.page >= 0) && (item.kind !== "session-stop" && item.kind !== "session-start-error" && item.kind !== "resume-picker" && item.kind !== "startup-conflict" || typeof item.sessionId === "string") && typeof item.agent === "string" && isAgentId(item.agent) && typeof item.action === "string" && typeof item.paneId === "string" && typeof item.fingerprint === "string" && typeof item.chatId === "string" && typeof item.nonce === "string" && typeof item.expiresAt === "number" && typeof item.sig === "string";
1444
+ }
1445
+
1446
+ // src/workspace/session-create.ts
1447
+ function emptySessionCreateDraft() {
1448
+ return { agent: "codex", resumeMode: "new" };
1234
1449
  }
1235
1450
 
1236
1451
  // src/lark/cards.ts
@@ -1614,10 +1829,33 @@ var SESSION_CREATE_SUBMIT_ACTION = "session_create_submit";
1614
1829
  var SESSION_CREATE_NAME_FIELD = "session_name";
1615
1830
  var SESSION_CREATE_AGENT_FIELD = "session_agent";
1616
1831
  var SESSION_CREATE_CWD_FIELD = "session_cwd";
1832
+ var SESSION_CREATE_PROJECT_FIELD = "session_project";
1617
1833
  var SESSION_CREATE_RESUME_FIELD = "session_resume";
1618
- function sessionCreateCard() {
1834
+ var SESSION_CREATE_MANUAL_VALUE = "__manual__";
1835
+ function sessionCreateCard(view2 = {
1836
+ mode: "manual",
1837
+ page: 0,
1838
+ pageCount: 1,
1839
+ candidates: [],
1840
+ partial: false,
1841
+ warnings: [],
1842
+ draft: emptySessionCreateDraft()
1843
+ }) {
1844
+ const directoryElements = view2.mode === "projects" ? projectDirectoryFields(view2) : [{
1845
+ tag: "input",
1846
+ name: SESSION_CREATE_CWD_FIELD,
1847
+ default_value: view2.draft.manualCwd,
1848
+ placeholder: { tag: "plain_text", content: "~/workspace/project \u6216 /absolute/path" },
1849
+ label: { tag: "plain_text", content: "\u9879\u76EE\u76EE\u5F55" }
1850
+ }];
1851
+ const noticeLines = [
1852
+ ...view2.warnings.map((warning) => `\u26A0\uFE0F ${escapeMarkdown(warning)}`),
1853
+ ...view2.partial ? ["\u26A0\uFE0F \u90E8\u5206\u76EE\u5F55\u672A\u52A0\u8F7D\uFF1B\u53EF\u9009\u62E9\u5DF2\u663E\u793A\u9879\u76EE\u6216\u624B\u52A8\u586B\u5199\u8DEF\u5F84\u3002"] : []
1854
+ ];
1855
+ const notices = noticeLines.length > 0 ? [{ tag: "markdown", content: noticeLines.join("\n") }] : [];
1619
1856
  return cardElements("\u65B0\u5EFA Coding Session", [
1620
1857
  { tag: "markdown", content: "\u5728\u672C\u673A\u53D7\u7BA1 tmux \u4E2D\u542F\u52A8\u4E00\u4E2A\u65B0\u4F1A\u8BDD\uFF1B\u521B\u5EFA\u6210\u529F\u540E\u4F1A\u81EA\u52A8\u8FDE\u63A5\u3002" },
1858
+ ...notices,
1621
1859
  {
1622
1860
  tag: "form",
1623
1861
  name: "session_create_form",
@@ -1628,6 +1866,7 @@ function sessionCreateCard() {
1628
1866
  tag: "input",
1629
1867
  name: SESSION_CREATE_NAME_FIELD,
1630
1868
  required: true,
1869
+ default_value: view2.draft.sessionId,
1631
1870
  placeholder: { tag: "plain_text", content: "Session \u540D\u79F0\uFF0C\u4F8B\u5982 helix" },
1632
1871
  label: { tag: "plain_text", content: "Session \u540D\u79F0" }
1633
1872
  },
@@ -1636,24 +1875,18 @@ function sessionCreateCard() {
1636
1875
  name: SESSION_CREATE_AGENT_FIELD,
1637
1876
  required: true,
1638
1877
  placeholder: { tag: "plain_text", content: "\u9009\u62E9 Agent" },
1639
- initial_option: "codex",
1878
+ initial_option: view2.draft.agent,
1640
1879
  options: listAgentAdapters().map((adapter) => ({
1641
1880
  text: { tag: "plain_text", content: adapter.displayName },
1642
1881
  value: adapter.id
1643
1882
  }))
1644
1883
  },
1645
- {
1646
- tag: "input",
1647
- name: SESSION_CREATE_CWD_FIELD,
1648
- required: true,
1649
- placeholder: { tag: "plain_text", content: "/absolute/path/to/project" },
1650
- label: { tag: "plain_text", content: "\u5DE5\u4F5C\u76EE\u5F55\uFF08\u5FC5\u987B\u4E3A\u7EDD\u5BF9\u8DEF\u5F84\uFF09" }
1651
- },
1884
+ ...directoryElements,
1652
1885
  {
1653
1886
  tag: "select_static",
1654
1887
  name: SESSION_CREATE_RESUME_FIELD,
1655
1888
  placeholder: { tag: "plain_text", content: "\u9009\u62E9\u542F\u52A8\u65B9\u5F0F" },
1656
- initial_option: "new",
1889
+ initial_option: view2.draft.resumeMode,
1657
1890
  options: [
1658
1891
  { text: { tag: "plain_text", content: "\u65B0\u4F1A\u8BDD" }, value: "new" },
1659
1892
  { text: { tag: "plain_text", content: "\u6253\u5F00\u539F\u751F Resume Picker" }, value: "picker" }
@@ -1672,6 +1905,26 @@ function sessionCreateCard() {
1672
1905
  }
1673
1906
  ]);
1674
1907
  }
1908
+ function projectDirectoryFields(view2) {
1909
+ const options = [...view2.candidates.map((candidate) => ({
1910
+ text: { tag: "plain_text", content: candidate.label },
1911
+ value: candidate.cwd
1912
+ })), { text: { tag: "plain_text", content: "\u624B\u52A8\u586B\u5199\u5176\u4ED6\u8DEF\u5F84\u2026" }, value: SESSION_CREATE_MANUAL_VALUE }];
1913
+ return [{
1914
+ tag: "select_static",
1915
+ name: SESSION_CREATE_PROJECT_FIELD,
1916
+ placeholder: { tag: "plain_text", content: "\u9009\u62E9\u9879\u76EE\u76EE\u5F55" },
1917
+ initial_option: view2.draft.projectCwd,
1918
+ required: true,
1919
+ options
1920
+ }, {
1921
+ tag: "input",
1922
+ name: SESSION_CREATE_CWD_FIELD,
1923
+ default_value: view2.draft.manualCwd,
1924
+ placeholder: { tag: "plain_text", content: "\u9009\u62E9\u201C\u624B\u52A8\u586B\u5199\u5176\u4ED6\u8DEF\u5F84\u2026\u201D\u65F6\u586B\u5199" },
1925
+ label: { tag: "plain_text", content: "\u5176\u4ED6\u8DEF\u5F84\uFF08\u53EF\u9009\uFF09" }
1926
+ }];
1927
+ }
1675
1928
  function sessionCreateResultCard(success, content, session) {
1676
1929
  const details = session ? `
1677
1930
 
@@ -1702,6 +1955,25 @@ function sessionCreateFailureCard(chatId, content, signer) {
1702
1955
  ], "red");
1703
1956
  }
1704
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
+ }
1705
1977
  const exitStatus = failure.exitStatus === void 0 ? "\u672A\u77E5" : String(failure.exitStatus);
1706
1978
  return cardElements("Session \u542F\u52A8\u5931\u8D25", [
1707
1979
  {
@@ -2012,7 +2284,7 @@ var LarkGateway = class {
2012
2284
  httpTimeoutMs: 3e4,
2013
2285
  respectProxyEnv: true
2014
2286
  });
2015
- this.channel.on("message", async (message) => this.handler.onMessage(message));
2287
+ this.channel.on("message", async (message2) => this.handler.onMessage(message2));
2016
2288
  this.channel.on("cardAction", async (event) => {
2017
2289
  console.error(`[lca] card action received: tag=${event.action.tag ?? "unknown"} name=${event.action.name ?? "-"} message=${event.messageId}`);
2018
2290
  const mappedFormAction = event.action.formValue && event.action.name ? this.formActions.get(event.messageId)?.get(event.action.name) : void 0;
@@ -2038,10 +2310,10 @@ var LarkGateway = class {
2038
2310
  const result2 = await this.handler.onAction(event, action);
2039
2311
  if (result2.type === "error") return { toast: result2 };
2040
2312
  if (result2.type === "session-create-form") {
2041
- this.rememberSessionCreateFormAction(event.messageId, event.chatId);
2313
+ this.rememberSessionCreateFormActions(event.messageId, event.chatId, result2.view);
2042
2314
  return {
2043
2315
  toast: { type: "success", content: result2.content },
2044
- card: { type: "raw", data: sessionCreateCard() }
2316
+ card: { type: "raw", data: sessionCreateCard(result2.view) }
2045
2317
  };
2046
2318
  }
2047
2319
  if (result2.type === "manual") {
@@ -2179,8 +2451,13 @@ var LarkGateway = class {
2179
2451
  return;
2180
2452
  }
2181
2453
  if (result2.type === "session-create-form") {
2182
- const sent = await this.sendMessage(event.chatId, { card: sessionCreateCard() });
2183
- this.rememberSessionCreateFormAction(sent.messageId, event.chatId);
2454
+ if (action.kind === "session-create" && action.action !== "open") {
2455
+ await this.updateCardAfterAction(event.messageId, sessionCreateCard(result2.view));
2456
+ this.rememberSessionCreateFormActions(event.messageId, event.chatId, result2.view);
2457
+ return;
2458
+ }
2459
+ const sent = await this.sendMessage(event.chatId, { card: sessionCreateCard(result2.view) });
2460
+ this.rememberSessionCreateFormActions(sent.messageId, event.chatId, result2.view);
2184
2461
  const sourceCard = action.kind === "session-create" && result2.sessions ? sessionPickerCard(event.chatId, result2.sessions, result2.activeSessionId, this.signer, void 0, false) : sessionCreateOpenedCard();
2185
2462
  await this.updateCardAfterAction(event.messageId, sourceCard).catch(async (error) => {
2186
2463
  const detail = cardErrorDetail(error);
@@ -2227,7 +2504,7 @@ var LarkGateway = class {
2227
2504
  const retryDelays = [0, 300, 800, 1600];
2228
2505
  let lastError;
2229
2506
  for (const delay of retryDelays) {
2230
- if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
2507
+ if (delay > 0) await new Promise((resolve2) => setTimeout(resolve2, delay));
2231
2508
  try {
2232
2509
  await this.channel.updateCard(messageId, card2);
2233
2510
  return;
@@ -2257,22 +2534,22 @@ var LarkGateway = class {
2257
2534
  await Promise.all([...chats].map((chatId) => this.clearProcessing(chatId)));
2258
2535
  await this.channel.disconnect();
2259
2536
  }
2260
- async startProcessing(message) {
2261
- await this.clearProcessing(message.chatId);
2262
- 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;
2263
2540
  try {
2264
- const reactionId = await this.channel.addReaction(message.messageId, "Typing");
2265
- if (this.processingGenerations.get(message.chatId) !== generation) {
2266
- 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);
2267
2544
  return;
2268
2545
  }
2269
2546
  const timer = setTimeout(() => {
2270
- void this.clearProcessing(message.chatId, generation);
2547
+ void this.clearProcessing(message2.chatId, generation);
2271
2548
  }, 10 * 6e4);
2272
2549
  timer.unref?.();
2273
- this.processingReactions.set(message.chatId, { messageId: message.messageId, reactionId, timer, generation });
2550
+ this.processingReactions.set(message2.chatId, { messageId: message2.messageId, reactionId, timer, generation });
2274
2551
  } catch (error) {
2275
- 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)}`);
2276
2553
  }
2277
2554
  }
2278
2555
  sendText(chatId, text) {
@@ -2318,18 +2595,19 @@ var LarkGateway = class {
2318
2595
  [MANUAL_SUBMIT_ACTION, this.signer.sign({ ...common, action: MANUAL_SUBMIT_ACTION }, 10 * 6e4)]
2319
2596
  ]));
2320
2597
  }
2321
- rememberSessionCreateFormAction(messageId, chatId) {
2322
- this.formActions.set(messageId, /* @__PURE__ */ new Map([[
2323
- SESSION_CREATE_SUBMIT_ACTION,
2324
- this.signer.sign({
2325
- kind: "session-create",
2326
- agent: "codex",
2327
- action: "submit",
2328
- paneId: "",
2329
- fingerprint: "create",
2330
- chatId
2331
- }, 10 * 6e4)
2332
- ]]));
2598
+ rememberSessionCreateFormActions(messageId, chatId, view2) {
2599
+ const actions = /* @__PURE__ */ new Map();
2600
+ actions.set(SESSION_CREATE_SUBMIT_ACTION, this.signer.sign({
2601
+ kind: "session-create",
2602
+ agent: view2.draft.agent,
2603
+ action: "submit",
2604
+ paneId: "",
2605
+ fingerprint: view2.mode,
2606
+ chatId,
2607
+ snapshotId: view2.snapshotId,
2608
+ page: view2.page
2609
+ }, 10 * 6e4));
2610
+ this.formActions.set(messageId, actions);
2333
2611
  }
2334
2612
  rememberFormActions(messageId, chatId, paneId, screen, agent) {
2335
2613
  if (screen.interaction?.semantics?.activation !== "toggle") {
@@ -2365,9 +2643,9 @@ var LarkGateway = class {
2365
2643
  sendStartupConflict(chatId, request, owner) {
2366
2644
  return this.sendMessage(chatId, { card: startupConflictCard(chatId, requestSession(request), owner, this.signer) });
2367
2645
  }
2368
- async sendSessionCreate(chatId) {
2369
- const result2 = await this.sendMessage(chatId, { card: sessionCreateCard() });
2370
- this.rememberSessionCreateFormAction(result2.messageId, chatId);
2646
+ async sendSessionCreate(chatId, view2) {
2647
+ const result2 = await this.sendMessage(chatId, { card: sessionCreateCard(view2) });
2648
+ this.rememberSessionCreateFormActions(result2.messageId, chatId, view2);
2371
2649
  return result2;
2372
2650
  }
2373
2651
  sendSessionStartupFailure(chatId, failure) {
@@ -2434,8 +2712,8 @@ function cardErrorDetail(error) {
2434
2712
  const data = response && typeof response === "object" ? response.data : void 0;
2435
2713
  const record = data && typeof data === "object" ? data : void 0;
2436
2714
  const code = typeof record?.code === "number" || typeof record?.code === "string" ? String(record.code) : void 0;
2437
- const message = typeof record?.msg === "string" ? record.msg : error instanceof Error ? error.message : String(error);
2438
- 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);
2439
2717
  return code ? `code=${code} ${sanitized}` : sanitized;
2440
2718
  }
2441
2719
 
@@ -2640,9 +2918,155 @@ function redactSecrets(value) {
2640
2918
  ).replace(/([?&](?:api[_-]?key|access[_-]?token|token|secret|signature)=)[^&#\s]+/gi, "$1[REDACTED]");
2641
2919
  }
2642
2920
 
2921
+ // src/workspace/discovery.ts
2922
+ import { lstat, readdir as readdir2, stat as stat2 } from "fs/promises";
2923
+ import { homedir as homedir3 } from "os";
2924
+ import { basename, dirname as dirname2, join as join2, relative } from "path";
2925
+ async function discoverWorkspaces(options) {
2926
+ const maxDepth = options.maxDepth ?? 1;
2927
+ const maxDirectories = options.maxDirectories ?? 500;
2928
+ const timeBudgetMs = options.timeBudgetMs ?? 2e3;
2929
+ const now = options.now ?? Date.now;
2930
+ const home = options.home ?? homedir3();
2931
+ const startedAt = now();
2932
+ const warnings = [];
2933
+ const byPath = /* @__PURE__ */ new Map();
2934
+ let visitedDirectories = 0;
2935
+ let partial = false;
2936
+ const budgetAvailable = () => {
2937
+ const available = visitedDirectories < maxDirectories && now() - startedAt <= timeBudgetMs;
2938
+ if (!available) partial = true;
2939
+ return available;
2940
+ };
2941
+ const add = async (input, source) => {
2942
+ if (!budgetAvailable()) return;
2943
+ let cwd;
2944
+ try {
2945
+ cwd = normalizeWorkspacePath(input, home);
2946
+ } catch {
2947
+ return;
2948
+ }
2949
+ if (byPath.has(cwd)) return;
2950
+ visitedDirectories += 1;
2951
+ try {
2952
+ const info = await lstat(cwd);
2953
+ if (!info.isDirectory() || info.isSymbolicLink()) return;
2954
+ byPath.set(cwd, { cwd, label: workspaceLabel(cwd, home), source, git: await hasGitEntry(cwd) });
2955
+ } catch {
2956
+ if (source === "configured") warnings.push(`\u65E0\u6CD5\u8BFB\u53D6 workspace\uFF1A${cwd}`);
2957
+ }
2958
+ };
2959
+ for (const session of options.activeSessions) await add(session.cwd, "active");
2960
+ for (const recent of [...options.recentWorkspaces].sort((a, b) => b.lastUsedAt - a.lastUsedAt)) {
2961
+ await add(recent.cwd, "recent");
2962
+ }
2963
+ for (const rootInput of options.workspaceRoots) {
2964
+ if (!budgetAvailable()) break;
2965
+ let root;
2966
+ try {
2967
+ root = normalizeWorkspacePath(rootInput, home);
2968
+ const info = await lstat(root);
2969
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("not a directory");
2970
+ } catch {
2971
+ warnings.push(`\u65E0\u6CD5\u8BFB\u53D6 workspace\uFF1A${rootInput}`);
2972
+ continue;
2973
+ }
2974
+ await add(root, "configured");
2975
+ let frontier = [root];
2976
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0 && budgetAvailable(); depth += 1) {
2977
+ const next = [];
2978
+ for (const parent of frontier) {
2979
+ if (!budgetAvailable()) break;
2980
+ let entries;
2981
+ try {
2982
+ entries = await readdir2(parent, { withFileTypes: true });
2983
+ } catch {
2984
+ warnings.push(`\u65E0\u6CD5\u8BFB\u53D6\u76EE\u5F55\uFF1A${parent}`);
2985
+ continue;
2986
+ }
2987
+ for (const entry of entries) {
2988
+ if (!budgetAvailable()) break;
2989
+ if (!entry.isDirectory() || entry.isSymbolicLink() || entry.name.startsWith(".")) continue;
2990
+ const child = join2(parent, entry.name);
2991
+ await add(child, "configured");
2992
+ if (depth < maxDepth) next.push(child);
2993
+ }
2994
+ }
2995
+ frontier = next;
2996
+ }
2997
+ }
2998
+ const priority = { active: 0, recent: 1, configured: 2 };
2999
+ const candidates = [...byPath.values()].sort(
3000
+ (left, right) => priority[left.source] - priority[right.source] || Number(right.git) - Number(left.git) || left.label.localeCompare(right.label, "zh-CN")
3001
+ );
3002
+ return { candidates, partial, warnings: [...new Set(warnings)], visitedDirectories };
3003
+ }
3004
+ function workspaceLabel(cwd, home = homedir3()) {
3005
+ const parent = dirname2(cwd);
3006
+ const rel = relative(home, parent);
3007
+ const displayParent = rel === "" ? "~" : !rel.startsWith("..") ? `~/${rel}` : parent;
3008
+ return `${basename(cwd) || cwd} \xB7 ${displayParent}`;
3009
+ }
3010
+ async function hasGitEntry(cwd) {
3011
+ return stat2(join2(cwd, ".git")).then(() => true).catch(() => false);
3012
+ }
3013
+
3014
+ // src/workspace/recent.ts
3015
+ function rememberRecentWorkspace(existing, cwd, now = Date.now(), limit = 30) {
3016
+ const normalized = normalizeWorkspacePath(cwd);
3017
+ return [
3018
+ { cwd: normalized, lastUsedAt: now },
3019
+ ...(existing ?? []).filter((item) => normalizeWorkspacePath(item.cwd) !== normalized)
3020
+ ].slice(0, limit);
3021
+ }
3022
+
3023
+ // src/workspace/snapshot.ts
3024
+ import { randomBytes as randomBytes4 } from "crypto";
3025
+ var WorkspaceSnapshotStore = class {
3026
+ constructor(ttlMs = 10 * 6e4, capacity = 64, now = Date.now, createId = () => randomBytes4(12).toString("base64url")) {
3027
+ this.ttlMs = ttlMs;
3028
+ this.capacity = capacity;
3029
+ this.now = now;
3030
+ this.createId = createId;
3031
+ }
3032
+ ttlMs;
3033
+ capacity;
3034
+ now;
3035
+ createId;
3036
+ values = /* @__PURE__ */ new Map();
3037
+ create(input) {
3038
+ const now = this.now();
3039
+ this.prune(now);
3040
+ const snapshot = {
3041
+ ...input,
3042
+ id: this.createId(),
3043
+ createdAt: now,
3044
+ expiresAt: now + this.ttlMs
3045
+ };
3046
+ this.values.set(snapshot.id, snapshot);
3047
+ while (this.values.size > this.capacity) {
3048
+ const oldest = this.values.keys().next().value;
3049
+ if (!oldest) break;
3050
+ this.values.delete(oldest);
3051
+ }
3052
+ return snapshot;
3053
+ }
3054
+ get(id, chatId, ownerOpenId) {
3055
+ const now = this.now();
3056
+ this.prune(now);
3057
+ const value = this.values.get(id);
3058
+ return value?.chatId === chatId && value.ownerOpenId === ownerOpenId ? value : void 0;
3059
+ }
3060
+ prune(now) {
3061
+ for (const [id, value] of this.values) {
3062
+ if (value.expiresAt <= now) this.values.delete(id);
3063
+ }
3064
+ }
3065
+ };
3066
+
2643
3067
  // src/daemon/server.ts
2644
3068
  var AssistantDaemon = class {
2645
- 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) {
2646
3070
  this.store = store;
2647
3071
  this.paths = paths2;
2648
3072
  this.gatewayFactory = gatewayFactory;
@@ -2650,6 +3074,7 @@ var AssistantDaemon = class {
2650
3074
  this.stopHookCommand = stopHookCommand;
2651
3075
  this.completionQuietMs = completionQuietMs;
2652
3076
  this.appVersion = appVersion;
3077
+ this.sessionStarts = new SessionStartCoordinator(sessionStartTimeoutMs, (message2) => this.log(message2));
2653
3078
  }
2654
3079
  store;
2655
3080
  paths;
@@ -2681,6 +3106,9 @@ var AssistantDaemon = class {
2681
3106
  pendingAgentSessionClaims = /* @__PURE__ */ new Map();
2682
3107
  pendingResumePickers = /* @__PURE__ */ new Map();
2683
3108
  pendingStartupConflicts = /* @__PURE__ */ new Map();
3109
+ workspaceSnapshots = new WorkspaceSnapshotStore();
3110
+ sessionStarts;
3111
+ startStateWrites = Promise.resolve();
2684
3112
  pendingInteractionInput;
2685
3113
  attachAttempts = /* @__PURE__ */ new Map();
2686
3114
  server = createServer((socket) => this.handleSocket(socket));
@@ -2695,10 +3123,10 @@ var AssistantDaemon = class {
2695
3123
  this.tmux,
2696
3124
  this.sessionName,
2697
3125
  3,
2698
- (message) => this.log(message),
2699
- async (agent) => {
3126
+ (message2) => this.log(message2),
3127
+ async (agent, signal) => {
2700
3128
  const adapter = getAgentAdapter(agent);
2701
- return (await runFile(adapter.binary(this.config), [...adapter.versionArgs])).stdout.trim();
3129
+ return (await runFile(adapter.binary(this.config), [...adapter.versionArgs], { signal })).stdout.trim();
2702
3130
  }
2703
3131
  );
2704
3132
  const secrets = await this.store.loadSecrets();
@@ -2707,13 +3135,13 @@ var AssistantDaemon = class {
2707
3135
  try {
2708
3136
  await this.reconcileSessions(true);
2709
3137
  await rm(this.paths.socket, { force: true });
2710
- await new Promise((resolve, reject) => {
3138
+ await new Promise((resolve2, reject) => {
2711
3139
  this.server.once("error", reject);
2712
- this.server.listen(this.paths.socket, () => resolve());
3140
+ this.server.listen(this.paths.socket, () => resolve2());
2713
3141
  });
2714
3142
  await chmod3(this.paths.socket, 384);
2715
3143
  this.gateway = this.gatewayFactory(config, secrets, {
2716
- onMessage: (message) => this.onLarkMessage(message),
3144
+ onMessage: (message2) => this.onLarkMessage(message2),
2717
3145
  onAction: (event, action) => this.onLarkAction(event, action),
2718
3146
  onResumePickerDeliveryFailure: (session) => this.handleResumePickerDeliveryFailure(session)
2719
3147
  });
@@ -2732,7 +3160,7 @@ var AssistantDaemon = class {
2732
3160
  if (this.timer) clearTimeout(this.timer);
2733
3161
  await this.pollInFlight?.catch(() => void 0);
2734
3162
  await this.gateway?.disconnect().catch(() => void 0);
2735
- await new Promise((resolve) => this.server.close(() => resolve()));
3163
+ await new Promise((resolve2) => this.server.close(() => resolve2()));
2736
3164
  await this.releaseRuntimeFiles();
2737
3165
  }
2738
3166
  async acquireRuntimeFiles() {
@@ -2817,13 +3245,36 @@ var AssistantDaemon = class {
2817
3245
  return this.handleTurnComplete(request.candidate);
2818
3246
  }
2819
3247
  }
2820
- async startSession(sessionId, cwd, agentId, resume) {
3248
+ async startSession(sessionId, cwd, agentId, resume, source = "cli") {
2821
3249
  try {
2822
- await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
3250
+ const validated = await validateStartSessionRequest({ sessionId, cwd, agent: agentId, resume });
3251
+ cwd = validated.cwd;
2823
3252
  } catch (error) {
2824
3253
  return fail(error);
2825
3254
  }
2826
- 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));
2827
3278
  const existing = this.state.sessions?.[sessionId];
2828
3279
  if (existing) {
2829
3280
  return fail(new AppError(
@@ -2843,14 +3294,17 @@ var AssistantDaemon = class {
2843
3294
  const binary = adapter.binary(this.config);
2844
3295
  let agentVersion;
2845
3296
  try {
2846
- 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();
2847
3301
  } catch (error) {
2848
- 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 }));
2849
3303
  }
2850
3304
  const tmuxSessionName = `${this.sessionName}-${sessionId}`;
2851
3305
  let pane;
2852
3306
  try {
2853
- pane = await this.tmux.create({
3307
+ pane = await context.stage("tmux-create", () => this.tmux.create({
2854
3308
  sessionName: tmuxSessionName,
2855
3309
  cwd,
2856
3310
  binary,
@@ -2863,8 +3317,10 @@ var AssistantDaemon = class {
2863
3317
  LARK_CODING_ASSISTANT_SESSION_ID: sessionId,
2864
3318
  LARK_CODING_ASSISTANT_AGENT: agentId
2865
3319
  },
2866
- preserveOnExit: true
2867
- });
3320
+ preserveOnExit: true,
3321
+ signal: context.signal
3322
+ }));
3323
+ created(pane.sessionName);
2868
3324
  } catch (error) {
2869
3325
  return fail(isAppError(error) ? error : new AppError("START_FAILED", "failed to create tmux session", { sessionId }, { cause: error }));
2870
3326
  }
@@ -2879,80 +3335,128 @@ var AssistantDaemon = class {
2879
3335
  updatedAt: Date.now()
2880
3336
  };
2881
3337
  const pendingClaim = this.pendingAgentSessionClaims.get(sessionId);
2882
- if (pendingClaim?.agent === agentId) {
2883
- const owner = this.findAgentSessionOwner(agentId, pendingClaim.agentSessionId, sessionId);
2884
- if (owner) {
2885
- this.pendingAgentSessionClaims.delete(sessionId);
2886
- await this.tmux.killSession(pane.sessionName).catch(() => void 0);
2887
- return fail(agentSessionInUse(sessionId, owner.id));
2888
- }
2889
- session.agentSessionId = pendingClaim.agentSessionId;
2890
- this.pendingAgentSessionClaims.delete(sessionId);
2891
- }
2892
- try {
2893
- await this.tmux.writeMetadata(pane.sessionName, {
2894
- managed: true,
2895
- sessionId,
2896
- agent: agentId,
2897
- cwd,
2898
- agentVersion,
2899
- agentSessionId: session.agentSessionId
2900
- });
2901
- } catch (error) {
2902
- await this.tmux.killSession(pane.sessionName).catch((cleanupError) => this.log(
2903
- `failed to clean session ${sessionId} after metadata error: ${errorMessage3(cleanupError)}`
2904
- ));
2905
- return fail(new AppError("START_FAILED", "failed to persist tmux session metadata", { sessionId }, { cause: error }));
2906
- }
2907
- const nextState = {
2908
- ...this.state,
2909
- sessions: { ...this.state.sessions, [sessionId]: session },
2910
- activeSessionId: this.state.activeSessionId ?? sessionId,
2911
- boundChatId: binding.mode === "reused" ? this.state.boundChatId : void 0,
2912
- bindCodeHash: binding.mode === "code" ? hashBindCode(binding.bindCode) : void 0,
2913
- bindCodeExpiresAt: binding.mode === "code" ? Date.now() + 10 * 6e4 : void 0,
2914
- updatedAt: Date.now()
2915
- };
2916
- try {
2917
- await this.store.saveState(nextState);
2918
- } catch (error) {
2919
- await this.tmux.killSession(pane.sessionName).catch((cleanupError) => this.log(
2920
- `failed to clean session ${sessionId} after state error: ${errorMessage3(cleanupError)}`
2921
- ));
2922
- return fail(new AppError("START_FAILED", "failed to persist session state", { sessionId }, { cause: error }));
2923
- }
2924
- this.state = nextState;
2925
- const lateClaim = this.pendingAgentSessionClaims.get(sessionId);
2926
- if (lateClaim) {
2927
- const claimed = await this.handleAgentSessionStarted(lateClaim);
2928
- if (!claimed.ok) {
2929
- await this.stopSession(sessionId).catch(() => void 0);
2930
- return claimed;
2931
- }
3338
+ if (pendingClaim) {
3339
+ const claimed = this.claimStartingAgentSession(session, pendingClaim);
3340
+ if (!claimed.ok) return claimed;
2932
3341
  }
2933
3342
  if (resume && resume.mode !== "picker") {
2934
- 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
+ );
2935
3347
  if (!initialClaim.ok) {
2936
- await this.stopSession(sessionId).catch(() => void 0);
2937
3348
  return initialClaim;
2938
3349
  }
2939
- } else if (!resume) {
2940
- 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
+ );
2941
3355
  if (!stable.ok) {
2942
- await this.stopSession(sessionId).catch(() => void 0);
2943
3356
  return stable;
2944
3357
  }
2945
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;
2946
3374
  if (resume?.mode !== "picker") {
2947
- 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(
2948
3376
  `failed to disable startup preservation for ${sessionId}: ${errorMessage3(error)}`
2949
3377
  ));
3378
+ await this.rememberSessionWorkspace(session.cwd);
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;
2950
3402
  }
2951
3403
  await this.log(
2952
3404
  `session created: session=${session.id} agent=${session.agent} pane=${session.paneId} active=${this.state.activeSessionId === session.id}`
2953
3405
  );
2954
- await this.poll().catch((error) => this.log(`initial poll failed for ${sessionId}: ${errorMessage3(error)}`));
2955
- 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;
2956
3460
  }
2957
3461
  createSessionBinding() {
2958
3462
  if (this.state.ownerOpenId && this.state.boundChatId && !this.state.autoBindDisabled) {
@@ -3015,26 +3519,26 @@ var AssistantDaemon = class {
3015
3519
  await this.tmux.sendText(session.paneId, text);
3016
3520
  return { ok: true };
3017
3521
  }
3018
- async onLarkMessage(message) {
3019
- if (message.chatType !== "p2p" || message.senderIsBot || message.senderType === "bot") return;
3020
- 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();
3021
3525
  const attachCode = text.match(/^\/attach\s+([^\s]+)\s*$/)?.[1];
3022
3526
  if (!this.state.boundChatId) {
3023
- if (this.canAutoBind(message)) {
3024
- await this.bindChat(message, true);
3527
+ if (this.canAutoBind(message2)) {
3528
+ await this.bindChat(message2, true);
3025
3529
  } else {
3026
3530
  if (!attachCode) return;
3027
- await this.handleAttach(message, attachCode);
3531
+ await this.handleAttach(message2, attachCode);
3028
3532
  return;
3029
3533
  }
3030
3534
  }
3031
- 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;
3032
3536
  if (text === "/start") {
3033
3537
  try {
3034
- await this.gateway?.sendSessionCreate(message.chatId);
3538
+ await this.gateway?.sendSessionCreate(message2.chatId, await this.createSessionWorkspaceView(message2.chatId));
3035
3539
  } catch (error) {
3036
3540
  await this.log(`session create card failed: ${errorMessage3(error)}`);
3037
- 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");
3038
3542
  }
3039
3543
  return;
3040
3544
  }
@@ -3043,37 +3547,37 @@ var AssistantDaemon = class {
3043
3547
  try {
3044
3548
  request = parseStartCommand(text);
3045
3549
  } catch (error) {
3046
- await this.gateway?.sendText(message.chatId, remoteError(fail(error)));
3550
+ await this.gateway?.sendText(message2.chatId, remoteError(fail(error)));
3047
3551
  return;
3048
3552
  }
3049
3553
  const result3 = await this.startRemoteSession(request);
3050
3554
  if (!result3.ok) {
3051
3555
  const failure = sessionStartupFailure(result3.error, request);
3052
- if (failure) await this.gateway?.sendSessionStartupFailure(message.chatId, failure);
3053
- 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));
3054
3558
  } else if (result3.state === "picker") {
3055
3559
  try {
3056
- await this.gateway?.sendResumePicker(message.chatId, result3.session, result3.picker);
3560
+ await this.gateway?.sendResumePicker(message2.chatId, result3.session, result3.picker);
3057
3561
  } catch (error) {
3058
3562
  await this.handleResumePickerDeliveryFailure(result3.session);
3059
3563
  await this.log(`resume picker notification failed: session=${result3.session.id} error=${errorMessage3(error)}`);
3060
- 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");
3061
3565
  }
3062
- } else if (result3.state === "conflict") await this.gateway?.sendStartupConflict(message.chatId, result3.request, result3.owner);
3063
- 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));
3064
3568
  return;
3065
3569
  }
3066
3570
  const tailMatch = text.match(/^\/tail(?:\s+(\d+))?$/);
3067
3571
  if (tailMatch) {
3068
3572
  const lines = tailMatch[1] ? Number(tailMatch[1]) : 80;
3069
3573
  if (!Number.isInteger(lines) || lines < 20 || lines > 300) {
3070
- 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]");
3071
3575
  return;
3072
3576
  }
3073
3577
  const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorMessage3(error)}`);
3074
3578
  const session = this.activeSession();
3075
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**";
3076
- await this.gateway?.sendMarkdown(message.chatId, `${metadata}
3580
+ await this.gateway?.sendMarkdown(message2.chatId, `${metadata}
3077
3581
 
3078
3582
  \`\`\`text
3079
3583
  ${escapeFence2(output).slice(-6800)}
@@ -3081,20 +3585,20 @@ ${escapeFence2(output).slice(-6800)}
3081
3585
  return;
3082
3586
  }
3083
3587
  if (text.startsWith("/tail")) {
3084
- 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]");
3085
3589
  return;
3086
3590
  }
3087
3591
  if (text === "/manual") {
3088
3592
  await this.poll();
3089
3593
  const view2 = this.currentManualView();
3090
- 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");
3091
3595
  else {
3092
3596
  try {
3093
- await this.gateway?.sendManual(message.chatId, view2);
3597
+ await this.gateway?.sendManual(message2.chatId, view2);
3094
3598
  } catch (error) {
3095
3599
  await this.log(`manual card failed: ${errorMessage3(error)}`);
3096
3600
  await this.gateway?.sendText(
3097
- message.chatId,
3601
+ message2.chatId,
3098
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"
3099
3603
  );
3100
3604
  }
@@ -3103,44 +3607,44 @@ ${escapeFence2(output).slice(-6800)}
3103
3607
  }
3104
3608
  const manualKey = text.match(/^\/key\s+(up|down|left|right|enter|esc|tab|space|backspace|ctrl-c)$/i)?.[1];
3105
3609
  if (manualKey) {
3106
- await this.executeManualCommand(message.chatId, `\u6309\u952E ${manualKey}`, async (session) => {
3610
+ await this.executeManualCommand(message2.chatId, `\u6309\u952E ${manualKey}`, async (session) => {
3107
3611
  await this.tmux.sendKey(session.paneId, manualTmuxKey(manualKey));
3108
3612
  });
3109
3613
  return;
3110
3614
  }
3111
3615
  if (text.startsWith("/key")) {
3112
- 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");
3113
3617
  return;
3114
3618
  }
3115
3619
  const typeMatch = text.match(/^\/(type|submit)\s+([\s\S]+)$/);
3116
3620
  if (typeMatch?.[1] && typeMatch[2]?.trim()) {
3117
3621
  const submit = typeMatch[1] === "submit";
3118
- 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) => {
3119
3623
  await this.tmux.sendText(session.paneId, typeMatch[2], submit);
3120
3624
  });
3121
3625
  return;
3122
3626
  }
3123
3627
  if (text === "/type" || text === "/submit") {
3124
- 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>`);
3125
3629
  return;
3126
3630
  }
3127
3631
  if (text === "/status") {
3128
3632
  const status = await this.runtimeStatus();
3129
- await this.gateway?.sendStatus(message.chatId, status);
3633
+ await this.gateway?.sendStatus(message2.chatId, status);
3130
3634
  return;
3131
3635
  }
3132
3636
  if (text === "/sessions") {
3133
3637
  const sessions = await this.reconcileSessions(true);
3134
3638
  try {
3135
3639
  await this.gateway?.sendSessionPicker(
3136
- message.chatId,
3640
+ message2.chatId,
3137
3641
  sessions,
3138
3642
  this.state.activeSessionId
3139
3643
  );
3140
3644
  } catch (error) {
3141
3645
  await this.log(`session picker notification failed: ${errorMessage3(error)}`);
3142
3646
  await this.gateway?.sendText(
3143
- message.chatId,
3647
+ message2.chatId,
3144
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"
3145
3649
  );
3146
3650
  }
@@ -3152,7 +3656,7 @@ ${escapeFence2(output).slice(-6800)}
3152
3656
  const target = this.state.sessions?.[useSessionId];
3153
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}`;
3154
3658
  await this.gateway?.sendText(
3155
- message.chatId,
3659
+ message2.chatId,
3156
3660
  reply
3157
3661
  );
3158
3662
  return;
@@ -3169,16 +3673,16 @@ ${escapeFence2(output).slice(-6800)}
3169
3673
  updatedAt: Date.now()
3170
3674
  };
3171
3675
  await this.store.saveState(this.state);
3172
- 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");
3173
3677
  return;
3174
3678
  }
3175
3679
  if (text === "/stop") {
3176
3680
  await this.poll();
3177
3681
  const session = this.activeSession();
3178
3682
  if (!session || !this.screen) {
3179
- 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");
3180
3684
  } else {
3181
- 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);
3182
3686
  }
3183
3687
  return;
3184
3688
  }
@@ -3187,15 +3691,15 @@ ${escapeFence2(output).slice(-6800)}
3187
3691
  const session = this.activeSession();
3188
3692
  if (!session || session.id !== pendingInput.sessionId) {
3189
3693
  this.pendingInteractionInput = void 0;
3190
- 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");
3191
3695
  return;
3192
3696
  }
3193
- await this.tmux.sendText(session.paneId, message.content, pendingInput.submitOnInput);
3697
+ await this.tmux.sendText(session.paneId, message2.content, pendingInput.submitOnInput);
3194
3698
  this.pendingInteractionInput = void 0;
3195
3699
  if (pendingInput.submitOnInput) {
3196
3700
  await this.waitForInteractionChange(pendingInput.interactionId);
3197
3701
  if (this.screen?.interaction?.interactionId === pendingInput.interactionId) {
3198
- 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");
3199
3703
  return;
3200
3704
  }
3201
3705
  const content = `\u5DF2\u5411 ${getAgentAdapter(session.agent).displayName} \u63D0\u4EA4\u8865\u5145\u5185\u5BB9\u3002`;
@@ -3205,12 +3709,12 @@ ${escapeFence2(output).slice(-6800)}
3205
3709
  }
3206
3710
  if (pendingInput.role === "custom-input") {
3207
3711
  const refreshed = await this.withInteractionNotificationsSuppressed(async () => {
3208
- await this.waitForCustomInputValue(pendingInput.interactionId, pendingInput.controlId, message.content);
3712
+ await this.waitForCustomInputValue(pendingInput.interactionId, pendingInput.controlId, message2.content);
3209
3713
  const current = this.screen;
3210
3714
  if (current?.interaction?.semantics && (current.interaction.actionConfidence ?? 0) >= 0.85) {
3211
3715
  await this.gateway?.updateChoice(
3212
3716
  pendingInput.cardMessageId,
3213
- message.chatId,
3717
+ message2.chatId,
3214
3718
  session.paneId,
3215
3719
  current,
3216
3720
  session.agent
@@ -3221,43 +3725,43 @@ ${escapeFence2(output).slice(-6800)}
3221
3725
  });
3222
3726
  if (refreshed) return;
3223
3727
  }
3224
- 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`);
3225
3729
  return;
3226
3730
  }
3227
- await this.gateway?.startProcessing(message);
3731
+ await this.gateway?.startProcessing(message2);
3228
3732
  await this.poll();
3229
3733
  if (this.shouldQueueMessage()) {
3230
3734
  if (this.pendingMessages.length >= 100) {
3231
- 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");
3232
3736
  return;
3233
3737
  }
3234
- this.pendingMessages.push(message.content);
3235
- 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`);
3236
3740
  return;
3237
3741
  }
3238
- const result2 = await this.send(message.content);
3239
- 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}`);
3240
3744
  }
3241
- async handleAttach(message, code) {
3242
- if (this.state.ownerOpenId && message.senderId !== this.state.ownerOpenId) return;
3243
- 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;
3244
3748
  const valid = Boolean(
3245
3749
  this.state.bindCodeHash && this.state.bindCodeExpiresAt && this.state.bindCodeExpiresAt >= Date.now() && verifyBindCode(code, this.state.bindCodeHash)
3246
3750
  );
3247
3751
  if (!valid) {
3248
- 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");
3249
3753
  return;
3250
3754
  }
3251
- await this.bindChat(message, false);
3755
+ await this.bindChat(message2, false);
3252
3756
  }
3253
- canAutoBind(message) {
3254
- 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;
3255
3759
  }
3256
- async bindChat(message, automatic) {
3760
+ async bindChat(message2, automatic) {
3257
3761
  this.state = {
3258
3762
  ...this.state,
3259
- ownerOpenId: this.state.ownerOpenId ?? message.senderId,
3260
- boundChatId: message.chatId,
3763
+ ownerOpenId: this.state.ownerOpenId ?? message2.senderId,
3764
+ boundChatId: message2.chatId,
3261
3765
  autoBindDisabled: false,
3262
3766
  bindCodeHash: void 0,
3263
3767
  bindCodeExpiresAt: void 0,
@@ -3265,7 +3769,7 @@ ${escapeFence2(output).slice(-6800)}
3265
3769
  };
3266
3770
  await this.store.saveState(this.state);
3267
3771
  await this.gateway?.sendText(
3268
- message.chatId,
3772
+ message2.chatId,
3269
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"
3270
3774
  );
3271
3775
  this.previousScreen = void 0;
@@ -3296,7 +3800,11 @@ ${escapeFence2(output).slice(-6800)}
3296
3800
  }
3297
3801
  if (action.kind === "session-stop") return this.handleSessionStopAction(action);
3298
3802
  if (action.kind === "session-start-error") {
3299
- if (action.action === "create") return { type: "session-create-form", content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002" };
3803
+ if (action.action === "create") return {
3804
+ type: "session-create-form",
3805
+ content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
3806
+ view: await this.createSessionWorkspaceView(event.chatId)
3807
+ };
3300
3808
  if (action.action === "sessions") {
3301
3809
  await this.reconcileSessions(true);
3302
3810
  return this.sessionPickerActionResult("\u5DF2\u53D1\u9001\u6700\u65B0 Sessions\u3002");
@@ -3310,15 +3818,53 @@ ${escapeFence2(output).slice(-6800)}
3310
3818
  return {
3311
3819
  type: "session-create-form",
3312
3820
  content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
3821
+ view: await this.createSessionWorkspaceView(event.chatId),
3313
3822
  sessions: Object.values(this.state.sessions ?? {}),
3314
3823
  activeSessionId: this.state.activeSessionId
3315
3824
  };
3316
3825
  }
3317
- if (action.action !== "submit" || !event.action.formValue) {
3826
+ const snapshot = action.snapshotId ? this.workspaceSnapshots.get(action.snapshotId, event.chatId, event.operator.openId) : void 0;
3827
+ if (!snapshot && action.snapshotId) {
3828
+ return {
3829
+ type: "session-create-form",
3830
+ content: "\u9879\u76EE\u76EE\u5F55\u5217\u8868\u5DF2\u8FC7\u671F\uFF0C\u5DF2\u91CD\u65B0\u52A0\u8F7D\u3002",
3831
+ view: await this.createSessionWorkspaceView(event.chatId)
3832
+ };
3833
+ }
3834
+ if (action.action !== "submit") return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u64CD\u4F5C\u3002" };
3835
+ if (!event.action.formValue) {
3318
3836
  return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u8868\u5355\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
3319
3837
  }
3320
- const request = startRequestFromForm(event.action.formValue);
3321
- if (!request.ok) return { type: "error", content: request.error };
3838
+ const submittedResumeMode = formString(event.action.formValue[SESSION_CREATE_RESUME_FIELD]) || "new";
3839
+ if (submittedResumeMode === "last") {
3840
+ return { type: "error", content: "\u98DE\u4E66\u5DF2\u4E0D\u518D\u652F\u6301\u201C\u6062\u590D\u4E0A\u6B21\u4F1A\u8BDD\u201D\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u8868\u5355\u5E76\u4F7F\u7528 Resume Picker\u3002" };
3841
+ }
3842
+ if (submittedResumeMode !== "new" && submittedResumeMode !== "picker") {
3843
+ return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
3844
+ }
3845
+ const submittedAgent = formString(event.action.formValue[SESSION_CREATE_AGENT_FIELD]);
3846
+ if (!normalizeAgentId(submittedAgent)) {
3847
+ return { type: "error", content: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
3848
+ }
3849
+ const draft = sessionCreateDraftFromForm(event.action.formValue);
3850
+ const submittedProject = formString(event.action.formValue[SESSION_CREATE_PROJECT_FIELD]);
3851
+ const manualForm = action.fingerprint === "manual";
3852
+ const selectedProject = submittedProject !== SESSION_CREATE_MANUAL_VALUE ? submittedProject : "";
3853
+ if (selectedProject && !manualForm && (!snapshot || !snapshot.candidates.some((candidate) => candidate.cwd === selectedProject))) {
3854
+ return {
3855
+ type: "session-create-form",
3856
+ content: "\u9879\u76EE\u76EE\u5F55\u5217\u8868\u5DF2\u53D8\u5316\uFF0C\u5DF2\u91CD\u65B0\u52A0\u8F7D\u3002",
3857
+ view: await this.createSessionWorkspaceView(event.chatId)
3858
+ };
3859
+ }
3860
+ const request = startRequestFromDraft(draft);
3861
+ if (!request.ok) {
3862
+ return {
3863
+ type: "session-create-form",
3864
+ content: request.error,
3865
+ view: this.sessionCreateRetryView(snapshot, draft)
3866
+ };
3867
+ }
3322
3868
  const result3 = await this.startRemoteSession(request.value);
3323
3869
  if (!result3.ok) return larkStartupError(result3.error, request.value);
3324
3870
  if (result3.state === "picker") {
@@ -3492,21 +4038,42 @@ ${escapeFence2(output).slice(-6800)}
3492
4038
  const optionId = action.action.startsWith("select:") ? action.action.slice("select:".length) : "";
3493
4039
  const option2 = picker.options.find((candidate) => candidate.id === optionId);
3494
4040
  if (!option2) return { type: "resume-picker", content: "\u6240\u9009\u9879\u5DF2\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u3002", session, picker };
3495
- const delta = option2.visibleIndex - picker.selectedIndex;
3496
- const key = delta < 0 ? "Up" : "Down";
3497
- for (let step = 0; step < Math.abs(delta); step += 1) await this.tmux.sendKey(session.paneId, key);
3498
- const pane = await this.tmux.inspect(session.paneId);
3499
- if (!pane || pane.dead) {
3500
- this.pendingResumePickers.delete(sessionId);
3501
- return larkStartupError(fail(await this.startupExitedError(session, pane)), request);
3502
- }
3503
- await this.tmux.sendKey(session.paneId, "Enter");
3504
- this.pendingResumePickers.delete(sessionId);
3505
- const claimed = await this.waitForInitialAgentSessionClaim(sessionId, pane.pid);
3506
- if (!claimed.ok) {
3507
- await this.stopSession(sessionId).catch(() => void 0);
3508
- if (claimed.errorCode === "AGENT_SESSION_IN_USE") {
3509
- 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;
3510
4077
  const owner = ownerSessionId ? this.state.sessions?.[ownerSessionId] : void 0;
3511
4078
  if (owner) {
3512
4079
  this.pendingStartupConflicts.set(sessionId, { request, ownerSessionId: owner.id });
@@ -3518,17 +4085,17 @@ ${escapeFence2(output).slice(-6800)}
3518
4085
  };
3519
4086
  }
3520
4087
  }
3521
- return larkStartupError(claimed, request);
4088
+ return larkStartupError(failed, request);
3522
4089
  }
3523
- await this.tmux.preserveOnExit(session.sessionName, false).catch(() => void 0);
3524
4090
  const selected = await this.useSession(sessionId);
3525
4091
  if (!selected.ok) return { type: "error", content: remoteError(selected) };
4092
+ await this.rememberSessionWorkspace(session.cwd);
3526
4093
  return { type: "session-created", content: remoteStartSuccess(session), session };
3527
4094
  }
3528
- async readResumePicker(session) {
3529
- const pane = await this.tmux.inspect(session.paneId);
4095
+ async readResumePicker(session, signal) {
4096
+ const pane = await this.tmux.inspect(session.paneId, signal);
3530
4097
  if (!pane || pane.dead) return void 0;
3531
- const raw = await this.tmux.capture(session.paneId, 120).catch(() => "");
4098
+ const raw = await this.tmux.capture(session.paneId, 120, signal).catch(() => "");
3532
4099
  return parseResumePicker(raw, session.agent);
3533
4100
  }
3534
4101
  async handleResumePickerDeliveryFailure(candidate) {
@@ -3537,13 +4104,14 @@ ${escapeFence2(output).slice(-6800)}
3537
4104
  await this.log(`resume picker delivery failed; rolling back provisional session: session=${candidate.id} pane=${candidate.paneId}`);
3538
4105
  await this.stopSession(candidate.id);
3539
4106
  }
3540
- async waitForResumePicker(session, previousFingerprint, timeoutMs = 2500) {
4107
+ async waitForResumePicker(session, previousFingerprint, timeoutMs = 2500, signal) {
3541
4108
  const deadline = Date.now() + timeoutMs;
3542
4109
  let latest;
3543
4110
  while (Date.now() < deadline) {
3544
- latest = await this.readResumePicker(session);
4111
+ if (signal?.aborted) throw signal.reason;
4112
+ latest = await this.readResumePicker(session, signal);
3545
4113
  if (latest && (!previousFingerprint || latest.fingerprint !== previousFingerprint)) return latest;
3546
- await new Promise((resolve) => setTimeout(resolve, 100));
4114
+ await abortableDelay(100, signal);
3547
4115
  }
3548
4116
  return latest;
3549
4117
  }
@@ -3556,7 +4124,7 @@ ${escapeFence2(output).slice(-6800)}
3556
4124
  let previousFingerprint;
3557
4125
  const deadline = Date.now() + 900;
3558
4126
  do {
3559
- await new Promise((resolve) => setTimeout(resolve, 120));
4127
+ await new Promise((resolve2) => setTimeout(resolve2, 120));
3560
4128
  await this.poll();
3561
4129
  const fingerprint = this.screen?.fingerprint;
3562
4130
  if (fingerprint && fingerprint === previousFingerprint) return;
@@ -3584,7 +4152,7 @@ ${escapeFence2(output).slice(-6800)}
3584
4152
  }
3585
4153
  try {
3586
4154
  await execute(session);
3587
- await new Promise((resolve) => setTimeout(resolve, 120));
4155
+ await new Promise((resolve2) => setTimeout(resolve2, 120));
3588
4156
  await this.poll();
3589
4157
  const output = this.screen ? tailScreen(this.screen.normalized, 60) : "\u65E0\u6CD5\u8BFB\u53D6\u6700\u65B0\u7EC8\u7AEF\u753B\u9762\u3002";
3590
4158
  await this.gateway?.sendMarkdown(chatId, `**\u624B\u52A8\u64CD\u4F5C\uFF1A${operation}**
@@ -3635,7 +4203,7 @@ ${escapeFence2(output).slice(-6500)}
3635
4203
  };
3636
4204
  }
3637
4205
  if (target?.role === "custom-input" || target?.role === "chat") {
3638
- await new Promise((resolve) => setTimeout(resolve, 100));
4206
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3639
4207
  await this.poll();
3640
4208
  if (!before?.interactionId) return { type: "error", content: "\u65E0\u6CD5\u786E\u8BA4\u5F53\u524D\u4EA4\u4E92\uFF0C\u8BF7\u7528 /tail \u68C0\u67E5\u3002" };
3641
4209
  this.pendingInteractionInput = {
@@ -3768,7 +4336,7 @@ ${escapeFence2(output).slice(-6500)}
3768
4336
  if (focusedIndex === targetIndex) return { ok: true };
3769
4337
  const direction = targetIndex > focusedIndex ? "Down" : "Up";
3770
4338
  await this.tmux.sendKey(paneId, direction);
3771
- await new Promise((resolve) => setTimeout(resolve, 40));
4339
+ await new Promise((resolve2) => setTimeout(resolve2, 40));
3772
4340
  await this.poll();
3773
4341
  const nextFocused = this.screen?.actions.findIndex(({ focused }) => focused) ?? -1;
3774
4342
  if (nextFocused === focusedIndex) return { ok: false, error: "terminal focus did not move as expected" };
@@ -3778,7 +4346,7 @@ ${escapeFence2(output).slice(-6500)}
3778
4346
  async waitForInteractionChange(interactionId) {
3779
4347
  const deadline = Date.now() + 1500;
3780
4348
  while (Date.now() < deadline) {
3781
- await new Promise((resolve) => setTimeout(resolve, 75));
4349
+ await new Promise((resolve2) => setTimeout(resolve2, 75));
3782
4350
  await this.poll();
3783
4351
  const current = this.screen?.interaction;
3784
4352
  if (!current || current.interactionId !== interactionId) return;
@@ -3787,7 +4355,7 @@ ${escapeFence2(output).slice(-6500)}
3787
4355
  async waitForControlMarker(interactionId, controlId, marker) {
3788
4356
  const deadline = Date.now() + 1500;
3789
4357
  while (Date.now() < deadline) {
3790
- await new Promise((resolve) => setTimeout(resolve, 75));
4358
+ await new Promise((resolve2) => setTimeout(resolve2, 75));
3791
4359
  await this.poll();
3792
4360
  const current = this.screen?.interaction;
3793
4361
  const control = this.screen?.actions.find(({ id }) => id === controlId);
@@ -3798,7 +4366,7 @@ ${escapeFence2(output).slice(-6500)}
3798
4366
  const expected = input.trim();
3799
4367
  const deadline = Date.now() + 1500;
3800
4368
  while (Date.now() < deadline) {
3801
- await new Promise((resolve) => setTimeout(resolve, 75));
4369
+ await new Promise((resolve2) => setTimeout(resolve2, 75));
3802
4370
  await this.poll();
3803
4371
  const control = this.screen?.actions.find(({ id }) => id === controlId);
3804
4372
  if (this.screen?.interaction?.interactionId === interactionId && control?.inputValue && (control.inputValue === expected || expected.startsWith(control.inputValue))) return;
@@ -3843,7 +4411,7 @@ ${escapeFence2(output).slice(-6500)}
3843
4411
  if (!navigation.ok) return navigation;
3844
4412
  if (desired.editor) {
3845
4413
  if (desired.editor.openKey) await this.tmux.sendKey(paneId, desired.editor.openKey);
3846
- await new Promise((resolve) => setTimeout(resolve, 100));
4414
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3847
4415
  await this.tmux.sendKey(paneId, "C-u");
3848
4416
  await this.tmux.sendKey(paneId, "C-k");
3849
4417
  await this.tmux.sendText(paneId, desired.input, false);
@@ -3861,7 +4429,7 @@ ${escapeFence2(output).slice(-6500)}
3861
4429
  }
3862
4430
  return { ok: true, committed: false };
3863
4431
  }
3864
- async stopSession(sessionId = this.state.activeSessionId) {
4432
+ async stopSession(sessionId = this.state.activeSessionId, signal) {
3865
4433
  if (!sessionId) {
3866
4434
  return fail(new AppError("SESSION_NOT_FOUND", "no active managed session", { sessionId: "default" }));
3867
4435
  }
@@ -3869,9 +4437,11 @@ ${escapeFence2(output).slice(-6500)}
3869
4437
  if (!session) {
3870
4438
  return fail(new AppError("SESSION_NOT_FOUND", `unknown session: ${sessionId}`, { sessionId }));
3871
4439
  }
3872
- if (await this.tmux.hasSession(session.sessionName)) {
3873
- await this.tmux.killSession(session.sessionName);
3874
- }
4440
+ await this.tmux.killSession(session.sessionName, signal);
4441
+ await this.forgetSessionState(sessionId);
4442
+ return { ok: true };
4443
+ }
4444
+ async forgetSessionState(sessionId) {
3875
4445
  const sessions = { ...this.state.sessions };
3876
4446
  delete sessions[sessionId];
3877
4447
  this.pendingResumePickers.delete(sessionId);
@@ -3898,7 +4468,6 @@ ${escapeFence2(output).slice(-6500)}
3898
4468
  this.unresolvedNotified.clear();
3899
4469
  }
3900
4470
  await this.store.saveState(this.state);
3901
- return { ok: true };
3902
4471
  }
3903
4472
  async resetOwner() {
3904
4473
  this.pendingMessages.length = 0;
@@ -4107,37 +4676,75 @@ ${escapeFence2(output).slice(-6500)}
4107
4676
  });
4108
4677
  }
4109
4678
  }
4110
- async waitForInitialAgentSessionClaim(sessionId, panePid) {
4679
+ async waitForInitialAgentSessionClaim(session, panePid, signal) {
4111
4680
  const deadline = Date.now() + 3500;
4112
4681
  while (Date.now() < deadline) {
4113
- const session2 = this.state.sessions?.[sessionId];
4114
- if (!session2) return { ok: false, error: `session disappeared during startup: ${sessionId}` };
4115
- if (session2.agentSessionId) return { ok: true };
4116
- const pane2 = await this.tmux.inspect(session2.paneId);
4117
- if (!pane2 || pane2.dead) return fail(await this.startupExitedError(session2, pane2));
4118
- const agentSessionId = await resolveNativeAgentSessionId(session2.agent, panePid).catch(() => void 0);
4682
+ if (session.agentSessionId) return { ok: true };
4683
+ const committedClaim2 = this.state.sessions?.[session.id]?.agentSessionId;
4684
+ if (committedClaim2) {
4685
+ session.agentSessionId = committedClaim2;
4686
+ return { ok: true };
4687
+ }
4688
+ const pending2 = this.pendingAgentSessionClaims.get(session.id);
4689
+ if (pending2) {
4690
+ const claimed = this.claimStartingAgentSession(session, pending2);
4691
+ if (!claimed.ok || session.agentSessionId) return claimed;
4692
+ }
4693
+ if (signal?.aborted) throw signal.reason;
4694
+ const pane2 = await this.tmux.inspect(session.paneId, signal);
4695
+ if (signal?.aborted) throw signal.reason;
4696
+ if (!pane2 || pane2.dead) return fail(await this.startupExitedError(session, pane2));
4697
+ const agentSessionId = await resolveNativeAgentSessionId(
4698
+ session.agent,
4699
+ panePid,
4700
+ void 0,
4701
+ signal
4702
+ ).catch(() => void 0);
4119
4703
  if (agentSessionId) {
4120
- return this.handleAgentSessionStarted({
4121
- sessionId,
4122
- agent: session2.agent,
4704
+ return this.claimStartingAgentSession(session, {
4705
+ sessionId: session.id,
4706
+ agent: session.agent,
4123
4707
  agentSessionId,
4124
- cwd: session2.cwd,
4708
+ cwd: session.cwd,
4125
4709
  source: "startup-discovery"
4126
4710
  });
4127
4711
  }
4128
- await new Promise((resolve) => setTimeout(resolve, 100));
4712
+ await abortableDelay(100, signal);
4129
4713
  }
4130
- const session = this.state.sessions?.[sessionId];
4131
- if (!session) return fail(new AppError("SESSION_NOT_FOUND", `session disappeared during startup: ${sessionId}`, { sessionId }));
4132
- const pane = await this.tmux.inspect(session.paneId);
4714
+ const pane = await this.tmux.inspect(session.paneId, signal);
4715
+ if (signal?.aborted) throw signal.reason;
4133
4716
  if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4134
- if (session.agentSessionId || this.pendingAgentSessionClaims.has(sessionId)) return { ok: true };
4717
+ const pending = this.pendingAgentSessionClaims.get(session.id);
4718
+ if (pending) return this.claimStartingAgentSession(session, pending);
4719
+ const committedClaim = this.state.sessions?.[session.id]?.agentSessionId;
4720
+ if (committedClaim) {
4721
+ session.agentSessionId = committedClaim;
4722
+ return { ok: true };
4723
+ }
4724
+ if (session.agentSessionId) return { ok: true };
4135
4725
  return fail(new AppError(
4136
4726
  "AGENT_IDENTITY_TIMEOUT",
4137
- `unable to identify resumed native session: ${sessionId}`,
4138
- { sessionId, agent: session.agent }
4727
+ `unable to identify resumed native session: ${session.id}`,
4728
+ { sessionId: session.id, agent: session.agent }
4139
4729
  ));
4140
4730
  }
4731
+ claimStartingAgentSession(session, candidate) {
4732
+ if (candidate.agent !== session.agent) {
4733
+ return fail(new AppError("START_FAILED", "agent-session candidate does not match starting session", {
4734
+ sessionId: session.id,
4735
+ agent: session.agent
4736
+ }));
4737
+ }
4738
+ const owner = this.findAgentSessionOwner(candidate.agent, candidate.agentSessionId, session.id);
4739
+ if (owner) {
4740
+ this.pendingAgentSessionClaims.delete(session.id);
4741
+ return fail(agentSessionInUse(session.id, owner.id));
4742
+ }
4743
+ session.agentSessionId = candidate.agentSessionId;
4744
+ session.updatedAt = Date.now();
4745
+ this.pendingAgentSessionClaims.delete(session.id);
4746
+ return { ok: true };
4747
+ }
4141
4748
  async startupExitedError(session, pane) {
4142
4749
  const terminalTail = await this.tmux.capture(session.paneId, 40).then((output) => tailScreen(output, 40).slice(-3e3)).catch(() => "");
4143
4750
  return new AppError(
@@ -4151,12 +4758,14 @@ ${escapeFence2(output).slice(-6500)}
4151
4758
  }
4152
4759
  );
4153
4760
  }
4154
- async waitForStartupStability(session, durationMs) {
4761
+ async waitForStartupStability(session, durationMs, signal) {
4155
4762
  const deadline = Date.now() + durationMs;
4156
4763
  while (Date.now() < deadline) {
4157
- const pane = await this.tmux.inspect(session.paneId);
4764
+ if (signal?.aborted) throw signal.reason;
4765
+ const pane = await this.tmux.inspect(session.paneId, signal);
4766
+ if (signal?.aborted) throw signal.reason;
4158
4767
  if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
4159
- await new Promise((resolve) => setTimeout(resolve, 80));
4768
+ await abortableDelay(80, signal);
4160
4769
  }
4161
4770
  return { ok: true };
4162
4771
  }
@@ -4219,13 +4828,13 @@ ${output}`
4219
4828
  const session = this.activeSession();
4220
4829
  if (!session || this.shouldQueueMessage()) return;
4221
4830
  while (this.pendingMessages.length > 0 && !this.shouldQueueMessage()) {
4222
- const message = this.pendingMessages[0];
4223
- if (!message) {
4831
+ const message2 = this.pendingMessages[0];
4832
+ if (!message2) {
4224
4833
  this.pendingMessages.shift();
4225
4834
  continue;
4226
4835
  }
4227
4836
  this.clearPendingCompletion();
4228
- await this.tmux.sendText(session.paneId, message);
4837
+ await this.tmux.sendText(session.paneId, message2);
4229
4838
  this.pendingMessages.shift();
4230
4839
  }
4231
4840
  if (this.pendingMessages.length === 0 && this.state.boundChatId) {
@@ -4234,9 +4843,9 @@ ${output}`
4234
4843
  activeSession() {
4235
4844
  return this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
4236
4845
  }
4237
- async reconcileSessions(discover = false) {
4846
+ async reconcileSessions(discover = false, signal) {
4238
4847
  const previousActive = this.state.activeSessionId;
4239
- const result2 = await this.reconciler.reconcile(this.state, discover);
4848
+ const result2 = await this.reconciler.reconcile(this.state, discover, signal);
4240
4849
  if (!result2.changed) return result2.liveSessions;
4241
4850
  const activeChanged = result2.state.activeSessionId !== previousActive;
4242
4851
  if (result2.removedActive) {
@@ -4293,7 +4902,7 @@ ${output}`
4293
4902
  await this.log(
4294
4903
  `remote session create requested: session=${request.sessionId} agent=${request.agent} resume=${request.resume?.mode ?? "new"}`
4295
4904
  );
4296
- const started = await this.startSession(request.sessionId, request.cwd, request.agent, request.resume);
4905
+ const started = await this.startSession(request.sessionId, request.cwd, request.agent, request.resume, "lark");
4297
4906
  if (!started.ok) {
4298
4907
  await this.log(`remote session create failed: session=${request.sessionId} code=${started.errorCode ?? "UNKNOWN"}`);
4299
4908
  if (started.errorCode === "AGENT_SESSION_IN_USE") {
@@ -4313,7 +4922,7 @@ ${output}`
4313
4922
  }
4314
4923
  const session = selected.value;
4315
4924
  if (request.resume?.mode === "picker") {
4316
- const picker = await this.waitForResumePicker(session);
4925
+ const picker = started.value.resumePicker;
4317
4926
  if (picker) {
4318
4927
  this.pendingResumePickers.set(session.id, request);
4319
4928
  return { ok: true, state: "picker", session, picker };
@@ -4324,14 +4933,97 @@ ${output}`
4324
4933
  }
4325
4934
  return { ok: true, state: "ready", session };
4326
4935
  }
4327
- async log(message) {
4328
- await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
4936
+ async createSessionWorkspaceView(chatId, draft = emptySessionCreateDraft()) {
4937
+ await this.reconcileSessions(true);
4938
+ const latestConfig = await this.store.loadConfig();
4939
+ const discovered = await discoverWorkspaces({
4940
+ workspaceRoots: latestConfig?.workspaceRoots ?? this.config.workspaceRoots,
4941
+ activeSessions: Object.values(this.state.sessions ?? {}),
4942
+ recentWorkspaces: this.state.recentWorkspaces ?? []
4943
+ });
4944
+ for (const warning of discovered.warnings) await this.log(`workspace discovery warning: ${warning}`);
4945
+ const snapshot = this.workspaceSnapshots.create({
4946
+ chatId,
4947
+ ownerOpenId: this.state.ownerOpenId ?? "",
4948
+ candidates: discovered.candidates,
4949
+ warnings: discovered.warnings,
4950
+ partial: discovered.partial
4951
+ });
4952
+ return discovered.candidates.length > 0 ? this.sessionWorkspaceView(snapshot, draft) : this.manualSessionCreateView(snapshot, draft);
4953
+ }
4954
+ sessionWorkspaceView(snapshot, draft) {
4955
+ const visibleCandidates = snapshot.candidates.slice(0, 99);
4956
+ return {
4957
+ mode: "projects",
4958
+ snapshotId: snapshot.id,
4959
+ page: 0,
4960
+ pageCount: 1,
4961
+ hasProjectCandidates: snapshot.candidates.length > 0,
4962
+ candidates: visibleCandidates,
4963
+ warnings: snapshot.warnings,
4964
+ partial: snapshot.partial || visibleCandidates.length < snapshot.candidates.length,
4965
+ draft
4966
+ };
4967
+ }
4968
+ sessionCreateRetryView(snapshot, draft) {
4969
+ return snapshot && snapshot.candidates.length > 0 ? this.sessionWorkspaceView(snapshot, draft) : this.manualSessionCreateView(snapshot, draft);
4970
+ }
4971
+ manualSessionCreateView(snapshot, draft, requestedPage = 0) {
4972
+ const pageCount = Math.max(1, Math.ceil((snapshot?.candidates.length ?? 0) / 20));
4973
+ const page = Math.max(0, Math.min(pageCount - 1, requestedPage));
4974
+ const noCandidates = snapshot && snapshot.candidates.length === 0 ? ["\u5C1A\u672A\u53D1\u73B0\u53EF\u9009\u9879\u76EE\uFF1B\u8BF7\u624B\u52A8\u586B\u5199\u8DEF\u5F84\uFF0C\u6216\u5728\u672C\u673A\u8FD0\u884C lca workspace add ~/workspace\u3002"] : [];
4975
+ return {
4976
+ mode: "manual",
4977
+ snapshotId: snapshot?.id,
4978
+ page,
4979
+ hasProjectCandidates: (snapshot?.candidates.length ?? 0) > 0,
4980
+ pageCount,
4981
+ candidates: [],
4982
+ warnings: [...snapshot?.warnings ?? [], ...noCandidates],
4983
+ partial: snapshot?.partial ?? false,
4984
+ draft: { ...draft, manualCwd: draft.manualCwd ?? (draft.cwd !== SESSION_CREATE_MANUAL_VALUE ? draft.cwd : void 0) }
4985
+ };
4986
+ }
4987
+ async rememberSessionWorkspace(cwd) {
4988
+ this.state = {
4989
+ ...this.state,
4990
+ recentWorkspaces: rememberRecentWorkspace(this.state.recentWorkspaces, cwd),
4991
+ updatedAt: Date.now()
4992
+ };
4993
+ await this.store.saveState(this.state).catch((error) => this.log(
4994
+ `failed to persist recent workspace: cwd=${cwd} error=${errorMessage3(error)}`
4995
+ ));
4996
+ }
4997
+ async log(message2) {
4998
+ await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message2}
4329
4999
  `, { mode: 384 });
4330
5000
  }
4331
5001
  };
4332
5002
  function fail(error) {
4333
5003
  return { ok: false, ...serializeAppError(error) };
4334
5004
  }
5005
+ function daemonResultAppError(result2) {
5006
+ return new AppError(
5007
+ result2.errorCode ?? "START_FAILED",
5008
+ result2.error,
5009
+ result2.errorContext ?? {}
5010
+ );
5011
+ }
5012
+ function abortableDelay(ms, signal) {
5013
+ if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
5014
+ if (signal.aborted) return Promise.reject(signal.reason);
5015
+ return new Promise((resolve2, reject) => {
5016
+ const timer = setTimeout(() => {
5017
+ signal.removeEventListener("abort", aborted);
5018
+ resolve2();
5019
+ }, ms);
5020
+ const aborted = () => {
5021
+ clearTimeout(timer);
5022
+ reject(signal.reason);
5023
+ };
5024
+ signal.addEventListener("abort", aborted, { once: true });
5025
+ });
5026
+ }
4335
5027
  function agentSessionInUse(sessionId, ownerSessionId) {
4336
5028
  return new AppError(
4337
5029
  "AGENT_SESSION_IN_USE",
@@ -4361,24 +5053,29 @@ function remember(values, value, limit) {
4361
5053
  values.delete(oldest);
4362
5054
  }
4363
5055
  }
4364
- function startRequestFromForm(values) {
5056
+ function sessionCreateDraftFromForm(values) {
4365
5057
  const sessionId = formString(values[SESSION_CREATE_NAME_FIELD]);
4366
5058
  const agentValue = formString(values[SESSION_CREATE_AGENT_FIELD]);
4367
- const cwd = formString(values[SESSION_CREATE_CWD_FIELD]);
5059
+ const projectCwd = formString(values[SESSION_CREATE_PROJECT_FIELD]);
5060
+ const manualCwd = formString(values[SESSION_CREATE_CWD_FIELD]);
4368
5061
  const resumeMode = formString(values[SESSION_CREATE_RESUME_FIELD]) || "new";
5062
+ const cwd = projectCwd === SESSION_CREATE_MANUAL_VALUE ? manualCwd : projectCwd || manualCwd;
5063
+ return {
5064
+ sessionId: sessionId || void 0,
5065
+ agent: normalizeAgentId(agentValue) ?? "codex",
5066
+ resumeMode: resumeMode === "picker" ? "picker" : "new",
5067
+ cwd: cwd || void 0,
5068
+ projectCwd: projectCwd || void 0,
5069
+ manualCwd: manualCwd || void 0
5070
+ };
5071
+ }
5072
+ function startRequestFromDraft(draft) {
5073
+ const sessionId = draft.sessionId?.trim() ?? "";
4369
5074
  if (!sessionId) return { ok: false, error: "\u8BF7\u586B\u5199 Session \u540D\u79F0\u3002" };
4370
- const agent = normalizeAgentId(agentValue);
4371
- if (!agent) return { ok: false, error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
4372
- if (!cwd) return { ok: false, error: "\u8BF7\u586B\u5199\u7EDD\u5BF9\u5DE5\u4F5C\u76EE\u5F55\u3002" };
5075
+ if (!draft.cwd || draft.cwd === SESSION_CREATE_MANUAL_VALUE) return { ok: false, error: "\u8BF7\u9009\u62E9\u6216\u586B\u5199\u9879\u76EE\u76EE\u5F55\u3002" };
4373
5076
  let resume;
4374
- if (resumeMode === "last") {
4375
- return { ok: false, error: "\u98DE\u4E66\u5DF2\u4E0D\u518D\u652F\u6301\u201C\u6062\u590D\u4E0A\u6B21\u4F1A\u8BDD\u201D\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u8868\u5355\u5E76\u4F7F\u7528 Resume Picker\u3002" };
4376
- }
4377
- if (resumeMode === "picker") resume = { mode: "picker" };
4378
- else if (resumeMode !== "new") {
4379
- return { ok: false, error: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
4380
- }
4381
- return { ok: true, value: { sessionId, agent, cwd, resume } };
5077
+ if (draft.resumeMode === "picker") resume = { mode: "picker" };
5078
+ return { ok: true, value: { sessionId, agent: draft.agent, cwd: draft.cwd, resume } };
4382
5079
  }
4383
5080
  function formString(value) {
4384
5081
  if (typeof value === "string") return value.trim();
@@ -4400,7 +5097,22 @@ function remoteError(result2) {
4400
5097
  const sessionId = typeof context.sessionId === "string" ? context.sessionId : "\u8BE5\u540D\u79F0";
4401
5098
  switch (result2.errorCode) {
4402
5099
  case "SESSION_EXISTS":
4403
- return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A\u8BE5 session \u5DF2\u5728\u8FD0\u884C\u3002\u8BF7\u6362\u4E00\u4E2A\u540D\u79F0\uFF0C\u6216\u7528 /sessions \u8FDE\u63A5\u73B0\u6709 session\u3002`;
5100
+ 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`;
5101
+ case "SESSION_STARTING":
5102
+ return `session\u300C${sessionId}\u300D\u6B63\u5728\u542F\u52A8\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D\u64CD\u4F5C\u5B8C\u6210\u540E\u518D\u8BD5\u3002`;
5103
+ case "SESSION_START_TIMEOUT": {
5104
+ const agent = typeof context.agent === "string" ? context.agent : "Agent";
5105
+ const cwd = typeof context.cwd === "string" ? context.cwd : "\u672A\u77E5\u76EE\u5F55";
5106
+ const stage = typeof context.stage === "string" ? context.stage : "unknown";
5107
+ const excerpt = typeof context.terminalExcerpt === "string" && context.terminalExcerpt.trim() ? `
5108
+
5109
+ \u6700\u8FD1\u7EC8\u7AEF\u8F93\u51FA\uFF1A
5110
+ ${context.terminalExcerpt}` : "";
5111
+ 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
5112
+
5113
+ \u5DE5\u4F5C\u76EE\u5F55\uFF1A${cwd}
5114
+ \u8D85\u65F6\u9636\u6BB5\uFF1A${stage}${excerpt}`;
5115
+ }
4404
5116
  case "AGENT_SESSION_IN_USE": {
4405
5117
  const ownerSessionId = typeof context.ownerSessionId === "string" ? context.ownerSessionId : "\u73B0\u6709 session";
4406
5118
  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`;
@@ -4476,20 +5188,20 @@ function manualTimestamp() {
4476
5188
  }
4477
5189
 
4478
5190
  // src/core/paths.ts
4479
- import { homedir as homedir2 } from "os";
4480
- import { join as join2 } from "path";
5191
+ import { homedir as homedir4 } from "os";
5192
+ import { join as join3 } from "path";
4481
5193
  function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
4482
- const base = root || join2(homedir2(), ".lark-coding-assistant");
5194
+ const base = root || join3(homedir4(), ".lark-coding-assistant");
4483
5195
  return {
4484
5196
  root: base,
4485
- config: join2(base, "config.json"),
4486
- secrets: join2(base, "secrets.json"),
4487
- state: join2(base, "state.json"),
4488
- logsDir: join2(base, "logs"),
4489
- logFile: join2(base, "logs", "assistant.log"),
4490
- runtimeDir: join2(base, "runtime"),
4491
- socket: join2(base, "runtime", "daemon.sock"),
4492
- pid: join2(base, "runtime", "daemon.pid")
5197
+ config: join3(base, "config.json"),
5198
+ secrets: join3(base, "secrets.json"),
5199
+ state: join3(base, "state.json"),
5200
+ logsDir: join3(base, "logs"),
5201
+ logFile: join3(base, "logs", "assistant.log"),
5202
+ runtimeDir: join3(base, "runtime"),
5203
+ socket: join3(base, "runtime", "daemon.sock"),
5204
+ pid: join3(base, "runtime", "daemon.pid")
4493
5205
  };
4494
5206
  }
4495
5207