@mastra/factory 0.10.2-alpha.0 → 0.10.2-alpha.1

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.
@@ -1,3 +1,4 @@
1
+ import { hasResolvedOrg, seedSessionOrg } from "./org-seed.js";
1
2
  import { DEFAULT_OM_MODEL_ID } from "@mastra/code-sdk/constants";
2
3
  //#region src/session/memory-settings-hydration.ts
3
4
  /** Default thresholds mirror the TUI `/om` fallbacks. */
@@ -28,27 +29,44 @@ async function applyStoredMemorySettings(session, record, fallbackOmModelId) {
28
29
  if (Object.keys(updates).length > 0) await session.state.set(updates);
29
30
  }
30
31
  /**
31
- * Seed a freshly created controller session's observational-memory settings
32
- * from the owner's stored `memory-settings` row. Registered as a blocking
33
- * session-created listener so the seed lands before the caller can start a run.
32
+ * Seed a freshly created controller session's tenant org and its
33
+ * observational-memory settings from the owner's source-control row. Registered
34
+ * as a blocking session-created listener so the seed lands before the caller can
35
+ * start a run.
34
36
  *
35
- * Sessions tagged `factoryProjectId` (work/review runs, created with that tag)
36
- * hydrate through the start coordinator; sessions without a GitHub
37
- * source-control row (e.g. chat-only channel sessions) hydrate through
38
- * `hydrateFactorySession` with their own resolved tenant. Both are skipped
39
- * here. Best-effort: failures are logged, never thrown.
37
+ * The org seed matters beyond settings. Subconscious knowledge capture scopes
38
+ * every node and record on `factoryOrgId`; before the SDK refusal guard,
39
+ * missing it made capture substitute the session owner id. For web chat sessions
40
+ * that is the agent controller's own id rather than a tenant, so captured
41
+ * knowledge landed under an org rung no reader ever queries. Same rule as the
42
+ * start coordinator: the org
43
+ * comes from the row the session was created from, never improvised from an
44
+ * owner id.
45
+ *
46
+ * Memory settings for sessions tagged `factoryProjectId` (work/review runs) are
47
+ * owned by the start coordinator, and sessions without a GitHub source-control
48
+ * row (e.g. chat-only channel sessions) hydrate through `hydrateFactorySession`
49
+ * with their own resolved tenant; both are skipped here. The org seed is not
50
+ * skipped on the tag alone: a web chat session persists `factoryProjectId` from
51
+ * its browser seed, so on resume it carries the tag without ever having been
52
+ * through the coordinator. Best-effort: failures are logged, never thrown.
40
53
  */
41
54
  async function hydrateSessionMemorySettings(session, { sourceControl, memorySettings }) {
42
- if (session.state.get()?.factoryProjectId) return;
55
+ const state = session.state.get() ?? {};
56
+ const isFactoryRun = Boolean(state.factoryProjectId);
57
+ if (isFactoryRun && hasResolvedOrg(state.factoryOrgId)) return;
43
58
  try {
44
59
  const record = await sourceControl.sessions.getBySessionId(session.identity.getResourceId());
60
+ await seedSessionOrg(session, record?.orgId);
45
61
  if (!record) return;
62
+ if (isFactoryRun) return;
46
63
  await applyStoredMemorySettings(session, await memorySettings.get({
47
64
  orgId: record.orgId,
48
65
  userId: record.userId
49
66
  }));
50
67
  } catch (error) {
51
68
  console.warn("[Factory memory-settings hydration] Unable to apply stored memory settings.", error);
69
+ if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, void 0);
52
70
  }
53
71
  }
54
72
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"memory-settings-hydration.js","names":[],"sources":["../../src/session/memory-settings-hydration.ts"],"sourcesContent":["import { DEFAULT_OM_MODEL_ID } from '@mastra/code-sdk/constants';\n\nimport type { MemorySettingsRecord, MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\n/** Default thresholds mirror the TUI `/om` fallbacks. */\nexport const DEFAULT_OBSERVATION_THRESHOLD = 30_000;\nexport const DEFAULT_REFLECTION_THRESHOLD = 40_000;\n\n/** One observational-memory role's read/switch surface. */\ninterface OMRoleSlice {\n modelId: () => string | undefined;\n switchModel: (args: { modelId: string }) => Promise<unknown>;\n}\n\n/**\n * Session-state fields memory-settings hydration writes. The index signatures\n * mirror `MastraCodeState` so the concrete `Session.state.set(Partial<MastraCodeState>)`\n * stays assignable to this minimal surface (contravariant parameter check).\n */\ninterface OMStateWrites {\n [key: string]: unknown;\n [key: `subagentModelId_${string}`]: string | undefined;\n observationThreshold?: number;\n reflectionThreshold?: number;\n observeAttachments?: 'auto' | boolean;\n}\n\n/** The slice of a session needed to apply stored observational-memory settings. */\nexport interface OMConfigurableSession {\n om: { observer: OMRoleSlice; reflector: OMRoleSlice };\n state: {\n get: () => Record<string, unknown> | undefined;\n set: (updates: OMStateWrites) => Promise<void> | void;\n };\n}\n\n/**\n * Apply a stored memory-settings row onto a session, so the DB — not whatever\n * happens to sit in persisted session state (e.g. a stale boot-time seed from\n * before memory settings moved to the DB) — is what the web surface reads and\n * what the session's OM actually runs with. The row is authoritative: knobs\n * without a stored value reset to the built-in defaults. This is the single\n * application path shared by the settings routes, coordinator hydration, and\n * the web session boot seed.\n */\nexport async function applyStoredMemorySettings(\n session: OMConfigurableSession,\n record: MemorySettingsRecord | null,\n fallbackOmModelId?: string,\n): Promise<void> {\n for (const role of ['observer', 'reflector'] as const) {\n const stored = role === 'observer' ? record?.observerModelId : record?.reflectorModelId;\n const target = stored ?? fallbackOmModelId ?? DEFAULT_OM_MODEL_ID;\n if (session.om[role].modelId() !== target) {\n await session.om[role].switchModel({ modelId: target });\n }\n }\n const state = session.state.get() ?? {};\n const updates: OMStateWrites = {};\n const observationThreshold = record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD;\n if (state.observationThreshold !== observationThreshold) {\n updates.observationThreshold = observationThreshold;\n }\n const reflectionThreshold = record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD;\n if (state.reflectionThreshold !== reflectionThreshold) {\n updates.reflectionThreshold = reflectionThreshold;\n }\n const observeAttachments = record?.observeAttachments ?? 'auto';\n if ((state.observeAttachments ?? 'auto') !== observeAttachments) {\n updates.observeAttachments = observeAttachments;\n }\n if (Object.keys(updates).length > 0) await session.state.set(updates);\n}\n\nexport interface MemorySettingsHydrationSession extends OMConfigurableSession {\n readonly identity: { getResourceId(): string };\n}\n\nexport interface MemorySettingsHydrationDependencies {\n /** GitHub-integration source-control rows — the only creator of web user sessions today. */\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n memorySettings: Pick<MemorySettingsStorage, 'get'>;\n}\n\n/**\n * Seed a freshly created controller session's observational-memory settings\n * from the owner's stored `memory-settings` row. Registered as a blocking\n * session-created listener so the seed lands before the caller can start a run.\n *\n * Sessions tagged `factoryProjectId` (work/review runs, created with that tag)\n * hydrate through the start coordinator; sessions without a GitHub\n * source-control row (e.g. chat-only channel sessions) hydrate through\n * `hydrateFactorySession` with their own resolved tenant. Both are skipped\n * here. Best-effort: failures are logged, never thrown.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl, memorySettings }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n if (session.state.get()?.factoryProjectId) return;\n try {\n const record = await sourceControl.sessions.getBySessionId(session.identity.getResourceId());\n if (!record) return;\n const settings = await memorySettings.get({ orgId: record.orgId, userId: record.userId });\n await applyStoredMemorySettings(session, settings);\n } catch (error) {\n console.warn('[Factory memory-settings hydration] Unable to apply stored memory settings.', error);\n }\n}\n"],"mappings":";;;AAMA,MAAa,gCAAgC;AAC7C,MAAa,+BAA+B;;;;;;;;;;AAuC5C,eAAsB,0BACpB,SACA,QACA,mBACe;CACf,KAAK,MAAM,QAAQ,CAAC,YAAY,WAAW,GAAY;EAErD,MAAM,UADS,SAAS,aAAa,QAAQ,kBAAkB,QAAQ,qBAC9C,qBAAqB;EAC9C,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,MAAM,QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,EAAE,SAAS,OAAO,CAAC;CAE1D;CACA,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,UAAyB,CAAC;CAChC,MAAM,uBAAuB,QAAQ,wBAAA;CACrC,IAAI,MAAM,yBAAyB,sBACjC,QAAQ,uBAAuB;CAEjC,MAAM,sBAAsB,QAAQ,uBAAA;CACpC,IAAI,MAAM,wBAAwB,qBAChC,QAAQ,sBAAsB;CAEhC,MAAM,qBAAqB,QAAQ,sBAAsB;CACzD,KAAK,MAAM,sBAAsB,YAAY,oBAC3C,QAAQ,qBAAqB;CAE/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG,MAAM,QAAQ,MAAM,IAAI,OAAO;AACtE;;;;;;;;;;;;AAyBA,eAAsB,6BACpB,SACA,EAAE,eAAe,kBACF;CACf,IAAI,QAAQ,MAAM,IAAI,CAAC,EAAE,kBAAkB;CAC3C,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC;EAC3F,IAAI,CAAC,QAAQ;EAEb,MAAM,0BAA0B,SAAS,MADlB,eAAe,IAAI;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;EAAO,CAAC,CACvC;CACnD,SAAS,OAAO;EACd,QAAQ,KAAK,+EAA+E,KAAK;CACnG;AACF"}
1
+ {"version":3,"file":"memory-settings-hydration.js","names":[],"sources":["../../src/session/memory-settings-hydration.ts"],"sourcesContent":["import { DEFAULT_OM_MODEL_ID } from '@mastra/code-sdk/constants';\n\nimport type { MemorySettingsRecord, MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { hasResolvedOrg, seedSessionOrg } from './org-seed.js';\n\n/** Default thresholds mirror the TUI `/om` fallbacks. */\nexport const DEFAULT_OBSERVATION_THRESHOLD = 30_000;\nexport const DEFAULT_REFLECTION_THRESHOLD = 40_000;\n\n/** One observational-memory role's read/switch surface. */\ninterface OMRoleSlice {\n modelId: () => string | undefined;\n switchModel: (args: { modelId: string }) => Promise<unknown>;\n}\n\n/**\n * Session-state fields memory-settings hydration writes. The index signatures\n * mirror `MastraCodeState` so the concrete `Session.state.set(Partial<MastraCodeState>)`\n * stays assignable to this minimal surface (contravariant parameter check).\n */\ninterface OMStateWrites {\n [key: string]: unknown;\n [key: `subagentModelId_${string}`]: string | undefined;\n observationThreshold?: number;\n reflectionThreshold?: number;\n observeAttachments?: 'auto' | boolean;\n factoryOrgId?: string;\n}\n\n/** The slice of a session needed to apply stored observational-memory settings. */\nexport interface OMConfigurableSession {\n om: { observer: OMRoleSlice; reflector: OMRoleSlice };\n state: {\n get: () => Record<string, unknown> | undefined;\n set: (updates: OMStateWrites) => Promise<void> | void;\n };\n}\n\n/**\n * Apply a stored memory-settings row onto a session, so the DB — not whatever\n * happens to sit in persisted session state (e.g. a stale boot-time seed from\n * before memory settings moved to the DB) — is what the web surface reads and\n * what the session's OM actually runs with. The row is authoritative: knobs\n * without a stored value reset to the built-in defaults. This is the single\n * application path shared by the settings routes, coordinator hydration, and\n * the web session boot seed.\n */\nexport async function applyStoredMemorySettings(\n session: OMConfigurableSession,\n record: MemorySettingsRecord | null,\n fallbackOmModelId?: string,\n): Promise<void> {\n for (const role of ['observer', 'reflector'] as const) {\n const stored = role === 'observer' ? record?.observerModelId : record?.reflectorModelId;\n const target = stored ?? fallbackOmModelId ?? DEFAULT_OM_MODEL_ID;\n if (session.om[role].modelId() !== target) {\n await session.om[role].switchModel({ modelId: target });\n }\n }\n const state = session.state.get() ?? {};\n const updates: OMStateWrites = {};\n const observationThreshold = record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD;\n if (state.observationThreshold !== observationThreshold) {\n updates.observationThreshold = observationThreshold;\n }\n const reflectionThreshold = record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD;\n if (state.reflectionThreshold !== reflectionThreshold) {\n updates.reflectionThreshold = reflectionThreshold;\n }\n const observeAttachments = record?.observeAttachments ?? 'auto';\n if ((state.observeAttachments ?? 'auto') !== observeAttachments) {\n updates.observeAttachments = observeAttachments;\n }\n if (Object.keys(updates).length > 0) await session.state.set(updates);\n}\n\nexport interface MemorySettingsHydrationSession extends OMConfigurableSession {\n readonly identity: { getResourceId(): string };\n}\n\nexport interface MemorySettingsHydrationDependencies {\n /** GitHub-integration source-control rows — the only creator of web user sessions today. */\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n memorySettings: Pick<MemorySettingsStorage, 'get'>;\n}\n\n/**\n * Seed a freshly created controller session's tenant org and its\n * observational-memory settings from the owner's source-control row. Registered\n * as a blocking session-created listener so the seed lands before the caller can\n * start a run.\n *\n * The org seed matters beyond settings. Subconscious knowledge capture scopes\n * every node and record on `factoryOrgId`; before the SDK refusal guard,\n * missing it made capture substitute the session owner id. For web chat sessions\n * that is the agent controller's own id rather than a tenant, so captured\n * knowledge landed under an org rung no reader ever queries. Same rule as the\n * start coordinator: the org\n * comes from the row the session was created from, never improvised from an\n * owner id.\n *\n * Memory settings for sessions tagged `factoryProjectId` (work/review runs) are\n * owned by the start coordinator, and sessions without a GitHub source-control\n * row (e.g. chat-only channel sessions) hydrate through `hydrateFactorySession`\n * with their own resolved tenant; both are skipped here. The org seed is not\n * skipped on the tag alone: a web chat session persists `factoryProjectId` from\n * its browser seed, so on resume it carries the tag without ever having been\n * through the coordinator. Best-effort: failures are logged, never thrown.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl, memorySettings }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\n // A coordinator-hydrated run already carries both halves. Nothing to add.\n if (isFactoryRun && hasResolvedOrg(state.factoryOrgId)) return;\n try {\n const record = await sourceControl.sessions.getBySessionId(session.identity.getResourceId());\n // No row, or a row whose org is blank, leaves the session with no tenant.\n // Mark it rather than returning silently: an unmarked projectless factory\n // session is indistinguishable from a local one, and capture would file it\n // under the local scope — the same bug wearing a different rung.\n await seedSessionOrg(session, record?.orgId);\n if (!record) return;\n if (isFactoryRun) return;\n const settings = await memorySettings.get({ orgId: record.orgId, userId: record.userId });\n await applyStoredMemorySettings(session, settings);\n } catch (error) {\n console.warn('[Factory memory-settings hydration] Unable to apply stored memory settings.', error);\n // A failed lookup is an unresolved org, not an absent one — unless the seed\n // already landed and a later step is what threw.\n if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, undefined);\n }\n}\n"],"mappings":";;;;AAOA,MAAa,gCAAgC;AAC7C,MAAa,+BAA+B;;;;;;;;;;AAwC5C,eAAsB,0BACpB,SACA,QACA,mBACe;CACf,KAAK,MAAM,QAAQ,CAAC,YAAY,WAAW,GAAY;EAErD,MAAM,UADS,SAAS,aAAa,QAAQ,kBAAkB,QAAQ,qBAC9C,qBAAqB;EAC9C,IAAI,QAAQ,GAAG,KAAK,CAAC,QAAQ,MAAM,QACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,EAAE,SAAS,OAAO,CAAC;CAE1D;CACA,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,UAAyB,CAAC;CAChC,MAAM,uBAAuB,QAAQ,wBAAA;CACrC,IAAI,MAAM,yBAAyB,sBACjC,QAAQ,uBAAuB;CAEjC,MAAM,sBAAsB,QAAQ,uBAAA;CACpC,IAAI,MAAM,wBAAwB,qBAChC,QAAQ,sBAAsB;CAEhC,MAAM,qBAAqB,QAAQ,sBAAsB;CACzD,KAAK,MAAM,sBAAsB,YAAY,oBAC3C,QAAQ,qBAAqB;CAE/B,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG,MAAM,QAAQ,MAAM,IAAI,OAAO;AACtE;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,eAAsB,6BACpB,SACA,EAAE,eAAe,kBACF;CACf,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,eAAe,QAAQ,MAAM,gBAAgB;CAEnD,IAAI,gBAAgB,eAAe,MAAM,YAAY,GAAG;CACxD,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC;EAK3F,MAAM,eAAe,SAAS,QAAQ,KAAK;EAC3C,IAAI,CAAC,QAAQ;EACb,IAAI,cAAc;EAElB,MAAM,0BAA0B,SAAS,MADlB,eAAe,IAAI;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;EAAO,CAAC,CACvC;CACnD,SAAS,OAAO;EACd,QAAQ,KAAK,+EAA+E,KAAK;EAGjG,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,MAAM,eAAe,SAAS,KAAA,CAAS;CACjF;AACF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The tenant organization a session's knowledge is scoped to.
3
+ *
4
+ * Subconscious knowledge capture scopes every node and record on
5
+ * `factoryOrgId`. A session that reaches the capture seam without one used to
6
+ * fall back to the session owner id — for factory sessions the agent
7
+ * controller's own id — so the knowledge landed under an org rung no reader
8
+ * ever queries. The write succeeded and the read could never see it.
9
+ *
10
+ * The fix is that every session-creation path seeds the org it already holds,
11
+ * and a path that cannot resolve one marks the session `factoryOrgUnresolved`
12
+ * so the capture side refuses loudly instead of inventing an identity. "No
13
+ * project id" is not a proxy for "not a factory session" — chat sessions and
14
+ * Slack channel sessions are factory-owned and carry no project id — which is
15
+ * why the unresolved case needs its own explicit marker.
16
+ */
17
+ /**
18
+ * Session-state fields org seeding writes. The index signatures mirror
19
+ * `MastraCodeState` so a concrete `Session.state.set(Partial<MastraCodeState>)`
20
+ * stays assignable to this minimal surface (contravariant parameter check).
21
+ */
22
+ export interface OrgSeedStateWrites {
23
+ [key: string]: unknown;
24
+ [key: `subagentModelId_${string}`]: string | undefined;
25
+ factoryOrgId?: string;
26
+ factoryOrgUnresolved?: boolean;
27
+ }
28
+ /** The slice of a session needed to seed its organization. */
29
+ export interface OrgSeedableSession {
30
+ state: {
31
+ get: () => Record<string, unknown> | undefined;
32
+ set: (updates: OrgSeedStateWrites) => Promise<void> | void;
33
+ };
34
+ }
35
+ /**
36
+ * A request context carrying the tenant on its `user` key. Slack stamps
37
+ * `{ id, organizationId }` and the GitHub webhook `{ workosId, organizationId }`,
38
+ * so only the shared `organizationId` field may be read here.
39
+ */
40
+ export interface OrgBearingRequestContext {
41
+ get: (key: string) => unknown;
42
+ }
43
+ /** Read the tenant org off a request context's `user` key, if there is one. */
44
+ export declare function readRequestContextOrgId(requestContext: OrgBearingRequestContext | undefined): string | undefined;
45
+ /**
46
+ * Whether a session state value counts as a resolved organization.
47
+ *
48
+ * The capture side trims before deciding (`sdk/src/agents/memory.ts`), so the
49
+ * recovery guards have to agree with it: a whitespace-only value that reads as
50
+ * truthy here would look resolved to a heal path while capture still refuses,
51
+ * and nothing would ever repair it. Not every seam routes its seed through
52
+ * `seedSessionOrg`, so this cannot be assumed away.
53
+ */
54
+ export declare function hasResolvedOrg(orgId: unknown): boolean;
55
+ /**
56
+ * Seed the session's organization, or mark it unresolved when there is none.
57
+ *
58
+ * An absent, empty, or whitespace-only org is a refusal, not a fallback: a
59
+ * blank org rung is not something canonicalization can save. A successful
60
+ * resolve also clears a stale marker, because the session-start hook runs at
61
+ * most once per session per process and nothing else would ever clear it.
62
+ *
63
+ * Best-effort by contract — every caller is a session-created listener that
64
+ * must not sink a run that is otherwise ready.
65
+ */
66
+ export declare function seedSessionOrg(session: OrgSeedableSession, orgId: string | null | undefined): Promise<void>;
67
+ //# sourceMappingURL=org-seed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-seed.d.ts","sourceRoot":"","sources":["../../src/session/org-seed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,mBAAmB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC/C,GAAG,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC5D,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAC/B;AAED,+EAA+E;AAC/E,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,wBAAwB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMhH;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBjH"}
@@ -0,0 +1,53 @@
1
+ //#region src/session/org-seed.ts
2
+ /** Read the tenant org off a request context's `user` key, if there is one. */
3
+ function readRequestContextOrgId(requestContext) {
4
+ if (!requestContext) return void 0;
5
+ const user = requestContext.get("user");
6
+ if (!user || typeof user !== "object") return void 0;
7
+ const orgId = user.organizationId;
8
+ return typeof orgId === "string" ? orgId : void 0;
9
+ }
10
+ /**
11
+ * Whether a session state value counts as a resolved organization.
12
+ *
13
+ * The capture side trims before deciding (`sdk/src/agents/memory.ts`), so the
14
+ * recovery guards have to agree with it: a whitespace-only value that reads as
15
+ * truthy here would look resolved to a heal path while capture still refuses,
16
+ * and nothing would ever repair it. Not every seam routes its seed through
17
+ * `seedSessionOrg`, so this cannot be assumed away.
18
+ */
19
+ function hasResolvedOrg(orgId) {
20
+ return typeof orgId === "string" && orgId.trim().length > 0;
21
+ }
22
+ /**
23
+ * Seed the session's organization, or mark it unresolved when there is none.
24
+ *
25
+ * An absent, empty, or whitespace-only org is a refusal, not a fallback: a
26
+ * blank org rung is not something canonicalization can save. A successful
27
+ * resolve also clears a stale marker, because the session-start hook runs at
28
+ * most once per session per process and nothing else would ever clear it.
29
+ *
30
+ * Best-effort by contract — every caller is a session-created listener that
31
+ * must not sink a run that is otherwise ready.
32
+ */
33
+ async function seedSessionOrg(session, orgId) {
34
+ const resolved = typeof orgId === "string" ? orgId.trim() : "";
35
+ if (typeof session.state?.get !== "function" || typeof session.state?.set !== "function") return;
36
+ try {
37
+ const state = session.state.get() ?? {};
38
+ if (!resolved) {
39
+ if (state.factoryOrgUnresolved !== true) await session.state.set({ factoryOrgUnresolved: true });
40
+ return;
41
+ }
42
+ const updates = {};
43
+ if (state.factoryOrgId !== resolved) updates.factoryOrgId = resolved;
44
+ if (state.factoryOrgUnresolved) updates.factoryOrgUnresolved = false;
45
+ if (Object.keys(updates).length > 0) await session.state.set(updates);
46
+ } catch (error) {
47
+ console.warn("[Factory org seed] Unable to record the session organization.", error);
48
+ }
49
+ }
50
+ //#endregion
51
+ export { hasResolvedOrg, readRequestContextOrgId, seedSessionOrg };
52
+
53
+ //# sourceMappingURL=org-seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-seed.js","names":[],"sources":["../../src/session/org-seed.ts"],"sourcesContent":["/**\n * The tenant organization a session's knowledge is scoped to.\n *\n * Subconscious knowledge capture scopes every node and record on\n * `factoryOrgId`. A session that reaches the capture seam without one used to\n * fall back to the session owner id — for factory sessions the agent\n * controller's own id — so the knowledge landed under an org rung no reader\n * ever queries. The write succeeded and the read could never see it.\n *\n * The fix is that every session-creation path seeds the org it already holds,\n * and a path that cannot resolve one marks the session `factoryOrgUnresolved`\n * so the capture side refuses loudly instead of inventing an identity. \"No\n * project id\" is not a proxy for \"not a factory session\" — chat sessions and\n * Slack channel sessions are factory-owned and carry no project id — which is\n * why the unresolved case needs its own explicit marker.\n */\n\n/**\n * Session-state fields org seeding writes. The index signatures mirror\n * `MastraCodeState` so a concrete `Session.state.set(Partial<MastraCodeState>)`\n * stays assignable to this minimal surface (contravariant parameter check).\n */\nexport interface OrgSeedStateWrites {\n [key: string]: unknown;\n [key: `subagentModelId_${string}`]: string | undefined;\n factoryOrgId?: string;\n factoryOrgUnresolved?: boolean;\n}\n\n/** The slice of a session needed to seed its organization. */\nexport interface OrgSeedableSession {\n state: {\n get: () => Record<string, unknown> | undefined;\n set: (updates: OrgSeedStateWrites) => Promise<void> | void;\n };\n}\n\n/**\n * A request context carrying the tenant on its `user` key. Slack stamps\n * `{ id, organizationId }` and the GitHub webhook `{ workosId, organizationId }`,\n * so only the shared `organizationId` field may be read here.\n */\nexport interface OrgBearingRequestContext {\n get: (key: string) => unknown;\n}\n\n/** Read the tenant org off a request context's `user` key, if there is one. */\nexport function readRequestContextOrgId(requestContext: OrgBearingRequestContext | undefined): string | undefined {\n if (!requestContext) return undefined;\n const user = requestContext.get('user');\n if (!user || typeof user !== 'object') return undefined;\n const orgId = (user as { organizationId?: unknown }).organizationId;\n return typeof orgId === 'string' ? orgId : undefined;\n}\n\n/**\n * Whether a session state value counts as a resolved organization.\n *\n * The capture side trims before deciding (`sdk/src/agents/memory.ts`), so the\n * recovery guards have to agree with it: a whitespace-only value that reads as\n * truthy here would look resolved to a heal path while capture still refuses,\n * and nothing would ever repair it. Not every seam routes its seed through\n * `seedSessionOrg`, so this cannot be assumed away.\n */\nexport function hasResolvedOrg(orgId: unknown): boolean {\n return typeof orgId === 'string' && orgId.trim().length > 0;\n}\n\n/**\n * Seed the session's organization, or mark it unresolved when there is none.\n *\n * An absent, empty, or whitespace-only org is a refusal, not a fallback: a\n * blank org rung is not something canonicalization can save. A successful\n * resolve also clears a stale marker, because the session-start hook runs at\n * most once per session per process and nothing else would ever clear it.\n *\n * Best-effort by contract — every caller is a session-created listener that\n * must not sink a run that is otherwise ready.\n */\nexport async function seedSessionOrg(session: OrgSeedableSession, orgId: string | null | undefined): Promise<void> {\n const resolved = typeof orgId === 'string' ? orgId.trim() : '';\n // Some session shapes (approval stubs, lightweight doubles) carry no state at\n // all. There is nothing to seed and nothing to mark, so this is not a warning.\n if (typeof session.state?.get !== 'function' || typeof session.state?.set !== 'function') return;\n try {\n const state = session.state.get() ?? {};\n if (!resolved) {\n if (state.factoryOrgUnresolved !== true) {\n await session.state.set({ factoryOrgUnresolved: true });\n }\n return;\n }\n const updates: OrgSeedStateWrites = {};\n if (state.factoryOrgId !== resolved) updates.factoryOrgId = resolved;\n if (state.factoryOrgUnresolved) updates.factoryOrgUnresolved = false;\n if (Object.keys(updates).length > 0) await session.state.set(updates);\n } catch (error) {\n console.warn('[Factory org seed] Unable to record the session organization.', error);\n }\n}\n"],"mappings":";;AA+CA,SAAgB,wBAAwB,gBAA0E;CAChH,IAAI,CAAC,gBAAgB,OAAO,KAAA;CAC5B,MAAM,OAAO,eAAe,IAAI,MAAM;CACtC,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO,KAAA;CAC9C,MAAM,QAAS,KAAsC;CACrD,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;;;;;;;;;;AAWA,SAAgB,eAAe,OAAyB;CACtD,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS;AAC5D;;;;;;;;;;;;AAaA,eAAsB,eAAe,SAA6B,OAAiD;CACjH,MAAM,WAAW,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CAG5D,IAAI,OAAO,QAAQ,OAAO,QAAQ,cAAc,OAAO,QAAQ,OAAO,QAAQ,YAAY;CAC1F,IAAI;EACF,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;EACtC,IAAI,CAAC,UAAU;GACb,IAAI,MAAM,yBAAyB,MACjC,MAAM,QAAQ,MAAM,IAAI,EAAE,sBAAsB,KAAK,CAAC;GAExD;EACF;EACA,MAAM,UAA8B,CAAC;EACrC,IAAI,MAAM,iBAAiB,UAAU,QAAQ,eAAe;EAC5D,IAAI,MAAM,sBAAsB,QAAQ,uBAAuB;EAC/D,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG,MAAM,QAAQ,MAAM,IAAI,OAAO;CACtE,SAAS,OAAO;EACd,QAAQ,KAAK,iEAAiE,KAAK;CACrF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/factory",
3
- "version": "0.10.2-alpha.0",
3
+ "version": "0.10.2-alpha.1",
4
4
  "description": "Mastra Software Factory module: the server core behind the Mastra Software Factory — storage domains, integrations, and surfaces for agent-powered software delivery",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -53,9 +53,9 @@
53
53
  "zod": "^4.3.6",
54
54
  "@mastra/auth-studio": "1.3.4",
55
55
  "@mastra/auth-workos": "1.6.4",
56
- "@mastra/core": "1.63.1-alpha.0",
56
+ "@mastra/code-sdk": "1.5.2-alpha.1",
57
57
  "@mastra/slack": "1.6.2",
58
- "@mastra/code-sdk": "1.5.2-alpha.0"
58
+ "@mastra/core": "1.63.1-alpha.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "22.20.1",
@@ -66,8 +66,8 @@
66
66
  "vitest": "4.1.10",
67
67
  "@internal/lint": "0.0.127",
68
68
  "@mastra/libsql": "1.22.0",
69
- "@mastra/pg": "1.22.0",
70
- "@internal/types-builder": "0.0.102"
69
+ "@internal/types-builder": "0.0.102",
70
+ "@mastra/pg": "1.22.0"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=22.19.0"