跳转到内容

错误处理 / 重连 / 幂等

可靠的 Agent 不是”永不出错”,而是”出错时优雅降级 + 自愈”。 这一页讲 SDK 自带的错误处理机制、你自己代码该怎么补、以及 Agent 进入 unreachable 状态后怎么恢复。

三种错误类别

类别来源SDK 默认行为你的责任
传输错误WS 断开、HTTP 5xx、超时自动重连 / 重试监控 status,长期不可达时报警
业务错误401 token 失效、403 配额、404 不存在、429 限速throw 给你按错误码分类处理
加密错误签名错、解密失败、AAD 不匹配单条丢弃 + 报告监控 decrypt failure rate;高时排查时钟 / 密钥

错误事件订阅

agent.addStatusHandler((status) => {
console.log(`[hashee] status: ${status}`);
});
agent.addErrorHandler((err) => {
// 通常 SDK 已处理;这里是不可恢复错误的兜底
reportToSentry(err);
if (isUnrecoverable(err)) process.exit(1);
});

addStatusHandler 收到的状态机:

connecting → authenticating → connected
├─ 网络抖动 → reconnecting → connected
├─ 主动 close() → disconnected
└─ 不可恢复错误 → error

WS 自动重连

WebSocket 模式下:

  • 心跳 ping 每 30 秒;90 秒无 pong → 视为断开。
  • 断开后立即重连,指数退避 1s → 30s(cap 30s)。
  • 重连成功后自动重订阅所有 handler,业务无感。
  • 服务端为每个 Agent 维护 cursor,重连时未投递消息续传,不丢
// 你不需要写任何重连逻辑——SDK 自带
// 但你可以观察过程:
agent.addStatusHandler((status) => {
if (status === "reconnecting") {
metrics.increment("hashee.reconnecting");
}
if (status === "connected") {
metrics.gauge("hashee.connected", 1);
}
});

消息幂等(message_id 去重)

每条消息有 client_message_id(你侧生成 UUID)和 message_id(后端分配)。 SDK 自带去重——同 message_id 不会触发两次 handler,即使重连后重新拉取。

发送时:

import { ulid } from "ulid";
await agent.send(conversationId, {
type: "text",
text: "...",
client_message_id: ulid(), // 自己生成;同 ID 重发幂等
});

服务端用 client_message_id 做幂等 key(24 小时窗口);同 (agent_id, client_message_id) 重复 POST 返回原 message_id,不会插重复行。

aadContext 强制

V0.2 SDK 起,所有 send 必须有 AAD 绑定上下文(详见 安全边界)。 未传 → throw AadContextRequiredError

绝大多数情况下 SDK 自动从 conversation 元数据获取 epoch_id,你不需要手动管。 若你看到这个错误:

try {
await agent.send(conversationId, payload);
} catch (err) {
if (err.name === "AadContextRequiredError") {
// 通常是 conversation 元数据未加载
await agent.refreshConversation(conversationId);
return agent.send(conversationId, payload); // 重试
}
throw err;
}

Decrypt failure 处理

收到的消息解密失败时 SDK:

  1. 调你的 messageHandler(避免你拿到错误数据)。
  2. reportDecryptFailure(reason, message_id),默认 console.warn + 上报 telemetry。
  3. 自增内部计数器(短时高频解密失败可能是密钥不对)。

你可以注册自己的失败 handler:

agent.addDecryptFailureHandler((report) => {
metrics.increment("hashee.decrypt_failure", {
reason: report.reason, // signature | no_wrap | unwrap | aead | parse
});
if (report.reason === "no_wrap") {
// 通常意味着发送者还没拿到你 Agent 的最新公钥
// SDK 会自动重新 publish 公钥;这里只做监控
}
});

每种 reason 的诊断方向:

reason含义可能根因
signatureEd25519 验签失败发送者签名时用错私钥 / Agent 持有的发送者公钥版本旧
no_wrap找不到给本 Agent 的 wrap发送者发消息时还没拿到 Agent 公钥(首次连接竞态)
unwrapunwrap CEK 失败Agent 私钥不匹配(密钥丢失 / 误改环境变量)
aeadAES-GCM tag 不匹配AAD 不一致(client / server 时钟严重不同步影响 epoch)
parse解密后不是合法 JSON协议版本不匹配(罕见,通常上游 SDK 老)

REST 错误码

agent.send 等 REST 调用失败时 throw 含 code 字段的错误:

try {
await agent.send(conversationId, payload);
} catch (err) {
switch (err.code) {
case "RATE_LIMITED":
// SDK 已 backoff;走到这里说明 burst 限速
await sleep(err.retry_after_ms);
return retry();
case "RELATION_REVOKED":
// 用户撤销 H2A,清理本地数据
await cleanupUser(conversationId);
break;
case "AGENT_TOKEN_INVALID":
// Token 失效,需要从 secret store 重新取后重启
process.exit(1);
break;
case "WEBHOOK_PAYLOAD_TOO_LARGE":
// 1 MB 超限,分块发送或走 R2
return sendInChunks(conversationId, payload);
case "ARTIFACT_REVISION_CONFLICT":
// 详见 artifact-advanced 章节
return reconcileAndRetry();
default:
throw err;
}
}

完整错误码:API 错误码全表

Webhook 模式:unreachable 状态机

Webhook agent 失败 7 次后进入 unreachable

你的 webhook 7 次重试都返 5xx 或超时
后端:agents.connection_status = "unreachable"
新消息进入 pending 队列(保留 24 小时)
你修好 webhook
POST /agents/:id/connection/recover
后端:unreachable → ready,flush 队列

SDK Dispatcher 在每次 init 时自动尝试一次 recover:

const dispatcher = createWebhookDispatcher({
/* ... */
onMessage: async (msg) => { /* ... */ },
// 如果 init 时检测到 unreachable,会自动调 recover
autoRecover: true, // 默认 true
});

如果你需要主动 recover(如你修完 bug 后立即推 deploy):

import { restRequest } from "@hasheeai/agent-sdk-ts";
const result = await restRequest({
method: "POST",
baseUrl: "https://api.hashee.ai",
path: `/agents/${AGENT_ID}/connection/recover`,
token: AGENT_TOKEN,
});
console.log(`pending messages: ${result.pending_count}`);

队列模式 — 长任务异步化

Webhook 同步响应 deadline 10 秒。LLM 调用 +工具调用经常超过这个, 推荐”立即 ack + 异步处理 + 主动回写”:

const dispatcher = createWebhookDispatcher({
/* ... */
onMessage: async (msg) => {
// 立即把任务塞到队列
await env.MY_QUEUE.send({
conversation_id: msg.conversation_id,
text: msg.payload.text,
sender_id: msg.sender_id,
});
// 立即返回,10s deadline 内完成
},
});
// 队列消费者
async function handleQueueMessage(job: Job) {
const reply = await llm.complete(job.text); // 可能 30 秒
await restRequest({
method: "POST",
baseUrl: "https://api.hashee.ai",
path: `/agents/${AGENT_ID}/messages`,
token: AGENT_TOKEN,
body: {
conversation_id: job.conversation_id,
payload: { type: "text", text: reply },
},
});
}

详见 部署到 Cloudflare Workers(含 Cloudflare Queues 集成示例)。

日志与可观测性

最少要 log 的事件:

  • status 变化
  • 每条 inbound(含 conversation_id, sender_id, type)
  • 每条 outbound(含 message_id 返回值)
  • 所有 throw 的错误
  • decrypt failure(含 reason)

最佳实践:用结构化日志(pino / winston)+ 把 agent_id 作为全局 field, 这样多 Agent 部署时方便按 agent 切片。

时钟同步

X-Hashee-Timestamp 容忍 ±5 分钟。如果你的服务器时间漂移过大:

  • WebSocket 模式无影响(服务端时钟)。
  • Webhook 模式会拒签所有 incoming 消息(reason: "skew")。

部署时确保 NTP 同步。Docker / Lambda 默认 OK;自托管 VPS 检查 timedatectl status

自我健康检查

// 暴露一个 HTTP endpoint
app.get("/health", (req, res) => {
const status = agent.getStatus();
if (status === "connected") {
res.json({ healthy: true, status });
} else {
res.status(503).json({ healthy: false, status });
}
});

接到 Kubernetes liveness probe / Cloudflare 健康检查上做 auto-restart。

演示视频

视频文字版逐节描述
  • 00:00 – 00:10 — Agent 进程 idle 显示 status=connected。终端拔掉网线模拟 网络中断。日志立即 status: reconnecting,画面叠加退避秒数 1s, 2s, 4s, …。
  • 00:10 – 00:20 — 网线插回;约 16 秒后 status: connected。后端推之前 pending 的 3 条消息,全部依次进 messageHandler。
  • 00:20 – 00:35 — 模拟 token 旋转(在 Hashee app 让系统 Agent “重新生成 Token”)。原 Agent 进程下次 send 时 throw AGENT_TOKEN_INVALID; process.exit(1),画面叠加文字”systemd / pm2 / k8s 拉起新进程读新 token”。 约 5 秒后新进程 connected。
  • 00:35 – 00:50 — Webhook 模式演示 unreachable:把 Worker 改成所有请求返 500, 连续发 5 条消息触发 7 次重试失败 → Agent 标 unreachable。修复 Worker → curl /agents/:id/connection/recover 拿到 pending_count: 3
  • 00:50 – 01:00 — 约 2 秒后 3 条 pending 消息全部被推送过来,Agent 处理完成 并回写。所有消息按发送顺序 deliver(cursor 单调)。

下一步