@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.
Files changed (161) hide show
  1. package/dist/agent-connect.d.ts +8 -0
  2. package/dist/agent-connect.js +15 -0
  3. package/dist/browser-access.js +3 -2
  4. package/dist/capabilities.d.ts +2 -2
  5. package/dist/capabilities.js +6 -1
  6. package/dist/cli.js +219 -176
  7. package/dist/contract.generated.d.ts +472 -4
  8. package/dist/contract.generated.js +1 -1
  9. package/dist/contracts/owners.generated.js +455 -6
  10. package/dist/conversations.d.ts +157 -4
  11. package/dist/conversations.js +69 -6
  12. package/dist/discovery.d.ts +1 -0
  13. package/dist/discovery.js +3 -3
  14. package/dist/extension-contract.d.ts +99 -0
  15. package/dist/extension-contract.js +52 -0
  16. package/dist/extension-directory.d.ts +9 -0
  17. package/dist/extension-directory.js +11 -0
  18. package/dist/extension-worker.d.ts +1 -0
  19. package/dist/extension-worker.js +36 -0
  20. package/dist/extensions.d.ts +157 -0
  21. package/dist/extensions.js +349 -0
  22. package/dist/plugin-cli.d.ts +1 -0
  23. package/dist/plugin-cli.js +139 -0
  24. package/dist/plugin-inventory.json +18 -0
  25. package/dist/plugins.d.ts +3 -3
  26. package/dist/project-work.d.ts +3 -1
  27. package/dist/project-work.js +15 -2
  28. package/dist/protocol.d.ts +8 -0
  29. package/dist/protocol.js +16 -7
  30. package/dist/server.d.ts +5 -10
  31. package/dist/server.js +122 -12
  32. package/dist/web/.vite/manifest.json +67 -67
  33. package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
  34. package/dist/web/assets/choices-D-YfnFP6.js +1 -0
  35. package/dist/web/assets/{console-Dbdo-5oO.js → console-Trqc0swD.js} +1 -1
  36. package/dist/web/assets/{dist-B2V0VDO0.js → dist-DZmszrjG.js} +2 -2
  37. package/dist/web/assets/dist-DpIHRk3B.js +1 -0
  38. package/dist/web/assets/{dist-D25Vn17i.js → dist-eMx6b8N1.js} +1 -1
  39. package/dist/web/assets/dist-pF902jHA.js +3 -0
  40. package/dist/web/assets/explore-in-BCHqjJ7B.js +1 -0
  41. package/dist/web/assets/graph-lENgfQNH.js +2 -0
  42. package/dist/web/assets/index-B3_OVy91.css +1 -0
  43. package/dist/web/assets/{index-BHQIaDsx.js → index-BpL_Kl_s.js} +87 -86
  44. package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-DcVGUBBY.js} +1 -1
  45. package/dist/web/assets/overview-CDnVKlOt.js +6 -0
  46. package/dist/web/assets/preview-BEC1E9MK.js +1 -0
  47. package/dist/web/assets/{review-CmWou4Iv.js → review-DYP0aCXb.js} +1 -1
  48. package/dist/web/assets/{skills-Dy7n3L9d.js → skills-1ec0Z-jx.js} +1 -1
  49. package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-DNexV4OJ.js} +1 -1
  50. package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-EYQgikxq.js} +2 -2
  51. package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-Chj2Gbx7.js} +1 -1
  52. package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-CT9RVcWY.js} +1 -1
  53. package/dist/web/assets/workspace-COmCbGV3.js +5 -0
  54. package/dist/web/composition.json +65 -65
  55. package/dist/web/index.html +3 -3
  56. package/dist/web/renderer.json +1 -1
  57. package/dist/web-assets.d.ts +2 -1
  58. package/dist/web-assets.js +3 -2
  59. package/dist/work-contract.js +2 -2
  60. package/docs/FRONTEND.md +26 -18
  61. package/docs/HEADLESS.md +38 -28
  62. package/docs/PLUGIN-AUTHORING.md +4 -0
  63. package/docs/THIRD-PARTY-PLUGINS.md +190 -0
  64. package/node_modules/@rho-workspace/plugin-agent/README.md +68 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
  66. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
  67. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  68. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
  69. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
  70. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
  71. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
  72. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
  73. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
  74. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +20 -0
  75. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +84 -0
  76. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
  77. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
  78. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
  79. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  80. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  81. package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
  82. package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
  83. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
  84. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
  85. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
  86. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
  87. package/node_modules/@rho-workspace/plugin-agent/package.json +6 -1
  88. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  89. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  90. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  91. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  92. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  93. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  94. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  95. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  96. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  97. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  98. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  99. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  100. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  101. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  102. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  103. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  104. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  105. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  106. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  107. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  108. package/node_modules/hono/dist/jsx/base.js +49 -32
  109. package/node_modules/hono/dist/jsx/components.js +2 -2
  110. package/node_modules/hono/dist/jsx/constants.js +2 -1
  111. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  112. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  113. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  114. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  115. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  116. package/node_modules/hono/dist/utils/cookie.js +2 -2
  117. package/node_modules/hono/dist/utils/mime.js +1 -1
  118. package/node_modules/hono/dist/utils/url.js +1 -1
  119. package/node_modules/hono/package.json +1 -1
  120. package/package.json +5 -3
  121. package/protocol/index.json +93 -33
  122. package/protocol/operations/agent.sessions.list.json +146 -0
  123. package/protocol/operations/agent.sessions.read.json +315 -0
  124. package/protocol/operations/conversation.configure.json +29 -0
  125. package/protocol/operations/conversation.decide.json +29 -0
  126. package/protocol/operations/conversation.list.json +29 -0
  127. package/protocol/operations/conversation.permission.json +29 -0
  128. package/protocol/operations/conversation.read.json +29 -0
  129. package/protocol/operations/conversation.restore.json +29 -0
  130. package/protocol/operations/conversation.send.json +29 -0
  131. package/protocol/operations/conversation.settings.check.json +92 -0
  132. package/protocol/operations/conversation.settings.read.json +92 -0
  133. package/protocol/operations/conversation.settings.update.json +92 -0
  134. package/protocol/operations/conversation.start.json +46 -1
  135. package/protocol/operations/conversation.stop.json +29 -0
  136. package/protocol/operations/conversation.trash.json +29 -0
  137. package/protocol/operations/extension.call.json +132 -0
  138. package/protocol/operations/extension.content.json +78 -0
  139. package/protocol/operations/extension.describe.json +195 -0
  140. package/protocol/operations/extension.install.json +198 -0
  141. package/protocol/operations/extension.list.json +123 -0
  142. package/protocol/operations/extension.manage.json +55 -0
  143. package/protocol/operations/extension.read.json +112 -0
  144. package/protocol/operations/observation.snapshot.json +29 -0
  145. package/protocol/operations/project.work.read.json +3 -2
  146. package/protocol/operations/session.grant.json +9 -0
  147. package/protocol/operations/session.instructions.json +43 -0
  148. package/scripts/third-party-acceptance.mjs +77 -0
  149. package/vendor/composition.json +83 -83
  150. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  151. package/web/theme.css +12 -0
  152. package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
  153. package/dist/web/assets/dist-BV3hWfn_.js +0 -3
  154. package/dist/web/assets/dist-CLNF2g-E.js +0 -1
  155. package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
  156. package/dist/web/assets/graph-hS2kvHTI.js +0 -2
  157. package/dist/web/assets/index-Cj1_iqyK.css +0 -1
  158. package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
  159. package/dist/web/assets/preview-n5lOOdjN.js +0 -1
  160. package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
  161. 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 0.5.1
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 Claude Code or Codex
135
+ ### Connect your own Agent
136
136
 
137
- The running service's operator connection adds Rho to the person's own Claude Code
138
- or Codex for one project, as the project page's "Add to …" button does:
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
- ```sh
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 --project PROJECT_FOLDER [--session NAME]
149
+ rho --grant /absolute/service-state/connection.json \
150
+ --project-id EXISTING_PROJECT_ID --session my-agent
156
151
  ```
157
152
 
158
- `--project` is the folder relative to `--root`; `--session` defaults to
159
- `claude-code`, the session `--agent claude-code` uses, and `--session codex` is the one
160
- `--agent codex` uses. When the service is not running, both forms say so and exit 1. The JSON reply names the project, the private session connection
161
- file, the number of granted methods (the server default), an `mcp` entry
162
- (`name`, `command`, `args`) for an MCP configuration file, and the equivalent
163
- `claude mcp add rho-<folder> -- … --mcp <file>` command. `rho --mcp FILE` serves
164
- that grant on stdio with the four tools `rho_index`, `rho_resolve`, `rho_call` and
165
- `rho_read`; nothing else writes to stdout in that mode. Every connection file is a
166
- credential. `rho --help` prints the usage; an unknown option exits 2 with it.
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. |
@@ -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. */
@@ -8,6 +8,7 @@ export interface AcpConversationOptions {
8
8
  mcpServers: AgentMcpServer[];
9
9
  limits: AgentProcessLimits;
10
10
  sessionId?: string;
11
+ forkFrom?: string;
11
12
  command: {
12
13
  file: string;
13
14
  args: string[];
@@ -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 payload(value, options) {
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 = payload(data, options);
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
+ }