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, and S4 implemented and locally verified; S5-S7 pending
Status: G0 complete; S-1 and S0 merged; S1+S2, S3, S4, and S5 implemented and locally verified; S6-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 @@ -400,7 +400,7 @@ input accepted

## Slice S3: Context Segmentation And Shared Projection

Status: implemented on `codex/continuity-v2-context-projector` and locally verified.
Status: implemented on `codex/continuity-v2-context-projection` and locally verified.
Contracts (`35/35`), Core (`355/355`), Daemon (`888/888`), Remote Protocol
(`40/40`), and Web (`42/42`) tests pass; Web typecheck and `git diff --check` pass.
Model-facing Channel consumers use the shared projector, including causally later queued
Expand Down Expand Up @@ -447,7 +447,7 @@ context. Paging for UI remains allowed; context construction goes through the pr

## Slice S4: Intent v2 And Issue #421 Closure

Status: implemented on `codex/continuity-v2-context-projector`. The clean-break SQLite
Status: implemented on `codex/continuity-v2-intent-v2`. The clean-break SQLite
migration, contracts, API/client filters, selector, structured clarification, typed
execution, Web diagnostics, and Issue #421 regression fixtures are locally verified.
The focused channel workspace suite passes 186/186, including article continuation,
Expand Down Expand Up @@ -500,6 +500,11 @@ bad clarification -> unrelated snake game

## Slice S5: Proactive Summaries And Fork Snapshots

Status: implemented on `codex/continuity-v2-summaries-snapshots` and locally verified.
Core (`357/357`) and Daemon (`894/894`) tests pass, including provider-aware hard
summarization, stable raw-tail reconstruction, stale-result fencing, immutable fork
snapshot reconstruction, and the one strict context-length fallback.

### Changes

- Add a summary coordinator that watches committed stable ranges per segment.
Expand Down Expand Up @@ -804,7 +809,7 @@ interfaces at the end of S3 and keep implementation files separate before parall
- Verify: projection consistency, permission, correction, and consumer adapter tests.
- [x] **T5 (P1, human: ~2 days / Codex: ~3-4 hours)**: cut intent decisions to v2 and satisfy issue #421 closure criteria.
- Verify: daemon regression suite, explicit file safety, clarification actionability, and eval cases.
- [ ] **T6 (P2, human: ~2 days / Codex: ~3-5 hours)**: add proactive summaries, coverage watermarks, stale-result rejection, and fork snapshots.
- [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.
- Verify: shared-core, stale fan-in, handoff, and security fixtures.
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/conversation-context-store-types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type {
ContextSegment,
ContextSegmentState,
ConversationContextSnapshot,
ConversationContextSummary,
ConversationContextSummaryScope,
ConversationDomainRef,
ConversationItemVisibilityV1,
TurnContextAssignment,
TurnContextAssignmentStatus,
TurnContextDecisionSource,
Expand Down Expand Up @@ -51,3 +54,71 @@ export interface ListConversationContextSummariesOptions {
readonly throughSequence?: number;
readonly limit?: number;
}

export interface ConversationContextSummarySourceFence {
readonly itemId: string;
readonly sequence: number;
readonly revision: number;
readonly updatedAt: string;
readonly visibility: ConversationItemVisibilityV1;
readonly visibilityOwnerId?: string;
readonly turnId: string;
readonly assignmentId: string;
readonly assignmentRevision: number;
}

export interface CommitConversationContextSummaryInput {
readonly threadId: string;
readonly segmentId?: string;
readonly scope: ConversationContextSummaryScope;
readonly coveredFromSequence: number;
readonly coveredThroughSequence: number;
readonly objective?: string;
readonly decisions?: readonly string[];
readonly constraints?: readonly string[];
readonly openQuestions?: readonly string[];
readonly activeWorkRefs?: readonly ConversationDomainRef[];
readonly sourceRefs: readonly ConversationDomainRef[];
readonly policyVersion: string;
readonly projectionVersion: string;
readonly visibilityFingerprint: string;
readonly expectedActiveSummaryId: string | null;
readonly sourceFence: readonly ConversationContextSummarySourceFence[];
}

export type ConversationContextSummaryDiscardReason =
| "active_summary_changed"
| "segment_changed"
| "source_items_changed"
| "assignments_changed"
| "visibility_changed";

export type CommitConversationContextSummaryResult =
| {
readonly status: "committed";
readonly summary: ConversationContextSummary;
}
| {
readonly status: "discarded_stale";
readonly reason: ConversationContextSummaryDiscardReason;
};

export interface CreateConversationContextSnapshotInput {
readonly id?: string;
readonly threadId: string;
readonly segmentId: string;
readonly projectionVersion: string;
readonly throughItemSequence: number;
readonly summaryIds?: readonly string[];
readonly sourceRefs: readonly ConversationDomainRef[];
readonly policyVersion: string;
}

export interface ListConversationContextSnapshotsOptions {
readonly threadId: string;
readonly limit?: number;
}

export interface CreateConversationContextSnapshotResult {
readonly snapshot: ConversationContextSnapshot;
}
1 change: 1 addition & 0 deletions packages/core/src/conversation-item-store-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface ListConversationItemsForSegmentOptions {
readonly segmentId: string;
readonly throughSequence: number;
readonly afterSequence?: number;
readonly order?: "oldest" | "newest";
readonly limit?: number;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/conversation-thread-topology-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { LingThread } from "./run-types.js";
import type { CreateConversationContextSnapshotInput } from "./conversation-context-store-types.js";

interface ConversationThreadTopologyBaseInput {
readonly id?: string;
Expand All @@ -19,6 +20,8 @@ export type ResolveOrCreateConversationThreadInput =
readonly surfaceKind: "branch";
readonly parentThreadId: string;
readonly parentTurnId: string;
readonly forkedFromRunId?: string;
readonly forkContextSnapshot: Omit<CreateConversationContextSnapshotInput, "threadId">;
});

export interface ResolveOrCreateConversationThreadResult {
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/run-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,20 @@ export interface RunStore {
listConversationContextSummaries(
options: import("./conversation-context-store-types.js").ListConversationContextSummariesOptions,
): readonly import("@ling-stack/contracts").ConversationContextSummary[];
commitConversationContextSummary(
input: import("./conversation-context-store-types.js").CommitConversationContextSummaryInput,
now: string,
): import("./conversation-context-store-types.js").CommitConversationContextSummaryResult;
createConversationContextSnapshot(
input: import("./conversation-context-store-types.js").CreateConversationContextSnapshotInput,
now: string,
): import("@ling-stack/contracts").ConversationContextSnapshot;
getConversationContextSnapshot(
id: string,
): import("@ling-stack/contracts").ConversationContextSnapshot | undefined;
listConversationContextSnapshots(
options: import("./conversation-context-store-types.js").ListConversationContextSnapshotsOptions,
): readonly import("@ling-stack/contracts").ConversationContextSnapshot[];
createChannelMessageRouteDecision(input: CreateChannelMessageRouteDecisionInput, now: string): ChannelMessageRouteDecision;
getChannelMessageRouteDecision(id: string): ChannelMessageRouteDecision | undefined;
listChannelMessageRouteDecisions(options?: ListChannelMessageRouteDecisionsOptions): ChannelMessageRouteDecision[];
Expand Down Expand Up @@ -2278,6 +2292,10 @@ export type ChannelStoreFacet = RunStoreFacet<
| "applyTurnContextAssignment"
| "getConversationContextSummary"
| "listConversationContextSummaries"
| "commitConversationContextSummary"
| "createConversationContextSnapshot"
| "getConversationContextSnapshot"
| "listConversationContextSnapshots"
| "createChannelMessageRouteDecision"
| "getChannelMessageRouteDecision"
| "listChannelMessageRouteDecisions"
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/storage/sqlite-run-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,8 @@ export class SqliteRunStore implements RunStore {
{
createLingThread: (input, now) => this.lingThreads.createLingThread(input, now),
getLingThread: (threadId) => this.lingThreads.getLingThread(threadId),
createConversationContextSnapshotWithinTransaction: (input, now) =>
this.conversationContext.createConversationContextSnapshotWithinTransaction(input, now),
},
);
this.lingThreadCleanup = new SqliteLingThreadCleanupStore(
Expand Down Expand Up @@ -2547,6 +2549,32 @@ export class SqliteRunStore implements RunStore {
return this.conversationContext.listConversationContextSummaries(options);
}

commitConversationContextSummary(
input: import("../conversation-context-store-types.js").CommitConversationContextSummaryInput,
now: string,
): import("../conversation-context-store-types.js").CommitConversationContextSummaryResult {
return this.conversationContext.commitConversationContextSummary(input, now);
}

createConversationContextSnapshot(
input: import("../conversation-context-store-types.js").CreateConversationContextSnapshotInput,
now: string,
): import("@ling-stack/contracts").ConversationContextSnapshot {
return this.conversationContext.createConversationContextSnapshot(input, now);
}

getConversationContextSnapshot(
id: string,
): import("@ling-stack/contracts").ConversationContextSnapshot | undefined {
return this.conversationContext.getConversationContextSnapshot(id);
}

listConversationContextSnapshots(
options: import("../conversation-context-store-types.js").ListConversationContextSnapshotsOptions,
): readonly import("@ling-stack/contracts").ConversationContextSnapshot[] {
return this.conversationContext.listConversationContextSnapshots(options);
}

createChannelMessage(input: CreateChannelMessageInput, now: string): ChannelMessage {
if (this.legacyContinuityFixtureMode) return this.createLegacyChannelMessageFixture(input, now);
const channel = this.getChannel(input.channelId);
Expand Down
Loading
Loading