PAGE_SIZE = 128 (was 24), one constant for the fill job, public pages,
dumps, and DiigoBridge, so a cache never mixes sizes: ~131 pages instead
of ~698 for 16.7k bookmarks.
Cache invalidation, agreed beforehand: pages are cached by (page number,
size), so pages cached at 24 become misses and refetch at 128. Items are
keyed by link_id and just re-upsert; nothing is lost.
Unknown whether diigo honors count=128, so a short page that isn't the
last is refused loudly (logged, thrown; a fill fails with the reason)
rather than silently skipping items. The fake gains a capped account to
test it; the plain account grows to 300 (3 pages), the deep one to 6000.
Generated-by: Claude Opus 5.5
/cache/ without a fragment is now a login form: petname
(autocomplete=username) and cache id (type=password,
autocomplete=current-password), the shape password managers fill from
the entries the save button makes. Its inputs have no name, so a native
submit (script failed) sends nothing; the key stays in memory, out of
the address bar and history. Home links to it.
Generated-by: Claude Opus 5.5
Past the transitional idiom: the cache logic (DiigoCacheLogic,
PublicCacheLogic in cache.ts) requires its Clock, with no default, so
cache.ts reaches no ambient time. The worker's entry module is the
edge: it makes the live clock and exports the runtime-constructed
DiigoCache/PublicCache as (ctx, env) subclasses that supply it (the
Cloudflare constraint: the runtime constructs DOs with ctx and env
only). Binding class names are unchanged, so stored state is too. The
tests' FixedClockCache extends the logic class.
Generated-by: Claude Opus 5.5
No module-level liveClock: the live clock is made in the default
argument expression, at the edge where the runtime constructs the DO,
so module scope holds no powerful object.
Generated-by: Claude Opus 5.5
Ocap discipline: no ambient time in logic. PagedCache reads time and
waits only through a Clock { now, sleep }, a default constructor
argument (the live clock; the runtime constructs DOs with ctx and env
only). The fill job's timestamps and alarms, the etch time, and the
diigo-call pacing all go through it.
Tests interpose through the constructor: tests/test-worker.ts (now the
tests' main module) adds FixedClockCache, bound as CLOCKED_DO, and a
test pins startedAt, updatedAt, and the alarm time to the fixed clock
(an ambient new Date() fails it).
Generated-by: Claude Opus 5.5
The cache page now follows the paper's form (fragment + skeleton page),
with the XHR replaced by a Cap'n Web call taking the key in-band.
Generated-by: Claude Opus 5.5
Starting a fill no longer POSTs the cache id to /_fill. Instead:
- the created page links the cache's webkey, /cache/#<cache id>: a
skeleton page, the same for every key, that reads the key from the
fragment (never sent to the server or in a Referer)
- its script trades the key in-band for a capability over Cap'n Web at
/api: CacheApi.cache(key) returns a CacheCap facet (fill, status,
fingerprint, the public companion's fill) and never the DO's wiring
methods; malformed and unknown keys get one refusal that never echoes
the key, and each refusal is logged without it
- then calls startFill(): trade and call pipeline into one round trip
- the worker serves capnweb's browser build itself at /_capnweb.js (a
wrangler Text rule), so no third-party script sits beside the key
The /_fill route and its forms are gone. Tests run the real capnweb
client against the worker (fetch routed to worker.fetch).
Generated-by: Claude Opus 5.5
POST /_fill (cache id in the body; target private|public; action
start|stop) starts or stops a background walk of every diigo page into
the cache. DO alarms advance it pagesPerStep pages per step (default
20), a second apart; progress (FillStatus) is stored in the DO after
each page, so it survives eviction. Cached pages are skipped; starting
while running rejoins the job; a stop mid-step wins; a failure is
recorded with its reason and logged once. The created page gets fill
buttons; the public page shows "filling: n of m pages".
Fill state is new, additive DO storage (fill, fillStep): no existing
state changes meaning.
The 13 fill specs are unskipped and green, plus one: a fill makes one
diigo call per page fetched.
Generated-by: Claude Opus 5.5
Two bugs in page()'s miss path, found while building the fill job:
- the webapp stubs (app, pathjoin, query) were never disposed, which
kept the DO from being evicted (evictDurableObject hung after a single
miss); they are now disposed once the page is in hand
- a miss on page n > 1 also fetched page 1 and threw it away, doubling
diigo calls; every page carries `total`, so only page n is fetched
Regression test: a miss, then eviction, then a cache hit. tsconfig adds
the ESNext.Disposable lib for Symbol.dispose.
Generated-by: Claude Opus 5.5
A button that grabs every page into a cache: owner-only POST /_fill
(cache id in the body; target private|public; action start|stop),
advanced by DO alarms a few pages per step, progress stored in the DO
(new, additive state) so it survives eviction, cached pages skipped,
a rerun rejoins the running job, failures recorded and logged once.
The created page gets the button; the public page shows progress.
startFill/fillStatus/stopFill on PagedCache are JSDoc'd stubs; 13
specs are describe.skip until it lands (alarms driven by
runDurableObjectAlarm, eviction by evictDurableObject).
Generated-by: Claude Opus 5.5
The public page's filters, toward the weekly PESOS roundup (#23):
?days=N (7/30/90/365 presets), gmail-style after/before (UTC days,
half-open: after inclusive, before exclusive; yyyy/mm/dd accepted), and
q (words, "phrases", #tags exact against diigo's comma-separated tags,
- negation); q alone searches the last 365 days and says so; malformed
filters are a 400. Filtering walks diigo's updated-desc pages from 1 and
stops at the window, so ?days=7 costs one page.
src/query.ts is the typed, JSDoc'd API (stubs that throw); 22 specs are
describe.skip until it lands. The fake webapp gains a dated account.
Generated-by: Claude Opus 5.5
The list is in diigo's updated order; each item now leads with its
updated_at as Date#toISOString() (diigo sends epoch seconds; updatedISO
converts, tested), or "(no date)".
Generated-by: Claude Opus 5.5
A hit must replay diigo's order as fetched (page_members.pos), not a
column sort. The fake webapp's link_id and updated_at are scrambled so
neither column sorts back into diigo's order; ordering the replay by
updated or item_id fails the new test.
Generated-by: Claude Opus 5.5
vitest, @cloudflare/vitest-plugin, and typescript are devDependencies
here instead of being borrowed from diigo-bookmarks through hand-made
node_modules symlinks and a deep-path import.
- lint: wrangler types, then tsc over src/ and tests/ (Cloudflare's
documented typecheck shape: regenerate types, then tsc)
- test: vitest run
- tests/tsconfig.json per the vitest-plugin docs; the dangling
typecheck script (tsconfig.check.json never existed) is gone
Generated-by: Claude Opus 5.5
wrangler bundles; tsc only checks. Without noEmit a bare `tsc` wrote
compiled .js beside the sources (a stray webapp/src/index.js).
Generated-by: Claude Opus 5.5
- checklist: 1c (diigo-dump evolves into a bookmark cache) with its
create-flow, id-wiring, and verification subgoals
- notes: diigo-cache replaces diigo-dump in What-we-built
- AGENTS: no worker secret since 1c; durable state — be LOUD and
conservative about breaking changes to stored DB state; pointer to
the workspace skills for commit trailers
Generated-by: opencode (GLM)
Generated-by: Claude Opus 5.5
The dump worker becomes a page cache: a hit is served from DO SQLite
storage with no diigo call; a miss asks the webapp instance in the
cache's wiring and etches the page.
- two independent caches, one behavior (PagedCache): DiigoCache keeps
every item; PublicCache keeps shared items only, dropping private
rows before they are stored; served at /public/<username> (HTML page
form, or JSON)
- create flow on one port: /_connect relays webapp's mint form, /_mint
proxies the mint, /_wire wires a private or public cache; /_publish
adds a public cache later
- ids are webkeys: kept out of URLs (POST bodies), shown and logged
only as a hash fingerprint; saved to the password manager under the
holder's petname (never sent to the server)
- items keyed on diigo's link_id; username from u_name; unkeyable
items and every failure path are logged; each page read logs hit/miss
- tests run the real worker + DOs in the vitest pool against a fake
webapp (tests/fake-webapp.js)
Generated-by: opencode (GLM)
Generated-by: Claude Opus 5.5
Close's web-key pattern (W2SP 2008) and its Waterken precursors, applied
to diigo-cache: cache ids are webkeys in substance; they travel in POST
bodies, never query strings, and a one-way hash (swissHash discipline)
stands in for them where log correlation is wanted.
Generated-by: opencode (GLM)
wrangler's multi-config dev binds aux workers but doesn't serve their
HTTP, so diigo-cache relays webapp's own mint form:
- WebAppFactory gains fetch(): the HTTP surface over the binding
- GET /form?render=1 leaves the form action as a placeholder the proxy
points at its relay; ?title= sets the page title
- a dedicated cookie field (sugar for the cookie header; must agree
with the JSON if both are given)
Generated-by: opencode (GLM)
Pure rename ahead of the dump worker's evolution into a bookmark cache
(goal 1c), so history follows the files. The diigo-login helper script
is dropped: the cookie now enters through webapp's mint form, never a
local script.
capsite-notes: What-we-built gains the webapp/diigo-dump layers (links
to READMEs and the goal tree, not repeats); root Goal restated as
'confine the diigo secret to a small auditable piece behind an
attenuated interface' per discussion, with the dead API-key path noted
and details delegated to the tree; credentials section de-staled
(placeholder values, unconditional 401 finding, cookie-path direction);
private-bookmarks note no longer recommends the dead HTTP Basic path.
diigo-bookmarks/README: title corrected sandboxed->native with the
conversion note and a disclaimer that its Develop/Publish sections are
inherited scaffold text (larger rework deferred).
Generated-by: opencode (GLM)
The hard rules move from the goal tree into a proper AGENTS.md at the
capsite root (discoverable by agents without reading the tree; the
workspace AGENTS.md's commit-trailer policy stays where it is, linked).
Checklist keeps a one-line pointer, so a continuation prompt still
surfaces them. Error hygiene (no header/param values in messages)
groups with secrets discipline there rather than as its own rule.
Generated-by: opencode (GLM)
diigo-dump/README was stale three ways: described the pre-DO connect
flow (askpass + DIIGO_COOKIE, both retired), referenced deleted files
(src/webapp.ts), and named the old entrypoint. Rewritten against
current code (DiigoBridge, DO storage state surface, state-aware
/_connect); run/connect/deploy instructions now live only in the
capsite checklist, linked (intra-repo docs link rather than repeat).
webapp/README Run section: port note corrected (two-config run makes
diigo-dump the primary) and pointed at the checklist as the source.
Generated-by: opencode (GLM)
Two prose referrals (preamble + root goal) and the Pointer bullet now
use [the capsite notes](capsite-notes.md) — descriptive, frontloaded,
context-free per the hypertext-links skill; no raw filename visible in
any anchor text. Also wrapped the bib-sync issue URL in the root goal.
Generated-by: opencode (GLM)
Pointers section now uses [descriptive text](url) links with
frontloaded, context-free anchor text (intra-repo relative paths for
local docs; mm-diigo repo link for the provenance collection).
New opencode skill hypertext-links encodes the Prism
docs/research/STYLE.md link rules for reuse here.
Generated-by: opencode (GLM)
ses_eec320fe4ffehaMAn44U1nTKJU verified both real keys, all documented
auth schemes, and two endpoints -> 401 unconditionally, with historical
proof the same creds worked in 2024-2025 (13.5k bookmarks downloaded).
Goal 2 is now 'plugin rides the webapp cookie path', with the cookie-
rotation cost explicit (2-iii); the earlier DECISION POINT framing
('try a real key') contradicted that record and is removed.
Generated-by: opencode (GLM)
Explicit tree semantics in the header: a goal is checked only when all
its children are (leaves: when verified). Architecture sketch moved out
(its content already lives in capsite-notes.md + per-worker READMEs;
duplicates would drift) — the tree's items carry just enough provenance
to stay self-contained. Adds 1b-i/1b-ii mint-loop subgoals and the 2-iii
decision point (API key vs cookie path for the plugin) surfaced this
session; hard rules + run instructions retained.
Generated-by: opencode (GLM)
Now carries the architecture sketch (webapp factory / diigo-dump
consumer / madmode site), the ladder state including what's verified
vs not (DO round trip still to exercise end-to-end), the run/how-to
commands, the hard rules learned this session (assistant never runs
dev servers; secrets discipline; error hygiene; wrangler types),
and pointers into the notes and provenance repos.
Generated-by: opencode (GLM)
webapp/ (new, site-agnostic, independently deployable):
- WebApp: fluent client (pathjoin/query/withHeaders/cookies/get/post)
as RpcTarget; flower-style unions cite NuxiNL/flower v0.11 (Jan 2019);
url/headers private (RpcTarget property reads are RPC); Error messages
carry no header/param values; query-stripped URLs only
- WebAppDo: per-instance authority in DO storage (survives eviction);
one-shot configure(), refuses different-url re-configure
- WebAppFactory: newWebApp(init) -> {id, app}, at(id); ids are bearer
capabilities and are not logged
- HTTP surface: GET / mint form (secret header keys render blank),
POST / mint (form or JSON), POST /at/<id> drive-by-capability
- types via wrangler types (worker-configuration.d.ts, gitignored)
diigo-dump/: now a webapp consumer. The diigo knowledge is only the
mint call: url=interact_api/, UA + x-requested-with, cookie, 500ms gate.
- src/webapp.ts, src/diigo-web.ts deleted (webapp's are canonical);
diigo.ts slimmed to dumpBookmarks + PagedGet
- /_connect is state-aware: connect form pre-mint; health check
(count=1 probe) + re-mint path post-mint; home-page link relabeled
- mint success page: narrow field + copy button (attribute-quoting
fixed; id in x-diigo-web-id header for scripts)
- scripts/diigo-login: askpass flow, marked OBSOLETE (page flow is
the primary); speaks the form protocol; writes only DIIGO_WEB_ID —
the cookie lives in DO storage, never on disk
- package.json: dev = wrangler dev -c wrangler.jsonc -c ../webapp/...
(verified: Ready on :8787, GET / 200, /_connect 200);
wrangler ^4.148.0 devDependency after node_modules was cleaned
TODO(webapp) in webapp/README.md: proxy-page vs primary-worker mint
form; reconsider as the UI grows.
Generated-by: opencode (GLM)
Restore the fluent client from mm-diigo's WebApp.js as RpcTarget classes
(src/webapp.ts): WebApp with pathjoin/query/withHeaders/cookies/get/post,
flower-style unions (NuxiNL/flower v0.11, Jan 2019 — merge only if no
shared key contradicts), pathjoin containment guard; RateLimitedWebApp
shares one gate across all derived handles, enforced at get/post only.
src/diigo.ts ports makeDiigoAPI and the dumpBookmarks generator.
RPC surface: DiigoService (WorkerEntrypoint) with open() handing a
rate-limited stub whose cookie is attached server-side only, and
dumpNdjson. Promise pipelining makes a whole chain a single round trip.
Divergences from the original for the RPC setting:
- url is a private field: a public property on an RpcTarget is readable
by stub holders, and query strings can carry credentials
- Error messages carry no header/param values; URLs have query stripped
(structured details via @endo/errors deferred until node_modules)
- fetch default is fetch.bind(globalThis): a detached fetch throws
'Illegal invocation' in workerd (found live, fixed, verified 200)
- dumpPage/open log failures via console.error for observability
README documents config, RPC use, and each divergence.
Generated-by: opencode (GLM)
fixup: scrub failed API-key ping from checklist
diigo-ping never authenticated (Diigo 401); the working path is the
session-cookie dump worker. Renumber steps, note the scrub with the
reason, and drop the ping how-to.
Generated-by: opencode (GLM)
Documents the cookie-path worker: endpoint and headers mirrored from
the mm-diigo original, DIIGO_COOKIE secret setup (local .dev.vars vs
wrangler secret put), the inert-GET / POST-only-submit behavior, and
the manual test result (200, total 1674, 2026-10-06) with pointers to
the checklist and capsite notes.
Generated-by: opencode (GLM)
Port of mm-diigo/projects/diigo-bak/diigo-dump.mjs as a Cloudflare Worker:
one page-number field + button, GET interact_api/load_user_items
(sort=updated, count=24) with DIIGO_COOKIE as a Worker secret.
GET / is inert; dumps happen only on POST submit.
Manual integration testing (local wrangler dev, port 8788, 2026-10-06)
shows it works: status 200, total 1674 bookmarks, page-1 items returned.
The browser-session-cookie path authenticates; the static API-key path
(diigo-ping worker, EmDash plugin) is still rejected by Diigo (401).
Also: project checklist (capsite-checklist.md) with step results, and a
.gitignore for local artifacts (.dev.vars, node_modules, wrangler state).
Generated-by: opencode (GLM)