feat: durable command lifecycle logging in the executor (command/run + command/done)
CommandService.execute appends the log-only pair around every resolved handler — run before invocation, done at settlement, including thrown and aborted handlers (kind:'error'); admission misses log nothing. commandId is minted monotonically per instance; per-session appends serialize through a tail queue over SessionStore.appendOutOfBand (zero-step wrap on an idle log, direct join inside an open turn). The invariant companion now asserts the pairing relation (unique run ids; a done requires a prior in-log run). CommandSource is a minimal merge-extensible map (user variant only). Dependent benches mount SessionStore; TUI/e2e snapshots re-recorded for the executor's durable-append timing and the /status event counts.
This commit is contained in:
@@ -8,7 +8,7 @@ Plugin-owned human-command registry consumed by interactive UI adapters. The [pl
|
||||
|
||||
`ctx.commands.register(definition)` registers one lowercase command name, description, optional unstructured-input hint, and abortable handler. A registered command is available to every composed command adapter; a plugin that is incompatible with a deployment does not register there. A plain-context registration is global. A command-producing plugin mounted beneath `agent.ctx` declares its own `commands` injection and creates an exact agent-scoped definition; it shadows a global definition with the same name. This child-injection shape preserves the agent scope without making the core agent loop depend on a UI service. Duplicate names within one layer fail during registration. Every disposer is the exact Cordis effect disposer, and registration or removal notifies every `commands/change` observer so live adapters can refresh discovery; observer failures are logged and cannot veto the registry mutation or starve later observers.
|
||||
|
||||
`list(agent)` returns immutable, name-sorted descriptors after scoped shadowing. `find(agent, name)` returns the corresponding definition. `execute(agent, line, signal)` uses `parseCommand()` and runs only a known command, returning `undefined` for invalid syntax or unknown names.
|
||||
`list(agent)` returns immutable, name-sorted descriptors after scoped shadowing. `find(agent, name)` returns the corresponding definition. `execute(agent, line, signal)` uses `parseCommand()` and runs only a known command, returning `undefined` for invalid syntax or unknown names. A resolved command's lifecycle is durably logged on the receiving agent's session as the log-only pair `command/run` (before the handler, with a minted `commandId`, the exact line, and the issuing `CommandSource`) and `command/done` (at settlement, with the outcome kind and verbatim text; a thrown or aborted handler settles as `kind: 'error'`). Admission misses log nothing. Lifecycle appends are serialized per session through `SessionStore.appendOutOfBand`, so the service requires a composed `sessions` service.
|
||||
|
||||
`parseCommand()` recognizes a slash at byte zero, a lowercase name containing letters, digits, `_`, or `-`, and either end-of-input or whitespace. It returns every byte after the name as `rawInput`, including separator whitespace; consumers own their command-specific grammar and may normalize only what that grammar permits.
|
||||
|
||||
@@ -37,5 +37,4 @@ Registry metadata, command input, and direct output never enter a model request
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Only unstructured text input** — forms, completion schemas, and typed arguments remain command-owned parsing concerns.
|
||||
- **No persisted command output** — adapters display results live, but the generic registry does not add them to the session log or reconstruct them after reconnect.
|
||||
- **Cooperative side-effect cancellation** — dispatch stops awaiting on abort; handlers must honor the signal to stop work that has already escaped into external systems.
|
||||
|
||||
Reference in New Issue
Block a user