@xingular/harness 1.6.0-unstable → 2.0.1-unstable
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/cli.js +23375 -19883
- package/dist/sdk/cli.js +16 -1
- package/dist/sdk/harness.js +2 -1
- package/dist/sdk/prompt/reporting.d.ts +1 -1
- package/dist/sdk/prompt/reporting.js +8 -3
- package/dist/sdk/server/methods.js +12 -3
- package/dist/sdk/server/serve.d.ts +5 -2
- package/dist/sdk/server/serve.js +10 -3
- package/dist/sdk/server/socket.d.ts +28 -0
- package/dist/sdk/server/socket.js +122 -0
- package/dist/sdk/sessions/contracts.d.ts +14 -1
- package/dist/sdk/sessions/contracts.js +9 -0
- package/dist/sdk/sessions/extensionUi.js +4 -4
- package/dist/sdk/sessions/live.d.ts +101 -0
- package/dist/sdk/sessions/live.js +265 -0
- package/dist/sdk/sessions/registry.d.ts +12 -0
- package/dist/sdk/sessions/registry.js +3 -0
- package/dist/sdk/sessions/runner.d.ts +25 -3
- package/dist/sdk/sessions/runner.js +171 -31
- package/dist/sdk/sessions/sessions.d.ts +49 -0
- package/dist/sdk/sessions/sessions.js +165 -8
- package/dist/sdk/{terminal → sessions}/stallWatch.d.ts +4 -0
- package/dist/sdk/{terminal → sessions}/stallWatch.js +10 -4
- package/dist/sdk/sessions/subagentService.js +1 -1
- package/dist/sdk/terminal/controller.js +1 -3
- package/dist/sdk/terminal/daemon.d.ts +44 -0
- package/dist/sdk/terminal/daemon.js +264 -0
- package/dist/sdk/terminal/liveTerminal.d.ts +96 -0
- package/dist/sdk/terminal/liveTerminal.js +436 -0
- package/dist/sdk/terminal/standalone.d.ts +19 -1
- package/dist/sdk/terminal/standalone.js +75 -25
- package/dist/sdk/terminal/terminals.d.ts +43 -0
- package/dist/sdk/terminal/terminals.js +171 -14
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +24 -2
- package/dist/sdk/terminal/tui/harnessRuntime.d.ts +8 -0
- package/dist/sdk/terminal/tui/harnessRuntime.js +771 -0
- package/dist/sdk/terminal/tui/thinkingLevels.d.ts +11 -0
- package/dist/sdk/terminal/tui/thinkingLevels.js +31 -0
- package/dist/terminal-opentui.js +2013 -975
- package/dist/terminal-pi.js +9 -0
- package/docs/architecture/02-sessions-and-runs.md +61 -3
- package/docs/architecture/04-prompt.md +8 -6
- package/docs/architecture/10-server-protocol.md +23 -1
- package/docs/architecture/11-terminal.md +67 -3
- package/docs/architecture/12-storage.md +3 -1
- package/docs/architecture/14-surfaces-and-parity.md +4 -0
- package/docs/architecture/15-remote-control.md +3 -1
- package/docs/architecture/README.md +5 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +9 -2
- package/docs/decisions/ADR-0083-one-live-session-runtime.md +256 -0
- package/docs/parity.md +4 -3
- package/docs/releases/2.0.1-unstable.md +63 -0
- package/docs/terminal-tui.md +10 -5
- package/package.json +1 -1
package/dist/terminal-pi.js
CHANGED
|
@@ -384,6 +384,15 @@ var questionRequestedPayload = z.object({
|
|
|
384
384
|
multiSelect: z.boolean(),
|
|
385
385
|
allowText: z.boolean(),
|
|
386
386
|
userOnly: z.boolean().optional(),
|
|
387
|
+
/** The Pi extension dialog this question stands for (ADR-0083); a host may ignore it. */
|
|
388
|
+
dialog: z.object({
|
|
389
|
+
kind: z.enum(["select", "confirm", "input", "editor"]),
|
|
390
|
+
title: z.string().max(4e3),
|
|
391
|
+
message: z.string().max(4e3).optional(),
|
|
392
|
+
placeholder: z.string().max(4e3).optional(),
|
|
393
|
+
prefill: z.string().max(1e5).optional(),
|
|
394
|
+
options: z.array(z.string().max(4e3)).max(200).optional()
|
|
395
|
+
}).optional(),
|
|
387
396
|
...questionBatchFields
|
|
388
397
|
}).superRefine(questionBatchRefinement).superRefine((value, context) => {
|
|
389
398
|
const error = questionSketchError(value.uiSketch, value.options);
|
|
@@ -261,6 +261,58 @@ not positional message counts. A host can route `/history` to Harness `sessions.
|
|
|
261
261
|
navigate the same saved session; it must not substitute a fork. Shared session catalog registration
|
|
262
262
|
is approved but remains in progress; no completed cross-surface catalog is implied (PAR-008).
|
|
263
263
|
|
|
264
|
+
## Live sessions (ADR-0083)
|
|
265
|
+
|
|
266
|
+
A *live session* is a Pi session the harness keeps open between the runs of one conversation, so a
|
|
267
|
+
later run reuses it instead of opening and loading a new one. `sessions/live.ts::LiveSessions` holds
|
|
268
|
+
them, one per `run.sessionId`.
|
|
269
|
+
|
|
270
|
+
- **Opt in.** `SessionRunInput.live: true` keeps the session open after the run and reuses an open
|
|
271
|
+
one. It is refused (`invalid_request`) for an unattended, child, nested, probe, improver or
|
|
272
|
+
reflection run, and refused with `session_busy` while another run of the session is using it.
|
|
273
|
+
Without `live` a run opens and disposes its own session, as before.
|
|
274
|
+
- **Pins.** A live session serves a run only when everything the run pins equals what the session
|
|
275
|
+
was opened with: account and its config folder, variant scope and version, variant extensions and
|
|
276
|
+
skill paths, the frozen capability set, working folder, workspace root and history folder, plan
|
|
277
|
+
and plan-repair flags, the allowed, gated and bound tool names, the stable system prompt, model,
|
|
278
|
+
effort, the run-scoped extension names and whether provider tools are asked. A supplied history
|
|
279
|
+
file and native session id must be the session's own. Any difference closes the idle session at
|
|
280
|
+
the turn boundary and the run opens a fresh one on the same native history.
|
|
281
|
+
- **Rebinding.** `piSessionFactory` reads the run's gate, bound tools, extension host and
|
|
282
|
+
plan flag through a binding that `PiSessionLike.rebind` replaces at the start of each run. The
|
|
283
|
+
run-scoped inline extensions (lineage join, subagents, compaction checkpoint, work slice, goal,
|
|
284
|
+
first-party hooks) are loaded in slots: a rebind removes the last run's handlers (Pi's `on`
|
|
285
|
+
returns the remover) and runs the next run's factory against the same extension. A session whose
|
|
286
|
+
run-scoped extension registers anything but handlers is not reusable. Between runs an extension
|
|
287
|
+
dialog is refused with `run_not_active`.
|
|
288
|
+
- **One writer of the history.** A top-level run of the same session that is not live (a command
|
|
289
|
+
run, for example), and `sessions.navigate` or `sessions.rename` on its history folder, close an
|
|
290
|
+
idle live session first so nothing holds a stale copy of the file; while the live session is
|
|
291
|
+
replying they are refused with `session_busy`.
|
|
292
|
+
- **Settling.** `runTurn` calls `TurnParams.release` instead of disposing the session. An idle live
|
|
293
|
+
session closes after 30 minutes (`SessionsOptions.liveIdleMs`), on `sessions.close`, or when the
|
|
294
|
+
harness closes.
|
|
295
|
+
- **Follow-ups.** `sessions.followUp { sessionId, text }` starts the next run at once when the
|
|
296
|
+
session is idle, or queues the text (at most 20) for after the active run. The queue follows the
|
|
297
|
+
queued-message dispatch rule above: it is sent after a reply that completed or that the person
|
|
298
|
+
stopped, and parked after a failure or any other interruption; a work-slice continuation runs
|
|
299
|
+
first. A follow-up run repeats the last live run's input with its own text and no Bash, images,
|
|
300
|
+
command, continuation or goal. `runs.steer` steers the active run as before.
|
|
301
|
+
- **Other run inputs a live client uses.** `newHistory: true` starts a new native history in
|
|
302
|
+
`sessionDir` (refused with `resume`, `continueFrom` or `command`). `hiddenNotes` (at most 8, each a
|
|
303
|
+
lower-case type and up to 20,000 characters) are saved as hidden messages before the request, as
|
|
304
|
+
the context update is. `planTools: 'active'` makes a plan run's frozen tool set the plan's own
|
|
305
|
+
(`planModeTools`), so the request carries only those tools; the default `gate` keeps every tool in
|
|
306
|
+
the request and refuses at the gate. An initial Bash command is accepted on a live turn that
|
|
307
|
+
resumes its history.
|
|
308
|
+
- **Question dialogs.** `question.requested` may carry `dialog { kind: select | confirm | input |
|
|
309
|
+
editor, title, message?, placeholder?, prefill?, options? }` when a Pi extension's dialog asked it
|
|
310
|
+
(`sessions/extensionUi.ts`). A host may ignore it; the answer is the usual `QuestionAnswer`.
|
|
311
|
+
- **Methods.** `sessions.live {}` → `{ sessions: LiveSessionStatus[] }`; `sessions.close { sessionId }`
|
|
312
|
+
→ `{ closed, afterRun }` (a busy session closes when its run settles);
|
|
313
|
+
`sessions.followUp { sessionId, text }` → `{ runId } | { queued }`;
|
|
314
|
+
`sessions.followUp.clear { sessionId }` → `{ removed }`.
|
|
315
|
+
|
|
264
316
|
## Invariants
|
|
265
317
|
|
|
266
318
|
- **INV-RUN-1** A run pins one account, one variant version, one capability set and one canonical
|
|
@@ -387,10 +439,16 @@ is approved but remains in progress; no completed cross-surface catalog is impli
|
|
|
387
439
|
the tools running at that moment). Every live reply emits a
|
|
388
440
|
heartbeat every 30 seconds, terminal replies included. A heartbeat older than five minutes while
|
|
389
441
|
the owner process lives marks the run `interrupted` with reason `stalled`; a run that never emitted
|
|
390
|
-
one keeps the 30-minute silence rule. A
|
|
391
|
-
|
|
442
|
+
one keeps the 30-minute silence rule. A heartbeat of a run with no work slice carries zero slice
|
|
443
|
+
counters. A reply whose heartbeat keeps arriving but which produces no
|
|
444
|
+
other event for fifteen minutes is a wedged turn, not a busy one: the harness ends it through the
|
|
392
445
|
native abort and records `interrupted` with reason `stalled`, and grants a reply whose oldest
|
|
393
|
-
running tool has run for thirty minutes the same ending.
|
|
446
|
+
running tool has run for thirty minutes the same ending. `runTurn` applies this to every run
|
|
447
|
+
(a structured session, a live session, the client terminal, an unattended run) with the same
|
|
448
|
+
windows (`sessions/stallWatch.ts`); the in-process terminal's controller applies it to its own
|
|
449
|
+
reply. A live session whose turn stalled is closed rather than reused, its queued follow-ups
|
|
450
|
+
parked for the next session, and closed under the turn after thirty seconds if the abort did not
|
|
451
|
+
unwind it (ADR-0083). A reply whose only running tools are
|
|
394
452
|
`wait_subagents` or `spawn_subagent` is judged by its children instead: each child has its own
|
|
395
453
|
clock (its last event other than a heartbeat; its running tools), and a child quiet for the same
|
|
396
454
|
windows is interrupted with reason `stalled`, so the wait returns and the parent carries on by
|
|
@@ -91,17 +91,19 @@ by ADR-0079: the reply is self-contained, outcome-first, explains the work and i
|
|
|
91
91
|
limits, and gives the reader enough context to pick it up cold. It explicitly reports checks that
|
|
92
92
|
were not run or failed; a person-owned next step appears only when one is needed. For substantial
|
|
93
93
|
replies with distinct parts, it often uses content-named Markdown `#` and `##` headings; there is no
|
|
94
|
-
fixed heading vocabulary or required template, and short replies may stay in prose.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
fixed heading vocabulary or required template, and short replies may stay in prose. Code fences name
|
|
95
|
+
the language; commands the person must run use a `bash` fence, while short references stay inline.
|
|
96
|
+
Tables are used for mappings and comparisons. Length follows complexity; prose comes first, with
|
|
97
|
+
lists when the information is genuinely parallel, sequential or easier to compare. References and formatting
|
|
97
98
|
follow rules supplied by the active session or host. Progress updates stay in the standard `writing`
|
|
98
99
|
section, not `reporting`. A subagent's reply gives its caller the concise work
|
|
99
100
|
summary and key findings, with useful absolute paths and evidence; snippets appear only when exact
|
|
100
101
|
text is load-bearing, and code merely read is not recapped. The section ships two examples (a
|
|
101
102
|
headed screen change and a heading-free blocked publish), the shape-by-work table and a four-line
|
|
102
|
-
pre-send list; the
|
|
103
|
-
format takes precedence. The section names no application, is never dynamic, and is never cut; its
|
|
104
|
-
budget is 12,000 characters, with
|
|
103
|
+
pre-send list; the blocked-publish example puts its login command in a `bash` fence. A person,
|
|
104
|
+
application or mode format takes precedence. The section names no application, is never dynamic, and is never cut; its
|
|
105
|
+
budget is 12,000 characters, with 5,081 currently delivered (ADR-0079, amended for language-labeled
|
|
106
|
+
code fences).
|
|
105
107
|
|
|
106
108
|
## When a reply may end
|
|
107
109
|
|
|
@@ -96,6 +96,10 @@ harness → host { v: 1, id, call, params } host → harness { v: 1
|
|
|
96
96
|
- Run streams also carry `tool.refused` with the call id, name, refusal code and bounded reason.
|
|
97
97
|
Run records expose nullable `terminalCode`; `terminalReason` remains descriptive text. Older
|
|
98
98
|
records and events may omit the code. Trajectory search exposes both fields (pages 02 and 08).
|
|
99
|
+
- `sessions.run` accepts `live: true` (ADR-0083): the Pi session stays open for the conversation's
|
|
100
|
+
next run. `sessions.live`, `sessions.close`, `sessions.followUp` and `sessions.followUp.clear`
|
|
101
|
+
manage live sessions (page 02). A run refused because the live session is replying fails with
|
|
102
|
+
`session_busy`.
|
|
99
103
|
- Errors carry a stable `code`, a message and an optional hint (`core/errors.ts`).
|
|
100
104
|
|
|
101
105
|
## Browser configuration and extension (ADR-0082)
|
|
@@ -199,6 +203,23 @@ export instead of prompt submission (page 02). These retain tracked run lifecycl
|
|
|
199
203
|
Saved-history helpers currently validate file scope but do not themselves fence active work;
|
|
200
204
|
host adapters must not infer concurrency safety from method availability.
|
|
201
205
|
|
|
206
|
+
## Socket transport and the long-lived harness (ADR-0083)
|
|
207
|
+
|
|
208
|
+
`server/socket.ts::serveSocket` serves the same framing on a Unix socket, readable and writable by
|
|
209
|
+
its owner only, with one peer per connection; a closed connection stops receiving events and
|
|
210
|
+
notices. `connectHarness` is the matching client. The long-lived harness (page 11) adds
|
|
211
|
+
`daemon.status` → `{ pid, startedAt, variantScope, live, detached }` and `daemon.stop` on its own
|
|
212
|
+
socket only.
|
|
213
|
+
|
|
214
|
+
| Method | Input | Reply |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `terminal.detach` | `{ sessionId }` | `{ detached, activeRunId }`; a client terminal with a running reply keeps it without a window, anything else is disposed |
|
|
217
|
+
| `terminal.detached` | `{}` | `{ sessions: DetachedTerminal[] }`: session id, folder, history folder, account, active run, history file, time |
|
|
218
|
+
| `terminal.takeRelaunch` | `{ sessionId }` | the `TerminalRelaunch` the session asked for, or null |
|
|
219
|
+
|
|
220
|
+
`terminal.prepare` accepts `attach: true` to reconnect to a detached session of the same id, folder,
|
|
221
|
+
history folder and account. The private `terminal.live.*` routes are listed on page 11.
|
|
222
|
+
|
|
202
223
|
## Invariants
|
|
203
224
|
|
|
204
225
|
- **INV-SRV-1** `src/server/methods.ts` is the contract for every non-Node host. Adding a method
|
|
@@ -207,7 +228,8 @@ host adapters must not infer concurrency safety from method availability.
|
|
|
207
228
|
- **INV-SRV-2** A method handler calls the same in-process API a Node application uses. The server
|
|
208
229
|
holds no logic of its own beyond parameter checking.
|
|
209
230
|
- **INV-SRV-3** Events reach the host only after they are persisted and redacted (`INV-RUN-3`).
|
|
210
|
-
- **INV-SRV-4** Changing the framing or `PROTOCOL_VERSION` is an architecture change.
|
|
231
|
+
- **INV-SRV-4** Changing the framing or `PROTOCOL_VERSION` is an architecture change. A transport
|
|
232
|
+
that carries the same framing (stdio, the owner-only socket) is not; a new one is listed here.
|
|
211
233
|
- **INV-SRV-5** The harness works with none of the host calls supplied (`INV-EXT-2`).
|
|
212
234
|
- **INV-SRV-6** After a change to the server, the CLI or startup, `npm run prove` must pass: it
|
|
213
235
|
starts the server on a throwaway data root with no application present and exercises accounts,
|
|
@@ -144,13 +144,67 @@ The `/plugins` Proposals tab previews the complete stored files and generated ma
|
|
|
144
144
|
confirmation. Accept and reject pass the preview hash through the control-key-protected socket;
|
|
145
145
|
the proposal service checks it again. Acceptance leaves the package switched off and untrusted.
|
|
146
146
|
|
|
147
|
+
## Client terminal and the long-lived harness (ADR-0083)
|
|
148
|
+
|
|
149
|
+
The OpenTUI frontend is a client of the harness by default: `tui/harnessRuntime.ts` implements the
|
|
150
|
+
same `TerminalRuntime` as `tui/runtime.ts`, so the screen code is shared.
|
|
151
|
+
`XINGULAR_TERMINAL_RUNTIME=inprocess` runs the in-process runtime instead, for plugins that need
|
|
152
|
+
their own terminal components (INV-TUI-8).
|
|
153
|
+
|
|
154
|
+
- **Parent.** `terminal/liveTerminal.ts::LiveTerminal`, created on the first `terminal.live.*`
|
|
155
|
+
call, runs each turn as `sessions.run { live: true }` of the terminal's own session id, account,
|
|
156
|
+
folder, history folder, pinned variant version (`{ dir, scope }`), launch sections, Pi's four
|
|
157
|
+
default tools plus the launch and switched-on world tools, and no gated tools, as the in-process
|
|
158
|
+
terminal's policy has it. It journals the live session's native events (a newer
|
|
159
|
+
`message_update` replaces an unread one; at most 4,000) and serves them by long poll.
|
|
160
|
+
- **Routes.** `terminal.live.state`, `.events { after, wait?, waitMs? }`, `.run { text, model?,
|
|
161
|
+
effort?, resume? | newHistory?, images?, plan?, goal?, command?, initialBashCommand?,
|
|
162
|
+
hiddenNotes? }`, `.steer`, `.followUp`, `.followUp.clear`, `.interrupt`, `.close { forget? }`,
|
|
163
|
+
`.rename`, `.pending`, `.pending.wait`, `.approve`, `.answer`, `.answers`. Once a client exists,
|
|
164
|
+
`terminal.pending|pending.wait|approve|answer|answers|interrupt` and remote control's send and
|
|
165
|
+
stop reach it instead of `TerminalController`. Pending decisions include the active reply's
|
|
166
|
+
subagents.
|
|
167
|
+
- **Client.** It keeps the model catalogue, settings, the skills, prompts and themes it lists, and
|
|
168
|
+
`ModeChoices`. `$name` skills and goal notes go as `hiddenNotes`; a plugin command is sent as its
|
|
169
|
+
`/<prefix>:<name>` text; `/compact`, `/tree` navigation and HTML export are command runs;
|
|
170
|
+
`/new`, `/resume`, fork and import change the file it resumes and close the live session; `/session`
|
|
171
|
+
takes a history file or the session id its runs record. A `!` command is the run's initial Bash.
|
|
172
|
+
A plan run sends only the plan's tools (`planTools: 'active'`). A mode with no saved model keeps
|
|
173
|
+
the current one. Closing the client never interrupts a reply; the parent decides.
|
|
174
|
+
- **What the window draws.** Besides the native events, the journal carries `harness_notice` (a
|
|
175
|
+
plugin's notice, status line, widget or title, and a failed reply's message), `harness_settled`
|
|
176
|
+
(the reply is over, with its goal state and history file, even when Pi's loop never started) and
|
|
177
|
+
`harness_dialog` / `harness_dialog_done` (a plugin's `select`, `confirm`, `input` or `editor`,
|
|
178
|
+
shown with the window's own dialog and closed when another surface answers first; the window's
|
|
179
|
+
question dock asks `terminal.pending.wait { ownDialogs: true }` and leaves them out). Before the
|
|
180
|
+
first turn the parent lists the trusted extensions' commands by loading them without a session;
|
|
181
|
+
one that cannot load stops the window's start, as in process. A session change or a steer first
|
|
182
|
+
asks the harness whether a reply really runs, so a window a moment behind its event stream does
|
|
183
|
+
not refuse `/new` or steer a reply that has ended.
|
|
184
|
+
- **Failure.** When the window's harness is unreachable twice in a row, the window closes cleanly.
|
|
185
|
+
An error nothing caught in the standalone host (a plugin's timer, say) closes the window first,
|
|
186
|
+
then exits 1. The long-lived harness logs such an error and keeps its other sessions.
|
|
187
|
+
- **Identity.** A client terminal's runs are harness runs: they record Pi's session id as
|
|
188
|
+
`nativeSessionId`, where an in-process terminal's record the history file. `/todo`,
|
|
189
|
+
`/trajectory`, `/context`, subagent rows and remote-control history match by the client's id, and
|
|
190
|
+
a relaunch resumes its history file.
|
|
191
|
+
- **Long-lived harness.** `xagent daemon start|stop|status` runs one harness per state folder
|
|
192
|
+
(`terminal/daemon.ts`): a lock (`harness.lock`) refuses a second, and `harness.sock` (owner-only)
|
|
193
|
+
serves the protocol with `daemon.status` and `daemon.stop`. `xagent --daemon` starts it when
|
|
194
|
+
needed, prepares its terminal there and runs the client frontend. When the window closes while a
|
|
195
|
+
reply runs, `terminal.detach` keeps the reply and its live session without a window until it
|
|
196
|
+
settles; `xagent attach [session]` reattaches a new window to it (`prepare { attach: true }`, same
|
|
197
|
+
folder and account). Without a running reply, closing the window ends the session as before.
|
|
198
|
+
|
|
147
199
|
## Invariants
|
|
148
200
|
|
|
149
201
|
- **INV-TUI-1** OpenTUI is the only terminal frontend. Pi's `InteractiveMode` is never constructed
|
|
150
202
|
([ADR-0006](../decisions/ADR-0006-opentui-only-terminal.md)).
|
|
151
203
|
- **INV-TUI-2** The harness owns screen composition, input, scrolling, command routing and plugin
|
|
152
204
|
UI. Pi owns the execution runtime, native history and rich content components. A host owns the
|
|
153
|
-
PTY and process supervision ([ADR-0001](../decisions/ADR-0001-owned-terminal.md)).
|
|
205
|
+
PTY and process supervision ([ADR-0001](../decisions/ADR-0001-owned-terminal.md)). In the default
|
|
206
|
+
client terminal the Pi session runs in the harness process as a live session (ADR-0083); the
|
|
207
|
+
window keeps composition, input and rendering.
|
|
154
208
|
- **INV-TUI-3** Execution, account isolation, trust, authorization and run recording stay behind
|
|
155
209
|
their existing layers. The terminal adds no second path for any of them; its runs are written
|
|
156
210
|
through the controller and the run store.
|
|
@@ -167,8 +221,14 @@ the proposal service checks it again. Acceptance leaves the package switched off
|
|
|
167
221
|
reconnects and resynchronizes after errors, and cancels its outstanding request on shutdown.
|
|
168
222
|
- **INV-TUI-5** Every resource reload revalidates the original trusted selection; changed or
|
|
169
223
|
revoked code needs review and a fresh terminal.
|
|
170
|
-
- **INV-TUI-6** `sessions.run` stays the embedding and unattended path. The terminal
|
|
171
|
-
persistent child instead of opening a session per turn
|
|
224
|
+
- **INV-TUI-6** `sessions.run` stays the embedding and unattended path. The in-process terminal
|
|
225
|
+
keeps a persistent child instead of opening a session per turn; the client terminal (below) runs
|
|
226
|
+
every turn as a `sessions.run { live: true }` of one live session, which is equally persistent.
|
|
227
|
+
- **INV-TUI-8** A client terminal runs no agent loop, tool or Pi extension of its own. It draws the
|
|
228
|
+
live session from its native events and its own read of the history file, and every action is a
|
|
229
|
+
protected `terminal.live.*` call (ADR-0083). Plugin presentation that needs an in-process
|
|
230
|
+
terminal (custom components, key handlers, renderers, shortcuts) is unavailable there, as it is
|
|
231
|
+
in a host: extensions get the dialog, notice, status and text-widget subset.
|
|
172
232
|
- **INV-TUI-7** Harness core commands use unprefixed names and are registered independently of
|
|
173
233
|
feature-plugin command metadata. This includes `/doctor`, `/worktree`, `/trajectory`, `/subagents`
|
|
174
234
|
and `/agents`; `/history` remains session-branch navigation, and `/mcp` remains a Connections
|
|
@@ -439,6 +499,10 @@ has run for thirty minutes ends the same way, and a reply waiting on an approval
|
|
|
439
499
|
cut short. An interrupt, a stall included, also ends a settling reply's wait for its subagents
|
|
440
500
|
(`terminal.subagents.delivery`).
|
|
441
501
|
|
|
502
|
+
The client terminal (ADR-0083) has no controller of its own: its replies are harness runs, so
|
|
503
|
+
`runTurn` heartbeats and applies the same stall rule (page 02, INV-RUN-14), and the live session of a
|
|
504
|
+
stalled reply is closed. The in-process runtime keeps the controller's watch.
|
|
505
|
+
|
|
442
506
|
## Subagent activity monitor
|
|
443
507
|
|
|
444
508
|
During an active parent run, the terminal polls a private read-only monitor once per second. It
|
|
@@ -49,7 +49,9 @@ index.
|
|
|
49
49
|
browser.json device-local BrowserConfig: backend, CDP, private-address policy and domains (13)
|
|
50
50
|
browser/ isolated profiles, downloads, and owner-only extension-bridge.json pairing state (13, ADR-0082)
|
|
51
51
|
notes-bridge.json the page bridge's port and token, kept so an app's script tag stays valid (13, PROP-0018)
|
|
52
|
-
harness.log standalone terminal diagnostics
|
|
52
|
+
harness.log standalone terminal and long-lived harness diagnostics
|
|
53
|
+
harness.sock the long-lived harness's protocol socket, owner-only, while it runs (10, ADR-0083)
|
|
54
|
+
harness.lock { pid, startedAt, socket } of the one long-lived harness for this folder (10, ADR-0083)
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
`/remote-control` (page 15) stores nothing here: its pairing and device keys live in memory and end
|
|
@@ -113,6 +113,10 @@ terminal command or page, a host screen.
|
|
|
113
113
|
harness serves. It is not a host and owns no capability. A paired device acts through the same
|
|
114
114
|
`Terminals` and controller paths as the terminal, and reaches no command, shell, link, plugin or
|
|
115
115
|
account control.
|
|
116
|
+
- The terminal is a client of the harness by default (page 11, ADR-0083): it presents the same
|
|
117
|
+
capabilities over harness live sessions, and its plugin presentation is the host subset
|
|
118
|
+
(INV-TUI-8). Its runs, approvals and questions are the structured path's, so the terminal and a
|
|
119
|
+
host share one lifecycle. `XINGULAR_TERMINAL_RUNTIME=inprocess` keeps the in-process runtime.
|
|
116
120
|
- The register of capabilities and their surfaces is [`docs/parity.md`](../parity.md). It is
|
|
117
121
|
descriptive; the invariants below are binding.
|
|
118
122
|
- A host keeps its own register of host-only surfaces. The Harness names no host surface.
|
|
@@ -78,7 +78,9 @@ Phone browser ── http://<tailscale or LAN address>:<port> ──> RemoteCont
|
|
|
78
78
|
the data root or the state directory; a phone keeps its own device key in its browser storage.
|
|
79
79
|
- **INV-RC-3** A paired device acts only through the terminal's existing paths: `Terminals.send`,
|
|
80
80
|
and `TerminalController.control` for `terminal.interrupt`, `terminal.approve`,
|
|
81
|
-
`terminal.answer`, `terminal.answers` and `terminal.pending.wait`.
|
|
81
|
+
`terminal.answer`, `terminal.answers` and `terminal.pending.wait`. For a client terminal
|
|
82
|
+
(ADR-0083) the same calls reach its `LiveTerminal`, and a message goes as a new reply or a
|
|
83
|
+
follow-up of the live session. The validation those paths
|
|
82
84
|
apply (a busy session, a terminal draft, the rememberable check on "always allow", the policy
|
|
83
85
|
recheck of a direct approval) applies unchanged.
|
|
84
86
|
- **INV-RC-4** A decision is shown on the phone with its complete action text, never shortened,
|
|
@@ -109,6 +109,11 @@ resolve relevant drift by updating the implementation and specification together
|
|
|
109
109
|
|
|
110
110
|
| ID | Where | What disagrees |
|
|
111
111
|
|---|---|---|
|
|
112
|
+
| D-32 | Ordered and nested list wrapping, `opentui/markdown.ts` (`alignListItems`) | The transcript renderer was written to keep a list item's wrapped lines beneath its text (nested lists and ordered markers wider than one digit). A general OpenTUI `renderNode` drops the blank rows between paragraphs and headings, so the renderer is code-block-only (`codeBlockOnly`) and lists no longer reach `alignListItems`: they wrap as in the previous release, continuation rows at column 4 where 7 was wanted. The verifier no longer checks it. Open; needs either structured lists with the block spacing restored, or OpenTUI support for both. |
|
|
113
|
+
| D-31 | `scripts/verify-terminal.py --markdown-only` — resolved 2026-11-04 | The fixture sent 100 list items and its checks read the 40 visible rows. It now sends three short lists (starting at 1, 10 and 100) on a 120x72 screen. The suite also found the blank rows between paragraphs and headings missing with the new renderer, which is why that renderer is now code-block-only (D-32). |
|
|
114
|
+
| D-30 | `scripts/verify-terminal.py` focused suites — resolved 2026-11-04 | The `browser`, `palette`, `accounts` and `trajectory` suites asserted layouts the terminal no longer draws; the product was right. Browser: the skill's description wraps in the 40-column completion column ("Inspect a" / "browser fixture."). Palette: command names carry their owner (ADR-0061) and wrap, and a narrow palette shows three lines per row. Accounts at 120x40: the search field holds the typed text, so its placeholder is not shown. Trajectory at 120x40: a wide event row shows its summary, not the event type. The script now reads the name column down its rows, matches wrapped text and compares columns. |
|
|
115
|
+
| D-29 | `scripts/verify-terminal.py`, approval titles — resolved 2026-11-03 | The script expected `Approve ask_user?` and `Approve workspace-write?`; the terminal titles dialogs with display names (`AskUser`, `WorkspaceWrite`), and a harness run asks `ask_user` questions without an approval. The script now accepts either title, and the question with or without an approval. |
|
|
116
|
+
| D-28 | Client terminal record contract — resolved 2026-11-03 | The script asserted that a terminal run's `nativeSessionId` is a history file. Client terminal runs record Pi's session id, as host runs do (page 11, Identity); the script resolves either to the history file, and `/session` takes either. |
|
|
112
117
|
| D-27 | Subagent compaction, pages 02 and 04 | A subagent compacted at the context threshold (run `5cd1d8aa`, 2026-11-01) produced a summary whose `## Goal` read "No specific research question or task was provided" while its split-turn section kept the brief. The model that continued still had the task through the turn context, but the summary's headline contradicts it. Not resolved here; the cause (the brief sits in the split part of the turn) is not confirmed. |
|
|
113
118
|
| D-26 | App browser tool catalog, pages 13 and 14 (PAR-015) — rechecked with ADR-0082 | Brave mode now supplies all shared Harness browser tools except the App-only upload through `browserToolsForSession` (`agent-app/sidecar/src/app.ts:172-177`, `appTools.ts:91-126`); WebKit still filters unsupported snapshot/ref operations. The Rust `app.capabilities` response remains backend-agnostic (`src-tauri/src/app_tools.rs:111-123`), while its `TOOL_IDS` and shared contract contain the new Browser tools (`shared/contracts.ts:557-562`, `src-tauri/src/agents.rs:90-103`). Thus a connected Brave backend implements the advertised refs, but the capability catalog can still mark WebKit-unsupported tools available. Partly resolved; backend-aware capability reporting remains open. |
|
|
114
119
|
| D-25 | Native WebKit interaction, page 13 — rechecked with ADR-0082 | The Brave adapter now uses the shared ref-based definitions, but the App's Rust backend still routes `browser_click`, `browser_type` and the App-only `browser_upload` by CSS `selector` (`agent-app/src-tauri/src/browser_workspace.rs:1493`, `agent-app/shared/appTools.ts:20`). The shared provider refuses those WebKit operations and never maps refs to selectors. Open: extend the contract to selectors or remove the native selector paths. |
|
|
@@ -4,9 +4,9 @@ title: Own the terminal frontend and keep Pi as the session runtime
|
|
|
4
4
|
status: accepted
|
|
5
5
|
created: 2026-10-17
|
|
6
6
|
updated: 2026-10-17
|
|
7
|
-
related: []
|
|
7
|
+
related: [ADR-0083]
|
|
8
8
|
supersedes: null
|
|
9
|
-
superseded_by:
|
|
9
|
+
superseded_by: ADR-0083 (runtime only)
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# ADR-0001 — Own the terminal frontend and keep Pi as the session runtime
|
|
@@ -85,3 +85,10 @@ had been reached. The isolated credentials were deleted, the source sign-in rema
|
|
|
85
85
|
and terminal restoration passed; see the [live evidence](../evidence/terminal-2026-10-17-followup/live-codex/README.md).
|
|
86
86
|
Successful live streaming/tool acceptance and App renderer acceptance remain open. This evidence
|
|
87
87
|
does not establish a released package or exhaustive platform/visual parity.
|
|
88
|
+
|
|
89
|
+
## Partly superseded — 2026-11-03
|
|
90
|
+
|
|
91
|
+
[ADR-0083](ADR-0083-one-live-session-runtime.md) moved the terminal's persistent Pi runtime into
|
|
92
|
+
the harness as a live session that every front end reaches through server methods; the client
|
|
93
|
+
terminal is the default. The owned OpenTUI presentation decided here stays. The in-process runtime
|
|
94
|
+
this record describes remains available with `XINGULAR_TERMINAL_RUNTIME=inprocess`.
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-0083
|
|
3
|
+
title: One live session runtime in the harness
|
|
4
|
+
status: accepted
|
|
5
|
+
created: 2026-11-03
|
|
6
|
+
updated: 2026-11-03
|
|
7
|
+
related: [ADR-0001, ADR-0035, ADR-0056, INV-RUN-1, INV-SRV-1, INV-SRV-4, INV-TUI-2, INV-RC-5, INV-PAR-1, INV-PAR-2, PAR-008, PAR-018]
|
|
8
|
+
supersedes: null
|
|
9
|
+
superseded_by: null
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# ADR-0083 — One live session runtime in the harness
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
The harness runs a conversation in two different runtimes:
|
|
17
|
+
|
|
18
|
+
- **Structured sessions** (`xagent serve`, the App sidecar): `Sessions.run` opens a Pi session for
|
|
19
|
+
one run and disposes it when the run settles (`sessions/runner.ts`, `piSessionFactory` and the
|
|
20
|
+
`finally` of `runTurn`).
|
|
21
|
+
- **The terminal**: a Bun child keeps one Pi `AgentSessionRuntime` alive across turns
|
|
22
|
+
(`terminal/tui/runtime.ts`). Its gate, telemetry and run accounting go through
|
|
23
|
+
`terminal/piExtension.ts`, the session socket and `TerminalController`.
|
|
24
|
+
|
|
25
|
+
ADR-0001 chose the second runtime because calling `sessions.run` per prompt "recreates and disposes
|
|
26
|
+
the runtime per turn, losing persistent plugin UI/queue state". That reason still holds for the
|
|
27
|
+
structured path as it is built today.
|
|
28
|
+
|
|
29
|
+
Two runtimes mean many lifecycle features are built twice and drift: plan-outcome repair (D-22),
|
|
30
|
+
the queued-message rules, work slices (a headless run continues as a new run, a terminal reply in
|
|
31
|
+
place), stall guards (`terminal/stallWatch.ts`, now `sessions/stallWatch.ts`), `get_capabilities`, and about fifteen private
|
|
32
|
+
`terminal.*` routes that mirror public methods. A terminal session cannot be reached from the App
|
|
33
|
+
(PAR-018) and the two keep separate session catalogs (PAR-008). A run ends when its window closes.
|
|
34
|
+
|
|
35
|
+
Codex keeps the agent loop in one server process (`codex app-server`) and makes every front end a
|
|
36
|
+
client of it: threads, turns and items over JSON-RPC, approvals as server-to-client requests.
|
|
37
|
+
Claude Code keeps the loop inside the CLI process and lets other front ends spawn that process.
|
|
38
|
+
The harness's server protocol (page 10) and run events (page 02) already have Codex's shape
|
|
39
|
+
(session ≈ thread, run ≈ turn, run events ≈ items). The missing part is a live session the server
|
|
40
|
+
owns across runs.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
The harness owns one session runtime. A *live session* is a Pi session the harness keeps open
|
|
45
|
+
across runs of the same native conversation. Every front end, including the terminal, reaches it
|
|
46
|
+
through server methods. The terminal keeps its owned OpenTUI presentation (the screen half of
|
|
47
|
+
ADR-0001); only its private runtime goes away.
|
|
48
|
+
|
|
49
|
+
Work proceeds in phases; each lands with its spec pages and checks.
|
|
50
|
+
|
|
51
|
+
1. **Live sessions.** A `LiveSessions` service in `src/sessions/` keeps a Pi session open between
|
|
52
|
+
runs. `Sessions.run` reuses it when the new run's pins match the open session, and opens a fresh
|
|
53
|
+
one otherwise. Unattended triggers (`probe`, `improver`, `reflection`) and refinement keep the
|
|
54
|
+
per-run open and dispose. A caller opts in with `sessions.run { live: true }`; the first such run
|
|
55
|
+
opens the live session. Additive methods: `sessions.live`, `sessions.close`, `sessions.followUp`,
|
|
56
|
+
`sessions.followUp.clear`. Steering stays `runs.steer`, and replay stays `runs.since`, which
|
|
57
|
+
already exist. The App adopts this first.
|
|
58
|
+
2. **The terminal as a client.** The OpenTUI child talks to the harness over the protocol on a
|
|
59
|
+
local socket and renders from run events. The private `terminal.*` mirrors give way to the
|
|
60
|
+
public methods, scoped by the caller identity the control key proves. The old path stays behind
|
|
61
|
+
a flag until production PTY acceptance at 40x20, 80x24 and 100x30, then is removed.
|
|
62
|
+
3. **Optional daemon.** Decided after phase 2: a long-lived `xagent serve --daemon` on a Unix
|
|
63
|
+
socket under the state directory, with one writer per data root, so a run outlives its window,
|
|
64
|
+
and remote control belongs to a live session rather than a terminal (PAR-018).
|
|
65
|
+
|
|
66
|
+
### Phase 1 design: what a run binds
|
|
67
|
+
|
|
68
|
+
A run pins its account, variant version and capability set (INV-RUN-1), so a live session may only
|
|
69
|
+
serve a run whose pins equal the session's. The reuse key is: account, variant version, frozen
|
|
70
|
+
capability fingerprint, working folder and workspace root, plan flag, allowed tool names, the
|
|
71
|
+
stable system prompt, and the loaded plugin set. Model and effort changes go through Pi's own
|
|
72
|
+
setters. Any other difference closes the live session at the turn boundary and opens a new one on
|
|
73
|
+
the same native history, as `/variant` already does (ADR-0065).
|
|
74
|
+
|
|
75
|
+
Today these are created per run and baked into the Pi session when it opens
|
|
76
|
+
(`sessions/sessions.ts`, the `params` and `factory` call; `sessions/runner.ts`,
|
|
77
|
+
`piSessionFactory`). Each must read a *current run binding* that `LiveSessions` swaps at the start
|
|
78
|
+
of every run:
|
|
79
|
+
|
|
80
|
+
| Bound per run today | Where | Becomes |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| The gate (`checkDetailed`, workspace scope requests, plan refusals) | `gatedExtension` in `piSessionFactory` | A session-level `tool_call` handler that asks the current binding's gate |
|
|
83
|
+
| Custom tool specs bound to the run (`spec.execute`) | `customTools` in `piSessionFactory` | Session-level tool definitions that call the current binding's spec of the same name |
|
|
84
|
+
| The extension host (`ask`, `notice`) | `bindExtensions` in `piSessionFactory` | A host that forwards to the current run, and refuses between runs |
|
|
85
|
+
| `runLineageJoin(root, runId)`, `subagents.extension(runId)` | `params.inline` | Extensions that read the current run id |
|
|
86
|
+
| Compaction checkpoint (`initialTodo`, plan instructions) | `params.inline` | Reads the current run's todo and plan flag |
|
|
87
|
+
| Work-slice guard and goal loop (their state and controller) | `params.inline` | Installed once, enabled and fed by the current binding |
|
|
88
|
+
| `onRefusal`, `activatedPlugins`, the heartbeat and `settle` | `params`, `runTurn` | Already per run in `runTurn`; unchanged |
|
|
89
|
+
|
|
90
|
+
`runTurn` stops disposing the session; `LiveSessions` disposes it on `sessions.close`, on a pin
|
|
91
|
+
change, after an idle timeout, and at harness shutdown. A live session runs one run at a time; a
|
|
92
|
+
second `sessions.run` while one is active is refused, or delivered as a steer or follow-up when the
|
|
93
|
+
caller asks for that.
|
|
94
|
+
|
|
95
|
+
## Alternatives and rejection reasons
|
|
96
|
+
|
|
97
|
+
- **Keep two runtimes.** Every lifecycle feature keeps its second copy, and the drift and parity
|
|
98
|
+
lists keep growing.
|
|
99
|
+
- **Claude Code's model** (the terminal process is the engine, hosts spawn it). Simple for one front
|
|
100
|
+
end, but the App, the phone surface and evaluation would each drive a terminal process, and the
|
|
101
|
+
structured path would remain a second runtime.
|
|
102
|
+
- **Make the terminal call today's `sessions.run`.** Rejected by ADR-0001 for the same reason it
|
|
103
|
+
gives: a per-run runtime loses queues and persistent plugin UI. Phase 1 removes that reason first.
|
|
104
|
+
- **A rewrite in one release.** Too large to verify; each phase must pass the full checks and, for
|
|
105
|
+
phase 2, production PTY acceptance.
|
|
106
|
+
|
|
107
|
+
## Consequences and risks
|
|
108
|
+
|
|
109
|
+
- This is a 2.0-level change: it adds methods and a socket transport (INV-SRV-1, INV-SRV-4),
|
|
110
|
+
changes who owns execution in the terminal (INV-TUI-2), and moves remote control's lifetime
|
|
111
|
+
(INV-RC-5). Pages 02, 10, 11, 14 and 15 change as each phase lands.
|
|
112
|
+
- Every inline extension must be safe to keep across runs. A missed per-run closure would leak one
|
|
113
|
+
run's state into the next; phase 1 tests run two turns on one live session and check each run's
|
|
114
|
+
events, refusals and records belong to that run only.
|
|
115
|
+
- Streaming over IPC adds latency to the terminal; phase 2 measures first-delta latency before and
|
|
116
|
+
after.
|
|
117
|
+
- Live checks may use real model requests only with the owner's permission (granted for this work
|
|
118
|
+
with a Codex account on Luna 6, or the OpenCode account on DeepSeek V4.1 Flash), never against
|
|
119
|
+
`~/.codex`, `~/.claude` or `~/.pi`.
|
|
120
|
+
|
|
121
|
+
## Evidence — phase 1, 2026-11-03
|
|
122
|
+
|
|
123
|
+
Implemented in `src/sessions/live.ts`, `src/sessions/runner.ts` (`rebind`, `release`, run slots),
|
|
124
|
+
`src/sessions/sessions.ts` (`live`, follow-ups) and `src/server/methods.ts`. `src/sessions/live.test.ts`
|
|
125
|
+
covers reuse across runs, a pin change, the per-run default, busy refusal, queued, idle and parked
|
|
126
|
+
follow-ups, close and shutdown, refusal for unattended runs, and a rebind of a real Pi session (an
|
|
127
|
+
`input` handler and a bound tool replaced by the next run's) with no model request. Harness:
|
|
128
|
+
1,803 tests passed, `npm run prove` passed. App sidecar typecheck and 89 tests passed against this
|
|
129
|
+
tree.
|
|
130
|
+
|
|
131
|
+
Live check with owner permission, account OpenCode, model `opencode-go/deepseek-v4.1-flash`, a
|
|
132
|
+
throwaway data root and the installed sign-in:
|
|
133
|
+
|
|
134
|
+
| Run | Live | Status | Native session | Session ready (ms) | Input tokens | Cache read |
|
|
135
|
+
|---|---|---|---|---|---|---|
|
|
136
|
+
| b1 | no | completed | `01a19f83-2ef3…` | 1002 (cold start) | 4796 | 0 |
|
|
137
|
+
| b2 | no | completed | same | 73 | 73 | 4736 |
|
|
138
|
+
| l1 | yes | completed | `01a19f83-408c…` | 75 | 4796 | 0 |
|
|
139
|
+
| l2 | yes | completed | same, reused | 52 | 73 | 4736 |
|
|
140
|
+
| l3 | yes, steered | completed | same, reused | 59 | 326 | 9472 |
|
|
141
|
+
|
|
142
|
+
The live session served three runs (`runs: 3`). Reuse keeps the provider cache exactly as the
|
|
143
|
+
per-run path does. A steer sent after the first delta was recorded as `message.steered` and the
|
|
144
|
+
model answered it in the same run. A reused session is ready slightly sooner (52–59 ms against 73 ms
|
|
145
|
+
warm); the larger gains are steering, follow-ups and plugin state that survive between runs.
|
|
146
|
+
|
|
147
|
+
## Evidence — phases 2 and 3, 2026-11-03
|
|
148
|
+
|
|
149
|
+
**Phase 2, the terminal as a client.** `XINGULAR_TERMINAL_RUNTIME=harness` selects
|
|
150
|
+
`terminal/tui/harnessRuntime.ts`; the parent side is `terminal/liveTerminal.ts`. The in-process
|
|
151
|
+
runtime stays the default. Decision recorded with the owner: plugin presentation that needs an
|
|
152
|
+
in-process terminal is unavailable in client mode (INV-TUI-8), as it is in a host.
|
|
153
|
+
|
|
154
|
+
- Tests with real Pi and a scripted model: `terminal/liveTerminal.test.ts` (two turns on one live
|
|
155
|
+
session, the native event journal, a plan-mode `ask_user` answered through the decision routes,
|
|
156
|
+
busy refusal) and `terminal/tui/harnessRuntime.socket.test.ts` over the real session socket (the
|
|
157
|
+
mirrored transcript, one native session, the in-process tool set, `/todo` and `/trajectory`
|
|
158
|
+
lookup, the system prompt, a plan question, `/new` on a new file).
|
|
159
|
+
- Live, OpenCode account, `opencode-go/deepseek-v4.1-flash`, real OpenTUI frontend in a PTY at
|
|
160
|
+
100x30, 80x24 and 40x20: five turns and a queued follow-up each, all completed on one native
|
|
161
|
+
session. Mean time from Enter to the reply on screen for turns 2–5: client 1.60 s, 1.79 s and
|
|
162
|
+
1.37 s against in-process 1.59 s at 100x30, within the model's own variation. The fixed overhead
|
|
163
|
+
from Enter to the harness run starting is about 30 ms against about 5 ms in process. Startup to
|
|
164
|
+
ready: 2.2–2.9 s client, 2.6–2.8 s in process.
|
|
165
|
+
- Found and fixed on the way: the client first lacked the world tools and had `grep`, `find` and
|
|
166
|
+
`ls`, which the in-process terminal does not activate; a new conversation re-adopted the old file
|
|
167
|
+
from stale state; a mode with no saved model sent none; child runs inherited `live: true` (caught
|
|
168
|
+
by the App's sidecar test).
|
|
169
|
+
- Not passing yet: the production PTY suite stops early in client mode on a record-contract
|
|
170
|
+
difference (Known drift D-28).
|
|
171
|
+
|
|
172
|
+
**Phase 3, the long-lived harness.** `server/socket.ts`, `terminal/daemon.ts`, `Terminals.detach`
|
|
173
|
+
and `prepare { attach }`, CLI `xagent daemon start|stop|status`, `xagent --daemon`, `xagent attach`.
|
|
174
|
+
|
|
175
|
+
- Tests: `terminal/daemon.test.ts` (one lock per state folder and a stale lock replaced, the
|
|
176
|
+
protocol on an owner-only socket to several connections, a full daemon run that prepares over its
|
|
177
|
+
socket, reports and stops); `harnessRuntime.socket.test.ts` (a reply keeps running when its
|
|
178
|
+
window closes and a new window reattaches and sees it finish; a detached session ends when its
|
|
179
|
+
reply settles).
|
|
180
|
+
- Live, same account and model: `xagent daemon start`, a `--daemon` window asked for 600 numbers,
|
|
181
|
+
the window was hung up (SIGHUP) mid-reply; it printed the `xagent attach <id>` line, `daemon
|
|
182
|
+
status` reported one live session and one reply without a window, `xagent attach` showed the
|
|
183
|
+
reply finish 3.6 s later, and the run recorded `completed`. Afterwards no live or detached
|
|
184
|
+
session remained and `daemon stop` removed the lock and socket.
|
|
185
|
+
- Found and fixed: the daemon took its lock before the state folder existed.
|
|
186
|
+
|
|
187
|
+
Checks at the end: harness typecheck clean, 1,820 tests pass, `npm run prove` passes; App sidecar
|
|
188
|
+
typecheck and 89 tests pass against this tree, `check:layers` flags none of the new files. Open
|
|
189
|
+
and unrelated to this change: the App's `soloCommands.test.ts` expects Solo to handle `/goal`
|
|
190
|
+
(PAR-017), and the App's root typecheck reports button-prop errors in its UI.
|
|
191
|
+
|
|
192
|
+
## The client terminal is the default — 2026-11-03
|
|
193
|
+
|
|
194
|
+
The owner asked to finish the client terminal and make it the default once it works. `xagent` now
|
|
195
|
+
runs the client terminal unless `XINGULAR_TERMINAL_RUNTIME=inprocess` is set. The in-process
|
|
196
|
+
runtime stays for plugins that need their own terminal components.
|
|
197
|
+
|
|
198
|
+
What finishing it took, each with a test:
|
|
199
|
+
|
|
200
|
+
- Parity of what a person sees: plugin notices, status lines, widgets and titles (`harness_notice`);
|
|
201
|
+
a failed reply's message; a settle marker when Pi's loop never started; plugin `select`,
|
|
202
|
+
`confirm`, `input` and `editor` shown with the window's own dialogs (`question.requested.dialog`);
|
|
203
|
+
plugin commands listed before the first turn and sent by their owner-qualified route; a plugin
|
|
204
|
+
that cannot load stops the window's start; `/goal` run as a harness goal with its state shown.
|
|
205
|
+
- Parity of what a turn does: plan runs carry only the plan's tools (`planTools: 'active'`); `!`
|
|
206
|
+
commands run as the turn's initial Bash on a resumed history; `/session` takes a file or a session
|
|
207
|
+
id; `/new` and steering ask the harness whether a reply really runs before refusing.
|
|
208
|
+
- Lifecycle: `/stop` and Esc return at once and Pi's abort finishes behind them; the window closes
|
|
209
|
+
cleanly when its harness is gone; an uncaught error in the host closes the window before exiting
|
|
210
|
+
1; a frontend left after its terminal hangs up is ended after three seconds (this also leaked
|
|
211
|
+
in-process windows); the long-lived harness survives one plugin's uncaught error.
|
|
212
|
+
- A harness fix every host gets: a provider request that timed out and was retried and answered no
|
|
213
|
+
longer marks the run failed.
|
|
214
|
+
|
|
215
|
+
Evidence:
|
|
216
|
+
|
|
217
|
+
- Harness: typecheck clean, 1,829 tests pass, `npm run prove` passes; App sidecar typecheck and 89
|
|
218
|
+
tests pass; `check:layers` flags none of the new files.
|
|
219
|
+
- Production PTY suite (`scripts/verify-terminal.py`, its local provider, no real requests) in the
|
|
220
|
+
default client terminal: the full run passes at 40x20, 80x24, 100x30 and 120x40, and so do the
|
|
221
|
+
welcome, click-focus, composer, file-completion, history, resume, todo, questions, jump-latest,
|
|
222
|
+
help, command-completion, attachments, dialog-paste, thinking, queue and compaction suites. The
|
|
223
|
+
script now accepts what differs by design: either approval title, a question asked with or without
|
|
224
|
+
an approval, either `nativeSessionId` contract, a plugin overlay the client cannot draw, a size
|
|
225
|
+
shown in bytes, a varying usage-event count, and a long command name wrapped after its owner
|
|
226
|
+
prefix. Browser, palette, accounts at 120x40 and trajectory at 120x40 fail the same way on the
|
|
227
|
+
previous commit (Known drift D-30); markdown fails only with uncommitted work outside this change
|
|
228
|
+
(D-31). The full run's `/stop` step, which allows 0.8 seconds, failed twice while the machine's
|
|
229
|
+
load average was above 20 and passed every time below it.
|
|
230
|
+
- Live, OpenCode account, `opencode-go/deepseek-v4.1-flash`, real frontend at 100x30: five turns
|
|
231
|
+
and a queued follow-up on one native session; `/stop` and Esc recorded the reply interrupted in
|
|
232
|
+
33–77 ms against 35–62 ms in process; turn latency within the provider's variation. One turn hit
|
|
233
|
+
a ten-minute provider timeout and its retry answered; the run now records `completed`. With
|
|
234
|
+
`--daemon`, a window hung up mid-reply left the reply running; `xagent attach` showed it complete.
|
|
235
|
+
|
|
236
|
+
## Closing the migration — 2026-11-04
|
|
237
|
+
|
|
238
|
+
- **Stall guard for every harness run.** The terminal's stall rule (15 minutes without an event, 30
|
|
239
|
+
with a tool running; ADR-0054, ADR-0078) lived only in the in-process controller, so a client
|
|
240
|
+
terminal reply, a live session and an App turn had only Pi's own request timeout (about ten
|
|
241
|
+
minutes, retried). `runTurn` now applies it to every run (`sessions/stallWatch.ts`, moved from
|
|
242
|
+
`terminal/`): the run settles `interrupted` with `stalled`, the native turn is aborted, and a live
|
|
243
|
+
session is closed (queued follow-ups parked), or closed under the turn if the abort has not
|
|
244
|
+
unwound it after 30 seconds. A pending approval or question, and a wait for subagents (each child
|
|
245
|
+
has its own guard), restart the clock. Pi's request timeout is unchanged: a shorter one multiplies
|
|
246
|
+
with the retry policy and could cut a slow reasoning model's first token, so the progress rule is
|
|
247
|
+
the cap.
|
|
248
|
+
- **Heartbeat for every run.** `runTurn` emitted `heartbeat` only for work-slice runs, against
|
|
249
|
+
INV-RUN-14; a live reply with a quiet tool for 30 minutes could be settled `owner_gone`. It is now
|
|
250
|
+
unconditional (slice counters zero without a slice).
|
|
251
|
+
- **Verifier.** The four D-30 suites asserted stale layouts, not product faults, and are fixed; the
|
|
252
|
+
`/stop` step polls the run record up to 8 seconds instead of reading for 0.8; the in-process
|
|
253
|
+
fallback's full run passes at 80x24 (the deny/approval-cancel stop did not reproduce). D-31 (the
|
|
254
|
+
markdown suite) is the other change's fixture overflowing the screen. Tests: `sessions/stall.test.ts`
|
|
255
|
+
(8) cover the guard and heartbeat.
|
|
256
|
+
- Not done: PAR-018 (remote control bound to a live session). Notes in HANDOFF.md.
|