Skip to content

Agent: add zmodem_send for unattended PC→device upload - #27

Merged
typeless merged 1 commit into
mainfrom
feature/agent-zmodem-send
Jul 22, 2026
Merged

Agent: add zmodem_send for unattended PC→device upload#27
typeless merged 1 commit into
mainfrom
feature/agent-zmodem-send

Conversation

@typeless

Copy link
Copy Markdown
Owner

Adds a zmodem_send agent method so a client can push a local file to the device over the session Tera Term already holds open — no file-picker, no DDE, no window relaunch. It closes the one remaining gap in hands-off ZMODEM.

Why

Device→PC download is already unattended via the built-in auto-receiver (verified this cycle: 128 KB byte-perfect). The reverse was stuck:

  • Built-in auto-send stops at a GUI file-picker (ZMODEMStartSend(NULL,...)_GetMultiFname).
  • ttpmacro cannot attach to an interactively-launched window — it registers no DDE server without /D= (InitDDE guarded by strlen(TopicName)>0; the live window's cmdline has no /D=, verified), and its connect spawns a new ttermpro that fights for the COM port.

So neither existing route is unattended. zmodem_send drives Tera Term's own send engine over the existing session instead.

How

  • Agent requests already run on the GUI thread (the hidden A.wnd), so be_zmodem_send calls ZMODEMStartSend inline — no cross-thread marshaling.
  • The transfer is async on the protocol pump; its outcome is surfaced through a new transfer object in status — a tagged union (state: idle|active|done, with ok present only when done), fed by a hook at the single ProtoEnd completion funnel.
  • Client flow: arm the device receiver (rz -b -y), call zmodem_send, poll status.transfer until done.
  • Reuses the existing send-armed gate — grants nothing beyond send_bytes (reads a local file, streams it out the serial line, both already within the arm's scope).

Scope

Local (focused) window only. A foreign window's transfer is neither driveable nor observable from this process (status reads this window's cv_ProtoFlag), so cross-window ZMODEM is deferred and returns NOTALLOWED; send_bytes/text still hand off cross-window via the shm queue.

Verification — split, and stated honestly

Host-tested (ASan/UBSan, all green):

  • test_zmodem_send — JSON dispatch / path+binary parse / send-armed gate (4 cases)
  • test_status_transfer — the transfer tagged-union across all 4 states
  • test_agent_mcptools/list count + zmodem_send discoverability

NOT built by the host suite — needs the CI product build + hardware run:

  • agent_server.cpp (be_zmodem_send, status fields) and filesys_proto.cpp (ProtoEnd hook, ProtoGetLastResult) are Windows-only. They are code-reviewed for signature/type/linkage but uncompiled and unexercised here.

The two gates that remain: (1) the clang-cl+xwin build compiles the Windows halves; (2) a hardware e2e (rzzmodem_send → poll status.transfer → checksum) proves behavior — and confirms driving the built-in sender over the existing session sidesteps the ~64 KB client-bridge ceiling.

TDD trail

RED→GREEN on the parse/gate layer (test written and observed failing — unknown methodok:false — before call_zmodem_send existed), then the GUI-side and status hook added with the one host-testable half (surfacing) covered by test_status_transfer.

https://claude.ai/code/session_01YEkLFtTYQZuQHgQP5qYtq7

Adds a `zmodem_send` agent method so a client can push a local file to the
device over the session Tera Term already holds open -- no file-picker, no
DDE, no window relaunch. This is the reverse of the already-working built-in
auto-receive (device->PC), and it closes the one gap in hands-off ZMODEM: the
built-in auto-*send* path stops at a GUI file-picker (ZMODEMStartSend(NULL,...)
-> _GetMultiFname), and ttpmacro cannot attach to an interactively-launched
window (it registers no DDE server without /D=), so neither existing route is
unattended.

Because agent requests are dispatched on the GUI thread (the hidden A.wnd),
be_zmodem_send calls ZMODEMStartSend inline -- no cross-thread marshaling. The
transfer runs async on the protocol pump; its outcome is surfaced through a new
`transfer` object in `status` (a tagged union: state idle|active|done, with
`ok` only when done), fed by a hook at the ProtoEnd completion funnel. The
client arms the device receiver (`rz`), calls zmodem_send, then polls
status.transfer until done.

The method reuses the existing send-armed gate, so it grants no capability
beyond send_bytes: it reads a local file on the Windows box and streams it out
the serial line, both already within the arm's scope.

Scope: local (focused) window only. A foreign window's transfer would be
neither driveable nor observable from this process (status reads this window's
cv_ProtoFlag), so cross-window ZMODEM is deferred and returns NOTALLOWED;
send_bytes/text still hand off cross-window via the shm queue.

Host-tested (ASan/UBSan): the JSON dispatch/parse/gate (test_zmodem_send), the
transfer tagged-union surfacing (test_status_transfer), and MCP tools/list
discovery (test_agent_mcp). The Windows-only halves -- be_zmodem_send in
agent_server.cpp and the ProtoEnd hook in filesys_proto.cpp -- are not built by
the host suite; they need the clang-cl+xwin product build to compile and a
hardware run to verify behavior.

Claude-Session: https://claude.ai/code/session_01YEkLFtTYQZuQHgQP5qYtq7
@typeless
typeless merged commit 9f8ccdb into main Jul 22, 2026
14 of 15 checks passed
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