Agent: add zmodem_send for unattended PC→device upload - #27
Merged
Conversation
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
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.
Adds a
zmodem_sendagent 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:
ZMODEMStartSend(NULL,...)→_GetMultiFname).ttpmacrocannot attach to an interactively-launched window — it registers no DDE server without/D=(InitDDEguarded bystrlen(TopicName)>0; the live window's cmdline has no/D=, verified), and itsconnectspawns a newttermprothat fights for the COM port.So neither existing route is unattended.
zmodem_senddrives Tera Term's own send engine over the existing session instead.How
A.wnd), sobe_zmodem_sendcallsZMODEMStartSendinline — no cross-thread marshaling.transferobject instatus— a tagged union (state: idle|active|done, withokpresent only whendone), fed by a hook at the singleProtoEndcompletion funnel.rz -b -y), callzmodem_send, pollstatus.transferuntildone.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 (
statusreads this window'scv_ProtoFlag), so cross-window ZMODEM is deferred and returnsNOTALLOWED;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— thetransfertagged-union across all 4 statestest_agent_mcp—tools/listcount +zmodem_senddiscoverabilityNOT built by the host suite — needs the CI product build + hardware run:
agent_server.cpp(be_zmodem_send, status fields) andfilesys_proto.cpp(ProtoEndhook,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 (
rz→zmodem_send→ pollstatus.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 method→ok:false— beforecall_zmodem_sendexisted), then the GUI-side and status hook added with the one host-testable half (surfacing) covered bytest_status_transfer.https://claude.ai/code/session_01YEkLFtTYQZuQHgQP5qYtq7