@mastra/code-sdk 1.15.0-alpha.0 → 1.15.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/dist/acp/agent.d.ts.map +1 -1
- package/dist/acp/agent.js +1 -1
- package/dist/acp/agent.js.map +1 -1
- package/dist/acp/event-mapper.d.ts.map +1 -1
- package/dist/acp/event-mapper.js +1 -1
- package/dist/acp/event-mapper.js.map +1 -1
- package/dist/agent-connections/signal-provider.d.ts +1 -1
- package/dist/agent-connections/signal-provider.d.ts.map +1 -1
- package/dist/agent-connections/signal-provider.js.map +1 -1
- package/dist/agent-connections/state-processor.d.ts +2 -2
- package/dist/agent-connections/state-processor.d.ts.map +1 -1
- package/dist/agent-connections/state-processor.js.map +1 -1
- package/dist/agent-connections/thread-state.d.ts +1 -1
- package/dist/agent-connections/thread-state.d.ts.map +1 -1
- package/dist/agent-connections/thread-state.js.map +1 -1
- package/dist/agent-connections/tools.d.ts.map +1 -1
- package/dist/agent-connections/tools.js +1 -1
- package/dist/agent-connections/tools.js.map +1 -1
- package/dist/agents/background-completion.d.ts +15 -2
- package/dist/agents/background-completion.d.ts.map +1 -1
- package/dist/agents/background-completion.js +19 -2
- package/dist/agents/background-completion.js.map +1 -1
- package/dist/agents/credential-resolver.d.ts +6 -0
- package/dist/agents/credential-resolver.d.ts.map +1 -1
- package/dist/agents/credential-resolver.js +13 -4
- package/dist/agents/credential-resolver.js.map +1 -1
- package/dist/agents/memory.d.ts.map +1 -1
- package/dist/agents/memory.js +1 -1
- package/dist/agents/memory.js.map +1 -1
- package/dist/agents/sandbox-filesystem.d.ts +32 -10
- package/dist/agents/sandbox-filesystem.d.ts.map +1 -1
- package/dist/agents/sandbox-filesystem.js +208 -66
- package/dist/agents/sandbox-filesystem.js.map +1 -1
- package/dist/agents/workspace.d.ts.map +1 -1
- package/dist/agents/workspace.js +1 -0
- package/dist/agents/workspace.js.map +1 -1
- package/dist/experimental-agent.d.ts +1 -1
- package/dist/experimental-agent.d.ts.map +1 -1
- package/dist/experimental-agent.js.map +1 -1
- package/dist/factory/client.d.ts +41 -0
- package/dist/factory/client.d.ts.map +1 -0
- package/dist/factory/client.js +98 -0
- package/dist/factory/client.js.map +1 -0
- package/dist/factory/remote-controller.d.ts +34 -0
- package/dist/factory/remote-controller.d.ts.map +1 -0
- package/dist/factory/remote-controller.js +43 -0
- package/dist/factory/remote-controller.js.map +1 -0
- package/dist/factory/remote-session.d.ts +210 -0
- package/dist/factory/remote-session.d.ts.map +1 -0
- package/dist/factory/remote-session.js +420 -0
- package/dist/factory/remote-session.js.map +1 -0
- package/dist/headless/cli.d.ts +7 -0
- package/dist/headless/cli.d.ts.map +1 -1
- package/dist/headless/cli.js +19 -3
- package/dist/headless/cli.js.map +1 -1
- package/dist/headless/flags.d.ts.map +1 -1
- package/dist/headless/flags.js +2 -1
- package/dist/headless/flags.js.map +1 -1
- package/dist/headless/format.d.ts +2 -0
- package/dist/headless/format.d.ts.map +1 -1
- package/dist/headless/format.js +13 -1
- package/dist/headless/format.js.map +1 -1
- package/dist/headless/run-mc.d.ts.map +1 -1
- package/dist/headless/run-mc.js +48 -20
- package/dist/headless/run-mc.js.map +1 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -23
- package/dist/index.js.map +1 -1
- package/dist/onboarding/pack-activation.d.ts +2 -11
- package/dist/onboarding/pack-activation.d.ts.map +1 -1
- package/dist/onboarding/pack-activation.js.map +1 -1
- package/dist/onboarding/settings.d.ts +18 -0
- package/dist/onboarding/settings.d.ts.map +1 -1
- package/dist/onboarding/settings.js +28 -3
- package/dist/onboarding/settings.js.map +1 -1
- package/dist/platform/index.d.ts +74 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +200 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/process-memory-diagnostics.d.ts +2 -1
- package/dist/process-memory-diagnostics.d.ts.map +1 -1
- package/dist/process-memory-diagnostics.js.map +1 -1
- package/dist/providers/model-ids.d.ts +1 -0
- package/dist/providers/model-ids.d.ts.map +1 -1
- package/dist/providers/model-ids.js +6 -4
- package/dist/providers/model-ids.js.map +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js.map +1 -1
- package/dist/tools/workflows/create-workflow.d.ts.map +1 -1
- package/dist/tools/workflows/create-workflow.js +5 -1
- package/dist/tools/workflows/create-workflow.js.map +1 -1
- package/dist/utils/local-observability.d.ts +19 -0
- package/dist/utils/local-observability.d.ts.map +1 -0
- package/dist/utils/local-observability.js +57 -0
- package/dist/utils/local-observability.js.map +1 -0
- package/dist/utils/notification-dispatch.d.ts +27 -12
- package/dist/utils/notification-dispatch.d.ts.map +1 -1
- package/dist/utils/notification-dispatch.js +46 -24
- package/dist/utils/notification-dispatch.js.map +1 -1
- package/dist/utils/signals-pubsub.d.ts +5 -3
- package/dist/utils/signals-pubsub.d.ts.map +1 -1
- package/dist/utils/signals-pubsub.js +8 -6
- package/dist/utils/signals-pubsub.js.map +1 -1
- package/dist/utils/wake-session.d.ts +44 -0
- package/dist/utils/wake-session.d.ts.map +1 -0
- package/dist/utils/wake-session.js +86 -0
- package/dist/utils/wake-session.js.map +1 -0
- package/dist/workflows/register-primitives.js +4 -1
- package/dist/workflows/register-primitives.js.map +1 -1
- package/package.json +25 -25
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-workflow.js","names":[],"sources":["../../../src/tools/workflows/create-workflow.ts"],"sourcesContent":["/**\n * Parent-mode tool: delegate workflow construction to the workflow-builder\n * sub-agent. The parent code-agent calls this with the user's request verbatim;\n * the sub-agent runs its own discovery → compose → save loop and returns a\n * one-paragraph summary the parent relays to the user.\n *\n * The split exists so the parent code-agent's system prompt stays focused on\n * coding and isn't polluted with the long workflow-authoring contract.\n */\nimport type { Mastra } from '@mastra/core/mastra';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/**\n * Coerce an unknown thrown value into a readable string. Sub-agent tool errors\n * come through the stream in various shapes (Error, plain object, string) —\n * normalise so the parent tool's error message is always useful.\n */\nfunction stringifyError(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n try {\n return JSON.stringify(err) ?? String(err);\n } catch {\n return String(err);\n }\n}\n\nexport const createWorkflowTool = createTool({\n id: 'create-workflow',\n description:\n 'Build and save a Dynamic Workflow on behalf of the user. Pass the user request verbatim — a focused sub-agent handles discovery, construction, and persistence, then returns a summary. Use this whenever the user asks to \"build a workflow\", \"compose a workflow\", or similar. Do NOT try to construct workflows inline yourself.',\n inputSchema: z.object({\n request: z.string().describe('The user request, verbatim — do not paraphrase or summarise.'),\n }),\n outputSchema: z.object({\n summary: z.string().describe('Natural-language summary of what the sub-agent built. Relay this to the user.'),\n workflowId: z.string().optional().describe('The id of the saved workflow, if save-workflow returned ok.'),\n }),\n execute: async ({ request }, { mastra, requestContext }) => {\n if (!mastra) throw new Error('create-workflow requires a Mastra context.');\n const builder = (mastra as Mastra).getAgent('workflow-builder' as never);\n if (!builder) {\n throw new Error(\n 'The \"workflow-builder\" sub-agent is not registered on this Mastra instance. Cannot build workflows.',\n );\n }\n\n // Propagate the parent code-agent's requestContext so the sub-agent's\n // dynamic model resolver (getDynamicModel) sees controller.session.modelId.\n // Without this the sub-agent throws \"No model selected\" even when the user\n // has /models configured for the main code-agent.\n const stream = await builder.stream(request, { requestContext });\n\n // Sub-agent runs its own tool loop. We MUST verify save-workflow actually\n // ran and returned ok — otherwise the sub-agent's natural-language \"summary\"\n // is worthless (it will happily claim success without ever calling the tool,\n // or claim success after save-workflow threw). Surface every error the\n // sub-agent's tools produce so the caller sees them instead of a fake ok.\n let workflowId: string | undefined;\n let saveAttempted = false;\n let saveSucceeded = false;\n const toolErrors: Array<{ toolName: string; error: string }> = [];\n\n const reader = stream.fullStream.getReader();\n let streamCompleted = false;\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n streamCompleted = true;\n break;\n }\n if (value?.type === 'tool-call' && value.payload?.toolName === 'save-workflow') {\n saveAttempted = true;\n }\n if (value?.type === 'tool-result' && value.payload?.toolName === 'save-workflow') {\n const result = value.payload.result as { id?: string; ok?: boolean } | undefined;\n if (result && result.ok === true && typeof result.id === 'string') {\n workflowId = result.id;\n saveSucceeded = true;\n }\n }\n if (value?.type === 'tool-error' && typeof value.payload?.toolName === 'string') {\n toolErrors.push({ toolName: value.payload.toolName, error: stringifyError(value.payload.error) });\n }\n }\n } finally {\n if (!streamCompleted) {\n await reader.cancel().catch(() => undefined);\n }\n reader.releaseLock();\n }\n\n const summary = await stream.text;\n\n // If save-workflow errored, surface that error explicitly — do NOT return\n // the sub-agent's summary as if things were fine.\n const saveErrors = toolErrors.filter(e => e.toolName === 'save-workflow');\n if (saveErrors.length > 0 && !saveSucceeded) {\n throw new Error(\n `create-workflow failed: save-workflow threw ${saveErrors.length} error(s):\\n- ${saveErrors\n .map(e => e.error)\n .join('\\n- ')}\\n\\nSub-agent summary (unreliable, save did not succeed):\\n${summary}`,\n );\n }\n\n // If save-workflow was never called, the sub-agent gave up (or hallucinated\n // success). Fail loudly so the caller doesn't think a workflow exists.\n if (!saveAttempted) {\n const otherErrors = toolErrors.length\n ? `\\n\\nOther sub-agent tool errors:\\n- ${toolErrors.map(e => `${e.toolName}: ${e.error}`).join('\\n- ')}`\n : '';\n throw new Error(\n `create-workflow failed: the workflow-builder sub-agent never called save-workflow. No workflow was persisted.${otherErrors}\\n\\nSub-agent summary:\\n${summary}`,\n );\n }\n\n // Save was attempted but never returned ok (no tool-result with { ok: true, id }).\n if (!saveSucceeded) {\n const otherErrors = toolErrors.length\n ? `\\n\\nSub-agent tool errors:\\n- ${toolErrors.map(e => `${e.toolName}: ${e.error}`).join('\\n- ')}`\n : '';\n throw new Error(\n `create-workflow failed: save-workflow was called but did not return { ok: true }.${otherErrors}\\n\\nSub-agent summary:\\n${summary}`,\n );\n }\n\n return { summary, workflowId };\n },\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"create-workflow.js","names":[],"sources":["../../../src/tools/workflows/create-workflow.ts"],"sourcesContent":["/**\n * Parent-mode tool: delegate workflow construction to the workflow-builder\n * sub-agent. The parent code-agent calls this with the user's request verbatim;\n * the sub-agent runs its own discovery → compose → save loop and returns a\n * one-paragraph summary the parent relays to the user.\n *\n * The split exists so the parent code-agent's system prompt stays focused on\n * coding and isn't polluted with the long workflow-authoring contract.\n */\nimport type { Mastra } from '@mastra/core/mastra';\nimport { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\n/**\n * Coerce an unknown thrown value into a readable string. Sub-agent tool errors\n * come through the stream in various shapes (Error, plain object, string) —\n * normalise so the parent tool's error message is always useful.\n */\nfunction stringifyError(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === 'string') return err;\n try {\n return JSON.stringify(err) ?? String(err);\n } catch {\n return String(err);\n }\n}\n\nexport const createWorkflowTool = createTool({\n id: 'create-workflow',\n description:\n 'Build and save a Dynamic Workflow on behalf of the user. Pass the user request verbatim — a focused sub-agent handles discovery, construction, and persistence, then returns a summary. Use this whenever the user asks to \"build a workflow\", \"compose a workflow\", or similar. Do NOT try to construct workflows inline yourself.',\n inputSchema: z.object({\n request: z.string().describe('The user request, verbatim — do not paraphrase or summarise.'),\n }),\n outputSchema: z.object({\n summary: z.string().describe('Natural-language summary of what the sub-agent built. Relay this to the user.'),\n workflowId: z.string().optional().describe('The id of the saved workflow, if save-workflow returned ok.'),\n }),\n execute: async ({ request }, { mastra, requestContext }) => {\n if (!mastra) throw new Error('create-workflow requires a Mastra context.');\n const builder = (mastra as Mastra).getAgent('workflow-builder' as never);\n if (!builder) {\n throw new Error(\n 'The \"workflow-builder\" sub-agent is not registered on this Mastra instance. Cannot build workflows.',\n );\n }\n\n // Propagate the parent code-agent's requestContext so the sub-agent's\n // dynamic model resolver (getDynamicModel) sees controller.session.modelId.\n // Without this the sub-agent throws \"No model selected\" even when the user\n // has /models configured for the main code-agent.\n // Drop the reserved memory keys: they would bind the builder to the parent\n // run's thread, and the builder would wait on the run that is waiting on it.\n const builderRequestContext = requestContext ? new RequestContext(requestContext.entries()) : undefined;\n builderRequestContext?.delete(MASTRA_THREAD_ID_KEY);\n builderRequestContext?.delete(MASTRA_RESOURCE_ID_KEY);\n const stream = await builder.stream(request, { requestContext: builderRequestContext });\n\n // Sub-agent runs its own tool loop. We MUST verify save-workflow actually\n // ran and returned ok — otherwise the sub-agent's natural-language \"summary\"\n // is worthless (it will happily claim success without ever calling the tool,\n // or claim success after save-workflow threw). Surface every error the\n // sub-agent's tools produce so the caller sees them instead of a fake ok.\n let workflowId: string | undefined;\n let saveAttempted = false;\n let saveSucceeded = false;\n const toolErrors: Array<{ toolName: string; error: string }> = [];\n\n const reader = stream.fullStream.getReader();\n let streamCompleted = false;\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n streamCompleted = true;\n break;\n }\n if (value?.type === 'tool-call' && value.payload?.toolName === 'save-workflow') {\n saveAttempted = true;\n }\n if (value?.type === 'tool-result' && value.payload?.toolName === 'save-workflow') {\n const result = value.payload.result as { id?: string; ok?: boolean } | undefined;\n if (result && result.ok === true && typeof result.id === 'string') {\n workflowId = result.id;\n saveSucceeded = true;\n }\n }\n if (value?.type === 'tool-error' && typeof value.payload?.toolName === 'string') {\n toolErrors.push({ toolName: value.payload.toolName, error: stringifyError(value.payload.error) });\n }\n }\n } finally {\n if (!streamCompleted) {\n await reader.cancel().catch(() => undefined);\n }\n reader.releaseLock();\n }\n\n const summary = await stream.text;\n\n // If save-workflow errored, surface that error explicitly — do NOT return\n // the sub-agent's summary as if things were fine.\n const saveErrors = toolErrors.filter(e => e.toolName === 'save-workflow');\n if (saveErrors.length > 0 && !saveSucceeded) {\n throw new Error(\n `create-workflow failed: save-workflow threw ${saveErrors.length} error(s):\\n- ${saveErrors\n .map(e => e.error)\n .join('\\n- ')}\\n\\nSub-agent summary (unreliable, save did not succeed):\\n${summary}`,\n );\n }\n\n // If save-workflow was never called, the sub-agent gave up (or hallucinated\n // success). Fail loudly so the caller doesn't think a workflow exists.\n if (!saveAttempted) {\n const otherErrors = toolErrors.length\n ? `\\n\\nOther sub-agent tool errors:\\n- ${toolErrors.map(e => `${e.toolName}: ${e.error}`).join('\\n- ')}`\n : '';\n throw new Error(\n `create-workflow failed: the workflow-builder sub-agent never called save-workflow. No workflow was persisted.${otherErrors}\\n\\nSub-agent summary:\\n${summary}`,\n );\n }\n\n // Save was attempted but never returned ok (no tool-result with { ok: true, id }).\n if (!saveSucceeded) {\n const otherErrors = toolErrors.length\n ? `\\n\\nSub-agent tool errors:\\n- ${toolErrors.map(e => `${e.toolName}: ${e.error}`).join('\\n- ')}`\n : '';\n throw new Error(\n `create-workflow failed: save-workflow was called but did not return { ok: true }.${otherErrors}\\n\\nSub-agent summary:\\n${summary}`,\n );\n }\n\n return { summary, workflowId };\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PruneResult, RetentionConfig } from '@mastra/core/storage';
|
|
2
|
+
import { DuckDBStore } from '@mastra/duckdb';
|
|
3
|
+
/**
|
|
4
|
+
* Age limits for the local trace store: 14 days for spans, metrics and logs,
|
|
5
|
+
* 30 days for scores and feedback. Spans, logs and scores use the same windows
|
|
6
|
+
* as libsql's `DEFAULT_RETENTION`, which has no metrics or feedback entries.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LOCAL_OBSERVABILITY_RETENTION: RetentionConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Open the local observability DuckDB file used when `/observability local on`.
|
|
11
|
+
* Connects eagerly so a lock error surfaces now, not mid-session, then prunes
|
|
12
|
+
* aged events in the background so the file cannot grow without bound.
|
|
13
|
+
* `pruned` never rejects; anything left over is pruned on the next open.
|
|
14
|
+
*/
|
|
15
|
+
export declare function openLocalObservabilityStore(path: string): Promise<{
|
|
16
|
+
store: DuckDBStore;
|
|
17
|
+
pruned: Promise<PruneResult[]>;
|
|
18
|
+
}>;
|
|
19
|
+
//# sourceMappingURL=local-observability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-observability.d.ts","sourceRoot":"","sources":["../../src/utils/local-observability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,EAAE,eAQ3C,CAAC;AAqBF;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;CAAE,CAAC,CAMjE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DuckDBStore } from "@mastra/duckdb";
|
|
2
|
+
//#region src/utils/local-observability.ts
|
|
3
|
+
/**
|
|
4
|
+
* Age limits for the local trace store: 14 days for spans, metrics and logs,
|
|
5
|
+
* 30 days for scores and feedback. Spans, logs and scores use the same windows
|
|
6
|
+
* as libsql's `DEFAULT_RETENTION`, which has no metrics or feedback entries.
|
|
7
|
+
*/
|
|
8
|
+
const LOCAL_OBSERVABILITY_RETENTION = { observability: {
|
|
9
|
+
spans: { maxAge: "14d" },
|
|
10
|
+
metrics: { maxAge: "14d" },
|
|
11
|
+
logs: { maxAge: "14d" },
|
|
12
|
+
scores: { maxAge: "30d" },
|
|
13
|
+
feedback: { maxAge: "30d" }
|
|
14
|
+
} };
|
|
15
|
+
/** Upper bound on rows deleted per table per session, so startup pruning never runs long. */
|
|
16
|
+
const MAX_ROWS_PER_OPEN = 5e4;
|
|
17
|
+
const OBSERVABILITY_TABLES = [
|
|
18
|
+
"span_events",
|
|
19
|
+
"metric_events",
|
|
20
|
+
"log_events",
|
|
21
|
+
"score_events",
|
|
22
|
+
"feedback_events"
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Prune only when every observability table already exists. A brand-new file
|
|
26
|
+
* has no tables until the session composite runs init(), and pruning it would
|
|
27
|
+
* log a missing-table error from the storage layer for a file with nothing to delete.
|
|
28
|
+
*/
|
|
29
|
+
async function pruneExistingTables(store) {
|
|
30
|
+
if ((await store.db.query(`SELECT table_name FROM information_schema.tables WHERE table_name IN (${OBSERVABILITY_TABLES.map(() => "?").join(", ")})`, OBSERVABILITY_TABLES)).length < OBSERVABILITY_TABLES.length) return [];
|
|
31
|
+
return store.prune({
|
|
32
|
+
maxRows: MAX_ROWS_PER_OPEN,
|
|
33
|
+
pauseMs: 10
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Open the local observability DuckDB file used when `/observability local on`.
|
|
38
|
+
* Connects eagerly so a lock error surfaces now, not mid-session, then prunes
|
|
39
|
+
* aged events in the background so the file cannot grow without bound.
|
|
40
|
+
* `pruned` never rejects; anything left over is pruned on the next open.
|
|
41
|
+
*/
|
|
42
|
+
async function openLocalObservabilityStore(path) {
|
|
43
|
+
const store = new DuckDBStore({
|
|
44
|
+
id: "mastra-code-observability",
|
|
45
|
+
path,
|
|
46
|
+
retention: LOCAL_OBSERVABILITY_RETENTION
|
|
47
|
+
});
|
|
48
|
+
await store.db.getConnection();
|
|
49
|
+
return {
|
|
50
|
+
store,
|
|
51
|
+
pruned: pruneExistingTables(store).catch(() => [])
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { LOCAL_OBSERVABILITY_RETENTION, openLocalObservabilityStore };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=local-observability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-observability.js","names":[],"sources":["../../src/utils/local-observability.ts"],"sourcesContent":["import type { PruneResult, RetentionConfig } from '@mastra/core/storage';\nimport { DuckDBStore } from '@mastra/duckdb';\n\n/**\n * Age limits for the local trace store: 14 days for spans, metrics and logs,\n * 30 days for scores and feedback. Spans, logs and scores use the same windows\n * as libsql's `DEFAULT_RETENTION`, which has no metrics or feedback entries.\n */\nexport const LOCAL_OBSERVABILITY_RETENTION: RetentionConfig = {\n observability: {\n spans: { maxAge: '14d' },\n metrics: { maxAge: '14d' },\n logs: { maxAge: '14d' },\n scores: { maxAge: '30d' },\n feedback: { maxAge: '30d' },\n },\n};\n\n/** Upper bound on rows deleted per table per session, so startup pruning never runs long. */\nconst MAX_ROWS_PER_OPEN = 50_000;\n\nconst OBSERVABILITY_TABLES = ['span_events', 'metric_events', 'log_events', 'score_events', 'feedback_events'];\n\n/**\n * Prune only when every observability table already exists. A brand-new file\n * has no tables until the session composite runs init(), and pruning it would\n * log a missing-table error from the storage layer for a file with nothing to delete.\n */\nasync function pruneExistingTables(store: DuckDBStore): Promise<PruneResult[]> {\n const existing = await store.db.query<{ table_name: string }>(\n `SELECT table_name FROM information_schema.tables WHERE table_name IN (${OBSERVABILITY_TABLES.map(() => '?').join(', ')})`,\n OBSERVABILITY_TABLES,\n );\n if (existing.length < OBSERVABILITY_TABLES.length) return [];\n return store.prune({ maxRows: MAX_ROWS_PER_OPEN, pauseMs: 10 });\n}\n\n/**\n * Open the local observability DuckDB file used when `/observability local on`.\n * Connects eagerly so a lock error surfaces now, not mid-session, then prunes\n * aged events in the background so the file cannot grow without bound.\n * `pruned` never rejects; anything left over is pruned on the next open.\n */\nexport async function openLocalObservabilityStore(\n path: string,\n): Promise<{ store: DuckDBStore; pruned: Promise<PruneResult[]> }> {\n const store = new DuckDBStore({ id: 'mastra-code-observability', path, retention: LOCAL_OBSERVABILITY_RETENTION });\n await store.db.getConnection();\n // No init() here: the session composite initializes the domain.\n const pruned = pruneExistingTables(store).catch(() => []);\n return { store, pruned };\n}\n"],"mappings":";;;;;;;AAQA,MAAa,gCAAiD,EAC5D,eAAe;CACb,OAAO,EAAE,QAAQ,MAAM;CACvB,SAAS,EAAE,QAAQ,MAAM;CACzB,MAAM,EAAE,QAAQ,MAAM;CACtB,QAAQ,EAAE,QAAQ,MAAM;CACxB,UAAU,EAAE,QAAQ,MAAM;AAC5B,EACF;;AAGA,MAAM,oBAAoB;AAE1B,MAAM,uBAAuB;CAAC;CAAe;CAAiB;CAAc;CAAgB;AAAiB;;;;;;AAO7G,eAAe,oBAAoB,OAA4C;CAK7E,KAAI,MAJmB,MAAM,GAAG,MAC9B,yEAAyE,qBAAqB,UAAU,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,IACxH,oBACF,EAAA,CACa,SAAS,qBAAqB,QAAQ,OAAO,CAAC;CAC3D,OAAO,MAAM,MAAM;EAAE,SAAS;EAAmB,SAAS;CAAG,CAAC;AAChE;;;;;;;AAQA,eAAsB,4BACpB,MACiE;CACjE,MAAM,QAAQ,IAAI,YAAY;EAAE,IAAI;EAA6B;EAAM,WAAW;CAA8B,CAAC;CACjH,MAAM,MAAM,GAAG,cAAc;CAG7B,OAAO;EAAE;EAAO,QADD,oBAAoB,KAAK,CAAC,CAAC,YAAY,CAAC,CAClC;CAAE;AACzB"}
|
|
@@ -2,34 +2,48 @@ import type { LeaseProvider } from '@mastra/core/events';
|
|
|
2
2
|
import type { Mastra } from '@mastra/core/mastra';
|
|
3
3
|
/**
|
|
4
4
|
* Whether this call runs inside this process's own dispatch pass for
|
|
5
|
-
* `resourceId`,
|
|
6
|
-
*
|
|
5
|
+
* `resourceId`, holding the lease for threads of `scope` (or with no `scope`,
|
|
6
|
+
* for threads that carry none), as opposed to the dispatch schedule every
|
|
7
|
+
* process sharing the database takes turns running.
|
|
7
8
|
*/
|
|
8
|
-
export declare function isDispatchingNotificationsFor(resourceId: string): boolean;
|
|
9
|
+
export declare function isDispatchingNotificationsFor(resourceId: string, scope?: string): boolean;
|
|
9
10
|
/**
|
|
10
11
|
* Whether a delivery-time policy should leave a due notification pending
|
|
11
12
|
* (`hold`). Deliveries happen from this process's own dispatch pass for the
|
|
12
|
-
* record's resource
|
|
13
|
-
*
|
|
14
|
-
* dispatcher runs, its own resources are
|
|
15
|
-
* their dispatch lease, never by the
|
|
13
|
+
* record's resource, under the lease that covers the record's thread. Anything
|
|
14
|
+
* else holds: a thread no session here owns belongs to another process sharing
|
|
15
|
+
* the database, and once this process's dispatcher runs, its own resources are
|
|
16
|
+
* delivered by whichever process holds their dispatch lease, never by the
|
|
17
|
+
* shared dispatch schedule.
|
|
16
18
|
*/
|
|
17
|
-
export declare function shouldHoldNotificationDelivery({ resourceId, ownedHere, dispatcher, }: {
|
|
19
|
+
export declare function shouldHoldNotificationDelivery({ resourceId, scope, ownedHere, dispatcher, }: {
|
|
18
20
|
resourceId: string;
|
|
21
|
+
/** The thread's scope (`WakeSession.scope`), unset for a thread that carries none. */
|
|
22
|
+
scope?: string;
|
|
19
23
|
/** Whether a session in this process owns the resource. */
|
|
20
24
|
ownedHere: boolean;
|
|
21
25
|
dispatcher: {
|
|
22
26
|
readonly running: boolean;
|
|
23
27
|
};
|
|
24
28
|
}): boolean;
|
|
29
|
+
type SessionScope = {
|
|
30
|
+
resourceId: string;
|
|
31
|
+
scope?: string;
|
|
32
|
+
};
|
|
25
33
|
export type ResourceNotificationDispatcherOptions = {
|
|
26
34
|
getMastra: () => Mastra | undefined;
|
|
27
|
-
/**
|
|
28
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The resources this process runs sessions for, each with the thread scope
|
|
37
|
+
* (`threadScopeKey`) of a session serving it, unset when it has none.
|
|
38
|
+
*/
|
|
39
|
+
getSessionScopes: () => Iterable<SessionScope> | Promise<Iterable<SessionScope>>;
|
|
29
40
|
/**
|
|
30
41
|
* Cross-process leases. When set, a resource's notifications are dispatched
|
|
31
|
-
* only by the
|
|
32
|
-
* the same resource
|
|
42
|
+
* only by the processes holding its dispatch leases, so two processes
|
|
43
|
+
* serving the same resource never both deliver one: the threads of each
|
|
44
|
+
* scope (each project, for processes in different directories) go to the
|
|
45
|
+
* process holding that scope's lease, and threads with no scope to the one
|
|
46
|
+
* holding the resource's own.
|
|
33
47
|
*/
|
|
34
48
|
leases?: LeaseProvider;
|
|
35
49
|
/** Unique to this process. */
|
|
@@ -56,4 +70,5 @@ export declare function createResourceNotificationDispatcher(options: ResourceNo
|
|
|
56
70
|
tick: () => Promise<void>;
|
|
57
71
|
};
|
|
58
72
|
export type ResourceNotificationDispatcher = ReturnType<typeof createResourceNotificationDispatcher>;
|
|
73
|
+
export {};
|
|
59
74
|
//# sourceMappingURL=notification-dispatch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-dispatch.d.ts","sourceRoot":"","sources":["../../src/utils/notification-dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"notification-dispatch.d.ts","sourceRoot":"","sources":["../../src/utils/notification-dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAoBlD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAIzF;AAED;;;;;;;;GAQG;AACH,wBAAgB,8BAA8B,CAAC,EAC7C,UAAU,EACV,KAAK,EACL,SAAS,EACT,UAAU,GACX,EAAE;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAC3C,GAAG,OAAO,CAGV;AAED,KAAK,YAAY,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,qCAAqC,GAAG;IAClD,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACpC;;;OAGG;IACH,gBAAgB,EAAE,MAAM,QAAQ,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACjF;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,oCAAoC,CAAC,OAAO,EAAE,qCAAqC;sBA2EhF,OAAO;aAGb,IAAI;YAMC,OAAO,CAAC,IAAI,CAAC;IAM3B,iEAAiE;gBA1BlD,OAAO,CAAC,IAAI,CAAC;EA6B/B;AAED,MAAM,MAAM,8BAA8B,GAAG,UAAU,CAAC,OAAO,oCAAoC,CAAC,CAAC"}
|
|
@@ -4,26 +4,33 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
4
4
|
//#region src/utils/notification-dispatch.ts
|
|
5
5
|
const DEFAULT_INTERVAL_MS = 1e4;
|
|
6
6
|
const DEFAULT_LEASE_TTL_MS = 6e4;
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The resource the current async context is dispatching notifications for,
|
|
9
|
+
* and which of its dispatch leases this pass holds.
|
|
10
|
+
*/
|
|
11
|
+
const dispatching = new AsyncLocalStorage();
|
|
9
12
|
/**
|
|
10
13
|
* Whether this call runs inside this process's own dispatch pass for
|
|
11
|
-
* `resourceId`,
|
|
12
|
-
*
|
|
14
|
+
* `resourceId`, holding the lease for threads of `scope` (or with no `scope`,
|
|
15
|
+
* for threads that carry none), as opposed to the dispatch schedule every
|
|
16
|
+
* process sharing the database takes turns running.
|
|
13
17
|
*/
|
|
14
|
-
function isDispatchingNotificationsFor(resourceId) {
|
|
15
|
-
|
|
18
|
+
function isDispatchingNotificationsFor(resourceId, scope) {
|
|
19
|
+
const pass = dispatching.getStore();
|
|
20
|
+
if (pass?.resourceId !== resourceId) return false;
|
|
21
|
+
return scope === void 0 ? pass.unscoped : pass.scopes.has(scope);
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Whether a delivery-time policy should leave a due notification pending
|
|
19
25
|
* (`hold`). Deliveries happen from this process's own dispatch pass for the
|
|
20
|
-
* record's resource
|
|
21
|
-
*
|
|
22
|
-
* dispatcher runs, its own resources are
|
|
23
|
-
* their dispatch lease, never by the
|
|
26
|
+
* record's resource, under the lease that covers the record's thread. Anything
|
|
27
|
+
* else holds: a thread no session here owns belongs to another process sharing
|
|
28
|
+
* the database, and once this process's dispatcher runs, its own resources are
|
|
29
|
+
* delivered by whichever process holds their dispatch lease, never by the
|
|
30
|
+
* shared dispatch schedule.
|
|
24
31
|
*/
|
|
25
|
-
function shouldHoldNotificationDelivery({ resourceId, ownedHere, dispatcher }) {
|
|
26
|
-
if (isDispatchingNotificationsFor(resourceId)) return false;
|
|
32
|
+
function shouldHoldNotificationDelivery({ resourceId, scope, ownedHere, dispatcher }) {
|
|
33
|
+
if (isDispatchingNotificationsFor(resourceId, scope)) return false;
|
|
27
34
|
return !ownedHere || dispatcher.running;
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
@@ -39,19 +46,20 @@ function shouldHoldNotificationDelivery({ resourceId, ownedHere, dispatcher }) {
|
|
|
39
46
|
function createResourceNotificationDispatcher(options) {
|
|
40
47
|
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
41
48
|
const leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
49
|
+
/** The dispatch lease keys this process holds. */
|
|
42
50
|
const held = /* @__PURE__ */ new Set();
|
|
43
51
|
let timer;
|
|
44
52
|
let ticking;
|
|
45
|
-
const release = async (
|
|
46
|
-
held.delete(
|
|
47
|
-
await options.leases?.releaseLease(
|
|
53
|
+
const release = async (key) => {
|
|
54
|
+
held.delete(key);
|
|
55
|
+
await options.leases?.releaseLease(key, options.owner).catch(options.onError);
|
|
48
56
|
};
|
|
49
|
-
const claim = async (
|
|
57
|
+
const claim = async (key) => {
|
|
50
58
|
if (!options.leases) return true;
|
|
51
59
|
try {
|
|
52
|
-
const { acquired } = await options.leases.acquireLease(
|
|
53
|
-
if (acquired) held.add(
|
|
54
|
-
else held.delete(
|
|
60
|
+
const { acquired } = await options.leases.acquireLease(key, options.owner, leaseTtlMs);
|
|
61
|
+
if (acquired) held.add(key);
|
|
62
|
+
else held.delete(key);
|
|
55
63
|
return acquired;
|
|
56
64
|
} catch (error) {
|
|
57
65
|
options.onError?.(error);
|
|
@@ -62,12 +70,26 @@ function createResourceNotificationDispatcher(options) {
|
|
|
62
70
|
const mastra = options.getMastra();
|
|
63
71
|
const storage = await mastra?.getStorage()?.getStore("notifications");
|
|
64
72
|
if (!mastra || !storage) return;
|
|
65
|
-
const
|
|
66
|
-
for (const resourceId of
|
|
67
|
-
|
|
68
|
-
if (
|
|
73
|
+
const scopesByResource = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const { resourceId, scope } of await options.getSessionScopes()) {
|
|
75
|
+
const scopes = scopesByResource.get(resourceId) ?? /* @__PURE__ */ new Set();
|
|
76
|
+
if (scope !== void 0) scopes.add(scope);
|
|
77
|
+
scopesByResource.set(resourceId, scopes);
|
|
78
|
+
}
|
|
79
|
+
const wanted = new Set([...scopesByResource].flatMap(([resourceId, scopes]) => [notificationDispatchLeaseKey(resourceId), ...[...scopes].map((scope) => notificationDispatchLeaseKey(resourceId, scope))]));
|
|
80
|
+
for (const key of [...held]) if (!wanted.has(key)) await release(key);
|
|
81
|
+
for (const [resourceId, scopes] of scopesByResource) {
|
|
82
|
+
if (!timer) continue;
|
|
83
|
+
const unscoped = await claim(notificationDispatchLeaseKey(resourceId));
|
|
84
|
+
const heldScopes = /* @__PURE__ */ new Set();
|
|
85
|
+
for (const scope of scopes) if (await claim(notificationDispatchLeaseKey(resourceId, scope))) heldScopes.add(scope);
|
|
86
|
+
if (!unscoped && heldScopes.size === 0) continue;
|
|
69
87
|
try {
|
|
70
|
-
await
|
|
88
|
+
await dispatching.run({
|
|
89
|
+
resourceId,
|
|
90
|
+
unscoped,
|
|
91
|
+
scopes: heldScopes
|
|
92
|
+
}, () => dispatchDueNotifications({
|
|
71
93
|
mastra,
|
|
72
94
|
storage,
|
|
73
95
|
resourceId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-dispatch.js","names":[],"sources":["../../src/utils/notification-dispatch.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\n\nimport type { LeaseProvider } from '@mastra/core/events';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { dispatchDueNotifications } from '@mastra/core/notifications';\n\nimport { notificationDispatchLeaseKey } from './signals-pubsub.js';\n\nconst DEFAULT_INTERVAL_MS = 10_000;\nconst DEFAULT_LEASE_TTL_MS = 60_000;\n\n
|
|
1
|
+
{"version":3,"file":"notification-dispatch.js","names":[],"sources":["../../src/utils/notification-dispatch.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\n\nimport type { LeaseProvider } from '@mastra/core/events';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { dispatchDueNotifications } from '@mastra/core/notifications';\n\nimport { notificationDispatchLeaseKey } from './signals-pubsub.js';\n\nconst DEFAULT_INTERVAL_MS = 10_000;\nconst DEFAULT_LEASE_TTL_MS = 60_000;\n\n/**\n * The resource the current async context is dispatching notifications for,\n * and which of its dispatch leases this pass holds.\n */\nconst dispatching = new AsyncLocalStorage<{\n resourceId: string;\n /** The resource's own lease: its threads that carry no scope. */\n unscoped: boolean;\n /** The thread scopes whose leases this pass holds. */\n scopes: ReadonlySet<string>;\n}>();\n\n/**\n * Whether this call runs inside this process's own dispatch pass for\n * `resourceId`, holding the lease for threads of `scope` (or with no `scope`,\n * for threads that carry none), as opposed to the dispatch schedule every\n * process sharing the database takes turns running.\n */\nexport function isDispatchingNotificationsFor(resourceId: string, scope?: string): boolean {\n const pass = dispatching.getStore();\n if (pass?.resourceId !== resourceId) return false;\n return scope === undefined ? pass.unscoped : pass.scopes.has(scope);\n}\n\n/**\n * Whether a delivery-time policy should leave a due notification pending\n * (`hold`). Deliveries happen from this process's own dispatch pass for the\n * record's resource, under the lease that covers the record's thread. Anything\n * else holds: a thread no session here owns belongs to another process sharing\n * the database, and once this process's dispatcher runs, its own resources are\n * delivered by whichever process holds their dispatch lease, never by the\n * shared dispatch schedule.\n */\nexport function shouldHoldNotificationDelivery({\n resourceId,\n scope,\n ownedHere,\n dispatcher,\n}: {\n resourceId: string;\n /** The thread's scope (`WakeSession.scope`), unset for a thread that carries none. */\n scope?: string;\n /** Whether a session in this process owns the resource. */\n ownedHere: boolean;\n dispatcher: { readonly running: boolean };\n}): boolean {\n if (isDispatchingNotificationsFor(resourceId, scope)) return false;\n return !ownedHere || dispatcher.running;\n}\n\ntype SessionScope = { resourceId: string; scope?: string };\n\nexport type ResourceNotificationDispatcherOptions = {\n getMastra: () => Mastra | undefined;\n /**\n * The resources this process runs sessions for, each with the thread scope\n * (`threadScopeKey`) of a session serving it, unset when it has none.\n */\n getSessionScopes: () => Iterable<SessionScope> | Promise<Iterable<SessionScope>>;\n /**\n * Cross-process leases. When set, a resource's notifications are dispatched\n * only by the processes holding its dispatch leases, so two processes\n * serving the same resource never both deliver one: the threads of each\n * scope (each project, for processes in different directories) go to the\n * process holding that scope's lease, and threads with no scope to the one\n * holding the resource's own.\n */\n leases?: LeaseProvider;\n /** Unique to this process. */\n owner: string;\n intervalMs?: number;\n leaseTtlMs?: number;\n onError?: (error: unknown) => void;\n};\n\n/**\n * Delivers the due notifications of the resources this process serves, on a\n * timer of its own.\n *\n * Every Mastra Code process shares one database, so one notification store and\n * one dispatch schedule, and the scheduler gives each fire to whichever process\n * claims it first, usually the same one for as long as it runs. That process\n * cannot run another project's thread, so its delivery policy holds those\n * records. Each process therefore dispatches its own resources here.\n */\nexport function createResourceNotificationDispatcher(options: ResourceNotificationDispatcherOptions) {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;\n /** The dispatch lease keys this process holds. */\n const held = new Set<string>();\n let timer: ReturnType<typeof setInterval> | undefined;\n let ticking: Promise<void> | undefined;\n\n const release = async (key: string) => {\n held.delete(key);\n await options.leases?.releaseLease(key, options.owner).catch(options.onError);\n };\n\n const claim = async (key: string): Promise<boolean> => {\n if (!options.leases) return true;\n try {\n // Acquiring a lease this process already holds renews it.\n const { acquired } = await options.leases.acquireLease(key, options.owner, leaseTtlMs);\n if (acquired) held.add(key);\n else held.delete(key);\n return acquired;\n } catch (error) {\n options.onError?.(error);\n return false;\n }\n };\n\n const runTick = async () => {\n const mastra = options.getMastra();\n const storage = await mastra?.getStorage()?.getStore('notifications');\n if (!mastra || !storage) return;\n const scopesByResource = new Map<string, Set<string>>();\n for (const { resourceId, scope } of await options.getSessionScopes()) {\n const scopes = scopesByResource.get(resourceId) ?? new Set<string>();\n if (scope !== undefined) scopes.add(scope);\n scopesByResource.set(resourceId, scopes);\n }\n const wanted = new Set(\n [...scopesByResource].flatMap(([resourceId, scopes]) => [\n notificationDispatchLeaseKey(resourceId),\n ...[...scopes].map(scope => notificationDispatchLeaseKey(resourceId, scope)),\n ]),\n );\n for (const key of [...held]) {\n if (!wanted.has(key)) await release(key);\n }\n for (const [resourceId, scopes] of scopesByResource) {\n if (!timer) continue;\n const unscoped = await claim(notificationDispatchLeaseKey(resourceId));\n const heldScopes = new Set<string>();\n for (const scope of scopes) {\n if (await claim(notificationDispatchLeaseKey(resourceId, scope))) heldScopes.add(scope);\n }\n if (!unscoped && heldScopes.size === 0) continue;\n try {\n await dispatching.run({ resourceId, unscoped, scopes: heldScopes }, () =>\n dispatchDueNotifications({ mastra, storage, resourceId }),\n );\n } catch (error) {\n options.onError?.(error);\n }\n }\n };\n\n const tick = (): Promise<void> => {\n // Ticks run from a timer, so a failure must be reported, never left as an unhandled rejection.\n ticking ??= runTick()\n .catch(error => options.onError?.(error))\n .finally(() => {\n ticking = undefined;\n });\n return ticking;\n };\n\n return {\n get running(): boolean {\n return timer !== undefined;\n },\n start(): void {\n if (timer) return;\n timer = setInterval(() => void tick(), intervalMs);\n timer.unref?.();\n void tick();\n },\n async stop(): Promise<void> {\n if (timer) clearInterval(timer);\n timer = undefined;\n await ticking;\n await Promise.all([...held].map(release));\n },\n /** Runs one dispatch pass now. Only dispatches while started. */\n tick,\n };\n}\n\nexport type ResourceNotificationDispatcher = ReturnType<typeof createResourceNotificationDispatcher>;\n"],"mappings":";;;;AAQA,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;;;;;AAM7B,MAAM,cAAc,IAAI,kBAMrB;;;;;;;AAQH,SAAgB,8BAA8B,YAAoB,OAAyB;CACzF,MAAM,OAAO,YAAY,SAAS;CAClC,IAAI,MAAM,eAAe,YAAY,OAAO;CAC5C,OAAO,UAAU,KAAA,IAAY,KAAK,WAAW,KAAK,OAAO,IAAI,KAAK;AACpE;;;;;;;;;;AAWA,SAAgB,+BAA+B,EAC7C,YACA,OACA,WACA,cAQU;CACV,IAAI,8BAA8B,YAAY,KAAK,GAAG,OAAO;CAC7D,OAAO,CAAC,aAAa,WAAW;AAClC;;;;;;;;;;;AAqCA,SAAgB,qCAAqC,SAAgD;CACnG,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,aAAa,QAAQ,cAAc;;CAEzC,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI;CACJ,IAAI;CAEJ,MAAM,UAAU,OAAO,QAAgB;EACrC,KAAK,OAAO,GAAG;EACf,MAAM,QAAQ,QAAQ,aAAa,KAAK,QAAQ,KAAK,CAAC,CAAC,MAAM,QAAQ,OAAO;CAC9E;CAEA,MAAM,QAAQ,OAAO,QAAkC;EACrD,IAAI,CAAC,QAAQ,QAAQ,OAAO;EAC5B,IAAI;GAEF,MAAM,EAAE,aAAa,MAAM,QAAQ,OAAO,aAAa,KAAK,QAAQ,OAAO,UAAU;GACrF,IAAI,UAAU,KAAK,IAAI,GAAG;QACrB,KAAK,OAAO,GAAG;GACpB,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,UAAU,KAAK;GACvB,OAAO;EACT;CACF;CAEA,MAAM,UAAU,YAAY;EAC1B,MAAM,SAAS,QAAQ,UAAU;EACjC,MAAM,UAAU,MAAM,QAAQ,WAAW,CAAC,EAAE,SAAS,eAAe;EACpE,IAAI,CAAC,UAAU,CAAC,SAAS;EACzB,MAAM,mCAAmB,IAAI,IAAyB;EACtD,KAAK,MAAM,EAAE,YAAY,WAAW,MAAM,QAAQ,iBAAiB,GAAG;GACpE,MAAM,SAAS,iBAAiB,IAAI,UAAU,qBAAK,IAAI,IAAY;GACnE,IAAI,UAAU,KAAA,GAAW,OAAO,IAAI,KAAK;GACzC,iBAAiB,IAAI,YAAY,MAAM;EACzC;EACA,MAAM,SAAS,IAAI,IACjB,CAAC,GAAG,gBAAgB,CAAC,CAAC,SAAS,CAAC,YAAY,YAAY,CACtD,6BAA6B,UAAU,GACvC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,KAAI,UAAS,6BAA6B,YAAY,KAAK,CAAC,CAC7E,CAAC,CACH;EACA,KAAK,MAAM,OAAO,CAAC,GAAG,IAAI,GACxB,IAAI,CAAC,OAAO,IAAI,GAAG,GAAG,MAAM,QAAQ,GAAG;EAEzC,KAAK,MAAM,CAAC,YAAY,WAAW,kBAAkB;GACnD,IAAI,CAAC,OAAO;GACZ,MAAM,WAAW,MAAM,MAAM,6BAA6B,UAAU,CAAC;GACrE,MAAM,6BAAa,IAAI,IAAY;GACnC,KAAK,MAAM,SAAS,QAClB,IAAI,MAAM,MAAM,6BAA6B,YAAY,KAAK,CAAC,GAAG,WAAW,IAAI,KAAK;GAExF,IAAI,CAAC,YAAY,WAAW,SAAS,GAAG;GACxC,IAAI;IACF,MAAM,YAAY,IAAI;KAAE;KAAY;KAAU,QAAQ;IAAW,SAC/D,yBAAyB;KAAE;KAAQ;KAAS;IAAW,CAAC,CAC1D;GACF,SAAS,OAAO;IACd,QAAQ,UAAU,KAAK;GACzB;EACF;CACF;CAEA,MAAM,aAA4B;EAEhC,YAAY,QAAQ,CAAC,CAClB,OAAM,UAAS,QAAQ,UAAU,KAAK,CAAC,CAAC,CACxC,cAAc;GACb,UAAU,KAAA;EACZ,CAAC;EACH,OAAO;CACT;CAEA,OAAO;EACL,IAAI,UAAmB;GACrB,OAAO,UAAU,KAAA;EACnB;EACA,QAAc;GACZ,IAAI,OAAO;GACX,QAAQ,kBAAkB,KAAK,KAAK,GAAG,UAAU;GACjD,MAAM,QAAQ;GACd,KAAU;EACZ;EACA,MAAM,OAAsB;GAC1B,IAAI,OAAO,cAAc,KAAK;GAC9B,QAAQ,KAAA;GACR,MAAM;GACN,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC;EAC1C;;EAEA;CACF;AACF"}
|
|
@@ -15,10 +15,12 @@ export type SignalsPubSubOptions = {
|
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* The lease that lets exactly one process dispatch a resource's due
|
|
18
|
-
* notifications
|
|
19
|
-
*
|
|
18
|
+
* notifications: those of the resource's threads that carry `scope` (a
|
|
19
|
+
* `threadScopeKey`, one project's threads), or with no `scope`, those of its
|
|
20
|
+
* threads that carry none. Held in that resource's directory, so every process
|
|
21
|
+
* serving the resource contends for it whichever project it was started in.
|
|
20
22
|
*/
|
|
21
|
-
export declare function notificationDispatchLeaseKey(resourceId: string): string;
|
|
23
|
+
export declare function notificationDispatchLeaseKey(resourceId: string, scope?: string): string;
|
|
22
24
|
/**
|
|
23
25
|
* A PubSub that manages one Unix socket per topic for cross-process signal
|
|
24
26
|
* coordination between mastracode processes on this machine.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signals-pubsub.d.ts","sourceRoot":"","sources":["../../src/utils/signals-pubsub.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAyBrH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAsCF
|
|
1
|
+
{"version":3,"file":"signals-pubsub.d.ts","sourceRoot":"","sources":["../../src/utils/signals-pubsub.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAyBrH,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAsCF;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAEvF;AA0DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,cAAM,aAAc,SAAQ,MAAM;;gBAcpB,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB;IAsBlE,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;IAED;;;;OAIG;IACH,gBAAgB,IAAI,aAAa;IAI3B,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAIV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE;;;;;OAKG;IACY,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAc5B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAoMvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAErG"}
|
|
@@ -57,19 +57,21 @@ function decodeThreadTopic(topic) {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* The lease that lets exactly one process dispatch a resource's due
|
|
60
|
-
* notifications
|
|
61
|
-
*
|
|
60
|
+
* notifications: those of the resource's threads that carry `scope` (a
|
|
61
|
+
* `threadScopeKey`, one project's threads), or with no `scope`, those of its
|
|
62
|
+
* threads that carry none. Held in that resource's directory, so every process
|
|
63
|
+
* serving the resource contends for it whichever project it was started in.
|
|
62
64
|
*/
|
|
63
|
-
function notificationDispatchLeaseKey(resourceId) {
|
|
64
|
-
return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}`;
|
|
65
|
+
function notificationDispatchLeaseKey(resourceId, scope) {
|
|
66
|
+
return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}${scope === void 0 ? "" : `${THREAD_KEY_SEPARATOR}${scope}`}`;
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
69
|
* The resource a lease key belongs to: a thread's run lease and claim lease,
|
|
68
|
-
* and a resource's notification dispatch
|
|
70
|
+
* and a resource's notification dispatch leases.
|
|
69
71
|
*/
|
|
70
72
|
function resourceOfLeaseKey(key) {
|
|
71
73
|
if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {
|
|
72
|
-
const resourceId = key.slice(22);
|
|
74
|
+
const [resourceId = ""] = key.slice(22).split(THREAD_KEY_SEPARATOR, 1);
|
|
73
75
|
return isSafePathSegment(resourceId) ? resourceId : void 0;
|
|
74
76
|
}
|
|
75
77
|
return resourceOfThreadKey(key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(13) : key);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signals-pubsub.js","names":["#resourceId","#rootDir","#leaseSockets","#sockets","#pending","#refs","#liveSubscriptions","#clearGenerations","#closing","#leaseProvider","#sharedPeerDiscovery","#canShareDiscovery","#leasesFor","#closed","#publishTo","#dirFor","#subscribeTo","#unsubscribeFrom","#release","#isRefCounted","#getOrCreate","#retain","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\n\nimport { PubSub, UnixSocketPubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\n\nconst DEFAULT_SOCKET_ROOT = '/tmp/mc';\nconst SHARED_SCOPE_DIR = '_shared';\nconst THREAD_STREAM_PREFIX = 'agent.thread-stream.';\nconst THREAD_KEY_SEPARATOR = '\\0';\nconst THREAD_CLAIM_LEASE_PREFIX = 'thread-claim:';\nconst NOTIFICATION_DISPATCH_LEASE_PREFIX = 'notification-dispatch:';\nconst LEASE_SOCKET_NAME = '.leases.sock';\nconst OWNER_DISCOVERY_TOPIC = 'agent.thread-owner-discovery';\nconst PEER_DISCOVERY_TOPIC = 'agent.thread-peer-discovery';\nconst RESERVED_SOCKET_NAME = /^agent_thread-(?:peer|owner)-discovery(?:_|$)/i;\nconst MAX_PATH_SEGMENT_LENGTH = 128;\nconst UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';\nconst DISCOVERY_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-(?:peer|owner)-discovery\\\\.${UUID_PATTERN}$`, 'i');\nconst IDLE_ACCEPTANCE_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-stream\\\\..+\\\\.idle-acceptance\\\\.${UUID_PATTERN}$`, 'i');\n\n/**\n * One-shot reply topics are minted per request (`<topic>.<uuid>`), used for a\n * single round trip, and never published to again once the request settles.\n */\nfunction isEphemeralTopic(topic: string): boolean {\n return DISCOVERY_REPLY_TOPIC.test(topic) || IDLE_ACCEPTANCE_REPLY_TOPIC.test(topic);\n}\n\nexport type SignalsPubSubOptions = {\n /**\n * Also list this process's threads to, and find threads in, other projects on\n * this machine: peer discovery runs in `<root>/_shared/` instead of this\n * resource's directory.\n */\n sharedAgentDiscovery?: boolean;\n /**\n * Directory that holds every resource's socket directory. Defaults to the\n * absolute path in `MASTRACODE_SIGNALS_SOCKET_ROOT`, else `/tmp/mc`.\n */\n rootDir?: string;\n};\n\nfunction socketRootFromEnv(): string {\n const root = process.env.MASTRACODE_SIGNALS_SOCKET_ROOT;\n return root && isAbsolute(root) ? root : DEFAULT_SOCKET_ROOT;\n}\n\n/**\n * Whether an id can name a single directory: no path separators or control\n * characters, not `.`/`..`, and bounded in length.\n */\nfunction isSafePathSegment(value: string): boolean {\n if (!value || value.length > MAX_PATH_SEGMENT_LENGTH || value === '.' || value === '..') return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/** The resource a thread key (`<resourceId>\\0<threadId>`) belongs to. */\nfunction resourceOfThreadKey(key: string): string | undefined {\n const separatorIdx = key.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx <= 0) return undefined;\n const resourceId = key.slice(0, separatorIdx);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n}\n\n/** Decodes `agent.thread-stream.<encoded resourceId\\0threadId>[...]`. */\nfunction decodeThreadTopic(topic: string): string | undefined {\n if (!topic.startsWith(THREAD_STREAM_PREFIX)) return undefined;\n try {\n return decodeURIComponent(topic.slice(THREAD_STREAM_PREFIX.length));\n } catch {\n return undefined;\n }\n}\n\n/**\n * The lease that lets exactly one process dispatch a resource's due\n * notifications. Held in that resource's directory, so every process serving\n * the resource contends for it whichever project it was started in.\n */\nexport function notificationDispatchLeaseKey(resourceId: string): string {\n return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}`;\n}\n\n/**\n * The resource a lease key belongs to: a thread's run lease and claim lease,\n * and a resource's notification dispatch lease.\n */\nfunction resourceOfLeaseKey(key: string): string | undefined {\n if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {\n const resourceId = key.slice(NOTIFICATION_DISPATCH_LEASE_PREFIX.length);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n }\n return resourceOfThreadKey(\n key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(THREAD_CLAIM_LEASE_PREFIX.length) : key,\n );\n}\n\nfunction isOwnerDiscoveryTopic(topic: string): boolean {\n return topic === OWNER_DISCOVERY_TOPIC || topic.startsWith(`${OWNER_DISCOVERY_TOPIC}.`);\n}\n\nfunction isPeerDiscoveryTopic(topic: string): boolean {\n return topic === PEER_DISCOVERY_TOPIC || topic.startsWith(`${PEER_DISCOVERY_TOPIC}.`);\n}\n\n/**\n * Whether a threadId can be the base name of a thread's socket file: no\n * separators or control characters, not `.`/`..`, and not a name another\n * socket in the same directory already uses (the lease socket, and the\n * sanitized peer/owner discovery topics and their reply topics), compared\n * case-insensitively because default macOS and Windows filesystems are.\n */\nfunction isSafeFileName(value: string): boolean {\n if (!value || value === '.' || value === '..' || `${value}.sock`.toLowerCase() === LEASE_SOCKET_NAME) return false;\n if (RESERVED_SOCKET_NAME.test(value)) return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/**\n * Derive a filesystem-safe key for the topic. Thread-stream topics embed\n * a threadId; all other topics use a sanitized version of the topic name.\n */\nfunction topicKey(topic: string): string {\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) {\n const separatorIdx = decoded.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx !== -1) return decoded.slice(separatorIdx + 1);\n }\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n}\n\nfunction socketKey(dir: string, topic: string): string {\n return `${dir}/${topicKey(topic)}`;\n}\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination between mastracode processes on this machine.\n *\n * Socket paths use `<root>/<dir>/<sanitized-topic>.sock` (root `/tmp/mc`)\n * for inspectability and automatic OS cleanup. Each topic gets its own\n * socket, so broker election and message routing are per-topic.\n *\n * Every process shares one database, so any process can act on any project's\n * thread — the notification dispatcher, for one, delivers whatever is due.\n * Whatever coordinates a thread therefore lives with the thread, not with the\n * process touching it:\n *\n * - A thread's stream topic and its leases (the run lease and the claim\n * lease) live in the directory of the resource that owns the thread, so a\n * process from another project sees that thread's active run and claim.\n * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds\n * a thread claimed can reach the owner and hand it the signal. It only\n * answers for a thread the asker names, so it discloses nothing new.\n * - Peer discovery (which lists threads) runs in `<root>/_shared/` when\n * `sharedAgentDiscovery` is set, so processes in every project can list each\n * other's threads. Without it, it stays in this resource's directory.\n * - Everything else stays in this process's own resource directory.\n *\n * Stale sockets from crashed processes are handled by\n * {@link UnixSocketPubSub}'s built-in election logic: it detects\n * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.\n * No blanket cleanup is needed here — that would break concurrent\n * mc instances sharing a directory.\n *\n * One-shot reply topics (discovery and idle-acceptance replies) are\n * reference-counted: every in-flight publish/subscribe and every live\n * subscription holds a reference, and the topic's socket is closed as soon as\n * the count drops to zero. Without this each request would keep a socket, a\n * file descriptor and (for the broker) a socket file for the process lifetime.\n * With `sharedAgentDiscovery`, another resource's thread topics are counted the\n * same way, so a process that signals many threads elsewhere does not keep a\n * socket open for each.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #rootDir: string;\n readonly #leaseSockets = new Map<string, UnixSocketPubSub>();\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n readonly #refs = new Map<string, number>();\n readonly #liveSubscriptions = new Map<string, Set<EventCallback>>();\n readonly #clearGenerations = new Map<string, number>();\n readonly #closing = new Map<string, Promise<void>>();\n readonly #leaseProvider: LeaseProvider;\n readonly #sharedPeerDiscovery: boolean;\n #closed = false;\n\n constructor(resourceId: string, options: SignalsPubSubOptions = {}) {\n super();\n this.#resourceId = resourceId;\n this.#rootDir = options.rootDir ?? socketRootFromEnv();\n this.#sharedPeerDiscovery = Boolean(options.sharedAgentDiscovery) && this.#canShareDiscovery();\n // Created eagerly, as before, so this resource's lease socket exists from the start.\n this.#leasesFor(resourceId);\n const route = (key: string) => {\n // Lease sockets are created per resource on demand; once closed, never\n // reopen one (a lease renewal timer may outlive close()).\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);\n };\n this.#leaseProvider = {\n acquireLease: async (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),\n getLeaseOwner: async key => route(key).getLeaseOwner(key),\n releaseLease: async (key, owner) => route(key).releaseLease(key, owner),\n renewLease: async (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),\n transferLease: async (key, fromOwner, toOwner, ttlMs) => route(key).transferLease(key, fromOwner, toOwner, ttlMs),\n };\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /**\n * Filesystem leases. A thread's run and claim leases are held in the\n * directory of the thread's resource, so every process contends for the\n * same lease; any other key stays in this resource's directory.\n */\n getLeaseProvider(): LeaseProvider {\n return this.#leaseProvider;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n await this.#publishTo(topic, this.#dirFor(topic), event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n await this.#subscribeTo(topic, this.#dirFor(topic), cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n await this.#unsubscribeFrom(topic, this.#dirFor(topic), cb);\n }\n\n /**\n * Drops a one-shot reply topic: its remaining subscriptions release their\n * references and the socket closes once no operation still uses it. Other\n * topics (workflow events, thread streams) have subscribers in other\n * processes that need the broker, so clearing them is a no-op.\n */\n override async clearTopic(topic: string): Promise<void> {\n if (!isEphemeralTopic(topic)) return;\n const key = socketKey(this.#dirFor(topic), topic);\n if (!this.#refs.has(key)) return;\n this.#clearGenerations.set(key, (this.#clearGenerations.get(key) ?? 0) + 1);\n const subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) return;\n this.#liveSubscriptions.delete(key);\n for (let i = 0; i < subscriptions.size; i++) this.#release(key);\n }\n\n async flush(): Promise<void> {\n await Promise.all([...this.#sockets.values()].map(s => s.flush()));\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n await Promise.allSettled([\n ...[...this.#leaseSockets.values()].map(s => s.close()),\n ...[...this.#sockets.values()].map(s => s.close()),\n ...this.#closing.values(),\n ]);\n this.#sockets.clear();\n this.#leaseSockets.clear();\n this.#refs.clear();\n this.#liveSubscriptions.clear();\n this.#clearGenerations.clear();\n }\n\n /** Get the underlying socket for a topic (for testing/inspection). */\n getSocket(topic: string): UnixSocketPubSub | undefined {\n return this.#sockets.get(socketKey(this.#dirFor(topic), topic));\n }\n\n #canShareDiscovery(): boolean {\n if (isSafePathSegment(this.#resourceId) && this.#resourceId !== SHARED_SCOPE_DIR) return true;\n console.warn(\n `Cross-project agent discovery is disabled: resource id ${JSON.stringify(this.#resourceId)} cannot be used as a directory name, so other projects could not reach this instance.`,\n );\n return false;\n }\n\n /**\n * One-shot reply topics close once unused. With shared discovery, so do thread\n * topics routed to another resource; without it, they behave as before.\n */\n #isRefCounted(topic: string): boolean {\n if (isEphemeralTopic(topic)) return true;\n if (!this.#sharedPeerDiscovery) return false;\n const decoded = decodeThreadTopic(topic);\n if (decoded === undefined) return false;\n const resourceId = resourceOfThreadKey(decoded);\n return resourceId !== undefined && resourceId !== this.#resourceId;\n }\n\n async #publishTo(\n topic: string,\n dir: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n return;\n }\n const key = socketKey(dir, topic);\n // Retain synchronously so a concurrent release cannot close the socket\n // this publish is about to use.\n this.#retain(key);\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n } finally {\n this.#release(key);\n }\n }\n\n async #subscribeTo(topic: string, dir: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n return;\n }\n const key = socketKey(dir, topic);\n this.#retain(key);\n const generation = this.#clearGenerations.get(key) ?? 0;\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n if ((this.#clearGenerations.get(key) ?? 0) !== generation) {\n // The topic was cleared while this subscribe was in flight.\n await socket.unsubscribe(topic, cb);\n return;\n }\n let subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) {\n subscriptions = new Set();\n this.#liveSubscriptions.set(key, subscriptions);\n }\n if (!subscriptions.has(cb)) {\n subscriptions.add(cb);\n this.#retain(key);\n }\n } finally {\n this.#release(key);\n }\n }\n\n async #unsubscribeFrom(topic: string, dir: string, cb: EventCallback): Promise<void> {\n const key = socketKey(dir, topic);\n const socket = this.#sockets.get(key);\n const subscriptions = this.#isRefCounted(topic) ? this.#liveSubscriptions.get(key) : undefined;\n if (!subscriptions?.delete(cb)) {\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\n return;\n }\n if (subscriptions.size === 0) this.#liveSubscriptions.delete(key);\n try {\n await socket?.unsubscribe(topic, cb);\n } finally {\n this.#release(key);\n }\n }\n\n #leasesFor(resourceId: string): UnixSocketPubSub {\n let socket = this.#leaseSockets.get(resourceId);\n if (!socket) {\n socket = new UnixSocketPubSub(join(this.#rootDir, resourceId, LEASE_SOCKET_NAME));\n this.#leaseSockets.set(resourceId, socket);\n }\n return socket;\n }\n\n /** The directory, under the root, that holds a topic's socket. */\n #dirFor(topic: string): string {\n if (isOwnerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n if (this.#sharedPeerDiscovery && isPeerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) return resourceOfThreadKey(decoded) ?? this.#resourceId;\n return this.#resourceId;\n }\n\n #retain(key: string): void {\n this.#refs.set(key, (this.#refs.get(key) ?? 0) + 1);\n }\n\n #release(key: string): void {\n const refs = (this.#refs.get(key) ?? 0) - 1;\n if (refs > 0) {\n this.#refs.set(key, refs);\n return;\n }\n this.#refs.delete(key);\n this.#clearGenerations.delete(key);\n this.#liveSubscriptions.delete(key);\n const socket = this.#sockets.get(key);\n if (!socket) return;\n this.#sockets.delete(key);\n const closing: Promise<void> = socket\n .close()\n .catch(() => {})\n .finally(() => {\n if (this.#closing.get(key) === closing) this.#closing.delete(key);\n });\n this.#closing.set(key, closing);\n }\n\n async #getOrCreate(topic: string, dir: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = socketKey(dir, topic);\n // A socket for this key may still be closing after its last reference\n // was released; never hand out the closing instance.\n const closing = this.#closing.get(key);\n if (closing) {\n await closing;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n }\n const existing = this.#sockets.get(key);\n if (existing) return existing;\n // Deduplicate concurrent callers so only one socket is created per topic.\n let inflight = this.#pending.get(key);\n if (!inflight) {\n inflight = this.#initSocket(topic, dir, key);\n this.#pending.set(key, inflight);\n }\n const socket = await inflight;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return socket;\n }\n\n async #initSocket(topic: string, dir: string, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic, dir);\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const socket = new UnixSocketPubSub(socketPath);\n this.#sockets.set(key, socket);\n return socket;\n } finally {\n this.#pending.delete(key);\n }\n }\n\n async #socketPath(topic: string, scope: string): Promise<string> {\n let key = topicKey(topic);\n // A threadId names a file in its resource's directory, so never let it\n // reach outside that directory, regardless of who owns the thread or\n // whether cross-project discovery is enabled.\n if (decodeThreadTopic(topic) !== undefined && !isSafeFileName(key)) {\n throw new Error('Cannot route an agent thread topic whose threadId is not a safe file name');\n }\n const dir = join(this.#rootDir, scope);\n await mkdir(dir, { recursive: true });\n const candidate = join(dir, `${key}.sock`);\n // macOS sun_path limit is 104 bytes; Linux is 108. Use 104 as the\n // conservative bound. When the path is too long, replace the key with\n // a short hash so the socket can still be created.\n if (Buffer.byteLength(candidate) > 104) {\n key = Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)))\n .toString('hex')\n .slice(0, 16);\n return join(dir, `${key}.sock`);\n }\n return candidate;\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination between mastracode processes.\n *\n * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and\n * leases live under its own resource's directory and thread-owner discovery\n * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). With\n * `sharedAgentDiscovery`, peer discovery runs there too. Stale sockets from\n * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s\n * broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string, options?: SignalsPubSubOptions): SignalsPubSub {\n return new SignalsPubSub(resourceId, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"signals-pubsub.js","names":["#resourceId","#rootDir","#leaseSockets","#sockets","#pending","#refs","#liveSubscriptions","#clearGenerations","#closing","#leaseProvider","#sharedPeerDiscovery","#canShareDiscovery","#leasesFor","#closed","#publishTo","#dirFor","#subscribeTo","#unsubscribeFrom","#release","#isRefCounted","#getOrCreate","#retain","#initSocket","#socketPath"],"sources":["../../src/utils/signals-pubsub.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\n\nimport { PubSub, UnixSocketPubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\n\nconst DEFAULT_SOCKET_ROOT = '/tmp/mc';\nconst SHARED_SCOPE_DIR = '_shared';\nconst THREAD_STREAM_PREFIX = 'agent.thread-stream.';\nconst THREAD_KEY_SEPARATOR = '\\0';\nconst THREAD_CLAIM_LEASE_PREFIX = 'thread-claim:';\nconst NOTIFICATION_DISPATCH_LEASE_PREFIX = 'notification-dispatch:';\nconst LEASE_SOCKET_NAME = '.leases.sock';\nconst OWNER_DISCOVERY_TOPIC = 'agent.thread-owner-discovery';\nconst PEER_DISCOVERY_TOPIC = 'agent.thread-peer-discovery';\nconst RESERVED_SOCKET_NAME = /^agent_thread-(?:peer|owner)-discovery(?:_|$)/i;\nconst MAX_PATH_SEGMENT_LENGTH = 128;\nconst UUID_PATTERN = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';\nconst DISCOVERY_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-(?:peer|owner)-discovery\\\\.${UUID_PATTERN}$`, 'i');\nconst IDLE_ACCEPTANCE_REPLY_TOPIC = new RegExp(`^agent\\\\.thread-stream\\\\..+\\\\.idle-acceptance\\\\.${UUID_PATTERN}$`, 'i');\n\n/**\n * One-shot reply topics are minted per request (`<topic>.<uuid>`), used for a\n * single round trip, and never published to again once the request settles.\n */\nfunction isEphemeralTopic(topic: string): boolean {\n return DISCOVERY_REPLY_TOPIC.test(topic) || IDLE_ACCEPTANCE_REPLY_TOPIC.test(topic);\n}\n\nexport type SignalsPubSubOptions = {\n /**\n * Also list this process's threads to, and find threads in, other projects on\n * this machine: peer discovery runs in `<root>/_shared/` instead of this\n * resource's directory.\n */\n sharedAgentDiscovery?: boolean;\n /**\n * Directory that holds every resource's socket directory. Defaults to the\n * absolute path in `MASTRACODE_SIGNALS_SOCKET_ROOT`, else `/tmp/mc`.\n */\n rootDir?: string;\n};\n\nfunction socketRootFromEnv(): string {\n const root = process.env.MASTRACODE_SIGNALS_SOCKET_ROOT;\n return root && isAbsolute(root) ? root : DEFAULT_SOCKET_ROOT;\n}\n\n/**\n * Whether an id can name a single directory: no path separators or control\n * characters, not `.`/`..`, and bounded in length.\n */\nfunction isSafePathSegment(value: string): boolean {\n if (!value || value.length > MAX_PATH_SEGMENT_LENGTH || value === '.' || value === '..') return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/** The resource a thread key (`<resourceId>\\0<threadId>`) belongs to. */\nfunction resourceOfThreadKey(key: string): string | undefined {\n const separatorIdx = key.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx <= 0) return undefined;\n const resourceId = key.slice(0, separatorIdx);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n}\n\n/** Decodes `agent.thread-stream.<encoded resourceId\\0threadId>[...]`. */\nfunction decodeThreadTopic(topic: string): string | undefined {\n if (!topic.startsWith(THREAD_STREAM_PREFIX)) return undefined;\n try {\n return decodeURIComponent(topic.slice(THREAD_STREAM_PREFIX.length));\n } catch {\n return undefined;\n }\n}\n\n/**\n * The lease that lets exactly one process dispatch a resource's due\n * notifications: those of the resource's threads that carry `scope` (a\n * `threadScopeKey`, one project's threads), or with no `scope`, those of its\n * threads that carry none. Held in that resource's directory, so every process\n * serving the resource contends for it whichever project it was started in.\n */\nexport function notificationDispatchLeaseKey(resourceId: string, scope?: string): string {\n return `${NOTIFICATION_DISPATCH_LEASE_PREFIX}${resourceId}${scope === undefined ? '' : `${THREAD_KEY_SEPARATOR}${scope}`}`;\n}\n\n/**\n * The resource a lease key belongs to: a thread's run lease and claim lease,\n * and a resource's notification dispatch leases.\n */\nfunction resourceOfLeaseKey(key: string): string | undefined {\n if (key.startsWith(NOTIFICATION_DISPATCH_LEASE_PREFIX)) {\n const [resourceId = ''] = key.slice(NOTIFICATION_DISPATCH_LEASE_PREFIX.length).split(THREAD_KEY_SEPARATOR, 1);\n return isSafePathSegment(resourceId) ? resourceId : undefined;\n }\n return resourceOfThreadKey(\n key.startsWith(THREAD_CLAIM_LEASE_PREFIX) ? key.slice(THREAD_CLAIM_LEASE_PREFIX.length) : key,\n );\n}\n\nfunction isOwnerDiscoveryTopic(topic: string): boolean {\n return topic === OWNER_DISCOVERY_TOPIC || topic.startsWith(`${OWNER_DISCOVERY_TOPIC}.`);\n}\n\nfunction isPeerDiscoveryTopic(topic: string): boolean {\n return topic === PEER_DISCOVERY_TOPIC || topic.startsWith(`${PEER_DISCOVERY_TOPIC}.`);\n}\n\n/**\n * Whether a threadId can be the base name of a thread's socket file: no\n * separators or control characters, not `.`/`..`, and not a name another\n * socket in the same directory already uses (the lease socket, and the\n * sanitized peer/owner discovery topics and their reply topics), compared\n * case-insensitively because default macOS and Windows filesystems are.\n */\nfunction isSafeFileName(value: string): boolean {\n if (!value || value === '.' || value === '..' || `${value}.sock`.toLowerCase() === LEASE_SOCKET_NAME) return false;\n if (RESERVED_SOCKET_NAME.test(value)) return false;\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (char === '/' || char === '\\\\' || code < 0x20 || code === 0x7f) return false;\n }\n return true;\n}\n\n/**\n * Derive a filesystem-safe key for the topic. Thread-stream topics embed\n * a threadId; all other topics use a sanitized version of the topic name.\n */\nfunction topicKey(topic: string): string {\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) {\n const separatorIdx = decoded.indexOf(THREAD_KEY_SEPARATOR);\n if (separatorIdx !== -1) return decoded.slice(separatorIdx + 1);\n }\n return topic.replace(/[^a-zA-Z0-9_-]/g, '_');\n}\n\nfunction socketKey(dir: string, topic: string): string {\n return `${dir}/${topicKey(topic)}`;\n}\n\n/**\n * A PubSub that manages one Unix socket per topic for cross-process signal\n * coordination between mastracode processes on this machine.\n *\n * Socket paths use `<root>/<dir>/<sanitized-topic>.sock` (root `/tmp/mc`)\n * for inspectability and automatic OS cleanup. Each topic gets its own\n * socket, so broker election and message routing are per-topic.\n *\n * Every process shares one database, so any process can act on any project's\n * thread — the notification dispatcher, for one, delivers whatever is due.\n * Whatever coordinates a thread therefore lives with the thread, not with the\n * process touching it:\n *\n * - A thread's stream topic and its leases (the run lease and the claim\n * lease) live in the directory of the resource that owns the thread, so a\n * process from another project sees that thread's active run and claim.\n * - Thread-owner discovery lives in `<root>/_shared/`, so a process that finds\n * a thread claimed can reach the owner and hand it the signal. It only\n * answers for a thread the asker names, so it discloses nothing new.\n * - Peer discovery (which lists threads) runs in `<root>/_shared/` when\n * `sharedAgentDiscovery` is set, so processes in every project can list each\n * other's threads. Without it, it stays in this resource's directory.\n * - Everything else stays in this process's own resource directory.\n *\n * Stale sockets from crashed processes are handled by\n * {@link UnixSocketPubSub}'s built-in election logic: it detects\n * ECONNREFUSED on a dead broker socket, unlinks it, and re-elects.\n * No blanket cleanup is needed here — that would break concurrent\n * mc instances sharing a directory.\n *\n * One-shot reply topics (discovery and idle-acceptance replies) are\n * reference-counted: every in-flight publish/subscribe and every live\n * subscription holds a reference, and the topic's socket is closed as soon as\n * the count drops to zero. Without this each request would keep a socket, a\n * file descriptor and (for the broker) a socket file for the process lifetime.\n * With `sharedAgentDiscovery`, another resource's thread topics are counted the\n * same way, so a process that signals many threads elsewhere does not keep a\n * socket open for each.\n */\nclass SignalsPubSub extends PubSub {\n readonly #resourceId: string;\n readonly #rootDir: string;\n readonly #leaseSockets = new Map<string, UnixSocketPubSub>();\n readonly #sockets = new Map<string, UnixSocketPubSub>();\n readonly #pending = new Map<string, Promise<UnixSocketPubSub>>();\n readonly #refs = new Map<string, number>();\n readonly #liveSubscriptions = new Map<string, Set<EventCallback>>();\n readonly #clearGenerations = new Map<string, number>();\n readonly #closing = new Map<string, Promise<void>>();\n readonly #leaseProvider: LeaseProvider;\n readonly #sharedPeerDiscovery: boolean;\n #closed = false;\n\n constructor(resourceId: string, options: SignalsPubSubOptions = {}) {\n super();\n this.#resourceId = resourceId;\n this.#rootDir = options.rootDir ?? socketRootFromEnv();\n this.#sharedPeerDiscovery = Boolean(options.sharedAgentDiscovery) && this.#canShareDiscovery();\n // Created eagerly, as before, so this resource's lease socket exists from the start.\n this.#leasesFor(resourceId);\n const route = (key: string) => {\n // Lease sockets are created per resource on demand; once closed, never\n // reopen one (a lease renewal timer may outlive close()).\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return this.#leasesFor(resourceOfLeaseKey(key) ?? this.#resourceId);\n };\n this.#leaseProvider = {\n acquireLease: async (key, owner, ttlMs) => route(key).acquireLease(key, owner, ttlMs),\n getLeaseOwner: async key => route(key).getLeaseOwner(key),\n releaseLease: async (key, owner) => route(key).releaseLease(key, owner),\n renewLease: async (key, owner, ttlMs) => route(key).renewLease(key, owner, ttlMs),\n transferLease: async (key, fromOwner, toOwner, ttlMs) => route(key).transferLease(key, fromOwner, toOwner, ttlMs),\n };\n }\n\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['push'];\n }\n\n /**\n * Filesystem leases. A thread's run and claim leases are held in the\n * directory of the thread's resource, so every process contends for the\n * same lease; any other key stays in this resource's directory.\n */\n getLeaseProvider(): LeaseProvider {\n return this.#leaseProvider;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n await this.#publishTo(topic, this.#dirFor(topic), event, options);\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n await this.#subscribeTo(topic, this.#dirFor(topic), cb, options);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n await this.#unsubscribeFrom(topic, this.#dirFor(topic), cb);\n }\n\n /**\n * Drops a one-shot reply topic: its remaining subscriptions release their\n * references and the socket closes once no operation still uses it. Other\n * topics (workflow events, thread streams) have subscribers in other\n * processes that need the broker, so clearing them is a no-op.\n */\n override async clearTopic(topic: string): Promise<void> {\n if (!isEphemeralTopic(topic)) return;\n const key = socketKey(this.#dirFor(topic), topic);\n if (!this.#refs.has(key)) return;\n this.#clearGenerations.set(key, (this.#clearGenerations.get(key) ?? 0) + 1);\n const subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) return;\n this.#liveSubscriptions.delete(key);\n for (let i = 0; i < subscriptions.size; i++) this.#release(key);\n }\n\n async flush(): Promise<void> {\n await Promise.all([...this.#sockets.values()].map(s => s.flush()));\n }\n\n async close(): Promise<void> {\n this.#closed = true;\n await Promise.allSettled([\n ...[...this.#leaseSockets.values()].map(s => s.close()),\n ...[...this.#sockets.values()].map(s => s.close()),\n ...this.#closing.values(),\n ]);\n this.#sockets.clear();\n this.#leaseSockets.clear();\n this.#refs.clear();\n this.#liveSubscriptions.clear();\n this.#clearGenerations.clear();\n }\n\n /** Get the underlying socket for a topic (for testing/inspection). */\n getSocket(topic: string): UnixSocketPubSub | undefined {\n return this.#sockets.get(socketKey(this.#dirFor(topic), topic));\n }\n\n #canShareDiscovery(): boolean {\n if (isSafePathSegment(this.#resourceId) && this.#resourceId !== SHARED_SCOPE_DIR) return true;\n console.warn(\n `Cross-project agent discovery is disabled: resource id ${JSON.stringify(this.#resourceId)} cannot be used as a directory name, so other projects could not reach this instance.`,\n );\n return false;\n }\n\n /**\n * One-shot reply topics close once unused. With shared discovery, so do thread\n * topics routed to another resource; without it, they behave as before.\n */\n #isRefCounted(topic: string): boolean {\n if (isEphemeralTopic(topic)) return true;\n if (!this.#sharedPeerDiscovery) return false;\n const decoded = decodeThreadTopic(topic);\n if (decoded === undefined) return false;\n const resourceId = resourceOfThreadKey(decoded);\n return resourceId !== undefined && resourceId !== this.#resourceId;\n }\n\n async #publishTo(\n topic: string,\n dir: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n return;\n }\n const key = socketKey(dir, topic);\n // Retain synchronously so a concurrent release cannot close the socket\n // this publish is about to use.\n this.#retain(key);\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.publish(topic, event, options);\n } finally {\n this.#release(key);\n }\n }\n\n async #subscribeTo(topic: string, dir: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (!this.#isRefCounted(topic)) {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n return;\n }\n const key = socketKey(dir, topic);\n this.#retain(key);\n const generation = this.#clearGenerations.get(key) ?? 0;\n try {\n const socket = await this.#getOrCreate(topic, dir);\n await socket.subscribe(topic, cb, options);\n if ((this.#clearGenerations.get(key) ?? 0) !== generation) {\n // The topic was cleared while this subscribe was in flight.\n await socket.unsubscribe(topic, cb);\n return;\n }\n let subscriptions = this.#liveSubscriptions.get(key);\n if (!subscriptions) {\n subscriptions = new Set();\n this.#liveSubscriptions.set(key, subscriptions);\n }\n if (!subscriptions.has(cb)) {\n subscriptions.add(cb);\n this.#retain(key);\n }\n } finally {\n this.#release(key);\n }\n }\n\n async #unsubscribeFrom(topic: string, dir: string, cb: EventCallback): Promise<void> {\n const key = socketKey(dir, topic);\n const socket = this.#sockets.get(key);\n const subscriptions = this.#isRefCounted(topic) ? this.#liveSubscriptions.get(key) : undefined;\n if (!subscriptions?.delete(cb)) {\n if (!socket) return;\n await socket.unsubscribe(topic, cb);\n return;\n }\n if (subscriptions.size === 0) this.#liveSubscriptions.delete(key);\n try {\n await socket?.unsubscribe(topic, cb);\n } finally {\n this.#release(key);\n }\n }\n\n #leasesFor(resourceId: string): UnixSocketPubSub {\n let socket = this.#leaseSockets.get(resourceId);\n if (!socket) {\n socket = new UnixSocketPubSub(join(this.#rootDir, resourceId, LEASE_SOCKET_NAME));\n this.#leaseSockets.set(resourceId, socket);\n }\n return socket;\n }\n\n /** The directory, under the root, that holds a topic's socket. */\n #dirFor(topic: string): string {\n if (isOwnerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n if (this.#sharedPeerDiscovery && isPeerDiscoveryTopic(topic)) return SHARED_SCOPE_DIR;\n const decoded = decodeThreadTopic(topic);\n if (decoded !== undefined) return resourceOfThreadKey(decoded) ?? this.#resourceId;\n return this.#resourceId;\n }\n\n #retain(key: string): void {\n this.#refs.set(key, (this.#refs.get(key) ?? 0) + 1);\n }\n\n #release(key: string): void {\n const refs = (this.#refs.get(key) ?? 0) - 1;\n if (refs > 0) {\n this.#refs.set(key, refs);\n return;\n }\n this.#refs.delete(key);\n this.#clearGenerations.delete(key);\n this.#liveSubscriptions.delete(key);\n const socket = this.#sockets.get(key);\n if (!socket) return;\n this.#sockets.delete(key);\n const closing: Promise<void> = socket\n .close()\n .catch(() => {})\n .finally(() => {\n if (this.#closing.get(key) === closing) this.#closing.delete(key);\n });\n this.#closing.set(key, closing);\n }\n\n async #getOrCreate(topic: string, dir: string): Promise<UnixSocketPubSub> {\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const key = socketKey(dir, topic);\n // A socket for this key may still be closing after its last reference\n // was released; never hand out the closing instance.\n const closing = this.#closing.get(key);\n if (closing) {\n await closing;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n }\n const existing = this.#sockets.get(key);\n if (existing) return existing;\n // Deduplicate concurrent callers so only one socket is created per topic.\n let inflight = this.#pending.get(key);\n if (!inflight) {\n inflight = this.#initSocket(topic, dir, key);\n this.#pending.set(key, inflight);\n }\n const socket = await inflight;\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n return socket;\n }\n\n async #initSocket(topic: string, dir: string, key: string): Promise<UnixSocketPubSub> {\n try {\n const socketPath = await this.#socketPath(topic, dir);\n if (this.#closed) throw new Error('SignalsPubSub is closed');\n const socket = new UnixSocketPubSub(socketPath);\n this.#sockets.set(key, socket);\n return socket;\n } finally {\n this.#pending.delete(key);\n }\n }\n\n async #socketPath(topic: string, scope: string): Promise<string> {\n let key = topicKey(topic);\n // A threadId names a file in its resource's directory, so never let it\n // reach outside that directory, regardless of who owns the thread or\n // whether cross-project discovery is enabled.\n if (decodeThreadTopic(topic) !== undefined && !isSafeFileName(key)) {\n throw new Error('Cannot route an agent thread topic whose threadId is not a safe file name');\n }\n const dir = join(this.#rootDir, scope);\n await mkdir(dir, { recursive: true });\n const candidate = join(dir, `${key}.sock`);\n // macOS sun_path limit is 104 bytes; Linux is 108. Use 104 as the\n // conservative bound. When the path is too long, replace the key with\n // a short hash so the socket can still be created.\n if (Buffer.byteLength(candidate) > 104) {\n key = Buffer.from(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(key)))\n .toString('hex')\n .slice(0, 16);\n return join(dir, `${key}.sock`);\n }\n return candidate;\n }\n}\n\n/**\n * Creates a per-topic PubSub backed by Unix sockets for cross-process signal\n * and workflow event coordination between mastracode processes.\n *\n * Topics live under `/tmp/mc/<resourceId>/`, except that a thread's stream and\n * leases live under its own resource's directory and thread-owner discovery\n * under `/tmp/mc/_shared/` (see {@link SignalsPubSub}). With\n * `sharedAgentDiscovery`, peer discovery runs there too. Stale sockets from\n * crashed processes are handled by the underlying {@link UnixSocketPubSub}'s\n * broker election logic.\n */\nexport function createSignalsPubSub(resourceId: string, options?: SignalsPubSubOptions): SignalsPubSub {\n return new SignalsPubSub(resourceId, options);\n}\n"],"mappings":"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"}
|