Skip to content

feat(goal): let the user arm a Goal from the composer - #3199

Merged
Astro-Han merged 12 commits into
apache:mainfrom
Astro-Han:feat/goal-arm-from-composer
Aug 20, 2026
Merged

feat(goal): let the user arm a Goal from the composer#3199
Astro-Han merged 12 commits into
apache:mainfrom
Astro-Han:feat/goal-arm-from-composer

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

A Goal could only be armed by the model, from inside a Turn, with the GoalSet tool. There was no Host operation for it, no bridge method, and no control anywhere in the product — the user could stop a Goal but never start one.

This adds goal.arm as a real Host operation, next to goal.query and goal.control, and carries it out to the + menu:

  • HostGoalCoordinator#arm creates the Goal through the same GoalManager the tool uses, under the same Session admission gate, with the same durable record. It schedules nothing: a Goal armed outside a Turn takes hold on the next one, which beginObservedTurn binds to the live control lease. A Session that already has an unfinished Goal gets operation_conflict.
  • One authority for the bounds. The two budget ceilings and the token floor move to @maka/core/goal, so the protocol codec and the GoalSet tool schema validate against one number instead of two copies of it.
  • Remote owners are granted goal.arm, at the same tier as goal.control. Withholding it would withhold nothing: a remote owner sends Turns, and the model arms its own Goal inside one. The only thing a refusal removes is the explicit path the user can see and stop.
  • The IPC handler takes the Session from the scoped channel, never from the renderer's frame, and normalizes the budgets without clamping them — an out-of-range value is refused once, by the Host, rather than quietly becoming a Goal the user did not ask for.
  • The + menu gains one "设定 Goal…" row in its action group, at the same 28px rhythm as the rest, and app-shell opens a dialog that collects the condition and the two budgets. The row explains itself when a Goal is already running (arming would conflict) or a Turn is in flight (the Goal would only take hold on the next one).

Stacked on #3198. That PR restructured the same menu, and this row belongs in the action group it establishes. Both branches live on the same fork, so GitHub cannot base this one on that branch directly — it is opened against main and its diff therefore contains #3198's commit as well. Review only the second commit (feat(goal): let the user arm a Goal from the composer), and merge #3198 first.

What it looks like

Shot live in Storybook, same viewport, each surface cropped to its own bounds and composited at 1:1 pixel scale. (The mode rows below the divider are #3198's doing, not this PR's.)

image

Left: 设定 Goal… joins 添加文件或目录 and 选择技能 in the action group — the same 28px row, no new component kind in the panel. Middle: arming refuses a second Goal, so the row says why instead of opening a dialog that would fail on submit; it is the same mechanism the Skills row already uses for an empty catalog, and the only row that grows past 28px is the one carrying an explanation. Right: the two budgets that stop a Goal are visible while it is being armed, the condition counter is the shared GOAL_CONDITION_TEXT_LIMIT, and 开始 stays disabled until there is a condition.

Verification

  • packages/runtime-host: goal-protocol.test.ts (+3 tests — exact frame, budget bounds, remote-owner grant) and goal-coordinator.test.ts (+1 — arm, conflict, not-found, archived) pass.
  • apps/desktop: runtime-host-client-operations.test.ts (+1 — one request, no retry on conflict) and runtime-host-session-domains-ipc-main.test.ts (+1 — scoped Session wins over the frame's, budget normalization) pass.
  • packages/runtime goal-tools.test.ts / goal-state.test.ts and packages/core goal-authority.test.ts pass unchanged, confirming the constant move is behaviour-preserving.
  • Typecheck: @maka/core, @maka/runtime, @maka/runtime-host, @maka/ui, and all four apps/desktop tsconfig projects. npm run format:check and biome lint clean.
  • Storybook: GoalDialogOpen and GoalAlreadySet added. Measured live — the seven + rows stay 28px, and the disabled Goal row grows to 48px only because it carries its explanation, the same way the Skills row already does with an empty catalog.
  • Not run: repository-wide suites (left to CI); no E2E added.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Code (Opus) wrote the diff, the tests, and the commit message. The decision to add a real Host operation rather than route the user through the model is the human contributor's. The commit carries a Generated-by: Claude Code trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above

Review focus

Runtime Host is the sole execution authority, and this adds an operation to it. Two things deserve the attention:

  1. #arm schedules nothing. A Goal armed while a Turn is in flight does not drive that Turn — it registered before the Goal existed, and activateGoal declines exactly that case (goal_not_observed). The UI disables the entry during a Turn so this is never a silent no-op, but the Host accepts it either way; the Goal simply starts from the next Turn.
  2. The remote-owner grant. REMOTE_OWNER_OPERATION_GRANTS is fail-closed by design, and this PR opens one entry in it. The reasoning is in the test that asserts it; if you disagree, the grant is the one line to revert.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Astro-Han, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Limit details: You’ve used all 3 included reviews currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 48132eeb-1c1d-4b7c-ad6b-2c5ad7530966

📥 Commits

Reviewing files that changed from the base of the PR and between 66f5d39 and 77a5c1f.

📒 Files selected for processing (11)
  • apps/desktop/src/main/__tests__/session-mode-ipc-main.test.ts
  • apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
  • apps/desktop/src/preload/bridge-contract.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/app-shell.tsx
  • apps/desktop/src/renderer/locales/shell-copy.ts
  • apps/desktop/src/renderer/styles/composer.css
  • apps/desktop/stories/app-shell.stories.tsx
  • packages/ui/src/__tests__/composer-plus-menu.test.tsx
  • packages/ui/src/composer.tsx
  • packages/ui/src/conversation-copy.ts
📝 Walkthrough

Problem solved

This PR lets users arm a Goal from the composer + menu through the new goal.arm Host operation.

The operation:

  • Rejects arming when the Session has an unfinished Goal.
  • Applies a Goal armed outside a Turn on the next Turn.
  • Does not apply a Goal armed during the active Turn to that Turn.
  • Rejects invalid budgets instead of clamping them.
  • Supports optional maximum-iteration and token budgets.
  • Supports local and remote-owner callers.

Source of truth

The PR extends the existing GoalManager, Session admission gate, durable records, Goal projection, and continuation logic. It does not create a parallel Goal execution path.

The PR moves shared budget limits into @maka/core/goal. Runtime tools, Host protocol validation, and the Goal dialog use the same limits.

The PR replaces separate Session mode setters with atomic setSessionMode updates and a shared ComposerSessionMode mapping. This extends the existing Session configuration model.

Smallest coherent solution

The Host operation requires coordinated changes across the protocol, coordinator, IPC, preload, runtime, and UI layers. The Goal dialog and composer entry point are required for the stated user flow.

The unified Session mode removes three independent mode states and callbacks. It reduces invalid combinations and keeps persisted mode updates atomic.

No code or test can be safely deleted based on the supplied diff. Boundary validation, compatibility handling, and regression coverage protect distinct behavior.

Validation and risks

The PR adds coverage for:

  • Request encoding, conflict handling, and no-retry behavior.
  • Scoped Session propagation through IPC.
  • Nullable, bounded, and non-clamped budgets.
  • Unknown-field and Session-consistency rejection.
  • Remote-owner operation grants.
  • Goal creation, duplicate arming, missing Sessions, archived Sessions, and restart recovery.
  • Goal timing and condition trimming.
  • Composer menus, Goal dialog states, and budget validation.
  • Atomic Session mode updates and legal mode combinations.

The objectives state that typechecks, formatting, and lint coverage were updated. Final required-check status is unverified.

Complexity delta

  • Authorities: Reuses existing Goal authorities and adds one coordinator operation.
  • States: Replaces separate Plan, Swarm, and Graph state with one ComposerSessionMode. Adds Goal dialog and Goal-active state.
  • Branches: Adds validation for condition text, budgets, Session scope, archive state, unfinished Goals, and Turn timing.
  • Configuration: Adds shared budget constants, a compatibility-epoch increment, and a remote-owner grant.
  • Public surface: Adds goal.arm across Host, runtime, IPC, preload, and UI contracts. Replaces separate Session mode setters with atomic setSessionMode.
  • Test burden: Adds cross-layer Goal, mode, IPC, protocol, UI, and end-to-end coverage. Removes duplicated budget assumptions and mode-state combinations.

The feature increases public-surface, validation, and UI complexity. The increase is justified by the requested capability. Shared limits and unified mode state reduce related maintenance complexity.

Review-relevant risks

  • The PR changes user-visible composer behavior and adds Goal controls. Material UI changes require independent human review under repository policy.
  • The PR adds the public goal.arm operation across multiple contracts. Material public-contract changes require independent human review under repository policy.
  • The PR grants remote owners access to goal.arm. This changes the security and governance boundary and requires independent human review under repository policy.
  • The PR changes Goal budget validation, recovery, and Turn timing. Material runtime-behavior changes require independent human review under repository policy.
  • The PR changes the runtime-host compatibility epoch. Material release and compatibility changes require independent human review under repository policy.
  • No licensing effect was identified in the current diff.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

Changes

Goal arming and unified composer modes

Layer / File(s) Summary
Goal arm contracts and limits
packages/core/src/goal.ts, packages/runtime/src/..., packages/runtime-host/src/protocol/...
Added shared Goal limits and the goal.arm protocol with validation, session consistency checks, remote-owner grants, and tests.
Goal arming host flow
packages/runtime-host/src/server/..., apps/desktop/src/main/..., apps/desktop/src/preload/...
Added Goal creation and persistence, token-baseline caching, desktop transport, IPC handlers, preload exposure, and integration tests.
Unified composer session modes
packages/ui/src/composer.tsx, apps/desktop/src/renderer/app-shell.tsx, apps/desktop/src/shared/session-mode.ts
Replaced separate Plan, Swarm, and Graph controls with unified session-mode state, atomic persistence, validation, and transitions.
Goal dialog integration and presentation
apps/desktop/src/renderer/goal-dialog.tsx, apps/desktop/src/renderer/locales/shell-copy.ts, apps/desktop/src/renderer/styles/*, apps/desktop/stories/app-shell.stories.tsx
Added the session-aware Goal dialog, localized form states, Goal menu controls, styling, and stories.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 66f5d

User-initiated Goal arming is not merge-ready because the desktop bridge can pass a composite session key while using a Host-local identifier, causing arming to target an unknown Session. The related test and inventory issues should also be corrected or explicitly accepted before merging.

Sequence Diagram(s)

sequenceDiagram
  participant GoalDialog
  participant PreloadBridge
  participant GoalArmIPC
  participant DesktopRuntimeHostClient
  participant HostGoalCoordinator
  GoalDialog->>PreloadBridge: call window.maka.goal.arm
  PreloadBridge->>GoalArmIPC: invoke goal:arm
  GoalArmIPC->>DesktopRuntimeHostClient: call armGoal
  DesktopRuntimeHostClient->>HostGoalCoordinator: request goal.arm
  HostGoalCoordinator-->>GoalArmIPC: return projected Goal
  GoalArmIPC-->>GoalDialog: return GoalState
Loading

Possibly related PRs

Suggested reviewers: likun666661, hqhq1025, m4n5ter

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: allowing users to arm a Goal from the composer.
Description check ✅ Passed The description covers the required summary, verification, AI use, checklist, behavior change, and review focus sections with specific implementation and test details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Ai Use Disclosure ✅ Passed The PR selects only substantive generative use, names Claude Code and its scope, and every one of the 15 commits from main..HEAD has a standalone Generated-by: Claude Code trailer.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@Astro-Han
Astro-Han marked this pull request as ready for review August 18, 2026 09:21
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Arm Goals from the composer and unify session mode selection

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds Host-authoritative Goal arming from the desktop composer with lifecycle and conflict
 safeguards.
• Centralizes Goal budget limits across protocol, UI, and model tools.
• Consolidates composer modes into one mutually exclusive session-mode selector.
Diagram

sequenceDiagram
  actor U as User
  participant C as Composer
  participant D as Goal Dialog
  participant B as Bridge and IPC
  participant P as Host Protocol
  participant G as Goal Coordinator
  participant M as Goal Manager
  participant S as Goal Store
  U->>C: Choose Set Goal
  C->>D: Open scoped form
  U->>D: Submit condition
  D->>B: Invoke goal.arm
  B->>P: Send scoped request
  P->>G: Validate and dispatch
  G->>M: Create under gate
  M->>S: Persist goal
  S-->>G: Durable record
  G-->>D: Return projection
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Arm through a synthetic model Turn
  • ➕ Reuses the existing GoalSet tool without adding a Host operation.
  • ➕ Avoids introducing another explicit protocol operation.
  • ➖ Starts model work merely to configure a Goal.
  • ➖ Adds latency, token cost, and nondeterministic tool invocation.
  • ➖ Provides weaker conflict handling and a less explicit user-controlled path.
2. Persist Goals directly from Desktop Main
  • ➕ Shortens the request path for the desktop-only workflow.
  • ➕ Could avoid extending the Host protocol.
  • ➖ Creates a second Goal authority outside GoalManager.
  • ➖ Risks inconsistent admission, token baselines, durability, and remote behavior.
  • ➖ Couples the desktop application directly to runtime storage semantics.

Recommendation: Keep the PR's Host-authoritative approach. A dedicated goal.arm operation provides deterministic user intent while reusing GoalManager, Session admission, protocol validation, and durable storage; synthetic Turns and desktop-owned persistence both weaken authority boundaries. Deferring execution until the next observed Turn also preserves existing control-lease semantics.

Files changed (23) +1068 / -251

Enhancement (12) +712 / -228
runtime-host-client.tsAdd the desktop goal.arm client operation +15/-0

Add the desktop goal.arm client operation

• Adds armGoal as a direct Host request without optimistic conflict retries.

apps/desktop/src/main/runtime-host-client.ts

runtime-host-session-domains-ipc-main.tsRegister session-scoped Goal-arm IPC +43/-0

Register session-scoped Goal-arm IPC

• Adds the goal:arm handler, derives the Session from the scoped argument, and normalizes optional positive-integer budgets without clamping Host-owned bounds.

apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts

bridge-contract.d.tsExpose Goal arming in the bridge contract +11/-0

Expose Goal arming in the bridge contract

• Defines the renderer-facing goal.arm input, result, lifecycle behavior, and conflict semantics.

apps/desktop/src/preload/bridge-contract.d.ts

preload.tsBridge renderer Goal-arm requests to IPC +8/-0

Bridge renderer Goal-arm requests to IPC

• Implements window.maka.goal.arm and forwards the scoped Session plus form values to goal:arm.

apps/desktop/src/preload/preload.ts

app-shell.tsxWire Goal dialog and unified session modes into the shell +122/-67

Wire Goal dialog and unified session modes into the shell

• Opens a Session-keyed Goal dialog from the composer and supplies active/conflict state. It also replaces independent Plan, Swarm, and Graph booleans with one mode value and sequences persisted mode changes safely.

apps/desktop/src/renderer/app-shell.tsx

goal-dialog.tsxAdd the Goal configuration dialog +149/-0

Add the Goal configuration dialog

• Introduces a localized form for the completion condition, maximum turns, and token budget. It submits through the preload bridge, prevents closure while arming, and reports Host errors without clamping values.

apps/desktop/src/renderer/goal-dialog.tsx

shell-copy.tsAdd localized Goal and session-mode messaging +68/-36

Add localized Goal and session-mode messaging

• Adds Chinese and English Goal dialog copy, active-Turn guidance, and shared mode transition messages.

apps/desktop/src/renderer/locales/shell-copy.ts

goal.tsDefine the goal.arm Host protocol +82/-0

Define the goal.arm Host protocol

• Adds typed input and output codecs, exact-field and shared-bound validation, operation errors, and Session ownership assertions.

packages/runtime-host/src/protocol/goal.ts

operations.tsGrant goal.arm to remote owners +1/-0

Grant goal.arm to remote owners

• Adds explicit Goal arming to the remote-owner operation grant list alongside Goal control and query.

packages/runtime-host/src/protocol/operations.ts

goal-coordinator.tsImplement Host-authoritative Goal arming +59/-2

Implement Host-authoritative Goal arming

• Creates Goals through GoalManager under the Session admission gate, validates Session lifecycle, persists immediately, and returns conflicts for unfinished Goals. It shares the continuation token cache and deliberately schedules no Turn.

packages/runtime-host/src/server/goal-coordinator.ts

composer.tsxAdd Goal action and mutually exclusive mode selection +136/-105

Add Goal action and mutually exclusive mode selection

• Adds the Set Goal menu row with contextual disabled reasons. Replaces three mode checkboxes with a four-option radio group and one active-mode footer control.

packages/ui/src/composer.tsx

conversation-copy.tsLocalize Goal actions and unified mode controls +18/-18

Localize Goal actions and unified mode controls

• Adds Chinese and English labels for Goal arming, existing-Goal conflicts, the session-mode group, and its default option while removing obsolete toggle wording.

packages/ui/src/conversation-copy.ts

Refactor (5) +40 / -3
composer.cssStyle mode radio rows with check indicators +18/-0

Style mode radio rows with check indicators

• Suppresses the default radio circle within the composer plus panel so selected session modes use the themed trailing check mark.

apps/desktop/src/renderer/styles/composer.css

goal.tsCentralize Goal budget boundaries +12/-0

Centralize Goal budget boundaries

• Defines shared maximum iteration and block-cap values plus the minimum useful token budget.

packages/core/src/goal.ts

goal-state.tsRe-export shared Goal budget constants +3/-0

Re-export shared Goal budget constants

• Makes the centralized Goal limits available through the runtime Goal-state module.

packages/runtime/src/goal-state.ts

goal-tools.tsUse shared limits in the GoalSet schema +6/-3

Use shared limits in the GoalSet schema

• Replaces duplicated numeric bounds for iterations, blocked turns, and token budgets with core Goal constants.

packages/runtime/src/goal-tools.ts

components.tsxExport the composer session-mode type +1/-0

Export the composer session-mode type

• Publishes ComposerSessionMode through the UI component entry point for desktop consumers.

packages/ui/src/components.tsx

Tests (5) +314 / -20
runtime-host-client-operations.test.tsVerify one-shot Goal arming and conflict handling +46/-0

Verify one-shot Goal arming and conflict handling

• Tests that the desktop client sends exactly one goal.arm request, returns the projection, and does not retry an operation_conflict response.

apps/desktop/src/main/tests/runtime-host-client-operations.test.ts

runtime-host-session-domains-ipc-main.test.tsVerify scoped Goal-arm IPC validation +45/-0

Verify scoped Goal-arm IPC validation

• Confirms that the IPC channel Session overrides renderer frame data, omitted budgets become null, and malformed inputs are rejected.

apps/desktop/src/main/tests/runtime-host-session-domains-ipc-main.test.ts

app-shell.stories.tsxCover unified modes and Goal states in stories +31/-20

Cover unified modes and Goal states in stories

• Migrates stories to the single sessionMode contract and adds scenarios for an existing Goal and an open Goal dialog.

apps/desktop/stories/app-shell.stories.tsx

goal-coordinator.test.tsTest Host Goal creation and admission failures +115/-0

Test Host Goal creation and admission failures

• Exercises durable Goal arming, duplicate conflicts, missing Sessions, archived Sessions, and the guarantee that arming alone schedules no execution.

packages/runtime-host/src/tests/goal-coordinator.test.ts

goal-protocol.test.tsTest goal.arm frames, bounds, and remote grants +77/-0

Test goal.arm frames, bounds, and remote grants

• Covers exact nullable input frames, shared budget limits, Session-consistent results, and remote-owner authorization.

packages/runtime-host/src/tests/goal-protocol.test.ts

Other (1) +2 / -0
astryx-mount.cssApply shell theme variables to composer menus +2/-0

Apply shell theme variables to composer menus

• Extends Astryx surface color mappings to the composer plus menu and permission-mode icon.

apps/desktop/src/renderer/styles/astryx-mount.css

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/desktop/src/renderer/app-shell.tsx (1)

1154-1187: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Session identity is read from render state, not from the ref the writes use.

setSessionMode reads activeSessionMode, which is derived at render time from activeId. The chained writes (setPlanMode, setSwarmMode, setGraphMode) each re-read activeIdRef.current. Between the first await and the next branch the active Session can change, so current can describe a Session that is no longer the write target. The result is a transition computed for one Session and applied to another.

The existing pending gates limit the window, and this matches the pattern used by the permission control, so this is follow-up rather than a blocker. If you want it closed now, capture the session id and its mode once at entry and pass both down.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2723400f-0d43-4f1b-bd13-48fdddd5fc9e

📥 Commits

Reviewing files that changed from the base of the PR and between 49f8675 and e412263.

📒 Files selected for processing (23)
  • apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
  • apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts
  • apps/desktop/src/main/runtime-host-client.ts
  • apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
  • apps/desktop/src/preload/bridge-contract.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/app-shell.tsx
  • apps/desktop/src/renderer/goal-dialog.tsx
  • apps/desktop/src/renderer/locales/shell-copy.ts
  • apps/desktop/src/renderer/styles/astryx-mount.css
  • apps/desktop/src/renderer/styles/composer.css
  • apps/desktop/stories/app-shell.stories.tsx
  • packages/core/src/goal.ts
  • packages/runtime-host/src/__tests__/goal-coordinator.test.ts
  • packages/runtime-host/src/__tests__/goal-protocol.test.ts
  • packages/runtime-host/src/protocol/goal.ts
  • packages/runtime-host/src/protocol/operations.ts
  • packages/runtime-host/src/server/goal-coordinator.ts
  • packages/runtime/src/goal-state.ts
  • packages/runtime/src/goal-tools.ts
  • packages/ui/src/components.tsx
  • packages/ui/src/composer.tsx
  • packages/ui/src/conversation-copy.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.

Comment thread apps/desktop/src/main/runtime-host-client.ts Outdated
Comment thread apps/desktop/src/renderer/goal-dialog.tsx Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Stale orchestration mode remains ✓ Resolved 🐞 Bug ≡ Correctness
Description
For a legacy/model-created mixed state with Plan plus Swarm/Graph, selecting Default clears only
Plan. The stale orchestration field remains because cleanup branches on the unchanged local `current
=== 'plan'`, causing the session to resolve back to Swarm/Graph after refresh.
Code

apps/desktop/src/renderer/app-shell.tsx[R1167-1170]

+  async function setSessionMode(next: ComposerSessionMode): Promise<void> {
+    const current = activeSessionMode;
+    if (next === current) return;
+    if (current === 'plan' && !(await setPlanMode(false))) return;
Relevance

●●● Strong

This is a concrete stale-state correctness bug; history accepts cross-session and
orchestration-state consistency fixes.

PR-#2384
PR-#2956

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The projection explicitly prioritizes Plan and documents mixed records as possible; the transition
code clears Plan then uses the original current for the only Swarm/Graph cleanup branches.

apps/desktop/src/renderer/app-shell.tsx[1337-1350]
apps/desktop/src/renderer/app-shell.tsx[1167-1186]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A Session can contain `collaborationMode: 'plan'` together with `orchestrationMode: 'swarm'` or `'graph'`, including records written by older builds or model-side changes. `activeSessionMode` shows that as Plan, but `setSessionMode('default')` only invokes `setPlanMode(false)`; its later orchestration cleanup still tests the original `current === 'plan'`, so it never clears the stale orchestration mode.

## Issue Context
The UI can temporarily appear to reach Default, but the remaining persisted orchestration value reasserts itself when the session projection refreshes.

## Fix Focus Areas
- apps/desktop/src/renderer/app-shell.tsx[1167-1186]
- apps/desktop/src/renderer/app-shell.tsx[1342-1349]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Remote Goal budget unbounded ⊘ Outdated 🐞 Bug ⛨ Security
Description
The new remote-owner goal.arm grant permits direct autonomous Goal creation, but tokenBudget has
only a minimum and no maximum. A remote owner can request any safe-integer budget, allowing
unattended spend until the iteration limit or that effectively unbounded budget is reached.
Code

packages/runtime-host/src/protocol/operations.ts[236]

+  'goal.arm',
Relevance

●● Moderate

Security concern is plausible, but the PR explicitly documents an intentionally unbounded optional
token budget and evidence lacks a close rejection precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
goal.arm joins the remote-owner whitelist, while the decoder tests only whether tokenBudget is
below its minimum; Goal state stores and uses the supplied budget as a continuation stopping
condition.

packages/runtime-host/src/protocol/operations.ts[233-238]
packages/runtime-host/src/protocol/goal.ts[213-220]
packages/runtime/src/goal-state.ts[200-215]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`goal.arm` is newly granted to remote owners, while its `tokenBudget` validation enforces only the 1,000-token floor and no upper ceiling. This gives a remote caller a direct path to arm an autonomous continuation loop with an arbitrarily large budget.

## Issue Context
`maxIterations` already has a shared ceiling. The GoalSet tool has the same historical absence of a token ceiling, but this PR newly exposes the capability to remote owners without requiring model/tool participation in a Turn. A shared core maximum is preferable so GoalSet and goal.arm cannot drift.

## Fix Focus Areas
- packages/runtime-host/src/protocol/operations.ts[233-238]
- packages/runtime-host/src/protocol/goal.ts[217-220]
- packages/core/src/goal.ts[269-280]
- packages/runtime/src/goal-tools.ts[194-204]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Goal condition whitespace differs ⊘ Outdated 🐞 Bug ⚙ Maintainability
Description
GoalSet trims surrounding condition whitespace before persistence, but goal.arm only uses trim()
to test emptiness and returns the original condition. Non-dialog callers can therefore persist
conditions with surrounding whitespace unlike model-created Goals.
Code

packages/runtime-host/src/protocol/goal.ts[R210-225]

+  if (!condition.trim() || condition.length > GOAL_CONDITION_TEXT_LIMIT.codeUnits) {
+    throw invalidProtocolFrame('Invalid Goal condition');
+  }
+  const maxIterations = requireNullablePositiveCount(record.maxIterations, 'Goal maxIterations');
+  if (maxIterations !== null && maxIterations > GOAL_MAX_ITERATIONS_LIMIT) {
+    throw invalidProtocolFrame('Goal maxIterations exceeds its limit');
+  }
+  const tokenBudget = requireNullablePositiveCount(record.tokenBudget, 'Goal tokenBudget');
+  if (tokenBudget !== null && tokenBudget < GOAL_TOKEN_BUDGET_MINIMUM) {
+    throw invalidProtocolFrame('Goal tokenBudget is below its minimum');
+  }
+  return {
+    sessionId: requireEntityId(record.sessionId, 'sessionId'),
+    condition,
+    maxIterations,
+    tokenBudget,
Relevance

●●● Strong

Normalizing persisted user input is a small deterministic consistency fix across sibling Goal
creation paths.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
GoalSet uses .trim() before calling GoalManager, while decodeGoalArmInput validates
condition.trim() but returns the untrimmed variable.

packages/runtime-host/src/protocol/goal.ts[205-226]
packages/runtime/src/goal-tools.ts[169-177]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The goal.arm protocol decoder checks `condition.trim()` only for blankness but returns the original text. GoalSet normalizes its condition with Zod `.trim()` before it calls GoalManager, so the two Goal creation paths persist different values for equivalent input.

## Issue Context
The current GoalDialog trims before sending, but the Host protocol is a public operation boundary and should provide the same normalization for every caller.

## Fix Focus Areas
- packages/runtime-host/src/protocol/goal.ts[205-226]
- packages/runtime/src/goal-tools.ts[169-177]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. IPC input silently drops fields ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The new IPC validator accepts arbitrary object keys and reconstructs only recognized fields, so
renderer input mistakes are silently ignored instead of rejected. This bypasses the exact-frame
contract enforced by the Host protocol and diverges from sibling IPC validators that reject unknown
keys.
Code

apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts[R292-300]

+  const record = value as Record<string, unknown>;
+  if (typeof record.condition !== 'string' || record.condition.trim().length === 0) {
+    throw new TypeError('Goal condition is required');
+  }
+  return {
+    condition: record.condition,
+    maxIterations: optionalCount(record.maxIterations, 'Goal maxIterations'),
+    tokenBudget: optionalCount(record.tokenBudget, 'Goal tokenBudget'),
+  };
Relevance

●●● Strong

Exact-frame enforcement is a deterministic protocol invariant, and accepted history favors rejecting
malformed or ambiguous inputs.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper only reads three fields and creates a new object, whereas the protocol decoder requires
exactly sessionId, condition, maxIterations, and tokenBudget.

apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts[284-300]
packages/runtime-host/src/protocol/goal.ts[198-204]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`requireGoalArmBudgets` validates recognized fields but performs no exact-key check. Unknown fields are discarded before the request reaches the protocol decoder, even though `goal.arm` itself requires an exact input frame.

## Issue Context
This is non-blocking but makes renderer-to-main behavior inconsistent with sibling exact-shape IPC validators and can hide payload typos or unsupported caller intent.

## Fix Focus Areas
- apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts[284-300]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a broad, behavior-dense feature spanning Host protocol/admission and persistence, IPC security boundaries, renderer UI state, and public UI contracts, with many independent paths where a redundant review can catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@Astro-Han
Astro-Han force-pushed the feat/goal-arm-from-composer branch from e412263 to 974a407 Compare August 18, 2026 12:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/desktop/src/renderer/app-shell.tsx (1)

2188-2190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two spellings of "is this Session in swarm".

These status branches read activeSessionForView?.orchestrationMode directly. activeSessionMode already answers the same question through the shared sessionModeOf authority, and setOrchestrationSessionMode (line 1105) uses it. Reading it here too keeps one derivation.

♻️ Optional consolidation
-        const active = activeIdRef.current
-          ? (activeSessionForView?.orchestrationMode ?? 'default') === 'swarm'
-          : newChatSessionMode === 'swarm';
+        const active = activeSessionMode === 'swarm';

Also applies to: 2234-2236


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fb46c9d-3383-409b-8d00-caf4cbbf8891

📥 Commits

Reviewing files that changed from the base of the PR and between e412263 and 974a407.

📒 Files selected for processing (18)
  • apps/desktop/src/main/__tests__/runtime-host-session-domains-ipc-main.test.ts
  • apps/desktop/src/main/__tests__/session-mode-ipc-main.test.ts
  • apps/desktop/src/main/__tests__/session-mode.test.ts
  • apps/desktop/src/main/runtime-host-client.ts
  • apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
  • apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
  • apps/desktop/src/preload/bridge-contract.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/app-shell.tsx
  • apps/desktop/src/renderer/goal-dialog.tsx
  • apps/desktop/src/renderer/locales/shell-copy.ts
  • apps/desktop/src/shared/session-mode.ts
  • docs/astryx-surface-file-inventory.md
  • docs/astryx-surface-file-inventory.paths
  • packages/runtime/src/__tests__/goal-state.test.ts
  • packages/runtime/src/goal-state.ts
  • packages/ui/src/__tests__/composer-plus-menu.test.tsx
  • packages/ui/src/composer.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/desktop/src/main/runtime-host-client.ts
  • apps/desktop/src/main/tests/runtime-host-session-domains-ipc-main.test.ts
  • packages/runtime/src/goal-state.ts
  • apps/desktop/src/renderer/goal-dialog.tsx
  • apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
  • packages/ui/src/composer.tsx
  • apps/desktop/src/renderer/locales/shell-copy.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 2 remain after this review.

Comment thread apps/desktop/src/main/__tests__/session-mode-ipc-main.test.ts Outdated
Comment thread apps/desktop/src/preload/preload.ts Outdated
Comment thread apps/desktop/src/renderer/app-shell.tsx
Comment thread packages/ui/src/__tests__/composer-plus-menu.test.tsx Outdated
@Astro-Han
Astro-Han force-pushed the feat/goal-arm-from-composer branch from 974a407 to 6e5ea80 Compare August 18, 2026 12:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/desktop/src/renderer/app-shell.tsx (2)

2003-2003: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse SESSION_MODE_FIELDS for the new-chat field derivation.

Lines 2003, 2056, and 2057-2060 re-derive the mode-to-field mapping that SESSION_MODE_FIELDS already owns. applySessionMode reads that table for the Session path, so the same mapping now has two authorities. If a fifth mode is added to the table, these ternaries still compile and map it to agent/default.

♻️ Proposed reuse of the existing table
-    newSessionCollaborationMode: newChatSessionMode === 'plan' ? 'plan' : 'agent',
+    newSessionCollaborationMode: SESSION_MODE_FIELDS[newChatSessionMode].collaborationMode,
-    newChatCollaborationMode: newChatSessionMode === 'plan' ? 'plan' : 'agent',
-    newChatOrchestrationMode:
-      newChatSessionMode === 'graph' || newChatSessionMode === 'swarm'
-        ? newChatSessionMode
-        : 'default',
+    newChatCollaborationMode: SESSION_MODE_FIELDS[newChatSessionMode].collaborationMode,
+    newChatOrchestrationMode: SESSION_MODE_FIELDS[newChatSessionMode].orchestrationMode,

As per path instructions: "Choose remedies in this order: delete an unnecessary path, consolidate duplicated authority, reuse the closest existing seam".

Also applies to: 2056-2060

Source: Path instructions


2185-2194: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Report /swarm and /graph status from activeSessionMode.

Both status branches read activeSessionForView?.orchestrationMode directly. The matching set_mode branches act on activeSessionMode, which gives Plan precedence. For a record that carries collaborationMode: 'plan' together with orchestrationMode: 'swarm', the status reports swarm enabled while /swarm off treats the Session as not in swarm. Reading activeSessionMode in both branches removes the second authority.

♻️ Proposed alignment
-        const active = activeIdRef.current
-          ? (activeSessionForView?.orchestrationMode ?? 'default') === 'swarm'
-          : newChatSessionMode === 'swarm';
+        const active = activeSessionMode === 'swarm';
-        const active = activeIdRef.current
-          ? (activeSessionForView?.orchestrationMode ?? 'default') === 'graph'
-          : newChatSessionMode === 'graph';
+        const active = activeSessionMode === 'graph';

As per path instructions: "consolidate duplicated authority".

Also applies to: 2231-2240

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 59eb421b-1bee-404f-b5bc-2a20f71db0d0

📥 Commits

Reviewing files that changed from the base of the PR and between 974a407 and 6e5ea80.

📒 Files selected for processing (7)
  • apps/desktop/src/main/__tests__/runtime-host-client-operations.test.ts
  • apps/desktop/src/main/runtime-host-client.ts
  • apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
  • apps/desktop/src/renderer/app-shell.tsx
  • packages/runtime-host/src/__tests__/goal-coordinator.test.ts
  • packages/runtime-host/src/server/goal-coordinator.ts
  • packages/ui/src/conversation-copy.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/desktop/src/main/tests/runtime-host-client-operations.test.ts
  • apps/desktop/src/main/runtime-host-client.ts
  • packages/ui/src/conversation-copy.ts
  • apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
  • packages/runtime-host/src/tests/goal-coordinator.test.ts
  • packages/runtime-host/src/server/goal-coordinator.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 0 remain after this review.

@Astro-Han
Astro-Han force-pushed the feat/goal-arm-from-composer branch from 6e5ea80 to 3e6bbc3 Compare August 18, 2026 12:24
@Astro-Han
Astro-Han requested review from M4n5ter, hqhq1025, jackwener and likun666661 and removed request for jackwener August 18, 2026 19:26

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

English

Requesting changes. I found five non-duplicate correctness and compatibility issues:

  1. an armed-but-not-started Goal begins autonomously after Host restart;
  2. the first Goal-bound Turn is excluded from a manually selected token budget;
  3. invalid pending budget text can silently arm a Goal with different or unlimited bounds;
  4. the new wire operation does not advance the compatibility epoch;
  5. a response-lost arm can commit durably while the UI reports failure.

The Host admission, scoped Session binding, conflict/archive handling, and durable flush paths otherwise look coherent. The simplify audit also found a second Desktop-side runtime schema for goal.arm; consolidating it into the canonical Host operation decoder is a worthwhile non-blocking follow-up, but I am not opening a separate inline thread because it substantially overlaps the existing single-source and exact-IPC-shape discussions.

One additional upgrade reminder: credentials issued to remote owners before this change do not contain the new goal.arm grant. The current Remote Host user population is effectively negligible and reissuing those credentials is inexpensive, so the practical impact can be ignored for this PR; it should not block the change.

All checks on the current head are green, but the affected restart, first-Turn budget, mixed-version, invalid pending-input, and dispatched-interruption paths are not covered by those tests.

简体中文

请求修改。本次发现 5 个没有与现有评论重复的 correctness 和 compatibility 问题:

  1. 尚未开始的手动 Goal 会在 Host 重启后自行执行;
  2. 首个受 Goal 约束的 Turn 不计入手动选择的 token budget;
  3. 无效的待输入预算可能被静默替换为其他限制或无限制;
  4. 新增 wire operation 后没有提升 compatibility epoch;
  5. arm 响应丢失时,Goal 可能已经持久化,但 UI 会错误报告失败。

除此之外,Host admission、scoped Session 绑定、conflict/archive 处理和 durable flush 路径整体一致。simplify-audit 还发现 Desktop 为 goal.arm 建立了第二套 runtime schema;将其收敛到 Host operation 的 canonical decoder 是有价值的非阻塞 follow-up,但它与现有 single-source 和 exact IPC shape 讨论高度相邻,因此不再单独创建 inline thread。

另有一项升级提醒:本次变更之前签发给 remote owner 的 credential 不包含新增的 goal.arm grant。目前 Remote Host 用户量几乎可以忽略,重新签发 credential 的成本也很低,因此该项的实际影响可以忽略,不作为本 PR 的阻塞理由。

当前 head 的所有 checks 均为绿色,但 restart、首个 Turn 预算、混合版本、无效待输入以及 dispatched interruption 这些路径没有被现有测试覆盖。

Comment thread packages/runtime-host/src/server/goal-coordinator.ts
Comment thread packages/runtime-host/src/server/goal-coordinator.ts Outdated
Comment thread apps/desktop/src/renderer/goal-dialog.tsx Outdated
Comment thread packages/runtime-host/src/protocol/goal.ts
Comment thread apps/desktop/src/main/runtime-host-client.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 929de8b0-f211-4644-8b7c-4f76ac2fa215

📥 Commits

Reviewing files that changed from the base of the PR and between 6e5ea80 and 66f5d39.

📒 Files selected for processing (20)
  • apps/desktop/e2e/goal-dialog-budget.spec.ts
  • apps/desktop/src/main/__tests__/session-mode-ipc-main.test.ts
  • apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
  • apps/desktop/src/main/runtime-host-session-domains-ipc-main.ts
  • apps/desktop/src/preload/bridge-contract.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/goal-dialog.tsx
  • apps/desktop/src/renderer/locales/shell-copy.ts
  • apps/desktop/src/renderer/use-session-goal.ts
  • apps/desktop/src/shared/goal-arm.ts
  • docs/astryx-surface-file-inventory.md
  • packages/core/src/goal.ts
  • packages/runtime-host/src/__tests__/goal-coordinator.test.ts
  • packages/runtime-host/src/protocol/index.ts
  • packages/runtime-host/src/server/goal-coordinator.ts
  • packages/runtime/src/__tests__/goal-state.test.ts
  • packages/runtime/src/__tests__/goal-tools.test.ts
  • packages/runtime/src/goal-state.ts
  • packages/runtime/src/goal-tools.ts
  • packages/ui/src/__tests__/composer-plus-menu.test.tsx

Included review availability: Your plan provides up to 3 included reviews per hour; 0 remain after this review.

Comment thread docs/astryx-surface-file-inventory.md
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Thanks for the careful pass — the restart and budget findings were both real, and chasing them turned up more than the diff showed.

All five are answered in their own threads: 1, 3 and 4 are fixed; for 2 I took the finding but not the remedy, since the baseline parameter turned out to be inert on both paths, so it is removed and the semantics stated; 5 is deferred to #3234, where it belongs as a contract shared by every control operation. Two of your suggested implementations I did not take, with reasons in the threads.

Would appreciate another look when you have time.

AI disclosure: this round's code, tests and thread replies were produced with Claude Code. I reviewed the final diff and own the result; each fix was verified by first making its test fail for its own reason.


感谢细致 review——重启和预算这两条都成立,而且顺着查下去发现的比 diff 上能看到的更多。

五条都已在各自 thread 中回复:1、3、4 已修;第 2 条我接受问题但未采用该修法——那个 baseline 参数在两条路径上都从未生效,因此删除并明确语义;第 5 条推迟到 #3234,它本就属于所有 control 操作共享的契约。另有两处你建议的实现我没有采纳,理由写在对应 thread 里。

方便时麻烦再看一轮。

AI 披露:本轮代码、测试与 thread 回复由 Claude Code 产出。我审阅了最终 diff 并对结果负责;每处修复都先让其测试因它自己的原因失败来验证。

@Astro-Han
Astro-Han requested a review from M4n5ter August 19, 2026 04:46
M4n5ter
M4n5ter previously approved these changes Aug 19, 2026

@M4n5ter M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

English

Approved on head 66f5d39 for the Goal-specific delta stacked after c759107 (#3198).

I reverified all five previous findings. The restart behavior, invalid budget input, and compatibility epoch are fixed on their real paths. Removing the inert creation-time token baseline correctly exposes the existing Goal budget semantics. The dispatched-interruption ambiguity remains real but is narrow, recoverable through the canonical Goal projection, and tracked across control operations in #3234; it is a non-blocking follow-up here.

No new P0–P2 issue was found. The automated composite-Session-key concern is a false positive: preload resolves the Desktop key to its Host scope and Host-local Session id before invoking goal:arm, then projects the returned identifiers back to Desktop keys. All checks on this head are green.

Scope caveat: this PR is explicitly stacked on #3198. This approval assumes #3198 lands first with its outstanding mode-pair authority fix, followed by the documented rebase; it does not supersede that review.

简体中文

批准 head 66f5d39 中叠加在 c759107#3198)之后的 Goal 专属增量。

我重新验证了上一轮的五个问题。重启行为、无效预算输入和 compatibility epoch 均已在真实路径上修复。删除创建时无效的 token baseline,正确显式化了既有 Goal budget 语义。Dispatch 后中断造成的结果不确定仍然真实存在,但触发窗口较窄,可以通过 canonical Goal projection 恢复确认,并已在 #3234 中按所有 control operation 的共用问题跟踪;这里降为非阻塞 follow-up。

未发现新的 P0–P2。自动审查提出的 composite Session key 问题是误报:preload 会先把 Desktop key 解析为对应 Host scope 和 Host-local Session id,再调用 goal:arm,最后才把返回标识重新投影为 Desktop key。当前 head 的所有 checks 均为绿色。

范围说明:本 PR 明确叠加在 #3198 之上。本 approval 假设 #3198 会先带着仍待修复的 mode-pair authority 问题合并,随后按说明 rebase;它不会覆盖 #3198 的 review 结论。

@Astro-Han
Astro-Han force-pushed the feat/goal-arm-from-composer branch 2 times, most recently from d3c680f to 77a5c1f Compare August 19, 2026 09:41
@Astro-Han
Astro-Han force-pushed the feat/goal-arm-from-composer branch 2 times, most recently from 1defc12 to 360b23e Compare August 20, 2026 02:05
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Heads-up on a cross-PR collision — not a review comment on your change.

RUNTIME_HOST_COMPATIBILITY_EPOCH is 27 on main, and three open PRs based on main each take it to 28 with different wire changes: #3236 (access credential prepare/finalize), #3199 (goal.arm), #3133 (session trace cursor pages). #3299 sits at 29 on the assumption that exactly one 28 lands.

The trap is that this does not conflict. All three branches write the same text to that line, so git's three-way merge takes it silently; only the adjacent comment block conflicts, and keeping both comments is the natural resolution. Each PR's own assert epoch > 27 still passes. The result is two incompatible protocols sharing epoch 28 — and since client/connection.ts compares with strict inequality, a matching epoch admits the peer, and the unknown operation then fails decode and tears down the transport, bypassing the structured incompatibility path the epoch exists to provide.

Please re-check against main immediately before merge rather than at rebase time; whoever lands second needs to re-bump. Filed #3313 to stop doing this by hand.

(Posted with Claude Code (Opus 5) assistance; the epoch values were read from each branch head.)

A Goal could only be armed by the model, from inside a Turn, with the
GoalSet tool. There was no Host operation for it, no bridge method, and no
control anywhere in the product — the user could stop a Goal but never
start one.

Add `goal.arm` as a real Host operation, next to `goal.query` and
`goal.control`, and carry it out to the + menu:

- `HostGoalCoordinator#arm` creates the Goal through the same GoalManager
  the tool uses, under the same Session admission gate, with the same
  durable record. It schedules nothing: a Goal armed outside a Turn takes
  hold on the next one, which `beginObservedTurn` binds to the live control
  lease. A Session that already has an unfinished Goal gets
  `operation_conflict`.
- The two budget ceilings and the token floor move to `@maka/core/goal`,
  so the protocol codec and the GoalSet tool schema validate against one
  number instead of two copies of it.
- Remote owners are granted `goal.arm` at the same tier as `goal.control`:
  withholding it would withhold nothing, since a remote owner sends Turns
  and the model arms its own Goal inside one. The only thing a refusal
  removes is the explicit path the user can see and stop.
- The IPC handler takes the Session from the scoped channel, never from
  the renderer's frame, and normalizes the budgets without clamping them —
  an out-of-range value is refused once, by the Host.
- The + menu gains one "设定 Goal…" row in its action group, at the same
  28px rhythm as the rest, and app-shell opens a dialog that collects the
  condition and the two budgets. The row explains itself when a Goal is
  already running or a Turn is in flight.

Generated-by: Claude Code
The GoalSet tool trimmed the condition in its own schema, so `goal.arm`
persisted surrounding whitespace that a model-created Goal never could.
Two callers each normalizing in their own way is the defect, so the trim
moves to `GoalManager.create` — the one place every caller reaches and the
only place the condition is stored.

Generated-by: Claude Code
Three places on the arm path restated what the protocol already owns:
`armGoal` declared a structural copy of the operation input, the dialog
carried the token-budget floor as a literal beside the constant this change
introduces for it, and the IPC validator rebuilt the frame from recognized
fields, silently dropping anything else.

The validator now refuses any key the frame does not carry. That is only
safe if both sides read one declaration of what it carries, so the shape
lives in `shared/goal-arm.ts`, which preload types its parameter with and
the validator derives its key check from. The Session is not in it: it
travels as the scoped IPC argument, so preload passes it separately rather
than restating it in the payload, and a renderer-side Session id is now a
compile error instead of a rejected frame at runtime.

Generated-by: Claude Code
`useSessionGoal` kept the old Session's running Goal until the new fetch
resolved, so switching Sessions briefly showed the + menu's Goal entry as
already armed for a Session that may have no Goal at all. The state now
resets with the Session rather than with the response.

Generated-by: Claude Code
`goal.arm` is a new wire operation, so a client that has it and a Host that
does not are no longer safely interoperable. Without a new epoch the pair
completes the handshake, the client offers Set Goal, and the first arm reaches
an older decoder as an unknown operation — a protocol error that tears the
connection down instead of refusing the pair up front and routing the user to
the Host upgrade path.

The epoch is the boundary the repository already uses for this: apache#3145 advanced
it for a new operation and apache#3165 advanced it merely for removing catalog
filters. Handshake fixtures name deliberately stale epochs to exercise
rejection, so they stay as they are.

Generated-by: Claude Code
…r recovery

`goal.arm` persists an active Goal whose contract is that arming alone starts
nothing: the Goal takes hold when `beginObservedTurn` binds the user's next
Turn. The durable record it writes has no current execution, which is also what
a Goal sitting between continuations looks like, so a Host restart handed it to
`recoverActiveGoal`, which built a continuation intent and scheduled it. A user
who armed a Goal and quit the app came back to it spending tokens on its own.

`status` cannot separate those two, but `iterations` already can: only a Turn
that settled while bound to the Goal raises it, so a Goal at zero has never
been carried by one. Recovery reads that instead, under the name
`hasCarriedTurn`, and leaves an uncarried Goal exactly where the restart found
it. A current execution is its own proof of carrying, so that branch is
unchanged.

This also covers a Goal that `GoalSet` created inside a Turn the Host never
lived to settle. That Turn is gone after the restart, so waiting for the user's
next one is the same answer, and it is the safe direction: the Goal stays
armed and visible instead of starting unattended.

Generated-by: Claude Code
`GoalManager.create` took a `tokensAtStart`, and both callers computed one:
`GoalSet` read the token count from its deps and `goal.arm` read the Host's
continuation cache. Neither ever reached a decision. `create` wrote the value
into `tokensAtStart` and `tokensNow` together, so the spend it implies is zero
whatever it holds, and the first settled Turn overwrote both from
`tokensBaselinePending` before any budget was compared. The parameter has been
inert since it was introduced.

Removing it settles what the token budget measures, which a review of the new
arm path asked for: the budget starts at the first Turn carrying the Goal and
covers everything the Goal drives after it. That Turn is one the user is
present for — `GoalSet` runs inside it, and an armed Goal takes hold on the
one the user sends next — while the budget exists to bound what runs
unattended. Naming an earlier baseline would need a token count read at the
moment a Turn binds the Goal, and there is no such read point today; arming
cannot substitute its own, because an unrelated Turn may be in flight and its
spend would be charged to a Goal that never caused it.

`#tokenCache` stays. It is what a settling Turn reports, not a baseline.

Generated-by: Claude Code
The two budget fields were Astryx `NumberInput`s carrying the Host's bounds.
That component only reports a value its own parse accepts and keeps anything
else to itself until blur, when it silently reverts — and the dialog's submit
guard read the condition alone. So a user who typed a 500-token budget armed a
Goal with `null`, which is not a smaller ceiling but no ceiling at all, and one
who typed 250 turns armed the Host's default instead. Narrowing a run's budget
was the one gesture that could widen it.

The fields now hold the user's own text and the dialog reads it, so what it
arms is what the form shows and an entry the Host would refuse blocks Start
with the reason beside the field. `readGoalBudget` repeats the operation's
bounds to say that locally rather than as a rejected frame; the numbers still
come from the shared Goal limits, and the copy takes them as arguments so no
sentence carries its own copy of a limit.

Verified end to end: the new e2e spec fails on the previous dialog with Start
still enabled under an invalid budget, and reads the armed Goal back from the
Host rather than watching the bridge call.

Generated-by: Claude Code
A Goal is born into one of two situations. The model sets one from inside a
Turn, so that Turn is already bound to it and settles into the Goal's first
continuation -- the loop is running from the moment the Goal exists. `goal.arm`
creates one from outside every Turn and deliberately starts nothing: it waits
for a Turn to take hold of it.

Recovery has to tell those apart, and until now it inferred the answer from
`iterations === 0`. That reads as "no Turn has carried it", which is true of
both: an armed Goal, and a model-set Goal whose birth Turn has yet to settle.
So the inference was right for the case it was written for and wrong for the
other one -- and `resumeFromControl` never consulted it at all, which is how a
resumed Goal could be given a continuation the restart afterwards refused to
put back. One question, two sites, two different answers.

The distinction is now on the Goal. `armedAt` is stamped once by `goal.arm`,
never cleared, and says how the Goal began rather than where it is now;
`isDrivingGoal` combines it with `iterations` to answer the only question
either site actually asks -- is this Goal in a loop that should be put back.
An armed Goal is not, until a Turn settles while bound to it.

The rule itself moves to the one place both callers pass through.
`restoreDrive` is what recovery and resume now share, so the coordinator no
longer keeps a copy of the predicate, a third restoration door would inherit
it, and `hasCarriedTurn` -- a name for the proxy, not for the question -- is
gone.

Generated-by: Claude Code
Session admission is a queue, and the composition begins to drain without
waiting for it to empty. `beginDrain` clears the Goal manager immediately, so
an operation that was admitted before the drain and is still queued behind
another one runs after that: it wakes up holding a lease on a composition that
is already shutting down.

Every other Goal operation survives that honestly. They read a manager that no
longer holds anything and answer `not_found`, which is what a drained Host has
to say. `goal.arm` is the one operation that answers by creating something --
it repopulated the map its owner had just emptied, flushed the new Goal to
durable storage, and reported success -- so the Goal outlived the composition
that was told to stop.

It now rechecks the drain inside the admitted callback, before any read or
mutation, and returns `host_draining` -- already one of the operation's
declared errors, and the same recheck-after-the-wait that `message-coordinator`
does at its own admitted entry points.

`GoalManager.dispose()` also becomes a disposal rather than a `clear()` that
leaves the object usable. Creating a Goal on a disposed manager is not a
smaller version of creating one, it is a mistake about lifecycle, so it throws
instead of quietly succeeding. The recheck above means no caller reaches it;
the throw is there so the next entry point that forgets fails loudly rather
than persisting a Goal nobody can stop.

Generated-by: Claude Code
…tests

apache#3249 removed `backend` from `CreateSessionInput`, so the four Session
fixtures this branch adds no longer compile against `main`. The field carried
no choice for a live build, and the store stamps every new header itself, so
the fixtures need nothing in its place.

Generated-by: Claude Code
@Astro-Han
Astro-Han force-pushed the feat/goal-arm-from-composer branch from 2902ecc to fa0dc52 Compare August 20, 2026 14:46
@Astro-Han

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (fa0dc52b6).

Two adaptations: #3332 took epoch 28, so goal.arm moved to 29 — the collision flagged above; #3249 removed backend from CreateSessionInput, so the four Session fixtures here drop that field.

Verified on the rebased tree: typecheck, build, Biome, Astryx inventory, plus runtime-host Goal 23/23, runtime Goal 82/82, desktop 33/33, @maka/ui composer 6/6.

@hqhq1025 both findings are answered in the threads above — re-review appreciated. @M4n5ter @jackwener @likun666661 a second pair of eyes is very welcome too, whenever you have time.

(Rebase and local verification run with Claude Code (Opus 5).)

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] The existing Resume-intent finding remains unresolved on this head.

At packages/runtime/src/goal-continuation.ts:421, resumeFromControl() returns an active Goal but restoreDrive() refuses to schedule it when armedAt exists and iterations === 0. This is a normal product path: arm a Goal, pause it before the first carried Turn, then press Resume. The UI promises that resuming continues autonomous iteration immediately and renders the Goal as running, but no continuation runs, and a restart preserves the inert state. The new regression at packages/runtime-host/src/__tests__/goal-coordinator.test.ts:743 asserts this mismatch.

Please persist explicit current drive/resume intent, or an equivalently simple phase, and use it for both Resume and recovery. The regression should cover pause -> Resume while admission is busy -> restart before currentExecution is recorded.

I did not open a duplicate inline thread because this is the existing unresolved hqhq1025 finding. I reviewed the complete revision; the required check and 73 focused local tests pass, but this P1 keeps this exact head from merge readiness.

Codex-assisted review performed under the maintainer-approved review workflow.

Resuming an armed Goal did nothing. `restoreDrive` refused any Goal with
`armedAt` and `iterations: 0`, and the control that sends the resume promises
the opposite — "Resuming continues autonomous iteration immediately". The Goal
came back `active`, the chip showed it running, and nothing ran until the user
typed again.

`iterations > 0` was standing in for "a Turn has taken hold of this Goal", and
it is not that fact. Iterations only rise when a carried Turn settles into an
evaluation, so the whole of a Goal's first Turn reads as untouched — and a
pause taken during that Turn discards the settlement, leaving a Goal that was
genuinely driving permanently indistinguishable from one nothing ever carried.

So record the fact instead of inferring it. `armedAt` now means the Goal is
waiting to be driven rather than how it was born, and the two events that end
that wait clear it: a carried Turn settling into a continuation, and the user
resuming. `isDrivingGoal` reads that one field. Both writes ride commits the
Goal already makes, so no revision moves that did not move before, and absence
still means driving — which is what every Goal persisted before arming existed
is.

The regression covers the case that has nothing else to fall back on: arm,
pause, resume into a busy admission, then restart before any execution is
recorded. Only the Goal itself can say the user asked for continuation.

Generated-by: Claude Code

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the current revision, including the fix for the prior resume/restart finding. The durable armedAt transition now distinguishes an armed Goal that is still waiting from one whose first settlement or explicit resume made it autonomous, and the recovery regression covers busy admission across restart. The repository build and 96 focused Goal tests passed, and the required CI check is green.

Codex-assisted review performed under the maintainer-approved review workflow.

@Astro-Han
Astro-Han merged commit 57e08d8 into apache:main Aug 20, 2026
1 check passed
@Astro-Han
Astro-Han deleted the feat/goal-arm-from-composer branch August 20, 2026 19:42
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.

3 participants