feat(chat): migrate from flow.kind=definition hack to flow.kind=value

Oracle deferred caveat #2 — chat-on-flow-collection hack — CLOSED.

Previous design used flow.kind=definition + source_context=EA2_CHAT_THREAD
for chat threads and EA2_CHAT_MSG for messages, then filtered them out of
hub/wizard surfaces via a NON_BUSINESS_SOURCE_CONTEXTS exclusion. That
was containment, not a clean model.

Verified EA2 contract: flow.kind='value' is a valid enum value (probed
against ea2.baseline.svc — kind=value returns 200, kind=conversation/
thread/message/channel/note all return 400 schema violation).

- Thread: flow.kind=value, source_context=CANVAS_CHAT_THREAD
- Message: flow.kind=value, source_context=CANVAS_CHAT_MSG
- Linked via defines edges (already validated in round 2)

Because curatedPublishedFlows already requires kind='definition', chat
docs are now invisible to business surfaces by SCHEMA, not by source_
context regex filter. The filter is kept as defence-in-depth (+ legacy
docs created during round 2 still need to be filtered out).

Tried data collection first; it requires relates(role=sourced_from) edge
to an sdx_connections handle per the P19 invariant — wrong model for
chat, so flow.kind=value is the right home.
This commit is contained in:
2026-06-14 13:46:19 +04:00
parent 1cfd787179
commit 8f35f7b88b
2 changed files with 11 additions and 9 deletions
+9 -9
View File
@@ -48,12 +48,12 @@ async function jsonFetch(path: string, init?: RequestInit) {
}
export const chatApi = {
/** List existing chat-thread flow docs for the current tenant. */
/** List existing chat-thread flow docs (kind=value, source_context=CANVAS_CHAT_THREAD). */
async listThreads(signal?: AbortSignal): Promise<ChatThread[]> {
const res = await jsonFetch(`/api/ea2/flow/processes?limit=200`, { signal });
const res = await jsonFetch(`/api/ea2/flow/processes?limit=500&kind=value`, { signal });
const items = (res?.items || []) as any[];
return items
.filter((it) => it?.source_context === "EA2_CHAT_THREAD")
.filter((it) => it?.source_context === "CANVAS_CHAT_THREAD")
.map((it) => ({
_key: it._key,
display_name: it.display_name || "Chat",
@@ -64,17 +64,17 @@ export const chatApi = {
},
/**
* Create a thread between two persona emails. Returns the new thread key.
* Falls back to a deterministic key built from the participants when present.
* Create a thread as flow.kind=value so it is invisible to process-definition
* surfaces by EA2 schema rather than by source_context filtering.
*/
async createThread(displayName: string, participants: string[], signal?: AbortSignal): Promise<string> {
const payload = {
kind: "definition",
kind: "value",
status: "published",
name: `chat_${Date.now()}`,
display_name: displayName,
description: `Conversation between ${participants.join(" & ")}`,
source_context: "EA2_CHAT_THREAD",
source_context: "CANVAS_CHAT_THREAD",
config: { chat: { participants } },
};
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow`, {
@@ -124,12 +124,12 @@ export const chatApi = {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
kind: "definition",
kind: "value",
status: "published",
name: `msg_${Date.now()}`,
display_name: body.slice(0, 80),
description: body,
source_context: "EA2_CHAT_MSG",
source_context: "CANVAS_CHAT_MSG",
config: { chat: { author_email: authorEmail, thread_key: threadKey } },
}),
signal,