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