@mastra/factory 0.24.0-alpha.9 → 0.25.0-alpha.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 +40 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +18 -4
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +3 -2
- package/dist/boards/review.js.map +1 -1
- 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 +3 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +172 -88
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +15 -2
- 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/{session → integrations/slack}/channel-reply-processor.d.ts +11 -9
- package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
- package/dist/integrations/slack/channel-reply-processor.js +61 -0
- package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
- package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
- package/dist/{session → integrations/slack}/channel-session-link-processor.js +14 -4
- package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
- 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.d.ts +25 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +56 -6
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
- package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
- package/dist/integrations/slack/reply-reminder-processor.js +58 -0
- package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
- package/dist/integrations/slack/reply-tool.d.ts +31 -0
- package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
- package/dist/integrations/slack/reply-tool.js +203 -0
- package/dist/integrations/slack/reply-tool.js.map +1 -0
- package/dist/integrations/slack/share-file-tool.d.ts +45 -0
- package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
- package/dist/integrations/slack/share-file-tool.js +106 -0
- package/dist/integrations/slack/share-file-tool.js.map +1 -0
- 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 +141 -40
- 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/events.d.ts +73 -0
- package/dist/mcp/events.d.ts.map +1 -0
- package/dist/mcp/events.js +158 -0
- package/dist/mcp/events.js.map +1 -0
- package/dist/mcp/routes.d.ts +0 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +31 -4
- 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/platform-auth-host.d.ts.map +1 -1
- package/dist/platform-auth-host.js +1 -1
- package/dist/platform-auth-host.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 +8 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +70 -28
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +2 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +2 -2
- 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 +60 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +202 -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 +23 -68
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +8 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +239 -19
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +7 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +20 -8
- 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 +21 -13
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +2 -2
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +3 -3
- package/dist/rules/processor.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.d.ts +11 -0
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +38 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +2 -0
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +5 -1
- package/dist/sandbox/session-sandbox.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/credential-policy.d.ts +4 -2
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +18 -7
- 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/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +14 -1
- package/dist/session/factory-session.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/run-audit.d.ts +1 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +7 -2
- package/dist/session/run-audit.js.map +1 -1
- package/dist/session/session-threads.d.ts +63 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +97 -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 +6 -13
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +19 -24
- 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/session-owner-signal.d.ts +2 -0
- package/dist/session-owner-signal.d.ts.map +1 -0
- package/dist/session-owner-signal.js +6 -0
- package/dist/session-owner-signal.js.map +1 -0
- package/dist/skills/service.d.ts +11 -14
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +12 -22
- 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/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +2 -1
- package/dist/storage/domains/audit/actions.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.d.ts +18 -2
- package/dist/storage/domains/audit/base.d.ts.map +1 -1
- package/dist/storage/domains/audit/base.js +22 -4
- 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.d.ts +8 -0
- package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
- package/dist/storage/domains/model-defaults/base.js +18 -0
- 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 +44 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +109 -3
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +23 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +35 -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 +3 -2
- 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/sandbox-tools.d.ts +12 -0
- package/dist/tools/sandbox-tools.d.ts.map +1 -0
- package/dist/tools/sandbox-tools.js +40 -0
- package/dist/tools/sandbox-tools.js.map +1 -0
- package/dist/tools/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +5 -7
- 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 +39 -18
- 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/work-item-branch.d.ts +11 -3
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +34 -9
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +7 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +32 -17
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +2 -2
- package/package.json +16 -15
- package/dist/session/channel-reply-processor.d.ts.map +0 -1
- package/dist/session/channel-reply-processor.js +0 -46
- package/dist/session/channel-reply-processor.js.map +0 -1
- package/dist/session/channel-session-link-processor.d.ts.map +0 -1
- package/dist/session/channel-session-link-processor.js.map +0 -1
- /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { externallyAuthoredWorkItem } from "../rules/types.js";
|
|
2
|
+
import { sessionStampCounts } from "./session-threads.js";
|
|
3
|
+
//#region src/session/resource-migration.ts
|
|
4
|
+
/** Slack-started sessions live on a `slack/<thread>` branch (see `threadBranch` in the Slack integration). */
|
|
5
|
+
const SLACK_SESSION_BRANCH_PREFIX = "slack/";
|
|
6
|
+
/**
|
|
7
|
+
* Set on a thread while it moves and turned off once the move, vectors included, is done. A
|
|
8
|
+
* crash in between leaves the thread moved in storage but no longer listed under its
|
|
9
|
+
* session id, so the next start finds it by this flag and its session stamp and finishes the move.
|
|
10
|
+
*/
|
|
11
|
+
const MOVE_PENDING_KEY = "factoryResourceMovePending";
|
|
12
|
+
/** How often a long resource migration reports how far it got. */
|
|
13
|
+
const PROGRESS_LOG_INTERVAL_MS = 1e4;
|
|
14
|
+
/**
|
|
15
|
+
* The memory resource a session belongs to: its Factory for runs, side
|
|
16
|
+
* sessions and Slack channel threads, its owner for sessions a person opened,
|
|
17
|
+
* private side sessions and Slack DMs. A run on untrusted content (a pull request, or a card
|
|
18
|
+
* written outside the write-access circle) keeps its own resource, like a new
|
|
19
|
+
* run would; so does a run whose work item no longer loads, since nothing then
|
|
20
|
+
* shows its content was trusted. `null` when a Factory-owned session has no
|
|
21
|
+
* Factory recorded — guessing would merge it into the wrong resource.
|
|
22
|
+
*/
|
|
23
|
+
function sessionMemoryResource(session, boundItems) {
|
|
24
|
+
if (boundItems.some((item) => item === null || untrustedWorkItem(item))) return {
|
|
25
|
+
owner: "isolated",
|
|
26
|
+
resourceId: session.sessionId
|
|
27
|
+
};
|
|
28
|
+
const sharedWithOrg = session.visibility !== "private" && (session.originSessionId !== null || session.branch.startsWith(SLACK_SESSION_BRANCH_PREFIX));
|
|
29
|
+
return boundItems.length > 0 || sharedWithOrg ? {
|
|
30
|
+
owner: "factory",
|
|
31
|
+
resourceId: session.factoryProjectId
|
|
32
|
+
} : {
|
|
33
|
+
owner: "user",
|
|
34
|
+
resourceId: session.userId
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function untrustedWorkItem(item) {
|
|
38
|
+
return item.externalSource?.type === "pull-request" || externallyAuthoredWorkItem(item);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Move sessions stored under the legacy `resourceId = sessionId` onto their
|
|
42
|
+
* Factory or user resource. Threads keep their id (`threadId === sessionId`)
|
|
43
|
+
* and get `factorySessionId` so per-session listings still find them.
|
|
44
|
+
*
|
|
45
|
+
* Threads move through the sessions' `Memory`, so their observation vectors
|
|
46
|
+
* follow them and recall search finds their past observations under the new
|
|
47
|
+
* resource. Every write only touches values still equal to the session id, so
|
|
48
|
+
* a rerun is a no-op and a crash leaves a state the next run resumes from. A
|
|
49
|
+
* session is marked once moved, so later starts only read sessions that are not
|
|
50
|
+
* (new sessions are created marked).
|
|
51
|
+
* Nothing is deleted. Thread-scoped observational memory is keyed by thread id
|
|
52
|
+
* and keeps working. Resource-scoped observational memory written under
|
|
53
|
+
* `resource:<sessionId>` stays under that key and is no longer read: several
|
|
54
|
+
* sessions merge into one resource, and their records cannot be merged.
|
|
55
|
+
*/
|
|
56
|
+
async function migrateSessionResources(deps) {
|
|
57
|
+
const report = {
|
|
58
|
+
sessions: 0,
|
|
59
|
+
factorySessions: 0,
|
|
60
|
+
userSessions: 0,
|
|
61
|
+
isolatedSessions: 0,
|
|
62
|
+
skipped: [],
|
|
63
|
+
failed: [],
|
|
64
|
+
subscriptionsPending: [],
|
|
65
|
+
threads: 0,
|
|
66
|
+
runBindings: 0,
|
|
67
|
+
subscriptions: 0,
|
|
68
|
+
resourceObservationsLeft: []
|
|
69
|
+
};
|
|
70
|
+
const sessions = await deps.sessions.listSessionsAwaitingResourceMigration();
|
|
71
|
+
if (sessions.length > 0) console.info(`[factory] session resource migration: moving ${sessions.length} sessions in the background`);
|
|
72
|
+
let lastProgressAt = Date.now();
|
|
73
|
+
for (const session of sessions) {
|
|
74
|
+
report.sessions += 1;
|
|
75
|
+
const { sessionId } = session;
|
|
76
|
+
try {
|
|
77
|
+
const { owner, resourceId } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(sessionId));
|
|
78
|
+
if (!resourceId) {
|
|
79
|
+
report.skipped.push(sessionId);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (owner === "factory") report.factorySessions += 1;
|
|
83
|
+
else if (owner === "isolated") report.isolatedSessions += 1;
|
|
84
|
+
else report.userSessions += 1;
|
|
85
|
+
if (resourceId === sessionId) {
|
|
86
|
+
await deps.sessions.markResourcesMigrated(sessionId);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const { threads: interrupted } = await deps.memory.listThreads({
|
|
90
|
+
filter: {
|
|
91
|
+
resourceId,
|
|
92
|
+
metadata: {
|
|
93
|
+
factorySessionId: sessionId,
|
|
94
|
+
[MOVE_PENDING_KEY]: true
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
perPage: false
|
|
98
|
+
});
|
|
99
|
+
for (const thread of interrupted) {
|
|
100
|
+
if (thread.resourceId === sessionId) continue;
|
|
101
|
+
await moveThread(deps, thread, thread.resourceId);
|
|
102
|
+
report.threads += 1;
|
|
103
|
+
}
|
|
104
|
+
const { threads } = await deps.memory.listThreads({
|
|
105
|
+
filter: { resourceId: sessionId },
|
|
106
|
+
perPage: false
|
|
107
|
+
});
|
|
108
|
+
const stampsCount = sessionStampCounts(session, resourceId);
|
|
109
|
+
for (const thread of threads) {
|
|
110
|
+
if (!stampsCount && thread.id !== sessionId) continue;
|
|
111
|
+
const metadata = {
|
|
112
|
+
...thread.metadata,
|
|
113
|
+
factorySessionId: sessionId,
|
|
114
|
+
[MOVE_PENDING_KEY]: true
|
|
115
|
+
};
|
|
116
|
+
await deps.memory.patchThread({
|
|
117
|
+
id: thread.id,
|
|
118
|
+
metadata
|
|
119
|
+
});
|
|
120
|
+
await moveThread(deps, {
|
|
121
|
+
...thread,
|
|
122
|
+
metadata
|
|
123
|
+
}, resourceId);
|
|
124
|
+
report.threads += 1;
|
|
125
|
+
}
|
|
126
|
+
report.runBindings += await deps.workItems.moveRunBindingsToResource({
|
|
127
|
+
sessionId,
|
|
128
|
+
resourceId
|
|
129
|
+
});
|
|
130
|
+
if (deps.integrations) report.subscriptions += await deps.integrations.moveSubscriptionsToResource({
|
|
131
|
+
fromResourceId: sessionId,
|
|
132
|
+
toResourceId: resourceId
|
|
133
|
+
});
|
|
134
|
+
if (threads.length > 0 && deps.memory.supportsObservationalMemory && await deps.memory.getObservationalMemory(null, sessionId)) {
|
|
135
|
+
report.resourceObservationsLeft.push(sessionId);
|
|
136
|
+
console.warn(`[factory] session resource migration: session ${sessionId} has resource-scoped observational memory; it stays under resource ${sessionId} and is not carried into ${resourceId}`);
|
|
137
|
+
}
|
|
138
|
+
if (!deps.integrations) {
|
|
139
|
+
report.subscriptionsPending.push(sessionId);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
await deps.sessions.markResourcesMigrated(sessionId);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
report.failed.push(sessionId);
|
|
145
|
+
console.warn(`[factory] session resource migration: session ${sessionId} not moved: ${error instanceof Error ? error.message : error}`);
|
|
146
|
+
} finally {
|
|
147
|
+
if (report.sessions < sessions.length && Date.now() - lastProgressAt >= PROGRESS_LOG_INTERVAL_MS) {
|
|
148
|
+
lastProgressAt = Date.now();
|
|
149
|
+
console.info(`[factory] session resource migration: ${report.sessions}/${sessions.length} sessions checked, ${report.threads} threads moved`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (report.threads + report.runBindings + report.subscriptions + report.skipped.length + report.failed.length + report.subscriptionsPending.length > 0) console.info(`[factory] session resource migration: ${report.threads} threads, ${report.runBindings} run bindings, ${report.subscriptions} subscriptions moved; ${report.skipped.length} Factory sessions without a Factory skipped; ${report.failed.length} sessions failed; ${report.subscriptionsPending.length} sessions wait for integrations to move their subscriptions`);
|
|
154
|
+
return report;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Mark shared user sessions private when their conversation lives on their owner's resource.
|
|
158
|
+
* The resource already keeps teammates out of such a session; this makes lists agree, so they
|
|
159
|
+
* stop seeing a session they can't open. Sessions shared onto their Factory, runs, Slack threads
|
|
160
|
+
* and side sessions are left alone. Each session is checked once: new sessions start checked,
|
|
161
|
+
* and a session that throws stays unchecked so the next start retries it.
|
|
162
|
+
*/
|
|
163
|
+
async function privatizeOwnerResourceSessions(deps) {
|
|
164
|
+
let privatized = 0;
|
|
165
|
+
for (const session of await deps.sessions.listSessionsAwaitingVisibilityCheck()) try {
|
|
166
|
+
let makePrivate = false;
|
|
167
|
+
if (session.visibility !== "private") {
|
|
168
|
+
const { owner } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(session.sessionId));
|
|
169
|
+
makePrivate = (owner === "user" ? await deps.memory.getThreadById({ threadId: session.sessionId }) : null)?.resourceId === session.userId;
|
|
170
|
+
}
|
|
171
|
+
await deps.sessions.markVisibilityChecked(session.sessionId, { makePrivate });
|
|
172
|
+
if (makePrivate) privatized += 1;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn(`[factory] session visibility check: session ${session.sessionId} not checked: ${error instanceof Error ? error.message : error}`);
|
|
175
|
+
}
|
|
176
|
+
if (privatized > 0) console.info(`[factory] session visibility check: ${privatized} sessions on their owner's memory made private`);
|
|
177
|
+
return privatized;
|
|
178
|
+
}
|
|
179
|
+
async function moveThread(deps, thread, resourceId) {
|
|
180
|
+
await deps.threads.updateThreadResourceId({
|
|
181
|
+
threadId: thread.id,
|
|
182
|
+
resourceId
|
|
183
|
+
});
|
|
184
|
+
await deps.memory.patchThread({
|
|
185
|
+
id: thread.id,
|
|
186
|
+
metadata: {
|
|
187
|
+
...thread.metadata,
|
|
188
|
+
[MOVE_PENDING_KEY]: false
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { migrateSessionResources, privatizeOwnerResourceSessions, sessionMemoryResource };
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=resource-migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-migration.js","names":[],"sources":["../../src/session/resource-migration.ts"],"sourcesContent":["import type { StorageThreadType } from '@mastra/core/memory';\nimport type { MemoryStorage } from '@mastra/core/storage';\n\nimport { externallyAuthoredWorkItem } from '../rules/types.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow } from '../storage/domains/work-items/base.js';\nimport { sessionStampCounts } from './session-threads.js';\n\n/** Slack-started sessions live on a `slack/<thread>` branch (see `threadBranch` in the Slack integration). */\nconst SLACK_SESSION_BRANCH_PREFIX = 'slack/';\n\n/**\n * Set on a thread while it moves and turned off once the move, vectors included, is done. A\n * crash in between leaves the thread moved in storage but no longer listed under its\n * session id, so the next start finds it by this flag and its session stamp and finishes the move.\n */\nconst MOVE_PENDING_KEY = 'factoryResourceMovePending';\n/** How often a long resource migration reports how far it got. */\nconst PROGRESS_LOG_INTERVAL_MS = 10_000;\n\nexport interface SessionResourceMigrationDeps {\n sessions: {\n listSessionsAwaitingResourceMigration(): Promise<SourceControlSession[]>;\n markResourcesMigrated(sessionId: string): Promise<void>;\n };\n workItems: {\n listRunBoundItems(sessionId: string): Promise<(Pick<WorkItemRow, 'externalSource' | 'metadata'> | null)[]>;\n moveRunBindingsToResource(input: { sessionId: string; resourceId: string }): Promise<number>;\n };\n /** Absent when the integrations domain is not ready; subscriptions then stay where they are. */\n integrations?: {\n moveSubscriptionsToResource(input: { fromResourceId: string; toResourceId: string }): Promise<number>;\n };\n memory: Pick<MemoryStorage, 'listThreads' | 'patchThread' | 'getObservationalMemory' | 'supportsObservationalMemory'>;\n /**\n * Moves a thread with everything keyed by its resource, such as observation vectors. Pass the\n * sessions' `Memory`; the bare memory store only moves the thread and its messages.\n */\n threads: { updateThreadResourceId(input: { threadId: string; resourceId: string }): Promise<StorageThreadType> };\n}\n\nexport interface SessionResourceMigrationReport {\n sessions: number;\n factorySessions: number;\n userSessions: number;\n /** Runs on untrusted content; they keep their own resource. */\n isolatedSessions: number;\n /** Factory-owned sessions with no Factory to move them onto; left untouched. */\n skipped: string[];\n /** Sessions whose move threw; the next startup retries them. */\n failed: string[];\n /**\n * Moved sessions whose subscriptions couldn't move because the integrations\n * domain wasn't ready; left unmarked so a later startup moves them.\n */\n subscriptionsPending: string[];\n threads: number;\n runBindings: number;\n subscriptions: number;\n /** Moved sessions whose resource-scoped observational memory stays under their old resource. */\n resourceObservationsLeft: string[];\n}\n\n/**\n * The memory resource a session belongs to: its Factory for runs, side\n * sessions and Slack channel threads, its owner for sessions a person opened,\n * private side sessions and Slack DMs. A run on untrusted content (a pull request, or a card\n * written outside the write-access circle) keeps its own resource, like a new\n * run would; so does a run whose work item no longer loads, since nothing then\n * shows its content was trusted. `null` when a Factory-owned session has no\n * Factory recorded — guessing would merge it into the wrong resource.\n */\nexport function sessionMemoryResource(\n session: SourceControlSession,\n boundItems: (Pick<WorkItemRow, 'externalSource' | 'metadata'> | null)[],\n): { owner: 'factory' | 'user' | 'isolated'; resourceId: string | null } {\n if (boundItems.some(item => item === null || untrustedWorkItem(item))) {\n return { owner: 'isolated', resourceId: session.sessionId };\n }\n // A Slack DM or private side session is its owner's, so it belongs with their own sessions.\n const sharedWithOrg =\n session.visibility !== 'private' &&\n (session.originSessionId !== null || session.branch.startsWith(SLACK_SESSION_BRANCH_PREFIX));\n const factoryOwned = boundItems.length > 0 || sharedWithOrg;\n return factoryOwned\n ? { owner: 'factory', resourceId: session.factoryProjectId }\n : { owner: 'user', resourceId: session.userId };\n}\n\nfunction untrustedWorkItem(item: Pick<WorkItemRow, 'externalSource' | 'metadata'>): boolean {\n return item.externalSource?.type === 'pull-request' || externallyAuthoredWorkItem(item);\n}\n\n/**\n * Move sessions stored under the legacy `resourceId = sessionId` onto their\n * Factory or user resource. Threads keep their id (`threadId === sessionId`)\n * and get `factorySessionId` so per-session listings still find them.\n *\n * Threads move through the sessions' `Memory`, so their observation vectors\n * follow them and recall search finds their past observations under the new\n * resource. Every write only touches values still equal to the session id, so\n * a rerun is a no-op and a crash leaves a state the next run resumes from. A\n * session is marked once moved, so later starts only read sessions that are not\n * (new sessions are created marked).\n * Nothing is deleted. Thread-scoped observational memory is keyed by thread id\n * and keeps working. Resource-scoped observational memory written under\n * `resource:<sessionId>` stays under that key and is no longer read: several\n * sessions merge into one resource, and their records cannot be merged.\n */\nexport async function migrateSessionResources(\n deps: SessionResourceMigrationDeps,\n): Promise<SessionResourceMigrationReport> {\n const report: SessionResourceMigrationReport = {\n sessions: 0,\n factorySessions: 0,\n userSessions: 0,\n isolatedSessions: 0,\n skipped: [],\n failed: [],\n subscriptionsPending: [],\n threads: 0,\n runBindings: 0,\n subscriptions: 0,\n resourceObservationsLeft: [],\n };\n const sessions = await deps.sessions.listSessionsAwaitingResourceMigration();\n // This pass can take minutes against a remote database, so say what it is doing and\n // how far it got.\n if (sessions.length > 0) {\n console.info(`[factory] session resource migration: moving ${sessions.length} sessions in the background`);\n }\n let lastProgressAt = Date.now();\n\n for (const session of sessions) {\n report.sessions += 1;\n const { sessionId } = session;\n try {\n const { owner, resourceId } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(sessionId));\n if (!resourceId) {\n report.skipped.push(sessionId);\n continue;\n }\n if (owner === 'factory') report.factorySessions += 1;\n else if (owner === 'isolated') report.isolatedSessions += 1;\n else report.userSessions += 1;\n if (resourceId === sessionId) {\n await deps.sessions.markResourcesMigrated(sessionId);\n continue;\n }\n\n // Finish moves a crash interrupted after storage moved the thread.\n const { threads: interrupted } = await deps.memory.listThreads({\n filter: { resourceId, metadata: { factorySessionId: sessionId, [MOVE_PENDING_KEY]: true } },\n perPage: false,\n });\n for (const thread of interrupted) {\n if (thread.resourceId === sessionId) continue; // listed under the session id below\n await moveThread(deps, thread, thread.resourceId);\n report.threads += 1;\n }\n const { threads } = await deps.memory.listThreads({ filter: { resourceId: sessionId }, perPage: false });\n // A private session's stamps don't count under its Factory, so only its own\n // thread moves there; its other threads stay listed under its id.\n const stampsCount = sessionStampCounts(session, resourceId);\n for (const thread of threads) {\n if (!stampsCount && thread.id !== sessionId) continue;\n // Tag before moving: once moved, the thread is no longer found under the session id.\n const metadata = { ...thread.metadata, factorySessionId: sessionId, [MOVE_PENDING_KEY]: true };\n await deps.memory.patchThread({ id: thread.id, metadata });\n await moveThread(deps, { ...thread, metadata }, resourceId);\n report.threads += 1;\n }\n report.runBindings += await deps.workItems.moveRunBindingsToResource({ sessionId, resourceId });\n if (deps.integrations) {\n report.subscriptions += await deps.integrations.moveSubscriptionsToResource({\n fromResourceId: sessionId,\n toResourceId: resourceId,\n });\n }\n // Reported once, on the run that moves the session's threads.\n if (\n threads.length > 0 &&\n deps.memory.supportsObservationalMemory &&\n (await deps.memory.getObservationalMemory(null, sessionId))\n ) {\n report.resourceObservationsLeft.push(sessionId);\n console.warn(\n `[factory] session resource migration: session ${sessionId} has resource-scoped observational memory; ` +\n `it stays under resource ${sessionId} and is not carried into ${resourceId}`,\n );\n }\n if (!deps.integrations) {\n report.subscriptionsPending.push(sessionId);\n continue;\n }\n await deps.sessions.markResourcesMigrated(sessionId);\n } catch (error) {\n report.failed.push(sessionId);\n console.warn(\n `[factory] session resource migration: session ${sessionId} not moved: ${error instanceof Error ? error.message : error}`,\n );\n } finally {\n if (report.sessions < sessions.length && Date.now() - lastProgressAt >= PROGRESS_LOG_INTERVAL_MS) {\n lastProgressAt = Date.now();\n console.info(\n `[factory] session resource migration: ${report.sessions}/${sessions.length} sessions checked, ` +\n `${report.threads} threads moved`,\n );\n }\n }\n }\n\n if (\n report.threads +\n report.runBindings +\n report.subscriptions +\n report.skipped.length +\n report.failed.length +\n report.subscriptionsPending.length >\n 0\n ) {\n console.info(\n `[factory] session resource migration: ${report.threads} threads, ${report.runBindings} run bindings, ` +\n `${report.subscriptions} subscriptions moved; ${report.skipped.length} Factory sessions without a Factory skipped; ` +\n `${report.failed.length} sessions failed; ${report.subscriptionsPending.length} sessions wait for integrations ` +\n `to move their subscriptions`,\n );\n }\n return report;\n}\n\nexport interface SessionVisibilityBackfillDeps {\n sessions: {\n listSessionsAwaitingVisibilityCheck(): Promise<SourceControlSession[]>;\n markVisibilityChecked(sessionId: string, options: { makePrivate: boolean }): Promise<void>;\n };\n workItems: Pick<SessionResourceMigrationDeps['workItems'], 'listRunBoundItems'>;\n memory: Pick<MemoryStorage, 'getThreadById'>;\n}\n\n/**\n * Mark shared user sessions private when their conversation lives on their owner's resource.\n * The resource already keeps teammates out of such a session; this makes lists agree, so they\n * stop seeing a session they can't open. Sessions shared onto their Factory, runs, Slack threads\n * and side sessions are left alone. Each session is checked once: new sessions start checked,\n * and a session that throws stays unchecked so the next start retries it.\n */\nexport async function privatizeOwnerResourceSessions(deps: SessionVisibilityBackfillDeps): Promise<number> {\n let privatized = 0;\n for (const session of await deps.sessions.listSessionsAwaitingVisibilityCheck()) {\n try {\n let makePrivate = false;\n if (session.visibility !== 'private') {\n const { owner } = sessionMemoryResource(session, await deps.workItems.listRunBoundItems(session.sessionId));\n const thread = owner === 'user' ? await deps.memory.getThreadById({ threadId: session.sessionId }) : null;\n makePrivate = thread?.resourceId === session.userId;\n }\n await deps.sessions.markVisibilityChecked(session.sessionId, { makePrivate });\n if (makePrivate) privatized += 1;\n } catch (error) {\n console.warn(\n `[factory] session visibility check: session ${session.sessionId} not checked: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n if (privatized > 0) {\n console.info(`[factory] session visibility check: ${privatized} sessions on their owner's memory made private`);\n }\n return privatized;\n}\n\nasync function moveThread(\n deps: SessionResourceMigrationDeps,\n thread: StorageThreadType,\n resourceId: string,\n): Promise<void> {\n await deps.threads.updateThreadResourceId({ threadId: thread.id, resourceId });\n // Stores merge thread metadata, so the flag is switched off rather than removed.\n await deps.memory.patchThread({ id: thread.id, metadata: { ...thread.metadata, [MOVE_PENDING_KEY]: false } });\n}\n"],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@ declare const factoryOpenRunSchema: z.ZodObject<{
|
|
|
17
17
|
branch: z.ZodString;
|
|
18
18
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
19
19
|
heartbeatAt: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
startedAt: z.ZodOptional<z.ZodNumber>;
|
|
20
21
|
}, z.core.$strip>;
|
|
21
22
|
export type FactoryOpenRun = z.infer<typeof factoryOpenRunSchema>;
|
|
22
23
|
/** A heartbeat older than this means the owner is gone; nothing renews it once the run's dispatcher dies. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-audit.d.ts","sourceRoot":"","sources":["../../src/session/run-audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,eAAO,MAAM,yBAAyB,oBAAoB,CAAC;AAE3D,QAAA,MAAM,oBAAoB
|
|
1
|
+
{"version":3,"file":"run-audit.d.ts","sourceRoot":"","sources":["../../src/session/run-audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,eAAO,MAAM,yBAAyB,oBAAoB,CAAC;AAE3D,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;iBAuBxB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,6GAA6G;AAC7G,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,OAAO,CAOtG;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE;QACf,UAAU,CAAC,IAAI,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACpD,UAAU,CAAC,IAAI,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,OAAO,CAAA;SAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAClE,CAAC;IACF,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,KAAK,YAAY,GAAG,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AAInF;;;;GAIG;AACH,eAAO,MAAM,mCAAmC,OAAO,CAAC;AAExD,QAAA,MAAM,YAAY,8LASR,CAAC;AAEX,KAAK,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE1E;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACvB;IACE,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kGAAkG;IAClG,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC,GACD;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,CAAC;AAoE7B,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAGlG;AAED,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzF;AAsED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,oBAAoB,EAC7B,EACE,KAAK,EACL,GAAG,EACH,SAAS,EACT,WAAW,GACZ,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,GAAG,EAAE,cAAc,CAAC;IACpB,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,MAAM,YAAY,CAAC;CACjC,GACA,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED,4FAA4F;AAC5F,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,oBAAoB,EAC7B,SAAS,EAAE,MAAM,EACjB,EAAE,SAAa,GACd,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,aAAa,CAAA;CAAE,GAAG,MAAM,IAAI,CAcnH"}
|
|
@@ -21,7 +21,9 @@ const factoryOpenRunSchema = z.object({
|
|
|
21
21
|
* was recorded; those read as stale.
|
|
22
22
|
*/
|
|
23
23
|
ownerId: z.string().optional(),
|
|
24
|
-
heartbeatAt: z.number().optional()
|
|
24
|
+
heartbeatAt: z.number().optional(),
|
|
25
|
+
/** When the kickoff was accepted (epoch ms), so the run's end record spans the whole run. Absent on older entries. */
|
|
26
|
+
startedAt: z.number().optional()
|
|
25
27
|
});
|
|
26
28
|
/** A heartbeat older than this means the owner is gone; nothing renews it once the run's dispatcher dies. */
|
|
27
29
|
const FACTORY_OPEN_RUN_STALE_MS = 3e4;
|
|
@@ -162,7 +164,10 @@ function recordSessionRunStart(session, { audit, run, actorType, observedEnd })
|
|
|
162
164
|
const runs = await listSessionOpenRuns(session);
|
|
163
165
|
if (!runs.some((existing) => existing.kickoffId === run.kickoffId)) await session.thread.setSetting({
|
|
164
166
|
key: FACTORY_OPEN_RUNS_SETTING,
|
|
165
|
-
value: [...runs,
|
|
167
|
+
value: [...runs, {
|
|
168
|
+
startedAt: Date.now(),
|
|
169
|
+
...run
|
|
170
|
+
}]
|
|
166
171
|
});
|
|
167
172
|
await audit.record({
|
|
168
173
|
...runAuditInput(run),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-audit.js","names":[],"sources":["../../src/session/run-audit.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\nimport { z } from 'zod';\n\nimport type { AuditActorType } from '../storage/domains/audit/base.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\n\nexport const FACTORY_OPEN_RUNS_SETTING = 'factoryOpenRuns';\n\nconst factoryOpenRunSchema = z.object({\n kickoffId: z.string(),\n bindingId: z.string(),\n role: z.string(),\n startedBy: z.string(),\n orgId: z.string(),\n factoryProjectId: z.string(),\n workItemId: z.string(),\n workItemName: z.string().optional(),\n sessionId: z.string(),\n threadId: z.string(),\n branch: z.string(),\n /**\n * Which dispatcher process is watching this run, and when it last said so\n * (epoch ms). The ledger is a thread setting shared by every replica, so a\n * remote reader needs these to tell a run that is live elsewhere from one\n * that died with its process. Absent on entries written before ownership\n * was recorded; those read as stale.\n */\n ownerId: z.string().optional(),\n heartbeatAt: z.number().optional(),\n});\n\nexport type FactoryOpenRun = z.infer<typeof factoryOpenRunSchema>;\n\n/** A heartbeat older than this means the owner is gone; nothing renews it once the run's dispatcher dies. */\nexport const FACTORY_OPEN_RUN_STALE_MS = 30_000;\n\nexport function isOpenRunLiveElsewhere(run: FactoryOpenRun, ownerId: string, now = Date.now()): boolean {\n return (\n run.ownerId !== undefined &&\n run.ownerId !== ownerId &&\n run.heartbeatAt !== undefined &&\n now - run.heartbeatAt < FACTORY_OPEN_RUN_STALE_MS\n );\n}\n\nexport interface RunEndCaptureSession {\n readonly thread: {\n getSetting(args: { key: string }): Promise<unknown>;\n setSetting(args: { key: string; value: unknown }): Promise<void>;\n };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\ntype RunEndReason = Extract<AgentControllerEvent, { type: 'agent_end' }>['reason'];\ntype StepUsage = Extract<AgentControllerEvent, { type: 'usage_update' }>['usage'];\nconst pendingWrites = new WeakMap<RunEndCaptureSession, Promise<void>>();\n\n/**\n * JSON budget for the per-step entries on a run's audit record. Audit metadata\n * is capped at 4 KB as a whole and an oversized record loses all of it, so\n * later steps are dropped (and counted) instead; totals always cover every step.\n */\nexport const FACTORY_RUN_USAGE_STEPS_JSON_BUDGET = 2400;\n\nconst USAGE_FIELDS = [\n 'promptTokens',\n 'completionTokens',\n 'totalTokens',\n 'reasoningTokens',\n 'cachedInputTokens',\n 'cacheCreationInputTokens',\n 'cacheCreationInputTokens5m',\n 'cacheCreationInputTokens1h',\n] as const;\n\ntype UsageCounts = Partial<Record<(typeof USAGE_FIELDS)[number], number>>;\n\n/**\n * Token usage attached to a run's `factory.run.ended` audit record.\n * `unreported` means this process saw no usage for the run (it ran elsewhere,\n * the process restarted, or the model reported none). It is never zero spend.\n */\nexport type FactoryRunUsage =\n | {\n status: 'recorded';\n stepCount: number;\n totals: UsageCounts;\n steps: UsageCounts[];\n stepsTruncated?: number;\n /** Other runs that ended in the same agent turn; the turn's usage can't be split between them. */\n sharedWithKickoffIds?: string[];\n }\n | { status: 'unreported' };\n\nfunction usageCounts(usage: StepUsage): UsageCounts {\n const counts: UsageCounts = {};\n for (const field of USAGE_FIELDS) {\n const value = usage[field];\n if (typeof value === 'number') counts[field] = value;\n }\n return counts;\n}\n\nfunction runUsage(steps: UsageCounts[]): FactoryRunUsage {\n if (steps.length === 0) return { status: 'unreported' };\n const totals: UsageCounts = {};\n for (const step of steps) {\n for (const field of USAGE_FIELDS) {\n if (step[field] !== undefined) totals[field] = (totals[field] ?? 0) + step[field];\n }\n }\n const kept: UsageCounts[] = [];\n let length = 2;\n for (const step of steps) {\n length += JSON.stringify(step).length + 1;\n if (length > FACTORY_RUN_USAGE_STEPS_JSON_BUDGET) break;\n kept.push(step);\n }\n return {\n status: 'recorded',\n stepCount: steps.length,\n totals,\n steps: kept,\n ...(steps.length > kept.length ? { stepsTruncated: steps.length - kept.length } : {}),\n };\n}\n\ninterface EndedTurn {\n steps: UsageCounts[];\n claimed: boolean;\n}\n\ninterface SessionUsage {\n /** Steps of the turn in progress; a suspended turn keeps accumulating into its resume. */\n current: UsageCounts[];\n /** The most recently ended turn, which a run whose ledger entry lands after that end may still claim. */\n lastTurn?: EndedTurn;\n /** Usage of runs whose end record failed to persist, kept for the retry. */\n held: Map<string, FactoryRunUsage>;\n}\n\nconst sessionUsage = new WeakMap<RunEndCaptureSession, SessionUsage>();\n\nfunction usageFor(session: RunEndCaptureSession): SessionUsage {\n let usage = sessionUsage.get(session);\n if (!usage) {\n usage = { current: [], held: new Map() };\n sessionUsage.set(session, usage);\n }\n return usage;\n}\n\nfunction serializeRunAudit(session: RunEndCaptureSession, write: () => Promise<void>): Promise<void> {\n const pending = (pendingWrites.get(session) ?? Promise.resolve())\n .then(write)\n .catch(error => console.warn('[Factory run audit] Unable to record run lifecycle.', error));\n pendingWrites.set(session, pending);\n return pending;\n}\n\nexport async function listSessionOpenRuns(session: RunEndCaptureSession): Promise<FactoryOpenRun[]> {\n const stored = await session.thread.getSetting({ key: FACTORY_OPEN_RUNS_SETTING });\n return z.array(factoryOpenRunSchema).parse(stored ?? []);\n}\n\nexport async function waitForSessionRunAudit(session: RunEndCaptureSession): Promise<void> {\n await (pendingWrites.get(session) ?? Promise.resolve());\n}\n\nfunction runAuditInput(run: FactoryOpenRun) {\n // Ownership fields are dispatcher bookkeeping, not part of the run's audit record.\n const { orgId, factoryProjectId, workItemId, workItemName, ownerId, heartbeatAt, ...metadata } = run;\n void ownerId;\n void heartbeatAt;\n return {\n orgId,\n factoryProjectId,\n idempotencyKey: run.kickoffId,\n targets: [{ type: 'work_item', id: workItemId, ...(workItemName ? { name: workItemName } : {}) }],\n metadata,\n };\n}\n\nasync function recordRunEnds(\n session: RunEndCaptureSession,\n audit: AuditRecorder,\n reason: RunEndReason,\n kickoffId?: string,\n endedTurn?: EndedTurn,\n): Promise<void> {\n if (!reason || reason === 'suspended') return;\n const usage = usageFor(session);\n const runs = await listSessionOpenRuns(session);\n if (runs.length === 0) return;\n const ending = runs.filter(run => !kickoffId || run.kickoffId === kickoffId);\n // An end event bills the turn it closed. A run whose ledger entry lands after its turn ended\n // bills the latest ended turn, unless an open run already claimed it.\n const turn = endedTurn ?? usage.lastTurn;\n const steps = turn && !turn.claimed ? turn.steps : [];\n if (turn && ending.length > 0) turn.claimed = true;\n const turnUsage = runUsage(steps);\n const remaining = [];\n for (const run of runs) {\n if (!ending.includes(run)) {\n remaining.push(run);\n continue;\n }\n let runUsageRecord = usage.held.get(run.kickoffId);\n if (!runUsageRecord) {\n const shared = ending.filter(other => other !== run).map(other => other.kickoffId);\n runUsageRecord =\n turnUsage.status === 'recorded' && shared.length > 0\n ? { ...turnUsage, sharedWithKickoffIds: shared }\n : turnUsage;\n usage.held.set(run.kickoffId, runUsageRecord);\n }\n const input = runAuditInput(run);\n const recorded = await audit.record({\n ...input,\n actorId: `agent:${run.threadId}`,\n actorType: 'agent',\n action: 'factory.run.ended',\n metadata: { ...input.metadata, reason, usage: runUsageRecord },\n });\n if (recorded) {\n usage.held.delete(run.kickoffId);\n } else {\n console.warn(\n `[Factory run audit] Token usage for run ${run.kickoffId} (${run.role}) was not persisted; keeping it for the next attempt.`,\n runUsageRecord,\n );\n remaining.push(run);\n }\n }\n await session.thread.setSetting({ key: FACTORY_OPEN_RUNS_SETTING, value: remaining });\n}\n\nexport function recordSessionRunStart(\n session: RunEndCaptureSession,\n {\n audit,\n run,\n actorType,\n observedEnd,\n }: {\n audit: AuditRecorder;\n run: FactoryOpenRun;\n actorType: AuditActorType;\n observedEnd: () => RunEndReason;\n },\n): Promise<void> {\n return serializeRunAudit(session, async () => {\n const runs = await listSessionOpenRuns(session);\n if (!runs.some(existing => existing.kickoffId === run.kickoffId)) {\n await session.thread.setSetting({ key: FACTORY_OPEN_RUNS_SETTING, value: [...runs, run] });\n }\n await audit.record({\n ...runAuditInput(run),\n actorId: run.startedBy,\n actorType,\n action: 'factory.run.started',\n });\n await recordRunEnds(session, audit, observedEnd(), run.kickoffId);\n });\n}\n\n/** Renew this process's claim on an open run so other replicas keep treating it as live. */\nexport function heartbeatSessionOpenRun(\n session: RunEndCaptureSession,\n kickoffId: string,\n at = Date.now(),\n): Promise<void> {\n return serializeRunAudit(session, async () => {\n const runs = await listSessionOpenRuns(session);\n if (!runs.some(run => run.kickoffId === kickoffId)) return;\n await session.thread.setSetting({\n key: FACTORY_OPEN_RUNS_SETTING,\n value: runs.map(run => (run.kickoffId === kickoffId ? { ...run, heartbeatAt: at } : run)),\n });\n });\n}\n\nexport function observeSessionRunEnd(session: RunEndCaptureSession, { audit }: { audit: AuditRecorder }): () => void {\n const usage = usageFor(session);\n return session.subscribe(event => {\n if (event.type === 'usage_update') {\n usage.current.push(usageCounts(event.usage));\n return;\n }\n if (event.type !== 'agent_end' || !event.reason || event.reason === 'suspended') return;\n // Close the turn now so a following turn's steps never land in this run.\n const turn: EndedTurn = { steps: usage.current, claimed: false };\n usage.current = [];\n usage.lastTurn = turn;\n void serializeRunAudit(session, () => recordRunEnds(session, audit, event.reason, undefined, turn));\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"run-audit.js","names":[],"sources":["../../src/session/run-audit.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\nimport { z } from 'zod';\n\nimport type { AuditActorType } from '../storage/domains/audit/base.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\n\nexport const FACTORY_OPEN_RUNS_SETTING = 'factoryOpenRuns';\n\nconst factoryOpenRunSchema = z.object({\n kickoffId: z.string(),\n bindingId: z.string(),\n role: z.string(),\n startedBy: z.string(),\n orgId: z.string(),\n factoryProjectId: z.string(),\n workItemId: z.string(),\n workItemName: z.string().optional(),\n sessionId: z.string(),\n threadId: z.string(),\n branch: z.string(),\n /**\n * Which dispatcher process is watching this run, and when it last said so\n * (epoch ms). The ledger is a thread setting shared by every replica, so a\n * remote reader needs these to tell a run that is live elsewhere from one\n * that died with its process. Absent on entries written before ownership\n * was recorded; those read as stale.\n */\n ownerId: z.string().optional(),\n heartbeatAt: z.number().optional(),\n /** When the kickoff was accepted (epoch ms), so the run's end record spans the whole run. Absent on older entries. */\n startedAt: z.number().optional(),\n});\n\nexport type FactoryOpenRun = z.infer<typeof factoryOpenRunSchema>;\n\n/** A heartbeat older than this means the owner is gone; nothing renews it once the run's dispatcher dies. */\nexport const FACTORY_OPEN_RUN_STALE_MS = 30_000;\n\nexport function isOpenRunLiveElsewhere(run: FactoryOpenRun, ownerId: string, now = Date.now()): boolean {\n return (\n run.ownerId !== undefined &&\n run.ownerId !== ownerId &&\n run.heartbeatAt !== undefined &&\n now - run.heartbeatAt < FACTORY_OPEN_RUN_STALE_MS\n );\n}\n\nexport interface RunEndCaptureSession {\n readonly thread: {\n getSetting(args: { key: string }): Promise<unknown>;\n setSetting(args: { key: string; value: unknown }): Promise<void>;\n };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\ntype RunEndReason = Extract<AgentControllerEvent, { type: 'agent_end' }>['reason'];\ntype StepUsage = Extract<AgentControllerEvent, { type: 'usage_update' }>['usage'];\nconst pendingWrites = new WeakMap<RunEndCaptureSession, Promise<void>>();\n\n/**\n * JSON budget for the per-step entries on a run's audit record. Audit metadata\n * is capped at 4 KB as a whole and an oversized record loses all of it, so\n * later steps are dropped (and counted) instead; totals always cover every step.\n */\nexport const FACTORY_RUN_USAGE_STEPS_JSON_BUDGET = 2400;\n\nconst USAGE_FIELDS = [\n 'promptTokens',\n 'completionTokens',\n 'totalTokens',\n 'reasoningTokens',\n 'cachedInputTokens',\n 'cacheCreationInputTokens',\n 'cacheCreationInputTokens5m',\n 'cacheCreationInputTokens1h',\n] as const;\n\ntype UsageCounts = Partial<Record<(typeof USAGE_FIELDS)[number], number>>;\n\n/**\n * Token usage attached to a run's `factory.run.ended` audit record.\n * `unreported` means this process saw no usage for the run (it ran elsewhere,\n * the process restarted, or the model reported none). It is never zero spend.\n */\nexport type FactoryRunUsage =\n | {\n status: 'recorded';\n stepCount: number;\n totals: UsageCounts;\n steps: UsageCounts[];\n stepsTruncated?: number;\n /** Other runs that ended in the same agent turn; the turn's usage can't be split between them. */\n sharedWithKickoffIds?: string[];\n }\n | { status: 'unreported' };\n\nfunction usageCounts(usage: StepUsage): UsageCounts {\n const counts: UsageCounts = {};\n for (const field of USAGE_FIELDS) {\n const value = usage[field];\n if (typeof value === 'number') counts[field] = value;\n }\n return counts;\n}\n\nfunction runUsage(steps: UsageCounts[]): FactoryRunUsage {\n if (steps.length === 0) return { status: 'unreported' };\n const totals: UsageCounts = {};\n for (const step of steps) {\n for (const field of USAGE_FIELDS) {\n if (step[field] !== undefined) totals[field] = (totals[field] ?? 0) + step[field];\n }\n }\n const kept: UsageCounts[] = [];\n let length = 2;\n for (const step of steps) {\n length += JSON.stringify(step).length + 1;\n if (length > FACTORY_RUN_USAGE_STEPS_JSON_BUDGET) break;\n kept.push(step);\n }\n return {\n status: 'recorded',\n stepCount: steps.length,\n totals,\n steps: kept,\n ...(steps.length > kept.length ? { stepsTruncated: steps.length - kept.length } : {}),\n };\n}\n\ninterface EndedTurn {\n steps: UsageCounts[];\n claimed: boolean;\n}\n\ninterface SessionUsage {\n /** Steps of the turn in progress; a suspended turn keeps accumulating into its resume. */\n current: UsageCounts[];\n /** The most recently ended turn, which a run whose ledger entry lands after that end may still claim. */\n lastTurn?: EndedTurn;\n /** Usage of runs whose end record failed to persist, kept for the retry. */\n held: Map<string, FactoryRunUsage>;\n}\n\nconst sessionUsage = new WeakMap<RunEndCaptureSession, SessionUsage>();\n\nfunction usageFor(session: RunEndCaptureSession): SessionUsage {\n let usage = sessionUsage.get(session);\n if (!usage) {\n usage = { current: [], held: new Map() };\n sessionUsage.set(session, usage);\n }\n return usage;\n}\n\nfunction serializeRunAudit(session: RunEndCaptureSession, write: () => Promise<void>): Promise<void> {\n const pending = (pendingWrites.get(session) ?? Promise.resolve())\n .then(write)\n .catch(error => console.warn('[Factory run audit] Unable to record run lifecycle.', error));\n pendingWrites.set(session, pending);\n return pending;\n}\n\nexport async function listSessionOpenRuns(session: RunEndCaptureSession): Promise<FactoryOpenRun[]> {\n const stored = await session.thread.getSetting({ key: FACTORY_OPEN_RUNS_SETTING });\n return z.array(factoryOpenRunSchema).parse(stored ?? []);\n}\n\nexport async function waitForSessionRunAudit(session: RunEndCaptureSession): Promise<void> {\n await (pendingWrites.get(session) ?? Promise.resolve());\n}\n\nfunction runAuditInput(run: FactoryOpenRun) {\n // Ownership fields are dispatcher bookkeeping, not part of the run's audit record.\n const { orgId, factoryProjectId, workItemId, workItemName, ownerId, heartbeatAt, ...metadata } = run;\n void ownerId;\n void heartbeatAt;\n return {\n orgId,\n factoryProjectId,\n idempotencyKey: run.kickoffId,\n targets: [{ type: 'work_item', id: workItemId, ...(workItemName ? { name: workItemName } : {}) }],\n metadata,\n };\n}\n\nasync function recordRunEnds(\n session: RunEndCaptureSession,\n audit: AuditRecorder,\n reason: RunEndReason,\n kickoffId?: string,\n endedTurn?: EndedTurn,\n): Promise<void> {\n if (!reason || reason === 'suspended') return;\n const usage = usageFor(session);\n const runs = await listSessionOpenRuns(session);\n if (runs.length === 0) return;\n const ending = runs.filter(run => !kickoffId || run.kickoffId === kickoffId);\n // An end event bills the turn it closed. A run whose ledger entry lands after its turn ended\n // bills the latest ended turn, unless an open run already claimed it.\n const turn = endedTurn ?? usage.lastTurn;\n const steps = turn && !turn.claimed ? turn.steps : [];\n if (turn && ending.length > 0) turn.claimed = true;\n const turnUsage = runUsage(steps);\n const remaining = [];\n for (const run of runs) {\n if (!ending.includes(run)) {\n remaining.push(run);\n continue;\n }\n let runUsageRecord = usage.held.get(run.kickoffId);\n if (!runUsageRecord) {\n const shared = ending.filter(other => other !== run).map(other => other.kickoffId);\n runUsageRecord =\n turnUsage.status === 'recorded' && shared.length > 0\n ? { ...turnUsage, sharedWithKickoffIds: shared }\n : turnUsage;\n usage.held.set(run.kickoffId, runUsageRecord);\n }\n const input = runAuditInput(run);\n const recorded = await audit.record({\n ...input,\n actorId: `agent:${run.threadId}`,\n actorType: 'agent',\n action: 'factory.run.ended',\n metadata: { ...input.metadata, reason, usage: runUsageRecord },\n });\n if (recorded) {\n usage.held.delete(run.kickoffId);\n } else {\n console.warn(\n `[Factory run audit] Token usage for run ${run.kickoffId} (${run.role}) was not persisted; keeping it for the next attempt.`,\n runUsageRecord,\n );\n remaining.push(run);\n }\n }\n await session.thread.setSetting({ key: FACTORY_OPEN_RUNS_SETTING, value: remaining });\n}\n\nexport function recordSessionRunStart(\n session: RunEndCaptureSession,\n {\n audit,\n run,\n actorType,\n observedEnd,\n }: {\n audit: AuditRecorder;\n run: FactoryOpenRun;\n actorType: AuditActorType;\n observedEnd: () => RunEndReason;\n },\n): Promise<void> {\n return serializeRunAudit(session, async () => {\n const runs = await listSessionOpenRuns(session);\n if (!runs.some(existing => existing.kickoffId === run.kickoffId)) {\n await session.thread.setSetting({\n key: FACTORY_OPEN_RUNS_SETTING,\n value: [...runs, { startedAt: Date.now(), ...run }],\n });\n }\n await audit.record({\n ...runAuditInput(run),\n actorId: run.startedBy,\n actorType,\n action: 'factory.run.started',\n });\n await recordRunEnds(session, audit, observedEnd(), run.kickoffId);\n });\n}\n\n/** Renew this process's claim on an open run so other replicas keep treating it as live. */\nexport function heartbeatSessionOpenRun(\n session: RunEndCaptureSession,\n kickoffId: string,\n at = Date.now(),\n): Promise<void> {\n return serializeRunAudit(session, async () => {\n const runs = await listSessionOpenRuns(session);\n if (!runs.some(run => run.kickoffId === kickoffId)) return;\n await session.thread.setSetting({\n key: FACTORY_OPEN_RUNS_SETTING,\n value: runs.map(run => (run.kickoffId === kickoffId ? { ...run, heartbeatAt: at } : run)),\n });\n });\n}\n\nexport function observeSessionRunEnd(session: RunEndCaptureSession, { audit }: { audit: AuditRecorder }): () => void {\n const usage = usageFor(session);\n return session.subscribe(event => {\n if (event.type === 'usage_update') {\n usage.current.push(usageCounts(event.usage));\n return;\n }\n if (event.type !== 'agent_end' || !event.reason || event.reason === 'suspended') return;\n // Close the turn now so a following turn's steps never land in this run.\n const turn: EndedTurn = { steps: usage.current, claimed: false };\n usage.current = [];\n usage.lastTurn = turn;\n void serializeRunAudit(session, () => recordRunEnds(session, audit, event.reason, undefined, turn));\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentController, AgentControllerThread } from '@mastra/core/agent-controller';
|
|
2
|
+
import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
|
|
3
|
+
/** The Factory session row fields that say which memory resource it uses. */
|
|
4
|
+
export interface FactorySessionOwnership {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
orgId?: string;
|
|
8
|
+
factoryProjectId?: string | null;
|
|
9
|
+
branch?: string;
|
|
10
|
+
visibility?: string;
|
|
11
|
+
originSessionId?: string | null;
|
|
12
|
+
}
|
|
13
|
+
/** Run bindings: the server-side record that a session runs work for its Factory. */
|
|
14
|
+
export type SessionRunBindings = Pick<WorkItemsStorage, 'hasRunBindingForSession'>;
|
|
15
|
+
type ThreadLookup = Pick<AgentController<any>, 'queryThreadById'>;
|
|
16
|
+
/**
|
|
17
|
+
* The one memory resource a Factory session runs under. A Slack session uses
|
|
18
|
+
* its Factory's, or its sender's for a DM; a side session uses its Factory's,
|
|
19
|
+
* or its owner's when private; any other session uses its owner's. A session's own thread can say otherwise
|
|
20
|
+
* only within what the server recorded for it: its own id (created before
|
|
21
|
+
* resources were shared, or a run kept on its own resource), and the Factory's
|
|
22
|
+
* only for a session with a run binding. A client can create a thread under the
|
|
23
|
+
* session's id, so the thread alone never makes a session the Factory's.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sessionMemoryResourceId(controller: ThreadLookup | undefined, session: FactorySessionOwnership, runs: SessionRunBindings | undefined): Promise<string>;
|
|
26
|
+
export declare function isRunBound(session: FactorySessionOwnership, runs: SessionRunBindings | undefined): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* A side session works for the session that started it. Otherwise any binding,
|
|
29
|
+
* active or revoked, on any thread: a session that ever ran a role stays a worker.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isWorkerSession(session: FactorySessionOwnership, runs: SessionRunBindings | undefined): Promise<boolean>;
|
|
32
|
+
/** {@link sessionMemoryResourceId} for a caller that already loaded the session's own thread. */
|
|
33
|
+
export declare function memoryResourceOf(session: FactorySessionOwnership, own: {
|
|
34
|
+
resourceId?: string | null;
|
|
35
|
+
} | null | undefined, runBound: boolean): string;
|
|
36
|
+
/** The resource a person's session belongs on for a visibility: its Factory's once shared, its owner's while private. */
|
|
37
|
+
export declare function userSessionResourceFor(session: Pick<FactorySessionOwnership, 'userId' | 'factoryProjectId'>, visibility: 'org' | 'private'): string;
|
|
38
|
+
/**
|
|
39
|
+
* The threads to move when a person's session changes visibility and so moves
|
|
40
|
+
* onto `resourceId`: its own thread and the threads stamped with its id that
|
|
41
|
+
* sit on its owner's or Factory's resource. Threads under the session's own id
|
|
42
|
+
* (from before resources were shared) stay where they are.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sessionThreadsToMove(controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>, session: FactorySessionOwnership, resourceId: string): Promise<string[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a `factorySessionId` stamp naming this session counts on a thread
|
|
47
|
+
* under `resourceId`. Any org member can write a stamped thread under the
|
|
48
|
+
* Factory's resource, so a private session's stamp never counts there: only
|
|
49
|
+
* its own thread (`thread.id === sessionId`) does.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sessionStampCounts(session: FactorySessionOwnership, resourceId: string | null | undefined): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The threads of one Factory session. Sessions share their Factory's or user's
|
|
54
|
+
* resource, so listing by resource would return every session's threads. A
|
|
55
|
+
* session's own thread carries its id, threads it opens are stamped with
|
|
56
|
+
* `factorySessionId`, and a session created before resources were shared keeps
|
|
57
|
+
* its threads under its own id as the resource. Thread ids and stamps are
|
|
58
|
+
* client-writable, so the own and stamped threads only count under the
|
|
59
|
+
* session's memory resource.
|
|
60
|
+
*/
|
|
61
|
+
export declare function listFactorySessionThreads(controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>, session: FactorySessionOwnership, runs: SessionRunBindings | undefined): Promise<AgentControllerThread[]>;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=session-threads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-threads.d.ts","sourceRoot":"","sources":["../../src/session/session-threads.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE5F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,6EAA6E;AAC7E,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,yBAAyB,CAAC,CAAC;AAEnF,KAAK,YAAY,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,YAAY,GAAG,SAAS,EACpC,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,kBAAkB,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,kBAAkB,GAAG,SAAS,oBAOtG;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,kBAAkB,GAAG,SAAS,oBAE3G;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACtD,QAAQ,EAAE,OAAO,UA4BlB;AAED,yHAAyH;AACzH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,QAAQ,GAAG,kBAAkB,CAAC,EACrE,UAAU,EAAE,KAAK,GAAG,SAAS,GAC5B,MAAM,CAER;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,cAAc,GAAG,iBAAiB,CAAC,EAC1E,OAAO,EAAE,uBAAuB,EAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEnH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,cAAc,GAAG,iBAAiB,CAAC,EAC1E,OAAO,EAAE,uBAAuB,EAChC,IAAI,EAAE,kBAAkB,GAAG,SAAS,GACnC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAgBlC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//#region src/session/session-threads.ts
|
|
2
|
+
/**
|
|
3
|
+
* The one memory resource a Factory session runs under. A Slack session uses
|
|
4
|
+
* its Factory's, or its sender's for a DM; a side session uses its Factory's,
|
|
5
|
+
* or its owner's when private; any other session uses its owner's. A session's own thread can say otherwise
|
|
6
|
+
* only within what the server recorded for it: its own id (created before
|
|
7
|
+
* resources were shared, or a run kept on its own resource), and the Factory's
|
|
8
|
+
* only for a session with a run binding. A client can create a thread under the
|
|
9
|
+
* session's id, so the thread alone never makes a session the Factory's.
|
|
10
|
+
*/
|
|
11
|
+
async function sessionMemoryResourceId(controller, session, runs) {
|
|
12
|
+
const [own, runBound] = await Promise.all([controller?.queryThreadById({ threadId: session.sessionId }), isRunBound(session, runs)]);
|
|
13
|
+
return memoryResourceOf(session, own, runBound);
|
|
14
|
+
}
|
|
15
|
+
async function isRunBound(session, runs) {
|
|
16
|
+
if (!runs || !session.orgId || !session.factoryProjectId) return false;
|
|
17
|
+
return runs.hasRunBindingForSession({
|
|
18
|
+
orgId: session.orgId,
|
|
19
|
+
factoryProjectId: session.factoryProjectId,
|
|
20
|
+
sessionId: session.sessionId
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A side session works for the session that started it. Otherwise any binding,
|
|
25
|
+
* active or revoked, on any thread: a session that ever ran a role stays a worker.
|
|
26
|
+
*/
|
|
27
|
+
async function isWorkerSession(session, runs) {
|
|
28
|
+
return Boolean(session.originSessionId) || isRunBound(session, runs);
|
|
29
|
+
}
|
|
30
|
+
/** {@link sessionMemoryResourceId} for a caller that already loaded the session's own thread. */
|
|
31
|
+
function memoryResourceOf(session, own, runBound) {
|
|
32
|
+
const factoryProjectId = session.factoryProjectId ?? void 0;
|
|
33
|
+
const shared = (session.branch?.startsWith("slack/") ?? false) || session.originSessionId ? session.visibility === "private" ? session.userId : factoryProjectId : void 0;
|
|
34
|
+
if (shared) {
|
|
35
|
+
if (own?.resourceId && [session.sessionId, shared].includes(own.resourceId)) return own.resourceId;
|
|
36
|
+
return shared;
|
|
37
|
+
}
|
|
38
|
+
const allowed = [
|
|
39
|
+
session.sessionId,
|
|
40
|
+
session.userId,
|
|
41
|
+
...(runBound || session.visibility === "org") && factoryProjectId ? [factoryProjectId] : []
|
|
42
|
+
];
|
|
43
|
+
if (own?.resourceId && allowed.includes(own.resourceId)) return own.resourceId;
|
|
44
|
+
return userSessionResourceFor(session, session.visibility === "org" ? "org" : "private");
|
|
45
|
+
}
|
|
46
|
+
/** The resource a person's session belongs on for a visibility: its Factory's once shared, its owner's while private. */
|
|
47
|
+
function userSessionResourceFor(session, visibility) {
|
|
48
|
+
return visibility === "org" && session.factoryProjectId ? session.factoryProjectId : session.userId;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The threads to move when a person's session changes visibility and so moves
|
|
52
|
+
* onto `resourceId`: its own thread and the threads stamped with its id that
|
|
53
|
+
* sit on its owner's or Factory's resource. Threads under the session's own id
|
|
54
|
+
* (from before resources were shared) stay where they are.
|
|
55
|
+
*/
|
|
56
|
+
async function sessionThreadsToMove(controller, session, resourceId) {
|
|
57
|
+
const movable = /* @__PURE__ */ new Set([session.userId, ...session.factoryProjectId ? [session.factoryProjectId] : []]);
|
|
58
|
+
const [own, stamped] = await Promise.all([controller.queryThreadById({ threadId: session.sessionId }), controller.queryThreads({ metadata: { factorySessionId: session.sessionId } })]);
|
|
59
|
+
const threads = [...own ? [own] : [], ...stamped].filter((thread) => thread.resourceId !== resourceId && movable.has(thread.resourceId));
|
|
60
|
+
return [...new Set(threads.map((thread) => thread.id))];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether a `factorySessionId` stamp naming this session counts on a thread
|
|
64
|
+
* under `resourceId`. Any org member can write a stamped thread under the
|
|
65
|
+
* Factory's resource, so a private session's stamp never counts there: only
|
|
66
|
+
* its own thread (`thread.id === sessionId`) does.
|
|
67
|
+
*/
|
|
68
|
+
function sessionStampCounts(session, resourceId) {
|
|
69
|
+
return !(session.visibility === "private" && !!session.factoryProjectId && resourceId === session.factoryProjectId);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The threads of one Factory session. Sessions share their Factory's or user's
|
|
73
|
+
* resource, so listing by resource would return every session's threads. A
|
|
74
|
+
* session's own thread carries its id, threads it opens are stamped with
|
|
75
|
+
* `factorySessionId`, and a session created before resources were shared keeps
|
|
76
|
+
* its threads under its own id as the resource. Thread ids and stamps are
|
|
77
|
+
* client-writable, so the own and stamped threads only count under the
|
|
78
|
+
* session's memory resource.
|
|
79
|
+
*/
|
|
80
|
+
async function listFactorySessionThreads(controller, session, runs) {
|
|
81
|
+
const { sessionId } = session;
|
|
82
|
+
const [own, runBound, stamped, legacy] = await Promise.all([
|
|
83
|
+
controller.queryThreadById({ threadId: sessionId }),
|
|
84
|
+
isRunBound(session, runs),
|
|
85
|
+
controller.queryThreads({ metadata: { factorySessionId: sessionId } }),
|
|
86
|
+
controller.queryThreads({ resourceId: sessionId })
|
|
87
|
+
]);
|
|
88
|
+
const resourceId = memoryResourceOf(session, own, runBound);
|
|
89
|
+
const threads = /* @__PURE__ */ new Map();
|
|
90
|
+
const owned = [...own?.resourceId === resourceId ? [own] : [], ...sessionStampCounts(session, resourceId) ? stamped.filter((thread) => thread.resourceId === resourceId) : []];
|
|
91
|
+
for (const thread of [...owned, ...legacy]) threads.set(thread.id, thread);
|
|
92
|
+
return [...threads.values()];
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { isRunBound, isWorkerSession, listFactorySessionThreads, memoryResourceOf, sessionMemoryResourceId, sessionStampCounts, sessionThreadsToMove, userSessionResourceFor };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=session-threads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-threads.js","names":[],"sources":["../../src/session/session-threads.ts"],"sourcesContent":["import type { AgentController, AgentControllerThread } from '@mastra/core/agent-controller';\n\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** The Factory session row fields that say which memory resource it uses. */\nexport interface FactorySessionOwnership {\n sessionId: string;\n userId: string;\n orgId?: string;\n factoryProjectId?: string | null;\n branch?: string;\n visibility?: string;\n originSessionId?: string | null;\n}\n\n/** Run bindings: the server-side record that a session runs work for its Factory. */\nexport type SessionRunBindings = Pick<WorkItemsStorage, 'hasRunBindingForSession'>;\n\ntype ThreadLookup = Pick<AgentController<any>, 'queryThreadById'>;\n\n/**\n * The one memory resource a Factory session runs under. A Slack session uses\n * its Factory's, or its sender's for a DM; a side session uses its Factory's,\n * or its owner's when private; any other session uses its owner's. A session's own thread can say otherwise\n * only within what the server recorded for it: its own id (created before\n * resources were shared, or a run kept on its own resource), and the Factory's\n * only for a session with a run binding. A client can create a thread under the\n * session's id, so the thread alone never makes a session the Factory's.\n */\nexport async function sessionMemoryResourceId(\n controller: ThreadLookup | undefined,\n session: FactorySessionOwnership,\n runs: SessionRunBindings | undefined,\n): Promise<string> {\n const [own, runBound] = await Promise.all([\n controller?.queryThreadById({ threadId: session.sessionId }),\n isRunBound(session, runs),\n ]);\n return memoryResourceOf(session, own, runBound);\n}\n\nexport async function isRunBound(session: FactorySessionOwnership, runs: SessionRunBindings | undefined) {\n if (!runs || !session.orgId || !session.factoryProjectId) return false;\n return runs.hasRunBindingForSession({\n orgId: session.orgId,\n factoryProjectId: session.factoryProjectId,\n sessionId: session.sessionId,\n });\n}\n\n/**\n * A side session works for the session that started it. Otherwise any binding,\n * active or revoked, on any thread: a session that ever ran a role stays a worker.\n */\nexport async function isWorkerSession(session: FactorySessionOwnership, runs: SessionRunBindings | undefined) {\n return Boolean(session.originSessionId) || isRunBound(session, runs);\n}\n\n/** {@link sessionMemoryResourceId} for a caller that already loaded the session's own thread. */\nexport function memoryResourceOf(\n session: FactorySessionOwnership,\n own: { resourceId?: string | null } | null | undefined,\n runBound: boolean,\n) {\n const factoryProjectId = session.factoryProjectId ?? undefined;\n const slack = session.branch?.startsWith('slack/') ?? false;\n // A private Slack DM or side session stays with its owner.\n const shared =\n slack || session.originSessionId\n ? session.visibility === 'private'\n ? session.userId\n : factoryProjectId\n : undefined;\n if (shared) {\n if (own?.resourceId && [session.sessionId, shared].includes(own.resourceId)) return own.resourceId;\n return shared;\n }\n // A session a person started runs on their resource while private and its\n // Factory's once its owner shares it, which moves its threads there. Its own\n // thread says where it is; a client cannot make a private session the\n // Factory's by writing that thread, since the Factory is only allowed for a\n // shared or run-bound session.\n const allowed = [\n session.sessionId,\n session.userId,\n ...((runBound || session.visibility === 'org') && factoryProjectId ? [factoryProjectId] : []),\n ];\n if (own?.resourceId && allowed.includes(own.resourceId)) return own.resourceId;\n // No thread yet: where a session with this visibility starts.\n return userSessionResourceFor(session, session.visibility === 'org' ? 'org' : 'private');\n}\n\n/** The resource a person's session belongs on for a visibility: its Factory's once shared, its owner's while private. */\nexport function userSessionResourceFor(\n session: Pick<FactorySessionOwnership, 'userId' | 'factoryProjectId'>,\n visibility: 'org' | 'private',\n): string {\n return visibility === 'org' && session.factoryProjectId ? session.factoryProjectId : session.userId;\n}\n\n/**\n * The threads to move when a person's session changes visibility and so moves\n * onto `resourceId`: its own thread and the threads stamped with its id that\n * sit on its owner's or Factory's resource. Threads under the session's own id\n * (from before resources were shared) stay where they are.\n */\nexport async function sessionThreadsToMove(\n controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>,\n session: FactorySessionOwnership,\n resourceId: string,\n): Promise<string[]> {\n const movable = new Set([session.userId, ...(session.factoryProjectId ? [session.factoryProjectId] : [])]);\n const [own, stamped] = await Promise.all([\n controller.queryThreadById({ threadId: session.sessionId }),\n controller.queryThreads({ metadata: { factorySessionId: session.sessionId } }),\n ]);\n const threads = [...(own ? [own] : []), ...stamped].filter(\n thread => thread.resourceId !== resourceId && movable.has(thread.resourceId),\n );\n return [...new Set(threads.map(thread => thread.id))];\n}\n\n/**\n * Whether a `factorySessionId` stamp naming this session counts on a thread\n * under `resourceId`. Any org member can write a stamped thread under the\n * Factory's resource, so a private session's stamp never counts there: only\n * its own thread (`thread.id === sessionId`) does.\n */\nexport function sessionStampCounts(session: FactorySessionOwnership, resourceId: string | null | undefined): boolean {\n return !(session.visibility === 'private' && !!session.factoryProjectId && resourceId === session.factoryProjectId);\n}\n\n/**\n * The threads of one Factory session. Sessions share their Factory's or user's\n * resource, so listing by resource would return every session's threads. A\n * session's own thread carries its id, threads it opens are stamped with\n * `factorySessionId`, and a session created before resources were shared keeps\n * its threads under its own id as the resource. Thread ids and stamps are\n * client-writable, so the own and stamped threads only count under the\n * session's memory resource.\n */\nexport async function listFactorySessionThreads(\n controller: Pick<AgentController<any>, 'queryThreads' | 'queryThreadById'>,\n session: FactorySessionOwnership,\n runs: SessionRunBindings | undefined,\n): Promise<AgentControllerThread[]> {\n const { sessionId } = session;\n const [own, runBound, stamped, legacy] = await Promise.all([\n controller.queryThreadById({ threadId: sessionId }),\n isRunBound(session, runs),\n controller.queryThreads({ metadata: { factorySessionId: sessionId } }),\n controller.queryThreads({ resourceId: sessionId }),\n ]);\n const resourceId = memoryResourceOf(session, own, runBound);\n const threads = new Map<string, AgentControllerThread>();\n const owned = [\n ...(own?.resourceId === resourceId ? [own] : []),\n ...(sessionStampCounts(session, resourceId) ? stamped.filter(thread => thread.resourceId === resourceId) : []),\n ];\n for (const thread of [...owned, ...legacy]) threads.set(thread.id, thread);\n return [...threads.values()];\n}\n"],"mappings":"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"}
|
|
@@ -36,10 +36,10 @@ function authIdentity(requestContext) {
|
|
|
36
36
|
userId: getFactoryAuthUserId(user)
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
async function resolveProviderSession(
|
|
39
|
+
async function resolveProviderSession(sessionId, providers) {
|
|
40
40
|
const matches = (await Promise.all(providers.map(async (provider) => ({
|
|
41
41
|
provider,
|
|
42
|
-
session: await provider.storage.sessions.getBySessionId(
|
|
42
|
+
session: await provider.storage.sessions.getBySessionId(sessionId)
|
|
43
43
|
})))).filter((match) => match.session !== null);
|
|
44
44
|
if (matches.length !== 1) throw new Error(matches.length === 0 ? "The active session is not backed by a source-control repository." : "The active session is ambiguous across source-control providers.");
|
|
45
45
|
return matches[0];
|
|
@@ -47,8 +47,8 @@ async function resolveProviderSession(resourceId, providers) {
|
|
|
47
47
|
async function resolveSessionTarget(requestContext, providers, options = {}) {
|
|
48
48
|
const context = requestContext.get("controller");
|
|
49
49
|
const { orgId, userId } = authIdentity(requestContext);
|
|
50
|
-
if (!context?.
|
|
51
|
-
const { provider, session } = await resolveProviderSession(context.
|
|
50
|
+
if (!context?.sessionId || !orgId || !userId) throw new Error("Source-control tools require an authenticated repository session.");
|
|
51
|
+
const { provider, session } = await resolveProviderSession(context.sessionId, providers);
|
|
52
52
|
if (session.orgId !== orgId || session.visibility === "private" && session.userId !== userId) throw new Error("The active source-control session is not available to the authenticated user.");
|
|
53
53
|
const ownRepository = session.projectRepositoryId ? await provider.storage.projectRepositories.get({
|
|
54
54
|
orgId,
|
|
@@ -102,9 +102,9 @@ async function resolveSessionTarget(requestContext, providers, options = {}) {
|
|
|
102
102
|
* descriptions. `undefined` when the session cannot be resolved; the tools
|
|
103
103
|
* then describe the default without naming it.
|
|
104
104
|
*/
|
|
105
|
-
async function describeSessionRepositories(
|
|
105
|
+
async function describeSessionRepositories(sessionId, providers) {
|
|
106
106
|
try {
|
|
107
|
-
const { provider, session } = await resolveProviderSession(
|
|
107
|
+
const { provider, session } = await resolveProviderSession(sessionId, providers);
|
|
108
108
|
const repositories = await resolveSessionRepositories({
|
|
109
109
|
sourceControl: provider.storage,
|
|
110
110
|
session
|
|
@@ -152,8 +152,8 @@ async function createSourceControlTools({ requestContext, providers, audit, test
|
|
|
152
152
|
if (!requestContext || typeof requestContext.get !== "function") return {};
|
|
153
153
|
const controller = requestContext.get("controller");
|
|
154
154
|
const identity = authIdentity(requestContext);
|
|
155
|
-
if (!controller?.
|
|
156
|
-
const repositoryArgument = repositoryArgumentDescription(await describeSessionRepositories(controller.
|
|
155
|
+
if (!controller?.sessionId || !identity.orgId || !identity.userId || providers.length === 0) return {};
|
|
156
|
+
const repositoryArgument = repositoryArgumentDescription(await describeSessionRepositories(controller.sessionId, providers));
|
|
157
157
|
const withTarget = (options) => resolveSessionTarget(requestContext, providers, options);
|
|
158
158
|
const targetFor = (input) => withTarget(input.repository !== void 0 ? { repository: input.repository } : void 0);
|
|
159
159
|
const reference = async (target) => ({
|