0468b0176aeeefdc11b72908f9d6d8cc2c23a84f
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e30d994d74 |
proto: fix conformance run.sh flakiness, prune the veloxd xfail list
Two run.sh fixes plus the xfail prune, all requested together:
1. VELOX_PAIR_AUTO=1 for the isolated veloxd. Pairing is the D1 dev stub
(EnvAutoApprover) and denies without it, so session.pair never issued
a token and the WS half of the veloxd step could never even connect.
2. WS_PORT was hardcoded to 52080 with no free-port search, so one leaked
mockd made every future run fail EADDRINUSE. free_port() binds :0 and
asks the kernel instead. The EXIT trap's stop() used `pkill -P "$pid"`,
which only reaps direct children — tsx's actual listener is often a
grandchild, which that missed and left holding the port. Every server
(mockd, slow mockd, veloxd) now launches under `setsid`, making it the
leader of its own process group, so stop() does `kill -TERM -"$pid"`
(a process-group kill) and reaches everything it spawned in one shot.
3. Pruned the xfail list now that D2, D4b and most of D3 have landed.
Pruning surfaced two more bugs than expected, both in the test harness
itself, not veloxd — worth recording since they were indistinguishable
from real daemon hangs until isolated:
- errors/session.hello.version-mismatch.json documents that the *server*
closes the connection after replying (correct, intended behavior). The
harness replays every fixture on one shared connection per transport,
so once this fixture ran, every later UDS fixture sent into the dead
socket and just sat there until its own timeout — including ones still
on the xfail list, which applyXfail waved through as "expected -32603"
regardless of the real reason. Fixed with a `closesConnection` fixture
flag: replay() reconnects (fresh session.hello) right after such a
fixture instead of leaving the rest of the run to time out one by one.
This is what was actually behind queue.*/session.*/download.remove
appearing to hang — none of them do; verified individually and via a
raw probe script before finding the real cause.
- category.remove.json (deletes the "firmware" category) sorted before
category.upsert.json (creates it) alphabetically, so it was failing
-32602 "no such category" against a fresh DB — never a daemon bug.
Added it to DESTRUCTIVE so it now replays after every other fixture.
Also fixed while verifying "confirm each really passes": download.addBatch.json's
`defaults.categoryId` was "compressed", a category nothing ever creates —
real veloxd correctly enforces the FK on tasks.category_id, so all three
batch items failed instead of the two expected. Changed to "programs" (a
migration-seeded builtin).
Of the 15 fixtures named for pruning, 10 turned out to cleanly pass and
are gone from the list entirely: download.pause/resume/start/cancel,
download.remove, download.addBatch, queue.upsert/stop, download.probe's
success path (D2, including errors/download.probe.probe-failed.json),
and category.upsert. Two do NOT cleanly pass and are kept, with reasons
rewritten to match what's actually happening now instead of the stale D3
text: download.probe.json (see below) and errors/download.provideAuth.not-found.json,
a real bug — on_download_provideAuth never checks the task exists, so an
unknown taskId gets a normal `{ok:false}` result instead of -32010.
Five more fixtures newly needed xfail entries to reach green, none of
them stubs:
- category.list.json — documented gap (deferrals.md's D3a note): the
categories table has no mimeTypes/sortOrder columns.
- download.probe.json, download.get.json, download.list.json,
session.hello.json — not bugs. Each golden depicts a richer lifecycle
state (a probed/in-progress download, a daemon with media/grabber/
Secret Service implemented) than this harness's bound tasks, which are
always fresh and never started, can produce. Optional/omit-if-absent
fields (effectiveUrl, requiresAuth, capabilities) are correctly absent;
the mismatch is against the golden's illustrative values, not the
contract.
- queue.start.json, category.remove.json — same class: startedTaskIds /
reassignedTaskIds are correctly empty because this run's queue/category
have no real membership.
`ctest -L conformance` is green: 100% (2/2), 81.7s (down from ~240s now
that pairing and the port/reconnect fixes remove the retries and the
5-10s timeouts the connection-death bug was producing).
One thing NOT fixed here, flagged for a follow-up decision rather than
touched mid-task: download.add.json's fixture is `startMode: "now"`
against a real, large (~6GB) Ubuntu ISO on the real internet, with
saveDir hardcoded to /home/sami/Downloads/Programs. Every run against a
real veloxd writes a real multi-GB file into that path — confirmed by
running this repeatedly during verification. Isolating the daemon's XDG
dirs doesn't isolate this. Worth its own change (startMode: "later"
would still exercise the add path without the transfer) but out of scope
for a fixture I wasn't asked to touch beyond what blocked this task.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SFeUKLbdHizrJjLBeK7ffz
|
||
|
|
5e3e21543a |
proto: give the generated C++ Dispatcher a real error channel (P1, 1.4.0)
DAEMON's daemon/docs/proto-requests-m1.md P1: velox::proto::Dispatcher's
on_* methods returned Result<T> = expected<T, ParseError>, and dispatch()
mapped every handler error to -32603 InternalError. A handler had no way to
return -32010 (download.get not-found), -32011 (download.add invalid-path)
or -32013 (probe-failed) with their data payloads -- three error fixtures a
conformant server must satisfy were unreachable, blocking DAEMON's
"conformance as a server" M1 DoD.
Two error channels now, kept separate on purpose:
- parse: Result<T> / ParseError -- dispatch() failing to turn the wire into
typed params. Always -32602, always structural.
- handler: HandlerResult<T> / HandlerError -- a handler deciding the request
can't be fulfilled. Carries any ErrorCode + message + free-form data.
struct HandlerError {
ErrorCode code{ErrorCode::InternalError}; // bare {} is a valid -32603
std::string message;
nlohmann::json data = nullptr; // straight into the error's data
};
template <class T> using HandlerResult = std::expected<T, HandlerError>;
dispatch()'s handler branch is now
make_error(id, r.error().code, r.error().message, r.error().data)
instead of a hard-coded InternalError. -32001/-32002/-32003 stay the server
layer's to raise around dispatch(), as DAEMON already does.
Verified end to end against the real dispatch() path: a handler returning
TaskNotFound/InvalidPath/ProbeFailed produces -32010/-32011/-32013 with the
data object intact, and a bare HandlerError{} still yields a clean -32603
with no data field. The `= nullptr` on the member (not `{nullptr}`) matters:
brace-init of nlohmann::json from nullptr is the array [null], not JSON null.
FixtureDispatcher regenerated to HandlerResult; conformance_main.cpp only
inspects dispatch()'s JSON and needed no change. TS side is untouched beyond
the version string -- no server Dispatcher is generated there.
P2 also handled: session.hello.version-mismatch's data.expected was a stale
"1.0.0"; now $any, with a note that the error-fixture compare is on `code`
only so a server echoing kProtocolVersion there is fine.
Version: minor, 1.3.0 -> 1.4.0. Wire is byte-identical (no schema, fixture,
or OpenRPC change) but every Dispatcher implementer must swap Result ->
HandlerResult on regen, and the bump is how lanes are told to. Not an ADR:
one lane consumes this binding, it's the one that asked, and the shape is
the one they proposed. Answered in contracts/proto-answers-daemon-m1.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
|
||
|
|
6db304a0ae |
proto: widen error-on-paused for ADR 0013's auto-pause signal (1.3.0)
DAEMON's docs/adr/0013-task-state-machine-ownership.md needs a wire signal
for the difference between a paused task the daemon entered unilaterally
(auth_required, server_file_changed, disk_full) and one that was requested
(user, schedule, queue stop, admission reconcile) -- without it, DAEMON's §3
resume rule ("resume only when the reason matches the event that justifies
resuming") has nothing correctness-preserving to key on, and would have to
guess from timing. CORE has already accepted the ADR; this was the sole
remaining blocker per DAEMON's own status line on it.
No retype, no new field -- error was already TaskError | null on both
event.task.state and TaskSummary, exactly as DAEMON characterized the ask.
Only the *description* of when it is populated widens: previously "failed or
retry_wait", now also "paused, when the daemon entered it on its own
initiative". A deliberate pause still carries error: null. TaskError's own
top-level description gets the same widening, since it previously also said
"failed or retry_wait" and would otherwise contradict the field that embeds
it.
New fixture (event.task.state.auto-paused.json) exercises the case directly:
an auth_required pause with error populated, contrasted in its own
description against download.pause.json's error: null for a requested pause.
The existing event.task.state.json fixture's first assertion was stale
("error is present exactly when failed or retry_wait") and is corrected.
Minor bump, 1.2.0 -> 1.3.0: a description widening on an already-nullable,
already-optional field changes no JSON Schema shape, but it is a real
behavioral commitment change worth a version bump so downstream regenerates
and notices, per the same reasoning ADR 0010 applied to TaskErrorCode.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
|
||
|
|
2d36e9fef0 |
proto: land F2 — download.provideAuth (1.2.0)
The last contract gap blocking an M1 definition-of-done item: CORE's "401
handled" has no return path without it, and B2a's sibling F2 was accepted in
proto-answers-m1.md but never actually landed.
download.provideAuth {taskId, username, password, save?} -> {ok}, exactly as
proposed there. Privileged and Unix-socket-only: a credential-bearing method
must never be reachable from the browser, which is the other half of the
promise event.auth.required's own description already makes ("never back
through this event, never into a log"). It answers the challenge; it does not
itself resume the task -- the daemon retries with the credential attached and
the ordinary event.task.state reports the task leaving retry_wait, the same
as any other state change.
save only tells the daemon whether to persist the credential in the Secret
Service for next time, or use it for this attempt alone -- it never touches
SQLite or a log either way, in keeping with CLAUDE.md's secrets rule.
Three fixtures: the success path, -32010 for a task that no longer exists
(credentials submitted for it are simply discarded), and -32003 confirming
the extension has no path to this method under any transport.
mockd gets a real handler rather than falling through to the generic fixture
responder: it validates the taskId exists (so the -32010 fixture is
replayable) and actually transitions the task out of retry_wait.
Minor bump, 1.1.0 -> 1.2.0: additive method, no existing type touched.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
|
||
|
|
60363a7142 |
proto: land B4 and B2a — buffer bounds, budget knobs, effective readback (1.1.0)
Minor bump on 1.0.0, per core/docs/buffer-sizing.md. B4 — bufferBytes bounds corrected in all four locations (DownloadSpec, TaskDetail, download.update's patch, Settings.connection.bufferBytes): was 4 KiB-8 MiB with no stated default, now 64 KiB-16 MiB with a 1 MiB default. 64 KiB because 4 KiB is smaller than one libcurl HTTP/2 write-callback delivery; 16 MiB because throughput from write size is flat past ~1-4 MiB and past 16 MiB there is stall-cover left to buy but no memory left to spend it on; 1 MiB default because it is the only candidate for which docs/04's 60 MB RSS target actually holds once buffers are counted per segment, not per download. Two new settings keys: connection.maxTotalBufferBytes (128 MiB default) and connection.maxActiveSegments (32 default). Without them CORE's clamp — reduce every live segment's buffer to fit the global cap — has no wire configuration surface, and "20 active downloads" has no meaning distinct from 160 live TLS connections. B2a — TaskDetail.effectiveBufferBytes: what a segment is actually using right now, after the clamp. Placed on TaskDetail next to bufferBytes, following the requested/effective pattern ADR 0010 already established for segments. The download.get fixture now demonstrates a real clamp (16 MiB requested, 4 MiB effective) rather than a case where the cap happens not to bind. docs/04-engine-design.md §4 and §8 updated in the same change per CORE's request and CLAUDE.md rule 5: the RSS target is now stated as conditional on maxActiveSegments = 32, and the old 4 MiB/64 MiB/256 MiB numbers are corrected to match the schema. ADR 0012 records the reasoning and explicitly keeps the 60 MB target over CORE's offered 120 MB alternative, with the arithmetic that makes 60 MB achievable with margin. Numbered 0012 rather than 0011: DAEMON is independently drafting ADR 0011 (admission control / segment budget split) in a peer session at time of writing, so 0011 was reserved to avoid a collision. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV |
||
|
|
2c8f5e5d7d |
proto: answer CORE's freeze-blockers before 1.0.0 lands
Three corrections into 1.0.0, all of which would be major bumps once the contract has landed. It has not: main still carries 1.0.0-draft, so these are corrections to an unpublished version rather than changes to a released one. ADR 0010 records that and the reasoning behind each. B1 — TaskError.code was a bare integer, and the integer space in the contract is JSON-RPC's, which is a different thing; TaskError's own description said so while typing its code as one. Freeze TaskErrorCode: a string enum mirroring vdm::Error by name and in order, all 27 failure values, verified against core/include/vdm/util/error.hpp mechanically. ErrorCode says why a call failed; TaskErrorCode says why a download failed, and a download fails while every RPC succeeds. Adds TaskError.cause so max_retries_exhausted names what kept failing. B2 — TaskSummary.segments is now explicitly the effective count in use right now, after the per-host cap and the non-resumable demotion to 1. DownloadSpec.segments and download.update's patch say they are the requested value. B3 — Segment.endByte's "minimum: 0" contradicted the description's own empty-range encoding of startByte - 1, which is -1 for the first segment of every download. Empty ranges are no longer representable and are not needed. The range stays CLOSED and INCLUSIVE, matching the HTTP Range header the two fields are copied into verbatim, and that is now stated in the schema, the README, an ADR, a fixture assertion and a conformance check. CORE asked for half-open and gets a written notice rather than a silent schema edit. Segment state spells 'downloading' as CORE asked, not 'receiving'. check_contract.py now enforces segment contiguity, coverage of exactly [0, sizeBytes-1], downloadedBytes within the range size, and the entry count matching TaskSummary.segments. The download.get fixture claimed 8 segments while carrying 2; it now carries 8 contiguous ones covering the whole file. contracts/proto-answers-m1.md answers every item in core/docs/proto-requests-m1.md, including the ones not being landed now: B2a and F2 accepted as follow-ups, F1 answered with the notify path for M1, F3 already frozen as a Checksum object rather than a string, and D1 left for DAEMON to draft as the three-way ADR it is. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV |
||
|
|
53421d6cb8 |
proto: freeze the wire contract at 1.0.0
Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.
Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:
gen_cpp.py -> core/generated/velox_proto.{hpp,cpp}
gen_ts.py -> extension/src/shared/protocol/
gen_openrpc.py -> contracts/openrpc.json
gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp
Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.
59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.
run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.
core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
|
||
|
|
8bb683b09d |
scaffold: project structure, wire contract, roadmap and agent briefs
Lays out Velox Download Manager (IDM-class download manager for Ubuntu 26.04) as a monorepo ready for parallel lane development. No implementation code by design. - docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design, Firefox extension spec, risks/spikes, packaging - contracts/: wire-contract skeleton (JSON Schema + fixture templates) — the single synchronization point between lanes - docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA) with owned directories, build order and definition of done - CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables - CMake scaffolding with dev/tsan/release/ci presets Two environment findings shape the design: Firefox here is the Mozilla snap (native-messaging risk, so the extension carries a loopback-WebSocket fallback), and Wayland forbids passive clipboard monitoring (so clipboard capture is explicit-action-first). Co-Authored-By: Claude Opus 5 <[email protected]> |