@threadbase-sh/streamer 1.70.1 → 1.70.3

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/index.js CHANGED
@@ -9831,6 +9831,58 @@ var PromptRegistry = class {
9831
9831
  this.publish(entry);
9832
9832
  return copyPrompt(entry.prompt);
9833
9833
  }
9834
+ /**
9835
+ * The provider's prompt left the screen.
9836
+ *
9837
+ * Distinct from `transition` because the detector cannot tell a teardown it
9838
+ * observed on its own from one OUR OWN write caused. On a screen-scraped gate
9839
+ * the answer keys remove the box, and pty-manager's sendKeys fires the close
9840
+ * synchronously from inside the write — so the close always arrives before
9841
+ * the answer that caused it has settled, and reporting it as a cancel failed
9842
+ * every answer whose bytes had already landed (#720).
9843
+ *
9844
+ * While an answer is writing, the close is deferred and the answer decides:
9845
+ * `resolved` if the write landed, the deferred close if it did not. Success
9846
+ * is the adapter's own result, never the prompt's absence from the screen —
9847
+ * a prompt that does not vanish when answered (a multi-select form) simply
9848
+ * never reaches here, and is settled by the same adapter result (#721).
9849
+ *
9850
+ * ONLY this path defers. `replaced` from a different gate taking the screen,
9851
+ * `unavailable` from invalidateSession and `expired` all still go through
9852
+ * `transition` and still win, because none of them was caused by our write.
9853
+ */
9854
+ providerClosed(promptId, reason) {
9855
+ const entry = this.byId.get(promptId);
9856
+ if (!entry) return null;
9857
+ if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") return null;
9858
+ if (entry.answering) {
9859
+ entry.deferredClose = reason;
9860
+ return null;
9861
+ }
9862
+ return this.transition(promptId, "cancelled", reason);
9863
+ }
9864
+ /**
9865
+ * Run a provider write that may close the prompt as a side effect, so the
9866
+ * close it causes is deferred rather than applied. For callers outside this
9867
+ * class that write without going through `answer()` — the legacy permission
9868
+ * route. `performAnswer` manages the same two fields directly, because it
9869
+ * needs the deferred value in its own control flow.
9870
+ *
9871
+ * The write's own failure needs no unwinding here: pty-manager fires the
9872
+ * close as the last statement of a successful sendKeys, so a throw means no
9873
+ * close was ever deferred and the record is still open for the caller.
9874
+ */
9875
+ whileAnswering(promptId, write) {
9876
+ const entry = this.byId.get(promptId);
9877
+ if (!entry) return write();
9878
+ entry.answering = true;
9879
+ try {
9880
+ return write();
9881
+ } finally {
9882
+ entry.answering = false;
9883
+ entry.deferredClose = void 0;
9884
+ }
9885
+ }
9834
9886
  invalidateSession(sessionId, reason = "session_ended") {
9835
9887
  const transitioned = [];
9836
9888
  for (const entry of this.bySession.get(sessionId)?.values() ?? []) {
@@ -9908,21 +9960,33 @@ var PromptRegistry = class {
9908
9960
  const responseError = this.validateResponses(prompt, answer);
9909
9961
  if (responseError) return { ok: false, code: responseError };
9910
9962
  if (!entry.adapter) return { ok: false, code: "prompt_unavailable" };
9963
+ entry.answering = true;
9911
9964
  let adapterResult;
9965
+ let threw = false;
9912
9966
  try {
9913
9967
  adapterResult = await entry.adapter({ prompt: copyPrompt(prompt), answer });
9914
9968
  } catch {
9915
- return { ok: false, code: "provider_error" };
9969
+ threw = true;
9970
+ adapterResult = { ok: false, code: "provider_error" };
9971
+ } finally {
9972
+ entry.answering = false;
9916
9973
  }
9974
+ const deferred = entry.deferredClose;
9975
+ entry.deferredClose = void 0;
9976
+ const settle = (outcome) => {
9977
+ if (deferred) this.providerClosed(prompt.promptId, deferred);
9978
+ return outcome;
9979
+ };
9980
+ if (threw) return settle({ ok: false, code: "provider_error" });
9917
9981
  if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
9918
- return { ok: false, code: terminalError(entry.prompt.state) };
9982
+ return settle({ ok: false, code: terminalError(entry.prompt.state) });
9919
9983
  }
9920
9984
  if (entry.prompt.revision !== answer.revision) {
9921
- return {
9985
+ return settle({
9922
9986
  ok: false,
9923
9987
  code: "prompt_revision_mismatch",
9924
9988
  currentRevision: entry.prompt.revision
9925
- };
9989
+ });
9926
9990
  }
9927
9991
  if (!adapterResult.ok) {
9928
9992
  if (adapterResult.terminal) {
@@ -9932,7 +9996,7 @@ var PromptRegistry = class {
9932
9996
  adapterResult.terminal.reason
9933
9997
  );
9934
9998
  }
9935
- return { ok: false, code: adapterResult.code };
9999
+ return settle({ ok: false, code: adapterResult.code });
9936
10000
  }
9937
10001
  return { ok: true, prompt: this.transition(prompt.promptId, "resolved", "answered") };
9938
10002
  }
@@ -11143,10 +11207,7 @@ var SessionHandlers = class {
11143
11207
  if (gate === null) {
11144
11208
  const prior2 = this.pendingPermission.get(sessionId);
11145
11209
  if (!prior2) return;
11146
- const prompt2 = prior2.promptId ? this.promptRegistry.get(prior2.promptId) : null;
11147
- if (prompt2?.state === "open" || prompt2?.state === "updated") {
11148
- this.promptRegistry.transition(prompt2.promptId, "cancelled", "provider_closed");
11149
- }
11210
+ if (prior2.promptId) this.promptRegistry.providerClosed(prior2.promptId, "provider_closed");
11150
11211
  this.pendingPermission.delete(sessionId);
11151
11212
  this.pendingPermissionKey.delete(sessionId);
11152
11213
  this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
@@ -11371,7 +11432,13 @@ var SessionHandlers = class {
11371
11432
  return;
11372
11433
  }
11373
11434
  try {
11374
- this.ptyManager.sendKeys(sessionId, option.answerKeys ?? permissionAnswerKeys(option.index));
11435
+ this.promptRegistry.whileAnswering(
11436
+ gate.promptId ?? "",
11437
+ () => this.ptyManager.sendKeys(
11438
+ sessionId,
11439
+ option.answerKeys ?? permissionAnswerKeys(option.index)
11440
+ )
11441
+ );
11375
11442
  } catch (err) {
11376
11443
  const message = err instanceof Error ? err.message : "Failed to send answer";
11377
11444
  json(res, 400, { ok: false, reason: message });
@@ -18448,23 +18515,16 @@ var StreamerServer = class {
18448
18515
  * it.
18449
18516
  */
18450
18517
  async resolveConversationTarget(sessionId) {
18451
- let jsonlPath = this.conversationHandlers.findJsonlPath(sessionId);
18452
- let conv = await this.conversationHandlers.findConversationByUuid(sessionId);
18453
- let historyId = sessionId;
18454
- let registryProvider;
18455
- if (!jsonlPath && !conv) {
18456
- const row = this.managedSessionsRepo?.get(sessionId) ?? null;
18457
- const boundId = row ? resumeIdForRow(row) : null;
18458
- if (boundId != null && boundId !== sessionId) {
18459
- historyId = boundId;
18460
- registryProvider = row?.provider;
18461
- jsonlPath = this.conversationHandlers.findJsonlPath(boundId);
18462
- conv = await this.conversationHandlers.findConversationByUuid(boundId);
18463
- }
18464
- }
18518
+ const row = this.managedSessionsRepo?.get(sessionId) ?? null;
18519
+ const liveSession = this.sessionStore.getManaged(sessionId);
18520
+ const resumeId = (row ? resumeIdForRow(row) : null) ?? (liveSession?.provider === CODEX_CLI_PROVIDER ? liveSession.boundConversationId ?? null : null);
18521
+ const historyId = resumeId ?? sessionId;
18522
+ const managedProvider = row?.provider ?? liveSession?.provider;
18523
+ const jsonlPath = this.conversationHandlers.findJsonlPath(historyId);
18524
+ const conv = await this.conversationHandlers.findConversationByUuid(historyId);
18465
18525
  const cachedConvMeta = this.cache?.getMetaById(historyId);
18466
18526
  const cachedPath = cachedConvMeta?.filePath ? toNativeFilePath(cachedConvMeta.filePath) : null;
18467
- const cachedCodexPath = (registryProvider === CODEX_CLI_PROVIDER || cachedConvMeta?.provider === CODEX_CLI_PROVIDER) && cachedPath != null && existsSync16(cachedPath) ? cachedPath : null;
18527
+ const cachedCodexPath = (managedProvider === CODEX_CLI_PROVIDER || cachedConvMeta?.provider === CODEX_CLI_PROVIDER) && cachedPath != null && existsSync16(cachedPath) ? cachedPath : null;
18468
18528
  const jsonlCwd = jsonlPath ? await this.conversationHandlers.readCwdFromJsonl(jsonlPath) : null;
18469
18529
  const projectPath = jsonlCwd ?? conv?.projectPath ?? (cachedCodexPath ? cachedConvMeta?.projectPath : null);
18470
18530
  if (!projectPath) {
@@ -18474,7 +18534,7 @@ var StreamerServer = class {
18474
18534
  return { ok: false, reason: "no_project_path" };
18475
18535
  }
18476
18536
  const provider = coerceProviderForRunner(
18477
- conv?.provider ?? cachedConvMeta?.provider ?? registryProvider
18537
+ conv?.provider ?? cachedConvMeta?.provider ?? managedProvider
18478
18538
  );
18479
18539
  return {
18480
18540
  ok: true,