@@ -121,6 +121,9 @@ type DirectoryState = {
121121 * invalidate and reload config between turns, re-running the overlay without
122122 * touching MCP. */
123123 applied : Overlay | null | undefined
124+ /** When the last overlay attempt threw. A failed attempt is retried at the
125+ * probe TTL, not on every turn — each retry invalidates the whole config. */
126+ failedAt ?: number
124127}
125128const directories = new Map < string , DirectoryState > ( )
126129
@@ -142,13 +145,26 @@ function sameEntry(a: LocalMcpConfig | null, b: LocalMcpConfig | null): boolean
142145 * Called from the config loader after external MCP discovery, so it has the
143146 * last word over every other source of the key. Mutates `config.mcp` only when
144147 * the directory is bound with the pilot on. Never throws. */
145- export async function overlay ( directory : string , config : { mcp ?: Record < string , unknown > } ) : Promise < void > {
148+ export async function overlay (
149+ directory : string ,
150+ config : { mcp ?: Record < string , unknown > } ,
151+ opts : { managed ?: boolean } = { } ,
152+ ) : Promise < void > {
146153 const state = stateFor ( directory )
154+ state . failedAt = undefined
147155 try {
148156 if ( ! isEnabled ( ) || isServe ( ) ) {
149157 state . current = null
150158 return
151159 }
160+ if ( opts . managed ) {
161+ // Organisation-managed config (MDM) is authoritative over everything,
162+ // this overlay included: the key stays as managed, and nothing here
163+ // claims it, so its writers are not refused either.
164+ log . info ( "workspace engine overlay skipped: the datamate key is set by managed preferences" , { directory } )
165+ state . current = null
166+ return
167+ }
152168 const binding = await resolveBinding ( directory )
153169 if ( ! binding ) {
154170 // Logged because "flag on, nothing happened" is the question every
@@ -182,6 +198,7 @@ export async function overlay(directory: string, config: { mcp?: Record<string,
182198 } catch ( err ) {
183199 log . warn ( "workspace engine overlay failed; leaving the MCP config as loaded" , { err : String ( err ) } )
184200 state . current = null
201+ state . failedAt = now ( )
185202 }
186203}
187204
@@ -196,6 +213,17 @@ export function managedWorkspace(directory: string | null = currentDirectory()):
196213 return directories . get ( directory ) ?. current ?. workspace ?? null
197214}
198215
216+ /** `managedWorkspace` once the overlay has run for this instance. The overlay
217+ * runs inside config load, and on a fresh instance a writer's request can be
218+ * the first thing that happens — asked before the load, the key looks free. */
219+ export async function managedWorkspaceLoaded (
220+ directory : string | null = currentDirectory ( ) ,
221+ ) : Promise < { id : string ; name : string } | null > {
222+ if ( ! directory ) return null
223+ await config ( ) . get ( )
224+ return managedWorkspace ( directory )
225+ }
226+
199227// ── per-session outcome ─────────────────────────────────────────────────────
200228
201229/** `retried`: this session already spent its one re-add on a failed handshake.
@@ -353,7 +381,9 @@ async function reconcile(sessionID: string, directory: string, state: DirectoryS
353381
354382 // Reload the overlay when the binding moved, or when a refused engine may
355383 // have appeared since (the probe memo bounds how often that is asked).
356- let reload = ! state . current || state . current . workspace . id !== workspaceId
384+ let reload = state . current
385+ ? state . current . workspace . id !== workspaceId
386+ : state . failedAt === undefined || now ( ) - state . failedAt >= FAILED_PROBE_TTL_MS
357387 if ( ! reload && state . current && ! state . current . entry ) {
358388 const probe = await probeEngine ( )
359389 reload = probe . kind === "ok"
@@ -377,7 +407,10 @@ async function reconcile(sessionID: string, directory: string, state: DirectoryS
377407 // derived entry changed, drop it when there is none any more.
378408 if ( overlayNow . entry ) {
379409 if ( ! sameEntry ( state . applied ?. entry ?? null , overlayNow . entry ) ) await mcp ( ) . add ( DATAMATE_KEY , overlayNow . entry )
380- } else if ( state . applied ?. entry ) {
410+ } else if ( state . applied ?. entry || DATAMATE_KEY in ( await mcp ( ) . status ( ) ) ) {
411+ // Ours to drop — or a client that predates the link, which MCP bootstrapped
412+ // from an IDE or hosted entry while the directory was unbound. With the
413+ // overlay refusing, nothing may serve the workspace under the key.
381414 await mcp ( ) . remove ( DATAMATE_KEY )
382415 }
383416 state . applied = overlayNow
@@ -480,7 +513,8 @@ async function reconcile(sessionID: string, directory: string, state: DirectoryS
480513export async function announceRefusal ( sessionID : string , outcome : Outcome , toast : Toast ) : Promise < void > {
481514 const rec = sessions . get ( sessionID ) ?? record ( sessionID , outcome )
482515 const detail = "error" in outcome ? outcome . error : "found" in outcome ? String ( outcome . found ) : ""
483- const signature = `${ outcome . kind } :${ detail } :${ toast . title } `
516+ const declared = "declared" in outcome ? String ( outcome . declared ?? "?" ) : ""
517+ const signature = `${ outcome . kind } :${ detail } :${ declared } :${ toast . title } `
484518 if ( rec . announced === signature ) return
485519 rec . announced = signature
486520 if ( isHeadless ( ) ) {
0 commit comments