From 9b6a70495d5dc4d64c747e343f8f2d0b607e560c Mon Sep 17 00:00:00 2001 From: Winterrks <160774215+Winterrks@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:02:17 +0100 Subject: [PATCH 1/2] guided: the stub accepts the engine's merged first message (reminder block, no isMeta), with a cross-check fed from the pinned real request layout; compile trigger moves to cowork/guided-packet-compile-fixture-5 (not armed) --- .../cowork-guided-packet-compile-fixture.yml | 4 +- scripts/guest-guided-build/README.md | 2 +- .../guided-compile-inputs.pin.json | 2 +- .../guest-guided-build/test_guided_build.py | 2 +- .../real-request-layout.fixture.json | 52 ++++++++++++++++++ scripts/guest-guided-provider/stub.test.ts | 53 +++++++++++++++++++ scripts/guest-guided-provider/stub.ts | 13 ++++- .../gates/guided-packet-compile-gates.test.ts | 12 +++-- test/gates/guided-packet-pin.test.ts | 4 +- 9 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 scripts/guest-guided-provider/real-request-layout.fixture.json diff --git a/.github/workflows/cowork-guided-packet-compile-fixture.yml b/.github/workflows/cowork-guided-packet-compile-fixture.yml index d7eae89..7a3219f 100644 --- a/.github/workflows/cowork-guided-packet-compile-fixture.yml +++ b/.github/workflows/cowork-guided-packet-compile-fixture.yml @@ -8,7 +8,7 @@ name: guided-packet-compile-fixture # The first steps refuse unless BUILDER_HEAD is an ancestor of the pushed commit and the whole diff is that one line. on: push: - branches: [cowork/guided-packet-compile-fixture-4] + branches: [cowork/guided-packet-compile-fixture-5] permissions: contents: read env: @@ -22,7 +22,7 @@ env: DEPENDENCY_LIST_SHA256: f8255bc2b622416d260ff522134beca24b5c0c503700343012a6d2a7e29cdfbe jobs: compile-only: - if: github.repository == 'yanlingLabs/winter-runtime-sdk' && github.event_name == 'push' && github.ref == 'refs/heads/cowork/guided-packet-compile-fixture-4' && github.event.created == true && github.run_attempt == '1' + if: github.repository == 'yanlingLabs/winter-runtime-sdk' && github.event_name == 'push' && github.ref == 'refs/heads/cowork/guided-packet-compile-fixture-5' && github.event.created == true && github.run_attempt == '1' runs-on: ubuntu-24.04-arm timeout-minutes: 30 steps: diff --git a/scripts/guest-guided-build/README.md b/scripts/guest-guided-build/README.md index 42e536e..243eb39 100644 --- a/scripts/guest-guided-build/README.md +++ b/scripts/guest-guided-build/README.md @@ -293,7 +293,7 @@ pruned-edge and tsconfig-`paths` rules, which the reader builder refuses. It is ## The run `.github/workflows/cowork-guided-packet-compile-fixture.yml` fires only on the **first creation** of the branch -`cowork/guided-packet-compile-fixture-4` (`created == true`, `run_attempt == '1'`). It has no pull request, +`cowork/guided-packet-compile-fixture-5` (`created == true`, `run_attempt == '1'`). It has no pull request, dispatch, schedule or tag trigger, no `paths` filter, no secrets, `contents: read`, no sudo, and it runs on `ubuntu-24.04-arm`. It curls the pinned Bun zip and the 20 pinned tarballs (each checked by a strict checksum), runs `build.py` (which runs `bun build --compile` four times, `cc` six times, plus the semantic parser runs) and diff --git a/scripts/guest-guided-build/guided-compile-inputs.pin.json b/scripts/guest-guided-build/guided-compile-inputs.pin.json index 3362b08..f5a357c 100644 --- a/scripts/guest-guided-build/guided-compile-inputs.pin.json +++ b/scripts/guest-guided-build/guided-compile-inputs.pin.json @@ -234,7 +234,7 @@ ] }, "trigger": { - "branch": "cowork/guided-packet-compile-fixture-4", + "branch": "cowork/guided-packet-compile-fixture-5", "workflow": "cowork-guided-packet-compile-fixture.yml", "sourceWorkflow": "cowork-guided-build-source.yml" }, diff --git a/scripts/guest-guided-build/test_guided_build.py b/scripts/guest-guided-build/test_guided_build.py index 40b5e9b..2942ab9 100644 --- a/scripts/guest-guided-build/test_guided_build.py +++ b/scripts/guest-guided-build/test_guided_build.py @@ -427,7 +427,7 @@ def test_retention_is_capped_at_seven_days_and_the_limit_is_stated(self): self.assertEqual(pins.MAX_PACKET_BYTES,1024**3) def test_trigger_branch_is_new_and_not_a_reader_or_hosted_name(self): for used in ('cowork/instruction-reader-compile-fixture','cowork/reader-hosted-dependency-probe','cowork/reader-hosted-fixture-stderr','cowork/guided-packet-build'):self.assertNotEqual(pins.TRIGGER_BRANCH,used) - self.assertEqual(pins.TRIGGER_BRANCH,'cowork/guided-packet-compile-fixture-4') + self.assertEqual(pins.TRIGGER_BRANCH,'cowork/guided-packet-compile-fixture-5') class NoExecutionTests(unittest.TestCase): SOURCES=('build.py','graph.py','acquire.py','pins.py') diff --git a/scripts/guest-guided-provider/real-request-layout.fixture.json b/scripts/guest-guided-provider/real-request-layout.fixture.json new file mode 100644 index 0000000..d718e1e --- /dev/null +++ b/scripts/guest-guided-provider/real-request-layout.fixture.json @@ -0,0 +1,52 @@ +{ + "source": { + "note": "buildRequestMessages output (renderUserContext with one claudeMd entry, then the prompt), generated from the pinned agent-sdk source; regenerate only when the pin moves", + "repository": "yanlingLabs/winter-agent-sdk", + "commit": "9c980c326638489e0183d7fcddcf1a93553be0ba", + "path": "packages/runtime/src/context/request-layout.ts", + "sha256": "8a664c059c43016f188f34bde83ad1fb3681025e10fb9d86f4541fa36d4fd605" + }, + "dispatch1": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "\nAs you answer the user's questions, you can use the following context:\n# claudeMd\nGuided hosted run guidance, first version.\nThe shared guidance marker is ALPHA.\n\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n\n\n" + }, + { + "type": "text", + "text": "Answer with the first scripted reply." + } + ] + } + ], + "dispatch2": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "\nAs you answer the user's questions, you can use the following context:\n# claudeMd\nGuided hosted run guidance, second version.\nThe shared guidance marker is BRAVO.\n\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n\n\n" + }, + { + "type": "text", + "text": "Answer with the first scripted reply." + } + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "first deterministic answer" + } + ] + }, + { + "role": "user", + "content": "Answer with the second scripted reply." + } + ] +} diff --git a/scripts/guest-guided-provider/stub.test.ts b/scripts/guest-guided-provider/stub.test.ts index d9ff2aa..c148e04 100644 --- a/scripts/guest-guided-provider/stub.test.ts +++ b/scripts/guest-guided-provider/stub.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { expect, test } from "bun:test"; import { createGuidedFixturePort, encodeFrame, LIMITS, SELECTION, STANDIN_SELECTION, sha256 } from "./stub.ts"; @@ -223,3 +224,55 @@ test("the selection argument is explicit and exact: undefined is the default; a expect(() => createGuidedFixturePort(bytes, hash, SELECTION)).not.toThrow(); for (const bad of [{ ...SELECTION }, { ...STANDIN_SELECTION }, null, "winter-standin", {}, 0] as any[]) expect(() => createGuidedFixturePort(bytes, hash, bad)).toThrow("fixture refused"); }); + +// The engine merges the meta userContext message into the following user turn (request-layout.ts buildRequestMessages), so a real first message has no `isMeta`. +const merged = (second = false) => ({ ...turn(second), messages: [{ role: "user", content: [{ type: "text", text: `\n# claudeMd\nSHARED:${second ? "new" : "old"}\nSOURCE:root\nSOURCE:cwd\n\n\n` }, { type: "text", text: "question" }] }] }); +const withTurn = (value: unknown, ordinal = 1, id = "invocation-one") => ({ ...request("begin", 0, ordinal, id), turn: value }); +const refusedAt = async (value: unknown) => { const p = port(); try { await p.exchange(encodeFrame(withTurn(value))); } catch (error: any) { return error.site; } return undefined; }; + +test("the real merged first message (a reminder block then the prompt, no isMeta) is the current userContext, and the old separate-meta shape still is", async () => { + const p = port(); + expect(decode(await p.exchange(encodeFrame(withTurn(merged()))))).toMatchObject({ kind: "accepted" }); + await finish(port()); + const q = port(); + expect(decode(await q.exchange(encodeFrame(withTurn(turn()))))).toMatchObject({ kind: "accepted" }); +}); +test("a first message that is neither meta nor opens with the reminder marker is not the userContext", async () => { + const plain = { ...turn(), messages: [{ role: "user", content: [{ type: "text", text: "SHARED:old\nSOURCE:root\nSOURCE:cwd" }, { type: "text", text: "question" }] }] }; + expect(await refusedAt(plain)).toBe("not_meta"); + const late = { ...turn(), messages: [{ role: "user", content: [{ type: "text", text: "question" }, { type: "text", text: "\nSHARED:old\nSOURCE:root\nSOURCE:cwd\n" }] }] }; + expect(await refusedAt(late)).toBe("not_meta"); + const text = { ...turn(), messages: [{ role: "user", content: "SHARED:old\nSOURCE:root\nSOURCE:cwd" }] }; + expect(await refusedAt(text)).toBe("not_meta"); + expect(await refusedAt({ ...turn(), messages: [{ role: "assistant", content: "" }, { role: "user", content: "q" }] })).toBe("not_meta"); + expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: "\nSHARED:old\nSOURCE:root\nSOURCE:cwd\n" }] })).toBeUndefined(); +}); +test("the reminder marker does not excuse a missing or stale marker: user, system and absent checks still apply to the merged shape", async () => { + const stale = merged(); (stale.messages[0]!.content[0] as any).text = (stale.messages[0]!.content[0] as any).text.replace("SHARED:old", "SHARED:old\nSHARED:new"); + expect(await refusedAt(stale)).toBe("absent_marker"); + const missing = merged(); (missing.messages[0]!.content[0] as any).text = "\nnothing\n"; + expect(await refusedAt(missing)).toBe("user_marker"); + expect(await refusedAt({ ...merged(), system: "nothing", systemBlocks: undefined })).toBe("system_marker"); +}); + +// The cross-check: the REAL engine layout, vendored with its source pinned, run through the stub for both dispatches of the hosted fixture. +const layout = JSON.parse(readFileSync(new URL("./real-request-layout.fixture.json", import.meta.url), "utf8")); +test("the vendored fixture names the pinned agent-sdk source it was generated from", () => { + expect(layout.source).toMatchObject({ repository: "yanlingLabs/winter-agent-sdk", commit: "9c980c326638489e0183d7fcddcf1a93553be0ba", path: "packages/runtime/src/context/request-layout.ts", sha256: "8a664c059c43016f188f34bde83ad1fb3681025e10fb9d86f4541fa36d4fd605" }); + expect(layout.dispatch1[0].isMeta).toBeUndefined(); + expect(layout.dispatch1).toHaveLength(1); expect(layout.dispatch1[0].content).toHaveLength(2); + expect(layout.dispatch2[0].isMeta).toBeUndefined(); expect(layout.dispatch2.map((m: any) => m.role)).toEqual(["user", "assistant", "user"]); +}); +test("the stub accepts the real engine's request layout for both hosted dispatches, with the real guidance markers", async () => { + const markers = (version: string, mark: string) => [`Guided hosted run guidance, ${version} version.`, `The shared guidance marker is ${mark}.`]; + const hosted = { version: 1, scope: { ...scope }, selection: SELECTION, steps: [ + { system: ["Cowork current host context"], user: markers("first", "ALPHA"), absentSystem: [], absentUser: markers("second", "BRAVO"), response: "first deterministic answer" }, + { system: ["Cowork current host context"], user: markers("second", "BRAVO"), absentSystem: [], absentUser: markers("first", "ALPHA"), response: "second deterministic answer" }] }; + const p = port(hosted); + for (const [ordinal, key] of [[1, "dispatch1"], [2, "dispatch2"]] as const) { + const id = `invocation-${ordinal}`, t = { model: SELECTION.providerModelId, system: "Cowork current host context\n\nbase", messages: layout[key] }; + expect(decode(await p.exchange(encodeFrame(withTurn(t, ordinal, id))))).toMatchObject({ kind: "accepted" }); + for (const sequence of [1, 2]) await p.exchange(encodeFrame(request("next", sequence, ordinal, id))); + } + expect(p.receipt()).toMatchObject({ receivedBegins: 2, acceptedBegins: 2, completed: 2 }); +}); diff --git a/scripts/guest-guided-provider/stub.ts b/scripts/guest-guided-provider/stub.ts index b09ed2c..61fcacd 100644 --- a/scripts/guest-guided-provider/stub.ts +++ b/scripts/guest-guided-provider/stub.ts @@ -60,6 +60,14 @@ function content(value: unknown): string { if (!Array.isArray(value)) refuse(); return value.map(item => { const block = object(item); keys(block, ["type", "text"]); if (block.type !== "text" || typeof block.text !== "string") refuse(); return block.text; }).join("\n"); } +const REMINDER_OPEN = ""; +/** True when the message content's first text (the string itself, or the first block) opens with the system-reminder marker. */ +function startsWithReminder(value: unknown): boolean { + if (typeof value === "string") return value.startsWith(REMINDER_OPEN); + if (!Array.isArray(value) || !value.length) return false; + const first = value[0] as ObjectValue | null; + return !!first && typeof first === "object" && first.type === "text" && typeof first.text === "string" && first.text.startsWith(REMINDER_OPEN); +} function optionalTurnFields(turn: ObjectValue): void { for (const key of ["cacheKey"]) if (turn[key] !== undefined) text(turn[key], LIMITS.requestBytes); if (turn.effort !== undefined && !(typeof turn.effort === "number" && Number.isFinite(turn.effort)) && !(typeof turn.effort === "string" && ["low", "medium", "high", "xhigh", "max"].includes(turn.effort))) refuse(); @@ -98,7 +106,10 @@ function validateTurn(value: unknown, step: Step, selection: StubSelection): Obj const current = object(turn.messages[0]); // Match current userContext only; stale markers in historical user/assistant text // cannot satisfy the next-dispatch guidance oracle. - if (current.role !== "user" || (selection !== STANDIN_SELECTION && current.isMeta !== true)) refuse("not_meta"); + // The engine merges the meta userContext message into the user turn that follows it (request-layout.ts buildRequestMessages: consecutive + // user-role messages become one, which drops `isMeta`), so the real first message is `[ block, prompt block]` with no flag. + // Either shape is the current userContext; anything else as the first message is not. + if (current.role !== "user" || (selection !== STANDIN_SELECTION && current.isMeta !== true && !startsWithReminder(current.content))) refuse("not_meta"); ordered(turn.system, step.system, "system_marker"); ordered(content(current.content), step.user, "user_marker"); absent(turn.system, step.absentSystem); absent(content(current.content), step.absentUser); return frozen(turn); diff --git a/test/gates/guided-packet-compile-gates.test.ts b/test/gates/guided-packet-compile-gates.test.ts index c85b1bd..3f7c0d6 100644 --- a/test/gates/guided-packet-compile-gates.test.ts +++ b/test/gates/guided-packet-compile-gates.test.ts @@ -16,11 +16,11 @@ const WORKFLOWS_DIR = join(ROOT, ".github", "workflows"); const BUILD_DIR = "scripts/guest-guided-build"; const WORKFLOW_NAME = "cowork-guided-packet-compile-fixture.yml"; const SOURCE_NAME = "cowork-guided-build-source.yml"; -const TRIGGER_BRANCH = "cowork/guided-packet-compile-fixture-4"; +const TRIGGER_BRANCH = "cowork/guided-packet-compile-fixture-5"; const REPOSITORY = "yanlingLabs/winter-runtime-sdk"; const ALLOWED_ACTIONS = new Set(["actions/checkout", "actions/upload-artifact"]); // Branches that already fired, or are armed for, another workflow, or that carry this builder's own source: never reusable here. -const USED_BRANCHES = ["cowork/instruction-reader-compile-fixture", "cowork/reader-hosted-dependency-probe", "cowork/reader-hosted-fixture-stderr", "cowork/guided-packet-build", "cowork/guided-packet-compile-fixture", "cowork/guided-packet-compile-fixture-2", "cowork/guided-packet-compile-fixture-3"]; +const USED_BRANCHES = ["cowork/instruction-reader-compile-fixture", "cowork/reader-hosted-dependency-probe", "cowork/reader-hosted-fixture-stderr", "cowork/guided-packet-build", "cowork/guided-packet-compile-fixture", "cowork/guided-packet-compile-fixture-2", "cowork/guided-packet-compile-fixture-3", "cowork/guided-packet-compile-fixture-4"]; const ZERO_HEAD = "0".repeat(40); const BUN_URL = "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-aarch64.zip"; const WORKFLOW_PATH = ".github/workflows/cowork-guided-packet-compile-fixture.yml"; @@ -265,7 +265,7 @@ describe("guided packet compile-fixture workflow gates", () => { test("the trigger branch is new: not a reader, hosted or builder-source branch", () => { expect(USED_BRANCHES).not.toContain(TRIGGER_BRANCH); - expect(TRIGGER_BRANCH).toBe("cowork/guided-packet-compile-fixture-4"); + expect(TRIGGER_BRANCH).toBe("cowork/guided-packet-compile-fixture-5"); }); test("the trigger is the exact first-creation push of one new branch, and nothing else", () => { @@ -313,9 +313,11 @@ const MUTATIONS: Array<[string, (text: string) => string, string]> = [ ["widen the branch to a glob", sub(`[${TRIGGER_BRANCH}]`, "['cowork/**']"), "on.push must be exactly one literal branch and nothing else"], ["add a tags filter", sub(" push:\n branches:", " push:\n tags: ['*']\n branches:"), "on.push must be exactly one literal branch and nothing else"], ["add a paths filter", sub(`branches: [${TRIGGER_BRANCH}]`, `branches: [${TRIGGER_BRANCH}]\n paths: ['scripts/**']`), "on.push must be exactly one literal branch and nothing else"], - ["a further compile-fixture name replaces the trigger", (t) => t.replaceAll(TRIGGER_BRANCH, "cowork/guided-packet-compile-fixture-5"), "workflow names a compile-fixture branch other than the trigger: cowork/guided-packet-compile-fixture-5"], - ["a further compile-fixture name in a comment", (t) => t.replace("name: guided-packet-compile-fixture", "# cowork/guided-packet-compile-fixture-5\nname: guided-packet-compile-fixture"), "workflow names a compile-fixture branch other than the trigger: cowork/guided-packet-compile-fixture-5"], + ["a further compile-fixture name replaces the trigger", (t) => t.replaceAll(TRIGGER_BRANCH, "cowork/guided-packet-compile-fixture-6"), "workflow names a compile-fixture branch other than the trigger: cowork/guided-packet-compile-fixture-6"], + ["a further compile-fixture name in a comment", (t) => t.replace("name: guided-packet-compile-fixture", "# cowork/guided-packet-compile-fixture-6\nname: guided-packet-compile-fixture"), "workflow names a compile-fixture branch other than the trigger: cowork/guided-packet-compile-fixture-6"], ["a bare prefix of the trigger name", (t) => t.replace("name: guided-packet-compile-fixture", "# cowork/guided-packet-compile-fixture\nname: guided-packet-compile-fixture"), "workflow names a compile-fixture branch other than the trigger: cowork/guided-packet-compile-fixture"], + ["reuse the frozen fourth-run branch", (t) => t.replaceAll(TRIGGER_BRANCH, "cowork/guided-packet-compile-fixture-4"), "workflow names a used branch: cowork/guided-packet-compile-fixture-4"], + ["name the frozen fourth-run branch beside the trigger", (t) => t.replace("name: guided-packet-compile-fixture", "# cowork/guided-packet-compile-fixture-4\nname: guided-packet-compile-fixture"), "workflow names a used branch: cowork/guided-packet-compile-fixture-4"], ["reuse the frozen third-run branch", (t) => t.replaceAll(TRIGGER_BRANCH, "cowork/guided-packet-compile-fixture-3"), "workflow names a used branch: cowork/guided-packet-compile-fixture-3"], ["name the frozen third-run branch beside the trigger", (t) => t.replace("name: guided-packet-compile-fixture", "# cowork/guided-packet-compile-fixture-3\nname: guided-packet-compile-fixture"), "workflow names a used branch: cowork/guided-packet-compile-fixture-3"], ["reuse the frozen second-run branch", (t) => t.replaceAll(TRIGGER_BRANCH, "cowork/guided-packet-compile-fixture-2"), "workflow names a used branch: cowork/guided-packet-compile-fixture-2"], diff --git a/test/gates/guided-packet-pin.test.ts b/test/gates/guided-packet-pin.test.ts index 636bc64..6607a0e 100644 --- a/test/gates/guided-packet-pin.test.ts +++ b/test/gates/guided-packet-pin.test.ts @@ -87,13 +87,15 @@ const RUN4_ORIGINS: Record; placeholders: string[]; superseded: readonly string[] } /** The runs this directory pins, by run id. A pin whose run id is not here is refused. */ const RUNS: Record = { 39165162201: { branch: "cowork/guided-packet-compile-fixture", artifactId: 12149668717, origins: RUN1_ORIGINS, placeholders: PLACEHOLDERS, superseded: SUPERSEDED_BUILDER_FILES }, 39227365159: { branch: "cowork/guided-packet-compile-fixture-2", artifactId: 12174387240, origins: RUN2_ORIGINS, placeholders: [], superseded: RUN2_HISTORY_BUILDER_FILES }, 39327386044: { branch: "cowork/guided-packet-compile-fixture-3", artifactId: 12223150881, origins: RUN3_ORIGINS, placeholders: [], superseded: RUN3_SUPERSEDED_BUILDER_FILES }, - 39391435225: { branch: "cowork/guided-packet-compile-fixture-4", artifactId: 12249582986, origins: RUN4_ORIGINS, placeholders: [], superseded: [] }, + 39391435225: { branch: "cowork/guided-packet-compile-fixture-4", artifactId: 12249582986, origins: RUN4_ORIGINS, placeholders: [], superseded: RUN4_SUPERSEDED_BUILDER_FILES }, }; export function pinFiles(): string[] { From 072239d450cf05b43ee2e296797552bf6b25c933 Mon Sep 17 00:00:00 2001 From: Winterrks <160774215+Winterrks@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:05:22 +0100 Subject: [PATCH 2/2] guided: in the merged layout the stub matches user and absent markers against the closed reminder block only, and refuses a single-block or unclosed reminder --- scripts/guest-guided-provider/stub.test.ts | 18 +++++++++++++- scripts/guest-guided-provider/stub.ts | 28 +++++++++++++--------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/scripts/guest-guided-provider/stub.test.ts b/scripts/guest-guided-provider/stub.test.ts index c148e04..a8b0052 100644 --- a/scripts/guest-guided-provider/stub.test.ts +++ b/scripts/guest-guided-provider/stub.test.ts @@ -245,7 +245,23 @@ test("a first message that is neither meta nor opens with the reminder marker is const text = { ...turn(), messages: [{ role: "user", content: "SHARED:old\nSOURCE:root\nSOURCE:cwd" }] }; expect(await refusedAt(text)).toBe("not_meta"); expect(await refusedAt({ ...turn(), messages: [{ role: "assistant", content: "" }, { role: "user", content: "q" }] })).toBe("not_meta"); - expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: "\nSHARED:old\nSOURCE:root\nSOURCE:cwd\n" }] })).toBeUndefined(); + // the reminder-first shape needs an ARRAY of at least two blocks whose first is one closed reminder + expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: "\nSHARED:old\nSOURCE:root\nSOURCE:cwd\n" }] })).toBe("not_meta"); + expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: [{ type: "text", text: "\nSHARED:old\nSOURCE:root\nSOURCE:cwd\n" }] }] })).toBe("not_meta"); + expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: [{ type: "text", text: "\nSHARED:old\nSOURCE:root\nSOURCE:cwd" }, { type: "text", text: "q" }] }] })).toBe("not_meta"); + expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: [{ type: "text", text: "\nSHARED:old\n\nSOURCE:root\nSOURCE:cwd\n" }, { type: "text", text: "q" }] }] })).toBe("not_meta"); +}); +test("reviewer probes: markers carried by the prompt block, or by one block the user typed, are not the userContext", async () => { + // (a) a reminder block with no markers, then a prompt block that carries them + const a = { ...turn(), messages: [{ role: "user", content: [{ type: "text", text: "\nnothing here\n\n" }, { type: "text", text: "SHARED:old\nSOURCE:root\nSOURCE:cwd" }] }] }; + expect(await refusedAt(a)).toBe("user_marker"); + // (b) one user-typed block that opens with the tag and carries the markers (no second block, no closing tag) + const b = { ...turn(), messages: [{ role: "user", content: [{ type: "text", text: " SHARED:old\nSOURCE:root\nSOURCE:cwd" }] }] }; + expect(await refusedAt(b)).toBe("not_meta"); + expect(await refusedAt({ ...turn(), messages: [{ role: "user", content: " SHARED:old\nSOURCE:root\nSOURCE:cwd" }] })).toBe("not_meta"); + // an absent marker that only the prompt block carries is not the userContext's, so it does not refuse a good reminder + const c = merged(); (c.messages[0]!.content as any[])[1].text = "SHARED:new"; + const p = port(); expect(decode(await p.exchange(encodeFrame(withTurn(c))))).toMatchObject({ kind: "accepted" }); }); test("the reminder marker does not excuse a missing or stale marker: user, system and absent checks still apply to the merged shape", async () => { const stale = merged(); (stale.messages[0]!.content[0] as any).text = (stale.messages[0]!.content[0] as any).text.replace("SHARED:old", "SHARED:old\nSHARED:new"); diff --git a/scripts/guest-guided-provider/stub.ts b/scripts/guest-guided-provider/stub.ts index 61fcacd..5e5fce4 100644 --- a/scripts/guest-guided-provider/stub.ts +++ b/scripts/guest-guided-provider/stub.ts @@ -60,13 +60,15 @@ function content(value: unknown): string { if (!Array.isArray(value)) refuse(); return value.map(item => { const block = object(item); keys(block, ["type", "text"]); if (block.type !== "text" || typeof block.text !== "string") refuse(); return block.text; }).join("\n"); } -const REMINDER_OPEN = ""; -/** True when the message content's first text (the string itself, or the first block) opens with the system-reminder marker. */ -function startsWithReminder(value: unknown): boolean { - if (typeof value === "string") return value.startsWith(REMINDER_OPEN); - if (!Array.isArray(value) || !value.length) return false; +const REMINDER_OPEN = "", REMINDER_CLOSE = ""; +/** The merged layout's userContext: message content that is an array of at least two text blocks whose FIRST is one closed system-reminder + * (the prompt follows in its own block). Returns that first block's text, or undefined for any other content. */ +function reminderBlock(value: unknown): string | undefined { + if (!Array.isArray(value) || value.length < 2) return undefined; const first = value[0] as ObjectValue | null; - return !!first && typeof first === "object" && first.type === "text" && typeof first.text === "string" && first.text.startsWith(REMINDER_OPEN); + if (!first || typeof first !== "object" || first.type !== "text" || typeof first.text !== "string") return undefined; + const body = first.text.trimEnd(); + return first.text.startsWith(REMINDER_OPEN) && body.endsWith(REMINDER_CLOSE) && body.indexOf(REMINDER_CLOSE) === body.length - REMINDER_CLOSE.length ? first.text : undefined; } function optionalTurnFields(turn: ObjectValue): void { for (const key of ["cacheKey"]) if (turn[key] !== undefined) text(turn[key], LIMITS.requestBytes); @@ -107,11 +109,15 @@ function validateTurn(value: unknown, step: Step, selection: StubSelection): Obj // Match current userContext only; stale markers in historical user/assistant text // cannot satisfy the next-dispatch guidance oracle. // The engine merges the meta userContext message into the user turn that follows it (request-layout.ts buildRequestMessages: consecutive - // user-role messages become one, which drops `isMeta`), so the real first message is `[ block, prompt block]` with no flag. - // Either shape is the current userContext; anything else as the first message is not. - if (current.role !== "user" || (selection !== STANDIN_SELECTION && current.isMeta !== true && !startsWithReminder(current.content))) refuse("not_meta"); - ordered(turn.system, step.system, "system_marker"); ordered(content(current.content), step.user, "user_marker"); - absent(turn.system, step.absentSystem); absent(content(current.content), step.absentUser); + // user-role messages become one, which drops `isMeta`), so the real first message is `[closed block, prompt block]` with no + // flag. The separate `isMeta` message is the other accepted shape. In the merged shape the markers are matched against the reminder block + // ONLY: a marker in the prompt block, or in a block the user typed, is not the userContext. + if (current.role !== "user") refuse("not_meta"); + let userText: string; + if (selection === STANDIN_SELECTION || current.isMeta === true) userText = content(current.content); + else { const block = reminderBlock(current.content); if (block === undefined) return refuse("not_meta"); userText = block; } + ordered(turn.system, step.system, "system_marker"); ordered(userText, step.user, "user_marker"); + absent(turn.system, step.absentSystem); absent(userText, step.absentUser); return frozen(turn); } export interface CapturedTurn { readonly invocationId: string; readonly ordinal: number; readonly sha256: string; readonly turn: Readonly }