oasis_test 0.1.103 → 0.1.104

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 +143 -33
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -7364,9 +7364,9 @@ function checkTerminated(state) {
7364
7364
  if (!isBusinessDependencyEdge(state, edge)) continue;
7365
7365
  if (isEdgeInputUnobtainable(state, edge)) continue;
7366
7366
  const upstream = state.node(edge.fromNodeId);
7367
- if (!upstream?.latestWorkId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestWorkId)) return false;
7367
+ if (!upstream?.latestAcceptId || !businessHandoffBarrierSatisfied(state, upstream.id, upstream.latestAcceptId)) return false;
7368
7368
  }
7369
- return live.every((n) => n.latestWorkId !== null && n.latestAcceptId === n.latestWorkId);
7369
+ return live.every((n) => n.latestAcceptId !== null && (n.latestAcceptId === n.latestWorkId || n.latestAcceptId === n.latestSuccessWorkId));
7370
7370
  }
7371
7371
  function isEdgeInputUnobtainable(state, edge) {
7372
7372
  const upstream = state.node(edge.fromNodeId);
@@ -7394,10 +7394,17 @@ function acceptBlock(state, workId) {
7394
7394
  }
7395
7395
  const node = state.node(w2.nodeId);
7396
7396
  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)) {
7397
+ if (!hasOutput(w2, state.artifactsOf(w2.id).length)) {
7398
+ if (!w2.endedAt) {
7399
+ return {
7400
+ code: "work-in-flight",
7401
+ 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`,
7402
+ overridable: false
7403
+ };
7404
+ }
7398
7405
  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`,
7406
+ code: "no-output",
7407
+ 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
7408
  overridable: false
7402
7409
  };
7403
7410
  }
@@ -7408,10 +7415,10 @@ function handoffBlock(state, nodeId, workId) {
7408
7415
  if (!requiresBusinessHandoff(state, nodeId, work)) return null;
7409
7416
  if (work?.acceptanceState === "accepted" && work.overrideBy) return null;
7410
7417
  const node = state.node(nodeId);
7411
- if (node?.latestWorkId !== workId) {
7418
+ if (workId !== node?.latestWorkId && workId !== node?.latestSuccessWorkId) {
7412
7419
  return {
7413
7420
  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`,
7421
+ 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
7422
  overridable: false
7416
7423
  };
7417
7424
  }
@@ -7504,6 +7511,7 @@ CREATE TABLE IF NOT EXISTS ${schema}.workflow_nodes (
7504
7511
  assignee_actor_id text,
7505
7512
  assignee_role text,
7506
7513
  latest_work_id text,
7514
+ latest_success_work_id text,
7507
7515
  latest_accept_id text,
7508
7516
  is_final_output boolean NOT NULL DEFAULT false,
7509
7517
  fields jsonb,
@@ -7765,6 +7773,9 @@ CREATE INDEX IF NOT EXISTS issues_workorder_alive_idx ON ${schema}.issues (worko
7765
7773
  CREATE INDEX IF NOT EXISTS issues_about_alive_idx ON ${schema}.issues (about_node_id) WHERE resolved_at IS NULL;
7766
7774
  CREATE INDEX IF NOT EXISTS issues_raised_alive_idx ON ${schema}.issues (raised_by_node_id) WHERE resolved_at IS NULL;
7767
7775
  ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS fields jsonb;
7776
+ -- \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
7777
+ -- \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
7778
+ ALTER TABLE ${schema}.workflow_nodes ADD COLUMN IF NOT EXISTS latest_success_work_id text;
7768
7779
  -- \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
7780
  -- DROP COLUMN \u4F1A\u62A5 "cannot drop column ... because other objects depend on it"\uFF0C\u4E14\u672C\u6BB5\u662F\u5355\u6761
7770
7781
  -- \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 +7831,16 @@ UPDATE ${schema}.reviews SET status = CASE
7820
7831
  WHEN ended_at IS NOT NULL THEN 'failed'
7821
7832
  ELSE 'running' END
7822
7833
  WHERE status IS NULL;
7834
+ -- \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
7835
+ -- \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
7836
+ -- \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
7837
+ UPDATE ${schema}.workflow_nodes n SET latest_success_work_id = (
7838
+ SELECT w.id FROM ${schema}.works w
7839
+ WHERE w.node_id = n.id AND w.status = 'success' AND w.reply_to_issue_id IS NULL
7840
+ ORDER BY w.created_at DESC, w.id DESC
7841
+ LIMIT 1
7842
+ )
7843
+ WHERE n.latest_success_work_id IS NULL;
7823
7844
  `;
7824
7845
  }
7825
7846
  function ENGINE_VIEWS(schema) {
@@ -7835,20 +7856,25 @@ SELECT r.*, r.status AS state
7835
7856
  FROM ${schema}.reviews r;
7836
7857
 
7837
7858
  DROP VIEW IF EXISTS ${schema}.node_view;
7859
+ -- \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
7860
+ -- \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
7861
+ -- \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
7862
+ -- \u5DF2\u5B8C\u6210/\u8BC4\u5BA1\u4E2D\uFF1B\u518D\u5426\u5219\u672B\u6B21\u5C1D\u8BD5\u5931\u8D25/dead \u2192 \u5361\u4F4F\u3002
7838
7863
  CREATE VIEW ${schema}.node_view AS
7839
7864
  SELECT n.*,
7840
7865
  CASE
7841
7866
  WHEN n.cancelled_at IS NOT NULL THEN 'dead' -- \u5DF2\u4F5C\u5E9F\uFF08\u79FB\u51FA\u8BA1\u5212\uFF09
7842
7867
  WHEN lw.id IS NULL THEN 'waiting' -- \u672A\u8FD0\u884C\uFF08latest_work \u4E3A\u7A7A\uFF09
7843
7868
  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
7869
+ WHEN lw.status = 'running' THEN 'running' -- \u5728\u8FDB\u884C\uFF08\u91CD\u8DD1\u8FDB\u884C\u4E2D\u4E5F\u4F18\u5148\u663E\u793A\u5728\u8FDB\u884C\uFF09
7870
+ 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
7871
+ 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
7872
+ WHEN lw.status IN ('failed','dead') THEN 'dead' -- \u4ECE\u672A\u4EA7\u51FA\u4E14\u672B\u6B21\u5C1D\u8BD5\u5931\u8D25 \u2192 \u5361\u4F4F\u4E86
7848
7873
  ELSE 'waiting'
7849
7874
  END AS state
7850
7875
  FROM ${schema}.workflow_nodes n
7851
- LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id;
7876
+ LEFT JOIN ${schema}.works lw ON lw.id = n.latest_work_id
7877
+ LEFT JOIN ${schema}.works ls ON ls.id = n.latest_success_work_id;
7852
7878
 
7853
7879
  DROP VIEW IF EXISTS ${schema}.workorder_view;
7854
7880
  CREATE VIEW ${schema}.workorder_view AS
@@ -13128,6 +13154,7 @@ var init_store_postgres = __esm({
13128
13154
  assigneeActorId: n.assignee_actor_id,
13129
13155
  assigneeRole: n.assignee_role,
13130
13156
  latestWorkId: n.latest_work_id,
13157
+ latestSuccessWorkId: n.latest_success_work_id ?? null,
13131
13158
  latestAcceptId: n.latest_accept_id,
13132
13159
  isFinalOutput: n.is_final_output,
13133
13160
  cancelledAt: iso(n.cancelled_at),
@@ -13304,9 +13331,9 @@ var init_store_postgres = __esm({
13304
13331
  await this.c.query(
13305
13332
  `INSERT INTO ${this.s}.workflow_nodes
13306
13333
  (id,workorder_id,type,title,spec,version,assignee_actor_id,assignee_role,
13307
- latest_work_id,latest_accept_id,is_final_output,
13334
+ latest_work_id,latest_success_work_id,latest_accept_id,is_final_output,
13308
13335
  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)
13336
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
13310
13337
  ON CONFLICT (id) DO NOTHING`,
13311
13338
  [
13312
13339
  n.id,
@@ -13318,6 +13345,7 @@ var init_store_postgres = __esm({
13318
13345
  n.assigneeActorId,
13319
13346
  n.assigneeRole,
13320
13347
  n.latestWorkId,
13348
+ n.latestSuccessWorkId,
13321
13349
  n.latestAcceptId,
13322
13350
  n.isFinalOutput,
13323
13351
  n.fields ? JSON.stringify(n.fields) : null,
@@ -13586,11 +13614,14 @@ var init_store_postgres = __esm({
13586
13614
  * `NOT EXISTS` 那一段是"我前面还有没跑完的兄弟就别动我";`SKIP LOCKED` 是
13587
13615
  * "别的 worker 正在处理这张工单就跳过整张"。两者合起来 = 按工单分区的串行队列。
13588
13616
  */
13589
- async claimNextEvent(now) {
13617
+ async claimNextEvent(now, recoverAppliedBefore) {
13590
13618
  const res = await this.c.query(
13591
13619
  `SELECT e.* FROM ${this.s}.events e
13592
- WHERE e.status = 'pending'
13593
- AND (e.available_at IS NULL OR e.available_at <= $1)
13620
+ WHERE (
13621
+ (e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1))
13622
+ -- \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
13623
+ OR ($2::timestamptz IS NOT NULL AND e.status = 'applied' AND e.applied_at < $2::timestamptz)
13624
+ )
13594
13625
  AND NOT EXISTS (
13595
13626
  SELECT 1 FROM ${this.s}.events p
13596
13627
  WHERE p.workorder_id = e.workorder_id
@@ -13599,7 +13630,7 @@ var init_store_postgres = __esm({
13599
13630
  ORDER BY e.seq
13600
13631
  FOR UPDATE OF e SKIP LOCKED
13601
13632
  LIMIT 1`,
13602
- [now]
13633
+ [now, recoverAppliedBefore ?? null]
13603
13634
  );
13604
13635
  return res.rows[0] ? toRecord(res.rows[0]) : null;
13605
13636
  }
@@ -13640,10 +13671,14 @@ var init_store_postgres = __esm({
13640
13671
  );
13641
13672
  return (res.rowCount ?? 0) > 0;
13642
13673
  }
13643
- async claimAllPending(now) {
13674
+ async claimAllPending(now, recoverAppliedBefore) {
13644
13675
  const res = await this.c.query(
13645
13676
  `SELECT e.* FROM ${this.s}.events e
13646
- WHERE e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1)
13677
+ WHERE (
13678
+ (e.status = 'pending' AND (e.available_at IS NULL OR e.available_at <= $1))
13679
+ -- \u5D29\u6E83\u81EA\u6108\uFF1A\u89C1 claimNextEvent \u7684\u6CE8\u91CA
13680
+ OR ($2::timestamptz IS NOT NULL AND e.status = 'applied' AND e.applied_at < $2::timestamptz)
13681
+ )
13647
13682
  AND NOT EXISTS (SELECT 1 FROM ${this.s}.events p WHERE p.workorder_id = e.workorder_id
13648
13683
  AND p.status IN ('pending','applied') AND p.seq < e.seq)
13649
13684
  -- \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 +13688,7 @@ var init_store_postgres = __esm({
13653
13688
  CASE WHEN e.origin = 'apply' AND e.caused_by_seq IS NULL THEN 0 ELSE 1 END,
13654
13689
  e.seq
13655
13690
  FOR UPDATE OF e SKIP LOCKED`,
13656
- [now]
13691
+ [now, recoverAppliedBefore ?? null]
13657
13692
  );
13658
13693
  return res.rows.map(toRecord);
13659
13694
  }
@@ -13810,6 +13845,7 @@ function newNode(id, workorderId, at, version2) {
13810
13845
  assigneeActorId: null,
13811
13846
  assigneeRole: null,
13812
13847
  latestWorkId: null,
13848
+ latestSuccessWorkId: null,
13813
13849
  latestAcceptId: null,
13814
13850
  isFinalOutput: false,
13815
13851
  cancelledAt: null,
@@ -14138,6 +14174,10 @@ var init_workorder = __esm({
14138
14174
  });
14139
14175
 
14140
14176
  // ../engine/src/handlers/work.ts
14177
+ function latestSuccessOf(state, nodeId) {
14178
+ const successes = state.worksOf(nodeId).filter((w2) => !w2.replyToIssueId && w2.status === "success").sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
14179
+ return successes.length > 0 ? successes[successes.length - 1].id : null;
14180
+ }
14141
14181
  var workCreate, workKill, workResponse, workHandoffRecorded, workStart, workSubmitOutput, workTimeout;
14142
14182
  var init_work = __esm({
14143
14183
  "../engine/src/handlers/work.ts"() {
@@ -14267,6 +14307,12 @@ var init_work = __esm({
14267
14307
  ...w2.status !== "dead" ? { status: "dead" } : {}
14268
14308
  });
14269
14309
  }
14310
+ if (node.latestSuccessWorkId) {
14311
+ const ls = state.work(node.latestSuccessWorkId);
14312
+ if (!ls || ls.status !== "success") {
14313
+ state.updateNode(e.nodeId, { latestSuccessWorkId: latestSuccessOf(state, e.nodeId) });
14314
+ }
14315
+ }
14270
14316
  killNodeReviews(state, e.nodeId, ctx.at);
14271
14317
  },
14272
14318
  async effect(e, ctx) {
@@ -14300,6 +14346,9 @@ var init_work = __esm({
14300
14346
  outcomeDetail: businessFailed ? { ...e.detail ?? {}, reason: "agent business handoff not delivered" } : e.detail,
14301
14347
  status
14302
14348
  });
14349
+ if (status === "success") {
14350
+ state.updateNode(w2.nodeId, { latestSuccessWorkId: e.workId });
14351
+ }
14303
14352
  }
14304
14353
  };
14305
14354
  workHandoffRecorded = {
@@ -14309,17 +14358,20 @@ var init_work = __esm({
14309
14358
  const w2 = state.work(e.workId);
14310
14359
  if (!w2 || e.producedBy !== "agent") return;
14311
14360
  if (w2.agentHandoffAttemptId && w2.agentHandoffAttemptId !== e.attemptId) return;
14361
+ const flipsToFailed = e.status === "not_delivered" && w2.endedAt && !w2.deadAt;
14312
14362
  state.updateWork(e.workId, {
14313
14363
  agentHandoffAttemptId: e.attemptId,
14314
14364
  businessHandoffStatus: e.status,
14315
14365
  businessHandoffRecordedAt: ctx.at,
14316
- // handoff 可以在 work.response 后到达。明确未交付必须回到现有失败/重试路径。
14317
- ...e.status === "not_delivered" && w2.endedAt && !w2.deadAt ? {
14366
+ ...flipsToFailed ? {
14318
14367
  status: "failed",
14319
14368
  outcome: "failed",
14320
14369
  outcomeDetail: { ...w2.outcomeDetail ?? {}, reason: "agent business handoff not delivered" }
14321
14370
  } : {}
14322
14371
  });
14372
+ if (flipsToFailed && state.node(w2.nodeId)?.latestSuccessWorkId === e.workId) {
14373
+ state.updateNode(w2.nodeId, { latestSuccessWorkId: latestSuccessOf(state, w2.nodeId) });
14374
+ }
14323
14375
  }
14324
14376
  };
14325
14377
  workStart = {
@@ -14865,7 +14917,7 @@ function emptySnapshotPlaceholder(record8) {
14865
14917
  reviews: []
14866
14918
  };
14867
14919
  }
14868
- var SYSTEM_ACTOR2, NOOP_IO, SYSTEM_CLOCK, HandlerRegistry, EngineBus, ApplyFailure, sleep;
14920
+ var SYSTEM_ACTOR2, NOOP_IO, SYSTEM_CLOCK, HandlerRegistry, DEFAULT_STUCK_APPLIED_RECOVERY_MS, EngineBus, ApplyFailure, sleep;
14869
14921
  var init_bus = __esm({
14870
14922
  "../engine/src/bus.ts"() {
14871
14923
  "use strict";
@@ -14903,6 +14955,7 @@ var init_bus = __esm({
14903
14955
  return [...this.byKind.values()].flat();
14904
14956
  }
14905
14957
  };
14958
+ DEFAULT_STUCK_APPLIED_RECOVERY_MS = 5 * 6e4;
14906
14959
  EngineBus = class {
14907
14960
  store;
14908
14961
  io;
@@ -14911,6 +14964,7 @@ var init_bus = __esm({
14911
14964
  policy;
14912
14965
  registry;
14913
14966
  maxEffectAttempts;
14967
+ stuckAppliedRecoveryMs;
14914
14968
  running = false;
14915
14969
  /** 事件监听器(内存 pubsub):每批事件处理完调用,供 /api/events SSE 推送(替代旧 oplog.subscribe)。 */
14916
14970
  eventListener = null;
@@ -14922,10 +14976,19 @@ var init_bus = __esm({
14922
14976
  this.policy = opts.policy ?? CODE_DEFAULT_POLICY;
14923
14977
  this.registry = new HandlerRegistry(opts.handlers);
14924
14978
  this.maxEffectAttempts = opts.maxEffectAttempts ?? 3;
14979
+ this.stuckAppliedRecoveryMs = opts.stuckAppliedRecoveryMs ?? DEFAULT_STUCK_APPLIED_RECOVERY_MS;
14925
14980
  }
14926
14981
  setIO(io) {
14927
14982
  this.io = io;
14928
14983
  }
14984
+ /**
14985
+ * 崩溃自愈水位线:早于此刻仍停在 `applied` 的事件,认为是「进程死在落状态与收尾之间」的残留,
14986
+ * 回收重跑。`stuckAppliedRecoveryMs <= 0` 时返回 undefined(= 关闭自愈,维持旧行为)。
14987
+ */
14988
+ recoverAppliedBefore() {
14989
+ if (this.stuckAppliedRecoveryMs <= 0) return void 0;
14990
+ return new Date(Date.parse(this.clock.now()) - this.stuckAppliedRecoveryMs).toISOString();
14991
+ }
14929
14992
  /** 注册事件监听器——新事件处理完即回调(serve 桥接 SSE 实时推送,替代 oplog.subscribe)。返回退订。 */
14930
14993
  onEvent(fn) {
14931
14994
  this.eventListener = fn;
@@ -15014,7 +15077,7 @@ var init_bus = __esm({
15014
15077
  let lastFullScan = 0;
15015
15078
  while (true) {
15016
15079
  try {
15017
- const batch = await this.store.transaction((tx) => tx.claimAllPending(this.clock.now()));
15080
+ const batch = await this.store.transaction((tx) => tx.claimAllPending(this.clock.now(), this.recoverAppliedBefore()));
15018
15081
  const touched = /* @__PURE__ */ new Set();
15019
15082
  if (batch.length > 0) {
15020
15083
  for (const record8 of batch) {
@@ -15247,6 +15310,10 @@ var init_bus = __esm({
15247
15310
  };
15248
15311
  let ok = true;
15249
15312
  for (const h of handlers) {
15313
+ const effectKey = `${h.name}#effect`;
15314
+ if (record8.appliedAt && await this.store.transaction((tx) => tx.isHandlerDone(record8.seq, effectKey))) {
15315
+ continue;
15316
+ }
15250
15317
  let lastErr = "";
15251
15318
  for (let attempt = 1; attempt <= this.maxEffectAttempts; attempt++) {
15252
15319
  try {
@@ -15259,6 +15326,8 @@ var init_bus = __esm({
15259
15326
  }
15260
15327
  if (lastErr) {
15261
15328
  ok = false;
15329
+ } else {
15330
+ await this.store.transaction((tx) => tx.markHandlerDone(record8.seq, effectKey));
15262
15331
  }
15263
15332
  }
15264
15333
  await this.store.transaction(async (tx) => {
@@ -15317,7 +15386,7 @@ var init_bus = __esm({
15317
15386
  /* ═══════════════════════ 内部:旧 applyPhase(claim + apply 一体) ═══════════════════════ */
15318
15387
  async applyPhase() {
15319
15388
  return this.store.transaction(async (tx) => {
15320
- const record8 = await tx.claimNextEvent(this.clock.now());
15389
+ const record8 = await tx.claimNextEvent(this.clock.now(), this.recoverAppliedBefore());
15321
15390
  if (!record8) return null;
15322
15391
  const snapshot = await tx.loadWorkorder(record8.workorderId);
15323
15392
  if (snapshot?.workorder.doneAt) {
@@ -15442,6 +15511,10 @@ function mapWorkStateToRevisionState(w2) {
15442
15511
  if (w2.acceptanceState === "rejected") return "rejected";
15443
15512
  return "proposed";
15444
15513
  // success but not yet judged
15514
+ // ★ ADR 0148:failed 尝试没有对应的 RevisionState(枚举只有 working/proposed/merged/rejected/
15515
+ // superseded,改枚举会牵动契约与前端,见红线 4)——仍投影成 superseded 保持生命周期语义,但**内容
15516
+ // 如实为 empty**(workToRevision 对无产出 work 给 contentKind="empty")。读面(CLI 版本列表)据此把
15517
+ // 「superseded + empty」如实标成「未产出」,不再让「这次失败了」被读成「被更好的版本取代」。
15445
15518
  case "failed":
15446
15519
  return "superseded";
15447
15520
  case "retry":
@@ -15711,8 +15784,16 @@ function bridgeLoadReadModel(input) {
15711
15784
  const lw = node.latestWorkId ? workById.get(node.latestWorkId) : void 0;
15712
15785
  if (lw) {
15713
15786
  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;
15787
+ let uiSt;
15788
+ if (st === "retry") uiSt = null;
15789
+ else if (st === "running") uiSt = "running";
15790
+ else if (node.latestSuccessWorkId) uiSt = "success";
15791
+ else uiSt = st;
15792
+ art.latestWorkState = uiSt;
15793
+ art.latestAccepted = node.latestSuccessWorkId !== null && node.latestAcceptId === node.latestSuccessWorkId;
15794
+ if ((st === "failed" || st === "dead") && node.latestSuccessWorkId) {
15795
+ art.currentRev = node.latestSuccessWorkId;
15796
+ }
15716
15797
  } else {
15717
15798
  art.latestWorkState = null;
15718
15799
  art.latestAccepted = false;
@@ -15902,6 +15983,26 @@ var init_bridge_readmodel = __esm({
15902
15983
  }
15903
15984
  });
15904
15985
 
15986
+ // ../core/src/propose-guard.ts
15987
+ function proposeBlock(head, headArtifactCount) {
15988
+ if (!head) return null;
15989
+ const state = workState(head, hasOutput(head, headArtifactCount));
15990
+ if (state === "dead") {
15991
+ const why = head.rejectedReason ? `\uFF1A${head.rejectedReason}` : "";
15992
+ return {
15993
+ code: "head-dead",
15994
+ 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`
15995
+ };
15996
+ }
15997
+ return null;
15998
+ }
15999
+ var init_propose_guard = __esm({
16000
+ "../core/src/propose-guard.ts"() {
16001
+ "use strict";
16002
+ init_src3();
16003
+ }
16004
+ });
16005
+
15905
16006
  // ../core/src/preview-intervention.ts
15906
16007
  function resolveOwner(type, typeDef, roleIndex, schemaSize) {
15907
16008
  if (!typeDef) {
@@ -16367,6 +16468,7 @@ var init_kernel_bridge = __esm({
16367
16468
  init_closure_gate();
16368
16469
  init_kernel();
16369
16470
  init_bridge_readmodel();
16471
+ init_propose_guard();
16370
16472
  init_src3();
16371
16473
  init_src3();
16372
16474
  init_preview_intervention();
@@ -16697,8 +16799,14 @@ var init_kernel_bridge = __esm({
16697
16799
  }
16698
16800
  }
16699
16801
  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;
16802
+ const snap = await this.store.transaction((tx) => tx.loadWorkorder(wid));
16803
+ const headId = snap?.nodes.find((n) => n.id === args.artifactId)?.latestWorkId ?? null;
16804
+ if (headId) {
16805
+ const headWork = snap?.works.find((w2) => w2.id === headId);
16806
+ const headOutputs = snap?.artifacts.filter((a) => a.workId === headId).length ?? 0;
16807
+ const block = proposeBlock(headWork, headOutputs);
16808
+ if (block) throw new KernelError(block.detail, block.code);
16809
+ }
16702
16810
  const worksCount = [...this.model.revisions.values()].filter((r) => r.artifactId === args.artifactId).length;
16703
16811
  const revisionId = `wk:${args.artifactId}:${worksCount + 1}`;
16704
16812
  await this.commit({
@@ -16974,7 +17082,8 @@ var init_kernel_bridge = __esm({
16974
17082
  const artifact = this.model.artifacts.get(args.artifactId);
16975
17083
  const currentRev = artifact?.currentRev ? this.model.revisions.get(artifact.currentRev) : void 0;
16976
17084
  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);
17085
+ const targetNode = snap?.nodes.find((n) => n.id === args.artifactId);
17086
+ const head = (currentRev && currentRev.state === "proposed" ? currentRev.id : null) ?? concludedHead(this.model, args.artifactId) ?? (targetNode?.latestSuccessWorkId ?? targetNode?.latestWorkId ?? null);
16978
17087
  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
17088
  const block = snap ? acceptBlock(new WorkorderState(snap), head) : null;
16980
17089
  if (block?.code === "already-accepted") return;
@@ -201490,7 +201599,8 @@ ${res.warning}`);
201490
201599
  r.id === v2.artifact.currentRev ? "\u2190 current head" : "",
201491
201600
  r.id === v2.concludedHead ? "\u2190 \u5DF2\u4EA4\u4ED8" : ""
201492
201601
  ].filter(Boolean).join(" ");
201493
- println(` ${r.id} ${r.state} by ${r.author} "${r.reason}"${marks ? ` ${marks}` : ""}`);
201602
+ const stateLabel = r.state === "superseded" && r.contentKind === "empty" ? "\u672A\u4EA7\u51FA" : r.state;
201603
+ println(` ${r.id} ${stateLabel} by ${r.author} "${r.reason}"${marks ? ` ${marks}` : ""}`);
201494
201604
  }
201495
201605
  const NOTE_ABSTRACT = 150;
201496
201606
  const voteRows = [];
@@ -201779,7 +201889,7 @@ function syncRuntimeAssets(candidateRoots, binDir) {
201779
201889
  }
201780
201890
 
201781
201891
  // src/index.ts
201782
- var PKG_VERSION = true ? "0.1.103" : "dev";
201892
+ var PKG_VERSION = true ? "0.1.104" : "dev";
201783
201893
  var OASIS_DIR = path26.join(os10.homedir(), ".oasis");
201784
201894
  var CONFIG_FILE = path26.join(OASIS_DIR, "node-config.json");
201785
201895
  var PID_FILE = path26.join(OASIS_DIR, "node.pid");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.103",
3
+ "version": "0.1.104",
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": "2b9f5db18e1ad2cf93a2e1c7c49aa47765d228c8"
30
30
  }
31
31
  }