@evomap/evolver-core 2.0.0-beta.9 → 2.0.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 (142) hide show
  1. package/dist/algo/candidateAssembly.d.ts +7 -2
  2. package/dist/algo/candidateAssembly.js +81 -13
  3. package/dist/algo/capabilityCandidates.js +10 -0
  4. package/dist/algo/cycleEngine.d.ts +23 -2
  5. package/dist/algo/cycleEngine.js +236 -36
  6. package/dist/algo/exploration.d.ts +7 -0
  7. package/dist/algo/exploration.js +16 -3
  8. package/dist/algo/geneHealth.d.ts +2 -2
  9. package/dist/algo/geneHealth.js +5 -4
  10. package/dist/algo/geneSelection.d.ts +63 -5
  11. package/dist/algo/geneSelection.js +330 -30
  12. package/dist/algo/index.d.ts +2 -0
  13. package/dist/algo/index.js +2 -0
  14. package/dist/algo/orchestrator.d.ts +8 -1
  15. package/dist/algo/orchestrator.js +15 -3
  16. package/dist/algo/publishEligibility.d.ts +34 -0
  17. package/dist/algo/publishEligibility.js +52 -0
  18. package/dist/algo/ucb1.d.ts +53 -0
  19. package/dist/algo/ucb1.js +156 -0
  20. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  21. package/dist/assetstore/assetSidecarRecords.js +238 -0
  22. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  23. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  24. package/dist/assetstore/assetStoreHealth.js +56 -14
  25. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  26. package/dist/assetstore/assetStoreStorage.js +27 -9
  27. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  28. package/dist/assetstore/assetSyncLedger.js +683 -4
  29. package/dist/assetstore/index.d.ts +1 -0
  30. package/dist/assetstore/index.js +1 -0
  31. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  32. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  33. package/dist/assetstore/localJsonl.d.ts +4 -2
  34. package/dist/assetstore/localJsonl.js +89 -9
  35. package/dist/assetstore/provenance.d.ts +80 -4
  36. package/dist/assetstore/provenance.js +313 -5
  37. package/dist/assetstore/provider.d.ts +58 -1
  38. package/dist/assetstore/provider.js +97 -6
  39. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  40. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  41. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  42. package/dist/benchmark/antiGeneRollout.js +4 -3
  43. package/dist/benchmark/index.d.ts +2 -1
  44. package/dist/benchmark/index.js +2 -1
  45. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  46. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  47. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  48. package/dist/bootstrap/envFingerprint.js +5 -0
  49. package/dist/bootstrap/index.d.ts +2 -1
  50. package/dist/bootstrap/index.js +2 -1
  51. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  52. package/dist/bootstrap/v1EnvCompat.js +280 -0
  53. package/dist/events/eventArchive.d.ts +2 -0
  54. package/dist/events/eventArchive.js +13 -3
  55. package/dist/events/eventSchema.d.ts +7 -7
  56. package/dist/events/eventStore.d.ts +2 -0
  57. package/dist/events/eventStore.js +5 -1
  58. package/dist/events/ingest.d.ts +1 -0
  59. package/dist/events/ingest.js +1 -0
  60. package/dist/events/paths.d.ts +3 -1
  61. package/dist/events/paths.js +4 -0
  62. package/dist/events/public.d.ts +2 -2
  63. package/dist/events/public.js +2 -2
  64. package/dist/events/reports.d.ts +2 -0
  65. package/dist/events/reports.js +4 -0
  66. package/dist/exec/autoExec.d.ts +43 -2
  67. package/dist/exec/autoExec.js +76 -9
  68. package/dist/exec/autonomousCycle.d.ts +22 -4
  69. package/dist/exec/autonomousCycle.js +64 -13
  70. package/dist/exec/claudeBridge.d.ts +32 -7
  71. package/dist/exec/claudeBridge.js +281 -29
  72. package/dist/exec/prompt.js +5 -1
  73. package/dist/exec/runnerRegistry.d.ts +68 -26
  74. package/dist/exec/runnerRegistry.js +307 -72
  75. package/dist/exec/selfPr.js +1 -7
  76. package/dist/feedback/envelope.d.ts +61 -0
  77. package/dist/feedback/envelope.js +168 -0
  78. package/dist/feedback/index.d.ts +1 -0
  79. package/dist/feedback/index.js +1 -0
  80. package/dist/hooks/hooks.js +1 -0
  81. package/dist/hub/assetCallLog.d.ts +35 -1
  82. package/dist/hub/assetCallLog.js +124 -1
  83. package/dist/hub/bindings.d.ts +8 -1
  84. package/dist/hub/bindings.js +17 -6
  85. package/dist/hub/capability.d.ts +90 -3
  86. package/dist/hub/fake.d.ts +2 -2
  87. package/dist/hub/fake.js +1 -1
  88. package/dist/hub/questionGenerator.d.ts +5 -1
  89. package/dist/hub/questionGenerator.js +8 -6
  90. package/dist/index.d.ts +3 -1
  91. package/dist/index.js +4 -1
  92. package/dist/mailbox/dispatch.d.ts +1 -1
  93. package/dist/mailbox/dispatch.js +22 -6
  94. package/dist/mailbox/envelope.d.ts +7 -1
  95. package/dist/mailbox/envelope.js +9 -2
  96. package/dist/mailbox/ipcServer.d.ts +10 -2
  97. package/dist/mailbox/ipcServer.js +163 -13
  98. package/dist/mailbox/store.d.ts +89 -3
  99. package/dist/mailbox/store.js +895 -41
  100. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  101. package/dist/ops/evolutionGraphProjection.js +315 -0
  102. package/dist/ops/index.d.ts +2 -1
  103. package/dist/ops/index.js +2 -1
  104. package/dist/ops/selfUpdate.d.ts +8 -0
  105. package/dist/ops/selfUpdate.js +24 -8
  106. package/dist/personality/schema.d.ts +16 -16
  107. package/dist/schema/evolutionGraph.d.ts +784 -0
  108. package/dist/schema/evolutionGraph.js +187 -0
  109. package/dist/schema/index.d.ts +1 -0
  110. package/dist/schema/index.js +1 -0
  111. package/dist/schema/signal.d.ts +3 -3
  112. package/dist/schema/signal.js +1 -1
  113. package/dist/signals/curriculum.d.ts +55 -0
  114. package/dist/signals/curriculum.js +202 -0
  115. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  116. package/dist/signals/expand.d.ts +15 -1
  117. package/dist/signals/expand.js +169 -1
  118. package/dist/signals/extractor.d.ts +2 -2
  119. package/dist/signals/extractor.js +31 -6
  120. package/dist/signals/index.d.ts +3 -1
  121. package/dist/signals/index.js +3 -1
  122. package/dist/signals/metaSignals.d.ts +4 -0
  123. package/dist/signals/metaSignals.js +42 -0
  124. package/dist/signals/signalGate.js +1 -1
  125. package/dist/signals/taskDomain.d.ts +22 -0
  126. package/dist/signals/taskDomain.js +43 -0
  127. package/dist/strategy/constraintAblation.js +115 -369
  128. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  129. package/dist/strategy/constraintAblationPredicates.js +339 -0
  130. package/dist/trace/index.d.ts +3 -1
  131. package/dist/trace/index.js +3 -1
  132. package/dist/trace/learningTrace.d.ts +209 -0
  133. package/dist/trace/learningTrace.js +276 -0
  134. package/dist/trace/proxyTurns.d.ts +31 -0
  135. package/dist/trace/proxyTurns.js +137 -0
  136. package/dist/verify/sandboxRunner.d.ts +28 -0
  137. package/dist/verify/sandboxRunner.js +218 -19
  138. package/dist/verify/sandboxedValidation.d.ts +9 -0
  139. package/dist/verify/sandboxedValidation.js +113 -13
  140. package/dist/verify/validation.d.ts +11 -1
  141. package/dist/verify/validation.js +31 -0
  142. package/package.json +5 -2
@@ -21,7 +21,7 @@ const EXPLORE_STOPWORDS = new Set([
21
21
  'agent', 'about', 'into', 'then', 'than', 'they', 'them', 'their', 'there',
22
22
  'here', 'will', 'would', 'could', 'should', 'been', 'were', 'using', 'used',
23
23
  'cycle', 'evolution', 'error', 'errors', 'failed', 'failure', 'null', 'undefined',
24
- 'true', 'false', 'console', 'return', 'function', 'const', 'value', 'result',
24
+ 'true', 'false', 'console', 'return', 'function', 'const', 'value', 'result', 'redacted',
25
25
  ]);
26
26
  const SENSITIVE_TOPIC_RE = /secret|token|api[_-]?key|password|passwd|credential|bearer|authorization|cookie|session[_-]?id|private[_-]?key|oauth|refresh[_-]?token/i;
27
27
  const LONG_OPAQUE_RE = /^[a-z0-9_-]{32,}$/i;
@@ -106,8 +106,8 @@ function safeTopicToken(token) {
106
106
  return false;
107
107
  return true;
108
108
  }
109
- export function extractTopicKeywords(transcript, memory, max = 5) {
110
- const text = `${String(transcript ?? '')} ${String(memory ?? '')}`.toLowerCase();
109
+ export function extractTopicKeywords(transcript, memory, max = 5, opts = {}) {
110
+ const text = redactString(`${String(transcript ?? '')} ${String(memory ?? '')}`).toLowerCase();
111
111
  const words = text.match(/[a-z][a-z0-9_-]{4,}/g) ?? [];
112
112
  const freq = new Map();
113
113
  for (const word of words) {
@@ -115,9 +115,11 @@ export function extractTopicKeywords(transcript, memory, max = 5) {
115
115
  continue;
116
116
  freq.set(word, (freq.get(word) ?? 0) + 1);
117
117
  }
118
- return [...freq.entries()]
119
- .filter(([, count]) => count >= 2)
120
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
118
+ const entries = [...freq.entries()]
119
+ .filter(([, count]) => count >= Math.max(1, Math.trunc(opts.minOccurrences ?? 2)));
120
+ if (!opts.preserveSourceOrder)
121
+ entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
122
+ return entries
121
123
  .map(([word]) => word)
122
124
  .slice(0, max);
123
125
  }
package/dist/index.d.ts CHANGED
@@ -25,4 +25,6 @@ export * as shadow from './shadow/index.js';
25
25
  export * as ops from './ops/index.js';
26
26
  export * as util from './util/index.js';
27
27
  export * as trace from './trace/index.js';
28
- export * as issueReporter from './issueReporter/index.js';
28
+ export * as issueReporter from './issueReporter/index.js';
29
+ export * as feedback from './feedback/index.js';
30
+ export { isValidationCommandAllowed } from './verify/validation.js';
package/dist/index.js CHANGED
@@ -25,4 +25,7 @@ export * as shadow from './shadow/index.js';
25
25
  export * as ops from './ops/index.js';
26
26
  export * as util from './util/index.js';
27
27
  export * as trace from './trace/index.js';
28
- export * as issueReporter from './issueReporter/index.js';
28
+ export * as issueReporter from './issueReporter/index.js';
29
+ export * as feedback from './feedback/index.js';
30
+ // Re-export commonly used validation functions at top level for convenience
31
+ export { isValidationCommandAllowed } from './verify/validation.js';
@@ -1,6 +1,6 @@
1
1
  import type { Envelope } from './envelope.js';
2
2
  import type { Handler } from './catalog.js';
3
- import type { MailboxStore } from './store.js';
3
+ import { type MailboxStore } from './store.js';
4
4
  export interface DispatchResult {
5
5
  id: string;
6
6
  handler: Handler;
@@ -1,3 +1,4 @@
1
+ import { mailboxClaimOwner } from './store.js';
1
2
  /**
2
3
  * 选择性 Material 判定(M2-7). 默认遵目录 feedsMaterial; 两类在 dispatch 决定:
3
4
  * - asset_publish_result: 仅 rejected 进(失败=养料 #40); 通过无需.
@@ -25,10 +26,16 @@ export class Dispatcher {
25
26
  }
26
27
  async dispatchOne(e) {
27
28
  const now = this.deps.now();
29
+ const claimOwner = mailboxClaimOwner(e);
30
+ if (!claimOwner) {
31
+ return { id: e.id, handler: e.handler, handled: false, fedMaterial: false, note: 'claim-owner-missing' };
32
+ }
28
33
  // 副作用幂等: 业务键(非默认=id)命中 → 跳过不二次执行, 直接 complete (money-safety A7/A13)
29
34
  const sideEffecting = e.handler !== 'agent' && e.idempotencyKey !== e.id;
30
35
  if (sideEffecting && this.deps.store.isProcessed(e.idempotencyKey)) {
31
- this.deps.store.complete(e.id, now);
36
+ if (!this.deps.store.completeClaimed(e.id, now, claimOwner)) {
37
+ return { id: e.id, handler: e.handler, handled: false, fedMaterial: false, note: 'claim-lost' };
38
+ }
32
39
  return { id: e.id, handler: e.handler, handled: true, fedMaterial: false, note: 'idempotent-skip' };
33
40
  }
34
41
  try {
@@ -38,14 +45,23 @@ export class Dispatcher {
38
45
  await this.deps.onMaterial(e);
39
46
  fedMaterial = true;
40
47
  }
41
- if (sideEffecting)
42
- this.deps.store.markProcessed(e.idempotencyKey, result ?? null, now);
43
- this.deps.store.complete(e.id, now);
48
+ const completed = sideEffecting
49
+ ? this.deps.store.completeClaimedAndMarkProcessed(e.id, e.idempotencyKey, result ?? null, now, claimOwner)
50
+ : this.deps.store.completeClaimed(e.id, now, claimOwner);
51
+ if (!completed) {
52
+ return { id: e.id, handler: e.handler, handled: false, fedMaterial, note: 'claim-lost' };
53
+ }
44
54
  return { id: e.id, handler: e.handler, handled: true, fedMaterial };
45
55
  }
46
56
  catch (err) {
47
- this.deps.store.fail(e.id, err instanceof Error ? err.message : String(err), now);
48
- return { id: e.id, handler: e.handler, handled: false, fedMaterial: false, note: 'failed' };
57
+ const failed = this.deps.store.failClaimed(e.id, err instanceof Error ? err.message : String(err), now, claimOwner);
58
+ return {
59
+ id: e.id,
60
+ handler: e.handler,
61
+ handled: false,
62
+ fedMaterial: false,
63
+ note: failed ? 'failed' : 'claim-lost',
64
+ };
49
65
  }
50
66
  }
51
67
  /** 拉一批并分派. 默认 daemon 侧 core+proxy; agent 由 runtime 经 IPC 拉取. */
@@ -1,6 +1,8 @@
1
1
  import { specForType, type Direction, type Handler } from './catalog.js';
2
2
  export type Status = 'pending' | 'in_flight' | 'done' | 'failed' | 'expired';
3
- export declare const ENVELOPE_SCHEMA_VERSION = "1.0.0";
3
+ export type Priority = 'high' | 'normal' | 'low';
4
+ export declare const PRIORITIES: readonly ["high", "normal", "low"];
5
+ export declare const ENVELOPE_SCHEMA_VERSION = "1.1.0";
4
6
  export interface Envelope {
5
7
  id: string;
6
8
  type: string;
@@ -15,6 +17,7 @@ export interface Envelope {
15
17
  sourceAgent: string;
16
18
  targetAgent: string;
17
19
  runtimeNamespace: string;
20
+ priority: Priority;
18
21
  attempts: number;
19
22
  nextRetryAt: number | null;
20
23
  ttlAt: number | null;
@@ -22,6 +25,7 @@ export interface Envelope {
22
25
  updatedAt: number;
23
26
  schemaVersion: string;
24
27
  feedsMaterial: boolean;
28
+ lastError: string | null;
25
29
  }
26
30
  /** 固定 envelope 字段集 (schema-snapshot 锁; 增删字段必须改此处). */
27
31
  export declare const ENVELOPE_FIELDS: readonly (keyof Envelope)[];
@@ -37,7 +41,9 @@ export interface CreateEnvelopeInput {
37
41
  sourceAgent?: string;
38
42
  targetAgent?: string;
39
43
  runtimeNamespace?: string;
44
+ priority?: unknown;
40
45
  now?: number;
41
46
  }
47
+ export declare function normalizePriority(value: unknown): Priority;
42
48
  export declare function createEnvelope(input: CreateEnvelopeInput): Envelope;
43
49
  export { specForType };
@@ -1,11 +1,13 @@
1
1
  import { ulid as makeUlid } from 'ulid';
2
2
  import { specForType, assertKnownType } from './catalog.js';
3
- export const ENVELOPE_SCHEMA_VERSION = '1.0.0';
3
+ export const PRIORITIES = Object.freeze(['high', 'normal', 'low']);
4
+ export const ENVELOPE_SCHEMA_VERSION = '1.1.0';
4
5
  /** 固定 envelope 字段集 (schema-snapshot 锁; 增删字段必须改此处). */
5
6
  export const ENVELOPE_FIELDS = [
6
7
  'id', 'type', 'direction', 'status', 'handler', 'payload', 'correlationId', 'replyTo',
7
8
  'receiptId', 'idempotencyKey', 'sourceAgent', 'targetAgent', 'runtimeNamespace',
8
- 'attempts', 'nextRetryAt', 'ttlAt', 'createdAt', 'updatedAt', 'schemaVersion', 'feedsMaterial',
9
+ 'priority', 'attempts', 'nextRetryAt', 'ttlAt', 'createdAt', 'updatedAt', 'schemaVersion', 'feedsMaterial',
10
+ 'lastError',
9
11
  ];
10
12
  const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
11
13
  /** 防原型污染 (v1 GHSA). */
@@ -22,6 +24,9 @@ export function sanitizePayload(v) {
22
24
  return v;
23
25
  }
24
26
  const TTL_MS = { control: 60 * 60 * 1000, default: 7 * 24 * 60 * 60 * 1000 };
27
+ export function normalizePriority(value) {
28
+ return value === 'high' || value === 'low' ? value : 'normal';
29
+ }
25
30
  export function createEnvelope(input) {
26
31
  const spec = assertKnownType(input.type);
27
32
  const id = input.id ?? makeUlid();
@@ -40,6 +45,7 @@ export function createEnvelope(input) {
40
45
  sourceAgent: input.sourceAgent ?? '',
41
46
  targetAgent: input.targetAgent ?? '',
42
47
  runtimeNamespace: input.runtimeNamespace ?? 'default',
48
+ priority: normalizePriority(input.priority),
43
49
  attempts: 0,
44
50
  nextRetryAt: null,
45
51
  ttlAt: now + TTL_MS[spec.ttlClass],
@@ -47,6 +53,7 @@ export function createEnvelope(input) {
47
53
  updatedAt: now,
48
54
  schemaVersion: ENVELOPE_SCHEMA_VERSION,
49
55
  feedsMaterial: spec.feedsMaterial,
56
+ lastError: null,
50
57
  };
51
58
  }
52
59
  export { specForType };
@@ -1,11 +1,13 @@
1
1
  import { type IncomingMessage, type ServerResponse } from 'node:http';
2
2
  import { type Envelope } from './envelope.js';
3
- import type { MailboxStore } from './store.js';
3
+ import { type MailboxStore } from './store.js';
4
4
  export interface IpcServerOptions {
5
5
  store: MailboxStore;
6
6
  token: string;
7
7
  host?: string;
8
8
  now?: () => number;
9
+ /** When set, every mailbox route is pinned to this daemon namespace. */
10
+ runtimeNamespace?: string;
9
11
  extraRoutes?: IpcRouteHandler[];
10
12
  onSend?: (envelope: Envelope, result: {
11
13
  receiptId: string;
@@ -34,6 +36,7 @@ export declare class MailboxIpcServer {
34
36
  private readonly token;
35
37
  private readonly host;
36
38
  private readonly now;
39
+ private readonly runtimeNamespace;
37
40
  private readonly extraRoutes;
38
41
  private readonly onSend;
39
42
  private readonly onAuthFailure;
@@ -42,5 +45,10 @@ export declare class MailboxIpcServer {
42
45
  close(): Promise<void>;
43
46
  private handle;
44
47
  private readJson;
48
+ private resolveSendNamespace;
49
+ private resolveRuntimeNamespace;
50
+ private messageInRuntime;
51
+ private canMutateMessage;
45
52
  private json;
46
- }
53
+ }
54
+ export declare function legacyMailboxMessage(message: Envelope): Record<string, unknown>;
@@ -1,8 +1,33 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { timingSafeEqual } from 'node:crypto';
3
3
  import { createEnvelope } from './envelope.js';
4
+ import { mailboxClaimOwner } from './store.js';
4
5
  const MAX_BODY = 256 * 1024; // 256KB 上限(防本机注入超大体)
5
6
  const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
7
+ const STATUSES = new Set(['pending', 'in_flight', 'done', 'failed', 'expired']);
8
+ const DIRECTIONS = new Set(['outbound', 'inbound', 'local']);
9
+ const PRIORITIES = new Set(['high', 'normal', 'low']);
10
+ function serializeClaimedMessage(message) {
11
+ const claimToken = mailboxClaimOwner(message);
12
+ if (!claimToken)
13
+ throw new Error('claimed mailbox message is missing its claim token');
14
+ return { ...message, claimToken };
15
+ }
16
+ function requiredClaimToken(value) {
17
+ if (typeof value !== 'string' || value.length === 0)
18
+ throw new Error('claimToken must be a non-empty string');
19
+ return value;
20
+ }
21
+ function optionalQueryInteger(url, name) {
22
+ const raw = url.searchParams.get(name);
23
+ if (raw === null)
24
+ return undefined;
25
+ const value = Number(raw);
26
+ if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
27
+ throw new Error(`${name} must be a non-negative integer`);
28
+ }
29
+ return value;
30
+ }
6
31
  /** 定长常量时间比较(防 token 计时侧信道). */
7
32
  function tokenEq(a, b) {
8
33
  const ba = Buffer.from(a), bb = Buffer.from(b);
@@ -20,6 +45,7 @@ export class MailboxIpcServer {
20
45
  token;
21
46
  host;
22
47
  now;
48
+ runtimeNamespace;
23
49
  extraRoutes;
24
50
  onSend;
25
51
  onAuthFailure;
@@ -28,6 +54,7 @@ export class MailboxIpcServer {
28
54
  this.token = opts.token;
29
55
  this.host = opts.host ?? '127.0.0.1';
30
56
  this.now = opts.now ?? (() => Date.now());
57
+ this.runtimeNamespace = opts.runtimeNamespace;
31
58
  this.extraRoutes = opts.extraRoutes ?? [];
32
59
  this.onSend = opts.onSend;
33
60
  this.onAuthFailure = opts.onAuthFailure;
@@ -90,41 +117,98 @@ export class MailboxIpcServer {
90
117
  }
91
118
  if (route === 'POST /mailbox/send') {
92
119
  const body = (await this.readJson(req));
93
- const env = createEnvelope({ ...body, now });
120
+ if (body.priority !== undefined && !PRIORITIES.has(body.priority)) {
121
+ throw new Error('priority must be high, normal, or low');
122
+ }
123
+ const legacyRef = typeof body.ref_id === 'string' && body.ref_id ? body.ref_id : undefined;
124
+ const runtimeNamespace = this.resolveSendNamespace(body);
125
+ let env = createEnvelope({
126
+ ...body,
127
+ runtimeNamespace,
128
+ correlationId: body.correlationId ?? legacyRef,
129
+ now,
130
+ });
131
+ if (body.expires_at !== undefined) {
132
+ if (typeof body.expires_at !== 'number' || !Number.isFinite(body.expires_at)) {
133
+ throw new Error('expires_at must be a finite epoch-millisecond number');
134
+ }
135
+ env = { ...env, ttlAt: body.expires_at };
136
+ }
94
137
  const r = this.store.send(env);
95
138
  if (r.stored)
96
139
  this.onSend?.(env, r);
97
- return this.json(res, 200, { id: env.id, receiptId: r.receiptId, stored: r.stored, correlationId: env.correlationId });
140
+ return this.json(res, 200, {
141
+ id: env.id,
142
+ receiptId: r.receiptId,
143
+ stored: r.stored,
144
+ correlationId: env.correlationId,
145
+ message_id: env.id,
146
+ status: env.status,
147
+ });
98
148
  }
99
149
  if (route === 'POST /mailbox/claim') {
100
150
  const b = (await this.readJson(req));
101
- const got = this.store.claim(b.handler, b.limit ?? 16, b.leaseMs ?? 30_000, now, b.runtimeNamespace);
102
- return this.json(res, 200, { messages: got });
151
+ const runtimeNamespace = this.resolveRuntimeNamespace(b.runtimeNamespace);
152
+ const got = this.store.claim(b.handler, b.limit ?? 16, b.leaseMs ?? 30_000, now, runtimeNamespace);
153
+ return this.json(res, 200, { messages: got.map(serializeClaimedMessage) });
103
154
  }
104
155
  if (route === 'POST /mailbox/complete') {
105
156
  const b = (await this.readJson(req));
106
- this.store.complete(b.id, now);
157
+ if (!this.canMutateMessage(b.id))
158
+ return this.json(res, 404, { error: 'not found' });
159
+ const claimToken = requiredClaimToken(b.claimToken);
160
+ if (!this.store.completeClaimed(b.id, now, claimToken)) {
161
+ return this.json(res, 409, { error: 'mailbox claim is no longer owned by this worker', code: 'claim_conflict' });
162
+ }
107
163
  return this.json(res, 200, { ok: true });
108
164
  }
109
165
  if (route === 'POST /mailbox/fail') {
110
166
  const b = (await this.readJson(req));
111
- this.store.fail(b.id, b.error ?? 'ipc fail', now);
167
+ if (!this.canMutateMessage(b.id))
168
+ return this.json(res, 404, { error: 'not found' });
169
+ const claimToken = requiredClaimToken(b.claimToken);
170
+ if (!this.store.failClaimed(b.id, b.error ?? 'ipc fail', now, claimToken)) {
171
+ return this.json(res, 409, { error: 'mailbox claim is no longer owned by this worker', code: 'claim_conflict' });
172
+ }
112
173
  return this.json(res, 200, { ok: true });
113
174
  }
114
- if (req.method === 'GET' && url.pathname === '/mailbox/status') {
115
- const id = url.searchParams.get('id') ?? '';
175
+ const statusPathMatch = req.method === 'GET' ? /^\/mailbox\/status\/([^/]+)$/.exec(url.pathname) : null;
176
+ if (req.method === 'GET' && (url.pathname === '/mailbox/status' || statusPathMatch)) {
177
+ const id = statusPathMatch ? decodeURIComponent(statusPathMatch[1] ?? '') : url.searchParams.get('id') ?? '';
178
+ const message = this.messageInRuntime(id);
179
+ if (!message)
180
+ return this.json(res, 404, { error: 'not found' });
181
+ if (statusPathMatch) {
182
+ return this.json(res, 200, legacyMailboxMessage(message));
183
+ }
116
184
  const s = this.store.getStatus(id);
117
185
  return s ? this.json(res, 200, s) : this.json(res, 404, { error: 'not found' });
118
186
  }
119
187
  if (req.method === 'GET' && url.pathname === '/mailbox/list') {
120
- const status = url.searchParams.get('status') ?? undefined;
188
+ const type = url.searchParams.get('type') ?? undefined;
189
+ const legacy = type !== undefined;
190
+ const rawStatus = url.searchParams.get('status') ?? undefined;
191
+ const statusValue = legacyMailboxStatus(rawStatus);
192
+ if (rawStatus !== undefined && statusValue === undefined)
193
+ throw new Error('invalid status');
194
+ const directionValue = url.searchParams.get('direction') ?? undefined;
195
+ if (directionValue !== undefined && !DIRECTIONS.has(directionValue)) {
196
+ throw new Error('invalid direction');
197
+ }
121
198
  const got = this.store.list({
122
- status: status,
199
+ status: statusValue,
123
200
  handler: url.searchParams.get('handler') ?? undefined,
124
- runtimeNamespace: url.searchParams.get('runtimeNamespace') ?? undefined,
125
- limit: Number(url.searchParams.get('limit') ?? 200),
201
+ runtimeNamespace: this.resolveRuntimeNamespace(url.searchParams.get('runtimeNamespace') ?? undefined),
202
+ type,
203
+ direction: directionValue,
204
+ newestFirst: legacy,
205
+ offset: optionalQueryInteger(url, 'offset'),
206
+ limit: optionalQueryInteger(url, 'limit') ?? 200,
207
+ });
208
+ return this.json(res, 200, {
209
+ messages: legacy ? got.map(legacyMailboxMessage) : got,
210
+ count: got.length,
126
211
  });
127
- return this.json(res, 200, { messages: got });
128
212
  }
129
213
  return this.json(res, 404, { error: 'unknown route' });
130
214
  }
@@ -161,8 +245,74 @@ export class MailboxIpcServer {
161
245
  req.on('error', reject);
162
246
  });
163
247
  }
248
+ resolveSendNamespace(body) {
249
+ const bound = this.runtimeNamespace;
250
+ if (bound === undefined)
251
+ return body.runtimeNamespace;
252
+ const requested = body.runtimeNamespace;
253
+ if (requested !== undefined && (typeof requested !== 'string' || requested !== bound)) {
254
+ throw new Error('runtimeNamespace does not match the IPC server namespace');
255
+ }
256
+ const channel = body.channel;
257
+ if (channel !== undefined && (typeof channel !== 'string'
258
+ || (channel !== 'evomap-hub' && channel !== bound))) {
259
+ throw new Error('channel does not match the IPC server namespace');
260
+ }
261
+ return bound;
262
+ }
263
+ resolveRuntimeNamespace(requested) {
264
+ if (requested !== undefined && typeof requested !== 'string') {
265
+ throw new Error('runtimeNamespace must be a string');
266
+ }
267
+ const bound = this.runtimeNamespace;
268
+ if (bound === undefined)
269
+ return requested;
270
+ if (requested !== undefined && requested !== bound) {
271
+ throw new Error('runtimeNamespace does not match the IPC server namespace');
272
+ }
273
+ return bound;
274
+ }
275
+ messageInRuntime(id) {
276
+ const message = this.store.getById(id);
277
+ if (this.runtimeNamespace !== undefined && message?.runtimeNamespace !== this.runtimeNamespace)
278
+ return undefined;
279
+ return message;
280
+ }
281
+ canMutateMessage(id) {
282
+ return this.messageInRuntime(id) !== undefined;
283
+ }
164
284
  json(res, code, body) {
165
285
  res.writeHead(code, { 'content-type': 'application/json' });
166
286
  res.end(JSON.stringify(body));
167
287
  }
288
+ }
289
+ export function legacyMailboxMessage(message) {
290
+ return {
291
+ id: message.id,
292
+ message_id: message.id,
293
+ channel: message.runtimeNamespace === 'default' ? 'evomap-hub' : message.runtimeNamespace,
294
+ direction: message.direction,
295
+ type: message.type,
296
+ status: message.status === 'done' ? (message.direction === 'inbound' ? 'delivered' : 'synced') : message.status,
297
+ payload: message.payload,
298
+ priority: message.priority,
299
+ ref_id: message.replyTo ?? message.correlationId,
300
+ created_at: message.createdAt,
301
+ synced_at: message.status === 'done' ? message.updatedAt : null,
302
+ expires_at: message.ttlAt,
303
+ retry_count: message.attempts,
304
+ next_retry_at: message.nextRetryAt,
305
+ error: message.lastError,
306
+ };
307
+ }
308
+ function legacyMailboxStatus(value) {
309
+ if (value === undefined)
310
+ return undefined;
311
+ if (STATUSES.has(value))
312
+ return value;
313
+ if (value === 'delivered' || value === 'synced' || value === 'acked')
314
+ return 'done';
315
+ if (value === 'rejected')
316
+ return 'failed';
317
+ return undefined;
168
318
  }
@@ -1,7 +1,59 @@
1
1
  import { type Envelope, type Status, type CreateEnvelopeInput } from './envelope.js';
2
2
  export declare const MAX_ATTEMPTS = 5;
3
3
  export declare const DEFAULT_IMPORT_JSONL_MAX_LINE_BYTES: number;
4
+ export declare const DEFAULT_IMPORT_JSONL_MAX_BYTES: number;
5
+ export declare const DEFAULT_IMPORT_JSONL_MAX_RECORDS = 100000;
6
+ export declare const MAILBOX_CLAIM_OWNER: unique symbol;
7
+ export type ClaimedEnvelope = Envelope & {
8
+ readonly [MAILBOX_CLAIM_OWNER]: string;
9
+ };
10
+ export type MailboxImportRecord = Readonly<{
11
+ kind: 'message';
12
+ envelope: Readonly<Envelope>;
13
+ }> | Readonly<{
14
+ kind: 'update';
15
+ id: string;
16
+ fields: Readonly<Record<string, unknown>>;
17
+ }>;
18
+ export type PreparedMailboxImport = Readonly<{
19
+ records: readonly MailboxImportRecord[];
20
+ sourceRecords: number;
21
+ messageCandidates: number;
22
+ uniqueMessageCandidates: number;
23
+ updateCandidates: number;
24
+ }>;
25
+ export type MailboxImportPreview = Readonly<{
26
+ sourceRecords: number;
27
+ messageCandidates: number;
28
+ uniqueMessageCandidates: number;
29
+ updateCandidates: number;
30
+ insertedMessages: number;
31
+ updatedMessages: number;
32
+ managedMessages: number;
33
+ protectedMessages: number;
34
+ }>;
35
+ export type MailboxJsonlImportErrorCode = 'line_too_large' | 'journal_too_large' | 'too_many_records' | 'invalid_utf8' | 'invalid_json' | 'invalid_record' | 'invalid_update' | 'unsupported_operation' | 'invalid_message';
36
+ export declare class MailboxJsonlImportError extends Error {
37
+ readonly code: MailboxJsonlImportErrorCode;
38
+ readonly line: number;
39
+ constructor(code: MailboxJsonlImportErrorCode, line: number, message: string);
40
+ }
41
+ export declare function mailboxClaimOwner(envelope: Envelope): string | undefined;
4
42
  export declare function expBackoffMs(attempt: number): number;
43
+ /** Read one mailbox state value without creating, migrating, or taking write ownership of the database. */
44
+ export declare function readMailboxState(path: string, key: string): string | undefined;
45
+ /**
46
+ * Parse and normalize a V1 mailbox journal without opening or mutating a V2 mailbox.
47
+ * The returned batch is sealed so dry-run and apply can consume the exact same records.
48
+ */
49
+ export declare function prepareMailboxJsonlImport(source: string | number, options?: {
50
+ maxLineBytes?: number;
51
+ maxBytes?: number;
52
+ maxRecords?: number;
53
+ now?: number;
54
+ }): PreparedMailboxImport;
55
+ /** Preview a prepared import against an optional existing mailbox without creating or migrating it. */
56
+ export declare function previewPreparedMailboxImport(batch: PreparedMailboxImport, mailboxPath?: string): MailboxImportPreview;
5
57
  /** mailbox sqlite 引擎 (WAL + busy_timeout). 状态机 pending→in_flight→done/failed/expired + 租约 + 重试/DLQ. */
6
58
  export declare class MailboxStore {
7
59
  private readonly db;
@@ -47,12 +99,31 @@ export declare class MailboxStore {
47
99
  hasMessageWithIdempotencyKey(idempotencyKey: string): boolean;
48
100
  hasMessageWithPayload(type: string, payload: unknown): boolean;
49
101
  /** 原子 claim: pending(或租约过期的 in_flight) → in_flight + 租约. 可按 runtimeNamespace 分区. */
50
- claim(handler: string, limit: number, leaseMs: number, now: number, runtimeNamespace?: string): Envelope[];
102
+ claim(handler: string, limit: number, leaseMs: number, now: number, runtimeNamespace?: string): ClaimedEnvelope[];
51
103
  complete(id: string, now: number): void;
52
104
  /** 失败: attempts<N→退避回 pending; ≥N→DLQ(不自动丢). */
53
105
  fail(id: string, err: string, now: number, maxAttempts?: number): void;
54
106
  /** 暂缓: transient upstream outage 不消耗 attempts, 仅设置下次可 claim 时间. */
55
107
  defer(id: string, err: string, now: number, retryAfterMs: number): void;
108
+ deferUnlessProcessed(id: string, processedKey: string, err: string, now: number, retryAfterMs: number): boolean;
109
+ failUnlessProcessed(id: string, processedKey: string, err: string, now: number, maxAttempts?: number): boolean;
110
+ completeClaimed(id: string, now: number, claimOwner: string): boolean;
111
+ /** Atomically complete the current worker's lease and persist its idempotency result. */
112
+ completeClaimedAndMarkProcessed(id: string, processedKey: string, result: unknown, now: number, claimOwner: string): boolean;
113
+ renewClaim(id: string, now: number, leaseMs: number, claimOwner: string): boolean;
114
+ setClaimedCheckpoint(id: string, checkpoint: unknown, now: number, claimOwner: string): boolean;
115
+ getClaimedCheckpoint(id: string): unknown | undefined;
116
+ deferClaimed(id: string, err: string, now: number, retryAfterMs: number, claimOwner: string): boolean;
117
+ failClaimed(id: string, err: string, now: number, claimOwner: string, maxAttempts?: number): boolean;
118
+ /** Transition only the row held by the current worker, unless a concurrent success marker already exists. */
119
+ deferClaimedUnlessProcessed(id: string, processedKey: string, err: string, now: number, retryAfterMs: number, claimOwner: string): boolean;
120
+ /** Transition only the row held by the current worker, unless a concurrent success marker already exists. */
121
+ failClaimedUnlessProcessed(id: string, processedKey: string, err: string, now: number, claimOwner: string, maxAttempts?: number): boolean;
122
+ /** Atomically fail a non-leased intent and persist the replayable terminal result. */
123
+ failAndMarkProcessedUnlessProcessed(id: string, blockingProcessedKeys: readonly string[], resultKey: string, result: unknown, err: string, now: number, maxAttempts?: number): boolean;
124
+ /** Atomically fail the current worker's leased intent and persist the replayable terminal result. */
125
+ failClaimedAndMarkProcessedUnlessProcessed(id: string, blockingProcessedKeys: readonly string[], resultKey: string, result: unknown, err: string, now: number, claimOwner: string, maxAttempts?: number): boolean;
126
+ private transitionUnlessProcessed;
56
127
  /** DLQ 重放(人工/agent 显式, 不自动丢). */
57
128
  replayDlq(id: string, now: number): void;
58
129
  dlq(): Envelope[];
@@ -73,16 +144,31 @@ export declare class MailboxStore {
73
144
  id: string;
74
145
  type: string;
75
146
  status: Status;
147
+ priority: Envelope['priority'];
76
148
  attempts: number;
77
149
  nextRetryAt: number | null;
78
150
  ttlAt: number | null;
79
151
  dlq: boolean;
152
+ lastError: string | null;
80
153
  } | undefined;
81
154
  /** 幂等(A13): 副作用 handler 用 idempotencyKey 去重; 命中返缓存不重跑. */
82
155
  isProcessed(key: string): boolean;
83
156
  getProcessed(key: string): unknown;
84
157
  markProcessed(key: string, result: unknown, now: number): void;
85
- /** v1 messages.jsonl sqlite 迁移(幂等可重入). */
86
- importJsonl(path: string): number;
158
+ replaceProcessed(key: string, result: unknown, now: number): void;
159
+ /** Persist a monotonic outcome and its lightweight concurrency marker in one SQLite transaction. */
160
+ replaceProcessedWithMarker(key: string, result: unknown, markerKey: string, markerResult: unknown, now: number): void;
161
+ /** Backfill a concurrency marker only when the durable source result still satisfies the caller's predicate. */
162
+ markProcessedIf(sourceKey: string, markerKey: string, markerResult: unknown, now: number, matches: (sourceResult: unknown) => boolean): boolean;
163
+ deleteProcessed(keys: readonly string[]): void;
164
+ private messageClaimOwner;
165
+ /** Build the target-aware plan shared by preview and apply. */
166
+ private buildPreparedImportPlan;
167
+ /** Preview the exact import result against the current mailbox snapshot without writing to it. */
168
+ previewPreparedImport(batch: PreparedMailboxImport): MailboxImportPreview;
169
+ private writeImportedEnvelopeFields;
170
+ /** Apply a parsed batch; planning and writes share one transaction and one normalized journal. */
171
+ importPrepared(batch: PreparedMailboxImport): number;
172
+ importJsonl(source: string | number): number;
87
173
  close(): void;
88
174
  }