@vitest-agent/mcp 2.0.4 → 2.0.6

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/README.md CHANGED
@@ -13,6 +13,7 @@ The `vitest-agent-mcp` MCP server bin. Exposes action-keyed tools over stdio tha
13
13
  - **29 action-keyed tools** — per-CRUD families collapse into single tools dispatching on an `action` discriminator; covers `test_status`, `test_overview`, `test_coverage`, `test_errors`, `run_tests`, `note`, `hypothesis`, `tdd_task`, `tdd_goal`, `tdd_behavior` and more
14
14
  - **Six framing prompts** — `triage`, `why-flaky`, `regression-since-pass`, `explain-failure`, `tdd-resume`, `wrapup`
15
15
  - **Idempotency middleware** — `tdd_task`, `tdd_goal`, `tdd_behavior` and `hypothesis` create-actions are idempotent on derived keys
16
+ - **Programmatic API** — `buildMcpServer` constructs the server without connecting a transport; `parseSessionEnvExports` and `recoverSessionContextFromSessionEnv` recover the host session context from the Claude Code plugin's session-env files, so agent attribution survives a plugin reload
16
17
 
17
18
  ## Install
18
19
 
@@ -2,6 +2,7 @@
2
2
  import { createCurrentSessionIdRef, createSessionContextRef, sessionContextFromEnv } from "../context.js";
3
3
  import { McpLive } from "../layers/McpLive.js";
4
4
  import { startMcpServer } from "../server.js";
5
+ import { recoverSessionContextFromSessionEnv } from "../session-env.js";
5
6
  import * as NodeServices from "@effect/platform-node/NodeServices";
6
7
  import { PathResolutionLive, formatFatalError, resolveDataPath, resolveLogFile, resolveLogLevel } from "@vitest-agent/sdk";
7
8
  import { Effect, ManagedRuntime } from "effect";
@@ -58,7 +59,7 @@ async function main() {
58
59
  runtime,
59
60
  cwd: projectDir,
60
61
  currentSessionId: createCurrentSessionIdRef(initialSessionId ?? recoveredContext?.chatId ?? null),
61
- sessionContext: createSessionContextRef(recoveredContext)
62
+ sessionContext: createSessionContextRef(recoveredContext, () => recoverSessionContextFromSessionEnv({ projectDir }))
62
63
  };
63
64
  const chatIdResolved = initialSessionId ?? recoveredContext?.chatId ?? null;
64
65
  console.error("[vitest-agent-mcp] Starting...");
package/context.js CHANGED
@@ -20,14 +20,27 @@ const createCurrentSessionIdRef = (initial = null) => {
20
20
  /**
21
21
  * Creates a new {@link SessionContextRef} with an optional initial value.
22
22
  *
23
+ * When a `recover` thunk is supplied, `get()` invokes it lazily while the
24
+ * held value is `null` and caches the first non-null result. This is how
25
+ * a null boot-time context heals at the first tool call that needs it:
26
+ * boot-time env recovery loses both the fresh-launch race (the MCP child
27
+ * can spawn before SessionStart writes `CLAUDE_ENV_FILE`) and the
28
+ * `/reload-plugins` restart (fresh environment, no session exports), but
29
+ * by first-tool-call time the SessionStart hook's session-env file is on
30
+ * disk for the recover thunk to read.
31
+ *
23
32
  * @param initial - the starting session context, or `null` when not yet recovered
33
+ * @param recover - optional call-time recovery attempted by `get()` while the value is `null`
24
34
  * @returns a mutable ref holding the current session context
25
35
  * @public
26
36
  */
27
- const createSessionContextRef = (initial = null) => {
37
+ const createSessionContextRef = (initial = null, recover) => {
28
38
  let value = initial;
29
39
  return {
30
- get: () => value,
40
+ get: () => {
41
+ if (value === null && recover !== void 0) value = recover();
42
+ return value;
43
+ },
31
44
  set: (ctx) => {
32
45
  value = ctx;
33
46
  }
package/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { DataReader, DataStore, OutputRenderer, ProjectDiscovery } from "@vitest
2
2
  import { Layer, LogLevel, ManagedRuntime } from "effect";
3
3
  import * as NodeServices from "@effect/platform-node/NodeServices";
4
4
  import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
6
  //#region src/context.d.ts
6
7
  /**
7
8
  * Mutable holder for the MCP server's currently-associated host chat
@@ -58,11 +59,21 @@ interface SessionContextRef {
58
59
  /**
59
60
  * Creates a new {@link SessionContextRef} with an optional initial value.
60
61
  *
62
+ * When a `recover` thunk is supplied, `get()` invokes it lazily while the
63
+ * held value is `null` and caches the first non-null result. This is how
64
+ * a null boot-time context heals at the first tool call that needs it:
65
+ * boot-time env recovery loses both the fresh-launch race (the MCP child
66
+ * can spawn before SessionStart writes `CLAUDE_ENV_FILE`) and the
67
+ * `/reload-plugins` restart (fresh environment, no session exports), but
68
+ * by first-tool-call time the SessionStart hook's session-env file is on
69
+ * disk for the recover thunk to read.
70
+ *
61
71
  * @param initial - the starting session context, or `null` when not yet recovered
72
+ * @param recover - optional call-time recovery attempted by `get()` while the value is `null`
62
73
  * @returns a mutable ref holding the current session context
63
74
  * @public
64
75
  */
65
- declare const createSessionContextRef: (initial?: SessionContext | null) => SessionContextRef;
76
+ declare const createSessionContextRef: (initial?: SessionContext | null, recover?: () => SessionContext | null) => SessionContextRef;
66
77
  /**
67
78
  * tRPC context carrying a ManagedRuntime for Effect service access.
68
79
  *
@@ -1410,7 +1421,8 @@ declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1410
1421
  hypothesis: import("@trpc/server").TRPCMutationProcedure<{
1411
1422
  input: {
1412
1423
  readonly action: "record";
1413
- readonly sessionId?: number | undefined;
1424
+ readonly tddTaskId?: string | number | undefined;
1425
+ readonly sessionId?: string | number | undefined;
1414
1426
  readonly content: string;
1415
1427
  readonly createdTurnId?: number | undefined;
1416
1428
  readonly citedTestErrorId?: number | undefined;
@@ -1530,18 +1542,58 @@ declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1530
1542
  }>>;
1531
1543
  //#endregion
1532
1544
  //#region src/server.d.ts
1545
+ /**
1546
+ * Builds the fully-registered MCP server without connecting a transport.
1547
+ *
1548
+ * Constructs the MCP server instance, registers all tRPC-backed tools
1549
+ * (wired through `ctx.runtime`), and calls `registerAllPrompts`. Split
1550
+ * from {@link startMcpServer} so tests can connect the identical server
1551
+ * to an in-memory transport and assert the *served* tool schemas — the
1552
+ * MCP-SDK-side registrations here are hand-synced with the tRPC inputs
1553
+ * in `tools/`, and a missed sync is invisible to router-level tests.
1554
+ *
1555
+ * @param ctx - the MCP context carrying the shared ManagedRuntime and session refs
1556
+ * @public
1557
+ */
1558
+ declare function buildMcpServer(ctx: McpContext): McpServer;
1533
1559
  /**
1534
1560
  * Starts the MCP server over stdio, registering all tools and prompts.
1535
1561
  *
1536
- * Constructs the MCP server instance, registers all tRPC-backed tools (wired through
1537
- * `ctx.runtime`), calls `registerAllPrompts`, then connects
1538
- * a `StdioServerTransport`. Returns when the transport disconnects.
1562
+ * Builds the server via {@link buildMcpServer}, then connects a
1563
+ * `StdioServerTransport`. Returns when the transport disconnects.
1539
1564
  *
1540
1565
  * @param ctx - the MCP context carrying the shared ManagedRuntime and session refs
1541
1566
  * @public
1542
1567
  */
1543
1568
  declare function startMcpServer(ctx: McpContext): Promise<void>;
1544
1569
  //#endregion
1570
+ //#region src/session-env.d.ts
1571
+ /**
1572
+ * Parse `export KEY=value` lines from a session-env hook file into a
1573
+ * plain record. Non-export lines are ignored.
1574
+ *
1575
+ * @param content - the raw text of a session-env hook file
1576
+ * @returns a record of export names to unquoted values
1577
+ * @public
1578
+ */
1579
+ declare const parseSessionEnvExports: (content: string) => Record<string, string>;
1580
+ /**
1581
+ * Recover a {@link SessionContext} from the newest session-env hook file
1582
+ * whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
1583
+ *
1584
+ * Returns `null` when the session-env root is missing, unreadable, or no
1585
+ * session dir matches the project. Never throws — recovery is best-effort
1586
+ * and callers fall back to their existing null-context behavior.
1587
+ *
1588
+ * @param opts - `projectDir` to match against; `sessionEnvRoot` overrides
1589
+ * the default `~/.claude/session-env` (tests)
1590
+ * @public
1591
+ */
1592
+ declare const recoverSessionContextFromSessionEnv: (opts: {
1593
+ readonly projectDir: string;
1594
+ readonly sessionEnvRoot?: string;
1595
+ }) => SessionContext | null;
1596
+ //#endregion
1545
1597
  //#region src/tools/_tdd-error-envelope.d.ts
1546
1598
  /**
1547
1599
  * Suggested recovery action attached to a TDD error envelope.
@@ -1584,5 +1636,5 @@ interface TddErrorEnvelope {
1584
1636
  */
1585
1637
  declare const CURRENT_MCP_VERSION: string;
1586
1638
  //#endregion
1587
- export { CURRENT_MCP_VERSION, type CurrentSessionIdRef, type McpContext, McpLive, type Remediation, type SessionContext, type SessionContextRef, type TddErrorEnvelope, appRouter, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, startMcpServer };
1639
+ export { CURRENT_MCP_VERSION, type CurrentSessionIdRef, type McpContext, McpLive, type Remediation, type SessionContext, type SessionContextRef, type TddErrorEnvelope, appRouter, buildMcpServer, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, parseSessionEnvExports, recoverSessionContextFromSessionEnv, startMcpServer };
1588
1640
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createCallerFactory, createCurrentSessionIdRef, createSessionContextRef } from "./context.js";
2
2
  import { McpLive } from "./layers/McpLive.js";
3
3
  import { appRouter } from "./router.js";
4
- import { startMcpServer } from "./server.js";
4
+ import { buildMcpServer, startMcpServer } from "./server.js";
5
+ import { parseSessionEnvExports, recoverSessionContextFromSessionEnv } from "./session-env.js";
5
6
 
6
7
  //#region src/index.ts
7
8
  /**
@@ -11,7 +12,7 @@ import { startMcpServer } from "./server.js";
11
12
  *
12
13
  * @public
13
14
  */
14
- const CURRENT_MCP_VERSION = "2.0.4";
15
+ const CURRENT_MCP_VERSION = "2.0.6";
15
16
 
16
17
  //#endregion
17
- export { CURRENT_MCP_VERSION, McpLive, appRouter, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, startMcpServer };
18
+ export { CURRENT_MCP_VERSION, McpLive, appRouter, buildMcpServer, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, parseSessionEnvExports, recoverSessionContextFromSessionEnv, startMcpServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/mcp",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "private": false,
5
5
  "description": "Model Context Protocol server for vitest-agent. Exposes 53 tools for agent access to test data, TDD lifecycle, and session management.",
6
6
  "keywords": [
@@ -42,7 +42,7 @@
42
42
  "@effect/sql-sqlite-node": "4.0.0-beta.99",
43
43
  "@modelcontextprotocol/sdk": "^1.29.0",
44
44
  "@trpc/server": "^11.18.0",
45
- "@vitest-agent/sdk": "2.0.4",
45
+ "@vitest-agent/sdk": "2.0.6",
46
46
  "effect": "4.0.0-beta.99",
47
47
  "zod": "^4.4.3"
48
48
  },
package/server.js CHANGED
@@ -122,16 +122,19 @@ function structuredJsonResult(value) {
122
122
  return structuredResult(JSON.stringify(value, null, 2), value);
123
123
  }
124
124
  /**
125
- * Starts the MCP server over stdio, registering all tools and prompts.
125
+ * Builds the fully-registered MCP server without connecting a transport.
126
126
  *
127
- * Constructs the MCP server instance, registers all tRPC-backed tools (wired through
128
- * `ctx.runtime`), calls `registerAllPrompts`, then connects
129
- * a `StdioServerTransport`. Returns when the transport disconnects.
127
+ * Constructs the MCP server instance, registers all tRPC-backed tools
128
+ * (wired through `ctx.runtime`), and calls `registerAllPrompts`. Split
129
+ * from {@link startMcpServer} so tests can connect the identical server
130
+ * to an in-memory transport and assert the *served* tool schemas — the
131
+ * MCP-SDK-side registrations here are hand-synced with the tRPC inputs
132
+ * in `tools/`, and a missed sync is invisible to router-level tests.
130
133
  *
131
134
  * @param ctx - the MCP context carrying the shared ManagedRuntime and session refs
132
135
  * @public
133
136
  */
134
- async function startMcpServer(ctx) {
137
+ function buildMcpServer(ctx) {
135
138
  const server = new McpServer({
136
139
  name: "vitest-agent",
137
140
  version: "0.1.0"
@@ -701,14 +704,15 @@ async function startMcpServer(ctx) {
701
704
  return structuredResult(Schema.decodeSync(TddArtifactListAsMarkdown)(data), data);
702
705
  });
703
706
  server.registerTool("hypothesis", {
704
- description: "Use to manage debugging hypotheses, with a CRUD action discriminator: action='record' (sessionId, content, optional citation ids) writes a hypothesis; action='validate' (id, outcome, validatedAt) records a validation outcome; action='list' (sessionId?, outcome?, limit?) returns matching hypotheses as markdown.",
707
+ description: "Use to manage debugging hypotheses, with a CRUD action discriminator: action='record' (content, tddTaskId?, optional citation ids) writes a hypothesis — the binding session is resolved server-side from the recovered host context (active TDD subagent, else main session); pass tddTaskId (returned by tdd_task action='start') to bind deterministically to that task's session, and do not pass sessionId when recording; action='validate' (id, outcome, validatedAt) records a validation outcome; action='list' (sessionId?, outcome?, limit?) returns matching hypotheses as markdown.",
705
708
  inputSchema: {
706
709
  action: z.enum([
707
710
  "record",
708
711
  "validate",
709
712
  "list"
710
713
  ]).describe("CRUD discriminator"),
711
- sessionId: z.optional(z.coerce.number()).describe("Session id (required for record; filter for list)"),
714
+ sessionId: z.optional(z.coerce.number()).describe("list: filter by session id. record: dev/test fallback only — ignored when host context is recovered; never pass a tddTaskId value here"),
715
+ tddTaskId: z.optional(z.coerce.number()).describe("record: tdd task id returned by tdd_task action='start' — binds the hypothesis to that task's session deterministically"),
712
716
  content: z.optional(z.string()).describe("Hypothesis content (action=record)"),
713
717
  createdTurnId: z.optional(z.coerce.number()),
714
718
  citedTestErrorId: z.optional(z.coerce.number()),
@@ -728,8 +732,9 @@ async function startMcpServer(ctx) {
728
732
  }, async (args) => {
729
733
  if (args.action === "record") return structuredJsonResult(await caller.hypothesis({
730
734
  action: "record",
731
- sessionId: args.sessionId,
732
735
  content: args.content,
736
+ ...args.tddTaskId !== void 0 && { tddTaskId: args.tddTaskId },
737
+ ...args.sessionId !== void 0 && { sessionId: args.sessionId },
733
738
  ...args.createdTurnId !== void 0 && { createdTurnId: args.createdTurnId },
734
739
  ...args.citedTestErrorId !== void 0 && { citedTestErrorId: args.citedTestErrorId },
735
740
  ...args.citedStackFrameId !== void 0 && { citedStackFrameId: args.citedStackFrameId }
@@ -828,9 +833,22 @@ async function startMcpServer(ctx) {
828
833
  return structuredResult(data.message, data);
829
834
  });
830
835
  registerAllPrompts(server);
836
+ return server;
837
+ }
838
+ /**
839
+ * Starts the MCP server over stdio, registering all tools and prompts.
840
+ *
841
+ * Builds the server via {@link buildMcpServer}, then connects a
842
+ * `StdioServerTransport`. Returns when the transport disconnects.
843
+ *
844
+ * @param ctx - the MCP context carrying the shared ManagedRuntime and session refs
845
+ * @public
846
+ */
847
+ async function startMcpServer(ctx) {
848
+ const server = buildMcpServer(ctx);
831
849
  const transport = new StdioServerTransport();
832
850
  await server.connect(transport);
833
851
  }
834
852
 
835
853
  //#endregion
836
- export { startMcpServer };
854
+ export { buildMcpServer, startMcpServer };
package/session-env.js ADDED
@@ -0,0 +1,112 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+
5
+ //#region src/session-env.ts
6
+ /**
7
+ * Call-time SessionContext recovery from the per-session env files the
8
+ * plugin's SessionStart hook writes to `~/.claude/session-env/`.
9
+ *
10
+ * The boot-time env recovery (`sessionContextFromEnv`) depends on Claude
11
+ * Code auto-sourcing `CLAUDE_ENV_FILE` into the MCP child — which loses
12
+ * two races:
13
+ *
14
+ * 1. **Boot race.** On a fresh Claude Code launch the MCP child can spawn
15
+ * before the SessionStart hook has written `CLAUDE_ENV_FILE`, so the
16
+ * child's `process.env` never carries the canonical UUIDs (observed
17
+ * live: MCP spawn at 00:41:50, env file written 00:41:51).
18
+ * 2. **`/reload-plugins`.** A plugin reload restarts the MCP server
19
+ * mid-session with a fresh environment that has no session exports.
20
+ *
21
+ * In both cases the SessionStart hook has (or will have) written the same
22
+ * exports to a second, known-name surface:
23
+ * `~/.claude/session-env/<chat_id>/vitest-agent-hook.sh`. This module
24
+ * reads that surface directly, so a null boot context can be recovered
25
+ * lazily at the first tool call that needs it.
26
+ *
27
+ * Selection rule: among all session dirs whose exports name this server's
28
+ * `projectDir`, the newest-mtime file wins — the most recently started
29
+ * session for this project. With two live Claude Code windows on the same
30
+ * project this can name the other window's session; that ambiguity is
31
+ * inherent to a per-project (not per-process) surface and is accepted —
32
+ * the pre-existing alternative was no attribution at all.
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+ const EXPORT_LINE = /^export ([A-Z_][A-Z0-9_]*)=(.*)$/;
37
+ /**
38
+ * Undo the `printf '%q'` quoting the SessionStart hook applies to export
39
+ * values. UUIDs and plain paths arrive bare; values with specials arrive
40
+ * as `$'...'`, `'...'`, `"..."`, or backslash-escaped words.
41
+ */
42
+ const unquote = (raw) => {
43
+ let v = raw.trim();
44
+ if (v.startsWith("$'") && v.endsWith("'") && v.length >= 3) v = v.slice(2, -1);
45
+ else if (v.startsWith("'") && v.endsWith("'") || v.startsWith("\"") && v.endsWith("\"")) {
46
+ if (v.length >= 2) v = v.slice(1, -1);
47
+ }
48
+ return v.replace(/\\(.)/g, "$1");
49
+ };
50
+ /**
51
+ * Parse `export KEY=value` lines from a session-env hook file into a
52
+ * plain record. Non-export lines are ignored.
53
+ *
54
+ * @param content - the raw text of a session-env hook file
55
+ * @returns a record of export names to unquoted values
56
+ * @public
57
+ */
58
+ const parseSessionEnvExports = (content) => {
59
+ const out = {};
60
+ for (const line of content.split("\n")) {
61
+ const m = EXPORT_LINE.exec(line.trim());
62
+ if (m?.[1] !== void 0 && m[2] !== void 0) out[m[1]] = unquote(m[2]);
63
+ }
64
+ return out;
65
+ };
66
+ /**
67
+ * Recover a {@link SessionContext} from the newest session-env hook file
68
+ * whose `VITEST_AGENT_PROJECT_DIR` matches `projectDir`.
69
+ *
70
+ * Returns `null` when the session-env root is missing, unreadable, or no
71
+ * session dir matches the project. Never throws — recovery is best-effort
72
+ * and callers fall back to their existing null-context behavior.
73
+ *
74
+ * @param opts - `projectDir` to match against; `sessionEnvRoot` overrides
75
+ * the default `~/.claude/session-env` (tests)
76
+ * @public
77
+ */
78
+ const recoverSessionContextFromSessionEnv = (opts) => {
79
+ const root = opts.sessionEnvRoot ?? join(homedir(), ".claude", "session-env");
80
+ const wantDir = resolve(opts.projectDir);
81
+ let entries;
82
+ try {
83
+ entries = readdirSync(root);
84
+ } catch {
85
+ return null;
86
+ }
87
+ let best = null;
88
+ for (const entry of entries) {
89
+ const file = join(root, entry, "vitest-agent-hook.sh");
90
+ try {
91
+ const st = statSync(file);
92
+ const env = parseSessionEnvExports(readFileSync(file, "utf8"));
93
+ const chatId = env.VITEST_AGENT_CHAT_ID;
94
+ const conversationId = env.VITEST_AGENT_CONVERSATION_ID;
95
+ const mainAgentId = env.VITEST_AGENT_MAIN_AGENT_ID ?? env.VITEST_AGENT_AGENT_ID;
96
+ const fileProjectDir = env.VITEST_AGENT_PROJECT_DIR;
97
+ if (chatId === void 0 || chatId.length === 0 || conversationId === void 0 || conversationId.length === 0 || mainAgentId === void 0 || mainAgentId.length === 0 || fileProjectDir === void 0 || resolve(fileProjectDir) !== wantDir) continue;
98
+ if (best === null || st.mtimeMs > best.mtimeMs) best = {
99
+ mtimeMs: st.mtimeMs,
100
+ ctx: {
101
+ chatId,
102
+ conversationId,
103
+ mainAgentId
104
+ }
105
+ };
106
+ } catch {}
107
+ }
108
+ return best === null ? null : best.ctx;
109
+ };
110
+
111
+ //#endregion
112
+ export { parseSessionEnvExports, recoverSessionContextFromSessionEnv };
@@ -57,7 +57,8 @@ const formatHypothesisListMarkdown = (data) => {
57
57
  };
58
58
  const RecordVariant = Schema.Struct({
59
59
  action: Schema.Literal("record"),
60
- sessionId: Schema.optional(Schema.Number),
60
+ tddTaskId: Schema.optional(Schema.Union([Schema.Number, Schema.FiniteFromString])),
61
+ sessionId: Schema.optional(Schema.Union([Schema.Number, Schema.FiniteFromString])),
61
62
  content: Schema.String,
62
63
  createdTurnId: Schema.optional(Schema.Number),
63
64
  citedTestErrorId: Schema.optional(Schema.Number),
@@ -95,20 +96,31 @@ const hypothesis = idempotentProcedure.input(Schema.toStandardSchemaV1(Hypothesi
95
96
  record: (variant) => Effect.gen(function* () {
96
97
  const store = yield* DataStore;
97
98
  const reader = yield* DataReader;
98
- const sc = ctx.sessionContext.get();
99
- let resolvedSessionId = variant.sessionId;
100
- if (sc !== null) {
101
- const main = yield* reader.getSessionByChatId(sc.chatId);
102
- if (Option.isSome(main)) {
103
- const sub = yield* reader.findActiveSubagentSession(main.value.id);
104
- resolvedSessionId = Option.isSome(sub) ? sub.value.id : main.value.id;
99
+ let resolvedSessionId;
100
+ if (variant.tddTaskId !== void 0) {
101
+ const bound = yield* reader.getSessionByTddTaskId(variant.tddTaskId);
102
+ if (Option.isNone(bound)) return yield* Effect.fail(new DataStoreError({
103
+ operation: "write",
104
+ table: "hypotheses",
105
+ reason: `unknown tddTaskId ${variant.tddTaskId}: no session found to attribute hypothesis`
106
+ }));
107
+ resolvedSessionId = bound.value.id;
108
+ } else {
109
+ const sc = ctx.sessionContext.get();
110
+ resolvedSessionId = variant.sessionId;
111
+ if (sc !== null) {
112
+ const main = yield* reader.getSessionByChatId(sc.chatId);
113
+ if (Option.isSome(main)) {
114
+ const sub = yield* reader.findActiveSubagentSession(main.value.id);
115
+ resolvedSessionId = Option.isSome(sub) ? sub.value.id : main.value.id;
116
+ }
105
117
  }
118
+ if (resolvedSessionId === void 0) return yield* Effect.fail(new DataStoreError({
119
+ operation: "write",
120
+ table: "hypotheses",
121
+ reason: "no recovered session context: pass tddTaskId (the id returned by tdd_task action:start) to bind this hypothesis to your task's session — do not retry with a raw sessionId, and never pass tddTaskId under a sessionId key"
122
+ }));
106
123
  }
107
- if (resolvedSessionId === void 0) return yield* Effect.fail(new DataStoreError({
108
- operation: "write",
109
- table: "hypotheses",
110
- reason: "no recovered session context and no sessionId supplied to attribute hypothesis"
111
- }));
112
124
  return {
113
125
  action: "record",
114
126
  id: yield* store.writeHypothesis({
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.11"
8
+ "packageVersion": "7.58.12"
9
9
  }
10
10
  ]
11
11
  }