Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5db74f6
docs: define session configuration laws
Aug 7, 2026
7685774
docs: state provider compatibility law
Aug 8, 2026
22d3c80
fix: validate managed provider migrations
Aug 8, 2026
d077235
fix: retain configuration intent through preflight
Aug 8, 2026
a099c08
fix: own preflight configuration intent
Aug 8, 2026
2465866
fix: retain active session mutation queues
Aug 8, 2026
9290850
docs: place session configuration laws with chat
Aug 8, 2026
908c194
fix(acp): publish deferred load snapshots after rejected preflight
Aug 8, 2026
caa38b1
fix(acp): retain deferred load settlement across preflights
Aug 8, 2026
952f17d
fix(acp): preserve terminal supersession settlement
Aug 8, 2026
6c3dd00
Simplify superseded ACP load handling
Aug 8, 2026
1dd3315
fix(acp): reserve configuration intent before migration proof
Aug 8, 2026
80c28ff
fix(goose): choose proven migration inventory fallback
Aug 8, 2026
784abe2
fix(goose): bound inventory migration proof
Aug 8, 2026
0baf8d2
fix(goose): bound client acquisition during migration proof
Aug 10, 2026
673c378
fix(goose): preserve provider harness boundaries
Aug 10, 2026
d3c6013
fix(goose): discard stale ACP initialization
Aug 10, 2026
b4b7455
fix(goose): own ACP initialization attempts
Aug 10, 2026
fca632d
fix(goose): share bounded inventory proof
Aug 10, 2026
7edf16c
fix(agents): canonicalize persisted harness targets
Aug 10, 2026
83fcb63
fix(agents): clear unsupported persisted models
Aug 10, 2026
c5fc5ad
test(agents): cover startup target migration
Aug 11, 2026
0045434
fix(agents): separate model proof from display candidates
Aug 11, 2026
eacaed2
fix(agents): keep runtime model seeds provisional
Aug 11, 2026
38d7012
test(providers): model inventory proof explicitly
Aug 11, 2026
9a6a14a
fix(providers): distinguish empty model proof
Aug 11, 2026
95a1552
fix(providers): enforce proven model inventory
Aug 11, 2026
c827703
test: provide proven model inventory in picker mocks
Aug 11, 2026
7d90386
fix(providers): preserve intent without model proof
Aug 13, 2026
910af88
fix(providers): avoid inventory lookup on prompt send
Aug 13, 2026
baad031
test: reconcile suite with session-target lease and model-proof gating
loganj Aug 17, 2026
1d3682e
test: reconcile queued send ACP mock
loganj Aug 19, 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
17 changes: 17 additions & 0 deletions src/app/AppShell.navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,19 @@ 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),
});
}
return { providers };
});
}

vi.mock("@/shared/profile/buildProfile", () => ({
Expand Down Expand Up @@ -440,6 +453,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 @@ -1403,6 +1417,7 @@ describe("AppShell global navigation", () => {
"openai",
"~/goose artifacts",
expect.any(Object),
expect.objectContaining({ clear: expect.any(Function) }),
);
});
expect(
Expand Down Expand Up @@ -3426,6 +3441,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 @@ -3488,6 +3504,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
19 changes: 15 additions & 4 deletions src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2700,17 +2700,28 @@ export function AppShell({
const persona = agentState.personas.find(
(candidate) => candidate.id === agentId,
);
const cachedModels = [
...useProviderModelCacheStore.getState().providers,
].flatMap(([providerId, entry]) =>
entry.models.map((model) => ({
const modelCache = useProviderModelCacheStore.getState();
const cachedModels = [...modelCache.providers].flatMap(
([providerId, entry]) =>
entry.models.map((model) => ({
...model,
providerId: model.providerId ?? providerId,
})),
);
const provenModels = [...modelCache.providers].flatMap(([providerId]) =>
modelCache.getProvenModelsForProvider(providerId).map((model) => ({
...model,
providerId: model.providerId ?? providerId,
})),
);
const executionTarget = personaExecutionTarget(persona, {
providers: agentState.providers,
models: cachedModels,
getProvenModelsForHarness: () => provenModels,
isModelInventoryAuthoritative: (providerId) =>
useProviderModelCacheStore
.getState()
.isModelInventoryAuthoritative(providerId),
catalogEntries: getProviderCatalog(),
});

Expand Down
99 changes: 94 additions & 5 deletions src/app/lib/chatRuntimeStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ const mockLoadPersistedMessageQueues = vi.hoisted(() =>
);
const mockGetClient = vi.hoisted(() => vi.fn<() => Promise<unknown>>());
const mockRefreshAllModelProviders = vi.hoisted(() => vi.fn());
const mockMigratePersonaTargetIfUnchanged = vi.hoisted(() => vi.fn());
const mockAgentState = vi.hoisted(() => ({
personas: [] as Array<Record<string, unknown>>,
providers: [] as Array<{ id: string; label?: string }>,
}));
const mockModelCacheState = vi.hoisted(() => ({
providers: new Map<
string,
{ models: Array<Record<string, unknown>>; provenModelIds?: string[] }
>(),
runtimeManagedProviderIds: new Set<string>(),
}));

// The latch under test wraps startChatRuntime, whose body touches most of the
// startup module graph. Everything it reaches is stubbed inert (resolved,
Expand All @@ -18,6 +30,7 @@ vi.mock("@/features/agents/stores/agentStore", () => ({
setProviders: () => {},
setPersonas: () => {},
setPersonasLoading: () => {},
...mockAgentState,
}),
},
}));
Expand Down Expand Up @@ -48,12 +61,15 @@ vi.mock("@/features/providers/runtimeProviderConstraints", () => ({
}));

vi.mock("@/features/providers/modelCacheRefresh", () => ({
getModelCacheRefreshProviderIds: () => [],
getModelCacheRefreshProviderIds: () => ["claude-acp"],
}));

vi.mock("@/features/providers/providerCatalog", () => ({
canonicalProviderCatalogIdFromEntries: (_entries: unknown, id: string) => id,
getModelProviders: () => [],
getProviderCatalog: () => [],
resolveAgentProviderCatalogIdStrictFromEntries: () => null,
resolveModelProviderCatalogIdStrictFromEntries: () => null,
}));

vi.mock("@/features/providers/runtimeProviderConfig", () => ({
Expand Down Expand Up @@ -110,8 +126,7 @@ vi.mock("@/features/providers/stores/defaultProviderReadinessStore", () => ({
vi.mock("@/features/providers/stores/providerModelCacheStore", () => ({
useProviderModelCacheStore: {
getState: () => ({
providers: new Map(),
runtimeManagedProviderIds: new Set(),
...mockModelCacheState,
loadPersisted: () => {},
refreshAllModelProviders: (...args: unknown[]) =>
mockRefreshAllModelProviders(...args),
Expand Down Expand Up @@ -166,8 +181,9 @@ vi.mock("@/shared/api/distro", () => ({
}));

vi.mock("@/shared/api/agents", () => ({
listPersonas: async () => [],
migratePersonaTargetIfUnchanged: async () => null,
listPersonas: async () => mockAgentState.personas,
migratePersonaTargetIfUnchanged: (...args: unknown[]) =>
mockMigratePersonaTargetIfUnchanged(...args),
}));

function deferred<T>() {
Expand All @@ -190,6 +206,12 @@ describe("runChatRuntimeStartup", () => {
mockGetClient.mockResolvedValue({});
mockRefreshAllModelProviders.mockReset();
mockRefreshAllModelProviders.mockResolvedValue(undefined);
mockMigratePersonaTargetIfUnchanged.mockReset();
mockMigratePersonaTargetIfUnchanged.mockResolvedValue(null);
mockAgentState.personas = [];
mockAgentState.providers = [];
mockModelCacheState.providers = new Map();
mockModelCacheState.runtimeManagedProviderIds = new Set();
});

it("collapses concurrent callers onto one startup run", async () => {
Expand Down Expand Up @@ -221,6 +243,73 @@ describe("runChatRuntimeStartup", () => {
inventoryRefresh.resolve();
});

it("does not migrate a runtime-managed configuration seed before live discovery", async () => {
mockAgentState.providers = [{ id: "claude-acp", label: "Claude Code" }];
mockAgentState.personas = [
{
id: "persona-1",
displayName: "Configured Claude",
systemPrompt: "Help.",
provider: "claude-acp",
modelProviderId: "claude-acp",
model: "configured-model",
isBuiltin: false,
writable: true,
},
];
mockModelCacheState.runtimeManagedProviderIds = new Set(["claude-acp"]);
mockModelCacheState.providers = new Map([
[
"claude-acp",
{
models: [{ id: "configured-model", providerId: "claude-acp" }],
},
],
]);

const { runChatRuntimeStartup } = await import("./chatRuntimeStartup");
await runChatRuntimeStartup();

expect(mockMigratePersonaTargetIfUnchanged).not.toHaveBeenCalled();
});

it("migrates an authoritative unsupported persona model without losing its harness", async () => {
mockAgentState.providers = [{ id: "claude-acp", label: "Claude Code" }];
mockAgentState.personas = [
{
id: "persona-1",
displayName: "Legacy Claude",
systemPrompt: "Help.",
provider: "claude-acp",
modelProviderId: "openai",
model: "gpt-5",
isBuiltin: false,
writable: true,
},
];
mockModelCacheState.providers = new Map([
["claude-acp", { models: [], provenModelIds: [] }],
]);
mockRefreshAllModelProviders.mockImplementation(async () => {
mockModelCacheState.providers = new Map([
["claude-acp", { models: [], provenModelIds: [] }],
]);
});

const { runChatRuntimeStartup } = await import("./chatRuntimeStartup");
await runChatRuntimeStartup();
await vi.waitFor(() => {
expect(mockMigratePersonaTargetIfUnchanged).toHaveBeenCalledWith(
expect.objectContaining({ id: "persona-1" }),
{
provider: "claude-acp",
modelProviderId: "claude-acp",
model: null,
},
);
});
});

it("stays latched after a successful run", async () => {
const { runChatRuntimeStartup } = await import("./chatRuntimeStartup");
const first = runChatRuntimeStartup();
Expand Down
23 changes: 14 additions & 9 deletions src/app/lib/chatRuntimeStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,19 @@ async function startChatRuntime(
const cachedModels = [...modelState.providers].flatMap(
([providerId, entry]) =>
authoritativeProviderIds.has(providerId)
? entry.models.map((model) => ({
...model,
providerId: model.providerId ?? providerId,
}))
? entry.models
.filter((model) => entry.provenModelIds?.includes(model.id))
.map((model) => ({
...model,
providerId: model.providerId ?? providerId,
}))
: [],
);
const targetContext = {
providers: useAgentStore.getState().providers,
models: cachedModels,
isModelInventoryAuthoritative: (providerId: string) =>
authoritativeProviderIds.has(providerId),
catalogEntries: getProviderCatalog(),
};
const personas = useAgentStore.getState().personas;
Expand Down Expand Up @@ -311,13 +315,14 @@ async function startChatRuntime(
);
await modelCacheStore.refreshAllModelProviders(refreshProviderIds);
const modelState = useProviderModelCacheStore.getState();
return new Set([
...modelState.runtimeManagedProviderIds,
...refreshProviderIds.filter((providerId) => {
return new Set(
refreshProviderIds.filter((providerId) => {
const entry = modelState.providers.get(providerId);
return entry != null && !entry.error;
return (
entry != null && !entry.error && entry.provenModelIds !== undefined
);
}),
]);
);
};

const loadSessionState = async () => {
Expand Down
1 change: 1 addition & 0 deletions src/app/views/__tests__/NavigationPanesView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ vi.mock("@/features/chat/stores/chatStore", () => ({
}));

vi.mock("@/shared/api/acp", () => ({
reserveAcpSessionConfiguration: () => ({ sequence: 0, clear: () => {} }),
acpSearchSessions: (...args: unknown[]) => mockAcpSearchSessions(...args),
}));

Expand Down
Loading