@xiaohhhh1/canvas-agent 0.4.55 → 0.4.57
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.
|
@@ -9,6 +9,7 @@ const canvasAgentMcp = canvasAgentMcpCommand();
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const STREAM_UPDATE_INTERVAL_MS = 40;
|
|
11
11
|
const TURN_RECONCILE_INTERVAL_MS = 5_000;
|
|
12
|
+
const APP_SERVER_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
12
13
|
/** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
|
|
13
14
|
export class CodexAppClient {
|
|
14
15
|
child;
|
|
@@ -52,9 +53,15 @@ export class CodexAppClient {
|
|
|
52
53
|
onExit();
|
|
53
54
|
emit("agent_log", { text: `Codex app-server exited: ${code ?? 0}` });
|
|
54
55
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
try {
|
|
57
|
+
await withTimeout(client.request("initialize", { clientInfo: { name: "canvas-agent", title: "Infinite Canvas Agent", version: VERSION }, capabilities: { experimentalApi: true, requestAttestation: false } }), APP_SERVER_INITIALIZE_TIMEOUT_MS, "Codex app-server initialize timed out");
|
|
58
|
+
client.notify("initialized");
|
|
59
|
+
return client;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
await client.terminate("Codex app-server 初始化超时,已回收失去响应的进程");
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
/** 创建新的 Codex 线程。 */
|
|
60
67
|
async startThread(cwd, permissionMode = "request", modelSettings = {}) {
|
|
@@ -408,6 +415,21 @@ export class CodexAppClient {
|
|
|
408
415
|
this.currentTurnId = "";
|
|
409
416
|
}
|
|
410
417
|
}
|
|
418
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
419
|
+
let timer;
|
|
420
|
+
try {
|
|
421
|
+
return await Promise.race([
|
|
422
|
+
promise,
|
|
423
|
+
new Promise((_resolve, reject) => {
|
|
424
|
+
timer = setTimeout(() => reject(new Error(message)), Math.max(1, timeoutMs));
|
|
425
|
+
}),
|
|
426
|
+
]);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
if (timer)
|
|
430
|
+
clearTimeout(timer);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
411
433
|
/** 只有正常 completed 才能被调用方视为可用脚本。 */
|
|
412
434
|
export function isTerminalTurnStatus(status) {
|
|
413
435
|
return ["completed", "failed", "interrupted", "cancelled", "canceled"].includes(String(status || "").toLowerCase());
|
package/dist/agent/codex.d.ts
CHANGED
|
@@ -56,6 +56,16 @@ export declare function runCodexWorkflowTurn(prompt: string, emit: AgentEmit, op
|
|
|
56
56
|
onWorkerStart?: () => void;
|
|
57
57
|
onWorkerFinish?: () => void;
|
|
58
58
|
}): Promise<CodexWorkflowRunResult>;
|
|
59
|
+
/**
|
|
60
|
+
* 给 Flow C 的完整 app-server 链路设置硬截止时间。超时后不再等待原 Promise
|
|
61
|
+
* 自行结束;否则初始化或 thread/start 永不返回时会永久占住 worker lane。
|
|
62
|
+
*/
|
|
63
|
+
export declare function runBoundedWorkflowOperation<T>(operation: Promise<T>, timeoutMs: number, onTimeout: () => Promise<void> | void): Promise<{
|
|
64
|
+
timedOut: false;
|
|
65
|
+
value: T;
|
|
66
|
+
} | {
|
|
67
|
+
timedOut: true;
|
|
68
|
+
}>;
|
|
59
69
|
/** Contract/preflight 4xx errors are deterministic and must never burn every fallback chunk size. */
|
|
60
70
|
export declare function isDeterministicWorkflowContractError(error: unknown): boolean;
|
|
61
71
|
/** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -44,26 +44,26 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
44
44
|
const threadStartedAt = Date.now();
|
|
45
45
|
let threadStartMs = 0;
|
|
46
46
|
let modelStartedAt = 0;
|
|
47
|
-
let timer;
|
|
48
47
|
let app = workflowCodexApps.get(workerIndex);
|
|
49
48
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
const operation = (async () => {
|
|
50
|
+
if (!app)
|
|
51
|
+
app = await startWorkflowCodexApp(workerIndex, options.appEmit || emit);
|
|
52
|
+
const thread = await app.startThread(options.cwd, options.permissionMode || "request", modelSettings);
|
|
53
|
+
const threadId = String(field(thread, "id") || "");
|
|
54
|
+
options.onThread?.(threadId);
|
|
55
|
+
threadStartMs = Date.now() - threadStartedAt;
|
|
56
|
+
modelStartedAt = Date.now();
|
|
57
|
+
return await app.startTurn(threadId, prompt, [], options.permissionMode || "request", options.onTurn, options.outputSchema, modelSettings);
|
|
58
|
+
})();
|
|
59
|
+
const result = await runBoundedWorkflowOperation(operation, options.timeoutMs, async () => {
|
|
61
60
|
workflowCodexApps.delete(workerIndex);
|
|
62
|
-
await app
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
await app?.terminate("Flow C 脚本执行链路超时,已仅回收当前 worker");
|
|
62
|
+
});
|
|
63
|
+
if (result.timedOut) {
|
|
64
|
+
return { ok: false, error: "Flow C 脚本执行链路超过 8 分钟,已自动终止当前 worker", retryable: true, timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
65
65
|
}
|
|
66
|
-
return { ok: true, text: result, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
66
|
+
return { ok: true, text: result.value, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
69
|
logger.error("Flow C Codex worker failed", { workerIndex, error });
|
|
@@ -72,12 +72,36 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
72
72
|
return { ok: false, error: message, retryable: !isDeterministicWorkflowContractError(message), timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
73
73
|
}
|
|
74
74
|
finally {
|
|
75
|
-
if (timer)
|
|
76
|
-
clearTimeout(timer);
|
|
77
75
|
options.onWorkerFinish?.();
|
|
78
76
|
}
|
|
79
77
|
});
|
|
80
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 给 Flow C 的完整 app-server 链路设置硬截止时间。超时后不再等待原 Promise
|
|
81
|
+
* 自行结束;否则初始化或 thread/start 永不返回时会永久占住 worker lane。
|
|
82
|
+
*/
|
|
83
|
+
export async function runBoundedWorkflowOperation(operation, timeoutMs, onTimeout) {
|
|
84
|
+
let timer;
|
|
85
|
+
const timeout = new Promise((resolve) => {
|
|
86
|
+
timer = setTimeout(() => resolve({ timedOut: true }), Math.max(1, timeoutMs));
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const result = await Promise.race([
|
|
90
|
+
operation.then((value) => ({ timedOut: false, value })),
|
|
91
|
+
timeout,
|
|
92
|
+
]);
|
|
93
|
+
if (!result.timedOut)
|
|
94
|
+
return result;
|
|
95
|
+
// Avoid an unhandled rejection if process termination settles the abandoned operation later.
|
|
96
|
+
void operation.catch(() => undefined);
|
|
97
|
+
await onTimeout();
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timer)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
81
105
|
/** Contract/preflight 4xx errors are deterministic and must never burn every fallback chunk size. */
|
|
82
106
|
export function isDeterministicWorkflowContractError(error) {
|
|
83
107
|
const message = errorMessage(error);
|
|
@@ -37,7 +37,7 @@ export class WorkerPool {
|
|
|
37
37
|
this.available.push(workerIndex);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
export function boundedWorkerConcurrency(value, fallback =
|
|
40
|
+
export function boundedWorkerConcurrency(value, fallback = 4, maximum = 4) {
|
|
41
41
|
const parsed = Number(value);
|
|
42
42
|
if (!Number.isInteger(parsed) || parsed < 1)
|
|
43
43
|
return fallback;
|
|
@@ -5,6 +5,6 @@ export declare const FLOW_C_SCRIPT_CHUNK_SIZES: readonly [10, 5, 1];
|
|
|
5
5
|
* 长视频每条都包含总脚本和 2/3 套完整分镜,不能沿用短脚本的 30 条输出量。
|
|
6
6
|
* 小段回传既能更早显示进度,也避免单个 Codex turn 因输出过大而被中断。
|
|
7
7
|
*/
|
|
8
|
-
export declare function flowCScriptChunkSizes(durationSeconds: 10 | 20 | 30): readonly [10, 5, 1] | readonly [
|
|
8
|
+
export declare function flowCScriptChunkSizes(durationSeconds: 10 | 20 | 30): readonly [10, 5, 1] | readonly [1];
|
|
9
9
|
/** 将当前缺失 ordinal 切成可独立持久化的子批;长视频绝不恢复 30 条大回合。 */
|
|
10
10
|
export declare function flowCScriptChunks(durationSeconds: 10 | 20 | 30, ordinals: number[], size?: number): number[][];
|