Conversation
convert becomes a standalone action alongside the existing click command, both delegating to convert_can_trace so the two surfaces cannot diverge. It runs with the extension stopped: no bus, no live connection. - database_file is optional and falls back to the first database_file in the saved extension config, which is at-rest state (written on Start, persists across stop). Applied in the action body rather than as a schema default, because the inventory is dumped at package time before any config exists. - Path inputs use text + file_path_picker, not @action.file — the latter means 'upload as a base64 data URL' and validates the value as one, so it rejects filesystem paths. - SERVICE_NAME moves to the package and is re-exported from main.py, so the live namespace (init(name=...)) and the packaged at-rest inventory read one constant and cannot drift apart.
The constant names the prefix actions are addressed under — `can/convert` — so name it for that rather than for the SDK's `init(name=...)` parameter. Value is unchanged, so live and at-rest action paths are unaffected. The standalone inventory dump reads this constant from the entry module, and `init()` reads it for the live registration, so one declaration keeps both halves agreeing.
Actions signal failure by raising. Returning {"status": "error"} is a
normal return, which the harness reads as "no verdict" and the wire maps
to DONE, so every failed conversion reported success to the CLI (exit 0),
the app, and the AI. Fixed at every site that returned an error payload.
convert:
- timeout 600s -> 1800s. Conversion is I/O bound over files that reach
multi-GB; the ceiling is for the pathological case, not an expectation.
The AI tool bridge still clamps its own calls to 5 min.
- new `open_on_complete` (default false) hands the finished .trz to the
desktop app via the OS file association. The opener gets its own
session and detached stdio: a standalone action runs in a setsid
one-shot whose process group the supervisor kills on abnormal exit, and
a child inheriting the capture pipes holds them open past the action's
return, which stalls the run and then trips the terminate path. Failing
to open is reported in-band rather than raised, since the trace exists
by then and raising would invite a re-run of finished work.
converter:
- decode through zelos_can.CanDecoder (Rust, GIL released) instead of the
cantools-backed CanCodec. Both call sites share one `_make_decoder`.
- decoder.flush() replaces a time.sleep(2.0), which cost two seconds per
conversion and never actually guaranteed the tail had landed.
- metrics sampled on the logging cadence rather than three Rust crossings
per frame.
Source name stays `can_codec` and timestamp_mode stays `absolute`: the
first is the leading segment of every signal path in the output, and the
second preserves each frame's own timestamp. Changing either would
silently re-path or re-stamp every future conversion.
Timestamps: a frame stamped exactly 0.0 was being re-stamped with wall-clock now. `decode_message` treats 0.0 as "no hardware timestamp", which is right for a live bus (python-can reports 0.0 when the driver gives nothing) and wrong for file replay, where 0.0 is the first frame of any relative-timestamped capture. Converting a candump whose first line reads (0.000000) produced a trace spanning from that frame's wall-clock stamp to the rest of the file's epoch times, non-deterministically. Timestamps are now passed explicitly via decode_frame, and the timing stats use `is not None` rather than truthiness for the same reason. export_trace_to_log: the zero-frames branch still returned an error-shaped payload. A plain return means "no verdict", which the wire maps to DONE, and export_to_candump writes no file in that case — so a caller chaining on exit status proceeded against a file that does not exist. Raises now, like every other failure path. convert: resolve the output path. A relative path resolved against the extension's working directory rather than the caller's, and an unresolved path beginning with `-` reached the platform opener as a flag (`open -a.trz` parses as `open -a <app>`). Tests: the standalone `convert` had none — the existing suite covered `convert_trace_file`, a different action. Adds its five raise paths, plus the `_open_in_app` spawn contract (start_new_session and the DEVNULL trio, both load-bearing against the supervisor's process-group kill and pipe drain) and the rule that a failed open never fails a finished conversion.
`extension.toml` declares `name = "CAN"`, which is what a user sees in the extension list, but actions were served under `can/`. The address someone reads and the address they type should be the same string. `ACTION_PREFIX` is already the single source for both surfaces — the live `zelos_sdk.init(name=ACTION_PREFIX, actions=True)` registration and the at-rest inventory `main.py` re-exports — so one edit moves both. Verified via the harness dump: `CAN/convert`. The AI tool slug is unaffected. `action_tool_name` lowercases every ASCII alphanumeric, so `CAN/convert` still surfaces as `ext_can_convert` and no saved tool name or disabled-set entry changes.
The rename left four comments describing the old address, one of which gave two different namespaces for the same surface in a single sentence. Since the webapp now discovers the prefix rather than assuming it, the docs say `<prefix>/<name>` instead of baking in a literal that the code deliberately does not assert either.
Packaging never generated actions.json, so a registry install advertised no standalone actions at all: the at-rest convert existed only on developer machines, and every test gate stayed green while it did. Generate it from the SDK harness before the archive is assembled, and fail the run rather than warn -- an unattended release whose only gate is "did an archive appear" would otherwise ship a stale or missing inventory. Conversions unlinked the destination and then wrote it in place, so a run that raised or was killed left a truncated trace where a good one had been, indistinguishable from a complete capture. Stage into a sibling temp dir and rename onto the destination only on a clean return. Same shape fixed in convert_trace_file and export_trace_to_log, the latter of which could delete a good log and then raise without writing anything. The Windows opener passed a caller-supplied path through cmd, which re-parses it: list2cmdline quotes for spaces but not for cmd metacharacters, so a space-free path could select a command. os.startfile hands the path to ShellExecuteW with no shell at all. Claude-Session: https://claude.ai/code/session_01KehJeZkLpffYEhp7Y2Ai2m
tkeairns
force-pushed
the
standalone-actions
branch
from
August 20, 2026 23:01
b2b2641 to
69b31bc
Compare
Staging into a scratch dir and renaming on success was more machinery than a convert tool warrants. The plain behavior is the expected one: write to the path given, require overwrite to replace an existing file, and whatever reaches disk is what the run produced -- a Ctrl+C partway through leaves a partial trace, which is fine and is what every other conversion tool does. Claude-Session: https://claude.ai/code/session_01KehJeZkLpffYEhp7Y2Ai2m
Standalone actions need `zelos_sdk.extensions.actions`, which first ships in 0.0.11a1. The floor has to name the prerelease to be satisfiable by one at all: a resolver offered `>=0.0.10` picks the newest *final* release, which has no harness, so an installed extension advertises its at-rest actions and fails to run them. The lockfile is the half that decides. Installs resolve through `uv sync --frozen`, so a floor raised without relocking changes nothing. Verified on a packaged install built only from this lockfile, with no workspace SDK overlaid: zelos-sdk 0.0.11a1 from PyPI, `CAN/convert` listed and executed while the extension was stopped. Claude-Session: https://claude.ai/code/session_01KehJeZkLpffYEhp7Y2Ai2m
This was referenced Sep 17, 2026
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.
Draft — blocked on a zelos-sdk release, see below.
Companion to zeloscloud/src#1345, which lets the agent serve and execute an extension's actions while the extension is stopped.
What
convertbecomes a standalone action alongside the existing click command. Both delegate toconvert_can_trace, so the two surfaces can't diverge. It runs with the extension stopped — no bus, no live connection — which is what makes it useful: converting a CAN log has never needed a running extension, but until now it was only reachable by hand from a source checkout.database_fileis optional and falls back to the firstdatabase_filein the saved extension config. Config is at-rest state (written on Start, persists across stop), so this resolves whether or not the extension is running. It's applied in the action body rather than as a schema default, because the inventory is dumped at package time, before any config exists.@action.text(..., widget="file_path_picker"), not@action.file— the latter means "upload as a base64 data URL" and validates the value as one, so it rejects filesystem paths.SERVICE_NAME→ACTION_PREFIX, moved to the package and re-exported frommain.py. The standalone dump reads it from the entry module andinit()reads it for live registration, so one declaration keepscan/convertidentical on both halves.Blocked on
pyproject.tomlpinszelos-sdk>=0.0.10, butstandalone=Trueandzelos_sdk.extensions.actionsare unreleased. Packaging today fails the inventory dump with "No module named zelos_sdk.extensions.actions", which the agent treats as harness-unavailable — so the extension would ship with no standalone inventory. This merges once the SDK release goes out, and the floor here should be raised to that version in the same change.Verification
Run against the local SDK via a
PYTHONPATHoverlay:action_prefix: canwithconvertonly — the seven runtime-dependent actions are correctly excluded..trz.database_fileomitted entirely, it resolved the configured DBC out of the installed extension'sconfig.jsonwhile stopped.