/** * 招聘后同进程调 agent-creator 填自己的定义文件——带互斥锁 + 有限重试 + 失败落地。 * * 32 号文档方案 B 的落地。修的是 `company.js` hire 路由原来那发 * `fetch('http://127.0.0.1:${PORT}/api/agent/agent-creator/chat', ...).catch(...)`: * fire-and-forget,`.catch` 只接网络层错误,不检查响应状态码;agent-creator 是单会话, * 撞车时 `/api/agent/:agentId/chat` 返回 429,被原样当成"成功"吞掉——新招的角色永久停在 * base 空壳 + `status=training`,唯一痕迹是一行 console.error(撞车走的是 429 正常响应, * 连这行都不会触发)。 * * 这里改成同进程直接调 `AgentManager.callAgent`,绕开那次多余的 HTTP 回环,换来: * - 真实的调用结果/异常,而不是网络层 `.catch`; * - 撞车时**等一等再试**(有限次数 + 退避),不是当场丢弃; * - 重试耗尽后在本体目录写一个哨兵文件,而不是一行日志——不动 * `system_agents.status`/`config.json.status`(那两个字段还有 draft→active 的 * 毕业逻辑在读,改它们的取值域风险和收益不对等,见 32 号文档 §四.3)。 * * ⚠️ 互斥锁复用的是 `agent-session-lock.js`,跟 `routes/agent-chat.js` 的 * `POST /:agentId/chat` 共享同一个 Map——不能各起一把。原因见该模块头注: * `SessionPool.getOrCreate()` 不检查上一次调用有没有跑完,两条路径各自以为自己 * 没有并发的话,会把第二轮 prompt 发进一个还在处理中的 CLI 会话,比现在的 * "被拒绝丢弃"更危险(协议层内容串台,未定义行为)。 */ const fs = require('fs'); const path = require('path'); const locator = require('./agent-locator'); const { agentChatLocks, AGENT_LOCK_TIMEOUT } = require('../src/core/agent-session-lock'); const MAX_ATTEMPTS = 3; // 重试之间的等待——不是精算出来的数字,是"给撞车的那一方一点时间跑完"的量级; // 真正的硬顶是 MAX_ATTEMPTS,不是靠拉长等待时间去覆盖更长的撞车窗口。 const RETRY_DELAY_MS = [5_000, 15_000]; const FAILURE_MARKER_NAME = '.body_fill_failed.json'; function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function failureMarkerPath(bodyDir) { return path.join(bodyDir, FAILURE_MARKER_NAME); } /** 重试耗尽后落地——**必须能被查询/扫描到**,这是 32 号文档方案 B 的唯一硬要求。 */ function writeFailureMarker(bodyDir, info) { try { fs.writeFileSync(failureMarkerPath(bodyDir), JSON.stringify({ ...info, failedAt: new Date().toISOString(), }, null, 2)); } catch (e) { // 连哨兵文件都写不出去(目录权限/磁盘满)——这一步再失败也不能让调用方炸, // 招聘流程本身(scaffoldNewAgent 已经建好目录)不该被这个次要写入拖下水。 console.error(`[BodyFill] ⚠️ 写失败标记本身也失败了: ${e.message}`); } } /** 成功之后必须清掉上一轮可能留下的失败标记——不然人工重试成功了,哨兵文件还在骗人。 */ function clearFailureMarker(bodyDir) { try { fs.unlinkSync(failureMarkerPath(bodyDir)); } catch { // 本来就没有这个文件(多数情况)——不是错误 } } /** * @param {object} a * @param {object} a.agentManager `AgentManager` 实例(调用方传入,不在这里 new—— * 测试时可以注入假实现,不用真起 Claude CLI) * @param {string} a.targetAgentId 要填定义的那个新招角色 id(== 本体目录名) * @param {string} a.message 发给 agent-creator 的完整 brief(调用方已经拼好, * 这个函数不关心内容,只关心「发出去、等结果、失败重试」) * @returns {Promise<{ok: boolean, attempts: number, error?: string}>} */ async function fillAgentBodyReliably({ agentManager, targetAgentId, message }) { const bodyDir = locator.bodyDir(targetAgentId); let lastError = null; for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { const existingLock = agentChatLocks.get('agent-creator'); if (existingLock && Date.now() - existingLock.startedAt < AGENT_LOCK_TIMEOUT) { // 忙——等一等,不占位、不硬抢、不当场放弃。这是原来那发 fetch 唯一缺的东西。 console.log(`[BodyFill] 🔒 agent-creator 正忙(conv ${existingLock.convId}),` + `第 ${attempt}/${MAX_ATTEMPTS} 次尝试改为等待`); lastError = new Error(`agent-creator 正忙(活跃会话 ${existingLock.convId})`); if (attempt < MAX_ATTEMPTS) await sleep(RETRY_DELAY_MS[attempt - 1] ?? RETRY_DELAY_MS.at(-1)); continue; } const convId = `body-fill-${targetAgentId}-a${attempt}-${Date.now().toString(36)}`; agentChatLocks.set('agent-creator', { convId, startedAt: Date.now() }); try { await agentManager.callAgent('agent-creator', message, { metadata: { taskId: convId }, }); clearFailureMarker(bodyDir); return { ok: true, attempts: attempt }; } catch (err) { lastError = err; console.error(`[BodyFill] ⚠️ agent-creator 第 ${attempt}/${MAX_ATTEMPTS} 次调用失败: ${err.message}`); if (attempt < MAX_ATTEMPTS) await sleep(RETRY_DELAY_MS[attempt - 1] ?? RETRY_DELAY_MS.at(-1)); } finally { // 只释放"自己那一把"——万一在途期间锁已经被别的机制清过又被第三方占了, // 不能凭空把第三方的锁摘掉(同 agent-chat.js releaseLock 的判据)。 const cur = agentChatLocks.get('agent-creator'); if (cur && cur.convId === convId) agentChatLocks.delete('agent-creator'); } } writeFailureMarker(bodyDir, { targetAgentId, attempts: MAX_ATTEMPTS, lastError: lastError ? lastError.message : null, }); return { ok: false, attempts: MAX_ATTEMPTS, error: lastError ? lastError.message : null }; } module.exports = { fillAgentBodyReliably, FAILURE_MARKER_NAME };