pi-long-task 0.4.0 → 0.6.0

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.
@@ -1,10 +1,21 @@
1
+ import {
2
+ NetworkRecoveryConfigError,
3
+ resolveNetworkRecoveryConfig,
4
+ type NetworkRecoveryConfigInput,
5
+ } from "./network_recovery_config.ts";
6
+
1
7
  export interface ParsedWorkerRuntimeConfig {
2
8
  modelName?: string;
3
9
  maxAttemptsPerTask?: number;
4
10
  taskTimeoutMs?: number;
5
11
  maxBashTimeoutMs?: number;
12
+ workerSessionReuseEnabled?: boolean;
13
+ workerSessionReuseContextThresholdPercent?: number;
14
+ networkRecovery?: NetworkRecoveryConfigInput;
6
15
  }
7
16
 
17
+ type MutableWorkerRuntimeConfig = ParsedWorkerRuntimeConfig & { provider?: string; model?: string };
18
+
8
19
  const MODEL_TOKEN_RE = /[A-Za-z0-9][A-Za-z0-9._~:+/@-]*/;
9
20
  const STOP_WORDS = new Set([
10
21
  "and",
@@ -28,10 +39,13 @@ const STOP_WORDS = new Set([
28
39
  ]);
29
40
 
30
41
  export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfig {
31
- const state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string } = {};
42
+ const state: MutableWorkerRuntimeConfig = {};
32
43
 
33
44
  parseLineDirectives(text, state);
34
45
  parseNaturalLanguageDirectives(text, state);
46
+ if (state.networkRecovery) {
47
+ resolveNetworkRecoveryConfig(state.networkRecovery);
48
+ }
35
49
 
36
50
  const modelName = combineProviderAndModel(state.provider, state.model);
37
51
  return {
@@ -39,13 +53,17 @@ export function parseWorkerRuntimeConfig(text: string): ParsedWorkerRuntimeConfi
39
53
  ...(state.maxAttemptsPerTask !== undefined ? { maxAttemptsPerTask: state.maxAttemptsPerTask } : {}),
40
54
  ...(state.taskTimeoutMs !== undefined ? { taskTimeoutMs: state.taskTimeoutMs } : {}),
41
55
  ...(state.maxBashTimeoutMs !== undefined ? { maxBashTimeoutMs: state.maxBashTimeoutMs } : {}),
56
+ ...(state.workerSessionReuseEnabled !== undefined
57
+ ? { workerSessionReuseEnabled: state.workerSessionReuseEnabled }
58
+ : {}),
59
+ ...(state.workerSessionReuseContextThresholdPercent !== undefined
60
+ ? { workerSessionReuseContextThresholdPercent: state.workerSessionReuseContextThresholdPercent }
61
+ : {}),
62
+ ...(state.networkRecovery ? { networkRecovery: { ...state.networkRecovery } } : {}),
42
63
  };
43
64
  }
44
65
 
45
- function parseLineDirectives(
46
- text: string,
47
- state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
48
- ): void {
66
+ function parseLineDirectives(text: string, state: MutableWorkerRuntimeConfig): void {
49
67
  for (const rawLine of text.split(/\r?\n/)) {
50
68
  const line = rawLine.replace(/^\s{0,3}>+\s?/, "").trim();
51
69
  const match = line.match(
@@ -64,10 +82,7 @@ function parseLineDirectives(
64
82
  }
65
83
  }
66
84
 
67
- function parseNaturalLanguageDirectives(
68
- text: string,
69
- state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
70
- ): void {
85
+ function parseNaturalLanguageDirectives(text: string, state: MutableWorkerRuntimeConfig): void {
71
86
  captureTokens(
72
87
  text,
73
88
  /\bworker\s+(?:model|provider\/model)\s*(?:is|=|:|to|as)?\s*[`'"]?([A-Za-z0-9][A-Za-z0-9._~:+/@-]*)/gi,
@@ -139,13 +154,43 @@ function parseNaturalLanguageDirectives(
139
154
  state.maxBashTimeoutMs = value;
140
155
  },
141
156
  );
157
+
158
+ for (const match of text.matchAll(
159
+ /\b(?:worker\s+)?session\s+reuse(?:\s+is|\s*=|\s*:|\s+to)?\s+(enabled|disabled|on|off|true|false)\b/gi,
160
+ )) {
161
+ const enabled = booleanSetting(match[1] ?? "");
162
+ if (enabled !== undefined) state.workerSessionReuseEnabled = enabled;
163
+ }
164
+ for (const match of text.matchAll(
165
+ /\b(?:worker\s+)?(?:session\s+)?reuse\s+context(?:\s+usage)?\s+threshold\s*(?:is|=|:|to|of)?\s*(\d+(?:\.\d+)?)\s*%/gi,
166
+ )) {
167
+ const threshold = percentageFromText(match[1] ?? "");
168
+ if (threshold !== undefined) state.workerSessionReuseContextThresholdPercent = threshold;
169
+ }
142
170
  }
143
171
 
144
- function applyDirective(
145
- key: string,
146
- value: string,
147
- state: ParsedWorkerRuntimeConfig & { provider?: string; model?: string },
148
- ): void {
172
+ function applyDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
173
+ if (/\bnetwork\b/.test(key) && /\brecover(?:y|ies)?\b/.test(key)) {
174
+ applyNetworkRecoveryDirective(key, value, state);
175
+ return;
176
+ }
177
+
178
+ if (/\breuse\b/.test(key) && /\b(?:threshold|context)\b/.test(key)) {
179
+ const threshold = percentageFromText(value);
180
+ if (threshold !== undefined) {
181
+ state.workerSessionReuseContextThresholdPercent = threshold;
182
+ }
183
+ return;
184
+ }
185
+
186
+ if (/\breuse\b/.test(key)) {
187
+ const enabled = booleanSetting(value);
188
+ if (enabled !== undefined) {
189
+ state.workerSessionReuseEnabled = enabled;
190
+ }
191
+ return;
192
+ }
193
+
149
194
  if (/\bprovider\b/.test(key)) {
150
195
  const token = modelToken(value);
151
196
  if (token) {
@@ -186,6 +231,64 @@ function applyDirective(
186
231
  }
187
232
  }
188
233
 
234
+ function applyNetworkRecoveryDirective(key: string, value: string, state: MutableWorkerRuntimeConfig): void {
235
+ const recovery = (state.networkRecovery ??= {});
236
+
237
+ if (/\bbase\b/.test(key) && /\bdelay\b/.test(key)) {
238
+ recovery.baseDelayMs = requiredNetworkRecoveryDuration("base delay", value);
239
+ return;
240
+ }
241
+ if (/\bmax(?:imum)?\b/.test(key) && /\bdelay\b/.test(key)) {
242
+ recovery.maxDelayMs = requiredNetworkRecoveryDuration("maximum delay", value);
243
+ return;
244
+ }
245
+ if (/\b(?:outage|duration|wait)\b/.test(key)) {
246
+ const normalized = trimDirectiveValue(value)
247
+ .toLowerCase()
248
+ .replace(/[.!]+$/g, "")
249
+ .trim();
250
+ if (/^(?:unlimited|indefinite|indefinitely|until cancelled|until canceled)$/.test(normalized)) {
251
+ recovery.maxOutageMs = null;
252
+ return;
253
+ }
254
+ recovery.maxOutageMs = requiredNetworkRecoveryDuration("maximum outage", value);
255
+ return;
256
+ }
257
+ if (/\b(?:enabled?|enablement)\b/.test(key) || /\bnetwork recovery\b/.test(key)) {
258
+ const enabled = booleanSetting(value);
259
+ if (enabled === undefined) {
260
+ throw new NetworkRecoveryConfigError(
261
+ "Network recovery must be configured as enabled/disabled, on/off, true/false, or yes/no.",
262
+ );
263
+ }
264
+ recovery.enabled = enabled;
265
+ return;
266
+ }
267
+
268
+ throw new NetworkRecoveryConfigError(`Unknown network recovery configuration directive: ${key}.`);
269
+ }
270
+
271
+ function requiredNetworkRecoveryDuration(label: string, value: string): number {
272
+ const trimmed = trimDirectiveValue(value)
273
+ .replace(/[.!]+$/g, "")
274
+ .trim();
275
+ const match = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?$/i.exec(
276
+ trimmed,
277
+ );
278
+ if (!match) {
279
+ throw new NetworkRecoveryConfigError(
280
+ `Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
281
+ );
282
+ }
283
+ const milliseconds = durationMsFromText(trimmed, { allowBareSeconds: true });
284
+ if (milliseconds === undefined) {
285
+ throw new NetworkRecoveryConfigError(
286
+ `Network recovery ${label} must be a positive finite duration (for example 1000ms, 30s, or 5m).`,
287
+ );
288
+ }
289
+ return milliseconds;
290
+ }
291
+
189
292
  function captureTokens(text: string, pattern: RegExp, apply: (token: string) => void): void {
190
293
  for (const match of text.matchAll(pattern)) {
191
294
  const token = modelToken(match[1] ?? "");
@@ -250,6 +353,26 @@ function positiveIntegerFromText(value: string): number | undefined {
250
353
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
251
354
  }
252
355
 
356
+ function percentageFromText(value: string): number | undefined {
357
+ const match = /\d+(?:\.\d+)?/.exec(value);
358
+ if (!match) {
359
+ return undefined;
360
+ }
361
+ const parsed = Number.parseFloat(match[0]);
362
+ return Number.isFinite(parsed) && parsed > 0 && parsed <= 100 ? parsed : undefined;
363
+ }
364
+
365
+ function booleanSetting(value: string): boolean | undefined {
366
+ const normalized = trimDirectiveValue(value).toLowerCase().split(/\s+/)[0];
367
+ if (normalized === "enabled" || normalized === "on" || normalized === "true" || normalized === "yes") {
368
+ return true;
369
+ }
370
+ if (normalized === "disabled" || normalized === "off" || normalized === "false" || normalized === "no") {
371
+ return false;
372
+ }
373
+ return undefined;
374
+ }
375
+
253
376
  function durationMsFromText(value: string, options: { allowBareSeconds: boolean }): number | undefined {
254
377
  const match = /(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h)?\b/i.exec(
255
378
  value,
@@ -0,0 +1,389 @@
1
+ import path from "node:path";
2
+
3
+ /** Reuse starts conservatively rotating before two thirds of the context is occupied. */
4
+ export const DEFAULT_WORKER_SESSION_REUSE_CONTEXT_THRESHOLD_PERCENT = 62.5;
5
+ export const DEFAULT_WORKER_SESSION_REUSE_ENABLED = true;
6
+
7
+ export interface WorkerSessionReuseConfig {
8
+ enabled: boolean;
9
+ contextThresholdPercent: number;
10
+ }
11
+
12
+ export interface WorkerSessionCompatibilityInput {
13
+ coordinatorRunId: string;
14
+ repositoryRoot: string;
15
+ worktreeRoot?: string;
16
+ provider?: string;
17
+ modelName?: string;
18
+ model?: unknown;
19
+ tools?: readonly string[];
20
+ thinkingLevel?: string;
21
+ /** Any additional worker option whose value changes session behavior. */
22
+ workerOptions?: Readonly<Record<string, unknown>>;
23
+ agentDir?: string;
24
+ modelRuntime?: unknown;
25
+ authStorage?: unknown;
26
+ modelRegistry?: unknown;
27
+ settingsManager?: unknown;
28
+ resourceLoader?: unknown;
29
+ sessionFactory?: unknown;
30
+ /** Additional options that can alter the constructed AgentSession. */
31
+ sessionConfiguration?: Readonly<Record<string, unknown>>;
32
+ }
33
+
34
+ /**
35
+ * A run-scoped description of every input that can make two worker sessions
36
+ * incompatible. Individual fields are retained so policy diagnostics can name
37
+ * the mismatch instead of reporting only an opaque hash.
38
+ */
39
+ export interface WorkerSessionCompatibilityFingerprint {
40
+ coordinatorRunId: string;
41
+ repositoryRoot: string;
42
+ worktreeRoot: string;
43
+ provider: string;
44
+ model: string;
45
+ workerOptions: string;
46
+ sessionConfiguration: string;
47
+ }
48
+
49
+ export type WorkerSessionHealth =
50
+ | "healthy"
51
+ | "unknown"
52
+ | "timed_out"
53
+ | "aborted"
54
+ | "cancelled"
55
+ | "unrecoverable_error"
56
+ | "invalid_state";
57
+
58
+ export type WorkerSessionReuseState = "reusable" | "rotation_required" | "isolated";
59
+
60
+ export type WorkerSessionRetryMode = "no_retry" | "safe_partial_continuation" | "independent_retry";
61
+
62
+ export type WorkerSessionRetryReasonCode =
63
+ | "task_completed"
64
+ | "explicit_partial_work"
65
+ | "retry_after_timeout"
66
+ | "retry_after_abort"
67
+ | "retry_after_cancellation"
68
+ | "retry_after_error"
69
+ | "retry_after_invalid_result"
70
+ | "retry_after_non_partial_status";
71
+
72
+ export interface WorkerSessionRetryInput {
73
+ done: boolean;
74
+ reportedStatus: string;
75
+ completeTaskResult: boolean;
76
+ timedOut: boolean;
77
+ aborted: boolean;
78
+ cancelled?: boolean;
79
+ error?: string;
80
+ }
81
+
82
+ export interface WorkerSessionRetryDecision {
83
+ mode: WorkerSessionRetryMode;
84
+ reasonCode: WorkerSessionRetryReasonCode;
85
+ mayContinueInSession: boolean;
86
+ }
87
+
88
+ export type WorkerSessionReuseReasonCode =
89
+ | "reuse_eligible"
90
+ | "reuse_disabled"
91
+ | "session_disposed"
92
+ | "session_active"
93
+ | "health_unknown"
94
+ | "health_timed_out"
95
+ | "health_aborted"
96
+ | "health_cancelled"
97
+ | "health_unrecoverable_error"
98
+ | "health_invalid_state"
99
+ | "coordinator_run_mismatch"
100
+ | "repository_mismatch"
101
+ | "worktree_mismatch"
102
+ | "provider_mismatch"
103
+ | "model_mismatch"
104
+ | "worker_options_mismatch"
105
+ | "session_configuration_mismatch"
106
+ | "context_usage_unavailable"
107
+ | "context_usage_invalid"
108
+ | "context_threshold_reached";
109
+
110
+ export interface WorkerSessionReuseCandidate {
111
+ health: WorkerSessionHealth;
112
+ compatibility: WorkerSessionCompatibilityFingerprint;
113
+ /** A normalized percentage in the inclusive range 0..100. */
114
+ contextUsagePercent?: number;
115
+ assignmentState: "idle" | "active";
116
+ disposed: boolean;
117
+ }
118
+
119
+ export interface WorkerSessionReusePolicyInput {
120
+ config: WorkerSessionReuseConfig;
121
+ candidate: WorkerSessionReuseCandidate;
122
+ requestedCompatibility: WorkerSessionCompatibilityFingerprint;
123
+ }
124
+
125
+ export interface WorkerSessionReuseDecision {
126
+ state: WorkerSessionReuseState;
127
+ reasonCode: WorkerSessionReuseReasonCode;
128
+ reusable: boolean;
129
+ healthy: boolean;
130
+ contextUsagePercent?: number;
131
+ contextThresholdPercent: number;
132
+ }
133
+
134
+ const objectIds = new WeakMap<object, number>();
135
+ const symbolIds = new Map<symbol, number>();
136
+ let nextObjectId = 1;
137
+
138
+ export function normalizeWorkerSessionReuseThreshold(value: unknown): number {
139
+ return isValidThresholdPercentage(value) ? value : DEFAULT_WORKER_SESSION_REUSE_CONTEXT_THRESHOLD_PERCENT;
140
+ }
141
+
142
+ export function resolveWorkerSessionReuseConfig(options: {
143
+ enabled?: boolean;
144
+ contextThresholdPercent?: number;
145
+ }): WorkerSessionReuseConfig {
146
+ return {
147
+ enabled: options.enabled ?? DEFAULT_WORKER_SESSION_REUSE_ENABLED,
148
+ contextThresholdPercent: normalizeWorkerSessionReuseThreshold(options.contextThresholdPercent),
149
+ };
150
+ }
151
+
152
+ export function createWorkerSessionCompatibilityFingerprint(
153
+ input: WorkerSessionCompatibilityInput,
154
+ ): WorkerSessionCompatibilityFingerprint {
155
+ const parsedModel = splitProviderModel(input.modelName);
156
+ const provider = normalizeOptional(input.provider) ?? parsedModel.provider ?? "<default>";
157
+ const model = parsedModel.model
158
+ ? `name:${parsedModel.model}`
159
+ : input.model !== undefined
160
+ ? `value:${fingerprintValue(input.model)}`
161
+ : "<default>";
162
+ const repositoryRoot = path.resolve(input.repositoryRoot);
163
+ const worktreeRoot = path.resolve(input.worktreeRoot ?? repositoryRoot);
164
+
165
+ return {
166
+ coordinatorRunId: input.coordinatorRunId,
167
+ repositoryRoot,
168
+ worktreeRoot,
169
+ provider,
170
+ model,
171
+ workerOptions: canonicalRecord({
172
+ tools: input.tools ? [...input.tools] : undefined,
173
+ thinkingLevel: input.thinkingLevel,
174
+ workerOptions: input.workerOptions,
175
+ }),
176
+ sessionConfiguration: canonicalRecord({
177
+ agentDir: input.agentDir ? path.resolve(input.agentDir) : undefined,
178
+ modelRuntime: fingerprintOpaqueOption(input.modelRuntime),
179
+ authStorage: fingerprintOpaqueOption(input.authStorage),
180
+ modelRegistry: fingerprintOpaqueOption(input.modelRegistry),
181
+ settingsManager: fingerprintOpaqueOption(input.settingsManager),
182
+ resourceLoader: fingerprintOpaqueOption(input.resourceLoader),
183
+ sessionFactory: fingerprintOpaqueOption(input.sessionFactory),
184
+ sessionConfiguration: input.sessionConfiguration,
185
+ }),
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Classify retry ownership independently from session health. Only an explicit,
191
+ * complete `partial` result is eligible to continue in the same session; every
192
+ * failure mode remains an independent attempt and therefore starts fresh.
193
+ */
194
+ export function classifyWorkerSessionRetry(input: WorkerSessionRetryInput): WorkerSessionRetryDecision {
195
+ if (input.timedOut) {
196
+ return { mode: "independent_retry", reasonCode: "retry_after_timeout", mayContinueInSession: false };
197
+ }
198
+ if (input.cancelled) {
199
+ return { mode: "independent_retry", reasonCode: "retry_after_cancellation", mayContinueInSession: false };
200
+ }
201
+ if (input.aborted) {
202
+ return { mode: "independent_retry", reasonCode: "retry_after_abort", mayContinueInSession: false };
203
+ }
204
+ if (input.error) {
205
+ return { mode: "independent_retry", reasonCode: "retry_after_error", mayContinueInSession: false };
206
+ }
207
+ if (input.done) {
208
+ return { mode: "no_retry", reasonCode: "task_completed", mayContinueInSession: false };
209
+ }
210
+ if (!input.completeTaskResult) {
211
+ return { mode: "independent_retry", reasonCode: "retry_after_invalid_result", mayContinueInSession: false };
212
+ }
213
+ if (input.reportedStatus.trim().toLowerCase() === "partial") {
214
+ return { mode: "safe_partial_continuation", reasonCode: "explicit_partial_work", mayContinueInSession: true };
215
+ }
216
+ return { mode: "independent_retry", reasonCode: "retry_after_non_partial_status", mayContinueInSession: false };
217
+ }
218
+
219
+ /** Decide whether an idle completed session can accept the next assignment. */
220
+ export function decideWorkerSessionReuse(input: WorkerSessionReusePolicyInput): WorkerSessionReuseDecision {
221
+ const threshold = normalizeWorkerSessionReuseThreshold(input.config.contextThresholdPercent);
222
+ const base = {
223
+ contextThresholdPercent: threshold,
224
+ ...(input.candidate.contextUsagePercent !== undefined
225
+ ? { contextUsagePercent: input.candidate.contextUsagePercent }
226
+ : {}),
227
+ };
228
+
229
+ if (!input.config.enabled) {
230
+ return {
231
+ ...base,
232
+ state: "isolated",
233
+ reasonCode: "reuse_disabled",
234
+ reusable: false,
235
+ healthy: input.candidate.health === "healthy" && !input.candidate.disposed,
236
+ };
237
+ }
238
+ if (input.candidate.disposed) {
239
+ return { ...base, state: "rotation_required", reasonCode: "session_disposed", reusable: false, healthy: false };
240
+ }
241
+ if (input.candidate.assignmentState === "active") {
242
+ return { ...base, state: "rotation_required", reasonCode: "session_active", reusable: false, healthy: false };
243
+ }
244
+ if (input.candidate.health !== "healthy") {
245
+ return {
246
+ ...base,
247
+ state: "rotation_required",
248
+ reasonCode: healthReason(input.candidate.health),
249
+ reusable: false,
250
+ healthy: false,
251
+ };
252
+ }
253
+
254
+ const incompatibility = compatibilityReason(input.candidate.compatibility, input.requestedCompatibility);
255
+ if (incompatibility) {
256
+ return { ...base, state: "rotation_required", reasonCode: incompatibility, reusable: false, healthy: true };
257
+ }
258
+
259
+ const usage = input.candidate.contextUsagePercent;
260
+ if (usage === undefined) {
261
+ return {
262
+ ...base,
263
+ state: "rotation_required",
264
+ reasonCode: "context_usage_unavailable",
265
+ reusable: false,
266
+ healthy: true,
267
+ };
268
+ }
269
+ if (!isValidContextUsagePercentage(usage)) {
270
+ return {
271
+ ...base,
272
+ state: "rotation_required",
273
+ reasonCode: "context_usage_invalid",
274
+ reusable: false,
275
+ healthy: true,
276
+ };
277
+ }
278
+ if (usage >= threshold) {
279
+ return {
280
+ ...base,
281
+ state: "rotation_required",
282
+ reasonCode: "context_threshold_reached",
283
+ reusable: false,
284
+ healthy: true,
285
+ };
286
+ }
287
+
288
+ return { ...base, state: "reusable", reasonCode: "reuse_eligible", reusable: true, healthy: true };
289
+ }
290
+
291
+ function compatibilityReason(
292
+ current: WorkerSessionCompatibilityFingerprint,
293
+ requested: WorkerSessionCompatibilityFingerprint,
294
+ ): WorkerSessionReuseReasonCode | undefined {
295
+ if (current.coordinatorRunId !== requested.coordinatorRunId) return "coordinator_run_mismatch";
296
+ if (current.repositoryRoot !== requested.repositoryRoot) return "repository_mismatch";
297
+ if (current.worktreeRoot !== requested.worktreeRoot) return "worktree_mismatch";
298
+ if (current.provider !== requested.provider) return "provider_mismatch";
299
+ if (current.model !== requested.model) return "model_mismatch";
300
+ if (current.workerOptions !== requested.workerOptions) return "worker_options_mismatch";
301
+ if (current.sessionConfiguration !== requested.sessionConfiguration) return "session_configuration_mismatch";
302
+ return undefined;
303
+ }
304
+
305
+ function healthReason(health: Exclude<WorkerSessionHealth, "healthy">): WorkerSessionReuseReasonCode {
306
+ return health === "unknown" ? "health_unknown" : `health_${health}`;
307
+ }
308
+
309
+ function splitProviderModel(modelName: string | undefined): { provider?: string; model?: string } {
310
+ const normalized = normalizeOptional(modelName);
311
+ if (!normalized) return {};
312
+ const separator = normalized.indexOf("/");
313
+ if (separator <= 0 || separator === normalized.length - 1) return { model: normalized };
314
+ return { provider: normalized.slice(0, separator), model: normalized.slice(separator + 1) };
315
+ }
316
+
317
+ function canonicalRecord(value: Readonly<Record<string, unknown>>, seen = new Set<object>()): string {
318
+ return Object.keys(value)
319
+ .sort()
320
+ .filter((key) => value[key] !== undefined)
321
+ .map((key) => `${JSON.stringify(key)}:${fingerprintValue(value[key], seen)}`)
322
+ .join("|");
323
+ }
324
+
325
+ function fingerprintValue(value: unknown, seen = new Set<object>()): string {
326
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
327
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : `number:${String(value)}`;
328
+ if (typeof value === "bigint" || typeof value === "symbol" || typeof value === "function") {
329
+ return opaqueValue(value);
330
+ }
331
+ if (value === undefined) return "undefined";
332
+ if (Array.isArray(value)) {
333
+ if (seen.has(value)) return opaqueValue(value);
334
+ seen.add(value);
335
+ const result = `[${value.map((item) => fingerprintValue(item, seen)).join(",")}]`;
336
+ seen.delete(value);
337
+ return result;
338
+ }
339
+ if (typeof value === "object") {
340
+ if (seen.has(value)) return opaqueValue(value);
341
+ const prototype = Object.getPrototypeOf(value);
342
+ if (prototype === Object.prototype || prototype === null) {
343
+ seen.add(value);
344
+ const result = `{${canonicalRecord(value as Record<string, unknown>, seen)}}`;
345
+ seen.delete(value);
346
+ return result;
347
+ }
348
+ return opaqueValue(value);
349
+ }
350
+ return String(value);
351
+ }
352
+
353
+ function fingerprintOpaqueOption(value: unknown): string | undefined {
354
+ return value === undefined ? undefined : opaqueValue(value);
355
+ }
356
+
357
+ function opaqueValue(value: unknown): string {
358
+ if ((typeof value !== "object" || value === null) && typeof value !== "function" && typeof value !== "symbol") {
359
+ return `${typeof value}:${String(value)}`;
360
+ }
361
+ if (typeof value === "symbol") {
362
+ let symbolId = symbolIds.get(value);
363
+ if (!symbolId) {
364
+ symbolId = nextObjectId++;
365
+ symbolIds.set(value, symbolId);
366
+ }
367
+ return `identity:${symbolId}`;
368
+ }
369
+ const object = value as object;
370
+ let objectId = objectIds.get(object);
371
+ if (!objectId) {
372
+ objectId = nextObjectId++;
373
+ objectIds.set(object, objectId);
374
+ }
375
+ return `identity:${objectId}`;
376
+ }
377
+
378
+ function normalizeOptional(value: string | undefined): string | undefined {
379
+ const normalized = value?.trim();
380
+ return normalized || undefined;
381
+ }
382
+
383
+ function isValidThresholdPercentage(value: unknown): value is number {
384
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 100;
385
+ }
386
+
387
+ function isValidContextUsagePercentage(value: unknown): value is number {
388
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100;
389
+ }