oasis_test 0.1.36 → 0.1.38

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 +485 -96
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -184,6 +184,7 @@ function fold(model, op) {
184
184
  anchor: { target: op.target, ...p2.anchorSpan !== void 0 ? { span: p2.anchorSpan } : {} },
185
185
  author: op.actor,
186
186
  body: p2.body,
187
+ ...p2.attachments !== void 0 && p2.attachments.length > 0 ? { attachments: p2.attachments.map((a) => ({ ...a })) } : {},
187
188
  state: "open",
188
189
  kind: p2.kind ?? defaultKind,
189
190
  stagedResolution: null,
@@ -1858,17 +1859,17 @@ var init_kernel = __esm({
1858
1859
  const artifact = this.artifactOrThrow(args.artifactId);
1859
1860
  this.enforceAcl("propose", this.schema.get(artifact.type), args.actor, artifact.owner);
1860
1861
  const proposingPart = args.part ?? null;
1861
- if (unresolvedGapsOf(this.model, args.artifactId).some((g2) => g2.part === proposingPart)) {
1862
+ if (proposingPart !== null && unresolvedGapsOf(this.model, args.artifactId).some((g2) => g2.part === proposingPart)) {
1862
1863
  throw new KernelError(
1863
- `${args.artifactId}${args.part ? `/${args.part}` : ""} \u6709\u672A\u89E3\u6790\u7684 gap\uFF0C\u4E0D\u80FD propose\u2014\u2014\u524D\u7F6E\u5C31\u7EEA\u4F1A\u91CD\u65B0\u5524\u8D77\u4F60\u7EED\u505A\uFF08\u7EBF2 #1\uFF1A\u63D0 gap \u7684\u5DE5\u4F5C\u9879\u4FDD\u6301 pending\uFF09\u3002`
1864
+ `\u5931\u8D25\uFF1A\u8FD9\u4E2A\u90E8\u4EF6\uFF08${args.part}\uFF09\u8FD8\u6709\u672A\u89E3\u51B3\u7684 gap\uFF0C\u4E0D\u80FD\u63D0\u4EA4\u3002\u53EF\u4EE5\u76F4\u63A5\u6536\u5DE5\u2014\u2014gap \u6709\u7ED3\u679C\u540E\u7CFB\u7EDF\u4F1A\u91CD\u65B0\u5524\u8D77\u4F60\u7EED\u505A\uFF0C\u4E0D\u7528\u7B49\u5728\u8FD9\u91CC\u3002`
1864
1865
  );
1865
1866
  }
1866
- const proposingHasOpenBackpressure = [...this.model.annotations.values()].some(
1867
+ const proposingHasOpenBackpressure = proposingPart !== null && [...this.model.annotations.values()].some(
1867
1868
  (a) => a.raisedFrom === args.artifactId && a.state === "open" && a.raisedFromPart === proposingPart
1868
1869
  );
1869
1870
  if (proposingHasOpenBackpressure) {
1870
1871
  throw new KernelError(
1871
- `${args.artifactId}${args.part ? `/${args.part}` : ""} \u6709\u672A\u7B54\u590D\u7684\u56DE\u538B annotation\uFF0C\u4E0D\u80FD propose\u2014\u2014\u7B49\u4E0A\u6E38\u56DE\u5E94\u540E\u7531\u6D3E\u53D1\u5668\u91CD\u6D3E\u4F60\u7EED\u505A\uFF08\u7EBF2 #1\uFF1A\u63D0\u56DE\u538B\u7684\u5DE5\u4F5C\u9879\u4FDD\u6301 pending\uFF09\u3002`
1872
+ `\u5931\u8D25\uFF1A\u8FD9\u4E2A\u90E8\u4EF6\uFF08${args.part}\uFF09\u8FD8\u6709\u672A\u7B54\u590D\u7684\u56DE\u538B\uFF08\u4F60\u5411\u4E0A\u6E38\u63D0\u7684\u7591\u95EE\u8FD8\u6CA1\u56DE\uFF09\uFF0C\u4E0D\u80FD\u63D0\u4EA4\u3002\u53EF\u4EE5\u76F4\u63A5\u6536\u5DE5\u2014\u2014\u4E0A\u6E38\u7B54\u590D\u540E\u7CFB\u7EDF\u4F1A\u91CD\u65B0\u5524\u8D77\u4F60\u7EED\u505A\u3002`
1872
1873
  );
1873
1874
  }
1874
1875
  const base = args.base === void 0 ? artifact.currentRev : args.base;
@@ -1892,6 +1893,14 @@ var init_kernel = __esm({
1892
1893
  `part "${args.part}" \u4E0D\u662F ${args.artifactId} \u58F0\u660E\u7684\u90E8\u4EF6\uFF08\u6709\uFF1A${(artifact.parts ?? []).map((p2) => p2.name).join("\u3001") || "\u65E0"}\uFF09`
1893
1894
  );
1894
1895
  }
1896
+ const alreadyProduced = revisionsOf(this.model, args.artifactId).some(
1897
+ (r) => r.part === args.part && r.state !== "rejected"
1898
+ );
1899
+ if (alreadyProduced) {
1900
+ throw new KernelError(
1901
+ `\u5931\u8D25\uFF1A\u8FD9\u4E2A\u90E8\u4EF6\uFF08${args.part}\uFF09\u5DF2\u7ECF\u63D0\u4EA4\u8FC7\u4E00\u7248\u4E86\u2014\u2014\u4E00\u90E8\u4EF6\u4E00\u7248\u3001\u4EA4\u4E86\u5373\u7EC8\u7ED3\uFF0C\u4E0D\u80FD\u518D\u4EA4\u3002\u53EF\u4EE5\u76F4\u63A5\u6536\u5DE5\u3002`
1902
+ );
1903
+ }
1895
1904
  }
1896
1905
  if (args.rebasedFrom !== void 0) {
1897
1906
  const old = this.model.revisions.get(args.rebasedFrom);
@@ -2081,6 +2090,12 @@ var init_kernel = __esm({
2081
2090
  `\u8FD8\u6709\u672A\u7B54\u590D\u7684\u56DE\u538B annotation\uFF08\u81EA\u63D0 ${openSelfAnno.length} \u6761\uFF09\uFF0C\u4E0D\u80FD conclude\u2014\u2014\u7B49\u4E0A\u6E38\u56DE\u5E94\u3001\u90A3\u6761 revision \u7EED\u5B8C\u518D\u6536\u53E3\uFF08\u7EBF2 \u2462\uFF09`
2082
2091
  );
2083
2092
  }
2093
+ const openReceived = openBackpressureOf(this.model, args.artifactId);
2094
+ if (openReceived.length > 0) {
2095
+ throw new KernelError(
2096
+ `\u5931\u8D25\uFF1A\u8FD8\u6709 ${openReceived.length} \u6761\u4E0B\u6E38\u56DE\u538B\u6CA1\u7B54\u590D\uFF08\u4E0B\u6E38\u660E\u786E\u6253\u56DE\u4E86\u5F53\u524D\u7248\u672C\uFF09\uFF0C\u4E0D\u80FD conclude\u2014\u2014\u5148\u6309\u56DE\u538B\u6539\u5185\u5BB9\u3001resolve \u56DE\u5E94\u4E0B\u6E38\uFF0C\u518D\u6536\u53E3\uFF08\u4E24\u5C42\u9501\u70B9 \xA76.3\uFF09\u3002`
2097
+ );
2098
+ }
2084
2099
  const unproduced = (artifact.parts ?? []).filter(
2085
2100
  (p2) => !revisionsOf(this.model, args.artifactId).some((r) => r.part === p2.name && r.state !== "rejected")
2086
2101
  );
@@ -2262,6 +2277,7 @@ var init_kernel = __esm({
2262
2277
  const payload = {
2263
2278
  annotationId,
2264
2279
  body: args.body,
2280
+ ...args.attachments !== void 0 && args.attachments.length > 0 ? { attachments: args.attachments } : {},
2265
2281
  // 信箱语义(v3.1 ①a):命令层一律盖章——新批注都是"寄给 owner 的信";存量旧 op 无此字段不参与唤醒
2266
2282
  mail: true,
2267
2283
  ...args.span !== void 0 ? { anchorSpan: args.span } : {},
@@ -2577,7 +2593,7 @@ var init_kernel = __esm({
2577
2593
  * 派生里程碑带 forced(by, reason),消费者可见。
2578
2594
  */
2579
2595
  async forceConclude(args) {
2580
- this.bounceHeadMovers(args.artifactId);
2596
+ this.bounceHeadMovers(args.artifactId, { allowPendingGate: true });
2581
2597
  this.ensureActive(args.artifactId);
2582
2598
  const artifact = this.artifactOrThrow(args.artifactId);
2583
2599
  if (artifact.currentRev === null) {
@@ -3029,10 +3045,10 @@ ${res.candidates.map((c) => ` - ${c.type}:${c.title ?? "(\u65E0title)"} \u2192
3029
3045
  return this.blobs;
3030
3046
  }
3031
3047
  /** 推进 head / 改变队列结局的命令:两档冻结都拦;收尾 gate 待审中 = 派生的 merge 档冻结(§5.7)。 */
3032
- bounceHeadMovers(artifactId) {
3048
+ bounceHeadMovers(artifactId, opts) {
3033
3049
  const scope = this.frozen.get(artifactId);
3034
3050
  if (scope) throw new FrozenError(artifactId, scope);
3035
- if (pendingConcludeAttempt(this.model, artifactId)) throw new FrozenError(artifactId, "merge");
3051
+ if (!opts?.allowPendingGate && pendingConcludeAttempt(this.model, artifactId)) throw new FrozenError(artifactId, "merge");
3036
3052
  }
3037
3053
  /** propose(emit)类:只有 write 档(§11.7 干预)才拦——merge 档下照常入队。 */
3038
3054
  bounceWrites(artifactId) {
@@ -3757,31 +3773,6 @@ function partHasOpenBackpressure(model, id, part) {
3757
3773
  function partProduced(model, artifactId, part) {
3758
3774
  return revisionsOf(model, artifactId).some((r) => r.part === part && r.state !== "rejected");
3759
3775
  }
3760
- function partLastRevSeq(model, artifactId, part) {
3761
- let max = null;
3762
- for (const r of revisionsOf(model, artifactId)) {
3763
- if (r.part !== part || r.state === "rejected") continue;
3764
- const seq = model.proposeSeq.get(r.id);
3765
- if (seq !== void 0 && (max === null || seq > max)) max = seq;
3766
- }
3767
- return max;
3768
- }
3769
- function partReworkNeeded(model, artifactId, part) {
3770
- const mySeq = partLastRevSeq(model, artifactId, part);
3771
- if (mySeq === null) return false;
3772
- for (const a of pendingBackpressureResponsesOf(model, artifactId)) {
3773
- if (a.raisedFromPart === part) return true;
3774
- }
3775
- for (const g2 of model.gaps.get(artifactId) ?? []) {
3776
- if (g2.part === part && g2.resolved && g2.resolvedAtSeq !== void 0 && g2.resolvedAtSeq > mySeq) return true;
3777
- }
3778
- const spec = (model.artifacts.get(artifactId)?.parts ?? []).find((p2) => p2.name === part);
3779
- for (const dep of spec?.deps ?? []) {
3780
- const depSeq = partLastRevSeq(model, artifactId, dep);
3781
- if (depSeq !== null && depSeq > mySeq) return true;
3782
- }
3783
- return false;
3784
- }
3785
3776
  function computeProduceJobs(model) {
3786
3777
  const out = [];
3787
3778
  for (const artifact of model.artifacts.values()) {
@@ -3801,7 +3792,7 @@ function computeProduceJobs(model) {
3801
3792
  if (partHasOpenBackpressure(model, artifact.id, null)) continue;
3802
3793
  for (const part of parts) {
3803
3794
  if (gaps.some((g2) => g2.part === part.name) || partHasOpenBackpressure(model, artifact.id, part.name)) continue;
3804
- if (partProduced(model, artifact.id, part.name) && !partReworkNeeded(model, artifact.id, part.name)) continue;
3795
+ if (partProduced(model, artifact.id, part.name)) continue;
3805
3796
  const deps = part.deps ?? [];
3806
3797
  if (!deps.every((d) => partProduced(model, artifact.id, d))) continue;
3807
3798
  out.push({ artifactId: artifact.id, part: part.name });
@@ -3817,7 +3808,6 @@ function computeIntegrateJobs(model) {
3817
3808
  if (!isProducible(model, artifact.id)) continue;
3818
3809
  if (openBackpressureOf(model, artifact.id).length > 0) continue;
3819
3810
  if (!parts.every((p2) => partProduced(model, artifact.id, p2.name))) continue;
3820
- if (parts.some((p2) => partReworkNeeded(model, artifact.id, p2.name))) continue;
3821
3811
  if (queueOf(model, artifact.id).length === 0) continue;
3822
3812
  out.push(artifact.id);
3823
3813
  }
@@ -21687,6 +21677,121 @@ var init_router = __esm({
21687
21677
  }
21688
21678
  });
21689
21679
 
21680
+ // ../server/src/live-chat.ts
21681
+ var DEFAULT_BUFFER_MAX, DEFAULT_GRACE_MS, LiveChatRegistry;
21682
+ var init_live_chat = __esm({
21683
+ "../server/src/live-chat.ts"() {
21684
+ "use strict";
21685
+ DEFAULT_BUFFER_MAX = 4e3;
21686
+ DEFAULT_GRACE_MS = 6e4;
21687
+ LiveChatRegistry = class {
21688
+ turns = /* @__PURE__ */ new Map();
21689
+ bufferMax;
21690
+ graceMs;
21691
+ constructor(opts = {}) {
21692
+ this.bufferMax = opts.bufferMax ?? DEFAULT_BUFFER_MAX;
21693
+ this.graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
21694
+ }
21695
+ /**
21696
+ * 开始一轮。若该会话已有驻留的旧轮(上一轮已 done 但还在 grace 期),先清掉。
21697
+ * 同一会话不允许两轮 running 并存——上层保证一个 chatSessionId 一次只发一轮。
21698
+ */
21699
+ start(chatSessionId, handle) {
21700
+ const prev = this.turns.get(chatSessionId);
21701
+ if (prev?.evictTimer) clearTimeout(prev.evictTimer);
21702
+ const turn = {
21703
+ chatSessionId,
21704
+ runtimeSessionId: handle.runtimeSessionId,
21705
+ ...handle.runId ? { runId: handle.runId } : {},
21706
+ seq: 0,
21707
+ buffer: [],
21708
+ minBufferedSeq: 0,
21709
+ subscribers: /* @__PURE__ */ new Set(),
21710
+ status: "running",
21711
+ handle
21712
+ };
21713
+ this.turns.set(chatSessionId, turn);
21714
+ return {
21715
+ runtimeSessionId: turn.runtimeSessionId,
21716
+ emit: (part) => {
21717
+ const withSeq = { ...part, seq: ++turn.seq };
21718
+ turn.buffer.push(withSeq);
21719
+ if (turn.minBufferedSeq === 0) turn.minBufferedSeq = withSeq.seq;
21720
+ if (turn.buffer.length > this.bufferMax) {
21721
+ const dropped = turn.buffer.splice(0, turn.buffer.length - this.bufferMax);
21722
+ const last = dropped[dropped.length - 1];
21723
+ if (last) turn.minBufferedSeq = last.seq + 1;
21724
+ }
21725
+ for (const sub of turn.subscribers) {
21726
+ try {
21727
+ sub(withSeq);
21728
+ } catch {
21729
+ }
21730
+ }
21731
+ return withSeq;
21732
+ },
21733
+ subscribe: (sub) => {
21734
+ turn.subscribers.add(sub);
21735
+ return () => {
21736
+ turn.subscribers.delete(sub);
21737
+ };
21738
+ },
21739
+ finish: (status) => {
21740
+ turn.status = status;
21741
+ turn.subscribers.clear();
21742
+ turn.evictTimer = setTimeout(() => {
21743
+ if (this.turns.get(chatSessionId) === turn) this.turns.delete(chatSessionId);
21744
+ }, this.graceMs);
21745
+ turn.evictTimer.unref?.();
21746
+ },
21747
+ bufferedParts: () => turn.buffer.slice()
21748
+ };
21749
+ }
21750
+ /**
21751
+ * 观众接入运行中的一轮。无该会话的 live 轮时返回 null(前端回退 getChatSessionDetail 读 DB 最终态)。
21752
+ * 有则返回需补发的历史 part + 当前状态 + detach。live part 后续经 sub 推送。
21753
+ */
21754
+ attach(chatSessionId, fromSeq, sub) {
21755
+ const turn = this.turns.get(chatSessionId);
21756
+ if (!turn) return null;
21757
+ const gap = turn.minBufferedSeq > 0 && fromSeq + 1 < turn.minBufferedSeq;
21758
+ const replay = turn.buffer.filter((p2) => p2.seq > fromSeq);
21759
+ if (turn.status === "running") {
21760
+ turn.subscribers.add(sub);
21761
+ }
21762
+ return {
21763
+ replay,
21764
+ status: turn.status,
21765
+ gap,
21766
+ detach: () => {
21767
+ turn.subscribers.delete(sub);
21768
+ }
21769
+ };
21770
+ }
21771
+ /** 显式停止:kill agent 这一轮。返回是否命中一个 live 轮。 */
21772
+ stop(chatSessionId) {
21773
+ const turn = this.turns.get(chatSessionId);
21774
+ if (!turn) return false;
21775
+ if (turn.status === "running") {
21776
+ try {
21777
+ turn.handle.kill();
21778
+ } catch {
21779
+ }
21780
+ }
21781
+ return true;
21782
+ }
21783
+ /** 该会话当前是否有一轮 running(供上层判断是否要挡并发发送)。 */
21784
+ isRunning(chatSessionId) {
21785
+ return this.turns.get(chatSessionId)?.status === "running";
21786
+ }
21787
+ /** 测试/排障用:当前驻留的会话数。 */
21788
+ size() {
21789
+ return this.turns.size;
21790
+ }
21791
+ };
21792
+ }
21793
+ });
21794
+
21690
21795
  // ../server/src/governance/workorder-drafts.ts
21691
21796
  var import_node_fs, WorkorderDraftStore;
21692
21797
  var init_workorder_drafts = __esm({
@@ -23315,6 +23420,17 @@ function truncateOutput(text) {
23315
23420
  return text.length > CHAT_OUTPUT_LIMIT ? `${text.slice(0, CHAT_OUTPUT_LIMIT)}
23316
23421
  ...[truncated]` : text;
23317
23422
  }
23423
+ function startStreamKeepalive(res, ms = 15e3) {
23424
+ const timer = setInterval(() => {
23425
+ if (res.destroyed || !res.writable) return;
23426
+ try {
23427
+ res.write("\n");
23428
+ } catch {
23429
+ }
23430
+ }, ms);
23431
+ timer.unref?.();
23432
+ return () => clearInterval(timer);
23433
+ }
23318
23434
  function chatPartFromTrajectoryEvent(event) {
23319
23435
  const payload = asObj2(event.payload);
23320
23436
  if (event.kind === "message") {
@@ -23418,6 +23534,16 @@ function optStr(args, key) {
23418
23534
  if (typeof v2 !== "string") throw new Error(`\u53C2\u6570 ${key} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
23419
23535
  return v2;
23420
23536
  }
23537
+ function attachmentArgs(args) {
23538
+ const raw = args["attachments"];
23539
+ if (!Array.isArray(raw)) return void 0;
23540
+ return raw.filter((a) => typeof a === "object" && a !== null).filter((a) => typeof a.name === "string" && (typeof a.text === "string" || typeof a.blobRef === "string")).slice(0, 5).map((a) => ({
23541
+ name: a.name,
23542
+ ...typeof a.text === "string" ? { text: a.text } : {},
23543
+ ...typeof a.blobRef === "string" ? { blobRef: a.blobRef } : {},
23544
+ ...typeof a.contentType === "string" ? { contentType: a.contentType } : {}
23545
+ }));
23546
+ }
23421
23547
  function hasOpenHumanChangeRequest2(kernel, artifactId) {
23422
23548
  return hasOpenHumanChangeRequest(kernel.model, artifactId);
23423
23549
  }
@@ -23802,7 +23928,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
23802
23928
  });
23803
23929
  const parts = kernel.model.artifacts.get(artifactId)?.parts ?? [];
23804
23930
  let landed = false;
23805
- if (!holdForReview && !requiresHumanReview && qBefore === 0 && parts.length <= 1) {
23931
+ if (!holdForReview && !requiresHumanReview && qBefore === 0 && (revision.part === void 0 || parts.length <= 1)) {
23806
23932
  try {
23807
23933
  const r = await kernel.advanceQueue(artifactId, actor);
23808
23934
  landed = r.landed.some((x2) => x2.id === revision.id);
@@ -23963,6 +24089,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
23963
24089
  }
23964
24090
  case "annotate": {
23965
24091
  const raisedFrom = optStr(args, "raisedFrom");
24092
+ const attachments = attachmentArgs(args);
23966
24093
  const mentions = Array.isArray(args["mentions"]) ? args["mentions"].filter((x2) => typeof x2 === "string" && x2.startsWith("actor:")) : void 0;
23967
24094
  if (mentions !== void 0 && mentions.length > 0 && isAgent(actor)) {
23968
24095
  throw new Error("@ \u6743\u53EA\u5C5E\u4E8E\u4EBA\uFF1Aagent \u627E agent \u7528\u56DE\u538B\uFF08annotate --from\uFF09\u6216\u5199\u4FE1\u5230\u5B83\u7684\u4EA7\u7269\uFF1B\u66FF\u4EBA @ \u9700\u5728 1:1 \u4F1A\u8BDD\u4E2D\uFF08\u7F72\u540D\u4EE3\u7F72\uFF09\u3002");
@@ -23974,6 +24101,7 @@ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
23974
24101
  ...hand !== void 0 ? { hand } : {},
23975
24102
  // 署名(§四):1:1 会话中作者=对话的人,agent 手留审计
23976
24103
  ...mentions !== void 0 && mentions.length > 0 ? { mentions } : {},
24104
+ ...attachments !== void 0 && attachments.length > 0 ? { attachments } : {},
23977
24105
  ...optStr(args, "revisionId") !== void 0 ? { revisionId: optStr(args, "revisionId") } : {},
23978
24106
  ...raisedFrom !== void 0 ? { raisedFrom } : {},
23979
24107
  ...optStr(args, "raisedFromPart") !== void 0 ? { raisedFromPart: optStr(args, "raisedFromPart") } : {},
@@ -24311,6 +24439,7 @@ async function startOasisServer(opts) {
24311
24439
  const resolveEngine = opts.resolveEngine ?? (async () => ({ kernel: opts.kernel, oplog: opts.oplog, blobs: opts.blobs, ...opts.registry ? { registry: opts.registry } : {} }));
24312
24440
  const domainRouter = new Router();
24313
24441
  for (const register of opts.domains ?? []) register(domainRouter);
24442
+ const liveChat = new LiveChatRegistry();
24314
24443
  const httpServer = http.createServer((req, res) => {
24315
24444
  void (async () => {
24316
24445
  try {
@@ -24937,6 +25066,61 @@ async function startOasisServer(opts) {
24937
25066
  }
24938
25067
  return;
24939
25068
  }
25069
+ if (req.method === "GET" && /^\/api\/chat-sessions\/[^/]+\/stream$/.test(url.pathname)) {
25070
+ const sid = decodeURIComponent(url.pathname.split("/")[3] ?? "");
25071
+ const cs = opts.chatSession ? await opts.chatSession.getSession(sid).catch(() => null) : null;
25072
+ if (!cs || cs.humanActorId !== actor) {
25073
+ res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "CHAT_SESSION_NOT_FOUND" }));
25074
+ return;
25075
+ }
25076
+ const fromSeq = Math.max(0, Number(url.searchParams.get("fromSeq") ?? "0") || 0);
25077
+ const sub = (part) => {
25078
+ if (res.destroyed) return;
25079
+ res.write(`${JSON.stringify(part)}
25080
+ `);
25081
+ if (part.type === "done") res.end();
25082
+ };
25083
+ const att = liveChat.attach(sid, fromSeq, sub);
25084
+ if (!att) {
25085
+ res.writeHead(204).end();
25086
+ return;
25087
+ }
25088
+ res.writeHead(200, {
25089
+ "content-type": "application/x-ndjson; charset=utf-8",
25090
+ "transfer-encoding": "chunked",
25091
+ "access-control-expose-headers": "X-Session-Id, X-Replay-Gap",
25092
+ "x-session-id": cs.runtimeSessionId ?? "",
25093
+ ...att.gap ? { "x-replay-gap": "1" } : {}
25094
+ });
25095
+ res.flushHeaders();
25096
+ const stopKa = startStreamKeepalive(res);
25097
+ for (const part of att.replay) {
25098
+ if (!res.destroyed) res.write(`${JSON.stringify(part)}
25099
+ `);
25100
+ }
25101
+ if (att.status !== "running") {
25102
+ att.detach();
25103
+ stopKa();
25104
+ res.end();
25105
+ return;
25106
+ }
25107
+ res.on("close", () => {
25108
+ stopKa();
25109
+ att.detach();
25110
+ });
25111
+ return;
25112
+ }
25113
+ if (req.method === "POST" && /^\/api\/chat-sessions\/[^/]+\/stop$/.test(url.pathname)) {
25114
+ const sid = decodeURIComponent(url.pathname.split("/")[3] ?? "");
25115
+ const cs = opts.chatSession ? await opts.chatSession.getSession(sid).catch(() => null) : null;
25116
+ if (!cs || cs.humanActorId !== actor) {
25117
+ res.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "CHAT_SESSION_NOT_FOUND" }));
25118
+ return;
25119
+ }
25120
+ const stopped = liveChat.stop(sid);
25121
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ stopped }));
25122
+ return;
25123
+ }
24940
25124
  if (url.pathname === "/api/chat" && req.method === "POST") {
24941
25125
  try {
24942
25126
  const body = JSON.parse(rawBody);
@@ -24980,23 +25164,52 @@ async function startOasisServer(opts) {
24980
25164
  ...persistAttachments.length ? { attachments: persistAttachments } : {}
24981
25165
  }).catch(() => void 0);
24982
25166
  }
24983
- let assistantText = "";
24984
- let persisted = false;
24985
- const streamedParts = [];
24986
- const finalizeAssistant = async () => {
24987
- if (persisted || !chatStore || !persistTarget) return;
24988
- persisted = true;
24989
- if (!assistantText && !session.runId && streamedParts.length === 0) return;
25167
+ let assistantMsgId;
25168
+ if (chatStore && persistTarget) {
24990
25169
  const { randomUUID: randomUUID19 } = await import("node:crypto");
25170
+ const id = randomUUID19();
24991
25171
  await chatStore.appendMessage({
24992
- id: randomUUID19(),
25172
+ id,
24993
25173
  sessionId: persistTarget.id,
24994
25174
  role: "assistant",
24995
- content: assistantText,
25175
+ content: "",
24996
25176
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
24997
- ...session.runId ? { runId: session.runId } : {},
24998
- ...streamedParts.length ? { parts: streamedParts } : {}
25177
+ status: "running",
25178
+ ...session.runId ? { runId: session.runId } : {}
25179
+ }).then(() => {
25180
+ assistantMsgId = id;
24999
25181
  }).catch(() => void 0);
25182
+ }
25183
+ let assistantText = "";
25184
+ let persisted = false;
25185
+ let live = null;
25186
+ const localParts = [];
25187
+ const collectedParts = () => (live ? live.bufferedParts() : localParts).filter((p2) => p2.type !== "done");
25188
+ const finalizeAssistant = async (status = "done") => {
25189
+ if (persisted || !chatStore || !persistTarget) return;
25190
+ persisted = true;
25191
+ const parts = collectedParts();
25192
+ if (assistantMsgId) {
25193
+ await chatStore.updateMessage(assistantMsgId, {
25194
+ content: assistantText,
25195
+ status,
25196
+ ...parts.length ? { parts } : {},
25197
+ ...session.runId ? { runId: session.runId } : {}
25198
+ }).catch(() => void 0);
25199
+ } else {
25200
+ if (!assistantText && !session.runId && parts.length === 0) return;
25201
+ const { randomUUID: randomUUID19 } = await import("node:crypto");
25202
+ await chatStore.appendMessage({
25203
+ id: randomUUID19(),
25204
+ sessionId: persistTarget.id,
25205
+ role: "assistant",
25206
+ content: assistantText,
25207
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
25208
+ status,
25209
+ ...session.runId ? { runId: session.runId } : {},
25210
+ ...parts.length ? { parts } : {}
25211
+ }).catch(() => void 0);
25212
+ }
25000
25213
  await chatStore.updateSession(persistTarget.id, {
25001
25214
  // 优先落 runtime-native id(codex rollout / ACP session/new 返回),让下一轮 resume
25002
25215
  // 能命中 runtime 认可的 id;claude-code 等接受任意外部 id 的仍回退 session.id 兼容原行为。
@@ -25005,21 +25218,40 @@ async function startOasisServer(opts) {
25005
25218
  }).catch(() => void 0);
25006
25219
  };
25007
25220
  let finished = false;
25221
+ let detachViewer;
25222
+ let stopKeepalive;
25008
25223
  res.on("close", () => {
25009
- if (!finished) {
25224
+ stopKeepalive?.();
25225
+ if (finished) return;
25226
+ if (live) {
25227
+ detachViewer?.();
25228
+ } else {
25010
25229
  void session.kill?.();
25011
25230
  void finalizeAssistant();
25012
25231
  }
25013
25232
  });
25014
25233
  if (typedParts) {
25015
- let seq = 0;
25016
- const writePart = (part) => {
25017
- const withSeq = { ...part, seq: ++seq };
25018
- if (part.type !== "done") streamedParts.push(withSeq);
25019
- if (res.destroyed) return;
25020
- res.write(`${JSON.stringify(withSeq)}
25234
+ live = body.chatSessionId ? liveChat.start(body.chatSessionId, {
25235
+ runtimeSessionId: session.id,
25236
+ ...session.runId ? { runId: session.runId } : {},
25237
+ kill: () => {
25238
+ void session.kill?.();
25239
+ }
25240
+ }) : null;
25241
+ let localSeq = 0;
25242
+ let ckptDirty = false;
25243
+ const writeToRes = (part) => {
25244
+ if (!res.destroyed) res.write(`${JSON.stringify(part)}
25021
25245
  `);
25022
25246
  };
25247
+ const emit = (part) => {
25248
+ ckptDirty = true;
25249
+ if (live) return live.emit(part);
25250
+ const withSeq = { ...part, seq: ++localSeq };
25251
+ if (part.type !== "done") localParts.push(withSeq);
25252
+ writeToRes(withSeq);
25253
+ return withSeq;
25254
+ };
25023
25255
  res.writeHead(200, {
25024
25256
  "content-type": "application/x-ndjson; charset=utf-8",
25025
25257
  "transfer-encoding": "chunked",
@@ -25027,25 +25259,40 @@ async function startOasisServer(opts) {
25027
25259
  "x-session-id": session.id,
25028
25260
  ...session.runId ? { "x-run-id": session.runId } : {}
25029
25261
  });
25262
+ res.flushHeaders();
25263
+ stopKeepalive = startStreamKeepalive(res);
25264
+ const ckptTimer = setInterval(() => {
25265
+ if (!ckptDirty || !assistantMsgId || !chatStore) return;
25266
+ ckptDirty = false;
25267
+ const parts = collectedParts();
25268
+ void chatStore.updateMessage(assistantMsgId, { content: assistantText, status: "running", ...parts.length ? { parts } : {} }).catch(() => void 0);
25269
+ }, 2e3);
25270
+ ckptTimer.unref?.();
25271
+ if (live) detachViewer = live.subscribe(writeToRes);
25030
25272
  let sawTextOutput = false;
25031
25273
  session.onOutput((chunk) => {
25032
25274
  sawTextOutput = true;
25033
25275
  assistantText += chunk;
25034
- writePart({ type: "text", text: chunk });
25276
+ emit({ type: "text", text: chunk });
25035
25277
  });
25036
25278
  session.onTelemetry?.((event) => {
25037
25279
  if (event.kind === "message" && sawTextOutput) return;
25038
25280
  const part = chatPartFromTrajectoryEvent(event);
25039
- if (part) writePart(part);
25281
+ if (part) emit(part);
25040
25282
  });
25283
+ let errored = false;
25041
25284
  try {
25042
25285
  await session.done;
25043
25286
  } catch (err) {
25044
- writePart({ type: "error", text: err instanceof Error ? err.message : String(err) });
25287
+ errored = true;
25288
+ emit({ type: "error", text: err instanceof Error ? err.message : String(err) });
25045
25289
  }
25046
25290
  finished = true;
25047
- await finalizeAssistant();
25048
- writePart({ type: "done", sessionId: session.id });
25291
+ clearInterval(ckptTimer);
25292
+ await finalizeAssistant(errored ? "error" : "done");
25293
+ emit({ type: "done", sessionId: session.id });
25294
+ live?.finish(errored ? "error" : "done");
25295
+ stopKeepalive?.();
25049
25296
  res.end();
25050
25297
  return;
25051
25298
  }
@@ -25481,6 +25728,7 @@ var init_server3 = __esm({
25481
25728
  init_src2();
25482
25729
  init_mcp2();
25483
25730
  init_router();
25731
+ init_live_chat();
25484
25732
  init_workorder_drafts();
25485
25733
  init_draft_edit();
25486
25734
  init_workorder_detail();
@@ -28684,6 +28932,17 @@ var init_dev_store = __esm({
28684
28932
  this.messages.set(m2.sessionId, list);
28685
28933
  return record5;
28686
28934
  }
28935
+ async updateMessage(id, patch) {
28936
+ for (const list of this.messages.values()) {
28937
+ const item = list.find((x2) => x2.id === id);
28938
+ if (!item) continue;
28939
+ if (patch.content !== void 0) item.content = patch.content;
28940
+ if (patch.parts !== void 0) item.parts = patch.parts;
28941
+ if (patch.status !== void 0) item.status = patch.status;
28942
+ if (patch.runId !== void 0) item.runId = patch.runId;
28943
+ return;
28944
+ }
28945
+ }
28687
28946
  async listMessages(sessionId, limit) {
28688
28947
  const list = (this.messages.get(sessionId) ?? []).sort((a, b2) => a.seq - b2.seq);
28689
28948
  return limit ? list.slice(-limit) : list;
@@ -28732,6 +28991,10 @@ var init_dev_store = __esm({
28732
28991
  this.save();
28733
28992
  return record5;
28734
28993
  }
28994
+ async updateMessage(id, patch) {
28995
+ await super.updateMessage(id, patch);
28996
+ this.save();
28997
+ }
28735
28998
  async linkWorkOrder(sessionId, workOrderId) {
28736
28999
  await super.linkWorkOrder(sessionId, workOrderId);
28737
29000
  this.save();
@@ -37714,6 +37977,31 @@ function codexSessionsRoot() {
37714
37977
  }
37715
37978
  return "";
37716
37979
  }
37980
+ function codexRolloutExists(sessionId, root = codexSessionsRoot()) {
37981
+ if (!sessionId || !root) return false;
37982
+ const suffix = `-${sessionId}.jsonl`;
37983
+ let hit = false;
37984
+ const walk = (d) => {
37985
+ if (hit) return;
37986
+ let ents;
37987
+ try {
37988
+ ents = fs8.readdirSync(d, { withFileTypes: true });
37989
+ } catch {
37990
+ return;
37991
+ }
37992
+ for (const ent of ents) {
37993
+ if (hit) return;
37994
+ const p2 = path8.join(d, ent.name);
37995
+ if (ent.isDirectory()) walk(p2);
37996
+ else if (ent.isFile() && ent.name.startsWith("rollout-") && ent.name.endsWith(suffix)) {
37997
+ hit = true;
37998
+ return;
37999
+ }
38000
+ }
38001
+ };
38002
+ walk(root);
38003
+ return hit;
38004
+ }
37717
38005
  function listRecentRollouts(root, sinceMs) {
37718
38006
  const margin = sinceMs - 6e4;
37719
38007
  const found = [];
@@ -37964,14 +38252,17 @@ var init_codex = __esm({
37964
38252
  const task = job.bundle.files["TASK.md"];
37965
38253
  if (!task) throw new Error("bundle \u7F3A TASK.md\uFF08\u88C5\u914D\u5668\u5951\u7EA6\uFF09");
37966
38254
  const prompt = job.systemPrompt ? ["# System Prompt", job.systemPrompt, "", "# Task", task].join("\n") : task;
37967
- const resumeArgs = job.resumeRuntimeSession && job.runtimeSessionId ? ["exec", "resume", job.runtimeSessionId] : ["exec"];
38255
+ const isResume = Boolean(
38256
+ job.resumeRuntimeSession && job.runtimeSessionId && codexRolloutExists(job.runtimeSessionId)
38257
+ );
37968
38258
  const args = [
37969
- ...resumeArgs,
38259
+ "exec",
38260
+ "-C",
38261
+ dir,
38262
+ ...isResume ? ["resume", job.runtimeSessionId] : [],
37970
38263
  "--json",
37971
38264
  "--dangerously-bypass-approvals-and-sandbox",
37972
38265
  "--skip-git-repo-check",
37973
- "-C",
37974
- dir,
37975
38266
  ...job.model ?? this.opts.model ? ["--model", job.model ?? this.opts.model] : [],
37976
38267
  // 打开 reasoning 摘要通道:默认 auto,让 codex 产出 reasoning item(→ thought → 前端思考块)。
37977
38268
  ...this.opts.reasoningSummary !== "none" ? ["-c", `model_reasoning_summary=${this.opts.reasoningSummary ?? "auto"}`] : [],
@@ -38577,6 +38868,9 @@ function parseArgsJSON(argsText) {
38577
38868
  function extractSessionID(result) {
38578
38869
  return result?.sessionId ?? "";
38579
38870
  }
38871
+ function extractStopReason(result) {
38872
+ return result?.stopReason ?? "";
38873
+ }
38580
38874
  async function runACPSession(job, cfg) {
38581
38875
  const slug6 = job.artifactId.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32);
38582
38876
  const binTag = path9.basename(cfg.bin).replace(/[^a-zA-Z0-9]/g, "").slice(0, 12) || "acp";
@@ -38611,8 +38905,11 @@ async function runACPSession(job, cfg) {
38611
38905
  let killedByUs = false;
38612
38906
  let actualModel;
38613
38907
  let capturedSessionId;
38908
+ let usageClient;
38614
38909
  const finish = (info) => {
38615
38910
  if (exited) return;
38911
+ const usage = info.usage ?? (usageClient ? buildUsage(usageClient.usage, estimateCost(usageClient.usage, actualModel)) : void 0);
38912
+ if (usage && !info.usage) info = { ...info, usage };
38616
38913
  if (actualModel && !info.model) info = { ...info, model: actualModel };
38617
38914
  if (capturedSessionId && !info.runtimeSessionId) info = { ...info, runtimeSessionId: capturedSessionId };
38618
38915
  if (cfg.cleanupWorkdir) {
@@ -38660,6 +38957,7 @@ async function runACPSession(job, cfg) {
38660
38957
  },
38661
38958
  () => streamingCurrentTurn
38662
38959
  );
38960
+ usageClient = client;
38663
38961
  const rl = readline3.createInterface({ input: child.stdout });
38664
38962
  const readerDone = new Promise((resolve4) => {
38665
38963
  rl.on("line", (line) => {
@@ -38677,7 +38975,9 @@ async function runACPSession(job, cfg) {
38677
38975
  });
38678
38976
  child.on("exit", (code) => {
38679
38977
  clearTimeout(timer);
38680
- finish({ code: killedByUs ? null : code, reason: killedByUs ? "timeout" : code === 0 ? "clean" : "error", transcriptRef });
38978
+ void readerDone.then(() => {
38979
+ finish({ code: killedByUs ? null : code, reason: killedByUs ? "timeout" : code === 0 ? "clean" : "error", transcriptRef });
38980
+ });
38681
38981
  });
38682
38982
  void (async () => {
38683
38983
  try {
@@ -38688,6 +38988,7 @@ async function runACPSession(job, cfg) {
38688
38988
  });
38689
38989
  const resolvedModel = job.model ?? cfg.model;
38690
38990
  let sessionId = "";
38991
+ let ambiguousResume = false;
38691
38992
  if (job.resumeRuntimeSession && job.runtimeSessionId) {
38692
38993
  const method = cfg.resumeMethod ?? "session/load";
38693
38994
  try {
@@ -38697,7 +38998,11 @@ async function runACPSession(job, cfg) {
38697
38998
  mcpServers: [],
38698
38999
  ...resolvedModel ? { model: resolvedModel } : {}
38699
39000
  });
38700
- sessionId = extractSessionID(resumeRes) || job.runtimeSessionId;
39001
+ sessionId = extractSessionID(resumeRes);
39002
+ if (!sessionId) {
39003
+ sessionId = job.runtimeSessionId;
39004
+ ambiguousResume = true;
39005
+ }
38701
39006
  } catch {
38702
39007
  sessionId = "";
38703
39008
  }
@@ -38722,10 +39027,24 @@ async function runACPSession(job, cfg) {
38722
39027
 
38723
39028
  ${task}` : task;
38724
39029
  streamingCurrentTurn = true;
38725
- await client.request("session/prompt", {
39030
+ let promptResult = await client.request("session/prompt", {
38726
39031
  sessionId,
38727
39032
  prompt: [{ type: "text", text: userText }]
38728
39033
  });
39034
+ if (ambiguousResume && extractStopReason(promptResult) === "refusal" && !hasUsage(client.usage)) {
39035
+ const sessionRes = await client.request("session/new", {
39036
+ cwd: dir,
39037
+ mcpServers: [],
39038
+ ...resolvedModel ? { model: resolvedModel } : {}
39039
+ });
39040
+ sessionId = extractSessionID(sessionRes);
39041
+ if (!sessionId) throw new Error("session/new returned no sessionId");
39042
+ capturedSessionId = sessionId;
39043
+ promptResult = await client.request("session/prompt", {
39044
+ sessionId,
39045
+ prompt: [{ type: "text", text: userText }]
39046
+ });
39047
+ }
38729
39048
  const usage = buildUsage(client.usage, estimateCost(client.usage, actualModel));
38730
39049
  if (!exited) finish({ code: 0, reason: "clean", transcriptRef, usage, model: actualModel });
38731
39050
  child.kill("SIGTERM");
@@ -39975,39 +40294,47 @@ function nodesDomain(deps) {
39975
40294
  if (msg.type !== "hello") return;
39976
40295
  const d = hub.connectedDaemons().find((c) => c.daemonId === daemonId);
39977
40296
  if (!d) return;
39978
- if (msg.meta.runtimes) {
39979
- const existing = await nodeStore.getNode(daemonId);
39980
- await nodeStore.upsertNode({
39981
- id: daemonId,
39982
- hostname: d.meta.hostname,
39983
- nodeVersion: d.meta.nodeVersion ?? "unknown",
39984
- status: "online",
39985
- // revives a previously soft-deleted node
39986
- lastSeenAt: now(),
39987
- enrolledAt: existing?.enrolledAt ?? now(),
39988
- ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
39989
- });
39990
- for (const cap of msg.meta.runtimes) {
39991
- await nodeStore.addRuntimeIfAbsent({
39992
- id: `runtime:${daemonId}:${cap.kind}`,
39993
- nodeId: daemonId,
39994
- kind: cap.kind,
40297
+ try {
40298
+ if (msg.meta.runtimes) {
40299
+ const existing = await nodeStore.getNode(daemonId);
40300
+ await nodeStore.upsertNode({
40301
+ id: daemonId,
39995
40302
  hostname: d.meta.hostname,
39996
- ...cap.binary ? { binary: cap.binary } : {},
39997
- ...cap.version ? { version: cap.version } : {},
40303
+ nodeVersion: d.meta.nodeVersion ?? "unknown",
39998
40304
  status: "online",
39999
- lastSeenAt: now()
40305
+ // revives a previously soft-deleted node
40306
+ lastSeenAt: now(),
40307
+ enrolledAt: existing?.enrolledAt ?? now(),
40308
+ ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
40000
40309
  });
40310
+ for (const cap of msg.meta.runtimes) {
40311
+ await nodeStore.addRuntimeIfAbsent({
40312
+ id: `runtime:${daemonId}:${cap.kind}`,
40313
+ nodeId: daemonId,
40314
+ kind: cap.kind,
40315
+ hostname: d.meta.hostname,
40316
+ ...cap.binary ? { binary: cap.binary } : {},
40317
+ ...cap.version ? { version: cap.version } : {},
40318
+ status: "online",
40319
+ lastSeenAt: now()
40320
+ });
40321
+ }
40322
+ await nodeStore.setNodeRuntimesOnline(daemonId, true);
40323
+ } else {
40324
+ await nodeStore.setNodeOnline(daemonId, true);
40325
+ await nodeStore.setNodeRuntimesOnline(daemonId, true);
40001
40326
  }
40002
- await nodeStore.setNodeRuntimesOnline(daemonId, true);
40003
- } else {
40004
- await nodeStore.setNodeOnline(daemonId, true);
40005
- await nodeStore.setNodeRuntimesOnline(daemonId, true);
40327
+ } catch (err) {
40328
+ console.warn(`[nodes] hello \u5904\u7406\u5931\u8D25(node=${daemonId})\uFF1A${err instanceof Error ? err.message : String(err)}`);
40006
40329
  }
40007
40330
  });
40008
40331
  hub.onDaemonDisconnected = async (daemonId) => {
40009
- await nodeStore.setNodeOnline(daemonId, false);
40010
- await nodeStore.setNodeRuntimesOnline(daemonId, false);
40332
+ try {
40333
+ await nodeStore.setNodeOnline(daemonId, false);
40334
+ await nodeStore.setNodeRuntimesOnline(daemonId, false);
40335
+ } catch (err) {
40336
+ console.warn(`[nodes] disconnect \u5904\u7406\u5931\u8D25(node=${daemonId})\uFF1A${err instanceof Error ? err.message : String(err)}`);
40337
+ }
40011
40338
  };
40012
40339
  }
40013
40340
  return (router) => {
@@ -40821,6 +41148,39 @@ function collabDomain(opts) {
40821
41148
  if (draft) return { status: 200, body: buildWorkorderDraftDetail(draft, resolve4) };
40822
41149
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${req.params.id}` } } };
40823
41150
  });
41151
+ router.post("/api/workorders/:id/hold", async (req) => {
41152
+ const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
41153
+ const ws = req.params.id;
41154
+ const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws);
41155
+ if (arts.length === 0) {
41156
+ return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
41157
+ }
41158
+ if (!artifacts) {
41159
+ return { status: 503, body: { error: { code: "no_store", message: "\u63A7\u5236\u9762\u65C1\u5B58\u672A\u63A5\u5165\uFF0C\u65E0\u6CD5\u6682\u505C" } } };
41160
+ }
41161
+ const held = (await artifacts.listWorkspaceDispatchHeld()).includes(ws);
41162
+ if (held) return { status: 200, body: { held: false } };
41163
+ await artifacts.setWorkspaceDispatchHold(ws, true);
41164
+ return { status: 200, body: { held: true } };
41165
+ });
41166
+ router.post("/api/workorders/:id/cancel", async (req) => {
41167
+ if (!isHumanActor(req.auth.actor)) {
41168
+ return { status: 403, body: { error: { code: "forbidden", message: "\u53D6\u6D88\u5DE5\u5355\u662F\u9500\u6BC1\u6027\u6CBB\u7406\u64CD\u4F5C\uFF0C\u4EC5\u4EBA\u7C7B\u53EF\u6267\u884C" } } };
41169
+ }
41170
+ const { kernel } = await resolveCtx(req.auth.companyId);
41171
+ const ws = req.params.id;
41172
+ const active = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws && lifecycleOf(kernel.model, a.id) === "active");
41173
+ const all = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws);
41174
+ if (all.length === 0) {
41175
+ return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
41176
+ }
41177
+ let cancelled = 0;
41178
+ for (const a of active) {
41179
+ await kernel.seal({ artifactId: a.id, actor: req.auth.actor, reason: "cancelled", note: "\u5DE5\u5355\u53D6\u6D88\uFF08\u6279\u91CF\uFF09" });
41180
+ cancelled += 1;
41181
+ }
41182
+ return { status: 200, body: { cancelled } };
41183
+ });
40824
41184
  router.post("/api/workorders/:id/dispatch", async (req) => {
40825
41185
  const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
40826
41186
  const ws = req.params.id;
@@ -40891,6 +41251,7 @@ function collabDomain(opts) {
40891
41251
  var init_collab = __esm({
40892
41252
  "../server/src/domains/collab/index.ts"() {
40893
41253
  "use strict";
41254
+ init_src2();
40894
41255
  init_workorders();
40895
41256
  init_workorder_detail();
40896
41257
  init_inbox();
@@ -53957,6 +54318,7 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
53957
54318
  await pool.query(`ALTER TABLE "${s2}".chat_messages ADD COLUMN IF NOT EXISTS run_id text`);
53958
54319
  await pool.query(`ALTER TABLE "${s2}".chat_messages ADD COLUMN IF NOT EXISTS parts jsonb`);
53959
54320
  await pool.query(`ALTER TABLE "${s2}".chat_messages ADD COLUMN IF NOT EXISTS attachments jsonb`);
54321
+ await pool.query(`ALTER TABLE "${s2}".chat_messages ADD COLUMN IF NOT EXISTS status text`);
53960
54322
  await pool.query(`ALTER TABLE "${s2}".chat_sessions ADD COLUMN IF NOT EXISTS workspace text`);
53961
54323
  await pool.query(`
53962
54324
  WITH ranked AS (
@@ -54022,11 +54384,11 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
54022
54384
  async appendMessage(m2) {
54023
54385
  const insert = async () => {
54024
54386
  const r = await this.pool.query(
54025
- `INSERT INTO ${this.s}.chat_messages (id, session_id, role, content, seq, created_at, run_id, parts, attachments)
54026
- SELECT $1,$2,$3,$4, COALESCE(MAX(seq),0)+1, $5,$6,$7,$8
54387
+ `INSERT INTO ${this.s}.chat_messages (id, session_id, role, content, seq, created_at, run_id, parts, attachments, status)
54388
+ SELECT $1,$2,$3,$4, COALESCE(MAX(seq),0)+1, $5,$6,$7,$8,$9
54027
54389
  FROM ${this.s}.chat_messages WHERE session_id = $2
54028
54390
  RETURNING seq`,
54029
- [m2.id, m2.sessionId, m2.role, m2.content, m2.createdAt, m2.runId ?? null, m2.parts !== void 0 ? JSON.stringify(m2.parts) : null, m2.attachments !== void 0 ? JSON.stringify(m2.attachments) : null]
54391
+ [m2.id, m2.sessionId, m2.role, m2.content, m2.createdAt, m2.runId ?? null, m2.parts !== void 0 ? JSON.stringify(m2.parts) : null, m2.attachments !== void 0 ? JSON.stringify(m2.attachments) : null, m2.status ?? null]
54030
54392
  );
54031
54393
  return Number(r.rows[0].seq);
54032
54394
  };
@@ -54040,6 +54402,29 @@ var PostgresChatSessionStore = class _PostgresChatSessionStore {
54040
54402
  }
54041
54403
  }
54042
54404
  }
54405
+ async updateMessage(id, patch) {
54406
+ const sets = [];
54407
+ const args = [];
54408
+ if (patch.content !== void 0) {
54409
+ args.push(patch.content);
54410
+ sets.push(`content = $${args.length}`);
54411
+ }
54412
+ if (patch.parts !== void 0) {
54413
+ args.push(JSON.stringify(patch.parts));
54414
+ sets.push(`parts = $${args.length}`);
54415
+ }
54416
+ if (patch.status !== void 0) {
54417
+ args.push(patch.status);
54418
+ sets.push(`status = $${args.length}`);
54419
+ }
54420
+ if (patch.runId !== void 0) {
54421
+ args.push(patch.runId);
54422
+ sets.push(`run_id = $${args.length}`);
54423
+ }
54424
+ if (sets.length === 0) return;
54425
+ args.push(id);
54426
+ await this.pool.query(`UPDATE ${this.s}.chat_messages SET ${sets.join(", ")} WHERE id = $${args.length}`, args);
54427
+ }
54043
54428
  async listMessages(sessionId, limit) {
54044
54429
  const limitClause = limit ? `LIMIT ${limit}` : "";
54045
54430
  const r = await this.pool.query(
@@ -54083,7 +54468,8 @@ var rowToMessage = (row) => ({
54083
54468
  createdAt: new Date(row.created_at).toISOString(),
54084
54469
  ...row.run_id != null ? { runId: row.run_id } : {},
54085
54470
  ...row.parts != null ? { parts: row.parts } : {},
54086
- ...row.attachments != null ? { attachments: row.attachments } : {}
54471
+ ...row.attachments != null ? { attachments: row.attachments } : {},
54472
+ ...row.status != null ? { status: row.status } : {}
54087
54473
  });
54088
54474
 
54089
54475
  // ../storage/src/postgres-nodes.ts
@@ -54787,6 +55173,9 @@ async function startServe(opts) {
54787
55173
  return service.listSkillFiles(skillId);
54788
55174
  }
54789
55175
  });
55176
+ process.on("unhandledRejection", (reason) => {
55177
+ console.error(`[serve] unhandledRejection\uFF08\u5DF2\u515C\u5E95\uFF0C\u4E0D\u5D29\u8FDB\u7A0B\uFF09\uFF1A${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
55178
+ });
54790
55179
  const server = await startOasisServer({
54791
55180
  kernel: engine.kernel,
54792
55181
  blobs: engine.blobs,
@@ -57730,7 +58119,7 @@ ${res.warning}`);
57730
58119
  }
57731
58120
 
57732
58121
  // src/index.ts
57733
- var PKG_VERSION = true ? "0.1.36" : "dev";
58122
+ var PKG_VERSION = true ? "0.1.38" : "dev";
57734
58123
  var OASIS_DIR = path18.join(os10.homedir(), ".oasis");
57735
58124
  var CONFIG_FILE = path18.join(OASIS_DIR, "node-config.json");
57736
58125
  var PID_FILE = path18.join(OASIS_DIR, "node.pid");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"