@evomap/evolver-proxy 2.0.0-beta.19 → 2.0.0-beta.22

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 (43) hide show
  1. package/dist/bin/evolver-proxy.d.ts +12 -0
  2. package/dist/bin/evolver-proxy.js +257 -56
  3. package/dist/daemon/proxyDaemon.d.ts +12 -0
  4. package/dist/daemon/proxyDaemon.js +313 -18
  5. package/dist/daemon/systemdNotifier.d.ts +2 -0
  6. package/dist/daemon/systemdNotifier.js +11 -1
  7. package/dist/llm/upstream.js +54 -7
  8. package/dist/private/accountAssetCompatibility.d.ts +1 -0
  9. package/dist/private/accountAssetCompatibility.js +3 -3
  10. package/dist/private/adapterLoader.js +4 -3
  11. package/dist/router/messagesRoute.js +9 -3
  12. package/dist/router/providerRoutes.js +7 -3
  13. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  14. package/dist/selfUpdate/bootstrap.js +3524 -0
  15. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  16. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  17. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  18. package/dist/selfUpdate/builtinKey.js +16 -0
  19. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  20. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  21. package/dist/selfUpdate/executor.d.ts +18 -7
  22. package/dist/selfUpdate/executor.js +159 -59
  23. package/dist/selfUpdate/failureCodes.d.ts +4 -0
  24. package/dist/selfUpdate/failureCodes.js +7 -0
  25. package/dist/selfUpdate/index.d.ts +2 -1
  26. package/dist/selfUpdate/index.js +2 -1
  27. package/dist/selfUpdate/migration.d.ts +158 -0
  28. package/dist/selfUpdate/migration.js +2672 -0
  29. package/dist/selfUpdate/policy.d.ts +19 -2
  30. package/dist/selfUpdate/policy.js +76 -2
  31. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  32. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  33. package/dist/selfUpdate/releaseBinary.d.ts +3 -0
  34. package/dist/selfUpdate/releaseBinary.js +50 -4
  35. package/dist/selfUpdate/transaction.d.ts +8 -0
  36. package/dist/selfUpdate/transaction.js +166 -18
  37. package/dist/selfUpdate/unixController.d.ts +8 -0
  38. package/dist/selfUpdate/unixController.js +366 -38
  39. package/dist/selfUpdate/windowsController.d.ts +14 -2
  40. package/dist/selfUpdate/windowsController.js +484 -103
  41. package/dist/selfUpdate/windowsUpdater.d.ts +25 -0
  42. package/dist/selfUpdate/windowsUpdater.js +174 -7
  43. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { dirname, join } from 'node:path';
3
- import { mailbox, hub as hubNs, shadow as shadow_, assetstore, wire, util } from '@evomap/evolver-core';
3
+ import { mailbox, hub as hubNs, shadow as shadow_, assetstore, wire, util, verify } from '@evomap/evolver-core';
4
4
  import { AuthError, HubClientError, HubUnreachableError } from '@evomap/evolver-adapter-public';
5
5
  import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
6
6
  import { LifecycleManager } from '../lifecycle/manager.js';
@@ -10,6 +10,7 @@ import { backfillProxyTraceUploads } from '../llm/traceBackfill.js';
10
10
  import { hubAuthFailureHint } from './selectHub.js';
11
11
  import { CollaborationFacade } from './collaborationFacade.js';
12
12
  import { PublishRecallVerifier, resolvePublishRecallConfig, } from './publishRecallVerifier.js';
13
+ const DEFAULT_PUBLISH_EXECUTION_VERIFY_TIMEOUT_MS = 30_000;
13
14
  export const DEFAULT_IPC_PORT = 19820;
14
15
  // V1 local-proxy compatibility contract; independent of the V2 mailbox envelope schema.
15
16
  const PROXY_PROTOCOL_VERSION = '0.1.0';
@@ -35,6 +36,27 @@ const SYNC_ASSET_SUBMIT_TYPE_RANK = {
35
36
  const SYNC_ASSET_SUBMIT_SCOPE_STATE_KEY = 'sync_asset_submit:idempotency_scope:v1';
36
37
  const SYNC_ASSET_SUBMIT_DIRECT_RETRY_GRACE_MS = 30_000;
37
38
  const DEFAULT_SYNC_ASSET_SUBMIT_RESPONSE_TIMEOUT_MS = 15_000;
39
+ function withSynchronousPublishStatus(value, publishStatus) {
40
+ const body = isRecordValue(value) ? { ...value } : { receipt: value };
41
+ return { ...body, publish_status: publishStatus, queued: false };
42
+ }
43
+ function legacySynchronousResultStatus(result) {
44
+ if (result.ok)
45
+ return 'accepted';
46
+ // 202 表示兼容层已持久化但尚未完成;校验或终态/传输失败即使在 mailbox
47
+ // 中保留可重试意图,聚合仍归类为 failed。
48
+ return result.statusCode === 202 || result.error === 'publish_pending' ? 'pending' : 'failed';
49
+ }
50
+ function summarizeLegacySynchronousResults(results) {
51
+ const statuses = new Set(results.map(legacySynchronousResultStatus));
52
+ // publish_status 表示聚合终态,而 queued 独立表示是否仍有 durable mailbox 工作。
53
+ // 因此 pending+failed 必须是 failed/true:不能掩盖失败,也不能谎报没有排队。
54
+ if (statuses.has('failed') || statuses.size === 0)
55
+ return { publishStatus: 'failed', queued: statuses.has('pending') };
56
+ if (statuses.has('pending'))
57
+ return { publishStatus: 'pending', queued: true };
58
+ return { publishStatus: 'accepted', queued: false };
59
+ }
38
60
  class OutboundHubModeMismatchError extends Error {
39
61
  retryable = true;
40
62
  retryAfterMs = 1_000;
@@ -64,6 +86,8 @@ export class ProxyDaemon {
64
86
  collaborationFacade;
65
87
  publishRecallVerifier;
66
88
  proxyHandler;
89
+ hub;
90
+ recipeComposeStarted = new Set();
67
91
  ipc;
68
92
  now;
69
93
  random;
@@ -98,6 +122,8 @@ export class ProxyDaemon {
98
122
  scheduledForceUpdateKey;
99
123
  traceBackfillDraining = false;
100
124
  loopWakeHandler;
125
+ /** 守护进程停止时取消宿主验证。 */
126
+ publishAbortController = new AbortController();
101
127
  constructor(deps) {
102
128
  this.deps = deps;
103
129
  this.now = deps.now ?? (() => Date.now());
@@ -124,6 +150,7 @@ export class ProxyDaemon {
124
150
  this.assetStore = deps.assetStore ?? (assetStoreDir ? new assetstore.LocalJsonlProvider(assetStoreDir) : undefined);
125
151
  this.atp = deps.atp;
126
152
  const hubToUse = shadow ? shadow_.shadowHubCapability(deps.hub, deps.shadowSink, 'shadow') : deps.hub;
153
+ this.hub = hubToUse;
127
154
  const hubBindings = hubNs.makeHubBindings(hubToUse, deps.publishSanitizeEnv
128
155
  ? { sanitize: { env: deps.publishSanitizeEnv } }
129
156
  : {});
@@ -195,6 +222,8 @@ export class ProxyDaemon {
195
222
  }
196
223
  catch { /* best-effort */ }
197
224
  }
225
+ // Recipe is the preferred public artifact. Failure here must not retry the already-accepted asset publish.
226
+ this.composeRecipeAfterAcceptedSubmit(envelope);
198
227
  },
199
228
  onOutboundTerminal: (envelope, error) => {
200
229
  this.collaborationFacade.handleOutboundTerminal(envelope, error);
@@ -260,6 +289,8 @@ export class ProxyDaemon {
260
289
  async start() {
261
290
  if (this.started)
262
291
  throw new Error('ProxyDaemon 已启动');
292
+ if (this.publishAbortController.signal.aborted)
293
+ this.publishAbortController = new AbortController();
263
294
  try {
264
295
  this.daemon.start();
265
296
  this.ipc = new mailbox.MailboxIpcServer({
@@ -507,6 +538,9 @@ export class ProxyDaemon {
507
538
  async stop() {
508
539
  this.started = false;
509
540
  this.lifecycleArmed = false;
541
+ if (!this.publishAbortController.signal.aborted) {
542
+ this.publishAbortController.abort(new Error('proxy_daemon_stopped'));
543
+ }
510
544
  this.nextTickDueAt = undefined;
511
545
  if (this.forceUpdateTimer) {
512
546
  clearTimeout(this.forceUpdateTimer);
@@ -631,6 +665,7 @@ export class ProxyDaemon {
631
665
  return { ok: false, reason: 'self_update_not_configured' };
632
666
  const currentVersion = this.deps.selfUpdate.currentVersion ?? this.deps.evolverVersion ?? '0.0.0';
633
667
  const originalTelemetry = this.deps.selfUpdate.onTelemetry;
668
+ const originalCleanupWarning = this.deps.selfUpdate.onCleanupWarning;
634
669
  const selfUpdateDeps = {
635
670
  ...this.deps.selfUpdate,
636
671
  currentVersion,
@@ -641,6 +676,15 @@ export class ProxyDaemon {
641
676
  });
642
677
  originalTelemetry?.(result);
643
678
  },
679
+ onCleanupWarning: (warning, result) => {
680
+ try {
681
+ this.store.setState('self_update:last_cleanup_warning', safeDaemonMessage(`self_update_cleanup_warning:${warning}`, MAX_PROXY_TICK_ERROR_LENGTH));
682
+ }
683
+ catch {
684
+ // The returned result still carries cleanupWarning when operator state is unavailable.
685
+ }
686
+ originalCleanupWarning?.(warning, result);
687
+ },
644
688
  };
645
689
  return executeForceUpdate(directive, selfUpdateDeps);
646
690
  }
@@ -799,6 +843,65 @@ export class ProxyDaemon {
799
843
  ctx.json(200, { results, assets: results, query: body });
800
844
  return true;
801
845
  }
846
+ if (ctx.route === 'POST /recipe/search') {
847
+ const body = (await ctx.readJson());
848
+ if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
849
+ ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
850
+ return true;
851
+ }
852
+ const recipes = this.hub.recipes;
853
+ if (!recipes) {
854
+ ctx.json(501, { error: 'recipe_unsupported' });
855
+ return true;
856
+ }
857
+ const limit = boundedRequestLimit(body.limit, 10, 50);
858
+ if (limit === undefined) {
859
+ ctx.json(400, { error: 'invalid_limit' });
860
+ return true;
861
+ }
862
+ const q = [body.q, body.query, body.text].find((value) => typeof value === 'string' && value.trim().length > 0);
863
+ const cursor = typeof body.cursor === 'string' && body.cursor.trim() ? body.cursor.trim() : undefined;
864
+ const sort = typeof body.sort === 'string' && body.sort.trim() ? body.sort.trim() : undefined;
865
+ const request = {
866
+ ...(q ? { q } : {}),
867
+ limit,
868
+ ...(cursor ? { cursor } : {}),
869
+ ...(sort ? { sort } : {}),
870
+ };
871
+ const receipt = q ? await recipes.search(request) : await recipes.list(request);
872
+ ctx.json(200, {
873
+ recipes: receipt.recipes,
874
+ ...(receipt.nextCursor ? { nextCursor: receipt.nextCursor } : {}),
875
+ ...(receipt.hasMore !== undefined ? { hasMore: receipt.hasMore } : {}),
876
+ query: body,
877
+ });
878
+ return true;
879
+ }
880
+ if (ctx.route === 'POST /recipe/express') {
881
+ const body = asRecord(await ctx.readJson());
882
+ if (hubModeMismatch(body['expected_hub_mode'], this.deps.hubMode)) {
883
+ ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
884
+ return true;
885
+ }
886
+ const recipes = this.hub.recipes;
887
+ if (!recipes) {
888
+ ctx.json(501, { error: 'recipe_unsupported' });
889
+ return true;
890
+ }
891
+ const recipeId = typeof body['recipe_id'] === 'string'
892
+ ? body['recipe_id']
893
+ : typeof body['recipeId'] === 'string'
894
+ ? body['recipeId']
895
+ : '';
896
+ if (!recipeId.trim()) {
897
+ ctx.json(400, { error: 'recipe_id_required' });
898
+ return true;
899
+ }
900
+ const inputPayload = asRecord(body['input_payload']) ?? asRecord(body['inputPayload']) ?? {};
901
+ const receipt = await recipes.express(recipeId.trim(), { inputPayload });
902
+ ctx.json(200, receipt);
903
+ return true;
904
+ }
802
905
  if (ctx.route === 'POST /asset/fetch') {
803
906
  const body = (await ctx.readJson());
804
907
  if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
@@ -871,11 +974,12 @@ export class ProxyDaemon {
871
974
  ctx.json(400, { error: 'mode=sync requires a full asset bundle' });
872
975
  return true;
873
976
  }
874
- await this.publishAssetSubmitSynchronously(ctx, outboundBundle);
977
+ await this.publishAssetSubmitSynchronously(ctx, outboundBundle, hubNs.recipeComposeRequested(body));
875
978
  return true;
876
979
  }
877
980
  const payload = {
878
981
  assets: outboundBundle,
982
+ compose_recipe: hubNs.recipeComposeRequested(body),
879
983
  [OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
880
984
  };
881
985
  const requestId = typeof body['request_id'] === 'string' && ASYNC_ASSET_SUBMIT_REQUEST_ID.test(body['request_id'])
@@ -954,23 +1058,75 @@ export class ProxyDaemon {
954
1058
  }
955
1059
  if (ctx.route === 'POST /conversation/distill') {
956
1060
  const body = (await ctx.readJson());
1061
+ const publishRequested = body['publish'] === true;
1062
+ const abortPublish = () => {
1063
+ if (ctx.signal?.aborted)
1064
+ return true;
1065
+ if (!this.publishAbortController.signal.aborted)
1066
+ return false;
1067
+ // 守护进程停止时仍需结束开放的响应,否则 IPC server.close() 会等待该连接而无法完成停机。
1068
+ if (!ctx.res.destroyed && !ctx.res.writableEnded) {
1069
+ // 'blocked' (not 'failed'): the request was never queued, it was intercepted by
1070
+ // shutdown; the 503 + error field already explains why.
1071
+ ctx.json(503, { error: 'proxy_shutting_down', publish_status: 'blocked', queued: false });
1072
+ }
1073
+ return true;
1074
+ };
957
1075
  if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
958
1076
  ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
959
1077
  return true;
960
1078
  }
961
- const distill = await hubNs.distillConversation(body, { persist: body.persist === true, store: this.assetStore });
1079
+ // 请求已取消时禁止继续生成或持久化发布草稿,避免把取消前的陈旧状态留下。
1080
+ if (publishRequested && abortPublish())
1081
+ return true;
1082
+ const verifiedExecution = publishRequested
1083
+ ? await resolveVerifiedExecutionAfterPreflight(this.deps.publishExecutionVerifier, body, this.deps.publishExecutionVerifierTimeoutMs, [this.publishAbortController.signal, ...(ctx.signal ? [ctx.signal] : [])])
1084
+ : undefined;
1085
+ if (publishRequested && abortPublish())
1086
+ return true;
1087
+ const distill = await hubNs.distillConversation(body, {
1088
+ // 调用方明确要求持久化时,即使发布被证据或质量闸门拦截,也保留可审查草稿;永不因此进入队列。
1089
+ persist: body.persist === true,
1090
+ store: this.assetStore,
1091
+ ...(verifiedExecution ? { verifiedExecution } : {}),
1092
+ });
1093
+ if (publishRequested && abortPublish())
1094
+ return true;
962
1095
  if (!distill.ok) {
963
- ctx.json(200, { ...distill, queued: false, submission: null });
1096
+ ctx.json(200, {
1097
+ ...distill,
1098
+ queued: false,
1099
+ submission: null,
1100
+ publish_status: publishRequested ? 'blocked' : 'not_requested',
1101
+ });
964
1102
  return true;
965
1103
  }
966
1104
  let submission = null;
967
- if (body['publish'] === true) {
1105
+ if (publishRequested) {
1106
+ if (abortPublish())
1107
+ return true;
1108
+ // 未达到可复用质量阈值的蒸馏结果仍可作为有用草稿,
1109
+ // 但不得进入出站发布队列。安全与内容完整性闸门保持严格,质量负责晋级。
1110
+ if (distill.publishable !== true) {
1111
+ const reason = distill.quality.ok && verifiedExecution === undefined ? 'execution_evidence' : 'quality_gate';
1112
+ ctx.json(200, {
1113
+ ...distill,
1114
+ queued: false,
1115
+ submission: null,
1116
+ publish_blocked: reason,
1117
+ publish_status: 'blocked',
1118
+ });
1119
+ return true;
1120
+ }
968
1121
  const env = mailbox.createEnvelope({
969
1122
  type: 'asset_submit',
970
1123
  payload: {
971
1124
  source: 'conversation_distillation',
972
1125
  distill_id: distill.distill_id,
973
1126
  assets: [distill.gene, distill.capsule],
1127
+ compose_recipe: body['publish_recipe'] !== false,
1128
+ title: typeof body.title === 'string' ? body.title : undefined,
1129
+ description: typeof body.summary === 'string' ? body.summary : undefined,
974
1130
  [OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
975
1131
  },
976
1132
  runtimeNamespace: this.deps.runtimeNamespace ?? 'default',
@@ -981,7 +1137,12 @@ export class ProxyDaemon {
981
1137
  this.notifyNewOutbound();
982
1138
  submission = { id: env.id, message_id: env.id, receiptId: r.receiptId, status: 'pending', stored: r.stored };
983
1139
  }
984
- ctx.json(200, { ...distill, queued: submission !== null, submission });
1140
+ ctx.json(200, {
1141
+ ...distill,
1142
+ queued: submission !== null,
1143
+ submission,
1144
+ publish_status: publishRequested ? 'queued' : 'not_requested',
1145
+ });
985
1146
  return true;
986
1147
  }
987
1148
  if (ctx.route === 'POST /agent/search') {
@@ -1128,25 +1289,39 @@ export class ProxyDaemon {
1128
1289
  staleUntil: now + this.assetSearchCacheTtlMs + this.assetSearchStaleGraceMs,
1129
1290
  });
1130
1291
  }
1131
- async publishAssetSubmitSynchronously(ctx, items) {
1292
+ async publishAssetSubmitSynchronously(ctx, items, composeRecipe = true) {
1293
+ const abortSynchronousPublish = () => {
1294
+ if (ctx.signal?.aborted)
1295
+ return true;
1296
+ if (!this.publishAbortController.signal.aborted)
1297
+ return false;
1298
+ if (!ctx.res.destroyed && !ctx.res.writableEnded) {
1299
+ ctx.json(503, { error: 'proxy_shutting_down', publish_status: 'failed', queued: false });
1300
+ }
1301
+ return true;
1302
+ };
1132
1303
  const classified = classifySynchronousAssetSubmit(items);
1133
1304
  if (!classified.ok) {
1134
1305
  ctx.json(422, { error: classified.error, code: 'invalid_asset_submit' });
1135
1306
  return;
1136
1307
  }
1137
1308
  if (classified.kind === 'wire') {
1138
- const envelope = this.createSynchronousAssetSubmitEnvelope(classified.bundle, undefined, ctx.now);
1309
+ if (abortSynchronousPublish())
1310
+ return;
1311
+ const envelope = this.createSynchronousAssetSubmitEnvelope(classified.bundle, undefined, ctx.now, composeRecipe);
1139
1312
  this.writeSynchronousAssetSubmitOutcome(ctx, await this.publishSynchronousBundle(envelope));
1140
1313
  return;
1141
1314
  }
1142
1315
  const results = [];
1143
1316
  for (const item of classified.items) {
1144
- const converted = await convertLegacyLooseAsset(item);
1317
+ const converted = await convertLegacyLooseAsset(item, this.deps.publishExecutionVerifier, this.deps.publishExecutionVerifierTimeoutMs, ctx.signal, this.publishAbortController.signal);
1145
1318
  if (!converted.ok) {
1146
1319
  results.push({ ok: false, error: converted.error, statusCode: 422 });
1147
1320
  continue;
1148
1321
  }
1149
- const envelope = this.createSynchronousAssetSubmitEnvelope(converted.bundle, 'v1_loose_asset_compat', ctx.now);
1322
+ if (abortSynchronousPublish())
1323
+ return;
1324
+ const envelope = this.createSynchronousAssetSubmitEnvelope(converted.bundle, 'v1_loose_asset_compat', ctx.now, composeRecipe);
1150
1325
  const outcome = await this.publishSynchronousBundle(envelope);
1151
1326
  if (outcome.kind === 'accepted') {
1152
1327
  const receipt = outcome.receipt;
@@ -1175,13 +1350,16 @@ export class ProxyDaemon {
1175
1350
  });
1176
1351
  }
1177
1352
  }
1353
+ const summary = summarizeLegacySynchronousResults(results);
1178
1354
  ctx.json(200, {
1179
1355
  published: results.filter((result) => result.ok).length,
1180
1356
  total: results.length,
1181
1357
  results,
1358
+ publish_status: summary.publishStatus,
1359
+ queued: summary.queued,
1182
1360
  });
1183
1361
  }
1184
- createSynchronousAssetSubmitEnvelope(bundle, source, now) {
1362
+ createSynchronousAssetSubmitEnvelope(bundle, source, now, composeRecipe = true) {
1185
1363
  const canonicalBundle = [...bundle].sort(compareSynchronousAssetSubmitAssets);
1186
1364
  const runtimeNamespace = this.deps.runtimeNamespace ?? 'default';
1187
1365
  const idempotencyKey = synchronousAssetSubmitKey(this.synchronousAssetSubmitScope, runtimeNamespace, this.currentHubMode(), canonicalBundle);
@@ -1191,6 +1369,7 @@ export class ProxyDaemon {
1191
1369
  payload: {
1192
1370
  ...(source ? { source } : {}),
1193
1371
  assets: canonicalBundle,
1372
+ compose_recipe: composeRecipe,
1194
1373
  [OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
1195
1374
  },
1196
1375
  idempotencyKey,
@@ -1198,6 +1377,15 @@ export class ProxyDaemon {
1198
1377
  now,
1199
1378
  });
1200
1379
  }
1380
+ composeRecipeAfterAcceptedSubmit(envelope) {
1381
+ if (envelope.type !== 'asset_submit')
1382
+ return;
1383
+ const key = envelope.idempotencyKey || envelope.id;
1384
+ if (this.recipeComposeStarted.has(key))
1385
+ return;
1386
+ this.recipeComposeStarted.add(key);
1387
+ void hubNs.composeRecipeAfterAssetPublish(this.hub, asRecord(envelope.payload)).catch(() => { });
1388
+ }
1201
1389
  currentHubMode() {
1202
1390
  return this.deps.hubMode ?? 'public';
1203
1391
  }
@@ -1267,6 +1455,7 @@ export class ProxyDaemon {
1267
1455
  this.publishRecallVerifier.observeAcceptedPublish(envelope, receipt);
1268
1456
  }
1269
1457
  catch { /* best-effort */ }
1458
+ this.composeRecipeAfterAcceptedSubmit(envelope);
1270
1459
  }
1271
1460
  if (this.store.getById(envelope.id)?.status !== 'in_flight')
1272
1461
  this.store.complete(envelope.id, this.now());
@@ -1348,6 +1537,7 @@ export class ProxyDaemon {
1348
1537
  this.publishRecallVerifier.observeAcceptedPublish(envelope, receipt);
1349
1538
  }
1350
1539
  catch { /* best-effort */ }
1540
+ this.composeRecipeAfterAcceptedSubmit(envelope);
1351
1541
  }
1352
1542
  return receipt;
1353
1543
  }
@@ -1420,13 +1610,19 @@ export class ProxyDaemon {
1420
1610
  }
1421
1611
  writeSynchronousAssetSubmitOutcome(ctx, outcome) {
1422
1612
  if (outcome.kind === 'accepted') {
1423
- ctx.json(200, outcome.receipt);
1613
+ ctx.json(200, withSynchronousPublishStatus(outcome.receipt, 'accepted'));
1424
1614
  }
1425
1615
  else if (outcome.kind === 'failed') {
1426
- ctx.json(outcome.statusCode, outcome.body);
1616
+ ctx.json(outcome.statusCode, withSynchronousPublishStatus(outcome.body, 'failed'));
1427
1617
  }
1428
1618
  else {
1429
- ctx.json(202, { status: 'pending', message_id: outcome.messageId, durable: true });
1619
+ ctx.json(202, {
1620
+ status: 'pending',
1621
+ message_id: outcome.messageId,
1622
+ durable: true,
1623
+ publish_status: 'pending',
1624
+ queued: true,
1625
+ });
1430
1626
  }
1431
1627
  }
1432
1628
  async handleAtpRoute(ctx) {
@@ -1670,14 +1866,113 @@ function isClearlyLegacyLooseAsset(value) {
1670
1866
  return false;
1671
1867
  return ['content', 'summary', 'strategy'].some((key) => Object.prototype.hasOwnProperty.call(value, key));
1672
1868
  }
1673
- async function convertLegacyLooseAsset(value) {
1869
+ async function resolveVerifiedExecution(verifier, input, expectedValidation, timeoutMs = DEFAULT_PUBLISH_EXECUTION_VERIFY_TIMEOUT_MS, parentSignals = []) {
1870
+ if (!verifier)
1871
+ return undefined;
1872
+ if (parentSignals.some((signal) => signal.aborted))
1873
+ return undefined;
1874
+ const boundedTimeout = Number.isSafeInteger(timeoutMs) && timeoutMs > 0
1875
+ ? timeoutMs
1876
+ : DEFAULT_PUBLISH_EXECUTION_VERIFY_TIMEOUT_MS;
1877
+ const controller = new AbortController();
1878
+ let timer;
1879
+ let resolveAborted;
1880
+ const aborted = new Promise((resolve) => {
1881
+ resolveAborted = () => resolve(null);
1882
+ });
1883
+ const onParentAbort = () => {
1884
+ if (!controller.signal.aborted)
1885
+ controller.abort(new Error('publish_verification_aborted'));
1886
+ resolveAborted();
1887
+ };
1888
+ for (const signal of parentSignals)
1889
+ signal.addEventListener('abort', onParentAbort, { once: true });
1890
+ try {
1891
+ const timeout = new Promise((resolve) => {
1892
+ timer = setTimeout(() => {
1893
+ if (!controller.signal.aborted)
1894
+ controller.abort(new Error('publish_verification_timeout'));
1895
+ resolve(null);
1896
+ }, boundedTimeout);
1897
+ timer.unref?.();
1898
+ });
1899
+ const candidate = await Promise.race([verifier(input, controller.signal), timeout, aborted]);
1900
+ if (!candidate || !Array.isArray(candidate.trace) || candidate.trace.length === 0)
1901
+ return undefined;
1902
+ if (controller.signal.aborted)
1903
+ return undefined;
1904
+ if (candidate.trace.some((row) => (!row
1905
+ || typeof row.command !== 'string'
1906
+ || row.command.trim().length === 0
1907
+ || !Number.isInteger(row.exit)
1908
+ || row.exit !== 0)))
1909
+ return undefined;
1910
+ if (candidate.trace.length !== expectedValidation.length)
1911
+ return undefined;
1912
+ if (candidate.trace.some((row, index) => row.command.trim() !== expectedValidation[index]))
1913
+ return undefined;
1914
+ return { ...candidate, validation: expectedValidation };
1915
+ }
1916
+ catch {
1917
+ return undefined;
1918
+ }
1919
+ finally {
1920
+ if (timer !== undefined)
1921
+ clearTimeout(timer);
1922
+ for (const signal of parentSignals)
1923
+ signal.removeEventListener('abort', onParentAbort);
1924
+ if (!controller.signal.aborted)
1925
+ controller.abort(new Error('publish_verification_complete'));
1926
+ }
1927
+ }
1928
+ function declaredValidationCommands(input) {
1929
+ const raw = input.validation ?? input.verification ?? (input.execution && typeof input.execution === 'object' && !Array.isArray(input.execution)
1930
+ ? input.execution['validation']
1931
+ : undefined);
1932
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > 8)
1933
+ return undefined;
1934
+ const commands = raw.map((command) => typeof command === 'string' ? command.trim() : '');
1935
+ if (commands.some((command) => command.length === 0 || command.length > 180 || !verify.isValidationCommandAllowed(command))) {
1936
+ return undefined;
1937
+ }
1938
+ return commands;
1939
+ }
1940
+ async function resolveVerifiedExecutionAfterPreflight(verifier, input, timeoutMs, parentSignals = []) {
1941
+ if (!verifier)
1942
+ return undefined;
1943
+ if (parentSignals.some((signal) => signal.aborted))
1944
+ return undefined;
1945
+ const preflight = await hubNs.distillConversation(input, { persist: false });
1946
+ if (parentSignals.some((signal) => signal.aborted) || !preflight.ok || !preflight.quality.ok)
1947
+ return undefined;
1948
+ const validation = declaredValidationCommands(input);
1949
+ if (!validation)
1950
+ return undefined;
1951
+ // 只把通过质量与命令策略预检的最小验证输入交给宿主,绝不转发调用方的 execution/status/trace。
1952
+ return resolveVerifiedExecution(verifier, { validation }, validation, timeoutMs, parentSignals);
1953
+ }
1954
+ async function convertLegacyLooseAsset(value, verifyExecution, verifyExecutionTimeoutMs, ...parentSignals) {
1674
1955
  const normalized = legacyLooseDistillInput(value);
1675
1956
  if (!normalized.ok)
1676
1957
  return normalized;
1677
1958
  try {
1678
- const distilled = await hubNs.distillConversation(normalized.input, { persist: false });
1679
- if (!distilled.ok) {
1959
+ const verifiedExecution = await resolveVerifiedExecutionAfterPreflight(verifyExecution, normalized.input, verifyExecutionTimeoutMs, parentSignals.filter((signal) => signal !== undefined));
1960
+ const distilled = await hubNs.distillConversation(normalized.input, {
1961
+ persist: false,
1962
+ ...(verifiedExecution ? { verifiedExecution } : {}),
1963
+ });
1964
+ if (!distilled.ok)
1680
1965
  return { ok: false, error: `legacy_distill_${safeIdentifier(distilled.reason)}` };
1966
+ // Mirror the /conversation/distill route: a draft that passed the quality gate but was
1967
+ // blocked only by the missing host execution evidence must not be reported as a
1968
+ // quality-gate failure.
1969
+ if (distilled.publishable !== true) {
1970
+ return {
1971
+ ok: false,
1972
+ error: distilled.quality.ok && verifiedExecution === undefined
1973
+ ? 'legacy_distill_execution_evidence'
1974
+ : 'legacy_distill_quality_gate',
1975
+ };
1681
1976
  }
1682
1977
  const gene = {
1683
1978
  ...distilled.gene,
@@ -1809,7 +2104,7 @@ function parseLegacyCategory(value) {
1809
2104
  return { ok: false, error: 'legacy category is invalid' };
1810
2105
  }
1811
2106
  function deterministicDistilledAsset(asset) {
1812
- const draft = { ...asset, asset_id: '' };
2107
+ const draft = wire.stripGeneHints({ ...asset, asset_id: '' });
1813
2108
  // `_source` is local distiller provenance and is not part of the current GEP Gene schema.
1814
2109
  // It also contains a wall-clock timestamp, so it must not influence compatibility asset ids.
1815
2110
  delete draft['_source'];
@@ -19,6 +19,7 @@ interface SystemdNotifierOptions {
19
19
  execFile?: SystemdNotifyExec;
20
20
  readyRetryDelaysMs?: readonly number[];
21
21
  sleep?: (delayMs: number) => Promise<void>;
22
+ notifyCommand?: string;
22
23
  }
23
24
  export declare function systemdWatchdogIntervalMs(env?: NodeJS.ProcessEnv): number;
24
25
  export declare class SystemdNotifier {
@@ -29,6 +30,7 @@ export declare class SystemdNotifier {
29
30
  private readonly execFile;
30
31
  private readonly readyRetryDelaysMs;
31
32
  private readonly sleep;
33
+ private readonly notifyCommand;
32
34
  private timer;
33
35
  private readySent;
34
36
  private readyInFlight;
@@ -1,9 +1,15 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
2
3
  const SYSTEMD_NOTIFY_TIMEOUT_MS = 5_000;
3
4
  const MIN_WATCHDOG_INTERVAL_MS = 1_000;
4
5
  const DEFAULT_READY_RETRY_DELAYS_MS = [250, 750];
5
6
  const MAX_READY_RETRIES = 4;
6
7
  const MAX_READY_RETRY_DELAY_MS = 5_000;
8
+ const SYSTEMD_NOTIFY_CANDIDATES = [
9
+ '/usr/bin/systemd-notify',
10
+ '/bin/systemd-notify',
11
+ '/run/current-system/sw/bin/systemd-notify',
12
+ ];
7
13
  const defaultSystemdNotifyExec = (command, args, options, callback) => {
8
14
  execFile(command, [...args], options, (error) => { callback(error); });
9
15
  };
@@ -21,6 +27,7 @@ export class SystemdNotifier {
21
27
  execFile;
22
28
  readyRetryDelaysMs;
23
29
  sleep;
30
+ notifyCommand;
24
31
  timer;
25
32
  readySent = false;
26
33
  readyInFlight;
@@ -32,6 +39,9 @@ export class SystemdNotifier {
32
39
  this.execFile = options.execFile ?? defaultSystemdNotifyExec;
33
40
  this.readyRetryDelaysMs = normalizeReadyRetryDelays(options.readyRetryDelaysMs ?? DEFAULT_READY_RETRY_DELAYS_MS);
34
41
  this.sleep = options.sleep ?? sleepMs;
42
+ this.notifyCommand = options.notifyCommand
43
+ ?? SYSTEMD_NOTIFY_CANDIDATES.find((candidate) => existsSync(candidate))
44
+ ?? SYSTEMD_NOTIFY_CANDIDATES[0];
35
45
  }
36
46
  async ready() {
37
47
  if (!this.active())
@@ -123,7 +133,7 @@ export class SystemdNotifier {
123
133
  notify(state) {
124
134
  return new Promise((resolve) => {
125
135
  try {
126
- this.execFile('systemd-notify', [state], {
136
+ this.execFile(this.notifyCommand, [state], {
127
137
  env: this.env,
128
138
  timeout: SYSTEMD_NOTIFY_TIMEOUT_MS,
129
139
  windowsHide: true,