flows status --cloud / flows logs: show running steps, add --watch and --follow - #545
Conversation
`flows status --cloud` and `flows logs` were built for a run that had already finished. This adds the two live forms and the live step rows the watched page needs. - `flows status --cloud` renders `running`, `backoff`, `waiting` and `needs_human` rows in the local view's grammar, with the number of the attempt now running and the time since its start. Only what Cloud's step snapshot establishes is printed: no maximum-attempt denominator, no wait id, no attempt for a row that was never dispatched, and no advancing of a finished step whose end timestamp is missing. A running run with no rows says so rather than claiming the run has no steps. - `flows status --cloud --watch` redraws every two seconds until the run is terminal and leaves the final page up. Each frame is rendered, checked against the abort, then written in one call, so an interrupt cannot leave a torn page and a failed poll leaves the previous page alone. - `flows logs <run> --follow` appends new runner output until the run ends and Cloud marks the log complete. Lines are buffered across polls and redacted whole, so a secret split on a chunk boundary cannot escape. - Both exit with the run's outcome, not the read's, through the validator `flows run --cloud --wait` already blocks on (extracted to `cloud-run-record.ts` and shared): 0 only on an attested completed/success, 1 on an attested failure or cancellation, and `cloud_invalid_response` on a terminal record that attests neither. Ctrl-C is `observation_aborted`, exit 1, and does not cancel the run. The runner log is re-read whole on each poll and only the new suffix is printed: the route's `offset` is a byte count and the content is a string, and mixing them loses text on the first non-ASCII character. A snapshot that no longer extends what was printed refuses with `cloud_log_rewritten` rather than guessing. `--follow --step` is refused before any request; a step transcript is not an append-only stream. `cli/cloud-read.ts` was over the 500-line smell threshold, so the shared formatting, refusals and status view moved to dependency-only modules that the one-shot and live entry points both import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Two review findings on #545. F1 (P1, review.md): `flows logs --follow` split the new content into lines before redacting each one, while the one-shot read redacts the whole content first. A secret env value spanning more than one line — a PEM private key is the usual one — matches no single line, so every line of key material reached stdout. The un-released remainder is now redacted as one block: a cut is released only where no secret value has begun and not ended (`openSecretStart`, which also covers a value whose halves arrive in two different polls) and only where redacting the block alone gives the same text as the front of the whole redacted remainder, so a cut that fell inside something the redactor would have caught moves back a line instead. A line can therefore appear one poll late; none of it can appear unredacted. Cursor Bugbot (inline, medium): `flows status --cloud` preferred a step row's `duration_ms` over the derived elapsed time. A row still in flight can carry the `wallclockMs` of the attempt that already ended, so a watched frame froze its clock at that attempt's duration. The derived time comes first now; `liveElapsed` is null for every row that is not demonstrably in flight, so a finished row still prints what Cloud reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Relayflow: the adversarial review did not pass. This branch is not approved: the flow stopped here and did not mark it ready to merge. Review of PR #545Reviewed head: Changes requested. One credential-disclosure finding remains. P1 — Preserve unfinished credential-header context between pollsLocation: When a running log ends with The assumption that header shapes cannot span lines is false: the named-value patterns in Retain incomplete named-credential context across polls (or use an incremental redactor with equivalent semantics to whole-log redaction). Add regression coverage where the first snapshot ends immediately after each header and the next completes its value; assert that no intermediate or final stdout write contains the credential. ReproductionAfter the package build, the following command uses only fake responses, transitioning running → completed. All credential strings are synthetic. cat > /tmp/flows-review-current-probe.mjs <<'EOF'
import { runCloudLogsFollow } from '/home/daytona/.relayflow-v2-supervisor/durable/repository/packages/sdk/dist/cli/cloud-live.js';
import { redact } from '/home/daytona/.relayflow-v2-supervisor/durable/repository/packages/sdk/dist/redact.js';
for (const prefix of ['x-callback-token:\n', 'Bearer\n', 'authorization:\n']) {
const full = prefix + 'opaque-sensitive-value\n';
let tick = -1;
globalThis.fetch = async input => {
const logs = String(input).endsWith('/logs');
if (!logs) tick++;
const content = tick === 0 ? prefix : full;
return new Response(JSON.stringify(logs ? {content, offset: Buffer.byteLength(content), totalSize: Buffer.byteLength(content), done: tick > 0} : {runId:'review-run', relayflowVersion:'v2', status: tick === 0 ? 'running' : 'completed', result: tick === 0 ? undefined : {ok:true, status:'completed', completionReason:'success'}}));
};
const stdout = [], stderr = [];
const exit = await runCloudLogsFollow({runId:'review-run', step:undefined, json:false}, {stdout: x=>stdout.push(x), stderr:x=>stderr.push(x)}, {apiUrl:'https://fake.example', token:'fake-test-token',env:{},sleep:async()=>{}});
console.log(JSON.stringify({prefix, oneShot:redact(full,{}),exit,stdout,stderr}));
}
EOF
node /tmp/flows-review-current-probe.mjs > /tmp/flows-review-current-probe.txt
cat /tmp/flows-review-current-probe.txtCaptured output (exit 0): Prior findings and PR commentsRead the PR description, issue comment, submitted review, and inline comment. CodeRabbit skipped review. Cursor's inline finding concerns snapshot duration freezing live elapsed time; current Commands used to capture the comments: gh api --paginate repos/AgentWorkforce/flows/pulls/545/comments --jq '.[] | {path,line,body}' > /tmp/flows-review-current-comments.json
gh pr view 545 --json headRefOid,comments,reviews > /tmp/flows-review-current-pr.jsonThe complete captured responses are in those files; no comment was posted or changed. Affected checksFrom npm run typecheck && npm run build && npm run typecheck:tests && npx vitest run tests/cloud-live.test.ts tests/cloud-read.test.ts tests/cloud-run.test.ts tests/cli-status.test.ts tests/relay-cli-surface.test.ts tests/cli.test.ts tests/redact.test.ts > /tmp/flows-review-current-focused.txt 2>&1Captured typecheck/build output: Captured focused test output: Scope and limitsReviewed the diff, new tests, shared outcome validator, transport mapping, signal ownership, rendering and redaction paths. No live hosted run was exercised; server compatibility is not attested by this review. No production code or existing test was edited. No mutation-verification claim is made. Kernel code is unchanged; no standalone kernel test suite was run. Full SDK package suiteFrom npm test > /tmp/flows-review-current-package.txt 2>&1Exit 1: 8 files failed, 154 passed, 3 skipped; 41 tests failed, 2563 passed, 25 skipped; 1 unhandled error. Failures include missing daemon paths ( Complete captured package output |
followLog released any trailing partial line unless it matched an environment-secret opener. A poll that split inside a named credential header (authorization:, Bearer, x-callback-token:, x-nightcto-evidence-token:) or inside a JSON credential field emitted the prefix in clear, and the completing poll then bypassed whole-log redaction semantics. Add openCredentialStart() mirroring openSecretStart(): retain the tail whenever it opens one of those contexts without its terminator, and hold a bare trailing 'Bearer' since the scheme alone discloses nothing but its continuation does. Regression tests split each reviewer repro across polls and assert zero secret bytes before the completing poll.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb389ca6e8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- require a post-terminal log read before a done snapshot drains the follow, so a stale upload marker cannot truncate the run's last bytes - never end a released block on a bare credential header; its value may begin on the next line, which the block-prefix check cannot see
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ebd76b1. Configure here.
Two follow-on findings on the header-hold: the grow-then-recheck could bounce cut between a bare credential header and its next line forever (header + blank line or Bearer + unterminated tail), and the grown cut could pass openSecretStart's bound, releasing a header plus the first line of a still-arriving multiline env secret — leaving the rest of the value unredactable on the next poll. The fix removes growth entirely: a candidate block that ends on a bare credential header simply shrinks until the header is inside the held remainder; it releases together with its value once that line completes. Mutation-verified: buggy version hangs on the unterminated-tail test (exit 124) and prints beta/gamma verbatim on the multiline test.
|
The "not approved" verdict above is stale — it reviewed
The finding is addressed as asked. if (endsWithOpenCredentialHeader(raw.slice(state.released, cut))) {
cut = lineBoundaryAtOrBefore(raw, cut - 1);
continue;
}
The requested regression coverage exists, in the shape the review specified — first snapshot ending immediately after each header, a later poll completing the value, across all three header forms: it.each(['x-callback-token:', 'Bearer', 'authorization:'])(...)
{ run: RUNNING, log: logBody('x-callback-token:\n') },
{ run: RUNNING, log: logBody('x-callback-token:\nopaque-sensitive-value') },
{ run: COMPLETED, log: logBody('x-callback-token:\nopaque-sensitive-value\n', { done: true }) },plus the Verification: Mutation-proved — disabling the header hold fails exactly the guarding test and nothing else: Since the flow could not convert this PR back to a draft, its non-draft state was never a verdict either way; the evidence above is. |

flows status --cloud --watchandflows logs --followCloses the ticket's three items: live step rows on the hosted status page, a
watched page, and a followed runner log.
What changed
Live step rows (
flows status --cloud, one-shot and watched).runningandbackoffrender↻,waitingandneeds_humanrender⏸,matching
cli/status.ts's glyphs. A row in flight shows the number of theattempt now running and the time since its
startTime:Only what Cloud's snapshot establishes is printed:
retryCount + 1, notattempts.length—those entries are completion records, so during a second attempt the array
still holds one and would print
attempt 1. It is printed only when the rowalso carries a
startTime; a row with no dispatch evidence shows no attemptat all rather than
attempt 1./maxdenominator, noawaiting <kind>: <id>, nobackoff until …:Cloud's step rows carry no maximum, no wait id and no backoff deadline.
finished step whose
endTimeis missing keeps the duration Cloud reportedinstead of being advanced to now. A start in the future clamps to zero.
steps 0thenNo step snapshot available yet.— a fact about the snapshot, where a baresteps 0would be a claim about the run.flows status --cloud --watch. Polls every two seconds; each tick readsthe run record, then the step rows, renders the whole page, checks the abort,
and emits it in one
io.stdoutcall. ANSI clear only for a TTY (threadedthrough
CliIo.tty); redirected output appends complete pages. A failed pollleaves the previous page untouched. The terminal page is printed once and no
further tick is scheduled.
flows logs <run> --follow. Reads the run record, then the log, each tick;prints only whole lines, holding a partial line across polls so a secret split
on a chunk boundary is redacted as one line. Stops only when the run is
terminal and the envelope says
doneand the served bytes covertotalSize;donewith an active run keeps following, and a terminal run withdone: falsekeeps draining. Prints the header once and the run's footer once(
COMPLETED <run> completionReason: success).Exit codes are the run's, not the read's. The raw-record validator behind
getCloudFlowRunmoved tocloud-run-record.tsand is now shared, so--watch/--followblock on exactly whatflows run --cloud --waitblockson: 0 only on an attested
completed/success, 1 on an attested failure orcancellation, and
cloud_invalid_response(exit 1) on a terminal record thatattests neither, or on a run status this client does not know.
getCloudRunDetailLivevalidates and maps the same response, so no secondGET per poll. Ctrl-C is
observation_aborted, exit 1, and says the hosted runwas not cancelled. Existing read refusals keep their codes and exits.
Deliberate decisions
offsetis a byte count andcontentis aJavaScript string; subtracting one from the other silently loses text the
moment the log contains a non-ASCII character — which the runner's own
transition lines do (
▶,✓,·). The Cloud-side contract could not bechecked from this repo, so
--followre-reads the log whole each poll andprints the new suffix, refusing with
cloud_log_rewrittenif what wasprinted is no longer a prefix. Cost: a full read per poll, and the log held
in memory. Benefit: no line can be duplicated or skipped. Documented in
docs/CLOUD.md.--follow --stepis refused (invalid_invocation, before any request).A retry replaces a transcript and the rendered form is built from the whole
JSONL; following it is a separate feature.
--jsonemits one final document, not a frame stream — the one theone-shot form would have printed, with
--followcarrying the whole redactedlog. Deliberately unlike
check --watch's report-per-check.ok: truemeansthe read succeeded; the run's outcome is the exit code. Called out in the
docs as a choice, not a repository rule.
projections; a step row can lag its header by a poll. Documented rather than
papered over.
donekeeps draining. Stoppingafter N identical polls would be an arbitrary truncation; Ctrl-C ends it.
Documented.
Module layout
cli/cloud-read.tswas 518 lines, past the AGENTS §1 threshold. The sharedpieces moved to dependency-only modules, so the one-shot and live entry points
import them without depending on each other (
cli.tsdispatches both):cli/cloud-format.tssafe,thousands,dollars,instant,ago,errorLinescli/cloud-refusal.tsrefusalFor,fail,RUN_ID_REQUIRED,isTransientReadcli/cloud-status-view.tscli/cloud-live.tscli/cloud-read.tscloud-run-record.tscloudRunState,isCloudRunActiveVerification
All commands run from
packages/sdk.npm run typecheck && npm run build && npm run typecheck:tests— clean:npx vitest run tests/cloud-live.test.ts tests/cloud-read.test.ts tests/cloud-run.test.ts tests/cli-status.test.ts tests/relay-cli-surface.test.ts tests/cli.test.ts:tests/cloud-live.test.tsis new: 43 cases over live rows (all four states,retry in progress, unknown attempt, absent/invalid/future timestamps, elapsed
growing across frames, empty snapshot), watch lifecycle (running → completed,
already terminal, failed, cancelled, exactly one final frame and no extra poll,
TTY vs redirected), outcome integrity (completed with no valid result,
contradictory reason, unknown status, mismatched run id,
needs_humanstep onan active run), follow content (growth, no-op poll, blank and repeated lines,
CRLF, partial lines, unterminated final line, empty log, multibyte log),
draining (
done+ active, terminal + notdone, transient failure on the finalread, finished failed run), security (secret split across polls), cancellation
(pre-aborted, between the two reads, during the sleep, aborted fetch, no
process handler installed), refusals and retries (404/401, 503 backoff growth,
cap and reset, zero requests on the refused combination),
--jsonfor bothverbs, and the argv/
runCliwiring.Mutation verification
Two behaviours, each reverted, failed, restored byte-for-byte (md5 compared),
and re-passed.
1. Attempt number from
retryCount + 1, notattempts.length. Changedcli/cloud-status-view.ts:85to`attempt ${step.attempts.length}`:Restored (
md5sumof the file and the backup bothc736206b848f19d523c4353d2c8cd071) and re-run:2. The abort check before a frame is emitted. Deleted the
if (isAborted(signal)) return aborted(...)line between rendering and writingthe page in
cli/cloud-live.ts:Restored (both
53d54ee5994b3ecfb687eb9ceb638c42) and re-run:Full package suite: 7 files fail, for environment reasons, before this change
npm testinpackages/sdk:The failures are
tests/live-kernel.test.ts,tests/webhook-live.test.ts,tests/mcp.test.ts,tests/provider-trigger-executor.test.ts,tests/authored-node-runtime.test.ts,tests/canonical-software-factory.test.tsandtests/stuck-run-triage.test.ts, with two causes, neither touched by thischange:
spawn …/kernel/target/debug/relayflowd ENOENT—ops/cargo.shputs buildoutput under
$HOME/.relayflows-toolchain/target/<hash>/, notkernel/target/, so the live-kernel suites find no binary in this sandbox.expected an @relayflows/surface flow handle— this sandbox had nonode_modulesat all; installing pulled the published@relayflows/surfacerather than linking
packages/surface, so two copies of the surface areloaded.
Confirmed pre-existing by stashing the change (
git stash push -- docs packages) and re-running exactly those seven files on the unmodified tree:Same seven files, same 39 tests. The stash was popped and the working tree
restored before committing.
No kernel code was touched and no kernel test was run. The change is
TypeScript CLI and transport only.
Not done
(cloud#3918) could not be verified from this repo — both issue URLs were
unreachable. Item 1 is therefore exercised against fixtures shaped like the
documented step-row contract, not against a live hosted run.
--follow --step(rendered or raw) is refused rather than implemented.--watchfor the localflows status; it stays a one-file offline read.Note
Medium Risk
Changes CLI exit semantics, streaming log redaction, and shared Cloud run attestation—security- and automation-sensitive—but behavior is heavily specified and tested; no server or kernel changes.
Overview
Adds live hosted-run observation via
flows status --cloud --watch(2s redraw until terminal) andflows logs <run-id> --follow(append runner output until the log drains). Both use the same outcome rules asflows run --cloud --wait: exit 0 only on attestedcompleted/success, 1 on failure/cancel or Ctrl-C (observation_aborted), with shared validation in newcloud-run-record.tsandgetCloudRunDetailLive.flows status --cloudnow renders in-flight steps like the local view (↻/⏸, attempt fromretryCount + 1, elapsed fromstartTime) without inventing max attempts or wait metadata; empty snapshots on active runs get an explicit message.--followre-reads the full log each poll (no byte offset paging), refuses--stepandcloud_log_rewrittenif the prefix changes, and releases lines only after block-wise redaction (openSecretStart/openCredentialStart) so secrets and split credential headers cannot leak across polls.Refactors oversized
cloud-read.tsinto shared format/refusal/status modules pluscloud-live.ts;CliIo.ttycontrols ANSI clear on watch. Docs (CLOUD.md,SURFACE.md) andtests/cloud-live.test.tscover the new behavior.Reviewed by Cursor Bugbot for commit be836ba. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds live reads for hosted runs:
flows status --cloudnow renders running steps with elapsed time,--watchredraws the page every two seconds until the run ends, andflows logs <run> --followappends new runner output until the run is terminal.flows run --cloud --waitalready uses (extracted tocloud-run-record.tsand shared): 0 only on an attestedcompleted/success, 1 on failure or cancellation,cloud_invalid_responseon a terminal record that attests neither.observation_aborted, exit 1) without cancelling the hosted run.startTimefor in-flight rows and is never overridden by a staleduration_ms, so a watched frame's clock keeps running.--followre-reads the whole log each poll and prints only the new suffix, redacting the un-released remainder as one block rather than line by line, so a multi-line secret such as a PEM key cannot leak; a snapshot that no longer extends printed output refuses withcloud_log_rewritten. A trailing named-credential header (authorization:,Bearer,x-callback-token:,x-nightcto-evidence-token:) split across polls is held rather than printed in clear: the release cut shrinks until the header is inside the held remainder and never grows over it, so the header and its value leave together once the line completes — which also prevents the hold from bouncing forever between a header and a blank line, or releasing a header plus the first line of a still-arriving multiline secret. A done snapshot drains only after a post-terminal log read, so a stale upload marker cannot truncate the run's last bytes. Offset paging was avoided because the route'soffsetis a byte count and the content is a string, which would lose text on the first non-ASCII character.--follow --stepis refused before any request.Refactors
cli/cloud-read.ts(518 lines, past the project's threshold) was split into dependency-only modules —cloud-format.ts,cloud-refusal.ts,cloud-status-view.ts,cloud-live.ts— so the one-shot and live entry points share formatting, refusals, and the status view without import cycles. Addscloud-run-record.ts(cloudRunState,isCloudRunActive), now the single source of truth for what a run record attests.tests/cloud-live.test.tscovers 43 cases across live rows, watch lifecycle, draining, follow content, redaction, cancellation, and refusals.Written for commit be836ba. Summary will update on new commits.
Fixes #540