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:
Yaowei Zheng
2026-07-21 17:43:31 +08:00
committed by GitHub
parent abf0a2f248
commit d4faee3a1e
214 changed files with 9406 additions and 1688 deletions
+39 -3
View File
@@ -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);
});
});