donobu 5.27.1 → 5.27.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.
@@ -107,8 +107,33 @@ export declare class DonobuFlow {
107
107
  private onUnexpectedException;
108
108
  /**
109
109
  * This method is called when a flow is complete (i.e. when {@link DonobuFlow.run} should return).
110
+ *
111
+ * Browser session state and the terminal-state metadata write are
112
+ * committed by whichever code path produced the terminal state
113
+ * (transitionState for tool-driven completion; onTargetClosed /
114
+ * onPersistentGptFailure / onInsufficientQuota / onUnexpectedException
115
+ * for failure paths) — by the time we reach onComplete those have
116
+ * already happened. This method just runs the post-completion side
117
+ * effects.
110
118
  */
111
119
  private onComplete;
120
+ /**
121
+ * Persists the current browser session state if the flow's config has
122
+ * `persistState` enabled. Must be called BEFORE the in-memory `state`
123
+ * is mutated to a terminal value at every site that produces a
124
+ * terminal state — otherwise FlowCatalog.getFlowById can read the
125
+ * live FlowMetadata object (LOCAL deployments) and a frontend that
126
+ * observes the terminal state will race the (potentially network-
127
+ * bound) upload here, getting a 404 from a subsequent browser-state
128
+ * fetch.
129
+ *
130
+ * The browser context typically survives all-pages-closed (the read
131
+ * goes against the context, not a specific page), so this is safe to
132
+ * call from failure handlers like onTargetClosed. If the read does
133
+ * fail, persistSessionState catches and logs internally — it doesn't
134
+ * propagate.
135
+ */
136
+ private persistTerminalSessionStateIfNeeded;
112
137
  /**
113
138
  * Attempt to POST a JSON body containing given flow ID to the given
114
139
  * ${@link callbackUrl} if the URL is non-null. Note that there is no retying
@@ -215,9 +215,18 @@ class DonobuFlow {
215
215
  async onTargetClosed() {
216
216
  const result = await this.targetInspector.handleTargetClosed();
217
217
  if (!result.recovered) {
218
- this.metadata.state = 'FAILED';
218
+ // Persist browser state BEFORE flipping the in-memory `state` to
219
+ // a terminal value. FlowCatalog.getFlowById serves the *live*
220
+ // FlowMetadata object for LOCAL deployments, so the next frontend
221
+ // poll observes terminal state the moment we mutate `state`.
222
+ // If we do that before the (potentially network-bound) session
223
+ // upload, an eager browser-state fetch from the frontend (e.g.
224
+ // FlowDeveloperTools auto-loads on terminal) races the upload and
225
+ // 404s. Same rationale as the ordering in transitionState.
219
226
  Logger_1.appLogger.error(result.reason);
227
+ await this.persistTerminalSessionStateIfNeeded();
220
228
  this.metadata.result = { failed: result.reason };
229
+ this.metadata.state = 'FAILED';
221
230
  await this.persistence.setFlowMetadata(this.metadata);
222
231
  }
223
232
  }
@@ -226,12 +235,13 @@ class DonobuFlow {
226
235
  * are internal retries). This method will mark the flow as a failure.
227
236
  */
228
237
  async onPersistentGptFailure(error) {
229
- this.metadata.state = 'FAILED';
230
238
  Logger_1.appLogger.error(`Stopped flow due to the ${this.gptClient?.config.type} GPT platform throwing an internal error!`, error);
239
+ await this.persistTerminalSessionStateIfNeeded();
231
240
  this.metadata.result = {
232
241
  failed: `Stopped flow due to the ${this.gptClient?.config.type} GPT platform throwing an internal error!`,
233
242
  context: error.message,
234
243
  };
244
+ this.metadata.state = 'FAILED';
235
245
  await this.persistence.setFlowMetadata(this.metadata);
236
246
  }
237
247
  /**
@@ -239,14 +249,15 @@ class DonobuFlow {
239
249
  * usage quota or credits have been exhausted (HTTP 402).
240
250
  */
241
251
  async onInsufficientQuota(error) {
242
- this.metadata.state = 'FAILED';
243
252
  const platform = error.gptPlatform;
244
253
  const isDonobu = platform === 'DONOBU';
245
254
  const failedMessage = isDonobu
246
255
  ? 'Your Donobu AI credits have been exhausted. Please add more credits to your account to continue running flows.'
247
256
  : `Your ${platform} API quota has been exhausted. Please check your account's billing and usage limits; this may happen if there is a lack of funds in the account`;
248
257
  Logger_1.appLogger.error(failedMessage, error);
258
+ await this.persistTerminalSessionStateIfNeeded();
249
259
  this.metadata.result = { failed: failedMessage };
260
+ this.metadata.state = 'FAILED';
250
261
  await this.persistence.setFlowMetadata(this.metadata);
251
262
  }
252
263
  /**
@@ -336,23 +347,49 @@ class DonobuFlow {
336
347
  * method will mark the flow as a failure.
337
348
  */
338
349
  async onUnexpectedException(error) {
339
- this.metadata.state = 'FAILED';
340
350
  Logger_1.appLogger.error('Stopped flow due to exception!', error);
351
+ await this.persistTerminalSessionStateIfNeeded();
341
352
  this.metadata.result = {
342
353
  failed: 'Internal error 🙈',
343
354
  };
355
+ this.metadata.state = 'FAILED';
344
356
  await this.persistence.setFlowMetadata(this.metadata);
345
357
  }
346
358
  /**
347
359
  * This method is called when a flow is complete (i.e. when {@link DonobuFlow.run} should return).
360
+ *
361
+ * Browser session state and the terminal-state metadata write are
362
+ * committed by whichever code path produced the terminal state
363
+ * (transitionState for tool-driven completion; onTargetClosed /
364
+ * onPersistentGptFailure / onInsufficientQuota / onUnexpectedException
365
+ * for failure paths) — by the time we reach onComplete those have
366
+ * already happened. This method just runs the post-completion side
367
+ * effects.
348
368
  */
349
369
  async onComplete() {
350
- await this.persistence.setFlowMetadata(this.metadata);
370
+ DonobuFlow.invokeFlowFinishedCallback(this.metadata.callbackUrl, this.metadata.id);
371
+ this.controlPanel.close();
372
+ }
373
+ /**
374
+ * Persists the current browser session state if the flow's config has
375
+ * `persistState` enabled. Must be called BEFORE the in-memory `state`
376
+ * is mutated to a terminal value at every site that produces a
377
+ * terminal state — otherwise FlowCatalog.getFlowById can read the
378
+ * live FlowMetadata object (LOCAL deployments) and a frontend that
379
+ * observes the terminal state will race the (potentially network-
380
+ * bound) upload here, getting a 404 from a subsequent browser-state
381
+ * fetch.
382
+ *
383
+ * The browser context typically survives all-pages-closed (the read
384
+ * goes against the context, not a specific page), so this is safe to
385
+ * call from failure handlers like onTargetClosed. If the read does
386
+ * fail, persistSessionState catches and logs internally — it doesn't
387
+ * propagate.
388
+ */
389
+ async persistTerminalSessionStateIfNeeded() {
351
390
  if (this.metadata.web?.browser?.persistState) {
352
391
  await this.targetInspector.persistSessionState(this.persistence, this.metadata.id);
353
392
  }
354
- DonobuFlow.invokeFlowFinishedCallback(this.metadata.callbackUrl, this.metadata.id);
355
- this.controlPanel.close();
356
393
  }
357
394
  /**
358
395
  * Attempt to POST a JSON body containing given flow ID to the given
@@ -623,8 +660,12 @@ Message: ${dialog.message()}`;
623
660
  // the result object before the final state is set. If we did not do
624
661
  // this, then someone polling for a flow's state may see the flow
625
662
  // finished but still see a null result.
663
+ //
664
+ // The same rationale applies to the browser session state — see
665
+ // persistTerminalSessionStateIfNeeded for the full ordering note.
626
666
  if ((0, FlowMetadata_1.isComplete)(nextState)) {
627
667
  this.metadata.result = await this.createResultJson(nextState);
668
+ await this.persistTerminalSessionStateIfNeeded();
628
669
  }
629
670
  const lastState = this.metadata.state;
630
671
  this.metadata.state = nextState;
@@ -107,8 +107,33 @@ export declare class DonobuFlow {
107
107
  private onUnexpectedException;
108
108
  /**
109
109
  * This method is called when a flow is complete (i.e. when {@link DonobuFlow.run} should return).
110
+ *
111
+ * Browser session state and the terminal-state metadata write are
112
+ * committed by whichever code path produced the terminal state
113
+ * (transitionState for tool-driven completion; onTargetClosed /
114
+ * onPersistentGptFailure / onInsufficientQuota / onUnexpectedException
115
+ * for failure paths) — by the time we reach onComplete those have
116
+ * already happened. This method just runs the post-completion side
117
+ * effects.
110
118
  */
111
119
  private onComplete;
120
+ /**
121
+ * Persists the current browser session state if the flow's config has
122
+ * `persistState` enabled. Must be called BEFORE the in-memory `state`
123
+ * is mutated to a terminal value at every site that produces a
124
+ * terminal state — otherwise FlowCatalog.getFlowById can read the
125
+ * live FlowMetadata object (LOCAL deployments) and a frontend that
126
+ * observes the terminal state will race the (potentially network-
127
+ * bound) upload here, getting a 404 from a subsequent browser-state
128
+ * fetch.
129
+ *
130
+ * The browser context typically survives all-pages-closed (the read
131
+ * goes against the context, not a specific page), so this is safe to
132
+ * call from failure handlers like onTargetClosed. If the read does
133
+ * fail, persistSessionState catches and logs internally — it doesn't
134
+ * propagate.
135
+ */
136
+ private persistTerminalSessionStateIfNeeded;
112
137
  /**
113
138
  * Attempt to POST a JSON body containing given flow ID to the given
114
139
  * ${@link callbackUrl} if the URL is non-null. Note that there is no retying
@@ -215,9 +215,18 @@ class DonobuFlow {
215
215
  async onTargetClosed() {
216
216
  const result = await this.targetInspector.handleTargetClosed();
217
217
  if (!result.recovered) {
218
- this.metadata.state = 'FAILED';
218
+ // Persist browser state BEFORE flipping the in-memory `state` to
219
+ // a terminal value. FlowCatalog.getFlowById serves the *live*
220
+ // FlowMetadata object for LOCAL deployments, so the next frontend
221
+ // poll observes terminal state the moment we mutate `state`.
222
+ // If we do that before the (potentially network-bound) session
223
+ // upload, an eager browser-state fetch from the frontend (e.g.
224
+ // FlowDeveloperTools auto-loads on terminal) races the upload and
225
+ // 404s. Same rationale as the ordering in transitionState.
219
226
  Logger_1.appLogger.error(result.reason);
227
+ await this.persistTerminalSessionStateIfNeeded();
220
228
  this.metadata.result = { failed: result.reason };
229
+ this.metadata.state = 'FAILED';
221
230
  await this.persistence.setFlowMetadata(this.metadata);
222
231
  }
223
232
  }
@@ -226,12 +235,13 @@ class DonobuFlow {
226
235
  * are internal retries). This method will mark the flow as a failure.
227
236
  */
228
237
  async onPersistentGptFailure(error) {
229
- this.metadata.state = 'FAILED';
230
238
  Logger_1.appLogger.error(`Stopped flow due to the ${this.gptClient?.config.type} GPT platform throwing an internal error!`, error);
239
+ await this.persistTerminalSessionStateIfNeeded();
231
240
  this.metadata.result = {
232
241
  failed: `Stopped flow due to the ${this.gptClient?.config.type} GPT platform throwing an internal error!`,
233
242
  context: error.message,
234
243
  };
244
+ this.metadata.state = 'FAILED';
235
245
  await this.persistence.setFlowMetadata(this.metadata);
236
246
  }
237
247
  /**
@@ -239,14 +249,15 @@ class DonobuFlow {
239
249
  * usage quota or credits have been exhausted (HTTP 402).
240
250
  */
241
251
  async onInsufficientQuota(error) {
242
- this.metadata.state = 'FAILED';
243
252
  const platform = error.gptPlatform;
244
253
  const isDonobu = platform === 'DONOBU';
245
254
  const failedMessage = isDonobu
246
255
  ? 'Your Donobu AI credits have been exhausted. Please add more credits to your account to continue running flows.'
247
256
  : `Your ${platform} API quota has been exhausted. Please check your account's billing and usage limits; this may happen if there is a lack of funds in the account`;
248
257
  Logger_1.appLogger.error(failedMessage, error);
258
+ await this.persistTerminalSessionStateIfNeeded();
249
259
  this.metadata.result = { failed: failedMessage };
260
+ this.metadata.state = 'FAILED';
250
261
  await this.persistence.setFlowMetadata(this.metadata);
251
262
  }
252
263
  /**
@@ -336,23 +347,49 @@ class DonobuFlow {
336
347
  * method will mark the flow as a failure.
337
348
  */
338
349
  async onUnexpectedException(error) {
339
- this.metadata.state = 'FAILED';
340
350
  Logger_1.appLogger.error('Stopped flow due to exception!', error);
351
+ await this.persistTerminalSessionStateIfNeeded();
341
352
  this.metadata.result = {
342
353
  failed: 'Internal error 🙈',
343
354
  };
355
+ this.metadata.state = 'FAILED';
344
356
  await this.persistence.setFlowMetadata(this.metadata);
345
357
  }
346
358
  /**
347
359
  * This method is called when a flow is complete (i.e. when {@link DonobuFlow.run} should return).
360
+ *
361
+ * Browser session state and the terminal-state metadata write are
362
+ * committed by whichever code path produced the terminal state
363
+ * (transitionState for tool-driven completion; onTargetClosed /
364
+ * onPersistentGptFailure / onInsufficientQuota / onUnexpectedException
365
+ * for failure paths) — by the time we reach onComplete those have
366
+ * already happened. This method just runs the post-completion side
367
+ * effects.
348
368
  */
349
369
  async onComplete() {
350
- await this.persistence.setFlowMetadata(this.metadata);
370
+ DonobuFlow.invokeFlowFinishedCallback(this.metadata.callbackUrl, this.metadata.id);
371
+ this.controlPanel.close();
372
+ }
373
+ /**
374
+ * Persists the current browser session state if the flow's config has
375
+ * `persistState` enabled. Must be called BEFORE the in-memory `state`
376
+ * is mutated to a terminal value at every site that produces a
377
+ * terminal state — otherwise FlowCatalog.getFlowById can read the
378
+ * live FlowMetadata object (LOCAL deployments) and a frontend that
379
+ * observes the terminal state will race the (potentially network-
380
+ * bound) upload here, getting a 404 from a subsequent browser-state
381
+ * fetch.
382
+ *
383
+ * The browser context typically survives all-pages-closed (the read
384
+ * goes against the context, not a specific page), so this is safe to
385
+ * call from failure handlers like onTargetClosed. If the read does
386
+ * fail, persistSessionState catches and logs internally — it doesn't
387
+ * propagate.
388
+ */
389
+ async persistTerminalSessionStateIfNeeded() {
351
390
  if (this.metadata.web?.browser?.persistState) {
352
391
  await this.targetInspector.persistSessionState(this.persistence, this.metadata.id);
353
392
  }
354
- DonobuFlow.invokeFlowFinishedCallback(this.metadata.callbackUrl, this.metadata.id);
355
- this.controlPanel.close();
356
393
  }
357
394
  /**
358
395
  * Attempt to POST a JSON body containing given flow ID to the given
@@ -623,8 +660,12 @@ Message: ${dialog.message()}`;
623
660
  // the result object before the final state is set. If we did not do
624
661
  // this, then someone polling for a flow's state may see the flow
625
662
  // finished but still see a null result.
663
+ //
664
+ // The same rationale applies to the browser session state — see
665
+ // persistTerminalSessionStateIfNeeded for the full ordering note.
626
666
  if ((0, FlowMetadata_1.isComplete)(nextState)) {
627
667
  this.metadata.result = await this.createResultJson(nextState);
668
+ await this.persistTerminalSessionStateIfNeeded();
628
669
  }
629
670
  const lastState = this.metadata.state;
630
671
  this.metadata.state = nextState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donobu",
3
- "version": "5.27.1",
3
+ "version": "5.27.3",
4
4
  "description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/esm/main.js",