diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md
index 76e30fa9..e9ce8001 100644
--- a/docs/BUG_HISTORY.md
+++ b/docs/BUG_HISTORY.md
@@ -2766,6 +2766,23 @@
- 复发自:无(新 Agentic Domain 主链;旧防线缺失的直接原因见下)
- 修复版本:本地 staging 候选(未 push / deploy)
+### 后续推进(2026-08-13 · stream agent execution + entry routing lane)
+
+- 状态:resolved(local candidate;真实 PostgreSQL fixture 待远端 runner 执行)
+- 最近更新:2026-08-13
+- 现象:旧 `/api/rectification/agent` 仍消费 `textStream`,客户端 history 可覆盖服务端历史,`maxSteps=8` 全局固定,billing identity 是 sessionId;旧工具让模型传 `candidate_range/events[]`,`confirmedGate` 在进程内;UI 的“思考状态”不能证明 Skill/工具真的执行过,候选卡从 Agent 文本或隐藏 sentinel 解析。
+- 根因:第一轮 V9 runtime 落地了 Case/Evidence/Turn/Receipt 表与 open API,但 agent 执行层仍是旧 Direct Agentic 传话筒:没有 durable turn(pending→completed)、没有 fullStream→allowlist NDJSON 映射、没有 Case-ref 工具、没有 Skill 真实加载证据、没有 DB 驱动的 runtime selector。
+- 修复:
+ 1. 重构 `agentic-rectification.ts`:系统提示词压缩为约 25 行高优先边界(不再复制 gate→scan→score→diagnostics);固定加载 `skills/jyotish-birth-time-rectification`;`maxSteps` 按 action 有界(opening/read-only 6、evidence 8、rescore 12、accept/confirm 6)+ 硬上限 16 + 重复工具调用检测(>3 次相同调用中止)。
+ 2. 新增十个 Case-ref 工具(`rectification-v9-tools.ts`):read-case / propose-evidence / confirm-evidence / revise-evidence / compare-candidates / read-diagnostics / offer-candidates / accept-candidate / confirm-birth-time / close-case;input 只含 caseId/sourceTurnId/evidenceId/resultId/candidateId/quote/proposedKind 等最小引用,绝不接 userId/出生资料/range/events[]/分数/权限开关;每个工具走 RLS 仅 service_role 的 RPC(evidence ledger、指纹缓存、receipt、幂等);`accepted` 与 `confirmed` 由 RPC 分列,confirm 需要 confirmation gate + 用户原话 consent quote 原文匹配;candidate 相同 evidence/range/engine 指纹复用缓存。
+ 3. `/api/rectification/agent` 重写为 Case-ref API:请求仅 caseId/sessionId/requestId/action/message;服务端验证 Case↔Session exact binding 与 `chat_sessions.agentic_rectification_case_id`;客户端 history 不再进入上下文,每轮先从持久化 dossier 读取 turns/evidence;先原子 pending Turn,成功才 completed(半截文本永不成为 settled history);消费 `result.fullStream` 并按 allowlist 输出 NDJSON(run.started/skill.started/skill.loaded/case.loaded/evidence.*/candidates.*/diagnostics.completed/candidate.accepted/birth_time.confirmed/answer.delta/run.completed/run.failed),reasoning/raw/provider metadata/tool args/results/出生资料/评分/DB 错误永不透传;skill 加载证据来自真实框架(`agent.getSkill` 预校验 + fullStream skill tool-call/tool-result),首轮无 skill.started/loaded 允许一次受控重试,仍失败则 Turn failed/retryable 并释放用量;billing identity 绑定 `rectification:case:{caseId}`,opening/read-only 不扣,首次实质运行预留一次,恢复/重试不重复;断线 abort 传播释放。
+ 4. 新增向前业务迁移 `20260813010000_agentic_rectification_v9_agent_api.sql`:case dossier / case compute(含 baseline 但不向模型暴露)/ turn finalize / candidate persist(指纹缓存) / case-scoped accept / consent-gated confirm / guarded transition / needs_rebaseline profile guard trigger / `agentic_rectification_run_phases`(持久化 skill.started/loaded 等 phases)/ turn receipt 读取;`rectification_runtime_version` feature flag(published、100%,config version=v9 legacy_mode=readonly),DB 驱动 selector,两个 runtime 不可能同时写 profile/扣费/confirm。
+ 5. 前端:`page.tsx` 删除 `hasRectificationSession` 与 `sessions.find(sessionType==='birth_time_rectification')`;新增 `openRectificationFromHomepage/openRectificationSession(sessionId)/startNewRectification`,全部请求服务端 Case open API 并使用返回的 exact sessionId/caseId;首页 CTA 由 entry-summary 驱动(开始/继续上次/再次校正);侧边栏校正 Session 点击走 intent=session + 精确 sessionId(terminal 只读 + “再次校正”);`shouldStartOpening` 只来自服务端;聊天组件改为 caseId/sessionId/readonly/shouldStartOpening 初始化并从持久化 Turns 恢复,候选卡来自 Candidate Snapshot API,活动展示来自真实 NDJSON + 持久化 receipt(可折叠“本轮做了什么”,不显示 reasoning),删除本地 timer 模拟与隐藏 sentinel。
+ 6. 新增 `GET cases/[caseId]?sessionId=` 返回持久化 turns/evidence/latest_result/逐轮 receipt 支持刷新恢复;新增 `POST cases/[caseId]/candidates/accept`(UI 候选卡直连,不扣费、幂等、case-scoped)。
+- 验证:新增 `rectification-v9-entry-routing.test.ts`(13)、`rectification-v9-evidence.test.ts`(11)、`rectification-v9-agent.test.ts`(12)、`rectification-v9-stream.test.ts`(9)、`rectification-v9-status-security.test.ts`(11);`rectification-v9-migration.test.ts` 扩 9 项静态合同;`rectification-v9-database.test.ts` 扩 agent API 迁移/flag/consent 测试(本机无 Docker 按环境 skip);重写 `rectification-agentic-entry.test.ts`(旧测试锁死了 `hasRectificationSession + sessions.find` 与 maxSteps=8、textStream、sessions.find 复用等错误行为,全部改为断言 V9 服务端路由);`consultation-entrypoint.test.ts` / `application-billing-contract.test.ts` 的 rectification 段同步为 caseId 绑定契约。本机运行:v9 聚焦 107 通过 / 0 失败 / 6 Docker skip;完整 frontend 套件 1231 通过 / 18 失败(全部为无 Docker/PostgreSQL 的环境类既有失败,基线 20 失败,本轮未新增环境失败);`tsc --noEmit` 仅剩 6 个既有测试文件错误(均未触碰);ESLint 0 error;`git diff --check` 通过。
+- 防复发(补充):任何新 rectification 前端逻辑不得再根据消息数/候选存在/session 排序推断 Case 状态;`/api/rectification/agent` 只能消费 fullStream 并输出 allowlist NDJSON;工具输入 schema 必须 strict 且只含最小引用;首轮必须保留真实 skill 加载证据;billing request identity 只能绑定 caseId;DB 驱动 feature flag 是 runtime selector 的唯一来源。
+- 修复版本:本地 staging 候选(未 push / deploy)
+
### 旧防线为何没拦住(hasRectificationSession + sessions.find())
- `hasRectificationSession` 只断言“存在任意校正 Session”,不区分 draft / collecting / candidate_ready / candidate_accepted / confirmed / closed,因此已完成会话始终被当作可继续,且没有服务端 Case 状态可被测试断言。
diff --git a/frontend/tests/application-billing-contract.test.ts b/frontend/tests/application-billing-contract.test.ts
index 014dc436..f473ea9d 100644
--- a/frontend/tests/application-billing-contract.test.ts
+++ b/frontend/tests/application-billing-contract.test.ts
@@ -9,11 +9,11 @@ const consultRoute = readFileSync(new URL("src/app/api/consult/route.ts", root),
const packagesRoute = readFileSync(new URL("src/app/api/admin/packages/route.ts", root), "utf8");
test("Agentic rectification reuses one case-level usage authorization and the session-pinned model version", () => {
- assert.match(rectificationRoute, /select\("id,messages,session_type,model_id,model_config_version"\)/);
+ assert.match(rectificationRoute, /select\("id,messages,session_type,model_id,model_config_version,agentic_rectification_case_id"\)/);
assert.match(rectificationRoute, /resolveSessionLanguageModel\(\s*chatSession\.model_id,\s*chatSession\.model_config_version,?\s*\)/);
assert.match(rectificationRoute, /modelConfigVersion: selectedModel\.configVersion/);
assert.doesNotMatch(rectificationRoute, /loadLanguageModelCatalog|resolveLanguageModelFromCatalog|\bresolveLanguageModel\(|\bdefaultLanguageModel\(/);
- assert.match(rectificationRoute, /const billingRequestPrefix = `rectification:\$\{sessionId\}`/);
+ assert.match(rectificationRoute, /const billingRequestPrefix = `rectification:case:\$\{caseId\}`/);
assert.match(rectificationRoute, /from\("usage_reservations"\)[\s\S]*\.eq\("feature_key", "rectification"\)[\s\S]*\.like\("request_id", `\$\{billingRequestPrefix\}%`\)/);
assert.match(rectificationRoute, /return reservations.length === 0[\s\S]*`\$\{billingRequestPrefix\}:retry:\$\{reservations.length\}`/);
assert.match(rectificationRoute, /authorizeUsage\(accounting, \{[\s\S]*requestId: billingRequestId/);
diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts
index 4cede283..4b848790 100644
--- a/frontend/tests/consultation-entrypoint.test.ts
+++ b/frontend/tests/consultation-entrypoint.test.ts
@@ -78,13 +78,14 @@ test("ordinary product drafts keep the public question and clear hidden routing
assert.match(source, /setDraft\(pending\.question\);[\s\S]*?setDraftTheme\(pending\.theme\);[\s\S]*?setDraftEntrypoint\(pending\.entrypoint\);/);
});
-test("homepage birth-time card opens the latest Agentic surface instead of ordinary consultation", () => {
+test("homepage birth-time card opens the V9 Agentic surface via the server case API", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8");
- assert.match(source, /function openBirthTimeRectification/);
+ assert.match(source, /function openRectificationCase/);
+ assert.match(source, /openRectificationFromHomepage/);
assert.match(source, //);
+ assert.match(component, //);
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
assert.doesNotMatch(source, /chooseSuggestedQuestion\([\s\S]{0,180}"birth_time_rectification"/);
assert.doesNotMatch(source, /draftBirthTimeRectificationQuestion/);
@@ -96,40 +97,38 @@ test("homepage mounts the Agentic surface without invoking retired rectification
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
assert.match(page, / {
+test("homepage opens through the server Case API and merges the returned session", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
- const start = source.indexOf("async function openBirthTimeRectification");
- const end = source.indexOf("function handleRectificationProfileIncomplete", start);
+ const start = source.indexOf("async function openRectificationCase");
+ const end = source.indexOf("async function openRectificationFromHomepage", start);
const handler = source.slice(start, end);
- const create = handler.indexOf('createSession(modelCatalog.defaultModelId, "birth_time_rectification")');
- const persist = handler.indexOf("await rectificationPersistence.current.enqueue");
- const addToSessionList = handler.indexOf("setSessions((current) => [", persist);
- const reveal = handler.indexOf("setActiveSessionId(rectificationSession.id)");
+ const open = handler.indexOf('/api/rectification/cases/open');
+ const merge = handler.indexOf('setSessions((current) => [merged,');
+ const reveal = handler.indexOf('setActiveSessionId(opened.sessionId)');
- assert.ok(create >= 0);
- assert.ok(persist > create);
- assert.ok(addToSessionList > persist);
- assert.ok(reveal > addToSessionList);
- assert.match(handler, /rectificationOpenInFlight\.current/);
+ assert.ok(open >= 0);
+ assert.ok(merge > open);
+ assert.ok(reveal > merge);
assert.match(handler, /rectificationOpenInFlight\.current = true;[\s\S]*?finally \{[\s\S]*?rectificationOpenInFlight\.current = false;/);
assert.doesNotMatch(handler, /onNarrativeDelta/);
assert.match(source, /const rectificationSurfaceOpen = activeRectificationSession\s*&& activeSession\.id === rectificationSessionId/);
- assert.match(source, /rectificationSurfaceOpen && \([\s\S]*? {
+test("the page never creates the session shell locally; the server owns session creation", () => {
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8");
- const start = page.indexOf("async function openBirthTimeRectification");
- const end = page.indexOf("function handleRectificationProfileIncomplete", start);
+ const start = page.indexOf("async function openRectificationCase");
+ const end = page.indexOf("async function openRectificationFromHomepage", start);
const handler = page.slice(start, end);
- assert.match(handler, /persistSession\(rectificationSession, "create"\)/);
- assert.match(component, / {
@@ -141,7 +140,7 @@ test("rectification cards render only inside the active rectification session",
assert.doesNotMatch(source, /这个会话保存了生时校正入口|恢复生时校正<\/button>/);
});
-test("selecting a rectification session resumes it without an intermediate confirmation", () => {
+test("selecting a rectification session resumes it through the exact-session open API", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const selectSession = source.slice(
source.indexOf("function selectSession("),
@@ -149,36 +148,24 @@ test("selecting a rectification session resumes it without an intermediate confi
);
assert.match(selectSession, /nextSession\?\.sessionType === "birth_time_rectification"/);
- assert.match(selectSession, /resumeRectificationSession\.current\(nextSession\)/);
+ assert.match(selectSession, /void openRectificationSession\(nextSession\.id\)/);
assert.match(source, /resumeRectificationSession\.current\(activeSession\)/);
assert.doesNotMatch(source, /RectificationLoadingState|重试恢复/);
assert.match(source, / {
+test("homepage restart and sidebar selection both resolve through the server open API", () => {
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8");
- const start = page.indexOf("async function openBirthTimeRectification");
- const end = page.indexOf("function handleRectificationProfileIncomplete", start);
- const handler = page.slice(start, end);
- assert.match(handler, /sessions\.find\(\(session\) => session\.sessionType === "birth_time_rectification"\)/);
- assert.match(handler, /existing \?\? createSession/);
- assert.match(component, / session\.sessionType === "birth_time_rectification"\)/);
+ assert.match(component, / {
- const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
- const component = readFileSync(new URL("../src/components/conversational-birth-time-rectification.tsx", import.meta.url), "utf8");
- const start = page.indexOf("async function openBirthTimeRectification");
- const end = page.indexOf("function handleRectificationProfileIncomplete", start);
- const handler = page.slice(start, end);
-
- assert.match(handler, /sourceSession\.sessionType === "birth_time_rectification"[\s\S]*?sourceSession[\s\S]*?sessions\.find/);
- assert.match(component, / {
+test("rectify-first suggestions hand the source question to the homepage open flow", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const start = source.indexOf("function chooseConversationSuggestion");
const end = source.indexOf("function draftDailyStarlanguageQuestion", start);
@@ -186,7 +173,7 @@ test("rectify-first suggestions hand the source question to a dedicated rectific
assert.match(handler, /suggestion !== rectifyBeforeConsultationSuggestion/);
assert.match(handler, /find\(\(message\) => message\.role === "user"\)/);
- assert.match(handler, /openBirthTimeRectification\(originalQuestion, activeSession\)/);
+ assert.match(handler, /openRectificationFromHomepage\(originalQuestion \?\? null\)/);
assert.match(source, /onClick=\{\(\) => chooseConversationSuggestion\(question\)\}/);
});
@@ -196,7 +183,7 @@ test("rectify-first handoffs stay as Agent context", () => {
assert.match(source, /pendingConsultationQuestion=\{rectificationPendingQuestion\}/);
assert.match(chat, /pendingConsultationQuestion\?\.trim\(\)/);
- assert.match(chat, /之后再回到你原来的问题/);
+ assert.match(chat, /之后会回到你原来的问题/);
});
test("ordinary consultation uses current birth data without a rectification notice", () => {
diff --git a/frontend/tests/database-local-business.test.ts b/frontend/tests/database-local-business.test.ts
index 834caf79..6cb6bb12 100644
--- a/frontend/tests/database-local-business.test.ts
+++ b/frontend/tests/database-local-business.test.ts
@@ -49,6 +49,7 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
assert.match(migration.stdout, /applied 20260806050000_operations_feature_flags\.sql/);
assert.match(migration.stdout, /applied 20260811010000_consultation_status_service_role_read\.sql/);
assert.match(migration.stdout, /applied 20260812010000_agentic_rectification_v9_runtime\.sql/);
+ assert.match(migration.stdout, /applied 20260813010000_agentic_rectification_v9_agent_api\.sql/);
assert.equal(
fixture.psql(`
@@ -113,6 +114,7 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
"agentic_rectification_evidence",
"agentic_rectification_open_ledger",
"agentic_rectification_results",
+ "agentic_rectification_run_phases",
"agentic_rectification_tool_receipts",
"agentic_rectification_turns",
"billing_products",
diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts
index f621739c..75841466 100644
--- a/frontend/tests/rectification-agentic-entry.test.ts
+++ b/frontend/tests/rectification-agentic-entry.test.ts
@@ -21,27 +21,35 @@ const agent = readFileSync(
);
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
-test("birth-time rectification entry mounts the Agentic chat", () => {
- assert.match(component, /return /);
+test("birth-time rectification entry mounts the V9 case-ref chat", () => {
+ assert.match(component, /return /);
+ assert.match(component, /caseId: string;/);
+ assert.match(component, /readonly: boolean;/);
+ assert.match(component, /shouldStartOpening: boolean;/);
+ assert.match(component, /initialTurns:/);
});
-test("opening is a server-owned operation rather than a hidden user prompt", () => {
+test("opening is server-owned: shouldStartOpening drives the first turn, never client history", () => {
+ assert.doesNotMatch(chat, /initialMessages\.length > 0 \|\| openingStarted/);
assert.doesNotMatch(chat, /agenticOpeningInstruction|用户刚进入生时校正会话/);
- assert.match(chat, /action: "opening"/);
- assert.match(route, /z\.literal\("opening"\)/);
- assert.match(route, /conversation\.action === "opening"/);
+ assert.match(chat, /shouldStartOpening/);
+ assert.match(chat, /if \(readonly \|\| openingStarted\.current \|\| !shouldStartOpening\) return/);
+ assert.match(chat, /void send\("opening", ""\)/);
+ assert.match(route, /action: z\.enum\(\["opening", "message", "read_only"\]\)/);
+ assert.match(page, /shouldStartOpening=\{rectificationShouldStartOpening\}/);
+ assert.match(page, /setRectificationShouldStartOpening\(opened\.shouldStartOpening\)/);
});
-test("incomplete profiles stay in the shared onboarding flow", () => {
+test("incomplete profiles stay in the shared onboarding flow before any open request", () => {
const opening = page.slice(
- page.indexOf("async function openBirthTimeRectification"),
- page.indexOf("resumeRectificationSession.current ="),
+ page.indexOf("async function openRectificationCase"),
+ page.indexOf("async function openRectificationFromHomepage"),
);
assert.match(opening, /const missingStep = missingProfileStep\(profile\)/);
assert.match(opening, /setOnboardingStep\(missingStep\)/);
assert.ok(
opening.indexOf("const missingStep = missingProfileStep(profile)")
- < opening.indexOf("setRectificationSessionId(rectificationSession.id)"),
+ < opening.indexOf("/api/rectification/cases/open"),
);
assert.match(chat, /payload\?\.code === "profile_incomplete"/);
});
@@ -71,7 +79,6 @@ test("account rehydration normalizes persisted ISO birth dates before completene
assert.match(profileReader, /const date = normalizePersistedBirthDate\(/);
});
-
test("candidate acceptance refreshes the profile result without overwriting an open draft", () => {
const refresh = page.slice(
page.indexOf("async function refreshAccount"),
@@ -82,75 +89,72 @@ test("candidate acceptance refreshes the profile result without overwriting an o
assert.doesNotMatch(refresh, /setProfileDraft/);
});
-test("agent tool calls leave a final step for visible prose and never end silently", () => {
- assert.match(route, /const agenticRectificationMaxSteps = 8/);
- assert.match(route, /\{ maxSteps: agenticRectificationMaxSteps \}/);
- assert.match(route, /if \(!emitted \|\| !reply\.text\) \{[\s\S]*type: "error"[\s\S]*await settle\(false\)[\s\S]*return;/);
- assert.doesNotMatch(route, /send\(\{ type: "done", emitted \}\)/);
+test("agent tool calls never end silently; the runner owns completion and failure", () => {
+ assert.doesNotMatch(route, /agenticRectificationMaxSteps = 8/);
+ assert.match(agent, /RECTIFICATION_AGENT_STEP_BUDGETS/);
+ assert.match(agent, /RECTIFICATION_AGENT_HARD_STEP_LIMIT/);
+ assert.match(route, /send\(\{ type: "done", emitted: true \}\)/);
+ assert.doesNotMatch(route, /send\(\{ type: "done", emitted: false \}\)/);
});
-test("rectification messages survive remounts and suppress duplicate openings", () => {
- assert.match(chat, /initialMessages: readonly ChatMessage\[\]/);
- assert.match(chat, /if \(initialMessages\.length > 0 \|\| openingStarted\.current\) return/);
- assert.match(chat, /sessionId,/);
- assert.match(chat, /onMessagesChange\?\.\(/);
- assert.match(page, /key=\{rectificationSessionId\}/);
- assert.match(page, /initialMessages=\{activeSession\?\.messages \?\? \[\]\}/);
+test("persisted turns survive remounts; duplicate openings are suppressed by the server", () => {
+ assert.match(chat, /initialTurns/);
+ assert.match(chat, /const openingStarted = useRef\(false\)/);
+ assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
+ assert.match(page, /initialTurns=\{rectificationTurns\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
});
-test("successful Agent turns are persisted by the authenticated rectification route", () => {
+test("the agent route verifies the exact Case/Session binding before any turn", () => {
+ assert.match(route, /caseId: z\.string\(\)\.uuid\(\)/);
assert.match(route, /sessionId: z\.string\(\)\.uuid\(\)/);
- assert.match(route, /\.from\("chat_sessions"\)[\s\S]*\.eq\("user_id", userId\)/);
- assert.match(route, /conversation\.action === "opening" && persistedMessages\.length > 0/);
- assert.match(route, /\.update\(\{ messages: nextMessages, updated_at:/);
- assert.match(route, /if \(saveError \|\| !savedSession\) throw new Error\("RectificationSessionPersistenceError"\)/);
+ assert.match(route, /requestId: z\.string\(\)\.uuid\(\)/);
+ assert.match(route, /boundSessionId !== sessionId/);
+ assert.match(route, /chatSession\.agentic_rectification_case_id !== caseId/);
+ assert.doesNotMatch(route, /conversation\.action === "opening" && persistedMessages\.length > 0/);
+ assert.doesNotMatch(route, /\.update\(\{ messages: nextMessages, updated_at:/);
});
+test("candidate results restore through the durable Candidate Snapshot API, never agent text", () => {
+ assert.match(chat, /\/api\/rectification\/cases\/\$\{encodeURIComponent\(caseId\)\}\?sessionId=/);
+ assert.doesNotMatch(chat, /savedSentinel|AYANAM_RECTIFICATION_SAVED|hidden block/);
+ assert.doesNotMatch(chat, /