oasis_test_v2 2.2.0 → 2.2.2

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 +696 -200
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -11107,13 +11107,22 @@ var init_store_postgres = __esm({
11107
11107
  async close() {
11108
11108
  await this.pool.end();
11109
11109
  }
11110
+ /**
11111
+ * ★ 与 {@link tx} 同一条纪律:schema 名进的是**标识符位**,必须 {@link quoteIdent}。
11112
+ *
11113
+ * 这两个直连 pool 的方法(本方法与 {@link listNeverStartedWorks})是上一轮修连字符 schema 时
11114
+ * 漏掉的两处——`tx()` 那条路已经引号化了,它们没有。表现不是「一读就 500」而是**两口钟在带
11115
+ * 连字符的公司上永远转不动**:2026-09-09 v2 生产日志里 `[deadline-clock] [company:test-ryzs]`
11116
+ * 与 `[dispatch-drop]` 每 5 秒 / 每分钟各报一次 `syntax error at or near "-"`,
11117
+ * 一分钟约 50 行;也就是说这些公司的 deadline 最迟触发保证与丢单补派**从来没生效过**。
11118
+ */
11110
11119
  async listOpenWorkDeadlines(opts) {
11111
11120
  const res = await this.pool.query(
11112
11121
  `SELECT workorder_id AS "workorderId",
11113
11122
  id AS "workId",
11114
11123
  assignee_actor_id AS "assigneeActorId",
11115
11124
  COALESCE(last_activity_at, started_at, created_at) AS "deadlineFrom"
11116
- FROM ${this.schema}.works
11125
+ FROM ${quoteIdent(this.schema)}.works
11117
11126
  WHERE ended_at IS NULL AND dead_at IS NULL AND cancelled_at IS NULL
11118
11127
  AND ($1::text IS NULL OR id > $1)
11119
11128
  ORDER BY id
@@ -11134,7 +11143,7 @@ var init_store_postgres = __esm({
11134
11143
  node_id AS "nodeId",
11135
11144
  assignee_actor_id AS "assigneeActorId",
11136
11145
  created_at AS "createdAt"
11137
- FROM ${this.schema}.works
11146
+ FROM ${quoteIdent(this.schema)}.works
11138
11147
  WHERE ended_at IS NULL AND dead_at IS NULL AND cancelled_at IS NULL
11139
11148
  AND started_at IS NULL AND session_ref IS NULL
11140
11149
  AND created_at < $1::timestamptz
@@ -11865,6 +11874,7 @@ function markNodeForRetry(state, nodeId, at) {
11865
11874
  state.updateWork(lw.id, { retryAt: at, status: "retry" });
11866
11875
  }
11867
11876
  if (lw && !lw.deadAt && workState(lw, hasOutput(lw, state.artifactsOf(lw.id).length)) === "success") {
11877
+ if (lw.acceptanceState === "rejected") state.updateWork(lw.id, { retryAt: at });
11868
11878
  for (const req of state.requirementsOf(node2.id)) {
11869
11879
  if (!req.latestReviewId) continue;
11870
11880
  const r = state.review(req.latestReviewId);
@@ -28028,6 +28038,15 @@ var init_chat_item_ledger = __esm({
28028
28038
  this.messageId = deps.messageId ?? null;
28029
28039
  this.telemetryTextInContent = deps.telemetryTextInContent === true;
28030
28040
  this.persistEnabled = Boolean(deps.items) && chatItemsDoubleWriteEnabled();
28041
+ this.clock = Math.max(0, deps.versionSeed ?? 0);
28042
+ if (deps.versionSeed === void 0 && deps.items) {
28043
+ void deps.items.sessionVersionCursor(deps.sessionId).then(
28044
+ (seed) => {
28045
+ if (seed > this.clock) this.clock = seed;
28046
+ },
28047
+ () => void 0
28048
+ );
28049
+ }
28031
28050
  }
28032
28051
  /** 账本轮次 id(`chat_session_turns.id`;未接账本时是 {@link fallbackTurnId})。v3 wire 上 item 帧的 turnId 用它(ADR-0510 D1)。 */
28033
28052
  get turnId() {
@@ -28043,6 +28062,11 @@ var init_chat_item_ledger = __esm({
28043
28062
  /** 内存账本:itemId → 行。插入顺序即 seq 顺序。 */
28044
28063
  rows = /* @__PURE__ */ new Map();
28045
28064
  seqCounter = 0;
28065
+ /**
28066
+ * 会话逻辑时钟(ADR-0510 附录 A)。**一次写走一格**——不描述某条 item 改了第几次,描述
28067
+ * 「这条会话被写了第几次」。于是「一次建行要发两帧」这个死结不存在:想发几帧就取几个号。
28068
+ */
28069
+ clock;
28046
28070
  segment = 0;
28047
28071
  messageId;
28048
28072
  /** 已排队但还没执行的那一次 `append_text`——排队期间到的片段合并进它(见 queueAppend)。 */
@@ -28055,6 +28079,18 @@ var init_chat_item_ledger = __esm({
28055
28079
  finished = false;
28056
28080
  /** 上一次见到的归一层 turnId——变了就清映射(provider 开了新 turn)。 */
28057
28081
  lastTurnId = null;
28082
+ /** 会话逻辑时钟走一格。**唯一发号点**(ADR-0510 附录 A.4)——别在别处自己数。 */
28083
+ tick() {
28084
+ return ++this.clock;
28085
+ }
28086
+ /**
28087
+ * 该 item 的服务端展示序;INSERT 还没返回(或没开双写)时是 undefined。
28088
+ * 调用方(`emitV3`)拿不到就不在帧上带号——**不许自己编一个**。
28089
+ */
28090
+ ordFor(itemId) {
28091
+ const ord = this.rows.get(itemId)?.ord;
28092
+ return ord === null ? void 0 : ord;
28093
+ }
28058
28094
  get failures() {
28059
28095
  return this.failureCount;
28060
28096
  }
@@ -28091,20 +28127,17 @@ var init_chat_item_ledger = __esm({
28091
28127
  if (event.type === "turn") {
28092
28128
  this.itemKeyMap.clear();
28093
28129
  this.lastTurnId = null;
28094
- return;
28130
+ return null;
28095
28131
  }
28096
28132
  if (this.lastTurnId !== null && this.lastTurnId !== event.turnId) {
28097
28133
  this.itemKeyMap.clear();
28098
28134
  }
28099
28135
  this.lastTurnId = event.turnId;
28100
28136
  const existingId = this.itemKeyMap.get(event.providerItemKey);
28101
- if (existingId === void 0) {
28102
- this.createFrom(event);
28103
- return;
28104
- }
28137
+ if (existingId === void 0) return this.createFrom(event);
28105
28138
  const row = this.rows.get(existingId);
28106
- if (!row) return;
28107
- this.applyOpTo(row, event);
28139
+ if (!row) return null;
28140
+ return this.applyOpTo(row, event);
28108
28141
  }
28109
28142
  // ── 非 provider 来源(服务端自己产的行)────────────────────────────────────
28110
28143
  /**
@@ -28299,34 +28332,49 @@ var init_chat_item_ledger = __esm({
28299
28332
  metadata: { [CHAT_ITEM_META_LEGACY_CONTENT]: inContent }
28300
28333
  });
28301
28334
  this.itemKeyMap.set(event.providerItemKey, id);
28302
- if (event.opShape.op === "set_status") this.applyOpTo(this.rows.get(id), event);
28335
+ const row = this.rows.get(id);
28336
+ const outcome = {
28337
+ itemId: id,
28338
+ version: row.wireVersion,
28339
+ startedVersion: row.startedVersion,
28340
+ ord: row.ord
28341
+ };
28342
+ if (event.opShape.op === "set_status") {
28343
+ const followUp = this.applyOpTo(row, event);
28344
+ if (followUp) outcome.version = followUp.version;
28345
+ }
28346
+ return outcome;
28303
28347
  }
28304
28348
  applyOpTo(row, event) {
28305
28349
  const op = event.opShape;
28306
28350
  if (event.attrs) row.attrs = { ...row.attrs, ...event.attrs };
28307
28351
  switch (op.op) {
28308
28352
  case "append_text":
28309
- if (op.text === "") return;
28353
+ if (op.text === "") return null;
28310
28354
  row.text += op.text;
28311
28355
  this.queueAppend(row, op.text);
28312
- return;
28356
+ return this.outcomeOf(row);
28313
28357
  case "set_text":
28314
- if (row.text === op.text) return;
28358
+ if (row.text === op.text) return null;
28315
28359
  row.text = op.text;
28316
28360
  this.sealPendingAppend();
28317
28361
  this.persist(row, row.kind === "text" || row.kind === "thinking" ? { op: "set_text", text: op.text } : { op: "set_payload", payload: this.payloadOf(row) }, "set_text");
28318
- return;
28362
+ return this.outcomeOf(row);
28319
28363
  case "patch_input":
28320
28364
  row.attrs = { ...row.attrs, input: op.input };
28321
28365
  this.persist(row, { op: "patch_input", input: op.input }, "patch_input");
28322
- return;
28366
+ return this.outcomeOf(row);
28323
28367
  case "set_status":
28324
- if (row.status === op.status) return;
28368
+ if (row.status === op.status) return null;
28325
28369
  row.status = op.status;
28326
28370
  this.persist(row, { op: "set_status", status: op.status }, "set_status");
28327
- return;
28371
+ return this.outcomeOf(row);
28328
28372
  }
28329
28373
  }
28374
+ /** 这一次写的回执。`persist` 已经在里面走过钟了,这里只是把读数带出去给 wire。 */
28375
+ outcomeOf(row) {
28376
+ return { itemId: row.id, version: row.wireVersion, ord: row.ord };
28377
+ }
28330
28378
  /** 关掉所有还 streaming 的行(收尾/切段)。 */
28331
28379
  closeStreamingRows(status) {
28332
28380
  this.sealPendingAppend();
@@ -28361,9 +28409,14 @@ var init_chat_item_ledger = __esm({
28361
28409
  }
28362
28410
  insert(input) {
28363
28411
  const id = input.deterministicId ?? this.newId();
28412
+ const startedVersion = this.tick();
28413
+ const writeVersion = this.tick();
28364
28414
  const row = {
28365
28415
  id,
28366
28416
  seq: ++this.seqCounter,
28417
+ ord: null,
28418
+ wireVersion: writeVersion,
28419
+ startedVersion,
28367
28420
  kind: input.kind,
28368
28421
  role: input.role,
28369
28422
  status: input.status,
@@ -28383,7 +28436,7 @@ var init_chat_item_ledger = __esm({
28383
28436
  const payload = input.payload ?? this.payloadOf(row);
28384
28437
  const metadata = input.metadata ?? { [CHAT_ITEM_META_LEGACY_CONTENT]: input.inContent };
28385
28438
  this.enqueue(async () => {
28386
- await this.deps.items.createItem({
28439
+ const created = await this.deps.items.createItem({
28387
28440
  id,
28388
28441
  sessionId: this.deps.sessionId,
28389
28442
  messageId,
@@ -28395,8 +28448,10 @@ var init_chat_item_ledger = __esm({
28395
28448
  payload,
28396
28449
  metadata,
28397
28450
  providerItemKey: input.providerItemKey ?? null,
28398
- createdAt
28451
+ createdAt,
28452
+ version: writeVersion
28399
28453
  });
28454
+ row.ord = created.ord;
28400
28455
  }, `create:${input.kind}`);
28401
28456
  return id;
28402
28457
  }
@@ -28405,6 +28460,7 @@ var init_chat_item_ledger = __esm({
28405
28460
  * 语义没变(`version` 数的是落库次数),投影是拼接,合不合并结果逐字相同。
28406
28461
  */
28407
28462
  queueAppend(row, text5) {
28463
+ row.wireVersion = this.tick();
28408
28464
  if (!row.persist || !this.persistEnabled) return;
28409
28465
  const pending = this.pendingAppend;
28410
28466
  if (pending && pending.id === row.id) {
@@ -28418,7 +28474,11 @@ var init_chat_item_ledger = __esm({
28418
28474
  next.text = "";
28419
28475
  if (this.pendingAppend === next) this.pendingAppend = null;
28420
28476
  if (!merged) return;
28421
- const applied = await this.deps.items.applyOp(row.id, { op: "append_text", text: merged }, { at: this.now() });
28477
+ const applied = await this.deps.items.applyOp(
28478
+ row.id,
28479
+ { op: "append_text", text: merged },
28480
+ { at: this.now(), version: row.wireVersion }
28481
+ );
28422
28482
  if (!applied) throw new Error(`item ${row.id} \u4E0D\u5B58\u5728\uFF08append_text \u843D\u7A7A\uFF09`);
28423
28483
  }, "append_text");
28424
28484
  }
@@ -28427,9 +28487,11 @@ var init_chat_item_ledger = __esm({
28427
28487
  this.pendingAppend = null;
28428
28488
  }
28429
28489
  persist(row, op, what) {
28490
+ const version2 = this.tick();
28491
+ row.wireVersion = version2;
28430
28492
  if (!row.persist || !this.persistEnabled) return;
28431
28493
  this.enqueue(async () => {
28432
- await this.deps.items.applyOp(row.id, op, { at: this.now() });
28494
+ await this.deps.items.applyOp(row.id, op, { at: this.now(), version: version2 });
28433
28495
  }, what);
28434
28496
  }
28435
28497
  /** 串行链 + 失败吞掉计数。**串行是必须的**:append_text 依赖建行已经落地。 */
@@ -159267,13 +159329,15 @@ var init_live_chat = __esm({
159267
159329
  * 账本坏掉不影响对话;v3 emit 抛异常也不影响 v2 通路与账本落盘——两条独立通路。
159268
159330
  */
159269
159331
  applyNormalizedEvent(turn, event) {
159332
+ let outcome = null;
159270
159333
  try {
159271
- turn.items?.apply(event);
159334
+ outcome = turn.items?.apply(event) ?? null;
159272
159335
  } catch {
159273
159336
  }
159274
159337
  if (event.type === "turn") return;
159338
+ if (!outcome) return;
159275
159339
  try {
159276
- this.emitV3(turn, event);
159340
+ this.emitV3(turn, event, outcome);
159277
159341
  } catch {
159278
159342
  }
159279
159343
  }
@@ -159293,18 +159357,17 @@ var init_live_chat = __esm({
159293
159357
  wireTurnId(turn) {
159294
159358
  return turn.items?.turnId ?? turn.liveTurnId;
159295
159359
  }
159296
- emitV3(turn, event) {
159360
+ emitV3(turn, event, outcome) {
159297
159361
  if (!turn.items) return;
159298
- const itemId = turn.items.itemIdForKey(event.providerItemKey);
159299
- if (!itemId) return;
159362
+ const itemId = outcome.itemId;
159300
159363
  const itemType = normalizedKindToItemType(event.kind);
159301
159364
  const operation = normalizedOpToV3Op(event.opShape.op, event.kind);
159302
159365
  if (!operation) return;
159303
- const perItem = turn.v3Items.get(itemId) ?? { version: 0, textOffset: 0, started: false };
159366
+ const perItem = turn.v3Items.get(itemId) ?? { textOffset: 0, started: false };
159304
159367
  const isTextual2 = event.kind === "text" || event.kind === "thinking";
159305
159368
  const opShape = event.opShape;
159306
- if (!perItem.started) {
159307
- perItem.version += 1;
159369
+ const ord = turn.items.ordFor(itemId) ?? outcome.ord ?? void 0;
159370
+ if (!perItem.started && outcome.startedVersion !== void 0) {
159308
159371
  const startedFrame = {
159309
159372
  protocolVersion: 3,
159310
159373
  streamId: turn.liveStreamId,
@@ -159312,7 +159375,8 @@ var init_live_chat = __esm({
159312
159375
  itemId,
159313
159376
  itemType,
159314
159377
  operation: "started",
159315
- itemVersion: perItem.version,
159378
+ itemVersion: outcome.startedVersion,
159379
+ ...ord !== void 0 ? { ord } : {},
159316
159380
  offset: 0,
159317
159381
  payload: v3StartedPayload(event.role, event.attrs, itemType, event.kind)
159318
159382
  };
@@ -159334,7 +159398,6 @@ var init_live_chat = __esm({
159334
159398
  } else if (opShape.op === "set_status") {
159335
159399
  if (opShape.status === "streaming") return;
159336
159400
  }
159337
- perItem.version += 1;
159338
159401
  turn.v3Items.set(itemId, perItem);
159339
159402
  const frame = {
159340
159403
  protocolVersion: 3,
@@ -159343,7 +159406,9 @@ var init_live_chat = __esm({
159343
159406
  itemId,
159344
159407
  itemType,
159345
159408
  operation,
159346
- itemVersion: perItem.version,
159409
+ // 账本这一次写用掉的那一格钟——**库里那一行的 version 就是它**,快照与增量流同一套版本空间。
159410
+ itemVersion: outcome.version,
159411
+ ...ord !== void 0 ? { ord } : {},
159347
159412
  ...offset !== void 0 ? { offset } : {},
159348
159413
  ...payload !== void 0 ? { payload } : {}
159349
159414
  };
@@ -159369,6 +159434,7 @@ var init_live_chat = __esm({
159369
159434
  const payload = pending.frame.payload;
159370
159435
  pending.frame.payload = { ...payload, text: String(payload.text ?? "") + text5 };
159371
159436
  pending.frame.itemVersion = frame.itemVersion;
159437
+ if (frame.ord !== void 0) pending.frame.ord = frame.ord;
159372
159438
  pending.chunks += 1;
159373
159439
  pending.chars += text5.length;
159374
159440
  return;
@@ -160175,7 +160241,7 @@ var init_workorder_drafts = __esm({
160175
160241
  }
160176
160242
  };
160177
160243
  ident = (s2) => {
160178
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160244
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160179
160245
  return s2;
160180
160246
  };
160181
160247
  seqNoOf = (id, fallback) => {
@@ -160433,7 +160499,7 @@ var init_workorder_draft_planner_issues = __esm({
160433
160499
  }
160434
160500
  };
160435
160501
  ident2 = (s2) => {
160436
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160502
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
160437
160503
  return s2;
160438
160504
  };
160439
160505
  PostgresWorkorderDraftPlannerIssuesStore = class _PostgresWorkorderDraftPlannerIssuesStore extends MemoryWorkorderDraftPlannerIssuesStore {
@@ -201362,14 +201428,15 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
201362
201428
  }
201363
201429
  const projectId2 = optStr(args, "project_id") ?? optStr(args, "projectId");
201364
201430
  const schemaForSeed = (createdViaArg ?? "manual-spawn") === "seed" ? ctx.schema : void 0;
201431
+ const bindingState = ctx.workspaceBindingState ?? ctx.artifactState;
201365
201432
  if (projectId2 !== void 0) {
201366
- if (!ctx.projectState || !ctx.artifactState) {
201433
+ if (!ctx.projectState || !bindingState) {
201367
201434
  throw new Error("spawn \u5E26 project_id \u9700\u8981\u670D\u52A1\u7AEF\u6CE8\u5165\u9879\u76EE\u5B58\u50A8\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u5B58\u50A8");
201368
201435
  }
201369
201436
  const project = await ctx.projectState.getProject(projectId2);
201370
201437
  if (!project) throw new Error(`project not found: ${projectId2}`);
201371
- await ctx.artifactState.upsertWorkspaceBinding({ workOrderId: workspace, projectId: projectId2 });
201372
- } else if (createdViaArg === "seed" && ctx.projectState && ctx.artifactState) {
201438
+ await bindingState.upsertWorkspaceBinding({ workOrderId: workspace, projectId: projectId2 });
201439
+ } else if (createdViaArg === "seed" && ctx.projectState && bindingState) {
201373
201440
  await ensureWorkorderProject({
201374
201441
  workOrderId: workspace,
201375
201442
  ...optStr(args, "title") ? { title: optStr(args, "title") } : {},
@@ -201380,7 +201447,7 @@ async function runCommand(kernel, blobs, oplog, engineStore, actor, command, arg
201380
201447
  // 由读侧从工单 brief.owner 派生(service.resolveProjectCreator)。
201381
201448
  ...ctx.workorderInitiator?.humanActorId ? { createdBy: ctx.workorderInitiator.humanActorId } : !isAgent(actor) ? { createdBy: actor } : {},
201382
201449
  createProject: makeEnsureProjectFromStore(ctx.projectState),
201383
- upsertBinding: (b2) => ctx.artifactState.upsertWorkspaceBinding(b2),
201450
+ upsertBinding: (b2) => bindingState.upsertWorkspaceBinding(b2),
201384
201451
  onWarn: (m2) => console.warn(m2)
201385
201452
  });
201386
201453
  }
@@ -203175,6 +203242,7 @@ async function startOasisServer(opts) {
203175
203242
  }
203176
203243
  };
203177
203244
  const artifactStateNow = async () => opts.artifactStateFor ? await opts.artifactStateFor(currentCompanyId) : opts.artifactState;
203245
+ const projectStateNow = async () => opts.projectStateFor ? await opts.projectStateFor(currentCompanyId) : opts.projectState;
203178
203246
  if (url.pathname.startsWith("/api/channels")) {
203179
203247
  const json = (status, body2) => {
203180
203248
  res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body2));
@@ -203273,6 +203341,10 @@ async function startOasisServer(opts) {
203273
203341
  }
203274
203342
  }
203275
203343
  const chatAnchorMessageId = await anchorMessageIdFor(chatOrigin, chatStoreNowOrNull);
203344
+ const spawnArgs = body2.args;
203345
+ const needsProject = body2.command === "spawn" && (spawnArgs?.["createdVia"] === "seed" || spawnArgs?.["project_id"] !== void 0 || spawnArgs?.["projectId"] !== void 0);
203346
+ const commandProjects = needsProject ? await projectStateNow() : opts.projectState;
203347
+ const commandBindings = needsProject && commandProjects ? await artifactStateNow() : void 0;
203276
203348
  const result = await runCommand(
203277
203349
  engine.kernel,
203278
203350
  engine.blobs,
@@ -203291,13 +203363,14 @@ async function startOasisServer(opts) {
203291
203363
  ...chatOrigin !== void 0 ? { chatOrigin } : {},
203292
203364
  ...chatAnchorMessageId !== void 0 ? { chatAnchorMessageId } : {},
203293
203365
  ...sessionEscalationId !== void 0 ? { sessionEscalationId } : {},
203294
- ...opts.projectState ? { projectState: opts.projectState } : {},
203366
+ ...commandProjects ? { projectState: commandProjects } : {},
203367
+ ...commandBindings ? { workspaceBindingState: commandBindings } : {},
203295
203368
  ...opts.artifactState ? { artifactState: opts.artifactState } : {},
203296
203369
  ...opts.schema ? { schema: opts.schema } : {},
203297
203370
  ...engine.registry ? { registry: engine.registry } : {},
203298
203371
  ...opts.resolveAssignableRoles ? { resolveAssignableRoles: opts.resolveAssignableRoles } : {},
203299
203372
  ...opts.planWorkorderWithAgent ? { planWorkorderWithAgent: opts.planWorkorderWithAgent } : {},
203300
- ...opts.resolveCodeRepo ? { resolveCodeRepo: opts.resolveCodeRepo } : {},
203373
+ ...opts.resolveCodeRepo ? { resolveCodeRepo: (ws) => opts.resolveCodeRepo(ws, currentCompanyId) } : {},
203301
203374
  ...opts.dispatchProduce ? { dispatchProduce: opts.dispatchProduce } : {},
203302
203375
  ...wodrafts ? { workorderDrafts: wodrafts } : {},
203303
203376
  workorderDraftPlannerIssues: wodraftPlannerIssues,
@@ -203585,6 +203658,7 @@ async function startOasisServer(opts) {
203585
203658
  }
203586
203659
  const resuming = resumeState.hasNodes;
203587
203660
  let sessionProjectId;
203661
+ let sessionBindingState;
203588
203662
  if (body2.chatSessionId && opts.chatSession) {
203589
203663
  const applyChatStore = await chatStoreNow();
203590
203664
  const session = await applyChatStore.getSession(body2.chatSessionId);
@@ -203593,10 +203667,12 @@ async function startOasisServer(opts) {
203593
203667
  }
203594
203668
  sessionProjectId = session.projectId ?? void 0;
203595
203669
  if (sessionProjectId) {
203596
- if (!opts.projectState || !opts.artifactState) {
203670
+ const projects = await projectStateNow();
203671
+ sessionBindingState = await artifactStateNow();
203672
+ if (!projects || !sessionBindingState) {
203597
203673
  throw new Error("chat session \u5E26 projectId \u65F6\u9700\u8981\u670D\u52A1\u7AEF\u6CE8\u5165\u9879\u76EE\u5B58\u50A8\u4E0E\u5DE5\u5355\u7ED1\u5B9A\u5B58\u50A8");
203598
203674
  }
203599
- const project = await opts.projectState.getProject(sessionProjectId);
203675
+ const project = await projects.getProject(sessionProjectId);
203600
203676
  if (!project) throw new Error(`project not found: ${sessionProjectId}`);
203601
203677
  }
203602
203678
  }
@@ -203646,7 +203722,7 @@ async function startOasisServer(opts) {
203646
203722
  return;
203647
203723
  }
203648
203724
  if (targetWs && sessionProjectId) {
203649
- await opts.artifactState.upsertWorkspaceBinding({ workOrderId: targetWs, projectId: sessionProjectId });
203725
+ await sessionBindingState.upsertWorkspaceBinding({ workOrderId: targetWs, projectId: sessionProjectId });
203650
203726
  }
203651
203727
  const result = resuming ? { interventionId: "", applied: 0, resumed: true } : await engine.kernel.applyIntervention(body2.plan, body2.baseSeqs, actor);
203652
203728
  if (resuming) {
@@ -204829,8 +204905,10 @@ ${composed}`;
204829
204905
  });
204830
204906
  }
204831
204907
  const itemStore = persistTarget ? opts.resolveChatItems ? await opts.resolveChatItems(currentCompanyId).catch(() => void 0) : opts.chatItems : void 0;
204908
+ const versionSeed = itemStore ? await itemStore.sessionVersionCursor(persistTarget?.id ?? session.id).catch(() => 0) : 0;
204832
204909
  const itemLedger = new ChatItemLedger({
204833
204910
  ...itemStore ? { items: itemStore } : {},
204911
+ versionSeed,
204834
204912
  sessionId: persistTarget?.id ?? session.id,
204835
204913
  // 优先账本 id(`chat_session_turns.id`,跨进程唯一、重启不失忆);账本没接入才兜底。
204836
204914
  turnId: heldTurn?.id ?? fallbackTurnId(session.runId),
@@ -206701,7 +206779,9 @@ var init_memory_registry_store = __esm({
206701
206779
  return [...this.runtimeModelOverrides.values()].filter((o) => o.runtimeId === runtimeId).map((o) => ({ ...o }));
206702
206780
  }
206703
206781
  async upsertConnector(c) {
206704
- this.connectors.set(c.id, { ...c });
206782
+ const prev = this.connectors.get(c.id);
206783
+ const keptBy = c.connectedBy ?? prev?.connectedBy;
206784
+ this.connectors.set(c.id, { ...c, ...keptBy ? { connectedBy: keptBy } : {} });
206705
206785
  }
206706
206786
  async listConnectors() {
206707
206787
  return [...this.connectors.values()].map((c) => ({ ...c }));
@@ -206760,7 +206840,12 @@ var init_memory_registry_store = __esm({
206760
206840
  async putVariable(v2) {
206761
206841
  const mk = this._varKey(v2.key, v2.actorId, v2.projectId);
206762
206842
  const ex = this.variables.get(mk);
206763
- this.variables.set(mk, { ...v2, ...ex?.lastUsedAt !== void 0 ? { lastUsedAt: ex.lastUsedAt } : {} });
206843
+ const keptName = v2.name ?? ex?.name;
206844
+ this.variables.set(mk, {
206845
+ ...v2,
206846
+ ...ex?.lastUsedAt !== void 0 ? { lastUsedAt: ex.lastUsedAt } : {},
206847
+ ...keptName ? { name: keptName } : {}
206848
+ });
206764
206849
  }
206765
206850
  async touchVariableUsage(key, actorId, projectId2, when) {
206766
206851
  const mk = this._varKey(key, actorId, projectId2);
@@ -209261,6 +209346,9 @@ function foldSingleNodeTasks(items) {
209261
209346
  }
209262
209347
  return folded;
209263
209348
  }
209349
+ function engineContentKind(k2) {
209350
+ return k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
209351
+ }
209264
209352
  function namedError(name, message) {
209265
209353
  const err = new Error(message);
209266
209354
  err.name = name;
@@ -209482,6 +209570,13 @@ var init_service4 = __esm({
209482
209570
  async listProjects() {
209483
209571
  return [...this.projects.values()].map((project) => ({ ...project }));
209484
209572
  }
209573
+ /** 与 Postgres 同语义:主记录 + 成员 + 上传文件 + 内容位置一并清掉;不存在的 id 幂等返回。 */
209574
+ async deleteProject(id) {
209575
+ this.projects.delete(id);
209576
+ this.members.delete(id);
209577
+ for (const [fileId, f2] of this.files) if (f2.projectId === id) this.files.delete(fileId);
209578
+ for (const [locId, l] of this.contentLocations) if (l.projectId === id) this.contentLocations.delete(locId);
209579
+ }
209485
209580
  async replaceProjectMembers(projectId2, members) {
209486
209581
  this.members.set(projectId2, new Map(members.map((member) => [member.actorId, cloneProjectMember(member)])));
209487
209582
  }
@@ -209627,6 +209722,7 @@ var init_service4 = __esm({
209627
209722
  }
209628
209723
  projectCreatedHandler;
209629
209724
  onProjectNameChanged;
209725
+ onProjectDeleted;
209630
209726
  /**
209631
209727
  * Cross-domain follow-up for project-scoped resources. Project persistence stays authoritative:
209632
209728
  * a failed follow-up is reported but never turns an already-created project into a false 500.
@@ -209641,6 +209737,10 @@ var init_service4 = __esm({
209641
209737
  setOnProjectNameChanged(handler) {
209642
209738
  this.onProjectNameChanged = handler;
209643
209739
  }
209740
+ /** 删项目后的跨域清理钩子(会话解绑 + 项目变量清理)。装配层注入;缺省 = 无副作用。 */
209741
+ setOnProjectDeleted(handler) {
209742
+ this.onProjectDeleted = handler;
209743
+ }
209644
209744
  /** 工单→项目绑定查询(content-location add 的项目匹配守卫用)。 */
209645
209745
  async getWorkspaceBinding(workOrderId) {
209646
209746
  return this.artifacts.getWorkspaceBinding(workOrderId);
@@ -210029,6 +210129,60 @@ var init_service4 = __esm({
210029
210129
  await this.artifacts.upsertWorkspaceBinding(binding);
210030
210130
  return binding;
210031
210131
  }
210132
+ /**
210133
+ * 删除项目(帧 `444:1792` 三点菜单第二项,口径由发起人 2026-09-09 拍板)。
210134
+ *
210135
+ * **只删项目这个壳**——项目主记录 + 成员名单 + 上传文件记录 + 内容位置。任务和会话一条都不删:
210136
+ *
210137
+ * 1. 绑到本项目的工单**不管在跑还是没在跑**,一律 `unlinkWorkorder` 回落到自己的
210138
+ * `proj_tmp_<slug>`,也就是读侧的「无分类项目」。发起人明确不要「有任务在跑就不让删」
210139
+ * 这道闸,所以这里没有任何在跑判断。
210140
+ * 2. 搬工单的同时**把本项目的 git 类内容位置复制一份到每个工单的临时项目上**。
210141
+ * 这条不是可选的:`git/resolve.ts` 的 `resolveCodeRepo` 是**派发时**沿
210142
+ * `binding → project → listContentLocations` 实时解的,不带过去的话,正在跑的任务
210143
+ * 下一个节点就取不到代码仓——正是 `ephemeral-project.ts` 顶部那条五环事故链。
210144
+ * 3. 会话的 `project_id` 置空、项目作用域的变量/密钥清理,走 {@link OnProjectDeleted} 钩子
210145
+ * (本 service 对会话域与凭据域零耦合,同 `onProjectNameChanged` 的注入模式)。
210146
+ *
210147
+ * 拒绝两类 id:`proj:uncategorized`(读侧虚拟卡,存储里没有真行)与 `proj_tmp_*`
210148
+ * (工单的兜底归属,删掉等于把工单变成无绑定态——契约里 `Workspace.projectId` 必填)。
210149
+ */
210150
+ async deleteProject(id) {
210151
+ const projectId2 = id?.trim();
210152
+ if (!projectId2) throw new Error("project id is required");
210153
+ if (isUncategorizedProjectId(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
210154
+ if (isEphemeralProject(projectId2)) throw new Error(`project not deletable: ${projectId2}`);
210155
+ const project = await this.projects.getProject(projectId2);
210156
+ if (!project) throw new Error(`project not found: ${projectId2}`);
210157
+ const gitLocations = (await this.projects.listContentLocations(projectId2)).filter((loc) => loc.kind === "external-home" && loc.externalHome?.system === "git");
210158
+ const now = (/* @__PURE__ */ new Date()).toISOString();
210159
+ const bindings = await this.artifacts.listWorkspaceBindings();
210160
+ const movedWorkOrderIds = [];
210161
+ for (const binding of bindings) {
210162
+ if (binding.projectId !== projectId2) continue;
210163
+ const moved = await this.unlinkWorkorder(binding.workOrderId);
210164
+ movedWorkOrderIds.push(binding.workOrderId);
210165
+ for (const loc of gitLocations) {
210166
+ await this.projects.addContentLocation({
210167
+ ...loc,
210168
+ projectId: moved.projectId,
210169
+ // 派生 id:同一条位置搬到同一个临时项目永远算出同一个 id(addContentLocation 是
210170
+ // upsert),重跑不会堆出重复的仓库行。
210171
+ locationId: `pcl-${moved.projectId}-carried-${loc.locationId}`,
210172
+ updatedAt: now
210173
+ });
210174
+ }
210175
+ }
210176
+ await this.projects.deleteProject(projectId2);
210177
+ if (this.onProjectDeleted) {
210178
+ try {
210179
+ await this.onProjectDeleted({ projectId: projectId2, projectName: project.name });
210180
+ } catch (error2) {
210181
+ console.warn(`[projects] project-deleted follow-up failed for ${projectId2}: ${error2 instanceof Error ? error2.message : String(error2)}`);
210182
+ }
210183
+ }
210184
+ return { projectId: projectId2, movedWorkOrderIds, carriedGitLocationCount: gitLocations.length };
210185
+ }
210032
210186
  /**
210033
210187
  * 项目关联工单(帧 `1355:4918`):把内部私有的 `listProjectWorkorders` 暴露给路由层。
210034
210188
  * `projectId="proj:uncategorized"` 时聚合所有 `proj_tmp_*` 绑定的工单。
@@ -210138,6 +210292,9 @@ var init_service4 = __esm({
210138
210292
  size: file?.size ?? null,
210139
210293
  taskId,
210140
210294
  taskTitle,
210295
+ // 内容形态/引用只挂在**整份产物**那一行;装箱单摊出来的成员行有自己的后缀,
210296
+ // 按后缀走图标即可,挂上 manifest 反而会把 `Dockerfile` 这种无后缀成员画成压缩包。
210297
+ ...file ? {} : { contentKind: revision.contentKind, contentRef: revision.contentRef },
210141
210298
  creatorKind: creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null,
210142
210299
  creatorId,
210143
210300
  creatorName: creatorId ? this.resolveActorName(creatorId) : null,
@@ -210292,6 +210449,8 @@ var init_service4 = __esm({
210292
210449
  depth: 2,
210293
210450
  type: artifactType || "",
210294
210451
  size: null,
210452
+ contentKind: engineContentKind(a.contentKind),
210453
+ ...a.contentRef ? { contentRef: a.contentRef } : {},
210295
210454
  taskId: workOrderId,
210296
210455
  taskTitle,
210297
210456
  creatorKind,
@@ -210643,7 +210802,6 @@ var init_service4 = __esm({
210643
210802
  const worksById = new Map(snap.works.map((w2) => [w2.id, w2]));
210644
210803
  const binding = this.artifacts.getWorkspaceBinding ? await this.artifacts.getWorkspaceBinding(workOrderId).catch(() => null) : null;
210645
210804
  const projectId2 = binding?.projectId ?? ephemeralProjectIdFor(workOrderId);
210646
- const mapContentKind = (k2) => k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
210647
210805
  const out = [];
210648
210806
  for (const node2 of snap.nodes) {
210649
210807
  if (!node2.latestAcceptId) continue;
@@ -210663,7 +210821,7 @@ var init_service4 = __esm({
210663
210821
  contentRef: a.contentRef,
210664
210822
  title: node2.title || a.name,
210665
210823
  // 节点标题优先;否则用 engine artifact 的 "output" 兜底
210666
- contentKind: mapContentKind(a.contentKind)
210824
+ contentKind: engineContentKind(a.contentKind)
210667
210825
  }));
210668
210826
  out.push({
210669
210827
  outputId: `engine:${acceptedId}`,
@@ -217497,6 +217655,20 @@ ${input.description}
217497
217655
  upsertConnector(c) {
217498
217656
  return this.opts.store.upsertConnector(c);
217499
217657
  }
217658
+ /**
217659
+ * 记下「配置人」(子 PRD《组织页》§3.3:连接器详情展示配置人)。
217660
+ *
217661
+ * **只动这一格**:读回整条再原样写回、只覆盖 `connectedBy`,不碰 status / account / verifiedAt
217662
+ * ——那几样是授权流程刚写进去的,调用方(控制台)手上的未必是最新值,整条覆盖会把它们写旧。
217663
+ * 连接器不存在时抛,不悄悄建一条空记录。
217664
+ */
217665
+ async setConnectorConfiguredBy(connectorId, actorId) {
217666
+ const current = (await this.opts.store.listConnectors()).find((c) => c.id === connectorId);
217667
+ if (!current) throw new Error(`connector not found: ${connectorId}`);
217668
+ const next = { ...current, connectedBy: actorId };
217669
+ await this.opts.store.upsertConnector(next);
217670
+ return next;
217671
+ }
217500
217672
  listConnectors() {
217501
217673
  return this.opts.store.listConnectors();
217502
217674
  }
@@ -217702,6 +217874,8 @@ ${input.description}
217702
217874
  }
217703
217875
  await this.opts.store.putVariable({
217704
217876
  key: v2.key,
217877
+ // 展示名(子 PRD《密钥管理》§2)。没传就不传下去——存储层按「不动」处理,不抹既有名字。
217878
+ ...v2.name !== void 0 && v2.name !== "" ? { name: v2.name } : {},
217705
217879
  scope: v2.scope,
217706
217880
  ...v2.actorId !== void 0 ? { actorId: v2.actorId } : {},
217707
217881
  ...v2.projectId !== void 0 ? { projectId: v2.projectId } : {},
@@ -217770,6 +217944,7 @@ ${input.description}
217770
217944
  const isPlain = r.valueEncrypted.startsWith("plain:");
217771
217945
  return {
217772
217946
  key: r.key,
217947
+ ...r.name !== void 0 ? { name: r.name } : {},
217773
217948
  scope: r.scope,
217774
217949
  ...r.actorId !== void 0 ? { actorId: r.actorId } : {},
217775
217950
  ...r.projectId !== void 0 ? { projectId: r.projectId } : {},
@@ -219512,6 +219687,13 @@ function actorsDomain(opts) {
219512
219687
  });
219513
219688
  return { status: 201, body: { ok: true } };
219514
219689
  });
219690
+ router.post("/api/connectors/:id/configured-by", async (req) => {
219691
+ const { service } = await resolveCtx(req.auth.companyId);
219692
+ const conn = await service.setConnectorConfiguredBy(req.params.id, req.auth.actor).catch((e) => {
219693
+ throw new ApiError(404, "NOT_FOUND", e.message);
219694
+ });
219695
+ return { status: 200, body: conn };
219696
+ });
219515
219697
  router.get("/api/actors/:id/connectors", async (req) => {
219516
219698
  const { service } = await resolveCtx(req.auth.companyId);
219517
219699
  return { status: 200, body: await service.actorConnectorState(req.params.id) };
@@ -219590,6 +219772,8 @@ function actorsDomain(opts) {
219590
219772
  key: b2.key,
219591
219773
  value: b2.value,
219592
219774
  scope,
219775
+ // 展示名(子 PRD《密钥管理》§2「名称」);不传 = 不改既有名字。
219776
+ ...typeof b2.name === "string" && b2.name.trim() ? { name: b2.name.trim() } : {},
219593
219777
  ...scope === "project" ? { projectId: b2.projectId } : {},
219594
219778
  ...b2.deliveryMode !== void 0 ? { deliveryMode: b2.deliveryMode } : {},
219595
219779
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
@@ -219661,6 +219845,8 @@ function actorsDomain(opts) {
219661
219845
  value: b2.value,
219662
219846
  actorId,
219663
219847
  scope: "personal",
219848
+ // 展示名(同上)。组织页「添加密钥 → 新建」走的就是这条端点。
219849
+ ...typeof b2.name === "string" && b2.name.trim() ? { name: b2.name.trim() } : {},
219664
219850
  ...b2.deliveryMode !== void 0 ? { deliveryMode: b2.deliveryMode } : {},
219665
219851
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
219666
219852
  ...b2.overrides !== void 0 ? { overrides: b2.overrides } : {},
@@ -220717,6 +220903,22 @@ function projectsDomain(opts) {
220717
220903
  throw mapProjectError(err);
220718
220904
  }
220719
220905
  });
220906
+ router.delete("/api/projects/:id", async (req) => {
220907
+ try {
220908
+ const result = await (await svc(req)).deleteProject(req.params.id);
220909
+ return {
220910
+ status: 200,
220911
+ body: {
220912
+ ok: true,
220913
+ project_id: result.projectId,
220914
+ moved_work_order_ids: result.movedWorkOrderIds,
220915
+ carried_git_location_count: result.carriedGitLocationCount
220916
+ }
220917
+ };
220918
+ } catch (err) {
220919
+ throw mapProjectError(err);
220920
+ }
220921
+ });
220720
220922
  router.get("/api/projects/:id/members", async (req) => {
220721
220923
  try {
220722
220924
  const members = await (await svc(req)).listProjectMembers(req.params.id);
@@ -221139,6 +221341,9 @@ function mapProjectError(err) {
221139
221341
  if (err instanceof Error && err.message.startsWith("project not found:")) {
221140
221342
  return new ApiError(404, "NOT_FOUND", err.message);
221141
221343
  }
221344
+ if (err instanceof Error && err.message.startsWith("project not deletable:")) {
221345
+ return new ApiError(400, "PROJECT_NOT_DELETABLE", err.message);
221346
+ }
221142
221347
  if (err instanceof Error && err.message.startsWith("missing artifact revision registration:")) {
221143
221348
  return new ApiError(400, "MISSING_ARTIFACT_REVISION", err.message);
221144
221349
  }
@@ -221446,6 +221651,10 @@ function toApiProjectFilesView(view) {
221446
221651
  file_path: item.filePath ?? null,
221447
221652
  depth: item.depth,
221448
221653
  type: item.type,
221654
+ // 内容形态 + 内容引用:前端名称列给**没有后缀**的行选图标用(正文→文本、git 提交→代码、
221655
+ // 其它外部引用→链接)。交付物整份产物行才有,上传文件与文件夹为 null。
221656
+ content_kind: item.contentKind ?? null,
221657
+ content_ref: item.contentRef ?? null,
221449
221658
  size: item.size,
221450
221659
  task_id: item.taskId,
221451
221660
  task_title: item.taskTitle,
@@ -221501,6 +221710,10 @@ function createProjectsDomain(opts) {
221501
221710
  const handler = opts.onProjectNameChangedFor(companyId);
221502
221711
  if (handler) svc.setOnProjectNameChanged(handler);
221503
221712
  }
221713
+ if (opts.onProjectDeletedFor) {
221714
+ const handler = opts.onProjectDeletedFor(companyId);
221715
+ if (handler) svc.setOnProjectDeleted(handler);
221716
+ }
221504
221717
  return svc;
221505
221718
  };
221506
221719
  const service = build(void 0, void 0);
@@ -222326,8 +222539,8 @@ var init_offboarding = __esm({
222326
222539
  return { ownedAgents, inFlight };
222327
222540
  }
222328
222541
  /**
222329
- * 现在**能不能直接删**这个成员。DELETE 路由先问它:非空即 409 HANDOFF_REQUIRED。
222330
- * 这是「阻断直接移除」的唯一判据,DELETE 与 commit 共用它,不会两处各判一套。
222542
+ * 现在**能不能直接删**这个成员。DELETE commit 共用它,不会两处各判一套。
222543
+ * 交接删掉之后这里只剩两条:不是成员(NOT_FOUND)、公司最后一位 owner(LAST_OWNER)。
222331
222544
  */
222332
222545
  async blockersForRemoval(companyId, accountId) {
222333
222546
  const blockers = [];
@@ -222510,6 +222723,15 @@ function companiesDomain(opts) {
222510
222723
  if (!agentCredentials) throw new ApiError(501, "NOT_IMPLEMENTED", "\u672C\u90E8\u7F72\u672A\u63A5\u5165 agent \u51ED\u8BC1");
222511
222724
  return agentCredentials;
222512
222725
  };
222726
+ const syncActorNameToDisplayName = async (member, by) => {
222727
+ if (!resolveActorsCtx) return;
222728
+ const name = member.displayName?.trim();
222729
+ if (!name) return;
222730
+ const { service: actorsSvc } = await resolveActorsCtx(member.companyId);
222731
+ const existing = await actorsSvc.getActor(member.actorId);
222732
+ if (!existing || existing.name === name) return;
222733
+ await actorsSvc.upsertActor({ ...existing, name }, by);
222734
+ };
222513
222735
  const requireIfMatch = (req) => {
222514
222736
  const raw = req.raw.headers["if-match"];
222515
222737
  const text5 = (typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0)?.replace(/^"|"$/g, "");
@@ -222625,6 +222847,7 @@ function companiesDomain(opts) {
222625
222847
  throw new ApiError(400, "BAD_REQUEST", "displayName \u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
222626
222848
  }
222627
222849
  member = await service.setMemberDisplayName(cid, aid, b2.displayName).catch(mapErr);
222850
+ await syncActorNameToDisplayName(member, req.auth.actor);
222628
222851
  }
222629
222852
  return { status: 200, body: member };
222630
222853
  });
@@ -224675,6 +224898,8 @@ function buildWorkorderActivity(input) {
224675
224898
  const groupKey = (nodeId) => `${nodeId}#${groupIndexOf.get(nodeId) ?? 0}`;
224676
224899
  const reviewCards = /* @__PURE__ */ new Map();
224677
224900
  const issueCards = /* @__PURE__ */ new Map();
224901
+ const workOfIssue = /* @__PURE__ */ new Map();
224902
+ const plainCommentIssueIds = /* @__PURE__ */ new Set();
224678
224903
  const acceptCards = /* @__PURE__ */ new Map();
224679
224904
  const standalone = [];
224680
224905
  const touch = (d, rec) => {
@@ -224732,10 +224957,22 @@ function buildWorkorderActivity(input) {
224732
224957
  const executorId = w2?.assigneeActorId ?? rec.actorId;
224733
224958
  if (isPendingActor(executorId)) break;
224734
224959
  const status = reasonKinds.has("rework") ? ACTIVITY_COPY.reworking(nodeName(nodeId)) : reasonKinds.has("resumed") ? ACTIVITY_COPY.resumed(nameOf(executorId), nodeName(nodeId)) : reasonKinds.has("spec_changed") ? ACTIVITY_COPY.specChanged(nodeName(nodeId)) : reasonKinds.has("issue") ? ACTIVITY_COPY.issueDriven(nodeName(nodeId)) : ACTIVITY_COPY.working(nodeName(nodeId));
224960
+ const drivingIssueId = str4(reasons.find((r) => r.kind === "issue")?.issueId);
224961
+ if (drivingIssueId) {
224962
+ workOfIssue.set(drivingIssueId, workId);
224963
+ const ic = plainCommentIssueIds.has(drivingIssueId) ? issueCards.get(drivingIssueId) : void 0;
224964
+ const commentAuthor = issueById.get(drivingIssueId)?.authorActorId;
224965
+ if (ic && commentAuthor) {
224966
+ touch(ic, rec);
224967
+ ic.executorId = executorId;
224968
+ ic.status = ACTIVITY_COPY.commentHandling(nameOf(executorId), nameOf(commentAuthor));
224969
+ }
224970
+ }
224735
224971
  const reworkReason = str4(reasons.find((r) => r.kind === "rework")?.reason);
224736
224972
  const key = groupKey(nodeId);
224737
224973
  const existingGroup = groupCards.get(key);
224738
224974
  if (existingGroup) {
224975
+ if (!existingGroup.traceWorkIds.includes(workId)) existingGroup.traceWorkIds.push(workId);
224739
224976
  touch(existingGroup, rec);
224740
224977
  existingGroup.executorId = executorId;
224741
224978
  existingGroup.phase = "running";
@@ -224748,6 +224985,7 @@ function buildWorkorderActivity(input) {
224748
224985
  }
224749
224986
  const fresh = {
224750
224987
  id: `work:${workId}`,
224988
+ traceWorkIds: [workId],
224751
224989
  seq: rec.seq,
224752
224990
  at: rec.createdAt,
224753
224991
  updatedAt: rec.createdAt,
@@ -224790,8 +225028,9 @@ function buildWorkorderActivity(input) {
224790
225028
  }
224791
225029
  break;
224792
225030
  }
225031
+ const engineVerdict = workById.get(workId)?.status;
224793
225032
  touch(d, rec);
224794
- if (ev.outcome === "failed") {
225033
+ if (ev.outcome === "failed" && engineVerdict !== "success") {
224795
225034
  d.phase = "stuck";
224796
225035
  d.stuckReason = "failed";
224797
225036
  d.status = ACTIVITY_COPY.failed(nameOf(d.executorId));
@@ -225023,6 +225262,7 @@ function buildWorkorderActivity(input) {
225023
225262
  });
225024
225263
  break;
225025
225264
  }
225265
+ if (!isGap && !isEscalation && !isBackpressure) plainCommentIssueIds.add(issueId);
225026
225266
  const nodeAssignee = nodeId ? nodeById.get(nodeId)?.assigneeActorId : void 0;
225027
225267
  const commentCounterpart = nodeAssignee ?? ownerId;
225028
225268
  const commentTargetId = commentCounterpart && commentCounterpart !== authorId ? commentCounterpart : void 0;
@@ -225111,7 +225351,10 @@ function buildWorkorderActivity(input) {
225111
225351
  d.phase = "done";
225112
225352
  d.actions = [];
225113
225353
  d.executorId = resolverId;
225114
- d.status = ACTIVITY_COPY.commentResolved(nameOf(resolverId), nodeName(d.nodeId));
225354
+ const drivenWorkId = workOfIssue.get(issueId);
225355
+ const producedNewVersion = !!(drivenWorkId && workById.get(drivenWorkId)?.conclusion);
225356
+ const commentAuthorId = issue2?.authorActorId;
225357
+ d.status = producedNewVersion && commentAuthorId ? ACTIVITY_COPY.commentAddressed(nameOf(resolverId), nameOf(commentAuthorId), nodeName(d.nodeId)) : ACTIVITY_COPY.commentResolved(nameOf(resolverId), nodeName(d.nodeId));
225115
225358
  break;
225116
225359
  }
225117
225360
  case "plan.node_retry": {
@@ -225166,12 +225409,37 @@ function buildWorkorderActivity(input) {
225166
225409
  ...issueCards.values(),
225167
225410
  ...acceptCards.values()
225168
225411
  ].sort((a, b2) => a.updatedAt.localeCompare(b2.updatedAt) || a.seq - b2.seq || a.id.localeCompare(b2.id));
225169
- const runIdByTarget = input.runIdByTarget;
225170
- const runIdOfCard = (cardId) => {
225171
- if (!runIdByTarget) return void 0;
225172
- if (cardId.startsWith("work:")) return runIdByTarget.get(`work:${cardId.slice("work:".length)}`);
225173
- if (cardId.startsWith("review:")) return runIdByTarget.get(`review:${cardId.slice("review:".length)}`);
225174
- return void 0;
225412
+ const issueWorks = /* @__PURE__ */ new Map();
225413
+ const reasonIds = /* @__PURE__ */ new Map();
225414
+ for (const rec of input.events) {
225415
+ const ev = rec.event;
225416
+ if (ev.kind === "work.create") reasonIds.set(
225417
+ ev.workId,
225418
+ ev.reasons.filter((r) => r.kind === "issue").map((r) => r.issueId)
225419
+ );
225420
+ }
225421
+ for (const w2 of [...snap?.works ?? []].sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id))) {
225422
+ const replyIssueId = replyIssueIdOf(w2);
225423
+ for (const id of new Set(w2.frozenIssueIds ?? reasonIds.get(w2.id) ?? (replyIssueId ? [replyIssueId] : []))) {
225424
+ const issue2 = issueById.get(id);
225425
+ if (issue2?.kind !== "comment" && issue2?.kind !== "change_request") continue;
225426
+ const ids2 = issueWorks.get(id) ?? [];
225427
+ ids2.push(w2.id);
225428
+ issueWorks.set(id, ids2);
225429
+ }
225430
+ }
225431
+ for (const d of drafts) {
225432
+ if (d.id.startsWith("issue:")) d.traceWorkIds = issueWorks.get(d.id.slice("issue:".length));
225433
+ }
225434
+ const runIdOfCard = (d) => {
225435
+ const map = input.runIdByTarget;
225436
+ if (d.traceWorkIds?.length) {
225437
+ for (const id of [...d.traceWorkIds].reverse()) {
225438
+ const run = map?.get(`work:${id}`);
225439
+ if (run) return run;
225440
+ }
225441
+ }
225442
+ return d.id.startsWith("review:") ? map?.get(d.id) : void 0;
225175
225443
  };
225176
225444
  const cards = drafts.map((d) => ({
225177
225445
  id: d.id,
@@ -225186,7 +225454,8 @@ function buildWorkorderActivity(input) {
225186
225454
  ...d.detail ? { detail: d.detail } : {},
225187
225455
  artifacts: d.artifacts,
225188
225456
  actions: d.actions,
225189
- .../* @__PURE__ */ ((r) => r ? { runId: r } : {})(runIdOfCard(d.id))
225457
+ ...d.traceWorkIds?.length ? { traceWorkIds: d.traceWorkIds, traceAvailable: true } : {},
225458
+ .../* @__PURE__ */ ((r) => r ? { runId: r, traceAvailable: true } : {})(runIdOfCard(d))
225190
225459
  }));
225191
225460
  return { workorderId, cards, truncated };
225192
225461
  }
@@ -225195,6 +225464,7 @@ var init_activity2 = __esm({
225195
225464
  "../server/src/domains/collab/activity.ts"() {
225196
225465
  "use strict";
225197
225466
  init_src();
225467
+ init_src4();
225198
225468
  init_workorder_manager();
225199
225469
  ACTIVITY_EVENT_KINDS = [
225200
225470
  "plan.changed",
@@ -225324,6 +225594,14 @@ var init_activity2 = __esm({
225324
225594
  * 留言 / 回压收口——**同样一句索引**。收口 note 与开口正文同源、同样能长到几千字:
225325
225595
  * 全库实测 `ws:wo-56bba3c1` 那条收口后小状态仍是 **8096 字**。全文去留言线看。
225326
225596
  */
225597
+ /** 有人接手这条留言、开工了——由 `work.create` 的 `reasons` 引用该 issueId 判定。 */
225598
+ commentHandling: (executor, author) => `${executor} \u6B63\u5728\u5904\u7406\u6765\u81EA ${author} \u7684\u7559\u8A00\u3002`,
225599
+ /**
225600
+ * 处理完**并且真出了新一版**。
225601
+ * ⚠ 只在那一轮真有 conclusion 时才说「更新了一版」——resolve 也可能是「看了但不改」,
225602
+ * 那种情况说出了新版就是一句卡面兑现不了的断言(同「未经 X 确认」那一族)。
225603
+ */
225604
+ commentAddressed: (executor, author, node2) => `${executor} \u5DF2\u57FA\u4E8E ${author} \u7684\u7559\u8A00\u66F4\u65B0\u4E86\u4E00\u7248\u300A${node2}\u300B\u3002`,
225327
225605
  commentResolved: (by, node2) => `${by} \u5DF2\u5904\u7406\u300A${node2}\u300B\u7684\u7559\u8A00\u3002`,
225328
225606
  issueResolved: (note) => note
225329
225607
  };
@@ -225347,6 +225625,125 @@ function targetOf(cardId) {
225347
225625
  return null;
225348
225626
  }
225349
225627
  function buildWorkorderActivityTrace(input) {
225628
+ const { workorderId, cardId, snap } = input;
225629
+ const ref2 = input.ref ?? ((id) => ({ id }));
225630
+ const empty2 = { workorderId, cardId, attempts: [] };
225631
+ if (!snap) return empty2;
225632
+ const card2 = input.events ? buildWorkorderActivity({
225633
+ workorderId,
225634
+ snap,
225635
+ events: input.events,
225636
+ ref: ref2
225637
+ }).cards.find((c) => c.id === cardId) : void 0;
225638
+ const workIds = card2?.traceWorkIds ?? (cardId.startsWith("work:") ? [cardId.slice(5)] : []);
225639
+ if (workIds.length > 0) {
225640
+ let firstBriefing;
225641
+ const works = [];
225642
+ for (const workId of workIds) {
225643
+ const work = snap.works.find((w2) => w2.id === workId);
225644
+ if (!work) continue;
225645
+ const single = buildSingleTrace({ ...input, cardId: `work:${workId}` });
225646
+ const briefing = single.briefing;
225647
+ if (!briefing) continue;
225648
+ firstBriefing ??= briefing;
225649
+ const sources = sourcesOfWork(work, input, briefing);
225650
+ const artifacts = single.attempts.at(-1)?.artifacts ?? artifactsOfWork(work, input);
225651
+ works.push({
225652
+ workId,
225653
+ at: work.createdAt,
225654
+ nodeTitle: briefing.nodeTitle,
225655
+ ...briefing.spec ? { spec: briefing.spec } : {},
225656
+ sources,
225657
+ attempts: single.attempts,
225658
+ ...work.conclusion?.trim() ? { conclusion: work.conclusion.trim() } : {},
225659
+ artifacts,
225660
+ ...single.attempts.length ? {} : { emptyReason: input.dispatches === void 0 ? "records_unavailable" : work.startedAt || work.endedAt || work.deadAt || work.cancelledAt || work.conclusion || work.status && work.status !== "running" ? "records_missing" : "not_dispatched" }
225661
+ });
225662
+ }
225663
+ if (works.length) {
225664
+ return {
225665
+ workorderId,
225666
+ cardId,
225667
+ briefing: firstBriefing,
225668
+ works,
225669
+ attempts: works.flatMap((w2) => w2.attempts).sort((a, b2) => a.at.localeCompare(b2.at) || a.runId.localeCompare(b2.runId))
225670
+ };
225671
+ }
225672
+ }
225673
+ return buildSingleTrace(input);
225674
+ }
225675
+ function sourcesOfWork(work, input, briefing) {
225676
+ const snap = input.snap;
225677
+ const ref2 = input.ref ?? ((id) => ({ id }));
225678
+ const created = input.events?.find((r) => r.event.kind === "work.create" && r.event.workId === work.id);
225679
+ const event = created?.event;
225680
+ const replyIssueId = replyIssueIdOf(work);
225681
+ const ids2 = work.frozenIssueIds ?? (event?.kind === "work.create" ? event.reasons.filter((r) => r.kind === "issue").map((r) => r.issueId) : replyIssueId ? [replyIssueId] : void 0);
225682
+ if (!ids2) return [{ kind: "unavailable", at: work.createdAt }];
225683
+ const sources = [];
225684
+ for (const issueId of new Set(ids2)) {
225685
+ const issue2 = snap.issues.find((i) => i.id === issueId);
225686
+ if (!issue2 || issue2.createdAt > work.createdAt) {
225687
+ sources.push({ kind: "unavailable", at: work.createdAt, issueId });
225688
+ continue;
225689
+ }
225690
+ if (issue2.kind !== "comment" && issue2.kind !== "change_request") continue;
225691
+ const replySeq = (id) => input.events?.find((e) => e.event.kind === "issue.reply" && e.event.replyId === id)?.seq;
225692
+ if (snap.issueReplies.some((r) => r.issueId === issueId && r.createdAt === work.createdAt && (!created || replySeq(r.id) === void 0))) {
225693
+ sources.push({ kind: "unavailable", at: work.createdAt, issueId });
225694
+ continue;
225695
+ }
225696
+ const replies = snap.issueReplies.filter((r) => {
225697
+ if (r.issueId !== issueId || r.createdAt > work.createdAt) return false;
225698
+ const seq = replySeq(r.id);
225699
+ return created && seq !== void 0 ? seq < created.seq : r.createdAt < work.createdAt;
225700
+ }).sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || (replySeq(a.id) ?? 0) - (replySeq(b2.id) ?? 0) || a.id.localeCompare(b2.id));
225701
+ const reply = replies.at(-1);
225702
+ if (reply?.viaWorkId && snap.works.some((w2) => w2.id === reply.viaWorkId && w2.nodeId === work.nodeId)) {
225703
+ sources.push({ kind: "unavailable", at: work.createdAt, issueId });
225704
+ continue;
225705
+ }
225706
+ sources.push({
225707
+ kind: issueId.startsWith("ann:rework:") ? "gap_resume" : "comment",
225708
+ actor: ref2(reply?.authorActorId ?? issue2.authorActorId),
225709
+ at: reply?.createdAt ?? issue2.createdAt,
225710
+ content: reply?.body ?? issue2.body,
225711
+ issueId,
225712
+ ...reply ? { replyId: reply.id } : {}
225713
+ });
225714
+ }
225715
+ return sources.length ? sources : [{
225716
+ kind: "assignment",
225717
+ actor: briefing.actor,
225718
+ at: work.createdAt,
225719
+ ...briefing.spec ? { content: briefing.spec } : {}
225720
+ }];
225721
+ }
225722
+ function artifactsOfWork(work, input) {
225723
+ const snap = input.snap;
225724
+ const workId = work.id;
225725
+ const nodeId = work.nodeId;
225726
+ const node2 = snap.nodes.find((n) => n.id === nodeId);
225727
+ const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
225728
+ const isRootBrief = node2?.type === "brief" && !nodesWithInEdges.has(nodeId);
225729
+ const list2 = [];
225730
+ for (const a of snap.artifacts) {
225731
+ if (a.workId !== workId) continue;
225732
+ const artifactNodeId = a.nodeId;
225733
+ const files = a.contentKind === "manifest" ? input.manifestFiles?.get(a.contentRef) : void 0;
225734
+ if (files && files.length > 0) {
225735
+ for (const path41 of files) {
225736
+ list2.push({ id: a.id, name: path41.split("/").pop() || path41, path: path41, nodeId: artifactNodeId });
225737
+ }
225738
+ } else if (isRootBrief) {
225739
+ list2.push({ id: a.id, name: "\u4EFB\u52A1\u76EE\u6807", path: "\u4EFB\u52A1\u76EE\u6807.md", nodeId: artifactNodeId });
225740
+ } else {
225741
+ list2.push({ id: a.id, name: a.name, nodeId: artifactNodeId });
225742
+ }
225743
+ }
225744
+ return list2;
225745
+ }
225746
+ function buildSingleTrace(input) {
225350
225747
  const { workorderId, cardId, snap } = input;
225351
225748
  const ref2 = input.ref ?? ((id) => ({ id }));
225352
225749
  const empty2 = { workorderId, cardId, attempts: [] };
@@ -225359,26 +225756,6 @@ function buildWorkorderActivityTrace(input) {
225359
225756
  const nodeId = work?.nodeId ?? review?.nodeId;
225360
225757
  if (!nodeId) return empty2;
225361
225758
  const node2 = nodeById.get(nodeId);
225362
- const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
225363
- const isRootBrief = node2?.type === "brief" && !nodesWithInEdges.has(nodeId);
225364
- const artifactsOfWork = (workId) => {
225365
- const list2 = [];
225366
- for (const a of snap.artifacts) {
225367
- if (a.workId !== workId) continue;
225368
- const artifactNodeId = a.nodeId;
225369
- const files = a.contentKind === "manifest" ? input.manifestFiles?.get(a.contentRef) : void 0;
225370
- if (files && files.length > 0) {
225371
- for (const path41 of files) {
225372
- list2.push({ id: a.id, name: path41.split("/").pop() || path41, path: path41, nodeId: artifactNodeId });
225373
- }
225374
- } else if (isRootBrief) {
225375
- list2.push({ id: a.id, name: "\u4EFB\u52A1\u76EE\u6807", path: "\u4EFB\u52A1\u76EE\u6807.md", nodeId: artifactNodeId });
225376
- } else {
225377
- list2.push({ id: a.id, name: a.name, nodeId: artifactNodeId });
225378
- }
225379
- }
225380
- return list2;
225381
- };
225382
225759
  const managerId = resolveWorkorderManager(snap) ?? snap.workorder.ownerActorId;
225383
225760
  const startedAt = work?.createdAt ?? review?.createdAt;
225384
225761
  const spec = node2?.spec?.trim();
@@ -225388,18 +225765,18 @@ function buildWorkorderActivityTrace(input) {
225388
225765
  nodeTitle: node2?.title || nodeId.split(":").slice(2).join(":") || nodeId,
225389
225766
  ...spec ? { spec } : {}
225390
225767
  };
225391
- const rows = (input.dispatches ?? []).filter((d) => d.targetKind === target.kind && d.targetId === target.id).slice().sort((a, b2) => a.createdAt.localeCompare(b2.createdAt));
225768
+ const rows = (input.dispatches ?? []).filter((d) => d.targetKind === target.kind && d.targetId === target.id).slice().sort((a, b2) => a.createdAt.localeCompare(b2.createdAt) || a.id.localeCompare(b2.id));
225392
225769
  const executorId = work?.assigneeActorId ?? review?.reviewerActorId;
225393
225770
  const conclusion = work?.conclusion?.trim() || review?.note?.trim() || void 0;
225394
225771
  const attempts = rows.map((d, i) => {
225395
225772
  const isLast = i === rows.length - 1;
225396
- const actorId = executorId ?? d.actorId;
225773
+ const actorId = d.actorId ?? executorId;
225397
225774
  return {
225398
225775
  runId: d.id,
225399
225776
  at: d.createdAt,
225400
225777
  actor: ref2(actorId),
225401
225778
  ...isLast && conclusion ? { conclusion } : {},
225402
- artifacts: isLast && work ? artifactsOfWork(work.id) : []
225779
+ artifacts: isLast && work ? artifactsOfWork(work, input) : []
225403
225780
  };
225404
225781
  });
225405
225782
  return { workorderId, cardId, briefing, attempts };
@@ -225407,6 +225784,8 @@ function buildWorkorderActivityTrace(input) {
225407
225784
  var init_activity_trace = __esm({
225408
225785
  "../server/src/domains/collab/activity-trace.ts"() {
225409
225786
  "use strict";
225787
+ init_src4();
225788
+ init_activity2();
225410
225789
  init_workorder_manager();
225411
225790
  }
225412
225791
  });
@@ -227260,10 +227639,7 @@ function collabDomain(opts) {
227260
227639
  const hit = cache.get(companyId);
227261
227640
  if (hit) return hit;
227262
227641
  const e = await opts.resolveEngine(companyId);
227263
- if (e.kernel === opts.kernel) {
227264
- cache.set(companyId, defaultCtx);
227265
- return defaultCtx;
227266
- }
227642
+ const artifacts = opts.resolveArtifacts ? await opts.resolveArtifacts(companyId) : e.kernel === opts.kernel ? opts.artifacts : void 0;
227267
227643
  const ctx = {
227268
227644
  kernel: e.kernel,
227269
227645
  oplog: e.oplog,
@@ -227273,7 +227649,7 @@ function collabDomain(opts) {
227273
227649
  // ★ 产物旁存也要按公司取(见 resolveArtifacts 的注释):漏了这一行,非默认公司的
227274
227650
  // `/api/workorders/:id/files` 与 node_output 清单恒空——写侧写对了、读侧根本没接线。
227275
227651
  // 抛出来就抛出来:宁可 500 说"这家公司的数据面解析不了",也不能回落默认公司的库。
227276
- ...opts.resolveArtifacts ? { artifacts: await opts.resolveArtifacts(companyId) } : {}
227652
+ ...artifacts ? { artifacts } : {}
227277
227653
  };
227278
227654
  cache.set(companyId, ctx);
227279
227655
  return ctx;
@@ -227527,7 +227903,10 @@ function collabDomain(opts) {
227527
227903
  ...extra?.avatar ? { avatar: extra.avatar } : {}
227528
227904
  };
227529
227905
  };
227530
- const snap = engineStore ? await engineStore.transaction((tx) => tx.loadWorkorder(workorderId)) : null;
227906
+ const [snap, events] = engineStore ? await engineStore.transaction(async (tx) => [
227907
+ await tx.loadWorkorder(workorderId),
227908
+ await tx.listEvents(workorderId, 0, ACTIVITY_EVENT_LIMIT, ACTIVITY_EVENT_KINDS)
227909
+ ]) : [null, []];
227531
227910
  const manifestFiles = snap ? await resolveActivityManifestFiles(snap.artifacts, blobs) : void 0;
227532
227911
  const dispatches = await listDispatchRows(workorderId, opts.dispatchesOfWorkorder);
227533
227912
  return {
@@ -227536,6 +227915,7 @@ function collabDomain(opts) {
227536
227915
  workorderId,
227537
227916
  cardId,
227538
227917
  snap,
227918
+ events,
227539
227919
  ref: ref2,
227540
227920
  ...dispatches ? { dispatches } : {},
227541
227921
  ...manifestFiles ? { manifestFiles } : {}
@@ -231085,42 +231465,17 @@ function mapChatItemToSnapshotEntry(row) {
231085
231465
  itemType,
231086
231466
  role,
231087
231467
  status,
231088
- itemVersion: row.turnVersion,
231468
+ // ADR-0510 附录 A:版本轴 = 会话逻辑时钟(`chat_items.version`)。
231469
+ // 此前这里填的是 `turnVersion`(轮级 resync 游标),而增量流填的是进程内每 item 计数——
231470
+ // 两套编号空间,一轮中途拉快照就把随后每一帧判 stale,进 resync 死循环。
231471
+ itemVersion: row.version,
231089
231472
  seq: row.seq,
231473
+ ord: row.ord,
231090
231474
  messageId: row.messageId,
231091
231475
  updatedAt: row.updatedAt,
231092
231476
  payload: row.payload
231093
231477
  };
231094
231478
  }
231095
- function synthesizeMessageItem(msg, seq) {
231096
- const attachments = Array.isArray(msg.attachments) ? msg.attachments.map((a) => ({
231097
- name: a.name,
231098
- ...a.blobRef !== void 0 ? { blobRef: a.blobRef } : {},
231099
- ...a.contentType !== void 0 ? { contentType: a.contentType } : {}
231100
- })) : void 0;
231101
- const clientSubmitId = msg.role === "user" ? msg.clientSubmitId ?? clientSubmitIdFromMessageKey(msg.messageKey) : void 0;
231102
- const role = msg.role === "user" || msg.role === "system" ? msg.role : "assistant";
231103
- const status = msg.status === "running" ? "streaming" : msg.status === "error" ? "failed" : "completed";
231104
- return {
231105
- // ADR-0510 D1:有 csid 就用**与账本 user item、前端乐观桶位同名**的身份;没有(历史行 / 老客户端)
231106
- // 才退回合成 id。三方同名,桶按 itemId 合并才闭环——否则同一条 user 消息画两个气泡。
231107
- itemId: clientSubmitId ? optimisticUserItemId(clientSubmitId) : `chat-message:${msg.id}`,
231108
- turnId: msg.runId ?? `chat-message-turn:${msg.id}`,
231109
- itemType: "message",
231110
- role,
231111
- status,
231112
- itemVersion: 0,
231113
- seq,
231114
- messageId: msg.id,
231115
- updatedAt: msg.completedAt ?? msg.createdAt,
231116
- payload: {
231117
- role,
231118
- text: msg.content ?? "",
231119
- ...attachments?.length ? { attachments } : {},
231120
- ...clientSubmitId ? { clientSubmitId } : {}
231121
- }
231122
- };
231123
- }
231124
231479
  function throwWorkdirError(code2) {
231125
231480
  switch (code2) {
231126
231481
  case "PATH_ESCAPE":
@@ -231482,57 +231837,50 @@ function createChatSessionsDomain(opts) {
231482
231837
  if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
231483
231838
  const chatSessionId = req.params.id;
231484
231839
  const sinceVersion = Math.max(0, Number(req.query?.get("sinceVersion") ?? "0") || 0);
231840
+ const beforeOrdRaw = Number(req.query?.get("beforeOrd") ?? "0") || 0;
231841
+ const beforeOrd = beforeOrdRaw > 0 ? beforeOrdRaw : void 0;
231842
+ const limitRaw = Number(req.query?.get("limit") ?? "0") || 0;
231843
+ const limit = limitRaw > 0 ? Math.min(limitRaw, MAX_SNAPSHOT_ITEMS) : DEFAULT_SNAPSHOT_ITEMS;
231485
231844
  const cursor = opts.liveChat?.cursorFor(chatSessionId) ?? null;
231486
231845
  let turnId = cursor?.turnId ?? null;
231487
- let inflightRunId = opts.liveChat?.runFor(chatSessionId) ?? null;
231488
231846
  if (!turnId) {
231489
231847
  const turns = await turnsFor(req);
231490
231848
  const active = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
231491
231849
  turnId = active?.id ?? null;
231492
- inflightRunId ??= active?.assistantRunId ?? null;
231493
231850
  }
231494
231851
  const snapshotEntries = [];
231495
231852
  let nextSinceVersion = sinceVersion;
231496
- if (turnId) {
231497
- const rows = await items.listItemsByTurn(chatSessionId, turnId, { sinceTurnVersion: sinceVersion }).catch(() => []);
231853
+ let hasMoreBefore = false;
231854
+ if (sinceVersion > 0) {
231855
+ if (turnId) {
231856
+ const rows = await items.listItemsByTurn(chatSessionId, turnId, { sinceTurnVersion: sinceVersion }).catch(() => []);
231857
+ for (const row of rows) {
231858
+ snapshotEntries.push(mapChatItemToSnapshotEntry(row));
231859
+ if (row.turnVersion > nextSinceVersion) nextSinceVersion = row.turnVersion;
231860
+ }
231861
+ }
231862
+ } else {
231863
+ const rows = await items.listSessionItems(chatSessionId, {
231864
+ limit,
231865
+ ...beforeOrd !== void 0 ? { beforeOrd } : {}
231866
+ }).catch(() => []);
231867
+ hasMoreBefore = rows.length >= limit;
231498
231868
  for (const row of rows) {
231499
231869
  snapshotEntries.push(mapChatItemToSnapshotEntry(row));
231500
231870
  if (row.turnVersion > nextSinceVersion) nextSinceVersion = row.turnVersion;
231501
231871
  }
231502
231872
  }
231503
- const activeTurnItemIds = new Set(snapshotEntries.map((e) => e.itemId));
231504
- const HISTORY_TURNS = 12;
231505
- const historicalEntries = [];
231506
- if (sinceVersion === 0) {
231507
- const listRecentTurns2 = store2.listRecentTurns;
231508
- const messages = listRecentTurns2 ? (await listRecentTurns2.call(store2, chatSessionId, HISTORY_TURNS).catch(() => ({ messages: [], hasMoreBefore: false }))).messages : windowMessagesByTurns(await store2.listMessages(chatSessionId).catch(() => []), HISTORY_TURNS).messages;
231509
- let historySeq = 0;
231510
- for (const msg of messages) {
231511
- if (!msg.id) continue;
231512
- const rows = await items.listItemsByMessage(msg.id).catch(() => []);
231513
- const historical = turnId ? rows.filter((row) => row.turnId !== turnId) : rows;
231514
- if (rows.length > 0) {
231515
- for (const row of [...historical].sort((a, b2) => a.seq - b2.seq)) {
231516
- historySeq += 1;
231517
- historicalEntries.push({ ...mapChatItemToSnapshotEntry(row), seq: historySeq });
231518
- }
231519
- continue;
231520
- }
231521
- if (msg.role === "assistant" && msg.status === "running" && inflightRunId && msg.runId === inflightRunId) continue;
231522
- const synthesized = synthesizeMessageItem(msg, historySeq + 1);
231523
- if (activeTurnItemIds.has(synthesized.itemId)) continue;
231524
- historySeq += 1;
231525
- historicalEntries.push(synthesized);
231526
- }
231527
- }
231873
+ const oldestOrd = snapshotEntries.find((e) => e.ord !== null)?.ord ?? null;
231528
231874
  const body2 = {
231529
231875
  chatSessionId,
231530
231876
  turnId,
231531
- // 历史合成条在前,active turn chat_items 在后——顺序即桶排序,桶按 seq 单调排列。
231532
- items: [...historicalEntries, ...snapshotEntries],
231877
+ // 已按 ord 升序——服务端给什么顺序,客户端就按 ord 排出什么顺序,两边同一把尺子。
231878
+ items: snapshotEntries,
231533
231879
  nextSinceVersion,
231534
231880
  frameCursor: cursor ? { epoch: cursor.epoch, seq: cursor.seq } : { epoch: 0, seq: 0 },
231535
- canAppend: cursor?.canAppend ?? false
231881
+ canAppend: cursor?.canAppend ?? false,
231882
+ oldestOrd,
231883
+ hasMoreBefore
231536
231884
  };
231537
231885
  return { status: 200, body: body2 };
231538
231886
  });
@@ -231885,7 +232233,7 @@ function createChatSessionsDomain(opts) {
231885
232233
  });
231886
232234
  };
231887
232235
  }
231888
- var import_node_crypto59, WORKDIR_READ_MAX_BYTES;
232236
+ var import_node_crypto59, DEFAULT_SNAPSHOT_ITEMS, MAX_SNAPSHOT_ITEMS, WORKDIR_READ_MAX_BYTES;
231889
232237
  var init_chat_sessions = __esm({
231890
232238
  "../server/src/domains/chat-sessions/index.ts"() {
231891
232239
  "use strict";
@@ -231906,6 +232254,8 @@ var init_chat_sessions = __esm({
231906
232254
  init_workorder_terminal();
231907
232255
  init_session_workdir();
231908
232256
  init_project_bundle();
232257
+ DEFAULT_SNAPSHOT_ITEMS = 400;
232258
+ MAX_SNAPSHOT_ITEMS = 1e3;
231909
232259
  WORKDIR_READ_MAX_BYTES = 2 * 1024 * 1024;
231910
232260
  }
231911
232261
  });
@@ -236568,6 +236918,9 @@ function makeStoreCodeRepoResolver(deps) {
236568
236918
  }
236569
236919
  };
236570
236920
  }
236921
+ function makeCompanyCodeRepoResolver(storesFor) {
236922
+ return async (ws, companyId) => makeStoreCodeRepoResolver(await storesFor(companyId)).resolveCodeRepo(ws);
236923
+ }
236571
236924
  var init_resolve = __esm({
236572
236925
  "../server/src/git/resolve.ts"() {
236573
236926
  "use strict";
@@ -237518,16 +237871,28 @@ var init_fused_signal_router = __esm({
237518
237871
  }
237519
237872
  });
237520
237873
 
237874
+ // ../storage/src/pg-ident.ts
237875
+ function quoteIdent2(name) {
237876
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(name)) throw new Error(`invalid schema name: ${name}`);
237877
+ return `"${name}"`;
237878
+ }
237879
+ var init_pg_ident = __esm({
237880
+ "../storage/src/pg-ident.ts"() {
237881
+ "use strict";
237882
+ }
237883
+ });
237884
+
237521
237885
  // ../storage/src/postgres.ts
237522
237886
  var import_node_crypto67, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
237523
237887
  var init_postgres = __esm({
237524
237888
  "../storage/src/postgres.ts"() {
237525
237889
  "use strict";
237526
237890
  import_node_crypto67 = require("node:crypto");
237891
+ init_pg_ident();
237527
237892
  init_esm();
237528
237893
  init_src();
237529
237894
  ident3 = (s2) => {
237530
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema identifier: ${s2}`);
237895
+ quoteIdent2(s2);
237531
237896
  return s2;
237532
237897
  };
237533
237898
  isUniqueViolation = (err) => typeof err === "object" && err !== null && err.code === "23505";
@@ -237749,7 +238114,9 @@ function rowToVariable(row) {
237749
238114
  updatedAt: new Date(row.updated_at).toISOString(),
237750
238115
  // ADR 凭据保险库补章 §D2.1:到期/取用时间戳(text 列存 ISO 字符串,原样带出;null/空 = undefined)。
237751
238116
  ...row.expires_at ? { expiresAt: row.expires_at } : {},
237752
- ...row.last_used_at ? { lastUsedAt: row.last_used_at } : {}
238117
+ ...row.last_used_at ? { lastUsedAt: row.last_used_at } : {},
238118
+ // 展示名(存量行没有 → 不带这一格,展示侧回落成 key)。
238119
+ ...row.name ? { name: row.name } : {}
237753
238120
  };
237754
238121
  }
237755
238122
  var ident4, PostgresRegistryStore, rowToActor, rowToConfig, rowToAgentPrefs;
@@ -237757,9 +238124,10 @@ var init_postgres_registry = __esm({
237757
238124
  "../storage/src/postgres-registry.ts"() {
237758
238125
  "use strict";
237759
238126
  init_esm();
238127
+ init_pg_ident();
237760
238128
  init_src();
237761
238129
  ident4 = (s2) => {
237762
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
238130
+ quoteIdent2(s2);
237763
238131
  return s2;
237764
238132
  };
237765
238133
  PostgresRegistryStore = class _PostgresRegistryStore {
@@ -237854,6 +238222,7 @@ var init_postgres_registry = __esm({
237854
238222
  account text,
237855
238223
  verified_at timestamptz
237856
238224
  )`);
238225
+ await pool.query(`ALTER TABLE "${s2}".connectors ADD COLUMN IF NOT EXISTS connected_by text`);
237857
238226
  await pool.query(`
237858
238227
  CREATE TABLE IF NOT EXISTS "${s2}".actor_connector_connections (
237859
238228
  actor_id text NOT NULL,
@@ -237914,6 +238283,7 @@ var init_postgres_registry = __esm({
237914
238283
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS project_id text`);
237915
238284
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS environment text`);
237916
238285
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS delivery_mode text`);
238286
+ await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS name text`);
237917
238287
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS expires_at text`);
237918
238288
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS last_used_at text`);
237919
238289
  await pool.query(`DROP INDEX IF EXISTS "${s2}"."${s2}_variables_key_actor_uq"`);
@@ -238224,9 +238594,11 @@ var init_postgres_registry = __esm({
238224
238594
  /* ---------- 连接器 ---------- */
238225
238595
  async upsertConnector(c) {
238226
238596
  await this.pool.query(
238227
- `INSERT INTO ${this.s}.connectors (id, name, mode, status, account, verified_at) VALUES ($1,$2,$3,$4,$5,$6)
238228
- ON CONFLICT (id) DO UPDATE SET name=$2, mode=$3, status=$4, account=$5, verified_at=$6`,
238229
- [c.id, c.name, c.mode, c.status, c.account ?? null, c.verifiedAt ?? null]
238597
+ `INSERT INTO ${this.s}.connectors (id, name, mode, status, account, verified_at, connected_by)
238598
+ VALUES ($1,$2,$3,$4,$5,$6,$7)
238599
+ ON CONFLICT (id) DO UPDATE SET name=$2, mode=$3, status=$4, account=$5, verified_at=$6,
238600
+ connected_by=COALESCE($7, ${this.s}.connectors.connected_by)`,
238601
+ [c.id, c.name, c.mode, c.status, c.account ?? null, c.verifiedAt ?? null, c.connectedBy ?? null]
238230
238602
  );
238231
238603
  }
238232
238604
  async listConnectors() {
@@ -238237,7 +238609,8 @@ var init_postgres_registry = __esm({
238237
238609
  mode: row.mode,
238238
238610
  status: row.status,
238239
238611
  ...row.account !== null ? { account: row.account } : {},
238240
- ...row.verified_at !== null ? { verifiedAt: new Date(row.verified_at).toISOString() } : {}
238612
+ ...row.verified_at !== null ? { verifiedAt: new Date(row.verified_at).toISOString() } : {},
238613
+ ...row.connected_by ? { connectedBy: row.connected_by } : {}
238241
238614
  }));
238242
238615
  }
238243
238616
  async deleteConnector(id) {
@@ -238409,10 +238782,13 @@ var init_postgres_registry = __esm({
238409
238782
  /* ---------- 变量 ---------- */
238410
238783
  async putVariable(v2) {
238411
238784
  await this.pool.query(
238412
- `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at)
238413
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
238785
+ /* `name` COALESCE:**没传 = 不动**。连接器授权流程写变量时手上没有「名称」,
238786
+ 普通赋值会把人在密钥表单里填的名字抹掉——同 connectors.connected_by 那条。 */
238787
+ `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at, name)
238788
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
238414
238789
  ON CONFLICT (key, COALESCE(actor_id, ''), COALESCE(project_id, '')) DO UPDATE
238415
- SET scope=$2, actor_id=$3, project_id=$4, connector_id=$5, overrides=$6, delivery_mode=$7, value_encrypted=$8, updated_at=$9, expires_at=$10`,
238790
+ SET scope=$2, actor_id=$3, project_id=$4, connector_id=$5, overrides=$6, delivery_mode=$7, value_encrypted=$8, updated_at=$9, expires_at=$10,
238791
+ name=COALESCE($11, ${this.s}.variables.name)`,
238416
238792
  [
238417
238793
  v2.key,
238418
238794
  v2.scope,
@@ -238423,7 +238799,8 @@ var init_postgres_registry = __esm({
238423
238799
  v2.deliveryMode ?? null,
238424
238800
  v2.valueEncrypted,
238425
238801
  v2.updatedAt,
238426
- v2.expiresAt ?? null
238802
+ v2.expiresAt ?? null,
238803
+ v2.name ?? null
238427
238804
  ]
238428
238805
  );
238429
238806
  }
@@ -238519,17 +238896,6 @@ var init_postgres_registry = __esm({
238519
238896
  }
238520
238897
  });
238521
238898
 
238522
- // ../storage/src/pg-ident.ts
238523
- function quoteIdent2(name) {
238524
- if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(name)) throw new Error(`invalid schema name: ${name}`);
238525
- return `"${name}"`;
238526
- }
238527
- var init_pg_ident = __esm({
238528
- "../storage/src/pg-ident.ts"() {
238529
- "use strict";
238530
- }
238531
- });
238532
-
238533
238899
  // ../storage/src/postgres-assistant-store.ts
238534
238900
  var ASSISTANT_LOCK_NAMESPACE, rowToBinding, rowToHistory, PostgresAssistantBindStore, PostgresHumanPrefsStore;
238535
238901
  var init_postgres_assistant_store = __esm({
@@ -238838,8 +239204,9 @@ var init_postgres_channels = __esm({
238838
239204
  "../storage/src/postgres-channels.ts"() {
238839
239205
  "use strict";
238840
239206
  init_esm();
239207
+ init_pg_ident();
238841
239208
  ident5 = (s2) => {
238842
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
239209
+ quoteIdent2(s2);
238843
239210
  return s2;
238844
239211
  };
238845
239212
  PostgresChannelStore = class _PostgresChannelStore {
@@ -239113,6 +239480,24 @@ var init_postgres_projects = __esm({
239113
239480
  const r = await this.pool.query(`SELECT * FROM ${this.s}.projects ORDER BY created_at`);
239114
239481
  return r.rows.map(rowToProject);
239115
239482
  }
239483
+ /** 主行 + 三张子表一并删,一个事务里做完——四张表之间没有外键,分开删失败一半就留孤儿行。 */
239484
+ async deleteProject(id) {
239485
+ const client = await this.pool.connect();
239486
+ try {
239487
+ await client.query("BEGIN");
239488
+ await client.query(`DELETE FROM ${this.s}.project_members WHERE project_id = $1`, [id]);
239489
+ await client.query(`DELETE FROM ${this.s}.project_files WHERE project_id = $1`, [id]);
239490
+ await client.query(`DELETE FROM ${this.s}.project_content_locations WHERE project_id = $1`, [id]);
239491
+ await client.query(`DELETE FROM ${this.s}.projects WHERE id = $1`, [id]);
239492
+ await client.query("COMMIT");
239493
+ } catch (e) {
239494
+ await client.query("ROLLBACK").catch(() => {
239495
+ });
239496
+ throw e;
239497
+ } finally {
239498
+ client.release();
239499
+ }
239500
+ }
239116
239501
  async replaceProjectMembers(projectId2, members) {
239117
239502
  const client = await this.pool.connect();
239118
239503
  try {
@@ -239907,8 +240292,9 @@ var init_postgres_control_plane = __esm({
239907
240292
  "../storage/src/postgres-control-plane.ts"() {
239908
240293
  "use strict";
239909
240294
  init_esm();
240295
+ init_pg_ident();
239910
240296
  ident7 = (s2) => {
239911
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
240297
+ quoteIdent2(s2);
239912
240298
  return s2;
239913
240299
  };
239914
240300
  PostgresControlPlaneStore = class _PostgresControlPlaneStore {
@@ -240833,11 +241219,12 @@ var ident8, mergeContinuityRefs3, PostgresTraceStore, rowToRun, terminalHandoffO
240833
241219
  var init_postgres_trace = __esm({
240834
241220
  "../storage/src/postgres-trace.ts"() {
240835
241221
  "use strict";
241222
+ init_pg_ident();
240836
241223
  init_esm();
240837
241224
  init_src();
240838
241225
  init_pg_sanitize();
240839
241226
  ident8 = (s2) => {
240840
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
241227
+ quoteIdent2(s2);
240841
241228
  return s2;
240842
241229
  };
240843
241230
  mergeContinuityRefs3 = (...lists) => {
@@ -242822,10 +243209,11 @@ var init_postgres_automations = __esm({
242822
243209
  "../storage/src/postgres-automations.ts"() {
242823
243210
  "use strict";
242824
243211
  init_esm();
243212
+ init_pg_ident();
242825
243213
  init_postgres_chat_turns();
242826
243214
  init_src();
242827
243215
  ident10 = (s2) => {
242828
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
243216
+ quoteIdent2(s2);
242829
243217
  return s2;
242830
243218
  };
242831
243219
  genId = (prefix) => `${prefix}_${Math.random().toString(36).slice(2, 12)}`;
@@ -244064,6 +244452,15 @@ var init_postgres_chat_sessions = __esm({
244064
244452
  args
244065
244453
  );
244066
244454
  }
244455
+ /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
244456
+ async clearProjectBinding(projectId2) {
244457
+ if (!projectId2) return 0;
244458
+ const r = await this.pool.query(
244459
+ `UPDATE ${this.s}.chat_sessions SET project_id = NULL WHERE project_id = $1 AND company_id = $2`,
244460
+ [projectId2, this.companyId]
244461
+ );
244462
+ return r.rowCount ?? 0;
244463
+ }
244067
244464
  async deleteSession(id) {
244068
244465
  if (!await this.scopedSessionExists(id)) return;
244069
244466
  await this.pool.query(`DELETE FROM ${this.s}.chat_messages WHERE session_id = $1`, [id]);
@@ -244887,12 +245284,15 @@ var init_postgres_chat_items = __esm({
244887
245284
  const r = await this.pool.query(
244888
245285
  `INSERT INTO ${this.s}.chat_items
244889
245286
  (id, session_id, message_id, turn_id, run_id, seq, kind, role, status,
244890
- version, turn_version, provider_item_key, payload, metadata, created_at, updated_at, completed_at)
245287
+ version, turn_version, provider_item_key, payload, metadata, created_at, updated_at, ord, completed_at)
244891
245288
  SELECT $1, $2, $3, $4, $5,
244892
245289
  COALESCE(MAX(c.seq), 0) + 1,
244893
- $6, $7, $8::text, 0,
245290
+ $6, $7, $8::text, COALESCE($13::bigint, 0),
244894
245291
  COALESCE(MAX(c.turn_version), 0) + 1,
244895
245292
  $9, $10::jsonb, $11::jsonb, $12::timestamptz, $12::timestamptz,
245293
+ -- ord\uFF1A\u8D26\u672C\u9884\u6D3E\u4E86\u5C31\u7528\u5B83\uFF08\u5EFA\u884C\u5373\u6709\u53F7\u3001\u7B2C\u4E00\u5E27\u5C31\u80FD\u5E26\u4E0A wire\uFF09\uFF1B\u6CA1\u7ED9\u624D\u8D70\u5217 DEFAULT
245294
+ -- \u7684 nextval\u2014\u2014\u4E24\u6761\u8DEF\u53D6\u7684\u662F\u540C\u4E00\u6761\u8868\u7EA7\u5E8F\u5217\uFF0C\u8BED\u4E49\u76F8\u540C\uFF0C\u53EA\u5DEE\u62FF\u53F7\u53D1\u751F\u5728\u54EA\u4E00\u4FA7\u3002
245295
+ COALESCE($14::bigint, nextval('${this.s}.chat_items_ord_seq'::regclass)),
244896
245296
  -- $12/$8 \u4E09\u5904\u90FD\u663E\u5F0F\u8F6C\u578B\uFF1A\u4E0D\u8F6C\u7684\u8BDD PG \u4F1A\u5728\u540C\u4E00\u6761\u8BED\u53E5\u91CC\u7ED9\u540C\u4E00\u4E2A\u53C2\u6570\u63A8\u51FA\u4E0D\u540C\u7C7B\u578B
244897
245297
  -- \uFF08\u5217\u4F4D\u662F timestamptz/text\u3001CASE \u91CC\u662F unknown\uFF09\uFF0C\u76F4\u63A5\u62A5
244898
245298
  -- inconsistent types deduced for parameter $12\u3002
@@ -244912,7 +245312,9 @@ var init_postgres_chat_items = __esm({
244912
245312
  scrubPgString(input.providerItemKey ?? null),
244913
245313
  payload,
244914
245314
  metadata,
244915
- at
245315
+ at,
245316
+ input.version ?? null,
245317
+ input.ord ?? null
244916
245318
  ]
244917
245319
  );
244918
245320
  return rowToItem(r.rows[0]);
@@ -244950,10 +245352,13 @@ var init_postgres_chat_items = __esm({
244950
245352
  setExpr = `status = $3::text, completed_at = CASE WHEN $3 IN ('completed','failed','interrupted') THEN $2::timestamptz ELSE c.completed_at END`;
244951
245353
  break;
244952
245354
  }
245355
+ const versionParamIndex = args.length + 1;
245356
+ args.push(opts?.version ?? null);
244953
245357
  const r = await this.pool.query(
244954
245358
  `UPDATE ${this.s}.chat_items c
244955
245359
  SET ${setExpr},
244956
- version = c.version + 1,
245360
+ version = CASE WHEN $${versionParamIndex}::bigint IS NULL THEN c.version + 1
245361
+ ELSE GREATEST(c.version, $${versionParamIndex}::bigint) END,
244957
245362
  turn_version = ${bumpTurnVersion},
244958
245363
  updated_at = $2
244959
245364
  WHERE c.id = $1
@@ -245001,6 +245406,49 @@ var init_postgres_chat_items = __esm({
245001
245406
  );
245002
245407
  return r.rows.map((row) => rowToItem(row));
245003
245408
  }
245409
+ /**
245410
+ * 一条会话的 item,按展示序升序(ADR-0510 D3:历史的唯一出口)。
245411
+ *
245412
+ * 排序键 `(ord IS NULL) DESC, ord DESC` 的意思是「取最新的一段」:带号的行按号从大到小、
245413
+ * 没号的存量行排在最后(= 最老)。截断之后再翻回升序返回。
245414
+ * 走索引 `chat_items_session_ord_idx (session_id, ord)`。
245415
+ */
245416
+ async listSessionItems(sessionId, opts) {
245417
+ const limit = Math.max(1, Math.min(opts?.limit ?? 500, 2e3));
245418
+ const before = opts?.beforeOrd;
245419
+ const r = before === void 0 ? await this.pool.query(
245420
+ `SELECT * FROM ${this.s}.chat_items WHERE session_id = $1
245421
+ ORDER BY (ord IS NULL) ASC, ord DESC, seq DESC LIMIT $2`,
245422
+ [sessionId, limit]
245423
+ ) : await this.pool.query(
245424
+ `SELECT * FROM ${this.s}.chat_items WHERE session_id = $1 AND ord IS NOT NULL AND ord < $2
245425
+ ORDER BY ord DESC LIMIT $3`,
245426
+ [sessionId, before, limit]
245427
+ );
245428
+ const rows = r.rows.map((row) => rowToItem(row));
245429
+ rows.reverse();
245430
+ return rows.sort((a, b2) => {
245431
+ if (a.ord === null && b2.ord === null) return a.seq - b2.seq;
245432
+ if (a.ord === null) return -1;
245433
+ if (b2.ord === null) return 1;
245434
+ return a.ord - b2.ord;
245435
+ });
245436
+ }
245437
+ async sessionVersionCursor(sessionId) {
245438
+ const r = await this.pool.query(
245439
+ `SELECT COALESCE(MAX(version), 0) AS v FROM ${this.s}.chat_items WHERE session_id = $1`,
245440
+ [sessionId]
245441
+ );
245442
+ return Number(r.rows[0]?.v ?? 0);
245443
+ }
245444
+ async reserveOrds(count2) {
245445
+ const n = Math.max(1, Math.min(Math.floor(count2), 1e3));
245446
+ const r = await this.pool.query(
245447
+ `SELECT nextval('${this.s}.chat_items_ord_seq'::regclass) AS ord FROM generate_series(1, $1)`,
245448
+ [n]
245449
+ );
245450
+ return r.rows.map((row) => Number(row.ord));
245451
+ }
245004
245452
  async turnCursor(sessionId, turnId) {
245005
245453
  const r = await this.pool.query(
245006
245454
  `SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq, COALESCE(MAX(turn_version), 0) + 1 AS next_turn_version
@@ -245337,8 +245785,14 @@ function planSessionOrd(sessionId, messages, items, turns, opts) {
245337
245785
  }
245338
245786
  async function listChatItemsSchemas(pool) {
245339
245787
  const r = await pool.query(
245340
- `SELECT n.nspname AS s FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
245341
- WHERE c.relname = 'chat_items' AND c.relkind = 'r' ORDER BY 1`
245788
+ `SELECT n.nspname AS s
245789
+ FROM pg_class c
245790
+ JOIN pg_namespace n ON n.oid = c.relnamespace
245791
+ WHERE c.relname = 'chat_items' AND c.relkind = 'r'
245792
+ AND (SELECT count(*) FROM information_schema.columns col
245793
+ WHERE col.table_schema = n.nspname AND col.table_name = 'chat_items'
245794
+ AND col.column_name IN ('session_id', 'turn_id', 'seq', 'kind', 'payload')) = 5
245795
+ ORDER BY 1`
245342
245796
  );
245343
245797
  return r.rows.map((x2) => x2.s);
245344
245798
  }
@@ -245932,9 +246386,10 @@ var init_postgres_nodes = __esm({
245932
246386
  "../storage/src/postgres-nodes.ts"() {
245933
246387
  "use strict";
245934
246388
  import_node_crypto68 = require("node:crypto");
246389
+ init_pg_ident();
245935
246390
  init_esm();
245936
246391
  ident12 = (s2) => {
245937
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
246392
+ quoteIdent2(s2);
245938
246393
  return s2;
245939
246394
  };
245940
246395
  PostgresNodeStore = class _PostgresNodeStore {
@@ -246199,8 +246654,9 @@ var init_postgres_model_prices = __esm({
246199
246654
  "../storage/src/postgres-model-prices.ts"() {
246200
246655
  "use strict";
246201
246656
  init_esm();
246657
+ init_pg_ident();
246202
246658
  ident13 = (s2) => {
246203
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
246659
+ quoteIdent2(s2);
246204
246660
  return s2;
246205
246661
  };
246206
246662
  PostgresModelPriceStore = class _PostgresModelPriceStore {
@@ -246410,10 +246866,11 @@ var init_postgres_actor_memory = __esm({
246410
246866
  "../storage/src/postgres-actor-memory.ts"() {
246411
246867
  "use strict";
246412
246868
  import_node_crypto69 = require("node:crypto");
246869
+ init_pg_ident();
246413
246870
  init_src();
246414
246871
  matchClause = (q2) => q2.requireMatch && q2.keywords.length > 0 ? " WHERE m > 0" : "";
246415
246872
  ident14 = (s2) => {
246416
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
246873
+ quoteIdent2(s2);
246417
246874
  return s2;
246418
246875
  };
246419
246876
  PostgresActorMemoryStore = class _PostgresActorMemoryStore {
@@ -246719,9 +247176,10 @@ var init_postgres_inbox_read = __esm({
246719
247176
  "../storage/src/postgres-inbox-read.ts"() {
246720
247177
  "use strict";
246721
247178
  init_esm();
247179
+ init_pg_ident();
246722
247180
  SEED_MARKER_ACTOR = "actor:system";
246723
247181
  ident15 = (s2) => {
246724
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
247182
+ quoteIdent2(s2);
246725
247183
  return s2;
246726
247184
  };
246727
247185
  PostgresReadMarkerStore = class _PostgresReadMarkerStore {
@@ -251973,10 +252431,11 @@ var init_postgres_knowledge = __esm({
251973
252431
  "../storage/src/postgres-knowledge.ts"() {
251974
252432
  "use strict";
251975
252433
  init_esm();
252434
+ init_pg_ident();
251976
252435
  init_pg_sanitize();
251977
252436
  init_knowledge_snapshot_crypto();
251978
252437
  ident17 = (value2) => {
251979
- if (!/^[a-z_][a-z0-9_]*$/.test(value2)) throw new Error(`invalid schema name: ${value2}`);
252438
+ quoteIdent2(value2);
251980
252439
  return value2;
251981
252440
  };
251982
252441
  TABLES = [
@@ -255598,6 +256057,36 @@ async function startServe(opts) {
255598
256057
  projectId2,
255599
256058
  afterName
255600
256059
  );
256060
+ },
256061
+ /**
256062
+ * 删项目后的跨域清理(发起人 2026-09-09 拍板的口径里,projects 域自己管不到的那两件):
256063
+ * ① 绑到该项目的会话 `project_id` 置空 —— 会话一条都不删,回到「不指定项目」。不清的话
256064
+ * 项目 chip 会按既定口径显示一串裸项目 id(`pages/chat/FRAME-DATA-MAP.md`)。
256065
+ * ② 该项目作用域下的变量/密钥删掉 —— 项目没了,`resolveProjectVariables` 再也解析不到它们,
256066
+ * 留着只是一堆谁也读不到的明文凭据。
256067
+ * 与改名钩子同款:`chatStoreFor` / `engineRouter` 的 const 都在本域构造之后,所以只能在
256068
+ * handler 体内(删除发生时)解析,不能在工厂里提前引用——那会撞 TDZ。
256069
+ */
256070
+ onProjectDeletedFor: (companyId) => async ({ projectId: projectId2 }) => {
256071
+ const failures = [];
256072
+ try {
256073
+ const sessions = await chatStoreFor(companyId);
256074
+ if (sessions.clearProjectBinding) await sessions.clearProjectBinding(projectId2);
256075
+ else failures.push("\u4F1A\u8BDD store \u4E0D\u652F\u6301 clearProjectBinding\uFF0C\u4F1A\u8BDD\u7684\u9879\u76EE\u7ED1\u5B9A\u672A\u6E05");
256076
+ } catch (e) {
256077
+ failures.push(`\u6E05\u4F1A\u8BDD\u9879\u76EE\u7ED1\u5B9A\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
256078
+ }
256079
+ try {
256080
+ const registry2 = companyId && pgPool ? (await engineRouter.getEngine(companyId)).registry : registryStore;
256081
+ if (registry2.resolveProjectVariables) {
256082
+ for (const row of await registry2.resolveProjectVariables(projectId2)) {
256083
+ await registry2.deleteVariable(row.key, void 0, { projectId: projectId2 });
256084
+ }
256085
+ }
256086
+ } catch (e) {
256087
+ failures.push(`\u6E05\u9879\u76EE\u53D8\u91CF\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
256088
+ }
256089
+ if (failures.length) throw new Error(failures.join("\uFF1B"));
255601
256090
  }
255602
256091
  });
255603
256092
  const currentArtifactProjection = async () => {
@@ -256584,6 +257073,16 @@ async function startServe(opts) {
256584
257073
  if (!stores.artifacts) throw new Error(`[work-order-files] \u516C\u53F8 ${companyId} \u6CA1\u6709 artifacts store\uFF0C\u62D2\u7EDD\u56DE\u843D\u9ED8\u8BA4\u516C\u53F8`);
256585
257074
  return stores.artifacts;
256586
257075
  };
257076
+ const projectStateFor = async (companyId) => {
257077
+ if (!chatStoreRouter) return projectStateStore;
257078
+ const stores = await chatStoreRouter.open(companyId, "work-order-projects");
257079
+ if (!stores.projects) throw new Error(`[work-order-projects] \u516C\u53F8 ${companyId} \u6CA1\u6709 projects store\uFF0C\u62D2\u7EDD\u56DE\u843D\u9ED8\u8BA4\u516C\u53F8`);
257080
+ return stores.projects;
257081
+ };
257082
+ const resolveCompanyCodeRepo = makeCompanyCodeRepoResolver(async (companyId) => ({
257083
+ projects: await projectStateFor(companyId),
257084
+ bindings: await artifactStateFor(companyId)
257085
+ }));
256587
257086
  if (pgDsn && pgPool && chatStoreRouter) {
256588
257087
  const router = chatStoreRouter;
256589
257088
  void (async () => {
@@ -256772,7 +257271,6 @@ async function startServe(opts) {
256772
257271
  }
256773
257272
  return token;
256774
257273
  };
256775
- let resolveCodeRepoRef;
256776
257274
  let coordinatorRef = null;
256777
257275
  let coordinatorSpawnProbe = null;
256778
257276
  const dispatchTimers = /* @__PURE__ */ new Set();
@@ -256932,9 +257430,9 @@ async function startServe(opts) {
256932
257430
  port: opts.port ?? 7320,
256933
257431
  projectState: projectStateStore,
256934
257432
  artifactState: artifactStateStore,
256935
- // 建单交接的 `work_order_files` 按**当前公司**落库;与 collab 读侧 `resolveArtifacts` 同一个闭包。
257433
+ // 建单的项目校验、绑定与输入文件按当前公司落库;与项目/任务读侧使用同一个 router。
256936
257434
  // 装了路由(有 PG)才给——dev 文件模式没有 schema 概念,沿用上面那个单实例。
256937
- ...chatStoreRouter ? { artifactStateFor } : {},
257435
+ ...chatStoreRouter ? { artifactStateFor, projectStateFor } : {},
256938
257436
  schema,
256939
257437
  registry: registryStore,
256940
257438
  // 会话级模型覆盖的候选清单:与 `GET /api/runtimes/:id/models` **同一份**实现
@@ -257004,7 +257502,7 @@ async function startServe(opts) {
257004
257502
  });
257005
257503
  return { plannerActor: planner.id, text: chunks.join("") };
257006
257504
  },
257007
- resolveCodeRepo: (ws) => resolveCodeRepoRef?.(ws) ?? null,
257505
+ resolveCodeRepo: resolveCompanyCodeRepo,
257008
257506
  // propose sha 校验兼镜像根:verifyExternalPinSha 在 ls-remote 未命中(祖先提交)时 `git fetch <remote> <sha>`
257009
257507
  // 进此目录留档(校验即留档,见 git/collect.ts)。目录按需 `init --bare`。(历史:曾是合并器/CI 闸的 git-mirrors 根,已删。)
257010
257508
  gitMirrorRoot: path29.join(opts.dir, "propose-mirrors"),
@@ -257081,8 +257579,8 @@ async function startServe(opts) {
257081
257579
  collabDomain({
257082
257580
  kernel: defaultCompanyKernel,
257083
257581
  oplog,
257084
- blobs,
257085
- // 诊断 3e3219e4-22bb19e4 §②:详情页取 seed brief head 正文作页头真源 + overview 写入口
257582
+ blobs: engine.blobs,
257583
+ // 页头正文读写与内容接口同源:默认公司的事实柜。
257086
257584
  // 资产柜:POST /api/workorders/:id/files 上传字节落这里(与 uploadChatAttachment 同一柜),
257087
257585
  // 再让 artifacts.addWorkOrderFile 写行——手动上传不进 oplog / events,删了不伤重放。
257088
257586
  assets,
@@ -259101,11 +259599,10 @@ async function startServe(opts) {
259101
259599
  for (const w2 of projectsCfg?.workspaces ?? []) {
259102
259600
  await artifactStateStore.upsertWorkspaceBinding({ workOrderId: w2.id, projectId: w2.projectId });
259103
259601
  }
259104
- const { resolveCodeRepo, resolveProject } = makeStoreCodeRepoResolver({
259602
+ const { resolveProject } = makeStoreCodeRepoResolver({
259105
259603
  projects: projectStateStore,
259106
259604
  bindings: artifactStateStore
259107
259605
  });
259108
- resolveCodeRepoRef = resolveCodeRepo;
259109
259606
  if (opts.dispatch) {
259110
259607
  const startDispatcher = (companyId, companyEngine) => {
259111
259608
  if (dispatchers.has(companyId)) return;
@@ -259381,8 +259878,7 @@ async function startServe(opts) {
259381
259878
  } : {},
259382
259879
  schema: liveSchemaMap,
259383
259880
  // 活视图:类型目录变更后由 rebuildLiveSchema 原地重填(Dispatcher 每轮现读 opts.schema)
259384
- resolveCodeRepo,
259385
- // §4/C:异步从权威源(store)解析;dispatcher 装配前 await
259881
+ resolveCodeRepo: (ws) => resolveCompanyCodeRepo(ws, companyId),
259386
259882
  // #2 派单开关:工单"待派发"(控制面旁存标 held)时不派其活。held 集通常空、表极小,每次查可接受。
259387
259883
  resolveDispatchHold: async (ws) => (await artifactStateStore.listWorkspaceDispatchHeld()).includes(ws),
259388
259884
  // §D3 评审人连续性:锁定审核人不可用(熔断/离岗)→ 系统身份幂等升级,交管理者改派(不静默换人)。
@@ -260604,7 +261100,7 @@ ${nodeFault}` : "");
260604
261100
  log: (m2) => console.log(m2),
260605
261101
  batchLimit: envNum("OASIS_AUTOMATION_EVENT_BATCH", 200),
260606
261102
  // 工单→项目绑定是合法薄旁存(B5),不是内核事实——列项目下的工单只认它。
260607
- listProjectWorkspaces: async ({ projectId: projectId2 }) => (await artifactStateStore.listWorkspaceBindings()).filter((b2) => b2.projectId === projectId2).map((b2) => b2.workOrderId),
261103
+ listProjectWorkspaces: async ({ companyId, projectId: projectId2 }) => (await (await artifactStateFor(companyId)).listWorkspaceBindings()).filter((b2) => b2.projectId === projectId2).map((b2) => b2.workOrderId),
260608
261104
  // 完成判据必须复用 D3 resolver(不复制 deriveStage),且与成员快照同一次读取。
260609
261105
  // ensureTracked:内核跟踪集只在启动时装载一次、此后只随本进程写入增长——多实例部署下,
260610
261106
  // 另一实例建出来的工单在这里会被读成 unknown(既不触发也不留痕,QA OBS-1 实测)。
@@ -260615,7 +261111,7 @@ ${nodeFault}` : "");
260615
261111
  return makeAutomationWorkorderSnapshotResolver(engine2.kernel)(workspace);
260616
261112
  },
260617
261113
  // 触发时复查归因主体的项目读取权(与配置期同一口径)。
260618
- projectReadable: async ({ projectId: projectId2 }) => await projectStateStore.getProject(projectId2) !== null
261114
+ projectReadable: async ({ companyId, projectId: projectId2 }) => await (await projectStateFor(companyId)).getProject(projectId2) !== null
260619
261115
  });
260620
261116
  automationEventTimer = setInterval(() => {
260621
261117
  void eventPoller.tick().catch((err) => console.error(`[project-event-poller] tick \u5931\u8D25: ${String(err)}`));
@@ -267844,7 +268340,7 @@ function shimScript() {
267844
268340
  }
267845
268341
 
267846
268342
  // src/index.ts
267847
- var PKG_VERSION = true ? "2.2.0" : "dev";
268343
+ var PKG_VERSION = true ? "2.2.2" : "dev";
267848
268344
  var LOCAL_BIN = localBin();
267849
268345
  var NPM_PREFIX = npmPrefix();
267850
268346
  var INSTANCE = DEFAULT_INSTANCE;