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