fix(core): Enable Pi agent to successfully use slash commands: Accumulate multi-chunk AI commands before parsing#1581
Conversation
📝 WalkthroughWalkthroughAdds conservative full-command detection and continued accumulation for AI-generated orchestrator slash-commands so commands split across streamed assistant chunks are reconstructed and parsed in both stream and batch modes; emits corrective assistant tail chunks for partially streamed assistant output. Tests and mocks updated to cover multi-chunk command reconstruction and tail-completion cases. ChangesMulti-Chunk Command Parsing and Accumulation
Bridge: Streaming Tail Completion
Sequence DiagramsequenceDiagram
participant Provider as AI Provider
participant Stream as Stream Handler
participant Batch as Batch Handler
participant Parser as Command Parser
participant Dispatcher as Workflow Dispatcher
participant Platform as Platform (UI)
rect rgba(220,120,120,0.5)
Note over Provider,Dispatcher: Before fix — prefix suppressed but accumulation stopped
Provider->>Stream: Chunk 1: /invoke-workflow (prefix)
Stream->>Platform: suppress output
Provider->>Stream: Chunk 2: some-workflow
Provider->>Stream: Chunk 3: --project X
Stream->>Parser: Parse incomplete buffer
Parser->>Dispatcher: no dispatch
end
rect rgba(120,220,120,0.5)
Note over Provider,Dispatcher: After fix — accumulate until full command, then parse & dispatch
Provider->>Stream: Chunk 1: /invoke-workflow (prefix)
Stream->>Platform: suppress output
Note over Stream: accumulate chunks until isCommandFullyParsed == true
Provider->>Stream: Chunk 2: some-workflow
Provider->>Stream: Chunk 3: --project X
Stream->>Parser: Parse full accumulated text
Parser->>Dispatcher: dispatch /invoke-workflow
Dispatcher->>Platform: background workflow started (sessionId)
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes orchestrator command dispatch reliability when /invoke-workflow and /register-project are streamed across multiple assistant chunks, by continuing to accumulate assistant text past prefix detection (while still suppressing command output), adjusting batch parsing to use raw concatenation, and adding regression coverage.
Changes:
- Add module-level prefix/full regex constants and a
commandFullyParsedflag to decouple UI suppression from command accumulation in both stream and batch modes. - In batch mode, parse orchestrator commands from
assistantMessages.join('')instead of thefilterToolIndicators(...)-formatted output to avoid separator-induced parse breaks. - Add regression tests covering split-across-chunks behavior for both commands in stream and batch modes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/core/src/orchestrator/orchestrator-agent.ts | Updates stream/batch accumulation logic and batch parsing input; introduces shared regex constants. |
| packages/core/src/orchestrator/orchestrator-agent.test.ts | Adds regression tests for multi-chunk command accumulation and refactors a mock for reuse/reset. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/core/src/orchestrator/orchestrator-agent.ts (1)
962-1000: 💤 Low valueOptional: extract the full-pattern check to remove 4× duplication.
The block
if (INVOKE_WORKFLOW_FULL_RE.test(accumulated) || REGISTER_PROJECT_FULL_RE.test(accumulated)) { commandFullyParsed = true; }appears twice in
handleStreamMode(lines 982–987, 994–999) and twice inhandleBatchMode(lines 1157–1162, 1166–1170). A small helper would reduce repetition and centralize the "is the command fully captured?" decision alongside the regex constants near line 100.♻️ Suggested helper
+const isCommandFullyParsed = (s: string): boolean => + INVOKE_WORKFLOW_FULL_RE.test(s) || REGISTER_PROJECT_FULL_RE.test(s); + +const hasCommandPrefix = (s: string): boolean => + INVOKE_WORKFLOW_PREFIX_RE.test(s) || REGISTER_PROJECT_PREFIX_RE.test(s);Then both modes collapse to:
if (!commandDetected) { const accumulated = allMessages.join(''); if (hasCommandPrefix(accumulated)) { commandDetected = true; if (isCommandFullyParsed(accumulated)) commandFullyParsed = true; } else { await platform.sendMessage(conversationId, msg.content); } } else if (!commandFullyParsed) { if (isCommandFullyParsed(allMessages.join(''))) commandFullyParsed = true; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 962 - 1000, Extract the duplicated full-pattern check into a small helper (e.g. isCommandFullyParsed(accumulated)) placed near the regex constants so it returns true when INVOKE_WORKFLOW_FULL_RE.test(accumulated) || REGISTER_PROJECT_FULL_RE.test(accumulated); then replace the four duplicated if-blocks in handleStreamMode and handleBatchMode with calls to isCommandFullyParsed(allMessages.join('')) (or the accumulated variable) and set commandFullyParsed = true when it returns true; keep the existing flow and only centralize the regex logic into that helper.packages/core/src/orchestrator/orchestrator-agent.test.ts (1)
1839-1868: 💤 Low valueOptional: strengthen the suppression test by asserting parsed command body, not just
sendMessagecalls.This test verifies that
extra trailingis not streamed viasendMessage, but doesn't directly assert the post-command chunk was excluded from accumulation/parsing. SincemockParseCommandis stubbed to returnargs: ['Foo', '/path', 'extra', 'trailing'], the actualdefault_cwdpassed tocreateCodebaseis decoupled from the regex parse — so a regression wherecommandFullyParsedfailed to engage and the suppressed chunk was appended would still pass this test.Consider asserting
mockCreateCodebasewas called with the parsed-from-regex value to lock in the accumulation behavior:♻️ Stronger assertion
// Full command was accumulated and parsed - expect(mockCreateCodebase).toHaveBeenCalled(); + // The mocked parseCommand returns ['Foo','/path','extra','trailing'], but createCodebase + // is invoked through `/register-project ${name} ${path}` synthesized from the regex parse. + // If post-command suppression failed, the regex would see ' extra trailing' and the + // synthesized command path would include it. We assert the path stops at /path. + expect(mockCreateCodebase).toHaveBeenCalled(); + // Optional: spy on handleRegisterProject input to assert the synthesized message + // ends with '/path' (not '/path extra trailing').🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.test.ts` around lines 1839 - 1868, Add an assertion that the accumulated/parsed command fed to createCodebase matches the expected parsed args (i.e., that mockCreateCodebase was invoked with the parsed default_cwd and not with the appended "extra trailing" part) so the test verifies accumulation/parsing, not just suppression of sendMessage; specifically, after calling handleMessage check mockCreateCodebase (or the function that ultimately consumes the parsed command) was called and inspect its argument(s) to assert default_cwd (or args) equals the value returned by mockParseCommand (e.g., '/path' and ['Foo','/path','extra','trailing'] or assert the post-command text is not present), using the existing mocks mockParseCommand and mockCreateCodebase to locate where to add the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 100-101: REGISTER_PROJECT_FULL_RE currently uses
/^\/register-project\s+\S+\s+\S+/m which treats the path as a single non-space
token and can flip commandFullyParsed prematurely when a path contains spaces
across chunks; change REGISTER_PROJECT_FULL_RE to anchor the path to the end of
line so it accepts spaces (e.g. use a pattern like
/^\/register-project\s+\S+\s+.*$/m or /^\/register-project\s+\S+\s+.+$/m) and
update related streaming tests to cover split-across-chunks input, or if quoted
paths are required by design, add an inline comment and documentation enforcing
quoted-paths to make the asymmetry with parseOrchestratorCommands explicit
(referencing REGISTER_PROJECT_FULL_RE, INVOKE_WORKFLOW_FULL_RE and
parseOrchestratorCommands).
---
Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.test.ts`:
- Around line 1839-1868: Add an assertion that the accumulated/parsed command
fed to createCodebase matches the expected parsed args (i.e., that
mockCreateCodebase was invoked with the parsed default_cwd and not with the
appended "extra trailing" part) so the test verifies accumulation/parsing, not
just suppression of sendMessage; specifically, after calling handleMessage check
mockCreateCodebase (or the function that ultimately consumes the parsed command)
was called and inspect its argument(s) to assert default_cwd (or args) equals
the value returned by mockParseCommand (e.g., '/path' and
['Foo','/path','extra','trailing'] or assert the post-command text is not
present), using the existing mocks mockParseCommand and mockCreateCodebase to
locate where to add the assertion.
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 962-1000: Extract the duplicated full-pattern check into a small
helper (e.g. isCommandFullyParsed(accumulated)) placed near the regex constants
so it returns true when INVOKE_WORKFLOW_FULL_RE.test(accumulated) ||
REGISTER_PROJECT_FULL_RE.test(accumulated); then replace the four duplicated
if-blocks in handleStreamMode and handleBatchMode with calls to
isCommandFullyParsed(allMessages.join('')) (or the accumulated variable) and set
commandFullyParsed = true when it returns true; keep the existing flow and only
centralize the regex logic into that helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a2108524-99fa-43da-aad7-6544fe0ae54d
📒 Files selected for processing (2)
packages/core/src/orchestrator/orchestrator-agent.test.tspackages/core/src/orchestrator/orchestrator-agent.ts
…etion Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/core/src/orchestrator/orchestrator-agent.test.ts (1)
1780-1816: ⚡ Quick winTest gap:
/invoke-workflow … --promptsplit across chunks isn't covered.The regression suite covers the bare
/invoke-workflow assist --project my-projectshape but never exercises a chunk boundary that lands between--project <name>and--prompt "…". Perprompt-builder.ts, the synthesized--promptis mandatory and must be self-contained, so it's the most likely token to be dropped if accumulation stops too early. As noted in the comment onINVOKE_WORKFLOW_FULL_REinorchestrator-agent.ts, the current full-pattern matches as soon as the project name is seen — a test like the one below would expose that gap and pin behavior once it's fixed.♻️ Proposed additional test
+ test('stream mode — invoke-workflow with --prompt split across 3 chunks preserves synthesizedPrompt', async () => { + mockListCodebases.mockReturnValueOnce(Promise.resolve([makeCodebase('my-project')])); + mockDiscoverWorkflowsWithConfig.mockReturnValueOnce( + Promise.resolve({ workflows: [makeTestWorkflowWithSource({ name: 'assist' })], errors: [] }) + ); + mockSendQuery.mockImplementationOnce(async function* () { + yield { type: 'assistant', content: 'Running.\n\n/invoke-workflow assist --project my-project ' }; + yield { type: 'assistant', content: '--prompt "Add dark mode support"' }; + yield { type: 'result', sessionId: 'sess-1' }; + }); + + const platform = makePlatform(); + (platform.getStreamingMode as ReturnType<typeof mock>).mockReturnValue('stream'); + await handleMessage(platform, 'conv-1', 'add dark mode'); + + expect(mockDispatchBackgroundWorkflow).toHaveBeenCalled(); + const dispatchArgs = mockDispatchBackgroundWorkflow.mock.calls[0][0] as { originalMessage: string }; + expect(dispatchArgs.originalMessage).toBe('Add dark mode support'); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.test.ts` around lines 1780 - 1816, The tests currently cover chunk splits before the project flag but not a split that separates the required --prompt argument; add a new test in orchestrator-agent.test.ts that mirrors the existing "split across 2 chunks" cases but yields assistant chunks where the first chunk ends with '--project my-project ' and the second chunk begins with '--prompt "some prompt text"' (use mockSendQuery to produce those two assistant yields and a final result), exercise handleMessage with both streaming and batch platform.getStreamingMode values, and assert that the workflow is dispatched/executed according to the intended behavior; reference the existing test structure and symbols handleMessage, mockSendQuery, mockDispatchBackgroundWorkflow, mockExecuteWorkflow, and the INVOKE_WORKFLOW_FULL_RE behavior in orchestrator-agent.ts so the test targets the prompt-boundary regression described.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Line 100: INVOKE_WORKFLOW_FULL_RE currently marks the command complete as soon
as the project token is matched, which can drop a following --prompt delivered
in a later chunk; update the detection so the command is considered "fully
parsed" only when a line terminator or a closing --prompt quote is present
(mirror the approach used for REGISTER_PROJECT_FULL_RE). Locate
INVOKE_WORKFLOW_FULL_RE and the isCommandFullyParsed/parseOrchestratorCommands
flow and change the regex/logic to require either a newline terminator or an
explicit --prompt closure before flipping isCommandFullyParsed, ensuring
synthesizedPrompt is captured for handleWorkflowInvocationResult instead of
falling back to originalMessage.
---
Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.test.ts`:
- Around line 1780-1816: The tests currently cover chunk splits before the
project flag but not a split that separates the required --prompt argument; add
a new test in orchestrator-agent.test.ts that mirrors the existing "split across
2 chunks" cases but yields assistant chunks where the first chunk ends with
'--project my-project ' and the second chunk begins with '--prompt "some prompt
text"' (use mockSendQuery to produce those two assistant yields and a final
result), exercise handleMessage with both streaming and batch
platform.getStreamingMode values, and assert that the workflow is
dispatched/executed according to the intended behavior; reference the existing
test structure and symbols handleMessage, mockSendQuery,
mockDispatchBackgroundWorkflow, mockExecuteWorkflow, and the
INVOKE_WORKFLOW_FULL_RE behavior in orchestrator-agent.ts so the test targets
the prompt-boundary regression described.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b27e2196-ee09-40ed-ab66-9053b2810ac2
📒 Files selected for processing (2)
packages/core/src/orchestrator/orchestrator-agent.test.tspackages/core/src/orchestrator/orchestrator-agent.ts
…t accumulation in multi-chunk messages Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/core/src/orchestrator/orchestrator-agent.test.ts (1)
1892-1931: 💤 Low valueConsider tightening this test to avoid masking real path-corruption behavior.
The mocked
parseCommandreturning clean['Foo', '/path']letscreateCodebasesucceed even though the accumulated command text is/register-project Foo /path extra trailing(trailing chunk concatenated into the unquoted path). The comment honestly notes this would failexistsSyncin production, but the assertionexpect(mockCreateCodebase).toHaveBeenCalledWith(expect.objectContaining({ default_cwd: '/path' }))reads as if the path was correctly isolated when it actually is not.A safer formulation would be either (a) terminate chunk 2 with
\nsocommandFullyParsedfires and chunk 3 is genuinely suppressed (then assertdefault_cwd: '/path'truthfully), or (b) drop themockParseCommandoverride and assert the suppression-only behavior onplatform.sendMessage— leavingcreateCodebasecall expectations out — so the test doesn't appear to validate path correctness it isn't actually validating. Optional, since the streaming-suppression invariant is what this test is really about.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.test.ts` around lines 1892 - 1931, The test currently masks real path-corruption because mockParseCommand returns clean args while mockSendQuery yields a third " extra trailing" chunk that would actually corrupt the reconstructed unquoted path; either (a) make the second assistant chunk include a trailing "\n" (e.g. yield { type: 'assistant', content: ' /register-project Foo /path\n' }) so commandFullyParsed becomes true and the third chunk is suppressed (keep mockParseCommand and the createCodebase assertion), or (b) remove the mockParseCommand override and drop the createCodebase assertion so the test only asserts streaming behavior via platform.sendMessage and does not pretend to validate path correctness (adjust mockSendQuery accordingly); update the mockSendQuery yields and expectations to match the chosen approach and keep references to handleMessage, mockSendQuery, mockParseCommand, and mockCreateCodebase when editing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.test.ts`:
- Around line 1892-1931: The test currently masks real path-corruption because
mockParseCommand returns clean args while mockSendQuery yields a third " extra
trailing" chunk that would actually corrupt the reconstructed unquoted path;
either (a) make the second assistant chunk include a trailing "\n" (e.g. yield {
type: 'assistant', content: ' /register-project Foo /path\n' }) so
commandFullyParsed becomes true and the third chunk is suppressed (keep
mockParseCommand and the createCodebase assertion), or (b) remove the
mockParseCommand override and drop the createCodebase assertion so the test only
asserts streaming behavior via platform.sendMessage and does not pretend to
validate path correctness (adjust mockSendQuery accordingly); update the
mockSendQuery yields and expectations to match the chosen approach and keep
references to handleMessage, mockSendQuery, mockParseCommand, and
mockCreateCodebase when editing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2cbe714b-5b84-494d-8136-342fbb318de0
📒 Files selected for processing (3)
packages/core/src/orchestrator/orchestrator-agent.test.tspackages/core/src/orchestrator/orchestrator-agent.tspackages/core/src/orchestrator/orchestrator.test.ts
…i-chunk messages Co-authored-by: Copilot <copilot@github.com>
… truncation Fixes two failure modes where the Pi provider silently drops /register-project and /invoke-workflow commands, leaving projects unregistered with no error shown to the user. Mode A — Markdown bold wrapping (orchestrator-agent.ts) Pi occasionally emits commands wrapped in markdown bold: **/register-project Name "/path"**. The existing prefix and full-command regexes require lines to start with /, so these were never detected. Adds normalizeCommandText() which strips leading/trailing * characters from lines whose first non-asterisk character is /. Applied to all six detection and parse sites: isCommandFullyParsed, handleStreamMode, handleBatchMode, parseOrchestratorCommands (3 uses), and handleProjectRegistrationResult. Mode B — Streaming truncation (event-bridge.ts) Pi's text_delta events sometimes stop delivering characters mid-command (e.g. /register-project arrives but SaberEngine "/path" never does as a delta). The complete text is present in agent_end.messages but was previously discarded. Adds extractLastAssistantText() to read the fully-assembled text from the agent_end transcript. In bridgeSession, tracks the per-turn streamed length, and if the assembled text is strictly longer and starts with what was streamed, emits the missing suffix as a corrective assistant chunk before the result chunk — making it available to the orchestrator's accumulator. Tests 5 new tests in orchestrator-agent.test.ts covering bold/italic stripping for both commands, including multiline responses and quoted paths 5 new tests in event-bridge.test.ts covering: truncation detected and emitted, no false emission when complete, mismatch guard, per-turn reset on turn_start, and assistantBuffer inclusion when wantsStructured
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/core/src/orchestrator/orchestrator-agent.ts (1)
177-179: 💤 Low value
normalizeCommandTextwon't strip bold wrappers on indented lines.The pattern
/^\*+(\/[^\n]*?)\**$/gmanchors directly on*at line start, so**/register-project foo /path**(any leading whitespace, blockquote, list marker, etc.) won't be normalized and the prefix regex will still miss the line. If observed Pi outputs always emit bold-wrapped commands flush-left, this is fine; otherwise, allow optional leading whitespace and trim trailing whitespace too.♻️ Optional: tolerate leading/trailing whitespace
-function normalizeCommandText(text: string): string { - return text.replace(/^\*+(\/[^\n]*?)\**$/gm, '$1'); -} +function normalizeCommandText(text: string): string { + // Tolerate leading whitespace (blockquote/indent) and trailing whitespace + // around the bold/italic wrappers — some models indent their output. + return text.replace(/^[ \t]*\*+(\/[^\n]*?)\**[ \t]*$/gm, '$1'); +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 177 - 179, normalizeCommandText fails to strip bold wrappers when lines have leading whitespace; update the function (normalizeCommandText) to allow optional leading whitespace before the opening asterisks and to trim trailing whitespace after the closing asterisks (e.g., accept /^\s*\*+(\/[^\n]*?)\**\s*$/gm or alternatively apply .trim() to the captured text) so indented or spaced lines like " **/register-project foo /path**" are normalized correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 177-179: normalizeCommandText fails to strip bold wrappers when
lines have leading whitespace; update the function (normalizeCommandText) to
allow optional leading whitespace before the opening asterisks and to trim
trailing whitespace after the closing asterisks (e.g., accept
/^\s*\*+(\/[^\n]*?)\**\s*$/gm or alternatively apply .trim() to the captured
text) so indented or spaced lines like " **/register-project foo /path**" are
normalized correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9bf69065-164b-4c2a-95be-98fd30f62ba1
📒 Files selected for processing (5)
packages/core/src/handlers/command-handler.test.tspackages/core/src/orchestrator/orchestrator-agent.test.tspackages/core/src/orchestrator/orchestrator-agent.tspackages/providers/src/community/pi/event-bridge.test.tspackages/providers/src/community/pi/event-bridge.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/orchestrator/orchestrator-agent.ts (1)
1449-1474:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
indexOf('/register-project')may land on a prose mention rather than the actual command line.
parseOrchestratorCommandsuses^\/register-projectwith the/mflag, so only line-anchored occurrences are treated as the command.handleProjectRegistrationResult, however, slices on the first substring occurrence:const regIndex = normalizedForExtraction.indexOf('/register-project'); const textBeforeReg = normalizedForExtraction.slice(0, regIndex).trim();If the assistant explains itself before invoking — e.g.
You can use /register-project to register a project. /register-project foo /path/to/foo—
regIndexpoints at the prose mention on line 1, so the line containing the actual command is dropped fromtextBeforeReg, but the parser still dispatches based on line 2. The user message comes out truncated.♻️ Suggested fix — anchor the lookup to the parsed command line
- // Normalize before extraction so that Mode A's bold markers ('**') are - // stripped from the command line; otherwise textBeforeReg would include a - // trailing '**' when the model wrapped the command in markdown bold. - const normalizedForExtraction = normalizeCommandText(fullResponse); - const regIndex = normalizedForExtraction.indexOf('/register-project'); - const textBeforeReg = normalizedForExtraction.slice(0, regIndex).trim(); + // Normalize before extraction so that Mode A's bold markers ('**') are + // stripped from the command line; otherwise textBeforeReg would include a + // trailing '**' when the model wrapped the command in markdown bold. + const normalizedForExtraction = normalizeCommandText(fullResponse); + // Match line-anchored to avoid landing on a prose mention of "/register-project". + const regLineMatch = /^\/register-project\b/m.exec(normalizedForExtraction); + const regIndex = regLineMatch?.index ?? normalizedForExtraction.indexOf('/register-project'); + const textBeforeReg = normalizedForExtraction.slice(0, regIndex).trim();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 1449 - 1474, handleProjectRegistrationResult currently uses normalizedForExtraction.indexOf('/register-project') which can match a prose mention; change the lookup to find the line-anchored command like parseOrchestratorCommands does (e.g., search with a /^\s*\/register-project/m style match) and use the match's index to compute textBeforeReg so only text before the actual command line is sent; ensure you fall back safely if no anchored match is found and keep the subsequent call to handleRegisterProject using the exact matched command line.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 1449-1474: handleProjectRegistrationResult currently uses
normalizedForExtraction.indexOf('/register-project') which can match a prose
mention; change the lookup to find the line-anchored command like
parseOrchestratorCommands does (e.g., search with a /^\s*\/register-project/m
style match) and use the match's index to compute textBeforeReg so only text
before the actual command line is sent; ensure you fall back safely if no
anchored match is found and keep the subsequent call to handleRegisterProject
using the exact matched command line.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ee4b66fd-db45-4843-a0cb-443312a81490
📒 Files selected for processing (1)
packages/core/src/orchestrator/orchestrator-agent.ts
…n text extraction
There was a problem hiding this comment.
🧹 Nitpick comments (3)
packages/core/src/orchestrator/orchestrator-agent.ts (3)
170-185: 💤 Low value
normalizeCommandTextonly strips asterisks — confirm Pi/Claude don't also wrap with backticks/underscores.The docstring mentions "bold/italic decorators", but the regex only matches
\*+. Markdown italic with underscores (_/register-project ..._) or inline code (\/register-project ...`) would still defeat bothINVOKE_WORKFLOW_PREFIX_REandREGISTER_PROJECT_PREFIX_RE`. If the only observed bad-output mode from Pi was asterisks, this is fine as-is; otherwise consider broadening the pattern to cover the other two wrappers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 170 - 185, normalizeCommandText currently only removes surrounding asterisks, but other markdown wrappers (backticks or underscores) can also prevent INVOKE_WORKFLOW_PREFIX_RE / REGISTER_PROJECT_PREFIX_RE and the full regexes from matching; update normalizeCommandText to also strip a single pair of surrounding backticks and/or underscores (and combinations like `_*...*_`) so lines whose first non-wrapper char is '/' are normalized. Keep the normalization used by isCommandFullyParsed (which tests INVOKE_WORKFLOW_FULL_RE and REGISTER_PROJECT_FULL_RE) consistent with the prefix regexes by expanding the replacement pattern to remove leading/trailing `, _, and * wrappers around the /... command text while preserving internal characters.
1457-1467: 💤 Low valueNormalize-before-extract is correct, but the
indexOffallback can land on prose.Normalizing first ensures the
**wrappers don't leak intotextBeforeReg— good. The fallbacknormalizedForExtraction.indexOf('/register-project')at line 1463 is defensive but, by definition, will trigger only when the line-anchored regex misses, in which case the only available occurrence is non-line-anchored (i.e., almost certainly a prose mention like "use/register-project ..."). In that case we'd send the prose containing the command as the user-visible preamble and then attempt registration with whateverparseOrchestratorCommandscaptured — which itself is line-anchored, soregistrationshould never be set in this branch in the first place. Consider replacing the fallback with an early return / log warning so the inconsistency is visible rather than silently producing an odd preamble.♻️ Optional: make the no-line-match case explicit
- const normalizedForExtraction = normalizeCommandText(fullResponse); - // Match line-anchored to avoid landing on a prose mention of "/register-project". - const regLineMatch = /^\/register-project\b/m.exec(normalizedForExtraction); - const regIndex = regLineMatch?.index ?? normalizedForExtraction.indexOf('/register-project'); - const textBeforeReg = normalizedForExtraction.slice(0, regIndex).trim(); + const normalizedForExtraction = normalizeCommandText(fullResponse); + // Match line-anchored to avoid landing on a prose mention of "/register-project". + const regLineMatch = /^\/register-project\b/m.exec(normalizedForExtraction); + if (!regLineMatch) { + getLog().warn({ conversationId }, 'orchestrator.register_project_line_not_found'); + } + const regIndex = regLineMatch?.index ?? normalizedForExtraction.length; + const textBeforeReg = normalizedForExtraction.slice(0, regIndex).trim();As per coding guidelines: "Use Pino logger factory with event naming format
{domain}.{action}_{state}".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 1457 - 1467, The code currently falls back to normalizedForExtraction.indexOf('/register-project') which will match prose and produce an inconsistent preamble; change the logic in the block around normalizeCommandText, regLineMatch and regIndex so that if the line-anchored regex (/^\/register-project\b/m.exec) returns null you do not use indexOf but instead log a warning and return early so no prose is sent; specifically, remove the normalizedForExtraction.indexOf fallback, add an explicit branch when regLineMatch is falsy that calls the module Pino logger (use event name like "orchestrator.extract_no_line_match" per the pino format) and then return without calling platform.sendMessage(conversationId, ...); keep parseOrchestratorCommands behavior as-is.
1206-1256: 💤 Low valueBatch-mode gating and unconditional total-chunk cap look correct.
Pushing into
allChunksunconditionally while gatingassistantMessagesaccumulation/truncation on!commandFullyParsedcorrectly preserves the parse tokens, and enforcingMAX_BATCH_TOTAL_CHUNKSoutside thecommandDetectedbranch keeps the unconditionalallChunks.pushbounded. One minor caveat worth noting in a comment: if the AI never completes a detected command (no newline / closing quote ever arrives),assistantMessageswill skip theMAX_BATCH_ASSISTANT_CHUNKSshift and grow up toMAX_BATCH_TOTAL_CHUNKS— that's still bounded by the outer cap, but it's a behavior change from “always cap assistant chunks” worth calling out for future maintainers.Also applies to: 1299-1304
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 1206 - 1256, Add a clarifying comment near the loop handling allChunks/assistantMessages (around allChunks, assistantMessages, commandDetected, commandFullyParsed logic) that explains the edge case: once a command is detected (commandDetected=true) we stop enforcing the MAX_BATCH_ASSISTANT_CHUNKS shift, so if the AI never yields a complete command (isCommandFullyParsed never true) assistantMessages can grow until the outer MAX_BATCH_TOTAL_CHUNKS bound is hit; explicitly document this behavior and rationale so future maintainers know why assistant chunk capping is conditional and that total chunks still provide an overall cap.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 170-185: normalizeCommandText currently only removes surrounding
asterisks, but other markdown wrappers (backticks or underscores) can also
prevent INVOKE_WORKFLOW_PREFIX_RE / REGISTER_PROJECT_PREFIX_RE and the full
regexes from matching; update normalizeCommandText to also strip a single pair
of surrounding backticks and/or underscores (and combinations like `_*...*_`) so
lines whose first non-wrapper char is '/' are normalized. Keep the normalization
used by isCommandFullyParsed (which tests INVOKE_WORKFLOW_FULL_RE and
REGISTER_PROJECT_FULL_RE) consistent with the prefix regexes by expanding the
replacement pattern to remove leading/trailing `, _, and * wrappers around the
/... command text while preserving internal characters.
- Around line 1457-1467: The code currently falls back to
normalizedForExtraction.indexOf('/register-project') which will match prose and
produce an inconsistent preamble; change the logic in the block around
normalizeCommandText, regLineMatch and regIndex so that if the line-anchored
regex (/^\/register-project\b/m.exec) returns null you do not use indexOf but
instead log a warning and return early so no prose is sent; specifically, remove
the normalizedForExtraction.indexOf fallback, add an explicit branch when
regLineMatch is falsy that calls the module Pino logger (use event name like
"orchestrator.extract_no_line_match" per the pino format) and then return
without calling platform.sendMessage(conversationId, ...); keep
parseOrchestratorCommands behavior as-is.
- Around line 1206-1256: Add a clarifying comment near the loop handling
allChunks/assistantMessages (around allChunks, assistantMessages,
commandDetected, commandFullyParsed logic) that explains the edge case: once a
command is detected (commandDetected=true) we stop enforcing the
MAX_BATCH_ASSISTANT_CHUNKS shift, so if the AI never yields a complete command
(isCommandFullyParsed never true) assistantMessages can grow until the outer
MAX_BATCH_TOTAL_CHUNKS bound is hit; explicitly document this behavior and
rationale so future maintainers know why assistant chunk capping is conditional
and that total chunks still provide an overall cap.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 890e54cd-07c6-44cf-a044-870d8009478c
📒 Files selected for processing (1)
packages/core/src/orchestrator/orchestrator-agent.ts
…rove error handling
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 1469-1475: The current code returns when regLineMatch is falsy,
which can silently drop a valid /register-project dispatch; instead, remove the
early return so registration proceeds even if regLineMatch is missing, keep the
existing getLog().warn({ conversationId }, 'orchestrator.extract_no_line_match')
call, and ensure that when regLineMatch is absent you set/leave textBeforeReg
empty (or omit textBeforeReg from the downstream dispatch) before calling the
registration flow that parseOrchestratorCommands previously validated; update
the code paths around regLineMatch, textBeforeReg, and the /register-project
dispatch to continue processing while only skipping inclusion of textBeforeReg.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a1ce3b99-6737-4cf9-97e9-90ca8aca7ea4
📒 Files selected for processing (1)
packages/core/src/orchestrator/orchestrator-agent.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/core/src/orchestrator/orchestrator-agent.ts (1)
170-179: 💤 Low valueAsymmetric markdown stripping — minor edge case.
The pattern
/^\s*\*+(\/[^\n]*?)\**\s*$/gmrequires at least one leading asterisk (\*+), so it normalizes symmetric (**/cmd ...**,*/cmd ...*) and leading-only (**/cmd ...) cases, but a trailing-only emission (/register-project foo bar**) is left untouched and would leak**into the path token captured byparseOrchestratorCommands's(.+)$. Symmetric bold is by far the common emission pattern, so this is unlikely to fire in practice; flagging for awareness only.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/orchestrator/orchestrator-agent.ts` around lines 170 - 179, The regex in normalizeCommandText requires at least one leading asterisk so it misses trailing-only cases like "/register-project foo**"; change the quantifier on the leading asterisks from \*+ to \** so lines with zero or more leading asterisks are normalized (i.e., update the pattern in normalizeCommandText from /^\s*\*+(\/[^\n]*?)\**\s*$/gm to use /^\s*\**(\/[^\n]*?)\**\s*$/gm) to strip trailing-only, leading-only, and symmetric asterisks consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 170-179: The regex in normalizeCommandText requires at least one
leading asterisk so it misses trailing-only cases like "/register-project
foo**"; change the quantifier on the leading asterisks from \*+ to \** so lines
with zero or more leading asterisks are normalized (i.e., update the pattern in
normalizeCommandText from /^\s*\*+(\/[^\n]*?)\**\s*$/gm to use
/^\s*\**(\/[^\n]*?)\**\s*$/gm) to strip trailing-only, leading-only, and
symmetric asterisks consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 47412e40-ba53-418e-9573-b0712b76b80a
📒 Files selected for processing (1)
packages/core/src/orchestrator/orchestrator-agent.ts
Review SummaryVerdict: ready-to-merge Clean, well-targeted bug fix. The core change accumulates multi-chunk AI streaming output until full Suggested fixes
Minor / nice-to-have
Compliments
Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality. |
…late multi-chunk AI commands before parsing (coleam00#1581) * fix: improve command accumulation handling for multi-chunk messages * fix: enhance command parsing logic to prevent premature command completion Co-authored-by: Copilot <copilot@github.com> * fix: enhance INVOKE_WORKFLOW_FULL_RE regex to properly handle --prompt accumulation in multi-chunk messages Co-authored-by: Copilot <copilot@github.com> * fix: update command chunk handling to prevent path corruption in multi-chunk messages Co-authored-by: Copilot <copilot@github.com> * fix: add mock for listCodebases to enhance test coverage * Fix Pi provider silent failures: markdown bold wrapping and streaming truncation Fixes two failure modes where the Pi provider silently drops /register-project and /invoke-workflow commands, leaving projects unregistered with no error shown to the user. Mode A — Markdown bold wrapping (orchestrator-agent.ts) Pi occasionally emits commands wrapped in markdown bold: **/register-project Name "/path"**. The existing prefix and full-command regexes require lines to start with /, so these were never detected. Adds normalizeCommandText() which strips leading/trailing * characters from lines whose first non-asterisk character is /. Applied to all six detection and parse sites: isCommandFullyParsed, handleStreamMode, handleBatchMode, parseOrchestratorCommands (3 uses), and handleProjectRegistrationResult. Mode B — Streaming truncation (event-bridge.ts) Pi's text_delta events sometimes stop delivering characters mid-command (e.g. /register-project arrives but SaberEngine "/path" never does as a delta). The complete text is present in agent_end.messages but was previously discarded. Adds extractLastAssistantText() to read the fully-assembled text from the agent_end transcript. In bridgeSession, tracks the per-turn streamed length, and if the assembled text is strictly longer and starts with what was streamed, emits the missing suffix as a corrective assistant chunk before the result chunk — making it available to the orchestrator's accumulator. Tests 5 new tests in orchestrator-agent.test.ts covering bold/italic stripping for both commands, including multiline responses and quoted paths 5 new tests in event-bridge.test.ts covering: truncation detected and emitted, no false emission when complete, mismatch guard, per-turn reset on turn_start, and assistantBuffer inclusion when wantsStructured * fix: improve command normalization to handle leading/trailing whitespace * fix: improve command registration handling to avoid false positives in text extraction * fix: enhance command accumulation logic to prevent token loss and improve error handling * fix: improve command registration handling to prevent silent drops of valid commands --------- Co-authored-by: Copilot <copilot@github.com>


Summary
Problem: When an AI provider emits an orchestrator command (
/invoke-workflowor/register-project) across multiple streaming chunks, the orchestrator stopped accumulating text as soon as the command prefix was detected — so the command body never arrived, parse failed, and the workflow was silently not dispatched. A second independent bug affected batch mode:filterToolIndicatorsjoins chunks with'\n\n---\n\n'before splitting on'\n\n', injecting bare separator lines that break cross-chunk command patterns at parse time.Why it matters: Any provider that emits smaller text deltas (e.g. Pi, or any provider under load) would silently fail to dispatch AI-generated workflow invocations and project registrations, with no error shown to the user. The same response that triggered a workflow in one run would do nothing in another.
What changed: Both
handleStreamModeandhandleBatchModeinorchestrator-agent.tsnow accumulate assistant chunks until the complete command pattern is present, then stop. AcommandFullyParsedflag decouples accumulation from streaming suppression —commandDetectedstill gates UI output, butcommandFullyParsedgates the push. Batch-mode command parsing now usesassistantMessages.join('')(raw concatenation) instead offilterToolIndicatorsoutput. Four module-level regex constants replace inline literals duplicated across both modes. Six regression tests cover stream and batch mode for both commands, including split-across-chunks and trailing-chunk scenarios.What did not change (scope boundary): Workflow routing semantics, workflow execution, provider APIs, command syntax, the
filterToolIndicatorsfunction itself, user-visible message formatting for non-command responses, and all other orchestrator paths.UX Journey
Before
After
Architecture Diagram
Before
After
Connection inventory:
handleStreamModeallMessages[]accumulation!commandFullyParsedinstead of!commandDetectedhandleStreamModeplatform.sendMessage!commandDetectedhandleBatchModeassistantMessages[]accumulation!commandFullyParsedhandleBatchModeallChunks[]accumulation!commandDetected)handleBatchModeparseOrchestratorCommandsfilterToolIndicators(assistantMessages)toassistantMessages.join('')handleBatchModefilterToolIndicatorsfinalMessageparseOrchestratorCommandsINVOKE_WORKFLOW_PREFIX_RE/REGISTER_PROJECT_PREFIX_REINVOKE_WORKFLOW_FULL_RE/REGISTER_PROJECT_FULL_RELabel Snapshot
risk: lowsize: Score|testscore:orchestratorChange Metadata
bugcoreLinked Issue
Validation Evidence (required)
Result: 3574 pass, 0 fail across all packages. Exit code 0. All six checks passed:
check:bundled,check:bundled-skill,type-check,lint(0 warnings),format:check,test.New regression tests in
orchestrator-agent.test.ts:handleMessage — multi-chunk command accumulation (regression)— 6 tests, all passEvidence provided:
bun run validateexit 0, all test suites greenIf any command is intentionally skipped, explain why: N/A — full suite ran
Security Impact (required)
No)No)No)No)Compatibility / Migration
Yes)No)No)Human Verification (required)
What was personally validated beyond CI:
/invoke-workflowacross 2 chunks (stream and batch); split/register-projectacross 3 chunks (stream and batch); single-chunk command (non-regression); pre-command prose streamed correctly, post-command trailing chunks suppressed; existingorchestrator.test.tstest'silences further output after /invoke-workflow detected'continues to pass with no modification.commandFullyParsedimmediately, same behavior as before); batch modefilterToolIndicatorsoutput still used for user-visible non-command prose.Side Effects / Blast Radius (required)
handleStreamModeandhandleBatchModeinorchestrator-agent.ts— the two AI response processing paths. All platforms route through one of these two functions./invoke-workflowor/register-projectas prose examples (not commands), streaming suppression already fired before this change and continues to fire. ThecommandFullyParsedstop condition means accumulation halts once a valid full-command pattern matches — if the AI emits command-shaped prose that also matches the full pattern, it will be parsed and dispatched. This was already true before the change; the new stop condition does not widen the surface.commandDetected+commandFullyParsedstate machine is straightforward enough to audit by inspection.Rollback Plan (required)
git revert HEAD). No schema, config, or data changes to undo./invoke-workflowand/register-projectcommands from providers that emit small chunks (e.g. Pi) silently fail to dispatch — user sees no workflow started, no error message.Risks and Mitigations
Risk:
commandFullyParsedfull-pattern regexes diverge fromparseOrchestratorCommandsparser regexes over time, causing accumulation to stop before the parser can match.INVOKE_WORKFLOW_FULL_RE,REGISTER_PROJECT_FULL_RE) are module-level and co-located with the parser. They are intentionally conservative (require fewer tokens than the parser to guarantee the parser will match). The regression tests exercise both the accumulation stop and the downstream parse in a single assertion, so any divergence would break the tests.Risk: A provider emits the command prefix (
/invoke-workflow) but the turn ends before the full command arrives —commandDetectedhas suppressed all output,commandFullyParsedis never set, and the post-loop parse returns null. The user receives no response.commandDetectedlogic). It is not introduced by this change and is out of scope. It is noted here for future work.Summary by CodeRabbit
Bug Fixes
Tests