oasis_test 0.1.117 → 0.1.119

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 +1086 -195
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2866,9 +2866,11 @@ var init_execution_continuity = __esm({
2866
2866
  });
2867
2867
 
2868
2868
  // ../contract/src/automation.ts
2869
+ var AUTOMATION_EVENT_NAMES;
2869
2870
  var init_automation = __esm({
2870
2871
  "../contract/src/automation.ts"() {
2871
2872
  "use strict";
2873
+ AUTOMATION_EVENT_NAMES = ["workorder.completed"];
2872
2874
  }
2873
2875
  });
2874
2876
 
@@ -4799,6 +4801,11 @@ function createPolicy(cfg) {
4799
4801
  }
4800
4802
  };
4801
4803
  }
4804
+ function policyGetter(source, fallback = CODE_DEFAULT_POLICY) {
4805
+ if (typeof source === "function") return source;
4806
+ if (source) return () => source;
4807
+ return () => fallback;
4808
+ }
4802
4809
  var MINUTE, HOUR, DAY, AGENT_WORK_DEADLINE_MS, AGENT_REVIEW_DEADLINE_MS, AGENT_REVIEW_MAX_RETRIES, AGENT_WORK_MAX_RETRIES, RETRY_BASE_MS, RUNTIME_MAX_RETRIES, DEFAULT_IDLE_BY_RUNTIME, DEFAULT_IDLE_MS, DEFAULT_WALL_CLOCK_BY_RUNTIME, DEFAULT_WALL_CLOCK_MS, DEFAULT_HUMAN_CADENCE, CODE_DEFAULT_POLICY;
4803
4810
  var init_policy = __esm({
4804
4811
  "../timeout-policy/src/policy.ts"() {
@@ -7526,7 +7533,11 @@ function tryToRun(state, nodeId, ctx, events) {
7526
7533
  nodeId,
7527
7534
  workId,
7528
7535
  reasons,
7529
- continuesWorkId: null,
7536
+ // ★ 基线(配套一):base=上一已验收版(latestAcceptId),parents[0]=最近一个有产出版
7537
+ //(latestProposedWorkId)。两者解耦:中间夹超时/零产出空壳时 base 仍在上一已验收版、parents 跳过空壳。
7538
+ // 不用 latestWorkId——它含空壳,会把基线钉到未验收/无产出的 work 上(isStale 比已验收 head,错)。
7539
+ continuesWorkId: node.latestAcceptId,
7540
+ parentWorkId: node.latestProposedWorkId,
7530
7541
  // 工单 e4a43a42(接续只对短时重试成立):**已收口过的节点被重新激活 = 返工**(变更请求/驳回/上游
7531
7542
  // 变更把已 accept 的节点重开)→ 起全新会话,别接续那场过时的旧对话;**从未收口的节点** = 首跑或
7532
7543
  // 失败后重试,接续 WIP 是对的。判据 = 有没有 latest_accept。派发层据此决定 resumeSession。
@@ -7565,6 +7576,10 @@ function inputsMatch(state, node, latestWork) {
7565
7576
  const latestInputs = new Set(state.workInputsOf(latestWork.id).map((i) => i.upstreamWorkId));
7566
7577
  return currInputs.size === latestInputs.size && [...currInputs].every((x2) => latestInputs.has(x2));
7567
7578
  }
7579
+ function reviewMaxRetriesOf(policy) {
7580
+ const act = policy.resolve({ executor: "agent", scenario: "review" }).timeoutAction;
7581
+ return act.kind === "kill-rebuild" ? act.maxRetries : AGENT_REVIEW_MAX_RETRIES;
7582
+ }
7568
7583
  function processReviews(state, nodeId, workId, ctx, events) {
7569
7584
  const policy = ctx.policy ?? CODE_DEFAULT_POLICY;
7570
7585
  const reqs = state.requirementsOf(nodeId);
@@ -7614,7 +7629,7 @@ function processReviews(state, nodeId, workId, ctx, events) {
7614
7629
  }
7615
7630
  } else if (info.status === "failed") {
7616
7631
  const req = reqs.find((r) => r.reviewGroup === g2);
7617
- const overLimit = countFailedForReviewGroup(g2, reviews) > AGENT_REVIEW_MAX_RETRIES;
7632
+ const overLimit = countFailedForReviewGroup(g2, reviews) > reviewMaxRetriesOf(policy);
7618
7633
  if (overLimit && classify(req.reviewerActorId) === "agent") {
7619
7634
  const lrId = req.latestReviewId;
7620
7635
  if (lrId) events.push({
@@ -7901,6 +7916,9 @@ CREATE TABLE IF NOT EXISTS ${schema}.works (
7901
7916
  outcome_detail jsonb,
7902
7917
  session_ref text,
7903
7918
  continues_work_id text,
7919
+ -- \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
7920
+ -- \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
7921
+ parent_work_id text,
7904
7922
  -- \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
7923
  -- "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
7924
  lane text CHECK (lane IN ('main','reply')),
@@ -8078,6 +8096,10 @@ CREATE TABLE IF NOT EXISTS ${schema}.event_handler_log (
8078
8096
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS retry_at timestamptz;
8079
8097
  ALTER TABLE ${schema}.reviews ADD COLUMN IF NOT EXISTS retry_at timestamptz;
8080
8098
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS status text;
8099
+ ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS parent_work_id text;
8100
+ -- \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
8101
+ -- \u65B0 work \u624D\u5199\u300C\u6700\u8FD1\u6709\u4EA7\u51FA\u7248\u300D\uFF08latest_proposed_work_id\uFF09\u5B9E\u73B0 base/parents \u89E3\u8026\u3002
8102
+ UPDATE ${schema}.works SET parent_work_id = continues_work_id WHERE parent_work_id IS NULL;
8081
8103
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS agent_handoff_attempt_id text;
8082
8104
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_status text;
8083
8105
  ALTER TABLE ${schema}.works ADD COLUMN IF NOT EXISTS business_handoff_recorded_at timestamptz;
@@ -9427,7 +9449,7 @@ var require_utils2 = __commonJS({
9427
9449
  var nodeCrypto = require("crypto");
9428
9450
  module2.exports = {
9429
9451
  postgresMd5PasswordHash,
9430
- randomBytes: randomBytes7,
9452
+ randomBytes: randomBytes8,
9431
9453
  deriveKey,
9432
9454
  sha256,
9433
9455
  hashByName,
@@ -9437,7 +9459,7 @@ var require_utils2 = __commonJS({
9437
9459
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
9438
9460
  var subtleCrypto = webCrypto.subtle;
9439
9461
  var textEncoder = new TextEncoder();
9440
- function randomBytes7(length) {
9462
+ function randomBytes8(length) {
9441
9463
  return webCrypto.getRandomValues(Buffer.alloc(length));
9442
9464
  }
9443
9465
  async function md5(string5) {
@@ -13569,6 +13591,7 @@ var init_store_postgres = __esm({
13569
13591
  outcomeDetail: w2.outcome_detail,
13570
13592
  sessionRef: w2.session_ref,
13571
13593
  continuesWorkId: w2.continues_work_id,
13594
+ parentWorkId: w2.parent_work_id,
13572
13595
  // ★ lane 兜底(存量行迁移前 / 迁移与回填之间的竞态窗):无值时按 reply_to_issue_id 派生,
13573
13596
  // 保证 TS 侧 `Work.lane` 恒非空。reply_to_issue_id 在读写映射层可直接读(护栏白名单)。
13574
13597
  lane: w2.lane ?? (w2.reply_to_issue_id == null ? "main" : "reply"),
@@ -13784,10 +13807,10 @@ var init_store_postgres = __esm({
13784
13807
  await this.c.query(
13785
13808
  `INSERT INTO ${this.s}.works
13786
13809
  (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,
13810
+ outcome_detail,session_ref,continues_work_id,parent_work_id,reply_to_issue_id,output_version_no,
13788
13811
  node_version,conclusion,proposal_reason,last_activity_at,frozen_issue_ids,agent_handoff_attempt_id,business_handoff_status,business_handoff_recorded_at,
13789
13812
  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)
13813
+ 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
13814
  ON CONFLICT (id) DO NOTHING`,
13792
13815
  [
13793
13816
  w2.id,
@@ -13805,6 +13828,7 @@ var init_store_postgres = __esm({
13805
13828
  w2.outcomeDetail ? JSON.stringify(w2.outcomeDetail) : null,
13806
13829
  w2.sessionRef,
13807
13830
  w2.continuesWorkId,
13831
+ w2.parentWorkId,
13808
13832
  w2.replyToIssueId ?? null,
13809
13833
  w2.outputVersionNo,
13810
13834
  w2.nodeVersion,
@@ -14596,6 +14620,7 @@ var init_work = __esm({
14596
14620
  outcomeDetail: null,
14597
14621
  sessionRef: null,
14598
14622
  continuesWorkId: e.continuesWorkId,
14623
+ parentWorkId: e.parentWorkId ?? null,
14599
14624
  replyToIssueId: e.replyToIssueId,
14600
14625
  outputVersionNo: null,
14601
14626
  conclusion: null,
@@ -14634,6 +14659,7 @@ var init_work = __esm({
14634
14659
  outcomeDetail: null,
14635
14660
  sessionRef: null,
14636
14661
  continuesWorkId: e.continuesWorkId,
14662
+ parentWorkId: e.parentWorkId ?? null,
14637
14663
  outputVersionNo: null,
14638
14664
  conclusion: null,
14639
14665
  frozenIssueIds: frozen.length > 0 ? frozen : null,
@@ -15410,7 +15436,8 @@ var init_bus = __esm({
15410
15436
  io;
15411
15437
  clock;
15412
15438
  config;
15413
- policy;
15439
+ /** 取当前策略(不是策略本身)——见 {@link EngineBusOptions.policy} 的热载不变量。 */
15440
+ policyOf;
15414
15441
  registry;
15415
15442
  maxEffectAttempts;
15416
15443
  stuckAppliedRecoveryMs;
@@ -15422,7 +15449,7 @@ var init_bus = __esm({
15422
15449
  this.io = opts.io ?? NOOP_IO;
15423
15450
  this.clock = opts.clock ?? SYSTEM_CLOCK;
15424
15451
  this.config = opts.config ?? DEFAULT_CONFIG;
15425
- this.policy = opts.policy ?? CODE_DEFAULT_POLICY;
15452
+ this.policyOf = policyGetter(opts.policy);
15426
15453
  this.registry = new HandlerRegistry(opts.handlers);
15427
15454
  this.maxEffectAttempts = opts.maxEffectAttempts ?? 3;
15428
15455
  this.stuckAppliedRecoveryMs = opts.stuckAppliedRecoveryMs ?? DEFAULT_STUCK_APPLIED_RECOVERY_MS;
@@ -15811,7 +15838,7 @@ var init_bus = __esm({
15811
15838
  if (!snapshot) return { emitted: 0, terminated: false };
15812
15839
  if (snapshot.workorder.lifecycle !== "active") return { emitted: 0, terminated: false };
15813
15840
  const state = new WorkorderState(snapshot);
15814
- const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR2, config: this.config, policy: this.policy };
15841
+ const scanCtx = { at: this.clock.now(), actorId: SYSTEM_ACTOR2, config: this.config, policy: this.policyOf() };
15815
15842
  const { events, terminated } = scan(state, scanCtx);
15816
15843
  await tx.applyMutations(workorderId, state.mutations);
15817
15844
  const allEvents = [...state.emitted, ...events];
@@ -15958,8 +15985,9 @@ function mapWorkStateToRevisionState(w2, artifactCount) {
15958
15985
  case "success":
15959
15986
  if (w2.acceptanceState === "accepted") return "merged";
15960
15987
  if (w2.acceptanceState === "rejected") return "rejected";
15988
+ if (w2.conclusion == null) return "working";
15961
15989
  return "proposed";
15962
- // success but not yet judged
15990
+ // conclude、未裁决
15963
15991
  // ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
15964
15992
  // superseded,改枚举会牵动契约与前端,见红线 4)——仍投影成 superseded 保持生命周期语义,但**内容
15965
15993
  // 如实为 empty**(workToRevision 对无产出 work 给 contentKind="empty")。读面(CLI 版本列表)据此把
@@ -16037,7 +16065,9 @@ function workToRevision(w2, workInputs, artifactFiles) {
16037
16065
  id: w2.id,
16038
16066
  artifactId: w2.nodeId,
16039
16067
  base: w2.continuesWorkId,
16040
- parents: w2.continuesWorkId ? [w2.continuesWorkId] : [],
16068
+ // 配套一:base parents 解耦——base=上一已验收版(continuesWorkId),parents[0]=最近一个有产出版
16069
+ //(parentWorkId)。两者可不同:中间夹超时/零产出空壳时 base 不动、parents 跳过空壳指向上一个有产出。
16070
+ parents: w2.parentWorkId ? [w2.parentWorkId] : [],
16041
16071
  contentRef,
16042
16072
  contentKind,
16043
16073
  basedOn: Object.keys(basedOn).length > 0 ? basedOn : void 0,
@@ -16256,6 +16286,16 @@ function bridgeLoadReadModel(input) {
16256
16286
  for (const w2 of allWorks) {
16257
16287
  revisions.set(w2.id, workToRevision(w2, allWorkInputs, artifactFiles));
16258
16288
  }
16289
+ for (const r of revisions.values()) {
16290
+ if (r.state !== "proposed") continue;
16291
+ for (const parentId of r.parents) {
16292
+ const parent = revisions.get(parentId);
16293
+ if (parent && parent.state === "proposed" && (parent.part ?? void 0) === (r.part ?? void 0)) {
16294
+ parent.state = "superseded";
16295
+ parent.supersededBy = r.id;
16296
+ }
16297
+ }
16298
+ }
16259
16299
  const recipientsByIssue = /* @__PURE__ */ new Map();
16260
16300
  for (const r of allIssueRecipients) {
16261
16301
  const list = recipientsByIssue.get(r.issueId) ?? [];
@@ -17304,10 +17344,12 @@ var init_kernel_bridge = __esm({
17304
17344
  nodeId: args.artifactId,
17305
17345
  workId: revisionId,
17306
17346
  reasons: [{ kind: "initial" }],
17307
- // ★ base 三态:undefined=缺省续写(基当前 head,旧引擎 propose 即基上版——否则新版 base=∅、
17308
- // 相对当前 head stale advanceQueue 永不落地,annotation-resolve routing 实测);null=并发
17347
+ // ★ base 三态:undefined=缺省续写(基「上一已验收版」node.latestAcceptId——isStale concludedHead,
17348
+ // 只有 latestAcceptId 对;旧口径 artifact.currentRev=latestWorkId 可能是一条未验收/空壳 work);null=并发
17309
17349
  // 作者显式声明 base=∅(autoland stale 兜底:不落、留队);具体 revisionId=钉旧版。
17310
- continuesWorkId: args.base !== void 0 ? args.base : artifact?.currentRev ?? null
17350
+ // parents[0]=「最近一个有产出版」node.latestProposedWorkId(跳过超时/零产出空壳),与 base 解耦。
17351
+ continuesWorkId: args.base !== void 0 ? args.base : nodeRow?.latestAcceptId ?? null,
17352
+ parentWorkId: nodeRow?.latestProposedWorkId ?? null
17311
17353
  }
17312
17354
  });
17313
17355
  workId = revisionId;
@@ -40107,7 +40149,7 @@ var require_websocket = __commonJS({
40107
40149
  var http2 = require("http");
40108
40150
  var net = require("net");
40109
40151
  var tls = require("tls");
40110
- var { randomBytes: randomBytes7, createHash: createHash19 } = require("crypto");
40152
+ var { randomBytes: randomBytes8, createHash: createHash21 } = require("crypto");
40111
40153
  var { Duplex, Readable } = require("stream");
40112
40154
  var { URL: URL2 } = require("url");
40113
40155
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -40645,7 +40687,7 @@ var require_websocket = __commonJS({
40645
40687
  }
40646
40688
  }
40647
40689
  const defaultPort = isSecure ? 443 : 80;
40648
- const key = randomBytes7(16).toString("base64");
40690
+ const key = randomBytes8(16).toString("base64");
40649
40691
  const request2 = isSecure ? https.request : http2.request;
40650
40692
  const protocolSet = /* @__PURE__ */ new Set();
40651
40693
  let perMessageDeflate;
@@ -40775,7 +40817,7 @@ var require_websocket = __commonJS({
40775
40817
  abortHandshake(websocket, socket, "Invalid Upgrade header");
40776
40818
  return;
40777
40819
  }
40778
- const digest = createHash19("sha1").update(key + GUID).digest("base64");
40820
+ const digest = createHash21("sha1").update(key + GUID).digest("base64");
40779
40821
  if (res.headers["sec-websocket-accept"] !== digest) {
40780
40822
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
40781
40823
  return;
@@ -41144,7 +41186,7 @@ var require_websocket_server = __commonJS({
41144
41186
  var EventEmitter = require("events");
41145
41187
  var http2 = require("http");
41146
41188
  var { Duplex } = require("stream");
41147
- var { createHash: createHash19 } = require("crypto");
41189
+ var { createHash: createHash21 } = require("crypto");
41148
41190
  var extension2 = require_extension();
41149
41191
  var PerMessageDeflate2 = require_permessage_deflate();
41150
41192
  var subprotocol2 = require_subprotocol();
@@ -41451,7 +41493,7 @@ var require_websocket_server = __commonJS({
41451
41493
  );
41452
41494
  }
41453
41495
  if (this._state > RUNNING) return abortHandshake(socket, 503);
41454
- const digest = createHash19("sha1").update(key + GUID).digest("base64");
41496
+ const digest = createHash21("sha1").update(key + GUID).digest("base64");
41455
41497
  const headers = [
41456
41498
  "HTTP/1.1 101 Switching Protocols",
41457
41499
  "Upgrade: websocket",
@@ -128706,13 +128748,13 @@ ${block}
128706
128748
  throw err;
128707
128749
  }
128708
128750
  const delay = base * Math.pow(3, attempt - 1);
128709
- yield sleep4(delay);
128751
+ yield sleep5(delay);
128710
128752
  }
128711
128753
  }
128712
128754
  throw lastErr;
128713
128755
  });
128714
128756
  }
128715
- function sleep4(ms) {
128757
+ function sleep5(ms) {
128716
128758
  return new Promise((r) => setTimeout(r, ms));
128717
128759
  }
128718
128760
  var DEFAULT_CHUNK_LIMIT = 3500;
@@ -158018,6 +158060,40 @@ var init_resume_classify = __esm({
158018
158060
  }
158019
158061
  });
158020
158062
 
158063
+ // ../adapters/src/_core/root-bypass.ts
158064
+ function decideRootBypass(sources) {
158065
+ if (sources.uid !== 0) return { inject: false, reason: "not-root" };
158066
+ if (sources.env["IS_SANDBOX"] === "1") return { inject: false, reason: "already-sandboxed" };
158067
+ if (sources.env["CLAUDE_CODE_BUBBLEWRAP"]) return { inject: false, reason: "bubblewrap" };
158068
+ const sw = (sources.env["OASIS_ALLOW_ROOT_BYPASS"] ?? "").trim().toLowerCase();
158069
+ if (OFF_VALUES.has(sw)) return { inject: false, reason: "disabled" };
158070
+ return { inject: true, reason: "root-needs-sandbox-flag" };
158071
+ }
158072
+ function rootBypassEnv(runtime, sources = { uid: process.getuid?.(), env: process.env }) {
158073
+ const d = decideRootBypass(sources);
158074
+ if (d.inject || d.reason === "disabled") {
158075
+ const key = `${runtime}:${d.reason}`;
158076
+ if (!loggedOnce.has(key)) {
158077
+ loggedOnce.add(key);
158078
+ console.warn(`[adapter:${runtime}] ` + JSON.stringify({
158079
+ event: "root-bypass",
158080
+ injected: d.inject,
158081
+ reason: d.reason,
158082
+ 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"
158083
+ }));
158084
+ }
158085
+ }
158086
+ return d.inject ? { IS_SANDBOX: "1" } : {};
158087
+ }
158088
+ var OFF_VALUES, loggedOnce;
158089
+ var init_root_bypass = __esm({
158090
+ "../adapters/src/_core/root-bypass.ts"() {
158091
+ "use strict";
158092
+ OFF_VALUES = /* @__PURE__ */ new Set(["off", "0", "false", "no"]);
158093
+ loggedOnce = /* @__PURE__ */ new Set();
158094
+ }
158095
+ });
158096
+
158021
158097
  // ../adapters/src/_core/transcript.ts
158022
158098
  function openTranscript(file) {
158023
158099
  const out = fs8.createWriteStream(file);
@@ -158268,6 +158344,7 @@ var init_claude_code = __esm({
158268
158344
  init_materialize_bundle();
158269
158345
  init_container_runtime();
158270
158346
  init_resume_classify();
158347
+ init_root_bypass();
158271
158348
  init_session_paths();
158272
158349
  init_transcript();
158273
158350
  init_filter_extra_args();
@@ -158427,7 +158504,11 @@ var init_claude_code = __esm({
158427
158504
  OASIS_SERVER: job.server.url,
158428
158505
  OASIS_TOKEN: job.actorToken,
158429
158506
  // OTel 遥测(默认关;resolveOtelEnv 决定开不开,runId 进 resource 属性自然归位到本次 run)。
158430
- ...otelEnv
158507
+ ...otelEnv,
158508
+ // root 下放行 bypassPermissions(见 _core/root-bypass.ts:claude 的闸认 IS_SANDBOX=1)。
158509
+ // 铺在最后=系统强制层(同 OASIS_SERVER/TOKEN):员工 env 或工单 executionEnv 里的陈旧值
158510
+ // 不得把它顶掉。非 root / 宿主已给 / 显式关闭时是空对象,行为逐字节不变。
158511
+ ...rootBypassEnv("claude-code")
158431
158512
  },
158432
158513
  stdio: ["pipe", "pipe", "pipe"]
158433
158514
  });
@@ -161493,14 +161574,17 @@ async function materialize(job, cfg) {
161493
161574
  const task = materializeBundle(dir, job.bundle);
161494
161575
  return { id, dir, task };
161495
161576
  }
161496
- function buildEnv(job, extra) {
161577
+ function buildEnv(job, cfg) {
161497
161578
  return {
161498
161579
  ...scrubLeakyEnv(process.env),
161499
- ...extra,
161580
+ ...cfg.env,
161500
161581
  ...job.env,
161501
161582
  ...job.wrapperPaths?.length ? { PATH: `${job.wrapperPaths.map((p2) => path12.dirname(p2)).join(path12.delimiter)}${path12.delimiter}${job.env?.["PATH"] ?? process.env["PATH"] ?? ""}` } : {},
161502
161583
  OASIS_SERVER: job.server.url,
161503
- OASIS_TOKEN: job.actorToken
161584
+ OASIS_TOKEN: job.actorToken,
161585
+ // root 下放行 bypassPermissions(仅 claude 家族 runtime 声明;见 _core/root-bypass.ts)。
161586
+ // 与 OASIS_SERVER/TOKEN 同属系统强制层:员工 env 顶不掉。
161587
+ ...cfg.rootBypassRuntime ? rootBypassEnv(cfg.rootBypassRuntime) : {}
161504
161588
  };
161505
161589
  }
161506
161590
  function makeFinish(id, cfg, transcriptRefPtr, transcriptOut, dir, workdirKey, runtimeSessionId, exitCbs) {
@@ -161559,7 +161643,7 @@ async function runStreamJson(job, cfg) {
161559
161643
  stdio: ["ignore", "pipe", "pipe"]
161560
161644
  }) : (0, import_node_child_process11.spawn)(cfg.bin, args, {
161561
161645
  cwd: dir,
161562
- env: buildEnv(job, cfg.env),
161646
+ env: buildEnv(job, cfg),
161563
161647
  stdio: ["ignore", "pipe", "pipe"]
161564
161648
  });
161565
161649
  if (child.pid !== void 0 && job.workdirKey) lockWorkdir(dir, child.pid, "session", job.workdirKey);
@@ -161659,7 +161743,7 @@ async function runOneShotText(job, cfg) {
161659
161743
  stdio: ["ignore", "pipe", "pipe"]
161660
161744
  }) : (0, import_node_child_process11.spawn)(cfg.bin, args, {
161661
161745
  cwd: dir,
161662
- env: buildEnv(job, cfg.env),
161746
+ env: buildEnv(job, cfg),
161663
161747
  stdio: ["ignore", "pipe", "pipe"]
161664
161748
  });
161665
161749
  if (child.pid !== void 0 && job.workdirKey) lockWorkdir(dir, child.pid, "session", job.workdirKey);
@@ -161743,6 +161827,7 @@ var init_subprocess = __esm({
161743
161827
  init_session_paths();
161744
161828
  init_transcript();
161745
161829
  init_filter_extra_args();
161830
+ init_root_bypass();
161746
161831
  }
161747
161832
  });
161748
161833
 
@@ -161859,7 +161944,10 @@ var init_codebuddy = __esm({
161859
161944
  workRoot: opts.workRoot,
161860
161945
  cleanupWorkdir: opts.cleanupWorkdir,
161861
161946
  env: opts.env,
161862
- extraArgs: opts.extraArgs
161947
+ extraArgs: opts.extraArgs,
161948
+ // CodeBuddy 是 Claude Code 分叉,走同一个 bypassPermissions flag,root 下大概率撞同一条闸
161949
+ // (claude 侧已实测)。声明后由 _core/root-bypass.ts 在 root 下注入 IS_SANDBOX=1。
161950
+ rootBypassRuntime: "codebuddy"
161863
161951
  });
161864
161952
  }
161865
161953
  };
@@ -168058,6 +168146,10 @@ var init_memory_automation_store = __esm({
168058
168146
  hasSigningSecret: false
168059
168147
  };
168060
168148
  }
168149
+ if (input.kind === "event" && input.event) {
168150
+ t.enabled = false;
168151
+ t.event = { source: "project", projectId: input.event.projectId, events: [...input.event.events], baselineState: "aligning" };
168152
+ }
168061
168153
  return t;
168062
168154
  }
168063
168155
  async replaceTriggers(automationId, triggers) {
@@ -168085,6 +168177,7 @@ var init_memory_automation_store = __esm({
168085
168177
  if (t.webhook && patch.webhook && "eventFilters" in patch.webhook) {
168086
168178
  t.webhook.eventFilters = patch.webhook.eventFilters ?? null;
168087
168179
  }
168180
+ if (t.event && patch.event?.events) t.event.events = [...patch.event.events];
168088
168181
  return structuredClone(t);
168089
168182
  }
168090
168183
  async deleteTrigger(triggerId) {
@@ -168113,6 +168206,33 @@ var init_memory_automation_store = __esm({
168113
168206
  if (t) t.publishedBy = publishedBy;
168114
168207
  }
168115
168208
  }
168209
+ /* ---------- event 触发器(ADR 0163) ---------- */
168210
+ async listEventTriggers() {
168211
+ const out = [];
168212
+ for (const t of this.triggers.values()) {
168213
+ if (t.kind !== "event") continue;
168214
+ const a = this.automations.get(t.automationId);
168215
+ if (!a || !a.enabled || a.archivedAt) continue;
168216
+ out.push(structuredClone(t));
168217
+ }
168218
+ return out.sort((x2, y) => x2.id < y.id ? -1 : x2.id > y.id ? 1 : 0);
168219
+ }
168220
+ async completeEventBaseline(triggerId, _at) {
168221
+ const t = this.triggers.get(triggerId);
168222
+ if (!t?.event) return;
168223
+ t.event.baselineState = "ready";
168224
+ t.enabled = true;
168225
+ }
168226
+ async resetEventBaseline(triggerId) {
168227
+ const t = this.triggers.get(triggerId);
168228
+ if (!t?.event) return;
168229
+ t.event.baselineState = "aligning";
168230
+ t.enabled = false;
168231
+ }
168232
+ async findRunBySpawnedWorkspace(workspace) {
168233
+ 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);
168234
+ return hits[0] ? structuredClone(hits[0]) : null;
168235
+ }
168116
168236
  /* ---------- runs ---------- */
168117
168237
  async createRun(run) {
168118
168238
  if (run.triggerId && run.plannedAt) {
@@ -173812,7 +173932,9 @@ async function openNewEngine(opts) {
173812
173932
  const bus = new EngineBus({
173813
173933
  store,
173814
173934
  io: NOOP_IO,
173815
- clock: SYSTEM_CLOCK
173935
+ clock: SYSTEM_CLOCK,
173936
+ // 原样交给 bus(bus 内部归一成取值函数)——这里绝不能 call 一次存下来,那就把热载掐死了。
173937
+ policy: opts.policy
173816
173938
  });
173817
173939
  const kernel = await EngineKernel.open(store, bus, {
173818
173940
  schema: opts.schemaDefs ?? [],
@@ -183313,7 +183435,11 @@ var init_readonly_bundle = __esm({
183313
183435
  function makeSeededWorkorderCreator(deps) {
183314
183436
  const genWorkspace = deps.genWorkspace ?? (() => `ws:wo-${(0, import_node_crypto33.randomUUID)().slice(0, 8)}`);
183315
183437
  return async (input) => {
183316
- const workspace = genWorkspace();
183438
+ const workspace = input.idempotencyKey ? workspaceForKey(input.idempotencyKey) : genWorkspace();
183439
+ if (input.idempotencyKey) {
183440
+ const existing = existingWorkorder(deps.kernel, workspace);
183441
+ if (existing) return existing;
183442
+ }
183317
183443
  if (deps.artifactState && (input.projectId || deps.createProject)) {
183318
183444
  await ensureWorkorderProject({
183319
183445
  workOrderId: workspace,
@@ -183392,6 +183518,20 @@ function makeSeededWorkorderCreator(deps) {
183392
183518
  return { workspace, rootArtifactId: briefId, spawned: planned.spawned };
183393
183519
  };
183394
183520
  }
183521
+ function workspaceForKey(key) {
183522
+ return `ws:wo-${(0, import_node_crypto33.createHash)("sha256").update(key).digest("hex").slice(0, 8)}`;
183523
+ }
183524
+ function existingWorkorder(kernel, workspace) {
183525
+ const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === workspace);
183526
+ if (arts.length === 0) return null;
183527
+ const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
183528
+ return {
183529
+ workspace,
183530
+ rootArtifactId: seed.id,
183531
+ // role 取自 planner 的规划结果、不是 artifact 上的字段——重试路径拿不到也无所谓(可省)。
183532
+ spawned: arts.map((a) => ({ id: a.id, type: a.type, owner: a.owner }))
183533
+ };
183534
+ }
183395
183535
  var import_node_crypto33, enc2;
183396
183536
  var init_create_seeded_workorder = __esm({
183397
183537
  "../server/src/domains/collab/create-seeded-workorder.ts"() {
@@ -186076,6 +186216,7 @@ function automationSubstantiveChange(before, after) {
186076
186216
  }
186077
186217
  function triggerSubstantiveChange(before, after) {
186078
186218
  if (before.enabled !== after.enabled) return true;
186219
+ if (!jsonEq(before.event?.events ?? null, after.event?.events ?? null)) return true;
186079
186220
  if ((before.schedule?.cron ?? null) !== (after.schedule?.cron ?? null)) return true;
186080
186221
  if ((before.schedule?.tz ?? null) !== (after.schedule?.tz ?? null)) return true;
186081
186222
  const filtersOf = (t) => t.webhook?.eventFilters ?? null;
@@ -186927,6 +187068,17 @@ function renderDate(at, tz) {
186927
187068
  return at.toISOString().slice(0, 10);
186928
187069
  }
186929
187070
  }
187071
+ function eventCommandKey(runId) {
187072
+ return `automation-event:${runId}`;
187073
+ }
187074
+ function eventDispatchState(run) {
187075
+ const r = run.result;
187076
+ if (r && typeof r === "object" && !Array.isArray(r)) {
187077
+ const d = r["eventDispatch"];
187078
+ if (d && typeof d === "object" && !Array.isArray(d)) return d;
187079
+ }
187080
+ return {};
187081
+ }
186930
187082
  function clipReply(reply) {
186931
187083
  const t = reply.trim();
186932
187084
  return t.length > CHAT_REPLY_MAX ? `${t.slice(0, CHAT_REPLY_MAX)}
@@ -186958,7 +187110,7 @@ function appendSourceNote(brief, automation, source, atIso) {
186958
187110
  ---
186959
187111
  \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
187112
  }
186961
- var import_node_crypto36, DATE_TOKEN, AutomationsService, CHAT_REPLY_MAX;
187113
+ 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
187114
  var init_service8 = __esm({
186963
187115
  "../server/src/domains/automations/service.ts"() {
186964
187116
  "use strict";
@@ -187051,7 +187203,7 @@ var init_service8 = __esm({
187051
187203
  await seedScheduleNextRuns(this.store, [await this.store.getTrigger(t.id) ?? t].filter(Boolean), this.now());
187052
187204
  return await this.store.getTrigger(t.id);
187053
187205
  }
187054
- /** 单条触发器 PATCH。归因:比较持久化前后行,实质变更(enabled/cron/tz/eventFilters)只转移**该行**。 */
187206
+ /** 单条触发器 PATCH。归因:比较持久化前后行,实质变更(enabled/cron/tz/eventFilters/events)只转移**该行**。 */
187055
187207
  async updateTrigger(triggerId, patch, editor) {
187056
187208
  const before = await this.store.getTrigger(triggerId);
187057
187209
  if (!before) throw new Error(`trigger not found: ${triggerId}`);
@@ -187062,6 +187214,9 @@ var init_service8 = __esm({
187062
187214
  if (after.kind === "schedule") {
187063
187215
  await seedScheduleNextRuns(this.store, [after], this.now());
187064
187216
  }
187217
+ if (after.kind === "event" && patch.enabled === true && !before.enabled) {
187218
+ await this.store.resetEventBaseline(triggerId);
187219
+ }
187065
187220
  return await this.store.getTrigger(triggerId);
187066
187221
  }
187067
187222
  async deleteTrigger(triggerId, editor) {
@@ -187141,7 +187296,17 @@ var init_service8 = __esm({
187141
187296
  };
187142
187297
  const { run, created } = await this.store.createRun(base);
187143
187298
  if (!created) return run;
187144
- const skip = await this.admissionSkip(automation);
187299
+ return this.dispatchRun(automation, trigger, run, source, opts);
187300
+ }
187301
+ /**
187302
+ * 对一条**已占位的 pending run** 做准入 + 派发。
187303
+ *
187304
+ * 与 `fire()` 拆开是因为 event 触发必须「先原子占位、再决定派不派」(ADR 0163 §4.2 第 4/5 步):
187305
+ * 占位赢家才做权限/自环准入,输的那个直接停手。reconciler 重试也复用这里——同一 runId、
187306
+ * 同一下游命令键,取回同一副作用。
187307
+ */
187308
+ async dispatchRun(automation, trigger, run, source, opts = {}) {
187309
+ const skip = await this.admissionSkip(automation, source);
187145
187310
  if (skip) return this.settleSkip(run, skip.reasonCode, skip.error);
187146
187311
  if (automation.executionMode === "chat") {
187147
187312
  return this.fireChat(automation, trigger, run, opts);
@@ -187161,6 +187326,14 @@ var init_service8 = __esm({
187161
187326
  return settled ?? run;
187162
187327
  } catch (e) {
187163
187328
  const msg = e instanceof Error ? e.message : String(e);
187329
+ if (source === "event") {
187330
+ await this.store.settleRun(run.id, {
187331
+ status: "pending",
187332
+ result: { eventDispatch: { attempts: eventAttempts(run) + 1, lastAttemptAt: this.now().toISOString(), lastError: msg } },
187333
+ completedAt: null
187334
+ });
187335
+ return await this.store.getRun(run.id);
187336
+ }
187164
187337
  await this.store.settleRun(run.id, {
187165
187338
  status: "failed",
187166
187339
  error: msg,
@@ -187194,12 +187367,93 @@ var init_service8 = __esm({
187194
187367
  });
187195
187368
  return run;
187196
187369
  }
187197
- async admissionSkip(automation) {
187370
+ /* ---------- 订阅触发(event,ADR 0163) ---------- */
187371
+ /**
187372
+ * 能力位:这条自动化现在能不能用订阅触发。**唯一真值在这里**——详情响应把它原样带给前端,
187373
+ * 前端只消费不推断(否则前后端两套规则必漂移)。
187374
+ */
187375
+ eventTriggerCapability(automation) {
187376
+ if (automation.executionMode === "chat" && !this.deps.chatSupportsIdempotency) {
187377
+ return { supported: false, reason: "chat_no_idempotency" };
187378
+ }
187379
+ return { supported: true };
187380
+ }
187381
+ /**
187382
+ * 事件消费键占位:用同一把键原子落一条 run。
187383
+ * 唯一约束 `(automation_id, idempotency_key)` 负责多实例竞争——`created=false` 的那个实例停手。
187384
+ */
187385
+ claimEventRun(automation, trigger, key, envelope, status = "pending", reasonCode = null, error2 = null) {
187386
+ const nowIso = this.now().toISOString();
187387
+ return this.store.createRun({
187388
+ id: this.genId("arun"),
187389
+ automationId: automation.id,
187390
+ triggerId: trigger.id,
187391
+ source: "event",
187392
+ status,
187393
+ spawnedWorkspaceId: null,
187394
+ spawnedArtifactId: null,
187395
+ plannedAt: null,
187396
+ deliveryId: null,
187397
+ idempotencyKey: key,
187398
+ ruleVersionId: null,
187399
+ payload: envelope,
187400
+ result: null,
187401
+ error: error2,
187402
+ reasonCode,
187403
+ triggeredAt: nowIso,
187404
+ completedAt: status === "skipped" ? nowIso : null
187405
+ });
187406
+ }
187407
+ /** 把一条占位成功的 event run 派出去(准入 → 建单/开会话),命令键固定为 automation-event:<runId>。 */
187408
+ dispatchEventRun(automation, trigger, run) {
187409
+ return this.dispatchRun(automation, trigger, run, "event", {
187410
+ ...run.payload != null ? { envelope: run.payload } : {},
187411
+ commandKey: eventCommandKey(run.id)
187412
+ });
187413
+ }
187414
+ /** 已占位但被准入拒绝:落 skipped + reasonCode(**留痕**,不是静默丢弃)。 */
187415
+ skipEventRun(run, reasonCode, error2) {
187416
+ return this.settleSkip(run, reasonCode, error2);
187417
+ }
187418
+ /**
187419
+ * event pending 的可恢复派发(ADR 0163 §4.4)。
187420
+ *
187421
+ * **不走**「pending 超时即 failed」那条分支:那条规则会把「占位了但还没产生副作用」的 run 直接
187422
+ * 判死,等于永久漏一次触发。这里按稳定 runId 重试同一下游命令键,有界退避;
187423
+ * 达到重试上限的基础设施错误才落 failed 并告警(该 run 仍占着事件键,不自动制造第二次业务执行)。
187424
+ */
187425
+ async reconcileEventRun(run) {
187426
+ const attempts = eventAttempts(run);
187427
+ const nowMs = this.now().getTime();
187428
+ const lastAt = eventLastAttemptAt(run) ?? Date.parse(run.triggeredAt);
187429
+ if (attempts >= EVENT_RETRY_MAX_ATTEMPTS) {
187430
+ await this.store.settleRun(run.id, {
187431
+ status: "failed",
187432
+ error: `\u8BA2\u9605\u89E6\u53D1\u6D3E\u53D1\u91CD\u8BD5 ${attempts} \u6B21\u4ECD\u5931\u8D25\uFF1A${eventLastError(run) ?? "\u672A\u77E5\u9519\u8BEF"}`,
187433
+ reasonCode: "spawn_failed",
187434
+ completedAt: this.now().toISOString()
187435
+ });
187436
+ 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`);
187437
+ return;
187438
+ }
187439
+ const backoffMs = attempts === 0 ? 0 : Math.min(EVENT_RETRY_BASE_MS * 2 ** (attempts - 1), EVENT_RETRY_MAX_MS);
187440
+ if (nowMs - lastAt < backoffMs) return;
187441
+ const automation = await this.store.getAutomation(run.automationId);
187442
+ if (!automation) return;
187443
+ const trigger = run.triggerId ? await this.store.getTrigger(run.triggerId) : null;
187444
+ await this.dispatchEventRun(automation, trigger, run).catch((e) => {
187445
+ this.log?.(`[automations] \u8BA2\u9605\u89E6\u53D1\u91CD\u8BD5\u5F02\u5E38 ${run.id}\uFF1A${String(e)}`);
187446
+ });
187447
+ }
187448
+ async admissionSkip(automation, source = "manual") {
187198
187449
  if (automation.archivedAt) return { reasonCode: "automation_archived", error: "\u81EA\u52A8\u5316\u5DF2\u5F52\u6863" };
187199
187450
  if (!automation.enabled) return { reasonCode: "automation_paused", error: "\u81EA\u52A8\u5316\u5DF2\u6682\u505C" };
187200
187451
  if (automation.executionMode === "chat") {
187201
187452
  if (!automation.agentId) return { reasonCode: "config_error", error: "chat \u6A21\u5F0F\u7F3A\u5C11\u6267\u884C\u4EBA\uFF08agentId\uFF09" };
187202
187453
  if (!this.deps.runChat) return { reasonCode: "config_error", error: "\u672C\u5B9E\u4F8B\u672A\u63A5 chat \u6267\u884C" };
187454
+ if (source === "event" && !this.deps.chatSupportsIdempotency) {
187455
+ 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" };
187456
+ }
187203
187457
  if (this.deps.agentReady) {
187204
187458
  const ready = await this.deps.agentReady(automation.agentId);
187205
187459
  if (!ready.ok) return { reasonCode: ready.reason, error: ready.error };
@@ -187319,7 +187573,9 @@ var init_service8 = __esm({
187319
187573
  ...human ? { humanActorId: human } : {},
187320
187574
  ...tagId ? { tags: [tagId] } : {},
187321
187575
  // 成本安全(ADR 0010):dispatch=false(默认)→ 建单即设 dispatch-hold,待人放行。
187322
- ...automation.dispatch ? {} : { dispatch: false }
187576
+ ...automation.dispatch ? {} : { dispatch: false },
187577
+ // 稳定下游键(ADR 0163 §4.4):重试落在同一张工单上,占位后崩溃不会建出第二张。
187578
+ ...opts.commandKey ? { idempotencyKey: opts.commandKey } : {}
187323
187579
  });
187324
187580
  return { workspace: result.workspace, rootArtifactId: result.rootArtifactId };
187325
187581
  }
@@ -187362,6 +187618,10 @@ var init_service8 = __esm({
187362
187618
  continue;
187363
187619
  }
187364
187620
  if (run.status === "pending") {
187621
+ if (run.source === "event") {
187622
+ await this.reconcileEventRun(run);
187623
+ continue;
187624
+ }
187365
187625
  if (nowMs - Date.parse(run.triggeredAt) > 10 * 60 * 1e3) {
187366
187626
  await this.store.settleRun(run.id, {
187367
187627
  status: "failed",
@@ -187438,6 +187698,15 @@ var init_service8 = __esm({
187438
187698
  }
187439
187699
  }
187440
187700
  };
187701
+ EVENT_RETRY_BASE_MS = 3e4;
187702
+ EVENT_RETRY_MAX_MS = 10 * 6e4;
187703
+ EVENT_RETRY_MAX_ATTEMPTS = 5;
187704
+ eventAttempts = (run) => eventDispatchState(run).attempts ?? 0;
187705
+ eventLastAttemptAt = (run) => {
187706
+ const at = eventDispatchState(run).lastAttemptAt;
187707
+ return at ? Date.parse(at) : null;
187708
+ };
187709
+ eventLastError = (run) => eventDispatchState(run).lastError ?? null;
187441
187710
  CHAT_REPLY_MAX = 2e4;
187442
187711
  }
187443
187712
  });
@@ -187821,7 +188090,8 @@ function buildAutomationSummary(automation, triggers, recent, canWrite) {
187821
188090
  lastRun,
187822
188091
  nextRunAt,
187823
188092
  recent: ordered.slice(0, RECENT_LIMIT).map(stripRunPayload),
187824
- canWrite
188093
+ canWrite,
188094
+ capabilities: { eventTrigger: { supported: true } }
187825
188095
  };
187826
188096
  }
187827
188097
  function deriveNextRunAt(triggers) {
@@ -187867,10 +188137,12 @@ function validateTriggers(raw) {
187867
188137
  label: t.label ?? null,
187868
188138
  webhook: { provider, ...eventFilters !== void 0 ? { eventFilters } : {} }
187869
188139
  });
188140
+ } else if (t?.kind === "event") {
188141
+ out.push({ kind: "event", enabled: t.enabled ?? true, label: t.label ?? null, event: validateEventConfig(t.event) });
187870
188142
  } else if (t?.kind === "manual") {
187871
188143
  out.push({ kind: "manual", enabled: t.enabled ?? true, label: t.label ?? null });
187872
188144
  } else {
187873
- throw new ApiError(400, "BAD_REQUEST", "trigger.kind \u53EA\u80FD\u662F schedule\u3001webhook \u6216 manual");
188145
+ throw new ApiError(400, "BAD_REQUEST", "trigger.kind \u53EA\u80FD\u662F schedule\u3001webhook\u3001event \u6216 manual");
187874
188146
  }
187875
188147
  }
187876
188148
  return out;
@@ -187892,6 +188164,28 @@ function validateEventFilters(raw) {
187892
188164
  }
187893
188165
  return out;
187894
188166
  }
188167
+ function validateEventConfig(raw) {
188168
+ const cfg = raw ?? {};
188169
+ if (cfg.source !== void 0 && cfg.source !== "project") {
188170
+ 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`);
188171
+ }
188172
+ const projectId = typeof cfg.projectId === "string" ? cfg.projectId.trim() : "";
188173
+ if (!projectId) throw new ApiError(400, "BAD_REQUEST", "event \u89E6\u53D1\u5668\u9700\u8981 event.projectId");
188174
+ const events = normalizeEventNames(cfg.events);
188175
+ return { source: "project", projectId, events };
188176
+ }
188177
+ function normalizeEventNames(raw) {
188178
+ if (!Array.isArray(raw) || raw.length === 0) {
188179
+ throw new ApiError(400, "BAD_REQUEST", `event.events \u9700\u8981\u975E\u7A7A\u6570\u7EC4\uFF08\u53EF\u9009\u503C\uFF1A${AUTOMATION_EVENT_NAMES.join(", ")}\uFF09`);
188180
+ }
188181
+ const known = new Set(AUTOMATION_EVENT_NAMES);
188182
+ for (const e of raw) {
188183
+ if (typeof e !== "string" || !known.has(e)) {
188184
+ 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`);
188185
+ }
188186
+ }
188187
+ return AUTOMATION_EVENT_NAMES.filter((n) => raw.includes(n));
188188
+ }
187895
188189
  function assertValidCron(cron, tz) {
187896
188190
  const v2 = validateCron(cron, tz);
187897
188191
  if (!v2.ok) throw new ApiError(400, v2.code, v2.message);
@@ -187954,13 +188248,66 @@ function automationsDomain(opts) {
187954
188248
  throw new ApiError(403, "NOT_ALLOWED", "\u53EA\u6709\u521B\u5EFA\u8005\u6216\u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u534F\u4F5C\u8005");
187955
188249
  }
187956
188250
  };
188251
+ const decorateEventTriggers = async (automation, triggers, req) => {
188252
+ if (!triggers.some((t) => t.kind === "event")) return triggers;
188253
+ const cache = /* @__PURE__ */ new Map();
188254
+ const out = [];
188255
+ for (const t of triggers) {
188256
+ if (t.kind !== "event" || !t.event) {
188257
+ out.push(t);
188258
+ continue;
188259
+ }
188260
+ const pid = t.event.projectId;
188261
+ if (!cache.has(pid)) {
188262
+ cache.set(pid, await opts.projectAccess?.({ companyId: companyOf(req), projectId: pid, actor: req.auth.actor }) ?? null);
188263
+ }
188264
+ const project = cache.get(pid) ?? null;
188265
+ out.push({
188266
+ ...t,
188267
+ event: { ...t.event, projectAvailable: project !== null, projectName: project?.name ?? null }
188268
+ });
188269
+ }
188270
+ return out;
188271
+ };
187957
188272
  const summarize3 = async (automation, req) => {
187958
188273
  const [triggers, recent, canWrite] = await Promise.all([
187959
188274
  service.listTriggers(automation.id),
187960
188275
  service.listRuns(automation.id, { limit: RECENT_LIMIT2 }),
187961
188276
  canWriteOf(automation, req)
187962
188277
  ]);
187963
- return buildAutomationSummary(automation, triggers, recent, canWrite);
188278
+ const decorated = await decorateEventTriggers(automation, triggers, req);
188279
+ return {
188280
+ ...buildAutomationSummary(automation, decorated, recent, canWrite),
188281
+ // 能力位服务端算(ADR 0163 §10):前端只消费不推断,否则前后端两套规则必漂移。
188282
+ capabilities: { eventTrigger: service.eventTriggerCapability(automation) }
188283
+ };
188284
+ };
188285
+ const assertEventTriggerWritable = async (mode, cfg, existing, req) => {
188286
+ if (!service.eventTriggerCapability({ executionMode: mode }).supported) {
188287
+ 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");
188288
+ }
188289
+ const project = await opts.projectAccess?.({ companyId: companyOf(req), projectId: cfg.projectId, actor: req.auth.actor });
188290
+ if (!project) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u9879\u76EE\uFF1A${cfg.projectId}`);
188291
+ for (const other of existing) {
188292
+ if (other.projectId !== cfg.projectId) continue;
188293
+ const dup = cfg.events.find((e) => other.events.includes(e));
188294
+ if (dup) throw new ApiError(409, "DUPLICATE_SUBSCRIPTION", `\u8BE5\u9879\u76EE\u7684\u8FD9\u4E2A\u4E8B\u4EF6\u5DF2\u8BA2\u9605\uFF1A${dup}`);
188295
+ }
188296
+ };
188297
+ const assertEventTriggerInputs = async (mode, inputs, kept, req) => {
188298
+ const seen = [...kept];
188299
+ for (const input of inputs ?? []) {
188300
+ if (input.kind !== "event" || !input.event) continue;
188301
+ await assertEventTriggerWritable(mode, input.event, seen, req);
188302
+ seen.push(input.event);
188303
+ }
188304
+ };
188305
+ const assertModeKeepsEventTriggers = async (automation, effMode) => {
188306
+ if (service.eventTriggerCapability({ ...automation, executionMode: effMode }).supported) return;
188307
+ const hasEvent = (await service.listTriggers(automation.id)).some((t) => t.kind === "event");
188308
+ if (hasEvent) {
188309
+ 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");
188310
+ }
187964
188311
  };
187965
188312
  return (router) => {
187966
188313
  router.get("/api/automations/templates", async () => {
@@ -188027,6 +188374,7 @@ function automationsDomain(opts) {
188027
188374
  assertTitleTemplate(b2.titleTemplate);
188028
188375
  const subscribers = assertActorIds(b2.subscribers, "subscribers");
188029
188376
  const triggers = validateTriggers(b2.triggers);
188377
+ await assertEventTriggerInputs(mode, triggers, [], req);
188030
188378
  const created = await service.createAutomation({
188031
188379
  companyId: companyOf(req),
188032
188380
  name: b2.name,
@@ -188060,6 +188408,9 @@ function automationsDomain(opts) {
188060
188408
  assertTitleTemplate(b2.titleTemplate);
188061
188409
  const subscribers = assertActorIds(b2.subscribers, "subscribers");
188062
188410
  const triggers = validateTriggers(b2.triggers);
188411
+ const kept = triggers !== void 0 ? [] : (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.event).map((t) => t.event);
188412
+ await assertEventTriggerInputs(effMode, triggers, kept, req);
188413
+ if (triggers === void 0) await assertModeKeepsEventTriggers(automation, effMode);
188063
188414
  const updated = await service.updateAutomation(
188064
188415
  automation.id,
188065
188416
  {
@@ -188108,6 +188459,8 @@ function automationsDomain(opts) {
188108
188459
  const automation = await mustGet(req);
188109
188460
  await requireWrite(automation, req);
188110
188461
  const inputs = validateTriggers([req.body]) ?? [];
188462
+ const kept = (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.event).map((t) => t.event);
188463
+ await assertEventTriggerInputs(automation.executionMode, inputs, kept, req);
188111
188464
  const trigger = await service.addTrigger(automation.id, inputs[0], req.auth.actor);
188112
188465
  return { status: 201, body: trigger };
188113
188466
  });
@@ -188137,6 +188490,17 @@ function automationsDomain(opts) {
188137
188490
  const eventFilters = validateEventFilters(b2.webhook.eventFilters);
188138
188491
  if (eventFilters !== void 0) patch.webhook = { eventFilters };
188139
188492
  }
188493
+ if (b2.event !== void 0) {
188494
+ if (existing.kind !== "event") throw new ApiError(400, "BAD_REQUEST", "\u975E event \u89E6\u53D1\u5668\u4E0D\u63A5\u53D7 event \u5B57\u6BB5");
188495
+ const raw = b2.event;
188496
+ if (raw["projectId"] !== void 0 || raw["source"] !== void 0) {
188497
+ 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");
188498
+ }
188499
+ const events = normalizeEventNames(raw["events"]);
188500
+ const kept = (await service.listTriggers(automation.id)).filter((t) => t.kind === "event" && t.id !== existing.id && t.event).map((t) => t.event);
188501
+ await assertEventTriggerWritable(automation.executionMode, { projectId: existing.event.projectId, events }, kept, req);
188502
+ patch.event = { events };
188503
+ }
188140
188504
  const updated = await service.updateTrigger(existing.id, patch, req.auth.actor);
188141
188505
  return { status: 200, body: updated };
188142
188506
  });
@@ -188218,6 +188582,7 @@ var RECENT_LIMIT2;
188218
188582
  var init_routes9 = __esm({
188219
188583
  "../server/src/domains/automations/routes.ts"() {
188220
188584
  "use strict";
188585
+ init_src2();
188221
188586
  init_templates();
188222
188587
  init_router();
188223
188588
  init_service8();
@@ -188230,17 +188595,25 @@ var init_routes9 = __esm({
188230
188595
  });
188231
188596
 
188232
188597
  // ../server/src/domains/automations/workorder-state.ts
188233
- function makeAutomationWorkorderStateResolver(kernel) {
188598
+ function makeAutomationWorkorderSnapshotResolver(kernel) {
188234
188599
  return async (workspace) => {
188235
188600
  const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === workspace);
188236
- if (arts.length === 0) return "unknown";
188601
+ if (arts.length === 0) return { state: "unknown", members: [], rootArtifactId: null };
188237
188602
  const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
188238
- if (lifecycleOf(kernel.model, seed.id) === "sealed:cancelled") return "cancelled";
188603
+ 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);
188604
+ const rootArtifactId = String(seed.id);
188605
+ if (lifecycleOf(kernel.model, seed.id) === "sealed:cancelled") {
188606
+ return { state: "cancelled", members, rootArtifactId };
188607
+ }
188239
188608
  const stage = deriveStage(arts.map((a) => nodeInfo(kernel.model, a)));
188240
- if (stage === "concluded" || stage === "sealed") return "done";
188241
- return "active";
188609
+ if (stage === "concluded" || stage === "sealed") return { state: "done", members, rootArtifactId };
188610
+ return { state: "active", members, rootArtifactId };
188242
188611
  };
188243
188612
  }
188613
+ function makeAutomationWorkorderStateResolver(kernel) {
188614
+ const snapshot = makeAutomationWorkorderSnapshotResolver(kernel);
188615
+ return async (workspace) => (await snapshot(workspace)).state;
188616
+ }
188244
188617
  function makeAutomationReplyResolver(kernel, blobs) {
188245
188618
  return async (workspace) => {
188246
188619
  const model = kernel.model;
@@ -188304,7 +188677,12 @@ function createAutomationsDomain(opts) {
188304
188677
  return {
188305
188678
  service,
188306
188679
  webhook,
188307
- register: automationsDomain({ service, webhook, ...opts.isAdmin ? { isAdmin: opts.isAdmin } : {} })
188680
+ register: automationsDomain({
188681
+ service,
188682
+ webhook,
188683
+ ...opts.isAdmin ? { isAdmin: opts.isAdmin } : {},
188684
+ ...opts.projectAccess ? { projectAccess: opts.projectAccess } : {}
188685
+ })
188308
188686
  };
188309
188687
  }
188310
188688
  var init_automations = __esm({
@@ -188596,6 +188974,210 @@ var init_playbooks = __esm({
188596
188974
  }
188597
188975
  });
188598
188976
 
188977
+ // ../server/src/automations/project-event-poller.ts
188978
+ function completionFingerprint(workspace, members) {
188979
+ const canonical = JSON.stringify({
188980
+ workspace,
188981
+ 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 }))
188982
+ });
188983
+ return (0, import_node_crypto39.createHash)("sha256").update(canonical).digest("hex");
188984
+ }
188985
+ function eventConsumptionKey(args) {
188986
+ const raw = `project-event:${EVENT_KEY_VERSION}:${args.triggerId}:${args.event}:${args.workspace}:${args.fingerprint}`;
188987
+ return raw.length <= 200 ? raw : `project-event:${EVENT_KEY_VERSION}:sha256:${(0, import_node_crypto39.createHash)("sha256").update(raw).digest("hex")}`;
188988
+ }
188989
+ var import_node_crypto39, EVENT_KEY_VERSION, EMPTY_METRICS, AUTOMATION_EVENT_HANDLERS, ProjectEventPoller;
188990
+ var init_project_event_poller = __esm({
188991
+ "../server/src/automations/project-event-poller.ts"() {
188992
+ "use strict";
188993
+ import_node_crypto39 = require("node:crypto");
188994
+ EVENT_KEY_VERSION = "v1";
188995
+ EMPTY_METRICS = () => ({
188996
+ scanDurationMs: 0,
188997
+ projectsScanned: 0,
188998
+ workordersScanned: 0,
188999
+ newEvents: 0,
189000
+ duplicateEvents: 0,
189001
+ skippedEvents: 0,
189002
+ baselineEvents: 0,
189003
+ pollErrors: 0,
189004
+ oldestPendingAgeMs: null
189005
+ });
189006
+ AUTOMATION_EVENT_HANDLERS = {
189007
+ // 完成判据必须来自 D3 resolver(snapshot.state),不在这里复制 deriveStage。
189008
+ // active / unknown 是「还没完成」,cancelled 永不产生 completed——四态里只有 done 触发。
189009
+ "workorder.completed": {
189010
+ matches: (snapshot) => snapshot.state === "done",
189011
+ fingerprint: (workspace, snapshot) => completionFingerprint(workspace, snapshot.members)
189012
+ }
189013
+ };
189014
+ ProjectEventPoller = class {
189015
+ deps;
189016
+ now;
189017
+ log;
189018
+ batchLimit;
189019
+ inFlight = false;
189020
+ lastMetrics = EMPTY_METRICS();
189021
+ constructor(deps) {
189022
+ this.deps = deps;
189023
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
189024
+ if (deps.log) this.log = deps.log;
189025
+ this.batchLimit = deps.batchLimit ?? 200;
189026
+ }
189027
+ metrics() {
189028
+ return { ...this.lastMetrics };
189029
+ }
189030
+ /** 一轮扫描。单实例用 inFlight 防重入;多实例无需选主——并发由数据库唯一键裁决。 */
189031
+ async tick() {
189032
+ if (this.inFlight) return this.lastMetrics;
189033
+ this.inFlight = true;
189034
+ const startedAt = this.now().getTime();
189035
+ const m2 = EMPTY_METRICS();
189036
+ try {
189037
+ const triggers = await this.deps.store.listEventTriggers();
189038
+ if (triggers.length === 0) return this.lastMetrics = { ...m2, scanDurationMs: this.now().getTime() - startedAt };
189039
+ const snapshots2 = /* @__PURE__ */ new Map();
189040
+ let budget = this.batchLimit;
189041
+ for (const trigger of triggers) {
189042
+ if (budget <= 0) break;
189043
+ const config2 = trigger.event;
189044
+ if (!config2?.projectId || config2.events.length === 0) continue;
189045
+ const automation = await this.deps.store.getAutomation(trigger.automationId);
189046
+ if (!automation) continue;
189047
+ const groupKey = `${automation.companyId}\0${config2.projectId}`;
189048
+ let group = snapshots2.get(groupKey);
189049
+ if (!group) {
189050
+ try {
189051
+ group = await this.scanProject(automation.companyId, config2.projectId);
189052
+ m2.projectsScanned += 1;
189053
+ m2.workordersScanned += group.length;
189054
+ } catch (e) {
189055
+ m2.pollErrors += 1;
189056
+ this.log?.(`[project-event-poller] \u626B\u63CF\u9879\u76EE\u5931\u8D25 ${config2.projectId}\uFF1A${String(e)}`);
189057
+ group = [];
189058
+ }
189059
+ snapshots2.set(groupKey, group);
189060
+ }
189061
+ try {
189062
+ budget -= await this.evaluateTrigger(automation, trigger, group, m2, budget);
189063
+ } catch (e) {
189064
+ m2.pollErrors += 1;
189065
+ this.log?.(`[project-event-poller] \u89E6\u53D1\u5668\u6C42\u503C\u5931\u8D25 ${trigger.id}\uFF1A${String(e)}`);
189066
+ }
189067
+ }
189068
+ m2.oldestPendingAgeMs = await this.oldestPendingAge();
189069
+ } catch (e) {
189070
+ m2.pollErrors += 1;
189071
+ this.log?.(`[project-event-poller] tick \u5931\u8D25\uFF1A${String(e)}`);
189072
+ } finally {
189073
+ m2.scanDurationMs = this.now().getTime() - startedAt;
189074
+ this.lastMetrics = m2;
189075
+ this.inFlight = false;
189076
+ }
189077
+ if (m2.newEvents || m2.skippedEvents || m2.baselineEvents || m2.pollErrors) {
189078
+ this.log?.(
189079
+ `[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`
189080
+ );
189081
+ }
189082
+ return this.lastMetrics;
189083
+ }
189084
+ async scanProject(companyId, projectId) {
189085
+ const workspaces = await this.deps.listProjectWorkspaces({ companyId, projectId });
189086
+ const out = [];
189087
+ for (const workspace of workspaces) {
189088
+ out.push({ workspace, snapshot: await this.deps.workorderSnapshot({ companyId, workspace }) });
189089
+ }
189090
+ return out;
189091
+ }
189092
+ /** 返回本触发器消耗掉的预算条数。 */
189093
+ async evaluateTrigger(automation, trigger, candidates, m2, budget) {
189094
+ const config2 = trigger.event;
189095
+ const baselining = config2.baselineState !== "ready";
189096
+ let used = 0;
189097
+ for (const event of config2.events) {
189098
+ const handler = AUTOMATION_EVENT_HANDLERS[event];
189099
+ if (!handler) continue;
189100
+ for (const { workspace, snapshot } of candidates) {
189101
+ if (!handler.matches(snapshot)) continue;
189102
+ if (used >= budget) return used;
189103
+ const fingerprint = handler.fingerprint(workspace, snapshot);
189104
+ const key = eventConsumptionKey({ triggerId: trigger.id, event, workspace, fingerprint });
189105
+ const envelope = {
189106
+ event,
189107
+ companyId: automation.companyId,
189108
+ projectId: config2.projectId,
189109
+ workspace,
189110
+ rootArtifactId: snapshot.rootArtifactId,
189111
+ completionFingerprint: fingerprint,
189112
+ observedAt: this.now().toISOString(),
189113
+ ...await this.originAutomationOf(workspace)
189114
+ };
189115
+ used += 1;
189116
+ if (baselining) {
189117
+ const { created: created2 } = await this.deps.service.claimEventRun(
189118
+ automation,
189119
+ trigger,
189120
+ key,
189121
+ envelope,
189122
+ "skipped",
189123
+ "subscription_baseline",
189124
+ "\u8BA2\u9605\u57FA\u7EBF\u5BF9\u9F50\uFF1A\u8BA2\u9605\u751F\u6548\u524D\u5DF2\u5B8C\u6210\uFF0C\u4E0D\u56DE\u653E\u5386\u53F2"
189125
+ );
189126
+ if (created2) m2.baselineEvents += 1;
189127
+ else m2.duplicateEvents += 1;
189128
+ continue;
189129
+ }
189130
+ const { run, created } = await this.deps.service.claimEventRun(automation, trigger, key, envelope);
189131
+ if (!created) {
189132
+ m2.duplicateEvents += 1;
189133
+ continue;
189134
+ }
189135
+ const denial = await this.admit(automation, trigger, envelope);
189136
+ if (denial) {
189137
+ await this.deps.service.skipEventRun(run, denial.reasonCode, denial.error);
189138
+ m2.skippedEvents += 1;
189139
+ continue;
189140
+ }
189141
+ await this.deps.service.dispatchEventRun(automation, trigger, run);
189142
+ m2.newEvents += 1;
189143
+ }
189144
+ }
189145
+ if (baselining) {
189146
+ await this.deps.store.completeEventBaseline(trigger.id, this.now().toISOString());
189147
+ }
189148
+ return used;
189149
+ }
189150
+ /** 权限复查 + 自环准入。两条都只在**占位成功之后**做,因为拒绝也要留痕在同一把键上。 */
189151
+ async admit(automation, trigger, envelope) {
189152
+ if (envelope.originAutomationId && envelope.originAutomationId === automation.id) {
189153
+ 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" };
189154
+ }
189155
+ const accountable = trigger.publishedBy ?? (await this.deps.store.latestRuleVersion(automation.id))?.publishedBy ?? automation.createdBy ?? null;
189156
+ const readable = await this.deps.projectReadable({
189157
+ companyId: automation.companyId,
189158
+ projectId: envelope.projectId,
189159
+ actor: accountable
189160
+ });
189161
+ if (!readable) {
189162
+ return { reasonCode: "project_access_revoked", error: `\u8BA2\u9605\u9879\u76EE\u5DF2\u4E0D\u53EF\u8BBF\u95EE\uFF1A${envelope.projectId}` };
189163
+ }
189164
+ return null;
189165
+ }
189166
+ async originAutomationOf(workspace) {
189167
+ const origin = await this.deps.store.findRunBySpawnedWorkspace(workspace).catch(() => null);
189168
+ return origin ? { originAutomationId: origin.automationId } : {};
189169
+ }
189170
+ async oldestPendingAge() {
189171
+ const unsettled = await this.deps.store.listUnsettledRuns(50).catch(() => []);
189172
+ const pending = unsettled.filter((r) => r.source === "event" && r.status === "pending");
189173
+ if (pending.length === 0) return null;
189174
+ const oldest = Math.min(...pending.map((r) => Date.parse(r.triggeredAt)));
189175
+ return this.now().getTime() - oldest;
189176
+ }
189177
+ };
189178
+ }
189179
+ });
189180
+
188599
189181
  // ../server/src/daemon-adapter.ts
188600
189182
  function graceFromEnv(fallback) {
188601
189183
  const v2 = process.env["OASIS_NODE_LOST_GRACE_MS"];
@@ -188623,11 +189205,11 @@ function remoteAppendInput(hub, nodeId, dispatchId, entry, input, timeoutMs) {
188623
189205
  entry.appendWaiters.push({ resolve: resolve10, timer });
188624
189206
  });
188625
189207
  }
188626
- var import_node_crypto39, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
189208
+ var import_node_crypto40, STASH_TTL_MS, STASH_MAX_FRAMES_PER_ID, STASH_MAX_IDS, DaemonHubAdapter, BindingRouterAdapter, WorkdirBridge;
188627
189209
  var init_daemon_adapter = __esm({
188628
189210
  "../server/src/daemon-adapter.ts"() {
188629
189211
  "use strict";
188630
- import_node_crypto39 = require("node:crypto");
189212
+ import_node_crypto40 = require("node:crypto");
188631
189213
  STASH_TTL_MS = 5 * 6e4;
188632
189214
  STASH_MAX_FRAMES_PER_ID = 500;
188633
189215
  STASH_MAX_IDS = 50;
@@ -188823,7 +189405,7 @@ var init_daemon_adapter = __esm({
188823
189405
  async spawn(job) {
188824
189406
  const nodeId = job.binding?.nodeId;
188825
189407
  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)()}`;
189408
+ const dispatchId = job.dispatchId ?? `dispatch:${(0, import_node_crypto40.randomUUID)()}`;
188827
189409
  const entry = {
188828
189410
  nodeId,
188829
189411
  ...job.binding?.runtimeKind ? { runtimeKind: job.binding.runtimeKind } : {},
@@ -188979,7 +189561,7 @@ var init_daemon_adapter = __esm({
188979
189561
  }));
188980
189562
  }
188981
189563
  request(nodeId, buildFrame) {
188982
- const requestId = (0, import_node_crypto39.randomUUID)();
189564
+ const requestId = (0, import_node_crypto40.randomUUID)();
188983
189565
  const sent = this.hub.dispatch(nodeId, buildFrame(requestId));
188984
189566
  if (!sent) return Promise.resolve({ ok: false, code: "NODE_OFFLINE" });
188985
189567
  return new Promise((resolve10) => {
@@ -189087,13 +189669,13 @@ function classifyPage(page, lastPushedHash, serviceAccount) {
189087
189669
  if (sha(body) === lastPushedHash) return { kind: "echo" };
189088
189670
  return { kind: "human-edit", content: body, updatedBy: page.updatedBy };
189089
189671
  }
189090
- var import_node_crypto40, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
189672
+ var import_node_crypto41, sha, enc3, dec, MirrorEngine, MapMirrorIdentities;
189091
189673
  var init_engine = __esm({
189092
189674
  "../server/src/mirror/engine.ts"() {
189093
189675
  "use strict";
189094
- import_node_crypto40 = require("node:crypto");
189676
+ import_node_crypto41 = require("node:crypto");
189095
189677
  init_src2();
189096
- sha = (s2) => (0, import_node_crypto40.createHash)("sha256").update(s2, "utf8").digest("hex");
189678
+ sha = (s2) => (0, import_node_crypto41.createHash)("sha256").update(s2, "utf8").digest("hex");
189097
189679
  enc3 = (s2) => new TextEncoder().encode(s2);
189098
189680
  dec = (b2) => new TextDecoder().decode(b2);
189099
189681
  MirrorEngine = class {
@@ -189300,11 +189882,11 @@ function humanExitCause(exit) {
189300
189882
  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
189883
  return exit.errorMessage ? `${base}\uFF1A${exit.errorMessage.slice(0, 200)}` : base;
189302
189884
  }
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;
189885
+ 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
189886
  var init_worker = __esm({
189305
189887
  "../server/src/coordinator/worker.ts"() {
189306
189888
  "use strict";
189307
- import_node_crypto41 = require("node:crypto");
189889
+ import_node_crypto42 = require("node:crypto");
189308
189890
  init_src5();
189309
189891
  init_identity();
189310
189892
  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 +190133,7 @@ ${HIGH_RISK_COMMANDS}`;
189551
190133
  return this.deps.kernel.model.lastSeq.get(artifactId) ?? 0;
189552
190134
  }
189553
190135
  evaluationKey(kind, artifactId, evidence) {
189554
- const fingerprint = (0, import_node_crypto41.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
190136
+ const fingerprint = (0, import_node_crypto42.createHash)("sha256").update(JSON.stringify(evidence)).digest("hex");
189555
190137
  return `${kind}:${artifactId}:${fingerprint}`;
189556
190138
  }
189557
190139
  artifactEvidence(id) {
@@ -189928,8 +190510,8 @@ ${ctx.nodeFault}
189928
190510
  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
190511
  }
189930
190512
  }
189931
- const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto41.randomUUID)();
189932
- const coordinatorRunId = `coordinate:${(0, import_node_crypto41.randomUUID)()}`;
190513
+ const runtimeSessionId = opts?.resumeSessionId ?? (0, import_node_crypto42.randomUUID)();
190514
+ const coordinatorRunId = `coordinate:${(0, import_node_crypto42.randomUUID)()}`;
189933
190515
  const taskWithContext = coordinatorContext ? [
189934
190516
  task,
189935
190517
  ``,
@@ -189970,7 +190552,7 @@ ${ctx.nodeFault}
189970
190552
  const startedAtMs = Date.now();
189971
190553
  let trajectoryStarted = false;
189972
190554
  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")]));
190555
+ const bundleManifest = Object.fromEntries(Object.entries(job.bundle.files).map(([name, content]) => [name, (0, import_node_crypto42.createHash)("sha256").update(content).digest("hex")]));
189974
190556
  const session = {
189975
190557
  runId: coordinatorRunId,
189976
190558
  runtimeSessionId,
@@ -190371,6 +190953,7 @@ var init_src10 = __esm({
190371
190953
  init_playbooks();
190372
190954
  init_overrides();
190373
190955
  init_scheduler();
190956
+ init_project_event_poller();
190374
190957
  init_daemon_adapter();
190375
190958
  init_dispatch_fence();
190376
190959
  init_node_health();
@@ -190391,11 +190974,11 @@ var init_src10 = __esm({
190391
190974
  });
190392
190975
 
190393
190976
  // ../storage/src/postgres.ts
190394
- var import_node_crypto42, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
190977
+ var import_node_crypto43, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
190395
190978
  var init_postgres = __esm({
190396
190979
  "../storage/src/postgres.ts"() {
190397
190980
  "use strict";
190398
- import_node_crypto42 = require("node:crypto");
190981
+ import_node_crypto43 = require("node:crypto");
190399
190982
  init_esm();
190400
190983
  init_src2();
190401
190984
  ident3 = (s2) => {
@@ -190574,7 +191157,7 @@ var init_postgres = __esm({
190574
191157
  return new _PostgresBlobStore(pool, schema);
190575
191158
  }
190576
191159
  async put(bytes) {
190577
- const hash = (0, import_node_crypto42.createHash)("sha256").update(bytes).digest("hex");
191160
+ const hash = (0, import_node_crypto43.createHash)("sha256").update(bytes).digest("hex");
190578
191161
  await this.pool.query(
190579
191162
  `INSERT INTO ${this.t} (hash, bytes, size, content_type) VALUES ($1, $2, $3, $4) ON CONFLICT (hash) DO NOTHING`,
190580
191163
  [hash, Buffer.from(bytes), bytes.byteLength, sniffContentType(bytes) ?? null]
@@ -194843,6 +195426,14 @@ var init_postgres_trace = __esm({
194843
195426
  });
194844
195427
 
194845
195428
  // ../storage/src/postgres-automations.ts
195429
+ function storedEventConfig(t) {
195430
+ return {
195431
+ source: "project",
195432
+ projectId: t.event?.projectId ?? "",
195433
+ events: t.event?.events ?? [],
195434
+ baselineAt: null
195435
+ };
195436
+ }
194846
195437
  function buildTrigger(automationId, input) {
194847
195438
  const t = {
194848
195439
  id: genId("trig"),
@@ -194863,6 +195454,10 @@ function buildTrigger(automationId, input) {
194863
195454
  hasSigningSecret: false
194864
195455
  };
194865
195456
  }
195457
+ if (input.kind === "event" && input.event) {
195458
+ t.enabled = false;
195459
+ t.event = { source: "project", projectId: input.event.projectId, events: [...input.event.events], baselineState: "aligning" };
195460
+ }
194866
195461
  return t;
194867
195462
  }
194868
195463
  function genWebhookToken2() {
@@ -195166,7 +195761,8 @@ var init_postgres_automations = __esm({
195166
195761
  t.schedule?.claimedAt ?? null,
195167
195762
  t.webhook?.token ?? null,
195168
195763
  t.webhook?.provider ?? null,
195169
- t.webhook?.eventFilters != null ? JSON.stringify(t.webhook.eventFilters) : null
195764
+ // event_filters 一列两用(ADR 0163 §3.2:不新增列):webhook 存过滤数组,event 存订阅配置对象。
195765
+ t.kind === "event" ? JSON.stringify(storedEventConfig(t)) : t.webhook?.eventFilters != null ? JSON.stringify(t.webhook.eventFilters) : null
195170
195766
  ]
195171
195767
  );
195172
195768
  }
@@ -195212,6 +195808,10 @@ var init_postgres_automations = __esm({
195212
195808
  if (patch.webhook && "eventFilters" in patch.webhook) {
195213
195809
  push("event_filters", patch.webhook.eventFilters != null ? JSON.stringify(patch.webhook.eventFilters) : null, "::jsonb");
195214
195810
  }
195811
+ if (patch.event?.events) {
195812
+ args.push(JSON.stringify(patch.event.events));
195813
+ sets.push(`event_filters=jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{events}', $${args.length}::jsonb, true)`);
195814
+ }
195215
195815
  if (sets.length === 0) return await this.getTrigger(triggerId);
195216
195816
  args.push(triggerId);
195217
195817
  const r = await this.pool.query(
@@ -195498,6 +196098,42 @@ var init_postgres_automations = __esm({
195498
196098
  );
195499
196099
  return r.rows.map(rowToTrigger);
195500
196100
  }
196101
+ /* ---------- event 触发器(ADR 0163) ---------- */
196102
+ async listEventTriggers() {
196103
+ const r = await this.pool.query(
196104
+ `SELECT t.* FROM ${this.s}.automation_triggers t
196105
+ JOIN ${this.s}.automations a ON a.id = t.automation_id
196106
+ WHERE t.kind = 'event' AND a.enabled AND a.archived_at IS NULL
196107
+ ORDER BY t.id`
196108
+ );
196109
+ return r.rows.map(rowToTrigger);
196110
+ }
196111
+ async completeEventBaseline(triggerId, at) {
196112
+ await this.pool.query(
196113
+ `UPDATE ${this.s}.automation_triggers
196114
+ SET event_filters = jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{baselineAt}', to_jsonb($2::text), true),
196115
+ enabled = true
196116
+ WHERE id = $1 AND kind = 'event'`,
196117
+ [triggerId, at]
196118
+ );
196119
+ }
196120
+ async resetEventBaseline(triggerId) {
196121
+ await this.pool.query(
196122
+ `UPDATE ${this.s}.automation_triggers
196123
+ SET event_filters = jsonb_set(COALESCE(event_filters, '{}'::jsonb), '{baselineAt}', 'null'::jsonb, true),
196124
+ enabled = false
196125
+ WHERE id = $1 AND kind = 'event'`,
196126
+ [triggerId]
196127
+ );
196128
+ }
196129
+ async findRunBySpawnedWorkspace(workspace) {
196130
+ const r = await this.pool.query(
196131
+ `SELECT * FROM ${this.s}.automation_runs WHERE spawned_workspace_id = $1
196132
+ ORDER BY triggered_at ASC, id ASC LIMIT 1`,
196133
+ [workspace]
196134
+ );
196135
+ return r.rows[0] ? rowToRun2(r.rows[0]) : null;
196136
+ }
195501
196137
  };
195502
196138
  rowToAutomation = (row) => ({
195503
196139
  id: row.id,
@@ -195546,6 +196182,15 @@ var init_postgres_automations = __esm({
195546
196182
  hasSigningSecret: row.signing_secret != null
195547
196183
  };
195548
196184
  }
196185
+ if (t.kind === "event") {
196186
+ const raw = row.event_filters ?? null;
196187
+ t.event = {
196188
+ source: "project",
196189
+ projectId: raw?.projectId ?? "",
196190
+ events: raw?.events ?? [],
196191
+ baselineState: raw?.baselineAt ? "ready" : "aligning"
196192
+ };
196193
+ }
195549
196194
  return t;
195550
196195
  };
195551
196196
  rowToRun2 = (row) => ({
@@ -195909,11 +196554,11 @@ var init_postgres_chat_sessions = __esm({
195909
196554
  });
195910
196555
 
195911
196556
  // ../storage/src/postgres-nodes.ts
195912
- var import_node_crypto43, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
196557
+ var import_node_crypto44, ident12, PostgresNodeStore, PostgresNodeTokenStore, rowToNode, rowToRuntime;
195913
196558
  var init_postgres_nodes = __esm({
195914
196559
  "../storage/src/postgres-nodes.ts"() {
195915
196560
  "use strict";
195916
- import_node_crypto43 = require("node:crypto");
196561
+ import_node_crypto44 = require("node:crypto");
195917
196562
  init_esm();
195918
196563
  ident12 = (s2) => {
195919
196564
  if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
@@ -196062,7 +196707,7 @@ var init_postgres_nodes = __esm({
196062
196707
  return store;
196063
196708
  }
196064
196709
  issue(nodeId) {
196065
- const token = `ont_${(0, import_node_crypto43.randomBytes)(24).toString("base64url")}`;
196710
+ const token = `ont_${(0, import_node_crypto44.randomBytes)(24).toString("base64url")}`;
196066
196711
  this.cache.set(token, nodeId);
196067
196712
  void this.pool.query(`INSERT INTO ${this.s}.node_tokens (token,node_id) VALUES ($1,$2)`, [token, nodeId]);
196068
196713
  return token;
@@ -196274,11 +196919,11 @@ var init_postgres_type_registry = __esm({
196274
196919
  });
196275
196920
 
196276
196921
  // ../storage/src/postgres-actor-memory.ts
196277
- var import_node_crypto44, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
196922
+ var import_node_crypto45, matchClause, ident15, PostgresActorMemoryStore, rowToIndexEntry, rowToRecord;
196278
196923
  var init_postgres_actor_memory = __esm({
196279
196924
  "../storage/src/postgres-actor-memory.ts"() {
196280
196925
  "use strict";
196281
- import_node_crypto44 = require("node:crypto");
196926
+ import_node_crypto45 = require("node:crypto");
196282
196927
  init_src2();
196283
196928
  matchClause = (q) => q.requireMatch && q.keywords.length > 0 ? " WHERE m > 0" : "";
196284
196929
  ident15 = (s2) => {
@@ -196483,7 +197128,7 @@ var init_postgres_actor_memory = __esm({
196483
197128
  }
196484
197129
  async write(input, now) {
196485
197130
  if (input.memId === void 0) {
196486
- const memId = `mem:${(0, import_node_crypto44.randomUUID)()}`;
197131
+ const memId = `mem:${(0, import_node_crypto45.randomUUID)()}`;
196487
197132
  const r2 = await this.pool.query(
196488
197133
  `INSERT INTO ${this.s}.actor_memories
196489
197134
  (mem_id, actor_id, project_id, keywords, content, version, created_at, updated_at, accessed_at, source_artifact_id, source_session_id)
@@ -196996,19 +197641,20 @@ var fs38 = __toESM(require("node:fs"));
196996
197641
  var os13 = __toESM(require("node:os"));
196997
197642
  var path32 = __toESM(require("node:path"));
196998
197643
  var import_node_child_process19 = require("node:child_process");
197644
+ var import_node_crypto52 = require("node:crypto");
196999
197645
 
197000
197646
  // ../cli/src/cli.ts
197001
197647
  var fs34 = __toESM(require("node:fs"), 1);
197002
197648
  var os11 = __toESM(require("node:os"), 1);
197003
197649
  var path28 = __toESM(require("node:path"), 1);
197004
- var import_node_crypto49 = require("node:crypto");
197650
+ var import_node_crypto50 = require("node:crypto");
197005
197651
 
197006
197652
  // ../cli/src/serve.ts
197007
197653
  var fs30 = __toESM(require("node:fs"), 1);
197008
197654
  var os7 = __toESM(require("node:os"), 1);
197009
197655
  var path24 = __toESM(require("node:path"), 1);
197010
197656
  var import_node_child_process15 = require("node:child_process");
197011
- var import_node_crypto45 = require("node:crypto");
197657
+ var import_node_crypto46 = require("node:crypto");
197012
197658
  var import_node_url7 = require("node:url");
197013
197659
  init_src5();
197014
197660
  init_src10();
@@ -197755,7 +198401,10 @@ async function startServe(opts) {
197755
198401
  schemaDefs: schema,
197756
198402
  registry: kernelRegistry,
197757
198403
  reworkWavefront,
197758
- convergenceUnified
198404
+ convergenceUnified,
198405
+ // ★ ADR 0147:引擎 scan 的 work/review 超时与重试上限走策略中心(与 dispatcher 墙钟、
198406
+ // waiting-inbox 催办同源)。thunk 而非实例——随 SIGHUP / POST /api/policy/reload 热载而变。
198407
+ policy: () => activeTimeoutPolicy
197759
198408
  });
197760
198409
  const kernel = newEngine.kernel;
197761
198410
  const dispatchLedger = await PostgresDispatchStore.open(pgPool, pgSchema);
@@ -198131,7 +198780,7 @@ async function startServe(opts) {
198131
198780
  // dispatchChat 收流、done 后落助手消息并回报最终回复。会话在控制台 chat 页可回看。
198132
198781
  runChat: async ({ agentId, humanActorId, title, prompt }, onDone) => {
198133
198782
  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)();
198783
+ const chatSessionId = (0, import_node_crypto46.randomUUID)();
198135
198784
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
198136
198785
  await chatSessionStore.createSession({
198137
198786
  id: chatSessionId,
@@ -198143,7 +198792,7 @@ async function startServe(opts) {
198143
198792
  touchedAt: nowIso,
198144
198793
  createdAt: nowIso
198145
198794
  }).catch(() => void 0);
198146
- await chatSessionStore.appendMessage({ id: (0, import_node_crypto45.randomUUID)(), sessionId: chatSessionId, role: "user", content: prompt, createdAt: nowIso }).catch(() => void 0);
198795
+ await chatSessionStore.appendMessage({ id: (0, import_node_crypto46.randomUUID)(), sessionId: chatSessionId, role: "user", content: prompt, createdAt: nowIso }).catch(() => void 0);
198147
198796
  const session = await automationDispatchChat({ actorId: agentId, message: prompt, chatSessionId });
198148
198797
  let buf = "";
198149
198798
  session.onOutput((c) => {
@@ -198152,7 +198801,7 @@ async function startServe(opts) {
198152
198801
  void session.done.then(async () => {
198153
198802
  if (buf || session.runId) {
198154
198803
  await chatSessionStore.appendMessage({
198155
- id: (0, import_node_crypto45.randomUUID)(),
198804
+ id: (0, import_node_crypto46.randomUUID)(),
198156
198805
  sessionId: chatSessionId,
198157
198806
  role: "assistant",
198158
198807
  content: buf,
@@ -198166,6 +198815,10 @@ async function startServe(opts) {
198166
198815
  });
198167
198816
  return { chatSessionId };
198168
198817
  },
198818
+ // 订阅触发 × chat 的 fail-closed 开关(ADR 0163 §4.4)。当前 runChat 每次 randomUUID 建会话、
198819
+ // 没有任何持久去重:占位后崩溃重试会多开一个会话。**在 chat 适配器补上持久幂等键之前保持 false**,
198820
+ // 由此 event+chat 在配置期就被拒(能力位 supported=false),而不是上线后偶发重复开会话。
198821
+ chatSupportsIdempotency: false,
198169
198822
  // chat 准入(≈ multica AgentReadiness):在岗 + 有 active runtime 绑定,注定失败的触发别去开会话。
198170
198823
  agentReady: async (agentId) => {
198171
198824
  const actor = await registryStore.getActor(agentId);
@@ -198183,6 +198836,13 @@ async function startServe(opts) {
198183
198836
  const member = await controlPlaneStore.getMember(companyId, actor);
198184
198837
  return member?.role === "owner" || member?.role === "admin";
198185
198838
  },
198839
+ // 订阅项目可读判据(ADR 0163 §5.2):复用 projects 域的可见性口径,automation 侧不复制成员规则。
198840
+ // 现状:projects 域对登录成员不做逐项目读限制,故「可读」= 项目存在;项目被删/查不到 → null → 404。
198841
+ // 哪天 projects 加了成员级可见性,改这一个闭包即可,automation 侧不动。
198842
+ projectAccess: async ({ projectId }) => {
198843
+ const project = await projectStateStore.getProject(projectId);
198844
+ return project ? { id: project.id, name: project.name } : null;
198845
+ },
198186
198846
  log: (m2) => console.log(m2)
198187
198847
  });
198188
198848
  const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, pgSchema) : await FileControlPlaneStore.open(path24.join(opts.dir, "control-plane.json"));
@@ -198353,7 +199013,9 @@ async function startServe(opts) {
198353
199013
  schemaDefs: schema,
198354
199014
  registry: kernelRegistry,
198355
199015
  reworkWavefront,
198356
- convergenceUnified
199016
+ convergenceUnified,
199017
+ // ★ 同默认公司:漏了这里,非默认公司的引擎照旧按代码默认 30min 判超时(策略文件形同虚设)。
199018
+ policy: () => activeTimeoutPolicy
198357
199019
  });
198358
199020
  await ensureCompanyActors(controlPlaneStore, companyId, registryStore);
198359
199021
  await syncRegistryRolesToKernel(registryStore, engine2.kernel);
@@ -198604,7 +199266,7 @@ async function startServe(opts) {
198604
199266
  }));
198605
199267
  }
198606
199268
  const limits = { wallClockMs: wallClockForKind("planner") };
198607
- const artifactId = `artifact:planner:${(0, import_node_crypto45.randomUUID)()}`;
199269
+ const artifactId = `artifact:planner:${(0, import_node_crypto46.randomUUID)()}`;
198608
199270
  const handle = await chatRemoteAdapter.spawn({
198609
199271
  actor: planner.id,
198610
199272
  // companyId 同协调者:planner 用的就是默认公司的 actors 服务,漏签会让它一调公司域端点就 404。
@@ -199021,10 +199683,10 @@ async function startServe(opts) {
199021
199683
  { nodeId: priorChatSession?.runtimeId, runtimeKind: priorChatSession?.runtimeKind },
199022
199684
  { nodeId: binding.nodeId, runtimeKind: binding.runtimeKind }
199023
199685
  ) : false;
199024
- const runtimeSessionId = sessionId ?? (0, import_node_crypto45.randomUUID)();
199686
+ const runtimeSessionId = sessionId ?? (0, import_node_crypto46.randomUUID)();
199025
199687
  const resumeRuntimeSession = Boolean(sessionId);
199026
- const traceRunId = `chat-run:${(0, import_node_crypto45.randomUUID)()}`;
199027
- const artifactId = `artifact:chat:${(0, import_node_crypto45.randomUUID)()}`;
199688
+ const traceRunId = `chat-run:${(0, import_node_crypto46.randomUUID)()}`;
199689
+ const artifactId = `artifact:chat:${(0, import_node_crypto46.randomUUID)()}`;
199028
199690
  const traceStartedAt = (/* @__PURE__ */ new Date()).toISOString();
199029
199691
  let lastProgressTouchMs = 0;
199030
199692
  const PROGRESS_TOUCH_THROTTLE_MS2 = 2e4;
@@ -199677,7 +200339,7 @@ async function startServe(opts) {
199677
200339
  console.log(`[new-engine] dispatchWork ${workId} \u662F human work\u2014\u2014\u4E0D\u6D3E runtime\uFF0C\u7B49\u5F85\u4EBA\u5DE5\u5904\u7406`);
199678
200340
  return;
199679
200341
  }
199680
- const ledgerId = `dispatch:${(0, import_node_crypto45.randomUUID)()}`;
200342
+ const ledgerId = `dispatch:${(0, import_node_crypto46.randomUUID)()}`;
199681
200343
  try {
199682
200344
  await dispatchLedger.insertOpen({
199683
200345
  id: ledgerId,
@@ -199753,7 +200415,7 @@ async function startServe(opts) {
199753
200415
  const kernelModel = newKernel.model;
199754
200416
  const art = kernelModel.artifacts.get(nodeId);
199755
200417
  const revWid = art?.workspace ?? "";
199756
- const ledgerId = `dispatch:${(0, import_node_crypto45.randomUUID)()}`;
200418
+ const ledgerId = `dispatch:${(0, import_node_crypto46.randomUUID)()}`;
199757
200419
  try {
199758
200420
  await dispatchLedger.insertOpen({
199759
200421
  id: ledgerId,
@@ -200875,6 +201537,31 @@ ${st.detail}
200875
201537
  const automationTimer = setInterval(() => {
200876
201538
  void automationScheduler.tick().catch((err) => console.error(`[automation-scheduler] tick \u5931\u8D25: ${String(err)}`));
200877
201539
  }, 3e4);
201540
+ const eventPollMs = Number(process.env["OASIS_AUTOMATION_EVENT_POLL_MS"] ?? 1e4);
201541
+ let automationEventTimer;
201542
+ if (Number.isFinite(eventPollMs) && eventPollMs > 0) {
201543
+ const eventPoller = new ProjectEventPoller({
201544
+ store: automationStore,
201545
+ service: automations.service,
201546
+ log: (m2) => console.log(m2),
201547
+ batchLimit: envNum("OASIS_AUTOMATION_EVENT_BATCH", 200),
201548
+ // 工单→项目绑定是合法薄旁存(B5),不是内核事实——列项目下的工单只认它。
201549
+ listProjectWorkspaces: async ({ projectId }) => (await artifactStateStore.listWorkspaceBindings()).filter((b2) => b2.projectId === projectId).map((b2) => b2.workOrderId),
201550
+ // 完成判据必须复用 D3 resolver(不复制 deriveStage),且与成员快照同一次读取。
201551
+ workorderSnapshot: async ({ companyId, workspace }) => {
201552
+ const engine2 = await getEngine(companyId);
201553
+ return makeAutomationWorkorderSnapshotResolver(engine2.kernel)(workspace);
201554
+ },
201555
+ // 触发时复查归因主体的项目读取权(与配置期同一口径)。
201556
+ projectReadable: async ({ projectId }) => await projectStateStore.getProject(projectId) !== null
201557
+ });
201558
+ automationEventTimer = setInterval(() => {
201559
+ void eventPoller.tick().catch((err) => console.error(`[project-event-poller] tick \u5931\u8D25: ${String(err)}`));
201560
+ }, eventPollMs);
201561
+ 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`);
201562
+ } else {
201563
+ console.log("[serve] \u9879\u76EE\u4E8B\u4EF6\u8BA2\u9605\u8F6E\u8BE2\u5668\u5DF2\u88AB OASIS_AUTOMATION_EVENT_POLL_MS \u663E\u5F0F\u5173\u95ED");
201564
+ }
200878
201565
  let boardWatchTimer;
200879
201566
  const boardWatchFlag = (process.env["OASIS_BOARD_WATCH"] ?? "").trim().toLowerCase();
200880
201567
  if (!["0", "false", "off"].includes(boardWatchFlag)) {
@@ -200900,6 +201587,7 @@ ${st.detail}
200900
201587
  if (orphanSweepTimer) clearInterval(orphanSweepTimer);
200901
201588
  if (zeroOutputBreakerTimer) clearInterval(zeroOutputBreakerTimer);
200902
201589
  clearInterval(automationTimer);
201590
+ if (automationEventTimer) clearInterval(automationEventTimer);
200903
201591
  if (boardWatchTimer) clearInterval(boardWatchTimer);
200904
201592
  clearInterval(serverHeartbeatTimer);
200905
201593
  hub?.close();
@@ -200967,7 +201655,7 @@ var SessionManager = class {
200967
201655
  // ../cli/src/daemon/ws-client.ts
200968
201656
  init_wrapper();
200969
201657
  var import_node_os9 = require("node:os");
200970
- var import_node_crypto46 = require("node:crypto");
201658
+ var import_node_crypto47 = require("node:crypto");
200971
201659
  init_src8();
200972
201660
  init_src7();
200973
201661
 
@@ -201279,6 +201967,9 @@ var PENDING_STREAM_MAX = 2e3;
201279
201967
  var AUTH_REJECT_BACKOFF_MAX_MS = 15 * 6e4;
201280
201968
  var AUTH_REJECT_GIVE_UP_AFTER = 3;
201281
201969
  var AUTH_REJECT_RE = /Unexpected server response:\s*(401|403)/;
201970
+ var DRAIN_TIMEOUT_MS = 15e3;
201971
+ var DRAIN_POLL_MS = 50;
201972
+ var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
201282
201973
  async function preflightOasisAccess(job) {
201283
201974
  const base = job.server.url.replace(/\/+$/, "");
201284
201975
  try {
@@ -201323,6 +202014,8 @@ var DaemonWsClient = class {
201323
202014
  stopped = false;
201324
202015
  /** 收到 update 但有活跃会话时置真:延后到所有会话结束再自更新,避免打断执行中的 agent。 */
201325
202016
  pendingUpdate = false;
202017
+ /** 停机排空中(见 {@link drain}):不再接新派发、不再触发延后的自更新。 */
202018
+ draining = false;
201326
202019
  /** 已回 dispatch_received、尚未完成 runtime spawn 的派发;也随 hello 上报供重连对账。 */
201327
202020
  pendingDispatchIds = /* @__PURE__ */ new Set();
201328
202021
  /** setup 期间收到 kill 的派发。spawn 返回后先杀句柄,不得再宣告 session_started。 */
@@ -201379,6 +202072,73 @@ var DaemonWsClient = class {
201379
202072
  if (this.pingTimer) clearInterval(this.pingTimer);
201380
202073
  this.ws?.close();
201381
202074
  }
202075
+ /** 排空中?(观测/测试用) */
202076
+ isDraining() {
202077
+ return this.draining;
202078
+ }
202079
+ /**
202080
+ * 停机排空——在进程退出前,把「在途会话已经死了」这件事告诉服务端。
202081
+ *
202082
+ * ## 为什么必须有这一步(2026-08-25 生产实证)
202083
+ *
202084
+ * 引擎里一条 work 从 `running` 变成别的状态,全后端只有三个入口:收到 `session_exited` 帧走主路
202085
+ * (`serve.ts` 的 work.response 桥)、同一帧走 journal 反查的兜底、以及 scan 里 **30 分钟**的
202086
+ * `work.timeout`。前两个都要求**这一帧真的被发出来**。
202087
+ *
202088
+ * 而这一帧的生成者,是本进程 dispatch 分支里注册的那个 `handle.onExit` 回调——**它住在 daemon
202089
+ * 进程内部**。子进程被杀不会自动产生任何帧;进程一死,回调就再没机会跑。
202090
+ *
202091
+ * 旧行为:SIGTERM handler 只删 pid 文件就 `process.exit(0)`;systemd unit 又没设 `KillMode`,
202092
+ * 默认 `control-group` 把 daemon 和它拉起的所有会话**同时** SIGTERM——父子同死,一帧都发不出去。
202093
+ * 实测 node-6745008c1fc7:5389 次会话起跑里 48 个没有配对退出帧,其中 43 个(90%)是重启带走的;
202094
+ * 每一个都让对应的 work 空转到 30 分钟超时才被判死,控制台全程显示"运行中"。
202095
+ *
202096
+ * ## 做法:让真实退出帧走正路,只有两类才合成
202097
+ *
202098
+ * 不图省事直接合成假帧——`kill()` 掉在途会话,adapter 的 `killReason` 归因成 `cancelled`,既有
202099
+ * `onExit` 正路会带着 `transcriptRef` 把**真实**帧发出去。只有两类兜底才合成:还没 spawn 的派发
202100
+ * (没有子进程可 kill,不可能有真实帧),以及超时后仍未退出的会话(宁可给服务端一帧不精确的死讯,
202101
+ * 也好过让它等满 30 分钟)。
202102
+ *
202103
+ * 排空期间 `draining` 置真:不再接新派发(见 dispatch 分支),也不再触发延后的自更新。
202104
+ */
202105
+ async drain(reason, timeoutMs = DRAIN_TIMEOUT_MS) {
202106
+ if (this.draining) return;
202107
+ this.draining = true;
202108
+ const active = this.sessions.activeSessions();
202109
+ const pending = [...this.pendingDispatchIds];
202110
+ if (active.length === 0 && pending.length === 0) {
202111
+ this.stop();
202112
+ return;
202113
+ }
202114
+ log2("[node-cli]", `\u505C\u673A\u6392\u7A7A\uFF08${reason}\uFF09\uFF1A\u5728\u9014\u4F1A\u8BDD ${active.length} \u4E2A\u3001\u5F85\u8D77\u6D3E\u53D1 ${pending.length} \u4E2A\u2014\u2014\u53D1\u5B8C\u9000\u51FA\u5E27\u518D\u9000\u51FA`);
202115
+ for (const dispatchId of pending) {
202116
+ this.pendingDispatchIds.delete(dispatchId);
202117
+ this.sendKeyFrame(dispatchId, {
202118
+ type: "session_exited",
202119
+ dispatchId,
202120
+ sessionId: "node-shutdown",
202121
+ info: { code: null, reason: "cancelled", errorMessage: `\u8282\u70B9 daemon \u505C\u673A\uFF08${reason}\uFF09\uFF1A\u6D3E\u53D1\u5C1A\u672A\u8D77\u4F1A\u8BDD` }
202122
+ });
202123
+ }
202124
+ await Promise.all(active.map((s2) => this.sessions.kill(s2.dispatchId).catch(() => void 0)));
202125
+ const deadline = Date.now() + timeoutMs;
202126
+ while (Date.now() < deadline && (this.sessions.activeCount() > 0 || this.outbox.size > 0)) await sleep4(DRAIN_POLL_MS);
202127
+ const stuck = this.sessions.activeSessions();
202128
+ for (const s2 of stuck) {
202129
+ log2("[node-cli]", ` \u26A0 \u4F1A\u8BDD ${s2.sessionId} \u5728 ${timeoutMs}ms \u5185\u6CA1\u9000\u51FA\uFF0C\u6309\u505C\u673A\u5408\u6210\u9000\u51FA\u5E27`);
202130
+ this.sendKeyFrame(s2.dispatchId, {
202131
+ type: "session_exited",
202132
+ dispatchId: s2.dispatchId,
202133
+ sessionId: s2.sessionId,
202134
+ info: { code: null, reason: "cancelled", errorMessage: `\u8282\u70B9 daemon \u505C\u673A\uFF08${reason}\uFF09\uFF1A\u4F1A\u8BDD\u672A\u5728\u6392\u7A7A\u7A97\u53E3\u5185\u9000\u51FA` }
202135
+ });
202136
+ }
202137
+ if (stuck.length > 0) await sleep4(DRAIN_POLL_MS * 4);
202138
+ if (this.outbox.size > 0) log2("[node-cli]", `\u26A0 \u505C\u673A\u6392\u7A7A\u7ED3\u675F\uFF1A\u4ECD\u6709 ${this.outbox.size} \u4E2A\u9000\u51FA\u5E27\u672A\u786E\u8BA4\u9001\u8FBE\uFF08\u8FD9\u4E9B work \u4F1A\u9000\u56DE 30 \u5206\u949F\u8D85\u65F6\u515C\u5E95\uFF09`);
202139
+ else log2("[node-cli]", "\u505C\u673A\u6392\u7A7A\u5B8C\u6210\uFF1A\u5728\u9014\u4F1A\u8BDD\u7684\u9000\u51FA\u5E27\u5747\u5DF2\u786E\u8BA4\u9001\u8FBE");
202140
+ this.stop();
202141
+ }
201382
202142
  connect() {
201383
202143
  if (this.stopped) return;
201384
202144
  const ws = new import_websocket.default(this.serverUrl, {
@@ -201455,6 +202215,16 @@ var DaemonWsClient = class {
201455
202215
  const { dispatchId, job } = msg;
201456
202216
  const t0 = Date.now();
201457
202217
  log2("[node-cli]", `\u2190 dispatch ${dispatchId} artifact=${job.artifactId}`);
202218
+ if (this.draining) {
202219
+ log2("[node-cli]", ` \u62D2\u6536\uFF1A\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A \u2192 \u56DE\u9000\u51FA\u5E27\uFF0C\u670D\u52A1\u7AEF\u53EF\u7ACB\u5373\u6539\u6D3E`);
202220
+ this.sendKeyFrame(dispatchId, {
202221
+ type: "session_exited",
202222
+ dispatchId,
202223
+ sessionId: "node-draining",
202224
+ info: { code: null, reason: "cancelled", errorMessage: "\u8282\u70B9\u6B63\u5728\u505C\u673A\u6392\u7A7A\uFF0C\u672C\u6B21\u6D3E\u53D1\u672A\u8D77\u4F1A\u8BDD" }
202225
+ });
202226
+ break;
202227
+ }
201458
202228
  if (this.pendingDispatchIds.has(dispatchId) || this.sessions.get(dispatchId)) {
201459
202229
  this.send({ type: "dispatch_received", dispatchId });
201460
202230
  break;
@@ -201572,6 +202342,10 @@ var DaemonWsClient = class {
201572
202342
  log2("[node-cli]", "\u2190 update \u5FFD\u7565\uFF1A\u672C\u8FDB\u7A0B\u672A\u6CE8\u5165\u81EA\u66F4\u65B0\u903B\u8F91\uFF08\u975E node-daemon \u6258\u7BA1\uFF1F\uFF09");
201573
202343
  break;
201574
202344
  }
202345
+ if (this.draining) {
202346
+ log2("[node-cli]", "\u2190 update \u5FFD\u7565\uFF1A\u672C\u8FDB\u7A0B\u6B63\u5728\u505C\u673A\u6392\u7A7A");
202347
+ break;
202348
+ }
201575
202349
  const active = this.sessions.activeCount();
201576
202350
  if (active > 0) {
201577
202351
  this.pendingUpdate = true;
@@ -201592,7 +202366,7 @@ var DaemonWsClient = class {
201592
202366
  */
201593
202367
  async queryArtifactStates(artifactIds, timeoutMs = 15e3) {
201594
202368
  if (artifactIds.length === 0) return {};
201595
- const requestId = (0, import_node_crypto46.randomUUID)();
202369
+ const requestId = (0, import_node_crypto47.randomUUID)();
201596
202370
  return new Promise((resolve10, reject) => {
201597
202371
  const timer = setTimeout(() => {
201598
202372
  this.gcPending.delete(requestId);
@@ -201659,6 +202433,7 @@ var DaemonWsClient = class {
201659
202433
  }
201660
202434
  /** 会话结束后调用:若之前收到过 update 但因忙延后了,且现已空闲,则触发自更新。 */
201661
202435
  maybeRunPendingUpdate() {
202436
+ if (this.draining) return;
201662
202437
  if (this.pendingUpdate && this.sessions.activeCount() === 0) {
201663
202438
  this.pendingUpdate = false;
201664
202439
  log2("[node-cli]", "\u6240\u6709\u4F1A\u8BDD\u5DF2\u7ED3\u675F,\u6267\u884C\u5EF6\u540E\u7684\u66F4\u65B0\uFF1A\u62C9\u53D6\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F");
@@ -201822,6 +202597,7 @@ async function startNode(opts) {
201822
202597
  });
201823
202598
  console.log(`[oasis node] ${opts.nodeId} \u2192 ${opts.serverUrl}`);
201824
202599
  client.start();
202600
+ opts.onReady?.({ drain: (reason) => client.drain(reason) });
201825
202601
  const { workRoot, legacyRoot } = resolveWorkRoots(opts.workRoot);
201826
202602
  const workRootIsEphemeral = workRoot.startsWith(import_node_os11.default.tmpdir());
201827
202603
  if (opts.gcEnabled === false && !workRootIsEphemeral) {
@@ -201929,7 +202705,7 @@ function forwardSignal(child, sig, killGroup = (pgid, signal) => {
201929
202705
  // ../cli/src/daemon/machine-id.ts
201930
202706
  var import_node_child_process17 = require("node:child_process");
201931
202707
  var import_node_fs21 = require("node:fs");
201932
- var import_node_crypto47 = require("node:crypto");
202708
+ var import_node_crypto48 = require("node:crypto");
201933
202709
  var import_node_os12 = require("node:os");
201934
202710
  function linuxMachineId() {
201935
202711
  for (const p2 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
@@ -201995,7 +202771,7 @@ var defaultSources = {
201995
202771
  function resolveNodeId(sources = {}) {
201996
202772
  const s2 = { ...defaultSources, ...sources };
201997
202773
  const material = `${s2.machineFingerprint()}:${s2.osUser()}`;
201998
- const digest = (0, import_node_crypto47.createHash)("sha256").update(material).digest("hex").slice(0, 12);
202774
+ const digest = (0, import_node_crypto48.createHash)("sha256").update(material).digest("hex").slice(0, 12);
201999
202775
  return `node-${digest}`;
202000
202776
  }
202001
202777
 
@@ -202329,17 +203105,22 @@ var COMMAND_DECLS = {
202329
203105
  },
202330
203106
  "automation-trigger-add": {
202331
203107
  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",
203108
+ usage: "oasis automation-trigger-add <id> --kind schedule|webhook|event [--cron <expr> --tz <tz>] [--provider generic|github] [--project <projectId> --event workorder.completed] [--label <\u540D>]",
203109
+ 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
203110
  positional: [{ name: "<id>", required: true, desc: "\u81EA\u52A8\u5316 id" }],
202335
203111
  flags: [
202336
- { name: "kind", desc: "schedule|webhook\uFF08\u9ED8\u8BA4 schedule\uFF09" },
203112
+ { name: "kind", desc: "schedule|webhook|event\uFF08\u9ED8\u8BA4 schedule\uFF09" },
202337
203113
  { name: "cron", desc: "5 \u5B57\u6BB5 cron\uFF08schedule \u5FC5\u586B\uFF09" },
202338
203114
  { name: "tz", desc: "IANA \u65F6\u533A\uFF08\u9ED8\u8BA4 UTC\uFF09" },
202339
203115
  { name: "provider", desc: "webhook provider\uFF1Ageneric|github" },
203116
+ { name: "project", desc: "\u8BA2\u9605\u7684\u9879\u76EE id\uFF08event \u5FC5\u586B\uFF1Bschedule/webhook \u4E0D\u63A5\u53D7\uFF09" },
203117
+ { 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
203118
  { name: "label", desc: "\u663E\u793A\u540D" }
202341
203119
  ],
202342
- examples: ['oasis automation-trigger-add auto_abc123 --kind schedule --cron "0 18 * * 5" --tz Asia/Shanghai']
203120
+ examples: [
203121
+ 'oasis automation-trigger-add auto_abc123 --kind schedule --cron "0 18 * * 5" --tz Asia/Shanghai',
203122
+ "oasis automation-trigger-add auto_abc123 --kind event --project oasis --event workorder.completed"
203123
+ ]
202343
203124
  },
202344
203125
  "automation-trigger-update": {
202345
203126
  group: "\u81EA\u52A8\u5316",
@@ -203607,13 +204388,13 @@ function ownFlagsFromDecl(command) {
203607
204388
  }
203608
204389
 
203609
204390
  // ../cli/src/connector-effect-runner.ts
203610
- var import_node_crypto48 = require("node:crypto");
204391
+ var import_node_crypto49 = require("node:crypto");
203611
204392
  var import_node_child_process18 = require("node:child_process");
203612
204393
  init_src8();
203613
204394
  function stableKey(effect) {
203614
204395
  const keyFields = Object.fromEntries(Object.entries(effect.keyFields).sort(([a], [b2]) => a.localeCompare(b2)));
203615
204396
  const canonical = JSON.stringify({ kind: effect.kind, target: effect.target, keyFields });
203616
- return `sha256:${(0, import_node_crypto48.createHash)("sha256").update(canonical).digest("hex")}`;
204397
+ return `sha256:${(0, import_node_crypto49.createHash)("sha256").update(canonical).digest("hex")}`;
203617
204398
  }
203618
204399
  async function runConnectorEffect(connector, toolArgs, deps) {
203619
204400
  const effect = connector.describeExternalEffect?.(toolArgs) ?? null;
@@ -204208,7 +204989,7 @@ async function runCli(argv, println = console.log, progressln = console.error) {
204208
204989
  const store = createNodeTokenStore(path28.join(dir, "node-tokens.json"));
204209
204990
  const sub = positional[0];
204210
204991
  if (sub === "issue") {
204211
- const id = flags.get("id") ?? `node-${(0, import_node_crypto49.randomUUID)()}`;
204992
+ const id = flags.get("id") ?? `node-${(0, import_node_crypto50.randomUUID)()}`;
204212
204993
  const token2 = store.issue(id);
204213
204994
  println(token2);
204214
204995
  process.stderr.write(
@@ -204821,10 +205602,36 @@ ${p2.ref} ${p2.name}`);
204821
205602
  break;
204822
205603
  }
204823
205604
  case "automation-trigger-add": {
204824
- const id = needPos(positional, 0, "oasis automation-trigger-add <id> --kind schedule|webhook ...");
205605
+ const id = needPos(positional, 0, "oasis automation-trigger-add <id> --kind schedule|webhook|event ...");
204825
205606
  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));
205607
+ if (kind !== "schedule" && kind !== "webhook" && kind !== "event") {
205608
+ throw new Error(`--kind \u53EA\u80FD\u662F schedule\u3001webhook \u6216 event\uFF08\u6536\u5230 ${kind}\uFF09`);
205609
+ }
205610
+ if (kind !== "event" && (flags.has("project") || flags.has("event"))) {
205611
+ throw new Error(`--project / --event \u53EA\u7528\u4E8E --kind event\uFF08\u5F53\u524D --kind ${kind}\uFF09`);
205612
+ }
205613
+ if (kind === "event" && (flags.has("cron") || flags.has("tz") || flags.has("provider"))) {
205614
+ throw new Error("--kind event \u4E0D\u63A5\u53D7 --cron / --tz / --provider");
205615
+ }
205616
+ const label = flags.has("label") ? { label: flags.get("label") } : {};
205617
+ let body;
205618
+ if (kind === "schedule") {
205619
+ body = { kind, schedule: { cron: need(flags, "cron"), tz: flags.get("tz") ?? "UTC" }, ...label };
205620
+ } else if (kind === "webhook") {
205621
+ body = { kind, webhook: { provider: flags.get("provider") ?? "generic" }, ...label };
205622
+ } else {
205623
+ const events = (flags.get("event") ?? "").split(",").map((x2) => x2.trim()).filter(Boolean);
205624
+ if (!events.length) throw new Error("--kind event \u9700\u8981\u81F3\u5C11\u4E00\u4E2A --event\uFF08\u5982 --event workorder.completed\uFF09");
205625
+ body = { kind, event: { source: "project", projectId: need(flags, "project"), events }, ...label };
205626
+ }
205627
+ const created = await api.post(
205628
+ `/api/automations/${encodeURIComponent(id)}/triggers`,
205629
+ body
205630
+ );
205631
+ println(JSON.stringify(created, null, 2));
205632
+ if (kind === "event" && created.event?.baselineState === "aligning") {
205633
+ 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");
205634
+ }
204828
205635
  break;
204829
205636
  }
204830
205637
  case "automation-trigger-update": {
@@ -206673,12 +207480,12 @@ var LEGACY_SYSTEMD_UNIT = "oasis-node.service";
206673
207480
  var LEGACY_LAUNCHD_LABEL = "com.oasis.node";
206674
207481
 
206675
207482
  // src/install.ts
206676
- var import_node_crypto50 = require("node:crypto");
207483
+ var import_node_crypto51 = require("node:crypto");
206677
207484
  var fs37 = __toESM(require("node:fs"));
206678
207485
  var path31 = __toESM(require("node:path"));
206679
207486
  function versionLabel(sourceFile, pkgVersion, npmPrefixDir) {
206680
207487
  if (isFromNpmPrefix(sourceFile, npmPrefixDir)) return pkgVersion;
206681
- const digest = (0, import_node_crypto50.createHash)("sha256").update(fs37.readFileSync(sourceFile)).digest("hex").slice(0, 8);
207488
+ const digest = (0, import_node_crypto51.createHash)("sha256").update(fs37.readFileSync(sourceFile)).digest("hex").slice(0, 8);
206682
207489
  return `${pkgVersion}+local.${digest}`;
206683
207490
  }
206684
207491
  function isFromNpmPrefix(sourceFile, npmPrefixDir) {
@@ -206758,6 +207565,37 @@ function gcVersions(versionsDirPath, opts) {
206758
207565
  }
206759
207566
  return deleted;
206760
207567
  }
207568
+ function systemdUnitText(o) {
207569
+ return [
207570
+ "[Unit]",
207571
+ `Description=Oasis Node Daemon (${o.name})`,
207572
+ "After=network.target",
207573
+ "",
207574
+ // OASIS_CLI_HOME 由 daemon 继承给它 spawn 的 agent 会话——非 default 实例的会话因此
207575
+ // 用的是**本实例**的 CLI 版本,而不是 PATH 上那个默认指向 default 的 shim。
207576
+ // (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
207577
+ "[Service]",
207578
+ `ExecStart=${o.cmd}`,
207579
+ `Environment=PATH=${o.daemonPath}`,
207580
+ `Environment=OASIS_CLI_HOME=${o.cliHome}`,
207581
+ `StandardOutput=append:${o.logFile}`,
207582
+ `StandardError=append:${o.logFile}`,
207583
+ "Restart=on-failure",
207584
+ "RestartSec=5",
207585
+ // 🚨 停机时只 SIGTERM 主进程,子会话留给 daemon 自己收——它要先给每个在途会话发 session_exited,
207586
+ // 那是服务端唯一的死亡通知渠道(生成者是 daemon 进程内的 onExit 回调)。
207587
+ // systemd 默认的 KillMode=control-group 会把 daemon 和它拉起的所有 claude 会话**同时**杀掉:
207588
+ // 父子同死,一帧都发不出去,对应的 work 只能空转到 scan 的 30 分钟超时才被判死。
207589
+ // 实测占本机孤儿会话的 90%(43/48)。详见 change-record 20260825-daemon-restart-swallows-exit-frames。
207590
+ // TimeoutStopSec 必须明显大于排空预算 DRAIN_TIMEOUT_MS(15s),否则排空没做完就被 SIGKILL。
207591
+ "KillMode=mixed",
207592
+ "TimeoutStopSec=30",
207593
+ "",
207594
+ "[Install]",
207595
+ "WantedBy=default.target",
207596
+ ""
207597
+ ].join("\n");
207598
+ }
206761
207599
  function shimScript() {
206762
207600
  return [
206763
207601
  "#!/bin/sh",
@@ -206770,7 +207608,7 @@ function shimScript() {
206770
207608
  }
206771
207609
 
206772
207610
  // src/index.ts
206773
- var PKG_VERSION = true ? "0.1.117" : "dev";
207611
+ var PKG_VERSION = true ? "0.1.119" : "dev";
206774
207612
  var LOCAL_BIN = localBin();
206775
207613
  var NPM_PREFIX = npmPrefix();
206776
207614
  var INSTANCE = DEFAULT_INSTANCE;
@@ -206852,9 +207690,8 @@ function ensureRuntimeAssets(targetDir = BIN_DIR) {
206852
207690
  console.error(`[oasis] connector runtime assets: \u4ECE ${synced.from} \u540C\u6B65\u4E86 ${synced.copied} \u4E2A .sh \u5230 ${targetDir}`);
206853
207691
  }
206854
207692
  }
206855
- function installSoftware() {
207693
+ function installSoftware(label) {
206856
207694
  const src = process.argv[1];
206857
- const label = versionLabel(src, PKG_VERSION, NPM_PREFIX);
206858
207695
  const vDir = versionDir(label);
206859
207696
  installBinaryAtomic(src, path32.join(vDir, "oasis.js"));
206860
207697
  ensureRuntimeAssets(vDir);
@@ -206885,27 +207722,13 @@ function setupAutostart(name) {
206885
207722
  fs38.mkdirSync(unitDir, { recursive: true });
206886
207723
  const unit = systemdUnitName(name);
206887
207724
  fs38.mkdirSync(path32.dirname(inst.logFile), { recursive: true });
206888
- fs38.writeFileSync(path32.join(unitDir, unit), [
206889
- "[Unit]",
206890
- `Description=Oasis Node Daemon (${name})`,
206891
- "After=network.target",
206892
- "",
206893
- // OASIS_CLI_HOME 由 daemon 继承给它 spawn 的 agent 会话——非 default 实例的会话因此
206894
- // 用的是**本实例**的 CLI 版本,而不是 PATH 上那个默认指向 default 的 shim。
206895
- // (台阶 3 的 D4 后半截会把它细到「每个会话钉 spawn 那一刻的版本」,这里先钉到实例。)
206896
- "[Service]",
206897
- `ExecStart=${cmd}`,
206898
- `Environment=PATH=${daemonPath}`,
206899
- `Environment=OASIS_CLI_HOME=${inst.currentLink}`,
206900
- `StandardOutput=append:${inst.logFile}`,
206901
- `StandardError=append:${inst.logFile}`,
206902
- "Restart=on-failure",
206903
- "RestartSec=5",
206904
- "",
206905
- "[Install]",
206906
- "WantedBy=default.target",
206907
- ""
206908
- ].join("\n"));
207725
+ fs38.writeFileSync(path32.join(unitDir, unit), systemdUnitText({
207726
+ name,
207727
+ cmd,
207728
+ daemonPath,
207729
+ cliHome: inst.currentLink,
207730
+ logFile: inst.logFile
207731
+ }));
206909
207732
  if (name === DEFAULT_INSTANCE) {
206910
207733
  try {
206911
207734
  (0, import_node_child_process19.execSync)(`systemctl --user disable --now ${LEGACY_SYSTEMD_UNIT} 2>/dev/null`, { stdio: "ignore" });
@@ -206992,20 +207815,12 @@ function stopDaemon(name) {
206992
207815
  }
206993
207816
  }
206994
207817
  }
206995
- function selfUpdate(opts = {}) {
207818
+ function selfUpdate() {
206996
207819
  const cfg = readConfig();
206997
207820
  if (!cfg?.serverUrl) throw new Error("no config \u2014 run `oasis daemon start` first");
206998
- if (cfg.pinnedVersion && !opts.force) {
206999
- console.error(`[oasis] \u5B9E\u4F8B ${INSTANCE} \u5DF2\u9489\u5728 ${cfg.pinnedVersion}\uFF0C\u8DF3\u8FC7\u81EA\u66F4\u65B0\uFF08\u8981\u89E3\u9664\uFF1Aoasis daemon update --instance ${INSTANCE} --force\uFF09`);
207000
- return false;
207001
- }
207002
- if (cfg.pinnedVersion && opts.force) {
207003
- saveConfig({ ...cfg, pinnedVersion: void 0 });
207004
- console.error(`[oasis] --force\uFF1A\u5DF2\u89E3\u9664 ${INSTANCE} \u7684\u7248\u672C\u9489\u6B7B\uFF08\u539F ${cfg.pinnedVersion}\uFF09`);
207005
- }
207006
207821
  const nameArg = cfg.name ? ` --name '${cfg.name.replace(/'/g, "'\\''")}'` : "";
207007
207822
  const oasisBin = path32.join(NPM_PREFIX, "bin", "oasis");
207008
- const startCmd = `"${oasisBin}" daemon start --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
207823
+ const startCmd = `"${oasisBin}" daemon start --upgrade --instance ${INSTANCE} --server-ws '${cfg.serverUrl}'${nameArg}`;
207009
207824
  const OFFICIAL = "https://registry.npmjs.org/";
207010
207825
  const installOfficial = `npm install -g --prefer-online --registry '${OFFICIAL}' --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
207011
207826
  const installFallback = `npm install -g --prefer-online --prefix '${NPM_PREFIX}' '${PKG_NAME}@latest'`;
@@ -207030,7 +207845,6 @@ function selfUpdate(opts = {}) {
207030
207845
  const child = (0, import_node_child_process19.spawn)("sh", ["-c", inner], { detached: true, stdio: ["ignore", log3, log3] });
207031
207846
  child.unref();
207032
207847
  }
207033
- return true;
207034
207848
  }
207035
207849
  function resolveInstance(explicit) {
207036
207850
  if (!explicit) return DEFAULT_INSTANCE;
@@ -207040,6 +207854,103 @@ function resolveInstance(explicit) {
207040
207854
  }
207041
207855
  return explicit;
207042
207856
  }
207857
+ function currentLabelOf(inst) {
207858
+ try {
207859
+ return path32.basename(fs38.realpathSync(inst.currentLink));
207860
+ } catch {
207861
+ return null;
207862
+ }
207863
+ }
207864
+ function instanceHasSoftware(inst) {
207865
+ return fs38.existsSync(inst.binFile) && currentLabelOf(inst) !== null;
207866
+ }
207867
+ function newInstanceName() {
207868
+ const existing = new Set(listInstances());
207869
+ if (!existing.has(DEFAULT_INSTANCE)) return DEFAULT_INSTANCE;
207870
+ for (; ; ) {
207871
+ const n = `inst-${(0, import_node_crypto52.randomBytes)(3).toString("hex")}`;
207872
+ if (!existing.has(n)) return n;
207873
+ }
207874
+ }
207875
+ function planStart(explicit, serverUrl) {
207876
+ if (explicit) {
207877
+ const name = resolveInstance(explicit);
207878
+ return { mode: fs38.existsSync(instancePaths(name).configFile) ? "resume" : "new", names: [name] };
207879
+ }
207880
+ const all = listInstances();
207881
+ if (all.length === 0) return { mode: "new", names: [DEFAULT_INSTANCE] };
207882
+ const want = normalizeServerUrl(serverUrl);
207883
+ const hits = all.filter((n) => {
207884
+ const c = readConfigAt(instancePaths(n).configFile);
207885
+ return !!c?.serverUrl && normalizeServerUrl(c.serverUrl) === want;
207886
+ });
207887
+ if (hits.length === 0) return { mode: "new", names: [newInstanceName()] };
207888
+ return { mode: "resume", names: hits };
207889
+ }
207890
+ async function startOneInstance(o) {
207891
+ const { serverUrl, upgrade, flags } = o;
207892
+ const prev = readConfig();
207893
+ if (prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
207894
+ 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`);
207895
+ console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
207896
+ console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
207897
+ }
207898
+ const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
207899
+ let nodeId = serverChanged ? void 0 : prev?.nodeId;
207900
+ let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
207901
+ const enrollToken = o.mode === "new" ? flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"] : void 0;
207902
+ const derivedNodeId = derivedNodeIdFor(INSTANCE);
207903
+ if (enrollToken) {
207904
+ const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
207905
+ method: "POST",
207906
+ headers: { "content-type": "application/json" },
207907
+ body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
207908
+ });
207909
+ if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
207910
+ const body = await res.json();
207911
+ token = body.token;
207912
+ nodeId = body.nodeId;
207913
+ } else if (prev?.nodeId && prev?.token && !serverChanged) {
207914
+ nodeId = prev.nodeId;
207915
+ token = prev.token;
207916
+ }
207917
+ if (!token || !nodeId) {
207918
+ console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
207919
+ process.exit(1);
207920
+ }
207921
+ const name = o.applyName ? flags.get("name") ?? prev?.name : prev?.name;
207922
+ const nameChanged = prev?.name !== void 0 && name !== prev.name;
207923
+ saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
207924
+ const before = currentLabelOf(INST);
207925
+ let label = before;
207926
+ if (!instanceHasSoftware(INST) || upgrade) {
207927
+ const next = versionLabel(process.argv[1], PKG_VERSION, NPM_PREFIX);
207928
+ if (before && isLocalLabel(before) && before !== next) {
207929
+ console.log(` \u26A0 \u5373\u5C06\u7528 ${next} \u8986\u76D6\u672C\u5B9E\u4F8B\u6B63\u5728\u8DD1\u7684\u672C\u5730\u6784\u5EFA ${before}`);
207930
+ }
207931
+ label = installSoftware(next);
207932
+ } else {
207933
+ fs38.mkdirSync(path32.dirname(LOCAL_BIN), { recursive: true });
207934
+ fs38.writeFileSync(LOCAL_BIN, shimScript(), { mode: 493 });
207935
+ }
207936
+ const pidNow = readPid();
207937
+ const alive = !!pidNow && isRunning2(pidNow);
207938
+ if (alive && label === before && !serverChanged && !nameChanged) {
207939
+ 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`);
207940
+ return;
207941
+ }
207942
+ stopDaemon(INSTANCE);
207943
+ const tookOver = setupAutostart(INSTANCE);
207944
+ const suffix = nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : "";
207945
+ if (!tookOver) {
207946
+ const pid = spawnDaemon();
207947
+ console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
207948
+ } else {
207949
+ console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
207950
+ }
207951
+ console.log(` instance: ${INSTANCE} nodeId: ${nodeId} version: ${label ?? "?"} config: ${INST.configFile}`);
207952
+ console.log(` logs: tail -f ${LOG_FILE}`);
207953
+ }
207043
207954
  function derivedNodeIdFor(name) {
207044
207955
  if (name === DEFAULT_INSTANCE) return resolveNodeId();
207045
207956
  const user = (() => {
@@ -207087,19 +207998,32 @@ void (async () => {
207087
207998
  }
207088
207999
  const cleanup = () => {
207089
208000
  try {
207090
- fs38.unlinkSync(PID_FILE);
208001
+ if (fs38.readFileSync(PID_FILE, "utf8").trim() === String(process.pid)) fs38.unlinkSync(PID_FILE);
207091
208002
  } catch {
207092
208003
  }
207093
208004
  };
207094
208005
  process.on("exit", cleanup);
207095
- process.on("SIGTERM", () => {
207096
- cleanup();
207097
- process.exit(0);
207098
- });
207099
- process.on("SIGINT", () => {
207100
- cleanup();
207101
- process.exit(0);
207102
- });
208006
+ let drainNode = null;
208007
+ let shuttingDown = false;
208008
+ const shutdown = (signal) => {
208009
+ if (shuttingDown) {
208010
+ console.error(`[oasis] \u6536\u5230\u7B2C\u4E8C\u6B21 ${signal}\uFF1A\u653E\u5F03\u6392\u7A7A\uFF0C\u7ACB\u5373\u9000\u51FA`);
208011
+ cleanup();
208012
+ process.exit(0);
208013
+ }
208014
+ shuttingDown = true;
208015
+ void (async () => {
208016
+ try {
208017
+ await drainNode?.(signal);
208018
+ } catch (e) {
208019
+ console.error("[oasis] \u505C\u673A\u6392\u7A7A\u5931\u8D25\uFF08\u5728\u9014\u4F1A\u8BDD\u7684\u9000\u51FA\u5E27\u53EF\u80FD\u6CA1\u53D1\u51FA\u53BB\uFF09:", String(e));
208020
+ }
208021
+ cleanup();
208022
+ process.exit(0);
208023
+ })();
208024
+ };
208025
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
208026
+ process.on("SIGINT", () => shutdown("SIGINT"));
207103
208027
  const cleanPath = sanitizeDaemonPath(process.env["PATH"]);
207104
208028
  if (cleanPath !== process.env["PATH"]) {
207105
208029
  console.error(`[oasis] daemon PATH \u5DF2\u6E05\u6D17\uFF1A\u5254\u9664 agent \u4F1A\u8BDD\u4E34\u65F6 wrapper \u76EE\u5F55 ${(process.env["PATH"] ?? "").split(":").length - cleanPath.split(":").length} \u6761`);
@@ -207139,66 +208063,33 @@ void (async () => {
207139
208063
  } catch (e) {
207140
208064
  console.error("[oasis] self-update failed:", String(e));
207141
208065
  }
208066
+ },
208067
+ onReady: (ctl) => {
208068
+ drainNode = ctl.drain;
207142
208069
  }
207143
208070
  });
207144
208071
  return;
207145
208072
  }
207146
208073
  if (dsub === "start") {
208074
+ const explicit = flags.get("instance") || void 0;
208075
+ const upgrade = flags.has("upgrade");
207147
208076
  const serverUrlFlag = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"];
207148
- useInstance(resolveInstance(flags.get("instance")));
207149
- const prev = readConfig();
207150
- const serverUrl = serverUrlFlag ?? prev?.serverUrl;
208077
+ const probe = explicit ?? listInstances()[0] ?? DEFAULT_INSTANCE;
208078
+ const serverUrl = serverUrlFlag ?? readConfigAt(instancePaths(probe).configFile)?.serverUrl;
207151
208079
  if (!serverUrl) {
207152
- console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
207153
- process.exit(1);
207154
- }
207155
- if (!flags.get("instance") && prev?.serverUrl && normalizeServerUrl(prev.serverUrl) !== normalizeServerUrl(serverUrl)) {
207156
- console.error(`[oasis] \u26A0 \u5B9E\u4F8B "${INSTANCE}" \u539F\u672C\u8FDE ${prev.serverUrl}\uFF0C\u672C\u6B21\u5C06\u6539\u8FDE ${serverUrl}\u2014\u2014\u8FD9\u662F**\u8FC1\u79FB**\u4E0D\u662F\u65B0\u589E\u3002`);
207157
- console.error(`[oasis] \u60F3\u8BA9\u4E24\u4E2A\u63A7\u5236\u9762\u5E76\u5B58\uFF0C\u8BF7\u4E2D\u6B62\uFF08Ctrl-C\uFF09\u5E76\u6539\u7528\uFF1A`);
207158
- console.error(`[oasis] oasis daemon start --instance <\u65B0\u540D\u5B57> --server-ws ${serverUrl} --enroll-token <ent_...>`);
207159
- }
207160
- const serverChanged = !!prev?.serverUrl && prev.serverUrl !== serverUrl;
207161
- let nodeId = serverChanged ? void 0 : prev?.nodeId;
207162
- let token = serverChanged ? flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] : flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
207163
- const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"];
207164
- const derivedNodeId = derivedNodeIdFor(INSTANCE);
207165
- if (enrollToken) {
207166
- const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
207167
- method: "POST",
207168
- headers: { "content-type": "application/json" },
207169
- body: JSON.stringify({ enrollToken, nodeId: derivedNodeId, hostname: os13.hostname() })
207170
- });
207171
- if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
207172
- const body = await res.json();
207173
- token = body.token;
207174
- nodeId = body.nodeId;
207175
- } else if (prev?.nodeId && prev?.token && !serverChanged) {
207176
- nodeId = prev.nodeId;
207177
- token = prev.token;
207178
- }
207179
- if (!token || !nodeId) {
207180
- console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>]");
208080
+ console.error("Usage: oasis daemon start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--instance <name>] [--upgrade]");
207181
208081
  process.exit(1);
207182
208082
  }
207183
- const name = flags.get("name") ?? prev?.name;
207184
- const nameChanged = prev?.name !== void 0 && name !== prev.name;
207185
- saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
207186
- const installedLabel = installSoftware();
207187
- if (isLocalLabel(installedLabel)) {
207188
- saveConfig({ ...readConfig(), pinnedVersion: installedLabel });
207189
- console.log(` \u672C\u5730\u6784\u5EFA \u2192 \u5DF2\u9489\u7248\u672C ${installedLabel}\uFF08\u81EA\u66F4\u65B0\u5BF9\u672C\u5B9E\u4F8B\u5931\u6548\uFF1B\u89E3\u9664\uFF1Aoasis daemon update --instance ${INSTANCE} --force\uFF09`);
208083
+ const plan = planStart(explicit, serverUrl);
208084
+ if (plan.mode === "resume" && plan.names.length > 1) {
208085
+ 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(", ")}`);
208086
+ } else if (plan.mode === "new" && !explicit && listInstances().length > 0) {
208087
+ 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]}`);
207190
208088
  }
207191
- stopDaemon(INSTANCE);
207192
- const tookOver = setupAutostart(INSTANCE);
207193
- const suffix = `${nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : ""}`;
207194
- if (!tookOver) {
207195
- const pid = spawnDaemon();
207196
- console.log(`\u2713 daemon started (instance ${INSTANCE}, PID ${pid})${suffix}`);
207197
- } else {
207198
- console.log(`\u2713 daemon started via systemd (${systemdUnitName(INSTANCE)})${suffix}`);
208089
+ for (const name of plan.names) {
208090
+ useInstance(name);
208091
+ await startOneInstance({ mode: plan.mode, serverUrl, upgrade, flags, applyName: plan.names.length === 1 });
207199
208092
  }
207200
- console.log(` instance: ${INSTANCE} nodeId: ${nodeId} config: ${INST.configFile}`);
207201
- console.log(` logs: tail -f ${LOG_FILE}`);
207202
208093
  if (!process.env["PATH"]?.includes(path32.dirname(LOCAL_BIN)))
207203
208094
  console.log(` add to PATH: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc`);
207204
208095
  return;
@@ -207226,13 +208117,13 @@ void (async () => {
207226
208117
  console.error("no config \u2014 run `oasis daemon start` first");
207227
208118
  process.exit(1);
207228
208119
  }
207229
- const forced = flags.has("force") || dargs.includes("--force");
207230
- console.log(`\u2192 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026${forced ? "\uFF08--force\uFF1A\u5FFD\u7565\u5E76\u89E3\u9664\u7248\u672C\u9489\u6B7B\uFF09" : ""}`);
207231
- if (selfUpdate({ force: forced })) {
207232
- console.log(`\u2713 \u66F4\u65B0\u5DF2\u89E6\u53D1\uFF08\u540E\u53F0\u62C9\u53D6 ${PKG_NAME}@latest \u5E76\u91CD\u542F\uFF09\uFF1B\u67E5\u770B\u65E5\u5FD7\uFF1Atail -f ${LOG_FILE}`);
207233
- } else {
207234
- console.log(`\uFF08\u672A\u89E6\u53D1\u66F4\u65B0\u2014\u2014\u672C\u5B9E\u4F8B\u5DF2\u9489\u7248\u672C\uFF09`);
208120
+ const cur = currentLabelOf(INST);
208121
+ if (cur && isLocalLabel(cur)) {
208122
+ console.log(` \u26A0 \u672C\u5B9E\u4F8B\u73B0\u5728\u8DD1\u7684\u662F\u672C\u5730\u6784\u5EFA ${cur}\uFF0C\u672C\u6B21\u66F4\u65B0\u4F1A\u7528 ${PKG_NAME}@latest \u8986\u76D6\u5B83\u3002`);
207235
208123
  }
208124
+ console.log(`\u2192 \u66F4\u65B0 ${PKG_NAME} \u5230\u6700\u65B0\u7248\u672C\u5E76\u91CD\u542F\u8282\u70B9\u2026`);
208125
+ selfUpdate();
208126
+ console.log(`\u2713 \u66F4\u65B0\u5DF2\u89E6\u53D1\uFF08\u540E\u53F0\u62C9\u53D6 ${PKG_NAME}@latest \u5E76\u91CD\u542F\uFF09\uFF1B\u67E5\u770B\u65E5\u5FD7\uFF1Atail -f ${LOG_FILE}`);
207236
208127
  return;
207237
208128
  }
207238
208129
  if (dsub === "status") {