oasis_test 0.1.36 → 0.1.37

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 +332 -57
  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);
@@ -24982,11 +25166,14 @@ async function startOasisServer(opts) {
24982
25166
  }
24983
25167
  let assistantText = "";
24984
25168
  let persisted = false;
24985
- const streamedParts = [];
25169
+ let live = null;
25170
+ const localParts = [];
25171
+ const collectedParts = () => (live ? live.bufferedParts() : localParts).filter((p2) => p2.type !== "done");
24986
25172
  const finalizeAssistant = async () => {
24987
25173
  if (persisted || !chatStore || !persistTarget) return;
24988
25174
  persisted = true;
24989
- if (!assistantText && !session.runId && streamedParts.length === 0) return;
25175
+ const parts = collectedParts();
25176
+ if (!assistantText && !session.runId && parts.length === 0) return;
24990
25177
  const { randomUUID: randomUUID19 } = await import("node:crypto");
24991
25178
  await chatStore.appendMessage({
24992
25179
  id: randomUUID19(),
@@ -24995,7 +25182,7 @@ async function startOasisServer(opts) {
24995
25182
  content: assistantText,
24996
25183
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
24997
25184
  ...session.runId ? { runId: session.runId } : {},
24998
- ...streamedParts.length ? { parts: streamedParts } : {}
25185
+ ...parts.length ? { parts } : {}
24999
25186
  }).catch(() => void 0);
25000
25187
  await chatStore.updateSession(persistTarget.id, {
25001
25188
  // 优先落 runtime-native id(codex rollout / ACP session/new 返回),让下一轮 resume
@@ -25005,21 +25192,38 @@ async function startOasisServer(opts) {
25005
25192
  }).catch(() => void 0);
25006
25193
  };
25007
25194
  let finished = false;
25195
+ let detachViewer;
25196
+ let stopKeepalive;
25008
25197
  res.on("close", () => {
25009
- if (!finished) {
25198
+ stopKeepalive?.();
25199
+ if (finished) return;
25200
+ if (live) {
25201
+ detachViewer?.();
25202
+ } else {
25010
25203
  void session.kill?.();
25011
25204
  void finalizeAssistant();
25012
25205
  }
25013
25206
  });
25014
25207
  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)}
25208
+ live = body.chatSessionId ? liveChat.start(body.chatSessionId, {
25209
+ runtimeSessionId: session.id,
25210
+ ...session.runId ? { runId: session.runId } : {},
25211
+ kill: () => {
25212
+ void session.kill?.();
25213
+ }
25214
+ }) : null;
25215
+ let localSeq = 0;
25216
+ const writeToRes = (part) => {
25217
+ if (!res.destroyed) res.write(`${JSON.stringify(part)}
25021
25218
  `);
25022
25219
  };
25220
+ const emit = (part) => {
25221
+ if (live) return live.emit(part);
25222
+ const withSeq = { ...part, seq: ++localSeq };
25223
+ if (part.type !== "done") localParts.push(withSeq);
25224
+ writeToRes(withSeq);
25225
+ return withSeq;
25226
+ };
25023
25227
  res.writeHead(200, {
25024
25228
  "content-type": "application/x-ndjson; charset=utf-8",
25025
25229
  "transfer-encoding": "chunked",
@@ -25027,25 +25231,32 @@ async function startOasisServer(opts) {
25027
25231
  "x-session-id": session.id,
25028
25232
  ...session.runId ? { "x-run-id": session.runId } : {}
25029
25233
  });
25234
+ res.flushHeaders();
25235
+ stopKeepalive = startStreamKeepalive(res);
25236
+ if (live) detachViewer = live.subscribe(writeToRes);
25030
25237
  let sawTextOutput = false;
25031
25238
  session.onOutput((chunk) => {
25032
25239
  sawTextOutput = true;
25033
25240
  assistantText += chunk;
25034
- writePart({ type: "text", text: chunk });
25241
+ emit({ type: "text", text: chunk });
25035
25242
  });
25036
25243
  session.onTelemetry?.((event) => {
25037
25244
  if (event.kind === "message" && sawTextOutput) return;
25038
25245
  const part = chatPartFromTrajectoryEvent(event);
25039
- if (part) writePart(part);
25246
+ if (part) emit(part);
25040
25247
  });
25248
+ let errored = false;
25041
25249
  try {
25042
25250
  await session.done;
25043
25251
  } catch (err) {
25044
- writePart({ type: "error", text: err instanceof Error ? err.message : String(err) });
25252
+ errored = true;
25253
+ emit({ type: "error", text: err instanceof Error ? err.message : String(err) });
25045
25254
  }
25046
25255
  finished = true;
25047
25256
  await finalizeAssistant();
25048
- writePart({ type: "done", sessionId: session.id });
25257
+ emit({ type: "done", sessionId: session.id });
25258
+ live?.finish(errored ? "error" : "done");
25259
+ stopKeepalive?.();
25049
25260
  res.end();
25050
25261
  return;
25051
25262
  }
@@ -25481,6 +25692,7 @@ var init_server3 = __esm({
25481
25692
  init_src2();
25482
25693
  init_mcp2();
25483
25694
  init_router();
25695
+ init_live_chat();
25484
25696
  init_workorder_drafts();
25485
25697
  init_draft_edit();
25486
25698
  init_workorder_detail();
@@ -37964,14 +38176,15 @@ var init_codex = __esm({
37964
38176
  const task = job.bundle.files["TASK.md"];
37965
38177
  if (!task) throw new Error("bundle \u7F3A TASK.md\uFF08\u88C5\u914D\u5668\u5951\u7EA6\uFF09");
37966
38178
  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"];
38179
+ const isResume = Boolean(job.resumeRuntimeSession && job.runtimeSessionId);
37968
38180
  const args = [
37969
- ...resumeArgs,
38181
+ "exec",
38182
+ "-C",
38183
+ dir,
38184
+ ...isResume ? ["resume", job.runtimeSessionId] : [],
37970
38185
  "--json",
37971
38186
  "--dangerously-bypass-approvals-and-sandbox",
37972
38187
  "--skip-git-repo-check",
37973
- "-C",
37974
- dir,
37975
38188
  ...job.model ?? this.opts.model ? ["--model", job.model ?? this.opts.model] : [],
37976
38189
  // 打开 reasoning 摘要通道:默认 auto,让 codex 产出 reasoning item(→ thought → 前端思考块)。
37977
38190
  ...this.opts.reasoningSummary !== "none" ? ["-c", `model_reasoning_summary=${this.opts.reasoningSummary ?? "auto"}`] : [],
@@ -38577,6 +38790,9 @@ function parseArgsJSON(argsText) {
38577
38790
  function extractSessionID(result) {
38578
38791
  return result?.sessionId ?? "";
38579
38792
  }
38793
+ function extractStopReason(result) {
38794
+ return result?.stopReason ?? "";
38795
+ }
38580
38796
  async function runACPSession(job, cfg) {
38581
38797
  const slug6 = job.artifactId.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32);
38582
38798
  const binTag = path9.basename(cfg.bin).replace(/[^a-zA-Z0-9]/g, "").slice(0, 12) || "acp";
@@ -38611,8 +38827,11 @@ async function runACPSession(job, cfg) {
38611
38827
  let killedByUs = false;
38612
38828
  let actualModel;
38613
38829
  let capturedSessionId;
38830
+ let usageClient;
38614
38831
  const finish = (info) => {
38615
38832
  if (exited) return;
38833
+ const usage = info.usage ?? (usageClient ? buildUsage(usageClient.usage, estimateCost(usageClient.usage, actualModel)) : void 0);
38834
+ if (usage && !info.usage) info = { ...info, usage };
38616
38835
  if (actualModel && !info.model) info = { ...info, model: actualModel };
38617
38836
  if (capturedSessionId && !info.runtimeSessionId) info = { ...info, runtimeSessionId: capturedSessionId };
38618
38837
  if (cfg.cleanupWorkdir) {
@@ -38660,6 +38879,7 @@ async function runACPSession(job, cfg) {
38660
38879
  },
38661
38880
  () => streamingCurrentTurn
38662
38881
  );
38882
+ usageClient = client;
38663
38883
  const rl = readline3.createInterface({ input: child.stdout });
38664
38884
  const readerDone = new Promise((resolve4) => {
38665
38885
  rl.on("line", (line) => {
@@ -38677,7 +38897,9 @@ async function runACPSession(job, cfg) {
38677
38897
  });
38678
38898
  child.on("exit", (code) => {
38679
38899
  clearTimeout(timer);
38680
- finish({ code: killedByUs ? null : code, reason: killedByUs ? "timeout" : code === 0 ? "clean" : "error", transcriptRef });
38900
+ void readerDone.then(() => {
38901
+ finish({ code: killedByUs ? null : code, reason: killedByUs ? "timeout" : code === 0 ? "clean" : "error", transcriptRef });
38902
+ });
38681
38903
  });
38682
38904
  void (async () => {
38683
38905
  try {
@@ -38688,6 +38910,7 @@ async function runACPSession(job, cfg) {
38688
38910
  });
38689
38911
  const resolvedModel = job.model ?? cfg.model;
38690
38912
  let sessionId = "";
38913
+ let ambiguousResume = false;
38691
38914
  if (job.resumeRuntimeSession && job.runtimeSessionId) {
38692
38915
  const method = cfg.resumeMethod ?? "session/load";
38693
38916
  try {
@@ -38697,7 +38920,11 @@ async function runACPSession(job, cfg) {
38697
38920
  mcpServers: [],
38698
38921
  ...resolvedModel ? { model: resolvedModel } : {}
38699
38922
  });
38700
- sessionId = extractSessionID(resumeRes) || job.runtimeSessionId;
38923
+ sessionId = extractSessionID(resumeRes);
38924
+ if (!sessionId) {
38925
+ sessionId = job.runtimeSessionId;
38926
+ ambiguousResume = true;
38927
+ }
38701
38928
  } catch {
38702
38929
  sessionId = "";
38703
38930
  }
@@ -38722,10 +38949,24 @@ async function runACPSession(job, cfg) {
38722
38949
 
38723
38950
  ${task}` : task;
38724
38951
  streamingCurrentTurn = true;
38725
- await client.request("session/prompt", {
38952
+ let promptResult = await client.request("session/prompt", {
38726
38953
  sessionId,
38727
38954
  prompt: [{ type: "text", text: userText }]
38728
38955
  });
38956
+ if (ambiguousResume && extractStopReason(promptResult) === "refusal" && !hasUsage(client.usage)) {
38957
+ const sessionRes = await client.request("session/new", {
38958
+ cwd: dir,
38959
+ mcpServers: [],
38960
+ ...resolvedModel ? { model: resolvedModel } : {}
38961
+ });
38962
+ sessionId = extractSessionID(sessionRes);
38963
+ if (!sessionId) throw new Error("session/new returned no sessionId");
38964
+ capturedSessionId = sessionId;
38965
+ promptResult = await client.request("session/prompt", {
38966
+ sessionId,
38967
+ prompt: [{ type: "text", text: userText }]
38968
+ });
38969
+ }
38729
38970
  const usage = buildUsage(client.usage, estimateCost(client.usage, actualModel));
38730
38971
  if (!exited) finish({ code: 0, reason: "clean", transcriptRef, usage, model: actualModel });
38731
38972
  child.kill("SIGTERM");
@@ -40821,6 +41062,39 @@ function collabDomain(opts) {
40821
41062
  if (draft) return { status: 200, body: buildWorkorderDraftDetail(draft, resolve4) };
40822
41063
  return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${req.params.id}` } } };
40823
41064
  });
41065
+ router.post("/api/workorders/:id/hold", async (req) => {
41066
+ const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
41067
+ const ws = req.params.id;
41068
+ const arts = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws);
41069
+ if (arts.length === 0) {
41070
+ return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
41071
+ }
41072
+ if (!artifacts) {
41073
+ return { status: 503, body: { error: { code: "no_store", message: "\u63A7\u5236\u9762\u65C1\u5B58\u672A\u63A5\u5165\uFF0C\u65E0\u6CD5\u6682\u505C" } } };
41074
+ }
41075
+ const held = (await artifacts.listWorkspaceDispatchHeld()).includes(ws);
41076
+ if (held) return { status: 200, body: { held: false } };
41077
+ await artifacts.setWorkspaceDispatchHold(ws, true);
41078
+ return { status: 200, body: { held: true } };
41079
+ });
41080
+ router.post("/api/workorders/:id/cancel", async (req) => {
41081
+ if (!isHumanActor(req.auth.actor)) {
41082
+ 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" } } };
41083
+ }
41084
+ const { kernel } = await resolveCtx(req.auth.companyId);
41085
+ const ws = req.params.id;
41086
+ const active = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws && lifecycleOf(kernel.model, a.id) === "active");
41087
+ const all = [...kernel.model.artifacts.values()].filter((a) => a.workspace === ws);
41088
+ if (all.length === 0) {
41089
+ return { status: 404, body: { error: { code: "not_found", message: `\u5DE5\u5355\u4E0D\u5B58\u5728: ${ws}` } } };
41090
+ }
41091
+ let cancelled = 0;
41092
+ for (const a of active) {
41093
+ await kernel.seal({ artifactId: a.id, actor: req.auth.actor, reason: "cancelled", note: "\u5DE5\u5355\u53D6\u6D88\uFF08\u6279\u91CF\uFF09" });
41094
+ cancelled += 1;
41095
+ }
41096
+ return { status: 200, body: { cancelled } };
41097
+ });
40824
41098
  router.post("/api/workorders/:id/dispatch", async (req) => {
40825
41099
  const { kernel, artifacts } = await resolveCtx(req.auth.companyId);
40826
41100
  const ws = req.params.id;
@@ -40891,6 +41165,7 @@ function collabDomain(opts) {
40891
41165
  var init_collab = __esm({
40892
41166
  "../server/src/domains/collab/index.ts"() {
40893
41167
  "use strict";
41168
+ init_src2();
40894
41169
  init_workorders();
40895
41170
  init_workorder_detail();
40896
41171
  init_inbox();
@@ -57730,7 +58005,7 @@ ${res.warning}`);
57730
58005
  }
57731
58006
 
57732
58007
  // src/index.ts
57733
- var PKG_VERSION = true ? "0.1.36" : "dev";
58008
+ var PKG_VERSION = true ? "0.1.37" : "dev";
57734
58009
  var OASIS_DIR = path18.join(os10.homedir(), ".oasis");
57735
58010
  var CONFIG_FILE = path18.join(OASIS_DIR, "node-config.json");
57736
58011
  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.37",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"