feat(logs): true realtime --follow via apper SSE stream (CF tail) — DRAFT - #595
Open
davidsu wants to merge 8 commits into
Open
feat(logs): true realtime --follow via apper SSE stream (CF tail) — DRAFT#595davidsu wants to merge 8 commits into
--follow via apper SSE stream (CF tail) — DRAFT#595davidsu wants to merge 8 commits into
Conversation
…allback
--follow now connects to the new apper endpoint
GET /api/apps/{app_id}/functions-mgmt/logs/stream (SSE, same Bearer
auth as the bounded logs route) and prints log events as they arrive.
On connect failure or after one reconnect attempt it falls back to
today's 2s poll loop with a one-line stderr notice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
David's ruling from the draft review: the backfill-then-attach seam had a silent ~17-20s data hole (backfill reads the lagging bounded index while the stream tails from connect), which is data loss in a debugging tool. Guard the combination like --until/--order instead; also rename followViaStream to streamUntilExhausted so the fallback sequence below it reads as the failure path it is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
David's ruling: the server must not translate a diagnosed condition
into a bare EOF. The bridge now self-heals degraded tails invisibly
and, when it gives up, sends 'event: end' with
{reason, retriable} before closing. The cli replaces its magic
retry-counter with a reason-driven policy: retriable:false → poll
fallback; retriable:true → reconnect after 1s; bare EOF → reconnect,
giving up after 2 consecutive drops that produced no events (counter
resets on any event, so long-lived sessions never exhaust a budget).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probe found a backend outage could mute --follow indefinitely. Two gaps, both in the stream leg: a half-open connection blocks reader.read() forever (the cli never enforced keepalive arrival), and the reconnect fetch had no connect timeout against a wedged backend. Now: 60s line-silence watchdog (any line incl. ': ping' resets it — transport liveness, so quiet-but-healthy apps never trigger it) treats silence as a bare drop, and a 10s connect-phase timeout guards the fetch (body deliberately unguarded — body liveness is the watchdog's job). Worst-case mute is now bounded, ending in the loud poll error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Actions were re-enabled on the org with a policy requiring third-party actions to be SHA-locked; tag-pinned workflows now die at startup (startup_failure, 0s). Pin every uses: reference to a full commit SHA with the tag kept as a trailing comment, matching the form the already-passing workflows use for actions/checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # .github/workflows/preview-publish.yml
Contributor
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.1.9-pr.595.21d640dPrefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.1.9-pr.595.21d640d"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.1.9-pr.595.21d640d"
}
}
Preview published to npm registry — try new features instantly! |
davidsu
marked this pull request as ready for review
August 13, 2026 12:15
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.
Summary
Turns
base44 logs --followfrom a 2s poll over an eventually-consistent index (~20-30s lag) into a consumer of apper's new realtime SSE stream.Companion apper PR (the SSE endpoint): base44-dev/apper#19705.
The locked contract
GET /api/apps/{app_id}/functions-mgmt/logs/stream— app-level SSE stream on the same AppAdminRouter as the bounded logs route. App-level is correct as fact, not assumption: one WfP script per app hosts all its functions; function identity is log-line attribution, per-function tails don't exist.Authorization: Bearer <app-user JWT>(orapi_keyheader for workspace keys, which 403 on logs routes today; the stream inherits that — acceptable for draft).data: {"time": ISO, "level": "debug|info|warn|error", "function": "name"|null, "message": "..."}— one log line per event, fields additive later.functionMAY BE NULL (unattributed lines): the CLI prints them without a[fn]prefix, never drops them.?function=<csv>and?env=preview|prod(default preview) applied server-side.: pingcomment ~20s. No event ids / resume in v1 (KISS).event: end+data: {"reason": "<slug>", "retriable": true|false}before closing. Unnamed data events remain log rows; unknown event names are ignored (forward-compat). (The server's delivery mechanism changed after this PR was written — dispatcher tail → per-app Tail Worker, see the apper PR — with no change to this wire contract.)Decisions taken
fetch+ hand-parsed SSE, not ky/EventSource — EventSource can't set auth headers; ky is built for bounded requests. Naive reconnect, no state machine.--since+--followis REJECTED in v1 (superseding the earlier backfill-then-attach decision): the seam between backfill (reads the bounded index, measured lagging ~17s) and the stream (tails from connect) left a silent data hole — lines landing in neither source — which is data loss in a debugging tool under the exact flag whose purpose is "miss nothing around time X". Closing it properly means poll-until-caught-up splicing, anti-KISS for a draft. The combination now errors like--until/--order("--since cannot be combined with --follow yet");--sincealone (bounded path) is unchanged. Honesty note: the removed backfill combo was never exercised by the live demo or any spec.--levelstays client-side filtered, exactly like today's poll path (contract only putsfunction/envserver-side).endevent decides —retriable: false→ poll fallback immediately;retriable: true→ reconnect after 1s; bare EOF/error with noendevent (genuine infra drop) → reconnect, giving up only after 2 consecutive drops that produced zero events. The drop counter resets whenever a stream produces any event, so long-lived sessions never exhaust a retry budget. Initial connect 404/error → immediate poll fallback, unchanged. Every fallback prints a one-line stderr notice (empty-is-ambiguous — silence must mean silence). Prompting the user for retry-vs-poll was considered and rejected: the primary consumer is a non-TTY agent, and no prompt answer beats "keep it working".--jsonunder--followstays NDJSON (one JSON object per line) — this is today's existing behavior and the documented standing exception to the single-JSON-document rule; the stream keeps the exact sameLogEntryshape (time,level,message,source).warn→ CLI-internalwarning, matching the bounded route's existing Zod preprocess, so--level warningfilters both paths consistently.--followindefinitely — a half-open connection blocksreader.read()forever, and the reconnect fetch had no connect timeout. Now a 60s line-silence watchdog treats transport silence as a bare drop (any line including: pingkeepalives resets it — this is transport liveness, distinct from the rejected data-level client watchdog: quiet-but-healthy apps keep pinging and never trigger it), and the stream fetch gets a 10s connect-phase timeout (headers only; body liveness is the watchdog's job). Worst-case mute is bounded at ~70s, ending in the loud poll-path error. The poll path itself was never mute-capable: a failed poll iteration throws into the command error envelope (pre-existing behavior).Changes
core/resources/function/stream-api.ts(new):openLogStream(filters)— fetch with Bearer/api_key auth (reusing the auth-config helpers incl. proactive token refresh), returns an async generator of Zod-validatedStreamEvents (alog/endunion; the SSE reader tracksevent:names, so the typedendpayload is parsed rather than silently failing the log schema);parseStreamEventexported for tests.cli/commands/project/logs.ts:followLogsnow orchestratesstreamUntilExhausted(stream + one reconnect; returns only once streaming is no longer possible) → stderr notice → poll fallback; the old poll loop survives aspollLogswith a seedable cursor.--since/--until/--orderare all rejected with--follow.tests/cli/logs.spec.ts: unit tests for the SSE line parser (valid event, warn→warning, null function kept, keepalives/malformed ignored) + the--since-rejection spec. The stream itself can't be child-process integration-tested (--follownever exits) — the live two-terminal demo covers end-to-end.Demo (definition of done)
Terminal A: local apper stack with the SSE endpoint + a function that logs.
Terminal B:
BASE44_API_URL=http://localhost:<apper-port> base44 logs --followExpected: invocation lines appear near-instantly (verified live: ~0.65s end-to-end through this CLI, vs 17.4s on the poll path).
Notes
logs.spec.tsfully green (36/36 incl. the new rejection spec). Full suite at the downscope commit: 724/738 — the 14 failures are allexec.spec.ts, root-caused to machine state, not this diff: the wix-embargo npm setup on this machine interceptsregistry.npmjs.orgTLS (Deno fetches@base44/sdkat exec time and fails withinvalid peer certificate: CaUsedAsEndEntity; Deno ignores npm'scafile, and the testkit spawns the CLI with a clean env so noDENO_CERTcan reach it). The same suite was 737/737 on this branch before the embargo hosts/cert change landed.🤖 Generated with Claude Code