@xmoxmo/bncr 0.3.5 → 0.3.7

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 (164) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/package.json +8 -4
  5. package/scripts/check-pack.mjs +93 -18
  6. package/scripts/check-register-drift.mjs +35 -13
  7. package/scripts/selfcheck.mjs +80 -11
  8. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  9. package/src/bootstrap/cli.ts +97 -0
  10. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  11. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  12. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  13. package/src/bootstrap/register-runtime.ts +201 -0
  14. package/src/bootstrap/runtime-discovery.ts +187 -0
  15. package/src/bootstrap/runtime-loader.ts +54 -0
  16. package/src/channel.ts +1590 -4967
  17. package/src/core/accounts.ts +23 -4
  18. package/src/core/dead-letter-diagnostics.ts +37 -5
  19. package/src/core/diagnostics.ts +31 -15
  20. package/src/core/downlink-health.ts +3 -11
  21. package/src/core/extended-diagnostics.ts +78 -36
  22. package/src/core/file-transfer-payloads.ts +1 -1
  23. package/src/core/logging.ts +1 -0
  24. package/src/core/outbox-enqueue.ts +13 -2
  25. package/src/core/outbox-entry-builders.ts +2 -0
  26. package/src/core/outbox-summary.ts +75 -3
  27. package/src/core/permissions.ts +15 -2
  28. package/src/core/persisted-outbox-entry.ts +21 -6
  29. package/src/core/policy.ts +45 -4
  30. package/src/core/probe.ts +3 -15
  31. package/src/core/register-trace.ts +3 -3
  32. package/src/core/status.ts +43 -4
  33. package/src/core/targets.ts +216 -205
  34. package/src/core/types.ts +221 -0
  35. package/src/core/value-sanitize.ts +29 -0
  36. package/src/messaging/inbound/commands.ts +147 -172
  37. package/src/messaging/inbound/context-facts.ts +4 -2
  38. package/src/messaging/inbound/contracts.ts +70 -0
  39. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  40. package/src/messaging/inbound/dispatch.ts +49 -462
  41. package/src/messaging/inbound/gate.ts +18 -5
  42. package/src/messaging/inbound/last-route.ts +10 -4
  43. package/src/messaging/inbound/media-url-download.ts +109 -0
  44. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  45. package/src/messaging/inbound/parse.ts +2 -1
  46. package/src/messaging/inbound/remote-media.ts +49 -0
  47. package/src/messaging/inbound/reply-config.ts +16 -4
  48. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  49. package/src/messaging/inbound/runtime-compat.ts +31 -10
  50. package/src/messaging/inbound/session-label.ts +15 -7
  51. package/src/messaging/inbound/turn-context.ts +131 -0
  52. package/src/messaging/outbound/actions.ts +24 -10
  53. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  54. package/src/messaging/outbound/diagnostics.ts +31 -355
  55. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  56. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  57. package/src/messaging/outbound/media.ts +24 -13
  58. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  59. package/src/messaging/outbound/reply-enqueue.ts +57 -134
  60. package/src/messaging/outbound/send-params.ts +3 -0
  61. package/src/messaging/outbound/send.ts +19 -10
  62. package/src/messaging/outbound/session-route.ts +18 -3
  63. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  64. package/src/openclaw/config-runtime.ts +13 -7
  65. package/src/openclaw/inbound-session-runtime.ts +7 -3
  66. package/src/openclaw/ingress-runtime.ts +17 -27
  67. package/src/openclaw/reply-runtime.ts +54 -59
  68. package/src/openclaw/routing-runtime.ts +35 -18
  69. package/src/openclaw/runtime-surface.ts +156 -12
  70. package/src/openclaw/sdk-helpers.ts +8 -1
  71. package/src/openclaw/session-route-runtime.ts +12 -12
  72. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  73. package/src/plugin/bridge-ack-facade.ts +137 -0
  74. package/src/plugin/bridge-connection-facade.ts +111 -0
  75. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  76. package/src/plugin/bridge-drain-facade.ts +98 -0
  77. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  78. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  79. package/src/plugin/bridge-lifecycle.ts +156 -0
  80. package/src/plugin/bridge-media-facade.ts +241 -0
  81. package/src/plugin/bridge-outbox-facade.ts +182 -0
  82. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  83. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  84. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  85. package/src/plugin/bridge-status-facade.ts +76 -0
  86. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  87. package/src/plugin/bridge-support-runtime.ts +137 -0
  88. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  89. package/src/plugin/bridge-surface-helpers.ts +28 -0
  90. package/src/plugin/capabilities.ts +1 -3
  91. package/src/plugin/channel-components.ts +289 -0
  92. package/src/plugin/channel-inbound-helpers.ts +149 -0
  93. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  94. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  95. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  96. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  97. package/src/plugin/channel-runtime-builders.ts +25 -0
  98. package/src/plugin/channel-runtime-constants.ts +40 -0
  99. package/src/plugin/channel-runtime-types.ts +146 -0
  100. package/src/plugin/channel-send-runtime-group.ts +37 -0
  101. package/src/plugin/channel-send.ts +226 -0
  102. package/src/plugin/channel-utils.ts +102 -0
  103. package/src/plugin/config.ts +24 -3
  104. package/src/plugin/connection-handlers-helpers.ts +254 -0
  105. package/src/plugin/connection-handlers.ts +440 -0
  106. package/src/plugin/connection-state-helpers.ts +159 -0
  107. package/src/plugin/connection-state-runtime-group.ts +51 -0
  108. package/src/plugin/connection-state.ts +527 -0
  109. package/src/plugin/diagnostics-handlers.ts +211 -0
  110. package/src/plugin/error-message.ts +15 -0
  111. package/src/plugin/file-ack-runtime.ts +284 -0
  112. package/src/plugin/file-inbound-abort.ts +112 -0
  113. package/src/plugin/file-inbound-chunk.ts +146 -0
  114. package/src/plugin/file-inbound-complete.ts +153 -0
  115. package/src/plugin/file-inbound-handlers.ts +19 -0
  116. package/src/plugin/file-inbound-init.ts +122 -0
  117. package/src/plugin/file-inbound-runtime.ts +51 -0
  118. package/src/plugin/file-inbound-state.ts +62 -0
  119. package/src/plugin/file-transfer-logs.ts +227 -0
  120. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  121. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  122. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  123. package/src/plugin/file-transfer-send.ts +89 -0
  124. package/src/plugin/file-transfer-setup.ts +206 -0
  125. package/src/plugin/gateway-event-context.ts +41 -0
  126. package/src/plugin/gateway-runtime.ts +14 -4
  127. package/src/plugin/inbound-acceptance.ts +107 -0
  128. package/src/plugin/inbound-handlers.ts +248 -0
  129. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  130. package/src/plugin/media-dedupe-runtime.ts +90 -0
  131. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  132. package/src/plugin/message-ack-runtime.ts +284 -0
  133. package/src/plugin/message-send.ts +16 -6
  134. package/src/plugin/messaging.ts +98 -36
  135. package/src/plugin/outbound.ts +50 -8
  136. package/src/plugin/outbox-ack-logs.ts +136 -0
  137. package/src/plugin/outbox-ack-outcome.ts +128 -0
  138. package/src/plugin/outbox-drain-ack.ts +145 -0
  139. package/src/plugin/outbox-drain-failure.ts +84 -0
  140. package/src/plugin/outbox-drain-loop.ts +554 -0
  141. package/src/plugin/outbox-drain-post-push.ts +159 -0
  142. package/src/plugin/outbox-drain-runtime.ts +141 -0
  143. package/src/plugin/outbox-drain-schedule.ts +116 -0
  144. package/src/plugin/outbox-file-push-flow.ts +69 -0
  145. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  146. package/src/plugin/outbox-push.ts +267 -0
  147. package/src/plugin/outbox-route.ts +181 -0
  148. package/src/plugin/outbox-text-push-flow.ts +90 -0
  149. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  150. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  151. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  152. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  153. package/src/plugin/setup.ts +33 -6
  154. package/src/plugin/state-store.ts +249 -0
  155. package/src/plugin/state-transient-runtime-group.ts +105 -0
  156. package/src/plugin/status-runtime.ts +251 -0
  157. package/src/plugin/status.ts +33 -7
  158. package/src/plugin/target-runtime.ts +141 -0
  159. package/src/plugin/target-status-runtime-group.ts +130 -0
  160. package/src/plugin/transient-state-runtime.ts +82 -0
  161. package/src/runtime/outbound-ack-timeout.ts +5 -3
  162. package/src/runtime/outbound-flags.ts +24 -8
  163. package/src/runtime/status-snapshots.ts +36 -7
  164. package/src/runtime/status-worker.ts +34 -4
@@ -0,0 +1,211 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import { BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId } from '../core/accounts.ts';
3
+ import {
4
+ parseDeadLetterLimit,
5
+ parseDeadLetterOffset,
6
+ parseDeadLetterOlderThan,
7
+ summarizeDeadLetterEntry,
8
+ } from '../core/dead-letter-diagnostics.ts';
9
+ import { buildDiagnosticsPayload } from '../core/diagnostics.ts';
10
+ import type { BncrExtendedDiagnostics } from '../core/extended-diagnostics.ts';
11
+ import type {
12
+ BncrAccountRuntimeSnapshot,
13
+ buildIntegratedDiagnostics as buildIntegratedDiagnosticsFromRuntime,
14
+ } from '../core/status.ts';
15
+ import type {
16
+ BncrDeadLetterDiagnosticsSummary,
17
+ BncrDownlinkHealthSummary,
18
+ OutboxEntry,
19
+ } from '../core/types.ts';
20
+ import { getOpenClawRuntimeConfig } from '../openclaw/config-runtime.ts';
21
+ import type { BncrRuntimeFlags } from '../runtime/outbound-flags.ts';
22
+
23
+ type RuntimeStatusInput = Parameters<typeof buildIntegratedDiagnosticsFromRuntime>[0];
24
+ type IntegratedDiagnostics = ReturnType<typeof buildIntegratedDiagnosticsFromRuntime>;
25
+ type DiagnosticsRuntimeStatusOverrides = {
26
+ running: boolean;
27
+ invalidOutboxSessionKeys?: number;
28
+ legacyAccountResidue?: number;
29
+ };
30
+ type DiagnosticsRuntimeStatusInput = RuntimeStatusInput & {
31
+ running: boolean | undefined;
32
+ channelRoot: string;
33
+ };
34
+
35
+ type RuntimeApiHolder = {
36
+ runtime?: {
37
+ config?: unknown;
38
+ [key: string]: unknown;
39
+ };
40
+ [key: string]: unknown;
41
+ };
42
+
43
+ export type BncrDiagnosticsHandlerRuntime = {
44
+ getApi: () => RuntimeApiHolder;
45
+ channelId: string;
46
+ asString: (value: unknown, fallback?: string) => string;
47
+ now: () => number;
48
+ countInvalidOutboxSessionKeys: (accountId: string) => number;
49
+ countLegacyAccountResidue: (accountId: string) => number;
50
+ buildRuntimeStatusInput: (
51
+ accountId: string,
52
+ overrides: DiagnosticsRuntimeStatusOverrides,
53
+ ) => DiagnosticsRuntimeStatusInput;
54
+ getAccountRuntimeSnapshot: (
55
+ accountId: string,
56
+ runtimeStatusInput: DiagnosticsRuntimeStatusInput,
57
+ ) => BncrAccountRuntimeSnapshot;
58
+ buildIntegratedDiagnostics: (
59
+ accountId: string,
60
+ runtimeStatusInput: DiagnosticsRuntimeStatusInput,
61
+ ) => IntegratedDiagnostics;
62
+ buildExtendedDiagnostics: (
63
+ accountId: string,
64
+ args: {
65
+ runtimeStatusInput: DiagnosticsRuntimeStatusInput;
66
+ integratedDiagnostics: IntegratedDiagnostics;
67
+ },
68
+ ) => BncrExtendedDiagnostics;
69
+ buildDownlinkHealth: (accountId: string) => BncrDownlinkHealthSummary;
70
+ buildRuntimeFlags: (accountId: string) => BncrRuntimeFlags;
71
+ activeConnectionCount: (accountId: string) => number;
72
+ getMessageAckWaiterCount: () => number;
73
+ getFileAckWaiterCount: () => number;
74
+ filterDeadLetterEntries: (args: {
75
+ accountId: string;
76
+ reason: string | null;
77
+ olderThan: number | null;
78
+ }) => OutboxEntry[];
79
+ listDeadLetterEntries: () => OutboxEntry[];
80
+ buildDeadLetterDiagnostics: (accountId: string) => BncrDeadLetterDiagnosticsSummary;
81
+ replaceDeadLetterEntries: (nextEntries: OutboxEntry[]) => void;
82
+ scheduleSave: () => void;
83
+ logDeadLetterSummary: (accountId: string, args: { force: boolean; source: string }) => void;
84
+ };
85
+
86
+ export function createBncrDiagnosticsHandlers(runtime: BncrDiagnosticsHandlerRuntime) {
87
+ return {
88
+ handleDiagnostics: async ({ params, respond }: GatewayRequestHandlerOptions) => {
89
+ const accountId = normalizeAccountId(runtime.asString(params?.accountId || ''));
90
+ const cfg = getOpenClawRuntimeConfig(runtime.getApi());
91
+ const invalidOutboxSessionKeys = runtime.countInvalidOutboxSessionKeys(accountId);
92
+ const legacyAccountResidue = runtime.countLegacyAccountResidue(accountId);
93
+ const runtimeStatusInput = runtime.buildRuntimeStatusInput(accountId, {
94
+ running: true,
95
+ invalidOutboxSessionKeys,
96
+ legacyAccountResidue,
97
+ });
98
+ const runtimeSnapshot = runtime.getAccountRuntimeSnapshot(accountId, runtimeStatusInput);
99
+ const integratedDiagnostics = runtime.buildIntegratedDiagnostics(
100
+ accountId,
101
+ runtimeStatusInput,
102
+ );
103
+ const diagnostics = runtime.buildExtendedDiagnostics(accountId, {
104
+ runtimeStatusInput,
105
+ integratedDiagnostics,
106
+ });
107
+
108
+ respond(
109
+ true,
110
+ buildDiagnosticsPayload({
111
+ cfg,
112
+ channelId: runtime.channelId,
113
+ accountId,
114
+ runtime: runtimeSnapshot,
115
+ diagnostics,
116
+ downlinkHealth: runtime.buildDownlinkHealth(accountId),
117
+ runtimeFlags: runtime.buildRuntimeFlags(accountId),
118
+ waiters: {
119
+ messageAck: runtime.getMessageAckWaiterCount(),
120
+ fileAck: runtime.getFileAckWaiterCount(),
121
+ },
122
+ activeConnections: runtime.activeConnectionCount(accountId),
123
+ invalidOutboxSessionKeys,
124
+ legacyAccountResidue,
125
+ now: runtime.now(),
126
+ }),
127
+ );
128
+ },
129
+
130
+ handleDeadLetterInspect: async ({ params, respond }: GatewayRequestHandlerOptions) => {
131
+ const accountId = normalizeAccountId(
132
+ runtime.asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
133
+ );
134
+ const reason = runtime.asString(params?.reason || '').trim() || null;
135
+ const olderThan = parseDeadLetterOlderThan(params?.olderThan);
136
+ const limit = parseDeadLetterLimit(params?.limit, 20);
137
+ const offset = parseDeadLetterOffset(params?.offset, 0);
138
+ const matches = runtime
139
+ .filterDeadLetterEntries({ accountId, reason, olderThan })
140
+ .slice()
141
+ .sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0));
142
+
143
+ respond(true, {
144
+ ok: true,
145
+ accountId,
146
+ filters: { reason, olderThan },
147
+ total: matches.length,
148
+ offset,
149
+ limit,
150
+ entries: matches
151
+ .slice(offset, offset + limit)
152
+ .map((entry) => summarizeDeadLetterEntry(entry)),
153
+ summary: runtime.buildDeadLetterDiagnostics(accountId),
154
+ now: runtime.now(),
155
+ });
156
+ },
157
+
158
+ handleDeadLetterPrune: async ({ params, respond }: GatewayRequestHandlerOptions) => {
159
+ const accountId = normalizeAccountId(
160
+ runtime.asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
161
+ );
162
+ const reason = runtime.asString(params?.reason || '').trim() || null;
163
+ const olderThan = parseDeadLetterOlderThan(params?.olderThan);
164
+ const limit = parseDeadLetterLimit(params?.limit, 100);
165
+ const dryRun = params?.dryRun !== false;
166
+ const hasDestructiveFilter = Boolean(reason || olderThan !== null);
167
+ if (!dryRun && !hasDestructiveFilter) {
168
+ respond(false, {
169
+ ok: false,
170
+ error: 'deadLetter-prune-requires-filter',
171
+ message: 'dryRun=false requires at least one destructive filter: reason or olderThan',
172
+ dryRun,
173
+ accountId,
174
+ filters: { reason, olderThan },
175
+ summary: runtime.buildDeadLetterDiagnostics(accountId),
176
+ now: runtime.now(),
177
+ });
178
+ return;
179
+ }
180
+ const matches = runtime
181
+ .filterDeadLetterEntries({ accountId, reason, olderThan })
182
+ .slice()
183
+ .sort((a, b) => Number(a.createdAt || 0) - Number(b.createdAt || 0));
184
+ const selected = matches.slice(0, limit);
185
+ const selectedEntries = new Set(selected);
186
+
187
+ if (!dryRun && selectedEntries.size > 0) {
188
+ const nextEntries = runtime
189
+ .listDeadLetterEntries()
190
+ .filter((entry) => !selectedEntries.has(entry));
191
+ runtime.replaceDeadLetterEntries(nextEntries);
192
+ runtime.scheduleSave();
193
+ runtime.logDeadLetterSummary(accountId, { force: true, source: 'prune' });
194
+ }
195
+
196
+ respond(true, {
197
+ ok: true,
198
+ dryRun,
199
+ accountId,
200
+ filters: { reason, olderThan },
201
+ matched: matches.length,
202
+ pruned: dryRun ? 0 : selected.length,
203
+ wouldPrune: selected.length,
204
+ limit,
205
+ entries: selected.map((entry) => summarizeDeadLetterEntry(entry)),
206
+ summary: runtime.buildDeadLetterDiagnostics(accountId),
207
+ now: runtime.now(),
208
+ });
209
+ },
210
+ };
211
+ }
@@ -0,0 +1,15 @@
1
+ export function getErrorMessage(error: unknown, fallback: string): string {
2
+ if (typeof error === 'object' && error && 'message' in error) {
3
+ const message = (error as { message?: unknown }).message;
4
+ if (typeof message === 'string' && message.trim()) return message;
5
+ }
6
+
7
+ if (typeof error === 'string' && error.trim()) return error;
8
+
9
+ if (error != null) {
10
+ const rendered = String(error).trim();
11
+ if (rendered) return rendered;
12
+ }
13
+
14
+ return fallback;
15
+ }
@@ -0,0 +1,284 @@
1
+ import { buildFileAckKey } from '../core/file-ack.ts';
2
+ import { OUTBOUND_TERMINAL_REASON } from '../messaging/outbound/reasons.ts';
3
+
4
+ export type FileAckPayloadState = {
5
+ payload: Record<string, unknown>;
6
+ ok: boolean;
7
+ at: number;
8
+ };
9
+
10
+ export type FileAckWaiter = {
11
+ promise: Promise<Record<string, unknown>>;
12
+ resolve: (payload: Record<string, unknown>) => void;
13
+ reject: (err: Error) => void;
14
+ timer: NodeJS.Timeout;
15
+ };
16
+
17
+ export function createBncrFileAckRuntime(runtime: {
18
+ bridgeId: string;
19
+ now: () => number;
20
+ asString: (value: unknown, fallback?: string) => string;
21
+ clampFiniteNumber: (value: unknown, fallback: number, min?: number, max?: number) => number;
22
+ fileAckTimeoutMs: number;
23
+ maxEarlyFileAcks: number;
24
+ fileAckWaiters: Map<string, FileAckWaiter>;
25
+ earlyFileAcks: Map<string, FileAckPayloadState>;
26
+ getFileAckOwnerInfo: (transferId: string) => Record<string, unknown>;
27
+ logInfo: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
28
+ logWarn: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
29
+ }) {
30
+ function rememberEarlyFileAck(key: string, state: FileAckPayloadState) {
31
+ runtime.earlyFileAcks.set(key, state);
32
+ while (runtime.earlyFileAcks.size > runtime.maxEarlyFileAcks) {
33
+ const oldestKey = runtime.earlyFileAcks.keys().next().value;
34
+ if (!oldestKey) break;
35
+ runtime.earlyFileAcks.delete(oldestKey);
36
+ }
37
+ }
38
+
39
+ function fileAckKey(transferId: string, stage: string, chunkIndex?: number): string {
40
+ return buildFileAckKey({ transferId, stage, chunkIndex });
41
+ }
42
+
43
+ function buildFileAckWaitContext(params: {
44
+ transferId: string;
45
+ stage: string;
46
+ chunkIndex?: number;
47
+ timeoutMs?: number;
48
+ }) {
49
+ const transferId = runtime.asString(params.transferId).trim();
50
+ const stage = runtime.asString(params.stage).trim();
51
+ const chunkIndex = Number.isFinite(Number(params.chunkIndex))
52
+ ? Number(params.chunkIndex)
53
+ : undefined;
54
+ return {
55
+ transferId,
56
+ stage,
57
+ chunkIndex,
58
+ key: fileAckKey(transferId, stage, params.chunkIndex),
59
+ timeoutMs: runtime.clampFiniteNumber(
60
+ params.timeoutMs,
61
+ runtime.fileAckTimeoutMs,
62
+ 1_000,
63
+ 120_000,
64
+ ),
65
+ ownerInfo: runtime.getFileAckOwnerInfo(transferId),
66
+ };
67
+ }
68
+
69
+ function settleFileAckWaiter(
70
+ waiter: {
71
+ resolve: (payload: Record<string, unknown>) => void;
72
+ reject: (err: Error) => void;
73
+ },
74
+ payload: Record<string, unknown>,
75
+ ok: boolean,
76
+ ) {
77
+ if (ok) {
78
+ waiter.resolve(payload);
79
+ return;
80
+ }
81
+ waiter.reject(
82
+ new Error(runtime.asString(payload?.errorMessage || payload?.error || 'file ack failed')),
83
+ );
84
+ }
85
+
86
+ function consumeEarlyFileAck(ack: {
87
+ transferId: string;
88
+ stage: string;
89
+ chunkIndex?: number;
90
+ key: string;
91
+ ownerInfo: Record<string, unknown>;
92
+ }): Promise<Record<string, unknown>> | null {
93
+ const cached = runtime.earlyFileAcks.get(ack.key);
94
+ if (!cached) return null;
95
+ runtime.earlyFileAcks.delete(ack.key);
96
+ runtime.logInfo(
97
+ 'file-ack-cache-hit',
98
+ JSON.stringify({
99
+ bridge: runtime.bridgeId,
100
+ transferId: ack.transferId,
101
+ stage: ack.stage,
102
+ ackStage: ack.stage,
103
+ ackOutcome: cached.ok ? 'acked' : 'failed',
104
+ waiterReused: false,
105
+ chunkIndex: ack.chunkIndex,
106
+ key: ack.key,
107
+ ...ack.ownerInfo,
108
+ ok: cached.ok,
109
+ payload: cached.payload,
110
+ }),
111
+ { debugOnly: true },
112
+ );
113
+ if (cached.ok) return Promise.resolve(cached.payload);
114
+ return Promise.reject(
115
+ new Error(
116
+ runtime.asString(
117
+ cached.payload?.errorMessage || cached.payload?.error || 'file ack failed',
118
+ ),
119
+ ),
120
+ );
121
+ }
122
+
123
+ function waitForFileAck(params: {
124
+ transferId: string;
125
+ stage: string;
126
+ chunkIndex?: number;
127
+ timeoutMs?: number;
128
+ }) {
129
+ const ack = buildFileAckWaitContext(params);
130
+
131
+ const cached = consumeEarlyFileAck(ack);
132
+ if (cached) return cached;
133
+
134
+ const existing = runtime.fileAckWaiters.get(ack.key);
135
+ if (existing) {
136
+ runtime.logWarn(
137
+ 'file-ack-waiter-reuse',
138
+ JSON.stringify({
139
+ bridge: runtime.bridgeId,
140
+ transferId: ack.transferId,
141
+ stage: ack.stage,
142
+ ackStage: ack.stage,
143
+ ackOutcome: 'waiter-reused',
144
+ waiterReused: true,
145
+ chunkIndex: ack.chunkIndex,
146
+ key: ack.key,
147
+ ...ack.ownerInfo,
148
+ }),
149
+ { debugOnly: true },
150
+ );
151
+ return existing.promise;
152
+ }
153
+
154
+ runtime.logInfo(
155
+ 'file-ack-wait',
156
+ JSON.stringify({
157
+ bridge: runtime.bridgeId,
158
+ transferId: ack.transferId,
159
+ stage: ack.stage,
160
+ ackStage: ack.stage,
161
+ ackOutcome: 'waiting',
162
+ waiterReused: false,
163
+ chunkIndex: ack.chunkIndex,
164
+ key: ack.key,
165
+ ...ack.ownerInfo,
166
+ timeoutMs: ack.timeoutMs,
167
+ }),
168
+ { debugOnly: true },
169
+ );
170
+
171
+ let timer: NodeJS.Timeout;
172
+ let resolveWaiter!: (payload: Record<string, unknown>) => void;
173
+ let rejectWaiter!: (err: Error) => void;
174
+ const promise = new Promise<Record<string, unknown>>((resolve, reject) => {
175
+ resolveWaiter = resolve;
176
+ rejectWaiter = reject;
177
+ timer = setTimeout(() => {
178
+ runtime.fileAckWaiters.delete(ack.key);
179
+ runtime.logWarn(
180
+ OUTBOUND_TERMINAL_REASON.FILE_ACK_TIMEOUT,
181
+ JSON.stringify({
182
+ bridge: runtime.bridgeId,
183
+ transferId: ack.transferId,
184
+ stage: ack.stage,
185
+ ackStage: ack.stage,
186
+ ackOutcome: 'timeout',
187
+ waiterReused: false,
188
+ chunkIndex: ack.chunkIndex,
189
+ key: ack.key,
190
+ ...ack.ownerInfo,
191
+ timeoutMs: ack.timeoutMs,
192
+ }),
193
+ { debugOnly: true },
194
+ );
195
+ reject(new Error(`file ack timeout: ${ack.key}`));
196
+ }, ack.timeoutMs);
197
+ });
198
+ runtime.fileAckWaiters.set(ack.key, {
199
+ promise,
200
+ resolve: resolveWaiter,
201
+ reject: rejectWaiter,
202
+ timer: timer!,
203
+ });
204
+ return promise;
205
+ }
206
+
207
+ function resolveFileAck(params: {
208
+ transferId: string;
209
+ stage: string;
210
+ chunkIndex?: number;
211
+ payload: Record<string, unknown>;
212
+ ok: boolean;
213
+ }) {
214
+ const ack = buildFileAckWaitContext(params);
215
+ const waiter = runtime.fileAckWaiters.get(ack.key);
216
+ if (!waiter) {
217
+ rememberEarlyFileAck(ack.key, {
218
+ payload: params.payload,
219
+ ok: params.ok,
220
+ at: runtime.now(),
221
+ });
222
+ runtime.logInfo(
223
+ 'file-ack-early-cache',
224
+ JSON.stringify({
225
+ bridge: runtime.bridgeId,
226
+ transferId: ack.transferId,
227
+ stage: ack.stage,
228
+ ackStage: ack.stage,
229
+ ackOutcome: params.ok ? 'early-acked' : 'early-failed',
230
+ waiterReused: false,
231
+ chunkIndex: ack.chunkIndex,
232
+ key: ack.key,
233
+ ...ack.ownerInfo,
234
+ ok: params.ok,
235
+ payload: params.payload,
236
+ cached: true,
237
+ }),
238
+ { debugOnly: true },
239
+ );
240
+ return false;
241
+ }
242
+ runtime.fileAckWaiters.delete(ack.key);
243
+ clearTimeout(waiter.timer);
244
+ runtime.logInfo(
245
+ 'file-ack-resolve',
246
+ JSON.stringify({
247
+ bridge: runtime.bridgeId,
248
+ transferId: ack.transferId,
249
+ stage: ack.stage,
250
+ ackStage: ack.stage,
251
+ ackOutcome: params.ok ? 'acked' : 'failed',
252
+ waiterReused: false,
253
+ chunkIndex: ack.chunkIndex,
254
+ key: ack.key,
255
+ ...ack.ownerInfo,
256
+ ok: params.ok,
257
+ payload: params.payload,
258
+ }),
259
+ { debugOnly: true },
260
+ );
261
+ settleFileAckWaiter(waiter, params.payload, params.ok);
262
+ return true;
263
+ }
264
+
265
+ function clearAllFileAckWaiters(reason: string) {
266
+ for (const waiter of runtime.fileAckWaiters.values()) {
267
+ clearTimeout(waiter.timer);
268
+ waiter.reject(new Error(reason));
269
+ }
270
+ runtime.fileAckWaiters.clear();
271
+ runtime.earlyFileAcks.clear();
272
+ }
273
+
274
+ return {
275
+ rememberEarlyFileAck,
276
+ fileAckKey,
277
+ buildFileAckWaitContext,
278
+ consumeEarlyFileAck,
279
+ settleFileAckWaiter,
280
+ waitForFileAck,
281
+ resolveFileAck,
282
+ clearAllFileAckWaiters,
283
+ };
284
+ }
@@ -0,0 +1,112 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrFileInboundRuntime } from './file-inbound-runtime.ts';
3
+ import {
4
+ isAbortedInboundTransfer,
5
+ isCompletedInboundTransfer,
6
+ markInboundTransferAborted,
7
+ } from './file-inbound-state.ts';
8
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
9
+
10
+ export function createBncrFileInboundAbortHandler(runtime: BncrFileInboundRuntime) {
11
+ return async function handleFileAbort({
12
+ params,
13
+ respond,
14
+ client,
15
+ context,
16
+ }: GatewayRequestHandlerOptions) {
17
+ const gatewayContext = buildBncrGatewayEventContext({
18
+ params,
19
+ client,
20
+ context,
21
+ asString: runtime.asString,
22
+ normalizeAccountId: runtime.normalizeAccountId,
23
+ now: runtime.now,
24
+ });
25
+ const { accountId, connId, clientId } = gatewayContext;
26
+
27
+ const transferId = runtime.asString(params?.transferId || '').trim();
28
+ if (!transferId) {
29
+ respond(false, { error: 'transferId required' });
30
+ return;
31
+ }
32
+
33
+ const st = runtime.fileRecvTransfers.get(transferId);
34
+ if (!st) {
35
+ respond(true, { ok: true, transferId, message: 'not-found' });
36
+ return;
37
+ }
38
+ if (isCompletedInboundTransfer(st)) {
39
+ respond(true, {
40
+ ok: true,
41
+ transferId,
42
+ status: 'completed',
43
+ path: st.completedPath,
44
+ ignored: true,
45
+ terminal: true,
46
+ });
47
+ return;
48
+ }
49
+ if (isAbortedInboundTransfer(st)) {
50
+ respond(true, {
51
+ ok: true,
52
+ transferId,
53
+ status: 'aborted',
54
+ error: st.error,
55
+ ignored: true,
56
+ terminal: true,
57
+ });
58
+ return;
59
+ }
60
+
61
+ const staleObserved = runtime.observeLease('file.abort', params ?? {});
62
+ if (staleObserved.stale) {
63
+ if (
64
+ !runtime.matchesTransferOwner({
65
+ ownerConnId: st.ownerConnId,
66
+ ownerClientId: st.ownerClientId,
67
+ connId,
68
+ clientId,
69
+ })
70
+ ) {
71
+ runtime.logWarn(
72
+ 'stale',
73
+ `ignore kind=file.abort accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} reason=owner-mismatch ownerConnId=${st.ownerConnId || '-'} ownerClientId=${st.ownerClientId || '-'}`,
74
+ { debugOnly: true },
75
+ );
76
+ respond(true, { ok: true, stale: true, ignored: true });
77
+ return;
78
+ }
79
+ } else {
80
+ runtime.refreshAcceptedFileTransferLiveState({
81
+ accountId,
82
+ connId,
83
+ clientId,
84
+ context: gatewayContext.context,
85
+ });
86
+ }
87
+
88
+ const abortedState = markInboundTransferAborted(
89
+ st,
90
+ runtime.asString(params?.reason || 'aborted'),
91
+ runtime.now(),
92
+ );
93
+ runtime.fileRecvTransfers.set(transferId, abortedState);
94
+
95
+ respond(
96
+ true,
97
+ staleObserved.stale
98
+ ? {
99
+ ok: true,
100
+ transferId,
101
+ status: 'aborted',
102
+ stale: true,
103
+ staleAccepted: true,
104
+ }
105
+ : {
106
+ ok: true,
107
+ transferId,
108
+ status: 'aborted',
109
+ },
110
+ );
111
+ };
112
+ }