oasis_test 0.1.103 → 0.1.105

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 +403 -107
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4440,6 +4440,14 @@ function classify(actorId) {
4440
4440
  if (actorId.startsWith("actor:human:")) return "human";
4441
4441
  return "unknown";
4442
4442
  }
4443
+ function pickReminderStep(cadence2, elapsedMs) {
4444
+ let hit = null;
4445
+ for (let i = 0; i < cadence2.length; i++) {
4446
+ const s2 = cadence2[i];
4447
+ if (elapsedMs >= s2.atMs) hit = { step: s2, index: i };
4448
+ }
4449
+ return hit;
4450
+ }
4443
4451
  function mergeConfig(cfg) {
4444
4452
  return {
4445
4453
  version: cfg?.version ?? "code-default-v1",
@@ -7364,9 +7372,9 @@ function checkTerminated(state) {
7364
7372
  if (!isBusinessDependencyEdge(state, edge)) continue;
7365
7373
  if (isEdgeInputUnobtainable(state, edge)) continue;
7366
7374
  const upstream = state.node(edge.fromNodeId);
7367
- if (!upstream?.latestWorkId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestWorkId)) return false;
7375
+ if (!upstream?.latestAcceptId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestAcceptId)) return false;
7368
7376
  }
7369
- return live.every((n) => n.latestWorkId !== null && n.latestAcceptId === n.latestWorkId);
7377
+ return live.every((n) => n.latestAcceptId !== null && (n.latestAcceptId === n.latestWorkId || n.latestAcceptId === n.latestSuccessWorkId));
7370
7378
  }
7371
7379
  function isEdgeInputUnobtainable(state, edge) {
7372
7380
  const upstream = state.node(edge.fromNodeId);
@@ -7394,10 +7402,17 @@ function acceptBlock(state, workId) {
7394
7402
  }
7395
7403
  const node = state.node(w2.nodeId);
7396
7404
  if (!node) return { code: "node-missing", detail: `\u8282\u70B9\u4E0D\u5728\u518C\uFF1A${w2.nodeId}`, overridable: false };
7397
- if (!w2.endedAt && !hasOutput(w2, state.artifactsOf(w2.id).length)) {
7405
+ if (!hasOutput(w2, state.artifactsOf(w2.id).length)) {
7406
+ if (!w2.endedAt) {
7407
+ return {
7408
+ code: "work-in-flight",
7409
+ detail: `\u8FD9\u4E00\u7248\u8FD8\u5728\u8DD1\u3001\u4E14\u8FD8\u6CA1\u6709\u4EFB\u4F55\u4EA7\u51FA\u2014\u2014\u7B49\u5B83\u4EA4\u7A3F\u540E\u518D\u6536\u53E3\uFF0C\u6216\u5148\u505C\u6389\uFF08hold\uFF09/ \u91CD\u8DD1`,
7410
+ overridable: false
7411
+ };
7412
+ }
7398
7413
  return {
7399
- code: "work-in-flight",
7400
- detail: `\u8FD9\u4E00\u7248\u8FD8\u5728\u8DD1\u3001\u4E14\u8FD8\u6CA1\u6709\u4EFB\u4F55\u4EA7\u51FA\u2014\u2014\u7B49\u5B83\u4EA4\u7A3F\u540E\u518D\u6536\u53E3\uFF0C\u6216\u5148\u505C\u6389\uFF08hold\uFF09/ \u91CD\u8DD1`,
7414
+ code: "no-output",
7415
+ detail: `\u8FD9\u4E00\u7248\u5DF2\u7ED3\u675F\u4F46\u6CA1\u6709\u4EFB\u4F55\u4EA7\u51FA\uFF08\u7A7A\u58F3\uFF09\u2014\u2014\u4E0D\u80FD\u628A\u6CA1\u6709\u5185\u5BB9\u7684\u7248\u672C\u6536\u53E3\u6210\u4EA4\u4ED8\u7248\u672C\uFF1B\u6539\u5BF9\u6700\u8FD1\u4E00\u7248\u6709\u4EA7\u51FA\u7684\u7248\u672C\u64CD\u4F5C\uFF0C\u6216\u91CD\u8DD1\u672C\u8282\u70B9`,
7401
7416
  overridable: false
7402
7417
  };
7403
7418
  }
@@ -7408,10 +7423,10 @@ function handoffBlock(state, nodeId, workId) {
7408
7423
  if (!requiresBusinessHandoff(state, nodeId, work)) return null;
7409
7424
  if (work?.acceptanceState === "accepted" && work.overrideBy) return null;
7410
7425
  const node = state.node(nodeId);
7411
- if (node?.latestWorkId !== workId) {
7426
+ if (workId !== node?.latestWorkId && workId !== node?.latestSuccessWorkId) {
7412
7427
  return {
7413
7428
  code: "not-latest-work",
7414
- detail: `\u4E0D\u662F\u672C\u8282\u70B9\u6700\u65B0\u4E00\u7248\uFF08\u6700\u65B0\uFF1A${node?.latestWorkId ?? "\u65E0"}\uFF09\u2014\u2014\u6536\u53E3\u53EA\u8BA4\u6700\u65B0\u7248`,
7429
+ detail: `\u4E0D\u662F\u672C\u8282\u70B9\u6700\u65B0\u7248\uFF08\u6700\u65B0\u5C1D\u8BD5\uFF1A${node?.latestWorkId ?? "\u65E0"}\uFF1B\u6700\u65B0\u6709\u6548\u4EA7\u51FA\uFF1A${node?.latestSuccessWorkId ?? "\u65E0"}\uFF09\u2014\u2014\u6536\u53E3\u53EA\u8BA4\u6700\u65B0\u4E00\u7248\u6216\u6700\u65B0\u6709\u6548\u4EA7\u51FA`,
7415
7430
  overridable: false
7416
7431
  };
7417
7432
  }
@@ -7504,6 +7519,7 @@ CREATE TABLE IF NOT EXISTS ${schema}.workflow_nodes (
7504
7519
  assignee_actor_id text,
7505
7520
  assignee_role text,
7506
7521
  latest_work_id text,
7522
+ latest_success_work_id text,
7507
7523
  latest_accept_id text,
7508
7524
  is_final_output boolean NOT NULL DEFAULT false,
7509
7525
  fields jsonb,
@@ -7765,6 +7781,9 @@ CREATE INDEX IF NOT EXISTS issues_workorder_alive_idx ON ${schema}.issues (worko
7765
7781
  CREATE INDEX IF NOT EXISTS issues_about_alive_idx ON ${schema}.issues (about_node_id) WHERE resolved_at IS NULL;
7766
7782
  CREATE INDEX IF NOT EXISTS issues_raised_alive_idx ON ${schema}.issues (raised_by_node_id) WHERE resolved_at IS NULL;
7767
7783
  ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS fields jsonb;
7784
+ -- \u2605 \u6709\u6548\u4EA7\u51FA\u6307\u9488\uFF08ADR 0148\uFF09\uFF1Anode \u4E0A\u300C\u6700\u8FD1\u4E00\u6B21\u5C1D\u8BD5\u300D\uFF08latest_work_id\uFF09\u4E4B\u5916\u518D\u5B58\u300C\u6700\u8FD1\u4E00\u6B21\u6709\u6548\u4EA7\u51FA\u300D\uFF0C
7785
+ -- \u5931\u8D25\u7A7A\u6D3E\u53D1\u4E0D\u518D\u9876\u6389\u6210\u54C1\u3002\u56DE\u586B\u5728\u4E0B\u65B9 works.status \u56DE\u586B\u4E4B\u540E\u6267\u884C\uFF08\u4F9D\u8D56 status \u5DF2\u843D\u5E93\uFF09\u3002
7786
+ ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS latest_success_work_id text;
7768
7787
  -- \u2605 \u5148\u5220\u89C6\u56FE\u518D\u5220\u5217\uFF1Awork_view/node_view/workorder_view \u7528 SELECT *\uFF0C\u5BF9\u57FA\u8868\u5217\u6709\u4F9D\u8D56\u2014\u2014\u5B58\u91CF\u5E93\u4E0A
7769
7788
  -- DROP COLUMN \u4F1A\u62A5 "cannot drop column ... because other objects depend on it"\uFF0C\u4E14\u672C\u6BB5\u662F\u5355\u6761
7770
7789
  -- \u591A\u8BED\u53E5 Query\uFF08\u4E00\u4E2A\u9690\u5F0F\u4E8B\u52A1\uFF09\uFF0C\u4E00\u5904\u5931\u8D25\u5168\u6BB5\u56DE\u6EDA\u3001migrate \u629B\u9519 \u2192 serve \u8D77\u4E0D\u6765\u3002\u89C6\u56FE\u968F\u540E\u7531
@@ -7820,6 +7839,16 @@ UPDATE ${schema}.reviews SET status = CASE
7820
7839
  WHEN ended_at IS NOT NULL THEN 'failed'
7821
7840
  ELSE 'running' END
7822
7841
  WHERE status IS NULL;
7842
+ -- \u2605 \u6709\u6548\u4EA7\u51FA\u6307\u9488\u56DE\u586B\uFF08ADR 0148\uFF09\uFF1Alatest_success_work_id = \u8BE5\u8282\u70B9\u6700\u8FD1\u4E00\u4E2A status=success \u7684**\u4E3B\u94FE** work
7843
+ -- \uFF08\u6392\u9664\u56DE\u4FE1 work reply_to_issue_id\uFF09\u3002\u5FC5\u987B\u5728\u4E0A\u9762 works.status \u56DE\u586B\u4E4B\u540E\uFF08\u4F9D\u8D56 status \u5DF2\u843D\u5E93\uFF09\u3002
7844
+ -- \u53EA\u586B\u5F53\u524D\u4E3A NULL \u7684\u884C \u2192 \u5E42\u7B49\uFF1A\u91CD\u542F\u65F6\u4E0D\u8986\u76D6 handler \u5DF2\u7EF4\u62A4\u7684\u503C\uFF1B\u4ECE\u672A\u6210\u529F\u8FC7\u7684\u8282\u70B9\u4FDD\u6301 NULL\u3002
7845
+ UPDATE ${schema}.workflow_nodes n SET latest_success_work_id = (
7846
+ SELECT w.id FROM ${schema}.works w
7847
+ WHERE w.node_id = n.id AND w.status = 'success' AND w.reply_to_issue_id IS NULL
7848
+ ORDER BY w.created_at DESC, w.id DESC
7849
+ LIMIT 1
7850
+ )
7851
+ WHERE n.latest_success_work_id IS NULL;
7823
7852
  `;
7824
7853
  }
7825
7854
  function ENGINE_VIEWS(schema) {
@@ -7835,20 +7864,25 @@ SELECT r.*, r.status AS state
7835
7864
  FROM ${schema}.reviews r;
7836
7865
 
7837
7866
  DROP VIEW IF EXISTS ${schema}.node_view;
7867
+ -- \u2605 ADR 0148\uFF1A\u4E3B\u6001\u6309 latest_success\uFF08\u6700\u8FD1\u4E00\u4E2A\u6709\u6548\u4EA7\u51FA\uFF09\u8D70\u2014\u2014\u300C\u5DF2\u4EA4\u4ED8 v3\u3001\u672B\u6B21\u5C1D\u8BD5 v4 \u5D29\u4E86\u300D\u663E\u793A\u5DF2\u5B8C\u6210\uFF0C
7868
+ -- \u672B\u6B21\u5931\u8D25\u53EA\u4F5C\u526F\u6807\u8BB0\uFF08\u7248\u672C\u5217\u8868\u91CC\u90A3\u6761 failed \u4ECD\u5728\uFF09\uFF0C\u4E0D\u518D\u51FA\u73B0\u300C\u56FE\u4E0A\u7EA2\u3001\u4E0B\u6E38\u5728\u8DD1\u300D\u7684\u5272\u88C2\u3002
7869
+ -- \u4E0E views.ts nodeState \u540C\u6E90\uFF1A\u5728\u8DD1\uFF08latest_work running\uFF09\u4F18\u5148\uFF1B\u5426\u5219\u6709 latest_success \u5C31\u6309\u5176\u9A8C\u6536\u4E0E\u5426\u5206
7870
+ -- \u5DF2\u5B8C\u6210/\u8BC4\u5BA1\u4E2D\uFF1B\u518D\u5426\u5219\u672B\u6B21\u5C1D\u8BD5\u5931\u8D25/dead \u2192 \u5361\u4F4F\u3002
7838
7871
  CREATE VIEW ${schema}.node_view AS
7839
7872
  SELECT n.*,
7840
7873
  CASE
7841
7874
  WHEN n.cancelled_at IS NOT NULL THEN 'dead' -- \u5DF2\u4F5C\u5E9F\uFF08\u79FB\u51FA\u8BA1\u5212\uFF09
7842
7875
  WHEN lw.id IS NULL THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08latest_work \u4E3A\u7A7A\uFF09
7843
7876
  WHEN lw.status = 'retry' THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08\u7528\u6237\u4ECB\u5165\u91CD\u8BD5\uFF0C\u7B49 try_to_run\uFF09
7844
- WHEN lw.status = 'running' THEN 'running' -- \u5728\u8FDB\u884C
7845
- WHEN lw.status = 'success' AND n.latest_accept_id IS DISTINCT FROM lw.id THEN 'reviewing' -- \u8BC4\u5BA1\u4E2D
7846
- WHEN lw.status = 'success' AND n.latest_accept_id = lw.id THEN 'done' -- \u5DF2\u5B8C\u6210
7847
- WHEN lw.status IN ('failed','dead') THEN 'dead' -- \u5361\u4F4F\u4E86
7877
+ WHEN lw.status = 'running' THEN 'running' -- \u5728\u8FDB\u884C\uFF08\u91CD\u8DD1\u8FDB\u884C\u4E2D\u4E5F\u4F18\u5148\u663E\u793A\u5728\u8FDB\u884C\uFF09
7878
+ WHEN ls.id IS NOT NULL AND n.latest_accept_id = ls.id THEN 'done' -- \u5DF2\u5B8C\u6210\uFF08\u6709\u6548\u4EA7\u51FA\u5DF2\u9A8C\u6536\uFF09
7879
+ WHEN ls.id IS NOT NULL AND n.latest_accept_id IS DISTINCT FROM ls.id THEN 'reviewing' -- \u8BC4\u5BA1\u4E2D\uFF08\u6709\u6548\u4EA7\u51FA\u672A\u9A8C\u6536\uFF09
7880
+ WHEN lw.status IN ('failed','dead') THEN 'dead' -- \u4ECE\u672A\u4EA7\u51FA\u4E14\u672B\u6B21\u5C1D\u8BD5\u5931\u8D25 \u2192 \u5361\u4F4F\u4E86
7848
7881
  ELSE 'waiting'
7849
7882
  END AS state
7850
7883
  FROM ${schema}.workflow_nodes n
7851
- LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id;
7884
+ LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id
7885
+ LEFT JOIN ${schema}.works ls ON ls.id = n.latest_success_work_id;
7852
7886
 
7853
7887
  DROP VIEW IF EXISTS ${schema}.workorder_view;
7854
7888
  CREATE VIEW ${schema}.workorder_view AS
@@ -13128,6 +13162,7 @@ var init_store_postgres = __esm({
13128
13162
  assigneeActorId: n.assignee_actor_id,
13129
13163
  assigneeRole: n.assignee_role,
13130
13164
  latestWorkId: n.latest_work_id,
13165
+ latestSuccessWorkId: n.latest_success_work_id ?? null,
13131
13166
  latestAcceptId: n.latest_accept_id,
13132
13167
  isFinalOutput: n.is_final_output,
13133
13168
  cancelledAt: iso(n.cancelled_at),
@@ -13304,9 +13339,9 @@ var init_store_postgres = __esm({
13304
13339
  await this.c.query(
13305
13340
  `INSERT INTO ${this.s}.workflow_nodes
13306
13341
  (id,workorder_id,type,title,spec,version,assignee_actor_id,assignee_role,
13307
- latest_work_id,latest_accept_id,is_final_output,
13342
+ latest_work_id,latest_success_work_id,latest_accept_id,is_final_output,
13308
13343
  fields,cancelled_at,row_version,created_at,updated_at)
13309
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
13344
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
13310
13345
  ON CONFLICT (id) DO NOTHING`,
13311
13346
  [
13312
13347
  n.id,
@@ -13318,6 +13353,7 @@ var init_store_postgres = __esm({
13318
13353
  n.assigneeActorId,
13319
13354
  n.assigneeRole,
13320
13355
  n.latestWorkId,
13356
+ n.latestSuccessWorkId,
13321
13357
  n.latestAcceptId,
13322
13358
  n.isFinalOutput,
13323
13359
  n.fields ? JSON.stringify(n.fields) : null,
@@ -13586,11 +13622,14 @@ var init_store_postgres = __esm({
13586
13622
  * `NOT EXISTS` 那一段是"我前面还有没跑完的兄弟就别动我";`SKIP LOCKED` 是
13587
13623
  * "别的 worker 正在处理这张工单就跳过整张"。两者合起来 = 按工单分区的串行队列。
13588
13624
  */
13589
- async claimNextEvent(now) {
13625
+ async claimNextEvent(now, recoverAppliedBefore) {
13590
13626
  const res = await this.c.query(
13591
13627
  `SELECT e.* FROM ${this.s}.events e
13592
- WHERE e.status = 'pending'
13593
- AND (e.available_at IS NULL OR e.available_at <= $1)
13628
+ WHERE (
13629
+ (e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1))
13630
+ -- \u5D29\u6E83\u81EA\u6108\uFF1A\u5361\u5728 applied \u592A\u4E45\u7684\uFF08\u8FDB\u7A0B\u6B7B\u5728\u300C\u843D\u72B6\u6001\u300D\u4E0E\u300C\u6536\u5C3E\u300D\u4E4B\u95F4\uFF09\u4E5F\u56DE\u6536\u91CD\u8DD1
13631
+ OR ($2::timestamptz IS NOT NULL AND e.status = 'applied' AND e.applied_at < $2::timestamptz)
13632
+ )
13594
13633
  AND NOT EXISTS (
13595
13634
  SELECT 1 FROM ${this.s}.events p
13596
13635
  WHERE p.workorder_id = e.workorder_id
@@ -13599,7 +13638,7 @@ var init_store_postgres = __esm({
13599
13638
  ORDER BY e.seq
13600
13639
  FOR UPDATE OF e SKIP LOCKED
13601
13640
  LIMIT 1`,
13602
- [now]
13641
+ [now, recoverAppliedBefore ?? null]
13603
13642
  );
13604
13643
  return res.rows[0] ? toRecord(res.rows[0]) : null;
13605
13644
  }
@@ -13640,10 +13679,14 @@ var init_store_postgres = __esm({
13640
13679
  );
13641
13680
  return (res.rowCount ?? 0) > 0;
13642
13681
  }
13643
- async claimAllPending(now) {
13682
+ async claimAllPending(now, recoverAppliedBefore) {
13644
13683
  const res = await this.c.query(
13645
13684
  `SELECT e.* FROM ${this.s}.events e
13646
- WHERE e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1)
13685
+ WHERE (
13686
+ (e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1))
13687
+ -- \u5D29\u6E83\u81EA\u6108\uFF1A\u89C1 claimNextEvent \u7684\u6CE8\u91CA
13688
+ OR ($2::timestamptz IS NOT NULL AND e.status = 'applied' AND e.applied_at < $2::timestamptz)
13689
+ )
13647
13690
  AND NOT EXISTS (SELECT 1 FROM ${this.s}.events p WHERE p.workorder_id = e.workorder_id
13648
13691
  AND p.status IN ('pending','applied') AND p.seq < e.seq)
13649
13692
  -- \u2605 scan \u4E8B\u4EF6\u6392\u524D\u9762\uFF1Aorigin='apply' \u4E14\u65E0\u4E0A\u6E38\u4E8B\u4EF6\uFF08\u4E0D\u662F\u88AB\u5176\u4ED6\u4E8B\u4EF6\u89E6\u53D1\u7684\uFF09
@@ -13653,7 +13696,7 @@ var init_store_postgres = __esm({
13653
13696
  CASE WHEN e.origin = 'apply' AND e.caused_by_seq IS NULL THEN 0 ELSE 1 END,
13654
13697
  e.seq
13655
13698
  FOR UPDATE OF e SKIP LOCKED`,
13656
- [now]
13699
+ [now, recoverAppliedBefore ?? null]
13657
13700
  );
13658
13701
  return res.rows.map(toRecord);
13659
13702
  }
@@ -13810,6 +13853,7 @@ function newNode(id, workorderId, at, version2) {
13810
13853
  assigneeActorId: null,
13811
13854
  assigneeRole: null,
13812
13855
  latestWorkId: null,
13856
+ latestSuccessWorkId: null,
13813
13857
  latestAcceptId: null,
13814
13858
  isFinalOutput: false,
13815
13859
  cancelledAt: null,
@@ -14138,6 +14182,10 @@ var init_workorder = __esm({
14138
14182
  });
14139
14183
 
14140
14184
  // ../engine/src/handlers/work.ts
14185
+ function latestSuccessOf(state, nodeId) {
14186
+ const successes = state.worksOf(nodeId).filter((w2) => !w2.replyToIssueId && w2.status === "success").sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14187
+ return successes.length > 0 ? successes[successes.length - 1].id : null;
14188
+ }
14141
14189
  var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workTimeout;
14142
14190
  var init_work = __esm({
14143
14191
  "../engine/src/handlers/work.ts"() {
@@ -14267,6 +14315,12 @@ var init_work = __esm({
14267
14315
  ...w2.status !== "dead" ? { status: "dead" } : {}
14268
14316
  });
14269
14317
  }
14318
+ if (node.latestSuccessWorkId) {
14319
+ const ls = state.work(node.latestSuccessWorkId);
14320
+ if (!ls || ls.status !== "success") {
14321
+ state.updateNode(e.nodeId, { latestSuccessWorkId: latestSuccessOf(state, e.nodeId) });
14322
+ }
14323
+ }
14270
14324
  killNodeReviews(state, e.nodeId, ctx.at);
14271
14325
  },
14272
14326
  async effect(e, ctx) {
@@ -14300,6 +14354,9 @@ var init_work = __esm({
14300
14354
  outcomeDetail: businessFailed ? { ...e.detail ?? {}, reason: "agent business handoff not delivered" } : e.detail,
14301
14355
  status
14302
14356
  });
14357
+ if (status === "success") {
14358
+ state.updateNode(w2.nodeId, { latestSuccessWorkId: e.workId });
14359
+ }
14303
14360
  }
14304
14361
  };
14305
14362
  workHandoffRecorded = {
@@ -14309,17 +14366,20 @@ var init_work = __esm({
14309
14366
  const w2 = state.work(e.workId);
14310
14367
  if (!w2 || e.producedBy !== "agent") return;
14311
14368
  if (w2.agentHandoffAttemptId && w2.agentHandoffAttemptId !== e.attemptId) return;
14369
+ const flipsToFailed = e.status === "not_delivered" && w2.endedAt && !w2.deadAt;
14312
14370
  state.updateWork(e.workId, {
14313
14371
  agentHandoffAttemptId: e.attemptId,
14314
14372
  businessHandoffStatus: e.status,
14315
14373
  businessHandoffRecordedAt: ctx.at,
14316
- // handoff 可以在 work.response 后到达。明确未交付必须回到现有失败/重试路径。
14317
- ...e.status === "not_delivered" && w2.endedAt && !w2.deadAt ? {
14374
+ ...flipsToFailed ? {
14318
14375
  status: "failed",
14319
14376
  outcome: "failed",
14320
14377
  outcomeDetail: { ...w2.outcomeDetail ?? {}, reason: "agent business handoff not delivered" }
14321
14378
  } : {}
14322
14379
  });
14380
+ if (flipsToFailed && state.node(w2.nodeId)?.latestSuccessWorkId === e.workId) {
14381
+ state.updateNode(w2.nodeId, { latestSuccessWorkId: latestSuccessOf(state, w2.nodeId) });
14382
+ }
14323
14383
  }
14324
14384
  };
14325
14385
  workStart = {
@@ -14865,7 +14925,7 @@ function emptySnapshotPlaceholder(record8) {
14865
14925
  reviews: []
14866
14926
  };
14867
14927
  }
14868
- var SYSTEM_ACTOR2, NOOP_IO, SYSTEM_CLOCK, HandlerRegistry, EngineBus, ApplyFailure, sleep;
14928
+ var SYSTEM_ACTOR2, NOOP_IO, SYSTEM_CLOCK, HandlerRegistry, DEFAULT_STUCK_APPLIED_RECOVERY_MS, EngineBus, ApplyFailure, sleep;
14869
14929
  var init_bus = __esm({
14870
14930
  "../engine/src/bus.ts"() {
14871
14931
  "use strict";
@@ -14903,6 +14963,7 @@ var init_bus = __esm({
14903
14963
  return [...this.byKind.values()].flat();
14904
14964
  }
14905
14965
  };
14966
+ DEFAULT_STUCK_APPLIED_RECOVERY_MS = 5 * 6e4;
14906
14967
  EngineBus = class {
14907
14968
  store;
14908
14969
  io;
@@ -14911,6 +14972,7 @@ var init_bus = __esm({
14911
14972
  policy;
14912
14973
  registry;
14913
14974
  maxEffectAttempts;
14975
+ stuckAppliedRecoveryMs;
14914
14976
  running = false;
14915
14977
  /** 事件监听器(内存 pubsub):每批事件处理完调用,供 /api/events SSE 推送(替代旧 oplog.subscribe)。 */
14916
14978
  eventListener = null;
@@ -14922,10 +14984,19 @@ var init_bus = __esm({
14922
14984
  this.policy = opts.policy ?? CODE_DEFAULT_POLICY;
14923
14985
  this.registry = new HandlerRegistry(opts.handlers);
14924
14986
  this.maxEffectAttempts = opts.maxEffectAttempts ?? 3;
14987
+ this.stuckAppliedRecoveryMs = opts.stuckAppliedRecoveryMs ?? DEFAULT_STUCK_APPLIED_RECOVERY_MS;
14925
14988
  }
14926
14989
  setIO(io) {
14927
14990
  this.io = io;
14928
14991
  }
14992
+ /**
14993
+ * 崩溃自愈水位线:早于此刻仍停在 `applied` 的事件,认为是「进程死在落状态与收尾之间」的残留,
14994
+ * 回收重跑。`stuckAppliedRecoveryMs <= 0` 时返回 undefined(= 关闭自愈,维持旧行为)。
14995
+ */
14996
+ recoverAppliedBefore() {
14997
+ if (this.stuckAppliedRecoveryMs <= 0) return void 0;
14998
+ return new Date(Date.parse(this.clock.now()) - this.stuckAppliedRecoveryMs).toISOString();
14999
+ }
14929
15000
  /** 注册事件监听器——新事件处理完即回调(serve 桥接 SSE 实时推送,替代 oplog.subscribe)。返回退订。 */
14930
15001
  onEvent(fn) {
14931
15002
  this.eventListener = fn;
@@ -15014,7 +15085,7 @@ var init_bus = __esm({
15014
15085
  let lastFullScan = 0;
15015
15086
  while (true) {
15016
15087
  try {
15017
- const batch = await this.store.transaction((tx) => tx.claimAllPending(this.clock.now()));
15088
+ const batch = await this.store.transaction((tx) => tx.claimAllPending(this.clock.now(), this.recoverAppliedBefore()));
15018
15089
  const touched = /* @__PURE__ */ new Set();
15019
15090
  if (batch.length > 0) {
15020
15091
  for (const record8 of batch) {
@@ -15247,6 +15318,10 @@ var init_bus = __esm({
15247
15318
  };
15248
15319
  let ok = true;
15249
15320
  for (const h of handlers) {
15321
+ const effectKey = `${h.name}#effect`;
15322
+ if (record8.appliedAt && await this.store.transaction((tx) => tx.isHandlerDone(record8.seq, effectKey))) {
15323
+ continue;
15324
+ }
15250
15325
  let lastErr = "";
15251
15326
  for (let attempt = 1; attempt <= this.maxEffectAttempts; attempt++) {
15252
15327
  try {
@@ -15259,6 +15334,8 @@ var init_bus = __esm({
15259
15334
  }
15260
15335
  if (lastErr) {
15261
15336
  ok = false;
15337
+ } else {
15338
+ await this.store.transaction((tx) => tx.markHandlerDone(record8.seq, effectKey));
15262
15339
  }
15263
15340
  }
15264
15341
  await this.store.transaction(async (tx) => {
@@ -15317,7 +15394,7 @@ var init_bus = __esm({
15317
15394
  /* ═══════════════════════ 内部:旧 applyPhase(claim + apply 一体) ═══════════════════════ */
15318
15395
  async applyPhase() {
15319
15396
  return this.store.transaction(async (tx) => {
15320
- const record8 = await tx.claimNextEvent(this.clock.now());
15397
+ const record8 = await tx.claimNextEvent(this.clock.now(), this.recoverAppliedBefore());
15321
15398
  if (!record8) return null;
15322
15399
  const snapshot = await tx.loadWorkorder(record8.workorderId);
15323
15400
  if (snapshot?.workorder.doneAt) {
@@ -15442,6 +15519,10 @@ function mapWorkStateToRevisionState(w2) {
15442
15519
  if (w2.acceptanceState === "rejected") return "rejected";
15443
15520
  return "proposed";
15444
15521
  // success but not yet judged
15522
+ // ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
15523
+ // superseded,改枚举会牵动契约与前端,见红线 4)——仍投影成 superseded 保持生命周期语义,但**内容
15524
+ // 如实为 empty**(workToRevision 对无产出 work 给 contentKind="empty")。读面(CLI 版本列表)据此把
15525
+ // 「superseded + empty」如实标成「未产出」,不再让「这次失败了」被读成「被更好的版本取代」。
15445
15526
  case "failed":
15446
15527
  return "superseded";
15447
15528
  case "retry":
@@ -15711,8 +15792,16 @@ function bridgeLoadReadModel(input) {
15711
15792
  const lw = node.latestWorkId ? workById.get(node.latestWorkId) : void 0;
15712
15793
  if (lw) {
15713
15794
  const st = lw.status ?? (lw.retryAt ? "retry" : lw.deadAt ? "dead" : lw.endedAt ? lw.outcome === "failed" ? "failed" : "success" : "running");
15714
- art.latestWorkState = st === "retry" ? null : st;
15715
- art.latestAccepted = node.latestAcceptId === node.latestWorkId;
15795
+ let uiSt;
15796
+ if (st === "retry") uiSt = null;
15797
+ else if (st === "running") uiSt = "running";
15798
+ else if (node.latestSuccessWorkId) uiSt = "success";
15799
+ else uiSt = st;
15800
+ art.latestWorkState = uiSt;
15801
+ art.latestAccepted = node.latestSuccessWorkId !== null && node.latestAcceptId === node.latestSuccessWorkId;
15802
+ if ((st === "failed" || st === "dead") && node.latestSuccessWorkId) {
15803
+ art.currentRev = node.latestSuccessWorkId;
15804
+ }
15716
15805
  } else {
15717
15806
  art.latestWorkState = null;
15718
15807
  art.latestAccepted = false;
@@ -15902,6 +15991,26 @@ var init_bridge_readmodel = __esm({
15902
15991
  }
15903
15992
  });
15904
15993
 
15994
+ // ../core/src/propose-guard.ts
15995
+ function proposeBlock(head, headArtifactCount) {
15996
+ if (!head) return null;
15997
+ const state = workState(head, hasOutput(head, headArtifactCount));
15998
+ if (state === "dead") {
15999
+ const why = head.rejectedReason ? `\uFF1A${head.rejectedReason}` : "";
16000
+ return {
16001
+ code: "head-dead",
16002
+ detail: `\u8FD9\u4E00\u7248\uFF08${head.id}\uFF09\u5DF2\u88AB\u505C\u6389\uFF08${head.deadAt ?? "?"}${why}\uFF09\u2014\u2014\u4F5C\u5E9F\u7248\u4E4B\u4E0A\u4E0D\u80FD\u518D\u4EA4\u7A3F\u3002\u8981\u7EE7\u7EED\uFF0C\u5148\u91CD\u8DD1\u672C\u8282\u70B9\uFF08\u5728\u63A7\u5236\u53F0\u70B9\u300C\u91CD\u65B0\u8FD0\u884C\u300D\uFF0C\u6216\u8BA9\u7BA1\u7406\u8005\u91CD\u6D3E\uFF09\u62FF\u5230\u4E00\u4E2A\u53EF\u5199\u7684\u65B0\u7248\u672C\uFF0C\u518D propose\u3002`
16003
+ };
16004
+ }
16005
+ return null;
16006
+ }
16007
+ var init_propose_guard = __esm({
16008
+ "../core/src/propose-guard.ts"() {
16009
+ "use strict";
16010
+ init_src3();
16011
+ }
16012
+ });
16013
+
15905
16014
  // ../core/src/preview-intervention.ts
15906
16015
  function resolveOwner(type, typeDef, roleIndex, schemaSize) {
15907
16016
  if (!typeDef) {
@@ -16206,7 +16315,11 @@ function previewIntervention(model, plan, config2) {
16206
16315
  if (lifecycleOf(fork, a.id) !== "active") continue;
16207
16316
  for (const e of a.inputs) {
16208
16317
  if (lifecycleOf(fork, e.to) === "sealed:cancelled") {
16209
- violations.push(`\u4F9D\u8D56\u5E9F\u5F03\u8282\u70B9: ${a.id} \u2192 ${e.to}\uFF08cancelled \u4E0D\u53EF\u88AB\u4F9D\u8D56\uFF09`);
16318
+ const consumerWs = fork.artifacts.get(a.id)?.workspace;
16319
+ const cancelledWs = fork.artifacts.get(e.to)?.workspace;
16320
+ violations.push(
16321
+ `\u4F9D\u8D56\u5E9F\u5F03\u8282\u70B9: ${a.id}${consumerWs ? `\uFF08${consumerWs}\uFF09` : ""} \u2192 ${e.to}${cancelledWs ? `\uFF08${cancelledWs}\uFF09` : ""}\uFF08cancelled \u4E0D\u53EF\u88AB\u4F9D\u8D56\uFF09`
16322
+ );
16210
16323
  }
16211
16324
  }
16212
16325
  }
@@ -16367,6 +16480,7 @@ var init_kernel_bridge = __esm({
16367
16480
  init_closure_gate();
16368
16481
  init_kernel();
16369
16482
  init_bridge_readmodel();
16483
+ init_propose_guard();
16370
16484
  init_src3();
16371
16485
  init_src3();
16372
16486
  init_preview_intervention();
@@ -16479,26 +16593,26 @@ var init_kernel_bridge = __esm({
16479
16593
  convergenceUnified: this.model.convergenceUnified,
16480
16594
  disabledActors: this.model.disabledActors
16481
16595
  });
16482
- for (const [draftId, card] of this.draftCards) {
16483
- const anchorFromPlan = card.plan?.ops.find((o) => "artifactId" in o && o.artifactId) ?? card.plan?.ops.find((o) => "id" in o && o.id);
16484
- const anchorId = card.anchor ?? (anchorFromPlan ? anchorFromPlan.artifactId ?? anchorFromPlan.id : void 0) ?? "";
16485
- const cardWs = card.plan?.workspace ?? this.model.artifacts.get(anchorId)?.workspace ?? card.anchor ?? "";
16596
+ for (const [draftId, card2] of this.draftCards) {
16597
+ const anchorFromPlan = card2.plan?.ops.find((o) => "artifactId" in o && o.artifactId) ?? card2.plan?.ops.find((o) => "id" in o && o.id);
16598
+ const anchorId = card2.anchor ?? (anchorFromPlan ? anchorFromPlan.artifactId ?? anchorFromPlan.id : void 0) ?? "";
16599
+ const cardWs = card2.plan?.workspace ?? this.model.artifacts.get(anchorId)?.workspace ?? card2.anchor ?? "";
16486
16600
  fresh.pendingReviews.set(draftId, {
16487
16601
  draftId,
16488
- plan: card.plan ?? { workspace: cardWs, reason: "", ops: [] },
16489
- changeClass: card.changeClass ?? "B",
16490
- nonMonotonicOps: card.nonMonotonicOps ?? [],
16491
- proposedBy: card.proposedBy,
16492
- origin: card.origin ?? null,
16602
+ plan: card2.plan ?? { workspace: cardWs, reason: "", ops: [] },
16603
+ changeClass: card2.changeClass ?? "B",
16604
+ nonMonotonicOps: card2.nonMonotonicOps ?? [],
16605
+ proposedBy: card2.proposedBy,
16606
+ origin: card2.origin ?? null,
16493
16607
  workspace: cardWs,
16494
16608
  advisory: [],
16495
16609
  downstreamClosure: [],
16496
16610
  at: "",
16497
16611
  // ★ anchor 也兜底到 plan ops 的产物 id——inbox change-confirm 用 r.anchor 找「anchor owner 的
16498
16612
  // 人类上级」(leadOf(r.anchor));graph-edit 卡之前恒空、boss 收不到卡(collab-inbox-audit 实测)。
16499
- anchor: anchorId || card.anchor || "",
16500
- kind: card.kind,
16501
- ...card.command ? { command: { command: card.command, args: card.commandArgs ?? {}, effectLabel: card.effectLabel ?? "" } } : {}
16613
+ anchor: anchorId || card2.anchor || "",
16614
+ kind: card2.kind,
16615
+ ...card2.command ? { command: { command: card2.command, args: card2.commandArgs ?? {}, effectLabel: card2.effectLabel ?? "" } } : {}
16502
16616
  });
16503
16617
  }
16504
16618
  Object.assign(this.model, fresh);
@@ -16596,8 +16710,8 @@ var init_kernel_bridge = __esm({
16596
16710
  typeName: a.type
16597
16711
  };
16598
16712
  }
16599
- cancelCascadeEdges(_id) {
16600
- return [];
16713
+ cancelCascadeEdges(id) {
16714
+ return edgesTouching(this.model, id).map((e) => ({ from: e.artifactId, to: e.to }));
16601
16715
  }
16602
16716
  /* ═══════════════════════ 写方法 ═══════════════════════ */
16603
16717
  // ── 建节点 ──
@@ -16697,8 +16811,14 @@ var init_kernel_bridge = __esm({
16697
16811
  }
16698
16812
  }
16699
16813
  if (!workId) {
16700
- const deadRev = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId && r.state === "superseded").sort((a, b2) => b2.createdAt.localeCompare(a.createdAt))[0];
16701
- if (deadRev) return deadRev;
16814
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16815
+ const headId = snap?.nodes.find((n) => n.id === args.artifactId)?.latestWorkId ?? null;
16816
+ if (headId) {
16817
+ const headWork = snap?.works.find((w2) => w2.id === headId);
16818
+ const headOutputs = snap?.artifacts.filter((a) => a.workId === headId).length ?? 0;
16819
+ const block = proposeBlock(headWork, headOutputs);
16820
+ if (block) throw new KernelError(block.detail, block.code);
16821
+ }
16702
16822
  const worksCount = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId).length;
16703
16823
  const revisionId = `wk:${args.artifactId}:${worksCount + 1}`;
16704
16824
  await this.commit({
@@ -16974,7 +17094,8 @@ var init_kernel_bridge = __esm({
16974
17094
  const artifact = this.model.artifacts.get(args.artifactId);
16975
17095
  const currentRev = artifact?.currentRev ? this.model.revisions.get(artifact.currentRev) : void 0;
16976
17096
  const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16977
- const head = (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? concludedHead(this.model, args.artifactId) ?? (snap?.nodes.find((n) => n.id === args.artifactId)?.latestWorkId ?? null);
17097
+ const targetNode = snap?.nodes.find((n) => n.id === args.artifactId);
17098
+ const head = (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? concludedHead(this.model, args.artifactId) ?? (targetNode?.latestSuccessWorkId ?? targetNode?.latestWorkId ?? null);
16978
17099
  if (!head) throw new KernelError(`force-conclude \u65E0\u4ECE\u4E0B\u624B\uFF08${args.artifactId}\uFF09\uFF1A\u672C\u8282\u70B9\u8FD8\u6CA1\u6709\u4EFB\u4F55\u4E00\u7248\u4EA7\u51FA`, "no-head");
16979
17100
  const block = snap ? acceptBlock(new WorkorderState(snap), head) : null;
16980
17101
  if (block?.code === "already-accepted") return;
@@ -17326,6 +17447,13 @@ var init_kernel_bridge = __esm({
17326
17447
  }
17327
17448
  async seal(args) {
17328
17449
  const wid = this.wo(args.artifactId);
17450
+ if (args.reason === "cancelled") {
17451
+ const preserve = args.preserveEdgesAmong;
17452
+ for (const e of edgesTouching(this.model, args.artifactId)) {
17453
+ if (preserve && preserve.has(e.artifactId) && preserve.has(e.to)) continue;
17454
+ await this.unlinkInput({ artifactId: e.artifactId, to: e.to, actor: args.actor });
17455
+ }
17456
+ }
17329
17457
  await this.commit({
17330
17458
  companyId: "",
17331
17459
  workorderId: wid,
@@ -17521,10 +17649,10 @@ var init_kernel_bridge = __esm({
17521
17649
  async proposeCommandAuthorization(args) {
17522
17650
  const originNorm = args.origin ?? null;
17523
17651
  const argsJson = JSON.stringify(args.args ?? {});
17524
- for (const [id, card] of this.draftCards) {
17525
- if (card.kind !== "command" || card.command !== args.command) continue;
17526
- if ((card.origin ?? null) !== originNorm) continue;
17527
- if (JSON.stringify(card.commandArgs ?? {}) !== argsJson) continue;
17652
+ for (const [id, card2] of this.draftCards) {
17653
+ if (card2.kind !== "command" || card2.command !== args.command) continue;
17654
+ if ((card2.origin ?? null) !== originNorm) continue;
17655
+ if (JSON.stringify(card2.commandArgs ?? {}) !== argsJson) continue;
17528
17656
  await this.refreshModel();
17529
17657
  return { draftId: id };
17530
17658
  }
@@ -17542,23 +17670,23 @@ var init_kernel_bridge = __esm({
17542
17670
  return { draftId };
17543
17671
  }
17544
17672
  async applyAuthorizedCommand(args) {
17545
- const card = this.draftCards.get(args.draftId);
17546
- if (!card || card.kind !== "command" || !card.command) {
17673
+ const card2 = this.draftCards.get(args.draftId);
17674
+ if (!card2 || card2.kind !== "command" || !card2.command) {
17547
17675
  throw new Error(`\u4E0D\u662F\u547D\u4EE4\u6388\u6743\u8BF7\u6C42\u6216\u5DF2\u5904\u7406\uFF1A${args.draftId}`);
17548
17676
  }
17549
- const artifactId = card.anchor;
17550
- if (card.command === "forceConclude") {
17677
+ const artifactId = card2.anchor;
17678
+ if (card2.command === "forceConclude") {
17551
17679
  await this.forceConclude({
17552
17680
  artifactId,
17553
17681
  actor: args.actor,
17554
- reason: typeof card.commandArgs?.["reason"] === "string" ? card.commandArgs["reason"] : "\u7BA1\u7406\u8005\u8BF7\u6C42\u3001\u4EBA\u5DF2\u6388\u6743"
17682
+ reason: typeof card2.commandArgs?.["reason"] === "string" ? card2.commandArgs["reason"] : "\u7BA1\u7406\u8005\u8BF7\u6C42\u3001\u4EBA\u5DF2\u6388\u6743"
17555
17683
  });
17556
- } else if (card.command === "resolveEscalation") {
17557
- const escalationId = typeof card.commandArgs?.["escalationId"] === "string" ? card.commandArgs["escalationId"] : void 0;
17684
+ } else if (card2.command === "resolveEscalation") {
17685
+ const escalationId = typeof card2.commandArgs?.["escalationId"] === "string" ? card2.commandArgs["escalationId"] : void 0;
17558
17686
  if (!escalationId) {
17559
17687
  throw new Error(`resolveEscalation \u6388\u6743\u5361\u7F3A escalationId\uFF1A${args.draftId}`);
17560
17688
  }
17561
- const reason = typeof card.commandArgs?.["reason"] === "string" ? card.commandArgs["reason"] : void 0;
17689
+ const reason = typeof card2.commandArgs?.["reason"] === "string" ? card2.commandArgs["reason"] : void 0;
17562
17690
  await this.resolveEscalation({
17563
17691
  artifactId,
17564
17692
  actor: args.actor,
@@ -17566,23 +17694,39 @@ var init_kernel_bridge = __esm({
17566
17694
  ...reason !== void 0 ? { reason } : {}
17567
17695
  });
17568
17696
  } else {
17569
- throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u4E0D\u652F\u6301\u6267\u884C ${card.command}`);
17697
+ throw new Error(`\u547D\u4EE4\u6388\u6743\uFF1A\u6682\u4E0D\u652F\u6301\u6267\u884C ${card2.command}`);
17570
17698
  }
17571
17699
  this.draftCards.delete(args.draftId);
17572
17700
  await this.refreshModel();
17573
- return { command: card.command, artifactId };
17701
+ return { command: card2.command, artifactId };
17574
17702
  }
17575
17703
  async rejectIntervention(args) {
17576
17704
  this.draftCards.delete(args.draftId);
17577
17705
  await this.refreshModel();
17578
17706
  }
17707
+ /**
17708
+ * 有效 workspace:优先 plan.workspace,缺省时从首个带 workspace 的 spawn op 兜底(与
17709
+ * applyIntervention 里 wid 的兜底同源,见下方 line ~1453)。
17710
+ * ★ 诊断 9f589b81 §② B:建单方案(buildDynamicWorkorderPlan)曾漏传顶层 plan.workspace,导致
17711
+ * preview 的 workspace 过滤短路、拿全公司读模型跑终态校验——别家工单里一条僵尸边就能拒掉无关建单
17712
+ * (事故半径=全组织)。planner 已补 plan.workspace(根治),此兜底再保一层:只要 plan 里有带
17713
+ * workspace 的 spawn op,就绝不静默 fallback 到全公司。两者皆缺才回退全模型(如纯 seal/unlink 单节点
17714
+ * 改图,此时 wo() 反查也能定位,全局校验只是更宽、不误伤建单)。
17715
+ */
17716
+ effectiveWorkspace(plan) {
17717
+ if (plan.workspace) return plan.workspace;
17718
+ const spawn8 = plan.ops.find(
17719
+ (o) => o.action === "spawn" && o.workspace
17720
+ );
17721
+ return spawn8?.workspace;
17722
+ }
17579
17723
  async previewIntervention(plan) {
17580
- const ws = plan.workspace;
17724
+ const ws = this.effectiveWorkspace(plan);
17581
17725
  const scopedModel = ws ? filterReadModelByWorkspace(this.model, ws) : this.model;
17582
17726
  return previewIntervention(scopedModel, plan, { schema: this.schema, roleIndex: this.roleIndex });
17583
17727
  }
17584
17728
  async applyIntervention(plan, _baseSeqs, _actor) {
17585
- const ws = plan.workspace;
17729
+ const ws = this.effectiveWorkspace(plan);
17586
17730
  const scopedModel = ws ? filterReadModelByWorkspace(this.model, ws) : this.model;
17587
17731
  plan = resolveInterventionRefs(scopedModel, plan, this.schema);
17588
17732
  const preview2 = await this.previewIntervention(plan);
@@ -18128,9 +18272,9 @@ var init_crypto = __esm({
18128
18272
  function buildTextCard(text, opts) {
18129
18273
  const elements = [{ tag: "div", text: { tag: "lark_md", content: text || " " } }];
18130
18274
  if (opts?.footer) elements.push({ tag: "note", elements: [{ tag: "lark_md", content: opts.footer }] });
18131
- const card = { config: { wide_screen_mode: true, update_multi: true }, elements };
18132
- if (opts?.header) card["header"] = { title: { tag: "plain_text", content: opts.header } };
18133
- return card;
18275
+ const card2 = { config: { wide_screen_mode: true, update_multi: true }, elements };
18276
+ if (opts?.header) card2["header"] = { title: { tag: "plain_text", content: opts.header } };
18277
+ return card2;
18134
18278
  }
18135
18279
  var BASE, FeishuApiClient;
18136
18280
  var init_api = __esm({
@@ -18209,26 +18353,26 @@ var init_api = __esm({
18209
18353
  };
18210
18354
  }
18211
18355
  /** 发一张 interactive 卡片,返回 message_id(后续 patch 同一条消息实现流式更新)。 */
18212
- async sendCard(chatId, card) {
18356
+ async sendCard(chatId, card2) {
18213
18357
  const token = await this.tenantAccessToken();
18214
18358
  const res = await this.fetchImpl(`${this.base}/open-apis/im/v1/messages?receive_id_type=chat_id`, {
18215
18359
  method: "POST",
18216
18360
  headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token}` },
18217
- body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) })
18361
+ body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card2) })
18218
18362
  });
18219
18363
  const body = await res.json();
18220
18364
  if (body.code !== 0) throw new Error(`feishu send card failed: code=${body.code} msg=${body.msg ?? ""}`);
18221
18365
  return { messageId: body.data?.message_id ?? "" };
18222
18366
  }
18223
18367
  /** 就地更新一张已发出的 interactive 卡片(流式回复靠反复 patch 同一条消息)。 */
18224
- async patchCard(messageId, card) {
18368
+ async patchCard(messageId, card2) {
18225
18369
  const token = await this.tenantAccessToken();
18226
18370
  const res = await this.fetchImpl(
18227
18371
  `${this.base}/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`,
18228
18372
  {
18229
18373
  method: "PATCH",
18230
18374
  headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token}` },
18231
- body: JSON.stringify({ content: JSON.stringify(card) })
18375
+ body: JSON.stringify({ content: JSON.stringify(card2) })
18232
18376
  }
18233
18377
  );
18234
18378
  const body = await res.json();
@@ -18671,11 +18815,11 @@ var init_service = __esm({
18671
18815
  this.dirty = false;
18672
18816
  const snapshot = this.buf;
18673
18817
  try {
18674
- const card = buildTextCard(snapshot, { footer: "\u25CF \u751F\u6210\u4E2D\u2026" });
18818
+ const card2 = buildTextCard(snapshot, { footer: "\u25CF \u751F\u6210\u4E2D\u2026" });
18675
18819
  if (!this.messageId) {
18676
- this.messageId = (await this.client.sendCard(this.chatId, card)).messageId;
18820
+ this.messageId = (await this.client.sendCard(this.chatId, card2)).messageId;
18677
18821
  } else {
18678
- await this.client.patchCard(this.messageId, card);
18822
+ await this.client.patchCard(this.messageId, card2);
18679
18823
  }
18680
18824
  } catch (e) {
18681
18825
  this.log(`feishu card stream tick failed: ${errMsg(e)}`);
@@ -18689,10 +18833,10 @@ var init_service = __esm({
18689
18833
  clearInterval(this.timer);
18690
18834
  this.timer = null;
18691
18835
  }
18692
- const card = buildTextCard(this.buf || "\uFF08\u65E0\u8F93\u51FA\uFF09");
18836
+ const card2 = buildTextCard(this.buf || "\uFF08\u65E0\u8F93\u51FA\uFF09");
18693
18837
  try {
18694
- if (!this.messageId) await this.client.sendCard(this.chatId, card);
18695
- else await this.client.patchCard(this.messageId, card);
18838
+ if (!this.messageId) await this.client.sendCard(this.chatId, card2);
18839
+ else await this.client.patchCard(this.messageId, card2);
18696
18840
  } catch (e) {
18697
18841
  this.log(`feishu card finalize failed: ${errMsg(e)}`);
18698
18842
  }
@@ -18703,10 +18847,10 @@ var init_service = __esm({
18703
18847
  clearInterval(this.timer);
18704
18848
  this.timer = null;
18705
18849
  }
18706
- const card = buildTextCard(`${this.buf ? this.buf + "\n\n" : ""}\u26A0\uFE0F ${message}`);
18850
+ const card2 = buildTextCard(`${this.buf ? this.buf + "\n\n" : ""}\u26A0\uFE0F ${message}`);
18707
18851
  try {
18708
- if (!this.messageId) await this.client.sendCard(this.chatId, card);
18709
- else await this.client.patchCard(this.messageId, card);
18852
+ if (!this.messageId) await this.client.sendCard(this.chatId, card2);
18853
+ else await this.client.patchCard(this.messageId, card2);
18710
18854
  } catch (e) {
18711
18855
  this.log(`feishu card error-finalize failed: ${errMsg(e)}`);
18712
18856
  }
@@ -127492,14 +127636,14 @@ var require_lib4 = __commonJS({
127492
127636
  });
127493
127637
  }
127494
127638
  };
127495
- function appendErrorFooter(card) {
127496
- const cardObj = card;
127639
+ function appendErrorFooter(card2) {
127640
+ const cardObj = card2;
127497
127641
  const elements = Array.isArray(cardObj === null || cardObj === void 0 ? void 0 : cardObj.elements) ? [...cardObj.elements] : [];
127498
127642
  elements.push({
127499
127643
  tag: "note",
127500
127644
  elements: [{ tag: "plain_text", content: "\u26A0\uFE0F \u751F\u6210\u4E2D\u65AD" }]
127501
127645
  });
127502
- return Object.assign(Object.assign({}, card), { elements });
127646
+ return Object.assign(Object.assign({}, card2), { elements });
127503
127647
  }
127504
127648
  var CardStreamController = class {
127505
127649
  constructor(sender, to, idType, opts, initial) {
@@ -128268,13 +128412,13 @@ ${block}
128268
128412
  });
128269
128413
  }
128270
128414
  // ─── card ─────────────────────────────────────────────
128271
- sendCard(to, idType, card, opts) {
128415
+ sendCard(to, idType, card2, opts) {
128272
128416
  return __awaiter(this, void 0, void 0, function* () {
128273
128417
  const id = yield this.sendOneWithFallback({
128274
128418
  to,
128275
128419
  idType,
128276
128420
  msgType: "interactive",
128277
- content: card,
128421
+ content: card2,
128278
128422
  replyTo: opts.replyTo,
128279
128423
  replyInThread: opts.replyInThread
128280
128424
  });
@@ -128386,11 +128530,11 @@ ${block}
128386
128530
  * and the public `channel.updateCard()` low-level API. Not suitable for
128387
128531
  * text streaming (use native cardkit streaming below for that).
128388
128532
  */
128389
- patchCard(messageId, card) {
128533
+ patchCard(messageId, card2) {
128390
128534
  return __awaiter(this, void 0, void 0, function* () {
128391
128535
  yield this.client.im.v1.message.patch({
128392
128536
  path: { message_id: messageId },
128393
- data: { content: JSON.stringify(card) }
128537
+ data: { content: JSON.stringify(card2) }
128394
128538
  });
128395
128539
  });
128396
128540
  }
@@ -129941,9 +130085,9 @@ ${lines.join("\n")}
129941
130085
  });
129942
130086
  }
129943
130087
  // ─── low-level ─────────────────────────────────────────
129944
- updateCard(messageId, card) {
130088
+ updateCard(messageId, card2) {
129945
130089
  return __awaiter(this, void 0, void 0, function* () {
129946
- yield this.sender.patchCard(messageId, card);
130090
+ yield this.sender.patchCard(messageId, card2);
129947
130091
  });
129948
130092
  }
129949
130093
  /**
@@ -149182,7 +149326,7 @@ async function buildDynamicWorkorderPlan(input) {
149182
149326
  }
149183
149327
  }
149184
149328
  const plan = withClosureReport(
149185
- { reason: `\u52A8\u6001\u89C4\u5212\u5DE5\u5355\uFF1A${displayTitle}`, ops },
149329
+ { reason: `\u52A8\u6001\u89C4\u5212\u5DE5\u5355\uFF1A${displayTitle}`, ops, workspace: input.workspace },
149186
149330
  { draftedBy: input.actor }
149187
149331
  );
149188
149332
  const closureOp = plan.ops.find(
@@ -179630,6 +179774,126 @@ var init_inbox_chat = __esm({
179630
179774
  }
179631
179775
  });
179632
179776
 
179777
+ // ../server/src/domains/collab/waiting-inbox.ts
179778
+ function card(kind, row, severity, since, summary) {
179779
+ return {
179780
+ kind,
179781
+ ...row.artifactId ? { artifactId: row.artifactId } : {},
179782
+ workspace: row.workorderId,
179783
+ ...row.title ? { title: row.title } : {},
179784
+ since,
179785
+ actionRequired: true,
179786
+ summary,
179787
+ // info 档只提示不算 overdue;warn/urgent 置 overdue(前端红点/排序)。
179788
+ ...severity === "info" ? {} : { overdue: true }
179789
+ };
179790
+ }
179791
+ function buildWaitingInbox(reviews, works, policy, nowIso, me) {
179792
+ const now = Date.parse(nowIso);
179793
+ const out = [];
179794
+ const consider = (rows, scenario, kind) => {
179795
+ for (const r of rows) {
179796
+ if (r.actorId !== me) continue;
179797
+ const start = r.startedAt || r.createdAt;
179798
+ if (r.status === "dead") {
179799
+ out.push(card(kind, r, "urgent", start, `\u4F60\u7684${KIND_LABEL[kind]}\u5DF2\u4E2D\u6B62\uFF0C\u9700\u8981\u4F60\u4ECB\u5165\u91CD\u5F00`));
179800
+ continue;
179801
+ }
179802
+ if (r.status !== "running") continue;
179803
+ if (classify(me) !== "human") continue;
179804
+ const action = policy.resolve({ executor: "human", scenario }).timeoutAction;
179805
+ if (action.kind !== "remind") continue;
179806
+ const elapsed = Number.isFinite(now) ? now - Date.parse(start) : NaN;
179807
+ if (!Number.isFinite(elapsed)) continue;
179808
+ const picked = pickReminderStep(action.cadence, elapsed);
179809
+ if (!picked) continue;
179810
+ const mins = Math.round(elapsed / 6e4);
179811
+ out.push(card(kind, r, picked.step.severity, start, `\u6709\u4E00\u9879${KIND_LABEL[kind]}\u5728\u7B49\u4F60\uFF0C\u5DF2 ${mins} \u5206\u949F\u672A\u52A8`));
179812
+ }
179813
+ };
179814
+ consider(reviews, "review", "review-waiting");
179815
+ consider(works, "work", "work-waiting");
179816
+ return out;
179817
+ }
179818
+ function waitingWorkspacesFor(model, me) {
179819
+ const out = /* @__PURE__ */ new Set();
179820
+ for (const a of model.artifacts.values()) {
179821
+ if (lifecycleOf(model, a.id) !== "active") continue;
179822
+ if (a.owner === me) {
179823
+ out.add(a.workspace);
179824
+ continue;
179825
+ }
179826
+ if (pendingConcludeAttempt(model, a.id)?.gate?.eligible.includes(me)) out.add(a.workspace);
179827
+ }
179828
+ return out;
179829
+ }
179830
+ function collectWaitingRows(snapshots2, me) {
179831
+ const reviews = [];
179832
+ const works = [];
179833
+ const latestBy = (map, row) => {
179834
+ const prev = map.get(row.nodeId);
179835
+ if (!prev || row.createdAt > prev.createdAt) map.set(row.nodeId, row);
179836
+ };
179837
+ for (const snap of snapshots2) {
179838
+ const nodeTitle = new Map(snap.nodes.map((n) => [n.id, n.title]));
179839
+ const cancelled = new Set(snap.nodes.filter((n) => n.cancelledAt).map((n) => n.id));
179840
+ const outputCountOf = (workId) => snap.artifacts.reduce((n, a) => a.workId === workId ? n + 1 : n, 0);
179841
+ const latestReview = /* @__PURE__ */ new Map();
179842
+ for (const r of snap.reviews) {
179843
+ if (r.reviewerActorId !== me || cancelled.has(r.nodeId)) continue;
179844
+ latestBy(latestReview, r);
179845
+ }
179846
+ for (const r of latestReview.values()) {
179847
+ const st = reviewState(r);
179848
+ if (st !== "running" && st !== "dead") continue;
179849
+ reviews.push({
179850
+ id: r.id,
179851
+ workorderId: snap.workorder.id,
179852
+ artifactId: r.nodeId,
179853
+ actorId: r.reviewerActorId,
179854
+ ...nodeTitle.get(r.nodeId) ? { title: nodeTitle.get(r.nodeId) } : {},
179855
+ createdAt: r.createdAt,
179856
+ startedAt: r.startedAt,
179857
+ status: st
179858
+ });
179859
+ }
179860
+ const latestMain = /* @__PURE__ */ new Map();
179861
+ const latestReply = /* @__PURE__ */ new Map();
179862
+ for (const w2 of snap.works) {
179863
+ if (w2.assigneeActorId !== me || cancelled.has(w2.nodeId)) continue;
179864
+ latestBy(w2.replyToIssueId ? latestReply : latestMain, w2);
179865
+ }
179866
+ for (const w2 of [...latestMain.values(), ...latestReply.values()]) {
179867
+ const st = workState(w2, hasOutput(w2, outputCountOf(w2.id)));
179868
+ if (st !== "running" && st !== "dead") continue;
179869
+ works.push({
179870
+ id: w2.id,
179871
+ workorderId: snap.workorder.id,
179872
+ artifactId: w2.nodeId,
179873
+ actorId: w2.assigneeActorId,
179874
+ ...nodeTitle.get(w2.nodeId) ? { title: nodeTitle.get(w2.nodeId) } : {},
179875
+ createdAt: w2.createdAt,
179876
+ startedAt: w2.startedAt,
179877
+ status: st
179878
+ });
179879
+ }
179880
+ }
179881
+ return { reviews, works };
179882
+ }
179883
+ var KIND_LABEL;
179884
+ var init_waiting_inbox = __esm({
179885
+ "../server/src/domains/collab/waiting-inbox.ts"() {
179886
+ "use strict";
179887
+ init_src4();
179888
+ init_src3();
179889
+ init_src2();
179890
+ KIND_LABEL = {
179891
+ "review-waiting": "\u8BC4\u5BA1",
179892
+ "work-waiting": "\u5904\u7406"
179893
+ };
179894
+ }
179895
+ });
179896
+
179633
179897
  // ../server/src/domains/collab/audit.ts
179634
179898
  function toAuditEntry(op) {
179635
179899
  return {
@@ -179638,7 +179902,7 @@ function toAuditEntry(op) {
179638
179902
  actor: op.actor,
179639
179903
  op: op.kind,
179640
179904
  artifactId: op.artifactId,
179641
- summary: `${KIND_LABEL[op.kind] ?? op.kind} \xB7 ${op.artifactId}`
179905
+ summary: `${KIND_LABEL2[op.kind] ?? op.kind} \xB7 ${op.artifactId}`
179642
179906
  };
179643
179907
  }
179644
179908
  function eventToAuditEntry(rec) {
@@ -179650,7 +179914,7 @@ function eventToAuditEntry(rec) {
179650
179914
  actor: rec.actorId,
179651
179915
  op: ev.kind,
179652
179916
  artifactId,
179653
- summary: `${KIND_LABEL[ev.kind] ?? ev.kind}${artifactId ? ` \xB7 ${artifactId}` : ""}`
179917
+ summary: `${KIND_LABEL2[ev.kind] ?? ev.kind}${artifactId ? ` \xB7 ${artifactId}` : ""}`
179654
179918
  };
179655
179919
  }
179656
179920
  function enrichAudit(entry, model, titleOf) {
@@ -179726,12 +179990,12 @@ async function buildAudit(source, q = {}, model) {
179726
179990
  }
179727
179991
  return { items, cursor: truncated ? lastCursor : null };
179728
179992
  }
179729
- var KIND_LABEL;
179993
+ var KIND_LABEL2;
179730
179994
  var init_audit = __esm({
179731
179995
  "../server/src/domains/collab/audit.ts"() {
179732
179996
  "use strict";
179733
179997
  init_node_state();
179734
- KIND_LABEL = {
179998
+ KIND_LABEL2 = {
179735
179999
  // 新引擎事件 kind
179736
180000
  "plan.update_spec": "\u6539\u4EFB\u52A1",
179737
180001
  "plan.add_node": "\u52A0\u8282\u70B9",
@@ -180838,7 +181102,7 @@ function collabDomain(opts) {
180838
181102
  return { status: 200, body: { dispatched: !already } };
180839
181103
  });
180840
181104
  router.get("/api/inbox", async (req) => {
180841
- const { kernel, registry: registry2 } = await resolveCtx(req.auth.companyId);
181105
+ const { kernel, registry: registry2, engineStore } = await resolveCtx(req.auth.companyId);
180842
181106
  const me = req.auth.actor;
180843
181107
  const [leadOf, resolveActor, markers] = await Promise.all([
180844
181108
  buildLeadResolver(registry2),
@@ -180851,7 +181115,18 @@ function collabDomain(opts) {
180851
181115
  const inboxSlaMs = opts.sla?.humanMs ?? 24 * 36e5;
180852
181116
  const kernelItems = buildInbox(kernel.model, me, leadOf, resolveActor, markers, inboxSlaMs, (/* @__PURE__ */ new Date()).toISOString());
180853
181117
  const chatItems = opts.chatSession && opts.readMarkers ? await buildChatInbox(opts.chatSession, markers, me, resolveActor) : [];
180854
- return { status: 200, body: { items: mergeInbox(kernelItems, chatItems) } };
181118
+ let waitingItems = [];
181119
+ if (engineStore) {
181120
+ const snaps = (await Promise.all(
181121
+ [...waitingWorkspacesFor(kernel.model, me)].map(
181122
+ (wid) => engineStore.transaction((tx) => tx.loadWorkorder(wid))
181123
+ )
181124
+ )).filter((s2) => s2 !== null);
181125
+ const { reviews, works } = collectWaitingRows(snaps, me);
181126
+ const policy = opts.policy?.() ?? CODE_DEFAULT_POLICY;
181127
+ waitingItems = buildWaitingInbox(reviews, works, policy, (/* @__PURE__ */ new Date()).toISOString(), me);
181128
+ }
181129
+ return { status: 200, body: { items: mergeInbox(kernelItems, [...chatItems, ...waitingItems]) } };
180855
181130
  });
180856
181131
  const companyOf = (req) => req.auth.companyId ?? "company:default";
180857
181132
  const noTagStore = { status: 501, body: { error: { code: "not_implemented", message: "\u672C\u90E8\u7F72\u672A\u542F\u7528\u5DE5\u5355\u6807\u7B7E" } } };
@@ -181091,6 +181366,8 @@ var init_collab = __esm({
181091
181366
  init_workorder_detail();
181092
181367
  init_inbox();
181093
181368
  init_inbox_chat();
181369
+ init_waiting_inbox();
181370
+ init_src2();
181094
181371
  init_audit();
181095
181372
  init_dashboard();
181096
181373
  init_workorder_metrics();
@@ -194431,6 +194708,8 @@ async function startServe(opts) {
194431
194708
  // 工单标签词表(组织级受控、旁账,与 playbook-overrides 同款侧存)。
194432
194709
  tags: workorderTagStore,
194433
194710
  ...opts.sla ? { sla: opts.sla } : {},
194711
+ // ADR 0147:waiting 投影的 human 阶梯从策略中心取;getter 保证随 SIGHUP/reload 热载而变。
194712
+ policy: () => activeTimeoutPolicy,
194434
194713
  resolveEngine: (cid) => getEngine(cid ?? defaultCompanyId)
194435
194714
  }),
194436
194715
  trace.register,
@@ -201490,7 +201769,8 @@ ${res.warning}`);
201490
201769
  r.id === v2.artifact.currentRev ? "\u2190 current head" : "",
201491
201770
  r.id === v2.concludedHead ? "\u2190 \u5DF2\u4EA4\u4ED8" : ""
201492
201771
  ].filter(Boolean).join(" ");
201493
- println(` ${r.id} ${r.state} by ${r.author} "${r.reason}"${marks ? ` ${marks}` : ""}`);
201772
+ const stateLabel = r.state === "superseded" && r.contentKind === "empty" ? "\u672A\u4EA7\u51FA" : r.state;
201773
+ println(` ${r.id} ${stateLabel} by ${r.author} "${r.reason}"${marks ? ` ${marks}` : ""}`);
201494
201774
  }
201495
201775
  const NOTE_ABSTRACT = 150;
201496
201776
  const voteRows = [];
@@ -201779,7 +202059,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
201779
202059
  }
201780
202060
 
201781
202061
  // src/index.ts
201782
- var PKG_VERSION = true ? "0.1.103" : "dev";
202062
+ var PKG_VERSION = true ? "0.1.105" : "dev";
201783
202063
  var OASIS_DIR = path26.join(os10.homedir(), ".oasis");
201784
202064
  var CONFIG_FILE = path26.join(OASIS_DIR, "node-config.json");
201785
202065
  var PID_FILE = path26.join(OASIS_DIR, "node.pid");
@@ -201882,8 +202162,10 @@ function setupAutostart() {
201882
202162
  ].join("\n"));
201883
202163
  try {
201884
202164
  (0, import_node_child_process17.execSync)("systemctl --user daemon-reload && systemctl --user enable --now oasis-node", { stdio: "ignore" });
202165
+ return true;
201885
202166
  } catch {
201886
202167
  }
202168
+ return false;
201887
202169
  } else if (process.platform === "darwin") {
201888
202170
  const plist = path26.join(os10.homedir(), "Library", "LaunchAgents", "com.oasis.node.plist");
201889
202171
  fs33.mkdirSync(path26.dirname(plist), { recursive: true });
@@ -201899,9 +202181,12 @@ function setupAutostart() {
201899
202181
  </dict></plist>`);
201900
202182
  try {
201901
202183
  (0, import_node_child_process17.execSync)(`launchctl load "${plist}"`, { stdio: "ignore" });
202184
+ return true;
201902
202185
  } catch {
201903
202186
  }
202187
+ return false;
201904
202188
  }
202189
+ return false;
201905
202190
  }
201906
202191
  function spawnDaemon() {
201907
202192
  const log3 = fs33.openSync(LOG_FILE, "a");
@@ -201910,9 +202195,7 @@ function spawnDaemon() {
201910
202195
  stdio: ["ignore", log3, log3]
201911
202196
  });
201912
202197
  child.unref();
201913
- const pid = child.pid ?? 0;
201914
- fs33.writeFileSync(PID_FILE, String(pid));
201915
- return pid;
202198
+ return child.pid ?? 0;
201916
202199
  }
201917
202200
  function stopDaemon() {
201918
202201
  const pid = readPid();
@@ -201984,12 +202267,21 @@ void (async () => {
201984
202267
  console.error("[oasis] no config \u2014 run `oasis daemon start` first");
201985
202268
  process.exit(1);
201986
202269
  }
201987
- const alivePid = readPid();
201988
- if (alivePid && alivePid !== process.pid && isRunning2(alivePid)) {
201989
- console.error(`[oasis] daemon already running (PID ${alivePid}) \u2014 exiting duplicate`);
201990
- process.exit(0);
202270
+ try {
202271
+ const fd = fs33.openSync(PID_FILE, "wx");
202272
+ fs33.writeSync(fd, String(process.pid));
202273
+ fs33.closeSync(fd);
202274
+ } catch {
202275
+ const alivePid = readPid();
202276
+ if (alivePid && alivePid !== process.pid && isRunning2(alivePid)) {
202277
+ console.error(`[oasis] daemon already running (PID ${alivePid}) \u2014 exiting duplicate`);
202278
+ process.exit(0);
202279
+ }
202280
+ try {
202281
+ fs33.writeFileSync(PID_FILE, String(process.pid));
202282
+ } catch {
202283
+ }
201991
202284
  }
201992
- fs33.writeFileSync(PID_FILE, String(process.pid));
201993
202285
  const cleanup = () => {
201994
202286
  try {
201995
202287
  fs33.unlinkSync(PID_FILE);
@@ -202083,9 +202375,13 @@ void (async () => {
202083
202375
  saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {}, reportRuntimesOnNextStart: true });
202084
202376
  installBinary();
202085
202377
  stopDaemon();
202086
- setupAutostart();
202087
- const pid = spawnDaemon();
202088
- console.log(`\u2713 daemon started (PID ${pid})${nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : ""}`);
202378
+ const tookOver = setupAutostart();
202379
+ if (!tookOver) {
202380
+ const pid = spawnDaemon();
202381
+ console.log(`\u2713 daemon started (PID ${pid})${nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : ""}`);
202382
+ } else {
202383
+ console.log(`\u2713 daemon started via systemd (oasis-node.service)${nameChanged ? ` [name updated: ${prev.name} \u2192 ${name}]` : ""}`);
202384
+ }
202089
202385
  console.log(` logs: tail -f ${LOG_FILE}`);
202090
202386
  if (!process.env["PATH"]?.includes(path26.dirname(LOCAL_BIN)))
202091
202387
  console.log(` add to PATH: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.103",
3
+ "version": "0.1.105",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "5450f6cd7525aeb5858fa4682141f85dbf80fd16"
29
+ "sourceHead": "60d88fd92311e481e7bbfe922d75db13a3483643"
30
30
  }
31
31
  }