@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/LICENSE +92 -0
  3. package/README.md +101 -2
  4. package/bin/hellolynk-client-setup.mjs +2748 -0
  5. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  6. package/hellolynk-client-pack-manifest.json +619 -0
  7. package/hellolynk-client-pack-pin.json +4 -0
  8. package/package.json +58 -4
  9. package/plugins/hellolynk-codex/.app.json +3 -0
  10. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
  11. package/plugins/hellolynk-codex/.mcp.json +19 -0
  12. package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
  13. package/plugins/hellolynk-codex/package.json +11 -0
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  18. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  19. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  20. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  21. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  22. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  24. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
  37. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  38. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  39. package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
  40. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  41. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  42. package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  43. package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  44. package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  45. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  46. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  47. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  48. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  49. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  50. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  51. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  52. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  53. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
  54. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
  55. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  56. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
  57. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  58. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  59. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
  60. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  61. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  62. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  63. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  64. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  65. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  66. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  67. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  68. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  69. package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  70. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
  71. package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  72. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  73. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  74. package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
  75. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
  76. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
  77. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
  78. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
  79. package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
  80. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
  81. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  82. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  83. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  84. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  85. package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
  86. package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
  87. package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
  88. package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
  89. package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
  90. package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
  91. package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
  92. package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
  93. package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
  94. package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
  95. package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
  96. package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
  97. package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
  98. package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
  99. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  100. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
  101. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
  102. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
  103. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
  104. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
  105. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
  106. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
  107. package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
  108. package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
  109. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  110. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  111. package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
  112. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
  113. package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
  114. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
  115. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
  116. package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
  117. package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
  118. package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
  119. package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
  120. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
  121. package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
  122. package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
  123. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
@@ -0,0 +1,4374 @@
1
+ import { nativeConversationId, nativeConversationQuery, nativeDirectoryResult, queryLocalNativeConversations } from "./hellolynk-native-conversations.mjs";
2
+ import { DIRECT_TEXT_E2EE_MAX_TTL_MS } from "./hellolynk-direct-text-e2ee.mjs";
3
+ import { runModelInbox } from "./hellolynk-model-inbox.mjs";
4
+ import crypto from "node:crypto";
5
+ import fs from "node:fs/promises";
6
+ import http from "node:http";
7
+ import path from "node:path";
8
+ import { withLocalProcessLock, inspectProcessIdentity } from "./hellolynk-local-process-lock.mjs";
9
+ import {
10
+ createHostRequestAuth,
11
+ createHostNonce,
12
+ } from "./hellolynk-host-auth.mjs";
13
+ import {
14
+ createHelloLynkHostCore,
15
+ HELLOLYNK_HOST_PATHS,
16
+ } from "./hellolynk-host.mjs";
17
+ import { createNeutralHostWorkbenchReadModel } from "./hellolynk-neutral-host-workbench-read-model.mjs";
18
+ import {
19
+ deriveInstanceConversationBinding,
20
+ getMessageConversationBinding,
21
+ getOutgoingConversationIntent,
22
+ instanceConversationKey,
23
+ } from "./hellolynk-instance-conversation.mjs";
24
+ import {
25
+ HELLOLYNK_WORKBENCH_CAPABILITIES,
26
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES,
27
+ HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
28
+ HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
29
+ validateWorkbenchSnapshot,
30
+ } from "../workbench/contract.mjs";
31
+ import { createHelloLynkWorkbenchService } from "../workbench/service.mjs";
32
+
33
+ export const HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT = "hellolynk-neutral-host-process-v1";
34
+ export const HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT = "hellolynk-neutral-host-discovery-v1";
35
+ // Raised with every release batch, together with the six client versions. A Host reports it
36
+ // in its health; the higher release stays and the lower one makes room.
37
+ export const HELLOLYNK_HOST_RELEASE_ORDINAL = 13;
38
+
39
+ const INSTALLATION_LOCK_CONTRACT = "hellolynk-neutral-host-installation-lock-v2";
40
+ const INSTALLATION_LOCK_MAX_BYTES = 4 * 1024;
41
+ const INSTALLATION_LOCK_STARTUP_GRACE_MS = 30_000;
42
+ const PROCESS_START_REUSE_TOLERANCE_MS = 2_000;
43
+ // An installer that has published new bindings asks a Host of a lower release
44
+ // to make room; the Host leaves through its normal drain once nothing is in flight.
45
+ const HANDOVER_REQUEST_CONTRACT = "hellolynk-neutral-host-handover-request-v1";
46
+ const HANDOVER_REQUEST_MAX_BYTES = 1024;
47
+ // Whoever asks for the handover also records its release and the time, and the Host that
48
+ // leaves stamps the time again as it frees the slot. For a short hold after that a Host of a
49
+ // lower release does not start, so the newer release gets the slot; then the record no
50
+ // longer blocks, and a client that has not updated yet still gets a Host.
51
+ const EXPECTED_RELEASE_CONTRACT = "hellolynk-neutral-host-expected-release-v1";
52
+ const EXPECTED_RELEASE_MAX_BYTES = 1024;
53
+ const EXPECTED_RELEASE_HOLD_MS = 60_000;
54
+ const HANDOVER_CHECK_EVERY_MS = 1_000;
55
+ // The native confirmation TTL: a bounded wait so an open window is not closed.
56
+ const HANDOVER_NATIVE_CONFIRMATION_QUIET_MS = 120_000;
57
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$/;
58
+ const APPLICATION_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/;
59
+ const HELLOLYNK_ID = /^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/;
60
+ const LOOPBACK_HOST = "127.0.0.1";
61
+ const MAX_REQUEST_BYTES = 64 * 1024;
62
+ const APPLICATION_CHANGE_CAPACITY = 256;
63
+ // AI history browsing returns one page of the newest incoming metadata. Older
64
+ // pages are explicit `before` requests; the pending queue has no such window.
65
+ const AUTHORIZED_INBOX_HISTORY_LIMIT = 10;
66
+ const MANUAL_RETRYABLE_STATUSES = new Set(["recipient_offline", "failed_retryable", "failed_permanent"]);
67
+ const CONTACT_REQUEST_STATES = new Set(["pending", "accepted", "rejected", "cancelled", "blocked", "expired"]);
68
+ const WORKBENCH_CLEAR_RESOLVED_CONTACT_REQUESTS_COMMAND = "contact-request.clear-resolved";
69
+ const WORKBENCH_LEGACY_LIFECYCLE_ROUTES_UNAVAILABLE =
70
+ "neutral_host_workbench_legacy_lifecycle_routes_unavailable";
71
+ const WORKBENCH_LOCAL_ONLY_CAPABILITIES = new Set([
72
+ "contact.authorization.manage", "contact.delete", "contact.instance.delete",
73
+ "messages.mark-read", "messages.delete-read", "contact-request.clear-resolved",
74
+ "messages.mark-all-read",
75
+ "messages.auto-read.manage",
76
+ "contact.instance.rename",
77
+ "contact.rename",
78
+ "messages.delete-sent", "threads.manage-local",
79
+ "contacts.read", "messages.read", "outbox.read", "search.local", "security.read", "threads.read",
80
+ ]);
81
+ const WORKBENCH_LIFECYCLE_DEPENDENT_CAPABILITIES = new Set([
82
+ "block.remove",
83
+ "blocks.read",
84
+ "contact.save",
85
+ "contact.instance.save",
86
+ "relationship.end",
87
+ "relationships.read",
88
+ ]);
89
+ const WORKBENCH_RELATIONSHIP_METADATA_CAPABILITIES = new Set([
90
+ ...WORKBENCH_LIFECYCLE_DEPENDENT_CAPABILITIES,
91
+ "contact.instances.list", "contact-request.respond",
92
+ "contact-request.cancel", "contact-request.send",
93
+ "contact-request.recover-decision", "principal.search",
94
+ ]);
95
+ const WORKBENCH_RELATIONSHIP_METADATA_REFRESH_MS = 15_000;
96
+ const PREKEY_REFILL_RETRY_MIN_MS = 1_000;
97
+ const PREKEY_REFILL_RETRY_MAX_MS = 60_000;
98
+ const RELAY_RETRY_MIN_MS = 1_000;
99
+ const RELAY_RETRY_MAX_MS = 60_000;
100
+ const RELAY_AUTHORITY_DENIAL_CODES = new Set([
101
+ "host_transport_host_revoked", "host_transport_credential_revoked", "host_transport_credential_inactive",
102
+ "device_credential_invalid", "device_credential_revoked", "delegate_revoked", "delegate_inactive",
103
+ ]);
104
+ const MESSAGE_LOCAL_CAPACITY_CODES = new Set(["local_message_store_too_large"]);
105
+ const CONTACT_REQUEST_LOCAL_CAPACITY_CODES = new Set([
106
+ "contact_request_local_capacity_exceeded",
107
+ "contact_request_local_store_too_large",
108
+ ]);
109
+ const KNOWN_CONTACT_REQUEST_SEND_FAILURE_CODES = new Set([
110
+ "contact_request_recipient_prekey_unavailable",
111
+ "contact_request_recipient_endpoint_ambiguous",
112
+ ]);
113
+ const INBOUND_LOCAL_CAPACITY_CODES = new Set([
114
+ ...MESSAGE_LOCAL_CAPACITY_CODES,
115
+ ...CONTACT_REQUEST_LOCAL_CAPACITY_CODES,
116
+ ]);
117
+ const WORKBENCH_COMMAND_CAPABILITIES = Object.freeze({
118
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend]: "messages.request-send",
119
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus]: "messages.request-send",
120
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.markRead]: "messages.mark-read",
121
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.markAllRead]: "messages.mark-all-read",
122
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.setAutoRead]: "messages.auto-read.manage",
123
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteRead]: "messages.delete-read",
124
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent]: "messages.delete-sent",
125
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread]: "threads.manage-local",
126
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread]: "threads.manage-local",
127
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread]: "threads.manage-local",
128
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread]: "threads.manage-local",
129
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads]: "threads.manage-local",
130
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.configureHistory]: "threads.manage-local",
131
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestRetry]: "outbox.request-retry",
132
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply]: "outbox.request-retry",
133
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.saveContact]: "contact.save",
134
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact]: "contact.rename",
135
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContact]: "contact.delete",
136
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances]: "contact.instances.list",
137
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.saveContactInstance]: "contact.instance.save",
138
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance]: "contact.instance.rename",
139
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContactInstance]: "contact.instance.delete",
140
+ [WORKBENCH_CLEAR_RESOLVED_CONTACT_REQUESTS_COMMAND]:
141
+ WORKBENCH_CLEAR_RESOLVED_CONTACT_REQUESTS_COMMAND,
142
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.recoverContactRequestDecision]: "contact-request.recover-decision",
143
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.respondContactRequest]: "contact-request.respond",
144
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.cancelContactRequest]: "contact-request.cancel",
145
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal]: "principal.search",
146
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest]: "contact-request.send",
147
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.setContactAuthorization]: "contact.authorization.manage",
148
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.endRelationship]: "relationship.end",
149
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.removeBlock]: "block.remove",
150
+ [HELLOLYNK_WORKBENCH_COMMAND_TYPES.openAccount]: "security.open-account",
151
+ });
152
+
153
+ /**
154
+ * Starts the process-owning neutral Host. This is intentionally dependency
155
+ * injected: P5a proves lifecycle and ordering, not a production migration of
156
+ * the existing local stores or the Codex/OpenClaw adapters.
157
+ */
158
+ export async function startHelloLynkNeutralHostProcess(options = {}) {
159
+ const tuple = normalizeTuple(options);
160
+ const stableRegion = tuple.region === "international" ? "intl" : tuple.region;
161
+ const runtimeRoot = path.resolve(String(options.runtimeRoot || ""));
162
+ if (!options.runtimeRoot) throw failure("neutral_host_runtime_root_required");
163
+ requireInterface(options.transport, ["registerEndpoint", "openEventStream", "acknowledge"], "neutral_host_transport_invalid");
164
+ requireInterface(options.localStore, ["persistIncomingRecoverable"], "neutral_host_local_store_invalid");
165
+ requireInterface(options.contactStore, ["persistIncomingRecoverable"], "neutral_host_contact_store_invalid");
166
+ requireInterface(options.outbox, ["persistBeforeEncryption", "markUnknown", "markResult", "freezeUnknown"], "neutral_host_outbox_invalid");
167
+ if (typeof options.capabilityResolver !== "function") throw failure("neutral_host_capability_resolver_required");
168
+ if (options.nativeHumanConfirmationBrokerFactory != null
169
+ && typeof options.nativeHumanConfirmationBrokerFactory !== "function") {
170
+ throw failure("neutral_host_native_broker_factory_invalid");
171
+ }
172
+ if (options.changeSink != null && typeof options.changeSink !== "function") throw failure("neutral_host_change_sink_invalid");
173
+ if (options.prepareLocalData != null && typeof options.prepareLocalData !== "function") {
174
+ throw failure("neutral_host_local_data_preparer_invalid");
175
+ }
176
+ if (options.sameHostDeliveryRegistrar != null && typeof options.sameHostDeliveryRegistrar !== "function") {
177
+ throw failure("neutral_host_same_host_delivery_registrar_invalid");
178
+ }
179
+ if (options.humanRelationships != null) {
180
+ requireInterface(
181
+ options.humanRelationships,
182
+ ["search", "send", "list", "decide", "clearResolved"],
183
+ "neutral_host_human_relationships_invalid",
184
+ );
185
+ }
186
+ if (options.accountLauncher != null && typeof options.accountLauncher !== "function") {
187
+ throw failure("neutral_host_account_launcher_invalid");
188
+ }
189
+
190
+ const endpoints = normalizeEndpoints(options.endpoints, tuple);
191
+ const handoverCheckEveryMs = integerInRange(options.handoverCheckEveryMs, HANDOVER_CHECK_EVERY_MS, 25, 60_000);
192
+ // Only a test can report a lower release than this code.
193
+ const releaseOrdinal = integerInRange(options.releaseOrdinal, HELLOLYNK_HOST_RELEASE_ORDINAL, 0, HELLOLYNK_HOST_RELEASE_ORDINAL);
194
+ const directoryPending = new Map();
195
+ const friendTargets = new Map();
196
+ const layout = discoveryLayout(runtimeRoot, tuple.hostInstallationId);
197
+ await ensureSafeRuntimeDirectory(runtimeRoot, layout.directory);
198
+ await assertHelloLynkNeutralHostReleaseCurrent({ runtimeRoot, hostInstallationId: tuple.hostInstallationId, releaseOrdinal });
199
+ const lockHandle = await claimInstallationLock(layout.lockPath, layout.discoveryPath, tuple).catch(async (error) => {
200
+ // A Host of a lower release in the slot is asked to make room, as an installer would.
201
+ if (error?.code === "neutral_host_already_running") {
202
+ await requestHelloLynkNeutralHostHandover({ runtimeRoot, hostInstallationId: tuple.hostInstallationId, releaseOrdinal,
203
+ publishedAt: new Date().toISOString(), waitMs: 0 }).catch(() => {});
204
+ }
205
+ throw error;
206
+ });
207
+ try {
208
+ await options.prepareLocalData?.(Object.freeze({
209
+ ...tuple,
210
+ lockHeld: true,
211
+ }));
212
+ } catch (error) {
213
+ await lockHandle.release().catch(() => {});
214
+ throw error;
215
+ }
216
+ const hostGeneration = crypto.randomBytes(24).toString("hex");
217
+ const hostStartedAt = new Date(nowMs(options.clock)).toISOString();
218
+ const pendingOperations = new Set();
219
+ const changeSink = options.changeSink || (() => undefined);
220
+ const applicationInboxStore = options.inboxStore || options.localStore;
221
+ const applicationInbox = normalizeApplicationInbox(applicationInboxStore);
222
+ const applicationContacts = normalizeApplicationContacts(options.contacts || options.contactDirectory);
223
+ const applicationContactRequests = normalizeApplicationContactRequests(
224
+ options.contactRequests || options.contactRequestStore || options.contactStore,
225
+ );
226
+ const applicationOutbox = normalizeApplicationOutbox(options.applicationOutbox, options.outbox, options.messenger);
227
+ const sharedApplicationMessageStore = typeof applicationInboxStore?.list === "function"
228
+ && applicationInboxStore === (options.applicationOutbox || options.outbox);
229
+ const applicationChanges = [];
230
+ const workbenchServices = new Map();
231
+ const workbenchRelationshipTargets = new Map();
232
+ let applicationChangeCursor = 0;
233
+ let applicationMutationTail = Promise.resolve();
234
+ const clock = options.clock || undefined;
235
+ let lifecycle = "starting";
236
+ let failureCode = null;
237
+ let discovery = null;
238
+ let inboundCapacityWarningActive = false;
239
+ let contactRequestNoticeRead = null;
240
+ let inboundPaused = false;
241
+ let inboundPauseCode = null;
242
+ let inboundResumeTask = null;
243
+ let server = null;
244
+ let stream = null;
245
+ let receiveTask = null;
246
+ let relayConnectTask = null;
247
+ let receiverState = null;
248
+ const relayAbortController = new AbortController();
249
+ const relayProvisionedEndpoints = new Set();
250
+ const relayState = { status: "connecting", failureCount: 0, nextAttemptAtMs: 0, errorCode: null };
251
+ const rejectedInboundTimes = new WeakSet();
252
+ let lastRejectedInbound = null;
253
+ let maintenanceTimer = null;
254
+ let historyMaintenanceAt = -Infinity;
255
+ let historyMaintenanceTask = null;
256
+ const preKeyRefillStates = new Map();
257
+ const preKeyRefillQueue = new Set();
258
+ const preKeyRefillAbortController = new AbortController();
259
+ let preKeyRefillTask = null;
260
+ let stopPromise = null;
261
+ let handoverRequested = false;
262
+ let handoverCheckedAt = -Infinity;
263
+ let nativeConfirmationAt = -Infinity;
264
+ let ownsDiscovery = false;
265
+ let modelCalls = 0;
266
+ let nativeBroker = null;
267
+ let relationshipRefreshInFlight = null;
268
+ // Display projection only. Authority checks always use the original controller.
269
+ // The enclosing Host owns this cache; no account/generation survives a restart.
270
+ let workbenchRelationshipMetadata = null;
271
+ let workbenchRelationshipMetadataState = "loading";
272
+ let workbenchRelationshipMetadataAttemptAt = -Infinity;
273
+ let workbenchRelationshipMetadataEpoch = 0;
274
+ let workbenchRelationshipMetadataRefresh = null;
275
+ let workbenchRelationshipMetadataReadContext = null;
276
+ for (const endpoint of endpoints.values()) {
277
+ const initial = endpoint.e2ee?.preKeyInventoryStatus;
278
+ if (initial?.status !== "retrying") continue;
279
+ const state = preKeyRefillState(endpoint.delegateId);
280
+ state.failureCount = 1;
281
+ state.nextAttemptAtMs = Math.max(nowMs(clock), retryAtTimestamp(initial.retryAt) || 0);
282
+ state.errorCode = safeCode(
283
+ { code: initial.errorCode },
284
+ "e2ee_local_prekey_capacity_exceeded",
285
+ );
286
+ }
287
+ try {
288
+ nativeBroker = options.nativeHumanConfirmationBrokerFactory
289
+ ? await options.nativeHumanConfirmationBrokerFactory({
290
+ hostGeneration,
291
+ onApproved: handleNativeApproval,
292
+ })
293
+ : null;
294
+ if (nativeBroker && ["request", "status", "detachAdapter", "close"].some(
295
+ (method) => typeof nativeBroker[method] !== "function",
296
+ )) throw failure("neutral_host_native_broker_invalid");
297
+ } catch (error) {
298
+ await nativeBroker?.close?.().catch(() => {});
299
+ await lockHandle.release().catch(() => {});
300
+ throw error;
301
+ }
302
+
303
+ const workbenchCapabilities = resolveWorkbenchCapabilities({
304
+ transport: options.transport,
305
+ applicationInbox,
306
+ applicationContacts,
307
+ applicationContactRequests,
308
+ applicationOutbox,
309
+ nativeBroker,
310
+ humanRelationships: options.humanRelationships,
311
+ accountLauncher: options.accountLauncher,
312
+ });
313
+
314
+ const core = createHelloLynkHostCore({
315
+ readModelNotices: async () => {
316
+ // Read only on the first use or after contact-request changes, never scan messages
317
+ // or wait for the cloud to add a short notice to an existing local response.
318
+ contactRequestNoticeRead ??= (async () => {
319
+ const records = await applicationContactRequests?.listForHuman({ states: ["pending"] });
320
+ return records?.filter(record => record.state === "pending" && record.decisionOutcome == null).length ?? 0;
321
+ })().catch(() => null);
322
+ const noticeRead = contactRequestNoticeRead;
323
+ const pendingContactRequests = await noticeRead;
324
+ if (pendingContactRequests == null && contactRequestNoticeRead === noticeRead) contactRequestNoticeRead = null;
325
+ return { pendingContactRequests, localStorageFull: inboundPaused || inboundCapacityWarningActive };
326
+ },
327
+ hostGeneration,
328
+ clock,
329
+ capabilityResolver: async (capabilityId) => {
330
+ const capability = await options.capabilityResolver(capabilityId);
331
+ if (capability?.region !== tuple.region
332
+ || capability?.principalId !== tuple.principalId
333
+ || capability?.hostInstallationId !== tuple.hostInstallationId) {
334
+ throw failure("neutral_host_capability_owner_mismatch");
335
+ }
336
+ return capability;
337
+ },
338
+ actionHandlers: {
339
+ localPeerExchange: (context) => {
340
+ if (typeof options.localPeerExchange !== "function") throw failure("neutral_host_local_peer_unavailable");
341
+ return trackOperation(options.localPeerExchange(context), pendingOperations);
342
+ },
343
+ localIdentityProof: (context) => {
344
+ if (typeof options.localIdentityProof !== "function") throw failure("neutral_host_local_identity_unavailable");
345
+ return trackOperation(options.localIdentityProof(context), pendingOperations);
346
+ },
347
+ detach: (context) => nativeBroker?.detachAdapter({
348
+ capabilityId: context.capabilityId,
349
+ adapterInstanceId: context.adapterInstanceId,
350
+ }),
351
+ appInboxMessages: (context) => applicationInboxMessages(context),
352
+ appInboxUnreadCount: (context) => applicationInboxUnreadCount(context),
353
+ appInboxMarkRead: (context) => trackApplicationMutation(() => applicationInboxMarkRead(context)),
354
+ appInboxDeleteRead: (context) => trackApplicationMutation(() => applicationInboxDeleteRead(context)),
355
+ appContactsList: (context) => applicationContactsList(context),
356
+ appContactsUpsert: (context) => trackApplicationMutation(() => applicationContactsUpsert(context)),
357
+ appContactsRemove: (context) => trackApplicationMutation(() => applicationContactsRemove(context)),
358
+ appContactAutonomousSendAuthorization: (context) => trackApplicationMutation(
359
+ () => applicationContactAutonomousSendAuthorization(context),
360
+ ),
361
+ appAuthorizedContactsList: (context) => applicationAuthorizedContactsList(context),
362
+ appModelInbox: (context) => trackOperation(applicationModelInbox(context), pendingOperations),
363
+ appAuthorizedInboxMessages: (context) => applicationAuthorizedInboxMessages(context),
364
+ appAuthorizedInboxRead: (context) => applicationAuthorizedInboxRead(context),
365
+ appAuthorizedInboxActivate: (context) => applicationAuthorizedInboxActivate(context),
366
+ appAuthorizedInboxPending: (context) => applicationAuthorizedInboxMessages(context, true),
367
+ appAuthorizedInboxPendingQuery: (context) => applicationAuthorizedInboxMessages(context, true, context.request),
368
+ appAuthorizedInboxClaim: (context) => trackOperation(context.request.batchId
369
+ ? applicationAuthorizedInboxBatch(context, "Claim") : applicationAuthorizedInboxClaim(context), pendingOperations),
370
+ appAuthorizedInboxBatchReserve: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Reserve"), pendingOperations),
371
+ appAuthorizedInboxBatchOpen: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Open"), pendingOperations),
372
+ appAuthorizedInboxBatchAttempt: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Attempt"), pendingOperations),
373
+ appAuthorizedInboxBatchConfirm: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Confirm"), pendingOperations),
374
+ appAuthorizedInboxBatchCancel: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Cancel"), pendingOperations),
375
+ appAuthorizedInboxBatchExpire: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Expire"), pendingOperations),
376
+ appAuthorizedInboxBatchClosed: (context) => trackOperation(applicationAuthorizedInboxBatch(context, "Closed"), pendingOperations),
377
+ appAuthorizedInboxFinish: (context) => trackOperation(applicationAuthorizedInboxClaim(context, true), pendingOperations),
378
+ appAuthorizedContactRequestsList: (context) => applicationAuthorizedContactRequestsList(context),
379
+ appAuthorizedContactRequestRead: (context) => applicationAuthorizedContactRequestRead(context),
380
+ // Contact admission is locked by the store; keep slow delivery outside the
381
+ // application mutation queue so human revocations can complete meanwhile.
382
+ appAuthorizedSend: (context) => trackOperation(applicationAuthorizedSend(context), pendingOperations),
383
+ appAuthorizedReply: (context) => trackOperation(applicationAuthorizedReply(context), pendingOperations),
384
+ appContactRequestsList: () => applicationContactRequestsList(),
385
+ appOutboxMessages: (context) => applicationOutboxMessages(context),
386
+ appOutboxRetry: (context) => trackOperation(applicationOutboxRetry(context), pendingOperations),
387
+ appHumanSendRequest: (context) => nativeHumanSendRequest(context),
388
+ appHumanSendRequestStatus: (context) => nativeHumanSendRequestStatus(context),
389
+ appChangesSnapshot: (context) => applicationChangesSnapshot(context),
390
+ appChangesSubscribe: (context) => applicationChangesSubscribe(context),
391
+ appWorkbenchSnapshot: (context) => applicationWorkbenchSnapshot(context),
392
+ appWorkbenchActionGrant: (context) => assertWorkbenchLegacyLifecycleCapabilityAvailable(
393
+ WORKBENCH_COMMAND_CAPABILITIES[context.request?.command],
394
+ ),
395
+ appWorkbenchCommand: (context) => trackOperation(applicationWorkbenchCommand(context), pendingOperations),
396
+ idleShutdown: () => beginDrain("last_attachment_lease_ended"),
397
+ },
398
+ });
399
+
400
+ function trackApplicationMutation(operation) {
401
+ const current = applicationMutationTail.then(() => operation());
402
+ applicationMutationTail = current.catch(() => undefined);
403
+ return trackOperation(current, pendingOperations);
404
+ }
405
+
406
+ async function nativeHumanSendRequest(context) {
407
+ if (!nativeBroker) throw hostApplicationFailure("host_app_native_broker_unavailable");
408
+ const nativeAuthority = context.nativeAuthority;
409
+ if (typeof nativeAuthority?.assertCurrent !== "function" || typeof nativeAuthority?.revalidate !== "function") {
410
+ throw hostApplicationFailure("human_confirmation_authority_invalidated");
411
+ }
412
+ nativeAuthority.assertCurrent();
413
+ const target = await resolveSavedInstance(context.request, context.delegateId);
414
+ await nativeAuthority.revalidate();
415
+ nativeAuthority.assertCurrent();
416
+ nativeConfirmationAt = nowMs(clock);
417
+ const result = await nativeBroker.request({
418
+ capabilityId: context.capabilityId,
419
+ principalId: context.principalId,
420
+ delegateId: context.delegateId,
421
+ hostInstallationId: context.hostInstallationId,
422
+ adapterId: context.adapterId,
423
+ adapterInstanceId: context.adapterInstanceId,
424
+ hostGeneration,
425
+ recipientHelloLynkId: context.request.recipientHelloLynkId,
426
+ recipientDelegateId: target.recipientDelegateId,
427
+ recipientInstanceDisplayName: target.instance.displayName,
428
+ clientMessageId: context.request.clientMessageId,
429
+ text: context.request.text,
430
+ }, { nativeAuthority });
431
+ return result.body;
432
+ }
433
+
434
+ async function nativeHumanSendRequestStatus(context) {
435
+ if (!nativeBroker) throw hostApplicationFailure("host_app_native_broker_unavailable");
436
+ const result = await nativeBroker.status({
437
+ capabilityId: context.capabilityId,
438
+ adapterInstanceId: context.adapterInstanceId,
439
+ actionId: context.request.actionId,
440
+ });
441
+ return result.body;
442
+ }
443
+
444
+ async function handleNativeApproval(approval, approvalContext = {}) {
445
+ if (lifecycle !== "running") throw failure("human_confirmation_broker_closed");
446
+ const nativeAuthority = approvalContext.nativeAuthority;
447
+ if (typeof nativeAuthority?.assertCurrent !== "function" || typeof nativeAuthority?.revalidate !== "function") {
448
+ throw failure("human_confirmation_authority_invalidated");
449
+ }
450
+ nativeAuthority.assertCurrent();
451
+ if (approval.principalId !== tuple.principalId
452
+ || approval.hostInstallationId !== tuple.hostInstallationId
453
+ || approval.hostGeneration !== hostGeneration
454
+ || approval.intentSource !== "human_native_dialog") {
455
+ throw failure("human_confirmation_binding_mismatch");
456
+ }
457
+ const endpoint = endpoints.get(approval.delegateId);
458
+ if (!endpoint?.outbound || typeof endpoint.outbound.send !== "function") {
459
+ throw failure("human_confirmation_approval_handler_missing");
460
+ }
461
+ // Approval binds the stable target. Re-read the saved entry after the
462
+ // native dialog; deleting/replacing an entry cannot retarget this action.
463
+ const target = await resolveSavedInstance(approval, approval.delegateId);
464
+ await nativeAuthority.revalidate();
465
+ // No await between this final lifetime check and outbound admission. Once
466
+ // admitted, keep the actual result; detach must not make it safe to resend.
467
+ nativeAuthority.assertCurrent();
468
+ // The approval arrives outside an HTTP request; a drain must still wait for it.
469
+ const result = await trackOperation(sendOutboundOnce({
470
+ endpoint,
471
+ delegateId: approval.delegateId,
472
+ clientMessageId: approval.clientMessageId,
473
+ input: {
474
+ recipientHelloLynkId: approval.recipientHelloLynkId,
475
+ recipientPrincipalId: target.principalId,
476
+ recipientDelegateId: target.recipientDelegateId,
477
+ region: tuple.region,
478
+ route: savedInstanceRoute(target),
479
+ text: approval.text,
480
+ clientMessageId: approval.clientMessageId,
481
+ intentSource: approval.intentSource,
482
+ signal: approvalContext.signal,
483
+ },
484
+ }), pendingOperations);
485
+ return result;
486
+ }
487
+
488
+ try {
489
+ await options.outbox.freezeUnknown();
490
+ server = http.createServer(handleHttp);
491
+ server.on("clientError", (_error, socket) => socket.destroy());
492
+ await listenLoopback(server, Number(options.port || 0));
493
+ discovery = Object.freeze({
494
+ contractId: HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT,
495
+ processContractId: HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT,
496
+ region: tuple.region,
497
+ principalId: tuple.principalId,
498
+ hostInstallationId: tuple.hostInstallationId,
499
+ tupleDigest: digestTuple(tuple),
500
+ host: LOOPBACK_HOST,
501
+ port: server.address().port,
502
+ pid: process.pid,
503
+ hostGeneration,
504
+ startedAt: new Date(nowMs(clock)).toISOString(),
505
+ });
506
+ await atomicWriteJson(layout.discoveryPath, discovery);
507
+ ownsDiscovery = true;
508
+ lifecycle = "running";
509
+ // Local identity, data and authenticated IPC are ready independently of the
510
+ // relay. Cloud setup/recovery must never hold local discovery hostage.
511
+ void connectRelay();
512
+ const maintenanceEveryMs = integerInRange(options.maintenanceEveryMs, 250, 25, 10_000);
513
+ maintenanceTimer = setInterval(() => void runMaintenance().catch(() => undefined), maintenanceEveryMs);
514
+ maintenanceTimer.unref?.();
515
+ } catch (error) {
516
+ try { await cleanupAfterFailedStart(); }
517
+ catch (cleanupError) {
518
+ // Keep the startup failure authoritative, even if its Error is frozen.
519
+ try { error.cleanupErrorCode = safeCode(cleanupError, "neutral_host_startup_cleanup_failed"); }
520
+ catch {}
521
+ }
522
+ throw error;
523
+ }
524
+
525
+ async function handleHttp(request, response) {
526
+ if (!isLoopbackAddress(request.socket.remoteAddress)) {
527
+ writeJson(response, 403, { ok: false, error: "neutral_host_loopback_required" });
528
+ return;
529
+ }
530
+ let requestUrl;
531
+ try { requestUrl = new URL(request.url || "/", "http://127.0.0.1"); }
532
+ catch {
533
+ writeJson(response, 400, { ok: false, error: "neutral_host_request_target_invalid" });
534
+ return;
535
+ }
536
+ const pathname = requestUrl.pathname;
537
+ if (requestUrl.search !== "" || requestUrl.hash !== "" || request.url !== pathname) {
538
+ writeJson(response, 400, { ok: false, error: "neutral_host_request_target_invalid" });
539
+ return;
540
+ }
541
+ if (lifecycle !== "running") {
542
+ if (request.method === "GET" && pathname === HELLOLYNK_HOST_PATHS.health) {
543
+ writeJson(response, 200, wrapperHealth());
544
+ } else {
545
+ writeJson(response, 503, { ok: false, error: "neutral_host_draining" });
546
+ }
547
+ return;
548
+ }
549
+ const rawBody = await readBody(request, MAX_REQUEST_BYTES).catch((error) => {
550
+ writeJson(response, error?.code === "neutral_host_request_too_large" ? 413 : 400, {
551
+ ok: false,
552
+ error: safeCode(error, "neutral_host_request_invalid"),
553
+ });
554
+ return null;
555
+ });
556
+ if (rawBody == null) return;
557
+ if (lifecycle !== "running") {
558
+ writeJson(response, 503, { ok: false, error: "neutral_host_draining" });
559
+ return;
560
+ }
561
+ if (request.method === "GET" && pathname === HELLOLYNK_HOST_PATHS.health) {
562
+ const result = await trackOperation(core.dispatch({
563
+ method: request.method,
564
+ pathname,
565
+ rawBody,
566
+ headers: request.headers,
567
+ }), pendingOperations);
568
+ writeJson(response, result.status, result.status === 200 ? runningHealth(result.body) : result.body);
569
+ return;
570
+ }
571
+ const result = await trackOperation(core.dispatch({
572
+ method: request.method,
573
+ pathname,
574
+ rawBody,
575
+ headers: request.headers,
576
+ }), pendingOperations);
577
+ writeJson(response, result.status, result.body);
578
+ }
579
+
580
+ async function receiveLoop() {
581
+ let inboundFailed = false;
582
+ try {
583
+ for await (const item of stream.events) {
584
+ if (lifecycle !== "running") return;
585
+ if (!item || item.event === "ready" || item.event === "heartbeat") continue;
586
+ // A relay closure is a transport control, not an encrypted message.
587
+ // Temporary closures reconnect below; explicit revocation still drains.
588
+ if (item.event === "host_transport_closed") {
589
+ throw failure(safeCode({ code: item.data?.reason }, "neutral_host_transport_stream_closed"));
590
+ }
591
+ const operation = trackOperation(handleInbound(item), pendingOperations);
592
+ try {
593
+ await operation;
594
+ lastRejectedInbound = null;
595
+ } catch (error) {
596
+ // These errors were raised before persistence/prekey consumption.
597
+ // Do not ACK or retry this frame; let the relay expire it while later
598
+ // valid frames continue. Disk/commit/ACK failures still drain below.
599
+ if (rejectedInboundTimes.has(error)) {
600
+ lastRejectedInbound = { errorCode: error.code };
601
+ continue;
602
+ }
603
+ if (!isInboundLocalCapacityError(error)) {
604
+ inboundFailed = true;
605
+ throw error;
606
+ }
607
+ await pauseInboundForLocalCapacity(error);
608
+ return;
609
+ }
610
+ }
611
+ if (lifecycle === "running") throw failure("neutral_host_transport_stream_closed");
612
+ } catch (error) {
613
+ if (lifecycle !== "running") return;
614
+ // Preserve fail-closed persistence/ACK and other malformed-inbound errors.
615
+ if (inboundFailed || RELAY_AUTHORITY_DENIAL_CODES.has(error?.code)) throw error;
616
+ await stream?.close?.().catch(() => {});
617
+ stream = null;
618
+ scheduleRelayRetry(error);
619
+ if (error?.code === "host_transport_session_rotation") void connectRelay();
620
+ }
621
+ }
622
+
623
+ function scheduleRelayRetry(error) {
624
+ relayState.status = "retrying";
625
+ relayState.errorCode = safeCode(error, "neutral_host_relay_unavailable");
626
+ relayState.failureCount = Math.min(relayState.failureCount + 1, 31);
627
+ const delay = error?.code === "host_transport_session_rotation" ? 0
628
+ : Math.min(RELAY_RETRY_MAX_MS, RELAY_RETRY_MIN_MS * 2 ** Math.min(relayState.failureCount - 1, 16));
629
+ relayState.nextAttemptAtMs = nowMs(clock) + delay;
630
+ }
631
+
632
+ function connectRelay() {
633
+ if (relayConnectTask || lifecycle !== "running" || inboundPaused || relayState.status === "ready") {
634
+ return relayConnectTask;
635
+ }
636
+ relayState.status = "connecting";
637
+ const signal = relayAbortController.signal;
638
+ const task = (async () => {
639
+ let connected = null;
640
+ try {
641
+ const registrations = await Promise.allSettled([...endpoints.values()].map(async (endpoint) => {
642
+ if (endpoint.e2ee?.preKeyInventoryStatus?.status === "deferred"
643
+ && !relayProvisionedEndpoints.has(endpoint.delegateId)) {
644
+ try {
645
+ await endpoint.e2ee.ensurePreKeyInventory({ signal });
646
+ } catch (error) {
647
+ if (error?.code !== "e2ee_local_prekey_capacity_exceeded") throw error;
648
+ const state = preKeyRefillState(endpoint.delegateId);
649
+ state.failureCount = 1;
650
+ state.nextAttemptAtMs = Math.max(nowMs(clock) + PREKEY_REFILL_RETRY_MIN_MS, retryAtTimestamp(error.retryAt) || 0);
651
+ state.errorCode = error.code;
652
+ }
653
+ relayProvisionedEndpoints.add(endpoint.delegateId);
654
+ }
655
+ if (signal.aborted || lifecycle !== "running") return;
656
+ await options.transport.registerEndpoint({
657
+ delegateId: endpoint.delegateId, identityKeyId: endpoint.identityKeyId,
658
+ runtimeHeaders: endpoint.runtimeHeaders, signal,
659
+ });
660
+ }));
661
+ if (signal.aborted || lifecycle !== "running") return;
662
+ const failed = registrations.find((result) => result.status === "rejected"
663
+ && RELAY_AUTHORITY_DENIAL_CODES.has(result.reason?.code))
664
+ || registrations.find((result) => result.status === "rejected");
665
+ if (failed) throw failed.reason;
666
+ connected = await openRelayStream(signal);
667
+ if (!connected?.events || typeof connected.events[Symbol.asyncIterator] !== "function") {
668
+ throw failure("neutral_host_transport_stream_invalid");
669
+ }
670
+ if (signal.aborted || lifecycle !== "running") {
671
+ await connected.close?.().catch(() => {});
672
+ return;
673
+ }
674
+ stream = connected;
675
+ Object.assign(relayState, { status: "ready", failureCount: 0, nextAttemptAtMs: 0, errorCode: null });
676
+ startReceiveTask();
677
+ } catch (error) {
678
+ await connected?.close?.().catch(() => {});
679
+ if (signal.aborted || lifecycle !== "running") return;
680
+ // An explicit revocation of this Host still fences local access. A
681
+ // timeout/expired cloud session alone is not a local revocation.
682
+ if (RELAY_AUTHORITY_DENIAL_CODES.has(error?.code)) {
683
+ // Failure is exposed as host.status/failureCode to the release entry.
684
+ void beginDrain(error.code).catch(() => {});
685
+ return;
686
+ }
687
+ scheduleRelayRetry(error);
688
+ }
689
+ })();
690
+ // Relay setup is cancellable connection work, not a message persist/ACK
691
+ // unit. Its late completion is fenced above and must not hold the local
692
+ // shutdown lock after the caller has aborted it.
693
+ relayConnectTask = task;
694
+ relayConnectTask.finally(() => { relayConnectTask = null; });
695
+ return relayConnectTask;
696
+ }
697
+
698
+ async function openRelayStream(signal) {
699
+ signal.throwIfAborted();
700
+ // The installation lock is held for this Host's entire lifetime. Persist
701
+ // before sending, so a crash cannot reuse an accepted attempt number.
702
+ // Drain tracks this local write before releasing ownership to a successor.
703
+ const receiverAttempt = await trackOperation((async () => {
704
+ if (!receiverState) {
705
+ await assertSafePath(layout.receiverStatePath, { mustExist: false, directory: false });
706
+ let saved;
707
+ try { saved = JSON.parse(await fs.readFile(layout.receiverStatePath, "utf8")); }
708
+ catch (error) { if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error; }
709
+ receiverState = saved;
710
+ }
711
+ // This is disposable receive bookkeeping, not an account credential.
712
+ // A fresh ID cannot reuse an old attempt or displace another receiver.
713
+ if (!receiverState || typeof receiverState !== "object" || Array.isArray(receiverState)
714
+ || receiverState.contract !== "hellolynk-host-receiver-state-v1" || receiverState.tupleDigest !== digestTuple(tuple)
715
+ || typeof receiverState.receiverId !== "string" || !SAFE_ID.test(receiverState.receiverId)
716
+ || !Number.isSafeInteger(receiverState.sequence) || receiverState.sequence < 0 || receiverState.sequence >= Number.MAX_SAFE_INTEGER) {
717
+ receiverState = { contract: "hellolynk-host-receiver-state-v1", tupleDigest: digestTuple(tuple), receiverId: crypto.randomUUID(), sequence: 0 };
718
+ }
719
+ const next = { ...receiverState, sequence: receiverState.sequence + 1 };
720
+ await atomicWriteJson(layout.receiverStatePath, next);
721
+ receiverState = next;
722
+ return { receiverId: next.receiverId, sequence: next.sequence };
723
+ })(), pendingOperations);
724
+ signal.throwIfAborted();
725
+ if (lifecycle !== "running") throw failure("neutral_host_draining");
726
+ return options.transport.openEventStream({ signal, receiverAttempt });
727
+ }
728
+
729
+ async function handleInbound(item) {
730
+ const event = assertPlainRecord(item.data, "neutral_host_inbound_event_invalid");
731
+ if (event.recipientPrincipalId !== tuple.principalId || event.recipientHostId !== tuple.hostInstallationId) {
732
+ throw failure("neutral_host_inbound_owner_mismatch");
733
+ }
734
+ const endpoint = endpoints.get(safeId(event.recipientDelegateId, "neutral_host_inbound_delegate_invalid"));
735
+ if (!endpoint) throw failure("neutral_host_inbound_endpoint_unknown");
736
+ const isContactRequest = item.event === "contact_request";
737
+ if (!isContactRequest && item.event !== "message") throw failure("neutral_host_inbound_kind_invalid");
738
+ const decryptor = isContactRequest ? endpoint.contactE2ee : endpoint.e2ee;
739
+ let opened;
740
+ try {
741
+ opened = await decryptor.openIncoming({
742
+ event,
743
+ recipientPrincipalId: tuple.principalId,
744
+ recipientDelegateId: endpoint.delegateId,
745
+ });
746
+ } catch (error) {
747
+ if (["e2ee_envelope_expired", "e2ee_created_at_in_future",
748
+ "contact_request_envelope_expired", "contact_request_created_at_in_future"].includes(error?.code)) {
749
+ rejectedInboundTimes.add(error);
750
+ }
751
+ throw error;
752
+ }
753
+ if (!opened || typeof opened.text !== "string" || typeof opened.commit !== "function") {
754
+ throw failure("neutral_host_inbound_decrypt_invalid");
755
+ }
756
+ if (!isContactRequest && opened.rejectedNativeContent) return consumeNativeFrame(event, opened, endpoint, item.localDelivery === true);
757
+ if (!isContactRequest && opened.nativeDirectory) return handleNativeDirectory(event, opened, endpoint, item.localDelivery === true);
758
+ if (opened.targetConversationId && (opened.replyToClientMessageId
759
+ || (opened.senderPrincipalId !== tuple.principalId && !isFriendTargetFrame(opened, item.localDelivery))))
760
+ return consumeNativeFrame(event, opened, endpoint, item.localDelivery === true);
761
+ const friendTarget = opened.targetConversationId && opened.senderPrincipalId !== tuple.principalId
762
+ ? admitFriendTarget(opened, endpoint, event.clientMessageId) : null;
763
+ const store = isContactRequest ? options.contactStore : options.localStore;
764
+ // The persistence coordinator orders business-record durability before
765
+ // prekey commit and journals recovery. It is not a cross-store transaction.
766
+ const persisted = await store.persistIncomingRecoverable({
767
+ event,
768
+ text: opened.text,
769
+ envelopeDigest: opened.envelopeDigest || event.envelopeDigest,
770
+ ...(isContactRequest ? { introDigestSalt: opened.introDigestSalt } : {
771
+ senderPrincipalId: opened.senderPrincipalId,
772
+ senderDelegateId: opened.senderDelegateId,
773
+ authorizationScope: opened.authorizationScope,
774
+ intentSource: opened.intentSource,
775
+ ...(opened.replyToClientMessageId ? { replyToClientMessageId: opened.replyToClientMessageId } : {}),
776
+ ...(opened.targetConversationId ? { targetConversationId: opened.targetConversationId } : {}),
777
+ ...(friendTarget ? { friendTarget } : {}),
778
+ conversationBinding: opened.e2eeVersion === 2 ? deriveInstanceConversationBinding(opened, {
779
+ direction: "incoming", region: tuple.region, principalId: tuple.principalId,
780
+ }) : null,
781
+ }),
782
+ commitPrekey: opened.commit,
783
+ humanOnly: isContactRequest,
784
+ modelVisibility: "none",
785
+ });
786
+ // The record and prekey commit are durable now. Refresh the application
787
+ // before network ACK can delay visibility.
788
+ await publishApplicationChange(
789
+ isContactRequest ? "contact_requests_changed" : "inbox_changed",
790
+ persisted.localRecordId || event.eventId,
791
+ );
792
+ let acknowledged = true;
793
+ if (item.localDelivery !== true) {
794
+ try {
795
+ await options.transport.acknowledge({ event, kind: item.event, persisted });
796
+ } catch (error) {
797
+ const deadline = typeof event.ackDeadlineAt === "string" ? Date.parse(event.ackDeadlineAt) : NaN;
798
+ if (isContactRequest || error?.code !== "relay_ack_delivery_attempt_not_found" || error?.httpStatus !== 409
799
+ || !Number.isFinite(deadline) || new Date(deadline).toISOString() !== event.ackDeadlineAt
800
+ || nowMs(clock) < deadline) throw error;
801
+ // The relay already removed this expired attempt. Keep the durable
802
+ // message and receiver, without claiming a cloud ACK or retrying a send.
803
+ acknowledged = false;
804
+ }
805
+ }
806
+ if (acknowledged && item.localDelivery !== true && !persisted.duplicate
807
+ && preKeyRefillMethod(endpoint)) {
808
+ // A consumed one-time prekey must remain retained for replay protection.
809
+ // Refill only after its ACK has made the server inventory authoritative;
810
+ // a refill error must never roll back an acknowledged local record or stop
811
+ // the Host receiver.
812
+ requestPreKeyRefill(endpoint);
813
+ }
814
+ const change = Object.freeze({
815
+ contractId: HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT,
816
+ type: isContactRequest ? "contact_requests_changed" : "inbox_changed",
817
+ eventId: event.eventId,
818
+ duplicate: persisted.duplicate === true,
819
+ ...(acknowledged ? {} : { acknowledged: false }),
820
+ modelVisibility: "none",
821
+ });
822
+ assertNoSensitiveEventFields(change);
823
+ await changeSink(change);
824
+ return Object.freeze({
825
+ localRecordId: persisted.localRecordId,
826
+ duplicate: persisted.duplicate === true,
827
+ acknowledged,
828
+ ...(persisted.replyTargetDeleted ? { replyTargetDeleted: true } : {}),
829
+ });
830
+ }
831
+
832
+ options.sameHostDeliveryRegistrar?.((item) => {
833
+ if (lifecycle !== "running") throw failure("neutral_host_same_host_delivery_unavailable");
834
+ if (item?.localDelivery !== true || item?.event !== "message") {
835
+ throw failure("neutral_host_same_host_delivery_invalid");
836
+ }
837
+ return trackOperation(handleInbound(item), pendingOperations);
838
+ });
839
+
840
+ async function reportInboundCapacity() {
841
+ const change = Object.freeze({
842
+ contractId: HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT,
843
+ type: "local_storage_full",
844
+ modelVisibility: "none",
845
+ });
846
+ assertNoSensitiveEventFields(change);
847
+ await publishChange(change, "local-storage-capacity");
848
+ inboundCapacityWarningActive = true;
849
+ }
850
+
851
+ function startReceiveTask() {
852
+ receiveTask = receiveLoop().catch((error) => {
853
+ if (lifecycle === "running") {
854
+ // Do not turn a terminal drain failure into an unhandled process abort.
855
+ void beginDrain(safeCode(error, "neutral_host_receive_failed")).catch(() => {});
856
+ }
857
+ });
858
+ }
859
+
860
+ function requestPreKeyRefill(endpoint) {
861
+ const state = preKeyRefillState(endpoint.delegateId);
862
+ if (state.failureCount === 0) state.nextAttemptAtMs = 0;
863
+ preKeyRefillQueue.add(endpoint.delegateId);
864
+ if (preKeyRefillTask) return preKeyRefillTask;
865
+ const task = trackOperation(runPreKeyRefillQueue(), pendingOperations);
866
+ preKeyRefillTask = task;
867
+ task.then(
868
+ () => { if (preKeyRefillTask === task) preKeyRefillTask = null; },
869
+ () => { if (preKeyRefillTask === task) preKeyRefillTask = null; },
870
+ );
871
+ return task;
872
+ }
873
+
874
+ async function runPreKeyRefillQueue() {
875
+ while (preKeyRefillQueue.size > 0 && lifecycle === "running") {
876
+ const delegateId = preKeyRefillQueue.values().next().value;
877
+ preKeyRefillQueue.delete(delegateId);
878
+ const endpoint = endpoints.get(delegateId);
879
+ const refill = preKeyRefillMethod(endpoint);
880
+ if (!refill) continue;
881
+ const state = preKeyRefillState(delegateId);
882
+ const attemptedAtMs = nowMs(clock);
883
+ if (attemptedAtMs < state.nextAttemptAtMs) continue;
884
+ try {
885
+ await refill.call(endpoint.e2ee, { signal: preKeyRefillAbortController.signal });
886
+ state.failureCount = 0;
887
+ state.nextAttemptAtMs = 0;
888
+ state.errorCode = null;
889
+ } catch (error) {
890
+ if (preKeyRefillAbortController.signal.aborted || lifecycle !== "running") return;
891
+ state.failureCount = Math.min(state.failureCount + 1, 31);
892
+ const exponent = Math.min(state.failureCount - 1, 16);
893
+ const backoffMs = Math.min(
894
+ PREKEY_REFILL_RETRY_MAX_MS,
895
+ PREKEY_REFILL_RETRY_MIN_MS * (2 ** exponent),
896
+ );
897
+ state.nextAttemptAtMs = Math.max(
898
+ attemptedAtMs + backoffMs,
899
+ retryAtTimestamp(error?.retryAt) || 0,
900
+ );
901
+ state.errorCode = safeCode(error, "e2ee_prekey_refill_failed");
902
+ }
903
+ }
904
+ }
905
+
906
+ function preKeyRefillMethod(endpoint) {
907
+ if (typeof endpoint?.e2ee?.ensurePreKeyInventory === "function") {
908
+ return endpoint.e2ee.ensurePreKeyInventory;
909
+ }
910
+ return typeof endpoint?.e2ee?.replenishPreKeyInventory === "function"
911
+ ? endpoint.e2ee.replenishPreKeyInventory
912
+ : null;
913
+ }
914
+
915
+ function preKeyRefillState(delegateId) {
916
+ let state = preKeyRefillStates.get(delegateId);
917
+ if (!state) {
918
+ state = { failureCount: 0, nextAttemptAtMs: 0, errorCode: null };
919
+ preKeyRefillStates.set(delegateId, state);
920
+ }
921
+ return state;
922
+ }
923
+
924
+ function preKeyInventoryHealth() {
925
+ const failures = [...preKeyRefillStates.values()].filter((state) => state.failureCount > 0);
926
+ if (failures.length === 0) return { status: "ready", failedEndpointCount: 0 };
927
+ const next = failures.reduce((selected, state) => (
928
+ !selected || state.nextAttemptAtMs < selected.nextAttemptAtMs ? state : selected
929
+ ), null);
930
+ return {
931
+ status: "retrying",
932
+ failedEndpointCount: failures.length,
933
+ retryAt: new Date(next.nextAttemptAtMs).toISOString(),
934
+ errorCode: next.errorCode,
935
+ };
936
+ }
937
+
938
+ async function runMaintenance() {
939
+ const health = await core.runMaintenance();
940
+ if (lifecycle !== "running") return health;
941
+ const now = nowMs(clock);
942
+ if (await handOverWhenQuiet(now)) return health;
943
+ if (relayState.status === "retrying" && now >= relayState.nextAttemptAtMs) void connectRelay();
944
+ if (typeof applicationInbox?.maintainLocalHistory === "function" && !historyMaintenanceTask
945
+ && now - historyMaintenanceAt >= 60_000) {
946
+ historyMaintenanceAt = now;
947
+ // Bounded local bookkeeping only; it does not call or schedule AI work.
948
+ historyMaintenanceTask = trackOperation(Promise.resolve().then(async () => {
949
+ const result = await applicationInbox.maintainLocalHistory({ limit: 50 });
950
+ if (result?.archivedCount > 0 && lifecycle === "running") {
951
+ await publishApplicationChange("inbox_changed", "local_history_maintenance");
952
+ }
953
+ }), pendingOperations).catch(() => undefined).finally(() => { historyMaintenanceTask = null; });
954
+ }
955
+ for (const [delegateId, state] of preKeyRefillStates) {
956
+ if (state.failureCount > 0 && state.nextAttemptAtMs <= now) preKeyRefillQueue.add(delegateId);
957
+ }
958
+ if (preKeyRefillQueue.size > 0) {
959
+ const endpoint = endpoints.get(preKeyRefillQueue.values().next().value);
960
+ if (endpoint) await requestPreKeyRefill(endpoint);
961
+ }
962
+ return runningHealth(health);
963
+ }
964
+
965
+ async function handOverWhenQuiet(now) {
966
+ if (!handoverRequested && now - handoverCheckedAt >= handoverCheckEveryMs) {
967
+ handoverCheckedAt = now;
968
+ const request = await readHandoverRequest(layout.handoverPath).catch(() => null);
969
+ handoverRequested = request?.hostGeneration === hostGeneration;
970
+ }
971
+ // Sends, receipts, prekey refills and verifications are all pendingOperations.
972
+ if (!handoverRequested || lifecycle !== "running" || pendingOperations.size > 0
973
+ || nowMs(clock) - nativeConfirmationAt < HANDOVER_NATIVE_CONFIRMATION_QUIET_MS) return false;
974
+ // The idle-stop drain: admitted work finishes, discovery and lock are
975
+ // released, and adapters reconnect to the release now bound on disk.
976
+ void beginDrain("release_handover_requested").catch(() => undefined);
977
+ return true;
978
+ }
979
+
980
+ async function pauseInboundForLocalCapacity(error) {
981
+ if (inboundPaused) return;
982
+ inboundPaused = true;
983
+ inboundPauseCode = String(error?.code || "");
984
+ await stream?.close?.().catch(() => {});
985
+ if (!inboundCapacityWarningActive) await reportInboundCapacity().catch(() => undefined);
986
+ }
987
+
988
+ async function resumeInboundAfterHumanCleanup(acceptedCapacityCodes) {
989
+ if (!inboundPaused || lifecycle !== "running") return false;
990
+ if (!acceptedCapacityCodes?.has(inboundPauseCode)) return false;
991
+ if (inboundResumeTask) return inboundResumeTask;
992
+ inboundResumeTask = (async () => {
993
+ try {
994
+ const resumed = await openRelayStream(relayAbortController.signal);
995
+ if (!resumed?.events || typeof resumed.events[Symbol.asyncIterator] !== "function") {
996
+ throw failure("neutral_host_transport_stream_invalid");
997
+ }
998
+ if (lifecycle !== "running") {
999
+ await resumed.close?.().catch(() => {});
1000
+ return false;
1001
+ }
1002
+ stream = resumed;
1003
+ inboundPaused = false;
1004
+ inboundPauseCode = null;
1005
+ inboundCapacityWarningActive = false;
1006
+ Object.assign(relayState, { status: "ready", failureCount: 0, nextAttemptAtMs: 0, errorCode: null });
1007
+ startReceiveTask();
1008
+ return true;
1009
+ } catch {
1010
+ return false;
1011
+ } finally {
1012
+ inboundResumeTask = null;
1013
+ }
1014
+ })();
1015
+ return inboundResumeTask;
1016
+ }
1017
+
1018
+ async function sendOutboundOnce({ endpoint, delegateId, clientMessageId, input }) {
1019
+ try {
1020
+ const result = await endpoint.outbound.send(input);
1021
+ if (result?.message?.lastErrorCode === "reply_target_deleted" || result?.relay?.status === "reply_target_deleted") {
1022
+ throw hostApplicationFailure("host_app_reply_target_deleted");
1023
+ }
1024
+ const status = String(result?.status || "");
1025
+ const delivered = [
1026
+ "acknowledged_by_recipient_local_store",
1027
+ "delivered_to_live_connection",
1028
+ ].includes(status);
1029
+ if (result?.ok === false
1030
+ || (status && !delivered)
1031
+ || (!status && result?.ok !== true)) {
1032
+ throw failure("neutral_host_outbound_delivery_failed");
1033
+ }
1034
+ await publishOutboxChange(clientMessageId, delegateId);
1035
+ return result;
1036
+ } catch (error) {
1037
+ // The production messenger persists the failed/unknown outbox state before
1038
+ // throwing. Publish that durable state and return the failure to the caller.
1039
+ // This path never calls retry and never schedules another send attempt.
1040
+ await publishOutboxChange(clientMessageId, delegateId).catch(() => undefined);
1041
+ throw error;
1042
+ }
1043
+ }
1044
+
1045
+ async function publishOutboxChange(clientMessageId, delegateId) {
1046
+ const change = Object.freeze({
1047
+ contractId: HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT,
1048
+ type: "outbox_changed",
1049
+ clientMessageId,
1050
+ modelVisibility: "none",
1051
+ });
1052
+ assertNoSensitiveEventFields(change);
1053
+ await publishChange(change, clientMessageId, delegateId);
1054
+ }
1055
+
1056
+ async function applicationInboxMessages(context) {
1057
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1058
+ const messages = await applicationInbox.list();
1059
+ return {
1060
+ messages: messages
1061
+ .filter((message) => message?.direction === "incoming")
1062
+ .map((message) => projectInboxMessage(message, tuple)),
1063
+ };
1064
+ }
1065
+
1066
+ async function applicationInboxUnreadCount(context) {
1067
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1068
+ if (typeof applicationInbox.getMessageCounts !== "function") throw hostApplicationFailure("host_app_unavailable");
1069
+ // The protected index updates these counts with every message mutation.
1070
+ // A badge needs no bodies and must not include another AI's conversations.
1071
+ const counts = await applicationInbox.getMessageCounts(localHistoryScope(context));
1072
+ if (!Number.isSafeInteger(counts?.inbox?.unread) || counts.inbox.unread < 0) {
1073
+ throw hostApplicationFailure("host_app_response_invalid");
1074
+ }
1075
+ return { unreadCount: counts.inbox.unread };
1076
+ }
1077
+
1078
+ async function applicationInboxMarkRead(context) {
1079
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1080
+ const localMessageId = context.request.localMessageId;
1081
+ const message = await applicationInbox.getIncoming(localMessageId);
1082
+ if (!message) throw hostApplicationFailure("host_app_record_not_found");
1083
+ if (message.direction !== "incoming") throw hostApplicationFailure("host_app_record_not_found");
1084
+ if (!isUnreadIncoming(message)) {
1085
+ scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1086
+ return { localMessageId, unread: false };
1087
+ }
1088
+ const updated = await applicationInbox.markRead(localMessageId);
1089
+ if (!updated || isUnreadIncoming(updated)) throw hostApplicationFailure("host_app_action_failed");
1090
+ await publishApplicationChange("inbox_changed", localMessageId);
1091
+ scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1092
+ return { localMessageId, unread: false };
1093
+ }
1094
+
1095
+ async function applicationInboxMarkAllRead(context) {
1096
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1097
+ if (typeof applicationInbox.markIncomingRead !== "function") throw hostApplicationFailure("host_app_capability_unavailable");
1098
+ const { threadId, beforeReceiptSeq } = context.request;
1099
+ if (threadId !== null) await requireLocalConversation(context, threadId);
1100
+ const { markedCount } = await applicationInbox.markIncomingRead({ scope: localHistoryScope(context), threadId, beforeReceiptSeq });
1101
+ if (markedCount > 0) await publishApplicationChange("inbox_changed", threadId || context.delegateId, context.delegateId);
1102
+ scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1103
+ return { kind: "messages-marked-read", markedCount, threadId, beforeReceiptSeq };
1104
+ }
1105
+ async function applicationConversationAutoRead(context) {
1106
+ if (typeof applicationInbox?.configureConversationAutoRead !== "function") throw hostApplicationFailure("host_app_capability_unavailable");
1107
+ const { threadId, enabled } = context.request;
1108
+ const conversation = await requireLocalConversation(context, threadId);
1109
+ if (!conversation?.binding) throw hostApplicationFailure("host_app_record_not_found");
1110
+ const result = await applicationInbox.configureConversationAutoRead({ scope: localHistoryScope(context),
1111
+ threadId, enabled, conversationBinding: conversation.binding, peerHelloLynkId: conversation.peerHelloLynkId });
1112
+ await publishApplicationChange("inbox_changed", threadId, context.delegateId);
1113
+ return { kind: "conversation-auto-read", ...result };
1114
+ }
1115
+
1116
+ async function applicationInboxDeleteRead(context) {
1117
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1118
+ const localMessageIds = context.request.localMessageIds;
1119
+ const selected = await Promise.all(localMessageIds.map((id) => applicationInbox.getIncoming(id)));
1120
+ if (selected.some((message) => !message || message.direction !== "incoming")) {
1121
+ throw hostApplicationFailure("host_app_record_not_found");
1122
+ }
1123
+ if (selected.some(isUnreadIncoming)) throw hostApplicationFailure("host_app_state_conflict");
1124
+ const result = await applicationInbox.deleteRead(localMessageIds);
1125
+ const deletedIds = result?.deletedIds;
1126
+ if (!Array.isArray(deletedIds) || new Set(deletedIds).size !== deletedIds.length
1127
+ || deletedIds.some((id) => !localMessageIds.includes(id))) {
1128
+ throw hostApplicationFailure("host_app_action_failed");
1129
+ }
1130
+ // Earlier or concurrent cleanup may have left only deletion tombstones.
1131
+ // The store's committed result is authoritative; report the actual count
1132
+ // instead of turning successful cleanup into a server error.
1133
+ for (const localMessageId of deletedIds) await publishApplicationChange("inbox_changed", localMessageId);
1134
+ scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1135
+ return { deletedCount: deletedIds.length, deletedIds };
1136
+ }
1137
+
1138
+ function localHistoryScope(context) {
1139
+ return { principalId: tuple.principalId, delegateId: context.delegateId, region: tuple.region };
1140
+ }
1141
+
1142
+ function resumeAfterLocalHistoryCleanup(result) {
1143
+ if (result.deletedCount > 0) scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1144
+ }
1145
+
1146
+ function scheduleInboundResumeAfterHumanCleanup(acceptedCapacityCodes) {
1147
+ if (!inboundPaused || lifecycle !== "running" || !acceptedCapacityCodes.has(inboundPauseCode)) return false;
1148
+ // The completed local cleanup never waits for reconnecting the receive
1149
+ // stream. Track that separate recovery so Host shutdown can drain it.
1150
+ void trackOperation(Promise.resolve().then(() => resumeInboundAfterHumanCleanup(acceptedCapacityCodes)),
1151
+ pendingOperations).catch(() => {});
1152
+ return true;
1153
+ }
1154
+
1155
+ async function requireLocalConversation(context, threadId) {
1156
+ const scope = localHistoryScope(context);
1157
+ const summary = typeof applicationInbox.readWorkbenchSummary === "function"
1158
+ ? await applicationInbox.readWorkbenchSummary(scope) : null;
1159
+ // The indexed summary is authenticated and restricted to this scope (or
1160
+ // this principal's unbound legacy history). Local housekeeping grants no
1161
+ // messaging or execution authority to those legacy records.
1162
+ const savedThread = summary?.threads.find((thread) => thread.threadId === threadId
1163
+ && (thread.binding || thread.legacy === true));
1164
+ if (savedThread) return savedThread;
1165
+ const state = summary ? { messages: [], hiddenThreadIds: [] }
1166
+ : await applicationInbox.readWorkbenchState(scope);
1167
+ if (state.hiddenThreadIds.includes(threadId)) return;
1168
+ if (state.messages.some((message) => {
1169
+ try {
1170
+ const binding = getMessageConversationBinding(message, tuple) || getOutgoingConversationIntent(message, tuple);
1171
+ return binding?.localDelegateId === context.delegateId && instanceConversationKey(binding) === threadId;
1172
+ } catch {
1173
+ // Contradictory old evidence stays in read-only legacy history. It
1174
+ // neither authorizes this operation nor blocks a different valid route.
1175
+ return false;
1176
+ }
1177
+ })) return;
1178
+ const contacts = await applicationContacts.listForDelegate(context.delegateId);
1179
+ for (const contact of contacts) {
1180
+ if (!contact.principalId || contact.hellolynkId?.split(":")[1] !== stableRegion) continue;
1181
+ for (const instance of contact.instances || []) {
1182
+ const binding = { schemaVersion: 1, region: stableRegion,
1183
+ localPrincipalId: tuple.principalId, localDelegateId: context.delegateId,
1184
+ peerPrincipalId: contact.principalId, peerDelegateId: instance.delegateId };
1185
+ if (instanceConversationKey(binding) === threadId) return { binding, peerHelloLynkId: contact.hellolynkId };
1186
+ }
1187
+ }
1188
+ throw hostApplicationFailure("host_app_record_not_found");
1189
+ }
1190
+
1191
+ async function applicationLocalHistory(command, context) {
1192
+ const scope = localHistoryScope(context);
1193
+ const type = command.command;
1194
+ if ([HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread, HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads].includes(type)) {
1195
+ const allArchived = type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads;
1196
+ const conversation = allArchived ? null : await requireLocalConversation(context, command.payload.threadId);
1197
+ const method = allArchived ? "deleteArchivedConversations" : "deleteLocalConversation";
1198
+ if (typeof applicationInbox[method] !== "function") throw hostApplicationFailure("host_app_capability_unavailable");
1199
+ const result = await applicationInbox[method]({ ...scope, ...command.payload,
1200
+ ...(conversation?.binding ? { conversationBinding: conversation.binding, peerHelloLynkId: conversation.peerHelloLynkId } : {}) });
1201
+ resumeAfterLocalHistoryCleanup(result);
1202
+ await publishApplicationChange("inbox_changed", command.payload.threadId || "archived_conversations", context.delegateId);
1203
+ await publishApplicationChange("outbox_changed", command.payload.threadId || "archived_conversations", context.delegateId);
1204
+ return { kind: "local-history", ...result };
1205
+ }
1206
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.configureHistory) {
1207
+ if (typeof applicationInbox.configureLocalHistory !== "function") throw hostApplicationFailure("host_app_capability_unavailable");
1208
+ await applicationInbox.configureLocalHistory({ scope, ...command.payload });
1209
+ await publishApplicationChange("inbox_changed", "local_history_preferences", context.delegateId);
1210
+ historyMaintenanceAt = -Infinity;
1211
+ return {};
1212
+ }
1213
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent) {
1214
+ const result = await applicationInbox.deleteSentLocal({ ...scope, ...command.payload });
1215
+ resumeAfterLocalHistoryCleanup(result);
1216
+ for (const id of command.payload.localMessageIds) await publishApplicationChange("outbox_changed", id, context.delegateId);
1217
+ return { kind: "local-history", deletedCount: result.deletedCount,
1218
+ retainedUnreadCount: result.retainedUnreadCount, retainedOutgoingCount: result.retainedOutgoingCount,
1219
+ ...(result.retainedTaskCount != null ? { retainedTaskCount: result.retainedTaskCount } : {}),
1220
+ ...(result.deletedBytes != null ? { deletedBytes: result.deletedBytes } : {}) };
1221
+ }
1222
+ const { threadId } = command.payload;
1223
+ // The opaque key is never authority: require a saved instance, a scoped
1224
+ // history row, or this delegate's persisted hidden entry, all read locally.
1225
+ const conversation = await requireLocalConversation(context, threadId);
1226
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread) {
1227
+ await applicationInbox.hideLocalConversation({ ...scope, threadId,
1228
+ ...(conversation?.binding ? { conversationBinding: conversation.binding, peerHelloLynkId: conversation.peerHelloLynkId } : {}) });
1229
+ } else if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread) {
1230
+ await applicationInbox.restoreLocalConversation({ ...scope, threadId });
1231
+ } else {
1232
+ const result = await applicationInbox.clearLocalConversation({ ...scope, threadId,
1233
+ ...(Object.hasOwn(command.payload, "before") ? { before: command.payload.before } : {}) });
1234
+ resumeAfterLocalHistoryCleanup(result);
1235
+ await publishApplicationChange("inbox_changed", threadId, context.delegateId);
1236
+ await publishApplicationChange("outbox_changed", threadId, context.delegateId);
1237
+ return { kind: "local-history", deletedCount: result.deletedCount,
1238
+ retainedUnreadCount: result.retainedUnreadCount, retainedOutgoingCount: result.retainedOutgoingCount,
1239
+ ...(result.retainedTaskCount != null ? { retainedTaskCount: result.retainedTaskCount } : {}),
1240
+ ...(result.deletedBytes != null ? { deletedBytes: result.deletedBytes } : {}) };
1241
+ }
1242
+ await publishApplicationChange("inbox_changed", threadId, context.delegateId);
1243
+ return {};
1244
+ }
1245
+
1246
+ async function applicationContactsList(context) {
1247
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1248
+ return {
1249
+ contacts: (await applicationContacts.listForDelegate(context.delegateId)).map(projectContact),
1250
+ };
1251
+ }
1252
+
1253
+ async function readContactInstances(context) {
1254
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1255
+ if (typeof options.transport.listAgentInstances !== "function") {
1256
+ throw hostApplicationFailure("host_app_capability_unavailable");
1257
+ }
1258
+ const contact = (await applicationContacts.listForDelegate(context.delegateId))
1259
+ .find((item) => item.contactId === context.request.contactId);
1260
+ if (!contact) throw hostApplicationFailure("host_app_record_not_found");
1261
+ if (!contact.hellolynkId.startsWith(`hellolynk:${stableRegion}:`)) {
1262
+ throw hostApplicationFailure("host_app_authorization_required");
1263
+ }
1264
+ const directory = await options.transport.listAgentInstances({ hellolynkId: contact.hellolynkId });
1265
+ if (directory.hellolynkId !== contact.hellolynkId
1266
+ || (contact.principalId && directory.principalId !== contact.principalId)
1267
+ || (contact.hellolynkId === endpoints.get(context.delegateId)?.hellolynkId
1268
+ && directory.principalId !== tuple.principalId)
1269
+ || !Array.isArray(directory.instances)) {
1270
+ throw hostApplicationFailure("host_app_instance_directory_invalid");
1271
+ }
1272
+ const principalId = safeId(directory.principalId, "host_app_instance_directory_invalid");
1273
+ const seen = new Set();
1274
+ const instances = directory.instances.map((item) => {
1275
+ const delegateId = safeId(item.delegateId, "host_app_instance_directory_invalid");
1276
+ if (seen.has(delegateId) || item.status !== "active"
1277
+ || !["codex", "openclaw", "trae-work", "workbuddy", "cursor", "dsh", "claude-code"].includes(item.clientType)) {
1278
+ throw hostApplicationFailure("host_app_instance_directory_invalid");
1279
+ }
1280
+ seen.add(delegateId);
1281
+ const createdAtMs = typeof item.createdAt === "string" && item.createdAt.length <= 64 ? Date.parse(item.createdAt) : NaN;
1282
+ return { delegateId, displayName: safeDisplayName(item.displayName, "host_app_instance_directory_invalid"),
1283
+ clientType: item.clientType, status: "active",
1284
+ ...(Number.isFinite(createdAtMs) ? { createdAt: new Date(createdAtMs).toISOString() } : {}) };
1285
+ }).filter((item) => principalId !== tuple.principalId || item.delegateId !== context.delegateId);
1286
+ return { kind: "contact-instances", contactId: contact.contactId,
1287
+ principalId, hellolynkId: contact.hellolynkId, instances };
1288
+ }
1289
+
1290
+ async function saveContactInstance(context) {
1291
+ const directory = await readContactInstances(context);
1292
+ const instance = directory.instances.find((item) => item.delegateId === context.request.recipientDelegateId);
1293
+ if (!instance || typeof applicationContacts.upsertInstance !== "function") {
1294
+ throw hostApplicationFailure("host_app_record_not_found");
1295
+ }
1296
+ const storedContact = (await applicationContacts.listForDelegate(context.delegateId))
1297
+ .find((item) => item.contactId === directory.contactId);
1298
+ const storedInstance = storedContact?.instances?.find((item) => item.delegateId === instance.delegateId);
1299
+ const displayName = context.request.displayName
1300
+ ?? storedInstance?.displayName
1301
+ ?? instance.displayName;
1302
+ await applicationContacts.upsertInstance(directory.contactId, {
1303
+ principalId: directory.principalId, delegateId: instance.delegateId,
1304
+ clientType: instance.clientType,
1305
+ displayName: safeDisplayName(displayName, "host_app_instance_display_name_invalid"),
1306
+ });
1307
+ await publishApplicationChange("contacts_changed", directory.contactId);
1308
+ }
1309
+
1310
+ async function renameContact(context) {
1311
+ if (typeof applicationContacts?.rename !== "function") throw hostApplicationFailure("host_app_capability_unavailable");
1312
+ const { contactId, displayName, expectedDisplayName, expectedHelloLynkId } = context.request;
1313
+ if (!expectedHelloLynkId.startsWith(`hellolynk:${stableRegion}:`)) throw hostApplicationFailure("host_app_authorization_required");
1314
+ try {
1315
+ await applicationContacts.rename(contactId, { displayName, expectedDisplayName, expectedHelloLynkId });
1316
+ } catch (error) {
1317
+ if (error?.code === "contact_name_conflict") throw hostApplicationFailure("host_app_revision_conflict");
1318
+ if (/^contact_(?:display_name|hellolynk_id|id)_/.test(String(error?.code || ""))) {
1319
+ throw hostApplicationFailure("host_app_input_invalid");
1320
+ }
1321
+ throw error;
1322
+ }
1323
+ await publishApplicationChange("contacts_changed", contactId);
1324
+ }
1325
+
1326
+ async function renameContactInstance(context) {
1327
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1328
+ if (typeof applicationContacts.renameInstance !== "function") {
1329
+ throw hostApplicationFailure("host_app_capability_unavailable");
1330
+ }
1331
+ const request = context.request;
1332
+ if (!request.expectedHelloLynkId.startsWith(`hellolynk:${stableRegion}:`)
1333
+ || request.recipientDelegateId === context.delegateId
1334
+ || (request.expectedHelloLynkId === endpoints.get(context.delegateId)?.hellolynkId
1335
+ && request.expectedPrincipalId !== tuple.principalId)) {
1336
+ throw hostApplicationFailure("host_app_authorization_required");
1337
+ }
1338
+ try {
1339
+ await applicationContacts.renameInstance(request.contactId, {
1340
+ delegateId: request.recipientDelegateId,
1341
+ displayName: request.displayName,
1342
+ expectedDisplayName: request.expectedDisplayName,
1343
+ expectedHelloLynkId: request.expectedHelloLynkId,
1344
+ expectedPrincipalId: request.expectedPrincipalId,
1345
+ });
1346
+ } catch (error) {
1347
+ if (error?.code === "contact_instance_name_conflict") throw hostApplicationFailure("host_app_revision_conflict");
1348
+ if (/^contact_(?:display_name|hellolynk_id|delegate|id)_/.test(String(error?.code || ""))) {
1349
+ throw hostApplicationFailure("host_app_input_invalid");
1350
+ }
1351
+ throw error;
1352
+ }
1353
+ await publishApplicationChange("contacts_changed", request.contactId);
1354
+ }
1355
+
1356
+ async function deleteContactInstance(context) {
1357
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1358
+ if (typeof applicationContacts.removeInstance !== "function") {
1359
+ throw hostApplicationFailure("host_app_capability_unavailable");
1360
+ }
1361
+ await applicationContacts.removeInstance(context.request.contactId, context.request.recipientDelegateId);
1362
+ await publishApplicationChange("contacts_changed", context.request.contactId);
1363
+ }
1364
+
1365
+ async function resolveSavedInstance(input, delegateId) {
1366
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1367
+ if (!endpoints.has(delegateId) || typeof applicationContacts.resolveInstance !== "function") {
1368
+ throw hostApplicationFailure("host_app_authorization_required");
1369
+ }
1370
+ const recipientDelegateId = safeId(input.recipientDelegateId, "host_app_recipient_delegate_invalid");
1371
+ if (recipientDelegateId === delegateId) throw hostApplicationFailure("host_app_authorization_required");
1372
+ const contact = (await applicationContacts.listForDelegate(delegateId)).find((item) => (
1373
+ input.contactId ? item.contactId === input.contactId : item.hellolynkId === input.recipientHelloLynkId
1374
+ ));
1375
+ if (!contact || (input.recipientHelloLynkId && contact.hellolynkId !== input.recipientHelloLynkId)
1376
+ || !contact.hellolynkId.startsWith(`hellolynk:${stableRegion}:`)) {
1377
+ throw hostApplicationFailure("host_app_record_not_found");
1378
+ }
1379
+ try {
1380
+ return await applicationContacts.resolveInstance(contact.contactId, recipientDelegateId);
1381
+ } catch (error) {
1382
+ if (["contact_not_found", "contact_instance_not_found"].includes(error?.code)) {
1383
+ throw hostApplicationFailure("host_app_record_not_found");
1384
+ }
1385
+ throw error;
1386
+ }
1387
+ }
1388
+
1389
+ function savedInstanceRoute(target) {
1390
+ if (target.principalId !== tuple.principalId || !endpoints.has(target.recipientDelegateId)) {
1391
+ return { routeKind: "instance_direct", recipientDelegateId: target.recipientDelegateId };
1392
+ }
1393
+ return { routeKind: "owned_delegate", recipientDelegateId: target.recipientDelegateId,
1394
+ recipientHostId: tuple.hostInstallationId };
1395
+ }
1396
+
1397
+
1398
+ async function applicationContactsUpsert(context) {
1399
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1400
+ const contactWriteKeys = Object.keys(context.request || {}).sort();
1401
+ const allowedContactWriteKeys = Object.hasOwn(context.request || {}, "contactId")
1402
+ ? ["contactId", "displayName", "hellolynkId"]
1403
+ : ["displayName", "hellolynkId"];
1404
+ if (contactWriteKeys.length !== allowedContactWriteKeys.length
1405
+ || contactWriteKeys.some((key, index) => key !== allowedContactWriteKeys[index])) {
1406
+ // Contact edits must not inject internal E2EE identity fingerprints.
1407
+ // Neither the direct application route nor Workbench may transport one.
1408
+ throw hostApplicationFailure("host_app_input_invalid");
1409
+ }
1410
+ const ownHelloLynkId = endpoints.get(context.delegateId)?.hellolynkId;
1411
+ if (!ownHelloLynkId || context.request.hellolynkId !== ownHelloLynkId) {
1412
+ if (!options.humanRelationships
1413
+ || typeof options.humanRelationships.assertActiveRelationship !== "function") {
1414
+ throw hostApplicationFailure("host_app_relationship_status_unavailable");
1415
+ }
1416
+ try {
1417
+ await options.humanRelationships.assertActiveRelationship({
1418
+ hellolynkId: context.request.hellolynkId,
1419
+ });
1420
+ } catch (error) {
1421
+ if (error?.code === "human_relationship_active_relationship_required") {
1422
+ throw hostApplicationFailure("host_app_contact_relationship_not_active");
1423
+ }
1424
+ throw hostApplicationFailure("host_app_relationship_status_unavailable");
1425
+ }
1426
+ }
1427
+ let stored;
1428
+ try {
1429
+ stored = await applicationContacts.upsert(context.request);
1430
+ } catch (error) {
1431
+ if (/^contact_(?:display_name|hellolynk_id|safety_code|id|capacity)/.test(String(error?.code || ""))) {
1432
+ throw hostApplicationFailure("host_app_input_invalid");
1433
+ }
1434
+ throw error;
1435
+ }
1436
+ const contact = projectContactMutation(stored);
1437
+ await publishApplicationChange("contacts_changed", contact.contactId);
1438
+ return { contact };
1439
+ }
1440
+
1441
+ async function applicationContactsRemove(context) {
1442
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1443
+ const contactId = context.request.contactId;
1444
+ const removed = await applicationContacts.remove(contactId);
1445
+ if (removed) await publishApplicationChange("contacts_changed", contactId);
1446
+ return { contactId, removed: Boolean(removed) };
1447
+ }
1448
+
1449
+ async function applicationContactAutonomousSendAuthorization(context) {
1450
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1451
+ let stored;
1452
+ try {
1453
+ stored = await applicationContacts.setAutonomousSendAuthorization(
1454
+ context.request.contactId,
1455
+ context.delegateId,
1456
+ context.request.enabled,
1457
+ context.request.recipientDelegateId,
1458
+ );
1459
+ } catch (error) {
1460
+ if (["contact_not_found", "contact_instance_not_found"].includes(error?.code)) throw hostApplicationFailure("host_app_record_not_found");
1461
+ if (/^contact_(?:authorization|delegate)/.test(String(error?.code || ""))) {
1462
+ throw hostApplicationFailure("host_app_input_invalid");
1463
+ }
1464
+ throw error;
1465
+ }
1466
+ const contact = projectContactAuthorizationMutation(stored);
1467
+ await publishApplicationChange("contacts_changed", contact.contactId, context.delegateId);
1468
+ return { contact };
1469
+ }
1470
+
1471
+
1472
+ async function applicationContactRequestsClearResolved() {
1473
+ if (!options.humanRelationships
1474
+ || typeof options.humanRelationships.clearResolved !== "function") {
1475
+ throw hostApplicationFailure("host_app_capability_unavailable");
1476
+ }
1477
+ const result = await options.humanRelationships.clearResolved();
1478
+ if (!Number.isSafeInteger(result?.deletedCount) || result.deletedCount < 0
1479
+ || result.localOnly !== true) {
1480
+ throw hostApplicationFailure("host_app_contact_request_clear_response_invalid");
1481
+ }
1482
+ if (result.deletedCount > 0) {
1483
+ await publishApplicationChange("contact_requests_changed", "resolved-contact-requests");
1484
+ }
1485
+ const resumeScheduled = scheduleInboundResumeAfterHumanCleanup(CONTACT_REQUEST_LOCAL_CAPACITY_CODES);
1486
+ return {
1487
+ deletedCount: result.deletedCount,
1488
+ localOnly: result.localOnly === true,
1489
+ inboundResume: resumeScheduled ? "resume_scheduled" : "not_applicable",
1490
+ modelCalls: 0,
1491
+ };
1492
+ }
1493
+
1494
+ async function applicationAuthorizedContactsList(context) {
1495
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1496
+ const contacts = (await applicationContacts.listAuthorizedForDelegate(context.delegateId)).map(projectAuthorizedContact);
1497
+ if (!context.request?.recipientDelegateId) return { contacts };
1498
+ const authorization = await resolveAuthorizedSendRequest(context);
1499
+ const { contactId: _contact, recipientDelegateId: _recipient, ...query } = context.request;
1500
+ const nativeConversations = await queryOwnNativeDirectory(context.delegateId, authorization, query);
1501
+ await context.revalidateCapability();
1502
+ await resolveAuthorizedSendRequest(context);
1503
+ return { contacts, nativeConversations };
1504
+ }
1505
+
1506
+ async function localNativeDirectory(endpoint, query) {
1507
+ if (!endpoint?.nativeDirectoryAdapterId) return nativeDirectoryResult({ conversations: [], coverage: "unavailable",
1508
+ available: false, exactTargetSupported: false, reason: "native_directory_unavailable" });
1509
+ try {
1510
+ await endpoint.assertNativeDirectoryCurrent?.();
1511
+ const result = await queryLocalNativeConversations({ runtimeRoot, region: tuple.region, principalId: tuple.principalId,
1512
+ delegateId: endpoint.delegateId, adapterId: endpoint.nativeDirectoryAdapterId }, query);
1513
+ await endpoint.assertNativeDirectoryCurrent?.();
1514
+ return result;
1515
+ } catch (error) {
1516
+ if (error?.code === "native_directory_cursor_invalid") throw error;
1517
+ throw hostApplicationFailure("native_directory_unavailable");
1518
+ }
1519
+ }
1520
+
1521
+ async function queryOwnNativeDirectory(delegateId, authorization, query) {
1522
+ if (authorization.principalId !== tuple.principalId) throw hostApplicationFailure("native_directory_own_ai_only");
1523
+ query = nativeConversationQuery(query);
1524
+ const local = endpoints.get(authorization.recipientDelegateId);
1525
+ if (local) return localNativeDirectory(local, query);
1526
+ return queryPeerNativeDirectory(delegateId, authorization, { kind: "request", query });
1527
+ }
1528
+
1529
+ async function queryPeerNativeDirectory(delegateId, authorization, request) {
1530
+ const sender = endpoints.get(delegateId);
1531
+ if (!sender?.outbound?.sendDirectory) throw hostApplicationFailure("native_directory_unavailable");
1532
+ const friend = request.kind === "friend_target_request";
1533
+ const unavailable = friend ? null : nativeDirectoryResult({ conversations: [], coverage: "unavailable",
1534
+ available: false, exactTargetSupported: false, reason: "native_directory_unavailable" });
1535
+ const requestId = "directory_" + crypto.randomUUID();
1536
+ let timer;
1537
+ const response = new Promise(resolve => {
1538
+ timer = setTimeout(() => resolve(unavailable), 12_000);
1539
+ directoryPending.set(requestId, { delegateId, peerDelegateId: authorization.recipientDelegateId,
1540
+ peerPrincipalId: authorization.principalId, peerHelloLynkId: authorization.hellolynkId, friend, resolve });
1541
+ });
1542
+ try {
1543
+ const sent = await sender.outbound.sendDirectory({ clientMessageId: "hellolynk_local_" + crypto.randomUUID(), recipientHelloLynkId: authorization.hellolynkId,
1544
+ recipientPrincipalId: authorization.principalId, route: savedInstanceRoute(authorization),
1545
+ nativeDirectory: { ...request, requestId } });
1546
+ if (["recipient_e2ee_device_offline", "recipient_offline"].includes(sent?.status))
1547
+ throw hostApplicationFailure("recipient_e2ee_device_offline");
1548
+ if (friend && (sent?.status !== "acknowledged_by_recipient_local_store"
1549
+ || typeof sent.targetConnectionId !== "string" || !SAFE_ID.test(sent.targetConnectionId))) return unavailable;
1550
+ if (!["acknowledged_by_recipient_local_store", "delivered_to_live_connection"].includes(sent?.status)) return unavailable;
1551
+ const result = await response;
1552
+ return friend && result ? { ...result, friendTargetConnectionId: sent.targetConnectionId } : result;
1553
+ } catch (error) {
1554
+ if (friend && ["ordinary_message_not_permitted", "e2ee_recipient_external_contact_disabled"].includes(error?.code))
1555
+ throw hostApplicationFailure("host_app_authorization_required");
1556
+ if (["e2ee_peer_identity_pin_mismatch",
1557
+ "recipient_e2ee_device_offline", "e2ee_prekey_claim_rate_limited"].includes(error?.code)) throw hostApplicationFailure(error.code);
1558
+ throw hostApplicationFailure("native_directory_unavailable");
1559
+ } finally { clearTimeout(timer); directoryPending.delete(requestId); }
1560
+ }
1561
+
1562
+ function isFriendTargetFrame(opened, localDelivery) {
1563
+ // The authenticated relay checks the active friendship, blocks and exact
1564
+ // exposed AI on both the key claim and the write. Local delivery is for
1565
+ // one account only. Receiving doesn't require saving the sender's AI.
1566
+ return localDelivery !== true && opened.e2eeVersion === 2 && opened.routeKind === "instance_direct"
1567
+ && opened.intentSource === "human_preauthorized_contact";
1568
+ }
1569
+
1570
+ function friendTargetKey(opened, endpoint, clientMessageId, targetConversationId) {
1571
+ return JSON.stringify([opened.senderPrincipalId, opened.senderDelegateId, opened.senderIdentityKeyId,
1572
+ endpoint.delegateId, clientMessageId, targetConversationId]);
1573
+ }
1574
+
1575
+ function admitFriendTarget(opened, endpoint, clientMessageId) {
1576
+ const key = friendTargetKey(opened, endpoint, clientMessageId, opened.targetConversationId);
1577
+ const expires = friendTargets.get(key) ?? 0;
1578
+ // The store keeps the first disposition on replay, even after a restart.
1579
+ return expires > nowMs(clock) ? "direct" : "held";
1580
+ }
1581
+
1582
+ function rememberFriendTarget(opened, endpoint, clientMessageId, targetConversationId) {
1583
+ const now = nowMs(clock);
1584
+ for (const [key, expires] of friendTargets) {
1585
+ if (expires <= now) friendTargets.delete(key);
1586
+ }
1587
+ const key = friendTargetKey(opened, endpoint, clientMessageId, targetConversationId);
1588
+ if (!friendTargets.has(key) && friendTargets.size >= 1024) return false;
1589
+ // Allow time to prepare a ciphertext after the lookup. An unused grant is
1590
+ // tied to this message, sending key and local AI for its entire lifetime.
1591
+ friendTargets.set(key, now + DIRECT_TEXT_E2EE_MAX_TTL_MS + 90_000);
1592
+ return true;
1593
+ }
1594
+
1595
+ function uniqueTitleTarget(catalog) {
1596
+ const [row] = catalog.conversations;
1597
+ return catalog.available && catalog.exactTargetSupported && catalog.conversations.length === 1
1598
+ && ["idle", "active", "notLoaded"].includes(row?.state) ? row.targetConversationId : null;
1599
+ }
1600
+
1601
+ async function queryFriendTarget(delegateId, authorization, title, clientMessageId) {
1602
+ const result = await queryPeerNativeDirectory(delegateId, authorization, { kind: "friend_target_request", title, clientMessageId });
1603
+ if (!result) throw hostApplicationFailure("native_target_unsupported_or_unavailable");
1604
+ if (!result.targetConversationId) throw hostApplicationFailure("native_target_title_not_unique");
1605
+ return { ...result, targetConversationTitle: title };
1606
+ }
1607
+
1608
+ async function consumeNativeFrame(event, opened, endpoint, localDelivery) {
1609
+ // A reserved protocol frame is never a chat message. A malformed or
1610
+ // unauthorized frame cannot stop the Host or be sent to a model as text.
1611
+ // Storage/commit errors still propagate; only validated content is dropped.
1612
+ await opened.commit();
1613
+ let acknowledged = true;
1614
+ if (!localDelivery) try { await options.transport.acknowledge({ event, kind: "message", persisted: {} }); }
1615
+ catch (error) {
1616
+ const deadline = typeof event.ackDeadlineAt === "string" ? Date.parse(event.ackDeadlineAt) : NaN;
1617
+ if (error?.code !== "relay_ack_delivery_attempt_not_found" || error?.httpStatus !== 409
1618
+ || !Number.isFinite(deadline) || new Date(deadline).toISOString() !== event.ackDeadlineAt
1619
+ || nowMs(clock) < deadline) throw error;
1620
+ acknowledged = false;
1621
+ }
1622
+ if (acknowledged && !localDelivery && preKeyRefillMethod(endpoint)) requestPreKeyRefill(endpoint);
1623
+ return { acknowledged, duplicate: false };
1624
+ }
1625
+
1626
+ async function handleNativeDirectory(event, opened, endpoint, localDelivery) {
1627
+ const control = opened.nativeDirectory;
1628
+ const friend = control.kind === "friend_target_request" || control.kind === "friend_target_response";
1629
+ if (opened.replyToClientMessageId || opened.targetConversationId || opened.e2eeVersion !== 2
1630
+ || (friend ? opened.senderPrincipalId === tuple.principalId || !isFriendTargetFrame(opened, localDelivery)
1631
+ : opened.senderPrincipalId !== tuple.principalId)) return consumeNativeFrame(event, opened, endpoint, localDelivery);
1632
+ // Read-only control traffic has no human unread state and never reaches a
1633
+ // model. Consume its authenticated prekey before acknowledging transport.
1634
+ const consumed = await consumeNativeFrame(event, opened, endpoint, localDelivery);
1635
+ if (control.kind === "response" || control.kind === "friend_target_response") {
1636
+ const pending = directoryPending.get(control.requestId);
1637
+ if (pending?.delegateId === endpoint.delegateId && pending.peerDelegateId === opened.senderDelegateId
1638
+ && pending.peerPrincipalId === opened.senderPrincipalId && pending.peerHelloLynkId === opened.senderHelloLynkId
1639
+ && pending.friend === friend) pending.resolve(friend
1640
+ ? { targetConversationId: control.targetConversationId } : nativeDirectoryResult(control.result));
1641
+ } else {
1642
+ // Do not await the response on the inbound event stream: two simultaneous
1643
+ // queries must not wait for each other's receiver to process an ACK.
1644
+ void (async () => {
1645
+ const query = friend ? { title: control.title, exactTitle: true } : control.query;
1646
+ let result;
1647
+ try { result = await localNativeDirectory(endpoint, query); }
1648
+ catch { result = nativeDirectoryResult({ conversations: [], coverage: "unavailable", available: false,
1649
+ exactTargetSupported: false, reason: "native_directory_unavailable" }); }
1650
+ let response = { kind: "response", requestId: control.requestId, result };
1651
+ if (friend) {
1652
+ const target = uniqueTitleTarget(result);
1653
+ response = { kind: "friend_target_response", requestId: control.requestId,
1654
+ targetConversationId: target && rememberFriendTarget(opened, endpoint, control.clientMessageId, target) ? target : null };
1655
+ }
1656
+ await endpoint.outbound.sendDirectory({ clientMessageId: "hellolynk_local_" + crypto.randomUUID(),
1657
+ recipientHelloLynkId: opened.senderHelloLynkId, recipientPrincipalId: opened.senderPrincipalId,
1658
+ route: { routeKind: "instance_direct", recipientDelegateId: opened.senderDelegateId },
1659
+ nativeDirectory: response });
1660
+ })().catch(() => {}); // The caller times out; control queries never resend user messages.
1661
+ }
1662
+ return consumed;
1663
+ }
1664
+
1665
+ async function applicationAuthorizedInboxActivate(context) {
1666
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1667
+ if (typeof applicationInbox.activateInboxReader !== "function") {
1668
+ throw hostApplicationFailure("host_app_unavailable");
1669
+ }
1670
+ await context.revalidateCapability();
1671
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1672
+ if (typeof applicationContacts.listReadableRoutesForDelegate !== "function") throw hostApplicationFailure("host_app_unavailable");
1673
+ // Activation records the owner's durable inbox boundary, not a contact
1674
+ // grant. It needs no contact labels and must not reread them on idle polls.
1675
+ await applicationInbox.activateInboxReader(context.adapterId, context.delegateId);
1676
+ await context.revalidateCapability();
1677
+ return { active: true };
1678
+ }
1679
+
1680
+ async function applicationModelInbox(context) {
1681
+ const identity = { adapterId: context.adapterId, delegateId: context.delegateId };
1682
+ const child = (query, extra = {}) => ({ ...context, request: query, ...extra });
1683
+ await context.revalidateCapability();
1684
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1685
+ if (typeof applicationInbox.getInboxNativeAssignment !== "function") throw hostApplicationFailure("host_app_unavailable");
1686
+ return runModelInbox(context.request, {
1687
+ nativeScope: { runtimeRoot, region: tuple.region, principalId: tuple.principalId, ...identity },
1688
+ assignment: query => applicationInbox.getInboxNativeAssignment({ ...identity, ...query }),
1689
+ assignments: localMessageIds => applicationInbox.getInboxNativeAssignments({ ...identity, localMessageIds }),
1690
+ metadata: async query => {
1691
+ const message = await applicationInbox.getIncoming(query.localMessageId);
1692
+ const binding = message && getMessageConversationBinding(message, tuple);
1693
+ if (!message || message.friendTargetHeld || message.messageType === "local-deleted")
1694
+ throw hostApplicationFailure("host_app_record_not_found");
1695
+ if (!binding || binding.localDelegateId !== context.delegateId || query.conversationKey !== instanceConversationKey(binding)) {
1696
+ throw hostApplicationFailure("host_app_authorization_required");
1697
+ }
1698
+ return projectAuthorizedInboxMetadata(message, null, binding);
1699
+ },
1700
+ list: query => applicationAuthorizedInboxMessages(child(query, { modelInbox: true })),
1701
+ pending: query => applicationAuthorizedInboxMessages(child(query, { modelInbox: true }), true, query),
1702
+ read: query => applicationAuthorizedInboxRead(child(query)),
1703
+ claim: query => query.batchId ? applicationAuthorizedInboxBatch(child(query), "Claim") : applicationAuthorizedInboxClaim(child(query)),
1704
+ });
1705
+ }
1706
+
1707
+ async function inboxWakeBoundary(context) {
1708
+ return await applicationInbox.getInboxReaderBoundary?.(context.adapterId, context.delegateId) ?? null;
1709
+ }
1710
+
1711
+ async function applicationAuthorizedInboxMessages(context, pendingOnly = false, query = null) {
1712
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1713
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1714
+ if (typeof applicationContacts.listReadableRoutesForDelegate !== "function") return query ? { messages: [], nextCursor: null } : { messages: [] };
1715
+ await context.revalidateCapability();
1716
+ const indexedPending = pendingOnly && typeof applicationInbox.listPendingInboxCandidates === "function";
1717
+ if (pendingOnly && !indexedPending && (query || typeof applicationInbox.listInboxClaimIds !== "function")) throw hostApplicationFailure("host_app_unavailable");
1718
+ const afterSeq = await inboxWakeBoundary(context);
1719
+ if (pendingOnly && afterSeq == null) return query ? { messages: [], nextCursor: null } : { messages: [] };
1720
+ const target = query?.localMessageId == null ? null : query;
1721
+ let nextCursor = null;
1722
+ const consumed = pendingOnly && !indexedPending ? new Set(await applicationInbox.listInboxClaimIds(context.delegateId)) : null;
1723
+ let storedMessages;
1724
+ const before = pendingOnly ? null : context.request?.before ?? null;
1725
+ const indexedHistory = !pendingOnly && typeof applicationInbox.listInboxMessagesForDelegate === "function";
1726
+ if (before != null && !indexedHistory) throw hostApplicationFailure("host_app_unavailable");
1727
+ if (!pendingOnly && context.request?.unreadOnly === true) {
1728
+ if (typeof applicationInbox.listInboxMessagesForDelegate !== "function") throw hostApplicationFailure("host_app_unavailable");
1729
+ storedMessages = await applicationInbox.listInboxMessagesForDelegate(context.delegateId, {
1730
+ unreadOnly: true, limit: context.request.limit, scope: localHistoryScope(context),
1731
+ });
1732
+ } else if (indexedPending) {
1733
+ // GET is always the first page. Receivers scan later pages with their
1734
+ // own cursor or recheck one message; the Host keeps no shared position.
1735
+ storedMessages = await applicationInbox.listPendingInboxCandidates({
1736
+ delegateId: context.delegateId, afterSeq: Math.max(afterSeq, query?.cursor ?? 0),
1737
+ limit: target ? 1 : 1000, modelPending: context.modelInbox === true, ...(target ? { localMessageId: target.localMessageId } : {}),
1738
+ });
1739
+ // Ownership is checked below; contact membership does not control access
1740
+ // to already received data. Pages follow stored rows, not visible results.
1741
+ if (!target && storedMessages.length === 1000) nextCursor = storedMessages.at(-1).receiptSeq;
1742
+ } else if (indexedHistory) {
1743
+ try {
1744
+ storedMessages = await applicationInbox.listInboxMessagesForDelegate(context.delegateId, {
1745
+ limit: context.modelInbox ? 1000 : AUTHORIZED_INBOX_HISTORY_LIMIT, scope: localHistoryScope(context), ...(before == null ? {} : { before }),
1746
+ });
1747
+ } catch (error) {
1748
+ // The page anchor must be an incoming message of this AI and account.
1749
+ if (error?.code === "local_history_cursor_invalid") throw hostApplicationFailure("host_app_record_not_found");
1750
+ throw error;
1751
+ }
1752
+ } else storedMessages = typeof applicationInbox.listInboxMessagesForDelegate === "function"
1753
+ ? await applicationInbox.listInboxMessagesForDelegate(context.delegateId, {})
1754
+ : await applicationInbox.list();
1755
+ // Contacts only label records in this response. Empty indexed pages are
1756
+ // common while waiting for mail; don't read/parse every contact for them.
1757
+ // Nonempty pages still get fresh labels, without a cross-request cache.
1758
+ const authorized = new Map((storedMessages.length === 0 ? []
1759
+ : await applicationContacts.listReadableRoutesForDelegate(context.delegateId))
1760
+ .map((contact) => [JSON.stringify([contact.hellolynkId, contact.recipientDelegateId]), contact]));
1761
+ const messages = [];
1762
+ if (pendingOnly) storedMessages.sort((left, right) => (left.receiptSeq ?? 0) - (right.receiptSeq ?? 0));
1763
+ for (const message of storedMessages) {
1764
+ if (message?.direction !== "incoming" || message.friendTargetHeld) continue;
1765
+ const binding = getMessageConversationBinding(message, tuple);
1766
+ if (!binding || binding.localDelegateId !== context.delegateId) continue;
1767
+ const contact = authorized.get(JSON.stringify([
1768
+ message.peerHelloLynkId || message.senderHelloLynkId, binding.peerDelegateId,
1769
+ ]));
1770
+ const label = contact?.principalId === binding.peerPrincipalId ? contact : null;
1771
+ const metadata = projectAuthorizedInboxMetadata(message, label, binding, afterSeq);
1772
+ if (pendingOnly && (!metadata.wakeEligible
1773
+ || consumed?.has(metadata.localMessageId) || (target && metadata.conversationKey !== target.conversationKey))) continue;
1774
+ messages.push(context.modelInbox && pendingOnly ? { ...metadata, receiptSeq: message.receiptSeq } : metadata);
1775
+ if (pendingOnly && messages.length === 1000) break;
1776
+ }
1777
+ await context.revalidateCapability();
1778
+ if (query) return { messages, nextCursor };
1779
+ return {
1780
+ // History browsing is a recent context window, separate from the durable
1781
+ // automatic wake window. Older unhandled messages remain manually claimable.
1782
+ // The index already orders newest first; keeping that order lets the next
1783
+ // explicit page start exactly after this page's last message.
1784
+ messages: pendingOnly ? messages : (indexedHistory ? messages : messages.sort((left, right) =>
1785
+ String(right.createdAt).localeCompare(String(left.createdAt)))).slice(0, context.modelInbox ? 1000 : AUTHORIZED_INBOX_HISTORY_LIMIT),
1786
+ };
1787
+ }
1788
+
1789
+ async function applicationAuthorizedInboxRead(context) {
1790
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1791
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1792
+ const afterSeq = await inboxWakeBoundary(context);
1793
+ if (typeof applicationInbox.readIncomingForModel !== "function") throw hostApplicationFailure("host_app_unavailable");
1794
+ const message = await applicationInbox.readIncomingForModel(context.request.localMessageId,
1795
+ (value, highWater, automatic) => authorizeInboxMessage(context, value, afterSeq, highWater, false, false, automatic));
1796
+ if (message.humanReadAutomatically === true) {
1797
+ await publishApplicationChange("inbox_changed", context.request.localMessageId, context.delegateId).catch(() => undefined);
1798
+ scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1799
+ }
1800
+ return { message };
1801
+ }
1802
+
1803
+ async function authorizeInboxMessage(context, message, afterSeq, highWater, requireWake = false, finish = false, automatic = false) {
1804
+ await context.revalidateCapability();
1805
+ if (!message || message.direction !== "incoming" || message.friendTargetHeld || (message.messageType === "local-deleted" && !finish)) {
1806
+ throw hostApplicationFailure("host_app_record_not_found");
1807
+ }
1808
+ const binding = getMessageConversationBinding(message, tuple);
1809
+ if (!binding || binding.localDelegateId !== context.delegateId
1810
+ || context.request.conversationKey !== instanceConversationKey(binding)) {
1811
+ throw hostApplicationFailure("host_app_authorization_required");
1812
+ }
1813
+ const contacts = await applicationContacts.listReadableRoutesForDelegate(context.delegateId);
1814
+ const contact = contacts.find((candidate) => candidate.principalId === binding.peerPrincipalId
1815
+ && candidate.recipientDelegateId === binding.peerDelegateId
1816
+ && candidate.hellolynkId === (message.peerHelloLynkId || message.senderHelloLynkId)) || null;
1817
+ if (finish && message.messageType === "local-deleted") {
1818
+ await context.revalidateCapability();
1819
+ return {}; // Completing the existing claim exposes no erased body.
1820
+ }
1821
+ const projected = projectAuthorizedInboxMessage(message, contact, binding, afterSeq, automatic);
1822
+ if (requireWake && !projected.wakeEligible) throw hostApplicationFailure("host_app_authorization_required");
1823
+ await context.revalidateCapability();
1824
+ return projected;
1825
+ }
1826
+
1827
+ async function applicationAuthorizedInboxBatch(context, operation) {
1828
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1829
+ await context.revalidateCapability();
1830
+ const method = ({ Reserve: "reserveInboxBatch", Open: "listInboxBatches", Attempt: "attemptInboxBatch",
1831
+ Confirm: "confirmInboxBatch", Cancel: "cancelInboxBatch", Expire: "expireInboxBatch",
1832
+ Closed: "closeInboxBatch", Claim: "claimInboxBatch" })[operation];
1833
+ if (typeof applicationInbox[method] !== "function") throw hostApplicationFailure("host_app_unavailable");
1834
+ // Labels are read once for the whole batch. Ownership is checked per record;
1835
+ // a removed contact never revokes access to this AI's already received mail.
1836
+ const contacts = operation === "Claim"
1837
+ ? await applicationContacts.listReadableRoutesForDelegate(context.delegateId) : [];
1838
+ const labels = new Map(contacts.map(contact => [JSON.stringify([contact.hellolynkId, contact.recipientDelegateId]), contact]));
1839
+ const authorize = (message, afterSeq, highWater, automatic = false) => {
1840
+ if (message.friendTargetHeld) throw hostApplicationFailure("host_app_record_not_found");
1841
+ const binding = getMessageConversationBinding(message, tuple);
1842
+ if (!binding || binding.localDelegateId !== context.delegateId || message.direction !== "incoming") {
1843
+ throw hostApplicationFailure("host_app_authorization_required");
1844
+ }
1845
+ if (operation !== "Claim") return null;
1846
+ const label = labels.get(JSON.stringify([message.peerHelloLynkId || message.senderHelloLynkId, binding.peerDelegateId]));
1847
+ return projectAuthorizedInboxMessage(message, label?.principalId === binding.peerPrincipalId ? label : null, binding, afterSeq, automatic);
1848
+ };
1849
+ const result = await applicationInbox[method]({ ...context.request, adapterId: context.adapterId, delegateId: context.delegateId }, authorize);
1850
+ await context.revalidateCapability();
1851
+ if (["Reserve", "Expire", "Claim", "Closed"].includes(operation)) {
1852
+ await publishApplicationChange("inbox_changed", context.delegateId, context.delegateId).catch(() => undefined);
1853
+ }
1854
+ if (operation === "Closed" || result.messages?.some(message => message.humanReadAutomatically === true)) {
1855
+ scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1856
+ }
1857
+ return result;
1858
+ }
1859
+
1860
+ async function applicationAuthorizedInboxClaim(context, finish = false) {
1861
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1862
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1863
+ const method = finish ? "finishInboxClaim" : "claimInboxMessage";
1864
+ if (typeof applicationInbox[method] !== "function") throw hostApplicationFailure("host_app_unavailable");
1865
+ try {
1866
+ const result = await applicationInbox[method]({ ...context.request, adapterId: context.adapterId, delegateId: context.delegateId },
1867
+ (message, afterSeq, highWater, automatic) => authorizeInboxMessage(context, message, afterSeq, highWater, true, finish, automatic));
1868
+ if (result.claimed === true) await publishApplicationChange("inbox_changed", context.request.localMessageId).catch(() => undefined);
1869
+ if (result.message?.humanReadAutomatically === true) scheduleInboundResumeAfterHumanCleanup(MESSAGE_LOCAL_CAPACITY_CODES);
1870
+ return result;
1871
+ } catch (error) {
1872
+ if (error?.code === "local_inbox_claim_ineligible") throw hostApplicationFailure("host_app_authorization_required");
1873
+ if (["local_inbox_claim_mismatch", "local_inbox_claim_terminal"].includes(error?.code)) {
1874
+ throw hostApplicationFailure("host_app_state_conflict");
1875
+ }
1876
+ throw error;
1877
+ }
1878
+ }
1879
+
1880
+ function contactRequestConversationKey(delegateId) {
1881
+ return `requests_${crypto.createHash("sha256")
1882
+ .update("hellolynk-contact-request-conversation-v1\n")
1883
+ .update(JSON.stringify([stableRegion, tuple.principalId, delegateId]))
1884
+ .digest("hex").slice(0, 40)}`;
1885
+ }
1886
+
1887
+ function projectAuthorizedContactRequestMetadata(record, context) {
1888
+ const request = projectContactRequest(record);
1889
+ return {
1890
+ localRecordId: request.localRecordId,
1891
+ conversationKey: contactRequestConversationKey(context.delegateId),
1892
+ localDelegateId: context.delegateId,
1893
+ sender: request.sender,
1894
+ state: request.state,
1895
+ receivedAt: request.receivedAt,
1896
+ };
1897
+ }
1898
+
1899
+ async function applicationAuthorizedContactRequestsList(context) {
1900
+ requireApplicationStore(applicationContactRequests, "neutral_host_app_contact_requests_unavailable");
1901
+ await context.revalidateCapability();
1902
+ const requests = await applicationContactRequests.listForHuman();
1903
+ await context.revalidateCapability();
1904
+ return { requests: requests.map((record) => projectAuthorizedContactRequestMetadata(record, context)) };
1905
+ }
1906
+
1907
+ async function applicationAuthorizedContactRequestRead(context) {
1908
+ requireApplicationStore(applicationContactRequests, "neutral_host_app_contact_requests_unavailable");
1909
+ await context.revalidateCapability();
1910
+ if (context.request.conversationKey !== contactRequestConversationKey(context.delegateId)) {
1911
+ throw hostApplicationFailure("host_app_authorization_required");
1912
+ }
1913
+ const requests = await applicationContactRequests.listForHuman();
1914
+ const record = requests.find((item) => item.localRecordId === context.request.localRecordId);
1915
+ if (!record) throw hostApplicationFailure("host_app_record_not_found");
1916
+ await context.revalidateCapability();
1917
+ return { request: {
1918
+ ...projectAuthorizedContactRequestMetadata(record, context),
1919
+ introText: projectContactRequest(record).introText,
1920
+ source: "hellolynk_contact_request",
1921
+ externalContentIsUntrustedData: true,
1922
+ readDoesNotMarkRead: true,
1923
+ } };
1924
+ }
1925
+
1926
+ async function applicationAuthorizedSend(context) {
1927
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1928
+ let authorization = await resolveAuthorizedSendRequest(context);
1929
+ const { targetConversationId, targetConversationTitle, clientMessageId } = context.request;
1930
+ let target = {};
1931
+ if (targetConversationTitle || targetConversationId) {
1932
+ if (authorization.principalId !== tuple.principalId) {
1933
+ if (!targetConversationTitle) throw hostApplicationFailure("native_target_title_required");
1934
+ target = await queryFriendTarget(context.delegateId, authorization, targetConversationTitle, clientMessageId);
1935
+ } else {
1936
+ const query = targetConversationTitle ? { title: targetConversationTitle, exactTitle: true } : { targetConversationId };
1937
+ const catalog = await queryOwnNativeDirectory(context.delegateId, authorization, query);
1938
+ if (!catalog.exactTargetSupported) throw hostApplicationFailure("native_target_unsupported_or_unavailable");
1939
+ if (targetConversationTitle) {
1940
+ const resolved = uniqueTitleTarget(catalog);
1941
+ if (!resolved) throw hostApplicationFailure("native_target_title_not_unique");
1942
+ target = { targetConversationId: resolved };
1943
+ } else {
1944
+ if (catalog.conversations.some(row => row.targetConversationId === targetConversationId && ["archived", "deleted"].includes(row.state)))
1945
+ throw hostApplicationFailure("native_target_unavailable");
1946
+ target = { targetConversationId };
1947
+ }
1948
+ }
1949
+ await context.revalidateCapability();
1950
+ authorization = await recheckTargetRecipient(context, authorization);
1951
+ }
1952
+ return sendAuthorizedContactText(context, authorization, null, target);
1953
+ }
1954
+
1955
+ async function recheckTargetRecipient(context, original) {
1956
+ const current = await resolveAuthorizedSendRequest(context);
1957
+ if (current.principalId !== original.principalId || current.hellolynkId !== original.hellolynkId
1958
+ || current.recipientDelegateId !== original.recipientDelegateId) throw hostApplicationFailure("host_app_authorization_required");
1959
+ return current;
1960
+ }
1961
+
1962
+ async function resolveAuthorizedSendRequest(context) {
1963
+ let authorization;
1964
+ try {
1965
+ authorization = await applicationContacts.resolveAuthorizedSend(
1966
+ context.request.contactId,
1967
+ context.delegateId,
1968
+ context.request.recipientDelegateId,
1969
+ );
1970
+ } catch (error) {
1971
+ if (["contact_not_found", "contact_instance_not_found"].includes(error?.code)) throw hostApplicationFailure("host_app_record_not_found");
1972
+ if (error?.code === "contact_autonomous_send_not_authorized") {
1973
+ throw hostApplicationFailure("host_app_authorization_required");
1974
+ }
1975
+ throw error;
1976
+ }
1977
+ return authorization;
1978
+ }
1979
+
1980
+ async function applicationAuthorizedReply(context) {
1981
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
1982
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
1983
+ if (typeof applicationContacts.resolveAuthorizedReply !== "function") {
1984
+ throw hostApplicationFailure("host_app_authorization_required");
1985
+ }
1986
+ const message = await applicationInbox.getIncoming(context.request.localMessageId);
1987
+ if (!message || message.direction !== "incoming" || message.friendTargetHeld || message.messageType === "local-deleted") {
1988
+ throw hostApplicationFailure("host_app_record_not_found");
1989
+ }
1990
+ const binding = getMessageConversationBinding(message, tuple);
1991
+ if (!binding || binding.localDelegateId !== context.delegateId
1992
+ || context.request.conversationKey !== instanceConversationKey(binding)) {
1993
+ throw hostApplicationFailure("host_app_authorization_required");
1994
+ }
1995
+ let authorization;
1996
+ try {
1997
+ authorization = await applicationContacts.resolveAuthorizedReply(
1998
+ message.peerHelloLynkId || message.senderHelloLynkId, context.delegateId, binding.peerDelegateId,
1999
+ );
2000
+ } catch (error) {
2001
+ if (["contact_not_found", "contact_instance_not_found", "contact_continuous_read_not_authorized",
2002
+ "contact_autonomous_send_not_authorized"].includes(error?.code)) {
2003
+ throw hostApplicationFailure("host_app_authorization_required");
2004
+ }
2005
+ throw error;
2006
+ }
2007
+ if (authorization.principalId !== binding.peerPrincipalId) {
2008
+ throw hostApplicationFailure("host_app_authorization_required");
2009
+ }
2010
+ return sendAuthorizedContactText(context, authorization, message.clientMessageId);
2011
+ }
2012
+
2013
+ async function sendAuthorizedContactText(context, authorization, replyToClientMessageId = null, target = {}) {
2014
+ const endpoint = endpoints.get(context.delegateId);
2015
+ if (!endpoint?.outbound || typeof endpoint.outbound.send !== "function") {
2016
+ throw hostApplicationFailure("host_app_unavailable");
2017
+ }
2018
+ let result;
2019
+ try {
2020
+ result = await sendOutboundOnce({
2021
+ endpoint,
2022
+ delegateId: context.delegateId,
2023
+ clientMessageId: context.request.clientMessageId,
2024
+ input: {
2025
+ clientMessageId: context.request.clientMessageId,
2026
+ recipientHelloLynkId: authorization.hellolynkId,
2027
+ recipientPrincipalId: authorization.principalId,
2028
+ recipientDelegateId: authorization.recipientDelegateId,
2029
+ region: tuple.region,
2030
+ route: savedInstanceRoute(authorization),
2031
+ text: context.request.text,
2032
+ intentSource: "human_preauthorized_contact",
2033
+ ...(replyToClientMessageId ? { replyToClientMessageId } : {}),
2034
+ ...target,
2035
+ },
2036
+ });
2037
+ } catch (error) {
2038
+ if (error?.code === "host_app_reply_target_deleted" || error?.code === "reply_target_deleted") throw hostApplicationFailure("host_app_reply_target_deleted");
2039
+ if (["native_directory_unavailable", "e2ee_peer_identity_pin_mismatch",
2040
+ "recipient_e2ee_device_offline", "e2ee_prekey_claim_rate_limited", "relay_request_body_too_large"].includes(error?.code)) throw hostApplicationFailure(error.code);
2041
+ throw hostApplicationFailure("host_app_send_failed");
2042
+ }
2043
+ return {
2044
+ accepted: true,
2045
+ authorization: authorization.authorization,
2046
+ clientMessageId: context.request.clientMessageId,
2047
+ contactId: authorization.contactId,
2048
+ // Keep the recipient-store receipt when this call actually received one.
2049
+ // The durable outbox intentionally retains its coarser delivery status;
2050
+ // neither status means the human read it or the model executed it.
2051
+ status: safeId(result?.relay?.status === "acknowledged_by_recipient_local_store"
2052
+ ? result.relay.status : result?.status || "accepted", "neutral_host_app_outbox_record_invalid"),
2053
+ };
2054
+ }
2055
+
2056
+ async function applicationContactRequestsList() {
2057
+ requireApplicationStore(applicationContactRequests, "neutral_host_app_contact_requests_unavailable");
2058
+ // Remote reconciliation is best-effort for a local-first human UI. A cloud
2059
+ // outage must not hide already authenticated and locally persisted requests;
2060
+ // each local record is still validated below and any malformed record keeps
2061
+ // the endpoint fail-closed.
2062
+ await refreshHumanRelationshipsBestEffort();
2063
+ return {
2064
+ requests: (await applicationContactRequests.listForHuman())
2065
+ .map(projectContactRequest),
2066
+ };
2067
+ }
2068
+
2069
+ async function applicationContactRequestRespond(context) {
2070
+ requireApplicationStore(
2071
+ applicationContactRequests,
2072
+ "neutral_host_app_contact_requests_unavailable",
2073
+ );
2074
+ if (!options.humanRelationships || typeof options.humanRelationships.decide !== "function") {
2075
+ throw hostApplicationFailure("host_app_capability_unavailable");
2076
+ }
2077
+ const requestId = context.request.requestId;
2078
+ const local = (await applicationContactRequests.listForHuman()).find((record) => (
2079
+ record?.humanOnly === true
2080
+ && record?.modelVisibility === "none"
2081
+ && record?.serverContactRequestId === requestId
2082
+ ));
2083
+ if (!local) throw hostApplicationFailure("host_app_record_not_found");
2084
+ if (!Object.hasOwn(local, "decisionOutcome")
2085
+ || !Object.hasOwn(local, "decisionAction")
2086
+ || local.decisionOutcome != null) {
2087
+ throw hostApplicationFailure("host_app_contact_request_decision_unresolved");
2088
+ }
2089
+ if (local.state !== "pending" || local.actionReady !== true
2090
+ || local.decisionOutcome != null || local.decisionAction != null) {
2091
+ throw hostApplicationFailure("host_app_record_not_found");
2092
+ }
2093
+ await options.humanRelationships.decide({
2094
+ requestId,
2095
+ action: context.request.decision,
2096
+ });
2097
+ await publishApplicationChange(
2098
+ "contact_requests_changed",
2099
+ safeApplicationId(local.localRecordId, "neutral_host_app_contact_request_record_invalid"),
2100
+ );
2101
+ }
2102
+
2103
+ async function applicationContactRequestRecover(context) {
2104
+ requireApplicationStore(
2105
+ applicationContactRequests,
2106
+ "neutral_host_app_contact_requests_unavailable",
2107
+ );
2108
+ if (!options.humanRelationships
2109
+ || typeof options.humanRelationships.recoverDecision !== "function") {
2110
+ throw hostApplicationFailure("host_app_capability_unavailable");
2111
+ }
2112
+ const requestId = context.request.requestId;
2113
+ const local = (await applicationContactRequests.listForHuman()).find((record) => (
2114
+ record?.humanOnly === true
2115
+ && record?.modelVisibility === "none"
2116
+ && record?.serverContactRequestId === requestId
2117
+ ));
2118
+ if (!local) throw hostApplicationFailure("host_app_record_not_found");
2119
+ const recoverable = local.state === "pending"
2120
+ && local.actionReady === false
2121
+ && ((local.decisionOutcome === "unknown"
2122
+ && ["accept", "reject", "block"].includes(local.decisionAction))
2123
+ || (local.decisionOutcome === "recovery-required" && local.decisionAction == null));
2124
+ if (!recoverable) {
2125
+ throw hostApplicationFailure("host_app_contact_request_recovery_not_required");
2126
+ }
2127
+ await options.humanRelationships.recoverDecision({ requestId });
2128
+ await publishApplicationChange(
2129
+ "contact_requests_changed",
2130
+ safeApplicationId(local.localRecordId, "neutral_host_app_contact_request_record_invalid"),
2131
+ );
2132
+ }
2133
+
2134
+ async function applicationContactRequestCancel(context) {
2135
+ if (!options.humanRelationships
2136
+ || typeof options.humanRelationships.cancel !== "function") {
2137
+ throw hostApplicationFailure("host_app_capability_unavailable");
2138
+ }
2139
+ const result = await options.humanRelationships.cancel({
2140
+ requestId: context.request.requestId,
2141
+ });
2142
+ await publishApplicationChange("contact_requests_changed", result.requestId);
2143
+ }
2144
+
2145
+ async function applicationRelationshipEnd(context) {
2146
+ if (!options.humanRelationships
2147
+ || typeof options.humanRelationships.endRelationship !== "function") {
2148
+ throw hostApplicationFailure("host_app_capability_unavailable");
2149
+ }
2150
+ const result = await options.humanRelationships.endRelationship({
2151
+ relationshipId: context.request.relationshipId,
2152
+ });
2153
+ if (result.localHistoryDeleted !== false) {
2154
+ throw hostApplicationFailure("host_app_relationship_history_invariant_failed");
2155
+ }
2156
+ await publishApplicationChange("relationships_changed", result.relationship.relationshipId);
2157
+ }
2158
+
2159
+ async function applicationBlockRemove(context) {
2160
+ if (!options.humanRelationships
2161
+ || typeof options.humanRelationships.removeBlock !== "function") {
2162
+ throw hostApplicationFailure("host_app_capability_unavailable");
2163
+ }
2164
+ const result = await options.humanRelationships.removeBlock({
2165
+ blockId: context.request.blockId,
2166
+ });
2167
+ if (result.relationshipRestored !== false || result.authorizationRestored !== false) {
2168
+ throw hostApplicationFailure("host_app_block_remove_invariant_failed");
2169
+ }
2170
+ await publishApplicationChange("blocks_changed", result.block.blockId);
2171
+ }
2172
+
2173
+ async function applicationAccountOpen(context) {
2174
+ if (typeof options.accountLauncher !== "function") {
2175
+ throw hostApplicationFailure("host_app_capability_unavailable");
2176
+ }
2177
+ const result = await options.accountLauncher(Object.freeze({
2178
+ target: context.request.target,
2179
+ }));
2180
+ if (result && !Array.isArray(result) && typeof result === "object"
2181
+ && Object.keys(result).length === 1 && result.opened === true) return;
2182
+ if (result && !Array.isArray(result) && typeof result === "object"
2183
+ && Object.keys(result).length === 1 && result.opened === false) {
2184
+ throw hostApplicationFailure("host_app_account_open_failed");
2185
+ }
2186
+ throw failure("neutral_host_account_launcher_response_invalid");
2187
+ }
2188
+
2189
+ async function refreshHumanRelationshipsBestEffort() {
2190
+ if (!options.humanRelationships) return;
2191
+ relationshipRefreshInFlight ||= Promise.resolve()
2192
+ .then(() => options.humanRelationships.list())
2193
+ .catch(() => undefined)
2194
+ .finally(() => { relationshipRefreshInFlight = null; });
2195
+ let timeout;
2196
+ await Promise.race([
2197
+ relationshipRefreshInFlight,
2198
+ new Promise((resolve) => {
2199
+ timeout = setTimeout(resolve, 1_500);
2200
+ timeout.unref?.();
2201
+ }),
2202
+ ]);
2203
+ clearTimeout(timeout);
2204
+ }
2205
+
2206
+ async function applicationOutboxMessages(context) {
2207
+ requireApplicationStore(applicationOutbox, "neutral_host_app_outbox_unavailable");
2208
+ const messages = await applicationOutbox.list();
2209
+ return {
2210
+ messages: messages
2211
+ .filter((message) => message?.direction === "outgoing")
2212
+ .filter((message) => message.senderPrincipalId === tuple.principalId)
2213
+ .filter((message) => message.senderDelegateId === context.delegateId)
2214
+ .map(projectOutboxMessage),
2215
+ };
2216
+ }
2217
+
2218
+ async function applicationResendDeletedReply(context) {
2219
+ if (context.workbenchRetryAuthorized !== true) throw hostApplicationFailure("host_app_outbox_retry_grant_invalid");
2220
+ requireApplicationStore(applicationOutbox, "neutral_host_app_outbox_unavailable");
2221
+ const original = await applicationOutbox.get(context.request.clientMessageId);
2222
+ if (!original || original.direction !== "outgoing" || original.messageType !== "text"
2223
+ || original.senderPrincipalId !== tuple.principalId || original.senderDelegateId !== context.delegateId
2224
+ || original.status !== "failed_permanent" || original.lastErrorCode !== "reply_target_deleted" || !original.replyToClientMessageId) {
2225
+ throw hostApplicationFailure("host_app_state_conflict");
2226
+ }
2227
+ const clientMessageId = `human_resend_${crypto.createHash("sha256").update(JSON.stringify([
2228
+ tuple.region, tuple.principalId, context.delegateId, original.clientMessageId,
2229
+ ])).digest("hex")}`;
2230
+ const readResult = async () => {
2231
+ const saved = await applicationOutbox.get(clientMessageId);
2232
+ if (!saved || saved.resendSourceClientMessageId !== original.clientMessageId
2233
+ || saved.senderPrincipalId !== tuple.principalId || saved.senderDelegateId !== context.delegateId) {
2234
+ throw hostApplicationFailure("host_app_workbench_outcome_unknown");
2235
+ }
2236
+ return { kind: "deleted-reply-resend", originalClientMessageId: original.clientMessageId, clientMessageId,
2237
+ status: saved.status === "delivered_to_live_connection" ? "sent"
2238
+ : ["failed_retryable", "failed_permanent", "recipient_offline", "cancelled"].includes(saved.status) ? "not-delivered" : "unknown" };
2239
+ };
2240
+ if (original.resendClientMessageId) {
2241
+ if (original.resendClientMessageId !== clientMessageId) throw hostApplicationFailure("host_app_state_conflict");
2242
+ return readResult();
2243
+ }
2244
+ const binding = getMessageConversationBinding(original, tuple) || getOutgoingConversationIntent(original, tuple);
2245
+ if (!binding || binding.localDelegateId !== context.delegateId) throw hostApplicationFailure("host_app_state_conflict");
2246
+ const target = await resolveSavedInstance({ recipientHelloLynkId: original.peerHelloLynkId,
2247
+ recipientDelegateId: binding.peerDelegateId }, context.delegateId);
2248
+ if (target.principalId !== binding.peerPrincipalId) throw hostApplicationFailure("host_app_state_conflict");
2249
+ const endpoint = endpoints.get(context.delegateId);
2250
+ if (typeof endpoint?.outbound?.send !== "function") throw hostApplicationFailure("host_app_unavailable");
2251
+ await context.revalidateCapability?.();
2252
+ try {
2253
+ await sendOutboundOnce({ endpoint, delegateId: context.delegateId, clientMessageId, input: {
2254
+ clientMessageId, recipientHelloLynkId: original.peerHelloLynkId, recipientPrincipalId: binding.peerPrincipalId,
2255
+ recipientDelegateId: binding.peerDelegateId, region: tuple.region, route: savedInstanceRoute(target),
2256
+ text: original.text, intentSource: "human_native_dialog", resendSourceClientMessageId: original.clientMessageId,
2257
+ } });
2258
+ } catch (error) {
2259
+ // Inspect the one reserved copy; never start another send or task here.
2260
+ if (!(await applicationOutbox.get(clientMessageId))) {
2261
+ if (error?.code === "reply_resend_invalid") throw hostApplicationFailure("host_app_state_conflict");
2262
+ throw error;
2263
+ }
2264
+ }
2265
+ return readResult();
2266
+ }
2267
+
2268
+ async function applicationOutboxRetry(context) {
2269
+ requireApplicationStore(applicationOutbox, "neutral_host_app_outbox_unavailable");
2270
+ const clientMessageId = context.request.clientMessageId;
2271
+ const current = await applicationOutbox.get(clientMessageId);
2272
+ if (!current || current.direction !== "outgoing"
2273
+ || current.senderPrincipalId !== tuple.principalId
2274
+ || current.senderDelegateId !== context.delegateId) {
2275
+ throw hostApplicationFailure("host_app_record_not_found");
2276
+ }
2277
+ if (!MANUAL_RETRYABLE_STATUSES.has(current.status) || current.lastErrorCode === "reply_target_deleted") {
2278
+ throw hostApplicationFailure("host_app_state_conflict");
2279
+ }
2280
+ const binding = getMessageConversationBinding(current, tuple) || getOutgoingConversationIntent(current, tuple);
2281
+ if (!binding || binding.localDelegateId !== context.delegateId) {
2282
+ throw hostApplicationFailure("host_app_state_conflict");
2283
+ }
2284
+ const target = await resolveSavedInstance({
2285
+ recipientHelloLynkId: current.peerHelloLynkId || current.recipientHelloLynkId,
2286
+ recipientDelegateId: binding.peerDelegateId,
2287
+ }, context.delegateId);
2288
+ if (target.principalId !== binding.peerPrincipalId) throw hostApplicationFailure("host_app_state_conflict");
2289
+ let retryOptions;
2290
+ if (current.targetConversationId && binding.peerPrincipalId !== tuple.principalId) {
2291
+ if (!current.targetConversationTitle) throw hostApplicationFailure("native_target_unsupported_or_unavailable");
2292
+ const authorizationContext = { delegateId: context.delegateId, request: target };
2293
+ const authorization = await recheckTargetRecipient(authorizationContext, target);
2294
+ const verified = await queryFriendTarget(context.delegateId, authorization, current.targetConversationTitle, clientMessageId);
2295
+ if (verified.targetConversationId !== current.targetConversationId) throw hostApplicationFailure("native_target_unavailable");
2296
+ await recheckTargetRecipient(authorizationContext, authorization);
2297
+ await context.nativeAuthority.revalidate();
2298
+ retryOptions = { friendTargetConnectionId: verified.friendTargetConnectionId };
2299
+ }
2300
+ try {
2301
+ await applicationOutbox.retry(clientMessageId, context.delegateId, retryOptions);
2302
+ } catch (error) {
2303
+ // Another admitted retry may already have claimed the same message.
2304
+ // Report the state conflict without retrying or adopting its result.
2305
+ if (error?.code === "local_outgoing_message_not_retryable") {
2306
+ throw hostApplicationFailure("host_app_state_conflict");
2307
+ }
2308
+ // LocalMessenger persists a definite recipient rejection before reporting
2309
+ // it as an error. Project that completed state to the human, but never
2310
+ // turn an unrelated retry/lock/E2EE failure into a successful mutation.
2311
+ const updated = await applicationOutbox.get(clientMessageId);
2312
+ const sameAttempt = (
2313
+ updated?.clientMessageId === current.clientMessageId
2314
+ && updated?.localMessageId === current.localMessageId
2315
+ && updated.direction === "outgoing"
2316
+ && updated.senderPrincipalId === tuple.principalId
2317
+ && updated.senderDelegateId === context.delegateId
2318
+ && Number.isSafeInteger(Number(current.attempts))
2319
+ && Number.isSafeInteger(Number(updated.attempts))
2320
+ && Number(updated.attempts) === Number(current.attempts) + 1
2321
+ );
2322
+ if (sameAttempt && updated.status === "failed_permanent") {
2323
+ await publishApplicationChange("outbox_changed", clientMessageId, context.delegateId);
2324
+ return { message: projectOutboxMutation(updated) };
2325
+ }
2326
+ if (sameAttempt && ["delivery_state_unknown", "delivery_state_unconfirmed"].includes(updated.status)) {
2327
+ await publishApplicationChange("outbox_changed", clientMessageId, context.delegateId);
2328
+ }
2329
+ throw error;
2330
+ }
2331
+ const updated = await applicationOutbox.get(clientMessageId);
2332
+ if (!updated || updated.senderPrincipalId !== tuple.principalId
2333
+ || updated.senderDelegateId !== context.delegateId) {
2334
+ throw hostApplicationFailure("host_app_action_failed");
2335
+ }
2336
+ await publishApplicationChange("outbox_changed", clientMessageId, context.delegateId);
2337
+ return { message: projectOutboxMutation(updated) };
2338
+ }
2339
+
2340
+ async function applicationChangesSnapshot(context) {
2341
+ if (typeof applicationInbox?.getMessageCounts !== "function" || !sharedApplicationMessageStore) {
2342
+ throw hostApplicationFailure("host_app_unavailable");
2343
+ }
2344
+ const [counts, contacts] = await Promise.all([
2345
+ applicationInbox.getMessageCounts(localHistoryScope(context)),
2346
+ applicationContacts ? applicationContacts.list() : [],
2347
+ ]);
2348
+ return {
2349
+ cursor: applicationChangeCursor,
2350
+ inbox: counts.inbox,
2351
+ contacts: { total: contacts.length },
2352
+ outbox: { total: counts.outbox.total, retryEligible: counts.outbox.retryEligible },
2353
+ modelVisibility: "none",
2354
+ };
2355
+ }
2356
+
2357
+ async function applicationChangesSubscribe(context) {
2358
+ // Existing UI change polling also refreshes display metadata when due. This
2359
+ // never adds a snapshot or network wait to the cheap changes response.
2360
+ const metadataRead = workbenchRelationshipMetadataReadContext;
2361
+ if (metadataRead && nowMs(clock) >= workbenchRelationshipMetadataAttemptAt + WORKBENCH_RELATIONSHIP_METADATA_REFRESH_MS) {
2362
+ startWorkbenchRelationshipMetadataRefresh(metadataRead.createReadModel);
2363
+ }
2364
+ const afterCursor = context.request.afterCursor;
2365
+ const oldestCursor = applicationChanges[0]?.cursor ?? applicationChangeCursor;
2366
+ // The counter belongs to this Host process. A page can retain the previous
2367
+ // process's cursor across reconnects; this read needs a fresh snapshot, not
2368
+ // a write-conflict error. Existing command/revision checks are unaffected.
2369
+ const snapshotRequired = afterCursor > applicationChangeCursor
2370
+ || (applicationChanges.length > 0 && afterCursor < oldestCursor - 1);
2371
+ return {
2372
+ cursor: applicationChangeCursor,
2373
+ snapshotRequired,
2374
+ events: snapshotRequired
2375
+ ? []
2376
+ : applicationChanges
2377
+ .filter((event) => event.cursor > afterCursor)
2378
+ .filter((event) => event.audienceDelegateId == null || event.audienceDelegateId === context.delegateId)
2379
+ .map(projectApplicationChange),
2380
+ };
2381
+ }
2382
+
2383
+ async function applicationWorkbenchSnapshot(context) {
2384
+ const source = workbenchSource(context.adapterId);
2385
+ const resolved = await workbenchServiceFor(context, { initialRequest: context.request, source });
2386
+ return {
2387
+ snapshot: await resolved.service.readSnapshot(context.request, {
2388
+ source,
2389
+ authority: context,
2390
+ }),
2391
+ };
2392
+ }
2393
+
2394
+ async function applicationWorkbenchCommand(context) {
2395
+ const { service } = await workbenchServiceFor(context);
2396
+ return service.executeCommand(context.request, {
2397
+ source: workbenchSource(context.adapterId),
2398
+ authority: context,
2399
+ });
2400
+ }
2401
+
2402
+ async function workbenchServiceFor(context, initialization = {}) {
2403
+ const delegateId = safeId(context.delegateId, "neutral_host_workbench_delegate_invalid");
2404
+ let pending = workbenchServices.get(delegateId);
2405
+ if (pending) {
2406
+ const resolved = await pending;
2407
+ return { service: resolved.service };
2408
+ }
2409
+ const initialRequest = initialization.initialRequest || {};
2410
+ const initialSource = initialization.source || workbenchSource(context.adapterId);
2411
+ pending = (async () => {
2412
+ // Validate the real delegate projection before exposing any service
2413
+ // result, including a refusal of a foreign/retired command window.
2414
+ const initial = validateWorkbenchSnapshot(
2415
+ await buildApplicationWorkbenchSnapshot(context, initialRequest, initialSource),
2416
+ );
2417
+ let seedSnapshot = Object.hasOwn(initialization, "initialRequest") ? initial : null;
2418
+ const service = createHelloLynkWorkbenchService({
2419
+ hostGeneration,
2420
+ writerRegistrationKey: `${hostGeneration}:${delegateId}`,
2421
+ refreshBeforeCommand: true,
2422
+ revision: initial.revision,
2423
+ changeCursor: initial.changeCursor,
2424
+ runCommandOperation(_command, operation) {
2425
+ // Take the global queue only after this delegate's service turn, and
2426
+ // keep the fresh revision check and admission in the same turn.
2427
+ // The service detaches retry completion after admission, so its
2428
+ // network wait holds neither this queue nor the delegate's queue.
2429
+ return trackApplicationMutation(operation);
2430
+ },
2431
+ async readSnapshot(request, serviceContext) {
2432
+ const authority = requireWorkbenchAuthority(serviceContext.authority, delegateId);
2433
+ const useSeed = seedSnapshot && request === initialRequest && authority === context
2434
+ && Object.keys(initialSource).every((key) => serviceContext.source[key] === initialSource[key]);
2435
+ const snapshot = useSeed ? seedSnapshot : null;
2436
+ seedSnapshot = null;
2437
+ if (snapshot) return snapshot;
2438
+ return buildApplicationWorkbenchSnapshot(authority, request, serviceContext.source,
2439
+ { authoritativeMetadata: serviceContext.beforeCommand === true
2440
+ && WORKBENCH_RELATIONSHIP_METADATA_CAPABILITIES.has(WORKBENCH_COMMAND_CAPABILITIES[serviceContext.commandType]) });
2441
+ },
2442
+ async executeCommand(command, serviceContext) {
2443
+ const authority = requireWorkbenchAuthority(serviceContext.authority, delegateId);
2444
+ return executeAuthoritativeWorkbenchCommand(command, authority, serviceContext.source, serviceContext);
2445
+ },
2446
+ });
2447
+ return Object.freeze({ service });
2448
+ })();
2449
+ workbenchServices.set(delegateId, pending);
2450
+ try {
2451
+ const resolved = await pending;
2452
+ return {
2453
+ service: resolved.service,
2454
+ };
2455
+ } catch (error) {
2456
+ if (workbenchServices.get(delegateId) === pending) workbenchServices.delete(delegateId);
2457
+ throw error;
2458
+ }
2459
+ }
2460
+
2461
+ function requireWorkbenchAuthority(value, delegateId) {
2462
+ if (!value || value.delegateId !== delegateId || value.hostGeneration !== hostGeneration) {
2463
+ throw hostApplicationFailure("host_app_authorization_required");
2464
+ }
2465
+ return value;
2466
+ }
2467
+
2468
+ async function assertWorkbenchCommandCapabilityAvailable(command) {
2469
+ const requiredCapability = WORKBENCH_COMMAND_CAPABILITIES[command?.command];
2470
+ if (!requiredCapability || !workbenchCapabilities.includes(requiredCapability)) {
2471
+ throw hostApplicationFailure("host_app_capability_unavailable");
2472
+ }
2473
+ await assertWorkbenchLegacyLifecycleCapabilityAvailable(requiredCapability);
2474
+ }
2475
+
2476
+ async function assertWorkbenchLegacyLifecycleCapabilityAvailable(requiredCapability) {
2477
+ if (!WORKBENCH_LIFECYCLE_DEPENDENT_CAPABILITIES.has(requiredCapability)) return;
2478
+ if (!hasRemoteWorkbenchLifecycle()) {
2479
+ throw hostApplicationFailure("host_app_capability_unavailable");
2480
+ }
2481
+ try {
2482
+ await readRemoteWorkbenchLifecyclePages();
2483
+ } catch {
2484
+ // A lifecycle-dependent mutation must never rely on a stale successful
2485
+ // snapshot. Any probe failure closes both grant issuance and execution.
2486
+ throw hostApplicationFailure("host_app_capability_unavailable");
2487
+ }
2488
+ }
2489
+
2490
+ async function executeAuthoritativeWorkbenchCommand(command, context, source, serviceContext) {
2491
+ try {
2492
+ await assertWorkbenchCommandCapabilityAvailable(command);
2493
+ let publicResult = {};
2494
+ if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal) {
2495
+ if (!options.humanRelationships || typeof options.humanRelationships.search !== "function") {
2496
+ throw hostApplicationFailure("host_app_capability_unavailable");
2497
+ }
2498
+ const found = await options.humanRelationships.search(command.payload);
2499
+ publicResult = {
2500
+ kind: "principal-search",
2501
+ mode: command.payload.mode,
2502
+ ...found,
2503
+ };
2504
+ bindWorkbenchRelationshipTargets(found.results, context);
2505
+ // Directory search does not change the local snapshot. Return its
2506
+ // authority-bound results without making unrelated cloud lists a
2507
+ // second condition for a successful read.
2508
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2509
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest) {
2510
+ if (!options.humanRelationships || typeof options.humanRelationships.send !== "function") {
2511
+ throw hostApplicationFailure("host_app_capability_unavailable");
2512
+ }
2513
+ requireWorkbenchRelationshipTarget(command.payload.targetRef, context);
2514
+ publicResult = {
2515
+ kind: "contact-request-send",
2516
+ ...(await options.humanRelationships.send(command.payload)),
2517
+ };
2518
+ // The controller result is the authoritative submission outcome. Do not
2519
+ // let an unrelated relationship-list refresh replace it after the cloud
2520
+ // has already admitted or frozen this exact request attempt.
2521
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2522
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend) {
2523
+ publicResult = projectHumanSendStatus(
2524
+ await nativeHumanSendRequest({ ...context, request: command.payload }),
2525
+ );
2526
+ // The broker has admitted this exact saved peer and returns its action
2527
+ // state, not a delivery acknowledgement. An unrelated cloud refresh
2528
+ // must not hide that action after its confirmation has already started.
2529
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2530
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus) {
2531
+ publicResult = projectHumanSendStatus(
2532
+ await nativeHumanSendRequestStatus({ ...context, request: command.payload }),
2533
+ );
2534
+ // The broker is the authority for this action. Do not make an unrelated
2535
+ // cloud/relationship snapshot a second condition for observing it.
2536
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2537
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.markRead) {
2538
+ await applicationInboxMarkRead({ ...context, request: command.payload });
2539
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.markAllRead) {
2540
+ publicResult = await applicationInboxMarkAllRead({ ...context, request: command.payload });
2541
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2542
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.setAutoRead) {
2543
+ publicResult = await applicationConversationAutoRead({ ...context, request: command.payload });
2544
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2545
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteRead) {
2546
+ await applicationInboxDeleteRead({ ...context, request: command.payload });
2547
+ } else if ([HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent, HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread,
2548
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread, HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread,
2549
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread, HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads,
2550
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.configureHistory].includes(command.command)) {
2551
+ publicResult = await applicationLocalHistory(command, context);
2552
+ // Durable local completion is the acknowledgement. A later independent
2553
+ // snapshot refresh must not turn the completed operation into unknown.
2554
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2555
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply) {
2556
+ publicResult = await applicationResendDeletedReply({ ...context, request: command.payload });
2557
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2558
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestRetry) {
2559
+ if (context.workbenchRetryAuthorized !== true) {
2560
+ throw hostApplicationFailure("host_app_outbox_retry_grant_invalid");
2561
+ }
2562
+ await applicationOutboxRetry({ ...context, request: command.payload });
2563
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.saveContact) {
2564
+ // This is deliberately the same Host-owned path as the application API:
2565
+ // the workbench never writes the local contact file on its own.
2566
+ await applicationContactsUpsert({ ...context, request: command.payload });
2567
+ // The durable local write is the acknowledgement boundary. An unrelated
2568
+ // cloud-list refresh must not turn a committed edit into an unknown
2569
+ // outcome. Keep the checked command baseline; the contacts_changed event
2570
+ // and the UI's separate read refresh the snapshot before the next edit.
2571
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2572
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact) {
2573
+ await renameContact({ ...context, request: command.payload });
2574
+ return workbenchSuccess(command.requestId, serviceContext);
2575
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances) {
2576
+ publicResult = await readContactInstances({ ...context, request: command.payload });
2577
+ // Directory reads do not change the snapshot. Return the checked
2578
+ // result without a second, unrelated cloud metadata refresh.
2579
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2580
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.saveContactInstance) {
2581
+ await saveContactInstance({ ...context, request: command.payload });
2582
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance) {
2583
+ await renameContactInstance({ ...context, request: command.payload });
2584
+ return workbenchSuccess(command.requestId, serviceContext);
2585
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContactInstance) {
2586
+ await deleteContactInstance({ ...context, request: command.payload });
2587
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContact) {
2588
+ // Deletion removes the local relationship only. Any later autonomous
2589
+ // action still re-resolves the contact and therefore fails closed.
2590
+ await applicationContactsRemove({ ...context, request: command.payload });
2591
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.setContactAuthorization) {
2592
+ await applicationContactAutonomousSendAuthorization({ ...context, request: command.payload });
2593
+ } else if (command.command === WORKBENCH_CLEAR_RESOLVED_CONTACT_REQUESTS_COMMAND) {
2594
+ publicResult = {
2595
+ kind: "contact-request-clear-resolved",
2596
+ ...(await applicationContactRequestsClearResolved()),
2597
+ };
2598
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.recoverContactRequestDecision) {
2599
+ await applicationContactRequestRecover({ ...context, request: command.payload });
2600
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2601
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.respondContactRequest) {
2602
+ await applicationContactRequestRespond({ ...context, request: command.payload });
2603
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2604
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.cancelContactRequest) {
2605
+ await applicationContactRequestCancel({ ...context, request: command.payload });
2606
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2607
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.endRelationship) {
2608
+ await applicationRelationshipEnd({ ...context, request: command.payload });
2609
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2610
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.removeBlock) {
2611
+ await applicationBlockRemove({ ...context, request: command.payload });
2612
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2613
+ } else if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.openAccount) {
2614
+ await applicationAccountOpen({ ...context, request: command.payload });
2615
+ } else {
2616
+ throw hostApplicationFailure("host_app_workbench_command_not_enabled");
2617
+ }
2618
+ if (WORKBENCH_LOCAL_ONLY_CAPABILITIES.has(WORKBENCH_COMMAND_CAPABILITIES[command.command])) {
2619
+ return workbenchSuccess(command.requestId, serviceContext, publicResult);
2620
+ }
2621
+ const snapshot = await buildApplicationWorkbenchSnapshot(context, {}, source, {
2622
+ authoritativeMetadata: WORKBENCH_RELATIONSHIP_METADATA_CAPABILITIES.has(WORKBENCH_COMMAND_CAPABILITIES[command.command]),
2623
+ });
2624
+ return workbenchSuccess(command.requestId, snapshot, publicResult);
2625
+ } catch (error) {
2626
+ if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal) {
2627
+ const code = String(error?.code || "");
2628
+ if (code === "directory_search_rate_limited" && error.httpStatus === 429) {
2629
+ return workbenchFailure(command.requestId, code, serviceContext, "known-failed");
2630
+ }
2631
+ const safeCode = /^(?:host_app|host_transport|human_relationship)_[a-z0-9_]{1,100}$/.test(code)
2632
+ ? code : "host_app_principal_search_failed";
2633
+ // A timed-out search may already have consumed its cloud quota. Keep
2634
+ // that uncertainty and never replay it, but retain the useful code.
2635
+ const refusedBeforeSearch = safeCode.startsWith("host_app_") && safeCode !== "host_app_principal_search_failed";
2636
+ return workbenchFailure(command.requestId, safeCode, refusedBeforeSearch ? serviceContext : null,
2637
+ refusedBeforeSearch ? "known-failed" : "unknown");
2638
+ }
2639
+ // A contact-request refusal is already authoritative. Refreshing unrelated
2640
+ // lists here can hide that known failure behind the browser's timeout.
2641
+ if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest
2642
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend
2643
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply
2644
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus
2645
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.recoverContactRequestDecision
2646
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.respondContactRequest
2647
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.cancelContactRequest
2648
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.endRelationship
2649
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.removeBlock
2650
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances) {
2651
+ const knownCode = knownWorkbenchCommandFailure(error);
2652
+ return workbenchFailure(
2653
+ command.requestId,
2654
+ knownCode || "host_app_workbench_outcome_unknown",
2655
+ knownCode ? serviceContext : null,
2656
+ knownCode ? "known-failed" : "unknown",
2657
+ );
2658
+ }
2659
+ const knownCode = knownWorkbenchCommandFailure(error);
2660
+ if (WORKBENCH_LOCAL_ONLY_CAPABILITIES.has(WORKBENCH_COMMAND_CAPABILITIES[command.command])) {
2661
+ // The local CAS already determined whether this edit was rejected.
2662
+ // Cloud metadata cannot improve that answer or acknowledge a failed write.
2663
+ return workbenchFailure(command.requestId, knownCode || "host_app_workbench_outcome_unknown",
2664
+ knownCode ? serviceContext : null, knownCode ? "known-failed" : "unknown");
2665
+ }
2666
+ if (knownCode) {
2667
+ const snapshot = await buildApplicationWorkbenchSnapshot(context, {}, source, {
2668
+ authoritativeMetadata: WORKBENCH_RELATIONSHIP_METADATA_CAPABILITIES.has(WORKBENCH_COMMAND_CAPABILITIES[command.command]),
2669
+ }).catch(() => null);
2670
+ return workbenchFailure(command.requestId, knownCode, snapshot, "known-failed");
2671
+ }
2672
+ return workbenchFailure(command.requestId, "host_app_workbench_outcome_unknown", null, "unknown");
2673
+ }
2674
+ }
2675
+
2676
+ function bindWorkbenchRelationshipTargets(results, context) {
2677
+ const now = nowMs(clock);
2678
+ for (const [targetRef, binding] of workbenchRelationshipTargets) {
2679
+ if (binding.expiresAtMs <= now) workbenchRelationshipTargets.delete(targetRef);
2680
+ }
2681
+ for (const result of results) {
2682
+ const expiresAtMs = Date.parse(result.expiresAt);
2683
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now) {
2684
+ throw hostApplicationFailure("host_app_contact_target_expired");
2685
+ }
2686
+ workbenchRelationshipTargets.set(result.targetRef, Object.freeze({
2687
+ delegateId: context.delegateId,
2688
+ capabilityId: context.capabilityId,
2689
+ adapterInstanceId: context.adapterInstanceId,
2690
+ hostGeneration,
2691
+ expiresAtMs,
2692
+ }));
2693
+ }
2694
+ while (workbenchRelationshipTargets.size > 64) {
2695
+ workbenchRelationshipTargets.delete(workbenchRelationshipTargets.keys().next().value);
2696
+ }
2697
+ }
2698
+
2699
+ function requireWorkbenchRelationshipTarget(targetRef, context) {
2700
+ const binding = workbenchRelationshipTargets.get(targetRef);
2701
+ if (binding?.expiresAtMs <= nowMs(clock)) workbenchRelationshipTargets.delete(targetRef);
2702
+ if (!binding || binding.expiresAtMs <= nowMs(clock)
2703
+ || binding.delegateId !== context.delegateId
2704
+ || binding.capabilityId !== context.capabilityId
2705
+ || binding.adapterInstanceId !== context.adapterInstanceId
2706
+ || binding.hostGeneration !== hostGeneration) {
2707
+ throw hostApplicationFailure("host_app_contact_target_expired");
2708
+ }
2709
+ }
2710
+
2711
+ async function buildApplicationWorkbenchSnapshot(context, request, source, behavior = {}) {
2712
+ requireApplicationStore(applicationInbox, "neutral_host_app_inbox_unavailable");
2713
+ requireApplicationStore(applicationContacts, "neutral_host_app_contacts_unavailable");
2714
+ requireApplicationStore(applicationContactRequests, "neutral_host_app_contact_requests_unavailable");
2715
+ requireApplicationStore(applicationOutbox, "neutral_host_app_outbox_unavailable");
2716
+ const endpoint = endpoints.get(context.delegateId);
2717
+ if (!endpoint?.hellolynkId || !Number.isSafeInteger(endpoint.authorizationVersion)) {
2718
+ throw hostApplicationFailure("host_app_workbench_subject_unavailable");
2719
+ }
2720
+ const remoteMetadata = hasRemoteWorkbenchLifecycle();
2721
+ let observedMetadata = null;
2722
+ const createReadModel = ({ capabilities, legacyLifecycleFallback = false, localOnly = false,
2723
+ metadata, metadataState }) => (
2724
+ createNeutralHostWorkbenchReadModel({
2725
+ subject: {
2726
+ region: tuple.region,
2727
+ principalId: tuple.principalId,
2728
+ principalLabel: endpoint.principalLabel ?? null,
2729
+ hellolynkId: endpoint.hellolynkId,
2730
+ delegateId: endpoint.delegateId,
2731
+ delegateLabel: endpoint.delegateLabel ?? null,
2732
+ hostInstallationId: tuple.hostInstallationId,
2733
+ authorizationVersion: endpoint.authorizationVersion,
2734
+ },
2735
+ hostGeneration,
2736
+ applicationChangeCursor: String(applicationChangeCursor),
2737
+ messageStore: applicationInbox,
2738
+ contactStore: applicationContacts,
2739
+ contactRequestStore: {
2740
+ async listForHuman(input) {
2741
+ if (metadata) return projectLocalWorkbenchRelationshipMetadata(metadata);
2742
+ if (localOnly) return { requests: [], nextCursor: null, relationships: [], blocks: [] };
2743
+ observedMetadata = await applicationWorkbenchRelationshipState(input, { legacyLifecycleFallback });
2744
+ return observedMetadata;
2745
+ },
2746
+ },
2747
+ localDelegateLabels: [...endpoints.values()].map((item) => ({
2748
+ principalId: item.principalId,
2749
+ delegateId: item.delegateId,
2750
+ delegateLabel: item.delegateLabel ?? null,
2751
+ })),
2752
+ capabilities,
2753
+ ...(metadataState ? { relationshipMetadataState: metadataState } : {}),
2754
+ hostStatus: !localOnly && lifecycle === "running" ? "online" : "offline",
2755
+ connectedClients: core.connectedClients(),
2756
+ changeEventTypes: [...new Set(applicationChanges.map((item) => item.type))].sort(),
2757
+ snapshotRequired: false,
2758
+ now: () => new Date(hostStartedAt),
2759
+ })
2760
+ );
2761
+ if (remoteMetadata) workbenchRelationshipMetadataReadContext = { createReadModel };
2762
+ if (remoteMetadata && behavior.authoritativeMetadata !== true && !Object.hasOwn(request, "contactRequestCursor")) {
2763
+ const now = nowMs(clock);
2764
+ // Never put the network task on the service's serialized read/write tail.
2765
+ // Capture this projection before starting it so a cold read is coherent.
2766
+ const metadataState = workbenchRelationshipMetadataState;
2767
+ const metadata = workbenchRelationshipMetadata || { requests: [], nextCursor: null, relationships: [], blocks: [] };
2768
+ if (now >= workbenchRelationshipMetadataAttemptAt + WORKBENCH_RELATIONSHIP_METADATA_REFRESH_MS) {
2769
+ startWorkbenchRelationshipMetadataRefresh(createReadModel);
2770
+ }
2771
+ return createReadModel({ metadata, metadataState,
2772
+ capabilities: metadataState === "fresh" ? workbenchCapabilities
2773
+ : workbenchCapabilities.filter((capability) => !WORKBENCH_RELATIONSHIP_METADATA_CAPABILITIES.has(capability)
2774
+ // Starting a new relationship does not depend on the cached list
2775
+ // of existing relationships. Keep the discovery form available;
2776
+ // request submission still performs its authoritative preflight.
2777
+ || capability === "principal.search" || capability === "contact-request.send"),
2778
+ }).readSnapshot(request, source);
2779
+ }
2780
+ // A newer authoritative read supersedes any older background projection,
2781
+ // including when only the server observed a relationship change.
2782
+ const metadataEpoch = remoteMetadata ? ++workbenchRelationshipMetadataEpoch : workbenchRelationshipMetadataEpoch;
2783
+ try {
2784
+ const snapshot = await createReadModel({ capabilities: workbenchCapabilities,
2785
+ ...(remoteMetadata ? { metadataState: "fresh" } : {}) })
2786
+ .readSnapshot(request, source);
2787
+ if (remoteMetadata && request.contactRequestCursor == null && observedMetadata) {
2788
+ await commitWorkbenchRelationshipMetadata(observedMetadata, metadataEpoch, false);
2789
+ }
2790
+ return { ...snapshot };
2791
+ } catch (error) {
2792
+ if (remoteMetadata && metadataEpoch === workbenchRelationshipMetadataEpoch) {
2793
+ workbenchRelationshipMetadataState = "stale";
2794
+ }
2795
+ if (isTransientWorkbenchCloudFailure(error)
2796
+ || error?.code === "host_transport_account_authorization_required") {
2797
+ // Local history does not need a successful cloud metadata read. Do not
2798
+ // present empty remote lists as authoritative. Local records and local
2799
+ // send settings remain usable without a successful remote metadata read.
2800
+ return { ...(await createReadModel({
2801
+ capabilities: workbenchCapabilities.filter((capability) => WORKBENCH_LOCAL_ONLY_CAPABILITIES.has(capability)),
2802
+ localOnly: true,
2803
+ }).readSnapshot(request, source)) };
2804
+ }
2805
+ if (error?.code !== WORKBENCH_LEGACY_LIFECYCLE_ROUTES_UNAVAILABLE) throw error;
2806
+ const degradedCapabilities = workbenchCapabilities.filter(
2807
+ (capability) => !WORKBENCH_LIFECYCLE_DEPENDENT_CAPABILITIES.has(capability),
2808
+ );
2809
+ return {
2810
+ ...(await createReadModel({
2811
+ capabilities: degradedCapabilities,
2812
+ legacyLifecycleFallback: true,
2813
+ }).readSnapshot(request, source)),
2814
+ };
2815
+ }
2816
+ }
2817
+
2818
+ async function commitWorkbenchRelationshipMetadata(metadata, epoch, notify = true) {
2819
+ if (lifecycle !== "running" || epoch !== workbenchRelationshipMetadataEpoch) return;
2820
+ // Keep only validated relationship display metadata. Local intro text,
2821
+ // receipts and decision state are reread from the protected store below.
2822
+ metadata = workbenchRelationshipMetadataProjection(metadata);
2823
+ const changed = workbenchRelationshipMetadataState !== "fresh"
2824
+ || JSON.stringify(metadata) !== JSON.stringify(workbenchRelationshipMetadata);
2825
+ workbenchRelationshipMetadata = structuredClone(metadata);
2826
+ workbenchRelationshipMetadataState = "fresh";
2827
+ workbenchRelationshipMetadataAttemptAt = nowMs(clock);
2828
+ if (changed && notify) await publishApplicationChange("contacts_changed", "relationship-metadata");
2829
+ }
2830
+
2831
+ function workbenchRelationshipMetadataProjection(metadata) {
2832
+ return { ...metadata, requests: metadata.requests.map((request) => ({
2833
+ requestId: request.serverContactRequestId ?? request.requestId ?? request.localRecordId,
2834
+ direction: request.direction ?? "incoming", status: request.state ?? request.status,
2835
+ otherPrincipal: request.otherPrincipal ?? {
2836
+ displayName: request.sender?.verifiedDisplayName ?? null, hellolynkId: request.sender?.hellolynkId,
2837
+ },
2838
+ createdAt: request.receivedAt ?? request.createdAt,
2839
+ humanOnly: true, modelVisibility: "none",
2840
+ })) };
2841
+ }
2842
+
2843
+ async function projectLocalWorkbenchRelationshipMetadata(metadata) {
2844
+ const local = await applicationContactRequests.listForHuman();
2845
+ const byId = new Map(local.map((request) => [request.serverContactRequestId ?? request.localRecordId, request]));
2846
+ const requests = metadata.requests.map((remote) => {
2847
+ const current = remote.direction === "incoming" ? byId.get(remote.requestId) : null;
2848
+ byId.delete(remote.requestId);
2849
+ if (!current) return { ...remote, actionReady: false, decisionOutcome: null, decisionAction: null };
2850
+ const state = current.state === "pending" && current.decisionOutcome != null
2851
+ ? "pending" : remote.status === "pending" ? current.state : remote.status;
2852
+ const consistent = state === current.state;
2853
+ return { ...remote, ...current, state,
2854
+ actionReady: consistent && current.actionReady === true,
2855
+ decisionOutcome: consistent ? current.decisionOutcome : null,
2856
+ decisionAction: consistent ? current.decisionAction : null,
2857
+ };
2858
+ });
2859
+ return { ...structuredClone(metadata), requests: [...requests, ...byId.values()] };
2860
+ }
2861
+
2862
+ function startWorkbenchRelationshipMetadataRefresh(createReadModel) {
2863
+ if (lifecycle !== "running" || workbenchRelationshipMetadataRefresh) return;
2864
+ const epoch = workbenchRelationshipMetadataEpoch;
2865
+ workbenchRelationshipMetadataAttemptAt = nowMs(clock);
2866
+ const task = Promise.resolve().then(async () => {
2867
+ try {
2868
+ // Refreshing unchanged display data is not a business change. Keep the
2869
+ // last successful projection while this read is pending; real failures
2870
+ // and relationship events still mark it stale. Commands keep their
2871
+ // authoritative preflight and never rely on this display cache.
2872
+ const metadata = await applicationWorkbenchRelationshipState();
2873
+ if (lifecycle !== "running" || epoch !== workbenchRelationshipMetadataEpoch) return;
2874
+ // Validate the same relationship projection used by snapshots. Building
2875
+ // a complete local snapshot here would reread unrelated chat history.
2876
+ createReadModel({ capabilities: workbenchCapabilities, metadataState: "fresh" })
2877
+ .validateRelationshipMetadata(metadata);
2878
+ await commitWorkbenchRelationshipMetadata(metadata, epoch);
2879
+ } catch {
2880
+ if (lifecycle !== "running" || epoch !== workbenchRelationshipMetadataEpoch) return;
2881
+ const changed = workbenchRelationshipMetadataState !== "stale";
2882
+ workbenchRelationshipMetadataState = "stale";
2883
+ if (changed) await publishApplicationChange("contacts_changed", "relationship-metadata");
2884
+ }
2885
+ }).finally(() => {
2886
+ if (workbenchRelationshipMetadataRefresh === task) workbenchRelationshipMetadataRefresh = null;
2887
+ });
2888
+ workbenchRelationshipMetadataRefresh = task;
2889
+ trackOperation(task, pendingOperations);
2890
+ }
2891
+
2892
+ async function applicationWorkbenchRelationshipState(input = {}, behavior = {}) {
2893
+ const cursor = input?.cursor ?? null;
2894
+ const hasRemoteLifecycle = hasRemoteWorkbenchLifecycle();
2895
+ if (hasRemoteLifecycle) {
2896
+ if (behavior.legacyLifecycleFallback === true) {
2897
+ const contactRequestPage = await options.humanRelationships.list(
2898
+ cursor == null ? {} : { cursor },
2899
+ );
2900
+ return {
2901
+ requests: contactRequestPage.requests,
2902
+ nextCursor: contactRequestPage.nextCursor,
2903
+ relationships: [],
2904
+ blocks: [],
2905
+ };
2906
+ }
2907
+ // The lifecycle lists are authoritative as one read surface. Never
2908
+ // turn a remote failure into an incomplete page or empty arrays while
2909
+ // advertising the corresponding read and mutation capabilities.
2910
+ // These snapshot reads are independent. Starting all three together
2911
+ // also shares the transport's in-flight protected session resolution.
2912
+ // Settle every read before choosing an error; a failed page must never
2913
+ // be exposed as an authoritative empty list.
2914
+ const [lifecycleResult, contactRequestResult] = await Promise.allSettled([
2915
+ readRemoteWorkbenchLifecyclePages(),
2916
+ Promise.resolve().then(() => options.humanRelationships.list(cursor == null ? {} : { cursor })),
2917
+ ]);
2918
+ if (contactRequestResult.status === "rejected"
2919
+ && !isTransientWorkbenchCloudFailure(contactRequestResult.reason)
2920
+ && contactRequestResult.reason?.code !== "host_transport_account_authorization_required") {
2921
+ throw contactRequestResult.reason;
2922
+ }
2923
+ if (lifecycleResult.status === "rejected") throw lifecycleResult.reason;
2924
+ if (contactRequestResult.status === "rejected") throw contactRequestResult.reason;
2925
+ const { relationshipPage, blockPage } = lifecycleResult.value;
2926
+ const contactRequestPage = contactRequestResult.value;
2927
+ return {
2928
+ requests: contactRequestPage.requests,
2929
+ nextCursor: contactRequestPage.nextCursor,
2930
+ relationships: relationshipPage.relationships,
2931
+ blocks: blockPage.blocks,
2932
+ };
2933
+ } else if (cursor != null) {
2934
+ throw hostApplicationFailure("host_app_capability_unavailable");
2935
+ }
2936
+ return {
2937
+ requests: await applicationContactRequests.listForHuman(),
2938
+ nextCursor: null,
2939
+ relationships: [],
2940
+ blocks: [],
2941
+ };
2942
+ }
2943
+
2944
+ function hasRemoteWorkbenchLifecycle() {
2945
+ return Boolean(options.humanRelationships
2946
+ && typeof options.humanRelationships.list === "function"
2947
+ && typeof options.humanRelationships.listRelationships === "function"
2948
+ && typeof options.humanRelationships.listBlocks === "function");
2949
+ }
2950
+
2951
+ async function readRemoteWorkbenchLifecyclePages() {
2952
+ const [relationshipResult, blockResult] = await Promise.allSettled([
2953
+ options.humanRelationships.listRelationships(),
2954
+ options.humanRelationships.listBlocks(),
2955
+ ]);
2956
+ if (relationshipResult.status === "rejected" || blockResult.status === "rejected") {
2957
+ if (relationshipResult.status === "rejected"
2958
+ && blockResult.status === "rejected"
2959
+ && isMissingLegacyLifecycleRoute(relationshipResult.reason)
2960
+ && isMissingLegacyLifecycleRoute(blockResult.reason)) {
2961
+ throw failure(WORKBENCH_LEGACY_LIFECYCLE_ROUTES_UNAVAILABLE);
2962
+ }
2963
+ const permanentFailure = [relationshipResult, blockResult].find(
2964
+ (result) => result.status === "rejected"
2965
+ && !isMissingLegacyLifecycleRoute(result.reason)
2966
+ && !isTransientWorkbenchCloudFailure(result.reason)
2967
+ && result.reason?.code !== "host_transport_account_authorization_required",
2968
+ );
2969
+ const nonMissingRouteFailure = [relationshipResult, blockResult].find(
2970
+ (result) => result.status === "rejected" && !isMissingLegacyLifecycleRoute(result.reason),
2971
+ );
2972
+ const firstFailure = permanentFailure || nonMissingRouteFailure
2973
+ || [relationshipResult, blockResult].find((result) => result.status === "rejected");
2974
+ throw firstFailure.reason;
2975
+ }
2976
+ return {
2977
+ relationshipPage: relationshipResult.value,
2978
+ blockPage: blockResult.value,
2979
+ };
2980
+ }
2981
+
2982
+ async function publishChange(change, entityId, audienceDelegateId = null) {
2983
+ assertNoSensitiveEventFields(change);
2984
+ await changeSink(change);
2985
+ await publishApplicationChange(change.type, entityId, audienceDelegateId);
2986
+ }
2987
+
2988
+ async function publishApplicationChange(type, entityId, audienceDelegateId = null) {
2989
+ if (type === "contacts_changed" && entityId === "relationship-metadata") contactRequestNoticeRead = null;
2990
+ if (["contact_requests_changed", "relationships_changed", "blocks_changed"].includes(type)) {
2991
+ contactRequestNoticeRead = null;
2992
+ workbenchRelationshipMetadataEpoch += 1;
2993
+ workbenchRelationshipMetadataAttemptAt = -Infinity;
2994
+ workbenchRelationshipMetadataState = workbenchRelationshipMetadata ? "stale" : "loading";
2995
+ }
2996
+ const event = Object.freeze({
2997
+ cursor: ++applicationChangeCursor,
2998
+ type: safeId(type, "neutral_host_change_type_invalid"),
2999
+ entityId: safeApplicationId(entityId, "neutral_host_change_entity_invalid"),
3000
+ changedAt: new Date(nowMs(clock)).toISOString(),
3001
+ modelVisibility: "none",
3002
+ audienceDelegateId,
3003
+ });
3004
+ assertNoSensitiveEventFields(event);
3005
+ applicationChanges.push(event);
3006
+ if (applicationChanges.length > APPLICATION_CHANGE_CAPACITY) applicationChanges.shift();
3007
+ }
3008
+
3009
+ async function beginDrain(reason = "stop_requested") {
3010
+ if (stopPromise) return stopPromise;
3011
+ lifecycle = "draining";
3012
+ workbenchRelationshipMetadataEpoch += 1;
3013
+ workbenchRelationshipMetadata = null;
3014
+ workbenchRelationshipMetadataReadContext = null;
3015
+ workbenchRelationshipMetadataState = "loading";
3016
+ stopPromise = (async () => {
3017
+ if (maintenanceTimer) clearInterval(maintenanceTimer);
3018
+ maintenanceTimer = null;
3019
+ preKeyRefillAbortController.abort();
3020
+ relayAbortController.abort();
3021
+ // A confirmation-window cleanup failure must not retain the Host's
3022
+ // transport, listener or installation lock after a requested stop.
3023
+ try { await nativeBroker?.close(); } catch {}
3024
+ // Fence new callbacks before waiting. Anything already in pendingOperations
3025
+ // crossed the running gate and is allowed to finish its persist/ACK unit.
3026
+ await stream?.close?.().catch(() => {});
3027
+ const drainTimeoutMs = integerInRange(options.drainTimeoutMs, 5_000, 100, 60_000);
3028
+ // Give admitted persist/ACK units a bounded grace period. After that,
3029
+ // closing the transport cancels slow cloud work; the second wait still
3030
+ // protects all admitted local writes before releasing ownership.
3031
+ // Close belongs here, not only in assembly.stop: idle/revocation also use
3032
+ // this path, and the installation lock is still owned at this point.
3033
+ try { await waitForOperations(pendingOperations, drainTimeoutMs); }
3034
+ catch (error) {
3035
+ if (error?.code !== "neutral_host_drain_timeout") throw error;
3036
+ }
3037
+ finally {
3038
+ const transportDrain = options.transport.close?.();
3039
+ if (transportDrain) trackOperation(transportDrain, pendingOperations);
3040
+ }
3041
+ await waitForOperations(pendingOperations, drainTimeoutMs);
3042
+ if (server) await closeServer(server);
3043
+ await receiveTask?.catch(() => {});
3044
+ if (ownsDiscovery) await removeOwnedDiscovery(layout.discoveryPath, hostGeneration);
3045
+ // The same generation check clears a handover request addressed to this Host.
3046
+ await removeOwnedDiscovery(layout.handoverPath, hostGeneration);
3047
+ if (handoverRequested) await restampExpectedRelease(layout, releaseOrdinal).catch(() => {});
3048
+ await lockHandle.release();
3049
+ lifecycle = "stopped";
3050
+ return { ok: true, reason, status: lifecycle };
3051
+ })().catch(async (error) => {
3052
+ failureCode = safeCode(error, "neutral_host_stop_failed");
3053
+ // A failed drain is terminal. Keep the lock while this process is alive,
3054
+ // close its listener, and let the owning release entry exit with failure.
3055
+ // Never leave a live, unusable Host indefinitely advertising "draining".
3056
+ try { options.transport.close?.()?.catch(() => {}); } catch {}
3057
+ if (server) {
3058
+ const closing = closeServer(server);
3059
+ server.closeAllConnections?.();
3060
+ await closing.catch(() => {});
3061
+ }
3062
+ lifecycle = "failed";
3063
+ throw error;
3064
+ });
3065
+ return stopPromise;
3066
+ }
3067
+
3068
+ async function cleanupAfterFailedStart() {
3069
+ if (maintenanceTimer) clearInterval(maintenanceTimer);
3070
+ preKeyRefillAbortController.abort();
3071
+ relayAbortController.abort();
3072
+ await nativeBroker?.close().catch(() => {});
3073
+ await stream?.close?.().catch(() => {});
3074
+ try {
3075
+ if (options.transport.close) {
3076
+ trackOperation(options.transport.close(), pendingOperations);
3077
+ await waitForOperations(pendingOperations, integerInRange(options.drainTimeoutMs, 5_000, 100, 60_000));
3078
+ }
3079
+ } finally {
3080
+ // A failed local drain must retain ownership, but not an unusable listener.
3081
+ if (server) await closeServer(server).catch(() => {});
3082
+ }
3083
+ await lockHandle.release().catch(() => {});
3084
+ }
3085
+
3086
+ return Object.freeze({
3087
+ contractId: HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT,
3088
+ tuple,
3089
+ discovery,
3090
+ core,
3091
+ stop: beginDrain,
3092
+ runMaintenance,
3093
+ get status() { return lifecycle; },
3094
+ get failureCode() { return failureCode; },
3095
+ get modelCalls() { return modelCalls; },
3096
+ get pendingOperationCount() { return pendingOperations.size; },
3097
+ });
3098
+
3099
+ function wrapperHealth() {
3100
+ return {
3101
+ ok: lifecycle === "running",
3102
+ contractId: HELLOLYNK_NEUTRAL_HOST_PROCESS_CONTRACT,
3103
+ hostGeneration,
3104
+ status: lifecycle,
3105
+ releaseOrdinal,
3106
+ receive: receiveHealth(),
3107
+ };
3108
+ }
3109
+
3110
+ function runningHealth(health = core.publicHealth()) {
3111
+ return {
3112
+ ...health,
3113
+ releaseOrdinal,
3114
+ receive: receiveHealth(),
3115
+ };
3116
+ }
3117
+
3118
+ function receiveHealth() {
3119
+ const preKeyInventory = preKeyInventoryHealth();
3120
+ return {
3121
+ status: inboundPaused
3122
+ ? "paused"
3123
+ : preKeyInventory.status === "ready" && relayState.status === "ready" && !lastRejectedInbound ? "ready" : "degraded",
3124
+ ...(lastRejectedInbound ? { lastRejectedInbound } : {}),
3125
+ preKeyInventory,
3126
+ relay: {
3127
+ status: relayState.status,
3128
+ ...(relayState.errorCode ? { errorCode: relayState.errorCode } : {}),
3129
+ ...(relayState.status === "retrying" ? { retryAt: new Date(relayState.nextAttemptAtMs).toISOString() } : {}),
3130
+ },
3131
+ };
3132
+ }
3133
+ }
3134
+
3135
+ export async function discoverHelloLynkNeutralHost(options = {}) {
3136
+ const tuple = normalizeTuple(options);
3137
+ const runtimeRoot = path.resolve(String(options.runtimeRoot || ""));
3138
+ if (!options.runtimeRoot) throw failure("neutral_host_runtime_root_required");
3139
+ const layout = discoveryLayout(runtimeRoot, tuple.hostInstallationId);
3140
+ await assertNoReparseAncestors(runtimeRoot);
3141
+ await assertSafePath(runtimeRoot, { mustExist: true, directory: true });
3142
+ await assertSafePath(layout.directory, { mustExist: true, directory: true });
3143
+ await assertSafePath(layout.discoveryPath, { mustExist: true, directory: false });
3144
+ let record;
3145
+ try {
3146
+ record = JSON.parse(await fs.readFile(layout.discoveryPath, "utf8"));
3147
+ } catch (error) {
3148
+ if (error?.code === "ENOENT") return null;
3149
+ throw failure("neutral_host_discovery_invalid", error);
3150
+ }
3151
+ validateDiscovery(record);
3152
+ if (record.tupleDigest !== digestTuple(tuple)
3153
+ || record.region !== tuple.region
3154
+ || record.principalId !== tuple.principalId
3155
+ || record.hostInstallationId !== tuple.hostInstallationId) {
3156
+ throw failure("neutral_host_discovery_owner_mismatch");
3157
+ }
3158
+ const health = await fetch(`http://${record.host}:${record.port}${HELLOLYNK_HOST_PATHS.health}`, {
3159
+ redirect: "error",
3160
+ signal: AbortSignal.timeout(2_000),
3161
+ }).catch(() => null);
3162
+ if (!health || health.status !== 200) throw failure("neutral_host_discovery_stale");
3163
+ const body = await health.json().catch(() => null);
3164
+ if (body?.hostGeneration !== record.hostGeneration || body?.status !== "running") {
3165
+ throw failure("neutral_host_discovery_generation_mismatch");
3166
+ }
3167
+ return Object.freeze(structuredClone(record));
3168
+ }
3169
+
3170
+ export async function attachHelloLynkNeutralHost(options = {}) {
3171
+ const record = options.discovery || await discoverHelloLynkNeutralHost(options);
3172
+ if (!record) throw failure("neutral_host_not_found");
3173
+ return requestHelloLynkNeutralHost({
3174
+ ...options,
3175
+ discovery: record,
3176
+ method: "POST",
3177
+ pathname: HELLOLYNK_HOST_PATHS.attachmentAttach,
3178
+ body: { adapterInstanceId: safeId(options.adapterInstanceId, "neutral_host_adapter_instance_invalid") },
3179
+ });
3180
+ }
3181
+
3182
+ // For installers, after new bindings are committed, for adapters that find a running Host
3183
+ // or one without a route they need, and for a Host start that finds the slot taken. A Host
3184
+ // of a lower release is asked to hand over; one of the same or a higher release is
3185
+ // "current" and left alone. Only a different, healthy generation is "restarted", which
3186
+ // still says nothing about its release; a removed discovery is "stopped"; anything
3187
+ // unobservable stays pending/unconfirmed. Never an install failure.
3188
+ export async function requestHelloLynkNeutralHostHandover(options = {}) {
3189
+ if (!options.runtimeRoot) throw failure("neutral_host_runtime_root_required");
3190
+ const runtimeRoot = path.resolve(String(options.runtimeRoot));
3191
+ const hostInstallationId = safeId(options.hostInstallationId, "neutral_host_installation_invalid");
3192
+ const publishedAtMs = Date.parse(String(options.publishedAt || ""));
3193
+ if (!Number.isFinite(publishedAtMs)) throw failure("neutral_host_handover_published_at_invalid");
3194
+ const waitMs = integerInRange(options.waitMs, 5_000, 0, 30_000);
3195
+ // Only a test can ask for a lower release than this code.
3196
+ const releaseOrdinal = integerInRange(options.releaseOrdinal, HELLOLYNK_HOST_RELEASE_ORDINAL, 0, HELLOLYNK_HOST_RELEASE_ORDINAL);
3197
+ // An adapter starts its own release's Host next, so it stops waiting once the slot is free.
3198
+ const untilStopped = options.untilStopped === true;
3199
+ const layout = discoveryLayout(runtimeRoot, hostInstallationId);
3200
+ try {
3201
+ const record = await readDiscoveryRecord(layout.discoveryPath);
3202
+ if (record === null) return Object.freeze({ status: "no_host" });
3203
+ const health = await discoveryHealth(record);
3204
+ if (!health) return Object.freeze({ status: "unconfirmed" });
3205
+ if (releaseOrdinalOf(health) >= releaseOrdinal) {
3206
+ return Object.freeze({ status: "current", hostGeneration: record.hostGeneration });
3207
+ }
3208
+ // A failed write only loses the hold; the request still goes out.
3209
+ await recordExpectedRelease(runtimeRoot, layout, releaseOrdinal).catch(() => {});
3210
+ await atomicWriteJson(layout.handoverPath, {
3211
+ contract: HANDOVER_REQUEST_CONTRACT,
3212
+ hostGeneration: record.hostGeneration,
3213
+ publishedAt: new Date(publishedAtMs).toISOString(),
3214
+ });
3215
+ let stopped = false;
3216
+ for (const deadline = Date.now() + waitMs; Date.now() < deadline;) {
3217
+ await new Promise((resolve) => setTimeout(resolve, 250));
3218
+ const current = await readDiscoveryRecord(layout.discoveryPath).catch(() => undefined);
3219
+ if (current === null) { stopped = true; if (untilStopped) break; continue; }
3220
+ if (current && current.hostGeneration !== record.hostGeneration && await discoveryRunning(current)) {
3221
+ return Object.freeze({ status: "restarted", previousHostGeneration: record.hostGeneration,
3222
+ hostGeneration: current.hostGeneration });
3223
+ }
3224
+ }
3225
+ return Object.freeze(stopped
3226
+ ? { status: "stopped", previousHostGeneration: record.hostGeneration }
3227
+ : { status: "pending", requestWritten: true, hostGeneration: record.hostGeneration });
3228
+ } catch {
3229
+ return Object.freeze({ status: "unconfirmed" });
3230
+ }
3231
+ }
3232
+
3233
+ // While a newer release is taking over (a fresh record of a higher release), a Host of a lower
3234
+ // release leaves the start to it. A stale or unreadable record never blocks a start. Read-only
3235
+ // and cheap, so the assembly calls it before it reads profiles, credentials and keys.
3236
+ export async function assertHelloLynkNeutralHostReleaseCurrent(options = {}) {
3237
+ if (!options.runtimeRoot) throw failure("neutral_host_runtime_root_required");
3238
+ const runtimeRoot = path.resolve(String(options.runtimeRoot));
3239
+ const hostInstallationId = safeId(options.hostInstallationId, "neutral_host_installation_invalid");
3240
+ // Only a test can report a lower release than this code.
3241
+ const releaseOrdinal = integerInRange(options.releaseOrdinal, HELLOLYNK_HOST_RELEASE_ORDINAL, 0, HELLOLYNK_HOST_RELEASE_ORDINAL);
3242
+ const recorded = await readExpectedRelease(discoveryLayout(runtimeRoot, hostInstallationId).expectedReleasePath)
3243
+ .catch(() => null);
3244
+ if (recorded && recorded.releaseOrdinal > releaseOrdinal && expectedReleaseHolds(recorded)) {
3245
+ throw failure("neutral_host_release_superseded");
3246
+ }
3247
+ }
3248
+
3249
+ // For an explicit restore to an earlier release: a record this release wrote would hold the
3250
+ // restored release back. A higher release's record or another format stays; none is fine.
3251
+ export async function removeHelloLynkNeutralHostExpectedRelease(options = {}) {
3252
+ if (!options.runtimeRoot) throw failure("neutral_host_runtime_root_required");
3253
+ const runtimeRoot = path.resolve(String(options.runtimeRoot));
3254
+ const hostInstallationId = safeId(options.hostInstallationId, "neutral_host_installation_invalid");
3255
+ const layout = discoveryLayout(runtimeRoot, hostInstallationId);
3256
+ await assertNoReparseAncestors(layout.expectedReleasePath);
3257
+ await assertSafePath(layout.expectedReleasePath, { mustExist: false, directory: false });
3258
+ let text;
3259
+ try {
3260
+ const metadata = await fs.lstat(layout.expectedReleasePath);
3261
+ text = metadata.size > EXPECTED_RELEASE_MAX_BYTES ? "" : await fs.readFile(layout.expectedReleasePath, "utf8");
3262
+ } catch (error) {
3263
+ if (error?.code === "ENOENT") return Object.freeze({ removed: false });
3264
+ throw error;
3265
+ }
3266
+ let recorded = null;
3267
+ try { recorded = JSON.parse(text); } catch {}
3268
+ if (recorded && typeof recorded === "object" && recorded.contract !== EXPECTED_RELEASE_CONTRACT) {
3269
+ return Object.freeze({ removed: false });
3270
+ }
3271
+ if (Number.isSafeInteger(recorded?.releaseOrdinal) && recorded.releaseOrdinal > HELLOLYNK_HOST_RELEASE_ORDINAL) {
3272
+ return Object.freeze({ removed: false, releaseOrdinal: recorded.releaseOrdinal });
3273
+ }
3274
+ await unlinkWithWindowsRetry(layout.expectedReleasePath);
3275
+ return Object.freeze({ removed: true });
3276
+ }
3277
+
3278
+ // Records the asking release before a handover request. A fresh record of a higher release stays.
3279
+ async function recordExpectedRelease(runtimeRoot, layout, releaseOrdinal) {
3280
+ await ensureSafeRuntimeDirectory(runtimeRoot, layout.directory);
3281
+ const recorded = await readExpectedRelease(layout.expectedReleasePath).catch(() => null);
3282
+ if (recorded && recorded.releaseOrdinal > releaseOrdinal && expectedReleaseHolds(recorded)) return;
3283
+ await atomicWriteJson(layout.expectedReleasePath, {
3284
+ contract: EXPECTED_RELEASE_CONTRACT,
3285
+ releaseOrdinal,
3286
+ publishedAt: new Date().toISOString(),
3287
+ });
3288
+ }
3289
+
3290
+ // The Host that was asked to leave stamps the time again as it frees the slot: admitted work
3291
+ // and an open native confirmation can keep it running past a hold counted from the request.
3292
+ async function restampExpectedRelease(layout, releaseOrdinal) {
3293
+ const recorded = await readExpectedRelease(layout.expectedReleasePath);
3294
+ if (!recorded || recorded.releaseOrdinal <= releaseOrdinal) return;
3295
+ await atomicWriteJson(layout.expectedReleasePath, {
3296
+ contract: EXPECTED_RELEASE_CONTRACT,
3297
+ releaseOrdinal: recorded.releaseOrdinal,
3298
+ publishedAt: new Date().toISOString(),
3299
+ });
3300
+ }
3301
+
3302
+ function expectedReleaseHolds(record) {
3303
+ return Math.abs(Date.now() - Date.parse(String(record.publishedAt || ""))) < EXPECTED_RELEASE_HOLD_MS;
3304
+ }
3305
+
3306
+ export async function requestHelloLynkNeutralHost(options = {}) {
3307
+ const record = options.discovery;
3308
+ validateDiscovery(record);
3309
+ const capability = options.capability;
3310
+ if (!capability || typeof capability !== "object") throw failure("neutral_host_capability_required");
3311
+ const method = String(options.method || "GET").toUpperCase();
3312
+ const pathname = String(options.pathname || "");
3313
+ const rawBody = options.body == null ? Buffer.alloc(0) : Buffer.from(JSON.stringify(options.body));
3314
+ const auth = createHostRequestAuth({
3315
+ capabilityId: capability.capabilityId,
3316
+ adapterId: capability.adapterId,
3317
+ hostGeneration: record.hostGeneration,
3318
+ method,
3319
+ pathname,
3320
+ timestampMs: Number(options.timestampMs ?? Date.now()),
3321
+ nonce: options.nonce || createHostNonce(),
3322
+ rawBody,
3323
+ secretBuffer: capability.secretBuffer,
3324
+ });
3325
+ const timeoutMs = options.timeoutMs == null ? null : Number(options.timeoutMs);
3326
+ if (timeoutMs != null && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100
3327
+ || timeoutMs > 30_000)) {
3328
+ throw failure("neutral_host_request_timeout_invalid");
3329
+ }
3330
+
3331
+ const response = await fetch(`http://${record.host}:${record.port}${pathname}`, {
3332
+ method,
3333
+ headers: { "content-type": "application/json", ...auth.headers, ...(options.extraHeaders || {}) },
3334
+ body: rawBody.length ? rawBody : undefined,
3335
+ redirect: "error",
3336
+ ...((timeoutMs == null && !options.signal) ? {} : { signal: AbortSignal.any([
3337
+ ...(timeoutMs == null ? [] : [AbortSignal.timeout(timeoutMs)]), ...(options.signal ? [options.signal] : []),
3338
+ ]) }),
3339
+ });
3340
+ const body = await response.json().catch(() => ({}));
3341
+ return Object.freeze({ status: response.status, body });
3342
+ }
3343
+
3344
+ export async function probeLegacyStoresReadOnly(paths = []) {
3345
+ const results = [];
3346
+ for (const candidate of paths) {
3347
+ const resolved = path.resolve(String(candidate || ""));
3348
+ if (!candidate) continue;
3349
+ try {
3350
+ const stat = await fs.stat(resolved);
3351
+ results.push(Object.freeze({ path: resolved, exists: true, kind: stat.isFile() ? "file" : "other" }));
3352
+ } catch (error) {
3353
+ if (error?.code === "ENOENT") results.push(Object.freeze({ path: resolved, exists: false, kind: "missing" }));
3354
+ else throw failure("neutral_host_legacy_probe_failed", error);
3355
+ }
3356
+ }
3357
+ return Object.freeze(results);
3358
+ }
3359
+
3360
+ function normalizeApplicationInbox(value) {
3361
+ if (!value) return null;
3362
+ const localHistory = Object.fromEntries([
3363
+ "readWorkbenchState", "deleteSentLocal", "hideLocalConversation", "restoreLocalConversation", "clearLocalConversation", "deleteLocalConversation", "deleteArchivedConversations",
3364
+ "getReceiptHighWater", "getInboxReaderBoundary", "activateInboxReader",
3365
+ "listInboxClaimIds", "claimInboxMessage", "finishInboxClaim", "readIncomingForModel",
3366
+ "reserveInboxBatch", "listInboxBatches", "getInboxNativeAssignment", "getInboxNativeAssignments", "attemptInboxBatch", "confirmInboxBatch",
3367
+ "cancelInboxBatch", "expireInboxBatch", "closeInboxBatch", "claimInboxBatch",
3368
+ "readWorkbenchSummary", "readWorkbenchRevision", "queryConversationPage", "readLocalHistoryStats", "queryOutbox", "searchWorkbenchThreads", "markIncomingRead", "configureConversationAutoRead",
3369
+ "configureLocalHistory", "maintainLocalHistory", "listPendingInboxCandidates", "listInboxMessagesForDelegate", "getMessageCounts",
3370
+ ].filter((name) => typeof value[name] === "function").map((name) => [name, (...args) => value[name](...args)]));
3371
+ if (typeof value.list === "function" && typeof value.getIncoming === "function"
3372
+ && typeof value.updateIncoming === "function" && typeof value.deleteIncoming === "function") {
3373
+ return Object.freeze({
3374
+ ...localHistory,
3375
+ list: () => value.list(),
3376
+ getIncoming: (id) => value.getIncoming(id),
3377
+ markRead: (id) => value.updateIncoming(id, { status: "fetched" }),
3378
+ deleteRead: (ids) => value.deleteIncoming(ids),
3379
+ });
3380
+ }
3381
+ return null;
3382
+ }
3383
+
3384
+ function normalizeApplicationContacts(value) {
3385
+ if (!value) return null;
3386
+ const list = typeof value.list === "function" ? () => value.list() : null;
3387
+ const upsert = typeof value.save === "function" ? (input) => value.save(input)
3388
+ : typeof value.upsert === "function" ? (input) => value.upsert(input) : null;
3389
+ const remove = typeof value.delete === "function" ? (id) => value.delete(id)
3390
+ : typeof value.remove === "function" ? (id) => value.remove(id) : null;
3391
+ const listForDelegate = typeof value.listForDelegate === "function"
3392
+ ? (delegateId) => value.listForDelegate(delegateId) : null;
3393
+ const listAuthorizedForDelegate = typeof value.listAuthorizedForDelegate === "function"
3394
+ ? (delegateId) => value.listAuthorizedForDelegate(delegateId) : null;
3395
+ const resolveAuthorizedSend = typeof value.resolveAuthorizedSend === "function"
3396
+ ? (contactId, delegateId, peerDelegateId) => value.resolveAuthorizedSend(contactId, delegateId, peerDelegateId) : null;
3397
+ const setAutonomousSendAuthorization = typeof value.setAutonomousSendAuthorization === "function"
3398
+ ? (contactId, delegateId, enabled, peerDelegateId) => value.setAutonomousSendAuthorization(contactId, delegateId, enabled, peerDelegateId)
3399
+ : null;
3400
+ return list && upsert && remove && listForDelegate && listAuthorizedForDelegate
3401
+ && resolveAuthorizedSend && setAutonomousSendAuthorization
3402
+ ? Object.freeze({
3403
+ list,
3404
+ upsert,
3405
+ remove,
3406
+ listForDelegate,
3407
+ listAuthorizedForDelegate,
3408
+ resolveAuthorizedSend,
3409
+ setAutonomousSendAuthorization,
3410
+ ...Object.fromEntries([
3411
+ "listReadableRoutesForDelegate",
3412
+ "resolveAuthorizedReply",
3413
+ ].filter((name) => typeof value[name] === "function")
3414
+ .map((name) => [name, (...args) => value[name](...args)])),
3415
+ ...(typeof value.resolveInstance === "function" ? { resolveInstance: (...args) => value.resolveInstance(...args) } : {}),
3416
+ ...(typeof value.upsertInstance === "function" ? { upsertInstance: (...args) => value.upsertInstance(...args) } : {}),
3417
+ ...(typeof value.renameInstance === "function" ? { renameInstance: (...args) => value.renameInstance(...args) } : {}),
3418
+ ...(typeof value.rename === "function" ? { rename: (...args) => value.rename(...args) } : {}),
3419
+ ...(typeof value.removeInstance === "function" ? { removeInstance: (...args) => value.removeInstance(...args) } : {}),
3420
+ })
3421
+ : null;
3422
+ }
3423
+
3424
+ function normalizeApplicationContactRequests(value) {
3425
+ if (!value || typeof value.listForHuman !== "function") return null;
3426
+ return Object.freeze({ listForHuman: (input) => value.listForHuman(input) });
3427
+ }
3428
+
3429
+ function resolveWorkbenchCapabilities(dependencies) {
3430
+ const {
3431
+ transport,
3432
+ applicationInbox,
3433
+ applicationContacts,
3434
+ applicationContactRequests,
3435
+ applicationOutbox,
3436
+ nativeBroker,
3437
+ humanRelationships,
3438
+ accountLauncher,
3439
+ } = dependencies;
3440
+ // A snapshot currently reads all four local projections atomically. Do not
3441
+ // advertise a partial surface that cannot produce the snapshot containing
3442
+ // its controls.
3443
+ if (!applicationInbox || !applicationContacts
3444
+ || !applicationContactRequests || !applicationOutbox) return Object.freeze([]);
3445
+ const capabilities = new Set([
3446
+ "contacts.read",
3447
+ "messages.read",
3448
+ "outbox.read",
3449
+ "search.local",
3450
+ "security.read",
3451
+ "threads.read",
3452
+ "messages.mark-read",
3453
+ "messages.delete-read",
3454
+ "outbox.request-retry",
3455
+ "contact.delete",
3456
+ "contact.authorization.manage",
3457
+ ]);
3458
+ if (typeof applicationInbox.deleteSentLocal === "function") capabilities.add("messages.delete-sent");
3459
+ if (typeof applicationInbox.markIncomingRead === "function") capabilities.add("messages.mark-all-read");
3460
+ if (typeof applicationInbox.configureConversationAutoRead === "function") capabilities.add("messages.auto-read.manage");
3461
+ if (["readWorkbenchState", "hideLocalConversation", "restoreLocalConversation", "clearLocalConversation"]
3462
+ .every((name) => typeof applicationInbox[name] === "function")) capabilities.add("threads.manage-local");
3463
+ if (humanRelationships && typeof humanRelationships.assertActiveRelationship === "function") {
3464
+ capabilities.add("contact.save");
3465
+ }
3466
+ if (typeof transport?.listAgentInstances === "function") {
3467
+ capabilities.add("contact.instances.list");
3468
+ if (typeof applicationContacts.upsertInstance === "function") capabilities.add("contact.instance.save");
3469
+ }
3470
+ if (typeof applicationContacts.removeInstance === "function") capabilities.add("contact.instance.delete");
3471
+ if (typeof applicationContacts.renameInstance === "function") capabilities.add("contact.instance.rename");
3472
+ if (typeof applicationContacts.rename === "function") capabilities.add("contact.rename");
3473
+ if (nativeBroker) capabilities.add("messages.request-send");
3474
+ if (humanRelationships && typeof humanRelationships.decide === "function") {
3475
+ capabilities.add("contact-request.respond");
3476
+ }
3477
+ if (humanRelationships && typeof humanRelationships.search === "function") {
3478
+ capabilities.add("principal.search");
3479
+ }
3480
+ if (humanRelationships && typeof humanRelationships.send === "function") {
3481
+ capabilities.add("contact-request.send");
3482
+ }
3483
+ if (humanRelationships && typeof humanRelationships.recoverDecision === "function") {
3484
+ capabilities.add("contact-request.recover-decision");
3485
+ }
3486
+ if (humanRelationships && typeof humanRelationships.clearResolved === "function"
3487
+ && HELLOLYNK_WORKBENCH_CAPABILITIES.includes(
3488
+ WORKBENCH_CLEAR_RESOLVED_CONTACT_REQUESTS_COMMAND,
3489
+ )) {
3490
+ capabilities.add(WORKBENCH_CLEAR_RESOLVED_CONTACT_REQUESTS_COMMAND);
3491
+ }
3492
+ if (humanRelationships && typeof humanRelationships.cancel === "function") {
3493
+ capabilities.add("contact-request.cancel");
3494
+ }
3495
+ if (humanRelationships && typeof humanRelationships.listRelationships === "function") {
3496
+ capabilities.add("relationships.read");
3497
+ }
3498
+ if (humanRelationships && typeof humanRelationships.endRelationship === "function") {
3499
+ capabilities.add("relationship.end");
3500
+ }
3501
+ if (humanRelationships && typeof humanRelationships.listBlocks === "function") {
3502
+ capabilities.add("blocks.read");
3503
+ }
3504
+ if (humanRelationships && typeof humanRelationships.removeBlock === "function") {
3505
+ capabilities.add("block.remove");
3506
+ }
3507
+ if (typeof accountLauncher === "function") capabilities.add("security.open-account");
3508
+ return Object.freeze([...capabilities].sort());
3509
+ }
3510
+
3511
+ function normalizeApplicationOutbox(explicit, store, messenger) {
3512
+ const value = explicit || store;
3513
+ if (!value) return null;
3514
+ const list = typeof value.list === "function" ? () => value.list() : null;
3515
+ const get = typeof value.get === "function" ? (id) => value.get(id)
3516
+ : typeof value.getOutgoing === "function" ? (id) => value.getOutgoing(id)
3517
+ : list ? async (id) => (await list()).find((message) => message.clientMessageId === id) || null : null;
3518
+ const retry = typeof value.retry === "function" ? (id, delegateId, options) => value.retry(id, delegateId, options)
3519
+ : typeof messenger?.retry === "function" ? (id, _delegateId, options) => messenger.retry(id, options) : null;
3520
+ return list && get && retry ? Object.freeze({ list, get, retry,
3521
+ ...(typeof value.queryOutbox === "function" ? { queryOutbox: (input) => value.queryOutbox(input) } : {}),
3522
+ }) : null;
3523
+ }
3524
+
3525
+ function projectInboxMessage(message, owner) {
3526
+ const localMessageId = safeApplicationId(message.localMessageId || message.id, "neutral_host_app_inbox_record_invalid");
3527
+ const binding = getMessageConversationBinding(message, owner);
3528
+ return {
3529
+ localMessageId,
3530
+ clientMessageId: safeApplicationId(message.clientMessageId || message.id, "neutral_host_app_inbox_record_invalid"),
3531
+ peerPrincipalId: optionalSafeId(message.peerPrincipalId || message.senderPrincipalId),
3532
+ peerHelloLynkId: optionalHelloLynkId(message.peerHelloLynkId || message.senderHelloLynkId),
3533
+ senderDelegateId: safeId(message.senderDelegateId, "neutral_host_app_inbox_record_invalid"),
3534
+ conversationKey: binding ? instanceConversationKey(binding) : null,
3535
+ localDelegateId: binding?.localDelegateId ?? null,
3536
+ peerDelegateId: binding?.peerDelegateId ?? null,
3537
+ senderPrincipalDisplayName: optionalDisplayName(message.senderPrincipalDisplayName),
3538
+ senderDelegateDisplayName: optionalDisplayName(message.senderDelegateDisplayName),
3539
+ messageType: safeId(message.messageType || "text", "neutral_host_app_inbox_record_invalid"),
3540
+ text: safeStoredText(message.text),
3541
+ unread: isUnreadIncoming(message),
3542
+ createdAt: safeIsoTimestamp(message.createdAt || message.receivedAt),
3543
+ updatedAt: safeIsoTimestamp(message.updatedAt || message.readAt || message.receivedAt || message.createdAt),
3544
+ };
3545
+ }
3546
+
3547
+ function projectContact(contact) {
3548
+ return {
3549
+ contactId: safeApplicationId(contact.contactId, "neutral_host_app_contact_record_invalid"),
3550
+ hellolynkId: safeHelloLynkId(contact.hellolynkId, "neutral_host_app_contact_record_invalid"),
3551
+ displayName: safeDisplayName(contact.displayName, "neutral_host_app_contact_record_invalid"),
3552
+ autonomousSendAuthorized: contact.autonomousSendAuthorized === true,
3553
+ createdAt: safeIsoTimestamp(contact.createdAt),
3554
+ updatedAt: safeIsoTimestamp(contact.updatedAt),
3555
+ };
3556
+ }
3557
+
3558
+ function projectContactAuthorizationMutation(contact) {
3559
+ return {
3560
+ contactId: safeApplicationId(contact.contactId, "neutral_host_app_contact_record_invalid"),
3561
+ autonomousSendAuthorized: contact.autonomousSendAuthorized === true,
3562
+ updatedAt: safeIsoTimestamp(contact.updatedAt),
3563
+ };
3564
+ }
3565
+
3566
+ function projectAuthorizedContact(contact) {
3567
+ return {
3568
+ contactId: safeApplicationId(contact.contactId, "neutral_host_app_contact_record_invalid"),
3569
+ displayName: safeDisplayName(contact.displayName, "neutral_host_app_contact_record_invalid"),
3570
+ recipientDelegateId: safeId(contact.recipientDelegateId, "neutral_host_app_contact_record_invalid"),
3571
+ instanceDisplayName: safeDisplayName(contact.instanceDisplayName, "neutral_host_app_contact_record_invalid"),
3572
+ };
3573
+ }
3574
+
3575
+ function projectAuthorizedInboxMetadata(message, contact, binding, activationAfterSeq = null) {
3576
+ // First activation and durable claims prevent history from becoming new work.
3577
+ const wakeEligible = message.messageType !== "local-deleted"
3578
+ && message.automaticDeliveryState !== "expired"
3579
+ && Number.isSafeInteger(activationAfterSeq) && activationAfterSeq >= 0
3580
+ && Number.isSafeInteger(message.receiptSeq) && message.receiptSeq > 0
3581
+ && message.receiptSeq > activationAfterSeq;
3582
+ return {
3583
+ localMessageId: safeApplicationId(message.localMessageId || message.id, "neutral_host_app_inbox_record_invalid"),
3584
+ contactId: contact ? safeApplicationId(contact.contactId, "neutral_host_app_contact_record_invalid") : null,
3585
+ displayName: safeDisplayName(contact?.displayName || (message.peerHelloLynkId
3586
+ || message.senderHelloLynkId || binding.peerPrincipalId).slice(0, 80), "neutral_host_app_contact_record_invalid"),
3587
+ // The contact name identifies the person; the saved instance label identifies
3588
+ // their AI. Neither label is an authority or a substitute for the bound IDs.
3589
+ ...(contact?.instanceDisplayName ? {
3590
+ peerInstanceDisplayName: safeDisplayName(contact.instanceDisplayName, "neutral_host_app_contact_record_invalid"),
3591
+ } : {}),
3592
+ conversationKey: instanceConversationKey(binding),
3593
+ localDelegateId: binding.localDelegateId,
3594
+ peerDelegateId: binding.peerDelegateId,
3595
+ // Already verified against the encrypted envelope and original local send
3596
+ // by the indexed store. This is a correlation identifier, never a body or
3597
+ // a caller-supplied native session destination.
3598
+ ...(message.replyToClientMessageId ? { replyToClientMessageId: safeApplicationId(message.replyToClientMessageId, "neutral_host_app_inbox_record_invalid") } : {}),
3599
+ ...(message.targetConversationId ? { targetConversationId: nativeConversationId(message.targetConversationId) } : {}),
3600
+ unread: isUnreadIncoming(message),
3601
+ ...(message.automaticDeliveryState === "expired" ? { automaticDeliveryState: "expired" } : {}),
3602
+ wakeEligible,
3603
+ createdAt: safeIsoTimestamp(message.createdAt || message.receivedAt),
3604
+ };
3605
+ }
3606
+
3607
+ function projectAuthorizedInboxMessage(message, contact, binding, activationAfterSeq = null, automatic = false) {
3608
+ return {
3609
+ ...projectAuthorizedInboxMetadata(message, contact, binding, activationAfterSeq),
3610
+ peerPrincipalId: binding.peerPrincipalId,
3611
+ text: safeStoredText(message.text),
3612
+ source: "hellolynk_external_message",
3613
+ externalContentIsUntrustedData: true,
3614
+ readDoesNotMarkRead: automatic !== true,
3615
+ ...(automatic === true ? { humanReadAutomatically: true } : {}),
3616
+ };
3617
+ }
3618
+
3619
+ function projectContactMutation(contact) {
3620
+ return {
3621
+ contactId: safeApplicationId(contact.contactId, "neutral_host_app_contact_record_invalid"),
3622
+ hellolynkId: safeHelloLynkId(contact.hellolynkId, "neutral_host_app_contact_record_invalid"),
3623
+ displayName: safeDisplayName(contact.displayName, "neutral_host_app_contact_record_invalid"),
3624
+ updatedAt: safeIsoTimestamp(contact.updatedAt),
3625
+ };
3626
+ }
3627
+
3628
+ function projectContactRequest(record) {
3629
+ if (record?.humanOnly !== true || record?.modelVisibility !== "none") {
3630
+ throw failure("neutral_host_app_contact_request_record_invalid");
3631
+ }
3632
+ const introText = safeStoredText(record.introText);
3633
+ const introWeight = record.introWeight;
3634
+ const state = safeId(record.state, "neutral_host_app_contact_request_record_invalid");
3635
+ const serverContactRequestId = record.serverContactRequestId == null
3636
+ ? null : safeApplicationId(
3637
+ record.serverContactRequestId,
3638
+ "neutral_host_app_contact_request_record_invalid",
3639
+ );
3640
+ const hasDecisionOutcome = Object.hasOwn(record, "decisionOutcome");
3641
+ const hasDecisionAction = Object.hasOwn(record, "decisionAction");
3642
+ if (hasDecisionOutcome !== hasDecisionAction) {
3643
+ throw failure("neutral_host_app_contact_request_record_invalid");
3644
+ }
3645
+ const decisionOutcome = hasDecisionOutcome
3646
+ ? record.decisionOutcome
3647
+ : null;
3648
+ const decisionAction = hasDecisionAction ? record.decisionAction : null;
3649
+ const actionReady = hasDecisionOutcome ? record.actionReady : false;
3650
+ if (typeof record.introText !== "string" || Buffer.byteLength(introText, "utf8") > 256
3651
+ || !Number.isSafeInteger(introWeight) || introWeight < 0 || introWeight > 40
3652
+ || !CONTACT_REQUEST_STATES.has(state)
3653
+ || typeof record.actionReady !== "boolean"
3654
+ || !new Set([null, "unknown", "recovery-required"]).has(decisionOutcome)
3655
+ || (decisionAction != null && !new Set(["accept", "reject", "block"]).has(decisionAction))
3656
+ || (actionReady && (serverContactRequestId == null
3657
+ || state !== "pending"
3658
+ || decisionOutcome != null
3659
+ || decisionAction != null))
3660
+ || (state !== "pending" && actionReady)
3661
+ || ((decisionOutcome === "unknown") !== (decisionAction != null))
3662
+ || (decisionOutcome != null && (state !== "pending" || actionReady))) {
3663
+ throw failure("neutral_host_app_contact_request_record_invalid");
3664
+ }
3665
+ return {
3666
+ humanOnly: true,
3667
+ modelVisibility: "none",
3668
+ localRecordId: safeApplicationId(
3669
+ record.localRecordId,
3670
+ "neutral_host_app_contact_request_record_invalid",
3671
+ ),
3672
+ serverContactRequestId,
3673
+ state,
3674
+ actionReady,
3675
+ decisionOutcome,
3676
+ decisionAction,
3677
+ introText,
3678
+ introWeight,
3679
+ sender: {
3680
+ hellolynkId: safeHelloLynkId(record.sender?.hellolynkId, "neutral_host_app_contact_request_record_invalid"),
3681
+ verifiedDisplayName: optionalDisplayName(record.sender?.verifiedDisplayName),
3682
+ },
3683
+ receivedAt: safeIsoTimestamp(record.receivedAt),
3684
+ serverUpdatedAt: record.serverUpdatedAt == null ? null : safeIsoTimestamp(record.serverUpdatedAt),
3685
+ };
3686
+ }
3687
+
3688
+ function projectOutboxMessage(message) {
3689
+ const text = safeStoredText(message.text);
3690
+ const status = safeId(message.status || message.rawStatus, "neutral_host_app_outbox_record_invalid");
3691
+ return {
3692
+ localMessageId: safeApplicationId(message.localMessageId, "neutral_host_app_outbox_record_invalid"),
3693
+ clientMessageId: safeApplicationId(message.clientMessageId, "neutral_host_app_outbox_record_invalid"),
3694
+ recipientHelloLynkId: safeHelloLynkId(message.recipientHelloLynkId || message.peerHelloLynkId, "neutral_host_app_outbox_record_invalid"),
3695
+ messageType: safeId(message.messageType || "text", "neutral_host_app_outbox_record_invalid"),
3696
+ text,
3697
+ textSha256: crypto.createHash("sha256").update(text, "utf8").digest("hex"),
3698
+ status,
3699
+ retryAllowed: message.lastErrorCode !== "reply_target_deleted" && MANUAL_RETRYABLE_STATUSES.has(status),
3700
+ createdAt: safeIsoTimestamp(message.createdAt),
3701
+ updatedAt: safeIsoTimestamp(message.updatedAt || message.createdAt),
3702
+ };
3703
+ }
3704
+
3705
+ function projectOutboxMutation(message) {
3706
+ const status = safeId(message.status || message.rawStatus, "neutral_host_app_outbox_record_invalid");
3707
+ return {
3708
+ clientMessageId: safeApplicationId(message.clientMessageId, "neutral_host_app_outbox_record_invalid"),
3709
+ status,
3710
+ retryAllowed: message.lastErrorCode !== "reply_target_deleted" && MANUAL_RETRYABLE_STATUSES.has(status),
3711
+ updatedAt: safeIsoTimestamp(message.updatedAt || message.createdAt),
3712
+ };
3713
+ }
3714
+
3715
+ function projectApplicationChange(event) {
3716
+ return {
3717
+ cursor: event.cursor,
3718
+ type: event.type,
3719
+ entityId: event.entityId,
3720
+ changedAt: event.changedAt,
3721
+ modelVisibility: "none",
3722
+ };
3723
+ }
3724
+
3725
+ function isUnreadIncoming(message) {
3726
+ if (typeof message?.unread === "boolean") return message.unread;
3727
+ return message?.status === "received";
3728
+ }
3729
+
3730
+ function requireApplicationStore(value, code) {
3731
+ if (!value) throw hostApplicationFailure("host_app_unavailable", code);
3732
+ }
3733
+
3734
+ function optionalSafeId(value) {
3735
+ return value == null ? null : safeId(value, "neutral_host_app_record_invalid");
3736
+ }
3737
+
3738
+ function optionalHelloLynkId(value) {
3739
+ return value == null ? null : safeHelloLynkId(value, "neutral_host_app_record_invalid");
3740
+ }
3741
+
3742
+ function optionalDisplayName(value) {
3743
+ return value == null ? null : safeDisplayName(value, "neutral_host_app_record_invalid");
3744
+ }
3745
+
3746
+ function safeHelloLynkId(value, code) {
3747
+ const normalized = String(value || "");
3748
+ if (!HELLOLYNK_ID.test(normalized)) throw failure(code);
3749
+ return normalized;
3750
+ }
3751
+
3752
+ function safeApplicationId(value, code) {
3753
+ const normalized = String(value || "");
3754
+ if (!APPLICATION_ID.test(normalized)) throw failure(code);
3755
+ return normalized;
3756
+ }
3757
+
3758
+ function safeDisplayName(value, code) {
3759
+ const normalized = String(value ?? "");
3760
+ if (!normalized || [...normalized].length > 80 || /[\u0000-\u001F\u007F-\u009F]/u.test(normalized)) {
3761
+ throw failure(code);
3762
+ }
3763
+ return normalized;
3764
+ }
3765
+
3766
+ function projectHumanSendStatus(value) {
3767
+ const status = assertPlainRecord(value, "neutral_host_human_send_status_invalid");
3768
+ return {
3769
+ kind: "message-send",
3770
+ actionId: status.actionId,
3771
+ state: status.state,
3772
+ code: status.code ?? null,
3773
+ createdAtMs: status.createdAtMs,
3774
+ expiresAtMs: status.expiresAtMs,
3775
+ };
3776
+ }
3777
+
3778
+ function safeStoredText(value) {
3779
+ const text = String(value ?? "");
3780
+ if (Buffer.byteLength(text, "utf8") > 64 * 1024 || text.includes("\u0000")) {
3781
+ throw failure("neutral_host_app_record_invalid");
3782
+ }
3783
+ return text;
3784
+ }
3785
+
3786
+ function safeIsoTimestamp(value) {
3787
+ const timestamp = Date.parse(String(value || ""));
3788
+ if (!Number.isFinite(timestamp)) throw failure("neutral_host_app_record_invalid");
3789
+ return new Date(timestamp).toISOString();
3790
+ }
3791
+
3792
+ function hostApplicationFailure(code, causeCode) {
3793
+ const error = new Error(code);
3794
+ error.code = code;
3795
+ if (causeCode) error.causeCode = causeCode;
3796
+ return error;
3797
+ }
3798
+
3799
+ async function claimInstallationLock(lockPath, discoveryPath, tuple, staleAudit = true) {
3800
+ return withLocalProcessLock(lockPath, () => claimInstallationLockUnderGuard(lockPath, discoveryPath, tuple, staleAudit));
3801
+ }
3802
+
3803
+ async function claimInstallationLockUnderGuard(lockPath, discoveryPath, tuple, staleAudit = true) {
3804
+ const token = crypto.randomBytes(32).toString("hex");
3805
+ const candidate = `${lockPath}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.candidate`;
3806
+ const ownerProcess = await inspectProcessIdentity(process.pid);
3807
+ const candidateRecord = Object.freeze({
3808
+ contract: INSTALLATION_LOCK_CONTRACT,
3809
+ tupleDigest: digestTuple(tuple),
3810
+ pid: process.pid,
3811
+ ownerIdentity: ownerProcess?.identity || null,
3812
+ claimedAtMs: Date.now(),
3813
+ token,
3814
+ });
3815
+ let candidateHandle;
3816
+ try {
3817
+ await assertSafePath(lockPath, { mustExist: false, directory: false });
3818
+ candidateHandle = await fs.open(candidate, "wx", 0o600);
3819
+ await candidateHandle.writeFile(`${JSON.stringify(candidateRecord)}\n`, "utf8");
3820
+ await candidateHandle.sync();
3821
+ await candidateHandle.close();
3822
+ candidateHandle = null;
3823
+ await linkWithWindowsRetry(candidate, lockPath);
3824
+ // Publication already succeeded. A leftover candidate is harmless; it
3825
+ // must not turn this successful claim into an ownerless rejected promise.
3826
+ await fs.unlink(candidate).catch(() => {});
3827
+ return Object.freeze({
3828
+ token,
3829
+ async release() {
3830
+ return withLocalProcessLock(lockPath, async () => {
3831
+ let current;
3832
+ try {
3833
+ current = (await readInstallationLock(lockPath)).record;
3834
+ } catch (error) {
3835
+ if (error?.code === "ENOENT") return;
3836
+ throw failure("neutral_host_singleton_lock_invalid", error);
3837
+ }
3838
+ if (current?.token !== token || current?.pid !== process.pid) {
3839
+ throw failure("neutral_host_singleton_lock_ownership_lost");
3840
+ }
3841
+ await unlinkWithWindowsRetry(lockPath);
3842
+ });
3843
+ },
3844
+ });
3845
+ } catch (error) {
3846
+ await candidateHandle?.close().catch(() => {});
3847
+ await fs.unlink(candidate).catch(() => {});
3848
+ if (error?.code !== "EEXIST") throw failure("neutral_host_singleton_claim_failed", error);
3849
+ let lockSnapshot;
3850
+ try {
3851
+ lockSnapshot = await readInstallationLock(lockPath);
3852
+ } catch { throw failure("neutral_host_singleton_lock_invalid"); }
3853
+ const existing = lockSnapshot.record;
3854
+ validateLockRecord(existing);
3855
+ if (existing?.tupleDigest !== digestTuple(tuple)) throw failure("neutral_host_singleton_owner_mismatch");
3856
+ if (staleAudit) {
3857
+ const discoveryHealthy = await discoveryRecordHealthy(
3858
+ discoveryPath,
3859
+ existing.tupleDigest,
3860
+ existing.pid,
3861
+ );
3862
+ if (discoveryHealthy) throw failure("neutral_host_already_running");
3863
+ const alive = processAlive(existing.pid);
3864
+ if (alive === null) throw failure("neutral_host_singleton_owner_unknown");
3865
+ if (!alive) return reclaimInstallationLock(lockPath, discoveryPath, tuple, lockSnapshot);
3866
+
3867
+ const observedOwner = await inspectProcessIdentity(existing.pid);
3868
+ if (existing.contract === INSTALLATION_LOCK_CONTRACT
3869
+ && existing.ownerIdentity && observedOwner?.identity === existing.ownerIdentity) {
3870
+ // A matching process-birth identity is authoritative during the interval
3871
+ // before discovery becomes healthy. PID liveness alone is never enough.
3872
+ throw failure("neutral_host_already_running");
3873
+ }
3874
+ if (existing.contract === INSTALLATION_LOCK_CONTRACT
3875
+ && existing.ownerIdentity && observedOwner?.identity
3876
+ && observedOwner.identity !== existing.ownerIdentity) {
3877
+ return reclaimInstallationLock(lockPath, discoveryPath, tuple, lockSnapshot);
3878
+ }
3879
+
3880
+ const aliveAfterInspection = processAlive(existing.pid);
3881
+ if (aliveAfterInspection === false) {
3882
+ return reclaimInstallationLock(lockPath, discoveryPath, tuple, lockSnapshot);
3883
+ }
3884
+ if (aliveAfterInspection === null) throw failure("neutral_host_singleton_owner_unknown");
3885
+
3886
+ // Legacy locks did not persist a process-birth marker. A current process
3887
+ // born after the lock is conclusive PID-reuse evidence; otherwise retain a
3888
+ // short startup grace and fail safely instead of risking split ownership.
3889
+ if (observedOwner?.startedAtMs != null
3890
+ && observedOwner.startedAtMs > lockSnapshot.mtimeMs + PROCESS_START_REUSE_TOLERANCE_MS) {
3891
+ return reclaimInstallationLock(lockPath, discoveryPath, tuple, lockSnapshot);
3892
+ }
3893
+ if (Date.now() - lockSnapshot.mtimeMs <= INSTALLATION_LOCK_STARTUP_GRACE_MS) {
3894
+ throw failure("neutral_host_already_running");
3895
+ }
3896
+ throw failure("neutral_host_singleton_owner_unknown");
3897
+ }
3898
+ throw failure("neutral_host_already_running");
3899
+ }
3900
+ }
3901
+
3902
+ function validateLockRecord(value) {
3903
+ const legacy = value?.contract == null;
3904
+ const expectedKeys = legacy
3905
+ ? ["pid", "token", "tupleDigest"]
3906
+ : ["claimedAtMs", "contract", "ownerIdentity", "pid", "token", "tupleDigest"];
3907
+ const actualKeys = value && typeof value === "object" && !Array.isArray(value)
3908
+ ? Object.keys(value).sort()
3909
+ : [];
3910
+ if (!value || actualKeys.length !== expectedKeys.length
3911
+ || actualKeys.some((item, index) => item !== expectedKeys[index])
3912
+ || (!legacy && value.contract !== INSTALLATION_LOCK_CONTRACT)
3913
+ || !/^[a-f0-9]{64}$/.test(String(value.tupleDigest || ""))
3914
+ || !Number.isSafeInteger(value.pid) || value.pid < 1
3915
+ || (!legacy && (!Number.isSafeInteger(value.claimedAtMs) || value.claimedAtMs < 1))
3916
+ || (!legacy && value.ownerIdentity != null
3917
+ && !/^[A-Za-z0-9][A-Za-z0-9:._-]{7,198}$/.test(String(value.ownerIdentity)))
3918
+ || !/^[a-f0-9]{64}$/.test(String(value.token || ""))) {
3919
+ throw failure("neutral_host_singleton_lock_invalid");
3920
+ }
3921
+ }
3922
+
3923
+ async function readInstallationLock(lockPath) {
3924
+ await assertSafePath(lockPath, { mustExist: true, directory: false });
3925
+ const metadata = await fs.lstat(lockPath);
3926
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > INSTALLATION_LOCK_MAX_BYTES) {
3927
+ throw failure("neutral_host_singleton_lock_invalid");
3928
+ }
3929
+ const text = await fs.readFile(lockPath, "utf8");
3930
+ let record;
3931
+ try { record = JSON.parse(text); } catch { throw failure("neutral_host_singleton_lock_invalid"); }
3932
+ validateLockRecord(record);
3933
+ return Object.freeze({ record, text, mtimeMs: metadata.mtimeMs });
3934
+ }
3935
+
3936
+ async function reclaimInstallationLock(lockPath, discoveryPath, tuple, snapshot) {
3937
+ await new Promise((resolve) => setTimeout(resolve, 25));
3938
+ let second;
3939
+ try { second = await readInstallationLock(lockPath); } catch {
3940
+ throw failure("neutral_host_singleton_lock_changed");
3941
+ }
3942
+ if (second.text !== snapshot.text) throw failure("neutral_host_singleton_lock_changed");
3943
+ const quarantine = `${lockPath}.stale.${Date.now()}.${crypto.randomBytes(8).toString("hex")}`;
3944
+ try { await fs.rename(lockPath, quarantine); } catch (renameError) {
3945
+ throw failure("neutral_host_singleton_stale_quarantine_failed", renameError);
3946
+ }
3947
+ try {
3948
+ return await claimInstallationLockUnderGuard(lockPath, discoveryPath, tuple, false);
3949
+ } finally {
3950
+ await fs.unlink(quarantine).catch(() => {});
3951
+ }
3952
+ }
3953
+
3954
+ function processAlive(pid) {
3955
+ try { process.kill(pid, 0); return true; } catch (error) {
3956
+ if (error?.code === "ESRCH") return false;
3957
+ if (error?.code === "EPERM") return true;
3958
+ return null;
3959
+ }
3960
+ }
3961
+
3962
+ async function discoveryRecordHealthy(discoveryPath, tupleDigest, expectedPid) {
3963
+ let record;
3964
+ try {
3965
+ await assertSafePath(discoveryPath, { mustExist: true, directory: false });
3966
+ record = JSON.parse(await fs.readFile(discoveryPath, "utf8"));
3967
+ validateDiscovery(record);
3968
+ } catch { return false; }
3969
+ if (record.tupleDigest !== tupleDigest || record.pid !== expectedPid) return false;
3970
+ const response = await fetch(`http://${record.host}:${record.port}${HELLOLYNK_HOST_PATHS.health}`, {
3971
+ redirect: "error",
3972
+ signal: AbortSignal.timeout(500),
3973
+ }).catch(() => null);
3974
+ if (!response?.ok) return false;
3975
+ const body = await response.json().catch(() => null);
3976
+ return body?.status === "running" && body?.hostGeneration === record.hostGeneration;
3977
+ }
3978
+
3979
+ async function linkWithWindowsRetry(candidate, target) {
3980
+ let last;
3981
+ for (const delayMs of [0, 10, 25, 50, 100, 200]) {
3982
+ if (delayMs) await new Promise((resolve) => setTimeout(resolve, delayMs));
3983
+ try { await fs.link(candidate, target); return; } catch (error) {
3984
+ if (error?.code === "EEXIST") throw error;
3985
+ last = error;
3986
+ if (!isWindowsTransientFsError(error)) break;
3987
+ }
3988
+ }
3989
+ throw last;
3990
+ }
3991
+
3992
+ async function unlinkWithWindowsRetry(target) {
3993
+ let last;
3994
+ for (const delayMs of [0, 10, 25, 50, 100, 200]) {
3995
+ if (delayMs) await new Promise((resolve) => setTimeout(resolve, delayMs));
3996
+ try { await fs.unlink(target); return; } catch (error) {
3997
+ if (error?.code === "ENOENT") return;
3998
+ last = error;
3999
+ if (!isWindowsTransientFsError(error)) break;
4000
+ }
4001
+ }
4002
+ throw last;
4003
+ }
4004
+
4005
+ function discoveryLayout(runtimeRoot, hostInstallationId) {
4006
+ const key = crypto.createHash("sha256").update(hostInstallationId).digest("hex");
4007
+ const directory = path.join(runtimeRoot, "neutral-host");
4008
+ return {
4009
+ directory,
4010
+ lockPath: path.join(directory, `${key}.lock`),
4011
+ discoveryPath: path.join(directory, `${key}.json`),
4012
+ receiverStatePath: path.join(directory, `${key}.receiver.json`),
4013
+ handoverPath: path.join(directory, `${key}.handover.json`),
4014
+ expectedReleasePath: path.join(directory, `${key}.expected-release.json`),
4015
+ };
4016
+ }
4017
+
4018
+ async function readHandoverRequest(target) {
4019
+ const metadata = await fs.lstat(target);
4020
+ if (!metadata.isFile() || metadata.size > HANDOVER_REQUEST_MAX_BYTES) return null;
4021
+ const record = JSON.parse(await fs.readFile(target, "utf8"));
4022
+ return record?.contract === HANDOVER_REQUEST_CONTRACT ? record : null;
4023
+ }
4024
+
4025
+ async function readExpectedRelease(target) {
4026
+ const metadata = await fs.lstat(target);
4027
+ if (!metadata.isFile() || metadata.size > EXPECTED_RELEASE_MAX_BYTES) return null;
4028
+ const record = JSON.parse(await fs.readFile(target, "utf8"));
4029
+ return record?.contract === EXPECTED_RELEASE_CONTRACT && Number.isSafeInteger(record.releaseOrdinal)
4030
+ && record.releaseOrdinal >= 0 ? record : null;
4031
+ }
4032
+
4033
+ // A Host without the field is older than every release that has an ordinal.
4034
+ function releaseOrdinalOf(health) {
4035
+ return Number.isSafeInteger(health?.releaseOrdinal) && health.releaseOrdinal > 0 ? health.releaseOrdinal : 0;
4036
+ }
4037
+
4038
+ // null only when there is no discovery record; an unreadable one throws.
4039
+ async function readDiscoveryRecord(discoveryPath) {
4040
+ let text;
4041
+ try { text = await fs.readFile(discoveryPath, "utf8"); }
4042
+ catch (error) { if (error?.code === "ENOENT") return null; throw error; }
4043
+ const record = JSON.parse(text);
4044
+ validateDiscovery(record);
4045
+ return record;
4046
+ }
4047
+
4048
+ // The health body when the recorded generation is running, otherwise null.
4049
+ async function discoveryHealth(record) {
4050
+ const health = await fetch(`http://${record.host}:${record.port}${HELLOLYNK_HOST_PATHS.health}`, {
4051
+ redirect: "error",
4052
+ signal: AbortSignal.timeout(2_000),
4053
+ }).catch(() => null);
4054
+ const body = health?.status === 200 ? await health.json().catch(() => null) : null;
4055
+ return body?.hostGeneration === record.hostGeneration && body?.status === "running" ? body : null;
4056
+ }
4057
+
4058
+ async function discoveryRunning(record) {
4059
+ return await discoveryHealth(record) !== null;
4060
+ }
4061
+
4062
+ async function atomicWriteJson(target, value) {
4063
+ await assertSafePath(target, { mustExist: false, directory: false });
4064
+ const temporary = `${target}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`;
4065
+ const handle = await fs.open(temporary, "wx", 0o600);
4066
+ try {
4067
+ await handle.writeFile(`${JSON.stringify(value)}\n`, "utf8");
4068
+ await handle.sync();
4069
+ } finally { await handle.close(); }
4070
+ try { await fs.rename(temporary, target); } catch (error) { await fs.unlink(temporary).catch(() => {}); throw error; }
4071
+ }
4072
+
4073
+ async function removeOwnedDiscovery(target, generation) {
4074
+ let current;
4075
+ try {
4076
+ await assertSafePath(target, { mustExist: true, directory: false });
4077
+ current = JSON.parse(await fs.readFile(target, "utf8"));
4078
+ } catch { return; }
4079
+ if (current?.hostGeneration === generation) await unlinkWithWindowsRetry(target).catch(() => {});
4080
+ }
4081
+
4082
+ async function ensureSafeRuntimeDirectory(runtimeRoot, hostDirectory) {
4083
+ await assertNoReparseAncestors(path.dirname(runtimeRoot));
4084
+ await fs.mkdir(runtimeRoot, { recursive: true, mode: 0o700 });
4085
+ await assertNoReparseAncestors(runtimeRoot);
4086
+ await assertSafePath(runtimeRoot, { mustExist: true, directory: true });
4087
+ await fs.mkdir(hostDirectory, { recursive: true, mode: 0o700 });
4088
+ await assertSafePath(hostDirectory, { mustExist: true, directory: true });
4089
+ }
4090
+
4091
+ async function assertNoReparseAncestors(target) {
4092
+ let current = path.resolve(target);
4093
+ while (true) {
4094
+ let stat;
4095
+ try { stat = await fs.lstat(current); } catch (error) {
4096
+ if (error?.code !== "ENOENT") throw error;
4097
+ current = path.dirname(current);
4098
+ continue;
4099
+ }
4100
+ if (stat.isSymbolicLink()) throw failure("neutral_host_unsafe_reparse_path");
4101
+ const parent = path.dirname(current);
4102
+ if (parent === current) return;
4103
+ current = parent;
4104
+ }
4105
+ }
4106
+
4107
+ async function assertSafePath(target, options) {
4108
+ let stat;
4109
+ try { stat = await fs.lstat(target); } catch (error) {
4110
+ if (error?.code === "ENOENT" && options.mustExist === false) return;
4111
+ throw error;
4112
+ }
4113
+ if (stat.isSymbolicLink()) throw failure("neutral_host_unsafe_reparse_path");
4114
+ if (options.directory === true && !stat.isDirectory()) throw failure("neutral_host_runtime_path_invalid");
4115
+ if (options.directory === false && !stat.isFile()) throw failure("neutral_host_runtime_path_invalid");
4116
+ }
4117
+
4118
+ function isWindowsTransientFsError(error) {
4119
+ return process.platform === "win32" && ["EPERM", "EACCES", "EBUSY"].includes(error?.code);
4120
+ }
4121
+
4122
+ function normalizeTuple(options) {
4123
+ return Object.freeze({
4124
+ region: safeId(options.region, "neutral_host_region_invalid"),
4125
+ principalId: safeId(options.principalId, "neutral_host_principal_invalid"),
4126
+ hostInstallationId: safeId(options.hostInstallationId, "neutral_host_installation_invalid"),
4127
+ });
4128
+ }
4129
+
4130
+ function normalizeEndpoints(value, tuple) {
4131
+ if (!Array.isArray(value) || value.length === 0 || value.length > 64) throw failure("neutral_host_endpoints_invalid");
4132
+ const map = new Map();
4133
+ for (const item of value) {
4134
+ const delegateId = safeId(item?.delegateId, "neutral_host_delegate_invalid");
4135
+ if (item?.principalId !== tuple.principalId || item?.hostInstallationId !== tuple.hostInstallationId) {
4136
+ throw failure("neutral_host_endpoint_owner_mismatch");
4137
+ }
4138
+ if (!item.e2ee || typeof item.e2ee.openIncoming !== "function") throw failure("neutral_host_endpoint_e2ee_invalid");
4139
+ if (!item.contactE2ee || typeof item.contactE2ee.openIncoming !== "function") {
4140
+ throw failure("neutral_host_endpoint_contact_e2ee_invalid");
4141
+ }
4142
+ if (map.has(delegateId)) throw failure("neutral_host_endpoint_duplicate");
4143
+ map.set(delegateId, Object.freeze({ ...item, delegateId }));
4144
+ }
4145
+ return map;
4146
+ }
4147
+
4148
+ function workbenchSource(adapterId) {
4149
+ const id = safeId(adapterId, "neutral_host_workbench_adapter_invalid");
4150
+ const clients = {
4151
+ codex: ["mcp-app-rpc", "Codex"],
4152
+ cursor: ["cursor-webview-rpc", "Cursor"],
4153
+ openclaw: ["loopback-http", "OpenClaw"],
4154
+ "trae-work": ["loopback-http", "TRAE Work"],
4155
+ workbuddy: ["loopback-http", "WorkBuddy"],
4156
+ };
4157
+ const [transport, clientLabel] = clients[id] || ["loopback-http", id];
4158
+ return Object.freeze({
4159
+ mode: "neutral-host",
4160
+ transport,
4161
+ clientId: id,
4162
+ clientLabel,
4163
+ });
4164
+ }
4165
+
4166
+ function workbenchSuccess(requestId, snapshot, result = {}) {
4167
+ return {
4168
+ contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
4169
+ schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
4170
+ ok: true,
4171
+ requestId,
4172
+ revision: snapshot.revision,
4173
+ changeCursor: snapshot.changeCursor,
4174
+ result,
4175
+ };
4176
+ }
4177
+
4178
+ function workbenchFailure(requestId, code, snapshot, outcome) {
4179
+ const known = outcome === "known-failed";
4180
+ return {
4181
+ contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
4182
+ schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
4183
+ ok: false,
4184
+ requestId,
4185
+ revision: known ? snapshot?.revision ?? null : null,
4186
+ changeCursor: known ? snapshot?.changeCursor ?? null : null,
4187
+ error: {
4188
+ code,
4189
+ retryable: false,
4190
+ outcome,
4191
+ retryAfterMs: null,
4192
+ currentRevision: known ? snapshot?.revision ?? null : null,
4193
+ },
4194
+ };
4195
+ }
4196
+
4197
+ function knownWorkbenchCommandFailure(error) {
4198
+ const code = String(error?.code || "");
4199
+ if (new Set([
4200
+ "human_relationship_decision_outcome_unknown",
4201
+ "human_relationship_cancel_outcome_unknown",
4202
+ "human_relationship_cancel_response_invalid",
4203
+ "human_relationship_relationship_end_outcome_unknown",
4204
+ "human_relationship_relationship_end_response_invalid",
4205
+ "human_relationship_block_remove_outcome_unknown",
4206
+ "human_relationship_block_remove_response_invalid",
4207
+ // The cloud write has already succeeded before these local/invariant
4208
+ // checks fail. They cannot prove rejection or permit another submission.
4209
+ "human_relationship_local_authorization_revoke_failed",
4210
+ "host_app_relationship_history_invariant_failed",
4211
+ "host_app_block_remove_invariant_failed",
4212
+ ]).has(code)) return null;
4213
+ if (code === "human_relationship_cancel_known_failed") {
4214
+ return "host_app_contact_request_cancel_failed";
4215
+ }
4216
+ if (code === "human_relationship_relationship_end_known_failed") {
4217
+ return "host_app_relationship_end_failed";
4218
+ }
4219
+ if (code === "human_relationship_block_remove_known_failed") {
4220
+ return "host_app_block_remove_failed";
4221
+ }
4222
+ if (code === "human_relationship_decision_known_failed") {
4223
+ return "host_app_contact_request_decision_failed";
4224
+ }
4225
+ if (code === "human_relationship_decision_not_applied") {
4226
+ return "host_app_contact_request_decision_not_applied";
4227
+ }
4228
+ if (code === "human_relationship_target_expired") return "host_app_contact_target_expired";
4229
+ if (code === "human_relationship_retry_input_conflict") return "host_app_contact_request_input_conflict";
4230
+ if (code === "contact_identity_mismatch") return "host_app_authorization_required";
4231
+ if (["local_history_unread_protected", "local_history_outgoing_protected", "local_history_task_protected"].includes(code)) return `host_app_${code}`;
4232
+ if (code === "local_history_record_not_found") return "host_app_record_not_found";
4233
+ if (["local_history_scope_invalid", "local_history_thread_invalid", "local_history_message_invalid"].includes(code)) {
4234
+ return "host_app_input_invalid";
4235
+ }
4236
+ if (["contact_not_found", "contact_instance_not_found"].includes(code)) return "host_app_record_not_found";
4237
+ if (/^contact_(?:authorization|delegate)/.test(code)) return "host_app_input_invalid";
4238
+ if (/^contact_request_intro_[a-z0-9_]{1,80}$/.test(code)) return code;
4239
+ if (error?.httpStatus === 409 && KNOWN_CONTACT_REQUEST_SEND_FAILURE_CODES.has(code)) return code;
4240
+ if (/^human_relationship_[a-z0-9_]{1,100}$/.test(code)) return "host_app_contact_request_failed";
4241
+ if (/^host_app_[a-z0-9_]{1,100}$/.test(code)) return code;
4242
+ if (/^human_confirmation_[a-z0-9_]{1,100}$/.test(code)) return code;
4243
+ return null;
4244
+ }
4245
+
4246
+ function validateDiscovery(record) {
4247
+ if (!record || record.contractId !== HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT
4248
+ || record.host !== LOOPBACK_HOST || !Number.isInteger(record.port) || record.port < 1 || record.port > 65535
4249
+ || !/^[a-f0-9]{48}$/.test(String(record.hostGeneration || ""))
4250
+ || !/^[a-f0-9]{64}$/.test(String(record.tupleDigest || ""))) {
4251
+ throw failure("neutral_host_discovery_invalid");
4252
+ }
4253
+ }
4254
+
4255
+ function digestTuple(tuple) {
4256
+ return crypto.createHash("sha256")
4257
+ .update(`${tuple.region}\0${tuple.principalId}\0${tuple.hostInstallationId}`)
4258
+ .digest("hex");
4259
+ }
4260
+
4261
+ function requireInterface(value, methods, code) {
4262
+ if (!value || methods.some((method) => typeof value[method] !== "function")) throw failure(code);
4263
+ }
4264
+
4265
+ function trackOperation(promise, set) {
4266
+ const tracked = Promise.resolve(promise);
4267
+ set.add(tracked);
4268
+ tracked.finally(() => set.delete(tracked)).catch(() => {});
4269
+ return tracked;
4270
+ }
4271
+
4272
+ async function waitForOperations(set, timeoutMs) {
4273
+ const deadline = Date.now() + timeoutMs;
4274
+ while (set.size > 0) {
4275
+ const remaining = deadline - Date.now();
4276
+ if (remaining <= 0) throw failure("neutral_host_drain_timeout");
4277
+ await Promise.race([Promise.allSettled([...set]), new Promise((resolve) => setTimeout(resolve, Math.min(remaining, 25)))]);
4278
+ }
4279
+ }
4280
+
4281
+ function listenLoopback(server, port) {
4282
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw failure("neutral_host_port_invalid");
4283
+ return new Promise((resolve, reject) => {
4284
+ server.once("error", reject);
4285
+ server.listen(port, LOOPBACK_HOST, () => { server.off("error", reject); resolve(); });
4286
+ });
4287
+ }
4288
+
4289
+ function closeServer(server) {
4290
+ return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
4291
+ }
4292
+
4293
+ function readBody(request, maximum) {
4294
+ return new Promise((resolve, reject) => {
4295
+ const chunks = [];
4296
+ let size = 0;
4297
+ request.on("data", (chunk) => {
4298
+ size += chunk.length;
4299
+ if (size > maximum) { reject(failure("neutral_host_request_too_large")); request.destroy(); return; }
4300
+ chunks.push(chunk);
4301
+ });
4302
+ request.on("end", () => resolve(Buffer.concat(chunks)));
4303
+ request.on("error", reject);
4304
+ });
4305
+ }
4306
+
4307
+ function writeJson(response, status, body) {
4308
+ const raw = Buffer.from(JSON.stringify(body));
4309
+ response.writeHead(status, { "content-type": "application/json", "content-length": raw.length, "cache-control": "no-store" });
4310
+ response.end(raw);
4311
+ }
4312
+
4313
+ function isLoopbackAddress(value) {
4314
+ return value === "127.0.0.1" || value === "::1" || value === "::ffff:127.0.0.1";
4315
+ }
4316
+
4317
+ function assertNoSensitiveEventFields(value) {
4318
+ for (const key of Object.keys(value)) {
4319
+ if (/text|body|content|secret|token|credential|envelope|prekey/i.test(key)) throw failure("neutral_host_change_event_sensitive");
4320
+ }
4321
+ }
4322
+
4323
+ function isInboundLocalCapacityError(error) {
4324
+ return INBOUND_LOCAL_CAPACITY_CODES.has(String(error?.code || ""));
4325
+ }
4326
+
4327
+ function safeId(value, code) {
4328
+ const normalized = String(value || "");
4329
+ if (!SAFE_ID.test(normalized)) throw failure(code);
4330
+ return normalized;
4331
+ }
4332
+
4333
+ function assertPlainRecord(value, code) {
4334
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw failure(code);
4335
+ const prototype = Object.getPrototypeOf(value);
4336
+ if (prototype !== Object.prototype && prototype !== null) throw failure(code);
4337
+ return value;
4338
+ }
4339
+
4340
+ function integerInRange(value, fallback, minimum, maximum) {
4341
+ const normalized = Number(value ?? fallback);
4342
+ if (!Number.isSafeInteger(normalized) || normalized < minimum || normalized > maximum) throw failure("neutral_host_limit_invalid");
4343
+ return normalized;
4344
+ }
4345
+
4346
+ function nowMs(clock) {
4347
+ return typeof clock === "function" ? clock() : typeof clock?.now === "function" ? clock.now() : Date.now();
4348
+ }
4349
+
4350
+ function retryAtTimestamp(value) {
4351
+ if (value == null || value === "") return null;
4352
+ const parsed = Date.parse(String(value));
4353
+ return Number.isFinite(parsed) ? parsed : null;
4354
+ }
4355
+
4356
+ function safeCode(error, fallback) {
4357
+ const code = String(error?.code || fallback);
4358
+ return /^[a-z][a-z0-9_]{1,127}$/.test(code) ? code : fallback;
4359
+ }
4360
+
4361
+ function isTransientWorkbenchCloudFailure(error) {
4362
+ return new Set(["host_transport_request_timeout", "host_transport_request_failed"]).has(error?.code)
4363
+ || (Number.isInteger(error?.httpStatus) && error.httpStatus >= 500 && error.httpStatus <= 599);
4364
+ }
4365
+
4366
+ function isMissingLegacyLifecycleRoute(error) {
4367
+ return error?.code === "host_transport_http_error" && error?.httpStatus === 404;
4368
+ }
4369
+
4370
+ function failure(code, cause) {
4371
+ const error = new Error(code, cause ? { cause } : undefined);
4372
+ error.code = code;
4373
+ return error;
4374
+ }