21 KiB
Relay log — issue-62-authstorage
Append-only. One concise entry per leg. Do not read end-to-end for orientation;
use status.md. Targeted lookups only.
Leg 0 — Planning (assessment + relay packet)
Did:
- Read issue #62 and confirmed the crash: static
import { AuthStorage }fails at ESM load with Pi 0.80.8+. - Investigated all
AuthStorage/ModelRegistryusage insrc/(3 production files + 5 test/support files undersrc/server/sessions/; other pi-coding-agent imports unaffected). - Downloaded and diffed real npm tarballs (pi-coding-agent 0.80.7/0.80.8/0.80.10
and pi-ai 0.80.7/0.80.8) into
/srv/dev/pi-inspect(scratch, outside repo) to establish the exact new export surface:AuthStorageand its backends removed from exports; newModelRuntime(async) +readStoredCredential; changedModelRegistry(constructed from a runtime,refresh()now async, noauthStorage); pi-aiCredentialStore/InMemoryCredentialStore/AuthInteractionmodel. Confirmed 0.80.8 and 0.80.10.d.tsare identical for the affected files (stable target). - Wrote
ASSESSMENT-issue-62.md(root).
Decisions:
- Clean migration to
ModelRuntime, no dual-version compat shim. Rationale: sync→async, credential-store contract change, OAuth callback contract change, and session-services option change span both surfaces with no small clean adapter; 0.80.0–0.80.7 is already broken/superseded (latest= 0.80.10). - Dep range fix: peerDeps
>=0.80.0 <1→>=0.80.8 <0.81for all three@earendil-works/*packages; upper bound<0.81because this line ships breaking changes within0.80.x. - Relay packet placed under
relays/issue-62-authstorage/(committed; not inpackage.jsonfiles, so not published;.pi-web/is gitignored so not used).
Artifacts changed: ASSESSMENT-issue-62.md;
relays/issue-62-authstorage/{charter,status,log}.md.
Status update: last completed leg 0, next leg 1 = charter slice 0 (Bootstrap: install Pi 0.80.8+, correct dep ranges).
Blockers: none. Noted /tmp disk-quota issue (install in worktree) and the
pending sessiond restart for later daemon-path slices.
Handoff: Planning only — NOT auto-spawning the first implementation leg. Assessment + relay plan are laid out ready to be kicked off by the user.
Leg 1 — Slice 0 Bootstrap (deps + range correction)
Did:
- Corrected
package.json: three@earendil-works/*devDependencies^0.80.6→^0.80.8; peerDependencies>=0.80.0 <1→>=0.80.8 <0.81. npm installin the worktree. The default/tmp-based node-gyp build ofnode-ptyfailed with "Disk quota exceeded" (/tmp is a 5.8G tmpfs at ~81%). Re-ran withTMPDIR="$PWD/.tmp-build" npm install, which succeeded (618 packages, 0 vulnerabilities). Removed.tmp-buildafter.- Confirmed installed versions: all three packages resolve to 0.80.10 (lockfile updated).
- Verified the new export surface via a node ESM check:
ModelRuntime✓,readStoredCredential✓,ModelRegistry✓,AuthStorageabsent ✓, pi-aiInMemoryCredentialStore✓. - Ran
npx tsc --noEmit: 24 errors, all insrc/server/sessions/at the expected migration sites (removedAuthStorage,ModelRegistry.create/inMemory,authStorageon session-services options,PiAgentSession.modelRegistry). This matches assessment §2/§3 — the new surface resolves; only Pi Web's old usage is broken.
Decisions:
- Committed with
git commit --no-verify: the pre-commit hook runs a whole-project typecheck which cannot pass until the migration lands. Charter explicitly permits non-verify-green legs. Documented this + theTMPDIRworkaround instatus.mdso future legs don't rediscover them. - Did not touch any
src/migration code — kept strictly to slice 0 scope.
Artifacts changed: package.json, package-lock.json (commit 0fa9d0e);
relays/issue-62-authstorage/{status,log}.md. node_modules/ installed
(gitignored, not committed).
Status update: last completed leg 1, next leg 2 = charter slice 1
(authService.ts core migration → propagate async construction to sessiond.ts).
Blockers: none. Sessiond restart still pending (becomes relevant from leg 2
onward once sessiond.ts changes land).
Handoff: spawning leg 2 (slice 1).
Leg 2 — slice 1: authService.ts core migration (commit e37148c)
What I did: migrated src/server/sessions/authService.ts off the removed
AuthStorage / ModelRegistry.create surface onto the async ModelRuntime
API, and propagated the async construction into the session daemon.
authService.ts:- Imports
ModelRuntimefrom@earendil-works/pi-coding-agentandAuthInteraction(type) from@earendil-works/pi-ai. DroppedAuthStorage/ModelRegistry. createModelRegistryForAgentDir→createModelRuntimeForAgentDir(agentDir)returning `ModelRuntime.create({ authPath: /auth.json, modelsPath: /models.json })`.- Construction is now async: private constructor + static
AuthService.create({ agentDir? | runtime? | authFlows? }).runtimedep replaces the oldmodelRegistrydep; no-agentDir fallback isModelRuntime.create({}). - Public field
readonly runtime: ModelRuntimereplacesmodelRegistry. saveApiKey→runtime.login(providerId, "api_key", interaction)whereinteractionis a non-interactiveAuthInteraction(prompt: async () => key,notify: () => {}). Verified against pi-aienvApiKeyAuth().login, which callsinteraction.prompt({ type: "secret" })and persists the returned{ type:"api_key", key }throughcredentials.modifyinsideModels.login. This is the credential-persistence path the assessment (§5.1) called for.logoutProvider→await runtime.logout(providerId).refreshAuthState→await runtime.refresh()(no moreauthStorage.reload()— the file store is re-read by the runtime). Now async.authProvidersandrequireOAuthLoginProviderbecame async, awaitingruntime.refresh()and the now-asyncgetLogin/LogoutProviderOptions.startOAuthLoginpassesruntime: this.runtimeintoOAuthLoginFlowService.start(slice 3 will consume it viaruntime.login).
- Imports
sessiond.ts:createRuntime()is nowasync;new AuthService(...)→await AuthService.create({ agentDir });PiSessionServicenow receivesmodelRuntime: auth.runtimeinstead ofmodelRegistry: auth.modelRegistry.sessiond/sessionDaemonStartup.ts:createRuntimemay now returnRuntime | Promise<Runtime>andrunSessionDaemonStartupawaits it. The existing sync test doubles still satisfy the widened type.
Decisions:
- saveApiKey via
runtime.login("api_key", …)rather than reaching for a rawCredentialStore.modify: the pi-aiCredentialStoreis not exposed offModelRuntimepublicly, and the provider's own api-keyloginis the intended persistence entry point (it writes throughcredentials.modify). Feeding the key back through a non-interactiveAuthInteraction.promptkeeps us on the supported public surface. This matches assessment §5.1's "credential persistence via the pi-ai CredentialStore.modify path" without depending on unexported internals. - Kept
AuthServiceconstruction async via a static factory (private ctor) rather than aninit()method — cleaner async boundary, single valid construction path (code-quality-architecture skill). - Did NOT touch
authProviderOptions.ts,oauthLoginFlowService.ts,piSessionService.ts, or any test/support files — strictly slice 1 scope. The async call sites I introduced (await getLoginProviderOptions(...),runtime:inauthFlows.start,modelRuntime:in PiSessionService deps) deliberately point at the interfaces slices 2–4 will expose.
Typecheck state: npx tsc --noEmit = 31 errors (was 24 at slice 0). The
increase is expected and honest: the migrated authService now calls
runtime-based interfaces that slices 2/3/4 have not migrated yet. All remaining
authService.ts / sessiond.ts errors are cross-slice (authProviderOptions
shape → slice 2; OAuthLoginFlowService.start runtime param → slice 3;
PiSessionServiceDependencies.modelRuntime → slice 4). Test/support files
(slice 5) still import the removed AuthStorage.
Artifacts changed: src/server/sessions/authService.ts,
src/server/sessiond.ts, src/server/sessiond/sessionDaemonStartup.ts
(commit e37148c); relays/issue-62-authstorage/{status,log}.md.
Status update: last completed leg 2, next leg 3 = charter slice 2
(authProviderOptions.ts migration).
Blockers: none. Sessiond restart now ACTIVE-pending — slice 1 changed
sessiond.ts and the daemon auth construction path; the human must manually
restart the sessiond service once the migration lands (noted in status.md).
Also: human confirmed /tmp is usable again, so the TMPDIR install
workaround is no longer required (status.md updated).
Handoff: spawning leg 3 (slice 2).
Leg 3 — slice 2: authProviderOptions.ts migration (commit d09d7cc)
Did: Migrated src/server/sessions/authProviderOptions.ts off the removed
authStorage-centric surface onto the new ModelRuntime API.
- Replaced the
AuthProviderModelRegistrystructural interface (which requiredauthStorage.getOAuthProviders()/list()/get(),getAll(),getProviderDisplayName()) with a runtime-shapedAuthProviderRuntimeinterface exposinggetProviders()({ id, name, auth: { apiKey?, oauth? } }),listCredentials()(Promise<{ providerId, type }[]>), andgetProviderAuthStatus(id). Kept it structural (notPick<ModelRuntime,...>) so the test can supply a lightweight double; verified the realModelRuntimesatisfies it (call sites inauthService.tstypecheck clean). - Made
getLoginProviderOptions/getLogoutProviderOptionsasyncto match theawaitcall sites already present inauthService.ts(leg 2). - Login options: OAuth options from providers with
auth.oauth; api-key options from providers withauth.apiKeyfiltered through the unchangedOAUTH_ONLY_PROVIDERS/isApiKeyLoginProviderlogic. Display names now come fromProvider.name(replacinggetProviderDisplayName). Logout options derived fromlistCredentials(), mapping provider id -> name viagetProviders(). - Rewrote the
authProviderOptions.test.tsdouble to the runtime shape (agetProvidersarray with per-providerauth, alistCredentialspromise,getProviderAuthStatus); made the two option-building tests async. All 3 tests pass.
Decisions: AuthProviderInfo.auth typed as { apiKey?: unknown; oauth?: unknown } — presence is all this module needs, and unknown keeps the double
trivial while remaining assignable-from the real ProviderAuth. Structural
interface (not Pick<ModelRuntime>) chosen for testability per
code-quality-architecture (injectable seam, no SDK construction in unit test).
Verify state: npx tsc --noEmit 31 -> 28 errors. No authProviderOptions
errors; getLogin/LogoutProviderOptions call sites in authService.ts clean.
Remaining 28 are cross-slice: authService.ts line-83 OAuthLoginFlowService. start still expects authStorage (slice 3); sessiond.ts(1)+piSessionService. ts(6) slice 4; test/support files slice 5.
Artifacts: src/server/sessions/authProviderOptions.ts,
src/server/sessions/authProviderOptions.test.ts; status.md updated; committed
d09d7cc with --no-verify (migration not yet verify-green, permitted).
Handoff: spawning leg 4 (slice 3, oauthLoginFlowService.ts). Sessiond restart from leg 2 still pending — carried forward, not cleared.
Leg 4 — slice 3: oauthLoginFlowService.ts migration (commit 1c3d6db)
What: Reimplemented OAuthLoginFlowService against the pi-ai
AuthInteraction contract and rewrote its test.
start()now takesruntime: Pick<ModelRuntime, "login">instead ofauthStorage: Pick<AuthStorage, "login">; login driven viaruntime.login(providerId, "oauth", interaction). Resolves theauthService.tsline-83 tsc error (authService.ts now at 0 errors).- Built a single
AuthInteractionadapter ({ signal, prompt, notify }) replacing the sixOAuthLoginCallbacks(onAuth/onDeviceCode/onPrompt/onManualCodeInput/onSelect/onProgress). - Mapping decisions (verified carefully — riskiest slice):
prompt(AuthPrompt)dispatches ontype:select→waitForSelect(options{id,label,description?}→ CommandOption{value:id,label}, resolves chosen id);manual_code→ web-UI prompt kindmanual;text/secret→ web-UI prompt kindprompt. Old code special-casedonManualCodeInputwith a hardcoded message; now the provider supplies themanual_codemessage, which is more correct.notify(AuthEvent):auth_url→auth:{url,instructions?};device_code→ reuseauthfield (url: verificationUri, instructions"Enter code: <userCode>") exactly as the oldonDeviceCodedid;info+progress→ appendmessagetoprogress(old code only hadonProgress;infofolds in naturally).- Old
OAuthPrompt.allowEmpty/placeholderhandling: the newAuthPrompthas noallowEmpty, so interactive prompts are always required (allowEmpty:false);selectkeepsallowEmpty:true. Placeholder still forwarded when present.
- New behavior: per-prompt
AuthPrompt.signalnow aborts just that pending request (rejects"Prompt cancelled", clears the interaction from state) without ending the overall flow — the documentedmanual_code-vs-callback race. AddedbindPromptSignal+ a dedicated test for it. - Test: rewrote
oauthLoginFlowService.test.tswith afakeRuntimelogindouble (returns a stub oauth credential). Replaced the old device-code-via-onDeviceCode coverage with an explicitnotifydevice_code test and a per-prompt-signal-abort test. 9 tests pass; both files lint clean.
tsc: 28 → 26 errors. authService.ts = 0. Remaining: slice 4
(sessiond.ts 1, piSessionService.ts 6) and slice 5 test/support files
(authService.test.ts 10, .testSupport.ts 3, .promptQueue.test.ts 2,
.warnings.test.ts 4).
Status: updated (current position, leg tracking → last leg 4 / next leg 5,
next task = slice 4). Committed with --no-verify (migration not yet
verify-green, per charter).
Blockers: none. Sessiond-restart-pending note still ACTIVE (unchanged; this slice did not touch the daemon path, but slice 1 did). Handing off to leg 5 (slice 4).
Leg 5 — slice 4: piSessionService.ts migration (commit 4ccd4f8)
What: Migrated src/server/sessions/piSessionService.ts to the new
ModelRuntime API.
createDefaultRuntimeFactorynow takes aModelRuntimeand passesmodelRuntimetocreateAgentSessionServices({ cwd, agentDir, modelRuntime })(dropped theauthStorage+modelRegistryargs).PiAgentSession.modelRegistry: ModelRegistryInstance→modelRuntime: ModelRuntime. Removed theModelRegistryInstancetype alias and theAuthStorage/ModelRegistrySDK imports; addedtype ModelRuntime+readStoredCredentialimports andjoinfrom node:path.authService.jsimport reduced to justAuthChange(droppedcreateModelRegistryForAgentDir).anthropicSubscriptionWarning(session, authPath?): reads viareadStoredCredential("anthropic", authPath); param narrowed toPick<PiAgentSession, "model" | "settingsManager">.warningsForSessionpassesjoin(this.agentDir, "auth.json").- Model reads rederived onto the runtime:
availableModels/setModel→await modelRuntime.refresh()+getAvailableSnapshot()+getModel(...);syncCurrentModelAuthWarning→getModel(...)+hasConfiguredAuth(providerId). applyAuthChangeno longer refreshes a registry (shared runtime is refreshed by AuthService before emit; all sessions share it), keeping the subscribe callback synchronous.
Decision: made modelRuntime a required PiSessionServiceDependencies
field rather than keeping an optional modelRegistry?-style fallback. The old
fallback built a registry synchronously in the constructor; ModelRuntime can
only be created by the async ModelRuntime.create, which cannot run in a
constructor. sessiond.ts already injects modelRuntime: auth.runtime (slice
1), so production wiring is unaffected. Consequence: the slice-5 test surface is
wider than the four files the assessment listed — every new PiSessionService(...) in tests now needs modelRuntime, and fakeRuntime/the
TestSession type in testSupport.ts must expose modelRuntime. Documented in
status.md "Next task".
Result: npx tsc --noEmit — sessiond.ts and piSessionService.ts at 0
errors; all production code migrated (tsc output filtered to non-test/support
files is empty). Remaining errors are slice-5 test/support only:
authService.test (10), testSupport (4), warnings (5), promptQueue (17),
lifecycle (19), archiveCleanup (9), spawnSession (3), spawnSubsession (18),
sessionRoutes (1). piSessionService.ts lints clean. Committed --no-verify
(migration not yet verify-green, per charter).
Blockers: none. Sessiond-restart-pending note still ACTIVE — this slice
added piSessionService.ts (a session-daemon path) to the pending-restart
surface; do not clear the note. Handing off to leg 6 (slice 5: tests +
testSupport).
Leg 6 — slice 5 (tests + testSupport migration) — commit d0cc55c
What: Migrated all test doubles + piSessionService.testSupport.ts off the
removed AuthStorage.inMemory / ModelRegistry.create|inMemory surface to the
pi-ai InMemoryCredentialStore + async ModelRuntime.create({ credentials }).
npm run verify is now fully green (typecheck + lint + knip + 1390 tests,
2 skipped) — the migration goal (charter criteria 1, 2, 5) is met; only the
changeset (criterion 4) remains for slice 6.
Files changed (all src/server/sessions/):
piSessionService.testSupport.ts: new seamscreateTestModelRuntime, sharedtestModelRuntime(top-level await),seedCredential;fakeRuntimeandtestModelmoved ontomodelRuntime; dropped AuthStorage/ModelRegistry.archiveCleanup/lifecycle/promptQueue/spawnSession/spawnSubsession/sessionRoutestests: injectedmodelRuntime: testModelRuntimeinto everynew PiSessionService(...)(now required) + imported the shared runtime.promptQueue.test.tsauth-loss test: liveInMemoryCredentialStore+createTestModelRuntime(credentials), driving changes throughdelete/seedCredential+refresh()+applyAuthChange(...).warnings.test.ts:anthropicSubscriptionWarningseam is now a tempauth.jsonread viareadStoredCredential(id, authPath); type narrowed.authService.test.ts: reworked to asyncAuthService.create+InMemoryCredentialStore; awaits async ops; OAuth-complete usesvi.waitFor.
Decisions:
- Used a single shared
testModelRuntime(top-levelawaitin testSupport, an allowed ESM pattern here) for the no-auth catalog case so the manyPiSessionServiceconstructions andfakeRuntimesessions inject it synchronously — avoided makingfakeRuntimeitself async (which would have rippled through ~90 call sites). Auth-dependent tests build a dedicated per-test runtime viacreateTestModelRuntime(credentials). anthropicSubscriptionWarningseam: chose the on-disk tempauth.json+readStoredCredentialpath (matches production exactly) rather than adding a new injectable credential-read seam. Clean; no intervention needed.- Made
getLoginProviderOptionssynchronous (it did no async work) to satisfyrequire-await; de-awaited its 2 call sites inauthService.tsand the test. Also fixed pre-existing lint debt from earlier slices surfaced now that lint ran green for the first time:authRoutes.tsreturn-await,authService.tsapi-key interaction (() => Promise.resolve(key)/notify: () => undefined). - Verified SDK behavior empirically with throwaway probe scripts (removed)
before writing doubles: providers/models catalog, api-key login persistence,
getModel,hasConfiguredAuthacross store mutations + refresh, andreadStoredCredentialagainst a temp auth.json.
Verification: npm run verify green. Pre-commit hook (whole-project
typecheck + knip + eslint + related vitest) passed — committed normally
(no --no-verify needed since the tree is verify-green).
Blockers: none. Sessiond-restart-pending note still ACTIVE — unchanged this leg but slices 1 + 4 touched session-daemon paths; only the human clears it after restarting the sessiond service. Handing off to leg 7 (slice 6: changeset + final verify + cleanup; the finish line — no PR).