Skip to content

feat: wire Stream Event v1 transport and renderer log - #43

Open
sjr666666 wants to merge 13 commits into
helsome:mainfrom
sjr666666:feat/27-stream-event-transport
Open

feat: wire Stream Event v1 transport and renderer log#43
sjr666666 wants to merge 13 commits into
helsome:mainfrom
sjr666666:feat/27-stream-event-transport

Conversation

@sjr666666

@sjr666666 sjr666666 commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Wires the Stream Event Protocol v1 across transport and renderer (issue #27), additive to the existing AgentEvent path:

Transport (#43)

  • apps/electron/src/main/kernelHost.ts: attach() also subscribes RunManager.subscribeStream and forwards { sessionId, event } over a new IPC channel agent:stream (cleaned up on re-attach). Legacy agent:event untouched.
  • apps/electron/src/preload/index.ts + index.cjs: expose kernel.onStreamEvent(cb) -> unsubscribe, mirroring onAgentEvent.

Renderer data layer (#43)

  • packages/ui/src/atoms/streamAtoms.ts (new): pure reduceStreamLog keeps per-run events ordered by sequence, dedupes replays (idempotency requirement) into a drops counter, and flags gaps/out-of-order delivery as anomalies. Exposed as Jotai streamLogAtom / applyStreamEventAtom.
  • KernelBridge subscribes onStreamEvent into the log; the legacy agent:eventapplyAgentEventAtom path is untouched.
  • FinagentClient interface + fallback noop + finagentClient.ts wiring.

Type fix (#43)

No visual change — renderer data layer only; rendering from text_delta (and the explicit cancelled state) is the follow-up, best after the protocol open questions settle.

Verification

  • Protocol tests: stream-events.test.ts + stream-event-adapter.test.ts → 10 passed
  • Renderer: streamAtoms.test.ts (5) + sessionAtoms.test.ts (5) passed
  • Kernel E2E regression: agent-kernel.test.ts → 6 passed
  • tsc --noEmit for electron/core/ui/shared → no new errors beyond the known pre-existing i18next resolution issue (local bun 1.4 layout)

Dependency

Builds on #42 (RunManager stream channel) → #41 (protocol types). The discriminated-union fix lives here so #41 merges as-is.

Open questions

Same as ADR 0001 (resume data source, status.phase, messageId vs runId).

@sjr666666 sjr666666 changed the title feat(electron): forward Stream Event v1 channel to renderer feat: wire Stream Event v1 transport and renderer log Sep 11, 2026

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这组 #41#42#43 的方向是对的,但当前还不建议把协议定型进 main,有两个架构阻塞需要先解决:

  1. messageId === runId 不应作为 v1 固化。#34 已明确要求 message / generation / run 是不同身份:message 有稳定 id,assistant generation 绑定具体 run。现在把两者合并会直接给 edit/regenerate/fork 留下错误语义。建议让 message 级事件携带真实 messageId;run 级事件可只依赖 runId,或 messageId 可选,但不要把两者定义成同一身份。
  2. ADR 写了 reconnect=lastSequence replay,但当前 stack 只有 parallel event channel + renderer log/dedupe,没有 replay source/API/buffer,也没有“明确无法恢复”的失败路径。也就是说 contract 文档承诺了实现尚不存在的行为。

另外 #43 自己说明 #41StreamEvent 不是正确 discriminated union,要到 #43 才修,所以不要单独 merge #41/#42 后在 main 留一个已知有缺陷的协议类型。

建议:在本 stack 顶部统一修掉 identity contract,并至少实现一个明确的 reconnect 行为(内存 replay 也可以,或明确 terminal failure),然后补 Issue #27 要求的真实 Copilot E2E:model + tool + citation,分别验证 cancel 和 transport interruption。完成后这组三层可以按依赖顺序合。

sjr666666 pushed a commit to sjr666666/folio that referenced this pull request Sep 11, 2026
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence.

Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
@sjr666666

sjr666666 commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you for the detailed review. Both blockers are now addressed at the top of this stack (new commits just pushed):

1. Identity contract (#34)messageId is decoupled from runId:

  • Envelope: messageId?: string; message-level events (message_started / text_delta / message_completed / cancelled) carry the real assistant messageId, run-level events (run_started / run_completed / tool_* / citation_added / status / error) carry only runId.
  • RunManager now pre-assigns one stable assistant message id per run and threads it through the message-level events (and reuses it as the persisted assistant message id, so stream ids and storage ids are the same object identity).
  • Idempotency key is now runId + sequence only.
  • ADR 0001 updated; open question 3 marked resolved.

2. Reconnect — implemented a concrete in-memory replay plus an explicit unrecoverable path:

  • StreamEventHistory (packages/shared/src/kernel/stream-history.ts): per-run in-memory tail, hardened for eviction.
  • RunManager.replayStream(runId, lastSequence) returns the contiguous tail; when the run is unknown or the tail is non-contiguous (eviction) it returns recoverable: false.
  • Exposed over IPC: runs:stream-replay (main handler + preload ts/cjs + FinagentClient contract).

Verification: adapter (7, incl. messageId injection) + history (5) + kernel E2E regression (6) all pass; electron/core/ui/shared tsc show no new errors beyond the pre-existing local i18next resolution issue.

On the real Copilot E2E (model + tool + citation, cancel and transport interruption): I'll wire this next against the eval fixture infrastructure so it runs without external API keys — will follow up in this thread before we merge.

@helsome
helsome dismissed their stale review September 11, 2026 08:00

作者已解决原 review 的两个实质 blocker(message/run identity 与 reconnect replay)。重新评估后,真实完整 E2E 不再作为当前合入前置条件;剩余仅需解决与最新 main 的 Git 冲突并通过基础 CI。

helsome
helsome previously approved these changes Sep 11, 2026

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

重新审查后通过代码方向。你已经解决了上一轮的两个核心 blocker:messageId / runId 身份拆分,以及明确的 in-memory replay / unrecoverable reconnect 路径。完整真实 Copilot E2E 可以作为后续增强,不再作为当前合入前置条件。现在只剩 Git 层面的 dirty:请把最终 #43 rebase 到最新 main,保留 Run Budget / Provider Router 等主干后续改动,并让基础 CI(unit/typecheck/secret scan)跑通;完成后可直接合。

@sjr666666
sjr666666 force-pushed the feat/27-stream-event-transport branch from f345099 to d953606 Compare September 11, 2026 08:37
sjr666666 pushed a commit to sjr666666/folio that referenced this pull request Sep 11, 2026
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence.

Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
@sjr666666
sjr666666 force-pushed the feat/27-stream-event-transport branch from d953606 to 05ae328 Compare September 11, 2026 08:53
sjr666666 pushed a commit to sjr666666/folio that referenced this pull request Sep 11, 2026
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence.

Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
@sjr666666
sjr666666 force-pushed the feat/27-stream-event-transport branch from 517434d to b9e2cc5 Compare September 11, 2026 09:12
@helsome
helsome dismissed their stale review September 11, 2026 11:39

审核标准已更新:Ready PR 在 APPROVE 前必须补齐可复现测试报告(至少 Bun 版本、OS、实际命令与结果)。原审批基于旧标准,先撤销,代码方向本身仍认可。

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

代码方向仍然认可,上一轮 identity/reconnect blocker 已修;这次 Changes Requested 只是同步当前统一验收规则。请在 clean rebase 最新 main 时,把测试报告补成可复现格式:Bun 版本、OS/平台、实际 focused test/typecheck 命令与 pass/fail;现有 protocol/renderer/kernel 测试范围够了,不需要补完整真实 Copilot E2E。rebase + 基础 CI + 报告字段齐全后即可恢复 APPROVE。

石敬荣 added 13 commits September 11, 2026 19:49
Introduce the pure-type foundation for the structured streaming event protocol (issue helsome#27): versioned envelope, 12 typed events, and the idempotency/cancel/reconnect contracts as types. Zero runtime change.

Adds ADR 0001 documenting context, decision, migration path and open questions for maintainer review.
Emit Stream Event Protocol v1 events alongside the existing AgentEvent stream (issue helsome#27, ADR 0001 migration step 2). Adds toStreamEvents mapping (8 AgentEvent types -> 12 protocol events, cancel normalized to 'cancelled') and RunManager.subscribeStream. Existing AgentEvent consumers are untouched; the parallel channel only activates when a stream subscriber is registered.
StreamEvent was a single indexed-union instantiation (Tagged StreamEventEnvelope<StreamEventType>), so payload could not be narrowed by type in switch/if. Rewrite as a distributive mapped union; on-disk type shape is unchanged. Adjusts adapter unit-test helper accordingly.
KernelHost subscribes RunManager.subscribeStream (issue helsome#27) and forwards { sessionId, event } over IPC channel 'agent:stream'; preload exposes electronAPI.kernel.onStreamEvent. Legacy 'agent:event' delivery untouched. Transport only; renderer consumption follows.
Renderer-side data layer for issue helsome#27: KernelBridge subscribes client.kernel.onStreamEvent into a parallel StreamEvent log. reduceStreamLog keeps per-run events ordered by sequence, dedupes replays (drops), and flags gaps/out-of-order (anomalies) as a protocol health signal. FinagentClient and preload.cjs wire onStreamEvent; FinagentClient adds onStreamEvent contract with fallback noop. No visual change: existing AgentEvent rendering untouched.
Addresses helsome#43 review: messageId is no longer fused with runId (issue helsome#34 - message/generation/run are distinct identities). message-level events carry the real assistant messageId, pre-assigned per run in RunManager; run-level events omit it; idempotency key is now runId + sequence.

Adds StreamEventHistory: in-memory per-run tail used by RunManager.replayStream(runId, lastSequence) for reconnect resume, with an explicit recoverable:false path when the run is unknown or the tail is non-contiguous (eviction).
Adds 'runs:stream-replay' handler (KernelHost.streamReplay -> RunManager.replayStream), preload (ts + cjs) streamReplay, finagentClient wiring and FinagentClient contract with fallback noop.
…plementation

Envelope messageId is now optional (message-level events only); idempotency key runId+sequence; Reconnect row documents implemented StreamEventHistory + IPC with explicit unrecoverable path; open question 3 resolved.
run_started(seq 1)与 runtime 自产的首个事件(同样 seq 1)冲突,
导致 replay(runId, 0) 被判为不连续(recoverable: false),renderer 的
幂等去重也会误丢事件。RunManager 现为每个 run 持有 RunProtocol 计数器,
扇出前把所有 AgentEvent 统一重排为 1..N;messageId 随该对象传递,
崩溃/取消兜底合成的 terminal 事件也保住 helsome#34 的身份契约。replay 游标
超出已知最大 sequence 时,由静默视为已同步改为明确不可恢复。
kernel 级(stream-replay.e2e.test.ts):真实 LocalRuntimeAdapter +
RunManager + 持久化。六个用例:完整 run 后全量补发、无实时订阅者时
事件仍入历史、按 lastSequence 中途断线补发、取消路径产生可补发的显式
cancelled 事件、带工具 run 的全量流、未知 run 与伪造游标返回明确的
不可恢复路径。

app 级(e2e/stream-replay.mjs):真实 Electron + preload IPC(CDP)。
四个用例:replay(runId, 0) 与实时投递逐字节一致、断线补发拼接还原
完整流、未知 run 跨 IPC 返回不可恢复、非法 lastSequence 被拒以
INVALID_ARGUMENT。
AgentKernelHost.attach wires the Stream Event v1 channel through
kernel.runs.subscribeStream; the fake kernel in kernelHost.test.ts lacked
the method, so the transport test crashed with TypeError before asserting.
Align the fake with the kernel surface (subscribeStream + replayStream)
so focused CI runs green again.
The Stream Event v1 IPC surface (streamReplay / onStreamEvent) landed in
the kernel channel types; the test kernel client was not updated, which
broke the ui + i18n + electron typecheck gates. Align the stub with the
channel so typecheck is green again.
streamReplay returned an inferred StreamReplayResult that TS could not
name portably across the @finagent/shared boundary (error TS2742). Export
the type from the shared package and annotate the kernelHost surface so
every package typecheck gate passes.
@sjr666666
sjr666666 force-pushed the feat/27-stream-event-transport branch from b9e2cc5 to e0d46c4 Compare September 11, 2026 11:49

helsome commented Sep 11, 2026

Copy link
Copy Markdown
Owner

这轮复查有两个更新:

合并策略调整为:#43 clean 后直接以 stack 顶部合入 #41#42#43 的完整协议增量,不再分别 squash #41/#42,避免重复历史;随后再处理 #76 的 persistence-only delta。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants