@evomap/evolver-core 2.0.0-beta.18 → 2.0.0-beta.19

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 (68) hide show
  1. package/dist/algo/candidateAssembly.js +21 -2
  2. package/dist/algo/cycleEngine.d.ts +12 -0
  3. package/dist/algo/cycleEngine.js +36 -4
  4. package/dist/algo/geneHealth.d.ts +2 -2
  5. package/dist/algo/geneHealth.js +5 -4
  6. package/dist/algo/geneSelection.d.ts +1 -1
  7. package/dist/algo/orchestrator.js +9 -2
  8. package/dist/assetstore/assetSidecarRecords.js +4 -0
  9. package/dist/assetstore/assetStoreHealth.js +41 -24
  10. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  11. package/dist/assetstore/assetStoreStorage.js +16 -7
  12. package/dist/assetstore/localJsonl.d.ts +2 -1
  13. package/dist/assetstore/localJsonl.js +54 -10
  14. package/dist/assetstore/provenance.d.ts +24 -0
  15. package/dist/assetstore/provenance.js +219 -12
  16. package/dist/assetstore/provider.d.ts +20 -1
  17. package/dist/assetstore/provider.js +34 -1
  18. package/dist/bootstrap/index.d.ts +2 -1
  19. package/dist/bootstrap/index.js +2 -1
  20. package/dist/bootstrap/v1EnvCompat.d.ts +110 -0
  21. package/dist/bootstrap/v1EnvCompat.js +256 -0
  22. package/dist/events/public.d.ts +1 -1
  23. package/dist/events/public.js +1 -1
  24. package/dist/events/reports.d.ts +2 -0
  25. package/dist/events/reports.js +4 -0
  26. package/dist/exec/autoExec.d.ts +1 -1
  27. package/dist/exec/autoExec.js +8 -10
  28. package/dist/exec/autonomousCycle.d.ts +19 -4
  29. package/dist/exec/autonomousCycle.js +63 -13
  30. package/dist/exec/claudeBridge.d.ts +25 -7
  31. package/dist/exec/claudeBridge.js +264 -29
  32. package/dist/exec/prompt.js +5 -1
  33. package/dist/exec/runnerRegistry.d.ts +68 -26
  34. package/dist/exec/runnerRegistry.js +307 -72
  35. package/dist/exec/selfPr.js +1 -7
  36. package/dist/feedback/envelope.d.ts +61 -0
  37. package/dist/feedback/envelope.js +168 -0
  38. package/dist/feedback/index.d.ts +1 -0
  39. package/dist/feedback/index.js +1 -0
  40. package/dist/hub/assetCallLog.d.ts +35 -1
  41. package/dist/hub/assetCallLog.js +124 -1
  42. package/dist/hub/bindings.d.ts +8 -1
  43. package/dist/hub/bindings.js +17 -6
  44. package/dist/hub/capability.d.ts +11 -1
  45. package/dist/hub/fake.d.ts +2 -2
  46. package/dist/hub/fake.js +1 -1
  47. package/dist/index.d.ts +3 -1
  48. package/dist/index.js +4 -1
  49. package/dist/mailbox/dispatch.d.ts +1 -1
  50. package/dist/mailbox/dispatch.js +22 -6
  51. package/dist/mailbox/envelope.d.ts +7 -1
  52. package/dist/mailbox/envelope.js +9 -2
  53. package/dist/mailbox/ipcServer.d.ts +10 -2
  54. package/dist/mailbox/ipcServer.js +163 -13
  55. package/dist/mailbox/store.d.ts +38 -2
  56. package/dist/mailbox/store.js +416 -27
  57. package/dist/signals/curriculum.d.ts +55 -0
  58. package/dist/signals/curriculum.js +202 -0
  59. package/dist/signals/expand.js +17 -6
  60. package/dist/signals/index.d.ts +2 -1
  61. package/dist/signals/index.js +2 -1
  62. package/dist/strategy/constraintAblation.js +115 -369
  63. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  64. package/dist/strategy/constraintAblationPredicates.js +339 -0
  65. package/dist/trace/proxyTurns.js +15 -7
  66. package/dist/verify/validation.d.ts +11 -1
  67. package/dist/verify/validation.js +31 -0
  68. package/package.json +4 -1
@@ -0,0 +1,168 @@
1
+ export const PRIORITY_AXES = Object.freeze([
2
+ 'task_success',
3
+ 'user_preference',
4
+ 'quality',
5
+ 'safety',
6
+ 'cost',
7
+ 'latency',
8
+ 'other',
9
+ ]);
10
+ export const LABELS = Object.freeze(['positive', 'negative', 'mixed', 'neutral']);
11
+ export const ATTENTION_LEVELS = Object.freeze(['full', 'limited', 'skimmed', 'unknown']);
12
+ export const EVIDENCE_KINDS = Object.freeze([
13
+ 'evolution_event',
14
+ 'evolution_outcome',
15
+ 'user_override',
16
+ 'review',
17
+ 'turn',
18
+ 'external',
19
+ ]);
20
+ export function clamp01(value) {
21
+ const n = Number(value);
22
+ if (!Number.isFinite(n))
23
+ return 0.5;
24
+ if (n < 0)
25
+ return 0;
26
+ if (n > 1)
27
+ return 1;
28
+ return n;
29
+ }
30
+ export function labelFromScalar(value) {
31
+ const scalar = clamp01(value);
32
+ if (scalar >= 0.6)
33
+ return 'positive';
34
+ if (scalar <= 0.4)
35
+ return 'negative';
36
+ return 'mixed';
37
+ }
38
+ export function normalizeAttention(input) {
39
+ const record = asRecord(input);
40
+ if (!record)
41
+ return { level: 'unknown' };
42
+ const attention = {
43
+ level: enumValue(record['level'], ATTENTION_LEVELS, 'unknown'),
44
+ };
45
+ const observedItems = nonNegativeInteger(record['observed_items']);
46
+ const elapsedMs = nonNegativeInteger(record['elapsed_ms']);
47
+ if (observedItems !== null)
48
+ attention.observed_items = observedItems;
49
+ if (elapsedMs !== null)
50
+ attention.elapsed_ms = elapsedMs;
51
+ return attention;
52
+ }
53
+ export function evidenceRef(kind, id, options = {}) {
54
+ const ref = {
55
+ kind: enumValue(kind, EVIDENCE_KINDS, 'external'),
56
+ id: String(id ?? '').trim() || 'unknown',
57
+ };
58
+ if (typeof options.summary === 'string' && options.summary.trim()) {
59
+ ref.summary = options.summary.trim();
60
+ }
61
+ return ref;
62
+ }
63
+ export function normalizeEvidenceRef(input) {
64
+ const record = asRecord(input);
65
+ if (!record)
66
+ return evidenceRef('external', 'unknown');
67
+ return evidenceRef(record['kind'], record['id'], { summary: record['summary'] });
68
+ }
69
+ export function envelopeUncertainty(scalar, attentionLevel, indecision, conflict) {
70
+ const normalizedScalar = clamp01(scalar);
71
+ const ambiguity = 1 - Math.abs(normalizedScalar - 0.5) * 2;
72
+ const attentionPenalty = {
73
+ full: 0,
74
+ limited: 0.15,
75
+ skimmed: 0.30,
76
+ unknown: 0.20,
77
+ };
78
+ return clamp01(0.10
79
+ + 0.30 * ambiguity
80
+ + attentionPenalty[attentionLevel]
81
+ + (indecision ? 0.25 : 0)
82
+ + (conflict ? 0.35 : 0));
83
+ }
84
+ export function fromScalarFeedback(options = {}) {
85
+ const input = options ?? {};
86
+ const scalar = clamp01(input.scalar);
87
+ const label = labelFromScalar(scalar);
88
+ const indecision = Boolean(input.indecision) || label === 'mixed';
89
+ const conflict = Boolean(input.conflict);
90
+ const evaluatorAttention = normalizeAttention(input.evaluator_attention ?? input.evaluatorAttention);
91
+ const priorityAxis = enumValue(input.priority_axis ?? input.priorityAxis, PRIORITY_AXES, 'task_success');
92
+ const evidence = input.evidence_ref ?? input.evidenceRef ?? evidenceRef('external', 'unknown');
93
+ return {
94
+ priority_axis: priorityAxis,
95
+ label,
96
+ scalar,
97
+ indecision,
98
+ conflict,
99
+ evaluator_attention: evaluatorAttention,
100
+ evidence_ref: normalizeEvidenceRef(evidence),
101
+ uncertainty: envelopeUncertainty(scalar, evaluatorAttention.level, indecision, conflict),
102
+ };
103
+ }
104
+ export function fromOutcomeScalar(outcome, options = {}) {
105
+ const record = asRecord(outcome);
106
+ if (!record)
107
+ return null;
108
+ const scalar = record['user_override'] ?? record['score'];
109
+ if (scalar === null || scalar === undefined)
110
+ return null;
111
+ return fromScalarFeedback({ ...options, scalar });
112
+ }
113
+ export function withConflict(envelope) {
114
+ const evaluatorAttention = normalizeAttention(envelope.evaluator_attention);
115
+ return {
116
+ ...envelope,
117
+ conflict: true,
118
+ evaluator_attention: evaluatorAttention,
119
+ uncertainty: envelopeUncertainty(envelope.scalar, evaluatorAttention.level, envelope.indecision, true),
120
+ };
121
+ }
122
+ export function withIndecision(envelope) {
123
+ const evaluatorAttention = normalizeAttention(envelope.evaluator_attention);
124
+ return {
125
+ ...envelope,
126
+ indecision: true,
127
+ evaluator_attention: evaluatorAttention,
128
+ uncertainty: envelopeUncertainty(envelope.scalar, evaluatorAttention.level, true, envelope.conflict),
129
+ };
130
+ }
131
+ export function aggregateFeedbackEnvelopes(envelopes) {
132
+ const list = Array.isArray(envelopes) ? envelopes.filter(Boolean) : [];
133
+ if (list.length === 0)
134
+ return { dominant_label: null, uncertainty: 1, sample_count: 0 };
135
+ const sampleCount = list.length;
136
+ const meanUncertainty = list.reduce((sum, envelope) => sum + clamp01(envelope.uncertainty), 0) / sampleCount;
137
+ const hasPositive = list.some((envelope) => envelope.label === 'positive');
138
+ const hasNegative = list.some((envelope) => envelope.label === 'negative');
139
+ const hasConflict = list.some((envelope) => envelope.conflict) || (hasPositive && hasNegative);
140
+ const hasIndecision = list.some((envelope) => envelope.indecision || envelope.label === 'mixed' || envelope.label === 'neutral');
141
+ const hasLowAttention = list.some((envelope) => normalizeAttention(envelope.evaluator_attention).level !== 'full');
142
+ const uncertainty = clamp01(meanUncertainty
143
+ + (hasConflict ? 0.25 : 0)
144
+ + (hasIndecision ? 0.10 : 0)
145
+ + (hasLowAttention ? 0.10 : 0));
146
+ let dominantLabel = null;
147
+ if (!hasConflict && !hasLowAttention && uncertainty < 0.5) {
148
+ if (hasPositive)
149
+ dominantLabel = 'positive';
150
+ else if (hasNegative)
151
+ dominantLabel = 'negative';
152
+ }
153
+ return { dominant_label: dominantLabel, uncertainty, sample_count: sampleCount };
154
+ }
155
+ function enumValue(value, allowed, fallback) {
156
+ return typeof value === 'string' && allowed.includes(value)
157
+ ? value
158
+ : fallback;
159
+ }
160
+ function nonNegativeInteger(value) {
161
+ const n = Number(value);
162
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : null;
163
+ }
164
+ function asRecord(value) {
165
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
166
+ ? value
167
+ : null;
168
+ }
@@ -0,0 +1 @@
1
+ export * from './envelope.js';
@@ -0,0 +1 @@
1
+ export * from './envelope.js';
@@ -1,4 +1,5 @@
1
- export type AssetCallAction = 'hub_search_hit' | 'hub_search_miss' | 'asset_reuse' | 'asset_reference' | 'asset_publish' | 'asset_publish_skip' | 'hub_review_submitted' | 'hub_review_rejected' | 'hub_review_failed';
1
+ export type AssetCallAction = 'hub_search_hit' | 'hub_search_miss' | 'asset_reuse' | 'asset_reference' | 'asset_publish' | 'asset_publish_skip' | 'asset_inject' | 'asset_inject_shadow' | 'hub_review_submitted' | 'hub_review_rejected' | 'hub_review_failed';
2
+ export type TokensSavedBasis = 'measured' | 'cost_index' | 'estimated_blast_radius' | 'estimated_default';
2
3
  export interface AssetCallEntry {
3
4
  run_id?: string | null;
4
5
  action: AssetCallAction;
@@ -10,6 +11,9 @@ export interface AssetCallEntry {
10
11
  mode?: 'direct' | 'reference';
11
12
  signals?: readonly string[];
12
13
  reason?: string;
14
+ tokens_saved?: number | null;
15
+ tokens_saved_basis?: TokensSavedBasis | null;
16
+ tokens_spent?: number | null;
13
17
  extra?: Record<string, unknown>;
14
18
  }
15
19
  export interface AssetCallRecord extends AssetCallEntry {
@@ -30,6 +34,32 @@ export interface CallLogSummary {
30
34
  by_action: Record<string, number>;
31
35
  entries: AssetCallRecord[];
32
36
  }
37
+ export interface AssetReuseAttribution {
38
+ asset_id: string;
39
+ source_node_id: string | null;
40
+ chain_id: string | null;
41
+ reuse: number;
42
+ reference: number;
43
+ tokens_saved: number;
44
+ }
45
+ export interface ReuseAttributionSummary {
46
+ total_reuse: number;
47
+ total_reference: number;
48
+ total_tokens_saved: number;
49
+ by_asset: AssetReuseAttribution[];
50
+ }
51
+ export interface ReuseSavingsMetric {
52
+ tokens_saved: number;
53
+ tokens_saved_basis: TokensSavedBasis;
54
+ }
55
+ /** Measured derivation cost carried by an asset (or by a Hub wrapper's payload). */
56
+ export declare function assetDerivationTokenCost(asset: unknown): number | undefined;
57
+ /**
58
+ * Attribute savings without inventing a measured value: asset telemetry wins, then this node's publish-cost
59
+ * index, then the savings-core blast/default estimator. Reference reuse applies the same fractional discount
60
+ * to measured and estimated costs.
61
+ */
62
+ export declare function reuseSavingsForAsset(asset: unknown, mode: 'direct' | 'reference', indexedTokenCost?: unknown): ReuseSavingsMetric;
33
63
  export declare class AssetCallLog {
34
64
  private readonly path;
35
65
  private readonly now;
@@ -40,4 +70,8 @@ export declare class AssetCallLog {
40
70
  read(opts?: ReadOpts): AssetCallRecord[];
41
71
  /** Totals + per-action counts (for CLI / observability). */
42
72
  summarize(opts?: ReadOpts): CallLogSummary;
73
+ /** Local-only attribution rollup over reuse/reference audit rows. */
74
+ reuseAttributionSummary(opts?: ReadOpts): ReuseAttributionSummary;
75
+ /** Later valid publish rows win; malformed/non-positive costs never erase a prior measurement. */
76
+ assetCostIndex(opts?: ReadOpts): Record<string, number>;
43
77
  }
@@ -5,6 +5,69 @@
5
5
  // or fails an evolution. The log path is injected so it's testable without a real home dir.
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
7
7
  import { dirname } from 'node:path';
8
+ import { REUSE_ESTIMATOR, reuseEstimate } from '../ops/savingsCore.js';
9
+ function objectRecord(value) {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
11
+ ? value
12
+ : undefined;
13
+ }
14
+ function nonEmptyString(value) {
15
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
16
+ }
17
+ function positiveNumber(value, coerceLegacy = false) {
18
+ const number = typeof value === 'number' ? value : coerceLegacy ? Number(value) : Number.NaN;
19
+ return Number.isFinite(number) && number > 0 ? number : undefined;
20
+ }
21
+ function nestedAssetRecord(asset) {
22
+ const direct = objectRecord(asset);
23
+ return { ...(direct ? { direct } : {}), ...(objectRecord(direct?.['payload']) ? { payload: objectRecord(direct?.['payload']) } : {}) };
24
+ }
25
+ /** Measured derivation cost carried by an asset (or by a Hub wrapper's payload). */
26
+ export function assetDerivationTokenCost(asset) {
27
+ const records = nestedAssetRecord(asset);
28
+ for (const record of [records.direct, records.payload]) {
29
+ const derivation = objectRecord(record?.['derivation_tokens']);
30
+ const total = positiveNumber(derivation?.['total_tokens']);
31
+ if (total !== undefined)
32
+ return total;
33
+ }
34
+ return undefined;
35
+ }
36
+ function assetBlastRadiusLines(asset) {
37
+ const records = nestedAssetRecord(asset);
38
+ for (const record of [records.direct, records.payload]) {
39
+ const blast = objectRecord(record?.['blast_radius']);
40
+ const lines = positiveNumber(blast?.['lines']);
41
+ if (lines !== undefined)
42
+ return lines;
43
+ }
44
+ return undefined;
45
+ }
46
+ /**
47
+ * Attribute savings without inventing a measured value: asset telemetry wins, then this node's publish-cost
48
+ * index, then the savings-core blast/default estimator. Reference reuse applies the same fractional discount
49
+ * to measured and estimated costs.
50
+ */
51
+ export function reuseSavingsForAsset(asset, mode, indexedTokenCost) {
52
+ const measured = assetDerivationTokenCost(asset);
53
+ const indexed = measured === undefined ? positiveNumber(indexedTokenCost) : undefined;
54
+ const cost = measured ?? indexed;
55
+ if (cost !== undefined) {
56
+ return {
57
+ tokens_saved: Math.round(mode === 'reference' ? cost * REUSE_ESTIMATOR.reference_saving_fraction : cost),
58
+ tokens_saved_basis: measured !== undefined ? 'measured' : 'cost_index',
59
+ };
60
+ }
61
+ const estimated = reuseEstimate(assetBlastRadiusLines(asset), mode);
62
+ return { tokens_saved: estimated.tokens_saved, tokens_saved_basis: estimated.basis };
63
+ }
64
+ function isAssetCallRecord(value) {
65
+ const record = objectRecord(value);
66
+ return record !== undefined
67
+ && typeof record['timestamp'] === 'string'
68
+ && typeof record['action'] === 'string'
69
+ && record['action'].length > 0;
70
+ }
8
71
  export class AssetCallLog {
9
72
  path;
10
73
  now;
@@ -33,7 +96,9 @@ export class AssetCallLog {
33
96
  if (!line)
34
97
  continue;
35
98
  try {
36
- entries.push(JSON.parse(line));
99
+ const parsed = JSON.parse(line);
100
+ if (isAssetCallRecord(parsed))
101
+ entries.push(parsed);
37
102
  }
38
103
  catch { /* skip corrupt */ }
39
104
  }
@@ -69,4 +134,62 @@ export class AssetCallLog {
69
134
  }
70
135
  return { total_entries: entries.length, unique_assets: assets.size, unique_runs: runs.size, by_action: byAction, entries };
71
136
  }
137
+ /** Local-only attribution rollup over reuse/reference audit rows. */
138
+ reuseAttributionSummary(opts = {}) {
139
+ const entries = this.read(opts).filter((entry) => entry.action === 'asset_reuse' || entry.action === 'asset_reference');
140
+ const byAsset = new Map();
141
+ let totalTokensSaved = 0;
142
+ let totalReuse = 0;
143
+ let totalReference = 0;
144
+ for (const entry of entries) {
145
+ const id = nonEmptyString(entry.asset_id) ?? '(unknown)';
146
+ let aggregate = byAsset.get(id);
147
+ if (!aggregate) {
148
+ aggregate = {
149
+ asset_id: id,
150
+ source_node_id: nonEmptyString(entry.source_node_id) ?? null,
151
+ chain_id: nonEmptyString(entry.chain_id) ?? null,
152
+ reuse: 0,
153
+ reference: 0,
154
+ tokens_saved: 0,
155
+ };
156
+ byAsset.set(id, aggregate);
157
+ }
158
+ if (entry.action === 'asset_reuse') {
159
+ aggregate.reuse += 1;
160
+ totalReuse += 1;
161
+ }
162
+ else {
163
+ aggregate.reference += 1;
164
+ totalReference += 1;
165
+ }
166
+ const tokensSaved = positiveNumber(entry['tokens_saved'], true);
167
+ if (tokensSaved !== undefined) {
168
+ aggregate.tokens_saved += tokensSaved;
169
+ totalTokensSaved += tokensSaved;
170
+ }
171
+ aggregate.source_node_id ??= nonEmptyString(entry.source_node_id) ?? null;
172
+ aggregate.chain_id ??= nonEmptyString(entry.chain_id) ?? null;
173
+ }
174
+ const byAssetRows = [...byAsset.values()].sort((left, right) => (right.reuse + right.reference) - (left.reuse + left.reference));
175
+ return {
176
+ total_reuse: totalReuse,
177
+ total_reference: totalReference,
178
+ total_tokens_saved: totalTokensSaved,
179
+ by_asset: byAssetRows,
180
+ };
181
+ }
182
+ /** Later valid publish rows win; malformed/non-positive costs never erase a prior measurement. */
183
+ assetCostIndex(opts = {}) {
184
+ const costs = new Map();
185
+ for (const entry of this.read(opts)) {
186
+ if (entry.action !== 'asset_publish')
187
+ continue;
188
+ const assetId = nonEmptyString(entry.asset_id);
189
+ const tokensSpent = positiveNumber(entry['tokens_spent'], true);
190
+ if (assetId && tokensSpent !== undefined)
191
+ costs.set(assetId, tokensSpent);
192
+ }
193
+ return Object.fromEntries(costs);
194
+ }
72
195
  }
@@ -1,4 +1,4 @@
1
- import type { HubCapability, HubBindings } from './capability.js';
1
+ import type { HubCapability, HubBindings, PublishReceipt } from './capability.js';
2
2
  import { type LeakCheckMode } from './sanitize.js';
3
3
  /** publish 回执非 accepted → 抛此错; SyncEngine 据 terminal 决定是否重试. */
4
4
  export declare class PublishRejectedError extends Error {
@@ -19,6 +19,13 @@ export interface HubBindingsOptions {
19
19
  mode?: LeakCheckMode;
20
20
  };
21
21
  }
22
+ /**
23
+ * Internal publish-handler result. `submittedAssetIds` identifies the exact sanitized
24
+ * content sent to the Hub, even when the Hub's accepted receipt omits asset ids.
25
+ */
26
+ export interface PublishDispatchResult extends PublishReceipt {
27
+ submittedAssetIds?: string[];
28
+ }
22
29
  /**
23
30
  * 把 HubCapability 接到 core 两 seam(M6-1):
24
31
  * - asProxyHandler: Dispatcher.handlers.proxy — 按 envelope.type 路由到 hub. 抛错→store.fail()重试;
@@ -37,14 +37,18 @@ export function makeHubBindings(cap, options = {}) {
37
37
  const sanEnabled = sanCfg.enabled ?? true;
38
38
  const sanEnv = sanCfg.env ?? (typeof process !== 'undefined' ? process.env : {});
39
39
  /** Single publish chokepoint: sanitize (on by default) → strict + leak found → refuse (terminal, not retryable) → cap.publish the redacted bundle. */
40
- const publishSanitized = (bundle) => {
40
+ const publishSanitized = async (bundle, publishOptions) => {
41
41
  if (!sanEnabled)
42
- return cap.publish(bundle);
42
+ return publishOptions ? cap.publish(bundle, publishOptions) : cap.publish(bundle);
43
43
  const r = sanitizeBundle(bundle, { env: sanEnv, ...(sanCfg.mode ? { mode: sanCfg.mode } : {}) });
44
44
  if (r.blocked) {
45
45
  throw new PublishRejectedError('leak_blocked', true, `sensitive data detected before publish, refused (not retryable): ${summarizeLeaks(r.leaks)}`);
46
46
  }
47
- return cap.publish(r.bundle);
47
+ const receipt = publishOptions
48
+ ? await cap.publish(r.bundle, publishOptions)
49
+ : await cap.publish(r.bundle);
50
+ const submittedAssetIds = contentAssetIds(r.bundle);
51
+ return submittedAssetIds ? { ...receipt, submittedAssetIds } : receipt;
48
52
  };
49
53
  return {
50
54
  asProxyHandler() {
@@ -54,9 +58,10 @@ export function makeHubBindings(cap, options = {}) {
54
58
  // payload 可为 bundle {assets:[...]} 或单资产; 统一成 bundle 数组发.
55
59
  const p = e.payload;
56
60
  const bundle = Array.isArray(p.assets) ? p.assets : [p];
57
- const r = await publishSanitized(bundle);
58
- if (r.status !== 'accepted')
59
- throw new PublishRejectedError(r.status, r.terminal ?? true, r.reason);
61
+ const r = await publishSanitized(bundle, { idempotencyKey: e.idempotencyKey });
62
+ if (r.status !== 'accepted') {
63
+ throw new PublishRejectedError(r.rejection?.code ?? r.status, r.terminal ?? true, r.reason, r.rejection?.retryAfterMs);
64
+ }
60
65
  return r;
61
66
  }
62
67
  case 'task_claim': {
@@ -98,6 +103,12 @@ export function makeHubBindings(cap, options = {}) {
98
103
  },
99
104
  };
100
105
  }
106
+ function contentAssetIds(bundle) {
107
+ const ids = bundle.map((asset) => asset.asset_id);
108
+ return ids.every((id) => typeof id === 'string' && /^sha256:[a-f0-9]{64}$/i.test(id))
109
+ ? ids
110
+ : undefined;
111
+ }
101
112
  function firstString(...values) {
102
113
  for (const value of values) {
103
114
  if (typeof value !== 'string')
@@ -16,6 +16,11 @@ export interface PublishReceipt {
16
16
  assetIds?: string[];
17
17
  /** 终态标记: true=不可重试(reject/quarantine/402), SyncEngine 不得 incrementRetry(money-safety risk). */
18
18
  terminal?: boolean;
19
+ /** Adapter-owned structured rejection metadata; callers must not infer policy from free-form `reason`. */
20
+ rejection?: {
21
+ code: 'invalid_request' | 'credit_shortage' | 'node_unauthorized' | 'not_found' | 'duplicate' | 'invalid_payload' | 'cooldown' | 'hub_rejected';
22
+ retryAfterMs?: number;
23
+ };
19
24
  /** M8-1: 公版经济侧只读回执(core 不解释经济语义, 仅透传供观测/UI). private hub 多为空. */
20
25
  economic?: {
21
26
  creditShortage?: boolean;
@@ -25,6 +30,10 @@ export interface PublishReceipt {
25
30
  gdiScore?: number;
26
31
  };
27
32
  }
33
+ export interface PublishOptions {
34
+ /** Stable, non-blank per logical publish so adapters can make transport retries idempotent. */
35
+ idempotencyKey?: string;
36
+ }
28
37
  /** 资产查询. 复用 assetstore SearchQuery, 不发明第二套查询 DSL. */
29
38
  export type HubQuery = SearchQuery;
30
39
  /** task 子系统事件(订阅流元素). 公版含经济上下文(bounty/credit), 私有仅 workflow. */
@@ -274,6 +283,7 @@ export interface HttpRequestLike {
274
283
  method: string;
275
284
  path: string;
276
285
  body?: string;
286
+ signal?: AbortSignal;
277
287
  }
278
288
  /**
279
289
  * Pluggable auth. Example implementations: public OAuth device token / Ed25519 keypair / enterprise SSO (OIDC/LDAP).
@@ -295,7 +305,7 @@ export interface HubCapability {
295
305
  * 异步发布. 公版 /a2a/publish 收 **bundle**[Gene,Capsule,(EvolutionEvent)](一个 cycle 的产物一起发, +GDI);
296
306
  * 传单资产=[asset]。core 已 normalizeForPut(算/校验 asset_id), hub 做经济/治理/质量 gate。
297
307
  */
298
- publish(bundle: AssetRecord[]): Promise<PublishReceipt>;
308
+ publish(bundle: AssetRecord[], options?: PublishOptions): Promise<PublishReceipt>;
299
309
  /** 拉取资产. hub 侧排序/scope(公版 GDI, 私有 RBAC); core 不重排. */
300
310
  fetch(query: HubQuery): Promise<AssetRecord[]>;
301
311
  /** 搜索资产. 与 fetch 同 shape, 差异在 hub 侧召回策略. */
@@ -1,4 +1,4 @@
1
- import type { HubCapability, PublishReceipt, AgentEvent, TaskEvent, AuthProvider, HubQuery, AssetRecord, TaskCompleteContext } from './capability.js';
1
+ import type { HubCapability, PublishReceipt, AgentEvent, TaskEvent, AuthProvider, HubQuery, AssetRecord, TaskCompleteContext, PublishOptions } 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'>;
@@ -28,7 +28,7 @@ export declare class FakeHubCapability implements HubCapability {
28
28
  constructor(opts?: FakeHubOptions);
29
29
  /** 测试注入: 排一条 inbound 事件供 poll 拉. */
30
30
  seedInbound(e: AgentEvent): void;
31
- publish(bundle: AssetRecord[]): Promise<PublishReceipt>;
31
+ publish(bundle: AssetRecord[], _options?: PublishOptions): Promise<PublishReceipt>;
32
32
  fetch(query: HubQuery): Promise<AssetRecord[]>;
33
33
  search(query: HubQuery): Promise<AssetRecord[]>;
34
34
  task: {
package/dist/hub/fake.js CHANGED
@@ -26,7 +26,7 @@ export class FakeHubCapability {
26
26
  }
27
27
  /** 测试注入: 排一条 inbound 事件供 poll 拉. */
28
28
  seedInbound(e) { this.inbox.push(e); }
29
- async publish(bundle) {
29
+ async publish(bundle, _options) {
30
30
  const assetIds = bundle.map((a) => computeAssetId(a));
31
31
  const gate = this.opts.publishGate?.(bundle[0] ?? {}) ?? { status: 'accepted' };
32
32
  if (gate.status === 'accepted')
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 };