Skip to content

feat(assistant): coordinate work through a durable inbox bot - #6124

Merged
xxhZs merged 17 commits into
mainfrom
feat/assistant-inbox-bot
Oct 18, 2026
Merged

xxhZs merged 17 commits into
mainfrom
feat/assistant-inbox-bot

Conversation

@xxhZs

@xxhZs xxhZs commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Summary

The personal assistant now coordinates user messages and background work through a persistent Inbox Bot. Only SendUser publishes chat messages; the Bot can delegate work, manage indexes, handle task questions and results, and sleep until more input arrives. Ordinary model text does not become a user-facing reply.

  • Persist and batch Inbox delivery, make SendUser.messages idempotent, and finish Sleep only after successful tool persistence and the Host's finish guards.
  • Route discovery reports, task clarification/results, and index completion/failure feedback to the Bot. Give it direct index-management tools and owner-bound task controls.
  • Deliver current task Inbox contents before every model request, including amendments and Bot answers received while the task is running. Refresh the task files without advancing acknowledgement or settlement boundaries; this no longer depends on Recall finding a correction.
  • Add compact, differentiated assistant chat bubbles, avatars (emoji or image), and assistant-managed avatar updates.

Builds on #6004. The proactive exploration prompt itself is retained; discovery output is handed to the Bot for user communication.

Verification

  • Type checks passed for @maka/runtime, @maka/runtime-host, @maka/ui, and @maka/desktop.
  • Runtime Host build and desktop main build passed; git diff --check passed.
  • Assistant suites passed: memory network 76, initiative 23, proactive matters 78.
  • Selected runtime, Host/Bot, and UI regression suites: 387 tests. One Host integration fixture still used the retired SendUser.text shape; changed it to messages, then reran its entire 54-test composition suite successfully. The other 333 tests had passed in the initial run.
  • Desktop session-local suite: 79 passed.
  • Real gpt-6.1-sol / medium scenario tests used synthetic source records and simulated business time, with all user requests manually entered through the assistant. The scenario harness and data remain outside this repository. The main scenario included 24 historical sessions, 27 external records, two grouped indexes, three delegated tasks, a discovery heartbeat, scope corrections, clarification, selective pauses, next-day task execution, and scheduled incremental indexing.
  • A separate real-model delivery check captured two amendments and one Bot answer in subsequent model requests of the same task activation (approximately 4.57s, 3.25s, and 3.68s after their forwarding calls). These observations are not latency guarantees.

User-visible examples from the synthetic scenario:

Input or source change Observed result
Pause reimbursement, keep the handoff running, create two indexes, and answer a simple time conversion Paused only reimbursement, created one grouped index worker, and answered the conversion directly
User clarifies that their sister accompanies their mother Passed the correction to the existing exploration task; did not continue reporting a two-place scheduling conflict
School entry deadline changes; a previously recommended exhibition is cancelled Discovery read updated originals despite stale indexes; the Bot communicated both changes
Next-day handoff evidence and warehouse receipt arrive Existing tasks resumed, verified the evidence, completed, and notified the user
Pause discovery and the life index, retain work-index maintenance Discovery/life maintenance stayed paused; the work index completed its incremental checkpoint

Not run for this PR: full repository tests, full desktop packaging, and an additional visual browser/Electron smoke run.

Known behavioral limitations

  • Forwarding an unchanged “keep the existing tasks running” instruction woke two waiting tasks unnecessarily in the scenario. They were not duplicate tasks, but performed extra rounds.
  • The Bot can still over-confirm constraints or combine several findings into one long chat bubble. No key scenario finding was lost.
  • Live input is incorporated at the next model request; it cannot undo a tool that has already started or completed. Cancellation remains a separate control operation.

AI use

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

Tool(s) and scope: OpenAI Codex contributed implementation, tests, debugging, and this PR description. The commit includes a Generated-by: Codex trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally
    • Type checks and affected tests passed as listed above; whole-change lint/format checks were not run. The latest task-delivery edits were formatted.

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

Route assistant conversations through a persistent Bot loop with explicit user messaging and sleep. Connect task clarification and feedback, direct index management, discovery reports, and per-request task inbox delivery. Add assistant chat bubbles and avatar support.

Generated-by: Codex

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

First review of the full PR, abb2dec (merge-base; #6004 merged) through aae6420. One commit, 61 files, +3612/-169. About 1,880 added lines are product code and 1,730 are tests/fixtures. No linked issue and no maintainer discussion yet.

Findings

P2: A desktop message can permanently block the assistant outbox after an interrupted publish. appendConversation (execution-composition.ts:482) checks for a replay with JSON.stringify(existing) !== JSON.stringify(message). existing comes back from the session store, which re-canonicalizes user messages: content fields such as inlineReferences and displayText move after the envelope keys. The outbox copy keeps the original order ({...content, type, id, turnId, ts}). The desktop always sends inlineReferences, even if it is []. So if appendMessages commits and the process dies or refreshCanonical throws before store.delivered(), every later flush compares two equal messages with different key order and throws Assistant message identity conflict. flush() goes through pending rows in order and run() calls flush() first, so that one row blocks the client's retry, every later user message, SendUser delivery, and the Bot loop. Recovery needs manual DB repair.

I reproduced this against the real Host composition. I changed the existing "assistant messages enter a durable Inbox" test to send inlineReferences: [], then reset that outbox row to delivered=0 to simulate the crash window. The retry and a later message with a new id both failed with internal_failure: Assistant message identity conflict. With plain { text } content, the same steps succeed. The unit fixture in assistant-bot.test.ts mocks appendConversation, so the "ambiguous publication" test never reaches this code. Suggested fix: compare canonical forms on both sides (for example, run both through decodeCanonicalMessage, or compare id/type/text plus normalized content), and add a Host-level test with inlineReferences.

P3: Inbox and outbox grow without bound. Rows in inbox, outbox and outbox_batches are never pruned. pendingVisible() filters on delivered=0 without an index, and the 1 s tick() calls it on every pass. The single long-lived Bot worker Session also keeps growing. This won't break anything soon, but it needs a retention or compaction plan before this becomes a persistent background feature.

P3: The wake wait has no bound. For a queued wake (controller.ts:189), run() polls JSON.stringify(await agent.transcript()) every 25 ms until the wake id shows up. There is no timeout, and the cost is O(transcript) per poll. If a queued follow-up is dropped or cancelled, running never clears and the Bot stalls until restart. I did not reproduce a dropped follow-up; this is from reading the code.

P3: Undocumented change to task concurrency. proactive-matters/controller.ts:104 raises concurrent task runs from 2 to 15. The PR body doesn't mention it, and it changes resource and provider-rate behavior a lot. Please explain it or move it to its own change.

P3: Untrusted content and authority (design note, not a regression). Inbox writers are host-gated. User messages arrive only through turn.message.submit on the bound owner Session. Discovery posts are checked against discoverySession. Task posts require an active, unsettled activation with a recorded delegation. Bot tools reject any caller other than the worker, and only the worker gets Matter*/Memory*/Initiative* calls remapped to the owner. I found no path for another session to post or forge user_message items. However, discovery reports and task results that carry externally sourced text reach a Bot that can call MatterDelegate and the index-control tools. Only the prompt line "background content is not new user authorization" separates them. Moving discovery out of the owner chat is an improvement over #6004. A non-prompt guard, such as requiring a user_message in the current batch before MatterDelegate, would be worth considering.

P3: Scope. This PR combines the Inbox Bot core, new protocol dispositions, task step-context delivery, clarification (MatterAskBot/MatterReply), discovery isolation, index tooling, avatars (two new API methods and image fetching), and UI bubble restyling. Avatar/UI and the step-context change could be split out to make review easier. This has no runtime impact.

Protocol / compatibility epoch

The PR changes protocol/index.ts, message.ts and personal-assistant.ts. It adds turn.message.submit disposition inbox, execution-query state inbox, and the methods assistant.avatar and assistant.avatar-image. Older clients would reject the new disposition, so the bump is needed. It declares RUNTIME_HOST_COMPATIBILITY_EPOCH = 223. Current main (1751c39) is at 222, so this is the next number. However, open PRs #6115 and #5980 also declare 223. Whichever merges second must rebase to 224 (P3).

Verification done here

  • Built core, storage, mcp, runtime and runtime-host at the head.
  • assistant-bot, assistant-memory-feedback and execution-composition: 73/75 passed. The two WorkHub failures ("steering preserves question policy…", "Resume and Stop follow logical lineage…") passed in isolation and on 3/3 reruns on both head and merge-base. They are timing flakes, not caused by this PR.

CI / mergeability

GitHub reports MERGEABLE and the tree merges cleanly onto current main. State is BLOCKED, waiting on a required review. The label and test checks are still queued at this head, so there is no CI result yet.

(row) => row.id === message.id,
);
if (existing) {
if (JSON.stringify(existing) !== JSON.stringify(message))

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.

P2: existing is re-canonicalized by the session store (content fields such as inlineReferences/displayText move after type/id/turnId/ts), but message keeps outbox key order. The desktop always sends inlineReferences. So after a crash or refreshCanonical failure between appendMessages and store.delivered(), this check throws on every retry. Because flush() stops at the first pending row, the assistant outbox and Bot are blocked permanently. I reproduced it in the real composition test using inlineReferences: [] plus a reset delivered flag: the retry and a later new message both returned Assistant message identity conflict. Please compare canonical forms on both sides.

if (!['turn_started', 'followup'].includes(admitted?.disposition))
throw Error('Bot wake was not admitted');
// A queued wake is not complete until its message entered the worker history.
while (

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.

P3: this transcript poll has no deadline and serializes the whole worker transcript every 25 ms. If the queued follow-up never appears (for example, it is cancelled), running stays set and the Bot stalls until restart. Consider a bounded wait that records an error, or an admission-acknowledgement signal.

this.store.enqueueDue();
for (const m of this.store.list()) {
if (!this.active || this.runs.size >= 2) break;
if (!this.active || this.runs.size >= 15) break;

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.

P3: concurrent task runs go from 2 to 15, which the PR description doesn't mention. Is this intended? It significantly changes resource and provider-rate load.

@github-actions github-actions Bot added the effort/XXL Over 2500 readable lines label Oct 15, 2026
Track wake submission IDs through Runtime execution state instead of scanning transcripts. Preserve pending submissions across recovery and release cancelled waits without losing Inbox entries. Compare canonical message values when replaying the outbox.

Generated-by: Codex

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review: aae6420 to 712d531. This adds one commit, "fix(assistant): reconcile durable wakes and canonical outbox retries" (8 files, +352/-31). There was no rebase, and the merge-base is still abb2dec. I reviewed the full PR in the previous round.

Prior findings

  • P2 (outbox identity conflict): fixed. appendConversation now compares isDeepStrictEqual(decodeCanonicalMessage(existing), decodeCanonicalMessage(message)). I reproduced the original bug against the real Host composition using desktop-style content: a /skill: token in text, a different displayText, and a non-empty inlineReferences. The test commits the canonical row, resets the outbox row to delivered=0 to simulate the crash window, and retries. The retry now succeeds, the outbox drains, and a later message with a new id is admitted normally.
    I also checked that the new comparison is not too loose. With the same id, I put each of these into the pending outbox row: a key-reordered copy, a changed displayText, a changed inlineReferences[0].label, dropped inlineReferences, a changed ts, a changed turnId, and an extra unknown key. Only the key-reordered copy was accepted. All the others were rejected: the extra key with "Invalid stored message schema", the rest with "Assistant message identity conflict". The decoder checks exact shape and only normalizes representation, so I see no path where a different message is silently accepted.
  • P3 (wake wait without a bound): mostly addressed. The wake now gets a durable messageId before submission, and reconciliation goes through turn.message.execution.query once per second instead of scanning the transcript every 25 ms. A cancelled state ends the wait, and transient query failures are retried on the next tick. What remains is covered by the second new P3 below.
  • Still open, unchanged: unbounded growth of inbox/outbox/outbox_batches plus the unindexed delivered=0 scan (P3); task concurrency raised from 2 to 15 without being mentioned (P3); background content reaching the Bot with only a prompt guard (P3, design); scope (P3).

New findings

P2: CI fails on this PR's own change. The test job fails in "Check locale hygiene" with packages/ui/src/composer.tsx: locale-literal-compare 0 -> 1, at line 2516: label={locale === 'zh-CN' ? '聊天选项' : 'Chat options'}. That line was added in aae6420 (CI had not run on that head during the last review), and main has no such branch in composer.tsx. The fix is to move the string into the UI catalog.

P3: A crash in the middle of a Bot turn now leaves a stale owned wake, and restart turns it into an error. The wake row is written before submission and cleared only after whenIdle(). If the Host stops while the Bot is working (after the wake is owned, before Sleep), run() on restart finds the persisted wake. It asks for its state, gets owned, skips submitting a new wake, and whenIdle() returns immediately. It then throws "Bot ended without Sleep" (or "Bot stopped: aborted"). I reproduced this with the real BotController/BotStore and a stub agent that reports owned plus idle: 0 submissions, error set, and head 1 vs deliveredThrough 0. The pending mail then waits until some new Inbox post clears the error.
At aae6420, a restart with an idle worker simply submitted a fresh wake, so this is a regression in the restart path. I did not confirm against the real Host which worker status an interrupted turn reports after restart, hence P3. A possible fix: when the persisted wake is owned but the worker is not running and sleepTurn is unset, treat the wake as consumed and submit a new one rather than raising an error.

P3: An omitted resolution is treated as a fatal error. queryMessageExecutions deliberately leaves out an id whose submit is still in #pendingSubmits; omission means "cannot say yet". The controller then reads resolutions[0] as undefined, which falls through to Unexpected Bot wake execution state: undefined. That error is raised outside the inner try, so it becomes a sticky error instead of a retry. The window is narrow, but undefined should be handled like pending.

The rest of the delta looks fine. PluginAgentRuntime.messageExecution is optional and checks the ownership tree (a test covers rejection from outside the tree). followup now passes a caller-chosen messageId through to submitAgentMessage. The new wakeError status field is cleared on success and on an empty Inbox.

Protocol / compatibility epoch

The delta does not touch protocol files. The PR still declares epoch 223, and main is still at 222. Open PRs #6115 and #5980 also still declare 223, so whichever of them merges second must move to 224 (P3, unchanged).

Verification done here

  • Built core, storage, mcp, runtime and runtime-host at 712d531.
  • assistant-bot: 22/22 passed. Execution-composition "durable Inbox" and "Plugin Agent snapshot": 2/2 passed. plugin-agent-service: 4/4 passed.
  • The desktop-content and variant repro described above was run in the real Host composition test. The stale-wake repro was run against the compiled controller.

CI / mergeability

test fails (locale hygiene, caused by this PR, see above). GitHub reports MERGEABLE, state BLOCKED pending a required review.

Comment thread packages/ui/src/composer.tsx Outdated
</DropdownMenu>
</span>
) : null}
<ComposerConfiguration compact={props.compactControls} label={locale === 'zh-CN' ? '聊天选项' : 'Chat options'}>

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.

P2: This new locale === 'zh-CN' branch fails the required locale-hygiene check (locale-literal-compare 0 -> 1), so the test job is red. Please move the label into the UiCatalog.

if (['blocked', 'waiting_for_user'].includes(status))
throw Error('Bot requires attention in its worker Session');
let wake = this.store.get<{ messageId: string; text: string }>('wake');
if (wake || status !== 'running') {

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.

P3: After a Host crash in the middle of a Bot turn, the persisted wake reports owned. This branch then skips submitting a new wake, whenIdle() returns at once, and run() throws "Bot ended without Sleep", so the pending mail waits for the next Inbox post. I reproduced this with BotController/BotStore and a stub agent (owned + idle): 0 submissions, error set. At aae6420 a fresh wake was submitted in this case. Consider treating an owned wake as consumed when the worker isn't running and sleepTurn is unset.

this.store.set('wake', null);
throw Error('Bot wake was cancelled; Inbox is preserved');
}
if (!['pending', 'not_admitted'].includes(resolution?.state))

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.

P3: queryMessageExecutions leaves out ids that are still in #pendingSubmits ("cannot say yet"). That makes resolution undefined, which ends up here as a sticky error. It should be retried like pending.

xxhZs added 4 commits October 16, 2026 13:47
Separate enabling general discovery from changing its cadence, preserve existing schedules, and report mutation scope explicitly. Align Bot guidance and task/index tool descriptions around work, configuration, and notification responsibilities.

Generated-by: Codex
Keep reasoning choices with the Bot, document inbox pagination and sleep behavior on tools, and clarify existing-task communication and recovery capabilities.

Generated-by: Codex

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

I reviewed the increment from 712d531 to d6ff911: three additional commits, 12 files, +178/-45, no rebase. Both heads have merge-base abb2dec. This increment separates discovery enablement from cadence changes, revises Bot/tool instructions, and adds mutation receipts and tests.

P2 — The existing CI failure is still open, and formatting also needs repair. The current run fails at the same packages/ui/src/composer.tsx:2516 locale comparison reported previously. That file is unchanged by these three commits. I independently reproduced locale-literal-compare 0 -> 1 with node scripts/check-locale-hygiene.mjs --base 331340aba5c7de5fc7d53e2441123f40f03ba34b; move the label into the UI catalog.

After that fix, CI will also reach a failing npm run format:check. The new .describe(...) chain at packages/runtime-host/src/assistant/proactive-matters/delegation.ts:188 is not Biome-formatted; the same file at 712d531 matches its formatter output exactly. The full formatting check reports six files: this new failure plus five already-unformatted files at the previous head (assistant/bot/controller.ts, assistant/bot/host.ts, assistant/bot/store.ts, assistant/service.ts, and server/execution-composition.ts). Run npm run format and commit the formatting changes as well. These are additional requirements to close the existing CI finding, not a separate runtime defect.

I found no additional demonstrated P0–P2 runtime defect in the increment. InitiativeEnable({}) now preserves the saved interval and an enabled schedule's next wake/revision; InitiativeConfigure preserves enabled/paused state and rejects extra, missing, fractional, and out-of-range arguments. The existing active-heartbeat fence remains in index-initiative/store.ts:99. I exercised the registered tools through the assistant fixture, including foreign-session rejection, configuring a paused schedule, repeated enablement during a running heartbeat without losing its identity, rejecting configuration during that heartbeat, and normal completion afterward. Bot capability routing includes the new tool. Task and index receipts describe mutations without changing their existing execution paths.

The earlier outbox canonical-comparison repair and the previously reported Bot wake-recovery paths are unchanged; these commits do not address the two wake-recovery P3 concerns. The revised instructions also do not establish a non-prompt authorization boundary for background content. I did not rerun the author's live-model scenarios, so deterministic tests do not establish how reliably the shorter Bot prompt handles ambiguous or untrusted messages.

Verification: normal npm run build:test; all 178 assistant tests (76 memory-network, 24 initiative, 78 proactive-matters); 29 Bot/feedback/Plugin Agent tests; the registered-tool probe above; Runtime Host typecheck; changed-source lint; Windows test inventory; and git diff --check passed. Locale hygiene and full formatting failed as described. I did not rerun the full repository suite or desktop UI.

No protocol delta in these commits; the PR still declares epoch 223, while current main 331340a declares 222. A static merge onto that main is conflict-free, but I did not execute the merged tree. At the reviewed head, test is failed and GitHub reports MERGEABLE/BLOCKED; the CI finding remains unresolved.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

I reviewed all 35 files in d6ff911 → 788bbb1: five additional commits, +863/-78, no rebase. This adds wake recovery, authored message summaries and typing status, shares source refreshes across index listings, and broadens browser background access.

P1 — Ordinary Sessions bypass the browser's visible-lease security boundary. browser-ipc-main.ts:102 now grants background access to any selected Session on an active Host, not just the reserved WorkHub coordination Session. A selected ordinary Session remains selected when its parent view/window is hidden or minimized. canDrive returns before the visible/viewport checks, and visibility changes no longer revoke its in-flight action. The inline comment includes the reproduction and fixes. This contradicts the explicit security policy in browser/logic.ts:99–107, including its warning about off-screen reads of logged-in pages. Renderer ownership is not user-visible authorization.

I exercised the registered IPC and actual withBrowserPage gate with the existing test's fake native/CDP endpoints: observe, navigate and mutate callbacks all ran in each of hidden-parent, hidden-window and minimized-window states. Replacing only the IPC module with the actual d6ff911 implementation blocked all nine actions. A collapsed panel additionally permits mutation without a viewport in the new implementation; the previous implementation blocks that mutation. The committed test explicitly changes false expectations to true and replaces the in-flight revocation assertion with successful completion (browser-ipc-main.test.ts:223–257). This demonstrates admission/revocation behavior, not a live authenticated-browser exploit.

P2 — New mandatory-check failures remain after the old CI repairs. The prior locale comparison is correctly replaced by assistantChatCopy(locale).chatOptions, and both locale hygiene and full formatting now pass locally. However, the Astryx inventory check rejects the new raw button in packages/ui/src/message-summary.tsx:37. Also, the changed SendUser.messages schema requires objects while three assistant integration tests still send string arrays. The assistant command returns 179 passes and three failures. Converting only the fixture's legacy arguments to {text} makes the affected four integration cases pass. These need updating before the normal test chain can succeed.

The two earlier wake-recovery P3 findings are addressed: an interrupted owned wake gets a fresh persisted identity, and an omitted execution resolution is retried. The four new recovery cases pass at this head and all four fail when only the actual previous BotController module is loaded. Summary text survives canonical decoding, ledger backfill, projection and materialization in the focused tests. The 80 memory-network tests, including the new shared-source concurrency/failure/scope cases, pass without advancing index coverage or starting workers during listing.

Verification: normal npm run build:test; 109 focused browser IPC, Bot, Host composition, runtime-event and UI summary/materialization tests; Desktop's four TypeScript configurations; Windows test inventory; locale hygiene; full formatting; and git diff --check passed. The assistant suites total 182 tests: 179 passed, three failed as described. I did not rerun the full repository suite, real-model scenarios, or native desktop UI. No additional protocol-file or database-schema delta; the PR still declares compatibility epoch 223 versus main's 222. At this head, GitHub reports MERGEABLE/BLOCKED and CI run 38878148790 is queued, not successful. The P1 and local mandatory-check failures need resolution before merging.

if (!owner || !browserParentForRenderer(owner)) return false;
const ref = parseDesktopSessionResourceKey(sessionId);
return isWorkHubCoordinationSessionId(ref.sessionId) && deps.isHostActive(ref);
return deps.isHostActive(ref);

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] Preserve the ordinary-Session browser visibility gate

Removing the coordination-Session predicate grants every selected ordinary Session a background action lease. A renderer can remain selected while its parent view or native window is hidden/minimized. createBrowserViewHost.canDrive then returns true before checking visibility/viewport, and revokeHiddenActions no longer severs its running action. An agent can therefore read or mutate an authenticated page that the user cannot see, contrary to the explicit security policy in browser/logic.ts:99–107.

Using the registered IPC and actual withBrowserPage path with the existing native/CDP fixture, I observed all three action callbacks execute with the ordinary owner hidden, its window hidden, and its window minimized. Loading only the actual d6ff911 IPC module blocks all nine with BrowserActionBlockedError. Collapsing the panel also newly admits mutation without a viewport. The changed test now expects these hidden actions to succeed and removes the revocation assertion.

Keep background access restricted to WorkHub coordination as before, or explicitly revoke ordinary-Session access when the view/window is hidden, minimized or the panel is collapsed. Retain negative tests for hidden reads, mutations and in-flight revocation; selection/Host identity alone is not an equivalent security boundary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The feature owner has explicitly confirmed that background browser operation includes a hidden or minimized Maka window, as well as a collapsed browser panel. This does not exceed the requested product scope; the changed admission expectations are intentional. We will retain this capability rather than restore visibility-based denial. This does not authorize arbitrary unselected sessions: selected-session ownership, renderer lifetime, active Host and epoch checks still apply. The old visibility-only policy comment needs to be reconciled with this intentional policy change; it should not be treated as evidence that the requested behavior itself is accidental.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Astro-Han Browser operation while the Maka window is hidden or minimized, or the browser panel is collapsed, is intended product behavior. Please re-review the browser finding on that basis, including whether the remaining selected-session ownership, renderer lifetime, active Host and epoch checks are sufficient for this intended capability. This clarification does not claim that the separate Inbox admission race or architecture-check failures have been fixed.

Comment thread packages/ui/src/message-summary.tsx Outdated
<div className="maka-message-summary">
<div className="maka-message-summary-preview"><Markdown text={summary} density="compact" /></div>
<div id={bodyId} className="maka-message-summary-body" hidden={!expanded}>{children}</div>
<button

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.

[P2] Restore the mandatory checks for the new summary component and SendUser shape

npm run astryx:surface-inventory fails on this new raw <button>: Astryx owns this primitive, so the checker requires Astryx Button (and its documented variants) rather than a hand-written control/custom-control styling. This is a mandatory CI step, independent of the now-fixed locale/format checks.

The normal assistant test chain also still uses legacy string arrays at proactive-matters/test/assistant-loop.test.ts:44,190 and source-assistant-loop.test.ts:149, while SendUser now validates {text, summary?} objects. The unchanged tests fail (179/182 pass); adapting only their fixture arguments to objects makes the four affected integration cases pass. Please migrate those callers as well as replacing this control, then rerun both mandatory checks.

@me2seeks me2seeks 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.

One actionable P2 remains at this head. Please address the durable Inbox admission race described in the inline comment so a message cannot be reported as failed after it has already been persisted for later processing.

},
appendConversation: async (sessionId, message) => {
await sessionAdmission.run(sessionId, async (lease) => {
if (!personalAssistant.isConversation(sessionId) || !personalAssistant.enabled)

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.

[P2] Keep durable Inbox admission consistent with the visible receipt

BotController.receive() persists the Inbox/outbox row before flush() calls appendConversation. That callback waits for sessionAdmission, then checks personalAssistant.enabled here. assistant.set-enabled(false) clears this.runtime without taking that admission gate. If an append is queued behind an existing admission and reaches this check after disablement, it throws even though the Inbox row is already durable; turn.message.submit returns internal_failure.

While disabled, hasInboxMessage() returns false and the transcript row was never appended, so the execution query can report the message as not admitted. Re-enabling starts a Bot that can flush and process the original row after the client has treated it as failed; retrying with a new message ID can process it twice. Serialize disablement with pending append/admission, or make the durable Inbox receipt authoritative and queryable across disablement.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.Incremental review: 788bbb16..e781f3f6, one commit (+19/-19, six files).

P1, still open: hidden ordinary sessions can drive the browser. This commit does not touch the browser code. apps/desktop/src/main/browser-ipc-main.ts is unchanged since 788bbb16, so the P1 in our previous review stands.

P2, partly fixed; CI test still fails because of this PR. The two items from last round are fixed:

  • MessageSummary now uses Astryx Button (ghost, sm), and the surface inventory lists the file.
  • The three SendUser fixtures now pass { text } objects.

CI now gets further and fails at check:architecture. That run uses the merge with current main (2c429077, which records AppShell at 7280 tokens):

  • src/renderer/chat-message-surface.tsx imports ./features/personal-assistant/typing.js directly. Feature imports must go through the feature's index, so re-export PersonalAssistantTyping from features/personal-assistant/index.tsx and import it from there.
  • This PR adds new dependencies and a new hook call to chat-message-surface.tsx and adds 52 tokens to app-shell.tsx. After fixing the import, rebase onto main and refresh apps/desktop/renderer-architecture.json so the ledger records these intended changes.

The removed hand-written toggle CSS is covered by the Button's ghost styling. The rotate rule still targets the chevron through the wrapper.

useUiLocale,
type LiveTurnProjection,
} from '@maka/ui';
import { PersonalAssistantTyping } from './features/personal-assistant/typing.js';

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.

P2: check:architecture rejects this direct feature import (feature imports must use index). Re-export PersonalAssistantTyping from ./features/personal-assistant/index.js and import it from there.

@Astro-Han

Copy link
Copy Markdown
Contributor

@xxhZs Thanks for pushing this forward! The Inbox and SendUser changes feel much closer to the Bot discussed in #6076.

One thought as I was reading: could this and WorkHub gradually become the same thing? They both receive requests, delegate work and bring results back. I’d love to end up with one Bot I can talk to from the desktop or IM, and let it manage the Sessions underneath.

I also feel there are quite a few concepts and special flows now, across both implementations. I’d be happy to drop more of them—for example, I don’t need every task to go through a separate model review or a draft/checkpoint/settlement process. If the existing Host, Session and scheduling capabilities can do the job, I’d prefer to build on those and remove the extra machinery.

No need to fit all of this into this PR. I mainly wanted to compare notes on the direction and see what could disappear as the two come together. What do you think?

中文版

@xxhZs 感谢推进!Inbox 和 SendUser 这部分,感觉已经更接近 #6076 里讨论的 Bot 了。

我看下来有个想法:这套是不是可以逐渐和 WorkHub 合成一个?两边都在接收要求、委派工作、拿回结果。我希望最后就是一个 Bot,桌面和 IM 都能找它,底下的 Session 由它管理。

另外,两边现在的概念和专用流程都有点多,我觉得可以再大胆删一些。比如我不需要每个任务都经过独立模型审核,也不一定需要草稿、checkpoint、结算这一整套流程。现有 Host、Session 和调度能力能解决的,就尽量复用,把多出来的机制去掉。

不用都放进这个 PR,我主要想先聊聊这个方向,看看两边合到一起时,哪些东西可以顺便删掉。你觉得呢?

@xxhZs

xxhZs commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han Yes, the long-term plan is to bring the assistant/Bot and WorkHub together as one. There are quite a few design decisions and changes involved at the moment, so we are keeping them separate for now and will gradually merge them over time.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by automated review agents. It is not an independent human review and does not replace one.

Adversarial review of e781f3f69c7e168dbc4d7ef363ef4a7779d5cb28. Four reviewers took separate angles: browser background operation, persistence and recovery, bot tool permissions and untrusted input, and CI/protocol/UI. Every P0–P2 below was reproduced on this head, or confirmed against the code where noted. The PR is mergeable, but CI test fails.

On the browser change (bac660d): the maintainers accept that a selected Session may operate the browser while the window is hidden or minimized or the panel is collapsed, as intended product behaviour. We are no longer reporting that as a defect, and our previous P1 on it is withdrawn. We reviewed the remaining guards on that basis; see the P2 below.

P1 — Every ChatView loses its empty state and its "load earlier" control

packages/ui/src/chat-view.tsx:854 and :1062 add {props.activityContent} as a second child of ChatMessageList, after the expression that is deliberately kept as a single child. When activityContent is undefined, the children are [null, undefined]. ChatMessageList treats that as non-empty and drops emptyState. This affects every session, not only the assistant:

  • The new-task hero and prompt suggestions disappear.
  • A session whose loaded window has no visible rows loses its only "load earlier history" control.

Two existing tests fail on this head:

  • packages/ui chat-view-load-earlier.test ("offers to load earlier history even with nothing to show")
  • apps/desktop chat-view-optimistic-render.test ("ChatView shows the empty state when there is neither a bubble nor a running turn")

Deleting only those two lines makes both pass, and restoring them brings the failures back. Render the activity inside the single child expression, and count a present activityContent as content when computing the empty state.

P2 — The old Session keeps background browser access while a switch is pending

The renderer sends browser:active-session only after the new Session's Host identity resolves. createBrowserSelectionCoordinator.setActiveSession (apps/desktop/src/preload/browser-selection.ts:63-84) calls sink.show in resolved.then(...) and sends nothing to main beforehand. Until then, main keeps the previous selection. Before this PR, a hidden previous Session could not act anyway. Now that hidden ordinary Sessions may act (browser-ipc-main.ts:102), that interval lets the Session the user just left keep observing, navigating and mutating the browser in the background. We reproduced this with the real preload setActiveSession, the selection coordinator and the registered IPC: while B's resolution was pending, a hidden and collapsed A still executed browser_click. The CDP page was a counting fixture, not a real account. The window lasts as long as identity resolution; on a profile-map miss that includes a runtimeHostScopeList() round trip.

Fix: when the selection changes, revoke the previous selection immediately, for example by sending a hide/relinquish for the old generation before resolving the new Session. Then show B when it resolves.

Related P3 (docs): the repository's browser visibility policy still says actions must happen in front of the user. Please update it to describe the new background-operation rule and the guards that remain.

P2 — CI test fails because of this PR

  • check:architecture (the current CI failure):
    • chat-message-surface.tsx imports ./features/personal-assistant/typing.js directly. Re-export it from the feature's index.
    • Even after --write, the --strict-base ratchet still rejects two things. app-shell.tsx nonTriviaTokens grows by about 52: move the assistant-specific className/compactControls/placeholder logic out of app-shell.tsx. chat-message-surface.tsx gains a new hook call (useAssistantAvatar): move it into a feature-owned component.
  • Runtime Host test, not yet reached in CI. personal-assistant.test.ts "native tools and APIs initialize without installing any plugin or invoking a model" expects assistant.binding to return exactly {state: null}. This PR adds a bot block and does not update the test.

P2 (me2seeks's finding, corroborated by code reading) — Disabling the assistant can still admit one message

appendConversation checks personalAssistant.enabled once before its awaits (execution-composition.ts:479-482). BotController.flush() (controller.ts:108-125) marks the row delivered afterwards. A set-enabled(false) that lands during publishBotConversationMessage therefore lets that message become durable and be acted on after re-enable. At most one message gets through, because the next iteration's ready() throws. Re-check enabled, or whether the admission lease has been revoked, before committing; or make disable wait for in-flight admissions.

P3

  • Epoch 223 is also declared by open #6115, #6091 and #6090, so whichever merges second must move. The 223 comment should also list assistant.avatar/assistant.avatar-image, the bot block on assistant.binding, the inbox execution-query state and the summary field.
  • Provenance: connector-delivered text enters both the bot inbox (source: 'user') and the conversation (type: 'user') with no sender identity (controller.ts:96-103). Inside this diff, all generated content is tagged correctly; the risk depends on upstream connectors accepting non-owner senders. Consider a distinct source for externally delivered text. Similarly, MatterTaskMessage stores bot-authored amendments as source: 'user' (delegation.ts:305-311); a coordinator source would let executors tell them apart.
  • Initiative config: index-initiative/controller.ts:167-190 binds the config owner to the first caller when the store is empty, and DiscoveryReport ignores the post result (host.ts:63). Binding from the resolved assistant conversation at start would be safer.
  • AssistantSetAvatar (bot/avatar.ts:74-90) fetches any URL or local path the worker chooses. Consider constraining origins and paths in readAvatarImage.
  • Renderer:
    • app-shell.tsx:1370 uses an inline zh-CN ternary, so zh-TW gets English.
    • The avatar and typing aria-labels are Chinese-only.
    • avatar.tsx:60 sets a fresh error state every 3 s while the Host is unreachable, re-rendering the transcript.
    • The compact composer settings <details> has no Escape/outside-click dismissal.
    • The typing indicator shows for any Bot turn, not only replies.
  • Compatibility: nothing released is affected; old 222 peers are rejected by the epoch. The string-array branches in queueVisibleBatch only serve data from earlier commits of this PR and are unreachable under the strict SendUser schema, so they can be deleted.

Checked and not a problem

  • Bot-delegated clarification routing works. The assistant/service.ts:126-148 wrapper maps worker sessionId to the owner for all Matter*/Initiative*/Memory* tools, so MatterAskBot posts to the owner conversation. Verified by executing the real wrapper and bridge.
  • A crash between the conversation append and the delivered mark does not duplicate entries. publishBotConversationMessage dedupes by refs.storedMessageId (bot/conversation.ts:44-46).
  • There is no migration risk. The bot store is a new database file under personal-assistant/, and all tables use CREATE TABLE IF NOT EXISTS.
  • Other guards hold: the worker tool ceiling, SendUser has no target parameter, MatterReply/MatterAskBot identity anchoring, and the ownership checks on task message, adopt and feedback-retry.
  • Passing locally: lint, format, locale hygiene, ASF headers, e2e budget, Windows inventory, protocol-epoch check, Astryx inventory and theme, build, typecheck, and knip.

Comment thread packages/ui/src/chat-view.tsx Outdated
{tail}
</>
) : null}
{props.activityContent}

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: This adds a second child after the deliberately single null child (see the comment above). When activityContent is undefined, the children become [null, undefined], ChatMessageList treats the list as non-empty, and emptyState (the hero and the only load-earlier control) never renders. This happens in every session. Repro: chat-view-load-earlier.test.js and desktop chat-view-optimistic-render.test.js both fail on this head and pass with these two lines removed.

Comment thread packages/ui/src/chat-view.tsx Outdated
)}
</>
)}
{props.activityContent}

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: Same issue in the transcript branch. When showEmptyState is true, this extra sibling suppresses emptyState, which holds loadEarlierHistoryControl.

useUiLocale,
type LiveTurnProjection,
} from '@maka/ui';
import { PersonalAssistantTyping } from './features/personal-assistant/typing.js';

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.

P2 (check:architecture): Import this through features/personal-assistant/index.js. Even after that change and --write, the strict-base ratchet still rejects the new useAssistantAvatar hook call here (hookCalls 4 -> 5) and the app-shell.tsx token growth (7285 -> 7337). Move that logic behind a feature-owned component.

Comment thread apps/desktop/src/renderer/app-shell.tsx Outdated
activeSession={activeSessionForView}
compactControls={activeSessionForView?.labels?.includes('personal-assistant')}
placeholder={activeSessionForView?.labels?.includes('personal-assistant')
? (uiLocale === 'zh-CN' ? '聊点什么…' : 'Send a message…') : undefined}

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.

P3: The inline locale ternary sends zh-TW to English and grows app-shell.tsx tokens, which the architecture ratchet rejects. Use a catalog entry owned by the feature.

// interoperability. Mismatches are rejected before domain commands are admitted.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 222 as const;
// 223: Assistant conversation message admission can return an Inbox receipt without a model Turn.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 223 as const;

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.

P3: 223 is also claimed by open PRs #6115, #6091 and #6090. The comment should also list assistant.avatar, assistant.avatar-image, the bot block on assistant.binding, the inbox execution-query state and the optional text summary.

? old : { scope: sessionId, avatar });
} catch {
// A disconnected Host must not display another Host's identity.
if (live) setState({ scope: sessionId, avatar: DEFAULT_ASSISTANT_AVATAR });

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.

P3: On Host failure this sets a fresh object every 3 s. That changes the assistantAvatar element and re-renders every memoized TurnView. Dedupe like the success path does.

await sessionAdmission.run(sessionId, async (lease) => {
if (!personalAssistant.isConversation(sessionId) || !personalAssistant.enabled)
throw Error('Assistant conversation is unavailable');
const header = await stores.sessionStore.readHeaderSnapshot(sessionId);

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.

P2 (me2seeks's finding, corroborated by code reading): The enabled guard is read once, before the awaits, and never re-checked, and the caller (BotController.flush(), controller.ts:108-125) awaits appendConversation before marking the outbox row delivered. So the interleaving the code permits is: flush enters appendConversation with one pending row; assistant.set-enabled(false) lands inside await publishBotConversationMessage(...); the guard is not re-read, so both durable writes complete and the row is marked delivered; the user_message is then consumed by seq on the bot's next run, i.e. after re-enable. Bounded to at most one message, because the next loop iteration's this.ready() throws post-abort. I did not build or run a test for this.

if (!owner || !browserParentForRenderer(owner)) return false;
const ref = parseDesktopSessionResourceKey(sessionId);
return isWorkHubCoordinationSessionId(ref.sessionId) && deps.isHostActive(ref);
return deps.isHostActive(ref);

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.

P2: With hidden ordinary Sessions now allowed to act, the previous Session keeps this access until the renderer's next browser:active-session arrives. browser-selection.ts:63-84 sends that only after the new Session's Host identity resolves, and sends nothing to revoke the old selection first. Reproduced: while B's resolution was pending, a hidden and collapsed A still executed browser_click. Revoke the old selection immediately on change, then show the new one when it resolves.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review of #6124 from our last reviewed head e781f3f69c7e168dbc4d7ef363ef4a7779d5cb28 to 9cba5c2918813d5991485ed8d04aad1666c4087a. The range has two commits:

  • af03f0d merges main at d14cec5. Its remerge-diff only resolves a conflict in the totals line of docs/astryx-surface-file-inventory.md, and the inventory gate passes on the head.
  • 9cba5c2 is the author's fix commit.

I built the head and reran the relevant gates and tests locally.

CI and mergeability. CI test fails on this head, and this PR causes it (P2, below). The PR is mergeable, but mergeStateStatus is BLOCKED. Main at d14cec5 is green.

P2 (new, caused by the fix commit): CI test fails at Knip (apps/desktop)

Every step before Knip passes in CI on this head, including lint, format, check:renderer-architecture, the Astryx inventory, build and typecheck. npx knip --workspace apps/desktop then reports three unused exports, and I reproduced the same result locally:

  • PersonalAssistantAvatar and useAssistantAvatar (features/personal-assistant/index.tsx:28)
  • PersonalAssistantTyping (index.tsx:467)

The fix commit moved their only consumer, chat-message-surface.tsx, onto the new PersonalAssistantChatPresentation. That component imports ./avatar.js and ./typing.js directly, so the barrel re-exports no longer have a user. index.tsx still uses avatar internally through its own import on line 27. I deleted only line 28 and the line 467 re-export, and knip --workspace apps/desktop then passed. check:renderer-architecture --base d14cec57 --strict-base still passed afterwards. CI stopped at Knip, so the test steps after it have not run in CI. I ran them locally; results are below.

Prior findings

  • P1, empty state and "load earlier": fixed. activityContent now renders inside the single child expression in both branches, and showEmptyState and hasOptimisticContent count it as content. On this head, packages/ui chat-view-load-earlier.test passes 9/9, including the two new cases. Desktop chat-view-optimistic-render.test also passes. Both full suites pass: packages/ui 819/819 and apps/desktop test:dist 3849/3849.

    The assistant passes a typing element even when it is idle, and that element renders null. I checked that this does not hide the loading spinner, the load-error retry or the empty-hero content: the transcript branch renders emptyContent itself when nothing is visible. A probe ChatView test with an idle activityContent still shows all three.

  • P2, the previous Session keeps background browser access during a switch: fixed. setActiveSession now clears current and sends hide for a new generation synchronously, before it resolves the new Session. show uses a second, newer generation. A failed resolution stays revoked.

    The new browser-ipc-main.test case drives the real preload coordinator into the registered IPC. While the replacement is pending, it checks that the old hidden Session's canDrive is false for observe, navigate and mutate, and that withBrowserPage(..., 'click') is rejected. To confirm the test catches the bug, I reverted only the coordinator change in the built preload. The IPC test and all three browser-selection tests then fail, and they pass with the fix. Main's advanceSelection admits the two increasing generations as expected.

  • P2, check:architecture and the personal-assistant.test binding assertion: fixed. The architecture check passes in CI and locally against d14cec5 with --strict-base. The direct typing.js import is gone. The app-shell className, compact-controls and placeholder logic now lives in the feature-owned PersonalAssistantChatLayout/PersonalAssistantComposer, and app-shell nonTriviaTokens drops 7276 -> 7274. The useAssistantAvatar call moved into PersonalAssistantChatPresentation. personal-assistant.test passes 9/9. The cleanup left the unused re-exports that now break Knip (above).

  • P2 (me2seeks), disable racing an in-flight appendConversation: fixed. assertAvailable() is now checked again right before the first durable write inside publishBotConversationMessage, and again before appendMessages. A throw leaves the outbox row undelivered, so the message is replayed after re-enable, not dropped. The new execution-composition test pauses publication, issues set-enabled(false), and checks three things: the message is not committed, new admission is rejected, and the message replays exactly once after re-enable. It passes. With both new assertAvailable() calls removed from the build, it fails. One narrow interleaving remains: a disable that lands while the runtime-event import itself is in flight commits the ledger copy but not the session row. That converges on replay, because publication dedupes by storedMessageId.

  • P3, epoch 223: open. #6115 and #6091 still declare 223 (#6090 is now at 222). The epoch comment still mentions only the Inbox receipt. It should also mention assistant.avatar/assistant.avatar-image, the bot block on assistant.binding, the inbox execution-query state and summary.

  • P3, browser visibility policy doc: partly fixed. The browserActionAllowed doc in browser/logic.ts now describes the background authorization and the revoke-on-switch rule. The comments in browser/session.ts (around 147-152 and 335-338) still say the lease "gates the START on screen" and that "only the shown conversation may have one in flight".

  • P3, provenance (controller.ts connector text as source: 'user'; delegation.ts amendments as source: 'user'): open. These files are unchanged.

  • P3, initiative first-caller config binding and the ignored post result: open. Unchanged.

  • P3, AssistantSetAvatar fetches any URL or local path: open. Unchanged.

  • P3, renderer nits: partly fixed.

    • Fixed: the placeholder now has zh-TW. It is an inline map in chat-presentation.tsx, not the feature catalog.
    • Still open:
      • the Chinese-only avatar and typing aria-labels (typing.tsx:47)
      • the avatar.tsx error path, which sets a fresh object every 3 s
      • the <details> popover with no Escape or outside-click dismissal
      • the typing indicator, which shows for any Bot turn
      • the unreachable string-array branches in queueVisibleBatch

Other checks on the fix commit

  • I found no new runtime regression beyond the Knip failure. PersonalAssistantComposer reads the session from context. Its only call site is the main chat composer region, which sits inside PersonalAssistantChatLayout, so no other composer picks up assistant props.
  • original-reader.css moved from a TSX side-effect import to an unlayered @import in styles.css. It was unlayered before as well, so cascade precedence does not change.
  • Local runs on this head:
    • passing: build, check:renderer-architecture (strict-base), protocol-epoch check (222 -> 223), packages/ui tests 819/819, apps/desktop test:dist 3849/3849, and the runtime-host execution-composition, personal-assistant and assistant tests 64/64
    • failing: Knip (apps/desktop)

import { SideNavItem } from '@astryxdesign/core/SideNav';
import { Sparkles } from '@maka/ui/icons';
import { PersonalAssistantAvatar, useAssistantAvatar } from './avatar.js';
export { PersonalAssistantAvatar, useAssistantAvatar } from './avatar.js';

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.

P2 (CI): Knip (apps/desktop) fails on this head because PersonalAssistantAvatar and useAssistantAvatar are no longer imported through the barrel. chat-presentation.tsx imports ./avatar.js directly, and this file uses them through line 27. Deleting this re-export, together with the PersonalAssistantTyping re-export at line 467, makes npx knip --workspace apps/desktop pass locally, and the strict-base architecture check still passes.

export { createInternalUriDispatcher } from './internal-uri.js';

export { PersonalAssistantChatPresentation, PersonalAssistantChatLayout, PersonalAssistantComposer } from './chat-presentation.js';
export { PersonalAssistantTyping } from './typing.js';

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.

P2 (CI): This re-export is unused, because chat-presentation.tsx imports ./typing.js directly. Knip reports it as an unused export, and that is the current CI test failure.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.Incremental review: 9cba5c29..db11126c, one commit (-2 lines). Our last P2 is fixed: the three unused re-exports (PersonalAssistantAvatar, useAssistantAvatar, PersonalAssistantTyping) are removed from features/personal-assistant/index.tsx, and Knip passes. CI test now passes on this head, so the unit and E2E suites ran too.

At this head, all P0–P2 findings from our earlier reviews are resolved. These P3s remain open:

  • epoch 223 is also declared by open #6115 and #6091, and the 223 comment doesn't list the other protocol additions
  • the browser/session.ts comments still describe the old on-screen-only rule
  • externally delivered text has no provenance
  • the initiative config binds to whichever caller comes first
  • avatar fetching is unconstrained
  • the renderer nits from the previous review

@xxhZs

xxhZs commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han P3 follow-up is pushed in f471bce. Thanks for the detailed review; please re-review the changes and qualifications below.

Changes

  • Protocol/docs: moved the epoch to 224 after checking current main (222), feat(storage): add lossless compression for cold runtime history #6115 (223), feat(desktop): add annual token activity heatmap #6091 (223), and feat(desktop): create projects from folders dropped into the sidebar #6090 (currently 222). The comment now lists Inbox admission/query state, summaries, avatar operations, the binding bot block, and connector provenance. The merge-time epoch guard remains authoritative. Updated browser/session.ts comments and blocked-action guidance to describe selection/Host authorization rather than an on-screen/open-panel requirement. Hidden/minimized/collapsed-panel operation remains supported.
  • Provenance: added a strictly decoded externalSource admission field carrying connector/sender identity; when supplied, it survives the Bot bridge, durable Inbox and copied conversation, is marked external_connector, and is not recorded as human-authored ledger text. Conflicting dedupe retries cannot erase/change the origin. Task amendments now use coordinator, with explicit executor context that they are not new human authorization.
  • Initiative: startup resolves the existing assistant conversation instead of letting an arbitrary first tool caller become the config owner; configuration retains the bound cwd. DiscoveryReport now checks actual mailbox acceptance and propagates failures.
  • Avatar: local images must resolve inside the invoking Session's persisted workspace, retain normal Session filesystem authority, and pass file-type/size/replacement checks. Capture has a 30-second deadline and validates returned image bytes. URL loading retains the existing public-target DNS pinning, per-redirect checks, proxy/outbound policy and streamed 2 MiB limit; added executable tests for these boundaries as well as local traversal/symlink/oversize/cancellation cases.
  • Renderer/store: catalog-backed English/zh-CN/zh-TW placeholder and ARIA labels; stable state for identical disconnected-avatar polls; Escape/outside-pointer dismissal for compact composer settings; typing gated on unanswered human/optimistic input rather than Bot working state alone; removed unreachable queueVisibleBatch string-array branches. New renderer tests are in the normal desktop test entrypoint.

Validation

  • Passed: full build, workspace typecheck, lint, format, locale hygiene, ASF headers, Astryx surface inventory, desktop-scoped Knip, strict-base renderer architecture against d14cec57, and committed-tree epoch guard (222 → 224).
  • Passed: 106/106 focused core/Host tests (including assistant, avatar, initiative, conversation and execution composition); assistant suites 80/80 + 24/24 + 78/78; focused desktop/browser tests 8/8. The earlier full desktop and UI suites also passed.
  • Not claiming an all-green full workspace run: that invocation did not finish cleanly (CLI remained running until the invocation timeout). Managed-activation and eval shim failures reproduced on an isolated, rebuilt original db11126c baseline too. Node 25 TLS transport failures passed on cached Node 24; the missing reused PTY dependency patch was restored and its test passed. A full Node 24 runtime run had one process-cleanup failure, whose unchanged targeted suite passed on retry (12/12). An unrestricted root Knip scan reports fixture/script/generated-artifact findings, including the same 50 unused-file findings on the original baseline; the CI desktop scope passes. No tests, budgets, or check configuration were weakened. Local Electron E2E was not rerun; new-head CI is pending.

Explicit limits

  • No in-repository production connector currently supplies the new provenance field. This provides the native admission/persistence boundary; it does not establish that every upstream connector is integrated or authenticate a sender from their text.
  • Typing uses transcript pending-human state, not a new Host-certified causal reply-turn identity.
  • Local avatar containment/replacement checks are not descriptor-relative OS isolation against concurrent hostile filesystem mutation; configured proxies retain their existing trusted-egress role.

No merge or force-push was performed.

@xxhZs

xxhZs commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

@Astro-Han Follow-up c59ff5a is now pushed, completing the minimal provenance wiring for the existing desktop Bot-channel production path. This supersedes the API-only qualification in my previous comment for that path.

The existing flow remains createBotIncomingMainService → BotSessionAdapter → RuntimeHostBotSessionAdapter → turn.start: it now carries external_connector, the connector platform, and BotMessageEvent.userId as sender identity when available. Missing sender identity remains missing. These are untrusted source metadata, not owner authentication or new user authorization. Bot Session creation/routing, explore preparation, connector authentication and permissions are unchanged; no Bot Session was migrated into the personal-assistant Inbox. The separate native assistant externalSource support remains in place.

turn.start and durable root-descriptor decoding now strictly admit only the existing cloud_activation and new external_connector origin shapes. Empty/extra-field origins and goal/scheduled-task/agent-graph origin forgery remain rejected. Existing generic runtime message creation marks an origin-bearing message as host-authored rather than human-authored. Root admission retains the origin; same-origin replay does not restart inference, while removing/changing connector identity on replay is rejected.

On committed tree c59ff5a09, using cached Node 24.20.0: full build, workspace typecheck, lint, format, desktop-scoped Knip, strict-base renderer architecture against d14cec57, and epoch guard 222 → 224 all exited 0. Tests passed: 184/184 protocol + root-coordinator tests (including old cloud compatibility, missing sender, durable origin and replay checks), 11/11 real Bot producer/adapter tests, and 38/38 avatar/native-provenance/initiative boundary tests. The producer→wire decoder and decoder→real durable Host/ledger boundaries are tested separately; this is not a claim of a live vendor connector E2E run.

The broader local-run failures/timeout and remaining typing-causality/filesystem-race limits reported above are unchanged; I am not claiming all local workspace tests or E2E are green. The epoch remains 224 for this unreleased PR, with its comment updated. Both pushes preserve the original db11126c history (db11126c → f471bce2c → c59ff5a09), with no force-push or merge. Please re-review the two commits together.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review of #6124 from our last reviewed head 9cba5c2918813d5991485ed8d04aad1666c4087a to f471bce2c30882ceb2a4dfdce1a1d6f2d6bc5b64. Both heads sit on the same merge base, d14cec5, so there was no rebase. The delta is the author's two new commits:

  • db11126 removes the unused personal-assistant re-exports.
  • f471bce addresses the remaining provenance, safety and renderer nits.

Since then main has moved to 368f5c9, but only in three storage files (a Cursor session adapter). None of them are protocol files.

I built f471bce locally and ran the checks and suites listed below.

CI and mergeability. The PR is MERGEABLE, and mergeStateStatus is BLOCKED.

  • db11126: CI test passed on this commit.
  • f471bce, passing: every step through Lint, Format, renderer architecture, Build, Typecheck, both Knip steps, Linux sandbox smoke and the affected workspace tests.
  • f471bce, failing: CI test fails in "Run Runtime Host tests". Two event-wait-manager.test cases fail, both with Condition did not settle without advancing the observation clock:
    • "Local: a late Retry-After survives read timeout, wait cleanup and restart"
    • "Local: a subscription deferred by peer Retry-After rechecks allowed access"

I don't think this PR causes the failure:

  • The PR doesn't touch event-wait code.
  • The same Retry-After cases in that suite failed on the unrelated branch codex/4319-write-evidence (run 39031806684), and that branch's next run passed.
  • event-wait-manager.test passed 250/250 in five local runs on this head.

Because of that failure, CI skipped every step after it, including Desktop e2e, the browser smokes, Storybook and the CLI packaging. Those steps have therefore not run against the renderer changes in f471bce. Please rerun the job before merge.

Prior findings

  • P2, Knip (unused PersonalAssistantAvatar/useAssistantAvatar/PersonalAssistantTyping re-exports): fixed. db11126 removes both re-exports. Knip (apps/desktop) passes in CI on both new commits and locally.
  • P3, epoch collision and incomplete comment: fixed.
    • The epoch is now 224. Main is still 222, and #6115, #6091 and #6147 declare 223.
    • No other open PR that touches protocol/index.ts uses 224.
    • The epoch gate only rejects a backward move, so 222 -> 224 passes. "Guard the protocol compatibility epoch" passes in CI.
    • The comment now covers the Inbox receipt, the inbox execution-query resolutions, connector provenance, summaries, assistant.avatar/assistant.avatar-image and the bot block on assistant.binding.
    • Merge order still matters. If this PR lands first, the 223 PRs must move above 224.
  • P3, stale browser/session.ts comments: fixed.
    • The in-flight comment and the withBrowserPage comment now describe the selection and authorization lease, and the BrowserActionBlockedError text matches.
    • They also state that a hidden or minimized window, or a collapsed panel, does not revoke access. That is the maintainer-accepted behaviour.
  • P3, provenance: fixed.
    • TurnOrigin gains external_connector, with an exact-shape decoder.
    • turn.message.submit accepts an optional externalSource. It rejects one that does not decode, and returns operation_unavailable when the target is not the assistant conversation.
    • BotController.receive stores such input as source: 'external_connector' with the origin attached. The ledger copy gets author: 'host', and the replay identity check now compares origin and author too. The Bot role prompt says connector text is not an owner instruction.
    • delegation.ts amendments are now source: 'coordinator', and both the matter store and the matter prompt accept and describe that source.
    • No in-tree client sends externalSource yet, so this is the Host-side contract only.
  • P3, initiative first-caller binding and the ignored post result: fixed.
    • enable/configure now require an existing binding whose sessionId matches the caller. They configure with the stored session and cwd, not the caller's. Binding comes only from assistant.bind, or from startup resolution of a single labelled conversation.
    • DiscoveryReport now throws when bot.post returns false.
    • The new assistant-initiative-boundary.test covers both.
  • P3, AssistantSetAvatar reads any URL or local path: fixed.
    • The new avatar-image-source.ts limits local paths to the realpath of the invoking Session's cwd. It also checks the file before and after the read: same identity, size and mtime.
    • It still goes through the Session's normal file reader, and the size cap is checked before the read. URLs go through the existing Host downloader, which applies public-target pinning and checks each redirect hop.
    • The tool description says the same. assistant-avatar-boundary.test covers escape, symlink and redirect cases.
  • P3, renderer nits: fixed.
    • The avatar and typing aria-labels and the placeholder now come from a shipped zh-CN/zh-TW/en catalog (chat-copy.ts).
    • On disconnect the avatar state keeps its identity, so the 3 s poll no longer sets a new object each time.
    • The compact <details> popover now closes on Escape (focus returns to its summary) and on an outside pointerdown.
    • Typing shows only after an unanswered human message or a pending transcript send. A user message that has an origin does not count, and nothing shows while later history is unloaded.
    • The string-array branch is gone from queueVisibleBatch.

New findings in the delta

  • P3 (from reading the code, not reproduced): bindingReady is computed once and never retried. InitiativeController sets bindingReady = resolveBinding() once, in the constructor (controller.ts:50). assistant.bind, enable and configure all await it first.
    • If the startup sessionQuery.list() rejects, all three keep failing until the Host restarts. That includes a transient failure, not only the intended "multiple assistant conversations" fail-closed case.
    • If this Host does not own the lease at construction, the promise resolves without binding. That case is fine, because assistant.bind from the renderer still binds.
    • Suggestion: memoize only success, or retry resolveBinding on the next call after a rejection.

I found no other new defects in the delta. The display: contents wrapper around Composer doesn't affect the existing composer selectors, which all target elements inside the Composer.

Local runs on f471bce

  • Passing:
    • build:test
    • typecheck
    • knip --workspace apps/desktop
    • apps/desktop test:dist, 3850/3850
    • runtime-host test:assistant, 80 + 24 + 78
    • event-wait-manager.test, 250/250 in each of five runs
  • Failing, one test locally: the runtime-host test:dist case "production Host executes Bash against the current live sandbox boundary". It is specific to this machine's sandbox and passes in CI on this head.

Addendum for c59ff5a (pushed while this review was being written; this comment is posted against c59ff5a). The commit is a fast-forward on f471bce: bot turn.start now carries external_connector provenance (connectorId = platform, senderId = connector user id when non-empty). TurnStartInput.origin and the external_message root execution descriptor accept external_connector as well as cloud_activation, so recovery keeps the provenance. decodeTurnOrigin is exact-shape and rejects empty ids, and the desktop leaves out an empty userId. Because external_connector is non-owner input, a client that labels its own turn this way can only lower its own authority. This closes the "no client sends the connector tag yet" gap noted above. The epoch comment (224) is updated. No new findings. CI test on c59ff5a is still running, so the flaky event-wait-manager result above applies to f471bce only; the CI status of c59ff5a is pending.

this.owned = owned;
};
acquire();
this.bindingReady = this.resolveBinding();

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.

P3: this promise is computed once and never retried. If sessionQuery.list() rejects at startup, assistant.bind, enable and configure keep throwing until the Host restarts, even when the failure was transient. Consider memoizing only success, or re-running resolveBinding() after a rejection.

@Astro-Han Astro-Han 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.

Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.

Incremental review: c59ff5a → df549e5 (fast-forward, one commit: "retry failed initiative binding resolution").

Prior P3 (bindingReady computed once in the constructor and never retried): fixed. bindingReady is now a getter over a cached bindingResolution promise. Concurrent callers share the in-flight lookup and a success stays cached. A rejection clears the cache inside its own handler before rethrowing, so the next assistant.bind/enable/configure call retries resolveBinding() instead of failing until a Host restart. The startup call still observes its rejection (void this.bindingReady.catch(() => {})), so there is no unhandled rejection. resolveBinding only writes (fence + bind) after every check passes, so a retry after a partial failure does not double-bind. The new boundary test covers fail-then-retry.

New findings: none at P0–P2. One P3 note, which predates this commit: a lookup that returns early because this Host does not hold the lease (!this.owned) is cached as success, so a Host that later takes over the lease does not re-run the lookup. Consider caching only an owned result, or resetting bindingResolution when acquire() gains ownership.

CI: all checks on df549e5 pass. Mergeable. Earlier findings from the f471bce/c59ff5a0 review are all closed; protocol epoch is 224 (main 222).

@xxhZs
xxhZs merged commit 57cde86 into main Oct 18, 2026
1 check passed
@xxhZs
xxhZs deleted the feat/assistant-inbox-bot branch October 18, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants