dsh-bailinghub 0.3.0 → 0.5.0

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.
@@ -1,8 +1,403 @@
1
1
  # Agent Client Host Adapter Contract
2
2
 
3
- Status: public native Agent Client contract for `dsh-bailinghub@0.3.0`. This contract is not part
4
- of the legacy public `0.1.x` line. The multi-connection lifecycle described below is stable with
5
- BailingHub Core `0.5.1` and `bailinghub-mcp-server@0.3.0`.
3
+ ## Authorization subject display (0.5.0)
4
+
5
+ The business backend may supply `subject_display: { name }` for the subject actually approved by
6
+ the user. A subject can be an organization, account, project, department, workspace or another
7
+ business entity; the upstream contract does not prescribe a store or any particular product.
8
+ The name is display data, separate from both trusted authorization identity and `system_info`
9
+ (the product's purpose). Only the trusted business backend supplies this name; the DSH model,
10
+ local alias, device label and principal identifiers are not sources for it.
11
+
12
+ The matched SDK exposes these additive fields on `login`, `status` and every `connectionsList`
13
+ entry, and the plugin passes them to its host:
14
+
15
+ | Field | Meaning |
16
+ | --- | --- |
17
+ | `subjectDisplay` | `{ name: string }`, or `null` when unavailable |
18
+ | `subjectDisplayStatus` | `provided`, `missing`, `unsupported` or `unavailable` |
19
+ | `subjectDisplaySource` | `verified` after the SDK reads the original Session, `cache` for display-only local data, otherwise `none` |
20
+ | `subjectDisplayCacheStatus` | SDK auxiliary cache outcome: `saved`, `storage_error` or `not_cached` |
21
+ | `displayLabel` | Plugin presentation label: the provided name, otherwise `Authorization name pending sync` |
22
+
23
+ Names are trimmed, nonempty, at most 120 UTF-16 code units, with C0/C1 controls and Unicode line
24
+ separators U+2028/U+2029 and unpaired surrogate code units rejected. Valid Unicode emoji are
25
+ preserved. Only the `name` field enters model assembly. A matching name
26
+ does not establish an identity or a business relationship. Cached display data proves no current
27
+ authorization; a real identity-validation failure still blocks the complete original scope.
28
+ An old SDK without these fields yields `unsupported`; a supported response without a name yields
29
+ `missing`. Neither condition prevents the existing authorized tools from working. A display-cache
30
+ write error stays auxiliary and cannot replace a scope/archive `storage_error` or `recovery_gap`.
31
+
32
+ Custom hosts may omit a user-entered remark/name field and render the returned business name.
33
+ Keep the internal `connectionName` selector and fixed `connectionKey` independently; do not rename,
34
+ merge or recreate credentials because two subjects have the same name or an existing subject is
35
+ renamed. A host may combine a separately trusted product name with this subject name for display.
36
+ When unavailable, localize the generic pending-name message; never guess the name from a product
37
+ description, local alias, principal identifier or list order. Older hosts can keep their current
38
+ connection-management UI unchanged.
39
+
40
+ `setSessionScope`, `getSessionScope` and `restoreSessionScope` add `subjectDisplay`,
41
+ `subjectDisplayStatus` and `subjectDisplaySource` to each visible authorization. The legacy `label`
42
+ is the frozen historical value: newly selected scopes capture the supplied name (or the generic
43
+ pending-name label), while existing stored labels remain byte-for-byte unchanged. Hosts should
44
+ render the new display fields for current names, not reinterpret an old `label` as verified metadata.
45
+
46
+ After the whole scope is validated, current display fields are held separately in coordinator
47
+ memory. They do not enter binding comparisons, persisted v1/v2 scope records or archive identity.
48
+ Single, same-system and cross-system model directories include `subject_display`,
49
+ `subject_display_status` and `subject_display_source`, separately from `system_description` and
50
+ `metadata_status`. The current directory `label` and attributed tool-result label use this display
51
+ projection; the original `authorization_ref` remains the only model target selector.
52
+ Scope revalidation refreshes current names without changing the selected members, original Agent
53
+ Sessions, frozen scope revision, historical labels, visible events or archive context. Normal archive
54
+ ACK writes continue their existing CAS sequence; display refresh itself writes no archive record.
55
+
56
+ ## System descriptions (0.5.0)
57
+
58
+ Core 0.7.0 and SDK 0.5.0 add optional `transport.getSystemInfo({ connectionKey,
59
+ workspace, expectedBinding, signal })`. After the entire selected scope has passed identity
60
+ validation and before first model assembly, the adapter reads one description for each original
61
+ selected member. Requests include the original Hub/Client/workspace/Agent Session binding and
62
+ never user text, tools, or a run identifier. They do not create business runs or load business
63
+ context. Single, same-system multi-authorization and cross-system conversations use the same
64
+ description projection. The existing same-system first-turn run policy remains unchanged.
65
+
66
+ The response schema is `bailing.agent-system-info.v1`. Only `system.name`, `summary`, `domains`,
67
+ and `boundaries`, their metadata status/revision, and availability enter the model directory.
68
+ System references derive from the verified binding, including for a single authorization or
69
+ same-system group. Product purpose describes typical use; it does not grant an action or prove
70
+ that any tool is enabled. `not_loaded` means not yet loaded, not no capabilities. Product purpose,
71
+ authorization limits, tool loading and availability remain separate fields.
72
+
73
+ Descriptions are data, not executable model instructions. Missing configuration, an absent SDK
74
+ method, an unsupported old Core or a temporary metadata failure degrades description fields to
75
+ unknown without changing existing capability search. A 401/403 or changed response binding
76
+ blocks the original whole scope. Scope storage errors retain their existing priority. No description
77
+ is persisted in the scope, Session events or archive outbox; each turn reloads against the original
78
+ complete binding, so mutable labels or another runtime cannot supply stale system identity.
79
+ Cancelled or superseded requests cannot publish late metadata or reactivate business tools.
80
+
81
+ An exact HTTP 404 `route_unavailable` is different from unknown metadata: the selected target
82
+ remains in the directory with `metadata_status: "unknown"`, `availability: "unavailable"` and
83
+ directory-only `availability_reason: "route_unavailable"`. It is not old-version unsupported or
84
+ an authorization grant/revocation. Other ambiguous failures stay unknown. The Core wire's
85
+ `unavailable_reason` still accepts only the documented runtime/direct-switch reasons. A later
86
+ turn retries the same original binding and clears the directory reason after successful lookup.
87
+
88
+ Hosts keep the existing scope/restore/archive interfaces. The scope view now consistently includes
89
+ `clientAppId` and `systemRef` for all selected authorizations; this is additive and does not change
90
+ stored v1/v2 records. No local hard-coded product dictionary or extra body-reporting channel is
91
+ required. A host may keep a separately controlled fallback when metadata is unavailable, but must
92
+ not infer identity from local labels or expand the selected scope.
93
+
94
+ ## Cross-system extension (0.5.0)
95
+
96
+ This extension requires SDK 0.5.0 and Core 0.7.0 with migration 058 applied. Subject display
97
+ additionally requires migration 059. Existing same-system behavior, signatures and v1 records
98
+ remain supported; old selected scopes never expand to include newly available targets.
99
+
100
+ The host selects fixed connection keys from one normalized Hub. Different Client Apps or
101
+ workspaces form different capability sources. Each target must have a distinct original Agent
102
+ Session; two selected routes sharing a Session are rejected. A scope never expands to another Hub,
103
+ registry default, newly added connection, or surviving subset after one member is lost.
104
+
105
+ Cross-system scope uses persisted `bailing.agent-session-scope.v2` with shared `binding: { hubUrl }`
106
+ and `authorizations` containing `{ connectionKey, sessionId, label, workspace, clientAppId }`.
107
+ The host view keeps `bailing.agent-session-scope.v1` and adds `targetMode: "multi_system"` and
108
+ per-authorization `clientAppId`/`systemRef`. Existing `authorizationRef`, key and lifecycle fields
109
+ retain their meaning. System references are opaque identifiers for capability sources; labels
110
+ remain descriptive data, not business identity or object mappings. The subject-display extension
111
+ above separates current business names from historical local labels.
112
+
113
+ Selection/restoration verifies the whole original group and calls SDK
114
+ `getConversationArchiveCapabilities({ members })`. Support requires
115
+ `schema: "bailing.agent-conversation-audit-capabilities.v1"`, `cross_binding_members: true`
116
+ and `member_bindings: "session-client-route.v1"`. Missing support produces
117
+ `CROSS_SYSTEM_SCOPE_UNSUPPORTED` and no cross-system business run. Temporary discovery failures
118
+ remain retryable under the original selection; confirmed authorization replacement remains blocked.
119
+
120
+ Initial prompt assembly registers the target directory, capability search and known-invocation
121
+ recovery tools, without starting any business run. In this mode `search_business_capabilities`
122
+ requires `{ authorization_ref, query, limit? }`. Its bounded query (up to 500 characters) is the
123
+ task projection passed as `userInput` to that target's first `startTurn` in the current turn.
124
+ The original visible user text stays in the independent archive. Subsequent searches reuse that
125
+ target's run. Omission/unknown target fails before dispatch; another selected target is not
126
+ started merely because its authorization is available. Authorization and archive membership checks
127
+ may still inspect the full original set.
128
+
129
+ Tools are grouped only by capability-source binding and complete declaration. Host-issued
130
+ `bh_...` aliases keep same-named tools from different sources separate, even with identical schemas.
131
+ Each alias enumerates only its own `authorization_ref` values and wraps unchanged business
132
+ `arguments`. The server receives the original tool name. Revisions, invocation IDs, argument
133
+ snapshots and original target bindings remain fixed on replay. The SDK receives host-only
134
+ `expectedBinding: { hubUrl, clientAppId, workspace, sessionId }` together with an explicit key
135
+ on status and all business calls. Model arguments cannot replace these fields.
136
+ Explicit search prioritizes that target's results inside the existing twelve-tool budget;
137
+ same-named capabilities on earlier targets cannot permanently crowd it out.
138
+
139
+ Recovery accepts only a known original invocation. When needed in a later live turn, it opens
140
+ only that original target's run with a recovery-specific input and resumes the original invocation;
141
+ it does not call the business action again. It does not restore invocation state after process restart.
142
+ Cancellation and superseding turns cannot register tools or dispatch a new write from a late start.
143
+ Each cross-system turn owns an AbortSignal combined with the host's signal. SDK dispatch checks
144
+ that signal after local IO and before HTTP; an in-flight write with an unknown outcome keeps its
145
+ original invocation identity. Completion summaries and archive upload are separate from business
146
+ dispatch cancellation and may still synchronize after a turn ends.
147
+
148
+ Cross-system archives use `bailing.agent-conversation-outbox.v2`. Context binding is `{ hubUrl }`;
149
+ each member retains `{ connectionKey, hubUrl, clientAppId, workspace, expectedSessionId, label }`.
150
+ Event/ACK schemas, random archive identity, source hashes, ordering, local capture-gap reporting
151
+ and CAS semantics remain unchanged. The SDK uses explicit cross-binding create v2 and each
152
+ member's own credential to confirm; the Core verifies each original app/route/Session and run link.
153
+ The full transcript remains available only through the management audit read boundary.
154
+
155
+ Query minimization, object mapping and step planning are Agent responsibilities. The runtime
156
+ enforces target/tool/invocation authority; it does not establish a deterministic dependency DAG,
157
+ cross-system transaction, automatic rollback or field-level data-transfer policy. Business systems
158
+ continue owning their capabilities, permission checks, approvals and data semantics. See the
159
+ [user and host guide](CROSS_SYSTEM_CONVERSATIONS.md).
160
+
161
+ Status: native Agent Client contract for `dsh-bailinghub@0.5.0`, paired with
162
+ `bailinghub-mcp-server@0.5.0` and BailingHub Core `0.7.0`. The same-system scope and archive
163
+ baseline was introduced in plugin/SDK 0.4.0 and Core 0.6.0 (recommended historical Core 0.6.1).
164
+ This contract is separate from the legacy static `0.1.x` path. Version 0.3.0 supported user-managed
165
+ connections but did not include
166
+ explicit conversation scope, multi-authorization tool selection, or the visible conversation archive.
167
+
168
+ ## Same-System Authorization Selection
169
+
170
+ The retained same-system path supports multiple independently authorized identities for one
171
+ public `Hub + clientAppId + workspace` binding in one DSH conversation. It uses the existing v1
172
+ records and shared capability declarations. The cross-system v2 path above handles different
173
+ sources; neither path changes Core authorization or business approval rules.
174
+
175
+ Version 0.4.0 changed the default: a new conversation with no selected scope, or an explicitly
176
+ empty `connectionKeys: []`, is ordinary chat. It starts no BailingHub run and exposes no BailingHub
177
+ business tools. Browser authorization, the registry's current connection, and the four bootstrap
178
+ fields do not select a conversation's scope. There is no automatic discovery-and-enable fallback.
179
+
180
+ The host explicitly selects fixed SDK connection keys before sending the first user message.
181
+ Exactly one selected authorization keeps the original typed arguments and result shape; two or
182
+ more selected authorizations use the shared envelope below. All selected keys must be authorized
183
+ under the same Hub/client/workspace binding. Unselected connections are never added to the scope.
184
+ The model receives a projected directory containing
185
+ session-local `authorization_ref` values, local display names, and availability, not raw connection
186
+ keys, credentials, Agent Session metadata,
187
+ or permission to supply arbitrary route or identity values. A local name is untrusted display
188
+ data, not proof of a tenant or store. The model must resolve ambiguous user intent before acting;
189
+ available authorization is not a request to act on every listed identity.
190
+ Labels use existing local `connectionName` metadata, not token-derived business names or a new
191
+ Core identity-display field. Generic aliases such as `default` and `default-2` do not establish
192
+ an A/B business mapping; the user must supply clear labels while the business authorization page
193
+ continues to determine the trusted identity.
194
+
195
+ The directory's references resolve to the explicitly selected SDK connection bindings. The first
196
+ `user/message` event freezes the selection, with `agent/inbox/claimed` as a fallback for drivers
197
+ that do not emit that event, before `startTurn`. Changing the selected keys after
198
+ that point requires a new conversation, including switching between ordinary chat and business
199
+ mode. Adding a registry authorization or changing its alias never expands an existing scope.
200
+ Capturing a binding does not freeze credentials or bypass refresh, expiry, revocation, or
201
+ downstream authorization checks.
202
+ Before transport operations, `status({ connectionKey })` must report the captured connection key,
203
+ workspace, and the original authorized Agent Session id captured during selection. If any selected
204
+ authorization is missing, invalid, replaced, or cannot be checked, business access for the entire
205
+ conversation pauses. It neither falls back to a default nor silently shrinks to the remaining
206
+ authorizations. A new conversation can explicitly select the still-valid subset. These inspection
207
+ fields stay host-side. The
208
+ adapter passes `connectionKey` and `workspace` as explicit SDK host metadata instead of
209
+ resolving a mutable alias or default.
210
+
211
+ The full selection is validated before any selected system receives the turn. For each direct
212
+ user turn, the adapter starts one Core run per selected authorization before
213
+ assembling the model request. Instructions, governance, knowledge, memory, and tool results carry
214
+ authorization labels. The user input is sent to each of those runs. Separate run state preserves
215
+ attribution; all injected context still shares the local Agent/model boundary described in
216
+ [Privacy](../PRIVACY.md#same-system-authorization-selection).
217
+
218
+ Business definitions with the same name, description, input schema, and governance are registered
219
+ once.
220
+ Conflicting declarations are not merged for execution. Availability remains specific to each
221
+ authorization, and the conversation's total active business-tool limit remains 12. In a
222
+ multi-authorization session, each shared definition wraps its unchanged business schema:
223
+
224
+ ```json
225
+ {
226
+ "authorization_ref": "<host-issued reference from this session>",
227
+ "arguments": { "<business parameter>": "<value>" }
228
+ }
229
+ ```
230
+
231
+ The selector is host metadata; it is not forwarded as a business argument or new Core HTTP
232
+ field. A single-authorization session keeps the original unwrapped business schema.
233
+ `search_business_capabilities` accepts an optional `authorization_ref` using the same reference
234
+ boundary; omission searches all captured authorizations. Its responses update the selected
235
+ authorizations' capability revisions and definitions before rebuilding
236
+ the shared tool view. It cannot import a different binding into the conversation.
237
+
238
+ Invocation state captures the selected authorization, Core run, capability revision, tool, and
239
+ arguments. A replay with a different selector or payload fails closed. Recovery accepts only an
240
+ invocation known to this conversation and uses the original binding; it accepts no replacement
241
+ authorization selector. Pending approval and unknown dispatch outcomes follow the same
242
+ exact-invocation recovery rules as the baseline. Removing or selecting another default must not
243
+ retarget an existing invocation.
244
+ The local invocation map survives later turns of the same live conversation. It is not persisted
245
+ across process restarts or copied into new conversations, and unknown invocation ids fail closed.
246
+ This increment does not provide durable task recovery across those boundaries.
247
+
248
+ On multi-authorization completion, the adapter freezes one deterministic summary of each run's
249
+ own governed calls and synchronizes that run separately. It does not send the combined visible
250
+ assistant answer, another authorization's results, or hidden reasoning to every run. The combined
251
+ answer is instead eligible for the separate conversation audit below. Single-authorization completion keeps the existing visible-answer
252
+ behavior. Connection add/use/remove remain user-only commands, not model tools.
253
+
254
+ ### Host-owned session scope API
255
+
256
+ The runtime service exposes asynchronous `getSessionScope(sessionId)`,
257
+ `setSessionScope(sessionId, { connectionKeys, expectedRevision? })`, and
258
+ `restoreSessionScope(sessionId)`. They are host APIs, never model tools. Use keys returned by the
259
+ SDK registry, not aliases or model-provided identity values.
260
+
261
+ Native DSH users can inspect `/bailinghub scope`, choose ordinary chat with
262
+ `/bailinghub scope none`, or select fixed keys with `/bailinghub scope set <connection-key>...`
263
+ before the first user message. Obtain keys from `/bailinghub connections list`; the scope command
264
+ does not accept aliases, adopt a registry default, or start a business run. These are user-only
265
+ commands over the same scope API.
266
+
267
+ ```js
268
+ const runtime = ctx.get('bailingHubAgentClient')
269
+ const previous = await runtime.getSessionScope(sessionId)
270
+ const selected = await runtime.setSessionScope(sessionId, {
271
+ connectionKeys: selectedConnectionKeys, // [] explicitly chooses ordinary chat
272
+ expectedRevision: previous.revision,
273
+ })
274
+ // Render selected.mode and selected.authorizations, then enable sending.
275
+ // Do not dispatch the first user message while this operation is pending or failed.
276
+ ```
277
+
278
+ The returned view includes `schema`, `sessionId`, integer-or-null `revision`, `state`
279
+ (`unselected`, `ready`, or `needs_selection`), `locked`, `mode` (`chat`, `business`, or `blocked`),
280
+ and public authorization entries. `get` reads the scope; it does not grant access. A host must await
281
+ a successful `set`, show its returned selection, and only then send the first message. Never
282
+ optimistically send using an earlier scope. Before the first message, a failed replacement
283
+ selection leaves business access blocked; it cannot restore the previous broader selection.
284
+ After the scope is frozen, a change request returns `SESSION_SCOPE_LOCKED` and leaves the original
285
+ frozen scope unchanged. The host must open a new conversation, not treat that rejection as a
286
+ successful selection of the requested keys. Revision values may advance more than once
287
+ during selection; always use the returned value for the next compare-and-swap request.
288
+
289
+ Each `authorizations` entry retains the public host-facing fields
290
+ `{ authorizationRef, connectionKey, label, workspace }`. Version 0.5.0 consistently adds
291
+ `clientAppId`, `systemRef` and the current display fields documented above for single, same-system
292
+ and cross-system scopes. The selection accepts at most 64 unique
293
+ connection keys. The trusted host owns `sessionId`: it must remain stable when reopening the same
294
+ conversation and be unique within that store's namespace. Do not let the model or an untrusted
295
+ client choose another conversation's id, edit scope records, or control the storage namespace.
296
+ The store and its access policy belong to the host. The model sees only the projected reference,
297
+ label, and availability directory; raw connection keys and scope storage are not model APIs.
298
+
299
+ Reopening an existing conversation must call `restoreSessionScope` before sending. A valid
300
+ `locked: true` snapshot restores its original scope after the saved binding and Agent Sessions
301
+ are checked; it remains locked. A stored `locked: false` snapshot is only a draft, even if its
302
+ stored state says `ready`. Loading it into a new runtime returns `needs_selection` / `blocked`
303
+ without probing its previously selected SDK connections. A never-started draft remains
304
+ `locked: false`: the host must explicitly call `setSessionScope` again and show success before
305
+ sending. It must not silently reactivate the saved draft selection.
306
+
307
+ If a locked conversation is reopened offline, its business gate stays closed. A timeout,
308
+ connection failure, or incomplete authorization response is not proof of revocation: retry
309
+ `restoreSessionScope`, `getSessionScope`, or `syncSessionArchive` on the **same runtime and Session**
310
+ after connectivity returns. All original members must pass validation before business access or
311
+ archive upload resumes. Concurrent callers wait for the same in-flight whole-scope validation.
312
+ Retries preserve the original keys, Agent Sessions, binding, member set, and persisted scope revision.
313
+ Confirmed revocation, identity/binding replacement, corrupt storage, or a CAS conflict remains
314
+ blocked; retries never select a default, remaining subset, or replacement Session.
315
+
316
+ An old conversation without a valid snapshot stays blocked; missing or corrupt state must not
317
+ adopt today's registry default or discovered authorizations. Trusted history containing only
318
+ configuration or metadata still permits draft selection. Actual user-message or turn history
319
+ requires the original locked scope; changing that scope requires a new conversation. Embedded
320
+ hosts without the native commands or a scope-selection UI must integrate these APIs;
321
+ `connections use` is not a substitute.
322
+
323
+ ### Scope persistence seam
324
+
325
+ `createAgentClientPlugin({ scopeStore })` accepts an explicit store with `load(sessionId)` and
326
+ `save(sessionId, record, expectedRevision)`. `load` returns a validated record or `null` for absence;
327
+ errors and corrupt records are not absence. `save` compares the stored revision atomically:
328
+ `null` means the record must not exist, the first revision is `1`, and each update increments it.
329
+ Before validating a replacement selection, the coordinator attempts to save a `needs_selection`
330
+ record. If that first write fails, the previous draft can remain on disk; the write is not
331
+ reported as successful. Restart safety also depends on the draft rule above: every loaded
332
+ unlocked snapshot requires explicit confirmation, so a stale saved selection never becomes
333
+ `ready` automatically, even when its replacement marker could not be written.
334
+
335
+ The default `createFileSessionScopeStore()` saves non-secret JSON under
336
+ `$DSH_HOME/plugins/dsh-bailinghub/session-scopes`, using `~/.dsh` when `DSH_HOME` is unset. Session
337
+ ids are hashed into filenames. On POSIX, files use mode `0600` and directories `0700`; writes use a
338
+ cross-process lock, compare-and-swap, and atomic replacement. Lock conflicts and storage failures
339
+ fail closed; the adapter never switches to an in-memory fallback. Hosts may inject another
340
+ durable implementation. `createMemorySessionScopeStore()` is an explicit, non-persistent option
341
+ for tests or hosts that deliberately accept losing scope state on restart.
342
+
343
+ The snapshot contains only the scope schema, DSH session id, revision, state/lock, public binding,
344
+ and selected connection keys, sanitized labels, workspace, and original Agent Session ids. It
345
+ contains no credentials, tokens, prompts, business arguments/results, or invocation state.
346
+ Restoring a scope does **not** restore an invocation, approval, pending completion, or task.
347
+
348
+ ### Independent visible conversation archive
349
+
350
+ The original same-system visible archive uses SDK APIs introduced in `0.4.0` and Core APIs
351
+ introduced in `0.6.0`; the historical recommended Core release is `0.6.1`. For this release,
352
+ use SDK `0.5.0` with Core `0.7.0`, including cross-system member support. Earlier `0.3.0` SDK/plugin
353
+ packages do not provide this contract.
354
+ After a nonempty scope is frozen, the adapter captures claimed user text, every durable `assistant/message` text block, turn start/end, and verified original run links. It ignores
355
+ `assistant/chunk`, hidden reasoning, attachments, raw provider requests, and arbitrary tool payloads.
356
+ The archive is one record for the complete fixed authorization set; per-authorization run summaries
357
+ are unchanged. It is not written into each member's memory.
358
+
359
+ The optional SDK seam is `syncConversationArchive(envelope, { members })`. `envelope` contains a
360
+ durable random UUID `clientArchiveId`, the same `clientConversationId` used by `startTurn`, and
361
+ ordered `events`. Each event has a stable UUID `event_id`, contiguous `sequence` starting at one,
362
+ the original `client_turn_id`, and `kind`: `turn_start`, `user_message`, `assistant_message`,
363
+ `run_link`, or `turn_end`. Messages contain `content`; a run link contains the original `run_id`
364
+ and `member_session_id`; turn end contains `completed`, `failed`, or `cancelled` status. A late
365
+ original run response may attach its link to an already ended turn. It never reactivates its
366
+ business tools, dispatches that cancelled turn's remaining members, or replaces a newer active turn.
367
+ The SDK receives host-only member records `{ connectionKey, workspace, expectedSessionId, label }`,
368
+ checks every original authorization, and owns HTTP batching and credential use. The first frozen
369
+ member is the writer. Neither the old hash alias nor a random archive UUID confers read/write access.
370
+
371
+ `createAgentClientPlugin({ archiveStore })` accepts an independent CAS store with `load`/`save`,
372
+ using the same revision semantics as the scope store but a distinct schema and directory.
373
+ `createFileConversationArchiveStore({ directory? })` defaults to
374
+ `$DSH_HOME/plugins/dsh-bailinghub/conversation-outbox`, with private file permissions, atomic writes,
375
+ and a 32 MiB local record bound. `createMemoryConversationArchiveStore()` is explicitly volatile.
376
+ Outboxes retain the frozen membership, random identity, visible events, event hashes/ids, and
377
+ acknowledged cursor. They contain task text; they are not credential stores. The SDK rejects a
378
+ message above 64,000 characters; the adapter does not silently truncate it or discard pending text.
379
+ Core's conversation/event quotas and the local bound can leave synchronization pending.
380
+
381
+ Host APIs `getSessionArchiveStatus(sessionId)` and `syncSessionArchive(sessionId)` and the user-only
382
+ `/bailinghub archive status|sync` commands expose synchronization separately from business state.
383
+ Persisted pending events can be retried after reopening under the original valid frozen scope,
384
+ without replaying `startTurn`, `invoke`, `resume`, or `completeRun`. An older SDK reports
385
+ `unsupported` without accessing the outbox directory; an unavailable/older Core leaves saved events
386
+ pending or unsupported for retry. Empty scope does not load a transport or create an archive.
387
+
388
+ Capture begins with business turns enabled under this archive contract. Previously unarchived history is not silently
389
+ claimed as complete. Status compares saved visible events against the available DSH `session.events`:
390
+ missing user/assistant text or turn boundaries report `recovery_gap` / `coverage: incomplete`, even
391
+ if the saved prefix is synchronized. Hosts without durable history report `coverage: unverified`.
392
+ This check does not reconstruct missing business run ids. Network failure leaves durable events
393
+ retryable; a local write failure can leave only an in-memory pending event until storage recovers.
394
+ While scope validation or transport availability prevents upload, a known `storage_error` or
395
+ `recovery_gap` remains visible with the existing unsaved-event count and coverage. An optional
396
+ `availability` field describes the additional upload restriction; it does not clear the local error.
397
+ Capability-discovery network failures report `pending`, not a fabricated local storage failure.
398
+ If the process ends before that write succeeds, the host-history check reports the detectable gap;
399
+ there is no atomic transaction between DSH's event log, this outbox, and Core. It does not promise
400
+ recovery of absent host history, hidden content, or durable business task execution.
6
401
 
7
402
  ## Host Configuration
8
403
 
@@ -16,7 +411,8 @@ connectionName
16
411
  ```
17
412
 
18
413
  `hubUrl`, `clientAppId`, and `workspace` identify a public Hub-side application/workspace binding.
19
- `connectionName` selects one local SDK connection instance, but it is not an account, tenant, or
414
+ `connectionName` selects a local SDK connection for connection-management commands; it does not
415
+ select the conversation scope. It is not an account, tenant, or
20
416
  identity claim. The Hub Client App resolves to one stable business authorization endpoint; no
21
417
  business endpoint, authorization endpoint, token, secret, or business credential belongs in this
22
418
  config. The business authorization page owns sign-in, account switching, tenant selection, and
@@ -34,7 +430,7 @@ connectionsUse(connectionNameOrKey)
34
430
  connectionsRemove(connectionNameOrKey)
35
431
 
36
432
  login({ hubUrl, clientAppId, workspace, route, connectionName })
37
- status({ connectionName })
433
+ status({ connectionKey }) // conversation checks; management may use connectionName
38
434
  logout({ connectionName })
39
435
  workspaces({ connectionName })
40
436
  use({ workspace, route, connectionName })
@@ -50,7 +446,7 @@ startTurn({
50
446
 
51
447
  searchCapabilities({ query, limit?, runId? })
52
448
  invoke({ invocationId, capabilityRevision, agentRunId, tool, arguments })
53
- resume(invocationId, {}, { workspace, connectionName, signal? })
449
+ resume(invocationId, {}, { workspace, connectionKey, signal? })
54
450
  completeRun(runId, {
55
451
  assistantMessageId,
56
452
  content,
@@ -61,9 +457,11 @@ completeRun(runId, {
61
457
  })
62
458
  ```
63
459
 
64
- The adapter may pass a second host metadata argument (`workspace`, `connectionName`, and an
65
- `AbortSignal`) to turn/tool methods. The framework-neutral SDK DTO is always the first argument;
66
- an SDK implementation that does not consume host metadata may ignore it.
460
+ The adapter passes host metadata (`workspace`, fixed `connectionKey`, and optional `signal`)
461
+ separately from the business DTO to turn/tool methods, and as the third argument to completion
462
+ and resume. The transport must honor that exact selection; it must never ignore it or substitute
463
+ a mutable alias/default. Connection-management commands may use `connectionName`. The optional
464
+ archive seam and its complete frozen member selection are specified above.
67
465
 
68
466
  ## Browser Identity and Local Reconciliation
69
467
 
@@ -142,7 +540,15 @@ agent/inbox/claimed
142
540
  -> model request
143
541
  ```
144
542
 
145
- The adapter captures only a claimed message whose `source.kind` is `user`. On the authoritative
543
+ The adapter captures only a claimed message whose `source.kind` is `user`. The
544
+ first `user/message` event freezes scope; the inbox claim is a fallback if that event is absent.
545
+ The assembly gate permits business work only for the saved selection.
546
+ When inspecting attached or restored history, only `user/message` with `source.kind: 'user'` or
547
+ `turn/start` proves that a conversation has started. Configuration events, metadata, and
548
+ `session/end-seed` alone do not freeze a draft. `firstLiveSeq` is a seed-history boundary, never
549
+ proof of a started conversation by itself. The adapter separates the previously observed or
550
+ seeded prefix from new events so the current first message is not mistaken for an older turn.
551
+ On the authoritative
146
552
  `system-prompt/assemble` waterfall, it calls `startTurn`, registers the returned definitions
147
553
  through `agent.ctx.tools.register()`, and also adds their schemas to the already-sampled current
148
554
  assembly. Later steps receive the same agent-scoped definitions from the ordinary ToolRuntime
@@ -163,10 +569,11 @@ The adapter listens to durable `session/event` values:
163
569
 
164
570
  ## Tool Invocation and Recovery
165
571
 
166
- Every active Core tool becomes an agent-scoped DSH definition. Its invocation id is a stable
167
- 64-character lowercase SHA-256 digest of the session, run, DSH call id, and Core tool name. It
168
- calls the SDK `invoke` DTO without letting the model choose the run, capability revision, route,
169
- or identity.
572
+ Every active Core tool becomes an agent-scoped DSH definition. Its
573
+ invocation id is a stable 64-character lowercase SHA-256 digest of the session, run, DSH call id,
574
+ and Core tool name. It
575
+ calls the SDK `invoke` DTO without letting the model supply the run, capability revision, route,
576
+ or arbitrary identity. The constrained authorization selector is described above.
170
577
 
171
578
  An SDK error with `disposition === 'accepted_unknown'` starts recovery with the exact invocation
172
579
  id; it never repeats the business-tool `invoke`. Likewise, `awaiting_approval`, `in_progress`, and
@@ -197,41 +604,44 @@ Connection selector, workspace, conversation alias, Core run, active definitions
197
604
  state are isolated per DSH Agent/session. Named connections for different trusted identities own
198
605
  separate SDK credentials and Agent Sessions. Same-binding connections that resolve to the same
199
606
  trusted identity are reconciled to one local survivor after authorization. A workspace switch
200
- preserves the selected connection instance and affects future sessions; it is rejected while any
607
+ preserves the selected connection instance and changes connection-management defaults; it is rejected while any
201
608
  Core run is active/completing or has an unsynchronized completion payload.
202
609
 
203
610
  After a same-alias login resolves to a different trusted identity, the SDK-returned replacement
204
- alias becomes the adapter default for new sessions. The retained old alias and the new alias both
611
+ alias becomes the adapter's registry default. The retained old alias and the new alias both
205
612
  remain visible through `connections list` and user-selectable through `connections use`; existing
206
613
  DSH sessions remain pinned as described below.
207
614
 
208
615
  Multi-connection add/use/remove is exposed only through the `/bailinghub connections` user
209
- command. It is never registered as a model tool. Selecting a connection changes defaults for new
210
- Agent sessions only; existing states keep their captured connection and workspace. Removing a
211
- connection is rejected while any run is active or has an unsynchronized completion. The SDK then
616
+ command. It is never registered as a model tool. Selecting a connection changes registry defaults;
617
+ existing states keep their captured bindings. Defaults affect connection management only;
618
+ the host-owned scope API alone selects the conversation's authorization directory.
619
+ Removing a connection is rejected while any run is active or has an unsynchronized completion. The SDK then
212
620
  revokes only that instance's remote Agent Session before removing its local credentials and
213
621
  registry metadata; a revoke failure preserves both. Repeating add with the same name and public
214
622
  binding selects the existing instance; reusing a name for different public metadata fails.
215
623
  `connectionName` remains a local user selector and never becomes a trusted identity claim.
216
624
 
217
625
  After a successful remove, the adapter reads the registry again. A valid remaining
218
- `currentConnectionKey` replaces all four public defaults for future sessions, using the key itself
626
+ `currentConnectionKey` replaces all four public connection-management defaults, using the key itself
219
627
  when the profile has no alias; no remaining connection sets the adapter to unconfigured. A refresh
220
628
  failure does not change the successful remove result. It makes a removed default unavailable, but
221
629
  does not invalidate an unchanged non-current default. Existing session state is never rewritten.
222
630
 
223
- The four static adapter fields bootstrap SDK construction only. Before the first new Agent session
224
- or user command after process start, the adapter reads `connectionsList()` and adopts the public
631
+ The four static adapter fields bootstrap SDK construction only. For connection management, the
632
+ adapter reads `connectionsList()` and adopts the public
225
633
  metadata matching `currentConnectionKey`. Invalid, missing, or unavailable registry data leaves the
226
634
  bootstrap defaults in place and must not remove or block unrelated host tools. The lookup is not a
227
635
  model tool, and restoring or later selecting a default never mutates an already-created session.
636
+ These registry bootstrap rules do not grant business scope, even to a new
637
+ conversation. A failed scope load or selection never uses the bootstrap fields as a fallback.
228
638
 
229
639
  The completion request is restricted to:
230
640
 
231
641
  ```json
232
642
  {
233
643
  "assistant_message_id": "stable alias",
234
- "content": "visible final text",
644
+ "content": "visible final text for one authorization, or that run's deterministic call summary for a multi-authorization scope",
235
645
  "status": "completed | failed | cancelled",
236
646
  "model": "optional",
237
647
  "runtime": "optional",