@mastra/factory 0.17.1-alpha.0 → 0.17.1-alpha.2
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/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +7 -2
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/integrations/base.d.ts +7 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +7 -0
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +1 -0
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +29 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +85 -11
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +10 -0
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +19 -1
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +7 -0
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +1 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +2 -1
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +6 -2
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +4 -1
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +27 -8
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +22 -0
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +42 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/storage/domains/comments/routes.d.ts.map +1 -1
- package/dist/storage/domains/comments/routes.js +14 -6
- package/dist/storage/domains/comments/routes.js.map +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAgB5F,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;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;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,CAS5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,
|
|
1
|
+
{"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAgB5F,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;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;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,CAS5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,EAC5D,GAAG,CAAC,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CA+Df;AAYD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB3G;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,YAAY,EAAE,6BAA6B,GAC1C,MAAM,IAAI,CA2FZ"}
|
|
@@ -22,10 +22,10 @@ function parseFilesystemCaptureFiles(output) {
|
|
|
22
22
|
}
|
|
23
23
|
return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));
|
|
24
24
|
}
|
|
25
|
-
async function captureSessionFilesystem(session, { filesystem, sourceControl }) {
|
|
25
|
+
async function captureSessionFilesystem(session, { filesystem, sourceControl }, ids) {
|
|
26
26
|
try {
|
|
27
|
-
const resourceId = session.identity.getResourceId();
|
|
28
|
-
const threadId = session.thread.requireId();
|
|
27
|
+
const resourceId = ids?.resourceId ?? session.identity.getResourceId();
|
|
28
|
+
const threadId = ids?.threadId ?? session.thread.requireId();
|
|
29
29
|
const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);
|
|
30
30
|
const sandbox = session.getWorkspace()?.sandbox;
|
|
31
31
|
if (!sourceSession || !sandbox?.executeCommand) return;
|
|
@@ -85,13 +85,22 @@ const pendingCaptures = /* @__PURE__ */ new Map();
|
|
|
85
85
|
* capture body is fully try/catch contained.
|
|
86
86
|
*/
|
|
87
87
|
async function waitForPendingFilesystemCapture(resourceId, timeoutMs = 1e4) {
|
|
88
|
-
|
|
88
|
+
let pending = pendingCaptures.get(resourceId);
|
|
89
89
|
if (!pending) return;
|
|
90
90
|
let timer;
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
const deadline = new Promise((resolve) => {
|
|
93
|
+
timer = setTimeout(() => {
|
|
94
|
+
timedOut = true;
|
|
95
|
+
resolve();
|
|
96
|
+
}, timeoutMs);
|
|
97
|
+
});
|
|
91
98
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
while (pending && !timedOut) {
|
|
100
|
+
await Promise.race([pending, deadline]);
|
|
101
|
+
const next = pendingCaptures.get(resourceId);
|
|
102
|
+
pending = next === pending ? void 0 : next;
|
|
103
|
+
}
|
|
95
104
|
} finally {
|
|
96
105
|
clearTimeout(timer);
|
|
97
106
|
}
|
|
@@ -136,7 +145,17 @@ function observeSessionFilesystem(session, dependencies) {
|
|
|
136
145
|
fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));
|
|
137
146
|
return;
|
|
138
147
|
}
|
|
139
|
-
|
|
148
|
+
let threadId;
|
|
149
|
+
try {
|
|
150
|
+
threadId = session.thread.requireId();
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const ids = {
|
|
155
|
+
resourceId,
|
|
156
|
+
threadId
|
|
157
|
+
};
|
|
158
|
+
const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() => captureSessionFilesystem(session, dependencies, ids));
|
|
140
159
|
pendingCaptures.set(resourceId, chain);
|
|
141
160
|
chain.finally(() => {
|
|
142
161
|
if (pendingCaptures.get(resourceId) === chain) pendingCaptures.delete(resourceId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { AgentControllerEvent, SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport { peekSessionSandbox } from '../sandbox/session-sandbox.js';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { isMeaningfulToolName } from './first-exec-capture.js';\n\nconst GIT_CHANGED_FILES_SCRIPT = `\nset -e\nworkdir=$1\nbase_branch=$2\nbase=HEAD\nif merge_base=$(git -C \"$workdir\" merge-base HEAD \"origin/$base_branch\" 2>/dev/null); then\n base=$merge_base\nfi\ngit -C \"$workdir\" diff --name-only -z --find-renames --diff-filter=ACMRTUXB \"$base\"\ngit -C \"$workdir\" ls-files --others --exclude-standard -z\n`;\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 subscribe(listener: (event: AgentControllerEvent) => void): () => 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 for (const record of output.split('\\0')) {\n if (!record) continue;\n const path = record.replace(/^\\.\\//, '');\n if (path) 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 || !sandbox?.executeCommand) return;\n // The live workdir comes from the per-process memo (the deterministic\n // truth) ONLY — never the persisted observability column, which a row\n // written under a previous provider can point at a workdir that no\n // longer exists (the stale-workdir incident class). No memo entry means\n // no live sandbox worth capturing in this replica; capture is\n // best-effort telemetry, so skip.\n const entry = peekSessionSandbox(sourceSession.id);\n if (!entry) return;\n // Telemetry must never provision a VM: executeCommand lazily starts the\n // sandbox via ensureRunning, so a chat turn that never touched the\n // workspace would otherwise boot (and clone into) a fresh VM just to\n // capture an empty git status. Only capture when the turn already has a\n // running sandbox.\n if (entry.sandbox.status !== 'running') return;\n // Running-but-unresolved should not happen (the start hook resolves the\n // workdir), but capture is best-effort — skip rather than guess.\n const workdir = entry.workdir;\n if (!workdir) return;\n\n const result = await sandbox.executeCommand(\n 'sh',\n ['-c', GIT_CHANGED_FILES_SCRIPT, 'mastracode-changed-files', workdir, sourceSession.baseBranch],\n { timeout: 30_000 },\n );\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git changes.', result.stderr);\n return;\n }\n\n const artifacts = await sandbox.executeCommand('sh', ['-c', ARTIFACTS_LIST_COMMAND, 'sh', workdir], {\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\n/**\n * In-flight capture chains keyed by session resource id. Serves two purposes:\n * readers of the persisted file listing (the /web/workspace routes) await the\n * capture for the turn that just ended instead of racing it, and captures for\n * the same resource stay sequential (last write wins) even when multiple\n * scoped sessions observe the same resource id. In-process only: the factory\n * server that runs the session controller also serves those routes.\n */\nconst pendingCaptures = new Map<string, Promise<void>>();\n\n/**\n * Resolve once the session's in-flight filesystem capture (if any) has\n * persisted, or after `timeoutMs` so a reader that arrives mid-capture is\n * never blocked by the execs' worst-case 30s timeouts. Never rejects: the\n * capture body is fully try/catch contained.\n */\nexport async function waitForPendingFilesystemCapture(resourceId: string, timeoutMs = 10_000): Promise<void> {\n const pending = pendingCaptures.get(resourceId);\n if (!pending) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n pending,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, timeoutMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let fallbackChain = Promise.resolve();\n // Capture only runs after a turn that actually touched the workspace: a\n // successful workspace tool call both proves the sandbox is awake and is\n // the only way the listing can have changed. Without this gate every chat\n // turn would run two sandbox execs — and since executeCommand resumes an\n // idle VM, a pure-chat turn would keep waking a sandbox it never used.\n // Same bookkeeping as first-exec capture: tool names resolve at tool_end\n // via the tool_start map, and suspended calls survive agent_end so a\n // resumed run's eventual completion still counts for that turn.\n let workspaceTouched = false;\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribeEvents = session.subscribe(event => {\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError || event.denied) return;\n if (isMeaningfulToolName(toolName)) workspaceTouched = true;\n return;\n }\n }\n });\n const unsubscribeEnd = session.onBeforeAgentEnd(() => {\n // onBeforeAgentEnd fires before agent_end is emitted, so every tool_end\n // of the finishing run has already been observed; read then reset.\n if (!workspaceTouched) return;\n workspaceTouched = false;\n // Chain so captures stay sequential (last write wins), but do NOT return\n // the chain: finishAgentRun awaits every listener before emitting\n // agent_end, and the capture's sandbox execs (git status + artifacts\n // find, 30s timeouts each) must not gate turn completion. Readers that\n // need the fresh listing await the pending capture through\n // waitForPendingFilesystemCapture instead. The un-returned chain cannot\n // leak an unhandled rejection: captureSessionFilesystem's entire body\n // runs inside its own try/catch, and disposal of this listener does not\n // cancel an in-flight chain, so a final turn's capture still persists.\n let resourceId: string | undefined;\n try {\n resourceId = session.identity.getResourceId();\n } catch {\n // No resource id means no route can address this session's listing;\n // keep a per-observer chain so captures still run sequentially.\n fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));\n return;\n }\n // Extend the resource-level chain rather than a per-session one: sessions\n // are deduplicated by (resourceId, scope), so multiple scoped sessions can\n // observe the same resource, and their captures must not interleave.\n const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() =>\n captureSessionFilesystem(session, dependencies),\n );\n pendingCaptures.set(resourceId, chain);\n void chain.finally(() => {\n if (pendingCaptures.get(resourceId) === chain) {\n pendingCaptures.delete(resourceId);\n }\n });\n });\n return () => {\n unsubscribeEvents();\n unsubscribeEnd();\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { AgentControllerEvent, SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport { peekSessionSandbox } from '../sandbox/session-sandbox.js';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { isMeaningfulToolName } from './first-exec-capture.js';\n\nconst GIT_CHANGED_FILES_SCRIPT = `\nset -e\nworkdir=$1\nbase_branch=$2\nbase=HEAD\nif merge_base=$(git -C \"$workdir\" merge-base HEAD \"origin/$base_branch\" 2>/dev/null); then\n base=$merge_base\nfi\ngit -C \"$workdir\" diff --name-only -z --find-renames --diff-filter=ACMRTUXB \"$base\"\ngit -C \"$workdir\" ls-files --others --exclude-standard -z\n`;\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 subscribe(listener: (event: AgentControllerEvent) => void): () => 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 for (const record of output.split('\\0')) {\n if (!record) continue;\n const path = record.replace(/^\\.\\//, '');\n if (path) 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 ids?: { resourceId: string; threadId: string },\n): Promise<void> {\n try {\n // Scheduled captures pass ids read at schedule time: a session deleted\n // before the queued capture runs has already cleared its thread.\n const resourceId = ids?.resourceId ?? session.identity.getResourceId();\n const threadId = ids?.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 || !sandbox?.executeCommand) return;\n // The live workdir comes from the per-process memo (the deterministic\n // truth) ONLY — never the persisted observability column, which a row\n // written under a previous provider can point at a workdir that no\n // longer exists (the stale-workdir incident class). No memo entry means\n // no live sandbox worth capturing in this replica; capture is\n // best-effort telemetry, so skip.\n const entry = peekSessionSandbox(sourceSession.id);\n if (!entry) return;\n // Telemetry must never provision a VM: executeCommand lazily starts the\n // sandbox via ensureRunning, so a chat turn that never touched the\n // workspace would otherwise boot (and clone into) a fresh VM just to\n // capture an empty git status. Only capture when the turn already has a\n // running sandbox.\n if (entry.sandbox.status !== 'running') return;\n // Running-but-unresolved should not happen (the start hook resolves the\n // workdir), but capture is best-effort — skip rather than guess.\n const workdir = entry.workdir;\n if (!workdir) return;\n\n const result = await sandbox.executeCommand(\n 'sh',\n ['-c', GIT_CHANGED_FILES_SCRIPT, 'mastracode-changed-files', workdir, sourceSession.baseBranch],\n { timeout: 30_000 },\n );\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git changes.', result.stderr);\n return;\n }\n\n const artifacts = await sandbox.executeCommand('sh', ['-c', ARTIFACTS_LIST_COMMAND, 'sh', workdir], {\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\n/**\n * In-flight capture chains keyed by session resource id. Serves two purposes:\n * readers of the persisted file listing (the /web/workspace routes) await the\n * capture for the turn that just ended instead of racing it, and captures for\n * the same resource stay sequential (last write wins) even when multiple\n * scoped sessions observe the same resource id. In-process only: the factory\n * server that runs the session controller also serves those routes.\n */\nconst pendingCaptures = new Map<string, Promise<void>>();\n\n/**\n * Resolve once the session's in-flight filesystem capture (if any) has\n * persisted, or after `timeoutMs` so a reader that arrives mid-capture is\n * never blocked by the execs' worst-case 30s timeouts. Never rejects: the\n * capture body is fully try/catch contained.\n */\nexport async function waitForPendingFilesystemCapture(resourceId: string, timeoutMs = 10_000): Promise<void> {\n let pending = pendingCaptures.get(resourceId);\n if (!pending) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const deadline = new Promise<void>(resolve => {\n timer = setTimeout(() => {\n timedOut = true;\n resolve();\n }, timeoutMs);\n });\n try {\n // A turn that ends while we wait chains a successor capture; keep waiting\n // until the latest chain settles so the final snapshot is not lost.\n while (pending && !timedOut) {\n await Promise.race([pending, deadline]);\n const next = pendingCaptures.get(resourceId);\n pending = next === pending ? undefined : next;\n }\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let fallbackChain = Promise.resolve();\n // Capture only runs after a turn that actually touched the workspace: a\n // successful workspace tool call both proves the sandbox is awake and is\n // the only way the listing can have changed. Without this gate every chat\n // turn would run two sandbox execs — and since executeCommand resumes an\n // idle VM, a pure-chat turn would keep waking a sandbox it never used.\n // Same bookkeeping as first-exec capture: tool names resolve at tool_end\n // via the tool_start map, and suspended calls survive agent_end so a\n // resumed run's eventual completion still counts for that turn.\n let workspaceTouched = false;\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribeEvents = session.subscribe(event => {\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError || event.denied) return;\n if (isMeaningfulToolName(toolName)) workspaceTouched = true;\n return;\n }\n }\n });\n const unsubscribeEnd = session.onBeforeAgentEnd(() => {\n // onBeforeAgentEnd fires before agent_end is emitted, so every tool_end\n // of the finishing run has already been observed; read then reset.\n if (!workspaceTouched) return;\n workspaceTouched = false;\n // Chain so captures stay sequential (last write wins), but do NOT return\n // the chain: finishAgentRun awaits every listener before emitting\n // agent_end, and the capture's sandbox execs (git status + artifacts\n // find, 30s timeouts each) must not gate turn completion. Readers that\n // need the fresh listing await the pending capture through\n // waitForPendingFilesystemCapture instead. The un-returned chain cannot\n // leak an unhandled rejection: captureSessionFilesystem's entire body\n // runs inside its own try/catch, and disposal of this listener does not\n // cancel an in-flight chain, so a final turn's capture still persists.\n let resourceId: string | undefined;\n try {\n resourceId = session.identity.getResourceId();\n } catch {\n // No resource id means no route can address this session's listing;\n // keep a per-observer chain so captures still run sequentially.\n fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));\n return;\n }\n // Extend the resource-level chain rather than a per-session one: sessions\n // are deduplicated by (resourceId, scope), so multiple scoped sessions can\n // observe the same resource, and their captures must not interleave.\n let threadId: string;\n try {\n threadId = session.thread.requireId();\n } catch {\n return;\n }\n const ids = { resourceId, threadId };\n const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() =>\n captureSessionFilesystem(session, dependencies, ids),\n );\n pendingCaptures.set(resourceId, chain);\n void chain.finally(() => {\n if (pendingCaptures.get(resourceId) === chain) {\n pendingCaptures.delete(resourceId);\n }\n });\n });\n return () => {\n unsubscribeEvents();\n unsubscribeEnd();\n };\n}\n"],"mappings":"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"}
|
|
@@ -45,6 +45,28 @@ export interface OMConfigurableSession {
|
|
|
45
45
|
* the web session boot seed.
|
|
46
46
|
*/
|
|
47
47
|
export declare function applyStoredMemorySettings(session: OMConfigurableSession, record: MemorySettingsRecord | null, fallbackOmModelId?: string): Promise<void>;
|
|
48
|
+
export interface PersonalMemorySettingsArgs {
|
|
49
|
+
/** Without the domain there is no personal preference to read. */
|
|
50
|
+
memorySettings: Pick<MemorySettingsStorage, 'get'> | undefined;
|
|
51
|
+
orgId: string;
|
|
52
|
+
/** The human whose settings apply — for a channel session, the linked sender. */
|
|
53
|
+
userId: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Layer a user's own memory-settings row over whatever the session already runs
|
|
57
|
+
* with, so a personal preference beats the one the project path resolved.
|
|
58
|
+
*
|
|
59
|
+
* The row wins only for the knobs the user has actually saved: an unsaved knob
|
|
60
|
+
* keeps the value already on the session (the project's row, or the
|
|
61
|
+
* provider-aware fallback resolved from the factory default model). That
|
|
62
|
+
* distinction matters because `applyStoredMemorySettings` treats a null knob as
|
|
63
|
+
* "reset to the built-in default", and switching observation onto an
|
|
64
|
+
* uncredentialed provider would fail every observe cycle.
|
|
65
|
+
*
|
|
66
|
+
* Best-effort: a missing row, an uninitialized domain, or a read failure all
|
|
67
|
+
* mean "no personal preference" and leave the session exactly as it was.
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyPersonalMemorySettings(session: OMConfigurableSession, { memorySettings, orgId, userId }: PersonalMemorySettingsArgs): Promise<void>;
|
|
48
70
|
export interface MemorySettingsHydrationSession extends OMConfigurableSession {
|
|
49
71
|
readonly identity: {
|
|
50
72
|
getResourceId(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-settings-hydration.d.ts","sourceRoot":"","sources":["../../src/session/memory-settings-hydration.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,yDAAyD;AACzD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,2DAA2D;AAC3D,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,UAAU,aAAa;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,mBAAmB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,WAAW,CAAA;KAAE,CAAC;IACtD,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC/C,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACvD,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,MAAM,WAAW,mCAAmC;IAClD,4FAA4F;IAC5F,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,EACvC,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,mCAAmC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAyCf"}
|
|
1
|
+
{"version":3,"file":"memory-settings-hydration.d.ts","sourceRoot":"","sources":["../../src/session/memory-settings-hydration.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,4CAA4C,CAAC;AACpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,yDAAyD;AACzD,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD,2DAA2D;AAC3D,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D;AAED;;;;GAIG;AACH,UAAU,aAAa;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,mBAAmB,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mFAAmF;AACnF,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,WAAW,CAAA;KAAE,CAAC;IACtD,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC/C,GAAG,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KACvD,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,GAAG,SAAS,CAAC;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,qBAAqB,EAC9B,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,0BAA0B,GAC5D,OAAO,CAAC,IAAI,CAAC,CAiBf;AAUD,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;CAChD;AAED,MAAM,WAAW,mCAAmC;IAClD,4FAA4F;IAC5F,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,EACvC,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,mCAAmC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAyCf"}
|
|
@@ -31,6 +31,47 @@ async function applyStoredMemorySettings(session, record, fallbackOmModelId) {
|
|
|
31
31
|
if (Object.keys(updates).length > 0) await session.state.set(updates);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
+
* Layer a user's own memory-settings row over whatever the session already runs
|
|
35
|
+
* with, so a personal preference beats the one the project path resolved.
|
|
36
|
+
*
|
|
37
|
+
* The row wins only for the knobs the user has actually saved: an unsaved knob
|
|
38
|
+
* keeps the value already on the session (the project's row, or the
|
|
39
|
+
* provider-aware fallback resolved from the factory default model). That
|
|
40
|
+
* distinction matters because `applyStoredMemorySettings` treats a null knob as
|
|
41
|
+
* "reset to the built-in default", and switching observation onto an
|
|
42
|
+
* uncredentialed provider would fail every observe cycle.
|
|
43
|
+
*
|
|
44
|
+
* Best-effort: a missing row, an uninitialized domain, or a read failure all
|
|
45
|
+
* mean "no personal preference" and leave the session exactly as it was.
|
|
46
|
+
*/
|
|
47
|
+
async function applyPersonalMemorySettings(session, { memorySettings, orgId, userId }) {
|
|
48
|
+
if (!memorySettings) return;
|
|
49
|
+
try {
|
|
50
|
+
const record = await memorySettings.get({
|
|
51
|
+
orgId,
|
|
52
|
+
userId
|
|
53
|
+
});
|
|
54
|
+
if (!record) return;
|
|
55
|
+
const state = session.state.get() ?? {};
|
|
56
|
+
await applyStoredMemorySettings(session, {
|
|
57
|
+
...record,
|
|
58
|
+
observerModelId: record.observerModelId ?? session.om.observer.modelId() ?? null,
|
|
59
|
+
reflectorModelId: record.reflectorModelId ?? session.om.reflector.modelId() ?? null,
|
|
60
|
+
observationThreshold: record.observationThreshold ?? storedNumber(state.observationThreshold),
|
|
61
|
+
reflectionThreshold: record.reflectionThreshold ?? storedNumber(state.reflectionThreshold),
|
|
62
|
+
observeAttachments: record.observeAttachments ?? storedObserveAttachments(state.observeAttachments)
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn("[Factory memory-settings hydration] Unable to apply the user's memory settings.", error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function storedNumber(value) {
|
|
69
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
70
|
+
}
|
|
71
|
+
function storedObserveAttachments(value) {
|
|
72
|
+
return value === "auto" || typeof value === "boolean" ? value : null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
34
75
|
* Seed a freshly created controller session's tenant org and its
|
|
35
76
|
* observational-memory settings from the owner's source-control row. Registered
|
|
36
77
|
* as a blocking session-created listener so the seed lands before the caller can
|
|
@@ -83,6 +124,6 @@ async function hydrateSessionMemorySettings(session, { sourceControl, projects,
|
|
|
83
124
|
}
|
|
84
125
|
}
|
|
85
126
|
//#endregion
|
|
86
|
-
export { DEFAULT_OBSERVATION_THRESHOLD, DEFAULT_REFLECTION_THRESHOLD, applyStoredMemorySettings, hydrateSessionMemorySettings };
|
|
127
|
+
export { DEFAULT_OBSERVATION_THRESHOLD, DEFAULT_REFLECTION_THRESHOLD, applyPersonalMemorySettings, applyStoredMemorySettings, hydrateSessionMemorySettings };
|
|
87
128
|
|
|
88
129
|
//# sourceMappingURL=memory-settings-hydration.js.map
|
|
@@ -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';\nimport { resolveProviderOMDefault } from '@mastra/code-sdk/onboarding/packs';\n\nimport {\n factoryMemorySettingsUserId,\n type MemorySettingsRecord,\n type MemorySettingsStorage,\n} 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';\nimport { 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 projects: Pick<FactoryProjectsStorage, 'get'>;\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 curation scopes\n * every node and record on `factoryOrgId`; before the SDK refusal guard,\n * missing it made curation substitute the session owner id. For web chat sessions\n * that is the agent controller's own id rather than a tenant, so curated\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 * Tagged sessions may be coordinator-owned runs or web sessions that only carry\n * browser-seeded Factory state. This path resolves their source-control row and\n * reapplies a stored project settings row when one exists. If no project row\n * exists, it preserves the coordinator's provider-aware fallback.\n * Sessions without a GitHub source-control row (e.g. chat-only channel sessions)\n * hydrate through `hydrateFactorySession` with their own resolved tenant.\n * Best-effort: failures are logged, never thrown.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl, projects, memorySettings }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\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 curation 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 const factoryProjectId = isFactoryRun ? String(state.factoryProjectId) : undefined;\n const settings = await memorySettings.get({\n orgId: record.orgId,\n userId: factoryProjectId ? factoryMemorySettingsUserId(factoryProjectId) : record.userId,\n });\n // Coordinator hydration applies a provider-aware fallback when no project row\n // exists. Do not replace that fallback with the generic OM default here.\n if (factoryProjectId && !settings) return;\n const project = factoryProjectId ? await projects.get({ orgId: record.orgId, id: factoryProjectId }) : null;\n const provider = project?.defaultModelId?.split('/')[0];\n const fallbackOmModelId = provider\n ? resolveProviderOMDefault(provider, project.defaultModelId ?? undefined).modelId\n : undefined;\n await applyStoredMemorySettings(\n session,\n factoryProjectId && settings && !fallbackOmModelId\n ? {\n ...settings,\n observerModelId: settings?.observerModelId ?? session.om.observer.modelId() ?? null,\n reflectorModelId: settings?.reflectorModelId ?? session.om.reflector.modelId() ?? null,\n }\n : settings,\n fallbackOmModelId,\n );\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":";;;;;;AAaA,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;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,eAAsB,6BACpB,SACA,EAAE,eAAe,UAAU,kBACZ;CACf,MAAM,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC;CACtC,MAAM,eAAe,QAAQ,MAAM,gBAAgB;CACnD,IAAI;EACF,MAAM,SAAS,MAAM,cAAc,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC;EAK3F,MAAM,eAAe,SAAS,QAAQ,KAAK;EAC3C,IAAI,CAAC,QAAQ;EACb,MAAM,mBAAmB,eAAe,OAAO,MAAM,gBAAgB,IAAI,KAAA;EACzE,MAAM,WAAW,MAAM,eAAe,IAAI;GACxC,OAAO,OAAO;GACd,QAAQ,mBAAmB,4BAA4B,gBAAgB,IAAI,OAAO;EACpF,CAAC;EAGD,IAAI,oBAAoB,CAAC,UAAU;EACnC,MAAM,UAAU,mBAAmB,MAAM,SAAS,IAAI;GAAE,OAAO,OAAO;GAAO,IAAI;EAAiB,CAAC,IAAI;EACvG,MAAM,WAAW,SAAS,gBAAgB,MAAM,GAAG,CAAC,CAAC;EACrD,MAAM,oBAAoB,WACtB,yBAAyB,UAAU,QAAQ,kBAAkB,KAAA,CAAS,CAAC,CAAC,UACxE,KAAA;EACJ,MAAM,0BACJ,SACA,oBAAoB,YAAY,CAAC,oBAC7B;GACE,GAAG;GACH,iBAAiB,UAAU,mBAAmB,QAAQ,GAAG,SAAS,QAAQ,KAAK;GAC/E,kBAAkB,UAAU,oBAAoB,QAAQ,GAAG,UAAU,QAAQ,KAAK;EACpF,IACA,UACJ,iBACF;CACF,SAAS,OAAO;EACd,QAAQ,KAAK,+EAA+E,KAAK;EAGjG,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,EAAE,cAAc,MAAM,eAAe,SAAS,KAAA,CAAS;CACjF;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';\nimport { resolveProviderOMDefault } from '@mastra/code-sdk/onboarding/packs';\n\nimport {\n factoryMemorySettingsUserId,\n type MemorySettingsRecord,\n type MemorySettingsStorage,\n} 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';\nimport { 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 PersonalMemorySettingsArgs {\n /** Without the domain there is no personal preference to read. */\n memorySettings: Pick<MemorySettingsStorage, 'get'> | undefined;\n orgId: string;\n /** The human whose settings apply — for a channel session, the linked sender. */\n userId: string;\n}\n\n/**\n * Layer a user's own memory-settings row over whatever the session already runs\n * with, so a personal preference beats the one the project path resolved.\n *\n * The row wins only for the knobs the user has actually saved: an unsaved knob\n * keeps the value already on the session (the project's row, or the\n * provider-aware fallback resolved from the factory default model). That\n * distinction matters because `applyStoredMemorySettings` treats a null knob as\n * \"reset to the built-in default\", and switching observation onto an\n * uncredentialed provider would fail every observe cycle.\n *\n * Best-effort: a missing row, an uninitialized domain, or a read failure all\n * mean \"no personal preference\" and leave the session exactly as it was.\n */\nexport async function applyPersonalMemorySettings(\n session: OMConfigurableSession,\n { memorySettings, orgId, userId }: PersonalMemorySettingsArgs,\n): Promise<void> {\n if (!memorySettings) return;\n try {\n const record = await memorySettings.get({ orgId, userId });\n if (!record) return;\n const state = session.state.get() ?? {};\n await applyStoredMemorySettings(session, {\n ...record,\n observerModelId: record.observerModelId ?? session.om.observer.modelId() ?? null,\n reflectorModelId: record.reflectorModelId ?? session.om.reflector.modelId() ?? null,\n observationThreshold: record.observationThreshold ?? storedNumber(state.observationThreshold),\n reflectionThreshold: record.reflectionThreshold ?? storedNumber(state.reflectionThreshold),\n observeAttachments: record.observeAttachments ?? storedObserveAttachments(state.observeAttachments),\n });\n } catch (error) {\n console.warn(\"[Factory memory-settings hydration] Unable to apply the user's memory settings.\", error);\n }\n}\n\nfunction storedNumber(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction storedObserveAttachments(value: unknown): 'auto' | boolean | null {\n return value === 'auto' || typeof value === 'boolean' ? value : null;\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 projects: Pick<FactoryProjectsStorage, 'get'>;\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 curation scopes\n * every node and record on `factoryOrgId`; before the SDK refusal guard,\n * missing it made curation substitute the session owner id. For web chat sessions\n * that is the agent controller's own id rather than a tenant, so curated\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 * Tagged sessions may be coordinator-owned runs or web sessions that only carry\n * browser-seeded Factory state. This path resolves their source-control row and\n * reapplies a stored project settings row when one exists. If no project row\n * exists, it preserves the coordinator's provider-aware fallback.\n * Sessions without a GitHub source-control row (e.g. chat-only channel sessions)\n * hydrate through `hydrateFactorySession` with their own resolved tenant.\n * Best-effort: failures are logged, never thrown.\n */\nexport async function hydrateSessionMemorySettings(\n session: MemorySettingsHydrationSession,\n { sourceControl, projects, memorySettings }: MemorySettingsHydrationDependencies,\n): Promise<void> {\n const state = session.state.get() ?? {};\n const isFactoryRun = Boolean(state.factoryProjectId);\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 curation 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 const factoryProjectId = isFactoryRun ? String(state.factoryProjectId) : undefined;\n const settings = await memorySettings.get({\n orgId: record.orgId,\n userId: factoryProjectId ? factoryMemorySettingsUserId(factoryProjectId) : record.userId,\n });\n // Coordinator hydration applies a provider-aware fallback when no project row\n // exists. Do not replace that fallback with the generic OM default here.\n if (factoryProjectId && !settings) return;\n const project = factoryProjectId ? await projects.get({ orgId: record.orgId, id: factoryProjectId }) : null;\n const provider = project?.defaultModelId?.split('/')[0];\n const fallbackOmModelId = provider\n ? resolveProviderOMDefault(provider, project.defaultModelId ?? undefined).modelId\n : undefined;\n await applyStoredMemorySettings(\n session,\n factoryProjectId && settings && !fallbackOmModelId\n ? {\n ...settings,\n observerModelId: settings?.observerModelId ?? session.om.observer.modelId() ?? null,\n reflectorModelId: settings?.reflectorModelId ?? session.om.reflector.modelId() ?? null,\n }\n : settings,\n fallbackOmModelId,\n );\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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/comments/routes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAiB,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAOpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,aAAa,CAAC;AAWjE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,uBAAuB,CAAC;IAClC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,EAAE,sBAAsB,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAWD,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CAqSrF"}
|
|
@@ -5,8 +5,14 @@ import { decodeCommentCursor } from "./base.js";
|
|
|
5
5
|
import { UUID_RE, isRecord, parseCreateCommentBody, parseEditCommentBody, readJson } from "./parse.js";
|
|
6
6
|
import { toWireComment } from "./wire.js";
|
|
7
7
|
import { registerApiRoute } from "@mastra/core/server";
|
|
8
|
+
import { withAck } from "@mastra/core/events";
|
|
8
9
|
import { streamSSE } from "hono/streaming";
|
|
9
10
|
//#region src/storage/domains/comments/routes.ts
|
|
11
|
+
/**
|
|
12
|
+
* HTTP for the item feed. Every handler is the same shape: resolve the tenant,
|
|
13
|
+
* validate the path ids, delegate to a `CommentsDomain` service method, map its
|
|
14
|
+
* result status to a status code, then emit audit.
|
|
15
|
+
*/
|
|
10
16
|
const MAX_AUDIT_BODY_SNAPSHOT = 1024;
|
|
11
17
|
/** Comment frames only — proxies drop an idle stream, and `write` can't detect a dead peer. */
|
|
12
18
|
const FEED_KEEPALIVE_MS = 25e3;
|
|
@@ -259,15 +265,17 @@ function buildCommentRoutes(dependencies) {
|
|
|
259
265
|
})) return c.json({ error: "Project not found" }, 404);
|
|
260
266
|
const topic = feedTopic(tenant.orgId, projectId);
|
|
261
267
|
return streamSSE(c, async (stream) => {
|
|
262
|
-
const onEvent = async (event) => {
|
|
268
|
+
const onEvent = withAck(async (event) => {
|
|
263
269
|
if (stream.aborted) return;
|
|
264
270
|
const data = event.data;
|
|
265
271
|
const workItemId = isRecord(data) && typeof data.workItemId === "string" ? data.workItemId : void 0;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
try {
|
|
273
|
+
await stream.writeSSE({
|
|
274
|
+
event: "feed",
|
|
275
|
+
data: JSON.stringify(workItemId ? { workItemId } : {})
|
|
276
|
+
});
|
|
277
|
+
} catch {}
|
|
278
|
+
});
|
|
271
279
|
const closed = new Promise((resolve) => stream.onAbort(resolve));
|
|
272
280
|
let keepalive;
|
|
273
281
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","names":[],"sources":["../../../../src/storage/domains/comments/routes.ts"],"sourcesContent":["/**\n * HTTP for the item feed. Every handler is the same shape: resolve the tenant,\n * validate the path ids, delegate to a `CommentsDomain` service method, map its\n * result status to a status code, then emit audit.\n */\n\nimport type { EventCallback, PubSub } from '@mastra/core/events';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport { getFactoryAuthUser } from '../../../auth.js';\nimport { feedTopic } from '../../../feed-events.js';\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { AuditEmitter } from '../audit/domain.js';\nimport type { FactoryProjectsStorage } from '../projects/base.js';\nimport type { WorkItemsStorage } from '../work-items/base.js';\nimport { actorFromAuthUser } from './actor.js';\nimport type { WorkItemCommentsStorage } from './base.js';\nimport { decodeCommentCursor } from './base.js';\nimport type { CommentEditor, CommentsDomain } from './domain.js';\nimport { isRecord, parseCreateCommentBody, parseEditCommentBody, readJson, UUID_RE } from './parse.js';\nimport { toWireComment } from './wire.js';\nimport type { WireCommentPage } from './wire.js';\n\nconst MAX_AUDIT_BODY_SNAPSHOT = 1024;\n/** Comment frames only — proxies drop an idle stream, and `write` can't detect a dead peer. */\nconst FEED_KEEPALIVE_MS = 25_000;\n// Ceiling: one broker subscription per open tab, not per project. A per-replica\n// topic multiplexer is the upgrade path if tab counts ever make that hurt.\n\nexport interface CommentRouteDependencies {\n domain: CommentsDomain;\n auth: RouteAuth;\n comments: WorkItemCommentsStorage;\n workItems: WorkItemsStorage;\n projects: FactoryProjectsStorage;\n pubsub: PubSub;\n audit?: AuditEmitter;\n}\n\ninterface Tenant {\n orgId: string;\n userId: string;\n}\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nexport function buildCommentRoutes(dependencies: CommentRouteDependencies): ApiRoute[] {\n const { domain, auth, comments, workItems, projects, pubsub, audit } = dependencies;\n\n async function resolveTenant(c: Context): Promise<Tenant | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json({ error: 'organization_required', message: 'The item feed requires an organization.' }, 403),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n function editorFor(c: Context, tenant: Tenant): CommentEditor {\n return {\n userId: tenant.userId,\n canModerate: () => auth.isOrganizationAdmin(c, tenant.orgId),\n };\n }\n\n async function emitMentionAudit(\n c: Context,\n target: { factoryProjectId: string; workItemId: string; title?: string },\n commentId: string,\n mentionedIds: string[],\n ): Promise<void> {\n if (mentionedIds.length === 0) return;\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_mentioned',\n factoryProjectId: target.factoryProjectId,\n targets: [{ type: 'work_item', id: target.workItemId, ...(target.title ? { name: target.title } : {}) }],\n metadata: { commentId, mentionedIds },\n },\n });\n }\n\n return [\n registerApiRoute('/web/factory/work-items/:workItemId/comments', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: 'Work item not found' }, 404);\n await workItems.ensureReady();\n const workItem = await workItems.get({ orgId: tenant.orgId, id: workItemId });\n if (!workItem) return c.json({ error: 'Work item not found' }, 404);\n\n await comments.ensureReady();\n const limitRaw = c.req.query('limit');\n const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;\n const before = c.req.query('before') || undefined;\n if (before && !decodeCommentCursor(before)) return c.json({ error: 'invalid_cursor' }, 422);\n const around = c.req.query('around') || undefined;\n if (around && !UUID_RE.test(around)) return c.json({ error: 'invalid_comment_id' }, 422);\n const page = await comments.list({\n orgId: tenant.orgId,\n factoryProjectId: workItem.factoryProjectId,\n workItemId,\n before,\n ...(around ? { around } : {}),\n ...(limit !== undefined && Number.isFinite(limit) ? { limit } : {}),\n });\n const wirePage: WireCommentPage = {\n comments: page.comments.map(comment => toWireComment(comment, tenant.userId)),\n ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),\n };\n return c.json(wirePage);\n },\n }),\n\n registerApiRoute('/web/factory/work-items/:workItemId/comments', {\n method: 'POST',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: 'Work item not found' }, 404);\n\n const parsed = parseCreateCommentBody(await readJson(c));\n if (!parsed) return c.json({ error: 'invalid_comment' }, 422);\n\n const result = await domain.createComment({\n orgId: tenant.orgId,\n workItemId,\n author: actorFromAuthUser(tenant.userId, getFactoryAuthUser(c)),\n body: parsed.body,\n ...(parsed.replyTo ? { replyTo: parsed.replyTo } : {}),\n ...(parsed.mentions ? { mentions: parsed.mentions } : {}),\n ...(parsed.clientToken ? { clientToken: parsed.clientToken } : {}),\n });\n if (result.status === 'work_item_not_found') return c.json({ error: 'Work item not found' }, 404);\n if (result.status === 'token_conflict') return c.json({ error: 'comment_token_conflict' }, 409);\n if (result.status === 'invalid') return c.json({ error: 'invalid_comment', message: result.message }, 422);\n\n const { comment, workItem } = result;\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_created',\n factoryProjectId: workItem.factoryProjectId,\n targets: [{ type: 'work_item', id: workItem.id, name: workItem.title }],\n metadata: { commentId: comment.id },\n },\n });\n await emitMentionAudit(\n c,\n { factoryProjectId: workItem.factoryProjectId, workItemId: workItem.id, title: workItem.title },\n comment.id,\n comment.mentions.map(mention => mention.id).filter(id => id !== comment.author.id),\n );\n return c.json({ comment: toWireComment(comment, tenant.userId) }, 201);\n },\n }),\n\n registerApiRoute('/web/factory/work-items/:workItemId/comments/:commentId', {\n method: 'PATCH',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n const commentId = c.req.param('commentId');\n if (!workItemId || !UUID_RE.test(workItemId) || !commentId || !UUID_RE.test(commentId)) {\n return c.json({ error: 'Comment not found' }, 404);\n }\n\n const parsed = parseEditCommentBody(await readJson(c));\n if (!parsed) return c.json({ error: 'invalid_comment' }, 422);\n\n const result = await domain.editComment({\n orgId: tenant.orgId,\n workItemId,\n commentId,\n editor: editorFor(c, tenant),\n body: parsed.body,\n ...(parsed.mentions ? { mentions: parsed.mentions } : {}),\n ...(parsed.expectedRevision !== undefined ? { expectedRevision: parsed.expectedRevision } : {}),\n });\n if (result.status === 'not_found') return c.json({ error: 'Comment not found' }, 404);\n if (result.status === 'forbidden') return c.json({ error: 'not_comment_author' }, 403);\n if (result.status === 'not_editable') return c.json({ error: 'comment_not_editable' }, 409);\n if (result.status === 'conflict') return c.json({ error: 'comment_conflict' }, 409);\n if (result.status === 'invalid') return c.json({ error: 'invalid_comment', message: result.message }, 422);\n\n const { comment } = result;\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_edited',\n factoryProjectId: comment.factoryProjectId,\n targets: [{ type: 'work_item', id: comment.workItemId }],\n metadata: {\n commentId: comment.id,\n previousBody: result.previousBody.slice(0, MAX_AUDIT_BODY_SNAPSHOT),\n },\n },\n });\n await emitMentionAudit(\n c,\n { factoryProjectId: comment.factoryProjectId, workItemId: comment.workItemId },\n comment.id,\n result.addedMentions.map(mention => mention.id),\n );\n return c.json({ comment: toWireComment(comment, tenant.userId) });\n },\n }),\n\n registerApiRoute('/web/factory/work-items/:workItemId/comments/:commentId', {\n method: 'DELETE',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n const commentId = c.req.param('commentId');\n if (!workItemId || !UUID_RE.test(workItemId) || !commentId || !UUID_RE.test(commentId)) {\n return c.json({ error: 'Comment not found' }, 404);\n }\n\n const result = await domain.deleteComment({\n orgId: tenant.orgId,\n workItemId,\n commentId,\n editor: editorFor(c, tenant),\n });\n if (result.status === 'not_found') return c.json({ error: 'Comment not found' }, 404);\n if (result.status === 'forbidden') return c.json({ error: 'not_comment_author' }, 403);\n if (result.status === 'not_editable') return c.json({ error: 'comment_not_editable' }, 409);\n\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_deleted',\n factoryProjectId: result.comment.factoryProjectId,\n targets: [{ type: 'work_item', id: result.comment.workItemId }],\n metadata: { commentId: result.comment.id },\n },\n });\n return c.json({ comment: toWireComment(result.comment, tenant.userId) });\n },\n }),\n\n registerApiRoute('/web/factory/projects/:id/mention-roster', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const projectId = c.req.param('id');\n if (!projectId || !UUID_RE.test(projectId)) return c.json({ error: 'Project not found' }, 404);\n await projects.ensureReady();\n const project = await projects.get({ orgId: tenant.orgId, id: projectId });\n if (!project) return c.json({ error: 'Project not found' }, 404);\n\n await comments.ensureReady();\n const roster = await domain.mentionRoster({ orgId: tenant.orgId, factoryProjectId: projectId });\n const query = c.req.query('q')?.trim().toLowerCase();\n const members = query\n ? roster.filter(member => (member.name ?? member.id).toLowerCase().startsWith(query))\n : roster;\n return c.json({ members });\n },\n }),\n\n registerApiRoute('/web/factory/projects/:id/feed-events', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const projectId = c.req.param('id');\n if (!projectId || !UUID_RE.test(projectId)) return c.json({ error: 'Project not found' }, 404);\n await projects.ensureReady();\n const project = await projects.get({ orgId: tenant.orgId, id: projectId });\n if (!project) return c.json({ error: 'Project not found' }, 404);\n\n const topic = feedTopic(tenant.orgId, projectId);\n return streamSSE(c, async stream => {\n const onEvent: EventCallback = async event => {\n if (stream.aborted) return;\n const data = event.data;\n const workItemId = isRecord(data) && typeof data.workItemId === 'string' ? data.workItemId : undefined;\n await stream.writeSSE({ event: 'feed', data: JSON.stringify(workItemId ? { workItemId } : {}) });\n };\n // Claimed before any await: `onAbort` handlers registered after the\n // reader is gone never run, and a broker subscribe is a round trip.\n const closed = new Promise<void>(resolve => stream.onAbort(resolve));\n let keepalive: ReturnType<typeof setInterval> | undefined;\n try {\n // Without `latest`, a retaining broker replays its whole backlog into\n // every new connection as spurious invalidations.\n await pubsub.subscribe(topic, onEvent, { startFrom: 'latest' });\n if (!stream.aborted) {\n keepalive = setInterval(() => {\n if (stream.aborted) return;\n void stream.write(': ping\\n\\n');\n }, FEED_KEEPALIVE_MS);\n // `streamSSE` closes the stream the moment this callback returns.\n await closed;\n }\n } finally {\n clearInterval(keepalive);\n await pubsub.unsubscribe(topic, onEvent);\n }\n });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../../../src/storage/domains/comments/routes.ts"],"sourcesContent":["/**\n * HTTP for the item feed. Every handler is the same shape: resolve the tenant,\n * validate the path ids, delegate to a `CommentsDomain` service method, map its\n * result status to a status code, then emit audit.\n */\n\nimport { withAck } from '@mastra/core/events';\nimport type { EventCallback, PubSub } from '@mastra/core/events';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport { getFactoryAuthUser } from '../../../auth.js';\nimport { feedTopic } from '../../../feed-events.js';\nimport type { RouteAuth } from '../../../routes/route.js';\nimport type { AuditEmitter } from '../audit/domain.js';\nimport type { FactoryProjectsStorage } from '../projects/base.js';\nimport type { WorkItemsStorage } from '../work-items/base.js';\nimport { actorFromAuthUser } from './actor.js';\nimport type { WorkItemCommentsStorage } from './base.js';\nimport { decodeCommentCursor } from './base.js';\nimport type { CommentEditor, CommentsDomain } from './domain.js';\nimport { isRecord, parseCreateCommentBody, parseEditCommentBody, readJson, UUID_RE } from './parse.js';\nimport { toWireComment } from './wire.js';\nimport type { WireCommentPage } from './wire.js';\n\nconst MAX_AUDIT_BODY_SNAPSHOT = 1024;\n/** Comment frames only — proxies drop an idle stream, and `write` can't detect a dead peer. */\nconst FEED_KEEPALIVE_MS = 25_000;\n// Ceiling: one broker subscription per open tab, not per project. A per-replica\n// topic multiplexer is the upgrade path if tab counts ever make that hurt.\n\nexport interface CommentRouteDependencies {\n domain: CommentsDomain;\n auth: RouteAuth;\n comments: WorkItemCommentsStorage;\n workItems: WorkItemsStorage;\n projects: FactoryProjectsStorage;\n pubsub: PubSub;\n audit?: AuditEmitter;\n}\n\ninterface Tenant {\n orgId: string;\n userId: string;\n}\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nexport function buildCommentRoutes(dependencies: CommentRouteDependencies): ApiRoute[] {\n const { domain, auth, comments, workItems, projects, pubsub, audit } = dependencies;\n\n async function resolveTenant(c: Context): Promise<Tenant | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json({ error: 'organization_required', message: 'The item feed requires an organization.' }, 403),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n function editorFor(c: Context, tenant: Tenant): CommentEditor {\n return {\n userId: tenant.userId,\n canModerate: () => auth.isOrganizationAdmin(c, tenant.orgId),\n };\n }\n\n async function emitMentionAudit(\n c: Context,\n target: { factoryProjectId: string; workItemId: string; title?: string },\n commentId: string,\n mentionedIds: string[],\n ): Promise<void> {\n if (mentionedIds.length === 0) return;\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_mentioned',\n factoryProjectId: target.factoryProjectId,\n targets: [{ type: 'work_item', id: target.workItemId, ...(target.title ? { name: target.title } : {}) }],\n metadata: { commentId, mentionedIds },\n },\n });\n }\n\n return [\n registerApiRoute('/web/factory/work-items/:workItemId/comments', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: 'Work item not found' }, 404);\n await workItems.ensureReady();\n const workItem = await workItems.get({ orgId: tenant.orgId, id: workItemId });\n if (!workItem) return c.json({ error: 'Work item not found' }, 404);\n\n await comments.ensureReady();\n const limitRaw = c.req.query('limit');\n const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined;\n const before = c.req.query('before') || undefined;\n if (before && !decodeCommentCursor(before)) return c.json({ error: 'invalid_cursor' }, 422);\n const around = c.req.query('around') || undefined;\n if (around && !UUID_RE.test(around)) return c.json({ error: 'invalid_comment_id' }, 422);\n const page = await comments.list({\n orgId: tenant.orgId,\n factoryProjectId: workItem.factoryProjectId,\n workItemId,\n before,\n ...(around ? { around } : {}),\n ...(limit !== undefined && Number.isFinite(limit) ? { limit } : {}),\n });\n const wirePage: WireCommentPage = {\n comments: page.comments.map(comment => toWireComment(comment, tenant.userId)),\n ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),\n };\n return c.json(wirePage);\n },\n }),\n\n registerApiRoute('/web/factory/work-items/:workItemId/comments', {\n method: 'POST',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: 'Work item not found' }, 404);\n\n const parsed = parseCreateCommentBody(await readJson(c));\n if (!parsed) return c.json({ error: 'invalid_comment' }, 422);\n\n const result = await domain.createComment({\n orgId: tenant.orgId,\n workItemId,\n author: actorFromAuthUser(tenant.userId, getFactoryAuthUser(c)),\n body: parsed.body,\n ...(parsed.replyTo ? { replyTo: parsed.replyTo } : {}),\n ...(parsed.mentions ? { mentions: parsed.mentions } : {}),\n ...(parsed.clientToken ? { clientToken: parsed.clientToken } : {}),\n });\n if (result.status === 'work_item_not_found') return c.json({ error: 'Work item not found' }, 404);\n if (result.status === 'token_conflict') return c.json({ error: 'comment_token_conflict' }, 409);\n if (result.status === 'invalid') return c.json({ error: 'invalid_comment', message: result.message }, 422);\n\n const { comment, workItem } = result;\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_created',\n factoryProjectId: workItem.factoryProjectId,\n targets: [{ type: 'work_item', id: workItem.id, name: workItem.title }],\n metadata: { commentId: comment.id },\n },\n });\n await emitMentionAudit(\n c,\n { factoryProjectId: workItem.factoryProjectId, workItemId: workItem.id, title: workItem.title },\n comment.id,\n comment.mentions.map(mention => mention.id).filter(id => id !== comment.author.id),\n );\n return c.json({ comment: toWireComment(comment, tenant.userId) }, 201);\n },\n }),\n\n registerApiRoute('/web/factory/work-items/:workItemId/comments/:commentId', {\n method: 'PATCH',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n const commentId = c.req.param('commentId');\n if (!workItemId || !UUID_RE.test(workItemId) || !commentId || !UUID_RE.test(commentId)) {\n return c.json({ error: 'Comment not found' }, 404);\n }\n\n const parsed = parseEditCommentBody(await readJson(c));\n if (!parsed) return c.json({ error: 'invalid_comment' }, 422);\n\n const result = await domain.editComment({\n orgId: tenant.orgId,\n workItemId,\n commentId,\n editor: editorFor(c, tenant),\n body: parsed.body,\n ...(parsed.mentions ? { mentions: parsed.mentions } : {}),\n ...(parsed.expectedRevision !== undefined ? { expectedRevision: parsed.expectedRevision } : {}),\n });\n if (result.status === 'not_found') return c.json({ error: 'Comment not found' }, 404);\n if (result.status === 'forbidden') return c.json({ error: 'not_comment_author' }, 403);\n if (result.status === 'not_editable') return c.json({ error: 'comment_not_editable' }, 409);\n if (result.status === 'conflict') return c.json({ error: 'comment_conflict' }, 409);\n if (result.status === 'invalid') return c.json({ error: 'invalid_comment', message: result.message }, 422);\n\n const { comment } = result;\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_edited',\n factoryProjectId: comment.factoryProjectId,\n targets: [{ type: 'work_item', id: comment.workItemId }],\n metadata: {\n commentId: comment.id,\n previousBody: result.previousBody.slice(0, MAX_AUDIT_BODY_SNAPSHOT),\n },\n },\n });\n await emitMentionAudit(\n c,\n { factoryProjectId: comment.factoryProjectId, workItemId: comment.workItemId },\n comment.id,\n result.addedMentions.map(mention => mention.id),\n );\n return c.json({ comment: toWireComment(comment, tenant.userId) });\n },\n }),\n\n registerApiRoute('/web/factory/work-items/:workItemId/comments/:commentId', {\n method: 'DELETE',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const workItemId = c.req.param('workItemId');\n const commentId = c.req.param('commentId');\n if (!workItemId || !UUID_RE.test(workItemId) || !commentId || !UUID_RE.test(commentId)) {\n return c.json({ error: 'Comment not found' }, 404);\n }\n\n const result = await domain.deleteComment({\n orgId: tenant.orgId,\n workItemId,\n commentId,\n editor: editorFor(c, tenant),\n });\n if (result.status === 'not_found') return c.json({ error: 'Comment not found' }, 404);\n if (result.status === 'forbidden') return c.json({ error: 'not_comment_author' }, 403);\n if (result.status === 'not_editable') return c.json({ error: 'comment_not_editable' }, 409);\n\n await audit?.emit({\n context: c,\n input: {\n action: 'factory.work_item.comment_deleted',\n factoryProjectId: result.comment.factoryProjectId,\n targets: [{ type: 'work_item', id: result.comment.workItemId }],\n metadata: { commentId: result.comment.id },\n },\n });\n return c.json({ comment: toWireComment(result.comment, tenant.userId) });\n },\n }),\n\n registerApiRoute('/web/factory/projects/:id/mention-roster', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const projectId = c.req.param('id');\n if (!projectId || !UUID_RE.test(projectId)) return c.json({ error: 'Project not found' }, 404);\n await projects.ensureReady();\n const project = await projects.get({ orgId: tenant.orgId, id: projectId });\n if (!project) return c.json({ error: 'Project not found' }, 404);\n\n await comments.ensureReady();\n const roster = await domain.mentionRoster({ orgId: tenant.orgId, factoryProjectId: projectId });\n const query = c.req.query('q')?.trim().toLowerCase();\n const members = query\n ? roster.filter(member => (member.name ?? member.id).toLowerCase().startsWith(query))\n : roster;\n return c.json({ members });\n },\n }),\n\n registerApiRoute('/web/factory/projects/:id/feed-events', {\n method: 'GET',\n handler: async cc => {\n const c = loose(cc);\n const tenant = await resolveTenant(c);\n if ('response' in tenant) return tenant.response;\n\n const projectId = c.req.param('id');\n if (!projectId || !UUID_RE.test(projectId)) return c.json({ error: 'Project not found' }, 404);\n await projects.ensureReady();\n const project = await projects.get({ orgId: tenant.orgId, id: projectId });\n if (!project) return c.json({ error: 'Project not found' }, 404);\n\n const topic = feedTopic(tenant.orgId, projectId);\n return streamSSE(c, async stream => {\n // `withAck` settles every delivery: on a durable broker (Redis\n // Streams) an unacked event stays in the subscription's pending set\n // and in the broker client's in-flight bookkeeping for the life of\n // this connection — a leak that grows with every feed touch on\n // long-lived dashboard tabs. Feed frames are fire-and-forget\n // invalidations, so a delivery is acked even when the tab is gone.\n const onEvent: EventCallback = withAck(async event => {\n if (stream.aborted) return;\n const data = event.data;\n const workItemId = isRecord(data) && typeof data.workItemId === 'string' ? data.workItemId : undefined;\n try {\n await stream.writeSSE({ event: 'feed', data: JSON.stringify(workItemId ? { workItemId } : {}) });\n } catch {\n // A write to a half-closed socket must not reach `withAck` as a\n // rejection: the nack would republish the event to the shared\n // feed topic, duplicating the invalidation for every tab.\n }\n });\n // Claimed before any await: `onAbort` handlers registered after the\n // reader is gone never run, and a broker subscribe is a round trip.\n const closed = new Promise<void>(resolve => stream.onAbort(resolve));\n let keepalive: ReturnType<typeof setInterval> | undefined;\n try {\n // Without `latest`, a retaining broker replays its whole backlog into\n // every new connection as spurious invalidations.\n await pubsub.subscribe(topic, onEvent, { startFrom: 'latest' });\n if (!stream.aborted) {\n keepalive = setInterval(() => {\n if (stream.aborted) return;\n void stream.write(': ping\\n\\n');\n }, FEED_KEEPALIVE_MS);\n // `streamSSE` closes the stream the moment this callback returns.\n await closed;\n }\n } finally {\n clearInterval(keepalive);\n await pubsub.unsubscribe(topic, onEvent);\n }\n });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
package/dist/workspace.js
CHANGED
|
@@ -9,9 +9,9 @@ import { DEFAULT_COMMAND_TIMEOUT_MS, SetupCommandError, checkoutSessionBranch, m
|
|
|
9
9
|
import { getFactorySessionAddress } from "./rules/binding-context.js";
|
|
10
10
|
import { parseSupervisorResourceId } from "./supervisor/session.js";
|
|
11
11
|
import path, { dirname, join } from "path";
|
|
12
|
+
import { LocalSkillSource, Workspace } from "@mastra/core/workspace";
|
|
12
13
|
import { DEFAULT_CONFIG_DIR } from "@mastra/code-sdk/constants";
|
|
13
14
|
import { SandboxFilesystem } from "@mastra/code-sdk/agents/sandbox-filesystem";
|
|
14
|
-
import { LocalSkillSource, Workspace } from "@mastra/core/workspace";
|
|
15
15
|
import { existsSync } from "fs";
|
|
16
16
|
import { fileURLToPath } from "url";
|
|
17
17
|
import { MASTRACODE_WORKSPACE_TOOLS } from "@mastra/code-sdk/agents/tool-availability";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/factory",
|
|
3
|
-
"version": "0.17.1-alpha.
|
|
3
|
+
"version": "0.17.1-alpha.2",
|
|
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": {
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"posthog-node": "^5.46.1",
|
|
59
59
|
"zod": "^4.6.4",
|
|
60
60
|
"@mastra/auth-studio": "1.3.7",
|
|
61
|
-
"@mastra/code-sdk": "1.8.2-alpha.
|
|
61
|
+
"@mastra/code-sdk": "1.8.2-alpha.2",
|
|
62
62
|
"@mastra/auth-workos": "1.6.5",
|
|
63
|
-
"@mastra/
|
|
64
|
-
"@mastra/
|
|
63
|
+
"@mastra/core": "1.70.0-alpha.2",
|
|
64
|
+
"@mastra/slack": "1.6.4"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "22.20.1",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
71
|
"typescript-eslint": "^8.57.0",
|
|
72
72
|
"vitest": "4.1.11",
|
|
73
|
-
"@mastra/libsql": "1.23.1",
|
|
74
73
|
"@internal/lint": "0.0.135",
|
|
75
|
-
"@mastra/
|
|
74
|
+
"@mastra/libsql": "1.23.2-alpha.0",
|
|
76
75
|
"@internal/types-builder": "0.0.110",
|
|
77
|
-
"@internal/workspace": "0.0.7"
|
|
76
|
+
"@internal/workspace": "0.0.7",
|
|
77
|
+
"@mastra/pg": "1.27.0-alpha.2"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=22.19.0"
|