@cmmd-center/forge 0.13.88 → 0.13.89

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/bin.cjs CHANGED
@@ -66296,7 +66296,7 @@ function normalizeNumberish(value) {
66296
66296
  }
66297
66297
  //#endregion
66298
66298
  //#region package.json
66299
- var version$1 = "0.13.88";
66299
+ var version$1 = "0.13.89";
66300
66300
  //#endregion
66301
66301
  //#region src/sentry.ts
66302
66302
  const SERVER_APP_NAME = "forge-server";
@@ -241826,6 +241826,22 @@ function resolveSpriteTaskHoldConfig(env = process.env) {
241826
241826
  function spriteTaskHoldName(namePrefix, turnId) {
241827
241827
  return `${namePrefix}${turnId}`;
241828
241828
  }
241829
+ /**
241830
+ * Thrown by the production transport when the guest API answers a non-2xx
241831
+ * status. Carries the status so a caller can tell a 409 (already held, and
241832
+ * generally benign) apart from a real transport failure.
241833
+ */
241834
+ var SpriteTaskHoldTransportError = class extends Error {
241835
+ status;
241836
+ constructor(status) {
241837
+ super(`sprite task API responded ${status}`);
241838
+ this.name = "SpriteTaskHoldTransportError";
241839
+ this.status = status;
241840
+ }
241841
+ };
241842
+ function transportStatus(cause) {
241843
+ return cause instanceof SpriteTaskHoldTransportError ? cause.status : void 0;
241844
+ }
241829
241845
  const noTimer = () => {};
241830
241846
  /**
241831
241847
  * Resolve with `promise`, but stop waiting after `timeoutMs`. Rejections pass
@@ -241859,12 +241875,25 @@ var SpriteTaskHoldManager = class {
241859
241875
  #transport;
241860
241876
  #timer;
241861
241877
  #onError;
241878
+ #onEvent;
241862
241879
  #holds = /* @__PURE__ */ new Map();
241863
241880
  constructor(options) {
241864
241881
  this.#config = options.config;
241865
241882
  this.#transport = options.transport;
241866
241883
  this.#timer = options.timer ?? DEFAULT_TIMER;
241867
241884
  this.#onError = options.onError ?? (() => {});
241885
+ this.#onEvent = options.onEvent ?? (() => {});
241886
+ }
241887
+ /** Compose and report one acquire/refresh/release outcome, if a caller wants to log it. */
241888
+ #emitEvent(action, holdKey, hold, outcome, status) {
241889
+ this.#onEvent({
241890
+ action,
241891
+ holdKey,
241892
+ holdName: hold.name,
241893
+ threadId: hold.threadId,
241894
+ outcome,
241895
+ ...status !== void 0 ? { status } : {}
241896
+ });
241868
241897
  }
241869
241898
  /** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
241870
241899
  activeTurnIds() {
@@ -241873,10 +241902,19 @@ var SpriteTaskHoldManager = class {
241873
241902
  /**
241874
241903
  * `maxRefreshes` bounds a hold that has no terminal event of its own, such as a launch
241875
241904
  * whose turn may never start. When the budget is spent the hold releases itself.
241905
+ *
241906
+ * Resolves to whether this hold has ever landed a PUT, never throwing (a
241907
+ * hold problem must not fail a turn). A caller handing off from a
241908
+ * predecessor hold (forge#3589) must not release it on anything less than
241909
+ * "succeeded": a failed initial PUT still installs the refresher below and
241910
+ * keeps retrying every `SPRITE_TASK_HOLD_REFRESH_MS`, so the predecessor is
241911
+ * the only thing covering the sprite until one of those retries lands. A
241912
+ * duplicate call against a live, unreleased hold reports that hold's own
241913
+ * confirmed state rather than a blanket "succeeded", for the same reason.
241876
241914
  */
241877
241915
  async acquire(turnId, threadId = null, options = {}) {
241878
- const existing = this.#holds.get(turnId);
241879
- if (existing !== void 0 && !existing.released) return;
241916
+ const existing = this.#liveHold(turnId);
241917
+ if (existing !== void 0) return existing.confirmed ? "succeeded" : "failed";
241880
241918
  let refreshesLeft = options.maxRefreshes ?? Number.POSITIVE_INFINITY;
241881
241919
  const name = spriteTaskHoldName(this.#config.namePrefix, turnId);
241882
241920
  const hold = {
@@ -241888,24 +241926,54 @@ var SpriteTaskHoldManager = class {
241888
241926
  phase: "idle",
241889
241927
  releaseAttempt: null,
241890
241928
  deletion: null,
241891
- uncertainPut: false
241929
+ uncertainPut: false,
241930
+ confirmed: false
241892
241931
  };
241893
241932
  this.#holds.set(turnId, hold);
241894
241933
  const put = this.#transport.put(name, 300);
241895
241934
  const tracked = put.then(() => "succeeded", () => "failed");
241896
241935
  hold.inFlight.add(tracked);
241936
+ let outcome = "succeeded";
241897
241937
  try {
241898
241938
  await put;
241899
241939
  hold.uncertainPut = false;
241940
+ hold.confirmed = true;
241941
+ this.#emitEvent("acquire", turnId, hold, "success");
241900
241942
  } catch (cause) {
241943
+ outcome = "failed";
241901
241944
  hold.uncertainPut = true;
241902
241945
  hold.inFlight.delete(tracked);
241903
241946
  this.#onError(`failed to acquire sprite task hold for turn ${turnId}`, cause);
241904
- if (hold.released || this.#holds.get(turnId) !== hold) return;
241947
+ this.#emitEvent("acquire", turnId, hold, "failure", transportStatus(cause));
241948
+ if (this.#isStale(turnId, hold)) return outcome;
241905
241949
  }
241906
241950
  hold.inFlight.delete(tracked);
241907
- if (hold.released || this.#holds.get(turnId) !== hold) return;
241908
- hold.refresher = this.#timer.setInterval(() => {
241951
+ if (this.#isStale(turnId, hold)) return outcome;
241952
+ hold.refresher = this.#installRefresher(turnId, hold, name, refreshesLeft);
241953
+ return outcome;
241954
+ }
241955
+ /** The hold registered for `turnId`, if it is still live: not mid-release or concluded. */
241956
+ #liveHold(turnId) {
241957
+ const existing = this.#holds.get(turnId);
241958
+ return existing !== void 0 && !existing.released ? existing : void 0;
241959
+ }
241960
+ /**
241961
+ * Whether `hold` has been superseded since the caller last checked: either
241962
+ * released, or no longer the map's current entry for `turnId` (a fresh
241963
+ * acquire replaced it). Both `acquire`'s post-await fences use this so
241964
+ * neither has to repeat the compound condition inline.
241965
+ */
241966
+ #isStale(turnId, hold) {
241967
+ return hold.released || this.#holds.get(turnId) !== hold;
241968
+ }
241969
+ /**
241970
+ * Install a hold's refresh interval. Extracted out of `acquire` so that
241971
+ * method's own branching stays flat; the ordering and identity-fence rules
241972
+ * around it are unchanged, and this closure owns only the refresh tick.
241973
+ */
241974
+ #installRefresher(turnId, hold, name, initialBudget) {
241975
+ let refreshesLeft = initialBudget;
241976
+ return this.#timer.setInterval(() => {
241909
241977
  if (hold.inFlight.size > 0) return;
241910
241978
  if (refreshesLeft <= 0) {
241911
241979
  if (this.#holds.get(turnId) === hold) this.release(turnId);
@@ -241914,10 +241982,13 @@ var SpriteTaskHoldManager = class {
241914
241982
  refreshesLeft -= 1;
241915
241983
  const refresh = this.#transport.put(name, 300).then(() => {
241916
241984
  hold.uncertainPut = false;
241985
+ hold.confirmed = true;
241986
+ this.#emitEvent("refresh", turnId, hold, "success");
241917
241987
  return "succeeded";
241918
241988
  }, (cause) => {
241919
241989
  hold.uncertainPut = true;
241920
241990
  this.#onError(`failed to refresh sprite task hold for turn ${turnId}`, cause);
241991
+ this.#emitEvent("refresh", turnId, hold, "failure", transportStatus(cause));
241921
241992
  return "failed";
241922
241993
  });
241923
241994
  hold.inFlight.add(refresh);
@@ -241961,6 +242032,7 @@ var SpriteTaskHoldManager = class {
241961
242032
  hold.phase = "joining";
241962
242033
  const attempt = (async () => {
241963
242034
  const outcome = await this.#concludeRelease(hold, turnId, options);
242035
+ this.#emitEvent("release", turnId, hold, outcome);
241964
242036
  if (outcome === "unreleased") {
241965
242037
  hold.phase = "idle";
241966
242038
  this.#removeIfCurrent(turnId, hold);
@@ -242144,7 +242216,7 @@ function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
242144
242216
  signal: controller.signal,
242145
242217
  unix: config.socketPath
242146
242218
  });
242147
- if (!response.ok) throw new Error(`sprite task API responded ${response.status}`);
242219
+ if (!response.ok) throw new SpriteTaskHoldTransportError(response.status);
242148
242220
  } finally {
242149
242221
  clearTimeout(deadline);
242150
242222
  }
@@ -242180,13 +242252,14 @@ function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
242180
242252
  * Build a manager for this process, or null when the runtime is not a sprite.
242181
242253
  * Callers treat null as "this feature does not apply here".
242182
242254
  */
242183
- function makeSpriteTaskHoldManager(env = process.env, onError) {
242255
+ function makeSpriteTaskHoldManager(env = process.env, onError, onEvent) {
242184
242256
  const config = resolveSpriteTaskHoldConfig(env);
242185
242257
  if (config === null) return null;
242186
242258
  return new SpriteTaskHoldManager({
242187
242259
  config,
242188
242260
  transport: makeSpriteTaskHoldTransport(config),
242189
- ...onError ? { onError } : {}
242261
+ ...onError ? { onError } : {},
242262
+ ...onEvent ? { onEvent } : {}
242190
242263
  });
242191
242264
  }
242192
242265
  //#endregion
@@ -286770,7 +286843,7 @@ const AgentBrowserManagerLive = require_Schema$1.effect(BrowserManager, require_
286770
286843
  const displayPeers = createWebRtcDisplaySessionManager({
286771
286844
  mediaSourceFactory: displayCaptureRuntime.mediaSourceFactory,
286772
286845
  taskHold: displayTaskHoldManager ? {
286773
- acquire: (holdId) => displayTaskHoldManager.acquire(holdId),
286846
+ acquire: (holdId) => displayTaskHoldManager.acquire(holdId).then(() => {}),
286774
286847
  release: (holdId) => displayTaskHoldManager.release(holdId).then(() => {})
286775
286848
  } : void 0
286776
286849
  });
@@ -295145,6 +295218,15 @@ const make$6 = require_Schema$1.gen(function* () {
295145
295218
  const providerService = yield* ProviderService;
295146
295219
  const manager = makeSpriteTaskHoldManager(process.env, (message, cause) => {
295147
295220
  require_Schema$1.runFork(require_Schema$1.logWarning(message, { cause: cause instanceof Error ? cause.message : cause }));
295221
+ }, (event) => {
295222
+ require_Schema$1.runFork(require_Schema$1.logInfo("sprite task hold", {
295223
+ action: event.action,
295224
+ holdKey: event.holdKey,
295225
+ holdName: event.holdName,
295226
+ threadId: event.threadId,
295227
+ outcome: event.outcome,
295228
+ ...event.status !== void 0 ? { status: event.status } : {}
295229
+ }));
295148
295230
  });
295149
295231
  /**
295150
295232
  * Run a manager call, or nothing on runtimes that are not sprites. One
@@ -295160,27 +295242,58 @@ const make$6 = require_Schema$1.gen(function* () {
295160
295242
  deleteTimeoutMs: SPRITE_TASK_HOLD_RELEASE_DELETE_TIMEOUT_MS
295161
295243
  })));
295162
295244
  const launchCallers = /* @__PURE__ */ new Map();
295245
+ const launchGeneration = /* @__PURE__ */ new Map();
295246
+ const turnLaunchGeneration = /* @__PURE__ */ new Map();
295247
+ /**
295248
+ * Belt-and-braces cleanup for a turn reaching a terminal state
295249
+ * (turn.completed or turn.aborted): a launch hold whose hand-off never
295250
+ * confirmed (the turn hold's PUT kept failing) is still up, refreshing on
295251
+ * its own budget, and the turn ending either way means the launch phase is
295252
+ * over. `manager.release(String(turnId))` above can itself wait on the
295253
+ * guest API, so by the time this runs the NEXT stage's own dispatch may
295254
+ * already have acquired a fresh launch hold for the same thread; only
295255
+ * release it here when no such newer launch has started since this turn's
295256
+ * own turn.started was processed (CodeRabbit review, forge#3592). A no-op
295257
+ * when this turn's own generation is stale, or when the hold was already
295258
+ * released.
295259
+ */
295260
+ const releaseLingeringLaunchHold = async (turnId, threadId) => {
295261
+ const capturedGeneration = turnLaunchGeneration.get(turnId);
295262
+ turnLaunchGeneration.delete(turnId);
295263
+ if (manager === null || capturedGeneration === void 0) return;
295264
+ if (launchGeneration.get(threadId) !== capturedGeneration) return;
295265
+ await manager.release(launchHoldId(threadId));
295266
+ };
295163
295267
  const start = require_Schema$1.fn("start")(function* () {
295164
295268
  if (manager === null) return;
295165
295269
  yield* require_Schema$1.forkScoped(require_Path.runForEach(providerService.streamEvents, (event) => {
295166
295270
  if (event.type === "turn.started") {
295167
295271
  const turnId = event.turnId;
295168
295272
  if (turnId === void 0) return require_Schema$1.void_;
295273
+ const threadId = String(event.threadId);
295274
+ const generation = launchGeneration.get(threadId) ?? 0;
295169
295275
  return require_Schema$1.promise(async () => {
295170
- await manager.acquire(String(turnId), String(event.threadId));
295171
- launchCallers.delete(String(event.threadId));
295172
- await manager.release(launchHoldId(String(event.threadId)));
295276
+ const outcome = await manager.acquire(String(turnId), threadId);
295277
+ launchCallers.delete(threadId);
295278
+ turnLaunchGeneration.set(String(turnId), generation);
295279
+ if (outcome === "succeeded") await manager.release(launchHoldId(threadId));
295173
295280
  });
295174
295281
  }
295175
295282
  if (event.type === "turn.completed") {
295176
295283
  const turnId = event.turnId;
295177
295284
  if (turnId === void 0) return require_Schema$1.void_;
295178
- return require_Schema$1.promise(() => manager.release(String(turnId)).then(() => {}));
295285
+ return require_Schema$1.promise(async () => {
295286
+ await manager.release(String(turnId));
295287
+ await releaseLingeringLaunchHold(String(turnId), String(event.threadId));
295288
+ });
295179
295289
  }
295180
295290
  if (event.type === "turn.aborted") {
295181
295291
  const turnId = event.turnId;
295182
295292
  if (turnId === void 0) return require_Schema$1.void_;
295183
- return require_Schema$1.promise(() => manager.release(String(turnId)).then(() => {}));
295293
+ return require_Schema$1.promise(async () => {
295294
+ await manager.release(String(turnId));
295295
+ await releaseLingeringLaunchHold(String(turnId), String(event.threadId));
295296
+ });
295184
295297
  }
295185
295298
  if (event.type === "session.exited") {
295186
295299
  launchCallers.delete(String(event.threadId));
@@ -295192,7 +295305,9 @@ const make$6 = require_Schema$1.gen(function* () {
295192
295305
  /** Takes the launch hold for one more caller. The budget bounds a launch that never starts a turn. */
295193
295306
  const holdLaunch = (threadId) => require_Schema$1.promise(async () => {
295194
295307
  if (manager === null) return;
295195
- launchCallers.set(threadId, (launchCallers.get(threadId) ?? 0) + 1);
295308
+ const callers = launchCallers.get(threadId) ?? 0;
295309
+ launchCallers.set(threadId, callers + 1);
295310
+ if (callers === 0) launchGeneration.set(threadId, (launchGeneration.get(threadId) ?? 0) + 1);
295196
295311
  await manager.acquire(launchHoldId(threadId), threadId, { maxRefreshes: LAUNCH_HOLD_MAX_REFRESHES });
295197
295312
  });
295198
295313
  /** Lets one caller go. The hold itself is released when the last caller has let go. */
@@ -299363,7 +299478,7 @@ function resolveBuildCommitFromEnv(env) {
299363
299478
  * environment descriptor down instead of reporting an honest "unknown".
299364
299479
  */
299365
299480
  function readBakedBuildCommit() {
299366
- return "60b0a6baf90436ed3291c93ac0273567a58cd02e";
299481
+ return "394b2a7cd449282f2845ec5fa3bd9093f20b8bb8";
299367
299482
  }
299368
299483
  async function resolveServerBuildCommit(input) {
299369
299484
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -316821,6 +316936,7 @@ const makePipelineRunCoordinator = require_Schema$1.gen(function* () {
316821
316936
  const policy = yield* PipelineStagePolicy;
316822
316937
  const secretStore = yield* ServerSecretStore;
316823
316938
  const orchestration = yield* OrchestrationEngineService;
316939
+ const spriteTaskHoldReactor = yield* require_Schema$1.serviceOption(SpriteTaskHoldReactor);
316824
316940
  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
316825
316941
  const projectionTurns = yield* ProjectionTurnRepository;
316826
316942
  const changeInspector = yield* PipelineChangeInspector;
@@ -317548,6 +317664,23 @@ const makePipelineRunCoordinator = require_Schema$1.gen(function* () {
317548
317664
  });
317549
317665
  yield* drainReports();
317550
317666
  });
317667
+ /**
317668
+ * Holds the sprite awake around a freshly dispatched thread.turn.start, the
317669
+ * same way bootstrapLaunch.ts's withLaunchHold does for a turn started from
317670
+ * the Forge web composer or a bootstrap launch (forge#3589). A turn kicked
317671
+ * from a CMMD card reaches the engine through dispatchStage /
317672
+ * recoverMissingProviderStart below, never through that HTTP dispatch
317673
+ * route, so nothing else covers the window between this dispatch and the
317674
+ * provider's own turn.started.
317675
+ *
317676
+ * Releases only on error: a successful dispatch hands the hold off to the
317677
+ * turn's own, in SpriteTaskHoldReactor's turn.started handler.
317678
+ */
317679
+ const withPipelineLaunchHold = (threadId, dispatch) => {
317680
+ if (require_Schema$1.isNone(spriteTaskHoldReactor)) return dispatch;
317681
+ const reactor = spriteTaskHoldReactor.value;
317682
+ return require_Schema$1.uninterruptible(reactor.holdLaunch(threadId)).pipe(require_Schema$1.andThen(require_Schema$1.yieldNow), require_Schema$1.andThen(dispatch), require_Schema$1.onError(() => reactor.releaseLaunch(threadId)));
317683
+ };
317551
317684
  const dispatchStage = (binding, snapshot, authority) => require_Schema$1.gen(function* () {
317552
317685
  const thread = (yield* orchestration.getReadModel()).threads.find((candidate) => candidate.id === ThreadId.make(binding.threadId) && candidate.deletedAt === null);
317553
317686
  if (thread === void 0) {
@@ -317623,7 +317756,7 @@ const makePipelineRunCoordinator = require_Schema$1.gen(function* () {
317623
317756
  messageId: `${commandId}:message`,
317624
317757
  createdAt: startedAt
317625
317758
  });
317626
- yield* orchestration.dispatch(command).pipe(require_Schema$1.tapError(() => store.resetDispatch(binding.runId, commandId)));
317759
+ yield* withPipelineLaunchHold(binding.threadId, orchestration.dispatch(command)).pipe(require_Schema$1.tapError(() => store.resetDispatch(binding.runId, commandId)));
317627
317760
  yield* store.markDispatched(binding.runId, commandId);
317628
317761
  });
317629
317762
  const resumeRun = (runId) => require_Schema$1.gen(function* () {
@@ -318263,7 +318396,7 @@ const makePipelineRunCoordinator = require_Schema$1.gen(function* () {
318263
318396
  });
318264
318397
  if (workerCoordination.kind !== "single" && workerCoordination.kind !== "ready") return;
318265
318398
  const commandId = `${originalCommandId}:provider-start-recovery`;
318266
- yield* orchestration.dispatch(buildStageTurnCommand({
318399
+ yield* withPipelineLaunchHold(input.binding.threadId, orchestration.dispatch(buildStageTurnCommand({
318267
318400
  binding: input.binding,
318268
318401
  attempt: input.binding.attempt,
318269
318402
  snapshot: input.snapshot,
@@ -318274,7 +318407,7 @@ const makePipelineRunCoordinator = require_Schema$1.gen(function* () {
318274
318407
  commandId,
318275
318408
  messageId: `${commandId}:message`,
318276
318409
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
318277
- }));
318410
+ })));
318278
318411
  });
318279
318412
  }
318280
318413
  function reconcileActiveThread(input) {