@agent-native/core 0.220.0 → 0.220.1-nightly-20261024183236

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.
Files changed (46) hide show
  1. package/dist/action.d.ts +18 -0
  2. package/dist/action.js +15 -0
  3. package/dist/agent/production-agent.js +42 -15
  4. package/dist/agent/run-manager.js +24 -4
  5. package/dist/automation/index.d.ts +1 -1
  6. package/dist/client/sse-event-processor.d.ts +7 -0
  7. package/dist/client/sse-event-processor.js +3 -2
  8. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  9. package/dist/integrations/google-docs-poller.d.ts +9 -1
  10. package/dist/integrations/google-docs-poller.js +16 -5
  11. package/dist/integrations/pending-tasks-retry-job.d.ts +1 -1
  12. package/dist/integrations/pending-tasks-retry-job.js +6 -2
  13. package/dist/integrations/plugin.js +36 -5
  14. package/dist/integrations/remote-push-delivery-job.d.ts +1 -1
  15. package/dist/integrations/remote-push-delivery-job.js +6 -2
  16. package/dist/integrations/remote-retry-job.d.ts +1 -1
  17. package/dist/integrations/remote-retry-job.js +6 -2
  18. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  19. package/dist/notifications/routes.d.ts +4 -4
  20. package/dist/observability/routes.d.ts +7 -7
  21. package/dist/org/handlers.d.ts +1 -1
  22. package/dist/progress/routes.d.ts +1 -1
  23. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  24. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  25. package/dist/provider-api/corpus-jobs.js +3 -21
  26. package/dist/provider-api/index.js +17 -23
  27. package/dist/provider-api/request-body.d.ts +1 -0
  28. package/dist/provider-api/request-body.js +50 -0
  29. package/dist/provider-api/staging.js +2 -20
  30. package/dist/secrets/routes.d.ts +10 -10
  31. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  32. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  33. package/dist/server/background-jobs.d.ts +3 -0
  34. package/dist/server/background-jobs.js +33 -0
  35. package/dist/server/core-routes-plugin.js +20 -2
  36. package/dist/server/dev-database-lifecycle.js +13 -3
  37. package/dist/server/interval-job.d.ts +2 -1
  38. package/dist/server/interval-job.js +1 -1
  39. package/dist/shared/poll-engine.d.ts +2 -1
  40. package/dist/shared/poll-engine.js +25 -1
  41. package/dist/shared/progress-events.d.ts +1 -0
  42. package/dist/shared/progress-events.js +13 -0
  43. package/dist/triggers/actions/manage-automation.d.ts +6 -6
  44. package/dist/triggers/dispatcher.d.ts +1 -0
  45. package/dist/triggers/dispatcher.js +28 -2
  46. package/package.json +3 -3
package/dist/action.d.ts CHANGED
@@ -23,8 +23,26 @@ export interface ActionAutomationContext {
23
23
  triggerName: string;
24
24
  policyId?: string;
25
25
  }
26
+ export declare const DEFAULT_TOOL_RESULT_CHARS = 50000;
27
+ /**
28
+ * The most characters of a pretty-printed result the agent keeps for a call:
29
+ * the action's own limit, else the agent-wide limit, else the default, bounded
30
+ * by any hard limit. The agent loop and actions that fit their own results
31
+ * resolve it here so they cannot disagree.
32
+ */
33
+ export declare function resolveToolMaxResultChars(action: {
34
+ maxResultChars?: number;
35
+ }, limits?: {
36
+ maxResultChars?: number;
37
+ hardMaxResultChars?: number;
38
+ }): number;
26
39
  export interface ActionRunContext {
27
40
  send?: (event: AgentChatEvent) => void;
41
+ /**
42
+ * Result cap the agent loop enforces on this call, from
43
+ * `resolveToolMaxResultChars`. Absent outside the agent loop.
44
+ */
45
+ maxResultChars?: number;
28
46
  userEmail?: string;
29
47
  /**
30
48
  * Request headers for server-only actions that must delegate to an
package/dist/action.js CHANGED
@@ -48,6 +48,21 @@ export async function recordInvalidActionCall(entry, args, ctx, error) {
48
48
  }
49
49
  return true;
50
50
  }
51
+ export const DEFAULT_TOOL_RESULT_CHARS = 50_000;
52
+ /**
53
+ * The most characters of a pretty-printed result the agent keeps for a call:
54
+ * the action's own limit, else the agent-wide limit, else the default, bounded
55
+ * by any hard limit. The agent loop and actions that fit their own results
56
+ * resolve it here so they cannot disagree.
57
+ */
58
+ export function resolveToolMaxResultChars(action, limits) {
59
+ const configured = action.maxResultChars ??
60
+ limits?.maxResultChars ??
61
+ DEFAULT_TOOL_RESULT_CHARS;
62
+ return typeof limits?.hardMaxResultChars === "number"
63
+ ? Math.min(configured, limits.hardMaxResultChars)
64
+ : configured;
65
+ }
51
66
  export class ActionContractError extends Error {
52
67
  actionContractError = true;
53
68
  errorCode;
@@ -9,7 +9,7 @@ import { parseA2AAgentActivityPart } from "../a2a/activity.js";
9
9
  import { actionCallIsRetrySafe, actionChangeResource, classifyActionCall, } from "../action-call-classification.js";
10
10
  import { runOutsideActionInvocation, vouchedOutcome } from "../action-frame.js";
11
11
  import { ACTION_CHAT_UI_RECORD_CHANGE_RENDERER, normalizeActionChangeResult, } from "../action-ui.js";
12
- import { AgentConnectionRequiredError, describeToolParameterSignature, isActionContractError, isActionHiddenFromEveryAgentSurface, isAgentActionStopError, isAgentConnectionRequiredError, actionCallIntent, ActionInputValidationError, invocationEffect, runPreparedActionCall, boundActionCallIntent, preparedCallArguments, preparedCallNeedsApproval, recordInvalidActionCall, stripUnsupportedSchemaKeywords, } from "../action.js";
12
+ import { AgentConnectionRequiredError, describeToolParameterSignature, isActionContractError, isActionHiddenFromEveryAgentSurface, isAgentActionStopError, isAgentConnectionRequiredError, actionCallIntent, ActionInputValidationError, invocationEffect, runPreparedActionCall, boundActionCallIntent, preparedCallArguments, preparedCallNeedsApproval, recordInvalidActionCall, resolveToolMaxResultChars, stripUnsupportedSchemaKeywords, } from "../action.js";
13
13
  import { getAppConfig } from "../app-config/index.js";
14
14
  import { MAX_BACKGROUND_RUN_CONTINUATIONS, MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS, MAX_TURN_WALL_CLOCK_MS, } from "../app-config/run-lifecycle-invariants.js";
15
15
  import { readAppState } from "../application-state/script-helpers.js";
@@ -1239,6 +1239,7 @@ const VISIBLE_RETRY_THRESHOLD_MS = 10_000;
1239
1239
  */
1240
1240
  const FOREGROUND_FIRST_MODEL_EVENT_TIMEOUT_MS = 25_000;
1241
1241
  const EMPTY_FINAL_RESPONSE_RETRY_LIMIT = 2;
1242
+ const MAX_CONSECUTIVE_MAX_TOKENS_CONTINUATIONS = 3;
1242
1243
  const TRUNCATED_TOOL_CALL_RETRY_LIMIT = 2;
1243
1244
  const MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT = 6;
1244
1245
  const RUN_BUDGET_EXHAUSTED_ERROR_CODE = "run_budget_exhausted";
@@ -5151,6 +5152,8 @@ export async function runAgentLoop(opts) {
5151
5152
  const blockedA2ATargets = new Map();
5152
5153
  let finalGuardRetries = 0;
5153
5154
  let emptyFinalResponseRetries = 0;
5155
+ let preserveReasoningEffortForEmptyResponseRetries = false;
5156
+ let consecutiveMaxTokensContinuations = 0;
5154
5157
  let truncatedToolCallRetries = 0;
5155
5158
  let iterations = 0;
5156
5159
  let endedAtLoopLimit = false;
@@ -5888,21 +5891,42 @@ export async function runAgentLoop(opts) {
5888
5891
  send({ type: "text", text });
5889
5892
  };
5890
5893
  if (toolCallParts.length === 0) {
5894
+ const hasEmptyFinalResponse = collectTextParts(assistantContentForHistory).trim().length === 0 &&
5895
+ streamedAssistantText.trim().length === 0;
5891
5896
  if (followUpResults.length > 0)
5892
5897
  messages.push({ role: "user", content: followUpResults });
5893
5898
  if (terminalStopReason === "max_tokens") {
5894
- flushUnstreamedAssistantText();
5895
- appendAgentLoopContinuation(messages, "max_tokens");
5896
- continue;
5899
+ if (hasEmptyFinalResponse) {
5900
+ preserveReasoningEffortForEmptyResponseRetries = true;
5901
+ }
5902
+ else if (consecutiveMaxTokensContinuations <
5903
+ MAX_CONSECUTIVE_MAX_TOKENS_CONTINUATIONS) {
5904
+ consecutiveMaxTokensContinuations += 1;
5905
+ flushUnstreamedAssistantText();
5906
+ appendAgentLoopContinuation(messages, "max_tokens");
5907
+ continue;
5908
+ }
5909
+ else {
5910
+ flushUnstreamedAssistantText();
5911
+ terminalActionStop = {
5912
+ message: "This response kept reaching the output-token limit after several continuations and may be incomplete. Ask for a shorter or narrower answer.",
5913
+ errorCode: "max_tokens_continuation_limit",
5914
+ };
5915
+ sendTerminalActionStop(terminalActionStop);
5916
+ break;
5917
+ }
5918
+ }
5919
+ else {
5920
+ consecutiveMaxTokensContinuations = 0;
5897
5921
  }
5898
- const hasEmptyFinalResponse = collectTextParts(assistantContentForHistory).trim().length === 0 &&
5899
- streamedAssistantText.trim().length === 0;
5900
5922
  if (followUpResults.length > 0 && terminalStopReason === "tool_use") {
5901
5923
  if (streamedAssistantText)
5902
5924
  send({ type: "clear" });
5903
5925
  continue;
5904
5926
  }
5905
- if (followUpResults.length > 0 && hasEmptyFinalResponse) {
5927
+ if (followUpResults.length > 0 &&
5928
+ hasEmptyFinalResponse &&
5929
+ terminalStopReason !== "max_tokens") {
5906
5930
  flushUnstreamedAssistantText();
5907
5931
  continue;
5908
5932
  }
@@ -5910,9 +5934,13 @@ export async function runAgentLoop(opts) {
5910
5934
  completedFollowUpSuggestions = undefined;
5911
5935
  if (emptyFinalResponseRetries < EMPTY_FINAL_RESPONSE_RETRY_LIMIT) {
5912
5936
  emptyFinalResponseRetries += 1;
5937
+ const previousMaxOutputTokens = resolveMaxOutputTokensForEngine(engine.name, effectiveMaxOutputTokens, model);
5913
5938
  effectiveMaxOutputTokens =
5914
5939
  resolveEmptyResponseRetryMaxOutputTokens(model);
5915
- effectiveReasoningEffort = stepDownReasoningEffort(effectiveReasoningEffort);
5940
+ if (!preserveReasoningEffortForEmptyResponseRetries ||
5941
+ effectiveMaxOutputTokens <= previousMaxOutputTokens) {
5942
+ effectiveReasoningEffort = stepDownReasoningEffort(effectiveReasoningEffort);
5943
+ }
5916
5944
  send({ type: "clear" });
5917
5945
  appendAgentLoopContinuation(messages, "max_tokens");
5918
5946
  continue;
@@ -6006,6 +6034,8 @@ export async function runAgentLoop(opts) {
6006
6034
  }
6007
6035
  }
6008
6036
  emptyFinalResponseRetries = 0;
6037
+ preserveReasoningEffortForEmptyResponseRetries = false;
6038
+ consecutiveMaxTokensContinuations = 0;
6009
6039
  effectiveMaxOutputTokens = opts.maxOutputTokens;
6010
6040
  effectiveReasoningEffort = opts.reasoningEffort;
6011
6041
  if (loopBreakerCloseout)
@@ -6014,6 +6044,8 @@ export async function runAgentLoop(opts) {
6014
6044
  }
6015
6045
  finalGuardRetries = 0;
6016
6046
  emptyFinalResponseRetries = 0;
6047
+ preserveReasoningEffortForEmptyResponseRetries = false;
6048
+ consecutiveMaxTokensContinuations = 0;
6017
6049
  const toolCallTruncatedByOutputCap = terminalStopReason === "max_tokens";
6018
6050
  if (toolCallTruncatedByOutputCap) {
6019
6051
  if (truncatedToolCallRetries < TRUNCATED_TOOL_CALL_RETRY_LIMIT) {
@@ -6600,14 +6632,8 @@ export async function runAgentLoop(opts) {
6600
6632
  };
6601
6633
  }
6602
6634
  }
6603
- const DEFAULT_TOOL_RESULT_CHARS = 50_000;
6604
6635
  const toolTimeoutMs = toolTimeoutMsFor(actionEntry);
6605
- const configuredToolMaxResultChars = actionEntry.maxResultChars ??
6606
- opts.toolLimits?.maxResultChars ??
6607
- DEFAULT_TOOL_RESULT_CHARS;
6608
- const toolMaxResultChars = typeof opts.toolLimits?.hardMaxResultChars === "number"
6609
- ? Math.min(configuredToolMaxResultChars, opts.toolLimits.hardMaxResultChars)
6610
- : configuredToolMaxResultChars;
6636
+ const toolMaxResultChars = resolveToolMaxResultChars(actionEntry, opts.toolLimits);
6611
6637
  const executionFence = currentExecutionFence();
6612
6638
  const logicalExecutionStepId = logicalExecutionStepIds.get(originalToolCall);
6613
6639
  const interruptedStep = executionFence && toolCallJournal
@@ -7201,6 +7227,7 @@ export async function runAgentLoop(opts) {
7201
7227
  signal,
7202
7228
  actionName: toolCall.name,
7203
7229
  toolCallId: toolCall.id,
7230
+ maxResultChars: toolMaxResultChars,
7204
7231
  ...(wasApproved ? { approvedToolCallKey: approvalKey } : {}),
7205
7232
  };
7206
7233
  actionInvoked = true;
@@ -5,6 +5,7 @@ import { recordAgentRun } from "../observability/metrics.js";
5
5
  import { parseServiceIdentityEmail } from "../org/service-identity.js";
6
6
  import { ServicePrincipalRefusedError } from "../org/service-principal-guard.js";
7
7
  import { captureError } from "../server/capture-error.js";
8
+ import { isNonProgressEventType } from "../shared/progress-events.js";
8
9
  import { EXECUTION_LEASE_LOST_ABORT_REASON } from "./abort-reasons.js";
9
10
  import { isLlmCredentialError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
10
11
  import { classifyTerminalErrorCode, describeErrorWithCauses, isProviderConnectionError, } from "./engine/error-detail.js";
@@ -86,6 +87,7 @@ export function resolveRunNoProgressTimeoutMs(params) {
86
87
  return ceiling;
87
88
  return override === 0 ? 0 : Math.min(override, ceiling);
88
89
  }
90
+ const MAX_CONSECUTIVE_EMPTY_MODEL_CALLS = 3;
89
91
  function inFlightWorkDelta(event) {
90
92
  switch (event.type) {
91
93
  case "tool_start":
@@ -577,6 +579,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
577
579
  chunkAbort = new AbortController();
578
580
  armChunkSoftTimeout();
579
581
  lastRealProgressAt = Date.now();
582
+ consecutiveEmptyModelCalls = 0;
580
583
  return chunkAbort.signal;
581
584
  },
582
585
  };
@@ -888,8 +891,6 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
888
891
  scheduleProgressWrite();
889
892
  };
890
893
  const shouldBumpProgressForEvent = (event) => {
891
- if (event.type === "stream_keepalive")
892
- return false;
893
894
  if (event.type === "clear") {
894
895
  for (const [key, bytes] of preparingActivityBytes) {
895
896
  const toolKey = preparingActivityTools.get(key) ?? key;
@@ -897,8 +898,9 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
897
898
  }
898
899
  preparingActivityBytes.clear();
899
900
  preparingActivityTools.clear();
900
- return false;
901
901
  }
902
+ if (isNonProgressEventType(event.type))
903
+ return false;
902
904
  if (event.type === "activity" && isPreparingActionActivityEvent(event)) {
903
905
  const toolKey = event.tool?.trim() || event.label.trim();
904
906
  const activityKey = `${toolKey}:${event.id?.trim() || "no-id"}`;
@@ -954,6 +956,11 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
954
956
  return true;
955
957
  };
956
958
  let lastRealProgressAt = Date.now();
959
+ // Every completed model call refreshes the silence clock (its result is still
960
+ // being processed), so a loop of calls that produce no text, tool call or
961
+ // result is caught by counting consecutive empty calls instead.
962
+ let consecutiveEmptyModelCalls = 0;
963
+ let modelCallHadProgress = false;
957
964
  let inFlightWorkCount = 0;
958
965
  let inFlightMarkerSince = null;
959
966
  let inFlightMarkerWrite = Promise.resolve();
@@ -1163,7 +1170,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1163
1170
  if (inFlightWorkCount > 0)
1164
1171
  return;
1165
1172
  const silentForMs = Date.now() - lastRealProgressAt;
1166
- if (silentForMs < noProgressTimeoutMs)
1173
+ const emptyCallLoop = consecutiveEmptyModelCalls > MAX_CONSECUTIVE_EMPTY_MODEL_CALLS;
1174
+ if (!emptyCallLoop && silentForMs < noProgressTimeoutMs)
1167
1175
  return;
1168
1176
  const lastEventType = run.events.at(-1)?.event.type;
1169
1177
  if (!chunkAbort) {
@@ -1410,8 +1418,20 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1410
1418
  trackInFlightWork(runEvent.event);
1411
1419
  if (shouldBumpProgressForEvent(runEvent.event)) {
1412
1420
  lastRealProgressAt = Date.now();
1421
+ consecutiveEmptyModelCalls = 0;
1422
+ modelCallHadProgress = true;
1413
1423
  bumpProgressIfDue();
1414
1424
  }
1425
+ else if (runEvent.event.type === "model_stream") {
1426
+ if (runEvent.event.status === "start") {
1427
+ modelCallHadProgress = false;
1428
+ }
1429
+ else {
1430
+ lastRealProgressAt = Date.now();
1431
+ if (!modelCallHadProgress)
1432
+ consecutiveEmptyModelCalls++;
1433
+ }
1434
+ }
1415
1435
  };
1416
1436
  const queueRunEvent = (pendingWrite, runEvent) => {
1417
1437
  let resolve;
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;
@@ -297,6 +297,13 @@ export type PreparingActionState = {
297
297
  entries?: Map<string, PreparingActionEntry>;
298
298
  toolEntries?: Map<string, PreparingActionEntry>;
299
299
  };
300
+ /**
301
+ * The browser's durable liveness cursor must mirror the server's
302
+ * `last_progress_at` predicate. The stream watchdog intentionally has a
303
+ * broader background policy, so it cannot be reused for this cursor: raw
304
+ * keepalives and repeated zero-byte preparation are not proof of real work.
305
+ */
306
+ export declare function isDurableProgressEvent(ev: SSEEvent, actionPreparationProgress?: boolean): boolean;
300
307
  /**
301
308
  * Locate the tool call an `approval_required` event refers to.
302
309
  *
@@ -1,6 +1,7 @@
1
1
  import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "../agent/engine/credential-errors.js";
2
2
  import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR_CODE, } from "../agent/engine/error-detail.js";
3
3
  import { normalizeConnectRequiredResult } from "../shared/connect-required.js";
4
+ import { isNonProgressEventType } from "../shared/progress-events.js";
4
5
  import { toolInputsMatch } from "../shared/tool-inputs.js";
5
6
  import { emitChatFirstOpenApp } from "./chat-first-state.js";
6
7
  import { formatChatErrorText, normalizeChatError } from "./error-format.js";
@@ -205,8 +206,8 @@ function isMeaningfulProgressEvent(ev, actionPreparationProgress, options) {
205
206
  * broader background policy, so it cannot be reused for this cursor: raw
206
207
  * keepalives and repeated zero-byte preparation are not proof of real work.
207
208
  */
208
- function isDurableProgressEvent(ev, actionPreparationProgress) {
209
- if (ev.type === "stream_keepalive" || ev.type === "clear")
209
+ export function isDurableProgressEvent(ev, actionPreparationProgress) {
210
+ if (isNonProgressEventType(ev.type))
210
211
  return false;
211
212
  if (ev.type === "activity" && isPreparingActionActivity(ev)) {
212
213
  return actionPreparationProgress === true;
@@ -42,8 +42,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
42
42
  url: string;
43
43
  id: string;
44
44
  provider: string;
45
- error?: undefined;
46
45
  configured?: undefined;
46
+ error?: undefined;
47
47
  } | {
48
48
  url?: undefined;
49
49
  id?: undefined;
@@ -17,4 +17,12 @@ export declare function stopGoogleDocsWatchChannel(accessToken: string, channelI
17
17
  export declare function processChanges(options: GoogleDocsPollerOptions): Promise<void>;
18
18
  export declare function handlePushNotification(): Promise<void>;
19
19
  export declare function startGoogleDocsPoller(options: GoogleDocsPollerOptions): Promise<void>;
20
- export declare function stopGoogleDocsPoller(): Promise<void>;
20
+ /**
21
+ * Stops polling and watch renewal, then retires the shared Drive watch.
22
+ * With `shuttingDown`, it instead waits for a poll in flight to finish and
23
+ * leaves the watch registered for the servers still running, or the next
24
+ * start, to renew or replace.
25
+ */
26
+ export declare function stopGoogleDocsPoller(options?: {
27
+ shuttingDown?: boolean;
28
+ }): Promise<void>;
@@ -663,26 +663,37 @@ function startPollLoop(options, intervalMs) {
663
663
  pollerStartTimer = null;
664
664
  pollerJob = startIntervalJob(poll, { intervalMs });
665
665
  }, 5000);
666
+ pollerStartTimer.unref?.();
666
667
  const email = getServiceAccountEmail();
667
668
  if (process.env.DEBUG) {
668
669
  console.log(`[google-docs] Poll loop started (interval: ${intervalMs / 1000}s, service account: ${email ?? "not configured"})`);
669
670
  }
670
671
  }
671
- export async function stopGoogleDocsPoller() {
672
+ /**
673
+ * Stops polling and watch renewal, then retires the shared Drive watch.
674
+ * With `shuttingDown`, it instead waits for a poll in flight to finish and
675
+ * leaves the watch registered for the servers still running, or the next
676
+ * start, to renew or replace.
677
+ */
678
+ export async function stopGoogleDocsPoller(options = {}) {
672
679
  pollerGeneration += 1;
673
680
  activeOptions = null;
674
681
  if (pollerStartTimer) {
675
682
  clearTimeout(pollerStartTimer);
676
683
  pollerStartTimer = null;
677
684
  }
678
- if (pollerJob) {
679
- pollerJob.stop();
680
- pollerJob = null;
681
- }
682
685
  if (watchRenewalTimer) {
683
686
  clearTimeout(watchRenewalTimer);
684
687
  watchRenewalTimer = null;
685
688
  }
689
+ const running = pollerJob;
690
+ pollerJob = null;
691
+ if (options.shuttingDown) {
692
+ await running?.stop();
693
+ return;
694
+ }
695
+ // Disabling doesn't wait on a slow poll; the stop ends its scheduling.
696
+ void running?.stop();
686
697
  try {
687
698
  const stopResult = await stopWatch();
688
699
  if (stopResult.status === "retry") {
@@ -14,4 +14,4 @@ export declare function retryStuckPendingTasks(input?: string | PendingTasksSwee
14
14
  export declare function startPendingTasksRetryJob(options?: {
15
15
  webhookBaseUrl?: string;
16
16
  }): void;
17
- export declare function stopPendingTasksRetryJob(): void;
17
+ export declare function stopPendingTasksRetryJob(): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { getDbExec } from "../db/client.js";
2
+ import { registerBackgroundJobStop } from "../server/background-jobs.js";
2
3
  import { startIntervalJob, } from "../server/interval-job.js";
3
4
  import { hasActiveIntegrationCampaign } from "./integration-campaigns-store.js";
4
5
  import { configuredIntegrationDurableDispatchScopes, dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
@@ -242,16 +243,19 @@ export function startPendingTasksRetryJob(options) {
242
243
  });
243
244
  }, 10_000);
244
245
  startupTimer.unref?.();
246
+ registerBackgroundJobStop(stopPendingTasksRetryJob);
245
247
  if (process.env.DEBUG) {
246
248
  console.log(`[integrations] Pending-tasks retry job started (every ${RETRY_INTERVAL_MS / 1000}s)`);
247
249
  }
248
250
  }
249
- export function stopPendingTasksRetryJob() {
251
+ export async function stopPendingTasksRetryJob() {
250
252
  if (startupTimer) {
251
253
  clearTimeout(startupTimer);
252
254
  startupTimer = null;
253
255
  }
254
- job?.stop();
256
+ const running = job;
255
257
  job = null;
256
258
  activeWebhookBaseUrl = undefined;
259
+ if (running)
260
+ await running.stop();
257
261
  }
@@ -8,6 +8,7 @@ import { getOrgContext } from "../org/context.js";
8
8
  import { loadResourcesForPrompt } from "../server/agent-chat-plugin.js";
9
9
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
10
10
  import { getSession } from "../server/auth.js";
11
+ import { registerBackgroundJobStop } from "../server/background-jobs.js";
11
12
  import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
12
13
  import { resolveSecret } from "../server/credential-provider.js";
13
14
  import { getH3App, markDefaultPluginProvided, } from "../server/framework-request-handler.js";
@@ -41,13 +42,13 @@ import { dispatchPendingIntegrationTask, INTEGRATION_CAMPAIGN_PROCESSOR_FIELD, I
41
42
  import { forgetIntegrationMemory, integrationMemoryActions, listIntegrationMemory, rememberForIntegrationScope, } from "./integration-memory.js";
42
43
  import { extractBearerToken, verifyInternalToken } from "./internal-token.js";
43
44
  import { setMountedChannels, } from "./mounted-channels.js";
44
- import { retryStuckPendingTasks, startPendingTasksRetryJob, } from "./pending-tasks-retry-job.js";
45
+ import { retryStuckPendingTasks, startPendingTasksRetryJob, stopPendingTasksRetryJob, } from "./pending-tasks-retry-job.js";
45
46
  import { claimPendingTask, getNextPendingTaskForThread, getPendingTask, insertPendingTask, isDuplicateEventError, MAX_PENDING_TASK_ATTEMPTS, MAX_RECOVERABLE_PENDING_TASK_AGE_MS, markTaskCompleted, markTaskDeliveryRetryable, markTaskFailed, markTaskRetryable, stageTaskDeliveryPayload, } from "./pending-tasks-store.js";
46
47
  import { claimNextComputerCommand, claimNextRemoteCommand, enqueueComputerCommand, enqueueRemoteCommand as enqueueRemoteCommandRow, isRemoteCommandKind, listRemoteCommandsForOwner, updateRemoteCommandResult, } from "./remote-commands-store.js";
47
48
  import { authenticateRemoteDeviceToken, createRemoteDevice, getRemoteComputerCapabilities, getRemoteExecutionCapabilities, getRemoteDeviceForOwner, listRemoteDevicesForOwner, revokeRemoteDeviceForOwner, toPublicRemoteDevice, unregisterRemoteDevice, updateRemoteDeviceDetails, } from "./remote-devices-store.js";
48
- import { startRemotePushDeliveryJob } from "./remote-push-delivery-job.js";
49
+ import { startRemotePushDeliveryJob, stopRemotePushDeliveryJob, } from "./remote-push-delivery-job.js";
49
50
  import { listRemotePushNotificationsForOwner, listRemotePushRegistrationsForOwner, queueRemotePushNotifications, toPublicRemotePushRegistration, unregisterRemotePushRegistrationForOwner, upsertRemotePushRegistration, } from "./remote-push-store.js";
50
- import { startRemoteCommandsRetryJob } from "./remote-retry-job.js";
51
+ import { startRemoteCommandsRetryJob, stopRemoteCommandsRetryJob, } from "./remote-retry-job.js";
51
52
  import { insertRemoteRunEvents, listRemoteRunEvents, } from "./remote-run-events-store.js";
52
53
  import { integrationRequestContext, resolveIntegrationRequestScope, } from "./request-scope.js";
53
54
  import { listIntegrationScopes, saveIntegrationScope } from "./scope-store.js";
@@ -176,6 +177,17 @@ function startA2AContinuationRetryJob(adapters) {
176
177
  });
177
178
  }, 10_000);
178
179
  a2aContinuationStartupTimer.unref?.();
180
+ registerBackgroundJobStop(stopA2AContinuationRetryJob);
181
+ }
182
+ async function stopA2AContinuationRetryJob() {
183
+ if (a2aContinuationStartupTimer) {
184
+ clearTimeout(a2aContinuationStartupTimer);
185
+ a2aContinuationStartupTimer = null;
186
+ }
187
+ const running = a2aContinuationJob;
188
+ a2aContinuationJob = null;
189
+ if (running)
190
+ await running.stop();
179
191
  }
180
192
  export const BUILT_IN_INTEGRATION_ADAPTER_FACTORIES = Object.freeze([
181
193
  { platform: "slack", create: slackAdapter },
@@ -2749,8 +2761,10 @@ export function createIntegrationsPlugin(options) {
2749
2761
  startA2AContinuationRetryJob(adapterMap);
2750
2762
  startRemoteCommandsRetryJob();
2751
2763
  startRemotePushDeliveryJob();
2764
+ let googleDocsStartTimer = null;
2752
2765
  if (adapterMap.has("google-docs")) {
2753
- setTimeout(() => {
2766
+ googleDocsStartTimer = setTimeout(() => {
2767
+ googleDocsStartTimer = null;
2754
2768
  void runGoogleDocsPollerTransition(async () => {
2755
2769
  const config = await getIntegrationConfig("google-docs");
2756
2770
  const ownerEmail = config?.owner?.trim();
@@ -2763,7 +2777,24 @@ export function createIntegrationsPlugin(options) {
2763
2777
  await startGoogleDocsPoller(createGoogleDocsPollerOptions(ownerEmail));
2764
2778
  });
2765
2779
  }, 2000);
2766
- }
2780
+ googleDocsStartTimer.unref?.();
2781
+ }
2782
+ const stopIntegrationBackgroundJobs = async () => {
2783
+ await Promise.all([
2784
+ stopPendingTasksRetryJob(),
2785
+ stopA2AContinuationRetryJob(),
2786
+ stopRemoteCommandsRetryJob(),
2787
+ stopRemotePushDeliveryJob(),
2788
+ ]);
2789
+ if (googleDocsStartTimer) {
2790
+ clearTimeout(googleDocsStartTimer);
2791
+ googleDocsStartTimer = null;
2792
+ }
2793
+ if (adapterMap.has("google-docs")) {
2794
+ await runGoogleDocsPollerTransition(() => stopGoogleDocsPoller({ shuttingDown: true }));
2795
+ }
2796
+ };
2797
+ registerBackgroundJobStop(stopIntegrationBackgroundJobs);
2767
2798
  }
2768
2799
  if (process.env.DEBUG)
2769
2800
  console.log(`[integrations] Mounted integration routes for: ${adapters.map((a) => a.platform).join(", ")}`);
@@ -1,3 +1,3 @@
1
1
  export declare function runRemotePushDelivery(): Promise<void>;
2
2
  export declare function startRemotePushDeliveryJob(): void;
3
- export declare function stopRemotePushDeliveryJob(): void;
3
+ export declare function stopRemotePushDeliveryJob(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { registerBackgroundJobStop } from "../server/background-jobs.js";
1
2
  import { startIntervalJob, } from "../server/interval-job.js";
2
3
  import { deliverPendingRemotePushNotifications } from "./remote-push-delivery.js";
3
4
  const DELIVERY_INTERVAL_MS = 60_000;
@@ -23,12 +24,15 @@ export function startRemotePushDeliveryJob() {
23
24
  });
24
25
  }, 10_000);
25
26
  startupTimer.unref?.();
27
+ registerBackgroundJobStop(stopRemotePushDeliveryJob);
26
28
  }
27
- export function stopRemotePushDeliveryJob() {
29
+ export async function stopRemotePushDeliveryJob() {
28
30
  if (startupTimer) {
29
31
  clearTimeout(startupTimer);
30
32
  startupTimer = null;
31
33
  }
32
- job?.stop();
34
+ const running = job;
33
35
  job = null;
36
+ if (running)
37
+ await running.stop();
34
38
  }
@@ -3,4 +3,4 @@ export declare function retryRemoteCommands(): Promise<{
3
3
  failed: number;
4
4
  }>;
5
5
  export declare function startRemoteCommandsRetryJob(): void;
6
- export declare function stopRemoteCommandsRetryJob(): void;
6
+ export declare function stopRemoteCommandsRetryJob(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { registerBackgroundJobStop } from "../server/background-jobs.js";
1
2
  import { startIntervalJob, } from "../server/interval-job.js";
2
3
  import { retryStaleRemoteCommands } from "./remote-commands-store.js";
3
4
  const RETRY_INTERVAL_MS = 60_000;
@@ -29,12 +30,15 @@ export function startRemoteCommandsRetryJob() {
29
30
  });
30
31
  }, 10_000);
31
32
  startupTimer.unref?.();
33
+ registerBackgroundJobStop(stopRemoteCommandsRetryJob);
32
34
  }
33
- export function stopRemoteCommandsRetryJob() {
35
+ export async function stopRemoteCommandsRetryJob() {
34
36
  if (startupTimer) {
35
37
  clearTimeout(startupTimer);
36
38
  startupTimer = null;
37
39
  }
38
- job?.stop();
40
+ const running = job;
39
41
  job = null;
42
+ if (running)
43
+ await running.stop();
40
44
  }
@@ -13,10 +13,10 @@ declare const _default: import("../../action.js").ActionDefinition<{
13
13
  enabled?: undefined;
14
14
  nextRun?: undefined;
15
15
  } | {
16
- deleted?: undefined;
17
16
  resourceId: string;
18
17
  name: string;
19
18
  enabled: boolean;
20
19
  nextRun: string;
20
+ deleted?: undefined;
21
21
  }>;
22
22
  export default _default;
@@ -1,8 +1,8 @@
1
1
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
2
2
  count: number;
3
- updated?: undefined;
4
3
  error?: undefined;
5
4
  ok?: undefined;
5
+ updated?: undefined;
6
6
  } | {
7
7
  count?: undefined;
8
8
  updated: number;
@@ -10,12 +10,12 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
10
10
  ok?: undefined;
11
11
  } | {
12
12
  count?: undefined;
13
- updated?: undefined;
14
13
  error: string;
15
14
  ok?: undefined;
15
+ updated?: undefined;
16
16
  } | {
17
17
  count?: undefined;
18
- updated?: undefined;
19
- ok: boolean;
20
18
  error?: undefined;
19
+ ok: boolean;
20
+ updated?: undefined;
21
21
  }>>;
@@ -27,43 +27,43 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
27
27
  thumbsUpRate: number;
28
28
  avgEvalScore: number;
29
29
  } | {
30
+ ok?: undefined;
30
31
  error: string;
31
32
  message: string;
32
33
  summary?: undefined;
33
34
  spans?: undefined;
34
- ok?: undefined;
35
35
  } | {
36
+ error?: undefined;
37
+ ok?: undefined;
36
38
  message?: undefined;
37
39
  summary: import("./types.js").TraceSummary;
38
40
  spans: import("./types.js").TraceSpan[];
41
+ } | {
39
42
  error?: undefined;
40
43
  ok?: undefined;
41
- } | {
42
44
  message?: undefined;
43
45
  summary?: undefined;
44
46
  spans?: undefined;
45
47
  traceMissing: true;
46
48
  id: string;
49
+ } | {
47
50
  error?: undefined;
48
51
  ok?: undefined;
49
- } | {
50
52
  message?: undefined;
51
53
  summary?: undefined;
52
54
  spans?: undefined;
53
55
  traceMissing?: undefined;
54
56
  id: string;
55
- error?: undefined;
56
- ok?: undefined;
57
57
  } | {
58
+ ok?: undefined;
58
59
  message?: undefined;
59
60
  summary?: undefined;
60
61
  spans?: undefined;
61
62
  error: any;
62
- ok?: undefined;
63
63
  } | {
64
+ error?: undefined;
64
65
  message?: undefined;
65
66
  summary?: undefined;
66
67
  spans?: undefined;
67
- error?: undefined;
68
68
  ok: boolean;
69
69
  }>>;