Changelog, dev startup, README, AgentHub 0.4.0, model catalog, and landing site (#7)
Branch-length batch covering tooling, the model layer, the Web App and the public surfaces. Highlights: - Changelog: a per-release `changelog/<version>/` tree, grouped by the surface each change touches, with a root CHANGELOG.md holding one line per release. - Dev startup: `scripts/dev-prebuild.mjs` serializes the skills+core prebuild behind a lock and keeps `pnpm install` current; `pnpm dev` runs server+web together. - AgentHub 0.3.3 -> 0.4.0: OmniMessage complete payloads carry one opaque `fidelity` object in place of item-level `signature`/`phase`, threaded verbatim through Trace, replay and resume; malformed classification adapted to the new error types. - Model layer: a model is always referenced by an explicit `(provider, model_id)` pair. The provider is never inferred, guessed or defaulted -- both the catalog inference and the unique-match config resolution are gone, and CLI, SDK, server routes and run_subagent all require the complete pair. Catalog gains the Qwen Token Plan, Qwen Pay-As-You-Go and Fireworks AI gateways, plus an expanded OpenRouter group. - Web App: catalog preset sync and per-group speed test on the Models page, positional slash commands, a markdown renderer, skill-library update reminders, and a vertically centred draft page whose upward menus size themselves to the room available. - Public surfaces: restructured READMEs, the penguin.ooo landing site and blog, refreshed benchmark results for both suites, and the demo videos playing on the landing page. Includes the fixes from a full review of the branch: 23 confirmed findings, among them a provider-inference bug that could send one vendor's API key to another vendor's endpoint, and an Escape handler that destroyed the composer's contents unrecoverably. Verified on the branch head: pnpm test (1127 passing, 7 packages), pnpm typecheck and pnpm format:check clean, Playwright e2e 14/14.
This commit is contained in:
@@ -231,11 +231,14 @@ describe("resync rebuild", () => {
|
||||
h.resolveLoad([at(userText("question"), "2026-07-05T00:00:00.000Z")]);
|
||||
await p;
|
||||
expect(h.loadCalls()).toBe(2);
|
||||
// The old replay must not reset phase back to live: events arriving during rebuild
|
||||
// should still be buffered, not fed to the new model.
|
||||
// The old replay must not reset phase back to live: events arriving during rebuild are still
|
||||
// buffered, not fed to a model. And with the atomic swap the OLD transcript stays visible until
|
||||
// the rebuild's history load returns — mid-rebuild the model still shows the pre-resync content
|
||||
// (the question + "old event A"), never a blank, and the live event is not yet in it (feeding it
|
||||
// here as a third item would fail this assertion).
|
||||
const live = at(assistantText("output during rebuild"), "2026-07-05T00:00:02.000Z");
|
||||
h.controller.handleOmni(live);
|
||||
expect(h.controller.model.items).toHaveLength(0);
|
||||
expect(h.controller.model.items.map((i) => i.kind)).toEqual(["user_text", "assistant_text"]);
|
||||
// Second round of history (authoritative) returns: the transferred task_state and
|
||||
// buffered events replay in order.
|
||||
h.resolveLoad([
|
||||
@@ -278,4 +281,37 @@ describe("history load failure and retry (#6)", () => {
|
||||
await h.controller.retry();
|
||||
expect(h.loadCalls()).toBe(1);
|
||||
});
|
||||
|
||||
it("retry after a FAILED resync rebuild rebuilds into a fresh model (no transcript duplication)", async () => {
|
||||
// A successful initial load leaves the transcript populated; a mid-stream resync whose refetch
|
||||
// then fails deliberately keeps that old transcript on screen (the atomic swap only replaces it
|
||||
// on success). Retry must therefore push the refetched history into a FRESH model — pushing it
|
||||
// onto the retained one would duplicate the whole conversation (regression guard).
|
||||
const h = createHarness();
|
||||
const p = h.controller.load();
|
||||
h.resolveLoad([
|
||||
at(userText("q1"), "2026-07-05T00:00:00.000Z"),
|
||||
at(assistantText("a1"), "2026-07-05T00:00:01.000Z"),
|
||||
]);
|
||||
await p;
|
||||
expect(h.controller.model.items.map((i) => i.kind)).toEqual(["user_text", "assistant_text"]);
|
||||
|
||||
// Resync mid-session, but the rebuild's history refetch fails.
|
||||
h.controller.handleServer({ type: "resync_required" });
|
||||
h.rejectLoad(new Error("network error"));
|
||||
await flush();
|
||||
expect(h.errors[h.errors.length - 1]).toBe("network error");
|
||||
// The old transcript is retained (not blanked) while the error/Retry state is shown.
|
||||
expect(h.controller.model.items.map((i) => i.kind)).toEqual(["user_text", "assistant_text"]);
|
||||
|
||||
// Retry succeeds: the identical history must land in a fresh model, not be appended onto the retained one.
|
||||
const retryP = h.controller.retry();
|
||||
h.resolveLoad([
|
||||
at(userText("q1"), "2026-07-05T00:00:00.000Z"),
|
||||
at(assistantText("a1"), "2026-07-05T00:00:01.000Z"),
|
||||
]);
|
||||
await retryP;
|
||||
expect(h.controller.model.items.map((i) => i.kind)).toEqual(["user_text", "assistant_text"]);
|
||||
expect(h.controller.model.items.filter((i) => i.kind === "user_text")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user