oasis_test_v2 2.2.1 → 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 +664 -195
  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),
@@ -206762,7 +206840,12 @@ var init_memory_registry_store = __esm({
206762
206840
  async putVariable(v2) {
206763
206841
  const mk = this._varKey(v2.key, v2.actorId, v2.projectId);
206764
206842
  const ex = this.variables.get(mk);
206765
- 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
+ });
206766
206849
  }
206767
206850
  async touchVariableUsage(key, actorId, projectId2, when) {
206768
206851
  const mk = this._varKey(key, actorId, projectId2);
@@ -209263,6 +209346,9 @@ function foldSingleNodeTasks(items) {
209263
209346
  }
209264
209347
  return folded;
209265
209348
  }
209349
+ function engineContentKind(k2) {
209350
+ return k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
209351
+ }
209266
209352
  function namedError(name, message) {
209267
209353
  const err = new Error(message);
209268
209354
  err.name = name;
@@ -209484,6 +209570,13 @@ var init_service4 = __esm({
209484
209570
  async listProjects() {
209485
209571
  return [...this.projects.values()].map((project) => ({ ...project }));
209486
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
+ }
209487
209580
  async replaceProjectMembers(projectId2, members) {
209488
209581
  this.members.set(projectId2, new Map(members.map((member) => [member.actorId, cloneProjectMember(member)])));
209489
209582
  }
@@ -209629,6 +209722,7 @@ var init_service4 = __esm({
209629
209722
  }
209630
209723
  projectCreatedHandler;
209631
209724
  onProjectNameChanged;
209725
+ onProjectDeleted;
209632
209726
  /**
209633
209727
  * Cross-domain follow-up for project-scoped resources. Project persistence stays authoritative:
209634
209728
  * a failed follow-up is reported but never turns an already-created project into a false 500.
@@ -209643,6 +209737,10 @@ var init_service4 = __esm({
209643
209737
  setOnProjectNameChanged(handler) {
209644
209738
  this.onProjectNameChanged = handler;
209645
209739
  }
209740
+ /** 删项目后的跨域清理钩子(会话解绑 + 项目变量清理)。装配层注入;缺省 = 无副作用。 */
209741
+ setOnProjectDeleted(handler) {
209742
+ this.onProjectDeleted = handler;
209743
+ }
209646
209744
  /** 工单→项目绑定查询(content-location add 的项目匹配守卫用)。 */
209647
209745
  async getWorkspaceBinding(workOrderId) {
209648
209746
  return this.artifacts.getWorkspaceBinding(workOrderId);
@@ -210031,6 +210129,60 @@ var init_service4 = __esm({
210031
210129
  await this.artifacts.upsertWorkspaceBinding(binding);
210032
210130
  return binding;
210033
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
+ }
210034
210186
  /**
210035
210187
  * 项目关联工单(帧 `1355:4918`):把内部私有的 `listProjectWorkorders` 暴露给路由层。
210036
210188
  * `projectId="proj:uncategorized"` 时聚合所有 `proj_tmp_*` 绑定的工单。
@@ -210140,6 +210292,9 @@ var init_service4 = __esm({
210140
210292
  size: file?.size ?? null,
210141
210293
  taskId,
210142
210294
  taskTitle,
210295
+ // 内容形态/引用只挂在**整份产物**那一行;装箱单摊出来的成员行有自己的后缀,
210296
+ // 按后缀走图标即可,挂上 manifest 反而会把 `Dockerfile` 这种无后缀成员画成压缩包。
210297
+ ...file ? {} : { contentKind: revision.contentKind, contentRef: revision.contentRef },
210143
210298
  creatorKind: creatorId ? creatorId.startsWith("actor:human:") ? "human" : "agent" : null,
210144
210299
  creatorId,
210145
210300
  creatorName: creatorId ? this.resolveActorName(creatorId) : null,
@@ -210294,6 +210449,8 @@ var init_service4 = __esm({
210294
210449
  depth: 2,
210295
210450
  type: artifactType || "",
210296
210451
  size: null,
210452
+ contentKind: engineContentKind(a.contentKind),
210453
+ ...a.contentRef ? { contentRef: a.contentRef } : {},
210297
210454
  taskId: workOrderId,
210298
210455
  taskTitle,
210299
210456
  creatorKind,
@@ -210645,7 +210802,6 @@ var init_service4 = __esm({
210645
210802
  const worksById = new Map(snap.works.map((w2) => [w2.id, w2]));
210646
210803
  const binding = this.artifacts.getWorkspaceBinding ? await this.artifacts.getWorkspaceBinding(workOrderId).catch(() => null) : null;
210647
210804
  const projectId2 = binding?.projectId ?? ephemeralProjectIdFor(workOrderId);
210648
- const mapContentKind = (k2) => k2 === "manifest" ? "manifest" : k2 === "external" ? "external-pin" : "inline-blob";
210649
210805
  const out = [];
210650
210806
  for (const node2 of snap.nodes) {
210651
210807
  if (!node2.latestAcceptId) continue;
@@ -210665,7 +210821,7 @@ var init_service4 = __esm({
210665
210821
  contentRef: a.contentRef,
210666
210822
  title: node2.title || a.name,
210667
210823
  // 节点标题优先;否则用 engine artifact 的 "output" 兜底
210668
- contentKind: mapContentKind(a.contentKind)
210824
+ contentKind: engineContentKind(a.contentKind)
210669
210825
  }));
210670
210826
  out.push({
210671
210827
  outputId: `engine:${acceptedId}`,
@@ -217718,6 +217874,8 @@ ${input.description}
217718
217874
  }
217719
217875
  await this.opts.store.putVariable({
217720
217876
  key: v2.key,
217877
+ // 展示名(子 PRD《密钥管理》§2)。没传就不传下去——存储层按「不动」处理,不抹既有名字。
217878
+ ...v2.name !== void 0 && v2.name !== "" ? { name: v2.name } : {},
217721
217879
  scope: v2.scope,
217722
217880
  ...v2.actorId !== void 0 ? { actorId: v2.actorId } : {},
217723
217881
  ...v2.projectId !== void 0 ? { projectId: v2.projectId } : {},
@@ -217786,6 +217944,7 @@ ${input.description}
217786
217944
  const isPlain = r.valueEncrypted.startsWith("plain:");
217787
217945
  return {
217788
217946
  key: r.key,
217947
+ ...r.name !== void 0 ? { name: r.name } : {},
217789
217948
  scope: r.scope,
217790
217949
  ...r.actorId !== void 0 ? { actorId: r.actorId } : {},
217791
217950
  ...r.projectId !== void 0 ? { projectId: r.projectId } : {},
@@ -219613,6 +219772,8 @@ function actorsDomain(opts) {
219613
219772
  key: b2.key,
219614
219773
  value: b2.value,
219615
219774
  scope,
219775
+ // 展示名(子 PRD《密钥管理》§2「名称」);不传 = 不改既有名字。
219776
+ ...typeof b2.name === "string" && b2.name.trim() ? { name: b2.name.trim() } : {},
219616
219777
  ...scope === "project" ? { projectId: b2.projectId } : {},
219617
219778
  ...b2.deliveryMode !== void 0 ? { deliveryMode: b2.deliveryMode } : {},
219618
219779
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
@@ -219684,6 +219845,8 @@ function actorsDomain(opts) {
219684
219845
  value: b2.value,
219685
219846
  actorId,
219686
219847
  scope: "personal",
219848
+ // 展示名(同上)。组织页「添加密钥 → 新建」走的就是这条端点。
219849
+ ...typeof b2.name === "string" && b2.name.trim() ? { name: b2.name.trim() } : {},
219687
219850
  ...b2.deliveryMode !== void 0 ? { deliveryMode: b2.deliveryMode } : {},
219688
219851
  ...b2.connectorId !== void 0 ? { connectorId: b2.connectorId } : {},
219689
219852
  ...b2.overrides !== void 0 ? { overrides: b2.overrides } : {},
@@ -220740,6 +220903,22 @@ function projectsDomain(opts) {
220740
220903
  throw mapProjectError(err);
220741
220904
  }
220742
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
+ });
220743
220922
  router.get("/api/projects/:id/members", async (req) => {
220744
220923
  try {
220745
220924
  const members = await (await svc(req)).listProjectMembers(req.params.id);
@@ -221162,6 +221341,9 @@ function mapProjectError(err) {
221162
221341
  if (err instanceof Error && err.message.startsWith("project not found:")) {
221163
221342
  return new ApiError(404, "NOT_FOUND", err.message);
221164
221343
  }
221344
+ if (err instanceof Error && err.message.startsWith("project not deletable:")) {
221345
+ return new ApiError(400, "PROJECT_NOT_DELETABLE", err.message);
221346
+ }
221165
221347
  if (err instanceof Error && err.message.startsWith("missing artifact revision registration:")) {
221166
221348
  return new ApiError(400, "MISSING_ARTIFACT_REVISION", err.message);
221167
221349
  }
@@ -221469,6 +221651,10 @@ function toApiProjectFilesView(view) {
221469
221651
  file_path: item.filePath ?? null,
221470
221652
  depth: item.depth,
221471
221653
  type: item.type,
221654
+ // 内容形态 + 内容引用:前端名称列给**没有后缀**的行选图标用(正文→文本、git 提交→代码、
221655
+ // 其它外部引用→链接)。交付物整份产物行才有,上传文件与文件夹为 null。
221656
+ content_kind: item.contentKind ?? null,
221657
+ content_ref: item.contentRef ?? null,
221472
221658
  size: item.size,
221473
221659
  task_id: item.taskId,
221474
221660
  task_title: item.taskTitle,
@@ -221524,6 +221710,10 @@ function createProjectsDomain(opts) {
221524
221710
  const handler = opts.onProjectNameChangedFor(companyId);
221525
221711
  if (handler) svc.setOnProjectNameChanged(handler);
221526
221712
  }
221713
+ if (opts.onProjectDeletedFor) {
221714
+ const handler = opts.onProjectDeletedFor(companyId);
221715
+ if (handler) svc.setOnProjectDeleted(handler);
221716
+ }
221527
221717
  return svc;
221528
221718
  };
221529
221719
  const service = build(void 0, void 0);
@@ -222349,8 +222539,8 @@ var init_offboarding = __esm({
222349
222539
  return { ownedAgents, inFlight };
222350
222540
  }
222351
222541
  /**
222352
- * 现在**能不能直接删**这个成员。DELETE 路由先问它:非空即 409 HANDOFF_REQUIRED。
222353
- * 这是「阻断直接移除」的唯一判据,DELETE 与 commit 共用它,不会两处各判一套。
222542
+ * 现在**能不能直接删**这个成员。DELETE commit 共用它,不会两处各判一套。
222543
+ * 交接删掉之后这里只剩两条:不是成员(NOT_FOUND)、公司最后一位 owner(LAST_OWNER)。
222354
222544
  */
222355
222545
  async blockersForRemoval(companyId, accountId) {
222356
222546
  const blockers = [];
@@ -222533,6 +222723,15 @@ function companiesDomain(opts) {
222533
222723
  if (!agentCredentials) throw new ApiError(501, "NOT_IMPLEMENTED", "\u672C\u90E8\u7F72\u672A\u63A5\u5165 agent \u51ED\u8BC1");
222534
222724
  return agentCredentials;
222535
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
+ };
222536
222735
  const requireIfMatch = (req) => {
222537
222736
  const raw = req.raw.headers["if-match"];
222538
222737
  const text5 = (typeof raw === "string" ? raw : Array.isArray(raw) ? raw[0] : void 0)?.replace(/^"|"$/g, "");
@@ -222648,6 +222847,7 @@ function companiesDomain(opts) {
222648
222847
  throw new ApiError(400, "BAD_REQUEST", "displayName \u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
222649
222848
  }
222650
222849
  member = await service.setMemberDisplayName(cid, aid, b2.displayName).catch(mapErr);
222850
+ await syncActorNameToDisplayName(member, req.auth.actor);
222651
222851
  }
222652
222852
  return { status: 200, body: member };
222653
222853
  });
@@ -224698,6 +224898,8 @@ function buildWorkorderActivity(input) {
224698
224898
  const groupKey = (nodeId) => `${nodeId}#${groupIndexOf.get(nodeId) ?? 0}`;
224699
224899
  const reviewCards = /* @__PURE__ */ new Map();
224700
224900
  const issueCards = /* @__PURE__ */ new Map();
224901
+ const workOfIssue = /* @__PURE__ */ new Map();
224902
+ const plainCommentIssueIds = /* @__PURE__ */ new Set();
224701
224903
  const acceptCards = /* @__PURE__ */ new Map();
224702
224904
  const standalone = [];
224703
224905
  const touch = (d, rec) => {
@@ -224755,10 +224957,22 @@ function buildWorkorderActivity(input) {
224755
224957
  const executorId = w2?.assigneeActorId ?? rec.actorId;
224756
224958
  if (isPendingActor(executorId)) break;
224757
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
+ }
224758
224971
  const reworkReason = str4(reasons.find((r) => r.kind === "rework")?.reason);
224759
224972
  const key = groupKey(nodeId);
224760
224973
  const existingGroup = groupCards.get(key);
224761
224974
  if (existingGroup) {
224975
+ if (!existingGroup.traceWorkIds.includes(workId)) existingGroup.traceWorkIds.push(workId);
224762
224976
  touch(existingGroup, rec);
224763
224977
  existingGroup.executorId = executorId;
224764
224978
  existingGroup.phase = "running";
@@ -224771,6 +224985,7 @@ function buildWorkorderActivity(input) {
224771
224985
  }
224772
224986
  const fresh = {
224773
224987
  id: `work:${workId}`,
224988
+ traceWorkIds: [workId],
224774
224989
  seq: rec.seq,
224775
224990
  at: rec.createdAt,
224776
224991
  updatedAt: rec.createdAt,
@@ -224813,8 +225028,9 @@ function buildWorkorderActivity(input) {
224813
225028
  }
224814
225029
  break;
224815
225030
  }
225031
+ const engineVerdict = workById.get(workId)?.status;
224816
225032
  touch(d, rec);
224817
- if (ev.outcome === "failed") {
225033
+ if (ev.outcome === "failed" && engineVerdict !== "success") {
224818
225034
  d.phase = "stuck";
224819
225035
  d.stuckReason = "failed";
224820
225036
  d.status = ACTIVITY_COPY.failed(nameOf(d.executorId));
@@ -225046,6 +225262,7 @@ function buildWorkorderActivity(input) {
225046
225262
  });
225047
225263
  break;
225048
225264
  }
225265
+ if (!isGap && !isEscalation && !isBackpressure) plainCommentIssueIds.add(issueId);
225049
225266
  const nodeAssignee = nodeId ? nodeById.get(nodeId)?.assigneeActorId : void 0;
225050
225267
  const commentCounterpart = nodeAssignee ?? ownerId;
225051
225268
  const commentTargetId = commentCounterpart && commentCounterpart !== authorId ? commentCounterpart : void 0;
@@ -225134,7 +225351,10 @@ function buildWorkorderActivity(input) {
225134
225351
  d.phase = "done";
225135
225352
  d.actions = [];
225136
225353
  d.executorId = resolverId;
225137
- 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));
225138
225358
  break;
225139
225359
  }
225140
225360
  case "plan.node_retry": {
@@ -225189,12 +225409,37 @@ function buildWorkorderActivity(input) {
225189
225409
  ...issueCards.values(),
225190
225410
  ...acceptCards.values()
225191
225411
  ].sort((a, b2) => a.updatedAt.localeCompare(b2.updatedAt) || a.seq - b2.seq || a.id.localeCompare(b2.id));
225192
- const runIdByTarget = input.runIdByTarget;
225193
- const runIdOfCard = (cardId) => {
225194
- if (!runIdByTarget) return void 0;
225195
- if (cardId.startsWith("work:")) return runIdByTarget.get(`work:${cardId.slice("work:".length)}`);
225196
- if (cardId.startsWith("review:")) return runIdByTarget.get(`review:${cardId.slice("review:".length)}`);
225197
- 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;
225198
225443
  };
225199
225444
  const cards = drafts.map((d) => ({
225200
225445
  id: d.id,
@@ -225209,7 +225454,8 @@ function buildWorkorderActivity(input) {
225209
225454
  ...d.detail ? { detail: d.detail } : {},
225210
225455
  artifacts: d.artifacts,
225211
225456
  actions: d.actions,
225212
- .../* @__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))
225213
225459
  }));
225214
225460
  return { workorderId, cards, truncated };
225215
225461
  }
@@ -225218,6 +225464,7 @@ var init_activity2 = __esm({
225218
225464
  "../server/src/domains/collab/activity.ts"() {
225219
225465
  "use strict";
225220
225466
  init_src();
225467
+ init_src4();
225221
225468
  init_workorder_manager();
225222
225469
  ACTIVITY_EVENT_KINDS = [
225223
225470
  "plan.changed",
@@ -225347,6 +225594,14 @@ var init_activity2 = __esm({
225347
225594
  * 留言 / 回压收口——**同样一句索引**。收口 note 与开口正文同源、同样能长到几千字:
225348
225595
  * 全库实测 `ws:wo-56bba3c1` 那条收口后小状态仍是 **8096 字**。全文去留言线看。
225349
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`,
225350
225605
  commentResolved: (by, node2) => `${by} \u5DF2\u5904\u7406\u300A${node2}\u300B\u7684\u7559\u8A00\u3002`,
225351
225606
  issueResolved: (note) => note
225352
225607
  };
@@ -225370,6 +225625,125 @@ function targetOf(cardId) {
225370
225625
  return null;
225371
225626
  }
225372
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) {
225373
225747
  const { workorderId, cardId, snap } = input;
225374
225748
  const ref2 = input.ref ?? ((id) => ({ id }));
225375
225749
  const empty2 = { workorderId, cardId, attempts: [] };
@@ -225382,26 +225756,6 @@ function buildWorkorderActivityTrace(input) {
225382
225756
  const nodeId = work?.nodeId ?? review?.nodeId;
225383
225757
  if (!nodeId) return empty2;
225384
225758
  const node2 = nodeById.get(nodeId);
225385
- const nodesWithInEdges = new Set(snap.edges.map((e) => e.toNodeId));
225386
- const isRootBrief = node2?.type === "brief" && !nodesWithInEdges.has(nodeId);
225387
- const artifactsOfWork = (workId) => {
225388
- const list2 = [];
225389
- for (const a of snap.artifacts) {
225390
- if (a.workId !== workId) continue;
225391
- const artifactNodeId = a.nodeId;
225392
- const files = a.contentKind === "manifest" ? input.manifestFiles?.get(a.contentRef) : void 0;
225393
- if (files && files.length > 0) {
225394
- for (const path41 of files) {
225395
- list2.push({ id: a.id, name: path41.split("/").pop() || path41, path: path41, nodeId: artifactNodeId });
225396
- }
225397
- } else if (isRootBrief) {
225398
- list2.push({ id: a.id, name: "\u4EFB\u52A1\u76EE\u6807", path: "\u4EFB\u52A1\u76EE\u6807.md", nodeId: artifactNodeId });
225399
- } else {
225400
- list2.push({ id: a.id, name: a.name, nodeId: artifactNodeId });
225401
- }
225402
- }
225403
- return list2;
225404
- };
225405
225759
  const managerId = resolveWorkorderManager(snap) ?? snap.workorder.ownerActorId;
225406
225760
  const startedAt = work?.createdAt ?? review?.createdAt;
225407
225761
  const spec = node2?.spec?.trim();
@@ -225411,18 +225765,18 @@ function buildWorkorderActivityTrace(input) {
225411
225765
  nodeTitle: node2?.title || nodeId.split(":").slice(2).join(":") || nodeId,
225412
225766
  ...spec ? { spec } : {}
225413
225767
  };
225414
- 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));
225415
225769
  const executorId = work?.assigneeActorId ?? review?.reviewerActorId;
225416
225770
  const conclusion = work?.conclusion?.trim() || review?.note?.trim() || void 0;
225417
225771
  const attempts = rows.map((d, i) => {
225418
225772
  const isLast = i === rows.length - 1;
225419
- const actorId = executorId ?? d.actorId;
225773
+ const actorId = d.actorId ?? executorId;
225420
225774
  return {
225421
225775
  runId: d.id,
225422
225776
  at: d.createdAt,
225423
225777
  actor: ref2(actorId),
225424
225778
  ...isLast && conclusion ? { conclusion } : {},
225425
- artifacts: isLast && work ? artifactsOfWork(work.id) : []
225779
+ artifacts: isLast && work ? artifactsOfWork(work, input) : []
225426
225780
  };
225427
225781
  });
225428
225782
  return { workorderId, cardId, briefing, attempts };
@@ -225430,6 +225784,8 @@ function buildWorkorderActivityTrace(input) {
225430
225784
  var init_activity_trace = __esm({
225431
225785
  "../server/src/domains/collab/activity-trace.ts"() {
225432
225786
  "use strict";
225787
+ init_src4();
225788
+ init_activity2();
225433
225789
  init_workorder_manager();
225434
225790
  }
225435
225791
  });
@@ -227283,10 +227639,7 @@ function collabDomain(opts) {
227283
227639
  const hit = cache.get(companyId);
227284
227640
  if (hit) return hit;
227285
227641
  const e = await opts.resolveEngine(companyId);
227286
- if (e.kernel === opts.kernel) {
227287
- cache.set(companyId, defaultCtx);
227288
- return defaultCtx;
227289
- }
227642
+ const artifacts = opts.resolveArtifacts ? await opts.resolveArtifacts(companyId) : e.kernel === opts.kernel ? opts.artifacts : void 0;
227290
227643
  const ctx = {
227291
227644
  kernel: e.kernel,
227292
227645
  oplog: e.oplog,
@@ -227296,7 +227649,7 @@ function collabDomain(opts) {
227296
227649
  // ★ 产物旁存也要按公司取(见 resolveArtifacts 的注释):漏了这一行,非默认公司的
227297
227650
  // `/api/workorders/:id/files` 与 node_output 清单恒空——写侧写对了、读侧根本没接线。
227298
227651
  // 抛出来就抛出来:宁可 500 说"这家公司的数据面解析不了",也不能回落默认公司的库。
227299
- ...opts.resolveArtifacts ? { artifacts: await opts.resolveArtifacts(companyId) } : {}
227652
+ ...artifacts ? { artifacts } : {}
227300
227653
  };
227301
227654
  cache.set(companyId, ctx);
227302
227655
  return ctx;
@@ -227550,7 +227903,10 @@ function collabDomain(opts) {
227550
227903
  ...extra?.avatar ? { avatar: extra.avatar } : {}
227551
227904
  };
227552
227905
  };
227553
- 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, []];
227554
227910
  const manifestFiles = snap ? await resolveActivityManifestFiles(snap.artifacts, blobs) : void 0;
227555
227911
  const dispatches = await listDispatchRows(workorderId, opts.dispatchesOfWorkorder);
227556
227912
  return {
@@ -227559,6 +227915,7 @@ function collabDomain(opts) {
227559
227915
  workorderId,
227560
227916
  cardId,
227561
227917
  snap,
227918
+ events,
227562
227919
  ref: ref2,
227563
227920
  ...dispatches ? { dispatches } : {},
227564
227921
  ...manifestFiles ? { manifestFiles } : {}
@@ -231108,42 +231465,17 @@ function mapChatItemToSnapshotEntry(row) {
231108
231465
  itemType,
231109
231466
  role,
231110
231467
  status,
231111
- itemVersion: row.turnVersion,
231468
+ // ADR-0510 附录 A:版本轴 = 会话逻辑时钟(`chat_items.version`)。
231469
+ // 此前这里填的是 `turnVersion`(轮级 resync 游标),而增量流填的是进程内每 item 计数——
231470
+ // 两套编号空间,一轮中途拉快照就把随后每一帧判 stale,进 resync 死循环。
231471
+ itemVersion: row.version,
231112
231472
  seq: row.seq,
231473
+ ord: row.ord,
231113
231474
  messageId: row.messageId,
231114
231475
  updatedAt: row.updatedAt,
231115
231476
  payload: row.payload
231116
231477
  };
231117
231478
  }
231118
- function synthesizeMessageItem(msg, seq) {
231119
- const attachments = Array.isArray(msg.attachments) ? msg.attachments.map((a) => ({
231120
- name: a.name,
231121
- ...a.blobRef !== void 0 ? { blobRef: a.blobRef } : {},
231122
- ...a.contentType !== void 0 ? { contentType: a.contentType } : {}
231123
- })) : void 0;
231124
- const clientSubmitId = msg.role === "user" ? msg.clientSubmitId ?? clientSubmitIdFromMessageKey(msg.messageKey) : void 0;
231125
- const role = msg.role === "user" || msg.role === "system" ? msg.role : "assistant";
231126
- const status = msg.status === "running" ? "streaming" : msg.status === "error" ? "failed" : "completed";
231127
- return {
231128
- // ADR-0510 D1:有 csid 就用**与账本 user item、前端乐观桶位同名**的身份;没有(历史行 / 老客户端)
231129
- // 才退回合成 id。三方同名,桶按 itemId 合并才闭环——否则同一条 user 消息画两个气泡。
231130
- itemId: clientSubmitId ? optimisticUserItemId(clientSubmitId) : `chat-message:${msg.id}`,
231131
- turnId: msg.runId ?? `chat-message-turn:${msg.id}`,
231132
- itemType: "message",
231133
- role,
231134
- status,
231135
- itemVersion: 0,
231136
- seq,
231137
- messageId: msg.id,
231138
- updatedAt: msg.completedAt ?? msg.createdAt,
231139
- payload: {
231140
- role,
231141
- text: msg.content ?? "",
231142
- ...attachments?.length ? { attachments } : {},
231143
- ...clientSubmitId ? { clientSubmitId } : {}
231144
- }
231145
- };
231146
- }
231147
231479
  function throwWorkdirError(code2) {
231148
231480
  switch (code2) {
231149
231481
  case "PATH_ESCAPE":
@@ -231505,57 +231837,50 @@ function createChatSessionsDomain(opts) {
231505
231837
  if (!session || session.humanActorId !== req.auth.actor) throw new ApiError(404, "NOT_FOUND", "session not found");
231506
231838
  const chatSessionId = req.params.id;
231507
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;
231508
231844
  const cursor = opts.liveChat?.cursorFor(chatSessionId) ?? null;
231509
231845
  let turnId = cursor?.turnId ?? null;
231510
- let inflightRunId = opts.liveChat?.runFor(chatSessionId) ?? null;
231511
231846
  if (!turnId) {
231512
231847
  const turns = await turnsFor(req);
231513
231848
  const active = turns ? await turns.activeTurn(chatSessionId).catch(() => null) : null;
231514
231849
  turnId = active?.id ?? null;
231515
- inflightRunId ??= active?.assistantRunId ?? null;
231516
231850
  }
231517
231851
  const snapshotEntries = [];
231518
231852
  let nextSinceVersion = sinceVersion;
231519
- if (turnId) {
231520
- 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;
231521
231868
  for (const row of rows) {
231522
231869
  snapshotEntries.push(mapChatItemToSnapshotEntry(row));
231523
231870
  if (row.turnVersion > nextSinceVersion) nextSinceVersion = row.turnVersion;
231524
231871
  }
231525
231872
  }
231526
- const activeTurnItemIds = new Set(snapshotEntries.map((e) => e.itemId));
231527
- const HISTORY_TURNS = 12;
231528
- const historicalEntries = [];
231529
- if (sinceVersion === 0) {
231530
- const listRecentTurns2 = store2.listRecentTurns;
231531
- const messages = listRecentTurns2 ? (await listRecentTurns2.call(store2, chatSessionId, HISTORY_TURNS).catch(() => ({ messages: [], hasMoreBefore: false }))).messages : windowMessagesByTurns(await store2.listMessages(chatSessionId).catch(() => []), HISTORY_TURNS).messages;
231532
- let historySeq = 0;
231533
- for (const msg of messages) {
231534
- if (!msg.id) continue;
231535
- const rows = await items.listItemsByMessage(msg.id).catch(() => []);
231536
- const historical = turnId ? rows.filter((row) => row.turnId !== turnId) : rows;
231537
- if (rows.length > 0) {
231538
- for (const row of [...historical].sort((a, b2) => a.seq - b2.seq)) {
231539
- historySeq += 1;
231540
- historicalEntries.push({ ...mapChatItemToSnapshotEntry(row), seq: historySeq });
231541
- }
231542
- continue;
231543
- }
231544
- if (msg.role === "assistant" && msg.status === "running" && inflightRunId && msg.runId === inflightRunId) continue;
231545
- const synthesized = synthesizeMessageItem(msg, historySeq + 1);
231546
- if (activeTurnItemIds.has(synthesized.itemId)) continue;
231547
- historySeq += 1;
231548
- historicalEntries.push(synthesized);
231549
- }
231550
- }
231873
+ const oldestOrd = snapshotEntries.find((e) => e.ord !== null)?.ord ?? null;
231551
231874
  const body2 = {
231552
231875
  chatSessionId,
231553
231876
  turnId,
231554
- // 历史合成条在前,active turn chat_items 在后——顺序即桶排序,桶按 seq 单调排列。
231555
- items: [...historicalEntries, ...snapshotEntries],
231877
+ // 已按 ord 升序——服务端给什么顺序,客户端就按 ord 排出什么顺序,两边同一把尺子。
231878
+ items: snapshotEntries,
231556
231879
  nextSinceVersion,
231557
231880
  frameCursor: cursor ? { epoch: cursor.epoch, seq: cursor.seq } : { epoch: 0, seq: 0 },
231558
- canAppend: cursor?.canAppend ?? false
231881
+ canAppend: cursor?.canAppend ?? false,
231882
+ oldestOrd,
231883
+ hasMoreBefore
231559
231884
  };
231560
231885
  return { status: 200, body: body2 };
231561
231886
  });
@@ -231908,7 +232233,7 @@ function createChatSessionsDomain(opts) {
231908
232233
  });
231909
232234
  };
231910
232235
  }
231911
- var import_node_crypto59, WORKDIR_READ_MAX_BYTES;
232236
+ var import_node_crypto59, DEFAULT_SNAPSHOT_ITEMS, MAX_SNAPSHOT_ITEMS, WORKDIR_READ_MAX_BYTES;
231912
232237
  var init_chat_sessions = __esm({
231913
232238
  "../server/src/domains/chat-sessions/index.ts"() {
231914
232239
  "use strict";
@@ -231929,6 +232254,8 @@ var init_chat_sessions = __esm({
231929
232254
  init_workorder_terminal();
231930
232255
  init_session_workdir();
231931
232256
  init_project_bundle();
232257
+ DEFAULT_SNAPSHOT_ITEMS = 400;
232258
+ MAX_SNAPSHOT_ITEMS = 1e3;
231932
232259
  WORKDIR_READ_MAX_BYTES = 2 * 1024 * 1024;
231933
232260
  }
231934
232261
  });
@@ -236591,6 +236918,9 @@ function makeStoreCodeRepoResolver(deps) {
236591
236918
  }
236592
236919
  };
236593
236920
  }
236921
+ function makeCompanyCodeRepoResolver(storesFor) {
236922
+ return async (ws, companyId) => makeStoreCodeRepoResolver(await storesFor(companyId)).resolveCodeRepo(ws);
236923
+ }
236594
236924
  var init_resolve = __esm({
236595
236925
  "../server/src/git/resolve.ts"() {
236596
236926
  "use strict";
@@ -237541,16 +237871,28 @@ var init_fused_signal_router = __esm({
237541
237871
  }
237542
237872
  });
237543
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
+
237544
237885
  // ../storage/src/postgres.ts
237545
237886
  var import_node_crypto67, ident3, isUniqueViolation, PostgresOplogStore, PostgresBlobStore;
237546
237887
  var init_postgres = __esm({
237547
237888
  "../storage/src/postgres.ts"() {
237548
237889
  "use strict";
237549
237890
  import_node_crypto67 = require("node:crypto");
237891
+ init_pg_ident();
237550
237892
  init_esm();
237551
237893
  init_src();
237552
237894
  ident3 = (s2) => {
237553
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema identifier: ${s2}`);
237895
+ quoteIdent2(s2);
237554
237896
  return s2;
237555
237897
  };
237556
237898
  isUniqueViolation = (err) => typeof err === "object" && err !== null && err.code === "23505";
@@ -237772,7 +238114,9 @@ function rowToVariable(row) {
237772
238114
  updatedAt: new Date(row.updated_at).toISOString(),
237773
238115
  // ADR 凭据保险库补章 §D2.1:到期/取用时间戳(text 列存 ISO 字符串,原样带出;null/空 = undefined)。
237774
238116
  ...row.expires_at ? { expiresAt: row.expires_at } : {},
237775
- ...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 } : {}
237776
238120
  };
237777
238121
  }
237778
238122
  var ident4, PostgresRegistryStore, rowToActor, rowToConfig, rowToAgentPrefs;
@@ -237780,9 +238124,10 @@ var init_postgres_registry = __esm({
237780
238124
  "../storage/src/postgres-registry.ts"() {
237781
238125
  "use strict";
237782
238126
  init_esm();
238127
+ init_pg_ident();
237783
238128
  init_src();
237784
238129
  ident4 = (s2) => {
237785
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
238130
+ quoteIdent2(s2);
237786
238131
  return s2;
237787
238132
  };
237788
238133
  PostgresRegistryStore = class _PostgresRegistryStore {
@@ -237938,6 +238283,7 @@ var init_postgres_registry = __esm({
237938
238283
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS project_id text`);
237939
238284
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS environment text`);
237940
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`);
237941
238287
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS expires_at text`);
237942
238288
  await pool.query(`ALTER TABLE IF EXISTS "${s2}".variables ADD COLUMN IF NOT EXISTS last_used_at text`);
237943
238289
  await pool.query(`DROP INDEX IF EXISTS "${s2}"."${s2}_variables_key_actor_uq"`);
@@ -238436,10 +238782,13 @@ var init_postgres_registry = __esm({
238436
238782
  /* ---------- 变量 ---------- */
238437
238783
  async putVariable(v2) {
238438
238784
  await this.pool.query(
238439
- `INSERT INTO ${this.s}.variables (key, scope, actor_id, project_id, connector_id, overrides, delivery_mode, value_encrypted, updated_at, expires_at)
238440
- 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)
238441
238789
  ON CONFLICT (key, COALESCE(actor_id, ''), COALESCE(project_id, '')) DO UPDATE
238442
- 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)`,
238443
238792
  [
238444
238793
  v2.key,
238445
238794
  v2.scope,
@@ -238450,7 +238799,8 @@ var init_postgres_registry = __esm({
238450
238799
  v2.deliveryMode ?? null,
238451
238800
  v2.valueEncrypted,
238452
238801
  v2.updatedAt,
238453
- v2.expiresAt ?? null
238802
+ v2.expiresAt ?? null,
238803
+ v2.name ?? null
238454
238804
  ]
238455
238805
  );
238456
238806
  }
@@ -238546,17 +238896,6 @@ var init_postgres_registry = __esm({
238546
238896
  }
238547
238897
  });
238548
238898
 
238549
- // ../storage/src/pg-ident.ts
238550
- function quoteIdent2(name) {
238551
- if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(name)) throw new Error(`invalid schema name: ${name}`);
238552
- return `"${name}"`;
238553
- }
238554
- var init_pg_ident = __esm({
238555
- "../storage/src/pg-ident.ts"() {
238556
- "use strict";
238557
- }
238558
- });
238559
-
238560
238899
  // ../storage/src/postgres-assistant-store.ts
238561
238900
  var ASSISTANT_LOCK_NAMESPACE, rowToBinding, rowToHistory, PostgresAssistantBindStore, PostgresHumanPrefsStore;
238562
238901
  var init_postgres_assistant_store = __esm({
@@ -238865,8 +239204,9 @@ var init_postgres_channels = __esm({
238865
239204
  "../storage/src/postgres-channels.ts"() {
238866
239205
  "use strict";
238867
239206
  init_esm();
239207
+ init_pg_ident();
238868
239208
  ident5 = (s2) => {
238869
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
239209
+ quoteIdent2(s2);
238870
239210
  return s2;
238871
239211
  };
238872
239212
  PostgresChannelStore = class _PostgresChannelStore {
@@ -239140,6 +239480,24 @@ var init_postgres_projects = __esm({
239140
239480
  const r = await this.pool.query(`SELECT * FROM ${this.s}.projects ORDER BY created_at`);
239141
239481
  return r.rows.map(rowToProject);
239142
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
+ }
239143
239501
  async replaceProjectMembers(projectId2, members) {
239144
239502
  const client = await this.pool.connect();
239145
239503
  try {
@@ -239934,8 +240292,9 @@ var init_postgres_control_plane = __esm({
239934
240292
  "../storage/src/postgres-control-plane.ts"() {
239935
240293
  "use strict";
239936
240294
  init_esm();
240295
+ init_pg_ident();
239937
240296
  ident7 = (s2) => {
239938
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
240297
+ quoteIdent2(s2);
239939
240298
  return s2;
239940
240299
  };
239941
240300
  PostgresControlPlaneStore = class _PostgresControlPlaneStore {
@@ -240860,11 +241219,12 @@ var ident8, mergeContinuityRefs3, PostgresTraceStore, rowToRun, terminalHandoffO
240860
241219
  var init_postgres_trace = __esm({
240861
241220
  "../storage/src/postgres-trace.ts"() {
240862
241221
  "use strict";
241222
+ init_pg_ident();
240863
241223
  init_esm();
240864
241224
  init_src();
240865
241225
  init_pg_sanitize();
240866
241226
  ident8 = (s2) => {
240867
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
241227
+ quoteIdent2(s2);
240868
241228
  return s2;
240869
241229
  };
240870
241230
  mergeContinuityRefs3 = (...lists) => {
@@ -242849,10 +243209,11 @@ var init_postgres_automations = __esm({
242849
243209
  "../storage/src/postgres-automations.ts"() {
242850
243210
  "use strict";
242851
243211
  init_esm();
243212
+ init_pg_ident();
242852
243213
  init_postgres_chat_turns();
242853
243214
  init_src();
242854
243215
  ident10 = (s2) => {
242855
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
243216
+ quoteIdent2(s2);
242856
243217
  return s2;
242857
243218
  };
242858
243219
  genId = (prefix) => `${prefix}_${Math.random().toString(36).slice(2, 12)}`;
@@ -244091,6 +244452,15 @@ var init_postgres_chat_sessions = __esm({
244091
244452
  args
244092
244453
  );
244093
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
+ }
244094
244464
  async deleteSession(id) {
244095
244465
  if (!await this.scopedSessionExists(id)) return;
244096
244466
  await this.pool.query(`DELETE FROM ${this.s}.chat_messages WHERE session_id = $1`, [id]);
@@ -244914,12 +245284,15 @@ var init_postgres_chat_items = __esm({
244914
245284
  const r = await this.pool.query(
244915
245285
  `INSERT INTO ${this.s}.chat_items
244916
245286
  (id, session_id, message_id, turn_id, run_id, seq, kind, role, status,
244917
- 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)
244918
245288
  SELECT $1, $2, $3, $4, $5,
244919
245289
  COALESCE(MAX(c.seq), 0) + 1,
244920
- $6, $7, $8::text, 0,
245290
+ $6, $7, $8::text, COALESCE($13::bigint, 0),
244921
245291
  COALESCE(MAX(c.turn_version), 0) + 1,
244922
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)),
244923
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
244924
245297
  -- \uFF08\u5217\u4F4D\u662F timestamptz/text\u3001CASE \u91CC\u662F unknown\uFF09\uFF0C\u76F4\u63A5\u62A5
244925
245298
  -- inconsistent types deduced for parameter $12\u3002
@@ -244939,7 +245312,9 @@ var init_postgres_chat_items = __esm({
244939
245312
  scrubPgString(input.providerItemKey ?? null),
244940
245313
  payload,
244941
245314
  metadata,
244942
- at
245315
+ at,
245316
+ input.version ?? null,
245317
+ input.ord ?? null
244943
245318
  ]
244944
245319
  );
244945
245320
  return rowToItem(r.rows[0]);
@@ -244977,10 +245352,13 @@ var init_postgres_chat_items = __esm({
244977
245352
  setExpr = `status = $3::text, completed_at = CASE WHEN $3 IN ('completed','failed','interrupted') THEN $2::timestamptz ELSE c.completed_at END`;
244978
245353
  break;
244979
245354
  }
245355
+ const versionParamIndex = args.length + 1;
245356
+ args.push(opts?.version ?? null);
244980
245357
  const r = await this.pool.query(
244981
245358
  `UPDATE ${this.s}.chat_items c
244982
245359
  SET ${setExpr},
244983
- version = c.version + 1,
245360
+ version = CASE WHEN $${versionParamIndex}::bigint IS NULL THEN c.version + 1
245361
+ ELSE GREATEST(c.version, $${versionParamIndex}::bigint) END,
244984
245362
  turn_version = ${bumpTurnVersion},
244985
245363
  updated_at = $2
244986
245364
  WHERE c.id = $1
@@ -245028,6 +245406,49 @@ var init_postgres_chat_items = __esm({
245028
245406
  );
245029
245407
  return r.rows.map((row) => rowToItem(row));
245030
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
+ }
245031
245452
  async turnCursor(sessionId, turnId) {
245032
245453
  const r = await this.pool.query(
245033
245454
  `SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq, COALESCE(MAX(turn_version), 0) + 1 AS next_turn_version
@@ -245364,8 +245785,14 @@ function planSessionOrd(sessionId, messages, items, turns, opts) {
245364
245785
  }
245365
245786
  async function listChatItemsSchemas(pool) {
245366
245787
  const r = await pool.query(
245367
- `SELECT n.nspname AS s FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
245368
- 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`
245369
245796
  );
245370
245797
  return r.rows.map((x2) => x2.s);
245371
245798
  }
@@ -245959,9 +246386,10 @@ var init_postgres_nodes = __esm({
245959
246386
  "../storage/src/postgres-nodes.ts"() {
245960
246387
  "use strict";
245961
246388
  import_node_crypto68 = require("node:crypto");
246389
+ init_pg_ident();
245962
246390
  init_esm();
245963
246391
  ident12 = (s2) => {
245964
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
246392
+ quoteIdent2(s2);
245965
246393
  return s2;
245966
246394
  };
245967
246395
  PostgresNodeStore = class _PostgresNodeStore {
@@ -246226,8 +246654,9 @@ var init_postgres_model_prices = __esm({
246226
246654
  "../storage/src/postgres-model-prices.ts"() {
246227
246655
  "use strict";
246228
246656
  init_esm();
246657
+ init_pg_ident();
246229
246658
  ident13 = (s2) => {
246230
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
246659
+ quoteIdent2(s2);
246231
246660
  return s2;
246232
246661
  };
246233
246662
  PostgresModelPriceStore = class _PostgresModelPriceStore {
@@ -246437,10 +246866,11 @@ var init_postgres_actor_memory = __esm({
246437
246866
  "../storage/src/postgres-actor-memory.ts"() {
246438
246867
  "use strict";
246439
246868
  import_node_crypto69 = require("node:crypto");
246869
+ init_pg_ident();
246440
246870
  init_src();
246441
246871
  matchClause = (q2) => q2.requireMatch && q2.keywords.length > 0 ? " WHERE m > 0" : "";
246442
246872
  ident14 = (s2) => {
246443
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
246873
+ quoteIdent2(s2);
246444
246874
  return s2;
246445
246875
  };
246446
246876
  PostgresActorMemoryStore = class _PostgresActorMemoryStore {
@@ -246746,9 +247176,10 @@ var init_postgres_inbox_read = __esm({
246746
247176
  "../storage/src/postgres-inbox-read.ts"() {
246747
247177
  "use strict";
246748
247178
  init_esm();
247179
+ init_pg_ident();
246749
247180
  SEED_MARKER_ACTOR = "actor:system";
246750
247181
  ident15 = (s2) => {
246751
- if (!/^[a-z_][a-z0-9_]*$/.test(s2)) throw new Error(`invalid schema name: ${s2}`);
247182
+ quoteIdent2(s2);
246752
247183
  return s2;
246753
247184
  };
246754
247185
  PostgresReadMarkerStore = class _PostgresReadMarkerStore {
@@ -252000,10 +252431,11 @@ var init_postgres_knowledge = __esm({
252000
252431
  "../storage/src/postgres-knowledge.ts"() {
252001
252432
  "use strict";
252002
252433
  init_esm();
252434
+ init_pg_ident();
252003
252435
  init_pg_sanitize();
252004
252436
  init_knowledge_snapshot_crypto();
252005
252437
  ident17 = (value2) => {
252006
- if (!/^[a-z_][a-z0-9_]*$/.test(value2)) throw new Error(`invalid schema name: ${value2}`);
252438
+ quoteIdent2(value2);
252007
252439
  return value2;
252008
252440
  };
252009
252441
  TABLES = [
@@ -255625,6 +256057,36 @@ async function startServe(opts) {
255625
256057
  projectId2,
255626
256058
  afterName
255627
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"));
255628
256090
  }
255629
256091
  });
255630
256092
  const currentArtifactProjection = async () => {
@@ -256611,6 +257073,16 @@ async function startServe(opts) {
256611
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`);
256612
257074
  return stores.artifacts;
256613
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
+ }));
256614
257086
  if (pgDsn && pgPool && chatStoreRouter) {
256615
257087
  const router = chatStoreRouter;
256616
257088
  void (async () => {
@@ -256799,7 +257271,6 @@ async function startServe(opts) {
256799
257271
  }
256800
257272
  return token;
256801
257273
  };
256802
- let resolveCodeRepoRef;
256803
257274
  let coordinatorRef = null;
256804
257275
  let coordinatorSpawnProbe = null;
256805
257276
  const dispatchTimers = /* @__PURE__ */ new Set();
@@ -256959,9 +257430,9 @@ async function startServe(opts) {
256959
257430
  port: opts.port ?? 7320,
256960
257431
  projectState: projectStateStore,
256961
257432
  artifactState: artifactStateStore,
256962
- // 建单交接的 `work_order_files` 按**当前公司**落库;与 collab 读侧 `resolveArtifacts` 同一个闭包。
257433
+ // 建单的项目校验、绑定与输入文件按当前公司落库;与项目/任务读侧使用同一个 router。
256963
257434
  // 装了路由(有 PG)才给——dev 文件模式没有 schema 概念,沿用上面那个单实例。
256964
- ...chatStoreRouter ? { artifactStateFor } : {},
257435
+ ...chatStoreRouter ? { artifactStateFor, projectStateFor } : {},
256965
257436
  schema,
256966
257437
  registry: registryStore,
256967
257438
  // 会话级模型覆盖的候选清单:与 `GET /api/runtimes/:id/models` **同一份**实现
@@ -257031,7 +257502,7 @@ async function startServe(opts) {
257031
257502
  });
257032
257503
  return { plannerActor: planner.id, text: chunks.join("") };
257033
257504
  },
257034
- resolveCodeRepo: (ws) => resolveCodeRepoRef?.(ws) ?? null,
257505
+ resolveCodeRepo: resolveCompanyCodeRepo,
257035
257506
  // propose sha 校验兼镜像根:verifyExternalPinSha 在 ls-remote 未命中(祖先提交)时 `git fetch <remote> <sha>`
257036
257507
  // 进此目录留档(校验即留档,见 git/collect.ts)。目录按需 `init --bare`。(历史:曾是合并器/CI 闸的 git-mirrors 根,已删。)
257037
257508
  gitMirrorRoot: path29.join(opts.dir, "propose-mirrors"),
@@ -257108,8 +257579,8 @@ async function startServe(opts) {
257108
257579
  collabDomain({
257109
257580
  kernel: defaultCompanyKernel,
257110
257581
  oplog,
257111
- blobs,
257112
- // 诊断 3e3219e4-22bb19e4 §②:详情页取 seed brief head 正文作页头真源 + overview 写入口
257582
+ blobs: engine.blobs,
257583
+ // 页头正文读写与内容接口同源:默认公司的事实柜。
257113
257584
  // 资产柜:POST /api/workorders/:id/files 上传字节落这里(与 uploadChatAttachment 同一柜),
257114
257585
  // 再让 artifacts.addWorkOrderFile 写行——手动上传不进 oplog / events,删了不伤重放。
257115
257586
  assets,
@@ -259128,11 +259599,10 @@ async function startServe(opts) {
259128
259599
  for (const w2 of projectsCfg?.workspaces ?? []) {
259129
259600
  await artifactStateStore.upsertWorkspaceBinding({ workOrderId: w2.id, projectId: w2.projectId });
259130
259601
  }
259131
- const { resolveCodeRepo, resolveProject } = makeStoreCodeRepoResolver({
259602
+ const { resolveProject } = makeStoreCodeRepoResolver({
259132
259603
  projects: projectStateStore,
259133
259604
  bindings: artifactStateStore
259134
259605
  });
259135
- resolveCodeRepoRef = resolveCodeRepo;
259136
259606
  if (opts.dispatch) {
259137
259607
  const startDispatcher = (companyId, companyEngine) => {
259138
259608
  if (dispatchers.has(companyId)) return;
@@ -259408,8 +259878,7 @@ async function startServe(opts) {
259408
259878
  } : {},
259409
259879
  schema: liveSchemaMap,
259410
259880
  // 活视图:类型目录变更后由 rebuildLiveSchema 原地重填(Dispatcher 每轮现读 opts.schema)
259411
- resolveCodeRepo,
259412
- // §4/C:异步从权威源(store)解析;dispatcher 装配前 await
259881
+ resolveCodeRepo: (ws) => resolveCompanyCodeRepo(ws, companyId),
259413
259882
  // #2 派单开关:工单"待派发"(控制面旁存标 held)时不派其活。held 集通常空、表极小,每次查可接受。
259414
259883
  resolveDispatchHold: async (ws) => (await artifactStateStore.listWorkspaceDispatchHeld()).includes(ws),
259415
259884
  // §D3 评审人连续性:锁定审核人不可用(熔断/离岗)→ 系统身份幂等升级,交管理者改派(不静默换人)。
@@ -260631,7 +261100,7 @@ ${nodeFault}` : "");
260631
261100
  log: (m2) => console.log(m2),
260632
261101
  batchLimit: envNum("OASIS_AUTOMATION_EVENT_BATCH", 200),
260633
261102
  // 工单→项目绑定是合法薄旁存(B5),不是内核事实——列项目下的工单只认它。
260634
- 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),
260635
261104
  // 完成判据必须复用 D3 resolver(不复制 deriveStage),且与成员快照同一次读取。
260636
261105
  // ensureTracked:内核跟踪集只在启动时装载一次、此后只随本进程写入增长——多实例部署下,
260637
261106
  // 另一实例建出来的工单在这里会被读成 unknown(既不触发也不留痕,QA OBS-1 实测)。
@@ -260642,7 +261111,7 @@ ${nodeFault}` : "");
260642
261111
  return makeAutomationWorkorderSnapshotResolver(engine2.kernel)(workspace);
260643
261112
  },
260644
261113
  // 触发时复查归因主体的项目读取权(与配置期同一口径)。
260645
- projectReadable: async ({ projectId: projectId2 }) => await projectStateStore.getProject(projectId2) !== null
261114
+ projectReadable: async ({ companyId, projectId: projectId2 }) => await (await projectStateFor(companyId)).getProject(projectId2) !== null
260646
261115
  });
260647
261116
  automationEventTimer = setInterval(() => {
260648
261117
  void eventPoller.tick().catch((err) => console.error(`[project-event-poller] tick \u5931\u8D25: ${String(err)}`));
@@ -267871,7 +268340,7 @@ function shimScript() {
267871
268340
  }
267872
268341
 
267873
268342
  // src/index.ts
267874
- var PKG_VERSION = true ? "2.2.1" : "dev";
268343
+ var PKG_VERSION = true ? "2.2.2" : "dev";
267875
268344
  var LOCAL_BIN = localBin();
267876
268345
  var NPM_PREFIX = npmPrefix();
267877
268346
  var INSTANCE = DEFAULT_INSTANCE;