Skip to content

feat(workspace): a published deliverable lands in the shared tree, and the tab follows live (#552, #327) - #572

Merged
oxoxDev merged 19 commits into
tinyhumansai:mainfrom
oxoxDev:feat/552-publish-into-workspace
Aug 11, 2026
Merged

feat(workspace): a published deliverable lands in the shared tree, and the tab follows live (#552, #327)#572
oxoxDev merged 19 commits into
tinyhumansai:mainfrom
oxoxDev:feat/552-publish-into-workspace

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

publish_artifact drained into the ArtifactStore only, so a deliverable landed on its task card and nowhere else. The workspace tree is the one surface every agent can read, and there is no agent-facing artifact read tool anywhere in src/harness/ — so an agent asked to build on another agent's output had no way to reach it. Separately, workspace writes emitted no company event, so a file written while the operator watched the Workspace tab appeared only after a refresh.

Publishing now writes both places: the versioned artifact on the card, and a note under Agents/<agent>/<task>/ in the shared tree, linked so the two are one object seen twice. The tab follows writes live.

Closes #552. Closes #327.

API Or Behavior Changes

A published deliverable also becomes a workspace note. The drain calls ensure_agent_folder (member folders are minted on first use, per #570), creates the <task-id>/ child and any nested source folders, and writes the file. ArtifactVersion gains workspace_node_id (serde(default), additive on all three backends' JSON blob storage — no migration; pre-existing records load as None). A second agent reads the result through the existing workspace_read; no new tool was added, which is the point of routing through the tree rather than building a second reader.

The artifact chain stays authoritative; the note is its current-body projection. The alternative — node as storage, artifact referencing it — would force versioning onto WorkspaceStore across three backends and turn every artifact read into a two-store join. Invariant: the note never holds a body the version history has not recorded.

Writes are chain-first, uniformly. A re-publish inherits the node its previous version named, stamps the link, stores the version, then mirrors into the note. A fresh publish has no id to inherit, so v1 is stored unlinked, the node is minted, and a second write stamps the link. Ordering is pinned by failure injection, not by comment: an ArtifactStore that refuses upsert from the Nth call leaves the note on the previous body, and a refused first publish creates nothing in the tree at all. Reverting to node-first makes both tests fail with the divergence they exist to catch.

Residual, stated rather than hidden: if the second artifact write fails after the node write succeeded, the version and the note both hold the same body and only the link between them is missing. materialize re-adopts by path, so the next publish of the same source reuses the same node and repairs the link — pinned by an_unlinked_first_publish_is_repaired_by_the_next_one. It requires a store failing mid-sequence, not a store being down, and it never misreports what a human changed.

Edits on either surface stay in step. A console PUT to a published note appends an Operator version before writing the note; append_version mirrors the new body into the linked note; an agent's workspace_write over a published note records onto the chain as an Agent version. Without these, an edit on one surface is invisible to the other and #187's human_edit_diff silently rots. Appended versions inherit the node id on all three paths — otherwise the next reverse lookup finds nothing and mirroring stops after one hop.

New CompanyEvent::WorkspaceChanged { node_id, change } (opened / updated / removed), classified Prunable — a pure refetch signal: the tree is the sole truth, nothing addresses it by sequence, nothing reads it back at boot, and it is high volume. This diverges from TaskCardChanged's Permanent, where the board-lifecycle trail is itself evidence; flagging it since the default direction is keep-forever. A WorkspaceAnnouncer decorator emits at the store — mirroring BoardAnnouncer, whose docs name #327 as "the obvious next one" — so publish dual-writes, console PUTs, agent writes and deletes all announce with no per-caller emits. Folder deletion announces once, not once per descendant. Projected onto the operator SSE stream as workspace_changed; the stream is deny-by-default, so the explicit arm is what makes #327 actually fix.

Console: the Workspace tab refetches the tree on a live event and refetches the open file only in read mode, so an in-progress edit is never clobbered; a removed open node closes the pane. Artifact versions carrying a node id show "Open in workspace", deep-linking via the hash router.

Also in this PR, unrelated and standalone (5956f6c8): app::boot::test::releasing_the_instance_frees_the_root no longer asserts that a released data root is re-acquirable in the same instant. src/store/lock.rs documents why that is stricter than the lock promises — the lock lives on the open file description, and a fork() before exec() briefly shares it. The test now retries to a 2 s ceiling and reports the attempt count with the last real error. The lock itself is unchanged. Verified not to be a blindfold: holding the first instance instead of dropping it fails the test in 2.01 s after 158 refused attempts.

Tests

  • cargo fmt --all -- --check
  • cargo clippy --locked --no-deps --all-targets -- -D warnings
  • cargo clippy --locked --no-deps --all-targets --features openhuman,tinycortex -- -D warnings
  • cargo build --all-targets (via cargo check --locked --all-targets and --all-features --all-targets)
  • cargo test — 1813 passed, 0 failed
  • cargo test --locked --features sqlite --lib store::sqlite — 25 passed
  • cargo test --locked --features openhuman,tinycortex --tests — 2699 passed, 0 failed
  • frontend: npm run typecheck, npm run build, npx vitest run — 22 files, 236 tests passed

New coverage: materialize (folder find-vs-create through the minter, nested source segments, deleted-node recreate, same basename in two directories); the drain (dual-write, None-workspace no-op preserving today's behaviour byte-for-byte, node failure still records the artifact, re-publish updating the same node, the version carrying the id); four failure-injection tests pinning write ordering in both directions; the mirror hooks (a PUT on a published note appends an Operator version before the note write and human_edit_diff answers; a PUT on an unpublished note appends nothing; append_version mirrors); the announcer (cloned from BoardAnnouncer's suite shape, including a refusing log not failing the write); serde round-trips; the SSE projection; and an end-to-end test proving agent A publishes and agent B reads it.

Documentation

Module docs on the new src/company/artifact_mirror.rs, the announcer, and the amended publish drain carry the ordering argument and the residual. Note on placement: the plan put this module under src/harness/, which is #[cfg(feature = "openhuman")], while its callers in src/server/ops/ are always compiled — a default build would not have linked. It lives in src/company/ instead, following the workspace_links.rs precedent.

One test-only consequence worth recording: the boot scaffold's WorkspaceChanged frames consume journal sequence 0, and wire::cycle_id embeds the first event's sequence. Five hosted-brain tests hardcoded cycle_id(…, 0) and two asserted total journal length. Production is unaffected (the id is derived at runtime), but those tests encoded an assumption that is now false. The count is derived from the system-roots list so adding a root cannot silently desynchronise it again.

Related

Third of a sequence making the workspace a shared resource space:

Summary by CodeRabbit

  • New Features
    • Workspace now starts with only Agents/ and Desks/ roots; individual folders are created when needed.
    • Published artifacts are mirrored to workspace notes, with edits synchronized back to artifact history.
    • Workspace updates refresh views live, support direct note links, and close deleted notes automatically.
    • Artifact details now include an “Open in workspace” action.
  • Documentation
    • Updated workspace behavior, creation rules, and artifact synchronization guidance.
  • Bug Fixes
    • Improved handling of workspace collisions, missing folders, failed synchronization, and event delivery.

oxoxDev added 12 commits August 10, 2026 23:00
…tinyhumansai#552)

A published deliverable is about to live on two surfaces: the artifact
chain, which stays the authoritative version history, and one node in the
company's shared workspace tree, which holds the current body. This is the
link between them.

Per version rather than per record, for the same reason `run_id` is: an
operator may delete a published node, and the next publish materializes a
fresh one. A record-level field would rewrite history and claim the old
versions had always lived in the new node.

Additive on the JSON blob every backend stores, so no migration — a
pre-tinyhumansai#552 record loads with None.
The test dropped an instance and re-acquired the root in the same instant.
That is stricter than the lock promises. `store::lock`'s "The fork window"
section says so outright: the lock belongs to the open file description, so
between fork() and exec() a child transiently shares every descriptor its
parent held. This binary spawns subprocesses in sibling tests, and one
landing in the release window keeps the just-released root locked for the
microseconds until the child execs and its O_CLOEXEC copy closes.

Retry with a 10ms backoff up to 2s — several orders of magnitude above the
real window — and fail with the attempt count and the last refusal if it
expires. Verified not to be a blindfold: holding the first instance instead
of dropping it still turns the test red, in 2s, naming the real error.

No change to the lock itself; it was behaving as documented.
…ee (tinyhumansai#552)

The seam between a task artifact and the workspace tree, in both
directions: materialize() puts a publish at Agents/<agent>/<task>/<source>,
minting the agent's folder on demand; mirror_node_edit() records an edit to
a published node back onto the artifact chain.

The chain stays authoritative and the node is a projection of its latest
version. The two failure modes are not symmetric — a stale node is visible
and self-healing, an unrecorded edit is silent and corrupts
human_edit_diff — so writes go chain-first wherever there is a choice.

Interior path segments become folders, so specs/a.md and docs/a.md are two
deliverables rather than one. Ambiguity is refused, never guessed, matching
the fail-closed rule workspace_scaffold applies one level up.

In the default build, not the harness: the console's workspace and artifact
routes are always compiled and could not reach an openhuman-gated module.
… artifact store (tinyhumansai#552)

The publish drain filed a versioned ArtifactRecord and stopped, so an
explicitly published file was reachable only from the Artifacts tab of one
card — invisible to the operator browsing the tree and to every other
agent, whose sole view of shared company state is that tree.

The drain now also materializes the file at Agents/<agent>/<task>/<source>
and stamps the node id onto the version it wrote. A re-publish revises the
same node; an operator's deletion is honoured with a fresh one.

A failed node write logs at error and records the artifact anyway: dropping
an explicitly published deliverable over tree bookkeeping would be far
worse than one the operator has to reach through the Artifacts tab.

deps.workspace == None stays byte-identical to the old behaviour, which is
what keeps every pre-existing publish test on the unchanged path. The drain
remains the single write site — the claim sites and PendingPublishQueue are
untouched, so an unclaimed publish still refuses and creates neither.
…inyhumansai#552)

A note in the workspace tree may now be the projection of a task artifact,
so the two console routes that write one have to keep the pair honest.

The workspace PUT records an operator's save of a published note as an
operator version, which is precisely the datum human_edit_diff exists to
answer; without it the history would claim the agent's draft shipped
unchanged. Chain first, then the node — a stale node is visible and heals
on the next write, an unrecorded edit is silent and permanent.

The artifact append pushes the new body into the deliverable's note, or the
tree keeps serving a draft the history has superseded. A mirror failure
warns rather than failing a request that already recorded the version.

Both appended versions inherit the node id, without which mirroring would
silently stop after one hop. An ordinary note matches no artifact and takes
the unchanged path.
… chain (tinyhumansai#552)

A note in the shared tree may be another agent's published deliverable. An
overwrite the artifact chain never saw is the same silent divergence a
console save would cause, one surface over — and the Artifacts tab and
human_edit_diff read the chain, not the tree.

Recorded as an agent version stamped with the writing agent's id, so a
teammate's overwrite can never masquerade as the human edit the port exists
to isolate. It is not a publish: no queue, no claim, tinyhumansai#445 untouched.

Node first here, and forced rather than chosen — the write carries the
compare-and-swap token, so until it returns there is nothing to record and
a version appended earlier would claim an edit a stale-revision refusal
never made. A mirror failure warns rather than reporting a landed write as
failed.

The artifact store is threaded in as an optional builder argument; None
keeps every read tool and every path-resolution test on the old path.
…sai#327)

TaskCardChanged's counterpart for the note tree, missing for the same
reason: nothing on the feed said the workspace had moved, so a console with
the Workspace tab open saw an agent's write only on a manual refresh. Now
that agents create notes and published deliverables land in the tree, that
stale window is where most of the tree's activity happens.

Prunable, unlike its Permanent board sibling, and argued in-code rather
than assumed: the tree is the record, nothing addresses this by sequence,
nothing folds it at boot, and its whole meaning is 're-read the tree'.

Structural only on every surface — id and change word, never the node name.
A note's name is operator-authored free text, and none of the four consumers
(SSE, insight one-liners, the Medulla wire, the cycle classifier) is a place
to hand it out. The SSE projection is deny-by-default, so the explicit arm
there is what makes the frame reach the console at all.
BoardAnnouncer's counterpart for the note tree, wrapped in at the one place
the store is chosen so the console routes, the agent tools, the publish
drain and the seeder all announce without knowing they do. An emit per
write site is the shape of the bug: correct only for the paths somebody
remembered.

A rename that changes nothing says nothing, compared against what the tree
actually held rather than what the caller asked for. Deleting a folder is
one frame, not one per descendant — the console re-reads the tree and finds
everything that went with it. A refusing event log never fails the write.

An overwrite always announces, unlike the board's identical-re-save
suppression: a write advances the revision token the agent CAS compares
against, so it has changed the node in the way that matters.
…nyhumansai#327)

Boot's workspace scaffold now journals a WorkspaceChanged per reserved
root, so a fresh company's log is no longer empty and its first cycle event
no longer lands at seq 0.

Three tests encoded that as a count or a hardcoded cycle id. They now
filter to the event they are about, and the hosted cycle id is derived from
SYSTEM_ROOTS so adding a root cannot silently desynchronise the scripted id
from the one the runtime computes.
The tab could only learn about a write by refetching on refresh or window
refocus. That was tolerable when the operator was the only writer; it is
not now that agents create notes and published deliverables land in the
tree, because most of what appears there appears while nobody is touching
it.

This subscriber carries the payload rather than a counter, unlike the
board's: the tree is always refetched, but what happens to the OPEN note
depends on which node moved — leave it, refetch it, or close the pane
because it was deleted.

Two rules with teeth. The open note is refetched only in read mode, so a
live write can never clobber an operator's dirty buffer and in-flight
autosave. And no toast for any change word: an autosaving editor fires one
frame per settled keystroke burst, and toasting those would train the
operator to dismiss the toasts that matter.

handleEvent is exported and its routing pinned by a test — this switch has
been bitten three times by a frame the host was already sending falling
through to default: and vanishing with nothing to debug.
…humansai#552)

A published deliverable also lives in the shared workspace tree, which is
where teammates and the operator actually browse; without a control the two
surfaces were related only in the host's data.

Read off the shown version rather than the record, because the node id is
per version: an older revision points at the note that held it, which the
operator may since have deleted. The hash router follows hashchange, so
setting the hash is the whole navigation — no navigate prop threaded down
for one link. WorkspaceView resolves the landed id against the host, since
only it knows which node ids exist.
…yhumansai#552)

The drain materialized the note first and stored the version after, so a
failed artifact write could leave the tree holding a body the version
history had no record of. That is not a lesser bug than the one the
chain-first rule was chosen to prevent — it is the same tinyhumansai#187 rot arriving
through the tree: human_edit_diff computed from the chain goes quietly
wrong rather than loudly broken. Needing the store to half-fail bounds how
often that happens and not at all how bad it is, and on a data path a
silent wrong answer outlives the incident that caused it.

A re-publish inherits the node its previous version named, so the version
is stored before the tree is touched. A fresh publish has no id to inherit,
so its v1 is stored unlinked and a second write stamps the link once the
node exists — which also means a note is now only ever created for a
deliverable already recorded, so this path can no longer leave a note in
the tree with no artifact behind it.

A failed link is warned, not fatal: both surfaces already hold the body and
only the pointer is missing, so failing would discard the rest of the
batch to report something the next publish repairs — materialize
find-or-creates by path and re-adopts the same note rather than duplicating.

Pinned by failure injection rather than by comment. Reverting to node-first
turns two of the new tests red, one of them asserting exactly the rot: the
tree reads v2 while the chain holds only v1.
@oxoxDev

oxoxDev commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

Merge-order note for reviewers: this branch is stacked on #571, so its diff currently includes #571's three commits (Agents//Desks/ scaffolding and on-demand member folders). #571 should merge first; this PR's own commits start at 69f88298. Once #571 lands I'll merge main forward here so the diff narrows to just #552 + #327 — no rebase, no force-push, so review comments stay anchored.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

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

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

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 53d8b233-801d-418c-90a0-3cd5cdb71998

📥 Commits

Reviewing files that changed from the base of the PR and between 14c93ca and 4974b10.

📒 Files selected for processing (9)
  • docs/spec/runtime/manifest.md
  • frontend/src/views/WorkspaceView.tsx
  • frontend/test/unit/workspace-open-note-plan.test.ts
  • src/company/artifact_mirror.rs
  • src/harness/orchestrator.rs
  • src/harness/workspace_tools.rs
  • src/runtime/cycle.rs
  • src/server/ops/workspace.rs
  • src/server/ops/write_test.rs
📝 Walkthrough

Walkthrough

Workspace provisioning now creates only reserved roots and lazily creates member folders. Workspace writes emit SSE events. Published artifacts mirror into workspace nodes and preserve node links across revisions. The operator UI refreshes workspace content and supports artifact-to-workspace navigation.

Changes

Workspace provisioning and event pipeline

Layer / File(s) Summary
Lazy workspace scaffolding
docs/..., src/company/workspace_scaffold.rs, src/runtime/builder.rs, src/harness/..., src/server/...
Boot creates Agents/ and Desks/. Agent and desk folders are created on demand with validation, attribution, adoption, and collision handling.
Workspace change event pipeline
src/ports/types.rs, src/runtime/workspace_events.rs, src/server/operator.rs, src/runtime/...
Workspace mutations emit structural WorkspaceChanged events that flow through persistence, SSE projection, and runtime classification.
Artifact and workspace synchronization
src/company/artifact_mirror.rs, src/ports/artifacts.rs, src/harness/..., src/server/ops/...
Published artifacts materialize in the workspace. Agent and operator edits append artifact versions while preserving workspace node references.
Operator workspace updates
frontend/src/..., frontend/test/unit/workspace-events.test.ts
The console handles workspace SSE events, refreshes affected notes, supports deep links, and links artifact versions to workspace nodes.
Runtime and integration validation
src/app/boot.rs, src/server/graphql/test.rs
Tests cover delayed lock reacquisition and the new workspace tree and event behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ArtifactPublisher
  participant ArtifactStore
  participant WorkspaceStore
  participant WorkspaceAnnouncer
  participant OperatorSSE
  participant WorkspaceView

  ArtifactPublisher->>ArtifactStore: Persist artifact version
  ArtifactPublisher->>WorkspaceStore: Materialize or update workspace node
  WorkspaceStore->>WorkspaceAnnouncer: Complete workspace mutation
  WorkspaceAnnouncer->>OperatorSSE: Emit workspace_changed
  OperatorSSE->>WorkspaceView: Forward nodeId and change
  WorkspaceView->>WorkspaceStore: Refresh tree or open note
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: m3ga-mind, senamakel

Poem

A rabbit watched the workspace grow,
Two roots appeared in tidy rows.
Notes hopped in, events took flight,
Artifacts linked from day to night.
“Refresh!” cried Bun, “the tree is bright!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: publishing deliverables into the shared workspace and live Workspace tab updates.
Linked Issues check ✅ Passed The implementation satisfies the coding objectives for artifact mirroring and live workspace updates in [#552] and [#327].
Out of Scope Changes check ✅ Passed The changes remain within the linked objectives, including supporting provisioning, event handling, tests, and boot-lock timing adjustments.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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.

❤️ Share

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

@senamakel senamakel self-assigned this Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (4)
src/server/graphql/test.rs (1)

841-849: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the maya parent path.

The assertion compares only node names. It can pass if maya is created outside Agents, while the workspace contract requires agent content under Agents/<agent-id>/.... Include the parent or path in this query, or add a targeted workspace-store assertion, so the test verifies placement as well as authorship.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/graphql/test.rs` around lines 841 - 849, Extend the test around
the tree lookup to verify maya’s parent path is Agents/<agent-id>, not merely
that a maya-named node exists. Query or inspect the parent/path metadata for
maya and assert it belongs under Agents, while preserving the existing name and
createdBy assertions.
src/company/artifact_mirror.rs (1)

258-268: 🚀 Performance & Scalability | 🔵 Trivial

Note the scan's real cost dimension before it needs an index.

published_record_for_node loads every artifact in the company on every workspace save. An ArtifactRecord carries all of its version bodies, so the cost grows with total stored artifact content, not with the record count. The doc bounds it by "a task's drafts and posts, not a repository", which describes the record count only.

When you add the index, a node_id → artifact_id reverse map is enough; it avoids deserializing version bodies for a lookup that only needs one field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/company/artifact_mirror.rs` around lines 258 - 268, Update
published_record_for_node to avoid scanning and deserializing all artifact
version bodies; use an indexed node_id-to-artifact_id reverse lookup that
returns the matching artifact record directly. Revise the associated
documentation or bounds to describe the cost in terms of stored artifact
content, not merely record count, while preserving the existing optional result
behavior.
src/runtime/workspace_events.rs (1)

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

Add coverage for a parent-only move.

This test covers no-op and name changes. It does not cover the parent_id comparison in WorkspaceAnnouncer::rename_move.

Create a folder, move n-1 into it with Some(Some("folder-id")), and assert one updated event. This protects the move behavior from regressing while rename behavior remains correct.

Proposed test addition
+        let folder = WorkspaceNode {
+            kind: NodeKind::Folder,
+            ..note("f-1", "Specs", None)
+        };
+        store.create(&co, &folder, None).await.unwrap();
+        log.appended.lock().unwrap().clear();
+
+        store
+            .rename_move(&co, "n-1", None, Some(Some("f-1")))
+            .await
+            .unwrap();
+        assert_eq!(
+            changes(&log),
+            vec![("n-1".to_string(), "updated".to_string())]
+        );

As per coding guidelines, **/*.rs: Add focused tests with every behavior change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/workspace_events.rs` around lines 360 - 390, Add parent-only move
coverage to a_rename_announces_only_when_something_actually_moved: create a
folder, call rename_move for n-1 with no name change and Some(Some("folder-id"))
as the parent_id, then assert exactly one updated event. Keep the existing no-op
and rename assertions intact.

Source: Coding guidelines

src/runtime/cycle.rs (1)

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

Use journal-read bounds that cannot hide events after boot scaffolding.

The tests filter event types correctly, but fixed ten-event reads can truncate the records before filtering.

  • src/runtime/cycle.rs#L2498-L2513: read the complete test journal or derive the limit from boot records before selecting OperatorMessage.
  • src/runtime/scheduler.rs#L391-L405: use a full or derived limit before selecting the first ScheduleFired event.
  • src/runtime/scheduler.rs#L424-L430: use the same safe limit before counting the second ScheduleFired event.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/cycle.rs` around lines 2498 - 2513, Update the journal reads in
the cycle test at src/runtime/cycle.rs lines 2498-2513 and both scheduler tests
at src/runtime/scheduler.rs lines 391-405 and 424-430 to use a complete journal
or a limit derived from boot records, so scaffolding events cannot truncate
records before filtering; preserve the existing OperatorMessage and
ScheduleFired selection/counting assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/spec/runtime/manifest.md`:
- Around line 276-280: Update the documentation describing Agents/<agent-id>/
creation to state that both direct agent writes and artifact_mirror::materialize
via ensure_agent_folder can create the folder; remove the claim that direct
writing is the only creator while preserving the operator-only rename and delete
behavior.

In `@frontend/src/components/app-shell.tsx`:
- Around line 271-279: Scope workspaceEvent to the active client and company in
AppShell: reset it when either client or company changes, or include and
validate those scope values before WorkspaceView consumes event.nodeId. Ensure
events from a previous company or client cannot affect the currently open
workspace node.
- Around line 271-279: Update the workspace event state around workspaceEvent
and its setter to preserve pending events rather than replacing an earlier event
with a later one. Use a bounded queue or aggregate that retains every relevant
change for the currently open node, and update WorkspaceView to process those
pending changes so refresh, rename/move, and deletion actions are not lost while
preserving the existing tick-based duplicate-event behavior.

In `@frontend/src/views/WorkspaceView.tsx`:
- Around line 435-446: Update the removal handling in the WorkspaceView event
flow after loadTree refreshes nodes: determine whether the open file still
exists, including when an ancestor folder removal event does not match openId.
When it is gone, preserve any unsaved draft in an explicit recovery state before
closing the pane, and coordinate the pending autosave/debounce with that state
so flush cannot issue a failed write or discard text. Expose a save-as-new-note
action for the recovered draft, and cover both direct and ancestor-folder
removals during active editing.

In `@src/harness/orchestrator.rs`:
- Around line 784-791: Add a focused test alongside the existing summarize_event
tests for the CompanyEvent::WorkspaceChanged arm, asserting the summary contains
the change value and node_id while excluding node names and workspace/body text.

In `@src/runtime/cycle.rs`:
- Around line 1268-1272: Add focused Rust tests covering the updated
classification of CompanyEvent::WorkspaceChanged: verify it produces no cycle
task or thread ID when alone, and remains neutral when mixed with a dispatch or
addressed message so those events are not disqualified. Place the tests near the
existing cycle_task_id and cycle_thread_id coverage and preserve current
behavior for other event types.

In `@src/server/ops/workspace.rs`:
- Around line 286-299: Update mirror_node_edit to distinguish artifact lookup
failures from append failures, allowing the caller to continue the ordinary note
write when the artifact store cannot be queried while still propagating failures
after a published record is identified. Preserve fail-closed behavior for
published deliverables, and adjust the route documentation if the intentional
behavior remains to state that ordinary note saves depend on artifact-store
availability.

In `@src/server/ops/write_test.rs`:
- Around line 5651-5654: Align the documentation for the test around the
success-path assertions with its actual coverage: remove the claim that it
verifies version ordering when a node write is refused, or move that claim to
the failure-injection test that exercises the refusal path. Only add failure
injection here if this test is intended to cover that behavior.

---

Nitpick comments:
In `@src/company/artifact_mirror.rs`:
- Around line 258-268: Update published_record_for_node to avoid scanning and
deserializing all artifact version bodies; use an indexed node_id-to-artifact_id
reverse lookup that returns the matching artifact record directly. Revise the
associated documentation or bounds to describe the cost in terms of stored
artifact content, not merely record count, while preserving the existing
optional result behavior.

In `@src/runtime/cycle.rs`:
- Around line 2498-2513: Update the journal reads in the cycle test at
src/runtime/cycle.rs lines 2498-2513 and both scheduler tests at
src/runtime/scheduler.rs lines 391-405 and 424-430 to use a complete journal or
a limit derived from boot records, so scaffolding events cannot truncate records
before filtering; preserve the existing OperatorMessage and ScheduleFired
selection/counting assertions.

In `@src/runtime/workspace_events.rs`:
- Around line 360-390: Add parent-only move coverage to
a_rename_announces_only_when_something_actually_moved: create a folder, call
rename_move for n-1 with no name change and Some(Some("folder-id")) as the
parent_id, then assert exactly one updated event. Keep the existing no-op and
rename assertions intact.

In `@src/server/graphql/test.rs`:
- Around line 841-849: Extend the test around the tree lookup to verify maya’s
parent path is Agents/<agent-id>, not merely that a maya-named node exists.
Query or inspect the parent/path metadata for maya and assert it belongs under
Agents, while preserving the existing name and createdBy assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd646c51-c9ea-4230-aab6-9eba35e02403

📥 Commits

Reviewing files that changed from the base of the PR and between d67206f and 14c93ca.

📒 Files selected for processing (34)
  • docs/modules/runtime/README.md
  • docs/spec/runtime/api.md
  • docs/spec/runtime/manifest.md
  • docs/spec/runtime/ports-console.md
  • frontend/src/api/artifacts.ts
  • frontend/src/components/app-shell.tsx
  • frontend/src/hooks/use-events.ts
  • frontend/src/views/ArtifactsTab.tsx
  • frontend/src/views/WorkspaceView.tsx
  • frontend/test/unit/workspace-events.test.ts
  • src/app/boot.rs
  • src/brain/hosted/test.rs
  • src/brain/medulla/effects.rs
  • src/company/artifact_mirror.rs
  • src/company/mod.rs
  • src/company/workspace_agents.rs
  • src/company/workspace_scaffold.rs
  • src/harness/brain.rs
  • src/harness/build.rs
  • src/harness/mod.rs
  • src/harness/orchestrator.rs
  • src/harness/workspace_tools.rs
  • src/ports/artifacts.rs
  • src/ports/types.rs
  • src/runtime/builder.rs
  • src/runtime/cycle.rs
  • src/runtime/mod.rs
  • src/runtime/scheduler.rs
  • src/runtime/workspace_events.rs
  • src/server/graphql/test.rs
  • src/server/operator.rs
  • src/server/ops/artifacts.rs
  • src/server/ops/workspace.rs
  • src/server/ops/write_test.rs
💤 Files with no reviewable changes (1)
  • src/company/workspace_agents.rs

Comment thread docs/spec/runtime/manifest.md Outdated
Comment thread frontend/src/components/app-shell.tsx
Comment thread frontend/src/views/WorkspaceView.tsx Outdated
Comment thread src/harness/orchestrator.rs
Comment thread src/runtime/cycle.rs
Comment thread src/server/ops/workspace.rs Outdated
Comment thread src/server/ops/write_test.rs Outdated
@oxoxDev

oxoxDev commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

#571 is merged (817ae747), so main is merged forward here and the diff is now just #552 + #327 — 13 commits starting at 69f88298. No rebase, no force-push, so existing review anchors hold.

Rebased gates on the merged tree: cargo test 1822 passed, --features openhuman,tinycortex --tests 2713 passed, clippy and fmt clean.

…te (tinyhumansai#552)

Since tinyhumansai#552 the workspace PUT consults the artifact store on every save, to
ask whether the note is a published deliverable. That lookup propagated with
`?`, so an artifact-store fault rejected the save of an ordinary note — one
with no chain to protect, on a write that otherwise never touches that store.

`mirror_node_edit` now separates *cannot record* from *cannot tell*. A refused
append stays an `Err` and still fails closed: the store answered, so the node
is known to be a deliverable and must not be written behind a version that was
never appended. An unreadable store answers `Undetermined`, and the caller
decides — the console PUT warns and saves, the agent tool warns as before.

Folding the second case into the ordinary-note result is what would have
retired the fail-closed guarantee without anything appearing to change, so the
two are separate variants rather than one absent value.
… write (tinyhumansai#552)

The doc comment claimed the ordering was asserted — that the version is there
when the node write is refused — but the body only exercised the success path.

Inject the refusal instead of correcting the comment down: an artifact stamped
with a node id the tree does not have makes the lookup match and the append
run, then the real store refuses the write. No mock, and the ordering that
this route was designed around is now actually covered.
…inyhumansai#327)

The new match arms changed cycle_task_id and cycle_thread_id without tests.
Assert both halves of what neutral means: alone it claims no card and no
thread, and in a mixed batch it does not disqualify the dispatch or the
addressed message beside it — an agent writing a note mid-cycle is the
ordinary case, and treating that write as a rival would strip the stamp.
…tinyhumansai#327)

summarize_event's new arm had no coverage. Assert the exact string rather than
its parts: the claim is that nothing else is in there, and an arm widened to
look up the node's name would keep passing every `contains` check while
leaking operator-authored free text onto the insight surface.
…older

The direct-write path stopped being the only creator of Agents/<agent-id>/ in
this branch: artifact_mirror::materialize calls ensure_agent_folder before it
writes a published deliverable.
…mansai#327)

WorkspaceAnnouncer emits one `removed` frame naming the node somebody deleted
— a folder — never one per descendant. So an open note inside that folder
disappeared with no frame that ever said its id, the effect compared ids,
decided the frame was somebody else's, and returned. The pane stayed open on a
note that was gone, holding a draft and an armed autosave whose only possible
outcome was a 404. A direct removal cleared the draft with the debounce still
armed.

Disappearance is now settled against the refreshed tree, which is the only
thing that knows about descendants. When the open note is gone the debounce is
cancelled first, then any text the host has not already acknowledged moves
into a banner that offers it back rather than leaving with the note it was
written in.
@oxoxDev
oxoxDev merged commit 843d5c2 into tinyhumansai:main Aug 11, 2026
6 checks passed

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

             $0.3738 · 377,255 in / 86,606 out · 293,111 cached (78%) · z-ai/glm-5.2
critique:    $0.1925 · 125,806 in / 53,509 out · 101,185 cached (80%) · z-ai/glm-5.2
security:    $0.0799 · 108,514 in / 15,940 out · 86,960 cached (80%)  · z-ai/glm-5.2
tests:       $0.0636 · 70,529 in  / 12,920 out · 50,605 cached (72%)  · z-ai/glm-5.2
description: $0.0377 · 72,406 in  / 4,237 out  · 54,361 cached (75%)  · z-ai/glm-5.2

// re-reading it would only 404 and leave an error where a note was. Which
// notes vanished is settled against the refreshed tree, not against the
// frame's id; [`planOpenNote`] carries the reasoning.
useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium critique uncertain

Live-writes effect acts on a stale openId after the tree refetch

The effect captures openId, mode, draft, and openFile?.content from the render in which event.tick changed, then acts on them after await loadTree({ silent: true }). If the operator opens a different note during that refetch, the plan was computed for the old note, and the actions are applied to whatever is now open. In the reload branch, loadFile(openId) loads the stale note, overriding the operator's new selection. In the vanished branch, closeVanished calls setOpenId(null), which closes the new note the operator just opened — even though that note is untouched. The eslint-disable acknowledges that openId is read-not-watched, but the consequence goes beyond replaying a stale frame: it mutates state the operator has since changed. A ref mirroring openId (checked after the await) would let the effect bail out when the operator has moved on.

**[RULE] ** ·

@tinysweeper

tinysweeper Bot commented Aug 11, 2026

Copy link
Copy Markdown

What this change touches

29 files, +4245 -44 across 10 components. It reaches 3 untouched components (60 graph nodes walked).

flowchart LR
  n0["src/harness<br/>4 files +1020 -14"]:::changed
  n1["src/company<br/>3 files +945 -1"]:::changed
  n2["src/server<br/>4 files +650 -1"]:::changed
  n3["src/runtime<br/>5 files +573 -9"]:::changed
  n4["frontend/src<br/>5 files +444 -12<br/>1 finding"]:::flagged
  n5["frontend/test<br/>2 files +305 -0"]:::changed
  n6["src/ports<br/>2 files +238 -1"]:::changed
  n7["src/app<br/>1 file +41 -2"]:::changed
  n8["src/brain<br/>2 files +25 -2"]:::changed
  n9["docs/spec<br/>1 file +4 -2"]:::changed
  n10["frontend/src<br/>1 file reached"]:::impacted
  n11["src<br/>1 file reached"]:::impacted
  n12["src/ports<br/>1 file reached"]:::impacted
  n2 -->|12 refs| n12
  n0 -->|10 refs| n12
  n3 -->|9 refs| n12
  n0 -->|5 refs| n1
  n2 -->|4 refs| n11
  n3 -->|4 refs| n1
  n3 -->|3 refs| n10
  n3 -->|3 refs| n11
  n8 -->|3 refs| n12
  n12 -->|3 refs| n10
  n2 -->|2 refs| n1
  n2 -->|2 refs| n3
  n2 -->|2 refs| n10
  n8 -->|2 refs| n1
  n0 -->|1 ref| n10
  n0 -->|1 ref| n11
  n1 -->|1 ref| n11
  n3 -->|1 ref| n0
  n8 -->|1 ref| n3
  n11 -->|1 ref| n1
  n11 -->|1 ref| n3
  n11 -->|1 ref| n12
  n12 -->|1 ref| n1
  n12 -->|1 ref| n11
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
src/harness changed 4 +1020 -14
src/company changed 3 +945 -1
src/server changed 4 +650 -1
src/runtime changed 5 +573 -9
frontend/src changed 5 +444 -12 1 (medium)
frontend/test changed 2 +305 -0
src/ports changed 2 +238 -1
src/app changed 1 +41 -2
src/brain changed 2 +25 -2
docs/spec changed 1 +4 -2
frontend/src reached 1
src reached 1
src/ports reached 1
Changed files

src/harness

  • src/harness/brain.rs
  • src/harness/build.rs
  • src/harness/orchestrator.rs
  • src/harness/workspace_tools.rs

src/company

  • src/company/artifact_mirror.rs
  • src/company/mod.rs
  • src/company/workspace_scaffold.rs

src/server

  • src/server/operator.rs
  • src/server/ops/artifacts.rs
  • src/server/ops/workspace.rs
  • src/server/ops/write_test.rs

src/runtime

  • src/runtime/builder.rs
  • src/runtime/cycle.rs
  • src/runtime/mod.rs
  • src/runtime/scheduler.rs
  • src/runtime/workspace_events.rs

frontend/src

  • frontend/src/api/artifacts.ts
  • frontend/src/components/app-shell.tsx
  • frontend/src/hooks/use-events.ts
  • frontend/src/views/ArtifactsTab.tsx
  • frontend/src/views/WorkspaceView.tsx

frontend/test

  • frontend/test/unit/workspace-events.test.ts
  • frontend/test/unit/workspace-open-note-plan.test.ts

src/ports

  • src/ports/artifacts.rs
  • src/ports/types.rs

src/app

  • src/app/boot.rs

src/brain

  • src/brain/hosted/test.rs
  • src/brain/medulla/effects.rs

docs/spec

  • docs/spec/runtime/manifest.md

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. label Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Projects

None yet

2 participants