baro-ai 0.86.0 → 0.87.0

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.
package/dist/cli.mjs CHANGED
@@ -74788,6 +74788,86 @@ function extractPath2(item) {
74788
74788
  return null;
74789
74789
  }
74790
74790
 
74791
+ // ../baro-orchestrator/src/harness/cooperative-suspension.ts
74792
+ function laneQuiescenceWitness(backend) {
74793
+ return backend === "openai" ? "self" : "process";
74794
+ }
74795
+ var CooperativeSuspension = class {
74796
+ constructor(storyId, quiesce, timeoutMs) {
74797
+ this.storyId = storyId;
74798
+ this.quiesce = quiesce;
74799
+ this.timeoutMs = timeoutMs;
74800
+ }
74801
+ blockId = null;
74802
+ /** The accepted block this story is suspending for, if any. */
74803
+ get acceptedBlockId() {
74804
+ return this.blockId;
74805
+ }
74806
+ /** True once no further work may begin, which is what makes a witness one. */
74807
+ get blocksNewWork() {
74808
+ return this.blockId !== null;
74809
+ }
74810
+ /** How a settled suspension reads in the story's outcome. */
74811
+ get outcomeText() {
74812
+ return `suspended on dependency block ${this.blockId}`;
74813
+ }
74814
+ /**
74815
+ * Record the request and wait for the lane's witness. Throws rather than
74816
+ * returning a summary the host would read as a certificate: a refusal
74817
+ * costs a retained worktree, a false certificate costs a silent write into
74818
+ * a branch already snapshotted.
74819
+ */
74820
+ async request(blockId, summary) {
74821
+ this.record(blockId);
74822
+ if (!await this.quiesce(this.timeoutMs)) {
74823
+ throw new Error(
74824
+ `story ${this.storyId} quiescence could not be certified for dependency block ${blockId}`
74825
+ );
74826
+ }
74827
+ return summary();
74828
+ }
74829
+ record(blockId) {
74830
+ if (typeof blockId !== "string" || blockId.trim() !== blockId || !blockId) {
74831
+ throw new TypeError(
74832
+ "suspension blockId must be a non-empty trimmed string"
74833
+ );
74834
+ }
74835
+ if (this.blockId !== null && this.blockId !== blockId) {
74836
+ throw new Error(
74837
+ `story ${this.storyId} is already suspending for block ${this.blockId}`
74838
+ );
74839
+ }
74840
+ this.blockId ??= blockId;
74841
+ }
74842
+ };
74843
+ async function settledWithinBound(inFlight, timeoutMs) {
74844
+ const deadline = Date.now() + Math.max(0, timeoutMs);
74845
+ const awaited = /* @__PURE__ */ new Set();
74846
+ for (; ; ) {
74847
+ const pending = inFlight().filter((entry) => !awaited.has(entry));
74848
+ if (pending.length === 0) return true;
74849
+ const remaining = deadline - Date.now();
74850
+ if (remaining <= 0) return false;
74851
+ if (!await raceBound(Promise.allSettled(pending), remaining)) {
74852
+ return false;
74853
+ }
74854
+ for (const entry of pending) awaited.add(entry);
74855
+ }
74856
+ }
74857
+ async function raceBound(work, timeoutMs) {
74858
+ let timer;
74859
+ try {
74860
+ return await Promise.race([
74861
+ work.then(() => true),
74862
+ new Promise((resolve9) => {
74863
+ timer = setTimeout(() => resolve9(false), timeoutMs);
74864
+ })
74865
+ ]);
74866
+ } finally {
74867
+ if (timer) clearTimeout(timer);
74868
+ }
74869
+ }
74870
+
74791
74871
  // ../baro-orchestrator/src/harness/codex/stream-mapper.ts
74792
74872
  function mapCodexEvent(agentId, event) {
74793
74873
  const threadId = typeof event.thread_id === "string" ? event.thread_id : null;
@@ -75092,6 +75172,18 @@ var TurnMessageMailbox = class {
75092
75172
  this.queued.push(item);
75093
75173
  if (this.active) this.active(this.queued.shift());
75094
75174
  }
75175
+ /**
75176
+ * Everything waiting, without blocking for more. A backend that owns its
75177
+ * own turn loop reads peer messages between inference rounds rather than
75178
+ * only at the turn boundary, which is where `waitForNext` leaves them.
75179
+ */
75180
+ drainAll() {
75181
+ return this.queued.splice(0, this.queued.length);
75182
+ }
75183
+ /** End an active wait now — a suspension must not sit out the quiet timeout. */
75184
+ cancel() {
75185
+ this.active?.(null);
75186
+ }
75095
75187
  waitForNext(options) {
75096
75188
  if (this.queued.length > 0) return Promise.resolve(this.queued.shift());
75097
75189
  if (this.active) throw new Error("already waiting for the next turn message");
@@ -77678,6 +77770,20 @@ var OpenAIStoryAgent = class extends BaseObserver {
77678
77770
  done;
77679
77771
  turnMessages = new TurnMessageMailbox();
77680
77772
  turnReviews = new TurnReviewMailbox();
77773
+ /**
77774
+ * Tool invocations that have started and not yet settled.
77775
+ *
77776
+ * A CLI backend gets its quiescence proof from the operating system: the
77777
+ * process is gone, so nothing of its can still be writing. In process
77778
+ * there is no such witness, and aborting does not make one — the abort
77779
+ * wins its race while the write it interrupted carries on. Owning the
77780
+ * loop supplies a better witness than the process table ever did: no
77781
+ * invocation can start once a suspension is requested, so an empty set is
77782
+ * proof rather than inference.
77783
+ */
77784
+ inFlightTools = /* @__PURE__ */ new Set();
77785
+ suspension;
77786
+ attemptsMade = 0;
77681
77787
  constructor(spec, opts = {}) {
77682
77788
  super();
77683
77789
  this.spec = {
@@ -77708,6 +77814,10 @@ var OpenAIStoryAgent = class extends BaseObserver {
77708
77814
  0,
77709
77815
  Math.floor(opts.transportRetryDelayMs ?? 1e3)
77710
77816
  ),
77817
+ suspensionQuiescenceTimeoutMs: Math.max(
77818
+ 0,
77819
+ Math.floor(opts.suspensionQuiescenceTimeoutMs ?? 3e4)
77820
+ ),
77711
77821
  baseUrl: opts.baseUrl ?? "",
77712
77822
  apiKey: opts.apiKey ?? "",
77713
77823
  runtimeReplanDecisionAuthority: opts.runtimeReplanDecisionAuthority ?? null,
@@ -77731,6 +77841,11 @@ var OpenAIStoryAgent = class extends BaseObserver {
77731
77841
  ...this.runtimeReplanEnabled() ? [createRuntimeReplanTool(this.runtimeGraphVersion)] : []
77732
77842
  ];
77733
77843
  setModelTools(this.model, this.tools);
77844
+ this.suspension = new CooperativeSuspension(
77845
+ this.spec.id,
77846
+ (timeoutMs) => settledWithinBound(() => [...this.inFlightTools], timeoutMs),
77847
+ this.opts.suspensionQuiescenceTimeoutMs
77848
+ );
77734
77849
  this.done = new Promise((res) => {
77735
77850
  this.resolveDone = res;
77736
77851
  });
@@ -77800,6 +77915,7 @@ var OpenAIStoryAgent = class extends BaseObserver {
77800
77915
  }, this.spec.hardTimeoutSecs * 1e3) : null;
77801
77916
  for (let i2 = 0; i2 < maxAttempts; i2++) {
77802
77917
  if (this.abortController.signal.aborted) break;
77918
+ if (this.suspension.blocksNewWork) break;
77803
77919
  if (i2 > 0) {
77804
77920
  this.transition("waiting", `retrying (attempt ${i2 + 1}/${maxAttempts})`);
77805
77921
  try {
@@ -77810,6 +77926,7 @@ var OpenAIStoryAgent = class extends BaseObserver {
77810
77926
  if (this.abortController.signal.aborted) break;
77811
77927
  }
77812
77928
  attempts += 1;
77929
+ this.attemptsMade = attempts;
77813
77930
  try {
77814
77931
  const result = await this.runOneAttempt(attempts);
77815
77932
  if (this.currentPhase === "done") {
@@ -77833,6 +77950,11 @@ var OpenAIStoryAgent = class extends BaseObserver {
77833
77950
  lastError = this.abortReason ?? "story was aborted";
77834
77951
  lastFailure ??= this.abortFailure;
77835
77952
  }
77953
+ if (this.suspension.blocksNewWork) {
77954
+ lastError = this.suspension.outcomeText;
77955
+ lastFailure = void 0;
77956
+ this.transition("aborted", lastError);
77957
+ }
77836
77958
  const durationSecs = this.startedAt ? Math.round((Date.now() - this.startedAt) / 1e3) : 0;
77837
77959
  const success = this.currentPhase === "done";
77838
77960
  this.envRef?.deliverSemanticEvent(
@@ -78172,6 +78294,16 @@ var OpenAIStoryAgent = class extends BaseObserver {
78172
78294
  }
78173
78295
  context = context.addContextItem(outItem);
78174
78296
  }
78297
+ if (this.suspension.blocksNewWork) {
78298
+ return {
78299
+ context,
78300
+ success: false,
78301
+ assistantText,
78302
+ usage,
78303
+ error: this.suspension.outcomeText,
78304
+ retryable: false
78305
+ };
78306
+ }
78175
78307
  if (controlPlaneOutcomeUnknown || this.abortController.signal.aborted) {
78176
78308
  return {
78177
78309
  context,
@@ -78286,7 +78418,36 @@ var OpenAIStoryAgent = class extends BaseObserver {
78286
78418
  }
78287
78419
  async runOrdinaryTool(call) {
78288
78420
  const tool = this.tools.find((candidate) => candidate.name === call.name);
78289
- return tool ? runToolSafely(tool, call.args, this.abortController.signal) : `Error: tool '${call.name}' not registered`;
78421
+ if (!tool) return `Error: tool '${call.name}' not registered`;
78422
+ if (this.suspension.blocksNewWork) return this.suspendedToolOutput();
78423
+ const invocation = runToolSafely(
78424
+ tool,
78425
+ call.args,
78426
+ this.abortController.signal
78427
+ );
78428
+ this.inFlightTools.add(invocation);
78429
+ const settled = () => {
78430
+ this.inFlightTools.delete(invocation);
78431
+ };
78432
+ invocation.then(settled, settled);
78433
+ return invocation;
78434
+ }
78435
+ suspendedToolOutput() {
78436
+ return "Error: this story is suspending on an accepted dependency block; no further tool calls will run in this session.";
78437
+ }
78438
+ /**
78439
+ * Stop taking work and resolve only once nothing of this story's can still
78440
+ * write, so the host may snapshot the worktree. Inference is left to end
78441
+ * on its own bound — a model call touches no files, and killing it would
78442
+ * buy nothing the witness does not already give.
78443
+ */
78444
+ async suspend(blockId) {
78445
+ const pending = this.suspension.request(blockId, () => ({
78446
+ attempts: this.attemptsMade,
78447
+ durationSecs: this.startedAt ? Math.round((Date.now() - this.startedAt) / 1e3) : 0
78448
+ }));
78449
+ if (this.suspension.blocksNewWork) this.turnMessages.cancel();
78450
+ return pending;
78290
78451
  }
78291
78452
  async proposeRuntimeReplan(call) {
78292
78453
  const parsed = parseRuntimeReplanArgs(call.args);
@@ -79652,7 +79813,7 @@ function failureSummary2(failure) {
79652
79813
  // ../baro-orchestrator/src/execution/story-executor.ts
79653
79814
  var LocalStoryExecutor = class {
79654
79815
  supportsCooperativeSuspend(route) {
79655
- return route.backend !== "openai" && PROCESS_TREE_CAPABILITIES.cooperativeQuiescenceObservation;
79816
+ return laneQuiescenceWitness(route.backend) === "self" ? true : PROCESS_TREE_CAPABILITIES.cooperativeQuiescenceObservation;
79656
79817
  }
79657
79818
  start(req, route, cwd, env, opts) {
79658
79819
  if (opts.requireProcessQuiescenceCertification === true && route.backend !== "openai" && !PROCESS_TREE_CAPABILITIES.cooperativeQuiescenceObservation) {
@@ -82954,7 +83115,7 @@ var ContinuousGateRunner = class extends BaseObserver {
82954
83115
  if (result.commands.some((command) => wasKilled(command))) {
82955
83116
  throw new Error("a gate command was killed before it reported");
82956
83117
  }
82957
- return result.commands.map((command) => ({
83118
+ return result.commands.filter((command) => command.status !== "skipped").map((command) => ({
82958
83119
  label: command.command,
82959
83120
  passed: command.status === "passed",
82960
83121
  detail: command.status === "passed" ? "" : command.tail ?? ""