* feat(gateway): add web-only incognito sessions held in process memory
* feat(ui): add incognito toggle and badges to the web new-session flow
* fix(sessions): classify incognito by key shape, fail closed on stale keys, and gate memory writes
* fix(codex): start harness threads ephemeral for incognito sessions
* fix(sessions): reshape internal-effects incognito keys and add doctor repair for reserved key collisions
* refactor(plugin-sdk): export canonical incognito key classifier and guard the sentinel path
* fix(state): classify incognito DB handles from the recorded open-time set
* fix(gateway): isolate incognito sessions from durable lineage and allocation on read-only misses
* docs(sessions): pin the reserved incognito namespace ownership decision
* feat(gateway): admin-scope incognito visibility and incognito-blind cross-session surfaces
* fix(ci): repair kysely guardrails, dead export, docs map, protocol bindings, and ACP reset rotation
* fix(gateway): remove non-admin observability side channels for incognito sessions
* fix(gateway): enforce admin-scope incognito access and cover all parent-reference creation paths
* feat(sessions): enforce visibility and membership
* feat(ui): add session sharing controls
* docs: add session sharing implementation report
* refactor(sessions): use canonical creator identity
* fix(sessions): adopt creator ownership contract
* docs: refresh session sharing rebase report
* docs: record final creator integration proof
* docs: record final main rebase
* chore: drop worktree report artifact
* fix(sessions): keep drafts owner-only
* fix(ui): preserve redacted session restrictions
* fix(sessions): preserve scoped sharing authorization
* fix(sessions): re-verify session instance inside sharing mutation queue
* test(sessions): cover stale sharing mutation
* fix(sessions): bind membership to session instance, gate absence blocking on sharing
* fix(sessions): preserve entry normalization on rebase
* fix(sessions): atomic visibility instance guard, reset visibility on recreate
* docs(ui): name the absence-heuristic tradeoff and link follow-up
* feat(protocol): expose session sharing row state
* docs: note generated creator identity type
* fix(sessions): bind member writes and visibility rollback to session instance
* fix(ui): discard stale-connection sharing loads; drop worktree scratch files
* fix(ui): block composer only on observed sharing state, never on list absence
* fix(gateway): authorize agent runs against the resolved session (close keyless bypass)
* chore(protocol): allowlist Control UI-only session.sharing event for mobile
* test(config): record session.sharing keys in common-tier snapshot
* refactor(sessions): unexport internal sharing helpers
* test(gateway): update sessions changed routing assertions
* fix(sessions): align sharing identity with created actor
* fix(sessions): align membership identities and storage keys
* fix(gateway): re-filter drafts against fresh sharing state in sessions.list
* fix(gateway): drafts stay owner+admin only in the sessions.list fresh filter
* fix(ui): re-export sharing protocol types for the Control UI
* fix(ui): keep SessionSharingRole internal to satisfy deadcode gate
* fix(gateway): read runtime config lazily in session-mutation authorization
authorizeSessionMutation ran on every gateway request but eagerly called
context.getRuntimeConfig() — a non-trivial config resolve — for methods that
are never session mutations. Read config only once a real session-mutation
target is resolved. Also register the four session sharing methods in the
2026.7 release-train inventory test.
* fix(gateway): share one config snapshot across session-mutation authorization
Group rename/delete discovery and the authorization loop were each resolving
runtime config separately after the lazy-read change. Memoize the resolve so
non-session requests still pay nothing, while any session mutation resolves
config at most once and both discovery and authorization use that single
snapshot (no double reload, no mid-request config-change split).
* fix(gateway): resolve session-sharing CI gates
- isGatewayAdmin: null-safe connect access so internal/plugin-runtime runs
(which reach authorization with a connect-less client) do not crash.
- emitSessionsChanged: scope only to a concrete session key; a [undefined]
sessionKeys scope filters nothing correctly and would strip draft gating.
- session stores: mark the sync TOCTOU re-read and the sqlite_master
existence probe as narrowly-justified raw SQLite primitives.
- tests: provide getRuntimeConfig to the session-action contract context,
drop a shadowed 'call' binding, use structuredClone, and assert the
agent-scoped sessions.changed broadcast shape.
* docs(gateway): note best-effort participation gate + refresh native i18n baseline
Session ownership/visibility are usability features, not a security boundary
(docs/concepts/multi-user.md, SECURITY.md); document that the pre-dispatch
authorization is intentionally not commit-bound to the resolved instance.
Also refresh apps/.i18n/native-source.json after the session-sharing protocol
codegen shifted line numbers of existing native strings (position-only).
* test(gateway): reset session sharing snapshots
* style(gateway): format sharing reset import
* fix(ui): disable the thread-changes button when the workspace is not a git checkout
* chore(protocol): regenerate Swift gateway models for sessions.files.list gitCheckout
* feat(ui): redesign session observer HUD with labeled status and visibility-gated digests
* chore(protocol): regenerate app protocol bindings for sessions.observer.visibility
* feat(agents): mobile_ui agent tool for Android UI control (PR 3/3)
Add a dedicated model-facing tool that drives another Android app through the
PR2 mobile.ui.observe/act node commands, completing the vertical slice
(agent -> tool -> node.invoke -> AccessibilityService).
- Mirrors the desktop computer tool's safety model: owner-only + HTTP-denied
(dangerous-tools), raw node.invoke of mobile.ui.* redirected to this tool so
the generic nodes tool cannot bypass it, run/tool-call idempotency, and the
phone-arm workflow (mobile.ui.* must be explicitly armed).
- One call is observe or one act; every act automatically re-observes for
postcondition verification and preserves the landed outcome if re-observe fails.
- Fail-closed confirmation: all state-changing acts (activate, set_text, tap,
swipe) require confirmed=true after the model reviews the proposed effect;
observe, scroll, wait, and navigation global_actions do not. The keyword list
only enriches the confirmation message and is never the sole gate (a11y labels
are localized/iconographic/coordinate-blind).
- Node selection resolves an explicit id against the full device set first
(case-insensitively) and rejects an ineligible or ambiguous match, so an
explicit selection can never be silently redirected to the wrong phone.
- All observed UI text is treated as untrusted; the tool description forbids
following instructions found in app UI.
Additive; no protocol bump. mobile-ui-tool + registration + policy tests pass;
core tsgo and lint verified on Testbox. On-device drive is exercised via the
PR1/PR2 emulator proof; this PR is the agent-side tool + tests.
Follow-up: computer-tool.ts has the same latent node-resolution ordering bug
(explicit id searched only among eligible nodes); tracked separately.
* fix(agents,phone-control): migrate mobile_ui arming hints/tests to gateway.nodes.commands.allow
Match main's node-command arming rename (gateway.nodes.allowCommands ->
gateway.nodes.commands.allow / commands.deny): update the mobile_ui tool arm-hint
matchers to the gateway's current rejection strings and fix the phone-control /
tool tests to the current config shape. Production write path was already correct.
* fix(agents): register mobile_ui in owner-only denylist; trim phone-control under max-lines
- tool-resolution.exclude.test.ts: mobile_ui joins the owner-only core
tools, so add it to the expected plugin/inherited denylists.
- phone-control: derive ArmGroup from a single ARM_GROUPS const and
collapse parseGroup's ||-chain, dropping index.ts back under 700 lines.
* fix(agents): centralize mobile_ui global-action names; regenerate tool-display snapshot
- Extract GLOBAL_ACTION_NAMES const so the schema, action type, and
validator share one source. This also removes the bare `name: "back"`
type-annotation literal that scripts/tool-display.ts's name-regex was
misreading as a phantom runtime tool.
- Regenerate apps/.../tool-display.json to add the mobile_ui display entry
(was missing from the Swift snapshot).
* feat(sessions): first-class archived-session handling
Archived sessions are now exempt from every automatic maintenance path
(age prune, entry cap, model-run prune, disk budget, cleanup repairs);
only explicit sessions.delete removes them. sessions.list gains an
additive tri-state archived filter (true | false | "all"). The sidebar
gets a persisted Active/Archived/All status filter with inline dimmed
archived rows, replacing the nav-away View-archived button; the Sessions
page toggle becomes the same tri-state with dimmed+badged rows in All
mode; the chat composer's archived notice becomes a full-width banner
with an Unarchive action.
* chore(protocol): regenerate Swift gateway models for tri-state archived filter
* feat(android,gateway): expose mobile.ui.observe/act as node commands (PR 2/3)
Wire the PR1 AccessibilityService executor over the existing node.invoke
transport. Additive: no gateway protocol version bump.
- New commands mobile.ui.observe / mobile.ui.act (capability mobileUI),
generated into the protocol constants.
- thirdParty MobileUiHandler owns one mutex-serialized AccessibilityActionExecutor
and bridges JSON <-> the PR1 snapshot/action model; play flavor ships a
permanently-unavailable no-op stub (Play APK stays accessibility-free).
- Commands advertised only when accessibilityControlEnabled and the service is
connected; NodeRuntime refreshes the advertised surface on connect/disconnect.
- Classified dangerous in node-command-policy (declarable-but-armed, mirroring
computer.act): the transport cannot invoke them until gateway.nodes.allowCommands
explicitly arms them. Screen reads are treated as dangerous too, so observe is
gated as well as act.
Both flavors assemble; thirdParty/play unit tests, ktlint, android lint, and the
gateway node-command-policy tests pass; protocol generator is idempotent; Play
APK verified to contain zero accessibility classes. Emulator: no regression to
PR1 dev-screen observe.
* fix(gateway): migrate mobile.ui arming test/comment to gateway.nodes.commands.allow
Main renamed the node-command arming config from gateway.nodes.allowCommands to
gateway.nodes.commands.allow; update the mobile.ui policy test config shape and
the comment to the current contract. Production resolution already reads the new
path.
* chore(android): update native i18n baseline for PR2 line-number shifts
* feat(apple): add rare working claw stances
Add zen, drummer, and peekaboo animations to the shared iOS and macOS working indicator, with deterministic stance and pose coverage.
* chore(i18n): refresh native source anchors