@mastra/factory 0.9.0 → 0.10.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +179 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/factory.d.ts +8 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +24 -8
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +1 -0
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/provenance.d.ts +7 -2
- package/dist/integrations/github/provenance.d.ts.map +1 -1
- package/dist/integrations/github/provenance.js +16 -2
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +3 -0
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +165 -6
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +193 -127
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +50 -0
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +1 -0
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/routes/attention.d.ts +17 -0
- package/dist/routes/attention.d.ts.map +1 -0
- package/dist/routes/attention.js +305 -0
- package/dist/routes/attention.js.map +1 -0
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +11 -2
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/surface.d.ts +2 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +61 -43
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +35 -14
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/defaults.js +5 -5
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts +13 -0
- package/dist/rules/dispatch-errors.d.ts.map +1 -0
- package/dist/rules/dispatch-errors.js +77 -0
- package/dist/rules/dispatch-errors.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +9 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +118 -45
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +30 -14
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +1 -1
- package/dist/rules/terminal-cleanup.d.ts.map +1 -1
- package/dist/rules/terminal-cleanup.js +2 -2
- package/dist/rules/terminal-cleanup.js.map +1 -1
- package/dist/rules/transition-service.d.ts +2 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +2 -0
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +9 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/secret-encryption.d.ts +25 -0
- package/dist/secret-encryption.d.ts.map +1 -0
- package/dist/secret-encryption.js +96 -0
- package/dist/secret-encryption.js.map +1 -0
- package/dist/session/factory-session.d.ts +13 -3
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +15 -6
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +16 -10
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +74 -14
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +14 -10
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +17 -11
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/session-title.d.ts +7 -0
- package/dist/session/session-title.d.ts.map +1 -0
- package/dist/session/session-title.js +14 -0
- package/dist/session/session-title.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +35 -0
- package/dist/session/thread-title-mirror.d.ts.map +1 -0
- package/dist/session/thread-title-mirror.js +51 -0
- package/dist/session/thread-title-mirror.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +3 -1
- package/dist/storage/domains/credentials/base.d.ts.map +1 -1
- package/dist/storage/domains/credentials/base.js +33 -15
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.d.ts +3 -1
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +45 -25
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +6 -5
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +21 -13
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +3 -1
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +28 -12
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +8 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +14 -2
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +115 -12
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +497 -43
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/work-item-branch.d.ts +23 -0
- package/dist/work-item-branch.d.ts.map +1 -0
- package/dist/work-item-branch.js +39 -0
- package/dist/work-item-branch.js.map +1 -0
- package/factory-skills/factory-rereview/SKILL.md +2 -0
- package/factory-skills/factory-review/SKILL.md +2 -0
- package/package.json +7 -7
|
@@ -12,18 +12,24 @@ export interface FirstExecCaptureDependencies {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Record when a session's agent completed its first successful
|
|
16
|
-
* (the TTFME anchor — "time to first meaningful exec").
|
|
15
|
+
* Record when a session's agent completed its first successful meaningful
|
|
16
|
+
* tool call (the TTFME anchor — "time to first meaningful exec").
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* Meaningful = a workspace tool the agent invoked itself (filesystem,
|
|
19
|
+
* search, code intelligence, sandbox exec, process control). We accept any
|
|
20
|
+
* tool whose name starts with the workspace prefix `mastra_workspace_` or
|
|
21
|
+
* matches the post-remap mastracode tool names. Non-workspace tools (memory,
|
|
22
|
+
* notification inbox, subagent, etc.) and skill-loader / preflight /
|
|
23
|
+
* materializer `sandbox.executeCommand()` calls that never flow through the
|
|
24
|
+
* tool layer are intentionally excluded.
|
|
24
25
|
*
|
|
25
|
-
* Failed
|
|
26
|
-
*
|
|
26
|
+
* Failed tool calls (`isError === true`) don't count. Approval-denied and
|
|
27
|
+
* abort-while-parked tool completions (`denied === true`) also don't count:
|
|
28
|
+
* the run-engine emits `tool_end` with `isError: false` for those paths
|
|
29
|
+
* because the tool didn't fail, but it also never ran, so treating them as
|
|
30
|
+
* "meaningful exec" would re-introduce the same contamination this fix is
|
|
31
|
+
* removing on the message side. The listener stays subscribed until a
|
|
32
|
+
* successful qualifying end, then unsubscribes. The storage write is guarded
|
|
27
33
|
* (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized
|
|
28
34
|
* sessions, and sessions without a source-control row are no-ops.
|
|
29
35
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"first-exec-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-exec-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"first-exec-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-exec-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,CAAC;KACnF,CAAC;CACH;AAmCD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,uBAAuB,EAChC,EAAE,aAAa,EAAE,EAAE,4BAA4B,GAC9C,MAAM,IAAI,CAoDZ"}
|
|
@@ -1,27 +1,87 @@
|
|
|
1
|
+
import { WORKSPACE_TOOLS_PREFIX } from "@mastra/core/workspace";
|
|
1
2
|
//#region src/session/first-exec-capture.ts
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* Tool names that count as "meaningful exec" independent of the workspace
|
|
5
|
+
* prefix. The workspace tools get remapped from `mastra_workspace_*` to
|
|
6
|
+
* mastracode-friendly names (see `mastracode/sdk/src/tool-names.ts`), and
|
|
7
|
+
* this listener runs on the emitted tool name after remap, so both spellings
|
|
8
|
+
* need to match.
|
|
9
|
+
*/
|
|
10
|
+
const MEANINGFUL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
11
|
+
"view",
|
|
12
|
+
"write_file",
|
|
13
|
+
"string_replace_lsp",
|
|
14
|
+
"find_files",
|
|
15
|
+
"delete_file",
|
|
16
|
+
"file_stat",
|
|
17
|
+
"mkdir",
|
|
18
|
+
"search_content",
|
|
19
|
+
"ast_smart_edit",
|
|
20
|
+
"lsp_inspect",
|
|
21
|
+
"execute_command",
|
|
22
|
+
"get_process_output",
|
|
23
|
+
"kill_process"
|
|
24
|
+
]);
|
|
25
|
+
function isMeaningfulToolName(name) {
|
|
26
|
+
if (!name) return false;
|
|
27
|
+
if (name.startsWith(`${WORKSPACE_TOOLS_PREFIX}_`)) return true;
|
|
28
|
+
return MEANINGFUL_TOOL_NAMES.has(name);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Record when a session's agent completed its first successful meaningful
|
|
32
|
+
* tool call (the TTFME anchor — "time to first meaningful exec").
|
|
5
33
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
34
|
+
* Meaningful = a workspace tool the agent invoked itself (filesystem,
|
|
35
|
+
* search, code intelligence, sandbox exec, process control). We accept any
|
|
36
|
+
* tool whose name starts with the workspace prefix `mastra_workspace_` or
|
|
37
|
+
* matches the post-remap mastracode tool names. Non-workspace tools (memory,
|
|
38
|
+
* notification inbox, subagent, etc.) and skill-loader / preflight /
|
|
39
|
+
* materializer `sandbox.executeCommand()` calls that never flow through the
|
|
40
|
+
* tool layer are intentionally excluded.
|
|
12
41
|
*
|
|
13
|
-
* Failed
|
|
14
|
-
*
|
|
42
|
+
* Failed tool calls (`isError === true`) don't count. Approval-denied and
|
|
43
|
+
* abort-while-parked tool completions (`denied === true`) also don't count:
|
|
44
|
+
* the run-engine emits `tool_end` with `isError: false` for those paths
|
|
45
|
+
* because the tool didn't fail, but it also never ran, so treating them as
|
|
46
|
+
* "meaningful exec" would re-introduce the same contamination this fix is
|
|
47
|
+
* removing on the message side. The listener stays subscribed until a
|
|
48
|
+
* successful qualifying end, then unsubscribes. The storage write is guarded
|
|
15
49
|
* (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized
|
|
16
50
|
* sessions, and sessions without a source-control row are no-ops.
|
|
17
51
|
*/
|
|
18
52
|
function observeSessionFirstExec(session, { sourceControl }) {
|
|
19
53
|
let seen = false;
|
|
54
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
55
|
+
const suspended = /* @__PURE__ */ new Set();
|
|
20
56
|
const unsubscribe = session.subscribe((event) => {
|
|
21
|
-
if (seen
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
if (seen) return;
|
|
58
|
+
switch (event.type) {
|
|
59
|
+
case "tool_start":
|
|
60
|
+
toolNames.set(event.toolCallId, event.toolName);
|
|
61
|
+
return;
|
|
62
|
+
case "tool_suspended":
|
|
63
|
+
suspended.add(event.toolCallId);
|
|
64
|
+
return;
|
|
65
|
+
case "tool_suspension_cancelled":
|
|
66
|
+
toolNames.delete(event.toolCallId);
|
|
67
|
+
suspended.delete(event.toolCallId);
|
|
68
|
+
return;
|
|
69
|
+
case "agent_end":
|
|
70
|
+
for (const id of toolNames.keys()) if (!suspended.has(id)) toolNames.delete(id);
|
|
71
|
+
return;
|
|
72
|
+
case "tool_end": {
|
|
73
|
+
const toolName = toolNames.get(event.toolCallId);
|
|
74
|
+
toolNames.delete(event.toolCallId);
|
|
75
|
+
suspended.delete(event.toolCallId);
|
|
76
|
+
if (event.isError) return;
|
|
77
|
+
if (event.denied) return;
|
|
78
|
+
if (!isMeaningfulToolName(toolName)) return;
|
|
79
|
+
seen = true;
|
|
80
|
+
unsubscribe();
|
|
81
|
+
sourceControl.sessions.markFirstMeaningfulExec({ sessionId: session.identity.getResourceId() }).catch((error) => console.warn("[Factory first-exec capture] Unable to persist first exec time.", error));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
25
85
|
});
|
|
26
86
|
return unsubscribe;
|
|
27
87
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"first-exec-capture.js","names":[],"sources":["../../src/session/first-exec-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstExecCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstExecCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMeaningfulExec'>;\n };\n}\n\n/**\n * Record when a session's agent completed its first successful
|
|
1
|
+
{"version":3,"file":"first-exec-capture.js","names":[],"sources":["../../src/session/first-exec-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\nimport { WORKSPACE_TOOLS_PREFIX } from '@mastra/core/workspace';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstExecCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstExecCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMeaningfulExec'>;\n };\n}\n\n/**\n * Tool names that count as \"meaningful exec\" independent of the workspace\n * prefix. The workspace tools get remapped from `mastra_workspace_*` to\n * mastracode-friendly names (see `mastracode/sdk/src/tool-names.ts`), and\n * this listener runs on the emitted tool name after remap, so both spellings\n * need to match.\n */\nconst MEANINGFUL_TOOL_NAMES: ReadonlySet<string> = new Set([\n // Filesystem (post-remap)\n 'view',\n 'write_file',\n 'string_replace_lsp',\n 'find_files',\n 'delete_file',\n 'file_stat',\n 'mkdir',\n // Search\n 'search_content',\n // Code intelligence\n 'ast_smart_edit',\n 'lsp_inspect',\n // Sandbox\n 'execute_command',\n 'get_process_output',\n 'kill_process',\n]);\n\nfunction isMeaningfulToolName(name: string | undefined): boolean {\n if (!name) return false;\n if (name.startsWith(`${WORKSPACE_TOOLS_PREFIX}_`)) return true;\n return MEANINGFUL_TOOL_NAMES.has(name);\n}\n\n/**\n * Record when a session's agent completed its first successful meaningful\n * tool call (the TTFME anchor — \"time to first meaningful exec\").\n *\n * Meaningful = a workspace tool the agent invoked itself (filesystem,\n * search, code intelligence, sandbox exec, process control). We accept any\n * tool whose name starts with the workspace prefix `mastra_workspace_` or\n * matches the post-remap mastracode tool names. Non-workspace tools (memory,\n * notification inbox, subagent, etc.) and skill-loader / preflight /\n * materializer `sandbox.executeCommand()` calls that never flow through the\n * tool layer are intentionally excluded.\n *\n * Failed tool calls (`isError === true`) don't count. Approval-denied and\n * abort-while-parked tool completions (`denied === true`) also don't count:\n * the run-engine emits `tool_end` with `isError: false` for those paths\n * because the tool didn't fail, but it also never ran, so treating them as\n * \"meaningful exec\" would re-introduce the same contamination this fix is\n * removing on the message side. The listener stays subscribed until a\n * successful qualifying end, then unsubscribes. The storage write is guarded\n * (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row are no-ops.\n */\nexport function observeSessionFirstExec(\n session: FirstExecCaptureSession,\n { sourceControl }: FirstExecCaptureDependencies,\n): () => void {\n let seen = false;\n // toolCallId -> toolName. Populated on `tool_start`, consumed on `tool_end`.\n // Suspended entries survive `agent_end` so the resumed run can still resolve\n // its tool name when the eventual `tool_end` arrives.\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribe = session.subscribe(event => {\n if (seen) return;\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 // Drop any in-flight starts that will never see a matching `tool_end`\n // on this run. Genuinely suspended calls stay so their eventual\n // resume-time `tool_end` can still map back to a tool name. Without\n // this, sessions that repeatedly abort or error before a qualifying\n // completion would accumulate stale entries for the entire lifetime\n // of the subscription.\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) return;\n if (event.denied) return;\n if (!isMeaningfulToolName(toolName)) return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMeaningfulExec({ sessionId: session.identity.getResourceId() })\n .catch(error => console.warn('[Factory first-exec capture] Unable to persist first exec time.', error));\n return;\n }\n }\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;AAuBA,MAAM,wCAA6C,IAAI,IAAI;CAEzD;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,SAAS,qBAAqB,MAAmC;CAC/D,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,KAAK,WAAW,GAAG,uBAAuB,EAAE,GAAG,OAAO;CAC1D,OAAO,sBAAsB,IAAI,IAAI;AACvC;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,wBACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CAIX,MAAM,4BAAY,IAAI,IAAoB;CAC1C,MAAM,4BAAY,IAAI,IAAY;CAClC,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,MAAM;EACV,QAAQ,MAAM,MAAd;GACE,KAAK;IACH,UAAU,IAAI,MAAM,YAAY,MAAM,QAAQ;IAC9C;GAEF,KAAK;IACH,UAAU,IAAI,MAAM,UAAU;IAC9B;GAEF,KAAK;IACH,UAAU,OAAO,MAAM,UAAU;IACjC,UAAU,OAAO,MAAM,UAAU;IACjC;GAEF,KAAK;IAOH,KAAK,MAAM,MAAM,UAAU,KAAK,GAC9B,IAAI,CAAC,UAAU,IAAI,EAAE,GAAG,UAAU,OAAO,EAAE;IAE7C;GAEF,KAAK,YAAY;IACf,MAAM,WAAW,UAAU,IAAI,MAAM,UAAU;IAC/C,UAAU,OAAO,MAAM,UAAU;IACjC,UAAU,OAAO,MAAM,UAAU;IACjC,IAAI,MAAM,SAAS;IACnB,IAAI,MAAM,QAAQ;IAClB,IAAI,CAAC,qBAAqB,QAAQ,GAAG;IACrC,OAAO;IACP,YAAY;IACZ,cAAmB,SAChB,wBAAwB,EAAE,WAAW,QAAQ,SAAS,cAAc,EAAE,CAAC,CAAC,CACxE,OAAM,UAAS,QAAQ,KAAK,mEAAmE,KAAK,CAAC;IACxG;GACF;EACF;CACF,CAAC;CACD,OAAO;AACT"}
|
|
@@ -12,18 +12,22 @@ export interface FirstMessageCaptureDependencies {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Record when a session's agent first
|
|
15
|
+
* Record when a session's agent first produced an actual conversational
|
|
16
|
+
* message (the TTFI anchor — "time to first interaction").
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* We stamp on the first `message_start` whose role is `user` or `assistant`.
|
|
19
|
+
* The coordinator writes `role='signal'` rows into the thread (skill loads,
|
|
20
|
+
* phase markers, memory reminders) before the model is ever invoked; those
|
|
21
|
+
* signals used to trigger a stamp via `agent_start`, giving every session —
|
|
22
|
+
* including zero-message model-init failures — a false TTFI of a few seconds.
|
|
23
|
+
* Gating on real conversational roles keeps the TTFI column honest: sessions
|
|
24
|
+
* that never produced a user or assistant message stay NULL and drop out of
|
|
25
|
+
* TTFI percentiles instead of contaminating them.
|
|
23
26
|
*
|
|
24
|
-
* The listener unsubscribes after the first
|
|
25
|
-
* guarded (`first_message_at IS NULL`), so restarts, re-materialized
|
|
26
|
-
* and sessions without a source-control row (chat-only channels)
|
|
27
|
+
* The listener unsubscribes after the first qualifying message; the storage
|
|
28
|
+
* write is guarded (`first_message_at IS NULL`), so restarts, re-materialized
|
|
29
|
+
* sessions, and sessions without a source-control row (chat-only channels)
|
|
30
|
+
* are no-ops.
|
|
27
31
|
*/
|
|
28
32
|
export declare function observeSessionFirstMessage(session: FirstMessageCaptureSession, { sourceControl }: FirstMessageCaptureDependencies): () => void;
|
|
29
33
|
//# sourceMappingURL=first-message-capture.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"first-message-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-message-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,+BAA+B;IAC9C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC;KAC5E,CAAC;CACH;AAED
|
|
1
|
+
{"version":3,"file":"first-message-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-message-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,+BAA+B;IAC9C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC;KAC5E,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,0BAA0B,EACnC,EAAE,aAAa,EAAE,EAAE,+BAA+B,GACjD,MAAM,IAAI,CAaZ"}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
//#region src/session/first-message-capture.ts
|
|
2
2
|
/**
|
|
3
|
-
* Record when a session's agent first
|
|
3
|
+
* Record when a session's agent first produced an actual conversational
|
|
4
|
+
* message (the TTFI anchor — "time to first interaction").
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* We stamp on the first `message_start` whose role is `user` or `assistant`.
|
|
7
|
+
* The coordinator writes `role='signal'` rows into the thread (skill loads,
|
|
8
|
+
* phase markers, memory reminders) before the model is ever invoked; those
|
|
9
|
+
* signals used to trigger a stamp via `agent_start`, giving every session —
|
|
10
|
+
* including zero-message model-init failures — a false TTFI of a few seconds.
|
|
11
|
+
* Gating on real conversational roles keeps the TTFI column honest: sessions
|
|
12
|
+
* that never produced a user or assistant message stay NULL and drop out of
|
|
13
|
+
* TTFI percentiles instead of contaminating them.
|
|
11
14
|
*
|
|
12
|
-
* The listener unsubscribes after the first
|
|
13
|
-
* guarded (`first_message_at IS NULL`), so restarts, re-materialized
|
|
14
|
-
* and sessions without a source-control row (chat-only channels)
|
|
15
|
+
* The listener unsubscribes after the first qualifying message; the storage
|
|
16
|
+
* write is guarded (`first_message_at IS NULL`), so restarts, re-materialized
|
|
17
|
+
* sessions, and sessions without a source-control row (chat-only channels)
|
|
18
|
+
* are no-ops.
|
|
15
19
|
*/
|
|
16
20
|
function observeSessionFirstMessage(session, { sourceControl }) {
|
|
17
21
|
let seen = false;
|
|
18
22
|
const unsubscribe = session.subscribe((event) => {
|
|
19
|
-
if (seen || event.type !== "
|
|
23
|
+
if (seen || event.type !== "message_start") return;
|
|
24
|
+
const role = event.message.role;
|
|
25
|
+
if (role !== "user" && role !== "assistant") return;
|
|
20
26
|
seen = true;
|
|
21
27
|
unsubscribe();
|
|
22
28
|
sourceControl.sessions.markFirstMessage({ sessionId: session.identity.getResourceId() }).catch((error) => console.warn("[Factory first-message capture] Unable to persist first message time.", error));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"first-message-capture.js","names":[],"sources":["../../src/session/first-message-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstMessageCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstMessageCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMessage'>;\n };\n}\n\n/**\n * Record when a session's agent first
|
|
1
|
+
{"version":3,"file":"first-message-capture.js","names":[],"sources":["../../src/session/first-message-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstMessageCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstMessageCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMessage'>;\n };\n}\n\n/**\n * Record when a session's agent first produced an actual conversational\n * message (the TTFI anchor — \"time to first interaction\").\n *\n * We stamp on the first `message_start` whose role is `user` or `assistant`.\n * The coordinator writes `role='signal'` rows into the thread (skill loads,\n * phase markers, memory reminders) before the model is ever invoked; those\n * signals used to trigger a stamp via `agent_start`, giving every session —\n * including zero-message model-init failures — a false TTFI of a few seconds.\n * Gating on real conversational roles keeps the TTFI column honest: sessions\n * that never produced a user or assistant message stay NULL and drop out of\n * TTFI percentiles instead of contaminating them.\n *\n * The listener unsubscribes after the first qualifying message; the storage\n * write is guarded (`first_message_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row (chat-only channels)\n * are no-ops.\n */\nexport function observeSessionFirstMessage(\n session: FirstMessageCaptureSession,\n { sourceControl }: FirstMessageCaptureDependencies,\n): () => void {\n let seen = false;\n const unsubscribe = session.subscribe(event => {\n if (seen || event.type !== 'message_start') return;\n const role = event.message.role;\n if (role !== 'user' && role !== 'assistant') return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMessage({ sessionId: session.identity.getResourceId() })\n .catch(error => console.warn('[Factory first-message capture] Unable to persist first message time.', error));\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,2BACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CACX,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,QAAQ,MAAM,SAAS,iBAAiB;EAC5C,MAAM,OAAO,MAAM,QAAQ;EAC3B,IAAI,SAAS,UAAU,SAAS,aAAa;EAC7C,OAAO;EACP,YAAY;EACZ,cAAmB,SAChB,iBAAiB,EAAE,WAAW,QAAQ,SAAS,cAAc,EAAE,CAAC,CAAC,CACjE,OAAM,UAAS,QAAQ,KAAK,yEAAyE,KAAK,CAAC;CAChH,CAAC;CACD,OAAO;AACT"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one shape a session row's title may take, wherever it comes from — typed
|
|
3
|
+
* by a user at creation, generated for its thread, or refined by the observer.
|
|
4
|
+
* Returns null when nothing readable is left, so callers can reject or skip.
|
|
5
|
+
*/
|
|
6
|
+
export declare function normalizeSessionTitle(title: string): string | null;
|
|
7
|
+
//# sourceMappingURL=session-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title.d.ts","sourceRoot":"","sources":["../../src/session/session-title.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIlE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/session/session-title.ts
|
|
2
|
+
const MAX_SESSION_TITLE_LENGTH = 80;
|
|
3
|
+
/**
|
|
4
|
+
* The one shape a session row's title may take, wherever it comes from — typed
|
|
5
|
+
* by a user at creation, generated for its thread, or refined by the observer.
|
|
6
|
+
* Returns null when nothing readable is left, so callers can reject or skip.
|
|
7
|
+
*/
|
|
8
|
+
function normalizeSessionTitle(title) {
|
|
9
|
+
return [...title.replace(/\s+/g, " ").trim()].slice(0, MAX_SESSION_TITLE_LENGTH).join("").trimEnd() || null;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { normalizeSessionTitle };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=session-title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title.js","names":[],"sources":["../../src/session/session-title.ts"],"sourcesContent":["const MAX_SESSION_TITLE_LENGTH = 80;\n\n/**\n * The one shape a session row's title may take, wherever it comes from — typed\n * by a user at creation, generated for its thread, or refined by the observer.\n * Returns null when nothing readable is left, so callers can reject or skip.\n */\nexport function normalizeSessionTitle(title: string): string | null {\n // Cap code points, not UTF-16 units: an emoji straddling the cap would store a lone surrogate.\n const capped = [...title.replace(/\\s+/g, ' ').trim()].slice(0, MAX_SESSION_TITLE_LENGTH).join('');\n return capped.trimEnd() || null;\n}\n"],"mappings":";AAAA,MAAM,2BAA2B;;;;;;AAOjC,SAAgB,sBAAsB,OAA8B;CAGlE,OADe,CAAC,GAAG,MAAM,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,wBAAwB,CAAC,CAAC,KAAK,EAClF,CAAC,CAAC,QAAQ,KAAK;AAC7B"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentControllerEvent, AgentControllerThread } from '@mastra/core/agent-controller';
|
|
2
|
+
import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
|
|
3
|
+
export interface ThreadTitleMirrorSession {
|
|
4
|
+
readonly identity: {
|
|
5
|
+
getResourceId(): string;
|
|
6
|
+
};
|
|
7
|
+
readonly thread: {
|
|
8
|
+
getById(args: {
|
|
9
|
+
threadId: string;
|
|
10
|
+
}): Promise<AgentControllerThread | null>;
|
|
11
|
+
};
|
|
12
|
+
subscribe(listener: (event: AgentControllerEvent) => void): () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ThreadTitleMirrorDependencies {
|
|
15
|
+
sourceControl: {
|
|
16
|
+
sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId' | 'rename'>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Project the bound thread's title onto its source-control session row, from
|
|
21
|
+
* whichever namer produced it — core on the first turn, the observational-memory
|
|
22
|
+
* observer as the thread grows, or an explicit rename.
|
|
23
|
+
*
|
|
24
|
+
* The factory sidebar reads the session row, which otherwise keeps the raw first
|
|
25
|
+
* prompt (chat sessions) or nothing at all (work sessions, which then show their
|
|
26
|
+
* branch). Binding a thread reconciles the row against the stored title, so a
|
|
27
|
+
* session started before this ran — or one whose rename event was missed — is
|
|
28
|
+
* named the next time it is opened.
|
|
29
|
+
*
|
|
30
|
+
* A session names threads it is no longer showing, and each write reads the row
|
|
31
|
+
* before replacing it. Titles for any other thread are therefore dropped, and
|
|
32
|
+
* writes run one at a time so the last event wins rather than the fastest.
|
|
33
|
+
*/
|
|
34
|
+
export declare function observeSessionThreadTitle(session: ThreadTitleMirrorSession, { sourceControl }: ThreadTitleMirrorDependencies): () => void;
|
|
35
|
+
//# sourceMappingURL=thread-title-mirror.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-title-mirror.d.ts","sourceRoot":"","sources":["../../src/session/thread-title-mirror.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEjG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC;IAChG,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,6BAA6B;IAC5C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,GAAG,QAAQ,CAAC,CAAC;KACrF,CAAC;CACH;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,wBAAwB,EACjC,EAAE,aAAa,EAAE,EAAE,6BAA6B,GAC/C,MAAM,IAAI,CAuCZ"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { normalizeSessionTitle } from "./session-title.js";
|
|
2
|
+
//#region src/session/thread-title-mirror.ts
|
|
3
|
+
/**
|
|
4
|
+
* Project the bound thread's title onto its source-control session row, from
|
|
5
|
+
* whichever namer produced it — core on the first turn, the observational-memory
|
|
6
|
+
* observer as the thread grows, or an explicit rename.
|
|
7
|
+
*
|
|
8
|
+
* The factory sidebar reads the session row, which otherwise keeps the raw first
|
|
9
|
+
* prompt (chat sessions) or nothing at all (work sessions, which then show their
|
|
10
|
+
* branch). Binding a thread reconciles the row against the stored title, so a
|
|
11
|
+
* session started before this ran — or one whose rename event was missed — is
|
|
12
|
+
* named the next time it is opened.
|
|
13
|
+
*
|
|
14
|
+
* A session names threads it is no longer showing, and each write reads the row
|
|
15
|
+
* before replacing it. Titles for any other thread are therefore dropped, and
|
|
16
|
+
* writes run one at a time so the last event wins rather than the fastest.
|
|
17
|
+
*/
|
|
18
|
+
function observeSessionThreadTitle(session, { sourceControl }) {
|
|
19
|
+
const sessionId = session.identity.getResourceId();
|
|
20
|
+
let boundThreadId;
|
|
21
|
+
let writes = Promise.resolve();
|
|
22
|
+
const project = (threadId, rawTitle) => {
|
|
23
|
+
boundThreadId ??= threadId;
|
|
24
|
+
if (threadId !== boundThreadId) return;
|
|
25
|
+
const title = rawTitle ? normalizeSessionTitle(rawTitle) : null;
|
|
26
|
+
if (!title) return;
|
|
27
|
+
writes = writes.then(async () => {
|
|
28
|
+
if (threadId !== boundThreadId) return;
|
|
29
|
+
const row = await sourceControl.sessions.getBySessionId(sessionId);
|
|
30
|
+
if (!row || row.title === title) return;
|
|
31
|
+
await sourceControl.sessions.rename({
|
|
32
|
+
sessionId,
|
|
33
|
+
title
|
|
34
|
+
});
|
|
35
|
+
}).catch((error) => console.warn("[Factory thread-title mirror] Unable to persist the session title.", error));
|
|
36
|
+
};
|
|
37
|
+
return session.subscribe((event) => {
|
|
38
|
+
switch (event.type) {
|
|
39
|
+
case "thread_title_updated": return project(event.threadId, event.title);
|
|
40
|
+
case "om_thread_title_updated": return project(event.threadId, event.newTitle);
|
|
41
|
+
case "thread_changed":
|
|
42
|
+
boundThreadId = event.threadId;
|
|
43
|
+
session.thread.getById({ threadId: event.threadId }).then((thread) => project(event.threadId, thread?.title)).catch((error) => console.warn("[Factory thread-title mirror] Unable to read the bound thread.", error));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { observeSessionThreadTitle };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=thread-title-mirror.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-title-mirror.js","names":[],"sources":["../../src/session/thread-title-mirror.ts"],"sourcesContent":["import type { AgentControllerEvent, AgentControllerThread } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { normalizeSessionTitle } from './session-title.js';\n\nexport interface ThreadTitleMirrorSession {\n readonly identity: { getResourceId(): string };\n readonly thread: { getById(args: { threadId: string }): Promise<AgentControllerThread | null> };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface ThreadTitleMirrorDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId' | 'rename'>;\n };\n}\n\n/**\n * Project the bound thread's title onto its source-control session row, from\n * whichever namer produced it — core on the first turn, the observational-memory\n * observer as the thread grows, or an explicit rename.\n *\n * The factory sidebar reads the session row, which otherwise keeps the raw first\n * prompt (chat sessions) or nothing at all (work sessions, which then show their\n * branch). Binding a thread reconciles the row against the stored title, so a\n * session started before this ran — or one whose rename event was missed — is\n * named the next time it is opened.\n *\n * A session names threads it is no longer showing, and each write reads the row\n * before replacing it. Titles for any other thread are therefore dropped, and\n * writes run one at a time so the last event wins rather than the fastest.\n */\nexport function observeSessionThreadTitle(\n session: ThreadTitleMirrorSession,\n { sourceControl }: ThreadTitleMirrorDependencies,\n): () => void {\n const sessionId = session.identity.getResourceId();\n let boundThreadId: string | undefined;\n let writes: Promise<void> = Promise.resolve();\n\n const project = (threadId: string, rawTitle: string | undefined): void => {\n // A thread can be named before anything binds one, so the first thread to\n // surface a title speaks for the row until `thread_changed` rebinds it.\n boundThreadId ??= threadId;\n if (threadId !== boundThreadId) return;\n const title = rawTitle ? normalizeSessionTitle(rawTitle) : null;\n if (!title) return;\n\n writes = writes\n .then(async () => {\n if (threadId !== boundThreadId) return;\n const row = await sourceControl.sessions.getBySessionId(sessionId);\n if (!row || row.title === title) return;\n await sourceControl.sessions.rename({ sessionId, title });\n })\n .catch(error => console.warn('[Factory thread-title mirror] Unable to persist the session title.', error));\n };\n\n return session.subscribe(event => {\n switch (event.type) {\n case 'thread_title_updated':\n return project(event.threadId, event.title);\n case 'om_thread_title_updated':\n return project(event.threadId, event.newTitle);\n case 'thread_changed': {\n boundThreadId = event.threadId;\n void session.thread\n .getById({ threadId: event.threadId })\n .then(thread => project(event.threadId, thread?.title))\n .catch(error => console.warn('[Factory thread-title mirror] Unable to read the bound thread.', error));\n return;\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,SAAgB,0BACd,SACA,EAAE,iBACU;CACZ,MAAM,YAAY,QAAQ,SAAS,cAAc;CACjD,IAAI;CACJ,IAAI,SAAwB,QAAQ,QAAQ;CAE5C,MAAM,WAAW,UAAkB,aAAuC;EAGxE,kBAAkB;EAClB,IAAI,aAAa,eAAe;EAChC,MAAM,QAAQ,WAAW,sBAAsB,QAAQ,IAAI;EAC3D,IAAI,CAAC,OAAO;EAEZ,SAAS,OACN,KAAK,YAAY;GAChB,IAAI,aAAa,eAAe;GAChC,MAAM,MAAM,MAAM,cAAc,SAAS,eAAe,SAAS;GACjE,IAAI,CAAC,OAAO,IAAI,UAAU,OAAO;GACjC,MAAM,cAAc,SAAS,OAAO;IAAE;IAAW;GAAM,CAAC;EAC1D,CAAC,CAAC,CACD,OAAM,UAAS,QAAQ,KAAK,sEAAsE,KAAK,CAAC;CAC7G;CAEA,OAAO,QAAQ,WAAU,UAAS;EAChC,QAAQ,MAAM,MAAd;GACE,KAAK,wBACH,OAAO,QAAQ,MAAM,UAAU,MAAM,KAAK;GAC5C,KAAK,2BACH,OAAO,QAAQ,MAAM,UAAU,MAAM,QAAQ;GAC/C,KAAK;IACH,gBAAgB,MAAM;IACtB,QAAa,OACV,QAAQ,EAAE,UAAU,MAAM,SAAS,CAAC,CAAC,CACrC,MAAK,WAAU,QAAQ,MAAM,UAAU,QAAQ,KAAK,CAAC,CAAC,CACtD,OAAM,UAAS,QAAQ,KAAK,kEAAkE,KAAK,CAAC;IACvG;EAEJ;CACF,CAAC;AACH"}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import type { AuthCredential, OAuthCredential } from '@mastra/code-sdk/auth/types';
|
|
26
26
|
import { FactoryStorageDomain } from '@mastra/core/storage';
|
|
27
27
|
import type { CollectionSchema } from '@mastra/core/storage';
|
|
28
|
+
import type { FactorySecretEncryption } from '../../../secret-encryption.js';
|
|
28
29
|
/** Owning tenant of a credential row. `userId` absent = org-scoped row. */
|
|
29
30
|
export interface CredentialTenant {
|
|
30
31
|
orgId: string;
|
|
@@ -88,7 +89,8 @@ export declare const OAUTH_LOGIN_SESSIONS_SCHEMA: CollectionSchema;
|
|
|
88
89
|
*/
|
|
89
90
|
export declare class ModelCredentialsStorage extends FactoryStorageDomain {
|
|
90
91
|
#private;
|
|
91
|
-
|
|
92
|
+
private readonly encryption;
|
|
93
|
+
constructor(encryption?: FactorySecretEncryption);
|
|
92
94
|
init(): Promise<void>;
|
|
93
95
|
dangerouslyClearAll(): Promise<void>;
|
|
94
96
|
/** Read the tenant's credential for a provider at exactly that scope. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/credentials/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAwB,MAAM,sBAAsB,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAsC,MAAM,sBAAsB,CAAC;AAEjG,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7C,uDAAuD;AACvD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,EAAE,cAAc,CAAC;IAC3B,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,EAAE,cAAc,CAAC;CAC5B;AAED,2DAA2D;AAC3D,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,aAAa,CAAC;AAE5D,2DAA2D;AAC3D,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,KAAK,CAAC;AAElD,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC;IACvB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,oBAAoB,CAAC;IACvC,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,EAAE,IAAI,CAAC;IAChB,2EAA2E;IAC3E,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC;IACvB,eAAe,CAAC,EAAE,oBAAoB,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAED,0DAA0D;AAC1D,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,SAAa,GAAG,OAAO,CAE/F;AAED,eAAO,MAAM,wBAAwB,EAAE,gBAsBtC,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,gBAczC,CAAC;AA6CF;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,oBAAoB
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/credentials/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPlaintextFactorySecretEncryption } from "../../../secret-encryption.js";
|
|
1
2
|
import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
|
|
2
3
|
//#region src/storage/domains/credentials/base.ts
|
|
3
4
|
/** Mirror of `AuthStorage.getApiKey()`'s expiry check. */
|
|
@@ -86,11 +87,15 @@ function tenantWhere(tenant, provider) {
|
|
|
86
87
|
* invalidating each other's rotating tokens / double-claiming a flow.
|
|
87
88
|
*/
|
|
88
89
|
var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
89
|
-
|
|
90
|
+
encryption;
|
|
91
|
+
constructor(encryption = createPlaintextFactorySecretEncryption()) {
|
|
90
92
|
super("model-credentials");
|
|
93
|
+
this.encryption = encryption;
|
|
91
94
|
}
|
|
92
95
|
async init() {
|
|
93
96
|
await this.ensureCollections([MODEL_CREDENTIALS_SCHEMA, OAUTH_LOGIN_SESSIONS_SCHEMA]);
|
|
97
|
+
const rows = await this.ops.findMany("model_provider_credentials", {});
|
|
98
|
+
await Promise.all(rows.map((row) => this.#migrateCredential(row.id)));
|
|
94
99
|
}
|
|
95
100
|
async dangerouslyClearAll() {
|
|
96
101
|
await this.ops.deleteMany("oauth_login_sessions", {});
|
|
@@ -99,16 +104,28 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
|
99
104
|
get #db() {
|
|
100
105
|
return this.ops;
|
|
101
106
|
}
|
|
107
|
+
async #decryptCredential(value) {
|
|
108
|
+
return (await this.encryption.decrypt(value)).value;
|
|
109
|
+
}
|
|
110
|
+
async #migrateCredential(id) {
|
|
111
|
+
await this.#db.updateAtomic("model_provider_credentials", { id }, async (row) => {
|
|
112
|
+
const decrypted = await this.encryption.decrypt(row.data);
|
|
113
|
+
if (!decrypted.needsReencryption) return null;
|
|
114
|
+
return { data: await this.encryption.encrypt(decrypted.value) };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
102
117
|
/** Read the tenant's credential for a provider at exactly that scope. */
|
|
103
118
|
async getCredential(tenant, provider) {
|
|
104
|
-
|
|
119
|
+
const row = await this.#db.findOne("model_provider_credentials", tenantWhere(tenant, provider));
|
|
120
|
+
return row ? this.#decryptCredential(row.data) : void 0;
|
|
105
121
|
}
|
|
106
122
|
/** Upsert the tenant's credential (`created_at` is preserved on update). */
|
|
107
123
|
async setCredential(tenant, provider, credential) {
|
|
108
124
|
const where = tenantWhere(tenant, provider);
|
|
125
|
+
const encrypted = await this.encryption.encrypt(credential);
|
|
109
126
|
const update = async () => this.#db.updateMany("model_provider_credentials", where, {
|
|
110
127
|
type: credential.type,
|
|
111
|
-
data:
|
|
128
|
+
data: encrypted,
|
|
112
129
|
updated_at: /* @__PURE__ */ new Date()
|
|
113
130
|
});
|
|
114
131
|
let lastError;
|
|
@@ -121,7 +138,7 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
|
121
138
|
user_id: tenant.userId ?? null,
|
|
122
139
|
provider,
|
|
123
140
|
type: credential.type,
|
|
124
|
-
data:
|
|
141
|
+
data: encrypted,
|
|
125
142
|
created_at: now,
|
|
126
143
|
updated_at: now
|
|
127
144
|
});
|
|
@@ -146,17 +163,17 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
|
146
163
|
org_id: orgId,
|
|
147
164
|
user_id: null
|
|
148
165
|
})]);
|
|
149
|
-
return [...userRows.map((row) => ({
|
|
166
|
+
return Promise.all([...userRows.map(async (row) => ({
|
|
150
167
|
provider: row.provider,
|
|
151
168
|
scope: "user",
|
|
152
|
-
credential: row.data,
|
|
169
|
+
credential: await this.#decryptCredential(row.data),
|
|
153
170
|
updatedAt: row.updated_at
|
|
154
|
-
})), ...orgRows.map((row) => ({
|
|
171
|
+
})), ...orgRows.map(async (row) => ({
|
|
155
172
|
provider: row.provider,
|
|
156
173
|
scope: "org",
|
|
157
|
-
credential: row.data,
|
|
174
|
+
credential: await this.#decryptCredential(row.data),
|
|
158
175
|
updatedAt: row.updated_at
|
|
159
|
-
}))];
|
|
176
|
+
}))]);
|
|
160
177
|
}
|
|
161
178
|
/**
|
|
162
179
|
* Resolve the credential a caller should use for a provider. By default the
|
|
@@ -173,7 +190,7 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
|
173
190
|
return row ? {
|
|
174
191
|
provider: row.provider,
|
|
175
192
|
scope: "user",
|
|
176
|
-
credential: row.data
|
|
193
|
+
credential: await this.#decryptCredential(row.data)
|
|
177
194
|
} : void 0;
|
|
178
195
|
};
|
|
179
196
|
const readOrg = async () => {
|
|
@@ -185,7 +202,7 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
|
185
202
|
return row ? {
|
|
186
203
|
provider: row.provider,
|
|
187
204
|
scope: "org",
|
|
188
|
-
credential: row.data
|
|
205
|
+
credential: await this.#decryptCredential(row.data)
|
|
189
206
|
} : void 0;
|
|
190
207
|
};
|
|
191
208
|
return precedence === "org" ? await readOrg() ?? await readUser() : await readUser() ?? await readOrg();
|
|
@@ -201,16 +218,17 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
|
|
|
201
218
|
async refreshOAuth(tenant, provider, refreshFn) {
|
|
202
219
|
let result;
|
|
203
220
|
await this.#db.updateAtomic("model_provider_credentials", tenantWhere(tenant, provider), async (row) => {
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
const decrypted = await this.encryption.decrypt(row.data);
|
|
222
|
+
if (decrypted.value.type !== "oauth") return null;
|
|
223
|
+
const current = decrypted.value;
|
|
206
224
|
if (!isOAuthCredentialExpired(current)) {
|
|
207
225
|
result = current;
|
|
208
|
-
return null;
|
|
226
|
+
return decrypted.needsReencryption ? { data: await this.encryption.encrypt(current) } : null;
|
|
209
227
|
}
|
|
210
228
|
const next = await refreshFn(current);
|
|
211
229
|
result = next;
|
|
212
230
|
return {
|
|
213
|
-
data: next,
|
|
231
|
+
data: await this.encryption.encrypt(next),
|
|
214
232
|
updated_at: /* @__PURE__ */ new Date()
|
|
215
233
|
};
|
|
216
234
|
});
|