@gestaltrun/dsh-im-runtime 0.1.0 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +35 -20
- package/README.md +142 -10
- package/README.zh.md +141 -9
- package/lib/index.js +140 -2
- package/lib/inline-image-DuX6vSEI.js +20 -0
- package/lib/read-receipts.js +413 -0
- package/lib/read-state-CnUZKCuY.js +1868 -0
- package/lib/runtime-Bm6iSEpv.js +10939 -0
- package/lib/tools.js +1 -1
- package/lib/types/account-placement.d.ts +22 -0
- package/lib/types/admission-annotations.d.ts +40 -0
- package/lib/types/agent-coordinator.d.ts +47 -2
- package/lib/types/agent-media.d.ts +46 -0
- package/lib/types/agent-prompt.d.ts +8 -2
- package/lib/types/checkpoint-recovery.d.ts +32 -0
- package/lib/types/checkpoint.d.ts +127 -0
- package/lib/types/consumer-chat-links.d.ts +59 -0
- package/lib/types/delivery-schema.d.ts +126 -2
- package/lib/types/delivery-sqlite.d.ts +206 -6
- package/lib/types/delivery-types.d.ts +97 -1
- package/lib/types/delivery.d.ts +29 -6
- package/lib/types/errors.d.ts +1 -1
- package/lib/types/evaluation-accounts.d.ts +86 -0
- package/lib/types/index.d.ts +34 -2
- package/lib/types/inline-image.d.ts +7 -0
- package/lib/types/input-drain.d.ts +73 -0
- package/lib/types/knowledge-image.d.ts +64 -0
- package/lib/types/media-lifetime.d.ts +22 -0
- package/lib/types/media.d.ts +32 -0
- package/lib/types/message-preview.d.ts +4 -0
- package/lib/types/message-upload.d.ts +55 -0
- package/lib/types/nas-host-lease.d.ts +33 -0
- package/lib/types/placement-slice.d.ts +30 -0
- package/lib/types/policies.d.ts +10 -0
- package/lib/types/preset-model.d.ts +50 -0
- package/lib/types/read-context.d.ts +27 -0
- package/lib/types/read-receipts.d.ts +10 -0
- package/lib/types/read-records.d.ts +30 -0
- package/lib/types/read-state.d.ts +18 -0
- package/lib/types/read-types.d.ts +28 -0
- package/lib/types/restart-recovery.d.ts +38 -0
- package/lib/types/runtime.d.ts +318 -10
- package/lib/types/schema.d.ts +13 -2
- package/lib/types/script-clock.d.ts +14 -0
- package/lib/types/sdk.d.ts +108 -0
- package/lib/types/service-types.d.ts +161 -4
- package/lib/types/session-events.d.ts +9 -9
- package/lib/types/simulation-batch.d.ts +40 -0
- package/lib/types/simulation-controller.d.ts +16 -3
- package/lib/types/simulation-operator.d.ts +19 -0
- package/lib/types/sqlite-connection.d.ts +27 -0
- package/lib/types/time.d.ts +13 -0
- package/lib/types/transport.d.ts +44 -4
- package/lib/types/transports.d.ts +7 -0
- package/lib/types/types.d.ts +22 -5
- package/lib/types/typing.d.ts +33 -0
- package/lib/types/upload-types.d.ts +121 -0
- package/lib/types.js +2 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +67 -41
- package/lib/runtime-C0-S-_CC.js +0 -6758
package/README.i18n.yaml
CHANGED
|
@@ -6,38 +6,53 @@
|
|
|
6
6
|
en: 906a6c4920ae9a55
|
|
7
7
|
zh: 4e879f55b46a7c72
|
|
8
8
|
/gestaltrun-dsh-im-runtime:
|
|
9
|
-
en:
|
|
10
|
-
zh:
|
|
9
|
+
en: 2938694268375795
|
|
10
|
+
zh: b8983714986a32d8
|
|
11
11
|
/gestaltrun-dsh-im-runtime/summary:
|
|
12
|
-
en:
|
|
13
|
-
zh:
|
|
12
|
+
en: 3f4b510e36942846
|
|
13
|
+
zh: d2592a632e8dc11f
|
|
14
|
+
/gestaltrun-dsh-im-runtime/summary/migration:
|
|
15
|
+
en: 3bfac9c8cdd9d253
|
|
16
|
+
zh: 5e391cbb363cc7f1
|
|
14
17
|
/gestaltrun-dsh-im-runtime/table-of-contents:
|
|
15
|
-
en:
|
|
16
|
-
zh:
|
|
18
|
+
en: e5e3b610548a361c
|
|
19
|
+
zh: 46e41f21dbe15503
|
|
17
20
|
/gestaltrun-dsh-im-runtime/use-this-package:
|
|
18
|
-
en:
|
|
19
|
-
zh:
|
|
21
|
+
en: 901aa59059dce6a3
|
|
22
|
+
zh: 8c7de8d14f682093
|
|
20
23
|
/gestaltrun-dsh-im-runtime/use-this-package/when-to-choose-it:
|
|
21
|
-
en:
|
|
22
|
-
zh:
|
|
24
|
+
en: 33d0e3a4b2e920fd
|
|
25
|
+
zh: 3d0aa148f8894edb
|
|
23
26
|
/gestaltrun-dsh-im-runtime/use-this-package/minimal-configuration:
|
|
24
|
-
en:
|
|
25
|
-
zh:
|
|
27
|
+
en: 2253c0a4b74e57f7
|
|
28
|
+
zh: f641688f22bd1aaa
|
|
26
29
|
/gestaltrun-dsh-im-runtime/understand-the-implementation:
|
|
27
|
-
en:
|
|
28
|
-
zh:
|
|
30
|
+
en: 07d6185cbb1dc528
|
|
31
|
+
zh: 8af42d909500174a
|
|
32
|
+
/gestaltrun-dsh-im-runtime/understand-the-implementation/admission-annotations:
|
|
33
|
+
en: 1064ecd2a1cfcfa9
|
|
34
|
+
zh: 258681741aa836a6
|
|
29
35
|
/gestaltrun-dsh-im-runtime/further-exploration:
|
|
30
|
-
en:
|
|
31
|
-
zh:
|
|
36
|
+
en: e4c000d2ad843a06
|
|
37
|
+
zh: 41ed02f24d859b82
|
|
38
|
+
/gestaltrun-dsh-im-runtime/restart-recovery:
|
|
39
|
+
en: 76ee2eb5f68df9bf
|
|
40
|
+
zh: 1285073842567a60
|
|
41
|
+
/gestaltrun-dsh-im-runtime/buyer-read-receipts:
|
|
42
|
+
en: dc645bc5ad641830
|
|
43
|
+
zh: 6a1034d8195867a6
|
|
44
|
+
/gestaltrun-dsh-im-runtime/simulation-batches-and-script-time:
|
|
45
|
+
en: 0cf2ccfcb7ce43f1
|
|
46
|
+
zh: 52eb1a63274864e7
|
|
32
47
|
/gestaltrun-dsh-im-runtime/model-experience:
|
|
33
|
-
en:
|
|
34
|
-
zh:
|
|
48
|
+
en: 69f83d21d9e04a8a
|
|
49
|
+
zh: 118c552b300e5887
|
|
35
50
|
/gestaltrun-dsh-im-runtime/model-experience/kv-cache-effect:
|
|
36
51
|
en: c0457dc6d6e4b5ee
|
|
37
52
|
zh: 0f48dbcd0e2f60ed
|
|
38
53
|
/gestaltrun-dsh-im-runtime/known-limitations-and-deferred-work:
|
|
39
|
-
en:
|
|
40
|
-
zh:
|
|
54
|
+
en: fdc6cf32abf63818
|
|
55
|
+
zh: 3860ff71b803199b
|
|
41
56
|
/gestaltrun-dsh-im-runtime/known-limitations-and-deferred-work/dev-note:
|
|
42
57
|
en: c78baaa0547a5de3
|
|
43
58
|
zh: 70f24c35ac862142
|
package/README.md
CHANGED
|
@@ -7,17 +7,54 @@ kind: "package-reference"
|
|
|
7
7
|
|
|
8
8
|
English | [中文](README.zh.md)
|
|
9
9
|
|
|
10
|
+
The runtime publishes `imConsumerChatLinks`, an independent Host registry for consumer conversation links. `resolve({ accountId, platform }, signal)` validates the current account directory before calling a registered provider; missing providers return `unsupported`. Duplicate platforms reject. Registration belongs to the registering Cordis fiber and returns an idempotent disposer that cancels outstanding reads. Identity lookup failures remain errors; the registry does not persist URLs or change message transport requirements.
|
|
11
|
+
|
|
10
12
|
## Summary
|
|
11
13
|
|
|
12
14
|
Use this package to store safe DingTalk or Wangwang account facts, bind complete conversation routes to Workspaces, and retain message delivery state. Inbound pages, history imports, provider cursors, Session submission evidence, Agent task generations, outbound outcomes, customer lists, and handling pauses have durable query receipts. Credentials remain in the Credentials service. Platform providers own login, event conversion, sends, and receipt lookup; this runtime owns admission into ordinary Agents.
|
|
13
15
|
|
|
16
|
+
`snapshot()` exposes visible formal accounts and routes. It excludes evaluation targets and targets owned by accounts hidden during replacement; targets retained after formal account deletion remain visible with their revisions. Local and unleased standby Hosts use `ensureEvaluationAccount(sellerId, workspaceId)` to obtain a disconnected, credential-free Wangwang identity and one fixed disabled route. Each call aligns the route Workspace; `releaseEvaluationAccountRoute` removes only the observed route generation. The separate `gestaltrun_im_evaluation_accounts` domain uses version 1 and rejects credentials, connected intent, foreign identities and enabled routes. Invalid evaluation data disables evaluation with `IM_EVALUATION_UNAVAILABLE` while formal reception still starts. `isEvaluationAccount` classifies stored evaluation identities, optionally checking the seller; names and id prefixes grant no authority. Formal account operations cannot mutate these records, and a NAS-leased Host rejects evaluation creation. `createFormalSimulationInstance` checks the selected formal route inside serialized creation; Host evaluation keeps `createSimulationInstance`. See the [persistence acknowledgement](persistence-changes/2026-10-26-evaluation-accounts.md).
|
|
17
|
+
|
|
14
18
|
The installation supplies `@deepseek-ai/dsh-agent-preset-registry` as a runtime peer. Its matching development dependency supplies declarations and the test implementation.
|
|
15
19
|
|
|
20
|
+
The `cs-hosting` preset reads the optional `imPresetModelGate` before a batch is submitted. A missing gate fails that admission and does not use the member's default model. A blocked gate leaves the batch unsubmitted until the admission changes.
|
|
21
|
+
|
|
22
|
+
Provider cursor writes and reception state changes share the `checkpointIntervalMs` throttle per account. Reception status combines product intent, admission, model availability, inbox connectivity, and active Agent processing; history synchronization alone does not report `running`, and pending rows alone do not report `draining`. A failed report retains local facts for the next interval and writes a safe account-level failure category to stderr, with the Host logger as an independent fallback if the write throws. Thrown failures and retry outcomes retain safe `diagnostic.code`, `diagnostic.status`, `diagnostic.reason` and bounded nested causes; for example, a refused report exposes `NOT_FOUND` and HTTP 404. `checkpointFailureDiagnostic` exports the same machine-only projection for transports. Repeated failures with the same category and diagnostic are suppressed until either changes or a successful report emits recovery. Message text, cursors, and credentials never enter these diagnostics. A rejected epoch stops reporting until `resumeCheckpoint` starts a new local generation; late results from older generations cannot stop it.
|
|
23
|
+
|
|
24
|
+
Checkpoint `pendingCount` includes unsubmitted input, pending or dispatching sends, and unresolved current Agent tasks. A loaded task is safe only when its inbox is empty and a successful Session flush confirms a final completed turn. Unloaded tasks, unavailable completion evidence and interrupted or failed turns remain nonzero. Session completion and inbox changes schedule fresh reports; teardown joins checkpoint reads before releasing storage. Counts from older checkpoint producers do not establish these completion guarantees.
|
|
25
|
+
|
|
26
|
+
The optional SDK lease method `isDirectConversation` reports whether provider details confirm an opaque incoming cid as a single chat. A missing capability or an unconfirmed result leaves realtime reception to the SaaS feed.
|
|
27
|
+
|
|
28
|
+
An IM transport may expose an optional `sdk` Provider alongside its text capability. SDK status is process-local; `withConnection` admits trusted Host work only for the observed online generation. Read, upload and media consumers own recipient verification, durable preparation and submission authorization. The optional connection capability adds no Session event.
|
|
29
|
+
|
|
30
|
+
`ImTransportSink.receiveRealtime` accepts normalized SDK messages without reading or committing a provider cursor. Within account reception it checks account identity, the current online SDK generation, a unique existing direct peer and an enabled route. Accounts with enabled assigned-actor routes defer to the SaaS feed. New or ambiguous buyers also defer; SDK conversation ids never create runtime scopes. Durable write failures reject. Both channels share conversation ingestion and exact platform message identity. A repeated external message from the same buyer retains its stored sender information, so a later nickname does not publish another upload observation. The existing read synchronizer uses the newly durable message without waiting for SaaS; its records and behavior are unchanged.
|
|
31
|
+
|
|
32
|
+
The runtime observes typing only for subscribed Wangwang single chats. It verifies the local recipient UID, resolves the stored buyer UID through the current online SDK generation, and compares the SDK conversation ID. `typingTimeoutMs` defaults to 5000 and accepts a positive integer through 2147483647. Stop notifications, buyer messages, expiry, SDK loss and the last observer leaving remove the state. Failed conversation resolution leaves typing unconfirmed and logs a payload-free diagnostic without automatic retry. Typing is process-local and never changes message history, Session events, model input or persistent records.
|
|
33
|
+
|
|
34
|
+
The required `ImTransportSink.findInboundContent` looks up exact platform message ids within the listener account, optionally restricted to a conversation. It uses the existing message-row and account indexes to load only matching aggregates. SDK buyer lookup reads `direct_peer_id` from conversation summaries; legacy empty conversations may use their persisted direct recipient. It neither adds indexes nor parses every conversation aggregate. The required `receiveRealtime` and `isNativeSender` methods share the same runtime owner. Lookup failures propagate to the transport, which skips only that SDK message for SaaS reconciliation.
|
|
35
|
+
|
|
36
|
+
Inline PNG/JPEG/WebP/GIF data URLs in text, Markdown or image messages are admitted through the Session attachment provider before Agent submission. The logged input contains normalized durable image references and `[image]` labels, without base64 text. Attachment validation or storage failures stop submission and retain pending delivery. History reads use the same compact label; they do not repeat image bytes. Existing stored rows remain readable without a database migration.
|
|
37
|
+
|
|
38
|
+
`sendManualMessage` accepts one optional media file; `sendMediaMessage` provides the same outbox path to trusted Host callers, with a frozen route binding required for automatic sends. Selected bytes are staged privately in SQLite. Upload metadata is committed before the sole message attempt starts, and each preparation step rechecks cancellation, account availability and SDK generation. Prepared uploads from another process or generation are uploaded again only on an explicit call. Human attribution, known failure and unknown outcomes survive restart; unknown media cannot be retried, and matching authenticated receipts may settle them. `mediaMaxBytes` defaults to 10 MiB per decoded file; the SDK or platform may impose lower limits. File downloads resolve a fresh URL from stored metadata. See the [database migration guide](../../docs/upgrade-guide/v0.2.1-alpha.3/im-media-delivery/guide.md).
|
|
39
|
+
|
|
40
|
+
File upload carries the latest non-empty `senderDisplayName` observed for its verified direct buyer as `ImSdkUpload.receiverNick`. Event or merchant-catalog nicknames already feed this field; a later unnamed message does not erase that evidence. Other buyers’ names and conversation-title ID fallbacks are excluded. Missing nickname is a known `IM_MEDIA_INVALID` preparation failure before platform prepare; images do not require a nickname. No new nickname record is persisted. Each media intent first resolves its verified direct buyer into an SDK conversation identity. Resolution failure is a known preparation failure and never enters the message submission path. Uncertain platform file preparation or commit remains unknown without retry. Before publishing the runtime, a private write-ahead marker restores unfinished file preparation as unknown regardless of SDK or account availability, including previously misclassified failure rows. Attempt and retry admission preserve unresolved preparation; a confirmed preparation refusal clears the marker and remains explicitly retryable. Private preparation retains that identity and its generation; a media receipt must match it rather than the text transport conversation id. Legacy preparation without an SDK identity cannot confirm a new SDK receipt. The SQLite layout remains 3; pending work is prepared under the current owner, and submitted unknown work is never resent.
|
|
41
|
+
|
|
42
|
+
Runtime teardown stops media admission and provider page reception, cancels preparation, and waits for accepted sends (including manual-message publication), receipts and consumer cleanup before closing SQLite and account storage. The same owner stops and joins bound message-upload workers before storage closes, even if their caller remains mounted. Host and account input counters remain available until listener and Agent cleanup and final media writes complete. A completed SDK submission retains its observed result through unload and restart; teardown does not turn success into an unknown outcome.
|
|
43
|
+
|
|
44
|
+
Account records optionally retain `sdkConnectionIntent` (`online` or `kicked`) and `sdkConnectionIntentRevision`. Missing fields mean no automatic restoration permission. Explicit login writes `online` and a fresh intent generation; explicit logout removes both; kickout writes `kicked` under an intent-generation CAS. Unrelated account refreshes preserve that generation. Account placement transfers retain both fields. `stopAccountForPlacement` joins SDK logout and admitted kickout writes before final export without clearing intent; a failed stop blocks handoff. Successful account import or checkpoint recovery arms one process-local activation notification: releasing its reception hold publishes `sdkRecoveryRequested` with the runtime event revision. Replayed startup release does not grant another activation. The hosting owner waits for all input pauses and reception authority before signing in. Older readers strip the optional fields and may discard them when writing; after such a write, explicit login is required again. See the [persistence acknowledgement](persistence-changes/2026-10-22-sdk-connection-intent.md).
|
|
45
|
+
|
|
46
|
+
### Migration
|
|
47
|
+
|
|
48
|
+
Existing accounts, including production sandbox 57, have no saved SDK intent and must explicitly log in once after upgrading. Account connection, prior message history, an enabled hosting route, and successful cloud recovery do not imply consent. That login records online intent for later Host restarts and placement transfers.
|
|
49
|
+
|
|
50
|
+
`ImSdkProvider` implementations must implement `logout(accountId)` and accept optional cancellation in `login(accountId, signal)`. Logout joins accepted SDK work and page cleanup before returning offline. Cancellation revokes login admission; a late result cannot publish online.
|
|
51
|
+
|
|
16
52
|
## Table of Contents
|
|
17
53
|
|
|
18
54
|
- [Use this package](#use-this-package)
|
|
19
55
|
- [Understand the implementation](#understand-the-implementation)
|
|
20
56
|
- [Further Exploration](#further-exploration)
|
|
57
|
+
- [Buyer read receipts](#buyer-read-receipts)
|
|
21
58
|
- [Model Experience](#model-experience)
|
|
22
59
|
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
23
60
|
- [Dev Note](#dev-note)
|
|
@@ -29,13 +66,31 @@ The installation supplies `@deepseek-ai/dsh-agent-preset-registry` as a runtime
|
|
|
29
66
|
|
|
30
67
|
Mount Storage, a KV backend, StorageDomain, Credentials, Session persistence, Workspace, AgentLoop, AgentPresets, a default Agent model, this runtime, and at least one `ImTransport` provider. The runtime remains usable for configuration and history when the Agent services are absent; automatic admission activates only in their injected scope.
|
|
31
68
|
|
|
69
|
+
`renewAccount` verifies credentials for the same merchant, writes a new owned credential record, and installs it only while the observed account revision still matches. Concurrent disconnect or pause wins without changing the active credential. Successful renewal preserves rules, history and intent, stops the old listener before starting its successor, and deletes only the replaced runtime-owned credential record. Replacement records retain their prior connection intent in a Host-only optional field; absent fields keep legacy accounts visible. Hidden accounts remain disconnected, reject ordinary reconnect and late provider callbacks, and expose no active routes or simulation targets. Explicit verified restoration uses the retained id, rules and delivery scope; a different merchant uses a distinct account.
|
|
70
|
+
|
|
32
71
|
### When to choose it
|
|
33
72
|
|
|
34
73
|
Choose this package for the product IM configuration authority shared by Host providers and the IM BFF. Consumers that need wire DTOs or write-only account setup request types should import `@gestaltrun/dsh-im-runtime/types`; Host code imports the root entry for `ImRuntimeService`, `ImRuntimeError`, and `ImTransports`.
|
|
35
74
|
|
|
75
|
+
`pauseInput()` returns process-local pause ownership: new provider pages wait before persistence, new admission drivers retain pending messages, and new Host turns wait before their first step; admitted turns and their child Agents may finish. `drained(signal)` waits for accepted pages, admission drivers and the activities owning those turns without cancelling turns on timeout. Idle Agents wait through maintenance before Inbox claim. Draining excludes its own maintenance reservations; callers should await `drained`, rather than `agent.whenIdle()`, which includes those reservations. Callers must invoke idempotent `resume()` on cancellation and disposal; an upgrade caller that retains a pause after the short deadline releases it on cancellation, prepared expiry or disposal; releasing the final pause resumes receiving and wakes pending conversations. Disposal rejects waiting pages without advancing their provider cursors. The pause is not stored on NAS; a new Host recovers from persisted cursors. The [cloud upgrade entry](../cloud-runtime/README.md#cloud-upgrade) owns deadlines, authorization and final Session flushing.
|
|
76
|
+
|
|
77
|
+
`pauseAccountInput(accountId)` uses the same drain lifecycle for one account and its owned Agent children. Other accounts and personal Workspaces continue. Host-wide upgrade pauses and account handoff pauses reject concurrent ownership with `ImInputPauseError` (`IM_INPUT_PAUSE_CONFLICT`); callers keep their current operation intact and retry after it ends. Page receivers wait until both Host and account admit input, without holding either in-flight count while waiting. Releasing the account pause wakes its pending conversations without changing durable account or route revisions. Account drain observers mount when the counter is created or the Agent scope becomes available, so replies ending before a later pause retire from its count. The pause’s optional `observeReplies(listener)` immediately reports, then observes changes to the Session-deduplicated active-turn and reply-admission reservation count; maintenance activity and page reception are excluded. Activity without a running turn still blocks `drained`, even when the reply count is zero. The returned disposer stops observation, and runtime disposal clears all observers. An older Provider without the method has no count, not a count of zero.
|
|
78
|
+
|
|
79
|
+
`stopAccountForPlacement(accountId)` follows a successful account drain, permanently rejects waiting and later provider pages, confirms listener shutdown, and cancels unsent automatic replies. It retains Agent maintenance pauses, and an unknown shutdown result cannot resume reception. Ordinary route reconfiguration still accepts pages already in flight from an old listener and judges them against the current route; it does not permanently close account reception.
|
|
80
|
+
|
|
81
|
+
`placementStandby: true` holds the target's startup input pause and prevents listeners from starting. Import Session history through cloud-placement while this hold remains, then call `importAccountForPlacement(slice, targetWorkspaceId)` and prepare each resumed Agent through `prepareResume`. The Host calls `releasePlacementStartup()` only after control-plane authorization and preparation; it releases only its own startup hold. The flag defaults to false and does not change existing compositions. The handoff owner supplies Credentials separately, encrypted in transit; a slice contains only credential references, account/route facts and delivery state, never Agent configuration. Wire slices may reference only that account’s canonical IM credential record, never another plugin’s record. Export maps a current runtime-owned renewal key to that target reference without changing the source record; the exporter reads the source account’s actual credential key after checking its revision. `exportAccountForPlacement` selects task generations from the declared source builtin Workspace; current routes to other Workspaces reject instead of transferring their Sessions. Renewal-key ownership is checked against this account’s successful renewal operation receipts, independently of later pause, resume or authorization revisions; a foreign account’s renewal receipt or key is rejected.
|
|
82
|
+
|
|
83
|
+
`beginAccountPlacementImport(accountId, signal)` establishes account-specific standby in an already running Host, including a previously exported account. It drains and stops only that account and releases coordinator-owned Agent handles before Session import. Import and preparation accept this hold without requiring Host startup standby. After fresh lease authorization and preparation, `releaseAccountPlacement(accountId)` releases only the incoming hold; the source handoff owner must also release its earlier account pause. Other account and Host maintenance pauses remain held. Host startup or maintenance pauses reject this account-specific entry with `IM_INPUT_PAUSE_CONFLICT`; fresh standby Hosts use the startup import path. After pausing begins, failure retains the stop or input gate and requires reconciliation; it cannot activate reception. A later call retries a rejected attempt instead of returning its rejection, reusing the retained input hold so that release reopens input.
|
|
84
|
+
|
|
85
|
+
`recoverAccountPlacement(accountId, { checkpoint, sourceFence }, signal)` accepts a cursor-only cloud checkpoint under account import standby. The caller verifies and durably retains the completed outbound handoff, its cloud holder and continuous local automatic-reception fence before acquiring the new lease. `beginAccountPlacementImport` stops the listener before CAS. Under that hold, `validateAccountCheckpointRecovery(accountId, checkpoint, signal)` requires each existing nonnull local stream cursor to equal the checkpoint exactly; divergent opaque cursors reject with `IM_CHECKPOINT_CURSOR_CONFLICT`, without rewinding cursors or clearing deduplication. Recovery repeats this validation after CAS; cursor strings and cross-host timestamps are never ordered. Nonzero checkpoint pending or unknown-send counts reject with `IM_CHECKPOINT_BODIES_UNAVAILABLE`; omission of an existing nonnull local stream rejects with `IM_CHECKPOINT_STREAM_MISSING`. Recovery suppresses stale local pending input, preserves unknown-send outcomes, and creates fresh task generations without changing account or route revisions. Old local Session files remain unchanged; cloud Session bodies are unavailable. Each fresh Session receives a logged notice of the history gap. Stable task, Session and notice identities make retries of the same source handoff and checkpoint epoch idempotent across restart. Recovery keeps reception paused until the handoff owner completes fresh-lease authorization and releases its holds.
|
|
86
|
+
|
|
87
|
+
`prepareAccountPlacement(accountId, prepare, signal)` resumes each current account task using the destination preset and model gate while startup or account import remains paused. Before resuming, it verifies the task belongs to that real account scope and its existing persisted Session header matches the task’s Session id and Workspace directory. Missing Sessions reject without creating replacements. The callback receives that same idle Agent and verified task before reception. After releasing startup, `waitAccountPlacementReady` waits for the current listener reconciliation and Provider readiness; listener failure rejects instead of reporting catch-up success. Original paused or disconnected intent remains unchanged.
|
|
88
|
+
|
|
89
|
+
After an account input drain, `stopAccountForPlacement` stops its exact listener, aborts pages still waiting for admission and cancels pending automatic replies. It preserves unknown send outcomes. An unconfirmed stop throws `IM_PLACEMENT_STOP_UNKNOWN` and retains the stop gate. A pause cannot resume once this stop begins (`IM_PLACEMENT_ALREADY_STOPPED`); failures require handoff reconciliation rather than automatic recovery. Target account import commits delivery then configuration while reception remains stopped. A failure between them can leave partial effects; the handoff journal owns epoch checks, receipts and reconciliation before any activation.
|
|
90
|
+
|
|
36
91
|
### Minimal configuration
|
|
37
92
|
|
|
38
|
-
`admissionBatchSize` limits one model-visible batch and defaults to 1000. Group `everyN` values cannot exceed this limit. `accountSetupTtlMs` limits unconfirmed provider setup material in Host memory and defaults to five minutes. StorageDomain selects the durable backend for account configuration, and the Credentials implementation selects the secret store. Conversation delivery uses a Host-owned sqlite file.
|
|
93
|
+
`admissionBatchSize` limits one model-visible batch and defaults to 1000. Group `everyN` values cannot exceed this limit. `accountSetupTtlMs` limits unconfirmed provider setup material in Host memory and defaults to five minutes. `checkpointIntervalMs` is the minimum gap between hosting checkpoint reports and defaults to 10 seconds. Set it in `cordis.yml`. StorageDomain selects the durable backend for account configuration, and the Credentials implementation selects the secret store. Conversation delivery uses a Host-owned sqlite file.
|
|
39
94
|
|
|
40
95
|
```yaml
|
|
41
96
|
- name: '@deepseek-ai/dsh-storage'
|
|
@@ -53,19 +108,32 @@ Choose this package for the product IM configuration authority shared by Host pr
|
|
|
53
108
|
admissionBatchSize: 1000
|
|
54
109
|
accountSetupTtlMs: 300000
|
|
55
110
|
deliverySqlitePath: /absolute/path/to/storage/gestaltrun_im_delivery.sqlite
|
|
111
|
+
checkpointIntervalMs: 10000
|
|
56
112
|
```
|
|
57
113
|
|
|
58
|
-
|
|
114
|
+
Run the [listener-reconnection fixture](tests/fixtures/listener-reconnect/overlay.yml) with a separate `DSH_HOME` and a profile that includes the IM bundle. Its overlay replaces the complete runtime config and keeps the delivery sqlite file under that home. When copying the overlay elsewhere, resolve `./host.mjs` to the fixture's absolute file URL.
|
|
115
|
+
|
|
116
|
+
`listAccountCandidates` returns installed DingTalk profiles and any legacy admitted Wangwang merchants from the registered transport. `startPlatformLogin`, `platformLoginStatus`, and `cancelPlatformLogin` delegate an optional interactive provider login without persisting attempt state or exposing process output; provider teardown owns active attempts. DingTalk setup must select one listed profile. New Wangwang setup may instead carry use-time merchant, main service-account, HTTPS endpoint, and write-only AccessKey fields; the runtime delegates those fields to the transport and verifies the returned merchant and any verified service-account identity. `discoverConversations` returns provider-known conversation titles with stable platform ids for one configured account. `inspectConversation` resolves one conversation title by its platform id when the page listing does not include it. `previewAccountSetup` asks the transport to verify the write-only input and returns a safe identity, authorization fact, expiration time, and Host-minted setup id without creating an account or writing Credentials. `confirmAccountSetup` fixes that identity, stores the credential record and account with one durable operation receipt, and replays the same setup and operation id without creating another account, including after a Host restart. Optional `confirmAccountSetup.initialPaused: true` atomically persists the new account paused before publication; omission or false preserves the default unpaused behavior. The idempotency fingerprint includes this choice, so replay cannot change it. A provider may return `unchecked` when a signed operation succeeds without enough independent identity evidence; the account is saved but remains unusable for route setup until authorization refresh supplies a verified fact. A second DingTalk profile or Wangwang merchant that is already configured is rejected as `IM_ACCOUNT_EXISTS`. `deleteAccount` compares the observed revision, stops the listener, and removes the account and its routes; conversation history remains. `cancelAccountSetup`, request cancellation, setup expiry, and runtime disposal release unconfirmed in-memory material; JavaScript does not guarantee physical memory erasure. Call `queryAccountOperation` after an uncertain pause, disconnect, reconnect, refresh, or confirmed setup response. A successful `deleteAccount` removes the account aggregate, so that operation has no later query receipt. `inspectAccount` and `refreshAccount` report provider-observed authorization facts without changing the account identity. Connection intent, authorization, and listener state remain separate facts.
|
|
59
117
|
|
|
60
118
|
Every route includes platform, account, conversation kind, an `all` or `specific` target, and a Workspace owner. A specific direct target may retain provider peer identifiers separately from its stable platform conversation id. A specific route wins over an `all` route even when the specific route is disabled. Direct routes reject group settings; group routes require at least one of `mention`, positive `everyN`, or positive `fixedIntervalSeconds`. `createRoute` may record optional `agentPreset` and `controller`. A recorded preset is used as-is; absence takes the runtime default at admit time. Empty values are rejected, and a controller requires a recorded preset. Callers fail when a recorded preset cannot mount; they do not fall back. Product Providers register at `ctx.imRoutePolicies` for one controller identity. Missing Providers refuse admission and automated sends; ordinary routes without a controller skip that registry.
|
|
61
119
|
|
|
62
120
|
`createRoute`, `saveRoute`, `rebindRoute`, and `deleteRoute` store their result with the owning account aggregate. `saveRoute` cannot change the Workspace owner; it may replace `agentPreset` and `controller` when those fields are sent. `rebindRoute` and `deleteRoute` compare both the observed route revision and Workspace. Call `queryRouteOperation` after a transport-level timeout before deciding whether to submit another operation id. Simulation-target save and removal use the same explicit query pattern per Workspace. `registerObservation` keeps history flowing for one connected account without an enabled admit route; those leases live and die with the registering Cordis fiber.
|
|
63
121
|
|
|
64
|
-
`ingestInboundPage` durably stores one complete real or configured simulation conversation page, deduplicated by full scope plus platform message identity. A later duplicate can only advance explicit mention evidence from absent or false to true; a submitted row stays submitted and its earlier Session event is unchanged. JSONL imports enter query history and never enter pending Agent delivery. `reconcileSession` scans `SessionPersistence` after a crash and confirms matching stable sources. Delivery records and Session logs remain separate durable writes and do not claim a cross-log transaction. `listCustomers` pages rebuildable conversation summaries for one account and never mixes real and simulated rows; literal substring search escapes `%` and `_`. `ackCustomerRead` advances a Host-owned reader watermark with max semantics. `setHandlingControl` persists a customer-level automatic-handling pause. Customer-index tables share the delivery sqlite file; physical layout version
|
|
122
|
+
`ingestInboundPage` durably stores one complete real or configured simulation conversation page, deduplicated by full scope plus platform message identity. A later duplicate can only advance explicit mention evidence from absent or false to true; a submitted row stays submitted and its earlier Session event is unchanged. JSONL imports enter query history and never enter pending Agent delivery. `reconcileSession` scans `SessionPersistence` after a crash and confirms matching stable sources. Delivery records and Session logs remain separate durable writes and do not claim a cross-log transaction. `listCustomers` pages rebuildable conversation summaries for one account and never mixes real and simulated rows; literal substring search escapes `%` and `_`. `projectConversations(project)` keeps one consumer value per conversation, derived from its cursor, history and outbox pages whenever the document is parsed or written; later reads return it without a document read, and the caller disposes the projection. `ackCustomerRead` advances a Host-owned reader watermark with max semantics. `setHandlingControl` persists a customer-level automatic-handling pause. Customer-index tables share the delivery sqlite file; physical layout version 3 migrates from versions 1 and 2 explicitly, and a newer on-disk version is refused.
|
|
123
|
+
|
|
124
|
+
Customer-list `messagePreview` values use `[image]` for images and `[unsupported message type: <type>]` for unsupported content. Inline image data inside text or Markdown is replaced with `[image]`, preserving surrounding text. Canonical message content stays unchanged. Existing stored summaries are not scanned or migrated at startup; their previews refresh on the next conversation write.
|
|
125
|
+
|
|
126
|
+
`listCustomerSessions` applies the same grouping, source, search, ordering and keyset cursors as `listCustomers`, but returns only customer keys, kinds, activity times and latest Session ids; it does not query unread counts or handling controls. Full customer pages calculate unread counts only for returned groups and reuse them for the same delivery change sequence and reader. A delivery change invalidates those counts; acknowledging messages invalidates the affected reader. Bulk replacement clears the cache. This reuse relies on the delivery connection being the only writer.
|
|
127
|
+
|
|
128
|
+
The optional conversation `directRecipient` field extends the stored JSON document without changing `IM_DELIVERY_SCHEMA_VERSION`. New readers accept records without it; older readers strip this unknown field and retain their existing send restrictions. Reopening and writing a record in an older build can discard the peer. This field is separate from the declared Session persistence types covered by the [persistence review cookbook](../../docs/cookbook/reviewing-persistence-type-changes.md).
|
|
65
129
|
|
|
66
130
|
Provider polling cursors have explicit account-and-stream ownership. The runtime-owned listener sink accepts one provider page with stable per-conversation operation ids. It commits every conversation group, then commits the provider cursor with all page receipts. If the process stops or a later group fails between those writes, it re-reads the old provider cursor and safely replays the groups through durable deduplication. The listener receives an enabled-route plan that identifies whether mention evidence is required, plus optional observation leases that keep history flowing without admission. This permits a Wangwang merchant page to cover several conversations and lets DingTalk combine all-group and at-me observations without assigning a provider cursor to one conversation.
|
|
67
131
|
|
|
68
|
-
`
|
|
132
|
+
`executionForSession(sessionId)` returns the newest retained task generation for a real or simulated IM Agent Session through `ImRuntimeService`, or absence when no task is recorded. It neither creates an Agent nor reads Session history; products can use it to keep independent audit Sessions outside IM execution.
|
|
133
|
+
|
|
134
|
+
`sendManualMessage` accepts an optional `expectedHandlingEpoch` for text messages. Such a send requires the same durable paused epoch at admission and at the provider’s final external-submission check. Resuming automatic handling invalidates the old confirmation, including while the provider prepares asynchronously. Ordinary manual sends omit this constraint.
|
|
135
|
+
|
|
136
|
+
`registerOutbound` stores an intent before any platform call. `beginOutboundAttempt` grants one attempt; a restart or repeated begin while dispatch is unresolved records `result-unknown`, which callers query or confirm without a blind retry. Route and account generations are frozen for automated intents. `realScopeForSession` resolves a real Session only from its durable task generation and returns the frozen scope, Workspace, direct recipient, safe account identity, account and listener state, last conversation sync, and provider-known conversation label or group size. Unknown labels and sizes stay absent. The GUI uses `sendManualMessage`, `queryManualMessage`, and `confirmManualMessage` with one stable request id. Wangwang text confirmation keeps the stored result without a provider status query. Confirmation on other platforms calls the provider receipt lookup. `retryManualMessage` requires a new request id and a `result-unknown` predecessor from the same Session scope, and persists that relation. Callers may send optional `scopeId` and `accountId`; a scope send requires `accountId`. Manual DSH sends remain available while automatic handling is paused; disconnected, unauthorized, or unavailable-listener accounts reject a new manual intent. A settled human-manual send is also stored as live inbound with sender `human-dsh`, so the next Agent admission can include `[operator from DSH]` without waiting for a provider echo. A later provider echo of that same outbound request replaces the placeholder `dsh-manual:<requestId>` external id instead of opening a second inbound row. Automated intents remain blocked by pause and route-generation checks. A Wangwang direct recipient comes from an observed external sender, then the latest platform peer saved on the conversation. The conversation id is never a recipient. Inbound pages persist `directRecipient` from the platform even for seller messages. When both identities are absent, automatic and manual sends fail as `WANGWANG_DIRECT_RECIPIENT_UNAVAILABLE` without calling the transport. Old seller-only conversations need a new event to acquire the peer; sends do not query the platform to fill it in.
|
|
69
137
|
|
|
70
138
|
Providers pass actor and echo facts to `classifyInboundSender`. A sent automated outbox match yields `ai`, a sent manual match yields `human-dsh`, and explicit provider-native evidence yields `human-native`. An inbound platform message id that already exists on a sent outbox is classified as that echo even when the provider labeled the actor as external. A later inbound from that echo actor, or from the configured account's durable actor id, is `human-native` and still enters Agent admission. Text equality never changes sender attribution. Agent admission ignores `ai` rows. Mention activation uses only the provider's explicit mention metadata stored with the message. Native-client operator rows reach the model as `[operator from native client]`; DSH manual rows use `[operator from DSH]`. External counterpart rows use `[counterpart <name>]` or `[counterpart]` when the display name is absent. The role marker is a bracketed prefix followed by a space, not `[label]: dest`, because GFM would treat the latter as a link definition and Trajectory's Markdown preview would be empty. These labels stay platform-neutral; they do not call the other party a buyer.
|
|
71
139
|
|
|
@@ -79,19 +147,44 @@ Inbound content distinguishes text, Markdown, an image placeholder, and unsuppor
|
|
|
79
147
|
|
|
80
148
|
`beginStopSimulation` first persists `stopping`, which rejects later input and reply delivery, and returns without waiting for a tool caller's current turn. `waitSimulationStopped` is the GUI terminal barrier. The controller cancels only the paired live activities and the selected scope's coordinator work, retains the simulated-user Session and both logs, and stores `stopped` only after quiescence. Calls are deduplicated across GUI and bound `im_sim_stop` orderings. A stopped instance never resumes; loading the runtime does not resume either running Session merely to display it.
|
|
81
149
|
|
|
82
|
-
The `@gestaltrun/dsh-im-runtime/tools` entry registers `im_query_history`, `im_send_message`, and the scoped `im:takeover` system-prompt section on a trusted tested-Agent context. `im_send_message` is the only outbound path for that conversation; the model must not also write the same reply as assistant text. Neither tool accepts account, conversation, simulation instance, route, or Workspace arguments from the model. Eligible simulated-user Agents receive `im_sim_create`; a running pair adds allow-listed member send, managed-human send, and `im_sim_stop`. These tools bind the actual calling Session and current instance rather than accepting those identities from model arguments. Automated real sends use the task's recorded route and the
|
|
150
|
+
The `@gestaltrun/dsh-im-runtime/tools` entry registers `im_query_history`, `im_send_message`, and the scoped `im:takeover` system-prompt section on a trusted tested-Agent context. `im_send_message` is the only outbound path for that conversation; the model must not also write the same reply as assistant text. Neither tool accepts account, conversation, simulation instance, route, or Workspace arguments from the model. Eligible simulated-user Agents receive `im_sim_create`; a running pair adds allow-listed member send, managed-human send, and `im_sim_stop`. A Host driver that holds the buyer owns the simulated-user tool surface: while held, the Session receives none of these tools and automatic reply delivery stays paused; releasing restores them. These tools bind the actual calling Session and current instance rather than accepting those identities from model arguments. Automated real sends use the task's recorded route and the usable Wangwang counterpart; if that generation is stale, the outbox records `route-changed` before any provider call. A Wangwang send with neither an observed counterpart nor a stored platform peer fails before the provider call. The model-visible tool result is `Error: IM reply failed (WANGWANG_DIRECT_RECIPIENT_UNAVAILABLE <request id>): no counterpart sender or platform peer identity is stored for this Wangwang conversation, so nothing was sent. Wait for a new conversation event. Do not use the conversation id as the recipient.`
|
|
83
151
|
|
|
84
152
|
-----
|
|
85
153
|
|
|
86
154
|
<a id="understand-the-implementation"></a>
|
|
87
155
|
## Understand the implementation
|
|
88
156
|
|
|
157
|
+
`pauseAccountInput` closes new input only for the selected account and its actually owned Agents, including children. After a bounded drain wait, the handoff owner can call `cancelAccountActivityForPlacement`; it requires that account pause, cancels only running owned tasks and preserves pending inbox entries. Other Workspaces continue. `stopAccountForPlacement` then confirms listener stop and cancels pending automatic sends. Stopped accounts cannot resume that pause; placement recovery must reconcile the original handoff.
|
|
158
|
+
|
|
159
|
+
`ImDeliverySqlite.exportAccountSlice(accountId)` returns independent real-conversation records, task generations, provider cursor receipts, native actors, reader positions and handling controls; simulations and derived indexes are excluded. Pass transported JSON through `parseImDeliveryAccountSlice`, then use `replaceAccountSlice` only while writers and listeners are stopped and the handoff owner has authorized the destination epoch. Replacement is one SQLite transaction, removes stale real conversations, preserves other accounts and simulations, rebuilds indexes and takes the maximum reader position. Task/route generations remain unchanged; the handoff owner remaps builtin Workspace ids and stores the epoch receipt. A transaction failure retains the prior slice; NAS fencing requires stopped-state reconciliation. These methods do not grant placement or start reception.
|
|
160
|
+
|
|
89
161
|
<details>
|
|
90
162
|
<summary>Implementation internals — click to expand</summary>
|
|
91
163
|
|
|
92
|
-
The `gestaltrun_im_runtime` StorageDomain has one record per account aggregate, Workspace simulation target, and two-Session simulation instance, and stays on the JSON backend. Conversation delivery, provider cursors, Agent task generations, and customer-index tables live in a Host-owned sqlite file at monotonic physical schema version
|
|
164
|
+
The `gestaltrun_im_runtime` StorageDomain has one record per account aggregate, Workspace simulation target, and manual or two-Session simulation instance, and stays on the JSON backend. Conversation delivery, provider cursors, Agent task generations, and customer-index tables live in a Host-owned sqlite file at monotonic physical schema version 3. The runtime reads those records by key and keeps a small parsed-document cache; it does not load every conversation at open. Account conversation lists read the account index and customer summaries, and SDK message correlation keeps identifiers derived from each parsed or written conversation document, so checkpoint, change and SDK paths do not re-read conversation documents. A conversation write still stores one JSON document per scope, cursor, or execution row and updates the customer index in the same sqlite transaction. Provider cursor commits happen only after referenced page receipts exist. No operation claims atomicity across credentials, configuration, delivery, provider cursor, Agent task, simulation instance, or Session records.
|
|
165
|
+
|
|
166
|
+
Upload observations add optional fields to the existing aggregate JSON and require no new tables, columns or indexes. Both pre-media v2 databases and v2 databases with upload observations upgrade to v3 while retaining those fields and read positions. Hosted Wangwang mutations retain immutable upload observations in the same SQLite commit. `bindMessageUpload` transfers them to `<deliverySqlitePath>.uploads.json`, syncing the queue before clearing the transfer journal. Acknowledged rows leave the queue; network, response-validation and queue-storage failures retain recoverable work and never gate platform sends or receives. The worker aborts and awaits its request on disposal. `uploadRetryMs` controls retries (default 5000 ms, range 100–600000); pending counts and safe failure categories appear in Host logs. Keep the queue file together with the delivery database when moving Host state.
|
|
167
|
+
|
|
168
|
+
The queue owns an exclusive transaction in `<deliverySqlitePath>.uploads.json.writer.sqlite` until `close()` aborts and awaits its requests. Only the next writer removes this queue’s UUID-named `.pending` crash files before reading the committed queue. Pre-rename failures close owned descriptors and attempt to remove the temporary; cleanup failures preserve the operation as `cause`, retain every cleanup error, and report safe error codes in Host logs. The valid queue and runtime SQLite journal are never removed. Keep the writer lock file in place while any Host can use it.
|
|
169
|
+
|
|
170
|
+
Permanent rejection markers are stored beside the original entries in the same queue file. Rejected entries survive restart. Default-enabled diagnostics report their count and at most three ids/reasons only when that summary or another pending retry reason changes. They are excluded from automatic submission; other entries continue. Network failures still retry. Invalid or incomplete acknowledgements do not remove an entry.
|
|
171
|
+
|
|
172
|
+
To abandon rejected observations, stop the owning Host and back up the queue file before editing it. Select only ids present in `rejections`; remove those ids from both `entries` and `rejections`, preserve every other field and entry, and save the file atomically. Restart the Host and verify the remaining queue. Keep the writer lock file and delivery SQLite database. Deletion abandons that upload; it does not undo a control or a server fact. Restore the backup if the edited queue fails validation. To resolve a login mismatch, sign in with the matching account instead of deleting the event.
|
|
173
|
+
|
|
174
|
+
Message observations preserve captured platform sender and assignment fields separately. Outbound revisions retain the original sending account after login changes. Existing messages with no captured identity keep all identity fields unknown when revised; later assignments or configured accounts do not fill them. Runtime account aliases and conversation ids never supply platform sender identity. Successful product pause, resume and transfer results append a `conversation-event` observation in the same SQLite transaction as the handling control. Safety pauses without a confirmed transfer do not report a transfer event. Simulation and test traffic keep controls local because the server event format has no traffic label. The upload worker sends events through the existing queue and validates individual V2 acknowledgements before removal. Account placement waits for active uploads and includes unacknowledged file-queue observations in the account slice. Replayed observations do not execute controls. See the [persistence record](persistence-changes/2026-10-24-upload-identity.md) for reader compatibility.
|
|
175
|
+
|
|
176
|
+
Each upload preserves the Wangwang merchant id and provider message id; local scope ids and manual-history placeholders never become upstream ids. The runtime revision capture time supplies `sourceUpdatedAt` and `sourceOrder.kind=time`, and retries preserve it with the version hash. Outbound lifecycle and attribution revisions are separate observations. A real outbound without a provider id stays durable until a late receipt supplies that id; an outcome that never obtains an id cannot be uploaded through the current server API. An exact-echo revision already claimed as `ai` or `human-dsh` keeps `outboundRequestId` and also writes that same value as evidence `requestId`, the field the existing server upload consumer reads. An unclaimed `human-native` observation stays `native_human` and receives no request id. Simulation rows use instance-qualified synthetic ids and `traffic=simulation`; setting `uploadTestRunId` marks real-provider acceptance traffic as `test`. JSONL history imports stay outside realtime upload.
|
|
93
177
|
|
|
94
|
-
`ImTransports` reserves one live provider per platform and releases it with the registering Cordis fiber. The runtime starts its listener only when a connected, unpaused account has authorization `ready` and either an enabled route or a live observation lease; `unchecked`, `required`, or `failed` authorization keeps listening and manual sending stopped. Refresh an `unchecked` account after the provider can return independent identity evidence. A provider `listen` resolves only after that listener is established and verified usable. Its returned `done` promise reports later clean termination or failure. An intentional stop aborts the signal, calls `dispose`, and waits for `done`; changing the enabled-route plan follows that sequence before starting its replacement.
|
|
178
|
+
`ImTransports` reserves one live provider per platform and releases it with the registering Cordis fiber. The runtime starts its listener only when a connected, unpaused account has authorization `ready` and either an enabled route or a live observation lease; `unchecked`, `required`, or `failed` authorization keeps listening and manual sending stopped. Refresh an `unchecked` account after the provider can return independent identity evidence. A provider `listen` resolves only after that listener is established and verified usable. Its returned `done` promise reports later clean termination or failure. An intentional stop aborts the signal, calls `dispose`, and waits for `done`; changing the enabled-route plan follows that sequence before starting its replacement. Recoverable failures enter `reconnecting` and retry with exponential backoff: `listenerReconnectInitialMs` defaults to 1000, `listenerReconnectMaxMs` to 30000, and `listenerReconnectMaxAttempts` to 3. `listenerReconnectStableMs` defaults to 60000 and accepts integer milliseconds from 1 through 2147483647 (the Node timer limit); configure these fields in `cordis.yml`. Delays accept 1–600000 ms, the maximum must cover the initial delay, and the retry limit accepts 0–20 (zero disables retries). Authorization failures and exhausted retries enter `failed`; `lastError` retains a safe platform code. Entering `running` retains the budget. It resets only after that listener runs continuously for `listenerReconnectStableMs` or durably commits a provider page while already running. Startup pages, cursor conflicts, and completions from stopped listener generations do not reset it. Pause, disconnect, provider removal, and runtime disposal cancel retry and stability timers. A fresh accepted `reconnectAccount` operation also restarts a connected account’s failed listener and resets its retry budget; replaying the operation does not repeat that restart. `ctx.imRuntime.subscribe` and the typed `imRuntime/changed` event publish durable changes and later process listener transitions. Snapshot revisions order events within one process generation; durable operation ids and record revisions survive restart.
|
|
179
|
+
|
|
180
|
+
`createManualSimulationInstance({ routeId })` freezes an enabled all-direct route and creates only its tested Session. Its account-local `manual-sim-NNN` number exceeds every previously used number, including stopped and failed instances. The empty conversation is immediately listable as `人工模拟NNN`; the first buyer message keeps the customer key. Tested replies stay in its history and never drive another Agent. The paired `createSimulationInstance` entry keeps its simulated-user Session behavior. Manual records omit both simulated-user fields; paired records retain both. New readers accept both, while old readers reject manual records; downgrade is unsupported. The [persistence acknowledgement](persistence-changes/2026-10-16-manual-simulation.md) records the generated schemas and compatibility checks.
|
|
181
|
+
### Admission annotations
|
|
182
|
+
|
|
183
|
+
`registerAdmissionAnnotator(provider)` registers optional analysis at the shared ordinary/steer assembly point and returns an async disposer that cancels and joins its work. Providers declare an id, version, deadline and ownership predicate. Successful text follows inbound words under `[analysis <id>]`; timeout or failure leaves the original words intact and records its disposition. `inject: false` runs observation-only analysis without adding model text or source metadata; omission enables injection. The runtime freezes results before Session append and does not reanalyze a prepared batch on recovery. Providers must settle on cancellation and discard late results before updating owned state.
|
|
184
|
+
|
|
185
|
+
Customer-service hosting supplies paused conversation history through this same annotation slot. The public `formatInbound(message, timeZone)` renderer supplies sender labels and receipt times for product annotations. Its product-owned provider, validated record limit and deadline are documented in [the hosting package](../cs-hosting/README.md#paused-conversation-history); IM admission uses the generic `[analysis <id>]` instructions.
|
|
186
|
+
|
|
187
|
+
The optional pending-batch and source fields preserve legacy batch identities; see the [persistence acknowledgement](persistence-changes/2026-10-18-admission-annotations.md). Product packages own semantic questions, calibration, caches and enablement; the IM runtime does not classify messages.
|
|
95
188
|
|
|
96
189
|
| Source | Purpose |
|
|
97
190
|
|---|---|
|
|
@@ -107,7 +200,7 @@ The `gestaltrun_im_runtime` StorageDomain has one record per account aggregate,
|
|
|
107
200
|
| `src/route-resolve.ts` | Explicit Agent-preset resolution for a stored route |
|
|
108
201
|
| `src/transports.ts` | Reversible platform-provider registry |
|
|
109
202
|
| `src/agent-coordinator.ts` | Durable trigger evaluation and ordinary Agent create, resume, and steer lifecycle |
|
|
110
|
-
| `src/simulation-controller.ts` | Persistent two-Session creation, isolated reply delivery, recovery, and terminal stop |
|
|
203
|
+
| `src/simulation-controller.ts` | Persistent manual and two-Session creation, isolated reply delivery, recovery, and terminal stop |
|
|
111
204
|
| `src/simulation-tools.ts` | Trusted Session-bound simulation tool registration |
|
|
112
205
|
| `src/tools.ts` | Scope-bound history and outbound Agent tools |
|
|
113
206
|
|
|
@@ -125,13 +218,48 @@ The `gestaltrun_im_runtime` StorageDomain has one record per account aggregate,
|
|
|
125
218
|
|
|
126
219
|
-----
|
|
127
220
|
|
|
128
|
-
<a id="
|
|
221
|
+
<a id="restart-recovery"></a>
|
|
222
|
+
## Restart recovery
|
|
223
|
+
|
|
224
|
+
At startup, the Host reads only conversation indexes and latest task generations registered to current customer-service routes; it never enumerates Session storage. Public Agent resume repairs an unfinished tail with an `interrupted` ending and missing tool results. An ordinary follow-up then asks the same Session to check tools, durable children and background work, continue as needed, and reply to the customer. Product prompt text is persisted through Inbox and `user/message` events, so the Session log reconstructs model input.
|
|
225
|
+
|
|
226
|
+
The follow-up source identifies the interrupted turn's log position. One Host never duplicates it, and an already persisted but unconsumed follow-up wakes with its original message identity. A later Host creates another follow-up only for a new interruption. A Session already resumed by another consumer is checked through its `interrupted` evidence; turns started in this process are not crash tails. Hosting policy, current route generation, customer pause and model admission remain effective before and after resume, with rechecking when admission changes.
|
|
227
|
+
|
|
228
|
+
Read, Agent resume or follow-up persistence failure stops automatic admission for that conversation and reports `IM_RESTART_RECOVERY_FAILED` through default Host error logs. Check Session storage and model configuration, then restart the Host. A public Session write-ownership refusal retains the pending recovery and reports `IM_RESTART_OWNERSHIP_WAIT`; Agent publication or admission changes trigger another check without taking another process's write ownership. A Session not yet created remains eligible for ordinary pending-admission recovery. Durable child Sessions use existing subagent continuation; process-local jobs do not promise survival across restart. Persistent Schedule reminders retain their ordinary follow-up delivery to the original Session.
|
|
229
|
+
|
|
230
|
+
<a id="buyer-read-receipts"></a>
|
|
231
|
+
## Buyer read receipts
|
|
232
|
+
|
|
233
|
+
Mount `@gestaltrun/dsh-im-runtime/read-receipts` alongside the runtime and optional Wangwang SDK. The product bundle includes it. `pendingMessageLimit` defaults to 1000 and bounds SDK observations waiting for matching durable OpenAPI records; excess observations remain unknown and produce a safe diagnostic. This module owns a separate `gestaltrun_im_reads` domain with discriminator `gestaltrun.im-reads.v1`; SQLite and existing account records do not migrate.
|
|
234
|
+
|
|
235
|
+
Inbound read submission requires a live, persisted single-chat message, verified buyer identity and an exact SDK messageId/UUID match. Its attempt marker becomes durable before the SDK call; failure remains unconfirmed and is not retried. Outbound correlation accepts an exact configured-seller SDK echo, a successful SDK text or media receipt, or a read push whose message ID already belongs to a confirmed outbox record. Receipts and direct push associations verify the SDK conversation against the durable buyer identity; text-channel conversation IDs are not SDK IDs. Wangwang text now returns the SDK platform message id, and that id is the read and upload key. A late text receipt settles the outbound with the same request id and reattributes a live history row that already carries that platform message id to the outbound's AI or DSH-human sender. A row with any other id keeps its existing sender evidence. The corrected sender is uploaded when that row was already queued. That upload keeps `outboundRequestId` and writes the same value as evidence `requestId`. Historical SaaS-local text IDs without that platform id remain unknown; equal text or nearby timestamps cannot establish the mapping. Counts establish buyer reading only for a verified singleton receiver; explicit queries match the target buyer's UID and cntaobao domain with status 2. Status 0 establishes unread; status 1, absent fields, foreign receivers and self-reader notifications cannot establish buyer reading. There is at most one association query and one push-disambiguation query per message and SDK generation, with no polling or automatic failed-query retries.
|
|
236
|
+
|
|
237
|
+
Confirmed reads survive restarts and SDK loss. Unread projections require current-generation evidence. A bound Agent receives a required `im/message-read` Session fact and one `im-read` notice context at its next accepted step; idle Agents are not woken. The fact and context metadata reconstruct duplicate suppression after restart. Read records contain only association evidence, not message bodies or credentials. Closing the plugin joins active SDK operations and context consumers before closing its domain.
|
|
238
|
+
|
|
129
239
|
|
|
130
240
|
Simulation injections require a caller-minted `requestId`. Reusing it with the same instance, identity and text returns the persisted inbound receipt; changing identity or text fails. `querySimulationInjection` reads the original receipt, including after stop or restart, without delivering another message. Buyer input uses the frozen speaking-member allow-list. Seller input uses the managed account as `[operator from DSH]`. Both use instance-local delivery; stop closes input admission before draining work and awaiting both Sessions.
|
|
131
241
|
|
|
242
|
+
`injectSimulationOperator({ instanceId, requestId, text })` accepts private operator input only while the tested Agent is running. It persists an ordinary `user` message without buyer/operator labels; it creates no IM history row and makes no transport send. The returned Session/message ids acknowledge durable inbox acceptance, not a completed answer. The same request id and text are idempotent; conflicting text and stopped instances reject. `im_sim_operator_dm` exposes this operation to the interactive simulation side, bound to its current instance. Scheduled human actions call the Host interface directly; buyer-role tools remain separately controlled by their owner.
|
|
243
|
+
|
|
244
|
+
<a id="simulation-batches"></a>
|
|
245
|
+
## Simulation batches and script time
|
|
246
|
+
|
|
247
|
+
`ImSimulationBatch` takes the Host runtime and a validated `maxConcurrency` limit (default 2). `run(jobs, signal)` creates each requested pair, executes its callback, and awaits stop convergence before returning input-ordered results. `stop()` cancels queued creation and active callbacks and waits for cleanup. A cleanup failure cancels remaining work and preserves both execution and cleanup errors. The caller retains ownership of the supplied buyer Sessions; callbacks must settle on cancellation.
|
|
248
|
+
|
|
249
|
+
`ImScriptClock(initialAt)` validates a 13-digit Unix millisecond instant and advances with real elapsed time from that origin. `advance(at)` jumps to a later segment instant and resets the elapsed-time anchor; an earlier segment keeps the current clock running, and an invalid instant throws. Register it with `registerSimulationClock` to timestamp both Sessions’ current-time context, simulated message occurrence and receipt, and outbound creation and settlement. Advance between Host steps. A reply-wait timeout can end a Host step while the tested Agent continues; later messages and tool calls use the advanced script time. `holdSimulationBuyer(instanceId)` pauses automatic buyer delivery and removes simulation operator tools while a Host role drives the pair. `deliverSimulationBuyerReplies(instanceId)` delivers pending AI rows and eligible `human-dsh` rows through native IM input to the held live buyer. Staff delivery requires Host-driven confirmed handoff. The return value counts both kinds of durable buyer input, including inbox acceptance; zero means none were delivered. AI rows are marked submitted; staff rows keep their received stage and use the buyer Session input for delivery deduplication. Unheld or stopped pairs reject. Simulation inputs durably received during automatic-handling pause are suppressed immediately and do not await an AI reply. The Host prepares its state before delivery and waits for buyer completion before its next step; release its disposer after stopping the instance, or queued replies resume normal buyer delivery.
|
|
250
|
+
|
|
251
|
+
<a id="model-experience"></a>
|
|
132
252
|
## Model Experience
|
|
133
253
|
|
|
134
|
-
|
|
254
|
+
`timeZone` is the IANA shop time zone (default `Asia/Shanghai`); invalid zones reject plugin load. Every inbound record, including buyer and operator messages and `im_query_history` results, carries its persisted receipt time as `[YYYY-MM-DD HH:mm]` before the sender label. `occurredAt` remains separate provider evidence.
|
|
255
|
+
|
|
256
|
+
The Host can call `registerSimulationClock(instanceId, source)` for a creating or running instance. `source.now()` returns Unix milliseconds for both Sessions of that instance; `currentTime(sessionId)` otherwise reads system time. The caller advances the source between settled steps. Registration is exclusive, in memory, and removed by its idempotent disposer, instance termination, or Host disposal. Re-register after restart; source errors propagate without substituting system time.
|
|
257
|
+
|
|
258
|
+
`im_send_message` accepts optional ordered attachments. A path attachment is `{path, kind}`, where `kind` is `image` or `file`. A knowledge attachment is `{source:{type:'knowledge-image', referenceId}, kind:'image'}` and cannot also set `path`. At most four knowledge images are accepted, and only from references stored on this Session's knowledge tool results for the current shop. The Host reads those bytes itself; the Agent does not receive a file path and the hosting read-only file policy is unchanged. Paths use the Agent’s filesystem and current directory, including paths exposed by Session attachments. Path images select PNG, JPEG, GIF or BMP MIME from their file headers, regardless of the path suffix. Names without a suffix receive `.png`, `.jpg`, `.gif` or `.bmp` for IM; existing filenames are retained. Unrecognized formats, including WebP, return `IM_MEDIA_INVALID` with guidance to ask a human for a replacement PNG, JPEG, GIF or BMP image before any remaining media or text is sent. File attachments retain their names and exact bytes; knowledge-image references retain their prepared names and MIME. Every source is read within `mediaMaxBytes` and saved as an immutable file attachment before sending begins. Media is sent in array order through `sendMediaMessage`; non-empty `text` follows only after all media succeeds through the same online IM SDK generation. An empty text string sends attachments alone. Wangwang text and media require a real direct conversation and an online IM generation. Text does not fall back to OpenAPI when IM is offline, kicked, or uncertain. Media never falls back to text when IM is unavailable. See the [Wangwang text send guide](../../docs/upgrade-guide/v0.2.1-rc.2/im-sdk-text-send/guide.md). A generation change stops the remaining media and text.
|
|
259
|
+
|
|
260
|
+
Attachment calls return ordered `deliveries` with request ids, kinds, statuses and durable file references; partial success remains explicit. The first failed or unknown result stops subsequent messages. A later attachment-read or text-step exception returns `IM_MEDIA_BATCH_INTERRUPTED`, retains the completed or uncertain `deliveries`, and lists only known unsubmitted items in `unsubmitted`; retry those items in a new call after resolving the failure, and never resend sent or unknown items. Cancellation during a batch uses `IM_MEDIA_BATCH_CANCELLED`. A `tools/result` observer supplements delivery evidence only when the final tool result has lost `deliveries`, including cancellation during post-execute. The supplement enters the Agent’s durable next-step inbox as text only: request ids, kinds, statuses, error codes, file names and byte counts, and unsubmitted items. It contains no image or file blocks. Normal sent, failed and unknown results add no context. Standard inbox and user-message events reconstruct the supplement before it reaches the model. Repeating the same Session tool call reuses its media and text request ids, so an unknown result is not resubmitted. Corrected or intentionally new calls have distinct tool call ids. Standard `tool/call` and `tool/result` events retain arguments, results and immutable file references; no new Session event type is required. The outbox retains the provider-produced source file reference, so an unknown result remains queryable after restart even when the selected path is gone. Text-only Wangwang calls use that IM SDK generation. A result without a platform message id stays unknown and is not resent.
|
|
261
|
+
|
|
262
|
+
Each admitted batch reaches the model as one identified user message assembled by the target Agent preset. Its source carries the complete durable evidence required for crash reconciliation. The Agent-scoped `im:takeover` section sits after the deployment persona prefix and tells the model it speaks as the configured account in this one conversation. Labeled user-role lines are IM records. An unlabeled user message is the operator speaking directly to the Agent and always receives assistant text in this Session. Assistant text is shown to the operator and is never delivered to IM. The model chooses when to finish the turn; the IM prompt does not require an empty reply. Counterpart and operator labels on inbound lines are the only per-message identity. The scope-bound IM tools expose only the admitted conversation's history and outbound path. Simulation tools are present only for a configured simulated-user Workspace or an existing bound instance, and their instance identity comes from the live Agent Session.
|
|
135
263
|
|
|
136
264
|
#### KV Cache effect
|
|
137
265
|
|
|
@@ -148,6 +276,10 @@ One unchanged route generation reuses its Agent Session and prompt cache. An ena
|
|
|
148
276
|
|
|
149
277
|
Customer pauses invalidate automatic input and outbound authorization through the handling epoch. Resume admits new live input; previously queued AI replies cannot send. Pending admission batches retain their original epoch across reload. A forward journal owner projects unresolved pauses with `setAutomaticHandlingPausePending`; `isAutomaticHandlingBlocked` combines that hold with the durable handling control. The hold does not fabricate a persisted pause or epoch and remains until the journal owner reconciles persistence and acknowledgement, including across provider disposal. Pending holds block admission, pre-step, registration and final submission. Automatic provider preparation carries a scope-local epoch cancellation signal. Providers call `submission.authorizeSubmission()` after asynchronous preparation and immediately before external submission; that check also verifies the durable attempt and route generation. Customer pause cancels unsubmitted preparation, including across pause/resume. Already submitted messages retain the actual provider settlement. The runtime caller disposes the authorization after settlement. Human sends and provider history synchronization remain independent of customer AI pause.
|
|
150
278
|
|
|
279
|
+
云端配置通过 `requireNasLease` 等待新代次准入,`deliveryJournalMode` 控制 WAL/DELETE,默认 WAL。`deliveryLockingMode` 可选 `NORMAL`(默认)或 `EXCLUSIVE`;后者要求从首次事务到连接关闭始终只有一个持有者。`deliveryCacheSizeKiB` 为 1–2147483647 的整数,省略时保留 SQLite 默认页缓存;该值是缓存预算,不会在启动时预读整个库。配置在打开文件前校验,并核对 PRAGMA 实际值。投递库遇到另一持有者的锁时,`deliveryBusyTimeoutMs`(默认 2000 毫秒)控制单条语句的等待时长;迁移的清理、每批提交和版本戳在 `deliveryMigrationRetryBudgetMs`(默认 30000 毫秒)预算内先回滚再退避重试,超限抛 `IM_DELIVERY_SQLITE_BUSY`,`user_version` 保持不变,下次打开重新迁移。详见[云端 NAS 存储](../cs-hosting/nas-storage.md)。
|
|
280
|
+
|
|
281
|
+
With `requireNasLease: true`, `placementStandby: true` and an absolute `standbyStorageRoot`, a Host without a NAS lease opens the existing IM schema and delivery SQLite in an instance-private directory. It refuses real account setup, real message admission and real sends. Simulations use disconnected accounts without platform credentials. Placement standby blocks real scopes; explicit Host or account pauses still block simulations. The placement owner joins evaluation and CLI work, acquires the lease, then calls `reloadWithNasLease()` to close private resources and reload formal storage. The private runtime remains readable while lease admission waits. Retained evaluation resources and Session logs are not imported into formal storage.
|
|
282
|
+
|
|
151
283
|
<a id="dev-note"></a>
|
|
152
284
|
### Dev Note
|
|
153
285
|
|