@mastra/factory 0.24.0-alpha.9 → 0.24.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/README.md +4 -4
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +111 -19
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +1 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +24 -13
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +53 -9
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.d.ts +7 -4
- package/dist/integrations/github/subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/subscriptions.js +5 -5
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js +10 -6
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/subscriptions.js +5 -5
- package/dist/integrations/gitlab/subscriptions.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +16 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +87 -34
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +0 -1
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +1 -4
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +1 -1
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/config.d.ts +6 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +20 -19
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +33 -31
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +6 -1
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.d.ts +2 -4
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +19 -65
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +6 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +95 -12
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +11 -9
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +10 -10
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +4 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +36 -4
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/secret-encryption.d.ts.map +1 -1
- package/dist/secret-encryption.js +65 -25
- package/dist/secret-encryption.js.map +1 -1
- package/dist/session/channel-reply-processor.d.ts +7 -6
- package/dist/session/channel-reply-processor.d.ts.map +1 -1
- package/dist/session/channel-reply-processor.js +9 -9
- package/dist/session/channel-reply-processor.js.map +1 -1
- package/dist/session/channel-session-link-processor.js +1 -0
- package/dist/session/channel-session-link-processor.js.map +1 -1
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +7 -6
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/default-model-hydration.d.ts +1 -0
- package/dist/session/default-model-hydration.d.ts.map +1 -1
- package/dist/session/default-model-hydration.js +3 -2
- package/dist/session/default-model-hydration.js.map +1 -1
- package/dist/session/environment-state-processor.js +1 -1
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +3 -3
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +16 -17
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +1 -1
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +1 -1
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +1 -1
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +1 -1
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/live-sessions.d.ts +1 -1
- package/dist/session/live-sessions.js +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +1 -1
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +105 -0
- package/dist/session/resource-migration.d.ts.map +1 -0
- package/dist/session/resource-migration.js +195 -0
- package/dist/session/resource-migration.js.map +1 -0
- package/dist/session/session-threads.d.ts +58 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +90 -0
- package/dist/session/session-threads.js.map +1 -0
- package/dist/session/source-control-tools.js +8 -8
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +3 -4
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session/thread-session-stamp.d.ts +21 -0
- package/dist/session/thread-session-stamp.d.ts.map +1 -0
- package/dist/session/thread-session-stamp.js +20 -0
- package/dist/session/thread-session-stamp.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +1 -1
- package/dist/session/thread-title-mirror.d.ts.map +1 -1
- package/dist/session/thread-title-mirror.js +1 -1
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/skills/service.d.ts +6 -8
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +3 -3
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +2 -2
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +8 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +7 -0
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +19 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +42 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +14 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +25 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/approval-tools.js +1 -1
- package/dist/tools/approval-tools.js.map +1 -1
- package/dist/tools/deliver.d.ts +8 -5
- package/dist/tools/deliver.d.ts.map +1 -1
- package/dist/tools/deliver.js +22 -8
- package/dist/tools/deliver.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +203 -72
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/permissions.d.ts +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +1 -1
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +10 -3
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +3 -2
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +33 -16
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/side-sessions.d.ts +1 -1
- package/dist/tools/side-sessions.d.ts.map +1 -1
- package/dist/tools/side-sessions.js +1 -1
- package/dist/tools/side-sessions.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +28 -15
- package/dist/workspace.js.map +1 -1
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\n//#region src/setup-marker.ts\n/** Directory, relative to the template's build cwd, that holds every marker below. */\nconst SETUP_MARKER_DIR = \".mastra-sandbox\";\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*\n* This is the single-repository marker. Templates built from a repository\n* list write one marker per repository instead ({@link repoSetupMarkerPath}).\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/**\n* Marker for the workspace-level setup of a multi-repository template: the\n* commands that ran at the working directory after every repository was set\n* up. Written even when there were no such commands, so a consumer can tell\n* \"ran nothing\" from \"never built\".\n*/\nconst WORKSPACE_SETUP_MARKER_PATH = \".mastra-sandbox/workspace-setup\";\n/**\n* One line per repository directory whose setup failed while the build was\n* told to continue. Absent when nothing failed. Consumers check this before\n* trusting a per-repository marker: that marker means every step ran, not\n* that every step succeeded.\n*/\nconst SETUP_FAILED_MARKER_PATH = \".mastra-sandbox/setup-failed\";\n/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */\nfunction repoSetupMarkerPath(repoDir) {\n\treturn `${SETUP_MARKER_DIR}/repos/${repoDir}`;\n}\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {\n\treturn `mkdir -p \"$(dirname \"${markerPath}\")\" && printf '%s' '${content}' > \"${markerPath}\"`;\n}\n/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */\nconst REPO_DIR_PATTERN = /^[\\w.-]+$/;\n/**\n* Shell step that runs one setup command inside a repository clone.\n*\n* Without the guard this is the plain `cd \"<dir>\" && <cmd>` step. With it,\n* the `cd` sits inside the parenthesized group so that the failure branch\n* runs back at the build cwd and the failure list lands at the workspace\n* level, never inside the repository.\n*/\nfunction guardedSetupCommand({ repoDir, command, continueOnFailure }) {\n\tif (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory \"${repoDir}\" must match ${REPO_DIR_PATTERN}.`);\n\tif (!continueOnFailure) return `cd \"${repoDir}\" && ${command}`;\n\treturn `( cd \"${repoDir}\" && sh -c ${shellQuote$1(command)} ) || { mkdir -p \"${SETUP_MARKER_DIR}\" && grep -qxF -- '${repoDir}' \"${SETUP_FAILED_MARKER_PATH}\" 2>/dev/null || printf '%s\\\\n' '${repoDir}' >> \"${SETUP_FAILED_MARKER_PATH}\"; }`;\n}\nfunction shellQuote$1(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : \"\"}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\nexport { SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, guardedSetupCommand, normalizeSetupCommands, repoCloneCommand, repoSetupMarkerPath, setupMarkerCommand, setupMarkerContent };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;AAGA,MAAM,mBAAmB;;;;;;;;;;;;;;AAczB,MAAM,oBAAoB;;;;;;;AAO1B,MAAM,8BAA8B;;;;;;;AAOpC,MAAM,2BAA2B;;AAEjC,SAAS,oBAAoB,SAAS;CACrC,OAAO,GAAG,iBAAiB,SAAS;AACrC;;AAEA,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../../packages/_internals/workspace/dist/index.js"],"sourcesContent":["import { createHash } from \"crypto\";\nimport { posix } from \"path\";\nimport { Buffer } from \"buffer\";\nimport { DirectoryNotEmptyError, DirectoryNotFoundError, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, NotDirectoryError } from \"@mastra/core/workspace\";\n//#region src/setup-marker.ts\n/** Directory, relative to the template's build cwd, that holds every marker below. */\nconst SETUP_MARKER_DIR = \".mastra-sandbox\";\n/**\n* Setup completion marker shared by repo templates and their consumers.\n*\n* A repo template writes this file beside the checkout as its last build\n* step, so it exists only in images where every setup command succeeded. Its\n* content is a digest of the setup commands the image ran, letting a sandbox\n* booted from the image tell whether the setup it is about to run already\n* happened. Relative to the template's build cwd, which is also the runtime\n* working directory the repo was cloned into.\n*\n* This is the single-repository marker. Templates built from a repository\n* list write one marker per repository instead ({@link repoSetupMarkerPath}).\n*/\nconst SETUP_MARKER_PATH = \".mastra-sandbox/setup\";\n/**\n* Marker for the workspace-level setup of a multi-repository template: the\n* commands that ran at the working directory after every repository was set\n* up. Written even when there were no such commands, so a consumer can tell\n* \"ran nothing\" from \"never built\".\n*/\nconst WORKSPACE_SETUP_MARKER_PATH = \".mastra-sandbox/workspace-setup\";\n/**\n* One line per repository directory whose setup failed while the build was\n* told to continue. Absent when nothing failed. Consumers check this before\n* trusting a per-repository marker: that marker means every step ran, not\n* that every step succeeded.\n*/\nconst SETUP_FAILED_MARKER_PATH = \".mastra-sandbox/setup-failed\";\n/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */\nfunction repoSetupMarkerPath(repoDir) {\n\treturn `${SETUP_MARKER_DIR}/repos/${repoDir}`;\n}\n/** Blank entries never become build steps, so they never count toward the digest either. */\nfunction normalizeSetupCommands(setupCommand) {\n\treturn (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== \"\");\n}\n/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */\nfunction setupMarkerContent(setupCommand) {\n\treturn `sha256:${createHash(\"sha256\").update(normalizeSetupCommands(setupCommand).join(\"\\n\")).digest(\"hex\")}`;\n}\n/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */\nfunction setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {\n\treturn `mkdir -p \"$(dirname \"${markerPath}\")\" && printf '%s' '${content}' > \"${markerPath}\"`;\n}\n/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */\nconst REPO_DIR_PATTERN = /^[\\w.-]+$/;\n/**\n* Shell step that runs one setup command inside a repository clone.\n*\n* Without the guard this is the plain `cd \"<dir>\" && <cmd>` step. With it,\n* the `cd` sits inside the parenthesized group so that the failure branch\n* runs back at the build cwd and the failure list lands at the workspace\n* level, never inside the repository.\n*/\nfunction guardedSetupCommand({ repoDir, command, continueOnFailure }) {\n\tif (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory \"${repoDir}\" must match ${REPO_DIR_PATTERN}.`);\n\tif (!continueOnFailure) return `cd \"${repoDir}\" && ${command}`;\n\treturn `( cd \"${repoDir}\" && sh -c ${shellQuote$2(command)} ) || { mkdir -p \"${SETUP_MARKER_DIR}\" && grep -qxF -- '${repoDir}' \"${SETUP_FAILED_MARKER_PATH}\" 2>/dev/null || printf '%s\\\\n' '${repoDir}' >> \"${SETUP_FAILED_MARKER_PATH}\"; }`;\n}\nfunction shellQuote$2(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/repo-clone.ts\nfunction repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {\n\treturn `git ${tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : \"\"}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : \"\"}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;\n}\n/** Per-invocation auth header; `-c` config never reaches `.git/config`. */\nfunction gitAuthFlag(tokenEnv) {\n\treturn `-c http.extraheader=\"AUTHORIZATION: basic $(printf 'x-access-token:%s' \"$${tokenEnv}\" | base64 -w0)\"`;\n}\nfunction shellQuote$1(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n//#endregion\n//#region src/file-commands.ts\n/**\n* Shell scripts for the file operations a sandbox can only do atomically (or\n* without moving file bytes over the network) by running a command inside the\n* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the\n* caller runs it with `sh -c` and maps the exit codes below to typed errors.\n*/\n/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */\nconst FILE_COMMAND_EXIT = {\n\tnotFound: 20,\n\tisDirectory: 21,\n\texists: 22,\n\tnotDirectory: 23,\n\tdestIsDirectory: 24\n};\n/** Single-quote a string for safe POSIX shell interpolation. */\nfunction shellQuote(value) {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\nconst { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;\n/** `[ -e ]` is false for a broken symlink, which still occupies the path. */\nconst present = (variable) => `{ [ -e \"$${variable}\" ] || [ -L \"$${variable}\" ]; }`;\n/**\n* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on\n* one filesystem. Hidden, and unique per call.\n*/\nfunction siblingTempPath(path) {\n\tconst suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;\n\treturn posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);\n}\n/**\n* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when\n* something is at `$dest`. `ln` onto a directory (or a link to one) would\n* create the link inside it and succeed, so that case is undone and reported\n* as taken too.\n*/\nfunction linkTempIntoPlace() {\n\treturn [\n\t\t`ln \"$tmp\" \"$dest\" 2>/dev/null || { rm -f \"$tmp\"; ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$dest/$(basename \"$tmp\")\" \"$tmp\"; exit ${exists}; fi`,\n\t\t`rm -f \"$tmp\"`\n\t];\n}\n/**\n* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.\n*\n* A file source copies its content (a symlinked source is followed). A\n* directory source with `recursive` copies its contents into `dest`, keeping\n* symlinks inside the tree as links. With `overwrite: false` the destination\n* is claimed atomically: a directory with an exclusive `mkdir`, a file by\n* copying to a temp name and hard-linking it into place (`link(2)` fails when\n* the destination exists).\n*\n* Exits: `notFound` (no source), `isDirectory` (directory source without\n* `recursive`), `destIsDirectory` (a file onto an existing directory or a link\n* to one), `exists`.\n*/\nfunction copyCommand(src, dest, options = {}) {\n\tconst noClobber = options.overwrite === false;\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`[ -e \"$src\" ] || exit ${notFound}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`if [ -d \"$src\" ]; then`,\n\t\toptions.recursive ? ` :` : ` exit ${isDirectory}`,\n\t\tnoClobber ? ` mkdir \"$dest\" 2>/dev/null || exit ${exists}` : ` mkdir -p \"$dest\" || exit 1`,\n\t\t` cp -R -P \"$src\"/. \"$dest\"/ || exit 1`,\n\t\t` exit 0`,\n\t\t`fi`,\n\t\tnoClobber ? `${present(\"dest\")} && exit ${exists}` : `if [ -d \"$dest\" ]; then exit ${destIsDirectory}; fi`,\n\t\t...noClobber ? [\n\t\t\t`tmp=${shellQuote(siblingTempPath(dest))}`,\n\t\t\t`cp \"$src\" \"$tmp\" || { rm -f \"$tmp\"; exit 1; }`,\n\t\t\t...linkTempIntoPlace()\n\t\t] : [`cp \"$src\" \"$dest\" || exit 1`]\n\t].join(\"\\n\");\n}\n/**\n* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s\n* parent. A skipped `mv -n` is detected by the source surviving, whatever\n* its exit code. If a directory appears at `dest` between the check and the\n* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported\n* as success; the no-clobber rename itself is atomic.\n*\n* Exits: `notFound` (no source), `exists`.\n*/\nfunction moveNoClobberCommand(src, dest) {\n\treturn [\n\t\t`src=${shellQuote(src)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`${present(\"src\")} || exit ${notFound}`,\n\t\t`${present(\"dest\")} && exit ${exists}`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" || exit 1`,\n\t\t`mv -n \"$src\" \"$dest\" 2>/dev/null || { ${present(\"src\")} && ${present(\"dest\")} && exit ${exists}; exit 1; }`,\n\t\t`${present(\"src\")} && exit ${exists}`,\n\t\t`exit 0`\n\t].join(\"\\n\");\n}\n/**\n* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.\n* The content travels as a file rather than a command argument, so it has no\n* size limit. Creates `dest` (and its parent) when missing.\n*\n* Exits: `isDirectory` (dest is a directory).\n*/\nfunction appendFromCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ -d \"$dest\" ]; then rm -f \"$tmp\"; exit ${isDirectory}; fi`,\n\t\t`mkdir -p \"$(dirname \"$dest\")\" && cat \"$tmp\" >> \"$dest\"`,\n\t\t`status=$?`,\n\t\t`rm -f \"$tmp\"`,\n\t\t`exit $status`\n\t].join(\"\\n\");\n}\n/**\n* Publish the file already written to `tmp` at `dest` only if nothing is\n* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;\n* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.\n*\n* Exits: `exists`, `notDirectory` (dest's parent is not a directory).\n*/\nfunction linkNoClobberCommand(tmp, dest) {\n\treturn [\n\t\t`tmp=${shellQuote(tmp)}`,\n\t\t`dest=${shellQuote(dest)}`,\n\t\t`if [ ! -d \"$(dirname \"$dest\")\" ]; then rm -f \"$tmp\"; exit ${notDirectory}; fi`,\n\t\t`if ${present(\"dest\")}; then rm -f \"$tmp\"; exit ${exists}; fi`,\n\t\t...linkTempIntoPlace()\n\t].join(\"\\n\");\n}\n//#endregion\n//#region src/files-api-filesystem.ts\n/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */\nconst DEFAULT_MAX_DEPTH = 100;\nconst toAbsolute = (path) => posix.resolve(\"/\", path);\n/** The entry's own type: a symlink is a link even when it points at a directory. */\nconst isSymlink = (info) => info.symlinkTarget !== void 0;\nfunction toWriteData(content) {\n\tif (typeof content === \"string\") return content;\n\treturn content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);\n}\nfunction matchesExtension(name, extension) {\n\tif (!extension) return true;\n\tconst ext = posix.extname(name);\n\treturn (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));\n}\n/**\n* A sandbox's disk, read and written through E2B's envd files API, with the\n* operations the files API can't do in one call (copy, append, no-clobber\n* writes and moves) run as one shell command inside the VM so they are atomic\n* and file bytes never round-trip through the host. Paths are absolute inside\n* the sandbox; this class does not confine paths or symlinks to any root.\n*\n* Providers supply how to reach the files API and the shell, and how to read\n* their SDK's errors.\n*\n* If a provider's `runCommand` re-runs a command whose first run already\n* finished (e.g. a retry after a dropped connection), the second run sees the\n* first's result: a no-clobber copy, write or move reports `FileExistsError`\n* or `FileNotFoundError`, and an append fails, even though the work was done.\n*/\nvar FilesApiFilesystem = class extends MastraFilesystem {\n\tasync readFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst bytes = await this.runFiles(\"readFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.read(abs, { format: \"bytes\" });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new FileNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\t\treturn options?.encoding ? buffer.toString(options.encoding) : buffer;\n\t}\n\tasync writeFile(path, content, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tif (options?.overwrite !== false) {\n\t\t\tawait this.runFiles(\"writeFile\", async (files) => {\n\t\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\t\tawait this.#write(files, abs, path, toWriteData(content));\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = async (files) => {\n\t\t\tif (options.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\tawait this.#write(files, tmp, path, toWriteData(content));\n\t\t};\n\t\tawait this.#publishTemp(tmp, \"writeFile\", upload, linkNoClobberCommand(tmp, abs), path, {\n\t\t\texists: () => new FileExistsError(path),\n\t\t\tnotDirectory: () => new NotDirectoryError(posix.dirname(path))\n\t\t});\n\t}\n\tasync appendFile(path, content) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst tmp = siblingTempPath(abs);\n\t\tconst upload = (files) => this.#write(files, tmp, path, toWriteData(content));\n\t\tawait this.#publishTemp(tmp, \"appendFile\", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });\n\t}\n\tasync deleteFile(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"deleteFile\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new FileNotFoundError(path);\n\t\t\t}\n\t\t\tif (!isSymlink(info) && info.type === \"dir\") throw new IsDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\t/**\n\t* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as\n\t* links; `overwrite: false` claims the destination atomically.\n\t*/\n\tasync copyFile(src, dest, options) {\n\t\tconst command = copyCommand(toAbsolute(src), toAbsolute(dest), {\n\t\t\trecursive: options?.recursive,\n\t\t\toverwrite: options?.overwrite\n\t\t});\n\t\tawait this.#command(\"copyFile\", command, `${src} -> ${dest}`, {\n\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\tisDirectory: () => new IsDirectoryError(src),\n\t\t\tdestIsDirectory: () => new IsDirectoryError(dest),\n\t\t\texists: () => new FileExistsError(dest)\n\t\t});\n\t}\n\tasync moveFile(src, dest, options) {\n\t\tconst srcAbs = toAbsolute(src);\n\t\tconst destAbs = toAbsolute(dest);\n\t\tif (options?.overwrite === false) {\n\t\t\tawait this.#command(\"moveFile\", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {\n\t\t\t\tnotFound: () => new FileNotFoundError(src),\n\t\t\t\texists: () => new FileExistsError(dest)\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tawait this.runFiles(\"moveFile\", async (files) => {\n\t\t\ttry {\n\t\t\t\tawait files.rename(srcAbs, destAbs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"not-found\") throw new FileNotFoundError(src);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync mkdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"mkdir\", async (files) => {\n\t\t\tif (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);\n\t\t\ttry {\n\t\t\t\tawait files.makeDir(abs);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new FileExistsError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t}\n\tasync rmdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tawait this.runFiles(\"rmdir\", async (files) => {\n\t\t\tconst info = await this.#info(files, abs);\n\t\t\tif (!info) {\n\t\t\t\tif (options?.force) return;\n\t\t\t\tthrow new DirectoryNotFoundError(path);\n\t\t\t}\n\t\t\tif (!options?.recursive) {\n\t\t\t\tif (isSymlink(info) || info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\t\tif ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);\n\t\t\t} else if (!isSymlink(info) && info.type !== \"dir\") throw new NotDirectoryError(path);\n\t\t\tawait files.remove(abs);\n\t\t});\n\t}\n\tasync readdir(path, options) {\n\t\tconst abs = toAbsolute(path);\n\t\tconst depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;\n\t\tconst entries = await this.runFiles(\"readdir\", async (files) => {\n\t\t\ttry {\n\t\t\t\treturn await files.list(abs, { depth });\n\t\t\t} catch (error) {\n\t\t\t\tconst kind = this.errorKind(error);\n\t\t\t\tif (kind === \"not-found\") throw new DirectoryNotFoundError(path);\n\t\t\t\tif (kind === \"invalid-argument\") throw new NotDirectoryError(path);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\t\tconst result = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.type) continue;\n\t\t\tconst name = posix.relative(abs, entry.path);\n\t\t\tconst type = entry.type === \"dir\" ? \"directory\" : \"file\";\n\t\t\tif (type === \"file\" && !matchesExtension(name, options?.extension)) continue;\n\t\t\tconst link = isSymlink(entry);\n\t\t\tresult.push({\n\t\t\t\tname,\n\t\t\t\ttype,\n\t\t\t\t...type === \"file\" && !link && { size: entry.size },\n\t\t\t\t...link && {\n\t\t\t\t\tisSymlink: true,\n\t\t\t\t\tsymlinkTarget: entry.symlinkTarget\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\tasync exists(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"exists\", async (files) => Boolean((await this.#info(files, abs))?.type));\n\t}\n\tasync stat(path) {\n\t\tconst abs = toAbsolute(path);\n\t\treturn this.runFiles(\"stat\", async (files) => {\n\t\t\tlet info = await this.#info(files, abs);\n\t\t\tif (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));\n\t\t\tif (!info?.type) throw new FileNotFoundError(path);\n\t\t\tconst modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);\n\t\t\treturn {\n\t\t\t\tname: posix.basename(abs),\n\t\t\t\tpath: abs,\n\t\t\t\ttype: info.type === \"dir\" ? \"directory\" : \"file\",\n\t\t\t\tsize: info.type === \"dir\" ? 0 : info.size,\n\t\t\t\tcreatedAt: modifiedAt,\n\t\t\t\tmodifiedAt\n\t\t\t};\n\t\t});\n\t}\n\t/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */\n\tasync #info(files, abs) {\n\t\ttry {\n\t\t\treturn await files.getInfo(abs);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"not-found\") return void 0;\n\t\t\tthrow error;\n\t\t}\n\t}\n\tasync #assertParentDirectory(files, abs, path) {\n\t\tconst parent = await this.#info(files, posix.dirname(abs));\n\t\tif (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));\n\t\tif (parent.type !== \"dir\") throw new NotDirectoryError(posix.dirname(path));\n\t}\n\tasync #write(files, abs, path, data) {\n\t\ttry {\n\t\t\tawait files.write(abs, data);\n\t\t} catch (error) {\n\t\t\tif (this.errorKind(error) === \"invalid-argument\") throw new IsDirectoryError(path);\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/**\n\t* Upload the temp file `tmp`, then run the command that consumes it. The\n\t* script deletes it on every path it controls; if the upload or the command\n\t* fails part-way, delete it here so it doesn't linger in the user's tree.\n\t*/\n\tasync #publishTemp(tmp, operation, upload, script, subject, errors) {\n\t\ttry {\n\t\t\tawait this.runFiles(operation, upload);\n\t\t\tawait this.#command(operation, script, subject, errors);\n\t\t} catch (error) {\n\t\t\tawait this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});\n\t\t\tthrow error;\n\t\t}\n\t}\n\t/** Run a file command and turn its known exit codes into typed errors. */\n\tasync #command(operation, script, subject, errors) {\n\t\tconst result = await this.runCommand(operation, script);\n\t\tif (result.exitCode === 0) return;\n\t\tfor (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {\n\t\t\tconst toError = errors[outcome];\n\t\t\tif (code === result.exitCode && toError) throw toError();\n\t\t}\n\t\tthrow new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);\n\t}\n};\n//#endregion\nexport { FILE_COMMAND_EXIT, FilesApiFilesystem, SETUP_FAILED_MARKER_PATH, SETUP_MARKER_DIR, SETUP_MARKER_PATH, WORKSPACE_SETUP_MARKER_PATH, appendFromCommand, copyCommand, guardedSetupCommand, linkNoClobberCommand, moveNoClobberCommand, normalizeSetupCommands, repoCloneCommand, repoSetupMarkerPath, setupMarkerCommand, setupMarkerContent, shellQuote, siblingTempPath };\n\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;AAMA,MAAM,mBAAmB;;;;;;;;;;;;;;AAczB,MAAM,oBAAoB;;;;;;;AAO1B,MAAM,8BAA8B;;;;;;;AAOpC,MAAM,2BAA2B;;AAEjC,SAAS,oBAAoB,SAAS;CACrC,OAAO,GAAG,iBAAiB,SAAS;AACrC;;AAEA,SAAS,uBAAuB,cAAc;CAC7C,QAAQ,iBAAiB,KAAK,IAAI,CAAC,IAAI,MAAM,QAAQ,YAAY,IAAI,eAAe,CAAC,YAAY,EAAA,CAAG,QAAQ,YAAY,QAAQ,KAAK,MAAM,EAAE;AAC9I;;AAEA,SAAS,mBAAmB,cAAc;CACzC,OAAO,UAAU,WAAW,QAAQ,CAAC,CAAC,OAAO,uBAAuB,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK;AAC3G;;;;;;;;AA4CA,MAAM,oBAAoB;CACzB,UAAU;CACV,aAAa;CACb,QAAQ;CACR,cAAc;CACd,iBAAiB;AAClB;AAKA,MAAM,EAAE,UAAU,aAAa,QAAQ,cAAc,oBAAoB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention-activity.js","names":["#workItems","#comments","#resolve","#scan"],"sources":["../../src/routes/attention-activity.ts"],"sourcesContent":["/** Projects `activity.ts` fan-out rows. */\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type {\n WorkItemActivityRow,\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n} from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionReceiptRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { factoryActivityAttentionIdentity, factoryAttentionKey } from '../storage/domains/work-items/base.js';\nimport type {\n AttentionCounts,\n AttentionEntry,\n AttentionLatest,\n AttentionPageArgs,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n} from './attention-providers.js';\nimport {\n collectPage,\n markScanRead,\n matchesView,\n receiptScope,\n SCAN_PAGE_SIZE,\n scanBatches,\n} from './attention-providers.js';\n\ninterface ResolvedActivity {\n activity: WorkItemActivityRow;\n item: WorkItemRow;\n comment: WorkItemCommentRow | undefined;\n identity: FactoryAttentionIdentity;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nexport class ActivityAttentionProvider implements AttentionProvider {\n readonly kind = 'activity' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #comments: WorkItemCommentsStorage;\n\n constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage }) {\n this.#workItems = workItems;\n this.#comments = comments;\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<WorkItemActivityRow>(\n async cursor => {\n const rows = await this.#comments.listActivityForUser({\n ...receiptScope(scope),\n ...(cursor ? { before: cursor } : {}),\n limit: SCAN_PAGE_SIZE + 1,\n });\n return { rows: rows.slice(0, SCAN_PAGE_SIZE), hasMore: rows.length > SCAN_PAGE_SIZE };\n },\n activity => ({ occurredAt: activity.occurredAt, id: activity.id }),\n before,\n );\n }\n\n /**\n * A vanished work item drops the row; a deleted latest comment does not —\n * the entry still reports that the discussion moved, with the body replaced.\n */\n async #resolve(scope: AttentionScope, rows: WorkItemActivityRow[]): Promise<ResolvedActivity[]> {\n const [receipts, comments, items] = await Promise.all([\n this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: rows.map(row => factoryActivityAttentionIdentity(row.workItemId, row.occurrence)),\n }),\n this.#comments.listByIds({\n orgId: scope.orgId,\n ids: [...new Set(rows.map(row => row.latestCommentId))],\n }),\n this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: [...new Set(rows.map(row => row.workItemId))],\n }),\n ]);\n const receiptByKey = new Map(\n receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]),\n );\n const commentById = new Map(comments.map(comment => [comment.id, comment]));\n const itemById = new Map(items.map(item => [item.id, item]));\n\n const resolved: ResolvedActivity[] = [];\n for (const activity of rows) {\n const item = itemById.get(activity.workItemId);\n if (!item) continue;\n const identity = factoryActivityAttentionIdentity(activity.workItemId, activity.occurrence);\n resolved.push({\n activity,\n item,\n comment: commentById.get(activity.latestCommentId),\n identity,\n receipt: receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity)),\n });\n }\n return resolved;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for await (const batch of this.#scan(scope)) {\n for (const { receipt } of await this.#resolve(scope, batch.rows)) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const rows = await this.#comments.listActivityForUser({ ...receiptScope(scope), limit: 10 });\n const newest = (await this.#resolve(scope, rows))[0];\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.identity),\n at: newest.activity.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async rows => {\n const entries: AttentionEntry[] = [];\n for (const resolved of await this.#resolve(scope, rows)) {\n if (!matchesView(view, resolved.receipt)) continue;\n if (search && !matchesActivitySearch(resolved, search)) continue;\n entries.push({\n occurredAt: resolved.activity.occurredAt,\n resumeCursor: { occurredAt: resolved.activity.occurredAt, id: resolved.activity.id },\n item: toActivityItem(scope, resolved),\n });\n }\n return entries;\n });\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async rows => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: rows.map(row => factoryActivityAttentionIdentity(row.workItemId, row.occurrence)),\n now,\n });\n });\n }\n}\n\nfunction activityDetail({ comment }: ResolvedActivity): string {\n if (!comment || comment.deletedAt) return 'Comment removed';\n return comment.body.slice(0, 512);\n}\n\nfunction matchesActivitySearch(resolved: ResolvedActivity, search: string): boolean {\n return (\n resolved.item.title.toLowerCase().includes(search) ||\n activityDetail(resolved).toLowerCase().includes(search) ||\n resolved.activity.latestAuthorName?.toLowerCase().includes(search) === true\n );\n}\n\nfunction toActivityItem(scope: AttentionScope, resolved: ResolvedActivity) {\n const { activity, item, identity, receipt } = resolved;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'activity' as const,\n commentId: activity.latestCommentId,\n occurrence: activity.occurrence,\n workItemId: activity.workItemId,\n title: item.title,\n detail: activityDetail(resolved),\n authorId: activity.latestAuthorId,\n ...(activity.latestAuthorName ? { authorName: activity.latestAuthorName } : {}),\n occurredAt: activity.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: {\n kind: 'work-item' as const,\n workItemId: activity.workItemId,\n board: boardForWorkItem(item),\n commentId: activity.latestCommentId,\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"attention-activity.js","names":[],"sources":["../../src/routes/attention-activity.ts"],"sourcesContent":["/** Projects `activity.ts` fan-out rows. */\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type {\n WorkItemActivityRow,\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n} from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionReceiptRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { factoryActivityAttentionIdentity, factoryAttentionKey } from '../storage/domains/work-items/base.js';\nimport type {\n AttentionCounts,\n AttentionEntry,\n AttentionLatest,\n AttentionPageArgs,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n} from './attention-providers.js';\nimport {\n collectPage,\n markScanRead,\n matchesView,\n receiptScope,\n SCAN_PAGE_SIZE,\n scanBatches,\n} from './attention-providers.js';\n\ninterface ResolvedActivity {\n activity: WorkItemActivityRow;\n item: WorkItemRow;\n comment: WorkItemCommentRow | undefined;\n identity: FactoryAttentionIdentity;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nexport class ActivityAttentionProvider implements AttentionProvider {\n readonly kind = 'activity' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #comments: WorkItemCommentsStorage;\n\n constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage }) {\n this.#workItems = workItems;\n this.#comments = comments;\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<WorkItemActivityRow>(\n async cursor => {\n const rows = await this.#comments.listActivityForUser({\n ...receiptScope(scope),\n ...(cursor ? { before: cursor } : {}),\n limit: SCAN_PAGE_SIZE + 1,\n });\n return { rows: rows.slice(0, SCAN_PAGE_SIZE), hasMore: rows.length > SCAN_PAGE_SIZE };\n },\n activity => ({ occurredAt: activity.occurredAt, id: activity.id }),\n before,\n );\n }\n\n /**\n * A vanished work item drops the row; a deleted latest comment does not —\n * the entry still reports that the discussion moved, with the body replaced.\n */\n async #resolve(scope: AttentionScope, rows: WorkItemActivityRow[]): Promise<ResolvedActivity[]> {\n const [receipts, comments, items] = await Promise.all([\n this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: rows.map(row => factoryActivityAttentionIdentity(row.workItemId, row.occurrence)),\n }),\n this.#comments.listByIds({\n orgId: scope.orgId,\n ids: [...new Set(rows.map(row => row.latestCommentId))],\n }),\n this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: [...new Set(rows.map(row => row.workItemId))],\n }),\n ]);\n const receiptByKey = new Map(\n receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]),\n );\n const commentById = new Map(comments.map(comment => [comment.id, comment]));\n const itemById = new Map(items.map(item => [item.id, item]));\n\n const resolved: ResolvedActivity[] = [];\n for (const activity of rows) {\n const item = itemById.get(activity.workItemId);\n if (!item) continue;\n const identity = factoryActivityAttentionIdentity(activity.workItemId, activity.occurrence);\n resolved.push({\n activity,\n item,\n comment: commentById.get(activity.latestCommentId),\n identity,\n receipt: receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity)),\n });\n }\n return resolved;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for await (const batch of this.#scan(scope)) {\n for (const { receipt } of await this.#resolve(scope, batch.rows)) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const rows = await this.#comments.listActivityForUser({ ...receiptScope(scope), limit: 10 });\n const newest = (await this.#resolve(scope, rows))[0];\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.identity),\n at: newest.activity.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async rows => {\n const entries: AttentionEntry[] = [];\n for (const resolved of await this.#resolve(scope, rows)) {\n if (!matchesView(view, resolved.receipt)) continue;\n if (search && !matchesActivitySearch(resolved, search)) continue;\n entries.push({\n occurredAt: resolved.activity.occurredAt,\n resumeCursor: { occurredAt: resolved.activity.occurredAt, id: resolved.activity.id },\n item: toActivityItem(scope, resolved),\n });\n }\n return entries;\n });\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async rows => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: rows.map(row => factoryActivityAttentionIdentity(row.workItemId, row.occurrence)),\n now,\n });\n });\n }\n}\n\nfunction activityDetail({ comment }: ResolvedActivity): string {\n if (!comment || comment.deletedAt) return 'Comment removed';\n return comment.body.slice(0, 512);\n}\n\nfunction matchesActivitySearch(resolved: ResolvedActivity, search: string): boolean {\n return (\n resolved.item.title.toLowerCase().includes(search) ||\n activityDetail(resolved).toLowerCase().includes(search) ||\n resolved.activity.latestAuthorName?.toLowerCase().includes(search) === true\n );\n}\n\nfunction toActivityItem(scope: AttentionScope, resolved: ResolvedActivity) {\n const { activity, item, identity, receipt } = resolved;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'activity' as const,\n commentId: activity.latestCommentId,\n occurrence: activity.occurrence,\n workItemId: activity.workItemId,\n title: item.title,\n detail: activityDetail(resolved),\n authorId: activity.latestAuthorId,\n ...(activity.latestAuthorName ? { authorName: activity.latestAuthorName } : {}),\n occurredAt: activity.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: {\n kind: 'work-item' as const,\n workItemId: activity.workItemId,\n board: boardForWorkItem(item),\n commentId: activity.latestCommentId,\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention-parked.js","names":[
|
|
1
|
+
{"version":3,"file":"attention-parked.js","names":[],"sources":["../../src/routes/attention-parked.ts"],"sourcesContent":["/** Sessions parked on a tool, read live from the registry: the item exists exactly as long as an answer is owed. */\n\nimport type { LiveSessions } from '../session/live-sessions.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionReceiptRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { factoryAgentWaitingAttentionIdentity, factoryAttentionKey } from '../storage/domains/work-items/base.js';\nimport type {\n AttentionCounts,\n AttentionEntry,\n AttentionLatest,\n AttentionPageArgs,\n AttentionPageResult,\n AttentionProvider,\n AttentionScope,\n AttentionStreamPosition,\n} from './attention-providers.js';\nimport { matchesView, receiptScope } from './attention-providers.js';\n\ninterface ParkedSession {\n item: WorkItemRow;\n role: string;\n sessionId: string;\n threadId: string;\n toolName: string;\n occurredAt: Date;\n identity: FactoryAttentionIdentity;\n}\n\ninterface ReceiptedParkedSession {\n entry: ParkedSession;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nfunction parkedRunLabel(toolName: string): string {\n return toolName === 'submit_plan' ? 'Plan waiting for review' : 'Agent is waiting for an answer';\n}\n\nfunction olderThan(entry: ParkedSession, before: AttentionStreamPosition | undefined): boolean {\n if (!before) return true;\n const gap = entry.occurredAt.getTime() - before.occurredAt.getTime();\n return gap < 0 || (gap === 0 && entry.sessionId.localeCompare(before.id) > 0);\n}\n\nfunction toItem(scope: AttentionScope, { entry, receipt }: ReceiptedParkedSession): Record<string, unknown> {\n return {\n key: factoryAttentionKey(scope.factoryProjectId, entry.identity),\n kind: 'agent-waiting' as const,\n sessionId: entry.sessionId,\n threadId: entry.threadId,\n role: entry.role,\n toolName: entry.toolName,\n occurrence: entry.identity.occurrence,\n workItemId: entry.item.id,\n title: entry.item.title,\n detail: parkedRunLabel(entry.toolName),\n occurredAt: entry.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: { kind: 'thread' as const, sessionId: entry.sessionId, threadId: entry.threadId },\n };\n}\n\nexport class ParkedRunAttentionProvider implements AttentionProvider {\n readonly kind = 'agent-waiting' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #liveSessions: Pick<LiveSessions, 'parkedIn'>;\n\n constructor({\n workItems,\n liveSessions,\n }: {\n workItems: WorkItemsStorage;\n liveSessions: Pick<LiveSessions, 'parkedIn'>;\n }) {\n this.#workItems = workItems;\n this.#liveSessions = liveSessions;\n }\n\n /** Newest park first. A session bound to several cards is listed once, under the first card. */\n async #parked(scope: AttentionScope): Promise<ParkedSession[]> {\n const runBySession = new Map(\n this.#liveSessions.parkedIn(scope.factoryProjectId).map(({ sessionId, run }) => [sessionId, run]),\n );\n if (runBySession.size === 0) return [];\n const items = await this.#workItems.list({ orgId: scope.orgId, factoryProjectId: scope.factoryProjectId });\n const bySession = new Map<string, ParkedSession>();\n for (const item of items) {\n for (const [role, ref] of Object.entries(item.sessions)) {\n const run = runBySession.get(ref.sessionId);\n if (!run || bySession.has(ref.sessionId)) continue;\n bySession.set(ref.sessionId, {\n item,\n role,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n toolName: run.toolName,\n occurredAt: new Date(run.suspendedAt),\n identity: factoryAgentWaitingAttentionIdentity(ref.sessionId, run.suspendedAt),\n });\n }\n }\n return [...bySession.values()].sort(\n (a, b) => b.occurredAt.getTime() - a.occurredAt.getTime() || a.sessionId.localeCompare(b.sessionId),\n );\n }\n\n async #withReceipts(scope: AttentionScope, parked: ParkedSession[]): Promise<ReceiptedParkedSession[]> {\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: parked.map(entry => entry.identity),\n });\n const byKey = new Map(receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));\n return parked.map(entry => ({\n entry,\n receipt: byKey.get(factoryAttentionKey(scope.factoryProjectId, entry.identity)),\n }));\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for (const { receipt } of await this.#withReceipts(scope, await this.#parked(scope))) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const [newest] = await this.#withReceipts(scope, (await this.#parked(scope)).slice(0, 1));\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.entry.identity),\n at: newest.entry.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n const parked = (await this.#parked(scope)).filter(entry => olderThan(entry, before));\n const entries: AttentionEntry[] = [];\n for (const receipted of await this.#withReceipts(scope, parked)) {\n const { entry, receipt } = receipted;\n if (!matchesView(view, receipt)) continue;\n if (search && !`${entry.item.title} ${parkedRunLabel(entry.toolName)}`.toLowerCase().includes(search)) continue;\n if (entries.length === limit) return { entries, hasMore: true };\n entries.push({\n occurredAt: entry.occurredAt,\n resumeCursor: { occurredAt: entry.occurredAt, id: entry.sessionId },\n item: toItem(scope, receipted),\n });\n }\n return { entries, hasMore: false };\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean }> {\n const parked = (await this.#parked(scope)).filter(entry => olderThan(entry, before));\n if (parked.length > 0) {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: parked.map(entry => entry.identity),\n now,\n });\n }\n return { hasMore: false };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attention-providers.js","names":["#workItems","#spec","#boards","#scan","#receiptsFor","#linkedItems","#comments","#resolve"],"sources":["../../src/routes/attention-providers.ts"],"sourcesContent":["import { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { decisionRetryable, overtakenDecisionIds } from '../rules/decision-applicability.js';\n/**\n * Per-kind attention providers. Each kind owns its counts, its bounded page\n * scan, and its wire item shape; the route layer k-way merges provider pages\n * on `occurredAt desc` with one resumable cursor per kind.\n */\n\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type {\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n WorkItemMentionRow,\n} from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionKind,\n FactoryAttentionReceiptRecord,\n FactoryDeferredDecisionRecord,\n FactoryDispatchStatus,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport {\n factoryAttentionKey,\n factoryDecisionAttentionIdentity,\n factoryMentionAttentionIdentity,\n} from '../storage/domains/work-items/base.js';\n\nexport type FactoryAttentionView = 'open' | 'unread' | 'archived';\n\nexport interface AttentionScope {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n}\n\nexport interface AttentionStreamPosition {\n occurredAt: Date;\n id: string;\n}\n\nexport interface AttentionEntry {\n occurredAt: Date;\n /** Position resuming the provider's stream right after this entry. */\n resumeCursor: AttentionStreamPosition;\n item: Record<string, unknown>;\n}\n\nexport interface AttentionPageArgs {\n view: FactoryAttentionView;\n search?: string;\n before?: AttentionStreamPosition;\n limit: number;\n}\n\nexport interface AttentionPageResult {\n entries: AttentionEntry[];\n /** More rows behind the scan once every returned entry is consumed. */\n hasMore: boolean;\n /** Resume point for a scan-budget stop past the last returned entry. */\n continuation?: AttentionStreamPosition;\n}\n\nexport interface AttentionCounts {\n open: number;\n unread: number;\n}\n\nexport interface AttentionLatest {\n key: string;\n at: Date;\n unread: boolean;\n}\n\nexport interface AttentionProvider {\n kind: FactoryAttentionKind;\n counts(scope: AttentionScope): Promise<AttentionCounts>;\n latest(scope: AttentionScope): Promise<AttentionLatest | null>;\n page(scope: AttentionScope, args: AttentionPageArgs): Promise<AttentionPageResult>;\n markAllRead(\n scope: AttentionScope,\n args: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }>;\n}\n\nexport const SCAN_PAGE_SIZE = 50;\n// Receipt filtering is bounded per request; the response cursor resumes after the last scan.\nconst MAX_RECEIPT_SCAN_PAGES = 4;\n\ninterface ScanBatch<T> {\n rows: T[];\n /** Set only when the page budget stopped a scan that still had rows behind it. */\n continuation?: AttentionStreamPosition;\n}\n\n/**\n * One bounded keyset scan, shared by every provider read. Walks newest-first\n * from `before` until the stream runs dry or the page budget is spent; the\n * final batch of a budget stop carries the position to resume from.\n */\nexport async function* scanBatches<T>(\n fetchBatch: (before?: AttentionStreamPosition) => Promise<{ rows: T[]; hasMore: boolean }>,\n positionOf: (row: T) => AttentionStreamPosition,\n before?: AttentionStreamPosition,\n): AsyncGenerator<ScanBatch<T>> {\n let cursor = before;\n for (let scanned = 1; scanned <= MAX_RECEIPT_SCAN_PAGES; scanned += 1) {\n const { rows, hasMore } = await fetchBatch(cursor);\n const last = rows.at(-1);\n if (!last) return;\n const position = positionOf(last);\n yield { rows, ...(hasMore && scanned === MAX_RECEIPT_SCAN_PAGES ? { continuation: position } : {}) };\n if (!hasMore) return;\n cursor = position;\n }\n}\n\n/** Fills a page from the scan, stopping at `limit` entries or at the scan budget. */\nexport async function collectPage<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n limit: number,\n toEntries: (rows: T[]) => Promise<AttentionEntry[]>,\n): Promise<AttentionPageResult> {\n const entries: AttentionEntry[] = [];\n for await (const batch of batches) {\n for (const entry of await toEntries(batch.rows)) {\n if (entries.length === limit) return { entries, hasMore: true };\n entries.push(entry);\n }\n if (batch.continuation) return { entries, hasMore: true, continuation: batch.continuation };\n }\n return { entries, hasMore: false };\n}\n\n/** Marks every scanned batch read, reporting where a budget stop left off. */\nexport async function markScanRead<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n markBatch: (rows: T[]) => Promise<void>,\n): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n for await (const batch of batches) {\n await markBatch(batch.rows);\n if (batch.continuation) return { hasMore: true, continuation: batch.continuation };\n }\n return { hasMore: false };\n}\n\nexport function factoryDecisionType(decision: FactoryDeferredDecisionRecord): string {\n return typeof decision.decision.type === 'string' ? decision.decision.type.slice(0, 64) : 'unknown';\n}\n\nexport function matchesView(view: FactoryAttentionView, receipt: FactoryAttentionReceiptRecord | undefined): boolean {\n if (view === 'archived') return receipt?.state === 'archived';\n if (view === 'unread') return receipt === undefined;\n return receipt?.state !== 'archived';\n}\n\nfunction attentionTarget(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined) {\n if (!item) return { kind: 'rules' as const };\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : undefined;\n const session = role ? item.sessions[role] : undefined;\n if (session) {\n return {\n kind: 'thread' as const,\n sessionId: session.sessionId,\n threadId: session.threadId,\n };\n }\n return {\n kind: 'work-item' as const,\n workItemId: item.id,\n board: boardForWorkItem(item),\n };\n}\n\n/** One deferred-decision status seen as attention: failures and parked runs differ only by this. */\nexport interface DecisionAttentionSpec {\n kind: FactoryAttentionKind;\n status: FactoryDispatchStatus;\n identity(decision: FactoryDeferredDecisionRecord): FactoryAttentionIdentity;\n occurredAt(decision: FactoryDeferredDecisionRecord): Date;\n title(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n detail(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n extra?(decision: FactoryDeferredDecisionRecord, context: { overtaken: boolean }): Record<string, unknown>;\n matches(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined, search: string): boolean;\n}\n\nexport class DecisionAttentionProvider implements AttentionProvider {\n readonly kind: FactoryAttentionKind;\n readonly #workItems: WorkItemsStorage;\n readonly #spec: DecisionAttentionSpec;\n readonly #boards: BoardRegistry;\n\n constructor(\n { workItems, boards }: { workItems: WorkItemsStorage; boards: BoardRegistry },\n spec: DecisionAttentionSpec,\n ) {\n this.#workItems = workItems;\n this.#boards = boards;\n this.#spec = spec;\n this.kind = spec.kind;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n const [total, receiptCount, archivedCount] = await Promise.all([\n this.#workItems.countDeferredDecisionsByStatuses({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n statuses: [this.#spec.status],\n }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind, state: 'archived' }),\n ]);\n return {\n open: Math.max(0, total - archivedCount),\n unread: Math.max(0, total - receiptCount),\n };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n limit: 1,\n });\n const newest = page.decisions[0];\n if (!newest) return null;\n const identity = this.#spec.identity(newest);\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: [identity],\n });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: this.#spec.occurredAt(newest),\n unread: receipts.length === 0,\n };\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<FactoryDeferredDecisionRecord>(\n async cursor => {\n const page = await this.#workItems.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n before: cursor,\n limit: SCAN_PAGE_SIZE,\n });\n return { rows: page.decisions, hasMore: page.hasMore };\n },\n decision => ({ occurredAt: this.#spec.occurredAt(decision), id: decision.id }),\n before,\n );\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async decisions => {\n const [receiptByKey, itemById, overtaken] = await Promise.all([\n this.#receiptsFor(scope, decisions),\n this.#linkedItems(scope, decisions),\n overtakenDecisionIds(this.#workItems, this.#boards, scope, decisions),\n ]);\n const entries: AttentionEntry[] = [];\n for (const decision of decisions) {\n const identity = this.#spec.identity(decision);\n const receipt = receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity));\n if (!matchesView(view, receipt)) continue;\n const item = decision.workItemId ? itemById.get(decision.workItemId) : undefined;\n if (search && !this.#spec.matches(decision, item, search)) continue;\n const occurredAt = this.#spec.occurredAt(decision);\n entries.push({\n occurredAt,\n resumeCursor: { occurredAt, id: decision.id },\n item: {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: this.kind,\n decisionId: decision.id,\n occurrence: identity.occurrence,\n workItemId: decision.workItemId,\n title: this.#spec.title(decision, item),\n detail: this.#spec.detail(decision, item),\n decisionType: factoryDecisionType(decision),\n ...this.#spec.extra?.(decision, { overtaken: overtaken.has(decision.id) }),\n occurredAt: occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: attentionTarget(decision, item),\n },\n });\n }\n return entries;\n });\n }\n\n async #receiptsFor(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, FactoryAttentionReceiptRecord>> {\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: decisions.map(decision => this.#spec.identity(decision)),\n });\n return new Map(receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));\n }\n\n async #linkedItems(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, WorkItemRow>> {\n const items = await this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: decisions.flatMap(decision => (decision.workItemId ? [decision.workItemId] : [])),\n });\n return new Map(items.map(item => [item.id, item]));\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async decisions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: decisions.map(decision => this.#spec.identity(decision)),\n now,\n });\n });\n }\n}\n\nexport const failedDecisionAttentionSpec: DecisionAttentionSpec = {\n kind: 'automation-failed',\n status: 'failed',\n identity: decision => factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence),\n occurredAt: decision => decision.completedAt ?? decision.updatedAt,\n title: (decision, item) => item?.title ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n detail: decision => decision.lastError?.slice(0, 512) ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n extra: (decision, { overtaken }) => ({\n failureCode: decision.failureCode,\n canRetry: decisionRetryable(decision, overtaken),\n }),\n matches: (decision, item, search) =>\n item?.title.toLowerCase().includes(search) === true ||\n decision.lastError?.toLowerCase().includes(search) === true ||\n factoryDecisionType(decision).toLowerCase().includes(search),\n};\n\ninterface ResolvedMention {\n mention: WorkItemMentionRow;\n comment: WorkItemCommentRow;\n item: WorkItemRow;\n identity: FactoryAttentionIdentity;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nexport class MentionAttentionProvider implements AttentionProvider {\n readonly kind = 'mention' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #comments: WorkItemCommentsStorage;\n\n constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage }) {\n this.#workItems = workItems;\n this.#comments = comments;\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<WorkItemMentionRow>(\n async cursor => {\n const rows = await this.#comments.listMentionsForUser({\n ...receiptScope(scope),\n ...(cursor ? { before: cursor } : {}),\n limit: SCAN_PAGE_SIZE + 1,\n });\n return { rows: rows.slice(0, SCAN_PAGE_SIZE), hasMore: rows.length > SCAN_PAGE_SIZE };\n },\n mention => ({ occurredAt: mention.occurredAt, id: mention.id }),\n before,\n );\n }\n\n /**\n * Mention rows joined to what the inbox needs to show one — dropping the\n * ones it cannot: a deleted comment, or a work item gone from the project.\n */\n async #resolve(scope: AttentionScope, mentions: WorkItemMentionRow[]): Promise<ResolvedMention[]> {\n const [receipts, comments, items] = await Promise.all([\n this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n }),\n this.#comments.listByIds({\n orgId: scope.orgId,\n ids: [...new Set(mentions.map(mention => mention.commentId))],\n }),\n this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: [...new Set(mentions.map(mention => mention.workItemId))],\n }),\n ]);\n const receiptByKey = new Map(\n receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]),\n );\n const commentById = new Map(comments.map(comment => [comment.id, comment]));\n const itemById = new Map(items.map(item => [item.id, item]));\n\n const resolved: ResolvedMention[] = [];\n for (const mention of mentions) {\n const comment = commentById.get(mention.commentId);\n const item = itemById.get(mention.workItemId);\n if (!comment || comment.deletedAt || !item) continue;\n const identity = factoryMentionAttentionIdentity(mention.commentId);\n resolved.push({\n mention,\n comment,\n item,\n identity,\n receipt: receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity)),\n });\n }\n return resolved;\n }\n\n /**\n * Derived from the same bounded scan as `page`, never from raw aggregates:\n * the badge must count exactly what the views can show, down to stopping at\n * the same scan budget. Aggregates would count orphan mention rows the list\n * can never display or clear.\n */\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for await (const batch of this.#scan(scope)) {\n for (const { receipt } of await this.#resolve(scope, batch.rows)) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const mentions = await this.#comments.listMentionsForUser({ ...receiptScope(scope), limit: 10 });\n const newest = (await this.#resolve(scope, mentions))[0];\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.identity),\n at: newest.mention.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async mentions => {\n const entries: AttentionEntry[] = [];\n for (const resolved of await this.#resolve(scope, mentions)) {\n if (!matchesView(view, resolved.receipt)) continue;\n if (search && !matchesMentionSearch(resolved, search)) continue;\n entries.push({\n occurredAt: resolved.mention.occurredAt,\n resumeCursor: { occurredAt: resolved.mention.occurredAt, id: resolved.mention.id },\n item: toMentionItem(scope, resolved),\n });\n }\n return entries;\n });\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async mentions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n now,\n });\n });\n }\n}\n\nfunction matchesMentionSearch({ item, comment }: ResolvedMention, search: string): boolean {\n return (\n item.title.toLowerCase().includes(search) ||\n comment.body.toLowerCase().includes(search) ||\n comment.author.displayName?.toLowerCase().includes(search) === true\n );\n}\n\nfunction toMentionItem(scope: AttentionScope, { mention, comment, item, identity, receipt }: ResolvedMention) {\n const authorName = comment.author.displayName;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'mention' as const,\n commentId: mention.commentId,\n occurrence: 0,\n workItemId: mention.workItemId,\n title: item.title,\n detail: comment.body.slice(0, 512),\n authorId: comment.author.id,\n ...(authorName ? { authorName } : {}),\n occurredAt: mention.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: {\n kind: 'work-item' as const,\n workItemId: mention.workItemId,\n board: boardForWorkItem(item),\n commentId: mention.commentId,\n },\n };\n}\n\nexport function receiptScope(scope: AttentionScope): { orgId: string; factoryProjectId: string; userId: string } {\n return { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId, userId: scope.userId };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"attention-providers.js","names":[],"sources":["../../src/routes/attention-providers.ts"],"sourcesContent":["import { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { decisionRetryable, overtakenDecisionIds } from '../rules/decision-applicability.js';\n/**\n * Per-kind attention providers. Each kind owns its counts, its bounded page\n * scan, and its wire item shape; the route layer k-way merges provider pages\n * on `occurredAt desc` with one resumable cursor per kind.\n */\n\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type {\n WorkItemCommentRow,\n WorkItemCommentsStorage,\n WorkItemMentionRow,\n} from '../storage/domains/comments/base.js';\nimport type {\n FactoryAttentionIdentity,\n FactoryAttentionKind,\n FactoryAttentionReceiptRecord,\n FactoryDeferredDecisionRecord,\n FactoryDispatchStatus,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport {\n factoryAttentionKey,\n factoryDecisionAttentionIdentity,\n factoryMentionAttentionIdentity,\n} from '../storage/domains/work-items/base.js';\n\nexport type FactoryAttentionView = 'open' | 'unread' | 'archived';\n\nexport interface AttentionScope {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n}\n\nexport interface AttentionStreamPosition {\n occurredAt: Date;\n id: string;\n}\n\nexport interface AttentionEntry {\n occurredAt: Date;\n /** Position resuming the provider's stream right after this entry. */\n resumeCursor: AttentionStreamPosition;\n item: Record<string, unknown>;\n}\n\nexport interface AttentionPageArgs {\n view: FactoryAttentionView;\n search?: string;\n before?: AttentionStreamPosition;\n limit: number;\n}\n\nexport interface AttentionPageResult {\n entries: AttentionEntry[];\n /** More rows behind the scan once every returned entry is consumed. */\n hasMore: boolean;\n /** Resume point for a scan-budget stop past the last returned entry. */\n continuation?: AttentionStreamPosition;\n}\n\nexport interface AttentionCounts {\n open: number;\n unread: number;\n}\n\nexport interface AttentionLatest {\n key: string;\n at: Date;\n unread: boolean;\n}\n\nexport interface AttentionProvider {\n kind: FactoryAttentionKind;\n counts(scope: AttentionScope): Promise<AttentionCounts>;\n latest(scope: AttentionScope): Promise<AttentionLatest | null>;\n page(scope: AttentionScope, args: AttentionPageArgs): Promise<AttentionPageResult>;\n markAllRead(\n scope: AttentionScope,\n args: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }>;\n}\n\nexport const SCAN_PAGE_SIZE = 50;\n// Receipt filtering is bounded per request; the response cursor resumes after the last scan.\nconst MAX_RECEIPT_SCAN_PAGES = 4;\n\ninterface ScanBatch<T> {\n rows: T[];\n /** Set only when the page budget stopped a scan that still had rows behind it. */\n continuation?: AttentionStreamPosition;\n}\n\n/**\n * One bounded keyset scan, shared by every provider read. Walks newest-first\n * from `before` until the stream runs dry or the page budget is spent; the\n * final batch of a budget stop carries the position to resume from.\n */\nexport async function* scanBatches<T>(\n fetchBatch: (before?: AttentionStreamPosition) => Promise<{ rows: T[]; hasMore: boolean }>,\n positionOf: (row: T) => AttentionStreamPosition,\n before?: AttentionStreamPosition,\n): AsyncGenerator<ScanBatch<T>> {\n let cursor = before;\n for (let scanned = 1; scanned <= MAX_RECEIPT_SCAN_PAGES; scanned += 1) {\n const { rows, hasMore } = await fetchBatch(cursor);\n const last = rows.at(-1);\n if (!last) return;\n const position = positionOf(last);\n yield { rows, ...(hasMore && scanned === MAX_RECEIPT_SCAN_PAGES ? { continuation: position } : {}) };\n if (!hasMore) return;\n cursor = position;\n }\n}\n\n/** Fills a page from the scan, stopping at `limit` entries or at the scan budget. */\nexport async function collectPage<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n limit: number,\n toEntries: (rows: T[]) => Promise<AttentionEntry[]>,\n): Promise<AttentionPageResult> {\n const entries: AttentionEntry[] = [];\n for await (const batch of batches) {\n for (const entry of await toEntries(batch.rows)) {\n if (entries.length === limit) return { entries, hasMore: true };\n entries.push(entry);\n }\n if (batch.continuation) return { entries, hasMore: true, continuation: batch.continuation };\n }\n return { entries, hasMore: false };\n}\n\n/** Marks every scanned batch read, reporting where a budget stop left off. */\nexport async function markScanRead<T>(\n batches: AsyncGenerator<ScanBatch<T>>,\n markBatch: (rows: T[]) => Promise<void>,\n): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n for await (const batch of batches) {\n await markBatch(batch.rows);\n if (batch.continuation) return { hasMore: true, continuation: batch.continuation };\n }\n return { hasMore: false };\n}\n\nexport function factoryDecisionType(decision: FactoryDeferredDecisionRecord): string {\n return typeof decision.decision.type === 'string' ? decision.decision.type.slice(0, 64) : 'unknown';\n}\n\nexport function matchesView(view: FactoryAttentionView, receipt: FactoryAttentionReceiptRecord | undefined): boolean {\n if (view === 'archived') return receipt?.state === 'archived';\n if (view === 'unread') return receipt === undefined;\n return receipt?.state !== 'archived';\n}\n\nfunction attentionTarget(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined) {\n if (!item) return { kind: 'rules' as const };\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : undefined;\n const session = role ? item.sessions[role] : undefined;\n if (session) {\n return {\n kind: 'thread' as const,\n sessionId: session.sessionId,\n threadId: session.threadId,\n };\n }\n return {\n kind: 'work-item' as const,\n workItemId: item.id,\n board: boardForWorkItem(item),\n };\n}\n\n/** One deferred-decision status seen as attention: failures and parked runs differ only by this. */\nexport interface DecisionAttentionSpec {\n kind: FactoryAttentionKind;\n status: FactoryDispatchStatus;\n identity(decision: FactoryDeferredDecisionRecord): FactoryAttentionIdentity;\n occurredAt(decision: FactoryDeferredDecisionRecord): Date;\n title(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n detail(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string;\n extra?(decision: FactoryDeferredDecisionRecord, context: { overtaken: boolean }): Record<string, unknown>;\n matches(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined, search: string): boolean;\n}\n\nexport class DecisionAttentionProvider implements AttentionProvider {\n readonly kind: FactoryAttentionKind;\n readonly #workItems: WorkItemsStorage;\n readonly #spec: DecisionAttentionSpec;\n readonly #boards: BoardRegistry;\n\n constructor(\n { workItems, boards }: { workItems: WorkItemsStorage; boards: BoardRegistry },\n spec: DecisionAttentionSpec,\n ) {\n this.#workItems = workItems;\n this.#boards = boards;\n this.#spec = spec;\n this.kind = spec.kind;\n }\n\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n const [total, receiptCount, archivedCount] = await Promise.all([\n this.#workItems.countDeferredDecisionsByStatuses({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n statuses: [this.#spec.status],\n }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind }),\n this.#workItems.countAttentionReceipts({ ...receiptScope(scope), kind: this.kind, state: 'archived' }),\n ]);\n return {\n open: Math.max(0, total - archivedCount),\n unread: Math.max(0, total - receiptCount),\n };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const page = await this.#workItems.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n limit: 1,\n });\n const newest = page.decisions[0];\n if (!newest) return null;\n const identity = this.#spec.identity(newest);\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: [identity],\n });\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n at: this.#spec.occurredAt(newest),\n unread: receipts.length === 0,\n };\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<FactoryDeferredDecisionRecord>(\n async cursor => {\n const page = await this.#workItems.listDecisionPageByStatus({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n status: this.#spec.status,\n before: cursor,\n limit: SCAN_PAGE_SIZE,\n });\n return { rows: page.decisions, hasMore: page.hasMore };\n },\n decision => ({ occurredAt: this.#spec.occurredAt(decision), id: decision.id }),\n before,\n );\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async decisions => {\n const [receiptByKey, itemById, overtaken] = await Promise.all([\n this.#receiptsFor(scope, decisions),\n this.#linkedItems(scope, decisions),\n overtakenDecisionIds(this.#workItems, this.#boards, scope, decisions),\n ]);\n const entries: AttentionEntry[] = [];\n for (const decision of decisions) {\n const identity = this.#spec.identity(decision);\n const receipt = receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity));\n if (!matchesView(view, receipt)) continue;\n const item = decision.workItemId ? itemById.get(decision.workItemId) : undefined;\n if (search && !this.#spec.matches(decision, item, search)) continue;\n const occurredAt = this.#spec.occurredAt(decision);\n entries.push({\n occurredAt,\n resumeCursor: { occurredAt, id: decision.id },\n item: {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: this.kind,\n decisionId: decision.id,\n occurrence: identity.occurrence,\n workItemId: decision.workItemId,\n title: this.#spec.title(decision, item),\n detail: this.#spec.detail(decision, item),\n decisionType: factoryDecisionType(decision),\n ...this.#spec.extra?.(decision, { overtaken: overtaken.has(decision.id) }),\n occurredAt: occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: attentionTarget(decision, item),\n },\n });\n }\n return entries;\n });\n }\n\n async #receiptsFor(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, FactoryAttentionReceiptRecord>> {\n const receipts = await this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: decisions.map(decision => this.#spec.identity(decision)),\n });\n return new Map(receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]));\n }\n\n async #linkedItems(\n scope: AttentionScope,\n decisions: FactoryDeferredDecisionRecord[],\n ): Promise<Map<string, WorkItemRow>> {\n const items = await this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: decisions.flatMap(decision => (decision.workItemId ? [decision.workItemId] : [])),\n });\n return new Map(items.map(item => [item.id, item]));\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async decisions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: decisions.map(decision => this.#spec.identity(decision)),\n now,\n });\n });\n }\n}\n\nexport const failedDecisionAttentionSpec: DecisionAttentionSpec = {\n kind: 'automation-failed',\n status: 'failed',\n identity: decision => factoryDecisionAttentionIdentity(decision.id, decision.failureOccurrence),\n occurredAt: decision => decision.completedAt ?? decision.updatedAt,\n title: (decision, item) => item?.title ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n detail: decision => decision.lastError?.slice(0, 512) ?? factoryDispatchFailureMetadata(decision.failureCode).label,\n extra: (decision, { overtaken }) => ({\n failureCode: decision.failureCode,\n canRetry: decisionRetryable(decision, overtaken),\n }),\n matches: (decision, item, search) =>\n item?.title.toLowerCase().includes(search) === true ||\n decision.lastError?.toLowerCase().includes(search) === true ||\n factoryDecisionType(decision).toLowerCase().includes(search),\n};\n\ninterface ResolvedMention {\n mention: WorkItemMentionRow;\n comment: WorkItemCommentRow;\n item: WorkItemRow;\n identity: FactoryAttentionIdentity;\n receipt: FactoryAttentionReceiptRecord | undefined;\n}\n\nexport class MentionAttentionProvider implements AttentionProvider {\n readonly kind = 'mention' as const;\n readonly #workItems: WorkItemsStorage;\n readonly #comments: WorkItemCommentsStorage;\n\n constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage }) {\n this.#workItems = workItems;\n this.#comments = comments;\n }\n\n #scan(scope: AttentionScope, before?: AttentionStreamPosition) {\n return scanBatches<WorkItemMentionRow>(\n async cursor => {\n const rows = await this.#comments.listMentionsForUser({\n ...receiptScope(scope),\n ...(cursor ? { before: cursor } : {}),\n limit: SCAN_PAGE_SIZE + 1,\n });\n return { rows: rows.slice(0, SCAN_PAGE_SIZE), hasMore: rows.length > SCAN_PAGE_SIZE };\n },\n mention => ({ occurredAt: mention.occurredAt, id: mention.id }),\n before,\n );\n }\n\n /**\n * Mention rows joined to what the inbox needs to show one — dropping the\n * ones it cannot: a deleted comment, or a work item gone from the project.\n */\n async #resolve(scope: AttentionScope, mentions: WorkItemMentionRow[]): Promise<ResolvedMention[]> {\n const [receipts, comments, items] = await Promise.all([\n this.#workItems.listAttentionReceipts({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n }),\n this.#comments.listByIds({\n orgId: scope.orgId,\n ids: [...new Set(mentions.map(mention => mention.commentId))],\n }),\n this.#workItems.listByIds({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n ids: [...new Set(mentions.map(mention => mention.workItemId))],\n }),\n ]);\n const receiptByKey = new Map(\n receipts.map(receipt => [factoryAttentionKey(scope.factoryProjectId, receipt), receipt]),\n );\n const commentById = new Map(comments.map(comment => [comment.id, comment]));\n const itemById = new Map(items.map(item => [item.id, item]));\n\n const resolved: ResolvedMention[] = [];\n for (const mention of mentions) {\n const comment = commentById.get(mention.commentId);\n const item = itemById.get(mention.workItemId);\n if (!comment || comment.deletedAt || !item) continue;\n const identity = factoryMentionAttentionIdentity(mention.commentId);\n resolved.push({\n mention,\n comment,\n item,\n identity,\n receipt: receiptByKey.get(factoryAttentionKey(scope.factoryProjectId, identity)),\n });\n }\n return resolved;\n }\n\n /**\n * Derived from the same bounded scan as `page`, never from raw aggregates:\n * the badge must count exactly what the views can show, down to stopping at\n * the same scan budget. Aggregates would count orphan mention rows the list\n * can never display or clear.\n */\n async counts(scope: AttentionScope): Promise<AttentionCounts> {\n let open = 0;\n let unread = 0;\n for await (const batch of this.#scan(scope)) {\n for (const { receipt } of await this.#resolve(scope, batch.rows)) {\n if (receipt?.state !== 'archived') open += 1;\n if (receipt === undefined) unread += 1;\n }\n }\n return { open, unread };\n }\n\n async latest(scope: AttentionScope): Promise<AttentionLatest | null> {\n const mentions = await this.#comments.listMentionsForUser({ ...receiptScope(scope), limit: 10 });\n const newest = (await this.#resolve(scope, mentions))[0];\n if (!newest) return null;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, newest.identity),\n at: newest.mention.occurredAt,\n unread: newest.receipt === undefined,\n };\n }\n\n async page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise<AttentionPageResult> {\n return collectPage(this.#scan(scope, before), limit, async mentions => {\n const entries: AttentionEntry[] = [];\n for (const resolved of await this.#resolve(scope, mentions)) {\n if (!matchesView(view, resolved.receipt)) continue;\n if (search && !matchesMentionSearch(resolved, search)) continue;\n entries.push({\n occurredAt: resolved.mention.occurredAt,\n resumeCursor: { occurredAt: resolved.mention.occurredAt, id: resolved.mention.id },\n item: toMentionItem(scope, resolved),\n });\n }\n return entries;\n });\n }\n\n async markAllRead(\n scope: AttentionScope,\n { before, now }: { before?: AttentionStreamPosition; now: Date },\n ): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition }> {\n return markScanRead(this.#scan(scope, before), async mentions => {\n await this.#workItems.markAttentionReceiptsRead({\n ...receiptScope(scope),\n identities: mentions.map(mention => factoryMentionAttentionIdentity(mention.commentId)),\n now,\n });\n });\n }\n}\n\nfunction matchesMentionSearch({ item, comment }: ResolvedMention, search: string): boolean {\n return (\n item.title.toLowerCase().includes(search) ||\n comment.body.toLowerCase().includes(search) ||\n comment.author.displayName?.toLowerCase().includes(search) === true\n );\n}\n\nfunction toMentionItem(scope: AttentionScope, { mention, comment, item, identity, receipt }: ResolvedMention) {\n const authorName = comment.author.displayName;\n return {\n key: factoryAttentionKey(scope.factoryProjectId, identity),\n kind: 'mention' as const,\n commentId: mention.commentId,\n occurrence: 0,\n workItemId: mention.workItemId,\n title: item.title,\n detail: comment.body.slice(0, 512),\n authorId: comment.author.id,\n ...(authorName ? { authorName } : {}),\n occurredAt: mention.occurredAt.toISOString(),\n read: receipt !== undefined,\n archived: receipt?.state === 'archived',\n target: {\n kind: 'work-item' as const,\n workItemId: mention.workItemId,\n board: boardForWorkItem(item),\n commentId: mention.commentId,\n },\n };\n}\n\nexport function receiptScope(scope: AttentionScope): { orgId: string; factoryProjectId: string; userId: string } {\n return { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId, userId: scope.userId };\n}\n"],"mappings":";;;;;;;;;;;AAuFA,MAAa,iBAAiB;AAE9B,MAAM,yBAAyB;;;;;;AAa/B,gBAAuB,YACrB,YACA,YACA,QAC8B;CAC9B,IAAI,SAAS;CACb,KAAK,IAAI,UAAU,GAAG,WAAW,wBAAwB,WAAW,GAAG;EACrE,MAAM,EAAE,MAAM,YAAY,MAAM,WAAW,MAAM;EACjD,MAAM,OAAO,KAAK,GAAG,EAAE;EACvB,IAAI,CAAC,MAAM;EACX,MAAM,WAAW,WAAW,IAAI;EAChC,MAAM;GAAE;GAAM,GAAI,WAAW,YAAY,yBAAyB,EAAE,cAAc,SAAS,IAAI,CAAC;EAAG;EACnG,IAAI,CAAC,SAAS;EACd,SAAS;CACX;AACF;;AAGA,eAAsB,YACpB,SACA,OACA,WAC8B;CAC9B,MAAM,UAA4B,CAAC;CACnC,WAAW,MAAM,SAAS,SAAS;EACjC,KAAK,MAAM,SAAS,MAAM,UAAU,MAAM,IAAI,GAAG;GAC/C,IAAI,QAAQ,WAAW,OAAO,OAAO;IAAE;IAAS,SAAS;GAAK;GAC9D,QAAQ,KAAK,KAAK;EACpB;EACA,IAAI,MAAM,cAAc,OAAO;GAAE;GAAS,SAAS;GAAM,cAAc,MAAM;EAAa;CAC5F;CACA,OAAO;EAAE;EAAS,SAAS;CAAM;AACnC;;AAGA,eAAsB,aACpB,SACA,WACuE;CACvE,WAAW,MAAM,SAAS,SAAS;EACjC,MAAM,UAAU,MAAM,IAAI;EAC1B,IAAI,MAAM,cAAc,OAAO;GAAE,SAAS;GAAM,cAAc,MAAM;EAAa;CACnF;CACA,OAAO,EAAE,SAAS,MAAM;AAC1B;AAEA,SAAgB,oBAAoB,UAAiD;CACnF,OAAO,OAAO,SAAS,SAAS,SAAS,WAAW,SAAS,SAAS,KAAK,MAAM,GAAG,EAAE,IAAI;AAC5F;AAEA,SAAgB,YAAY,MAA4B,SAA6D;CACnH,IAAI,SAAS,YAAY,OAAO,SAAS,UAAU;CACnD,IAAI,SAAS,UAAU,OAAO,YAAY,KAAA;CAC1C,OAAO,SAAS,UAAU;AAC5B;AAEA,SAAS,gBAAgB,UAAyC,MAA+B;CAC/F,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,QAAiB;CAC3C,MAAM,OAAO,OAAO,SAAS,SAAS,SAAS,WAAW,SAAS,SAAS,OAAO,KAAA;CACnF,MAAM,UAAU,OAAO,KAAK,SAAS,QAAQ,KAAA;CAC7C,IAAI,SACF,OAAO;EACL,MAAM;EACN,WAAW,QAAQ;EACnB,UAAU,QAAQ;CACpB;CAEF,OAAO;EACL,MAAM;EACN,YAAY,KAAK;EACjB,OAAO,iBAAiB,IAAI;CAC9B;AACF;AAcA,IAAa,4BAAb,MAAoE;CAClE;CACA;CACA;CACA;CAEA,YACE,EAAE,WAAW,UACb,MACA;EACA,KAAK,aAAa;EAClB,KAAK,UAAU;EACf,KAAK,QAAQ;EACb,KAAK,OAAO,KAAK;CACnB;CAEA,MAAM,OAAO,OAAiD;EAC5D,MAAM,CAAC,OAAO,cAAc,iBAAiB,MAAM,QAAQ,IAAI;GAC7D,KAAK,WAAW,iCAAiC;IAC/C,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,UAAU,CAAC,KAAK,MAAM,MAAM;GAC9B,CAAC;GACD,KAAK,WAAW,uBAAuB;IAAE,GAAG,aAAa,KAAK;IAAG,MAAM,KAAK;GAAK,CAAC;GAClF,KAAK,WAAW,uBAAuB;IAAE,GAAG,aAAa,KAAK;IAAG,MAAM,KAAK;IAAM,OAAO;GAAW,CAAC;EACvG,CAAC;EACD,OAAO;GACL,MAAM,KAAK,IAAI,GAAG,QAAQ,aAAa;GACvC,QAAQ,KAAK,IAAI,GAAG,QAAQ,YAAY;EAC1C;CACF;CAEA,MAAM,OAAO,OAAwD;EAOnE,MAAM,UAAS,MANI,KAAK,WAAW,yBAAyB;GAC1D,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB,QAAQ,KAAK,MAAM;GACnB,OAAO;EACT,CAAC,EAAA,CACmB,UAAU;EAC9B,IAAI,CAAC,QAAQ,OAAO;EACpB,MAAM,WAAW,KAAK,MAAM,SAAS,MAAM;EAC3C,MAAM,WAAW,MAAM,KAAK,WAAW,sBAAsB;GAC3D,GAAG,aAAa,KAAK;GACrB,YAAY,CAAC,QAAQ;EACvB,CAAC;EACD,OAAO;GACL,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;GACzD,IAAI,KAAK,MAAM,WAAW,MAAM;GAChC,QAAQ,SAAS,WAAW;EAC9B;CACF;CAEA,MAAM,OAAuB,QAAkC;EAC7D,OAAO,YACL,OAAM,WAAU;GACd,MAAM,OAAO,MAAM,KAAK,WAAW,yBAAyB;IAC1D,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,QAAQ,KAAK,MAAM;IACnB,QAAQ;IACR,OAAA;GACF,CAAC;GACD,OAAO;IAAE,MAAM,KAAK;IAAW,SAAS,KAAK;GAAQ;EACvD,IACA,cAAa;GAAE,YAAY,KAAK,MAAM,WAAW,QAAQ;GAAG,IAAI,SAAS;EAAG,IAC5E,MACF;CACF;CAEA,MAAM,KAAK,OAAuB,EAAE,MAAM,QAAQ,QAAQ,SAA0D;EAClH,OAAO,YAAY,KAAK,MAAM,OAAO,MAAM,GAAG,OAAO,OAAM,cAAa;GACtE,MAAM,CAAC,cAAc,UAAU,aAAa,MAAM,QAAQ,IAAI;IAC5D,KAAK,aAAa,OAAO,SAAS;IAClC,KAAK,aAAa,OAAO,SAAS;IAClC,qBAAqB,KAAK,YAAY,KAAK,SAAS,OAAO,SAAS;GACtE,CAAC;GACD,MAAM,UAA4B,CAAC;GACnC,KAAK,MAAM,YAAY,WAAW;IAChC,MAAM,WAAW,KAAK,MAAM,SAAS,QAAQ;IAC7C,MAAM,UAAU,aAAa,IAAI,oBAAoB,MAAM,kBAAkB,QAAQ,CAAC;IACtF,IAAI,CAAC,YAAY,MAAM,OAAO,GAAG;IACjC,MAAM,OAAO,SAAS,aAAa,SAAS,IAAI,SAAS,UAAU,IAAI,KAAA;IACvE,IAAI,UAAU,CAAC,KAAK,MAAM,QAAQ,UAAU,MAAM,MAAM,GAAG;IAC3D,MAAM,aAAa,KAAK,MAAM,WAAW,QAAQ;IACjD,QAAQ,KAAK;KACX;KACA,cAAc;MAAE;MAAY,IAAI,SAAS;KAAG;KAC5C,MAAM;MACJ,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;MACzD,MAAM,KAAK;MACX,YAAY,SAAS;MACrB,YAAY,SAAS;MACrB,YAAY,SAAS;MACrB,OAAO,KAAK,MAAM,MAAM,UAAU,IAAI;MACtC,QAAQ,KAAK,MAAM,OAAO,UAAU,IAAI;MACxC,cAAc,oBAAoB,QAAQ;MAC1C,GAAG,KAAK,MAAM,QAAQ,UAAU,EAAE,WAAW,UAAU,IAAI,SAAS,EAAE,EAAE,CAAC;MACzE,YAAY,WAAW,YAAY;MACnC,MAAM,YAAY,KAAA;MAClB,UAAU,SAAS,UAAU;MAC7B,QAAQ,gBAAgB,UAAU,IAAI;KACxC;IACF,CAAC;GACH;GACA,OAAO;EACT,CAAC;CACH;CAEA,MAAM,aACJ,OACA,WACqD;EACrD,MAAM,WAAW,MAAM,KAAK,WAAW,sBAAsB;GAC3D,GAAG,aAAa,KAAK;GACrB,YAAY,UAAU,KAAI,aAAY,KAAK,MAAM,SAAS,QAAQ,CAAC;EACrE,CAAC;EACD,OAAO,IAAI,IAAI,SAAS,KAAI,YAAW,CAAC,oBAAoB,MAAM,kBAAkB,OAAO,GAAG,OAAO,CAAC,CAAC;CACzG;CAEA,MAAM,aACJ,OACA,WACmC;EACnC,MAAM,QAAQ,MAAM,KAAK,WAAW,UAAU;GAC5C,OAAO,MAAM;GACb,kBAAkB,MAAM;GACxB,KAAK,UAAU,SAAQ,aAAa,SAAS,aAAa,CAAC,SAAS,UAAU,IAAI,CAAC,CAAE;EACvF,CAAC;EACD,OAAO,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;CACnD;CAEA,MAAM,YACJ,OACA,EAAE,QAAQ,OAC6D;EACvE,OAAO,aAAa,KAAK,MAAM,OAAO,MAAM,GAAG,OAAM,cAAa;GAChE,MAAM,KAAK,WAAW,0BAA0B;IAC9C,GAAG,aAAa,KAAK;IACrB,YAAY,UAAU,KAAI,aAAY,KAAK,MAAM,SAAS,QAAQ,CAAC;IACnE;GACF,CAAC;EACH,CAAC;CACH;AACF;AAEA,MAAa,8BAAqD;CAChE,MAAM;CACN,QAAQ;CACR,WAAU,aAAY,iCAAiC,SAAS,IAAI,SAAS,iBAAiB;CAC9F,aAAY,aAAY,SAAS,eAAe,SAAS;CACzD,QAAQ,UAAU,SAAS,MAAM,SAAS,+BAA+B,SAAS,WAAW,CAAC,CAAC;CAC/F,SAAQ,aAAY,SAAS,WAAW,MAAM,GAAG,GAAG,KAAK,+BAA+B,SAAS,WAAW,CAAC,CAAC;CAC9G,QAAQ,UAAU,EAAE,iBAAiB;EACnC,aAAa,SAAS;EACtB,UAAU,kBAAkB,UAAU,SAAS;CACjD;CACA,UAAU,UAAU,MAAM,WACxB,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM,QAC/C,SAAS,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM,QACvD,oBAAoB,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,SAAS,MAAM;AAC/D;AAUA,IAAa,2BAAb,MAAmE;CACjE,OAAgB;CAChB;CACA;CAEA,YAAY,EAAE,WAAW,YAAgF;EACvG,KAAK,aAAa;EAClB,KAAK,YAAY;CACnB;CAEA,MAAM,OAAuB,QAAkC;EAC7D,OAAO,YACL,OAAM,WAAU;GACd,MAAM,OAAO,MAAM,KAAK,UAAU,oBAAoB;IACpD,GAAG,aAAa,KAAK;IACrB,GAAI,SAAS,EAAE,QAAQ,OAAO,IAAI,CAAC;IACnC,OAAO;GACT,CAAC;GACD,OAAO;IAAE,MAAM,KAAK,MAAM,GAAA,EAAiB;IAAG,SAAS,KAAK,SAAA;GAAwB;EACtF,IACA,aAAY;GAAE,YAAY,QAAQ;GAAY,IAAI,QAAQ;EAAG,IAC7D,MACF;CACF;;;;;CAMA,MAAM,SAAS,OAAuB,UAA4D;EAChG,MAAM,CAAC,UAAU,UAAU,SAAS,MAAM,QAAQ,IAAI;GACpD,KAAK,WAAW,sBAAsB;IACpC,GAAG,aAAa,KAAK;IACrB,YAAY,SAAS,KAAI,YAAW,gCAAgC,QAAQ,SAAS,CAAC;GACxF,CAAC;GACD,KAAK,UAAU,UAAU;IACvB,OAAO,MAAM;IACb,KAAK,CAAC,GAAG,IAAI,IAAI,SAAS,KAAI,YAAW,QAAQ,SAAS,CAAC,CAAC;GAC9D,CAAC;GACD,KAAK,WAAW,UAAU;IACxB,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,KAAK,CAAC,GAAG,IAAI,IAAI,SAAS,KAAI,YAAW,QAAQ,UAAU,CAAC,CAAC;GAC/D,CAAC;EACH,CAAC;EACD,MAAM,eAAe,IAAI,IACvB,SAAS,KAAI,YAAW,CAAC,oBAAoB,MAAM,kBAAkB,OAAO,GAAG,OAAO,CAAC,CACzF;EACA,MAAM,cAAc,IAAI,IAAI,SAAS,KAAI,YAAW,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;EAC1E,MAAM,WAAW,IAAI,IAAI,MAAM,KAAI,SAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;EAE3D,MAAM,WAA8B,CAAC;EACrC,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,UAAU,YAAY,IAAI,QAAQ,SAAS;GACjD,MAAM,OAAO,SAAS,IAAI,QAAQ,UAAU;GAC5C,IAAI,CAAC,WAAW,QAAQ,aAAa,CAAC,MAAM;GAC5C,MAAM,WAAW,gCAAgC,QAAQ,SAAS;GAClE,SAAS,KAAK;IACZ;IACA;IACA;IACA;IACA,SAAS,aAAa,IAAI,oBAAoB,MAAM,kBAAkB,QAAQ,CAAC;GACjF,CAAC;EACH;EACA,OAAO;CACT;;;;;;;CAQA,MAAM,OAAO,OAAiD;EAC5D,IAAI,OAAO;EACX,IAAI,SAAS;EACb,WAAW,MAAM,SAAS,KAAK,MAAM,KAAK,GACxC,KAAK,MAAM,EAAE,aAAa,MAAM,KAAK,SAAS,OAAO,MAAM,IAAI,GAAG;GAChE,IAAI,SAAS,UAAU,YAAY,QAAQ;GAC3C,IAAI,YAAY,KAAA,GAAW,UAAU;EACvC;EAEF,OAAO;GAAE;GAAM;EAAO;CACxB;CAEA,MAAM,OAAO,OAAwD;EACnE,MAAM,WAAW,MAAM,KAAK,UAAU,oBAAoB;GAAE,GAAG,aAAa,KAAK;GAAG,OAAO;EAAG,CAAC;EAC/F,MAAM,UAAU,MAAM,KAAK,SAAS,OAAO,QAAQ,EAAA,CAAG;EACtD,IAAI,CAAC,QAAQ,OAAO;EACpB,OAAO;GACL,KAAK,oBAAoB,MAAM,kBAAkB,OAAO,QAAQ;GAChE,IAAI,OAAO,QAAQ;GACnB,QAAQ,OAAO,YAAY,KAAA;EAC7B;CACF;CAEA,MAAM,KAAK,OAAuB,EAAE,MAAM,QAAQ,QAAQ,SAA0D;EAClH,OAAO,YAAY,KAAK,MAAM,OAAO,MAAM,GAAG,OAAO,OAAM,aAAY;GACrE,MAAM,UAA4B,CAAC;GACnC,KAAK,MAAM,YAAY,MAAM,KAAK,SAAS,OAAO,QAAQ,GAAG;IAC3D,IAAI,CAAC,YAAY,MAAM,SAAS,OAAO,GAAG;IAC1C,IAAI,UAAU,CAAC,qBAAqB,UAAU,MAAM,GAAG;IACvD,QAAQ,KAAK;KACX,YAAY,SAAS,QAAQ;KAC7B,cAAc;MAAE,YAAY,SAAS,QAAQ;MAAY,IAAI,SAAS,QAAQ;KAAG;KACjF,MAAM,cAAc,OAAO,QAAQ;IACrC,CAAC;GACH;GACA,OAAO;EACT,CAAC;CACH;CAEA,MAAM,YACJ,OACA,EAAE,QAAQ,OAC6D;EACvE,OAAO,aAAa,KAAK,MAAM,OAAO,MAAM,GAAG,OAAM,aAAY;GAC/D,MAAM,KAAK,WAAW,0BAA0B;IAC9C,GAAG,aAAa,KAAK;IACrB,YAAY,SAAS,KAAI,YAAW,gCAAgC,QAAQ,SAAS,CAAC;IACtF;GACF,CAAC;EACH,CAAC;CACH;AACF;AAEA,SAAS,qBAAqB,EAAE,MAAM,WAA4B,QAAyB;CACzF,OACE,KAAK,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,KACxC,QAAQ,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,KAC1C,QAAQ,OAAO,aAAa,YAAY,CAAC,CAAC,SAAS,MAAM,MAAM;AAEnE;AAEA,SAAS,cAAc,OAAuB,EAAE,SAAS,SAAS,MAAM,UAAU,WAA4B;CAC5G,MAAM,aAAa,QAAQ,OAAO;CAClC,OAAO;EACL,KAAK,oBAAoB,MAAM,kBAAkB,QAAQ;EACzD,MAAM;EACN,WAAW,QAAQ;EACnB,YAAY;EACZ,YAAY,QAAQ;EACpB,OAAO,KAAK;EACZ,QAAQ,QAAQ,KAAK,MAAM,GAAG,GAAG;EACjC,UAAU,QAAQ,OAAO;EACzB,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC,YAAY,QAAQ,WAAW,YAAY;EAC3C,MAAM,YAAY,KAAA;EAClB,UAAU,SAAS,UAAU;EAC7B,QAAQ;GACN,MAAM;GACN,YAAY,QAAQ;GACpB,OAAO,iBAAiB,IAAI;GAC5B,WAAW,QAAQ;EACrB;CACF;AACF;AAEA,SAAgB,aAAa,OAAoF;CAC/G,OAAO;EAAE,OAAO,MAAM;EAAO,kBAAkB,MAAM;EAAkB,QAAQ,MAAM;CAAO;AAC9F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-runs.js","names":["#resolveScope","#resolveProject","#writeAudit"],"sources":["../../src/routes/automation-runs.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for trusted external orchestrators to enqueue an\n * idempotent, deferred skill dispatch against a Factory work item.\n *\n * The endpoint is a constrained ingress in front of\n * `WorkItemsStorage.commitRuleEvaluation()`: it commits exactly one\n * `invokeSkill` decision, stamps a non-human system actor, and leaves\n * execution (session creation, auto-run/approval policy, retry, restart\n * recovery) to the `FactoryDecisionDispatcher`.\n *\n * Idempotency: the `requestId` becomes the ingress identity, so replaying the\n * same request returns the prior result without inserting a second decision.\n * Tenant-mode callers must administer the organization; local (no-auth)\n * deployments run under the shared `local` storage scope without inventing a\n * tenant user.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { AuditAction } from '../storage/domains/audit/actions.js';\nimport type { AuditTarget } from '../storage/domains/audit/base.js';\nimport type { AuditEmitter, AuditRecorder } from '../storage/domains/audit/domain.js';\nimport { auditRequestContext } from '../storage/domains/audit/domain.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_ROUTE_CONTRACTS } from './contracts.js';\nimport { Route } from './route.js';\nimport type { RouteDependencies } from './route.js';\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/** The invokeSkill payload this route commits, matched against a prior replay. */\ninterface AutomationInvokeSkillDecision {\n type: 'invokeSkill';\n idempotencyKey: string;\n role: string;\n skillName: string;\n arguments?: string;\n}\n\n/**\n * Whether a replayed rule-evaluation record describes the SAME operation as the\n * current request. `commitRuleEvaluation` keys idempotency on `(org, project,\n * ingress identity)` alone, so a reused `requestId` replays regardless of the\n * work item or decision payload. We reconstruct the prior `invokeSkill`\n * decision from the stored result and compare the item id, role, skill, and\n * arguments; any divergence is a collision, not a legitimate retry.\n */\nfunction replayMatchesRequest(\n result: { itemId?: string | null; decisions?: unknown[] },\n itemId: string,\n request: AutomationInvokeSkillDecision,\n): boolean {\n if (result.itemId != null && result.itemId !== itemId) return false;\n const decisions = Array.isArray(result.decisions) ? result.decisions : [];\n const prior = decisions.find(\n (candidate): candidate is Record<string, unknown> =>\n typeof candidate === 'object' &&\n candidate !== null &&\n (candidate as Record<string, unknown>).type === 'invokeSkill' &&\n (candidate as Record<string, unknown>).idempotencyKey === request.idempotencyKey,\n );\n if (!prior) return false;\n return (\n prior.role === request.role &&\n prior.skillName === request.skillName &&\n (prior.arguments ?? undefined) === (request.arguments ?? undefined)\n );\n}\n\nexport interface AutomationRunRoutesDeps extends RouteDependencies {\n audit: AuditEmitter & AuditRecorder;\n /** Factory projects domain — validates the `:id` project belongs to the caller's org. */\n projects: FactoryProjectsStorage;\n /** Work-items domain backing `commitRuleEvaluation`. */\n workItems: WorkItemsStorage;\n /** Config version stamped on the committed rule evaluation. */\n configVersion: string;\n}\n\n/** Resolved ingress scope for an automation-run request. */\ninterface AutomationRunScope {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n}\n\nexport class AutomationRunRoutes extends Route<AutomationRunRoutesDeps> {\n /**\n * Resolve the `(orgId, userId)` ingress scope. Tenant mode requires a\n * signed-in organization administrator; local (no-auth) mode runs under the\n * shared `local` scope without inventing a user.\n */\n async #resolveScope(c: Context): Promise<{ orgId: string; userId: string } | { response: Response }> {\n const { auth } = this.deps;\n if (!auth.enabled()) {\n return { orgId: 'local', userId: 'local' };\n }\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n { error: 'organization_required', message: 'The Factory board requires an organization.' },\n 403,\n ),\n };\n }\n if (!(await auth.isOrganizationAdmin(c, tenant.orgId))) {\n return {\n response: c.json(\n { error: 'forbidden', message: 'Organization administrator access is required for automation ingress.' },\n 403,\n ),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n /** Resolve the ingress scope AND the org-owned project from the `:id` param. */\n async #resolveProject(c: Context): Promise<AutomationRunScope | { response: Response }> {\n const scope = await this.#resolveScope(c);\n if ('response' in scope) return scope;\n\n const parsedPath = FACTORY_ROUTE_CONTRACTS.projectGet.pathSchema.safeParse({ id: c.req.param('id') });\n if (!parsedPath.success) {\n return { response: c.json({ error: 'Project not found' }, 404) };\n }\n const { projects } = this.deps;\n await projects.ensureReady();\n const project = await projects.get({ orgId: scope.orgId, id: parsedPath.data.id });\n if (!project) {\n return { response: c.json({ error: 'Project not found' }, 404) };\n }\n return { ...scope, factoryProjectId: parsedPath.data.id };\n }\n\n /**\n * Persist an audit event under the ingress-resolved scope. This route can run\n * under the synthetic `local` no-auth scope, where the tenant-gated\n * `audit.emit()` derives no org from the request and drops the event; writing\n * through `record()` with the explicit org + system actor keeps traceability\n * in every deployment mode.\n *\n * The `idempotencyKey` (`<action>:<requestId>`) makes the audit write itself\n * idempotent: replaying the same request re-enters this path, but the trail\n * keeps a single event per requestId instead of one duplicate per retry.\n */\n async #writeAudit(\n c: Context,\n scope: AutomationRunScope,\n action: AuditAction,\n requestId: string,\n targets: AuditTarget[],\n metadata: Record<string, unknown>,\n ): Promise<void> {\n await this.deps.audit.record({\n idempotencyKey: `${action}:${requestId}`,\n orgId: scope.orgId,\n actorId: 'factory-external-orchestrator',\n actorType: 'system',\n action,\n targets,\n metadata,\n factoryProjectId: scope.factoryProjectId,\n context: auditRequestContext(c),\n });\n }\n\n routes(): ApiRoute[] {\n const { workItems, configVersion } = this.deps;\n const contract = FACTORY_ROUTE_CONTRACTS.workItemAutomationRun;\n\n return [\n registerApiRoute(contract.path, {\n method: contract.method,\n handler: async c => {\n const context = loose(c);\n const resolved = await this.#resolveProject(context);\n if ('response' in resolved) return resolved.response;\n\n const parsedPath = contract.pathSchema.safeParse({\n id: c.req.param('id'),\n workItemId: c.req.param('workItemId'),\n });\n if (!parsedPath.success) return c.json({ error: 'Work item not found' }, 404);\n\n const body = await c.req.json().catch(() => undefined);\n if (body === undefined) return c.json({ error: 'Invalid JSON body' }, 400);\n const parsed = contract.bodySchema.safeParse(body);\n if (!parsed.success) return c.json({ error: 'invalid_automation_run_request' }, 400);\n const request = parsed.data as {\n requestId: string;\n expectedRevision: number;\n role: string;\n skillName: string;\n arguments?: string;\n };\n\n const item = await workItems.getForProject(\n resolved.orgId,\n resolved.factoryProjectId,\n (parsedPath.data as { workItemId: string }).workItemId,\n );\n if (!item) return c.json({ error: 'Work item not found' }, 404);\n\n const idempotencyKey = `external-orchestrator:${request.requestId}`;\n const decision = {\n type: 'invokeSkill' as const,\n idempotencyKey,\n role: request.role,\n skillName: request.skillName,\n ...(request.arguments !== undefined ? { arguments: request.arguments } : {}),\n };\n\n const now = new Date();\n const commit = await workItems.commitRuleEvaluation({\n orgId: resolved.orgId,\n factoryProjectId: resolved.factoryProjectId,\n workItemId: item.id,\n ingress: { identity: idempotencyKey, triggerType: 'external-orchestrator.invoke-skill' },\n configVersion,\n expectedRevision: request.expectedRevision,\n actor: { type: 'system', id: 'factory-external-orchestrator' },\n outcome: { status: 'accepted' },\n decisions: [decision],\n causalChain: [],\n now,\n });\n\n if (commit.status === 'missing') return c.json({ error: 'Work item not found' }, 404);\n\n // A stale commit persists nothing, so the caller can retry the same\n // requestId once it has re-read the work item's revision.\n const result = (commit.status === 'stale' ? { status: 'rejected', code: 'stale' } : commit.result) as {\n status?: string;\n code?: string;\n itemId?: string | null;\n decisions?: unknown[];\n };\n const resultStatus = result.status ?? 'accepted';\n const code = typeof result.code === 'string' ? result.code : undefined;\n const targets = [{ type: 'work_item' as const, id: item.id, name: item.title }];\n\n const auditMetadata = {\n requestId: request.requestId,\n role: request.role,\n skillName: request.skillName,\n commitStatus: commit.status,\n resultStatus,\n ...(code ? { code } : {}),\n };\n\n // A replay only means \"this request was already committed\" — it must\n // be the SAME operation. If the caller reused a requestId against a\n // different work item, role, skill, or arguments, returning\n // `replayed` would report durable success for work that was never\n // enqueued. Reject the collision instead of silently swallowing it.\n if (commit.status === 'replayed' && !replayMatchesRequest(result, item.id, decision)) {\n await this.#writeAudit(context, resolved, 'factory.run.rejected', request.requestId, targets, {\n ...auditMetadata,\n resultStatus: 'rejected',\n code: 'request_id_conflict',\n });\n return c.json(\n {\n status: 'rejected',\n code: 'request_id_conflict',\n requestId: request.requestId,\n },\n 409,\n );\n }\n\n if (resultStatus === 'accepted') {\n await this.#writeAudit(context, resolved, 'factory.run.queued', request.requestId, targets, auditMetadata);\n return c.json(\n { status: commit.status === 'replayed' ? 'replayed' : 'committed', requestId: request.requestId },\n commit.status === 'replayed' ? 200 : 202,\n );\n }\n\n await this.#writeAudit(context, resolved, 'factory.run.rejected', request.requestId, targets, auditMetadata);\n return c.json(\n { status: 'rejected', ...(code ? { code } : {}), requestId: request.requestId },\n code === 'stale' ? 409 : 422,\n );\n },\n }),\n ];\n }\n}\n\nexport function buildAutomationRunRoutes(deps: AutomationRunRoutesDeps): ApiRoute[] {\n return new AutomationRunRoutes(deps).routes();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"automation-runs.js","names":[],"sources":["../../src/routes/automation-runs.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for trusted external orchestrators to enqueue an\n * idempotent, deferred skill dispatch against a Factory work item.\n *\n * The endpoint is a constrained ingress in front of\n * `WorkItemsStorage.commitRuleEvaluation()`: it commits exactly one\n * `invokeSkill` decision, stamps a non-human system actor, and leaves\n * execution (session creation, auto-run/approval policy, retry, restart\n * recovery) to the `FactoryDecisionDispatcher`.\n *\n * Idempotency: the `requestId` becomes the ingress identity, so replaying the\n * same request returns the prior result without inserting a second decision.\n * Tenant-mode callers must administer the organization; local (no-auth)\n * deployments run under the shared `local` storage scope without inventing a\n * tenant user.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { AuditAction } from '../storage/domains/audit/actions.js';\nimport type { AuditTarget } from '../storage/domains/audit/base.js';\nimport type { AuditEmitter, AuditRecorder } from '../storage/domains/audit/domain.js';\nimport { auditRequestContext } from '../storage/domains/audit/domain.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { FACTORY_ROUTE_CONTRACTS } from './contracts.js';\nimport { Route } from './route.js';\nimport type { RouteDependencies } from './route.js';\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/** The invokeSkill payload this route commits, matched against a prior replay. */\ninterface AutomationInvokeSkillDecision {\n type: 'invokeSkill';\n idempotencyKey: string;\n role: string;\n skillName: string;\n arguments?: string;\n}\n\n/**\n * Whether a replayed rule-evaluation record describes the SAME operation as the\n * current request. `commitRuleEvaluation` keys idempotency on `(org, project,\n * ingress identity)` alone, so a reused `requestId` replays regardless of the\n * work item or decision payload. We reconstruct the prior `invokeSkill`\n * decision from the stored result and compare the item id, role, skill, and\n * arguments; any divergence is a collision, not a legitimate retry.\n */\nfunction replayMatchesRequest(\n result: { itemId?: string | null; decisions?: unknown[] },\n itemId: string,\n request: AutomationInvokeSkillDecision,\n): boolean {\n if (result.itemId != null && result.itemId !== itemId) return false;\n const decisions = Array.isArray(result.decisions) ? result.decisions : [];\n const prior = decisions.find(\n (candidate): candidate is Record<string, unknown> =>\n typeof candidate === 'object' &&\n candidate !== null &&\n (candidate as Record<string, unknown>).type === 'invokeSkill' &&\n (candidate as Record<string, unknown>).idempotencyKey === request.idempotencyKey,\n );\n if (!prior) return false;\n return (\n prior.role === request.role &&\n prior.skillName === request.skillName &&\n (prior.arguments ?? undefined) === (request.arguments ?? undefined)\n );\n}\n\nexport interface AutomationRunRoutesDeps extends RouteDependencies {\n audit: AuditEmitter & AuditRecorder;\n /** Factory projects domain — validates the `:id` project belongs to the caller's org. */\n projects: FactoryProjectsStorage;\n /** Work-items domain backing `commitRuleEvaluation`. */\n workItems: WorkItemsStorage;\n /** Config version stamped on the committed rule evaluation. */\n configVersion: string;\n}\n\n/** Resolved ingress scope for an automation-run request. */\ninterface AutomationRunScope {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n}\n\nexport class AutomationRunRoutes extends Route<AutomationRunRoutesDeps> {\n /**\n * Resolve the `(orgId, userId)` ingress scope. Tenant mode requires a\n * signed-in organization administrator; local (no-auth) mode runs under the\n * shared `local` scope without inventing a user.\n */\n async #resolveScope(c: Context): Promise<{ orgId: string; userId: string } | { response: Response }> {\n const { auth } = this.deps;\n if (!auth.enabled()) {\n return { orgId: 'local', userId: 'local' };\n }\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n { error: 'organization_required', message: 'The Factory board requires an organization.' },\n 403,\n ),\n };\n }\n if (!(await auth.isOrganizationAdmin(c, tenant.orgId))) {\n return {\n response: c.json(\n { error: 'forbidden', message: 'Organization administrator access is required for automation ingress.' },\n 403,\n ),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n /** Resolve the ingress scope AND the org-owned project from the `:id` param. */\n async #resolveProject(c: Context): Promise<AutomationRunScope | { response: Response }> {\n const scope = await this.#resolveScope(c);\n if ('response' in scope) return scope;\n\n const parsedPath = FACTORY_ROUTE_CONTRACTS.projectGet.pathSchema.safeParse({ id: c.req.param('id') });\n if (!parsedPath.success) {\n return { response: c.json({ error: 'Project not found' }, 404) };\n }\n const { projects } = this.deps;\n await projects.ensureReady();\n const project = await projects.get({ orgId: scope.orgId, id: parsedPath.data.id });\n if (!project) {\n return { response: c.json({ error: 'Project not found' }, 404) };\n }\n return { ...scope, factoryProjectId: parsedPath.data.id };\n }\n\n /**\n * Persist an audit event under the ingress-resolved scope. This route can run\n * under the synthetic `local` no-auth scope, where the tenant-gated\n * `audit.emit()` derives no org from the request and drops the event; writing\n * through `record()` with the explicit org + system actor keeps traceability\n * in every deployment mode.\n *\n * The `idempotencyKey` (`<action>:<requestId>`) makes the audit write itself\n * idempotent: replaying the same request re-enters this path, but the trail\n * keeps a single event per requestId instead of one duplicate per retry.\n */\n async #writeAudit(\n c: Context,\n scope: AutomationRunScope,\n action: AuditAction,\n requestId: string,\n targets: AuditTarget[],\n metadata: Record<string, unknown>,\n ): Promise<void> {\n await this.deps.audit.record({\n idempotencyKey: `${action}:${requestId}`,\n orgId: scope.orgId,\n actorId: 'factory-external-orchestrator',\n actorType: 'system',\n action,\n targets,\n metadata,\n factoryProjectId: scope.factoryProjectId,\n context: auditRequestContext(c),\n });\n }\n\n routes(): ApiRoute[] {\n const { workItems, configVersion } = this.deps;\n const contract = FACTORY_ROUTE_CONTRACTS.workItemAutomationRun;\n\n return [\n registerApiRoute(contract.path, {\n method: contract.method,\n handler: async c => {\n const context = loose(c);\n const resolved = await this.#resolveProject(context);\n if ('response' in resolved) return resolved.response;\n\n const parsedPath = contract.pathSchema.safeParse({\n id: c.req.param('id'),\n workItemId: c.req.param('workItemId'),\n });\n if (!parsedPath.success) return c.json({ error: 'Work item not found' }, 404);\n\n const body = await c.req.json().catch(() => undefined);\n if (body === undefined) return c.json({ error: 'Invalid JSON body' }, 400);\n const parsed = contract.bodySchema.safeParse(body);\n if (!parsed.success) return c.json({ error: 'invalid_automation_run_request' }, 400);\n const request = parsed.data as {\n requestId: string;\n expectedRevision: number;\n role: string;\n skillName: string;\n arguments?: string;\n };\n\n const item = await workItems.getForProject(\n resolved.orgId,\n resolved.factoryProjectId,\n (parsedPath.data as { workItemId: string }).workItemId,\n );\n if (!item) return c.json({ error: 'Work item not found' }, 404);\n\n const idempotencyKey = `external-orchestrator:${request.requestId}`;\n const decision = {\n type: 'invokeSkill' as const,\n idempotencyKey,\n role: request.role,\n skillName: request.skillName,\n ...(request.arguments !== undefined ? { arguments: request.arguments } : {}),\n };\n\n const now = new Date();\n const commit = await workItems.commitRuleEvaluation({\n orgId: resolved.orgId,\n factoryProjectId: resolved.factoryProjectId,\n workItemId: item.id,\n ingress: { identity: idempotencyKey, triggerType: 'external-orchestrator.invoke-skill' },\n configVersion,\n expectedRevision: request.expectedRevision,\n actor: { type: 'system', id: 'factory-external-orchestrator' },\n outcome: { status: 'accepted' },\n decisions: [decision],\n causalChain: [],\n now,\n });\n\n if (commit.status === 'missing') return c.json({ error: 'Work item not found' }, 404);\n\n // A stale commit persists nothing, so the caller can retry the same\n // requestId once it has re-read the work item's revision.\n const result = (commit.status === 'stale' ? { status: 'rejected', code: 'stale' } : commit.result) as {\n status?: string;\n code?: string;\n itemId?: string | null;\n decisions?: unknown[];\n };\n const resultStatus = result.status ?? 'accepted';\n const code = typeof result.code === 'string' ? result.code : undefined;\n const targets = [{ type: 'work_item' as const, id: item.id, name: item.title }];\n\n const auditMetadata = {\n requestId: request.requestId,\n role: request.role,\n skillName: request.skillName,\n commitStatus: commit.status,\n resultStatus,\n ...(code ? { code } : {}),\n };\n\n // A replay only means \"this request was already committed\" — it must\n // be the SAME operation. If the caller reused a requestId against a\n // different work item, role, skill, or arguments, returning\n // `replayed` would report durable success for work that was never\n // enqueued. Reject the collision instead of silently swallowing it.\n if (commit.status === 'replayed' && !replayMatchesRequest(result, item.id, decision)) {\n await this.#writeAudit(context, resolved, 'factory.run.rejected', request.requestId, targets, {\n ...auditMetadata,\n resultStatus: 'rejected',\n code: 'request_id_conflict',\n });\n return c.json(\n {\n status: 'rejected',\n code: 'request_id_conflict',\n requestId: request.requestId,\n },\n 409,\n );\n }\n\n if (resultStatus === 'accepted') {\n await this.#writeAudit(context, resolved, 'factory.run.queued', request.requestId, targets, auditMetadata);\n return c.json(\n { status: commit.status === 'replayed' ? 'replayed' : 'committed', requestId: request.requestId },\n commit.status === 'replayed' ? 200 : 202,\n );\n }\n\n await this.#writeAudit(context, resolved, 'factory.run.rejected', request.requestId, targets, auditMetadata);\n return c.json(\n { status: 'rejected', ...(code ? { code } : {}), requestId: request.requestId },\n code === 'stale' ? 409 : 422,\n );\n },\n }),\n ];\n }\n}\n\nexport function buildAutomationRunRoutes(deps: AutomationRunRoutesDeps): ApiRoute[] {\n return new AutomationRunRoutes(deps).routes();\n}\n"],"mappings":"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"}
|
package/dist/routes/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuthStorage } from '@mastra/code-sdk/auth/storage';
|
|
2
2
|
import type { ProviderAccess } from '@mastra/code-sdk/onboarding/packs';
|
|
3
|
-
import {
|
|
3
|
+
import type { ModelReasoningOption } from '@mastra/core/llm';
|
|
4
4
|
import type { ApiRoute } from '@mastra/core/server';
|
|
5
5
|
import type { CredentialRecord, LoginSessionKind, ModelCredentialsStorage } from '../storage/domains/credentials/base.js';
|
|
6
6
|
import type { CustomProviderRecord, CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';
|
|
@@ -71,7 +71,7 @@ export interface OMSession {
|
|
|
71
71
|
export interface ModelCatalog {
|
|
72
72
|
listAvailableModels: () => Promise<Array<{
|
|
73
73
|
id?: string;
|
|
74
|
-
modelName
|
|
74
|
+
modelName: string;
|
|
75
75
|
provider: string;
|
|
76
76
|
hasApiKey: boolean;
|
|
77
77
|
apiKeyEnvVar?: string;
|
|
@@ -80,7 +80,7 @@ export interface ModelCatalog {
|
|
|
80
80
|
id: string;
|
|
81
81
|
defaultModelId?: string;
|
|
82
82
|
}>;
|
|
83
|
-
|
|
83
|
+
getSession?: (sessionId: string) => Promise<OMSession | undefined>;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Build a deduplicated, sorted list of providers from the model catalog,
|
|
@@ -118,7 +118,7 @@ export declare function isDeploymentModelProvider(provider: string): boolean;
|
|
|
118
118
|
* ignored with a warning rather than failing boot.
|
|
119
119
|
*/
|
|
120
120
|
export declare function resolveDeploymentModelProviders(providers: readonly string[] | undefined): ReadonlySet<string>;
|
|
121
|
-
type ModelCredentialHolder = 'personal' | 'organization' | 'deployment';
|
|
121
|
+
export type ModelCredentialHolder = 'personal' | 'organization' | 'deployment';
|
|
122
122
|
/**
|
|
123
123
|
* Compute which providers the user can reach, mirroring the TUI's
|
|
124
124
|
* `/models-pack` access derivation: OAuth/api-key from the credential store for
|
|
@@ -134,9 +134,9 @@ export declare function buildProviderAccess({ controller, authStorage, tenantCre
|
|
|
134
134
|
export interface ModelCatalogEntry {
|
|
135
135
|
id: string;
|
|
136
136
|
provider: string;
|
|
137
|
-
modelName
|
|
137
|
+
modelName: string;
|
|
138
138
|
hasApiKey: true;
|
|
139
|
-
reasoningOptions
|
|
139
|
+
reasoningOptions?: ModelReasoningOption[];
|
|
140
140
|
credentialLabel?: string;
|
|
141
141
|
credentialHolder?: ModelCredentialHolder;
|
|
142
142
|
}
|
|
@@ -246,5 +246,4 @@ export interface ConfigRoutesDeps extends RouteDependencies {
|
|
|
246
246
|
export declare class ConfigRoutes extends Route<ConfigRoutesDeps> {
|
|
247
247
|
routes(): ApiRoute[];
|
|
248
248
|
}
|
|
249
|
-
export {};
|
|
250
249
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/routes/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/routes/config.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/config.js
CHANGED
|
@@ -241,7 +241,6 @@ async function buildModelCatalog({ controller, authStorage, tenantCredentials, d
|
|
|
241
241
|
provider: record.providerId,
|
|
242
242
|
modelName: model,
|
|
243
243
|
hasApiKey: true,
|
|
244
|
-
reasoningOptions: void 0,
|
|
245
244
|
...customProviderCredential
|
|
246
245
|
});
|
|
247
246
|
}
|
|
@@ -645,13 +644,19 @@ var ConfigRoutes = class extends Route {
|
|
|
645
644
|
let sessionPolicy;
|
|
646
645
|
const resourceId = c.req.query("resourceId");
|
|
647
646
|
const threadId = c.req.query("threadId");
|
|
648
|
-
|
|
647
|
+
const sessionId = c.req.query("sessionId");
|
|
648
|
+
if (resourceId !== void 0 || threadId !== void 0 || sessionId !== void 0) {
|
|
649
649
|
const address = z.object({
|
|
650
650
|
resourceId: z.string().trim().min(1),
|
|
651
|
-
threadId: z.string().trim().min(1)
|
|
652
|
-
|
|
651
|
+
threadId: z.string().trim().min(1),
|
|
652
|
+
sessionId: z.string().trim().min(1).optional()
|
|
653
|
+
}).transform((data) => ({
|
|
654
|
+
...data,
|
|
655
|
+
sessionId: data.sessionId ?? data.resourceId
|
|
656
|
+
})).safeParse({
|
|
653
657
|
resourceId,
|
|
654
|
-
threadId
|
|
658
|
+
threadId,
|
|
659
|
+
sessionId
|
|
655
660
|
});
|
|
656
661
|
if (!address.success) return c.json({ error: "invalid_session_context" }, 400);
|
|
657
662
|
if (!tenant && auth.enabled()) return c.json({ error: "unauthorized" }, 401);
|
|
@@ -766,7 +771,7 @@ var ConfigRoutes = class extends Route {
|
|
|
766
771
|
modelDefaults: options.modelDefaults
|
|
767
772
|
});
|
|
768
773
|
if ("response" in context) return context.response;
|
|
769
|
-
if (!options.factoryProjects || !options.sourceControls || !options.workItems || !options.controller.
|
|
774
|
+
if (!options.factoryProjects || !options.sourceControls || !options.workItems || !options.controller.getSession) return c.json({ error: "model_apply_unavailable" }, 503);
|
|
770
775
|
const record = await context.storage.get({
|
|
771
776
|
orgId: context.orgId,
|
|
772
777
|
userId: context.userId
|
|
@@ -797,7 +802,7 @@ var ConfigRoutes = class extends Route {
|
|
|
797
802
|
});
|
|
798
803
|
continue;
|
|
799
804
|
}
|
|
800
|
-
const session = await options.controller.
|
|
805
|
+
const session = await options.controller.getSession(row.sessionId);
|
|
801
806
|
if (!session?.thread || !session.model.switch) {
|
|
802
807
|
skipped.push({
|
|
803
808
|
sessionId: row.sessionId,
|
|
@@ -897,8 +902,7 @@ var ConfigRoutes = class extends Route {
|
|
|
897
902
|
method: "GET",
|
|
898
903
|
requiresAuth: false,
|
|
899
904
|
handler: async (c) => {
|
|
900
|
-
const
|
|
901
|
-
const scope = c.req.query("scope") || void 0;
|
|
905
|
+
const sessionId = c.req.query("sessionId");
|
|
902
906
|
const factoryProjectId = c.req.query("factoryId") || void 0;
|
|
903
907
|
const context = await resolveMemorySettingsContext({
|
|
904
908
|
c: loose(c),
|
|
@@ -915,7 +919,7 @@ var ConfigRoutes = class extends Route {
|
|
|
915
919
|
});
|
|
916
920
|
const fallback = await factoryOmFallback(factoryProjectId);
|
|
917
921
|
const availableProviders = await resolveOMResponseContext(loose(c));
|
|
918
|
-
const sessionModelId = (
|
|
922
|
+
const sessionModelId = (sessionId ? await controller.getSession?.(sessionId) : void 0)?.model.get();
|
|
919
923
|
return c.json({ config: readStoredOMConfig(record, sessionModelId || fallback, sessionModelId ? "live-session" : "configured-default", availableProviders) });
|
|
920
924
|
} catch (error) {
|
|
921
925
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
|
|
@@ -934,8 +938,7 @@ var ConfigRoutes = class extends Route {
|
|
|
934
938
|
} catch {
|
|
935
939
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
936
940
|
}
|
|
937
|
-
const
|
|
938
|
-
const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
|
|
941
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
|
|
939
942
|
const factoryProjectId = typeof body.factoryId === "string" && body.factoryId ? body.factoryId : void 0;
|
|
940
943
|
const model = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
941
944
|
if (!model) return c.json({ error: "Missing required field: modelId" }, 400);
|
|
@@ -955,7 +958,7 @@ var ConfigRoutes = class extends Route {
|
|
|
955
958
|
orgId: context.orgId,
|
|
956
959
|
userId: context.userId
|
|
957
960
|
});
|
|
958
|
-
const sessionModelId = (
|
|
961
|
+
const sessionModelId = (sessionId ? await controller.getSession?.(sessionId) : void 0)?.model.get();
|
|
959
962
|
const config = readStoredOMConfig(record, sessionModelId || await factoryOmFallback(factoryProjectId), sessionModelId ? "live-session" : "configured-default", availableProviders);
|
|
960
963
|
return c.json({
|
|
961
964
|
ok: true,
|
|
@@ -976,8 +979,7 @@ var ConfigRoutes = class extends Route {
|
|
|
976
979
|
} catch {
|
|
977
980
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
978
981
|
}
|
|
979
|
-
const
|
|
980
|
-
const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
|
|
982
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
|
|
981
983
|
const factoryProjectId = typeof body.factoryId === "string" && body.factoryId ? body.factoryId : void 0;
|
|
982
984
|
const observation = typeof body.observationThreshold === "number" && body.observationThreshold > 0 ? Math.round(body.observationThreshold) : void 0;
|
|
983
985
|
const reflection = typeof body.reflectionThreshold === "number" && body.reflectionThreshold > 0 ? Math.round(body.reflectionThreshold) : void 0;
|
|
@@ -1000,7 +1002,7 @@ var ConfigRoutes = class extends Route {
|
|
|
1000
1002
|
orgId: context.orgId,
|
|
1001
1003
|
userId: context.userId
|
|
1002
1004
|
});
|
|
1003
|
-
const sessionModelId = (
|
|
1005
|
+
const sessionModelId = (sessionId ? await controller.getSession?.(sessionId) : void 0)?.model.get();
|
|
1004
1006
|
const config = readStoredOMConfig(record, sessionModelId || await factoryOmFallback(factoryProjectId), sessionModelId ? "live-session" : "configured-default", availableProviders);
|
|
1005
1007
|
return c.json({
|
|
1006
1008
|
ok: true,
|
|
@@ -1021,8 +1023,7 @@ var ConfigRoutes = class extends Route {
|
|
|
1021
1023
|
} catch {
|
|
1022
1024
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1023
1025
|
}
|
|
1024
|
-
const
|
|
1025
|
-
const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
|
|
1026
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
|
|
1026
1027
|
const factoryProjectId = typeof body.factoryId === "string" && body.factoryId ? body.factoryId : void 0;
|
|
1027
1028
|
const raw = body.value;
|
|
1028
1029
|
const value = raw === "auto" || raw === true || raw === false ? raw : "auto";
|
|
@@ -1042,7 +1043,7 @@ var ConfigRoutes = class extends Route {
|
|
|
1042
1043
|
orgId: context.orgId,
|
|
1043
1044
|
userId: context.userId
|
|
1044
1045
|
});
|
|
1045
|
-
const sessionModelId = (
|
|
1046
|
+
const sessionModelId = (sessionId ? await controller.getSession?.(sessionId) : void 0)?.model.get();
|
|
1046
1047
|
const config = readStoredOMConfig(record, sessionModelId || await factoryOmFallback(factoryProjectId), sessionModelId ? "live-session" : "configured-default", availableProviders);
|
|
1047
1048
|
return c.json({
|
|
1048
1049
|
ok: true,
|