@agent-native/core 0.220.0-nightly-20261024033422 → 0.220.0

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 (38) hide show
  1. package/dist/a2a/handlers.js +7 -6
  2. package/dist/a2a/types.d.ts +1 -0
  3. package/dist/agent/engine/tool-call-journal-seed.js +24 -20
  4. package/dist/agent/execution-fence.d.ts +1 -1
  5. package/dist/agent/execution-fence.js +8 -1
  6. package/dist/agent/production-agent.js +31 -6
  7. package/dist/agent/reaped-run-thread.js +38 -3
  8. package/dist/agent/run-migrations.js +22 -0
  9. package/dist/agent/run-store.d.ts +19 -2
  10. package/dist/agent/run-store.fencing.worker.js +21 -1
  11. package/dist/agent/run-store.js +359 -35
  12. package/dist/agent/tool-approval-store.d.ts +13 -0
  13. package/dist/agent/tool-approval-store.js +64 -0
  14. package/dist/automation/index.d.ts +1 -1
  15. package/dist/client/background-agent-session.d.ts +4 -0
  16. package/dist/client/background-agent-session.js +17 -1
  17. package/dist/client/chat/runtime.js +38 -6
  18. package/dist/collab/routes.d.ts +2 -2
  19. package/dist/db/schema-snapshot.generated.js +14 -1
  20. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  21. package/dist/jobs/automation-recovery.d.ts +4 -0
  22. package/dist/jobs/automation-recovery.js +7 -0
  23. package/dist/jobs/background-automation-runner.js +19 -2
  24. package/dist/jobs/scheduler.js +3 -1
  25. package/dist/mcp/org-directory.d.ts +8 -7
  26. package/dist/mcp/org-directory.js +82 -1
  27. package/dist/notifications/routes.d.ts +1 -1
  28. package/dist/observability/routes.d.ts +7 -7
  29. package/dist/progress/routes.d.ts +1 -1
  30. package/dist/resources/handlers.d.ts +2 -2
  31. package/dist/secrets/routes.d.ts +10 -10
  32. package/dist/server/agent-chat-plugin.d.ts +10 -1
  33. package/dist/server/agent-chat-plugin.js +178 -11
  34. package/dist/server/agent-discovery.js +11 -49
  35. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  36. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  37. package/dist/usage/store.js +36 -33
  38. package/package.json +4 -4
@@ -87,6 +87,70 @@ export class AgentToolApprovalNotFoundError extends Error {
87
87
  this.name = "AgentToolApprovalNotFoundError";
88
88
  }
89
89
  }
90
+ export async function hasPendingAgentToolApproval(tx, input) {
91
+ const result = await tx.execute({
92
+ sql: `SELECT 1 FROM agent_tool_approvals
93
+ WHERE owner_email = ?
94
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = CAST(? AS TEXT))
95
+ AND thread_id = ?
96
+ AND status = 'pending'
97
+ AND expires_at > ?
98
+ LIMIT 1`,
99
+ args: [
100
+ input.ownerEmail,
101
+ input.orgId ?? null,
102
+ input.orgId ?? null,
103
+ input.threadId,
104
+ Date.now(),
105
+ ],
106
+ });
107
+ return result.rows.length > 0;
108
+ }
109
+ /** Denial is a durable resolution so queued turns can resume after a pause. */
110
+ export async function denyAgentToolApproval(input) {
111
+ await ensureAgentToolApprovalTable();
112
+ const now = Date.now();
113
+ const updated = await withFencedExecutionWrite((tx) => tx.execute({
114
+ sql: `UPDATE agent_tool_approvals
115
+ SET status = 'denied', updated_at = ?
116
+ WHERE id = ?
117
+ AND owner_email = ?
118
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = CAST(? AS TEXT))
119
+ AND thread_id = ?
120
+ AND status = 'pending'`,
121
+ args: [
122
+ now,
123
+ input.approvalId,
124
+ input.ownerEmail,
125
+ input.orgId ?? null,
126
+ input.orgId ?? null,
127
+ input.threadId,
128
+ ],
129
+ }), { threadId: input.threadId });
130
+ if (updated.rowsAffected === 1)
131
+ return "denied";
132
+ const existing = await getDbExec().execute({
133
+ sql: `SELECT status FROM agent_tool_approvals
134
+ WHERE id = ?
135
+ AND owner_email = ?
136
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = CAST(? AS TEXT))
137
+ AND thread_id = ?
138
+ LIMIT 1`,
139
+ args: [
140
+ input.approvalId,
141
+ input.ownerEmail,
142
+ input.orgId ?? null,
143
+ input.orgId ?? null,
144
+ input.threadId,
145
+ ],
146
+ });
147
+ const status = existing.rows[0]?.status;
148
+ if (status === "denied")
149
+ return "denied";
150
+ if (status && status !== "pending")
151
+ return "already_resolved";
152
+ return "not_applied";
153
+ }
90
154
  /**
91
155
  * Saves "always allow" for one tool name. With `approvalId`, the grant takes
92
156
  * the revision of that approval row, the card the user clicked, so it never
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
+ error?: undefined;
142
143
  accepted: boolean;
143
144
  duplicate: boolean;
144
145
  eventId: string;
145
- error?: undefined;
146
146
  }>;
@@ -42,3 +42,7 @@ export declare function cancelBackgroundAgentSession(options: {
42
42
  turnId: string;
43
43
  reason?: string;
44
44
  }): Promise<void>;
45
+ export type AgentRunThreadProjectionRetryState = "already_projected" | "projected" | "deferred";
46
+ export declare function retryAgentRunThreadProjection(runId: string): Promise<{
47
+ state: AgentRunThreadProjectionRetryState;
48
+ }>;
@@ -23,13 +23,16 @@ class BackgroundAgentSessionHttpError extends Error {
23
23
  }
24
24
  }
25
25
  async function responseError(response) {
26
+ return responseErrorFor(response, "Background agent session");
27
+ }
28
+ async function responseErrorFor(response, requestName) {
26
29
  const body = await response
27
30
  .json()
28
31
  .catch(() => null);
29
32
  const detail = body && typeof body.error === "string" && body.error.trim()
30
33
  ? `: ${body.error.trim()}`
31
34
  : "";
32
- return new BackgroundAgentSessionHttpError(response.status, `Background agent session was rejected (HTTP ${response.status})${detail}`);
35
+ return new BackgroundAgentSessionHttpError(response.status, `${requestName} was rejected (HTTP ${response.status})${detail}`);
33
36
  }
34
37
  async function drainResponse(response) {
35
38
  const reader = response.body?.getReader();
@@ -241,3 +244,16 @@ export async function cancelBackgroundAgentSession(options) {
241
244
  throw await responseError(turnResponse);
242
245
  }
243
246
  }
247
+ export async function retryAgentRunThreadProjection(runId) {
248
+ const response = await fetch(agentNativePath(`/_agent-native/agent-chat/runs/${encodeURIComponent(runId)}/retry-projection`), { method: "POST", credentials: "same-origin" });
249
+ if (!response.ok) {
250
+ throw await responseErrorFor(response, "Run projection retry");
251
+ }
252
+ const body = (await response.json());
253
+ if (body.state !== "already_projected" &&
254
+ body.state !== "projected" &&
255
+ body.state !== "deferred") {
256
+ throw new Error("Run projection retry returned an invalid response");
257
+ }
258
+ return { state: body.state };
259
+ }
@@ -3474,12 +3474,44 @@ export function createAgentNativeChatRuntime(options = {}, telemetry = {}) {
3474
3474
  });
3475
3475
  }
3476
3476
  if (!approval.approved) {
3477
- messageStates.delete(messageStateKey);
3478
- return terminalRuntimeTurn({
3479
- sessionId: session.id,
3480
- turnId: continuation.turnId,
3481
- reason: "complete",
3482
- });
3477
+ return (async () => {
3478
+ const threadId = session.threadId ?? options.threadId ?? session.id;
3479
+ const headers = await resolveHeaders(options.headers, {
3480
+ sessionId: session.id,
3481
+ turnId: continuation.turnId,
3482
+ });
3483
+ if (!headers.has("Content-Type")) {
3484
+ headers.set("Content-Type", "application/json");
3485
+ }
3486
+ const response = await fetchImpl(`${apiUrl.replace(/\/+$/, "")}/threads/${encodeURIComponent(threadId)}/approval-resolution`, {
3487
+ method: "POST",
3488
+ headers,
3489
+ credentials: "same-origin",
3490
+ body: JSON.stringify({
3491
+ approvalId: approval.id,
3492
+ decision: "deny",
3493
+ }),
3494
+ });
3495
+ let denialResolved = response.ok;
3496
+ if (!denialResolved && response.status === 409) {
3497
+ try {
3498
+ const body = asRecord(await response.clone().json());
3499
+ denialResolved = body?.code === "approval_not_pending";
3500
+ }
3501
+ catch {
3502
+ // coercion-ok: Preserve the original HTTP failure when conflict details are unreadable.
3503
+ }
3504
+ }
3505
+ if (!denialResolved) {
3506
+ throw new Error(await readErrorText(response));
3507
+ }
3508
+ messageStates.delete(messageStateKey);
3509
+ return terminalRuntimeTurn({
3510
+ sessionId: session.id,
3511
+ turnId: continuation.turnId,
3512
+ reason: "complete",
3513
+ });
3514
+ })();
3483
3515
  }
3484
3516
  const messageState = messageStates.get(messageStateKey);
3485
3517
  const approvalParts = [
@@ -13,12 +13,12 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
13
13
  };
14
14
  }>>;
15
15
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
+ error?: undefined;
19
20
  liveCopy: "derived" | "not_derived" | "unchanged";
20
21
  liveCopyReason?: string;
21
- error?: undefined;
22
22
  ok: boolean;
23
23
  } | {
24
24
  error?: undefined;
@@ -1,6 +1,7 @@
1
1
  export const SCHEMA_SNAPSHOT = {
2
2
  statements: [
3
3
  'CREATE SEQUENCE IF NOT EXISTS "agent_chat_submission_inbox_queue_order_seq";',
4
+ 'CREATE SEQUENCE IF NOT EXISTS "agent_run_projection_retry_order_seq";',
4
5
  'CREATE SEQUENCE IF NOT EXISTS "app_resource_change_seq";',
5
6
  'CREATE SEQUENCE IF NOT EXISTS "automation_trigger_event_queue_sequence_id_seq";',
6
7
  'CREATE TABLE "_better_auth_migrations" (\n "version" bigint NOT NULL\n);',
@@ -50,7 +51,7 @@ export const SCHEMA_SNAPSHOT = {
50
51
  'CREATE TABLE "agent_review_thread_preferences" (\n "thread_id" text NOT NULL,\n "user_email" text NOT NULL,\n "muted" integer DEFAULT 0 NOT NULL,\n "unread" integer DEFAULT 0 NOT NULL,\n "updated_at" text NOT NULL\n);',
51
52
  'CREATE TABLE "agent_run_events" (\n "run_id" text NOT NULL,\n "seq" bigint NOT NULL,\n "event_at" bigint,\n "event_data" text NOT NULL\n);',
52
53
  'CREATE TABLE "agent_run_outcome_daily" (\n "day" text NOT NULL,\n "status" text NOT NULL,\n "terminal_reason" text DEFAULT \'\'::text NOT NULL,\n "run_count" bigint DEFAULT 0 NOT NULL\n);',
53
- 'CREATE TABLE "agent_runs" (\n "id" text NOT NULL,\n "thread_id" text NOT NULL,\n "status" text DEFAULT \'running\'::text NOT NULL,\n "abort_reason" text,\n "started_at" bigint NOT NULL,\n "completed_at" bigint,\n "heartbeat_at" bigint,\n "last_progress_at" bigint,\n "turn_id" text,\n "error_code" text,\n "error_detail" text,\n "terminal_reason" text,\n "dispatch_mode" text,\n "diag_stage" text,\n "dispatch_payload" text,\n "peak_rss_mb" bigint,\n "continuation_order" bigint,\n "thread_projection_pending" boolean DEFAULT false NOT NULL,\n "worker_stage" text,\n "in_flight_since" bigint,\n "auto_continue_of" text,\n "worker_generation" bigint\n);',
54
+ 'CREATE TABLE "agent_runs" (\n "id" text NOT NULL,\n "thread_id" text NOT NULL,\n "status" text DEFAULT \'running\'::text NOT NULL,\n "abort_reason" text,\n "started_at" bigint NOT NULL,\n "completed_at" bigint,\n "heartbeat_at" bigint,\n "last_progress_at" bigint,\n "turn_id" text,\n "error_code" text,\n "error_detail" text,\n "terminal_reason" text,\n "dispatch_mode" text,\n "diag_stage" text,\n "dispatch_payload" text,\n "peak_rss_mb" bigint,\n "continuation_order" bigint,\n "thread_projection_pending" boolean DEFAULT false NOT NULL,\n "thread_projection_retry_order" bigint,\n "thread_projection_error_code" text,\n "worker_stage" text,\n "in_flight_since" bigint,\n "auto_continue_of" text,\n "worker_generation" bigint\n);',
54
55
  'CREATE TABLE "agent_satisfaction_scores" (\n "id" text NOT NULL,\n "thread_id" text NOT NULL,\n "user_id" text,\n "frustration_score" real DEFAULT 0 NOT NULL,\n "rephrasing_score" real DEFAULT 0 NOT NULL,\n "abandonment_score" real DEFAULT 0 NOT NULL,\n "sentiment_score" real DEFAULT 0 NOT NULL,\n "length_trend_score" real DEFAULT 0 NOT NULL,\n "computed_at" bigint NOT NULL\n);',
55
56
  'CREATE TABLE "agent_team_run_queue" (\n "task_id" text NOT NULL,\n "thread_id" text NOT NULL,\n "run_id" text NOT NULL,\n "status" text DEFAULT \'queued\'::text NOT NULL,\n "owner_email" text,\n "org_id" text,\n "payload" text NOT NULL,\n "continuation_count" bigint DEFAULT 0 NOT NULL,\n "attempts" bigint DEFAULT 0 NOT NULL,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL,\n "reconciliation_attempted_at" bigint\n);',
56
57
  'CREATE TABLE "agent_tool_approval_policies" (\n "id" text NOT NULL,\n "owner_email" text NOT NULL,\n "org_id" text,\n "tool_name" text NOT NULL,\n "enabled" boolean NOT NULL,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL,\n "tool_revision" text\n);',
@@ -432,6 +433,7 @@ export const SCHEMA_SNAPSHOT = {
432
433
  "CREATE INDEX idx_review_suggestion_proposal_members ON public.agent_review_suggestions USING btree (proposal_id, created_at);",
433
434
  "CREATE INDEX idx_review_suggestions_resource ON public.agent_review_suggestions USING btree (resource_type, resource_id, created_at);",
434
435
  "CREATE INDEX agent_runs_thread_projection_pending_idx ON public.agent_runs USING btree (completed_at, id) WHERE (thread_projection_pending IS TRUE);",
436
+ "CREATE INDEX agent_runs_thread_projection_retry_order_idx ON public.agent_runs USING btree (thread_projection_retry_order NULLS FIRST, completed_at, id) WHERE (thread_projection_pending IS TRUE);",
435
437
  "CREATE INDEX idx_satisfaction_thread ON public.agent_satisfaction_scores USING btree (thread_id);",
436
438
  "CREATE INDEX idx_satisfaction_user ON public.agent_satisfaction_scores USING btree (user_id, computed_at);",
437
439
  "CREATE INDEX idx_agent_team_run_queue_reconciliation ON public.agent_team_run_queue USING btree (status, reconciliation_attempted_at, updated_at) WHERE (status = ANY (ARRAY['queued'::text, 'running'::text]));",
@@ -833,12 +835,23 @@ export const SCHEMA_SNAPSHOT = {
833
835
  "agent-run-liveness-and-diagnostics-columns",
834
836
  "2a9d24ed6ffbea78",
835
837
  ],
838
+ [
839
+ "agent_runs",
840
+ "agent-run-thread-projection-quarantine",
841
+ "8eb065e2325e20ea",
842
+ ],
836
843
  ["agent_runs", "agent-run-thread-projection-retry", "d564128e21e3b424"],
844
+ [
845
+ "agent_runs",
846
+ "agent-run-thread-projection-retry-fairness",
847
+ "36150eae08f95f3d",
848
+ ],
837
849
  ["agent_runs", "agent-tool-ledger-arguments-hash", "495946626eda9dc4"],
838
850
  ["agent_runs", "agent-tool-ledger-chat-ui-result", "288137f1126005f6"],
839
851
  ["agent_runs", "agent-tool-ledger-result-type", "d4fe90219a833db8"],
840
852
  ["agent_runs", "agent-turn-execution-leases-and-steps", "bc8cfdc3fc695e2a"],
841
853
  ["agent_runs", "baseline", null],
854
+ ["agent_runs", "thread-projection-retry-quarantine-runtime-schema", null],
842
855
  ["agent_runs", "widen-bigint", null],
843
856
  ["agent_team_run_queue", "baseline", null],
844
857
  [
@@ -42,8 +42,8 @@ declare const _default: import("../../action.js").ActionDefinition<{
42
42
  url: string;
43
43
  id: string;
44
44
  provider: string;
45
- configured?: undefined;
46
45
  error?: undefined;
46
+ configured?: undefined;
47
47
  } | {
48
48
  url?: undefined;
49
49
  id?: undefined;
@@ -16,6 +16,10 @@ export declare class AutomationRecoveryStorageError extends Error {
16
16
  readonly errorCode = "automation_recovery_storage_unavailable";
17
17
  constructor(cause: unknown);
18
18
  }
19
+ export declare class AutomationRecoveryProjectionDeferredError extends Error {
20
+ readonly errorCode = "automation_recovery_projection_deferred";
21
+ constructor(runId: string);
22
+ }
19
23
  export declare function withAutomationRecoveryStorage<T>(operation: () => Promise<T>): Promise<T>;
20
24
  export declare function readAutomationRecoveryEvents(threadId: string, turnId: string): Promise<CurrentTurnRunEvent[]>;
21
25
  export declare function readAutomationRunDeliveryNote(runId: string): Promise<string>;
@@ -16,6 +16,13 @@ export class AutomationRecoveryStorageError extends Error {
16
16
  this.name = "AutomationRecoveryStorageError";
17
17
  }
18
18
  }
19
+ export class AutomationRecoveryProjectionDeferredError extends Error {
20
+ errorCode = "automation_recovery_projection_deferred";
21
+ constructor(runId) {
22
+ super(`Automation recovery is waiting for run ${runId} to project its thread`);
23
+ this.name = "AutomationRecoveryProjectionDeferredError";
24
+ }
25
+ }
19
26
  export async function withAutomationRecoveryStorage(operation) {
20
27
  try {
21
28
  return await operation();
@@ -8,7 +8,7 @@ import { actionsToEngineTools, filterInitialEngineTools, resolveOwnerEngineApiKe
8
8
  import { foldReapedRunIntoThread } from "../agent/reaped-run-thread.js";
9
9
  import { AgentLoopAbortedError, runAgentLoopDirectWithSoftTimeout, } from "../agent/run-loop-with-resume.js";
10
10
  import { abortRun, resolveBackgroundAutomationSoftTimeoutMs, resolveBackgroundRunHardTimeoutMs, startRun, } from "../agent/run-manager.js";
11
- import { claimBackgroundRun, beginAgentExecutionStep, completeAgentExecutionStep, markAgentExecutionStepDispatched, insertRun, tryClaimRunSlot, releaseBackgroundRunBeforeStart, } from "../agent/run-store.js";
11
+ import { claimBackgroundRun, beginAgentExecutionStep, completeAgentExecutionStep, markAgentExecutionStepDispatched, insertRun, tryClaimRunSlot, retryRunThreadProjection, releaseBackgroundRunBeforeStart, } from "../agent/run-store.js";
12
12
  import { buildAssistantMessageFromRunJournal } from "../agent/run-thread-projector.js";
13
13
  import { buildCurrentTimeUserContext, buildRuntimeContextPrompt, } from "../agent/runtime-context.js";
14
14
  import { buildAssistantMessage, buildUserMessage, extractThreadMeta, foldAssistantTurn, upsertUserMessage, threadDataToEngineMessages, threadMessageTextForEngine, } from "../agent/thread-data-builder.js";
@@ -28,7 +28,7 @@ import { runWithRequestContext, } from "../server/request-context.js";
28
28
  import { normalizeReasoningEffortForRequest } from "../shared/reasoning-effort.js";
29
29
  import automationNoOpAction, { AUTOMATION_NO_OP_TOOL, automationNoOpSchema, } from "./actions/automation-no-op.js";
30
30
  import { applyAutomationFailure, automationOwnerKind, classifyAutomationFailure, CONFIG_INVALID_ERROR_CODE, CONNECTION_REQUIRED_ERROR_CODE, MISSING_TOOLS_ERROR_CODE, OWNER_MISSING_ERROR_CODE, pausedMessage, withDeliveryNote, } from "./automation-outcome.js";
31
- import { AutomationRecoveryStorageError, deliveryNoteForEvents, readAutomationRecoveryEvents, readAutomationRunDeliveryNote, withAutomationRecoveryStorage, } from "./automation-recovery.js";
31
+ import { AutomationRecoveryProjectionDeferredError, AutomationRecoveryStorageError, deliveryNoteForEvents, readAutomationRecoveryEvents, readAutomationRunDeliveryNote, withAutomationRecoveryStorage, } from "./automation-recovery.js";
32
32
  import { automationRunTerminalError, } from "./automation-terminal-error.js";
33
33
  import { inspectAutomationWork } from "./automation-work-evidence.js";
34
34
  import { effectiveTimezone } from "./cron.js";
@@ -338,6 +338,7 @@ export async function runBackgroundAutomation(options, deps) {
338
338
  catch (err) {
339
339
  if (err instanceof StaleExecutionFenceError ||
340
340
  err instanceof AutomationSchedulerLeaseLostError ||
341
+ err instanceof AutomationRecoveryProjectionDeferredError ||
341
342
  err instanceof AutomationRecoveryStorageError ||
342
343
  (err instanceof BackgroundAutomationRunError &&
343
344
  (err.errorCode === "background_automation_claim_lost" ||
@@ -460,6 +461,8 @@ export async function runJobRunner(options) {
460
461
  }
461
462
  catch (err) {
462
463
  if (err instanceof AutomationSchedulerLeaseLostError ||
464
+ err instanceof AutomationRecoveryProjectionDeferredError ||
465
+ err instanceof AutomationRecoveryStorageError ||
463
466
  (err instanceof BackgroundAutomationRunError &&
464
467
  err.errorCode === "background_automation_history_write_failed"))
465
468
  throw err;
@@ -859,6 +862,20 @@ async function executeBackgroundAutomation(options, deps, historyId, runIdRef) {
859
862
  const systemPrompt = `${baseSystemPrompt}${workspaceAppContext ? `\n\n${workspaceAppContext}` : ""}\n\n${messages.noOpInstruction}`;
860
863
  assertHardDeadline(options.hardDeadlineAt);
861
864
  const resume = options.resume;
865
+ if (resume) {
866
+ const projection = await withAutomationRecoveryStorage(() => retryRunThreadProjection(resume.previousRunId));
867
+ switch (projection.state) {
868
+ case "deferred":
869
+ throw new AutomationRecoveryProjectionDeferredError(resume.previousRunId);
870
+ case "untracked":
871
+ // Inspection found this run before retention pruned its cleared
872
+ // projection marker; a pending marker prevents that pruning.
873
+ break;
874
+ case "projected":
875
+ case "already_projected":
876
+ break;
877
+ }
878
+ }
862
879
  const thread = resume
863
880
  ? await withAutomationRecoveryStorage(() => getThread(resume.threadId))
864
881
  : await createThread(ownerEmail, {
@@ -4,7 +4,7 @@ import { withDbExec } from "../db/client.js";
4
4
  import { ensureTable as ensureResourcesTable, resourceGetByPath, resourceListAllOwners, resourcePutIfCurrent, resourcePutIfCurrentInTransaction, } from "../resources/store.js";
5
5
  import { countAutomationCredentialState, trackAutomationPaused, trackAutomationResumed, } from "./automation-events.js";
6
6
  import { applyAutomationFailure, classifyAutomationFailure, CLEAR_FAILURE_STATE, CONFIG_INVALID_ERROR_CODE, hasStalePause, isMissingCredentialCode, isPausedByFramework, isReservedIdentityBlocked, isTransientPauseProbeDue, OWNER_MISSING_ERROR_CODE, OWNER_RESERVED_ERROR_CODE, pauseNow, RESUME_AUTOMATION_PATCH, reservedIdentityMessage, runtimeFailureNextRun, TRANSIENT_PROBE_RESUME_PATCH, withDeliveryNote, } from "./automation-outcome.js";
7
- import { AutomationRecoveryStorageError, deliveryNoteForEvents, inspectAutomationRecovery, } from "./automation-recovery.js";
7
+ import { AutomationRecoveryProjectionDeferredError, AutomationRecoveryStorageError, deliveryNoteForEvents, inspectAutomationRecovery, } from "./automation-recovery.js";
8
8
  import { backgroundRunCutOffReason, checkBackgroundAutomationCredentials, isBackgroundAutomationRunActive, resolveBackgroundAutomationIdentity, runBackgroundAutomation, runJobRunner, startBackgroundAutomationHistory, } from "./background-automation-runner.js";
9
9
  import { nextOccurrence, isValidCron, describeCron, effectiveTimezone, } from "./cron.js";
10
10
  import { buildJobResourceContent, isRecoveredFactoryJob, jobBelongsToApp, parseJobResource, patchJobFrontmatterFields, recoveredFactoryOwnerOrgId, } from "./frontmatter.js";
@@ -993,6 +993,8 @@ async function executeJob(resource, meta, body, deps, now, options = {}) {
993
993
  const failure = classifyAutomationFailure(err);
994
994
  if (err instanceof AutomationSchedulerLeaseLostError)
995
995
  throw err;
996
+ if (err instanceof AutomationRecoveryProjectionDeferredError)
997
+ throw err;
996
998
  if (err instanceof AutomationRecoveryStorageError)
997
999
  throw err;
998
1000
  if (failure.code === "background_automation_history_write_failed")
@@ -21,17 +21,18 @@
21
21
  *
22
22
  * ## Resolution + safety model
23
23
  *
24
- * - The directory origin is read from env: `AGENT_NATIVE_ORG_DIRECTORY_URL`
25
- * (dedicated) or `AGENT_NATIVE_IDENTITY_HUB_URL` (Dispatch is also the
26
- * identity hub). When *neither* is set the feature is simply inactive —
27
- * `fetchOrgApps()` returns `[]` and nothing changes anywhere (asserted by
28
- * a test). This makes the whole feature opt-in and back-compat.
24
+ * - A selected org's bound workspace URL wins for org-scoped requests; the
25
+ * deployment's `AGENT_NATIVE_ORG_DIRECTORY_URL` or
26
+ * `AGENT_NATIVE_IDENTITY_HUB_URL` is the fallback when no binding exists.
27
+ * When neither is set the feature is inactive — `fetchOrgApps()` returns
28
+ * `[]` and nothing changes anywhere (asserted by a test).
29
29
  * - `fetchOrgAppsResult()` preserves directory availability for callers
30
30
  * that cannot mistake a failed lookup for an empty directory.
31
31
  * `fetchOrgApps()` remains the best-effort compatibility wrapper: on ANY
32
32
  * error it returns `[]` and NEVER throws, so cross-app verbs still degrade
33
33
  * silently to their exact current local-only behavior.
34
- * - Credentialed requests require HTTPS and use manual redirects. A short
34
+ * - Mounted `/dispatch` requests retry at the host root only after a 404.
35
+ * Credentialed requests require HTTPS and use manual redirects. A short
35
36
  * in-memory TTL cache (default 60s) is keyed by directory origin and
36
37
  * caller identity/org scope so sibling app lists never cross tenants.
37
38
  * Successful responses are cached for 60s; transient unavailable results
@@ -51,7 +52,7 @@ export interface OrgApp {
51
52
  whenToUse?: string[];
52
53
  aliases?: string[];
53
54
  }
54
- export type OrgDirectoryUnavailableReason = "not-configured" | "authentication" | "unauthorized" | "authorization" | "invalid-origin" | "missing-route" | "timeout" | "network" | "invalid-response" | "ssrf-blocked" | "server-error" | "http-error";
55
+ export type OrgDirectoryUnavailableReason = "not-configured" | "authentication" | "unauthorized" | "authorization" | "invalid-origin" | "missing-route" | "timeout" | "network" | "invalid-response" | "ssrf-blocked" | "workspace-url-unavailable" | "server-error" | "http-error";
55
56
  export type OrgDirectoryFetchResult = {
56
57
  status: "available";
57
58
  apps: OrgApp[];
@@ -127,11 +127,92 @@ function serviceScopedCacheKey(origin, orgId, includeDirectoryApp = false, stric
127
127
  ].join("|");
128
128
  }
129
129
  async function fetchOrgAppsResultInternal(opts, strictValidation = true) {
130
+ const env = opts?.env ?? process.env;
131
+ const directoryBase = await resolveDirectoryBaseUrl(opts, env);
132
+ if (!directoryBase.ok)
133
+ return directoryBase.result;
134
+ const result = await fetchOrgAppsAtBaseUrl({ ...opts, directoryBaseUrl: directoryBase.url }, strictValidation);
135
+ if (result.status !== "unavailable" || result.reason !== "missing-route") {
136
+ return result;
137
+ }
138
+ const rootUrl = new URL(directoryBase.url).origin;
139
+ if (rootUrl === directoryBase.url.replace(/\/+$/, ""))
140
+ return result;
141
+ return fetchOrgAppsAtBaseUrl({ ...opts, directoryBaseUrl: rootUrl }, strictValidation);
142
+ }
143
+ async function resolveDirectoryBaseUrl(opts, env) {
144
+ if (opts?.directoryBaseUrl !== undefined) {
145
+ const url = normalizeDirectoryBaseUrl(opts.directoryBaseUrl);
146
+ return url
147
+ ? { ok: true, url }
148
+ : {
149
+ ok: false,
150
+ result: { status: "unavailable", reason: "invalid-origin" },
151
+ };
152
+ }
153
+ const serviceOrgId = opts?.serviceOrgId?.trim();
154
+ let orgId = serviceOrgId || undefined;
155
+ try {
156
+ if (!orgId) {
157
+ const { getRequestOrgId } = await import("../server/request-context.js");
158
+ orgId = getRequestOrgId();
159
+ }
160
+ if (orgId) {
161
+ const { getOrgWorkspaceUrl } = await import("../org/context.js");
162
+ const workspaceUrl = await getOrgWorkspaceUrl(orgId);
163
+ if (workspaceUrl) {
164
+ const url = normalizeOrgWorkspaceDirectoryBaseUrl(workspaceUrl);
165
+ return url
166
+ ? { ok: true, url }
167
+ : {
168
+ ok: false,
169
+ result: { status: "unavailable", reason: "invalid-origin" },
170
+ };
171
+ }
172
+ }
173
+ }
174
+ catch {
175
+ return {
176
+ ok: false,
177
+ result: { status: "unavailable", reason: "workspace-url-unavailable" },
178
+ };
179
+ }
180
+ const configuredUrl = configuredOrgDirectoryUrl(env);
181
+ const url = resolveOrgDirectoryOrigin(env);
182
+ if (url)
183
+ return { ok: true, url };
184
+ return {
185
+ ok: false,
186
+ result: {
187
+ status: "unavailable",
188
+ reason: configuredUrl ? "invalid-origin" : "not-configured",
189
+ },
190
+ };
191
+ }
192
+ function normalizeOrgWorkspaceDirectoryBaseUrl(value) {
193
+ try {
194
+ const url = new URL(value);
195
+ if (url.protocol !== "https:" ||
196
+ url.username ||
197
+ url.password ||
198
+ url.search ||
199
+ url.hash ||
200
+ (url.pathname !== "/" && url.pathname !== "")) {
201
+ return null;
202
+ }
203
+ return `${url.origin}/dispatch`;
204
+ }
205
+ catch {
206
+ // coercion-ok: Invalid bindings become typed invalid-origin failures above.
207
+ return null;
208
+ }
209
+ }
210
+ async function fetchOrgAppsAtBaseUrl(opts, strictValidation) {
130
211
  const env = opts?.env ?? process.env;
131
212
  const hasExplicitBaseUrl = opts?.directoryBaseUrl !== undefined;
132
213
  const hasConfiguredEnvUrl = configuredOrgDirectoryUrl(env) !== null;
133
214
  const origin = hasExplicitBaseUrl
134
- ? normalizeDirectoryBaseUrl(opts?.directoryBaseUrl ?? "")
215
+ ? normalizeDirectoryBaseUrl(opts.directoryBaseUrl ?? "")
135
216
  : resolveOrgDirectoryOrigin(env);
136
217
  if (!origin) {
137
218
  return {
@@ -16,6 +16,6 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
16
16
  } | {
17
17
  count?: undefined;
18
18
  updated?: undefined;
19
- error?: undefined;
20
19
  ok: boolean;
20
+ error?: 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;
31
30
  error: string;
32
31
  message: string;
33
32
  summary?: undefined;
34
33
  spans?: undefined;
35
- } | {
36
- error?: undefined;
37
34
  ok?: undefined;
35
+ } | {
38
36
  message?: undefined;
39
37
  summary: import("./types.js").TraceSummary;
40
38
  spans: import("./types.js").TraceSpan[];
41
- } | {
42
39
  error?: undefined;
43
40
  ok?: undefined;
41
+ } | {
44
42
  message?: undefined;
45
43
  summary?: undefined;
46
44
  spans?: undefined;
47
45
  traceMissing: true;
48
46
  id: string;
49
- } | {
50
47
  error?: undefined;
51
48
  ok?: undefined;
49
+ } | {
52
50
  message?: undefined;
53
51
  summary?: undefined;
54
52
  spans?: undefined;
55
53
  traceMissing?: undefined;
56
54
  id: string;
57
- } | {
55
+ error?: undefined;
58
56
  ok?: undefined;
57
+ } | {
59
58
  message?: undefined;
60
59
  summary?: undefined;
61
60
  spans?: undefined;
62
61
  error: any;
62
+ ok?: undefined;
63
63
  } | {
64
- error?: undefined;
65
64
  message?: undefined;
66
65
  summary?: undefined;
67
66
  spans?: undefined;
67
+ error?: undefined;
68
68
  ok: boolean;
69
69
  }>>;
@@ -2,6 +2,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
2
2
  error: string;
3
3
  ok?: undefined;
4
4
  } | {
5
- error?: undefined;
6
5
  ok: boolean;
6
+ error?: undefined;
7
7
  }>>;
@@ -42,8 +42,8 @@ export declare function handleDeleteResource(event: any): Promise<{
42
42
  error: string;
43
43
  ok?: undefined;
44
44
  } | {
45
- ok: boolean;
46
45
  error?: undefined;
46
+ ok: boolean;
47
47
  }>;
48
48
  export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
49
49
  error: string;
@@ -63,10 +63,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
63
63
  runId: string | null;
64
64
  expiresAt: number | null;
65
65
  metadata: string | null;
66
+ error?: undefined;
66
67
  url: string;
67
68
  provider: string;
68
69
  storageSetupRequired?: undefined;
69
- error?: undefined;
70
70
  } | {
71
71
  error: string;
72
72
  storageSetupRequired: boolean;
@@ -57,39 +57,39 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
57
57
  } | {
58
58
  error: string;
59
59
  errorCode?: undefined;
60
- status?: undefined;
61
60
  ok?: undefined;
61
+ status?: undefined;
62
62
  } | {
63
63
  error: string;
64
64
  errorCode: string;
65
- status?: undefined;
66
65
  ok?: undefined;
66
+ status?: undefined;
67
67
  } | {
68
+ error?: undefined;
68
69
  errorCode?: undefined;
69
70
  ok: boolean;
70
71
  status: string;
71
- error?: undefined;
72
72
  } | {
73
+ ok?: undefined;
73
74
  error: string;
74
75
  removed?: undefined;
75
- ok?: undefined;
76
76
  } | {
77
+ error?: undefined;
77
78
  ok: boolean;
78
79
  removed: boolean;
79
- error?: undefined;
80
80
  }>>;
81
81
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
82
+ ok?: undefined;
82
83
  error: string;
83
84
  note?: undefined;
84
- ok?: undefined;
85
85
  } | {
86
+ error?: undefined;
86
87
  ok: boolean;
87
88
  note?: undefined;
88
- error?: undefined;
89
89
  } | {
90
+ error?: undefined;
90
91
  ok: boolean;
91
92
  note: string;
92
- error?: undefined;
93
93
  } | {
94
94
  note?: undefined;
95
95
  ok: boolean;
@@ -117,11 +117,11 @@ export declare function createAdHocSecretHandler(): import("h3").EventHandlerWit
117
117
  } | {
118
118
  error: string;
119
119
  } | {
120
+ error?: undefined;
120
121
  ok: boolean;
121
122
  key: string;
122
- error?: undefined;
123
123
  } | {
124
+ error?: undefined;
124
125
  ok: boolean;
125
126
  removed: boolean;
126
- error?: undefined;
127
127
  }>>;