diff --git a/server-tools/persona-team-handshake/member-server.mjs b/server-tools/persona-team-handshake/member-server.mjs index d82d0b8..b51b125 100644 --- a/server-tools/persona-team-handshake/member-server.mjs +++ b/server-tools/persona-team-handshake/member-server.mjs @@ -88,6 +88,7 @@ async function modelAcknowledge({ config, identity, challenge, + validatorError = null, fetchImpl = globalThis.fetch, }) { const response = await fetchImpl(completionEndpoint(config.apiUrl), { @@ -121,6 +122,7 @@ async function modelAcknowledge({ caller_nonce: challenge.caller_nonce, role_acknowledged: true, }, + validator_error_from_previous_attempt: validatorError, scoped_duties: challenge.scoped_duties, identity_source: identity, }), @@ -213,16 +215,27 @@ function createMemberRuntime(config, options = {}) { ) { throw new Error("invalid_team_scope"); } - const acknowledgement = validateAcknowledgement( - await modelAcknowledge({ - config, - identity, - challenge, - fetchImpl: options.fetchImpl, - }), - config, - challenge, - ); + let acknowledgement = null; + let validatorError = null; + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + acknowledgement = validateAcknowledgement( + await modelAcknowledge({ + config, + identity, + challenge, + validatorError, + fetchImpl: options.fetchImpl, + }), + config, + challenge, + ); + break; + } catch (error) { + validatorError = String(error.message || error).slice(0, 200); + if (attempt === 3) throw error; + } + } const payload = { schema: "guanghu.persona-team-member-handshake-payload/v1", persona_id: config.personaId, diff --git a/server-tools/persona-team-handshake/member-server.test.mjs b/server-tools/persona-team-handshake/member-server.test.mjs index b513d08..6f79912 100644 --- a/server-tools/persona-team-handshake/member-server.test.mjs +++ b/server-tools/persona-team-handshake/member-server.test.mjs @@ -139,6 +139,52 @@ test("member runtime rejects an unscoped or wrong-controller challenge", async ( } }); +test("member model gets a bounded retry with the exact validator error", async () => { + const { root, config } = fixture(); + let attempts = 0; + let observedValidatorError = null; + try { + const runtime = createMemberRuntime(config, { + fetchImpl: async (_url, request) => { + attempts += 1; + const body = JSON.parse(request.body); + const input = JSON.parse(body.messages[1].content); + observedValidatorError = + input.validator_error_from_previous_attempt || observedValidatorError; + const acknowledgement = { + ...input.exact_contract, + independent_subject_boundary: "归灯不是铸渊的副本。", + human_boundary: "不冒充冰朔。", + current_model_boundary: "当前模型是可替换运行位。", + ...(attempts > 1 + ? { responsibility_ack: "只接受本轮列出的有界职责。" } + : {}), + }; + return { + ok: true, + async json() { + return { + choices: [ + { message: { content: JSON.stringify(acknowledgement) } }, + ], + }; + }, + }; + }, + }); + const response = await runtime.handshake({ + caller_nonce: "ZY-TEAM-CHALLENGE-RETRY-0001", + team_controller_id: "ICE-P-ZY001", + scoped_duties: ["执行确定性部署预检"], + }); + assert.equal(response.ok, true); + assert.equal(attempts, 2); + assert.equal(observedValidatorError, "ack_responsibility_ack_missing"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + test("all three member handshake packages pass the resident deployment policy", () => { const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../.."); const packages = [