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