@evomap/evolver-core 2.0.0-beta.1 → 2.0.0-beta.2

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 (75) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +17 -11
  3. package/dist/algo/capabilityCandidates.d.ts +2 -0
  4. package/dist/algo/capabilityCandidates.js +5 -0
  5. package/dist/algo/conversationSniffer.d.ts +18 -0
  6. package/dist/algo/conversationSniffer.js +132 -0
  7. package/dist/algo/cycleEngine.js +2 -0
  8. package/dist/algo/cycleFailureClassifier.js +13 -5
  9. package/dist/algo/geneIntake.d.ts +14 -4
  10. package/dist/algo/geneIntake.js +37 -9
  11. package/dist/algo/geneSelection.d.ts +6 -0
  12. package/dist/algo/geneSelection.js +19 -11
  13. package/dist/algo/index.d.ts +1 -0
  14. package/dist/algo/index.js +1 -0
  15. package/dist/assetstore/assetSyncLedger.d.ts +28 -0
  16. package/dist/assetstore/assetSyncLedger.js +86 -0
  17. package/dist/assetstore/index.d.ts +3 -1
  18. package/dist/assetstore/index.js +3 -1
  19. package/dist/assetstore/localJsonl.d.ts +9 -2
  20. package/dist/assetstore/localJsonl.js +75 -23
  21. package/dist/assetstore/pendingSignals.js +3 -1
  22. package/dist/assetstore/provenance.d.ts +7 -2
  23. package/dist/assetstore/provenance.js +85 -25
  24. package/dist/assetstore/reviewFilter.js +2 -1
  25. package/dist/assetstore/reviewLedger.d.ts +6 -0
  26. package/dist/assetstore/reviewLedger.js +9 -0
  27. package/dist/assetstore/seedGenes.d.ts +3 -0
  28. package/dist/assetstore/seedGenes.js +134 -0
  29. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  30. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  31. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  32. package/dist/benchmark/antiGeneImpact.js +34 -0
  33. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  34. package/dist/benchmark/antiGeneRollout.js +13 -1
  35. package/dist/events/eventArchive.d.ts +65 -0
  36. package/dist/events/eventArchive.js +343 -0
  37. package/dist/events/eventStore.js +2 -12
  38. package/dist/events/public.d.ts +3 -1
  39. package/dist/events/public.js +2 -1
  40. package/dist/events/retention.d.ts +24 -1
  41. package/dist/events/retention.js +133 -37
  42. package/dist/exec/autoExec.js +3 -0
  43. package/dist/exec/claudeBridge.js +23 -0
  44. package/dist/exec/runnerRegistry.d.ts +8 -6
  45. package/dist/exec/runnerRegistry.js +71 -18
  46. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  47. package/dist/exec/selfPrObfuscation.js +19 -6
  48. package/dist/hub/agentDirectory.d.ts +90 -0
  49. package/dist/hub/agentDirectory.js +104 -0
  50. package/dist/hub/bindings.js +13 -3
  51. package/dist/hub/capability.d.ts +14 -2
  52. package/dist/hub/fake.d.ts +4 -2
  53. package/dist/hub/fake.js +3 -2
  54. package/dist/hub/index.d.ts +1 -0
  55. package/dist/hub/index.js +1 -0
  56. package/dist/mailbox/catalog.js +3 -0
  57. package/dist/mailbox/ipcServer.js +2 -2
  58. package/dist/mailbox/store.d.ts +14 -0
  59. package/dist/mailbox/store.js +44 -2
  60. package/dist/material/consumer.js +9 -6
  61. package/dist/material/index.d.ts +1 -0
  62. package/dist/material/index.js +1 -0
  63. package/dist/material/materialArchive.d.ts +81 -0
  64. package/dist/material/materialArchive.js +466 -0
  65. package/dist/material/materialStore.d.ts +1 -0
  66. package/dist/material/materialStore.js +6 -13
  67. package/dist/schema/common.d.ts +1 -1
  68. package/dist/schema/common.js +1 -1
  69. package/dist/schema/material.d.ts +5 -5
  70. package/dist/trace/trajectoryExport.js +2 -2
  71. package/dist/wire/geneHints.d.ts +42 -1
  72. package/dist/wire/geneHints.js +52 -1
  73. package/dist/wire/index.d.ts +14 -2
  74. package/dist/wire/index.js +1 -1
  75. package/package.json +2 -1
@@ -0,0 +1,104 @@
1
+ export const AGENT_DIRECTORY_DEFAULT_LIMIT = 20;
2
+ export const AGENT_DIRECTORY_MAX_LIMIT = 50;
3
+ export const AGENT_DIRECTORY_DEFAULT_TIMEOUT_MS = 8_000;
4
+ export const AGENT_DIRECTORY_MAX_TIMEOUT_MS = 30_000;
5
+ export const AGENT_DIRECTORY_MAX_QUERY_LENGTH = 500;
6
+ export const AGENT_DIRECTORY_MAX_SIGNAL_COUNT = 20;
7
+ export const AGENT_DIRECTORY_MAX_SIGNAL_LENGTH = 64;
8
+ export const AGENT_DIRECTORY_MAX_CURSOR_LENGTH = 256;
9
+ export const AGENT_DIRECTORY_MAX_AGENT_ID_LENGTH = 128;
10
+ export class AgentDirectoryInputError extends Error {
11
+ code = 'invalid_request';
12
+ }
13
+ export function normalizeAgentSearchRequest(input) {
14
+ const query = optionalText(input.query, 'query', AGENT_DIRECTORY_MAX_QUERY_LENGTH);
15
+ const signals = normalizeSignals(input.signals);
16
+ const availability = input.availability ? enumValue(input.availability, ['online', 'busy', 'offline', 'unknown'], 'availability') : undefined;
17
+ if (!query && signals.length === 0)
18
+ throw new AgentDirectoryInputError('query_or_signals_required');
19
+ return {
20
+ ...(query ? { query } : {}),
21
+ ...(signals.length > 0 ? { signals } : {}),
22
+ ...(availability ? { availability } : {}),
23
+ ...(input.cursor ? { cursor: requiredText(input.cursor, 'cursor', AGENT_DIRECTORY_MAX_CURSOR_LENGTH) } : {}),
24
+ limit: boundedInteger(input.limit, AGENT_DIRECTORY_DEFAULT_LIMIT, 1, AGENT_DIRECTORY_MAX_LIMIT, 'limit'),
25
+ timeoutMs: boundedInteger(input.timeoutMs, AGENT_DIRECTORY_DEFAULT_TIMEOUT_MS, 100, AGENT_DIRECTORY_MAX_TIMEOUT_MS, 'timeout_ms'),
26
+ sort: input.sort ? enumValue(input.sort, ['relevance', 'reputation', 'recent', 'availability'], 'sort') : 'relevance',
27
+ order: input.order ? enumValue(input.order, ['asc', 'desc'], 'order') : 'desc',
28
+ };
29
+ }
30
+ export function normalizeAgentTaskDiscoveryRequest(input) {
31
+ const title = requiredText(input.title, 'title', AGENT_DIRECTORY_MAX_QUERY_LENGTH);
32
+ const description = optionalText(input.description, 'description', AGENT_DIRECTORY_MAX_QUERY_LENGTH);
33
+ const search = normalizeAgentSearchRequest({
34
+ query: [title, description].filter(Boolean).join('\n'),
35
+ signals: input.signals,
36
+ availability: input.availability,
37
+ sort: input.sort,
38
+ order: input.order,
39
+ cursor: input.cursor,
40
+ limit: input.limit,
41
+ timeoutMs: input.timeoutMs,
42
+ });
43
+ return { ...search, title, ...(description ? { description } : {}) };
44
+ }
45
+ export function normalizeAgentId(value) {
46
+ return requiredText(value, 'agent_id', AGENT_DIRECTORY_MAX_AGENT_ID_LENGTH);
47
+ }
48
+ export function normalizeAgentDirectoryTimeout(value) {
49
+ return boundedInteger(value, AGENT_DIRECTORY_DEFAULT_TIMEOUT_MS, 100, AGENT_DIRECTORY_MAX_TIMEOUT_MS, 'timeout_ms');
50
+ }
51
+ export function capabilityUnavailable(message = 'agent_directory_not_supported') {
52
+ return { ok: false, error: { code: 'capability_unavailable', retryable: false, message } };
53
+ }
54
+ export function unsupportedAgentDirectoryCapability(message) {
55
+ return {
56
+ search: async () => capabilityUnavailable(message),
57
+ getProfile: async () => capabilityUnavailable(message),
58
+ discoverForTask: async () => capabilityUnavailable(message),
59
+ };
60
+ }
61
+ function normalizeSignals(input) {
62
+ if (input === undefined)
63
+ return [];
64
+ if (!Array.isArray(input))
65
+ throw new AgentDirectoryInputError('signals_must_be_array');
66
+ if (input.length > AGENT_DIRECTORY_MAX_SIGNAL_COUNT)
67
+ throw new AgentDirectoryInputError('too_many_signals');
68
+ const out = [];
69
+ for (const value of input) {
70
+ if (typeof value !== 'string')
71
+ throw new AgentDirectoryInputError('signal_must_be_string');
72
+ const signal = requiredText(value, 'signal', AGENT_DIRECTORY_MAX_SIGNAL_LENGTH);
73
+ if (!out.includes(signal))
74
+ out.push(signal);
75
+ }
76
+ return out;
77
+ }
78
+ function optionalText(value, field, maxLength) {
79
+ if (value === undefined)
80
+ return undefined;
81
+ return requiredText(value, field, maxLength);
82
+ }
83
+ function requiredText(value, field, maxLength) {
84
+ if (typeof value !== 'string')
85
+ throw new AgentDirectoryInputError(`${field}_must_be_string`);
86
+ const text = value.trim();
87
+ if (!text)
88
+ throw new AgentDirectoryInputError(`${field}_required`);
89
+ if (text.length > maxLength)
90
+ throw new AgentDirectoryInputError(`${field}_too_long`);
91
+ return text;
92
+ }
93
+ function boundedInteger(value, fallback, min, max, field) {
94
+ if (value === undefined)
95
+ return fallback;
96
+ if (!Number.isInteger(value) || value < min || value > max)
97
+ throw new AgentDirectoryInputError(`${field}_out_of_range`);
98
+ return value;
99
+ }
100
+ function enumValue(value, allowed, field) {
101
+ if (!allowed.includes(value))
102
+ throw new AgentDirectoryInputError(`${field}_invalid`);
103
+ return value;
104
+ }
@@ -19,7 +19,7 @@ function envelopeToAgentEvent(e) {
19
19
  const stableId = e.type === 'proxy_trace' && e.idempotencyKey ? e.idempotencyKey : e.id;
20
20
  return {
21
21
  id: stableId,
22
- type: e.type,
22
+ type: e.type === 'dm_outbound' ? 'dm' : e.type,
23
23
  payload: e.payload,
24
24
  priority: 'medium',
25
25
  createdAt: e.createdAt,
@@ -59,10 +59,17 @@ export function makeHubBindings(cap, options = {}) {
59
59
  throw new PublishRejectedError(r.status, r.terminal ?? true, r.reason);
60
60
  return r;
61
61
  }
62
- case 'task_claim': return cap.task.claim(String(e.payload.taskId ?? ''));
62
+ case 'task_claim': {
63
+ const p = e.payload;
64
+ return cap.task.claim(firstString(p.taskId, p.task_id) ?? '');
65
+ }
63
66
  case 'task_complete': {
64
67
  const p = e.payload;
65
- return cap.task.complete(String(p.claimId ?? ''), p.result);
68
+ const taskId = firstString(p.taskId, p.task_id);
69
+ const assetId = firstString(p.assetId, p.asset_id);
70
+ const claimId = firstString(p.claimId, p.claim_id) ?? taskId ?? '';
71
+ const context = taskId && assetId ? { taskId, assetId } : undefined;
72
+ return cap.task.complete(claimId, p.result, context);
66
73
  }
67
74
  default:
68
75
  // 其余 outbound(atp_*/heartbeat/...) 走 mailbox.push
@@ -87,4 +94,7 @@ export function makeHubBindings(cap, options = {}) {
87
94
  };
88
95
  },
89
96
  };
97
+ }
98
+ function firstString(...values) {
99
+ return values.find((value) => typeof value === 'string' && value.length > 0);
90
100
  }
@@ -1,5 +1,6 @@
1
1
  import type { AssetRecord, AssetKind, SearchQuery, PutResult } from '../assetstore/provider.js';
2
2
  import type { Envelope } from '../mailbox/envelope.js';
3
+ import type { AgentDirectoryCapability } from './agentDirectory.js';
3
4
  /** publish 投递回执. hub 侧做 gate(经济/治理/RBAC 全在 hub), core 只读 status+reason. */
4
5
  export interface PublishReceipt {
5
6
  /** hub 返回的回执 id(公版=AgentEvent/decision id; 私有=审计 id). */
@@ -34,6 +35,15 @@ export interface TaskEvent {
34
35
  priority: 'high' | 'medium' | 'low';
35
36
  createdAt: number;
36
37
  }
38
+ /**
39
+ * Explicit task identity needed by hubs whose completion wire does not use their
40
+ * adapter-specific claim handle. claimId remains opaque and must never be parsed
41
+ * to recover either field.
42
+ */
43
+ export interface TaskCompleteContext {
44
+ taskId: string;
45
+ assetId: string;
46
+ }
37
47
  /** Proactive Hub question that a runtime may submit for ecosystem help. */
38
48
  export interface HubQuestion {
39
49
  question: string;
@@ -153,7 +163,7 @@ export interface RecipeCapability {
153
163
  get(recipeId: string): Promise<RecipeFetchReceipt>;
154
164
  express(recipeId: string, request?: RecipeExpressRequest): Promise<RecipeExpressionReceipt>;
155
165
  }
156
- export type HubCapabilityName = 'publish' | 'fetch' | 'search' | 'task' | 'mailbox' | 'auth' | 'audit' | 'air_gap' | 'tenant_isolation' | 'marketplace' | 'economy' | 'questions' | 'recipes';
166
+ export type HubCapabilityName = 'publish' | 'fetch' | 'search' | 'task' | 'mailbox' | 'auth' | 'audit' | 'air_gap' | 'tenant_isolation' | 'marketplace' | 'economy' | 'questions' | 'recipes' | 'agent_directory';
157
167
  /** hub 能力清单(可选). core 据此动态适配 UI/行为, "按需耦合". */
158
168
  export interface HubManifest {
159
169
  capabilities: HubCapabilityName[] | string[];
@@ -232,7 +242,7 @@ export interface HubCapability {
232
242
  claim(taskId: string): Promise<{
233
243
  claimId: string;
234
244
  }>;
235
- complete(claimId: string, result: unknown): Promise<{
245
+ complete(claimId: string, result: unknown, context?: TaskCompleteContext): Promise<{
236
246
  status: 'completed';
237
247
  }>;
238
248
  subscribe(filter: unknown): AsyncIterable<TaskEvent>;
@@ -243,6 +253,8 @@ export interface HubCapability {
243
253
  };
244
254
  /** Optional recipe/DNA seam. Recipe creation is draft-first; publishing remains explicit. */
245
255
  recipes?: RecipeCapability;
256
+ /** Optional agent discovery seam. Missing means the adapter does not support directory/profile discovery. */
257
+ agentDirectory?: AgentDirectoryCapability;
246
258
  /** mailbox 队列. poll/ack/push/status 四端点解耦(公版 /a2a/mailbox/*). */
247
259
  mailbox: {
248
260
  poll(): Promise<MailboxPollResult>;
@@ -1,4 +1,4 @@
1
- import type { HubCapability, PublishReceipt, AgentEvent, TaskEvent, AuthProvider, HubQuery, AssetRecord } from './capability.js';
1
+ import type { HubCapability, PublishReceipt, AgentEvent, TaskEvent, AuthProvider, HubQuery, AssetRecord, TaskCompleteContext } from './capability.js';
2
2
  export interface FakeHubOptions {
3
3
  /** 脚本化 publish gate: 返回 reject/quarantine 以测 PublishReceipt 终态路径. */
4
4
  publishGate?: (asset: AssetRecord) => Pick<PublishReceipt, 'status' | 'reason' | 'terminal'>;
@@ -20,8 +20,10 @@ export declare class FakeHubCapability implements HubCapability {
20
20
  readonly completed: Array<{
21
21
  claimId: string;
22
22
  result: unknown;
23
+ context?: TaskCompleteContext;
23
24
  }>;
24
25
  readonly auth: AuthProvider;
26
+ agentDirectory?: import('./agentDirectory.js').AgentDirectoryCapability;
25
27
  nextPollAfterMs: number | undefined;
26
28
  constructor(opts?: FakeHubOptions);
27
29
  /** 测试注入: 排一条 inbound 事件供 poll 拉. */
@@ -33,7 +35,7 @@ export declare class FakeHubCapability implements HubCapability {
33
35
  claim: (taskId: string) => Promise<{
34
36
  claimId: string;
35
37
  }>;
36
- complete: (claimId: string, result: unknown) => Promise<{
38
+ complete: (claimId: string, result: unknown, context?: TaskCompleteContext) => Promise<{
37
39
  status: "completed";
38
40
  }>;
39
41
  subscribe: (filter: unknown) => AsyncIterable<TaskEvent>;
package/dist/hub/fake.js CHANGED
@@ -19,6 +19,7 @@ export class FakeHubCapability {
19
19
  claims = [];
20
20
  completed = [];
21
21
  auth = fakeAuth;
22
+ agentDirectory;
22
23
  nextPollAfterMs;
23
24
  constructor(opts = {}) {
24
25
  this.opts = opts;
@@ -42,8 +43,8 @@ export class FakeHubCapability {
42
43
  this.claims.push({ taskId, claimId });
43
44
  return { claimId };
44
45
  },
45
- complete: async (claimId, result) => {
46
- this.completed.push({ claimId, result });
46
+ complete: async (claimId, result, context) => {
47
+ this.completed.push({ claimId, result, ...(context ? { context } : {}) });
47
48
  return { status: 'completed' };
48
49
  },
49
50
  subscribe: async function* () {
@@ -1,4 +1,5 @@
1
1
  export * from './capability.js';
2
+ export * from './agentDirectory.js';
2
3
  export * from './fake.js';
3
4
  export * from './bindings.js';
4
5
  export * from './sanitize.js';
package/dist/hub/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './capability.js';
2
+ export * from './agentDirectory.js';
2
3
  export * from './fake.js';
3
4
  export * from './bindings.js';
4
5
  export * from './sanitize.js';
@@ -14,6 +14,7 @@ export const MESSAGE_CATALOG = {
14
14
  task_complete: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'default' },
15
15
  task_subscribe: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'control' },
16
16
  task_unsubscribe: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'control' },
17
+ dm_outbound: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'default' },
17
18
  atp_order: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'default' },
18
19
  atp_deliver: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'default' },
19
20
  atp_settle: { direction: 'outbound', handler: 'proxy', feedsMaterial: false, ttlClass: 'default' },
@@ -23,6 +24,8 @@ export const MESSAGE_CATALOG = {
23
24
  asset_publish_result: { direction: 'inbound', handler: 'core', feedsMaterial: false, ttlClass: 'default' }, // 拒绝进 Material 在 dispatch 决定
24
25
  pending_evomap_task: { direction: 'inbound', handler: 'agent', feedsMaterial: false, ttlClass: 'default' },
25
26
  task_available: { direction: 'inbound', handler: 'agent', feedsMaterial: false, ttlClass: 'default' },
27
+ task_claim_result: { direction: 'inbound', handler: 'agent', feedsMaterial: false, ttlClass: 'default' },
28
+ task_complete_result: { direction: 'inbound', handler: 'agent', feedsMaterial: false, ttlClass: 'default' },
26
29
  work_assigned: { direction: 'inbound', handler: 'agent', feedsMaterial: false, ttlClass: 'default' },
27
30
  dialog_message: { direction: 'inbound', handler: 'agent', feedsMaterial: true, ttlClass: 'default' },
28
31
  peer_review_request: { direction: 'inbound', handler: 'agent', feedsMaterial: true, ttlClass: 'default' },
@@ -69,7 +69,7 @@ export class MailboxIpcServer {
69
69
  this.onAuthFailure?.();
70
70
  }
71
71
  catch { /* auth failure handling must never weaken fail-closed IPC auth */ }
72
- return this.json(res, 401, { error: 'bad token' });
72
+ return this.json(res, 401, { error: 'Unauthorized', code: 'unauthorized' });
73
73
  }
74
74
  const url = new URL(req.url ?? '/', 'http://localhost');
75
75
  const route = `${req.method} ${url.pathname}`;
@@ -129,7 +129,7 @@ export class MailboxIpcServer {
129
129
  return this.json(res, 404, { error: 'unknown route' });
130
130
  }
131
131
  catch (e) {
132
- return this.json(res, 400, { error: e instanceof Error ? e.message : String(e) });
132
+ return this.json(res, 400, { error: e instanceof Error ? e.message : String(e), code: 'invalid_request' });
133
133
  }
134
134
  }
135
135
  readJson(req) {
@@ -22,8 +22,22 @@ export declare class MailboxStore {
22
22
  status?: Status;
23
23
  handler?: string;
24
24
  runtimeNamespace?: string;
25
+ type?: string;
26
+ direction?: Envelope['direction'];
27
+ typeDirections?: readonly {
28
+ type: string;
29
+ direction: Envelope['direction'];
30
+ }[];
31
+ newestFirst?: boolean;
32
+ offset?: number;
25
33
  limit?: number;
26
34
  }): Envelope[];
35
+ countMessages(opts?: {
36
+ status?: Status;
37
+ runtimeNamespace?: string;
38
+ type?: string;
39
+ direction?: Envelope['direction'];
40
+ }): number;
27
41
  countByStatus(status: Status): number;
28
42
  /** pending 计数(可按 handler/runtimeNamespace 分区), 用于 agent wake 去抖. */
29
43
  countPending(handler?: string, runtimeNamespace?: string): number;
@@ -120,6 +120,7 @@ export class MailboxStore {
120
120
  schemaVersion TEXT, feedsMaterial INTEGER, dlq INTEGER DEFAULT 0, leasedUntil INTEGER, workerId TEXT, lastError TEXT)`);
121
121
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_status ON messages(status, handler)');
122
122
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_corr ON messages(correlationId)');
123
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_message_view ON messages(runtimeNamespace,type,direction,status,createdAt)');
123
124
  this.db.exec('CREATE TABLE IF NOT EXISTS idempotency (key TEXT PRIMARY KEY, result TEXT, at INTEGER)');
124
125
  this.db.exec('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)');
125
126
  }
@@ -157,7 +158,21 @@ export class MailboxStore {
157
158
  where.push('runtimeNamespace = ?');
158
159
  args.push(opts.runtimeNamespace);
159
160
  }
160
- const sql = `SELECT * FROM messages ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY createdAt LIMIT ?`;
161
+ if (opts.type) {
162
+ where.push('type = ?');
163
+ args.push(opts.type);
164
+ }
165
+ if (opts.direction) {
166
+ where.push('direction = ?');
167
+ args.push(opts.direction);
168
+ }
169
+ if (opts.typeDirections && opts.typeDirections.length > 0) {
170
+ const selectors = opts.typeDirections.slice(0, 20);
171
+ where.push(`(${selectors.map(() => '(type = ? AND direction = ?)').join(' OR ')})`);
172
+ for (const selector of selectors)
173
+ args.push(selector.type, selector.direction);
174
+ }
175
+ const sql = `SELECT * FROM messages ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY createdAt ${opts.newestFirst ? 'DESC' : 'ASC'} LIMIT ?`;
161
176
  // Clamp the LIMIT bind. The IPC list route feeds this `?limit=` straight from an external query string, so a
162
177
  // non-finite value (NaN from `?limit=abc`, Infinity from `?limit=1e400`) would bind as null/error and a huge
163
178
  // finite value is an unbounded read (memory DoS) even on the token-authed endpoint. Finite + positive + capped.
@@ -165,7 +180,34 @@ export class MailboxStore {
165
180
  ? Math.min(Math.floor(opts.limit), 10_000)
166
181
  : 1000;
167
182
  args.push(lim);
168
- return this.db.prepare(sql).all(...args).map(rowToEnvelope);
183
+ const offset = Number.isFinite(opts.offset) && opts.offset > 0
184
+ ? Math.min(Math.floor(opts.offset), 10_000)
185
+ : 0;
186
+ const pagedSql = `${sql} OFFSET ?`;
187
+ args.push(offset);
188
+ return this.db.prepare(pagedSql).all(...args).map(rowToEnvelope);
189
+ }
190
+ countMessages(opts = {}) {
191
+ const where = [];
192
+ const args = [];
193
+ if (opts.status) {
194
+ where.push('status = ?');
195
+ args.push(opts.status);
196
+ }
197
+ if (opts.runtimeNamespace) {
198
+ where.push('runtimeNamespace = ?');
199
+ args.push(opts.runtimeNamespace);
200
+ }
201
+ if (opts.type) {
202
+ where.push('type = ?');
203
+ args.push(opts.type);
204
+ }
205
+ if (opts.direction) {
206
+ where.push('direction = ?');
207
+ args.push(opts.direction);
208
+ }
209
+ const row = this.db.prepare(`SELECT COUNT(*) c FROM messages ${where.length ? `WHERE ${where.join(' AND ')}` : ''}`).get(...args);
210
+ return Number(row['c']);
169
211
  }
170
212
  countByStatus(status) {
171
213
  return Number(this.db.prepare('SELECT COUNT(*) c FROM messages WHERE status = ?').get(status)['c']);
@@ -11,9 +11,10 @@ export class ConsumerGroups {
11
11
  if (opts.path && existsSync(opts.path)) {
12
12
  try {
13
13
  const o = JSON.parse(readFileSync(opts.path, 'utf8'));
14
- for (const [k, v] of Object.entries(o))
15
- if (typeof v === 'number' && Number.isFinite(v))
14
+ for (const [k, v] of Object.entries(o)) {
15
+ if (typeof v === 'number' && Number.isSafeInteger(v) && v >= 0)
16
16
  this.committed.set(k, v);
17
+ }
17
18
  }
18
19
  catch { /* corrupt cursor → start from 0 (idempotent re-claim) */ }
19
20
  }
@@ -21,16 +22,18 @@ export class ConsumerGroups {
21
22
  cur(group) { return this.committed.get(group) ?? 0; }
22
23
  /** 取下一批未 ack 的 (不推进 committed → 崩溃重启再 claim 拿到同批). */
23
24
  claim(group, batchSize) {
24
- const all = this.opts.store.readAll();
25
- return all.slice(this.cur(group), this.cur(group) + batchSize);
25
+ return this.opts.store.readRange(this.cur(group), batchSize);
26
26
  }
27
27
  /** ack: 把 committed 推过 acked 的连续前缀. */
28
28
  ack(group, materialIds) {
29
29
  const ackSet = new Set(materialIds);
30
- const all = this.opts.store.readAll();
31
30
  let c = this.cur(group);
32
- while (c < all.length && ackSet.has(all[c].materialId))
31
+ const claimed = this.opts.store.readRange(c, ackSet.size);
32
+ for (const record of claimed) {
33
+ if (!ackSet.has(record.materialId))
34
+ break;
33
35
  c += 1;
36
+ }
34
37
  this.committed.set(group, c);
35
38
  this.persist();
36
39
  }
@@ -1,4 +1,5 @@
1
1
  export * from './materialStore.js';
2
+ export * from './materialArchive.js';
2
3
  export * from './watermark.js';
3
4
  export * from './sources.js';
4
5
  export * from './consumer.js';
@@ -1,4 +1,5 @@
1
1
  export * from './materialStore.js';
2
+ export * from './materialArchive.js';
2
3
  export * from './watermark.js';
3
4
  export * from './sources.js';
4
5
  export * from './consumer.js';
@@ -0,0 +1,81 @@
1
+ import { type Material } from '../schema/material.js';
2
+ export declare const MATERIAL_ARCHIVE_DEFAULT_KEEP_RECORDS = 1000;
3
+ export declare class InvalidMaterialLogError extends Error {
4
+ readonly line: number;
5
+ readonly code = "MATERIAL_ARCHIVE_INVALID_LOG";
6
+ constructor(line: number, reason: string);
7
+ }
8
+ export declare class InvalidMaterialArchiveError extends Error {
9
+ readonly segment: string;
10
+ readonly line: number;
11
+ readonly code = "MATERIAL_ARCHIVE_INVALID_ARCHIVE";
12
+ constructor(segment: string, line: number, reason: string);
13
+ }
14
+ export declare class MaterialArchiveRangeError extends Error {
15
+ readonly code = "MATERIAL_ARCHIVE_RANGE_INVALID";
16
+ constructor(reason: string);
17
+ }
18
+ export declare class MaterialArchiveSegmentConflictError extends Error {
19
+ readonly archiveId: string;
20
+ readonly code = "MATERIAL_ARCHIVE_SEGMENT_CONFLICT";
21
+ constructor(archiveId: string);
22
+ }
23
+ export declare class InvalidMaterialArchiveCursorError extends Error {
24
+ readonly code = "MATERIAL_ARCHIVE_CURSOR_INVALID";
25
+ constructor(reason: string);
26
+ }
27
+ export interface MaterialArchiveOptions {
28
+ path: string;
29
+ cursorPaths?: readonly string[];
30
+ keepRecords?: number;
31
+ }
32
+ export interface MaterialArchiveCursor {
33
+ group: string;
34
+ position: number;
35
+ }
36
+ export interface MaterialArchivePlan {
37
+ mode: 'preview';
38
+ activeRecords: number;
39
+ physicalActiveRecords: number;
40
+ archiveRecords: number;
41
+ historyRecords: number;
42
+ keepRecords: number;
43
+ minCursor: number;
44
+ cursors: MaterialArchiveCursor[];
45
+ overlapRecords: number;
46
+ wouldArchive: number;
47
+ retainedRecords: number;
48
+ archiveId: string | null;
49
+ }
50
+ export interface MaterialArchiveResult {
51
+ mode: 'write';
52
+ activeRecordsBefore: number;
53
+ physicalActiveRecordsBefore: number;
54
+ keepRecords: number;
55
+ minCursor: number;
56
+ cursors: MaterialArchiveCursor[];
57
+ archivedRecords: number;
58
+ retainedRecords: number;
59
+ archiveRecords: number;
60
+ historyRecords: number;
61
+ recoveredOverlap: number;
62
+ archiveId: string | null;
63
+ }
64
+ export interface MaterialArchiveStats {
65
+ exists: boolean;
66
+ segments: number;
67
+ bytes: number;
68
+ records: number;
69
+ invalidLines: number;
70
+ firstOffset: number | null;
71
+ lastOffset: number | null;
72
+ }
73
+ export declare function materialArchiveDir(path: string): string;
74
+ export declare function materialArchiveSegmentName(start: number, end: number): string;
75
+ export declare function planMaterialArchive(opts: MaterialArchiveOptions): MaterialArchivePlan;
76
+ export declare function archiveMaterialStore(opts: MaterialArchiveOptions): MaterialArchiveResult;
77
+ /** Operational, fail-soft full history reader. Strict archive writes validate every line and range. */
78
+ export declare function readMaterialHistory(path: string): Material[];
79
+ /** Absolute-offset range reader used by durable consumers after active records move into archive segments. */
80
+ export declare function readMaterialRange(path: string, start: number, count: number): Material[];
81
+ export declare function inspectMaterialArchive(path: string): MaterialArchiveStats;