Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
96eaa70
Extract dormant Jev transport and map product migration
MaggieAppleton Sep 30, 2026
e9be4a0
Preserve stored decision identifiers during validation
MaggieAppleton Sep 30, 2026
7e63f4a
Extract conversation contract and source provenance validation
MaggieAppleton Sep 30, 2026
52662d9
Extract exact quote parsing with product regression fixtures
MaggieAppleton Sep 30, 2026
38f3476
Extract event and correction guards without changing policy
MaggieAppleton Sep 30, 2026
86dca57
Extract research-offer and snapshot guards
MaggieAppleton Sep 30, 2026
a3012b0
Extract accepted-event replay in bounded modules
MaggieAppleton Sep 30, 2026
a70dde9
Extract pure conversation domain with original regressions
MaggieAppleton Sep 30, 2026
964433f
Preserve research-offer domain regressions
MaggieAppleton Sep 30, 2026
90852cd
Preserve card authority and scoped Save regressions
MaggieAppleton Sep 30, 2026
929af95
Extract stable decision option growth
MaggieAppleton Sep 30, 2026
85397c6
Preserve selected option identifiers in answers
MaggieAppleton Sep 30, 2026
c1f9346
Extract decision record validation and ID bounds
MaggieAppleton Sep 30, 2026
a3d9b61
Preserve pending-card choice draft behavior
MaggieAppleton Sep 30, 2026
121fa9e
Restore shared definitions without replacing durable IDs
MaggieAppleton Sep 30, 2026
626918f
Extract Store option and lifecycle operations
MaggieAppleton Sep 30, 2026
169c52b
Preserve Store suggestion metadata and explicit submit fallback
MaggieAppleton Sep 30, 2026
21750d0
Restore canonical question records with saved draft and source checks
MaggieAppleton Sep 30, 2026
ed2b3a1
Extract exact interpretation request builders into bounded modules
MaggieAppleton Sep 30, 2026
ca64bd1
Retain exact correction and review regression scenarios
MaggieAppleton Sep 30, 2026
c6c4fc1
Preserve policy scoring identity and clarification helpers
MaggieAppleton Sep 30, 2026
9de5b06
Extract ordered initial policy terminal stages with exact regressions
MaggieAppleton Sep 30, 2026
9e45bed
Extract remaining policy terminal stages with exact regression coverage
MaggieAppleton Sep 30, 2026
7d063c5
Extract exact candidate grouping and competing-choice setup
MaggieAppleton Sep 30, 2026
a8db3d7
Extract candidate entry and captured role checks with preserved skip …
MaggieAppleton Sep 30, 2026
6ebbb24
Extract live event factories and early verification handling
MaggieAppleton Sep 30, 2026
0937d8d
Extract scoped and direct new-choice policy handlers
MaggieAppleton Sep 30, 2026
7b8d314
Extract withdrawal agreement and question policy proposals
MaggieAppleton Sep 30, 2026
53c2e18
Extract contribution proposal policy branch
MaggieAppleton Sep 30, 2026
6db8e58
Extract support and objection policy proposals
MaggieAppleton Sep 30, 2026
d4bee82
Extract resolution and reopening policy proposals
MaggieAppleton Sep 30, 2026
5b85392
Extract ordinary policy event application and follow-ups
MaggieAppleton Sep 30, 2026
c377340
Complete pure conversation policy dispatcher and fallback
MaggieAppleton Sep 30, 2026
d69d291
Restore bounded conversation interpreter and original regressions
MaggieAppleton Sep 30, 2026
0040159
Restore conversation effects and durable Planner job models
MaggieAppleton Sep 30, 2026
3f21533
Restore conversation processor and durable hosted state
MaggieAppleton Sep 30, 2026
d331af7
Restore Planner delivery and durable card projection boundaries
MaggieAppleton Sep 30, 2026
a9a74b4
Integrate durable conversation processing and scoped Planner effects
MaggieAppleton Sep 30, 2026
5d6d0d4
Integrate durable accepted research and drain admitted commands befor…
MaggieAppleton Sep 30, 2026
309a6a4
Restore Conversation Save controls and shared questionnaire state
MaggieAppleton Sep 30, 2026
340dbde
Retain suggestion edit lifecycle and original reducer regressions
MaggieAppleton Sep 30, 2026
a7f6bf6
Project advisory card choices through the existing question view
MaggieAppleton Sep 30, 2026
a4e3375
Forward optional conversation settings through container configuration
MaggieAppleton Sep 30, 2026
6dcf832
Connect authoritative card metadata to editor and Decisions
MaggieAppleton Sep 30, 2026
233162b
Restore explicit research offer controls in Conversation
MaggieAppleton Sep 30, 2026
6ed0a40
Restore card option composer and original provenance cases
MaggieAppleton Sep 30, 2026
369ecb5
Retain evidence projection and original commitment safeguards
MaggieAppleton Sep 30, 2026
f60a724
Restore terminal card actions and original source safeguards
MaggieAppleton Sep 30, 2026
0e9d398
Restore previous answers and reply validation regressions
MaggieAppleton Sep 30, 2026
942e1ba
Restore refining status and recovery regression coverage
MaggieAppleton Sep 30, 2026
a21f05f
Restore open Discard and scoped-choice regressions
MaggieAppleton Sep 30, 2026
f533166
Complete pipeline retention and verify native source and card contracts
MaggieAppleton Sep 30, 2026
eb86dbf
Restore evidence popover and exact source controls
MaggieAppleton Sep 30, 2026
ced067e
Restore analysis overview and guarded excerpt correction
MaggieAppleton Sep 30, 2026
c6c01a0
Restore transcript source navigation and verify native correction forms
MaggieAppleton Sep 30, 2026
4f36d29
Integrate live evidence hover and Room source navigation
MaggieAppleton Sep 30, 2026
b8d5613
Integrate conversation analysis controls and correction evidence
MaggieAppleton Sep 30, 2026
242af27
Restore decided-prose reader controls and native coverage
MaggieAppleton Sep 30, 2026
c26af66
Restore answered-card collapse and protected deletion
MaggieAppleton Sep 30, 2026
4a61031
Restore open-card people and conversation source navigation
MaggieAppleton Sep 30, 2026
0f8d733
Restore synced canonical card-gap normalization
MaggieAppleton Sep 30, 2026
eeb4a24
Restore legacy Planner thread backfill before room exposure
MaggieAppleton Sep 30, 2026
248236e
Serialize joined snapshots and wait for conversation attachment
MaggieAppleton Sep 30, 2026
3e67261
Protect browser projections and fence research reference changes
MaggieAppleton Sep 30, 2026
0841b22
test: preserve scripted Planner interpreter regressions
MaggieAppleton Sep 30, 2026
c311f63
test: exercise scripted Planner through current Harness
MaggieAppleton Sep 30, 2026
cec9159
Fix discarded decision summaries and suggested choice attribution
MaggieAppleton Sep 30, 2026
08ccdb4
Verify scripted refine recovery and retain browser harness helpers
MaggieAppleton Oct 1, 2026
05d6bdb
Add prompt-driven scripted Planner harness without activation
MaggieAppleton Oct 1, 2026
6f20335
test: verify prompt-only suggest and prose Planner profiles
MaggieAppleton Oct 1, 2026
052bcf1
test: preserve original jobs and prose browser suites without activation
MaggieAppleton Oct 1, 2026
5d026dd
test: add gated dedicated current-Harness application setup
MaggieAppleton Oct 1, 2026
7b69b14
test: fence dedicated Planner fetch and proxy transport
MaggieAppleton Oct 1, 2026
341286b
Retain complete Jev runtime and prompt browser suites
MaggieAppleton Oct 1, 2026
3a5577d
Connect decision opening and ready research links
MaggieAppleton Oct 1, 2026
0a8516b
Restore conversation live announcements
MaggieAppleton Oct 1, 2026
ebf4e2e
Retain complete research browser regression units
MaggieAppleton Oct 1, 2026
264b422
Retain complete evidence and anchor browser regression units
MaggieAppleton Oct 1, 2026
4f0af50
Retain complete sidecar browser regression units
MaggieAppleton Oct 1, 2026
2403af9
Isolate Jev config tests and document interpretation data sharing
MaggieAppleton Oct 1, 2026
ffc89ee
Retain complete conversation browser regression units
MaggieAppleton Oct 1, 2026
0f40f94
test: retain original research lifecycle and recovery source
MaggieAppleton Oct 1, 2026
6de3749
test: retain responsive regressions and scope document status selectors
MaggieAppleton Oct 1, 2026
233b2ec
test: retain sidecar spacing and evidence absence source
MaggieAppleton Oct 1, 2026
dd4217c
test: retain complete asynchronous sidecar editing source
MaggieAppleton Oct 1, 2026
5c6ae66
test current question view contracts in isolated Chromium
MaggieAppleton Oct 1, 2026
b282c01
prepare a contained conversation application test runner
MaggieAppleton Oct 1, 2026
153d9c8
Merge current main and retain Jev conversation contracts
MaggieAppleton Oct 1, 2026
b1ee0a5
Attest dormant Docker interfaces in contained conversation tests
MaggieAppleton Oct 1, 2026
90160e9
Validate sysfs control entries before namespace collection
MaggieAppleton Oct 1, 2026
67c18c0
Register scripted Planner before contained test discovery
MaggieAppleton Oct 1, 2026
b8b2fee
Capture passive startup evidence for contained reader case
MaggieAppleton Oct 1, 2026
340031a
test: profile contained reader startup without pausing
MaggieAppleton Oct 1, 2026
991b7e9
fix: skip hidden-card correction for unresolved selections
MaggieAppleton Oct 1, 2026
600aa53
test: capture a bounded reader startup stack
MaggieAppleton Oct 1, 2026
5b89e06
Merge remote-tracking branch 'origin/main' into Maggie/jev-chat-product
MaggieAppleton Oct 1, 2026
a423b0b
fix: keep reader questionnaire drafts disconnected
MaggieAppleton Oct 1, 2026
f6cc547
docs: hand off Jev feature for quality review
MaggieAppleton Oct 2, 2026
d22862a
Merge current main into Jev conversation feature
MaggieAppleton Oct 2, 2026
e4fc781
fix: retain stance targeting after option refinement
MaggieAppleton Oct 2, 2026
f2d845c
refactor: remove unused decision UI implementations
MaggieAppleton Oct 2, 2026
28afd3f
fix: stage Planner card creation and cancellation
MaggieAppleton Oct 2, 2026
8c17a27
fix: hide unsupported research offers in child documents
MaggieAppleton Oct 2, 2026
3d88095
test: run Planner success scenarios in contained release checks
MaggieAppleton Oct 2, 2026
f8a499b
test: verify conversation recovery across PostgreSQL processes
MaggieAppleton Oct 2, 2026
b03aa88
test: include prompt and evidence stress in Planner release checks
MaggieAppleton Oct 2, 2026
2296c7b
fix: retain only reachable Planner execution contexts
MaggieAppleton Oct 2, 2026
a1678cc
fix: reserve durable event capacity for decision mirrors
MaggieAppleton Oct 2, 2026
c565ee5
fix: include recently touched decisions in bounded model context
MaggieAppleton Oct 2, 2026
471c0f9
test: restore shared draft and option browser scenarios
MaggieAppleton Oct 2, 2026
7128714
test: contain actual conversation inference restart recovery
MaggieAppleton Oct 2, 2026
d88110d
fix: report exhausted conversation history consistently
MaggieAppleton Oct 2, 2026
dee55a1
test: select all retained conversation and research browser cases
MaggieAppleton Oct 2, 2026
d260671
docs: record review fixes and remove obsolete design exception
MaggieAppleton Oct 2, 2026
1696d5a
test: align integration fixtures with current workspace contracts
MaggieAppleton Oct 2, 2026
cb49cff
fix: forward decided prose anchors into the editor
MaggieAppleton Oct 2, 2026
6bc6022
test: preserve decision scenarios against the mounted interface
MaggieAppleton Oct 2, 2026
aac5dcc
test: complete mixed-message fixtures and retain prompt history checks
MaggieAppleton Oct 2, 2026
3d7a02c
test: verify evidence and research against current authored surfaces
MaggieAppleton Oct 2, 2026
68fc53b
test: target authored comment and decision state precisely
MaggieAppleton Oct 2, 2026
98c33a7
Keep pending option input cancellable and preserve focus intent
MaggieAppleton Oct 2, 2026
b7b79ef
test: verify live decisions through semantic text and native focus
MaggieAppleton Oct 2, 2026
5e9a978
docs: hand off verified Jev quality review and remaining limits
MaggieAppleton Oct 2, 2026
8bcdf9d
Merge current main while preserving Jev conversation behavior
MaggieAppleton Oct 2, 2026
51a11ca
Use main decision headers and one Discard action
MaggieAppleton Oct 2, 2026
02bf32e
Extend main decision reader with Jev lifecycle actions
MaggieAppleton Oct 2, 2026
9633a89
Restore left Chat using main workspace controls
MaggieAppleton Oct 2, 2026
d75b299
Record UI reconciliation and align remaining source controls
MaggieAppleton Oct 2, 2026
27eb372
Record live Jev browser flows and anchor regression
MaggieAppleton Oct 2, 2026
67c1c0b
Align browser checks with the reconciled Jev UI
MaggieAppleton Oct 3, 2026
e1d28fb
Align resolved-decision browser fixtures with current controls
MaggieAppleton Oct 3, 2026
8d5ba88
Preserve child navigation during parent metadata updates
MaggieAppleton Oct 3, 2026
343bd82
Reduce filler payloads in full-history capacity tests
MaggieAppleton Oct 3, 2026
25811ba
Fix saved decision prose target precedence (JEV-001)
MaggieAppleton Oct 2, 2026
5343b97
Record JEV-001 fix PR and independent verification
MaggieAppleton Oct 2, 2026
0d1dc59
Remove planning artifacts and streamline regression coverage
MaggieAppleton Oct 3, 2026
8f6fbf2
Preserve the saved decision collapse transition
MaggieAppleton Oct 3, 2026
b626741
Renew reviewed questionnaire source hashes
MaggieAppleton Oct 3, 2026
f7f3ccb
Ground regenerated decision prose in current evidence
MaggieAppleton Oct 3, 2026
43866cc
Keep diagnostics buttons inside one-line chat messages
MaggieAppleton Oct 3, 2026
a7b6497
Widen the default chat pane by 60px
MaggieAppleton Oct 2, 2026
5bdf0e5
Record JEV-006 PR and verified width comparison
MaggieAppleton Oct 2, 2026
294e1ab
Remove planning artifacts and streamline regression coverage
MaggieAppleton Oct 3, 2026
8ae29f5
Open Jev debugging popovers from explicit code buttons
MaggieAppleton Oct 2, 2026
6beb182
Record JEV-005 PR and verified UI previews
MaggieAppleton Oct 2, 2026
830c458
Remove planning artifacts and streamline regression coverage
MaggieAppleton Oct 3, 2026
583a47c
Hide inline Jev chat pills and retain popover actions
MaggieAppleton Oct 2, 2026
6abaf7b
Record JEV007 PR and verified UI evidence
MaggieAppleton Oct 2, 2026
3149aa8
Remove planning artifacts and streamline regression coverage
MaggieAppleton Oct 3, 2026
2767318
Keep desktop workspace down to 500px
MaggieAppleton Oct 2, 2026
8e94aad
Record breakpoint PR and verified visual preview
MaggieAppleton Oct 2, 2026
f4942d0
Remove planning artifacts and streamline regression coverage
MaggieAppleton Oct 3, 2026
db97586
Simplify Jev inference and harden shared decision updates
MaggieAppleton Oct 3, 2026
623f133
Document current projection authority and recovery guarantees
MaggieAppleton Oct 3, 2026
2fe4a4a
Allow retrying exhausted research link checks
MaggieAppleton Oct 3, 2026
a4ad84e
Cover read-only research link retry in Chromium
MaggieAppleton Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ HARNESS=copilot-sdk
BACKGROUND_JOBS=on
WEB_RESEARCH=on

# Optional conversation-derived cards prototype; independent of AGENT.
CONVERSATION_PLAN=off
JEV_MODEL=jev-latest
# Per Jev request, in milliseconds (100–60000; default 30000).
JEV_TIMEOUT_MS=30000
# Required only when CONVERSATION_PLAN=on. Keep this key on the server.
JEV_API_KEY=

# GitHub App sign-in.
# Remote VM mode overrides this local origin in child processes; see docs/exe-dev.md.
APP_ORIGIN=http://127.0.0.1:8787
Expand Down
31 changes: 16 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ external implementation runs are durable.
- **The dialect is an allowlist.** Document MDX is parsed and rendered, never
evaluated. Keep imports, exports, expressions, raw HTML, and unknown JSX out.
- **Records own decisions.** Question answers and accepted comment decisions live
in sidecar records. Their document components are projections. Planner
operations protect those projections, but browser CRDT validation does not
yet cross-check them against records; do not treat the projection as authority.
in sidecar records. Their document components are projections. Browser CRDT
validation allows unchanged projections to move but rejects their creation,
removal or alteration. Domain operations update records and projections together;
do not treat the projection as authority.
- **Admission is not authorization.** Optional user and organization lists admit
an identity. Browser routes, sockets, and Planner tools separately recheck the
App installation and repository role.
Expand Down Expand Up @@ -139,13 +140,12 @@ replayed automatically because it may already have made durable tool changes.

## Questions, comments, and anchors

Question submission currently claims the shared draft with `claimSubmit()`,
projects the answer and authoritative record inside the room lock, calls
`Store.stage()`, persists the document or sidecar, and invokes the returned
finalizer. `stage()` removes the open draft before persistence, and the failure
path does not restore it after a storage error. Treat this as a known durability
gap: a proper two-phase refactor must retain or restore the draft until the
fenced commit succeeds.
Question submission claims the shared draft with `claimSubmit()`, then stages
the document, authoritative record, and copied question maps inside the room
lock. Publish the candidate only after its fenced commit and invoke the returned
`Store.stage()` finalizer afterward. A failed commit releases the claim and keeps
the accepted draft available for retry. Planner asks and cancellation follow the
same persistence-before-publication ordering.

A decision's definition is frozen except for appended options. Any writer may
send `question:option` to add one while the question is open (at most 10 options,
Expand All @@ -160,8 +160,9 @@ renders and derives.
Anchors combine Yjs relative positions with canonical block digests. A position
survives surrounding edits; a digest can recover one unique block after a move
or epoch replacement. Ambiguous matches must orphan rather than guess. The safe
ordering is to rebase against the old document before a server-authored edit;
the current Planner path reconciles first and is a known recovery gap.
ordering is to rebase against the old document before a server-authored edit.
Scoped decision-prose jobs follow that ordering. The ordinary `edit_plan` tool
still reconciles first and remains a recovery gap.

The browser starts a comment from a bounded quote locator, selected length,
offset hint, and block indices, not an unbounded copy of selected text. The
Expand Down Expand Up @@ -279,9 +280,9 @@ so merge ranges from all mounted editors before replacing a registry entry.
after `send()` loses edits on disconnect.
- Rebuilds and reconnects must replay unacknowledged updates only when the epoch
is still compatible.
- Idle eviction removes a room registry entry before its asynchronous close and
checkpoint completes. Until that lifecycle is serialized, avoid opening a
replacement room during close and test revision conflicts around eviction.
- Room opening and closing share the document lifecycle lock. Opening waits for
an active close; eviction forgets the room only after runtime shutdown and
persistence complete. Preserve that ordering.
- Sidecar restoration currently drops an invalid optional implementation graph
instead of rejecting the whole sidecar. Do not generalize that fail-open
behavior to other durable fields.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ and tool vocabulary remain optimized for planning.
credentials in an OS vault or an explicitly consented local file. Documents,
transcripts, decisions, research request staging, background-job inputs and
artifacts, and token-free session records are stored in PostgreSQL.
- Optional conversation-derived cards default to off. With `CONVERSATION_PLAN=on`,
current and recent Chat messages and selected decision context are sent to
TypeSafe's Jev service for interpretation. This uses the server's `JEV_API_KEY`,
separately from the Planner's harness credentials, and continues with `AGENT=off`.
- One Chopin process may write to a database at a time. Horizontal application
scaling and zero-downtime rolling deployment are not supported.

Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/agent/card-tool-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { jobToolContext } from "./job-tool-context";
import type { Context as JobContext } from "./job-tool-context";
import type { DocumentRoom } from "./tools";

export type Context = JobContext & { currentMemberRequest?: () => { text: string } | undefined };
export type CardToolRoom = DocumentRoom & Pick<Context, "currentMemberRequest">;

export function cardToolContext(room: CardToolRoom): Context {
return { ...jobToolContext(room), currentMemberRequest: room.currentMemberRequest };
}
53 changes: 53 additions & 0 deletions apps/server/src/agent/decision-prose-1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, test } from "bun:test";
import * as room from "../plan/room";
import * as Service from "../plan/service";
import * as Questions from "../questions/service";
import { createDecisionProseFixture } from "./decision-prose.test-fixtures";

let contexts: ReturnType<typeof createDecisionProseFixture>["contexts"];
let fixture = createDecisionProseFixture(() => contexts, value => {
contexts = value;
});
contexts = fixture.contexts;
let { CARD, WIDGET, opened, job, proseTool, input } = fixture;

test("write_decision_prose commits one staged insert and publishes anchors and metadata afterward", async () => {
let context = await opened();
job(context);
let tool = proseTool(context);
let before = room.project(context.plan.document);
let response = await tool.handler(input(context), {} as never);
expect(JSON.parse(String(response))).toEqual({
ok: true,
title: "Which authentication approach?",
mode: "insert",
revision: 1,
});
expect(room.project(context.plan.document)).toBe(
before.replace("Context.\n\n", "Context.\n\nWe chose GitHub Apps.\n\n"),
);
expect(context.plan.document.seq).toBe(1);
expect(context.broadcasts.filter(frame => frame.kind === "plan:update")).toHaveLength(1);
expect(context.broadcasts.filter(frame => frame.kind === "plan:anchors")).toHaveLength(1);
expect(context.broadcasts.filter(frame => frame.kind === "question:meta")).toHaveLength(1);
expect(tool.anchors()).toBe(1);
expect(tool.changes()).toBe(1);
expect(Questions.prose(context.plan)[0]).toMatchObject({ widget: CARD, orphaned: false });
expect(room.matchesAnchor(context.plan.document, context.plan.records.get(CARD)!.prose![0]!, 1))
.toBe(true);
let loaded = (await context.storage.collaboration.load(context.channel.id, context.now))!;
expect((await Service.readStored(loaded)).source).toBe(room.project(context.plan.document));
});

test("write_decision_prose uses one durable delta for a first-card two-step insertion", async () => {
let context = await opened(WIDGET);
job(context);
let before = room.project(context.plan.document);
let result = await proseTool(context).handler(input(context), {} as never);
expect(JSON.parse(String(result)).ok).toBe(true);
expect(room.project(context.plan.document)).toBe(`We chose GitHub Apps.\n\n${before}`);
expect(context.plan.document.seq).toBe(1);
expect(context.broadcasts.filter(frame => frame.kind === "plan:update")).toHaveLength(1);
expect(room.matchesAnchor(context.plan.document, context.plan.records.get(CARD)!.prose![0]!, 0))
.toBe(true);
});
98 changes: 98 additions & 0 deletions apps/server/src/agent/decision-prose-2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { expect, test } from "bun:test";
import { $createParagraphNode, $getRoot, $isParagraphNode } from "lexical";
import * as Y from "yjs";
import * as room from "../plan/room";
import * as Service from "../plan/service";
import { createDecisionProseFixture } from "./decision-prose.test-fixtures";

let contexts: ReturnType<typeof createDecisionProseFixture>["contexts"];
let fixture = createDecisionProseFixture(() => contexts, value => {
contexts = value;
});
contexts = fixture.contexts;
let { CARD, WIDGET, OTHER_WIDGET, opened, job, proseTool, input } = fixture;

test("write_decision_prose addresses a first card past live leading and inter-card carets", async () => {
let context = await opened(`${WIDGET}\n${OTHER_WIDGET}`);
context.plan.document.editor.update(() => {
let children = $getRoot().getChildren();
children[0]!.insertBefore($createParagraphNode());
children[1]!.insertBefore($createParagraphNode());
}, { discrete: true });
await room.settle();
let caretKeys: string[] = [];
context.plan.document.editor.getEditorState().read(() => {
caretKeys = $getRoot().getChildren()
.filter(node => $isParagraphNode(node) && node.getChildrenSize() === 0)
.map(node => node.getKey());
});
expect(caretKeys).toHaveLength(2);
let relatives = caretKeys.map(key => {
let type = context.plan.document.binding.collabNodeMap.get(key)?.getSharedType();
if (!type) throw new Error("caret has no shared identity");
return Y.createRelativePositionFromTypeIndex(type, 0, -1);
});
let peer = await room.restore(
context.plan.document.epoch,
Y.encodeStateAsUpdate(context.plan.document.doc),
room.project(context.plan.document),
[],
);
expect(room.digests(context.plan.document)).toHaveLength(2);
expect(room.questionnaireIndex(context.plan.document, CARD)).toBe(0);
job(context);
let response = await proseTool(context).handler(input(context), {} as never);
expect(JSON.parse(String(response)).ok).toBe(true);
expect(room.project(context.plan.document)).toStartWith(
"We chose GitHub Apps.\n\n<Questionnaire",
);
expect(room.questionnaireIndex(context.plan.document, CARD)).toBe(1);
expect(room.matchesAnchor(context.plan.document, context.plan.records.get(CARD)!.prose![0]!, 0))
.toBe(true);
context.plan.document.editor.getEditorState().read(() => {
let current = new Set($getRoot().getChildren().map(node => node.getKey()));
for (let key of caretKeys) expect(current.has(key)).toBe(true);
});
let update = context.broadcasts.find(frame => frame.kind === "plan:update")?.update;
if (typeof update !== "string") throw new Error("missing persisted document delta");
Y.applyUpdate(peer.doc, Buffer.from(update, "base64"), "remote");
await room.settle();
let absolutes = relatives.map(relative =>
Y.createAbsolutePositionFromRelativePosition(relative, peer.doc, false)
);
for (let absolute of absolutes) expect(absolute).not.toBeNull();
peer.editor.getEditorState().read(() => {
let empty = $getRoot().getChildren().filter(node =>
$isParagraphNode(node) && node.getChildrenSize() === 0
);
expect(empty).toHaveLength(2);
for (let absolute of absolutes) {
expect(
empty.some(node =>
peer.binding.collabNodeMap.get(node.getKey())?.getSharedType() === absolute?.type
),
).toBe(true);
}
});
peer.doc.destroy();
let expected = room.project(context.plan.document);
await Service.close(context.plan);
contexts = contexts.filter(item => item !== context);
let reopened = await Service.open(context.channel.id, context.backend, context.server);
contexts.push({ ...context, plan: reopened });
expect(room.project(reopened.document)).toBe(expected);
expect(room.matchesAnchor(reopened.document, reopened.records.get(CARD)!.prose![0]!, 0))
.toBe(true);
});

test("write_decision_prose replaces the anchored duplicate, leaving its twin alone", async () => {
let context = await opened(`Same.\n\nSame.\n\n${WIDGET}`);
context.plan.records.get(CARD)!.prose = [
room.anchorAt(context.plan.document, 1, room.digests(context.plan.document)[1]!),
];
job(context);
let result = await proseTool(context).handler(input(context, "New prose."), {} as never);
expect(JSON.parse(String(result)).mode).toBe("replace");
expect(room.project(context.plan.document)).toStartWith("Same.\n\nNew prose.\n\n");
expect(context.plan.records.get(CARD)!.prose).toHaveLength(1);
});
48 changes: 48 additions & 0 deletions apps/server/src/agent/decision-prose-3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, spyOn, test } from "bun:test";
import { $getRoot } from "lexical";
import * as room from "../plan/room";
import { createDecisionProseFixture } from "./decision-prose.test-fixtures";

let contexts: ReturnType<typeof createDecisionProseFixture>["contexts"];
let fixture = createDecisionProseFixture(() => contexts, value => {
contexts = value;
});
contexts = fixture.contexts;
let { CARD, WIDGET, opened, job, proseTool, input } = fixture;

test("write_decision_prose replaces uniquely moved prose at its new position", async () => {
let context = await opened(`Other context.\n\nOld decision prose.\n\n${WIDGET}`);
context.plan.records.get(CARD)!.prose = [
room.anchorAt(context.plan.document, 1, room.digests(context.plan.document)[1]!),
];
context.plan.document.editor.update(() => {
let children = $getRoot().getChildren();
children[0]!.insertBefore(children[1]!);
}, { discrete: true });
await room.settle();
job(context);
let result = await proseTool(context).handler(input(context, "New prose."), {} as never);
expect(JSON.parse(String(result)).mode).toBe("replace");
expect(room.project(context.plan.document)).toStartWith("New prose.\n\nOther context.\n\n");
});

test("write_decision_prose rejects missing or stale job generation and invalid text", async () => {
let context = await opened();
let tool = proseTool(context);
let before = room.project(context.plan.document);
let errors = spyOn(console, "error").mockImplementation(() => {});
try {
expect(String(await tool.handler(input(context), {} as never)))
.toContain("only available to a background Planner job");
job(context, 2);
expect(String(await tool.handler(input(context), {} as never))).toContain("generation");
job(context);
for (let text of ["# Heading", "One.\n\nTwo.", "Text <Badge />"]) {
expect(String(await tool.handler(input(context, text), {} as never))).toStartWith("Error:");
}
expect(room.project(context.plan.document)).toBe(before);
expect(context.plan.chat.jobOutput).toBeUndefined();
} finally {
errors.mockRestore();
}
});
70 changes: 70 additions & 0 deletions apps/server/src/agent/decision-prose-4.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, spyOn, test } from "bun:test";
import * as room from "../plan/room";
import type { ConversationPlan } from "@chopin/protocol";
import { createDecisionProseFixture } from "./decision-prose.test-fixtures";

let contexts: ReturnType<typeof createDecisionProseFixture>["contexts"];
let fixture = createDecisionProseFixture(() => contexts, value => {
contexts = value;
});
contexts = fixture.contexts;
let { CARD, OPTION, opened, job, proseTool, input } = fixture;

test("write_decision_prose fences a replaced job after an awaited staging read", async () => {
let context = await opened();
job(context);
let before = room.project(context.plan.document);
let original = room.restore;
let entered = Promise.withResolvers<void>();
let release = Promise.withResolvers<void>();
let held = spyOn(room, "restore").mockImplementation(async (...args) => {
entered.resolve();
await release.promise;
return original(...args);
});
let errors = spyOn(console, "error").mockImplementation(() => {});
try {
let action = proseTool(context).handler(input(context), {} as never);
await entered.promise;
context.plan.chat.job = { ...context.plan.chat.job!, id: "newer" } as ConversationPlan.Job;
release.resolve();
expect(String(await action)).toContain("background Planner job changed");
expect(room.project(context.plan.document)).toBe(before);
expect(context.plan.document.seq).toBe(0);
expect(context.plan.chat.jobOutput).toBeUndefined();
} finally {
errors.mockRestore();
held.mockRestore();
}
});

test("write_decision_prose fences a newer saved decision after an awaited staging read", async () => {
let context = await opened();
job(context);
let before = room.project(context.plan.document);
let original = room.restore;
let entered = Promise.withResolvers<void>();
let release = Promise.withResolvers<void>();
let held = spyOn(room, "restore").mockImplementation(async (...args) => {
entered.resolve();
await release.promise;
return original(...args);
});
let errors = spyOn(console, "error").mockImplementation(() => {});
try {
let action = proseTool(context).handler(input(context), {} as never);
await entered.promise;
let record = context.plan.records.get(CARD)!;
context.plan.records.set(CARD, {
...record,
history: [{ choices: [OPTION], owner: "mina", at: 1_758_645_000 }],
});
release.resolve();
expect(String(await action)).toContain("background Planner job changed");
expect(room.project(context.plan.document)).toBe(before);
expect(context.plan.chat.jobOutput).toBeUndefined();
} finally {
errors.mockRestore();
held.mockRestore();
}
});
Loading
Loading