@oh-my-pi/pi-mnemopi 16.1.7 → 16.1.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.8] - 2026-06-20
6
+
7
+ ### Fixed
8
+
9
+ - Capped per-input length in `embed()` at `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS` (default 8192 chars, override via the env var or `embeddings.maxInputChars` runtime option; `0` disables) so a long retention transcript can no longer overflow the embedding model's context window. Oversized inputs are clipped with a head/tail split so chronological transcripts keep both the opening setup and the most recent turns instead of losing the latest content under a naive prefix slice. llama.cpp's `/embeddings` server used to reject the request with `request (N tokens) exceeds the available context size`, silently dropping vector recall for that memory ([#3126](https://github.com/can1357/oh-my-pi/issues/3126)).
10
+ - Fixed the proactive-linking write path ignoring host configuration: `proactiveLinkIfEnabled` read `MNEMOPI_PROACTIVE_LINKING` directly, so a host that enabled proactive linking through `configureRecallFeatures()` had no effect unless the environment variable was also set. `proactiveLinking` is now a `RecallFeatureFlags` option resolved through a `proactiveLinkingEnabled()` fallback, matching the existing polyphonic and enhanced recall flags, with the `MNEMOPI_PROACTIVE_LINKING` environment variable still taking precedence whenever it is set. ([#2440](https://github.com/can1357/oh-my-pi/issues/2440))
11
+
5
12
  ## [16.1.3] - 2026-06-19
6
13
 
7
14
  ### Added
@@ -28,6 +28,23 @@ export declare function embeddingApiKey(env?: Env): string;
28
28
  export declare function embeddingApiUrl(env?: Env): string;
29
29
  export declare function embeddingsViaApi(env?: Env): boolean;
30
30
  export declare function embeddingsDisabled(env?: Env): boolean;
31
+ /**
32
+ * Per-input character cap applied inside `embed()` before any provider sees the text.
33
+ *
34
+ * Long retention transcripts (full multi-turn session windows) routinely outgrow
35
+ * embedding model context windows: BGE/E5 defaults are 512 tokens, bge-m3 is
36
+ * 8192, and OpenAI's text-embedding-3-* is 8192. llama.cpp's `/embeddings`
37
+ * server rejects oversized requests with `request (N tokens) exceeds the
38
+ * available context size`; OpenAI silently right-truncates. Capping at the
39
+ * source gives both backends deterministic behavior and prevents the silent
40
+ * recall degradation we saw in issue #3126.
41
+ *
42
+ * Default `8192` chars is intentionally conservative for 8192-token embedding
43
+ * contexts (bge-m3, OpenAI text-embedding-3) and CJK-heavy transcripts. Raise
44
+ * it for larger local contexts (for example Qwen3-Embedding with 32k ctx).
45
+ * `0` disables the cap.
46
+ */
47
+ export declare function embeddingMaxInputChars(env?: Env): number;
31
48
  export declare function isApiEmbeddingModel(model?: string, env?: Env): boolean;
32
49
  export declare function apiEmbeddingsAvailable(env?: Env): boolean;
33
50
  export declare function workingMemoryMaxItems(env?: Env): number;
@@ -57,21 +74,23 @@ export declare function ftsWeight(env?: Env): number;
57
74
  export declare function importanceWeight(env?: Env): number;
58
75
  export declare function normalizedRecallWeights(vec?: number, fts?: number, importance?: number): readonly [number, number, number];
59
76
  export declare function autoMigrateEnabled(env?: Env): boolean;
60
- export declare function proactiveLinkingEnabled(env?: Env): boolean;
61
77
  export interface RecallFeatureFlags {
62
78
  polyphonicRecall?: boolean;
63
79
  enhancedRecall?: boolean;
80
+ proactiveLinking?: boolean;
64
81
  }
65
82
  /**
66
83
  * Sets process-wide defaults for the env-gated recall features. Host configuration
67
- * (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall`
68
- * settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` / `MNEMOPI_ENHANCED_RECALL`
69
- * environment variables still win whenever they are set.
84
+ * (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` /
85
+ * `mnemopi.proactiveLinking` settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` /
86
+ * `MNEMOPI_ENHANCED_RECALL` / `MNEMOPI_PROACTIVE_LINKING` environment variables still
87
+ * win whenever they are set.
70
88
  */
71
89
  export declare function configureRecallFeatures(flags: RecallFeatureFlags): void;
72
90
  export declare function polyphonicRecallEnabled(env?: Env): boolean;
73
91
  export declare function temporalHalflifeHours(env?: Env): number;
74
92
  export declare function enhancedRecallEnabled(env?: Env): boolean;
93
+ export declare function proactiveLinkingEnabled(env?: Env): boolean;
75
94
  export declare function llmEnabled(env?: Env): boolean;
76
95
  export declare function llmMaxTokens(env?: Env): number;
77
96
  export declare function llmThreads(env?: Env): number;
@@ -39,6 +39,7 @@ export interface BeamConfig {
39
39
  useCloud: boolean;
40
40
  localLlmEnabled: boolean;
41
41
  maxEpisodeChars: number;
42
+ proactiveLinking?: boolean;
42
43
  }
43
44
  export interface BeamMemoryOptions {
44
45
  sessionId?: string;
@@ -47,6 +48,7 @@ export interface BeamMemoryOptions {
47
48
  authorType?: string | null;
48
49
  channelId?: string | null;
49
50
  useCloud?: boolean;
51
+ proactiveLinking?: boolean;
50
52
  eventEmitter?: (event: BeamEvent) => void;
51
53
  pluginManager?: BeamPluginManager | null;
52
54
  annotations?: AnnotationStoreLike | null;
@@ -27,6 +27,7 @@ export interface MnemopiOptions {
27
27
  readonly llmApiKey?: ApiKey;
28
28
  readonly llmModel?: string | Model<Api>;
29
29
  readonly llm?: false | MnemopiLlmRuntimeOptions | Model<Api> | MnemopiLlmCompletion;
30
+ readonly proactiveLinking?: boolean;
30
31
  /** Escalate best-effort failure logs (embedding pipeline) from debug to warn. */
31
32
  readonly debug?: boolean;
32
33
  /**
@@ -24,6 +24,8 @@ export interface MnemopiEmbeddingRuntimeOptions {
24
24
  apiUrl?: string;
25
25
  apiKey?: ApiKey;
26
26
  provider?: MnemopiEmbeddingProvider | ((texts: readonly string[]) => EmbeddingOutput | Promise<EmbeddingOutput>);
27
+ /** Override `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`. `0` disables the cap. See `config.embeddingMaxInputChars`. */
28
+ maxInputChars?: number;
27
29
  }
28
30
  export interface MnemopiLlmRuntimeOptions {
29
31
  enabled?: boolean;
@@ -49,6 +51,7 @@ export interface ResolvedMnemopiEmbeddingRuntimeOptions {
49
51
  apiUrl?: string;
50
52
  apiKey?: ApiKey;
51
53
  provider?: MnemopiEmbeddingProvider;
54
+ maxInputChars?: number;
52
55
  }
53
56
  export interface ResolvedMnemopiLlmRuntimeOptions {
54
57
  enabled?: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-mnemopi",
4
- "version": "16.1.7",
4
+ "version": "16.1.8",
5
5
  "description": "Local SQLite memory engine for Oh My Pi agents",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "16.1.7",
43
- "@oh-my-pi/pi-catalog": "16.1.7",
44
- "@oh-my-pi/pi-utils": "16.1.7",
42
+ "@oh-my-pi/pi-ai": "16.1.8",
43
+ "@oh-my-pi/pi-catalog": "16.1.8",
44
+ "@oh-my-pi/pi-utils": "16.1.8",
45
45
  "lru-cache": "11.5.1"
46
46
  },
47
47
  "peerDependencies": {
package/src/config.ts CHANGED
@@ -99,6 +99,26 @@ export function embeddingsDisabled(env: Env = process.env): boolean {
99
99
  return envString("MNEMOPI_NO_EMBEDDINGS", "", env) !== "";
100
100
  }
101
101
 
102
+ /**
103
+ * Per-input character cap applied inside `embed()` before any provider sees the text.
104
+ *
105
+ * Long retention transcripts (full multi-turn session windows) routinely outgrow
106
+ * embedding model context windows: BGE/E5 defaults are 512 tokens, bge-m3 is
107
+ * 8192, and OpenAI's text-embedding-3-* is 8192. llama.cpp's `/embeddings`
108
+ * server rejects oversized requests with `request (N tokens) exceeds the
109
+ * available context size`; OpenAI silently right-truncates. Capping at the
110
+ * source gives both backends deterministic behavior and prevents the silent
111
+ * recall degradation we saw in issue #3126.
112
+ *
113
+ * Default `8192` chars is intentionally conservative for 8192-token embedding
114
+ * contexts (bge-m3, OpenAI text-embedding-3) and CJK-heavy transcripts. Raise
115
+ * it for larger local contexts (for example Qwen3-Embedding with 32k ctx).
116
+ * `0` disables the cap.
117
+ */
118
+ export function embeddingMaxInputChars(env: Env = process.env): number {
119
+ return Math.max(0, envInt("MNEMOPI_EMBEDDING_MAX_INPUT_CHARS", 8192, env));
120
+ }
121
+
102
122
  export function isApiEmbeddingModel(model = embeddingModel(), env: Env = process.env): boolean {
103
123
  if (model.startsWith("openai/") || model.includes("text-embedding") || model.startsWith("text-embedding"))
104
124
  return true;
@@ -248,27 +268,27 @@ export function autoMigrateEnabled(env: Env = process.env): boolean {
248
268
  return envString("MNEMOPI_AUTO_MIGRATE", "1", env) !== "0";
249
269
  }
250
270
 
251
- export function proactiveLinkingEnabled(env: Env = process.env): boolean {
252
- return envString("MNEMOPI_PROACTIVE_LINKING", "0", env) === "1";
253
- }
254
-
255
271
  export interface RecallFeatureFlags {
256
272
  polyphonicRecall?: boolean;
257
273
  enhancedRecall?: boolean;
274
+ proactiveLinking?: boolean;
258
275
  }
259
276
 
260
277
  let polyphonicRecallDefault = false;
261
278
  let enhancedRecallDefault = false;
279
+ let proactiveLinkingDefault = false;
262
280
 
263
281
  /**
264
282
  * Sets process-wide defaults for the env-gated recall features. Host configuration
265
- * (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall`
266
- * settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` / `MNEMOPI_ENHANCED_RECALL`
267
- * environment variables still win whenever they are set.
283
+ * (e.g. the coding-agent `mnemopi.polyphonicRecall` / `mnemopi.enhancedRecall` /
284
+ * `mnemopi.proactiveLinking` settings) lands here; the `MNEMOPI_POLYPHONIC_RECALL` /
285
+ * `MNEMOPI_ENHANCED_RECALL` / `MNEMOPI_PROACTIVE_LINKING` environment variables still
286
+ * win whenever they are set.
268
287
  */
269
288
  export function configureRecallFeatures(flags: RecallFeatureFlags): void {
270
289
  if (flags.polyphonicRecall !== undefined) polyphonicRecallDefault = flags.polyphonicRecall;
271
290
  if (flags.enhancedRecall !== undefined) enhancedRecallDefault = flags.enhancedRecall;
291
+ if (flags.proactiveLinking !== undefined) proactiveLinkingDefault = flags.proactiveLinking;
272
292
  }
273
293
 
274
294
  export function polyphonicRecallEnabled(env: Env = process.env): boolean {
@@ -285,6 +305,11 @@ export function enhancedRecallEnabled(env: Env = process.env): boolean {
285
305
  return value === undefined ? enhancedRecallDefault : value === "1";
286
306
  }
287
307
 
308
+ export function proactiveLinkingEnabled(env: Env = process.env): boolean {
309
+ const value = envOptionalString("MNEMOPI_PROACTIVE_LINKING", env);
310
+ return value === undefined ? proactiveLinkingDefault : value === "1";
311
+ }
312
+
288
313
  export function llmEnabled(env: Env = process.env): boolean {
289
314
  return envBool("MNEMOPI_LLM_ENABLED", true, env);
290
315
  }
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import { existsSync } from "node:fs";
3
- import { ftsWeight, importanceWeight, maxEpisodeChars, vectorWeight } from "../../config";
3
+ import { ftsWeight, importanceWeight, maxEpisodeChars, proactiveLinkingEnabled, vectorWeight } from "../../config";
4
4
  import { closeQuietly, openDatabase } from "../../db";
5
5
  import { AnnotationStore } from "../annotations";
6
6
  import { EpisodicGraph } from "../episodic-graph";
@@ -69,11 +69,13 @@ const DEFAULT_CONFIG: BeamConfig = {
69
69
  useCloud: false,
70
70
  localLlmEnabled: false,
71
71
  maxEpisodeChars: 100_000,
72
+ proactiveLinking: false,
72
73
  };
73
74
 
74
75
  function normalizeConfig(options: BeamMemoryOptions): BeamConfig {
75
76
  const configured = options.config ?? {};
76
77
  const useCloud = options.useCloud ?? configured.useCloud ?? DEFAULT_CONFIG.useCloud;
78
+ const proactiveLinking = options.proactiveLinking ?? configured.proactiveLinking ?? proactiveLinkingEnabled({});
77
79
  return {
78
80
  workingMemoryLimit: configured.workingMemoryLimit ?? DEFAULT_CONFIG.workingMemoryLimit,
79
81
  workingMemoryTtlHours: configured.workingMemoryTtlHours ?? DEFAULT_CONFIG.workingMemoryTtlHours,
@@ -84,6 +86,7 @@ function normalizeConfig(options: BeamMemoryOptions): BeamConfig {
84
86
  useCloud,
85
87
  localLlmEnabled: configured.localLlmEnabled ?? DEFAULT_CONFIG.localLlmEnabled,
86
88
  maxEpisodeChars: configured.maxEpisodeChars ?? maxEpisodeChars(),
89
+ proactiveLinking,
87
90
  };
88
91
  }
89
92
  function autoMigrateAnnotations(db: Database, dbPath: string | undefined): void {
@@ -187,13 +187,18 @@ function addTemporalAnnotations(beam: BeamMemoryState, memoryId: string, timesta
187
187
  }
188
188
  }
189
189
 
190
+ function proactiveLinkingAllowed(beam: BeamMemoryState): boolean {
191
+ const override = process.env.MNEMOPI_PROACTIVE_LINKING;
192
+ return override === undefined ? beam.config.proactiveLinking === true : override === "1";
193
+ }
194
+
190
195
  function proactiveLinkIfEnabled(
191
196
  beam: BeamMemoryState,
192
197
  memoryId: string,
193
198
  content: string,
194
199
  extractEntities: boolean,
195
200
  ): void {
196
- if (process.env.MNEMOPI_PROACTIVE_LINKING !== "1") return;
201
+ if (!proactiveLinkingAllowed(beam)) return;
197
202
  try {
198
203
  const graph =
199
204
  beam.episodicGraph instanceof EpisodicGraph
@@ -54,6 +54,7 @@ export interface BeamConfig {
54
54
  useCloud: boolean;
55
55
  localLlmEnabled: boolean;
56
56
  maxEpisodeChars: number;
57
+ proactiveLinking?: boolean;
57
58
  }
58
59
 
59
60
  export interface BeamMemoryOptions {
@@ -63,6 +64,7 @@ export interface BeamMemoryOptions {
63
64
  authorType?: string | null;
64
65
  channelId?: string | null;
65
66
  useCloud?: boolean;
67
+ proactiveLinking?: boolean;
66
68
  eventEmitter?: (event: BeamEvent) => void;
67
69
  pluginManager?: BeamPluginManager | null;
68
70
  annotations?: AnnotationStoreLike | null;
@@ -120,6 +120,79 @@ export function embeddingsDisabled(): boolean {
120
120
  return $flag("MNEMOPI_NO_EMBEDDINGS");
121
121
  }
122
122
 
123
+ /**
124
+ * Resolved per-input character cap for {@link embed}.
125
+ *
126
+ * Reads (in order): the active runtime scope's `embeddings.maxInputChars`, then
127
+ * `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`, then the bundled `8192` default. `0`
128
+ * disables the cap entirely.
129
+ */
130
+ function effectiveMaxInputChars(): number {
131
+ const override = activeEmbeddingOptions()?.maxInputChars;
132
+ if (override !== undefined) return Math.max(0, Math.trunc(override));
133
+ const envValue = Number.parseInt($env.MNEMOPI_EMBEDDING_MAX_INPUT_CHARS ?? "", 10);
134
+ if (Number.isFinite(envValue) && envValue >= 0) return envValue;
135
+ return 8192;
136
+ }
137
+
138
+ /** Elision marker injected between the retained head and tail of an oversized input. */
139
+ const EMBEDDING_ELISION_MARKER = "\n\n[...]\n\n";
140
+
141
+ /**
142
+ * Right-clip a single oversized input to {@link max} chars while preserving
143
+ * both ends. Retention transcripts are chronological (oldest → newest), so a
144
+ * naive `slice(0, max)` would drop the most recent — and most semantically
145
+ * loaded — turns once a session passed the cap, leaving every later retained
146
+ * episode with essentially the same prefix vector. Keeping a head/tail split
147
+ * lets the embedding capture the topic setup at the start AND the latest
148
+ * exchanges at the end. Falls back to a tail-only clip when `max` is too
149
+ * small to fit the elision marker plus a useful slice on either side.
150
+ */
151
+ function clipToWindow(text: string, max: number): string {
152
+ if (text.length <= max) return text;
153
+ if (max <= EMBEDDING_ELISION_MARKER.length + 16) return text.slice(text.length - max);
154
+ const budget = max - EMBEDDING_ELISION_MARKER.length;
155
+ const headLen = budget >>> 1;
156
+ const tailLen = budget - headLen;
157
+ return text.slice(0, headLen) + EMBEDDING_ELISION_MARKER + text.slice(text.length - tailLen);
158
+ }
159
+
160
+ /**
161
+ * Clip every input to {@link effectiveMaxInputChars} so a runaway retention
162
+ * transcript can't blow past the embedding model's context window. Uses a
163
+ * head/tail split via {@link clipToWindow} so the embedding still sees the
164
+ * tail of the conversation (where the latest topic shifts live) and not just
165
+ * the stale prefix. Returns the original array when no input needs trimming
166
+ * (the common case); the new array is allocated only when at least one input
167
+ * is oversized so we don't churn arrays for the typical short-query path
168
+ * through `embedQuery`. Emits one debug-or-warn log per call summarizing how
169
+ * many inputs were trimmed and by how much — silent truncation was the
170
+ * original bug (#3126).
171
+ */
172
+ function capInputs(texts: readonly string[]): readonly string[] {
173
+ const max = effectiveMaxInputChars();
174
+ if (max === 0) return texts;
175
+ let trimmed: string[] | null = null;
176
+ let trimmedCount = 0;
177
+ let maxOriginalLen = 0;
178
+ for (let i = 0; i < texts.length; i++) {
179
+ const text = texts[i] ?? "";
180
+ if (text.length <= max) continue;
181
+ if (trimmed === null) trimmed = texts.slice() as string[];
182
+ trimmed[i] = clipToWindow(text, max);
183
+ trimmedCount++;
184
+ if (text.length > maxOriginalLen) maxOriginalLen = text.length;
185
+ }
186
+ if (trimmed === null) return texts;
187
+ logger[mnemopiDebugEnabled() ? "warn" : "debug"]("mnemopi: embedding input truncated", {
188
+ inputCount: texts.length,
189
+ trimmedCount,
190
+ maxOriginalLen,
191
+ maxInputChars: max,
192
+ });
193
+ return trimmed;
194
+ }
195
+
123
196
  function embeddingApiKey(): ApiKey {
124
197
  const active = activeEmbeddingOptions();
125
198
  if (active?.apiKey !== undefined) {
@@ -408,6 +481,7 @@ export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix |
408
481
  if (texts.length === 0 || embeddingsDisabled()) {
409
482
  return null;
410
483
  }
484
+ texts = capInputs(texts);
411
485
  const activeProvider = resolveEmbeddingProvider(activeEmbeddingOptions()?.provider);
412
486
  if (activeProvider !== undefined) {
413
487
  try {
@@ -43,6 +43,7 @@ export interface MnemopiOptions {
43
43
  readonly llmApiKey?: ApiKey;
44
44
  readonly llmModel?: string | Model<Api>;
45
45
  readonly llm?: false | MnemopiLlmRuntimeOptions | Model<Api> | MnemopiLlmCompletion;
46
+ readonly proactiveLinking?: boolean;
46
47
  /** Escalate best-effort failure logs (embedding pipeline) from debug to warn. */
47
48
  readonly debug?: boolean;
48
49
  /**
@@ -161,19 +162,22 @@ function resolveRuntimeOptions(options: MnemopiOptions): ResolvedMnemopiRuntimeO
161
162
  const embeddingApiUrl = options.embeddingApiUrl ?? nestedEmbeddings?.apiUrl;
162
163
  const embeddingApiKey = options.embeddingApiKey ?? nestedEmbeddings?.apiKey;
163
164
  const embeddingProvider = resolveEmbeddingProvider(nestedEmbeddings?.provider);
165
+ const embeddingMaxInputChars = nestedEmbeddings?.maxInputChars;
164
166
 
165
167
  const embeddings =
166
168
  embeddingDisabled !== undefined ||
167
169
  embeddingModel !== undefined ||
168
170
  embeddingApiUrl !== undefined ||
169
171
  embeddingApiKey !== undefined ||
170
- embeddingProvider !== undefined
172
+ embeddingProvider !== undefined ||
173
+ embeddingMaxInputChars !== undefined
171
174
  ? {
172
175
  disabled: embeddingDisabled,
173
176
  model: embeddingModel,
174
177
  apiUrl: embeddingApiUrl,
175
178
  apiKey: embeddingApiKey,
176
179
  provider: embeddingProvider,
180
+ maxInputChars: embeddingMaxInputChars,
177
181
  }
178
182
  : undefined;
179
183
 
@@ -380,6 +384,7 @@ export class Mnemopi {
380
384
  authorId: this.authorId,
381
385
  authorType: this.authorType,
382
386
  channelId: this.channelId,
387
+ proactiveLinking: options.proactiveLinking,
383
388
  });
384
389
  this.#ownsDb = options.db === undefined;
385
390
  if (options.db !== undefined) {
@@ -33,6 +33,8 @@ export interface MnemopiEmbeddingRuntimeOptions {
33
33
  apiUrl?: string;
34
34
  apiKey?: ApiKey;
35
35
  provider?: MnemopiEmbeddingProvider | ((texts: readonly string[]) => EmbeddingOutput | Promise<EmbeddingOutput>);
36
+ /** Override `MNEMOPI_EMBEDDING_MAX_INPUT_CHARS`. `0` disables the cap. See `config.embeddingMaxInputChars`. */
37
+ maxInputChars?: number;
36
38
  }
37
39
 
38
40
  export interface MnemopiLlmRuntimeOptions {
@@ -61,6 +63,7 @@ export interface ResolvedMnemopiEmbeddingRuntimeOptions {
61
63
  apiUrl?: string;
62
64
  apiKey?: ApiKey;
63
65
  provider?: MnemopiEmbeddingProvider;
66
+ maxInputChars?: number;
64
67
  }
65
68
 
66
69
  export interface ResolvedMnemopiLlmRuntimeOptions {