@threadbase-sh/streamer 1.52.2 → 1.52.4

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/index.d.cts CHANGED
@@ -114,7 +114,53 @@ interface FeatureFlagDefinition {
114
114
  /** Full env var name. */
115
115
  env: string;
116
116
  }
117
- type FeatureFlagValues = Record<string, boolean>;
117
+ /**
118
+ * Values arriving from a config source. PARTIAL on purpose: server.yaml, the
119
+ * CLI and ServerConfig each speak about the flags they mention and stay silent
120
+ * on the rest, which is what lets the precedence chain fall through.
121
+ */
122
+ type FeatureFlagValues = Partial<Record<FeatureFlagId, boolean>>;
123
+ /** Every flag, resolved. Total — see resolveFeatureFlags(). */
124
+ type ResolvedFeatureFlags = Record<FeatureFlagId, boolean>;
125
+ /**
126
+ * Which rung of the precedence chain decided a flag.
127
+ *
128
+ * Kept because the resolved boolean alone cannot answer "why is this on?", and
129
+ * that is the first question asked of a flag that is on when nobody expected it.
130
+ * `override` is the legacy ServerConfig field (see resolveFeatureFlags).
131
+ */
132
+ type FeatureFlagSource = "override" | "env" | "cli" | "yaml" | "default";
133
+ declare const FEATURE_FLAGS: readonly [{
134
+ readonly id: "codexSystemPrompt";
135
+ readonly description: string;
136
+ readonly default: false;
137
+ readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
138
+ }, {
139
+ readonly id: "sessionRehydration";
140
+ readonly description: string;
141
+ readonly default: true;
142
+ readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
143
+ }, {
144
+ readonly id: "liveActivityPush";
145
+ readonly description: string;
146
+ readonly default: false;
147
+ readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
148
+ }, {
149
+ readonly id: "ptyHost";
150
+ readonly description: string;
151
+ readonly default: false;
152
+ readonly env: "THREADBASE_FEATURE_PTY_HOST";
153
+ }];
154
+ /**
155
+ * The registry's ids as a union, derived rather than declared.
156
+ *
157
+ * This is what makes `flags.ptyHsot` a compile error instead of `undefined`.
158
+ * Under the old `Record<string, boolean>` an index signature accepted any key,
159
+ * so a typo in a consumer read as falsy and silently disabled the feature it
160
+ * was meant to gate — the exact failure the "total map" contract exists to
161
+ * prevent, reachable through the one door that contract left open.
162
+ */
163
+ type FeatureFlagId = (typeof FEATURE_FLAGS)[number]["id"];
118
164
 
119
165
  declare const CLAUDE_CODE_PROVIDER: "claude-code";
120
166
  declare const CODEX_CLI_PROVIDER: "codex-cli";
@@ -530,6 +576,7 @@ interface SessionCursor {
530
576
  }
531
577
  interface ServerConfig {
532
578
  port: number;
579
+ host?: string;
533
580
  apiKey?: string;
534
581
  /** 'cli' when --api-key was passed; rotation persists in-memory only and reverts on restart */
535
582
  apiKeySource?: "config" | "cli";
@@ -1694,7 +1741,8 @@ type ApiDeps = {
1694
1741
  };
1695
1742
  featureFlagsConfig: () => {
1696
1743
  registry: readonly FeatureFlagDefinition[];
1697
- values: FeatureFlagValues;
1744
+ values: ResolvedFeatureFlags;
1745
+ sources: Record<FeatureFlagId, FeatureFlagSource>;
1698
1746
  };
1699
1747
  publicUrl: string | null;
1700
1748
  browseRoot: string | null;
@@ -2014,6 +2062,7 @@ declare class StreamerServer {
2014
2062
  private dbPool;
2015
2063
  private dbInstanceId;
2016
2064
  private disableDb;
2065
+ private host;
2017
2066
  private skipStartupWarmup;
2018
2067
  private autoResumeOnBoot;
2019
2068
  private browseRoot;
@@ -2026,6 +2075,7 @@ declare class StreamerServer {
2026
2075
  private ptyGracePeriodMs;
2027
2076
  private defaultSystemPrompt;
2028
2077
  private featureFlags;
2078
+ private featureFlagSources;
2029
2079
  private codexSystemPromptEnabled;
2030
2080
  private defaultPermissionMode;
2031
2081
  private defaultModel;
package/dist/index.d.ts CHANGED
@@ -114,7 +114,53 @@ interface FeatureFlagDefinition {
114
114
  /** Full env var name. */
115
115
  env: string;
116
116
  }
117
- type FeatureFlagValues = Record<string, boolean>;
117
+ /**
118
+ * Values arriving from a config source. PARTIAL on purpose: server.yaml, the
119
+ * CLI and ServerConfig each speak about the flags they mention and stay silent
120
+ * on the rest, which is what lets the precedence chain fall through.
121
+ */
122
+ type FeatureFlagValues = Partial<Record<FeatureFlagId, boolean>>;
123
+ /** Every flag, resolved. Total — see resolveFeatureFlags(). */
124
+ type ResolvedFeatureFlags = Record<FeatureFlagId, boolean>;
125
+ /**
126
+ * Which rung of the precedence chain decided a flag.
127
+ *
128
+ * Kept because the resolved boolean alone cannot answer "why is this on?", and
129
+ * that is the first question asked of a flag that is on when nobody expected it.
130
+ * `override` is the legacy ServerConfig field (see resolveFeatureFlags).
131
+ */
132
+ type FeatureFlagSource = "override" | "env" | "cli" | "yaml" | "default";
133
+ declare const FEATURE_FLAGS: readonly [{
134
+ readonly id: "codexSystemPrompt";
135
+ readonly description: string;
136
+ readonly default: false;
137
+ readonly env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT";
138
+ }, {
139
+ readonly id: "sessionRehydration";
140
+ readonly description: string;
141
+ readonly default: true;
142
+ readonly env: "THREADBASE_FEATURE_SESSION_REHYDRATION";
143
+ }, {
144
+ readonly id: "liveActivityPush";
145
+ readonly description: string;
146
+ readonly default: false;
147
+ readonly env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH";
148
+ }, {
149
+ readonly id: "ptyHost";
150
+ readonly description: string;
151
+ readonly default: false;
152
+ readonly env: "THREADBASE_FEATURE_PTY_HOST";
153
+ }];
154
+ /**
155
+ * The registry's ids as a union, derived rather than declared.
156
+ *
157
+ * This is what makes `flags.ptyHsot` a compile error instead of `undefined`.
158
+ * Under the old `Record<string, boolean>` an index signature accepted any key,
159
+ * so a typo in a consumer read as falsy and silently disabled the feature it
160
+ * was meant to gate — the exact failure the "total map" contract exists to
161
+ * prevent, reachable through the one door that contract left open.
162
+ */
163
+ type FeatureFlagId = (typeof FEATURE_FLAGS)[number]["id"];
118
164
 
119
165
  declare const CLAUDE_CODE_PROVIDER: "claude-code";
120
166
  declare const CODEX_CLI_PROVIDER: "codex-cli";
@@ -530,6 +576,7 @@ interface SessionCursor {
530
576
  }
531
577
  interface ServerConfig {
532
578
  port: number;
579
+ host?: string;
533
580
  apiKey?: string;
534
581
  /** 'cli' when --api-key was passed; rotation persists in-memory only and reverts on restart */
535
582
  apiKeySource?: "config" | "cli";
@@ -1694,7 +1741,8 @@ type ApiDeps = {
1694
1741
  };
1695
1742
  featureFlagsConfig: () => {
1696
1743
  registry: readonly FeatureFlagDefinition[];
1697
- values: FeatureFlagValues;
1744
+ values: ResolvedFeatureFlags;
1745
+ sources: Record<FeatureFlagId, FeatureFlagSource>;
1698
1746
  };
1699
1747
  publicUrl: string | null;
1700
1748
  browseRoot: string | null;
@@ -2014,6 +2062,7 @@ declare class StreamerServer {
2014
2062
  private dbPool;
2015
2063
  private dbInstanceId;
2016
2064
  private disableDb;
2065
+ private host;
2017
2066
  private skipStartupWarmup;
2018
2067
  private autoResumeOnBoot;
2019
2068
  private browseRoot;
@@ -2026,6 +2075,7 @@ declare class StreamerServer {
2026
2075
  private ptyGracePeriodMs;
2027
2076
  private defaultSystemPrompt;
2028
2077
  private featureFlags;
2078
+ private featureFlagSources;
2029
2079
  private codexSystemPromptEnabled;
2030
2080
  private defaultPermissionMode;
2031
2081
  private defaultModel;
package/dist/index.js CHANGED
@@ -485,11 +485,12 @@ function validateFeatureFlagValues(raw) {
485
485
  const out = {};
486
486
  const dropped = [];
487
487
  for (const [id, value] of Object.entries(raw)) {
488
- if (!findFeatureFlag(id) || typeof value !== "boolean") {
488
+ const def = findFeatureFlag(id);
489
+ if (!def || typeof value !== "boolean") {
489
490
  dropped.push(id);
490
491
  continue;
491
492
  }
492
- out[id] = value;
493
+ out[def.id] = value;
493
494
  }
494
495
  if (dropped.length > 0) {
495
496
  getLogger("feature-flags").warn(
@@ -504,15 +505,29 @@ function validateFeatureFlagValues(raw) {
504
505
  }
505
506
  function resolveFeatureFlags(opts) {
506
507
  const env = opts?.env ?? process.env;
507
- const out = {};
508
+ const values = {};
509
+ const sources = {};
508
510
  for (const def of FEATURE_FLAGS) {
509
- out[def.id] = parseBooleanEnv(env[def.env]) ?? opts?.cli?.[def.id] ?? opts?.yaml?.[def.id] ?? def.default;
511
+ const rungs = [
512
+ ["override", opts?.override?.[def.id]],
513
+ ["env", parseBooleanEnv(env[def.env])],
514
+ ["cli", opts?.cli?.[def.id]],
515
+ ["yaml", opts?.yaml?.[def.id]]
516
+ ];
517
+ const won = rungs.find(([, v]) => v !== void 0);
518
+ values[def.id] = won ? won[1] : def.default;
519
+ sources[def.id] = won ? won[0] : "default";
510
520
  }
511
- return out;
521
+ return { values, sources };
512
522
  }
513
523
  function nonDefaultFeatureFlags(values) {
514
524
  return FEATURE_FLAGS.filter((f) => values[f.id] !== f.default).map((f) => f.id);
515
525
  }
526
+ function describeFeatureFlags(resolution) {
527
+ return FEATURE_FLAGS.map(
528
+ (f) => `${f.id}=${resolution.values[f.id]}(${resolution.sources[f.id]})`
529
+ ).join(" ");
530
+ }
516
531
 
517
532
  // src/auth.ts
518
533
  function configDir() {
@@ -14437,6 +14452,7 @@ var StreamerServer = class {
14437
14452
  dbPool = null;
14438
14453
  dbInstanceId = null;
14439
14454
  disableDb = false;
14455
+ host;
14440
14456
  // Skip the startup warm-up scan (test hook; see ServerConfig.skipStartupWarmup).
14441
14457
  skipStartupWarmup;
14442
14458
  autoResumeOnBoot;
@@ -14452,6 +14468,10 @@ var StreamerServer = class {
14452
14468
  // Resolved once at boot; see src/feature-flags.ts. Total map — every registry
14453
14469
  // id is present, so indexing it never yields undefined.
14454
14470
  featureFlags;
14471
+ // Which rung of the precedence chain decided each flag. Reported at boot and
14472
+ // over GET /api/config/feature-flags — the resolved boolean alone cannot say
14473
+ // whether a value came from the environment, the CLI, server.yaml or nowhere.
14474
+ featureFlagSources;
14455
14475
  // Derived from featureFlags.codexSystemPrompt. Kept as its own field so the
14456
14476
  // read site in startFresh() is unchanged.
14457
14477
  codexSystemPromptEnabled;
@@ -14558,16 +14578,20 @@ var StreamerServer = class {
14558
14578
  }
14559
14579
  this.verbose = config.verbose ?? false;
14560
14580
  this.disableDb = config.disableDb ?? false;
14581
+ this.host = config.host;
14561
14582
  this.skipStartupWarmup = config.skipStartupWarmup ?? false;
14562
14583
  this.autoResumeOnBoot = config.autoResumeOnBoot ?? false;
14563
14584
  this.scanProfiles = config.scanProfiles;
14564
14585
  this.codexRoots = config.codexRoots ?? [join23(homedir12(), ".codex", "sessions")];
14565
14586
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
14566
14587
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
14567
- this.featureFlags = resolveFeatureFlags({ cli: config.featureFlags, yaml: loadFeatureFlags() });
14568
- if (config.codexSystemPromptEnabled !== void 0) {
14569
- this.featureFlags.codexSystemPrompt = config.codexSystemPromptEnabled;
14570
- }
14588
+ const flagResolution = resolveFeatureFlags({
14589
+ override: config.codexSystemPromptEnabled === void 0 ? void 0 : { codexSystemPrompt: config.codexSystemPromptEnabled },
14590
+ cli: config.featureFlags,
14591
+ yaml: loadFeatureFlags()
14592
+ });
14593
+ this.featureFlags = flagResolution.values;
14594
+ this.featureFlagSources = flagResolution.sources;
14571
14595
  this.codexSystemPromptEnabled = this.featureFlags.codexSystemPrompt;
14572
14596
  this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
14573
14597
  this.defaultModel = config.defaultModel ?? "sonnet";
@@ -14612,13 +14636,12 @@ var StreamerServer = class {
14612
14636
  });
14613
14637
  this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
14614
14638
  this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
14615
- const enabledFlags = nonDefaultFeatureFlags(this.featureFlags);
14616
- if (enabledFlags.length > 0) {
14617
- this.log.info(`Feature flags active: ${enabledFlags.join(", ")}`, {
14618
- event: "config.feature_flags_active",
14619
- flags: enabledFlags
14620
- });
14621
- }
14639
+ this.log.info(`Feature flags: ${describeFeatureFlags(flagResolution)}`, {
14640
+ event: "config.feature_flags",
14641
+ values: this.featureFlags,
14642
+ sources: this.featureFlagSources,
14643
+ nonDefault: nonDefaultFeatureFlags(this.featureFlags)
14644
+ });
14622
14645
  const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config.browseRoot;
14623
14646
  if (rawRoot) {
14624
14647
  realpath2(rawRoot).then((resolved) => {
@@ -15210,7 +15233,7 @@ var StreamerServer = class {
15210
15233
  await runMigrations(this.dbPool);
15211
15234
  this.log.info("Database migrations applied", { event: "db.migrations_applied" });
15212
15235
  }
15213
- await this.bindWithRetry(port);
15236
+ await this.bindWithRetry(port, this.host);
15214
15237
  if (!this.ptyManager.isRemote()) {
15215
15238
  this.idleReaperTimer = setInterval(() => this.reapIdleSessions(), IDLE_REAP_SWEEP_MS);
15216
15239
  this.idleReaperTimer.unref?.();
@@ -15219,7 +15242,8 @@ var StreamerServer = class {
15219
15242
  {
15220
15243
  this.log.info(`Streamer server listening on port ${port}`, {
15221
15244
  port,
15222
- event: "server.listening"
15245
+ event: "server.listening",
15246
+ ...this.host !== void 0 && { host: this.host }
15223
15247
  });
15224
15248
  try {
15225
15249
  this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
@@ -15433,15 +15457,15 @@ var StreamerServer = class {
15433
15457
  // Bind the HTTP listener, retrying on a transient EADDRINUSE. See the call
15434
15458
  // site in listen() for why the race exists (kickstart -k relaunch). Total
15435
15459
  // worst case ≈ 6 × 500 ms = 3 s before the final attempt rethrows.
15436
- async bindWithRetry(port, attempts = 6, delayMs = 500) {
15460
+ async bindWithRetry(port, host, attempts = 6, delayMs = 500) {
15437
15461
  this.binding = true;
15438
15462
  try {
15439
- await this.bindWithRetryLoop(port, attempts, delayMs);
15463
+ await this.bindWithRetryLoop(port, host, attempts, delayMs);
15440
15464
  } finally {
15441
15465
  this.binding = false;
15442
15466
  }
15443
15467
  }
15444
- async bindWithRetryLoop(port, attempts, delayMs) {
15468
+ async bindWithRetryLoop(port, host, attempts, delayMs) {
15445
15469
  for (let attempt = 1; attempt <= attempts; attempt++) {
15446
15470
  try {
15447
15471
  await new Promise((resolve2, reject) => {
@@ -15455,7 +15479,11 @@ var StreamerServer = class {
15455
15479
  };
15456
15480
  this.httpServer.once("error", onError);
15457
15481
  this.httpServer.once("listening", onListening);
15458
- this.httpServer.listen(port);
15482
+ if (host === void 0) {
15483
+ this.httpServer.listen(port);
15484
+ } else {
15485
+ this.httpServer.listen(port, host);
15486
+ }
15459
15487
  });
15460
15488
  return;
15461
15489
  } catch (err) {
@@ -15463,13 +15491,18 @@ var StreamerServer = class {
15463
15491
  if (e.code === "EADDRINUSE" && attempt === attempts) {
15464
15492
  this.log.error(
15465
15493
  `port ${port} still busy (EADDRINUSE) after ${attempts} attempts; giving up`,
15466
- { port, attempts, event: "server.bind_failed" }
15494
+ {
15495
+ port,
15496
+ attempts,
15497
+ event: "server.bind_failed",
15498
+ ...host !== void 0 && { host }
15499
+ }
15467
15500
  );
15468
15501
  }
15469
15502
  if (e.code !== "EADDRINUSE" || attempt === attempts) throw err;
15470
15503
  this.log.debug?.(
15471
15504
  `port ${port} busy (EADDRINUSE), retry ${attempt}/${attempts - 1} in ${delayMs}ms`,
15472
- { port, attempt, event: "server.bind_retry" }
15505
+ { port, attempt, event: "server.bind_retry", ...host !== void 0 && { host } }
15473
15506
  );
15474
15507
  await new Promise((r) => setTimeout(r, delayMs));
15475
15508
  }
@@ -15631,7 +15664,11 @@ var StreamerServer = class {
15631
15664
  * the absence of that field is the signal that this endpoint is read-only.
15632
15665
  */
15633
15666
  getFeatureFlagsConfig() {
15634
- return { registry: FEATURE_FLAGS, values: this.featureFlags };
15667
+ return {
15668
+ registry: FEATURE_FLAGS,
15669
+ values: this.featureFlags,
15670
+ sources: this.featureFlagSources
15671
+ };
15635
15672
  }
15636
15673
  getClaudeFlagsConfig() {
15637
15674
  return {