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
24 changes: 15 additions & 9 deletions apps/web/e2e/channel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2356,7 +2356,7 @@ test("Ling Agent session acceptance matrix auto-starts executable messages", asy
await expect.poll(() => {
const decision = state.channelIntentDecisions[0];
return isRecord(decision) ? [decision.action, decision.status, decision.createdBy].join("|") : "";
}).toBe("direct_workflow|accepted|deterministic");
}).toBe("execute|accepted|deterministic");
});

test("Ling Agent session keeps ordinary questions on chat routing", async ({ page }) => {
Expand Down Expand Up @@ -2412,7 +2412,7 @@ test("ordinary channel feed hides routing and intent decision internals", async
updatedAt: "2026-05-25T00:10:00.000Z",
},
{
id: "msg_direct_workflow",
id: "msg_execute",
workspaceId: "workspace_default",
channelId: "channel_engineering",
kind: "message",
Expand Down Expand Up @@ -2447,10 +2447,10 @@ test("ordinary channel feed hides routing and intent decision internals", async
];
state.channelExecutionProposals = [
{
id: "proposal_direct_workflow",
id: "proposal_execute",
workspaceId: "workspace_default",
channelId: "channel_engineering",
sourceMessageId: "msg_direct_workflow",
sourceMessageId: "msg_execute",
proposedByTeammateId: "ling_agent",
title: "Edit README.md",
prompt: "Edit README.md with one test sentence.",
Expand All @@ -2467,13 +2467,19 @@ test("ordinary channel feed hides routing and intent decision internals", async
];
state.channelIntentDecisions = [
{
schemaVersion: 1,
id: "intent_direct_workflow",
schemaVersion: 2,
id: "intent_execute",
workspaceId: "workspace_default",
channelId: "channel_engineering",
sourceMessageId: "msg_direct_workflow",
action: "direct_workflow",
proposalId: "proposal_direct_workflow",
sourceTurnId: "turn_msg_execute",
sourceItemId: "msg_execute",
contextSegmentId: "segment_channel_engineering",
contextProjectionVersion: "ctxv2_mock_msg_execute",
action: "execute",
selectedTeammateIds: [],
proposalId: "proposal_execute",
resultItemIds: [],
execution: { kind: "file_edit", targetKind: "file" },
reason: "Direct workflow proposal created by mock intent selector.",
status: "accepted",
createdBy: "deterministic",
Expand Down
55 changes: 38 additions & 17 deletions apps/web/e2e/support/mock-daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5809,13 +5809,12 @@ function maybeCreateMockWorkflowProposal(
};
state.channelExecutionProposals.unshift(proposal);
state.channelIntentDecisions.push({
schemaVersion: 1,
...mockChannelIntentDecisionContext(channelId, message),
id: `intent_${state.channelIntentDecisions.length + 1}`,
workspaceId: "workspace_default",
channelId,
sourceMessageId: typeof message.id === "string" ? message.id : "",
action: "workflow_proposal",
selectedTeammateIds: finalSelectedTeammates.flatMap((teammate) => typeof teammate.id === "string" ? [teammate.id] : []),
proposalId: proposal.id,
resultItemIds: [],
reason: "Workflow proposal created from channel intent.",
status: "accepted",
createdBy: "deterministic",
Expand Down Expand Up @@ -5913,14 +5912,12 @@ function maybeCreateMockMaterializationClarifyReply(
};
state.channelMessages.push(aiReply);
state.channelIntentDecisions.push({
schemaVersion: 1,
...mockChannelIntentDecisionContext(channelId, message),
id: `intent_${state.channelIntentDecisions.length + 1}`,
workspaceId: "workspace_default",
channelId,
sourceMessageId: typeof message.id === "string" ? message.id : "",
action: "clarify",
selectedTeammateId: "ling_agent",
replyMessageId: aiReply.id,
selectedTeammateIds: ["ling_agent"],
resultItemIds: [aiReply.id],
clarification: { missingFields: ["materialized_input"], question: replyBody },
reason: decisionReason,
status: "accepted",
createdBy: "deterministic",
Expand Down Expand Up @@ -5987,13 +5984,13 @@ function maybeCreateMockSingleStepExecutableProposal(
};
state.channelExecutionProposals.unshift(proposal);
state.channelIntentDecisions.push({
schemaVersion: 1,
...mockChannelIntentDecisionContext(channelId, message),
id: `intent_${state.channelIntentDecisions.length + 1}`,
workspaceId: "workspace_default",
channelId,
sourceMessageId: typeof message.id === "string" ? message.id : "",
action: "direct_workflow",
action: "execute",
selectedTeammateIds: [],
proposalId: proposal.id,
resultItemIds: [],
execution: { kind: isEdit ? "file_edit" : "file_create", targetKind: "file" },
reason: "Direct single-step executable proposal created by mock intent selector.",
status: "accepted",
createdBy: "deterministic",
Expand All @@ -6004,6 +6001,30 @@ function maybeCreateMockSingleStepExecutableProposal(
return { autoStartedWorkflow };
}

function mockChannelIntentDecisionContext(
channelId: string,
message: Record<string, unknown>,
): {
readonly schemaVersion: 2;
readonly workspaceId: string;
readonly channelId: string;
readonly sourceTurnId: string;
readonly sourceItemId: string;
readonly contextSegmentId: string;
readonly contextProjectionVersion: string;
} {
const sourceItemId = typeof message.id === "string" ? message.id : "unknown_item";
return {
schemaVersion: 2,
workspaceId: "workspace_default",
channelId,
sourceTurnId: `turn_${sourceItemId}`,
sourceItemId,
contextSegmentId: `segment_${channelId}`,
contextProjectionVersion: `ctxv2_mock_${sourceItemId}`,
};
}

function mockExecutableFileTarget(state: MockState, channelId: string, body: string): string | undefined {
const explicitPath = body.match(/(?:^|[\s"'`([{(,。::])((?:\/(?!\/)|\.{1,2}\/|Users\/)[^\s"'`,。;;::!?!?))\]}]*?[\w.-]+\.(?:md|txt|json|ts|tsx|js|jsx|py|css|html|yml|yaml))/iu)?.[1];
if (explicitPath) {
Expand Down Expand Up @@ -10952,15 +10973,15 @@ export async function handleDaemonRoute(
const channelIntentDecisionsMatch = /^\/channels\/([^/]+)\/intent-decisions$/.exec(path);
if (channelIntentDecisionsMatch && request.method() === "GET") {
const channelId = decodeURIComponent(channelIntentDecisionsMatch[1]);
const sourceMessageId = url.searchParams.get("sourceMessageId");
const sourceItemId = url.searchParams.get("sourceItemId");
await route.fulfill({
json: {
cwd,
channelId,
decisions: state.channelIntentDecisions.filter((decision) =>
isRecord(decision) &&
decision.channelId === channelId &&
(!sourceMessageId || decision.sourceMessageId === sourceMessageId)),
(!sourceItemId || decision.sourceItemId === sourceItemId)),
},
});
return;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/api/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export async function listChannelMessageRouteDecisions(
export async function listChannelIntentDecisions(
cwd: string,
channelId: string,
options: { readonly sourceMessageId?: string; readonly action?: string; readonly status?: string; readonly limit?: number } = {},
options: { readonly sourceItemId?: string; readonly action?: string; readonly status?: string; readonly limit?: number } = {},
): Promise<ChannelIntentDecisionListResponse> {
return daemonClient.listChannelIntentDecisions(cwd, channelId, options);
}
Expand Down
63 changes: 49 additions & 14 deletions apps/web/src/features/workspace/ChannelInspectorPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,6 @@ function ChannelIntentInspectContent({
readonly teammates: readonly ChannelMention[];
readonly t: Translate;
}): JSX.Element {
const replyMessageIds = uniqueStrings([decision.replyMessageId, ...(decision.replyMessageIds ?? [])].filter((id): id is string => Boolean(id)));
return (
<>
<p>{channelIntentDecisionUserReason(decision, t)}</p>
Expand All @@ -792,13 +791,49 @@ function ChannelIntentInspectContent({
<dd>{channelIntentDecisionCreatedByLabel(decision, t)}</dd>
</div>
<div>
<dt>{t("Source message")}</dt>
<dd>{decision.sourceMessageId}</dd>
<dt>{t("Source item")}</dt>
<dd>{decision.sourceItemId}</dd>
</div>
<div>
<dt>{t("Source turn")}</dt>
<dd>{decision.sourceTurnId}</dd>
</div>
<div>
<dt>{t("Context segment")}</dt>
<dd>{decision.contextSegmentId}</dd>
</div>
<div>
<dt>{t("Projection version")}</dt>
<dd>{decision.contextProjectionVersion}</dd>
</div>
<div>
<dt>{t("Reply messages")}</dt>
<dd>{formatOptionalIdList(replyMessageIds, t)}</dd>
<dt>{t("Result items")}</dt>
<dd>{formatOptionalIdList(decision.resultItemIds, t)}</dd>
</div>
{decision.clarification ? (
<>
<div>
<dt>{t("Missing fields")}</dt>
<dd>{formatOptionalIdList(decision.clarification.missingFields, t)}</dd>
</div>
<div>
<dt>{t("Clarification question")}</dt>
<dd>{decision.clarification.question}</dd>
</div>
</>
) : null}
{decision.execution ? (
<>
<div>
<dt>{t("Execution kind")}</dt>
<dd>{decision.execution.kind}</dd>
</div>
<div>
<dt>{t("Target kind")}</dt>
<dd>{decision.execution.targetKind}</dd>
</div>
</>
) : null}
<div>
<dt>{t("Proposal")}</dt>
<dd>{decision.proposalId || t("None")}</dd>
Expand All @@ -807,10 +842,10 @@ function ChannelIntentInspectContent({
<dt>{t("Workflow")}</dt>
<dd>{decision.workflowId || t("None")}</dd>
</div>
{decision.errorMessage ? (
{decision.errorCode ? (
<div>
<dt>{t("Error")}</dt>
<dd>{decision.errorMessage}</dd>
<dd>{decision.errorCode}</dd>
</div>
) : null}
</dl>
Expand Down Expand Up @@ -913,25 +948,25 @@ function channelIntentDecisionActionLabel(action: ChannelIntentDecision["action"
switch (action) {
case "no_reply":
return t("No reply");
case "single_reply":
return t("Single reply");
case "reply":
return t("Reply");
case "visible_multi_reply":
return t("Visible multi-reply");
case "clarify":
return t("Clarify");
case "workflow_proposal":
return t("Workflow proposal");
case "direct_workflow":
return t("Direct workflow proposal");
case "execute":
return t("Execute");
}
}

function channelIntentDecisionCreatedByLabel(decision: ChannelIntentDecision, t: Translate): string {
return decision.createdBy === "model" ? t("Model selector") : t("Deterministic rule");
return decision.createdBy === "model_policy" ? t("Model selector") : t("Deterministic rule");
}

function channelIntentDecisionUserReason(decision: ChannelIntentDecision, t: Translate): string {
if (decision.errorMessage) {
if (decision.errorCode) {
return t("Ling could not safely handle this channel intent.");
}
return decision.reason || channelIntentDecisionActionLabel(decision.action, t);
Expand All @@ -949,7 +984,7 @@ function formatIntentDecisionTeammates(
teammates: readonly ChannelMention[],
t: Translate,
): string {
const ids = uniqueStrings([decision.selectedTeammateId, ...(decision.selectedTeammateIds ?? [])].filter((id): id is string => Boolean(id)));
const ids = decision.selectedTeammateIds;
if (ids.length === 0) {
return t("None");
}
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4264,4 +4264,14 @@ export const enUS = {
"Generate a fresh five-minute QR code, then confirm the phone on this PC.": "Generate a fresh five-minute QR code, then confirm the phone on this PC.",
"Listener": "Listener",
"Generate pairing code": "Generate pairing code",
"Source item": "Source item",
"Source turn": "Source turn",
"Context segment": "Context segment",
"Projection version": "Projection version",
"Result items": "Result items",
"Execute": "Execute",
"Missing fields": "Missing fields",
"Clarification question": "Clarification question",
"Execution kind": "Execution kind",
"Target kind": "Target kind",
} as const satisfies Readonly<Record<string, string>>;
9 changes: 9 additions & 0 deletions apps/web/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6103,4 +6103,13 @@ export const zhCN = {
"Generate a fresh five-minute QR code, then confirm the phone on this PC.": "生成一个新的五分钟配对码,然后在这台 PC 上确认手机。",
"Listener": "监听器",
"Generate pairing code": "生成配对码",
"Source item": "来源条目",
"Context segment": "上下文分段",
"Projection version": "投影版本",
"Result items": "结果条目",
"Execute": "执行",
"Missing fields": "缺失字段",
"Clarification question": "澄清问题",
"Execution kind": "执行类型",
"Target kind": "目标类型",
} as const satisfies Readonly<Record<string, string>>;
2 changes: 1 addition & 1 deletion apps/web/src/lib/conversation-turn-projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function buildConversationTurnProjection(input: BuildConversationTurnProj
);
const intentDecisionsBySourceMessageId = groupBy(
input.intentDecisions.filter((decision) => decision.channelId === input.channelId && decision.status === "accepted"),
(decision) => decision.sourceMessageId,
(decision) => decision.sourceItemId,
);
const workflowById = new Map(input.workflows.filter((workflow) => workflow.channelId === input.channelId).map((workflow) => [workflow.id, workflow]));
const runsById = new Map(input.runs.map((run) => [run.id, run]));
Expand Down
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 and S3 implemented and locally verified; S4-S7 pending
Status: G0 complete; S-1 and S0 merged; S1+S2, S3, and S4 implemented and locally verified; S5-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 @@ -447,6 +447,14 @@ 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
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,
explicit README execution, and unrelated-request segment isolation. S4-specific Web E2E
passes 3/3. The broader `channel.spec.ts` run is 108/112; four failures outside the S4
touch set remain part of the final S7/full-program E2E gate.

### Changes

- Replace v1 selector parsing and persistence with `ChannelIntentDecisionV2`.
Expand Down Expand Up @@ -792,9 +800,9 @@ interfaces at the end of S3 and keep implementation files separate before parall
idempotency, paging, and supersession tests.
- [x] **T3 (P1, human: ~2 days / Codex: ~3-4 hours)**: create every user `LingTurn` and map root/topic/branch explicitly.
- Verify: daemon ingest and thread-lineage tests.
- [ ] **T4 (P1, human: ~3 days / Codex: ~5-7 hours)**: implement segmentation policy and shared projector; remove raw fixed-window consumers.
- [x] **T4 (P1, human: ~3 days / Codex: ~5-7 hours)**: implement segmentation policy and shared projector; remove raw fixed-window consumers.
- Verify: projection consistency, permission, correction, and consumer adapter tests.
- [ ] **T5 (P1, human: ~2 days / Codex: ~3-4 hours)**: cut intent decisions to v2 and close issue #421.
- [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.
- Verify: budget, race, invalidation, reconstruction, and fallback tests.
Expand Down Expand Up @@ -848,7 +856,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 and S3 are implemented and
locally verified; S4-S7 retain the reviewed delivery order and exit criteria.
**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.

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 and S3 implemented and locally verified; S4-S7 pending
Status: approved design baseline; S-1 and S0 merged; S1+S2, S3, and S4 implemented and locally verified; S5-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/channel-intent-workflow-selector.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Channel Intent To Workflow Selector

Status: current v1 implementation reference; v2 clean-break contract approved in Channel Conversation Continuity v2
Status: superseded v1 implementation reference; S4 runtime now uses the v2 clean-break contract in Channel Conversation Continuity v2
Date: 2026-06-10

Related docs:
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 and S3 implemented and locally verified | S4-S7, final migration, recovery, performance, security, eval, and Web E2E gates pass |
| [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 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
Loading
Loading