Conversation
The pane names the workspace but says nothing about whether local state has
drifted from it — so there is no moment at which a user learns they have memory
the workspace never received, or skills that have not synced this session.
`/workspace sync` has no discoverability problem to solve if nothing ever
suggests running it.
Adds two lines under the existing name and manage URL:
12 memories · 3 not synced
skills synced 6m ago
Both are status, not affordances — the pane takes no input, and none of the five
sidebar plugins does.
`allowNetwork: false` on the status call is load-bearing, not a micro-optimisation.
The sidebar refreshes every 30s, and the memory-enabled cache is deliberately
positive-only, so a workspace with memory switched OFF is never memoized. Asking
the service on a cache miss would therefore put a request on the wire every 30
seconds for the lifetime of the session, for exactly the workspaces whose answer
is "no". With the network forbidden the counts are shown when the cache already
knows and omitted otherwise — unknown reported as unknown, because treating it as
enabled shows a backlog on a workspace that has memory off, and treating it as
disabled hides a real one.
`skillsSyncedAt` is null for "unknown", not "never": the store is per-process, so
a fresh session has not synced yet even where the on-disk snapshot is current.
The line is hidden rather than rendered as "never synced".
Supporting changes:
- `skill-sync`: `lastSuccessfulSyncAt`. `recentlySynced` answers a boolean against
the poll interval, which cannot say "6 minutes ago" — and a line whose whole job
is to make staleness visible needs the age, not a threshold. Reads the
process-global store, so the plugin realm sees the map the sync writes.
- `memory-sync`: `memoryEnabledCached`, the no-network read of the enablement
cache.
Tests: 8 in the manage suite, 486 across the workspace and plugin suites.
Mutation-checked — ignoring `allowNetwork`, and treating unknown enablement as
enabled, each fail a test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Both were caught by watching the real TUI, not by the suite. Both are now
covered.
**1. The sidebar's counts never appeared.** `allowNetwork: false` was implemented
as "never touch the network", so the counts could only render once something ELSE
warmed the shared enablement cache — a memory write, or opening `/workspace`. On a
session where neither happened they simply never showed, which defeats the point:
the line exists so drift is noticed without being told to look.
The requirement was a BOUND, not a ban. `memoryEnabledForPoller` resolves over the
network at most once per five minutes when the answer is "no", and not at all once
it is "yes" (the shared cache holds positives). `memoryEnabled` itself is untouched
and stays positive-only, so the WRITE path still picks up a newly enabled
workspace immediately — that is the property that matters for not losing memory,
and a poller being a few minutes behind costs nothing.
The counts now render on first paint. A poller also reports the LOCAL block count
even when memory is off: "how many memories do I have" needs no service, and only
"how many are outstanding" depends on the workspace setting.
**2. The sync toast reported a completely failed sweep as success.** On a project
where the service refused every block, `sync` returned
`{sent: 0, failed: 0, skipped: 0, declined: 19}` — and the message only checked
`sent === 0 && failed === 0`, so it said "Everything is already in the workspace."
while nineteen memories had just been turned away.
`declined` is now counted. It means the service said no — quota, permissions, a
workspace setting — which is a different outcome from having nothing to send, and
reporting it as the latter tells the user their memory arrived when none of it
did. The message moved into `syncMessage` with its own tests.
Simplified while there: a redundant `declined === 0` was removed from the
success guard. The branch above already takes every refused sweep with nothing
sent, so it was a second guard that could never be the one that fires — and a
reader has to prove that before trusting either. Both remaining branches are now
load-bearing; mutating either fails a test, which was not true before.
Tests: 493 across the workspace and plugin suites, 5 new for the toast wording.
Mutation-checked — the poller asking on every tick, the poller never asking at
all (the original hole), a stale negative memo surviving re-enablement, ignoring
a fully refused sweep, and dropping partial refusals each fail a test. Also fixed
an order-dependent assertion: counting ALL requests picked up the fire-and-forget
backfill that `recordApprovedBinding` starts, so it passed alone and failed in a
full run; it now counts only calls to the endpoint the poller uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
…ld bug The call site's comment justified `allowNetwork: false` as "the memory-enabled cache never memoizes a no, so asking on a miss would put a request on the wire every 30 seconds". That was the reasoning behind the bug the previous commit fixed, not behind the code as it now stands: `status` resolves through `memoryEnabledForPoller`, which asks at most once every few minutes on a "no" and never once it is "yes". Left alone, the next reader either trusts the comment and reintroduces the ban, or trusts the code and stops trusting the comments. Says bound, not ban, and points at `Manage.status` where the reasoning lives. Verified end to end against prod while recording the feature: the sidebar now renders "sidebar-demo · 6 memories · 6 not synced" on first paint with a cold cache — the exact case that used to show nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
…a refusal Third bug from the same recording, spotted by the user in the video: a sweep of six blocks reported "The workspace refused all 5 memories — nothing was sent." Six local, five named. The missing one was a transport failure, and the branch that claims a total refusal fired on `declined > 0 && sent === 0` without looking at `failed` at all. So the sentence was wrong twice. "all" was false — six were attempted, not five — and the failure itself vanished. That is the worst one to drop: a refusal is the service's considered no, while a failure is retryable and can mean the network or the credentials are wrong. The outcome the user could act on was the one the message swallowed. `failed === 0` now guards that branch, so a mixed sweep falls through to the parts list and reports both: "Nothing was sent, 1 failed, 5 refused by the workspace." The list also leads with "Nothing was sent" instead of "Sent 0 memories" — that path is reachable with `sent === 0` now, and a zero reads as a statistic when it is the headline. Same family as the two before it, and the same lesson: the sweep knows exactly what happened, and every one of these bugs was the message throwing part of it away. `partitionPending` is shared by the status line and the sweep precisely so the two can never disagree; that guarantee is worth nothing if the sentence built from the result drops a field. Tests: 496 pass, 3 new for the mixed case. Mutation-checked — reverting the `failed === 0` guard fails two tests, dropping the zero-sent phrasing fails one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
…oject
Fourth bug from the recordings, and the same shape as the first: the tile read
the binding through `readLocalBinding`, which never touches the network. On a
cold cache — a fresh machine, a new session, anything that has not resolved this
project yet — that returns null, and the tile rendered
Workspace
Not linked — run altimate-code link
about a project that IS linked. It corrected itself only when some unrelated
code path happened to warm the cache, so how long the lie lasted depended on
what else the session did. Caught on screen: the sidebar said "Not linked" at
22s and 38s into a session, then showed the workspace at 63s with nothing having
changed but time.
"Not linked" is the worst state to get wrong, because it is the one that tells
the user to go and run a command. The instruction was the false part.
Two changes. The tile now resolves through `resolveBindingOutcome` rather than
reading the cache, which asks the server on a miss and is already safe to poll:
a confirmed "unbound" is memoized for MISS_TTL_MS and a known binding is trusted
for REVALIDATE_MS, so the worst case is one request per five minutes — the same
bound the counts fix uses. And "unknown" (unreachable, 5xx) now leaves the last
answer standing instead of collapsing to null, so a blip cannot downgrade a
working tile to "Not linked"; `resolveBindingOutcome` exists precisely to keep
those two apart and this caller was throwing the distinction away.
The signal also carries a third state: `undefined` for "the first read has not
returned", `null` for "resolved, genuinely unlinked". Starting at `null` meant
the very first paint asserted a state nothing had checked yet.
Verified from a cold state directory against prod: the tile shows the workspace
and its counts on first paint, and the "Not linked" flash is gone. 496 tests
pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
Code Review SummaryThis review did not run. Your provider API key hit its rate limit, so the Previous Review Summaries (3 snapshots, latest commit 11e3084)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 11e3084)This review did not run. Your provider API key hit its rate limit, so the Previous review (commit 11e3084)Status: 10 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (9 files)
Fix these issues in Kilo Cloud Previous reviewThis review did not run. Your provider API key hit its rate limit, so the |
There was a problem hiding this comment.
All reported issues were addressed across 8 files
Not reviewed (too large): packages/opencode/src/provider/models-snapshot.ts (~2 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…king it wait Unlink took up to thirty seconds to show in the tile. The sidebar polls every POLL_MS and nothing published a binding change, so the pane kept naming the workspace long after the server and the local cache had both dropped it. What the user saw was a green "Unlinked from X" toast sitting beside a panel still reporting "X · 3 memories" — the UI contradicting itself, with the stale half looking the more authoritative of the two. The lag is 0-30s depending on where in the cycle the action lands, so it is also inconsistent between attempts, which reads as flakiness rather than latency. `state.ts` now keeps a listener set, notified from the two places that actually change a binding: `forgetBinding` (which every unlink and every server-says- unbound resolve funnels through) and `recordApprovedBinding`, the latter only when `bindingChanged` — a warm cache re-read is not a change, and waking the tile on every resolve would give back what the poll interval buys. A plain listener set rather than an event bus: every writer already funnels through this module, so one hook covers link, unlink and rebind, where a bus would mean threading a dependency through each writer for a single subscriber. The interval stays as the backstop — it is what catches a change made by another process, which no in-process notifier can see. Two details worth their lines. `notifyBindingChanged` is called OUTSIDE `forgetBinding`'s try: a listener is a UI refresh, its failure is not a failed cache drop, and notifying from inside logged a throwing subscriber as "could not drop a binding" — a misleading line about a write that had already succeeded. And the tile's in-flight guard now coalesces instead of dropping: a notification arriving mid-poll used to return early, leaving the tile stale until the next tick, which is precisely the lag this removes. Tests: 501 pass, 5 new. Mutation-checked — never notifying on a drop, notifying on a warm re-record, and rethrowing a listener error each fail a test. One mutation survives knowingly: notifying even when the cache write failed costs a redundant refresh and changes nothing a user can see, so it has no test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Seven findings, all valid. Two of them correct claims I made in earlier commit messages, which is worth saying plainly. **The poller was still hitting the network for enabled workspaces.** I claimed it asks "not at all once it is yes". True for sixty seconds — `memoryEnabled`'s positive TTL — after which an ENABLED workspace went back to the wire on every other tick, a steady drip of `/datamates` for the life of the session. The poller now keeps its own memo of BOTH answers on a five-minute TTL. **A transient network error was rendered as "0 not synced".** `memoryStatus` is deliberately three-way, with a comment saying an unreachable service must not be reported as "this workspace has no memory" — and then the poller path called `memoryEnabled`, which folds error into `false`, memoized that for five minutes, and `memoryCounts` turned it into `unsynced: 0`. A failed request rendered as "your memory is up to date". `unsynced` is now `number | null`; null means "not known", and both call sites print the bare count instead of claiming zero. This is the same defect as the sync toast, one layer down: an error wearing the costume of a clean answer. **The poller memo was keyed by workspace id alone.** Ids are tenant-local, so after an account switch a same-numbered workspace in the new tenant inherited the old tenant's answer for the whole TTL. Keyed by tenant and API URL now, the same scoping the binding cache already uses. **Unlink did not notify when the cache write failed.** I had guarded the notification on a successful drop and called the difference unobservable when a mutation survived. That was wrong: the server-side unlink has already happened, and the resolve path does not depend on this file being rewritten — it drops the revalidation stamp and records a lookup miss, so the next resolve hears "unbound" regardless. Guarding on the write meant the pane kept naming a workspace the project was no longer bound to, in the case where something had already gone wrong. **A rename did not wake the sidebar.** `bindingChanged` comes from `sameBinding`, which compares identity — id, remote, path — because it also gates the memory seed; widening it would re-seed a workspace on every rename. The tile renders the name, so the rename is checked separately. Also: the tile clears counts and the manage URL when the workspace actually changes, so a rebind cannot show the old numbers under the new name (an "unknown" outcome still leaves them standing, rather than blanking a working tile over a blip); and a queued refresh no longer starts after the view is disposed. Tests: 504 pass, 3 new. Mutation-checked — reporting unknown as 0, memoizing an error as "disabled", and ignoring renames each fail a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/altimate/workspace/manage.ts">
<violation number="1" location="packages/opencode/src/altimate/workspace/manage.ts:216">
P2: When the poller returns `unknown`, this line encodes it only as `unsynced: null`, but both status consumers render that as an ordinary memory count. Expose an explicit unknown state or render `unknown` so an outage is distinguishable from disabled/current, as promised by the status contract.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| // and say nothing about sync. | ||
| if (status !== "enabled") { | ||
| const blocks = await MemoryStore.listAll({ directory }) | ||
| return { local: blocks.length, unsynced: status === "disabled" ? 0 : null } |
There was a problem hiding this comment.
P2: When the poller returns unknown, this line encodes it only as unsynced: null, but both status consumers render that as an ordinary memory count. Expose an explicit unknown state or render unknown so an outage is distinguishable from disabled/current, as promised by the status contract.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/workspace/manage.ts, line 216:
<comment>When the poller returns `unknown`, this line encodes it only as `unsynced: null`, but both status consumers render that as an ordinary memory count. Expose an explicit unknown state or render `unknown` so an outage is distinguishable from disabled/current, as promised by the status contract.</comment>
<file context>
@@ -194,16 +197,24 @@ export async function sync(directory: string): Promise<SyncReport> {
+ // and say nothing about sync.
+ if (status !== "enabled") {
+ const blocks = await MemoryStore.listAll({ directory })
+ return { local: blocks.length, unsynced: status === "disabled" ? 0 : null }
+ }
}
</file context>
There was a problem hiding this comment.
By design, and the contract is kept: unsynced: null means no claim. Both renderers only ever print a count when it is a known non-zero backlog (unsynced > 0), so an outage renders as N memories — the same as the 0-backlog case, which is also silent. The sidebar surfaces a problem only when it knows there is one; it never prints "0 not synced" on the strength of a failed request, which was the P1 this contract was introduced to prevent. Rendering the word "unknown" every time a poll blips would make a 30s poll cadence noisy for no action the user can take. Leaving as is.
#1279 sits on top of #1278, and three commits landed on the base while this branch moved — the symlink guard, the three unlink defects, and the status/sweep gating fixes. GitHub had this PR as CONFLICTING. Both conflicts were additive rather than semantic: each side inserted new code at the same point, and git could not tell they were independent. `state.ts` — the base added `forgetBindingUnscoped` (the no-credentials unlink path) exactly where this branch added the binding-change listener registry. Both kept. The conflict split INSIDE `notifyBindingChanged`, so the closing braces after the marker belonged to only one of the two blocks and the naive resolution left the function unterminated; restored. `manage.test.ts` — the two import blocks are a union, not a choice: `onBindingChanged` and `resetPollMemoForTests` from this branch, `resolveProjectIdentifier` and `pendingCount` from the base. 508 tests pass, typecheck clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
The base took the second review round — six finishing defects — and three of
them landed on code this branch had also reworked. One of the four conflicts
was a genuine design collision rather than a textual one.
**`status()`.** The base made it cache-only for the workspace's memory setting,
because the `/workspace` menu awaits it before the dialog can open and a 15s
network budget there read as a frozen menu. This branch had given it a
bounded-network poller path, because on a cold cache nothing else would ever
warm the answer and the sidebar counts simply never appeared. Both are right
for their caller, so `status` now takes `{ poll }`: the sidebar sets it and may
ask on the rate-limited path; the menu leaves it unset and reads cache alone,
reporting `null` for not-known. Neither ever renders unknown as zero. The base's
resolver-based binding lookup is kept for both.
**`syncMessage`.** The base's version is a superset — deferred-aware, same
refused/failed guards — and replaces this branch's. The sync-message test file
was added on both sides; the union keeps the base's six (including the two
deferred cases) plus this branch's three that the base lacked (transport
failures not hidden behind a refusal, "all" only when it was all, leading with
what happened rather than a zero).
`manage.test.ts` imports unioned; two tests that called
`status(dir, { allowNetwork: false })` now call `status(dir, { poll: true })`.
514 tests pass across workspace + plugin, typecheck clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
…ate reasons Eight defects from the bot reviews on #1279, each with a test that fails without its fix. - `lastSuccessfulSyncAt` reads the managed manifest's mtime instead of the in-memory map. The map is per thread, and the per-message sync that stamps it runs in the server worker while the sidebar renders on the main thread — so the "skills synced Xm ago" line never saw the syncs that happened. - The poller no longer re-asks `/datamates` through `pendingCount`'s gate once its own scoped memo says enabled (`trustEnabled`). Without it the sidebar dripped one request per minute after the write path's 60s positive expired — the exact drip the five-minute memo exists to stop. - The poller no longer consults the bare-id `memoryEnabledCache` at all, and asks `memoryStatus` fresh on a memo miss. Workspace ids are tenant-local; the shared cache reopened a 60s window where a positive from the previous account was served to a same-numbered workspace in the next. - `SyncReport.gatedBecause` names why a sweep never ran; `syncMessage` stops telling the user "memory is off" when the real reason was a failed local read, a missing binding, or the build flag. - `lastValidatedAt` is stamped only when the server says bound. Stamping on unbound meant a persistently failing `forgetBinding` write let the next resolve trust the stale row for a whole revalidation window. - The sidebar clears detail and manage URL on an account switch even when the new workspace has the same id (`boundScope` vs the credentials' scope). - `describeAge` rounds each label from raw elapsed ms; rounding twice had squeezed "1m ago" into a ~30s window. - Dead `dropped` in `forgetBinding` and an unreachable `setManageUrl(null)` removed; `resetPollMemoForTests` now clears every memo the poller touches. Verified: 518 pass across `test/altimate/workspace` + `test/altimate/plugin`, typecheck clean. Mutation-checked: reverting each of the manifest read, the `trustEnabled` gate, the read-failed reason, and the fresh status read fails its test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
`Math.round` still gave "1m ago" only 60–89s. Floor, per the review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
1 similar comment
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
There was a problem hiding this comment.
All reported issues were addressed across 8 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…tatus # Conflicts: # packages/opencode/src/altimate/workspace/manage.ts # packages/opencode/src/altimate/workspace/state.ts
|
👋 This PR was automatically closed by our quality checks. Common reasons:
If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you. |
…tatus # Conflicts: # packages/opencode/test/altimate/workspace/manage.test.ts
…ated toasts Three follow-ups from the review of the previous batch. - `lastSuccessfulSyncAt` reads a `.synced-at` marker the sync writes at the managed root on exactly the runs that stamp its in-memory map — clean ones. The manifest's mtime could not serve: a partial run publishes a manifest too, so a snapshot with a hole in it read as freshly synced, and a clean run that finds the snapshot up to date publishes nothing, so the age grew stale while syncs succeeded. A missing marker is `null`, not the in-memory stamp: after an unlink, a rebind or an empty workspace there is no snapshot for an age to describe. - A gated sweep is no longer a green success toast. Every count is zero when a sweep never ran, and the count-based rule coloured "Could not read this project's local memory" as success. `syncVariant` pairs with `syncMessage`: a failed read is a warning, the other gates are information. Verified: 531 pass across `test/altimate/workspace` + `test/altimate/plugin`, typecheck clean. Mutation-checked: writing the marker on failed runs, falling back to the map on a missing marker, and colouring a gated sweep as success each fail a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
`Number("")` is 0. The marker is validated as a positive integer before use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Q51zFUmPg1WwtS5CrGJE6
…tatus # Conflicts: # packages/opencode/test/altimate/workspace/manage.test.ts
…tatus # Conflicts: # packages/opencode/src/altimate/workspace/manage.ts
Issue for this PR
Part of #1272 — the discoverability half.
Type of change
What does this PR do?
The workspace pane names the workspace but says nothing about whether local state
has drifted from it. So there is no moment at which a user learns they have memory
the workspace never received, or skills that have not synced this session — which
means
/workspace synchas no discoverability problem to solve, because nothingever suggests running it.
Two lines under the existing name and manage URL:
Both are status, not affordances. The pane takes no input and none of the five
sidebar plugins does, so nothing here becomes clickable.
allowNetwork: falseis load-bearing, not a micro-optimisation. The sidebarrefreshes every 30s, and the memory-enabled cache is deliberately positive-only —
a workspace with memory switched off is never memoized, so switching it on is
picked up immediately. Asking the service on a cache miss would therefore put a
request on the wire every 30 seconds for the lifetime of the session, for exactly
the workspaces whose answer is "no". With the network forbidden, counts show when
the cache already knows and are omitted otherwise.
Unknown is reported as unknown in both places, deliberately. Treating unknown
enablement as enabled shows a backlog on a workspace that has memory off; treating
it as disabled hides a real one. Likewise
skillsSyncedAtis null for "unknown",not "never" — the store is per-process, so a fresh session has not synced yet even
where the on-disk snapshot is current, and the line is hidden rather than rendered
as "never synced".
Supporting changes:
skill-sync.lastSuccessfulSyncAt(recentlySyncedanswers aboolean against the poll interval, which cannot say "6 minutes ago") and
memory-sync.memoryEnabledCached(the no-network read).How did you verify your code works?
8 tests in the manage suite, 486 across the workspace and plugin suites.
Typecheck clean.
Mutation-checked: ignoring
allowNetwork— the polling regression — and treatingunknown enablement as enabled each fail a test.
Screenshots / recordings
Text-only sidebar lines; the shape is in the code block above.
Checklist
Known gaps
existing harness in this repo, so coverage stops at the data
status()returns.name a link. Whichever lands second takes the rebase.
🤖 Generated with Claude Code
Summary by cubic
Adds memory drift and skill-sync age to the workspace sidebar so users can see when
/workspace syncis needed. Sync feedback now distinguishes refused memories, transport failures, and gated sweeps, so failed or skipped work is never reported as success.Sidebar
.synced-atmarker written only after clean skill-sync runs; missing or malformed markers remain unknown.Sync toast
Written for commit 313a808. Summary will update on new commits.