Summary
Two related flaws in the extension's startup/logging bootstrap (src/extension.ts,
src/logging.ts, src/utils/file.ts):
- No timeout when probing for a writeable tmp directory.
WriteableTmpDir.determine() is
awaited as the very first statement of activate(). Its per-directory write/delete probes have
no timeout, so a slow or hung filesystem call blocks the entire extension's activation
indefinitely — with no fallback.
- Misleading log noise on every production-build activation. Sentry initialization runs before
determine() is awaited, and indirectly triggers a Logger.debug() call that hits
WriteableTmpDir.get() while _tmpdir is still unset, producing a scary but harmless
"get() called before determine() was awaited." console error on every single activation where
telemetry is enabled (i.e. every official release build).
Observed symptoms
Reported in the field on v2.3.1 (macOS), with the extension host repeatedly going unresponsive and
restarting:
00:07:34 [error] Unexpected error during log file operations: Error: get() called before determine() was awaited.
...at confluentinc.vscode-confluent-2.3.1-darwin-x64/extension-AgQzEPwI.js...
00:07:42 [info] Extension host (LocalProcess pid: 80295) is unresponsive.
00:09:45 [warning] Attempted to report in-progress status for unknown chat session type 'claude-code'
00:09:47 [info] Started local extension host with pid 81851.
00:09:52 [error] Unexpected error during log file operations: Error: get() called before determine() was awaited. ← same bug again
Because all extensions in VS Code share a single extension host process, a hang during this
extension's activation can prevent other extensions (in this case, a chat extension) from finishing
their own activation before VS Code's heartbeat check restarts the host — and since activation runs
from scratch on every restart, the cycle can repeat indefinitely on an affected machine.
Root cause analysis
Flaw 1 — unbounded tmp-dir probing (src/utils/file.ts:81-113)
WriteableTmpDir.determine() tries up to 7 candidate directories in sequence:
for (const dir of possibleDirs) {
...
await writeFile(fileUri, Buffer.from("test"));
await deleteFile(fileUri);
...
}
writeFile/deleteFile route through vscode.workspace.fs, with no timeout anywhere in the loop.
extension.ts:123 awaits this as the literal first statement of activate() — nothing else in the
extension runs until it resolves. If any candidate directory's write/delete round-trip stalls
(rather than failing fast with a permission error), the whole extension hangs forever.
The code already anticipates flaky filesystem behavior in this area (see the existing comment about
JamfAppInstallers on macOS at utils/file.ts:79), but doesn't defend against a slow filesystem,
only an unwriteable one. Enterprise-managed machines (MDM/Jamf policy enforcement, EDR/AV scanning
newly-created files, VPN- or network-mounted home directories) can intermittently stall plain file
I/O well past what's noticeable on an unmanaged local dev machine — enough to trip VS Code's
extension-host unresponsiveness detector.
Flaw 2 — Sentry init logs before determine() resolves (src/extension.ts:11-13, src/telemetry/sentryClient.ts:21-23)
initSentry() is called at module-evaluation time, before activate() runs:
// extension.ts
if (process.env.SENTRY_DSN) {
initSentry();
}
Inside initSentry() → getSentryScope(), the first-ever call creates the scope and logs:
// sentryClient.ts
export function getSentryScope(): Scope {
if (!sentryScope) {
logger.debug("Creating new Sentry scope"); // <-- fires here, before determine() has run
sentryScope = new Scope();
}
return sentryScope;
}
That logger.debug(...) flows through Logger.debug → EXTENSION_OUTPUT_CHANNEL.debug →
writeToLogFile → RotatingLogManager.getDir() → WriteableTmpDir.get() (logging.ts:131), which
throws because determine() hasn't set _tmpdir yet. The throw is caught by writeToLogFile's own
try/catch (logging.ts:~235) and surfaces as
console.error("Unexpected error during log file operations:", error) — exactly the log line
reported above.
This is not conditional on anything failing — it fires on every activation where SENTRY_DSN is
set, which is every official release build (see "Why this wasn't caught earlier" below). It's
swallowed and harmless on its own, but it was a significant red herring during triage: the message
implies a code-ordering bug ("called before X was awaited"), when the actual condition is a
startup-sequencing gap between Sentry init and the extension's own logging readiness.
Secondary robustness gap — cleanupOldLogFiles() (src/logging.ts:408-412)
export function cleanupOldLogFiles() {
const logger = new Logger("logging.cleanup");
const logfileDir = WriteableTmpDir.getInstance().get(); // <-- no try/catch
...
Called from extension.ts:363, well after determine() has succeeded in the current control flow,
so not implicated in the reported symptom — but it calls .get() directly with no guard, which
would throw uncaught if a future refactor ever reordered activation.
Why this wasn't caught in local testing or CI
- Flaw 2 is dead code outside of official release builds.
Gulpfile.js's build() only calls
setupSentry() (which populates process.env.SENTRY_DSN from Vault) when
process.env.NODE_ENV === "production". None of gulp build, gulp test, gulp e2e, or
gulp functional set NODE_ENV=production — only the release packaging pipeline does. So
initSentry() never runs locally or in CI test suites, and the misleading log line is
structurally unreachable outside a real .vsix release build.
- Flaw 1 requires a slow/hung filesystem, which local and CI machines don't have. Local dev
machines and CI runners have fast, local, unmanaged tmp directories — determine()'s probes
complete in milliseconds. The hang needs the kind of managed/virtualized storage (MDM policy
enforcement, EDR/AV file-scan interception, network-mounted home directory) more likely to be
present on a corporate-managed laptop, and even there the stall appears to be intermittent rather
than constant (explaining the flaky/non-deterministic reproduction).
- Unit tests stub filesystem interactions, per project convention, so the real singleton timing
across a full activate() sequence is never exercised end-to-end by the existing test suite.
Suggested fixes
- Add a timeout to
WriteableTmpDir.determine()'s per-directory probe (utils/file.ts:97-101)
— wrap the writeFile/deleteFile pair in a bounded timeout (e.g. Promise.race against a 1-2s
cap) so one bad candidate can't block activation indefinitely; treat a timeout the same as any
other per-directory failure and move on to the next candidate.
- Fix the Sentry/logging bootstrap ordering so
initSentry()'s first getSentryScope() call
can't fire a log write before determine() resolves. Two options:
- Move the
initSentry() call in extension.ts to after
await WriteableTmpDir.getInstance().determine(), or
- Make
WriteableTmpDir.get() fail soft (e.g. return undefined and have callers skip the file
write) rather than throw when called before determine(), since an early call during Sentry
bootstrap is an expected occurrence, not a genuine "should never happen" condition.
- Guard
cleanupOldLogFiles()'s direct .get() call (logging.ts:412) defensively, so a
future activation-order refactor can't reintroduce an uncaught throw here.
Suggested test coverage
- Unit test:
WriteableTmpDir.determine() completes (recording an error for that candidate, not
hanging) when a candidate directory's write call never resolves.
- Unit test: pin
WriteableTmpDir.get()'s behavior before determine() has run vs. after it
resolves with no writeable directory found — these are currently conflated under one error
message.
- Regression test: with
SENTRY_DSN set, activating the extension should not produce a
"get() called before determine() was awaited" console error — this is the case that would have
caught flaw 2 before it shipped.
Relationships
Target: implement as a patch on top of the v2.3.x release branch, ship as the next patch release,
then forward-port (cherry-pick or merge) into main.
Summary
Two related flaws in the extension's startup/logging bootstrap (
src/extension.ts,src/logging.ts,src/utils/file.ts):WriteableTmpDir.determine()isawaited as the very first statement of
activate(). Its per-directory write/delete probes haveno timeout, so a slow or hung filesystem call blocks the entire extension's activation
indefinitely — with no fallback.
determine()is awaited, and indirectly triggers aLogger.debug()call that hitsWriteableTmpDir.get()while_tmpdiris still unset, producing a scary but harmless"get() called before determine() was awaited."console error on every single activation wheretelemetry is enabled (i.e. every official release build).
Observed symptoms
Reported in the field on v2.3.1 (macOS), with the extension host repeatedly going unresponsive and
restarting:
Because all extensions in VS Code share a single extension host process, a hang during this
extension's activation can prevent other extensions (in this case, a chat extension) from finishing
their own activation before VS Code's heartbeat check restarts the host — and since activation runs
from scratch on every restart, the cycle can repeat indefinitely on an affected machine.
Root cause analysis
Flaw 1 — unbounded tmp-dir probing (
src/utils/file.ts:81-113)WriteableTmpDir.determine()tries up to 7 candidate directories in sequence:writeFile/deleteFileroute throughvscode.workspace.fs, with no timeout anywhere in the loop.extension.ts:123awaits this as the literal first statement ofactivate()— nothing else in theextension runs until it resolves. If any candidate directory's write/delete round-trip stalls
(rather than failing fast with a permission error), the whole extension hangs forever.
The code already anticipates flaky filesystem behavior in this area (see the existing comment about
JamfAppInstallers on macOS at
utils/file.ts:79), but doesn't defend against a slow filesystem,only an unwriteable one. Enterprise-managed machines (MDM/Jamf policy enforcement, EDR/AV scanning
newly-created files, VPN- or network-mounted home directories) can intermittently stall plain file
I/O well past what's noticeable on an unmanaged local dev machine — enough to trip VS Code's
extension-host unresponsiveness detector.
Flaw 2 — Sentry init logs before
determine()resolves (src/extension.ts:11-13,src/telemetry/sentryClient.ts:21-23)initSentry()is called at module-evaluation time, beforeactivate()runs:Inside
initSentry()→getSentryScope(), the first-ever call creates the scope and logs:That
logger.debug(...)flows throughLogger.debug→EXTENSION_OUTPUT_CHANNEL.debug→writeToLogFile→RotatingLogManager.getDir()→WriteableTmpDir.get()(logging.ts:131), whichthrows because
determine()hasn't set_tmpdiryet. The throw is caught bywriteToLogFile's owntry/catch (
logging.ts:~235) and surfaces asconsole.error("Unexpected error during log file operations:", error)— exactly the log linereported above.
This is not conditional on anything failing — it fires on every activation where
SENTRY_DSNisset, which is every official release build (see "Why this wasn't caught earlier" below). It's
swallowed and harmless on its own, but it was a significant red herring during triage: the message
implies a code-ordering bug ("called before X was awaited"), when the actual condition is a
startup-sequencing gap between Sentry init and the extension's own logging readiness.
Secondary robustness gap —
cleanupOldLogFiles()(src/logging.ts:408-412)Called from
extension.ts:363, well afterdetermine()has succeeded in the current control flow,so not implicated in the reported symptom — but it calls
.get()directly with no guard, whichwould throw uncaught if a future refactor ever reordered activation.
Why this wasn't caught in local testing or CI
Gulpfile.js'sbuild()only callssetupSentry()(which populatesprocess.env.SENTRY_DSNfrom Vault) whenprocess.env.NODE_ENV === "production". None ofgulp build,gulp test,gulp e2e, orgulp functionalsetNODE_ENV=production— only the release packaging pipeline does. SoinitSentry()never runs locally or in CI test suites, and the misleading log line isstructurally unreachable outside a real
.vsixrelease build.machines and CI runners have fast, local, unmanaged tmp directories —
determine()'s probescomplete in milliseconds. The hang needs the kind of managed/virtualized storage (MDM policy
enforcement, EDR/AV file-scan interception, network-mounted home directory) more likely to be
present on a corporate-managed laptop, and even there the stall appears to be intermittent rather
than constant (explaining the flaky/non-deterministic reproduction).
across a full
activate()sequence is never exercised end-to-end by the existing test suite.Suggested fixes
WriteableTmpDir.determine()'s per-directory probe (utils/file.ts:97-101)— wrap the
writeFile/deleteFilepair in a bounded timeout (e.g.Promise.raceagainst a 1-2scap) so one bad candidate can't block activation indefinitely; treat a timeout the same as any
other per-directory failure and move on to the next candidate.
initSentry()'s firstgetSentryScope()callcan't fire a log write before
determine()resolves. Two options:initSentry()call inextension.tsto afterawait WriteableTmpDir.getInstance().determine(), orWriteableTmpDir.get()fail soft (e.g. returnundefinedand have callers skip the filewrite) rather than throw when called before
determine(), since an early call during Sentrybootstrap is an expected occurrence, not a genuine "should never happen" condition.
cleanupOldLogFiles()'s direct.get()call (logging.ts:412) defensively, so afuture activation-order refactor can't reintroduce an uncaught throw here.
Suggested test coverage
WriteableTmpDir.determine()completes (recording an error for that candidate, nothanging) when a candidate directory's write call never resolves.
WriteableTmpDir.get()'s behavior beforedetermine()has run vs. after itresolves with no writeable directory found — these are currently conflated under one error
message.
SENTRY_DSNset, activating the extension should not produce a"get() called before determine() was awaited"console error — this is the case that would havecaught flaw 2 before it shipped.
Relationships
Target: implement as a patch on top of the
v2.3.xrelease branch, ship as the next patch release,then forward-port (cherry-pick or merge) into
main.