Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions AGENT_COORDINATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ Server Error) for an unknown slug.

## Active claims (who is editing what RIGHT NOW)

- **2026-07-16 · Codex `/root` →** `convex/schema.ts#redesignChatRuns`,
`convex/domains/redesign/chatRuns.ts#continuation-context`,
`src/features/redesign/{pages/ReproducibleChatPage,surfaces/ChatSurface,RedesignShell}` ·
add durable, privacy-redacted receipt-to-live-chat continuation ·
branch `fix/receipt-continue-chat`, PR #557. CI deploy only; no out-of-band Convex deploy.

- **2026-06-03 · Claude →** `convex/*#handoff-token`, `src/.../ScratchnodePrivateBridge`,
`src/App.tsx#events-private-route`, `public/proto/home-v5.html#private-handoff` ·
shipping the cross-domain private-notes token bridge (opaque stateful token, PR #496) ·
Expand All @@ -68,6 +74,13 @@ Server Error) for an unknown slug.

## Hand-offs (built + ready for the other agent to call)

- **2026-07-16 - Codex `/root` ->** Receipt continuation contract is additive and ready
on `fix/receipt-continue-chat` (PR #557): `startChat` accepts optional
`conversationContext: Array<{role: "user" | "assistant"; text: string; sourceUrls?: string[]}>`
plus `parentRunHash?: string`. Context is bounded and sanitized before persistence and
grounding. Public `getByHash` receipts deliberately redact both fields so a shared hash
never exposes a private follow-up transcript.

- **2026-07-16 - Codex `/root` -> frontend/runtime agents** - additive optional
`redesignChatRuns.clientRequestId/provider/runtimeReceiptId/cancelRequestedAt/cancelledAt` fields;
`startChat({ ..., clientRequestId? })` dedupes by owner + request key;
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG/integrations/pipeline-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
Append-only lane for pipeline launch, activity, streaming, evaluation, schedule,
and secret-gated MCP bridge ownership contracts. Newest entries first.

## 2026-07-16 - Ground chat follow-ups in bounded receipt context

The pending candidate extends redesign chat runs with optional, sanitized conversation
turns and a parent receipt hash. The live action includes this transcript as context while
requiring factual claims to be re-grounded, and public hash receipts redact the private
continuation fields. See [`../pages/redesign-chat.md`](../pages/redesign-chat.md).

## 2026-07-15 — Derive pipeline ownership on the server

The pending candidate removes browser-selected `ownerKey` authority from public pipeline launch, history, detail, bundle, stream, scorecard, and schedule APIs. Cost-bearing single and composed launches and all schedule controls now require an authenticated server identity. Guest history, detail, bundle, stream, and evaluation reads require an anonymous-session possession credential; schedule changes verify row ownership, public responses omit owner keys, and cron or secret-gated MCP work uses explicit internal service contracts. Legacy anonymous schedules do not execute.
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG/pages/redesign-chat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Redesign Chat

Append-only lane for the public redesign chat, reproducible answer receipts, and their
transition into an authenticated live conversation. Newest entries first.

## 2026-07-16 - Continue an immutable answer in live chat

Reproducible answer receipts now separate two intents that were previously collapsed:
`Re-run prompt` starts a fresh reproducible run, while `Continue in chat` opens the real
chat surface with the receipt's prompt, answer, and source lineage already visible. The
composer remains available beneath that context, so the receipt is no longer a dead end.

Follow-up runs persist a bounded, role-aware conversation context and parent receipt hash.
The backend sanitizes this context before grounding the next answer and strips both fields
from public hash lookups, preventing a shared receipt URL from leaking private follow-up
conversation history. Reload recovery reconstructs the same transcript from stored run
context instead of inventing UI-only state.

The release also excludes the route-lazy markdown editor chunk from Workbox precaching;
it remains network loaded and runtime cached, avoiding an unrelated 2 MiB precache ceiling
from blocking production builds as dependency versions move.

**PR / canonical main commit**: pending CI-gated candidate.

**Evidence state**:
- Root TypeScript and 14 focused continuation/chat contract tests passed locally.
- Production build reached the full client bundle; final release proof is recorded on the PR.
- Responsive receipt-to-chat browser evidence remains outside git under `.qa/evidence/`.

**Author**: Homen Shum + Codex.
**Touches**: [`../integrations/pipeline-runtime.md`](../integrations/pipeline-runtime.md).
68 changes: 66 additions & 2 deletions convex/domains/redesign/chatRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ type TraceRow = {
durationMs: number;
};

export type ConversationContextTurn = {
role: "user" | "assistant";
text: string;
sourceUrls?: string[];
};

interface AnswerPacket {
shortAnswer: string;
whyItMatters: string;
Expand Down Expand Up @@ -143,6 +149,9 @@ type RuntimeContextPacket = {
// ───────── Constants ─────────

const MAX_PROMPT_CHARS = 4_000;
const MAX_CONVERSATION_TURNS = 10;
const MAX_CONVERSATION_TURN_CHARS = 2_000;
const MAX_CONVERSATION_SOURCE_URLS = 5;
const TIMEOUT_MS = 45_000;
const FALLBACK_SOURCE_TIMEOUT_MS = 12_000;
const FALLBACK_SOURCE_LIMIT = 5;
Expand Down Expand Up @@ -176,11 +185,34 @@ async function assertRunReadable(ctx: any, runId: string): Promise<any> {
return row;
}

function redactSharedRun(row: any): any {
export function sanitizeConversationContext(
rows: ConversationContextTurn[] | undefined,
): ConversationContextTurn[] | undefined {
if (!rows?.length) return undefined;
const normalized = rows
.slice(-MAX_CONVERSATION_TURNS)
.map((row) => ({
role: row.role,
text: row.text.trim().slice(0, MAX_CONVERSATION_TURN_CHARS),
...(row.sourceUrls?.length
? {
sourceUrls: row.sourceUrls
.filter((url) => /^https?:\/\//i.test(url))
.slice(0, MAX_CONVERSATION_SOURCE_URLS),
}
: {}),
}))
.filter((row) => row.text.length > 0);
return normalized.length ? normalized : undefined;
}

export function redactSharedRun(row: any): any {
if (!row) return null;
const {
userId: _userId,
clientRequestId: _clientRequestId,
conversationContext: _conversationContext,
parentRunHash: _parentRunHash,
cancelRequestedAt: _cancelRequestedAt,
...safeRow
} = row;
Expand Down Expand Up @@ -1030,6 +1062,13 @@ export const startChat = mutation({
text: v.string(),
source: v.optional(v.string()),
}))),
/** Bounded prior transcript. Persisted privately for reload recovery. */
conversationContext: v.optional(v.array(v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
text: v.string(),
sourceUrls: v.optional(v.array(v.string())),
}))),
parentRunHash: v.optional(v.string()),
/** Phase 5 — counterfactual probe. When set, the run is a probe
* re-evaluation of an earlier run with the cited source masked. */
probeOriginRunId: v.optional(v.string()),
Expand All @@ -1053,6 +1092,8 @@ export const startChat = mutation({
if (existing) return existing.runId;
}
const normalizedTier = normalizeChatTier(args.tier);
const conversationContext = sanitizeConversationContext(args.conversationContext);
const parentRunHash = args.parentRunHash?.trim().slice(0, 80);
const model = modelForTier(normalizedTier);
const runId = generateRunId();
await ctx.db.insert("redesignChatRuns", {
Expand All @@ -1064,6 +1105,8 @@ export const startChat = mutation({
model,
provider: "google-gemini",
runtimeReceiptId: `redesign-chat:${runId}`,
...(conversationContext ? { conversationContext } : {}),
...(parentRunHash ? { parentRunHash } : {}),
status: "pending",
createdAt: Date.now(),
});
Expand All @@ -1074,6 +1117,8 @@ export const startChat = mutation({
contextRef: args.contextRef,
model,
pinnedClaims: args.pinnedClaims,
conversationContext,
parentRunHash,
probeOriginRunId: args.probeOriginRunId,
probeMaskedSourceUrl: args.probeMaskedSourceUrl,
probeMaskedSourceIdx: args.probeMaskedSourceIdx,
Expand Down Expand Up @@ -1337,6 +1382,12 @@ export const runStreamingChat = internalAction({
text: v.string(),
source: v.optional(v.string()),
}))),
conversationContext: v.optional(v.array(v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
text: v.string(),
sourceUrls: v.optional(v.array(v.string())),
}))),
parentRunHash: v.optional(v.string()),
/** Phase 5 — counterfactual probe origin */
probeOriginRunId: v.optional(v.string()),
probeMaskedSourceUrl: v.optional(v.string()),
Expand Down Expand Up @@ -1469,6 +1520,12 @@ export const runStreamingChat = internalAction({
const pinnedSection = args.pinnedClaims && args.pinnedClaims.length > 0
? `\n\nPinned claims (carry forward as established context — do not contradict without explicit re-grounding):\n${args.pinnedClaims.map((p, i) => ` ${i + 1}. ${p.text}${p.source ? ` (source: ${p.source})` : ""}`).join("\n")}`
: "";
const conversationSection = args.conversationContext && args.conversationContext.length > 0
? `\n\nPrior conversation (context only; preserve the user's intent, but re-ground factual claims when needed):\n${args.conversationContext.map((turn, i) => {
const sources = turn.sourceUrls?.length ? `\n sources: ${turn.sourceUrls.join(", ")}` : "";
return ` ${i + 1}. ${turn.role.toUpperCase()}: ${turn.text}${sources}`;
}).join("\n")}`
: "";
// Phase 5 — counterfactual probe instruction
const probeSection = args.probeMaskedSourceUrl
? `\n\nIMPORTANT — counterfactual probe: The source previously at <${args.probeMaskedSourceUrl}> (originally cited as [${args.probeMaskedSourceIdx ?? "?"}] in run ${args.probeOriginRunId ?? "?"}) is being treated as UNRELIABLE for this answer. DO NOT cite it. DO NOT use it as the basis for any claim. Re-answer the same prompt and explicitly note in "Risks / unknowns" how the conclusion changes (or holds) if that source is excluded. Prefer alternative grounded sources.`
Expand Down Expand Up @@ -1509,14 +1566,21 @@ ${citationInstruction} ${liveGrounding.useLiveGrounding ? "Keep claims grounded

${calculationInstruction}

Context: ${JSON.stringify(contextBundle)}${pinnedSection}${probeSection}${verifiedCalculationSection}`;
Context: ${JSON.stringify(contextBundle)}${conversationSection}${pinnedSection}${probeSection}${verifiedCalculationSection}`;
// Emit a stage event so the UI can show "Probing without [N]" / "Carrying forward N pins"
if (probeSection) {
await append("stage", { stage: "probe", maskedUrl: args.probeMaskedSourceUrl, maskedIdx: args.probeMaskedSourceIdx, originRunId: args.probeOriginRunId });
}
if (pinnedSection) {
await append("stage", { stage: "pinned", count: args.pinnedClaims!.length });
}
if (conversationSection) {
await append("stage", {
stage: "conversation_context",
count: args.conversationContext!.length,
parentRunHash: args.parentRunHash,
});
}

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
Expand Down
8 changes: 8 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16026,6 +16026,14 @@ export default defineSchema({
/** Honest runtime receipt metadata; no provider response id is inferred. */
provider: v.optional(v.string()),
runtimeReceiptId: v.optional(v.string()),
/** Private, bounded transcript supplied to a continuation run. Never returned by getByHash. */
conversationContext: v.optional(v.array(v.object({
role: v.union(v.literal("user"), v.literal("assistant")),
text: v.string(),
sourceUrls: v.optional(v.array(v.string())),
}))),
/** Reproducible receipt this run continued from. Optional for existing rows. */
parentRunHash: v.optional(v.string()),
cancelRequestedAt: v.optional(v.number()),
cancelledAt: v.optional(v.number()),
createdAt: v.number(),
Expand Down
11 changes: 4 additions & 7 deletions src/features/redesign/RedesignShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { WorkspaceSurface } from "./surfaces/WorkspaceSurface";
import { ReproducibleChatPage } from "./pages/ReproducibleChatPage";
import { useLiveArtifacts, type LiveArtifactDetail } from "./hooks/useLiveArtifacts";
import type { ReportCardData, SurfaceId } from "./fixtures";
import { parseChatLaunchParams } from "./lib/chatContinuation";

const PATH_TO_SURFACE: Record<string, SurfaceId | "workspace"> = {
"": "home",
Expand Down Expand Up @@ -75,11 +76,6 @@ function pathToChatHash(pathname: string): string | null {
return match?.[1] ?? null;
}

function queryPrompt(search: string): string {
const params = new URLSearchParams(search);
return params.get("q")?.trim() ?? "";
}

function normalizeEntityKey(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
Expand Down Expand Up @@ -117,7 +113,7 @@ export default function RedesignShell() {
const reportId = useMemo(() => pathToReportId(location.pathname), [location.pathname]);
const chatHash = useMemo(() => pathToChatHash(location.pathname), [location.pathname]);
const workspace = useMemo(() => workspaceParams(location.search), [location.search]);
const initialChatPrompt = useMemo(() => queryPrompt(location.search), [location.search]);
const chatLaunch = useMemo(() => parseChatLaunchParams(location.search), [location.search]);
const focusHomeComposer = useMemo(
() => new URLSearchParams(location.search).get("focus") === "home-composer",
[location.search],
Expand Down Expand Up @@ -360,7 +356,8 @@ export default function RedesignShell() {
)}
{!isPrototypeKit && surface === "chat" && (
<ChatSurface
initialPrompt={initialChatPrompt}
initialPrompt={chatLaunch.prompt}
continuationHash={chatLaunch.continuationHash}
onActiveContextChange={setActiveChatDetail}
onAgentRailChange={setActiveChatAgentRail}
/>
Expand Down
53 changes: 53 additions & 0 deletions src/features/redesign/agent-workspace.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,61 @@
[data-redesign] .rd-chat-workspace {
display: grid;
grid-template-rows: minmax(0, 1fr) auto auto auto;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
overflow: hidden;
background: var(--rd-paper);
}

[data-redesign] .rd-chat-workspace > * {
min-width: 0;
}

[data-redesign] .rd-chat-workspace[data-continuation="true"] {
grid-template-rows: auto minmax(0, 1fr) auto auto auto;
}

[data-redesign] .rd-continuation-context {
width: min(100% - 32px, 840px);
box-sizing: border-box;
margin: 12px auto 0;
padding: 10px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
border: 1px solid var(--rd-accent-ring);
border-radius: var(--rd-r-md);
background: var(--rd-accent-tint);
color: var(--rd-ink);
}

[data-redesign] .rd-continuation-context p {
margin: 3px 0 0;
color: var(--rd-ink-mute);
font-size: 12px;
line-height: 1.4;
}

[data-redesign] .rd-continuation-context[data-state="unavailable"] {
border-color: var(--rd-amber-ring, var(--rd-line));
background: var(--rd-amber-tint, var(--rd-panel));
}

[data-redesign] .rd-chat-transcript {
width: 100%;
min-width: 0;
min-height: 0;
height: 100%;
overflow: hidden;
}

[data-redesign] .rd-chat-transcript__content {
width: min(100%, 840px);
min-width: 0;
box-sizing: border-box;
min-height: 100%;
margin: 0 auto;
padding: clamp(20px, 4vw, 44px) clamp(18px, 4vw, 40px) 48px;
Expand All @@ -71,6 +112,9 @@

[data-redesign] .rd-composer-dock {
position: relative;
width: 100%;
min-width: 0;
box-sizing: border-box;
bottom: auto;
z-index: 5;
padding: 10px clamp(14px, 4vw, 40px) max(14px, env(safe-area-inset-bottom));
Expand All @@ -81,6 +125,7 @@

[data-redesign] .rd-composer-shell {
width: min(100%, 840px);
min-width: 0;
margin: 0 auto;
}

Expand Down Expand Up @@ -183,6 +228,14 @@
}

@media (max-width: 760px) {
[data-redesign] .rd-continuation-context {
width: calc(100% - 24px);
margin-top: 8px;
align-items: flex-start;
flex-direction: column;
gap: 8px;
}

[data-redesign] .rd-run-scope > summary span:nth-of-type(2),
[data-redesign] .rd-run-scope > summary span:nth-of-type(3) {
display: none;
Expand Down
Loading
Loading