lark-coding-assistant 0.1.3 → 0.2.1
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 +61 -49
- package/bin/lca.mjs +2 -0
- package/dist/cli.js +349 -38
- package/dist/cli.js.map +1 -1
- package/dist/daemon-entry.js +1835 -199
- 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 };
|
|
569
|
+
}
|
|
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
|
+
}
|
|
274
591
|
}
|
|
275
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
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
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
|
+
]
|
|
1204
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
|
|
1205
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
|
+
];
|
|
1206
1734
|
}
|
|
1207
|
-
function sessionCard(chatId, session, active, signer) {
|
|
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
|
-
|
|
2020
|
+
if (!action) {
|
|
2021
|
+
if (this.formActionsInFlight.has(event.messageId)) {
|
|
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") {
|
|
1379
2029
|
if (this.formActionsInFlight.has(event.messageId)) {
|
|
1380
|
-
return { toast: { type: "warning", content: action
|
|
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 {
|
|
@@ -1407,22 +2064,129 @@ var LarkGateway = class {
|
|
|
1407
2064
|
card: { type: "raw", data: interactionInputCard(result2.agent, result2.label) }
|
|
1408
2065
|
};
|
|
1409
2066
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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") {
|
|
2093
|
+
return {
|
|
2094
|
+
toast: { type: "success", content: result2.content },
|
|
2095
|
+
card: { type: "raw", data: sessionPickerCard(
|
|
2096
|
+
event.chatId,
|
|
2097
|
+
result2.sessions,
|
|
2098
|
+
result2.activeSessionId,
|
|
2099
|
+
this.signer,
|
|
2100
|
+
result2.confirmingStopSessionId
|
|
2101
|
+
) }
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
return {
|
|
2105
|
+
toast: result2,
|
|
2106
|
+
card: { type: "raw", data: handledActionCard(action, result2.content) }
|
|
2107
|
+
};
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
handler;
|
|
2111
|
+
channel;
|
|
2112
|
+
signer;
|
|
2113
|
+
formActions = /* @__PURE__ */ new Map();
|
|
2114
|
+
formActionsInFlight = /* @__PURE__ */ new Set();
|
|
2115
|
+
async handleDeferredCardAction(event, action) {
|
|
2116
|
+
let initialResumePicker;
|
|
2117
|
+
try {
|
|
2118
|
+
if (action.kind === "session-create" && action.action === "submit") {
|
|
2119
|
+
await this.updateCardAfterAction(event.messageId, sessionCreateProgressCard());
|
|
2120
|
+
}
|
|
2121
|
+
const result2 = await this.handler.onAction(event, action);
|
|
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
|
+
}
|
|
2131
|
+
await this.channel.send(event.chatId, { text: `\u5361\u7247\u64CD\u4F5C\u672A\u5B8C\u6210\uFF1A${result2.content}` });
|
|
2132
|
+
return;
|
|
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
|
+
});
|
|
1426
2190
|
return;
|
|
1427
2191
|
}
|
|
1428
2192
|
if (result2.type === "refresh") {
|
|
@@ -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() {
|
|
@@ -1783,10 +2917,41 @@ var AssistantDaemon = class {
|
|
|
1783
2917
|
}
|
|
1784
2918
|
return { mode: "code", bindCode: createBindCode(), expiresInSeconds: 600 };
|
|
1785
2919
|
}
|
|
1786
|
-
async runtimeStatus(sessionId
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
|
|
2920
|
+
async runtimeStatus(sessionId) {
|
|
2921
|
+
const selectedSessionId = sessionId ?? this.state.activeSessionId;
|
|
2922
|
+
const selected = selectedSessionId ? this.state.sessions?.[selectedSessionId] : void 0;
|
|
2923
|
+
const requestedSessions = sessionId ? selected ? [selected] : [] : Object.values(this.state.sessions ?? {});
|
|
2924
|
+
const sessions = await Promise.all(requestedSessions.map((session) => this.sessionRuntimeStatus(session)));
|
|
2925
|
+
const selectedRuntime = selected ? sessions.find(({ session }) => session.id === selected.id) ?? await this.sessionRuntimeStatus(selected) : void 0;
|
|
2926
|
+
return {
|
|
2927
|
+
state: this.state,
|
|
2928
|
+
session: selected,
|
|
2929
|
+
screen: selectedRuntime?.screen,
|
|
2930
|
+
paneAlive: selectedRuntime?.paneAlive === true,
|
|
2931
|
+
sessions
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
async sessionRuntimeStatus(session) {
|
|
2935
|
+
const active = session.id === this.state.activeSessionId;
|
|
2936
|
+
try {
|
|
2937
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
2938
|
+
if (!pane || pane.dead) {
|
|
2939
|
+
return {
|
|
2940
|
+
session,
|
|
2941
|
+
screen: getAgentAdapter(session.agent).detectScreen("", false),
|
|
2942
|
+
paneAlive: false,
|
|
2943
|
+
active
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
const screen = active && this.screen ? this.screen : getAgentAdapter(session.agent).detectScreen(
|
|
2947
|
+
await this.tmux.capture(session.paneId, 160),
|
|
2948
|
+
true,
|
|
2949
|
+
{ x: pane.cursorX, y: pane.cursorY }
|
|
2950
|
+
);
|
|
2951
|
+
return { session, screen, paneAlive: true, active };
|
|
2952
|
+
} catch {
|
|
2953
|
+
return { session, active };
|
|
2954
|
+
}
|
|
1790
2955
|
}
|
|
1791
2956
|
async tail(lines) {
|
|
1792
2957
|
const session = this.activeSession();
|
|
@@ -1818,6 +2983,40 @@ var AssistantDaemon = class {
|
|
|
1818
2983
|
}
|
|
1819
2984
|
}
|
|
1820
2985
|
if (message.senderId !== this.state.ownerOpenId || message.chatId !== this.state.boundChatId) return;
|
|
2986
|
+
if (text === "/start") {
|
|
2987
|
+
try {
|
|
2988
|
+
await this.gateway?.sendSessionCreate(message.chatId);
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
await this.log(`session create card failed: ${errorMessage3(error)}`);
|
|
2991
|
+
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");
|
|
2992
|
+
}
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
if (text.startsWith("/start ")) {
|
|
2996
|
+
let request;
|
|
2997
|
+
try {
|
|
2998
|
+
request = parseStartCommand(text);
|
|
2999
|
+
} catch (error) {
|
|
3000
|
+
await this.gateway?.sendText(message.chatId, remoteError(fail(error)));
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
const result3 = await this.startRemoteSession(request);
|
|
3004
|
+
if (!result3.ok) {
|
|
3005
|
+
const failure = sessionStartupFailure(result3.error, request);
|
|
3006
|
+
if (failure) await this.gateway?.sendSessionStartupFailure(message.chatId, failure);
|
|
3007
|
+
else await this.gateway?.sendText(message.chatId, remoteError(result3.error));
|
|
3008
|
+
} else if (result3.state === "picker") {
|
|
3009
|
+
try {
|
|
3010
|
+
await this.gateway?.sendResumePicker(message.chatId, result3.session, result3.picker);
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
await this.handleResumePickerDeliveryFailure(result3.session);
|
|
3013
|
+
await this.log(`resume picker notification failed: session=${result3.session.id} error=${errorMessage3(error)}`);
|
|
3014
|
+
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");
|
|
3015
|
+
}
|
|
3016
|
+
} else if (result3.state === "conflict") await this.gateway?.sendStartupConflict(message.chatId, result3.request, result3.owner);
|
|
3017
|
+
else await this.gateway?.sendText(message.chatId, remoteStartSuccess(result3.session));
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
1821
3020
|
const tailMatch = text.match(/^\/tail(?:\s+(\d+))?$/);
|
|
1822
3021
|
if (tailMatch) {
|
|
1823
3022
|
const lines = tailMatch[1] ? Number(tailMatch[1]) : 80;
|
|
@@ -1825,7 +3024,7 @@ var AssistantDaemon = class {
|
|
|
1825
3024
|
await this.gateway?.sendText(message.chatId, "\u7528\u6CD5\uFF1A/tail [20-300]");
|
|
1826
3025
|
return;
|
|
1827
3026
|
}
|
|
1828
|
-
const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${
|
|
3027
|
+
const output = await this.tail(lines).catch((error) => `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorMessage3(error)}`);
|
|
1829
3028
|
const session = this.activeSession();
|
|
1830
3029
|
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
3030
|
await this.gateway?.sendMarkdown(message.chatId, `${metadata}
|
|
@@ -1841,13 +3040,13 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
1841
3040
|
}
|
|
1842
3041
|
if (text === "/manual") {
|
|
1843
3042
|
await this.poll();
|
|
1844
|
-
const
|
|
1845
|
-
if (!
|
|
3043
|
+
const view2 = this.currentManualView();
|
|
3044
|
+
if (!view2) await this.gateway?.sendText(message.chatId, "\u5F53\u524D\u6CA1\u6709\u53EF\u9065\u63A7\u7684 active tmux session\u3002");
|
|
1846
3045
|
else {
|
|
1847
3046
|
try {
|
|
1848
|
-
await this.gateway?.sendManual(message.chatId,
|
|
3047
|
+
await this.gateway?.sendManual(message.chatId, view2);
|
|
1849
3048
|
} catch (error) {
|
|
1850
|
-
await this.log(`manual card failed: ${
|
|
3049
|
+
await this.log(`manual card failed: ${errorMessage3(error)}`);
|
|
1851
3050
|
await this.gateway?.sendText(
|
|
1852
3051
|
message.chatId,
|
|
1853
3052
|
"\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 +3084,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
1885
3084
|
return;
|
|
1886
3085
|
}
|
|
1887
3086
|
if (text === "/sessions") {
|
|
1888
|
-
const sessions = await this.reconcileSessions();
|
|
3087
|
+
const sessions = await this.reconcileSessions(true);
|
|
1889
3088
|
try {
|
|
1890
3089
|
await this.gateway?.sendSessionPicker(
|
|
1891
3090
|
message.chatId,
|
|
@@ -1893,7 +3092,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
1893
3092
|
this.state.activeSessionId
|
|
1894
3093
|
);
|
|
1895
3094
|
} catch (error) {
|
|
1896
|
-
await this.log(`session picker notification failed: ${
|
|
3095
|
+
await this.log(`session picker notification failed: ${errorMessage3(error)}`);
|
|
1897
3096
|
await this.gateway?.sendText(
|
|
1898
3097
|
message.chatId,
|
|
1899
3098
|
"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 +3247,41 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
2048
3247
|
const result3 = await this.useSession(target.id);
|
|
2049
3248
|
return result3.ok ? { type: "success", content: `\u5DF2\u8FDE\u63A5\u5230 ${target.id}` } : { type: "error", content: result3.error };
|
|
2050
3249
|
}
|
|
3250
|
+
if (action.kind === "session-stop") return this.handleSessionStopAction(action);
|
|
3251
|
+
if (action.kind === "session-start-error") {
|
|
3252
|
+
if (action.action === "create") return { type: "session-create-form", content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002" };
|
|
3253
|
+
if (action.action === "sessions") {
|
|
3254
|
+
await this.reconcileSessions(true);
|
|
3255
|
+
return this.sessionPickerActionResult("\u5DF2\u53D1\u9001\u6700\u65B0 Sessions\u3002");
|
|
3256
|
+
}
|
|
3257
|
+
return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u5931\u8D25\u5361\u7247\u64CD\u4F5C\u3002" };
|
|
3258
|
+
}
|
|
3259
|
+
if (action.kind === "startup-conflict") return this.handleStartupConflictAction(action);
|
|
3260
|
+
if (action.kind === "resume-picker") return this.handleResumePickerAction(action);
|
|
3261
|
+
if (action.kind === "session-create") {
|
|
3262
|
+
if (action.action === "open") {
|
|
3263
|
+
return {
|
|
3264
|
+
type: "session-create-form",
|
|
3265
|
+
content: "\u8BF7\u586B\u5199\u542F\u52A8\u4FE1\u606F\u3002",
|
|
3266
|
+
sessions: Object.values(this.state.sessions ?? {}),
|
|
3267
|
+
activeSessionId: this.state.activeSessionId
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
if (action.action !== "submit" || !event.action.formValue) {
|
|
3271
|
+
return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u65B0\u5EFA Session \u8868\u5355\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
|
|
3272
|
+
}
|
|
3273
|
+
const request = startRequestFromForm(event.action.formValue);
|
|
3274
|
+
if (!request.ok) return { type: "error", content: request.error };
|
|
3275
|
+
const result3 = await this.startRemoteSession(request.value);
|
|
3276
|
+
if (!result3.ok) return larkStartupError(result3.error, request.value);
|
|
3277
|
+
if (result3.state === "picker") {
|
|
3278
|
+
return { type: "resume-picker", content: "\u8BF7\u9009\u62E9\u8981\u6062\u590D\u7684\u5386\u53F2 Session\u3002", session: result3.session, picker: result3.picker };
|
|
3279
|
+
}
|
|
3280
|
+
if (result3.state === "conflict") {
|
|
3281
|
+
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 };
|
|
3282
|
+
}
|
|
3283
|
+
return { type: "session-created", content: remoteStartSuccess(result3.session), session: result3.session };
|
|
3284
|
+
}
|
|
2051
3285
|
if (action.kind === "manual") {
|
|
2052
3286
|
return this.withInteractionNotificationsSuppressed(() => this.handleManualAction(action, event));
|
|
2053
3287
|
}
|
|
@@ -2112,7 +3346,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
2112
3346
|
return {
|
|
2113
3347
|
type: "manual",
|
|
2114
3348
|
content: "\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\u3002",
|
|
2115
|
-
view: this.manualView(session, current2, "error",
|
|
3349
|
+
view: this.manualView(session, current2, "error", errorMessage3(error), operation, action.manualMode)
|
|
2116
3350
|
};
|
|
2117
3351
|
}
|
|
2118
3352
|
const current = this.screen ?? screen;
|
|
@@ -2130,6 +3364,142 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
2130
3364
|
view: this.manualView(session, current, "active", void 0, operation, action.manualMode)
|
|
2131
3365
|
};
|
|
2132
3366
|
}
|
|
3367
|
+
async handleSessionStopAction(action) {
|
|
3368
|
+
const sessionId = action.sessionId ?? "";
|
|
3369
|
+
const target = this.state.sessions?.[sessionId];
|
|
3370
|
+
if (!target || target.agent !== action.agent || target.paneId !== action.paneId || String(target.updatedAt) !== action.fingerprint) {
|
|
3371
|
+
return { type: "error", content: "\u76EE\u6807 session \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
|
|
3372
|
+
}
|
|
3373
|
+
if (action.action === "request") {
|
|
3374
|
+
return this.sessionPickerActionResult(`\u8BF7\u786E\u8BA4\u662F\u5426\u5173\u95ED ${target.id}\u3002`, target.id);
|
|
3375
|
+
}
|
|
3376
|
+
if (action.action === "cancel") {
|
|
3377
|
+
return this.sessionPickerActionResult(`\u5DF2\u53D6\u6D88\u5173\u95ED ${target.id}\u3002`);
|
|
3378
|
+
}
|
|
3379
|
+
if (action.action !== "confirm") return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u5173\u95ED\u64CD\u4F5C\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 /sessions\u3002" };
|
|
3380
|
+
const stopped = await this.stopSession(target.id);
|
|
3381
|
+
if (!stopped.ok) return { type: "error", content: remoteError(stopped) };
|
|
3382
|
+
return this.sessionPickerActionResult(`\u5DF2\u5173\u95ED ${target.id}\u3002`);
|
|
3383
|
+
}
|
|
3384
|
+
async handleStartupConflictAction(action) {
|
|
3385
|
+
const requestedSessionId = action.sessionId ?? "";
|
|
3386
|
+
const pending = this.pendingStartupConflicts.get(requestedSessionId);
|
|
3387
|
+
const owner = pending ? this.state.sessions?.[pending.ownerSessionId] : void 0;
|
|
3388
|
+
if (!pending || !owner || pending.request.agent !== action.agent || owner.paneId !== action.paneId || String(owner.updatedAt) !== action.fingerprint) {
|
|
3389
|
+
return { type: "error", content: "Session \u51B2\u7A81\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA Session\u3002" };
|
|
3390
|
+
}
|
|
3391
|
+
if (action.action === "cancel") {
|
|
3392
|
+
this.pendingStartupConflicts.delete(requestedSessionId);
|
|
3393
|
+
return { type: "success", content: `\u5DF2\u53D6\u6D88\u521B\u5EFA ${requestedSessionId}\u3002` };
|
|
3394
|
+
}
|
|
3395
|
+
if (action.action === "connect") {
|
|
3396
|
+
const selected = await this.useSession(owner.id);
|
|
3397
|
+
if (!selected.ok) return { type: "error", content: remoteError(selected) };
|
|
3398
|
+
this.pendingStartupConflicts.delete(requestedSessionId);
|
|
3399
|
+
return { type: "session-created", content: `\u5DF2\u8FDE\u63A5\u73B0\u6709 session\u300C${owner.id}\u300D\u3002`, session: owner };
|
|
3400
|
+
}
|
|
3401
|
+
if (action.action !== "new") return { type: "error", content: "\u65E0\u6CD5\u8BC6\u522B\u51B2\u7A81\u5904\u7406\u64CD\u4F5C\u3002" };
|
|
3402
|
+
this.pendingStartupConflicts.delete(requestedSessionId);
|
|
3403
|
+
const result2 = await this.startRemoteSession({ ...pending.request, resume: void 0 });
|
|
3404
|
+
if (!result2.ok) return larkStartupError(result2.error, pending.request);
|
|
3405
|
+
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" };
|
|
3406
|
+
return { type: "session-created", content: remoteStartSuccess(result2.session), session: result2.session };
|
|
3407
|
+
}
|
|
3408
|
+
sessionPickerActionResult(content, confirmingStopSessionId) {
|
|
3409
|
+
return {
|
|
3410
|
+
type: "session-picker",
|
|
3411
|
+
content,
|
|
3412
|
+
sessions: Object.values(this.state.sessions ?? {}),
|
|
3413
|
+
activeSessionId: this.state.activeSessionId,
|
|
3414
|
+
confirmingStopSessionId
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
async handleResumePickerAction(action) {
|
|
3418
|
+
const sessionId = action.sessionId ?? "";
|
|
3419
|
+
const request = this.pendingResumePickers.get(sessionId);
|
|
3420
|
+
const session = this.state.sessions?.[sessionId];
|
|
3421
|
+
if (!request || !session || session.agent !== action.agent || session.paneId !== action.paneId) {
|
|
3422
|
+
return { type: "error", content: "Resume Picker \u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA Session\u3002" };
|
|
3423
|
+
}
|
|
3424
|
+
if (action.action === "cancel") {
|
|
3425
|
+
this.pendingResumePickers.delete(sessionId);
|
|
3426
|
+
const stopped = await this.stopSession(sessionId);
|
|
3427
|
+
return stopped.ok ? { type: "success", content: `\u5DF2\u53D6\u6D88\u521B\u5EFA ${sessionId}\u3002` } : { type: "error", content: remoteError(stopped) };
|
|
3428
|
+
}
|
|
3429
|
+
let picker = await this.readResumePicker(session);
|
|
3430
|
+
if (!picker) {
|
|
3431
|
+
this.pendingResumePickers.delete(sessionId);
|
|
3432
|
+
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" };
|
|
3433
|
+
}
|
|
3434
|
+
if (action.action !== "refresh" && picker.fingerprint !== action.fingerprint) {
|
|
3435
|
+
return { type: "resume-picker", content: "Picker \u5DF2\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u4E3A\u6700\u65B0\u5185\u5BB9\u3002", session, picker };
|
|
3436
|
+
}
|
|
3437
|
+
if (action.action === "refresh") {
|
|
3438
|
+
return { type: "resume-picker", content: "\u5DF2\u5237\u65B0 Resume Picker\u3002", session, picker };
|
|
3439
|
+
}
|
|
3440
|
+
if (action.action === "previous" || action.action === "next") {
|
|
3441
|
+
await this.tmux.sendKey(session.paneId, action.action === "previous" ? "PPage" : "NPage");
|
|
3442
|
+
picker = await this.waitForResumePicker(session, picker.fingerprint) ?? picker;
|
|
3443
|
+
return { type: "resume-picker", content: "\u5DF2\u5207\u6362 Picker \u9875\u9762\u3002", session, picker };
|
|
3444
|
+
}
|
|
3445
|
+
const optionId = action.action.startsWith("select:") ? action.action.slice("select:".length) : "";
|
|
3446
|
+
const option2 = picker.options.find((candidate) => candidate.id === optionId);
|
|
3447
|
+
if (!option2) return { type: "resume-picker", content: "\u6240\u9009\u9879\u5DF2\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u3002", session, picker };
|
|
3448
|
+
const delta = option2.visibleIndex - picker.selectedIndex;
|
|
3449
|
+
const key = delta < 0 ? "Up" : "Down";
|
|
3450
|
+
for (let step = 0; step < Math.abs(delta); step += 1) await this.tmux.sendKey(session.paneId, key);
|
|
3451
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
3452
|
+
if (!pane || pane.dead) {
|
|
3453
|
+
this.pendingResumePickers.delete(sessionId);
|
|
3454
|
+
return larkStartupError(fail(await this.startupExitedError(session, pane)), request);
|
|
3455
|
+
}
|
|
3456
|
+
await this.tmux.sendKey(session.paneId, "Enter");
|
|
3457
|
+
this.pendingResumePickers.delete(sessionId);
|
|
3458
|
+
const claimed = await this.waitForInitialAgentSessionClaim(sessionId, pane.pid);
|
|
3459
|
+
if (!claimed.ok) {
|
|
3460
|
+
await this.stopSession(sessionId).catch(() => void 0);
|
|
3461
|
+
if (claimed.errorCode === "AGENT_SESSION_IN_USE") {
|
|
3462
|
+
const ownerSessionId = typeof claimed.errorContext?.ownerSessionId === "string" ? claimed.errorContext.ownerSessionId : void 0;
|
|
3463
|
+
const owner = ownerSessionId ? this.state.sessions?.[ownerSessionId] : void 0;
|
|
3464
|
+
if (owner) {
|
|
3465
|
+
this.pendingStartupConflicts.set(sessionId, { request, ownerSessionId: owner.id });
|
|
3466
|
+
return {
|
|
3467
|
+
type: "startup-conflict",
|
|
3468
|
+
content: "\u6240\u9009\u539F\u751F Session \u5DF2\u7531\u73B0\u6709 LCA Session \u8FDE\u63A5\u3002",
|
|
3469
|
+
request,
|
|
3470
|
+
owner
|
|
3471
|
+
};
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return larkStartupError(claimed, request);
|
|
3475
|
+
}
|
|
3476
|
+
await this.tmux.preserveOnExit(session.sessionName, false).catch(() => void 0);
|
|
3477
|
+
const selected = await this.useSession(sessionId);
|
|
3478
|
+
if (!selected.ok) return { type: "error", content: remoteError(selected) };
|
|
3479
|
+
return { type: "session-created", content: remoteStartSuccess(session), session };
|
|
3480
|
+
}
|
|
3481
|
+
async readResumePicker(session) {
|
|
3482
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
3483
|
+
if (!pane || pane.dead) return void 0;
|
|
3484
|
+
const raw = await this.tmux.capture(session.paneId, 120).catch(() => "");
|
|
3485
|
+
return parseResumePicker(raw, session.agent);
|
|
3486
|
+
}
|
|
3487
|
+
async handleResumePickerDeliveryFailure(candidate) {
|
|
3488
|
+
const current = this.state.sessions?.[candidate.id];
|
|
3489
|
+
if (!current || current.paneId !== candidate.paneId || !this.pendingResumePickers.has(candidate.id)) return;
|
|
3490
|
+
await this.log(`resume picker delivery failed; rolling back provisional session: session=${candidate.id} pane=${candidate.paneId}`);
|
|
3491
|
+
await this.stopSession(candidate.id);
|
|
3492
|
+
}
|
|
3493
|
+
async waitForResumePicker(session, previousFingerprint, timeoutMs = 2500) {
|
|
3494
|
+
const deadline = Date.now() + timeoutMs;
|
|
3495
|
+
let latest;
|
|
3496
|
+
while (Date.now() < deadline) {
|
|
3497
|
+
latest = await this.readResumePicker(session);
|
|
3498
|
+
if (latest && (!previousFingerprint || latest.fingerprint !== previousFingerprint)) return latest;
|
|
3499
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3500
|
+
}
|
|
3501
|
+
return latest;
|
|
3502
|
+
}
|
|
2133
3503
|
currentManualView() {
|
|
2134
3504
|
const session = this.activeSession();
|
|
2135
3505
|
if (!session || !this.screen || this.screen.state === "exited") return void 0;
|
|
@@ -2176,7 +3546,7 @@ ${escapeFence2(output).slice(-6800)}
|
|
|
2176
3546
|
${escapeFence2(output).slice(-6500)}
|
|
2177
3547
|
\`\`\``);
|
|
2178
3548
|
} catch (error) {
|
|
2179
|
-
await this.gateway?.sendText(chatId, `\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF1A${
|
|
3549
|
+
await this.gateway?.sendText(chatId, `\u624B\u52A8\u64CD\u4F5C\u5931\u8D25\uFF1A${errorMessage3(error)}`);
|
|
2180
3550
|
}
|
|
2181
3551
|
}
|
|
2182
3552
|
async handleChoiceAction(action, session, event) {
|
|
@@ -2457,6 +3827,11 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
2457
3827
|
}
|
|
2458
3828
|
const sessions = { ...this.state.sessions };
|
|
2459
3829
|
delete sessions[sessionId];
|
|
3830
|
+
this.pendingResumePickers.delete(sessionId);
|
|
3831
|
+
this.pendingStartupConflicts.delete(sessionId);
|
|
3832
|
+
for (const [pendingId, conflict] of this.pendingStartupConflicts) {
|
|
3833
|
+
if (conflict.ownerSessionId === sessionId) this.pendingStartupConflicts.delete(pendingId);
|
|
3834
|
+
}
|
|
2460
3835
|
const wasActive = this.state.activeSessionId === sessionId;
|
|
2461
3836
|
const activeSessionId = wasActive ? Object.keys(sessions)[0] : this.state.activeSessionId;
|
|
2462
3837
|
this.state = {
|
|
@@ -2515,7 +3890,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
2515
3890
|
}
|
|
2516
3891
|
async runScheduledPoll() {
|
|
2517
3892
|
const operation = this.poll().catch(async (error) => {
|
|
2518
|
-
if (!this.closing) await this.log(`poll failed: ${
|
|
3893
|
+
if (!this.closing) await this.log(`poll failed: ${errorMessage3(error)}`);
|
|
2519
3894
|
});
|
|
2520
3895
|
this.pollInFlight = operation;
|
|
2521
3896
|
try {
|
|
@@ -2549,6 +3924,7 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
2549
3924
|
await this.maybeNotifyCompletion();
|
|
2550
3925
|
await this.flushPending();
|
|
2551
3926
|
await this.reconcileSessions();
|
|
3927
|
+
await this.refreshNativeAgentSessionClaims();
|
|
2552
3928
|
}
|
|
2553
3929
|
async notifyTransition() {
|
|
2554
3930
|
const current = this.screen;
|
|
@@ -2569,13 +3945,13 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
2569
3945
|
await this.gateway.sendText(this.state.boundChatId, `${agentName}/tmux pane \u5DF2\u9000\u51FA\u3002`);
|
|
2570
3946
|
}
|
|
2571
3947
|
} catch (error) {
|
|
2572
|
-
await this.log(`notification failed: ${
|
|
3948
|
+
await this.log(`notification failed: ${errorMessage3(error)}`);
|
|
2573
3949
|
}
|
|
2574
3950
|
}
|
|
2575
3951
|
async maybeNotifyUnresolved() {
|
|
2576
3952
|
const screen = this.screen;
|
|
2577
3953
|
const session = this.activeSession();
|
|
2578
|
-
if (this.interactionNotificationsSuppressed > 0 || !screen || !session || !this.gateway || !this.state.boundChatId || screen.state !== "input" && screen.state !== "unknown" || safeStructuredInteraction(screen)) {
|
|
3954
|
+
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
3955
|
this.unresolvedCandidate = void 0;
|
|
2580
3956
|
return;
|
|
2581
3957
|
}
|
|
@@ -2591,16 +3967,30 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
2591
3967
|
this.state.boundChatId,
|
|
2592
3968
|
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
3969
|
);
|
|
3970
|
+
await this.log(
|
|
3971
|
+
`manual fallback sent: session=${session.id} agent=${session.agent} pane=${session.paneId} state=${screen.state} fingerprint=${screen.fingerprint}`
|
|
3972
|
+
);
|
|
2594
3973
|
} catch (error) {
|
|
2595
|
-
await this.log(`manual fallback card failed: ${
|
|
3974
|
+
await this.log(`manual fallback card failed: ${errorMessage3(error)}`);
|
|
2596
3975
|
await this.gateway.sendText(
|
|
2597
3976
|
this.state.boundChatId,
|
|
2598
3977
|
"\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: ${
|
|
3978
|
+
).catch((sendError) => this.log(`manual fallback text failed: ${errorMessage3(sendError)}`));
|
|
2600
3979
|
}
|
|
2601
3980
|
}
|
|
2602
3981
|
async handleTurnComplete(candidate) {
|
|
2603
3982
|
if (!validTurnCompleteCandidate(candidate)) return { ok: false, error: "invalid turn-complete candidate" };
|
|
3983
|
+
const completingSession = this.state.sessions?.[candidate.sessionId];
|
|
3984
|
+
if (completingSession) {
|
|
3985
|
+
const claimed = await this.handleAgentSessionStarted({
|
|
3986
|
+
sessionId: candidate.sessionId,
|
|
3987
|
+
agent: completingSession.agent,
|
|
3988
|
+
agentSessionId: candidate.agentSessionId,
|
|
3989
|
+
cwd: candidate.cwd,
|
|
3990
|
+
source: "Stop"
|
|
3991
|
+
});
|
|
3992
|
+
if (!claimed.ok) return claimed;
|
|
3993
|
+
}
|
|
2604
3994
|
const eventKey = `${candidate.agentSessionId}:${candidate.eventId}`;
|
|
2605
3995
|
if (this.completedEvents.has(eventKey)) return { ok: true };
|
|
2606
3996
|
remember(this.completedEvents, eventKey, 256);
|
|
@@ -2610,6 +4000,142 @@ ${escapeFence2(output).slice(-6500)}
|
|
|
2610
4000
|
this.pendingCompletionAt = Date.now();
|
|
2611
4001
|
return { ok: true };
|
|
2612
4002
|
}
|
|
4003
|
+
async handleAgentSessionStarted(candidate) {
|
|
4004
|
+
if (!validSessionStartCandidate(candidate)) return { ok: false, error: "invalid agent-session candidate" };
|
|
4005
|
+
const session = this.state.sessions?.[candidate.sessionId];
|
|
4006
|
+
if (session && session.agent !== candidate.agent) {
|
|
4007
|
+
return { ok: false, error: "agent-session candidate does not match managed session" };
|
|
4008
|
+
}
|
|
4009
|
+
const owner = this.findAgentSessionOwner(candidate.agent, candidate.agentSessionId, candidate.sessionId);
|
|
4010
|
+
if (owner) {
|
|
4011
|
+
this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
|
|
4012
|
+
await this.log(
|
|
4013
|
+
`agent session conflict: session=${candidate.sessionId} agent=${candidate.agent} agentSession=${candidate.agentSessionId} owner=${owner.id}`
|
|
4014
|
+
);
|
|
4015
|
+
setTimeout(() => void this.rejectDuplicateAgentSession(candidate, owner), 100);
|
|
4016
|
+
return fail(agentSessionInUse(candidate.sessionId, owner.id));
|
|
4017
|
+
}
|
|
4018
|
+
if (!session) {
|
|
4019
|
+
this.pendingAgentSessionClaims.set(candidate.sessionId, candidate);
|
|
4020
|
+
return { ok: true };
|
|
4021
|
+
}
|
|
4022
|
+
if (session.agentSessionId === candidate.agentSessionId) return { ok: true };
|
|
4023
|
+
const updated = { ...session, agentSessionId: candidate.agentSessionId, updatedAt: Date.now() };
|
|
4024
|
+
this.state = {
|
|
4025
|
+
...this.state,
|
|
4026
|
+
sessions: { ...this.state.sessions, [session.id]: updated },
|
|
4027
|
+
updatedAt: Date.now()
|
|
4028
|
+
};
|
|
4029
|
+
this.pendingAgentSessionClaims.delete(session.id);
|
|
4030
|
+
await this.store.saveState(this.state);
|
|
4031
|
+
await this.tmux.writeMetadata(session.sessionName, {
|
|
4032
|
+
managed: true,
|
|
4033
|
+
sessionId: session.id,
|
|
4034
|
+
agent: session.agent,
|
|
4035
|
+
cwd: session.cwd,
|
|
4036
|
+
agentVersion: session.agentVersion,
|
|
4037
|
+
agentSessionId: candidate.agentSessionId
|
|
4038
|
+
}).catch((error) => this.log(`failed to persist agent session claim for ${session.id}: ${errorMessage3(error)}`));
|
|
4039
|
+
await this.log(
|
|
4040
|
+
`agent session claimed: session=${session.id} agent=${session.agent} agentSession=${candidate.agentSessionId}`
|
|
4041
|
+
);
|
|
4042
|
+
return { ok: true };
|
|
4043
|
+
}
|
|
4044
|
+
async refreshNativeAgentSessionClaims() {
|
|
4045
|
+
for (const session of Object.values(this.state.sessions ?? {})) {
|
|
4046
|
+
if (session.agentSessionId) continue;
|
|
4047
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
4048
|
+
if (!pane || pane.dead) continue;
|
|
4049
|
+
const agentSessionId = await resolveNativeAgentSessionId(session.agent, pane.pid).catch((error) => {
|
|
4050
|
+
void this.log(`failed to resolve native agent session for ${session.id}: ${errorMessage3(error)}`);
|
|
4051
|
+
return void 0;
|
|
4052
|
+
});
|
|
4053
|
+
if (!agentSessionId) continue;
|
|
4054
|
+
await this.handleAgentSessionStarted({
|
|
4055
|
+
sessionId: session.id,
|
|
4056
|
+
agent: session.agent,
|
|
4057
|
+
agentSessionId,
|
|
4058
|
+
cwd: session.cwd,
|
|
4059
|
+
source: "runtime-discovery"
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
async waitForInitialAgentSessionClaim(sessionId, panePid) {
|
|
4064
|
+
const deadline = Date.now() + 3500;
|
|
4065
|
+
while (Date.now() < deadline) {
|
|
4066
|
+
const session2 = this.state.sessions?.[sessionId];
|
|
4067
|
+
if (!session2) return { ok: false, error: `session disappeared during startup: ${sessionId}` };
|
|
4068
|
+
if (session2.agentSessionId) return { ok: true };
|
|
4069
|
+
const pane2 = await this.tmux.inspect(session2.paneId);
|
|
4070
|
+
if (!pane2 || pane2.dead) return fail(await this.startupExitedError(session2, pane2));
|
|
4071
|
+
const agentSessionId = await resolveNativeAgentSessionId(session2.agent, panePid).catch(() => void 0);
|
|
4072
|
+
if (agentSessionId) {
|
|
4073
|
+
return this.handleAgentSessionStarted({
|
|
4074
|
+
sessionId,
|
|
4075
|
+
agent: session2.agent,
|
|
4076
|
+
agentSessionId,
|
|
4077
|
+
cwd: session2.cwd,
|
|
4078
|
+
source: "startup-discovery"
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4082
|
+
}
|
|
4083
|
+
const session = this.state.sessions?.[sessionId];
|
|
4084
|
+
if (!session) return fail(new AppError("SESSION_NOT_FOUND", `session disappeared during startup: ${sessionId}`, { sessionId }));
|
|
4085
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
4086
|
+
if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
|
|
4087
|
+
if (session.agentSessionId || this.pendingAgentSessionClaims.has(sessionId)) return { ok: true };
|
|
4088
|
+
return fail(new AppError(
|
|
4089
|
+
"AGENT_IDENTITY_TIMEOUT",
|
|
4090
|
+
`unable to identify resumed native session: ${sessionId}`,
|
|
4091
|
+
{ sessionId, agent: session.agent }
|
|
4092
|
+
));
|
|
4093
|
+
}
|
|
4094
|
+
async startupExitedError(session, pane) {
|
|
4095
|
+
const terminalTail = await this.tmux.capture(session.paneId, 40).then((output) => tailScreen(output, 40).slice(-3e3)).catch(() => "");
|
|
4096
|
+
return new AppError(
|
|
4097
|
+
"AGENT_EXITED_DURING_STARTUP",
|
|
4098
|
+
`agent exited during startup: ${session.id}`,
|
|
4099
|
+
{
|
|
4100
|
+
sessionId: session.id,
|
|
4101
|
+
agent: session.agent,
|
|
4102
|
+
terminalExcerpt: startupTerminalExcerpt(terminalTail),
|
|
4103
|
+
exitStatus: pane?.exitStatus
|
|
4104
|
+
}
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
async waitForStartupStability(session, durationMs) {
|
|
4108
|
+
const deadline = Date.now() + durationMs;
|
|
4109
|
+
while (Date.now() < deadline) {
|
|
4110
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
4111
|
+
if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
|
|
4112
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
4113
|
+
}
|
|
4114
|
+
return { ok: true };
|
|
4115
|
+
}
|
|
4116
|
+
findAgentSessionOwner(agent, agentSessionId, excludeSessionId) {
|
|
4117
|
+
const persisted = Object.values(this.state.sessions ?? {}).find((candidate) => candidate.id !== excludeSessionId && candidate.agent === agent && candidate.agentSessionId === agentSessionId);
|
|
4118
|
+
if (persisted) return persisted;
|
|
4119
|
+
const pending = [...this.pendingAgentSessionClaims.values()].find((candidate) => candidate.sessionId !== excludeSessionId && candidate.agent === agent && candidate.agentSessionId === agentSessionId);
|
|
4120
|
+
return pending ? { id: pending.sessionId } : void 0;
|
|
4121
|
+
}
|
|
4122
|
+
async rejectDuplicateAgentSession(candidate, owner) {
|
|
4123
|
+
const duplicate = this.state.sessions?.[candidate.sessionId];
|
|
4124
|
+
if (duplicate) await this.stopSession(duplicate.id).catch((error) => this.log(
|
|
4125
|
+
`failed to stop duplicate agent session ${duplicate.id}: ${errorMessage3(error)}`
|
|
4126
|
+
));
|
|
4127
|
+
else {
|
|
4128
|
+
const sessionName = `${this.sessionName}-${candidate.sessionId}`;
|
|
4129
|
+
if (await this.tmux.hasSession(sessionName)) await this.tmux.killSession(sessionName).catch(() => void 0);
|
|
4130
|
+
}
|
|
4131
|
+
this.pendingAgentSessionClaims.delete(candidate.sessionId);
|
|
4132
|
+
if (this.state.boundChatId) {
|
|
4133
|
+
await this.gateway?.sendText(
|
|
4134
|
+
this.state.boundChatId,
|
|
4135
|
+
`${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`
|
|
4136
|
+
).catch((error) => this.log(`agent session conflict notification failed: ${errorMessage3(error)}`));
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
2613
4139
|
updateScreen(next) {
|
|
2614
4140
|
if (this.screen?.fingerprint !== next.fingerprint || this.screen.state !== next.state) {
|
|
2615
4141
|
this.outputStableSince = Date.now();
|
|
@@ -2661,31 +4187,29 @@ ${output}`
|
|
|
2661
4187
|
activeSession() {
|
|
2662
4188
|
return this.state.activeSessionId ? this.state.sessions?.[this.state.activeSessionId] : void 0;
|
|
2663
4189
|
}
|
|
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) {
|
|
4190
|
+
async reconcileSessions(discover = false) {
|
|
4191
|
+
const previousActive = this.state.activeSessionId;
|
|
4192
|
+
const result2 = await this.reconciler.reconcile(this.state, discover);
|
|
4193
|
+
if (!result2.changed) return result2.liveSessions;
|
|
4194
|
+
const activeChanged = result2.state.activeSessionId !== previousActive;
|
|
4195
|
+
if (result2.removedActive) {
|
|
4196
|
+
await this.log(
|
|
4197
|
+
`session removed: session=${result2.removedActive.id} agent=${result2.removedActive.agent} pane=${result2.removedActive.paneId} wasActive=true nextActive=${result2.state.activeSessionId ?? "-"}`
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
if (activeChanged && result2.removedActive && this.previousScreen?.state !== "exited" && this.state.boundChatId) {
|
|
2678
4201
|
await this.gateway?.sendText(
|
|
2679
4202
|
this.state.boundChatId,
|
|
2680
|
-
`${getAgentAdapter(removedActive.agent).displayName}/tmux pane \u5DF2\u9000\u51FA\u3002`
|
|
2681
|
-
).catch((error) => this.log(`exit notification failed: ${
|
|
4203
|
+
`${getAgentAdapter(result2.removedActive.agent).displayName}/tmux pane \u5DF2\u9000\u51FA\u3002`
|
|
4204
|
+
).catch((error) => this.log(`exit notification failed: ${errorMessage3(error)}`));
|
|
4205
|
+
}
|
|
4206
|
+
this.state = result2.state;
|
|
4207
|
+
for (const sessionId of this.pendingResumePickers.keys()) {
|
|
4208
|
+
if (!this.state.sessions?.[sessionId]) this.pendingResumePickers.delete(sessionId);
|
|
4209
|
+
}
|
|
4210
|
+
for (const [sessionId, conflict] of this.pendingStartupConflicts) {
|
|
4211
|
+
if (!this.state.sessions?.[conflict.ownerSessionId]) this.pendingStartupConflicts.delete(sessionId);
|
|
2682
4212
|
}
|
|
2683
|
-
this.state = {
|
|
2684
|
-
...this.state,
|
|
2685
|
-
sessions: liveById,
|
|
2686
|
-
activeSessionId,
|
|
2687
|
-
updatedAt: Date.now()
|
|
2688
|
-
};
|
|
2689
4213
|
if (activeChanged) {
|
|
2690
4214
|
this.screen = void 0;
|
|
2691
4215
|
this.previousScreen = void 0;
|
|
@@ -2697,11 +4221,13 @@ ${output}`
|
|
|
2697
4221
|
this.unresolvedNotified.clear();
|
|
2698
4222
|
}
|
|
2699
4223
|
await this.store.saveState(this.state);
|
|
2700
|
-
return liveSessions;
|
|
4224
|
+
return result2.liveSessions;
|
|
2701
4225
|
}
|
|
2702
4226
|
async useSession(sessionId) {
|
|
2703
4227
|
const session = this.state.sessions?.[sessionId];
|
|
2704
|
-
if (!session
|
|
4228
|
+
if (!session) return fail(new AppError("SESSION_NOT_FOUND", `unknown session: ${sessionId}`, { sessionId }));
|
|
4229
|
+
const pane = await this.tmux.inspect(session.paneId);
|
|
4230
|
+
if (!pane || pane.dead) return fail(await this.startupExitedError(session, pane));
|
|
2705
4231
|
this.state = { ...this.state, activeSessionId: sessionId, updatedAt: Date.now() };
|
|
2706
4232
|
this.screen = void 0;
|
|
2707
4233
|
this.previousScreen = void 0;
|
|
@@ -2713,8 +4239,44 @@ ${output}`
|
|
|
2713
4239
|
this.unresolvedNotified.clear();
|
|
2714
4240
|
await this.store.saveState(this.state);
|
|
2715
4241
|
await this.poll();
|
|
4242
|
+
await this.log(`session activated: session=${session.id} agent=${session.agent} pane=${session.paneId}`);
|
|
2716
4243
|
return { ok: true, value: session };
|
|
2717
4244
|
}
|
|
4245
|
+
async startRemoteSession(request) {
|
|
4246
|
+
await this.log(
|
|
4247
|
+
`remote session create requested: session=${request.sessionId} agent=${request.agent} resume=${request.resume?.mode ?? "new"}`
|
|
4248
|
+
);
|
|
4249
|
+
const started = await this.startSession(request.sessionId, request.cwd, request.agent, request.resume);
|
|
4250
|
+
if (!started.ok) {
|
|
4251
|
+
await this.log(`remote session create failed: session=${request.sessionId} code=${started.errorCode ?? "UNKNOWN"}`);
|
|
4252
|
+
if (started.errorCode === "AGENT_SESSION_IN_USE") {
|
|
4253
|
+
const ownerSessionId = typeof started.errorContext?.ownerSessionId === "string" ? started.errorContext.ownerSessionId : void 0;
|
|
4254
|
+
const owner = ownerSessionId ? this.state.sessions?.[ownerSessionId] : void 0;
|
|
4255
|
+
if (owner) {
|
|
4256
|
+
this.pendingStartupConflicts.set(request.sessionId, { request, ownerSessionId: owner.id });
|
|
4257
|
+
return { ok: true, state: "conflict", request, owner };
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
return { ok: false, error: started };
|
|
4261
|
+
}
|
|
4262
|
+
const selected = await this.useSession(request.sessionId);
|
|
4263
|
+
if (!selected.ok) {
|
|
4264
|
+
await this.log(`remote session activation failed: session=${request.sessionId} code=${selected.errorCode ?? "UNKNOWN"}`);
|
|
4265
|
+
return { ok: false, error: selected };
|
|
4266
|
+
}
|
|
4267
|
+
const session = selected.value;
|
|
4268
|
+
if (request.resume?.mode === "picker") {
|
|
4269
|
+
const picker = await this.waitForResumePicker(session);
|
|
4270
|
+
if (picker) {
|
|
4271
|
+
this.pendingResumePickers.set(session.id, request);
|
|
4272
|
+
return { ok: true, state: "picker", session, picker };
|
|
4273
|
+
}
|
|
4274
|
+
await this.tmux.preserveOnExit(session.sessionName, false).catch((error) => this.log(
|
|
4275
|
+
`failed to disable remain-on-exit after resume picker fallback: session=${session.id} error=${errorMessage3(error)}`
|
|
4276
|
+
));
|
|
4277
|
+
}
|
|
4278
|
+
return { ok: true, state: "ready", session };
|
|
4279
|
+
}
|
|
2718
4280
|
async log(message) {
|
|
2719
4281
|
await appendFile(this.paths.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}
|
|
2720
4282
|
`, { mode: 384 });
|
|
@@ -2723,7 +4285,14 @@ ${output}`
|
|
|
2723
4285
|
function fail(error) {
|
|
2724
4286
|
return { ok: false, ...serializeAppError(error) };
|
|
2725
4287
|
}
|
|
2726
|
-
function
|
|
4288
|
+
function agentSessionInUse(sessionId, ownerSessionId) {
|
|
4289
|
+
return new AppError(
|
|
4290
|
+
"AGENT_SESSION_IN_USE",
|
|
4291
|
+
`agent session is already managed by ${ownerSessionId}`,
|
|
4292
|
+
{ sessionId, ownerSessionId }
|
|
4293
|
+
);
|
|
4294
|
+
}
|
|
4295
|
+
function errorMessage3(error) {
|
|
2727
4296
|
return error instanceof Error ? error.message : String(error);
|
|
2728
4297
|
}
|
|
2729
4298
|
function isAlreadyExists(error) {
|
|
@@ -2737,9 +4306,6 @@ function processIsAlive(pid) {
|
|
|
2737
4306
|
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
2738
4307
|
}
|
|
2739
4308
|
}
|
|
2740
|
-
function validSessionId(value) {
|
|
2741
|
-
return /^[a-zA-Z0-9_-]{1,40}$/.test(value);
|
|
2742
|
-
}
|
|
2743
4309
|
function remember(values, value, limit) {
|
|
2744
4310
|
values.add(value);
|
|
2745
4311
|
while (values.size > limit) {
|
|
@@ -2748,6 +4314,76 @@ function remember(values, value, limit) {
|
|
|
2748
4314
|
values.delete(oldest);
|
|
2749
4315
|
}
|
|
2750
4316
|
}
|
|
4317
|
+
function startRequestFromForm(values) {
|
|
4318
|
+
const sessionId = formString(values[SESSION_CREATE_NAME_FIELD]);
|
|
4319
|
+
const agentValue = formString(values[SESSION_CREATE_AGENT_FIELD]);
|
|
4320
|
+
const cwd = formString(values[SESSION_CREATE_CWD_FIELD]);
|
|
4321
|
+
const resumeMode = formString(values[SESSION_CREATE_RESUME_FIELD]) || "new";
|
|
4322
|
+
if (!sessionId) return { ok: false, error: "\u8BF7\u586B\u5199 Session \u540D\u79F0\u3002" };
|
|
4323
|
+
const agent = normalizeAgentId(agentValue);
|
|
4324
|
+
if (!agent) return { ok: false, error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684 Agent\uFF1Acodex\u3001traex \u6216 claude\u3002" };
|
|
4325
|
+
if (!cwd) return { ok: false, error: "\u8BF7\u586B\u5199\u7EDD\u5BF9\u5DE5\u4F5C\u76EE\u5F55\u3002" };
|
|
4326
|
+
let resume;
|
|
4327
|
+
if (resumeMode === "last") {
|
|
4328
|
+
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" };
|
|
4329
|
+
}
|
|
4330
|
+
if (resumeMode === "picker") resume = { mode: "picker" };
|
|
4331
|
+
else if (resumeMode !== "new") {
|
|
4332
|
+
return { ok: false, error: "\u65E0\u6CD5\u8BC6\u522B\u542F\u52A8\u65B9\u5F0F\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u65B0\u5EFA Session \u8868\u5355\u3002" };
|
|
4333
|
+
}
|
|
4334
|
+
return { ok: true, value: { sessionId, agent, cwd, resume } };
|
|
4335
|
+
}
|
|
4336
|
+
function formString(value) {
|
|
4337
|
+
if (typeof value === "string") return value.trim();
|
|
4338
|
+
if (value && typeof value === "object" && typeof value.value === "string") {
|
|
4339
|
+
return value.value.trim();
|
|
4340
|
+
}
|
|
4341
|
+
return "";
|
|
4342
|
+
}
|
|
4343
|
+
function remoteStartSuccess(session) {
|
|
4344
|
+
return `\u5DF2\u542F\u52A8\u5E76\u8FDE\u63A5 ${getAgentAdapter(session.agent).displayName} session\u300C${session.id}\u300D\u3002`;
|
|
4345
|
+
}
|
|
4346
|
+
function larkStartupError(result2, request) {
|
|
4347
|
+
const failure = sessionStartupFailure(result2, request);
|
|
4348
|
+
return failure ? { type: "session-start-failed", content: `${failure.agent} \u542F\u52A8\u5931\u8D25\u3002`, failure } : { type: "error", content: remoteError(result2) };
|
|
4349
|
+
}
|
|
4350
|
+
function remoteError(result2) {
|
|
4351
|
+
if (result2.ok) return "\u64CD\u4F5C\u5DF2\u5B8C\u6210\u3002";
|
|
4352
|
+
const context = result2.errorContext ?? {};
|
|
4353
|
+
const sessionId = typeof context.sessionId === "string" ? context.sessionId : "\u8BE5\u540D\u79F0";
|
|
4354
|
+
switch (result2.errorCode) {
|
|
4355
|
+
case "SESSION_EXISTS":
|
|
4356
|
+
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`;
|
|
4357
|
+
case "AGENT_SESSION_IN_USE": {
|
|
4358
|
+
const ownerSessionId = typeof context.ownerSessionId === "string" ? context.ownerSessionId : "\u73B0\u6709 session";
|
|
4359
|
+
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`;
|
|
4360
|
+
}
|
|
4361
|
+
case "AGENT_EXITED_DURING_STARTUP": {
|
|
4362
|
+
const agent = typeof context.agent === "string" ? context.agent : "Agent";
|
|
4363
|
+
const exitStatus = typeof context.exitStatus === "number" ? `\uFF08\u9000\u51FA\u7801 ${context.exitStatus}\uFF09` : "";
|
|
4364
|
+
const excerpt = typeof context.terminalExcerpt === "string" ? context.terminalExcerpt : "Agent \u672A\u8F93\u51FA\u53EF\u7528\u9519\u8BEF\u4FE1\u606F\u3002";
|
|
4365
|
+
return `\u65E0\u6CD5\u542F\u52A8 session\u300C${sessionId}\u300D\uFF1A${agent} \u542F\u52A8\u540E\u7ACB\u5373\u9000\u51FA${exitStatus}\u3002
|
|
4366
|
+
|
|
4367
|
+
\u539F\u59CB\u9519\u8BEF\uFF1A
|
|
4368
|
+
${excerpt}`;
|
|
4369
|
+
}
|
|
4370
|
+
case "AGENT_IDENTITY_TIMEOUT":
|
|
4371
|
+
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`;
|
|
4372
|
+
case "INVALID_SESSION_NAME":
|
|
4373
|
+
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";
|
|
4374
|
+
case "INVALID_CWD":
|
|
4375
|
+
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`;
|
|
4376
|
+
case "BINARY_NOT_FOUND":
|
|
4377
|
+
return `\u627E\u4E0D\u5230 Agent \u547D\u4EE4\uFF1A${typeof context.binary === "string" ? context.binary : "\u8BF7\u68C0\u67E5\u5B89\u88C5\u4E0E PATH"}\u3002`;
|
|
4378
|
+
case "INVALID_OPTIONS":
|
|
4379
|
+
case "INVALID_RESUME":
|
|
4380
|
+
return typeof context.reason === "string" ? context.reason : "\u542F\u52A8\u53C2\u6570\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u540E\u91CD\u8BD5\u3002";
|
|
4381
|
+
case "START_FAILED":
|
|
4382
|
+
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`;
|
|
4383
|
+
default:
|
|
4384
|
+
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";
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
2751
4387
|
function escapeFence2(value) {
|
|
2752
4388
|
return value.replace(/```/g, "``\\`");
|
|
2753
4389
|
}
|
|
@@ -2794,27 +4430,27 @@ function manualTimestamp() {
|
|
|
2794
4430
|
|
|
2795
4431
|
// src/core/paths.ts
|
|
2796
4432
|
import { homedir as homedir2 } from "os";
|
|
2797
|
-
import { join } from "path";
|
|
4433
|
+
import { join as join2 } from "path";
|
|
2798
4434
|
function resolveAppPaths(root = process.env.LARK_CODING_ASSISTANT_HOME) {
|
|
2799
|
-
const base = root ||
|
|
4435
|
+
const base = root || join2(homedir2(), ".lark-coding-assistant");
|
|
2800
4436
|
return {
|
|
2801
4437
|
root: base,
|
|
2802
|
-
config:
|
|
2803
|
-
secrets:
|
|
2804
|
-
state:
|
|
2805
|
-
logsDir:
|
|
2806
|
-
logFile:
|
|
2807
|
-
runtimeDir:
|
|
2808
|
-
socket:
|
|
2809
|
-
pid:
|
|
4438
|
+
config: join2(base, "config.json"),
|
|
4439
|
+
secrets: join2(base, "secrets.json"),
|
|
4440
|
+
state: join2(base, "state.json"),
|
|
4441
|
+
logsDir: join2(base, "logs"),
|
|
4442
|
+
logFile: join2(base, "logs", "assistant.log"),
|
|
4443
|
+
runtimeDir: join2(base, "runtime"),
|
|
4444
|
+
socket: join2(base, "runtime", "daemon.sock"),
|
|
4445
|
+
pid: join2(base, "runtime", "daemon.pid")
|
|
2810
4446
|
};
|
|
2811
4447
|
}
|
|
2812
4448
|
|
|
2813
4449
|
// src/daemon-entry.ts
|
|
2814
|
-
import { readFile as
|
|
4450
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2815
4451
|
var paths = resolveAppPaths();
|
|
2816
4452
|
var packageInfo = JSON.parse(
|
|
2817
|
-
await
|
|
4453
|
+
await readFile4(new URL("../package.json", import.meta.url), "utf8")
|
|
2818
4454
|
);
|
|
2819
4455
|
var daemon = new AssistantDaemon(
|
|
2820
4456
|
new AppStore(paths),
|