Repository navigation
Conversation
|
This PR looks like the top of a two-PR dependency stack (depends on #358):
Holding off on automated review/merge here — this needs a human (or the relevant dev agent) to assess the whole stack together, not a per-PR pass, and both PRs are drafts. Nothing further is needed from the author; this comment exists so the dependency is recorded on the PR rather than in a run log. |
79b6c1f to
1ddd908
Compare
|
Heads up: |
53f367f to
5a7d90f
Compare
Resolutions: - productions.ts: keep Eyevinn#359's recording-doc helpers; main moved the guest invite sweep to services/guest-sweep.ts. Activation sets activationSucceeded before main's controller re-init. - production-lifecycle.test.ts: keep both sets of tests. - flow-generator-encoders.test.ts: Strom mock lists builtin.audioenc for Eyevinn#358's block check, and the recorder is fed on video_in_0 (Eyevinn#358). - record-inputs-lifecycle.test.ts: mock main's reinitConnectedControllers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main through Eyevinn#359. productions.ts: activation sets activationSucceeded before main's controller re-init. flow-generator-encoders.test.ts: the Strom mock lists builtin.audioenc for the block check. record-inputs-lifecycle.test.ts: mock main's reinitConnectedControllers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main through Eyevinn#359. productions.ts: activation sets activationSucceeded before main's controller re-init. flow-generator-encoders.test.ts: the Strom mock lists builtin.audioenc for the block check. record-inputs-lifecycle.test.ts: mock main's reinitConnectedControllers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9d1924c to
0016343
Compare
|
Claude (Claude Code), for @wagenet. Pushed New test in |
|
Claude (Claude Code), for @wagenet. Landing order agreed with @wagenet: #404 and #407 merge first, unchanged. This PR then rebases onto |
The recorder's output_dir was recordings/<production id> for every activation, and deactivate uploads everything in it. A file left there after its upload (its delete from Strom failed, or was skipped because the production changed mid-upload) was uploaded again at the next deactivate and registered as a new RecordingDoc. Give each activation its own subdirectory, store it on the production at activate, and read it at deactivate (falling back to the shared directory for productions activated before this change). Derive RecordingDoc ids from the object key so a retried deactivate conflicts instead of registering the same object twice. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A per-activation directory is forgotten once its deactivate clears recorderOutputDir, so a session whose upload or listing failed was never uploaded. Deactivate now lists every activation directory under recordings/<production id> and uploads each file whose RecordingDoc does not exist yet. The directory name carries the activation start, which becomes the RecordingDoc's startedAt; endedAt is the file's last write. The idle watchdog, startup reconcile and activation-failure reset now clear recorderBlockId and recorderOutputDir, and activation always writes both. Before, a later activation without a recorder kept the stale block id, and its deactivate split and uploaded the earlier session. The sweep keeps delete-after-upload: once the isStillRecording guard passes, it deletes each file that is in object storage, including files skipped as already registered, and removes each directory that leaves empty. Strom's media listing marks directories with is_directory, not is_dir. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Once uploadFiles sets abortedOnAuthError, uploadProductionRecordings skips the remaining activation directories and the legacy shared directory: the store would reject them the same way, each after a full download from Strom. Files already in object storage are still deleted from Strom, as on main. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
f9bef78 to
38cc1d9
Compare
|
Claude (Claude Code), for @wagenet. Unstacked from #358: the branch is now |
The sweep skipped any file whose RecordingDoc existed. A file registered while the recorder was still writing it left a truncated object, and if its delete from Strom then failed, the next sweep skipped the complete file and deleted it. Compare the registered size with the size Strom lists, upload again on a mismatch, and update the existing RecordingDoc's size and end time instead of leaving the stale ones. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude (Claude Code), for @wagenet. Pushed |
… recording A re-uploaded file's RecordingDoc was refreshed with the deactivate time as its end time, where every other RecordingDoc takes the file's last write. Use the file's last write, falling back to the deactivate time. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude (Claude Code), for @wagenet. Pushed a one-line follow-up: a re-uploaded file's |
|
This PR is part of wagenet's multi-PR recording feature line on Members, in dependency order:
Nothing further is needed from the author; this comment records the dependency on the PRs themselves rather than in a run log. |
birme
left a comment
There was a problem hiding this comment.
pr-reviewer — APPROVE. Read-only review of 063c1b7.
Prior findings
| # | Source | Finding | Status |
|---|---|---|---|
| 1 | stack-flag (birme, 2026-09-28 / 2026-10-12) |
Hold for holistic stack review | DROPPED — procedural hold, not a code finding; stack since unstacked, this PR merges independently |
| 2 | branch-blocked reason:conflict (birme, 2026-09-29) |
Real merge conflict with main |
FIXED — branch rebased/merged; a scratch merge onto current main applies cleanly here |
| — | No gh ... /reviews or /comments entries |
— | none |
No OPEN findings. No Closes/Fixes #N directive in the body, so nothing auto-closes.
Verified against real source (head)
- Strom
MediaEntryfields are real, not fabricated.Eyevinn/stromtypes/src/api.rsdefinesMediaFileEntry { is_directory: bool, modified: u64 /* UNIX epoch seconds */ }. The PR'sis_dir→is_directoryandmodified: string→modified: number(consumed asentry.modified * 1000ms) match it. The oldis_dirwas genuinely wrong (directories were never filtered out). - "Upload each recording once" holds.
RecordingDoc._idis now deterministic (recording-<sha256(bucket/key)[:32]>). The sweep skips a key whose doc exists and whosesizeBytesmatches (isRecordingRegistered); a size mismatch (file still being written when first registered) re-uploads andrefreshRegisteredRecordingupdates size+end time; concurrent/duplicate inserts hit a CouchDB conflict and are folded into the refresh path rather than inserting a second doc. Object keys are unchanged, so re-upload overwrites rather than duplicates. - Failed + orphaned sessions are swept at the next deactivate:
uploadProductionRecordingslists every activation directory underrecordings/<prod>, 404 ⇒ nothing to upload, dates each file by its activation-dir name (startedAt) and Strommodified(endedAt). The deactivate guard no longer requiresrecorderBlockId, so an idle-watchdog / reconcile / lost-flow session still gets uploaded. - No double/missed-upload: delete-from-Strom (#378) covers uploaded and already-registered files so a leftover whose delete failed is removed next time; failed uploads are never deleted and retry;
isStillRecordingguard leaves everything in place on reactivation;S3AuthErroraborts the whole production sweep (one wasted download, not one per dir). Recorder fields are now cleared by watchdog, reconcile and activation-failure reset, and always rewritten on activate. - CI
build-and-test= pass (covers typecheck + build + vitest; repo has no lint script). Co-author trailers present on all 5 commits; no secrets.
Tests
recording-reupload.test.ts covers the load-bearing cases: skip-already-registered, size-match skip, size-grew re-upload+relist, failed earlier activation picked up and dated by its own start, orphaned/legacy leftover with no current recorder, retried deactivate uploads nothing again, delete-including-registered, still-recording guard, cross-directory auth abort, never-recorded 404. production-lifecycle.test.ts pins watchdog+reconcile field clearing. recording-wiring.test.ts pins per-activation output_dir.
Non-blocking notes
recording-wiring.test.tslines ~326–376 still useis_dir: falsefixtures; they pass only because!e.is_directoryis!undefined→ truthy. Harmless (all are files), but the fixtures no longer match the realMediaEntryshape — worth aligning.- Transitional-only: a legacy shared-dir file previously registered under the old random-uuid id and whose Strom delete failed will be re-registered under the new deterministic id (one duplicate) on the first new-code deactivate. Strictly better than
main(which duplicated on every deactivate) and within the author's documented known-limits. Not blocking.
Merge-ordering
main moved ahead at 14:45 today with #180 (748d53b, "take a preview PiP to program from a macro"), after this branch's last main-merge (12:50). So git diff main..HEAD shows #180's controller.ts / ws-macro-pip.test.ts as "removed" — this is staleness, not a mismerge: #180 is not an ancestor of the head, and merging current main restores that code cleanly. This PR's own 5 commits touch only recording code.
#358 / #359 shared test file: both edit src/__tests__/recording-wiring.test.ts. This PR asserts main's video_in pad + per-activation output_dir; #358 changes the pad name and filename_prefix in the adjacent hunk. Merging #358 after this PR conflicts in that one hunk and must be resolved by hand — land them in a known order.
Safe to merge standalone on current main.
|
@wagenet #358 and #407 are now merged into Per your earlier note on #407, the resolution should be:
Please also:
The PR description still says "merges independently of #358" and "based on Once it's rebased and CI is green, I'll review the full diff. #404 is waiting on the storage-target decision in #534. |
Opened by Claude (Claude Code) on behalf of @wagenet.
Based on
main; merges independently of #358. Merging #358 afterwards conflicts only in one hunk ofsrc/__tests__/recording-wiring.test.ts(the pad name andfilename_prefixassertions next to this PR'soutput_dirassertions); a scratch merge resolved it and passed typecheck and 1097 tests.Deactivate uploads a production's recordings, then deletes them from Strom (#378). A file left behind, because its delete failed or was skipped when the production changed during the upload, was uploaded and registered again at the next deactivate, so the recordings list got duplicates. With this change each recording is uploaded and listed once. A session whose upload failed, or that ended without an explicit deactivate (idle timeout, lost flow), is uploaded at the production's next deactivate and dated by its own activation.
Live check against Strom
upstream/mainb28be77bwith #358 underneath (it supplies the recorder's AAC audio), run before the rebase onto #378 and before this branch moved ontomain, not repeated since. Two activations of one production, each deactivated through the real routes, gave 3 files, 3 uploads and 3RecordingDocs with no duplicates, all H.264 High + AAC-LC 48 kHz stereo.How to review
Five commits, in order:
2de79b6fix(recording): upload only the ending activation's recordingseae26c9fix(recording): sweep every activation's recordings at deactivate38cc1d9fix(recording): stop the production sweep at the first auth rejectiond8ffaf2fix(recording): upload a registered file again if its size changed7d5f32afix(recording): keep the file's end time when refreshing a registered recordingCause
The recorder's
output_dirwasrecordings/<production id>for every activation. Deactivate uploaded everything in it, nothing deleted the files, and each upload inserted aRecordingDocwith a fresh random id.Fix
One directory per activation:
recordings/<production id>/<activation start>-<uuid>, saved on the production asrecorderOutputDir.Deactivate sweeps every activation directory and skips files whose
RecordingDocexists. The id is now derived from bucket and key, so the check is oneget, and concurrent deactivates conflict instead of duplicating.startedAtcomes from the directory name,endedAtfrom the file's last write. No directory on Strom (404) means nothing to upload.Legacy productions (recorder but no
recorderOutputDir) also upload files directly inrecordings/<production id>. Other productions skip them, because older sessions there are registered under random ids and would be registered again.Stale recorder fields: the idle watchdog, startup reconcile and activation-failure reset now clear
recorderBlockIdandrecorderOutputDir, and activation always writes both. Before, a later activation without a recorder kept the old block id, and its deactivate split it and registered the old session with the wrong start time.MediaEntrymatches Strom (is_directory,modifiedin epoch seconds). The uploader was checkingis_dir, so it tried to download directories as files.A registered file is skipped only if its size matches Strom's. A file registered while the recorder was still writing it left a truncated object; if its delete then failed, the next sweep would have skipped the complete file and deleted it. A size mismatch now uploads it again and updates the
RecordingDoc's size and end time.Deleting from Strom (feat(recording): delete recordings from Strom after successful upload #378) covers the whole sweep. When the
isStillRecordingguard passes, deactivate deletes every swept file that is in object storage, including files skipped as already registered, so a file whose delete failed is removed at the next deactivate. Each activation directory that ends up empty is removed.An object-store auth rejection stops the whole sweep. fix(recording): fail fast on object-store auth errors in deactivate upload sweep #473 stops
uploadFilesat the firstS3AuthError. The production sweep now also skips the remaining activation directories and the legacy directory, so a rejected store costs one download, not one per directory. Files already in object storage are still deleted from Strom, as onmain, and deactivate still logs the abort.S3 keys are unchanged, so listing and playback need no changes.
Live check
Strom
upstream/mainb28be77b(0.6.10, hasbuiltin.audioenc). A scratch vitest (not committed) drove the realPOST /activateandPOST /deactivateroutes, the real flow generator andStromClient, with a__test1__source and a recording output. CouchDB was in memory. MinIO was stubbed: signed PUTs (SigV4Authorizationpresent) were captured and their bodies written to disk.RecordingDocs inserted…_100511_00000.mp4…_100511_00001.mp4, activation 2…_100522_00000.mp4Activation 1's
_00001is the segment the split opens. The split is asynchronous and lands after deactivate 1 lists the directory, so deactivate 1 misses it. The sweep picks it up at deactivate 2. It holds the frames between the split and the flow stop. Nothing was uploaded twice.Reviewer notes
getper file.main. If the split's new tail segment were already on disk when the sweep lists the directory, the sweep would upload it part-written and then delete it while the recorder is still writing it. In the live run the split landed after the listing.RecordingDocis written, as in feat(recording): delete recordings from Strom after successful upload #378. If that write fails, the object is in the bucket but not listed, and the file is gone from Strom.main:RecordingDocids ignore the activation directory. Two activations holding a same-named file (Strom names files by wall-clock second, so this needs two recorder starts in one second, such as a DST fall-back) collide, and the later file's bytes can be lost.RecordingDocis trusted without checking the object is still in the bucket. If the object was wiped, the Strom copy is deleted without re-upload.recordingsfolder yet, it answers 400 rather than 404 to the production listing, so every deactivate logs an upload error. Deactivate still succeeds.RecordingDocupdate after a re-upload fails, the listing keeps the old size although the object is complete; it is corrected at the next sweep only if the Strom file is still there.freeboxes that the recorder's muxer reserves, whatever the recording's length. The uploader reads each file fully into memory.Testing
recording-reupload.test.ts(new) drives the real deactivate and activate routes against a stateful fake CouchDB and Strom media store.production-lifecycle.test.tschecks that the watchdog and reconcile clear the fields.recording-wiring.test.tsasserts the per-activationoutput_dirand that a second activation gets a different one.pnpm typecheckpasses;pnpm testpasses 1077, 1085, 1086, 1088 and 1088 tests.output_dirrecording-wiringrecording-reupload(skip registered, legacy directory, delete registered)recording-reuploadproduction-lifecycleRecordingDocupdate, for a file that grewrecording-reuploadeachThe 404 handling is the exception: reverting it only adds an error log.
uploadRecordingstests still pass. The meeting rig was not run on this branch.🤖 Generated with Claude Code