@evomap/evolver-proxy 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +49 -0
  2. package/dist/bin/envFile.d.ts +10 -0
  3. package/dist/bin/envFile.js +68 -0
  4. package/dist/bin/evolver-llm-proxy.d.ts +2 -0
  5. package/dist/bin/evolver-llm-proxy.js +111 -0
  6. package/dist/bin/evolver-proxy.d.ts +83 -0
  7. package/dist/bin/evolver-proxy.js +511 -0
  8. package/dist/bin/proxySettings.d.ts +15 -0
  9. package/dist/bin/proxySettings.js +84 -0
  10. package/dist/bin/proxyStorePath.d.ts +1 -0
  11. package/dist/bin/proxyStorePath.js +16 -0
  12. package/dist/daemon/atpConsent.d.ts +13 -0
  13. package/dist/daemon/atpConsent.js +60 -0
  14. package/dist/daemon/ipcConfig.d.ts +1 -0
  15. package/dist/daemon/ipcConfig.js +13 -0
  16. package/dist/daemon/proxyDaemon.d.ts +191 -0
  17. package/dist/daemon/proxyDaemon.js +1015 -0
  18. package/dist/daemon/selectHub.d.ts +16 -0
  19. package/dist/daemon/selectHub.js +30 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.js +8 -0
  22. package/dist/lifecycle/deployGuard.d.ts +46 -0
  23. package/dist/lifecycle/deployGuard.js +53 -0
  24. package/dist/lifecycle/legacyNodeId.d.ts +96 -0
  25. package/dist/lifecycle/legacyNodeId.js +163 -0
  26. package/dist/lifecycle/manager.d.ts +106 -0
  27. package/dist/lifecycle/manager.js +390 -0
  28. package/dist/llm/bodyCapture.d.ts +31 -0
  29. package/dist/llm/bodyCapture.js +293 -0
  30. package/dist/llm/index.d.ts +3 -0
  31. package/dist/llm/index.js +3 -0
  32. package/dist/llm/server.d.ts +35 -0
  33. package/dist/llm/server.js +359 -0
  34. package/dist/llm/traceBackfill.d.ts +53 -0
  35. package/dist/llm/traceBackfill.js +525 -0
  36. package/dist/llm/traceConfig.d.ts +7 -0
  37. package/dist/llm/traceConfig.js +44 -0
  38. package/dist/llm/traceControl.d.ts +16 -0
  39. package/dist/llm/traceControl.js +85 -0
  40. package/dist/llm/traceEnvelope.d.ts +75 -0
  41. package/dist/llm/traceEnvelope.js +286 -0
  42. package/dist/llm/traceSink.d.ts +61 -0
  43. package/dist/llm/traceSink.js +278 -0
  44. package/dist/llm/traceUploadPayload.d.ts +14 -0
  45. package/dist/llm/traceUploadPayload.js +27 -0
  46. package/dist/llm/upstream.d.ts +68 -0
  47. package/dist/llm/upstream.js +491 -0
  48. package/dist/private/adapterLoader.d.ts +46 -0
  49. package/dist/private/adapterLoader.js +62 -0
  50. package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
  51. package/dist/private/privateRuntimeSmokeOptions.js +132 -0
  52. package/dist/router/cachePassthrough.d.ts +3 -0
  53. package/dist/router/cachePassthrough.js +13 -0
  54. package/dist/router/features.d.ts +9 -0
  55. package/dist/router/features.js +52 -0
  56. package/dist/router/index.d.ts +6 -0
  57. package/dist/router/index.js +6 -0
  58. package/dist/router/messagesRoute.d.ts +138 -0
  59. package/dist/router/messagesRoute.js +753 -0
  60. package/dist/router/modelRouter.d.ts +49 -0
  61. package/dist/router/modelRouter.js +66 -0
  62. package/dist/router/providerRoutes.d.ts +42 -0
  63. package/dist/router/providerRoutes.js +1579 -0
  64. package/dist/router/sseScan.d.ts +56 -0
  65. package/dist/router/sseScan.js +543 -0
  66. package/dist/selfUpdate/executor.d.ts +90 -0
  67. package/dist/selfUpdate/executor.js +179 -0
  68. package/dist/selfUpdate/failureCodes.d.ts +33 -0
  69. package/dist/selfUpdate/failureCodes.js +84 -0
  70. package/dist/selfUpdate/index.d.ts +5 -0
  71. package/dist/selfUpdate/index.js +5 -0
  72. package/dist/selfUpdate/lastUpdate.d.ts +43 -0
  73. package/dist/selfUpdate/lastUpdate.js +195 -0
  74. package/dist/selfUpdate/policy.d.ts +3 -0
  75. package/dist/selfUpdate/policy.js +9 -0
  76. package/dist/selfUpdate/releaseBinary.d.ts +56 -0
  77. package/dist/selfUpdate/releaseBinary.js +498 -0
  78. package/dist/selfUpdate/version.d.ts +2 -0
  79. package/dist/selfUpdate/version.js +14 -0
  80. package/dist/sync/engine.d.ts +65 -0
  81. package/dist/sync/engine.js +461 -0
  82. package/package.json +41 -0
@@ -0,0 +1,1015 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { mailbox, hub as hubNs, shadow as shadow_, assetstore } from '@evomap/evolver-core';
3
+ import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
4
+ import { LifecycleManager } from '../lifecycle/manager.js';
5
+ import { executeForceUpdate } from '../selfUpdate/executor.js';
6
+ import { reportPendingSelfUpdateLastUpdate, reportSelfUpdateLastUpdate } from '../selfUpdate/lastUpdate.js';
7
+ import { backfillProxyTraceUploads } from '../llm/traceBackfill.js';
8
+ import { hubAuthFailureHint } from './selectHub.js';
9
+ export const DEFAULT_IPC_PORT = 19820;
10
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
11
+ const MAX_PROXY_TICK_ERROR_LENGTH = 2_000;
12
+ const MAX_HEARTBEAT_TICK_ERROR_LENGTH = 1_000;
13
+ /**
14
+ * ProxyDaemon(M6-4) 装配层: 把 core(MailboxStore/Dispatcher/MailboxDaemon/IpcServer) +
15
+ * HubBindings(M6-1) + SyncEngine(M6-2) + LifecycleManager(M6-3) 拼成系统级 proxy.
16
+ * 职责分工(避免双 claim): MailboxDaemon 只 pump 'core'(本地确定性); proxy 出站归 SyncEngine.syncOutbound;
17
+ * inbound 由 SyncEngine.syncInbound 从 hub 拉; agent 消息留给 runtime 经 IPC claim.
18
+ */
19
+ export class ProxyDaemon {
20
+ deps;
21
+ store;
22
+ dispatcher;
23
+ daemon;
24
+ sync;
25
+ lifecycle;
26
+ assetStore;
27
+ remoteAssetById;
28
+ reuseResultReporter;
29
+ validator;
30
+ atp;
31
+ ipc;
32
+ now;
33
+ random;
34
+ nextHeartbeatAt;
35
+ heartbeatFailures = 0;
36
+ heartbeatGeneration = 0;
37
+ /** Resolver for an in-flight runner sleep(); set while sleeping, called to wake early on poke. */
38
+ wakeRunnerResolve;
39
+ /** A poke that arrived between ticks (no sleep in flight) parks the wake here so it is not lost. */
40
+ wakeRunnerPending = false;
41
+ started = false;
42
+ storeClosed = false;
43
+ forceUpdateTriggerInFlight = false;
44
+ forceUpdateLastTriggeredAt;
45
+ forceUpdateLastTriggeredKey;
46
+ pendingForceUpdateDirective;
47
+ forceUpdateTimer;
48
+ scheduledForceUpdateKey;
49
+ traceBackfillDraining = false;
50
+ loopWakeHandler;
51
+ constructor(deps) {
52
+ this.deps = deps;
53
+ this.now = deps.now ?? (() => Date.now());
54
+ this.random = deps.random ?? Math.random;
55
+ if (!deps.store && !deps.storePath)
56
+ throw new Error('ProxyDaemon: 需 store 或 storePath 之一');
57
+ const shadow = deps.shadowMode === 'shadow';
58
+ if (shadow && !deps.shadowSink)
59
+ throw new Error('ProxyDaemon: shadow 模式需 shadowSink');
60
+ // M8 shadow 装配: 在边界包 decorator, 下游 makeHubBindings/Dispatcher/SyncEngine/MailboxDaemon 零改.
61
+ this.store = deps.store
62
+ ?? (shadow ? new shadow_.ShadowMailboxStore({ path: deps.storePath }, deps.shadowSink, 'shadow') : new mailbox.MailboxStore({ path: deps.storePath }));
63
+ const assetStoreDir = deps.assetStoreDir ?? (deps.storePath ? join(dirname(deps.storePath), 'assets') : undefined);
64
+ this.assetStore = deps.assetStore ?? (assetStoreDir ? new assetstore.LocalJsonlProvider(assetStoreDir) : undefined);
65
+ this.atp = deps.atp;
66
+ const hubToUse = shadow ? shadow_.shadowHubCapability(deps.hub, deps.shadowSink, 'shadow') : deps.hub;
67
+ const hubBindings = hubNs.makeHubBindings(hubToUse);
68
+ const proxyHandler = hubBindings.asProxyHandler();
69
+ const assetByIdSource = isAssetByIdFetcher(deps.hub) ? deps.hub : (isAssetByIdFetcher(hubToUse) ? hubToUse : undefined);
70
+ this.remoteAssetById = assetByIdSource
71
+ ? async (assetId) => {
72
+ const fetched = await assetByIdSource.fetchAssetById(assetId);
73
+ return assetMatchesId(fetched, assetId) ? fetched : null;
74
+ }
75
+ : undefined;
76
+ this.reuseResultReporter = isReuseResultReporter(hubToUse)
77
+ ? hubToUse
78
+ : (!shadow && isReuseResultReporter(deps.hub) ? deps.hub : undefined);
79
+ // validate() makes a LIVE POST /a2a/validate to the real hub (content-safety scan over the asset
80
+ // bundle) — it is the dry-run for a publish that shadow suppresses, so it is disabled under shadow
81
+ // like recordReuseResult and degrades to { valid:false, reason:'validate_not_configured' }.
82
+ this.validator = isValidator(hubToUse)
83
+ ? hubToUse
84
+ : (!shadow && isValidator(deps.hub) ? deps.hub : undefined);
85
+ // proxy handler 装进 Dispatcher 仅供完整性; daemon 只 pump core, 实际出站走 SyncEngine.
86
+ this.dispatcher = new mailbox.Dispatcher({
87
+ store: this.store,
88
+ handlers: { core: (e) => this.handleCore(e), proxy: proxyHandler, agent: () => ({}) },
89
+ now: this.now,
90
+ });
91
+ this.daemon = new mailbox.MailboxDaemon({
92
+ store: this.store, dispatcher: this.dispatcher, now: this.now,
93
+ pumpHandlers: ['core'], // proxy 出站归 SyncEngine, 不在此双 claim
94
+ ...(deps.lockPath ? { lockPath: deps.lockPath } : {}),
95
+ });
96
+ this.sync = new SyncEngine({
97
+ store: this.store, hub: hubToUse, proxyHandler, now: this.now,
98
+ ...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
99
+ ...(deps.traceBackfill ? { onOutboundFlushed: () => { this.drainProxyTraceBackfill(); } } : {}),
100
+ });
101
+ this.lifecycle = new LifecycleManager({
102
+ store: this.store, auth: hubToUse.auth, hello: deps.hello, heartbeat: deps.heartbeat, now: this.now,
103
+ ...(deps.evolverVersion ? { evolverVersion: deps.evolverVersion } : {}),
104
+ ...(deps.helloMode ? { helloMode: deps.helloMode } : {}),
105
+ onForceUpdateDirective: (directive, source) => { this.triggerForceUpdateFromHeartbeat(directive, source); },
106
+ });
107
+ this.nextHeartbeatAt = this.now();
108
+ }
109
+ /**
110
+ * core handler(确定性, 不经 agent): 目前只接 force_update(#108). 其他 core 类型(asset_publish_result/
111
+ * feature_flag_update)仍由 Material/上层处理, 这里 no-op 标完成. force_update 仅当装配了 selfUpdate 才执行;
112
+ * 否则只标完成(不下载/不重启 — 默认 OFF 风险闸). 永不抛: 失败转结构化 telemetry, daemon 续跑旧版本.
113
+ */
114
+ async handleCore(e) {
115
+ if (e.type !== 'force_update')
116
+ return {};
117
+ if (!this.deps.selfUpdate)
118
+ return { ok: false, reason: 'self_update_not_configured' };
119
+ const directive = (e.payload ?? {});
120
+ return this.executeAndReportForceUpdate(directive);
121
+ }
122
+ recordTickError(phase, err) {
123
+ const authLike = isAuthLikeError(err);
124
+ // #314: surface the actionable hint on an auth failure, keyed off the hub error code so it is not silent and
125
+ // not misdirected (a2a_auth_required => private hub; other auth error => credential problem).
126
+ const hint = authLike ? hubAuthFailureHint(process.env, errorMessage(err)) : '';
127
+ const message = safeDaemonMessage(`${phase}_tick: ${errorMessage(err)}${hint ? `. ${hint}` : ''}`, MAX_PROXY_TICK_ERROR_LENGTH);
128
+ try {
129
+ this.store.setState('sync:last_error', message);
130
+ if (authLike)
131
+ this.store.setState('hub:auth_status', 'auth_failed');
132
+ }
133
+ catch {
134
+ // Telemetry write failures must not break tick phase isolation.
135
+ }
136
+ return message;
137
+ }
138
+ /** 启动: 锁 + IPC 监听 + 初次 hello. 返回 IPC 端口. */
139
+ async start() {
140
+ if (this.started)
141
+ throw new Error('ProxyDaemon 已启动');
142
+ try {
143
+ this.daemon.start();
144
+ this.ipc = new mailbox.MailboxIpcServer({
145
+ store: this.store, token: this.deps.ipcToken,
146
+ ...(this.deps.ipcHost ? { host: this.deps.ipcHost } : {}), now: this.now,
147
+ onSend: (env, result) => {
148
+ if (result.stored && env.handler === 'proxy')
149
+ this.notifyNewOutbound();
150
+ },
151
+ ...(this.deps.onIpcAuthFailure ? { onAuthFailure: this.deps.onIpcAuthFailure } : {}),
152
+ extraRoutes: [(ctx) => this.handleProxyRoute(ctx)],
153
+ });
154
+ const port = await this.ipc.listen(this.deps.ipcPort ?? DEFAULT_IPC_PORT);
155
+ try {
156
+ this.deps.onIpcListen?.(port);
157
+ }
158
+ catch { /* local discovery publishing must not block daemon startup */ }
159
+ await this.lifecycle.doHello();
160
+ this.drainProxyTraceBackfill();
161
+ this.started = true;
162
+ return port;
163
+ }
164
+ catch (err) {
165
+ await this.closeIpcBestEffort();
166
+ try {
167
+ await this.daemon.stop();
168
+ }
169
+ catch { /* best-effort cleanup */ }
170
+ try {
171
+ this.closeStoreOnce();
172
+ }
173
+ catch { /* best-effort cleanup */ }
174
+ this.started = false;
175
+ throw err;
176
+ }
177
+ }
178
+ /** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
179
+ async tick() {
180
+ const errors = [];
181
+ const finalErrors = [];
182
+ const addFailure = (phase, message) => {
183
+ errors.push({ phase, message });
184
+ finalErrors.push(message);
185
+ };
186
+ try {
187
+ await this.daemon.tick(); // core + TTL + wake
188
+ }
189
+ catch (err) {
190
+ addFailure('core', this.recordTickError('core', err));
191
+ }
192
+ let outbound = emptyOutboundResult();
193
+ let heartbeat;
194
+ try {
195
+ outbound = await this.sync.syncOutbound();
196
+ if (outbound.authFailed) {
197
+ // #314: carry the engine's specific auth error (e.g. a2a_auth_required) up here so the operator-visible
198
+ // sync:last_error shows the real code + the correctly-keyed hint, not a generic "auth_failure".
199
+ const detail = outbound.authErrorMessage ?? 'auth_failure';
200
+ const authHint = hubAuthFailureHint(process.env, detail);
201
+ const outboundError = safeDaemonMessage(`outbound_tick: ${detail}${authHint ? `. ${authHint}` : ''}`, MAX_PROXY_TICK_ERROR_LENGTH);
202
+ try {
203
+ const generation = this.heartbeatGeneration;
204
+ const reauthed = await this.lifecycle.reauthenticate();
205
+ if (reauthed) {
206
+ heartbeat = { ok: false, reauthed: true };
207
+ this.recordHeartbeatResult(heartbeat, generation);
208
+ }
209
+ else {
210
+ addFailure('outbound', outboundError);
211
+ }
212
+ }
213
+ catch (reauthErr) {
214
+ addFailure('outbound', outboundError);
215
+ addFailure('heartbeat', this.recordTickError('heartbeat', reauthErr));
216
+ }
217
+ }
218
+ }
219
+ catch (err) {
220
+ addFailure('outbound', this.recordTickError('outbound', err));
221
+ }
222
+ let inbound = emptyInboundResult();
223
+ try {
224
+ inbound = await this.sync.syncInbound();
225
+ }
226
+ catch (err) {
227
+ const inboundError = this.recordTickError('inbound', err);
228
+ if (isAuthLikeError(err)) {
229
+ try {
230
+ const generation = this.heartbeatGeneration;
231
+ const reauthed = await this.lifecycle.reauthenticate();
232
+ if (reauthed) {
233
+ heartbeat = { ok: false, reauthed: true };
234
+ this.recordHeartbeatResult(heartbeat, generation);
235
+ }
236
+ else {
237
+ addFailure('inbound', inboundError);
238
+ }
239
+ }
240
+ catch (reauthErr) {
241
+ addFailure('inbound', inboundError);
242
+ addFailure('heartbeat', this.recordTickError('heartbeat', reauthErr));
243
+ }
244
+ }
245
+ else {
246
+ addFailure('inbound', inboundError);
247
+ }
248
+ }
249
+ if (!heartbeat && this.now() >= this.nextHeartbeatAt) {
250
+ const generation = this.heartbeatGeneration;
251
+ try {
252
+ heartbeat = await this.lifecycle.doHeartbeat();
253
+ }
254
+ catch (err) {
255
+ const message = safeHeartbeatTickErrorMessage(err);
256
+ heartbeat = { ok: false, reauthed: false, error: message };
257
+ this.recordHeartbeatTickException(message);
258
+ addFailure('heartbeat', `heartbeat_tick_exception:${message}`);
259
+ }
260
+ this.recordHeartbeatResult(heartbeat, generation);
261
+ }
262
+ if (finalErrors.length > 0) {
263
+ try {
264
+ this.store.setState('sync:last_error', safeDaemonMessage(finalErrors.join('; '), MAX_PROXY_TICK_ERROR_LENGTH));
265
+ }
266
+ catch { /* ignore telemetry persistence failures */ }
267
+ }
268
+ const failedPhases = uniqueTickPhases(errors.map((err) => err.phase));
269
+ return {
270
+ outbound,
271
+ inbound,
272
+ ...(heartbeat ? { heartbeat } : {}),
273
+ ...(errors.length > 0 ? { errors, failedPhases, fatalCandidate: isFatalTickCandidate(outbound, inbound, failedPhases) } : { failedPhases: [], fatalCandidate: false }),
274
+ };
275
+ }
276
+ /** 下一轮建议延时: inbound 背压/idle 与 outbound pending cadence 取更快者. */
277
+ nextDelay(last) {
278
+ const inbound = this.sync.nextInboundDelay(last);
279
+ const outbound = this.sync.nextOutboundDelay();
280
+ const hubDirected = last.hasMore || last.nextPollAfterMs !== undefined;
281
+ const syncDelay = outbound !== SYNC_INTERVALS.outboundPending || hubDirected
282
+ ? inbound
283
+ : Math.min(inbound, outbound);
284
+ const heartbeatDelay = Math.max(0, this.nextHeartbeatAt - this.now());
285
+ return Math.min(syncDelay, heartbeatDelay);
286
+ }
287
+ setWakeHandler(wake) {
288
+ this.loopWakeHandler = wake;
289
+ }
290
+ notifyNewOutbound() {
291
+ if (this.loopWakeHandler) {
292
+ this.loopWakeHandler();
293
+ return;
294
+ }
295
+ this.wakeRunner();
296
+ }
297
+ /**
298
+ * Expedite the next heartbeat: clear the failure backoff, mark the heartbeat due now, and wake an
299
+ * in-flight runner sleep() so the next tick runs immediately. Wake-on-event for the pull-based
300
+ * loop — the interruptible sleep() lets this preempt a long backoff wait the way V1's timer-driven
301
+ * loop did (which armed a 0ms timer). The generation bump prevents a tick that was already in
302
+ * flight from overwriting this reschedule. No-op until the daemon is started.
303
+ */
304
+ pokeHeartbeatLoop() {
305
+ if (!this.started)
306
+ return;
307
+ this.heartbeatGeneration += 1;
308
+ this.heartbeatFailures = 0;
309
+ this.nextHeartbeatAt = this.now();
310
+ this.wakeRunner();
311
+ }
312
+ /**
313
+ * Interruptible delay for the resident runner loop (bin/evolver-proxy.ts): resolves after `ms`, OR
314
+ * immediately when pokeHeartbeatLoop() fires while sleeping. A poke that lands between ticks (before
315
+ * the next sleep starts) sets wakeRunnerPending so the wake is not lost. The timer is unref'd so it
316
+ * never keeps the process alive on its own.
317
+ */
318
+ async sleep(ms) {
319
+ if (this.wakeRunnerPending) {
320
+ this.wakeRunnerPending = false;
321
+ return;
322
+ }
323
+ return new Promise((resolve) => {
324
+ const timer = setTimeout(() => {
325
+ this.wakeRunnerResolve = undefined;
326
+ resolve();
327
+ }, ms);
328
+ if (typeof timer.unref === 'function')
329
+ timer.unref();
330
+ this.wakeRunnerResolve = () => {
331
+ clearTimeout(timer);
332
+ this.wakeRunnerResolve = undefined;
333
+ resolve();
334
+ };
335
+ });
336
+ }
337
+ wakeRunner() {
338
+ if (this.wakeRunnerResolve)
339
+ this.wakeRunnerResolve();
340
+ else
341
+ this.wakeRunnerPending = true;
342
+ }
343
+ health() {
344
+ return {
345
+ running: this.started,
346
+ ipcListening: !!this.ipc,
347
+ ...(this.lifecycle.nodeId ? { nodeId: this.lifecycle.nodeId } : {}),
348
+ lastWriteAt: this.daemon.lastWriteAt(),
349
+ };
350
+ }
351
+ async stop() {
352
+ if (this.forceUpdateTimer) {
353
+ clearTimeout(this.forceUpdateTimer);
354
+ this.forceUpdateTimer = undefined;
355
+ this.scheduledForceUpdateKey = undefined;
356
+ }
357
+ // Release a runner blocked on sleep() so shutdown doesn't wait out a long delay.
358
+ this.wakeRunnerPending = false;
359
+ if (this.wakeRunnerResolve)
360
+ this.wakeRunnerResolve();
361
+ let stopError;
362
+ try {
363
+ await this.closeIpc();
364
+ }
365
+ catch (err) {
366
+ stopError = stopError ?? err;
367
+ }
368
+ try {
369
+ await this.daemon.stop();
370
+ }
371
+ catch (err) {
372
+ stopError = stopError ?? err;
373
+ }
374
+ try {
375
+ this.closeStoreOnce();
376
+ }
377
+ catch (err) {
378
+ stopError = stopError ?? err;
379
+ }
380
+ this.started = false;
381
+ if (stopError)
382
+ throw stopError;
383
+ }
384
+ async closeIpc() {
385
+ const ipc = this.ipc;
386
+ this.ipc = undefined;
387
+ if (!ipc)
388
+ return;
389
+ await ipc.close();
390
+ }
391
+ async closeIpcBestEffort() {
392
+ try {
393
+ await this.closeIpc();
394
+ }
395
+ catch { /* best-effort cleanup */ }
396
+ }
397
+ closeStoreOnce() {
398
+ if (this.storeClosed)
399
+ return;
400
+ this.store.close();
401
+ this.storeClosed = true;
402
+ }
403
+ drainProxyTraceBackfill() {
404
+ const cfg = this.deps.traceBackfill;
405
+ const empty = {
406
+ scanned: 0,
407
+ queued: 0,
408
+ duplicates: 0,
409
+ skipped: 0,
410
+ files: 0,
411
+ reasons: {},
412
+ };
413
+ if (!cfg)
414
+ return empty;
415
+ if (this.traceBackfillDraining)
416
+ return { ...empty, deferred: true };
417
+ this.traceBackfillDraining = true;
418
+ try {
419
+ const stats = backfillProxyTraceUploads({
420
+ dir: cfg.dir,
421
+ store: this.store,
422
+ ...(cfg.env ? { env: cfg.env } : {}),
423
+ now: this.now,
424
+ ...(this.deps.runtimeNamespace ? { runtimeNamespace: this.deps.runtimeNamespace } : {}),
425
+ });
426
+ if (stats.queued > 0)
427
+ this.store.setState('llm_trace_backfill:last_queued', String(stats.queued));
428
+ return stats;
429
+ }
430
+ catch (err) {
431
+ this.store.setState('llm_trace_backfill:last_error', safeDaemonErrorMessage(err, MAX_PROXY_TICK_ERROR_LENGTH));
432
+ return { ...empty, skipped: 1, reasons: { thrown: 1 } };
433
+ }
434
+ finally {
435
+ this.traceBackfillDraining = false;
436
+ }
437
+ }
438
+ recordHeartbeatResult(result, generation) {
439
+ // A poke (pokeHeartbeatLoop) bumps heartbeatGeneration; a tick that began under an older
440
+ // generation has been superseded and must touch neither the failure count nor the schedule.
441
+ if (generation !== this.heartbeatGeneration)
442
+ return;
443
+ // V1 parity (lifecycle/manager.js _consecutiveFailures): every heartbeat failure backs off, not
444
+ // only thrown ones. The adapter maps non-auth 4xx (400/429/...) and unknown_node to a structured
445
+ // { ok:false } result with NO `error` field; the previous `!result.error` predicate left
446
+ // heartbeatFailures at 0 and pinned the cadence at the base interval forever — worst for 429,
447
+ // where the hub is rate-limiting and we kept hammering. A successful reauth (reauthed) is a
448
+ // recovery and resets, matching V1's 'recovered' branch. Hub-unreachable failures are diverted
449
+ // before this point and own their hubUnreachableUntil backoff, which nextHeartbeatDelay()
450
+ // prioritizes, so counting them here is harmless.
451
+ if (result.ok || result.reauthed) {
452
+ this.heartbeatFailures = 0;
453
+ }
454
+ else {
455
+ this.heartbeatFailures += 1;
456
+ }
457
+ this.nextHeartbeatAt = this.now() + this.lifecycle.nextHeartbeatDelay(this.heartbeatFailures);
458
+ }
459
+ recordHeartbeatTickException(message) {
460
+ try {
461
+ this.store.setState('sync:last_error', safeDaemonMessage(`heartbeat_tick_exception:${message}`, MAX_HEARTBEAT_TICK_ERROR_LENGTH));
462
+ }
463
+ catch {
464
+ // The daemon loop must keep running even if telemetry persistence is broken.
465
+ }
466
+ }
467
+ async executeAndReportForceUpdate(directive) {
468
+ if (!this.deps.selfUpdate)
469
+ return { ok: false, reason: 'self_update_not_configured' };
470
+ const currentVersion = this.deps.selfUpdate.currentVersion ?? this.deps.evolverVersion ?? '0.0.0';
471
+ const originalTelemetry = this.deps.selfUpdate.onTelemetry;
472
+ const selfUpdateDeps = {
473
+ ...this.deps.selfUpdate,
474
+ currentVersion,
475
+ onTelemetry: (result) => {
476
+ reportSelfUpdateLastUpdate(this.store, directive, result, {
477
+ fromVersion: currentVersion,
478
+ now: this.now(),
479
+ });
480
+ originalTelemetry?.(result);
481
+ },
482
+ };
483
+ return executeForceUpdate(directive, selfUpdateDeps);
484
+ }
485
+ triggerForceUpdateFromHeartbeat(directive, source) {
486
+ if (!this.deps.selfUpdate)
487
+ return;
488
+ const now = this.now();
489
+ const cooldownMs = forceUpdateRetryCooldownMs(process.env);
490
+ const key = forceUpdateDirectiveKey(directive);
491
+ if (this.forceUpdateTriggerInFlight) {
492
+ if (key !== this.forceUpdateLastTriggeredKey)
493
+ this.pendingForceUpdateDirective = { directive, source };
494
+ return;
495
+ }
496
+ if (this.scheduledForceUpdateKey) {
497
+ if (source === 'heartbeat_426') {
498
+ if (this.forceUpdateTimer)
499
+ clearTimeout(this.forceUpdateTimer);
500
+ this.forceUpdateTimer = undefined;
501
+ this.scheduledForceUpdateKey = undefined;
502
+ }
503
+ else {
504
+ if (key !== this.scheduledForceUpdateKey)
505
+ this.pendingForceUpdateDirective = { directive, source };
506
+ return;
507
+ }
508
+ }
509
+ if (source !== 'heartbeat_426'
510
+ && this.forceUpdateLastTriggeredAt !== undefined
511
+ && this.forceUpdateLastTriggeredKey === key
512
+ && now - this.forceUpdateLastTriggeredAt < cooldownMs)
513
+ return;
514
+ const delayMs = source === 'heartbeat_200' ? forceUpdateScheduleDelayMs(directive, this.random) : 0;
515
+ if (delayMs > 0) {
516
+ this.forceUpdateLastTriggeredAt = now;
517
+ this.forceUpdateLastTriggeredKey = key;
518
+ this.scheduledForceUpdateKey = key;
519
+ this.reportPendingForceUpdate(directive);
520
+ this.forceUpdateTimer = setTimeout(() => {
521
+ this.forceUpdateTimer = undefined;
522
+ this.scheduledForceUpdateKey = undefined;
523
+ this.startForceUpdateExecution(directive, key);
524
+ }, delayMs);
525
+ return;
526
+ }
527
+ this.startForceUpdateExecution(directive, key);
528
+ }
529
+ startForceUpdateExecution(directive, key) {
530
+ this.forceUpdateTriggerInFlight = true;
531
+ this.forceUpdateLastTriggeredAt = this.now();
532
+ this.forceUpdateLastTriggeredKey = key;
533
+ void this.executeAndReportForceUpdate(directive).finally(() => {
534
+ this.forceUpdateTriggerInFlight = false;
535
+ const pending = this.pendingForceUpdateDirective;
536
+ this.pendingForceUpdateDirective = undefined;
537
+ if (pending)
538
+ this.triggerForceUpdateFromHeartbeat(pending.directive, pending.source);
539
+ });
540
+ }
541
+ reportPendingForceUpdate(directive) {
542
+ if (!this.deps.selfUpdate)
543
+ return;
544
+ const currentVersion = this.deps.selfUpdate.currentVersion ?? this.deps.evolverVersion ?? '0.0.0';
545
+ reportPendingSelfUpdateLastUpdate(this.store, directive, {
546
+ fromVersion: currentVersion,
547
+ now: this.now(),
548
+ });
549
+ }
550
+ stateNumber(key) {
551
+ const raw = this.store.getState(key);
552
+ if (!raw)
553
+ return null;
554
+ const n = Number(raw);
555
+ return Number.isFinite(n) && n > 0 ? n : null;
556
+ }
557
+ async handleProxyRoute(ctx) {
558
+ const handledAtp = await this.handleAtpRoute(ctx);
559
+ if (handledAtp)
560
+ return true;
561
+ if (ctx.route === 'GET /proxy/status') {
562
+ ctx.json(200, {
563
+ running: true,
564
+ node_id: this.lifecycle.nodeId ?? null,
565
+ outbound_pending: this.store.countPending('proxy', this.deps.runtimeNamespace),
566
+ inbound_pending: this.store.countPending('agent', this.deps.runtimeNamespace) + this.store.countPending('core', this.deps.runtimeNamespace),
567
+ last_sync_at: this.store.getState('sync:last_sync_at') ?? null,
568
+ last_sync_error: this.store.getState('sync:last_error') || null,
569
+ hub_auth_status: this.store.getState('hub:auth_status') || null,
570
+ reauth_backoff_until: this.stateNumber('lifecycle:reauth_until'),
571
+ hello_rate_limit_until: this.stateNumber('lifecycle:hello_rl_until'),
572
+ });
573
+ return true;
574
+ }
575
+ if (ctx.route === 'POST /mailbox/poll') {
576
+ const body = (await ctx.readJson());
577
+ const limit = Math.max(1, Math.min(Number(body.limit ?? 10), 50));
578
+ const messages = this.store.list({ status: 'pending', limit: 500 })
579
+ .filter((m) => (body.type ? m.type === body.type : true))
580
+ .filter((m) => (body.direction ? m.direction === body.direction : true))
581
+ .slice(0, limit);
582
+ ctx.json(200, { messages, count: messages.length });
583
+ return true;
584
+ }
585
+ if (ctx.route === 'POST /asset/search') {
586
+ const body = (await ctx.readJson());
587
+ const limit = Math.max(1, Math.min(Number(body.limit ?? 5), 25));
588
+ const rawSignals = Array.isArray(body.signals) ? body.signals : body.signalsAny;
589
+ const signalsAny = Array.isArray(rawSignals) ? rawSignals.filter((s) => typeof s === 'string') : undefined;
590
+ const kind = assetKind(body.kind);
591
+ const query = {
592
+ ...(signalsAny && signalsAny.length > 0 ? { signalsAny } : {}),
593
+ ...(typeof body.text === 'string' ? { text: body.text } : {}),
594
+ ...(kind ? { kind } : {}),
595
+ ...(typeof body.category === 'string' ? { category: body.category } : {}),
596
+ ...(typeof body.gene === 'string' ? { gene: body.gene } : {}),
597
+ limit,
598
+ };
599
+ if (kind === 'AntiGene' && !this.assetStore) {
600
+ ctx.json(200, { results: [], assets: [], query: body });
601
+ return true;
602
+ }
603
+ const results = await this.searchAssets(query);
604
+ ctx.json(200, { results, assets: results, query: body });
605
+ return true;
606
+ }
607
+ if (ctx.route === 'POST /asset/fetch') {
608
+ const body = (await ctx.readJson());
609
+ const ids = uniqueStrings([
610
+ ...(Array.isArray(body.asset_ids) ? body.asset_ids : []),
611
+ ...(typeof body.asset_id === 'string' ? [body.asset_id] : []),
612
+ ]);
613
+ const assets = [];
614
+ const missing = [];
615
+ for (const id of ids) {
616
+ let got = null;
617
+ if (this.assetStore) {
618
+ got = await this.assetStore.get(id);
619
+ }
620
+ if (!got && this.remoteAssetById) {
621
+ got = await this.remoteAssetById(id);
622
+ }
623
+ if (assetMatchesId(got, id)) {
624
+ assets.push(got);
625
+ }
626
+ else {
627
+ missing.push(id);
628
+ }
629
+ }
630
+ ctx.json(200, { assets, missing, query: body });
631
+ return true;
632
+ }
633
+ if (ctx.route === 'POST /asset/submit') {
634
+ const body = (await ctx.readJson());
635
+ if (!body.assets && !body.asset_id) {
636
+ ctx.json(400, { error: 'assets or asset_id is required' });
637
+ return true;
638
+ }
639
+ const env = mailbox.createEnvelope({ type: 'asset_submit', payload: body, now: ctx.now });
640
+ const r = this.store.send(env);
641
+ if (r.stored)
642
+ this.notifyNewOutbound();
643
+ ctx.json(200, { id: env.id, message_id: env.id, receiptId: r.receiptId, status: 'pending', stored: r.stored });
644
+ return true;
645
+ }
646
+ if (ctx.route === 'POST /asset/validate') {
647
+ // Pre-publish dry-run against the hub's quality + content-safety gate (nothing stored, no credits).
648
+ // Same {assets:[…]} bundle shape as /asset/submit; the adapter wraps it in a GEP-A2A envelope.
649
+ const body = (await ctx.readJson());
650
+ const bundle = Array.isArray(body.assets)
651
+ ? body.assets.filter((a) => Boolean(a && typeof a === 'object'))
652
+ : (body.asset && typeof body.asset === 'object' && !Array.isArray(body.asset) ? [body.asset] : []);
653
+ if (bundle.length === 0) {
654
+ ctx.json(400, { valid: false, error: 'assets or asset is required' });
655
+ return true;
656
+ }
657
+ const sanitized = hubNs.sanitizeBundle(bundle, { env: typeof process !== 'undefined' ? process.env : {} });
658
+ if (sanitized.blocked) {
659
+ ctx.json(200, { valid: false, reason: 'leak_blocked' });
660
+ return true;
661
+ }
662
+ if (!this.validator) {
663
+ ctx.json(200, { valid: false, reason: 'validate_not_configured' });
664
+ return true;
665
+ }
666
+ try {
667
+ ctx.json(200, await this.validator.validate(sanitized.bundle));
668
+ }
669
+ catch (e) {
670
+ ctx.json(200, { valid: false, reason: errorMessage(e) });
671
+ }
672
+ return true;
673
+ }
674
+ if (ctx.route === 'POST /asset/reuse-result') {
675
+ const parsed = parseReuseResultReport(await ctx.readJson());
676
+ if ('error' in parsed) {
677
+ ctx.json(400, { recorded: false, error: parsed.error });
678
+ return true;
679
+ }
680
+ if (!this.reuseResultReporter) {
681
+ ctx.json(200, { recorded: false, reason: 'reuse_result_not_configured' });
682
+ return true;
683
+ }
684
+ try {
685
+ ctx.json(200, await this.reuseResultReporter.recordReuseResult(parsed.report));
686
+ }
687
+ catch (e) {
688
+ ctx.json(200, { recorded: false, reason: safeDaemonErrorMessage(e, MAX_PROXY_TICK_ERROR_LENGTH) });
689
+ }
690
+ return true;
691
+ }
692
+ if (ctx.route === 'POST /conversation/distill') {
693
+ const body = (await ctx.readJson());
694
+ const distill = await hubNs.distillConversation(body, { persist: body.persist === true, store: this.assetStore });
695
+ if (!distill.ok) {
696
+ ctx.json(200, { ...distill, queued: false, submission: null });
697
+ return true;
698
+ }
699
+ let submission = null;
700
+ if (body['publish'] === true) {
701
+ const env = mailbox.createEnvelope({
702
+ type: 'asset_submit',
703
+ payload: { source: 'conversation_distillation', distill_id: distill.distill_id, assets: [distill.gene, distill.capsule] },
704
+ now: ctx.now,
705
+ });
706
+ const r = this.store.send(env);
707
+ if (r.stored)
708
+ this.notifyNewOutbound();
709
+ submission = { id: env.id, message_id: env.id, receiptId: r.receiptId, status: 'pending', stored: r.stored };
710
+ }
711
+ ctx.json(200, { ...distill, queued: submission !== null, submission });
712
+ return true;
713
+ }
714
+ }
715
+ async searchAssets(query) {
716
+ const limit = Math.max(1, Math.min(Number(query.limit ?? 5), 25));
717
+ const local = this.assetStore ? await this.assetStore.search(query) : [];
718
+ if (query.kind === 'AntiGene')
719
+ return local.slice(0, limit);
720
+ const localSafe = local.filter((asset) => asset.type !== 'AntiGene');
721
+ let remote;
722
+ try {
723
+ remote = (await this.deps.hub.search(query)).filter((asset) => asset.type !== 'AntiGene');
724
+ }
725
+ catch (error) {
726
+ if (localSafe.length === 0)
727
+ throw error;
728
+ remote = [];
729
+ }
730
+ if (localSafe.length === 0)
731
+ return remote.slice(0, limit);
732
+ if (remote.length === 0)
733
+ return localSafe.slice(0, limit);
734
+ const seen = new Set();
735
+ const out = [];
736
+ // Keep proxy-backed PHub reuse visible even when the local asset cache has hits.
737
+ for (const asset of [...remote, ...localSafe]) {
738
+ if (seen.has(asset.asset_id))
739
+ continue;
740
+ seen.add(asset.asset_id);
741
+ out.push(asset);
742
+ if (out.length >= limit)
743
+ break;
744
+ }
745
+ return out;
746
+ }
747
+ async handleAtpRoute(ctx) {
748
+ if (!ctx.url.pathname.startsWith('/atp/'))
749
+ return false;
750
+ if (!this.atp) {
751
+ ctx.json(503, { ok: false, error: 'atp_not_configured' });
752
+ return true;
753
+ }
754
+ const body = ctx.req.method === 'GET' ? {} : asRecord(await ctx.readJson());
755
+ if (ctx.route === 'POST /atp/order') {
756
+ const consent = this.deps.atpOrderConsent;
757
+ if (!consent) {
758
+ ctx.json(403, { ok: false, status: 403, error: 'atp_spend_consent_required', message: 'ATP order refused: spend consent gate is not configured' });
759
+ return true;
760
+ }
761
+ try {
762
+ consent.assertAllowed();
763
+ }
764
+ catch (err) {
765
+ ctx.json(403, {
766
+ ok: false,
767
+ status: 403,
768
+ error: 'atp_spend_consent_required',
769
+ message: err instanceof Error ? err.message : 'ATP order refused: auto-spend consent is disabled',
770
+ });
771
+ return true;
772
+ }
773
+ const capabilities = Array.isArray(body['capabilities']) ? body['capabilities'].filter((s) => typeof s === 'string') : [];
774
+ this.writeAtpJson(ctx, await this.atp.placeOrder({
775
+ capabilities,
776
+ budget: numberBody(body, 'budget'),
777
+ routingMode: stringBody(body, 'routingMode') ?? stringBody(body, 'routing_mode'),
778
+ verifyMode: stringBody(body, 'verifyMode') ?? stringBody(body, 'verify_mode'),
779
+ question: stringBody(body, 'question'),
780
+ signals: Array.isArray(body['signals']) ? body['signals'].filter((s) => typeof s === 'string') : undefined,
781
+ minReputation: numberBody(body, 'minReputation') ?? numberBody(body, 'min_reputation'),
782
+ }));
783
+ return true;
784
+ }
785
+ if (ctx.route === 'POST /atp/deliver') {
786
+ const orderId = stringBody(body, 'orderId') ?? stringBody(body, 'order_id') ?? '';
787
+ this.writeAtpJson(ctx, await this.atp.submitDelivery(orderId, body['proofPayload'] ?? body['proof_payload'] ?? {}));
788
+ return true;
789
+ }
790
+ if (ctx.route === 'POST /atp/verify') {
791
+ const orderId = stringBody(body, 'orderId') ?? stringBody(body, 'order_id') ?? '';
792
+ this.writeAtpJson(ctx, await this.atp.verifyDelivery(orderId, stringBody(body, 'action') ?? 'confirm'));
793
+ return true;
794
+ }
795
+ if (ctx.route === 'POST /atp/settle') {
796
+ const orderId = stringBody(body, 'orderId') ?? stringBody(body, 'order_id') ?? '';
797
+ this.writeAtpJson(ctx, await this.atp.settleOrder(orderId));
798
+ return true;
799
+ }
800
+ if (ctx.route === 'POST /atp/dispute') {
801
+ const orderId = stringBody(body, 'orderId') ?? stringBody(body, 'order_id') ?? '';
802
+ this.writeAtpJson(ctx, await this.atp.disputeOrder(orderId, stringBody(body, 'reason') ?? ''));
803
+ return true;
804
+ }
805
+ if (ctx.route === 'GET /atp/merchant/tier') {
806
+ this.writeAtpJson(ctx, await this.atp.getMerchantTier(ctx.url.searchParams.get('node_id') ?? undefined));
807
+ return true;
808
+ }
809
+ if (ctx.req.method === 'GET' && ctx.url.pathname.startsWith('/atp/order/')) {
810
+ const orderId = decodeURIComponent(ctx.url.pathname.slice('/atp/order/'.length));
811
+ this.writeAtpJson(ctx, await this.atp.getOrderStatus(orderId));
812
+ return true;
813
+ }
814
+ if (ctx.route === 'GET /atp/proofs') {
815
+ this.writeAtpJson(ctx, await this.atp.listProofs({
816
+ nodeId: this.lifecycle.nodeId,
817
+ role: ctx.url.searchParams.get('role') ?? undefined,
818
+ status: ctx.url.searchParams.get('status') ?? undefined,
819
+ limit: numberQuery(ctx.url, 'limit'),
820
+ }));
821
+ return true;
822
+ }
823
+ if (ctx.route === 'GET /atp/policy') {
824
+ this.writeAtpJson(ctx, await this.atp.getAtpPolicy());
825
+ return true;
826
+ }
827
+ ctx.json(404, { ok: false, error: 'unknown_atp_route' });
828
+ return true;
829
+ }
830
+ writeAtpJson(ctx, body) {
831
+ const rec = asRecord(body);
832
+ const status = rec['ok'] === false && typeof rec['status'] === 'number' ? rec['status'] : 200;
833
+ ctx.json(status, body);
834
+ }
835
+ }
836
+ function isAssetByIdFetcher(value) {
837
+ return Boolean(value && typeof value === 'object' && typeof value.fetchAssetById === 'function');
838
+ }
839
+ function isReuseResultReporter(value) {
840
+ return Boolean(value && typeof value === 'object' && typeof value.recordReuseResult === 'function');
841
+ }
842
+ function isValidator(value) {
843
+ return Boolean(value && typeof value === 'object' && typeof value.validate === 'function');
844
+ }
845
+ function assetMatchesId(asset, assetId) {
846
+ return Boolean(asset && asset.asset_id === assetId);
847
+ }
848
+ function uniqueStrings(values) {
849
+ const seen = new Set();
850
+ const out = [];
851
+ for (const value of values) {
852
+ if (typeof value !== 'string' || value.length === 0 || seen.has(value))
853
+ continue;
854
+ seen.add(value);
855
+ out.push(value);
856
+ }
857
+ return out;
858
+ }
859
+ function assetKind(value) {
860
+ return value === 'Gene' || value === 'Capsule' || value === 'EvolutionEvent' || value === 'AntiGene' ? value : undefined;
861
+ }
862
+ function asRecord(value) {
863
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
864
+ }
865
+ function stringBody(body, key) {
866
+ const v = body[key];
867
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
868
+ }
869
+ function numberBody(body, key) {
870
+ const raw = body[key];
871
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? Number(raw) : NaN);
872
+ return Number.isFinite(n) ? n : undefined;
873
+ }
874
+ function optionalNonNegativeNumberBody(body, keys, error) {
875
+ let value;
876
+ for (const key of keys) {
877
+ if (!Object.prototype.hasOwnProperty.call(body, key))
878
+ continue;
879
+ const raw = body[key];
880
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw.trim().length > 0 ? Number(raw) : NaN);
881
+ if (!Number.isFinite(n) || n < 0)
882
+ return { error };
883
+ value ??= n;
884
+ }
885
+ return { value };
886
+ }
887
+ const REUSE_RESULT_OUTCOMES = new Set(['success', 'failed', 'mismatched', 'stale', 'unsafe']);
888
+ function parseReuseResultReport(value) {
889
+ const body = asRecord(value);
890
+ const assetId = stringBody(body, 'assetId') ?? stringBody(body, 'asset_id');
891
+ if (!assetId)
892
+ return { error: 'asset_id_required' };
893
+ const outcome = stringBody(body, 'outcome');
894
+ if (!isReuseResultOutcome(outcome))
895
+ return { error: 'invalid_outcome' };
896
+ const taskId = stringBody(body, 'taskId') ?? stringBody(body, 'task_id');
897
+ const traceId = stringBody(body, 'traceId') ?? stringBody(body, 'trace_id');
898
+ const tokensSavedParsed = optionalNonNegativeNumberBody(body, ['tokensSaved', 'tokens_saved'], 'invalid_tokens_saved');
899
+ if ('error' in tokensSavedParsed)
900
+ return { error: tokensSavedParsed.error };
901
+ const timeSavedSecondsParsed = optionalNonNegativeNumberBody(body, ['timeSavedSeconds', 'time_saved_seconds'], 'invalid_time_saved_seconds');
902
+ if ('error' in timeSavedSecondsParsed)
903
+ return { error: timeSavedSecondsParsed.error };
904
+ const reason = stringBody(body, 'reason');
905
+ return {
906
+ report: {
907
+ assetId,
908
+ outcome,
909
+ ...(taskId ? { taskId } : {}),
910
+ ...(traceId ? { traceId } : {}),
911
+ ...(timeSavedSecondsParsed.value !== undefined ? { timeSavedSeconds: timeSavedSecondsParsed.value } : {}),
912
+ ...(reason ? { reason: reason.slice(0, 1000) } : {}),
913
+ },
914
+ };
915
+ }
916
+ function isReuseResultOutcome(value) {
917
+ return typeof value === 'string' && REUSE_RESULT_OUTCOMES.has(value);
918
+ }
919
+ function errorMessage(err) {
920
+ return err instanceof Error ? err.message : String(err);
921
+ }
922
+ function safeDaemonMessage(message, maxLength) {
923
+ try {
924
+ return hubNs.redactString(message).slice(0, maxLength);
925
+ }
926
+ catch {
927
+ return '[REDACTED]';
928
+ }
929
+ }
930
+ function safeDaemonErrorMessage(err, maxLength) {
931
+ return safeDaemonMessage(errorMessage(err), maxLength);
932
+ }
933
+ function isAuthLikeError(err) {
934
+ const name = err?.name;
935
+ return name === 'AuthError' || /\b(401|403|unauthorized|forbidden|auth)\b/i.test(errorMessage(err));
936
+ }
937
+ function emptyOutboundResult() {
938
+ return { sent: 0, failed: 0, terminal: 0, deferred: 0 };
939
+ }
940
+ function emptyInboundResult() {
941
+ return { received: 0, enqueued: 0, hasMore: false };
942
+ }
943
+ function uniqueTickPhases(phases) {
944
+ return Array.from(new Set(phases));
945
+ }
946
+ function isFatalTickCandidate(outbound, inbound, failedPhases) {
947
+ const failed = new Set(failedPhases);
948
+ return failed.has('core')
949
+ && failed.has('outbound')
950
+ && failed.has('inbound')
951
+ && !hasTickSyncProgress(outbound, inbound);
952
+ }
953
+ function hasTickSyncProgress(outbound, inbound) {
954
+ return outbound.sent > 0
955
+ || outbound.terminal > 0
956
+ || inbound.received > 0
957
+ || inbound.enqueued > 0
958
+ || inbound.hasMore;
959
+ }
960
+ function safeHeartbeatTickErrorMessage(err) {
961
+ return safeDaemonErrorMessage(err, MAX_HEARTBEAT_TICK_ERROR_LENGTH);
962
+ }
963
+ function numberQuery(url, key) {
964
+ const raw = url.searchParams.get(key);
965
+ if (raw === null)
966
+ return undefined;
967
+ const n = Number(raw);
968
+ return Number.isFinite(n) ? n : undefined;
969
+ }
970
+ function forceUpdateRetryCooldownMs(env) {
971
+ const raw = env['EVOLVER_FORCE_UPDATE_RETRY_COOLDOWN_MS'];
972
+ if (raw === undefined || raw.trim() === '')
973
+ return 60_000;
974
+ const n = Number(raw);
975
+ return Number.isFinite(n) && n >= 0 ? n : 60_000;
976
+ }
977
+ function forceUpdateScheduleDelayMs(directive, random) {
978
+ const staggerWindowMs = nonNegativeFiniteNumber(directive.stagger_window_ms);
979
+ if (staggerWindowMs === undefined || staggerWindowMs <= 0)
980
+ return 0;
981
+ const deadlineMs = nonNegativeFiniteNumber(directive.deadline_ms);
982
+ const ratio = safeRandomRatio(random);
983
+ const sampledDelayMs = ratio * staggerWindowMs;
984
+ const maxDelayMs = Math.min(staggerWindowMs, deadlineMs ?? staggerWindowMs, MAX_TIMER_DELAY_MS);
985
+ return Math.max(0, Math.min(sampledDelayMs, maxDelayMs));
986
+ }
987
+ function nonNegativeFiniteNumber(value) {
988
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
989
+ }
990
+ function safeRandomRatio(random) {
991
+ try {
992
+ return clampedRandom(random());
993
+ }
994
+ catch {
995
+ return 0;
996
+ }
997
+ }
998
+ function clampedRandom(value) {
999
+ if (typeof value !== 'number')
1000
+ return 0;
1001
+ if (!Number.isFinite(value))
1002
+ return 0;
1003
+ return Math.max(0, Math.min(value, 1));
1004
+ }
1005
+ function forceUpdateDirectiveKey(directive) {
1006
+ const manifest = directive.manifest;
1007
+ const manifestVersion = manifest && typeof manifest === 'object' && !Array.isArray(manifest)
1008
+ ? manifest.version
1009
+ : undefined;
1010
+ return [
1011
+ directive.directive_id ?? '',
1012
+ directive.required_version ?? '',
1013
+ typeof manifestVersion === 'string' ? manifestVersion : '',
1014
+ ].join('\x1f');
1015
+ }