oasis_test_v2 2.2.30 → 2.2.31

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 +438 -33
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -23350,7 +23350,7 @@ function accumulateClaudeUsage(acc, p2) {
23350
23350
  acc.saw = true;
23351
23351
  }
23352
23352
  } else if (type === "result") {
23353
- acc.finalUsage = mergeFinalUsage(parseResultUsage(p2["usage"]), parseModelUsage(p2["modelUsage"]));
23353
+ acc.finalUsage = mergeFinalUsage(parseModelUsage(p2["modelUsage"]), parseResultUsage(p2["usage"]));
23354
23354
  const c = p2["total_cost_usd"];
23355
23355
  if (typeof c === "number" && Number.isFinite(c)) {
23356
23356
  acc.costUsd = c;
@@ -160517,6 +160517,35 @@ var init_infra_error = __esm({
160517
160517
  }
160518
160518
  });
160519
160519
 
160520
+ // ../server/src/keyed-mutex.ts
160521
+ function createKeyedMutex() {
160522
+ const tails = /* @__PURE__ */ new Map();
160523
+ return {
160524
+ async acquire(key) {
160525
+ const previous3 = tails.get(key) ?? Promise.resolve();
160526
+ let release;
160527
+ const mine = new Promise((resolve10) => {
160528
+ release = resolve10;
160529
+ });
160530
+ const tail = previous3.then(() => mine);
160531
+ tails.set(key, tail);
160532
+ await previous3;
160533
+ let released = false;
160534
+ return () => {
160535
+ if (released) return;
160536
+ released = true;
160537
+ release();
160538
+ if (tails.get(key) === tail) tails.delete(key);
160539
+ };
160540
+ }
160541
+ };
160542
+ }
160543
+ var init_keyed_mutex = __esm({
160544
+ "../server/src/keyed-mutex.ts"() {
160545
+ "use strict";
160546
+ }
160547
+ });
160548
+
160520
160549
  // ../server/src/live-chat.ts
160521
160550
  function normalizedKindToItemType(kind) {
160522
160551
  if (kind === "text") return "message";
@@ -203113,6 +203142,15 @@ function toApiError(err) {
203113
203142
  if (isInfrastructureError(err)) return new ApiError(INFRA_ERROR_STATUS, INFRA_ERROR_CODE, message);
203114
203143
  return new ApiError(400, "BAD_REQUEST", message);
203115
203144
  }
203145
+ function workorderApplyTarget(rawBody) {
203146
+ try {
203147
+ const body2 = JSON.parse(rawBody);
203148
+ const briefSpawn = body2.plan?.ops?.find((op) => op.action === "spawn" && op.id === body2.briefArtifactId && op.workspace);
203149
+ return briefSpawn?.workspace ?? body2.plan?.workspace ?? "";
203150
+ } catch {
203151
+ return "";
203152
+ }
203153
+ }
203116
203154
  async function loadWorkorderSnapshot(kernel, workorderId) {
203117
203155
  if (!workorderId) return null;
203118
203156
  const store = kernel.getStore?.();
@@ -204616,6 +204654,7 @@ async function startOasisServer(opts) {
204616
204654
  const stagingWorkspace = /* @__PURE__ */ new Map();
204617
204655
  const discussSessions = opts.discussSessions ?? /* @__PURE__ */ new Map();
204618
204656
  const openingDiscussions = /* @__PURE__ */ new Map();
204657
+ const workorderApplyLocks = createKeyedMutex();
204619
204658
  const reviewWaiters = /* @__PURE__ */ new Map();
204620
204659
  const leftWaiterIds = /* @__PURE__ */ new Map();
204621
204660
  const LEFT_WAITER_IDS_MAX_DRAFTS = 512;
@@ -205908,6 +205947,7 @@ async function startOasisServer(opts) {
205908
205947
  }
205909
205948
  }
205910
205949
  if (url.pathname === "/api/workorder/apply" && req.method === "POST") {
205950
+ const releaseApplyLock = await workorderApplyLocks.acquire(`${currentCompanyId ?? ""}\0${workorderApplyTarget(rawBody)}`);
205911
205951
  try {
205912
205952
  const body2 = JSON.parse(rawBody);
205913
205953
  let targetWs = body2.plan.workspace;
@@ -206066,6 +206106,8 @@ async function startOasisServer(opts) {
206066
206106
  res.writeHead(status, { "content-type": "application/json" }).end(
206067
206107
  JSON.stringify({ error: code2, message: err instanceof Error ? err.message : String(err) })
206068
206108
  );
206109
+ } finally {
206110
+ releaseApplyLock();
206069
206111
  }
206070
206112
  return;
206071
206113
  }
@@ -208787,6 +208829,7 @@ var init_server3 = __esm({
208787
208829
  init_company_data_store_router();
208788
208830
  init_router();
208789
208831
  init_infra_error();
208832
+ init_keyed_mutex();
208790
208833
  init_live_chat();
208791
208834
  init_chat_turn_wait();
208792
208835
  init_append_now();
@@ -212404,6 +212447,66 @@ function foldSingleNodeTasks(items) {
212404
212447
  }
212405
212448
  return folded;
212406
212449
  }
212450
+ function basenameOfPath(name) {
212451
+ const i = name.lastIndexOf("/");
212452
+ return i >= 0 ? name.slice(i + 1) : name;
212453
+ }
212454
+ function creatorKindOf(actorId) {
212455
+ if (!actorId) return null;
212456
+ return actorId.startsWith("actor:human:") ? "human" : "agent";
212457
+ }
212458
+ function emitChatFolders(items, chatFiles, resolveName) {
212459
+ const groups = /* @__PURE__ */ new Map();
212460
+ for (const file of chatFiles) {
212461
+ const list2 = groups.get(file.chatSessionId) ?? [];
212462
+ list2.push(file);
212463
+ groups.set(file.chatSessionId, list2);
212464
+ }
212465
+ for (const [sessionId, group] of groups) {
212466
+ const head = group[0];
212467
+ const folderPath = `chat:${sessionId}`;
212468
+ const latest = group.reduce((acc, file) => file.uploadedAt > acc ? file.uploadedAt : acc, group[0].uploadedAt);
212469
+ items.push({
212470
+ kind: "folder",
212471
+ id: `folder:${folderPath}`,
212472
+ name: head.chatTitle,
212473
+ path: folderPath,
212474
+ filePath: null,
212475
+ depth: 0,
212476
+ type: "folder",
212477
+ size: null,
212478
+ taskId: null,
212479
+ taskTitle: CHAT_FOLDER_TASK_LABEL,
212480
+ creatorKind: creatorKindOf(head.chatOwnerId),
212481
+ creatorId: head.chatOwnerId,
212482
+ creatorName: resolveName(head.chatOwnerId),
212483
+ updatedAt: latest,
212484
+ folderKind: "chat",
212485
+ chatSessionId: sessionId
212486
+ });
212487
+ for (const file of group) {
212488
+ items.push({
212489
+ kind: "file",
212490
+ id: file.fileId,
212491
+ name: file.name,
212492
+ path: `${folderPath}/${file.name}`,
212493
+ filePath: null,
212494
+ depth: 1,
212495
+ type: file.contentType ?? inferTypeFromName(basenameOfPath(file.name)),
212496
+ blobRef: file.blobId,
212497
+ contentType: file.contentType ?? null,
212498
+ size: file.size,
212499
+ taskId: null,
212500
+ taskTitle: CHAT_FOLDER_TASK_LABEL,
212501
+ creatorKind: creatorKindOf(file.uploadedBy),
212502
+ creatorId: file.uploadedBy ?? null,
212503
+ creatorName: file.uploadedBy ? resolveName(file.uploadedBy) : null,
212504
+ updatedAt: file.uploadedAt,
212505
+ chatSessionId: sessionId
212506
+ });
212507
+ }
212508
+ }
212509
+ }
212407
212510
  function fillFolderSizes(items) {
212408
212511
  const folders = items.filter((i) => i.kind === "folder" && i.path);
212409
212512
  if (folders.length === 0) return;
@@ -212623,7 +212726,7 @@ function laterOf(a, b2) {
212623
212726
  if (!b2) return a;
212624
212727
  return b2 > a ? b2 : a;
212625
212728
  }
212626
- var DEFAULT_AGENT_ACTOR, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
212729
+ var DEFAULT_AGENT_ACTOR, CHAT_FOLDER_TASK_LABEL, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
212627
212730
  var init_service4 = __esm({
212628
212731
  "../server/src/domains/projects/service.ts"() {
212629
212732
  "use strict";
@@ -212635,6 +212738,7 @@ var init_service4 = __esm({
212635
212738
  init_stats();
212636
212739
  init_node_state();
212637
212740
  DEFAULT_AGENT_ACTOR = "actor:agent:system";
212741
+ CHAT_FOLDER_TASK_LABEL = "\u5BF9\u8BDD";
212638
212742
  MemoryProjectStateStore = class {
212639
212743
  projects = /* @__PURE__ */ new Map();
212640
212744
  members = /* @__PURE__ */ new Map();
@@ -213410,12 +213514,12 @@ var init_service4 = __esm({
213410
213514
  ...items.filter((item) => item.blobRef).map((item) => `${item.name}\0${item.blobRef}`),
213411
213515
  ...files.map((file) => `${file.name}\0${file.blobId}`)
213412
213516
  ]);
213413
- if (this.listChatFiles && !isUncategorizedProjectId(projectId2)) {
213517
+ const chatFiles = [];
213518
+ if (this.listChatFiles) {
213414
213519
  for (const file of await this.listChatFiles(projectId2)) {
213415
- const key = `${file.name}\0${file.blobId}`;
213520
+ const key = `${basenameOfPath(file.name)}\0${file.blobId}`;
213416
213521
  if (existingFiles.has(key)) continue;
213417
- existingFiles.add(key);
213418
- files.push(file);
213522
+ chatFiles.push(file);
213419
213523
  }
213420
213524
  }
213421
213525
  for (const file of files) {
@@ -213441,8 +213545,18 @@ var init_service4 = __esm({
213441
213545
  updatedAt: file.uploadedAt
213442
213546
  });
213443
213547
  }
213548
+ emitChatFolders(items, chatFiles, (actorId) => this.resolveActorName(actorId));
213444
213549
  fillFolderSizes(items);
213550
+ const chatFolderTime = new Map(items.filter((item) => item.folderKind === "chat" && item.chatSessionId).map((item) => [item.chatSessionId, item.updatedAt]));
213445
213551
  items.sort((a, b2) => {
213552
+ const chatA = a.chatSessionId ? 1 : 0;
213553
+ const chatB = b2.chatSessionId ? 1 : 0;
213554
+ if (chatA !== chatB) return chatA - chatB;
213555
+ if (a.chatSessionId && b2.chatSessionId && a.chatSessionId !== b2.chatSessionId) {
213556
+ const timeA = chatFolderTime.get(a.chatSessionId) ?? "";
213557
+ const timeB = chatFolderTime.get(b2.chatSessionId) ?? "";
213558
+ return timeB.localeCompare(timeA) || a.chatSessionId.localeCompare(b2.chatSessionId);
213559
+ }
213446
213560
  const pathA = a.path ?? "";
213447
213561
  const pathB = b2.path ?? "";
213448
213562
  if (pathA !== pathB) return pathA.localeCompare(pathB);
@@ -214767,6 +214881,8 @@ var init_dev_store = __esm({
214767
214881
  sessionWorkOrderKinds = /* @__PURE__ */ new Map();
214768
214882
  /** ADR-0166 D6 待发队列(每会话一条,按 seq 升序 = 入队序)。 */
214769
214883
  pending = /* @__PURE__ */ new Map();
214884
+ /** `outputs/` 产出文件的服务端副本表(2026-09-14):sessionId → 路径 → 副本。 */
214885
+ outputCopies = /* @__PURE__ */ new Map();
214770
214886
  pendingSeq = 0;
214771
214887
  async claimPendingInsert(m2) {
214772
214888
  const list2 = this.pending.get(m2.chatSessionId) ?? [];
@@ -214950,6 +215066,7 @@ var init_dev_store = __esm({
214950
215066
  this.messages.delete(id);
214951
215067
  this.sessionWorkOrders.delete(id);
214952
215068
  this.sessionWorkOrderKinds.delete(id);
215069
+ this.outputCopies.delete(id);
214953
215070
  }
214954
215071
  async appendMessage(m2) {
214955
215072
  return (await this.appendMessageOnce(m2)).message;
@@ -215078,6 +215195,27 @@ var init_dev_store = __esm({
215078
215195
  }
215079
215196
  return rows.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.messageId.localeCompare(right.messageId));
215080
215197
  }
215198
+ async readOutputCopies(sessionId) {
215199
+ const hit = this.outputCopies.get(sessionId);
215200
+ return hit ? { ...hit } : null;
215201
+ }
215202
+ async mergeOutputCopies(sessionId, copies) {
215203
+ if (copies.length === 0 || !this.sessions.has(sessionId)) return;
215204
+ const next = { ...this.outputCopies.get(sessionId) ?? {} };
215205
+ for (const copy of copies) next[copy.path] = { ...copy };
215206
+ this.outputCopies.set(sessionId, next);
215207
+ }
215208
+ async listOutputCopies(projectId2) {
215209
+ const rows = [];
215210
+ for (const session of this.sessions.values()) {
215211
+ if (session.visibility === "internal") continue;
215212
+ if ((session.projectId ?? null) !== projectId2) continue;
215213
+ const copies = this.outputCopies.get(session.id);
215214
+ if (!copies || Object.keys(copies).length === 0) continue;
215215
+ rows.push({ sessionId: session.id, ...session.projectId ? { projectId: session.projectId } : {}, copies: { ...copies } });
215216
+ }
215217
+ return rows.sort((left, right) => left.sessionId.localeCompare(right.sessionId));
215218
+ }
215081
215219
  async listRunningAssistantMessages() {
215082
215220
  const out = [];
215083
215221
  for (const list2 of this.messages.values()) {
@@ -216103,6 +216241,148 @@ var init_outputs_scan = __esm({
216103
216241
  }
216104
216242
  });
216105
216243
 
216244
+ // ../server/src/domains/chat-sessions/outputs-copy.ts
216245
+ function isOutputPath(path41) {
216246
+ return path41.startsWith(`${SESSION_OUTPUTS_DIR}/`);
216247
+ }
216248
+ function basenameOf2(path41) {
216249
+ const i = path41.lastIndexOf("/");
216250
+ return i >= 0 ? path41.slice(i + 1) : path41;
216251
+ }
216252
+ function planOutputCopies(fingerprints, copies, limits = OUTPUT_COPY_LIMITS) {
216253
+ const have = copies ?? {};
216254
+ const plan = { toCopy: [], skipped: [] };
216255
+ let totalBytes = 0;
216256
+ for (const path41 of Object.keys(fingerprints).sort((a, b2) => a.localeCompare(b2))) {
216257
+ if (!isOutputPath(path41)) continue;
216258
+ const fp = fingerprints[path41];
216259
+ if (typeof fp.size !== "number" || !Number.isFinite(fp.size) || typeof fp.mtime !== "string" || fp.mtime === "") continue;
216260
+ const copy = have[path41];
216261
+ if (copy && copy.size === fp.size && copy.mtime === fp.mtime) continue;
216262
+ if (fp.size > limits.maxFileBytes) {
216263
+ plan.skipped.push({ path: path41, code: "TOO_LARGE" });
216264
+ continue;
216265
+ }
216266
+ if (plan.toCopy.length >= limits.maxFiles || totalBytes + fp.size > limits.maxTotalBytes) {
216267
+ plan.skipped.push({ path: path41, code: "BUDGET" });
216268
+ continue;
216269
+ }
216270
+ totalBytes += fp.size;
216271
+ plan.toCopy.push({ path: path41, size: fp.size, mtime: fp.mtime });
216272
+ }
216273
+ return plan;
216274
+ }
216275
+ async function copySessionOutputs(deps) {
216276
+ const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
216277
+ const limits = deps.limits ?? OUTPUT_COPY_LIMITS;
216278
+ const result = { copied: [], skipped: [] };
216279
+ let existing;
216280
+ try {
216281
+ existing = await deps.readCopies(deps.sessionId);
216282
+ } catch {
216283
+ return result;
216284
+ }
216285
+ const plan = planOutputCopies(deps.fingerprints, existing, limits);
216286
+ result.skipped.push(...plan.skipped);
216287
+ for (const file of plan.toCopy) {
216288
+ let read;
216289
+ try {
216290
+ read = await deps.bridge.read(
216291
+ deps.loc.nodeId,
216292
+ { workdirKey: deps.loc.workdirKey, runtimeKind: deps.loc.runtimeKind, rtId: deps.loc.rtId, path: file.path },
216293
+ limits.maxFileBytes
216294
+ );
216295
+ } catch {
216296
+ result.skipped.push({ path: file.path, code: "READ_FAILED" });
216297
+ continue;
216298
+ }
216299
+ if (!read.ok) {
216300
+ result.skipped.push({ path: file.path, code: read.code === "TOO_LARGE" ? "TOO_LARGE" : "READ_FAILED" });
216301
+ continue;
216302
+ }
216303
+ const bytes2 = Buffer.from(read.contentBase64, "base64");
216304
+ let blobRef;
216305
+ try {
216306
+ blobRef = await deps.putBlob(new Uint8Array(bytes2));
216307
+ } catch {
216308
+ result.skipped.push({ path: file.path, code: "STORE_FAILED" });
216309
+ continue;
216310
+ }
216311
+ result.copied.push({
216312
+ path: file.path,
216313
+ name: basenameOf2(file.path),
216314
+ blobRef,
216315
+ contentType: read.contentType ? read.contentType : null,
216316
+ size: bytes2.length,
216317
+ // 记**扫描时**的 mtime(不是读的那一刻):下一轮拿同一把节点时钟的指纹来比,相等才算复制过。
216318
+ mtime: file.mtime,
216319
+ copiedAt: now()
216320
+ });
216321
+ }
216322
+ if (result.copied.length > 0) {
216323
+ try {
216324
+ await deps.mergeCopies(deps.sessionId, result.copied);
216325
+ } catch {
216326
+ result.skipped.push(...result.copied.map((copy) => ({ path: copy.path, code: "STORE_FAILED" })));
216327
+ result.copied = [];
216328
+ }
216329
+ }
216330
+ return result;
216331
+ }
216332
+ function createOutputCopyScheduler(deps) {
216333
+ const inFlight = /* @__PURE__ */ new Set();
216334
+ return {
216335
+ schedule(args) {
216336
+ const { store, chatSessionId, companyId } = args;
216337
+ const readCopies = store.readOutputCopies?.bind(store);
216338
+ const mergeCopies = store.mergeOutputCopies?.bind(store);
216339
+ if (!readCopies || !mergeCopies) return Promise.resolve(null);
216340
+ const key = `${companyId ?? ""}|${chatSessionId}`;
216341
+ if (inFlight.has(key)) return Promise.resolve(null);
216342
+ inFlight.add(key);
216343
+ return (async () => {
216344
+ try {
216345
+ const session = await store.getSession(chatSessionId);
216346
+ if (!session || session.visibility === "internal" || session.analyzedRunId) return null;
216347
+ if (deps.isDelegationChild && await deps.isDelegationChild(companyId, chatSessionId).catch(() => false)) return null;
216348
+ const result = await copySessionOutputs({
216349
+ bridge: args.bridge,
216350
+ loc: args.loc,
216351
+ sessionId: chatSessionId,
216352
+ fingerprints: args.fingerprints,
216353
+ readCopies,
216354
+ mergeCopies,
216355
+ putBlob: deps.putBlob,
216356
+ ...deps.limits ? { limits: deps.limits } : {}
216357
+ });
216358
+ if (result.skipped.length > 0) {
216359
+ const head = result.skipped.slice(0, 5).map((s2) => `${s2.path}:${s2.code}`).join(", ");
216360
+ deps.log?.(`[outputs-copy] ${chatSessionId}: \u590D\u5236 ${result.copied.length} \u4E2A\uFF0C\u8DF3\u8FC7 ${result.skipped.length} \u4E2A\uFF08${head}${result.skipped.length > 5 ? " \u2026" : ""}\uFF09`);
216361
+ }
216362
+ return result;
216363
+ } catch (err) {
216364
+ deps.log?.(`[outputs-copy] ${chatSessionId}: ${err instanceof Error ? err.message : String(err)}`);
216365
+ return null;
216366
+ } finally {
216367
+ inFlight.delete(key);
216368
+ }
216369
+ })();
216370
+ }
216371
+ };
216372
+ }
216373
+ var OUTPUT_COPY_LIMITS;
216374
+ var init_outputs_copy = __esm({
216375
+ "../server/src/domains/chat-sessions/outputs-copy.ts"() {
216376
+ "use strict";
216377
+ init_src();
216378
+ OUTPUT_COPY_LIMITS = {
216379
+ maxFileBytes: 2 * 1024 * 1024,
216380
+ maxFiles: 50,
216381
+ maxTotalBytes: 50 * 1024 * 1024
216382
+ };
216383
+ }
216384
+ });
216385
+
216106
216386
  // ../server/src/org-model-backfill.ts
216107
216387
  function resolveDisplayName(existing, humanActor, accountName) {
216108
216388
  const pick3 = existing?.trim() || humanActor?.name?.trim() || accountName?.trim();
@@ -225139,7 +225419,11 @@ function toApiProjectFilesView(view) {
225139
225419
  creator_kind: item.creatorKind,
225140
225420
  creator_id: item.creatorId,
225141
225421
  creator_name: item.creatorName,
225142
- updated_at: item.updatedAt
225422
+ updated_at: item.updatedAt,
225423
+ // 对话文件夹(`"chat"`):前端据此换带对话气泡的文件夹图标。其余文件夹与文件行为 null。
225424
+ folder_kind: item.folderKind ?? null,
225425
+ // 对话文件夹及其子文件所属的对话;前端「所属任务」列的「对话」筛选档靠它。其余行为 null。
225426
+ chat_session_id: item.chatSessionId ?? null
225143
225427
  }))
225144
225428
  };
225145
225429
  }
@@ -229589,37 +229873,93 @@ var init_files_view = __esm({
229589
229873
  });
229590
229874
 
229591
229875
  // ../server/src/domains/projects/chat-files.ts
229592
- async function collectProjectChatFiles(chat, projectId2, meta) {
229593
- if (!chat.listAttachmentMessages) return [];
229594
- const messages = await chat.listAttachmentMessages(projectId2);
229595
- const sessions = new Map(await Promise.all([...new Set(messages.map((m2) => m2.sessionId))].map(async (id) => [id, await chat.getSession(id)])));
229596
- const visible = messages.filter((m2) => {
229597
- const session = sessions.get(m2.sessionId);
229598
- return session?.projectId === projectId2 && session.visibility !== "internal" && !session.analyzedRunId;
229599
- });
229600
- const byMessage = new Map(visible.map((m2) => [m2.messageId, m2]));
229601
- const rows = await collectMessageFiles(visible.map((m2) => ({ ...m2, id: m2.messageId })), {
229602
- meta
229603
- });
229604
- return mergeChatSessionFiles(rows, []).filter((row) => row.blob_ref !== null).map((row) => {
229605
- const message = byMessage.get(row.message_id);
229606
- const session = sessions.get(message.sessionId);
229607
- return {
229608
- projectId: projectId2,
229609
- fileId: `chat:${message.sessionId}:${row.id}`,
229610
- name: row.name,
229611
- blobId: row.blob_ref,
229612
- contentType: row.content_type ?? void 0,
229613
- size: row.size,
229614
- uploadedBy: session.humanActorId,
229615
- uploadedAt: row.updated_at
229616
- };
229876
+ function chatFolderTitle(session) {
229877
+ const title = session.title?.trim();
229878
+ return title ? title : "\u672A\u547D\u540D\u5BF9\u8BDD";
229879
+ }
229880
+ function outputDisplayName(path41) {
229881
+ return path41.startsWith("outputs/") ? path41.slice("outputs/".length) : path41;
229882
+ }
229883
+ async function collectProjectChatFiles(chat, projectId2, meta, options = {}) {
229884
+ const scope = isUncategorizedProjectId(projectId2) ? null : projectId2;
229885
+ const [attachmentMessages, outputSessions] = await Promise.all([
229886
+ chat.listAttachmentMessages ? chat.listAttachmentMessages(scope) : Promise.resolve([]),
229887
+ chat.listOutputCopies ? chat.listOutputCopies(scope) : Promise.resolve([])
229888
+ ]);
229889
+ const sessionIds = [.../* @__PURE__ */ new Set([
229890
+ ...attachmentMessages.map((m2) => m2.sessionId),
229891
+ ...outputSessions.map((s2) => s2.sessionId)
229892
+ ])];
229893
+ const sessions = /* @__PURE__ */ new Map();
229894
+ await Promise.all(sessionIds.map(async (id) => {
229895
+ const session = await chat.getSession(id);
229896
+ if (!session) return;
229897
+ if ((session.projectId ?? null) !== scope) return;
229898
+ if (session.visibility === "internal" || session.analyzedRunId) return;
229899
+ if (options.isDelegationChild && await options.isDelegationChild(id).catch(() => false)) return;
229900
+ sessions.set(id, session);
229901
+ }));
229902
+ const out = [];
229903
+ const base = (session) => ({
229904
+ projectId: projectId2,
229905
+ chatSessionId: session.id,
229906
+ chatTitle: chatFolderTitle(session),
229907
+ chatOwnerId: session.humanActorId
229617
229908
  });
229909
+ const bySession = /* @__PURE__ */ new Map();
229910
+ for (const message of attachmentMessages) {
229911
+ if (!sessions.has(message.sessionId)) continue;
229912
+ const list2 = bySession.get(message.sessionId) ?? [];
229913
+ list2.push(message);
229914
+ bySession.set(message.sessionId, list2);
229915
+ }
229916
+ for (const [sessionId, messages] of bySession) {
229917
+ const session = sessions.get(sessionId);
229918
+ const rows = await collectMessageFiles(messages.map((m2) => ({ ...m2, id: m2.messageId })), { meta });
229919
+ for (const row of mergeChatSessionFiles(rows, [])) {
229920
+ if (row.blob_ref === null) continue;
229921
+ out.push({
229922
+ ...base(session),
229923
+ fileId: `chat:${sessionId}:${row.id}`,
229924
+ name: row.name,
229925
+ blobId: row.blob_ref,
229926
+ ...row.content_type ? { contentType: row.content_type } : {},
229927
+ size: row.size,
229928
+ uploadedBy: session.humanActorId,
229929
+ uploadedAt: row.updated_at,
229930
+ chatSource: "attachment"
229931
+ });
229932
+ }
229933
+ }
229934
+ for (const record8 of outputSessions) {
229935
+ const session = sessions.get(record8.sessionId);
229936
+ if (!session) continue;
229937
+ const seen = new Set(out.filter((file) => file.chatSessionId === session.id).map((file) => `${file.name}\0${file.blobId}`));
229938
+ const copies = record8.copies ?? {};
229939
+ for (const path41 of Object.keys(copies).sort((a, b2) => a.localeCompare(b2))) {
229940
+ const copy = copies[path41];
229941
+ if (!copy?.blobRef) continue;
229942
+ if (seen.has(`${copy.name}\0${copy.blobRef}`)) continue;
229943
+ out.push({
229944
+ ...base(session),
229945
+ fileId: `chat:${session.id}:out:${copy.path}`,
229946
+ name: outputDisplayName(copy.path),
229947
+ blobId: copy.blobRef,
229948
+ ...copy.contentType ? { contentType: copy.contentType } : {},
229949
+ size: copy.size,
229950
+ uploadedBy: session.aiActorId,
229951
+ uploadedAt: Number.isNaN(Date.parse(copy.mtime)) ? copy.copiedAt : new Date(copy.mtime).toISOString(),
229952
+ chatSource: "output"
229953
+ });
229954
+ }
229955
+ }
229956
+ return out;
229618
229957
  }
229619
229958
  var init_chat_files2 = __esm({
229620
229959
  "../server/src/domains/projects/chat-files.ts"() {
229621
229960
  "use strict";
229622
229961
  init_files_view();
229962
+ init_uncategorized_project();
229623
229963
  }
229624
229964
  });
229625
229965
 
@@ -248817,6 +249157,7 @@ var init_postgres_chat_sessions = __esm({
248817
249157
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS model text`);
248818
249158
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS visibility text`);
248819
249159
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS outputs_fingerprints jsonb`);
249160
+ await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS outputs_copies jsonb`);
248820
249161
  await pool.query(`
248821
249162
  WITH ranked AS (
248822
249163
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -249064,6 +249405,53 @@ var init_postgres_chat_sessions = __esm({
249064
249405
  [JSON.stringify(fingerprints), sessionId, this.companyId]
249065
249406
  );
249066
249407
  }
249408
+ /**
249409
+ * `outputs/` 产出文件的服务端副本表(2026-09-14)。读:没有副本 → null。
249410
+ */
249411
+ async readOutputCopies(sessionId) {
249412
+ const r = await this.pool.query(
249413
+ `SELECT outputs_copies FROM ${this.s}.chat_sessions WHERE id = $1 AND company_id = $2`,
249414
+ [sessionId, this.companyId]
249415
+ );
249416
+ return r.rows[0]?.outputs_copies ?? null;
249417
+ }
249418
+ /**
249419
+ * 按路径**合并**写入:同一路径新副本覆盖旧副本,其余路径保留。合并在一条 UPDATE 里用 jsonb `||` 完成,
249420
+ * 不是读出来改完再整份写回——两次收尾的复制前后脚落库时,后写的不会把先写的吞掉。
249421
+ */
249422
+ async mergeOutputCopies(sessionId, copies) {
249423
+ if (copies.length === 0) return;
249424
+ const patch = {};
249425
+ for (const copy of copies) patch[copy.path] = copy;
249426
+ await this.pool.query(
249427
+ `UPDATE ${this.s}.chat_sessions
249428
+ SET outputs_copies = COALESCE(outputs_copies, '{}'::jsonb) || $1::jsonb
249429
+ WHERE id = $2 AND company_id = $3`,
249430
+ [JSON.stringify(patch), sessionId, this.companyId]
249431
+ );
249432
+ }
249433
+ /**
249434
+ * 某个范围内有副本的会话。口径同 {@link listAttachmentMessages}:排除内部会话、按公司收口、
249435
+ * `project_id IS NOT DISTINCT FROM $1`(null = 没绑项目的会话)。只取三列,不碰会话其余字段。
249436
+ */
249437
+ async listOutputCopies(projectId2) {
249438
+ const r = await this.pool.query(
249439
+ `SELECT id, project_id, outputs_copies
249440
+ FROM ${this.s}.chat_sessions
249441
+ WHERE outputs_copies IS NOT NULL
249442
+ AND outputs_copies <> '{}'::jsonb
249443
+ AND visibility IS DISTINCT FROM 'internal'
249444
+ AND project_id IS NOT DISTINCT FROM $1
249445
+ AND company_id = $2
249446
+ ORDER BY id`,
249447
+ [projectId2, this.companyId]
249448
+ );
249449
+ return r.rows.map((row) => ({
249450
+ sessionId: row.id,
249451
+ ...row.project_id ? { projectId: row.project_id } : {},
249452
+ copies: row.outputs_copies ?? {}
249453
+ }));
249454
+ }
249067
249455
  /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
249068
249456
  async clearProjectBinding(projectId2) {
249069
249457
  if (!projectId2) return 0;
@@ -259242,6 +259630,7 @@ var init_src11 = __esm({
259242
259630
  init_company_scoped_registry();
259243
259631
  init_session_workdir();
259244
259632
  init_outputs_scan();
259633
+ init_outputs_copy();
259245
259634
  init_org_model_backfill();
259246
259635
  init_types_admin();
259247
259636
  init_roles_admin();
@@ -261050,6 +261439,13 @@ async function startServe(opts) {
261050
261439
  }
261051
261440
  }
261052
261441
  return null;
261442
+ }, {
261443
+ // 委派子会话不进项目(ADR 0555:它的 outputs/ 是交给协调者的)。闭包在请求时才读
261444
+ // `delegationStoreFor`,避开它在下方声明的 TDZ。
261445
+ isDelegationChild: async (sessionId) => {
261446
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
261447
+ return Boolean(ledger && await ledger.getByChildSession(sessionId).catch(() => null));
261448
+ }
261053
261449
  });
261054
261450
  },
261055
261451
  projects: projectStateStore,
@@ -262212,6 +262608,14 @@ async function startServe(opts) {
262212
262608
  delegationStores.set(id, store);
262213
262609
  return store;
262214
262610
  };
262611
+ const outputCopyScheduler = createOutputCopyScheduler({
262612
+ putBlob: (bytes2) => assets.put(bytes2),
262613
+ isDelegationChild: async (companyId, sessionId) => {
262614
+ const ledger = await delegationStoreFor(companyId).catch(() => null);
262615
+ return Boolean(ledger && await ledger.getByChildSession(sessionId).catch(() => null));
262616
+ },
262617
+ log: (message) => console.warn(message)
262618
+ });
262215
262619
  const chatBroadcastMarkers = /* @__PURE__ */ new Map([
262216
262620
  [defaultCompanyId, chatBroadcastMarker]
262217
262621
  ]);
@@ -262943,6 +263347,7 @@ async function startServe(opts) {
262943
263347
  readFingerprints: read,
262944
263348
  writeFingerprints: write
262945
263349
  }).catch(() => null);
263350
+ if (diff && store) void outputCopyScheduler.schedule({ store, bridge, loc: located.loc, chatSessionId, companyId, fingerprints: diff.fingerprints });
262946
263351
  return diff?.changed ?? null;
262947
263352
  },
262948
263353
  delegationWorkdir: createDelegationWorkdirPort({
@@ -273778,7 +274183,7 @@ function shimScript() {
273778
274183
  }
273779
274184
 
273780
274185
  // src/index.ts
273781
- var PKG_VERSION = true ? "2.2.30" : "dev";
274186
+ var PKG_VERSION = true ? "2.2.31" : "dev";
273782
274187
  var LOCAL_BIN = localBin();
273783
274188
  var NPM_PREFIX = npmPrefix();
273784
274189
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.30",
3
+ "version": "2.2.31",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "013d645f571ea8a12f1a119c0dae1cc1f4c3caac"
29
+ "sourceHead": "bbc56bf65b7e0c10506c3919f42c9c7d8b7c7342"
30
30
  }
31
31
  }