@opengeni/worker-bundle 1.0.1 → 2.0.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.
- package/dist/activities/agent-turn/codex.d.ts +1 -1
- package/dist/activities/agent-turn/compaction-prep.d.ts +1 -5
- package/dist/activities/agent-turn/errors.d.ts +31 -5
- package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
- package/dist/activities/agent-turn/finalization.d.ts +1 -1
- package/dist/activities/agent-turn/governance-model.d.ts +0 -2
- package/dist/activities/agent-turn/quiescence.d.ts +1 -0
- package/dist/activities/agent-turn/recovery-warning.d.ts +8 -0
- package/dist/activities/agent-turn/run-credentials.d.ts +0 -1
- package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -1
- package/dist/activities/agent-turn/sandbox-route.d.ts +0 -20
- package/dist/activities/agent-turn/sandbox-runtime.d.ts +1 -1
- package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
- package/dist/activities/agent-turn/skill-search.d.ts +1 -1
- package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
- package/dist/activities/agent-turn/tool-environment.d.ts +0 -4
- package/dist/activities/agent-turn/turn-context.d.ts +2 -0
- package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
- package/dist/activities/codex-capacity.d.ts +6 -1
- package/dist/activities/codex-rotation.d.ts +1 -1
- package/dist/activities/context-compaction.d.ts +7 -1
- package/dist/activities/mcp-account-routes.d.ts +22 -0
- package/dist/activities/mcp-credentials.d.ts +12 -13
- package/dist/activities/mcp-operation-authority.d.ts +0 -5
- package/dist/activities/rig-verification.d.ts +2 -3
- package/dist/activities/sandbox-images.d.ts +1 -1
- package/dist/activities/sandbox-lease.d.ts +4 -3
- package/dist/activities/session-state.d.ts +57 -2
- package/dist/activities/types.d.ts +12 -3
- package/dist/{activities-control-HMFGSKC5.js → activities-control-BOLIY3H4.js} +366 -187
- package/dist/activities-control-BOLIY3H4.js.map +1 -0
- package/dist/activities-control.d.ts +110 -2
- package/dist/{activities-turn-AP67JADS.js → activities-turn-2B3WMHBB.js} +767 -471
- package/dist/activities-turn-2B3WMHBB.js.map +1 -0
- package/dist/activities.d.ts +110 -2
- package/dist/artifact-materializer-entry.js +5 -2
- package/dist/artifact-materializer-entry.js.map +1 -1
- package/dist/artifact-outbox-entry.js +2 -2
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/{chunk-A4XQWVZK.js → chunk-I7HKKMUJ.js} +77 -1
- package/dist/chunk-I7HKKMUJ.js.map +1 -0
- package/dist/{chunk-CYITU5UH.js → chunk-TVEE2UUC.js} +275 -146
- package/dist/chunk-TVEE2UUC.js.map +1 -0
- package/dist/editable-artifact-materializer-service.d.ts +2 -2
- package/dist/editable-artifact-outbox-service.d.ts +2 -2
- package/dist/index.d.ts +55 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +11 -3
- package/dist/retained-process-diagnostics.d.ts +4 -0
- package/dist/sandbox-resume.d.ts +2 -12
- package/dist/sandbox-routing.d.ts +5 -3
- package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
- package/dist/warm-capture-holder.d.ts +3 -0
- package/dist/workflow-bundle.js +51 -11
- package/package.json +15 -15
- package/src/activities/agent-turn/agent-build.ts +42 -5
- package/src/activities/agent-turn/claim.ts +12 -0
- package/src/activities/agent-turn/codex-capacity.ts +26 -4
- package/src/activities/agent-turn/codex.ts +6 -2
- package/src/activities/agent-turn/compaction-prep.ts +53 -72
- package/src/activities/agent-turn/errors.ts +98 -10
- package/src/activities/agent-turn/failure-settlement.ts +49 -28
- package/src/activities/agent-turn/finalization-monitor.ts +121 -0
- package/src/activities/agent-turn/finalization.ts +79 -21
- package/src/activities/agent-turn/governance-model.ts +15 -19
- package/src/activities/agent-turn/history.ts +24 -10
- package/src/activities/agent-turn/quiescence.ts +3 -0
- package/src/activities/agent-turn/recovery-warning.ts +18 -0
- package/src/activities/agent-turn/run-credentials.ts +3 -7
- package/src/activities/agent-turn/run.ts +68 -52
- package/src/activities/agent-turn/sandbox-establish.ts +10 -2
- package/src/activities/agent-turn/sandbox-route.ts +0 -20
- package/src/activities/agent-turn/sandbox-runtime.ts +40 -7
- package/src/activities/agent-turn/skill-remove.ts +46 -0
- package/src/activities/agent-turn/skill-save.ts +1 -1
- package/src/activities/agent-turn/skill-search.ts +1 -1
- package/src/activities/agent-turn/skill-selection.ts +2 -0
- package/src/activities/agent-turn/skill-tools.ts +10 -5
- package/src/activities/agent-turn/stream-attempt.ts +38 -15
- package/src/activities/agent-turn/tool-environment.ts +44 -29
- package/src/activities/agent-turn/turn-context.ts +3 -0
- package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
- package/src/activities/capabilities.ts +17 -2
- package/src/activities/codex-capacity.ts +38 -9
- package/src/activities/codex-rotation.ts +18 -5
- package/src/activities/context-compaction.ts +26 -4
- package/src/activities/goals.ts +1 -0
- package/src/activities/google-drive-publication.ts +1 -3
- package/src/activities/mcp-account-routes.ts +139 -0
- package/src/activities/mcp-credentials.ts +83 -194
- package/src/activities/mcp-operation-authority.ts +16 -41
- package/src/activities/parent-wake.ts +8 -0
- package/src/activities/rig-verification.ts +38 -37
- package/src/activities/run-input.ts +25 -16
- package/src/activities/sandbox-images.ts +0 -1
- package/src/activities/sandbox-lease.ts +177 -44
- package/src/activities/scheduled-tasks.ts +118 -93
- package/src/activities/session-state.ts +83 -3
- package/src/activities/types.ts +18 -2
- package/src/activities/workflow-wake.ts +2 -17
- package/src/editable-artifact-materializer-service.ts +7 -3
- package/src/editable-artifact-outbox-service.ts +4 -4
- package/src/github-rest-mcp.ts +0 -1
- package/src/index.ts +2 -1
- package/src/observability-metrics.ts +105 -2
- package/src/personal-github-git-credentials.ts +0 -1
- package/src/pr-review-credentials.ts +0 -2
- package/src/retained-process-diagnostics.ts +50 -0
- package/src/sandbox-resume.ts +58 -73
- package/src/sandbox-routing.ts +44 -19
- package/src/sandbox-snapshot-diagnostics.ts +112 -0
- package/src/warm-capture-holder.ts +15 -0
- package/src/workflows/session.ts +64 -2
- package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
- package/dist/activities/packs.d.ts +0 -55
- package/dist/activities-control-HMFGSKC5.js.map +0 -1
- package/dist/activities-turn-AP67JADS.js.map +0 -1
- package/dist/chunk-A4XQWVZK.js.map +0 -1
- package/dist/chunk-CYITU5UH.js.map +0 -1
- package/src/activities/agent-turn/knowledge-sources.ts +0 -86
- package/src/activities/packs.ts +0 -166
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type RuntimeMetricsHooks,
|
|
17
17
|
type SelfhostedOpObservation,
|
|
18
18
|
type SelfhostedOpObserver,
|
|
19
|
+
type ToolPreparationPhaseMeasurement,
|
|
19
20
|
} from "@opengeni/runtime";
|
|
20
21
|
|
|
21
22
|
export type TurnOutcome = "completed" | "failed" | "cancelled" | "recovering";
|
|
@@ -119,6 +120,10 @@ export function runtimeMetricsHooksForObservability(
|
|
|
119
120
|
): RuntimeMetricsHooks {
|
|
120
121
|
return {
|
|
121
122
|
onModelCall: ({ provider, outcome, durationSeconds }) => {
|
|
123
|
+
completedOperationSpan(observability, "worker.model.call", durationSeconds, {
|
|
124
|
+
provider,
|
|
125
|
+
outcome,
|
|
126
|
+
});
|
|
122
127
|
observability.incrementCounter({
|
|
123
128
|
name: "opengeni_model_calls_total",
|
|
124
129
|
help: "Total model calls by provider and outcome.",
|
|
@@ -172,6 +177,20 @@ export function runtimeMetricsHooksForObservability(
|
|
|
172
177
|
labels: { outcome, port: String(port) },
|
|
173
178
|
});
|
|
174
179
|
},
|
|
180
|
+
onWorkspaceCapture: ({ backend, outcome, durationSeconds }) => {
|
|
181
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) return;
|
|
182
|
+
const safeBackend = SandboxBackend.safeParse(backend).success ? backend : "unknown";
|
|
183
|
+
completedOperationSpan(observability, "worker.workspace_capture", durationSeconds, {
|
|
184
|
+
backend: safeBackend,
|
|
185
|
+
outcome,
|
|
186
|
+
});
|
|
187
|
+
observability.observeHistogram({
|
|
188
|
+
name: "opengeni_workspace_capture_duration_seconds",
|
|
189
|
+
help: "Physical warm workspace capture and publication duration, including late settlement after caller timeout.",
|
|
190
|
+
labels: { backend: safeBackend, outcome },
|
|
191
|
+
value: durationSeconds,
|
|
192
|
+
});
|
|
193
|
+
},
|
|
175
194
|
onWorkspaceArchiveObject: ({ outcome, backend }) => {
|
|
176
195
|
observability.incrementCounter({
|
|
177
196
|
name: "opengeni_workspace_archive_object_total",
|
|
@@ -180,6 +199,7 @@ export function runtimeMetricsHooksForObservability(
|
|
|
180
199
|
});
|
|
181
200
|
},
|
|
182
201
|
onMcpToolCall: ({ outcome, durationSeconds }) => {
|
|
202
|
+
completedOperationSpan(observability, "worker.mcp.tool_call", durationSeconds, { outcome });
|
|
183
203
|
observability.incrementCounter({
|
|
184
204
|
name: "opengeni_mcp_tool_calls_total",
|
|
185
205
|
help: "Total physical MCP tool calls by bounded structural outcome.",
|
|
@@ -1386,7 +1406,7 @@ export type TurnStartupPhase =
|
|
|
1386
1406
|
| "history_generated_image_materialization"
|
|
1387
1407
|
| "history_position_load"
|
|
1388
1408
|
| "owned_sandbox_setup"
|
|
1389
|
-
| "
|
|
1409
|
+
| "runtime_stream_initialization"
|
|
1390
1410
|
| "model_request_preparation"
|
|
1391
1411
|
| "model_sdk_serialization"
|
|
1392
1412
|
| "model_prepare_sandbox_agent_preparation"
|
|
@@ -1492,7 +1512,8 @@ export function turnStartupCountBucket(count: number | null): TurnStartupCountBu
|
|
|
1492
1512
|
}
|
|
1493
1513
|
|
|
1494
1514
|
/**
|
|
1495
|
-
* Measure
|
|
1515
|
+
* Measure startup operations, including nested and parallel work. These are
|
|
1516
|
+
* not additive critical-path intervals; use milestones for elapsed latency.
|
|
1496
1517
|
* Every label is a closed or configuration-derived enum; high-cardinality turn,
|
|
1497
1518
|
* session, credential, connection, file, and model identifiers are forbidden.
|
|
1498
1519
|
*/
|
|
@@ -1506,8 +1527,19 @@ export function recordTurnStartupPhase(
|
|
|
1506
1527
|
durationSeconds: number;
|
|
1507
1528
|
count?: number | null;
|
|
1508
1529
|
cache?: TurnStartupCache;
|
|
1530
|
+
/** Trace-only lookup for the completed claim while its execution root is still open. */
|
|
1531
|
+
executionCorrelationId?: string;
|
|
1509
1532
|
},
|
|
1510
1533
|
): void {
|
|
1534
|
+
completedOperationSpan(observability, `worker.prepare.${input.phase}`, input.durationSeconds, {
|
|
1535
|
+
provider: input.provider,
|
|
1536
|
+
backend: input.backend,
|
|
1537
|
+
outcome: input.outcome,
|
|
1538
|
+
...(input.phase === "claim_and_policy" &&
|
|
1539
|
+
/^turn_[0-9a-f]{32}$/.test(input.executionCorrelationId ?? "")
|
|
1540
|
+
? { correlationId: input.executionCorrelationId }
|
|
1541
|
+
: {}),
|
|
1542
|
+
});
|
|
1511
1543
|
observability.observeHistogram({
|
|
1512
1544
|
name: "opengeni_turn_startup_phase_duration_seconds",
|
|
1513
1545
|
help: "Turn startup phase duration before the model response stream begins.",
|
|
@@ -1524,6 +1556,77 @@ export function recordTurnStartupPhase(
|
|
|
1524
1556
|
});
|
|
1525
1557
|
}
|
|
1526
1558
|
|
|
1559
|
+
/** Background MCP preparation must not inflate startup phase distributions. */
|
|
1560
|
+
export function recordToolPreparationPhase(
|
|
1561
|
+
observability: Observability,
|
|
1562
|
+
input: ToolPreparationPhaseMeasurement & { provider: string; backend: string },
|
|
1563
|
+
): void {
|
|
1564
|
+
if (input.execution === "blocking") {
|
|
1565
|
+
recordTurnStartupPhase(observability, {
|
|
1566
|
+
...input,
|
|
1567
|
+
phase: `tool_${input.phase}`,
|
|
1568
|
+
});
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
completedOperationSpan(
|
|
1572
|
+
observability,
|
|
1573
|
+
`worker.tool_prepare.${input.phase}`,
|
|
1574
|
+
input.durationSeconds,
|
|
1575
|
+
{
|
|
1576
|
+
provider: input.provider,
|
|
1577
|
+
backend: input.backend,
|
|
1578
|
+
outcome: input.outcome,
|
|
1579
|
+
},
|
|
1580
|
+
);
|
|
1581
|
+
observability.observeHistogram({
|
|
1582
|
+
name: "opengeni_tool_background_preparation_duration_seconds",
|
|
1583
|
+
help: "Nonblocking MCP preparation operations; may overlap startup and later execution.",
|
|
1584
|
+
buckets: TURN_STARTUP_PHASE_BUCKETS,
|
|
1585
|
+
labels: {
|
|
1586
|
+
phase: input.phase,
|
|
1587
|
+
provider: input.provider,
|
|
1588
|
+
backend: input.backend,
|
|
1589
|
+
outcome: input.outcome,
|
|
1590
|
+
},
|
|
1591
|
+
value: Math.max(0, input.durationSeconds),
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/** Completed measurements become siblings under the scoped physical attempt.
|
|
1596
|
+
* They never establish ambient ancestry for work which has already finished. */
|
|
1597
|
+
function completedOperationSpan(
|
|
1598
|
+
observability: Observability,
|
|
1599
|
+
name: string,
|
|
1600
|
+
durationSeconds: number,
|
|
1601
|
+
attributes: {
|
|
1602
|
+
outcome: string;
|
|
1603
|
+
provider?: string;
|
|
1604
|
+
backend?: string;
|
|
1605
|
+
correlationId?: string | undefined;
|
|
1606
|
+
},
|
|
1607
|
+
): void {
|
|
1608
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) return;
|
|
1609
|
+
try {
|
|
1610
|
+
const failed = [
|
|
1611
|
+
"failed",
|
|
1612
|
+
"error",
|
|
1613
|
+
"provider_declared_error",
|
|
1614
|
+
"auth_needed",
|
|
1615
|
+
"outcome_uncertain",
|
|
1616
|
+
"timeout",
|
|
1617
|
+
"thrown_transport_error",
|
|
1618
|
+
"thrown_protocol_error",
|
|
1619
|
+
].includes(attributes.outcome);
|
|
1620
|
+
observability
|
|
1621
|
+
.startSpan(name, attributes, { startTimeMs: Date.now() - durationSeconds * 1_000 })
|
|
1622
|
+
.end({
|
|
1623
|
+
...(failed ? { error: true } : {}),
|
|
1624
|
+
});
|
|
1625
|
+
} catch {
|
|
1626
|
+
// Observers cannot change the completed model/tool/phase outcome.
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1527
1630
|
/**
|
|
1528
1631
|
* Explain why a turn did or did not keep provider sandbox work off the
|
|
1529
1632
|
* pre-model critical path. The reason is a closed enum: never attach session,
|
|
@@ -329,7 +329,6 @@ function exactPersonalGitHubDelegation(
|
|
|
329
329
|
delegation.providerDomain !== "github.com" ||
|
|
330
330
|
delegation.kind !== "oauth2" ||
|
|
331
331
|
!delegation.originWorkspaceId ||
|
|
332
|
-
!delegation.userDelegation ||
|
|
333
332
|
!delegation.personalGitHubRepositorySelection
|
|
334
333
|
) {
|
|
335
334
|
throw new Error("personal GitHub Git delegation is unavailable");
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
GitCredentialsRequest,
|
|
6
6
|
} from "@opengeni/contracts";
|
|
7
7
|
import { prReviewRegistrationIdFromCredentialBinding } from "@opengeni/core";
|
|
8
|
-
import { createNativeRemoteMcpCredentialsPort } from "@opengeni/core/remote-mcp-credentials";
|
|
9
8
|
import { decryptVariableSetValue, resolvePrReviewGitCredential, type Database } from "@opengeni/db";
|
|
10
9
|
import {
|
|
11
10
|
createGitHubAppInstallationTokenWithExpiry,
|
|
@@ -19,7 +18,6 @@ export function createStandaloneConnectionCredentialsPort(
|
|
|
19
18
|
db: Database,
|
|
20
19
|
): ConnectionCredentialsPort {
|
|
21
20
|
return {
|
|
22
|
-
...createNativeRemoteMcpCredentialsPort(settings, db),
|
|
23
21
|
gitCredentials: async (request) => await resolveStandaloneGitCredentials(settings, db, request),
|
|
24
22
|
};
|
|
25
23
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
databaseFailureCode,
|
|
3
|
+
nestedPostgresSqlState,
|
|
4
|
+
safeDatabaseErrorFacts,
|
|
5
|
+
SandboxWorkspaceMutationFencedError,
|
|
6
|
+
type SandboxRetainedProcess,
|
|
7
|
+
} from "@opengeni/db";
|
|
8
|
+
import type { Observability } from "@opengeni/observability";
|
|
9
|
+
|
|
10
|
+
/** Protected cause locations and schema facts; public logs receive only the diagnostic key. */
|
|
11
|
+
export function warnRetainedProcessProofFailure(
|
|
12
|
+
observability: Pick<Observability, "recordFailureDiagnostic" | "warn">,
|
|
13
|
+
error: unknown,
|
|
14
|
+
process: Pick<
|
|
15
|
+
SandboxRetainedProcess,
|
|
16
|
+
"id" | "sessionId" | "ownerAttemptId" | "ownerTurnId" | "reconcileAttempts"
|
|
17
|
+
>,
|
|
18
|
+
): void {
|
|
19
|
+
let correlationId: string | undefined;
|
|
20
|
+
try {
|
|
21
|
+
const sqlState = nestedPostgresSqlState(error);
|
|
22
|
+
const constraint = safeDatabaseErrorFacts(error).constraint;
|
|
23
|
+
correlationId = observability.recordFailureDiagnostic({
|
|
24
|
+
code:
|
|
25
|
+
error instanceof SandboxWorkspaceMutationFencedError
|
|
26
|
+
? "retained_process_fenced"
|
|
27
|
+
: sqlState
|
|
28
|
+
? databaseFailureCode(sqlState)
|
|
29
|
+
: "retained_process_proof_failed",
|
|
30
|
+
stage: "sandbox_retained_processes.proof",
|
|
31
|
+
error,
|
|
32
|
+
processId: process.id,
|
|
33
|
+
sessionId: process.sessionId,
|
|
34
|
+
...(process.ownerAttemptId ? { attemptId: process.ownerAttemptId } : {}),
|
|
35
|
+
...(process.ownerTurnId ? { turnId: process.ownerTurnId } : {}),
|
|
36
|
+
attempts: process.reconcileAttempts,
|
|
37
|
+
sqlState,
|
|
38
|
+
...(constraint ? { constraint } : {}),
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
// A diagnostic/export failure must not change reconciliation or release its fence.
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
observability.warn("sandbox reaper: retained-process proof checkpoint failed", {
|
|
45
|
+
...(correlationId ? { correlationId } : {}),
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
// Keep the original failure behavior even when the public sink fails.
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/sandbox-resume.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type Settings,
|
|
28
28
|
} from "@opengeni/config";
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
|
+
import { retainsHolderForWarmCapture } from "./warm-capture-holder";
|
|
30
31
|
import {
|
|
31
32
|
acquireLease,
|
|
32
33
|
adoptLegacyModalCheckpointArtifact,
|
|
@@ -86,7 +87,9 @@ import {
|
|
|
86
87
|
WorkspaceArchiveStorageError,
|
|
87
88
|
type ObjectStorage,
|
|
88
89
|
} from "@opengeni/storage";
|
|
89
|
-
import type
|
|
90
|
+
import { sandboxLeaseTelemetryKey, type Observability } from "@opengeni/observability";
|
|
91
|
+
import { safeSnapshotError } from "./sandbox-snapshot-diagnostics";
|
|
92
|
+
export { safeSnapshotError } from "./sandbox-snapshot-diagnostics";
|
|
90
93
|
import { parseWorkspaceArchiveObjectRef } from "@opengeni/contracts";
|
|
91
94
|
import {
|
|
92
95
|
persistWorkspaceArchiveCandidate,
|
|
@@ -125,9 +128,7 @@ export type SandboxResumeServices = {
|
|
|
125
128
|
db: Database;
|
|
126
129
|
settings: Settings;
|
|
127
130
|
objectStorage?: ObjectStorage | null;
|
|
128
|
-
|
|
129
|
-
* pack/deployment image. Fresh-create NotFound fallback must preserve an
|
|
130
|
-
* ID-only logical base instead of selecting the provider default. */
|
|
131
|
+
|
|
131
132
|
logicalFallbackSettings?: Settings;
|
|
132
133
|
sandboxMetrics?: RuntimeMetricsHooks;
|
|
133
134
|
/** Structured logger for lifecycle decisions (holder release reasons). Falls
|
|
@@ -369,54 +370,6 @@ class SnapshotTimeoutError extends Error {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
export function safeSnapshotError(error: unknown): {
|
|
373
|
-
errorClass: "SnapshotOperationError";
|
|
374
|
-
errorCode: "snapshot_operation_failed";
|
|
375
|
-
status?: number;
|
|
376
|
-
origin: "sandbox-resume";
|
|
377
|
-
causeName?: string;
|
|
378
|
-
integrityCode?: string;
|
|
379
|
-
} {
|
|
380
|
-
const fields: {
|
|
381
|
-
errorClass: "SnapshotOperationError";
|
|
382
|
-
errorCode: "snapshot_operation_failed";
|
|
383
|
-
status?: number;
|
|
384
|
-
origin: "sandbox-resume";
|
|
385
|
-
causeName?: string;
|
|
386
|
-
integrityCode?: string;
|
|
387
|
-
} = {
|
|
388
|
-
errorClass: "SnapshotOperationError",
|
|
389
|
-
errorCode: "snapshot_operation_failed",
|
|
390
|
-
origin: "sandbox-resume",
|
|
391
|
-
};
|
|
392
|
-
try {
|
|
393
|
-
if (error && typeof error === "object") {
|
|
394
|
-
const candidate = error as {
|
|
395
|
-
name?: unknown;
|
|
396
|
-
code?: unknown;
|
|
397
|
-
status?: unknown;
|
|
398
|
-
statusCode?: unknown;
|
|
399
|
-
};
|
|
400
|
-
if (
|
|
401
|
-
typeof candidate.name === "string" &&
|
|
402
|
-
/^[A-Z][A-Za-z0-9]{2,62}Error$/u.test(candidate.name)
|
|
403
|
-
) {
|
|
404
|
-
fields.causeName = candidate.name;
|
|
405
|
-
}
|
|
406
|
-
if (typeof candidate.code === "string" && /^[a-z0-9_]{1,64}$/u.test(candidate.code)) {
|
|
407
|
-
fields.integrityCode = candidate.code;
|
|
408
|
-
}
|
|
409
|
-
const rawStatus = candidate.status ?? candidate.statusCode;
|
|
410
|
-
const status = Number(rawStatus);
|
|
411
|
-
if (Number.isInteger(status) && status >= 100 && status <= 599) fields.status = status;
|
|
412
|
-
}
|
|
413
|
-
} catch {
|
|
414
|
-
// Public diagnostics are best-effort and must never replace the exact
|
|
415
|
-
// internal snapshot failure.
|
|
416
|
-
}
|
|
417
|
-
return fields;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
373
|
export async function waitForWarmSnapshot(
|
|
421
374
|
snapshot: Promise<unknown>,
|
|
422
375
|
timeoutMs: number,
|
|
@@ -787,6 +740,19 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
787
740
|
registerOwnedContinuation: (continuation: Promise<void>) => void,
|
|
788
741
|
): Promise<boolean> {
|
|
789
742
|
const { db, settings } = services;
|
|
743
|
+
const warnSnapshotFailure = (message: string, error: unknown): void => {
|
|
744
|
+
const fields = {
|
|
745
|
+
...safeSnapshotError(error),
|
|
746
|
+
sandboxLeaseKey: sandboxLeaseTelemetryKey(ids.workspaceId, ids.sandboxGroupId),
|
|
747
|
+
leaseEpoch,
|
|
748
|
+
};
|
|
749
|
+
try {
|
|
750
|
+
if (services.observability) services.observability.warn(message, fields);
|
|
751
|
+
else console.warn(message, fields);
|
|
752
|
+
} catch {
|
|
753
|
+
// Diagnostics cannot change provider settlement or capture-gate cleanup.
|
|
754
|
+
}
|
|
755
|
+
};
|
|
790
756
|
const intervalMs = settings.sandboxSnapshotIntervalMs;
|
|
791
757
|
if (intervalMs <= 0 && !force) {
|
|
792
758
|
return false;
|
|
@@ -896,6 +862,9 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
896
862
|
: {}),
|
|
897
863
|
});
|
|
898
864
|
if (claimed.status !== "claimed") {
|
|
865
|
+
// A scheduled interval not being due is normal, including after a failed
|
|
866
|
+
// attempt. Do not turn every ten-second heartbeat into an error log.
|
|
867
|
+
if (claimed.status === "throttled") return false;
|
|
899
868
|
console.error("mid-session workspace snapshot skipped (capture claim)", {
|
|
900
869
|
sandboxGroupId: ids.sandboxGroupId,
|
|
901
870
|
status: claimed.status,
|
|
@@ -951,6 +920,8 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
951
920
|
// turn signal resolves first. Its finally block is the only normal release
|
|
952
921
|
// of the exact admission gate; a late callback cannot release a successor.
|
|
953
922
|
const captureAndPublish = (async (): Promise<boolean> => {
|
|
923
|
+
const captureStarted = performance.now();
|
|
924
|
+
let captureOutcome: "completed" | "failed" = "failed";
|
|
954
925
|
let archive: VerifiedWorkspaceArchivePayload | undefined;
|
|
955
926
|
let candidate: { id: string } | null = null;
|
|
956
927
|
let publicationAttempted = false;
|
|
@@ -1001,6 +972,7 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1001
972
|
if (!wrote && candidate) {
|
|
1002
973
|
await abandonCandidate(candidate.id, "snapshot_publication_fenced");
|
|
1003
974
|
}
|
|
975
|
+
if (wrote) captureOutcome = "completed";
|
|
1004
976
|
return wrote;
|
|
1005
977
|
} catch (error) {
|
|
1006
978
|
if (candidate && !publicationAttempted)
|
|
@@ -1018,20 +990,24 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1018
990
|
expectedEpoch: leaseEpoch,
|
|
1019
991
|
expectedInstanceId: instanceId,
|
|
1020
992
|
}).catch((error) => {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
safeSnapshotError(error),
|
|
1024
|
-
);
|
|
993
|
+
captureOutcome = "failed";
|
|
994
|
+
warnSnapshotFailure("mid-session workspace capture gate release failed", error);
|
|
1025
995
|
});
|
|
996
|
+
try {
|
|
997
|
+
services.sandboxMetrics?.onWorkspaceCapture?.({
|
|
998
|
+
backend: lease.backend,
|
|
999
|
+
outcome: captureOutcome,
|
|
1000
|
+
durationSeconds: Math.max(0, performance.now() - captureStarted) / 1_000,
|
|
1001
|
+
});
|
|
1002
|
+
} catch {
|
|
1003
|
+
// A completed capture and released gate cannot depend on telemetry.
|
|
1004
|
+
}
|
|
1026
1005
|
}
|
|
1027
1006
|
})();
|
|
1028
1007
|
const settled = captureAndPublish.then(
|
|
1029
1008
|
(persisted) => ({ kind: "settled" as const, persisted }),
|
|
1030
1009
|
(error) => {
|
|
1031
|
-
|
|
1032
|
-
"mid-session workspace snapshot failed (turn unaffected)",
|
|
1033
|
-
safeSnapshotError(error),
|
|
1034
|
-
);
|
|
1010
|
+
warnSnapshotFailure("mid-session workspace snapshot failed (turn unaffected)", error);
|
|
1035
1011
|
return { kind: "settled" as const, persisted: false };
|
|
1036
1012
|
},
|
|
1037
1013
|
);
|
|
@@ -1063,12 +1039,10 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1063
1039
|
// exact claim cleanup, so it cannot leak an unhandled promise or artifact.
|
|
1064
1040
|
return outcome.kind === "settled" ? outcome.persisted : false;
|
|
1065
1041
|
} catch (error) {
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
safeSnapshotError(error),
|
|
1071
|
-
);
|
|
1042
|
+
// A failed snapshot does not fail the turn. Physical captures can still
|
|
1043
|
+
// fence commands until their exact gate settles; the caller timeout is not
|
|
1044
|
+
// permission to release that gate. The next eligible tick can retry.
|
|
1045
|
+
warnSnapshotFailure("mid-session workspace snapshot failed (turn unaffected)", error);
|
|
1072
1046
|
return false;
|
|
1073
1047
|
}
|
|
1074
1048
|
}
|
|
@@ -1276,7 +1250,16 @@ export async function resumeBoxForTurn(
|
|
|
1276
1250
|
// provider operation may still be mutating /workspace. Both loops therefore
|
|
1277
1251
|
// converge on "holder alive -> never release"; finalization performs the
|
|
1278
1252
|
// proof-bearing release.
|
|
1279
|
-
const releaseDeadHolder = (reason: string): void => {
|
|
1253
|
+
const releaseDeadHolder = async (reason: string): Promise<void> => {
|
|
1254
|
+
// Logical closure deliberately fences heartbeat authority. It is not proof
|
|
1255
|
+
// that the already-admitted capture has physically settled: the finalizer
|
|
1256
|
+
// owns that wait and the original deadline bounds dead-worker recovery.
|
|
1257
|
+
// Match the reaper's retention rule without touching holder/lease TTLs.
|
|
1258
|
+
if (reason === "holder_gone" && kind === "turn" && holderLeaseHeartbeat) {
|
|
1259
|
+
const expectedEpoch = holderLeaseHeartbeat.expectedEpoch;
|
|
1260
|
+
const lease = await readLease(db, ids.workspaceId, ids.sandboxGroupId);
|
|
1261
|
+
if (retainsHolderForWarmCapture(lease, expectedEpoch)) return;
|
|
1262
|
+
}
|
|
1280
1263
|
const message = "sandbox resume holder released: holder no longer live";
|
|
1281
1264
|
const fields = {
|
|
1282
1265
|
workspaceId: ids.workspaceId,
|
|
@@ -1301,13 +1284,13 @@ export async function resumeBoxForTurn(
|
|
|
1301
1284
|
expectedEpoch: heartbeat.expectedEpoch,
|
|
1302
1285
|
leaseTtlMs: heartbeat.leaseTtlMs,
|
|
1303
1286
|
})
|
|
1304
|
-
.then((status) => {
|
|
1287
|
+
.then(async (status) => {
|
|
1305
1288
|
if (heartbeat !== holderLeaseHeartbeat) return;
|
|
1306
1289
|
if (!status.holderAlive) {
|
|
1307
1290
|
// Reaped, released, or its exact attempt is no longer the active
|
|
1308
1291
|
// writer. Stop this otherwise-unbounded provider operation and
|
|
1309
1292
|
// idempotently drop any remaining holder state.
|
|
1310
|
-
releaseDeadHolder(status.fence);
|
|
1293
|
+
await releaseDeadHolder(status.fence);
|
|
1311
1294
|
return;
|
|
1312
1295
|
}
|
|
1313
1296
|
if (status.fence === "epoch") {
|
|
@@ -1316,7 +1299,7 @@ export async function resumeBoxForTurn(
|
|
|
1316
1299
|
// instance replaced, re-established by a later turn). The holder
|
|
1317
1300
|
// row is stale authority for an instance that no longer exists;
|
|
1318
1301
|
// drop it promptly instead of pinning a successor's lease.
|
|
1319
|
-
releaseDeadHolder(status.fence);
|
|
1302
|
+
await releaseDeadHolder(status.fence);
|
|
1320
1303
|
return;
|
|
1321
1304
|
}
|
|
1322
1305
|
// The box is still serving this live holder: keep the provider TTL
|
|
@@ -1338,13 +1321,13 @@ export async function resumeBoxForTurn(
|
|
|
1338
1321
|
kind,
|
|
1339
1322
|
holderId,
|
|
1340
1323
|
})
|
|
1341
|
-
.then((touched) => {
|
|
1324
|
+
.then(async (touched) => {
|
|
1342
1325
|
if (heartbeat !== holderLeaseHeartbeat) return;
|
|
1343
1326
|
// A canonical turn holder is rejected once its exact attempt is no
|
|
1344
1327
|
// longer the active writer (touch is attempt-fenced only; it never
|
|
1345
1328
|
// consults lease authority, so false here always means holder death).
|
|
1346
1329
|
if (!touched) {
|
|
1347
|
-
releaseDeadHolder("holder_gone");
|
|
1330
|
+
await releaseDeadHolder("holder_gone");
|
|
1348
1331
|
return;
|
|
1349
1332
|
}
|
|
1350
1333
|
void maybeRenewProviderExpiration().catch(() => undefined);
|
|
@@ -1422,7 +1405,8 @@ export async function resumeBoxForTurn(
|
|
|
1422
1405
|
const continuityRecovery = acquired.lease.recovery.continuity;
|
|
1423
1406
|
if (
|
|
1424
1407
|
(acquired.lease.recovery.archive.status === "available" &&
|
|
1425
|
-
acquired.lease.archiveComplete
|
|
1408
|
+
(acquired.lease.archiveComplete ||
|
|
1409
|
+
acquired.lease.historicalRecoveryAuthorized === true)) ||
|
|
1426
1410
|
(acquired.lease.recovery.archive.status === "none" &&
|
|
1427
1411
|
workspaceArchiveFieldsFromEnvelope(archiveSource) !== null)
|
|
1428
1412
|
) {
|
|
@@ -1875,6 +1859,7 @@ export async function resumeBoxForTurn(
|
|
|
1875
1859
|
sandboxGroupId: ids.sandboxGroupId,
|
|
1876
1860
|
expectedEpoch: leaseEpoch,
|
|
1877
1861
|
expectedInstanceId: live.instanceId,
|
|
1862
|
+
expectedBackend: ids.backend,
|
|
1878
1863
|
});
|
|
1879
1864
|
if (marked.status === "marked") {
|
|
1880
1865
|
await services.onSandboxLost?.({
|
package/src/sandbox-routing.ts
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
import { sandboxLifecycleTransitionWaitMs, type Settings } from "@opengeni/config";
|
|
18
18
|
import {
|
|
19
19
|
createProviderCommandRetainer,
|
|
20
|
-
|
|
20
|
+
supervisedCommandProtocolReady,
|
|
21
21
|
} from "@opengeni/db/retained-provider-commands";
|
|
22
22
|
import {
|
|
23
23
|
adoptConnectedMachineSessionBackgroundCommand,
|
|
24
24
|
adoptManagedSessionBackgroundCommand,
|
|
25
25
|
advanceWorkspaceGenerationForRetainedProcess,
|
|
26
|
+
retainedProviderCommandPersistence,
|
|
26
27
|
advanceWorkspaceGeneration,
|
|
27
28
|
getRetainedProcess,
|
|
28
29
|
retainWorkspaceMutationProcess,
|
|
@@ -72,6 +73,7 @@ import {
|
|
|
72
73
|
type RoutingMutationSettlementResult,
|
|
73
74
|
type RoutingSandboxFirstOperationObserver,
|
|
74
75
|
type RoutingSandboxOperationObserver,
|
|
76
|
+
type RoutingSandboxCaptureWaitObserver,
|
|
75
77
|
type RoutingSandboxWaitObservation,
|
|
76
78
|
type RoutingRetainedProcess,
|
|
77
79
|
type RoutingRetainedProcessTerminalProof,
|
|
@@ -112,6 +114,7 @@ export type RoutingWiringServices = {
|
|
|
112
114
|
onOp?: SelfhostedOpObserver;
|
|
113
115
|
/** Every physical routed provider call, across cloud and selfhosted homes. */
|
|
114
116
|
onSandboxOperation?: RoutingSandboxOperationObserver;
|
|
117
|
+
onSandboxCaptureWait?: RoutingSandboxCaptureWaitObserver;
|
|
115
118
|
/** The op-stream durable-resume journal (the Temporal adaptation from
|
|
116
119
|
* op-journal.ts): attach generation + settled-frontier persistence. Absent ⇒
|
|
117
120
|
* the runtime defaults (generation "1", no persistence) — tests / non-turn
|
|
@@ -124,11 +127,12 @@ export type RoutingWiringServices = {
|
|
|
124
127
|
instanceId: string;
|
|
125
128
|
leaseEpoch: number;
|
|
126
129
|
}) => Promise<void>;
|
|
127
|
-
/**
|
|
130
|
+
/** Prepare and publish the worker's home handle. Rejection leaves the route
|
|
131
|
+
* unpublished; replacement preparation must settle before the first tool op. */
|
|
128
132
|
onHomeSandboxRebound?: (input: {
|
|
129
133
|
established: EstablishedSandboxSession;
|
|
130
134
|
leaseEpoch: number;
|
|
131
|
-
}) => void
|
|
135
|
+
}) => void | Promise<void>;
|
|
132
136
|
/** Cancel bounded capture waits with the owning turn activity. */
|
|
133
137
|
waitSignal?: AbortSignal;
|
|
134
138
|
};
|
|
@@ -342,7 +346,7 @@ async function resolveCurrentHomeBackend(
|
|
|
342
346
|
// A route epoch can advance without a provider replacement. Reuse the exact
|
|
343
347
|
// established handle only when its identity still equals the durable one.
|
|
344
348
|
if (lease.instanceId === established.instanceId) {
|
|
345
|
-
services.onHomeSandboxRebound?.({
|
|
349
|
+
await services.onHomeSandboxRebound?.({
|
|
346
350
|
established,
|
|
347
351
|
leaseEpoch: lease.leaseEpoch,
|
|
348
352
|
});
|
|
@@ -393,6 +397,7 @@ async function resolveCurrentHomeBackend(
|
|
|
393
397
|
sandboxGroupId: ids.sandboxGroupId,
|
|
394
398
|
expectedEpoch: lease.leaseEpoch,
|
|
395
399
|
expectedInstanceId: lease.instanceId,
|
|
400
|
+
expectedBackend: lease.backend,
|
|
396
401
|
diagnostic: "provider_not_found_during_home_route_rebind",
|
|
397
402
|
});
|
|
398
403
|
if (marked.status === "marked") {
|
|
@@ -416,7 +421,7 @@ async function resolveCurrentHomeBackend(
|
|
|
416
421
|
if (rebound.instanceId !== lease.instanceId || reboundBackend !== resumeBackend) {
|
|
417
422
|
throw homeRouteRecoveryError(lease, lease.leaseEpoch);
|
|
418
423
|
}
|
|
419
|
-
services.onHomeSandboxRebound?.({
|
|
424
|
+
await services.onHomeSandboxRebound?.({
|
|
420
425
|
established: rebound,
|
|
421
426
|
leaseEpoch: lease.leaseEpoch,
|
|
422
427
|
});
|
|
@@ -747,7 +752,7 @@ function settleRetainedProcessForTurn(
|
|
|
747
752
|
if (
|
|
748
753
|
!durable ||
|
|
749
754
|
durable.providerSessionId !== process.providerSessionId ||
|
|
750
|
-
durable.providerBackend !== backend.kind ||
|
|
755
|
+
durable.providerBackend !== (sandboxBackendForSdkBackendId(backend.kind) ?? backend.kind) ||
|
|
751
756
|
durable.providerInstanceId !== backend.providerInstanceId ||
|
|
752
757
|
durable.leaseEpoch !== backend.leaseEpoch ||
|
|
753
758
|
durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
|
|
@@ -795,7 +800,7 @@ function adoptRetainedProcessAsBackgroundCommandForTurn(
|
|
|
795
800
|
if (
|
|
796
801
|
!durable ||
|
|
797
802
|
durable.providerSessionId !== process.providerSessionId ||
|
|
798
|
-
durable.providerBackend !== backend.kind ||
|
|
803
|
+
durable.providerBackend !== (sandboxBackendForSdkBackendId(backend.kind) ?? backend.kind) ||
|
|
799
804
|
durable.providerInstanceId !== backend.providerInstanceId ||
|
|
800
805
|
durable.leaseEpoch !== backend.leaseEpoch ||
|
|
801
806
|
durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
|
|
@@ -1026,16 +1031,25 @@ export function wrapTurnBoxWithRouting(
|
|
|
1026
1031
|
},
|
|
1027
1032
|
resolveActiveBackend: resolver,
|
|
1028
1033
|
...(services.onSandboxOperation ? { onOperation: services.onSandboxOperation } : {}),
|
|
1034
|
+
...(services.onSandboxCaptureWait ? { onCaptureWait: services.onSandboxCaptureWait } : {}),
|
|
1029
1035
|
providerCommandHandle: admittedCommandHandle,
|
|
1036
|
+
providerSupervisionReady: async () =>
|
|
1037
|
+
settings.modalCommandSupervisionEnabled && (await supervisedCommandProtocolReady(db)),
|
|
1030
1038
|
...(ids.workspaceMutationFence
|
|
1031
1039
|
? {
|
|
1032
1040
|
providerCommandPersistence: (process: RoutingRetainedProcess) =>
|
|
1033
|
-
retainedProviderCommandPersistence(
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1041
|
+
retainedProviderCommandPersistence(
|
|
1042
|
+
db,
|
|
1043
|
+
{
|
|
1044
|
+
accountId: ids.workspaceMutationFence!.accountId,
|
|
1045
|
+
workspaceId: ids.workspaceId,
|
|
1046
|
+
sessionId: ids.sessionId,
|
|
1047
|
+
processId: process.id,
|
|
1048
|
+
},
|
|
1049
|
+
services.bus
|
|
1050
|
+
? (events) => services.bus!.publish(ids.workspaceId, ids.sessionId, events)
|
|
1051
|
+
: undefined,
|
|
1052
|
+
),
|
|
1039
1053
|
}
|
|
1040
1054
|
: {}),
|
|
1041
1055
|
...(beforeMutation ? { beforeMutation } : {}),
|
|
@@ -1069,6 +1083,7 @@ export function wrapTurnBoxWithRouting(
|
|
|
1069
1083
|
sandboxGroupId: home.sandboxGroupId,
|
|
1070
1084
|
expectedEpoch,
|
|
1071
1085
|
expectedInstanceId,
|
|
1086
|
+
expectedBackend: home.backend,
|
|
1072
1087
|
diagnostic: "provider_not_found_during_routed_operation",
|
|
1073
1088
|
});
|
|
1074
1089
|
if (marked.status === "marked") {
|
|
@@ -1276,17 +1291,26 @@ export function wrapLazyTurnBoxWithRouting(
|
|
|
1276
1291
|
return routedResolver(pointer);
|
|
1277
1292
|
},
|
|
1278
1293
|
...(services.onSandboxOperation ? { onOperation: services.onSandboxOperation } : {}),
|
|
1294
|
+
...(services.onSandboxCaptureWait ? { onCaptureWait: services.onSandboxCaptureWait } : {}),
|
|
1279
1295
|
...(args.onFirstOperation ? { onFirstOperation: args.onFirstOperation } : {}),
|
|
1280
1296
|
providerCommandHandle: admittedCommandHandle,
|
|
1297
|
+
providerSupervisionReady: async () =>
|
|
1298
|
+
settings.modalCommandSupervisionEnabled && (await supervisedCommandProtocolReady(db)),
|
|
1281
1299
|
...(ids.workspaceMutationFence
|
|
1282
1300
|
? {
|
|
1283
1301
|
providerCommandPersistence: (process: RoutingRetainedProcess) =>
|
|
1284
|
-
retainedProviderCommandPersistence(
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1302
|
+
retainedProviderCommandPersistence(
|
|
1303
|
+
db,
|
|
1304
|
+
{
|
|
1305
|
+
accountId: ids.workspaceMutationFence!.accountId,
|
|
1306
|
+
workspaceId: ids.workspaceId,
|
|
1307
|
+
sessionId: ids.sessionId,
|
|
1308
|
+
processId: process.id,
|
|
1309
|
+
},
|
|
1310
|
+
services.bus
|
|
1311
|
+
? (events) => services.bus!.publish(ids.workspaceId, ids.sessionId, events)
|
|
1312
|
+
: undefined,
|
|
1313
|
+
),
|
|
1290
1314
|
}
|
|
1291
1315
|
: {}),
|
|
1292
1316
|
...(beforeMutation ? { beforeMutation } : {}),
|
|
@@ -1321,6 +1345,7 @@ export function wrapLazyTurnBoxWithRouting(
|
|
|
1321
1345
|
sandboxGroupId: home.sandboxGroupId,
|
|
1322
1346
|
expectedEpoch: backend.leaseEpoch,
|
|
1323
1347
|
expectedInstanceId: backend.providerInstanceId,
|
|
1348
|
+
expectedBackend: home.backend,
|
|
1324
1349
|
diagnostic: "provider_not_found_during_routed_operation",
|
|
1325
1350
|
});
|
|
1326
1351
|
if (marked.status === "marked") {
|