Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa5703337 | ||
|
|
06bed42ace | ||
|
|
f4147cef30 | ||
|
|
091cfcca96 |
@@ -22,7 +22,7 @@ the owner's OS user and can never be granted. Indirection there really is accide
|
||||
|
||||
## Themes
|
||||
|
||||
**109 open, 33 closed**, across 35 themes.
|
||||
**110 open, 33 closed**, across 35 themes.
|
||||
|
||||
| Theme | Open | Closed |
|
||||
| --- | --- | --- |
|
||||
@@ -41,7 +41,7 @@ the owner's OS user and can never be granted. Indirection there really is accide
|
||||
| [Headscale](todo/headscale/) | 0 | 5 |
|
||||
| [Infra (alpha)](todo/infra/) | 7 | 0 |
|
||||
| [Integrations — external tools worth wrapping as a plugin](todo/integrations/) | 1 | 0 |
|
||||
| [Known bugs](todo/known-bugs/) | 7 | 4 |
|
||||
| [Known bugs](todo/known-bugs/) | 8 | 4 |
|
||||
| [Memory — one store instead of per-harness files](todo/memory/) | 1 | 0 |
|
||||
| [Mobile apps](todo/mobile-apps/) | 5 | 0 |
|
||||
| [Multi-user](todo/multi-user/) | 17 | 5 |
|
||||
@@ -71,6 +71,7 @@ the owner's OS user and can never be granted. Indirection there really is accide
|
||||
|
||||
## Newest ten
|
||||
|
||||
- `2026-09-22` **caldav retries `brew install radicale` on every boot, and there is no such formula** — [known-bugs/radicale-cannot-be-installed-on-macos](todo/known-bugs/radicale-cannot-be-installed-on-macos.md)
|
||||
- `2026-09-04` **M3E Canvas as a plugin — sketch a UI, hand the prompt to the agent** — [backburner/m3e-canvas-as-a-plugin](todo/backburner/m3e-canvas-as-a-plugin.md)
|
||||
- `2026-08-29` **Officer blocks its own event loop for ~0.9s re-parsing a live transcript, and everything it serves stalls with it** — [known-bugs/officer-blocks-its-event-loop-parsing-transcripts](todo/known-bugs/officer-blocks-its-event-loop-parsing-transcripts.md)
|
||||
- `2026-08-27` **Turn an ordinary chat into a commander or a bot, carrying its context along** — [agent-coordination/turn-a-chat-into-a-commander-or-a-bot](todo/agent-coordination/turn-a-chat-into-a-commander-or-a-bot.md)
|
||||
@@ -80,4 +81,3 @@ the owner's OS user and can never be granted. Indirection there really is accide
|
||||
- `2026-08-27` **The calendar/contacts skill has never been exercised by an actual agent** — [known-bugs/calendar-skill-never-used-by-an-agent](todo/known-bugs/calendar-skill-never-used-by-an-agent.md)
|
||||
- `2026-08-27` **Setting up a phone for calendar and contacts should be one link inside Officer, not a manual account form** — [mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app](todo/mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app.md)
|
||||
- `2026-08-27` **Marketplace: browse by tag — filter list, card grid, detail with a back button** — [plugin-ecosystem/marketplace-browse-by-tag](todo/plugin-ecosystem/marketplace-browse-by-tag.md)
|
||||
- `2026-08-25` **An affordance: a scoped agent warns when a request lands outside its scope** — [affordances/an-affordance-a-scoped-agent-warns-on-the](todo/affordances/an-affordance-a-scoped-agent-warns-on-the.md)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# 2026.09.14.01 — report
|
||||
|
||||
**4 of 4 hosts succeeded.** `officer-dev`, `macbruno` and both Pertento hosts skipped, all deliberately.
|
||||
|
||||
Backend-only release (`be3139f9..4a37cfd2`): three new self-reported `command-hq` routes. No schema
|
||||
change, so no `db:push` and no schema query — the plan's two checks (`pm2 jlist`, `/api/health`) are the
|
||||
whole verification, and both ran on every host.
|
||||
|
||||
Every host went `be3139f9` (`deploy/2026.09.09.01`) → `4a37cfd2` (`deploy/2026.09.14.01`), by
|
||||
`git merge --ff-only` onto the TAG, not `master`. Every checkout path was read from that host's own
|
||||
`officer` `pm_cwd`, never assumed.
|
||||
|
||||
## Per host
|
||||
|
||||
| Host | Before | After | Dirty | plugins:update | Restarted | Back? |
|
||||
| ----------------------- | ---------- | ---------- | ----- | -------------- | --------- | --------------------------- |
|
||||
| `alpha` | `be3139f9` | `4a37cfd2` | clean | 17/17 | `officer` | online, 0→1, health `401` |
|
||||
| `fofos` | `be3139f9` | `4a37cfd2` | clean¹| 5/5 | `officer` | online, 1→2, health `401` |
|
||||
| `macbookpro` | `be3139f9` | `4a37cfd2` | clean | none installed | `officer` | online, 1→2, health `401` (:9010) |
|
||||
| `deadpool-rezio-bridge` | `be3139f9` | `4a37cfd2` | clean | 3/3 | `officer` | online, 0→1, health `401` |
|
||||
|
||||
¹ `fofos` carries two UNTRACKED files — `flip-records.ts` and `scripts/chk.ts`. Untracked is a note, not
|
||||
a skip: a fast-forward cannot touch them and nobody's work was discarded.
|
||||
|
||||
All four checkouts were clean of TRACKED changes, so no host was skipped for a dirty tree and every merge
|
||||
was a genuine fast-forward.
|
||||
|
||||
`401` is healthy here — `/api/health` is authenticated, and the plan names `200` or `401` as pass. The
|
||||
port was read from each host's own `.env` rather than assumed: `9010` on `macbookpro`, `9000` elsewhere.
|
||||
|
||||
## Steps that ran
|
||||
|
||||
Per host, in order: tracked-only dirty check → `git fetch origin --tags` → `git merge --ff-only
|
||||
deploy/2026.09.14.01` → `bun run plugins:update` → `pm2 restart officer` → `pm2 jlist` → health.
|
||||
|
||||
No `bun install` and no `bun db:push` on any host. The plan says neither is needed; running either would
|
||||
not have been a finding, but it was not needed and was not done.
|
||||
|
||||
`plugins:update` restarts sidecars itself. On `alpha` that is more processes moving than the one line
|
||||
above suggests — the table's "restarted" column is what the PLAN asked for, not the full count of what
|
||||
bounced.
|
||||
|
||||
## Skipped — none of these is a failure
|
||||
|
||||
- **`officer-dev`** — the host this rollout ran on. Skipped entirely per the standard rule: not pulled,
|
||||
nothing restarted. **It is still on `be3139f9` and the owner restarts it deliberately.**
|
||||
- **`macbruno`** — `ssh` timed out on port 22 (`Connection timed out`, 8s). The plan names it as
|
||||
check-live, unreachable-is-a-skip. Not attempted further; it cost one probe, not a retry budget.
|
||||
- **`production-pertento-bridge`, `edge-pertento-bridge`** — on the deliberately-skipped list in
|
||||
`docs/servers.md` since 2026-09-08. Owner's call. Nothing spent on either.
|
||||
|
||||
## What the owner must act on
|
||||
|
||||
- **`officer-dev` has not been deployed.** It is the one host still on the previous release. This is the
|
||||
designed outcome, not an oversight — restarting `officer` here would have killed the rollout mid-flight.
|
||||
- **`macbruno` is behind by one release** and unreachable as of this rollout. It will need the same
|
||||
four-step sequence whenever it is back.
|
||||
- Nothing else. No host failed, no process came back `errored`, no step was refused.
|
||||
|
||||
## Carried, not introduced
|
||||
|
||||
`bunx tsgo` reports one pre-existing type error in `src/servers/sidecar/claude/claude-manager.ts:667`
|
||||
(`SdkUserMessage` vs `SDKUserMessage`, from the agent SDK's own types). Named in the plan, present before
|
||||
this release, type-only. Not checked again here and not caused by this rollout.
|
||||
|
||||
## About this run
|
||||
|
||||
First release under a new watcher. The rollout procedure is proven — this is the tenth report — but this
|
||||
loop had never executed it before, so every host was narrated before it was touched. The claim was taken
|
||||
by the watcher that spawned this agent; this agent did not re-claim, and recorded `done` only after the
|
||||
fourth host came back healthy.
|
||||
@@ -433,6 +433,16 @@ router.post('/rename', async (ctx) => {
|
||||
*
|
||||
* The cache key contains the file's mtime and size, so this URL's answer genuinely cannot change: a
|
||||
* different file is a different URL. That is what makes a year-long cache correct rather than a bet.
|
||||
*
|
||||
* It was only ever half true, and the half that was missing is the half a browser can see. The DISK key
|
||||
* has always held mtime and size, so the server regenerated correctly every time. The URL held the path
|
||||
* and the box size and nothing else — so a browser told to keep the answer for a year kept it for a
|
||||
* year, and a file replaced or renamed into a name already drawn showed the picture that used to be
|
||||
* there. Nothing cleared it but somebody who knew to hard-reload.
|
||||
*
|
||||
* The client now sends `v=<mtime>-<size>` — see `getThumbUrl`. Nothing here reads it: this route stats
|
||||
* the file anyway, and a parameter the server depended on would be a second source of truth about which
|
||||
* file is meant. It is in the URL to be part of the URL, which is the whole job.
|
||||
*/
|
||||
router.get('/thumb', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
|
||||
@@ -212,6 +212,27 @@ export async function rebuildFrontend(): Promise<BuildResult> {
|
||||
plugins: [tailwind],
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
sourcemap: 'none',
|
||||
/*
|
||||
WITHOUT THIS A PLUGIN CAN TAKE THE WHOLE APP DOWN WITH ONE LINE.
|
||||
|
||||
`scripts/build/web.ts` — the platform's own bundle — defines `process.env.NODE_ENV`. This build
|
||||
defined nothing, so any `process.*` in a plugin's frontend was emitted verbatim into a browser
|
||||
chunk and threw `ReferenceError: process is not defined` at module evaluation. One throwing module
|
||||
fails the whole graph, so the symptom is not a broken panel, it is a blank platform.
|
||||
|
||||
Reached by vendored code rather than by anyone writing Node in a component: m3e-canvas carries four
|
||||
files from a Next.js original that read `process.env.NEXT_PUBLIC_BASE_PATH`. Vendored trees stay
|
||||
close to upstream here (see the hls.js note in CLAUDE.md), so the build is the right place to
|
||||
absorb it — and it protects every plugin rather than the one that happened to hit it.
|
||||
|
||||
Both keys, and the order does not matter: the longest match wins, so `NODE_ENV` keeps its real
|
||||
value and every other `process.env.X` becomes `({}).X`, which is `undefined` — exactly what the
|
||||
`?? ''` in that vendored code expects. Verified against `bun build` rather than assumed.
|
||||
*/
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV === 'production' ? 'production' : 'development'),
|
||||
'process.env': '({})',
|
||||
},
|
||||
});
|
||||
if (!result.success) {
|
||||
return {
|
||||
|
||||
+11
-2
@@ -52,7 +52,7 @@ import { cardStyle } from '@/components/Card';
|
||||
import type { DirEntry } from '../../../../hooks/useFilesAPI';
|
||||
import type { TaskGroup, TaskSummary } from '../../useTasks';
|
||||
import type { AgentGroup, AgentSummary } from '../../useAgents';
|
||||
import { getFileType } from '../../../FileViewer';
|
||||
import { getFileType, thumbVersion } from '../../../FileViewer';
|
||||
import { useTranslation } from '../../../../locales';
|
||||
import { useFormat } from 'i18n';
|
||||
|
||||
@@ -670,7 +670,16 @@ export const FileItem = ({
|
||||
rounded box would trim content off a photograph for decoration.
|
||||
*/
|
||||
const withThumb = (kind: 'image' | 'video', node: React.ReactNode, className: string, size: number) => (
|
||||
<Thumbnail path={path} root={root} size={size} isVideo={kind === 'video'} className={className}>
|
||||
<Thumbnail
|
||||
path={path}
|
||||
root={root}
|
||||
size={size}
|
||||
// Which version of the file, so replacing or renaming one does not leave the old picture in a
|
||||
// browser cache that was told it could keep it for a year. See `Thumbnail`.
|
||||
version={thumbVersion(entry)}
|
||||
isVideo={kind === 'video'}
|
||||
className={className}
|
||||
>
|
||||
{node}
|
||||
</Thumbnail>
|
||||
);
|
||||
|
||||
+13
-1
@@ -52,6 +52,7 @@ export const Thumbnail = ({
|
||||
path,
|
||||
root,
|
||||
size,
|
||||
version,
|
||||
isVideo,
|
||||
className,
|
||||
children,
|
||||
@@ -59,6 +60,16 @@ export const Thumbnail = ({
|
||||
/** Root-relative, the same path every other file-browser call uses. */
|
||||
path: string;
|
||||
root?: string;
|
||||
/**
|
||||
* Which VERSION of the file this is — `mtime-size`, from the listing.
|
||||
*
|
||||
* The response is `immutable` for a year, and the URL used to carry only the path, so a browser that
|
||||
* had drawn a thumbnail for a name went on drawing it however the file behind that name changed. A
|
||||
* rename into a name already seen showed the picture that used to be there, and nothing short of a
|
||||
* hard reload cleared it. The SERVER was never wrong — its own cache key has always held mtime and
|
||||
* size — the URL simply did not say which file it meant. See `getThumbUrl`.
|
||||
*/
|
||||
version?: string;
|
||||
/** What to ask the server for. Snapped server-side, so a near-enough number is fine. */
|
||||
size: number;
|
||||
/** Draws the play badge. The thumbnail of a video is a still, and a still is not obviously a video. */
|
||||
@@ -79,7 +90,8 @@ export const Thumbnail = ({
|
||||
|
||||
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
|
||||
const rootParam = root && root !== 'home' ? `&root=${encodeURIComponent(root)}` : '';
|
||||
const src = `${API_URL}/file-browser/thumb?path=${encodeURIComponent(path)}&size=${size}&token=${encodeURIComponent(token)}${rootParam}`;
|
||||
const versionParam = version ? `&v=${encodeURIComponent(version)}` : '';
|
||||
const src = `${API_URL}/file-browser/thumb?path=${encodeURIComponent(path)}&size=${size}&token=${encodeURIComponent(token)}${rootParam}${versionParam}`;
|
||||
|
||||
return (
|
||||
<span className={`relative inline-flex items-center justify-center overflow-hidden ${className ?? ''}`}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { useFilesAPI, type DirEntry } from '../../hooks/useFilesAPI';
|
||||
import { getFileType, getThumbUrl } from './file-types';
|
||||
import { getFileType, getThumbUrl, thumbVersion } from './file-types';
|
||||
import { useFileViewer } from './FileViewerContext';
|
||||
|
||||
/**
|
||||
@@ -128,7 +128,7 @@ export const FileCarousel = ({ onClose }: { onClose?: () => void }) => {
|
||||
>
|
||||
{entry.thumb ? (
|
||||
<img
|
||||
src={getThumbUrl(path, root)}
|
||||
src={getThumbUrl(path, root, 256, thumbVersion(entry))}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
|
||||
@@ -165,14 +165,31 @@ export function getRawUrl(filePath: string, root?: string, audioTrack?: number):
|
||||
*
|
||||
* Same route, same cache, same size — so a folder already browsed has a warm carousel strip, and a
|
||||
* format that needs a plugin to decode works here exactly where it works there.
|
||||
*
|
||||
* `version` is what makes the year-long `immutable` on that response honest. The SERVER's cache key has
|
||||
* always held the file's mtime and size; this URL held only the path, so a browser that had once drawn
|
||||
* a thumbnail for a name kept drawing it for a year however the file behind that name changed. Upload,
|
||||
* rename something else into the name, replace a photograph — the picture on screen stayed. Pass
|
||||
* `mtime-size` and a changed file is simply a different URL. The server ignores the parameter; it stats
|
||||
* the file regardless.
|
||||
*/
|
||||
export function getThumbUrl(filePath: string, root?: string, size = 256): string {
|
||||
export function getThumbUrl(filePath: string, root?: string, size = 256, version?: string): string {
|
||||
const headers = getHeaders();
|
||||
const token = headers['Authorization']?.replace('Bearer ', '') ?? '';
|
||||
const rootParam = root && root !== 'home' ? `&root=${encodeURIComponent(root)}` : '';
|
||||
return `${API_URL}/file-browser/thumb?path=${encodeURIComponent(filePath)}&size=${size}&token=${encodeURIComponent(token)}${rootParam}`;
|
||||
const versionParam = version ? `&v=${encodeURIComponent(version)}` : '';
|
||||
return `${API_URL}/file-browser/thumb?path=${encodeURIComponent(filePath)}&size=${size}&token=${encodeURIComponent(token)}${rootParam}${versionParam}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* What identifies this VERSION of a file, for a cache key — the same two facts the server hashes.
|
||||
*
|
||||
* One place, because the grid and the carousel strip must agree: two spellings would mean two URLs for
|
||||
* one file, which is two renders and two cache entries for no reason.
|
||||
*/
|
||||
export const thumbVersion = (entry: { modifiedAt?: number; size?: number }): string | undefined =>
|
||||
Number.isFinite(entry.modifiedAt) && Number.isFinite(entry.size) ? `${entry.modifiedAt}-${entry.size}` : undefined;
|
||||
|
||||
export function getAudioTracksUrl(filePath: string, root?: string): string {
|
||||
const headers = getHeaders();
|
||||
const token = headers['Authorization']?.replace('Bearer ', '') ?? '';
|
||||
|
||||
@@ -4,7 +4,7 @@ export { FileViewerProvider } from './FileViewerProvider';
|
||||
export { FileViewerHeader } from './FileViewerHeader';
|
||||
export { FileViewerBody } from './FileViewerBody';
|
||||
export { useFileViewer } from './FileViewerContext';
|
||||
export { getFileType, getLang, getExt, getArchiveBaseName, ARCHIVE_EXTS, type FileType } from './file-types';
|
||||
export { getFileType, getLang, getExt, getArchiveBaseName, thumbVersion, ARCHIVE_EXTS, type FileType } from './file-types';
|
||||
|
||||
// No `appRegistryMetas` here on purpose. The file viewer is not a panel app you can put on a dashboard:
|
||||
// it is mounted as an ephemeral panel by `useFileViewerPanels`, which supplies `FileViewerBody`/`Header`
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: known-bugs/radicale-cannot-be-installed-on-macos
|
||||
title: 'caldav retries `brew install radicale` on every boot, and there is no such formula'
|
||||
theme: known-bugs
|
||||
status: open
|
||||
created: 2026-09-22
|
||||
date_source: stated
|
||||
order: 8
|
||||
---
|
||||
|
||||
**On macOS, caldav fails its own dependency install on every single boot**, logging a line that reads like
|
||||
a transient error and is permanent:
|
||||
|
||||
```
|
||||
[caldav] radicale install failed (exit 1): … Warning: No available formula with the name "radicale".
|
||||
```
|
||||
|
||||
Confirmed on `macbookpro` (macOS 26.6.2) 2026-09-22 — four consecutive boots, four identical failures.
|
||||
|
||||
**Two things are wrong and only one is caldav's.** `plugins/caldav/manifest.ts` declares
|
||||
`packages: { apt, pacman, dnf }` with **no `brew` entry**, so the installer falls back to the binary name
|
||||
and runs `brew install radicale`. And the fallback cannot succeed anyway: **radicale is not a Homebrew
|
||||
formula.** It is a Python application, installed with `pipx install radicale` or `uv tool install radicale`.
|
||||
Homebrew's suggestion in the log — "Did you mean radicle or cadical?" — is two unrelated projects.
|
||||
|
||||
So the manifest has nothing correct to put in a `brew` field, which is the actual gap:
|
||||
`PackageManager` is `'apt' | 'pacman' | 'dnf' | 'brew'` (`plugins/manifest.ts`). There is no way to express
|
||||
"this comes from pipx", and no way to say "not available on this platform" — so the only outcomes are a
|
||||
wrong guess retried forever, or silence.
|
||||
|
||||
Consequences, in order of how much they matter: caldav does not work on that machine at all (no calendars,
|
||||
no contacts — the manifest's own `reason` says so); the failure repeats every boot with no backoff and no
|
||||
memory; and the log line is indistinguishable from something worth chasing, which is how it cost a
|
||||
debugging session that had been opened about something else entirely.
|
||||
|
||||
Worth deciding rather than patching: either `PackageManager` grows a `pipx`/`uv` member, or
|
||||
`osDependencies` grows a way to declare a platform where a binary is genuinely unavailable, so the
|
||||
installer can say it once and stop.
|
||||
|
||||
---
|
||||
|
||||
**Found alongside, and NOT a bug — recorded so the next person does not chase it.** The same log carried
|
||||
`[plugins] frontend rebuild failed: ResolveMessage: Could not resolve: "motion/react"`, which looked like
|
||||
the real failure and was already over. It appeared on exactly one boot: the m3e-canvas plugin had landed on
|
||||
disk before `plugins/node_modules/motion` existed. Verified on the machine afterwards — `motion` is listed
|
||||
in the generated `plugins/package.json`, installed, and `import('motion/react')` resolves with 398 exports;
|
||||
the current boot rebuilds cleanly.
|
||||
|
||||
**The gap it exposes is real even though it healed.** `installNpmDependencies` is called from exactly one
|
||||
place, `plugins/install.ts`, the install flow. Boot (`plugins/mount.ts`) rebuilds the plugin frontend and
|
||||
never installs anything. So a plugin that reaches `plugins/` without that flow completing — cloned by hand,
|
||||
or an install interrupted — leaves every subsequent boot failing the rebuild with a message that says
|
||||
_"Maybe you need to bun install"_ while nothing ever will. Here the install finished and the next boot was
|
||||
fine; on a machine where it does not, the platform cannot recover on its own.
|
||||
Reference in New Issue
Block a user