Skip to content

0.5.0 (4/5): telemetry switches, dg init, JSON output and listen output formats - #163

Closed
dg-coreylweathers wants to merge 13 commits into
release/0.5.0-3-transportfrom
release/0.5.0-4-init-output
Closed

dg-coreylweathers wants to merge 13 commits into
release/0.5.0-3-transportfrom
release/0.5.0-4-init-output

Conversation

@dg-coreylweathers

Copy link
Copy Markdown
Contributor

Part 4 of 5 of the 0.5.0 release, stacked on part 3. Review in order. Merge only after the PR below it has merged, and only after 0.4.1 is released.

Telemetry switches (DO_NOT_TRACK, CI, DEEPCTL_TELEMETRY=log|off) are accepted; this version still sends no telemetry. dg init clones only Deepgram templates, keeps repository settings from running programs, can resume, and needs --yes for setup steps outside a terminal. JSON output off a terminal skips rich. dg listen prints the server body for -o json, adds -o text, and saves large results to a file in agent sessions.

Commits

  • bbe70a8 feat(telemetry): honor DO_NOT_TRACK and CI
  • 96dc9dc perf(telemetry): skip the flush when no report was queued
  • 120bfde feat(telemetry): add DEEPCTL_TELEMETRY=log and =off
  • afc76d4 docs(telemetry): publish the report's field schema
  • c5ad9a4 fix(init): clone only Deepgram templates, with plain names
  • d4eed15 fix(init): keep repository settings from running programs
  • 454a6e0 feat(init): resume a rerun in a clone dg init recorded
  • b705938 feat(init): setup steps need --yes outside a terminal
  • 2b46aa4 perf(output): write JSON off a terminal without rich
  • a39ba78 test(transport): close the shared client after each test
  • c92df21 feat(listen): print the server's body for prerecorded -o json
  • c08fb54 feat(listen): add -o text for the transcript alone
  • 48efd85 feat(listen): save large results to a file in agent sessions

Notes

  • Tests for the full 0.5.0 head (Docker, Linux): Python 3.10 5,616 passed, 91 skipped, 0 failed; Python 3.14 5,617 passed, 90 skipped, 0 failed; lint, types, lock, floors, READMEs and build pass. CI on this PR runs the full matrix.
  • After 0.4.1's release PR merges, this stack is rebased onto main and the lockstep versions are re-baselined to the versions that release set.
  • Merge with a merge commit, not a squash: release-please reads the individual commits.

🤖 Generated with Claude Code

Error reports now also stay off when DO_NOT_TRACK or CI is set, with
the same rule as DEEPCTL_TELEMETRY_DISABLED: any non-empty value except
0, false, no or off. They are checked after the dg-specific switch, and
the help footer then reads "Telemetry is off.".

The README's opt-out list and the help footer name both. Both
variables join the root --help cache key, since the footer depends on
them.

Telemetry's own tests clear both variables, so they behave the same on
a CI runner.
When the report is dropped before it is queued (a closed-stream error,
or nothing left to send), there is nothing to wait for, so the flush
is skipped. Tests pin the two timing cases: a refused connection ends
the report at once, and an unanswered one is held to the budget.
With DEEPCTL_TELEMETRY=log, an unexpected error prints the report dg
would send to stderr as one JSON line, and nothing is sent: the SDK
gets a transport that only writes to stderr and no destination. It
changes nothing when telemetry is off.

DEEPCTL_TELEMETRY=off (or 0, false, no, disabled) turns telemetry off,
like DEEPCTL_TELEMETRY_DISABLED. Any other value keeps the default.
The variable joins the root --help cache key.
docs/telemetry.md lists every field a report can hold, the opt-outs in
the order they are checked, the time budget and the log mode. A test
sends a report through the real error path (an exception chain, with
the command context filled in) and checks that its fields are exactly
the documented ones. A missing doc fails the test.
The template service supplies both the repository URL and the template
name, and dg init used them as given. It now clones only
https://github.com/deepgram/<repo> and
https://github.com/deepgram-starters/<repo>: no other host, scheme,
port, user name, query, fragment, extra path segment, whitespace or
control character.

Both the repository name and the template name must be plain: ASCII
letters, digits, ".", "_" and "-", not starting with "-". Anything else
is an error before the name is used in a path, a command or output,
and nothing is cloned.
Every git command dg init runs now goes through one helper that adds
--no-pager and -c core.fsmonitor=false, core.hooksPath=/dev/null (NUL
on Windows), core.pager=cat and diff.external= to the command line, so
a repository's own configuration can't name a program for git to run.
The commands dg runs inside a repository (rev-parse, ls-files and
check-ignore) apply no filters and open no connection. The .env check
also skips the global excludes file, which as a FIFO would block it.

Inherited GIT_* variables are still dropped for commands in a
repository; the clone keeps them, as before.
dg init failed on any existing directory. It now records each clone it
makes in its own state directory (~/.deepctl/init-clones, 0700): one
owner-only file per clone, named after the directory's real path,
holding that path, the template, its repository, the commit cloned,
the phases done and the directory's device and inode. The file is
written atomically and never through a link.

A rerun into that directory resumes from the record, without cloning
again or repeating finished setup, only when all of these hold:
- the record names this real path and the template's repository;
- the directory, opened without following a link, has the recorded
  device and inode, so one deleted and made again at the path doesn't
  count, and it and its .git (a real directory) belong to this user
  (on Windows, where inodes and owners aren't reliable, only the link
  and .git checks apply);
- git then finds it the top of its own work tree, at the recorded
  commit.

Nothing inside the directory is trusted for this, and no git command
runs in it before the checks above. Any other existing directory,
however it looks, is invalid_argument: no .env is written and no setup
or start step runs. An empty directory can be cloned into.
An AI agent session, CI or a run without a terminal no longer runs a
template's setup commands unasked, and never starts the app on its own.

- New -y/--yes approves the setup commands. Without an AI agent, an
  explicit --install approves them too; in an agent session only --yes
  does. Otherwise init returns confirmation_required (exit 1) with
  details.flag --yes and a next_step that finishes without them:
  `{prog} init {template} --dir {dest} --no-install --no-start`.
- Commands the template service lists are confirmed before anything is
  cloned, and the refusal names exactly what --yes runs: the listed
  commands, or make check-prereqs and make init if the repository has
  a Makefile (whose targets take precedence, as before). A Makefile
  with no listed commands is confirmed after the clone; next_step then
  resumes in it.
- The start step runs only with --start; otherwise it is
  confirmation_required with next_step `... --no-start`.

A person at a terminal keeps the existing prompts.
Machine JSON on a pipe or file went through rich's print_json, which
re-parses the text and renders it as highlighted segments: 11 s for a
14 MB result. Off a terminal, write the same text rich printed
(json.dumps with indent 2 and ensure_ascii off, plus a newline) to the
console's stream in one write. A terminal still gets highlighting, and
--quiet still prints nothing.
The transport tests' fixture swapped the shared httpx client out with
monkeypatch and never closed the one a test had built, so a keep-alive
connection to that test's fake server stayed open until the collector
found it. It then raised a ResourceWarning inside whichever later test
happened to be running, which failed
test_find_available_port_closes_sockets in debug-browser. Close the
shared and verified clients a test made, and restore the ones before.
Prerecorded listen under -o json (asked for or implied) now posts
through the shared HTTP client and prints the response body as the
server sent it, decoded, plus a newline. The SDK model round trip
(construct_type, model_dump, re-serializing) is gone from this path:
a 50k-word result takes about 0.3 s instead of 10 s.

The body is printed only after it was read completely with a 2xx
status and parsed as a JSON object; the bytes printed are the ones
read. A 2xx body that isn't a JSON object (a portal's HTML page, a
stream cut short, invalid UTF-8, an array) is now an error,
network_unavailable with exit 1, as are a coding dg can't decode, a
corrupt one, and a body cut short in transfer. Error statuses map to
the usual coded errors, so stdout holds the whole body or the error
envelope, never part of a body. Requests retry as the SDK does, before
any response byte is read, and identify themselves with the SDK's
User-Agent and X-Fern headers. zstd is accepted only where it can be
decoded.

Keys the server didn't send no longer appear as null, and dates keep
the server's form. DEEPCTL_LISTEN_JSON=sdk prints the SDK's shape as
before; any other value is ignored with one warning. -o yaml now
prints the body as YAML instead of the table. Other backends keep the
SDK path.
dg listen -o text prints only the transcript (speaker-labelled with
--diarize), for files, URLs and live streams. It isn't a machine format:
errors stay on stderr. A command can now declare output
formats it takes after its name; the root -o is unchanged. Help specs
regenerated.
When an AI agent marked the environment, the format was chosen by
detection rather than asked for, stdout is neither a regular file nor
/dev/null, and the prerecorded result is over 64 KiB, dg listen saves
the full result and the plain transcript to a pair of 0600 files and
prints a summary instead: their paths, sizes, a 2 KiB preview, the
request ID and duration. One stderr line names the file, even under
--quiet. --full-output, on listen or the root, prints it all.

Files go in ~/.deepctl/results/ (0700), else in a private per-user
directory under the temp directory; a planted or shared one is refused
with a warning and a fresh one is used. They are created exclusively,
opened without following links and fsynced before stdout is written.
Pairs older than a day, then the oldest past 500 MB, are pruned. When
neither place can be written, the run fails with state_unwritable and
never prints the full result. Help specs regenerated.
@dg-coreylweathers

Copy link
Copy Markdown
Contributor Author

Consolidated into #165, which has all 74 commits of 0.5.0 in one PR.

@dg-coreylweathers
dg-coreylweathers deleted the release/0.5.0-4-init-output branch November 7, 2026 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant