Skip to content

Commit c1ec2f2

Browse files
committed
Return Option from the per-call project and session-id lookups
projectForCwd and the rebuildNarrowSet session-id branch returned Effect.succeed(undefined) as a real value, which @effect/tsgo's effectSucceedWithVoid flags and whose Effect.void rewrite does not typecheck. They now return Option and unwrap only where they meet the session context and the subscription seeding.
1 parent 8e37b4b commit c1ec2f2

3 files changed

Lines changed: 36 additions & 26 deletions

File tree

‎packages/mcp/server.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,12 @@ export const makeProgram = (
524524
// Per-call project resolver. Operator override (COMMY_PROJECT)
525525
// is authoritative; otherwise derive from the calling session's cwd
526526
// at call time — process cwd is irrelevant.
527-
const projectForCwd = (cwd: string | undefined): Effect.Effect<ProjectSlug | undefined> => {
528-
if (parsed.project !== undefined) return Effect.succeed(parsed.project)
529-
if (cwd === undefined) return Effect.succeed(undefined)
530-
return Effect.map(deriveProject({ cwd, readGitContext }), Option.getOrUndefined)
527+
const projectForCwd = (
528+
cwd: string | undefined,
529+
): Effect.Effect<Option.Option<ProjectSlug>> => {
530+
if (parsed.project !== undefined) return Effect.succeedSome(parsed.project)
531+
if (cwd === undefined) return Effect.succeedNone
532+
return deriveProject({ cwd, readGitContext })
531533
}
532534

533535
// Sample the realm-wide editing switch once, before the tool list is
@@ -593,20 +595,18 @@ export const makeProgram = (
593595
Effect.succeedNone
594596
const rebuildNarrowSet: Effect.Effect<void> = (
595597
parsed.botName === undefined
596-
? Deferred.await(sessionIdDeferred).pipe(
597-
Effect.map((sessionId): SessionId | undefined => sessionId),
598-
)
599-
: Effect.succeed(undefined)
598+
? Deferred.await(sessionIdDeferred).pipe(Effect.asSome)
599+
: Effect.succeedNone
600600
).pipe(
601-
Effect.flatMap((sessionId) =>
601+
Effect.flatMap((sessionId: Option.Option<SessionId>) =>
602602
withSessionContext(
603603
restoreSubscriptions({
604604
persisted: persistedTopicIntents,
605605
isBound: () => identityCache.boundIdentityIds().size > 0,
606606
narrowSet,
607607
inbox: adapter.inbox,
608608
}),
609-
{ sessionId, project: parsed.project },
609+
{ sessionId: Option.getOrUndefined(sessionId), project: parsed.project },
610610
),
611611
),
612612
Effect.catchAll((err) =>

‎packages/mcp/tools-session.test.ts‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface SessionRig {
2626

2727
const buildSessionRig = (
2828
options: {
29-
readonly projectForCwd?: (cwd: string | undefined) => Effect.Effect<ProjectSlug | undefined>
29+
readonly projectForCwd?: (cwd: string | undefined) => Effect.Effect<Option.Option<ProjectSlug>>
3030
readonly feedSessionId?: (sessionId: SessionId) => Effect.Effect<void>
3131
} = {},
3232
): Effect.Effect<SessionRig, never, Scope.Scope> =>
@@ -430,11 +430,9 @@ test('post with session_id + cwd mints cc-<project>-<sid-prefix> derived from cw
430430
Effect.gen(function* () {
431431
const rig = yield* buildSessionRig({
432432
projectForCwd: (cwd) =>
433-
Effect.succeed(
434-
cwd === '/home/x/myproject'
435-
? Option.getOrUndefined(sanitiseProjectSlug('myproject'))
436-
: undefined,
437-
),
433+
cwd === '/home/x/myproject'
434+
? Effect.succeed(sanitiseProjectSlug('myproject'))
435+
: Effect.succeedNone,
438436
})
439437
const result = yield* Effect.promise(() =>
440438
rig.client.callTool({
@@ -466,7 +464,8 @@ test('two sessions in different cwds mint two different project prefixes', () =>
466464
'/home/x/myproject-b': slug('myproject-b'),
467465
}
468466
const rig = yield* buildSessionRig({
469-
projectForCwd: (cwd) => Effect.succeed(cwd === undefined ? undefined : cwdToSlug[cwd]),
467+
projectForCwd: (cwd) =>
468+
Effect.succeed(Option.fromNullable(cwd === undefined ? undefined : cwdToSlug[cwd])),
470469
})
471470
yield* Effect.promise(() =>
472471
rig.client.callTool({
@@ -503,9 +502,9 @@ test('post with cwd from a non-project directory falls back to bare cc-<8>', ()
503502
Effect.runPromise(
504503
Effect.scoped(
505504
Effect.gen(function* () {
506-
// projectForCwd returns undefined when cwd is not in a known repo.
505+
// projectForCwd returns none when cwd is not in a known repo.
507506
// The minted name must NOT inherit the plugin's own location.
508-
const rig = yield* buildSessionRig({ projectForCwd: () => Effect.succeed(undefined) })
507+
const rig = yield* buildSessionRig({ projectForCwd: () => Effect.succeedNone })
509508
yield* Effect.promise(() =>
510509
rig.client.callTool({
511510
name: 'post',

‎packages/mcp/tools.ts‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ export interface RegisterToolsDeps {
194194
* minted `cc-<project>-<8>` name reflects the *calling* session's
195195
* project rather than the plugin's own location. Wired at boot in
196196
* `server.ts` from `COMMY_PROJECT` (operator override) and
197-
* the git probe; defaults to a constant `undefined` resolver when
197+
* the git probe; defaults to a resolver that always answers none when
198198
* omitted (tests that don't care about per-session derivation).
199199
*/
200-
readonly projectForCwd?: (cwd: string | undefined) => Effect.Effect<ProjectSlug | undefined>
200+
readonly projectForCwd?: (cwd: string | undefined) => Effect.Effect<Option.Option<ProjectSlug>>
201201
/**
202202
* Restore (or seed) this session's narrow set on its first `subscribe`/
203203
* `unsubscribe` — memoised once per session_id in `server.ts`, so it runs
@@ -506,7 +506,7 @@ const UploadFileArgs = Schema.Struct({ path: Schema.String })
506506

507507
const buildToolDefs = (deps: RegisterToolsDeps, cache: InternalCache): ReadonlyArray<ToolDef> => {
508508
const { adapter, identityCache, narrowSet } = deps
509-
const projectForCwd = deps.projectForCwd ?? (() => Effect.succeed(undefined))
509+
const projectForCwd = deps.projectForCwd ?? (() => Effect.succeedNone)
510510
/**
511511
* The tools that accept a host-supplied `session_id` (see
512512
* {@link ToolDef.hostSuppliedArgs}). EIGHT tools carry it. That is the same
@@ -548,7 +548,7 @@ const buildToolDefs = (deps: RegisterToolsDeps, cache: InternalCache): ReadonlyA
548548
}
549549
const projectForArgs = (
550550
args: Readonly<Record<string, unknown>>,
551-
): Effect.Effect<ProjectSlug | undefined> => projectForCwd(readCwd(args))
551+
): Effect.Effect<Option.Option<ProjectSlug>> => projectForCwd(readCwd(args))
552552
// Every tool call that carries arguments supplies its calling session's
553553
// context, uniformly. This DECIDES NOTHING about identity: it feeds the
554554
// shared session-id deferred (comms-k7cv) and puts the naming inputs where
@@ -569,7 +569,12 @@ const buildToolDefs = (deps: RegisterToolsDeps, cache: InternalCache): ReadonlyA
569569
projectForArgs(args).pipe(
570570
Effect.flatMap((project) =>
571571
feedSession(sessionId).pipe(
572-
Effect.zipRight(withSessionContext(effect, { sessionId, project })),
572+
Effect.zipRight(
573+
withSessionContext(effect, {
574+
sessionId,
575+
project: Option.getOrUndefined(project),
576+
}),
577+
),
573578
),
574579
),
575580
),
@@ -937,7 +942,10 @@ const buildToolDefs = (deps: RegisterToolsDeps, cache: InternalCache): ReadonlyA
937942
// the snapshot persisted below captures the full live set and the
938943
// store's presence stays a true resume signal.
939944
if (sessionId !== undefined && deps.ensureSessionSubscriptions !== undefined) {
940-
yield* deps.ensureSessionSubscriptions(sessionId, yield* projectForArgs(args))
945+
yield* deps.ensureSessionSubscriptions(
946+
sessionId,
947+
Option.getOrUndefined(yield* projectForArgs(args)),
948+
)
941949
}
942950
// Two sinks (see bootstrap.subscribeFromEnv): the consumer-side
943951
// narrow tells the event pump to tee matching events through;
@@ -983,7 +991,10 @@ const buildToolDefs = (deps: RegisterToolsDeps, cache: InternalCache): ReadonlyA
983991
// Fed by `runFor` before this effect runs (see subscribe).
984992
const sessionId = readSessionId(args)
985993
if (sessionId !== undefined && deps.ensureSessionSubscriptions !== undefined) {
986-
yield* deps.ensureSessionSubscriptions(sessionId, yield* projectForArgs(args))
994+
yield* deps.ensureSessionSubscriptions(
995+
sessionId,
996+
Option.getOrUndefined(yield* projectForArgs(args)),
997+
)
987998
}
988999
yield* Effect.sync(() => narrowSet.remove(intent)).pipe(
9891000
Effect.andThen(adapter.inbox.unsubscribe(intentToTarget(intent))),

0 commit comments

Comments
 (0)