@lotics/app-sdk 0.52.1 → 0.52.2

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.
@@ -449,6 +449,13 @@ export interface AgentRunOptions {
449
449
  /** Groups this run with prior runs in the same working session; the agent
450
450
  * re-reads them for context. Mint a new id to "clear context". */
451
451
  sessionId: string;
452
+ /** Deliberately abort the run in flight and start this one in its place.
453
+ * Without it, `run()` is SINGLE-FLIGHT: a call while a run is streaming
454
+ * returns the in-flight run's promise instead of starting (and billing) a
455
+ * second run — so an accidental double-press resolves with the first run's
456
+ * result. The aborted-and-replaced run still executes and bills server-side;
457
+ * replacement is a deliberate act, never a side effect of an extra click. */
458
+ replace?: boolean;
452
459
  }
453
460
  /** The live state + controls returned by `useAgentRun`. */
454
461
  export interface UseAgentRun<TInput, TOutput> {
package/dist/src/hooks.js CHANGED
@@ -440,6 +440,7 @@ export function useAgentRun(alias) {
440
440
  // header). Lets the hook poll the run to completion if the stream connection
441
441
  // drops, and cancel it server-side on an explicit stop.
442
442
  const runIdRef = useRef(null);
443
+ const inflightRef = useRef(null);
443
444
  // Guards setState after unmount and aborts any in-flight run on unmount, so a
444
445
  // stream never keeps writing to a dead component (or leaks the transport).
445
446
  const mountedRef = useRef(true);
@@ -455,6 +456,15 @@ export function useAgentRun(alias) {
455
456
  setState(s);
456
457
  }, []);
457
458
  const run = useCallback((input, opts) => {
459
+ // Single-flight: an extra press must not become a second paid run — the
460
+ // old abort-and-restart default kept the first run executing (and
461
+ // billing) server-side while the client went blind to it. Joining the
462
+ // in-flight promise makes a double-click resolve with the first run's
463
+ // result; `replace: true` is the explicit opt-in to abort-and-restart.
464
+ if (inflightRef.current && !opts.replace) {
465
+ captureAppEvent("app_agent_run_deduped", { alias });
466
+ return inflightRef.current;
467
+ }
458
468
  handleRef.current?.abort();
459
469
  runIdRef.current = null;
460
470
  let acc = initialAgentRunState();
@@ -486,7 +496,7 @@ export function useAgentRun(alias) {
486
496
  safeSetState(null);
487
497
  },
488
498
  };
489
- return handle.done
499
+ const inflight = handle.done
490
500
  .then(async () => {
491
501
  if (aborted)
492
502
  return undefined;
@@ -538,6 +548,12 @@ export function useAgentRun(alias) {
538
548
  safeSetState(acc);
539
549
  throw err;
540
550
  });
551
+ const tracked = inflight.finally(() => {
552
+ if (inflightRef.current === tracked)
553
+ inflightRef.current = null;
554
+ });
555
+ inflightRef.current = tracked;
556
+ return tracked;
541
557
  }, [alias, safeSetState]);
542
558
  const abort = useCallback(() => handleRef.current?.abort(), []);
543
559
  const cancel = useCallback(() => {
package/docs/ai.md CHANGED
@@ -49,7 +49,7 @@ await recognize.run({ image_file_id: fileId }, { sessionId });
49
49
 
50
50
  | Member | Type | What it is |
51
51
  |---|---|---|
52
- | `run` | `(input, { sessionId }) => Promise<TOutput \| undefined>` | Start a run. Streams progress into the hook's state and resolves to the structured output (`undefined` for a free-text or failed run). Calling it again **aborts any run still in flight** |
52
+ | `run` | `(input, { sessionId, replace? }) => Promise<TOutput \| undefined>` | Start a run. Streams progress into the hook's state and resolves to the structured output (`undefined` for a free-text or failed run). **Single-flight:** while a run is in flight, calling it again returns the in-flight run's promise — an accidental double-press joins the first run instead of billing a second one (each dedup emits the `app_agent_run_deduped` analytics event). Pass `replace: true` to deliberately abort-and-restart; the replaced run still executes and bills server-side |
53
53
  | `cancel` | `() => void` | Stop the run **server-side** (saves tokens) and locally. Wire a user-facing Stop button to this |
54
54
  | `abort` | `() => void` | Stop listening **locally only** — the run keeps executing server-side and its result is still persisted. This is the unmount path (the hook calls it automatically on unmount) |
55
55
  | `status` | `"idle" \| "streaming" \| "completed" \| "error"` | Whole-run state. `abort`/`cancel` reset it to `"idle"` (and clear the partial transcript) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.52.1",
3
+ "version": "0.52.2",
4
4
  "description": "Runtime SDK for Lotics custom-code apps — typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {