oasis_test 0.1.116 → 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 +992 -219
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1933,9 +1933,9 @@ function resolveNodeRef(ref2, candidates) {
1933
1933
  const hit = candidates.find((n) => n.id === r);
1934
1934
  return hit ? { ok: true, id: hit.id } : { ok: false, reason: "not_found" };
1935
1935
  }
1936
- const sep4 = r.indexOf(":");
1937
- const type = (sep4 >= 0 ? r.slice(0, sep4) : r).trim().toLowerCase();
1938
- const title = sep4 >= 0 ? r.slice(sep4 + 1).trim().toLowerCase() : null;
1936
+ const sep5 = r.indexOf(":");
1937
+ const type = (sep5 >= 0 ? r.slice(0, sep5) : r).trim().toLowerCase();
1938
+ const title = sep5 >= 0 ? r.slice(sep5 + 1).trim().toLowerCase() : null;
1939
1939
  const matches = candidates.filter((n) => n.type.toLowerCase() === type && (title === null || (n.title ?? "").toLowerCase() === title));
1940
1940
  if (matches.length === 0) return { ok: false, reason: "not_found" };
1941
1941
  if (matches.length === 1) return { ok: true, id: matches[0].id };
@@ -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
 
@@ -5726,8 +5728,8 @@ var init_dispatcher = __esm({
5726
5728
  /** 从 produce jobKey 解析 artifactId:`produce::<artifactId>` 或 `produce::<artifactId>::<part>`。 */
5727
5729
  artifactIdFromJobKey(jobKey) {
5728
5730
  const rest = jobKey.slice(jobKey.indexOf("::") + 2);
5729
- const sep4 = rest.indexOf("::");
5730
- return sep4 >= 0 ? rest.slice(0, sep4) : rest;
5731
+ const sep5 = rest.indexOf("::");
5732
+ return sep5 >= 0 ? rest.slice(0, sep5) : rest;
5731
5733
  }
5732
5734
  /**
5733
5735
  * 某节点的派发器运行时信号(stuckDiagnosis 的非 oplog 那半;proposal 协调者阻塞诊断 D1)。
@@ -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: createHash18 } = 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 = createHash18("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: createHash18 } = 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 = createHash18("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",
@@ -150077,9 +150109,9 @@ function agentEdgesFor(dependsOn, nodes, briefId, issues) {
150077
150109
  edges.add(briefId);
150078
150110
  continue;
150079
150111
  }
150080
- const sep4 = d.indexOf(":");
150081
- const depType = (sep4 >= 0 ? d.slice(0, sep4) : d).trim().toLowerCase();
150082
- const depTitle = sep4 >= 0 ? d.slice(sep4 + 1).trim().toLowerCase() : null;
150112
+ const sep5 = d.indexOf(":");
150113
+ const depType = (sep5 >= 0 ? d.slice(0, sep5) : d).trim().toLowerCase();
150114
+ const depTitle = sep5 >= 0 ? d.slice(sep5 + 1).trim().toLowerCase() : null;
150083
150115
  const match = nodes.find((n) => n.type === depType && (depTitle === null || (n.title ?? "").toLowerCase() === depTitle));
150084
150116
  if (match) edges.add(match.id);
150085
150117
  else issues.push({ code: "planner_invalid", severity: "warning", message: `\u8282\u70B9\u4F9D\u8D56\u300C${dep}\u300D\u627E\u4E0D\u5230\u5BF9\u5E94\u4E0A\u6E38\uFF08\u6309 type:title \u5339\u914D\uFF09\uFF0C\u5DF2\u5FFD\u7565\u8FD9\u6761\u8FB9\u3002` });
@@ -153848,17 +153880,17 @@ function stageConnectorWrapper(connector) {
153848
153880
  const cacheKey = `${commandName} ${scriptPath}`;
153849
153881
  const cached3 = stagedBinDirs.get(cacheKey);
153850
153882
  if (cached3 && (0, import_node_fs4.existsSync)((0, import_node_path4.join)(cached3, commandName))) return (0, import_node_path4.join)(cached3, commandName);
153851
- const binDir2 = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os.tmpdir)(), `oasis-conn-${connector.config.slug}-`));
153852
- const link = (0, import_node_path4.join)(binDir2, commandName);
153883
+ const binDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os.tmpdir)(), `oasis-conn-${connector.config.slug}-`));
153884
+ const link = (0, import_node_path4.join)(binDir, commandName);
153853
153885
  (0, import_node_fs4.symlinkSync)(scriptPath, link);
153854
153886
  if (!(0, import_node_fs4.existsSync)(link)) {
153855
153887
  try {
153856
- (0, import_node_fs4.rmSync)(binDir2, { recursive: true, force: true });
153888
+ (0, import_node_fs4.rmSync)(binDir, { recursive: true, force: true });
153857
153889
  } catch {
153858
153890
  }
153859
153891
  throw new Error(`connector ${connector.config.slug}: \u8F6F\u94FE\u5EFA\u5B8C\u4ECD\u4E0D\u53EF\u89E3\u6790\uFF08${link} \u2192 ${scriptPath}\uFF09`);
153860
153892
  }
153861
- stagedBinDirs.set(cacheKey, binDir2);
153893
+ stagedBinDirs.set(cacheKey, binDir);
153862
153894
  return link;
153863
153895
  }
153864
153896
  function isStagedWrapperUsable(link) {
@@ -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
  };
@@ -166463,10 +166541,10 @@ function createTokenIssuer(opts = {}) {
166463
166541
  if (revoked.has(token)) return null;
166464
166542
  if (!token.startsWith(SESSION_TOKEN_PREFIX)) return null;
166465
166543
  const encoded = token.slice(SESSION_TOKEN_PREFIX.length);
166466
- const sep4 = encoded.indexOf(".");
166467
- if (sep4 <= 0) return null;
166468
- const payload = encoded.slice(0, sep4);
166469
- const actualSig = encoded.slice(sep4 + 1);
166544
+ const sep5 = encoded.indexOf(".");
166545
+ if (sep5 <= 0) return null;
166546
+ const payload = encoded.slice(0, sep5);
166547
+ const actualSig = encoded.slice(sep5 + 1);
166470
166548
  const expectedSig = sign(secret, payload);
166471
166549
  if (!signatureMatches(actualSig, expectedSig)) return null;
166472
166550
  let claims;
@@ -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();
@@ -197452,8 +198086,8 @@ exec ${JSON.stringify(tsx)} ${JSON.stringify(main)} "$@"
197452
198086
  return wrapper;
197453
198087
  }
197454
198088
  function splitIdentityFiles3(prompt) {
197455
- const sep4 = /<!-- FILE: (.+?) -->\n?/g;
197456
- const parts = prompt.split(sep4);
198089
+ const sep5 = /<!-- FILE: (.+?) -->\n?/g;
198090
+ const parts = prompt.split(sep5);
197457
198091
  if (parts.length === 1) return { "identity/AGENTS.md": prompt };
197458
198092
  const out = {};
197459
198093
  for (let i = 1; i < parts.length; i += 2) {
@@ -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": {
@@ -206558,8 +207260,8 @@ function sanitizeDaemonPath(rawPath) {
206558
207260
  return !SESSION_SCOPED_PREFIXES.some((p2) => base.startsWith(p2));
206559
207261
  }).join(":");
206560
207262
  }
206561
- function installRuntimeAssets(srcRoot, binDir2) {
206562
- if (path29.resolve(srcRoot) === path29.resolve(binDir2)) return 0;
207263
+ function installRuntimeAssets(srcRoot, binDir) {
207264
+ if (path29.resolve(srcRoot) === path29.resolve(binDir)) return 0;
206563
207265
  let copied = 0;
206564
207266
  let entries;
206565
207267
  try {
@@ -206577,7 +207279,7 @@ function installRuntimeAssets(srcRoot, binDir2) {
206577
207279
  continue;
206578
207280
  }
206579
207281
  if (files.length === 0) continue;
206580
- const destDir = path29.join(binDir2, entry.name);
207282
+ const destDir = path29.join(binDir, entry.name);
206581
207283
  fs35.mkdirSync(destDir, { recursive: true });
206582
207284
  for (const f2 of files) {
206583
207285
  const dest = path29.join(destDir, f2);
@@ -206588,10 +207290,10 @@ function installRuntimeAssets(srcRoot, binDir2) {
206588
207290
  }
206589
207291
  return copied;
206590
207292
  }
206591
- function syncRuntimeAssets(candidateRoots, binDir2) {
206592
- fs35.mkdirSync(binDir2, { recursive: true });
207293
+ function syncRuntimeAssets(candidateRoots, binDir) {
207294
+ fs35.mkdirSync(binDir, { recursive: true });
206593
207295
  for (const root of candidateRoots) {
206594
- const copied = installRuntimeAssets(root, binDir2);
207296
+ const copied = installRuntimeAssets(root, binDir);
206595
207297
  if (copied > 0) return { from: root, copied };
206596
207298
  }
206597
207299
  return null;
@@ -206604,12 +207306,9 @@ var path30 = __toESM(require("node:path"));
206604
207306
  var DEFAULT_INSTANCE = "default";
206605
207307
  var home = () => os12.homedir();
206606
207308
  var oasisDir = () => path30.join(home(), ".oasis");
206607
- var binDir = () => path30.join(oasisDir(), "bin");
206608
- var binFile = () => path30.join(binDir(), "oasis.js");
206609
- var npmPrefix = () => path30.join(oasisDir(), "npm-global");
206610
207309
  var versionsDir = () => path30.join(oasisDir(), "versions");
206611
- var versionDir = (v2) => path30.join(versionsDir(), v2);
206612
- var currentLink = () => path30.join(oasisDir(), "current");
207310
+ var versionDir = (label) => path30.join(versionsDir(), label);
207311
+ var npmPrefix = () => path30.join(oasisDir(), "npm-global");
206613
207312
  var localBin = () => path30.join(home(), ".local", "bin", "oasis");
206614
207313
  var instancesRoot = () => path30.join(oasisDir(), "instances");
206615
207314
  function isValidInstanceName(name) {
@@ -206625,7 +207324,11 @@ function instancePaths(name) {
206625
207324
  dir,
206626
207325
  configFile: path30.join(dir, "node-config.json"),
206627
207326
  pidFile: path30.join(dir, "node.pid"),
206628
- logFile: path30.join(dir, "node.log")
207327
+ logFile: path30.join(dir, "node.log"),
207328
+ binDir: path30.join(dir, "bin"),
207329
+ binFile: path30.join(dir, "bin", "oasis.js"),
207330
+ currentLink: path30.join(dir, "current"),
207331
+ sessionsDir: path30.join(dir, "sessions")
206629
207332
  };
206630
207333
  }
206631
207334
  function listInstances() {
@@ -206644,6 +207347,16 @@ function listInstances() {
206644
207347
  }
206645
207348
  return found;
206646
207349
  }
207350
+ function referencedVersionLabels() {
207351
+ const labels = /* @__PURE__ */ new Set();
207352
+ for (const name of listInstances()) {
207353
+ try {
207354
+ labels.add(path30.basename(fs36.realpathSync(instancePaths(name).currentLink)));
207355
+ } catch {
207356
+ }
207357
+ }
207358
+ return [...labels];
207359
+ }
206647
207360
  function normalizeServerUrl(raw) {
206648
207361
  try {
206649
207362
  const u = new URL(raw);
@@ -206656,24 +207369,26 @@ function normalizeServerUrl(raw) {
206656
207369
  return raw.trim().replace(/\/+$/, "").toLowerCase();
206657
207370
  }
206658
207371
  }
206659
- function hostSlug(raw) {
206660
- let host;
206661
- try {
206662
- host = new URL(raw).hostname;
206663
- } catch {
206664
- host = raw.replace(/^[a-z]+:\/\//i, "").split("/")[0] ?? raw;
206665
- }
206666
- const slug6 = host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
206667
- return slug6 || "node";
206668
- }
206669
207372
  var systemdUnitName = (name) => `oasis-node@${name}.service`;
206670
207373
  var launchdLabel = (name) => name === DEFAULT_INSTANCE ? "com.oasis.node" : `com.oasis.node.${name}`;
206671
207374
  var LEGACY_SYSTEMD_UNIT = "oasis-node.service";
206672
207375
  var LEGACY_LAUNCHD_LABEL = "com.oasis.node";
206673
207376
 
206674
207377
  // src/install.ts
207378
+ var import_node_crypto51 = require("node:crypto");
206675
207379
  var fs37 = __toESM(require("node:fs"));
206676
207380
  var path31 = __toESM(require("node:path"));
207381
+ function versionLabel(sourceFile, pkgVersion, npmPrefixDir) {
207382
+ if (isFromNpmPrefix(sourceFile, npmPrefixDir)) return pkgVersion;
207383
+ const digest = (0, import_node_crypto51.createHash)("sha256").update(fs37.readFileSync(sourceFile)).digest("hex").slice(0, 8);
207384
+ return `${pkgVersion}+local.${digest}`;
207385
+ }
207386
+ function isFromNpmPrefix(sourceFile, npmPrefixDir) {
207387
+ const src = realpathOrSelf(sourceFile);
207388
+ const root = realpathOrSelf(path31.join(npmPrefixDir, "lib", "node_modules"));
207389
+ return src.startsWith(root + path31.sep);
207390
+ }
207391
+ var isLocalLabel = (label) => label.includes("+local.");
206677
207392
  function installBinaryAtomic(sourceFile, targetPath) {
206678
207393
  if (realpathOrSelf(sourceFile) === realpathOrSelf(targetPath)) return;
206679
207394
  fs37.mkdirSync(path31.dirname(targetPath), { recursive: true });
@@ -206715,8 +207430,9 @@ function pointCurrentAt(linkPath, targetDir) {
206715
207430
  }
206716
207431
  }
206717
207432
  function gcVersions(versionsDirPath, opts) {
206718
- const keepRecent = opts.keepRecent ?? 2;
206719
- const minAgeMs = opts.minAgeMs ?? 24 * 60 * 60 * 1e3;
207433
+ const keep = new Set(opts.keep);
207434
+ const keepSpare = opts.keepSpare ?? 1;
207435
+ const minAgeMs = opts.minAgeMs ?? 60 * 60 * 1e3;
206720
207436
  const now = opts.now ?? Date.now();
206721
207437
  let entries;
206722
207438
  try {
@@ -206724,7 +207440,7 @@ function gcVersions(versionsDirPath, opts) {
206724
207440
  } catch {
206725
207441
  return [];
206726
207442
  }
206727
- const candidates = entries.filter((name) => name !== opts.keep && !name.includes(".tmp.")).map((name) => {
207443
+ const candidates = entries.filter((name) => !keep.has(name) && !name.includes(".tmp.")).map((name) => {
206728
207444
  const full = path31.join(versionsDirPath, name);
206729
207445
  try {
206730
207446
  const st = fs37.statSync(full);
@@ -206734,7 +207450,7 @@ function gcVersions(versionsDirPath, opts) {
206734
207450
  }
206735
207451
  }).filter((x2) => x2 !== null).sort((a, b2) => b2.mtime - a.mtime);
206736
207452
  const deleted = [];
206737
- for (const c of candidates.slice(keepRecent)) {
207453
+ for (const c of candidates.slice(keepSpare)) {
206738
207454
  if (now - c.mtime < minAgeMs) continue;
206739
207455
  try {
206740
207456
  fs37.rmSync(c.full, { recursive: true, force: true });
@@ -206756,9 +207472,7 @@ function shimScript() {
206756
207472
  }
206757
207473
 
206758
207474
  // src/index.ts
206759
- var PKG_VERSION = true ? "0.1.116" : "dev";
206760
- var BIN_DIR = binDir();
206761
- var BIN_FILE = binFile();
207475
+ var PKG_VERSION = true ? "0.1.118" : "dev";
206762
207476
  var LOCAL_BIN = localBin();
206763
207477
  var NPM_PREFIX = npmPrefix();
206764
207478
  var INSTANCE = DEFAULT_INSTANCE;
@@ -206766,19 +207480,28 @@ var INST = instancePaths(DEFAULT_INSTANCE);
206766
207480
  var CONFIG_FILE = INST.configFile;
206767
207481
  var PID_FILE = INST.pidFile;
206768
207482
  var LOG_FILE = INST.logFile;
207483
+ var BIN_DIR = INST.binDir;
207484
+ var BIN_FILE = INST.binFile;
206769
207485
  function useInstance(name) {
206770
207486
  INSTANCE = name;
206771
207487
  INST = instancePaths(name);
206772
207488
  CONFIG_FILE = INST.configFile;
206773
207489
  PID_FILE = INST.pidFile;
206774
207490
  LOG_FILE = INST.logFile;
207491
+ BIN_DIR = INST.binDir;
207492
+ BIN_FILE = INST.binFile;
206775
207493
  }
206776
207494
  var PKG_NAME = "oasis_test";
206777
207495
  var parseFlags = (argv) => {
206778
207496
  const flags = /* @__PURE__ */ new Map();
206779
207497
  for (let i = 0; i < argv.length; i++) {
206780
207498
  const a = argv[i];
206781
- if (a.startsWith("--")) flags.set(a.slice(2), argv[++i] ?? "");
207499
+ if (!a.startsWith("--")) continue;
207500
+ const next = argv[i + 1];
207501
+ if (next !== void 0 && !next.startsWith("--")) {
207502
+ flags.set(a.slice(2), next);
207503
+ i++;
207504
+ } else flags.set(a.slice(2), "");
206782
207505
  }
206783
207506
  return flags;
206784
207507
  };
@@ -206831,22 +207554,24 @@ function ensureRuntimeAssets(targetDir = BIN_DIR) {
206831
207554
  console.error(`[oasis] connector runtime assets: \u4ECE ${synced.from} \u540C\u6B65\u4E86 ${synced.copied} \u4E2A .sh \u5230 ${targetDir}`);
206832
207555
  }
206833
207556
  }
206834
- function installSoftware() {
207557
+ function installSoftware(label) {
206835
207558
  const src = process.argv[1];
206836
- const vDir = versionDir(PKG_VERSION);
207559
+ const vDir = versionDir(label);
206837
207560
  installBinaryAtomic(src, path32.join(vDir, "oasis.js"));
206838
207561
  ensureRuntimeAssets(vDir);
206839
- pointCurrentAt(currentLink(), vDir);
206840
207562
  installBinaryAtomic(src, BIN_FILE);
206841
207563
  ensureRuntimeAssets(BIN_DIR);
207564
+ pointCurrentAt(INST.currentLink, vDir);
206842
207565
  fs38.mkdirSync(path32.dirname(LOCAL_BIN), { recursive: true });
206843
207566
  fs38.writeFileSync(LOCAL_BIN, shimScript(), { mode: 493 });
206844
- const removed = gcVersions(versionsDir(), { keep: PKG_VERSION });
206845
- if (removed.length) console.error(`[oasis] \u7248\u672C GC\uFF1A\u5220\u9664 ${removed.join(", ")}\uFF08\u4FDD\u7559 ${PKG_VERSION} + \u6700\u8FD1 2 \u7248\uFF09`);
207567
+ const keep = [.../* @__PURE__ */ new Set([label, ...referencedVersionLabels()])];
207568
+ const removed = gcVersions(versionsDir(), { keep });
207569
+ if (removed.length) console.error(`[oasis] \u7248\u672C GC\uFF1A\u5220\u9664 ${removed.join(", ")}\uFF08\u5728\u7528\uFF1A${keep.join(", ")}\uFF09`);
207570
+ return label;
206846
207571
  }
206847
207572
  function setupAutostart(name) {
206848
207573
  const inst = instancePaths(name);
206849
- const cmd = `${process.execPath} "${BIN_FILE}" --_daemon --instance ${name}`;
207574
+ const cmd = `${process.execPath} "${inst.binFile}" --_daemon --instance ${name}`;
206850
207575
  const home2 = os13.homedir();
206851
207576
  const pnpmHome = process.env["PNPM_HOME"] || path32.join(home2, ".local", "share", "pnpm");
206852
207577
  const extraBins = [
@@ -206866,9 +207591,13 @@ function setupAutostart(name) {
206866
207591
  `Description=Oasis Node Daemon (${name})`,
206867
207592
  "After=network.target",
206868
207593
  "",
207594
+ // OASIS_CLI_HOME 由 daemon 继承给它 spawn 的 agent 会话——非 default 实例的会话因此
207595
+ // 用的是**本实例**的 CLI 版本,而不是 PATH 上那个默认指向 default 的 shim。
207596
+ // (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
206869
207597
  "[Service]",
206870
207598
  `ExecStart=${cmd}`,
206871
207599
  `Environment=PATH=${daemonPath}`,
207600
+ `Environment=OASIS_CLI_HOME=${inst.currentLink}`,
206872
207601
  `StandardOutput=append:${inst.logFile}`,
206873
207602
  `StandardError=append:${inst.logFile}`,
206874
207603
  "Restart=on-failure",
@@ -206916,9 +207645,12 @@ function setupAutostart(name) {
206916
207645
  function spawnDaemon() {
206917
207646
  fs38.mkdirSync(path32.dirname(LOG_FILE), { recursive: true });
206918
207647
  const log3 = fs38.openSync(LOG_FILE, "a");
206919
- const child = (0, import_node_child_process19.spawn)(process.execPath, [BIN_FILE, "--_daemon", "--instance", INSTANCE], {
207648
+ const child = (0, import_node_child_process19.spawn)(process.execPath, [INST.binFile, "--_daemon", "--instance", INSTANCE], {
206920
207649
  detached: true,
206921
- stdio: ["ignore", log3, log3]
207650
+ stdio: ["ignore", log3, log3],
207651
+ // 与 systemd unit 里那条 Environment 等价——detached 兜底路径不能少,否则同一实例在两条
207652
+ // 启动路径下 spawn 出的会话会用不同版本的 CLI。
207653
+ env: { ...process.env, OASIS_CLI_HOME: INST.currentLink }
206922
207654
  });
206923
207655
  child.unref();
206924
207656
  return child.pid ?? 0;
@@ -206966,7 +207698,7 @@ function selfUpdate() {
206966
207698
  if (!cfg?.serverUrl) throw new Error("no config \u2014 run `oasis daemon start` first");
206967
207699
  const nameArg = cfg.name ? ` --name '${cfg.name.replace(/'/g, "'\\''")}'` : "";
206968
207700
  const oasisBin = path32.join(NPM_PREFIX, "bin", "oasis");
206969
- 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}`;
206970
207702
  const OFFICIAL = "https://registry.npmjs.org/";
206971
207703
  const installOfficial = `npm install -g --prefer-online --registry '${OFFICIAL}' --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
206972
207704
  const installFallback = `npm install -g --prefer-online --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
@@ -206992,36 +207724,110 @@ function selfUpdate() {
206992
207724
  child.unref();
206993
207725
  }
206994
207726
  }
206995
- function resolveInstance(explicit, serverUrl, forStart) {
207727
+ function resolveInstance(explicit) {
207728
+ if (!explicit) return DEFAULT_INSTANCE;
207729
+ if (!isValidInstanceName(explicit)) {
207730
+ console.error(`[oasis] \u975E\u6CD5\u5B9E\u4F8B\u540D "${explicit}"\uFF1A\u53EA\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/.-_\uFF0C\u4E14\u4EE5\u5B57\u6BCD\u6570\u5B57\u5F00\u5934`);
207731
+ process.exit(1);
207732
+ }
207733
+ return explicit;
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) {
206996
207754
  if (explicit) {
206997
- if (!isValidInstanceName(explicit)) {
206998
- console.error(`[oasis] \u975E\u6CD5\u5B9E\u4F8B\u540D "${explicit}"\uFF1A\u53EA\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/.-_\uFF0C\u4E14\u4EE5\u5B57\u6BCD\u6570\u5B57\u5F00\u5934`);
206999
- process.exit(1);
207000
- }
207001
- return explicit;
207755
+ const name = resolveInstance(explicit);
207756
+ return { mode: fs38.existsSync(instancePaths(name).configFile) ? "resume" : "new", names: [name] };
207002
207757
  }
207003
- const existing = listInstances();
207004
- if (existing.length === 0) return DEFAULT_INSTANCE;
207005
- if (existing.length === 1) return existing[0];
207006
- const demand = (why) => {
207007
- console.error(`[oasis] \u672C\u673A\u6709 ${existing.length} \u4E2A daemon \u5B9E\u4F8B\uFF08${existing.join(", ")}\uFF09\uFF1A${why}`);
207008
- console.error(` \u8BF7\u7528 --instance <name> \u663E\u5F0F\u6307\u5B9A\u3002`);
207009
- process.exit(1);
207010
- };
207011
- if (!serverUrl) return demand("\u6CA1\u7ED9 --server-ws\uFF0C\u65E0\u4ECE\u5224\u65AD\u4F60\u8981\u54EA\u4E00\u4E2A");
207758
+ const all = listInstances();
207759
+ if (all.length === 0) return { mode: "new", names: [DEFAULT_INSTANCE] };
207012
207760
  const want = normalizeServerUrl(serverUrl);
207013
- const hit = existing.filter((n) => {
207761
+ const hits = all.filter((n) => {
207014
207762
  const c = readConfigAt(instancePaths(n).configFile);
207015
207763
  return !!c?.serverUrl && normalizeServerUrl(c.serverUrl) === want;
207016
207764
  });
207017
- if (hit.length === 1) return hit[0];
207018
- if (hit.length > 1) return demand(`${hit.join(", ")} \u90FD\u8FDE\u7740 ${want}`);
207019
- if (!forStart) return demand(`\u6CA1\u6709\u5B9E\u4F8B\u8FDE\u7740 ${want}`);
207020
- const slug6 = hostSlug(serverUrl);
207021
- if (existing.includes(slug6)) {
207022
- return demand(`\u81EA\u52A8\u5B9E\u4F8B\u540D "${slug6}" \u5DF2\u88AB\u5360\u7528\uFF0C\u800C\u5B83\u8FDE\u7684\u4E0D\u662F ${want}`);
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 });
207023
207813
  }
207024
- return slug6;
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}`);
207025
207831
  }
207026
207832
  function derivedNodeIdFor(name) {
207027
207833
  if (name === DEFAULT_INSTANCE) return resolveNodeId();
@@ -207047,12 +207853,7 @@ void (async () => {
207047
207853
  }
207048
207854
  const flags = parseFlags(dargs);
207049
207855
  if (cmd === "--_daemon") {
207050
- const daemonInstance = flags.get("instance") ?? DEFAULT_INSTANCE;
207051
- if (!isValidInstanceName(daemonInstance)) {
207052
- console.error(`[oasis] \u975E\u6CD5\u5B9E\u4F8B\u540D "${daemonInstance}"`);
207053
- process.exit(1);
207054
- }
207055
- useInstance(daemonInstance);
207856
+ useInstance(resolveInstance(flags.get("instance")));
207056
207857
  const cfg = readConfig();
207057
207858
  if (!cfg) {
207058
207859
  console.error("[oasis] no config \u2014 run `oasis daemon start` first");
@@ -207132,69 +207933,37 @@ void (async () => {
207132
207933
  return;
207133
207934
  }
207134
207935
  if (dsub === "start") {
207936
+ const explicit = flags.get("instance") || void 0;
207937
+ const upgrade = flags.has("upgrade");
207135
207938
  const serverUrlFlag = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"];
207136
- useInstance(resolveInstance(flags.get("instance"), serverUrlFlag, true));
207137
- const prev = readConfig();
207138
- const serverUrl = serverUrlFlag ?? prev?.serverUrl;
207939
+ const probe = explicit ?? listInstances()[0] ?? DEFAULT_INSTANCE;
207940
+ const serverUrl = serverUrlFlag ?? readConfigAt(instancePaths(probe).configFile)?.serverUrl;
207139
207941
  if (!serverUrl) {
207140
- 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]");
207141
207943
  process.exit(1);
207142
207944
  }
207143
- if (!flags.get("instance") && prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
207144
- 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`);
207145
- console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
207146
- console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
207147
- }
207148
- const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
207149
- let nodeId = serverChanged ? void 0 : prev?.nodeId;
207150
- let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
207151
- const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"];
207152
- const derivedNodeId = derivedNodeIdFor(INSTANCE);
207153
- if (enrollToken) {
207154
- const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
207155
- method: "POST",
207156
- headers: { "content-type": "application/json" },
207157
- body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
207158
- });
207159
- if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
207160
- const body = await res.json();
207161
- token = body.token;
207162
- nodeId = body.nodeId;
207163
- } else if (prev?.nodeId && prev?.token && !serverChanged) {
207164
- nodeId = prev.nodeId;
207165
- token = prev.token;
207166
- }
207167
- if (!token || !nodeId) {
207168
- console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
207169
- 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]}`);
207170
207950
  }
207171
- const name = flags.get("name") ?? prev?.name;
207172
- const nameChanged = prev?.name !== void 0 && name !== prev.name;
207173
- saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
207174
- installSoftware();
207175
- stopDaemon(INSTANCE);
207176
- const tookOver = setupAutostart(INSTANCE);
207177
- const suffix = `${nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : ""}`;
207178
- if (!tookOver) {
207179
- const pid = spawnDaemon();
207180
- console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
207181
- } else {
207182
- console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
207951
+ for (const name of plan.names) {
207952
+ useInstance(name);
207953
+ await startOneInstance({ mode: plan.mode, serverUrl, upgrade, flags, applyName: plan.names.length === 1 });
207183
207954
  }
207184
- console.log(` instance: ${INSTANCE} nodeId: ${nodeId} config: ${INST.configFile}`);
207185
- console.log(` logs: tail -f ${LOG_FILE}`);
207186
207955
  if (!process.env["PATH"]?.includes(path32.dirname(LOCAL_BIN)))
207187
207956
  console.log(` add to PATH: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc`);
207188
207957
  return;
207189
207958
  }
207190
207959
  if (dsub === "stop") {
207191
- useInstance(resolveInstance(flags.get("instance"), flags.get("server-ws"), false));
207960
+ useInstance(resolveInstance(flags.get("instance")));
207192
207961
  stopDaemon(INSTANCE);
207193
207962
  console.log(`\u2713 stopped (instance ${INSTANCE})`);
207194
207963
  return;
207195
207964
  }
207196
207965
  if (dsub === "restart") {
207197
- useInstance(resolveInstance(flags.get("instance"), flags.get("server-ws"), false));
207966
+ useInstance(resolveInstance(flags.get("instance")));
207198
207967
  stopDaemon(INSTANCE);
207199
207968
  if (!readConfig()) {
207200
207969
  console.error("no config \u2014 run `oasis daemon start` first");
@@ -207205,11 +207974,15 @@ void (async () => {
207205
207974
  return;
207206
207975
  }
207207
207976
  if (dsub === "update") {
207208
- useInstance(resolveInstance(flags.get("instance"), flags.get("server-ws"), false));
207977
+ useInstance(resolveInstance(flags.get("instance")));
207209
207978
  if (!readConfig()?.serverUrl) {
207210
207979
  console.error("no config \u2014 run `oasis daemon start` first");
207211
207980
  process.exit(1);
207212
207981
  }
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`);
207985
+ }
207213
207986
  console.log(`\u2192 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026`);
207214
207987
  selfUpdate();
207215
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}`);