oasis_test 0.1.117 → 0.1.118

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 (2) hide show
  1. package/dist/index.js +909 -156
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2866,9 +2866,11 @@ var init_execution_continuity = __esm({
2866
2866
  });
2867
2867
 
2868
2868
  // ../contract/src/automation.ts
2869
+ var AUTOMATION_EVENT_NAMES;
2869
2870
  var init_automation = __esm({
2870
2871
  "../contract/src/automation.ts"() {
2871
2872
  "use strict";
2873
+ AUTOMATION_EVENT_NAMES = ["workorder.completed"];
2872
2874
  }
2873
2875
  });
2874
2876
 
@@ -7526,7 +7528,11 @@ function tryToRun(state, nodeId, ctx, events) {
7526
7528
  nodeId,
7527
7529
  workId,
7528
7530
  reasons,
7529
- continuesWorkId: null,
7531
+ // ★ 基线(配套一):base=上一已验收版(latestAcceptId),parents[0]=最近一个有产出版
7532
+ //(latestProposedWorkId)。两者解耦:中间夹超时/零产出空壳时 base 仍在上一已验收版、parents 跳过空壳。
7533
+ // 不用 latestWorkId——它含空壳,会把基线钉到未验收/无产出的 work 上(isStale 比已验收 head,错)。
7534
+ continuesWorkId: node.latestAcceptId,
7535
+ parentWorkId: node.latestProposedWorkId,
7530
7536
  // 工单 e4a43a42(接续只对短时重试成立):**已收口过的节点被重新激活 = 返工**(变更请求/驳回/上游
7531
7537
  // 变更把已 accept 的节点重开)→ 起全新会话,别接续那场过时的旧对话;**从未收口的节点** = 首跑或
7532
7538
  // 失败后重试,接续 WIP 是对的。判据 = 有没有 latest_accept。派发层据此决定 resumeSession。
@@ -7901,6 +7907,9 @@ CREATE TABLE IF NOT EXISTS ${schema}.works (
7901
7907
  outcome_detail jsonb,
7902
7908
  session_ref text,
7903
7909
  continues_work_id text,
7910
+ -- \u2605 \u8C31\u7CFB\u4EB2\u672C\uFF08parents[0]\uFF09\uFF1Awork \u5EFA\u65F6\u7684\u300C\u6700\u8FD1\u4E00\u4E2A\u6709\u4EA7\u51FA\u7248\u300D\uFF08node.latest_proposed_work_id\uFF09\u3002
7911
+ -- \u4E0E continues_work_id\uFF08base=\u5DF2\u9A8C\u6536\u7248\uFF09\u89E3\u8026\u2014\u2014\u4E24\u8005\u8BED\u4E49\u4E0D\u540C\u3001\u53D6\u503C\u5E38\u5F02\uFF08\u4E2D\u95F4\u5939\u7A7A\u58F3\u65F6\u5C24\u5176\u5982\u6B64\uFF09\u3002
7912
+ parent_work_id text,
7904
7913
  -- \u2605 work \u5C5E\u4E8E\u54EA\u6761\u9053\uFF08\u6B63\u5411\u3001\u663E\u5F0F\u4E8B\u5B9E\uFF09\uFF1A'main'=\u8282\u70B9\u4E3B\u94FE\u4EA7\u51FA / 'reply'=\u56DE\u4FE1\u8F6E\u3002\u53D6\u4EE3\u65E7\u7684\u5426\u5B9A\u5F0F\u5224\u636E
7905
7914
  -- "reply_to_issue_id IS NULL"\u3002\u5B58\u91CF\u884C\u6309 reply_to_issue_id \u56DE\u586B\uFF08\u89C1\u4E0B\u65B9\u8FC1\u79FB\u6BB5\uFF09\u3002
7906
7915
  lane text CHECK (lane IN ('main','reply')),
@@ -8078,6 +8087,10 @@ CREATE TABLE IF NOT EXISTS ${schema}.event_handler_log (
8078
8087
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS retry_at timestamptz;
8079
8088
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS retry_at timestamptz;
8080
8089
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS status text;
8090
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS parent_work_id text;
8091
+ -- \u5B58\u91CF\u56DE\u586B\uFF1Aparent_work_id \u7F3A\u7701\u6CBF\u7528 continues_work_id\uFF08\u65E7\u6865 parents=[continues_work_id] \u7684\u65E2\u6709\u8C31\u7CFB\uFF09\uFF0C
8092
+ -- \u65B0 work \u624D\u5199\u300C\u6700\u8FD1\u6709\u4EA7\u51FA\u7248\u300D\uFF08latest_proposed_work_id\uFF09\u5B9E\u73B0 base/parents \u89E3\u8026\u3002
8093
+ UPDATE ${schema}.works SET parent_work_id = continues_work_id WHERE parent_work_id IS NULL;
8081
8094
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS agent_handoff_attempt_id text;
8082
8095
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_status text;
8083
8096
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_recorded_at timestamptz;
@@ -9427,7 +9440,7 @@ var require_utils2 = __commonJS({
9427
9440
  var nodeCrypto = require("crypto");
9428
9441
  module2.exports = {
9429
9442
  postgresMd5PasswordHash,
9430
- randomBytes: randomBytes7,
9443
+ randomBytes: randomBytes8,
9431
9444
  deriveKey,
9432
9445
  sha256,
9433
9446
  hashByName,
@@ -9437,7 +9450,7 @@ var require_utils2 = __commonJS({
9437
9450
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
9438
9451
  var subtleCrypto = webCrypto.subtle;
9439
9452
  var textEncoder = new TextEncoder();
9440
- function randomBytes7(length) {
9453
+ function randomBytes8(length) {
9441
9454
  return webCrypto.getRandomValues(Buffer.alloc(length));
9442
9455
  }
9443
9456
  async function md5(string5) {
@@ -13569,6 +13582,7 @@ var init_store_postgres = __esm({
13569
13582
  outcomeDetail: w2.outcome_detail,
13570
13583
  sessionRef: w2.session_ref,
13571
13584
  continuesWorkId: w2.continues_work_id,
13585
+ parentWorkId: w2.parent_work_id,
13572
13586
  // ★ lane 兜底(存量行迁移前 / 迁移与回填之间的竞态窗):无值时按 reply_to_issue_id 派生,
13573
13587
  // 保证 TS 侧 `Work.lane` 恒非空。reply_to_issue_id 在读写映射层可直接读(护栏白名单)。
13574
13588
  lane: w2.lane ?? (w2.reply_to_issue_id == null ? "main" : "reply"),
@@ -13784,10 +13798,10 @@ var init_store_postgres = __esm({
13784
13798
  await this.c.query(
13785
13799
  `INSERT INTO ${this.s}.works
13786
13800
  (id,workorder_id,node_id,assignee_actor_id,created_at,started_at,ended_at,dead_at,cancelled_at,retry_at,status,outcome,
13787
- outcome_detail,session_ref,continues_work_id,reply_to_issue_id,output_version_no,
13801
+ outcome_detail,session_ref,continues_work_id,parent_work_id,reply_to_issue_id,output_version_no,
13788
13802
  node_version,conclusion,proposal_reason,last_activity_at,frozen_issue_ids,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
13789
13803
  acceptance_state,accepted_at,accepted_by,rejected_reason,override_by,override_reason,lane)
13790
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32)
13804
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
13791
13805
  ON CONFLICT (id) DO NOTHING`,
13792
13806
  [
13793
13807
  w2.id,
@@ -13805,6 +13819,7 @@ var init_store_postgres = __esm({
13805
13819
  w2.outcomeDetail ? JSON.stringify(w2.outcomeDetail) : null,
13806
13820
  w2.sessionRef,
13807
13821
  w2.continuesWorkId,
13822
+ w2.parentWorkId,
13808
13823
  w2.replyToIssueId ?? null,
13809
13824
  w2.outputVersionNo,
13810
13825
  w2.nodeVersion,
@@ -14596,6 +14611,7 @@ var init_work = __esm({
14596
14611
  outcomeDetail: null,
14597
14612
  sessionRef: null,
14598
14613
  continuesWorkId: e.continuesWorkId,
14614
+ parentWorkId: e.parentWorkId ?? null,
14599
14615
  replyToIssueId: e.replyToIssueId,
14600
14616
  outputVersionNo: null,
14601
14617
  conclusion: null,
@@ -14634,6 +14650,7 @@ var init_work = __esm({
14634
14650
  outcomeDetail: null,
14635
14651
  sessionRef: null,
14636
14652
  continuesWorkId: e.continuesWorkId,
14653
+ parentWorkId: e.parentWorkId ?? null,
14637
14654
  outputVersionNo: null,
14638
14655
  conclusion: null,
14639
14656
  frozenIssueIds: frozen.length > 0 ? frozen : null,
@@ -15958,8 +15975,9 @@ function mapWorkStateToRevisionState(w2, artifactCount) {
15958
15975
  case "success":
15959
15976
  if (w2.acceptanceState === "accepted") return "merged";
15960
15977
  if (w2.acceptanceState === "rejected") return "rejected";
15978
+ if (w2.conclusion == null) return "working";
15961
15979
  return "proposed";
15962
- // success but not yet judged
15980
+ // conclude、未裁决
15963
15981
  // ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
15964
15982
  // superseded,改枚举会牵动契约与前端,见红线 4)——仍投影成 superseded 保持生命周期语义,但**内容
15965
15983
  // 如实为 empty**(workToRevision 对无产出 work 给 contentKind="empty")。读面(CLI 版本列表)据此把
@@ -16037,7 +16055,9 @@ function workToRevision(w2, workInputs, artifactFiles) {
16037
16055
  id: w2.id,
16038
16056
  artifactId: w2.nodeId,
16039
16057
  base: w2.continuesWorkId,
16040
- parents: w2.continuesWorkId ? [w2.continuesWorkId] : [],
16058
+ // 配套一:base parents 解耦——base=上一已验收版(continuesWorkId),parents[0]=最近一个有产出版
16059
+ //(parentWorkId)。两者可不同:中间夹超时/零产出空壳时 base 不动、parents 跳过空壳指向上一个有产出。
16060
+ parents: w2.parentWorkId ? [w2.parentWorkId] : [],
16041
16061
  contentRef,
16042
16062
  contentKind,
16043
16063
  basedOn: Object.keys(basedOn).length > 0 ? basedOn : void 0,
@@ -16256,6 +16276,16 @@ function bridgeLoadReadModel(input) {
16256
16276
  for (const w2 of allWorks) {
16257
16277
  revisions.set(w2.id, workToRevision(w2, allWorkInputs, artifactFiles));
16258
16278
  }
16279
+ for (const r of revisions.values()) {
16280
+ if (r.state !== "proposed") continue;
16281
+ for (const parentId of r.parents) {
16282
+ const parent = revisions.get(parentId);
16283
+ if (parent && parent.state === "proposed" && (parent.part ?? void 0) === (r.part ?? void 0)) {
16284
+ parent.state = "superseded";
16285
+ parent.supersededBy = r.id;
16286
+ }
16287
+ }
16288
+ }
16259
16289
  const recipientsByIssue = /* @__PURE__ */ new Map();
16260
16290
  for (const r of allIssueRecipients) {
16261
16291
  const list = recipientsByIssue.get(r.issueId) ?? [];
@@ -17304,10 +17334,12 @@ var init_kernel_bridge = __esm({
17304
17334
  nodeId: args.artifactId,
17305
17335
  workId: revisionId,
17306
17336
  reasons: [{ kind: "initial" }],
17307
- // ★ base 三态:undefined=缺省续写(基当前 head,旧引擎 propose 即基上版——否则新版 base=∅、
17308
- // 相对当前 head stale advanceQueue 永不落地,annotation-resolve routing 实测);null=并发
17337
+ // ★ base 三态:undefined=缺省续写(基「上一已验收版」node.latestAcceptId——isStale concludedHead,
17338
+ // 只有 latestAcceptId 对;旧口径 artifact.currentRev=latestWorkId 可能是一条未验收/空壳 work);null=并发
17309
17339
  // 作者显式声明 base=∅(autoland stale 兜底:不落、留队);具体 revisionId=钉旧版。
17310
- continuesWorkId: args.base !== void 0 ? args.base : artifact?.currentRev ?? null
17340
+ // parents[0]=「最近一个有产出版」node.latestProposedWorkId(跳过超时/零产出空壳),与 base 解耦。
17341
+ continuesWorkId: args.base !== void 0 ? args.base : nodeRow?.latestAcceptId ?? null,
17342
+ parentWorkId: nodeRow?.latestProposedWorkId ?? null
17311
17343
  }
17312
17344
  });
17313
17345
  workId = revisionId;
@@ -40107,7 +40139,7 @@ var require_websocket = __commonJS({
40107
40139
  var http2 = require("http");
40108
40140
  var net = require("net");
40109
40141
  var tls = require("tls");
40110
- var { randomBytes: randomBytes7, createHash: createHash19 } = require("crypto");
40142
+ var { randomBytes: randomBytes8, createHash: createHash21 } = require("crypto");
40111
40143
  var { Duplex, Readable } = require("stream");
40112
40144
  var { URL: URL2 } = require("url");
40113
40145
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -40645,7 +40677,7 @@ var require_websocket = __commonJS({
40645
40677
  }
40646
40678
  }
40647
40679
  const defaultPort = isSecure ? 443 : 80;
40648
- const key = randomBytes7(16).toString("base64");
40680
+ const key = randomBytes8(16).toString("base64");
40649
40681
  const request2 = isSecure ? https.request : http2.request;
40650
40682
  const protocolSet = /* @__PURE__ */ new Set();
40651
40683
  let perMessageDeflate;
@@ -40775,7 +40807,7 @@ var require_websocket = __commonJS({
40775
40807
  abortHandshake(websocket, socket, "Invalid Upgrade header");
40776
40808
  return;
40777
40809
  }
40778
- const digest = createHash19("sha1").update(key + GUID).digest("base64");
40810
+ const digest = createHash21("sha1").update(key + GUID).digest("base64");
40779
40811
  if (res.headers["sec-websocket-accept"] !== digest) {
40780
40812
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
40781
40813
  return;
@@ -41144,7 +41176,7 @@ var require_websocket_server = __commonJS({
41144
41176
  var EventEmitter = require("events");
41145
41177
  var http2 = require("http");
41146
41178
  var { Duplex } = require("stream");
41147
- var { createHash: createHash19 } = require("crypto");
41179
+ var { createHash: createHash21 } = require("crypto");
41148
41180
  var extension2 = require_extension();
41149
41181
  var PerMessageDeflate2 = require_permessage_deflate();
41150
41182
  var subprotocol2 = require_subprotocol();
@@ -41451,7 +41483,7 @@ var require_websocket_server = __commonJS({
41451
41483
  );
41452
41484
  }
41453
41485
  if (this._state > RUNNING) return abortHandshake(socket, 503);
41454
- const digest = createHash19("sha1").update(key + GUID).digest("base64");
41486
+ const digest = createHash21("sha1").update(key + GUID).digest("base64");
41455
41487
  const headers = [
41456
41488
  "HTTP/1.1 101 Switching Protocols",
41457
41489
  "Upgrade: websocket",
@@ -158018,6 +158050,40 @@ var init_resume_classify = __esm({
158018
158050
  }
158019
158051
  });
158020
158052
 
158053
+ // ../adapters/src/_core/root-bypass.ts
158054
+ function decideRootBypass(sources) {
158055
+ if (sources.uid !== 0) return { inject: false, reason: "not-root" };
158056
+ if (sources.env["IS_SANDBOX"] === "1") return { inject: false, reason: "already-sandboxed" };
158057
+ if (sources.env["CLAUDE_CODE_BUBBLEWRAP"]) return { inject: false, reason: "bubblewrap" };
158058
+ const sw = (sources.env["OASIS_ALLOW_ROOT_BYPASS"] ?? "").trim().toLowerCase();
158059
+ if (OFF_VALUES.has(sw)) return { inject: false, reason: "disabled" };
158060
+ return { inject: true, reason: "root-needs-sandbox-flag" };
158061
+ }
158062
+ function rootBypassEnv(runtime, sources = { uid: process.getuid?.(), env: process.env }) {
158063
+ const d = decideRootBypass(sources);
158064
+ if (d.inject || d.reason === "disabled") {
158065
+ const key = `${runtime}:${d.reason}`;
158066
+ if (!loggedOnce.has(key)) {
158067
+ loggedOnce.add(key);
158068
+ console.warn(`[adapter:${runtime}] ` + JSON.stringify({
158069
+ event: "root-bypass",
158070
+ injected: d.inject,
158071
+ reason: d.reason,
158072
+ note: d.inject ? "\u4EE5 root \u8FD0\u884C\uFF1A\u6CE8\u5165 IS_SANDBOX=1 \u653E\u884C bypassPermissions\uFF08\u5173\u95ED\uFF1AOASIS_ALLOW_ROOT_BYPASS=off\uFF09" : "\u4EE5 root \u8FD0\u884C\u4E14 OASIS_ALLOW_ROOT_BYPASS=off\uFF1ACLI \u5C06\u62D2\u7EDD bypassPermissions\uFF0C\u4F1A\u8BDD\u4F1A\u7ACB\u5373\u9000\u51FA"
158073
+ }));
158074
+ }
158075
+ }
158076
+ return d.inject ? { IS_SANDBOX: "1" } : {};
158077
+ }
158078
+ var OFF_VALUES, loggedOnce;
158079
+ var init_root_bypass = __esm({
158080
+ "../adapters/src/_core/root-bypass.ts"() {
158081
+ "use strict";
158082
+ OFF_VALUES = /* @__PURE__ */ new Set(["off", "0", "false", "no"]);
158083
+ loggedOnce = /* @__PURE__ */ new Set();
158084
+ }
158085
+ });
158086
+
158021
158087
  // ../adapters/src/_core/transcript.ts
158022
158088
  function openTranscript(file) {
158023
158089
  const out = fs8.createWriteStream(file);
@@ -158268,6 +158334,7 @@ var init_claude_code = __esm({
158268
158334
  init_materialize_bundle();
158269
158335
  init_container_runtime();
158270
158336
  init_resume_classify();
158337
+ init_root_bypass();
158271
158338
  init_session_paths();
158272
158339
  init_transcript();
158273
158340
  init_filter_extra_args();
@@ -158427,7 +158494,11 @@ var init_claude_code = __esm({
158427
158494
  OASIS_SERVER: job.server.url,
158428
158495
  OASIS_TOKEN: job.actorToken,
158429
158496
  // OTel 遥测(默认关;resolveOtelEnv 决定开不开,runId 进 resource 属性自然归位到本次 run)。
158430
- ...otelEnv
158497
+ ...otelEnv,
158498
+ // root 下放行 bypassPermissions(见 _core/root-bypass.ts:claude 的闸认 IS_SANDBOX=1)。
158499
+ // 铺在最后=系统强制层(同 OASIS_SERVER/TOKEN):员工 env 或工单 executionEnv 里的陈旧值
158500
+ // 不得把它顶掉。非 root / 宿主已给 / 显式关闭时是空对象,行为逐字节不变。
158501
+ ...rootBypassEnv("claude-code")
158431
158502
  },
158432
158503
  stdio: ["pipe", "pipe", "pipe"]
158433
158504
  });
@@ -161493,14 +161564,17 @@ async function materialize(job, cfg) {
161493
161564
  const task = materializeBundle(dir, job.bundle);
161494
161565
  return { id, dir, task };
161495
161566
  }
161496
- function buildEnv(job, extra) {
161567
+ function buildEnv(job, cfg) {
161497
161568
  return {
161498
161569
  ...scrubLeakyEnv(process.env),
161499
- ...extra,
161570
+ ...cfg.env,
161500
161571
  ...job.env,
161501
161572
  ...job.wrapperPaths?.length ? { PATH: `${job.wrapperPaths.map((p2) => path12.dirname(p2)).join(path12.delimiter)}${path12.delimiter}${job.env?.["PATH"] ?? process.env["PATH"] ?? ""}` } : {},
161502
161573
  OASIS_SERVER: job.server.url,
161503
- OASIS_TOKEN: job.actorToken
161574
+ OASIS_TOKEN: job.actorToken,
161575
+ // root 下放行 bypassPermissions(仅 claude 家族 runtime 声明;见 _core/root-bypass.ts)。
161576
+ // 与 OASIS_SERVER/TOKEN 同属系统强制层:员工 env 顶不掉。
161577
+ ...cfg.rootBypassRuntime ? rootBypassEnv(cfg.rootBypassRuntime) : {}
161504
161578
  };
161505
161579
  }
161506
161580
  function makeFinish(id, cfg, transcriptRefPtr, transcriptOut, dir, workdirKey, runtimeSessionId, exitCbs) {
@@ -161559,7 +161633,7 @@ async function runStreamJson(job, cfg) {
161559
161633
  stdio: ["ignore", "pipe", "pipe"]
161560
161634
  }) : (0, import_node_child_process11.spawn)(cfg.bin, args, {
161561
161635
  cwd: dir,
161562
- env: buildEnv(job, cfg.env),
161636
+ env: buildEnv(job, cfg),
161563
161637
  stdio: ["ignore", "pipe", "pipe"]
161564
161638
  });
161565
161639
  if (child.pid !== void 0 && job.workdirKey) lockWorkdir(dir, child.pid, "session", job.workdirKey);
@@ -161659,7 +161733,7 @@ async function runOneShotText(job, cfg) {
161659
161733
  stdio: ["ignore", "pipe", "pipe"]
161660
161734
  }) : (0, import_node_child_process11.spawn)(cfg.bin, args, {
161661
161735
  cwd: dir,
161662
- env: buildEnv(job, cfg.env),
161736
+ env: buildEnv(job, cfg),
161663
161737
  stdio: ["ignore", "pipe", "pipe"]
161664
161738
  });
161665
161739
  if (child.pid !== void 0 && job.workdirKey) lockWorkdir(dir, child.pid, "session", job.workdirKey);
@@ -161743,6 +161817,7 @@ var init_subprocess = __esm({
161743
161817
  init_session_paths();
161744
161818
  init_transcript();
161745
161819
  init_filter_extra_args();
161820
+ init_root_bypass();
161746
161821
  }
161747
161822
  });
161748
161823
 
@@ -161859,7 +161934,10 @@ var init_codebuddy = __esm({
161859
161934
  workRoot: opts.workRoot,
161860
161935
  cleanupWorkdir: opts.cleanupWorkdir,
161861
161936
  env: opts.env,
161862
- extraArgs: opts.extraArgs
161937
+ extraArgs: opts.extraArgs,
161938
+ // CodeBuddy 是 Claude Code 分叉,走同一个 bypassPermissions flag,root 下大概率撞同一条闸
161939
+ // (claude 侧已实测)。声明后由 _core/root-bypass.ts 在 root 下注入 IS_SANDBOX=1。
161940
+ rootBypassRuntime: "codebuddy"
161863
161941
  });
161864
161942
  }
161865
161943
  };
@@ -168058,6 +168136,10 @@ var init_memory_automation_store = __esm({
168058
168136
  hasSigningSecret: false
168059
168137
  };
168060
168138
  }
168139
+ if (input.kind === "event" && input.event) {
168140
+ t.enabled = false;
168141
+ t.event = { source: "project", projectId: input.event.projectId, events: [...input.event.events], baselineState: "aligning" };
168142
+ }
168061
168143
  return t;
168062
168144
  }
168063
168145
  async replaceTriggers(automationId, triggers) {
@@ -168085,6 +168167,7 @@ var init_memory_automation_store = __esm({
168085
168167
  if (t.webhook && patch.webhook && "eventFilters" in patch.webhook) {
168086
168168
  t.webhook.eventFilters = patch.webhook.eventFilters ?? null;
168087
168169
  }
168170
+ if (t.event && patch.event?.events) t.event.events = [...patch.event.events];
168088
168171
  return structuredClone(t);
168089
168172
  }
168090
168173
  async deleteTrigger(triggerId) {
@@ -168113,6 +168196,33 @@ var init_memory_automation_store = __esm({
168113
168196
  if (t) t.publishedBy = publishedBy;
168114
168197
  }
168115
168198
  }
168199
+ /* ---------- event 触发器(ADR 0163) ---------- */
168200
+ async listEventTriggers() {
168201
+ const out = [];
168202
+ for (const t of this.triggers.values()) {
168203
+ if (t.kind !== "event") continue;
168204
+ const a = this.automations.get(t.automationId);
168205
+ if (!a || !a.enabled || a.archivedAt) continue;
168206
+ out.push(structuredClone(t));
168207
+ }
168208
+ return out.sort((x2, y) => x2.id < y.id ? -1 : x2.id > y.id ? 1 : 0);
168209
+ }
168210
+ async completeEventBaseline(triggerId, _at) {
168211
+ const t = this.triggers.get(triggerId);
168212
+ if (!t?.event) return;
168213
+ t.event.baselineState = "ready";
168214
+ t.enabled = true;
168215
+ }
168216
+ async resetEventBaseline(triggerId) {
168217
+ const t = this.triggers.get(triggerId);
168218
+ if (!t?.event) return;
168219
+ t.event.baselineState = "aligning";
168220
+ t.enabled = false;
168221
+ }
168222
+ async findRunBySpawnedWorkspace(workspace) {
168223
+ const hits = [...this.runs.values()].filter((r) => r.spawnedWorkspaceId === workspace).sort((a, b2) => a.triggeredAt < b2.triggeredAt ? -1 : a.triggeredAt > b2.triggeredAt ? 1 : a.id < b2.id ? -1 : 1);
168224
+ return hits[0] ? structuredClone(hits[0]) : null;
168225
+ }
168116
168226
  /* ---------- runs ---------- */
168117
168227
  async createRun(run) {
168118
168228
  if (run.triggerId && run.plannedAt) {
@@ -183313,7 +183423,11 @@ var init_readonly_bundle = __esm({
183313
183423
  function makeSeededWorkorderCreator(deps) {
183314
183424
  const genWorkspace = deps.genWorkspace ?? (() => `ws:wo-${(0, import_node_crypto33.randomUUID)().slice(0, 8)}`);
183315
183425
  return async (input) => {
183316
- const workspace = genWorkspace();
183426
+ const workspace = input.idempotencyKey ? workspaceForKey(input.idempotencyKey) : genWorkspace();
183427
+ if (input.idempotencyKey) {
183428
+ const existing = existingWorkorder(deps.kernel, workspace);
183429
+ if (existing) return existing;
183430
+ }
183317
183431
  if (deps.artifactState && (input.projectId || deps.createProject)) {
183318
183432
  await ensureWorkorderProject({
183319
183433
  workOrderId: workspace,
@@ -183392,6 +183506,20 @@ function makeSeededWorkorderCreator(deps) {
183392
183506
  return { workspace, rootArtifactId: briefId, spawned: planned.spawned };
183393
183507
  };
183394
183508
  }
183509
+ function workspaceForKey(key) {
183510
+ return `ws:wo-${(0, import_node_crypto33.createHash)("sha256").update(key).digest("hex").slice(0, 8)}`;
183511
+ }
183512
+ function existingWorkorder(kernel, workspace) {
183513
+ const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === workspace);
183514
+ if (arts.length === 0) return null;
183515
+ const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
183516
+ return {
183517
+ workspace,
183518
+ rootArtifactId: seed.id,
183519
+ // role 取自 planner 的规划结果、不是 artifact 上的字段——重试路径拿不到也无所谓(可省)。
183520
+ spawned: arts.map((a) => ({ id: a.id, type: a.type, owner: a.owner }))
183521
+ };
183522
+ }
183395
183523
  var import_node_crypto33, enc2;
183396
183524
  var init_create_seeded_workorder = __esm({
183397
183525
  "../server/src/domains/collab/create-seeded-workorder.ts"() {
@@ -186076,6 +186204,7 @@ function automationSubstantiveChange(before, after) {
186076
186204
  }
186077
186205
  function triggerSubstantiveChange(before, after) {
186078
186206
  if (before.enabled !== after.enabled) return true;
186207
+ if (!jsonEq(before.event?.events ?? null, after.event?.events ?? null)) return true;
186079
186208
  if ((before.schedule?.cron ?? null) !== (after.schedule?.cron ?? null)) return true;
186080
186209
  if ((before.schedule?.tz ?? null) !== (after.schedule?.tz ?? null)) return true;
186081
186210
  const filtersOf = (t) => t.webhook?.eventFilters ?? null;
@@ -186927,6 +187056,17 @@ function renderDate(at, tz) {
186927
187056
  return at.toISOString().slice(0, 10);
186928
187057
  }
186929
187058
  }
187059
+ function eventCommandKey(runId) {
187060
+ return `automation-event:${runId}`;
187061
+ }
187062
+ function eventDispatchState(run) {
187063
+ const r = run.result;
187064
+ if (r && typeof r === "object" && !Array.isArray(r)) {
187065
+ const d = r["eventDispatch"];
187066
+ if (d && typeof d === "object" && !Array.isArray(d)) return d;
187067
+ }
187068
+ return {};
187069
+ }
186930
187070
  function clipReply(reply) {
186931
187071
  const t = reply.trim();
186932
187072
  return t.length > CHAT_REPLY_MAX ? `${t.slice(0, CHAT_REPLY_MAX)}
@@ -186958,7 +187098,7 @@ function appendSourceNote(brief, automation, source, atIso) {
186958
187098
  ---
186959
187099
  \u672C\u5DE5\u5355\u7531\u81EA\u52A8\u5316\u300C${automation.name}\u300D\u4E8E ${atIso} \u89E6\u53D1\uFF08\u6765\u6E90\uFF1A${source}\uFF09\u3002\u5F00\u5DE5\u540E\u8BF7\u628A\u5DE5\u5355\u6807\u9898\u6539\u6210\u51C6\u786E\u53CD\u6620\u5B9E\u9645\u5DE5\u4F5C\u7684\u63CF\u8FF0\u3002`;
186960
187100
  }
186961
- var import_node_crypto36, DATE_TOKEN, AutomationsService, CHAT_REPLY_MAX;
187101
+ var import_node_crypto36, DATE_TOKEN, AutomationsService, EVENT_RETRY_BASE_MS, EVENT_RETRY_MAX_MS, EVENT_RETRY_MAX_ATTEMPTS, eventAttempts, eventLastAttemptAt, eventLastError, CHAT_REPLY_MAX;
186962
187102
  var init_service8 = __esm({
186963
187103
  "../server/src/domains/automations/service.ts"() {
186964
187104
  "use strict";
@@ -187051,7 +187191,7 @@ var init_service8 = __esm({
187051
187191
  await seedScheduleNextRuns(this.store, [await this.store.getTrigger(t.id) ?? t].filter(Boolean), this.now());
187052
187192
  return await this.store.getTrigger(t.id);
187053
187193
  }
187054
- /** 单条触发器 PATCH。归因:比较持久化前后行,实质变更(enabled/cron/tz/eventFilters)只转移**该行**。 */
187194
+ /** 单条触发器 PATCH。归因:比较持久化前后行,实质变更(enabled/cron/tz/eventFilters/events)只转移**该行**。 */
187055
187195
  async updateTrigger(triggerId, patch, editor) {
187056
187196
  const before = await this.store.getTrigger(triggerId);
187057
187197
  if (!before) throw new Error(`trigger not found: ${triggerId}`);
@@ -187062,6 +187202,9 @@ var init_service8 = __esm({
187062
187202
  if (after.kind === "schedule") {
187063
187203
  await seedScheduleNextRuns(this.store, [after], this.now());
187064
187204
  }
187205
+ if (after.kind === "event" && patch.enabled === true && !before.enabled) {
187206
+ await this.store.resetEventBaseline(triggerId);
187207
+ }
187065
187208
  return await this.store.getTrigger(triggerId);
187066
187209
  }
187067
187210
  async deleteTrigger(triggerId, editor) {
@@ -187141,7 +187284,17 @@ var init_service8 = __esm({
187141
187284
  };
187142
187285
  const { run, created } = await this.store.createRun(base);
187143
187286
  if (!created) return run;
187144
- const skip = await this.admissionSkip(automation);
187287
+ return this.dispatchRun(automation, trigger, run, source, opts);
187288
+ }
187289
+ /**
187290
+ * 对一条**已占位的 pending run** 做准入 + 派发。
187291
+ *
187292
+ * 与 `fire()` 拆开是因为 event 触发必须「先原子占位、再决定派不派」(ADR 0163 §4.2 第 4/5 步):
187293
+ * 占位赢家才做权限/自环准入,输的那个直接停手。reconciler 重试也复用这里——同一 runId、
187294
+ * 同一下游命令键,取回同一副作用。
187295
+ */
187296
+ async dispatchRun(automation, trigger, run, source, opts = {}) {
187297
+ const skip = await this.admissionSkip(automation, source);
187145
187298
  if (skip) return this.settleSkip(run, skip.reasonCode, skip.error);
187146
187299
  if (automation.executionMode === "chat") {
187147
187300
  return this.fireChat(automation, trigger, run, opts);
@@ -187161,6 +187314,14 @@ var init_service8 = __esm({
187161
187314
  return settled ?? run;
187162
187315
  } catch (e) {
187163
187316
  const msg = e instanceof Error ? e.message : String(e);
187317
+ if (source === "event") {
187318
+ await this.store.settleRun(run.id, {
187319
+ status: "pending",
187320
+ result: { eventDispatch: { attempts: eventAttempts(run) + 1, lastAttemptAt: this.now().toISOString(), lastError: msg } },
187321
+ completedAt: null
187322
+ });
187323
+ return await this.store.getRun(run.id);
187324
+ }
187164
187325
  await this.store.settleRun(run.id, {
187165
187326
  status: "failed",
187166
187327
  error: msg,
@@ -187194,12 +187355,93 @@ var init_service8 = __esm({
187194
187355
  });
187195
187356
  return run;
187196
187357
  }
187197
- async admissionSkip(automation) {
187358
+ /* ---------- 订阅触发(event,ADR 0163) ---------- */
187359
+ /**
187360
+ * 能力位:这条自动化现在能不能用订阅触发。**唯一真值在这里**——详情响应把它原样带给前端,
187361
+ * 前端只消费不推断(否则前后端两套规则必漂移)。
187362
+ */
187363
+ eventTriggerCapability(automation) {
187364
+ if (automation.executionMode === "chat" && !this.deps.chatSupportsIdempotency) {
187365
+ return { supported: false, reason: "chat_no_idempotency" };
187366
+ }
187367
+ return { supported: true };
187368
+ }
187369
+ /**
187370
+ * 事件消费键占位:用同一把键原子落一条 run。
187371
+ * 唯一约束 `(automation_id, idempotency_key)` 负责多实例竞争——`created=false` 的那个实例停手。
187372
+ */
187373
+ claimEventRun(automation, trigger, key, envelope, status = "pending", reasonCode = null, error2 = null) {
187374
+ const nowIso = this.now().toISOString();
187375
+ return this.store.createRun({
187376
+ id: this.genId("arun"),
187377
+ automationId: automation.id,
187378
+ triggerId: trigger.id,
187379
+ source: "event",
187380
+ status,
187381
+ spawnedWorkspaceId: null,
187382
+ spawnedArtifactId: null,
187383
+ plannedAt: null,
187384
+ deliveryId: null,
187385
+ idempotencyKey: key,
187386
+ ruleVersionId: null,
187387
+ payload: envelope,
187388
+ result: null,
187389
+ error: error2,
187390
+ reasonCode,
187391
+ triggeredAt: nowIso,
187392
+ completedAt: status === "skipped" ? nowIso : null
187393
+ });
187394
+ }
187395
+ /** 把一条占位成功的 event run 派出去(准入 → 建单/开会话),命令键固定为 automation-event:<runId>。 */
187396
+ dispatchEventRun(automation, trigger, run) {
187397
+ return this.dispatchRun(automation, trigger, run, "event", {
187398
+ ...run.payload != null ? { envelope: run.payload } : {},
187399
+ commandKey: eventCommandKey(run.id)
187400
+ });
187401
+ }
187402
+ /** 已占位但被准入拒绝:落 skipped + reasonCode(**留痕**,不是静默丢弃)。 */
187403
+ skipEventRun(run, reasonCode, error2) {
187404
+ return this.settleSkip(run, reasonCode, error2);
187405
+ }
187406
+ /**
187407
+ * event pending 的可恢复派发(ADR 0163 §4.4)。
187408
+ *
187409
+ * **不走**「pending 超时即 failed」那条分支:那条规则会把「占位了但还没产生副作用」的 run 直接
187410
+ * 判死,等于永久漏一次触发。这里按稳定 runId 重试同一下游命令键,有界退避;
187411
+ * 达到重试上限的基础设施错误才落 failed 并告警(该 run 仍占着事件键,不自动制造第二次业务执行)。
187412
+ */
187413
+ async reconcileEventRun(run) {
187414
+ const attempts = eventAttempts(run);
187415
+ const nowMs = this.now().getTime();
187416
+ const lastAt = eventLastAttemptAt(run) ?? Date.parse(run.triggeredAt);
187417
+ if (attempts >= EVENT_RETRY_MAX_ATTEMPTS) {
187418
+ await this.store.settleRun(run.id, {
187419
+ status: "failed",
187420
+ error: `\u8BA2\u9605\u89E6\u53D1\u6D3E\u53D1\u91CD\u8BD5 ${attempts} \u6B21\u4ECD\u5931\u8D25\uFF1A${eventLastError(run) ?? "\u672A\u77E5\u9519\u8BEF"}`,
187421
+ reasonCode: "spawn_failed",
187422
+ completedAt: this.now().toISOString()
187423
+ });
187424
+ this.log?.(`[automations] \u26A0 \u8BA2\u9605\u89E6\u53D1 run ${run.id} \u8FBE\u91CD\u8BD5\u4E0A\u9650\u5DF2\u843D failed\u2014\u2014\u8BE5 run \u4ECD\u5360\u7740\u4E8B\u4EF6\u952E\uFF0C\u4EBA\u5DE5\u6062\u590D\u5FC5\u987B\u91CD\u8BD5\u540C\u4E00 runId`);
187425
+ return;
187426
+ }
187427
+ const backoffMs = attempts === 0 ? 0 : Math.min(EVENT_RETRY_BASE_MS * 2 ** (attempts - 1), EVENT_RETRY_MAX_MS);
187428
+ if (nowMs - lastAt < backoffMs) return;
187429
+ const automation = await this.store.getAutomation(run.automationId);
187430
+ if (!automation) return;
187431
+ const trigger = run.triggerId ? await this.store.getTrigger(run.triggerId) : null;
187432
+ await this.dispatchEventRun(automation, trigger, run).catch((e) => {
187433
+ this.log?.(`[automations] \u8BA2\u9605\u89E6\u53D1\u91CD\u8BD5\u5F02\u5E38 ${run.id}\uFF1A${String(e)}`);
187434
+ });
187435
+ }
187436
+ async admissionSkip(automation, source = "manual") {
187198
187437
  if (automation.archivedAt) return { reasonCode: "automation_archived", error: "\u81EA\u52A8\u5316\u5DF2\u5F52\u6863" };
187199
187438
  if (!automation.enabled) return { reasonCode: "automation_paused", error: "\u81EA\u52A8\u5316\u5DF2\u6682\u505C" };
187200
187439
  if (automation.executionMode === "chat") {
187201
187440
  if (!automation.agentId) return { reasonCode: "config_error", error: "chat \u6A21\u5F0F\u7F3A\u5C11\u6267\u884C\u4EBA\uFF08agentId\uFF09" };
187202
187441
  if (!this.deps.runChat) return { reasonCode: "config_error", error: "\u672C\u5B9E\u4F8B\u672A\u63A5 chat \u6267\u884C" };
187442
+ if (source === "event" && !this.deps.chatSupportsIdempotency) {
187443
+ return { reasonCode: "config_error", error: "\u5BF9\u8BDD\u6267\u884C\u6682\u4E0D\u652F\u6301\u8BA2\u9605\u89E6\u53D1\uFF08chat \u9002\u914D\u5668\u7F3A\u6301\u4E45\u5E42\u7B49\u952E\uFF09" };
187444
+ }
187203
187445
  if (this.deps.agentReady) {
187204
187446
  const ready = await this.deps.agentReady(automation.agentId);
187205
187447
  if (!ready.ok) return { reasonCode: ready.reason, error: ready.error };
@@ -187319,7 +187561,9 @@ var init_service8 = __esm({
187319
187561
  ...human ? { humanActorId: human } : {},
187320
187562
  ...tagId ? { tags: [tagId] } : {},
187321
187563
  // 成本安全(ADR 0010):dispatch=false(默认)→ 建单即设 dispatch-hold,待人放行。
187322
- ...automation.dispatch ? {} : { dispatch: false }
187564
+ ...automation.dispatch ? {} : { dispatch: false },
187565
+ // 稳定下游键(ADR 0163 §4.4):重试落在同一张工单上,占位后崩溃不会建出第二张。
187566
+ ...opts.commandKey ? { idempotencyKey: opts.commandKey } : {}
187323
187567
  });
187324
187568
  return { workspace: result.workspace, rootArtifactId: result.rootArtifactId };
187325
187569
  }
@@ -187362,6 +187606,10 @@ var init_service8 = __esm({
187362
187606
  continue;
187363
187607
  }
187364
187608
  if (run.status === "pending") {
187609
+ if (run.source === "event") {
187610
+ await this.reconcileEventRun(run);
187611
+ continue;
187612
+ }
187365
187613
  if (nowMs - Date.parse(run.triggeredAt) > 10 * 60 * 1e3) {
187366
187614
  await this.store.settleRun(run.id, {
187367
187615
  status: "failed",
@@ -187438,6 +187686,15 @@ var init_service8 = __esm({
187438
187686
  }
187439
187687
  }
187440
187688
  };
187689
+ EVENT_RETRY_BASE_MS = 3e4;
187690
+ EVENT_RETRY_MAX_MS = 10 * 6e4;
187691
+ EVENT_RETRY_MAX_ATTEMPTS = 5;
187692
+ eventAttempts = (run) => eventDispatchState(run).attempts ?? 0;
187693
+ eventLastAttemptAt = (run) => {
187694
+ const at = eventDispatchState(run).lastAttemptAt;
187695
+ return at ? Date.parse(at) : null;
187696
+ };
187697
+ eventLastError = (run) => eventDispatchState(run).lastError ?? null;
187441
187698
  CHAT_REPLY_MAX = 2e4;
187442
187699
  }
187443
187700
  });
@@ -187821,7 +188078,8 @@ function buildAutomationSummary(automation, triggers, recent, canWrite) {
187821
188078
  lastRun,
187822
188079
  nextRunAt,
187823
188080
  recent: ordered.slice(0, RECENT_LIMIT).map(stripRunPayload),
187824
- canWrite
188081
+ canWrite,
188082
+ capabilities: { eventTrigger: { supported: true } }
187825
188083
  };
187826
188084
  }
187827
188085
  function deriveNextRunAt(triggers) {
@@ -187867,10 +188125,12 @@ function validateTriggers(raw) {
187867
188125
  label: t.label ?? null,
187868
188126
  webhook: { provider, ...eventFilters !== void 0 ? { eventFilters } : {} }
187869
188127
  });
188128
+ } else if (t?.kind === "event") {
188129
+ out.push({ kind: "event", enabled: t.enabled ?? true, label: t.label ?? null, event: validateEventConfig(t.event) });
187870
188130
  } else if (t?.kind === "manual") {
187871
188131
  out.push({ kind: "manual", enabled: t.enabled ?? true, label: t.label ?? null });
187872
188132
  } else {
187873
- throw new ApiError(400, "BAD_REQUEST", "trigger.kind \u53EA\u80FD\u662F schedule\u3001webhook \u6216 manual");
188133
+ throw new ApiError(400, "BAD_REQUEST", "trigger.kind \u53EA\u80FD\u662F schedule\u3001webhook\u3001event \u6216 manual");
187874
188134
  }
187875
188135
  }
187876
188136
  return out;
@@ -187892,6 +188152,28 @@ function validateEventFilters(raw) {
187892
188152
  }
187893
188153
  return out;
187894
188154
  }
188155
+ function validateEventConfig(raw) {
188156
+ const cfg = raw ?? {};
188157
+ if (cfg.source !== void 0 && cfg.source !== "project") {
188158
+ throw new ApiError(400, "BAD_REQUEST", `\u4E0D\u652F\u6301\u7684\u4E8B\u4EF6\u6E90\uFF1A${String(cfg.source)}\uFF08\u672C\u671F\u53EA\u652F\u6301 project\uFF09`);
188159
+ }
188160
+ const projectId = typeof cfg.projectId === "string" ? cfg.projectId.trim() : "";
188161
+ if (!projectId) throw new ApiError(400, "BAD_REQUEST", "event \u89E6\u53D1\u5668\u9700\u8981 event.projectId");
188162
+ const events = normalizeEventNames(cfg.events);
188163
+ return { source: "project", projectId, events };
188164
+ }
188165
+ function normalizeEventNames(raw) {
188166
+ if (!Array.isArray(raw) || raw.length === 0) {
188167
+ throw new ApiError(400, "BAD_REQUEST", `event.events \u9700\u8981\u975E\u7A7A\u6570\u7EC4\uFF08\u53EF\u9009\u503C\uFF1A${AUTOMATION_EVENT_NAMES.join(", ")}\uFF09`);
188168
+ }
188169
+ const known = new Set(AUTOMATION_EVENT_NAMES);
188170
+ for (const e of raw) {
188171
+ if (typeof e !== "string" || !known.has(e)) {
188172
+ throw new ApiError(400, "UNKNOWN_EVENT", `\u4E0D\u652F\u6301\u7684\u4E8B\u4EF6\u7C7B\u578B\uFF1A${String(e)}\uFF08\u53EF\u9009\u503C\uFF1A${AUTOMATION_EVENT_NAMES.join(", ")}\uFF09`);
188173
+ }
188174
+ }
188175
+ return AUTOMATION_EVENT_NAMES.filter((n) => raw.includes(n));
188176
+ }
187895
188177
  function assertValidCron(cron, tz) {
187896
188178
  const v2 = validateCron(cron, tz);
187897
188179
  if (!v2.ok) throw new ApiError(400, v2.code, v2.message);
@@ -187954,13 +188236,66 @@ function automationsDomain(opts) {
187954
188236
  throw new ApiError(403, "NOT_ALLOWED", "\u53EA\u6709\u521B\u5EFA\u8005\u6216\u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u534F\u4F5C\u8005");
187955
188237
  }
187956
188238
  };
188239
+ const decorateEventTriggers = async (automation, triggers, req) => {
188240
+ if (!triggers.some((t) => t.kind === "event")) return triggers;
188241
+ const cache = /* @__PURE__ */ new Map();
188242
+ const out = [];
188243
+ for (const t of triggers) {
188244
+ if (t.kind !== "event" || !t.event) {
188245
+ out.push(t);
188246
+ continue;
188247
+ }
188248
+ const pid = t.event.projectId;
188249
+ if (!cache.has(pid)) {
188250
+ cache.set(pid, await opts.projectAccess?.({ companyId: companyOf(req), projectId: pid, actor: req.auth.actor }) ?? null);
188251
+ }
188252
+ const project = cache.get(pid) ?? null;
188253
+ out.push({
188254
+ ...t,
188255
+ event: { ...t.event, projectAvailable: project !== null, projectName: project?.name ?? null }
188256
+ });
188257
+ }
188258
+ return out;
188259
+ };
187957
188260
  const summarize3 = async (automation, req) => {
187958
188261
  const [triggers, recent, canWrite] = await Promise.all([
187959
188262
  service.listTriggers(automation.id),
187960
188263
  service.listRuns(automation.id, { limit: RECENT_LIMIT2 }),
187961
188264
  canWriteOf(automation, req)
187962
188265
  ]);
187963
- return buildAutomationSummary(automation, triggers, recent, canWrite);
188266
+ const decorated = await decorateEventTriggers(automation, triggers, req);
188267
+ return {
188268
+ ...buildAutomationSummary(automation, decorated, recent, canWrite),
188269
+ // 能力位服务端算(ADR 0163 §10):前端只消费不推断,否则前后端两套规则必漂移。
188270
+ capabilities: { eventTrigger: service.eventTriggerCapability(automation) }
188271
+ };
188272
+ };
188273
+ const assertEventTriggerWritable = async (mode, cfg, existing, req) => {
188274
+ if (!service.eventTriggerCapability({ executionMode: mode }).supported) {
188275
+ throw new ApiError(400, "EVENT_TRIGGER_UNSUPPORTED", "\u8BE5\u6267\u884C\u65B9\u5F0F\u6682\u4E0D\u652F\u6301\u8BA2\u9605\u89E6\u53D1\uFF08\u5BF9\u8BDD\u6267\u884C\u7F3A\u6301\u4E45\u5E42\u7B49\u952E\uFF09");
188276
+ }
188277
+ const project = await opts.projectAccess?.({ companyId: companyOf(req), projectId: cfg.projectId, actor: req.auth.actor });
188278
+ if (!project) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u9879\u76EE\uFF1A${cfg.projectId}`);
188279
+ for (const other of existing) {
188280
+ if (other.projectId !== cfg.projectId) continue;
188281
+ const dup = cfg.events.find((e) => other.events.includes(e));
188282
+ if (dup) throw new ApiError(409, "DUPLICATE_SUBSCRIPTION", `\u8BE5\u9879\u76EE\u7684\u8FD9\u4E2A\u4E8B\u4EF6\u5DF2\u8BA2\u9605\uFF1A${dup}`);
188283
+ }
188284
+ };
188285
+ const assertEventTriggerInputs = async (mode, inputs, kept, req) => {
188286
+ const seen = [...kept];
188287
+ for (const input of inputs ?? []) {
188288
+ if (input.kind !== "event" || !input.event) continue;
188289
+ await assertEventTriggerWritable(mode, input.event, seen, req);
188290
+ seen.push(input.event);
188291
+ }
188292
+ };
188293
+ const assertModeKeepsEventTriggers = async (automation, effMode) => {
188294
+ if (service.eventTriggerCapability({ ...automation, executionMode: effMode }).supported) return;
188295
+ const hasEvent = (await service.listTriggers(automation.id)).some((t) => t.kind === "event");
188296
+ if (hasEvent) {
188297
+ throw new ApiError(400, "EVENT_TRIGGER_UNSUPPORTED", "\u8FD9\u6761\u81EA\u52A8\u5316\u6709\u8BA2\u9605\u89E6\u53D1\u5668\uFF0C\u6539\u6210\u5BF9\u8BDD\u6267\u884C\u4F1A\u5931\u53BB\u6301\u4E45\u5E42\u7B49\u4FDD\u8BC1\u2014\u2014\u8BF7\u5148\u5220\u9664\u8BA2\u9605\u89E6\u53D1\u5668");
188298
+ }
187964
188299
  };
187965
188300
  return (router) => {
187966
188301
  router.get("/api/automations/templates", async () => {
@@ -188027,6 +188362,7 @@ function automationsDomain(opts) {
188027
188362
  assertTitleTemplate(b2.titleTemplate);
188028
188363
  const subscribers = assertActorIds(b2.subscribers, "subscribers");
188029
188364
  const triggers = validateTriggers(b2.triggers);
188365
+ await assertEventTriggerInputs(mode, triggers, [], req);
188030
188366
  const created = await service.createAutomation({
188031
188367
  companyId: companyOf(req),
188032
188368
  name: b2.name,
@@ -188060,6 +188396,9 @@ function automationsDomain(opts) {
188060
188396
  assertTitleTemplate(b2.titleTemplate);
188061
188397
  const subscribers = assertActorIds(b2.subscribers, "subscribers");
188062
188398
  const triggers = validateTriggers(b2.triggers);
188399
+ const kept = triggers !== void 0 ? [] : (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.event).map((t) => t.event);
188400
+ await assertEventTriggerInputs(effMode, triggers, kept, req);
188401
+ if (triggers === void 0) await assertModeKeepsEventTriggers(automation, effMode);
188063
188402
  const updated = await service.updateAutomation(
188064
188403
  automation.id,
188065
188404
  {
@@ -188108,6 +188447,8 @@ function automationsDomain(opts) {
188108
188447
  const automation = await mustGet(req);
188109
188448
  await requireWrite(automation, req);
188110
188449
  const inputs = validateTriggers([req.body]) ?? [];
188450
+ const kept = (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.event).map((t) => t.event);
188451
+ await assertEventTriggerInputs(automation.executionMode, inputs, kept, req);
188111
188452
  const trigger = await service.addTrigger(automation.id, inputs[0], req.auth.actor);
188112
188453
  return { status: 201, body: trigger };
188113
188454
  });
@@ -188137,6 +188478,17 @@ function automationsDomain(opts) {
188137
188478
  const eventFilters = validateEventFilters(b2.webhook.eventFilters);
188138
188479
  if (eventFilters !== void 0) patch.webhook = { eventFilters };
188139
188480
  }
188481
+ if (b2.event !== void 0) {
188482
+ if (existing.kind !== "event") throw new ApiError(400, "BAD_REQUEST", "\u975E event \u89E6\u53D1\u5668\u4E0D\u63A5\u53D7 event \u5B57\u6BB5");
188483
+ const raw = b2.event;
188484
+ if (raw["projectId"] !== void 0 || raw["source"] !== void 0) {
188485
+ throw new ApiError(400, "BAD_REQUEST", "\u8BA2\u9605\u7684\u9879\u76EE\u4E0E\u4E8B\u4EF6\u6E90\u4E0D\u53EF\u4FEE\u6539\u2014\u2014\u8BF7\u5220\u9664\u8FD9\u6761\u89E6\u53D1\u5668\u540E\u91CD\u65B0\u6DFB\u52A0");
188486
+ }
188487
+ const events = normalizeEventNames(raw["events"]);
188488
+ const kept = (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.id !== existing.id && t.event).map((t) => t.event);
188489
+ await assertEventTriggerWritable(automation.executionMode, { projectId: existing.event.projectId, events }, kept, req);
188490
+ patch.event = { events };
188491
+ }
188140
188492
  const updated = await service.updateTrigger(existing.id, patch, req.auth.actor);
188141
188493
  return { status: 200, body: updated };
188142
188494
  });
@@ -188218,6 +188570,7 @@ var RECENT_LIMIT2;
188218
188570
  var init_routes9 = __esm({
188219
188571
  "../server/src/domains/automations/routes.ts"() {
188220
188572
  "use strict";
188573
+ init_src2();
188221
188574
  init_templates();
188222
188575
  init_router();
188223
188576
  init_service8();
@@ -188230,17 +188583,25 @@ var init_routes9 = __esm({
188230
188583
  });
188231
188584
 
188232
188585
  // ../server/src/domains/automations/workorder-state.ts
188233
- function makeAutomationWorkorderStateResolver(kernel) {
188586
+ function makeAutomationWorkorderSnapshotResolver(kernel) {
188234
188587
  return async (workspace) => {
188235
188588
  const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === workspace);
188236
- if (arts.length === 0) return "unknown";
188589
+ if (arts.length === 0) return { state: "unknown", members: [], rootArtifactId: null };
188237
188590
  const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
188238
- if (lifecycleOf(kernel.model, seed.id) === "sealed:cancelled") return "cancelled";
188591
+ const members = arts.map((a) => ({ artifactId: String(a.id), currentRevisionId: a.currentRev ? String(a.currentRev) : null })).sort((x2, y) => x2.artifactId < y.artifactId ? -1 : x2.artifactId > y.artifactId ? 1 : 0);
188592
+ const rootArtifactId = String(seed.id);
188593
+ if (lifecycleOf(kernel.model, seed.id) === "sealed:cancelled") {
188594
+ return { state: "cancelled", members, rootArtifactId };
188595
+ }
188239
188596
  const stage = deriveStage(arts.map((a) => nodeInfo(kernel.model, a)));
188240
- if (stage === "concluded" || stage === "sealed") return "done";
188241
- return "active";
188597
+ if (stage === "concluded" || stage === "sealed") return { state: "done", members, rootArtifactId };
188598
+ return { state: "active", members, rootArtifactId };
188242
188599
  };
188243
188600
  }
188601
+ function makeAutomationWorkorderStateResolver(kernel) {
188602
+ const snapshot = makeAutomationWorkorderSnapshotResolver(kernel);
188603
+ return async (workspace) => (await snapshot(workspace)).state;
188604
+ }
188244
188605
  function makeAutomationReplyResolver(kernel, blobs) {
188245
188606
  return async (workspace) => {
188246
188607
  const model = kernel.model;
@@ -188304,7 +188665,12 @@ function createAutomationsDomain(opts) {
188304
188665
  return {
188305
188666
  service,
188306
188667
  webhook,
188307
- register: automationsDomain({ service, webhook, ...opts.isAdmin ? { isAdmin: opts.isAdmin } : {} })
188668
+ register: automationsDomain({
188669
+ service,
188670
+ webhook,
188671
+ ...opts.isAdmin ? { isAdmin: opts.isAdmin } : {},
188672
+ ...opts.projectAccess ? { projectAccess: opts.projectAccess } : {}
188673
+ })
188308
188674
  };
188309
188675
  }
188310
188676
  var init_automations = __esm({
@@ -188596,6 +188962,210 @@ var init_playbooks = __esm({
188596
188962
  }
188597
188963
  });
188598
188964
 
188965
+ // ../server/src/automations/project-event-poller.ts
188966
+ function completionFingerprint(workspace, members) {
188967
+ const canonical = JSON.stringify({
188968
+ workspace,
188969
+ members: [...members].sort((a, b2) => a.artifactId < b2.artifactId ? -1 : a.artifactId > b2.artifactId ? 1 : 0).map((m2) => ({ artifactId: m2.artifactId, currentRevisionId: m2.currentRevisionId ?? null }))
188970
+ });
188971
+ return (0, import_node_crypto39.createHash)("sha256").update(canonical).digest("hex");
188972
+ }
188973
+ function eventConsumptionKey(args) {
188974
+ const raw = `project-event:${EVENT_KEY_VERSION}:${args.triggerId}:${args.event}:${args.workspace}:${args.fingerprint}`;
188975
+ return raw.length <= 200 ? raw : `project-event:${EVENT_KEY_VERSION}:sha256:${(0, import_node_crypto39.createHash)("sha256").update(raw).digest("hex")}`;
188976
+ }
188977
+ var import_node_crypto39, EVENT_KEY_VERSION, EMPTY_METRICS, AUTOMATION_EVENT_HANDLERS, ProjectEventPoller;
188978
+ var init_project_event_poller = __esm({
188979
+ "../server/src/automations/project-event-poller.ts"() {
188980
+ "use strict";
188981
+ import_node_crypto39 = require("node:crypto");
188982
+ EVENT_KEY_VERSION = "v1";
188983
+ EMPTY_METRICS = () => ({
188984
+ scanDurationMs: 0,
188985
+ projectsScanned: 0,
188986
+ workordersScanned: 0,
188987
+ newEvents: 0,
188988
+ duplicateEvents: 0,
188989
+ skippedEvents: 0,
188990
+ baselineEvents: 0,
188991
+ pollErrors: 0,
188992
+ oldestPendingAgeMs: null
188993
+ });
188994
+ AUTOMATION_EVENT_HANDLERS = {
188995
+ // 完成判据必须来自 D3 resolver(snapshot.state),不在这里复制 deriveStage。
188996
+ // active / unknown 是「还没完成」,cancelled 永不产生 completed——四态里只有 done 触发。
188997
+ "workorder.completed": {
188998
+ matches: (snapshot) => snapshot.state === "done",
188999
+ fingerprint: (workspace, snapshot) => completionFingerprint(workspace, snapshot.members)
189000
+ }
189001
+ };
189002
+ ProjectEventPoller = class {
189003
+ deps;
189004
+ now;
189005
+ log;
189006
+ batchLimit;
189007
+ inFlight = false;
189008
+ lastMetrics = EMPTY_METRICS();
189009
+ constructor(deps) {
189010
+ this.deps = deps;
189011
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
189012
+ if (deps.log) this.log = deps.log;
189013
+ this.batchLimit = deps.batchLimit ?? 200;
189014
+ }
189015
+ metrics() {
189016
+ return { ...this.lastMetrics };
189017
+ }
189018
+ /** 一轮扫描。单实例用 inFlight 防重入;多实例无需选主——并发由数据库唯一键裁决。 */
189019
+ async tick() {
189020
+ if (this.inFlight) return this.lastMetrics;
189021
+ this.inFlight = true;
189022
+ const startedAt = this.now().getTime();
189023
+ const m2 = EMPTY_METRICS();
189024
+ try {
189025
+ const triggers = await this.deps.store.listEventTriggers();
189026
+ if (triggers.length === 0) return this.lastMetrics = { ...m2, scanDurationMs: this.now().getTime() - startedAt };
189027
+ const snapshots2 = /* @__PURE__ */ new Map();
189028
+ let budget = this.batchLimit;
189029
+ for (const trigger of triggers) {
189030
+ if (budget <= 0) break;
189031
+ const config2 = trigger.event;
189032
+ if (!config2?.projectId || config2.events.length === 0) continue;
189033
+ const automation = await this.deps.store.getAutomation(trigger.automationId);
189034
+ if (!automation) continue;
189035
+ const groupKey = `${automation.companyId}\0${config2.projectId}`;
189036
+ let group = snapshots2.get(groupKey);
189037
+ if (!group) {
189038
+ try {
189039
+ group = await this.scanProject(automation.companyId, config2.projectId);
189040
+ m2.projectsScanned += 1;
189041
+ m2.workordersScanned += group.length;
189042
+ } catch (e) {
189043
+ m2.pollErrors += 1;
189044
+ this.log?.(`[project-event-poller] \u626B\u63CF\u9879\u76EE\u5931\u8D25 ${config2.projectId}\uFF1A${String(e)}`);
189045
+ group = [];
189046
+ }
189047
+ snapshots2.set(groupKey, group);
189048
+ }
189049
+ try {
189050
+ budget -= await this.evaluateTrigger(automation, trigger, group, m2, budget);
189051
+ } catch (e) {
189052
+ m2.pollErrors += 1;
189053
+ this.log?.(`[project-event-poller] \u89E6\u53D1\u5668\u6C42\u503C\u5931\u8D25 ${trigger.id}\uFF1A${String(e)}`);
189054
+ }
189055
+ }
189056
+ m2.oldestPendingAgeMs = await this.oldestPendingAge();
189057
+ } catch (e) {
189058
+ m2.pollErrors += 1;
189059
+ this.log?.(`[project-event-poller] tick \u5931\u8D25\uFF1A${String(e)}`);
189060
+ } finally {
189061
+ m2.scanDurationMs = this.now().getTime() - startedAt;
189062
+ this.lastMetrics = m2;
189063
+ this.inFlight = false;
189064
+ }
189065
+ if (m2.newEvents || m2.skippedEvents || m2.baselineEvents || m2.pollErrors) {
189066
+ this.log?.(
189067
+ `[project-event-poller] \u9879\u76EE ${m2.projectsScanned} / \u5DE5\u5355 ${m2.workordersScanned} \u2192 \u65B0\u4E8B\u4EF6 ${m2.newEvents}\u3001\u91CD\u590D ${m2.duplicateEvents}\u3001\u8DF3\u8FC7 ${m2.skippedEvents}\u3001\u57FA\u7EBF ${m2.baselineEvents}\u3001\u9519\u8BEF ${m2.pollErrors}\uFF08${m2.scanDurationMs}ms\uFF09`
189068
+ );
189069
+ }
189070
+ return this.lastMetrics;
189071
+ }
189072
+ async scanProject(companyId, projectId) {
189073
+ const workspaces = await this.deps.listProjectWorkspaces({ companyId, projectId });
189074
+ const out = [];
189075
+ for (const workspace of workspaces) {
189076
+ out.push({ workspace, snapshot: await this.deps.workorderSnapshot({ companyId, workspace }) });
189077
+ }
189078
+ return out;
189079
+ }
189080
+ /** 返回本触发器消耗掉的预算条数。 */
189081
+ async evaluateTrigger(automation, trigger, candidates, m2, budget) {
189082
+ const config2 = trigger.event;
189083
+ const baselining = config2.baselineState !== "ready";
189084
+ let used = 0;
189085
+ for (const event of config2.events) {
189086
+ const handler = AUTOMATION_EVENT_HANDLERS[event];
189087
+ if (!handler) continue;
189088
+ for (const { workspace, snapshot } of candidates) {
189089
+ if (!handler.matches(snapshot)) continue;
189090
+ if (used >= budget) return used;
189091
+ const fingerprint = handler.fingerprint(workspace, snapshot);
189092
+ const key = eventConsumptionKey({ triggerId: trigger.id, event, workspace, fingerprint });
189093
+ const envelope = {
189094
+ event,
189095
+ companyId: automation.companyId,
189096
+ projectId: config2.projectId,
189097
+ workspace,
189098
+ rootArtifactId: snapshot.rootArtifactId,
189099
+ completionFingerprint: fingerprint,
189100
+ observedAt: this.now().toISOString(),
189101
+ ...await this.originAutomationOf(workspace)
189102
+ };
189103
+ used += 1;
189104
+ if (baselining) {
189105
+ const { created: created2 } = await this.deps.service.claimEventRun(
189106
+ automation,
189107
+ trigger,
189108
+ key,
189109
+ envelope,
189110
+ "skipped",
189111
+ "subscription_baseline",
189112
+ "\u8BA2\u9605\u57FA\u7EBF\u5BF9\u9F50\uFF1A\u8BA2\u9605\u751F\u6548\u524D\u5DF2\u5B8C\u6210\uFF0C\u4E0D\u56DE\u653E\u5386\u53F2"
189113
+ );
189114
+ if (created2) m2.baselineEvents += 1;
189115
+ else m2.duplicateEvents += 1;
189116
+ continue;
189117
+ }
189118
+ const { run, created } = await this.deps.service.claimEventRun(automation, trigger, key, envelope);
189119
+ if (!created) {
189120
+ m2.duplicateEvents += 1;
189121
+ continue;
189122
+ }
189123
+ const denial = await this.admit(automation, trigger, envelope);
189124
+ if (denial) {
189125
+ await this.deps.service.skipEventRun(run, denial.reasonCode, denial.error);
189126
+ m2.skippedEvents += 1;
189127
+ continue;
189128
+ }
189129
+ await this.deps.service.dispatchEventRun(automation, trigger, run);
189130
+ m2.newEvents += 1;
189131
+ }
189132
+ }
189133
+ if (baselining) {
189134
+ await this.deps.store.completeEventBaseline(trigger.id, this.now().toISOString());
189135
+ }
189136
+ return used;
189137
+ }
189138
+ /** 权限复查 + 自环准入。两条都只在**占位成功之后**做,因为拒绝也要留痕在同一把键上。 */
189139
+ async admit(automation, trigger, envelope) {
189140
+ if (envelope.originAutomationId && envelope.originAutomationId === automation.id) {
189141
+ return { reasonCode: "self_trigger_blocked", error: "\u81EA\u89E6\u53D1\u5DF2\u963B\u65AD\uFF1A\u8FD9\u4E2A\u5DE5\u5355\u662F\u672C\u81EA\u52A8\u5316\u81EA\u5DF1\u5EFA\u7684" };
189142
+ }
189143
+ const accountable = trigger.publishedBy ?? (await this.deps.store.latestRuleVersion(automation.id))?.publishedBy ?? automation.createdBy ?? null;
189144
+ const readable = await this.deps.projectReadable({
189145
+ companyId: automation.companyId,
189146
+ projectId: envelope.projectId,
189147
+ actor: accountable
189148
+ });
189149
+ if (!readable) {
189150
+ return { reasonCode: "project_access_revoked", error: `\u8BA2\u9605\u9879\u76EE\u5DF2\u4E0D\u53EF\u8BBF\u95EE\uFF1A${envelope.projectId}` };
189151
+ }
189152
+ return null;
189153
+ }
189154
+ async originAutomationOf(workspace) {
189155
+ const origin = await this.deps.store.findRunBySpawnedWorkspace(workspace).catch(() => null);
189156
+ return origin ? { originAutomationId: origin.automationId } : {};
189157
+ }
189158
+ async oldestPendingAge() {
189159
+ const unsettled = await this.deps.store.listUnsettledRuns(50).catch(() => []);
189160
+ const pending = unsettled.filter((r) => r.source === "event" && r.status === "pending");
189161
+ if (pending.length === 0) return null;
189162
+ const oldest = Math.min(...pending.map((r) => Date.parse(r.triggeredAt)));
189163
+ return this.now().getTime() - oldest;
189164
+ }
189165
+ };
189166
+ }
189167
+ });
189168
+
188599
189169
  // ../server/src/daemon-adapter.ts
188600
189170
  function graceFromEnv(fallback) {
188601
189171
  const v2 = process.env["OASIS_NODE_LOST_GRACE_MS"];
@@ -188623,11 +189193,11 @@ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
188623
189193
  entry.appendWaiters.push({ resolve: resolve10, timer });
188624
189194
  });
188625
189195
  }
188626
- var import_node_crypto39, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
189196
+ var import_node_crypto40, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
188627
189197
  var init_daemon_adapter = __esm({
188628
189198
  "../server/src/daemon-adapter.ts"() {
188629
189199
  "use strict";
188630
- import_node_crypto39 = require("node:crypto");
189200
+ import_node_crypto40 = require("node:crypto");
188631
189201
  STASH_TTL_MS = 5 * 6e4;
188632
189202
  STASH_MAX_FRAMES_PER_ID = 500;
188633
189203
  STASH_MAX_IDS = 50;
@@ -188823,7 +189393,7 @@ var init_daemon_adapter = __esm({
188823
189393
  async spawn(job) {
188824
189394
  const nodeId = job.binding?.nodeId;
188825
189395
  if (!nodeId) throw new Error("DaemonHubAdapter \u9700\u8981 job.binding.nodeId\uFF08\u8DEF\u7531\u9519\u8BEF\uFF09");
188826
- const dispatchId = job.dispatchId ?? `dispatch:${(0, import_node_crypto39.randomUUID)()}`;
189396
+ const dispatchId = job.dispatchId ?? `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
188827
189397
  const entry = {
188828
189398
  nodeId,
188829
189399
  ...job.binding?.runtimeKind ? { runtimeKind: job.binding.runtimeKind } : {},
@@ -188979,7 +189549,7 @@ var init_daemon_adapter = __esm({
188979
189549
  }));
188980
189550
  }
188981
189551
  request(nodeId, buildFrame) {
188982
- const requestId = (0, import_node_crypto39.randomUUID)();
189552
+ const requestId = (0, import_node_crypto40.randomUUID)();
188983
189553
  const sent = this.hub.dispatch(nodeId, buildFrame(requestId));
188984
189554
  if (!sent) return Promise.resolve({ ok: false, code: "NODE_OFFLINE" });
188985
189555
  return new Promise((resolve10) => {
@@ -189087,13 +189657,13 @@ function classifyPage(page, lastPushedHash, serviceAccount) {
189087
189657
  if (sha(body) === lastPushedHash) return { kind: "echo" };
189088
189658
  return { kind: "human-edit", content: body, updatedBy: page.updatedBy };
189089
189659
  }
189090
- var import_node_crypto40, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
189660
+ var import_node_crypto41, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
189091
189661
  var init_engine = __esm({
189092
189662
  "../server/src/mirror/engine.ts"() {
189093
189663
  "use strict";
189094
- import_node_crypto40 = require("node:crypto");
189664
+ import_node_crypto41 = require("node:crypto");
189095
189665
  init_src2();
189096
- sha = (s2) => (0, import_node_crypto40.createHash)("sha256").update(s2, "utf8").digest("hex");
189666
+ sha = (s2) => (0, import_node_crypto41.createHash)("sha256").update(s2, "utf8").digest("hex");
189097
189667
  enc3 = (s2) => new TextEncoder().encode(s2);
189098
189668
  dec = (b2) => new TextDecoder().decode(b2);
189099
189669
  MirrorEngine = class {
@@ -189300,11 +189870,11 @@ function humanExitCause(exit) {
189300
189870
  const base = (exit.reason ? label[exit.reason] : void 0) ?? `\u4F1A\u8BDD\u5F02\u5E38\u7ED3\u675F\uFF08${exit.reason ?? "\u65E0\u9000\u51FA\u4FE1\u606F"}\uFF09`;
189301
189871
  return exit.errorMessage ? `${base}\uFF1A${exit.errorMessage.slice(0, 200)}` : base;
189302
189872
  }
189303
- var import_node_crypto41, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_ESCALATION_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
189873
+ var import_node_crypto42, AUTONOMOUS_ETHOS, CONVERSATIONAL_ETHOS, SHARED_GRAPH_BODY, AUTONOMOUS_RECOVERY_TOOLS, HIGH_RISK_COMMANDS, COORDINATOR_SYSTEM_PROMPT, CONVERSATIONAL_RECOVERY_TOOLS, CONVERSATIONAL_ESCALATION_TOOLS, CONVERSATIONAL_MANAGER_BODY, MACHINE_EXIT_CODES, CoordinatorWorker;
189304
189874
  var init_worker = __esm({
189305
189875
  "../server/src/coordinator/worker.ts"() {
189306
189876
  "use strict";
189307
- import_node_crypto41 = require("node:crypto");
189877
+ import_node_crypto42 = require("node:crypto");
189308
189878
  init_src5();
189309
189879
  init_identity();
189310
189880
  AUTONOMOUS_ETHOS = `\u4F60\u662F\u8FD9\u4E2A\u5DE5\u5355\u7684**\u7BA1\u7406\u8005**\u2014\u2014\u804C\u8D23\u662F\u8BA9\u5B83\u987A\u7545\u8DD1\u5B8C\u3002\u7CFB\u7EDF\u5728\u67D0\u4E2A\u8282\u70B9\u5361\u4F4F\u3001\u673A\u68B0\u5206\u8BCA\u786E\u8BA4"\u9700\u8981\u4F60"\u65F6\u5524\u8D77\u4F60\uFF08\u65E0\u4EBA\u5728\u573A\uFF0C\u4F60\u8FD9\u4E00\u8F6E\u628A\u80FD\u505A\u7684\u505A\u6389\uFF09\u3002\u4F60\u7684\u624B\u6BB5\u5F88\u5BBD\uFF1A
@@ -189551,7 +190121,7 @@ ${HIGH_RISK_COMMANDS}`;
189551
190121
  return this.deps.kernel.model.lastSeq.get(artifactId) ?? 0;
189552
190122
  }
189553
190123
  evaluationKey(kind, artifactId, evidence) {
189554
- const fingerprint = (0, import_node_crypto41.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
190124
+ const fingerprint = (0, import_node_crypto42.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
189555
190125
  return `${kind}:${artifactId}:${fingerprint}`;
189556
190126
  }
189557
190127
  artifactEvidence(id) {
@@ -189928,8 +190498,8 @@ ${ctx.nodeFault}
189928
190498
  this.deps.log?.(`[coordinator] ${workspace} \u5168\u5C40\u4E0A\u4E0B\u6587\u8BFB\u53D6\u5931\u8D25\uFF0C\u7EE7\u7EED\u6CBF\u7528\u539F\u534F\u8C03\u8005\u4EFB\u52A1\uFF1A${String(error2)}`);
189929
190499
  }
189930
190500
  }
189931
- const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto41.randomUUID)();
189932
- const coordinatorRunId = `coordinate:${(0, import_node_crypto41.randomUUID)()}`;
190501
+ const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto42.randomUUID)();
190502
+ const coordinatorRunId = `coordinate:${(0, import_node_crypto42.randomUUID)()}`;
189933
190503
  const taskWithContext = coordinatorContext ? [
189934
190504
  task,
189935
190505
  ``,
@@ -189970,7 +190540,7 @@ ${ctx.nodeFault}
189970
190540
  const startedAtMs = Date.now();
189971
190541
  let trajectoryStarted = false;
189972
190542
  if (this.deps.trajectory) {
189973
- const bundleManifest = Object.fromEntries(Object.entries(job.bundle.files).map(([name, content]) => [name, (0, import_node_crypto41.createHash)("sha256").update(content).digest("hex")]));
190543
+ const bundleManifest = Object.fromEntries(Object.entries(job.bundle.files).map(([name, content]) => [name, (0, import_node_crypto42.createHash)("sha256").update(content).digest("hex")]));
189974
190544
  const session = {
189975
190545
  runId: coordinatorRunId,
189976
190546
  runtimeSessionId,
@@ -190371,6 +190941,7 @@ var init_src10 = __esm({
190371
190941
  init_playbooks();
190372
190942
  init_overrides();
190373
190943
  init_scheduler();
190944
+ init_project_event_poller();
190374
190945
  init_daemon_adapter();
190375
190946
  init_dispatch_fence();
190376
190947
  init_node_health();
@@ -190391,11 +190962,11 @@ var init_src10 = __esm({
190391
190962
  });
190392
190963
 
190393
190964
  // ../storage/src/postgres.ts
190394
- var import_node_crypto42, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
190965
+ var import_node_crypto43, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
190395
190966
  var init_postgres = __esm({
190396
190967
  "../storage/src/postgres.ts"() {
190397
190968
  "use strict";
190398
- import_node_crypto42 = require("node:crypto");
190969
+ import_node_crypto43 = require("node:crypto");
190399
190970
  init_esm();
190400
190971
  init_src2();
190401
190972
  ident3 = (s2) => {
@@ -190574,7 +191145,7 @@ var init_postgres = __esm({
190574
191145
  return new _PostgresBlobStore(pool, schema);
190575
191146
  }
190576
191147
  async put(bytes) {
190577
- const hash = (0, import_node_crypto42.createHash)("sha256").update(bytes).digest("hex");
191148
+ const hash = (0, import_node_crypto43.createHash)("sha256").update(bytes).digest("hex");
190578
191149
  await this.pool.query(
190579
191150
  `INSERT INTO ${this.t} (hash, bytes, size, content_type) VALUES ($1, $2, $3, $4) ON CONFLICT (hash) DO NOTHING`,
190580
191151
  [hash, Buffer.from(bytes), bytes.byteLength, sniffContentType(bytes) ?? null]
@@ -194843,6 +195414,14 @@ var init_postgres_trace = __esm({
194843
195414
  });
194844
195415
 
194845
195416
  // ../storage/src/postgres-automations.ts
195417
+ function storedEventConfig(t) {
195418
+ return {
195419
+ source: "project",
195420
+ projectId: t.event?.projectId ?? "",
195421
+ events: t.event?.events ?? [],
195422
+ baselineAt: null
195423
+ };
195424
+ }
194846
195425
  function buildTrigger(automationId, input) {
194847
195426
  const t = {
194848
195427
  id: genId("trig"),
@@ -194863,6 +195442,10 @@ function buildTrigger(automationId, input) {
194863
195442
  hasSigningSecret: false
194864
195443
  };
194865
195444
  }
195445
+ if (input.kind === "event" && input.event) {
195446
+ t.enabled = false;
195447
+ t.event = { source: "project", projectId: input.event.projectId, events: [...input.event.events], baselineState: "aligning" };
195448
+ }
194866
195449
  return t;
194867
195450
  }
194868
195451
  function genWebhookToken2() {
@@ -195166,7 +195749,8 @@ var init_postgres_automations = __esm({
195166
195749
  t.schedule?.claimedAt ?? null,
195167
195750
  t.webhook?.token ?? null,
195168
195751
  t.webhook?.provider ?? null,
195169
- t.webhook?.eventFilters != null ? JSON.stringify(t.webhook.eventFilters) : null
195752
+ // event_filters 一列两用(ADR 0163 §3.2:不新增列):webhook 存过滤数组,event 存订阅配置对象。
195753
+ t.kind === "event" ? JSON.stringify(storedEventConfig(t)) : t.webhook?.eventFilters != null ? JSON.stringify(t.webhook.eventFilters) : null
195170
195754
  ]
195171
195755
  );
195172
195756
  }
@@ -195212,6 +195796,10 @@ var init_postgres_automations = __esm({
195212
195796
  if (patch.webhook && "eventFilters" in patch.webhook) {
195213
195797
  push("event_filters", patch.webhook.eventFilters != null ? JSON.stringify(patch.webhook.eventFilters) : null, "::jsonb");
195214
195798
  }
195799
+ if (patch.event?.events) {
195800
+ args.push(JSON.stringify(patch.event.events));
195801
+ sets.push(`event_filters=jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{events}', $${args.length}::jsonb, true)`);
195802
+ }
195215
195803
  if (sets.length === 0) return await this.getTrigger(triggerId);
195216
195804
  args.push(triggerId);
195217
195805
  const r = await this.pool.query(
@@ -195498,6 +196086,42 @@ var init_postgres_automations = __esm({
195498
196086
  );
195499
196087
  return r.rows.map(rowToTrigger);
195500
196088
  }
196089
+ /* ---------- event 触发器(ADR 0163) ---------- */
196090
+ async listEventTriggers() {
196091
+ const r = await this.pool.query(
196092
+ `SELECT t.* FROM ${this.s}.automation_triggers t
196093
+ JOIN ${this.s}.automations a ON a.id = t.automation_id
196094
+ WHERE t.kind = 'event' AND a.enabled AND a.archived_at IS NULL
196095
+ ORDER BY t.id`
196096
+ );
196097
+ return r.rows.map(rowToTrigger);
196098
+ }
196099
+ async completeEventBaseline(triggerId, at) {
196100
+ await this.pool.query(
196101
+ `UPDATE ${this.s}.automation_triggers
196102
+ SET event_filters = jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{baselineAt}', to_jsonb($2::text), true),
196103
+ enabled = true
196104
+ WHERE id = $1 AND kind = 'event'`,
196105
+ [triggerId, at]
196106
+ );
196107
+ }
196108
+ async resetEventBaseline(triggerId) {
196109
+ await this.pool.query(
196110
+ `UPDATE ${this.s}.automation_triggers
196111
+ SET event_filters = jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{baselineAt}', 'null'::jsonb, true),
196112
+ enabled = false
196113
+ WHERE id = $1 AND kind = 'event'`,
196114
+ [triggerId]
196115
+ );
196116
+ }
196117
+ async findRunBySpawnedWorkspace(workspace) {
196118
+ const r = await this.pool.query(
196119
+ `SELECT * FROM ${this.s}.automation_runs WHERE spawned_workspace_id = $1
196120
+ ORDER BY triggered_at ASC, id ASC LIMIT 1`,
196121
+ [workspace]
196122
+ );
196123
+ return r.rows[0] ? rowToRun2(r.rows[0]) : null;
196124
+ }
195501
196125
  };
195502
196126
  rowToAutomation = (row) => ({
195503
196127
  id: row.id,
@@ -195546,6 +196170,15 @@ var init_postgres_automations = __esm({
195546
196170
  hasSigningSecret: row.signing_secret != null
195547
196171
  };
195548
196172
  }
196173
+ if (t.kind === "event") {
196174
+ const raw = row.event_filters ?? null;
196175
+ t.event = {
196176
+ source: "project",
196177
+ projectId: raw?.projectId ?? "",
196178
+ events: raw?.events ?? [],
196179
+ baselineState: raw?.baselineAt ? "ready" : "aligning"
196180
+ };
196181
+ }
195549
196182
  return t;
195550
196183
  };
195551
196184
  rowToRun2 = (row) => ({
@@ -195909,11 +196542,11 @@ var init_postgres_chat_sessions = __esm({
195909
196542
  });
195910
196543
 
195911
196544
  // ../storage/src/postgres-nodes.ts
195912
- var import_node_crypto43, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
196545
+ var import_node_crypto44, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
195913
196546
  var init_postgres_nodes = __esm({
195914
196547
  "../storage/src/postgres-nodes.ts"() {
195915
196548
  "use strict";
195916
- import_node_crypto43 = require("node:crypto");
196549
+ import_node_crypto44 = require("node:crypto");
195917
196550
  init_esm();
195918
196551
  ident12 = (s2) => {
195919
196552
  if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
@@ -196062,7 +196695,7 @@ var init_postgres_nodes = __esm({
196062
196695
  return store;
196063
196696
  }
196064
196697
  issue(nodeId) {
196065
- const token = `ont_${(0, import_node_crypto43.randomBytes)(24).toString("base64url")}`;
196698
+ const token = `ont_${(0, import_node_crypto44.randomBytes)(24).toString("base64url")}`;
196066
196699
  this.cache.set(token, nodeId);
196067
196700
  void this.pool.query(`INSERT INTO ${this.s}.node_tokens (token,node_id) VALUES ($1,$2)`, [token, nodeId]);
196068
196701
  return token;
@@ -196274,11 +196907,11 @@ var init_postgres_type_registry = __esm({
196274
196907
  });
196275
196908
 
196276
196909
  // ../storage/src/postgres-actor-memory.ts
196277
- var import_node_crypto44, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
196910
+ var import_node_crypto45, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
196278
196911
  var init_postgres_actor_memory = __esm({
196279
196912
  "../storage/src/postgres-actor-memory.ts"() {
196280
196913
  "use strict";
196281
- import_node_crypto44 = require("node:crypto");
196914
+ import_node_crypto45 = require("node:crypto");
196282
196915
  init_src2();
196283
196916
  matchClause = (q) => q.requireMatch && q.keywords.length > 0 ? " WHERE m > 0" : "";
196284
196917
  ident15 = (s2) => {
@@ -196483,7 +197116,7 @@ var init_postgres_actor_memory = __esm({
196483
197116
  }
196484
197117
  async write(input, now) {
196485
197118
  if (input.memId === void 0) {
196486
- const memId = `mem:${(0, import_node_crypto44.randomUUID)()}`;
197119
+ const memId = `mem:${(0, import_node_crypto45.randomUUID)()}`;
196487
197120
  const r2 = await this.pool.query(
196488
197121
  `INSERT INTO ${this.s}.actor_memories
196489
197122
  (mem_id, actor_id, project_id, keywords, content, version, created_at, updated_at, accessed_at, source_artifact_id, source_session_id)
@@ -196996,19 +197629,20 @@ var fs38 = __toESM(require("node:fs"));
196996
197629
  var os13 = __toESM(require("node:os"));
196997
197630
  var path32 = __toESM(require("node:path"));
196998
197631
  var import_node_child_process19 = require("node:child_process");
197632
+ var import_node_crypto52 = require("node:crypto");
196999
197633
 
197000
197634
  // ../cli/src/cli.ts
197001
197635
  var fs34 = __toESM(require("node:fs"), 1);
197002
197636
  var os11 = __toESM(require("node:os"), 1);
197003
197637
  var path28 = __toESM(require("node:path"), 1);
197004
- var import_node_crypto49 = require("node:crypto");
197638
+ var import_node_crypto50 = require("node:crypto");
197005
197639
 
197006
197640
  // ../cli/src/serve.ts
197007
197641
  var fs30 = __toESM(require("node:fs"), 1);
197008
197642
  var os7 = __toESM(require("node:os"), 1);
197009
197643
  var path24 = __toESM(require("node:path"), 1);
197010
197644
  var import_node_child_process15 = require("node:child_process");
197011
- var import_node_crypto45 = require("node:crypto");
197645
+ var import_node_crypto46 = require("node:crypto");
197012
197646
  var import_node_url7 = require("node:url");
197013
197647
  init_src5();
197014
197648
  init_src10();
@@ -198131,7 +198765,7 @@ async function startServe(opts) {
198131
198765
  // dispatchChat 收流、done 后落助手消息并回报最终回复。会话在控制台 chat 页可回看。
198132
198766
  runChat: async ({ agentId, humanActorId, title, prompt }, onDone) => {
198133
198767
  if (!automationDispatchChat) throw new Error("chat \u6D3E\u53D1\u5C1A\u672A\u5C31\u7EEA\uFF08serve \u542F\u52A8\u4E2D\uFF09");
198134
- const chatSessionId = (0, import_node_crypto45.randomUUID)();
198768
+ const chatSessionId = (0, import_node_crypto46.randomUUID)();
198135
198769
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
198136
198770
  await chatSessionStore.createSession({
198137
198771
  id: chatSessionId,
@@ -198143,7 +198777,7 @@ async function startServe(opts) {
198143
198777
  touchedAt: nowIso,
198144
198778
  createdAt: nowIso
198145
198779
  }).catch(() => void 0);
198146
- await chatSessionStore.appendMessage({ id: (0, import_node_crypto45.randomUUID)(), sessionId: chatSessionId, role: "user", content: prompt, createdAt: nowIso }).catch(() => void 0);
198780
+ await chatSessionStore.appendMessage({ id: (0, import_node_crypto46.randomUUID)(), sessionId: chatSessionId, role: "user", content: prompt, createdAt: nowIso }).catch(() => void 0);
198147
198781
  const session = await automationDispatchChat({ actorId: agentId, message: prompt, chatSessionId });
198148
198782
  let buf = "";
198149
198783
  session.onOutput((c) => {
@@ -198152,7 +198786,7 @@ async function startServe(opts) {
198152
198786
  void session.done.then(async () => {
198153
198787
  if (buf || session.runId) {
198154
198788
  await chatSessionStore.appendMessage({
198155
- id: (0, import_node_crypto45.randomUUID)(),
198789
+ id: (0, import_node_crypto46.randomUUID)(),
198156
198790
  sessionId: chatSessionId,
198157
198791
  role: "assistant",
198158
198792
  content: buf,
@@ -198166,6 +198800,10 @@ async function startServe(opts) {
198166
198800
  });
198167
198801
  return { chatSessionId };
198168
198802
  },
198803
+ // 订阅触发 × chat 的 fail-closed 开关(ADR 0163 §4.4)。当前 runChat 每次 randomUUID 建会话、
198804
+ // 没有任何持久去重:占位后崩溃重试会多开一个会话。**在 chat 适配器补上持久幂等键之前保持 false**,
198805
+ // 由此 event+chat 在配置期就被拒(能力位 supported=false),而不是上线后偶发重复开会话。
198806
+ chatSupportsIdempotency: false,
198169
198807
  // chat 准入(≈ multica AgentReadiness):在岗 + 有 active runtime 绑定,注定失败的触发别去开会话。
198170
198808
  agentReady: async (agentId) => {
198171
198809
  const actor = await registryStore.getActor(agentId);
@@ -198183,6 +198821,13 @@ async function startServe(opts) {
198183
198821
  const member = await controlPlaneStore.getMember(companyId, actor);
198184
198822
  return member?.role === "owner" || member?.role === "admin";
198185
198823
  },
198824
+ // 订阅项目可读判据(ADR 0163 §5.2):复用 projects 域的可见性口径,automation 侧不复制成员规则。
198825
+ // 现状:projects 域对登录成员不做逐项目读限制,故「可读」= 项目存在;项目被删/查不到 → null → 404。
198826
+ // 哪天 projects 加了成员级可见性,改这一个闭包即可,automation 侧不动。
198827
+ projectAccess: async ({ projectId }) => {
198828
+ const project = await projectStateStore.getProject(projectId);
198829
+ return project ? { id: project.id, name: project.name } : null;
198830
+ },
198186
198831
  log: (m2) => console.log(m2)
198187
198832
  });
198188
198833
  const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, pgSchema) : await FileControlPlaneStore.open(path24.join(opts.dir, "control-plane.json"));
@@ -198604,7 +199249,7 @@ async function startServe(opts) {
198604
199249
  }));
198605
199250
  }
198606
199251
  const limits = { wallClockMs: wallClockForKind("planner") };
198607
- const artifactId = `artifact:planner:${(0, import_node_crypto45.randomUUID)()}`;
199252
+ const artifactId = `artifact:planner:${(0, import_node_crypto46.randomUUID)()}`;
198608
199253
  const handle = await chatRemoteAdapter.spawn({
198609
199254
  actor: planner.id,
198610
199255
  // companyId 同协调者:planner 用的就是默认公司的 actors 服务,漏签会让它一调公司域端点就 404。
@@ -199021,10 +199666,10 @@ async function startServe(opts) {
199021
199666
  { nodeId: priorChatSession?.runtimeId, runtimeKind: priorChatSession?.runtimeKind },
199022
199667
  { nodeId: binding.nodeId, runtimeKind: binding.runtimeKind }
199023
199668
  ) : false;
199024
- const runtimeSessionId = sessionId ?? (0, import_node_crypto45.randomUUID)();
199669
+ const runtimeSessionId = sessionId ?? (0, import_node_crypto46.randomUUID)();
199025
199670
  const resumeRuntimeSession = Boolean(sessionId);
199026
- const traceRunId = `chat-run:${(0, import_node_crypto45.randomUUID)()}`;
199027
- const artifactId = `artifact:chat:${(0, import_node_crypto45.randomUUID)()}`;
199671
+ const traceRunId = `chat-run:${(0, import_node_crypto46.randomUUID)()}`;
199672
+ const artifactId = `artifact:chat:${(0, import_node_crypto46.randomUUID)()}`;
199028
199673
  const traceStartedAt = (/* @__PURE__ */ new Date()).toISOString();
199029
199674
  let lastProgressTouchMs = 0;
199030
199675
  const PROGRESS_TOUCH_THROTTLE_MS2 = 2e4;
@@ -199677,7 +200322,7 @@ async function startServe(opts) {
199677
200322
  console.log(`[new-engine] dispatchWork ${workId} \u662F human work\u2014\u2014\u4E0D\u6D3E runtime\uFF0C\u7B49\u5F85\u4EBA\u5DE5\u5904\u7406`);
199678
200323
  return;
199679
200324
  }
199680
- const ledgerId = `dispatch:${(0, import_node_crypto45.randomUUID)()}`;
200325
+ const ledgerId = `dispatch:${(0, import_node_crypto46.randomUUID)()}`;
199681
200326
  try {
199682
200327
  await dispatchLedger.insertOpen({
199683
200328
  id: ledgerId,
@@ -199753,7 +200398,7 @@ async function startServe(opts) {
199753
200398
  const kernelModel = newKernel.model;
199754
200399
  const art = kernelModel.artifacts.get(nodeId);
199755
200400
  const revWid = art?.workspace ?? "";
199756
- const ledgerId = `dispatch:${(0, import_node_crypto45.randomUUID)()}`;
200401
+ const ledgerId = `dispatch:${(0, import_node_crypto46.randomUUID)()}`;
199757
200402
  try {
199758
200403
  await dispatchLedger.insertOpen({
199759
200404
  id: ledgerId,
@@ -200875,6 +201520,31 @@ ${st.detail}
200875
201520
  const automationTimer = setInterval(() => {
200876
201521
  void automationScheduler.tick().catch((err) => console.error(`[automation-scheduler] tick \u5931\u8D25: ${String(err)}`));
200877
201522
  }, 3e4);
201523
+ const eventPollMs = Number(process.env["OASIS_AUTOMATION_EVENT_POLL_MS"] ?? 1e4);
201524
+ let automationEventTimer;
201525
+ if (Number.isFinite(eventPollMs) && eventPollMs > 0) {
201526
+ const eventPoller = new ProjectEventPoller({
201527
+ store: automationStore,
201528
+ service: automations.service,
201529
+ log: (m2) => console.log(m2),
201530
+ batchLimit: envNum("OASIS_AUTOMATION_EVENT_BATCH", 200),
201531
+ // 工单→项目绑定是合法薄旁存(B5),不是内核事实——列项目下的工单只认它。
201532
+ listProjectWorkspaces: async ({ projectId }) => (await artifactStateStore.listWorkspaceBindings()).filter((b2) => b2.projectId === projectId).map((b2) => b2.workOrderId),
201533
+ // 完成判据必须复用 D3 resolver(不复制 deriveStage),且与成员快照同一次读取。
201534
+ workorderSnapshot: async ({ companyId, workspace }) => {
201535
+ const engine2 = await getEngine(companyId);
201536
+ return makeAutomationWorkorderSnapshotResolver(engine2.kernel)(workspace);
201537
+ },
201538
+ // 触发时复查归因主体的项目读取权(与配置期同一口径)。
201539
+ projectReadable: async ({ projectId }) => await projectStateStore.getProject(projectId) !== null
201540
+ });
201541
+ automationEventTimer = setInterval(() => {
201542
+ void eventPoller.tick().catch((err) => console.error(`[project-event-poller] tick \u5931\u8D25: ${String(err)}`));
201543
+ }, eventPollMs);
201544
+ console.log(`[serve] \u9879\u76EE\u4E8B\u4EF6\u8BA2\u9605\u8F6E\u8BE2\u5668\u5DF2\u542F\u7528\uFF08\u6BCF ${Math.round(eventPollMs / 1e3)}s \u4E00\u8F6E\uFF1BOASIS_AUTOMATION_EVENT_POLL_MS=0 \u53EF\u5173\uFF09`);
201545
+ } else {
201546
+ console.log("[serve] \u9879\u76EE\u4E8B\u4EF6\u8BA2\u9605\u8F6E\u8BE2\u5668\u5DF2\u88AB OASIS_AUTOMATION_EVENT_POLL_MS \u663E\u5F0F\u5173\u95ED");
201547
+ }
200878
201548
  let boardWatchTimer;
200879
201549
  const boardWatchFlag = (process.env["OASIS_BOARD_WATCH"] ?? "").trim().toLowerCase();
200880
201550
  if (!["0", "false", "off"].includes(boardWatchFlag)) {
@@ -200900,6 +201570,7 @@ ${st.detail}
200900
201570
  if (orphanSweepTimer) clearInterval(orphanSweepTimer);
200901
201571
  if (zeroOutputBreakerTimer) clearInterval(zeroOutputBreakerTimer);
200902
201572
  clearInterval(automationTimer);
201573
+ if (automationEventTimer) clearInterval(automationEventTimer);
200903
201574
  if (boardWatchTimer) clearInterval(boardWatchTimer);
200904
201575
  clearInterval(serverHeartbeatTimer);
200905
201576
  hub?.close();
@@ -200967,7 +201638,7 @@ var SessionManager = class {
200967
201638
  // ../cli/src/daemon/ws-client.ts
200968
201639
  init_wrapper();
200969
201640
  var import_node_os9 = require("node:os");
200970
- var import_node_crypto46 = require("node:crypto");
201641
+ var import_node_crypto47 = require("node:crypto");
200971
201642
  init_src8();
200972
201643
  init_src7();
200973
201644
 
@@ -201592,7 +202263,7 @@ var DaemonWsClient = class {
201592
202263
  */
201593
202264
  async queryArtifactStates(artifactIds, timeoutMs = 15e3) {
201594
202265
  if (artifactIds.length === 0) return {};
201595
- const requestId = (0, import_node_crypto46.randomUUID)();
202266
+ const requestId = (0, import_node_crypto47.randomUUID)();
201596
202267
  return new Promise((resolve10, reject) => {
201597
202268
  const timer = setTimeout(() => {
201598
202269
  this.gcPending.delete(requestId);
@@ -201929,7 +202600,7 @@ function forwardSignal(child, sig, killGroup = (pgid, signal) => {
201929
202600
  // ../cli/src/daemon/machine-id.ts
201930
202601
  var import_node_child_process17 = require("node:child_process");
201931
202602
  var import_node_fs21 = require("node:fs");
201932
- var import_node_crypto47 = require("node:crypto");
202603
+ var import_node_crypto48 = require("node:crypto");
201933
202604
  var import_node_os12 = require("node:os");
201934
202605
  function linuxMachineId() {
201935
202606
  for (const p2 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
@@ -201995,7 +202666,7 @@ var defaultSources = {
201995
202666
  function resolveNodeId(sources = {}) {
201996
202667
  const s2 = { ...defaultSources, ...sources };
201997
202668
  const material = `${s2.machineFingerprint()}:${s2.osUser()}`;
201998
- const digest = (0, import_node_crypto47.createHash)("sha256").update(material).digest("hex").slice(0, 12);
202669
+ const digest = (0, import_node_crypto48.createHash)("sha256").update(material).digest("hex").slice(0, 12);
201999
202670
  return `node-${digest}`;
202000
202671
  }
202001
202672
 
@@ -202329,17 +203000,22 @@ var COMMAND_DECLS = {
202329
203000
  },
202330
203001
  "automation-trigger-add": {
202331
203002
  group: "\u81EA\u52A8\u5316",
202332
- usage: "oasis automation-trigger-add <id> --kind schedule|webhook [--cron <expr> --tz <tz>] [--provider generic|github] [--label <\u540D>]",
202333
- description: "\u7ED9\u81EA\u52A8\u5316\u8FFD\u52A0\u4E00\u6761\u89E6\u53D1\u5668\uFF08\u4E00\u6761\u81EA\u52A8\u5316\u53EF\u6302\u591A\u4E2A\uFF09\u3002",
203003
+ usage: "oasis automation-trigger-add <id> --kind schedule|webhook|event [--cron <expr> --tz <tz>] [--provider generic|github] [--project <projectId> --event workorder.completed] [--label <\u540D>]",
203004
+ description: "\u7ED9\u81EA\u52A8\u5316\u8FFD\u52A0\u4E00\u6761\u89E6\u53D1\u5668\uFF08\u4E00\u6761\u81EA\u52A8\u5316\u53EF\u6302\u591A\u4E2A\uFF09\u3002event=\u8BA2\u9605\u9879\u76EE\uFF1A\u8BE5\u9879\u76EE\u4E0B\u5DE5\u5355\u5B8C\u6210\u65F6\u89E6\u53D1\u4E00\u6B21\uFF1B\u65B0\u5EFA\u540E\u5148\u8DD1\u4E00\u6B21\u57FA\u7EBF\u5BF9\u9F50\uFF08\u4E0D\u56DE\u653E\u5386\u53F2\uFF09\uFF0C\u5BF9\u9F50\u5B8C\u81EA\u52A8\u542F\u7528\u3002",
202334
203005
  positional: [{ name: "<id>", required: true, desc: "\u81EA\u52A8\u5316 id" }],
202335
203006
  flags: [
202336
- { name: "kind", desc: "schedule|webhook\uFF08\u9ED8\u8BA4 schedule\uFF09" },
203007
+ { name: "kind", desc: "schedule|webhook|event\uFF08\u9ED8\u8BA4 schedule\uFF09" },
202337
203008
  { name: "cron", desc: "5 \u5B57\u6BB5 cron\uFF08schedule \u5FC5\u586B\uFF09" },
202338
203009
  { name: "tz", desc: "IANA \u65F6\u533A\uFF08\u9ED8\u8BA4 UTC\uFF09" },
202339
203010
  { name: "provider", desc: "webhook provider\uFF1Ageneric|github" },
203011
+ { name: "project", desc: "\u8BA2\u9605\u7684\u9879\u76EE id\uFF08event \u5FC5\u586B\uFF1Bschedule/webhook \u4E0D\u63A5\u53D7\uFF09" },
203012
+ { name: "event", desc: "\u8BA2\u9605\u7684\u4E8B\u4EF6\u540D\uFF08event \u5FC5\u586B\uFF0C\u53EF\u91CD\u590D\u6216\u9017\u53F7\u5206\u9694\uFF1B\u672C\u671F\u53EA\u6709 workorder.completed\uFF09" },
202340
203013
  { name: "label", desc: "\u663E\u793A\u540D" }
202341
203014
  ],
202342
- examples: ['oasis automation-trigger-add auto_abc123 --kind schedule --cron "0 18 * * 5" --tz Asia/Shanghai']
203015
+ examples: [
203016
+ 'oasis automation-trigger-add auto_abc123 --kind schedule --cron "0 18 * * 5" --tz Asia/Shanghai',
203017
+ "oasis automation-trigger-add auto_abc123 --kind event --project oasis --event workorder.completed"
203018
+ ]
202343
203019
  },
202344
203020
  "automation-trigger-update": {
202345
203021
  group: "\u81EA\u52A8\u5316",
@@ -203607,13 +204283,13 @@ function ownFlagsFromDecl(command) {
203607
204283
  }
203608
204284
 
203609
204285
  // ../cli/src/connector-effect-runner.ts
203610
- var import_node_crypto48 = require("node:crypto");
204286
+ var import_node_crypto49 = require("node:crypto");
203611
204287
  var import_node_child_process18 = require("node:child_process");
203612
204288
  init_src8();
203613
204289
  function stableKey(effect) {
203614
204290
  const keyFields = Object.fromEntries(Object.entries(effect.keyFields).sort(([a], [b2]) => a.localeCompare(b2)));
203615
204291
  const canonical = JSON.stringify({ kind: effect.kind, target: effect.target, keyFields });
203616
- return `sha256:${(0, import_node_crypto48.createHash)("sha256").update(canonical).digest("hex")}`;
204292
+ return `sha256:${(0, import_node_crypto49.createHash)("sha256").update(canonical).digest("hex")}`;
203617
204293
  }
203618
204294
  async function runConnectorEffect(connector, toolArgs, deps) {
203619
204295
  const effect = connector.describeExternalEffect?.(toolArgs) ?? null;
@@ -204208,7 +204884,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
204208
204884
  const store = createNodeTokenStore(path28.join(dir, "node-tokens.json"));
204209
204885
  const sub = positional[0];
204210
204886
  if (sub === "issue") {
204211
- const id = flags.get("id") ?? `node-${(0, import_node_crypto49.randomUUID)()}`;
204887
+ const id = flags.get("id") ?? `node-${(0, import_node_crypto50.randomUUID)()}`;
204212
204888
  const token2 = store.issue(id);
204213
204889
  println(token2);
204214
204890
  process.stderr.write(
@@ -204821,10 +205497,36 @@ ${p2.ref} ${p2.name}`);
204821
205497
  break;
204822
205498
  }
204823
205499
  case "automation-trigger-add": {
204824
- const id = needPos(positional, 0, "oasis automation-trigger-add <id> --kind schedule|webhook ...");
205500
+ const id = needPos(positional, 0, "oasis automation-trigger-add <id> --kind schedule|webhook|event ...");
204825
205501
  const kind = flags.get("kind") ?? "schedule";
204826
- const body = kind === "schedule" ? { kind, schedule: { cron: need(flags, "cron"), tz: flags.get("tz") ?? "UTC" }, ...flags.has("label") ? { label: flags.get("label") } : {} } : { kind, webhook: { provider: flags.get("provider") ?? "generic" }, ...flags.has("label") ? { label: flags.get("label") } : {} };
204827
- println(JSON.stringify(await api.post(`/api/automations/${encodeURIComponent(id)}/triggers`, body), null, 2));
205502
+ if (kind !== "schedule" && kind !== "webhook" && kind !== "event") {
205503
+ throw new Error(`--kind \u53EA\u80FD\u662F schedule\u3001webhook \u6216 event\uFF08\u6536\u5230 ${kind}\uFF09`);
205504
+ }
205505
+ if (kind !== "event" && (flags.has("project") || flags.has("event"))) {
205506
+ throw new Error(`--project / --event \u53EA\u7528\u4E8E --kind event\uFF08\u5F53\u524D --kind ${kind}\uFF09`);
205507
+ }
205508
+ if (kind === "event" && (flags.has("cron") || flags.has("tz") || flags.has("provider"))) {
205509
+ throw new Error("--kind event \u4E0D\u63A5\u53D7 --cron / --tz / --provider");
205510
+ }
205511
+ const label = flags.has("label") ? { label: flags.get("label") } : {};
205512
+ let body;
205513
+ if (kind === "schedule") {
205514
+ body = { kind, schedule: { cron: need(flags, "cron"), tz: flags.get("tz") ?? "UTC" }, ...label };
205515
+ } else if (kind === "webhook") {
205516
+ body = { kind, webhook: { provider: flags.get("provider") ?? "generic" }, ...label };
205517
+ } else {
205518
+ const events = (flags.get("event") ?? "").split(",").map((x2) => x2.trim()).filter(Boolean);
205519
+ if (!events.length) throw new Error("--kind event \u9700\u8981\u81F3\u5C11\u4E00\u4E2A --event\uFF08\u5982 --event workorder.completed\uFF09");
205520
+ body = { kind, event: { source: "project", projectId: need(flags, "project"), events }, ...label };
205521
+ }
205522
+ const created = await api.post(
205523
+ `/api/automations/${encodeURIComponent(id)}/triggers`,
205524
+ body
205525
+ );
205526
+ println(JSON.stringify(created, null, 2));
205527
+ if (kind === "event" && created.event?.baselineState === "aligning") {
205528
+ println("\u2139 \u8BA2\u9605\u5DF2\u5EFA\uFF0C\u6B63\u5728\u5BF9\u9F50\u57FA\u7EBF\uFF1A\u628A\u9879\u76EE\u91CC**\u5DF2\u5B8C\u6210**\u7684\u5DE5\u5355\u8BB0\u4E3A\u300C\u5DF2\u77E5\u300D\uFF0C\u5BF9\u9F50\u5B8C\u81EA\u52A8\u542F\u7528\u3002\u5386\u53F2\u5DE5\u5355\u4E0D\u4F1A\u88AB\u8865\u89E6\u53D1\u3002");
205529
+ }
204828
205530
  break;
204829
205531
  }
204830
205532
  case "automation-trigger-update": {
@@ -206673,12 +207375,12 @@ var LEGACY_SYSTEMD_UNIT = "oasis-node.service";
206673
207375
  var LEGACY_LAUNCHD_LABEL = "com.oasis.node";
206674
207376
 
206675
207377
  // src/install.ts
206676
- var import_node_crypto50 = require("node:crypto");
207378
+ var import_node_crypto51 = require("node:crypto");
206677
207379
  var fs37 = __toESM(require("node:fs"));
206678
207380
  var path31 = __toESM(require("node:path"));
206679
207381
  function versionLabel(sourceFile, pkgVersion, npmPrefixDir) {
206680
207382
  if (isFromNpmPrefix(sourceFile, npmPrefixDir)) return pkgVersion;
206681
- const digest = (0, import_node_crypto50.createHash)("sha256").update(fs37.readFileSync(sourceFile)).digest("hex").slice(0, 8);
207383
+ const digest = (0, import_node_crypto51.createHash)("sha256").update(fs37.readFileSync(sourceFile)).digest("hex").slice(0, 8);
206682
207384
  return `${pkgVersion}+local.${digest}`;
206683
207385
  }
206684
207386
  function isFromNpmPrefix(sourceFile, npmPrefixDir) {
@@ -206770,7 +207472,7 @@ function shimScript() {
206770
207472
  }
206771
207473
 
206772
207474
  // src/index.ts
206773
- var PKG_VERSION = true ? "0.1.117" : "dev";
207475
+ var PKG_VERSION = true ? "0.1.118" : "dev";
206774
207476
  var LOCAL_BIN = localBin();
206775
207477
  var NPM_PREFIX = npmPrefix();
206776
207478
  var INSTANCE = DEFAULT_INSTANCE;
@@ -206852,9 +207554,8 @@ function ensureRuntimeAssets(targetDir = BIN_DIR) {
206852
207554
  console.error(`[oasis] connector runtime assets: \u4ECE ${synced.from} \u540C\u6B65\u4E86 ${synced.copied} \u4E2A .sh \u5230 ${targetDir}`);
206853
207555
  }
206854
207556
  }
206855
- function installSoftware() {
207557
+ function installSoftware(label) {
206856
207558
  const src = process.argv[1];
206857
- const label = versionLabel(src, PKG_VERSION, NPM_PREFIX);
206858
207559
  const vDir = versionDir(label);
206859
207560
  installBinaryAtomic(src, path32.join(vDir, "oasis.js"));
206860
207561
  ensureRuntimeAssets(vDir);
@@ -206992,20 +207693,12 @@ function stopDaemon(name) {
206992
207693
  }
206993
207694
  }
206994
207695
  }
206995
- function selfUpdate(opts = {}) {
207696
+ function selfUpdate() {
206996
207697
  const cfg = readConfig();
206997
207698
  if (!cfg?.serverUrl) throw new Error("no config \u2014 run `oasis daemon start` first");
206998
- if (cfg.pinnedVersion && !opts.force) {
206999
- console.error(`[oasis] \u5B9E\u4F8B ${INSTANCE} \u5DF2\u9489\u5728 ${cfg.pinnedVersion}\uFF0C\u8DF3\u8FC7\u81EA\u66F4\u65B0\uFF08\u8981\u89E3\u9664\uFF1Aoasis daemon update --instance ${INSTANCE} --force\uFF09`);
207000
- return false;
207001
- }
207002
- if (cfg.pinnedVersion && opts.force) {
207003
- saveConfig({ ...cfg, pinnedVersion: void 0 });
207004
- console.error(`[oasis] --force\uFF1A\u5DF2\u89E3\u9664 ${INSTANCE} \u7684\u7248\u672C\u9489\u6B7B\uFF08\u539F ${cfg.pinnedVersion}\uFF09`);
207005
- }
207006
207699
  const nameArg = cfg.name ? ` --name '${cfg.name.replace(/'/g, "'\\''")}'` : "";
207007
207700
  const oasisBin = path32.join(NPM_PREFIX, "bin", "oasis");
207008
- const startCmd = `"${oasisBin}" daemon start --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
207701
+ const startCmd = `"${oasisBin}" daemon start --upgrade --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
207009
207702
  const OFFICIAL = "https://registry.npmjs.org/";
207010
207703
  const installOfficial = `npm install -g --prefer-online --registry '${OFFICIAL}' --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
207011
207704
  const installFallback = `npm install -g --prefer-online --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
@@ -207030,7 +207723,6 @@ function selfUpdate(opts = {}) {
207030
207723
  const child = (0, import_node_child_process19.spawn)("sh", ["-c", inner], { detached: true, stdio: ["ignore", log3, log3] });
207031
207724
  child.unref();
207032
207725
  }
207033
- return true;
207034
207726
  }
207035
207727
  function resolveInstance(explicit) {
207036
207728
  if (!explicit) return DEFAULT_INSTANCE;
@@ -207040,6 +207732,103 @@ function resolveInstance(explicit) {
207040
207732
  }
207041
207733
  return explicit;
207042
207734
  }
207735
+ function currentLabelOf(inst) {
207736
+ try {
207737
+ return path32.basename(fs38.realpathSync(inst.currentLink));
207738
+ } catch {
207739
+ return null;
207740
+ }
207741
+ }
207742
+ function instanceHasSoftware(inst) {
207743
+ return fs38.existsSync(inst.binFile) && currentLabelOf(inst) !== null;
207744
+ }
207745
+ function newInstanceName() {
207746
+ const existing = new Set(listInstances());
207747
+ if (!existing.has(DEFAULT_INSTANCE)) return DEFAULT_INSTANCE;
207748
+ for (; ; ) {
207749
+ const n = `inst-${(0, import_node_crypto52.randomBytes)(3).toString("hex")}`;
207750
+ if (!existing.has(n)) return n;
207751
+ }
207752
+ }
207753
+ function planStart(explicit, serverUrl) {
207754
+ if (explicit) {
207755
+ const name = resolveInstance(explicit);
207756
+ return { mode: fs38.existsSync(instancePaths(name).configFile) ? "resume" : "new", names: [name] };
207757
+ }
207758
+ const all = listInstances();
207759
+ if (all.length === 0) return { mode: "new", names: [DEFAULT_INSTANCE] };
207760
+ const want = normalizeServerUrl(serverUrl);
207761
+ const hits = all.filter((n) => {
207762
+ const c = readConfigAt(instancePaths(n).configFile);
207763
+ return !!c?.serverUrl && normalizeServerUrl(c.serverUrl) === want;
207764
+ });
207765
+ if (hits.length === 0) return { mode: "new", names: [newInstanceName()] };
207766
+ return { mode: "resume", names: hits };
207767
+ }
207768
+ async function startOneInstance(o) {
207769
+ const { serverUrl, upgrade, flags } = o;
207770
+ const prev = readConfig();
207771
+ if (prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
207772
+ console.error(`[oasis] \u26A0 \u5B9E\u4F8B "${INSTANCE}" \u539F\u672C\u8FDE ${prev.serverUrl}\uFF0C\u672C\u6B21\u5C06\u6539\u8FDE ${serverUrl}\u2014\u2014\u8FD9\u662F**\u8FC1\u79FB**\u4E0D\u662F\u65B0\u589E\u3002`);
207773
+ console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
207774
+ console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
207775
+ }
207776
+ const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
207777
+ let nodeId = serverChanged ? void 0 : prev?.nodeId;
207778
+ let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
207779
+ const enrollToken = o.mode === "new" ? flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"] : void 0;
207780
+ const derivedNodeId = derivedNodeIdFor(INSTANCE);
207781
+ if (enrollToken) {
207782
+ const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
207783
+ method: "POST",
207784
+ headers: { "content-type": "application/json" },
207785
+ body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
207786
+ });
207787
+ if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
207788
+ const body = await res.json();
207789
+ token = body.token;
207790
+ nodeId = body.nodeId;
207791
+ } else if (prev?.nodeId && prev?.token && !serverChanged) {
207792
+ nodeId = prev.nodeId;
207793
+ token = prev.token;
207794
+ }
207795
+ if (!token || !nodeId) {
207796
+ console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
207797
+ process.exit(1);
207798
+ }
207799
+ const name = o.applyName ? flags.get("name") ?? prev?.name : prev?.name;
207800
+ const nameChanged = prev?.name !== void 0 && name !== prev.name;
207801
+ saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
207802
+ const before = currentLabelOf(INST);
207803
+ let label = before;
207804
+ if (!instanceHasSoftware(INST) || upgrade) {
207805
+ const next = versionLabel(process.argv[1], PKG_VERSION, NPM_PREFIX);
207806
+ if (before && isLocalLabel(before) && before !== next) {
207807
+ console.log(` \u26A0 \u5373\u5C06\u7528 ${next} \u8986\u76D6\u672C\u5B9E\u4F8B\u6B63\u5728\u8DD1\u7684\u672C\u5730\u6784\u5EFA ${before}`);
207808
+ }
207809
+ label = installSoftware(next);
207810
+ } else {
207811
+ fs38.mkdirSync(path32.dirname(LOCAL_BIN), { recursive: true });
207812
+ fs38.writeFileSync(LOCAL_BIN, shimScript(), { mode: 493 });
207813
+ }
207814
+ const pidNow = readPid();
207815
+ const alive = !!pidNow && isRunning2(pidNow);
207816
+ if (alive && label === before && !serverChanged && !nameChanged) {
207817
+ console.log(`\u2713 ${INSTANCE}\uFF1A\u5DF2\u5728\u8DD1\uFF08PID ${pidNow}\uFF0C${label ?? "?"}\uFF09\uFF0C\u7248\u672C\u4E0E\u914D\u7F6E\u5747\u672A\u53D8\uFF0C\u672A\u91CD\u542F`);
207818
+ return;
207819
+ }
207820
+ stopDaemon(INSTANCE);
207821
+ const tookOver = setupAutostart(INSTANCE);
207822
+ const suffix = nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : "";
207823
+ if (!tookOver) {
207824
+ const pid = spawnDaemon();
207825
+ console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
207826
+ } else {
207827
+ console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
207828
+ }
207829
+ console.log(` instance: ${INSTANCE} nodeId: ${nodeId} version: ${label ?? "?"} config: ${INST.configFile}`);
207830
+ console.log(` logs: tail -f ${LOG_FILE}`);
207831
+ }
207043
207832
  function derivedNodeIdFor(name) {
207044
207833
  if (name === DEFAULT_INSTANCE) return resolveNodeId();
207045
207834
  const user = (() => {
@@ -207144,61 +207933,25 @@ void (async () => {
207144
207933
  return;
207145
207934
  }
207146
207935
  if (dsub === "start") {
207936
+ const explicit = flags.get("instance") || void 0;
207937
+ const upgrade = flags.has("upgrade");
207147
207938
  const serverUrlFlag = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"];
207148
- useInstance(resolveInstance(flags.get("instance")));
207149
- const prev = readConfig();
207150
- const serverUrl = serverUrlFlag ?? prev?.serverUrl;
207939
+ const probe = explicit ?? listInstances()[0] ?? DEFAULT_INSTANCE;
207940
+ const serverUrl = serverUrlFlag ?? readConfigAt(instancePaths(probe).configFile)?.serverUrl;
207151
207941
  if (!serverUrl) {
207152
- console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
207942
+ console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
207153
207943
  process.exit(1);
207154
207944
  }
207155
- if (!flags.get("instance") && prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
207156
- console.error(`[oasis] \u26A0 \u5B9E\u4F8B "${INSTANCE}" \u539F\u672C\u8FDE ${prev.serverUrl}\uFF0C\u672C\u6B21\u5C06\u6539\u8FDE ${serverUrl}\u2014\u2014\u8FD9\u662F**\u8FC1\u79FB**\u4E0D\u662F\u65B0\u589E\u3002`);
207157
- console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
207158
- console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
207159
- }
207160
- const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
207161
- let nodeId = serverChanged ? void 0 : prev?.nodeId;
207162
- let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
207163
- const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"];
207164
- const derivedNodeId = derivedNodeIdFor(INSTANCE);
207165
- if (enrollToken) {
207166
- const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
207167
- method: "POST",
207168
- headers: { "content-type": "application/json" },
207169
- body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
207170
- });
207171
- if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
207172
- const body = await res.json();
207173
- token = body.token;
207174
- nodeId = body.nodeId;
207175
- } else if (prev?.nodeId && prev?.token && !serverChanged) {
207176
- nodeId = prev.nodeId;
207177
- token = prev.token;
207178
- }
207179
- if (!token || !nodeId) {
207180
- console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
207181
- process.exit(1);
207945
+ const plan = planStart(explicit, serverUrl);
207946
+ if (plan.mode === "resume" && plan.names.length > 1) {
207947
+ console.log(`\u2192 \u672C\u673A\u6709 ${plan.names.length} \u4E2A\u5B9E\u4F8B\u8FDE\u7740\u8BE5\u670D\u52A1\u7AEF\uFF0C\u9010\u4E2A\u6062\u590D\uFF1A${plan.names.join(", ")}`);
207948
+ } else if (plan.mode === "new" && !explicit && listInstances().length > 0) {
207949
+ console.log(`\u2192 \u672C\u673A\u5DF2\u6709\u5B9E\u4F8B\uFF08${listInstances().join(", ")}\uFF09\uFF0C\u4F46\u90FD\u4E0D\u8FDE\u8BE5\u670D\u52A1\u7AEF\uFF1B\u65B0\u5EFA\u5B9E\u4F8B ${plan.names[0]}`);
207182
207950
  }
207183
- const name = flags.get("name") ?? prev?.name;
207184
- const nameChanged = prev?.name !== void 0 && name !== prev.name;
207185
- saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
207186
- const installedLabel = installSoftware();
207187
- if (isLocalLabel(installedLabel)) {
207188
- saveConfig({ ...readConfig(), pinnedVersion: installedLabel });
207189
- console.log(` \u672C\u5730\u6784\u5EFA \u2192 \u5DF2\u9489\u7248\u672C ${installedLabel}\uFF08\u81EA\u66F4\u65B0\u5BF9\u672C\u5B9E\u4F8B\u5931\u6548\uFF1B\u89E3\u9664\uFF1Aoasis daemon update --instance ${INSTANCE} --force\uFF09`);
207951
+ for (const name of plan.names) {
207952
+ useInstance(name);
207953
+ await startOneInstance({ mode: plan.mode, serverUrl, upgrade, flags, applyName: plan.names.length === 1 });
207190
207954
  }
207191
- stopDaemon(INSTANCE);
207192
- const tookOver = setupAutostart(INSTANCE);
207193
- const suffix = `${nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : ""}`;
207194
- if (!tookOver) {
207195
- const pid = spawnDaemon();
207196
- console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
207197
- } else {
207198
- console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
207199
- }
207200
- console.log(` instance: ${INSTANCE} nodeId: ${nodeId} config: ${INST.configFile}`);
207201
- console.log(` logs: tail -f ${LOG_FILE}`);
207202
207955
  if (!process.env["PATH"]?.includes(path32.dirname(LOCAL_BIN)))
207203
207956
  console.log(` add to PATH: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc`);
207204
207957
  return;
@@ -207226,13 +207979,13 @@ void (async () => {
207226
207979
  console.error("no config \u2014 run `oasis daemon start` first");
207227
207980
  process.exit(1);
207228
207981
  }
207229
- const forced = flags.has("force") || dargs.includes("--force");
207230
- console.log(`\u2192 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026${forced ? "\uFF08--force\uFF1A\u5FFD\u7565\u5E76\u89E3\u9664\u7248\u672C\u9489\u6B7B\uFF09" : ""}`);
207231
- if (selfUpdate({ force: forced })) {
207232
- console.log(`\u2713 \u66F4\u65B0\u5DF2\u89E6\u53D1\uFF08\u540E\u53F0\u62C9\u53D6 ${PKG_NAME}@latest \u5E76\u91CD\u542F\uFF09\uFF1B\u67E5\u770B\u65E5\u5FD7\uFF1Atail -f ${LOG_FILE}`);
207233
- } else {
207234
- console.log(`\uFF08\u672A\u89E6\u53D1\u66F4\u65B0\u2014\u2014\u672C\u5B9E\u4F8B\u5DF2\u9489\u7248\u672C\uFF09`);
207982
+ const cur = currentLabelOf(INST);
207983
+ if (cur && isLocalLabel(cur)) {
207984
+ console.log(` \u26A0 \u672C\u5B9E\u4F8B\u73B0\u5728\u8DD1\u7684\u662F\u672C\u5730\u6784\u5EFA ${cur}\uFF0C\u672C\u6B21\u66F4\u65B0\u4F1A\u7528 ${PKG_NAME}@latest \u8986\u76D6\u5B83\u3002`);
207235
207985
  }
207986
+ console.log(`\u2192 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026`);
207987
+ selfUpdate();
207988
+ console.log(`\u2713 \u66F4\u65B0\u5DF2\u89E6\u53D1\uFF08\u540E\u53F0\u62C9\u53D6 ${PKG_NAME}@latest \u5E76\u91CD\u542F\uFF09\uFF1B\u67E5\u770B\u65E5\u5FD7\uFF1Atail -f ${LOG_FILE}`);
207236
207989
  return;
207237
207990
  }
207238
207991
  if (dsub === "status") {