@keyou007/agent-bot 0.1.19-alpha.1 → 0.1.20-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +2 -2
- package/dist/cli.js +5 -4
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +59 -1
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/feishu/CardRenderer.js +10 -6
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +34 -73
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/docs/technical-reference.md +1 -1
- package/docs/technical-reference.zh.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +2 -1
- package/src/cli/controlProtocol.ts +0 -1
- package/src/cli/help.ts +2 -2
- package/src/cli.ts +6 -7
- package/src/codex/CodexRuntime.ts +79 -1
- package/src/feishu/CardRenderer.ts +11 -7
- package/src/proxy/ProxySessionController.ts +39 -78
- package/src/runtime/types.ts +1 -0
|
@@ -261,7 +261,7 @@ Starting a Feishu thread from a mapped user message, progress card, or final res
|
|
|
261
261
|
|
|
262
262
|
`/forkgroup` has thread-aware source selection. An unbound thread, or a bound thread task with no completed turn of its own, forks directly from the thread's original anchor turn without creating an intermediate thread task. Once the thread task has completed a turn, `/forkgroup` uses its latest locally persisted completed turn. A newer active turn does not block the command and is excluded from the fork point.
|
|
263
263
|
|
|
264
|
-
Agent Bot sends the experimental `excludeTurns: true` field on every `thread/fork` request. This suppresses populated `thread.turns` in the response without changing the history copied into the fork. The App Server connection enables `experimentalApi`; no user-facing command option is required. If an older App Server explicitly rejects
|
|
264
|
+
Before forking a task that has never been bound to Agent Bot, Agent Bot reads metadata without Turns and pages backward through the lightweight `summary` Turn view only until it finds the latest completed Turn. It does not load the task's complete tool history as a Fork prerequisite. Agent Bot then sends the experimental `excludeTurns: true` field on every `thread/fork` request. This suppresses populated `thread.turns` in the response without changing the history copied into the fork. The App Server connection enables `experimentalApi`; no user-facing command option is required. If an older App Server explicitly rejects the lightweight Turn-list method or the `excludeTurns` field, Agent Bot falls back to the compatible complete-history read or Fork request. Timeouts, disconnects, and unrelated Fork errors are never retried because a timed-out Fork request may already have created a branch.
|
|
265
265
|
|
|
266
266
|
The new group's welcome message reports the persisted fork settings: Provider, model, reasoning effort, and permission type. Permission type is rendered as automatic execution or confirmation before execution.
|
|
267
267
|
|
|
@@ -261,7 +261,7 @@ AGENT_BOT_LARK_USER_OPEN_ID=<已保存的授权用户 open_id,可用时提供>
|
|
|
261
261
|
|
|
262
262
|
`/forkgroup` 会根据话题状态选择来源。话题尚未绑定任务,或者已绑定任务但尚未完成过自己的 turn 时,直接从话题原始锚点 turn fork,不创建中间话题任务。话题任务完成过 turn 后,使用本地持久化的最近完成 turn;更晚的执行中 turn 不会阻塞命令,也不会成为 fork 点。
|
|
263
263
|
|
|
264
|
-
Agent Bot
|
|
264
|
+
Fork 从未绑定过 Agent Bot 的任务前,Agent Bot 会先读取不含 Turns 的元数据,再通过轻量的 `summary` Turn 视图从新到旧分页,只读到最近一个已完成 Turn,不再把完整工具历史作为 Fork 前置条件。随后每个 `thread/fork` 请求仍默认发送实验性的 `excludeTurns: true`;它只阻止响应填充 `thread.turns`,不会改变复制到分支中的历史。App Server 连接已启用 `experimentalApi`,不需要用户提供命令参数。如果旧版 App Server 明确不支持轻量 Turn 列表或 `excludeTurns` 字段,Agent Bot 会分别回退到兼容的完整历史读取或 Fork 请求。超时、断连和无关的 Fork 错误绝不重试,因为超时的 Fork 请求可能已经创建分支。
|
|
265
265
|
|
|
266
266
|
新群欢迎消息会显示分支任务持久化后的 Provider、模型、思考强度和权限类型;权限类型显示为自动执行或执行前确认。
|
|
267
267
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20-alpha.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@keyou007/agent-bot",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.20-alpha.0",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "1.2.1",
|
package/package.json
CHANGED
|
@@ -74,9 +74,10 @@ agentbot task title "<title>"
|
|
|
74
74
|
- `prompt` posts the Prompt to the task's Feishu conversation before submitting it.
|
|
75
75
|
- `queue` creates a later turn instead of steering the active turn. `nosteer` is an alias.
|
|
76
76
|
- `stop` requests an Agent interrupt; it does not kill the Agent process.
|
|
77
|
+
- `release` sends a Lark confirmation card for releasing the Agent's shared App Server. It never releases automatically; the user must click **Release** or **Release Now** on the card.
|
|
77
78
|
- `dismiss --yes` irreversibly dissolves the task's current Feishu group and archives its current task. Use it only after the user explicitly confirms.
|
|
78
79
|
|
|
79
|
-
From Feishu, `/release`
|
|
80
|
+
From Feishu, `/release` sends a confirmation card for handing App Server tasks back to Codex Desktop without archiving them. It lists the current Agent's running and queued work and never releases automatically. **Release** confirms an idle release, **Release Now** interrupts active work and clears queued Prompts, and **Cancel** cancels the pending release. Because each App Server Agent is shared, releasing affects every task loaded by that Agent process.
|
|
80
81
|
|
|
81
82
|
Create or branch work in the same conversation:
|
|
82
83
|
|
package/src/cli/help.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
|
|
|
45
45
|
task shell [任务] <命令> 在任务目录执行本地命令
|
|
46
46
|
task stop [任务] 停止任务
|
|
47
47
|
task archive [任务] 归档任务
|
|
48
|
-
task release [任务]
|
|
48
|
+
task release [任务] 发送共享 App Server 释放确认卡片
|
|
49
49
|
task dismiss [任务] --yes 归档任务并解散其当前群聊
|
|
50
50
|
task title [任务] <标题> 重命名任务
|
|
51
51
|
task restart [任务] [--force] 从任务会话请求安全或立即重启
|
|
@@ -138,7 +138,7 @@ Task commands:
|
|
|
138
138
|
task shell [task] <command> Run a local command in the task directory
|
|
139
139
|
task stop [task] Stop a task
|
|
140
140
|
task archive [task] Archive a task
|
|
141
|
-
task release [task]
|
|
141
|
+
task release [task] Send a confirmation card to release the shared App Server
|
|
142
142
|
task dismiss [task] --yes Archive a task and dissolve its current group
|
|
143
143
|
task title [task] <title> Rename a task
|
|
144
144
|
task restart [task] [--force] Request a safe or immediate restart from a task
|
package/src/cli.ts
CHANGED
|
@@ -1337,15 +1337,14 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
1337
1337
|
`App Server tasks released for ${released.agentName}.\n`,
|
|
1338
1338
|
`已释放 ${released.agentName} 的 App Server 任务。\n`,
|
|
1339
1339
|
));
|
|
1340
|
-
} else if (released.releaseInSeconds !== undefined) {
|
|
1341
|
-
process.stdout.write(cliText(
|
|
1342
|
-
`App Server release scheduled for ${released.agentName} in ${released.releaseInSeconds} seconds.\n`,
|
|
1343
|
-
`已安排在 ${released.releaseInSeconds} 秒后释放 ${released.agentName} 的 App Server。\n`,
|
|
1344
|
-
));
|
|
1345
1340
|
} else {
|
|
1346
1341
|
process.stdout.write(cliText(
|
|
1347
|
-
|
|
1348
|
-
|
|
1342
|
+
released.blockingTaskCount > 0
|
|
1343
|
+
? `App Server release card sent for ${released.agentName} (${released.blockingTaskCount} blocking). Confirm in Lark to release.\n`
|
|
1344
|
+
: `App Server release card sent for ${released.agentName}. Confirm in Lark to release.\n`,
|
|
1345
|
+
released.blockingTaskCount > 0
|
|
1346
|
+
? `已发送 ${released.agentName} 的 App Server 释放卡片(${released.blockingTaskCount} 个任务阻塞)。请在飞书中确认释放。\n`
|
|
1347
|
+
: `已发送 ${released.agentName} 的 App Server 释放卡片。请在飞书中确认释放。\n`,
|
|
1349
1348
|
));
|
|
1350
1349
|
}
|
|
1351
1350
|
return;
|
|
@@ -42,6 +42,8 @@ const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
|
42
42
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
43
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
44
44
|
const THREAD_TURN_PAGE_SIZE = 100;
|
|
45
|
+
const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
46
|
+
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
45
47
|
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
|
|
46
48
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
47
49
|
// Wait for its response; connection closure still rejects the request.
|
|
@@ -305,7 +307,37 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
305
307
|
async readRemoteSession(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
306
308
|
const client = await this.client();
|
|
307
309
|
const response = await this.readThreadWithTurns(client, remoteSessionId);
|
|
308
|
-
|
|
310
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary(response.thread));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async readRemoteForkSource(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
314
|
+
const client = await this.client();
|
|
315
|
+
const metadata = await client.request<ThreadReadResponse>(
|
|
316
|
+
"thread/read",
|
|
317
|
+
{ threadId: remoteSessionId, includeTurns: false },
|
|
318
|
+
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
319
|
+
);
|
|
320
|
+
let turns: CodexTurnSnapshot[];
|
|
321
|
+
try {
|
|
322
|
+
turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (!isUnsupportedThreadTurnsListError(error)) throw error;
|
|
325
|
+
this.logger.warn(
|
|
326
|
+
{ error, remoteSessionId },
|
|
327
|
+
"App Server does not support lightweight Fork source reads; falling back to complete thread history.",
|
|
328
|
+
);
|
|
329
|
+
return this.readRemoteSession(remoteSessionId);
|
|
330
|
+
}
|
|
331
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({
|
|
332
|
+
...metadata.thread,
|
|
333
|
+
turns,
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async decorateRemoteSession(
|
|
338
|
+
remoteSessionId: string,
|
|
339
|
+
summary: RemoteSessionSummary,
|
|
340
|
+
): Promise<RemoteSessionSummary> {
|
|
309
341
|
const activeThreads = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
310
342
|
const activeSummary = activeThreads ? markLocallyDetectedActive(summary, activeThreads) : summary;
|
|
311
343
|
const settings = await this.localActivityDetector?.threadSettings([remoteSessionId]);
|
|
@@ -315,6 +347,46 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
315
347
|
};
|
|
316
348
|
}
|
|
317
349
|
|
|
350
|
+
private async listRecentThreadTurnsThroughCompleted(
|
|
351
|
+
client: AppServerClient,
|
|
352
|
+
remoteSessionId: string,
|
|
353
|
+
): Promise<CodexTurnSnapshot[]> {
|
|
354
|
+
const turns: CodexTurnSnapshot[] = [];
|
|
355
|
+
const seenCursors = new Set<string>();
|
|
356
|
+
let cursor: string | undefined;
|
|
357
|
+
do {
|
|
358
|
+
const response = await client.request<ThreadTurnsListResponse>(
|
|
359
|
+
"thread/turns/list",
|
|
360
|
+
{
|
|
361
|
+
threadId: remoteSessionId,
|
|
362
|
+
...(cursor ? { cursor } : {}),
|
|
363
|
+
limit: FORK_SOURCE_TURN_PAGE_SIZE,
|
|
364
|
+
sortDirection: "desc",
|
|
365
|
+
itemsView: "summary",
|
|
366
|
+
},
|
|
367
|
+
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
368
|
+
);
|
|
369
|
+
turns.push(...response.data);
|
|
370
|
+
const completedIndex = turns.findIndex((turn) => turn.status === "completed");
|
|
371
|
+
if (completedIndex >= 0) {
|
|
372
|
+
turns.splice(completedIndex + 1);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
const nextCursor = response.nextCursor ?? undefined;
|
|
376
|
+
if (!nextCursor) break;
|
|
377
|
+
if (seenCursors.has(nextCursor)) {
|
|
378
|
+
this.logger.warn(
|
|
379
|
+
{ remoteSessionId, cursor: nextCursor },
|
|
380
|
+
"App Server repeated a Fork source Turn cursor; stopping pagination.",
|
|
381
|
+
);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
seenCursors.add(nextCursor);
|
|
385
|
+
cursor = nextCursor;
|
|
386
|
+
} while (cursor);
|
|
387
|
+
return turns.reverse();
|
|
388
|
+
}
|
|
389
|
+
|
|
318
390
|
private async readThreadWithTurns(
|
|
319
391
|
client: AppServerClient,
|
|
320
392
|
remoteSessionId: string,
|
|
@@ -1229,6 +1301,12 @@ function isPaginatedThreadReadError(error: unknown): boolean {
|
|
|
1229
1301
|
return /paginated threads?[^\n]*do(?:es)? not support[^\n]*thread\/read[^\n]*includeTurns\s*=\s*true/i.test(details);
|
|
1230
1302
|
}
|
|
1231
1303
|
|
|
1304
|
+
function isUnsupportedThreadTurnsListError(error: unknown): boolean {
|
|
1305
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list") return false;
|
|
1306
|
+
return error.code === -32601
|
|
1307
|
+
|| /method not found|unknown method|unsupported|not supported/i.test(appServerErrorDetails(error));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1232
1310
|
function isUnmaterializedThreadReadError(error: unknown): boolean {
|
|
1233
1311
|
const details = appServerErrorDetails(error);
|
|
1234
1312
|
return /thread[^\n]*(?:not materialized|not loaded)[^\n]*includeTurns[^\n]*(?:unavailable|before first user message)/i
|
|
@@ -281,7 +281,7 @@ export interface AppServerReleaseCardView {
|
|
|
281
281
|
scheduleId: number;
|
|
282
282
|
agentName: string;
|
|
283
283
|
blockingTaskCount?: number;
|
|
284
|
-
|
|
284
|
+
blockingTaskTitles?: string[];
|
|
285
285
|
forced?: boolean;
|
|
286
286
|
error?: string;
|
|
287
287
|
}
|
|
@@ -405,16 +405,20 @@ export class CardRenderer {
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
const blockingTaskCount = Math.max(0, view.blockingTaskCount ?? 0);
|
|
408
|
-
const
|
|
409
|
-
|
|
408
|
+
const blockingTaskLines = (view.blockingTaskTitles ?? []).map((title, index) => {
|
|
409
|
+
const normalizedTitle = title.replace(/\s+/g, " ").trim() || "未命名任务";
|
|
410
|
+
return `${index + 1}. ${inlineCode(truncateText(normalizedTitle, 80))}`;
|
|
411
|
+
});
|
|
412
|
+
return sectionCard(blockingTaskCount > 0 ? "等待释放" : "确认释放", [
|
|
410
413
|
markdown([
|
|
411
414
|
`**Agent**:${agent}`,
|
|
412
415
|
blockingTaskCount > 0
|
|
413
416
|
? `**等待任务**:${blockingTaskCount}`
|
|
414
|
-
:
|
|
417
|
+
: "**状态**:可以释放",
|
|
418
|
+
...blockingTaskLines,
|
|
415
419
|
blockingTaskCount > 0
|
|
416
|
-
? ">
|
|
417
|
-
: ">
|
|
420
|
+
? "> 点击 **Release Now** 会中断执行;也可等待任务结束后再释放。"
|
|
421
|
+
: "> 点击 **Release** 释放,或取消。",
|
|
418
422
|
].join("\n")),
|
|
419
423
|
{
|
|
420
424
|
tag: "column_set",
|
|
@@ -428,7 +432,7 @@ export class CardRenderer {
|
|
|
428
432
|
vertical_align: "center",
|
|
429
433
|
elements: [{
|
|
430
434
|
tag: "button",
|
|
431
|
-
text: { tag: "plain_text", content: "Release Now" },
|
|
435
|
+
text: { tag: "plain_text", content: blockingTaskCount > 0 ? "Release Now" : "Release" },
|
|
432
436
|
type: "danger",
|
|
433
437
|
size: "small",
|
|
434
438
|
behaviors: [{
|
|
@@ -143,10 +143,9 @@ interface AppServerReleaseSchedule {
|
|
|
143
143
|
runtime: AgentRuntime;
|
|
144
144
|
status: AppServerReleaseCardView["status"];
|
|
145
145
|
blockingTaskCount: number;
|
|
146
|
+
blockingTaskTitles: string[];
|
|
146
147
|
targets: Map<string, AppServerReleaseCardTarget>;
|
|
147
148
|
forced: boolean;
|
|
148
|
-
releaseAt?: number;
|
|
149
|
-
releaseCountdownSeconds?: number;
|
|
150
149
|
releaseStarted?: boolean;
|
|
151
150
|
error?: string;
|
|
152
151
|
timer?: NodeJS.Timeout;
|
|
@@ -184,7 +183,6 @@ const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
|
184
183
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
185
184
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
186
185
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
187
|
-
const APP_SERVER_RELEASE_IDLE_DELAY_MS = 5_000;
|
|
188
186
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
189
187
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
190
188
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -454,7 +452,6 @@ export interface ControlTaskReleaseResult {
|
|
|
454
452
|
agentName: string;
|
|
455
453
|
status: "waiting" | "released";
|
|
456
454
|
blockingTaskCount: number;
|
|
457
|
-
releaseInSeconds?: number;
|
|
458
455
|
}
|
|
459
456
|
|
|
460
457
|
export interface ProxyLifecycle {
|
|
@@ -1287,7 +1284,6 @@ export class ProxySessionController {
|
|
|
1287
1284
|
agentName: result.agentName,
|
|
1288
1285
|
status: result.status,
|
|
1289
1286
|
blockingTaskCount: result.blockingTaskCount,
|
|
1290
|
-
...(result.releaseInSeconds !== undefined ? { releaseInSeconds: result.releaseInSeconds } : {}),
|
|
1291
1287
|
};
|
|
1292
1288
|
}
|
|
1293
1289
|
|
|
@@ -3041,7 +3037,7 @@ export class ProxySessionController {
|
|
|
3041
3037
|
runtime = this.runtimes.forAgent(agentName);
|
|
3042
3038
|
} else {
|
|
3043
3039
|
if (!taskId) throw new Error("缺少要 fork 的 App Server 任务 ID。");
|
|
3044
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
3040
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { forFork: true });
|
|
3045
3041
|
agentName = resolved.agentName;
|
|
3046
3042
|
runtime = resolved.runtime;
|
|
3047
3043
|
remote = resolved.remote;
|
|
@@ -6218,7 +6214,10 @@ export class ProxySessionController {
|
|
|
6218
6214
|
: this.store.findSessionByRemoteSessionId(reference, contextKey);
|
|
6219
6215
|
}
|
|
6220
6216
|
|
|
6221
|
-
private async resolveRemoteCodexSession(
|
|
6217
|
+
private async resolveRemoteCodexSession(
|
|
6218
|
+
reference: string,
|
|
6219
|
+
options: { forFork?: boolean } = {},
|
|
6220
|
+
): Promise<AgentRemoteSession> {
|
|
6222
6221
|
const scoped = parseRemoteSessionReference(reference);
|
|
6223
6222
|
const candidates = scoped
|
|
6224
6223
|
? [[scoped.agentName, this.runtimes.forAgent(scoped.agentName)] as const]
|
|
@@ -6229,10 +6228,13 @@ export class ProxySessionController {
|
|
|
6229
6228
|
if (runtime.kind !== "codex" || !runtime.readRemoteSession) {
|
|
6230
6229
|
throw new Error(`Agent ${agentName} 不支持读取远端任务。`);
|
|
6231
6230
|
}
|
|
6231
|
+
const remoteSessionId = scoped?.remoteSessionId ?? reference;
|
|
6232
6232
|
return {
|
|
6233
6233
|
agentName,
|
|
6234
6234
|
runtime,
|
|
6235
|
-
remote:
|
|
6235
|
+
remote: options.forFork && runtime.readRemoteForkSource
|
|
6236
|
+
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
6237
|
+
: await runtime.readRemoteSession(remoteSessionId),
|
|
6236
6238
|
} satisfies AgentRemoteSession;
|
|
6237
6239
|
}));
|
|
6238
6240
|
const matches = reads
|
|
@@ -6968,7 +6970,6 @@ export class ProxySessionController {
|
|
|
6968
6970
|
agentName: string;
|
|
6969
6971
|
status: "waiting" | "released" | "failed";
|
|
6970
6972
|
blockingTaskCount: number;
|
|
6971
|
-
releaseInSeconds?: number;
|
|
6972
6973
|
error?: string;
|
|
6973
6974
|
}> {
|
|
6974
6975
|
const record = requestedRecord ?? this.requireCurrentSession(contextKey);
|
|
@@ -6980,20 +6981,17 @@ export class ProxySessionController {
|
|
|
6980
6981
|
let schedule = this.appServerReleaseSchedules.get(record.agentName);
|
|
6981
6982
|
const created = !schedule;
|
|
6982
6983
|
if (!schedule) {
|
|
6983
|
-
const
|
|
6984
|
+
const blockers = this.appServerReleaseBlockers(record.agentName, runtime);
|
|
6985
|
+
const blockingTaskCount = blockers.length;
|
|
6984
6986
|
schedule = {
|
|
6985
6987
|
id: this.nextAppServerReleaseScheduleId++,
|
|
6986
6988
|
agentName: record.agentName,
|
|
6987
6989
|
runtime,
|
|
6988
6990
|
status: "waiting",
|
|
6989
6991
|
blockingTaskCount,
|
|
6992
|
+
blockingTaskTitles: this.appServerReleaseTaskTitles(blockers),
|
|
6990
6993
|
targets: new Map(),
|
|
6991
6994
|
forced: false,
|
|
6992
|
-
...(blockingTaskCount === 0
|
|
6993
|
-
? {
|
|
6994
|
-
releaseCountdownSeconds: Math.ceil(APP_SERVER_RELEASE_IDLE_DELAY_MS / 1_000),
|
|
6995
|
-
}
|
|
6996
|
-
: {}),
|
|
6997
6995
|
};
|
|
6998
6996
|
this.appServerReleaseSchedules.set(record.agentName, schedule);
|
|
6999
6997
|
}
|
|
@@ -7006,14 +7004,6 @@ export class ProxySessionController {
|
|
|
7006
7004
|
if (messageId) {
|
|
7007
7005
|
schedule.targets.set(`${contextKey}\u0000${messageId}`, { contextKey, messageId });
|
|
7008
7006
|
}
|
|
7009
|
-
if (
|
|
7010
|
-
schedule.status === "waiting"
|
|
7011
|
-
&& schedule.blockingTaskCount === 0
|
|
7012
|
-
&& schedule.releaseCountdownSeconds !== undefined
|
|
7013
|
-
&& schedule.releaseAt === undefined
|
|
7014
|
-
) {
|
|
7015
|
-
schedule.releaseAt = Date.now() + APP_SERVER_RELEASE_IDLE_DELAY_MS;
|
|
7016
|
-
}
|
|
7017
7007
|
} catch (error) {
|
|
7018
7008
|
if (created && schedule.targets.size === 0) {
|
|
7019
7009
|
this.clearAppServerReleaseSchedule(schedule);
|
|
@@ -7026,9 +7016,6 @@ export class ProxySessionController {
|
|
|
7026
7016
|
agentName: schedule.agentName,
|
|
7027
7017
|
status: schedule.status === "failed" ? "failed" : schedule.status === "released" ? "released" : "waiting",
|
|
7028
7018
|
blockingTaskCount: schedule.blockingTaskCount,
|
|
7029
|
-
...(schedule.releaseCountdownSeconds !== undefined
|
|
7030
|
-
? { releaseInSeconds: schedule.releaseCountdownSeconds }
|
|
7031
|
-
: {}),
|
|
7032
7019
|
...(schedule.error ? { error: schedule.error } : {}),
|
|
7033
7020
|
};
|
|
7034
7021
|
}
|
|
@@ -7040,7 +7027,7 @@ export class ProxySessionController {
|
|
|
7040
7027
|
if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || schedule.status !== "waiting") {
|
|
7041
7028
|
throw new Error("释放卡片已失效,请重新发送 /release。");
|
|
7042
7029
|
}
|
|
7043
|
-
await this.performAppServerRelease(schedule
|
|
7030
|
+
await this.performAppServerRelease(schedule);
|
|
7044
7031
|
}
|
|
7045
7032
|
|
|
7046
7033
|
private async cancelAppServerRelease(action: CardAction): Promise<void> {
|
|
@@ -7075,6 +7062,10 @@ export class ProxySessionController {
|
|
|
7075
7062
|
});
|
|
7076
7063
|
}
|
|
7077
7064
|
|
|
7065
|
+
private appServerReleaseTaskTitles(blockers: SessionRecord[]): string[] {
|
|
7066
|
+
return blockers.map((session) => session.title?.trim() || "未命名任务");
|
|
7067
|
+
}
|
|
7068
|
+
|
|
7078
7069
|
private armAppServerReleaseCheck(schedule: AppServerReleaseSchedule): void {
|
|
7079
7070
|
if (schedule.status !== "waiting" || schedule.timer || schedule.inFlight) return;
|
|
7080
7071
|
schedule.timer = setTimeout(() => {
|
|
@@ -7089,36 +7080,22 @@ export class ProxySessionController {
|
|
|
7089
7080
|
|
|
7090
7081
|
private async refreshAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): Promise<void> {
|
|
7091
7082
|
if (this.appServerReleaseSchedules.get(schedule.agentName) !== schedule || schedule.status !== "waiting") return;
|
|
7092
|
-
const
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
schedule.
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
this.armAppServerReleaseCheck(schedule);
|
|
7102
|
-
return;
|
|
7103
|
-
}
|
|
7104
|
-
schedule.blockingTaskCount = 0;
|
|
7105
|
-
if (schedule.releaseAt !== undefined) {
|
|
7106
|
-
const releaseCountdownSeconds = Math.max(0, Math.ceil((schedule.releaseAt - Date.now()) / 1_000));
|
|
7107
|
-
if (releaseCountdownSeconds > 0) {
|
|
7108
|
-
if (schedule.releaseCountdownSeconds !== releaseCountdownSeconds) {
|
|
7109
|
-
schedule.releaseCountdownSeconds = releaseCountdownSeconds;
|
|
7110
|
-
await this.publishAppServerReleaseSchedule(schedule);
|
|
7111
|
-
}
|
|
7112
|
-
this.armAppServerReleaseCheck(schedule);
|
|
7113
|
-
return;
|
|
7114
|
-
}
|
|
7083
|
+
const blockers = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime);
|
|
7084
|
+
const blockingTaskCount = blockers.length;
|
|
7085
|
+
const blockingTaskTitles = this.appServerReleaseTaskTitles(blockers);
|
|
7086
|
+
const titlesChanged = schedule.blockingTaskTitles.length !== blockingTaskTitles.length
|
|
7087
|
+
|| schedule.blockingTaskTitles.some((title, index) => title !== blockingTaskTitles[index]);
|
|
7088
|
+
if (schedule.blockingTaskCount !== blockingTaskCount || titlesChanged) {
|
|
7089
|
+
schedule.blockingTaskCount = blockingTaskCount;
|
|
7090
|
+
schedule.blockingTaskTitles = blockingTaskTitles;
|
|
7091
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7115
7092
|
}
|
|
7116
|
-
|
|
7093
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7117
7094
|
}
|
|
7118
7095
|
|
|
7119
|
-
private async performAppServerRelease(schedule: AppServerReleaseSchedule
|
|
7096
|
+
private async performAppServerRelease(schedule: AppServerReleaseSchedule): Promise<void> {
|
|
7120
7097
|
if (schedule.inFlight) return schedule.inFlight;
|
|
7121
|
-
const operation = this.performAppServerReleaseNow(schedule
|
|
7098
|
+
const operation = this.performAppServerReleaseNow(schedule);
|
|
7122
7099
|
schedule.inFlight = operation;
|
|
7123
7100
|
try {
|
|
7124
7101
|
await operation;
|
|
@@ -7128,31 +7105,17 @@ export class ProxySessionController {
|
|
|
7128
7105
|
}
|
|
7129
7106
|
}
|
|
7130
7107
|
|
|
7131
|
-
private async performAppServerReleaseNow(schedule: AppServerReleaseSchedule
|
|
7108
|
+
private async performAppServerReleaseNow(schedule: AppServerReleaseSchedule): Promise<void> {
|
|
7132
7109
|
if (schedule.timer) {
|
|
7133
7110
|
clearTimeout(schedule.timer);
|
|
7134
7111
|
schedule.timer = undefined;
|
|
7135
7112
|
}
|
|
7136
|
-
const interruptedWork =
|
|
7137
|
-
|
|
7138
|
-
else {
|
|
7139
|
-
const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
|
|
7140
|
-
if (blockingTaskCount > 0) {
|
|
7141
|
-
schedule.status = "waiting";
|
|
7142
|
-
schedule.blockingTaskCount = blockingTaskCount;
|
|
7143
|
-
schedule.releaseAt = undefined;
|
|
7144
|
-
schedule.releaseCountdownSeconds = undefined;
|
|
7145
|
-
schedule.releaseStarted = false;
|
|
7146
|
-
await this.publishAppServerReleaseSchedule(schedule);
|
|
7147
|
-
this.armAppServerReleaseCheck(schedule);
|
|
7148
|
-
return;
|
|
7149
|
-
}
|
|
7150
|
-
}
|
|
7113
|
+
const interruptedWork = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length > 0;
|
|
7114
|
+
await this.cancelQueuedPromptsForAppServerRelease(schedule.agentName);
|
|
7151
7115
|
|
|
7152
7116
|
schedule.status = "releasing";
|
|
7153
7117
|
schedule.blockingTaskCount = 0;
|
|
7154
|
-
schedule.
|
|
7155
|
-
schedule.releaseCountdownSeconds = undefined;
|
|
7118
|
+
schedule.blockingTaskTitles = [];
|
|
7156
7119
|
schedule.releaseStarted = false;
|
|
7157
7120
|
schedule.forced = interruptedWork;
|
|
7158
7121
|
await this.publishAppServerReleaseSchedule(schedule);
|
|
@@ -7162,18 +7125,18 @@ export class ProxySessionController {
|
|
|
7162
7125
|
) return;
|
|
7163
7126
|
schedule.releaseStarted = true;
|
|
7164
7127
|
try {
|
|
7165
|
-
const result = await schedule.runtime.release?.({ force });
|
|
7128
|
+
const result = await schedule.runtime.release?.({ force: true });
|
|
7166
7129
|
if (!result) throw new Error("当前 Agent 不支持释放 App Server。");
|
|
7167
7130
|
if (result.status === "busy") {
|
|
7131
|
+
const blockers = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime);
|
|
7168
7132
|
schedule.status = "waiting";
|
|
7169
7133
|
schedule.blockingTaskCount = Math.max(
|
|
7170
7134
|
result.activeSessionIds?.length ?? 0,
|
|
7171
|
-
|
|
7135
|
+
blockers.length,
|
|
7172
7136
|
1,
|
|
7173
7137
|
);
|
|
7138
|
+
schedule.blockingTaskTitles = this.appServerReleaseTaskTitles(blockers);
|
|
7174
7139
|
schedule.forced = false;
|
|
7175
|
-
schedule.releaseAt = undefined;
|
|
7176
|
-
schedule.releaseCountdownSeconds = undefined;
|
|
7177
7140
|
schedule.releaseStarted = false;
|
|
7178
7141
|
await this.publishAppServerReleaseSchedule(schedule);
|
|
7179
7142
|
this.armAppServerReleaseCheck(schedule);
|
|
@@ -7233,9 +7196,7 @@ export class ProxySessionController {
|
|
|
7233
7196
|
scheduleId: schedule.id,
|
|
7234
7197
|
agentName: schedule.agentName,
|
|
7235
7198
|
...(schedule.blockingTaskCount > 0 ? { blockingTaskCount: schedule.blockingTaskCount } : {}),
|
|
7236
|
-
...(schedule.
|
|
7237
|
-
? { releaseInSeconds: schedule.releaseCountdownSeconds }
|
|
7238
|
-
: {}),
|
|
7199
|
+
...(schedule.blockingTaskTitles.length > 0 ? { blockingTaskTitles: schedule.blockingTaskTitles } : {}),
|
|
7239
7200
|
...(schedule.forced ? { forced: true } : {}),
|
|
7240
7201
|
...(schedule.error ? { error: schedule.error } : {}),
|
|
7241
7202
|
};
|
package/src/runtime/types.ts
CHANGED
|
@@ -230,6 +230,7 @@ export interface AgentRuntime {
|
|
|
230
230
|
readSessionMetadata(remoteSessionId: string): Promise<RuntimeSessionMetadata>;
|
|
231
231
|
listRemoteSessions?(input?: { searchTerm?: string; cursor?: string; limit?: number }): Promise<RemoteSessionPage>;
|
|
232
232
|
readRemoteSession?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
233
|
+
readRemoteForkSource?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
233
234
|
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
234
235
|
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
235
236
|
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|