Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
121a77ce7f | ||
|
|
a7c9dab22c | ||
|
|
5005726994 |
@@ -0,0 +1,170 @@
|
||||
# 2026.08.26.01 — report
|
||||
|
||||
**5 of 5 target hosts succeeded.** Two hosts skipped by plan (`officer-dev`, `edge-pertento-bridge`);
|
||||
neither is a failure. Every target ended at `e9f1760f` = `deploy/2026.08.26.01` exactly.
|
||||
|
||||
**No schema change in this release**, and that was verified rather than assumed — the diff between
|
||||
`deploy/2026.08.25.01` and this tag touches no `schema.ts` and leaves `bun.lock` untouched. **No
|
||||
`bun install` was run anywhere.** **No direct SQL was run on any host**; none was authorised.
|
||||
|
||||
First release under a new watcher, and the first the loop has ever executed. The claim was taken by the
|
||||
watcher before this agent was spawned; this agent recorded `done` after the rollout.
|
||||
|
||||
## One finding the owner should see: `db:push` is not a no-op on any host
|
||||
|
||||
`bun db:push` exited 0 and printed `Changes applied` on all five. Per doctrine that is not evidence, so
|
||||
each host was re-run with `--verbose` to see what push actually wanted. **Every host wants the same
|
||||
statement, every single run:**
|
||||
|
||||
```sql
|
||||
ALTER TABLE "command_hq_sessions" ALTER COLUMN "reference_paths" SET DEFAULT '{}';
|
||||
```
|
||||
|
||||
`alpha` additionally wants a drop-and-re-add of `pk_music_now_playing` (the music plugin's composite
|
||||
primary key).
|
||||
|
||||
**This release did not cause it and it is not damage.** The reasoning is deductive rather than a guess:
|
||||
this release changes zero `schema.ts` files, so drizzle's desired state is byte-identical before and
|
||||
after the merge — anything push wants is pre-existing drift. It was verified by query that the database
|
||||
is already in the state the schema code asks for:
|
||||
|
||||
| Host | `command_hq_sessions.reference_paths` default | `pk_music_now_playing` | `roles` |
|
||||
| ---------------------------- | --------------------------------------------- | ---------------------- | ------- |
|
||||
| `alpha` | `'{}'::text[]` — correct | present (`contype=p`) | 4 |
|
||||
| `fofos` | `'{}'::text[]` — correct | n/a | 4 |
|
||||
| `production-pertento-bridge` | `'{}'::text[]` — correct | n/a | 4 |
|
||||
| `deadpool-rezio-bridge` | `'{}'::text[]` — correct | n/a | 4 |
|
||||
| `macbookpro` | `'{}'::text[]` — correct | n/a | 4 |
|
||||
|
||||
So drizzle cannot match `'{}'` against the stored `'{}'::text[]` and re-emits the ALTER forever. It is
|
||||
the array-default cousin of the named-composite-constraint mis-diff already documented in
|
||||
`src/databases/CLAUDE.md` → "Composite keys", and `pk_music_now_playing` on `alpha` is that exact
|
||||
documented case.
|
||||
|
||||
**Why it matters even though it is harmless today:** it permanently spends the signal. The plan said
|
||||
"expect no changes to platform tables — if it wants to alter one, stop and report it", and that stop
|
||||
condition can never fire cleanly while a platform table is on the standing-diff list. A future release
|
||||
that really does want an unexpected ALTER will look exactly like tonight. Worth fixing at the schema
|
||||
code, not on the hosts.
|
||||
|
||||
The rollout was not stopped for it. Non-destructive, pre-existing, and the databases are in the state
|
||||
the code asks for.
|
||||
|
||||
`postdb:push` seeders ran clean everywhere: `seed:roles` 0 seeded / 4 already present on all five,
|
||||
`backfill:session-models` 0 seeded on all five.
|
||||
|
||||
## The reason for this release — verified on `fofos`
|
||||
|
||||
The plan exists because `proxy-manager` imports `ChatHeader`, which did not exist before `2ed47fa6`, so
|
||||
the plugin's frontend rebuild failed and the SPA silently kept its old bundle.
|
||||
|
||||
Verified after the restart, from the logs and the build output rather than by eye:
|
||||
|
||||
- `[plugins] frontend built in 7693ms (3 outputs)` at out-log line **1777 of 1808** — the current boot.
|
||||
- The `No matching export … for import "ChatHeader"` line is in the error log **above** the last
|
||||
`gen-index` / `server.tsx` boot markers, i.e. it belongs to the **previous** boot. No `rebuild failed`
|
||||
after the successful build.
|
||||
- `build/` was rewritten at 04:46:04, seconds after the restart, and `chunk-b7epbzfm.js` contains
|
||||
`proxy-manager` 41 times.
|
||||
|
||||
`ChatHeader` itself greps to 0 in the bundle — expected, production bundling renames it. The absence of
|
||||
the build error is the signal, not the presence of the symbol.
|
||||
|
||||
**The owner still needs a hard refresh (Ctrl/Cmd-Shift-R) before judging `/proxy-manager` in a browser**
|
||||
that is holding the old bundle. Not checked from a browser by this agent.
|
||||
|
||||
## Per host
|
||||
|
||||
Steps in plan order everywhere: ff-merge to the tag → `bun db:push` → `bun run plugins:update` →
|
||||
`pm2 restart officer`.
|
||||
|
||||
### `alpha` — success
|
||||
|
||||
- HEAD `007cc9a1` → `e9f1760f`. Tree clean, no untracked files.
|
||||
- `db:push` exit 0. Standing diff as above; database verified correct by query.
|
||||
- `plugins:update`: **14/14 updated in 59s**, exit 0. This bounced fourteen sidecars on its own, before
|
||||
any restart the plan asked for — the report line below understates what actually moved.
|
||||
- `pm2 restart officer`: restart_time 3 → 4, `online`, uptime 14s at check.
|
||||
- All **21** PM2 entries `online`. `officer-claude-code` untouched (uptime 28h).
|
||||
|
||||
### `fofos` — success
|
||||
|
||||
- HEAD `007cc9a1` → `e9f1760f`. Tracked-clean. **One untracked file: `scripts/chk.ts`** — noted, not a
|
||||
reason to skip, and not touched.
|
||||
- `db:push` exit 0; `proxy_manager_connection`, `proxy_manager_dns_credentials`, `proxy_manager_domains`
|
||||
all present, as the plan predicted for a proxy-manager host.
|
||||
- `plugins:update`: 3/3 updated in 28s, exit 0 (browser, gitea, proxy-manager).
|
||||
- `pm2 restart officer`: restart_time 29 → 30, `online`, uptime 26s at check.
|
||||
- All **11** PM2 entries `online`. Frontend rebuild verified — see the section above.
|
||||
- Note: this host's `.env` names the database `OFFICERDEV_POSTGRES_URL`, not `POSTGRES_URL` as on the
|
||||
others. Cost one failed probe. Worth knowing for the next orchestrator that reads `.env` directly.
|
||||
|
||||
### `production-pertento-bridge` — success
|
||||
|
||||
- HEAD `007cc9a1` → `e9f1760f`. Clean, no untracked files.
|
||||
- `db:push` exit 0; standing diff only; verified correct by query. 39 tables in `public`.
|
||||
- `plugins:update`: 1/1 updated in 4s, exit 0 (gitea).
|
||||
- `pm2 restart officer`: restart_time 8 → 9, `online`, uptime 22s at check.
|
||||
- All **15** PM2 entries `online`. `officer-claude-code` untouched (uptime 76h).
|
||||
- Every command on this host prints an nvm/`.npmrc` `globalconfig` warning. Pre-existing, cosmetic,
|
||||
nothing failed because of it — but it is noise an orchestrator has to filter, and it will eventually
|
||||
hide something.
|
||||
|
||||
### `deadpool-rezio-bridge` — success
|
||||
|
||||
- Login `br`, checkout `/home/br/officerdev/platform`, taken from PM2's `pm_cwd`.
|
||||
- HEAD `007cc9a1` → `e9f1760f`. Clean, no untracked files.
|
||||
- `db:push` exit 0; standing diff only; verified correct by query.
|
||||
- `plugins:update`: 2/2 updated in 8s, exit 0 (email, offscale).
|
||||
- `pm2 restart officer`: restart_time 14 → 15, `online`, uptime 22s at check.
|
||||
- All **9** PM2 entries `online`.
|
||||
|
||||
### `macbookpro` — success (it was awake)
|
||||
|
||||
- Checkout `/Users/pastilhas/projects/officer-suite/platform`. `PATH` **appended** with
|
||||
`/opt/homebrew/bin` for the `bun` commands; `pm2` run as a separate command, per `docs/servers.md`.
|
||||
- HEAD `007cc9a1` → `e9f1760f`. Clean, no untracked files.
|
||||
- `db:push` exit 0; standing diff only; verified correct by query.
|
||||
- `plugins:update`: **no plugins installed — nothing to update**, exit 0.
|
||||
- `pm2 restart officer`: restart_time 16 → 17, `online`, uptime 27s at check.
|
||||
- All **6** PM2 entries `online`.
|
||||
|
||||
## Skipped
|
||||
|
||||
### `officer-dev` — skipped, by plan and by doctrine
|
||||
|
||||
Not pulled, nothing restarted. This agent runs inside `officer-claude-code` on this machine; restarting
|
||||
`officer` here ends the rollout. It already carries this code — its HEAD was `e9f1760f` before the
|
||||
rollout began.
|
||||
|
||||
**Its working tree holds another session's uncommitted work** — modified `src/servers/hono.ts` and
|
||||
`src/workspaces/hooks/src/useClient.ts`, plus an untracked `src/servers/hono-error-shape.test.ts`. Left
|
||||
alone. The report commit below names only its own path.
|
||||
|
||||
The owner restarts this host deliberately, as the last step, once happy with the rest.
|
||||
|
||||
### `edge-pertento-bridge` — skipped, by plan. Not contacted.
|
||||
|
||||
**And `docs/servers.md` disagrees with the plan about this host.** The doc's "Deliberately skipped"
|
||||
section says "None today" and lists `edge-pertento-bridge` in the platforms table as "a deploy target
|
||||
again". The plan says skip it: 300+ commits behind, **no `roles` table**, and a plain `db:push` there
|
||||
would create a foreign key onto an empty table. Its catch-up is a by-hand job the owner is doing
|
||||
separately.
|
||||
|
||||
The plan won — it is authoritative for a release, and it is the more recent statement. Nothing was sent
|
||||
to that host, not even a reachability probe. **`docs/servers.md` should be corrected**, because the next
|
||||
orchestrator that reads the doc rather than the plan will `db:push` a host that cannot take one. Not
|
||||
fixed here: a rollout does not edit documentation.
|
||||
|
||||
## What was NOT done
|
||||
|
||||
- **`officer-claude-code` was not restarted on any host**, per the plan. The widened chat credential
|
||||
(an API key follows its holder, minus `admin`) lives in that sidecar, so **it is not active anywhere
|
||||
yet**. It is the owner's call when to take that — it ends every live agent session on the host,
|
||||
including this one. **Pending on all five targets plus `officer-dev`.**
|
||||
- **`officer-dev` was not deployed.** Skipped whole, per above.
|
||||
- **The sonnet default was not confirmed from a UI on any host.** The plan names it as the easiest
|
||||
visible check; this agent has no browser. It shipped in the code that is now at `e9f1760f` on all
|
||||
five, unverified at the surface.
|
||||
- **`/proxy-manager` was not opened in a browser.** Verified from the build and the logs instead.
|
||||
- `bun run docs:check` was not run — not a rollout step, and this agent does not fix documentation.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 2026.08.26.02
|
||||
|
||||
**One fix, and it is a crash.** A screen whose layout names a panel it does not own now renders instead of
|
||||
failing with React #185, "maximum update depth exceeded".
|
||||
|
||||
**No schema change. No dependency change. Do not run `bun install`.** `bun db:push` stays a step because it
|
||||
is cheap and proves the database agrees with the code, but it must report no changes.
|
||||
|
||||
## What broke, and where it shows
|
||||
|
||||
`useAuthoredLayout` shipped in `2026.08.26.01`: a locked screen persists its SIZES and takes its STRUCTURE
|
||||
from source on every render. That is still right, and it is what lets a plugin change its layout and reach
|
||||
people who have already opened it.
|
||||
|
||||
What it did not account for is `WorkspaceView`'s `appTypes` guard, which repairs an unrecognised `appType`
|
||||
and **writes the repair back**. That was self-limiting while structure was persisted — the repair stuck and
|
||||
the next render agreed with it. With structure coming from source it can never stick, so it is recomputed,
|
||||
rewritten, discarded and recomputed every render until React gives up.
|
||||
|
||||
**The `browser` plugin's layout names `officerdev/chat`**, which is not one of its own panel keys. So
|
||||
`/browser` fails to render on any host with that plugin installed. That is every host running
|
||||
`2026.08.26.01` with it — which is the fleet.
|
||||
|
||||
The fix drops a write whose result equals the current value. Not an optimisation: it is what terminates the
|
||||
loop, and persisting an identical tree was pointless anyway.
|
||||
|
||||
## Every host
|
||||
|
||||
1. `git fetch origin --tags` then `git merge --ff-only deploy/2026.08.26.02`
|
||||
2. `bun db:push` — expect **no changes**. If it wants to alter something, stop and report it.
|
||||
3. `bun run plugins:update`
|
||||
4. `pm2 restart officer`
|
||||
|
||||
**`fofos` is already at the fix** — it was fast-forwarded by hand at 05:22 to `a7c9dab2` while the owner was
|
||||
working on it. Step 1 will fast-forward it to this tag anyway, which only adds the version bump and this
|
||||
plan. Nothing about that is a problem; note it rather than treating it as drift.
|
||||
|
||||
**`macbookpro`:** append `/opt/homebrew/bin` to PATH for the `bun` commands, run `pm2` separately. Asleep is
|
||||
a skip with a note.
|
||||
|
||||
**Do not restart `officer-claude-code`.** Unchanged from the last release, same reason: it ends every live
|
||||
agent session on the host, and it is the owner's call. Nothing in this release needs it.
|
||||
|
||||
## Hosts
|
||||
|
||||
- **`officer-dev`** — skip it. Restarting `officer` there ends the rollout. It already has this code.
|
||||
- **`edge-pertento-bridge`** — skipped. Still 300+ commits behind with no `roles` table; its catch-up is a
|
||||
by-hand job. Do not contact it.
|
||||
- **`alpha`**, **`fofos`**, **`production-pertento-bridge`**, **`deadpool-rezio-bridge`** — the targets.
|
||||
- **`macbookpro`** — a target if it is awake.
|
||||
|
||||
## What to check
|
||||
|
||||
**Open `/browser`.** It should render rather than showing "This screen failed to render". That is the whole
|
||||
release, and it is one click to confirm. Hard refresh first — a stale bundle looks identical to the bug.
|
||||
|
||||
No direct SQL is needed anywhere, and none is authorised.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "officer",
|
||||
"version": "2026.08.26.01",
|
||||
"version": "2026.08.26.02",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -96,3 +96,34 @@ describe('applySizes', () => {
|
||||
expect(JSON.stringify(source)).toBe(before);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The loop that broke `/browser`.
|
||||
*
|
||||
* `WorkspaceView` repairs an appType it does not recognise and writes the repair back. With the structure
|
||||
* coming from source, that repair can never persist — so unless an identical write is dropped, it is
|
||||
* recomputed and rewritten every render until React gives up (#185).
|
||||
*/
|
||||
describe('a repair that cannot stick must not be written', () => {
|
||||
test('grafting a repaired structure yields exactly the current value', () => {
|
||||
// What the guard produces: the same tree with a disallowed appType swapped for the fallback.
|
||||
const repaired = group('root', [
|
||||
{
|
||||
node: group('lists', [
|
||||
{ node: panel('hosts', 'proxy-manager-hosts'), size: 62 },
|
||||
{ node: panel('certs', 'proxy-manager-certificates'), size: 38 },
|
||||
]),
|
||||
size: 66,
|
||||
},
|
||||
// `chat` replaced by the fallback — the substitution the allow-list makes every render.
|
||||
{ node: panel('chat', 'proxy-manager-hosts'), size: 34 },
|
||||
]);
|
||||
|
||||
const current = applySizes(source, collectSizes(source));
|
||||
const afterRepair = applySizes(source, collectSizes(repaired));
|
||||
|
||||
// Identical, because only sizes survive and the repair changed none. `setValue` compares these and
|
||||
// returns without writing; if it ever writes, the render loop is back.
|
||||
expect(JSON.stringify(afterRepair)).toBe(JSON.stringify(current));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,27 @@ export function useAuthoredLayout(key: string, source: LayoutNode) {
|
||||
const setValue = useCallback(
|
||||
(update: LayoutNode | ((prev: LayoutNode) => LayoutNode)) => {
|
||||
const next = typeof update === 'function' ? update(value) : update;
|
||||
stored.setValue(applySizes(source, collectSizes(next)));
|
||||
const merged = applySizes(source, collectSizes(next));
|
||||
|
||||
/*
|
||||
A write that changes nothing is dropped, and this is not an optimisation — it is what stops an
|
||||
infinite render loop.
|
||||
|
||||
`WorkspaceView`'s `appTypes` guard repairs an unrecognised appType and WRITES THE REPAIR BACK. That
|
||||
was self-limiting while the structure was persisted: the repair stuck and the next render agreed
|
||||
with it. Here the structure always comes from `source`, so the repair can never stick — it is
|
||||
recomputed, rewritten, discarded and recomputed, every render, forever. React gives up with
|
||||
"maximum update depth exceeded" (#185) and the screen fails to render.
|
||||
|
||||
Not hypothetical: the `browser` plugin's layout names `officerdev/chat`, which is not one of its own
|
||||
panel keys, so every render of `/browser` produced a repair. Shipping the sizes-only hook turned a
|
||||
silent, harmless fallback into a hard crash on that screen.
|
||||
|
||||
Comparing the serialised value is enough — these trees are small, this runs only on a real change,
|
||||
and it is the same check `setValue` would otherwise force React Query to make against a new object.
|
||||
*/
|
||||
if (JSON.stringify(merged) === JSON.stringify(value)) return;
|
||||
stored.setValue(merged);
|
||||
},
|
||||
// `stored.setValue` is stable per key; `value` is memoised above.
|
||||
[source, value, stored],
|
||||
|
||||
Reference in New Issue
Block a user