@rho-workspace/rho 0.5.4 → 0.5.6
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/dist/agent-connect.d.ts +8 -0
- package/dist/agent-connect.js +15 -0
- package/dist/browser-access.js +3 -2
- package/dist/capabilities.d.ts +2 -2
- package/dist/capabilities.js +6 -1
- package/dist/cli.js +219 -176
- package/dist/contract.generated.d.ts +472 -4
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +455 -6
- package/dist/conversations.d.ts +157 -4
- package/dist/conversations.js +69 -6
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -3
- package/dist/extension-contract.d.ts +99 -0
- package/dist/extension-contract.js +52 -0
- package/dist/extension-directory.d.ts +9 -0
- package/dist/extension-directory.js +11 -0
- package/dist/extension-worker.d.ts +1 -0
- package/dist/extension-worker.js +36 -0
- package/dist/extensions.d.ts +157 -0
- package/dist/extensions.js +349 -0
- package/dist/plugin-cli.d.ts +1 -0
- package/dist/plugin-cli.js +139 -0
- package/dist/plugin-inventory.json +18 -0
- package/dist/plugins.d.ts +3 -3
- package/dist/project-work.d.ts +3 -1
- package/dist/project-work.js +15 -2
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.js +16 -7
- package/dist/server.d.ts +5 -10
- package/dist/server.js +122 -12
- package/dist/web/.vite/manifest.json +67 -67
- package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
- package/dist/web/assets/choices-D-YfnFP6.js +1 -0
- package/dist/web/assets/{console-Dbdo-5oO.js → console-Trqc0swD.js} +1 -1
- package/dist/web/assets/{dist-B2V0VDO0.js → dist-DZmszrjG.js} +2 -2
- package/dist/web/assets/dist-DpIHRk3B.js +1 -0
- package/dist/web/assets/{dist-D25Vn17i.js → dist-eMx6b8N1.js} +1 -1
- package/dist/web/assets/dist-pF902jHA.js +3 -0
- package/dist/web/assets/explore-in-BCHqjJ7B.js +1 -0
- package/dist/web/assets/graph-lENgfQNH.js +2 -0
- package/dist/web/assets/index-B3_OVy91.css +1 -0
- package/dist/web/assets/{index-BHQIaDsx.js → index-BpL_Kl_s.js} +87 -86
- package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-DcVGUBBY.js} +1 -1
- package/dist/web/assets/overview-CDnVKlOt.js +6 -0
- package/dist/web/assets/preview-BEC1E9MK.js +1 -0
- package/dist/web/assets/{review-CmWou4Iv.js → review-DYP0aCXb.js} +1 -1
- package/dist/web/assets/{skills-Dy7n3L9d.js → skills-1ec0Z-jx.js} +1 -1
- package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-DNexV4OJ.js} +1 -1
- package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-EYQgikxq.js} +2 -2
- package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-Chj2Gbx7.js} +1 -1
- package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-CT9RVcWY.js} +1 -1
- package/dist/web/assets/workspace-COmCbGV3.js +5 -0
- package/dist/web/composition.json +65 -65
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.d.ts +2 -1
- package/dist/web-assets.js +3 -2
- package/dist/work-contract.js +2 -2
- package/docs/FRONTEND.md +26 -18
- package/docs/HEADLESS.md +38 -28
- package/docs/PLUGIN-AUTHORING.md +4 -0
- package/docs/THIRD-PARTY-PLUGINS.md +190 -0
- package/node_modules/@rho-workspace/plugin-agent/README.md +68 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +20 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +84 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
- package/node_modules/@rho-workspace/plugin-agent/package.json +6 -1
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
- package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
- package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
- package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
- package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
- package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
- package/node_modules/hono/dist/cjs/utils/url.js +1 -1
- package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/jsx/base.js +49 -32
- package/node_modules/hono/dist/jsx/components.js +2 -2
- package/node_modules/hono/dist/jsx/constants.js +2 -1
- package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
- package/node_modules/hono/dist/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/utils/cookie.js +2 -2
- package/node_modules/hono/dist/utils/mime.js +1 -1
- package/node_modules/hono/dist/utils/url.js +1 -1
- package/node_modules/hono/package.json +1 -1
- package/package.json +5 -3
- package/protocol/index.json +93 -33
- package/protocol/operations/agent.sessions.list.json +146 -0
- package/protocol/operations/agent.sessions.read.json +315 -0
- package/protocol/operations/conversation.configure.json +29 -0
- package/protocol/operations/conversation.decide.json +29 -0
- package/protocol/operations/conversation.list.json +29 -0
- package/protocol/operations/conversation.permission.json +29 -0
- package/protocol/operations/conversation.read.json +29 -0
- package/protocol/operations/conversation.restore.json +29 -0
- package/protocol/operations/conversation.send.json +29 -0
- package/protocol/operations/conversation.settings.check.json +92 -0
- package/protocol/operations/conversation.settings.read.json +92 -0
- package/protocol/operations/conversation.settings.update.json +92 -0
- package/protocol/operations/conversation.start.json +46 -1
- package/protocol/operations/conversation.stop.json +29 -0
- package/protocol/operations/conversation.trash.json +29 -0
- package/protocol/operations/extension.call.json +132 -0
- package/protocol/operations/extension.content.json +78 -0
- package/protocol/operations/extension.describe.json +195 -0
- package/protocol/operations/extension.install.json +198 -0
- package/protocol/operations/extension.list.json +123 -0
- package/protocol/operations/extension.manage.json +55 -0
- package/protocol/operations/extension.read.json +112 -0
- package/protocol/operations/observation.snapshot.json +29 -0
- package/protocol/operations/project.work.read.json +3 -2
- package/protocol/operations/session.grant.json +9 -0
- package/protocol/operations/session.instructions.json +43 -0
- package/scripts/third-party-acceptance.mjs +77 -0
- package/vendor/composition.json +83 -83
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/web/theme.css +12 -0
- package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
- package/dist/web/assets/dist-BV3hWfn_.js +0 -3
- package/dist/web/assets/dist-CLNF2g-E.js +0 -1
- package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
- package/dist/web/assets/graph-hS2kvHTI.js +0 -2
- package/dist/web/assets/index-Cj1_iqyK.css +0 -1
- package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
- package/dist/web/assets/preview-n5lOOdjN.js +0 -1
- package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
- package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
package/docs/HEADLESS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Rho local backend
|
|
1
|
+
# Rho local backend
|
|
2
2
|
|
|
3
3
|
Rho selects nine independent capability packages: annotations plus documents, files, R,
|
|
4
4
|
requirements, tables, media, charts and Agent. Shared contracts contain generic envelopes/service ports and ordinary-file
|
|
@@ -132,38 +132,47 @@ Exact Host and supplied Origin are checked. The bearer credential is project
|
|
|
132
132
|
scoped for external callers. CLI shutdown stops only Hosts started by this
|
|
133
133
|
service and reports confirmation separately. Existing Hosts keep their owner.
|
|
134
134
|
|
|
135
|
-
### Connect
|
|
135
|
+
### Connect your own Agent
|
|
136
136
|
|
|
137
|
-
The
|
|
138
|
-
|
|
137
|
+
The project's **Agent → Agents** tab offers one prompt for any Agent
|
|
138
|
+
that can configure and run a local stdio MCP server. `session.instructions` is a
|
|
139
|
+
read-only operation for the local browser or operator: it returns the current
|
|
140
|
+
Node/launcher paths and existing project ID, without credential contents. Reading
|
|
141
|
+
or copying starts no Agent, grants no access and edits no client configuration.
|
|
142
|
+
The Agent uses its own supported configuration mechanism; Rho needs no additional
|
|
143
|
+
brand-specific form. The [connection episode](AGENT-CONNECT-EPISODE.md) records the
|
|
144
|
+
actual clients checked and the limits.
|
|
139
145
|
|
|
140
|
-
|
|
141
|
-
rho --grant /absolute/service-state/connection.json --project PROJECT_FOLDER --agent claude-code|codex
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
It gives that Agent its own grant to the project (kept when it still reaches the
|
|
145
|
-
service), runs that Agent's `mcp remove` and `mcp add` on this machine — Claude Code's
|
|
146
|
-
with `-s local` in the project folder, Codex's for every folder — and records it, so the
|
|
147
|
-
page shows it under that Agent. The JSON reply names the project, its root, the number
|
|
148
|
-
of granted methods, whether a new grant was made, and what was added (`name`, `scope`,
|
|
149
|
-
`at`). A session that was already open does not have the tools; a new one does.
|
|
150
|
-
|
|
151
|
-
Without `--agent`, the same connection grants one project to one Agent session and
|
|
152
|
-
prints how Claude Code starts that session's MCP server:
|
|
146
|
+
The running service's operator connection grants one project to a named Agent:
|
|
153
147
|
|
|
154
148
|
```sh
|
|
155
|
-
rho --grant /absolute/service-state/connection.json
|
|
149
|
+
rho --grant /absolute/service-state/connection.json \
|
|
150
|
+
--project-id EXISTING_PROJECT_ID --session my-agent
|
|
156
151
|
```
|
|
157
152
|
|
|
158
|
-
`--project`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
`--project-id` selects an already registered project, including one chosen outside
|
|
154
|
+
the service's projects folder. The older `--project PROJECT_FOLDER` form remains
|
|
155
|
+
available for a folder relative to `--root`; select exactly one form. Choose a
|
|
156
|
+
stable session name for each Agent and reuse it on retry. Without `--session`, the
|
|
157
|
+
legacy default is `claude-code`.
|
|
158
|
+
|
|
159
|
+
The JSON reply supplies `mcp.name`, `mcp.command` and `mcp.args` for the Agent to
|
|
160
|
+
register in its own client. `rho --mcp FILE` serves that project grant on stdio with
|
|
161
|
+
`rho_index`, `rho_resolve`, `rho_call` and `rho_read`. Connection files are credentials;
|
|
162
|
+
keep their contents out of prompts and projects. The reply also retains the older
|
|
163
|
+
`claude` command for compatibility.
|
|
164
|
+
|
|
165
|
+
Repeating the same grant keeps a valid credential with the same project, methods
|
|
166
|
+
and profile; changing scope, expired access or stale connection data requires a
|
|
167
|
+
fresh one. A client may need to reload its tools or open a new session. A saved
|
|
168
|
+
entry alone does not verify a connection: call `rho_index` in that client and check
|
|
169
|
+
its project scope. The page observes grants and actual calls separately and does
|
|
170
|
+
not claim that an old successful call means the Agent is online now.
|
|
171
|
+
|
|
172
|
+
The existing `--agent claude-code|codex` shortcut and `session.connect` remain
|
|
173
|
+
available. They use those clients' own `mcp add` commands (Claude Code in the project
|
|
174
|
+
folder, Codex in user configuration). They are no longer the page's setup workflow.
|
|
175
|
+
A stopped service is reported with exit 1; an unknown CLI option exits 2 with usage.
|
|
167
176
|
|
|
168
177
|
Agent 0.4.11 provides persistent Claude and Codex ACP entries and retains each
|
|
169
178
|
tool step's human title, ACP category and one identifying input (command, file path,
|
|
@@ -279,7 +288,8 @@ const check = await rho.call('requirements.check', {
|
|
|
279
288
|
| `context.create/read` | Exact buffer/version/range, declared selected entities, a media rectangle with page and pixels/points, or one run (`run: {operationId}`): its state, exit, stop, the last 8 KiB of retained stderr with its byte offset and Core's truncation flag, the script reference, args and declared files. |
|
|
280
289
|
| `session.grant`, `binding.open/close/list/activity` | Operator-selected method grant, external binding metadata and bounded tool receipts. Agent grants include the read-only `project.entries` and no other `project.*` method. |
|
|
281
290
|
| `session.connect` | The person at this machine only: grants Claude Code or Codex access to one project and runs that Agent's own `mcp add` (`claude mcp add -s local` in the project folder, `codex mcp add`) with the project-scoped connection. A grant that still reaches this service is kept; a new one the Agent refuses is removed. Each CLI call: 20 s and 8 KiB of output, then stopped with what it printed. |
|
|
282
|
-
| `conversation.start/send/stop/read/permission` | Only the browser on this machine. Original message identities, retained text/tool steps and one selected Agent session. New conversations use the selected Claude or Codex ACP connection. Cancel is confirmed by the original prompt response; lost transport keeps an unknown outcome. Reads and reconnect never replay a prompt. Native permission choices are scoped to the original pending request. Limits are in `conversationLimits` and `--limits`. |
|
|
291
|
+
| `conversation.start/send/stop/read/permission` | Only the browser on this machine. Original message identities, retained text/tool steps and one selected Agent session. New conversations use the selected Claude or Codex ACP connection; `continue: {session, copy}` instead loads a session listed by `agent.sessions.list` under its own native ID, or forks it, and is refused while another process holds that session. Cancel is confirmed by the original prompt response; lost transport keeps an unknown outcome. Reads and reconnect never replay a prompt. Native permission choices are scoped to the original pending request. Limits are in `conversationLimits` and `--limits`. |
|
|
292
|
+
| `agent.sessions.list/read` | Only the browser on this machine; excluded from Agent grants. Claude Code and Codex sessions in the project folder read from their own records, and observed Harness conversations, each with where it started, whether another window holds it now and its resume command. Reading starts no Agent and is bounded (`nativeSessionLimits` in the Agent plugin). |
|
|
283
293
|
| `composition.put/read/apply/undo`, `view.close` | Registered entry/version, props and binding checks; `apply`/`undo` are conditional tree transactions with receipts ([Collaboration](COLLABORATION.md#composition-and-selection)). Uninstalled UI is explicitly unavailable. `rho.action` (version `1`, no bindings) is Rho's own form for one proposed action: props `{title, action, actionVersion, operationId, input, repairOf?}`, and the action must be registered at that version. |
|
|
284
294
|
| `workspace.save/export/restore` | Drafts/undo, bytes/provenance, selections and composition intent; native execution is not replayed. |
|
|
285
295
|
| `action.export` | One run as ordinary files in a new project folder (default `exports/<operationId>`): `script/` holds the code that ran, from the run's own record; `outputs/` holds each declared file as the run left it; `provenance.json` and `PROVENANCE.md` give the run, script digest, arguments, inputs, each file's SHA-256 and whether it matches the inventory the run wrote. A declared file that was not written, or no longer matches that inventory, is named and left out, and the export says `partial`. When the script has unsaved changes the call must say `unsaved: "include"` (written as `<name>.unsaved.<ext>`, marked as never run) or `"leave"`. Runs nothing; never writes into an existing folder. |
|
package/docs/PLUGIN-AUTHORING.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Build a UI extension
|
|
2
2
|
|
|
3
|
+
For independently installed operations, Skills and live HTML views, use the
|
|
4
|
+
[third-party plugin channel](THIRD-PARTY-PLUGINS.md). This page covers compiled
|
|
5
|
+
React entries over an existing Rho capability.
|
|
6
|
+
|
|
3
7
|
A UI extension is a React view over an existing Rho capability, such as Documents
|
|
4
8
|
or Tables. The package owns its view; the existing capability owns data, edits and
|
|
5
9
|
operations. Rho supplies tabs, splits, focus and versioned resource bindings.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Develop and install third-party plugins
|
|
2
|
+
|
|
3
|
+
Rho can install independent npm packages while it is running. A package supplies
|
|
4
|
+
operations, Skills and standalone HTML views; it does not require rebuilding Rho
|
|
5
|
+
or becoming a base dependency. The first consumer is the optional
|
|
6
|
+
[sclet adapter](https://github.com/YuLab-SMU/Rho-plugins/tree/main/examples/sclet-plugin).
|
|
7
|
+
Its scientific implementation remains in sclet and its objects remain in the
|
|
8
|
+
project's shared R session. See the [observed episode](THIRD-PARTY-PLUGINS-EPISODE.md).
|
|
9
|
+
|
|
10
|
+
This is source functionality after Rho 0.5.4, not a new npm release. Build the
|
|
11
|
+
current checkout with `npm run build` and use `node dist/cli.js` in place of
|
|
12
|
+
`rho` until a distribution containing it is installed.
|
|
13
|
+
|
|
14
|
+
## Install and manage
|
|
15
|
+
|
|
16
|
+
Open a project, then **Plugins → Third-party plugins**. Install a directory entry
|
|
17
|
+
or enter a package source. The installed card shows its version, source, enabled
|
|
18
|
+
state and content digest. Open its view, read its guide, update, disable or remove
|
|
19
|
+
it there. An invalid source or failed update leaves the previous selection intact.
|
|
20
|
+
|
|
21
|
+
Only install packages you trust. Hosts run native code with the Rho process user's
|
|
22
|
+
OS permissions. Disabling npm lifecycle scripts prevents install hooks; it does
|
|
23
|
+
not make plugin execution a sandbox. No approval is added to already authorized
|
|
24
|
+
analysis. Paired browsers and Agent connections cannot install or manage packages.
|
|
25
|
+
|
|
26
|
+
The equivalent commands use the running service's private connection file:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
rho plugin add /absolute/path/my-plugin
|
|
30
|
+
rho plugin add /absolute/path/my-plugin-0.1.0.tgz
|
|
31
|
+
rho plugin add @your-org/rho-plugin@0.1.0
|
|
32
|
+
rho plugin add 'git+https://github.com/your-org/repository.git#COMMIT:packages/plugin'
|
|
33
|
+
rho plugin list
|
|
34
|
+
rho plugin update my-plugin
|
|
35
|
+
rho plugin update my-plugin --source @your-org/rho-plugin@0.2.0
|
|
36
|
+
rho plugin disable my-plugin
|
|
37
|
+
rho plugin enable my-plugin
|
|
38
|
+
rho plugin remove my-plugin
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Add `--connection /absolute/service-state/connection.json` for a nondefault
|
|
42
|
+
service. The default is `~/Rho/state/connection.json`. These commands do not start
|
|
43
|
+
a service. Never share the connection file. Public HTTPS `.tgz` URLs are also
|
|
44
|
+
accepted. Git sources require Git; package staging requires npm. Git submodules
|
|
45
|
+
and build hooks are not run. Build and include runtime files before distribution.
|
|
46
|
+
|
|
47
|
+
Installation stages an isolated dependency tree using npm with `--ignore-scripts`
|
|
48
|
+
and `--install-links`, validates the manifest and contained files, hashes the tree,
|
|
49
|
+
then atomically selects it. The source selector, resolved Git commit when present,
|
|
50
|
+
package version and runtime-tree digest remain visible. Listing does not import
|
|
51
|
+
plugin code. Content and calls verify the installed tree before use.
|
|
52
|
+
|
|
53
|
+
Disable blocks new calls and views. Remove takes the package out of the installed
|
|
54
|
+
selection. Both preserve native tasks, R sessions, project files and receipts.
|
|
55
|
+
Package revisions are retained in the service's `plugins/versions` directory;
|
|
56
|
+
remove is not a disk-purge command. Closing a view only releases its renderer.
|
|
57
|
+
|
|
58
|
+
## Create a package
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
rho plugin create my-plugin --id lab.my-plugin
|
|
62
|
+
rho plugin check ./my-plugin
|
|
63
|
+
rho plugin dev ./my-plugin
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`create` refuses to overwrite a directory. The generated package has a working
|
|
67
|
+
greeting operation, Skill and view. `dev` installs checked snapshots on source
|
|
68
|
+
changes, ignoring `.git` and `node_modules`. Reopen a view to load a new snapshot.
|
|
69
|
+
Invalid edits retain the last working installation. No Rho rebuild is needed.
|
|
70
|
+
|
|
71
|
+
| File | Responsibility |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `package.json` | npm name/version, runtime files and `rhoPlugin: "rho-plugin.json"` |
|
|
74
|
+
| `rho-plugin.json` | API version 1, unique plugin id, title, description and entries |
|
|
75
|
+
| `host.mjs` | `run(operation, input, context)` over the domain's public capabilities |
|
|
76
|
+
| `SKILL.md` | When/how to use the package, prerequisites, failures and result limits |
|
|
77
|
+
| `view.html` | Prebuilt HTML with inline scripts/styles; may bundle a framework |
|
|
78
|
+
|
|
79
|
+
Each operation declares `id`, `title`, `description`, `effect` (`read` or
|
|
80
|
+
`native`), the Rho `methods` it delegates, and an object `inputSchema`. Each Skill
|
|
81
|
+
or view declares `id`, `title` and a package-relative `file`. The manifest's
|
|
82
|
+
`operations`, `skills` and `views` arrays are required, but may be empty. A host
|
|
83
|
+
is required only when operations exist. The checker validates entry containment,
|
|
84
|
+
API compatibility, identifiers, duplicate entries, schemas and entry sizes.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
export async function run(operation, input, context) {
|
|
88
|
+
if (operation === 'read-sessions') {
|
|
89
|
+
const value = await context.call('kernel.read', { limit: 1 });
|
|
90
|
+
return { sessions: value.sessions };
|
|
91
|
+
}
|
|
92
|
+
throw new Error('Unknown operation');
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`context.projectRoot` is the selected project, `context.invocationId` identifies
|
|
97
|
+
this call, and `context.changeId(label)` supplies a stable delegated write id.
|
|
98
|
+
`context.call(method, body)` validates the ordinary Rho schema, original caller's
|
|
99
|
+
grant, current project and owner availability. Declare every delegated method.
|
|
100
|
+
A read operation cannot delegate a mutating method. It must also keep its own
|
|
101
|
+
native work read-only; native host code is trusted, not OS-confined.
|
|
102
|
+
|
|
103
|
+
The host runs in a disposable child process for each call, without the service
|
|
104
|
+
token. Keep long work in its actual native owner and return its handle promptly.
|
|
105
|
+
For example sclet submits `kernel.run` with `waitMs: 0`, then observes the same
|
|
106
|
+
cell with `kernel.read`. Do not create a second scientific state store in Rho.
|
|
107
|
+
|
|
108
|
+
## Agent and view entries
|
|
109
|
+
|
|
110
|
+
Agents discover installed packages through `extension.list`, read their schema
|
|
111
|
+
with `extension.describe`, and read a Skill through `extension.content` with
|
|
112
|
+
`kind: "skill"`. These operations are available through the existing layered MCP
|
|
113
|
+
discovery. They then call:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"pluginId": "lab.my-plugin",
|
|
118
|
+
"expectedDigest": "64-character digest from extension.describe",
|
|
119
|
+
"operation": "greet",
|
|
120
|
+
"input": { "name": "World" },
|
|
121
|
+
"changeId": "original-request-1"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This is the body of `extension.call`; direct HTTP clients also supply `projectId`.
|
|
126
|
+
The returned receipt separates plugin completion from the native result in
|
|
127
|
+
`value`. Native invocations retain pending/completed/unknown receipts and delegated
|
|
128
|
+
requests/replies. Repeat the **exact** request to observe the same receipt, even
|
|
129
|
+
after update or removal. `extension.read` reads it by `invocationId` for the same
|
|
130
|
+
caller/project. A restart converts pending receipts to unknown; it never replays
|
|
131
|
+
them. Read-only calls do not accumulate durable receipts. No plugin receipt makes
|
|
132
|
+
native work Core-managed or establishes scientific validity.
|
|
133
|
+
|
|
134
|
+
A view runs in an iframe with an opaque origin, no service credential or storage
|
|
135
|
+
access, and no network loading. Bundle assets inline or as data URLs. Its bridge
|
|
136
|
+
is restricted to the selected package and digest:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
const receipt = await rho.call('greet', { name: 'World' });
|
|
140
|
+
output.textContent = receipt.state === 'completed'
|
|
141
|
+
? receipt.value.greeting : receipt.error;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Pass a third argument to `rho.call` to retain an explicit original changeId.
|
|
145
|
+
The default generates one per deliberate call. A stale digest is refused; reopen
|
|
146
|
+
the view after an update. Several views may be declared and selected in the plugin
|
|
147
|
+
panel. These runtime views currently live on Plugins, not in Studio's resource
|
|
148
|
+
binding/layout contract. Existing compiled React extensions still use
|
|
149
|
+
[UI extension authoring](PLUGIN-AUTHORING.md).
|
|
150
|
+
|
|
151
|
+
## Contribute and distribute
|
|
152
|
+
|
|
153
|
+
Keep your repository, license, dependencies and release history independent. Check
|
|
154
|
+
the packed artifact, not just source: `npm pack --ignore-scripts`, then install
|
|
155
|
+
that tarball in a disposable Rho project. Exercise missing prerequisites, an input
|
|
156
|
+
error, recovery, update and removal. Record the Rho/API version, package digest,
|
|
157
|
+
native versions and actual result. Preserve user data and avoid credentials in
|
|
158
|
+
examples or diagnostics. Publish to your own npm namespace or share a public Git
|
|
159
|
+
source/tarball; Rho does not publish packages on your behalf.
|
|
160
|
+
|
|
161
|
+
No central registration is needed to install a package. To be discoverable in the
|
|
162
|
+
bundled directory, submit a PR editing
|
|
163
|
+
[`src/extension-directory.ts`](../src/extension-directory.ts), linking the public
|
|
164
|
+
source, license, author guide and a reproducible acceptance record. Propose an
|
|
165
|
+
immutable Git commit or exact npm version. Maintainers check the declared scope;
|
|
166
|
+
directory inclusion is not a security audit, release signature or scientific
|
|
167
|
+
endorsement. Future releases of the package can update that entry independently.
|
|
168
|
+
|
|
169
|
+
## Resource limits and current scope
|
|
170
|
+
|
|
171
|
+
Use the service's `--limits` JSON file with an `extensions` object to override
|
|
172
|
+
limits. Exceeding a limit refuses new work, retains the previous selected package
|
|
173
|
+
and obtained receipts, or records an unknown result after an interrupted call.
|
|
174
|
+
|
|
175
|
+
| Protected resource | Default configuration |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| Installed selections / retained revisions | `maxPlugins: 32`, `maxVersions: 128` |
|
|
178
|
+
| Admitted runtime tree | `maxFiles: 20000`, `maxPackageBytes: 134217728` |
|
|
179
|
+
| Skill/view bytes | `maxContentBytes: 524288` |
|
|
180
|
+
| Durable invocations / receipt and result bytes | `maxReceipts: 2000`, `maxResultBytes: 1048576` |
|
|
181
|
+
| Concurrent calls / delegated calls per invocation | `maxRunning: 4`, `maxDelegations: 32` |
|
|
182
|
+
| Worker / each install subprocess | `timeoutMs: 30000`, `installTimeoutMs: 180000` |
|
|
183
|
+
|
|
184
|
+
The tree-size check applies after npm staging; it is not a disk quota on npm/Git.
|
|
185
|
+
A timed-out worker does not prove a delegated native task stopped. Observe its
|
|
186
|
+
original identity. Retention limits refuse rather than silently evict identities;
|
|
187
|
+
raise them deliberately when appropriate. Manifests are capped at 64 KiB, with
|
|
188
|
+
32 operations and 16 entries of each content kind. The browser permits four
|
|
189
|
+
pending calls per view. macOS with Node 22 is observed in this episode; Windows,
|
|
190
|
+
Linux and independent developer usability have not been established for this path.
|
|
@@ -390,3 +390,71 @@ fetched; an explicit response excludes replies already present in that context.
|
|
|
390
390
|
ACP retains Codex's native MCP spelling while identifying its operation, and names
|
|
391
391
|
an otherwise unnamed permission from the original tool update. These observations
|
|
392
392
|
do not expand native permission or grant access to arbitrary session history.
|
|
393
|
+
|
|
394
|
+
## External conversations
|
|
395
|
+
|
|
396
|
+
Agent 0.4.29 exports `dsh-session` for a Harness adapter to retain a read-only
|
|
397
|
+
observation of a native version-4 session. `observeDshSession(projectRoot, header,
|
|
398
|
+
events)` requires the real native working folder to equal the selected project.
|
|
399
|
+
It preserves session, turn and tool-call identities. Repeated cuts replace the
|
|
400
|
+
same record; a changed prefix or backwards cursor is refused. It never creates
|
|
401
|
+
an Agent, executes a tool, changes a grant or resumes a runtime.
|
|
402
|
+
|
|
403
|
+
`external-session` supplies bounded `listExternalSessions` and
|
|
404
|
+
`readExternalSession` reads. The operation contracts are `agent.sessions.list`
|
|
405
|
+
and `agent.sessions.read`; an embedding application supplies authorization.
|
|
406
|
+
`ui-external-session` renders prompts, visible replies, operations and results,
|
|
407
|
+
with input explicitly retained by the native client. Reasoning, injected context
|
|
408
|
+
and system messages are excluded; credential-shaped strings are redacted.
|
|
409
|
+
This is an observation of the client's reports, not an independent execution proof.
|
|
410
|
+
|
|
411
|
+
A successful resource-registration reply carries its exact resource identity and
|
|
412
|
+
version. The application must verify that tuple against its own project resources.
|
|
413
|
+
A native deliverable without a receipt is only a reported path, with no historical
|
|
414
|
+
bytes asserted. Tool return, native exit code and turn completion remain separate.
|
|
415
|
+
An unfinished turn with no observation for 10 seconds has an unknown outcome.
|
|
416
|
+
|
|
417
|
+
`externalSessionLimits` names the fixed local observation budgets: 64 sessions
|
|
418
|
+
per project, 200 retained turns per session, 256 tools per turn, 32,768 source
|
|
419
|
+
events / 32 MiB per cut, 32,768 characters per prompt/reply and earlier-reply
|
|
420
|
+
collection, 8,192 characters per tool field, and 4 MiB per stored record.
|
|
421
|
+
Text/tools/earlier turns beyond their budgets are omitted with counts. An
|
|
422
|
+
oversized source, conflicting cut, invalid record or busy writer retains the
|
|
423
|
+
previous record and reports failure. Storage is `.rho/agent/external`, contained
|
|
424
|
+
in the project without symlink directories; atomic replacement leaves readers a
|
|
425
|
+
complete record. A positively dead PID lock can be reclaimed; an unknown/live
|
|
426
|
+
lock cannot. Reads create no directories or runtime state. These limits protect
|
|
427
|
+
local storage and observation work; they do not limit the native Agent.
|
|
428
|
+
|
|
429
|
+
`test/external-session.test.mjs` checks identity, prefix conflicts, omissions,
|
|
430
|
+
missing outcomes, redaction, project containment, retained failure and pagination.
|
|
431
|
+
The Rho application records its separate real Harness/browser acceptance.
|
|
432
|
+
|
|
433
|
+
### Native Claude Code and Codex sessions
|
|
434
|
+
|
|
435
|
+
Agent 0.4.30 exports `native-sessions`. `listNativeSessions(root, nativeSources(env))`
|
|
436
|
+
lists the Claude Code and Codex sessions whose recorded working folder is `root`,
|
|
437
|
+
read from the Agents' own records: `CLAUDE_CONFIG_DIR` (or `~/.claude`)
|
|
438
|
+
`/projects/<folder>/<id>.jsonl` and `CODEX_HOME` (or `~/.codex`)
|
|
439
|
+
`/sessions/**/rollout-*.jsonl`. Each summary keeps the native session ID, title,
|
|
440
|
+
times, turn counts, where it started (Claude Code's `entrypoint`, Codex's
|
|
441
|
+
`originator`), whether another process holds it now (Claude Code's live
|
|
442
|
+
`sessions/<pid>.json` with a running PID, Codex's `thread-writer-locks/<id>.lock`)
|
|
443
|
+
and the terminal resume command. `readNativeSession` streams one record into the
|
|
444
|
+
same turn shape as `readExternalSession`, with each turn's tools, results and
|
|
445
|
+
reply. Reading starts no Agent, takes no lock and writes nothing.
|
|
446
|
+
`nativeSessionLimits` names the budgets: 64 sessions per listing, 4,096 files
|
|
447
|
+
scanned per Claude Code folder and across Codex's sessions, 256 KiB read from each end of a file for a summary, at most 256 MiB
|
|
448
|
+
streamed from the end for a read, 200 turns, 256 tools per turn, and the text and
|
|
449
|
+
tool field limits above. Sidechains, injected reminders and local command output
|
|
450
|
+
are excluded. A recorded interruption is stopped. A last turn without an end record
|
|
451
|
+
is running while its file was written in the last minute; after that it is completed
|
|
452
|
+
when it has a reply and no unanswered tool, and unknown otherwise.
|
|
453
|
+
|
|
454
|
+
To continue such a session, the application passes its ID as `sessionId` (ACP
|
|
455
|
+
`session/load`). `forkFrom` instead asks the Agent for a separate copy through
|
|
456
|
+
`session/fork`; the connection is refused before sending when the Agent does not
|
|
457
|
+
advertise fork, or returns the same ID. `acpEnvironment` drops inherited Claude Code
|
|
458
|
+
session markers (`CLAUDECODE`, `CLAUDE_CODE_CHILD_SESSION` and related variables):
|
|
459
|
+
a Claude Code started under them saves no transcript, so its session could not be
|
|
460
|
+
listed or continued anywhere else.
|
|
@@ -7,6 +7,7 @@ export declare function acpCommand(agent: AcpAgent): {
|
|
|
7
7
|
* optional dependencies that an application need not install, so name the selected
|
|
8
8
|
* executable, else the one already named in the environment, else the client on PATH. */
|
|
9
9
|
export declare function acpEnvironment(agent: AcpAgent, env: NodeJS.ProcessEnv, executable?: string): NodeJS.ProcessEnv;
|
|
10
|
+
export declare function withoutSessionMarkers(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
10
11
|
/** Why the adapter could not start the person's client, or null when its executable is
|
|
11
12
|
* there. The adapters report a missing client only as a closed connection. */
|
|
12
13
|
export declare function missingClient(agent: AcpAgent, env: NodeJS.ProcessEnv): string | null;
|
|
@@ -14,7 +14,15 @@ const clientName = { claude: 'Claude Code', codex: 'Codex' };
|
|
|
14
14
|
* executable, else the one already named in the environment, else the client on PATH. */
|
|
15
15
|
export function acpEnvironment(agent, env, executable) {
|
|
16
16
|
const name = executableVariable[agent];
|
|
17
|
-
return { ...env, [name]: executable || env[name] || agent };
|
|
17
|
+
return { ...withoutSessionMarkers(env), [name]: executable || env[name] || agent };
|
|
18
|
+
}
|
|
19
|
+
// When Rho itself was started from inside a Claude Code session (an Agent ran the setup), the
|
|
20
|
+
// environment carries that session's markers. A Claude Code started with them takes itself for
|
|
21
|
+
// its child: it saves no transcript (so its session cannot be continued anywhere) and reports to
|
|
22
|
+
// the parent's socket. Rho's sessions are the person's own, so those markers are not passed on.
|
|
23
|
+
const sessionMarkers = /^(CLAUDECODE|CLAUDE_CODE_(CHILD_SESSION|ENTRYPOINT|SESSION_ID|SESSION_ATTENDED|MESSAGING_SOCKET|MESSAGING_TOKEN|EXECPATH|SSE_PORT)|CLAUDE_PID)$/;
|
|
24
|
+
export function withoutSessionMarkers(env) {
|
|
25
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !sessionMarkers.test(key)));
|
|
18
26
|
}
|
|
19
27
|
/** Why the adapter could not start the person's client, or null when its executable is
|
|
20
28
|
* there. The adapters report a missing client only as a closed connection. */
|
|
@@ -211,6 +211,16 @@ export class AcpConversation {
|
|
|
211
211
|
throw new Error('This Agent cannot load the original session. Nothing was resent.');
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
+
else if (o.forkFrom) {
|
|
215
|
+
const caps = initialized.agentCapabilities;
|
|
216
|
+
if (!caps?.sessionCapabilities?.fork)
|
|
217
|
+
throw new Error('This Agent cannot continue a copy of a session. Nothing was sent.');
|
|
218
|
+
const forked = await connection.agent.request('session/fork', { ...params, sessionId: o.forkFrom });
|
|
219
|
+
if (typeof forked?.sessionId !== 'string' || forked.sessionId === o.forkFrom)
|
|
220
|
+
throw new Error('The Agent did not create a separate copy of the session. Nothing was sent.');
|
|
221
|
+
this.sessionId = forked.sessionId;
|
|
222
|
+
this.settings = forked.configOptions ?? [];
|
|
223
|
+
}
|
|
214
224
|
else {
|
|
215
225
|
const created = await connection.agent.request(acp.methods.agent.session.new, params);
|
|
216
226
|
this.sessionId = created.sessionId;
|
|
@@ -10,5 +10,6 @@ export interface CaptureOptions {
|
|
|
10
10
|
maxEventBytes?: number;
|
|
11
11
|
}
|
|
12
12
|
export declare function canonicalActivity(value: any): string;
|
|
13
|
+
export declare function activityPayload(value: any, options: CaptureOptions): any;
|
|
13
14
|
export declare function normalizeClaude(records: any[], sessionId: string, project: string, options?: CaptureOptions): ObservedEvent[];
|
|
14
15
|
export declare function normalizeDsh(records: any[], options?: CaptureOptions): ObservedEvent[];
|
|
@@ -9,7 +9,7 @@ export function canonicalActivity(value) {
|
|
|
9
9
|
const sensitive = /(?:^|[_-])(?:authorization|cookie|password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|token)$/i;
|
|
10
10
|
const hidden = /^(?:thinking|reasoning|reasoning_content|signature)$/i;
|
|
11
11
|
const txt = (v) => typeof v === 'string' ? v : Array.isArray(v) ? v.filter(b => b?.type === 'text').map(b => b.text).join('\n') : '';
|
|
12
|
-
function
|
|
12
|
+
export function activityPayload(value, options) {
|
|
13
13
|
let redacted = false, truncated = false;
|
|
14
14
|
const cap = options.maxFieldBytes ?? 8192;
|
|
15
15
|
const identities = new Set(['native_id', 'call_id', 'root_call_id', 'job_id', 'parent_message_id']);
|
|
@@ -76,7 +76,7 @@ function inputSummary(input) {
|
|
|
76
76
|
function chained(items, options) {
|
|
77
77
|
let prefix = hash(activityNormalizer);
|
|
78
78
|
return items.map(({ kind, data }, sequence) => {
|
|
79
|
-
const safe =
|
|
79
|
+
const safe = activityPayload(data, options);
|
|
80
80
|
prefix = hash(prefix + canonicalActivity({ kind, data: safe }));
|
|
81
81
|
return { sequence, kind, data: { ...safe, prefix_sha256: prefix } };
|
|
82
82
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ExternalSessionRecord } from './external-session.js';
|
|
2
|
+
export declare function externalProjectPath(root: string, input: unknown): string | null;
|
|
3
|
+
/** Project the native committed log. System prompts, injected context and reasoning
|
|
4
|
+
* are excluded. Original session, turn and call identities survive every backfill. */
|
|
5
|
+
export declare function projectDshSession(root: string, header: any, events: readonly any[], observedAt?: string): ExternalSessionRecord;
|
|
6
|
+
export declare function observeDshSession(root: string, header: any, events: readonly any[]): import("./external-session.js").ExternalSessionSummary;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { activityPayload, canonicalActivity } from './activity-normalize.js';
|
|
5
|
+
import { externalSessionId, retainedExternalSession, saveExternalSession } from './external-session.js';
|
|
6
|
+
import { externalSessionLimits as limits } from './external-session-contract.js';
|
|
7
|
+
const text = (v) => typeof v === 'string' ? v : Array.isArray(v) ? v.filter(b => b?.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n') : '';
|
|
8
|
+
const time = (v) => { const n = typeof v === 'number' ? v : Date.parse(v); if (!Number.isFinite(n))
|
|
9
|
+
throw new Error('Native event has no valid timestamp.'); return new Date(n).toISOString(); };
|
|
10
|
+
const hash = (v) => createHash('sha256').update(canonicalActivity(v)).digest('hex');
|
|
11
|
+
const safe = (value, cap = limits.maxTextChars) => (activityPayload({ text: value.replace(/\b([\w-]*(?:token|cookie|authorization)["']?\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s'";]+)/gi, '$1[redacted]') }, { content: true, maxFieldBytes: cap * 4, maxEventBytes: cap * 8 }).text ?? '').slice(0, cap).replace(/[\uD800-\uDBFF]$/, '');
|
|
12
|
+
export function externalProjectPath(root, input) {
|
|
13
|
+
if (typeof input !== 'string' || input.length > 1024 || input.includes('\0'))
|
|
14
|
+
return null;
|
|
15
|
+
const relative = path.relative(root, path.resolve(root, input)).split(path.sep).join('/');
|
|
16
|
+
return relative && !relative.startsWith('../') && !path.isAbsolute(relative) && !relative.split('/').some(p => ['..', '.rho', '.git'].includes(p)) ? relative : null;
|
|
17
|
+
}
|
|
18
|
+
function paths(root, args, content) {
|
|
19
|
+
const candidates = [args?.path, args?.file_path, args?.filename, args?.declaration?.path];
|
|
20
|
+
// These are explicitly labelled mentions, never inferred file writes or historical snapshots.
|
|
21
|
+
for (const m of content.matchAll(/(?<![\w/.:\-])\/?(?:[\w.\-\u0080-\uFFFF]+\/)*[\w.\-\u0080-\uFFFF]+\.(?:R|r|py|md|csv|tsv|png|svg|pdf|json|txt|qmd|Rmd)\b/g))
|
|
22
|
+
candidates.push(m[0]);
|
|
23
|
+
return [...new Set(candidates.map(p => externalProjectPath(root, p)).filter((p) => !!p))].slice(0, 32);
|
|
24
|
+
}
|
|
25
|
+
/** Project the native committed log. System prompts, injected context and reasoning
|
|
26
|
+
* are excluded. Original session, turn and call identities survive every backfill. */
|
|
27
|
+
export function projectDshSession(root, header, events, observedAt = new Date().toISOString()) {
|
|
28
|
+
root = fs.realpathSync(root);
|
|
29
|
+
if (!header || header.version !== 4 || typeof header.id !== 'string' || !/^[A-Za-z0-9._:@-]{1,128}$/.test(header.id) || typeof header.cwd !== 'string' || fs.realpathSync(header.cwd) !== root)
|
|
30
|
+
throw new Error('Harness session does not match the selected project.');
|
|
31
|
+
if (events.length > limits.maxEvents || Buffer.byteLength(JSON.stringify(events)) > limits.maxSourceBytes)
|
|
32
|
+
throw new Error('Harness observation exceeds source limits; previous observation retained.');
|
|
33
|
+
const turns = [], byTurn = new Map(), tools = new Map();
|
|
34
|
+
let current, title = '', lastAt = time(header.createdAt), omittedTurns = 0;
|
|
35
|
+
const makeTurn = (id, at) => {
|
|
36
|
+
let t = byTurn.get(id);
|
|
37
|
+
if (t)
|
|
38
|
+
return t;
|
|
39
|
+
t = { id: 'turn:' + id, at, state: 'unknown', prompt: '', reply: '', messages: [], tools: [], results: [], omittedTools: 0, omittedChars: 0 };
|
|
40
|
+
byTurn.set(id, t);
|
|
41
|
+
turns.push(t);
|
|
42
|
+
if (turns.length > limits.maxTurns) {
|
|
43
|
+
const gone = turns.shift();
|
|
44
|
+
omittedTurns++;
|
|
45
|
+
for (const [k, v] of tools)
|
|
46
|
+
if (v.turn === gone)
|
|
47
|
+
tools.delete(k);
|
|
48
|
+
for (const [k, v] of byTurn)
|
|
49
|
+
if (v === gone)
|
|
50
|
+
byTurn.delete(k);
|
|
51
|
+
}
|
|
52
|
+
return t;
|
|
53
|
+
};
|
|
54
|
+
const appendText = (t, field, value, replace = false) => {
|
|
55
|
+
const full = replace ? value : t[field] + (t[field] ? '\n\n' : '') + value;
|
|
56
|
+
t[field] = safe(full);
|
|
57
|
+
t.omittedChars += Math.max(0, full.length - t[field].length);
|
|
58
|
+
};
|
|
59
|
+
for (let i = 0; i < events.length; i++) {
|
|
60
|
+
const e = events[i];
|
|
61
|
+
if (e.seq !== i)
|
|
62
|
+
throw new Error('Harness source has a missing or reordered native event.');
|
|
63
|
+
const d = e.data ?? {}, at = time(e.time);
|
|
64
|
+
lastAt = at;
|
|
65
|
+
if (e.type === 'session/title' && typeof d.title === 'string')
|
|
66
|
+
title = safe(d.title, 512);
|
|
67
|
+
if (e.type === 'turn/start') {
|
|
68
|
+
current = makeTurn(d.turn, at);
|
|
69
|
+
current.state = 'running';
|
|
70
|
+
}
|
|
71
|
+
if (e.type === 'user/message' && d.source?.kind === 'user') {
|
|
72
|
+
if (!current)
|
|
73
|
+
current = makeTurn(e.seq, at);
|
|
74
|
+
appendText(current, 'prompt', text(d.content));
|
|
75
|
+
}
|
|
76
|
+
if (e.type === 'assistant/message') {
|
|
77
|
+
const t = byTurn.get(d.turn) ?? current;
|
|
78
|
+
if (!t)
|
|
79
|
+
continue;
|
|
80
|
+
const message = text(d.message?.content);
|
|
81
|
+
if (message) {
|
|
82
|
+
if (t.reply)
|
|
83
|
+
t.messages.push({ at, text: t.reply });
|
|
84
|
+
while (t.messages.length > 200 || t.messages.reduce((n, m) => n + m.text.length, 0) > limits.maxTextChars)
|
|
85
|
+
t.omittedChars += t.messages.shift().text.length;
|
|
86
|
+
appendText(t, 'reply', message, true);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (e.type === 'tool/call') {
|
|
90
|
+
const t = byTurn.get(d.turn) ?? current;
|
|
91
|
+
if (!t || typeof d.callId !== 'string' || typeof d.name !== 'string')
|
|
92
|
+
continue;
|
|
93
|
+
if (tools.has(d.callId))
|
|
94
|
+
throw new Error('Harness call identity was reused.');
|
|
95
|
+
if (t.tools.length >= limits.maxTools) {
|
|
96
|
+
t.omittedTools++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
let args = d.arguments;
|
|
100
|
+
if (typeof args === 'string') {
|
|
101
|
+
try {
|
|
102
|
+
args = JSON.parse(args);
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
}
|
|
106
|
+
const name = d.name === 'mcp__rho__rho_call' && typeof args?.operation === 'string' ? args.operation : d.name;
|
|
107
|
+
const input = args?.input ?? args;
|
|
108
|
+
const detail = safe(typeof input?.command === 'string' ? input.command : JSON.stringify(input) ?? '', limits.maxDetailChars);
|
|
109
|
+
const tool = { id: d.callId, at, name, detail, output: '', outcome: 'requested', paths: paths(root, input, detail) };
|
|
110
|
+
t.tools.push(tool);
|
|
111
|
+
tools.set(d.callId, { turn: t, tool });
|
|
112
|
+
}
|
|
113
|
+
if (e.type === 'tool/result') {
|
|
114
|
+
const message = d.message, item = tools.get(message?.toolCallId ?? message?.source?.callId);
|
|
115
|
+
if (!item)
|
|
116
|
+
continue;
|
|
117
|
+
const output = text(message?.content);
|
|
118
|
+
item.tool.output = safe(output, limits.maxDetailChars);
|
|
119
|
+
item.tool.outcome = message?.isError ? 'failed' : 'returned';
|
|
120
|
+
item.turn.omittedChars += Math.max(0, output.length - item.tool.output.length);
|
|
121
|
+
// Only a successful Rho registration/capture reply asserts an exact resource version.
|
|
122
|
+
if (['resource.register', 'resource.capture', 'agent.activity.capture'].includes(item.tool.name) && !message?.isError) {
|
|
123
|
+
try {
|
|
124
|
+
const response = JSON.parse(output), r = response.value?.resource ?? response.value;
|
|
125
|
+
const relative = externalProjectPath(root, r?.path);
|
|
126
|
+
if (response.ok === true && relative && /^[a-f0-9]{64}$/.test(r.id) && /^[a-f0-9]{64}$/.test(r.version) && typeof r.projectId === 'string' && item.turn.results.length < 64)
|
|
127
|
+
item.turn.results.push({ path: relative, id: r.id, version: r.version, projectId: r.projectId, toolId: item.tool.id });
|
|
128
|
+
}
|
|
129
|
+
catch { /* The bounded receipt can be incomplete; never fabricate a resource. */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (e.type === 'job/settled') {
|
|
133
|
+
const item = tools.get(d.toolCall?.rootCallId ?? d.toolCall?.callId), exit = typeof d.detail === 'string' ? /^exit code: (-?\d+)$/.exec(d.detail) : null;
|
|
134
|
+
if (item && exit)
|
|
135
|
+
item.tool.exitCode = Number(exit[1]);
|
|
136
|
+
}
|
|
137
|
+
if (e.type === 'deliverables/presented') {
|
|
138
|
+
const t = byTurn.get(d.turn) ?? current;
|
|
139
|
+
if (!t)
|
|
140
|
+
continue;
|
|
141
|
+
for (const file of d.files ?? []) {
|
|
142
|
+
const relative = externalProjectPath(root, file.path);
|
|
143
|
+
if (relative && !t.results.some(r => r.path === relative) && t.results.length < 64)
|
|
144
|
+
t.results.push({ path: relative, toolId: d.callId ?? 'event:' + e.seq });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (e.type === 'turn/end') {
|
|
148
|
+
const t = byTurn.get(d.turn);
|
|
149
|
+
if (!t)
|
|
150
|
+
continue;
|
|
151
|
+
t.state = d.reason?.kind === 'completed' ? 'completed' : d.reason?.kind === 'cancelled' ? 'stopped' : d.reason?.kind === 'error' ? 'failed' : 'unknown';
|
|
152
|
+
t.endedAt = at;
|
|
153
|
+
for (const tool of t.tools)
|
|
154
|
+
if (tool.outcome === 'requested')
|
|
155
|
+
tool.outcome = 'unknown';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const summary = { id: externalSessionId('dsh', header.id), producer: 'dsh', sessionId: header.id, title: title || turns.find(t => t.prompt)?.prompt.slice(0, 160) || 'Harness conversation', lastAt, observedAt,
|
|
159
|
+
state: turns.at(-1)?.state ?? 'unknown', turns: turns.length + omittedTurns, sourceCursor: events.length, omittedTurns, inputOwner: 'external' };
|
|
160
|
+
const result = { format: 1, projectRoot: root, summary, turns, sourceDigest: hash(events) };
|
|
161
|
+
// A suffix is retained at the record budget; gaps are explicit, earlier files stay untouched.
|
|
162
|
+
while (Buffer.byteLength(JSON.stringify(result)) > limits.maxRecordBytes && result.turns.length > 1) {
|
|
163
|
+
result.turns.shift();
|
|
164
|
+
result.summary.omittedTurns++;
|
|
165
|
+
}
|
|
166
|
+
if (Buffer.byteLength(JSON.stringify(result)) > limits.maxRecordBytes)
|
|
167
|
+
throw new Error('Harness turn exceeds maxRecordBytes; previous observation retained.');
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
export function observeDshSession(root, header, events) {
|
|
171
|
+
const old = retainedExternalSession(root, 'dsh', header.id);
|
|
172
|
+
const value = projectDshSession(root, header, events);
|
|
173
|
+
return saveExternalSession(root, value, old ? hash(events.slice(0, old.summary.sourceCursor)) : undefined);
|
|
174
|
+
}
|