@mastra/factory 0.24.0-alpha.8 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +111 -19
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +1 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +24 -13
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +53 -9
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.d.ts +7 -4
- package/dist/integrations/github/subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/subscriptions.js +5 -5
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js +10 -6
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/subscriptions.js +5 -5
- package/dist/integrations/gitlab/subscriptions.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +16 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +87 -34
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +0 -1
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +1 -4
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +1 -1
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/attention-activity.d.ts.map +1 -1
- package/dist/routes/attention-activity.js +5 -2
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.d.ts +0 -1
- package/dist/routes/attention-providers.d.ts.map +1 -1
- package/dist/routes/attention-providers.js +5 -6
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/config.d.ts +6 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +20 -19
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +33 -31
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +6 -1
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/session-access.d.ts +55 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +165 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.d.ts +2 -4
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +19 -65
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +6 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +95 -12
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +5 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +11 -9
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +10 -10
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +4 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +36 -4
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/secret-encryption.d.ts.map +1 -1
- package/dist/secret-encryption.js +65 -25
- package/dist/secret-encryption.js.map +1 -1
- package/dist/session/channel-reply-processor.d.ts +7 -6
- package/dist/session/channel-reply-processor.d.ts.map +1 -1
- package/dist/session/channel-reply-processor.js +9 -9
- package/dist/session/channel-reply-processor.js.map +1 -1
- package/dist/session/channel-session-link-processor.js +1 -0
- package/dist/session/channel-session-link-processor.js.map +1 -1
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +7 -6
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/default-model-hydration.d.ts +1 -0
- package/dist/session/default-model-hydration.d.ts.map +1 -1
- package/dist/session/default-model-hydration.js +3 -2
- package/dist/session/default-model-hydration.js.map +1 -1
- package/dist/session/environment-state-processor.js +1 -1
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +3 -3
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +16 -17
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +1 -1
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +1 -1
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +1 -1
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +1 -1
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/live-sessions.d.ts +1 -1
- package/dist/session/live-sessions.js +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +1 -1
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +105 -0
- package/dist/session/resource-migration.d.ts.map +1 -0
- package/dist/session/resource-migration.js +195 -0
- package/dist/session/resource-migration.js.map +1 -0
- package/dist/session/session-threads.d.ts +58 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +90 -0
- package/dist/session/session-threads.js.map +1 -0
- package/dist/session/source-control-tools.js +8 -8
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +3 -4
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session/thread-session-stamp.d.ts +21 -0
- package/dist/session/thread-session-stamp.d.ts.map +1 -0
- package/dist/session/thread-session-stamp.js +20 -0
- package/dist/session/thread-session-stamp.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +1 -1
- package/dist/session/thread-title-mirror.d.ts.map +1 -1
- package/dist/session/thread-title-mirror.js +1 -1
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/skills/service.d.ts +6 -8
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +3 -3
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +2 -2
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +8 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +7 -0
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +19 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +42 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +5 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +14 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +25 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/approval-tools.js +1 -1
- package/dist/tools/approval-tools.js.map +1 -1
- package/dist/tools/deliver.d.ts +8 -5
- package/dist/tools/deliver.d.ts.map +1 -1
- package/dist/tools/deliver.js +22 -8
- package/dist/tools/deliver.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +203 -72
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/permissions.d.ts +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +1 -1
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +10 -3
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +3 -2
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +33 -16
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/side-sessions.d.ts +1 -1
- package/dist/tools/side-sessions.d.ts.map +1 -1
- package/dist/tools/side-sessions.js +1 -1
- package/dist/tools/side-sessions.js.map +1 -1
- package/dist/workspace.d.ts +5 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +28 -15
- package/dist/workspace.js.map +1 -1
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"board-version.js","names":[
|
|
1
|
+
{"version":3,"file":"board-version.js","names":[],"sources":["../../../../src/storage/domains/work-items/board-version.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { CollectionSchema, CollectionWhere, FactoryStorageOps } from '@mastra/core/storage';\n\n/**\n * One row per project whose `version` is replaced after every committed write to that project's\n * `work_items`. Readers of the full card list compare it against the version their snapshot was\n * taken at, so an unchanged board costs one indexed row read instead of every card.\n */\nexport const FACTORY_BOARD_VERSIONS_SCHEMA: CollectionSchema = {\n name: 'factory_board_versions',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n version: { type: 'text' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'factory_board_versions_scope_unique', columns: ['org_id', 'factory_project_id'] }],\n};\n\n/** A project, or every project in an org when a write's filter only names the org. */\nexport interface BoardScope {\n orgId: string;\n factoryProjectId?: string;\n}\n\nconst scopeKey = (scope: BoardScope) => `${scope.orgId}\\u0000${scope.factoryProjectId ?? ''}`;\n\nfunction scopeOfRow(row: Record<string, unknown> | null): BoardScope | undefined {\n if (!row || typeof row.org_id !== 'string') return undefined;\n return typeof row.factory_project_id === 'string'\n ? { orgId: row.org_id, factoryProjectId: row.factory_project_id }\n : { orgId: row.org_id };\n}\n\nfunction scopeOfWhere(where: CollectionWhere): BoardScope | undefined {\n if (typeof where.org_id !== 'string') return undefined;\n return typeof where.factory_project_id === 'string'\n ? { orgId: where.org_id, factoryProjectId: where.factory_project_id }\n : { orgId: where.org_id };\n}\n\n/**\n * Wrap `ops` so every write to `work_items` reports the board it changed, once the write has\n * resolved. Writes outside any tenant (`dangerouslyClearAll`) report nothing.\n */\nexport function trackBoardWrites(\n ops: FactoryStorageOps,\n changed: (scope: BoardScope) => void | Promise<void>,\n): FactoryStorageOps {\n const report = async (scope: BoardScope | undefined) => {\n if (scope) await changed(scope);\n };\n return new Proxy(ops, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n if (typeof value !== 'function') return value;\n switch (property) {\n case 'updateAtomic':\n return async (collection: string, where: unknown, updater: (row: unknown) => unknown, ...args: unknown[]) => {\n let patched = false;\n const row = await value.call(\n target,\n collection,\n where,\n async (current: unknown) => {\n const patch = await updater(current);\n patched = patch != null;\n return patch;\n },\n ...args,\n );\n if (collection === 'work_items' && patched) await report(scopeOfRow(row));\n return row;\n };\n case 'insertOne':\n case 'upsertOne':\n return async (collection: string, ...args: unknown[]) => {\n const row = await value.call(target, collection, ...args);\n if (collection === 'work_items') await report(scopeOfRow(row));\n return row;\n };\n case 'updateMany':\n case 'deleteMany':\n return async (collection: string, where: CollectionWhere, ...args: unknown[]) => {\n const count: number = await value.call(target, collection, where, ...args);\n if (collection === 'work_items' && count > 0) await report(scopeOfWhere(where));\n return count;\n };\n default:\n return value.bind(target);\n }\n },\n });\n}\n\n/** Collects the boards a transaction changed so they can be bumped once it has committed. */\nexport class BoardScopes {\n readonly #scopes = new Map<string, BoardScope>();\n\n add = (scope: BoardScope): void => {\n this.#scopes.set(scopeKey(scope), scope);\n };\n\n values(): BoardScope[] {\n return [...this.#scopes.values()];\n }\n}\n\nexport async function bumpBoardVersions(ops: FactoryStorageOps, scopes: readonly BoardScope[]): Promise<void> {\n for (const scope of scopes) {\n const version = randomUUID();\n const updated_at = new Date();\n if (scope.factoryProjectId) {\n await ops.upsertOne(FACTORY_BOARD_VERSIONS_SCHEMA.name, ['org_id', 'factory_project_id'], {\n org_id: scope.orgId,\n factory_project_id: scope.factoryProjectId,\n version,\n updated_at,\n });\n } else {\n // Projects without a row yet have no version, which never matches a snapshot.\n await ops.updateMany(FACTORY_BOARD_VERSIONS_SCHEMA.name, { org_id: scope.orgId }, { version, updated_at });\n }\n }\n}\n\nexport async function readBoardVersion(\n ops: FactoryStorageOps,\n orgId: string,\n factoryProjectId: string,\n): Promise<string | undefined> {\n const row = await ops.findOne<{ version: string }>(FACTORY_BOARD_VERSIONS_SCHEMA.name, {\n org_id: orgId,\n factory_project_id: factoryProjectId,\n });\n return row?.version;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.js","names":[],"sources":["../../../../src/storage/domains/work-items/metrics.ts"],"sourcesContent":["/**\n * Aggregation math for the Factory Overview page.\n *\n * Pure functions over `work_items` rows — throughput, lead time, in-flight\n * count, demand mix and per-stage agent coverage, all read from the\n * server-appended `stageHistory` log. Keeping this DB-free makes the math unit\n * testable and lets the route stay a thin shell.\n *\n * Everything windowed is counted as an event that happened inside the window,\n * never as \"the state the board happens to be in now\", so re-querying a past\n * window always returns the same numbers.\n */\n\nimport { isAgentActor } from './base.js';\nimport type { WorkItemRow } from './base.js';\n\n/** Default window span (days) when the request omits or malforms the range. */\nexport const DEFAULT_METRICS_WINDOW = 30;\n/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */\nexport const MAX_METRICS_WINDOW = 366;\n\nconst DAY_MS = 86_400_000;\n\n/** Terminal stage — items here count as completed, not in-flight. */\nconst DONE_STAGE = 'done';\n\n/** Terminal stage for tracked non-completions — never a completion. */\nconst CANCELED_STAGE = 'canceled';\n\n/**\n * Terminal stages — items holding only these are not in-flight. `done` is a\n * completion (feeds throughput/lead time); `canceled` is a tracked\n * non-completion outcome and feeds neither.\n */\nconst TERMINAL_STAGES = new Set([DONE_STAGE, CANCELED_STAGE]);\n\nconst INTAKE_STAGE = 'intake';\n\n/**\n * Pipeline work excludes the inbox as well as the terminal stages: an intake\n * card is queued, not in flight, and its pass through is the poller filing it.\n */\nfunction isPipelineStage(stage: string): boolean {\n return !TERMINAL_STAGES.has(stage) && stage !== INTAKE_STAGE;\n}\n\n/**\n * Cards the Factory ran: starting a run records its session on the row. The\n * integrations sync every issue and PR of a connected repo into the board and\n * those outnumber the Factory's own work by an order of magnitude, so counting\n * them reports the upstream repo's flow as the Factory's.\n */\nfunction hasFactoryRun(item: WorkItemRow): boolean {\n return Object.keys(item.sessions).length > 0;\n}\n\n/**\n * Flow metrics over the cards the Factory ran ({@link hasFactoryRun}) — synced\n * upstream issues and PRs nobody started a run on are not the Factory's work\n * and are excluded from every field below.\n */\nexport interface FactoryMetrics {\n /**\n * Days the series covers: the requested window clipped to the board's life.\n * Days before the first card could hold no completion, so counting them would\n * drag the per-day rate toward zero on a young board.\n */\n daysCovered: number;\n /** Entries into `done` per UTC day, gap-filled across the covered days. */\n throughput: { date: string; count: number }[];\n /** Card creation → `done` for every completion that landed in the window. */\n leadTime: { medianMs: number | null; p90Ms: number | null; samples: number };\n /** Distinct cards in a pipeline stage — past intake, not yet terminal. */\n wipTotal: number;\n /** Cards created in the window, by source. */\n sourceMix: { source: string; count: number }[];\n /** Per-stage agent coverage over first visits that ended in the window. */\n agentCoverage: {\n stage: string;\n /**\n * First visits to this stage that ended in the window. Repeat visits are\n * excluded from both sides: they are rework, already reported as such, and\n * counting them in the denominator alone caps a fully agent-run stage below\n * 100%.\n */\n passes: number;\n /**\n * Of those: passes an agent finished (`exitedBy` is an agent actor). The\n * entry actor is not required — the rules engine is what queues a card into\n * a stage, so demanding both ends would report 0% for stages agents run\n * end to end. Missing `exitedBy` (entries written before exit stamping)\n * does not count.\n */\n byAgent: number;\n /**\n * Outcomes of the agent-finished passes' items as of the window's end,\n * mutually exclusive, first match wins: `reworked` (a later visit to the\n * same stage — deliberately outranks `done`: a pass that needed a redo is a\n * failed pass even if the item eventually merged), then `done`, then\n * `canceled`, then `inFlight`.\n */\n outcomes: { done: number; canceled: number; reworked: number; inFlight: number };\n }[];\n}\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */\nconst ZONED_DATETIME_RE = /(?:[Zz]|[+-]\\d{2}:?\\d{2})$/;\n\nfunction parseRangeParam(value: unknown, boundary: 'from' | 'to'): number | undefined {\n if (typeof value !== 'string' || value.length === 0) return undefined;\n const dateOnly = DATE_ONLY_RE.test(value);\n // Timezone-less datetimes are parsed as server-local by Date.parse, so the\n // window would shift by deployment region — reject them as invalid.\n if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return undefined;\n const time = Date.parse(value);\n if (Number.isNaN(time)) return undefined;\n return boundary === 'to' && dateOnly ? time + DAY_MS : time;\n}\n\nfunction utcDayStart(time: number): number {\n return Date.parse(`${utcDay(time)}T00:00:00Z`);\n}\n\n/**\n * Resolve untrusted `from`/`to` into a bounded half-open UTC window. A date-only\n * `to` covers the whole day; an open/future end resolves to the end of the\n * current UTC day (not `now`) so an event at this instant stays inside the\n * window instead of on its excluded edge.\n */\nexport function parseMetricsRange(\n fromParam: unknown,\n toParam: unknown,\n now: Date,\n): { windowStart: number; windowEnd: number } {\n const nowMs = now.getTime();\n const endOfToday = utcDayStart(nowMs) + DAY_MS;\n const requestedEnd = parseRangeParam(toParam, 'to') ?? endOfToday;\n const windowEnd = Math.min(requestedEnd, endOfToday);\n const lastIncludedDay = utcDayStart(windowEnd - 1);\n const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;\n const parsedFrom = parseRangeParam(fromParam, 'from');\n let windowStart = parsedFrom !== undefined && parsedFrom < windowEnd ? parsedFrom : defaultStart;\n const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;\n if (windowStart < earliestStart) windowStart = earliestStart;\n return { windowStart, windowEnd };\n}\n\n/** Stage history is server-appended, so an unparsable stamp is a corrupt row. */\nfunction parseTime(iso: string): number {\n const time = Date.parse(iso);\n if (Number.isNaN(time)) throw new Error(`Unparsable stage-history timestamp: ${iso}`);\n return time;\n}\n\n/** Asserted up front so a corrupt row fails every window, not just the ones that read it. */\nfunction assertParsableHistory(items: WorkItemRow[]): void {\n for (const item of items) {\n for (const entry of item.stageHistory) {\n parseTime(entry.enteredAt);\n if (entry.exitedAt !== undefined) parseTime(entry.exitedAt);\n }\n }\n}\n\n/** Nearest-rank percentile over an unsorted sample list. */\nfunction percentile(samples: number[], fraction: number): number | null {\n if (samples.length === 0) return null;\n const sorted = [...samples].sort((a, b) => a - b);\n const rank = Math.max(1, Math.ceil(fraction * sorted.length));\n return sorted[rank - 1]!;\n}\n\n/** UTC `YYYY-MM-DD` for a timestamp. */\nfunction utcDay(time: number): string {\n return new Date(time).toISOString().slice(0, 10);\n}\n\n/** Stages the item was holding at `time`, replayed from its history. */\nfunction stagesHeldAt(item: WorkItemRow, time: number): Set<string> {\n const held = new Set<string>();\n for (const entry of item.stageHistory) {\n if (parseTime(entry.enteredAt) >= time) continue;\n if (entry.exitedAt !== undefined && parseTime(entry.exitedAt) <= time) continue;\n held.add(entry.stage);\n }\n return held;\n}\n\ntype Window = { windowStart: number; windowEnd: number };\n\n/**\n * Completions per UTC day, plus one lead-time sample each. A completion is an\n * entry *into* `done`, not the state of the card now: a card reopened today\n * must not erase the day it shipped, and a card that shipped twice shipped\n * twice. Days before the oldest card are left out rather than gap-filled with\n * zeroes that would drag the daily average down.\n */\nfunction completions(items: WorkItemRow[], { windowStart, windowEnd }: Window) {\n let earliestItem = Infinity;\n for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());\n const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;\n\n const byDay = new Map<string, number>();\n for (let day = Math.max(utcDayStart(windowStart), boardStart); day < windowEnd; day += DAY_MS) {\n byDay.set(utcDay(day), 0);\n }\n\n const leadSamples: number[] = [];\n for (const item of items) {\n for (const entry of item.stageHistory) {\n if (entry.stage !== DONE_STAGE) continue;\n const doneAt = parseTime(entry.enteredAt);\n if (doneAt < windowStart || doneAt >= windowEnd) continue;\n byDay.set(utcDay(doneAt), (byDay.get(utcDay(doneAt)) ?? 0) + 1);\n leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));\n }\n }\n return { byDay, leadSamples };\n}\n\n/** Cards holding at least one pipeline stage right now — window-independent. */\nfunction countInFlight(items: WorkItemRow[]): number {\n return items.filter(item => item.stages.some(isPipelineStage)).length;\n}\n\n/** Where the window's cards came from, most common first. */\nfunction demandMix(items: WorkItemRow[], { windowStart, windowEnd }: Window): FactoryMetrics['sourceMix'] {\n const counts = new Map<string, number>();\n for (const item of items) {\n const created = item.createdAt.getTime();\n if (created < windowStart || created >= windowEnd) continue;\n const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual';\n counts.set(source, (counts.get(source) ?? 0) + 1);\n }\n return [...counts.entries()].map(([source, count]) => ({ source, count })).sort((a, b) => b.count - a.count);\n}\n\n/**\n * How much of each stage's work an agent finished, counting a card's first\n * visit to a stage once. Rows appear in insertion order of each stage's first\n * counted exit; only pipeline stages get rows, since intake and terminal\n * stages have no pass to hand over.\n */\nfunction agentCoverage(items: WorkItemRow[], { windowStart, windowEnd }: Window): FactoryMetrics['agentCoverage'] {\n const byStage = new Map<string, FactoryMetrics['agentCoverage'][number]>();\n for (const item of items) {\n const heldAtWindowEnd = stagesHeldAt(item, windowEnd);\n const visited = new Set<string>();\n for (let i = 0; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n if (!isPipelineStage(entry.stage) || visited.has(entry.stage)) continue;\n visited.add(entry.stage);\n if (entry.exitedAt === undefined) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n\n let row = byStage.get(entry.stage);\n if (!row) {\n row = {\n stage: entry.stage,\n passes: 0,\n byAgent: 0,\n outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 },\n };\n byStage.set(entry.stage, row);\n }\n row.passes += 1;\n if (!isAgentActor(entry.exitedBy)) continue;\n row.byAgent += 1;\n\n const reworked = item.stageHistory.some(\n (later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd,\n );\n if (reworked) row.outcomes.reworked += 1;\n else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;\n else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;\n else row.outcomes.inFlight += 1;\n }\n }\n return [...byStage.values()];\n}\n\nexport function computeFactoryMetrics(boardItems: WorkItemRow[], window: Window): FactoryMetrics {\n const items = boardItems.filter(hasFactoryRun);\n assertParsableHistory(items);\n\n const { byDay, leadSamples } = completions(items, window);\n\n return {\n daysCovered: byDay.size,\n throughput: [...byDay.entries()]\n .map(([date, count]) => ({ date, count }))\n .sort((a, b) => a.date.localeCompare(b.date)),\n leadTime: {\n medianMs: percentile(leadSamples, 0.5),\n p90Ms: percentile(leadSamples, 0.9),\n samples: leadSamples.length,\n },\n wipTotal: countInFlight(items),\n sourceMix: demandMix(items, window),\n agentCoverage: agentCoverage(items, window),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"metrics.js","names":[],"sources":["../../../../src/storage/domains/work-items/metrics.ts"],"sourcesContent":["/**\n * Aggregation math for the Factory Overview page.\n *\n * Pure functions over `work_items` rows — throughput, lead time, in-flight\n * count, demand mix and per-stage agent coverage, all read from the\n * server-appended `stageHistory` log. Keeping this DB-free makes the math unit\n * testable and lets the route stay a thin shell.\n *\n * Everything windowed is counted as an event that happened inside the window,\n * never as \"the state the board happens to be in now\", so re-querying a past\n * window always returns the same numbers.\n */\n\nimport { isAgentActor } from './base.js';\nimport type { WorkItemRow } from './base.js';\n\n/** Default window span (days) when the request omits or malforms the range. */\nexport const DEFAULT_METRICS_WINDOW = 30;\n/** Hard cap on the range span (days) — bounds the gap-filled throughput array. */\nexport const MAX_METRICS_WINDOW = 366;\n\nconst DAY_MS = 86_400_000;\n\n/** Terminal stage — items here count as completed, not in-flight. */\nconst DONE_STAGE = 'done';\n\n/** Terminal stage for tracked non-completions — never a completion. */\nconst CANCELED_STAGE = 'canceled';\n\n/**\n * Terminal stages — items holding only these are not in-flight. `done` is a\n * completion (feeds throughput/lead time); `canceled` is a tracked\n * non-completion outcome and feeds neither.\n */\nconst TERMINAL_STAGES = new Set([DONE_STAGE, CANCELED_STAGE]);\n\nconst INTAKE_STAGE = 'intake';\n\n/**\n * Pipeline work excludes the inbox as well as the terminal stages: an intake\n * card is queued, not in flight, and its pass through is the poller filing it.\n */\nfunction isPipelineStage(stage: string): boolean {\n return !TERMINAL_STAGES.has(stage) && stage !== INTAKE_STAGE;\n}\n\n/**\n * Cards the Factory ran: starting a run records its session on the row. The\n * integrations sync every issue and PR of a connected repo into the board and\n * those outnumber the Factory's own work by an order of magnitude, so counting\n * them reports the upstream repo's flow as the Factory's.\n */\nfunction hasFactoryRun(item: WorkItemRow): boolean {\n return Object.keys(item.sessions).length > 0;\n}\n\n/**\n * Flow metrics over the cards the Factory ran ({@link hasFactoryRun}) — synced\n * upstream issues and PRs nobody started a run on are not the Factory's work\n * and are excluded from every field below.\n */\nexport interface FactoryMetrics {\n /**\n * Days the series covers: the requested window clipped to the board's life.\n * Days before the first card could hold no completion, so counting them would\n * drag the per-day rate toward zero on a young board.\n */\n daysCovered: number;\n /** Entries into `done` per UTC day, gap-filled across the covered days. */\n throughput: { date: string; count: number }[];\n /** Card creation → `done` for every completion that landed in the window. */\n leadTime: { medianMs: number | null; p90Ms: number | null; samples: number };\n /** Distinct cards in a pipeline stage — past intake, not yet terminal. */\n wipTotal: number;\n /** Cards created in the window, by source. */\n sourceMix: { source: string; count: number }[];\n /** Per-stage agent coverage over first visits that ended in the window. */\n agentCoverage: {\n stage: string;\n /**\n * First visits to this stage that ended in the window. Repeat visits are\n * excluded from both sides: they are rework, already reported as such, and\n * counting them in the denominator alone caps a fully agent-run stage below\n * 100%.\n */\n passes: number;\n /**\n * Of those: passes an agent finished (`exitedBy` is an agent actor). The\n * entry actor is not required — the rules engine is what queues a card into\n * a stage, so demanding both ends would report 0% for stages agents run\n * end to end. Missing `exitedBy` (entries written before exit stamping)\n * does not count.\n */\n byAgent: number;\n /**\n * Outcomes of the agent-finished passes' items as of the window's end,\n * mutually exclusive, first match wins: `reworked` (a later visit to the\n * same stage — deliberately outranks `done`: a pass that needed a redo is a\n * failed pass even if the item eventually merged), then `done`, then\n * `canceled`, then `inFlight`.\n */\n outcomes: { done: number; canceled: number; reworked: number; inFlight: number };\n }[];\n}\n\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n/** Datetime carrying an explicit `Z` or `±HH:MM` offset. */\nconst ZONED_DATETIME_RE = /(?:[Zz]|[+-]\\d{2}:?\\d{2})$/;\n\nfunction parseRangeParam(value: unknown, boundary: 'from' | 'to'): number | undefined {\n if (typeof value !== 'string' || value.length === 0) return undefined;\n const dateOnly = DATE_ONLY_RE.test(value);\n // Timezone-less datetimes are parsed as server-local by Date.parse, so the\n // window would shift by deployment region — reject them as invalid.\n if (!dateOnly && !ZONED_DATETIME_RE.test(value)) return undefined;\n const time = Date.parse(value);\n if (Number.isNaN(time)) return undefined;\n return boundary === 'to' && dateOnly ? time + DAY_MS : time;\n}\n\nfunction utcDayStart(time: number): number {\n return Date.parse(`${utcDay(time)}T00:00:00Z`);\n}\n\n/**\n * Resolve untrusted `from`/`to` into a bounded half-open UTC window. A date-only\n * `to` covers the whole day; an open/future end resolves to the end of the\n * current UTC day (not `now`) so an event at this instant stays inside the\n * window instead of on its excluded edge.\n */\nexport function parseMetricsRange(\n fromParam: unknown,\n toParam: unknown,\n now: Date,\n): { windowStart: number; windowEnd: number } {\n const nowMs = now.getTime();\n const endOfToday = utcDayStart(nowMs) + DAY_MS;\n const requestedEnd = parseRangeParam(toParam, 'to') ?? endOfToday;\n const windowEnd = Math.min(requestedEnd, endOfToday);\n const lastIncludedDay = utcDayStart(windowEnd - 1);\n const defaultStart = lastIncludedDay - (DEFAULT_METRICS_WINDOW - 1) * DAY_MS;\n const parsedFrom = parseRangeParam(fromParam, 'from');\n let windowStart = parsedFrom !== undefined && parsedFrom < windowEnd ? parsedFrom : defaultStart;\n const earliestStart = lastIncludedDay - (MAX_METRICS_WINDOW - 1) * DAY_MS;\n if (windowStart < earliestStart) windowStart = earliestStart;\n return { windowStart, windowEnd };\n}\n\n/** Stage history is server-appended, so an unparsable stamp is a corrupt row. */\nfunction parseTime(iso: string): number {\n const time = Date.parse(iso);\n if (Number.isNaN(time)) throw new Error(`Unparsable stage-history timestamp: ${iso}`);\n return time;\n}\n\n/** Asserted up front so a corrupt row fails every window, not just the ones that read it. */\nfunction assertParsableHistory(items: WorkItemRow[]): void {\n for (const item of items) {\n for (const entry of item.stageHistory) {\n parseTime(entry.enteredAt);\n if (entry.exitedAt !== undefined) parseTime(entry.exitedAt);\n }\n }\n}\n\n/** Nearest-rank percentile over an unsorted sample list. */\nfunction percentile(samples: number[], fraction: number): number | null {\n if (samples.length === 0) return null;\n const sorted = [...samples].sort((a, b) => a - b);\n const rank = Math.max(1, Math.ceil(fraction * sorted.length));\n return sorted[rank - 1]!;\n}\n\n/** UTC `YYYY-MM-DD` for a timestamp. */\nfunction utcDay(time: number): string {\n return new Date(time).toISOString().slice(0, 10);\n}\n\n/** Stages the item was holding at `time`, replayed from its history. */\nfunction stagesHeldAt(item: WorkItemRow, time: number): Set<string> {\n const held = new Set<string>();\n for (const entry of item.stageHistory) {\n if (parseTime(entry.enteredAt) >= time) continue;\n if (entry.exitedAt !== undefined && parseTime(entry.exitedAt) <= time) continue;\n held.add(entry.stage);\n }\n return held;\n}\n\ntype Window = { windowStart: number; windowEnd: number };\n\n/**\n * Completions per UTC day, plus one lead-time sample each. A completion is an\n * entry *into* `done`, not the state of the card now: a card reopened today\n * must not erase the day it shipped, and a card that shipped twice shipped\n * twice. Days before the oldest card are left out rather than gap-filled with\n * zeroes that would drag the daily average down.\n */\nfunction completions(items: WorkItemRow[], { windowStart, windowEnd }: Window) {\n let earliestItem = Infinity;\n for (const item of items) earliestItem = Math.min(earliestItem, item.createdAt.getTime());\n const boardStart = Number.isFinite(earliestItem) ? utcDayStart(earliestItem) : -Infinity;\n\n const byDay = new Map<string, number>();\n for (let day = Math.max(utcDayStart(windowStart), boardStart); day < windowEnd; day += DAY_MS) {\n byDay.set(utcDay(day), 0);\n }\n\n const leadSamples: number[] = [];\n for (const item of items) {\n for (const entry of item.stageHistory) {\n if (entry.stage !== DONE_STAGE) continue;\n const doneAt = parseTime(entry.enteredAt);\n if (doneAt < windowStart || doneAt >= windowEnd) continue;\n byDay.set(utcDay(doneAt), (byDay.get(utcDay(doneAt)) ?? 0) + 1);\n leadSamples.push(Math.max(0, doneAt - item.createdAt.getTime()));\n }\n }\n return { byDay, leadSamples };\n}\n\n/** Cards holding at least one pipeline stage right now — window-independent. */\nfunction countInFlight(items: WorkItemRow[]): number {\n return items.filter(item => item.stages.some(isPipelineStage)).length;\n}\n\n/** Where the window's cards came from, most common first. */\nfunction demandMix(items: WorkItemRow[], { windowStart, windowEnd }: Window): FactoryMetrics['sourceMix'] {\n const counts = new Map<string, number>();\n for (const item of items) {\n const created = item.createdAt.getTime();\n if (created < windowStart || created >= windowEnd) continue;\n const source = item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual';\n counts.set(source, (counts.get(source) ?? 0) + 1);\n }\n return [...counts.entries()].map(([source, count]) => ({ source, count })).sort((a, b) => b.count - a.count);\n}\n\n/**\n * How much of each stage's work an agent finished, counting a card's first\n * visit to a stage once. Rows appear in insertion order of each stage's first\n * counted exit; only pipeline stages get rows, since intake and terminal\n * stages have no pass to hand over.\n */\nfunction agentCoverage(items: WorkItemRow[], { windowStart, windowEnd }: Window): FactoryMetrics['agentCoverage'] {\n const byStage = new Map<string, FactoryMetrics['agentCoverage'][number]>();\n for (const item of items) {\n const heldAtWindowEnd = stagesHeldAt(item, windowEnd);\n const visited = new Set<string>();\n for (let i = 0; i < item.stageHistory.length; i++) {\n const entry = item.stageHistory[i]!;\n if (!isPipelineStage(entry.stage) || visited.has(entry.stage)) continue;\n visited.add(entry.stage);\n if (entry.exitedAt === undefined) continue;\n const exited = parseTime(entry.exitedAt);\n if (exited < windowStart || exited >= windowEnd) continue;\n\n let row = byStage.get(entry.stage);\n if (!row) {\n row = {\n stage: entry.stage,\n passes: 0,\n byAgent: 0,\n outcomes: { done: 0, canceled: 0, reworked: 0, inFlight: 0 },\n };\n byStage.set(entry.stage, row);\n }\n row.passes += 1;\n if (!isAgentActor(entry.exitedBy)) continue;\n row.byAgent += 1;\n\n const reworked = item.stageHistory.some(\n (later, j) => j > i && later.stage === entry.stage && parseTime(later.enteredAt) < windowEnd,\n );\n if (reworked) row.outcomes.reworked += 1;\n else if (heldAtWindowEnd.has(DONE_STAGE)) row.outcomes.done += 1;\n else if (heldAtWindowEnd.has(CANCELED_STAGE)) row.outcomes.canceled += 1;\n else row.outcomes.inFlight += 1;\n }\n }\n return [...byStage.values()];\n}\n\nexport function computeFactoryMetrics(boardItems: WorkItemRow[], window: Window): FactoryMetrics {\n const items = boardItems.filter(hasFactoryRun);\n assertParsableHistory(items);\n\n const { byDay, leadSamples } = completions(items, window);\n\n return {\n daysCovered: byDay.size,\n throughput: [...byDay.entries()]\n .map(([date, count]) => ({ date, count }))\n .sort((a, b) => a.date.localeCompare(b.date)),\n leadTime: {\n medianMs: percentile(leadSamples, 0.5),\n p90Ms: percentile(leadSamples, 0.9),\n samples: leadSamples.length,\n },\n wipTotal: countInFlight(items),\n sourceMix: demandMix(items, window),\n agentCoverage: agentCoverage(items, window),\n };\n}\n"],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ function createFactoryApprovalTools(scope, deps) {
|
|
|
36
36
|
if (!isOrigin && !follows) throw new Error(`This session doesn't follow session ${sessionId}: follow its work item first, or answer from that session.`);
|
|
37
37
|
const messaging = await deps.messaging();
|
|
38
38
|
if (!messaging) throw new Error("Answering approvals is not available on this server.");
|
|
39
|
-
const live = await messaging.controller.
|
|
39
|
+
const live = await messaging.controller.getSession(sessionId);
|
|
40
40
|
if (!live?.approval.isArmed({ toolCallId })) throw new Error("Nothing is waiting on that tool call any more: the run that asked has ended or it was already answered. Message the session with factory_signal_session if it should try again.");
|
|
41
41
|
const result = live.respondToToolApproval({
|
|
42
42
|
decision,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-tools.js","names":[],"sources":["../../src/tools/approval-tools.ts"],"sourcesContent":["import { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryApprovalToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'listSessionSubscriptions'>;\n auditRecorder: AuditRecorder;\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\n/** The parts of a live session an answer needs: its open approval gates. */\ninterface ApprovalGateSession {\n approval: { isArmed(filter: { toolCallId: string }): boolean };\n respondToToolApproval(input: {\n decision: 'approve' | 'decline';\n toolCallId: string;\n declineContext?: { reason?: string };\n }): { accepted: true } | { accepted: false; reason: string };\n}\n\n/** Answers a tool call another session holds for approval. Only a signed-in person's turn may answer. */\nexport function createFactoryApprovalTools(\n scope: FactoryToolScope,\n deps: FactoryApprovalToolDependencies,\n): IntegrationTools {\n const answer = createTool({\n id: 'factory_answer_approval',\n description: [\n 'Approve or decline a tool call a session you follow is waiting on, as you.',\n 'A `factory-session` notification of kind `approval` names the session, tool, arguments and toolCallId.',\n 'Answer only what the person in this conversation agreed to; decline with a reason the session will see.',\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().uuid().describe('The session holding the call, from the approval notice.'),\n toolCallId: z.string().trim().min(1).max(200),\n decision: z.enum(['approve', 'decline']),\n reason: z.string().trim().min(1).max(1000).optional().describe('Why, when declining.'),\n })\n .strict(),\n execute: async ({ sessionId, toolCallId, decision, reason }) => {\n requirePerson(scope, 'answer an approval');\n const row = await deps.sessions.getBySessionId(sessionId);\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) {\n throw new Error(`Session ${sessionId} was not found in this Factory.`);\n }\n if (row.visibility === 'private' && row.userId !== scope.userId) {\n throw new Error(`Session ${sessionId} is private to its owner.`);\n }\n\n // Only the sessions it reports to may answer it: its origin, or followers of its item.\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n });\n const isOrigin = row.originSessionId === scope.sessionId;\n const follows =\n binding !== null &&\n (\n await deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n })\n ).some(subscription => subscription.workItemId === binding.workItemId);\n if (!isOrigin && !follows) {\n throw new Error(\n `This session doesn't follow session ${sessionId}: follow its work item first, or answer from that session.`,\n );\n }\n\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Answering approvals is not available on this server.');\n const live = (await messaging.controller.
|
|
1
|
+
{"version":3,"file":"approval-tools.js","names":[],"sources":["../../src/tools/approval-tools.ts"],"sourcesContent":["import { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryApprovalToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'listSessionSubscriptions'>;\n auditRecorder: AuditRecorder;\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\n/** The parts of a live session an answer needs: its open approval gates. */\ninterface ApprovalGateSession {\n approval: { isArmed(filter: { toolCallId: string }): boolean };\n respondToToolApproval(input: {\n decision: 'approve' | 'decline';\n toolCallId: string;\n declineContext?: { reason?: string };\n }): { accepted: true } | { accepted: false; reason: string };\n}\n\n/** Answers a tool call another session holds for approval. Only a signed-in person's turn may answer. */\nexport function createFactoryApprovalTools(\n scope: FactoryToolScope,\n deps: FactoryApprovalToolDependencies,\n): IntegrationTools {\n const answer = createTool({\n id: 'factory_answer_approval',\n description: [\n 'Approve or decline a tool call a session you follow is waiting on, as you.',\n 'A `factory-session` notification of kind `approval` names the session, tool, arguments and toolCallId.',\n 'Answer only what the person in this conversation agreed to; decline with a reason the session will see.',\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().uuid().describe('The session holding the call, from the approval notice.'),\n toolCallId: z.string().trim().min(1).max(200),\n decision: z.enum(['approve', 'decline']),\n reason: z.string().trim().min(1).max(1000).optional().describe('Why, when declining.'),\n })\n .strict(),\n execute: async ({ sessionId, toolCallId, decision, reason }) => {\n requirePerson(scope, 'answer an approval');\n const row = await deps.sessions.getBySessionId(sessionId);\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) {\n throw new Error(`Session ${sessionId} was not found in this Factory.`);\n }\n if (row.visibility === 'private' && row.userId !== scope.userId) {\n throw new Error(`Session ${sessionId} is private to its owner.`);\n }\n\n // Only the sessions it reports to may answer it: its origin, or followers of its item.\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n });\n const isOrigin = row.originSessionId === scope.sessionId;\n const follows =\n binding !== null &&\n (\n await deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n })\n ).some(subscription => subscription.workItemId === binding.workItemId);\n if (!isOrigin && !follows) {\n throw new Error(\n `This session doesn't follow session ${sessionId}: follow its work item first, or answer from that session.`,\n );\n }\n\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Answering approvals is not available on this server.');\n const live = (await messaging.controller.getSession(sessionId)) as ApprovalGateSession | null;\n if (!live?.approval.isArmed({ toolCallId })) {\n throw new Error(\n 'Nothing is waiting on that tool call any more: the run that asked has ended or it was already answered. ' +\n 'Message the session with factory_signal_session if it should try again.',\n );\n }\n const result = live.respondToToolApproval({\n decision,\n toolCallId,\n ...(decision === 'decline'\n ? { declineContext: { reason: reason ?? 'Declined by a session following it.' } }\n : {}),\n });\n if (!result.accepted) throw new Error(`The answer was not accepted (${result.reason}).`);\n\n deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: scope.userId,\n actorType: 'human',\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n action: decision === 'approve' ? 'factory.run.approved' : 'factory.run.dismissed',\n factoryProjectId: scope.factoryProjectId,\n targets: [\n { type: 'session', id: sessionId },\n ...(binding ? [{ type: 'work_item' as const, id: binding.workItemId }] : []),\n ],\n metadata: { toolCallId, sessionId, decision, cause: 'factory_tools' },\n });\n return { answered: decision, sessionId, toolCallId };\n },\n });\n\n return { factory_answer_approval: answer };\n}\n"],"mappings":";;;;;AA6BA,SAAgB,2BACd,OACA,MACkB;CAmFlB,OAAO,EAAE,yBAlFM,WAAW;EACxB,IAAI;EACJ,aAAa;GACX;GACA;GACA;EACF,CAAC,CAAC,KAAK,GAAG;EACV,aAAa,EACV,OAAO;GACN,WAAW,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,yDAAyD;GAC/F,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG;GAC5C,UAAU,EAAE,KAAK,CAAC,WAAW,SAAS,CAAC;GACvC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,sBAAsB;EACvF,CAAC,CAAC,CACD,OAAO;EACV,SAAS,OAAO,EAAE,WAAW,YAAY,UAAU,aAAa;GAC9D,cAAc,OAAO,oBAAoB;GACzC,MAAM,MAAM,MAAM,KAAK,SAAS,eAAe,SAAS;GACxD,IAAI,CAAC,OAAO,IAAI,UAAU,MAAM,SAAS,IAAI,qBAAqB,MAAM,kBACtE,MAAM,IAAI,MAAM,WAAW,UAAU,gCAAgC;GAEvE,IAAI,IAAI,eAAe,aAAa,IAAI,WAAW,MAAM,QACvD,MAAM,IAAI,MAAM,WAAW,UAAU,0BAA0B;GAIjE,MAAM,UAAU,MAAM,KAAK,UAAU,+BAA+B;IAClE,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB;GACF,CAAC;GACD,MAAM,WAAW,IAAI,oBAAoB,MAAM;GAC/C,MAAM,UACJ,YAAY,SAEV,MAAM,KAAK,UAAU,yBAAyB;IAC5C,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,WAAW,MAAM;GACnB,CAAC,EAAA,CACD,MAAK,iBAAgB,aAAa,eAAe,QAAQ,UAAU;GACvE,IAAI,CAAC,YAAY,CAAC,SAChB,MAAM,IAAI,MACR,uCAAuC,UAAU,2DACnD;GAGF,MAAM,YAAY,MAAM,KAAK,UAAU;GACvC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,sDAAsD;GACtF,MAAM,OAAQ,MAAM,UAAU,WAAW,WAAW,SAAS;GAC7D,IAAI,CAAC,MAAM,SAAS,QAAQ,EAAE,WAAW,CAAC,GACxC,MAAM,IAAI,MACR,iLAEF;GAEF,MAAM,SAAS,KAAK,sBAAsB;IACxC;IACA;IACA,GAAI,aAAa,YACb,EAAE,gBAAgB,EAAE,QAAQ,UAAU,sCAAsC,EAAE,IAC9E,CAAC;GACP,CAAC;GACD,IAAI,CAAC,OAAO,UAAU,MAAM,IAAI,MAAM,gCAAgC,OAAO,OAAO,GAAG;GAEvF,KAAK,cAAc,OAAO;IACxB,OAAO,MAAM;IACb,SAAS,MAAM;IACf,WAAW;IACX,GAAI,MAAM,eAAe,EAAE,cAAc,MAAM,aAAa,IAAI,CAAC;IACjE,QAAQ,aAAa,YAAY,yBAAyB;IAC1D,kBAAkB,MAAM;IACxB,SAAS,CACP;KAAE,MAAM;KAAW,IAAI;IAAU,GACjC,GAAI,UAAU,CAAC;KAAE,MAAM;KAAsB,IAAI,QAAQ;IAAW,CAAC,IAAI,CAAC,CAC5E;IACA,UAAU;KAAE;KAAY;KAAW;KAAU,OAAO;IAAgB;GACtE,CAAC;GACD,OAAO;IAAE,UAAU;IAAU;IAAW;GAAW;EACrD;CACF,CAEuC,EAAE;AAC3C"}
|
package/dist/tools/deliver.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { MountedMastraCode } from '@mastra/code-sdk';
|
|
2
2
|
import type { SendNotificationSignalInput } from '@mastra/core/notifications';
|
|
3
3
|
import type { SourceControlSessionLookup } from '../session/factory-session.js';
|
|
4
|
+
import type { SessionRunBindings } from '../session/session-threads.js';
|
|
4
5
|
import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
|
|
5
6
|
/** Notifications between Factory sessions all carry this source. */
|
|
6
7
|
export declare const FACTORY_SESSION_NOTIFICATION_SOURCE = "factory-session";
|
|
7
|
-
export type FactoryDeliveryController = Pick<MountedMastraCode['controller'], 'queryThreadById' | 'queryThreads' | 'setThreadModel' | 'listAvailableModels' | '
|
|
8
|
+
export type FactoryDeliveryController = Pick<MountedMastraCode['controller'], 'queryThreadById' | 'queryThreads' | 'setThreadModel' | 'listAvailableModels' | 'getSession' | 'createSession' | 'getCurrentAgent'>;
|
|
8
9
|
export interface FactoryDeliveryTarget {
|
|
9
10
|
/** The target's source-control session id. */
|
|
10
11
|
sessionId: string;
|
|
@@ -39,9 +40,11 @@ export type FactoryDeliveryResult =
|
|
|
39
40
|
* the stored thread: waking a resource no session owns starts a run with no
|
|
40
41
|
* model. A live session's thread is never switched.
|
|
41
42
|
*/
|
|
42
|
-
export declare function deliverFactorySessionNotification({ controller, sessions, target, notification, onlyIfLive, }: {
|
|
43
|
+
export declare function deliverFactorySessionNotification({ controller, sessions, target, notification, onlyIfLive, runs, }: {
|
|
43
44
|
controller: FactoryDeliveryController;
|
|
44
45
|
sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;
|
|
46
|
+
/** Run bindings, which decide whether a session may use its Factory's resource. */
|
|
47
|
+
runs: SessionRunBindings | undefined;
|
|
45
48
|
target: FactoryDeliveryTarget;
|
|
46
49
|
notification: Omit<SendNotificationSignalInput, 'source'>;
|
|
47
50
|
/** Skip, without creating anything, when no live session owns the target (stage notices). */
|
|
@@ -54,15 +57,15 @@ export interface FactorySessionActivity {
|
|
|
54
57
|
lastActivityAt: string | null;
|
|
55
58
|
}
|
|
56
59
|
/** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */
|
|
57
|
-
export declare function sessionActivity(controller: Pick<FactoryDeliveryController, '
|
|
60
|
+
export declare function sessionActivity(controller: Pick<FactoryDeliveryController, 'getSession' | 'queryThreadById'>, sessionId: string, threadId: string): Promise<FactorySessionActivity>;
|
|
58
61
|
/**
|
|
59
62
|
* The thread a message to a session should land on: the thread its role runs
|
|
60
63
|
* on, else the thread it has open, else its most recently updated thread.
|
|
61
64
|
*/
|
|
62
65
|
export declare function resolveSessionThread({ controller, sessions, workItems, sessionId, }: {
|
|
63
|
-
controller: Pick<FactoryDeliveryController, '
|
|
66
|
+
controller: Pick<FactoryDeliveryController, 'getSession' | 'queryThreads' | 'queryThreadById'>;
|
|
64
67
|
sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;
|
|
65
|
-
workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession'>;
|
|
68
|
+
workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'hasRunBindingForSession'>;
|
|
66
69
|
sessionId: string;
|
|
67
70
|
}): Promise<string | null>;
|
|
68
71
|
//# sourceMappingURL=deliver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/tools/deliver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAG9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/tools/deliver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAG9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAA2B,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,oEAAoE;AACpE,eAAO,MAAM,mCAAmC,oBAAoB,CAAC;AAErE,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,iBAAiB,CAAC,YAAY,CAAC,EAC7B,iBAAiB,GACjB,cAAc,GACd,gBAAgB,GAChB,qBAAqB,GACrB,YAAY,GACZ,eAAe,GACf,iBAAiB,CACpB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,qBAAqB;AAC/B;;;;GAIG;AACD;IAAE,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,GAAG,YAAY,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACxG;IACE,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,mBAAmB,GAAG,SAAS,GAAG,WAAW,CAAC;CACjG,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAsB,iCAAiC,CAAC,EACtD,UAAU,EACV,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,UAAkB,EAClB,IAAI,GACL,EAAE;IACD,UAAU,EAAE,yBAAyB,CAAC;IACtC,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,mFAAmF;IACnF,IAAI,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACrC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,EAAE,IAAI,CAAC,2BAA2B,EAAE,QAAQ,CAAC,CAAC;IAC1D,6FAA6F;IAC7F,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAwDjC;AAED,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,OAAO,EAAE,OAAO,CAAC;IACjB,2FAA2F;IAC3F,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,yHAAyH;AACzH,wBAAsB,eAAe,CACnC,UAAU,EAAE,IAAI,CAAC,yBAAyB,EAAE,YAAY,GAAG,iBAAiB,CAAC,EAC7E,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,sBAAsB,CAAC,CAUjC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,UAAU,EACV,QAAQ,EACR,SAAS,EACT,SAAS,GACV,EAAE;IACD,UAAU,EAAE,IAAI,CAAC,yBAAyB,EAAE,YAAY,GAAG,cAAc,GAAG,iBAAiB,CAAC,CAAC;IAC/F,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,gCAAgC,GAAG,yBAAyB,CAAC,CAAC;IAChG,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBzB"}
|
package/dist/tools/deliver.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { listFactorySessionThreads, sessionMemoryResourceId, sessionStampCounts } from "../session/session-threads.js";
|
|
1
2
|
import { createSessionAsRowOwner } from "../integrations/subscription-session.js";
|
|
2
3
|
//#region src/tools/deliver.ts
|
|
3
4
|
/** Notifications between Factory sessions all carry this source. */
|
|
@@ -15,24 +16,26 @@ const FACTORY_SESSION_NOTIFICATION_SOURCE = "factory-session";
|
|
|
15
16
|
* the stored thread: waking a resource no session owns starts a run with no
|
|
16
17
|
* model. A live session's thread is never switched.
|
|
17
18
|
*/
|
|
18
|
-
async function deliverFactorySessionNotification({ controller, sessions, target, notification, onlyIfLive = false }) {
|
|
19
|
+
async function deliverFactorySessionNotification({ controller, sessions, target, notification, onlyIfLive = false, runs }) {
|
|
19
20
|
const thread = await controller.queryThreadById({ threadId: target.threadId });
|
|
20
21
|
if (!thread) return {
|
|
21
22
|
delivered: "skipped",
|
|
22
23
|
reason: "thread-not-found"
|
|
23
24
|
};
|
|
24
|
-
|
|
25
|
+
let targetRow;
|
|
26
|
+
const loadRow = () => targetRow ??= sessions.getBySessionId(target.sessionId);
|
|
27
|
+
if (!await isSessionThread(controller, runs, thread, target.sessionId, loadRow)) return {
|
|
25
28
|
delivered: "skipped",
|
|
26
29
|
reason: "thread-not-owned"
|
|
27
30
|
};
|
|
28
|
-
const ownerResourceId = target.sessionId;
|
|
29
|
-
let session = await controller.
|
|
31
|
+
const ownerResourceId = thread.resourceId || target.sessionId;
|
|
32
|
+
let session = await controller.getSession(target.sessionId);
|
|
30
33
|
if (!session) {
|
|
31
34
|
if (onlyIfLive) return {
|
|
32
35
|
delivered: "skipped",
|
|
33
36
|
reason: "offline"
|
|
34
37
|
};
|
|
35
|
-
const row = await
|
|
38
|
+
const row = await loadRow();
|
|
36
39
|
if (!row?.factoryProjectId) return {
|
|
37
40
|
delivered: "skipped",
|
|
38
41
|
reason: "session-not-found"
|
|
@@ -84,7 +87,7 @@ async function deliverFactorySessionNotification({ controller, sessions, target,
|
|
|
84
87
|
}
|
|
85
88
|
/** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */
|
|
86
89
|
async function sessionActivity(controller, sessionId, threadId) {
|
|
87
|
-
const [live, thread] = await Promise.all([controller.
|
|
90
|
+
const [live, thread] = await Promise.all([controller.getSession(sessionId), controller.queryThreadById({ threadId })]);
|
|
88
91
|
const updatedAt = thread?.updatedAt ? new Date(thread.updatedAt) : null;
|
|
89
92
|
return {
|
|
90
93
|
running: live?.stream.isActive() ?? false,
|
|
@@ -105,9 +108,20 @@ async function resolveSessionThread({ controller, sessions, workItems, sessionId
|
|
|
105
108
|
});
|
|
106
109
|
if (binding) return binding.threadId;
|
|
107
110
|
}
|
|
108
|
-
const current = (await controller.
|
|
111
|
+
const current = (await controller.getSession(sessionId))?.thread.getId();
|
|
109
112
|
if (current) return current;
|
|
110
|
-
return [...await controller
|
|
113
|
+
return [...await listFactorySessionThreads(controller, row ?? {
|
|
114
|
+
sessionId,
|
|
115
|
+
userId: ""
|
|
116
|
+
}, workItems)].sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())[0]?.id ?? null;
|
|
117
|
+
}
|
|
118
|
+
async function isSessionThread(controller, runs, thread, sessionId, loadRow) {
|
|
119
|
+
if (thread.resourceId === sessionId) return true;
|
|
120
|
+
if (thread.id !== sessionId && thread.metadata?.factorySessionId !== sessionId) return false;
|
|
121
|
+
const row = await loadRow();
|
|
122
|
+
if (!row) return false;
|
|
123
|
+
if (thread.id !== sessionId && !sessionStampCounts(row, thread.resourceId)) return false;
|
|
124
|
+
return thread.resourceId === await sessionMemoryResourceId(controller, row, runs);
|
|
111
125
|
}
|
|
112
126
|
//#endregion
|
|
113
127
|
export { FACTORY_SESSION_NOTIFICATION_SOURCE, deliverFactorySessionNotification, resolveSessionThread, sessionActivity };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deliver.js","names":[],"sources":["../../src/tools/deliver.ts"],"sourcesContent":["import type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { SendNotificationSignalInput } from '@mastra/core/notifications';\n\nimport { createSessionAsRowOwner } from '../integrations/subscription-session.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** Notifications between Factory sessions all carry this source. */\nexport const FACTORY_SESSION_NOTIFICATION_SOURCE = 'factory-session';\n\nexport type FactoryDeliveryController = Pick<\n MountedMastraCode['controller'],\n | 'queryThreadById'\n | 'queryThreads'\n | 'setThreadModel'\n | 'listAvailableModels'\n | 'getSessionByResource'\n | 'createSession'\n | 'getCurrentAgent'\n>;\n\nexport interface FactoryDeliveryTarget {\n /** The target's source-control session id. */\n sessionId: string;\n /** The stored thread the notification belongs to. Never the session's current thread by default. */\n threadId: string;\n}\n\nexport type FactoryDeliveryResult =\n /**\n * `woken`: the target was idle and the notification started a run.\n * `mid-run`: the target was already running; it reads the notification when that run\n * next checks its inbox, not now.\n */\n | { delivered: 'woken' | 'mid-run' | 'persisted' | 'summarized'; notificationId: string; reason?: string }\n | {\n delivered: 'skipped';\n notificationId?: string;\n reason: 'thread-not-found' | 'thread-not-owned' | 'session-not-found' | 'offline' | 'discarded';\n };\n\n/**\n * Deliver a notification to a Factory session's stored thread.\n *\n * Sent through the target's agent with an explicit `{ resourceId, threadId }`,\n * never `Session.sendNotificationSignal`: that one builds the wake from the\n * sender's request context and aims at the target's current thread. On this\n * path the SDK builds the wake from the session that owns the target resource,\n * so the woken run is the target row's owner, with that owner's credentials.\n *\n * A target that isn't live is created first, as its row's owner and bound to\n * the stored thread: waking a resource no session owns starts a run with no\n * model. A live session's thread is never switched.\n */\nexport async function deliverFactorySessionNotification({\n controller,\n sessions,\n target,\n notification,\n onlyIfLive = false,\n}: {\n controller: FactoryDeliveryController;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n target: FactoryDeliveryTarget;\n notification: Omit<SendNotificationSignalInput, 'source'>;\n /** Skip, without creating anything, when no live session owns the target (stage notices). */\n onlyIfLive?: boolean;\n}): Promise<FactoryDeliveryResult> {\n const thread = await controller.queryThreadById({ threadId: target.threadId });\n if (!thread) return { delivered: 'skipped', reason: 'thread-not-found' };\n // A Factory session's threads live under its session id. A thread owned by\n // anything else is not this session's, whatever the caller paired it with.\n if (thread.resourceId && thread.resourceId !== target.sessionId) {\n return { delivered: 'skipped', reason: 'thread-not-owned' };\n }\n const ownerResourceId = target.sessionId;\n\n // No request context: without a user the controller's access check is a no-op,\n // and this is a server-side lookup on the target's behalf, not the sender's.\n let session = await controller.getSessionByResource(ownerResourceId);\n if (!session) {\n if (onlyIfLive) return { delivered: 'skipped', reason: 'offline' };\n const row = await sessions.getBySessionId(target.sessionId);\n if (!row?.factoryProjectId) return { delivered: 'skipped', reason: 'session-not-found' };\n session = await createSessionAsRowOwner(controller, {\n sessionId: target.sessionId,\n ownerResourceId,\n row,\n tags: {\n factoryProjectId: row.factoryProjectId,\n ...(row.projectRepositoryId ? { projectRepositoryId: row.projectRepositoryId } : {}),\n },\n threadId: target.threadId,\n });\n }\n\n // Read before sending: the send itself starts a run on an idle target. Delivery is per thread,\n // so a run on another thread of the same session leaves the target thread idle.\n const busy = session.stream.isActive() && session.thread.getId() === target.threadId;\n const result = await controller\n .getCurrentAgent(session)\n .sendNotificationSignal(\n { ...notification, source: FACTORY_SESSION_NOTIFICATION_SOURCE },\n { resourceId: ownerResourceId, threadId: target.threadId },\n );\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n\n const notificationId = result.record.id;\n const reason = result.decision.reason ? { reason: result.decision.reason } : {};\n switch (result.decision.action) {\n case 'deliver':\n return { delivered: busy ? 'mid-run' : 'woken', notificationId, ...reason };\n case 'summarize':\n return { delivered: 'summarized', notificationId, ...reason };\n case 'discard':\n return { delivered: 'skipped', notificationId, reason: 'discarded' };\n default:\n return { delivered: 'persisted', notificationId, ...reason };\n }\n}\n\nexport interface FactorySessionActivity {\n /** Whether the session's agent is mid-run in this process. */\n running: boolean;\n /** When the session's thread last changed (a message saved, a run step persisted), ISO. */\n lastActivityAt: string | null;\n}\n\n/** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */\nexport async function sessionActivity(\n controller: Pick<FactoryDeliveryController, 'getSessionByResource' | 'queryThreadById'>,\n sessionId: string,\n threadId: string,\n): Promise<FactorySessionActivity> {\n const [live, thread] = await Promise.all([\n controller.getSessionByResource(sessionId),\n controller.queryThreadById({ threadId }),\n ]);\n const updatedAt = thread?.updatedAt ? new Date(thread.updatedAt) : null;\n return {\n running: live?.stream.isActive() ?? false,\n lastActivityAt: updatedAt && Number.isFinite(updatedAt.getTime()) ? updatedAt.toISOString() : null,\n };\n}\n\n/**\n * The thread a message to a session should land on: the thread its role runs\n * on, else the thread it has open, else its most recently updated thread.\n */\nexport async function resolveSessionThread({\n controller,\n sessions,\n workItems,\n sessionId,\n}: {\n controller: Pick<FactoryDeliveryController, 'getSessionByResource' | 'queryThreads'>;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession'>;\n sessionId: string;\n}): Promise<string | null> {\n const row = await sessions.getBySessionId(sessionId);\n if (row?.factoryProjectId) {\n const binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId,\n });\n if (binding) return binding.threadId;\n }\n const live = await controller.getSessionByResource(sessionId);\n const current = live?.thread.getId();\n if (current) return current;\n const threads = await controller.queryThreads({ resourceId: sessionId });\n const newest = [...threads].sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())[0];\n return newest?.id ?? null;\n}\n"],"mappings":";;;AAQA,MAAa,sCAAsC;;;;;;;;;;;;;;AA8CnD,eAAsB,kCAAkC,EACtD,YACA,UACA,QACA,cACA,aAAa,SAQoB;CACjC,MAAM,SAAS,MAAM,WAAW,gBAAgB,EAAE,UAAU,OAAO,SAAS,CAAC;CAC7E,IAAI,CAAC,QAAQ,OAAO;EAAE,WAAW;EAAW,QAAQ;CAAmB;CAGvE,IAAI,OAAO,cAAc,OAAO,eAAe,OAAO,WACpD,OAAO;EAAE,WAAW;EAAW,QAAQ;CAAmB;CAE5D,MAAM,kBAAkB,OAAO;CAI/B,IAAI,UAAU,MAAM,WAAW,qBAAqB,eAAe;CACnE,IAAI,CAAC,SAAS;EACZ,IAAI,YAAY,OAAO;GAAE,WAAW;GAAW,QAAQ;EAAU;EACjE,MAAM,MAAM,MAAM,SAAS,eAAe,OAAO,SAAS;EAC1D,IAAI,CAAC,KAAK,kBAAkB,OAAO;GAAE,WAAW;GAAW,QAAQ;EAAoB;EACvF,UAAU,MAAM,wBAAwB,YAAY;GAClD,WAAW,OAAO;GAClB;GACA;GACA,MAAM;IACJ,kBAAkB,IAAI;IACtB,GAAI,IAAI,sBAAsB,EAAE,qBAAqB,IAAI,oBAAoB,IAAI,CAAC;GACpF;GACA,UAAU,OAAO;EACnB,CAAC;CACH;CAIA,MAAM,OAAO,QAAQ,OAAO,SAAS,KAAK,QAAQ,OAAO,MAAM,MAAM,OAAO;CAC5E,MAAM,SAAS,MAAM,WAClB,gBAAgB,OAAO,CAAC,CACxB,uBACC;EAAE,GAAG;EAAc,QAAQ;CAAoC,GAC/D;EAAE,YAAY;EAAiB,UAAU,OAAO;CAAS,CAC3D;CACF,MAAM,QAAQ,IAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,CAAC,CAAC,OAAO,OAAO,CAAC;CAErE,MAAM,iBAAiB,OAAO,OAAO;CACrC,MAAM,SAAS,OAAO,SAAS,SAAS,EAAE,QAAQ,OAAO,SAAS,OAAO,IAAI,CAAC;CAC9E,QAAQ,OAAO,SAAS,QAAxB;EACE,KAAK,WACH,OAAO;GAAE,WAAW,OAAO,YAAY;GAAS;GAAgB,GAAG;EAAO;EAC5E,KAAK,aACH,OAAO;GAAE,WAAW;GAAc;GAAgB,GAAG;EAAO;EAC9D,KAAK,WACH,OAAO;GAAE,WAAW;GAAW;GAAgB,QAAQ;EAAY;EACrE,SACE,OAAO;GAAE,WAAW;GAAa;GAAgB,GAAG;EAAO;CAC/D;AACF;;AAUA,eAAsB,gBACpB,YACA,WACA,UACiC;CACjC,MAAM,CAAC,MAAM,UAAU,MAAM,QAAQ,IAAI,CACvC,WAAW,qBAAqB,SAAS,GACzC,WAAW,gBAAgB,EAAE,SAAS,CAAC,CACzC,CAAC;CACD,MAAM,YAAY,QAAQ,YAAY,IAAI,KAAK,OAAO,SAAS,IAAI;CACnE,OAAO;EACL,SAAS,MAAM,OAAO,SAAS,KAAK;EACpC,gBAAgB,aAAa,OAAO,SAAS,UAAU,QAAQ,CAAC,IAAI,UAAU,YAAY,IAAI;CAChG;AACF;;;;;AAMA,eAAsB,qBAAqB,EACzC,YACA,UACA,WACA,aAMyB;CACzB,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,KAAK,kBAAkB;EACzB,MAAM,UAAU,MAAM,UAAU,+BAA+B;GAC7D,OAAO,IAAI;GACX,kBAAkB,IAAI;GACtB;EACF,CAAC;EACD,IAAI,SAAS,OAAO,QAAQ;CAC9B;CAEA,MAAM,WAAU,MADG,WAAW,qBAAqB,SAAS,EAAA,EACtC,OAAO,MAAM;CACnC,IAAI,SAAS,OAAO;CAGpB,OADe,CAAC,GAAG,MADG,WAAW,aAAa,EAAE,YAAY,UAAU,CAAC,CAC7C,CAAC,CAAC,MAAM,MAAM,UAAU,MAAM,UAAU,QAAQ,IAAI,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,EAC3F,EAAE,MAAM;AACvB"}
|
|
1
|
+
{"version":3,"file":"deliver.js","names":[],"sources":["../../src/tools/deliver.ts"],"sourcesContent":["import type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { SendNotificationSignalInput } from '@mastra/core/notifications';\n\nimport { createSessionAsRowOwner } from '../integrations/subscription-session.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport { listFactorySessionThreads, sessionMemoryResourceId, sessionStampCounts } from '../session/session-threads.js';\nimport type { FactorySessionOwnership, SessionRunBindings } from '../session/session-threads.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** Notifications between Factory sessions all carry this source. */\nexport const FACTORY_SESSION_NOTIFICATION_SOURCE = 'factory-session';\n\nexport type FactoryDeliveryController = Pick<\n MountedMastraCode['controller'],\n | 'queryThreadById'\n | 'queryThreads'\n | 'setThreadModel'\n | 'listAvailableModels'\n | 'getSession'\n | 'createSession'\n | 'getCurrentAgent'\n>;\n\nexport interface FactoryDeliveryTarget {\n /** The target's source-control session id. */\n sessionId: string;\n /** The stored thread the notification belongs to. Never the session's current thread by default. */\n threadId: string;\n}\n\nexport type FactoryDeliveryResult =\n /**\n * `woken`: the target was idle and the notification started a run.\n * `mid-run`: the target was already running; it reads the notification when that run\n * next checks its inbox, not now.\n */\n | { delivered: 'woken' | 'mid-run' | 'persisted' | 'summarized'; notificationId: string; reason?: string }\n | {\n delivered: 'skipped';\n notificationId?: string;\n reason: 'thread-not-found' | 'thread-not-owned' | 'session-not-found' | 'offline' | 'discarded';\n };\n\n/**\n * Deliver a notification to a Factory session's stored thread.\n *\n * Sent through the target's agent with an explicit `{ resourceId, threadId }`,\n * never `Session.sendNotificationSignal`: that one builds the wake from the\n * sender's request context and aims at the target's current thread. On this\n * path the SDK builds the wake from the session that owns the target resource,\n * so the woken run is the target row's owner, with that owner's credentials.\n *\n * A target that isn't live is created first, as its row's owner and bound to\n * the stored thread: waking a resource no session owns starts a run with no\n * model. A live session's thread is never switched.\n */\nexport async function deliverFactorySessionNotification({\n controller,\n sessions,\n target,\n notification,\n onlyIfLive = false,\n runs,\n}: {\n controller: FactoryDeliveryController;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n /** Run bindings, which decide whether a session may use its Factory's resource. */\n runs: SessionRunBindings | undefined;\n target: FactoryDeliveryTarget;\n notification: Omit<SendNotificationSignalInput, 'source'>;\n /** Skip, without creating anything, when no live session owns the target (stage notices). */\n onlyIfLive?: boolean;\n}): Promise<FactoryDeliveryResult> {\n const thread = await controller.queryThreadById({ threadId: target.threadId });\n if (!thread) return { delivered: 'skipped', reason: 'thread-not-found' };\n // Sessions share their Factory's or owner's resource, so a thread is this\n // session's when it is the session's own thread or carries its session stamp\n // (or, before migration, sits under the session id). Anything else is not\n // this session's, whatever the caller paired it with.\n let targetRow: ReturnType<SourceControlSessionLookup['getBySessionId']> | undefined;\n const loadRow = () => (targetRow ??= sessions.getBySessionId(target.sessionId));\n if (!(await isSessionThread(controller, runs, thread, target.sessionId, loadRow))) {\n return { delivered: 'skipped', reason: 'thread-not-owned' };\n }\n const ownerResourceId = thread.resourceId || target.sessionId;\n\n // No request context: without a user the controller's access check is a no-op,\n // and this is a server-side lookup on the target's behalf, not the sender's.\n let session = await controller.getSession(target.sessionId);\n if (!session) {\n if (onlyIfLive) return { delivered: 'skipped', reason: 'offline' };\n const row = await loadRow();\n if (!row?.factoryProjectId) return { delivered: 'skipped', reason: 'session-not-found' };\n session = await createSessionAsRowOwner(controller, {\n sessionId: target.sessionId,\n ownerResourceId,\n row,\n tags: {\n factoryProjectId: row.factoryProjectId,\n ...(row.projectRepositoryId ? { projectRepositoryId: row.projectRepositoryId } : {}),\n },\n threadId: target.threadId,\n });\n }\n\n // Read before sending: the send itself starts a run on an idle target. Delivery is per thread,\n // so a run on another thread of the same session leaves the target thread idle.\n const busy = session.stream.isActive() && session.thread.getId() === target.threadId;\n const result = await controller\n .getCurrentAgent(session)\n .sendNotificationSignal(\n { ...notification, source: FACTORY_SESSION_NOTIFICATION_SOURCE },\n { resourceId: ownerResourceId, threadId: target.threadId },\n );\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n\n const notificationId = result.record.id;\n const reason = result.decision.reason ? { reason: result.decision.reason } : {};\n switch (result.decision.action) {\n case 'deliver':\n return { delivered: busy ? 'mid-run' : 'woken', notificationId, ...reason };\n case 'summarize':\n return { delivered: 'summarized', notificationId, ...reason };\n case 'discard':\n return { delivered: 'skipped', notificationId, reason: 'discarded' };\n default:\n return { delivered: 'persisted', notificationId, ...reason };\n }\n}\n\nexport interface FactorySessionActivity {\n /** Whether the session's agent is mid-run in this process. */\n running: boolean;\n /** When the session's thread last changed (a message saved, a run step persisted), ISO. */\n lastActivityAt: string | null;\n}\n\n/** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */\nexport async function sessionActivity(\n controller: Pick<FactoryDeliveryController, 'getSession' | 'queryThreadById'>,\n sessionId: string,\n threadId: string,\n): Promise<FactorySessionActivity> {\n const [live, thread] = await Promise.all([\n controller.getSession(sessionId),\n controller.queryThreadById({ threadId }),\n ]);\n const updatedAt = thread?.updatedAt ? new Date(thread.updatedAt) : null;\n return {\n running: live?.stream.isActive() ?? false,\n lastActivityAt: updatedAt && Number.isFinite(updatedAt.getTime()) ? updatedAt.toISOString() : null,\n };\n}\n\n/**\n * The thread a message to a session should land on: the thread its role runs\n * on, else the thread it has open, else its most recently updated thread.\n */\nexport async function resolveSessionThread({\n controller,\n sessions,\n workItems,\n sessionId,\n}: {\n controller: Pick<FactoryDeliveryController, 'getSession' | 'queryThreads' | 'queryThreadById'>;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'hasRunBindingForSession'>;\n sessionId: string;\n}): Promise<string | null> {\n const row = await sessions.getBySessionId(sessionId);\n if (row?.factoryProjectId) {\n const binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId,\n });\n if (binding) return binding.threadId;\n }\n const live = await controller.getSession(sessionId);\n const current = live?.thread.getId();\n if (current) return current;\n // Without a row no resource is the session's to claim stamped threads on.\n const threads = await listFactorySessionThreads(controller, row ?? { sessionId, userId: '' }, workItems);\n const newest = [...threads].sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())[0];\n return newest?.id ?? null;\n}\n\nasync function isSessionThread(\n controller: Pick<FactoryDeliveryController, 'queryThreadById'>,\n runs: SessionRunBindings | undefined,\n thread: { id: string; resourceId?: string | null; metadata?: Record<string, unknown> | null },\n sessionId: string,\n loadRow: () => Promise<FactorySessionOwnership | null>,\n): Promise<boolean> {\n // Before resources were shared a session's threads ran under its own id.\n if (thread.resourceId === sessionId) return true;\n if (thread.id !== sessionId && thread.metadata?.factorySessionId !== sessionId) return false;\n // Thread ids and stamps are client-writable: either only counts under the\n // session's own memory resource.\n const row = await loadRow();\n if (!row) return false;\n if (thread.id !== sessionId && !sessionStampCounts(row, thread.resourceId)) return false;\n return thread.resourceId === (await sessionMemoryResourceId(controller, row, runs));\n}\n"],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ export interface FactoryMessaging {
|
|
|
19
19
|
}
|
|
20
20
|
export interface FactoryMessagingToolDependencies {
|
|
21
21
|
sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin' | 'setOriginMuted'>;
|
|
22
|
-
workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'getForProject' | 'listSessionSubscriptions' | 'listRunBindings' | 'listWorkItemSubscribers'>;
|
|
22
|
+
workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'getForProject' | 'hasRunBindingForSession' | 'listSessionSubscriptions' | 'listRunBindings' | 'listWorkItemSubscribers'>;
|
|
23
23
|
auditRecorder: AuditRecorder;
|
|
24
24
|
/** The live controller and notifications store; resolved per call because they exist only once the mount is up. */
|
|
25
25
|
messaging: () => Promise<FactoryMessaging | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messaging-tools.d.ts","sourceRoot":"","sources":["../../src/tools/messaging-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"messaging-tools.d.ts","sourceRoot":"","sources":["../../src/tools/messaging-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAGV,oBAAoB,EAErB,MAAM,4BAA4B,CAAC;AAIpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAM9E,OAAO,KAAK,EAAE,yBAAyB,EAAyB,MAAM,cAAc,CAAC;AAIrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGnD,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAyCpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,EAAE,IAAI,CAAC,oBAAoB,EAAE,mBAAmB,GAAG,iBAAiB,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,cAAc,GAAG,gBAAgB,CAAC,CAAC;IACjG,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,gCAAgC,GAChC,eAAe,GACf,yBAAyB,GACzB,0BAA0B,GAC1B,iBAAiB,GACjB,yBAAyB,CAC5B,CAAC;IACF,aAAa,EAAE,aAAa,CAAC;IAC7B,mHAAmH;IACnH,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACnD;AAgBD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,gCAAgC,GACrC,gBAAgB,CAohBlB"}
|