@mastra/factory 0.6.0-alpha.14 → 0.6.0-alpha.16
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/CHANGELOG.md +43 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +4 -0
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +11 -9
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +8 -1
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +6 -1
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/rules/start-coordinator.js +1 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/sandbox/fleet.d.ts.map +1 -1
- package/dist/sandbox/fleet.js +3 -2
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +29 -21
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +1 -1
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +1 -1
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +31 -0
- package/dist/session/first-exec-capture.d.ts.map +1 -0
- package/dist/session/first-exec-capture.js +31 -0
- package/dist/session/first-exec-capture.js.map +1 -0
- package/dist/session/first-message-capture.d.ts +29 -0
- package/dist/session/first-message-capture.d.ts.map +1 -0
- package/dist/session/first-message-capture.js +29 -0
- package/dist/session/first-message-capture.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +23 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +29 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +6 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +16 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"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"}
|
package/dist/sandbox/fleet.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timedPhase } from "../timing.js";
|
|
1
2
|
import path from "path";
|
|
2
3
|
//#region src/sandbox/fleet.ts
|
|
3
4
|
/**
|
|
@@ -259,7 +260,7 @@ var SandboxFleet = class {
|
|
|
259
260
|
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
|
|
260
261
|
});
|
|
261
262
|
try {
|
|
262
|
-
await reattached.start();
|
|
263
|
+
await timedPhase("sandbox.reattach", () => reattached.start());
|
|
263
264
|
return reattached;
|
|
264
265
|
} catch {
|
|
265
266
|
await store.setSandboxId(null);
|
|
@@ -277,7 +278,7 @@ var SandboxFleet = class {
|
|
|
277
278
|
...env ? { env } : {},
|
|
278
279
|
...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
|
|
279
280
|
});
|
|
280
|
-
await sandbox.start();
|
|
281
|
+
await timedPhase("sandbox.provision", () => sandbox.start());
|
|
281
282
|
this.#liveCount += 1;
|
|
282
283
|
const providerSandboxId = await readProviderSandboxId(sandbox);
|
|
283
284
|
if (providerSandboxId) await store.setSandboxId(providerSandboxId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet.js","names":["#config","#inflight","#liveCount","#factory","#ensureSandboxUncoalesced","#build"],"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /**\n * Environment variables for commands run in the sandbox. Adapter-level\n * only: merged into every `executeCommand`, never baked into the provider\n * VM (see `SandboxFleet.#build`).\n */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Stable identity for one binding's in-flight provision work, used to coalesce\n * concurrent `ensureSandbox` calls. Prefer `checkpointName` — it is a pure\n * function of the owning session and is set before the first provision, which\n * is exactly when the herd forms (the stored `sandboxId` is still null then).\n * Fall back to the stored provider id, and skip coalescing entirely for\n * bindings with neither: keying those on a shared constant would wrongly\n * funnel *different* bindings onto one sandbox.\n */\nfunction coalesceKey(store: SandboxBindingStore): string | undefined {\n if (store.checkpointName) return `checkpoint:${store.checkpointName}`;\n if (store.sandboxId) return `sandbox:${store.sandboxId}`;\n return undefined;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n /** In-flight `ensureSandbox` work, keyed per binding so concurrent callers coalesce. */\n readonly #inflight = new Map<string, Promise<MaterializationSandbox>>();\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n *\n * `env` is deliberately NOT forwarded to the provider clone: remote\n * providers bake creation-time env into the VM for its whole lifetime\n * (`POST /sandbox`), which would persist credentials like `GH_TOKEN` inside\n * a VM that can outlive the session and be reused by another user via the\n * sandbox pool. Instead the env lives only on the adapter, which merges it\n * into every `executeCommand` — commands see the (refreshable) token, but\n * the VM itself never stores it.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n *\n * Concurrent calls for the same binding coalesce onto one in-flight\n * provision/reattach and share its sandbox handle — N simultaneous requests\n * for one cold session (e.g. several browser tabs polling right after boot)\n * must not each fire their own `POST /sandbox` against the provider.\n * Failures are not cached: once the shared attempt settles, the next call\n * starts fresh.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n\n const key = coalesceKey(store);\n if (!key) return this.#ensureSandboxUncoalesced(store, env, onProgress, options);\n\n const existing = this.#inflight.get(key);\n if (existing) return existing;\n\n const promise = this.#ensureSandboxUncoalesced(store, env, onProgress, options).finally(() => {\n // Only clear when this is still the entry we own.\n if (this.#inflight.get(key) === promise) this.#inflight.delete(key);\n });\n this.#inflight.set(key, promise);\n return promise;\n }\n\n /** The single provision/reattach attempt behind {@link ensureSandbox}. */\n async #ensureSandboxUncoalesced(\n store: SandboxBindingStore,\n env: Record<string, string> | undefined,\n onProgress: ProgressFn | undefined,\n options: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox> {\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n try {\n await reattached.start();\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fleet.js","names":["#config","#inflight","#liveCount","#factory","#ensureSandboxUncoalesced","#build"],"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport { timedPhase } from '../timing.js';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /**\n * Environment variables for commands run in the sandbox. Adapter-level\n * only: merged into every `executeCommand`, never baked into the provider\n * VM (see `SandboxFleet.#build`).\n */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Stable identity for one binding's in-flight provision work, used to coalesce\n * concurrent `ensureSandbox` calls. Prefer `checkpointName` — it is a pure\n * function of the owning session and is set before the first provision, which\n * is exactly when the herd forms (the stored `sandboxId` is still null then).\n * Fall back to the stored provider id, and skip coalescing entirely for\n * bindings with neither: keying those on a shared constant would wrongly\n * funnel *different* bindings onto one sandbox.\n */\nfunction coalesceKey(store: SandboxBindingStore): string | undefined {\n if (store.checkpointName) return `checkpoint:${store.checkpointName}`;\n if (store.sandboxId) return `sandbox:${store.sandboxId}`;\n return undefined;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n /** In-flight `ensureSandbox` work, keyed per binding so concurrent callers coalesce. */\n readonly #inflight = new Map<string, Promise<MaterializationSandbox>>();\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n *\n * `env` is deliberately NOT forwarded to the provider clone: remote\n * providers bake creation-time env into the VM for its whole lifetime\n * (`POST /sandbox`), which would persist credentials like `GH_TOKEN` inside\n * a VM that can outlive the session and be reused by another user via the\n * sandbox pool. Instead the env lives only on the adapter, which merges it\n * into every `executeCommand` — commands see the (refreshable) token, but\n * the VM itself never stores it.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n *\n * Concurrent calls for the same binding coalesce onto one in-flight\n * provision/reattach and share its sandbox handle — N simultaneous requests\n * for one cold session (e.g. several browser tabs polling right after boot)\n * must not each fire their own `POST /sandbox` against the provider.\n * Failures are not cached: once the shared attempt settles, the next call\n * starts fresh.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n\n const key = coalesceKey(store);\n if (!key) return this.#ensureSandboxUncoalesced(store, env, onProgress, options);\n\n const existing = this.#inflight.get(key);\n if (existing) return existing;\n\n const promise = this.#ensureSandboxUncoalesced(store, env, onProgress, options).finally(() => {\n // Only clear when this is still the entry we own.\n if (this.#inflight.get(key) === promise) this.#inflight.delete(key);\n });\n this.#inflight.set(key, promise);\n return promise;\n }\n\n /** The single provision/reattach attempt behind {@link ensureSandbox}. */\n async #ensureSandboxUncoalesced(\n store: SandboxBindingStore,\n env: Record<string, string> | undefined,\n onProgress: ProgressFn | undefined,\n options: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox> {\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n try {\n await timedPhase('sandbox.reattach', () => reattached.start());\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await timedPhase('sandbox.provision', () => sandbox.start());\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,KAAK,EAAwB,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;;OAKG;IACH,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GACrC,CAAC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,+BAA+B,CAAC,GACnD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,6BAA6B,CAAC,
|
|
1
|
+
{"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,KAAK,EAAwB,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;;OAKG;IACH,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GACrC,CAAC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,+BAA+B,CAAC,GACnD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAuCzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,+BAA+B,CAAC,IAAI,EAAE;IAC1D,aAAa,EAAE,0BAA0B,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAc9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,2BAA2B,CAAC,CA4BtC;AAcD,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnH"}
|
|
@@ -21,35 +21,43 @@ async function resolveFactoryDefaultModelId(projects, factoryProjectId) {
|
|
|
21
21
|
*/
|
|
22
22
|
async function resolveFactorySourceRepository(args) {
|
|
23
23
|
const { sourceControl, orgId, factoryProjectId, repositorySlug } = args;
|
|
24
|
-
const
|
|
24
|
+
const candidates = (await sourceControl.connections.list({
|
|
25
25
|
orgId,
|
|
26
26
|
factoryProjectId
|
|
27
|
-
})).
|
|
28
|
-
if (
|
|
27
|
+
})).filter((candidate) => candidate.integrationId === sourceControl.integrationId);
|
|
28
|
+
if (candidates.length === 0) return {
|
|
29
29
|
found: false,
|
|
30
30
|
reason: "connection"
|
|
31
31
|
};
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
for (const connection of candidates) {
|
|
33
|
+
let resolved;
|
|
34
|
+
try {
|
|
35
|
+
const projectRepositories = await sourceControl.projectRepositories.list({
|
|
36
|
+
orgId,
|
|
37
|
+
connectionId: connection.id
|
|
38
|
+
});
|
|
39
|
+
resolved = (await Promise.all(projectRepositories.map(async (projectRepository) => ({
|
|
40
|
+
projectRepository,
|
|
41
|
+
repository: await sourceControl.repositories.get({
|
|
42
|
+
orgId,
|
|
43
|
+
id: projectRepository.repositoryId
|
|
44
|
+
})
|
|
45
|
+
})))).find((candidate) => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug));
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!resolved?.repository) continue;
|
|
50
|
+
return {
|
|
51
|
+
found: true,
|
|
52
|
+
projectRepositoryId: resolved.projectRepository.id,
|
|
53
|
+
baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,
|
|
54
|
+
connectedByUserId: connection.createdByUserId
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
44
58
|
found: false,
|
|
45
59
|
reason: "repository"
|
|
46
60
|
};
|
|
47
|
-
return {
|
|
48
|
-
found: true,
|
|
49
|
-
projectRepositoryId: resolved.projectRepository.id,
|
|
50
|
-
baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,
|
|
51
|
-
connectedByUserId: connection.createdByUserId
|
|
52
|
-
};
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* Walk a Factory user-session id back to the project it belongs to.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory-session.js","names":[],"sources":["../../src/session/factory-session.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\n\nimport type { MemorySettingsRecord, MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\ntype FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;\n\n/**\n * Read the factory project's default model. Best-effort: a missing project or an\n * uninitialized storage domain means \"no default\", never a failed run.\n */\nexport async function resolveFactoryDefaultModelId(\n projects: FactoryProjectsStorage | undefined,\n factoryProjectId: string | undefined,\n): Promise<string | undefined> {\n if (!projects || !factoryProjectId) return undefined;\n try {\n const project = await projects.getById({ id: factoryProjectId });\n return project?.defaultModelId ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface EnsureFactorySourceSessionArgs {\n /**\n * Storage handle of the integration that owns source control. Nothing here is\n * provider-specific: the connection is matched by the handle's own\n * `integrationId`, so GitHub, Slack-on-behalf-of-GitHub, or any future owner\n * all resolve through the same traversal.\n */\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n branch: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n}\n\nexport interface EnsuredFactorySourceSession {\n sessionId: string;\n userId: string;\n projectRepositoryId: string;\n branch: string;\n baseBranch: string;\n}\n\nexport interface ResolvedFactorySourceRepository {\n projectRepositoryId: string;\n /** The repository's pinned branch, else its default branch. */\n baseBranch: string;\n /** Who connected the repository. The attribution for runs with no interactive user. */\n connectedByUserId: string;\n}\n\n/**\n * Outcome of {@link resolveFactorySourceRepository}. A miss carries which step\n * failed: callers differ on whether that is an error (an autonomous run cannot\n * proceed) or a routine fallback (a chat integration drops to a chat-only\n * session), and the two steps fail for different reasons worth reporting apart.\n */\nexport type FactorySourceRepositoryResult =\n | ({ found: true } & ResolvedFactorySourceRepository)\n | { found: false; reason: 'connection' | 'repository' };\n\n/**\n * Resolve which repository a factory project's source-control runs act on: the\n * owner's connection on the project, then one of its linked repositories.\n *\n * The owner is whichever integration owns source control, matched by the\n * handle's own `integrationId` — nothing here is provider-specific.\n */\nexport async function resolveFactorySourceRepository(args: {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n}): Promise<FactorySourceRepositoryResult> {\n const { sourceControl, orgId, factoryProjectId, repositorySlug } = args;\n\n const connections = await sourceControl.connections.list({ orgId, factoryProjectId });\n const connection = connections.find(candidate => candidate.integrationId === sourceControl.integrationId);\n if (!connection) return { found: false, reason: 'connection' };\n\n const projectRepositories = await sourceControl.projectRepositories.list({ orgId, connectionId: connection.id });\n const resolvedRepositories = await Promise.all(\n projectRepositories.map(async projectRepository => ({\n projectRepository,\n repository: await sourceControl.repositories.get({ orgId, id: projectRepository.repositoryId }),\n })),\n );\n const resolved = resolvedRepositories.find(\n candidate => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug),\n );\n if (!resolved?.repository) return { found: false, reason: 'repository' };\n\n return {\n found: true,\n projectRepositoryId: resolved.projectRepository.id,\n baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,\n connectedByUserId: connection.createdByUserId,\n };\n}\n\n/**\n * Walk a Factory user-session id back to the project it belongs to.\n *\n * Repo-backed channel threads are keyed by their Factory session id, which is\n * the only handle a session-start hook gets. This turns that id back into the\n * project whose configuration the session should adopt. Durable by\n * construction — it reads the same rows the session was created from, so it\n * survives restarts without any in-memory mapping.\n */\nexport async function resolveFactoryProjectForSession(args: {\n sourceControl: SourceControlStorageHandle;\n sessionId: string;\n}): Promise<{ factoryProjectId: string; orgId: string; userId: string } | null> {\n const { sourceControl, sessionId } = args;\n\n const session = await sourceControl.sessions.getBySessionId(sessionId);\n if (!session) return null;\n const projectRepository = await sourceControl.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) return null;\n const connection = await sourceControl.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n if (!connection) return null;\n\n return { factoryProjectId: connection.factoryProjectId, orgId: session.orgId, userId: session.userId };\n}\n\n/**\n * Create the source-control session a repo-backed factory run needs.\n *\n * `FactoryStartCoordinator.prepare` requires this record to already exist —\n * `resolveSourceSession` throws `Factory session not found` otherwise — so every\n * autonomous entry point has to produce one before it can start a run. This is\n * that step, in one place: the owner's connection on the factory project, one of\n * its linked repositories, and a session on the requested branch with the\n * repository's pinned or default branch as the base.\n *\n * The run is attributed to whoever connected the repository\n * (`connection.createdByUserId`), because an autonomous run has no interactive\n * user of its own.\n */\nexport async function ensureFactorySourceSession(\n args: EnsureFactorySourceSessionArgs,\n): Promise<EnsuredFactorySourceSession> {\n const { sourceControl, orgId, factoryProjectId, branch, repositorySlug } = args;\n\n const resolved = await resolveFactorySourceRepository({ sourceControl, orgId, factoryProjectId, repositorySlug });\n if (!resolved.found) {\n throw new Error(\n resolved.reason === 'connection'\n ? 'Factory source-control connection not found.'\n : 'Factory source-control repository not found.',\n );\n }\n\n const userId = resolved.connectedByUserId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: resolved.projectRepositoryId,\n orgId,\n userId,\n branch,\n baseBranch: resolved.baseBranch,\n });\n return {\n sessionId: session.sessionId,\n userId,\n projectRepositoryId: resolved.projectRepositoryId,\n branch: session.branch,\n baseBranch: resolved.baseBranch,\n };\n}\n\nasync function applyMemorySettings(session: FactorySession, record: MemorySettingsRecord | null): Promise<void> {\n if (record?.observerModelId) await session.om.observer.switchModel({ modelId: record.observerModelId });\n if (record?.reflectorModelId) await session.om.reflector.switchModel({ modelId: record.reflectorModelId });\n\n const state = {\n ...(record?.observationThreshold != null ? { observationThreshold: record.observationThreshold } : {}),\n ...(record?.reflectionThreshold != null ? { reflectionThreshold: record.reflectionThreshold } : {}),\n ...(record?.observeAttachments != null ? { observeAttachments: record.observeAttachments } : {}),\n };\n if (Object.keys(state).length > 0) await session.state.set(state);\n}\n\nexport interface HydrateFactorySessionArgs {\n orgId: string;\n userId: string;\n /** The factory project's default model. Without it the session keeps the SDK's built-in mode default. */\n defaultModelId?: string;\n /** Omitted when the storage domain is unavailable, in which case the session runs on memory defaults. */\n memorySettings?: MemorySettingsStorage;\n}\n\n/**\n * Apply a factory project's configuration to a freshly created session:\n * observational-memory settings, then the project's default model.\n *\n * Both steps are best-effort. A retired model id or an unreachable settings row\n * must not sink a run that is otherwise ready — the session simply keeps the\n * default it was created with, and the reason is logged.\n */\nexport async function hydrateFactorySession(session: FactorySession, args: HydrateFactorySessionArgs): Promise<void> {\n if (args.memorySettings) {\n try {\n const record = await args.memorySettings.get({ orgId: args.orgId, userId: args.userId });\n await applyMemorySettings(session, record);\n } catch (error) {\n console.warn('[Factory Start] Failed to apply observational-memory settings', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n if (args.defaultModelId) {\n try {\n await session.model.switch({ modelId: args.defaultModelId });\n } catch (error) {\n console.warn('[Factory Start] Failed to apply factory default model', {\n modelId: args.defaultModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"factory-session.js","names":[],"sources":["../../src/session/factory-session.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\n\nimport type { MemorySettingsRecord, MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\ntype FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;\n\n/**\n * Read the factory project's default model. Best-effort: a missing project or an\n * uninitialized storage domain means \"no default\", never a failed run.\n */\nexport async function resolveFactoryDefaultModelId(\n projects: FactoryProjectsStorage | undefined,\n factoryProjectId: string | undefined,\n): Promise<string | undefined> {\n if (!projects || !factoryProjectId) return undefined;\n try {\n const project = await projects.getById({ id: factoryProjectId });\n return project?.defaultModelId ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface EnsureFactorySourceSessionArgs {\n /**\n * Storage handle of the integration that owns source control. Nothing here is\n * provider-specific: the connection is matched by the handle's own\n * `integrationId`, so GitHub, Slack-on-behalf-of-GitHub, or any future owner\n * all resolve through the same traversal.\n */\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n branch: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n}\n\nexport interface EnsuredFactorySourceSession {\n sessionId: string;\n userId: string;\n projectRepositoryId: string;\n branch: string;\n baseBranch: string;\n}\n\nexport interface ResolvedFactorySourceRepository {\n projectRepositoryId: string;\n /** The repository's pinned branch, else its default branch. */\n baseBranch: string;\n /** Who connected the repository. The attribution for runs with no interactive user. */\n connectedByUserId: string;\n}\n\n/**\n * Outcome of {@link resolveFactorySourceRepository}. A miss carries which step\n * failed: callers differ on whether that is an error (an autonomous run cannot\n * proceed) or a routine fallback (a chat integration drops to a chat-only\n * session), and the two steps fail for different reasons worth reporting apart.\n */\nexport type FactorySourceRepositoryResult =\n | ({ found: true } & ResolvedFactorySourceRepository)\n | { found: false; reason: 'connection' | 'repository' };\n\n/**\n * Resolve which repository a factory project's source-control runs act on: the\n * owner's connection on the project, then one of its linked repositories.\n *\n * The owner is whichever integration owns source control, matched by the\n * handle's own `integrationId` — nothing here is provider-specific.\n */\nexport async function resolveFactorySourceRepository(args: {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n /** Pick a specific linked repository by slug. Defaults to the first linked repository. */\n repositorySlug?: string;\n}): Promise<FactorySourceRepositoryResult> {\n const { sourceControl, orgId, factoryProjectId, repositorySlug } = args;\n\n const connections = await sourceControl.connections.list({ orgId, factoryProjectId });\n const candidates = connections.filter(candidate => candidate.integrationId === sourceControl.integrationId);\n if (candidates.length === 0) return { found: false, reason: 'connection' };\n\n // A project can carry stale connections: a provider-app reinstall leaves the\n // old connection pointing at an installation that no longer exists, and that\n // row can sit ahead of the healthy one. Try every candidate and skip the ones\n // that no longer resolve rather than failing on the first.\n for (const connection of candidates) {\n let resolved;\n try {\n const projectRepositories = await sourceControl.projectRepositories.list({ orgId, connectionId: connection.id });\n const resolvedRepositories = await Promise.all(\n projectRepositories.map(async projectRepository => ({\n projectRepository,\n repository: await sourceControl.repositories.get({ orgId, id: projectRepository.repositoryId }),\n })),\n );\n resolved = resolvedRepositories.find(\n candidate => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug),\n );\n } catch {\n // The connection no longer resolves (e.g. its installation was deleted).\n continue;\n }\n if (!resolved?.repository) continue;\n\n return {\n found: true,\n projectRepositoryId: resolved.projectRepository.id,\n baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,\n connectedByUserId: connection.createdByUserId,\n };\n }\n\n return { found: false, reason: 'repository' };\n}\n\n/**\n * Walk a Factory user-session id back to the project it belongs to.\n *\n * Repo-backed channel threads are keyed by their Factory session id, which is\n * the only handle a session-start hook gets. This turns that id back into the\n * project whose configuration the session should adopt. Durable by\n * construction — it reads the same rows the session was created from, so it\n * survives restarts without any in-memory mapping.\n */\nexport async function resolveFactoryProjectForSession(args: {\n sourceControl: SourceControlStorageHandle;\n sessionId: string;\n}): Promise<{ factoryProjectId: string; orgId: string; userId: string } | null> {\n const { sourceControl, sessionId } = args;\n\n const session = await sourceControl.sessions.getBySessionId(sessionId);\n if (!session) return null;\n const projectRepository = await sourceControl.projectRepositories.get({\n orgId: session.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) return null;\n const connection = await sourceControl.connections.get({ orgId: session.orgId, id: projectRepository.connectionId });\n if (!connection) return null;\n\n return { factoryProjectId: connection.factoryProjectId, orgId: session.orgId, userId: session.userId };\n}\n\n/**\n * Create the source-control session a repo-backed factory run needs.\n *\n * `FactoryStartCoordinator.prepare` requires this record to already exist —\n * `resolveSourceSession` throws `Factory session not found` otherwise — so every\n * autonomous entry point has to produce one before it can start a run. This is\n * that step, in one place: the owner's connection on the factory project, one of\n * its linked repositories, and a session on the requested branch with the\n * repository's pinned or default branch as the base.\n *\n * The run is attributed to whoever connected the repository\n * (`connection.createdByUserId`), because an autonomous run has no interactive\n * user of its own.\n */\nexport async function ensureFactorySourceSession(\n args: EnsureFactorySourceSessionArgs,\n): Promise<EnsuredFactorySourceSession> {\n const { sourceControl, orgId, factoryProjectId, branch, repositorySlug } = args;\n\n const resolved = await resolveFactorySourceRepository({ sourceControl, orgId, factoryProjectId, repositorySlug });\n if (!resolved.found) {\n throw new Error(\n resolved.reason === 'connection'\n ? 'Factory source-control connection not found.'\n : 'Factory source-control repository not found.',\n );\n }\n\n const userId = resolved.connectedByUserId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: resolved.projectRepositoryId,\n orgId,\n userId,\n branch,\n baseBranch: resolved.baseBranch,\n });\n return {\n sessionId: session.sessionId,\n userId,\n projectRepositoryId: resolved.projectRepositoryId,\n branch: session.branch,\n baseBranch: resolved.baseBranch,\n };\n}\n\nasync function applyMemorySettings(session: FactorySession, record: MemorySettingsRecord | null): Promise<void> {\n if (record?.observerModelId) await session.om.observer.switchModel({ modelId: record.observerModelId });\n if (record?.reflectorModelId) await session.om.reflector.switchModel({ modelId: record.reflectorModelId });\n\n const state = {\n ...(record?.observationThreshold != null ? { observationThreshold: record.observationThreshold } : {}),\n ...(record?.reflectionThreshold != null ? { reflectionThreshold: record.reflectionThreshold } : {}),\n ...(record?.observeAttachments != null ? { observeAttachments: record.observeAttachments } : {}),\n };\n if (Object.keys(state).length > 0) await session.state.set(state);\n}\n\nexport interface HydrateFactorySessionArgs {\n orgId: string;\n userId: string;\n /** The factory project's default model. Without it the session keeps the SDK's built-in mode default. */\n defaultModelId?: string;\n /** Omitted when the storage domain is unavailable, in which case the session runs on memory defaults. */\n memorySettings?: MemorySettingsStorage;\n}\n\n/**\n * Apply a factory project's configuration to a freshly created session:\n * observational-memory settings, then the project's default model.\n *\n * Both steps are best-effort. A retired model id or an unreachable settings row\n * must not sink a run that is otherwise ready — the session simply keeps the\n * default it was created with, and the reason is logged.\n */\nexport async function hydrateFactorySession(session: FactorySession, args: HydrateFactorySessionArgs): Promise<void> {\n if (args.memorySettings) {\n try {\n const record = await args.memorySettings.get({ orgId: args.orgId, userId: args.userId });\n await applyMemorySettings(session, record);\n } catch (error) {\n console.warn('[Factory Start] Failed to apply observational-memory settings', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n if (args.defaultModelId) {\n try {\n await session.model.switch({ modelId: args.defaultModelId });\n } catch (error) {\n console.warn('[Factory Start] Failed to apply factory default model', {\n modelId: args.defaultModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ export interface FilesystemCaptureSession {
|
|
|
11
11
|
};
|
|
12
12
|
getWorkspace(): {
|
|
13
13
|
sandbox?: Pick<WorkspaceSandbox, 'executeCommand'>;
|
|
14
|
-
};
|
|
14
|
+
} | undefined;
|
|
15
15
|
onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;
|
|
16
16
|
}
|
|
17
17
|
export interface FilesystemCaptureDependencies {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAK5F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE;QAAE,SAAS,IAAI,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAK5F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE;QAAE,SAAS,IAAI,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IACnF,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;CACvE;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IACpD,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;CACH;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAmB5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,GAC3D,OAAO,CAAC,IAAI,CAAC,CA2Cf;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,YAAY,EAAE,6BAA6B,GAC1C,MAAM,IAAI,CAMZ"}
|
|
@@ -27,7 +27,7 @@ async function captureSessionFilesystem(session, { filesystem, sourceControl })
|
|
|
27
27
|
const resourceId = session.identity.getResourceId();
|
|
28
28
|
const threadId = session.thread.requireId();
|
|
29
29
|
const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);
|
|
30
|
-
const sandbox = session.getWorkspace()
|
|
30
|
+
const sandbox = session.getWorkspace()?.sandbox;
|
|
31
31
|
if (!sourceSession?.sandboxWorkdir || !sandbox?.executeCommand) return;
|
|
32
32
|
const result = await sandbox.executeCommand("git", [
|
|
33
33
|
"-C",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nconst GIT_STATUS_ARGS = ['status', '--porcelain=v1', '-z', '--untracked-files=all'];\nconst ARTIFACTS_LIST_COMMAND = 'cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true';\n\nexport interface FilesystemCaptureSession {\n readonly identity: { getResourceId(): string };\n readonly thread: { requireId(): string };\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'executeCommand'> };\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n}\n\nexport interface FilesystemCaptureDependencies {\n filesystem: Pick<FilesystemStorage, 'replaceFiles'>;\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\nexport function parseFilesystemCaptureFiles(output: string): FilesystemFile[] {\n const files = new Map<string, FilesystemFile>();\n const records = output.split('\\0');\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record || record.length < 4) continue;\n\n const code = record.slice(0, 2);\n let path = record.slice(3);\n const moved = code.includes('R') || code.includes('C');\n if (moved) index += 1;\n if (path.startsWith('./')) path = path.slice(2);\n if (!path || (!code.includes('U') && code.includes('D') && !moved)) continue;\n\n files.set(path, { path });\n }\n\n return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function captureSessionFilesystem(\n session: FilesystemCaptureSession,\n { filesystem, sourceControl }: FilesystemCaptureDependencies,\n): Promise<void> {\n try {\n const resourceId = session.identity.getResourceId();\n const threadId = session.thread.requireId();\n const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);\n const sandbox = session.getWorkspace()
|
|
1
|
+
{"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nconst GIT_STATUS_ARGS = ['status', '--porcelain=v1', '-z', '--untracked-files=all'];\nconst ARTIFACTS_LIST_COMMAND = 'cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true';\n\nexport interface FilesystemCaptureSession {\n readonly identity: { getResourceId(): string };\n readonly thread: { requireId(): string };\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'executeCommand'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n}\n\nexport interface FilesystemCaptureDependencies {\n filesystem: Pick<FilesystemStorage, 'replaceFiles'>;\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\nexport function parseFilesystemCaptureFiles(output: string): FilesystemFile[] {\n const files = new Map<string, FilesystemFile>();\n const records = output.split('\\0');\n\n for (let index = 0; index < records.length; index += 1) {\n const record = records[index];\n if (!record || record.length < 4) continue;\n\n const code = record.slice(0, 2);\n let path = record.slice(3);\n const moved = code.includes('R') || code.includes('C');\n if (moved) index += 1;\n if (path.startsWith('./')) path = path.slice(2);\n if (!path || (!code.includes('U') && code.includes('D') && !moved)) continue;\n\n files.set(path, { path });\n }\n\n return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function captureSessionFilesystem(\n session: FilesystemCaptureSession,\n { filesystem, sourceControl }: FilesystemCaptureDependencies,\n): Promise<void> {\n try {\n const resourceId = session.identity.getResourceId();\n const threadId = session.thread.requireId();\n const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);\n // Chat-only sessions run without a workspace; there is nothing to capture.\n const sandbox = session.getWorkspace()?.sandbox;\n if (!sourceSession?.sandboxWorkdir || !sandbox?.executeCommand) return;\n\n const result = await sandbox.executeCommand('git', ['-C', sourceSession.sandboxWorkdir, ...GIT_STATUS_ARGS], {\n timeout: 30_000,\n });\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git status.', result.stderr);\n return;\n }\n\n const artifacts = await sandbox.executeCommand(\n 'sh',\n ['-c', ARTIFACTS_LIST_COMMAND, 'sh', sourceSession.sandboxWorkdir],\n { timeout: 30_000 },\n );\n if (artifacts.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace artifacts.', artifacts.stderr);\n return;\n }\n\n const files = new Map(parseFilesystemCaptureFiles(result.stdout).map(file => [file.path, file]));\n for (const path of artifacts.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n\n await filesystem.replaceFiles({\n resourceId,\n threadId,\n files: [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path)),\n });\n } catch (error) {\n console.warn('[Factory filesystem capture] Unable to persist files.', error);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let capture = Promise.resolve();\n return session.onBeforeAgentEnd(() => {\n capture = capture.then(() => captureSessionFilesystem(session, dependencies));\n return capture;\n });\n}\n"],"mappings":";AAMA,MAAM,kBAAkB;CAAC;CAAU;CAAkB;CAAM;AAAuB;AAClF,MAAM,yBAAyB;AAgB/B,SAAgB,4BAA4B,QAAkC;CAC5E,MAAM,wBAAQ,IAAI,IAA4B;CAC9C,MAAM,UAAU,OAAO,MAAM,IAAI;CAEjC,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;EACtD,MAAM,SAAS,QAAQ;EACvB,IAAI,CAAC,UAAU,OAAO,SAAS,GAAG;EAElC,MAAM,OAAO,OAAO,MAAM,GAAG,CAAC;EAC9B,IAAI,OAAO,OAAO,MAAM,CAAC;EACzB,MAAM,QAAQ,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,GAAG;EACrD,IAAI,OAAO,SAAS;EACpB,IAAI,KAAK,WAAW,IAAI,GAAG,OAAO,KAAK,MAAM,CAAC;EAC9C,IAAI,CAAC,QAAS,CAAC,KAAK,SAAS,GAAG,KAAK,KAAK,SAAS,GAAG,KAAK,CAAC,OAAQ;EAEpE,MAAM,IAAI,MAAM,EAAE,KAAK,CAAC;CAC1B;CAEA,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC5E;AAEA,eAAsB,yBACpB,SACA,EAAE,YAAY,iBACC;CACf,IAAI;EACF,MAAM,aAAa,QAAQ,SAAS,cAAc;EAClD,MAAM,WAAW,QAAQ,OAAO,UAAU;EAC1C,MAAM,gBAAgB,MAAM,cAAc,SAAS,eAAe,UAAU;EAE5E,MAAM,UAAU,QAAQ,aAAa,CAAC,EAAE;EACxC,IAAI,CAAC,eAAe,kBAAkB,CAAC,SAAS,gBAAgB;EAEhE,MAAM,SAAS,MAAM,QAAQ,eAAe,OAAO;GAAC;GAAM,cAAc;GAAgB,GAAG;EAAe,GAAG,EAC3G,SAAS,IACX,CAAC;EACD,IAAI,OAAO,aAAa,GAAG;GACzB,QAAQ,KAAK,8DAA8D,OAAO,MAAM;GACxF;EACF;EAEA,MAAM,YAAY,MAAM,QAAQ,eAC9B,MACA;GAAC;GAAM;GAAwB;GAAM,cAAc;EAAc,GACjE,EAAE,SAAS,IAAO,CACpB;EACA,IAAI,UAAU,aAAa,GAAG;GAC5B,QAAQ,KAAK,oEAAoE,UAAU,MAAM;GACjG;EACF;EAEA,MAAM,QAAQ,IAAI,IAAI,4BAA4B,OAAO,MAAM,CAAC,CAAC,KAAI,SAAQ,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;EAC/F,KAAK,MAAM,QAAQ,UAAU,OAAO,MAAM,IAAI,GAAG;GAC/C,MAAM,iBAAiB,KAAK,QAAQ,SAAS,EAAE;GAC/C,IAAI,gBACF,MAAM,IAAI,gBAAgB,EAAE,MAAM,eAAe,CAAC;EAEtD;EAEA,MAAM,WAAW,aAAa;GAC5B;GACA;GACA,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;EAC5E,CAAC;CACH,SAAS,OAAO;EACd,QAAQ,KAAK,yDAAyD,KAAK;CAC7E;AACF;AAEA,SAAgB,yBACd,SACA,cACY;CACZ,IAAI,UAAU,QAAQ,QAAQ;CAC9B,OAAO,QAAQ,uBAAuB;EACpC,UAAU,QAAQ,WAAW,yBAAyB,SAAS,YAAY,CAAC;EAC5E,OAAO;CACT,CAAC;AACH"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AgentControllerEvent } from '@mastra/core/agent-controller';
|
|
2
|
+
import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
|
|
3
|
+
export interface FirstExecCaptureSession {
|
|
4
|
+
readonly identity: {
|
|
5
|
+
getResourceId(): string;
|
|
6
|
+
};
|
|
7
|
+
subscribe(listener: (event: AgentControllerEvent) => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface FirstExecCaptureDependencies {
|
|
10
|
+
sourceControl: {
|
|
11
|
+
sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMeaningfulExec'>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Record when a session's agent completed its first successful sandbox exec
|
|
16
|
+
* (the TTFME anchor — "time to first meaningful exec").
|
|
17
|
+
*
|
|
18
|
+
* `command_exit` is emitted only for the agent's own foreground
|
|
19
|
+
* `execute_command` tool calls: direct `sandbox.executeCommand()` calls from
|
|
20
|
+
* the skill loader / preflight / materializer never flow through the tool, so
|
|
21
|
+
* the first successful exit event is definitionally the agent's first
|
|
22
|
+
* meaningful exec — no command classifier needed. Background spawns emit no
|
|
23
|
+
* exit event at spawn time and are intentionally excluded.
|
|
24
|
+
*
|
|
25
|
+
* Failed commands (nonzero exit) don't count; the listener stays subscribed
|
|
26
|
+
* until a successful exit, then unsubscribes. The storage write is guarded
|
|
27
|
+
* (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized
|
|
28
|
+
* sessions, and sessions without a source-control row are no-ops.
|
|
29
|
+
*/
|
|
30
|
+
export declare function observeSessionFirstExec(session: FirstExecCaptureSession, { sourceControl }: FirstExecCaptureDependencies): () => void;
|
|
31
|
+
//# sourceMappingURL=first-exec-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-exec-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-exec-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,CAAC;KACnF,CAAC;CACH;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,uBAAuB,EAChC,EAAE,aAAa,EAAE,EAAE,4BAA4B,GAC9C,MAAM,IAAI,CAWZ"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/session/first-exec-capture.ts
|
|
2
|
+
/**
|
|
3
|
+
* Record when a session's agent completed its first successful sandbox exec
|
|
4
|
+
* (the TTFME anchor — "time to first meaningful exec").
|
|
5
|
+
*
|
|
6
|
+
* `command_exit` is emitted only for the agent's own foreground
|
|
7
|
+
* `execute_command` tool calls: direct `sandbox.executeCommand()` calls from
|
|
8
|
+
* the skill loader / preflight / materializer never flow through the tool, so
|
|
9
|
+
* the first successful exit event is definitionally the agent's first
|
|
10
|
+
* meaningful exec — no command classifier needed. Background spawns emit no
|
|
11
|
+
* exit event at spawn time and are intentionally excluded.
|
|
12
|
+
*
|
|
13
|
+
* Failed commands (nonzero exit) don't count; the listener stays subscribed
|
|
14
|
+
* until a successful exit, then unsubscribes. The storage write is guarded
|
|
15
|
+
* (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized
|
|
16
|
+
* sessions, and sessions without a source-control row are no-ops.
|
|
17
|
+
*/
|
|
18
|
+
function observeSessionFirstExec(session, { sourceControl }) {
|
|
19
|
+
let seen = false;
|
|
20
|
+
const unsubscribe = session.subscribe((event) => {
|
|
21
|
+
if (seen || event.type !== "command_exit" || !event.success) return;
|
|
22
|
+
seen = true;
|
|
23
|
+
unsubscribe();
|
|
24
|
+
sourceControl.sessions.markFirstMeaningfulExec({ sessionId: session.identity.getResourceId() }).catch((error) => console.warn("[Factory first-exec capture] Unable to persist first exec time.", error));
|
|
25
|
+
});
|
|
26
|
+
return unsubscribe;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { observeSessionFirstExec };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=first-exec-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-exec-capture.js","names":[],"sources":["../../src/session/first-exec-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstExecCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstExecCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMeaningfulExec'>;\n };\n}\n\n/**\n * Record when a session's agent completed its first successful sandbox exec\n * (the TTFME anchor — \"time to first meaningful exec\").\n *\n * `command_exit` is emitted only for the agent's own foreground\n * `execute_command` tool calls: direct `sandbox.executeCommand()` calls from\n * the skill loader / preflight / materializer never flow through the tool, so\n * the first successful exit event is definitionally the agent's first\n * meaningful exec — no command classifier needed. Background spawns emit no\n * exit event at spawn time and are intentionally excluded.\n *\n * Failed commands (nonzero exit) don't count; the listener stays subscribed\n * until a successful exit, then unsubscribes. The storage write is guarded\n * (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row are no-ops.\n */\nexport function observeSessionFirstExec(\n session: FirstExecCaptureSession,\n { sourceControl }: FirstExecCaptureDependencies,\n): () => void {\n let seen = false;\n const unsubscribe = session.subscribe(event => {\n if (seen || event.type !== 'command_exit' || !event.success) return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMeaningfulExec({ sessionId: session.identity.getResourceId() })\n .catch(error => console.warn('[Factory first-exec capture] Unable to persist first exec time.', error));\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,SAAgB,wBACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CACX,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,QAAQ,MAAM,SAAS,kBAAkB,CAAC,MAAM,SAAS;EAC7D,OAAO;EACP,YAAY;EACZ,cAAmB,SAChB,wBAAwB,EAAE,WAAW,QAAQ,SAAS,cAAc,EAAE,CAAC,CAAC,CACxE,OAAM,UAAS,QAAQ,KAAK,mEAAmE,KAAK,CAAC;CAC1G,CAAC;CACD,OAAO;AACT"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentControllerEvent } from '@mastra/core/agent-controller';
|
|
2
|
+
import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
|
|
3
|
+
export interface FirstMessageCaptureSession {
|
|
4
|
+
readonly identity: {
|
|
5
|
+
getResourceId(): string;
|
|
6
|
+
};
|
|
7
|
+
subscribe(listener: (event: AgentControllerEvent) => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface FirstMessageCaptureDependencies {
|
|
10
|
+
sourceControl: {
|
|
11
|
+
sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMessage'>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Record when a session's agent first started working on a message.
|
|
16
|
+
*
|
|
17
|
+
* `agent_start` is the earliest reliable session event for "a message reached
|
|
18
|
+
* the agent": it fires when the run engine begins processing an accepted
|
|
19
|
+
* message — a user's chat message, a factory run's kickoff prompt, or a
|
|
20
|
+
* channel message — for every session kind (user, work, review). The plain
|
|
21
|
+
* user message itself is never emitted as a `message_start` event, so the run
|
|
22
|
+
* start is the observable proxy, milliseconds after arrival.
|
|
23
|
+
*
|
|
24
|
+
* The listener unsubscribes after the first event; the storage write is
|
|
25
|
+
* guarded (`first_message_at IS NULL`), so restarts, re-materialized sessions,
|
|
26
|
+
* and sessions without a source-control row (chat-only channels) are no-ops.
|
|
27
|
+
*/
|
|
28
|
+
export declare function observeSessionFirstMessage(session: FirstMessageCaptureSession, { sourceControl }: FirstMessageCaptureDependencies): () => void;
|
|
29
|
+
//# sourceMappingURL=first-message-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-message-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-message-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,+BAA+B;IAC9C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC;KAC5E,CAAC;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,0BAA0B,EACnC,EAAE,aAAa,EAAE,EAAE,+BAA+B,GACjD,MAAM,IAAI,CAWZ"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/session/first-message-capture.ts
|
|
2
|
+
/**
|
|
3
|
+
* Record when a session's agent first started working on a message.
|
|
4
|
+
*
|
|
5
|
+
* `agent_start` is the earliest reliable session event for "a message reached
|
|
6
|
+
* the agent": it fires when the run engine begins processing an accepted
|
|
7
|
+
* message — a user's chat message, a factory run's kickoff prompt, or a
|
|
8
|
+
* channel message — for every session kind (user, work, review). The plain
|
|
9
|
+
* user message itself is never emitted as a `message_start` event, so the run
|
|
10
|
+
* start is the observable proxy, milliseconds after arrival.
|
|
11
|
+
*
|
|
12
|
+
* The listener unsubscribes after the first event; the storage write is
|
|
13
|
+
* guarded (`first_message_at IS NULL`), so restarts, re-materialized sessions,
|
|
14
|
+
* and sessions without a source-control row (chat-only channels) are no-ops.
|
|
15
|
+
*/
|
|
16
|
+
function observeSessionFirstMessage(session, { sourceControl }) {
|
|
17
|
+
let seen = false;
|
|
18
|
+
const unsubscribe = session.subscribe((event) => {
|
|
19
|
+
if (seen || event.type !== "agent_start") return;
|
|
20
|
+
seen = true;
|
|
21
|
+
unsubscribe();
|
|
22
|
+
sourceControl.sessions.markFirstMessage({ sessionId: session.identity.getResourceId() }).catch((error) => console.warn("[Factory first-message capture] Unable to persist first message time.", error));
|
|
23
|
+
});
|
|
24
|
+
return unsubscribe;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { observeSessionFirstMessage };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=first-message-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-message-capture.js","names":[],"sources":["../../src/session/first-message-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstMessageCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstMessageCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMessage'>;\n };\n}\n\n/**\n * Record when a session's agent first started working on a message.\n *\n * `agent_start` is the earliest reliable session event for \"a message reached\n * the agent\": it fires when the run engine begins processing an accepted\n * message — a user's chat message, a factory run's kickoff prompt, or a\n * channel message — for every session kind (user, work, review). The plain\n * user message itself is never emitted as a `message_start` event, so the run\n * start is the observable proxy, milliseconds after arrival.\n *\n * The listener unsubscribes after the first event; the storage write is\n * guarded (`first_message_at IS NULL`), so restarts, re-materialized sessions,\n * and sessions without a source-control row (chat-only channels) are no-ops.\n */\nexport function observeSessionFirstMessage(\n session: FirstMessageCaptureSession,\n { sourceControl }: FirstMessageCaptureDependencies,\n): () => void {\n let seen = false;\n const unsubscribe = session.subscribe(event => {\n if (seen || event.type !== 'agent_start') return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMessage({ sessionId: session.identity.getResourceId() })\n .catch(error => console.warn('[Factory first-message capture] Unable to persist first message time.', error));\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6BA,SAAgB,2BACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CACX,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,QAAQ,MAAM,SAAS,eAAe;EAC1C,OAAO;EACP,YAAY;EACZ,cAAmB,SAChB,iBAAiB,EAAE,WAAW,QAAQ,SAAS,cAAc,EAAE,CAAC,CAAC,CACjE,OAAM,UAAS,QAAQ,KAAK,yEAAyE,KAAK,CAAC;CAChH,CAAC;CACD,OAAO;AACT"}
|
|
@@ -152,6 +152,10 @@ export interface SourceControlSession {
|
|
|
152
152
|
sandboxId: string | null;
|
|
153
153
|
sandboxWorkdir: string | null;
|
|
154
154
|
materializedAt: Date | null;
|
|
155
|
+
/** When the first user message reached the session's agent. Write-once. */
|
|
156
|
+
firstMessageAt: Date | null;
|
|
157
|
+
/** When the agent's first successful sandbox exec finished. Write-once. */
|
|
158
|
+
firstMeaningfulExecAt: Date | null;
|
|
155
159
|
createdAt: Date;
|
|
156
160
|
updatedAt: Date;
|
|
157
161
|
}
|
|
@@ -348,6 +352,25 @@ export interface SourceControlStorageHandle {
|
|
|
348
352
|
markMaterialized(args: {
|
|
349
353
|
id: string;
|
|
350
354
|
}): Promise<void>;
|
|
355
|
+
/**
|
|
356
|
+
* Record when the session's agent received its first user message.
|
|
357
|
+
* Write-once: the guarded update only lands while the column is still
|
|
358
|
+
* NULL, so retries and process restarts are no-ops. Keyed by the
|
|
359
|
+
* controller-facing `sessionId` (not the row id) because the caller —
|
|
360
|
+
* the session observer — only knows the controller resourceId.
|
|
361
|
+
*/
|
|
362
|
+
markFirstMessage(args: {
|
|
363
|
+
sessionId: string;
|
|
364
|
+
}): Promise<void>;
|
|
365
|
+
/**
|
|
366
|
+
* Record when the session's agent completed its first successful sandbox
|
|
367
|
+
* exec (TTFME anchor). Same write-once contract as `markFirstMessage`:
|
|
368
|
+
* guarded on the column being NULL, keyed by the controller-facing
|
|
369
|
+
* `sessionId`.
|
|
370
|
+
*/
|
|
371
|
+
markFirstMeaningfulExec(args: {
|
|
372
|
+
sessionId: string;
|
|
373
|
+
}): Promise<void>;
|
|
351
374
|
delete(id: string): Promise<void>;
|
|
352
375
|
};
|
|
353
376
|
}
|