Skip to content

Build(deps): Bump yaml from 2.7.1 to 2.8.0#57

Closed
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/yaml-2.8.0
Closed

Build(deps): Bump yaml from 2.7.1 to 2.8.0#57
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/npm_and_yarn/yaml-2.8.0

Conversation

@dependabot
Copy link
Copy Markdown
Contributor

@dependabot dependabot Bot commented on behalf of github May 19, 2025

Bumps yaml from 2.7.1 to 2.8.0.

Release notes

Sourced from yaml's releases.

v2.8.0

  • Add node cache for faster alias resolution (#612)
  • Re-introduce compatibility with Node.js 14.6 (#614)
  • Add --merge option to CLI tool (#611)
  • Improve error for tag resolution error on null value (#616)
  • Allow empty string as plain scalar representation, for failsafe schema (#616)
  • docs: include cli example (#617)
Commits
  • c000eb7 2.8.0
  • 1e85fc8 style: Apply updated lint rules
  • 02f7d5f chore: Refresh lockfile
  • 389ca7c docs: include cli example (#617)
  • 0f29ce6 feat: Add --merge option to CLI tool (#611)
  • e00cab9 fix: Improve error for tag resolution error on null value (#616)
  • 2a841cc fix: Allow empty string as plain scalar representation, for failsafe schema (...
  • 55c5ef4 feat: Add node cache for faster alias resolution (#612)
  • ab17552 Merge pull request #614 from eemeli/engines-compat
  • b27c124 ci: Re-introduce tests for Node.js 14.6 and later
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot merge will merge this PR after your CI passes on it
  • @dependabot squash and merge will squash and merge this PR after your CI passes on it
  • @dependabot cancel merge will cancel a previously requested merge and block automerging
  • @dependabot reopen will reopen this PR if it is closed
  • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [yaml](https://github.com/eemeli/yaml) from 2.7.1 to 2.8.0.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](eemeli/yaml@v2.7.1...v2.8.0)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 2.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github
Copy link
Copy Markdown
Contributor Author

dependabot Bot commented on behalf of github May 19, 2025

The reviewers field in the dependabot.yml file will be removed soon. Please use the code owners file to specify reviewers for Dependabot PRs. For more information, see this blog post.

@dependabot dependabot Bot requested a review from joelteply May 19, 2025 22:15
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels May 19, 2025
@dependabot @github
Copy link
Copy Markdown
Contributor Author

dependabot Bot commented on behalf of github Aug 12, 2025

Superseded by #105.

@dependabot dependabot Bot closed this Aug 12, 2025
@dependabot dependabot Bot deleted the dependabot/npm_and_yarn/yaml-2.8.0 branch August 12, 2025 07:43
joelteply added a commit that referenced this pull request May 30, 2026
…position handler

Per Joel:
> "Yes please do." (re: chat/send next, the dual-write composition
>  stress-test)

chat/send is the chat module's first multi-cross-module-call handler:
chat → data (persist) then chat → airc (publish). The migration forces
the substrate to commit on partial-failure semantics that the
single-call handlers (chat/poll, data/query cursors) never had to face.

# Why this PR pushes the envelope

Two effects across two modules with no kernel-level transaction:

| data | airc | handler returns                                          |
|------|------|----------------------------------------------------------|
| ok   | ok   | `Ok(result with message_id + event_id)`                  |
| ok   | fail | `Ok(result with message_id, event_id=None, warning=...)` |
| fail | —    | `Err(...)` — no airc publish attempted                   |

The (ok, fail) cell is the substrate-shaped kink the design needed
proof of. An airc-only failure is NOT command-level failure: the
message IS in the local store, consumers see it via chat/poll, a
future retry/sync mechanism heals the broadcast. Surfacing this as
`Err` would tell the caller "your write didn't happen" — which is
wrong; half of the write did. The `warning` field is the right shape:
**degraded success**.

# Design decisions this PR locks in

## Ordering: data first, airc second

Local persistence is the ground truth. The reverse order would risk
publishing a message to peers that this node doesn't know about — a
peer reading back that message would find no local record. With
data-first, the worst case is *we have the message but peers don't* —
a degradation, not a divergence.

A test (`send_calls_data_before_airc`) pins the order via a shared
call-log Mutex. If the ordering ever flips, the bad-divergence case
becomes reachable; the test catches it.

## airc-fail returns Ok+warning, not Err

The `warning` field names the failing surface, surfaces the
underlying error (so callers can diagnose), confirms the message
wasn't lost ("stored locally"), and includes the message id (so
callers can correlate logs). Tested:
- `send_with_airc_failure_returns_warning_and_null_event_id`

## data-fail short-circuits — airc NEVER called

A test tracks airc invocations via `AtomicUsize` and asserts ZERO
calls when data failed. Same invariant for the subtle
data-returns-success=false path:
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
- `send_with_data_success_false_propagates_as_err_and_skips_airc`

## Wire contracts pinned by tests, not just docs

Two tests pin the on-the-wire shape chat hands to data + airc. If
either downstream module changes its parse expectations, these tests
catch the drift even though chat doesn't import their typed structs
(coupling lives at the command/wire surface, not at the Rust type
level — the substrate's whole point):

- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  → pins ChatMessageEntity layout (id/roomId/senderId/timestamp/
  content/replyToId/metadata.source/status, ISO-8601 UTC timestamps)
- `send_envelope_matches_airc_publish_wire_shape`
  → pins AircRealtimeEnvelope layout (eventId/roomId/sourceId/
  createdAtMs/delivery, tagged payload variant with
  schema=chat_transcript and inline message data)

# What this PR explicitly does NOT do

- **Does NOT migrate** chat/analyze or chat/export (still fail-loud
  stubs naming issue #57).
- **Does NOT register `ChatModule` at runtime startup.** Same reasoning
  as #1489 — until ALL chat commands are migrated, registration would
  break the remaining stubs at runtime.
- **Does NOT do sender/room name resolution.** Kernel command takes
  pre-resolved UUIDs; resolution stays in TS browser/CLI (or a future
  channel/resolve + user/resolve pair). Same compositional principle
  chat/poll established.
- **Does NOT externalize media.** Text-only for this migration; media
  paths (base64 → blob storage via MediaBlobService) are their own
  kink-finder.
- **Does NOT do vision pre-warming.** Fire-and-forget visual descriptor
  generation is deferred to vision-module migration.
- **Does NOT thread reply-to into threading metadata fully.** The
  `replyToId` field flows through to the stored entity + the airc
  payload, but the richer thread { threadId, replyCount, lastReplyAt }
  shape is deferred until the thread-tracking design is its own scope.
- **Does NOT solve idempotency.** A retried chat/send (network glitch
  on the caller side) currently produces two stored messages —
  matches today's TS behavior. Future PR can add a `client_dedup_id`
  param + TTL'd dedup map; the substrate is ready for it but the
  design is its own scope.

# Substrate kinks this PR surfaced

(For potential future refinement — none blocking, all annotated):

1. **No envelope construction helpers for cross-module calls.** Chat
   hand-rolls `json!({ "envelope": {...} })` for airc. If many
   modules call airc/realtime-publish from Rust, an
   `airc::realtime_publish_envelope(builder...) -> Value` helper in
   the airc-shared module would distill this. Out of scope here; flag
   for if a second consumer appears.
2. **No typed cross-module command call.** Chat calls
   `executor.execute_json("data/create", json!({...}))` with raw JSON
   and parses the response back via `.get("success")`. A typed
   `executor.execute_typed::<DataCreateParams, DataCreateResult>(...)`
   would catch wire-shape drift at compile time. Same kink the
   handle_id_or_legacy refinement (#1491) solved for a different
   surface — flag for potential future refinement after we see if it
   reappears with a second consumer.
3. **No transaction primitive across modules.** Today: chat hand-codes
   the data-first / airc-best-effort ordering inline. If many modules
   need similar dual-write composition, a substrate-level
   `dual_write!(primary => ..., best_effort => ...)` macro could
   centralize the partial-failure pattern (warning construction,
   ordering enforcement, etc.). Flag for if/when a second consumer
   appears.

# Tests (28/28 pass)

Pre-existing chat/poll (17, all unchanged behavior):
- StubDataModule extended to dispatch by command — back-compat
  `query_only` constructor preserves chat/poll's existing tests
  verbatim
- All 17 chat/poll tests still pass through the refactored stub

New chat/send (11):
- `send_happy_path_returns_message_id_and_event_id`
- `send_with_airc_failure_returns_warning_and_null_event_id` ←
  partial-failure cell
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
  ← hard-failure + ordering invariant
- `send_with_data_success_false_propagates_as_err_and_skips_airc` ←
  the subtle data-success-false path
- `send_calls_data_before_airc` ← ordering invariant via call log
- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  ← wire contract to data
- `send_envelope_matches_airc_publish_wire_shape` ← wire contract to
  airc
- `handle_command_routes_chat_send_through_typed_envelope` ← typed
  envelope round-trip end-to-end
- `handle_command_chat_send_accepts_legacy_collaboration_prefix` ←
  back-compat
- `unmigrated_commands_fail_loud_and_name_followup` (updated to
  exclude chat/send now that it's migrated)

ts-rs bindings (2):
- `export_bindings_chatsendparams`
- `export_bindings_chatsendresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts
├── ChatPollResult.ts
├── ChatSendParams.ts    // { roomId, senderId, text, replyToId? }
├── ChatSendResult.ts    // { messageId, eventId?, warning? }
└── index.ts
```

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1489 (ChatModule + chat/poll — the first migration)
- PR #1490 (data/query cursors — single-call HandleRef stress test)
- PR #1491 (substrate refinements distilled from #1490)
- Issue #57 (migration tracker)
- Issue #64 (this migration)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 31, 2026
…ll in Rust)

Per Joel:
> "Chat is gonna be airc man. So that's extracted period. Chat is of
>  course a bonafide command though. Do not cheapen it. So the
>  commands need to be or at least some to start, entirely rust."

The split:
- **Substrate** (delivery, pub/sub, peers, signing) → airc
- **Commands** (chat/send, chat/poll, chat/analyze, chat/export) →
  Continuum kernel-level ServiceModule, this PR

This is the FIRST real module migration from a TS command to a Rust
`ServiceModule`. The chat module exercises every pattern the substrate
floor PRs established:

- `ServiceModule` trait
- `CommandResult` cell shapes (PR #1485)
- `CommandRequest<P>` / `CommandResponse<T>` envelopes (PR #1486)
- Cross-module dispatch via the kernel executor (chat calls
  `data/query` — neither knows the other beyond the command surface)
- Scaffold shape that GeneratorModule (PR #1487) produces
- ts-rs typed wire boundary

# Scope of THIS PR

Only `chat/poll` ships in Rust. The other three commands (`chat/send`,
`chat/analyze`, `chat/export`) are wired into the dispatch table as
fail-loud stubs that name issue #57 as the migration tracker. Their
TS implementations stay live on canary — consumers see no regression.

Why staged: `chat/poll` is the cleanest outlier (pure read, no airc,
no media side-effects) which lets us validate the cross-module call
pattern (chat → data via the kernel executor) without dragging
substrate + media into the first migration. Subsequent commands fold
in real behavior incrementally.

# Module structure

```
src/workers/continuum-core/src/modules/chat/
├── mod.rs          // ChatModule, ServiceModule impl, poll handler
└── types.rs        // ChatPollParams, ChatPollResult (ts-rs exports)
```

`mod.rs` follows the GeneratorModule template exactly — `pub struct
ChatModule`, `impl ServiceModule`, `ModuleConfig` declaring both
`chat/` and `collaboration/chat/` prefixes (legacy back-compat), the
`handle_command` dispatch arms, the typed envelope pattern.

`types.rs` carries `#[derive(TS)]` on both param + result types,
exporting to `shared/generated/chat/`. Wire shape: camelCase, optional
fields elided when absent. `CHAT_MESSAGES_COLLECTION` constant +
`DEFAULT_POLL_LIMIT` constant centralized here.

# Cross-module call pattern

`chat/poll` doesn't open a database connection — it calls `data/query`
via the kernel executor. Chat is blind to which adapter implements
the storage; the data module routes per its own resolution rules.
This is exactly MODULE-ARCHITECTURE.md §5: commands call commands;
modules don't know about each other beyond the command surface.

The chat module accepts an optional executor override at construction
(`with_executor(...)`) — production uses the kernel-global, tests
inject their own. That lets every test in this module spin up a fresh
registry with a `StubDataModule` and exercise the full cross-module
path without trampling the global `OnceLock`.

# Tests (17/17 pass)

types.rs (5):
- `poll_params_defaults_to_all_none`
- `poll_params_round_trip_through_json_with_camel_case`
- `poll_params_accepts_missing_fields`
- `poll_result_omits_after_message_id_when_none`
- `poll_result_includes_after_message_id_when_set`

mod.rs (10):
- `config_advertises_both_command_prefixes`
- `unknown_command_returns_loud_error_naming_supported_commands`
- `unmigrated_commands_fail_loud_and_name_followup` (all 6 stub
  surfaces: chat/send, chat/analyze, chat/export, + collaboration/
  prefixed versions)
- `poll_returns_empty_result_when_data_module_returns_no_messages`
- `poll_without_anchor_queries_data_desc_and_returns_chronological`
- `poll_with_room_id_passes_filter_to_data_module`
- `poll_with_anchor_looks_up_timestamp_then_filters_gt`
- `poll_with_anchor_returns_err_when_anchor_missing`
- `handle_command_routes_chat_poll_through_typed_envelope`
- `handle_command_accepts_legacy_collaboration_prefix`

ts-rs exports (2):
- `export_bindings_chatpollparams`
- `export_bindings_chatpollresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts       // { roomId?, afterMessageId?, limit? }
├── ChatPollResult.ts       // { messages, count, afterMessageId? }
└── index.ts                // barrel
```

The master barrel (`shared/generated/index.ts`) gains
`export * from './chat'`. Other barrel drift (runtime, persona) is
PR #1488's territory — left untouched here so the two PRs don't
fight over the same lines.

# What this PR explicitly does NOT do

- Does NOT migrate `chat/send`, `chat/analyze`, `chat/export`.
  Stubs name issue #57. Each is a future PR.
- Does NOT register `ChatModule` at runtime startup. Adding
  `runtime.register(Arc::new(ChatModule::new()))` in `ipc::start_server`
  would route ALL `chat/*` traffic through this module — including
  the stubbed commands which would then break. Registration happens in
  the same PR that fills in the first real `chat/send` so consumers
  see one atomic change. Today: chat module exists, is tested, but
  the legacy TS path still owns every chat command at runtime.
- Does NOT do room-name resolution. The kernel command takes an
  already-resolved `roomId`; name → id stays in TS browser/CLI
  callsites (or a future `channel/resolve` command). Keeps the
  kernel command compositional with the future channel module.
- Does NOT auto-rebuild the master barrel from outside the chat
  directory — that drift was already on canary and is PR #1488's job.
  This PR only adds the `chat` entry.

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1486 (CommandRequest/Response envelopes — used here)
- PR #1487 (GeneratorModule — chat follows its template)
- Issue #57 (migration tracker — stubs name it)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 31, 2026
…position handler

Per Joel:
> "Yes please do." (re: chat/send next, the dual-write composition
>  stress-test)

chat/send is the chat module's first multi-cross-module-call handler:
chat → data (persist) then chat → airc (publish). The migration forces
the substrate to commit on partial-failure semantics that the
single-call handlers (chat/poll, data/query cursors) never had to face.

# Why this PR pushes the envelope

Two effects across two modules with no kernel-level transaction:

| data | airc | handler returns                                          |
|------|------|----------------------------------------------------------|
| ok   | ok   | `Ok(result with message_id + event_id)`                  |
| ok   | fail | `Ok(result with message_id, event_id=None, warning=...)` |
| fail | —    | `Err(...)` — no airc publish attempted                   |

The (ok, fail) cell is the substrate-shaped kink the design needed
proof of. An airc-only failure is NOT command-level failure: the
message IS in the local store, consumers see it via chat/poll, a
future retry/sync mechanism heals the broadcast. Surfacing this as
`Err` would tell the caller "your write didn't happen" — which is
wrong; half of the write did. The `warning` field is the right shape:
**degraded success**.

# Design decisions this PR locks in

## Ordering: data first, airc second

Local persistence is the ground truth. The reverse order would risk
publishing a message to peers that this node doesn't know about — a
peer reading back that message would find no local record. With
data-first, the worst case is *we have the message but peers don't* —
a degradation, not a divergence.

A test (`send_calls_data_before_airc`) pins the order via a shared
call-log Mutex. If the ordering ever flips, the bad-divergence case
becomes reachable; the test catches it.

## airc-fail returns Ok+warning, not Err

The `warning` field names the failing surface, surfaces the
underlying error (so callers can diagnose), confirms the message
wasn't lost ("stored locally"), and includes the message id (so
callers can correlate logs). Tested:
- `send_with_airc_failure_returns_warning_and_null_event_id`

## data-fail short-circuits — airc NEVER called

A test tracks airc invocations via `AtomicUsize` and asserts ZERO
calls when data failed. Same invariant for the subtle
data-returns-success=false path:
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
- `send_with_data_success_false_propagates_as_err_and_skips_airc`

## Wire contracts pinned by tests, not just docs

Two tests pin the on-the-wire shape chat hands to data + airc. If
either downstream module changes its parse expectations, these tests
catch the drift even though chat doesn't import their typed structs
(coupling lives at the command/wire surface, not at the Rust type
level — the substrate's whole point):

- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  → pins ChatMessageEntity layout (id/roomId/senderId/timestamp/
  content/replyToId/metadata.source/status, ISO-8601 UTC timestamps)
- `send_envelope_matches_airc_publish_wire_shape`
  → pins AircRealtimeEnvelope layout (eventId/roomId/sourceId/
  createdAtMs/delivery, tagged payload variant with
  schema=chat_transcript and inline message data)

# What this PR explicitly does NOT do

- **Does NOT migrate** chat/analyze or chat/export (still fail-loud
  stubs naming issue #57).
- **Does NOT register `ChatModule` at runtime startup.** Same reasoning
  as #1489 — until ALL chat commands are migrated, registration would
  break the remaining stubs at runtime.
- **Does NOT do sender/room name resolution.** Kernel command takes
  pre-resolved UUIDs; resolution stays in TS browser/CLI (or a future
  channel/resolve + user/resolve pair). Same compositional principle
  chat/poll established.
- **Does NOT externalize media.** Text-only for this migration; media
  paths (base64 → blob storage via MediaBlobService) are their own
  kink-finder.
- **Does NOT do vision pre-warming.** Fire-and-forget visual descriptor
  generation is deferred to vision-module migration.
- **Does NOT thread reply-to into threading metadata fully.** The
  `replyToId` field flows through to the stored entity + the airc
  payload, but the richer thread { threadId, replyCount, lastReplyAt }
  shape is deferred until the thread-tracking design is its own scope.
- **Does NOT solve idempotency.** A retried chat/send (network glitch
  on the caller side) currently produces two stored messages —
  matches today's TS behavior. Future PR can add a `client_dedup_id`
  param + TTL'd dedup map; the substrate is ready for it but the
  design is its own scope.

# Substrate kinks this PR surfaced

(For potential future refinement — none blocking, all annotated):

1. **No envelope construction helpers for cross-module calls.** Chat
   hand-rolls `json!({ "envelope": {...} })` for airc. If many
   modules call airc/realtime-publish from Rust, an
   `airc::realtime_publish_envelope(builder...) -> Value` helper in
   the airc-shared module would distill this. Out of scope here; flag
   for if a second consumer appears.
2. **No typed cross-module command call.** Chat calls
   `executor.execute_json("data/create", json!({...}))` with raw JSON
   and parses the response back via `.get("success")`. A typed
   `executor.execute_typed::<DataCreateParams, DataCreateResult>(...)`
   would catch wire-shape drift at compile time. Same kink the
   handle_id_or_legacy refinement (#1491) solved for a different
   surface — flag for potential future refinement after we see if it
   reappears with a second consumer.
3. **No transaction primitive across modules.** Today: chat hand-codes
   the data-first / airc-best-effort ordering inline. If many modules
   need similar dual-write composition, a substrate-level
   `dual_write!(primary => ..., best_effort => ...)` macro could
   centralize the partial-failure pattern (warning construction,
   ordering enforcement, etc.). Flag for if/when a second consumer
   appears.

# Tests (28/28 pass)

Pre-existing chat/poll (17, all unchanged behavior):
- StubDataModule extended to dispatch by command — back-compat
  `query_only` constructor preserves chat/poll's existing tests
  verbatim
- All 17 chat/poll tests still pass through the refactored stub

New chat/send (11):
- `send_happy_path_returns_message_id_and_event_id`
- `send_with_airc_failure_returns_warning_and_null_event_id` ←
  partial-failure cell
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
  ← hard-failure + ordering invariant
- `send_with_data_success_false_propagates_as_err_and_skips_airc` ←
  the subtle data-success-false path
- `send_calls_data_before_airc` ← ordering invariant via call log
- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  ← wire contract to data
- `send_envelope_matches_airc_publish_wire_shape` ← wire contract to
  airc
- `handle_command_routes_chat_send_through_typed_envelope` ← typed
  envelope round-trip end-to-end
- `handle_command_chat_send_accepts_legacy_collaboration_prefix` ←
  back-compat
- `unmigrated_commands_fail_loud_and_name_followup` (updated to
  exclude chat/send now that it's migrated)

ts-rs bindings (2):
- `export_bindings_chatsendparams`
- `export_bindings_chatsendresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts
├── ChatPollResult.ts
├── ChatSendParams.ts    // { roomId, senderId, text, replyToId? }
├── ChatSendResult.ts    // { messageId, eventId?, warning? }
└── index.ts
```

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1489 (ChatModule + chat/poll — the first migration)
- PR #1490 (data/query cursors — single-call HandleRef stress test)
- PR #1491 (substrate refinements distilled from #1490)
- Issue #57 (migration tracker)
- Issue #64 (this migration)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 31, 2026
…ll in Rust)

Per Joel:
> "Chat is gonna be airc man. So that's extracted period. Chat is of
>  course a bonafide command though. Do not cheapen it. So the
>  commands need to be or at least some to start, entirely rust."

The split:
- **Substrate** (delivery, pub/sub, peers, signing) → airc
- **Commands** (chat/send, chat/poll, chat/analyze, chat/export) →
  Continuum kernel-level ServiceModule, this PR

This is the FIRST real module migration from a TS command to a Rust
`ServiceModule`. The chat module exercises every pattern the substrate
floor PRs established:

- `ServiceModule` trait
- `CommandResult` cell shapes (PR #1485)
- `CommandRequest<P>` / `CommandResponse<T>` envelopes (PR #1486)
- Cross-module dispatch via the kernel executor (chat calls
  `data/query` — neither knows the other beyond the command surface)
- Scaffold shape that GeneratorModule (PR #1487) produces
- ts-rs typed wire boundary

# Scope of THIS PR

Only `chat/poll` ships in Rust. The other three commands (`chat/send`,
`chat/analyze`, `chat/export`) are wired into the dispatch table as
fail-loud stubs that name issue #57 as the migration tracker. Their
TS implementations stay live on canary — consumers see no regression.

Why staged: `chat/poll` is the cleanest outlier (pure read, no airc,
no media side-effects) which lets us validate the cross-module call
pattern (chat → data via the kernel executor) without dragging
substrate + media into the first migration. Subsequent commands fold
in real behavior incrementally.

# Module structure

```
src/workers/continuum-core/src/modules/chat/
├── mod.rs          // ChatModule, ServiceModule impl, poll handler
└── types.rs        // ChatPollParams, ChatPollResult (ts-rs exports)
```

`mod.rs` follows the GeneratorModule template exactly — `pub struct
ChatModule`, `impl ServiceModule`, `ModuleConfig` declaring both
`chat/` and `collaboration/chat/` prefixes (legacy back-compat), the
`handle_command` dispatch arms, the typed envelope pattern.

`types.rs` carries `#[derive(TS)]` on both param + result types,
exporting to `shared/generated/chat/`. Wire shape: camelCase, optional
fields elided when absent. `CHAT_MESSAGES_COLLECTION` constant +
`DEFAULT_POLL_LIMIT` constant centralized here.

# Cross-module call pattern

`chat/poll` doesn't open a database connection — it calls `data/query`
via the kernel executor. Chat is blind to which adapter implements
the storage; the data module routes per its own resolution rules.
This is exactly MODULE-ARCHITECTURE.md §5: commands call commands;
modules don't know about each other beyond the command surface.

The chat module accepts an optional executor override at construction
(`with_executor(...)`) — production uses the kernel-global, tests
inject their own. That lets every test in this module spin up a fresh
registry with a `StubDataModule` and exercise the full cross-module
path without trampling the global `OnceLock`.

# Tests (17/17 pass)

types.rs (5):
- `poll_params_defaults_to_all_none`
- `poll_params_round_trip_through_json_with_camel_case`
- `poll_params_accepts_missing_fields`
- `poll_result_omits_after_message_id_when_none`
- `poll_result_includes_after_message_id_when_set`

mod.rs (10):
- `config_advertises_both_command_prefixes`
- `unknown_command_returns_loud_error_naming_supported_commands`
- `unmigrated_commands_fail_loud_and_name_followup` (all 6 stub
  surfaces: chat/send, chat/analyze, chat/export, + collaboration/
  prefixed versions)
- `poll_returns_empty_result_when_data_module_returns_no_messages`
- `poll_without_anchor_queries_data_desc_and_returns_chronological`
- `poll_with_room_id_passes_filter_to_data_module`
- `poll_with_anchor_looks_up_timestamp_then_filters_gt`
- `poll_with_anchor_returns_err_when_anchor_missing`
- `handle_command_routes_chat_poll_through_typed_envelope`
- `handle_command_accepts_legacy_collaboration_prefix`

ts-rs exports (2):
- `export_bindings_chatpollparams`
- `export_bindings_chatpollresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts       // { roomId?, afterMessageId?, limit? }
├── ChatPollResult.ts       // { messages, count, afterMessageId? }
└── index.ts                // barrel
```

The master barrel (`shared/generated/index.ts`) gains
`export * from './chat'`. Other barrel drift (runtime, persona) is
PR #1488's territory — left untouched here so the two PRs don't
fight over the same lines.

# What this PR explicitly does NOT do

- Does NOT migrate `chat/send`, `chat/analyze`, `chat/export`.
  Stubs name issue #57. Each is a future PR.
- Does NOT register `ChatModule` at runtime startup. Adding
  `runtime.register(Arc::new(ChatModule::new()))` in `ipc::start_server`
  would route ALL `chat/*` traffic through this module — including
  the stubbed commands which would then break. Registration happens in
  the same PR that fills in the first real `chat/send` so consumers
  see one atomic change. Today: chat module exists, is tested, but
  the legacy TS path still owns every chat command at runtime.
- Does NOT do room-name resolution. The kernel command takes an
  already-resolved `roomId`; name → id stays in TS browser/CLI
  callsites (or a future `channel/resolve` command). Keeps the
  kernel command compositional with the future channel module.
- Does NOT auto-rebuild the master barrel from outside the chat
  directory — that drift was already on canary and is PR #1488's job.
  This PR only adds the `chat` entry.

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1486 (CommandRequest/Response envelopes — used here)
- PR #1487 (GeneratorModule — chat follows its template)
- Issue #57 (migration tracker — stubs name it)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 31, 2026
…position handler

Per Joel:
> "Yes please do." (re: chat/send next, the dual-write composition
>  stress-test)

chat/send is the chat module's first multi-cross-module-call handler:
chat → data (persist) then chat → airc (publish). The migration forces
the substrate to commit on partial-failure semantics that the
single-call handlers (chat/poll, data/query cursors) never had to face.

# Why this PR pushes the envelope

Two effects across two modules with no kernel-level transaction:

| data | airc | handler returns                                          |
|------|------|----------------------------------------------------------|
| ok   | ok   | `Ok(result with message_id + event_id)`                  |
| ok   | fail | `Ok(result with message_id, event_id=None, warning=...)` |
| fail | —    | `Err(...)` — no airc publish attempted                   |

The (ok, fail) cell is the substrate-shaped kink the design needed
proof of. An airc-only failure is NOT command-level failure: the
message IS in the local store, consumers see it via chat/poll, a
future retry/sync mechanism heals the broadcast. Surfacing this as
`Err` would tell the caller "your write didn't happen" — which is
wrong; half of the write did. The `warning` field is the right shape:
**degraded success**.

# Design decisions this PR locks in

## Ordering: data first, airc second

Local persistence is the ground truth. The reverse order would risk
publishing a message to peers that this node doesn't know about — a
peer reading back that message would find no local record. With
data-first, the worst case is *we have the message but peers don't* —
a degradation, not a divergence.

A test (`send_calls_data_before_airc`) pins the order via a shared
call-log Mutex. If the ordering ever flips, the bad-divergence case
becomes reachable; the test catches it.

## airc-fail returns Ok+warning, not Err

The `warning` field names the failing surface, surfaces the
underlying error (so callers can diagnose), confirms the message
wasn't lost ("stored locally"), and includes the message id (so
callers can correlate logs). Tested:
- `send_with_airc_failure_returns_warning_and_null_event_id`

## data-fail short-circuits — airc NEVER called

A test tracks airc invocations via `AtomicUsize` and asserts ZERO
calls when data failed. Same invariant for the subtle
data-returns-success=false path:
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
- `send_with_data_success_false_propagates_as_err_and_skips_airc`

## Wire contracts pinned by tests, not just docs

Two tests pin the on-the-wire shape chat hands to data + airc. If
either downstream module changes its parse expectations, these tests
catch the drift even though chat doesn't import their typed structs
(coupling lives at the command/wire surface, not at the Rust type
level — the substrate's whole point):

- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  → pins ChatMessageEntity layout (id/roomId/senderId/timestamp/
  content/replyToId/metadata.source/status, ISO-8601 UTC timestamps)
- `send_envelope_matches_airc_publish_wire_shape`
  → pins AircRealtimeEnvelope layout (eventId/roomId/sourceId/
  createdAtMs/delivery, tagged payload variant with
  schema=chat_transcript and inline message data)

# What this PR explicitly does NOT do

- **Does NOT migrate** chat/analyze or chat/export (still fail-loud
  stubs naming issue #57).
- **Does NOT register `ChatModule` at runtime startup.** Same reasoning
  as #1489 — until ALL chat commands are migrated, registration would
  break the remaining stubs at runtime.
- **Does NOT do sender/room name resolution.** Kernel command takes
  pre-resolved UUIDs; resolution stays in TS browser/CLI (or a future
  channel/resolve + user/resolve pair). Same compositional principle
  chat/poll established.
- **Does NOT externalize media.** Text-only for this migration; media
  paths (base64 → blob storage via MediaBlobService) are their own
  kink-finder.
- **Does NOT do vision pre-warming.** Fire-and-forget visual descriptor
  generation is deferred to vision-module migration.
- **Does NOT thread reply-to into threading metadata fully.** The
  `replyToId` field flows through to the stored entity + the airc
  payload, but the richer thread { threadId, replyCount, lastReplyAt }
  shape is deferred until the thread-tracking design is its own scope.
- **Does NOT solve idempotency.** A retried chat/send (network glitch
  on the caller side) currently produces two stored messages —
  matches today's TS behavior. Future PR can add a `client_dedup_id`
  param + TTL'd dedup map; the substrate is ready for it but the
  design is its own scope.

# Substrate kinks this PR surfaced

(For potential future refinement — none blocking, all annotated):

1. **No envelope construction helpers for cross-module calls.** Chat
   hand-rolls `json!({ "envelope": {...} })` for airc. If many
   modules call airc/realtime-publish from Rust, an
   `airc::realtime_publish_envelope(builder...) -> Value` helper in
   the airc-shared module would distill this. Out of scope here; flag
   for if a second consumer appears.
2. **No typed cross-module command call.** Chat calls
   `executor.execute_json("data/create", json!({...}))` with raw JSON
   and parses the response back via `.get("success")`. A typed
   `executor.execute_typed::<DataCreateParams, DataCreateResult>(...)`
   would catch wire-shape drift at compile time. Same kink the
   handle_id_or_legacy refinement (#1491) solved for a different
   surface — flag for potential future refinement after we see if it
   reappears with a second consumer.
3. **No transaction primitive across modules.** Today: chat hand-codes
   the data-first / airc-best-effort ordering inline. If many modules
   need similar dual-write composition, a substrate-level
   `dual_write!(primary => ..., best_effort => ...)` macro could
   centralize the partial-failure pattern (warning construction,
   ordering enforcement, etc.). Flag for if/when a second consumer
   appears.

# Tests (28/28 pass)

Pre-existing chat/poll (17, all unchanged behavior):
- StubDataModule extended to dispatch by command — back-compat
  `query_only` constructor preserves chat/poll's existing tests
  verbatim
- All 17 chat/poll tests still pass through the refactored stub

New chat/send (11):
- `send_happy_path_returns_message_id_and_event_id`
- `send_with_airc_failure_returns_warning_and_null_event_id` ←
  partial-failure cell
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
  ← hard-failure + ordering invariant
- `send_with_data_success_false_propagates_as_err_and_skips_airc` ←
  the subtle data-success-false path
- `send_calls_data_before_airc` ← ordering invariant via call log
- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  ← wire contract to data
- `send_envelope_matches_airc_publish_wire_shape` ← wire contract to
  airc
- `handle_command_routes_chat_send_through_typed_envelope` ← typed
  envelope round-trip end-to-end
- `handle_command_chat_send_accepts_legacy_collaboration_prefix` ←
  back-compat
- `unmigrated_commands_fail_loud_and_name_followup` (updated to
  exclude chat/send now that it's migrated)

ts-rs bindings (2):
- `export_bindings_chatsendparams`
- `export_bindings_chatsendresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts
├── ChatPollResult.ts
├── ChatSendParams.ts    // { roomId, senderId, text, replyToId? }
├── ChatSendResult.ts    // { messageId, eventId?, warning? }
└── index.ts
```

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1489 (ChatModule + chat/poll — the first migration)
- PR #1490 (data/query cursors — single-call HandleRef stress test)
- PR #1491 (substrate refinements distilled from #1490)
- Issue #57 (migration tracker)
- Issue #64 (this migration)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joelteply added a commit that referenced this pull request May 31, 2026
…t (first dual-write composition) (#1489)

* feat(modules): ChatModule — first proof-of-pattern migration (chat/poll in Rust)

Per Joel:
> "Chat is gonna be airc man. So that's extracted period. Chat is of
>  course a bonafide command though. Do not cheapen it. So the
>  commands need to be or at least some to start, entirely rust."

The split:
- **Substrate** (delivery, pub/sub, peers, signing) → airc
- **Commands** (chat/send, chat/poll, chat/analyze, chat/export) →
  Continuum kernel-level ServiceModule, this PR

This is the FIRST real module migration from a TS command to a Rust
`ServiceModule`. The chat module exercises every pattern the substrate
floor PRs established:

- `ServiceModule` trait
- `CommandResult` cell shapes (PR #1485)
- `CommandRequest<P>` / `CommandResponse<T>` envelopes (PR #1486)
- Cross-module dispatch via the kernel executor (chat calls
  `data/query` — neither knows the other beyond the command surface)
- Scaffold shape that GeneratorModule (PR #1487) produces
- ts-rs typed wire boundary

# Scope of THIS PR

Only `chat/poll` ships in Rust. The other three commands (`chat/send`,
`chat/analyze`, `chat/export`) are wired into the dispatch table as
fail-loud stubs that name issue #57 as the migration tracker. Their
TS implementations stay live on canary — consumers see no regression.

Why staged: `chat/poll` is the cleanest outlier (pure read, no airc,
no media side-effects) which lets us validate the cross-module call
pattern (chat → data via the kernel executor) without dragging
substrate + media into the first migration. Subsequent commands fold
in real behavior incrementally.

# Module structure

```
src/workers/continuum-core/src/modules/chat/
├── mod.rs          // ChatModule, ServiceModule impl, poll handler
└── types.rs        // ChatPollParams, ChatPollResult (ts-rs exports)
```

`mod.rs` follows the GeneratorModule template exactly — `pub struct
ChatModule`, `impl ServiceModule`, `ModuleConfig` declaring both
`chat/` and `collaboration/chat/` prefixes (legacy back-compat), the
`handle_command` dispatch arms, the typed envelope pattern.

`types.rs` carries `#[derive(TS)]` on both param + result types,
exporting to `shared/generated/chat/`. Wire shape: camelCase, optional
fields elided when absent. `CHAT_MESSAGES_COLLECTION` constant +
`DEFAULT_POLL_LIMIT` constant centralized here.

# Cross-module call pattern

`chat/poll` doesn't open a database connection — it calls `data/query`
via the kernel executor. Chat is blind to which adapter implements
the storage; the data module routes per its own resolution rules.
This is exactly MODULE-ARCHITECTURE.md §5: commands call commands;
modules don't know about each other beyond the command surface.

The chat module accepts an optional executor override at construction
(`with_executor(...)`) — production uses the kernel-global, tests
inject their own. That lets every test in this module spin up a fresh
registry with a `StubDataModule` and exercise the full cross-module
path without trampling the global `OnceLock`.

# Tests (17/17 pass)

types.rs (5):
- `poll_params_defaults_to_all_none`
- `poll_params_round_trip_through_json_with_camel_case`
- `poll_params_accepts_missing_fields`
- `poll_result_omits_after_message_id_when_none`
- `poll_result_includes_after_message_id_when_set`

mod.rs (10):
- `config_advertises_both_command_prefixes`
- `unknown_command_returns_loud_error_naming_supported_commands`
- `unmigrated_commands_fail_loud_and_name_followup` (all 6 stub
  surfaces: chat/send, chat/analyze, chat/export, + collaboration/
  prefixed versions)
- `poll_returns_empty_result_when_data_module_returns_no_messages`
- `poll_without_anchor_queries_data_desc_and_returns_chronological`
- `poll_with_room_id_passes_filter_to_data_module`
- `poll_with_anchor_looks_up_timestamp_then_filters_gt`
- `poll_with_anchor_returns_err_when_anchor_missing`
- `handle_command_routes_chat_poll_through_typed_envelope`
- `handle_command_accepts_legacy_collaboration_prefix`

ts-rs exports (2):
- `export_bindings_chatpollparams`
- `export_bindings_chatpollresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts       // { roomId?, afterMessageId?, limit? }
├── ChatPollResult.ts       // { messages, count, afterMessageId? }
└── index.ts                // barrel
```

The master barrel (`shared/generated/index.ts`) gains
`export * from './chat'`. Other barrel drift (runtime, persona) is
PR #1488's territory — left untouched here so the two PRs don't
fight over the same lines.

# What this PR explicitly does NOT do

- Does NOT migrate `chat/send`, `chat/analyze`, `chat/export`.
  Stubs name issue #57. Each is a future PR.
- Does NOT register `ChatModule` at runtime startup. Adding
  `runtime.register(Arc::new(ChatModule::new()))` in `ipc::start_server`
  would route ALL `chat/*` traffic through this module — including
  the stubbed commands which would then break. Registration happens in
  the same PR that fills in the first real `chat/send` so consumers
  see one atomic change. Today: chat module exists, is tested, but
  the legacy TS path still owns every chat command at runtime.
- Does NOT do room-name resolution. The kernel command takes an
  already-resolved `roomId`; name → id stays in TS browser/CLI
  callsites (or a future `channel/resolve` command). Keeps the
  kernel command compositional with the future channel module.
- Does NOT auto-rebuild the master barrel from outside the chat
  directory — that drift was already on canary and is PR #1488's job.
  This PR only adds the `chat` entry.

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1486 (CommandRequest/Response envelopes — used here)
- PR #1487 (GeneratorModule — chat follows its template)
- Issue #57 (migration tracker — stubs name it)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(modules/chat): chat/send migrates to Rust — first dual-write composition handler

Per Joel:
> "Yes please do." (re: chat/send next, the dual-write composition
>  stress-test)

chat/send is the chat module's first multi-cross-module-call handler:
chat → data (persist) then chat → airc (publish). The migration forces
the substrate to commit on partial-failure semantics that the
single-call handlers (chat/poll, data/query cursors) never had to face.

# Why this PR pushes the envelope

Two effects across two modules with no kernel-level transaction:

| data | airc | handler returns                                          |
|------|------|----------------------------------------------------------|
| ok   | ok   | `Ok(result with message_id + event_id)`                  |
| ok   | fail | `Ok(result with message_id, event_id=None, warning=...)` |
| fail | —    | `Err(...)` — no airc publish attempted                   |

The (ok, fail) cell is the substrate-shaped kink the design needed
proof of. An airc-only failure is NOT command-level failure: the
message IS in the local store, consumers see it via chat/poll, a
future retry/sync mechanism heals the broadcast. Surfacing this as
`Err` would tell the caller "your write didn't happen" — which is
wrong; half of the write did. The `warning` field is the right shape:
**degraded success**.

# Design decisions this PR locks in

## Ordering: data first, airc second

Local persistence is the ground truth. The reverse order would risk
publishing a message to peers that this node doesn't know about — a
peer reading back that message would find no local record. With
data-first, the worst case is *we have the message but peers don't* —
a degradation, not a divergence.

A test (`send_calls_data_before_airc`) pins the order via a shared
call-log Mutex. If the ordering ever flips, the bad-divergence case
becomes reachable; the test catches it.

## airc-fail returns Ok+warning, not Err

The `warning` field names the failing surface, surfaces the
underlying error (so callers can diagnose), confirms the message
wasn't lost ("stored locally"), and includes the message id (so
callers can correlate logs). Tested:
- `send_with_airc_failure_returns_warning_and_null_event_id`

## data-fail short-circuits — airc NEVER called

A test tracks airc invocations via `AtomicUsize` and asserts ZERO
calls when data failed. Same invariant for the subtle
data-returns-success=false path:
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
- `send_with_data_success_false_propagates_as_err_and_skips_airc`

## Wire contracts pinned by tests, not just docs

Two tests pin the on-the-wire shape chat hands to data + airc. If
either downstream module changes its parse expectations, these tests
catch the drift even though chat doesn't import their typed structs
(coupling lives at the command/wire surface, not at the Rust type
level — the substrate's whole point):

- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  → pins ChatMessageEntity layout (id/roomId/senderId/timestamp/
  content/replyToId/metadata.source/status, ISO-8601 UTC timestamps)
- `send_envelope_matches_airc_publish_wire_shape`
  → pins AircRealtimeEnvelope layout (eventId/roomId/sourceId/
  createdAtMs/delivery, tagged payload variant with
  schema=chat_transcript and inline message data)

# What this PR explicitly does NOT do

- **Does NOT migrate** chat/analyze or chat/export (still fail-loud
  stubs naming issue #57).
- **Does NOT register `ChatModule` at runtime startup.** Same reasoning
  as #1489 — until ALL chat commands are migrated, registration would
  break the remaining stubs at runtime.
- **Does NOT do sender/room name resolution.** Kernel command takes
  pre-resolved UUIDs; resolution stays in TS browser/CLI (or a future
  channel/resolve + user/resolve pair). Same compositional principle
  chat/poll established.
- **Does NOT externalize media.** Text-only for this migration; media
  paths (base64 → blob storage via MediaBlobService) are their own
  kink-finder.
- **Does NOT do vision pre-warming.** Fire-and-forget visual descriptor
  generation is deferred to vision-module migration.
- **Does NOT thread reply-to into threading metadata fully.** The
  `replyToId` field flows through to the stored entity + the airc
  payload, but the richer thread { threadId, replyCount, lastReplyAt }
  shape is deferred until the thread-tracking design is its own scope.
- **Does NOT solve idempotency.** A retried chat/send (network glitch
  on the caller side) currently produces two stored messages —
  matches today's TS behavior. Future PR can add a `client_dedup_id`
  param + TTL'd dedup map; the substrate is ready for it but the
  design is its own scope.

# Substrate kinks this PR surfaced

(For potential future refinement — none blocking, all annotated):

1. **No envelope construction helpers for cross-module calls.** Chat
   hand-rolls `json!({ "envelope": {...} })` for airc. If many
   modules call airc/realtime-publish from Rust, an
   `airc::realtime_publish_envelope(builder...) -> Value` helper in
   the airc-shared module would distill this. Out of scope here; flag
   for if a second consumer appears.
2. **No typed cross-module command call.** Chat calls
   `executor.execute_json("data/create", json!({...}))` with raw JSON
   and parses the response back via `.get("success")`. A typed
   `executor.execute_typed::<DataCreateParams, DataCreateResult>(...)`
   would catch wire-shape drift at compile time. Same kink the
   handle_id_or_legacy refinement (#1491) solved for a different
   surface — flag for potential future refinement after we see if it
   reappears with a second consumer.
3. **No transaction primitive across modules.** Today: chat hand-codes
   the data-first / airc-best-effort ordering inline. If many modules
   need similar dual-write composition, a substrate-level
   `dual_write!(primary => ..., best_effort => ...)` macro could
   centralize the partial-failure pattern (warning construction,
   ordering enforcement, etc.). Flag for if/when a second consumer
   appears.

# Tests (28/28 pass)

Pre-existing chat/poll (17, all unchanged behavior):
- StubDataModule extended to dispatch by command — back-compat
  `query_only` constructor preserves chat/poll's existing tests
  verbatim
- All 17 chat/poll tests still pass through the refactored stub

New chat/send (11):
- `send_happy_path_returns_message_id_and_event_id`
- `send_with_airc_failure_returns_warning_and_null_event_id` ←
  partial-failure cell
- `send_with_data_executor_failure_propagates_as_err_and_skips_airc`
  ← hard-failure + ordering invariant
- `send_with_data_success_false_propagates_as_err_and_skips_airc` ←
  the subtle data-success-false path
- `send_calls_data_before_airc` ← ordering invariant via call log
- `send_writes_chat_messages_collection_with_canonical_entity_shape`
  ← wire contract to data
- `send_envelope_matches_airc_publish_wire_shape` ← wire contract to
  airc
- `handle_command_routes_chat_send_through_typed_envelope` ← typed
  envelope round-trip end-to-end
- `handle_command_chat_send_accepts_legacy_collaboration_prefix` ←
  back-compat
- `unmigrated_commands_fail_loud_and_name_followup` (updated to
  exclude chat/send now that it's migrated)

ts-rs bindings (2):
- `export_bindings_chatsendparams`
- `export_bindings_chatsendresult`

# Wire output

```
shared/generated/chat/
├── ChatPollParams.ts
├── ChatPollResult.ts
├── ChatSendParams.ts    // { roomId, senderId, text, replyToId? }
├── ChatSendResult.ts    // { messageId, eventId?, warning? }
└── index.ts
```

# References

- [docs/architecture/MODULE-ARCHITECTURE.md](docs/architecture/MODULE-ARCHITECTURE.md)
  §5 (composition: commands call commands)
- PR #1489 (ChatModule + chat/poll — the first migration)
- PR #1490 (data/query cursors — single-call HandleRef stress test)
- PR #1491 (substrate refinements distilled from #1490)
- Issue #57 (migration tracker)
- Issue #64 (this migration)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(modules/chat): concurrency stress tests — multi-persona invariants pinned

Per Joel 2026-05-30: "Each persona exists in its own threads."

The kernel registers ONE ChatModule instance; every persona's thread
invokes its `&self` methods concurrently against the same executor.
The substrate is designed to be safe under that load — but until
now no test PROVED it. Single-threaded `#[tokio::test]` runs serialize
even genuinely racy code and would pass a substrate with a data race.

This commit adds 4 concurrency stress tests pinning the invariants
the dual-write / single-call composition designs depend on. Every
test uses `flavor = "multi_thread", worker_threads = 4` so tasks
actually preempt each other on distinct OS threads rather than
cooperatively interleaving on one.

# What's pinned

1. **`send_under_concurrent_load_stores_all_messages_with_distinct_ids`**
   50 concurrent personas all call `chat/send` through the same
   ChatModule. Asserts: every send completes, every send writes
   exactly once, every returned `message_id` is distinct (no UUID
   collision, no shared mutable state holding the id), and the SET
   of stored ids equals the SET of returned ids (no lost writes, no
   phantom writes).

2. **`send_preserves_per_call_ordering_under_concurrent_load`**
   25 concurrent sends interleave globally — but per-call
   `data/create` MUST still precede per-call `airc/realtime-publish`.
   The dual-write design's bad-divergence safety net (peers don't
   see a message the node hasn't stored) depends on this invariant
   holding under load. Tagging each observation with its
   `message_id` lets the test reconstruct per-call timelines from
   the interleaved global log.

3. **`send_isolates_mixed_outcomes_under_concurrent_load`**
   30 concurrent sends with half airc-failing (text flag tells the
   stub to fail). Each call's `warning` must reference THIS call's
   `message_id`, not a concurrent sibling's. Cross-contamination
   between concurrent results would mean shared mutable state in the
   handler — this catches it.

4. **`poll_isolates_results_under_concurrent_load`**
   30 concurrent `chat/poll` calls each polling a DIFFERENT room. The
   stub echoes the requested `roomId` in the synthetic result; the
   test asserts every task receives ITS OWN room's result. Catches
   result-swap bugs that would never appear single-threaded.

# Why this discipline matters

Concurrency tests aren't exercising rare paths — they're the
production scenario. A test suite full of single-threaded
`#[tokio::test]`s can sign off on a substrate that silently
miscomputes under multi-persona load. Pinning the invariants here
means the next refactor (e.g., adding a `dual_write!` macro or
typed cross-module command call) is held to the same bar.

The pattern goes into every future module that consumes the
kernel: when you add a new handler that touches shared state, add a
matching concurrency stress test.

# Tests (23/23 pass — 19 pre-existing + 4 new concurrency)

All previously-passing tests still pass. The new ones use real
multi-threaded tokio runtime + `Arc<Mutex>` + atomic tracking to
observe interleavings the substrate must handle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants