oasis_test_v2 2.2.16 → 2.2.17

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.
@@ -17,6 +17,18 @@
17
17
 
18
18
  set -euo pipefail
19
19
 
20
+ # 这个候选是不是一个 oasis wrapper?
21
+ #
22
+ # 判据与 TS 侧的 `isOasisWrapperPath` **同一条**:解析到底之后路径以 `/wrapper.sh` 结尾。
23
+ # 所有连接器的 wrapper 都是「以 commandName 之名软链到 <connector>/wrapper.sh」,
24
+ # 所以这一条既认得出自己、也认得出**别的会话/别的部署根** stage 出来的那些。
25
+ oasis_is_wrapper() {
26
+ case "$(realpath "$1" 2>/dev/null)" in
27
+ */wrapper.sh) return 0 ;;
28
+ *) return 1 ;;
29
+ esac
30
+ }
31
+
20
32
  # Resolve the real binary path (avoid recursion into self).
21
33
  # If ${OASIS_BIN_VAR} is already set (injected by inject()), use it — unless it points
22
34
  # back at this very wrapper. Otherwise scan PATH for the first executable that isn't us.
@@ -24,9 +36,6 @@ oasis_resolve_bin() {
24
36
  local bin_var="${OASIS_BIN_VAR:?OASIS_BIN_VAR must be set}"
25
37
  local cmd_name="${OASIS_CMD_NAME:?OASIS_CMD_NAME must be set}"
26
38
 
27
- local SELF
28
- SELF="$(realpath "$0" 2>/dev/null || echo "$0")"
29
-
30
39
  # If already set by inject() AND executable on THIS machine, skip the PATH scan —
31
40
  # but **only if it isn't this wrapper itself**.
32
41
  #
@@ -36,18 +45,28 @@ oasis_resolve_bin() {
36
45
  # 触发更新就会把 /tmp/oasis-conn-* 带进去)。于是 GH_BIN 指向 wrapper 自己 →
37
46
  # 这里的快路径直接 return → 后面 exec "$GH_BIN" 就是无限递归调用自身。
38
47
  # 症状还特别误导:递归被 oasis_check_available 拦下,报的是「cli is upgrading」。
39
- if [ -n "${!bin_var:-}" ] && [ -x "${!bin_var:-}" ] \
40
- && [ "$(realpath "${!bin_var}" 2>/dev/null)" != "$SELF" ]; then
48
+ # 判据是「是不是 wrapper」,不是「是不是**我**」:daemon PATH 被污染时,
49
+ # inject() 解出来的可能是**另一个**会话的 wrapper,信了照样递归。
50
+ if [ -n "${!bin_var:-}" ] && [ -x "${!bin_var:-}" ] && ! oasis_is_wrapper "${!bin_var}"; then
41
51
  return 0
42
52
  fi
43
53
  # 空 / 不可执行 / 指向自己:清掉,让下面的 PATH 扫描重新填。
44
54
  unset "$bin_var" 2>/dev/null || true
45
55
 
46
- # Fallback: scan PATH for real binary (skip self to avoid recursion)
56
+ # Fallback: scan PATH —— **跳过所有 oasis wrapper,不只是自己**。
57
+ #
58
+ # ⚠ 把 `oasis_is_wrapper` 换回 `!= "$SELF"` 会炸机(2026-09-11 两次实测,进程数冲到
59
+ # 1.1 万、load 391)。同一台机器上常常同时挂着**多个** staged wrapper 目录
60
+ # (不同会话、不同部署根),它们各自是**不同的 wrapper.sh 文件**,realpath 互不相等;
61
+ # 只比 `$0` 的话,A 把 B 当「真 CLI」exec 过去,B 再把 A 当真 CLI,
62
+ # 叠上 `oasis_check_available` 的 `--version` 自检,每跳再 fork 一次 → 指数级。
63
+ #
64
+ # 平时看不出来,是因为 inject() 会把 LARK_BIN/GH_BIN 直接设好、走上面那条快路径,
65
+ # 根本不扫 PATH。那个绝对路径**一旦失效**(文件被挪走/升级换名)就立刻掉进这里。
47
66
  local candidate
48
67
  while IFS= read -r dir; do
49
68
  candidate="$dir/$cmd_name"
50
- if [ -x "$candidate" ] && [ "$(realpath "$candidate" 2>/dev/null)" != "$SELF" ]; then
69
+ if [ -x "$candidate" ] && ! oasis_is_wrapper "$candidate"; then
51
70
  printf -v "$bin_var" '%s' "$candidate"
52
71
  export "$bin_var"
53
72
  return 0
@@ -67,7 +86,7 @@ oasis_resolve_bin() {
67
86
  # 还能被找回来,不至于「按 <cmd> 扫 PATH 一个都找不到」。**别删它**,也别据此重新引入自动挪动。
68
87
  while IFS= read -r dir; do
69
88
  candidate="$dir/$cmd_name.oasis-displaced"
70
- if [ -x "$candidate" ] && [ "$(realpath "$candidate" 2>/dev/null)" != "$SELF" ]; then
89
+ if [ -x "$candidate" ] && ! oasis_is_wrapper "$candidate"; then
71
90
  printf -v "$bin_var" '%s' "$candidate"
72
91
  export "$bin_var"
73
92
  return 0
package/dist/index.js CHANGED
@@ -25452,7 +25452,9 @@ var init_codex = __esm({
25452
25452
  const reasoningEffort = this.opts.reasoningEffort;
25453
25453
  return runProtocolSession(job, {
25454
25454
  bin: this.opts.codexBin ?? "codex",
25455
- args: ["app-server", "--stdio"],
25455
+ // exec 那条路:关掉登录 shell,否则 `~/.profile` 会把 `$HOME/.local/bin` 前置到
25456
+ // PATH 最前,压过注入的连接器 wrapper(理由与实证见 exec 分支里那段长注释)。
25457
+ args: ["app-server", "--stdio", "-c", "allow_login_shell=false"],
25456
25458
  extraArgs: this.opts.extraArgs,
25457
25459
  blockedFlags: CODEX_APP_SERVER_BLOCKED_FLAGS,
25458
25460
  workRoot: this.opts.workRoot,
@@ -25633,6 +25635,24 @@ ${task}` : task;
25633
25635
  // 打开 reasoning 摘要通道:默认 auto,让 codex 产出 reasoning item(→ thought → 前端思考块)。
25634
25636
  ...this.opts.reasoningSummary !== "none" ? ["-c", `model_reasoning_summary=${this.opts.reasoningSummary ?? "auto"}`] : [],
25635
25637
  ...this.opts.reasoningEffort ? ["-c", `model_reasoning_effort=${this.opts.reasoningEffort}`] : [],
25638
+ /* ★ 关掉登录 shell(2026-09-11)。
25639
+ codex 的 shell 工具默认把每条命令丢进 `/bin/bash -lc`——本机 codex 自己的
25640
+ thread_history 里逐字记着 `"command":"/bin/bash -lc \"pwd && rg …"`。
25641
+ 登录 shell 会重跑 `~/.profile`,而 Ubuntu 的 profile 里是
25642
+ `PATH="$HOME/.local/bin:$PATH"`(**前置**);于是任何装在 `~/.local/bin` 的同名 CLI
25643
+ 都会抢到注入的连接器 wrapper 前面。实测后果:飞书连接器凭据、连接记录全对,
25644
+ dev-agent(codex)跑 `lark-cli whoami` 却回 `not_configured`——wrapper 被绕过、
25645
+ 而隔离键 `LARKSUITE_CLI_CONFIG_DIR` 照旧生效指着空目录,两头落空。
25646
+ 同机同用户的 claude-code agent 一切正常,差别只有这一个 `-l`。
25647
+
25648
+ `allow_login_shell` 是 codex 的正式配置键(本机实测:给它传整数会报
25649
+ `invalid type: integer, expected a boolean in \`allow_login_shell\``,
25650
+ 而不存在的键是被静默忽略的),exec 与 app-server 两条路都吃 `-c` 覆盖。
25651
+
25652
+ ⚠ 未验证的一格:这个键**是否真的**就是那个 `-l` 的开关,只能等下一次
25653
+ codex 派单后在节点上 `ps` 看是 `bash -lc` 还是 `bash -c`。设错了是 no-op,不会更坏。 */
25654
+ "-c",
25655
+ "allow_login_shell=false",
25636
25656
  // ADR「agent CLI 启动参数员工级可配置化」:员工配置 job.extraArgs(在前)+ opts.extraArgs(静态默认,在后),
25637
25657
  // 过 CODEX_BLOCKED_FLAGS 拦截。追加在**位置 prompt 之前**(codex exec 的 [PROMPT] 必须是末位参数)。
25638
25658
  ...prepareExtraArgs(
@@ -28663,7 +28683,7 @@ function projectsIntoContent(kind, role, origin, telemetryTextInContent) {
28663
28683
  return true;
28664
28684
  }
28665
28685
  }
28666
- var import_node_crypto15, CHAT_ITEMS_DOUBLE_WRITE_ENV, ChatItemLedger;
28686
+ var import_node_crypto15, CHAT_ITEMS_DOUBLE_WRITE_ENV, SEGMENT_END_NOTE, ChatItemLedger;
28667
28687
  var init_chat_item_ledger = __esm({
28668
28688
  "../server/src/chat-item-ledger.ts"() {
28669
28689
  "use strict";
@@ -28671,6 +28691,7 @@ var init_chat_item_ledger = __esm({
28671
28691
  init_chat_item();
28672
28692
  import_node_crypto15 = require("node:crypto");
28673
28693
  CHAT_ITEMS_DOUBLE_WRITE_ENV = "OASIS_CHAT_ITEMS_DOUBLE_WRITE";
28694
+ SEGMENT_END_NOTE = "\u672C\u6BB5\u65E0\u8F93\u51FA\uFF08\u4EBA\u5728\u9996\u4E2A\u6B63\u6587\u4E4B\u524D\u63D2\u4E86\u8BDD\uFF09";
28674
28695
  ChatItemLedger = class {
28675
28696
  constructor(deps) {
28676
28697
  this.deps = deps;
@@ -28699,6 +28720,8 @@ var init_chat_item_ledger = __esm({
28699
28720
  log;
28700
28721
  persistEnabled;
28701
28722
  telemetryTextInContent;
28723
+ /** 本段的「无输出」注解已经提前落过(`closeSegmentBeforeUserInput`),`beginSegment` 别再落一条。 */
28724
+ segmentEndEmitted = false;
28702
28725
  /** **不变量 1**:`providerItemKey → itemId`,本账本唯一持有。切段/收尾清空。 */
28703
28726
  itemKeyMap = /* @__PURE__ */ new Map();
28704
28727
  /** 内存账本:itemId → 行。插入顺序即 seq 顺序。 */
@@ -28916,19 +28939,11 @@ var init_chat_item_ledger = __esm({
28916
28939
  * 映射一并清空——新段是新行,复用旧 itemId 会把新文本写进上一条消息。
28917
28940
  */
28918
28941
  beginSegment() {
28919
- this.closeStreamingRows("completed");
28920
- if (this.segmentItemCount === 0 && this.messageId) {
28921
- this.insert({
28922
- kind: "control",
28923
- role: "assistant",
28924
- status: "completed",
28925
- origin: "server",
28926
- text: "",
28927
- attrs: {},
28928
- inContent: false,
28929
- persist: true,
28930
- payload: { code: "segment_end", note: "\u672C\u6BB5\u65E0\u8F93\u51FA\uFF08\u4EBA\u5728\u9996\u4E2A\u6B63\u6587\u4E4B\u524D\u63D2\u4E86\u8BDD\uFF09" }
28931
- });
28942
+ if (this.segmentEndEmitted) {
28943
+ this.segmentEndEmitted = false;
28944
+ } else {
28945
+ this.closeStreamingRows("completed");
28946
+ if (this.segmentItemCount === 0 && this.messageId) this.insertSegmentEnd();
28932
28947
  }
28933
28948
  this.itemKeyMap.clear();
28934
28949
  this.lastTurnId = null;
@@ -28936,6 +28951,41 @@ var init_chat_item_ledger = __esm({
28936
28951
  this.segment += 1;
28937
28952
  this.segmentItemCount = 0;
28938
28953
  }
28954
+ /**
28955
+ * 人插话时,先把「这一段到此为止」落定;**必须在那条 user 行之前调用**。
28956
+ *
28957
+ * 展示序(`ord`)由落库那一刻的表级序列派,所以**谁先落库谁在上面**。此前这条注解跟在
28958
+ * `beginSegment()` 里、而 `beginSegment` 由 `onUserInput` 回调在 user 行之后触发,于是
28959
+ * 「本段无输出」排到了人说的那句话**下面**——发起人 2026-09-11 的现场(会话 `eca44da4`:
28960
+ * user 行 ord 8503、注解 ord 8504)。他要的顺序是:开启这一轮的那条消息 → agent 头像块
28961
+ * (里面就是这条注解)→ 人插的那句话 → agent 接着说。
28962
+ *
28963
+ * 返回落下的那一行(`null` = 本段有输出、或还没有 assistant 行可归属,不需要注解),
28964
+ * 调用方据它把这条推上 v3 流——不推的话它只在下一次快照才出现。
28965
+ */
28966
+ closeSegmentBeforeUserInput() {
28967
+ this.closeStreamingRows("completed");
28968
+ if (this.segmentEndEmitted) return null;
28969
+ if (this.segmentItemCount !== 0 || !this.messageId) return null;
28970
+ const id = this.insertSegmentEnd();
28971
+ this.segmentEndEmitted = true;
28972
+ const row = this.rows.get(id);
28973
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
28974
+ }
28975
+ /** 零输出段兜底(S1 评审 B1):那条 assistant 行库里已经在了,一条 item 都没有会被对账抓成假红。 */
28976
+ insertSegmentEnd() {
28977
+ return this.insert({
28978
+ kind: "control",
28979
+ role: "assistant",
28980
+ status: "completed",
28981
+ origin: "server",
28982
+ text: "",
28983
+ attrs: {},
28984
+ inContent: false,
28985
+ persist: true,
28986
+ payload: { code: "segment_end", note: SEGMENT_END_NOTE }
28987
+ });
28988
+ }
28939
28989
  /** 行落库了,把本段还挂着 NULL 的 assistant item 认领回去,并让后续 item 直接带上它。 */
28940
28990
  bindMessage(messageId) {
28941
28991
  this.messageId = messageId;
@@ -159847,6 +159897,7 @@ var DEFAULT_BUFFER_MAX, DEFAULT_GRACE_MS, DEFAULT_LIVE_COALESCE_MS, DEFAULT_LIVE
159847
159897
  var init_live_chat = __esm({
159848
159898
  "../server/src/live-chat.ts"() {
159849
159899
  "use strict";
159900
+ init_chat_item_ledger();
159850
159901
  DEFAULT_BUFFER_MAX = 4e3;
159851
159902
  DEFAULT_GRACE_MS = 6e4;
159852
159903
  DEFAULT_LIVE_COALESCE_MS = 24;
@@ -160593,6 +160644,24 @@ var init_live_chat = __esm({
160593
160644
  text: shown,
160594
160645
  ...opts?.attachments?.length ? { attachments: opts.attachments } : {}
160595
160646
  };
160647
+ const segmentEnd = (() => {
160648
+ try {
160649
+ return turn.items?.closeSegmentBeforeUserInput() ?? null;
160650
+ } catch {
160651
+ return null;
160652
+ }
160653
+ })();
160654
+ if (segmentEnd) {
160655
+ this.publishServerItem(turn, {
160656
+ itemId: segmentEnd.itemId,
160657
+ itemType: "control",
160658
+ startedVersion: segmentEnd.startedVersion,
160659
+ version: segmentEnd.version,
160660
+ ord: segmentEnd.ord,
160661
+ status: "completed",
160662
+ payload: { code: "segment_end", note: SEGMENT_END_NOTE }
160663
+ });
160664
+ }
160596
160665
  try {
160597
160666
  turn.items?.recordUserInput(shown, opts?.attachments, opts?.clientKeys);
160598
160667
  } catch {
@@ -212756,7 +212825,15 @@ var init_service4 = __esm({
212756
212825
  * 亲手提交过至少一版」。这是仓库里唯一一条真实存在的「产物 ↔ 人」关系;
212757
212826
  * 「谁 own 这个节点」不用(节点 owner 是**派给谁**,派了没干完的也会被算成产出,那是假数据)。
212758
212827
  *
212759
- * 同一产物被同一人提交多版只出现一次,`revisionCount` 记他提过几版、`updatedAt` 取他最近一次提交。
212828
+ * **空轮不算产出**(ADR 0539 补记):内核把**每一轮执行**都投影成一版 revision,这一轮一个产出文件
212829
+ * 都没有时 bridge 如实给 `contentKind="empty"`(运行中占位 / 被限流打死的空跑 / 只留结论没产正文,
212830
+ * 见 `bridge-readmodel.workToRevision` 与 ADR-0134)。那种版本这里一律跳过——它不是「他交了什么」,
212831
+ * 展示层拿到只会得到一行没有大小、点开写着「还没有可预览的正文」的空行。
212832
+ * 项目页「相关文件」早就不列这类轮次(`collectEngineFilesForProject` 的
212833
+ * `latestAcceptId` / `cancelledAt` / `arts.length === 0` 三道闸),本栏此前漏了这条口径。
212834
+ *
212835
+ * 于是 `revisionCount` 记的是**他交付过几版**、`latestRevisionId`/`updatedAt` 取**他最近一次真的
212836
+ * 交了东西的那一版**;一版都没交过内容的产物整条不出现。
212760
212837
  *
212761
212838
  * 数据同 `artifactDetail`:从 `kernel.model` 现算投影(新引擎 oplog 已退役、派生 store 恒空),
212762
212839
  * 不读 `artifact_revisions` 表——读那张表在新引擎上永远是空列表。
@@ -212774,6 +212851,7 @@ var init_service4 = __esm({
212774
212851
  const byArtifact = /* @__PURE__ */ new Map();
212775
212852
  for (const revision of projection.revisions) {
212776
212853
  if (revision.actorId !== actorId) continue;
212854
+ if (isEmptyContent(this.kernel.model.revisions.get(revision.revisionId)?.contentKind)) continue;
212777
212855
  const artifact = artifactById.get(revision.artifactId);
212778
212856
  if (!artifact) continue;
212779
212857
  const prev = byArtifact.get(revision.artifactId);
@@ -212816,8 +212894,10 @@ var init_service4 = __esm({
212816
212894
  * 同一个 {@link contentByteSize}**,不另写一套判据):装箱单摊成每个物理文件一行,
212817
212895
  * 其余(inline-blob / external-pin / 装箱单解析失败)整份产物一行。
212818
212896
  *
212819
- * 取哪一版:**他自己最近提交的那一版**(`latestRevisionId`),不是产物的 current head——
212820
- * 这一栏回答的是「他交了什么」,拿别人后来覆盖的版本冒充他的产出就是假数据。
212897
+ * 取哪一版:**他自己最近交付过内容的那一版**(`latestRevisionId`,空轮已在
212898
+ * {@link listActorArtifacts} 里滤掉),不是产物的 current head——这一栏回答的是「他交了什么」,
212899
+ * 拿别人后来覆盖的版本冒充他的产出就是假数据。该版本 id 随行发给前端(`revisionId`),
212900
+ * 预览栏据它定位同一版:行与预览必须指着同一个东西,否则会出现「行上有 20 KB、点开说没有正文」。
212821
212901
  */
212822
212902
  async listActorFiles(actorId) {
212823
212903
  const artifacts = await this.listActorArtifacts(actorId);
@@ -212829,6 +212909,7 @@ var init_service4 = __esm({
212829
212909
  const base = {
212830
212910
  artifactId: item.artifactId,
212831
212911
  projectId: item.projectId,
212912
+ revisionId: item.latestRevisionId,
212832
212913
  type: item.type,
212833
212914
  updatedAt: item.updatedAt,
212834
212915
  ...contentKind ? { contentKind } : {},
@@ -220147,6 +220228,20 @@ ${input.description}
220147
220228
  };
220148
220229
  });
220149
220230
  }
220231
+ /**
220232
+ * 这一行变量是谁建的(子 PRD《密钥管理》§1 的权限判据)。**只读元数据、不解密**——
220233
+ * `listVariables()` 会把每一行都解一遍出掩码,写路径上没必要付那个钱。
220234
+ *
220235
+ * 返回 undefined 有两种含义,调用方按「放行」处理:行不存在(新建),
220236
+ * 或行是 2026-09-10 之前落的(那时还没有 created_by 这一格)。
220237
+ */
220238
+ async variableCreatedBy(key, selector = {}) {
220239
+ const rows = await this.opts.store.listVariables();
220240
+ const hit = rows.find(
220241
+ (r) => r.key === key && (r.actorId ?? "") === (selector.actorId ?? "") && (r.projectId ?? "") === (selector.projectId ?? "")
220242
+ );
220243
+ return hit?.createdBy;
220244
+ }
220150
220245
  async revealVariable(key, actorId) {
220151
220246
  const ct = await this.opts.store.getVariableCiphertext(key, actorId);
220152
220247
  if (ct === null) return null;
@@ -221932,6 +222027,17 @@ function actorsDomain(opts) {
221932
222027
  );
221933
222028
  }
221934
222029
  };
222030
+ const requireSecretOwner = async (req, service, key, selector = {}) => {
222031
+ const owner = await service.variableCreatedBy(key, selector);
222032
+ if (!owner || owner === req.auth.actor) return;
222033
+ const manager = opts.isCompanyManager ? await opts.isCompanyManager(req.auth.companyId ?? "", req.auth.actor).catch(() => false) : false;
222034
+ if (manager) return;
222035
+ throw new ApiError(
222036
+ 403,
222037
+ "SECRET_NOT_OWNED",
222038
+ "\u8FD9\u4EFD\u5BC6\u94A5\u7531\u522B\u4EBA\u521B\u5EFA\uFF0C\u53EA\u6709\u521B\u5EFA\u8005\u672C\u4EBA\u6216\u7EC4\u7EC7\u7BA1\u7406\u8005\u53EF\u4EE5\u4FEE\u6539 / \u5220\u9664"
222039
+ );
222040
+ };
221935
222041
  const auditVariableChange = async (req, kind, variableKey, targetScope) => {
221936
222042
  await opts.credentialAudit?.({
221937
222043
  kind,
@@ -221986,6 +222092,7 @@ function actorsDomain(opts) {
221986
222092
  );
221987
222093
  }
221988
222094
  if (b2.value === void 0 || b2.value === "") return { status: 200, body: { ok: true } };
222095
+ await requireSecretOwner(req, service, b2.key, scope === "project" ? { projectId: b2.projectId } : {});
221989
222096
  await service.putVariable({
221990
222097
  key: b2.key,
221991
222098
  value: b2.value,
@@ -222013,6 +222120,7 @@ function actorsDomain(opts) {
222013
222120
  requireVariableManager(req);
222014
222121
  const { service } = await resolveCtx(req.auth.companyId);
222015
222122
  const projectId2 = req.query.get("projectId") ?? void 0;
222123
+ await requireSecretOwner(req, service, req.params.key, projectId2 ? { projectId: projectId2 } : {});
222016
222124
  await service.deleteVariable(
222017
222125
  req.params.key,
222018
222126
  void 0,
@@ -222060,6 +222168,7 @@ function actorsDomain(opts) {
222060
222168
  );
222061
222169
  }
222062
222170
  }
222171
+ await requireSecretOwner(req, service, b2.key, { actorId });
222063
222172
  await service.putVariable({
222064
222173
  key: b2.key,
222065
222174
  value: b2.value,
@@ -222080,6 +222189,7 @@ function actorsDomain(opts) {
222080
222189
  router.delete("/api/actors/:id/variables/:key", async (req) => {
222081
222190
  requireVariableManager(req);
222082
222191
  const { service } = await resolveCtx(req.auth.companyId);
222192
+ await requireSecretOwner(req, service, req.params.key, { actorId: req.params.id });
222083
222193
  await service.deleteVariable(req.params.key, req.params.id);
222084
222194
  await auditVariableChange(req, "credential_delete", req.params.key, `personal:${req.params.id}`);
222085
222195
  return { status: 200, body: { ok: true } };
@@ -222888,7 +222998,7 @@ function createActorsDomain(opts) {
222888
222998
  return {
222889
222999
  service: defaultCtx.service,
222890
223000
  resolveCtx,
222891
- register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
223001
+ register: actorsDomain({ resolveCtx, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.isCompanyManager ? { isCompanyManager: opts.isCompanyManager } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
222892
223002
  };
222893
223003
  }
222894
223004
  var import_node_crypto49;
@@ -223245,6 +223355,7 @@ ${errs.join("\n")}`);
223245
223355
  id: item.id,
223246
223356
  artifact_id: item.artifactId,
223247
223357
  project_id: item.projectId,
223358
+ revision_id: item.revisionId,
223248
223359
  type: item.type,
223249
223360
  name: item.name,
223250
223361
  file_path: item.filePath,
@@ -223920,6 +224031,15 @@ var init_routes5 = __esm({
223920
224031
  }
223921
224032
  });
223922
224033
 
224034
+ // ../server/src/domains/collab/activity-notice.ts
224035
+ var NOTICE_TITLE;
224036
+ var init_activity_notice = __esm({
224037
+ "../server/src/domains/collab/activity-notice.ts"() {
224038
+ "use strict";
224039
+ NOTICE_TITLE = "\u53D8\u66F4\u901A\u77E5";
224040
+ }
224041
+ });
224042
+
223923
224043
  // ../server/src/domains/collab/review-activity.ts
223924
224044
  function reviewCardId(targetWorkId, reviewerActorId) {
223925
224045
  return `${REVIEW_CARD_PREFIX}${encodeURIComponent(targetWorkId)}:${encodeURIComponent(reviewerActorId)}`;
@@ -224068,35 +224188,18 @@ var init_review_activity = __esm({
224068
224188
 
224069
224189
  // ../server/src/domains/collab/review-requirement-activity.ts
224070
224190
  function reviewRequirementActivity(snap, events, ref2) {
224071
- const previous3 = /* @__PURE__ */ new Map();
224072
224191
  const titles = new Map(snap.nodes.map((n) => [n.id, n.title]));
224073
224192
  const cards = [];
224074
224193
  const label = (r) => `${ref2(r.reviewerActorId).name || "\u672A\u547D\u540D\u5BA1\u6838\u4EBA"}${r.source === "closure" ? "\uFF08\u7ED3\u6848\u5BA1\u6838\uFF09" : ""}`;
224075
- const names = (rows) => [...new Set(rows.map(label))].join("\u3001") || "\u65E0";
224194
+ const names = (rows) => [...new Set(rows.map(label))].join("\u3001");
224195
+ const submitter = (id) => ref2(id).name?.trim() || "\u672A\u547D\u540D\u6210\u5458";
224196
+ const sentence = (id, nodeTitle, rows) => rows.length ? `${submitter(id)} \u66F4\u6539\u300A${nodeTitle}\u300B\u8282\u70B9\u7684\u5BA1\u6838\u4EBA\u4E3A ${names(rows)}\u3002` : `${submitter(id)} \u6E05\u7A7A\u4E86\u300A${nodeTitle}\u300B\u8282\u70B9\u7684\u5BA1\u6838\u4EBA\u3002`;
224076
224197
  for (const rec of [...events].sort((a, b2) => a.seq - b2.seq)) {
224077
224198
  if (rec.status === "pending" || rec.status === "failed") continue;
224078
224199
  const e = rec.event;
224079
- if (e.kind === "plan.changed") {
224080
- for (const node2 of e.addNodes ?? []) previous3.set(node2.id, node2.reviewers ?? []);
224081
- for (const id of e.removeNodes ?? []) previous3.delete(id);
224082
- continue;
224083
- }
224084
- if (e.kind === "plan.add_node") {
224085
- previous3.set(e.nodeId, e.reviewers ?? []);
224086
- continue;
224087
- }
224088
224200
  if (e.kind !== "plan.update_review_requirements") continue;
224089
- const before = previous3.get(e.nodeId);
224090
- const after = e.reviewers;
224091
- const beforeKeys = new Set(before?.map(requirementKey));
224092
- const afterKeys = new Set(after.map(requirementKey));
224093
- const added = before ? after.filter((r) => !beforeKeys.has(requirementKey(r))) : [];
224094
- const removed = before?.filter((r) => !afterKeys.has(requirementKey(r))) ?? [];
224201
+ if (!isAuthoredChange(rec)) continue;
224095
224202
  const nodeTitle = titles.get(e.nodeId) || "\u5DF2\u79FB\u9664\u7684\u8282\u70B9";
224096
- if (!isAuthoredChange(rec)) {
224097
- previous3.set(e.nodeId, after);
224098
- continue;
224099
- }
224100
224203
  cards.push({
224101
224204
  id: `wo:${rec.seq}`,
224102
224205
  seq: rec.seq,
@@ -224107,24 +224210,19 @@ function reviewRequirementActivity(snap, events, ref2) {
224107
224210
  executor: ref2(rec.actorId),
224108
224211
  ...rec.handActorId ? { handActor: ref2(rec.handActorId) } : {},
224109
224212
  phase: "done",
224110
- status: `\u66F4\u65B0\u4E86\u300A${nodeTitle}\u300B\u7684\u5BA1\u6838\u5217\u8868\u3002`,
224111
- detail: [
224112
- ...added.length ? [`\u65B0\u589E\uFF1A${names(added)}`] : [],
224113
- ...removed.length ? [`\u79FB\u9664\uFF1A${names(removed)}`] : [],
224114
- `\u66F4\u65B0\u540E\uFF1A${names(after)}`
224115
- ].join("\n"),
224213
+ noticeTitle: NOTICE_TITLE,
224214
+ status: sentence(rec.actorId, nodeTitle, e.reviewers),
224116
224215
  artifacts: [],
224117
224216
  actions: []
224118
224217
  });
224119
- previous3.set(e.nodeId, after);
224120
224218
  }
224121
224219
  return cards;
224122
224220
  }
224123
- var requirementKey, isAuthoredChange;
224221
+ var isAuthoredChange;
224124
224222
  var init_review_requirement_activity = __esm({
224125
224223
  "../server/src/domains/collab/review-requirement-activity.ts"() {
224126
224224
  "use strict";
224127
- requirementKey = (r) => JSON.stringify([r.reviewerActorId, r.reviewGroup, r.source]);
224225
+ init_activity_notice();
224128
224226
  isAuthoredChange = (rec) => rec.origin !== "apply" && rec.origin !== "effect" && !rec.actorId.startsWith("actor:system:");
224129
224227
  }
224130
224228
  });
@@ -224220,7 +224318,6 @@ function buildWorkorderActivity(input) {
224220
224318
  return tail || nodeId;
224221
224319
  };
224222
224320
  const cardOfWork = /* @__PURE__ */ new Map();
224223
- const latestCardOfNode = /* @__PURE__ */ new Map();
224224
224321
  const executionCards = [];
224225
224322
  const offTimeline = (w2) => isDispatchLayerFailure(w2.outcomeDetail) || w2.outcome === "completed" && statusOf2(w2) !== "success" && !artifactsByWork.get(w2.id)?.length;
224226
224323
  const statusOf2 = (w2) => w2.status ?? (w2.outcome === "failed" ? "failed" : w2.outcome === "completed" ? "success" : "running");
@@ -224249,35 +224346,45 @@ function buildWorkorderActivity(input) {
224249
224346
  works.sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id));
224250
224347
  const interrupts = [...interruptsOfNode.get(nodeId) ?? []].sort();
224251
224348
  const node2 = nodeById.get(nodeId);
224349
+ const nodeCancelledAt = node2?.cancelledAt ?? null;
224252
224350
  const name = nodeName(nodeId);
224253
- let card2 = null;
224351
+ let fold2 = null;
224254
224352
  for (const [idx, w2] of works.entries()) {
224255
- if (!card2) {
224256
- card2 = {
224257
- /* id 取组内**首个** work(React key 与滚动锚点因此稳定);`at` 是这一轮开工的时刻。 */
224258
- id: `work:${w2.id}`,
224259
- traceWorkIds: [],
224260
- seq: 0,
224261
- // 事件循环里按首个 work.create seq
224262
- at: w2.createdAt,
224263
- updatedAt: w2.createdAt,
224264
- nodeId,
224265
- executorId: w2.assigneeActorId,
224266
- phase: "running",
224267
- status: ACTIVITY_COPY.working(name),
224268
- artifacts: [],
224269
- actions: []
224270
- };
224271
- executionCards.push(card2);
224272
- latestCardOfNode.set(nodeId, card2);
224353
+ const next = works[idx + 1];
224354
+ const artifacts = artifactsByWork.get(w2.id) ?? [];
224355
+ const st = statusOf2(w2);
224356
+ const interrupt = interrupts.find((t) => t >= w2.createdAt && (!next || t < next.createdAt));
224357
+ const removedHere = !!nodeCancelledAt && nodeCancelledAt >= w2.createdAt && (!next || nodeCancelledAt < next.createdAt);
224358
+ const reassigned = !!next && next.assigneeActorId !== w2.assigneeActorId;
224359
+ const killedByPlan = !!(w2.deadAt || w2.cancelledAt) && !!next && (reassigned || (next.nodeVersion ?? 0) > (w2.nodeVersion ?? 0));
224360
+ const failedRound = !w2.conclusion && !interrupt && !removedHere && !killedByPlan && (!!w2.deadAt || !!w2.cancelledAt || st === "failed" || st === "retry");
224361
+ if (failedRound && fold2) {
224362
+ fold2.traceWorkIds.push(w2.id);
224363
+ cardOfWork.set(w2.id, fold2);
224364
+ fold2.updatedAt = lastStampOf(w2);
224365
+ fold2.stuckReason = w2.outcomeDetail?.reason === "timeout" ? "timeout" : "failed";
224366
+ fold2.foldedRounds = (fold2.foldedRounds ?? 1) + 1;
224367
+ fold2.status = ACTIVITY_COPY.foldedFailures(nameOf(fold2.executorId), name, fold2.foldedRounds);
224368
+ continue;
224273
224369
  }
224274
- card2.traceWorkIds.push(w2.id);
224370
+ fold2 = null;
224371
+ const card2 = {
224372
+ /* 卡 id = 这一轮的 workId(连败折叠时取第一轮,React key 与滚动锚点因此稳定)。 */
224373
+ id: `work:${w2.id}`,
224374
+ traceWorkIds: [w2.id],
224375
+ seq: 0,
224376
+ // 事件循环里按这一轮 work.create 的 seq 填
224377
+ at: w2.createdAt,
224378
+ updatedAt: lastStampOf(w2),
224379
+ nodeId,
224380
+ executorId: w2.assigneeActorId,
224381
+ phase: "running",
224382
+ status: ACTIVITY_COPY.working(name),
224383
+ artifacts,
224384
+ actions: []
224385
+ };
224386
+ executionCards.push(card2);
224275
224387
  cardOfWork.set(w2.id, card2);
224276
- card2.executorId = w2.assigneeActorId;
224277
- card2.updatedAt = lastStampOf(w2);
224278
- card2.artifacts = artifactsByWork.get(w2.id) ?? card2.artifacts;
224279
- delete card2.openWorkId;
224280
- delete card2.stuckReason;
224281
224388
  if (w2.conclusion) {
224282
224389
  card2.phase = "done";
224283
224390
  if (rootBriefNodeIds.has(nodeId)) {
@@ -224293,32 +224400,21 @@ function buildWorkorderActivity(input) {
224293
224400
  card2.status = awaits ? ACTIVITY_COPY.doneAwaitingReview(name) : ACTIVITY_COPY.done(name);
224294
224401
  }
224295
224402
  }
224296
- card2 = null;
224297
- continue;
224298
- }
224299
- const next = works[idx + 1];
224300
- const interrupt = interrupts.find((t) => t >= w2.createdAt && (!next || t < next.createdAt));
224301
- if (interrupt) {
224403
+ } else if (interrupt) {
224302
224404
  card2.phase = "stuck";
224303
224405
  card2.status = ACTIVITY_COPY.interruptedByIssue(name);
224304
224406
  if (interrupt > card2.updatedAt) card2.updatedAt = interrupt;
224305
- card2 = null;
224306
- continue;
224307
- }
224308
- const stuckKind = w2.outcomeDetail?.reason === "timeout" ? "timeout" : "failed";
224309
- const st = statusOf2(w2);
224310
- if (w2.deadAt || w2.cancelledAt) {
224407
+ } else if (removedHere || killedByPlan) {
224311
224408
  card2.phase = "stuck";
224312
- card2.stuckReason = stuckKind;
224313
- card2.status = ACTIVITY_COPY.killed(name);
224314
- } else if (st === "failed") {
224409
+ card2.status = removedHere ? ACTIVITY_COPY.interruptedByNodeRemoved(name) : reassigned ? ACTIVITY_COPY.interruptedByReassign(name, nameOf(next.assigneeActorId)) : ACTIVITY_COPY.interruptedByPlanChange(name);
224410
+ if (!removedHere && !reassigned) card2.planChangeUnnamed = true;
224411
+ if (removedHere && nodeCancelledAt > card2.updatedAt) card2.updatedAt = nodeCancelledAt;
224412
+ } else if (failedRound) {
224413
+ const stuckKind = w2.outcomeDetail?.reason === "timeout" ? "timeout" : "failed";
224315
224414
  card2.phase = "stuck";
224316
224415
  card2.stuckReason = stuckKind;
224317
- card2.status = stuckKind === "timeout" ? ACTIVITY_COPY.timedOut(nameOf(card2.executorId)) : ACTIVITY_COPY.failed(nameOf(card2.executorId));
224318
- } else if (st === "retry") {
224319
- card2.phase = "stuck";
224320
- card2.stuckReason = stuckKind;
224321
- card2.status = ACTIVITY_COPY.awaitingRedispatch(nameOf(card2.executorId), name);
224416
+ card2.status = w2.deadAt || w2.cancelledAt ? ACTIVITY_COPY.killed(name) : st === "retry" ? ACTIVITY_COPY.awaitingRedispatch(nameOf(card2.executorId), name) : stuckKind === "timeout" ? ACTIVITY_COPY.timedOut(nameOf(card2.executorId)) : ACTIVITY_COPY.failed(nameOf(card2.executorId));
224417
+ fold2 = card2;
224322
224418
  } else if (w2.endedAt) {
224323
224419
  card2.phase = "running";
224324
224420
  card2.status = ACTIVITY_COPY.awaitingConclude(name);
@@ -224339,17 +224435,28 @@ function buildWorkorderActivity(input) {
224339
224435
  if (rec.handActorId) d.handActorId = rec.handActorId;
224340
224436
  return d;
224341
224437
  };
224342
- const woCard = (rec, executorId, status, detail, phase = "done") => {
224438
+ const cardOpenAt = (nodeId, at) => {
224439
+ const list2 = worksOfNode.get(nodeId);
224440
+ if (!list2) return void 0;
224441
+ let hit;
224442
+ for (const w2 of list2) {
224443
+ if (w2.createdAt > at) break;
224444
+ hit = w2;
224445
+ }
224446
+ return hit ? cardOfWork.get(hit.id) : void 0;
224447
+ };
224448
+ const woCard = (rec, status, opts = {}) => {
224343
224449
  standalone.push({
224344
224450
  id: `wo:${rec.seq}`,
224345
224451
  seq: rec.seq,
224346
224452
  at: rec.createdAt,
224347
224453
  updatedAt: rec.createdAt,
224348
- executorId,
224454
+ executorId: rec.actorId,
224349
224455
  ...rec.handActorId ? { handActorId: rec.handActorId } : {},
224350
- phase,
224456
+ phase: "done",
224457
+ ...opts.notice ? { noticeTitle: NOTICE_TITLE } : {},
224351
224458
  status,
224352
- ...detail ? { detail } : {},
224459
+ ...opts.detail ? { detail: opts.detail } : {},
224353
224460
  artifacts: [],
224354
224461
  actions: []
224355
224462
  });
@@ -224362,20 +224469,29 @@ function buildWorkorderActivity(input) {
224362
224469
  case "plan.changed": {
224363
224470
  break;
224364
224471
  }
224472
+ /* 暂停 / 恢复是**人对这单做的事**,不是某一轮执行——走「变更通知」那套卡面(帧 `2874:1189`)。
224473
+ 此前它们沿用执行卡的标题模板,卡面第一行被拼成「{谁} 已完成」(`ACTIVITY_PHASE_LABEL.done`),
224474
+ 第二行才是「{谁}已暂停任务。」——标题和正文说的不是同一件事,而「已完成」在这一拍根本
224475
+ 没有所指(暂停不是完成)。 */
224365
224476
  case "workorder.paused":
224366
- woCard(rec, rec.actorId, ACTIVITY_COPY.paused(nameOf(rec.actorId)), str4(ev.reason));
224477
+ woCard(rec, ACTIVITY_COPY.paused(nameOf(rec.actorId)), { notice: true, detail: str4(ev.reason) });
224367
224478
  break;
224368
224479
  case "workorder.resumed":
224369
- woCard(rec, rec.actorId, ACTIVITY_COPY.resumedWorkorder(nameOf(rec.actorId)));
224480
+ woCard(rec, ACTIVITY_COPY.resumedWorkorder(nameOf(rec.actorId)), { notice: true });
224370
224481
  break;
224371
224482
  case "workorder.meta_changed": {
224372
224483
  const patch = ev.patch ?? {};
224373
224484
  if (!("goal" in patch) && !("description" in patch)) break;
224374
- woCard(rec, rec.actorId, ACTIVITY_COPY.goalChanged(nameOf(rec.actorId), managerName()));
224485
+ woCard(rec, ACTIVITY_COPY.goalChanged(nameOf(rec.actorId), managerName()));
224375
224486
  break;
224376
224487
  }
224377
224488
  case "plan.update_spec": {
224378
- woCard(rec, rec.actorId, ACTIVITY_COPY.specConfirmed, void 0, "running");
224489
+ const nodeId = String(ev.nodeId ?? "");
224490
+ const d = cardOpenAt(nodeId, rec.createdAt);
224491
+ if (d?.planChangeUnnamed) {
224492
+ d.status = ACTIVITY_COPY.interruptedBySpecChange(nameOf(rec.actorId), nodeName(nodeId));
224493
+ delete d.planChangeUnnamed;
224494
+ }
224379
224495
  break;
224380
224496
  }
224381
224497
  /* ── 执行(work)──────────────────────────────────────────────
@@ -224655,10 +224771,10 @@ function buildWorkorderActivity(input) {
224655
224771
  case "plan.node_retry": {
224656
224772
  const nodeId = String(ev.nodeId ?? "");
224657
224773
  const by = str4(ev.by) ?? rec.actorId;
224658
- const target = latestCardOfNode.get(nodeId);
224659
- if (!target || target.phase !== "stuck") break;
224774
+ const target = cardOpenAt(nodeId, rec.createdAt);
224775
+ if (!target || target.phase !== "stuck" || !target.stuckReason) break;
224660
224776
  touch(target, rec);
224661
- target.status = target.stuckReason === "timeout" ? ACTIVITY_COPY.retriedAfterTimeout(nameOf(target.executorId), nameOf(by)) : ACTIVITY_COPY.retriedAfterFailure(nameOf(target.executorId), nameOf(by));
224777
+ target.status = target.foldedRounds && target.foldedRounds > 1 ? ACTIVITY_COPY.foldedFailuresRetried(nameOf(target.executorId), nodeName(nodeId), target.foldedRounds, nameOf(by)) : target.stuckReason === "timeout" ? ACTIVITY_COPY.retriedAfterTimeout(nameOf(target.executorId), nameOf(by)) : ACTIVITY_COPY.retriedAfterFailure(nameOf(target.executorId), nameOf(by));
224662
224778
  break;
224663
224779
  }
224664
224780
  default:
@@ -224822,6 +224938,7 @@ function buildWorkorderActivity(input) {
224822
224938
  ...d.reviewWorkId ? { reviewWorkId: d.reviewWorkId, reviewers: d.reviewerIds?.map(ref2) } : {},
224823
224939
  ...d.handActorId ? { handActor: ref2(d.handActorId) } : {},
224824
224940
  phase: d.phase,
224941
+ ...d.noticeTitle ? { noticeTitle: d.noticeTitle } : {},
224825
224942
  status: d.status,
224826
224943
  ...d.detail ? { detail: d.detail } : {},
224827
224944
  ...d.reviewNotes?.length ? { reviewNotes: d.reviewNotes } : {},
@@ -224843,13 +224960,13 @@ var init_activity2 = __esm({
224843
224960
  "use strict";
224844
224961
  init_src();
224845
224962
  init_src4();
224963
+ init_activity_notice();
224846
224964
  init_review_activity();
224847
224965
  init_review_requirement_activity();
224848
224966
  init_workorder_manager();
224849
224967
  init_escalation_recipient();
224850
224968
  ACTIVITY_EVENT_KINDS = [
224851
224969
  "plan.changed",
224852
- "plan.add_node",
224853
224970
  "plan.update_review_requirements",
224854
224971
  "plan.update_spec",
224855
224972
  "plan.node_retry",
@@ -224923,7 +225040,39 @@ var init_activity2 = __esm({
224923
225040
  *
224924
225041
  * 不重复缺口卡那句「需要 @{协调者} 协助解决」:该找谁由缺口卡自己说,执行卡只说这一轮到此为止。
224925
225042
  */
225043
+ /**
225044
+ * **连败折叠**:同一节点上连续没跑成的 N 轮折成一张。不折叠的话 `ws:wo-6d588f8e` 的《隔离边界实现》
225045
+ * 会在时间线上连出 12 张,其中 9 张都是「执行失败。」——真正要给人看的是「这个节点一直没跑成」
225046
+ * 这一件事,逐轮的原因在轨迹抽屉里(`traceWorkIds` 收齐全部 N 轮,一轮不少)。
225047
+ */
225048
+ foldedFailures: (executor, node2, n) => `${executor} \u8FDE\u7EED ${n} \u6B21\u6CA1\u8DD1\u6210\u300A${node2}\u300B\u3002`,
225049
+ /** 折叠卡上补「谁按的重试」——不把「连续 N 次」换成单轮那句。 */
225050
+ foldedFailuresRetried: (executor, node2, n, by) => `${executor} \u8FDE\u7EED ${n} \u6B21\u6CA1\u8DD1\u6210\u300A${node2}\u300B\uFF0C${by}\u5DF2\u4ECB\u5165\u91CD\u8BD5\u3002`,
224926
225051
  interruptedByIssue: (node2) => `\u300A${node2}\u300B\u672C\u8F6E\u6267\u884C\u56E0\u5361\u70B9\u4E2D\u65AD\u3002`,
225052
+ /**
225053
+ * 这一轮**被计划变更打断**(P15)。与卡点中断同级:计划一动,这一轮就不再是「还在跑」,
225054
+ * 而是被引擎当场顶掉了——`plan.delete_node` 直接 `killLatestWork`;`plan.update_spec`(spec 真变)
225055
+ * 与 `plan.assign_actor`(换人)把 `node.version` 加一,scan 随即重派,`work.create` 自带
225056
+ * supersede 把在跑的这一轮翻成 dead。
225057
+ *
225058
+ * 此前这两条都不结束卡:删节点那档只落到 {@link killed}「本轮执行已终止」,不说是计划调整;
225059
+ * 改 spec / 改派那档**根本没有结局**——新一轮直接改写同一张卡,「中断」在页面上不存在,
225060
+ * 卡上署名还会换成新人(与 §2.1「一张卡只讲一个人的事」相抵)。
225061
+ */
225062
+ interruptedByPlanChange: (node2) => `\u300A${node2}\u300B\u672C\u8F6E\u6267\u884C\u56E0\u8BA1\u5212\u8C03\u6574\u4E2D\u65AD\u3002`,
225063
+ /**
225064
+ * 计划变更里最常见的那一种:**有人改了任务说明**,在跑的这一轮因此被顶掉。
225065
+ *
225066
+ * 这句取代了原来那张独立的任务级卡「已确认任务说明更新,正在判断该修改的影响。」——两张卡讲的
225067
+ * 本来就是同一件事:计划动了、这一轮做不下去了。旧卡还有两个毛病:`phase` 写死 `running`,
225068
+ * 没有任何一拍能让它收口(`ws:wo-217a0909` 12:13 那两张就是这么永久停在「进行中」的);文案是
225069
+ * 一句没有主语也没有宾语的固定串,两次内容不同的修改渲染成两行一模一样的字。
225070
+ */
225071
+ interruptedBySpecChange: (by, node2) => `${by} \u66F4\u65B0\u4E86\u4EFB\u52A1\u8BF4\u660E\uFF0C\u300A${node2}\u300B\u672C\u8F6E\u6267\u884C\u5230\u6B64\u4E2D\u65AD\u3002`,
225072
+ /** 换人那一档点破接手的人——卡到此为止,下一轮是新执行人的新卡。 */
225073
+ interruptedByReassign: (node2, next) => `\u300A${node2}\u300B\u672C\u8F6E\u6267\u884C\u56E0\u8BA1\u5212\u8C03\u6574\u4E2D\u65AD\uFF0C\u5DF2\u6539\u7531 ${next} \u63A5\u624B\u3002`,
225074
+ /** 节点被移出计划:这一轮再没有下文,也不会有新卡。 */
225075
+ interruptedByNodeRemoved: (node2) => `\u300A${node2}\u300B\u5DF2\u4ECE\u8BA1\u5212\u4E2D\u79FB\u9664\uFF0C\u672C\u8F6E\u6267\u884C\u4E2D\u65AD\u3002`,
224927
225076
  /**
224928
225077
  * 引擎判卷 success、`endedAt` 也写了,却始终没有 `work.conclude`:**交了稿没收口**。
224929
225078
  * 如实说这一档,不冒充「正在处理」——后者把一轮早就结束的执行画成还在跑。
@@ -224960,7 +225109,6 @@ var init_activity2 = __esm({
224960
225109
  paused: (actor) => `${actor}\u5DF2\u6682\u505C\u4EFB\u52A1\u3002`,
224961
225110
  resumedWorkorder: (actor) => `${actor}\u5DF2\u6062\u590D\u4EFB\u52A1\u3002`,
224962
225111
  goalChanged: (actor, manager) => `${actor}\u4FEE\u6539\u4E86\u4EFB\u52A1\u76EE\u6807\uFF0C\u9700\u8981 ${manager}\u91CD\u65B0\u8BC4\u4F30\u3002`,
224963
- specConfirmed: "\u5DF2\u786E\u8BA4\u4EFB\u52A1\u8BF4\u660E\u66F4\u65B0\uFF0C\u6B63\u5728\u5224\u65AD\u8BE5\u4FEE\u6539\u7684\u5F71\u54CD\u3002",
224964
225112
  handleEscalation: (target) => `${target} \u7684\u6267\u884C\u5F02\u5E38\u6301\u7EED\u672A\u89E3\u51B3\u3002`,
224965
225113
  handledEscalation: (target) => `\u5DF2\u5904\u7406 ${target} \u7684\u6267\u884C\u5F02\u5E38\u3002`,
224966
225114
  /**
@@ -225969,13 +226117,7 @@ function buildWaitingInbox(reviews, works, policy, nowIso, me, resolveActor) {
225969
226117
  const isSelf = r.actorId === me;
225970
226118
  const actorLabel = displayName(resolveActor, r.actorId);
225971
226119
  if (r.status === "dead" || r.status === "exhausted") {
225972
- out.push(isSelf ? card(kind, r, "urgent", start, `\u4F60\u7684${KIND_LABEL[kind]}\u5DF2\u4E2D\u6B62\uFF0C\u9700\u8981\u4F60\u4ECB\u5165\u91CD\u5F00`) : card(
225973
- kind,
225974
- r,
225975
- "urgent",
225976
- start,
225977
- `\u8FD9\u4E00\u6B65\u7684${KIND_LABEL[kind]}\u5DF2\u4E2D\u6B62\uFF08${actorLabel} \u8FDE\u7EED\u5931\u8D25\u8D85\u9650\uFF09\u2014\u2014\u672C\u8F6E${KIND_LABEL[kind]}\u6536\u4E0D\u9F50\u3001\u8282\u70B9\u505C\u5728\u8FD9\u91CC\uFF0C\u9700\u8981\u4F60\u91CD\u5F00\u8FD9\u4E00\u8F6E`
225978
- ));
226120
+ out.push(isSelf ? card(kind, r, "urgent", start, `\u4F60\u7684${KIND_LABEL[kind]}\u5DF2\u4E2D\u6B62\uFF0C\u9700\u8981\u4F60\u4ECB\u5165\u91CD\u5F00`) : card(kind, r, "urgent", start, kind === "review-waiting" ? `\u8FD9\u4E00\u6B65\u7684\u8BC4\u5BA1\u5DF2\u4E2D\u6B62\uFF08${actorLabel} \u8FDE\u7EED\u5931\u8D25\u8D85\u9650\uFF09\u2014\u2014\u672C\u8F6E\u8BC4\u5BA1\u6536\u4E0D\u9F50\u3001\u8282\u70B9\u505C\u5728\u8FD9\u91CC\uFF0C\u9700\u8981\u4F60\u91CD\u5F00\u8FD9\u4E00\u8F6E` : `\u8FD9\u4E00\u6B65\u7684\u5904\u7406\u5DF2\u4E2D\u6B62\uFF08${actorLabel} \u8FDE\u7EED\u5931\u8D25\u8D85\u9650\uFF09\u2014\u2014\u8282\u70B9\u505C\u5728\u8FD9\u91CC\uFF0C\u9700\u8981\u6709\u4EBA\u91CD\u5F00`));
225979
226121
  continue;
225980
226122
  }
225981
226123
  if (r.status !== "running") continue;
@@ -226027,14 +226169,20 @@ function collectWaitingRows(snapshots2, me, policy = CODE_DEFAULT_POLICY) {
226027
226169
  const requirementHeads = new Map(snap.requirements.map((q2) => [reqKey(q2), q2.latestReviewId]));
226028
226170
  const requirementHead = (r) => requirementHeads.get(reqKey(r));
226029
226171
  const nodesWithRequirements = new Set(snap.requirements.map((q2) => q2.nodeId));
226030
- const managerOfWo = snap.nodes.find((n) => n.type === "brief")?.fields?.["manager"];
226172
+ const briefNode = snap.nodes.find((n) => n.type === "brief");
226173
+ const managerOfWo = briefNode?.fields?.["manager"];
226031
226174
  const isManager = typeof managerOfWo === "string" && managerOfWo === me;
226175
+ const launcher = briefNode?.assigneeActorId ?? null;
226176
+ const openIssueNodes = new Set(
226177
+ snap.issues.filter((i) => i.resolvedAt === null && i.aboutNodeId !== null).map((i) => i.aboutNodeId)
226178
+ );
226179
+ const nodeHasOpenIssue = (nodeId) => openIssueNodes.has(nodeId);
226032
226180
  const latestReview = /* @__PURE__ */ new Map();
226033
226181
  for (const r of snap.reviews) {
226034
226182
  if (cancelled.has(r.nodeId)) continue;
226035
226183
  if (acceptedWork.get(r.nodeId) === r.targetWorkId) continue;
226036
226184
  const mine = r.reviewerActorId === me;
226037
- const oversees = nodeOwner.get(r.nodeId) === me || isManager;
226185
+ const oversees = nodeOwner.get(r.nodeId) === me || isManager || launcher === me;
226038
226186
  if (!mine && !oversees) continue;
226039
226187
  latestBy(latestReview, r);
226040
226188
  }
@@ -226053,10 +226201,12 @@ function collectWaitingRows(snapshots2, me, policy = CODE_DEFAULT_POLICY) {
226053
226201
  const humanPending = !stopped && classify(r.reviewerActorId) === "human";
226054
226202
  if (!mine && !stopped && !humanPending) continue;
226055
226203
  const audience = [r.reviewerActorId];
226056
- const owner = nodeOwner.get(r.nodeId);
226057
- if (stopped || humanPending) {
226058
- if (owner && !audience.includes(owner)) audience.push(owner);
226059
- if (typeof managerOfWo === "string" && !audience.includes(managerOfWo)) audience.push(managerOfWo);
226204
+ const addBystanders = humanPending || stopped && !nodeHasOpenIssue(r.nodeId);
226205
+ if (addBystanders) {
226206
+ for (const candidate of [launcher, managerOfWo, nodeOwner.get(r.nodeId)]) {
226207
+ if (typeof candidate !== "string" || classify(candidate) !== "human") continue;
226208
+ if (!audience.includes(candidate)) audience.push(candidate);
226209
+ }
226060
226210
  }
226061
226211
  reviews.push({
226062
226212
  id: r.id,
@@ -226073,17 +226223,25 @@ function collectWaitingRows(snapshots2, me, policy = CODE_DEFAULT_POLICY) {
226073
226223
  const latestMain = /* @__PURE__ */ new Map();
226074
226224
  const latestReply = /* @__PURE__ */ new Map();
226075
226225
  for (const w2 of snap.works) {
226076
- if (w2.assigneeActorId !== me || cancelled.has(w2.nodeId)) continue;
226226
+ if (cancelled.has(w2.nodeId)) continue;
226077
226227
  latestBy(isReplyWork(w2) ? latestReply : latestMain, w2);
226078
226228
  }
226079
226229
  for (const w2 of [...latestMain.values(), ...latestReply.values()]) {
226080
226230
  const st = workState(w2, hasOutput(w2, outputCountOf(w2.id)));
226081
226231
  if (st !== "running" && st !== "dead") continue;
226232
+ const audience = [w2.assigneeActorId];
226233
+ if (st === "dead" && !nodeHasOpenIssue(w2.nodeId)) {
226234
+ for (const candidate of [launcher, managerOfWo, nodeOwner.get(w2.nodeId)]) {
226235
+ if (typeof candidate !== "string" || classify(candidate) !== "human") continue;
226236
+ if (!audience.includes(candidate)) audience.push(candidate);
226237
+ }
226238
+ }
226082
226239
  works.push({
226083
226240
  id: w2.id,
226084
226241
  workorderId: snap.workorder.id,
226085
226242
  artifactId: w2.nodeId,
226086
226243
  actorId: w2.assigneeActorId,
226244
+ notifyActorIds: audience,
226087
226245
  ...nodeTitle.get(w2.nodeId) ? { title: nodeTitle.get(w2.nodeId) } : {},
226088
226246
  createdAt: w2.createdAt,
226089
226247
  startedAt: w2.startedAt,
@@ -259067,6 +259225,16 @@ async function startServe(opts) {
259067
259225
  // ADR 0125:写 scope=project 变量时校验 projectId 真实存在(getProject 直查,含临时项目)。
259068
259226
  // projectStateStore 在下方 ~行 1023 才赋值——本箭头只在请求时跑,那时已就绪(同 resolveDispatchScope 惰性模式)。
259069
259227
  projectExists: async (id) => await projectStateStore.getProject(id) !== null,
259228
+ /* 子 PRD《密钥管理》§1 的权限判据:密钥的写 / 删要认「创建者本人或组织管理者」。
259229
+ owner 与 admin 都算组织管理者(与组织页 `orgIdentityOf` 同一口径)。
259230
+ `controlPlaneStore` 在下方才赋值——本箭头只在请求时跑,那时已就绪(同 projectExists 的惰性模式)。
259231
+ 成员表按 accountId 主键,但行上带 actorId,密钥那边手里只有 actorId,所以按 actorId 找。 */
259232
+ isCompanyManager: async (companyId, actorId) => {
259233
+ if (!companyId || !actorId) return false;
259234
+ const members = await controlPlaneStore.listMembers(companyId).catch(() => []);
259235
+ const me = members.find((m2) => m2.actorId === actorId);
259236
+ return me?.role === "owner" || me?.role === "admin";
259237
+ },
259070
259238
  // CO-302 数据面隔离(actors 域试点):按当前公司取其引擎,用各自 registry 服务该域请求。
259071
259239
  // engineRouter 在下方(行 ~411)以 const 声明;此箭头只在请求时调用,那时已初始化,闭包引用合法
259072
259240
  // (TDZ 只在构造时访问才报错,这里不访问)。默认公司命中现有单实例(registry===registryStore),
@@ -271790,7 +271958,7 @@ function shimScript() {
271790
271958
  }
271791
271959
 
271792
271960
  // src/index.ts
271793
- var PKG_VERSION = true ? "2.2.16" : "dev";
271961
+ var PKG_VERSION = true ? "2.2.17" : "dev";
271794
271962
  var LOCAL_BIN = localBin();
271795
271963
  var NPM_PREFIX = npmPrefix();
271796
271964
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.16",
3
+ "version": "2.2.17",
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": "ece9e8f169691cc135c16c42f8b998b5809aced6"
29
+ "sourceHead": "1c93b971ada0fb88e327a81413365e770998710a"
30
30
  }
31
31
  }