oasis_test_v2 2.2.29 → 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 +474 -46
  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;
@@ -25935,11 +25935,19 @@ var init_codex = __esm({
25935
25935
  active: false,
25936
25936
  pendingInputs: /* @__PURE__ */ new Set(),
25937
25937
  codex: createCodexNormalizeState(),
25938
- // 本次 run 的用量:逐轮累加 `thread/tokenUsage/updated` 的 `last`(见该解析函数注释)。
25939
- usage: { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 },
25940
- sawUsage: false,
25941
- // `thread/started` 会直接给出本会话 rollout 的绝对路径——取实际模型时不必再满盘搜。
25942
- rolloutPath: void 0
25938
+ // 本次 run 的用量:`thread/tokenUsage/updated` 的 `last`(见该解析函数注释)按轮次记下,
25939
+ // 结算时只加**本次 run 发起的轮次**(runTurnIds)。原因:`thread/resume` 之后 codex 会把线程
25940
+ // **上一轮**的用量通知重放一遍(本机 codex 0.154.0 实测,turnId 是上一轮的),照单全收的话
25941
+ // 每次续接都把上一轮的 token 再记一遍。没带 turnId 的通知(老版本)照旧直接计入。
25942
+ usageByTurn: [],
25943
+ runTurnIds: /* @__PURE__ */ new Set(),
25944
+ // 已经发出本次 run 的 `turn/start`:在这之前出现的 `turn/started` 不是本次的轮次。
25945
+ turnRequested: false,
25946
+ // 会话 rollout 的绝对路径:新开线程时 `thread/started` 通知会给;**续接(thread/resume)不发这条通知**,
25947
+ // 只能从 resume 的响应里取(同上实测)。取不到它就读不到实际模型。
25948
+ rolloutPath: void 0,
25949
+ // thread/start / thread/resume 响应里 codex 报的线程模型:rollout 读不到时的兜底。
25950
+ threadModel: void 0
25943
25951
  }),
25944
25952
  initialCommand: () => ({
25945
25953
  jsonrpc: "2.0",
@@ -25956,12 +25964,16 @@ var init_codex = __esm({
25956
25964
  const threadId = thread.thread?.id ?? thread.id;
25957
25965
  if (typeof threadId !== "string" || !threadId) throw new Error("codex app-server returned no thread id");
25958
25966
  state.threadId = threadId;
25967
+ if (typeof thread.thread?.path === "string" && thread.thread.path) state.rolloutPath ??= thread.thread.path;
25968
+ const responseModel = typeof thread.model === "string" && thread.model ? thread.model : typeof thread.thread?.model === "string" && thread.thread.model ? thread.thread.model : void 0;
25969
+ if (responseModel) state.threadModel = responseModel;
25959
25970
  const text5 = currentJob.systemPrompt ? `${currentJob.systemPrompt}
25960
25971
 
25961
25972
  ---
25962
25973
 
25963
25974
  ${task}` : task;
25964
25975
  state.active = true;
25976
+ state.turnRequested = true;
25965
25977
  const turn = await send({
25966
25978
  jsonrpc: "2.0",
25967
25979
  method: "turn/start",
@@ -25974,7 +25986,10 @@ ${task}` : task;
25974
25986
  }
25975
25987
  });
25976
25988
  const turnId = turn.turn?.id ?? turn.id;
25977
- if (typeof turnId === "string" && turnId) state.turnId = turnId;
25989
+ if (typeof turnId === "string" && turnId) {
25990
+ state.turnId = turnId;
25991
+ state.runTurnIds.add(turnId);
25992
+ }
25978
25993
  if (!state.turnId) throw new Error("codex app-server returned no turn id");
25979
25994
  },
25980
25995
  liveEvents(message, state) {
@@ -26015,16 +26030,14 @@ ${task}` : task;
26015
26030
  }
26016
26031
  if (method === "thread/tokenUsage/updated") {
26017
26032
  const turn = parseCodexAppServerTurnUsage(params);
26018
- if (turn) {
26019
- addCodexBuckets(state.usage, turn);
26020
- state.sawUsage = true;
26021
- }
26033
+ if (turn) state.usageByTurn.push({ turnId: typeof params?.turnId === "string" ? params.turnId : void 0, buckets: turn });
26022
26034
  }
26023
26035
  if (method === "turn/started") {
26024
26036
  const turn = params?.turn;
26025
26037
  if (typeof turn?.id === "string") {
26026
26038
  state.turnId = turn.id;
26027
26039
  state.active = true;
26040
+ if (state.turnRequested) state.runTurnIds.add(turn.id);
26028
26041
  }
26029
26042
  }
26030
26043
  if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta") {
@@ -26056,9 +26069,16 @@ ${task}` : task;
26056
26069
  } catch {
26057
26070
  }
26058
26071
  }
26059
- model ??= resolvedModel2;
26072
+ model ??= state.threadModel ?? resolvedModel2;
26073
+ const usage = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
26074
+ let sawUsage = false;
26075
+ for (const entry of state.usageByTurn) {
26076
+ if (entry.turnId !== void 0 && !state.runTurnIds.has(entry.turnId)) continue;
26077
+ addCodexBuckets(usage, entry.buckets);
26078
+ sawUsage = true;
26079
+ }
26060
26080
  return {
26061
- ...state.sawUsage ? { usage: codexUsageFromBuckets(state.usage) } : {},
26081
+ ...sawUsage ? { usage: codexUsageFromBuckets(usage) } : {},
26062
26082
  ...model ? { model } : {}
26063
26083
  };
26064
26084
  },
@@ -26068,7 +26088,10 @@ ${task}` : task;
26068
26088
  await send({ jsonrpc: "2.0", method: "turn/interrupt", params: { threadId: state.threadId, turnId: state.turnId } });
26069
26089
  const next = await send({ jsonrpc: "2.0", method: "turn/start", params: { threadId: state.threadId, input: [{ type: "text", text: input.text }] } });
26070
26090
  const nextId = next.turn?.id;
26071
- if (typeof nextId === "string") state.turnId = nextId;
26091
+ if (typeof nextId === "string") {
26092
+ state.turnId = nextId;
26093
+ state.runTurnIds.add(nextId);
26094
+ }
26072
26095
  state.active = true;
26073
26096
  return { accepted: true, interrupt: "unknown" };
26074
26097
  }
@@ -160494,6 +160517,35 @@ var init_infra_error = __esm({
160494
160517
  }
160495
160518
  });
160496
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
+
160497
160549
  // ../server/src/live-chat.ts
160498
160550
  function normalizedKindToItemType(kind) {
160499
160551
  if (kind === "text") return "message";
@@ -203090,6 +203142,15 @@ function toApiError(err) {
203090
203142
  if (isInfrastructureError(err)) return new ApiError(INFRA_ERROR_STATUS, INFRA_ERROR_CODE, message);
203091
203143
  return new ApiError(400, "BAD_REQUEST", message);
203092
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
+ }
203093
203154
  async function loadWorkorderSnapshot(kernel, workorderId) {
203094
203155
  if (!workorderId) return null;
203095
203156
  const store = kernel.getStore?.();
@@ -204593,6 +204654,7 @@ async function startOasisServer(opts) {
204593
204654
  const stagingWorkspace = /* @__PURE__ */ new Map();
204594
204655
  const discussSessions = opts.discussSessions ?? /* @__PURE__ */ new Map();
204595
204656
  const openingDiscussions = /* @__PURE__ */ new Map();
204657
+ const workorderApplyLocks = createKeyedMutex();
204596
204658
  const reviewWaiters = /* @__PURE__ */ new Map();
204597
204659
  const leftWaiterIds = /* @__PURE__ */ new Map();
204598
204660
  const LEFT_WAITER_IDS_MAX_DRAFTS = 512;
@@ -205885,6 +205947,7 @@ async function startOasisServer(opts) {
205885
205947
  }
205886
205948
  }
205887
205949
  if (url.pathname === "/api/workorder/apply" && req.method === "POST") {
205950
+ const releaseApplyLock = await workorderApplyLocks.acquire(`${currentCompanyId ?? ""}\0${workorderApplyTarget(rawBody)}`);
205888
205951
  try {
205889
205952
  const body2 = JSON.parse(rawBody);
205890
205953
  let targetWs = body2.plan.workspace;
@@ -206043,6 +206106,8 @@ async function startOasisServer(opts) {
206043
206106
  res.writeHead(status, { "content-type": "application/json" }).end(
206044
206107
  JSON.stringify({ error: code2, message: err instanceof Error ? err.message : String(err) })
206045
206108
  );
206109
+ } finally {
206110
+ releaseApplyLock();
206046
206111
  }
206047
206112
  return;
206048
206113
  }
@@ -208764,6 +208829,7 @@ var init_server3 = __esm({
208764
208829
  init_company_data_store_router();
208765
208830
  init_router();
208766
208831
  init_infra_error();
208832
+ init_keyed_mutex();
208767
208833
  init_live_chat();
208768
208834
  init_chat_turn_wait();
208769
208835
  init_append_now();
@@ -212381,6 +212447,66 @@ function foldSingleNodeTasks(items) {
212381
212447
  }
212382
212448
  return folded;
212383
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
+ }
212384
212510
  function fillFolderSizes(items) {
212385
212511
  const folders = items.filter((i) => i.kind === "folder" && i.path);
212386
212512
  if (folders.length === 0) return;
@@ -212600,7 +212726,7 @@ function laterOf(a, b2) {
212600
212726
  if (!b2) return a;
212601
212727
  return b2 > a ? b2 : a;
212602
212728
  }
212603
- var DEFAULT_AGENT_ACTOR, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
212729
+ var DEFAULT_AGENT_ACTOR, CHAT_FOLDER_TASK_LABEL, MemoryProjectStateStore, MemoryArtifactStateStore, ProjectsService;
212604
212730
  var init_service4 = __esm({
212605
212731
  "../server/src/domains/projects/service.ts"() {
212606
212732
  "use strict";
@@ -212612,6 +212738,7 @@ var init_service4 = __esm({
212612
212738
  init_stats();
212613
212739
  init_node_state();
212614
212740
  DEFAULT_AGENT_ACTOR = "actor:agent:system";
212741
+ CHAT_FOLDER_TASK_LABEL = "\u5BF9\u8BDD";
212615
212742
  MemoryProjectStateStore = class {
212616
212743
  projects = /* @__PURE__ */ new Map();
212617
212744
  members = /* @__PURE__ */ new Map();
@@ -213387,12 +213514,12 @@ var init_service4 = __esm({
213387
213514
  ...items.filter((item) => item.blobRef).map((item) => `${item.name}\0${item.blobRef}`),
213388
213515
  ...files.map((file) => `${file.name}\0${file.blobId}`)
213389
213516
  ]);
213390
- if (this.listChatFiles && !isUncategorizedProjectId(projectId2)) {
213517
+ const chatFiles = [];
213518
+ if (this.listChatFiles) {
213391
213519
  for (const file of await this.listChatFiles(projectId2)) {
213392
- const key = `${file.name}\0${file.blobId}`;
213520
+ const key = `${basenameOfPath(file.name)}\0${file.blobId}`;
213393
213521
  if (existingFiles.has(key)) continue;
213394
- existingFiles.add(key);
213395
- files.push(file);
213522
+ chatFiles.push(file);
213396
213523
  }
213397
213524
  }
213398
213525
  for (const file of files) {
@@ -213418,8 +213545,18 @@ var init_service4 = __esm({
213418
213545
  updatedAt: file.uploadedAt
213419
213546
  });
213420
213547
  }
213548
+ emitChatFolders(items, chatFiles, (actorId) => this.resolveActorName(actorId));
213421
213549
  fillFolderSizes(items);
213550
+ const chatFolderTime = new Map(items.filter((item) => item.folderKind === "chat" && item.chatSessionId).map((item) => [item.chatSessionId, item.updatedAt]));
213422
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
+ }
213423
213560
  const pathA = a.path ?? "";
213424
213561
  const pathB = b2.path ?? "";
213425
213562
  if (pathA !== pathB) return pathA.localeCompare(pathB);
@@ -214744,6 +214881,8 @@ var init_dev_store = __esm({
214744
214881
  sessionWorkOrderKinds = /* @__PURE__ */ new Map();
214745
214882
  /** ADR-0166 D6 待发队列(每会话一条,按 seq 升序 = 入队序)。 */
214746
214883
  pending = /* @__PURE__ */ new Map();
214884
+ /** `outputs/` 产出文件的服务端副本表(2026-09-14):sessionId → 路径 → 副本。 */
214885
+ outputCopies = /* @__PURE__ */ new Map();
214747
214886
  pendingSeq = 0;
214748
214887
  async claimPendingInsert(m2) {
214749
214888
  const list2 = this.pending.get(m2.chatSessionId) ?? [];
@@ -214927,6 +215066,7 @@ var init_dev_store = __esm({
214927
215066
  this.messages.delete(id);
214928
215067
  this.sessionWorkOrders.delete(id);
214929
215068
  this.sessionWorkOrderKinds.delete(id);
215069
+ this.outputCopies.delete(id);
214930
215070
  }
214931
215071
  async appendMessage(m2) {
214932
215072
  return (await this.appendMessageOnce(m2)).message;
@@ -215055,6 +215195,27 @@ var init_dev_store = __esm({
215055
215195
  }
215056
215196
  return rows.sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.messageId.localeCompare(right.messageId));
215057
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
+ }
215058
215219
  async listRunningAssistantMessages() {
215059
215220
  const out = [];
215060
215221
  for (const list2 of this.messages.values()) {
@@ -216080,6 +216241,148 @@ var init_outputs_scan = __esm({
216080
216241
  }
216081
216242
  });
216082
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
+
216083
216386
  // ../server/src/org-model-backfill.ts
216084
216387
  function resolveDisplayName(existing, humanActor, accountName) {
216085
216388
  const pick3 = existing?.trim() || humanActor?.name?.trim() || accountName?.trim();
@@ -225116,7 +225419,11 @@ function toApiProjectFilesView(view) {
225116
225419
  creator_kind: item.creatorKind,
225117
225420
  creator_id: item.creatorId,
225118
225421
  creator_name: item.creatorName,
225119
- 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
225120
225427
  }))
225121
225428
  };
225122
225429
  }
@@ -229566,37 +229873,93 @@ var init_files_view = __esm({
229566
229873
  });
229567
229874
 
229568
229875
  // ../server/src/domains/projects/chat-files.ts
229569
- async function collectProjectChatFiles(chat, projectId2, meta) {
229570
- if (!chat.listAttachmentMessages) return [];
229571
- const messages = await chat.listAttachmentMessages(projectId2);
229572
- const sessions = new Map(await Promise.all([...new Set(messages.map((m2) => m2.sessionId))].map(async (id) => [id, await chat.getSession(id)])));
229573
- const visible = messages.filter((m2) => {
229574
- const session = sessions.get(m2.sessionId);
229575
- return session?.projectId === projectId2 && session.visibility !== "internal" && !session.analyzedRunId;
229576
- });
229577
- const byMessage = new Map(visible.map((m2) => [m2.messageId, m2]));
229578
- const rows = await collectMessageFiles(visible.map((m2) => ({ ...m2, id: m2.messageId })), {
229579
- meta
229580
- });
229581
- return mergeChatSessionFiles(rows, []).filter((row) => row.blob_ref !== null).map((row) => {
229582
- const message = byMessage.get(row.message_id);
229583
- const session = sessions.get(message.sessionId);
229584
- return {
229585
- projectId: projectId2,
229586
- fileId: `chat:${message.sessionId}:${row.id}`,
229587
- name: row.name,
229588
- blobId: row.blob_ref,
229589
- contentType: row.content_type ?? void 0,
229590
- size: row.size,
229591
- uploadedBy: session.humanActorId,
229592
- uploadedAt: row.updated_at
229593
- };
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
229594
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;
229595
229957
  }
229596
229958
  var init_chat_files2 = __esm({
229597
229959
  "../server/src/domains/projects/chat-files.ts"() {
229598
229960
  "use strict";
229599
229961
  init_files_view();
229962
+ init_uncategorized_project();
229600
229963
  }
229601
229964
  });
229602
229965
 
@@ -248794,6 +249157,7 @@ var init_postgres_chat_sessions = __esm({
248794
249157
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS model text`);
248795
249158
  await pool.query(`ALTER TABLE ${q2}.chat_sessions ADD COLUMN IF NOT EXISTS visibility text`);
248796
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`);
248797
249161
  await pool.query(`
248798
249162
  WITH ranked AS (
248799
249163
  SELECT id, row_number() OVER (PARTITION BY session_id ORDER BY seq, created_at, id) AS rn
@@ -249041,6 +249405,53 @@ var init_postgres_chat_sessions = __esm({
249041
249405
  [JSON.stringify(fingerprints), sessionId, this.companyId]
249042
249406
  );
249043
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
+ }
249044
249455
  /** 删项目时把本公司里绑到它的会话回落成「不指定项目」。会话一条都不删,只清 project_id。 */
249045
249456
  async clearProjectBinding(projectId2) {
249046
249457
  if (!projectId2) return 0;
@@ -259219,6 +259630,7 @@ var init_src11 = __esm({
259219
259630
  init_company_scoped_registry();
259220
259631
  init_session_workdir();
259221
259632
  init_outputs_scan();
259633
+ init_outputs_copy();
259222
259634
  init_org_model_backfill();
259223
259635
  init_types_admin();
259224
259636
  init_roles_admin();
@@ -261027,6 +261439,13 @@ async function startServe(opts) {
261027
261439
  }
261028
261440
  }
261029
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
+ }
261030
261449
  });
261031
261450
  },
261032
261451
  projects: projectStateStore,
@@ -262189,6 +262608,14 @@ async function startServe(opts) {
262189
262608
  delegationStores.set(id, store);
262190
262609
  return store;
262191
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
+ });
262192
262619
  const chatBroadcastMarkers = /* @__PURE__ */ new Map([
262193
262620
  [defaultCompanyId, chatBroadcastMarker]
262194
262621
  ]);
@@ -262920,6 +263347,7 @@ async function startServe(opts) {
262920
263347
  readFingerprints: read,
262921
263348
  writeFingerprints: write
262922
263349
  }).catch(() => null);
263350
+ if (diff && store) void outputCopyScheduler.schedule({ store, bridge, loc: located.loc, chatSessionId, companyId, fingerprints: diff.fingerprints });
262923
263351
  return diff?.changed ?? null;
262924
263352
  },
262925
263353
  delegationWorkdir: createDelegationWorkdirPort({
@@ -273755,7 +274183,7 @@ function shimScript() {
273755
274183
  }
273756
274184
 
273757
274185
  // src/index.ts
273758
- var PKG_VERSION = true ? "2.2.29" : "dev";
274186
+ var PKG_VERSION = true ? "2.2.31" : "dev";
273759
274187
  var LOCAL_BIN = localBin();
273760
274188
  var NPM_PREFIX = npmPrefix();
273761
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.29",
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": "68eff586a82e2fd668f109ffbdd672ebf77844ac"
29
+ "sourceHead": "bbc56bf65b7e0c10506c3919f42c9c7d8b7c7342"
30
30
  }
31
31
  }