Conversation
PLAN.md captures the agreed roadmap: philo-optic as a single-flight camera daemon (Philo's "eyes"), orchestration staying in TS, Telegram now / Matrix as Phase 3 C2, and a later cadence-in-Go milestone. Documents the WIP compile breakage on this branch and the warn+copy-forward capture policy (fill a gap with the previous frame up to 5 times, then abort). SOUL.md grounds Philo's persona and architecture: agent-vs-eyes split, no sleep (that is Phedora), astronomical rhythm, single-flight zero-interference, and the real feature set summarized from FEATURES.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finish the seams the atomic-folder refactor left dangling so the branch builds,
lints clean, and tests pass:
- Director.photo uses stateless camera.photo(); director.timelapse() creates the
atomic event folder + its public repo and returns { events, repo }. Removes the
{} as Repo placeholder in scheduleSunset and the deleted setupPrivateRepo/
repoTimelapse calls in Producer; restores cancelSunset().
- Drop the half-baked source/ subfolder: frames and the rendered mp4 live in the
one event folder (Repo.upload / makeTimelapsePage / ffmpeg expect them together).
- Frame naming: .jpg is now added in the Timelapse layer (previously supplied by
the old stateful Camera.filename); the file event emits the bare filename.
SOUL capture policy (warn, never abort on a hiccup): a failed capture is filled
by copying the previous good frame so the numbered sequence stays gap-free (ffmpeg
truncates at a real hole) and the rhythm continues. Up to 5 copies in a row are
tolerated; a real capture resets the streak; the 6th consecutive failure ends the
run. Frame 1 retries in place. timelapse-frames.test.ts rewritten accordingly and
switched to real tiny intervals (fake timers were fragile with the drift loop).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stakeholder.getMissingFrames/fixFrames does retroactively what the live loop now does: fill holes by copying the previous frame. Adds a plan section to repair the last year of dropped-frame holes and re-render complete videos, incl. resolving the 540-vs-541 expected-count discrepancy and wiring a real fix command. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
541 is the total file count of a complete event folder (540 frames + README.md), not the max frame number. Documents the resulting off-by-one in Stakeholder.getMissingFrames (iterating to 541 flags a nonexistent prefix-541.jpg): the file-count check uses 541 but frame iteration must use 540. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the vision: each day pull last year's event repo from GitHub, repair holes with Stakeholder.fixFrames if needed, re-render locally, post to Telegram as a memory, then delete the download. Reuses downloadBackups/fixFrames/stitchImages/ redraft. Notes inferring the frame count from the highest frame present (robust across eras), and the open choice of healing GitHub permanently vs ephemeral. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correct the Phase 0 model: daily/event repos are PRIVATE backups, not public. director.timelapse() now uses setupBackupRepo (private); setupPublicRepo is kept for the future publish flow. Per the vision, repos go public on demand only after Telegram moderation, via a publish button -> GitHub Pages. PLAN: add the "full publishing" epic (private -> moderate -> publish button -> Pages), higher-quality published renders, and using GitHub Actions compute for the public render instead of the Pi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fill the empty Go skeleton with a runnable, tested camera daemon (stdlib only,
zero external deps):
- main.go: HTTP server on PORT (default 8080), graceful SIGINT/SIGTERM shutdown,
slog logging. GET /frame?maxAgeMs=&timeoutMs= returns JPEG bytes + X-Frame-Id/
X-Captured-At/X-Attempts headers; GET /health returns {status,backend}.
- camera/service.go: hand-rolled single-flight. Concurrent callers share ONE
capture (one shutter, shared photons); maxAge reuses the cached last frame;
capture retries until a non-empty image lands or timeout. Replaces the drafted
semaphore select. captureOnce recovers from a panicking backend (stoic heron).
- camera/capturer.go: hexagonal Capturer interface with RpicamCapturer (rpicam-
still -> temp file -> non-empty guard -> bytes, same guard as the TS fix) and
MockCapturer (synthetic time-varying JPEG for PHILO_MODE=mock / CI).
- camera/service_test.go: force-fresh, cache reuse, concurrent coalescing (20
callers -> 1 shutter), retry-until-success, timeout.
- Makefile: keep arm64 cross-compile; add mock/test/vet/fmt targets.
Verified via mock server (valid JPEG, force-fresh vs cache) and go test.
Not yet run on the Pi against real rpicam.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ery epic Throwback repair: push repaired frames back to the private backup repo so holes are permanently healed (local clone still deleted). Fill every hole from the nearest neighbour - previous frame for interior/trailing gaps, next frame copied backwards for a leading gap (missing frame 1), incl. multiple in a row. So any run with >=1 frame heals fully; extend Stakeholder.fixFrames (currently previous-only) to fall back to the next frame. Live loop still retries frame 1 in place. Add Inventory & Gallery epic: one source of truth (state, health, backup provenance) and a public gallery generated from published entries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TS Camera no longer touches hardware: photo() POSTs to philo-optic and
writes the returned JPEG bytes to the output path. Orchestration stays in TS.
- Camera.ts: HTTP client (POST /frame?maxAgeMs=0&timeoutMs=), throws on non-ok
status or empty body (same contract, so Timelapse copy-forward is unchanged).
#mutex removed; the daemon serialises the device now. busy just tracks the
in-flight request for Timelapse.stop. Config: PHILO_OPTIC_URL,
PHILO_OPTIC_TIMEOUT_MS.
- libcamera-still.ts: extracted buildStillArgs() as the single source of truth
for rpicam args (--nopreview + defaults, minus --output); StillCamera reuses
it. Camera forwards buildStillArgs(this.options) so presets/ROI aren't lost.
Daemon made options-aware without duplicating arg logic:
- Capturer.Capture(ctx, args); RpicamCapturer runs the supplied args + --output
(falls back to a default snapshot when args are empty, e.g. a bare GET).
- service.go: single-flight keyed by args (different presets don't share a
frame) + a global device lock so only one physical shutter runs at a time.
- main.go: POST /frame reads {"args":[...]} from the body; GET still works.
- service_test.go: added different-args-capture-separately; existing tests
updated for the new signatures.
Verified: go build/vet/test, npm build/lint/test (12/12), and end-to-end mock
(TS Camera.photo -> daemon -> valid JPEG written). Not yet run on the Pi.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inline /publications handler in photoStage.ts duplicated Stakeholder's "list unpublished" logic (and used the raw pub.name as the folder instead of the type-dated name). It now delegates to Stakeholder.checkPublications() as the single source of truth. Drops the now-unused FileSystem import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the hours-long sleep before golden hour, #time is stale (from scheduling - possibly yesterday when the run was scheduled "for tomorrow"), so the session folder would be dated wrong. Refresh the time after waking, before building the event folder name. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ring
Daemon-down is now CRITICAL and unmistakable: Camera throws
CameraUnreachableError (naming URL, cause like ECONNREFUSED/timeout, and a
systemctl hint) when philo-optic never answers; Timelapse aborts the run
immediately on it - copy-forward healing must not fabricate a timeline while
nothing is observed. A daemon-reported capture failure (503/empty frame) stays a
plain recoverable error healed by copy-forward.
StillCamera (local rpicam spawner) is removed - no local fallback by decision, a
second capture path would bypass the device lock; buildStillArgs remains as the
single source of truth for capture args.
Camera.photo gains { maxAgeMs } (default 0 = fresh frame; positive lets a preview
reuse the daemon's cached frame).
New off-Pi tests: camera.test.ts (HTTP client contract incl. unreachable vs 503),
libcamera-still.test.ts rewritten for buildStillArgs, timelapse 'down' scenario
(abort, zero copies), Go main_test.go (HTTP layer: frame GET/POST, bad JSON,
blind 503, invalid query defaults, health). TS 24/24 green, go vet + test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan revision: when the philo-optic daemon is unreachable, Camera now logs it loudly and falls back to spawning rpicam-still locally, so the timeline continues even if the daemon dies mid-run. Fallback captures are serialised in Camera (#fallbackQueue) because degraded mode has no daemon device lock. Long-term removal of the fallback stays an open decision. CameraUnreachableError now means BOTH paths failed (daemon down AND local capture failed) and names both causes; Timelapse still treats it as critical and aborts without copy-forward. A daemon-reported 503/empty frame stays the recoverable blind-camera case. Tests: camera.test.ts exercises the real fallback via a mocked StillCamera (incl. serialisation of concurrent fallback captures); libcamera-still.test.ts restores the StillCamera output-file-guard suite alongside the buildStillArgs mapping. TS 29/29 green, lint 0, build 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EYES_GRACEFUL_DEGRADATION_PHILO_OPTIC_UNAVAILABLE_STILLCAMERA (default true) now controls whether Camera may fall back to a local rpicam-still capture when philo-optic is unreachable. Set to 'false' to make daemon-down always critical (CameraUnreachableError) without attempting the fallback at all - useful while the fallback path itself is still earning trust. Read per-call rather than cached at module load, so it's live-toggleable and simple to test. Documented in .env.example alongside PHILO_OPTIC_URL/PHILO_OPTIC_TIMEOUT_MS (previously undocumented since Phase 2). Tests cover toggle on (default) and off. TS 31/31 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mantra over the plan: the strangler path IS the tabula rasa, in an order that never costs a sunset (rewrite rejected as method, allowed as end state - guiding decisions 8+9). - Asymmetric fallback policy decided: prod toggles the local rpicam fallback ON, staging/clones OFF (fallbackQueue is in-process only). - Phase 3 reframed as clones: staging-first validates Phases 1-2 on-Pi (>=3 green sunsets incl. induced daemon restart before prod switch), hardware relief (media services -> Pholiere) as precondition, GitOps reconciler gets capture-window guard + rollback, Matrix C2 unblocked via LAN-only Dendrite as the human-steering surface. - Staging: no GitHub uploads (feature toggle); on-demand cloud-sky photo noted as anytime test path. Build-on-Pi vs CI still open. - Mass-repair dry-run designated first real divide_and_conquer ticket (cheap failure, mechanically verifiable, real value). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.