Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
923dfb7
docs: define session configuration laws
loganj Aug 7, 2026
2ab184c
docs: state provider compatibility law
loganj Aug 8, 2026
2166e11
fix: validate managed provider migrations
loganj Aug 8, 2026
b0d9815
fix: retain configuration intent through preflight
loganj Aug 8, 2026
ac47f55
fix: own preflight configuration intent
loganj Aug 8, 2026
f00bed5
fix: retain active session mutation queues
loganj Aug 8, 2026
143148f
docs: place session configuration laws with chat
loganj Aug 8, 2026
029a41a
fix(acp): publish deferred load snapshots after rejected preflight
loganj Aug 8, 2026
8096015
fix(acp): retain deferred load settlement across preflights
loganj Aug 8, 2026
8169937
fix(acp): preserve terminal supersession settlement
loganj Aug 8, 2026
cc64641
Simplify superseded ACP load handling
loganj Aug 8, 2026
be4e4d3
fix(acp): reserve configuration intent before migration proof
loganj Aug 8, 2026
0f5d420
fix(goose): choose proven migration inventory fallback
loganj Aug 8, 2026
ae3ce81
fix(goose): bound inventory migration proof
loganj Aug 8, 2026
6dfcfa3
fix(goose): bound client acquisition during migration proof
loganj Aug 10, 2026
57cb1bf
fix(goose): preserve provider harness boundaries
loganj Aug 10, 2026
60243e4
fix(goose): discard stale ACP initialization
loganj Aug 10, 2026
83fa8fb
fix(goose): own ACP initialization attempts
loganj Aug 10, 2026
c18dfcf
fix(goose): share bounded inventory proof
loganj Aug 10, 2026
235aaaa
fix(agents): canonicalize persisted harness targets
loganj Aug 10, 2026
5399767
fix(agents): clear unsupported persisted models
loganj Aug 10, 2026
b0542fc
test(agents): cover startup target migration
loganj Aug 11, 2026
f69eb76
fix(agents): separate model proof from display candidates
loganj Aug 11, 2026
e95c061
fix(agents): keep runtime model seeds provisional
loganj Aug 11, 2026
ce93ec4
test(providers): model inventory proof explicitly
loganj Aug 11, 2026
4f54a7b
fix(providers): distinguish empty model proof
loganj Aug 11, 2026
d36af8d
fix(providers): enforce proven model inventory
loganj Aug 11, 2026
7bfd13d
test: provide proven model inventory in picker mocks
loganj Aug 11, 2026
1543ef0
fix(providers): preserve intent without model proof
loganj Aug 13, 2026
e003a1c
fix(providers): avoid inventory lookup on prompt send
loganj Aug 13, 2026
02346cc
test: reconcile suite with session-target lease and model-proof gating
loganj Aug 17, 2026
76ff7be
test: reconcile queued send ACP mock
loganj Aug 19, 2026
929c548
fix: enforce immutable session configuration
loganj Aug 19, 2026
857a332
fix: invalidate unsupported active session models
loganj Aug 19, 2026
17f804a
fix: preserve authoritative session configuration
loganj Aug 19, 2026
6d51a21
fix: preserve model inventory proof on refresh failure
loganj Aug 19, 2026
841a879
fix: enforce immutable session configuration intent
loganj Aug 19, 2026
b8aaa3c
fix: close immutable configuration review gaps
loganj Aug 20, 2026
465c1ac
fix: clarify invalid agent recovery
loganj Aug 20, 2026
0ca8b15
fix: enforce immutable session target configuration
loganj Aug 20, 2026
f21b66c
fix: validate immutable session targets before acceptance
loganj Aug 20, 2026
ff8a07f
Fix immutable queued and explicit session targets
loganj Aug 20, 2026
895e973
fix(chat): preserve queued session authority
loganj Aug 20, 2026
0493015
fix(berdctl): enforce model inventory authority
loganj Aug 20, 2026
727810d
fix(sessions): retain resolved creation target
loganj Aug 20, 2026
4532563
fix(sessions): preserve immutable selection authority
loganj Aug 21, 2026
2181ae2
fix(sessions): own pending provider selections
loganj Aug 21, 2026
6891d58
fix(sessions): fence stale provider preparation
loganj Aug 21, 2026
5db65e9
Prevent stale ACP snapshot publication
loganj Aug 21, 2026
fa62199
test: cover stale ACP adapter store lifecycle
loganj Aug 21, 2026
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: 6 additions & 0 deletions LAWS/CHAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@

- A session's subagent activity MUST appear in the chat transcript with the subagent identity when known.
- A session's subagent activity MUST appear in the chat transcript with the delegated task when known.

## Session configuration

- A session鈥檚 provider MUST support its model, and its harness MUST support that provider.
- A session MUST have exactly one effective configuration.
- Berd MUST show the configuration that the session uses.
1 change: 1 addition & 0 deletions src/app/AppShell.berdctl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ vi.mock("@/app/views/NavigationPanesView", () => ({
}));

vi.mock("@/shared/api/acp", () => ({
reserveAcpSessionConfiguration: () => ({ sequence: 0, clear: () => {} }),
acpCreateSession: (...args: unknown[]) => mockAcpCreateSession(...args),
acpListSessionsPage: (...args: unknown[]) => mockAcpListSessionsPage(...args),
acpLoadSession: (...args: unknown[]) => mockAcpLoadSession(...args),
Expand Down
210 changes: 198 additions & 12 deletions src/app/AppShell.navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,20 @@ function seedProviderModels(
],
]),
);
// Simulate a successful live inventory response: seeding a provider's
// display candidates alone is advisory and never establishes proof.
useProviderModelCacheStore.setState((state) => {
const providers = new Map(state.providers);
const existing = providers.get(providerId);
if (existing) {
providers.set(providerId, {
...existing,
provenModelIds: models.map((model) => model.id),
fetchedAt: Date.now(),
});
}
return { providers };
});
}

vi.mock("@/shared/profile/buildProfile", () => ({
Expand Down Expand Up @@ -440,6 +454,7 @@ vi.mock("@/shared/api/acp", () => ({
acpListSessionsPage: (...args: unknown[]) => mockAcpListSessionsPage(...args),
acpLoadSession: (...args: unknown[]) => mockAcpLoadSession(...args),
discoverAcpProviders: vi.fn().mockResolvedValue([]),
reserveAcpSessionConfiguration: () => ({ sequence: 0, clear: () => {} }),
}));

vi.mock("@/shared/api/acpApi", () => ({
Expand Down Expand Up @@ -1412,6 +1427,7 @@ describe("AppShell global navigation", () => {
"openai",
"~/goose artifacts",
expect.any(Object),
expect.objectContaining({ clear: expect.any(Function) }),
);
});
expect(
Expand Down Expand Up @@ -1460,9 +1476,12 @@ describe("AppShell global navigation", () => {
expect(mockAcpCreateSession).toHaveBeenCalled();
});

it("allows a configured concrete provider when the BYO default is missing", async () => {
it("allows a configured concrete target when the BYO default is missing", async () => {
requireByoDefaultProviderSetup();
setResolvingPersona();
setResolvingPersona("goose-gpt-5-5", "databricks_v2", "databricks_v2");
seedProviderModels("databricks_v2", [
{ id: "goose-gpt-5-5", name: "GPT-5.5" },
]);
mockCheckAllProviderStatus.mockResolvedValue([
{ providerId: "databricks_v2", isConfigured: true },
]);
Expand All @@ -1481,7 +1500,7 @@ describe("AppShell global navigation", () => {
"~/goose artifacts",
{
deferProviderSetup: false,
modelId: undefined,
modelId: "goose-gpt-5-5",
projectId: undefined,
},
);
Expand Down Expand Up @@ -3402,6 +3421,39 @@ describe("AppShell global navigation", () => {
});
});

it("promotes a managed provider and model resolved during draft creation", async () => {
mockAcpCreateSession.mockResolvedValueOnce({
sessionId: "created-session",
configOptionsSnapshot: {
model: { modelId: "goose-gpt-5-5", modelName: "GPT-5.5" },
reasoningEffort: null,
},
resolvedSelection: {
providerId: "databricks_v2",
modelId: "goose-gpt-5-5",
modelName: "GPT-5.5",
},
});
const user = userEvent.setup();
renderAppShell();

await user.click(screen.getByRole("button", { name: "Sidebar new chat" }));

await waitFor(() => {
expect(
useChatSessionStore.getState().getSession("created-session"),
).toMatchObject({
executionTarget: {
harnessId: "goose",
modelProviderId: "databricks_v2",
modelId: "goose-gpt-5-5",
modelName: "GPT-5.5",
},
});
});
expect(mockAcpPrepareSession).not.toHaveBeenCalled();
});

it("applies the latest pending draft selection before promotion", async () => {
const pendingSession = deferred<{ sessionId: string }>();
const pendingPrepare = deferred<Record<string, never>>();
Expand Down Expand Up @@ -3435,6 +3487,7 @@ describe("AppShell global navigation", () => {
"codex-acp",
"~/goose artifacts",
expect.objectContaining({ modelId: "gpt-5.4-mini" }),
expect.objectContaining({ clear: expect.any(Function) }),
);
});
act(() => pendingPrepare.resolve({}));
Expand Down Expand Up @@ -3463,6 +3516,143 @@ describe("AppShell global navigation", () => {
expect(getModelSelectionIntent("created-session")).toBeUndefined();
});

it("does not promote a stale provider-only draft change over a newer provider", async () => {
const pendingSession = deferred<{ sessionId: string }>();
const pendingProviderB = deferred<Record<string, never>>();
mockAcpCreateSession.mockReturnValueOnce(pendingSession.promise);
mockAcpPrepareSession
.mockReturnValueOnce(pendingProviderB.promise)
.mockResolvedValueOnce({});
const user = userEvent.setup();
renderAppShell();

await user.click(screen.getByRole("button", { name: "Sidebar new chat" }));
await waitFor(() => expect(mockAcpCreateSession).toHaveBeenCalled());
const draftSessionId = useChatSessionStore.getState().activeSessionId ?? "";

act(() => {
beginModelSelectionIntent(draftSessionId, {
requestId: "provider-b",
target: { harnessId: "codex-acp" },
});
pendingSession.resolve({ sessionId: "created-session" });
});

await waitFor(() => {
expect(mockAcpPrepareSession).toHaveBeenCalledWith(
"created-session",
"codex-acp",
"~/goose artifacts",
expect.any(Object),
expect.objectContaining({ clear: expect.any(Function) }),
);
});

act(() => {
beginModelSelectionIntent(draftSessionId, {
requestId: "provider-c",
target: { harnessId: "claude-acp" },
});
pendingProviderB.resolve({});
});

await waitFor(() => {
expect(mockAcpPrepareSession).toHaveBeenCalledWith(
"created-session",
"claude-acp",
"~/goose artifacts",
expect.any(Object),
expect.objectContaining({ clear: expect.any(Function) }),
);
});
await waitFor(() => {
expect(useChatSessionStore.getState().activeSessionId).toBe(
"created-session",
);
});
expect(
useChatSessionStore.getState().getSession("created-session"),
).toMatchObject({ executionTarget: { harnessId: "claude-acp" } });
expect(getModelSelectionIntent("created-session")).toBeUndefined();
});

it("does not restore stale draft creation ownership after A to B to A", async () => {
const pendingSession = deferred<{
sessionId: string;
resolvedSelection: {
providerId: string;
modelId: string;
modelName: string;
};
}>();
mockAcpCreateSession.mockReturnValueOnce(pendingSession.promise);
const user = userEvent.setup();
renderAppShell();

await user.click(screen.getByRole("button", { name: "Sidebar new chat" }));
await waitFor(() => expect(mockAcpCreateSession).toHaveBeenCalled());
const draftSessionId = useChatSessionStore.getState().activeSessionId ?? "";
const originalTarget = useChatSessionStore
.getState()
.getSession(draftSessionId)?.executionTarget;
expect(originalTarget).toBeDefined();
if (!originalTarget) {
throw new Error(
"Expected the draft to retain its original execution target",
);
}

act(() => {
beginModelSelectionIntent(draftSessionId, {
requestId: "newer-b",
target: {
harnessId: "codex-acp",
modelProviderId: "codex-acp",
modelId: "gpt-5.4-mini",
modelName: "GPT-5.4 mini",
},
});
beginModelSelectionIntent(draftSessionId, {
requestId: "newer-a",
target: originalTarget,
preferenceAgentId: originalTarget.harnessId,
});
pendingSession.resolve({
sessionId: "created-session",
resolvedSelection: {
providerId: "anthropic",
modelId: "claude-fable",
modelName: "Claude Fable",
},
});
});

await waitFor(() => {
expect(mockAcpPrepareSession).toHaveBeenCalledWith(
"created-session",
"goose",
"~/goose artifacts",
expect.objectContaining({ selectionAlreadyResolved: true }),
expect.objectContaining({ clear: expect.any(Function) }),
);
});
await waitFor(() => {
expect(
useChatSessionStore.getState().getSession("created-session")
?.executionTarget,
).toMatchObject(originalTarget);
});
expect(
useChatSessionStore.getState().getSession("created-session")
?.executionTarget,
).not.toMatchObject({ modelId: "claude-fable" });
await waitFor(() => {
expect(useChatSessionStore.getState().activeSessionId).toBe(
"created-session",
);
});
});

it("adopts a repaired pending draft selection before promotion", async () => {
const pendingSession = deferred<{ sessionId: string }>();
mockAcpCreateSession.mockReturnValueOnce(pendingSession.promise);
Expand Down Expand Up @@ -3497,6 +3687,7 @@ describe("AppShell global navigation", () => {
"databricks_v2",
"~/goose artifacts",
expect.objectContaining({ modelId: "goose-gpt-5-5" }),
expect.objectContaining({ clear: expect.any(Function) }),
);
});
await waitFor(() => {
Expand Down Expand Up @@ -5076,7 +5267,7 @@ describe("AppShell global navigation", () => {
});
});

it("uses the normal new-chat target when a persona has no plausible target", async () => {
it("rejects starting an agent whose saved target is invalid", async () => {
useDefaultProviderReadinessStore.setState({
readiness: {
status: "ready",
Expand Down Expand Up @@ -5107,15 +5298,10 @@ describe("AppShell global navigation", () => {
);

await waitFor(() => {
expect(mockAcpCreateSession).toHaveBeenCalledWith(
"databricks_v2",
"~/goose artifacts",
expect.objectContaining({ modelId: "goose-default" }),
);
expect(screen.getByTestId("active-view")).toHaveTextContent("agents");
});
expect(
useChatSessionStore.getState().getSession("created-session"),
).toMatchObject({ personaId: "persona-unresolved" });
expect(mockAcpCreateSession).not.toHaveBeenCalled();
expect(useChatSessionStore.getState().sessions).toHaveLength(0);
});

it("tags a Home agent starter in the composer instead of opening a blank chat", async () => {
Expand Down
Loading