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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Channel Conversation Continuity v2 Implementation Plan

Status: G0 complete; S-1 and S0 merged; S1+S2, S3, S4, and S5 implemented and locally verified; S6-S7 pending
Status: G0 complete; S-1 and S0 merged; S1+S2 through S6 implemented and locally verified; S7 pending
Owner: conversation, channel context, Ling Agent Session, and storage maintainers
Date: 2026-08-08
Last verified: 2026-08-10
Expand Down Expand Up @@ -527,12 +527,21 @@ snapshot reconstruction, and the one strict context-length fallback.

## Slice S6: Multi-Agent Context Version Consistency

Status: implemented on `codex/continuity-v2-agent-consistency` and locally verified.
Contracts (`35/35`), Core (`360/360`), Daemon (`902/902`), and Web (`42/42`)
tests pass; Web typecheck and `git diff --check` pass. Agent requests, output packets,
fan-in, and handoffs carry durable context stamps; stale relevant facts and direct private
evidence copies fail closed, while safe metadata-only rebases and accepted-handoff retries
remain deterministic.

### Changes

- Include projection version, segment id, item watermark, and source refs in every agent
request and handoff packet.
- Provide every agent the same shared core, then add role-specific evidence.
- Enforce teammate-private memory and private-chat isolation.
- Keep owner-scoped execution prompts separate from durable Task, Run, and WorkUnit audit
snapshots; persist memory-use ids and policy evidence without copying private memory text.
- On result fan-in, compare consumed projection with current projection:
- accept if changed refs are unrelated;
- rebase metadata-only results when safe;
Expand Down Expand Up @@ -811,7 +820,7 @@ interfaces at the end of S3 and keep implementation files separate before parall
- Verify: daemon regression suite, explicit file safety, clarification actionability, and eval cases.
- [x] **T6 (P2, human: ~2 days / Codex: ~3-5 hours)**: add proactive summaries, coverage watermarks, stale-result rejection, and fork snapshots.
- Verify: budget, race, invalidation, reconstruction, and fallback tests.
- [ ] **T7 (P2, human: ~2 days / Codex: ~3-5 hours)**: enforce multi-agent projection versions and private-memory isolation.
- [x] **T7 (P2, human: ~2 days / Codex: ~3-5 hours)**: enforce multi-agent projection versions and private-memory isolation.
- Verify: shared-core, stale fan-in, handoff, and security fixtures.
- [ ] **T8 (P2, human: ~2 days / Codex: ~3-4 hours)**: switch transcript/API/Web diagnostics and remove old runtime authorities.
- Verify: package suites, channel E2E, old-symbol search, and migration smoke.
Expand Down Expand Up @@ -861,7 +870,7 @@ schema.
| Design Review | `/plan-design-review` | UI and UX gaps | 0 | not run | UI changes are limited to later advanced Context details |
| DX Review | `/plan-devex-review` | Developer experience gaps | 0 | not run | Not required for the design baseline |

**VERDICT:** ENG CLEARED. S-1 and S0 are merged; S1+S2, S3, and S4 are implemented
and locally verified; S5-S7 retain the reviewed delivery order and exit criteria.
**VERDICT:** ENG CLEARED. S-1 and S0 are merged; S1+S2 through S6 are implemented
and locally verified; S7 retains the reviewed delivery order and exit criteria.

NO UNRESOLVED DECISIONS
2 changes: 1 addition & 1 deletion docs/design/channel-conversation-continuity-v2.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Channel Conversation Continuity v2

Status: approved design baseline; S-1 and S0 merged; S1+S2, S3, and S4 implemented and locally verified; S5-S7 pending
Status: approved design baseline; S-1 and S0 merged; S1+S2 through S6 implemented and locally verified; S7 pending
Date: 2026-08-08
Last verified: 2026-08-10
Scope: channel conversation persistence, Thread/Turn/Item topology, context segmentation,
Expand Down
2 changes: 1 addition & 1 deletion docs/design/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ counts, screenshots, hashes and run diaries belong in CI or Release artifacts.

| Work | Current boundary | Closure condition |
|---|---|---|
| [Channel Conversation Continuity v2](channel-conversation-continuity-v2-implementation-plan.md) | G0 and S0 merged; S1+S2, S3, and S4 implemented and locally verified | S5-S7, final migration, recovery, performance, security, eval, and Web E2E gates pass |
| [Channel Conversation Continuity v2](channel-conversation-continuity-v2-implementation-plan.md) | G0, S0, and S1+S2 merged; S3-S6 implemented and locally verified | S7, final migration, recovery, performance, security, eval, and Web E2E gates pass |
| [Channel Context Processing](channel-context-processing-layer-implementation-plan.md) | Channel and direct-message context views exist | Condensation, quote eligibility, persistence and redaction ownership are closed |
| [Permission Presets](codex-inspired-settings-permission-presets-implementation-plan.md) | Enforcement modes exist | Preset UI, effective preview and run audit agree with daemon enforcement |
| [Conversation Surface](conversation-surface-contract-implementation-plan.md) | Shared primitives and initial package extraction are implemented | WorkCell, evidence, attachment, package CSS and Desktop-consumption boundaries close |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const SNAPSHOT_KEYS = new Set([

const PROJECTION_KEYS = new Set([
"projectionVersion",
"sourceStateFingerprint",
"threadId",
"turnId",
"segmentId",
Expand Down Expand Up @@ -393,6 +394,7 @@ export function isConversationContextProjectionMetadata(
): value is ConversationContextProjectionMetadata {
return isRecordWithOnlyKeys(value, PROJECTION_KEYS)
&& isText(value.projectionVersion)
&& isText(value.sourceStateFingerprint)
&& isText(value.threadId)
&& isText(value.turnId)
&& isText(value.segmentId)
Expand Down
6 changes: 6 additions & 0 deletions packages/contracts/src/conversation-continuity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,12 @@ export interface ConversationContextSnapshot {

export interface ConversationContextProjectionMetadata {
readonly projectionVersion: string;
/**
* Fingerprint of the effective assignment, summaries, and item revisions that
* contributed facts to this projection. Unlike projectionVersion, this does
* not change for metadata-only segment activity.
*/
readonly sourceStateFingerprint: string;
readonly threadId: string;
readonly turnId: string;
readonly segmentId: string;
Expand Down
29 changes: 29 additions & 0 deletions packages/contracts/src/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ export interface WorkflowPacketSourceRef {
readonly label?: string;
}

export interface WorkflowAgentContextStamp {
readonly schemaVersion: 1;
readonly sourceItemId: string;
readonly consumerTeammateId?: string;
readonly contextProjection: ConversationContextProjectionMetadata;
readonly roleEvidenceRefs: readonly WorkflowPacketSourceRef[];
readonly roleEvidenceFingerprint: string;
}

export type WorkflowFanInContextReviewStatus =
| "accepted_current"
| "rebased_metadata"
| "rejected_stale"
| "rejected_private_leak";

export interface WorkflowFanInContextReview {
readonly dependencyWorkflowStepId: string;
readonly status: WorkflowFanInContextReviewStatus;
readonly consumedProjectionVersion?: string;
readonly currentProjectionVersion?: string;
readonly reason: string;
}

export type WorkflowHandoffIntentStatus =
| "proposed"
| "validated"
Expand Down Expand Up @@ -148,6 +171,7 @@ export interface WorkflowHandoffIntent {
readonly expectedOutput: string;
readonly riskSummary?: string;
readonly sourceRefs: readonly WorkflowPacketSourceRef[];
readonly agentContext?: WorkflowAgentContextStamp;
readonly status: WorkflowHandoffIntentStatus;
readonly validation?: WorkflowHandoffValidation;
readonly createdBy: WorkflowHandoffIntentCreatedBy;
Expand Down Expand Up @@ -218,6 +242,7 @@ export interface ChannelWorkflowStepContextPacket {
readonly dependencies: readonly WorkflowStepOutputRef[];
readonly priorOutputArtifactIds: readonly string[];
readonly sourceRefs?: readonly WorkflowPacketSourceRef[];
readonly agentContext?: WorkflowAgentContextStamp;
readonly selectorDecisionIds?: readonly string[];
readonly handoffIntentIds?: readonly string[];
readonly requestedRunnerId?: string;
Expand Down Expand Up @@ -260,6 +285,7 @@ export interface WorkflowStepContextPackSnapshot {
readonly label?: string;
}[];
readonly contextProjection?: ConversationContextProjectionMetadata;
readonly agentContext?: WorkflowAgentContextStamp;
}

export interface ChannelWorkflowStepOutputPacket {
Expand All @@ -278,6 +304,7 @@ export interface ChannelWorkflowStepOutputPacket {
readonly blockers?: readonly string[];
readonly changedPaths?: readonly string[];
readonly sourceRefs?: readonly WorkflowPacketSourceRef[];
readonly agentContext?: WorkflowAgentContextStamp;
readonly selectorDecisionIds?: readonly string[];
readonly handoffIntentIds?: readonly string[];
readonly runnerAudit?: RunnerResolutionAudit;
Expand Down Expand Up @@ -320,6 +347,8 @@ export interface ChannelWorkflowStepMetadata {
readonly proposalStepSources?: readonly ChannelExecutionProposalStepSource[];
readonly fanInSummaryArtifactId?: string;
readonly memoryCandidateIds?: readonly string[];
readonly fanInContextReviews?: readonly WorkflowFanInContextReview[];
readonly resultContextReview?: WorkflowFanInContextReview;
}

export type WorkflowGraphNodeKind = "step" | "join";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ test("segment, assignment, summary, snapshot and projection contracts preserve s
}), true);
assert.equal(isConversationContextProjectionMetadata({
projectionVersion: "projection-1",
sourceStateFingerprint: "source-state-1",
threadId: "thread-1",
turnId: "turn-2",
segmentId: "segment-1",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/handoff-intents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
WorkflowHandoffIntent,
WorkflowHandoffIntentStatus,
WorkflowHandoffValidation,
WorkflowAgentContextStamp,
WorkflowPacketSourceRef,
} from "./workspace-types.js";

Expand All @@ -29,6 +30,7 @@ export interface LingHandoffInputPacket {
readonly requiredInput: string;
readonly expectedOutput: string;
readonly sourceRefs: readonly WorkflowPacketSourceRef[];
readonly agentContext?: WorkflowAgentContextStamp;
}

export interface LingHandoffIntent {
Expand All @@ -51,6 +53,7 @@ export interface LingHandoffIntent {
readonly riskSummary?: string;
readonly status: LingHandoffIntentStatus;
readonly sourceRefs: readonly WorkflowPacketSourceRef[];
readonly agentContext?: WorkflowAgentContextStamp;
readonly inputPacket: LingHandoffInputPacket;
readonly validation?: WorkflowHandoffValidation;
readonly createdBy: string;
Expand Down Expand Up @@ -134,6 +137,7 @@ export function normalizeWorkflowHandoffIntentToLingIntent(intent: WorkflowHando
riskSummary: intent.riskSummary,
status: intent.status,
sourceRefs,
agentContext: intent.agentContext,
inputPacket: {
schemaVersion: 1,
intentId: `workflow_handoff_intent:${intent.id}`,
Expand All @@ -147,6 +151,7 @@ export function normalizeWorkflowHandoffIntentToLingIntent(intent: WorkflowHando
requiredInput: intent.requiredInput,
expectedOutput: intent.expectedOutput,
sourceRefs,
agentContext: intent.agentContext,
},
validation: intent.validation,
createdBy: intent.createdBy,
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/redaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@ function redactValue(value: unknown, seen: WeakSet<object>, extraSecrets: readon
return "[Circular]";
}
seen.add(value);
return value.map((item) => redactValue(item, seen, extraSecrets));
try {
return value.map((item) => redactValue(item, seen, extraSecrets));
} finally {
seen.delete(value);
}
}

if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);

const result: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value)) {
result[key] = shouldRedactKey(key) ? REDACTED : redactValue(item, seen, extraSecrets);
try {
const result: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value)) {
result[key] = shouldRedactKey(key) ? REDACTED : redactValue(item, seen, extraSecrets);
}
return result;
} finally {
seen.delete(value);
}
return result;
}

return value;
Expand Down
72 changes: 72 additions & 0 deletions packages/core/src/storage/sqlite/channel-workflow-mappers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SQLOutputValue } from "node:sqlite";
import { isConversationContextProjectionMetadata } from "@ling-stack/contracts";
import type { RunTool } from "../../approval-policy.js";
import type { SkillApprovalProfile } from "../../skill-types.js";
import {
Expand All @@ -12,9 +13,11 @@ import {
type ChannelWorkflowStepStatus,
type ResponseLanguage,
type WorkflowFanInPolicy,
type WorkflowFanInContextReview,
type WorkflowHandoffIntent,
type WorkflowHandoffIntentStatus,
type WorkflowHandoffValidation,
type WorkflowAgentContextStamp,
type WorkflowPacketSourceRef,
type WorkflowSelectorAlternative,
type WorkflowSelectorDecision,
Expand Down Expand Up @@ -54,6 +57,7 @@ export function mapWorkflowHandoffIntent(row: Record<string, SQLOutputValue>): W
expectedOutput: readString(row, "expected_output"),
riskSummary: readNullableString(row, "risk_summary"),
sourceRefs: parseWorkflowPacketSourceRefs(readString(row, "source_refs_json")),
agentContext: parseOptionalWorkflowAgentContextStamp(readNullableString(row, "agent_context_json")),
status: readString(row, "status") as WorkflowHandoffIntentStatus,
validation: parseOptionalWorkflowHandoffValidation(readNullableString(row, "validation_json")),
createdBy: readString(row, "created_by") as WorkflowHandoffIntent["createdBy"],
Expand All @@ -62,6 +66,36 @@ export function mapWorkflowHandoffIntent(row: Record<string, SQLOutputValue>): W
});
}

function parseOptionalWorkflowAgentContextStamp(value: string | undefined): WorkflowAgentContextStamp | undefined {
if (!value) return undefined;
const parsed = parseJson(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
const record = parsed as Record<string, unknown>;
if (
record.schemaVersion !== 1
|| typeof record.sourceItemId !== "string"
|| record.sourceItemId.trim().length === 0
|| (record.consumerTeammateId !== undefined
&& (typeof record.consumerTeammateId !== "string" || record.consumerTeammateId.trim().length === 0))
|| !isConversationContextProjectionMetadata(record.contextProjection)
|| !Array.isArray(record.roleEvidenceRefs)
|| typeof record.roleEvidenceFingerprint !== "string"
|| record.roleEvidenceFingerprint.trim().length === 0
) {
return undefined;
}
const roleEvidenceRefs = parseWorkflowPacketSourceRefs(JSON.stringify(record.roleEvidenceRefs));
if (roleEvidenceRefs.some((ref) => ref.id.trim().length === 0)) return undefined;
return {
schemaVersion: 1,
sourceItemId: record.sourceItemId,
consumerTeammateId: record.consumerTeammateId as string | undefined,
contextProjection: record.contextProjection,
roleEvidenceRefs,
roleEvidenceFingerprint: record.roleEvidenceFingerprint,
};
}

export function mapWorkflowSelectorDecision(row: Record<string, SQLOutputValue>): WorkflowSelectorDecision {
return omitUndefined({
schemaVersion: 1 as const,
Expand Down Expand Up @@ -179,6 +213,44 @@ function parseOptionalChannelWorkflowStepMetadata(value: string | undefined): Ch
proposalStepSources: parseOptionalProposalStepSourcesValue(record.proposalStepSources),
fanInSummaryArtifactId: readOptionalStringValue(record.fanInSummaryArtifactId),
memoryCandidateIds: readOptionalStringListValue(record.memoryCandidateIds),
fanInContextReviews: parseOptionalWorkflowFanInContextReviews(record.fanInContextReviews),
resultContextReview: parseWorkflowFanInContextReview(record.resultContextReview),
});
}

function parseOptionalWorkflowFanInContextReviews(value: unknown): readonly WorkflowFanInContextReview[] | undefined {
if (!Array.isArray(value)) return undefined;
const reviews: WorkflowFanInContextReview[] = [];
for (const item of value) {
const review = parseWorkflowFanInContextReview(item);
if (!review) return undefined;
reviews.push(review);
}
return reviews;
}

function parseWorkflowFanInContextReview(value: unknown): WorkflowFanInContextReview | undefined {
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
const record = value as Record<string, unknown>;
const status = record.status;
if (
typeof record.dependencyWorkflowStepId !== "string"
|| (status !== "accepted_current"
&& status !== "rebased_metadata"
&& status !== "rejected_stale"
&& status !== "rejected_private_leak")
|| typeof record.reason !== "string"
|| (record.consumedProjectionVersion !== undefined && typeof record.consumedProjectionVersion !== "string")
|| (record.currentProjectionVersion !== undefined && typeof record.currentProjectionVersion !== "string")
) {
return undefined;
}
return omitUndefined({
dependencyWorkflowStepId: record.dependencyWorkflowStepId,
status: status as WorkflowFanInContextReview["status"],
consumedProjectionVersion: readOptionalStringValue(record.consumedProjectionVersion),
currentProjectionVersion: readOptionalStringValue(record.currentProjectionVersion),
reason: record.reason,
});
}

Expand Down
Loading
Loading