Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ external implementation runs are durable.
- **Repository node IDs are authoritative.** Owner and repository names resolve
GitHub requests but never replace the stored node identity.
- **Persistence should precede publication.** Do not acknowledge or broadcast a
domain mutation before its fenced durable commit. `anchor_plan` currently has
a known ordering race described below; do not copy that pattern.
domain mutation before its fenced durable commit.

## Conversation and Planner addressing

Expand Down Expand Up @@ -311,9 +310,6 @@ so merge ranges from all mounted editors before replacing a registry entry.
does not currently remove every Planner label from the UI.
- A success callback for persisted sidecar work is not optional. Calling it
after persistence prevents durable transcript state from being dropped.
- `anchor_plan` calls the asynchronous question-placement publish without
awaiting it before separate sidecar persistence and anchor broadcast. Fix the
serialization before relying on its ordering guarantee.

### Browser and editor

Expand Down
23 changes: 20 additions & 3 deletions apps/server/src/agent/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ test("read_reference accepts only ids made available by the active chat session"
expect(reads).toEqual([available]);
});

test("anchor_plan publishes moving a decision beside the validated prose", async () => {
test("anchor_plan waits for decision placement before persisting and broadcasting anchors", async () => {
let { plan, server } = await opened(SOURCE, {
revision: 1,
questions: [{
Expand All @@ -308,14 +308,25 @@ test("anchor_plan publishes moving a decision beside the validated prose", async
});
let published: unknown[] = [];
let anchors = 0;
let started = Promise.withResolvers<void>();
let release = Promise.withResolvers<void>();
let finished = Promise.withResolvers<void>();
let persisted = false;
let anchorPlan = fixtureTools({
plan,
server,
room: "test",
persist: () => Service.persist(plan),
persist: async () => {
persisted = true;
await Service.persist(plan);
},
exclusive: action => Service.exclusive(plan, action),
publish: async mutation => {
started.resolve();
await release.promise;
await Service.publish(plan, server, "test", mutation);
published.push(mutation);
finished.resolve();
},
anchors: () => anchors++,
changes() {},
Expand All @@ -328,12 +339,18 @@ test("anchor_plan publishes moving a decision beside the validated prose", async
revision: plan.revision,
anchors: [{ widget: WIDGET, question: QUESTION, blocks: [{ index: 1, digest }] }],
};
let response = await anchorPlan.handler(args, {
let pending = anchorPlan.handler(args, {
sessionId: "session",
toolCallId: "call",
toolName: "anchor_plan",
arguments: args,
});
await started.promise;
let beforeCommit = { persisted, anchors };
release.resolve();
let response = await pending;
await finished.promise;
expect(beforeCommit).toEqual({ persisted: false, anchors: 0 });
if (typeof response !== "string") throw new Error("anchor_plan returned no text");
let result = JSON.parse(response);

Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/agent/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ export const documentTools = {
}
}
let mutation = Questions.place(context.plan, placements);
if (mutation) context.publish(mutation);
if (mutation) await context.publish(mutation);

await context.persist();
context.anchors();
Expand Down
2 changes: 0 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,6 @@ or route for a person to approve the draft. See
The prototype still has several places where implementation falls short of the
intended boundaries above:

- `anchor_plan` does not await a question-placement document mutation before its
separate sidecar persistence and anchor broadcast, leaving an ordering race.
- Idle-room eviction removes the registry entry before its asynchronous final
close and checkpoint completes, so a replacement room can briefly overlap.
- Browser CRDT updates do not cross-check record-owned decision projections, and
Expand Down
Loading