Skip to content

fix: serve remote daemon request diagnostics to the caller - #1814

Merged
thymikee merged 1 commit into
mainfrom
fix/1801-remote-request-diagnostics
Aug 18, 2026
Merged

fix: serve remote daemon request diagnostics to the caller#1814
thymikee merged 1 commit into
mainfrom
fix/1801-remote-request-diagnostics

Conversation

@thymikee

@thymikee thymikee commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

A failed command against a remote daemon used to print a diagnostics path on the daemon's
filesystem, which the caller cannot open — a CI runner driving an EAS/limrun simulator got one line
of error and no route to the evidence. Now the record itself comes to the caller.

  • Daemon API: new authenticated GET /sessions/<session>/requests/<requestId>/diagnostics,
    streamed as application/x-ndjson, behind the same authorize gate as /artifacts/*. Session and
    request ids are validated with the same safe-name rule session artifact paths use, and the path is
    built only through resolveSessionRequestLogPath. A tenant may only read records inside its own
    <tenant>: session namespace, using the same rule that names those sessions
    (session-tenant-scope.ts, now shared with scopeRequestSession).
  • Error payload: the daemon additionally names the record it already names by path —
    DaemonError.diagnosticsRecord = { session, requestId } — built together with the path in one
    resolveSessionRequestLog result, so the two can never disagree.
  • Client: for a remote daemon the failure is rehydrated only after the record has been made
    readable. The daemon-host logPath is removed at the transport boundary (the stripped payload type
    makes it unrepresentable downstream) and the fetched copy at
    <state-dir>/remote-diagnostics/<session>/<request-id>.ndjson is the only thing that can become
    logPath. One attempt, 10s timeout, 8 MiB cap; a failed fetch names no path at all.
  • Rendering: Diagnostics Log: is now always a path on the calling machine.
    Remote Diagnostics: unavailable (remote daemon <url>, request <id>: <reason>) prints when the
    record stayed remote. --debug prints the fetched record's tail inline ([remote diagnostics]) and
    no longer reads the local daemon.log, which belongs to a different daemon (same gate
    maybeStartDaemonLogTail already had).

Before / after, same failing command against a remote daemon:

- Diagnostics Log: /Users/daemon-host/.agent-device/sessions/cwd_…_default/requests/3b54f6ae.ndjson
+ Diagnostics Log: /ci/state/remote-diagnostics/cwd_…_default/requests/3b54f6ae.ndjson

A CI job can archive remote-diagnostics/ wholesale, or hit the route itself with the base URL and
token it already holds.

Local daemons are unchanged: the path they print is on the same machine as the caller.

Closes #1801

Validation

Red first. With the production sources reverted to HEAD~1 (new tests kept), the four
cli-remote-diagnostics cases and four of five route cases fail. The motivating failure, verbatim:

FAIL src/__tests__/cli-remote-diagnostics.test.ts > a remote failure names a record the caller can actually read
AssertionError: The input did not match the regular expression
  /Diagnostics Log: …\/agent-device-remote-client-6up4di\/remote-diagnostics\/…\.ndjson/. Input:
  'Diagnostics Log: /tmp/…/agent-device-remote-daemon-VHTW2z/sessions/cwd_abcdef0123456789_default/requests/3b54f6ae4fbd332e.ndjson\n'

FAIL src/__tests__/cli-remote-diagnostics.test.ts > an unfetchable remote record says so instead of naming a path
AssertionError: Expected "actual" to be strictly unequal to:
  '/tmp/…/agent-device-remote-daemon-EEcq3H/sessions/cwd_abcdef0123456789_default/requests/c26ce7630cee0ddc.ndjson'

That is exactly the reported bug: the printed path is the daemon host's. All 12 pass after the fix.
One route case (404 for an unknown record) passes pre-fix too — with no route at all everything 404s;
it is meaningful only next to the 200 case on the same server, and is kept for that pairing.

Tests. src/daemon/__tests__/request-diagnostics-http.test.ts (real loopback daemon: 200 + ndjson

  • content-length, 401 without a token, 404 for unknown record/session, 400 for ../empty ids sent as
    a raw request target — fetch resolves .. away client-side, so the traversal case needs a
    hand-built target — and cross-tenant refusal). src/remote/__tests__/remote-request-diagnostics.test.ts
    (fetched copy becomes logPath; nothing serialized on the error, printed or JSON, equals the
    daemon-supplied path; unavailable reasons). src/__tests__/cli-remote-diagnostics.test.ts drives the
    real CLI through the real client against a real daemon HTTP server, covering human output, --json,
    --debug tail, and the no-route case.
    test/integration/provider-scenarios/remote-daemon-client.test.ts had asserted the old contract
    (logPath === '/remote/log.txt' from a remote daemon) and now asserts the new one.

Live (iOS simulator iPhone 16, 416440AE-…). Daemon in state dir A (dual transport) played the
remote host; the CLI ran from state dir B with AGENT_DEVICE_DAEMON_BASE_URL /
AGENT_DEVICE_DAEMON_AUTH_TOKEN, after open com.apple.Preferences:

$ agent-device wait '@e999' 2000 --platform ios          # state dir B, remote base URL
Error (INVALID_ARGS): Ref wait requires an existing snapshot in session.
Diagnostics Log: …/stateB/remote-diagnostics/cwd_214b000b467fb9e9_default/requests/cd1f30da7dec1ac0.ndjson
$ diff <B copy> <A/sessions/…/requests/cd1f30da7dec1ac0.ndjson>   # identical

--debug additionally printed [remote diagnostics] with both ndjson lines (request_start,
request_failed) and no [daemon log]. --json carried the B-local logPath plus
diagnosticsRecord. The same command against daemon A directly (no base URL) still printed A's own
session path. The route was also exercised directly: authenticated 200 / 1011 bytes / application/x-ndjson, no token 401, unknown request id 404. Session closed, both daemons stopped,
simulator shut down.

Gates. pnpm check:affected --run green (format, lint, typecheck, layering, fallow, unit +
provider-integration, wire-compat), plus pnpm check:daemon-wire-compat:
169 declarations, 7 changed, 0 removed, 18 added against v0.20.9.

Rebased onto main after #1817, so the frameworkTier parity failure this branch briefly inherited
from main is gone; the local gate is clean end to end.

Wire surface

Additive under ADR 0006 — protocol stays 2. compatibleChanges acks cover DaemonError,
NormalizedError, normalizeError, handleDaemonHttpResponseBody, toDaemonHttpRpcError, plus two
helper moves in the artifact route (readArtifactId and its mid-stream error branch now use shared
helpers). Both new route sides — producer and consumer — are listed in surface.ts.

Tradeoffs and follow-ups

  • open's initialSnapshotError.logPath is not covered. It rides inside a successful response's
    data, not through the error rehydration seam, so this change does not reach it. A remote open whose
    initial capture failed can still surface a daemon-host path there; worth a follow-up issue.
  • A daemon older than this change sends no locator, so the client prints
    unavailable (… the daemon named no diagnostics record) rather than a path — deliberate: the old
    behavior was the bug.
  • The unavailable line is its own Remote Diagnostics: line rather than a second Diagnostics Log:
    line, because the CLI's own client-side record may still be named and printing two lines with the
    same label would read as a contradiction.
  • The route serves one record by locator and deliberately does not enumerate; there is still no
    retention job for requests/*.ndjson on either side (pre-existing).
  • 36 files touched. Scope stayed within the diagnostics/daemon-transport boundary; the only adjacent
    edits are the mechanical flushDiagnosticsToSessionFile return-type update at its call sites and the
    two shared HTTP helpers fallow's duplication gate required.
  • isSafeSessionSegment (which this branch introduced) landed on main independently, where
    resolveSessionDir now refuses ./.. outright; this branch keeps only its route-level use of the
    shared predicate, so a malformed request target is refused with a 400 before the auth hook runs
    rather than by the path constructor afterwards.

@thymikee

Copy link
Copy Markdown
Member Author

Code review at f457c80 found no implementation or architecture defect in the remote diagnostic-record route. Readiness is blocked by the current merge conflict (DIRTY / CONFLICTING); only CodeQL has run on this state. Rebase/resolve the conflict, then obtain complete exact-head CI before ready-for-human.

thymikee added a commit that referenced this pull request Aug 18, 2026
…onDir (#1815)

safeSessionName only rewrites characters outside [a-zA-Z0-9._-], so the names
'.' and '..' survive unchanged and path.join resolves them to the sessions dir
itself or its parent, the daemon state dir. A remote caller's --session ..
would then land app.log / runner.log / requests/*.ndjson outside the sessions
tree.

SessionStore.resolveSessionDir is the one place a session name becomes a
directory (AGENTS.md: session artifact paths come from session-store), so it now
refuses such a name with INVALID_ARGS. Every request goes through it first
thing in createRequestExecutionScope, before any artifact path is used, so this
is also the admission-time rejection; every other caller passes an already
admitted name.

isSafeSessionSegment mirrors the predicate PR #1814 adds for its
request-diagnostics route; whichever lands second takes the trivial merge.

Regression tests were proven red against the pre-fix code: resolveSessionDir
returned the sessions dir / state dir for '.', '..', '' and the request scope
resolved runnerLogPath to <stateDir>/runner.log.
@thymikee
thymikee force-pushed the fix/1801-remote-request-diagnostics branch 2 times, most recently from 4112c14 to efba178 Compare August 18, 2026 10:18
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-18 13:36 UTC

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.28 MB 2.28 MB +1.8 kB
JS gzip 748.3 kB 749.4 kB +1.1 kB
npm tarball 870.1 kB 871.7 kB +1.6 kB
npm unpacked 3.04 MB 3.04 MB +3.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.4 ms 26.4 ms -0.0 ms
CLI --help 65.2 ms 65.2 ms +0.1 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/agent-device-client.js +2.3 kB +713 B
dist/src/session2.js +1.1 kB +422 B
dist/src/internal/daemon.js -1.0 kB -348 B
dist/src/sdk-batch-runner.js -1.9 kB -169 B
dist/src/cli-help.js +248 B +108 B

@thymikee

Copy link
Copy Markdown
Member Author

Delta re-review at efba178 found no PR-owned code or architecture issue; the prior conflict is resolved, and the added SessionStore segment hardening preserves containment. Readiness remains blocked because exact-head Coverage is red on hover frameworkTier parity and iOS Smoke is pending. The identical Coverage assertion fails on base run 32124582662, so this is inherited main owner-action rather than a #1814 regression; repair/rebase main and obtain complete exact-head green CI before ready-for-human.

@thymikee
thymikee force-pushed the fix/1801-remote-request-diagnostics branch from efba178 to 3aa034d Compare August 18, 2026 12:28
@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 18, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 3aa034d: clean and merge-ready. The only delta from the previously reviewed head is inherited main fixes (#1816/#1817); the remote diagnostics route/client/error architecture is unchanged. The former inherited Coverage blocker is resolved, and all exact-head required checks plus Android/iOS/macOS/Linux/Web lanes are green. Existing live iOS remote-route evidence remains applicable.

@thymikee
thymikee merged commit d0d5c85 into main Aug 18, 2026
33 checks passed
@thymikee
thymikee deleted the fix/1801-remote-request-diagnostics branch August 18, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A remote caller cannot read the diagnostics log the error names

1 participant