Completes Phase 5 of the multi-device mesh / session-transfer program (docs/MESH_TRANSFER.md)
except the two-Mac memory-carry spike. All additive + capability-gated; SyncProtocol stays v1.
- Moved-session visibility: additive SessionSummary.movedTo (MovedDestination), decode-defaulted.
A moved session no longer silently vanishes — the source keeps a read-only "Moved to <Mac>"
tombstone under Archived (name resolved live from paired Macs), surfaced on relaunch without
rebuilding a runnable controller, and sent on the wire so phones see it too.
- Relaunch recovery driven from launch (+ on every peer reconnect, single-flight):
AppStore.recoverInterruptedTransfers clears abandoned pre-tombstone locks, discards orphaned
inbound staging, and re-drives a tombstoned commit via SessionTransferCoordinator.recoverTombstoned
(bounded, idempotent; a dest that lost staging leaves the lock, never revives the source).
- Bulk "Hand off active sessions…": transferableSessions + moveSessionsToPeer (sequential, rollup
error) behind a "Hand off…" button → HandoffSheet checklist in RemoteAccessView.
- Arrived-from provenance (mirror of moved-to): GRDB v24 arrived_from_device_id/arrived_at; the
importer stamps them at staging; additive SessionSummary.arrivedFrom (ArrivedFrom); a subtle
"Arrived from <Mac>" marker on the sidebar (live name) + iOS row (host-baked name).
- Stranded-arrival "Activate anyway": the importer persists the staged Session to the staging dir
at .ready, so a destination that relaunches before commit can recoverableInboundTransfers() and
activateRecoveredTransfer()/clearInboundStaging(). AppStore surfaces pendingArrivedTransfers with
activate/discard, shown in a new "Interrupted arrivals" section. (A .ready lock with no manifest
is now cleared as unrecoverable.)
Tests: +6 core, +2 protocol across WireMessageTests, SessionTransferTests, AppStoreTests,
AppStoreSyncBridgeTests. Full package builds; Swift suites green. iOS NucleicRemote edits reviewed
but not compiled here (separate Xcode target).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
An opt-in (Beta, Nucleic-Control-only) versioning mode where a project's agent
sessions share one nucleic/trunk checkout, lock individual files per-edit, land each
completed edit into the trunk immediately, and release fast — instead of holding a
session-long lock until a big merge. Conflicts are structurally impossible within the
trunk (serialized per-file writes + forced re-ground), so 'merge' collapses to 'commit'.
- Phase A: ProjectNvrsion config, migration v20-nvrsion, nvrsionActive gate, Beta toggle
- Phase B: NvrsionTrunk actor (ensureTrunk/land/regroundOnGrant), shared-trunk topology
(no per-session worktree), per-edit host-mediated path-scoped commit + release
- Phase C: NvrsionReleaseGovernor keep-warm idle eviction, launch crash-recovery,
flip-safety guard
- Phase D: pre-land validation hook, trunk->base squash promotion + 'Promote trunk' UI
Design and rationale: docs/NVRSION.md. Full suite green (585 tests).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Three fixes to the Nucleic Control autoship/file-lock system.
1. File locking now covers every edit in a turn, not just the first.
handleApprovalCall arbitrated each edit only while !conflictOverridden,
and a successful lock grant (.granted -> .proceed) latched that flag, so
after the first edited file every later edit skipped arbitration and
acquired no lock. Parallel sessions could then edit the same file and
collide at autoship time. The latch was a leftover from the old
interactive-prompt model (arbitrate is now automatic and never prompts);
removed it so every edit re-arbitrates. Re-arbitrating a file the session
already holds is a cheap LockManager fast-path no-op.
2. A merge *conflict* no longer turns autoship off. It sets a sticky
autoShipConflict marker, leaves autoship armed (so the work re-ships on
the next clean merge), and is flagged prominently: the sidebar
"needs attention" icon, the header/Control-panel ship status, an in-chat
note, and lastError. A hard merge *error* still disarms via autoShipFailed.
3. When a conflict's work finally lands -- the queue's next merge or the
agent's own merge observed via the git shim -- the conflict marker clears
and the live ship status updates to .merged, so the UI recovers. Fixes the
stuck state where, after a conflict turned autoship off, the agent's own
merge appeared in the activity feed but markShippedIfLanded bailed (it
required autoShip) and the Control panel kept showing the stale conflict.
Adds a session column (auto_ship_conflict, migration v19), SessionController
mutators, UI wiring, and tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sort sidebar sessions by last user message instead of any activity.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
# Conflicts:
# Sources/NucleicCore/Persistence/GRDBMetadataStore.swift
Add Session.parentRef (release target), rootRef (lock domain = the tree's top-level branch, invariant under cascade), and parentSessionID. GRDB migration v14 + SessionRow round-trip. createSession computes them: a base that names a live session's branch makes this a nested child inheriting that session's rootRef. Repoint SessionController.integrate/unmergedFiles and AppStore.shipIfCompleted/shipTargetLabel from project.defaultBranch to the session's parentRef — so a session releases against and merges into its REAL parent (fixing the wrong-target half of the leak) and the merge queue serializes per real parent. New test parentAndRootRefsTrackTheTreeAndPersist; 262 tests pass. Implements LOCKING.md Phase A (§3).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
When autoship aborts on a merge conflict or merge error it turns itself off; the
chat's sidebar entry now shows an exclamation-triangle marker until the user
re-enables autoship. Unlike the shipped marker (lastEventWasAutoship), this is
sticky session state, not tied to the latest event — a failure needs attention
and shouldn't vanish the moment something else happens.
- Session + SessionSummary gain `autoShipFailed`; persisted via migration
v13-autoship-failed.
- SessionController.markAutoShipFailed() turns autoship off and sets the marker
atomically; the merge queue's conflict/failed paths now call it instead of a
plain setAutoShip(false). setAutoShip(true) clears the marker (re-opt-in). A
clean user toggle-off leaves it untouched.
- RootView.SessionRow renders the warning icon, taking precedence over the
shipped icon.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
When the merge queue logs an autoship note and nothing has happened in the
session since, the chat's sidebar entry now shows a shipping-box glyph. The
signal is tracked on the canonical event pipeline: `Session.lastEventWasAutoship`
is set true while the newest transcript event is an autoship note and cleared
the moment any other event (a user message, an agent turn) lands.
- NoteEvent: shared `autoshipPrefix` + `isAutoship` so the pipeline recognizes
merge-queue notes without coupling to each outcome's wording; AppStore's four
autoship notes now interpolate the prefix.
- SessionController.ingest: set/clear the flag per event.
- Session + SessionSummary carry the flag; persisted via migration
v12-last-event-autoship.
- RootView.SessionRow renders the icon in the trailing cluster.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a "Ship" toggle beside "Auto". When on, a session squash-merges its
branch into the project's default branch the moment the agent's turn is
classified `.completed` — no human babysitting the merge.
Safety / race handling:
- New `MergeQueue` actor serializes merges FIFO, strictly one-at-a-time per
(project, target) branch, with per-session dedupe so a re-fired `.completed`
can't double-ship. Different repos/branches still run in parallel.
- Each merge runs through the existing `integrate` path, which checks out the
live target tip and merges (never force) — a stale branch is merged into the
post-other-merges branch, never overwriting newer work.
- On conflict the merge aborts (branch intact, target tree clean), Ship turns
off for that session, and an error banner surfaces. No blind retries.
- Fix: squash-merge conflicts now reset --hard HEAD instead of `merge --abort`
(which is a no-op for --squash, leaving the main checkout dirty).
Ship implies Auto (an agent can't ship if every tool call blocks on a human).
Wiring: `Session.autoShip` (+ v9 GRDB migration), `SessionController.setAutoShip`,
`AppStore` owns the queue, injects the integrator, observes status into
`shipStatuses`, and triggers from `classifyDisposition`. Ship toggles added to
SessionDetailView (with a header status pill) and HomeView (new-chat default).
Tests: MergeQueue serialization/dedupe/status + git-backed end-to-end through
the queue (parallel ships land in order; conflicting second stops without
overwriting the first). Docs: RUNTIME_ARCHITECTURE §3.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Triage verdicts (level, rank, reason) are now computed in the background when
an idea is added or its text edited — and whenever the open set changes
(complete/dispatch/delete) — then persisted onto each Todo. The Triage button
just toggles between FIFO and the stored impact ranking; no pass runs on click.
- Todo gains triageLevel/triageRank/triageReason; migration v9-todo-triage adds
the columns and TodoRow maps them.
- AppStore drops the in-memory triageItems cache; openTodos and the triage
accessors read the stored fields. retriageOpenTodos() runs the provider pass
off the main loop, guarded by a token so a newer pass wins; ensureTodoTriage()
backfills on load.
- TriageButton toggles the sort and shows a spinner while a background pass
refreshes the stored verdicts.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
In conversational mode every finished turn lands on `.awaitingInput`, so the
sidebar showed "Awaiting input" both when the agent asked a question and when it
simply finished the task. Add a soft `TurnDisposition` refinement: the
Intelligence provider (Apple Foundation Models, heuristic fallback) classifies
the agent's last reply as awaiting-input vs. completed, and the sidebar renders
"Done"/green vs. "Awaiting input"/accent accordingly.
Canonical `SessionStatus` is unchanged — disposition is a presentation-only
signal, written through SessionController (still the single writer), persisted
(v8 migration), and cleared when a new turn starts. Classification runs for every
session on `runFinished`, not just the open one.
Also fixes a pre-existing race the new test surfaced: `summarizeTodoItem` wrote a
stale todo back into `self.todos` after an `await`, which could clobber a
concurrent move/edit/delete.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Opt-in, per-project execution sandbox: when enabled, a project's sessions run
`claude` inside an isolated Linux VM (Apple `container`) with the worktree
bind-mounted, instead of directly on the host. Off by default — existing
behavior is unchanged.
- Domain: ProjectSandbox (enabled/image/idleTimeout) on Project; ContainerSpec
on RunSpec/ResumeSpec.
- Persistence: migration v7 adds project.sandbox_config (JSON).
- ContainerRuntime: thin `container` CLI wrapper (preflight, default-image build,
run/exec/stop/delete/list, host-gateway discovery).
- ContainerManager: app-level per-session lifecycle — ensureRunning, idle
auto-stop, teardown, orphan reconcile.
- ClaudeCodeBackend: wraps the claude invocation in `container exec` when a
ContainerSpec is present; binds the approval MCP server on 0.0.0.0 and rewrites
its URL to the VM gateway so the containerized child can reach it.
- Repo root + worktree base mounted at identical paths (git links + cwd-hash
resolve); host ~/.claude mounted read-only and seeded into a writable
claude-home so credentials are never mutated but native resume still works.
- UI: ProjectSettingsSheet (toggle/image/idle) + "Sandboxed" badge; AppStore
gains updateProject.
- Tests: 9 new (arg construction, mount formatting, name parsing/derivation,
sandbox JSON round-trip, MCP host rewrite). 112 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Each open idea now gets its own short summary (model-backed with heuristic
fallback), shown emphasized with a sparkles "summarized" icon, and the full
original text beneath it — smaller and dimmer, since that text is what gets
dispatched as the agent's prompt. Lists are sorted oldest-first (first-in at
the top).
- Todo.summary (persisted, v6 migration): nil = not yet summarized, "" = no
distinct summary, else the summary; text stays canonical as the prompt
- summarizeTodo on IntelligenceProviding (default heuristic todoLine + AFM
override that compresses to a short imperative title)
- AppStore generates item summaries on add / on load (once) / on text edit,
off the main loop; short ideas fold to "" so they aren't retried
- openTodos + grouping sorted by createdAt ascending (stable)
- TodoRow: summary + sparkles icon over a dimmer original snippet
- Tests: long-vs-short item summary, oldest-first ordering, summary column
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Capture ideas anywhere with ⌘T into a persisted to-do inbox, listed on the
home dashboard. Each open idea can be "dispatched" — pick a project and it
spawns an agent seeded with the idea's text, marks the todo dispatched, and
links the spawned session. Lets the user park an idea while other agents are
busy and start it when they choose.
- Todo domain model + TodoStatus (open/dispatched/done)
- v5-todos GRDB migration, CRUD, and store protocol methods
- AppStore: todos state, quickTodoPresented flag, add/update/dispatch/delete
- QuickTodoSheet (⌘T) + TodoSection on the home screen
- Tests: GRDB round-trip, dispatch flow, edit/delete/complete
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- The summary card no longer overlays the transcript (it covered the first message
at the top). It's now an in-flow, right-aligned row below the header that pushes
the transcript down — collapsed it's a compact chip, expanded it grows in place.
- Model/effort dropdown items show a "house (default)" marker on the app default
(distinct from the checkmark on the currently-selected value).
- Summaries are cached on the session (DB v4 `summary` column) and persisted: opening
a chat shows the stored summary instantly with no model call; it only regenerates
when a turn ends or on manual refresh.
Test: summaryIsCachedAndNotRegeneratedOnReopen. Full suite 93 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Auto mode (per the safety question): wired to Claude's built-in --permission-mode
auto — its own classifier auto-approves safe actions and routes destructive ones
to our approval UI ("all but destructive"). Per-chat toggle (bolt) + a global
"start new chats in Auto" default. Session.auto + RunSpec/ResumeSpec.autoApprove +
DB v3 column; transcript still shows every auto-approved call.
- Model/effort: pickers list full SKUs (claude-opus-4-8, …[1m], sonnet, haiku) and
always show the effective concrete value (never "Default"); Settings adds default
model / default effort / default-auto; new chats inherit them.
- Favorite / archive / delete chats via right-click context menu AND swipe actions;
favorites sort first with a star, archived collapse into a per-project section,
archiving closes the open chat. Session.favorite/archived (DB v3).
- Summary reworked into sectioned, glanceable Markdown (Now / Done / Next) rendered
with MarkdownText; window title shows the project name (app name gone); padding
below project names.
Tests: newChatsInheritDefaults, favoriteArchiveDeleteChat. Full suite 92 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- SessionMetadataStore.deleteProject + GRDB impl.
- AppStore.deleteProject: stops each live session, removes its worktree + branch
via WorktreeManager.discard, deletes the session and project records, clears
selection. The user's actual repository is never touched — only the
.nucleic-worktrees checkouts.
- Sidebar project context menu gains a destructive "Delete Project…" with a
confirmation dialog spelling out what's removed.
Test: deleteProjectCascadesSessionsAndWorktrees (project + summaries gone,
worktree dir removed, branch deleted). Full suite 84 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Duplicate projects rejected: addProject compares canonical repo paths (symlinks
resolved) and refuses a repo that's already registered.
- Rename: SessionController.rename + AppStore.renameOpenSession (pencil in the chat
header → alert); AppStore.renameProject (sidebar project context menu → Rename).
Renames change the display title only; branch/worktree slug is untouched.
- Model + effort, per session, applied to the NEXT turn (each turn is a fresh
process, so they can change mid-conversation):
- --model (Default/Opus/Sonnet/Haiku) and --effort (low/medium/high/xhigh/max,
both real claude flags) threaded through RunSpec/ResumeSpec → ClaudeCodeBackend.
- Session.effort + GRDB v2 migration column; SessionController.setModel/setEffort.
- Toolbar menus in the session detail bound to the open session.
- AppStore exposes openSession (full state) so the detail view reads title/model/
effort/status directly.
Tests: project dedup, session+project rename, model/effort set, effort persistence
(v2 column). Full suite 83 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds the project's first external dependency, GRDB 7.11 (the plan's locked choice;
verified it resolves and builds under the native build system).
- SessionMetadataStore protocol in NucleicCore so the pipeline stays storage-
agnostic (tests/UI can swap in a fake).
- GRDBMetadataStore: WAL-mode DatabasePool on disk, in-memory DatabaseQueue for
tests; DatabaseMigrator v1 creates project/session/approval tables + indices per
§4 (device table deferred to M4). Domain types map through 1:1 row records;
approval input/decision stored as canonical JSON.
- SessionController gains an optional metadataStore sink at pipeline step 3,
persisting session metadata + approvals at boundaries (best-effort; never breaks
the canonical transcript or fan-out).
6 store tests (round-trips, scoped upsert, approval pending→resolved, on-disk
reopen, and end-to-end controller→store persistence). Full suite 75 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>