IMPLEMENTATION REFERENCE · PROTOCOL 3 · DRAFT
Tick Protocol Specification
| Item | Value |
|---|---|
| Protocol version | 3 |
| Status | Draft; describes the Tickloop 0.2 implementation in this repository |
| Date | 2026-10-14 |
| Reference implementation | Host types and validation in tick/protocol; Tick-side SDK in tick/sdk; example in tick/examples/counter.py |
Agent tools and package guidance
The protocol has three request methods (tick/describe, tick/read, tick/call) and one notification (tick/update). Tickloop exposes them through TickDescribe (capabilities), TickRead (current state), TickCall (declared operations) and TickUpdate (delivery event). TickRead does not repeat the descriptor or tool catalog. TickManage handles lifecycle, views, terminal input and Loop subscriptions in the Agent/host; it is not an additional Tick program RPC. Package Skills document launch commands and login prerequisites. A running instance's descriptor is the authoritative capability catalog.
1. Overview#
1.1 Purpose#
The Tick protocol defines how a terminal program exposes its state, callable operations, and business changes to a host. An AI Agent can read the program, call its tools, and resume work when new information arrives. A program implementing this protocol is a extension Tick.
An extension Tick serves two participants: a person using its terminal interface, and an Agent using the protocol through the host. Both interact with the same process and the same state.
1.2 Roles#
- Tick: the program implementing this protocol. It reads requests from standard input, writes protocol messages to standard output, and draws its interface on its controlling terminal.
- Host: the party that starts and communicates with the Tick. In Tickloop, this is the launcher process started for each Tick by Tickhost, together with Tickhost itself.
- Client: a party that uses a Tick through the host, usually an Agent's Loop. The interface between client and host is outside the scope of this specification.
- View: an interface displaying the Tick's controlling terminal, such as a Herdr Pane or Monitor overlay. Multiple views can attach to one Tick. Opening or closing a view does not affect the protocol session.
1.3 Design principles#
- The program is the tool. A Tick declares its capabilities in a tool catalog. The host does not need custom code for each kind of Tick.
- State is a complete, current snapshot. Each update carries the full current state, rather than a delta. The host keeps only the latest snapshot, so missing intermediate updates does not leave it with an inconsistent state.
- Changes can be approved. Tools declare whether they mutate external state. A call is bound to the catalog revision used for approval; changing the catalog invalidates that approval.
- An unknown outcome is not retried. Once a mutating call has been dispatched, a timeout or disconnection does not trigger an automatic resend.
- Implementation stays small. A language's standard library is enough to implement a compliant Tick.
1.4 Relationship to LSP and MCP#
Like LSP, this protocol uses JSON-RPC 2.0 over standard input and output. Unlike LSP, messages are delimited by newlines, with no Content-Length header (Section 3.1).
The tool catalog resembles MCP tools: names, descriptions, and inputs defined with JSON Schema. Three features distinguish the Tick protocol:
- A Tick also has readable, sequenced state and notifies the host when it changes.
- Tool calls are bound to a catalog
revision. - A Tick has a terminal interface for people, kept separate from protocol messages.
There is no capability negotiation in an initialization handshake. The host learns the Tick's capabilities from tick/describe.
2. Conventions#
2.1 Normative language#
The terms MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are interpreted as described in RFC 2119 and RFC 8174, only when shown in bold.
Unbolded explanations, examples, and notes are informative.
2.2 Type notation#
Message structures use TypeScript interfaces, following the notation of the LSP specification:
?marks a field that may be omitted.integeris an integer from -2^53+1 through 2^53-1;uintegeris an integer from 0 through 2^53-1.objectmeans a JSON object.- Timestamps are RFC 3339 strings.
- Paths within JSON values are RFC 6901 JSON Pointers.
type integer = number;
type uinteger = number;
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
/** A JSON Schema document whose top level is an object. */
type JSONSchema = { [key: string]: JSONValue };
2.3 Bytes and sizes#
All sizes are in UTF-8 bytes. Message body sizes are measured after the receiver re-encodes the parsed value (Section 3.5).
3. Base protocol#
3.1 Transport#
The host starts the Tick as a child process with three channels:
| Channel | Purpose |
|---|---|
| Standard input | Requests from the host to the Tick |
| Standard output | Responses and notifications from the Tick to the host; protocol messages only |
| Controlling terminal, also connected to standard error | The Tick's interface, displayed by views |
Message framing rules:
- Each message is a UTF-8 JSON value followed by a newline
\n(U+000A). - A message MUST NOT contain unescaped newlines.
- A Tick MUST NOT write non-protocol content to standard output, including debug output, progress bars, or third-party library output.
- A Tick MUST terminate the final message with a newline. Output that ends without one is a violation (Section 3.6).
- A Tick MUST write each message as a complete line. When multiple threads write to standard output, bytes from different messages MUST NOT interleave.
- Content written to standard error appears in the view. A Tick SHOULD NOT write diagnostic logs to standard error; it SHOULD use its own log file instead.
Note: This protocol does not use LSP's
Content-Lengthheader. Newline framing lets even a minimal implementation parse messages with a line reader.
3.2 Messages#
All messages follow JSON-RPC 2.0 and carry "jsonrpc": "2.0". Batch messages are not supported.
interface Message {
jsonrpc: "2.0";
}
interface RequestMessage extends Message {
/** Assigned by the host; unique among the requests in flight. */
id: uinteger;
method: string;
params?: object;
}
interface ResponseMessage extends Message {
/** The id of the request this answers. */
id: uinteger;
/** Present on success, absent on error. */
result?: JSONValue;
/** Present on error, absent on success. */
error?: ResponseError;
}
interface ResponseError {
code: integer;
message: string;
data?: JSONValue;
}
interface NotificationMessage extends Message {
method: string;
params?: object;
}
Message direction:
- Requests travel only from host to Tick. A Tick MUST NOT send requests to the host.
- The only notification from Tick to host is
tick/update. The host MUST ignore other notifications and continue the session. It MAY log or count them. - The host does not send notifications to the Tick.
Request IDs:
- The host assigns unsigned integer IDs.
- A Tick MUST respond to each request exactly once, using the same
id. - A response MUST contain exactly one of
resultanderror. - A Tick MUST NOT send a response with
id: null. If a request cannot be parsed or has no usableid, the Tick MUST discard the line without replying. It MAY record the event in its diagnostic log. The host does not send such requests.
Note: JSON-RPC 2.0 permits errors with
id: nullfor unparseable requests. This host only sends valid requests, so the Tick protocol prohibits such responses and treats them as explicit violations.
Method names beginning with tick/ are reserved for this protocol.
3.3 Concurrency and ordering#
- The host MAY send a new request before receiving a response to an earlier one.
- At most 8 requests may be in flight, including
tick/describeandtick/read(Section 3.5). - A Tick MAY process requests concurrently and MAY respond in any order. The host matches responses by
id. tick/describeandtick/readSHOULD respond promptly, without waiting for running tool calls.
A Tick may emit tick/update at any time: before initialization finishes, or before or after a response to an in-flight request. The host checks update sequence numbers in arrival order (Section 6.3).
3.4 Error codes#
Ticks use the following error codes. The host uses the code to determine whether a call may have had an effect.
| Code | Name | Meaning | Host interpretation |
|---|---|---|---|
| -32600 | InvalidRequest | Not a valid JSON-RPC request | Not executed |
| -32601 | MethodNotFound | Unknown method | Not executed |
| -32602 | InvalidParams | Invalid parameters or unknown tool | Not executed |
| -32001 | UnsupportedJsonRpc | jsonrpc is not "2.0" |
Not executed |
| -32003 | FrameTooLarge | Request exceeds the Tick's accepted size | Not executed |
| -32004 | RevisionMismatch | Call revision does not match the current catalog |
Not executed; catalog changed |
| -32000 | ToolFailed | Tool ran but failed | Executed |
| Other | — | Defined by the Tick | Executed |
- A call answered with a “not executed” error MUST have had no effect.
- If a tool fails after starting and may have had partial effects, the Tick MUST use -32000 or another “executed” error code.
messageSHOULD be a human-readable explanation.dataMAY carry structured details.
3.5 Limits#
| Item | Limit | If exceeded |
|---|---|---|
| Request | 512 KiB | The host does not send it; the Tick never receives it |
tick/describe result |
512 KiB after encoding | Initialization fails, or the request fails during normal operation |
tick/update notification |
512 KiB after encoding | Protocol violation; session ends |
| Response line | 8 MiB | Protocol violation; session ends |
Response result |
8 MiB after encoding | Outcome reported as unknown; session continues. A Tick that detects its own oversized result returns -32000 |
In-flight requests, including tick/describe and tick/read |
8 | The host does not send another request and reports busy to the client |
- Encoded size is measured after the host parses and re-encodes the value. Number representations can grow, for example from
1e15to1000000000000000.0. A Tick SHOULD leave headroom. - The host MAY impose stricter aggregate limits, such as 64 MiB of response buffers across all in-flight calls.
3.6 Violations#
The following are protocol violations. The host MUST end the session, treat all in-flight requests as having unknown outcomes, and terminate the Tick process:
- A line is not valid JSON, or
jsonrpcis not"2.0". - A line has neither an integer
idnor amethod, including responses with a null or stringid. - Output ends without a terminating newline.
- A line exceeds 8 MiB.
- A
tick/updateviolates its requirements (Section 8.2).
The following fail only the affected request; the session continues:
- A response contains both
resultanderror, or neither. - A
tick/readresult violates the sequence rules (Section 6.3). - An encoded
resultexceeds 8 MiB.
The host MUST discard a response whose id does not match an in-flight request.
4. Lifecycle#
4.1 Startup#
The host starts the Tick using the command supplied by the user:
argvmatches the command, withargv[0]resolved to an absolute path.- The working directory is the one specified when opening the Tick.
- The environment includes:
| Variable | Value |
|---|---|
TICKLOOP_INSTANCE_ID |
This run's instance identity, in the form hosted:<id> |
TERM |
xterm-256color |
TICKLOOP_INSTANCE_IDis for logging or distinguishing concurrent runs. It is absent from protocol messages and is not a credential.- A Tick MAY use its presence to distinguish host startup from a person launching it directly in a terminal.
- Other variables beginning with
TICKLOOP_are internal to the host. A Tick MUST NOT depend on them.
The Tick runs in the foreground process group of a session whose controlling terminal is the view terminal:
- Initial size is 120 columns by 30 rows.
- When a view changes size, the host resizes the terminal and the kernel sends
SIGWINCHto the Tick. - Resizing does not use the protocol.
4.2 Initialization#
After startup, the host sends, in order:
tick/describe, then validates its result (Section 5.3).tick/read, to obtain initial state.
Rules:
- Each request MUST receive a response within 10 seconds.
- The total time from startup through initialization is at most 10 seconds.
- Any failure, timeout, or invalid result fails initialization. The host terminates the Tick process; the instance does not appear in the client. The failure reason and the first 256 characters of the view screen are reported to the client.
- A Tick MAY emit
tick/updatebefore initialization completes. The host receives these updates normally.
4.3 Versioning#
The protocol field in tick/describe declares the version implemented by the Tick. Host requests do not carry a version.
- The host accepts only the version it implements. A mismatch fails initialization, with an error naming both versions, for example:
unsupported Tick protocol version 2; this host speaks Tick protocol 3. - The Tick does not receive this error. It appears in the view and is reported to the client.
- Extensions that preserve existing field meanings, such as new optional fields, do not change the protocol version.
- The host MUST ignore unknown fields in descriptors and state. A Tick SHOULD ignore unknown request parameters.
4.4 Operation#
After initialization, the host sends requests as needed:
| Event | Request |
|---|---|
| Client reads the Tick | tick/read, followed by tick/describe |
| Client prepares a tool call | tick/describe, to obtain the catalog and revision |
| Client calls a tool after approval | tick/call |
| Subscription observation stream is established or restored | tick/describe |
| No usable cached state is available at delivery time | tick/read |
- The host MAY send
tick/describeandtick/readat any time, without a limit on the total number of requests. - Both methods MUST be free of side effects and SHOULD respond within a few milliseconds.
4.5 Shutdown#
To terminate a Tick, the host sends SIGTERM to its process group. It MAY then send SIGKILL without a grace period. The host does not guarantee that it will close standard input first.
- A Tick MUST NOT rely on shutdown cleanup to save important data.
- On standard-input EOF, a Tick SHOULD wait for in-flight calls to finish, then exit.
- If a Tick exits on its own, the session ends. The host treats in-flight requests as having unknown outcomes, and the client sees that the instance has ended. The exit code is not part of the protocol.
5. tick/describe#
5.1 Request#
// method: "tick/describe"
interface DescribeParams {}
The host sends params: {}.
5.2 Result#
interface TickDescriptor {
/** The protocol version this Tick implements. */
protocol: 3;
/** What the program is, such as "example.counter". */
kind: string;
/** A short name shown to people and models. */
title: string;
/** One or two sentences on what the Tick shows and does. */
description: string;
/** Names this tool catalog. Changes whenever any tool changes. */
revision: string;
tools: TickTool[];
/** A query tool that rebuilds what latest-only updates dropped. */
recover_with?: string;
/** What the Tick declares about the data of its state. */
state?: StateDeclaration;
}
interface TickTool {
/** Unique within the catalog. */
name: string;
/** What the tool does, written for a model deciding whether to call it. */
description: string;
/** JSON Schema of `args`; its top level is an object. */
input_schema: JSONSchema;
/** Whether a call can change anything outside the call's own result. */
mutates: boolean;
}
interface StateDeclaration {
/** Arrays in `data` ordered in time. */
series?: StateSeries[];
}
interface StateSeries {
/** JSON Pointer to an array within `data`; "" is `data` itself. */
path: string;
/** The end of the array holding its newest element. */
newest: "first" | "last";
}
5.3 Validation#
The host MUST reject a result unless all of the following hold:
protocolequals the version implemented by the host.kindandtitleare nonempty after trimming whitespace.revisionis not an empty string.- Each tool's
nameis nonempty after trimming whitespace and unique within the catalog. - Each
input_schemais an object and a valid JSON Schema. - If present,
recover_withnames a catalog tool whosemutatesisfalse. - Each
state.seriespath is empty or starts with/, and paths are unique. - The encoded result is no larger than 512 KiB.
5.4 kind#
kind is the program's self-declared type. It is unrelated to the instance identity prefix (hosted:, shell:, or artifact:). Multiple instances may have the same kind.
A kind SHOULD be a lowercase, dot-separated name whose first component identifies the publisher, such as example.counter or acme.orderbook.
5.5 revision#
revision identifies the current tool catalog:
- A Tick MUST assign a new
revisionwhenever a tool's name, description, input schema, ormutateschanges. - A Tick whose catalog does not change during its lifetime MAY use a constant revision.
revisionis an opaque string. The host compares it only for equality.
5.6 mutates#
- If a call can change anything beyond its own result, the Tick MUST declare the tool as
mutates: true. This includes its own state, business content in its interface, files, and remote services. - Query tools that only read information declare
mutates: false.
The host uses mutates to decide whether approval is needed before a call and whether an unknown outcome may be retried. Declaring a mutating tool as false bypasses approval (Section 10).
6. tick/read#
6.1 Request#
// method: "tick/read"
interface ReadParams {}
6.2 Result#
interface TickState {
/** Grows with every update; see 6.3. */
seq: uinteger;
/** The complete current state, or null while the Tick has no content yet. */
data: JSONValue | null;
/** When the state was observed; the host uses its receipt time when absent. */
at?: string;
}
datais the Tick's complete current business state, not a delta from the previous state.- Null
datameans no content is available yet, for example while data is loading. - If present,
atMUST be a valid RFC 3339 timestamp.
6.3 Sequence rules#
The Tick maintains seq as the sequence number of its state:
- When sending
tick/read, the host records a read floor: the greater of the previous read result'sseqand the last update'sseqreceived so far. The read result'sseqMUST be at least this floor. - The read result is not compared with updates arriving around it. A Tick may send an update immediately before or after a read response, carrying the same or an earlier state.
6.4 Designing state#
State is a summary that helps the Agent decide whether to act, not a copy of all available data. The following guidance is informative:
- Typical state SHOULD fit within roughly 4 KiB. Subscription delivery trims to a budget of 16 KiB in total, with 4 KiB per item when multiple items are delivered.
- Expose large content through query tools and name those tools in the state. For example, a
candlestool can retrieve chart details for a time range. - Declare chronological arrays in
state.seriesso trimming preserves the newest end. Undeclared arrays retain their beginning and end. - Cursor position, scrolling, selection, colors, and other presentation details are not business state.
7. tick/call#
7.1 Request#
// method: "tick/call"
interface CallParams {
/** A tool name from the catalog. */
tool: string;
/** Arguments matching the tool's input_schema; {} when it takes none. */
args: object;
/** The revision of the catalog the call was prepared and approved against. */
revision: string;
}
7.2 Handling a call#
On receiving tick/call, the Tick:
- MUST return -32004 without executing if
revisiondiffers from its current catalog. - MUST return -32602 without executing if the tool is unknown or
argsis invalid. The host validates arguments againstinput_schemabefore sending, but the Tick SHOULD validate them independently. - Executes the tool and returns its output as
result, or an error as described in Section 3.4.
The result may be any JSON value. It goes only to the calling client and does not change the state sequence number. If the tool changes state, the Tick emits a separate tick/update (Section 8).
7.3 Timeouts and unknown outcomes#
tick/callhas a 60-second timeout. After it expires, the host stops waiting and reports an unknown outcome to the client.- The protocol has no cancellation message. A Tick may finish after timeout. A late response may be received or discarded, but it does not change the outcome already reported.
- The host MUST NOT automatically resend a call to a
mutates: truetool. When timeout, session termination, or a violation leaves the outcome unknown, the client or user decides what to do next. - Operations that may take more than 60 seconds SHOULD return immediately, continue in the background, and report progress and results through
tick/update.
7.4 Idempotency#
Because the host does not automatically retry mutating calls, the Tick does not need retry deduplication. Tools that change a remote service SHOULD include enough information in the result or state to check whether the change took effect, such as an order ID or the new value.
8. tick/update#
8.1 Notification#
// method: "tick/update"
// params: TickState
A Tick emits this notification when its business state changes. Parameters contain the complete state after the change.
8.2 Requirements#
Every tick/update MUST meet all of the following requirements. Otherwise the host ends the session as a protocol violation (Section 3.6):
datais not null.seqis strictly greater than theseqof every previoustick/update.- If present,
atis a valid RFC 3339 timestamp. - Encoded size does not exceed 512 KiB.
8.3 What counts as an update#
An update represents a business change, such as a new candle, a news item, or a value changed by a tool call.
Cursor movement, scrolling, repaints, resizing, and color changes MUST NOT emit updates.
The host retains only the latest state. At the subscription interval, it delivers that state to the client and reports how many updates were merged. Therefore:
- When output is blocked, a Tick MAY drop older updates that have not yet been written and send only the newest one.
- Update frequency SHOULD NOT exceed what the interface needs for refreshes.
9. Recovery#
Subscriptions deliver only the latest state, so intermediate changes may be merged away. Clients needing history can read it through the query tool named by recover_with:
- This tool MUST declare
mutates: false. - The host does not call it automatically. It includes the tool name with delivery, and the client decides whether to call it.
- A Tick without
recover_withdoes not provide a way to recover missed changes through this mechanism.
10. Security considerations#
- Permissions. A Tick runs with the permissions of the user who started it and can access what that user can access. Users SHOULD run only Ticks they trust.
- Approval relies on declarations. The host uses
mutatesto determine whether approval is needed. Tick authors MUST declare it truthfully. Hosts and clients SHOULD show the tool name, arguments, and description so users can assess the call. - Catalog changes invalidate approval. Approval is bound to
revision. A call prepared against an old catalog is rejected with -32004, preventing a tool from changing meaning after approval. - Output is untrusted data. State, tool results, and error messages enter the model context. Hosts and clients MUST treat them as data rather than instructions, and must not expand authorization based on their contents. Ticks SHOULD NOT put credentials, tokens, or sensitive personal information in state.
- Standard-output integrity. Non-protocol output ends the session. A Tick SHOULD redirect libraries that may print to standard output as early as startup.
- Instance identity is not a credential.
TICKLOOP_INSTANCE_IDis only for logging and distinguishing runs, not authentication.
Appendix A Example session#
→ marks a line written by the host to the Tick's standard input. ← marks a line written by the Tick to standard output. Some lines are wrapped for readability; on the wire, each message occupies one line.
→ {"jsonrpc":"2.0","id":1,"method":"tick/describe","params":{}}
← {"jsonrpc":"2.0","id":1,"result":{"protocol":3,"kind":"example.counter","title":"Counter",
"description":"Keyboard and tool calls share one value.","revision":"1",
"tools":[{"name":"increment","description":"Add an integer to the counter.",
"input_schema":{"type":"object","properties":{"amount":{"type":"integer"}},
"required":["amount"],"additionalProperties":false},
"mutates":true},
{"name":"history","description":"Read the last 256 changes.",
"input_schema":{"type":"object","additionalProperties":false},"mutates":false}],
"recover_with":"history","state":{"series":[{"path":"/recent","newest":"last"}]}}}
→ {"jsonrpc":"2.0","id":2,"method":"tick/read","params":{}}
← {"jsonrpc":"2.0","id":2,"result":{"seq":0,"data":{"value":0,"recent":[]}}}
(the user approves increment with amount 2; the call carries revision "1")
→ {"jsonrpc":"2.0","id":3,"method":"tick/call","params":{"tool":"increment","args":{"amount":2},"revision":"1"}}
← {"jsonrpc":"2.0","method":"tick/update","params":{"seq":1,"data":{"value":2,"recent":[{"seq":1,"value":2}]}}}
← {"jsonrpc":"2.0","id":3,"result":{"value":2}}
(a person types + in the Tick's view)
← {"jsonrpc":"2.0","method":"tick/update","params":{"seq":2,"data":{"value":3,"recent":[{"seq":1,"value":2},{"seq":2,"value":3}]}}}
(a call prepared against an older catalog)
→ {"jsonrpc":"2.0","id":4,"method":"tick/call","params":{"tool":"increment","args":{"amount":1},"revision":"0"}}
← {"jsonrpc":"2.0","id":4,"error":{"code":-32004,"message":"the Tick Tool catalog changed; describe the Tick and prepare the call again"}}
Appendix B Minimal implementation#
This Tick uses only the Python standard library. It stores a single note and exposes one mutating tool. It satisfies the Tick-side MUST requirements of this specification, but does not process requests concurrently or draw an interface.
#!/usr/bin/env python3
import json, sys
REVISION = "1"
TOOLS = [{"name": "set_note", "description": "Replace the note.", "mutates": True,
"input_schema": {"type": "object", "properties": {"text": {"type": "string"}},
"required": ["text"], "additionalProperties": False}}]
seq, note = 0, None
class RpcError(Exception):
def __init__(self, code, message):
super().__init__(message)
self.code = code
def send(frame):
print(json.dumps({"jsonrpc": "2.0", **frame}), flush=True)
def handle(method, params):
global seq, note
if method == "tick/describe":
return {"protocol": 3, "kind": "example.note", "title": "Note",
"description": "One line of text.", "revision": REVISION, "tools": TOOLS}
if method == "tick/read":
return {"seq": seq, "data": None if note is None else {"note": note}}
if method == "tick/call":
if params.get("revision") != REVISION:
raise RpcError(-32004, "the tool catalog changed")
text = params.get("args", {}).get("text")
if params.get("tool") != "set_note" or not isinstance(text, str):
raise RpcError(-32602, "set_note takes a text string")
seq, note = seq + 1, text
send({"method": "tick/update", "params": {"seq": seq, "data": {"note": note}}})
return {"saved": True}
raise RpcError(-32601, f"unknown method {method}")
for line in sys.stdin:
request = json.loads(line)
try:
send({"id": request["id"], "result": handle(request["method"], request.get("params", {}))})
except RpcError as error:
send({"id": request["id"], "error": {"code": error.code, "message": str(error)}})
Appendix C Implementation checklist#
- Write only protocol messages to standard output, one complete, newline-terminated line at a time, atomically across threads.
- Respond exactly once to each request with the matching
id; never respond withid: null. - Make
tick/describeandtick/readside-effect-free and prompt. - Change
revisionwhenever the catalog changes. Reject mismatched revisions with -32004 without executing. - Declare every mutating tool as
mutates: true. - Use “not executed” error codes only when the call has had no effect.
- Send complete, non-null state in every
tick/update, with strictly increasingseq. - Keep read sequence numbers from moving backwards or falling below the last emitted update.
- Do not emit updates for presentation changes such as cursor movement, scrolling, or repaints.
- Keep state within a few KiB, expose large content through query tools, and declare time series in
state.series. - Run operations longer than 60 seconds in the background and report results through updates.
- Exit promptly on
SIGTERMor standard-input EOF, without relying on shutdown cleanup.