oasis_test_v2 2.2.9 → 2.2.10

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 +210 -41
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -665,7 +665,20 @@ function isAgentAvatarPresetRef(ref2) {
665
665
  const id = ref2.slice(AGENT_AVATAR_PRESET_PREFIX.length);
666
666
  return AGENT_AVATAR_PRESET_IDS.includes(id);
667
667
  }
668
- var AGENT_AVATAR_PRESET_PREFIX, AGENT_AVATAR_LEGACY_PRESET_IDS, AGENT_AVATAR_V2_PRESET_IDS, AGENT_AVATAR_PRESET_IDS, SYSTEM_AVATAR_REF;
668
+ function humanAvatarToneOf(seed) {
669
+ let hash2 = 2166136261;
670
+ for (const ch of seed.trim()) {
671
+ hash2 = Math.imul(hash2 ^ (ch.codePointAt(0) ?? 0), 16777619) >>> 0;
672
+ }
673
+ return hash2 % HUMAN_AVATAR_TONE_COUNT;
674
+ }
675
+ function humanAvatarPresetRef(tone) {
676
+ return `${HUMAN_AVATAR_PRESET_PREFIX}${tone}`;
677
+ }
678
+ function defaultHumanAvatarRef(actorId) {
679
+ return humanAvatarPresetRef(humanAvatarToneOf(actorId));
680
+ }
681
+ var AGENT_AVATAR_PRESET_PREFIX, AGENT_AVATAR_LEGACY_PRESET_IDS, AGENT_AVATAR_V2_PRESET_IDS, AGENT_AVATAR_PRESET_IDS, SYSTEM_AVATAR_REF, HUMAN_AVATAR_PRESET_PREFIX, HUMAN_AVATAR_TONE_COUNT;
669
682
  var init_avatar = __esm({
670
683
  "../contract/src/avatar.ts"() {
671
684
  "use strict";
@@ -696,6 +709,8 @@ var init_avatar = __esm({
696
709
  ...AGENT_AVATAR_V2_PRESET_IDS
697
710
  ];
698
711
  SYSTEM_AVATAR_REF = "preset:system";
712
+ HUMAN_AVATAR_PRESET_PREFIX = "preset:human:";
713
+ HUMAN_AVATAR_TONE_COUNT = 8;
699
714
  }
700
715
  });
701
716
 
@@ -905,7 +920,17 @@ var init_link_windows = __esm({
905
920
  /** orphan-sweep 判「账上在跑、四张认领表都不认」为孤儿的宽限(`serve.ts`)。 */
906
921
  orphanGraceMs: 12e4,
907
922
  /** orphan-sweep 在 serve 冷启动后推迟判孤儿的窗口(`serve.ts`)。 */
908
- orphanColdStartGraceMs: 18e4
923
+ orphanColdStartGraceMs: 18e4,
924
+ /**
925
+ * `/items` 读时把「账上还 running、本进程 live 注册表里却没有」的一轮**显示**成 `interrupted`
926
+ * 的宽限(ADR-0510 D6 表格第二行)。
927
+ *
928
+ * 取值必须 **≥ {@link LINK_WINDOWS.orphanGraceMs}**:这条判定只写 wire、不写账本,它的作用是
929
+ * 让页面别在权威扫描还没开始看的时候就一直转圈;但它也**不该比权威更早下结论**——早于
930
+ * `orphanGraceMs` 就等于拿时间当判据抢在核实前面说话(ADR-0166)。两个数写在同一处,
931
+ * 谁单边调参 `tests/adr-0300-link-windows.test.ts` 当场变红。
932
+ */
933
+ chatTurnPhaseStaleAfterMs: 12e4
909
934
  };
910
935
  }
911
936
  });
@@ -914,12 +939,16 @@ var init_link_windows = __esm({
914
939
  function isSystemActorId(id) {
915
940
  return typeof id === "string" && id.startsWith(SYSTEM_ACTOR_ID_PREFIX);
916
941
  }
917
- var SYSTEM_ACTOR_ID_PREFIX, SYSTEM_ACTOR_DISPLAY_NAME, AGENT_PREFS_DEFAULTS, SkillOwnedByAnotherCompanyError;
942
+ function isHumanActorId(id) {
943
+ return typeof id === "string" && id.startsWith(HUMAN_ACTOR_ID_PREFIX);
944
+ }
945
+ var SYSTEM_ACTOR_ID_PREFIX, SYSTEM_ACTOR_DISPLAY_NAME, HUMAN_ACTOR_ID_PREFIX, AGENT_PREFS_DEFAULTS, SkillOwnedByAnotherCompanyError;
918
946
  var init_registry = __esm({
919
947
  "../contract/src/registry.ts"() {
920
948
  "use strict";
921
949
  SYSTEM_ACTOR_ID_PREFIX = "actor:system";
922
950
  SYSTEM_ACTOR_DISPLAY_NAME = "\u7CFB\u7EDF";
951
+ HUMAN_ACTOR_ID_PREFIX = "actor:human:";
923
952
  AGENT_PREFS_DEFAULTS = { memoryEnabled: true };
924
953
  SkillOwnedByAnotherCompanyError = class extends Error {
925
954
  constructor(skillId, companyId) {
@@ -5560,6 +5589,7 @@ CREATE TABLE IF NOT EXISTS ${q2}.work_artifacts (
5560
5589
  content_kind text NOT NULL CHECK (content_kind IN ('inline','external','manifest')),
5561
5590
  ordinal integer,
5562
5591
  created_at timestamptz NOT NULL,
5592
+ content_edited_at timestamptz,
5563
5593
  UNIQUE (work_id, logical_path)
5564
5594
  );
5565
5595
  CREATE INDEX IF NOT EXISTS work_artifacts_node_idx ON ${q2}.work_artifacts (node_id);
@@ -5725,6 +5755,9 @@ ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS proposal_reason text;
5725
5755
  -- \u2605 \u8FD0\u884C\u6D3B\u8DC3\u5FC3\u8DF3\uFF1Av2 submit_output / work.conclude \u5199\u5165\uFF1Bscan \u8D85\u65F6\u4EE5\u5B83\u8D77\u7B97\uFF08work \u751F\u547D\u5468\u671F\u5EF6\u957F\u540E
5726
5756
  -- 30min \u949F\u4E0D\u80FD\u518D\u7F69\u6B7B\u6574\u4E2A\u4F1A\u8BDD\uFF09\u3002\u5B58\u91CF\u4E3A NULL \u2192 \u56DE\u9000 started_at/created_at\uFF0C\u884C\u4E3A\u4E0D\u53D8\u3002
5727
5757
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS last_activity_at timestamptz;
5758
+ -- \u4EA7\u7269\u6B63\u6587\u5C31\u5730\u6539\u5199\uFF08work.content_edited\uFF09\u7684\u65F6\u523B\u3002\u5C31\u5730\u6539\u5199\u4E0D\u65B0\u5F00 work\u3001\u4E0D\u94F8\u65B0\u7248\uFF0C
5759
+ -- \u6CA1\u6709\u8FD9\u4E00\u5217\uFF0C\u300C\u76F8\u5173\u6587\u4EF6\u300D\u8868\u7684\u65F6\u95F4\u5217\u5C31\u53EA\u80FD\u505C\u5728\u9A8C\u6536\u65F6\u523B\u3002\u5B58\u91CF\u884C\u4E3A NULL = \u4ECE\u672A\u88AB\u6539\u5199\u8FC7\u3002
5760
+ ALTER TABLE ${q2}.work_artifacts ADD COLUMN IF NOT EXISTS content_edited_at timestamptz;
5728
5761
  -- \u2605 \u51BB\u7ED3\u6765\u4FE1\u96C6\u5408\uFF08\u8BA1\u5212 \xA76.5\uFF0C\u9636\u6BB5 E\uFF09\uFF1Awork.create \u6309 reasons \u7684 issue \u9879\u51BB\u7ED3\u7684 issue id \u6279\u6B21\uFF08jsonb \u6570\u7EC4\uFF09\u3002
5729
5762
  -- reply \u5F52\u5C5E/\u9500\u8D26/\u5BA1\u8BA1\u53EA\u8BA4\u5B83\uFF0C\u4E0D\u9760\u65F6\u95F4\u7A97\u731C\u6D4B\u3002\u5B58\u91CF\u4E3A NULL\uFF08\u65E0\u6279\u6B21\u8BED\u4E49\uFF09\u3002
5730
5763
  ALTER TABLE ${q2}.works ADD COLUMN IF NOT EXISTS frozen_issue_ids jsonb;
@@ -11388,7 +11421,11 @@ var init_store_postgres = __esm({
11388
11421
  contentRef: a.content_ref,
11389
11422
  contentKind: a.content_kind,
11390
11423
  ordinal: a.ordinal,
11391
- createdAt: iso(a.created_at)
11424
+ createdAt: iso(a.created_at),
11425
+ // 只在真被改写过时才带这个键:从未改写的行在内存态里是**没有这个字段**的,
11426
+ // PG 回读时补一个 `null` 会让「事件重放出来的态」和「从库里读出来的态」深比不相等
11427
+ // (packages/engine/tests/store-conformance.postgres.test.ts 就是钉这条一致性的)。
11428
+ ...iso(a.content_edited_at) ? { contentEditedAt: iso(a.content_edited_at) } : {}
11392
11429
  })),
11393
11430
  workInputs: inputs.rows.map((i) => ({
11394
11431
  workId: i.work_id,
@@ -11623,9 +11660,9 @@ var init_store_postgres = __esm({
11623
11660
  for (const a of m2.rows) {
11624
11661
  await this.c.query(
11625
11662
  `INSERT INTO ${this.s}.work_artifacts
11626
- (id,work_id,node_id,type,name,logical_path,content_ref,content_kind,ordinal,created_at)
11627
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT (id) DO NOTHING`,
11628
- [a.id, a.workId, a.nodeId, a.type, a.name, a.logicalPath, a.contentRef, a.contentKind, a.ordinal, a.createdAt]
11663
+ (id,work_id,node_id,type,name,logical_path,content_ref,content_kind,ordinal,created_at,content_edited_at)
11664
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (id) DO NOTHING`,
11665
+ [a.id, a.workId, a.nodeId, a.type, a.name, a.logicalPath, a.contentRef, a.contentKind, a.ordinal, a.createdAt, a.contentEditedAt ?? null]
11629
11666
  );
11630
11667
  }
11631
11668
  break;
@@ -12626,7 +12663,7 @@ var init_work = __esm({
12626
12663
  workContentEdited = {
12627
12664
  name: "work/content-edited",
12628
12665
  kind: "work.content_edited",
12629
- apply(e, state) {
12666
+ apply(e, state, ctx) {
12630
12667
  const work = state.work(e.workId);
12631
12668
  if (!work) return;
12632
12669
  const node2 = state.node(work.nodeId);
@@ -12634,7 +12671,7 @@ var init_work = __esm({
12634
12671
  const artifacts = state.artifactsOf(e.workId);
12635
12672
  const target = artifacts.find((a) => a.id === e.artifactId);
12636
12673
  if (!target || target.contentRef !== e.previousRef || target.contentKind === "external") return;
12637
- state.replaceArtifacts(e.workId, artifacts.map((a) => a.id === target.id ? { ...a, contentRef: e.contentRef } : a));
12674
+ state.replaceArtifacts(e.workId, artifacts.map((a) => a.id === target.id ? { ...a, contentRef: e.contentRef, contentEditedAt: ctx.at } : a));
12638
12675
  }
12639
12676
  };
12640
12677
  workConclude = {
@@ -28739,9 +28776,22 @@ var init_chat_item_ledger = __esm({
28739
28776
  ...deterministicId ? { deterministicId } : {}
28740
28777
  });
28741
28778
  }
28742
- /** 服务端合成的错误帧(session.done 抛 / exit 判失败)。不入正文,归 control。 */
28779
+ /**
28780
+ * 服务端合成的错误帧(session.done 抛 / exit 判失败)。不入正文,归 control。
28781
+ *
28782
+ * **返回 outcome 是为了让调用方把它推上 v3 流**(ADR-0510 D5/D6,2026-09-10 修)。
28783
+ * 在此之前这条行只落库、不发帧:客户端在这一轮里唯一收到的终态信号是 `emitV3TurnTerminal`
28784
+ * 发的 `turn_failed`,而那一帧不带任何正文、客户端的 `applyFrame` 又明说「不建 item、
28785
+ * 仅推进 frameSeq」——于是失败原因整条飞不到还开着的页面上,只有下一次 `/items` 快照
28786
+ * 才补得上。生产现场(v2.openfriday.ai,2026-09-10 05:39 那一轮):runtime 报
28787
+ * 「401 Authentication Fails」,库里三处都记着,页面上从头到尾是空的。
28788
+ *
28789
+ * 号取的是**建行时那两格钟**(`startedVersion` 给 wire 的 `started` 帧、`version` 给这一行
28790
+ * 本身的写),与 provider item 走 {@link ChatItemLedger.apply} 那条路逐字同构——
28791
+ * 快照与增量流因此还是同一套版本空间。
28792
+ */
28743
28793
  recordError(text5, extra) {
28744
- this.insert({
28794
+ const id = this.insert({
28745
28795
  kind: "control",
28746
28796
  role: "assistant",
28747
28797
  status: "failed",
@@ -28757,6 +28807,8 @@ var init_chat_item_ledger = __esm({
28757
28807
  ...extra?.detail ? { detail: extra.detail } : {}
28758
28808
  }
28759
28809
  });
28810
+ const row = this.rows.get(id);
28811
+ return { itemId: id, version: row.wireVersion, startedVersion: row.startedVersion, ord: row.ord };
28760
28812
  }
28761
28813
  /**
28762
28814
  * 回灌重启前已经落库的那段正文(`chat-recovery` 重挂 / 迟到对账用)。
@@ -159835,6 +159887,7 @@ var init_live_chat = __esm({
159835
159887
  },
159836
159888
  emitLive: (event) => this.emitLive(turn, event),
159837
159889
  applyNormalizedEvent: (event) => this.applyNormalizedEvent(turn, event),
159890
+ publishServerItem: (item) => this.publishServerItem(turn, item),
159838
159891
  subscribe: (sub) => {
159839
159892
  turn.subscribers.add(sub);
159840
159893
  return () => {
@@ -159981,6 +160034,45 @@ var init_live_chat = __esm({
159981
160034
  };
159982
160035
  this.publishV3Coalesced(turn, frame);
159983
160036
  }
160037
+ /**
160038
+ * 服务端合成 item → 两帧(`started` + 终态),与 {@link emitV3} 给 provider item 发的那两帧
160039
+ * **逐字同构**:先用建行预留的那一格发 `started`(客户端以它为该 item 首次亮相、payload 首次成形),
160040
+ * 再用这一行本身的写号发 `completed` / `failed`。
160041
+ *
160042
+ * 为什么不学 {@link publishUserItem} 用一帧 `set_text`:那条路是给「一次性成形的**正文**」用的,
160043
+ * 落在客户端桶里 `status` 恒为 `streaming`——用户那条消息无所谓,失败收尾的 control 行却必须是
160044
+ * 终态,否则这一轮在客户端看来永远没关。
160045
+ */
160046
+ publishServerItem(turn, item) {
160047
+ if (turn.status !== "running") return;
160048
+ const perItem = turn.v3Items.get(item.itemId) ?? { textOffset: 0, started: false };
160049
+ const ord = turn.items?.ordFor(item.itemId) ?? item.ord ?? void 0;
160050
+ const base = {
160051
+ protocolVersion: 3,
160052
+ streamId: turn.liveStreamId,
160053
+ turnId: this.wireTurnId(turn),
160054
+ itemId: item.itemId,
160055
+ itemType: item.itemType,
160056
+ ...ord !== void 0 ? { ord } : {},
160057
+ offset: 0
160058
+ };
160059
+ if (!perItem.started && item.startedVersion !== void 0) {
160060
+ this.publishV3Coalesced(turn, {
160061
+ ...base,
160062
+ operation: "started",
160063
+ itemVersion: item.startedVersion,
160064
+ payload: item.payload
160065
+ });
160066
+ perItem.started = true;
160067
+ }
160068
+ turn.v3Items.set(item.itemId, perItem);
160069
+ this.publishV3Coalesced(turn, {
160070
+ ...base,
160071
+ operation: item.status === "failed" ? "failed" : "completed",
160072
+ itemVersion: item.version,
160073
+ payload: item.payload
160074
+ });
160075
+ }
159984
160076
  assignV3Seq(turn, frame) {
159985
160077
  return { ...frame, epoch: turn.liveEpoch, seq: ++turn.v3Seq };
159986
160078
  }
@@ -160118,11 +160210,17 @@ var init_live_chat = __esm({
160118
160210
  const frame = {
160119
160211
  protocolVersion: 3,
160120
160212
  streamId: turn.liveStreamId,
160121
- turnId: turn.liveTurnId,
160213
+ // 账本轮次 id,与 item 帧同一套(#1249 把 emitV3 改成了 wireTurnId,收尾帧当时漏了)。
160214
+ // 客户端此前把收尾帧整条丢弃(`applyFrame`:「不建 item,仅推进 frameSeq」),所以这个
160215
+ // 号是对是错都看不出来;相位一挂上去它就得对——否则相位会记到一个桶里根本不存在的 turnId 上。
160216
+ turnId: this.wireTurnId(turn),
160122
160217
  itemId: `oasis-turn:${turn.liveEpoch}:${status}`,
160123
160218
  itemType: "control",
160124
160219
  operation,
160125
160220
  itemVersion: 0,
160221
+ // 相位被携带,不被推导(ADR-0510 D6):这一轮到底是成了还是败了,服务端此刻知道,
160222
+ // 客户端替换即可,不用再去把一轮里每条 item 的 status 取或来猜。
160223
+ phase: status === "done" ? "succeeded" : "failed",
160126
160224
  epoch: turn.liveEpoch,
160127
160225
  seq: ++turn.v3Seq
160128
160226
  };
@@ -162356,14 +162454,25 @@ var init_planner = __esm({
162356
162454
  function systemActorFallback(id) {
162357
162455
  return isSystemActorId(id) ? { name: SYSTEM_ACTOR_DISPLAY_NAME, avatar: SYSTEM_AVATAR_REF } : void 0;
162358
162456
  }
162457
+ function defaultAvatarRefOf(id, kind) {
162458
+ if (kind === "system" || isSystemActorId(id)) return SYSTEM_AVATAR_REF;
162459
+ if (kind === "human" || isHumanActorId(id)) return defaultHumanAvatarRef(id);
162460
+ return void 0;
162461
+ }
162359
162462
  function actorRefFieldsOf(a) {
162360
- const avatar = a.avatar ?? (a.kind === "system" ? SYSTEM_AVATAR_REF : void 0);
162463
+ const avatar = a.avatar ?? defaultAvatarRefOf(a.id, a.kind);
162361
162464
  return {
162362
162465
  name: a.name,
162363
162466
  ...a.roles[0] ? { role: a.roles[0] } : {},
162364
162467
  ...avatar ? { avatar } : {}
162365
162468
  };
162366
162469
  }
162470
+ function actorRefFallbackOf(id) {
162471
+ const system = systemActorFallback(id);
162472
+ if (system) return system;
162473
+ const avatar = defaultAvatarRefOf(id);
162474
+ return avatar ? { avatar } : void 0;
162475
+ }
162367
162476
  function overviewRootIdOf(arts) {
162368
162477
  const roots = arts.filter((a) => a.inputs.length === 0);
162369
162478
  if (roots.length <= 1) return roots[0]?.id;
@@ -198960,8 +199069,11 @@ var init_github = __esm({
198960
199069
  sessionEnv.set("GH_CONFIG_DIR", configDir);
198961
199070
  const appSlug = credentials["app_slug"];
198962
199071
  if (appSlug) sessionEnv.set("GH_APP_SLUG", appSlug);
198963
- const botEmail = credentials["app_bot_email"] ?? "";
198964
- const botName = credentials["app_bot_name"] ?? "";
199072
+ const rawBotEmail = credentials["app_bot_email"] ?? "";
199073
+ const rawBotName = credentials["app_bot_name"] ?? "";
199074
+ const staleBot = Boolean(appSlug && rawBotEmail && !rawBotEmail.includes(`+${appSlug}[bot]@`));
199075
+ const botEmail = staleBot ? "" : rawBotEmail;
199076
+ const botName = staleBot ? appSlug ? `${appSlug}[bot]` : "" : rawBotName;
198965
199077
  const signName = botName || name;
198966
199078
  sessionEnv.set("GIT_AUTHOR_NAME", signName);
198967
199079
  sessionEnv.set("GIT_AUTHOR_EMAIL", botEmail);
@@ -201251,10 +201363,8 @@ async function adoptGithubApp(store, args, deps = {}) {
201251
201363
  await put3("GITHUB_APP_TOKEN_EXPIRES_AT", String(minted.expiresAtMs), false);
201252
201364
  if (args.slug) {
201253
201365
  const bot = await fetchAppBotIdentity(args.slug, { token: minted.token }, deps);
201254
- if (bot) {
201255
- await put3("GITHUB_APP_BOT_EMAIL", bot.email, false);
201256
- await put3("GITHUB_APP_BOT_NAME", bot.login, false);
201257
- }
201366
+ await put3("GITHUB_APP_BOT_EMAIL", bot?.email ?? "", false);
201367
+ await put3("GITHUB_APP_BOT_NAME", bot?.login ?? `${args.slug}[bot]`, false);
201258
201368
  }
201259
201369
  return {
201260
201370
  account: args.slug ? `${args.slug}[bot]` : args.appId,
@@ -201922,7 +202032,7 @@ async function draftActorResolver(registry2) {
201922
202032
  const byId = new Map(actors.map((a) => [a.id, a]));
201923
202033
  return (id) => {
201924
202034
  const a = byId.get(id);
201925
- return a ? actorRefFieldsOf(a) : systemActorFallback(id);
202035
+ return a ? actorRefFieldsOf(a) : actorRefFallbackOf(id);
201926
202036
  };
201927
202037
  }
201928
202038
  async function draftRoleStaffingResolver(registry2) {
@@ -203154,18 +203264,27 @@ async function runView(kernel, oplog, engineStore, blobs, name, artifactId, para
203154
203264
  );
203155
203265
  const pendingGate = pendingConcludeAttempt(model, id);
203156
203266
  const rejectionByRevision = /* @__PURE__ */ new Map();
203267
+ const contentEditedByRevision = /* @__PURE__ */ new Map();
203157
203268
  if (engineStore) {
203158
203269
  const snap = await engineStore.transaction((tx) => tx.loadWorkorder(artifact.workspace));
203159
203270
  for (const w2 of snap?.works ?? []) {
203160
203271
  if (w2.rejectedReason) rejectionByRevision.set(w2.id, w2.rejectedReason);
203161
203272
  }
203273
+ for (const a of snap?.artifacts ?? []) {
203274
+ const at = a.contentEditedAt;
203275
+ if (!at) continue;
203276
+ const workId = a.workId;
203277
+ const prev = contentEditedByRevision.get(workId);
203278
+ if (!prev || at > prev) contentEditedByRevision.set(workId, at);
203279
+ }
203162
203280
  }
203163
203281
  const revisionViews = revisions.map((rev) => ({
203164
203282
  ...rev,
203165
203283
  stage: revisionStage(artifact, rev),
203166
203284
  approvals: (model.reviews.get(rev.id) ?? []).filter((v2) => v2.verdict === "approve").length,
203167
203285
  required: pendingGate?.head === rev.id ? pendingGate.gate?.quorum ?? 0 : 0,
203168
- ...rejectionByRevision.has(rev.id) ? { rejection: { reason: rejectionByRevision.get(rev.id) } } : {}
203286
+ ...rejectionByRevision.has(rev.id) ? { rejection: { reason: rejectionByRevision.get(rev.id) } } : {},
203287
+ ...contentEditedByRevision.has(rev.id) ? { contentEditedAt: contentEditedByRevision.get(rev.id) } : {}
203169
203288
  }));
203170
203289
  return {
203171
203290
  artifact,
@@ -206250,10 +206369,26 @@ ${composed}`;
206250
206369
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
206251
206370
  ...chatExit?.detail ? { detail: chatExit.detail } : {}
206252
206371
  });
206253
- itemLedger.recordError(errText2, {
206372
+ const errorOutcome = itemLedger.recordError(errText2, {
206254
206373
  ...chatExit?.reason ? { reason: chatExit.reason } : {},
206255
206374
  ...chatExit?.detail ? { detail: chatExit.detail } : {}
206256
206375
  });
206376
+ if (live) {
206377
+ live.publishServerItem({
206378
+ itemId: errorOutcome.itemId,
206379
+ itemType: "control",
206380
+ startedVersion: errorOutcome.startedVersion,
206381
+ version: errorOutcome.version,
206382
+ ord: errorOutcome.ord,
206383
+ status: "failed",
206384
+ payload: {
206385
+ code: "error",
206386
+ note: errText2,
206387
+ ...chatExit?.reason ? { reason: chatExit.reason } : {},
206388
+ ...chatExit?.detail ? { detail: chatExit.detail } : {}
206389
+ }
206390
+ });
206391
+ }
206257
206392
  }
206258
206393
  finished = true;
206259
206394
  clearInterval(ckptTimer);
@@ -206866,7 +207001,19 @@ ${composed}`;
206866
207001
  fromInstallation: Boolean(installationId),
206867
207002
  /** App 申请了、但这次安装还没被授予——多半是权限升级还等着组织 owner 批准。 */
206868
207003
  awaitingApproval: missingAppPermissions(effective).filter((m2) => meta.permissions[m2.key]).map((m2) => m2.key),
206869
- granted: GITHUB_APP_PERMISSION_LABELS.filter((r) => effective[r.key]),
207004
+ /**
207005
+ * 这次安装**真正被授予**的全部权限,一律用 GitHub 的英文原名 + 级别
207006
+ * (2026-09-10 发起人:「所有权限都用英文原文,不区分是不是八项权限」)。
207007
+ *
207008
+ * 此前这里是 `GITHUB_APP_PERMISSION_LABELS.filter(...)` —— 那张表只有本仓必需的
207009
+ * 八项中文标签,于是这一格实际上是「必备清单打勾」,人在 GitHub 上另外勾的一个都不显示,
207010
+ * 看上去像没生效。中间还试过「八项中文 + 额外项英文」两段式,也被否了:
207011
+ * 同一个列表两套命名,反而要人先分辨「这条为什么是中文那条为什么是英文」。
207012
+ *
207013
+ * **不编中文名**:GitHub 的权限键是开放集合且还在增,写死对照表迟早漏;
207014
+ * 而英文原名与 GitHub 设置页上的字**逐字一致**,对着改权限时不用做心理翻译。
207015
+ */
207016
+ granted: Object.entries(effective).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key)),
206870
207017
  missing,
206871
207018
  settingsUrl: meta.slug ? appPermissionSettingsUrl(meta) : null
206872
207019
  }));
@@ -206942,7 +207089,8 @@ ${composed}`;
206942
207089
  const perms = pending.permissions ?? {};
206943
207090
  const missing = missingAppPermissions(perms);
206944
207091
  return {
206945
- granted: GITHUB_APP_PERMISSION_LABELS.filter((r) => perms[r.key]),
207092
+ // app/permissions 那一处同口径:英文原名 + 级别,不分是不是本仓必需的八项。
207093
+ granted: Object.entries(perms).filter(([, level]) => Boolean(level)).map(([key, level]) => ({ key, level: String(level) })).sort((a, b2) => a.key.localeCompare(b2.key)),
206946
207094
  missing,
206947
207095
  ...pending.slug ? { settingsUrl: appPermissionSettingsUrl({
206948
207096
  slug: pending.slug,
@@ -206985,10 +207133,8 @@ ${composed}`;
206985
207133
  }
206986
207134
  if (pending.slug) {
206987
207135
  const bot = await fetchAppBotIdentity(pending.slug, { token: minted.token });
206988
- if (bot) {
206989
- await put3("GITHUB_APP_BOT_EMAIL", bot.email, false);
206990
- await put3("GITHUB_APP_BOT_NAME", bot.login, false);
206991
- }
207136
+ await put3("GITHUB_APP_BOT_EMAIL", bot?.email ?? "", false);
207137
+ await put3("GITHUB_APP_BOT_NAME", bot?.login ?? `${pending.slug}[bot]`, false);
206992
207138
  }
206993
207139
  pending.done = true;
206994
207140
  delete pending.privateKeyPem;
@@ -210642,6 +210788,10 @@ function buildRevisionDag(revisions) {
210642
210788
  }))
210643
210789
  };
210644
210790
  }
210791
+ function laterOf(a, b2) {
210792
+ if (!b2) return a;
210793
+ return b2 > a ? b2 : a;
210794
+ }
210645
210795
  var DEFAULT_AGENT_ACTOR, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
210646
210796
  var init_service4 = __esm({
210647
210797
  "../server/src/domains/projects/service.ts"() {
@@ -211498,7 +211648,11 @@ var init_service4 = __esm({
211498
211648
  if (arts.length === 0) continue;
211499
211649
  const work = worksById.get(acceptedId);
211500
211650
  const folderPath = `wo:${workOrderId}/node:${node2.id}`;
211501
- const folderUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211651
+ const workUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211652
+ const folderUpdatedAt = arts.reduce(
211653
+ (acc, a) => laterOf(acc, a.contentEditedAt),
211654
+ workUpdatedAt
211655
+ );
211502
211656
  const creatorId = work?.assigneeActorId ?? null;
211503
211657
  const creatorKind = creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null;
211504
211658
  const creatorName = creatorId ? this.resolveActorName(creatorId) : null;
@@ -211522,7 +211676,7 @@ var init_service4 = __esm({
211522
211676
  for (const a of sortedArts) {
211523
211677
  const artifactId = node2.id;
211524
211678
  const artifactType = a.type ?? node2.type;
211525
- const rowUpdatedAt = folderUpdatedAt;
211679
+ const rowUpdatedAt = laterOf(workUpdatedAt, a.contentEditedAt);
211526
211680
  if (a.contentKind === "manifest" && a.contentRef?.trim()) {
211527
211681
  const content3 = await this.readContentRefText(a.contentRef);
211528
211682
  const files = content3 !== null ? tryParseManifestFiles(content3) : void 0;
@@ -211918,6 +212072,7 @@ var init_service4 = __esm({
211918
212072
  const arts = snap.artifacts.filter((a) => a.workId === acceptedId);
211919
212073
  if (arts.length === 0) continue;
211920
212074
  const work = worksById.get(acceptedId);
212075
+ const workUpdatedAt = work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt;
211921
212076
  const documents = arts.slice().sort((a, b2) => a.ordinal - b2.ordinal).map((a) => ({
211922
212077
  // collab 层 artifactId === engine nodeId(见 bridge-readmodel.ts:215)——预览端读的
211923
212078
  // useArtifactView 也走这个 id。
@@ -211929,7 +212084,9 @@ var init_service4 = __esm({
211929
212084
  contentRef: a.contentRef,
211930
212085
  title: node2.title || a.name,
211931
212086
  // 节点标题优先;否则用 engine artifact 的 "output" 兜底
211932
- contentKind: engineContentKind(a.contentKind)
212087
+ contentKind: engineContentKind(a.contentKind),
212088
+ // 文档级时间:被人在预览里就地改写过就跳到改写时刻,否则就是这轮执行的时刻。
212089
+ updatedAt: laterOf(workUpdatedAt, a.contentEditedAt)
211933
212090
  }));
211934
212091
  out.push({
211935
212092
  outputId: `engine:${acceptedId}`,
@@ -211940,7 +212097,9 @@ var init_service4 = __esm({
211940
212097
  documents,
211941
212098
  payload: {},
211942
212099
  createdAt: work?.createdAt ?? node2.createdAt,
211943
- updatedAt: work?.acceptedAt ?? work?.endedAt ?? work?.lastActivityAt ?? work?.createdAt ?? node2.updatedAt
212100
+ // 节点(= 文件夹行)的时间取「这轮执行的时刻」与「其下任一文档最后被就地改写的时刻」里更晚的那个:
212101
+ // 改过某个文件之后,文件夹不能还显示比自己的孩子更早的时间。
212102
+ updatedAt: documents.reduce((acc, d) => laterOf(acc, d.updatedAt), workUpdatedAt)
211944
212103
  });
211945
212104
  }
211946
212105
  return out;
@@ -223012,7 +223171,8 @@ function toApiNodeOutputDocument(doc) {
223012
223171
  summary: doc.summary,
223013
223172
  ...doc.contentKind !== void 0 ? { content_kind: doc.contentKind } : {},
223014
223173
  ...doc.size !== void 0 ? { size: doc.size } : {},
223015
- ...doc.files !== void 0 ? { files: doc.files.map((f2) => ({ path: f2.path, ...f2.size !== void 0 ? { size: f2.size } : {} })) } : {}
223174
+ ...doc.files !== void 0 ? { files: doc.files.map((f2) => ({ path: f2.path, ...f2.size !== void 0 ? { size: f2.size } : {} })) } : {},
223175
+ ...doc.updatedAt !== void 0 ? { updated_at: doc.updatedAt } : {}
223016
223176
  };
223017
223177
  }
223018
223178
  function toApiNodeOutputValidation(validation) {
@@ -229369,7 +229529,7 @@ async function buildResolver(registry2) {
229369
229529
  const byId = new Map(actors.map((a) => [a.id, a]));
229370
229530
  return (id) => {
229371
229531
  const a = byId.get(id);
229372
- return a ? actorRefFieldsOf(a) : systemActorFallback(id);
229532
+ return a ? actorRefFieldsOf(a) : actorRefFallbackOf(id);
229373
229533
  };
229374
229534
  }
229375
229535
  async function buildProjectResolver(artifacts) {
@@ -233772,12 +233932,20 @@ function createChatSessionsDomain(opts) {
233772
233932
  const limitRaw = Number(req.query?.get("limit") ?? "0") || 0;
233773
233933
  const limit = limitRaw > 0 ? Math.min(limitRaw, MAX_SNAPSHOT_ITEMS) : DEFAULT_SNAPSHOT_ITEMS;
233774
233934
  const cursor = opts.liveChat?.cursorFor(chatSessionId) ?? null;
233775
- let turnId = cursor?.turnId ?? null;
233776
- if (!turnId) {
233777
- const turns = await turnsFor(req);
233778
- const active = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
233779
- turnId = active?.id ?? null;
233780
- }
233935
+ const turns = await turnsFor(req);
233936
+ const activeTurnRow = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
233937
+ const turnId = cursor?.turnId ?? activeTurnRow?.id ?? null;
233938
+ const activeTurn = (() => {
233939
+ if (!activeTurnRow) return null;
233940
+ const settled = activeTurnRow.status !== "reserved" && activeTurnRow.status !== "running";
233941
+ if (settled) return { turnId: activeTurnRow.id, phase: activeTurnRow.status };
233942
+ if (opts.liveChat?.isRunning(chatSessionId)) {
233943
+ return { turnId: activeTurnRow.id, phase: "running" };
233944
+ }
233945
+ const startedAt = Date.parse(activeTurnRow.startedAt ?? activeTurnRow.reservedAt);
233946
+ const stale = Number.isFinite(startedAt) && Date.now() - startedAt >= LINK_WINDOWS.chatTurnPhaseStaleAfterMs;
233947
+ return { turnId: activeTurnRow.id, phase: stale ? "interrupted" : "running" };
233948
+ })();
233781
233949
  const snapshotEntries = [];
233782
233950
  let nextSinceVersion = sinceVersion;
233783
233951
  let hasMoreBefore = false;
@@ -233809,6 +233977,7 @@ function createChatSessionsDomain(opts) {
233809
233977
  nextSinceVersion,
233810
233978
  frameCursor: cursor ? { epoch: cursor.epoch, seq: cursor.seq } : { epoch: 0, seq: 0 },
233811
233979
  canAppend: cursor?.canAppend ?? false,
233980
+ activeTurn,
233812
233981
  oldestOrd,
233813
233982
  hasMoreBefore
233814
233983
  };
@@ -270671,7 +270840,7 @@ function shimScript() {
270671
270840
  }
270672
270841
 
270673
270842
  // src/index.ts
270674
- var PKG_VERSION = true ? "2.2.9" : "dev";
270843
+ var PKG_VERSION = true ? "2.2.10" : "dev";
270675
270844
  var LOCAL_BIN = localBin();
270676
270845
  var NPM_PREFIX = npmPrefix();
270677
270846
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
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": "c340be41660585edc13bbb809b8585d4b6b4e95a"
29
+ "sourceHead": "3eac5f26307c5d0cf750e4a7edc6a5f60bdbe63d"
30
30
  }
31
31
  }