Skip to content

fix(chat): preserve multi-mode arrays through ChatOrchestrator - #2237

Merged
chubes4 merged 1 commit into
mainfrom
fix/chat-orchestrator-modes-array
May 25, 2026
Merged

fix(chat): preserve multi-mode arrays through ChatOrchestrator#2237
chubes4 merged 1 commit into
mainfrom
fix/chat-orchestrator-modes-array

Conversation

@chubes4

@chubes4 chubes4 commented May 25, 2026

Copy link
Copy Markdown
Member

Closes the upstream half of chubes4/cluckin-chuck#12.

Problem

Multi-mode chat requests get silently collapsed to a single junk mode whenever they flow through ChatOrchestrator. For a request like agent_modes = ['cluckin-chuck', 'chat']:

  1. processChat line 71 normalizes the array correctly: $modes = ['cluckin-chuck', 'chat']
  2. Line 72 joins to comma-string for logging: $mode = 'cluckin-chuck,chat'
  3. Line 212 passes only the joined string ('mode' => $mode) to executeConversationTurn
  4. executeConversationTurn line 715: normalizeModes($options['mode']) runs sanitize_key('cluckin-chuck,chat')
  5. sanitize_key strips the comma → 'cluckin-chuckchat' (single junk mode)
  6. The original cluckin-chuck mode no longer exists in the resolved list

processContinue line 388 has the same shape, and createSession line 641 sanitize_keys the multi-mode string when writing to session.mode — so the broken state persists across session resumption.

Why this matters

Mode-restricted tool allowlist filters (host plugins that hook datamachine_resolved_tools to lock down a public chat surface to a specific tool subset) silently no-op when their mode slug doesn't appear in the active mode list. That defeats the entire point of the multi-mode resolution pattern: a downstream that intentionally composes a custom mode + chat as the execution surface ends up with the unrestricted chat surface.

Concrete downstream example

Cluckin' Chuck uses this exact pattern for a public chat agent. The cluckin-chuck mode carries the wing-business directive + a tool allowlist filter that strips everything except 13 wing tools. chat is required as the execution surface or DM treats the call as pipeline mode and the filter never runs.

With this bug present:

  • frontend chat injects client_context.agent_modes = ['cluckin-chuck', 'chat']
  • DM resolves it to ['cluckin-chuckchat']
  • the allowlist filter exits early because in_array('cluckin-chuck', $modes, true) is false
  • the agent has visibility into the full ~128 admin chat tools (data-machine-socials publishers, GitHub, Reddit, pipeline management, etc.)
  • production transcript shows the agent successfully dispatching read_instagram from a public chat session

Fix

Three small changes inside ChatOrchestrator:

  1. processChat (line 212): also pass 'modes' => $modes (array) alongside the joined 'mode' string. executeConversationTurn line 715 already prefers the array form when present (!empty($options['modes'])), so the multi-mode resolution survives without further refactor.
  2. processContinue (line 388): recover the modes array from session.mode by splitting on comma instead of trusting sanitize_key. Same dual-key pattern ('modes' + 'mode').
  3. createSession (line 641): preserve comma-joined multi-mode strings when writing to session.mode. Sanitize each comma-separated part individually so the multi-mode shape survives subsequent processContinue calls.

Verification

End-to-end simulation against the live filter chain in a downstream that depends on this for surface restriction:

stored='chat'                → modes=[chat]                   (admin surface — correct)
stored='cluckin-chuck'       → modes=[cluckin-chuck]          (custom — correct)
stored='cluckin-chuck,chat'  → modes=[cluckin-chuck,chat]     (multi — FIXED)
stored='cluckin-chuckchat'   → modes=[cluckin-chuckchat]      (legacy junk, unchanged)

The legacy-junk row is unchanged on purpose — sessions created before this fix carry the broken session.mode value, and the DB is the source of truth. Operators can patch those out-of-band if needed; downstream Cluckin' Chuck just ran UPDATE wp_datamachine_chat_sessions SET mode='cluckin-chuck,chat' WHERE session_id=... for the one affected session.

Out of scope

A proper fix would persist agent_modes as a JSON array column in the session table, so the modes are first-class state instead of being recovered from a comma-string heuristic. That's a schema migration and a bigger PR. This change is the minimum required to close the surface-escape security issue without touching the schema.

Versioning

Patch bump (no breaking changes to public APIs — 'mode' is still passed for backward compat, 'modes' is additive). Handled by your release tooling.

Multi-mode chat requests (e.g. agent_modes=['cluckin-chuck','chat']) were
silently collapsed to a single junk mode like 'cluckin-chuckchat' because
ChatOrchestrator joined the array to a comma-string at the boundary and
downstream sanitize_key() calls stripped the comma.

Net effect: mode-restricted tool allowlist filters (host plugins using
'datamachine_resolved_tools' to lock down a public chat surface to a
specific tool subset) silently no-op'd. Tools that should be gated by a
custom mode would leak through whenever 'chat' was also in the active
modes — defeating the whole point of the multi-mode resolution pattern.

Symptom in a real downstream (Cluckin' Chuck):
- frontend chat injects client_context.agent_modes = ['cluckin-chuck','chat']
- the 'chat' mode is required as the execution surface; 'cluckin-chuck'
  carries the custom directive + tool allowlist filter
- Without this fix the joined string becomes 'cluckin-chuck,chat' →
  sanitize_key strips the comma → 'cluckin-chuckchat' (junk)
- The allowlist filter's in_array(SLUG, modes) check fails → filter exits
  early → every chat-mode tool (read_instagram, publish_*, etc.) is
  reachable from the public surface

Fix:
- processChat (line 212): pass 'modes' => $modes array alongside the joined
  'mode' string. executeConversationTurn already prefers the array when
  present (see line 715), so the multi-mode resolution survives.
- processContinue (line 388): recover the modes array from session.mode by
  splitting on comma instead of trusting sanitize_key. Existing sessions
  that were stored before this fix as junk single-mode strings will resume
  with their broken mode, which is acceptable — the DB row is the source
  of truth and can be patched out-of-band.
- createSession (line 641): preserve comma-joined multi-mode strings when
  writing to session.mode (sanitize each part individually) so subsequent
  processContinue calls can recover the array.

Verified end-to-end with a downstream that depends on this for security-
adjacent surface restriction:

  stored='chat'                → modes=[chat]                   (admin surface — correct)
  stored='cluckin-chuck'       → modes=[cluckin-chuck]          (custom — correct)
  stored='cluckin-chuck,chat'  → modes=[cluckin-chuck,chat]     (multi — FIXED)
  stored='cluckin-chuckchat'   → modes=[cluckin-chuckchat]      (legacy junk, unchanged)

Reported by Extra-Chill / Cluckin' Chuck integration. Closes the upstream
half of chubes4/cluckin-chuck#12.
@homeboy-ci

homeboy-ci Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Homeboy Results — data-machine

Lint

lint — passed

ℹ️ Full options: homeboy docs commands/lint
Deep dive: homeboy lint data-machine --changed-since 0d167c1

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-lint-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-lint-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/26408276526

Test

test — passed

  • 519 passed

ℹ️ Auto-fix lint issues: homeboy refactor data-machine --from lint --write
ℹ️ Collect coverage: homeboy test data-machine --coverage
ℹ️ Save test baseline: homeboy test data-machine --baseline
ℹ️ Pass args to test runner: homeboy test -- [args]
ℹ️ Full options: homeboy docs commands/test
Deep dive: homeboy test data-machine --changed-since 0d167c1

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-test-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-test-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/26408276526

Audit

audit — passed

  • requested_detectors — 8 finding(s)
  • intra-method-duplication — 7 finding(s)
  • dead_code — 3 finding(s)
  • Directives — 1 finding(s)
  • Retention — 1 finding(s)
  • Total: 20 finding(s)

Deep dive: homeboy audit data-machine --changed-since 0d167c1

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-audit-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-audit-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/26408276526
Tooling versions
  • Homeboy CLI: homeboy 0.197.14+6d1d0e98
  • Extension: wordpress from https://github.com/Extra-Chill/homeboy-extensions
  • Extension revision: bb0c5bb4
  • Action: unknown@unknown

@chubes4
chubes4 merged commit bee3e0f into main May 25, 2026
5 checks passed
@chubes4
chubes4 deleted the fix/chat-orchestrator-modes-array branch May 25, 2026 15:55
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.

1 participant