Feature/push schema unification 0.8#8
Conversation
Coordinated minor across the whole amsg ecosystem. New @rei-standard/amsg-shared
package defines the AmsgPush discriminated union keyed by `messageKind`
(`content` / `reasoning` / `tool_request` / `error`) as a literal-type
discriminator, plus the four builders and type guards. All four existing
amsg sub-packages migrate onto it.
Versions (all `*-next.0` pre-release; CI publishes to `next` dist-tag):
- @rei-standard/amsg-shared 0.1.0-next.0 (new)
- @rei-standard/amsg-instant 0.7.0 → 0.8.0-next.0
- @rei-standard/amsg-server 2.3.2 → 2.4.0-next.0
- @rei-standard/amsg-sw 2.0.1 → 2.1.0-next.0
- @rei-standard/amsg-client 2.2.3 → 2.3.0-next.0
Inter-package deps pinned exact (no caret).
Highlights:
- LLM-driven paths (instant legacy + agentic-loop hook, server prompted/auto)
auto-emit ReasoningPush before the content burst when the LLM response
carries non-empty `reasoning_content`. Same `sessionId` across reasoning
+ content + every agentic-loop iteration. Hook path has an
`autoEmitReasoning: false` opt-out on `createInstantHandler`.
- Legacy `{ type: 'error', code: '...' }` envelope removed. HOOK_THREW /
LOOP_EXCEEDED now ship as `ErrorPush` (`messageKind: 'error'`).
- SW dispatches per-kind `postMessage` events to controlled clients
(`rei-amsg-{content,reasoning,tool-request,error,unknown}-received`).
showNotification only fires for `content` (and legacy un-kinded payloads).
- Server messageId is now deterministic across retries for scheduled rows
(`msg_task_{id}_{i}`); UUID fallback only when `task.id == null`.
- shared package uses `tsc --allowJs --declaration --emitDeclarationOnly`
for type emission (tsup's `dts:true` doesn't extract JSDoc types from
.js entries) — the .d.ts now declares real discriminated-union types
so TS callers can narrow on `push.messageKind`.
Tests: 226/226 pass (client 6 / instant 121 / server 74 / shared 14 / sw 11).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
把 server 2.3.1 / instant 0.6.1 / client 2.2.3 引入的"不合法 avatarUrl 直接 400"放宽为"console.warn + 把字段置空 + 继续"。头像是装饰性字段, 单个错误 URL 不应该 fail 整个调度 / 推送。 - server validateScheduleMessagePayload:data:/oversized/malformed 的 avatarUrl 在 payload 上置 null,schedule 继续创建。 - server update-message handler:不合法的 avatarUrl 从 patch 里 delete, 存储里的旧头像保持不变;其它字段照常应用。 - instant validateInstantPayload / validateContinuePayload:同样置 null 策略,/instant 与 /continue 不再因为装饰字段挂掉。 - client _validateAvatarUrl → _sanitizeAvatarUrl:本地不再抛 INVALID_AVATAR_URL_LOCAL,改为 console.warn + 置空 + 照常发请求; updateMessage 路径 delete 字段以匹配服务端 update 语义。 PAYLOAD_TOO_LARGE_LOCAL(3KB 本地体积上限)保留不变,仍是真正的整包过大 信号。validateAvatarUrl 顶层 export 行为不变,仍是纯函数返回错误描述。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
standards 文档把 §6.2 从"严格 400"重写成"console.warn + 置空 + 继续", §1 增量摘要与 §11 版本史一起更新;各包 README 同步调用方契约(client README 的"本地预校验"小节改成"本地软清空",去掉 INVALID_AVATAR_URL_LOCAL 的 try/catch 示例,只留 PAYLOAD_TOO_LARGE_LOCAL)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
之前 SW createNotificationFromPayload 的标题链是
pushNotification.title || payload.title || 'New notification'
custom hook(0.7.x 自定义 envelope)如果忘了塞 title 字段,但塞了
contactName,通知就会掉到 'New notification' 这种英文兜底上。
加一档 contactName 兜底,与 server/instant 默认 envelope 的
title: '来自 ${contactName}' 行为对齐:
pushNotification.title
|| payload.title
|| (payload.contactName && '来自 ${payload.contactName}')
|| 'New notification'
amsg-sw 2.0.1 → 2.0.2(patch)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…题 fallback
四个 amsg next 包同步从 *-next.0 → *-next.1:
- @rei-standard/amsg-server 2.4.0-next.0 → 2.4.0-next.1
- @rei-standard/amsg-instant 0.8.0-next.0 → 0.8.0-next.1
- @rei-standard/amsg-client 2.3.0-next.0 → 2.3.0-next.1
- @rei-standard/amsg-sw 2.1.0-next.0 → 2.1.0-next.1
next.0 → next.1 只两件事:
1. avatarUrl 软清空(与 stable 2.3.3 / 0.7.1 / 2.2.4 对齐)
2. SW 标题 fallback 至 「来自 {contactName}」(与 stable 2.0.2 对齐)
三轴 push schema、ReasoningPush、shared 包都**完全不动**;
@rei-standard/amsg-shared 也保持 0.1.0-next.0 不需要 bump。
四个包对 shared 的 pinned-exact 依赖都不变。
package-lock.json 同步到新版号。
Tests: 226/226 pass (server 74 / instant 121 / sw 11 / shared 14 / client 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xt.2 shared next.2: - ReasoningPush 加四个可选索引字段(messageIndex/totalMessages, chunkIndex/totalChunks),单 chunk 单 segment 时不写到 wire,老 SW 字节级兼容 - 导出 chunkReasoningByUtf8Bytes — UTF-8 codepoint-safe 字节切分 helper instant next.2: - fix: splitPattern 在 hook 模式下重新生效(0.7 的「ignored 启动 warn」是设计抽风,splitPattern 跟 hook 决策完全正交) - new: reasoningSplitPattern / errorSplitPattern — 按 messageKind 独立的句切配置,两个 kind 默认不切 - new: reasoningChunkBytes handler option(默认 2000,null 禁用)— reasoning 超阈值自动按 UTF-8 边界切,绝大多数 reasoning-heavy 部署不再需要 BlobStore - ToolRequestPush 切片:toolCalls 绑定到含最后一段 prefix 的 chunk,前 N-1 段降级为 ContentPush(无 toolCalls) - 两层 cascade:reasoningSplitPattern(语义切)→ reasoningChunkBytes(字节切),Layer 1 段间 1500ms / Layer 2 chunk 间 100ms - new event: reasoning_chunked,仅在 byte chunking 实际触发时 fire 一次 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`npm ci` failed in the next.2 release workflow because the previous commit bumped amsg-instant's amsg-shared dependency string (next.0 → next.2) but didn't refresh the root package-lock.json. Run `npm install` to update the lockfile's amsg-instant workspace entry to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xt.3
Two fixes targeting the same leaky-typedef root cause — untyped spread
bypasses TS excess-property check, so silently-misused fields look
correct to the IDE but no-op (or get ignored) at runtime.
amsg-shared 0.1.0-next.3:
- `NotificationDirective` typedef + `ContentPush.notification?` / `ToolRequestPush.notification?` — types the 7 fields amsg-sw `createNotificationFromPayload` actually consumes (`title` / `body` / `icon` / `badge` / `tag` / `renotify` / `requireInteraction`), closing the leaky-typedef path where callers had to spread untyped
- `buildContentPush` / `buildToolRequestPush` 加 `notification?` 入参 — passthrough(同 `metadata`),shape-validate (plain object; string fields must be string; boolean fields must be boolean); unknown keys 透传保持 SW forward-compat
- ToolRequestPush 上挂 `notification` 的理由:amsg-instant 切片时 demoted 前 N-1 段成 ContentPush,spread 整个 cleanPushObj,notification 跟着走;Reasoning / Error 不加(SW silent dispatch)
- 7 字段全 typed(不只 title/body):只 typed 2 个会让另外 5 个仍走 untyped spread,recreate 同源 footgun
amsg-instant 0.8.0-next.3:
- fix: `pushPayload.splitPattern` per-push override。hook 在自己返回的 pushPayload 上写 `splitPattern: null` 不再被静默忽略——`splitHookPushPayload` 用 `pushPattern !== undefined` 检测在场(`undefined` 跟字段缺省等价,回退请求级;`null` / `[]` 显式关切)。字段名固定 `splitPattern`,不分 kind——push 的 messageKind 决定切谁的文本
- 校验沿用 validation.js 新 export 的 `validateSplitPattern`,形状错 / 正则不可编译 → 抛 `HookError` 信息形如 `pushPayload.splitPattern invalid: <原因>`(明确点位,避免跟请求级混;去重 validateSplitPattern 自带的 splitPattern 前缀)
- Strip 是 clone-based:`const {splitPattern, ...rest} = pushObj` 生成新对象,原 pushObj 不动;hook 复用模板对象不会被库吃掉字段
- splitHookPushPayload 每个 push 跑一次,N-段切片 / ToolRequestPush prefix 降级都从已剥离 cleanPushObj spread,无二次切
测试:
- shared 33/33(new: 8 个 notification arg 用例覆盖 ContentPush + ToolRequestPush 透传、undefined 不写 key、非 object 拒绝、字段类型校验、未知字段透传)
- instant 175/175(new: 12 个 per-push override 用例覆盖 null/[] override、override > outer、reasoning/error 上 override 开切、undefined fallback、malformed 抛 HookError、ToolRequestPush demote+strip、no-match strip、clone-based 不 mutate、5 段非递归切)
Coordinated:
- shared 0.1.0-next.2 → 0.1.0-next.3
- instant 0.8.0-next.2 → 0.8.0-next.3(amsg-shared dep pin 同步)
- package-lock.json 已 sync — 避免 next.2 那次的 `npm ci` 失败重演
- amsg-server / amsg-sw / amsg-client 不动
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Request body fields `splitPattern` / `reasoningSplitPattern` / `errorSplitPattern` are now rejected with INVALID_PAYLOAD_FORMAT and a migration hint pointing at `decision.pushPayloads`. Removes `validatePerKindSplitPatterns` from validation and stops re-exporting `splitMessageIntoSentences` (legacy path still uses it internally; hook authors don't get it back). `validateSplitPattern` itself stays for now — `splitHookPushPayload` in message-processor.js still calls it on the per-push override path. That call site is rewired by Task 2/3 of this migration, at which point the helper goes too. handler.test.mjs picks up `splitMessageIntoSentences` directly from src/message-processor.js (instead of via the dropped re-export) so the file still loads. The legacy describe blocks that exercise the now- rejected fields stay in place and fail loudly until Task 6 deletes them. Breaking on purpose — next.4 is pre-release; we're consolidating two overlapping mechanisms (lib-side splitPattern auto-split + hook-side pushPayload) into one (pushPayloads array) before 1.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hPayloads:[] assertValidDecision now requires `pushPayloads: [...]` on `finish` / `tool-request` decisions. The singular `pushPayload` field is rejected with a migration line. Empty `pushPayloads: []` is rejected and points the caller at `decision: 'skip-push'`. Per-push `splitPattern` is also rejected. These cases all route through the existing HOOK_THREW pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hook's finish/tool-request decisions now read `pushPayloads: PushPayload[]` and the lib delivers exactly that array in order with 1500ms spacing. Per-push: `messageId` is auto-filled when the hook didn't set one; `messageIndex` / `totalMessages` are always overwritten with the array-derived values. Removed: sendChunkedPush (only consumer was this branch). LOOP_EXCEEDED diagnostic now goes through sendPushWithMaybeBlob directly (it's one push by construction). `splitMessageIntoSentences` is no longer exported; still used internally by runLegacyInstant. splitHookPushPayload / pickSplitConfig / splitOnceByRegex / DEFAULT_SPLIT_REGEX / validateSplitPattern survive one more task so reasoning Layer-1 split keeps working — Task 4 wipes them as part of the reasoning rewrite. test/helpers.mjs: createFetchRouter gains a `pushHandler` option for per-call response control (used by the mid-array-throw test); existing `onPush` and the default 201 behaviour are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emitReasoning is now a single transform: byte chunking via chunkReasoningByUtf8Bytes when reasoningContent exceeds ctx.reasoningChunkBytes, single-push passthrough otherwise. The Layer-1 sentence split (reasoningSplitPattern) is gone — callers wanting sentence-level reasoning chunks should disable autoEmitReasoning and build the pushes themselves. Removed: splitHookPushPayload, pickSplitConfig, expandReasoningPushChunks, validateSplitPattern, SPLIT_PATTERN_MAX_LENGTH, SPLIT_PATTERN_MAX_ITEMS, and the validateSplitPattern import in message-processor.js. All five survived Task 3 only because expandReasoningPushChunks still called splitHookPushPayload for the now-deleted Layer-1 reasoning split. splitMessageIntoSentences (and its private helpers DEFAULT_SPLIT_REGEX + splitOnceByRegex) kept — runLegacyInstant still applies the default sentence regex to content. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation fallback + no-op event Code-review follow-ups to b68e249. Three pure-doc additions in message-processor.js: - Comment on `>= 4` threshold floor explaining the shared-lib RangeError contract it's mirroring. - JSDoc on sliceReasoningPush's `iteration` param documenting the legacy undefined → iter_0 fallback. - JSDoc on emitReasoning noting that reasoning_chunked does not fire on single-push (no-op chunking) emissions. No logic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical s/pushPayload:/pushPayloads: [/ on every finish/tool-request hook return across agentic-loop, reasoning-push, and the agentic-loop-skeleton example. The X inside each wrapper is byte-for-byte preserved; only the key name + array wrap change. Two agentic-loop assertions pinned the OLD "no auto-fill" behaviour and needed updating to match next.4's sendPushesSequentially: - "pushes the hook-returned payload and returns finished" — deep-equal on the decrypted body now includes auto-injected messageId / messageIndex:1 / totalMessages:1 (single-element array). - "ASCII length 2600 → direct push; length 2601 → blob" — JSON overhead recomputed to account for the three auto-fill fields the framework adds before the byte-cap check. split-pattern-hook.test.mjs intentionally untouched — Task 6 deletes the whole file. handler.test.mjs's splitMessageIntoSentences / splitPattern blocks are still slated for Task 6 deletion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
split-pattern-hook.test.mjs (1659 lines pinning lib-side hook split behaviour) is removed wholesale — the lib no longer splits anything in hook mode. handler.test.mjs's `splitPattern (0.6.0)` describe block and top-level `splitMessageIntoSentences` describe block are removed — the public split helper is gone (legacy path still uses it internally), and request-level splitPattern field rejection is covered by the new `next.4 — split-pattern fields removed` describe landed in Task 1. Also drops the `splitMessageIntoSentences` symbol from the test file's import (it was triggering a module-load failure since Task 3 un-exported it from message-processor.js). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pins the next.4 hook contract per spec §测试要求: single push, multi-push spacing, mid-array throw, HookError rejection cases, kind/decision decoupling, messageId precedence, index auto-fill, and reasoning auto-emit interplay. Most cases overlap with Task-2/Task-3 tests in agentic-loop.test.mjs, but this file is the dedicated, auditable contract document. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the splitPattern / reasoningSplitPattern / errorSplitPattern subsections + per-push override sub-section (~150 lines). Add the new pushPayloads contract block, the messageId/messageIndex/totalMessages auto-fill table, and three worked examples (single push, 3-chunk content with per-chunk notification.body, tool-request mixing content and multi-toolCalls). Also drops the splitPattern line from the request payload typedef and the "same splitPattern as v0.6" claim in the legacy-compat note (legacy still uses the default sentence regex internally; the public knob is gone). Historical references to splitPattern in version-history sections intentionally left intact — Task 9's migration guide handles them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…able uniformity Two code-review follow-ups to ff4b03e: - L311 said `messageKind / ... / messageKind 特定字段`; second occurrence generalised to "kind 特定字段" with concrete examples. - 3-row auto-fill table normalised to the same "when + what" pattern per row. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on bump Pre-release breaking change documented end-to-end: - CHANGELOG.md: BREAKING section enumerating Removed / Changed / Unchanged + migration cheat sheet - docs/migration-0.8.0-next.4.md: long-form companion with motivation, 5 worked recipes (one-shot, splitPattern→caller, per-chunk notification, tool-request mixed kinds, manual reasoning), FAQ, and stable-bits inventory - package.json: 0.8.0-next.3 → 0.8.0-next.4 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urce + CHANGELOG wording Three code-review follow-ups to 1088430: - Recipe 5's .map((piece) => ...) referenced an out-of-scope `i`; fixed to .map((piece, i) => ...). - Recipe 5 now imports `randomUUID` from node:crypto explicitly (or globalThis.crypto.randomUUID() on Workers/browsers) so the messageId template isn't an unresolved reference. - CHANGELOG "Removed" subsection's "all gone (or only kept where needed)" wording rewritten to honestly say which helpers stayed for the legacy path. No code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ dead-code cleanup Final pre-tag cleanup from aggregate review: - README §Migrating from v0.6 / §Public exports no longer claim buildInstantPushPayload + splitMessageIntoSentences are exported (next.0 / next.4 removed them respectively). - migration-0.8.0-next.4.md adds a "amsg-server unaffected" note so callers maintaining both packages don't accidentally carry the request-body splitPattern across. - runLegacyInstant drops `payload.splitPattern ?? null` — validation now rejects splitPattern unconditionally, so the fallback is dead code. - examples/README.md drops the splitPattern row that referenced amsg-instant. No code-behaviour change; tests still 128/128. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… content
Reviewer feedback flagged that the next.4 README + migration-guide
examples leak SullyOS-specific concepts (sanitize/raw split, business
tag names like [[SEND_EMOJI]], LLM personas like 来自 Sully,
SullyOS-specific tool names like notion_read_diary, CJK-space-as-line-
break heuristic) — readers were getting the impression the library is
specifically for Chinese chat-character apps.
Replaced with portable shapes:
- Motivation prose now talks about "multiple heuristics in one pass"
and "inline business tags" without naming a specific tag format.
- Example 2 / Recipe 3 uses generic <thinking> internal markup + a
game-master persona ("Quest Master") with English text — the point
is "banner ≠ payload" without dragging in CJK / business tags.
- Example 3 / Recipe 4 uses generic lookup_user / fetch_weather tool
names with English narration.
- "metadata.directives" SullyOS-ism replaced with a generic
description of per-chunk state (tracking IDs, A/B variants, locale,
single-fire side-effect flags).
No code change, no API change, no test change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… pin partial-failure error code Three post-tag follow-ups to 0.8.0-next.4: - Inline runLegacyInstant's sentence-split pipeline; delete the three module-internal helpers (DEFAULT_SPLIT_REGEX, splitOnceByRegex, splitMessageIntoSentences). Legacy v0.6-compat byte-for-byte preserved. - assertValidDecision now rejects pushPayloads[i].messageId when set to anything other than a non-empty string (catches '', null, 0, objects, etc.). Hook setting messageId = '' was previously preserved on the wire and broke SW IDB keyPath silently. - sendPushesSequentially wraps transport failures with code: 'PUSH_SEND_FAILED' / messageIndex / statusCode (matches runLegacyInstant's existing wrapping). HookError and PayloadTooLargeError keep their own codes to avoid re-classifying shape violations as transport failures. Case 3 of the contract matrix + the agentic-loop partial-failure test now both pin the error code. Tests: 128 → 131, all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lockfile drift from the version field update in 1088430 — the lock now matches package.json:version. Pure regenerated artifact, no dependency graph change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…0-next.5)
非破坏性修复:
- assistant + 非空 tool_calls: content 允许空串 / null / 缺省 (符合 OpenAI Chat
Completions 协议 — 只发工具调用不带 narration 是合法的). 对 tool_calls 数组
做轻量形状校验 ({ id, type:'function', function:{ name, arguments } }).
- tool 消息: content 允许空串 (工具返空结果合法); tool_call_id 强校为必填
字符串 — 这是 OpenAI 协议硬约束, 库之前漏校.
- 其他 role (system / user / 无 tool_calls 的 assistant): 维持原校验.
ChatMessage typedef 同步: role 收窄为字面量联合; content 加入 null;
tool_calls 改为结构化签名; tool_call_id 文档化必填. dist *.d.ts/*.d.cts 由
tsup 从 JSDoc 自动生成.
补 7 条 test (3 accept / 4 reject), 全量 138/138 通过.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a coordinated minor upgrade (v2.4 / v0.8) across the @rei-standard/amsg ecosystem (shared, server, instant, client, sw) to unify the push wire shape under the @rei-standard/amsg-shared AmsgPush discriminated union (using messageKind as the discriminator) and auto-emit ReasoningPush before content bursts. In amsg-instant, the legacy splitPattern request-body fields are removed, shifting splitting responsibility to the hook/caller side via the new pushPayloads array. Additionally, a generic _multipart transport is introduced to handle oversized payloads transparently, and waitUntil lifecycle support is added to protect background processing. The reviewer feedback identifies a critical race condition in the Service Worker's acceptMultipartChunk due to non-atomic IndexedDB operations under concurrent chunk delivery, recommending a promise chain lock. Other suggestions include making the onBusinessPayload promise check more robust by supporting any thenable, and shallow-copying user-returned push objects in sendPushesSequentially to avoid direct mutation.
close #6