@xiaohhhh1/canvas-agent 0.4.44 → 0.4.45

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.
@@ -1,5 +1,5 @@
1
1
  import { type JsonRecord } from "../utils/value.js";
2
- import type { CodexPlanUpdate, CodexRequestParams } from "./codex-protocol.js";
2
+ import type { CodexPlanUpdate, CodexRequestParams, CodexTurn } from "./codex-protocol.js";
3
3
  import type { AgentEmit, AgentPermissionMode } from "./types.js";
4
4
  /** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
5
5
  export declare class CodexAppClient {
@@ -66,6 +66,13 @@ export declare class CodexAppClient {
66
66
  private emitDelta;
67
67
  /** 合并短时间内的文本增量,减少 SSE 传输和前端渲染次数。 */
68
68
  private flushDelta;
69
+ /**
70
+ * app-server 的流式通知偶尔会在本地重连时丢失。定期读取线程的最终
71
+ * 状态,保证被中断的 turn 不会只因少了一条通知而永久占着 Flow C 队列。
72
+ */
73
+ private scheduleTurnReconciliation;
74
+ /** 清除 turn 等待器与其轮询计时器。 */
75
+ private clearActiveTurn;
69
76
  /** 自动回复 app-server 发起的授权或交互请求。 */
70
77
  private answerServerRequest;
71
78
  /** 完成指定 JSON-RPC 请求。 */
@@ -75,3 +82,9 @@ export declare class CodexAppClient {
75
82
  /** 拒绝进程退出时仍未完成的请求与 turn。 */
76
83
  private failAll;
77
84
  }
85
+ /** 只有正常 completed 才能被调用方视为可用脚本。 */
86
+ export declare function isTerminalTurnStatus(status: unknown): boolean;
87
+ /** interrupted 也必须作为失败传回,不能被误当成空的成功响应。 */
88
+ export declare function turnFailure(turn: Pick<CodexTurn, "status" | "error">): Error | null;
89
+ /** 补偿轮询发现终态时,尽量取回已完成的结构化文本。 */
90
+ export declare function turnText(turn: CodexTurn): string;
@@ -8,6 +8,7 @@ import { field } from "../utils/value.js";
8
8
  const canvasAgentMcp = canvasAgentMcpCommand();
9
9
  const require = createRequire(import.meta.url);
10
10
  const STREAM_UPDATE_INTERVAL_MS = 40;
11
+ const TURN_RECONCILE_INTERVAL_MS = 5_000;
11
12
  /** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
12
13
  export class CodexAppClient {
13
14
  child;
@@ -110,7 +111,10 @@ export class CodexAppClient {
110
111
  throw completed.error;
111
112
  return completed?.text || "";
112
113
  }
113
- return await new Promise((resolve, reject) => this.activeTurns.set(turnId, { resolve, reject }));
114
+ return await new Promise((resolve, reject) => {
115
+ this.activeTurns.set(turnId, { resolve, reject });
116
+ this.scheduleTurnReconciliation(threadId, turnId);
117
+ });
114
118
  }
115
119
  /** 中断当前正在运行的 Codex turn。 */
116
120
  async interruptCurrentTurn() {
@@ -267,11 +271,11 @@ export class CodexAppClient {
267
271
  const turn = params.turn;
268
272
  const turnId = turn.id;
269
273
  const pending = this.activeTurns.get(turnId);
270
- const error = turn.error;
274
+ const error = turnFailure(turn);
271
275
  const text = this.finalTextByTurn.get(turnId) || "";
272
276
  this.finalTextByTurn.delete(turnId);
273
277
  if (pending) {
274
- this.activeTurns.delete(turnId);
278
+ this.clearActiveTurn(turnId);
275
279
  error ? pending.reject(new Error(error.message || "Codex turn failed")) : pending.resolve(text);
276
280
  }
277
281
  else if (turnId) {
@@ -311,6 +315,53 @@ export class CodexAppClient {
311
315
  if (pending.delta)
312
316
  this.emit("agent_event", { agent: "codex", type: "item.updated", item: { id, type: pending.itemType, delta: pending.delta }, ...codexEventScope(pending.params) });
313
317
  }
318
+ /**
319
+ * app-server 的流式通知偶尔会在本地重连时丢失。定期读取线程的最终
320
+ * 状态,保证被中断的 turn 不会只因少了一条通知而永久占着 Flow C 队列。
321
+ */
322
+ scheduleTurnReconciliation(threadId, turnId) {
323
+ const check = async () => {
324
+ const pending = this.activeTurns.get(turnId);
325
+ if (!pending)
326
+ return;
327
+ try {
328
+ const result = await this.readThread(threadId, true);
329
+ const thread = field(result, "thread");
330
+ const turns = Array.isArray(field(thread, "turns")) ? field(thread, "turns") : [];
331
+ const turn = turns.find((item) => item?.id === turnId);
332
+ if (turn && isTerminalTurnStatus(turn.status)) {
333
+ const error = turnFailure(turn);
334
+ const text = turnText(turn);
335
+ this.finalTextByTurn.delete(turnId);
336
+ this.clearActiveTurn(turnId);
337
+ error ? pending.reject(new Error(error.message)) : pending.resolve(text);
338
+ if (turnId === this.currentTurnId) {
339
+ this.currentThreadId = "";
340
+ this.currentTurnId = "";
341
+ }
342
+ return;
343
+ }
344
+ }
345
+ catch (error) {
346
+ // The normal deadline in the workflow manager remains the hard
347
+ // fallback. A transient read error must not cancel live work.
348
+ logger.debug("Codex turn reconciliation read failed", { threadId, turnId, error });
349
+ }
350
+ const current = this.activeTurns.get(turnId);
351
+ if (current)
352
+ current.reconcileTimer = setTimeout(() => void check(), TURN_RECONCILE_INTERVAL_MS);
353
+ };
354
+ const pending = this.activeTurns.get(turnId);
355
+ if (pending)
356
+ pending.reconcileTimer = setTimeout(() => void check(), TURN_RECONCILE_INTERVAL_MS);
357
+ }
358
+ /** 清除 turn 等待器与其轮询计时器。 */
359
+ clearActiveTurn(turnId) {
360
+ const pending = this.activeTurns.get(turnId);
361
+ if (pending?.reconcileTimer)
362
+ clearTimeout(pending.reconcileTimer);
363
+ this.activeTurns.delete(turnId);
364
+ }
314
365
  /** 自动回复 app-server 发起的授权或交互请求。 */
315
366
  answerServerRequest(message) {
316
367
  const method = String(message.method);
@@ -339,7 +390,13 @@ export class CodexAppClient {
339
390
  }
340
391
  /** 拒绝进程退出时仍未完成的请求与 turn。 */
341
392
  failAll(message) {
342
- [...this.pending.values(), ...this.activeTurns.values()].forEach((item) => item.reject(new Error(message)));
393
+ [...this.pending.values()].forEach((item) => item.reject(new Error(message)));
394
+ [...this.activeTurns.entries()].forEach(([turnId, item]) => {
395
+ if (item.reconcileTimer)
396
+ clearTimeout(item.reconcileTimer);
397
+ this.activeTurns.delete(turnId);
398
+ item.reject(new Error(message));
399
+ });
343
400
  this.pendingDeltas.forEach((item) => clearTimeout(item.timer));
344
401
  this.pending.clear();
345
402
  this.activeTurns.clear();
@@ -351,6 +408,35 @@ export class CodexAppClient {
351
408
  this.currentTurnId = "";
352
409
  }
353
410
  }
411
+ /** 只有正常 completed 才能被调用方视为可用脚本。 */
412
+ export function isTerminalTurnStatus(status) {
413
+ return ["completed", "failed", "interrupted", "cancelled", "canceled"].includes(String(status || "").toLowerCase());
414
+ }
415
+ /** interrupted 也必须作为失败传回,不能被误当成空的成功响应。 */
416
+ export function turnFailure(turn) {
417
+ if (turn.error)
418
+ return new Error(turn.error.message || "Codex turn failed");
419
+ const status = String(turn.status || "completed").toLowerCase();
420
+ return status === "completed" ? null : new Error(`Codex turn ${status}`);
421
+ }
422
+ /** 补偿轮询发现终态时,尽量取回已完成的结构化文本。 */
423
+ export function turnText(turn) {
424
+ const items = Array.isArray(field(turn, "items")) ? field(turn, "items") : [];
425
+ for (const item of [...items].reverse()) {
426
+ if (!["agent_message", "agentMessage"].includes(String(field(item, "type") || "")))
427
+ continue;
428
+ const text = field(item, "text");
429
+ if (typeof text === "string" && text)
430
+ return text;
431
+ const content = field(item, "content");
432
+ if (Array.isArray(content)) {
433
+ const joined = content.map((part) => String(field(part, "text") || "")).join("");
434
+ if (joined)
435
+ return joined;
436
+ }
437
+ }
438
+ return "";
439
+ }
354
440
  /** 生成 Codex 调用 Canvas Agent MCP 的启动命令。 */
355
441
  function canvasAgentMcpCommand() {
356
442
  const current = process.argv.find((arg) => /index\.(t|j)s$/.test(arg)) || "";
@@ -6,6 +6,7 @@ export type CodexThread = JsonRecord & {
6
6
  };
7
7
  export type CodexTurn = JsonRecord & {
8
8
  id: string;
9
+ status?: string;
9
10
  error?: CodexTurnError | null;
10
11
  durationMs?: number | null;
11
12
  };
@@ -975,16 +975,16 @@ export declare const toolInputSchemas: {
975
975
  }, "strip", z.ZodTypeAny, {
976
976
  title: string;
977
977
  kind: "text" | "image";
978
- source?: string | undefined;
979
978
  content?: string | undefined;
979
+ source?: string | undefined;
980
980
  tags?: string[] | undefined;
981
981
  imageUrl?: string | undefined;
982
982
  note?: string | undefined;
983
983
  }, {
984
984
  title: string;
985
985
  kind: "text" | "image";
986
- source?: string | undefined;
987
986
  content?: string | undefined;
987
+ source?: string | undefined;
988
988
  tags?: string[] | undefined;
989
989
  imageUrl?: string | undefined;
990
990
  note?: string | undefined;
@@ -173,8 +173,8 @@ export declare function parseToolInput(name: ToolName, input: unknown): {
173
173
  } | {
174
174
  title: string;
175
175
  kind: "text" | "image";
176
- source?: string | undefined;
177
176
  content?: string | undefined;
177
+ source?: string | undefined;
178
178
  tags?: string[] | undefined;
179
179
  imageUrl?: string | undefined;
180
180
  note?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.44",
3
+ "version": "0.4.45",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",