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 }