@lotics/app-sdk 0.52.3 → 0.52.4
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/src/hooks.js +39 -6
- package/docs/ai.md +5 -3
- package/package.json +1 -1
package/dist/src/hooks.js
CHANGED
|
@@ -515,11 +515,23 @@ export function useAgentRun(alias) {
|
|
|
515
515
|
: null;
|
|
516
516
|
if (aborted)
|
|
517
517
|
return undefined;
|
|
518
|
-
|
|
518
|
+
// No settled row after the full poll window means the run's status
|
|
519
|
+
// could NOT be confirmed (an orphan the server's reaper hasn't
|
|
520
|
+
// repaired yet, or no run id ever arrived) — that is an error, never
|
|
521
|
+
// a fake "completed": a structured consumer reading a completed
|
|
522
|
+
// state with no output would render success around a missing result.
|
|
523
|
+
acc = settled
|
|
524
|
+
? adoptSettledRun(acc, settled)
|
|
525
|
+
: {
|
|
526
|
+
...acc,
|
|
527
|
+
status: "error",
|
|
528
|
+
error: "The run was interrupted and its result could not be confirmed. Run it again.",
|
|
529
|
+
};
|
|
519
530
|
safeSetState(acc);
|
|
520
531
|
// Fleet visibility: how often edges cut agent SSE streams. One
|
|
521
532
|
// event per truncation, with whether the row rescued the result.
|
|
522
533
|
captureAppEvent("app_agent_stream_truncated", {
|
|
534
|
+
kind: "clean_end",
|
|
523
535
|
recovered: settled != null,
|
|
524
536
|
settled_status: settled?.status ?? null,
|
|
525
537
|
has_output: acc.output !== undefined,
|
|
@@ -536,13 +548,24 @@ export function useAgentRun(alias) {
|
|
|
536
548
|
const runId = runIdRef.current;
|
|
537
549
|
if (runId) {
|
|
538
550
|
const settled = await pollAgentRunToSettle(runId, () => aborted || !mountedRef.current);
|
|
539
|
-
if (
|
|
551
|
+
if (aborted)
|
|
552
|
+
return undefined;
|
|
553
|
+
if (settled) {
|
|
540
554
|
acc = adoptSettledRun(acc, settled);
|
|
541
555
|
safeSetState(acc);
|
|
542
|
-
return acc.output;
|
|
543
556
|
}
|
|
544
|
-
|
|
545
|
-
|
|
557
|
+
// The same fleet-visibility beacon as the clean-end path — a dropped
|
|
558
|
+
// connection is the OTHER way a stream dies mid-run, and it was
|
|
559
|
+
// previously invisible (the beacon fired only on clean-end
|
|
560
|
+
// truncations, undercounting every network-error cut).
|
|
561
|
+
captureAppEvent("app_agent_stream_truncated", {
|
|
562
|
+
kind: "connection_error",
|
|
563
|
+
recovered: settled != null,
|
|
564
|
+
settled_status: settled?.status ?? null,
|
|
565
|
+
has_output: acc.output !== undefined,
|
|
566
|
+
});
|
|
567
|
+
if (settled)
|
|
568
|
+
return acc.output;
|
|
546
569
|
}
|
|
547
570
|
acc = { ...acc, status: "error", error: err.message };
|
|
548
571
|
safeSetState(acc);
|
|
@@ -583,6 +606,16 @@ export function useAgentRun(alias) {
|
|
|
583
606
|
/** Stable empty transcript so an idle hook returns a constant `items` reference
|
|
584
607
|
* (no new [] each render → dependents don't re-run needlessly). */
|
|
585
608
|
const EMPTY_ITEMS = [];
|
|
609
|
+
/**
|
|
610
|
+
* Bounded PAST the server-side max-run cap (20 min) plus settle grace. The
|
|
611
|
+
* server guarantees a LIVE run settles by its own cap timer, so a row still
|
|
612
|
+
* `running` at this deadline is an orphan (its process died mid-run) — the
|
|
613
|
+
* server's reapers repair the row; the client stops polling and surfaces the
|
|
614
|
+
* error. An earlier 11-minute bound gave up BEFORE the cap: a slow run that
|
|
615
|
+
* lost its stream showed failure while the server later completed it — the
|
|
616
|
+
* exact result-loss class this poll exists to prevent.
|
|
617
|
+
*/
|
|
618
|
+
const POLL_DEADLINE_MS = 22 * 60_000;
|
|
586
619
|
/**
|
|
587
620
|
* Poll a single run until it leaves `running` — the resume path when a run's
|
|
588
621
|
* stream connection drops mid-flight. Bounded just past the server-side max-run
|
|
@@ -590,7 +623,7 @@ const EMPTY_ITEMS = [];
|
|
|
590
623
|
* aborts or unmounts. Returns the settled run, or null if it never settled.
|
|
591
624
|
*/
|
|
592
625
|
async function pollAgentRunToSettle(runId, cancelled) {
|
|
593
|
-
const deadline = Date.now() +
|
|
626
|
+
const deadline = Date.now() + POLL_DEADLINE_MS;
|
|
594
627
|
while (Date.now() < deadline) {
|
|
595
628
|
await new Promise((r) => setTimeout(r, 2500));
|
|
596
629
|
if (cancelled())
|
package/docs/ai.md
CHANGED
|
@@ -123,10 +123,12 @@ Both settle the in-flight `run()` promise cleanly with `undefined` — a stop is
|
|
|
123
123
|
|
|
124
124
|
The run's lifetime is decoupled from the stream: the server drives it to completion and persists the result even if the connection drops. The hook reads the run id from the stream's start and recovers through the **persisted run row** — the source of truth — in BOTH failure shapes:
|
|
125
125
|
|
|
126
|
-
- **The connection drops with an error** → the hook polls the persisted run to completion (every 2.5 s
|
|
127
|
-
- **The stream ends cleanly WITHOUT a `finish` frame** — an edge/proxy can close a long SSE gracefully mid-run, which looks like completion but swallowed the trailing frames (including the structured result). The hook detects the missing `finish` and polls the row the same way.
|
|
126
|
+
- **The connection drops with an error** → the hook polls the persisted run to completion (every 2.5 s) and resolves with the settled result instead of surfacing a network error.
|
|
127
|
+
- **The stream ends cleanly WITHOUT a `finish` frame** — an edge/proxy can close a long SSE gracefully mid-run, which looks like completion but swallowed the trailing frames (including the structured result). The hook detects the missing `finish` and polls the row the same way.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Both truncation shapes emit the `app_agent_stream_truncated` analytics event (`kind: "connection_error" | "clean_end"`, with whether the row rescued the result), so edge-cut frequency is visible fleet-wide.
|
|
130
|
+
|
|
131
|
+
The poll is bounded at 22 minutes — deliberately PAST the server's 20-minute hard run cap, so a live run always settles before the client gives up. A row still `running` at the deadline means the run's process died mid-flight (e.g. a crash that skipped the server's shutdown drain); the poll surfaces an error and the server's reaper repairs the row. Only when no run id was ever received (the run never started) does the failure reject before any polling.
|
|
130
132
|
|
|
131
133
|
On recovery, `output` adopts the row's output **only when it is an object** (a structured result) — the "`output` is never a stray string" rule holds on every path. A **free-text** run recovered from truncation keeps only the streamed prefix in `text`; read the full settled answer via `useAgentRuns(sessionId)` if you need it.
|
|
132
134
|
|