From ce3885d4065c236049b8c13f5ca78c661135f3c9 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 27 May 2026 20:36:51 +0200 Subject: [PATCH] Handle pasted Scattermind payload wrappers --- scripts/check-rank-feedback.mjs | 36 +++++++++++++++++++++++++++++ server.js | 40 ++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 68ae05d..e9371b6 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -2153,6 +2153,42 @@ try { assert.equal(stringifiedRankerInput.handoff.readiness.status, 'ready'); assert.deepEqual(stringifiedRankerInput.handoff.warnings, []); + const proseWrappedPayloadEnvelopeResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + idea: `Here is the Scattermind handoff I copied from the reading page:\n\n${JSON.stringify({ + payload: { + sourceName: 'Scattermind', + reference_code: 'SM-PROSE-PAYLOAD-NEXT-STEPS-1', + working_name: 'Prose-wrapped payload bridge', + ideaText: 'A tired user pasted a wrapper payload with next_steps instead of a pristine Ranker object.', + context: 'Manual proof first. Avoid saved workspace dashboards before one source-traced action works.', + next_steps: [ + { id: 'payload-next-step-active', action: 'Payload source-traced build-order active slice', why: 'Turn the payload-wrapped Scattermind handoff into one defended build order with provenance.', evidence_needed: 'Can a payload-wrapped next_steps export still produce one Do first action?', suggested_lane: 'do-first', source_item_id: 'payload-next-step-1', source_title: 'Payload next steps', ranker_hints: { value: 10, effort: 1, confidence: 9, urgency: 9, risk: 1 } }, + { id: 'payload-next-step-copy', action: 'Payload next-step copy brief', evidence_needed: 'Does the copied result keep source trace?', suggested_lane: 'validate-next', source_item_id: 'payload-next-step-2', source_title: 'Payload next steps' }, + ], + parking_lot: [ + { id: 'payload-next-step-dashboard', action: 'Payload next-step dashboard', evidence_needed: 'Not before proof.', source_item_id: 'payload-next-step-3', source_title: 'Payload next steps' }, + ], + }, + })}`, + mode: 'mvp', + }), + }); + assert.equal(proseWrappedPayloadEnvelopeResponse.status, 200); + const proseWrappedPayloadEnvelope = await proseWrappedPayloadEnvelopeResponse.json(); + assert.equal(proseWrappedPayloadEnvelope.input.embeddedPayloadSource, 'idea'); + assert.equal(proseWrappedPayloadEnvelope.input.provenance.artifactId, 'SM-PROSE-PAYLOAD-NEXT-STEPS-1'); + assert.equal(proseWrappedPayloadEnvelope.input.provenance.snapshotTitle, 'Prose-wrapped payload bridge'); + assert.equal(proseWrappedPayloadEnvelope.input.optionCount, 3); + assert.equal(proseWrappedPayloadEnvelope.ranked[0].id, 'payload-next-step-active'); + assert.equal(proseWrappedPayloadEnvelope.ranked[0].provenance.sourceSection, 'nextActions'); + assert.ok(['do', 'test'].includes(proseWrappedPayloadEnvelope.ranked.find(item => item.id === 'payload-next-step-copy').lane.id)); + assert.equal(proseWrappedPayloadEnvelope.ranked.find(item => item.id === 'payload-next-step-dashboard').lane.id, 'park'); + assert.equal(proseWrappedPayloadEnvelope.handoff.readiness.status, 'ready'); + assert.deepEqual(proseWrappedPayloadEnvelope.handoff.warnings, []); + const gameRouteGuardrailResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index f10a051..c74a564 100644 --- a/server.js +++ b/server.js @@ -587,6 +587,11 @@ function looksLikeRankPayload(value = {}) { || value.concept_map_json || value.buildOrderPreview || value.build_order_preview + || value.payload + || value.rankPayload + || value.rank_payload + || value.scattermindPayload + || value.scattermind_payload || value.opening_reflection || value.restated_idea || value.ideaText @@ -597,6 +602,10 @@ function looksLikeRankPayload(value = {}) { || Array.isArray(value.recommended_actions) || Array.isArray(value.suggestedActions) || Array.isArray(value.suggested_actions) + || Array.isArray(value.nextSteps) + || Array.isArray(value.next_steps) + || Array.isArray(value.recommendedNextSteps) + || Array.isArray(value.recommended_next_steps) || Array.isArray(value.nextActions) || Array.isArray(value.next_actions) || Array.isArray(value.next48Hours) @@ -666,6 +675,24 @@ function looksLikeRankPayload(value = {}) { ); } +function payloadEnvelopeFrom(input = {}) { + const body = objectFrom(input); + return objectFrom( + body.payload + || body.rankPayload + || body.rank_payload + || body.scattermindPayload + || body.scattermind_payload + ); +} + +function unwrapPayloadEnvelope(input = {}) { + const body = objectFrom(input); + const payload = payloadEnvelopeFrom(body); + if (!Object.keys(payload).length || !looksLikeRankPayload(payload)) return body; + return { ...body, ...payload }; +} + function extractFirstJsonObject(text = '') { const start = text.indexOf('{'); if (start < 0) return ''; @@ -763,13 +790,14 @@ function expandEmbeddedRankPayload(body = {}) { ? original[key] : null; if (!embedded) continue; + const unwrappedEmbedded = unwrapPayloadEnvelope(embedded); const expanded = stringOnlyEmbeddedKeys.has(key) - ? { ...original, [key]: embedded } - : { ...original, ...embedded }; - if (key === 'idea' && !embedded.idea && !embedded.ideaText) expanded.idea = ''; - if (key === 'optionsText' && !embedded.optionsText) expanded.optionsText = ''; - if (original.mode && !embedded.mode) expanded.mode = original.mode; - if (original.context && !embedded.context) expanded.context = original.context; + ? { ...original, [key]: unwrappedEmbedded } + : { ...original, ...unwrappedEmbedded }; + if (key === 'idea' && !unwrappedEmbedded.idea && !unwrappedEmbedded.ideaText) expanded.idea = ''; + if (key === 'optionsText' && !unwrappedEmbedded.optionsText) expanded.optionsText = ''; + if (original.mode && !unwrappedEmbedded.mode) expanded.mode = original.mode; + if (original.context && !unwrappedEmbedded.context) expanded.context = original.context; expanded._embeddedPayloadSource = key; return expanded; }