From a147e46ca95aa550b8d1c3e29efb8319e484a42a Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 12:45:06 -0700 Subject: [PATCH 01/21] fix(cost): enforce verifiable scan spending limits --- sdk/typescript/src/cost.ts | 163 ++++++++- sdk/typescript/tests-ts/api.test.ts | 30 +- sdk/typescript/tests-ts/cost.test.ts | 476 ++++++++++++++++++++++++++- 3 files changed, 630 insertions(+), 39 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 5bb5c1061..19952f005 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -46,6 +46,7 @@ interface SessionUsage { pendingLine: Buffer[]; pendingLineBytes: number; unreadable: boolean; + unreadableError: unknown; threadId: string | null; parentThreadId: string | null; workingDirectory: string | null; @@ -101,7 +102,12 @@ export class ScanCostTracker { #timer: NodeJS.Timeout | null = null; #pending: Promise = Promise.resolve(); #snapshot: ScanCostSnapshot = { usage: null, cost: null }; + #finalSnapshot: ScanCostSnapshot | null = null; + #snapshotThreadUsage: ScanTokenUsage | null = null; #lastCost: number | null = null; + #failedSession: SessionUsage | null = null; + #hasDelegatedWorkers = false; + #hasUnverifiedWorkerUsage = false; #highestFilesCompleted = 0; #expectedFilesTotal: number | undefined; @@ -160,20 +166,91 @@ export class ScanCostTracker { } public async stop(fallbackUsage?: unknown): Promise { + if (this.#finalSnapshot !== null) return this.#finalSnapshot; if (this.#timer !== null) { clearInterval(this.#timer); this.#timer = null; } - await this.refresh(); - if (this.#snapshot.usage !== null) return this.#snapshot; - const cost = estimateScanCost(this.#options.model, fallbackUsage); - this.#snapshot = { usage: fallbackUsage ?? null, cost }; + let refreshError: unknown; + try { + await this.refresh(); + } catch (error) { + if ( + fallbackUsage === undefined || + (this.#options.maxCostUsd !== undefined && + (this.#failedSession?.threadId !== this.#threadId || + this.#hasDelegatedWorkers)) + ) { + throw error; + } + refreshError = error; + } + const completedRoot = tokenUsage(fallbackUsage); + const observedRoot = this.#snapshotThreadUsage; + const completedRootCost = estimateScanCost( + this.#options.model, + completedRoot, + ); + const observedRootCost = estimateScanCost( + this.#options.model, + observedRoot, + ); + const rootUsage = + completedRoot === null || + (observedRootCost !== null && + completedRootCost !== null && + observedRootCost.estimatedUsd >= completedRootCost.estimatedUsd) + ? observedRoot + : completedRoot; + let completedUsage: unknown = + rootUsage === completedRoot ? fallbackUsage : rootUsage; + let workerUsage: ScanTokenUsage | null = null; + if (this.#hasDelegatedWorkers && this.#snapshot.usage !== null) { + const previousUsage = tokenUsage(this.#snapshot.usage); + workerUsage = + previousUsage === null + ? null + : observedRoot === null + ? previousUsage + : subtractTokenUsage(previousUsage, observedRoot); + if (rootUsage !== null && workerUsage !== null) { + completedUsage = addTokenUsage(workerUsage, rootUsage); + } + } + const cost = estimateScanCost(this.#options.model, completedUsage); + if ( + this.#options.maxCostUsd !== undefined && + (rootUsage === null || + cost === null || + (this.#hasDelegatedWorkers && + (workerUsage === null || this.#hasUnverifiedWorkerUsage))) + ) { + throw ( + refreshError ?? + new Error( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ) + ); + } + if ( + this.#snapshot.usage !== null && + (cost === null || + (this.#snapshot.cost !== null && + this.#snapshot.cost.estimatedUsd >= cost.estimatedUsd)) + ) { + this.#finalSnapshot = this.#snapshot; + return this.#finalSnapshot; + } + this.#snapshot = { usage: completedUsage ?? null, cost }; this.#reportCost(cost); - return this.#snapshot; + this.#finalSnapshot = this.#snapshot; + return this.#finalSnapshot; } async #readSessions(): Promise { if (this.#threadId === null) return; + this.#failedSession = null; + const presentSessions = new Set(); const unreadable: Array<{ session: SessionUsage; error: unknown }> = []; for await (const path of sessionFiles( join(this.#options.codexHome, "sessions"), @@ -185,6 +262,7 @@ export class ScanCostTracker { pendingLine: [], pendingLineBytes: 0, unreadable: false, + unreadableError: null, threadId: null, parentThreadId: null, workingDirectory: null, @@ -204,9 +282,22 @@ export class ScanCostTracker { this.#sessions.set(path, session); } try { - await readSessionUsage(path, session, this.#options.repository); + presentSessions.add(path); + if ( + !(await readSessionUsage( + path, + session, + this.#options.repository, + this.#options.maxCostUsd !== undefined, + )) + ) { + presentSessions.delete(path); + } } catch (error) { - if (session.threadId === null) throw error; + if (session.threadId === null) { + this.#failedSession = session; + throw error; + } unreadable.push({ session, error }); } } @@ -258,25 +349,49 @@ export class ScanCostTracker { } } } + if (included.size > 1) this.#hasDelegatedWorkers = true; + if (this.#options.maxCostUsd !== undefined) { + for (const [path, session] of this.#sessions) { + if ( + session.threadId !== null && + included.has(session.threadId) && + !presentSessions.has(path) + ) { + throw new Error( + "A tracked scan session disappeared before its cost could be verified.", + ); + } + } + } for (const { session, error } of unreadable) { - if (included.has(session.threadId!)) throw error; + if (included.has(session.threadId!)) { + this.#failedSession = session; + throw error; + } + if (isSessionAccessDenied(error)) quarantineSession(session, error); } let usage: ScanTokenUsage | null = null; + let threadUsage: ScanTokenUsage | null = null; + let hasUnverifiedWorkerUsage = false; for (const session of this.#sessions.values()) { if (session.threadId !== null && included.has(session.threadId)) { if (session.threadId !== this.#threadId) { + if (session.usage === null) hasUnverifiedWorkerUsage = true; this.#reportWorkerActivities(session); this.#reportWorkerProgress(session); } if (session.usage !== null) { + if (session.threadId === this.#threadId) threadUsage = session.usage; usage = addTokenUsage(usage, session.usage); } } } + this.#hasUnverifiedWorkerUsage = hasUnverifiedWorkerUsage; if (usage === null) return; const cost = estimateScanCost(this.#options.model, usage); this.#snapshot = { usage, cost }; + this.#snapshotThreadUsage = threadUsage; this.#reportCost(cost); } @@ -367,13 +482,20 @@ async function readSessionUsage( path: string, session: SessionUsage, repository?: string, -): Promise { - if (session.unreadable) return; + requireReadableSessions = false, +): Promise { + if (session.unreadable) { + if (requireReadableSessions) throw session.unreadableError; + return true; + } let file; try { file = await open(path, "r"); } catch (error) { - if (isMissingFile(error)) return; + if (isMissingFile(error)) return false; + if (session.threadId === null && isSessionAccessDenied(error)) { + quarantineSession(session, error); + } throw error; } try { @@ -385,14 +507,12 @@ async function readSessionUsage( buffer.length, session.offset, ); - if (bytesRead === 0) return; + if (bytesRead === 0) return true; session.offset += bytesRead; try { readSessionChunk(buffer.subarray(0, bytesRead), session, repository); } catch (error) { - session.unreadable = true; - session.pendingLine = []; - session.pendingLineBytes = 0; + quarantineSession(session, error); throw error; } } @@ -401,6 +521,13 @@ async function readSessionUsage( } } +function quarantineSession(session: SessionUsage, error: unknown): void { + session.unreadable = true; + session.unreadableError = error; + session.pendingLine = []; + session.pendingLineBytes = 0; +} + function readSessionChunk( contents: Buffer, session: SessionUsage, @@ -831,6 +958,12 @@ function isMissingFile(error: unknown): boolean { return isRecord(error) && error["code"] === "ENOENT"; } +function isSessionAccessDenied(error: unknown): boolean { + return ( + isRecord(error) && (error["code"] === "EACCES" || error["code"] === "EPERM") + ); +} + export function estimateScanCost( model: string | undefined, usage: unknown, diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index a8d6233c0..aa4ed8ea5 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -3558,7 +3558,7 @@ describe("CodexSecurity orchestration", () => { }, ); - test("saves a budgeted scan with a warning when token usage is unavailable", async () => { + test("fails a budgeted scan when token usage cannot be verified", async () => { const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3613,26 +3613,24 @@ describe("CodexSecurity orchestration", () => { }, ); - const result = await client.run(repository, { - maxCostUsd: 1, - onWarning: (warning) => { - warnings.push(warning); - }, - }); - expect(result.threadId).toBe("scan-thread"); - expect(result.cost).toBeNull(); - expect(warnings).toEqual([ - "Scan completed, but its cost limit could not be verified because model pricing or token usage is unavailable.", - ]); + await expect( + client.run(repository, { + maxCostUsd: 1, + onWarning: (warning) => { + warnings.push(warning); + }, + }), + ).rejects.toThrow( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + expect(warnings).toEqual([]); expect(commands.map(([command]) => command)).toEqual([ "register-cli-scan", "get-scan-feedback", "set-scan-thread", - "prepare-scan-completion", - "complete-scan", - "list-global-findings", + "fail-scan", ]); - expect(commands.some((args) => args[0] === "fail-scan")).toBe(false); + expect(commands.some((args) => args[0] === "complete-scan")).toBe(false); await client.close(); }); diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 520efe90c..677033c65 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -1,6 +1,7 @@ import { spawnSync } from "node:child_process"; import { appendFile, + chmod, mkdir, mkdtemp, realpath, @@ -15,6 +16,7 @@ import type { ScanActivity } from "../src/scan-activity.js"; import type { ScanProgress } from "../src/worker-progress.js"; const temporaryDirectories: string[] = []; +const testPosix = process.platform === "win32" ? test.skip : test; async function waitFor(check: () => boolean): Promise { for (let attempt = 0; attempt < 100; attempt += 1) { @@ -43,7 +45,7 @@ async function codexHome(): Promise { async function writeSession( home: string, threadId: string, - usage: Record, + usage: Record | null, parentThreadId?: string, workingDirectory?: string, timestamp?: string, @@ -71,19 +73,56 @@ async function writeSession( }), }, }), - JSON.stringify({ - type: "event_msg", - payload: { - type: "token_count", - info: { total_token_usage: usage }, - }, - }), + ...(usage === null + ? [] + : [ + JSON.stringify({ + type: "event_msg", + payload: { + type: "token_count", + info: { total_token_usage: usage }, + }, + }), + ]), "", ].join("\n"), ); return path; } +async function workerScan({ + rootUsage = { input_tokens: 100, output_tokens: 10 }, + workerUsage = { input_tokens: 100, output_tokens: 10 }, + maxCostUsd, + onCost, +}: { + rootUsage?: Record | null; + workerUsage?: Record | null; + maxCostUsd?: number; + onCost?: (cost: { estimatedUsd: number }) => void; +} = {}): Promise<{ + root: string; + worker: string; + tracker: ScanCostTracker; +}> { + const home = await codexHome(); + const root = await writeSession(home, "scan-thread", rootUsage); + const worker = await writeSession( + home, + "worker-thread", + workerUsage, + "scan-thread", + ); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd, + onCost, + }); + tracker.start("scan-thread"); + return { root, worker, tracker }; +} + async function appendSessionItem( path: string, payload: Readonly>, @@ -1566,6 +1605,183 @@ describe("live scan cost tracking", () => { expect((await tracker.stop()).cost?.inputTokens).toBe(250); }); + testPosix( + "quarantines unreadable unrelated sessions after one failure", + async () => { + const home = await codexHome(); + const unreadable = await writeSession(home, "unrelated-thread", { + input_tokens: 99, + output_tokens: 1, + }); + await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + }); + tracker.start("scan-thread"); + await chmod(unreadable, 0o000); + + try { + await expect(tracker.refresh()).rejects.toThrow(); + expect((await tracker.refresh()).cost?.inputTokens).toBe(100); + expect((await tracker.stop()).cost?.inputTokens).toBe(100); + } finally { + await chmod(unreadable, 0o600); + } + }, + ); + + testPosix( + "keeps budgeted worker sessions fail-closed after they become unreadable", + async () => { + const { worker, tracker } = await workerScan({ + workerUsage: { input_tokens: 250, output_tokens: 20 }, + maxCostUsd: 1, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(350); + await chmod(worker, 0o000); + + try { + await expect(tracker.refresh()).rejects.toThrow(); + await expect(tracker.refresh()).rejects.toThrow(); + await expect( + tracker.stop({ input_tokens: 100, output_tokens: 10 }), + ).rejects.toThrow(); + } finally { + await chmod(worker, 0o600); + } + }, + ); + + testPosix( + "rejects root-only budget fallback when the scan also has delegated workers", + async () => { + const { root, tracker } = await workerScan({ + workerUsage: { input_tokens: 250, output_tokens: 20 }, + maxCostUsd: 1, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(350); + await chmod(root, 0o000); + + try { + await expect( + tracker.stop({ input_tokens: 1_000, output_tokens: 100 }), + ).rejects.toThrow(); + } finally { + await chmod(root, 0o600); + } + }, + ); + + testPosix( + "rejects budget fallback when a worker first appears during a failed refresh", + async () => { + const home = await codexHome(); + const active = await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: 0.001, + }); + tracker.start("scan-thread"); + await tracker.refresh(); + await writeSession( + home, + "worker-thread", + { input_tokens: 10_000, output_tokens: 1_000 }, + "scan-thread", + ); + await chmod(active, 0o000); + + try { + await expect( + tracker.stop({ input_tokens: 100, output_tokens: 10 }), + ).rejects.toThrow(); + } finally { + await chmod(active, 0o600); + } + }, + ); + + testPosix( + "rejects budget fallback when an unreadable session cannot be attributed", + async () => { + const home = await codexHome(); + const unreadable = await writeSession(home, "unidentified-thread", { + input_tokens: 100, + output_tokens: 10, + }); + await chmod(unreadable, 0o000); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: 1, + }); + tracker.start("scan-thread"); + + try { + await expect( + tracker.stop({ input_tokens: 1_000, output_tokens: 100 }), + ).rejects.toThrow(); + } finally { + await chmod(unreadable, 0o600); + } + }, + ); + + test.each(["scan-thread", "worker-thread"] as const)( + "rejects a budgeted scan when its tracked %s session disappears", + async (missingThread) => { + const { root, worker, tracker } = await workerScan({ maxCostUsd: 1 }); + await tracker.refresh(); + await rm(missingThread === "scan-thread" ? root : worker); + + await expect( + tracker.stop({ input_tokens: 1_000, output_tokens: 100 }), + ).rejects.toThrow( + "A tracked scan session disappeared before its cost could be verified.", + ); + }, + ); + + test("ignores unrelated disappearing sessions when enforcing a budget", async () => { + const home = await codexHome(); + await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const unrelated = await writeSession(home, "unrelated-thread", { + input_tokens: 1_000, + output_tokens: 100, + }); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: 1, + }); + tracker.start("scan-thread"); + await tracker.refresh(); + await rm(unrelated); + + expect((await tracker.stop()).cost?.inputTokens).toBe(100); + }); + + test("preserves known usage when an optional worker session disappears", async () => { + const { worker, tracker } = await workerScan(); + await tracker.refresh(); + await rm(worker); + + expect( + (await tracker.stop({ input_tokens: 1_000, output_tokens: 100 })).cost, + ).toMatchObject({ inputTokens: 1_100, outputTokens: 110 }); + }); + test("reports a changed running cost only once", async () => { const home = await codexHome(); await writeSession(home, "scan-thread", { @@ -1607,4 +1823,248 @@ describe("live scan cost tracking", () => { }, }); }); + + test.each([ + ["missing", undefined], + ["null", null], + ["malformed", {}], + ] as const)( + "rejects a budgeted scan when completed-turn usage is %s", + async (_description, usage) => { + const tracker = new ScanCostTracker({ + codexHome: await codexHome(), + model: "gpt-5.6-terra", + maxCostUsd: 1, + }); + tracker.start("scan-thread"); + + await expect(tracker.stop(usage)).rejects.toThrow( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + }, + ); + + test("rejects a budgeted scan when the completed model cannot be priced", async () => { + const tracker = new ScanCostTracker({ + codexHome: await codexHome(), + model: "unknown-model", + maxCostUsd: 1, + }); + tracker.start("scan-thread"); + + await expect( + tracker.stop({ input_tokens: 1_000, output_tokens: 100 }), + ).rejects.toThrow( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + }); + + test("allows unavailable completed-turn usage without an explicit budget", async () => { + const tracker = new ScanCostTracker({ + codexHome: await codexHome(), + model: "gpt-5.6-terra", + }); + tracker.start("scan-thread"); + + await expect(tracker.stop(null)).resolves.toEqual({ + usage: null, + cost: null, + }); + }); + + test.each([ + ["missing", undefined], + ["malformed", {}], + ] as const)( + "rejects worker-only usage when the budgeted root completion is %s", + async (_description, completedRoot) => { + const { tracker } = await workerScan({ + rootUsage: null, + maxCostUsd: 1, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(100); + + await expect(tracker.stop(completedRoot)).rejects.toThrow( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + }, + ); + + test.each([ + ["rejects", 1], + ["allows", undefined], + ] as const)( + "%s incomplete delegated-worker usage according to the explicit budget", + async (_result, maxCostUsd) => { + const { tracker } = await workerScan({ workerUsage: null, maxCostUsd }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(100); + + const completed = tracker.stop({ + input_tokens: 1_000, + output_tokens: 100, + }); + if (maxCostUsd === undefined) { + await expect(completed).resolves.toMatchObject({ + cost: { inputTokens: 1_000, outputTokens: 100 }, + }); + } else { + await expect(completed).rejects.toThrow( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + } + }, + ); + + test("keeps final budget enforcement stable when cleanup stops tracking twice", async () => { + const budget = 0.001; + const exceeded = new AbortController(); + const reportedCosts: number[] = []; + const { tracker } = await workerScan({ + maxCostUsd: budget, + onCost: (cost) => { + reportedCosts.push(cost.estimatedUsd); + if (cost.estimatedUsd > budget) exceeded.abort(); + }, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + + expect( + (await tracker.stop({ input_tokens: 1_000, output_tokens: 100 })).cost, + ).toMatchObject({ inputTokens: 1_100, outputTokens: 110 }); + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 1_100, + outputTokens: 110, + }); + expect(reportedCosts).toEqual([0.00064, 0.00352]); + expect(exceeded.signal.aborted).toBe(true); + }); + + test("preserves higher observed root usage when completed-turn usage is stale", async () => { + const { tracker } = await workerScan({ + rootUsage: { input_tokens: 1_000, output_tokens: 100 }, + maxCostUsd: 1, + }); + + expect( + (await tracker.stop({ input_tokens: 100, output_tokens: 10 })).cost, + ).toMatchObject({ inputTokens: 1_100, outputTokens: 110 }); + }); + + test("combines worker-only observations with a valid completed root", async () => { + const budget = 0.001; + const exceeded = new AbortController(); + const { tracker } = await workerScan({ + rootUsage: null, + maxCostUsd: budget, + onCost: (cost) => { + if (cost.estimatedUsd > budget) exceeded.abort(); + }, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(100); + + expect( + (await tracker.stop({ input_tokens: 1_000, output_tokens: 100 })).cost, + ).toMatchObject({ inputTokens: 1_100, outputTokens: 110 }); + expect(exceeded.signal.aborted).toBe(true); + }); + + test("uses completed-turn usage when the final session refresh fails", async () => { + const home = await codexHome(); + await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const reportedCosts: number[] = []; + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + onCost: (cost) => reportedCosts.push(cost.estimatedUsd), + }); + tracker.start("scan-thread"); + expect((await tracker.refresh()).cost?.estimatedUsd).toBe(0.00032); + tracker.refresh = async () => { + throw new Error("session read failed"); + }; + const usage = { input_tokens: 1_000, output_tokens: 100 }; + + expect(await tracker.stop(usage)).toMatchObject({ + usage, + cost: { inputTokens: 1_000, estimatedUsd: 0.0032 }, + }); + expect(reportedCosts).toEqual([0.00032, 0.0032]); + }); + + test("adds observed worker usage to the completed root after a failed refresh", async () => { + const { tracker } = await workerScan(); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + tracker.refresh = async () => { + throw new Error("session read failed"); + }; + + expect( + (await tracker.stop({ input_tokens: 1_000, output_tokens: 100 })).cost, + ).toMatchObject({ inputTokens: 1_100, outputTokens: 110 }); + }); + + testPosix( + "enforces a budget with completed-turn usage when only its root session is unreadable", + async () => { + const home = await codexHome(); + const active = await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const budget = 0.001; + const exceeded = new AbortController(); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: budget, + onCost: (cost) => { + if (cost.estimatedUsd > budget) exceeded.abort(); + }, + }); + tracker.start("scan-thread"); + await tracker.refresh(); + await chmod(active, 0o000); + + try { + await expect(tracker.refresh()).rejects.toThrow(); + expect( + (await tracker.stop({ input_tokens: 1_000, output_tokens: 100 })) + .cost, + ).toMatchObject({ estimatedUsd: 0.0032 }); + expect(exceeded.signal.aborted).toBe(true); + } finally { + await chmod(active, 0o600); + } + }, + ); + + testPosix( + "uses completed-turn usage when an unrelated session cannot be opened", + async () => { + const home = await codexHome(); + const unreadable = await writeSession(home, "unrelated-thread", { + input_tokens: 99, + output_tokens: 1, + }); + await chmod(unreadable, 0o000); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-luna", + }); + tracker.start("scan-thread"); + const usage = { input_tokens: 1_000, output_tokens: 20 }; + + try { + expect(await tracker.stop(usage)).toMatchObject({ + usage, + cost: { inputTokens: 1_000, estimatedUsd: 0.000224 }, + }); + } finally { + await chmod(unreadable, 0o600); + } + }, + ); }); From 95b3782aefa9c4ed190ff20b226d4e77c786e354 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 13:07:12 -0700 Subject: [PATCH 02/21] fix(cost): reject incomplete final session usage --- sdk/typescript/src/cost.ts | 13 ++--- sdk/typescript/tests-ts/cost.test.ts | 74 +++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 19952f005..1676b4df9 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -107,7 +107,7 @@ export class ScanCostTracker { #lastCost: number | null = null; #failedSession: SessionUsage | null = null; #hasDelegatedWorkers = false; - #hasUnverifiedWorkerUsage = false; + #hasUnverifiedSessionUsage = false; #highestFilesCompleted = 0; #expectedFilesTotal: number | undefined; @@ -222,8 +222,8 @@ export class ScanCostTracker { this.#options.maxCostUsd !== undefined && (rootUsage === null || cost === null || - (this.#hasDelegatedWorkers && - (workerUsage === null || this.#hasUnverifiedWorkerUsage))) + this.#hasUnverifiedSessionUsage || + (this.#hasDelegatedWorkers && workerUsage === null)) ) { throw ( refreshError ?? @@ -373,11 +373,12 @@ export class ScanCostTracker { let usage: ScanTokenUsage | null = null; let threadUsage: ScanTokenUsage | null = null; - let hasUnverifiedWorkerUsage = false; + let hasUnverifiedSessionUsage = false; for (const session of this.#sessions.values()) { if (session.threadId !== null && included.has(session.threadId)) { + if (session.pendingLineBytes > 0) hasUnverifiedSessionUsage = true; if (session.threadId !== this.#threadId) { - if (session.usage === null) hasUnverifiedWorkerUsage = true; + if (session.usage === null) hasUnverifiedSessionUsage = true; this.#reportWorkerActivities(session); this.#reportWorkerProgress(session); } @@ -387,7 +388,7 @@ export class ScanCostTracker { } } } - this.#hasUnverifiedWorkerUsage = hasUnverifiedWorkerUsage; + this.#hasUnverifiedSessionUsage = hasUnverifiedSessionUsage; if (usage === null) return; const cost = estimateScanCost(this.#options.model, usage); this.#snapshot = { usage, cost }; diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 677033c65..649b26d21 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -16,7 +16,8 @@ import type { ScanActivity } from "../src/scan-activity.js"; import type { ScanProgress } from "../src/worker-progress.js"; const temporaryDirectories: string[] = []; -const testPosix = process.platform === "win32" ? test.skip : test; +const testPosix = + process.platform === "win32" || process.geteuid?.() === 0 ? test.skip : test; async function waitFor(check: () => boolean): Promise { for (let attempt = 0; attempt < 100; attempt += 1) { @@ -101,6 +102,7 @@ async function workerScan({ maxCostUsd?: number; onCost?: (cost: { estimatedUsd: number }) => void; } = {}): Promise<{ + home: string; root: string; worker: string; tracker: ScanCostTracker; @@ -120,7 +122,7 @@ async function workerScan({ onCost, }); tracker.start("scan-thread"); - return { root, worker, tracker }; + return { home, root, worker, tracker }; } async function appendSessionItem( @@ -133,6 +135,19 @@ async function appendSessionItem( ); } +async function appendIncompleteTokenUsage(path: string): Promise { + const event = JSON.stringify({ + type: "event_msg", + payload: { + type: "token_count", + info: { + total_token_usage: { input_tokens: 10_000, output_tokens: 1_000 }, + }, + }, + }); + await appendFile(path, event.slice(0, -1)); +} + function progressMessage( filesCompleted: number, filesTotal = 8, @@ -1915,6 +1930,61 @@ describe("live scan cost tracking", () => { }, ); + test.each([ + ["budgeted root", "root", 0.001, true], + ["budgeted delegated worker", "worker", 0.001, true], + ["budgeted unrelated session", "unrelated", 0.001, false], + ["unbudgeted delegated worker", "worker", undefined, false], + ] as const)( + "handles an incomplete final event from a %s", + async (_description, session, maxCostUsd, shouldReject) => { + const { home, root, worker, tracker } = await workerScan({ maxCostUsd }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + const path = + session === "root" + ? root + : session === "worker" + ? worker + : await writeSession(home, "unrelated-thread", { + input_tokens: 100, + output_tokens: 10, + }); + await appendIncompleteTokenUsage(path); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + + const completed = tracker.stop({ input_tokens: 100, output_tokens: 10 }); + if (shouldReject) { + await expect(completed).rejects.toThrow( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + } else { + await expect(completed).resolves.toMatchObject({ + cost: { inputTokens: 200, outputTokens: 20 }, + }); + } + }, + ); + + test("rejects an incomplete final root event without delegated workers", async () => { + const home = await codexHome(); + const root = await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: 0.001, + }); + tracker.start("scan-thread"); + await appendIncompleteTokenUsage(root); + expect((await tracker.refresh()).cost?.inputTokens).toBe(100); + + await expect( + tracker.stop({ input_tokens: 100, output_tokens: 10 }), + ).rejects.toThrow("The scan cost limit could not be verified"); + }); + test("keeps final budget enforcement stable when cleanup stops tracking twice", async () => { const budget = 0.001; const exceeded = new AbortController(); From ce460fe36a282a0a09376202a543b6cbf8efde29 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Sat, 15 Aug 2026 13:32:16 -0700 Subject: [PATCH 03/21] fix(cost): verify delegated worker completion and ownership --- sdk/typescript/src/cost.ts | 61 +++++++--- sdk/typescript/tests-ts/cost.test.ts | 162 +++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 16 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 1676b4df9..dd0126227 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -54,6 +54,7 @@ interface SessionUsage { inheritedUsage: ScanTokenUsage | null; replaying: boolean; usage: ScanTokenUsage | null; + taskCompleted: boolean; calls: Map; activities: ScanActivity[]; progress: ScanProgress[]; @@ -108,6 +109,7 @@ export class ScanCostTracker { #failedSession: SessionUsage | null = null; #hasDelegatedWorkers = false; #hasUnverifiedSessionUsage = false; + #hasUnfinishedWorkers = false; #highestFilesCompleted = 0; #expectedFilesTotal: number | undefined; @@ -166,6 +168,7 @@ export class ScanCostTracker { } public async stop(fallbackUsage?: unknown): Promise { + const finalizing = arguments.length > 0; if (this.#finalSnapshot !== null) return this.#finalSnapshot; if (this.#timer !== null) { clearInterval(this.#timer); @@ -223,6 +226,7 @@ export class ScanCostTracker { (rootUsage === null || cost === null || this.#hasUnverifiedSessionUsage || + (finalizing && this.#hasUnfinishedWorkers) || (this.#hasDelegatedWorkers && workerUsage === null)) ) { throw ( @@ -270,6 +274,7 @@ export class ScanCostTracker { inheritedUsage: null, replaying: false, usage: null, + taskCompleted: false, calls: new Map(), activities: [], progress: [], @@ -303,35 +308,34 @@ export class ScanCostTracker { } const included = new Set([this.#threadId]); + const ambiguousWorkers = new Set(); if (this.#options.scanDirectory !== undefined) { const scanStartedAt = [...this.#sessions.values()].find( (session) => session.threadId === this.#threadId, )?.startedAt ?? null; + const artifactsDirectory = join(this.#options.scanDirectory, "artifacts"); for (const session of this.#sessions.values()) { - if ( - session.threadId === null || - session.workingDirectory === null || - scanStartedAt === null || - session.startedAt === null || - session.startedAt < scanStartedAt - ) { + if (session.threadId === null || session.workingDirectory === null) { continue; } - const artifactsDirectory = join( - this.#options.scanDirectory, - "artifacts", - ); const workerDirectory = relative( join(artifactsDirectory, "deep_discovery", "workers"), session.workingDirectory, ).split(sep); if ( - session.workingDirectory === artifactsDirectory || - (workerDirectory.length === 2 && workerDirectory[1] === "output") + session.workingDirectory !== artifactsDirectory && + !(workerDirectory.length === 2 && workerDirectory[1] === "output") ) { - included.add(session.threadId); + continue; } + if (scanStartedAt === null || session.startedAt === null) { + if (session.threadId !== this.#threadId) { + ambiguousWorkers.add(session.threadId); + } + continue; + } + if (session.startedAt >= scanStartedAt) included.add(session.threadId); } } let changed = true; @@ -349,6 +353,9 @@ export class ScanCostTracker { } } } + const hasUnverifiedWorkerAttribution = [...ambiguousWorkers].some( + (threadId) => !included.has(threadId), + ); if (included.size > 1) this.#hasDelegatedWorkers = true; if (this.#options.maxCostUsd !== undefined) { for (const [path, session] of this.#sessions) { @@ -373,12 +380,14 @@ export class ScanCostTracker { let usage: ScanTokenUsage | null = null; let threadUsage: ScanTokenUsage | null = null; - let hasUnverifiedSessionUsage = false; + let hasUnverifiedSessionUsage = hasUnverifiedWorkerAttribution; + let hasUnfinishedWorkers = false; for (const session of this.#sessions.values()) { if (session.threadId !== null && included.has(session.threadId)) { if (session.pendingLineBytes > 0) hasUnverifiedSessionUsage = true; if (session.threadId !== this.#threadId) { if (session.usage === null) hasUnverifiedSessionUsage = true; + if (!session.taskCompleted) hasUnfinishedWorkers = true; this.#reportWorkerActivities(session); this.#reportWorkerProgress(session); } @@ -389,6 +398,7 @@ export class ScanCostTracker { } } this.#hasUnverifiedSessionUsage = hasUnverifiedSessionUsage; + this.#hasUnfinishedWorkers = hasUnfinishedWorkers; if (usage === null) return; const cost = estimateScanCost(this.#options.model, usage); this.#snapshot = { usage, cost }; @@ -582,6 +592,7 @@ function readSessionEvent( if (event["type"] === "session_meta") { if (session.threadId !== null) { session.replaying = payload["id"] !== session.threadId; + session.taskCompleted = false; return; } if (typeof payload["id"] === "string") { @@ -615,9 +626,24 @@ function readSessionEvent( payload["started_at"] >= session.startedAt ) { session.replaying = false; + session.taskCompleted = false; } return; } + if (event["type"] === "event_msg") { + if (payload["type"] === "task_started") { + session.taskCompleted = false; + return; + } + if ( + payload["type"] === "task_complete" || + payload["type"] === "turn_complete" || + payload["type"] === "turn_aborted" + ) { + session.taskCompleted = true; + return; + } + } if (event["type"] === "response_item") { session.progress.push(...sessionProgressUpdates(payload)); if (repository === undefined) return; @@ -755,7 +781,10 @@ function readSessionEvent( session.inheritedUsage === null ? usage : subtractTokenUsage(usage, session.inheritedUsage); - if (ownUsage !== null) session.usage = ownUsage; + if (ownUsage !== null) { + session.usage = ownUsage; + session.taskCompleted = false; + } } function readSessionReasoning( diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 649b26d21..554dc096a 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -50,6 +50,7 @@ async function writeSession( parentThreadId?: string, workingDirectory?: string, timestamp?: string, + completed = false, ): Promise { const directory = join(home, "sessions", "2026", "07", "26"); await mkdir(directory, { recursive: true }); @@ -74,6 +75,7 @@ async function writeSession( }), }, }), + ...(completed ? [taskEvent("task_started")] : []), ...(usage === null ? [] : [ @@ -85,20 +87,38 @@ async function writeSession( }, }), ]), + ...(completed ? [taskEvent("task_complete")] : []), "", ].join("\n"), ); return path; } +function taskEvent( + type: "task_started" | "task_complete" | "turn_complete" | "turn_aborted", +): string { + return JSON.stringify({ + type: "event_msg", + payload: { + type, + turn_id: "fixture-worker-turn", + started_at: 1_785_067_320, + ...(type === "task_started" ? {} : { completed_at: 1_785_067_321 }), + ...(type === "turn_aborted" ? { reason: "interrupted" } : {}), + }, + }); +} + async function workerScan({ rootUsage = { input_tokens: 100, output_tokens: 10 }, workerUsage = { input_tokens: 100, output_tokens: 10 }, + workerCompleted = true, maxCostUsd, onCost, }: { rootUsage?: Record | null; workerUsage?: Record | null; + workerCompleted?: boolean; maxCostUsd?: number; onCost?: (cost: { estimatedUsd: number }) => void; } = {}): Promise<{ @@ -114,6 +134,9 @@ async function workerScan({ "worker-thread", workerUsage, "scan-thread", + undefined, + undefined, + workerCompleted, ); const tracker = new ScanCostTracker({ codexHome: home, @@ -611,6 +634,78 @@ describe("live scan cost tracking", () => { }); }); + test.each([ + ["root metadata is missing", "missing", "independent", 1, true], + ["root timing is missing", "untimed", "independent", 1, true], + ["worker timing is missing", "timed", "untimed", 1, true], + ["the worker parent is unobserved", "missing", "orphaned", 1, true], + ["the worker has a parent", "missing", "parented", 1, false], + ["the worker is unrelated", "missing", "unrelated", 1, false], + ["tracking is optional", "missing", "independent", undefined, false], + ["no worker needs attribution", "missing", "none", 1, false], + ] as const)( + "verifies independent Deep worker ownership when %s", + async (_scenario, rootState, workerState, maxCostUsd, shouldReject) => { + const home = await codexHome(); + const scanDirectory = join(home, "scans", "current"); + if (rootState !== "missing") { + await writeSession( + home, + "scan-thread", + { input_tokens: 100, output_tokens: 10 }, + undefined, + scanDirectory, + rootState === "timed" ? "2026-07-26T12:00:00Z" : undefined, + ); + } + if (workerState !== "none") { + await writeSession( + home, + "deep-worker", + { input_tokens: 1_000, output_tokens: 100 }, + workerState === "parented" + ? "scan-thread" + : workerState === "orphaned" + ? "unobserved-coordinator" + : undefined, + workerState === "unrelated" + ? join(home, "another-scan", "artifacts") + : join( + scanDirectory, + "artifacts", + "deep_discovery", + "workers", + "worker", + "output", + ), + workerState === "untimed" ? undefined : "2026-07-26T12:01:00Z", + true, + ); + } + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + scanDirectory, + maxCostUsd, + }); + tracker.start("scan-thread"); + await tracker.refresh(); + + const completed = tracker.stop({ input_tokens: 100, output_tokens: 10 }); + if (shouldReject) { + await expect(completed).rejects.toThrow( + "The scan cost limit could not be verified", + ); + } else { + await expect(completed).resolves.toMatchObject({ + cost: { + inputTokens: workerState === "parented" ? 1_100 : 100, + }, + }); + } + }, + ); + test("ignores replayed parent history in forked worker sessions", async () => { const home = await codexHome(); const inherited = { @@ -1930,6 +2025,73 @@ describe("live scan cost tracking", () => { }, ); + test.each([ + ["still-running worker", "running", 1, true], + ["completed worker", "task_complete", 1, false], + ["compatible completed worker", "turn_complete", 1, false], + ["canceled worker", "turn_aborted", 1, false], + ["worker restarted after completion", "task_started", 1, true], + ["optional still-running worker", "running", undefined, false], + ] as const)( + "verifies final delegated-worker completion for a %s", + async (_scenario, state, maxCostUsd, shouldReject) => { + const { worker, tracker } = await workerScan({ + workerCompleted: false, + maxCostUsd, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + if (state !== "running") { + if (state === "task_started") { + await appendFile(worker, `${taskEvent("task_complete")}\n`); + } + await appendFile(worker, `${taskEvent(state)}\n`); + } + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + + const completed = tracker.stop({ input_tokens: 100, output_tokens: 10 }); + if (shouldReject) { + await expect(completed).rejects.toThrow( + "The scan cost limit could not be verified", + ); + } else { + await expect(completed).resolves.toMatchObject({ + cost: { inputTokens: 200, outputTokens: 20 }, + }); + } + }, + ); + + test("preserves observed active-worker costs during budget failure cleanup", async () => { + const { tracker } = await workerScan({ + workerCompleted: false, + maxCostUsd: 1, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 200, + outputTokens: 20, + }); + }); + + test.each([ + ["null", null], + ["undefined", undefined], + ] as const)( + "rejects an active worker when completed-turn usage is %s", + async (_description, usage) => { + const { tracker } = await workerScan({ + workerCompleted: false, + maxCostUsd: 1, + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + + await expect(tracker.stop(usage)).rejects.toThrow( + "The scan cost limit could not be verified", + ); + }, + ); + test.each([ ["budgeted root", "root", 0.001, true], ["budgeted delegated worker", "worker", 0.001, true], From 55f73bd91f113507852cca1c4218d4957f27e6a7 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:11:58 -0700 Subject: [PATCH 04/21] fix(cost): require completed root evidence for scan budgets --- sdk/typescript/src/cost.ts | 116 ++++++++++++++------------- sdk/typescript/tests-ts/api.test.ts | 6 +- sdk/typescript/tests-ts/cost.test.ts | 65 +++++++++++++-- 3 files changed, 127 insertions(+), 60 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index dd0126227..209154ff0 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -45,8 +45,7 @@ interface SessionUsage { offset: number; pendingLine: Buffer[]; pendingLineBytes: number; - unreadable: boolean; - unreadableError: unknown; + unreadable: { error: unknown } | null; threadId: string | null; parentThreadId: string | null; workingDirectory: string | null; @@ -83,6 +82,14 @@ interface ScanCostSnapshot { cost: ScanCost | null; } +interface ObservedSessionUsage { + root: ScanTokenUsage | null; + workers: ScanTokenUsage | null; + rootCompleted: boolean; + unverified: boolean; + unfinishedWorkers: boolean; +} + const MODEL_PRICING_NANODOLLARS: Readonly> = { "gpt-5.6": [5_000, 500, 6_250, 30_000], "gpt-5.6-sol": [5_000, 500, 6_250, 30_000], @@ -104,12 +111,9 @@ export class ScanCostTracker { #pending: Promise = Promise.resolve(); #snapshot: ScanCostSnapshot = { usage: null, cost: null }; #finalSnapshot: ScanCostSnapshot | null = null; - #snapshotThreadUsage: ScanTokenUsage | null = null; + #observedUsage: ObservedSessionUsage | null = null; #lastCost: number | null = null; - #failedSession: SessionUsage | null = null; - #hasDelegatedWorkers = false; - #hasUnverifiedSessionUsage = false; - #hasUnfinishedWorkers = false; + #rootOnlyReadError = false; #highestFilesCompleted = 0; #expectedFilesTotal: number | undefined; @@ -174,6 +178,7 @@ export class ScanCostTracker { clearInterval(this.#timer); this.#timer = null; } + const completedRoot = tokenUsage(fallbackUsage); let refreshError: unknown; try { await this.refresh(); @@ -181,15 +186,14 @@ export class ScanCostTracker { if ( fallbackUsage === undefined || (this.#options.maxCostUsd !== undefined && - (this.#failedSession?.threadId !== this.#threadId || - this.#hasDelegatedWorkers)) + (completedRoot === null || !this.#rootOnlyReadError)) ) { throw error; } refreshError = error; } - const completedRoot = tokenUsage(fallbackUsage); - const observedRoot = this.#snapshotThreadUsage; + const observed = this.#observedUsage; + const observedRoot = observed?.root ?? null; const completedRootCost = estimateScanCost( this.#options.model, completedRoot, @@ -207,27 +211,22 @@ export class ScanCostTracker { : completedRoot; let completedUsage: unknown = rootUsage === completedRoot ? fallbackUsage : rootUsage; - let workerUsage: ScanTokenUsage | null = null; - if (this.#hasDelegatedWorkers && this.#snapshot.usage !== null) { - const previousUsage = tokenUsage(this.#snapshot.usage); - workerUsage = - previousUsage === null - ? null - : observedRoot === null - ? previousUsage - : subtractTokenUsage(previousUsage, observedRoot); - if (rootUsage !== null && workerUsage !== null) { - completedUsage = addTokenUsage(workerUsage, rootUsage); - } + const workerUsage = observed?.workers ?? null; + if (workerUsage !== null) { + completedUsage = + rootUsage === null + ? workerUsage + : addTokenUsage(workerUsage, rootUsage); } const cost = estimateScanCost(this.#options.model, completedUsage); if ( this.#options.maxCostUsd !== undefined && (rootUsage === null || cost === null || - this.#hasUnverifiedSessionUsage || - (finalizing && this.#hasUnfinishedWorkers) || - (this.#hasDelegatedWorkers && workerUsage === null)) + observed?.unverified || + (finalizing && + ((completedRoot === null && !observed?.rootCompleted) || + observed?.unfinishedWorkers))) ) { throw ( refreshError ?? @@ -253,7 +252,7 @@ export class ScanCostTracker { async #readSessions(): Promise { if (this.#threadId === null) return; - this.#failedSession = null; + this.#rootOnlyReadError = false; const presentSessions = new Set(); const unreadable: Array<{ session: SessionUsage; error: unknown }> = []; for await (const path of sessionFiles( @@ -265,8 +264,7 @@ export class ScanCostTracker { offset: 0, pendingLine: [], pendingLineBytes: 0, - unreadable: false, - unreadableError: null, + unreadable: null, threadId: null, parentThreadId: null, workingDirectory: null, @@ -299,10 +297,7 @@ export class ScanCostTracker { presentSessions.delete(path); } } catch (error) { - if (session.threadId === null) { - this.#failedSession = session; - throw error; - } + if (session.threadId === null) throw error; unreadable.push({ session, error }); } } @@ -356,7 +351,6 @@ export class ScanCostTracker { const hasUnverifiedWorkerAttribution = [...ambiguousWorkers].some( (threadId) => !included.has(threadId), ); - if (included.size > 1) this.#hasDelegatedWorkers = true; if (this.#options.maxCostUsd !== undefined) { for (const [path, session] of this.#sessions) { if ( @@ -372,37 +366,52 @@ export class ScanCostTracker { } for (const { session, error } of unreadable) { if (included.has(session.threadId!)) { - this.#failedSession = session; + this.#rootOnlyReadError = + session.threadId === this.#threadId && + included.size === 1 && + !hasUnverifiedWorkerAttribution; throw error; } if (isSessionAccessDenied(error)) quarantineSession(session, error); } - let usage: ScanTokenUsage | null = null; - let threadUsage: ScanTokenUsage | null = null; - let hasUnverifiedSessionUsage = hasUnverifiedWorkerAttribution; - let hasUnfinishedWorkers = false; + const observed: ObservedSessionUsage = { + root: null, + workers: null, + rootCompleted: false, + unverified: hasUnverifiedWorkerAttribution, + unfinishedWorkers: false, + }; for (const session of this.#sessions.values()) { if (session.threadId !== null && included.has(session.threadId)) { - if (session.pendingLineBytes > 0) hasUnverifiedSessionUsage = true; - if (session.threadId !== this.#threadId) { - if (session.usage === null) hasUnverifiedSessionUsage = true; - if (!session.taskCompleted) hasUnfinishedWorkers = true; + if (session.pendingLineBytes > 0) observed.unverified = true; + if (session.threadId === this.#threadId) { + observed.rootCompleted = session.taskCompleted; + if (session.usage !== null) { + observed.root = addTokenUsage(observed.root, session.usage); + } + } else { + if (session.usage === null) { + observed.unverified = true; + } else { + observed.workers = addTokenUsage(observed.workers, session.usage); + } + if (!session.taskCompleted) observed.unfinishedWorkers = true; this.#reportWorkerActivities(session); this.#reportWorkerProgress(session); } - if (session.usage !== null) { - if (session.threadId === this.#threadId) threadUsage = session.usage; - usage = addTokenUsage(usage, session.usage); - } } } - this.#hasUnverifiedSessionUsage = hasUnverifiedSessionUsage; - this.#hasUnfinishedWorkers = hasUnfinishedWorkers; + this.#observedUsage = observed; + const usage = + observed.root === null + ? observed.workers + : observed.workers === null + ? observed.root + : addTokenUsage(observed.root, observed.workers); if (usage === null) return; const cost = estimateScanCost(this.#options.model, usage); this.#snapshot = { usage, cost }; - this.#snapshotThreadUsage = threadUsage; this.#reportCost(cost); } @@ -495,8 +504,8 @@ async function readSessionUsage( repository?: string, requireReadableSessions = false, ): Promise { - if (session.unreadable) { - if (requireReadableSessions) throw session.unreadableError; + if (session.unreadable !== null) { + if (requireReadableSessions) throw session.unreadable.error; return true; } let file; @@ -533,8 +542,7 @@ async function readSessionUsage( } function quarantineSession(session: SessionUsage, error: unknown): void { - session.unreadable = true; - session.unreadableError = error; + session.unreadable = { error }; session.pendingLine = []; session.pendingLineBytes = 0; } diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 97627f5ed..2fa0ae76b 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -3615,7 +3615,7 @@ describe("CodexSecurity orchestration", () => { }, ); - test("fails a budgeted scan when token usage cannot be verified", async () => { + test("fails a budgeted scan when only unfinished root usage is available", async () => { const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3658,6 +3658,10 @@ describe("CodexSecurity orchestration", () => { }; executable._exec.run = async function* () { await copyCompletedScan(root); + await writeUsageSession(codexHome, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); yield JSON.stringify({ type: "thread.started", thread_id: "scan-thread", diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 554dc096a..291900e8b 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -101,7 +101,7 @@ function taskEvent( type: "event_msg", payload: { type, - turn_id: "fixture-worker-turn", + turn_id: "fixture-turn", started_at: 1_785_067_320, ...(type === "task_started" ? {} : { completed_at: 1_785_067_321 }), ...(type === "turn_aborted" ? { reason: "interrupted" } : {}), @@ -1786,10 +1786,11 @@ describe("live scan cost tracking", () => { }, ); - testPosix( - "rejects budget fallback when a worker first appears during a failed refresh", - async () => { + testPosix.each(["parented", "independent"] as const)( + "rejects budget fallback for a newly discovered %s worker during a failed refresh", + async (workerKind) => { const home = await codexHome(); + const scanDirectory = join(home, "scan"); const active = await writeSession(home, "scan-thread", { input_tokens: 100, output_tokens: 10, @@ -1797,6 +1798,7 @@ describe("live scan cost tracking", () => { const tracker = new ScanCostTracker({ codexHome: home, model: "gpt-5.6-terra", + scanDirectory, maxCostUsd: 0.001, }); tracker.start("scan-thread"); @@ -1805,7 +1807,11 @@ describe("live scan cost tracking", () => { home, "worker-thread", { input_tokens: 10_000, output_tokens: 1_000 }, - "scan-thread", + workerKind === "parented" ? "scan-thread" : undefined, + workerKind === "independent" + ? join(scanDirectory, "artifacts") + : undefined, + "2026-07-26T12:01:00Z", ); await chmod(active, 0o000); @@ -1954,6 +1960,37 @@ describe("live scan cost tracking", () => { }, ); + test.each([ + ["missing", undefined], + ["null", null], + ["malformed", {}], + ] as const)( + "requires completed root-session evidence when final usage is %s", + async (_description, usage) => { + const home = await codexHome(); + const root = await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: 1, + }); + tracker.start("scan-thread"); + expect((await tracker.refresh()).cost?.inputTokens).toBe(100); + + await expect(tracker.stop(usage)).rejects.toThrow( + "The scan cost limit could not be verified", + ); + await appendFile(root, `${taskEvent("task_complete")}\n`); + expect((await tracker.stop(usage)).cost).toMatchObject({ + inputTokens: 100, + outputTokens: 10, + }); + }, + ); + test("rejects a budgeted scan when the completed model cannot be priced", async () => { const tracker = new ScanCostTracker({ codexHome: await codexHome(), @@ -1982,6 +2019,24 @@ describe("live scan cost tracking", () => { }); }); + test("preserves unfinished root usage when tracking is optional", async () => { + const home = await codexHome(); + await writeSession(home, "scan-thread", { + input_tokens: 100, + output_tokens: 10, + }); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + }); + tracker.start("scan-thread"); + + expect((await tracker.stop(undefined)).cost).toMatchObject({ + inputTokens: 100, + outputTokens: 10, + }); + }); + test.each([ ["missing", undefined], ["malformed", {}], From 1f072dbf5e68d96e26ffa83a28252d0331416871 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:36:40 -0700 Subject: [PATCH 05/21] fix(cost): preserve definitive budget overages --- sdk/typescript/src/cost.ts | 63 ++++-- sdk/typescript/tests-ts/api.test.ts | 318 ++++++++++++++++----------- sdk/typescript/tests-ts/cost.test.ts | 54 ++++- 3 files changed, 276 insertions(+), 159 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 209154ff0..d28ee8a08 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -179,18 +179,11 @@ export class ScanCostTracker { this.#timer = null; } const completedRoot = tokenUsage(fallbackUsage); - let refreshError: unknown; + let refreshFailure: { error: unknown } | null = null; try { await this.refresh(); } catch (error) { - if ( - fallbackUsage === undefined || - (this.#options.maxCostUsd !== undefined && - (completedRoot === null || !this.#rootOnlyReadError)) - ) { - throw error; - } - refreshError = error; + refreshFailure = { error }; } const observed = this.#observedUsage; const observedRoot = observed?.root ?? null; @@ -219,6 +212,34 @@ export class ScanCostTracker { : addTokenUsage(workerUsage, rootUsage); } const cost = estimateScanCost(this.#options.model, completedUsage); + const snapshot = + this.#snapshot.usage !== null && + (cost === null || + (this.#snapshot.cost !== null && + this.#snapshot.cost.estimatedUsd >= cost.estimatedUsd)) + ? this.#snapshot + : { usage: completedUsage ?? null, cost }; + if ( + this.#options.maxCostUsd !== undefined && + snapshot.cost !== null && + snapshot.cost.estimatedUsd > this.#options.maxCostUsd + ) { + this.#snapshot = snapshot; + this.#finalSnapshot = snapshot; + this.#reportCost(snapshot.cost); + } + if (refreshFailure !== null) { + if ( + fallbackUsage === undefined || + (this.#options.maxCostUsd !== undefined && + (completedRoot === null || !this.#rootOnlyReadError)) + ) { + throw refreshFailure.error; + } + if (this.#options.maxCostUsd === undefined) { + this.#options.onError?.(refreshFailure.error); + } + } if ( this.#options.maxCostUsd !== undefined && (rootUsage === null || @@ -229,25 +250,16 @@ export class ScanCostTracker { observed?.unfinishedWorkers))) ) { throw ( - refreshError ?? + refreshFailure?.error ?? new Error( "The scan cost limit could not be verified because model pricing or token usage is unavailable.", ) ); } - if ( - this.#snapshot.usage !== null && - (cost === null || - (this.#snapshot.cost !== null && - this.#snapshot.cost.estimatedUsd >= cost.estimatedUsd)) - ) { - this.#finalSnapshot = this.#snapshot; - return this.#finalSnapshot; - } - this.#snapshot = { usage: completedUsage ?? null, cost }; - this.#reportCost(cost); - this.#finalSnapshot = this.#snapshot; - return this.#finalSnapshot; + this.#snapshot = snapshot; + this.#finalSnapshot = snapshot; + this.#reportCost(snapshot.cost); + return snapshot; } async #readSessions(): Promise { @@ -610,7 +622,10 @@ function readSessionEvent( session.workingDirectory = payload["cwd"]; } if (typeof payload["timestamp"] === "string") { - session.startedAt = Math.floor(Date.parse(payload["timestamp"]) / 1_000); + const startedAt = Date.parse(payload["timestamp"]); + session.startedAt = Number.isFinite(startedAt) + ? Math.floor(startedAt / 1_000) + : null; } const source = payload["source"]; const subagent = isRecord(source) ? source["subagent"] : undefined; diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 2fa0ae76b..5b35d76bb 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2472,7 +2472,6 @@ describe("CodexSecurity orchestration", () => { const scan = client.run(repository, { ...(enforceCostLimit ? { maxCostUsd: 1 } : {}), - onActivity: () => {}, onWarning: (warning) => warnings.push(warning), }); if (enforceCostLimit) { @@ -3345,142 +3344,168 @@ describe("CodexSecurity orchestration", () => { }, ); - test("stops and records a scan as soon as its live cost exceeds the limit", async () => { - const root = await temporaryDirectory(); - const repository = join(root, "repository"); - const codexHome = join(root, "codex-home"); - const scanDir = join(root, "scan"); - await mkdir(repository); - await mkdir(codexHome); - await mkdir(scanDir, { mode: 0o700 }); - const commands: Array = []; - const costs: number[] = []; - let turns = 0; - const cost = { - model: "gpt-5.6-sol", - inputTokens: 1_250, - cachedInputTokens: 200, - cacheWriteInputTokens: 0, - outputTokens: 30, - estimatedUsd: 0.00625, - }; - const client = new TestClient( - {}, - { - environment: {}, - prepareRuntime: async () => preparedRuntime(codexHome), - resolvePluginPython: async () => "/managed/python", - prepareOutputDir: async () => scanDir, - repositoryRevision: async () => "deadbeef", - runWorkbench: async (_options: unknown, args: readonly string[]) => { - commands.push(args); - if (args[0] === "register-cli-scan") { - return mockScanRegistration(args); - } - if (args[0] === "get-scan-feedback") { - return { - scanId: "scan_example_001", - targetId: "target_sha256_example", - falsePositives: [], - }; - } - return {}; - }, - createCodex: () => ({ - startThread: () => ({ - id: null, - async runStreamed( - _input: string, - options: { signal: AbortSignal }, - ) { - turns += 1; - async function* events(): AsyncGenerator { - yield { type: "thread.started", thread_id: "scan-thread" }; - await Promise.all([ - writeUsageSession(codexHome, "scan-thread", { - input_tokens: 500, - cached_input_tokens: 100, - output_tokens: 10, - }), - writeUsageSession( - codexHome, - "worker-thread", - { - input_tokens: 750, - cached_input_tokens: 100, - output_tokens: 20, - }, - "scan-thread", - ), - ]); - await new Promise((resolve) => { - if (options.signal.aborted) { - resolve(); - } else { - options.signal.addEventListener("abort", () => resolve(), { - once: true, - }); + test.each(["live", "completed"] as const)( + "stops and records a scan when its %s cost exceeds the limit", + async (costSource) => { + const root = await temporaryDirectory(); + const repository = join(root, "repository"); + const codexHome = join(root, "codex-home"); + const scanDir = join(root, "scan"); + await mkdir(repository); + await mkdir(codexHome); + await mkdir(scanDir, { mode: 0o700 }); + const commands: Array = []; + const costs: number[] = []; + let turns = 0; + const cost = { + model: "gpt-5.6-sol", + inputTokens: 1_250, + cachedInputTokens: 200, + cacheWriteInputTokens: 0, + outputTokens: 30, + estimatedUsd: 0.00625, + }; + const client = new TestClient( + {}, + { + environment: {}, + prepareRuntime: async () => preparedRuntime(codexHome), + resolvePluginPython: async () => "/managed/python", + prepareOutputDir: async () => scanDir, + repositoryRevision: async () => "deadbeef", + runWorkbench: async (_options: unknown, args: readonly string[]) => { + commands.push(args); + if (args[0] === "register-cli-scan") { + return mockScanRegistration(args); + } + if (args[0] === "get-scan-feedback") { + return { + scanId: "scan_example_001", + targetId: "target_sha256_example", + falsePositives: [], + }; + } + return {}; + }, + createCodex: () => ({ + startThread: () => ({ + id: null, + async runStreamed( + _input: string, + options: { signal: AbortSignal }, + ) { + turns += 1; + async function* events(): AsyncGenerator { + yield { type: "thread.started", thread_id: "scan-thread" }; + await Promise.all([ + writeUsageSession(codexHome, "scan-thread", { + input_tokens: costSource === "live" ? 500 : 100, + cached_input_tokens: costSource === "live" ? 100 : 0, + output_tokens: costSource === "live" ? 10 : 1, + }), + writeUsageSession( + codexHome, + "worker-thread", + { + input_tokens: costSource === "live" ? 750 : 250, + cached_input_tokens: 100, + output_tokens: costSource === "live" ? 20 : 10, + }, + "scan-thread", + ), + ]); + if (costSource === "completed") { + await copyCompletedScan(root); + yield { + type: "turn.completed", + usage: { + input_tokens: 1_000, + cached_input_tokens: 100, + cache_write_input_tokens: 0, + output_tokens: 20, + reasoning_output_tokens: 0, + }, + }; + return; } - }); - throw new DOMException("aborted", "AbortError"); - } - return { events: events() }; - }, + await new Promise((resolve) => { + if (options.signal.aborted) { + resolve(); + } else { + options.signal.addEventListener( + "abort", + () => resolve(), + { + once: true, + }, + ); + } + }); + throw new DOMException("aborted", "AbortError"); + } + return { events: events() }; + }, + }), }), - }), - }, - ); + }, + ); - // The fake Codex stream has no process handle to keep its unref'ed poll alive. - const keepEventLoopAlive = setTimeout(() => {}, 10_000); - try { - await expect( - client.run(repository, { + // The fake Codex stream has no process handle to keep its unref'ed poll alive. + const keepEventLoopAlive = setTimeout(() => {}, 10_000); + try { + await expect( + client.run(repository, { + maxCostUsd: 0.005, + postScanPrompt: "Record the scan cost.", + onCost: (cost) => costs.push(cost.estimatedUsd), + signal: AbortSignal.timeout(5_000), + }), + ).rejects.toMatchObject({ + name: ScanCostLimitExceededError.name, maxCostUsd: 0.005, - postScanPrompt: "Record the scan cost.", - onCost: (cost) => costs.push(cost.estimatedUsd), - signal: AbortSignal.timeout(5_000), - }), - ).rejects.toMatchObject({ - name: ScanCostLimitExceededError.name, - maxCostUsd: 0.005, - scanDir, - cost, - }); - } finally { - clearTimeout(keepEventLoopAlive); - } - expect(turns).toBe(1); - expect(costs.at(-1)).toBe(0.00625); - expect(commands[1]).toEqual([ - "get-scan-feedback", - "--scan-id", - "scan_example_001", - ]); - expect(commands[2]).toEqual([ - "set-scan-thread", - "--scan-id", - "scan_example_001", - "--thread-id", - "scan-thread", - ]); - expect(commands[3]).toEqual([ - "fail-scan", - "--scan-id", - "scan_example_001", - "--message", - `Scan stopped: estimated cost $0.00625 exceeded the $0.005 limit; partial output remains at ${scanDir}.`, - "--cost-json", - JSON.stringify(cost), - ]); - expect(commands.some((args) => args[0] === "complete-scan")).toBe(false); - await expect(stat(scanDir)).resolves.toBeDefined(); - await client.close(); - }); + scanDir, + cost, + }); + } finally { + clearTimeout(keepEventLoopAlive); + } + expect(turns).toBe(1); + expect(costs.at(-1)).toBe(0.00625); + expect(commands[1]).toEqual([ + "get-scan-feedback", + "--scan-id", + "scan_example_001", + ]); + expect(commands[2]).toEqual([ + "set-scan-thread", + "--scan-id", + "scan_example_001", + "--thread-id", + "scan-thread", + ]); + expect(commands[3]).toEqual([ + "fail-scan", + "--scan-id", + "scan_example_001", + "--message", + `Scan stopped: estimated cost $0.00625 exceeded the $0.005 limit; partial output remains at ${scanDir}.`, + "--cost-json", + JSON.stringify(cost), + ]); + expect(commands.some((args) => args[0] === "complete-scan")).toBe(false); + await expect(stat(scanDir)).resolves.toBeDefined(); + await client.close(); + }, + ); - test.each(["partial", "invalid", "unavailable"] as const)( - "recovers exhausted deep-scan budget when completion is %s", - async (completion) => { + test.each([ + ["partial", "live"], + ["invalid", "live"], + ["unavailable", "live"], + ["partial", "completed"], + ] as const)( + "recovers exhausted deep-scan budget when completion is %s using %s usage", + async (completion, costSource) => { const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3546,6 +3571,35 @@ describe("CodexSecurity orchestration", () => { turns += 1; async function* events(): AsyncGenerator { yield { type: "thread.started", thread_id: "scan-thread" }; + if (costSource === "completed") { + await Promise.all([ + writeUsageSession(codexHome, "scan-thread", { + input_tokens: 100, + output_tokens: 1, + }), + writeUsageSession( + codexHome, + "worker-thread", + { + input_tokens: 250, + cached_input_tokens: 100, + output_tokens: 10, + }, + "scan-thread", + ), + ]); + yield { + type: "turn.completed", + usage: { + input_tokens: 1_000, + cached_input_tokens: 100, + cache_write_input_tokens: 0, + output_tokens: 20, + reasoning_output_tokens: 0, + }, + }; + return; + } await writeUsageSession(codexHome, "scan-thread", { input_tokens: 1_250, cached_input_tokens: 200, diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 291900e8b..cd6c96f38 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -637,7 +637,9 @@ describe("live scan cost tracking", () => { test.each([ ["root metadata is missing", "missing", "independent", 1, true], ["root timing is missing", "untimed", "independent", 1, true], + ["root timing is invalid", "invalid", "independent", 1, true], ["worker timing is missing", "timed", "untimed", 1, true], + ["worker timing is invalid", "timed", "invalid", 1, true], ["the worker parent is unobserved", "missing", "orphaned", 1, true], ["the worker has a parent", "missing", "parented", 1, false], ["the worker is unrelated", "missing", "unrelated", 1, false], @@ -655,7 +657,11 @@ describe("live scan cost tracking", () => { { input_tokens: 100, output_tokens: 10 }, undefined, scanDirectory, - rootState === "timed" ? "2026-07-26T12:00:00Z" : undefined, + rootState === "timed" + ? "2026-07-26T12:00:00Z" + : rootState === "invalid" + ? "not a timestamp" + : undefined, ); } if (workerState !== "none") { @@ -678,7 +684,11 @@ describe("live scan cost tracking", () => { "worker", "output", ), - workerState === "untimed" ? undefined : "2026-07-26T12:01:00Z", + workerState === "untimed" + ? undefined + : workerState === "invalid" + ? "not a timestamp" + : "2026-07-26T12:01:00Z", true, ); } @@ -2226,6 +2236,40 @@ describe("live scan cost tracking", () => { expect(exceeded.signal.aborted).toBe(true); }); + test.each(["incomplete", "unfinished", "unreadable"] as const)( + "preserves a definitive overage when final worker evidence is %s", + async (evidence) => { + const reportedCosts: number[] = []; + const { worker, tracker } = await workerScan({ + workerCompleted: evidence !== "unfinished", + maxCostUsd: 0.001, + onCost: (cost) => reportedCosts.push(cost.estimatedUsd), + }); + expect((await tracker.refresh()).cost?.inputTokens).toBe(200); + if (evidence === "incomplete") { + await appendIncompleteTokenUsage(worker); + } else if (evidence === "unreadable") { + tracker.refresh = async () => { + throw new Error("session read failed"); + }; + } + + await expect( + tracker.stop({ input_tokens: 1_000, output_tokens: 100 }), + ).rejects.toThrow( + evidence === "unreadable" + ? "session read failed" + : "The scan cost limit could not be verified", + ); + expect(reportedCosts).toEqual([0.00064, 0.00352]); + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 1_100, + outputTokens: 110, + estimatedUsd: 0.00352, + }); + }, + ); + test("preserves higher observed root usage when completed-turn usage is stale", async () => { const { tracker } = await workerScan({ rootUsage: { input_tokens: 1_000, output_tokens: 100 }, @@ -2262,15 +2306,18 @@ describe("live scan cost tracking", () => { output_tokens: 10, }); const reportedCosts: number[] = []; + const reportedErrors: unknown[] = []; + const refreshError = new Error("session read failed"); const tracker = new ScanCostTracker({ codexHome: home, model: "gpt-5.6-terra", onCost: (cost) => reportedCosts.push(cost.estimatedUsd), + onError: (error) => reportedErrors.push(error), }); tracker.start("scan-thread"); expect((await tracker.refresh()).cost?.estimatedUsd).toBe(0.00032); tracker.refresh = async () => { - throw new Error("session read failed"); + throw refreshError; }; const usage = { input_tokens: 1_000, output_tokens: 100 }; @@ -2279,6 +2326,7 @@ describe("live scan cost tracking", () => { cost: { inputTokens: 1_000, estimatedUsd: 0.0032 }, }); expect(reportedCosts).toEqual([0.00032, 0.0032]); + expect(reportedErrors).toEqual([refreshError]); }); test("adds observed worker usage to the completed root after a failed refresh", async () => { From 830b6964dd33aaa897002abd8ae01fe4cbab515e Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:42:04 -0700 Subject: [PATCH 06/21] fix(api): retain definitive budget failure classification --- sdk/typescript/src/api.ts | 18 ++++- sdk/typescript/tests-ts/api.test.ts | 101 +++++++++++++++++++++------- 2 files changed, 90 insertions(+), 29 deletions(-) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index a826fafbc..b8e048165 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -1329,10 +1329,22 @@ export class CodexSecurity { // scan, and cleanup must treat all of those as a failure it is not allowed to mask. scanFailure = true; const snapshot = await costTracker?.stop().catch(() => null); + const knownCost = snapshot?.cost; const failure = - signal.reason instanceof ScanCostLimitExceededError - ? signal.reason - : error; + options.maxCostUsd !== undefined && + knownCost !== undefined && + knownCost !== null && + knownCost.estimatedUsd > options.maxCostUsd && + !this.#abortController.signal.aborted && + options.signal?.aborted !== true + ? new ScanCostLimitExceededError( + options.maxCostUsd, + knownCost, + scanDir, + ) + : signal.reason instanceof ScanCostLimitExceededError + ? signal.reason + : error; if ( failure instanceof ScanCostLimitExceededError && budgetRecovery !== null && diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 5b35d76bb..ee4c49150 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -20,7 +20,7 @@ import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { Codex, type CodexOptions, type ThreadEvent } from "@openai/codex-sdk"; -import { afterEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import { parse as parseToml } from "smol-toml"; import { AuthenticationRequiredError, @@ -44,7 +44,11 @@ import { OPENROUTER_CODEX_PROVIDER, type JsonObject, } from "../src/config.js"; -import { estimateScanCost, type ScanCost } from "../src/cost.js"; +import { + estimateScanCost, + type ScanCost, + ScanCostTracker, +} from "../src/cost.js"; import { resolveCodexCommand, runWorkbench, @@ -3344,9 +3348,14 @@ describe("CodexSecurity orchestration", () => { }, ); - test.each(["live", "completed"] as const)( - "stops and records a scan when its %s cost exceeds the limit", - async (costSource) => { + test.each([ + ["live polling", "live"], + ["turn completion", "completed"], + ["turn completion after a tracking failure", "tracking-failure"], + ["turn completion after user cancellation", "canceled"], + ] as const)( + "stops and records a scan with over-budget usage from %s", + async (_description, costSource) => { const root = await temporaryDirectory(); const repository = join(root, "repository"); const codexHome = join(root, "codex-home"); @@ -3356,6 +3365,8 @@ describe("CodexSecurity orchestration", () => { await mkdir(scanDir, { mode: 0o700 }); const commands: Array = []; const costs: number[] = []; + const cancellation = new AbortController(); + const userCanceled = costSource === "canceled"; let turns = 0; const cost = { model: "gpt-5.6-sol", @@ -3414,7 +3425,7 @@ describe("CodexSecurity orchestration", () => { "scan-thread", ), ]); - if (costSource === "completed") { + if (costSource !== "live") { await copyCompletedScan(root); yield { type: "turn.completed", @@ -3452,21 +3463,54 @@ describe("CodexSecurity orchestration", () => { // The fake Codex stream has no process handle to keep its unref'ed poll alive. const keepEventLoopAlive = setTimeout(() => {}, 10_000); + const originalRefresh = ScanCostTracker.prototype.refresh; + let firstRefresh = true; + let rejectPoll: ((error: unknown) => void) | undefined; + const refresh = + costSource === "tracking-failure" || userCanceled + ? spyOn(ScanCostTracker.prototype, "refresh").mockImplementation( + function (this: ScanCostTracker) { + if (firstRefresh) { + firstRefresh = false; + return new Promise< + Awaited> + >((_resolve, reject) => { + rejectPoll = reject; + }); + } + if (rejectPoll !== undefined) { + if (userCanceled) cancellation.abort(); + rejectPoll(new Error("session read failed")); + rejectPoll = undefined; + } + return originalRefresh.call(this); + }, + ) + : null; try { - await expect( - client.run(repository, { - maxCostUsd: 0.005, - postScanPrompt: "Record the scan cost.", - onCost: (cost) => costs.push(cost.estimatedUsd), - signal: AbortSignal.timeout(5_000), - }), - ).rejects.toMatchObject({ - name: ScanCostLimitExceededError.name, + const scan = client.run(repository, { maxCostUsd: 0.005, - scanDir, - cost, + postScanPrompt: "Record the scan cost.", + onCost: (cost) => costs.push(cost.estimatedUsd), + signal: AbortSignal.any([ + cancellation.signal, + AbortSignal.timeout(5_000), + ]), }); + if (userCanceled) { + const failure = await scan.catch((error: unknown) => error); + expect(failure).toBeInstanceOf(ScanInterruptedError); + expect(failure).not.toBeInstanceOf(ScanCostLimitExceededError); + } else { + await expect(scan).rejects.toMatchObject({ + name: ScanCostLimitExceededError.name, + maxCostUsd: 0.005, + scanDir, + cost, + }); + } } finally { + refresh?.mockRestore(); clearTimeout(keepEventLoopAlive); } expect(turns).toBe(1); @@ -3483,15 +3527,20 @@ describe("CodexSecurity orchestration", () => { "--thread-id", "scan-thread", ]); - expect(commands[3]).toEqual([ - "fail-scan", - "--scan-id", - "scan_example_001", - "--message", - `Scan stopped: estimated cost $0.00625 exceeded the $0.005 limit; partial output remains at ${scanDir}.`, - "--cost-json", - JSON.stringify(cost), - ]); + if (userCanceled) { + expect(commands[3]?.[0]).toBe("fail-scan"); + expect(commands[3]).toContain(JSON.stringify(cost)); + } else { + expect(commands[3]).toEqual([ + "fail-scan", + "--scan-id", + "scan_example_001", + "--message", + `Scan stopped: estimated cost $0.00625 exceeded the $0.005 limit; partial output remains at ${scanDir}.`, + "--cost-json", + JSON.stringify(cost), + ]); + } expect(commands.some((args) => args[0] === "complete-scan")).toBe(false); await expect(stat(scanDir)).resolves.toBeDefined(); await client.close(); From f04345eb2dec0c0433fd77902a86e073a59f71e5 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:45:07 -0700 Subject: [PATCH 07/21] fix(cost): retain overages discovered during cleanup --- sdk/typescript/src/cost.ts | 1 + sdk/typescript/tests-ts/api.test.ts | 48 ++++++++++++++++++++-------- sdk/typescript/tests-ts/cost.test.ts | 14 ++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index d28ee8a08..885d469bd 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -227,6 +227,7 @@ export class ScanCostTracker { this.#snapshot = snapshot; this.#finalSnapshot = snapshot; this.#reportCost(snapshot.cost); + if (!finalizing) return snapshot; } if (refreshFailure !== null) { if ( diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index ee4c49150..f397a38e7 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -3352,6 +3352,7 @@ describe("CodexSecurity orchestration", () => { ["live polling", "live"], ["turn completion", "completed"], ["turn completion after a tracking failure", "tracking-failure"], + ["failure cleanup after a tracking failure", "cleanup"], ["turn completion after user cancellation", "canceled"], ] as const)( "stops and records a scan with over-budget usage from %s", @@ -3368,6 +3369,29 @@ describe("CodexSecurity orchestration", () => { const cancellation = new AbortController(); const userCanceled = costSource === "canceled"; let turns = 0; + let rejectPoll: ((error: unknown) => void) | undefined; + const rootUsage: Record = + costSource === "cleanup" + ? { + input_tokens: 1_250, + cached_input_tokens: 200, + output_tokens: 30, + } + : costSource === "live" + ? { + input_tokens: 500, + cached_input_tokens: 100, + output_tokens: 10, + } + : { input_tokens: 100, output_tokens: 1 }; + const workerUsage: Record = + costSource === "cleanup" + ? {} + : { + input_tokens: costSource === "live" ? 750 : 250, + cached_input_tokens: 100, + output_tokens: costSource === "live" ? 20 : 10, + }; const cost = { model: "gpt-5.6-sol", inputTokens: 1_250, @@ -3409,23 +3433,20 @@ describe("CodexSecurity orchestration", () => { async function* events(): AsyncGenerator { yield { type: "thread.started", thread_id: "scan-thread" }; await Promise.all([ - writeUsageSession(codexHome, "scan-thread", { - input_tokens: costSource === "live" ? 500 : 100, - cached_input_tokens: costSource === "live" ? 100 : 0, - output_tokens: costSource === "live" ? 10 : 1, - }), + writeUsageSession(codexHome, "scan-thread", rootUsage), writeUsageSession( codexHome, "worker-thread", - { - input_tokens: costSource === "live" ? 750 : 250, - cached_input_tokens: 100, - output_tokens: costSource === "live" ? 20 : 10, - }, + workerUsage, "scan-thread", ), ]); - if (costSource !== "live") { + if (costSource === "cleanup") { + expect(rejectPoll).toBeDefined(); + rejectPoll?.(new Error("session read failed")); + rejectPoll = undefined; + } + if (costSource !== "live" && costSource !== "cleanup") { await copyCompletedScan(root); yield { type: "turn.completed", @@ -3465,9 +3486,10 @@ describe("CodexSecurity orchestration", () => { const keepEventLoopAlive = setTimeout(() => {}, 10_000); const originalRefresh = ScanCostTracker.prototype.refresh; let firstRefresh = true; - let rejectPoll: ((error: unknown) => void) | undefined; const refresh = - costSource === "tracking-failure" || userCanceled + costSource === "tracking-failure" || + costSource === "cleanup" || + userCanceled ? spyOn(ScanCostTracker.prototype, "refresh").mockImplementation( function (this: ScanCostTracker) { if (firstRefresh) { diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index cd6c96f38..f22682365 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -2270,6 +2270,20 @@ describe("live scan cost tracking", () => { }, ); + test("returns a definitive overage first discovered during failure cleanup", async () => { + const { worker, tracker } = await workerScan({ + rootUsage: { input_tokens: 1_000, output_tokens: 100 }, + maxCostUsd: 0.001, + }); + await appendIncompleteTokenUsage(worker); + + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 1_100, + outputTokens: 110, + estimatedUsd: 0.00352, + }); + }); + test("preserves higher observed root usage when completed-turn usage is stale", async () => { const { tracker } = await workerScan({ rootUsage: { input_tokens: 1_000, output_tokens: 100 }, From b9f37a9b17ed5f3c9122b25c6316ba01cc2ffde0 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 03:06:51 -0700 Subject: [PATCH 08/21] fix(cost): retain highest usage through budget cleanup --- sdk/typescript/src/api.ts | 48 +++++++++--- sdk/typescript/src/cost.ts | 72 +++++++++++------ sdk/typescript/tests-ts/api.test.ts | 113 ++++++++++++++++++++++++--- sdk/typescript/tests-ts/cost.test.ts | 44 +++++++++++ 4 files changed, 230 insertions(+), 47 deletions(-) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index b8e048165..474290d28 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -1328,23 +1328,47 @@ export class CodexSecurity { // Recorded first: everything below can throw a different error for this same failed // scan, and cleanup must treat all of those as a failure it is not allowed to mask. scanFailure = true; - const snapshot = await costTracker?.stop().catch(() => null); + const trackedSnapshot = await costTracker?.stop().catch(() => null); + const signaledOverage = + signal.reason instanceof ScanCostLimitExceededError + ? signal.reason + : null; + const trackedCost = trackedSnapshot?.cost; + const snapshot = + signaledOverage !== null && + (trackedCost === undefined || + trackedCost === null || + signaledOverage.cost.estimatedUsd > trackedCost.estimatedUsd) + ? { + cost: signaledOverage.cost, + usage: { + input_tokens: signaledOverage.cost.inputTokens, + cached_input_tokens: signaledOverage.cost.cachedInputTokens, + cache_write_input_tokens: + signaledOverage.cost.cacheWriteInputTokens, + output_tokens: signaledOverage.cost.outputTokens, + }, + } + : trackedSnapshot; const knownCost = snapshot?.cost; - const failure = + let failure: unknown = signaledOverage ?? error; + if ( options.maxCostUsd !== undefined && knownCost !== undefined && knownCost !== null && knownCost.estimatedUsd > options.maxCostUsd && - !this.#abortController.signal.aborted && - options.signal?.aborted !== true - ? new ScanCostLimitExceededError( - options.maxCostUsd, - knownCost, - scanDir, - ) - : signal.reason instanceof ScanCostLimitExceededError - ? signal.reason - : error; + (signaledOverage !== null || + (!this.#abortController.signal.aborted && + options.signal?.aborted !== true)) && + (signaledOverage === null || + knownCost.estimatedUsd > signaledOverage.cost.estimatedUsd) + ) { + failure = new ScanCostLimitExceededError( + options.maxCostUsd, + knownCost, + scanDir, + ); + } if ( failure instanceof ScanCostLimitExceededError && budgetRecovery !== null && diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 885d469bd..aac82bcaa 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -85,6 +85,7 @@ interface ScanCostSnapshot { interface ObservedSessionUsage { root: ScanTokenUsage | null; workers: ScanTokenUsage | null; + completedRoot: ScanTokenUsage | null; rootCompleted: boolean; unverified: boolean; unfinishedWorkers: boolean; @@ -111,7 +112,14 @@ export class ScanCostTracker { #pending: Promise = Promise.resolve(); #snapshot: ScanCostSnapshot = { usage: null, cost: null }; #finalSnapshot: ScanCostSnapshot | null = null; - #observedUsage: ObservedSessionUsage | null = null; + #observedUsage: ObservedSessionUsage = { + root: null, + workers: null, + completedRoot: null, + rootCompleted: false, + unverified: false, + unfinishedWorkers: false, + }; #lastCost: number | null = null; #rootOnlyReadError = false; #highestFilesCompleted = 0; @@ -178,7 +186,7 @@ export class ScanCostTracker { clearInterval(this.#timer); this.#timer = null; } - const completedRoot = tokenUsage(fallbackUsage); + const suppliedRoot = tokenUsage(fallbackUsage); let refreshFailure: { error: unknown } | null = null; try { await this.refresh(); @@ -186,25 +194,20 @@ export class ScanCostTracker { refreshFailure = { error }; } const observed = this.#observedUsage; - const observedRoot = observed?.root ?? null; - const completedRootCost = estimateScanCost( + const completedRoot = higherCostUsage( this.#options.model, - completedRoot, + observed.completedRoot, + suppliedRoot, ); - const observedRootCost = estimateScanCost( + observed.completedRoot = completedRoot; + const rootUsage = higherCostUsage( this.#options.model, - observedRoot, + observed.root, + completedRoot, ); - const rootUsage = - completedRoot === null || - (observedRootCost !== null && - completedRootCost !== null && - observedRootCost.estimatedUsd >= completedRootCost.estimatedUsd) - ? observedRoot - : completedRoot; let completedUsage: unknown = - rootUsage === completedRoot ? fallbackUsage : rootUsage; - const workerUsage = observed?.workers ?? null; + rootUsage === suppliedRoot ? fallbackUsage : rootUsage; + const workerUsage = observed.workers; if (workerUsage !== null) { completedUsage = rootUsage === null @@ -219,13 +222,12 @@ export class ScanCostTracker { this.#snapshot.cost.estimatedUsd >= cost.estimatedUsd)) ? this.#snapshot : { usage: completedUsage ?? null, cost }; + this.#snapshot = snapshot; if ( this.#options.maxCostUsd !== undefined && snapshot.cost !== null && snapshot.cost.estimatedUsd > this.#options.maxCostUsd ) { - this.#snapshot = snapshot; - this.#finalSnapshot = snapshot; this.#reportCost(snapshot.cost); if (!finalizing) return snapshot; } @@ -245,10 +247,10 @@ export class ScanCostTracker { this.#options.maxCostUsd !== undefined && (rootUsage === null || cost === null || - observed?.unverified || + observed.unverified || (finalizing && - ((completedRoot === null && !observed?.rootCompleted) || - observed?.unfinishedWorkers))) + ((completedRoot === null && !observed.rootCompleted) || + observed.unfinishedWorkers))) ) { throw ( refreshFailure?.error ?? @@ -257,8 +259,7 @@ export class ScanCostTracker { ) ); } - this.#snapshot = snapshot; - this.#finalSnapshot = snapshot; + if (finalizing) this.#finalSnapshot = snapshot; this.#reportCost(snapshot.cost); return snapshot; } @@ -391,6 +392,7 @@ export class ScanCostTracker { const observed: ObservedSessionUsage = { root: null, workers: null, + completedRoot: this.#observedUsage.completedRoot, rootCompleted: false, unverified: hasUnverifiedWorkerAttribution, unfinishedWorkers: false, @@ -424,8 +426,13 @@ export class ScanCostTracker { : addTokenUsage(observed.root, observed.workers); if (usage === null) return; const cost = estimateScanCost(this.#options.model, usage); - this.#snapshot = { usage, cost }; - this.#reportCost(cost); + if ( + this.#snapshot.cost === null || + (cost !== null && cost.estimatedUsd >= this.#snapshot.cost.estimatedUsd) + ) { + this.#snapshot = { usage, cost }; + } + this.#reportCost(this.#snapshot.cost); } #reportWorkerActivities(session: SessionUsage): void { @@ -970,6 +977,21 @@ function tokenUsage(value: unknown): ScanTokenUsage | null { }; } +function higherCostUsage( + model: string, + previous: ScanTokenUsage | null, + next: ScanTokenUsage | null, +): ScanTokenUsage | null { + if (next === null) return previous; + const previousCost = estimateScanCost(model, previous); + const nextCost = estimateScanCost(model, next); + return previousCost !== null && + nextCost !== null && + previousCost.estimatedUsd >= nextCost.estimatedUsd + ? previous + : next; +} + function addTokenUsage( previous: ScanTokenUsage | null, next: ScanTokenUsage, diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index f397a38e7..d42f1e285 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -68,6 +68,12 @@ const REPOSITORY_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); const EXAMPLE = join(PLUGIN_ROOT, "examples", "completed-scan"); const temporaryDirectories: string[] = []; const TEST_SNAPSHOT_DIGEST = `codex-security-snapshot/v1:sha256:${"a".repeat(64)}`; +const RESET_BUDGET_USAGE = { + input_tokens: 1_200, + cached_input_tokens: 200, + cache_write_input_tokens: 0, + output_tokens: 25, +}; const SHELL_ENVIRONMENT_PREFIX = process.platform === "win32" ? "$env:" : "$"; function shellEnvironmentReference(name: string, suffix = ""): string { @@ -234,11 +240,12 @@ async function writeUsageSession( threadId: string, usage: Record, parentThreadId?: string, -): Promise { +): Promise { const directory = join(codexHome, "sessions", "2026", "07", "26"); await mkdir(directory, { recursive: true }); + const path = join(directory, `rollout-${threadId}.jsonl`); await writeFile( - join(directory, `rollout-${threadId}.jsonl`), + path, [ JSON.stringify({ type: "session_meta", @@ -259,6 +266,7 @@ async function writeUsageSession( "", ].join("\n"), ); + return path; } async function* completedEvents(): AsyncGenerator { @@ -3350,6 +3358,7 @@ describe("CodexSecurity orchestration", () => { test.each([ ["live polling", "live"], + ["a smaller cleanup counter snapshot", "reset"], ["turn completion", "completed"], ["turn completion after a tracking failure", "tracking-failure"], ["failure cleanup after a tracking failure", "cleanup"], @@ -3368,6 +3377,7 @@ describe("CodexSecurity orchestration", () => { const costs: number[] = []; const cancellation = new AbortController(); const userCanceled = costSource === "canceled"; + const liveUsage = costSource === "live" || costSource === "reset"; let turns = 0; let rejectPoll: ((error: unknown) => void) | undefined; const rootUsage: Record = @@ -3377,7 +3387,7 @@ describe("CodexSecurity orchestration", () => { cached_input_tokens: 200, output_tokens: 30, } - : costSource === "live" + : liveUsage ? { input_tokens: 500, cached_input_tokens: 100, @@ -3388,9 +3398,9 @@ describe("CodexSecurity orchestration", () => { costSource === "cleanup" ? {} : { - input_tokens: costSource === "live" ? 750 : 250, + input_tokens: liveUsage ? 750 : 250, cached_input_tokens: 100, - output_tokens: costSource === "live" ? 20 : 10, + output_tokens: liveUsage ? 20 : 10, }; const cost = { model: "gpt-5.6-sol", @@ -3446,7 +3456,7 @@ describe("CodexSecurity orchestration", () => { rejectPoll?.(new Error("session read failed")); rejectPoll = undefined; } - if (costSource !== "live" && costSource !== "cleanup") { + if (!liveUsage && costSource !== "cleanup") { await copyCompletedScan(root); yield { type: "turn.completed", @@ -3485,7 +3495,25 @@ describe("CodexSecurity orchestration", () => { // The fake Codex stream has no process handle to keep its unref'ed poll alive. const keepEventLoopAlive = setTimeout(() => {}, 10_000); const originalRefresh = ScanCostTracker.prototype.refresh; + const originalStop = ScanCostTracker.prototype.stop; let firstRefresh = true; + const stop = + costSource === "reset" + ? spyOn(ScanCostTracker.prototype, "stop").mockImplementation( + async function ( + this: ScanCostTracker, + ...args: Parameters + ) { + const snapshot = await originalStop.apply(this, args); + return args.length === 0 + ? { + usage: RESET_BUDGET_USAGE, + cost: estimateScanCost("gpt-5.6-sol", RESET_BUDGET_USAGE), + } + : snapshot; + }, + ) + : null; const refresh = costSource === "tracking-failure" || costSource === "cleanup" || @@ -3532,6 +3560,7 @@ describe("CodexSecurity orchestration", () => { }); } } finally { + stop?.mockRestore(); refresh?.mockRestore(); clearTimeout(keepEventLoopAlive); } @@ -3574,6 +3603,8 @@ describe("CodexSecurity orchestration", () => { ["invalid", "live"], ["unavailable", "live"], ["partial", "completed"], + ["partial", "reset"], + ["partial", "flushed"], ] as const)( "recovers exhausted deep-scan budget when completion is %s using %s usage", async (completion, costSource) => { @@ -3588,7 +3619,9 @@ describe("CodexSecurity orchestration", () => { ]); const commands: Array = []; const warnings: string[] = []; + const expectedCostUsd = costSource === "flushed" ? 0.0078 : 0.00625; let turns = 0; + let workerSession: string | null = null; const client = new TestClient( {}, { @@ -3642,8 +3675,8 @@ describe("CodexSecurity orchestration", () => { turns += 1; async function* events(): AsyncGenerator { yield { type: "thread.started", thread_id: "scan-thread" }; - if (costSource === "completed") { - await Promise.all([ + if (costSource === "completed" || costSource === "flushed") { + const sessions = await Promise.all([ writeUsageSession(codexHome, "scan-thread", { input_tokens: 100, output_tokens: 1, @@ -3659,6 +3692,7 @@ describe("CodexSecurity orchestration", () => { "scan-thread", ), ]); + workerSession = sessions[1]!; yield { type: "turn.completed", usage: { @@ -3697,6 +3731,61 @@ describe("CodexSecurity orchestration", () => { }, ); const keepAlive = setTimeout(() => {}, 10_000); + const originalStop = ScanCostTracker.prototype.stop; + const stop = + costSource === "reset" || costSource === "flushed" + ? spyOn(ScanCostTracker.prototype, "stop").mockImplementation( + async function ( + this: ScanCostTracker, + ...args: Parameters + ) { + try { + const snapshot = await originalStop.apply(this, args); + return costSource === "reset" && args.length === 0 + ? { + usage: RESET_BUDGET_USAGE, + cost: estimateScanCost( + "gpt-5.6-sol", + RESET_BUDGET_USAGE, + ), + } + : snapshot; + } finally { + if ( + costSource === "flushed" && + args.length > 0 && + workerSession !== null + ) { + const path = workerSession; + workerSession = null; + await appendFile( + path, + [ + JSON.stringify({ + type: "event_msg", + payload: { + type: "token_count", + info: { + total_token_usage: { + input_tokens: 500, + cached_input_tokens: 100, + output_tokens: 20, + }, + }, + }, + }), + JSON.stringify({ + type: "event_msg", + payload: { type: "task_complete" }, + }), + "", + ].join("\n"), + ); + } + } + }, + ) + : null; try { const result = client.run(repository, { mode: "deep", @@ -3721,9 +3810,9 @@ describe("CodexSecurity orchestration", () => { expect(recovered.coverage.completeness).toBe(completion); expect(recovered.findings.findings).toHaveLength(1); expect(recovered.threadId).toBe("scan-thread"); - expect(recovered.cost?.estimatedUsd).toBe(0.00625); + expect(recovered.cost?.estimatedUsd).toBe(expectedCostUsd); expect(warnings).toEqual([ - `Scan stopped: estimated cost $0.00625 exceeded the $0.005 limit; partial output remains at ${scanDir}.`, + `Scan stopped: estimated cost $${expectedCostUsd} exceeded the $0.005 limit; partial output remains at ${scanDir}.`, ]); expect(commands.some((args) => args[0] === "fail-scan")).toBe(false); } @@ -3733,7 +3822,11 @@ describe("CodexSecurity orchestration", () => { ); expect(recovery?.includes("--cost-json")).toBe(true); expect(recovery?.includes("--message")).toBe(true); + expect( + JSON.parse(recovery![recovery!.indexOf("--cost-json") + 1]!), + ).toMatchObject({ estimatedUsd: expectedCostUsd }); } finally { + stop?.mockRestore(); clearTimeout(keepAlive); await client.close(); } diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index f22682365..cd297c025 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -1666,6 +1666,38 @@ describe("live scan cost tracking", () => { }); }); + test("retains the larger observed cost after a token-counter reset", async () => { + const home = await codexHome(); + const root = await writeSession(home, "scan-thread", { + input_tokens: 1_000, + output_tokens: 100, + }); + const costs: number[] = []; + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-terra", + maxCostUsd: 0.001, + onCost: (cost) => costs.push(cost.estimatedUsd), + }); + tracker.start("scan-thread"); + expect((await tracker.refresh()).cost?.estimatedUsd).toBe(0.0032); + await appendFile( + root, + `${JSON.stringify({ + type: "event_msg", + payload: { + type: "token_count", + info: { + total_token_usage: { input_tokens: 500, output_tokens: 50 }, + }, + }, + })}\n`, + ); + + expect((await tracker.stop()).cost?.estimatedUsd).toBe(0.0032); + expect(costs).toEqual([0.0032]); + }); + test("retains a partial event across incremental reads", async () => { const home = await codexHome(); const path = await writeSession(home, "scan-thread", { @@ -2245,6 +2277,7 @@ describe("live scan cost tracking", () => { maxCostUsd: 0.001, onCost: (cost) => reportedCosts.push(cost.estimatedUsd), }); + const refresh = tracker.refresh.bind(tracker); expect((await tracker.refresh()).cost?.inputTokens).toBe(200); if (evidence === "incomplete") { await appendIncompleteTokenUsage(worker); @@ -2267,6 +2300,17 @@ describe("live scan cost tracking", () => { outputTokens: 110, estimatedUsd: 0.00352, }); + tracker.refresh = refresh; + if (evidence !== "incomplete") { + await appendIncompleteTokenUsage(worker); + } + await appendFile(worker, `}\n${taskEvent("task_complete")}\n`); + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 11_000, + outputTokens: 1_100, + estimatedUsd: 0.0352, + }); + expect(reportedCosts.at(-1)).toBe(0.0352); }, ); From 9eb6500b29f0c5c7fe7e2b10377d82a737c13cd3 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:14:13 -0700 Subject: [PATCH 09/21] fix(cost): retain verifiable per-session accounting --- sdk/typescript/src/cost.ts | 86 ++++-- sdk/typescript/tests-ts/cost.test.ts | 394 ++++++++++++++++++++++++++- 2 files changed, 456 insertions(+), 24 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index aac82bcaa..b3b644a0d 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -52,7 +52,8 @@ interface SessionUsage { startedAt: number | null; inheritedUsage: ScanTokenUsage | null; replaying: boolean; - usage: ScanTokenUsage | null; + accounting: { usage: ScanTokenUsage; cost: ScanCost | null } | null; + accountingError: Error | null; taskCompleted: boolean; calls: Map; activities: ScanActivity[]; @@ -285,7 +286,8 @@ export class ScanCostTracker { startedAt: null, inheritedUsage: null, replaying: false, - usage: null, + accounting: null, + accountingError: null, taskCompleted: false, calls: new Map(), activities: [], @@ -304,6 +306,7 @@ export class ScanCostTracker { !(await readSessionUsage( path, session, + this.#options.model, this.#options.repository, this.#options.maxCostUsd !== undefined, )) @@ -378,15 +381,13 @@ export class ScanCostTracker { } } } + let readFailure: { session: SessionUsage; error: unknown } | null = null; for (const { session, error } of unreadable) { if (included.has(session.threadId!)) { - this.#rootOnlyReadError = - session.threadId === this.#threadId && - included.size === 1 && - !hasUnverifiedWorkerAttribution; - throw error; + readFailure ??= { session, error }; + } else if (isSessionAccessDenied(error)) { + quarantineSession(session, error); } - if (isSessionAccessDenied(error)) quarantineSession(session, error); } const observed: ObservedSessionUsage = { @@ -399,17 +400,24 @@ export class ScanCostTracker { }; for (const session of this.#sessions.values()) { if (session.threadId !== null && included.has(session.threadId)) { + if ( + this.#options.maxCostUsd !== undefined && + session.accountingError !== null + ) { + readFailure ??= { session, error: session.accountingError }; + } if (session.pendingLineBytes > 0) observed.unverified = true; + const usage = session.accounting?.usage ?? null; if (session.threadId === this.#threadId) { observed.rootCompleted = session.taskCompleted; - if (session.usage !== null) { - observed.root = addTokenUsage(observed.root, session.usage); + if (usage !== null) { + observed.root = addTokenUsage(observed.root, usage); } } else { - if (session.usage === null) { + if (usage === null) { observed.unverified = true; } else { - observed.workers = addTokenUsage(observed.workers, session.usage); + observed.workers = addTokenUsage(observed.workers, usage); } if (!session.taskCompleted) observed.unfinishedWorkers = true; this.#reportWorkerActivities(session); @@ -424,15 +432,23 @@ export class ScanCostTracker { : observed.workers === null ? observed.root : addTokenUsage(observed.root, observed.workers); - if (usage === null) return; - const cost = estimateScanCost(this.#options.model, usage); - if ( - this.#snapshot.cost === null || - (cost !== null && cost.estimatedUsd >= this.#snapshot.cost.estimatedUsd) - ) { - this.#snapshot = { usage, cost }; + if (usage !== null) { + const cost = estimateScanCost(this.#options.model, usage); + if ( + this.#snapshot.cost === null || + (cost !== null && cost.estimatedUsd >= this.#snapshot.cost.estimatedUsd) + ) { + this.#snapshot = { usage, cost }; + } + this.#reportCost(this.#snapshot.cost); + } + if (readFailure !== null) { + this.#rootOnlyReadError = + readFailure.session.threadId === this.#threadId && + included.size === 1 && + !hasUnverifiedWorkerAttribution; + throw readFailure.error; } - this.#reportCost(this.#snapshot.cost); } #reportWorkerActivities(session: SessionUsage): void { @@ -521,6 +537,7 @@ export async function* sessionFiles(directory: string): AsyncGenerator { async function readSessionUsage( path: string, session: SessionUsage, + model: string, repository?: string, requireReadableSessions = false, ): Promise { @@ -550,7 +567,12 @@ async function readSessionUsage( if (bytesRead === 0) return true; session.offset += bytesRead; try { - readSessionChunk(buffer.subarray(0, bytesRead), session, repository); + readSessionChunk( + buffer.subarray(0, bytesRead), + session, + model, + repository, + ); } catch (error) { quarantineSession(session, error); throw error; @@ -570,6 +592,7 @@ function quarantineSession(session: SessionUsage, error: unknown): void { function readSessionChunk( contents: Buffer, session: SessionUsage, + model: string, repository?: string, ): void { let lineStart = 0; @@ -588,12 +611,13 @@ function readSessionChunk( } if (session.pendingLineBytes === 0) { - readSessionEvent(fragment.toString("utf8"), session, repository); + readSessionEvent(fragment.toString("utf8"), session, model, repository); } else { if (fragment.length > 0) session.pendingLine.push(Buffer.from(fragment)); readSessionEvent( Buffer.concat(session.pendingLine, lineBytes).toString("utf8"), session, + model, repository, ); session.pendingLine = []; @@ -606,6 +630,7 @@ function readSessionChunk( function readSessionEvent( line: string, session: SessionUsage, + model: string, repository?: string, ): void { if (line.length === 0) return; @@ -613,6 +638,9 @@ function readSessionEvent( try { event = JSON.parse(line) as unknown; } catch { + session.accountingError ??= new Error( + "The scan cost limit could not be verified because a tracked session record could not be read.", + ); return; } if (!isRecord(event) || !isRecord(event["payload"])) return; @@ -813,7 +841,19 @@ function readSessionEvent( ? usage : subtractTokenUsage(usage, session.inheritedUsage); if (ownUsage !== null) { - session.usage = ownUsage; + const cost = estimateScanCost(model, ownUsage); + if (cost === null) { + session.accountingError ??= new Error( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + } + if ( + session.accounting?.cost == null || + (cost !== null && + cost.estimatedUsd >= session.accounting.cost.estimatedUsd) + ) { + session.accounting = { usage: ownUsage, cost }; + } session.taskCompleted = false; } } diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index cd297c025..14a65336c 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import * as fsPromises from "node:fs/promises"; import { appendFile, chmod, @@ -10,10 +11,11 @@ import { } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; import { estimateScanCost, ScanCostTracker } from "../src/cost.js"; import type { ScanActivity } from "../src/scan-activity.js"; import type { ScanProgress } from "../src/worker-progress.js"; +import { runMockInSubprocess } from "./support/isolated-mock.js"; const temporaryDirectories: string[] = []; const testPosix = @@ -109,6 +111,120 @@ function taskEvent( }); } +type MockAccountingEvent = Readonly> | Error; + +function accountingEvent( + usage: Readonly>, +): MockAccountingEvent { + return { + type: "event_msg", + payload: { type: "token_count", info: { total_token_usage: usage } }, + }; +} + +function accountingSession( + threadId: string, + events: readonly MockAccountingEvent[], + parentThreadId?: string, +): MockAccountingEvent[] { + return [ + { + type: "session_meta", + payload: { id: threadId, parent_thread_id: parentThreadId }, + }, + ...events, + { type: "event_msg", payload: { type: "task_complete" } }, + ]; +} + +async function withMockAccountingSessions( + sessions: Readonly>, + options: Omit[0], "codexHome">, + check: ( + tracker: ScanCostTracker, + append: (threadId: string, events: readonly MockAccountingEvent[]) => void, + ) => Promise, +): Promise { + const home = join(tmpdir(), "codex-security-mock-cost"); + const directory = join(home, "sessions"); + const files = new Map(); + const events = new Map(); + const append = ( + threadId: string, + next: readonly MockAccountingEvent[], + ): void => { + const path = join(directory, `rollout-${threadId}.jsonl`); + const lines = next.map((event) => { + // The reader sees valid markers; decoded events and errors stay in memory. + const marker = JSON.stringify({ mockSessionEvent: events.size }); + events.set(marker, event); + return `${marker}\n`; + }); + files.set( + path, + Buffer.concat([ + files.get(path) ?? Buffer.alloc(0), + Buffer.from(lines.join("")), + ]), + ); + }; + for (const [threadId, initial] of Object.entries(sessions)) { + append(threadId, initial); + } + const originalOpen = fsPromises.open; + const originalReaddir = fsPromises.readdir; + const originalParse = JSON.parse; + mock.module("node:fs/promises", () => ({ + ...fsPromises, + readdir: async (path: unknown) => { + if (String(path) !== directory) + throw new Error("Unexpected session directory"); + return [...files.keys()].map((path) => ({ + name: path.slice(directory.length + 1), + isDirectory: () => false, + isFile: () => true, + })); + }, + open: async (path: unknown) => { + const contents = files.get(String(path)); + if (contents === undefined) throw new Error("Unexpected session file"); + return { + read: async ( + buffer: Buffer, + offset: number, + length: number, + position: number, + ) => ({ + bytesRead: + position >= contents.length + ? 0 + : contents.copy(buffer, offset, position, position + length), + buffer, + }), + close: async () => {}, + }; + }, + })); + const parse = spyOn(JSON, "parse").mockImplementation((text, reviver) => { + const event = events.get(text); + if (event instanceof Error) throw event; + return event ?? originalParse(text, reviver); + }); + const tracker = new ScanCostTracker({ ...options, codexHome: home }); + tracker.start("scan-thread"); + try { + await check(tracker, append); + } finally { + await tracker.stop().catch(() => {}); + parse.mockRestore(); + mock.module("node:fs/promises", () => ({ + ...fsPromises, + open: originalOpen, + readdir: originalReaddir, + })); + } +} + async function workerScan({ rootUsage = { input_tokens: 100, output_tokens: 10 }, workerUsage = { input_tokens: 100, output_tokens: 10 }, @@ -1698,6 +1814,282 @@ describe("live scan cost tracking", () => { expect(costs).toEqual([0.0032]); }); + test("keeps mocked parse errors scoped to included budgeted sessions", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "keeps mocked parse errors scoped to included budgeted sessions", + ) + ) { + return; + } + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + const workerUsage = { input_tokens: 200, output_tokens: 20 }; + for (const [ + failedSession, + withWorker, + maxCostUsd, + authoritative, + rejects, + ] of [ + ["worker-thread", true, 1, true, true], + ["scan-thread", true, 1, true, true], + ["unrelated-thread", true, 1, true, false], + ["worker-thread", true, undefined, true, false], + ["scan-thread", false, undefined, false, false], + ["scan-thread", false, 1, true, false], + ["scan-thread", false, 1, false, true], + ] as const) { + const sessions: Record = { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + "unrelated-thread": accountingSession("unrelated-thread", [ + accountingEvent(workerUsage), + ]), + }; + if (withWorker) { + sessions["worker-thread"] = accountingSession( + "worker-thread", + [accountingEvent(workerUsage)], + "scan-thread", + ); + } + sessions[failedSession]!.unshift( + new SyntaxError("mock parser diagnostic"), + ); + const costs: number[] = []; + await withMockAccountingSessions( + sessions, + { + model: "gpt-5.6-terra", + maxCostUsd, + onCost: (cost) => costs.push(cost.estimatedUsd), + }, + async (tracker) => { + const refresh = tracker.refresh(); + if ( + maxCostUsd !== undefined && + failedSession !== "unrelated-thread" + ) { + await expect(refresh).rejects.toThrow( + "tracked session record could not be read", + ); + } else { + await expect(refresh).resolves.toMatchObject({ + cost: { inputTokens: withWorker ? 300 : 100 }, + }); + } + const completed = tracker.stop(authoritative ? rootUsage : undefined); + if (rejects) { + await expect(completed).rejects.toThrow( + "tracked session record could not be read", + ); + } else { + await expect(completed).resolves.toMatchObject({ + cost: { inputTokens: withWorker ? 300 : 100 }, + }); + } + expect(costs).toEqual([withWorker ? 0.00096 : 0.00032]); + }, + ); + } + }); + + test("retains mocked per-session priced high-water snapshots", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "retains mocked per-session priced high-water snapshots", + ) + ) { + return; + } + const inherited = { + input_tokens: 1_000, + cached_input_tokens: 500, + cache_write_input_tokens: 100, + output_tokens: 100, + reasoning_output_tokens: 20, + }; + const completed = { type: "event_msg", payload: { type: "task_complete" } }; + const costs: number[] = []; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent({ input_tokens: 100, output_tokens: 100 }), + accountingEvent({ + input_tokens: 1_000, + cached_input_tokens: 1_000, + output_tokens: 0, + }), + ]), + "worker-thread": [ + { + type: "session_meta", + payload: { + id: "worker-thread", + parent_thread_id: "scan-thread", + timestamp: "2026-07-26T12:02:00.000Z", + }, + }, + { type: "session_meta", payload: { id: "scan-thread" } }, + accountingEvent(inherited), + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, + }, + accountingEvent({ + input_tokens: 1_200, + cached_input_tokens: 500, + cache_write_input_tokens: 150, + output_tokens: 120, + reasoning_output_tokens: 25, + }), + accountingEvent({ + input_tokens: 1_500, + cached_input_tokens: 1_000, + cache_write_input_tokens: 100, + output_tokens: 101, + reasoning_output_tokens: 20, + }), + completed, + ], + }, + { + model: "gpt-5.6-terra", + maxCostUsd: 0.001, + onCost: (cost) => costs.push(cost.estimatedUsd), + }, + async (tracker, append) => { + const initialUsage = { + input_tokens: 300, + cached_input_tokens: 0, + cache_write_input_tokens: 50, + output_tokens: 120, + reasoning_output_tokens: 5, + total_tokens: 420, + }; + const initial = await tracker.refresh(); + expect(initial.usage).toEqual(initialUsage); + expect(initial.cost?.estimatedUsd).toBe(0.002065); + expect((await tracker.stop()).usage).toEqual(initialUsage); + + append("worker-thread", [ + accountingEvent({ + input_tokens: 1_300, + cached_input_tokens: 600, + cache_write_input_tokens: 150, + output_tokens: 140, + reasoning_output_tokens: 30, + }), + completed, + ]); + const final = await tracker.stop({ + input_tokens: 200, + output_tokens: 200, + }); + expect(final.usage).toEqual({ + input_tokens: 500, + cached_input_tokens: 100, + cache_write_input_tokens: 50, + output_tokens: 240, + reasoning_output_tokens: 10, + total_tokens: 740, + }); + expect(final.cost?.estimatedUsd).toBe(0.003725); + expect(await tracker.stop()).toEqual(final); + expect(costs).toEqual([0.002065, 0.002325, 0.003725]); + }, + ); + }); + + test("keeps mocked unverified evidence separate from known cost floors", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "keeps mocked unverified evidence separate from known cost floors", + ) + ) { + return; + } + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + const unpriceable = accountingEvent({ + input_tokens: Number.MAX_SAFE_INTEGER, + output_tokens: 0, + }); + for (const evidence of [ + new SyntaxError("mock parser diagnostic"), + unpriceable, + ]) { + const reports: Array = []; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + "worker-thread": accountingSession( + "worker-thread", + [ + accountingEvent({ input_tokens: 1_000, output_tokens: 100 }), + evidence, + accountingEvent(rootUsage), + ], + "scan-thread", + ), + }, + { + model: "gpt-5.6-terra", + maxCostUsd: 0.001, + onCost: (cost) => reports.push(cost.estimatedUsd), + onError: () => reports.push("error"), + }, + async (tracker) => { + await expect(tracker.stop(rootUsage)).rejects.toThrow( + "The scan cost limit could not be verified", + ); + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 1_100, + outputTokens: 110, + estimatedUsd: 0.00352, + }); + expect(reports[0]).toBe(0.00352); + expect(reports).toContain("error"); + }, + ); + } + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + unpriceable, + ]), + }, + { model: "gpt-5.6-terra", maxCostUsd: 1 }, + async (tracker) => { + expect( + (await tracker.stop({ input_tokens: 1_000, output_tokens: 100 })).cost + ?.estimatedUsd, + ).toBe(0.0032); + }, + ); + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + accountingEvent({ input_tokens: 200, output_tokens: 20 }), + ]), + }, + { model: "unknown-model" }, + async (tracker) => { + expect(await tracker.stop(undefined)).toMatchObject({ + usage: { input_tokens: 200, output_tokens: 20 }, + cost: null, + }); + }, + ); + }); + test("retains a partial event across incremental reads", async () => { const home = await codexHome(); const path = await writeSession(home, "scan-thread", { From 11a702ef9e6d8ae8da9a0287bd37202b2fbfe85c Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:46:43 -0700 Subject: [PATCH 10/21] fix(cost): respect proven worker ownership --- sdk/typescript/src/cost.ts | 75 ++++++++-- sdk/typescript/tests-ts/cost.test.ts | 216 ++++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 13 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index b3b644a0d..50eedcb53 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -1,5 +1,5 @@ import { open, readdir } from "node:fs/promises"; -import { join, relative, sep } from "node:path"; +import { isAbsolute, join, relative, sep } from "node:path"; import { scanActivityFromSessionEvent, type ScanActivity, @@ -266,7 +266,8 @@ export class ScanCostTracker { } async #readSessions(): Promise { - if (this.#threadId === null) return; + const rootThreadId = this.#threadId; + if (rootThreadId === null) return; this.#rootOnlyReadError = false; const presentSessions = new Set(); const unreadable: Array<{ session: SessionUsage; error: unknown }> = []; @@ -319,32 +320,78 @@ export class ScanCostTracker { } } - const included = new Set([this.#threadId]); + const parents = new Map(); + const conflictingParents = new Set(); + for (const session of this.#sessions.values()) { + if (session.threadId === null) continue; + const previous = parents.get(session.threadId); + if (previous !== undefined && previous !== session.parentThreadId) { + conflictingParents.add(session.threadId); + } else { + parents.set(session.threadId, session.parentThreadId); + } + } + const knownOwner = (threadId: string): string | null => { + const seen = new Set(); + while (threadId !== rootThreadId) { + if (seen.has(threadId) || conflictingParents.has(threadId)) return null; + seen.add(threadId); + const parent = parents.get(threadId); + if (parent === undefined) return null; + if (parent === null) return threadId; + threadId = parent; + } + return rootThreadId; + }; + + const included = new Set([rootThreadId]); const ambiguousWorkers = new Set(); if (this.#options.scanDirectory !== undefined) { const scanStartedAt = [...this.#sessions.values()].find( - (session) => session.threadId === this.#threadId, + (session) => session.threadId === rootThreadId, )?.startedAt ?? null; const artifactsDirectory = join(this.#options.scanDirectory, "artifacts"); + const workersDirectory = join( + artifactsDirectory, + "deep_discovery", + "workers", + ); for (const session of this.#sessions.values()) { - if (session.threadId === null || session.workingDirectory === null) { + if ( + session.threadId === null || + session.threadId === rootThreadId || + session.workingDirectory === null + ) { continue; } const workerDirectory = relative( - join(artifactsDirectory, "deep_discovery", "workers"), + workersDirectory, session.workingDirectory, - ).split(sep); + ); + const components = workerDirectory.split(sep); + const isWorkerDirectory = + !isAbsolute(workerDirectory) && + components.length === 2 && + components[0] !== ".." && + relative( + join(workersDirectory, components[0]!, "output"), + session.workingDirectory, + ) === ""; if ( - session.workingDirectory !== artifactsDirectory && - !(workerDirectory.length === 2 && workerDirectory[1] === "output") + relative(artifactsDirectory, session.workingDirectory) !== "" && + !isWorkerDirectory ) { continue; } + const owner = knownOwner(session.threadId); + if (owner === null) { + ambiguousWorkers.add(session.threadId); + continue; + } + if (owner !== session.threadId) continue; if (scanStartedAt === null || session.startedAt === null) { - if (session.threadId !== this.#threadId) { - ambiguousWorkers.add(session.threadId); - } + ambiguousWorkers.add(session.threadId); continue; } if (session.startedAt >= scanStartedAt) included.add(session.threadId); @@ -360,6 +407,10 @@ export class ScanCostTracker { included.has(session.parentThreadId) && !included.has(session.threadId) ) { + if (conflictingParents.has(session.threadId)) { + ambiguousWorkers.add(session.threadId); + continue; + } included.add(session.threadId); changed = true; } diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 14a65336c..5cc364e07 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -126,11 +126,12 @@ function accountingSession( threadId: string, events: readonly MockAccountingEvent[], parentThreadId?: string, + metadata: Readonly> = {}, ): MockAccountingEvent[] { return [ { type: "session_meta", - payload: { id: threadId, parent_thread_id: parentThreadId }, + payload: { ...metadata, id: threadId, parent_thread_id: parentThreadId }, }, ...events, { type: "event_msg", payload: { type: "task_complete" } }, @@ -832,6 +833,219 @@ describe("live scan cost tracking", () => { }, ); + test("resolves mocked worker ancestry before directory attribution", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "resolves mocked worker ancestry before directory attribution", + ) + ) { + return; + } + const scanDirectory = join(tmpdir(), "codex-security-mock-scan"); + const artifacts = join(scanDirectory, "artifacts"); + const workerDirectory = join( + artifacts, + "deep_discovery", + "workers", + "worker", + "output", + ); + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + const session = ( + id: string, + parent?: string, + cwd?: string, + timestamp?: string, + input = 1_000, + ) => + accountingSession( + id, + [accountingEvent({ input_tokens: input, output_tokens: input / 10 })], + parent, + { cwd, timestamp }, + ); + const roots = { + "scan-thread": session( + "scan-thread", + undefined, + scanDirectory, + "2026-07-26T12:00:00Z", + 100, + ), + "previous-root": session( + "previous-root", + undefined, + scanDirectory, + "2026-07-26T11:00:00Z", + ), + }; + const previousWorkers = { + "previous-coordinator": session( + "previous-coordinator", + "previous-root", + artifacts, + ), + "previous-worker": session( + "previous-worker", + "previous-coordinator", + workerDirectory, + "invalid timestamp", + ), + "resumed-previous-worker": session( + "resumed-previous-worker", + "previous-root", + workerDirectory, + "2026-07-26T12:02:00Z", + ), + "older-independent": session( + "older-independent", + undefined, + workerDirectory, + "2026-07-26T11:59:00Z", + ), + }; + await withMockAccountingSessions( + { + ...roots, + ...previousWorkers, + "current-child": session( + "current-child", + "current-independent", + workerDirectory, + undefined, + 300, + ), + "current-independent": session( + "current-independent", + undefined, + artifacts, + "2026-07-26T12:01:00Z", + 200, + ), + "unrelated-conflict-a": session("unrelated-conflict", "previous-root"), + "unrelated-conflict-b": session("unrelated-conflict", "missing-parent"), + }, + { model: "gpt-5.6-terra", maxCostUsd: 1, scanDirectory }, + async (tracker) => { + expect((await tracker.stop(rootUsage)).cost).toMatchObject({ + inputTokens: 600, + outputTokens: 60, + }); + }, + ); + await withMockAccountingSessions( + { + ...roots, + ...previousWorkers, + "scan-thread": [ + new SyntaxError("mock parser diagnostic"), + ...roots["scan-thread"], + ], + }, + { model: "gpt-5.6-terra", maxCostUsd: 1, scanDirectory }, + async (tracker) => { + expect((await tracker.stop(rootUsage)).cost?.inputTokens).toBe(100); + }, + ); + const unresolvedWorkers: Array> = [ + { + orphan: session( + "orphan", + "missing-parent", + workerDirectory, + "2026-07-26T12:01:00Z", + ), + }, + { orphan: session("orphan", "missing-parent", workerDirectory) }, + { + "cycle-a": session("cycle-a", "cycle-b", workerDirectory), + "cycle-b": session("cycle-b", "cycle-a"), + }, + { + "conflict-a": session("conflict", "scan-thread", workerDirectory), + "conflict-b": session("conflict", "previous-root"), + }, + { + "conflict-a": session("conflict", "scan-thread"), + "conflict-b": session("conflict", "previous-root"), + }, + ]; + for (const workers of unresolvedWorkers) { + await withMockAccountingSessions( + { ...roots, ...workers }, + { model: "gpt-5.6-terra", maxCostUsd: 1, scanDirectory }, + async (tracker) => { + await expect(tracker.stop(rootUsage)).rejects.toThrow( + "The scan cost limit could not be verified", + ); + }, + ); + } + }); + + test("limits mocked worker-directory attribution to contained output paths", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "limits mocked worker-directory attribution to contained output paths", + ) + ) { + return; + } + const scanDirectory = join(tmpdir(), "codex-security-mock-scan"); + const artifacts = join(scanDirectory, "artifacts"); + const workers = join(artifacts, "deep_discovery", "workers"); + const timestamp = "2026-07-26T12:01:00Z"; + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + for (const [cwd, startedAt, expectedInput] of [ + [artifacts, timestamp, 1_100], + [join(workers, "worker", "output"), timestamp, 1_100], + [join(workers, "worker", "output"), undefined, null], + [join(artifacts, "deep_discovery", "output"), undefined, 100], + [join(artifacts, "deep_discovery", "output"), timestamp, 100], + [ + join(artifacts, "deep_discovery", "workers-other", "worker", "output"), + undefined, + 100, + ], + ] as const) { + await withMockAccountingSessions( + { + "scan-thread": accountingSession( + "scan-thread", + [accountingEvent(rootUsage)], + undefined, + { + cwd: scanDirectory, + timestamp: "2026-07-26T12:00:00Z", + }, + ), + worker: accountingSession( + "worker", + [accountingEvent({ input_tokens: 1_000, output_tokens: 100 })], + undefined, + { + cwd, + timestamp: startedAt, + }, + ), + }, + { model: "gpt-5.6-terra", maxCostUsd: 1, scanDirectory }, + async (tracker) => { + const stopped = tracker.stop(rootUsage); + if (expectedInput === null) { + await expect(stopped).rejects.toThrow( + "The scan cost limit could not be verified", + ); + } else { + expect((await stopped).cost?.inputTokens).toBe(expectedInput); + } + }, + ); + } + }); + test("ignores replayed parent history in forked worker sessions", async () => { const home = await codexHome(); const inherited = { From b2fdfa4915cfb65b308e3dc130acaee3fb1c688a Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:14:09 -0700 Subject: [PATCH 11/21] fix(cost): exclude unrelated historical accounting evidence --- sdk/typescript/src/cost.ts | 17 ++++-- sdk/typescript/tests-ts/cost.test.ts | 88 +++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 50eedcb53..85af8c2a8 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -384,6 +384,13 @@ export class ScanCostTracker { ) { continue; } + if ( + scanStartedAt !== null && + session.startedAt !== null && + session.startedAt < scanStartedAt + ) { + continue; + } const owner = knownOwner(session.threadId); if (owner === null) { ambiguousWorkers.add(session.threadId); @@ -394,7 +401,7 @@ export class ScanCostTracker { ambiguousWorkers.add(session.threadId); continue; } - if (session.startedAt >= scanStartedAt) included.add(session.threadId); + included.add(session.threadId); } } let changed = true; @@ -689,9 +696,11 @@ function readSessionEvent( try { event = JSON.parse(line) as unknown; } catch { - session.accountingError ??= new Error( - "The scan cost limit could not be verified because a tracked session record could not be read.", - ); + if (!session.replaying) { + session.accountingError ??= new Error( + "The scan cost limit could not be verified because a tracked session record could not be read.", + ); + } return; } if (!isRecord(event) || !isRecord(event["payload"])) return; diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 5cc364e07..51263321e 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -904,6 +904,19 @@ describe("live scan cost tracking", () => { workerDirectory, "2026-07-26T11:59:00Z", ), + "older-orphan": session( + "older-orphan", + "missing-parent", + workerDirectory, + "2026-07-26T11:59:00Z", + ), + "older-cycle-a": session( + "older-cycle-a", + "older-cycle-b", + workerDirectory, + "2026-07-26T11:59:00Z", + ), + "older-cycle-b": session("older-cycle-b", "older-cycle-a"), }; await withMockAccountingSessions( { @@ -913,7 +926,7 @@ describe("live scan cost tracking", () => { "current-child", "current-independent", workerDirectory, - undefined, + "2026-07-26T11:59:00Z", 300, ), "current-independent": session( @@ -963,7 +976,12 @@ describe("live scan cost tracking", () => { "cycle-b": session("cycle-b", "cycle-a"), }, { - "conflict-a": session("conflict", "scan-thread", workerDirectory), + "conflict-a": session( + "conflict", + "scan-thread", + workerDirectory, + "2026-07-26T11:59:00Z", + ), "conflict-b": session("conflict", "previous-root"), }, { @@ -1046,6 +1064,72 @@ describe("live scan cost tracking", () => { } }); + test("keeps mocked replay errors outside owned accounting", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "keeps mocked replay errors outside owned accounting", + ) + ) { + return; + } + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + for (const location of ["replay", "owned-before", "owned-after"] as const) { + const costs: number[] = []; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + "worker-thread": [ + { + type: "session_meta", + payload: { + id: "worker-thread", + parent_thread_id: "scan-thread", + timestamp: "2026-07-26T12:02:00Z", + }, + }, + ...(location === "owned-before" + ? [new SyntaxError("mock owned parser diagnostic")] + : []), + { type: "session_meta", payload: { id: "scan-thread" } }, + new SyntaxError("mock replay parser diagnostic"), + accountingEvent({ input_tokens: 1_000, output_tokens: 100 }), + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, + }, + ...(location === "owned-after" + ? [new SyntaxError("mock owned parser diagnostic")] + : []), + accountingEvent({ input_tokens: 1_200, output_tokens: 120 }), + { type: "event_msg", payload: { type: "task_complete" } }, + ], + }, + { + model: "gpt-5.6-terra", + maxCostUsd: 1, + onCost: (cost) => costs.push(cost.estimatedUsd), + }, + async (tracker) => { + const stopped = tracker.stop(rootUsage); + if (location === "replay") { + await expect(stopped).resolves.toMatchObject({ + usage: { input_tokens: 300, output_tokens: 30 }, + cost: { estimatedUsd: 0.00096 }, + }); + } else { + await expect(stopped).rejects.toThrow( + "tracked session record could not be read", + ); + } + expect(costs).toEqual([0.00096]); + }, + ); + } + }); + test("ignores replayed parent history in forked worker sessions", async () => { const home = await codexHome(); const inherited = { From ff1827d3f942367196d797b1384cbe82862eb797 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:51:05 -0700 Subject: [PATCH 12/21] fix(cost): reverify late final accounting --- sdk/typescript/src/api.ts | 20 +++- sdk/typescript/tests-ts/api.test.ts | 174 ++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index 474290d28..b6ee49a84 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -1120,11 +1120,21 @@ export class CodexSecurity { } }, onFinalize: async (usage) => { - const snapshot = await tracker.stop(usage).catch((error: unknown) => { - if (options.maxCostUsd !== undefined) throw error; - reportTrackingError(error); - return { usage, cost: estimateScanCost(model, usage) }; - }); + const snapshot = await tracker + .stop(usage) + .catch(async (error: unknown) => { + if (options.maxCostUsd !== undefined) { + throwIfAborted(signal, scanDir); + try { + return await tracker.stop(usage); + } catch { + throwIfAborted(signal, scanDir); + throw error; + } + } + reportTrackingError(error); + return { usage, cost: estimateScanCost(model, usage) }; + }); throwIfAborted(signal, scanDir); if (options.maxCostUsd !== undefined && snapshot.cost === null) { notifyObserver( diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index d42f1e285..10dfc86e1 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2500,6 +2500,180 @@ describe("CodexSecurity orchestration", () => { }, ); + test.each([ + "complete", + "unverified", + "canceled-before", + "canceled-during", + "canceled-rejection", + "over-budget", + ] as const)( + "rechecks strict final accounting when the fresh result is %s", + async (state) => { + const root = await temporaryDirectory(); + const repository = join(root, "repository"); + const codexHome = join(root, "codex-home"); + const scanDir = join(root, "scan"); + await Promise.all([ + mkdir(repository), + mkdir(codexHome), + mkdir(scanDir, { mode: 0o700 }), + ]); + const model = "gpt-5.6-sol"; + const rootUsage = { + input_tokens: 100, + cached_input_tokens: 0, + cache_write_input_tokens: 0, + output_tokens: 10, + reasoning_output_tokens: 0, + }; + const finalUsage = { + ...rootUsage, + input_tokens: 200, + output_tokens: 20, + }; + const rootCost = estimateScanCost(model, rootUsage)!; + const finalCost = estimateScanCost(model, finalUsage)!; + const overBudget = state === "over-budget"; + const maxCostUsd = overBudget ? 0.001 : 1; + const firstError = new Error("Initial strict accounting check failed."); + const retryError = new Error("Fresh strict accounting check failed."); + const cancellation = new AbortController(); + const retryStarted = Promise.withResolvers(); + const releaseRetry = Promise.withResolvers(); + const commands: Array = []; + const finalizations: unknown[] = []; + let cleanupStops = 0; + let turns = 0; + const client = new TestClient( + { codexOverrides: { model } }, + { + environment: {}, + prepareRuntime: async () => preparedRuntime(codexHome), + resolvePluginPython: async () => "/managed/python", + prepareOutputDir: async () => scanDir, + repositoryRevision: async () => "deadbeef", + runWorkbench: async (_options: unknown, args: readonly string[]) => { + commands.push(args); + return mockWorkbench(args); + }, + createCodex: () => ({ + startThread: () => ({ + id: null, + async runStreamed() { + turns += 1; + await copyCompletedScan(root); + async function* events(): AsyncGenerator { + for await (const event of completedEvents()) { + yield event.type === "turn.completed" + ? { ...event, usage: rootUsage } + : event; + } + } + return { events: events() }; + }, + }), + }), + }, + ); + const start = spyOn( + ScanCostTracker.prototype, + "start", + ).mockImplementation(() => {}); + const originalStop = ScanCostTracker.prototype.stop; + const stop = spyOn(ScanCostTracker.prototype, "stop").mockImplementation( + async function ( + this: ScanCostTracker, + ...args: Parameters + ) { + if (args.length === 0) { + cleanupStops += 1; + return overBudget + ? { usage: rootUsage, cost: rootCost } + : { usage: finalUsage, cost: finalCost }; + } + finalizations.push(args[0]); + if (finalizations.length === 1) { + if (state === "canceled-before") cancellation.abort(); + throw firstError; + } + retryStarted.resolve(); + await releaseRetry.promise; + if (state === "unverified" || state === "canceled-rejection") { + throw retryError; + } + const snapshot = await originalStop.call(this, finalUsage); + if (overBudget) throw retryError; + return snapshot; + }, + ); + const outcome = client + .run(repository, { maxCostUsd, signal: cancellation.signal }) + .then( + (result) => ({ ok: true as const, result }), + (error: unknown) => ({ ok: false as const, error }), + ); + try { + if (state !== "canceled-before") { + await Promise.race([ + retryStarted.promise, + outcome.then(() => { + throw new Error("Scan ended before strict re-verification."); + }), + ]); + expect( + commands.some((args) => args[0] === "prepare-scan-completion"), + ).toBe(false); + if (state === "canceled-during" || state === "canceled-rejection") { + cancellation.abort(); + } + releaseRetry.resolve(); + } + const settled = await outcome; + if (state === "complete") { + expect(settled.ok).toBe(true); + if (!settled.ok) throw settled.error; + expect(settled.result.turnResult.usage).toMatchObject(finalUsage); + expect(settled.result.cost).toEqual(finalCost); + const completion = commands.find( + (args) => args[0] === "complete-scan", + ); + expect(completion).toContain(JSON.stringify(finalCost)); + expect(commands.some((args) => args[0] === "fail-scan")).toBe(false); + } else { + expect(settled.ok).toBe(false); + if (settled.ok) throw new Error("Unverified scan completed."); + if (state.startsWith("canceled-")) { + expect(settled.error).toBeInstanceOf(ScanInterruptedError); + } else if (overBudget) { + expect(settled.error).toMatchObject({ + name: ScanCostLimitExceededError.name, + maxCostUsd, + cost: finalCost, + }); + } else { + expect(settled.error).toBe(firstError); + } + const failure = commands.find((args) => args[0] === "fail-scan"); + expect(failure).toContain(JSON.stringify(finalCost)); + expect(commands.some((args) => args[0] === "complete-scan")).toBe( + false, + ); + } + expect(finalizations).toHaveLength(state === "canceled-before" ? 1 : 2); + for (const usage of finalizations) expect(usage).toBe(rootUsage); + expect(cleanupStops).toBe(state === "complete" ? 0 : 1); + expect(turns).toBe(1); + } finally { + releaseRetry.resolve(); + await outcome; + stop.mockRestore(); + start.mockRestore(); + await client.close(); + } + }, + ); + test("uses the actual scanner inventory instead of a stale workbench estimate", async () => { const root = await temporaryDirectory(); const repository = join(root, "repository"); From 59b259836494d230d37639ec7b9c3fd5a847eded Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 06:20:46 -0700 Subject: [PATCH 13/21] fix(cost): retain fresh usage from missing-session reads --- sdk/typescript/src/cost.ts | 25 ++++-- sdk/typescript/tests-ts/cost.test.ts | 114 +++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 85af8c2a8..074fc0c3e 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -426,6 +426,11 @@ export class ScanCostTracker { const hasUnverifiedWorkerAttribution = [...ambiguousWorkers].some( (threadId) => !included.has(threadId), ); + let readFailure: { + session: SessionUsage; + error: unknown; + rootOnlyRecoverable: boolean; + } | null = null; if (this.#options.maxCostUsd !== undefined) { for (const [path, session] of this.#sessions) { if ( @@ -433,16 +438,19 @@ export class ScanCostTracker { included.has(session.threadId) && !presentSessions.has(path) ) { - throw new Error( - "A tracked scan session disappeared before its cost could be verified.", - ); + readFailure ??= { + session, + error: new Error( + "A tracked scan session disappeared before its cost could be verified.", + ), + rootOnlyRecoverable: false, + }; } } } - let readFailure: { session: SessionUsage; error: unknown } | null = null; for (const { session, error } of unreadable) { if (included.has(session.threadId!)) { - readFailure ??= { session, error }; + readFailure ??= { session, error, rootOnlyRecoverable: true }; } else if (isSessionAccessDenied(error)) { quarantineSession(session, error); } @@ -462,7 +470,11 @@ export class ScanCostTracker { this.#options.maxCostUsd !== undefined && session.accountingError !== null ) { - readFailure ??= { session, error: session.accountingError }; + readFailure ??= { + session, + error: session.accountingError, + rootOnlyRecoverable: true, + }; } if (session.pendingLineBytes > 0) observed.unverified = true; const usage = session.accounting?.usage ?? null; @@ -502,6 +514,7 @@ export class ScanCostTracker { } if (readFailure !== null) { this.#rootOnlyReadError = + readFailure.rootOnlyRecoverable && readFailure.session.threadId === this.#threadId && included.size === 1 && !hasUnverifiedWorkerAttribution; diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 51263321e..642a7bca0 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -144,11 +144,13 @@ async function withMockAccountingSessions( check: ( tracker: ScanCostTracker, append: (threadId: string, events: readonly MockAccountingEvent[]) => void, + omit: (threadId: string) => void, ) => Promise, ): Promise { const home = join(tmpdir(), "codex-security-mock-cost"); const directory = join(home, "sessions"); const files = new Map(); + const omittedFiles = new Set(); const events = new Map(); const append = ( threadId: string, @@ -180,11 +182,13 @@ async function withMockAccountingSessions( readdir: async (path: unknown) => { if (String(path) !== directory) throw new Error("Unexpected session directory"); - return [...files.keys()].map((path) => ({ - name: path.slice(directory.length + 1), - isDirectory: () => false, - isFile: () => true, - })); + return [...files.keys()] + .filter((path) => !omittedFiles.has(path)) + .map((path) => ({ + name: path.slice(directory.length + 1), + isDirectory: () => false, + isFile: () => true, + })); }, open: async (path: unknown) => { const contents = files.get(String(path)); @@ -214,7 +218,9 @@ async function withMockAccountingSessions( const tracker = new ScanCostTracker({ ...options, codexHome: home }); tracker.start("scan-thread"); try { - await check(tracker, append); + await check(tracker, append, (threadId) => { + omittedFiles.add(join(directory, `rollout-${threadId}.jsonl`)); + }); } finally { await tracker.stop().catch(() => {}); parse.mockRestore(); @@ -2388,6 +2394,102 @@ describe("live scan cost tracking", () => { ); }); + test("reports mocked readable costs before rejecting missing sessions", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "reports mocked readable costs before rejecting missing sessions", + ) + ) { + return; + } + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + const freshUsage = { input_tokens: 1_000, output_tokens: 100 }; + const finalCost = { + inputTokens: 1_200, + outputTokens: 120, + estimatedUsd: 0.00384, + }; + const missingMessage = + "A tracked scan session disappeared before its cost could be verified."; + for (const [missingSession, maxCostUsd, rejects] of [ + ["scan-thread", 0.001, true], + ["retained-worker", 0.001, true], + ["unrelated-thread", 1, false], + ["retained-worker", undefined, false], + ] as const) { + const reports: Array = []; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + "retained-worker": accountingSession( + "retained-worker", + [accountingEvent(rootUsage)], + "scan-thread", + ), + "growing-worker": accountingSession( + "growing-worker", + [accountingEvent(rootUsage)], + "scan-thread", + ), + "unrelated-thread": accountingSession("unrelated-thread", [ + accountingEvent(freshUsage), + ]), + }, + { + model: "gpt-5.6-terra", + maxCostUsd, + onCost: (cost) => reports.push(cost.estimatedUsd), + }, + async (tracker, append, omit) => { + expect((await tracker.stop()).cost?.estimatedUsd).toBe(0.00096); + omit(missingSession); + append("growing-worker", [ + accountingEvent(freshUsage), + { type: "event_msg", payload: { type: "task_complete" } }, + ]); + const refreshed = tracker.refresh(); + if (rejects) { + await expect( + refreshed.catch((error: unknown) => { + reports.push("rejected"); + throw error; + }), + ).rejects.toThrow(missingMessage); + expect(reports).toEqual([0.00096, 0.00384, "rejected"]); + const cleanup = await tracker.stop(); + expect(cleanup.cost).toMatchObject(finalCost); + await expect(tracker.stop(rootUsage)).rejects.toThrow( + missingMessage, + ); + expect(await tracker.stop()).toEqual(cleanup); + } else { + await expect(refreshed).resolves.toMatchObject({ cost: finalCost }); + await expect(tracker.stop(rootUsage)).resolves.toMatchObject({ + cost: finalCost, + }); + expect(reports).toEqual([0.00096, 0.00384]); + } + }, + ); + } + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + }, + { model: "gpt-5.6-terra", maxCostUsd: 1 }, + async (tracker, _append, omit) => { + expect((await tracker.stop()).cost?.estimatedUsd).toBe(0.00032); + omit("scan-thread"); + await expect(tracker.stop(rootUsage)).rejects.toThrow(missingMessage); + }, + ); + }); + test("retains a partial event across incremental reads", async () => { const home = await codexHome(); const path = await writeSession(home, "scan-thread", { From 4c6c80729c6d7e578b33497bf470ec4bbc0e78b7 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 06:56:47 -0700 Subject: [PATCH 14/21] fix(cost): preserve unpriced completed usage --- sdk/typescript/src/cost.ts | 5 +- sdk/typescript/tests-ts/cost.test.ts | 124 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 074fc0c3e..b0d37b5b1 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -218,9 +218,10 @@ export class ScanCostTracker { const cost = estimateScanCost(this.#options.model, completedUsage); const snapshot = this.#snapshot.usage !== null && - (cost === null || + ((rootUsage === null && workerUsage === null) || (this.#snapshot.cost !== null && - this.#snapshot.cost.estimatedUsd >= cost.estimatedUsd)) + (cost === null || + this.#snapshot.cost.estimatedUsd >= cost.estimatedUsd))) ? this.#snapshot : { usage: completedUsage ?? null, cost }; this.#snapshot = snapshot; diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 642a7bca0..8e790e72f 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -2490,6 +2490,130 @@ describe("live scan cost tracking", () => { ); }); + test("prefers mocked completed usage when prices are unavailable", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "prefers mocked completed usage when prices are unavailable", + ) + ) { + return; + } + const observedRoot = { input_tokens: 100, output_tokens: 10 }; + const workerUsage = { input_tokens: 200, output_tokens: 20 }; + const completedRoot = { input_tokens: 1_000, output_tokens: 100 }; + for (const [withWorker, refreshFails] of [ + [false, true], + [true, true], + [true, false], + ] as const) { + const sessions: Record = { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(observedRoot), + ]), + }; + if (withWorker) { + sessions["worker-thread"] = accountingSession( + "worker-thread", + [accountingEvent(workerUsage)], + "scan-thread", + ); + } + const reportedErrors: unknown[] = []; + const reportedCosts: number[] = []; + const refreshError = new Error("Mock final accounting refresh failed."); + await withMockAccountingSessions( + sessions, + { + model: "unknown-model", + onCost: (cost) => reportedCosts.push(cost.estimatedUsd), + onError: (error) => reportedErrors.push(error), + }, + async (tracker) => { + expect((await tracker.stop()).usage).toMatchObject({ + input_tokens: withWorker ? 300 : 100, + }); + const refresh = refreshFails + ? spyOn(tracker, "refresh").mockRejectedValue(refreshError) + : null; + try { + const final = await tracker.stop(completedRoot); + expect(final).toMatchObject({ + usage: { + input_tokens: withWorker ? 1_200 : 1_000, + output_tokens: withWorker ? 120 : 100, + }, + cost: null, + }); + if (!withWorker) expect(final.usage).toBe(completedRoot); + expect(await tracker.stop()).toBe(final); + expect(reportedErrors).toEqual(refreshFails ? [refreshError] : []); + expect(reportedCosts).toEqual([]); + } finally { + refresh?.mockRestore(); + } + }, + ); + } + const unpriceable = { + input_tokens: Number.MAX_SAFE_INTEGER, + output_tokens: 0, + }; + for (const [replacement, maxCostUsd, rejects] of [ + [observedRoot, undefined, false], + [unpriceable, undefined, false], + [unpriceable, 1, true], + ] as const) { + const costs: number[] = []; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(completedRoot), + ]), + }, + { + model: "gpt-5.6-terra", + maxCostUsd, + onCost: (cost) => costs.push(cost.estimatedUsd), + }, + async (tracker) => { + const prior = await tracker.stop(); + expect(prior.cost?.estimatedUsd).toBe(0.0032); + const completed = tracker.stop(replacement); + if (rejects) { + await expect(completed).rejects.toThrow( + "The scan cost limit could not be verified", + ); + } else { + await expect(completed).resolves.toEqual(prior); + } + expect(costs).toEqual([0.0032]); + }, + ); + } + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", []), + "worker-thread": accountingSession( + "worker-thread", + [accountingEvent(workerUsage)], + "scan-thread", + ), + }, + { model: "unknown-model" }, + async (tracker, append) => { + const prior = await tracker.stop(); + expect(prior).toMatchObject({ usage: workerUsage, cost: null }); + append( + "conflicting-worker", + accountingSession("worker-thread", [], "another-thread"), + ); + await tracker.refresh(); + expect(await tracker.stop({})).toBe(prior); + }, + ); + }); + test("retains a partial event across incremental reads", async () => { const home = await codexHome(); const path = await writeSession(home, "scan-thread", { From b6f45b6ee5cc271c60ed513d010e92c43433fe1f Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 07:29:08 -0700 Subject: [PATCH 15/21] fix(cost): preserve finalization and usage boundaries --- sdk/typescript/src/api.ts | 1 + sdk/typescript/src/cost.ts | 6 ++++ sdk/typescript/tests-ts/api.test.ts | 16 +++++++--- sdk/typescript/tests-ts/cost.test.ts | 45 +++++++++++++++++++++------- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/sdk/typescript/src/api.ts b/sdk/typescript/src/api.ts index b6ee49a84..fc37ee8f6 100644 --- a/sdk/typescript/src/api.ts +++ b/sdk/typescript/src/api.ts @@ -1128,6 +1128,7 @@ export class CodexSecurity { try { return await tracker.stop(usage); } catch { + runPostScan = null; throwIfAborted(signal, scanDir); throw error; } diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index b0d37b5b1..cee96787b 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -1099,6 +1099,12 @@ function higherCostUsage( if (next === null) return previous; const previousCost = estimateScanCost(model, previous); const nextCost = estimateScanCost(model, next); + if (previous !== null && previousCost === null && nextCost === null) { + const previousTotal = + BigInt(previous.input_tokens) + BigInt(previous.output_tokens); + const nextTotal = BigInt(next.input_tokens) + BigInt(next.output_tokens); + return previousTotal > nextTotal ? previous : next; + } return previousCost !== null && nextCost !== null && previousCost.estimatedUsd >= nextCost.estimatedUsd diff --git a/sdk/typescript/tests-ts/api.test.ts b/sdk/typescript/tests-ts/api.test.ts index 10dfc86e1..8d759f597 100644 --- a/sdk/typescript/tests-ts/api.test.ts +++ b/sdk/typescript/tests-ts/api.test.ts @@ -2502,6 +2502,7 @@ describe("CodexSecurity orchestration", () => { test.each([ "complete", + "complete-followup", "unverified", "canceled-before", "canceled-during", @@ -2534,6 +2535,7 @@ describe("CodexSecurity orchestration", () => { }; const rootCost = estimateScanCost(model, rootUsage)!; const finalCost = estimateScanCost(model, finalUsage)!; + const completes = state === "complete" || state === "complete-followup"; const overBudget = state === "over-budget"; const maxCostUsd = overBudget ? 0.001 : 1; const firstError = new Error("Initial strict accounting check failed."); @@ -2608,7 +2610,13 @@ describe("CodexSecurity orchestration", () => { }, ); const outcome = client - .run(repository, { maxCostUsd, signal: cancellation.signal }) + .run(repository, { + maxCostUsd, + signal: cancellation.signal, + ...(state === "unverified" || state === "complete-followup" + ? { postScanPrompt: "Record the completed scan accounting." } + : {}), + }) .then( (result) => ({ ok: true as const, result }), (error: unknown) => ({ ok: false as const, error }), @@ -2630,7 +2638,7 @@ describe("CodexSecurity orchestration", () => { releaseRetry.resolve(); } const settled = await outcome; - if (state === "complete") { + if (completes) { expect(settled.ok).toBe(true); if (!settled.ok) throw settled.error; expect(settled.result.turnResult.usage).toMatchObject(finalUsage); @@ -2662,8 +2670,8 @@ describe("CodexSecurity orchestration", () => { } expect(finalizations).toHaveLength(state === "canceled-before" ? 1 : 2); for (const usage of finalizations) expect(usage).toBe(rootUsage); - expect(cleanupStops).toBe(state === "complete" ? 0 : 1); - expect(turns).toBe(1); + expect(cleanupStops).toBe(completes ? 0 : 1); + expect(turns).toBe(state === "complete-followup" ? 2 : 1); } finally { releaseRetry.resolve(); await outcome; diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 8e790e72f..3a885c5eb 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -2502,14 +2502,28 @@ describe("live scan cost tracking", () => { const observedRoot = { input_tokens: 100, output_tokens: 10 }; const workerUsage = { input_tokens: 200, output_tokens: 20 }; const completedRoot = { input_tokens: 1_000, output_tokens: 100 }; - for (const [withWorker, refreshFails] of [ - [false, true], - [true, true], - [true, false], + const equalTotalRoot = { input_tokens: 900, output_tokens: 200 }; + const boundaryObserved = { + input_tokens: Number.MAX_SAFE_INTEGER, + output_tokens: 2, + }; + const boundaryCompleted = { ...boundaryObserved, output_tokens: 1 }; + expect(boundaryObserved.input_tokens + boundaryObserved.output_tokens).toBe( + boundaryCompleted.input_tokens + boundaryCompleted.output_tokens, + ); + for (const [observed, completed, expected, withWorker, refreshFails] of [ + [observedRoot, completedRoot, completedRoot, false, true], + [observedRoot, completedRoot, completedRoot, true, true], + [observedRoot, completedRoot, completedRoot, true, false], + [completedRoot, observedRoot, completedRoot, false, true], + [completedRoot, observedRoot, completedRoot, true, true], + [completedRoot, equalTotalRoot, equalTotalRoot, false, true], + [completedRoot, equalTotalRoot, equalTotalRoot, true, true], + [boundaryObserved, boundaryCompleted, boundaryObserved, false, true], ] as const) { const sessions: Record = { "scan-thread": accountingSession("scan-thread", [ - accountingEvent(observedRoot), + accountingEvent(observed), ]), }; if (withWorker) { @@ -2531,21 +2545,32 @@ describe("live scan cost tracking", () => { }, async (tracker) => { expect((await tracker.stop()).usage).toMatchObject({ - input_tokens: withWorker ? 300 : 100, + input_tokens: + observed.input_tokens + + (withWorker ? workerUsage.input_tokens : 0), + output_tokens: + observed.output_tokens + + (withWorker ? workerUsage.output_tokens : 0), }); const refresh = refreshFails ? spyOn(tracker, "refresh").mockRejectedValue(refreshError) : null; try { - const final = await tracker.stop(completedRoot); + const final = await tracker.stop(completed); expect(final).toMatchObject({ usage: { - input_tokens: withWorker ? 1_200 : 1_000, - output_tokens: withWorker ? 120 : 100, + input_tokens: + expected.input_tokens + + (withWorker ? workerUsage.input_tokens : 0), + output_tokens: + expected.output_tokens + + (withWorker ? workerUsage.output_tokens : 0), }, cost: null, }); - if (!withWorker) expect(final.usage).toBe(completedRoot); + if (!withWorker && expected === completed) { + expect(final.usage).toBe(completed); + } expect(await tracker.stop()).toBe(final); expect(reportedErrors).toEqual(refreshFails ? [refreshError] : []); expect(reportedCosts).toEqual([]); From 392a1952c29fb22b46066ca256c4a24435c2fe09 Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 08:56:10 -0700 Subject: [PATCH 16/21] fix(cost): reject unusable owned accounting --- sdk/typescript/src/cost.ts | 21 ++--- sdk/typescript/tests-ts/cost.test.ts | 122 ++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 11 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index cee96787b..a91b01435 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -909,18 +909,19 @@ function readSessionEvent( return; } const usage = tokenUsage(payload["info"]["total_token_usage"]); - if (usage === null) return; const ownUsage = - session.inheritedUsage === null - ? usage - : subtractTokenUsage(usage, session.inheritedUsage); + usage === null + ? null + : session.inheritedUsage === null + ? usage + : subtractTokenUsage(usage, session.inheritedUsage); + const cost = ownUsage === null ? null : estimateScanCost(model, ownUsage); + if (cost === null) { + session.accountingError ??= new Error( + "The scan cost limit could not be verified because model pricing or token usage is unavailable.", + ); + } if (ownUsage !== null) { - const cost = estimateScanCost(model, ownUsage); - if (cost === null) { - session.accountingError ??= new Error( - "The scan cost limit could not be verified because model pricing or token usage is unavailable.", - ); - } if ( session.accounting?.cost == null || (cost !== null && diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 3a885c5eb..37dc5cb90 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -114,7 +114,7 @@ function taskEvent( type MockAccountingEvent = Readonly> | Error; function accountingEvent( - usage: Readonly>, + usage: Readonly> | null, ): MockAccountingEvent { return { type: "event_msg", @@ -2394,6 +2394,126 @@ describe("live scan cost tracking", () => { ); }); + test("keeps mocked unusable own usage scoped to accounting", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "keeps mocked unusable own usage scoped to accounting", + ) + ) { + return; + } + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + const inherited = { input_tokens: 1_000, output_tokens: 100 }; + const high = accountingEvent({ input_tokens: 2_000, output_tokens: 200 }); + const low = accountingEvent({ input_tokens: 1_100, output_tokens: 110 }); + const metadata = { timestamp: "2026-07-26T12:02:00Z" }; + const history = [ + { type: "session_meta", payload: { id: "inherited-thread" } }, + accountingEvent(inherited), + ]; + const started = { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, + }; + const infoOnly = { + type: "event_msg", + payload: { type: "token_count", info: null }, + }; + for (const unavailable of [ + accountingEvent(null), + accountingEvent({ input_tokens: 900, output_tokens: 90 }), + ]) { + for (const [scope, maxCostUsd, expectedInput] of [ + ["worker", 0.001, null], + ["first-error", 0.001, null], + ["worker", undefined, 1_100], + ["unrelated", 1, 100], + ["root", 1, 1_200], + ["replay", 1, 1_100], + ] as const) { + const events = + scope === "replay" + ? [ + ...history, + unavailable, + accountingEvent(inherited), + started, + high, + infoOnly, + low, + ] + : [ + ...history, + started, + high, + ...(scope === "first-error" + ? [new SyntaxError("mock existing accounting diagnostic")] + : []), + unavailable, + low, + ]; + const sessions: Readonly< + Record + > = + scope === "root" + ? { + "scan-thread": accountingSession( + "scan-thread", + events, + undefined, + metadata, + ), + } + : { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + "worker-thread": accountingSession( + "worker-thread", + events, + scope === "unrelated" ? undefined : "scan-thread", + metadata, + ), + }; + const costs: number[] = []; + await withMockAccountingSessions( + sessions, + { + model: "gpt-5.6-terra", + maxCostUsd, + onCost: (cost) => costs.push(cost.estimatedUsd), + }, + async (tracker) => { + const completed = tracker.stop( + scope === "root" + ? { input_tokens: 1_200, output_tokens: 120 } + : rootUsage, + ); + if (expectedInput === null) { + await expect(completed).rejects.toThrow( + scope === "first-error" + ? "tracked session record could not be read" + : "model pricing or token usage is unavailable", + ); + expect((await tracker.stop()).cost).toMatchObject({ + inputTokens: 1_100, + outputTokens: 110, + estimatedUsd: 0.00352, + }); + expect(costs[0]).toBe(0.00352); + } else { + expect((await completed).cost).toMatchObject({ + inputTokens: expectedInput, + outputTokens: expectedInput / 10, + }); + } + }, + ); + } + } + }); + test("reports mocked readable costs before rejecting missing sessions", async () => { if ( runMockInSubprocess( From 4e5e34cfacebf268c1c79047579b2c492a94ed1b Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:26:12 -0700 Subject: [PATCH 17/21] fix(cost): accumulate reset-aware session usage --- sdk/typescript/src/cost.ts | 86 ++++- sdk/typescript/tests-ts/cost.test.ts | 542 +++++++++++++++++++++------ 2 files changed, 509 insertions(+), 119 deletions(-) diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index a91b01435..91eb053f8 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -51,6 +51,8 @@ interface SessionUsage { workingDirectory: string | null; startedAt: number | null; inheritedUsage: ScanTokenUsage | null; + previousRawUsage: ScanTokenUsage | null; + accumulatedOwnUsage: ScanTokenUsage | null; replaying: boolean; accounting: { usage: ScanTokenUsage; cost: ScanCost | null } | null; accountingError: Error | null; @@ -287,6 +289,8 @@ export class ScanCostTracker { workingDirectory: null, startedAt: null, inheritedUsage: null, + previousRawUsage: null, + accumulatedOwnUsage: null, replaying: false, accounting: null, accountingError: null, @@ -750,7 +754,10 @@ function readSessionEvent( if (event["type"] !== "event_msg") return; if (payload["type"] === "token_count" && isRecord(payload["info"])) { const usage = tokenUsage(payload["info"]["total_token_usage"]); - if (usage !== null) session.inheritedUsage = usage; + if (usage !== null) { + session.inheritedUsage = usage; + session.previousRawUsage = usage; + } } if ( payload["type"] === "task_started" && @@ -909,6 +916,14 @@ function readSessionEvent( return; } const usage = tokenUsage(payload["info"]["total_token_usage"]); + const accumulated = + usage === null + ? null + : accumulateTokenUsage( + session.previousRawUsage, + session.accumulatedOwnUsage, + usage, + ); const ownUsage = usage === null ? null @@ -916,21 +931,41 @@ function readSessionEvent( ? usage : subtractTokenUsage(usage, session.inheritedUsage); const cost = ownUsage === null ? null : estimateScanCost(model, ownUsage); - if (cost === null) { + const accumulatedUsage = accumulated ?? session.accumulatedOwnUsage; + const accumulatedCost = estimateScanCost(model, accumulatedUsage); + if ( + accumulated === null || + accumulatedCost === null || + (ownUsage !== null && cost === null) + ) { session.accountingError ??= new Error( "The scan cost limit could not be verified because model pricing or token usage is unavailable.", ); } - if (ownUsage !== null) { + if (usage === null) return; + session.previousRawUsage = usage; + if (accumulated !== null) session.accumulatedOwnUsage = accumulated; + for (const candidate of [ + ownUsage === null ? null : { usage: ownUsage, cost }, + accumulatedUsage === null + ? null + : { usage: accumulatedUsage, cost: accumulatedCost }, + ]) { + if (candidate === null) continue; + const previous = session.accounting; if ( - session.accounting?.cost == null || - (cost !== null && - cost.estimatedUsd >= session.accounting.cost.estimatedUsd) + previous === null || + (candidate.cost !== null + ? previous.cost === null || + candidate.cost.estimatedUsd >= previous.cost.estimatedUsd + : previous.cost === null && + higherCostUsage(model, previous.usage, candidate.usage) === + candidate.usage) ) { - session.accounting = { usage: ownUsage, cost }; + session.accounting = candidate; } - session.taskCompleted = false; } + session.taskCompleted = false; } function readSessionReasoning( @@ -1113,6 +1148,41 @@ function higherCostUsage( : next; } +function accumulateTokenUsage( + previousRaw: ScanTokenUsage | null, + accumulated: ScanTokenUsage | null, + next: ScanTokenUsage, +): ScanTokenUsage | null { + const previousTotal = + BigInt(previousRaw?.input_tokens ?? 0) + + BigInt(previousRaw?.output_tokens ?? 0); + const nextTotal = BigInt(next.input_tokens) + BigInt(next.output_tokens); + const totalDelta = + nextTotal >= previousTotal ? nextTotal - previousTotal : nextTotal; + if (totalDelta === 0n) { + return accumulated ?? tokenUsage({ input_tokens: 0, output_tokens: 0 }); + } + const addDelta = ( + field: Exclude, + ): number | null => { + const previous = BigInt(previousRaw?.[field] ?? 0); + const value = BigInt(next[field]); + const delta = value >= previous ? value - previous : value; + const total = BigInt(accumulated?.[field] ?? 0) + delta; + return total <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(total) : null; + }; + const usage = { + input_tokens: addDelta("input_tokens"), + cached_input_tokens: addDelta("cached_input_tokens"), + cache_write_input_tokens: addDelta("cache_write_input_tokens"), + output_tokens: addDelta("output_tokens"), + reasoning_output_tokens: addDelta("reasoning_output_tokens"), + }; + return Object.values(usage).some((value) => value === null) + ? null + : tokenUsage(usage); +} + function addTokenUsage( previous: ScanTokenUsage | null, next: ScanTokenUsage, diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index 37dc5cb90..4aca9b125 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -2086,7 +2086,7 @@ describe("live scan cost tracking", () => { }); }); - test("retains the larger observed cost after a token-counter reset", async () => { + test("accumulates usage after a token-counter reset", async () => { const home = await codexHome(); const root = await writeSession(home, "scan-thread", { input_tokens: 1_000, @@ -2114,8 +2114,133 @@ describe("live scan cost tracking", () => { })}\n`, ); - expect((await tracker.stop()).cost?.estimatedUsd).toBe(0.0032); - expect(costs).toEqual([0.0032]); + expect((await tracker.stop()).cost?.estimatedUsd).toBe(0.0048); + expect(costs).toEqual([0.0032, 0.0048]); + }); + + test("accumulates mocked owned reset epochs without double counting", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "accumulates mocked owned reset epochs without double counting", + ) + ) { + return; + } + const completed = { type: "event_msg", payload: { type: "task_complete" } }; + const initialRoot = { input_tokens: 100, output_tokens: 10 }; + const loggedRoot = { input_tokens: 280, output_tokens: 28 }; + const inherited = { + input_tokens: 1_000, + cached_input_tokens: 400, + cache_write_input_tokens: 100, + output_tokens: 100, + reasoning_output_tokens: 20, + }; + const latestWorker = { + input_tokens: 140, + cached_input_tokens: 30, + cache_write_input_tokens: 15, + output_tokens: 25, + reasoning_output_tokens: 7, + }; + const withWorkers = (root: typeof initialRoot) => ({ + input_tokens: root.input_tokens + 340, + cached_input_tokens: 80, + cache_write_input_tokens: 35, + output_tokens: root.output_tokens + 65, + reasoning_output_tokens: 17, + total_tokens: root.input_tokens + root.output_tokens + 405, + }); + for (const [authoritative, expectedRoot] of [ + [{ input_tokens: 200, output_tokens: 20 }, loggedRoot], + [ + { input_tokens: 400, output_tokens: 40 }, + { input_tokens: 400, output_tokens: 40 }, + ], + ] as const) { + const costs: number[] = []; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(initialRoot), + ]), + "worker-thread": accountingSession( + "worker-thread", + [ + { type: "session_meta", payload: { id: "inherited-thread" } }, + accountingEvent(inherited), + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, + }, + accountingEvent({ + input_tokens: 1_200, + cached_input_tokens: 450, + cache_write_input_tokens: 120, + output_tokens: 140, + reasoning_output_tokens: 30, + }), + ], + "scan-thread", + { timestamp: "2026-07-26T12:02:00Z" }, + ), + }, + { + model: "gpt-5.6-terra", + maxCostUsd: 1, + onCost: (cost) => costs.push(cost.estimatedUsd), + }, + async (tracker, append) => { + const initial = await tracker.stop(); + expect(initial.usage).toMatchObject({ + input_tokens: 300, + output_tokens: 50, + }); + append("scan-thread", [ + accountingEvent({ input_tokens: 200, output_tokens: 20 }), + accountingEvent({ input_tokens: 50, output_tokens: 5 }), + accountingEvent({ input_tokens: 80, output_tokens: 8 }), + accountingEvent({ input_tokens: 80, output_tokens: 8 }), + completed, + ]); + append("worker-thread", [ + accountingEvent({ + input_tokens: 100, + cached_input_tokens: 20, + cache_write_input_tokens: 10, + output_tokens: 20, + reasoning_output_tokens: 5, + }), + accountingEvent(latestWorker), + completed, + ]); + const current = await tracker.refresh(); + expect(current.usage).toEqual(withWorkers(loggedRoot)); + expect(await tracker.refresh()).toEqual(current); + + append("worker-thread", [accountingEvent(latestWorker)]); + await expect(tracker.stop(authoritative)).rejects.toThrow( + "The scan cost limit could not be verified", + ); + append("worker-thread", [completed]); + const final = await tracker.stop(authoritative); + const expected = withWorkers(expectedRoot); + expect(final.usage).toEqual(expected); + expect(final.cost).toEqual( + estimateScanCost("gpt-5.6-terra", expected), + ); + expect(await tracker.stop()).toBe(final); + expect(costs).toEqual([ + initial.cost!.estimatedUsd, + current.cost!.estimatedUsd, + ...(final.cost!.estimatedUsd === current.cost!.estimatedUsd + ? [] + : [final.cost!.estimatedUsd]), + ]); + }, + ); + } }); test("keeps mocked parse errors scoped to included budgeted sessions", async () => { @@ -2209,103 +2334,175 @@ describe("live scan cost tracking", () => { ) { return; } - const inherited = { - input_tokens: 1_000, - cached_input_tokens: 500, - cache_write_input_tokens: 100, - output_tokens: 100, - reasoning_output_tokens: 20, + type Usage = Record & { + input_tokens: number; + output_tokens: number; }; - const completed = { type: "event_msg", payload: { type: "task_complete" } }; - const costs: number[] = []; - await withMockAccountingSessions( + const reclassified: Usage[] = [ + { input_tokens: 100, cached_input_tokens: 100, output_tokens: 0 }, + { input_tokens: 100, cached_input_tokens: 0, output_tokens: 0 }, + { input_tokens: 90, cached_input_tokens: 0, output_tokens: 10 }, + { input_tokens: 100, cached_input_tokens: 0, output_tokens: 10 }, + ]; + const equalPrice: Usage[] = [ + { input_tokens: 100, output_tokens: 1 }, { - "scan-thread": accountingSession("scan-thread", [ - accountingEvent({ input_tokens: 100, output_tokens: 100 }), - accountingEvent({ - input_tokens: 1_000, - cached_input_tokens: 1_000, - output_tokens: 0, - }), - ]), - "worker-thread": [ + input_tokens: 101, + cache_write_input_tokens: 20, + output_tokens: 0, + }, + ]; + const cases: Array<{ + model: string; + samples: Usage[]; + expected: Usage; + inherited?: Usage; + }> = [ + { + model: "gpt-5.6-terra", + samples: [ { - type: "session_meta", - payload: { - id: "worker-thread", - parent_thread_id: "scan-thread", - timestamp: "2026-07-26T12:02:00.000Z", - }, + input_tokens: 100, + cached_input_tokens: 20, + cache_write_input_tokens: 10, + output_tokens: 50, + reasoning_output_tokens: 10, }, - { type: "session_meta", payload: { id: "scan-thread" } }, - accountingEvent(inherited), { - type: "event_msg", - payload: { type: "task_started", started_at: 1_785_067_320 }, + input_tokens: 150, + cached_input_tokens: 10, + cache_write_input_tokens: 20, + output_tokens: 30, + reasoning_output_tokens: 5, }, - accountingEvent({ - input_tokens: 1_200, - cached_input_tokens: 500, - cache_write_input_tokens: 150, - output_tokens: 120, - reasoning_output_tokens: 25, - }), - accountingEvent({ - input_tokens: 1_500, - cached_input_tokens: 1_000, - cache_write_input_tokens: 100, - output_tokens: 101, - reasoning_output_tokens: 20, - }), - completed, ], + expected: { + input_tokens: 150, + cached_input_tokens: 30, + cache_write_input_tokens: 20, + output_tokens: 80, + reasoning_output_tokens: 15, + }, }, { model: "gpt-5.6-terra", - maxCostUsd: 0.001, - onCost: (cost) => costs.push(cost.estimatedUsd), + samples: reclassified, + expected: { input_tokens: 100, output_tokens: 10 }, }, - async (tracker, append) => { - const initialUsage = { - input_tokens: 300, - cached_input_tokens: 0, - cache_write_input_tokens: 50, - output_tokens: 120, - reasoning_output_tokens: 5, - total_tokens: 420, - }; - const initial = await tracker.refresh(); - expect(initial.usage).toEqual(initialUsage); - expect(initial.cost?.estimatedUsd).toBe(0.002065); - expect((await tracker.stop()).usage).toEqual(initialUsage); - - append("worker-thread", [ - accountingEvent({ - input_tokens: 1_300, - cached_input_tokens: 600, - cache_write_input_tokens: 150, - output_tokens: 140, - reasoning_output_tokens: 30, - }), - completed, - ]); - const final = await tracker.stop({ - input_tokens: 200, - output_tokens: 200, - }); - expect(final.usage).toEqual({ - input_tokens: 500, + { + model: "unknown-model", + samples: reclassified, + expected: { + input_tokens: 110, cached_input_tokens: 100, - cache_write_input_tokens: 50, - output_tokens: 240, - reasoning_output_tokens: 10, - total_tokens: 740, - }); - expect(final.cost?.estimatedUsd).toBe(0.003725); - expect(await tracker.stop()).toEqual(final); - expect(costs).toEqual([0.002065, 0.002325, 0.003725]); + output_tokens: 0, + }, }, - ); + { + model: "gpt-5.6-terra", + samples: equalPrice, + expected: { input_tokens: 100, output_tokens: 1 }, + }, + { + model: "unknown-model", + samples: equalPrice, + expected: { input_tokens: 100, output_tokens: 1 }, + }, + { + model: "unknown-model", + samples: [ + { input_tokens: Number.MAX_SAFE_INTEGER, output_tokens: 2 }, + { input_tokens: Number.MAX_SAFE_INTEGER, output_tokens: 1 }, + ], + expected: { + input_tokens: Number.MAX_SAFE_INTEGER, + output_tokens: 3, + }, + }, + { + model: "unknown-model", + samples: [ + { input_tokens: Number.MAX_SAFE_INTEGER - 5, output_tokens: 0 }, + { input_tokens: 10, output_tokens: 0 }, + { input_tokens: 11, output_tokens: 0 }, + ], + expected: { + input_tokens: Number.MAX_SAFE_INTEGER - 4, + output_tokens: 0, + }, + }, + { + model: "unknown-model", + inherited: { + input_tokens: Number.MAX_SAFE_INTEGER, + cached_input_tokens: Number.MAX_SAFE_INTEGER, + output_tokens: 0, + }, + samples: [ + { + input_tokens: Number.MAX_SAFE_INTEGER - 1, + cached_input_tokens: Number.MAX_SAFE_INTEGER - 2, + output_tokens: 0, + }, + { + input_tokens: Number.MAX_SAFE_INTEGER - 1, + cached_input_tokens: 3, + output_tokens: 1, + }, + { + input_tokens: Number.MAX_SAFE_INTEGER - 1, + cached_input_tokens: 3, + output_tokens: 2, + }, + ], + expected: { + input_tokens: Number.MAX_SAFE_INTEGER - 1, + cached_input_tokens: Number.MAX_SAFE_INTEGER - 2, + output_tokens: 1, + }, + }, + ]; + for (const { model, samples, expected, inherited } of cases) { + const events: MockAccountingEvent[] = [ + ...(inherited === undefined + ? [] + : [ + { + type: "session_meta", + payload: { id: "inherited-thread" }, + }, + accountingEvent(inherited), + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, + }, + ]), + ...samples.map(accountingEvent), + ]; + await withMockAccountingSessions( + { + "scan-thread": accountingSession("scan-thread", events, undefined, { + timestamp: "2026-07-26T12:02:00Z", + }), + }, + { + model, + ...(model === "unknown-model" ? {} : { maxCostUsd: 1 }), + }, + async (tracker) => { + const final = await tracker.stop(undefined); + expect(final.usage).toEqual({ + cached_input_tokens: 0, + cache_write_input_tokens: 0, + reasoning_output_tokens: 0, + ...expected, + total_tokens: expected.input_tokens + expected.output_tokens, + }); + expect(final.cost).toEqual(estimateScanCost(model, expected)); + expect(await tracker.stop()).toBe(final); + }, + ); + } }); test("keeps mocked unverified evidence separate from known cost floors", async () => { @@ -2322,10 +2519,10 @@ describe("live scan cost tracking", () => { input_tokens: Number.MAX_SAFE_INTEGER, output_tokens: 0, }); - for (const evidence of [ - new SyntaxError("mock parser diagnostic"), - unpriceable, - ]) { + for (const [evidence, expectedInput, expectedCost] of [ + [new SyntaxError("mock parser diagnostic"), 1_200, 0.00384], + [unpriceable, 1_100, 0.00352], + ] as const) { const reports: Array = []; await withMockAccountingSessions( { @@ -2353,11 +2550,11 @@ describe("live scan cost tracking", () => { "The scan cost limit could not be verified", ); expect((await tracker.stop()).cost).toMatchObject({ - inputTokens: 1_100, - outputTokens: 110, - estimatedUsd: 0.00352, + inputTokens: expectedInput, + outputTokens: expectedInput / 10, + estimatedUsd: expectedCost, }); - expect(reports[0]).toBe(0.00352); + expect(reports[0]).toBe(expectedCost); expect(reports).toContain("error"); }, ); @@ -2420,18 +2617,23 @@ describe("live scan cost tracking", () => { type: "event_msg", payload: { type: "token_count", info: null }, }; - for (const unavailable of [ - accountingEvent(null), - accountingEvent({ input_tokens: 900, output_tokens: 90 }), - ]) { - for (const [scope, maxCostUsd, expectedInput] of [ - ["worker", 0.001, null], - ["first-error", 0.001, null], - ["worker", undefined, 1_100], - ["unrelated", 1, 100], - ["root", 1, 1_200], - ["replay", 1, 1_100], + for (const [unavailable, invalid] of [ + [accountingEvent(null), true], + [accountingEvent({ input_tokens: 900, output_tokens: 90 }), false], + ] as const) { + for (const [scope, maxCostUsd] of [ + ["worker", 0.001], + ["first-error", 0.001], + ["worker", undefined], + ["unrelated", 1], + ["root", 1], + ["replay", 1], ] as const) { + const rejects = + scope === "first-error" || + (scope === "worker" && maxCostUsd !== undefined && invalid); + const expectedInput = + scope === "unrelated" ? 100 : scope === "root" ? 2_100 : 2_200; const events = scope === "replay" ? [ @@ -2490,18 +2692,18 @@ describe("live scan cost tracking", () => { ? { input_tokens: 1_200, output_tokens: 120 } : rootUsage, ); - if (expectedInput === null) { + if (rejects) { await expect(completed).rejects.toThrow( scope === "first-error" ? "tracked session record could not be read" : "model pricing or token usage is unavailable", ); expect((await tracker.stop()).cost).toMatchObject({ - inputTokens: 1_100, - outputTokens: 110, - estimatedUsd: 0.00352, + inputTokens: 2_200, + outputTokens: 220, + estimatedUsd: 0.00704, }); - expect(costs[0]).toBe(0.00352); + expect(costs[0]).toBe(0.00704); } else { expect((await completed).cost).toMatchObject({ inputTokens: expectedInput, @@ -2514,6 +2716,124 @@ describe("live scan cost tracking", () => { } }); + test("keeps mocked invalid accumulated usage fail closed", async () => { + if ( + runMockInSubprocess( + import.meta.path, + "keeps mocked invalid accumulated usage fail closed", + ) + ) { + return; + } + const rootUsage = { input_tokens: 100, output_tokens: 10 }; + const inherited = { + input_tokens: 1_000, + cached_input_tokens: 800, + output_tokens: 0, + }; + for (const [middle, latest, expectedInput] of [ + [ + accountingEvent(null), + { input_tokens: 120, cached_input_tokens: 80, output_tokens: 0 }, + 120, + ], + [ + accountingEvent({ + input_tokens: 110, + cached_input_tokens: 50, + output_tokens: 0, + }), + { input_tokens: 160, cached_input_tokens: 50, output_tokens: 0 }, + 150, + ], + ] as const) { + for (const [scope, maxCostUsd] of [ + ["worker", 0.0001], + ["worker", undefined], + ["unrelated", 1], + ["root", 1], + ] as const) { + const events: MockAccountingEvent[] = [ + { type: "session_meta", payload: { id: "inherited-thread" } }, + accountingEvent(inherited), + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, + }, + accountingEvent({ + input_tokens: 100, + cached_input_tokens: 80, + output_tokens: 0, + }), + middle, + accountingEvent(latest), + ]; + const metadata = { timestamp: "2026-07-26T12:02:00Z" }; + const sessions: Record = + scope === "root" + ? { + "scan-thread": accountingSession( + "scan-thread", + events, + undefined, + metadata, + ), + } + : { + "scan-thread": accountingSession("scan-thread", [ + accountingEvent(rootUsage), + ]), + "worker-thread": accountingSession( + "worker-thread", + events, + scope === "unrelated" ? undefined : "scan-thread", + metadata, + ), + }; + const authoritative = + scope === "root" + ? { input_tokens: 200, output_tokens: 20 } + : rootUsage; + const expected = + scope === "root" || scope === "unrelated" + ? authoritative + : { + input_tokens: expectedInput + rootUsage.input_tokens, + cached_input_tokens: 80, + output_tokens: rootUsage.output_tokens, + }; + const expectedCost = estimateScanCost("gpt-5.6-terra", expected); + const reports: number[] = []; + await withMockAccountingSessions( + sessions, + { + model: "gpt-5.6-terra", + maxCostUsd, + onCost: (cost) => reports.push(cost.estimatedUsd), + }, + async (tracker) => { + const final = tracker.stop(authoritative); + if (scope === "worker" && maxCostUsd !== undefined) { + await expect(final).rejects.toThrow( + "model pricing or token usage is unavailable", + ); + expect((await tracker.stop()).cost).toEqual(expectedCost); + expect(reports[0]).toBe(expectedCost!.estimatedUsd); + await expect(tracker.stop(authoritative)).rejects.toThrow( + "model pricing or token usage is unavailable", + ); + } else { + await expect(final).resolves.toMatchObject({ + usage: expected, + cost: expectedCost, + }); + } + }, + ); + } + } + }); + test("reports mocked readable costs before rejecting missing sessions", async () => { if ( runMockInSubprocess( From 7eaa2f1fde88e297217b1035c48d80e5a7d0821f Mon Sep 17 00:00:00 2001 From: mldangelo-oai <269034524+mldangelo-oai@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:35:43 -0700 Subject: [PATCH 18/21] fix(cost): preserve final accounting and worker shutdown --- .../_bundled_plugin/.codex-plugin/plugin.json | 2 +- .../mcp/server.mjs.br.part-000 | Bin 140000 -> 140000 bytes .../mcp/server.mjs.br.part-001 | Bin 67789 -> 67789 bytes sdk/typescript/src/api.ts | 10 +- sdk/typescript/src/cost.ts | 34 ++-- sdk/typescript/src/version.ts | 2 +- sdk/typescript/tests-ts/api.test.ts | 16 +- sdk/typescript/tests-ts/cost.test.ts | 147 +++++++++++++++++- .../deep-scan-worker-shutdown.test.ts | 53 +++++-- 9 files changed, 225 insertions(+), 39 deletions(-) diff --git a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json index 04c30b9c6..2530914a2 100644 --- a/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json +++ b/sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codex-security", - "version": "0.1.20", + "version": "0.1.25", "description": "Codex Security workflows for security scans, analysis, and investigation.", "author": { "name": "OpenAI" diff --git a/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-000 b/sdk/typescript/_bundled_plugin/mcp/server.mjs.br.part-000 index 4b7c17cca40371dc0c4cf946ad66f598c024c86b..11e945290ec4da2b9c11da57f90d01d291d30230 100644 GIT binary patch literal 140000 zcmV(lK=i+x%P7)C7PZN~%W1A;tx!|8uzg7eqn1#Y`w2!+SzO zO(!7BU{yC61aT|9y3IGgAgZdWx+&Q+#{4_le-A`a%oIh-TIy=;8$`*v0?wC06-o}a zhGW-IY|rR$LD|!QRuWW{M#W_6&m)Z^66$=hXVZjovX}*#at_EHO<3f5LB6eaxuSsL z?oBmCnuQ*6k{G{P&CiWu6LyIm92BiHwe)C6Jn1nl27AB#I1a1GS{0$-D>qA$ezt1i zpcpDsMSZ=<7H6yIQ94Qmx#ZzRNb+>&m!m|B&@f_z!WnBCo(XC5i|Ak)wcH%nE3JVN zZHfCfC50vn2gMC@i#LSwLObM;Ar*$A-rB=cT?qvnO~3IGy+u3@Td{>T*5vnj`@9f8 zMP)j_1!)L(jHO{gO(D<kdA*C*&4ynTVR z=U6M^2?q}I&b9d1Q1UN4WUu2(VW&Hf3TV2oVE&o}v>&1a~L$;x!h zy1PA{;QSN$L;4lzG}plieOqt~B+yF!5@&+6LKp`oHSXL3W*odvzi5XKbF^D6Q z6ETUixQMH`iMx1+Cy6WY79a5yzd7QcTq1f8E3TVJ^fn zSgqqk2x`k9^KI%ftX^}%_=(pV&iY`brX2PIyU;MslkeN*-B%jpM>TOxr#ilRrzm*=GGmQ%YaWUFgCvnqV`mRkNg zR);)T1a_`d(rw15A6mAyj&(E+w*TiVOAGpuCs zNH3=g$evs8HUQi7v-h>W{{8b6(9G_V3yOud`>~)FFnr6^)9y#P){l7=3>B5Z_`uTdBYr7*bp_GSn!AWD}x*FDW>wr|#dk}<_BSuNt%_+)^q+DEp z+thGcm9p}OT-w%`DGeKrCkELk;{?BzsoCpGX|eV+Kmels8(B4u&lHc1HBZG$(asop zf6czr)%Ejs@r^i1fCz4JOX-5;mwqdCVx_+IM(NZ>&-U*66RPq{BKboC5D8+?!u2$v z6{}tz+5hjCmS4Z`FiX%$f#Z^gNz*GvvQ1kH5mGOEHC%}|!aPz{$X1p`r(SQq^vihL zt%$KqjmbXQ!=PA}nf3Z6^zw0b+p>qV$9;uZ(WMYeOrk1N1_jLjwNUx`mQlKyaFK)f zJ|y5Ezh50tFdL%P#*=0ZZYU5g4t-HK(IR}anN^gkcw?suS}g&aX$PG(@!KHX%dYYA z>h#*@_iFyXf4(}ChkyhF)ur#T+1u}MP=zS8u+eZduiw>YKQK6Eko=CGbZ)YWLi{4Is2G|ZB1g8D+D%eJoP z-au!!4NVx_-m3%QS}Hw6lOz2-apGqIwf=u!wcK4NP@uFq)OPMu0wFr*rg0_Qci+3> zPZ%L_MH0}FNRS?qq7q5XHYurex<$t`>ZNhwjn;d{r_Ix%(NC9yPM8@ZwI4_bGAW6JVRuLAuBT? z#7ZZkOo~96B@!UEJLkT4-;2nI_ad^wWmaZnRv|01fGz{F0g(TD0&*>Drhcfwgr@(O z^D_;m8q8m0v9$_p8FaS{NICM4zBJO(Q1hAozV}sYCWDYqx zf(Qu1Vd?5?hQcHf)m zvJ`aS(91_LiSFupr@@d<2P}EU*-WOYepUVIf@?G|nBkl;Hj;@HqzVJYdY1;>jWfgL z1~f9X2#rA_5Vi6VV>nIWD5>+LS2$Wx146y@nV9*?f^OS zTfTBTjRmkET#zCCTU`qa3pKzy(p{c3+H*>$0WgKY15nh)LfqtaTU?SC8!*V&Y&4^x z7}`5y7aK##o*qb?X#{_E%uA+)0~Xk(CWo|LLV$6(>z~-m@K^nHv^uL>=Y6z52qA=T zTdbX1xy zJ4a>#(J}q)*kBOuIbro7)9Rozj$Zm zMu+hHxBEk_{$(y@T6TWfu|d#HH%Q9e%T)R2#Kx`X9}Q?NkaQ=82-YEUK3^;bpNjlZ zA!{5V8^QabTK}AN$=OnTzeIvzpbG|2UrSbFum4P=P1Phb(fb&kH5%%#vdD^1D)@q(57i9{ZU}sok_P1TQu(Ns{7w-DNqQ3 zQshf5XZ(=;>ibJi3vR2!?t2PJrSc*0SBl)u!LQKSSF9u0n18`Y>d8H+Cjfhg-Z7Sb zUrikQjFvtAS2-LG5fBK`t^SOq^2=t9)~5N#D%UA00)l(wek}dxPjX|@*VOhejSR!E zh=4mHDcld$;$QY+doNDnUoH}*4lq%0nO{1Zy3C66zZj#BFtDL^6mrNJ8Ub zJFvOEo#E;u(oZUEkk@(Mq%)JOT*J@Elx0&1TaE5MFVrk5cb|Imk8&{5;N|aRudccU z>Gbh``!zxK&*B(n^df<5dB_I)8vvARUgJKD)sQbl^<24anEdmyJNsIvPg?}|DcQ^g zXV#j5cC!B$GYe(mhd1bOGmpbzv>2v=-3Xm{RkkJzK(8oxC7FbWk{$+bK%lfLQ1Qi@ zZ4%`Vt#0O|m7*|!MFRxc_LEBY*=OK3E@9n4_`os?1hDGhDG)YennIms>%!!RSoa;0 zn7iz$*Z^EP!zCWT?yzIg>I(Tkd@ntQS~?L-b|<_dk{0S{RHc;APegJgA8f9=RAtVj z8Wvr_`1-KGP(zEEga^W;H;d$LA&gWXLhnz>a(Z(Fy>R+Hg%*k5!)(8Q}P3%n>t?w8PY&!urmN0MXowabs1pnlUt7_9u&~=@JwBU*Y?}*<=z{gG=|OJ?#>8{zRxWgAMhZ z(RgH8h_(?Xoz)GLl#X?%tjDJH@ol=*}7POSdZpyaR0xK`>gWW58 zGV-*?DzfL>euI0+q_PIT*8GVMwS^Y)x=ErnV4#wNI)vyo9mZ+U^+)0 zkGA5*Os6X!R#v0QIZwf4l%ha|8<R>kAOyI}JsUL55t(;PNQ`pDEp)%mjgNUvg%$}5H|CWAER}y)vb1-67Rco06y@H2N zLUP@Cm;y@tYBb*xrkXZs(yi@qs*?ycpg7r2@=JQDA%KpujR0fxrGS zfJ5>$QB~$QNDWU-#jgiTG#a?Gv~?=42SQIJuhxlc*u6D#CA(-altdWI^miuosdL*| z>I{RNn$Y@CXvc>e(`tl2tfvk)!a_W&S23u=FFY@a-V|&}ZMLU}o}5A4E_i>n>(^ri z?kshG9t>@D2w)T`SkF3zkrSHDQpo-&R*?(#0;IF(SU}a2eeyK*EQADyZ&-NKc z7|D(iXIW5~^Ty9NnMigRtz~a_(gA1#JGWB5Bu0@Ap@FZ-s^aLu{!3)EGj-IGN_LL9 zDZwCDZo8<7zWo*wNiVTPb?GWNTU)PP83DXNci?~K-K{TZ=Mj73d~NBIUh`0sJ%GN# z=8iqKGLWt_qC9G#;_Ceflsncn(8A0!meiL=u=(bah+le9Qdrs|Jy>j;N{f8|J6*6| zFDY=8X+o32fG^FNy)_;(I2l!>ic5{U90lr>LhBrt4#kzpMk^;YElGfgs$sltQDQDl z9inq&l~X=FR6i>V4|itHP^U3%w?Up21T?0T*)zC>g%j7K%}(%NpcFn?j1^ zB>F^wN_c{=#ewOmWwTpPa88p}*0n)BRIQdqpfIh$@OcT5Q)z{(Vq^uj?+m;Rb32Ij~e9a59RNpKx`u^h8KN|7WGB}AxbjE60 zo)0V^COKt^AOwk}+%+>*Gu&~3;(0VMhz$|Fql%7q>JSv!h@})c85W-~dt^=vVQKO9 zWRFW(IxRvid!~yP*j#gc0%uQugTMnU?D9DKxI!rDf-ah*psi)A;RaB%n-wT~=y2s| zw5DY;P9X|JFzlJmQbufeOveo53vpa(AeTwCK~Y$EfcJ-I|D43sLuNfa)mbv(*Y2!} zN!P6Y)NAzK!seSjq}y7#hKGG=WhWz&S@KfCE8kZpoK6p*js(EPt7+kG{@nm;9hVY( zIWo?BsANGb_x4xufLN4Poa!h}3|a!o%7~&0bVug;FP9%F2}WJn7ozAXL1kG2bi)ow zGKlz4C3`_hucpXNZ$ik4EMlTdtA*Lr@&_-%7~OL6Gv`_ba=8S0S4G=*z-U5t>F8!I z)N@OB4qbMiM+B1k;yb3(ZBHsIFtu$vYN0JoK2f|a+TA9;I?TT^uqgoV!{63_R*{%q z`PjXniK0;N6K#Hhv6Tq9s#IB1vk{Ce`3A9x`}YIT{;Z>GjrJOr==LdEMZBdEu~m~= zG6iJ>D_~sap7TaKGHp^C-Z)`(dzm555j;ZnX)$ADo%cW*Rhhtpv=$G4f(R?enENtL zy3-=qnXAbFr9?HVI0&3}01?oE&wFoKgIG8DM(WRWQOKZYo!a63t3^jjyPW_b7R&gX zDO3>W49a0HHh}p0Asf?py^~C`uA;9I?@nB)CzTK9u?KK)n@$|N`t(|^)HKQ{5-!}@ z4nhocmsPQ<7pna5q_0J_7BZH(`w;`T5a7VGbTcH(X|a1^;Q(Zh%u4gO{$h zeBIb&u~l+zbGA!Jb+Moz)!<)(Q~9q5=*`^)pt|>$@K&qsNd((#ONNgam!p*UNIinUAImaJce|9SX+VBN~ruG`4FBb1*C6)XAH=f>LnWWnUZ{Vr;vPFVnv{ zCggIT4NmMR36NL0S1Ft(hK&{h69Iz>OIfr+X~xY-@NTBdb$R0>54e9ag^^axTm~q( zTJX5}35$M>m+6)+2wQ5P7#vzK$lANr3$9;O z-%@R7nl04SP-h~5HGV57REpe}C@4eNGtN`Ao^V{d2Q&j#XkN1QA@?z480qfe>DN3b zat9YAJxRY5ZnWzjqQT$iQnJGj*@h;}VnmiOi(kV1oQ-u+P;-}Awt}V7n_5{wCvE=l zx`!{h043U_k-zQF!<1d8Z&`Qxmh*oJ5Fe%+NGnfy`B)h_uzb17Jvz8u7uJMyRxm{@ z8(arom5W;4EZ|M}A?e)0Jbco4ftQKD1&8Sf;u&dkg*GVA=mXPb%#|c~#x5vcX#wl^ zYh$?1a0O`U)?MK{YKw4ViK2%ZcA5cn^eBQ};|??^w}5#NA#UjI%Qj^O%rj20TADqK zg?8(tR-VQ`I*pNX@S7Ey#b`(%89)gsA1X<4chRO#`I@u@xJ_l-zsHUbS1l`pZp$m` ziDL&BmZCn&_2V(W(I)$s1uLG|tCg3lfPGgZu1`-|`EqyL$^g`BVO#Ok?XE2Y)j3=49O)!J`!K4`8 zo-fhHWJ#`I*QLO!>lSTFg^JO#3Gn#TPNasA!fA93i=h}EU(GVmLIWrBL9@1nge~LV zeXHT#IAvDDKX)^^jQ?%G{e1bCin<_W;2NW)4Qc zCcqO1%cb^jA-qr$W^_&3yAi}ceTfzqp48j-q1AJ$oWGk<&AUyZImM;)62`EGpR#N* zkm5@j?lOWl0UocK5#9qllM`xfrC|qBk3i#CuqprUo?C zG@pz17nv>3T5}$TT~_(-!=12RTv}-{TN`ZiIl6K(6~LJtx|LhhJR$S#1ZCjNuvC^Q z%lhbE9-4eWsIzkR?(9R>SvFtT%zSNPIN6d3lqUjKPxfcG@jZi+cic$5@Ahf~(!bV# zmeZYJ$UCaPf&FoU5ohC;_x~~JB2p2R<}7DQ{CScObcUHKDB6Jj%JZ4eQC4U6?J%5> zUn9H-&8S}QSgS)>>A6PmM#Z!411ah+_wl7R{Xk`|Et-<+GlNQohh{Oq!m~7lK45eV zDcRG$D}X(Ur)4WQ80yvfM&IAnZhHBynk~QD=w}2{^3KrDGo&;)>YpnO1F^A8tWAgEex|W4Uoh$U0|zWGpYqr}M3Y+AnHQWb5}h0z_pR8x z;$X0y#~0b9eP(B4x@ll63XZGsf3Eb8-=1nI%B7&xWLQm*5D<4HJ z*K?PcXQi~a8`HABoePjI+LNxhUb-8Xl>s6C-07dH**4!WmFqPRT>mC`k_j?bvx4=U zTlpki_hJ=RDYzAmItH}dpgi__7{2rE-@5vcSdQja&(JfQ`t)X#T=3h!27ta(JXKfK zh6}^92W4y5S%l!VN=^*EVOe(It`?rBWV zA2BGdfaxMt`U-Gxl{h}stptR(qKuZH#PvfjTshAQ8UON8iy4CXlGMXb&R@s_xa7=l zZ`NH>7`S*`b1qig|AjsQ4V*X?K4E`NRXS5(otorEWil?b3=r6G!s@y7M`B>khnWtV zelAH?&$HcLx#=cS|Mm=c@5v|ECMdD{QS1Ffe)blRu9WY!KInfIHgOt<>O*2YBG5=` z^_lG`{K+tt?I}ozMU~XJ6oRuXbREi|Uc_N{@Cia2XS4+GaAf+FV=YJEnN$uv4u?!DAjtNfD;yJ)J{&8tmY$S>=+FmqYQ*a7SA*o!{T*>vG9!nB zGrd-h&06R-=!Dso$HdazGz7iUM^E0K>S;%u03G@wd$`{lZ~nmG7axza4UJJU0(A04 ze$Ut}8t_ZezFL*kENN?nc=nv5xt!*Q7%WLG3u?d=q9e zGRPuzm);(ZMP0Uz?&dDJc=e=Bx+-@Hx+lNBFcWpNXjTny_p16{-r$TXQX>ZiydF`` z)H@4i(_Qc4!1fhWE?!w_GBg*N`l;Y@zhgjVxj>CIQC$&cXvmPpIBnF&?Gb?=pPP@k~mJJB^9 z#C@*y*-yLUNDMf|4n5|Ci{hvF{e0-$%H!8bBNM+ViL?Pe_$~xDs6T!k&NehR@H$kX zh^aglJ;K|l)Zk_vR9FkoS%DVBE6+F%{jV9{vb_A5Gh~H&!~6%H_yDGzJE)9BMYi7( z&SmnT^_n2*2~IcwcuTUTK{u4mIi%eyrFE(HU3Q4!tV1!r-v6ceKRC$$Ib^Ky^5c7r z^n?JPtEOO0cvuybf_4)@)|i(LmOnMzdCAo1Qrc7>b`yc70*YMg``L|ut??TjYEA4M zHyXt1!mVJ9;rAi_N-4j**w(MHJRjB`FrqyFG1Mh!+B92F>ajN2j*r9>?j}@JeNM@SM&6lJwLk( z>&MJu!&r?Z6e)m$3fLAOjo0NadhR>ZtxxvW$z|+qX!=?ecA#UAw_xyXQQ*>pQpj0z zqwXq^-*Okgf~GUPAmroL`5#TxwTTkuu zhEQ6HjrJBqTX8AWV_%ln=+De-AGP%~uD71X46KgolmI$trrg?No`r_iL3(+?GbyhdDNH+7)L9><-cXq?ExO*wr{tnnPOBXz(iOM*f}j*a z6SQ8LF03Qnax@6WV3=?Govv#T=XYLGv(S#x5WeIg zR|(|>-+nl(aHlXHdfYuMKN}s5N(+@n+PCZlAF-Kb?xlK-S*{pu*UdVrHiQJp3hXtz*e`jf#(LD! zKe~0nU(eKK(1>L>S%O_cr#geDmL zm?SCq{+Jc{E^86*6uY@|aS92e>^6{ zYCD|J)j9KQBcsQ|`l%O70ni1bVuzIGJVrQs|FfxI2$Pv&LJP_CR7s>Un;BWFx3;epVA*yIZ_yIp8 zE&Vt0;=?#`Ms9?^_gT^WCU5D|x6jbOTolIO0TTtSsMr>X9r-i2NQlHYW#rVQsY#1bUK4X{YK$R*6s*WfG z&+R^c-=);=uVJ#iT48Cq#BdT~$hd!&PP7-~)&IVe?1#}N-SL29iyDp?NANVE8(x{= zU~%EZjAYpvM76r)obVl=gn`NCpD|1S315awJi1fdk?uoWEP286nJZ^lFSfftGPKOb zjxco+X=QewMZb2jE=)TY(1Ovrs?Yy_LA19;EiZ}$ii~eFp}dn^%3Mib?Xw@~eV#&I zYYGV?3l+KnB(v-i+1pRhiAG31n$9ZXg5?}DXU%;$TX7|Px1y}-`6Ngwdr^wlIcx95 z*WKN4?2i#sc@lhmL1zPJSVc8Qm7 zjKa}NjR{b+C3B!7%>kOp!wXka~Wzth-LA!*q zm;$NGPgTyJcBeTE7cnaJ*LrHRPxa=k2Ae!MMUwX^8>WT%v zA^atcCkEFgy++idDV{m^j#!0ab|kD0DbMB3Rv&i^NX8i#RDjXd6B*A&aOtJ9EuHE| z(?(E*H>LmhkkvR=Lym|5WivgyXrG4ECoRiY#eq!`&!-V}s*Q;;+-ew&Cc8)*Jgi8p z;rytw5G$x?i-V*hNi-~}q@8(&nK;mBniZz9VQeVjC zJ*t3R{RbPmX5M@F8*vDG+K7id&W2Ztbz_{8eu+i)L{;tnKtupmpa(Pq&og^Yk(no- zTM_~L>;|CQPMOzfhkf<3cwx)`P*Yt2eMU(@R-6oUJ4EKt%66;TlQAm@XPC!|(_U-k`ZVkt6)I5S~oEvnyX)~_|}O1D?Xpgk@w*dOeCyh;xsSlNO-dxWm11;u{L*S@=D z(KkL9u08nme(e@Xg39{oJflmK!HTzE4USo7hcxmBvKBwr~v)c^PRkptwF(Eu>}`{N?M9gcj4skNf$Hgv(9JX5+Bg>Xc|h4ZqJ`_A}u1 z&!bFR#~)2I+r6hs*Aq(QDY3C#tH-^!abGL@FEwZGY5sq*lZp+c2h9+xx6brERf*oA zn1$;;vcB1W)a`iUMUU{hHVkyEY3q_T1n~hIpu&r1AuisilbF*D;MO-Xz^u4K8%XBC z7mvRh)GHL1c*nY-txm9@afJ0PkN{crl;%Et%|hf)eR$37?xot~9qjrnGN|F;Rkejl z;pF`;xB`;hwEP7yQ)(DLBy2U_t&<8Bt}Y5AMY>6)fO6C=*<6y>?wG*fqf2WCOJ;bo zJT$#Ky+(?cBloXxk`OYejxQYKg zU=#oQ!+b<1J_7xyvU<6{tTq}&iX3bI1&^h+X`a{Id(yesC5u;nfW{Lyeg%jr_U;g8 znWt_zafUf0S~hu~yxZa&)i@uv1a|Sv|7QIZlFl*>jmG#}SBoT&oo{M2SqvXBgeaTE z-*Kdi{6Zw}@NpV(|IjKs{-+_c=c(E=M!8|*S@p})p;IOgL&uQmJ6I_u)CHL?{R37? zXr!R%BSx&+Y)K2ixp!pO<&7CIsAby(Ls-Axt|7zA-Q;gsW)%7!vL>3+@_!f;{Z*|` ztT&?#tIp7hU#VzBwCa{ixm;70``a{g$1sb}vyo_dIeofgQ~ zfl%g7$1HXyf z0-lw=M{`kcOvHlyDbjHBFhXJQUbAtnAEu-s@%3Ov04xY{!`#Fd&r(f6%l%d6{5T0Z zGs(}#Lws{BM64Gi;WOF-SlXYmYF`gv=Nnt^^9^pYQ>uuK#l2K29&%nRNtirJjbtRG zXc{Jb&&EAO*GPD%CTmg@=w8LuPph`K8_ctY6f&{uxNm(SxD1LVEeE8d7HBrUMzos@S ztX$ki!LCQ2UUfA z_uXAEGqEBtxXB@uM9Xa;D}>5YKSs_#Ef=L#3jjszfEJ-bw*~Ma_NOZbbj!uu+DS~$ zEGwUbH?Ha4mLS2DC%>>sF;Bw03b!b;~|P zYejO^%%X9c?RMauL^Ot^A6LuuLRXj{e`ED6rn~yD1T5y~VQ`u4?I$*t!&WTws{_d2 z?s(?ehyA%sn|p>W>o7u7INhWkc$K_yom@a;M<=-R!x8jUidqANyD$X{aEB)||@ijJDUxMvXU&4#0BCBBIOoN3mgv z+sc)nTLJufxoLH60(T7KGduH}q;K#uz)|vwz)M3$ctBOAlzmi_Ry;?FQKE%%8D=qRZO!=^hFCI9)p-$Rap~ z83YXK*-IFS6#3uB@ZngU&bBNYbS_pl?@H`4K8mD{u|W~@K{EItfvGQI(^wVB3(N<% z)Skm`#F1hXyV3lI%+d)4gn={-Agw%n-1f@|80z&ke$fO|DwaL43{TUldpG& zSN;xkE$!j0hI3g^b0sW49Cf~-AI1H#rq9us#tfSMEbOrb?kk}cy3Z(c+rawf;#A)6 zksWD&EnHc}J*ry6zXm?eX#GjV5>&EmECu`4Js~6>zvrl&AbaX0}WlBX-O_&qB+)*5iR4tTr$7S z@G`rNnrZTg6Nr}<2IP3H6yM_1-O487R&vivD_*YBQ562`w;x}hHp=*0cN zxV**RyFYUjQm^sfsyi049*2F5k|K_UzVf9;WQc+S)U^DFYS1@Ggr~ilw!MUXP`;Sy zpL&vwVVN!&4oX!DQ)o$F#6YT>&YvL}4hbs=4%B~l>6iYT=hLv**wL7e zoIYP4Rw%;rYe;R5)UIEoKzy!^^v*GelukEzHH8VOIH&g$+t6Qn*1AUvw9=cV3kV#qz zMcbaM>Fy1cbXdJyPwasERjJ#SIm7{}hV#<>+RkUX@Q9S>^)$d>oHm5)KqJG!j@lkv z46+NS+W(AVNIQ(QL$Ig4hS^AEX~Ex!XG}6=MdH4#3Ohx^0}s9=Fze*>>e*laTnNRJ zj{`&JQ?c|A1zZ;jq^;)~b{Q(%vT*-SUwND)!oB>Ig%{`tX$o4qUM?Nl7(lXy(AQ!7 zTtP|DfB zg8rP<%NTh*@477%t)0#IK~{!VyU;CV&qB}wzYzClI^$!EdTxYxbvrOw-x^hk&w*G8 z&1==;ci)xxZ3A&vmOFpu2K50DiMkWOvE2n`W*+)JBqbPPa46{B#ZWt^pDp5G*DElx zkF4;lcXV!kj{*)NSHh3hm=_LTiKJvIH8>00Gh*oT-am#mL@}XJ>WnEO6_b%sj2*f) zXxrKNTlIj3R{HZWk7pfexN-$Zt*Iy^05SJ!u0T68UhGB6&81Wj=S8614RT zx*%&KF61Q8c345Z5^qodJgiV;wiS*D;cwqLejflFp|c_Ud?vY4D%rcFw2N)*E*5DD zb*3bJO&$^yZNN9VE{cF(V!j+ESq_{+tUvXcor7%C&`DF+Hj&+a=YMbg-jD!vYc=2H zi;FtNWy7*LDX(JxoH;i1X!KI=0);<%`CIRAlpF#QeYElo%rygNNe{(YiIjeKH=wD$&hlKF|(-qutT7eoXMol3CQ*0L2)>JcRJLU zv9tPsoq1p(uYVF7)y+7+h-e2eR+iNX{e={zN}h*&cov)0spk?HV5y5iT!x}2mw~#r zMWqAOr?AP&ED7KA`8}awM4wwA7Lkhs-2U!88L5wb6m^7f7piD;=4_a8Pu7$~I0)zC zA*wx<)u;D2!D?5tqC&D}3IG=<`7%geh}1L#3eV*anP?jYTyy-()ez|Tc^5w3O4sQz zG*2V{$Ozmu^jZglH%2ZwQHXzpq2S1!Voyhx(|#{gp3o4`!r(nwn*pz%NCBTXyR6Cf z>S%i^Z3}J@cxr!Yn(|!FPx5kORM<4Os+kb^t(|hbF)RZxz($SWHqM-`P(-F~cXzqq4kpPK z`SyTF05}L!970oYAsPhw5lrt4T+VGl<-}>$B$DuaNwl5+AgjjO-`(gCI+i*b*f0j0 zg-MxTzh1N`Yh$C`c$OoM6^O=eA;+BjkV8#bbZPZV-y7*Nwqa@oG-wYF8Pg;-aM1e% zQ;iOjcNo2l5Mrdw-4vr^dtBJC$95`Hm(830z9|>D?FrMsFuN@ql;QT;CF&Yu+1O1{ z%ZAd^YKa+WN6?A47J_5APdUg>rz{q=_Ca0*quX^PS%I%`EaGFPmW@Fa9ewUd#K;gt zUx^wTPme026q_2Nj^%?^`y!c!z6i#N`+_P$_X&qtr0>Fk+%OLt$L%mK_Otv#L|~5t_(usyj7jh9bRs>sv3prY`-DKAd3kKr%&qj zA|~B?m@UM}%eXk@s<>YpLSes3F1x^zFb$@O?+%b{iS}#UQW7~pIWDMREu3G-Y9wi% zay0bz>qb+?zCY~lOM0j+)C6-_}+3fK)wy9!FADv~#cDY``E)KXhlK?I1 z2X>##dTsUTeEM7S^_t5}BL%r6XPbWeuBDYa3c0J^42HQOv0W?Q;{)s%<`8qE+C3x1 z#U@)FElqxy)SGg7(Ev~8H{~ID7c)j6m6I8DTCJ@clp}9XTS@yxQ`*`(V?Ytt*0g%t zPTReL=h63|3%zJr}$Q}e8}D!m);*R2ld%#?llyycJXsLgCk2Q*>#qMZpa$-dh@TmoU_ zG2(<$R{7d;Go3V>fpJl-C|}f8ZaVzJ&VHz^@RDT(Pp?-yrW`_xSV$ZBjQf}>c(~CF zgEv;r%>OpKXsjBxMqx*g0vYzb7WzYOK=E6Q{H)gyvYhZYgCSo&|0wAtRui1tv|i|< z-%ZIc<{Y`p7gzPlO*lk%%3qo{!gAXo>tOI#pU1R4(sWKsHvW9_yr~Y|J|C!7RD|=y zElafe+nuq+!vh)p7!-9bdrjAt)~q8{+tYLq$6^~8Z)P!gTO;jHx*%}G4N4NDMd-|& zj-DnxUHJumxc^CR`$6)lUAbXZTd6jXHV7ow-z^=DlZQA1Hb>X0k@&l!05ppvXR`Ca zV@w;Sddi2QKm~1_cTN>0!eCZQvdJO2!54Z)HW@xaAid8Ia9tQ&Z<}aPW8Z~P(N>S+ zyhGL5l2-~!5GV}#Nr0Apz}FN?4?@!VPy!(^Wo^#IQFs0!_L9eAef?9JjKlG2sVOKc zSBtE>r!^f^lqEz~k-co6VmTvmp zcb0_=%7(ItWiD)j>Sc4Y=#-88{|8(2w1^)quvsvA$}Uy#Jgy}2Q9z*YYPZCRW$BKK zXE*_PF3hL$vY;^)NUDTEL`(K5H7`b2(7liy(2c+l5$$f)_AkjsTYkwg>;qdD2D@p^ zqLXJZzZ?f(L@t=w!8?A<>fDp==YV!){(*(kt)OYANjnncim7<`4r`Q9C`0^*2}5P($hL+=Apy}E zk<7@(r=@nV0ga`aip95B8cIV{lo5vnv1VN#($sIobyorT_I_|ymGpt&zPpstYbttZ z>phv}MiR9WFh3TVf>!WN-uRu#iPR3vV`cqRxf^2rzDLs+i4Asx!ly_|=l3lEjbB zM2FVEW$OgFOn^Thd*p1Q|9FUIfXGD1X`4j2bh1%~hi_93VkA1+k(4~AHR7c`&Mx@< zsqsWER5SFNz9cnL|V~a%(thSB)ls8!BCR^y8Tn zpd-6j$>f3Y3zo1E7<-KyhjdmHX&KmggvB((1?`zuR0#Qa8(E>_C>KV#Ra+?A&tllJ z*e-3Ha0FYn#Qg+$ErD}^W>s`7KZPZtG7O+DI^B{v%Te2s6$BNL=R+rE1U}`#Jj|ANa68y!VIevS!sPzjRJwe&NAN z6&o0-q8s(AP5swe&%A#K`BUEllip-99bP6Sf_K?eFI?Zm9#2!g;7s)uZB`+edqkLy z*IPMyiokI!g3l)7S-1K*!|wGWLHQv9q*Kfdc$`;s9Fnzbbm0}AoD$|F#azoK-1?QZ z;TzwSMF7O;4SvD$A~aGMsDWttB4J$vN-uHA`>chD=Z3y@F`(|ABan}n+4l}Q7f}9y z2#a;4t06YFlPo;O>`+z<0h2N8kAeV2K)S!3ZhI4WH`na4D6>Mux-bCl#cEL047o7R zEhAlw>4p~3d%(sy7QJhL%88aBRB~sSj=p=26RR!i8_;VN=x`Tqa52Qpl-q5<5mSI9 zhg2<6BgP2E0Xxi5&E-J!3ild>h#>yPO184|%}82?G}b)bk`DQ0N*#eWME00dntTxZ z_F}`LLCB|ZnUL0FBVza#R8h&P6W`dfW5;A^9cbar6jK(s2`-T?q}Bi4fJfuju!)pQ1slqgv4RD^+6@F4(LP^Hq*1Pr-;( zS8w;imTiUdc3sbHzU^2SwHqJDccH=CJHmTKt9 zKu+lD+Pyszz~4elfjLrreFWb#J#$K#mcQ)9r62n;>6JYmX<+}O+a1kiND->Vk1jks zf|dUD$~w59-0%#62J5}SFFXxygV>VN@0gQFaN_tlr*nS2)HJQ+#jF-fcQO*^+W|~g zyq!cVaHL*QnUSuCf+m`f`Fy0n78s~3{7$Yd6MqUsn)?}gti)yU(PbIK=aC6i!N6>< z7iO{EiP-tFI#O*8bX|pMw>l#uFy<)G+Z^wI9!0J2F(i0_BoHQ)xH3BCaY4tnZTkWK zj6q8nR(hSS4agY!P-Kmzw@ScDExm+$xv8%gJLzYD8Zd-YQ|N!wRAQ1g=>EUb4>S}Xzk0hb=W&)#`0M25+-l6DgUlBc>C+H2Ceyenz<(aN_$z_&gVR`J~$rPqO1 zcE^})k-om{1i)P5-2F2e(-McGd~n zrRxKGm6?jq&xCa=pt*ff$eeP{O_87B{o$~sp{1}E+ss;c=e$N9vfn%*J2vu(U}OGX zW_KpLG*1KJ{)M*9g^oZWYpuo?E}XzeF)si%D&B>5S-XrcHq^tFi|_t}AFjk+n_wH= z{zQojM&Kjm39O^PkUQv~;^lkDgmCY-IGYUK!%E<+5#yom6G_{`IxbB{k%lbcheULc zX?}qDLGsdr1jU2Clm}#kzXccNd-h+Hkn&YN%0VdsFhDH88j(Z^!II zRqU^thpBJV-0wH|$+|%s-D@1}CT|lN1%Lk)^^HvN8)c`ztxtQRSZyufMy=_FldvLA ztNVzeINna)rE+|&`#b?zEAVv zO|`H;LBe-h{RJCzZUg4%q3@nJ90j7QVMnT-IC-)Ex)c&i69M4d>dVhw49VCJjP3m< z2?7`#%we-<_nh}}$4F|M>lGKk{y!ME(gs^Ab3fuDcE^x z(xbI#{KPhQu8r%ZOf~Pv7$iQ5lm7LJAKY>YTlV?Z_ncpKqYInPzGu%l;EWshM+QdY z`WA+S&p&adM>b)h60J+Q8mB<}c+W-oAY_SAfe-6@ea@WWZ};_3p%r^0AI@nX@2 z)cdLR?`D>4)kgqnV)5~>pM z`i;^AzpSer{A#m~9hN`Kr#9qrJ6$n>MR-C#Ke#9&pWmrf7YejmUjoY^TB#`pr75k* z5aJSw*2pC)@k1p%@z}-zl?#P$+7+z%#-7iK#?uF<1m^0ib3^Voq-C%{#$0bo&gEDb zT!LlI4BV!Q8sGMDR8w)s^}!~n<* z+^p1%CHy(FHwTc}9!MNq1FtPGF8>;Uu`0%Tj1oD$aDFJamalaJ5QJ;ao?tE~hZtKH zpVVwWQVVSY>caW9&3?SV_pxpnF757e_96QPQ2nQqJfr=@wLSLRuK?k0mPK6w`ql=jst;s&kWcm{qpN|asQq(SPn#tnRW#P|$8*`OA^|t0)y8`kgZ)y*2z6>0H zxH5jMI`8BNV5}l(|_zB4nIi-WsK~M$d^x}=#vixZc z%?i;5HKzcl_a<$0q6fsWE=A5A0IvfDDm=I@*-;+&Os{x``3F?|xjZzf1$3ka{@pq) zpz9?kh_wa@91K~p9&)#_Iwqj^3baYFt*U73F?ayInb?)(; zE2rcg+D)nWql#!$PC3RAvdPmBkv#56!wq3tr2aG;zjvMFo0(w)fKOsmp*dxS=7mqa z<;5!K-7bg_auO_j=^r5>e|XfU{vqKenFV3!-=3J(b<%)ISSiIueSJq!b>~5K`pM!m z$Ost56s-(JOXo3Fr;X)?h)E64gkH?;?T_Cdzhe19nG*GSq;gcIc&vTAW%eRW2v^us z;FI~zkgynW7TXTMsx6T;)M44@%Intl6rRjp7F}G45{Dg^Sh|#~UDC+^WcUOgy@4s& zn#bV##0B-LhX{N*VE`gvZA$^Y;zv0#FdyHhm=NcWQ>S|!q=;d=koP~U9VOJlS*PMy zeBs~GdG7PaH{20bKQaikxr4V2v5eBZquD4f5NcspV^pIKF(}Iv^s@y}C|nle$kww& z!=IS$aAj4NfBX&PWJ-aa}j4G0lu715b6BIvBX=(5(CEko6j$3X5AbGTZSjq9A%Q zVp;iU1gzQZH{6_OqO+i~H=eNI2WMR*+_KJMmX?29M*uuSm+I)^d1UETKJn>v)W<%e zF?(zK{i-)K1zFBX;k`Z6lDmyI@6|TAl-FO&6W5Eqoh+t{>HGSev9-v`H+tmkM48%~ z!gHQ@=aC641ko9K@ zz^N_kt09WNgl;3b(%cxf>l!w?HtTgK=-A^>`1eaZ(@nXrvWPFM>)>sSQ<5)B^!Hb704jx52D%9;-qYa&p_?6 z<8`?53ml)FE6qn2!?V#xCHsUhMA^i5&4|;7qLWRXD1M-g6r0u+kvk<=a+?f0^|v#Z zoxBV!{y_%zIg=C#l}_931EiTZnpc7W*zg?S^e;+#Z2VQriCIJ~Dpm&_2cwwP=VGr3 zj-vX&bgHd+{*h?W@~Z^=vp`caoVCq_&M&BF;!cONeTf9zAPssI0q7COc)<Z5S+RRHd54`%6ZBr(DcQyR;T3c4z>xV=D^=G^;6;B z3G=x^R2)ZJ7k{?f>W&FO==I)njgkaNS18@P9gd7vYKv?;gr_a%% zF{&(NJ>R|si5Gt}X$X7A5wmKwDuAJZhjgaOP8|zYZFfrMTjSQxy6tjTtY=QtHNef2>ESj4q2Yxgcv4fqnFR#zFEs>H|o^ zamoJPqC*AyGxHql%L9-*&GF1ioDb9FGt+0FQL_Wv#+=C(ZNy?$2ON2Zj?R(m85RdR zgRS-`PkhEuzc;j6ghmkUe2iLvgUBpS9e56AKH306F0h^c`t|qh4u|) zGZ!1T#Fsg`e-63GHDwJwAG`PlcH$nyws+%EAYyA|CRI>tvmb-`V5mr}et*ge8DY`;DCJ+Q23nzNOElfPsEYui1rN=nt7~mm zDvfpbk{uOj&Pa>$D=?kc0Uc#J!FnQmpF|He#!M7#(b5A2H|q2Lkgk*1Z(ffbU|v4o zm-}xJmeWFKSyU zSe`?O;2LI{1St-eG-LQdlx<`mytQ+5+x{0XA0pEy6W$^r?~uG!734%@cJ7p>ZbudN ze=)%aml_dSe|Gb2I;Yt=FfOtI=RI5bo7i&wv|^Cj7?k6z5_B#zy6ukyqjcB6@`Mf z8~~3P&mG@et?BF~U>F-7{_HPg(cVHZ_vYfKj3^N8H6%4%(=E{gmO`5k-5%0vO?k$qtVTT}CQ&Gu%ZWt5vq<&*;JMq;Jf?+>M;VUPVJ#i>ubQ zyf6usu0W=-Imc?)Ze*9$dlMG2IB$$&m;Dm*bF-{mZiaxcj@6o+Bi6KyehnM0cuzIL z2Pf!$zU~!^2Ah(>(~cBEVgSFjD3N7f?SnB~<}-p%5IETuIdlCKMd4AL>{Q6cA z?cW7F+X0LJ{eR=y^%Mw+hi+){`6y9XG4;N70DR7;9WmfrCpvP0E=?_ctFDv%JoE-x zU!3dW_1}UFo7+8#d1emxBj3aM)YBgiiS|zo!#Y-j>)11+0ipwIm>P)h<@QfB#`zp0 zFu@3K6d9??;am(Sa}2ow6y@R{I}_XJ?bDfbT}?SzYu5g`XJhNy4yfl(R){TEEP?}| z=}tx6j{7ZyjZJ;-8d5{;Vo*&$g2qukApmy~JnVC%%^V~XQ%C}UwF9^gc==Ra5m+HY zwr00T6YLo#cpZeQw(`T+2nI2=g*WiQzTLDidIBFOmmK1QtQEPR+yk90tPk0TuwmIq zjh40dXyr7l1@d{4vOy8QV%{P+KefAON3diq7+Z->Uy ze)@y%x%V46&wCgn2nA5;O2A%(6>!PM6|oAUUP{$G2+E;vThYN1S&s}sUq4hW-n%c*p9=7$#DNzp+<-QE~e4KUnkLf zD--ZTK6!8Cjtm7Jx$y%A;y5P`8IRBV`9k1x{7U-aH{|%ulLzXYxHl`0#Q^|qpWeom zendDv!PCbs0u9oBEHBE?NaWTaxJVX=!8?2zg%xov6l;f466 ziBFkoj$Fas_}r%9=84p(vL4Ax#TEut)NH?t&q;Ujd7&iZ171v9-N!Fyo}WwXdkr93 z&V*Yy1p>r0vpFQ_KXF8&vQy&vB9JG-^>9C0duuBXTIQu~nT=|%4 zuL64^gjg7iC%Y=t^DC=vual6J_+~56V`V!8NS-X3{kkD2vDp>)1b(>GGt40r1v4F= zE`K&NhtJV|84BE<{=En5hKi5lS(0s87;6r%jd8Cx6dbrY?)%vQz5Ol29R{spz>u0i z+RGxG?mBa3k=uH_u9YmKnWbIOx#A`2lsCT|i9hsiz-^2Q_sdRzM<092-L4{&(!kt_ z>?9i6P;qJVx|D!_co^YB&E??|gCUr(uyMQ>(+7teeMs!bN0cn%5o0etN{KLPtZ;#7 z|3IjFFOMOO9s2{@%O%yF9^$W7;aQaw)^E2t{zv!1wb5S$HZfRW`yIOOF95N!{XvNY zbbQWrX{yb^#hBq7Vk^>&0(88RVBQb<#EqLDNLkxt)f9jNuU4^IU1MBeB33BE8(_=yqwvi-$gO)C%$d$W z_#{sI!Fk`ca=aWCxUx=Hn=D@XB+2`2 zzm7?Nf;Xto2!Je;HMq&(@B5f=*-nslJ5TVuPRk9ewbB5PTWWXyyDcqaroGSLREL`3 z;f3h|4FjYK3k7x?_R^~c^{(cmxCX`RWmMInF0y4+sk$xeFcK&3C+au$!S_8i zoX``cZxXqA^waG4* zM6S-RSF+r@%auyC6;T`dsj>=|J$N8iS)EBHc3pxSJTf3|6sUIW$85R4GL3%yQxBz7 z0d(d0arZL0ifp*3$uS|!(2Zh*B+HRdQZW4T#Nt9bz|!vrzEOvGQl+$%~+W*+{`ZKKg| z$*&i<6bg3T>mDU#f;n9hfCD?}%oil_vS??v|skf?I#>d5g6Y^q7 z#syx%U*KFk;cJ(?9OU~hoZGwNL4QJWTmt)8ZuZ6z$I#{vd)v-ib6A8*OVM2bD1mjc ze^?;-PC}E~UG|obec6kfL3SQ*ej;w=i!|TxQ2aK0D#QXGsb7vK%kX+4c|!Z?F6%}i zXuTU>cFt1aG&{4t|plOh?pHaBNYJ`gXd;#u1#L5#`xFwyeQNIlc}gHp>n8=n@zAOk}=% zUO7O#b0;j28b~$6u9;-8S1^pmujIl6rj7v~Du3QqEGghio7ygGJJ}0O!nahy+@xdfeEvdUCEZ~jsVa`tL(1(2O*vcQ;o@}574yd@l0qyG+EN6zm zu%erunSF6gENzoeB*Bz#@SCxf2!z6~={|omCL%~i!ud!s^@Vv?A*~z><*IFxV&8XU|!AEjm#cAeN;U%4WoinFMTAR9NDNzqLr0I(^SiYuI zzdUk^zeNK8oNI<*_A~!ug(&Yg%zuw>M0a5X(;s)yT@O&HiJ^uR>R6ZGGdb>e)T@R_ z!7=;yC;6>49*uT9j1D`X>>jsCTDwuo;EWg!?>p*E!7dIyVAB6Zg3Ciz^x5F(3z+Tk z2s%IIk?q6g*HUve#Nkg0oIY>x-UPO}Hy!MrMxCh*{?IEx%;Fww zwwb&911fo!&F0(Q?Ich?>6CuQ4dMQG=KOunk85+O?d#M1dTd^7SAGeMPPEi{*hUb(ALcY|$@%K36fEwz}- zO^(U!`p?+nvVR?T$i`1Nku5Vkxy^6X zTkD>LGJr$NFrwIDv^~{yXw~RHYk>+eH{5n-Meg<#gRD-k8zK-M>bl)$`=A%Y*uMF0 z=)-OG(Hq@)s5}|5vnu{p&y0qx%8eouUPsDrUIbQ0s%1-4{YfkONt;+7De|6VZVH6N z{l-&G+>y=8NFU50f6P@i4F2Gx&)>N7g&jAqKmKA3o(XTB)e92eElDcQ*tvx z(YN*nit_lTgvy^d{BYi%9;O-~zD(3*T8xR=D}I?NAi+I~{q~P1)b?s3byPza!A)lb z?ryl8l1~S`Lv~vF9x7Z#WSmJvETjc%BCRA`e>K&4>e`$bNuWz5iD~$h0u`j5oOcav zFKd{vk8XO1s7X5VVR4i~4n(=DuImfTm{i;@cS@XlgSUs_R z`q`ij2{8P8zz>Q#tQG8nbL+)0{8->&YY#YD2X+^>i8b#C^XnKPS}(PXsRun!)WBQD zMYijEfbIXne?Q4htN4HsV}G$N4G|Hk9qOjNaEX6!e&nlU!)vfKf9I)+g!OOG@b}_v zP%RJqCc}%+PSJ%FL{@ZD7j`P(zl#&qSDW#cNVZTr?0|msv3Wk&6p==A$$r1%sF)4r z>H0uP@Pbd9MnAj|>;|av=y(azA5yl;? z=jE(be$l!%Q!>vVb@icGm0$IRFNK$rL#c1K)mKLzF);zq=$DH1NXo0cw)kQ_k&+g zv(*o}zRX?0USi4G@vIXskdjMoX~vNTwZ~kTUbDAtbQYqo9g*1AVvrRn{=xo;0`rQ% zl2YzD-BUL@tUrH>o}b6lZhYm;0v^CT^xLhH{diH?m$bS-oi|pXLapZ7$+N<-j4_6YM|s%}-I~c^F`SoGSjhMS=F`nCM?x7Ek+J->&wLVF|PnD4b}}B0B!Y4;UkG(n9un$ArBm;M2idY=6ECS zgLVCF`Nlz3q5kMF!<%cxlo3+){ioT8f)`1#s;_s_rKM8&E01GIPIa=KV;3gVc) zO(JAQ76)G6|7|JMK^XDmCnV4c*vzJXVh%T=7UX;jIp&FW@UpK@H$IQ~{kyWSj0-Uv zu*aH&1s*8FH=t*7zQg(q&^C~^U1rsHS1CAQi+_CoLG@UY+nEj$+V7AU#>p;GWhLNu zdJXN0XwV#vvtyOq6&p~&N7HX#L80OVrs_|1W*wd)7>tcWrqddvZ?y zB|HAqdm*28WOeW9_HuLm#c9)qk7c>(HlMXc`Y}#i_{;~x#X{u-djB8)Tnxalx$`FI z&rp;+7ypl^Q0zWE>u6XwA@H0bH@01W>+uKMsQ=ea|8wD>zG6-V9ZHt(A-9i@Ny1zl z%Xc8>1P`9z&cVlSJdITRK6@a^Gx`zPg?=yGzXNa%>GwPe+gFm5&cveI6~%T#2Nt$r zFr4Z2+FO(XN@?R7ceJUEH$S9YP+Z`*a4mbIT8{Mkj-b37ANdm<#(X|}yW|H+gGfb` z^tUH`cKYGha%YT=0ooMfOKV7t8w=R;o)`@HJw&+j50&5Oz?w_ab`p0H4g(tNInuqV z^S!}@7cpA9$PRf&AGZR}%mykM5J;tQ?Av9)|NEuM(ZN^~V=(mU#!0po`y;dS>HfE! zg_hO+I1m9($;E#6dOz&DK{g~XMif(YEEeLbpx6XsetK}Vp9ij*e}8gL>5s38&iPa! z7~ceI64isC^=PS4LM`ICVp*x#B|ilgi7NFau3Nu=(YOG4M=$x#7JxHQxv^cx zMz^tG6{Ldv;$^BD;m~tIeQ;h6>QD6H+O#>DPP%*nL&{pR|6=e`GgVSL<*jl(WcCOD zrHxAm`zy=0@mPpeyJ3=1w{Ssok<~w(0=bnqcfw#BSOF4lW5mI)lgvccoo~buA!9q( z-RktY0ARVV@T6_E{V3kEPe>n}%-{3r3)miAoQub_3itYzXZAzOd~AI&$wb!Rfz&RL z{Y3QcFwTq@+chO64Cl_?$zV2&B_pmcs1G$Xm4_UMS<2sASu&h=l-S;rc^``P90F!X zADZWI29vS1Im~45>RgMA+T_J&!1Bum#0Mi}-510_m0^o?-}&yAcc<&XyEts6UIWRqQU zv1@@JtBk?ub083h-g43R3-FN9`}9LWi4LZ+IKg_O;-`>L;tYrM&&0dDubK`S-5+w9 zQjLRd%aEbdKC=A~%&)#1$8lrA4#cx&&KXy^|Gs9|JLz}W@0Z>7JI0NVp>e1EPVj^u zFW=_shUE6r2K8TQV`}3zH^(Xmm8wu$F1_QjDiF@T6-GQQ>5U^lpNjHbkWg_~B$5VQ z0>3?VA4P*Bg=^qN>E`y1<3_(4(^599UJtl&_~W-6|G-_nBn&&$bpo#5XjqQuiEij9 z?v$yH66}Od$9?;|(_;bAA7<+mM?Q7v3g$>=bj8Oz@|;D@Bx5upl;~rUdN>aIH&{@R zCc49RdE+XZXLUf3=mV;|Awi6PH1v4C+9J5)0Yom26~w3_=u`}s3SnUcNSWYT0YuY} zWo8$Oj=JF1!7qQ`p4&G;+<9Vm)RBW2zT8G6zr+8;mbZicT)j09E705Id0A>LpfKOk zm2((pkUx)trUg^s{miW#&l|ZiO~YImGu(mwPYK+ugNRPyB+|s&)_*_71JEy}t&D)D z6g22;52C2v1_!4nJ0VpVc?Yr8i9i>+E2z_cSR0quI?6Ll!n4Q~uH9jOVYg;S3eV|Z zUWQdPIB%e)Z}Gu!u|0YoC~EXH8mADkUBZO#1F&=fuyQf)wqBVFcDR07MvkrVYUO|& zs5F^KciZm5ZCcb9&$g#qr?}UfK(@Xgm}9B%h7Sm>xA>GW$#oZx(gXls${v^(-$#dz z8;zYCX*n&X`~yY|yr2QnmS4-S&0ecr{VQDi8g+5Ek6;do`IXav(z4m3cx9wD;&C9! zG~fJ23f4g-iOAlvEsgls#wU(RMr2tQV)U$P|doTlYKDSFd z)B;`(whRp0$jga{neta225U@riGOuaO>YGq{^KeXV~-yZlww#aeG*}b;9(w-f*=(A zfOd{~c;Jh!iDT#SY%9S5hWrEtyvd9|7{oVo`&j=-*~u3Cdotp+I7*RLpEm-DEF+$?jtoP}@I%yW70MQOq#-GATjvZ)a(S-|n9bA#$0^N3N_RBqaSN<;%`HpL zHnM^4T(ZgONR(>Z(s=D-WOw#KhmI<=pZR-)HV82#ALb{9HDFM+0jf7<<7$fKj@Y+&$tS3lY~?|XztZrh z1{}6WIPGGa+;M9Xur1)9L!%nc_e?eOGAx7`qVxG@6?8G5MU<)epHcmL>hszXE1T1~ z_q}&({x#MI7Wwv>rV>22x0ZTyMjwxC|329@mmkewk5nT0LjQ7(nb-O0>8I+y+I;`Z z?TNut{ATMw-rjQQleJO^_%;QqiFH!|5?OIBV26&838Fu!Z%;nHR3Uax9@{;&%-qig zPv{4f+G#qr80T?+hAIT%J)D)atf|%bHG?U_pI61w=G~kn`w09GXv~vsI!g9p4-h_} zJ8q;B9vA+@Xa}69^&n0)CM+9Ep@37S5mmVXK5imYt8ub*5>Rr})|qCTG2}q#+$rdg zn!D{qCMZWsZ*gtQm|s_4x*dU4;_>STQz-7TV_LX6jhg;c=RhWKg1ArzjHm6Jt-Y+x zVV7GOvaBeYka_v;_NxPRk)O-yBdD9FxYzR$@^8yAUbvSue|%wcfX+$^NSKRDD?_t& z;)CxaR+bYlNqz5HGnHVMC`x20Ec?z7*atsAy#7Tv?uJNM8psHvkr;>Z=^%VPgGH0x zyz@~JTHbe06Y~ul7Ud7>5#lA521;%U?<&&gms3Y!ALbQzf>eQ-<k?yCe{FXTyz{X+`OXTu^V5h|R6F+i4eI(03C^&#U6{RI7gEqP0@Dg!y z(2yl&TrKFl^2JGNIjzqjzRw-JQ5r`<@GGL>t0 zNp*Vi`p3LsVr0*h@GUal75_q;a5mJ#axCQ?uZ^)xad9@hsCd*L2`&Ab!H8QqN5rw`w`!jGbmLz2Bzhl zOg)y`rT9RkA*&jV(OQqv^SF%_xA_v!OIJM9+dv;HJ%)eltAvT39_Kyn5ejegPR zxj7WKxhUh7OmhL&R$+@=bGg?j?i&dp6WUp&!~OE-|D;TtbA5g>tz`mvQ zV+agRXO@7hwRcQyv%g|kCW{gM%)xINn+h{JJ1HO&eq5!+I2yt>*z9>mIjDPb5aWmc zeXxLTs9r;a@@Y?pWw*Z&vzV(E^F02H*0C_x+-B8D@56C=nlN}_i%2uYK3|q$qF8~2P zxI}~?Avv_45zudj?YZFLi@Y`VNLF% zWp-{req#<&$uRw=MNSyyhSONX^bIL=nni&5+#}>(I62GiQNshQ(cIQGo`e2Up&9-r z1kWaHRm1nZa`s*Ht`Q20f}fnm_35%O|qV#ftliVHIoFyLC}XIu~n+y{Ov^6T0ZC5_DOtHTF7H5RZ~_ND>y^OITeIYaes3jJAx*lQDDE zAJDeh?SX4ZpLHpZK}d+;TtxD9S5pL71ud3CNpD^r#4poOexuNOl^l3kMvClV-L?dH zok6GYRon9kSHJ2vRoNIrOU%BZ%Bh)#EkhL!B*TqBM20~(!g)eNl0)T{u?C11MGZ_6ql%$u$Y)W2xhpH&W~fOD zRHC{Abo+CoPyKCBohWLeza4(_-Ky-}DG@*TlkCj}w>wlI-d8r~%&p)yUTc&;4EzEC z;sj#VZ$AQz6dlml15Ce}Me}8k29G2u5RI0kjciE4NNwbVQ8VpsTA|O44@f#Z0xm$A z%sEQ(SQTliNdal{xG@fV9D2=sTF3n+fPM~W_eCQ9tFStGaEu-cLS-rbe3 zy>rhBP$s4hXAc=}&ssu>p@t*Nxt(q>OEa`Q8pWu&XHNkhUGGH*=1(myZ8%hC*Eokv zRx1f1hQ=$MM=X|r?5bnIwtx05-=8Xa5Ju*B+@RN=8Jz*`hH&nfNA>=vfMX017@TX! z#Zvr<9Vv2a+~RBWZE{p9gF8N<_pmA#W>y>Nlu2eI9prAd*GZoI@8ZfCPMC<;cvkc4 zn2yzZn=^5k5fVC&lYJXE_3{m7!g<=hNCfPK`8+RoFLwev?$GowgUU0V1x1MEDV|%y z#Zq>M3D(URj6!(QT$7E%gw5YHGaHm(j4Kv|WZ+&687Z7XNr_EkFXRhpmRbXbJ2h(* z^};O+m6uG5H&3mPmQ+hDZC@#}{}Uy)9y=rLhrd+DTU>xH<19)UimJEveKCNGL%>Mg z$G>;zR`zz>))tm6?$ewr;xlHMLm`?)DEX>q4Xw5lc9An6#F3HKjfGlnE&Vco`|U*8 z>f)iLxJfib8+uV62TsKK$3|B6F{Sw(H5kC`uF7t}I(&}?H9*eKzUDse^w*bB#hV~>z=h_^%5j^K!(r31g_pR9R?5NWyepjx>?IkUp!CE&7XE0c@mCy~eUtCkl5Rlo`Uo zqcx+;4|S6FaO$`$e(LFWQmQ{K-VJ~h#z5Ec#(745iR3=35u=3c!qD)IvuG1tU~<@i z_w=H4=EpnPc3wU@yH4x}0luz|2+sIxndx1aIi?nMD1ML`+TA8O=Tutb{`&9$A(Nay~FhE~8g{>wJ{oTrj0#Ko^x z|I&*U;P}E<+H?Tn2xNq3+#Cp9xZ4k6{=G|bI8=4@@gBz)tx5j-^wqg@+NHO)HhIIz zp7s^+&?`(IBqzH}L%^0{wco5QZKbFElAN}_VNz@SSFSzIkEaLhYc=~yXxff4gS&-|BOBW z#=d86iAMw%nFwf?h{)uOWqXVI$7VfEcC6vxX09W2o_G08f;CY@vlRd+nUUT&`{JuZ z^?FHz>N}^WlNb-_G4GTV&@2wY@#~i>g^Q2e+th;>e4VGn5aMmp71)oydbw9Q&W)qV zGY*y3nP_m>95w)mi|u~#xd`;a>5}w}W}`J!K~{L@ScL-ww7t~nflr4u{7lleg=B~a zL0jnAi%#s*%a`r$&pzyxVOmE@y;o-I(Oz7Vonx!u-l&ckqU6>@Q7&07|L{V5FHcz` zQ7rtjUYAxSyw0`!Q_p~`uW~~LBxKK>UjTz3#5ZY-#3ufD9+Z1%gh*(!P$gBV01z68+%FIkM>04*)EO7A&T*{1? znaSFi8OmZ-sL0MctYXxzJyDw)`)1-}6fNUz$j3>{(P1JYKeAIw7fyvO*zlY2ByPEm zk7{G_uIe|pj5O)e5P1#Ws3yWCgdVA=5uNK5ay(MjuSUK$8$FStPSl|O{o(gkaHy+ zx5ixNQO-mQVT~wl72v-Og~2f5_^X(?N{RPpTUoZCx>Zi{X>h_2opooJSEXeGqeed| zcuBRT8=7IwS+%%4e(0rznYpX$Er{sNVi=4()_a6#c0H-=gsgVZN3dzvwat^gre-mK zbI9Hfbp2*~3Q=au=^?iweJY>$eXby`eBD7x=O#SK7s7{HjC7XUr8G{~he5KUI}_yE zqLu7bh6S4-<@(OcVSQmlI*74GI7WoO#g+=51c*h42a#kNS~hsAS>yL7V2BAcnDano zbUM0B#}ZcsY-)YXHR#^WW^qLN#w|zbfIocP_6727o%hz}ya0lRTY8cATLC_5M`U__ znvmjq0KSA1bjk})v8@umaVMa8>J~efKsg`oj%4ilOLdn-fPwa!z)^rpXu0J+TNVq! zFWW$Q(Io-0%Iqh122k(@7TKA%q(J+I+siPjMVP!c)(CCO`LVjWjo4PFeb|FkJm_&Y zyF;yfZ1P(;CUoY3VfoVf`w-IT!;%iq9{2NC3U9^Mhzr}D{xV@@Idy;jg^x6YHu2># zEN*D`n=qw-`8Z*ni{k=W>xdQtr?(1YviIZB;Zl3`)?ubuCeBL$-ekZ(srD}WJ@cx~ zet$6p6vsGsQkmon6qE6*`aA_$w@v zk4r;0gfam?%OMY2zWQEjer;YaqttbMYodz94AnJIUVH6T3nn*kH5=$Ub!hkLglNQ> zokd2VR=B)8IbM6LuVq!jdIV(6Cgxt-N7$w|8@Hj zP>s>HdF{1Vu-JkQ4^mcO;~-bfM|>JXFz~eHwc>N&U2j`2l?tLlfg56LkIowM06{>$ zzxqs|)xApwJxI$AIRx-2w-)c>wa$3J{Zh7$95}&r=E{d2A&g})J?hO+CXgc*Vrgk}| zmUms7W3)Jktqodw%EI+k=-MecY5(Td2<%r**^cL%%bz^%y?WKW=^o{C$BXVHqeXqr zYQ$nmemgF$xYTLM;#1RO;@Wi?TG7>INs0Cv{M`1SvGLcE5SN}I)TNAm?c0c+i0Xr9 zA2I{v(qZfjG7QHl&$-G;NV`MO8Z0VCRuA)7<(#X}6*gPCmvH1ryvJ5zg@0D6wQ{|` zx#GsI@P;h~hZwHjb(;DbsUscX@a9p`lhfyD6rBa5YkOrtU`4I-zp`k5=>UGAtCBXB z+9gS09@hr?e!|jDA!tO*;M~3uwk`ZOLZRy=C0E4kPwu*#HEwm3e5`;lW|y0 zyfV5LLf;Cb+q>hc(Q zuQLttbARSZGwX-Ux;-0nLb>IxMtx^D6SJeTBkscKFWS%H)*$&>Q7i4Gr04>8kjsyq z=VRWv8JXZ4<%Vn)6CCc2(c%uC@Z`iwVgtK+5=sNm$dt#EHWQHvp>14rVW?CVf<+oP z?Aow0D_gbh%dxYKM=luwgpXSqGZt+E*$^wMhmhmV<1yM&$51TyA4`oBo=HdRPdH5>A1CO#Q~Va60@kQd#fm#C*R-P4<|4H*Wq|emIW`3PVseqjOJmZSt-gc0>4b@fbmnQEYoscmf+P4?nSH0q#@0krDG)4 z{PK^Tj!BqMLReZ%dt)151?F*Q)h>Md^_2bm*6wa>r)DLk9nkz|r~LG1Ls#xDeyVle zH0LNS?cMrLY`z|hN7PQMhKWsB@zl^#$JLK$2I0HdFP*v0@r#p_uyIsdv1V42h<#)_ zZ^8do*L9HQ6t1;r{9LuBlW(#I;>|@fu3Hae52oB-R!-8Hjl#NqX=-NjF5T1ggNTn8 zLBs_Co|HEnve>0C@*s_<_1dSfHpw78hz&QJ0x%bxTSw@+NE>smSB9Sc`f$y{bN=R+ zu5+Lw-d-w2y!}Q|#G9s3jJ5LMwGn*wDCVd(xOX|ZIhsfFVfiTGrI%7)i^yly2@ZcH zHMA7|64XAN9>{cQguoK8GDeRu+}dETDu_HM`8Yo$<2tEfNR;`?4qB!A&6tcW3(*`S z@t*`$i#qcv#Ik34Rxa3)c51Jh?9gX!omCj~lPMa;N`{UX*+X+ff>wmh4Yvf^MVVK* zaH`iTOZ(%l>Gt?GX)XvpyFI zlyNblxZ*nS33kJX-y-exzIli=W!(iwWpM1>=l8z73*xzCky)+M#=5eI{C}nT@SQc% zY6C1axX#%MOPnoU+pM{$S$3s*L?|hz>`NI&^8GdA)>^KtPkj6THvFlV;7bNq<_6x6D*x9~k(IRPg8bt=Y?Zoy&Ekg|g)N(PyL_(TeZXOW>&CelOc z2Kl>X4Sv9aKvk3G*8|H6kDa5WM&Ve1wi}h+wKr6mCe-;*#TxVXKz$UvbsnaIhW1z6 zei($)`corat^8s1Fmy1mR!YVbkGLY$5LOApla$k+qjz*46P-KjV>AIW{iT>M?AIa` zW0fPIJBU3fh8!=4P3oW!-(GqzpjO&XbtAUpVoAqk`zM1k-g^JjB-rcVuUXM_w~ zh9**0Ys5&FDNL+-=<5c81CeH(^zYo#nqnvwX$Gw#{AT?$aUsR|l0bDXi9BB#F_K}5 zns4~Gu~KY>&zUOWi(w?;sHT_qaJ)+=|DWH7suc)u|1TNrKka89(1)uN=syL0nScKqzS3%u z4adIu4MjA$OUWPd@x#1%*LxqC?pg!-YFtM$5Kkles;}u_oi&TO%%GgR5?;>7gmQes zkDeh*Da{qY3rla?CU9Y6(mJx9avvLXz`iyQBR`18?%|*^Q!}!xNL{V}H!0u>eNE78 zv&`M!o9o;3`9Hwkd%^mP3+H_@!>34j1@!B;?9uTQBU-9$af!)tr*xUsq!8%L)?LX6 zN1!>}jMl`yW9#Z^Pg&(mm>>_*6uf|I+44C2R7xPC%yA!2z|qK?Uy#jT<=iTK61Utw`2qF+ zJI{&<(0LPt?nSg&_@MO7nTTZAcX&bYScJ8T7HhOWNGOL!{RW#9q3}ud&z-C&Rxl$F zu`h5b2YXT?3@!u_>JxtD9ZI0d)wjvEIBejda{EDJ{2g>)Epjap-nHieRkI7`@^^!K zhh)@)T*c;nw)l4gtUN6YKcDFF!le8R+fw_4D_3EuTc{4*`GU89wroop3mqAwQ=lyl z43V{1w!Rru3i(Y;q!IN1El;M) zk^NOjBA~eh_MkQNkP&X*>ziH&@Ci+6m!}BEcTVc&O4IJ-XtV3^_rKW@d2U~{Cx@P# zp3sJb-gA*;fb1~AG{sAs{aZ&=!UdlzV_@e5YGpVcFHtv1R{r8-0)x`>`|#%NY8*F&*UTS;ut_f3!M(GY1sTj@o;op5XO1C?$t9?U zn_)&TyaoElxiRFmJ&oXeMNziK(2 z%R_cz;5AJF%i~H$A=mDwp{|)pjKfDw(sVG#<>Y~CY6D=mPKK4bn)!$u6PIEk-ev@D z31~ho%Mc$(d~Y(NDNted*-nM_PKurMi!^wv$+<9IuXg=`QtFG6HsGALk<@5$%ppt6 zIo7}g=*dm}VQ6aQFWS@W7lC?!d^Mg!F27KSwP}sI3&a#5#1rfgy;0&p51xfJW%`jm ztgQ6M1~oxX4pgg^dL+$nmWwC;rN-39^P8Jslqt&a+sLIcDmI_81|=*R%+pSnF1GT^ zp#|VfaVsh(oZvNi@|zd(<~v;_6jz0xGH(eR1SjX7N86sa!;XNaM*FK<#=qo1e*_y* zrl1|H;%IiqaL2afaIohSFXwx6TlOqey3%dRaGU#ty@M6}iM1yGXzLg00Kk%lStrdS zRWm?+FefrNwUJtxG##B(qbS*AhvlCa2kdMzIODo0IS;4yw_e8>3MmJzM#jviPN31} zVBT`Hc0byWLPq3ap9g+C=;a1wE+Uk_9bk&+Heo+@Xm!IPM$;<8ha2HpZlA>&NN#=| zbpefWx76iVLjYP!mvlybH}K}ScP?`)Y~^jbgY5!*06^u(WX6dbqqrf<9Wyw*Eq74U zL-Ivan{#oK?KWH7F^eg+i?ZqvyEm0D15vR*n~<=Z(NcRiO#Q;6lfG7Ee&lz6s`T5P zu2?LJu)aK74VH(@*_hd)PznDh>#7!+K7FkXvTA+Z3qp#*)pv&dM9n`NEu|N?U0PGY z1@3D$^TEI#L`-@!qs0{|+B05K@!LFFQy~Q!w{IGK75cytub37s8mCUs^FaB3?xPmw&q7&5+gpl|WM-5-ZY|R=`LuD=ODR zT3MeFzX5&RcHT|eN7Q=KX_aDU91i|O)FxsnMrqxmQv}ozw{WpfdCi>e_Ji2n)mp=z zI~x*&tvj*=5sr(A`Z@6IX1f&#=VaauD9OxG%HLJW&)p7L>)Jd)yjzCyA>ZzB|0N|; zfuZ1v9rMBfX=NK^S8$z`LtjIhfI?K;v(t;ypPKn54=mG zzt|ufqs@m2g*mv+THn7LXpgMNqevk5jIqdr<+*LlY_c&(`7epcP#dpJ88&9ecRD(1NzHl^_y0K$VnC+ZpX?R2r8HyKvntVonX z|4EEyRD@BZ4-)uLaXNRcsi+D##M1NuX{b2@z_M9C*p9k?dVp`-kmR-fCJmtscw~45 z-+c3CxN+TRg5h|Nx7XNwZb35=OvQNggqH|`*sd0m^ERhJ39v$5-n|JNB!Yn_r(_oN z{d5GSyl9mBYW}&M&Y-i!Hk{fI%GVUQcljO7Yq;0fnF>|1Tbb$lyt&a7Jpg|YH(+is ziQc0wLivS#L$9fy1`0e7{n>f6o@N^6WnIjE!dRoQHINJ@YNr+xoAbsAG`6Y}X>piu zv8`@Mt;Ke8%NX&6og-6>~C z4JA%~BirJ(5b@EjQZ)N$@e(?Bc6qZ2%n0y&8Z8W$O#IwJyYL2{(?;1P_cfo}SqkzN z{z~mtGRdj63g+%nr%E zH$kx7hU1skjulYB$`;nByL0%EdfeN;VjL|FsUbNPnf+v;) z+^si;!&Kf0Z61C@4rt{czdD()`)IyIEB8o=!e$sZszmcB!cl8h?hV)*ryRK12Ap!m zsyA#VZ&OC6ZWb$$SEyxKd7TpOX((+(+kRtnijEyGybo1u#GX$b0w}P}K8jVYg@3?I zcf`}D370sa4|?d@tts9k&AT)dw_S;QfQ6l5L1}M+JWn4JPj%qxx8A<8`aMNNG9 z0)orCW6fiP;B>~QYJ0NPW*Ir=OOIudeNAVE{Nwsak$AZJBFXS|EfNZO(%dtZJSgm? zR(uYong?8)%X<1woR%DUi;uCR#-nTDqz;`T1#Rgek9e#d$QSb9}SkTYxn9;$C2LyM^;LmR5`xNLm}5!Fm3{2?~@qPk()N*i+|FsBzGq zq&#d7hUd!hyHVPdnf6b3bwIb^H(WZ*QRIC7u#kkFZ~lEZGC}V69jaJGIZ%bVfjUVT z>;aioTJUrkD;%W|w5AO7PmPVV%GYp@&wUYN|Z5?NZfwfAjO;Z{>= zY`n~5kybGS2KYQMKYCffbz)M|NtD!03m>u^uNcbcVk{$pIinpkl6s%r7>g(lYktz6 zW+h?K`HO#nwbn%SFkldG7-%r0 zbpD`96|_}K-=w)<(3VAg{Ig_1Gq1oNu-L4g-h9ne8zO?-*^ekCF%M>COc*~u?MJ3> zt^)MdP2Z}@3P_vQHXM$7W&V3YLf>!1Vg-PmHOR=_^mPtCNa-Tp7zk7vp>hF@XRiJ7 zcIoG>Oqk*K({T1^-L{Ekb<0*iwVl%r5rSulq}>cTA5FfTs%19eSlz;U|L{o;&COj} z9yBbMY;?x*7=0YbaI;AB)oUIkU@f2eY6J&ZmM&Q{&f%dT7U0}A&va(l3qI|gc}B(T2;6qIvitXuzP zUnnKPI(D@fYg88jJ4Ck|Q}EY=-zm{ognz2CU4)b{Ux`xfIP<1EbXJ;jp0qe#!rqai z5BN8Qat2(|2GCMxQFr&;gcJ;0kM}7GAMu1k2{%-fKYoW)&SMf^9%An4p;#NWR?jQX z_G_G0_l##HFiWHbqmR)V_9zBZ#GRIDmRz(phe`ftPHt}mQYaSmXa@0&rp z3Jvgd0!#q&V}T34sOj7A=F$#ATjF>{`Cf>$w&Rc5r33gk4-DJovCFK?UdpMr&Ys8 zM*}K1{$|N&r37X$=mr%o9mo`fvIY_n|0;LW9?TX?Jbc5eT9!}IUM1tK0$R1wa&@=ORUkx_ie(H)+~mBaDyKf!y!QV#|g=am{lyOD)%&_I74SD z5G;=PMA4g-cQ-?hT*=8Maa>GljxBK8Jpd8%CSbHaV6>`d9NSi=vzdPn@pYPlIVe|u zT5ts}%G(MXB>06UK9YzPA;b+uxy3agwYbA)@-o{Si8vW zbr)dVz+&cx36`Jmp8ebgd+f`*j^w0yOflO6_B$Bl(RUgRKJ3o9Kw5y&(cYh_^?ac| z3zs@*Xo%G$s^rJbfEK3~rHQ%|DQpUd9My+2q=ZegBaZUDq@unk&E)PdV&QNcj$3r( zFvCnw;s$U~8NcfnV~J3|0flSy9K7Xf3)S!SuuxJyMH8R%4j}9YnyPLHyMuVL+M`LI zt4rXj(?;tz+4c&pfH#_qfU|ifGhC9o=gybh-Sij3dq6P~V`G(eoF~yQhZeYx=kf?; zY5wl|CF21ShWw6N)mKttJRx3QiPd~{R4_Hwd?kFnNSBNMI?L%h_MG+}lVO@e@qw8Q zw>rD$?DvYw3d`L{er4FPY3Xq1z2O$*=x#Ss>0zVnmQpm{5SM4gI~SA>^}O-SBjp4X zTzSYMj`lKa^i)+PNaHW%qQ>fXnWps2ZNB4(<4SoJSB0Ec<>dD-$6 zHQmVeeEt%fLV$nbF`+u@)ZuL5HcXa^$J8+NYV~-g3U&hrb=D%YzE0c7TEUl3&&z_h zDI}9w#NQ4}xxO~y5It&dY%r@2W>KYnCU)H7k=UlYE(S`vj3z`pCKNbUH>0i7Ocno$m*QhmZGigvk(e5&oDnJ< zbd)38^7MZtv&>xI^O_mUa!0@YR*TG2Kfj{Agaom&sA;0*P@{RYy=XIpmztjIdI^f0 zN)4Ccl=+bcJD@jllCn~VQG}WaQsIgA*GTsJjfXf-UwS$6ZuYcS2r-n4PhT(1@)3nC zl7_q6F#d_Kmp9GX6D=ZqhNoC*i_~Yy?DuB5fOWj|{tij+Ul-}UD%D$>pP+ynFtn8M zSoWj5aI}F23NzUTpXwirQVZL2!5rCJL#%Gwc%Xv;i_DUw`T7g3=mY*wNeTwPKXP0?FOjdGZ9?US)k3f9Ft>`J z3F;oxeauA>46=CY{{jAo4C(4w$gG`f+Ov1#`UlMYxytkGlL9q&g4sC&R zoGHjb+FdRGP)gfTrgGOhhv`yEqsLV{Um8XkmGf|MZ&qnjvf? z)iMOd$JT56SBwJt5BD7YmV1t5a_>7XxI){4D9W2FY6C|A`N^LW=5DO4!myrxO_agJ zE#~(F_>JidJNKPMF1PEk&2GOOPg0h5Y_)n3^d{^+k;h1wUl?IRUba$*eKC0KD&j1S z)r+AERxSUZC_J2U$QZLK`7tn)8Fa3P3Jh7EG;?|SuF_FBmgHL&u4QF+B_((tHjbwj z)qS_*V8eOU&mVAmF4vS`1QA9#p2IUtI_jFW;@u;YBE%Dx*q&VH^kpwbox27+``?XE zQvI6NX>;f{OsV+Oe1pRm0}MS-o~|m3pL|O!FMIdP-HXcH{b+^_zNtI5Zjz%9S#63& zqNNtMpq=RunO8+%KJ(@#p9p;RpN)^UoAnz$6IXd)><)S`c=f5s&GUu0u| z|A&z1-`o=YbCYC3nT@ukL>I%?YTaIky4?pHTD{@>pw@6d=ug~LtNrA4NYpc*oaT=D z*i(E)vL^4ENxC!_xtRHXy}z*Jo=0yM3kk2x&sF?DhgDK=qhOY$y2Fw9?ezPaEr~}X zX^N@l?{f`^X`Xkf{0teW!$k+R#$uK$!(&V!)gGrgmc>Wg#h>!xf%2*K$HQjPNY17G z6+E80Dnr%RU}ntp@+>aG#=)3)es1pAAG{g;U4X{{DN9mYnR=KDS$?fXXiK+K6EWMD z`MAZSPxLmM{4KGKZBKijJfaQ0+7QxLD~ofQPk2{JaO=eHaXM57FVrIbmN-nSp8M=T zk?sXZ@Z|yxHhh?#2yNUC6#Fa;-0;$amm!C=U3_lo+S#FnyZ6c~cJZU2vf|LPN9lue8VwwVW0pY{ zj^+~H#BC(d)L0sp0Sa;siL~)iHZbV2p@Ml4669r@BwHqhdejw$YF?O?nGLD2HX18# zHJ8_o_cBUVvs2o*m#El11>n6Cla~7m%}{+FrYSnw(`dSmm3R>5n0hvnhgflQJKb~B z6xP7#rdy6EkY?FRdEYz@u`Pm+X3=|<6#2S1W)b`fBUO0;(WtvRA&-V>%K&8$N0RKb zi9|Vx$@pkM7FWP@d}3Xbu&UL895uM?rKRX2n&?LNo1L9J#uz1v+M~Q5(Tw{0zx#&( zd~Y29z-EF9qCbprv?I%#2j1&zQ|TY#R^4JGm+3>YXNW+IeUr~F z(gSldz#Ur>j|QZ*K&%gS*uq-M0D^^e^q*oX3X=$^sviMGFmUmJae7F~DVem#w_z0RECca@%XQ2k=nyEnntL|HVjQb9gHFMm2%hSo*Jkv47 zWJQh4#&87Wm3Lb=iJgjQm)VKBL@GS>uQzg3CdziLpn`hUzRX&(p5#p~Nauz&a?I-RQOYMsyD9l*CqI zy8IH0&$OT0dd?@FI)4Y1Y)34mxZ9H3J7+j?K`uY(&l0AHKfN?hyYrAVc21^w5cE(+ z(i~(}+K&#<&etR^mA0O5p9Dj3YD27;BkXCgaDWdQ2NDZs(R3`ILekMdST#EnC~e7V za0e&+EbiAIl^Qm~TCL^6S37^IbFK-UOE>~yd*mUSTUEFD4~M9^XAaVINQqHGb<*L0 zr3m%Bw9>15d=`lc8cT(BsuzyaIfn=A*_X~ajbBzb03m_5IY_8L)|W@xQ2C3s`DPzh z5)dSvl|L*YmKiQkht<`7#-YHRj!gJj@)h(43ZbS0O_y+L?5V(O_8yUMvNx#^VDZQXEY7E2``bO~#^p`=6qJ{_=bR%m;Q_dV z);pt^ZY~JvOb&z$Y&>=convc{`j@O11xlgGCHX8))|J+y7WuD@)gwF8Gw%$TPX6^j z*wgc}L^NUQQ@>$# zPaGL9jKQ z*SC1CHhLOlMiqNsToEak)Ed8>(Aj2YZQV?ZudwrH z{3|;te4)~3<+9+9lW|7|1}_bAfa_FWWkw8UVCa05_1DiOT(V|=LPG;JY~85O1BjQ4 z>=Jb|cUCi++~tw{o;{un`n7}jnIdUkYgO`%vhO8O5W}d_L;cD8UQZDGs2Lm2LYVk| zpdjo#n14C(hJlGS5p8b(FD;L`=@q$=Il20wVve)Cd6p6SK|Reb8XR&8qI8g@aX6`u*l>(bv*|g z%`;WjlNZB-qT7Eln`*d=HO2|?m>HXFw&ui*dU4CD+3laf1_*U)z0f86!GAc8xhidmK7~k*P zL|-M><*s(QGIOWH=m2H)Y3-Zrj)+BE2r+IAhrR(1%j->c%$GHfvfc+qxS)Zs@|ouW zTlw#S*=v0YRQxpw)hDTmk-Q9a<6dl>(}3V<6=SBSCY@gMBK7MKN0eWU6yC)m8tn>D zVMN&_Ilgj#R0WjN48xK)x+DU&4rR60*Uy^rNvs^j)frxzLKQ%XdB(}9TjtovI;TYw zw_49`jhcc88bW>PpIrfVUNVmMc#!8P=2eb-uLY;$jv>ZfhcMCLJn=pL$|`lp)W6@7 z2{O2ETYWQm>@b5ed9H*1Ys)3}PVw4kEGPQmk()f-d#HBWjKB8wlG9+LHiIhWJ zLW-|GJ)O_Ph<2(5nUMciAtY=6qNCRYp0DAMy4;9~qLp(bZUQCDD$ILYtj7dkjRXXo z88w9moNH@`R36*R+ocaP+4ox?E!J)wpoo0mcB=S^m}kQ!cGf9Jxn*&u4)II5N=*^L z6<{IOG`5AMz%xDTJXfUSw+{SEqs?nQguGnAGB=)DL`u6AdgMlL_;* zkxc$m8&Yqb^4oS`x&Lwf871Bc<#$8RE{^TE5&WZIeVJH#UV~vpVbm#BaI> zqDFDG=Fu-tCPoES|H%5-s*-&cep{xV)&3;u z*^isvtN{KbV+1AJq+vxzU_&I@wvs zJFZQ1F{w+~T`ebMMtS@_>zLz3KY&G?fb`eMXNJ(|r#w>3N~p)qRzcfW>w#>{78p#U zR>66@7$kY9KDuM>qvx7DoRae^pX#e1oo6cioe$b4QU}=Aa7H!aa}Hh`qK=B%=I)gZ z>-ePs5svnP@9phq+qDDVu6!Q7<3w-e19nM9!%vqwe^50)VwM}n@-0*HmNWN2Ei;!i zTV;iO3n;Jz*Da@#VL*>ukp4Ft!xf9?266?TJ1gmz?ra~byZ?^O!xVFM<;Pjwm%FyS zzp1b2;cJjR#=C#W-)IU;lGh%K;+mXqWgw~a>FRU>npU}}?82QMa=cC`g6(BAO#2gS~e#aZj{Jvdu&ppXt#?bpGeq4kpk+%T@pleU=Ba zo2E$giG_R~{F3e^X@M)l#Bn)NjnPfC^xWdoLQ1cp`Y|1bZQNz5WUQj~?fHhNGrz!@ z1&0MnK#j-UafG7%>D9ysDAT!>fU9qUGIYq@z%VzbF4w$BmNs=c;zZPSLE64M>n96| zbo@2YvF{^UF~F18j{+3O8Cl64OY%<9#^n#Rv_22vV<*}^_0q+IS*?ZN%Q-3=N@r4e zNZp*ML?a5x>mUU3ZcMV6r$6y3KHTH{vz@EYPE%XP*2b+fuD!AFUBfqy%(V-_^s&k)gjYx|9f<0;W0mtVA{&Q?eQ2^4A><$$hL@>;R%q3(3U93pKa3#DhcPd2? zl~sIkQLQRH<}=`BG5QzQ%9=4z!|Pu%fe(3i@I>@3Ky+FM-(jBKzj|&C|0(*!l>jP# zm5$sE!Mu-rU4c-?q6XwIwCR1IZNBt6FYA{aWP8IZ5wATohrw=f#^*hTx(SIm?e-&1imkenKiobT4p8+` zihaY9BriKL7r-lsK`(vRwVINMJKvHD7?;{iq!V zphe~S)&cm%ZLGJC>F78mJ1==s=xZJ3C*X_U8dp0)v9uSWHPqjHQdmieRv#VAH(izZ z7JN63N*6f)0~`5e2qt}rp}gICa+!Fw^v$M`zqqpxtBX>PcI`CoZhJF*waHEo(-}?7 zy`5cs#yKqYh`0$vZix0U@_o4vY?O}0Uxb@1FJt*!c8!XLn^5uLk|IS%VOTd!ef@iV z8GkW6iqY54HCIgy|4{HGKeVL`laXkp;g(j&FmFHae>M$K)+H&;=VqDa^H`aWzgzm9 z96G@UsjXx$3)i_<1^vn8uNlYG-0*ILky0~p0FBbFsjWc&v<^!Xv)@R&{df=t^A}p@ zhvGW#r?i_DeLM)vzJz3rMFMelk68!tA-I=eK^6lv)03flT!_2xcY1LKSWq%Qp7-yw zcxovlN~4Arib6#k4W~Js>UY2oON3bye{6al`F`opygH7~D_dqVSMgon+X|QWug6qR zcJ*!n8U3DH8hnGaAl^1nS$K&>GvfHUH~t!HWK3t=*{cT$hJ! z4%-UQ;puY0es{D6l}ZRc^tN9BW_k<{1^PNj*hvLQrypEg;uF(1N@su3Tij|12v_;2p2ejKhx^)fe%>B!d@ zhwY0PkiG7;6$iy*H@<$wf${ha$FJ(0CgSNo{`P%g2jRKpb-M~JQ01ZZU zrDmXxr{c&F>6x%CVylZxp?fiMz;92=B|0-nPvL8!hPD_kF*?tN0s<2^?_aiY8&=hV zm2en1W(e2(TMnCGX^sQ!T{1OB%1( z3m<(Qf4MOC@om&(`15nc`J^ReedCf%YQXVp^P)=(hZmwUptq>VltS_Mfc)z~R zoDT9&ZYJNKFTI3v9uH0qT{3SlUYUjL?hE(qo;=-bLK;zcv2vSvZ&*0AvvTU;1D}>C z2DD_?7b0{QvUN;rJT!9aD~)bh3iaTIh0pJtH*fOG3jY@-b(wafpkT51{PdbSJMhpKlT3Qx>SI~pJp(MEMwjU`VdD}k(n4xgg=k{X$i?gzq6VA8o(769; z-S!3ig=>2c^$Vd`$h`g}MaHRNWM5_78?u1v3cN=vF?qq8J`KxZ=`s*zxZIemmi7K~+EPpN&C zR=d1n*j!^y!aNokaK;&x8$PmCnaX;0-*7@ftQBDh*5^K%jeJt5Sk8XoPf5!T`F|u| z|0Wh%tZ)Fqp7?^_4SRfop68JV1iJipG~Jgl2E3JBEqLZ5SgRe(N54lp*782k(A+xE z@XI>_fgFSXThYYPZDr-T2XYtX*Y{L31WmSpfmoZkTfn>GiEASM3fa4)3m;TV19?Xl zNX2TLeFcj;XyzIg?ZP7eX0sqP(Nkqz@Q-PdD(m(Ina&Ram0VIy-y9FFmmk$8O_~1O zM>{hCA9+_q-s|xEyCP*UO3Cum-82z@{Rjt~Mz(B2nldk2Fj5hMky{eoI20N2rCe%` z=YWnQf!HqUw#)JB8Tu2z3ay0sKevJyVrXjnxRMul;J6cW4T9yw`tdRLBbg%Vx6kv* zXWO5!!#`RX59;n}{EF?PzCV1VRXmeU{=^V5z6*OaU$ou6_Fe8Lp#8R@f%~?~r23w_ zKLYJ{Fk8Rd`W-pcffEj2{qA9VZXv=$c;x89-I5Kjuw^HrF8P7IKl-hh0U zjn;E3%)At4S*@6HNX*!1&=u~VnU@Z@!ML!mrR9Skc5=&V*sft2($v})b zvk`IGERK=JcYplSHcC1|T*wNBh+2)HhqyS76$!J3N-4Cd5*e`74$m?L7JxmSUR#y1 zkR23W{#%U$y4A<)C)NB*H>OP%;d?CQX01-GlC_h5sb1-Z58Y(l;`!Dfkpz+bpRz3( zlI=Ar%4AuHgR-+f@L7Ul2&|674uzOrgRNo~2Vlm4)lJS>6e4=RX@DZetJLzPS7Kd@Fy;v-6D-U=T38 zN~>fTL?xO@j6p1}@^g|5qEZi@gc*dBIP(DQ3S4n{iVZ#>5_L=rXRpJ!nDBG+t8)o@0U1-8CO=oM@i@taOG8p=;qBX}jjB*zN_)$~AhI&@KGs6#)MQT8;Kh zm)0k&`N0XO=(h>0rTS)Ab~;-$b033KdaUsG9QM_jl1pldHaabDc$+7@r7AU2n=>22 zM7N4cX}RBOp57{esX~_H#&6j!H(r|Xb10`1E&Wa?Wj`Vb^f*VU|ITiI}DcZ?|n?Y1K^C z`;Y5wST<{2r|FJAe;Wf7*+1_9M%P%d5Z%Vh+wW1H57$T`SQ&=fjy#Ew zmh9iZZ~yp*lRE4n?TjFTT<8s*xI6eKnQm^tvf6HY40;D zW9Qq&rF4br>h-0!NJ$-5YD%Cm5cR-&bE(n}H=wOjb~s>`M)3p4k;VC4G8w#G>48;4DNBD>2cGP#_? zTB6R-g&W@ZacP3@;r~%C{wb_ zywUIN(+`>FU|Vy|On{^U;OVSLOAI!8;<%rKcRCeyv#q&m{=;+u0K6t)k<1X9qyC*q zpjtDf3ZQ7LizSA@Jgf-*60KoU?HrS?Z!AN}0P>zO=S!JCUy)V);TiTh*l5mzCMRv6 z$@JV#Su<{{;>*+uAmVL3fk&F~|@G+hJc5Hio8 zneJnpK8A0GqN?Bie#;lPaVGD7Js1OoX0XuP83lOycR{5kX0~3w^;~g-vscB)=;%tH zjYySB2LhrQtvJClL%{t|bjl^pa;#28-!Ela873=n14t19 zzpF>u^q_PB0Py;20R=FUv49(uhgpq0u|pHH`d>GUEZUtjfRX8yWerhEH9@S=fUK@7 zpv8?^7T8dPylo#3T}%0IRviX}VXXCX?t+w%6}Yr=w9XstJZpj%ZVr?6D5R<`6Ce?A zC+}N&CbL1&-~(7Dm-PQt2LcL$_)%_fR81f?8=5tNjg$!cWcW)yws zqDo@9CCV8y1$eC-GUrMk;YmVS$6rW|NO zk9lqR0BwlY&lk~qz0)b`5LyWAVt3*+#!>msN=g~}WhzlTwh;PIaR1A=^$H&AP zj;W!}$Y3w()axa$r}@pmLzn*^RjEcsOh>Q*n4S~wX1ja%z0R-Q}A>jZo8bQL}ylK4I0Hm{?rZ>(&-S5!x)R-4scGM~XH#8NGj7RF=~*XfnSX z#Mv95q+i&Bk)Z@kstu{^cp5`}B)*f{F7@n|4 z8|B18S7+HIuFm3;;e*T=gUByEk1`7pW|ihOu@DMF`>=MpFR!4bS@&n1XZ@eK0MGFp z0s>~0=T$cVW8DdW*lB=5(O1^!(xUS+7}TwtL1cxIQaz)#M-nn_z;BYaEZ6!fI+=k+5@PNsNV}M^?%c z(Nm=hY%|Tppvd?Y_EPj4sD6#=4`^bK%V_fW8_=eWQ5CxMbJ>JQt(yZf)xV~NX;$~B zN{w;ye^}h8+k#N2&g+LSTWDpKS7lXI|N2dOI=uDJKsm$~rz7G`I_YZM9}r0D&9~Jt zY=49^ws;5nav&F#Scu%Gj`4b~^OjS-$n8YyGjrz8!q3}(-ujERfE^F6?ycqdC*2iY z?T>zUbeEc#B9ecn#BrxsEQMjZJ_#i#1Eq(0KK;If?_WLMAGvasCxpWB%n%Vk;CMJ5 zhK1sw^ia>g8JxcLhE07{=yy1eJ@4DgDI8z_^UisA8Lvh?i?i5rhfz-9__E!d|1&;m zbM&-0@B3k^)l&E*eiGJlID@TrLbOF(#yti(BaHsjU60PnDRKN1*5$QrHMN; zY7*zDH48nB^VnviTg%}Lw%zoUQ#ju4wjZT9j!fSH>iZccg*oB|kh%zqwo#*}ai0A3 zzmjV+@F+(Q^*kKw9Am-=eKqxmdXMF8O8{Y>IT#j-gW@>h-)k586Em$|jFwSYOgC^mGeiUsIG#x+2%%6M z3=6|TFfc436pAB&z_1VugfkYObJ_pIcl$()KE3>_^D;k7!qqNFT{$iTKOSz~GZu!{ z|2cvBnyJapsZIiYFkaJbaY9{laUf8g7+8 z2sdBh)jsF-#ZwSkARz#wv+6{ae7W#q0!Qv9cuGIPWAL;_LP1C2ZCkHmo8Ip zT`rdwu$lU@Wgd!yU?3P~2-J!%D03GyIg=1@L4ST~^Yle(7bL`*m`Jdk5vvH#vJJ1` z+X5x0?HhiJ-vnyD1coJoK`_h^u~lVvD;!U(a_hN(U?3PAxtrrlE4X{wa{I;{peHp z&}qylFz1!i;%TM>I9@sJScfd0k29jX0AcF?-e?$GNj?Gm@OoI_0*>o)b3@W6Qh>a} z00mRO@S6Igth!gVx)^EmIT8RwK)b)o^oFb?dCfq3%m5$BHeW%E%CM9Rfa5NlHOaxD z{rUiRDZv8PfM7xsYZ96mlgMP;OW6y2rb1 zDpu*#YH5jGrgSSY)@5a=wG}_xvL9Z;6mOmiIyZ4v|47@XhcCoR-dRpgN4(OEfXx?P*AN@M8w<)9Q3#Xf;QSNs)M^T zqbuiZ&xCMGgnqCs%-J?yEb2ozm}LD=Q|px>xt@t=D4{S*;F!Nu%mq)vc=o?s$SQ*s zoyRzSsdG>0098uE5+oI2X0RP9Egdx4aOQ=$$r@po1Y~LGnM{&Yg{uJ_oZ>xEK&ol;`&TGo{k6~%203U$8RF3%da=y*5KmQNvi2_I;ka|#=R7c6Qppvsp9^W`yI zLp3=!*mw%VEL@WPsh7Uf?&xDy5tWrS{ZI z2g;yyp@J^Z!m40M>@5j!M1ATKIyT)Wtfn!el^ZA;@z)%qBbK=+_N*|93v=nSuZy|J z+wWJz?1k@_&%kbr#rVk!m5ZAW@l$Z70j`$WzF$5!6&P>9ih#`fZ0}@s^AqIGShiQf z!8|v+1!wR4?`)JnWhNAU+fw*V??)m*DKj!H*}lsCai0723RjHv)sfC`bn?8Y|K~2p z9&bn;8SORXv$MTy71{xeZs2_F;zz^dL^y5>$0R!@+gmn_x?P$f=BqsqB^KQ#ki2WR z#EKA6$xANJ<+8l?uFuZ~mgQ=XCBUd!!>C!S7xYr%M`?V@XJdn(y&1C5Dy6RxtHxeeO&(?ly4oRC~4}M%Q zhYnZhC(K_9*Y_?K*)`Ycjn!@SF-{nT ziZ8=C>reMh&cfv}e*15vO&m5y9rbt6J!gM8p$e{OMS6z`LaC%@@MKE}Z!*!#55_=Z zVT6EcbScz5d`+h~N%qP?f?^&oarEn)wtbt8ah?jOBeL#(AnWe^-ObH0pIU9axt1wOi-@lb21J>D)cfKAEc;=_A^E>GI-WR=;qQ zdl;vGj%K^Wqvv$eA4M`(9my}$3kM4(T+l^i(T}xpr++JTb~h z^Ag3oY1*RZ?$RM+0yr1J<9-j?j)k@uT-nKbho&o*n|}Y|G>z_xlnl7ye#EscKYK>Y zfSfK()Kd?%&r{(}_B1r`t4MQr*h2}~EVw6N9&$dCl22|aXru6}MMJY(dLhJ=ncqN+ zQ;RwN`bkLn$%_Pkl<$=X?CX@eJ0MvWK^af_0a%5KE}fLvWzmt_N#FjWF3u#+oEg#Eo$b}LTX)DfLx(~` z-5&}ZT{*m7byWkjCp zzp$zqn~uXZW|<{s?zqP!v$ZiKw5^UG#<$eBk|rlP*+AMdnD?N|SrG}dMWHk^-8B(A z$_>?>nX9fJu5$Af?WOH?$FIi>!NpsAmYq%xCtv2mr3Ebo)RgO#up=*BnzZDbML(Ww z#IXGcnvQ8g5N;yPNTjSK6>91HB73=q(Gq_yFCVJZNQJtV4&c<9dmDYC8quo%@`xQzjmGmOegD?}`PP;H>n#tb z|9>C<<+8q5^RutVcE0I6gFUOw9^0nQVAZS9=-#NN^Bx3kW)io{*zad=vbCm9`}PZU zWGSi8SQwKG1+k234fFBkX^>j~$A1^;L-zC+{qgd#J0e%=QTy0mW#Mfc3cvr|58o1urL7ek$RM?;pQhNOBg%rxbuUq zUNbC76l5C7%=UsP7|eat63b|4oU6{2r1TY7Hueo9^bNL~wg>!vD1K8TbeH{XXfN)E@2lFqCMM={ zxX1|;yW{|x%>8^Zbr`RVR&&_8?KKKcwyv&bFkI+Ak>a7xs&!RTWI6r48I?HUN`?Hd z`hb7g1O63jaF0L9Is&Efnn2BZ{ndZ*NWeV&(pw9KBu&faBjGZYFWG%kR+}0Z-~{u! zL5ZDGQn!Iqq7zzbQ6x@n*=^n7yd}q#xZqRPZ^)k2+viyEwwi#4OdsZuwBHa`{+Uzy zd@z6T?CB&w(+{)_LEuNd!@LVO1}Zic(_3FsAO$5v(5Z^}-o2x_=YOx;54=sgROr^)lkx8O2TYziq1#u*G!Y9>dvtY{_9CsiIQ9f$(pZE|99b~Q5#I}s|R z^84@a~=hcjT-5dFB7C-O_jqzZp>L9PFnk4aT!gtqkD zD`U#$!Y5CjEefdqQmB|cC-{iQ42XX?Tp=Mu#pqq#kmL-hj#!j+UpC8AYIE`s#UXeG zGx9Y*t?58G57Y{Eph#0;px3E8;v{7dc3&AVW03$Tjya3wz@}sF{Dw&~nwuDZ_1eX% zpX|aAE(ZJEQ~;^BY(WIgO*oMABTSAD3phk{Tuuka`BIJK$4d6)Eju?O?=2(kOB^`?HPU;BYtzsx!F*Y~>X82lCeXHN?sqdyb zxFtje`QVuBuZ#MLfpw26smfB+U45rcdBfTV&Ymd7QYT_K5IXeAx!bkV!eS)@@&=89 zO4TIgWXcoJQuFJ%RBjPOV2MooQ7~_C<(b#$Uo3V$CLGF>7WRT1 zlNZ@+$^w?M#b5T(Ca`vZXpS!rbK-pDBFd|;jmAC_v+xiXWcTLU9d1l=)$e5VES!*R4}6P~F<`D*sTNQbVt zsQ9NYj93nwr!Ri_+Q7Y7=vV=z)$4#(!Sg)Nh-iaN5MfK;&Nrqabl2J(pY`JdO4*W^ zP|(OOAC`awENIEU)r4moWpb4G<^WPxANY|#6ki-zMg{oq#RKTQ<^cyWJ$C%@ zD`+qA34X*(z>M;HJy&8HBpB+a9T9MLsvq<#9f_KBhW&t@;RY|-T|tY zh-{rHxCqxf0xT@Pe!+sH#xPNu z`#73UKDct#e2lSe->IY?Th9AuH)@<~YWp{8FHEkVaJZ=hXb70bg%E^sXn?D;kQ1JY z*ko}G#Ci&Jb^bhSUFs&-@sIZ>@soS~AA9FvE4GX+-y31`S>LF=&@pI^QaPhCWF+jm zR6{xG!qqhF%ET<6=2rSk)!gi;MPpx;=uWG{n>mNzClOttQ=R3mjF;u%uU)7+uMey- z1Fyu@*R-oaMx|Zl^~wkdioL9Y8&ol=P}QRlF($bS}vU zqkYXaE&z|;Q}KqmRJ1B0S~Gm`j-&!mcNg&u2{ZF?T{V&4?S<&I)14y5kz$i}X(=x< zRr8S{V71_JiRv;C5pY)*lPgVO$mTb|EMK`DkHUIYph(lj;71|3>4Lla)WgFs9#yW^ z=E=5Ux;(=RQmgWM&bOXKbjIUW`((zeCfSI_^jzNuxP-u%E1+@;&dz;EM#+s6L}7@y zy^87=ts1P+D`D7GE8zkvp9pjz8Idl$I9X~^bulJZTueR&MSL@I1|_A>j-i~PqfoL{B{?EAu++?Emx>u;grMmq>Y%yY3_$lb z6+eU)4WL-BgSZe2S?Z2gGq1$Z6ncTmUZ;wqM!%Rk+4)XDlN#qtva4Z0ZxT{mNI@<2 zUz68^@56k5l1*oyP)6mI4bUDa7OCIhjhl?!iw*jndPq9?wSLbO?B@o&J1uXx%hE>$ zZl{)crR8(s+~_Rx)223K(teSwVTOA`5|VThy08Ur@v-R6^XiN?{-luYGfY;*cQ{I} ztnzp>tIK3fvL=G#VWWiit``_dsc!0e%3LHDhBR@5FPf`Z$O)-Xq0x{4ZLSUk=&f0P zjC8XbB^h?-)Cd!jpPgw#By+!J?kByInLCf-Y37FsJr0A~OxnDkSryL&i{Q}j-6X4x zc~0N(#XXvNSAK6l4^vJ6r8si162c3=e$UDqCC+m}Iv@LrK1m(V^osdSqs5;Nx(=F| z>pxs`*&~2kO@4S~PDN!(oQ+~``kjaz@+-4HwzKL9J^2MDBPIIWdRZ(JJmwWWWV?ZH zkB#!9rpa#2Q8=ZTbV?~pi!}Eu$=dIh?E+~9&maMfG_`us@Rxj}!r)L_O>9kP3{xpJ2pVVubtWfDEhpFQ>Ix}XsE->WDYQ6(H z)JVO6{*}<<+o=sQ9|r?rd!lU`^6#0_N+N}<1Ke@O*Q@!SDVkZUZI@KknEj9S_8J}1 zN6nD|0AYdIWA^BBo{+^)eK-$E=o@iCB?Y0t*@(Y^u&2ya+50=1xF_HpW=X448+8rT zSOqJMMB6aq${MgA*$qs-f0I8|W=76inz}Y4F~Si0AwFG@YM7ABUKeF$PG;o%qHv3g zSVLUeha64%Q)05TmB971y@0n%<$D)%3tz%&EBV|2Ii+v7iq%Yrdy#Iv4zMMdldNl1 zHWcTBVsA_}E&}u)tibsc7c@rlimSt~f}TUcdkWgdZ(KgjJKWhPbU04`*>H9uYTq@rP6fMT1?!IxM6;dG8_&Y|OMM3BSF zwQ(dz5i<$2iK! zL_Jjo1VJFAWRCbdPgLIPAV%qlr(pVML6hl)N0NG@%S> zG@0BRmCx$k`OX5ZQ)6+pSZzNvi$oHS-82@SI92ly2B`;MdY2^8lFyZ4AhyZy?p&tE$cL*Y8w8`*o5z_!4_n!AK0 zRwC^hR!ydCLmCyZeC&y6H4y$^kS_hM#MhAi#I^s-U1Dwg&Hm7gO?MM}j;ukVezbt$ zE1J~Is`*{LJ{XFLEcb$Y`ik$N(s1_qS08`%`L&O)eco)nfmjdB)+lV@?eo`u{FUx- zQd{CS^EMq`m^OTg>-Kf)K{BG>+ys)KSB*|rYDZ8xUdKT)!OM#;1P#k7G$CVq>?T57 z*6SZ1`haEq@T;2nYb#_o`^ReGaqE^fA_ZEub!ky^_;aB*_tHyK>_J$jtuaP2Oi{DE z#dj53T*;5-xGsBJoOolWBc$K4V8BkX_@!v$aZV2sGfRuWu3Y7Io2x8W+$otBS&(ph zmYw22d1_3Jc>J3I_UvS2*{7OknpPUG^$^EYQ z_<$2qSl;;?2-a_^0Yf@Un&?niet=EL@2Gx^D`%ZA<&%e3)y)V!!0mS{dA*|fVYdOp zpvFUFf4LT2)fsQXy40eyh?l~b&L+YPT}`MZ9>%5dXk3g35z|8ray}&&ICVuA*WoUG zWFCuWb_h&$ULftn?A8szj;6lNJk`V&ls>Uyk8w?^*CV^HA_?8s2b|Eo&^@o=SGtHT zKPsacoCm>stcB>UOI_z+71~xBxCw{)cM-Hw=1o)|>%dgcdkq}~EZK!rB4O1u2n=wD z4Sx7)h~-NAUios}{x`5rrHHaI{28$YzT?;t#4zViBrP?UpE7D>j5^5w>?0W4`e8Ej9DjHgneMb{$`9aG7|fq z5z5@w1PR(sdSTAZCuDTsUO{AQI^oS_jX42e5vkF?T(vdoLYEIT=V~=^aZzX!t-829 zs!I02Y{7iQ^2$uNaa{%dX_4Q$sGW_jjzaq?6Fz3ZNqfpL|Wy~K+q z;Mqf2R%T5D)#+Jw+VJm@Ub{qur0zc;21~^pg{BGIIG7|~e(dRnp-U~Kyh;!Eb=6;} zrl&F;gLj1>^inf8OBqD*#G-wg)ymf3j<@_FPS2zuI6m zS#ab&bJM2d0$w5*fPyJN+~5o{><0!^C`F*%Q2Cf$W9GmePB|LLSR})dAMcUc9xR zt6BUDeq6r{Z>=xcX$7p32u`7Lj+&j9{9FV(5$)Tjk z&YFRxK<={9T;kvuPV-XD5>d~}(dbXJsu4P)?oR} z|1-2_PDO*Z81A-{|2Cl}$~6(&zJTl>~ew9?8f=GK)nY zE~yp~`vA2eT!;8kV_3-?z0f4@*PgH3%wMKvG2`U$qJG~bE?eN2PB)2#V=hI>*glXO zmCO>plL^IL5Ir%4$hA8ZVIT&&EiJ%#MdOFWdi*!Z^wV2^N+V9V%0Kx2EPk%7{}aql z+{KMr{M=8UqqGCve_mky5#?+zz1yT}-s?GLe!`>LP_6T7d|@C$&KyKbY;rUsSg@8h zJ2+|CyhV9gUg%wMR^!A~&@~Hqdwwqy+~LT76ibdI`%nUhbVhQvd1?E}ap3&9royEc zF`NJ?K=AJo_KVTn7gkV$=6?^Xyad&Rntv&515qew|U1t34+3 zdY)1F_Tp+#_HjcLb#uZJtSyTp32dYTK!MyD0gFyblQ4IW)?H6deWugV8S)K0WqXF8 z);ZUei792y7XB?4{w`JQlsx`7@^@5cGS@xs?Cdzf#W5(N#^W#-nj;H_`9+2)9NTF1)P|! zO!TT;c5JT3p=$%i4rLu_;D3A#gdy*_xeMjFZt#*?zq zY~?$(uX%aYFcOX5iS^GXewcsadVE4k@Amdm^0FCV1WllPI4spDRSB}dHZ1Cjp#gxSL^~j-(S^IlkSA;*cd@vz`eqHF={ZUVA}qkp zR$fKOm{}q$KkRVB=J%*>^(gbdkQHfXg2B3U-!}=urF!;8k%R!r502O~r1+2l5H@e0oU!ho1nGgk<6{H=BD{nOuNtgYFD6 z4N=}H<90|#D`t*4`{2CAkb&|0sA46igkDIffX!h!@P4ie;eiz42s1JxJb0tmi#s&m zED?iSY|x*qS^9~q`6+Z5P@p@jI6bUCg$5tOU37%0ut(4nxf+38cMM$)bKJWIw5V@> z_8`9Kc77l;HKti+M9i5uJZTx^j~07@jBefLWG0tf_;lpLE&UF2vdy~+Bd-VhK>wFp zu_3W^HLLujMWu+`>LD7ItUz5hPakNEHSto6xJ~Teb3E9G35BU#7Kk4Xk^qOw%IM zmm>pJGIp6jz`XfJGyV$dsW_^M2o)e7#gX42w59fCkzQ%uT9tK^tn^|+ftS|v!>IyW zKxC)#P7;HHJl^ zPm~Qs{NRaZZ40$yB zLdw>O(#^n)j~||d7Xx3m1pJzH&3q%DY0%u;%-nMdeSZBt((%tUzjk|&2K`2pjz0%+ z8uSNR$GK=WJK0F#w`$XB?Pzhg#x0aTdcvkY?Jg8%2;xiJ{vHanL3C~xbn*TLV6&#H z!eCgbAUPF_s!OE+dz{&~NwTPW?Ve?Ud87Nbe2++wv|C#1260$U*~cQxpm~XYxG1yr z!Q!QmRD^&xMx9Wp7HNipRf@`RrGn%%Q~VqDPWp)FeB40f8YUgH+bblVNlH+y_Jk#( zcATH&Vb~4T`*kkY4xF-DT<7mA9uN)(Kgoc`N259($CVnEG?U(24=y$n^kxY>NJTsL zuN4XJqkYPWLjHcM^S!N&?;mLAvuf@&km_-wae9$RI_!|^7D09)h0L#5nbL)UiYwce z1W=tz;pp1kL7h)w4as(oG~l_tHvsY|yQSjSrfJqoM znuH#KP=4Rslht2h5d)6jzwLUEd8+R%O&@so|8y($a9Ro2utY&t?^)Let(4d&;99G@ zSYt7V0ZIY;2dwut7QhODZwO^iKaWS2qTm`MrlcbEq$}Htc5)DOBx;L#A)kka5(c*Q zJ%b_1KC_@s(YGFeAd$B!3EQle7Pe*sfXWy?)AR1rI=&8(CXY(f`@-l*yATE3y~2tD z9&?sE6v0DTQ8w z34(kI1Hr#-b}qronWu78nmH6oJzA%B?5C?y130uA;CHqcW9+VD zh6QSCFTiInv(?BNHx^|rmceCrF`2=ZO20gbsWbwvG33euy{i~audcN6R@j%*Xi%&d zf?|ozBKF8=(^CZyzVIk>FrYb5hRHzaVs>HQMAf$R!)2UW`U=56Hrr2(M3q~8AEf@& z!p_)C9ql3FeH>ZdE5XH;MR5|$}J$J1r?ShJH9%oyNDh^20V?t@$>NTTPe4L>*lcB zfN5}8%5vFG@%`B^yB+BcbTd9b)fb&MlpR1D8ou(%cCdciShbw`%XL65L*cSNI0VND zAS`MeIRAIS$6zKpXfF3I5p8s$3%v->;X1))9uQ~|e251I1O*Rfxq4(LJAgBhmZMch zi34@>olEDwb+Wy zrQspZsjf4$y@B~Q5SombG;SXuS%hFUHe~cELXBb4*GnzVYOMkvQFkyhWn=klf(#;MX)J)817W*^<}=^D!<^9DQ}>6%XFAb#+}9<;p=H&8@p;RL>3-*ju@s+4e=0 zZMOQy^Ge9i@kfvetiqT(Az>%oK^klDKL9L^prt4)UDso=t0rKvt0tz(YH08p7CeSJ zel5R`{KAlm*UQf$T@J;!au~jqLw_x&{zvutNMIGZ>EDm`C!4w?d=#@X#1TcxRE9|h zVKHQN5LLiW%qUfiQPl{MxHbQQGUScY$`;3U>h&!o4XHSGx7k>_Np+krboXa%^Xd9=}>&-RM`m|5YsH1wh-=vzlwkg&e)mG9OrsiD+eZ#)ZnW*W@Pbs`n`%@;@q z3w?eC>W?dh{4fYjWVS;ea7DA<@m?u9NfMYFX^Z>kosk>0z}4rO=K3wm4#xj45lH5G zfoWV{f^^O2M|TIg@n{(aqhv^-01jb!0l?LZg|*x!LRjm>j9dUn#m%FA7_^Nxxs!hR zzEzCq4DUW=r=5f4TKl36SmYjUZjuwRj2YXRFpmox-PFcpN-W=x1}>PPQH5j-O}k5yCv#kJl{FO%-;i7WrrN0e_Y!T?{!8&WDW8n) z4DZ*v`{!?ir9|35drg{eE8o9;_HVzpM=h_ax6onc2HlHTX9i!i?L-TFr-_yK(2FIJ zLz24k{xt{tC?8ua1b^?U_U_ zE~wulhj?ed|K9GP0l2FAWq25832Xsy&iC)_yKJ0M%I7)6R5?HFhze9?jTA?(CZ8~W z0W#*5rw5iq?szFeT}u+iIoMdT`)dv`=v04VnmjErc8f3(TJ-)BU^|DDinoS27k&r8 z`WuFMMy^h1U+%QwZS^&itNH#9hKE-;CI3Kzbkqu*YhIGy;qFf@U?^+c z)N4>H`Wm57TU_xc#IHYPi?HG10l5yZF)lz4r$Q?&a#Vdn>Y|^fyzXJH0Ba1&I;%G= zt%@412%nx-MbK8krC+z0%_-kAs-2;X#yU(Wus~{*q_A9Xf?cuuX6Oj#No@fZ`Ga;(~{3$IS z>PCG`Coefm;4hC1z9y~+WWt{bbs?{m21tZ3!8>V3JTE}6Y0q-rD!#X+gW6Nk-$jq_J@?ZZ=s%De=|nAUtv&n? z91_|NFOh`h43G%*AW~2yPHL7W$+}esQ?cA(+#NadF)DN)${Ie|XqwMh(?0M`!BI&0 z=EWl66gT~8&z^Gyu6o^KdE1t}-?y1AC)bkgGY4{)%JgL_%1eHpC=^2nAk&Z?1!f~w zx|4FN?P!@Zx?B=DOTez;nd0%-5|Yc4jf+j7tq{K|fgrJ=!#_w>C~#EthF3KEP$PCE zB3stA<)4}BL6SOcMtl<5sYr!i>G`~;ns!5MObf2Xrdh#&^Vh{C5v^|gyx%qba(HUB zBAF)M%aTBJW&EN`ef-L~+JAUGs_?q+zlk%nf?a*ut`inl!`poDG;zVFmOe4_-tb*TcFau1>8Yxtex`9~C5> zWENvz-{X^#L=Y=-2Ubgo!C_MGoK^~%B4>M|60oWqKxYtAHqW7o0^ksKpfw_w869Qc zm6Fdo&p=g`B23in-DDlLgtNm?B~*YSKYv&h+3HWhZ2^wr6fmudE!d32HLmypSi;ek(bg1qP0OHDuWr8B z_1flJZ=|MCj|~kw1D(-Qw*VFk0JZ9<)-Rpi{S4Z|!*P(OD5*PT+ZaD^IDhDi#?ZGE z*j3BblFN$)>CYd4*Q#1q?rJNjsA=YtT4BhT+Wjd3RWh7O=HQ0b-15BhG;d#hTU>(8 zA5M{T4H~(xA+RxHA}}fU3A*(kz&o)#;&cv`Z7EZ61v(STk_3MbqIcLssn)7Ci=d&^ z>;*~1Nqp=OzFKW+`%EfYWAG)HmnN1m-<2ac=L-a+jX*2PeQ39G~`G z)auw_CtKZ!dxiw*tkL|>`l~u$qZ=R%o$ZpX)d?Qzmvf@YeeRN6roi0T?%Fz3{Bq>k z4#|DxarWR-Wa70O+T%^1@TSGpIaw5GS9Gj4p?SIg7@nj~&z=*$NRGxfR(i{^_BSsM zdK5kZWGt!;zU?UG&$1#{O1RUH`Mi4YEPdkC8#_jXb8cWY*kG4m(?eKiA+G?AY$z(F zKi&$J>~$c4UKX`&cTGdsj`yVMtE44jLd!~T<{L{|u_qpkHWPassK_k&erg)q0L#i4zDRl ziu9!8fG0JjhIKn|YY@6hjy8laEs}p@?!@AMrv0?nYLWJk8gb><#-mvadBde%nn|rf zsAC{_(i7sJfuzS7!fnst^VA+C5e(pTNFsLIVoea+8Wep3y4*imZm zb@|&Gjr+F6J_~>s=4vAAB6Kh{c57|ZfThm27jUum+BXpM*3%c;3(V@JVa`>*>b3xG z6@P@c`pp1DTL>{W*|tJ^3-F%s0K<4juwH=LZJ7vyg#DU@_r-;h$YowBM<^2)TtbR% zSGygZB+^e0@$-OrlfgQb-msm zM^slZS*LP{N*5fRT@sXynP7fcHP|?cXcib#?Wd@Fp_fmzq^uQ(NR;roIR^K_tJ-)2u6E{2iuu^d}Z)LX)mLK^+rFXW3afBo`N?NOo%h)(YQNPF{cbBJI(+^D&HwzNGwhBW%=yZ*XT z75H&e#lelyh%W;i=8c zY#&@oApQtG)MLgUc2%1vMp9F39EBfnsuW_;Ak20DJ-R2#V3W;i3v{qIn%(x)+3gy*6=S*?e1wCy4_u**A}Jc{iPk7i#}v4pmK^Wob^uV3dTB-SxZ#A7DW&8TFca3SDd~`3 zx;p*qQz;5T`n8d^N{25lJER!nIJfq;=S?i=mxB0B{0`Sv3R$09jo)Fv3uoRgg{)Ek z236GRwk9Q7hAH0AUa>$x7NVi0q?Z)h(by41S z2-vJqbd3ecfAdFe^^;q-pZ4S*mEc>+0jYS(zEZ4PTp?k_60V$^kgeepA2r&KE}L?8 z@f**wXb?aI#lcl|W4U(xmEY87%WeAUi{CU34D!i=4iZgtV?!Y$N0oT|4%vp7|U#uJAm{a zX37)?ZOv$%;k?YROAf2&Ds{Ct&F2dS*3I#2z5mSbr$@Rh5g?7FV8>u!T{f%jguz*Z zbd8R?NOrxx9fgL6WS&gR*&lciao|&zU{7OKjS|A3HfB8{^MJMxaG&Djq7&l1%tin| zhPBWL-gwZYd_c>pO7k6Sjjh|V=!~NuvU@0K!_R{H*x3AbeE4E{Pst!z04w|H`vn0p zzdh?8dU(&ehW*L>cJ>^c0NVpnZ0rF#3sDR6xJm5^IHE^Q^JWCfut&U5e$!VQb22UN zMTwlH3iX`h=zQGvbV^!uZ5r^*XBP{NX!i+$h#->S{)lb;Fatm2d-f*nP#k%_ z3D$l<0PV<<{djmh@UYGdCKvY)^jk+f_i-C0pRaXk#x!v|se1B9U;hFQ9#@0Q<+2SR z;y_&=D(3?2bhdp6z$VKNrJhMUhw~%*izShBDBFFOv|*q#z65hXDd=`P>`3C!LVhHO z1>ovXR&7Vpm2heKf2$`VAO$*5#&yzzyp!?){hCZlgtx{+t6v^U$J4;1gI$*Z$hr1l z^e5Ff(D+da97t+~QszBZCVue)=r)3%dC6K+-c5e>04cD=Pp13n`;O6vc*a0esAS(m z8$Z)kPP(7b>N74I&Ds0PgbSp<4BpWurq|saaa!w+IB|Iiw=H( zqeJj4dEdSCF(}wX{e}0qjmzf=qt2ot_z}{C;i74=)_>TaLW5_``-%9+FZXNJnj$SN z7^&;E>Z9ceCBpFG+_Z4tVa1qB?>xpUe7Gd7LCVFttl2tL=B9@?7-SmSg5|g_(lgd9 zuJ8%{GZF9j$BKy?2Rq%2iiGp38p1EODRFp4vEybKLOI+>S@}#7&old0^y8Pwu+Pxcd)Q@*N^a>Iqt!a_04xa)KjuMyso-dD4 zZf>zzw~0$}%iM-i#^ap>u@iuo7g)G|Z!jNNtR)|1$y1hoD(bfF)*2JD=ursPgUKZy zUF=q&7O_WX>()jBtOMV_MEMgapYU=Rbxv(VhXtje6L5>i;Cb^hmUER-QUL}F#jCBh z(Sj#<0Y$#NPrTS>em_fDi|CL4)9SQ;GFI}t%lHQ@1z&MLNXi+8AjILk0qOyg(QbZz ztP=3tT>aJYm+RPbKEaYi7C-%u{WW;qwqUF#7qSnQLBX_-zm^6*Vz4ctD5_`qVlM|B zI8U5DupN&HIh^cj4F2&ETL~B4u*(l#c3+~?0ICJuX-+GOe2cxb&)0>sFfF->d*ef zbNTE{yjLT__HuZ^VClCug{ zm7}ImIcgr2Lmd{(8i%PEX;c?AkFIS>OlJS}oMiR6yc^#Osz1wBJ}Z6$##JAG<5ns+ zEySYmG4XS_EQ8du=B=PTfHvX5ri;*54rp;4gw1@&B0~pXWi|$dIvJ9caSRt%2-NCr ziCO(ig(75Q>34EOslrWgaRzl?*caO^&7ks(I?6=?9xb8g^#j05iO6g>;LCoJ`_1v% zWUX8_Ojfql#^!c$kiv^1N~jAu?Frwmp0^*3RgCk?e`ZM6L|XM>Zr8Kg zg?}v&W9{15jZ)&nuk~blnM@?7UOW9Acv(&K8+UN1DCmz!oZseJ)lt_D<#pB_tlu^Gl#DDR-F@(FQQopjZ|UB9<*9i<^uEWv+V zNit4e_{mV(c?(JTU;GLRFK=MB%29n=Mtl<6}xW5ROKH z$72*1EB(hr+6B^3sZ;_sN~`joTTgGr<;pgq=3_X0)x=cpy!q?8%m{bYOC?@VnD7+0 zhs61tf2{&=rAajI`uk1*LqNR0m{q3v z*rXkX{kKJ)6MsEb`$)Ckb~7&6gTh9ViecJHNxb_gAq%}{Hw8PLh^{OxrC(>2myJxY zy2`y+*<&x9F_VCKSCHOrK&lToos2$76@xX~^l{$u^IuvgXH1eJwS+&wmXWB1$dM+V zy5C^RYwzbr={O7=Vx^A1{8f*=fa+8jJiAk>z`wgTJz*1hKDiD|bdcn5; zMz3{9l(V`KGv3xHI&!sDEGCV+DBM@|(idw0=S7qT1FNStFMsROYa*@psO#1!=)bCO zVng1D+j(xwsaWU=&3?u(t6n*N0?pUW6I&nV-lhOr;~^3AJ%w~h5SCm)I&o=NeUhISndmiDW?8uW*PauRBdZ&4eo1w4GqR zJ6a3fVND?D7=M>rE=Qk+@7DV z{d0`R6E)F?rW*UFOtieqkYF}-Rx*|eYXpbdCCI$38$`TF-8D1vwCF?@r@Byo6*wLVd(Pg`w**x?=m4P=$~Bf<#beV;PZzO?n*r{Nh$g0#`Q_fwod z%U*{^%qE!`zup`3vB}wvu{Yu$AE~0Sy@0 z)RtRxp~sO$UTieDy;=K%*SXi(fQYf7{5yi7ty!`t=vv^v0)fgoUArftuI(rvi=sN! zf>Gt=Aeqr;_6_@Z#fh#FY?un-hB);Q;rWtrnzj%EqndMHtO90 z1rldcAKRwvnm1>ag5jAO6nS*S-H@(l2bmxvWqUSvzp!QQWG+7=z|%T^J_>Wi4&~UwTQ_1N zFLivy5OX~b!#I_IzueQWn8J+pMnSSDH|5ms>5jP8-(HxzsemP|dU<}OKcjRg=YC}`Vp$)*Uhv0WxhxlIbXz`tcb-|K5OPIe1JycfmUY++72A>TwqALoc zAG)4hOGD5&A;1Rn7i1iWu5o`K;RSMuGp7Rzg9&itcOP$bMmH+j6yvxzagDu|kF#kq zdkX`M14j$&kkBFS$m$iimi)EoilGgBuFqrDR_xm2)J`CR-sr&K=_jFxS9@WbnyCN$ z?J$SKbWEW0#Eo*%Ztl8=a#JQ8NXk)l6!XN)@Zjt-Q4(f_nfR{bJ9h0!Tc?dEFP6Ek zf#GrF?Q+gr&+eV6nUXG{)sd}bsItPOZ9-a&4GCWoA2n}!d07_Ii8DJuN91s%{yx)V@+ScT-%>z zD|Vr+moj}lU9Le8NH}iAr<(1HdI_hRjWRmvxda1g_Nb-@*U9Nz?V4zN@`OM+oM_tr zDQP-qX=h6RMyT1~>*BN!#9yRKDT700)q_#K(lGjSSZ?ML{NuDC&itz!~zlr{+ zXxEyRIv}~MMwfiZgabwgfjr7I9jepydGgfm*>q&jE44xp)cg}wD@AqdOeb|;ib^*z zwgp;1LzkMCN+Rc;bcm{9V%$ARogPU;IP`YtVgluij^3QSlZ+kT(e27^oyWCc~`v<1o zH6rdR`2?NC8rXE{2fyplIi$NWJ*~H0HpjT(*V^J;D}II;}gH!CzJ1 zsSqVNhw84rYcJGoU`N6E)|RN2o?LEqsRA6@OKHuqq>h+rE|S^xy8>TM`|D8KcQt~j z@D9PBC@(ZB)0aBS6wxwkKHhx45}3GZaVlMz=%iAbMhV2+?NO_Ta%CvMrUxFL2j)52 zOv;{`a5M89y@EVE3QYp~3!Xk($#oZY3j}2V`=T**J1c259L6`TDP0doR+2OR*?isD zU)8n@>3h)tOFH;bs-zp2Wwj^nbOH8n=R?mi{y9aG(lKGbdNyO4>8eD)FRaYw4m!K zcS}}cNkd(kIS|FF$onwM=1a4^vp><1w~LsXZgn4>Nymq2B?0R3^+!~%Z&DIwVVQMw%62AFO)T;Cyd5;&*Y9vyhpXKHM!S zm6JgWWs7AMrmx?IHf=&}N@Vqg(nVwYN*Fq&jNwq1Rj7M&6_T?23*TY7MT9RGoC=iC z3$eOq7Nro!XFS@^gSEcsH62fX_p3rW05p*7| z!{YmD!02OIOl3k_N6qHV_#{-^VnJHYHLm=_eI4lK+1Rj=iXAE%)2z`b_U`6_gv+&{Wmu@XzETnT<9Cf&2?NgG$C-E z7hWylRaRfy4#_M;uHEqVFfuqceUItUp zji#R8Y-KNi^`^t}6|+$)g}~p3UYq{#`Z|(+a|g73g8d5T1Nvs5WXx z!ciXmMmF_!GV!+TpR~ZW{8`r?*t-VE4tjj}6&9AC-*$jq>-W3g-XYejb$Gvwkpd1( zG?(}e1NTWco-n6*o%v@TS9UyLO78KlT0rH33ob5B;=wy?t)gHLyyXi_qZrx$1UGxo zhy*;aXG0)^A%HN>D**z6U6z2Gm z8#{o<>r*p&Pjbf&ck9zH4@T3nK9G1%TkZM;o=bk%fQx_BjrRGFd9VOxLYaRxCqR5Z z=_$X~$B&tmU-xWd8y91su~?rOB917Cx?&K)9CATqAtRlE=GZmIatqy3%@9N!qmsYy z5I?^|Qi;miE3OlEg9uzZRc`>}%#7UNi11-LU83G<%wt2u_QvzdB9g|Z!v(d__mS3- zB+M>LhaYCD!0LgpHb(+h-+sVY6NH7!Cyi3cV@|a6pw>41LzM~LLBj`Ew9&V(&!tmo z)l1_-y#jFmB)Z}a7_1h?tO(~!Il#-%{uajZx;S9_HVC>kB7E0OORgj=!wT3iz!qi7 z{Zjft6xonujsJs2HrkKZ5NTo6FX@gxyczKJ)S_skK87GGekVrc!0!cczczvP}kBf%+#DNtGXd06ZZ!zYgo_) z*sYYTQ>JOHg6zAW!5MUV^2{#a!t!(BmG!L3Ult10Z&T?v{i+O)i(H9Z`pV87;8((Y z7&_lywn2ajovF$?xN+;31#s?1!`F~KI@0dv(4>-4!sQ1u4j*E8=$M$hCWJ@ssEMBF ziHKJC}IM`bZm~v1KiArf!zN%!A&@o0=ZtFrYIw-0=Wy zRh0v}Xmv1_tIt1$kt=OjJeHV4y{cLZWMy_&yX!wJBD7PA9C!6XYqf`*rU2ZDe*_8{3L!bs5DNNric{!i?cB0zu8 z%nc|yI{v(x*hbjArgD9n|FToy-9c7k;sELQ#q~5}it3NMNnRTvh1~T53T=w=qIoui zF8g@@PKKY+A(my*>KrbCReqss^=e?5qzW4e9qWWT<6H4%VHm zyLUeRkDzhI_WO!^7*H1Md_L3P#^X%M(<|>e_>XG(=Ux5lS@b3KxlB~VT`RY)lPH*M zDKS}s*Y0JvJd617$1g}xU91KWaHvjYf`Frae3N}LMRAGOg9n6|^mnU#AY#bQ+#4x> zl@9I#+R|@xSP!l6mt(GWc!m9h?Ci-x`ZwbCt`5ndK3zQ&t${Yz)}G95v>TFPdMNECGoc zC5UaGwmYC5y>h!#TR1w*)dh~`3ZIbx-95E)H?DcrmHa1T9^0jwBGH>@2SB5$%dvbkT#MySh*dzWJGig`uC#pE?J~-*g|2rcOd_q1-f_bG*5uu8F_bY|H0J&CIcIL)J9c`sigf z?R!X&Z2ugbV)?Kr6asErxB}XT^{$)8n{e%&8fS6$2YI@g(>Ypce}_eG9pqTpgaGu7 zggwN4O;xBJF@&(T|GGcZ)d|nu<|ZIEIg9Lp>mGjIBX{8~h8sQq_;<4KUulzDkOL9D z+0XnBa4&Gi$MaZ)5qR@F21lBDQ*Zf1UG4v_I6WZiHprbbeEecELuZQque4{Rgb5xW z3Ws21Q;9>5wbUv&b{K~C-!;Q1p?o{*`9Q1k5(; zp@a-W3|ghba|WJ5Bj@m=shK$AFwF--?3N-iCSpR9kM*@p7uQL{&+nB6)-1`vIH*J-gmX_ z1{O{EIcZWq*yy6zhdGb9^99<(fQ<{t`o6zSfj-`gZn)5S$U(<%A2si3Yp;XdVC#(> z)FRam(+8(`phY7JwYa#`*JlWhJ#CrltzB%hU4Y-O9eXc9?KojOP z+fL-%_H;DpCy+Uuj1S+&gp~k&NS7f6++YX zp?U{_fNrcq-4rJ{D;81_k3{N*d_0ypW~v)Z!#?MTl*!SmmZWXEZ?YiX-u8tQ!I9(e z`F-<_NoBj82nXEmSI82%rN~p$I~Q$5l<4C@!u|c7*MZ$9gb&G+@SeP>@!#78vi9Lj zn8+0$gPu78iO?(DvxVU0fVVj*@h)%om#+&uR@CEo30e-ach09a>-*}1J+wZ%GEJy8 z(8nKC5si@I21TqRR&Q(AjGva!v4qbO9baEl@=ph-+lqc%|A|=`-NUkG-F73}J0leC z?j>3YP6e_C&(^Q1jP9`+-WJC4K5`XPzPAcWe=7I>f*YNv{qBecwVj#a zluduQ-(_hZrRLq8ig)CDr}7_L^z2tV;N*~m;3vV{OqskoSMZr-8agg}y=pEK3;O!c zi)py@APllu@h5R#-M4i=`>>6RLn8Pr?#f-`-MhtMmOsA}Ge>7;UFXbiz#^aJk9#ZH zq5_)kt@1_$Gm(&~_%!^3FV6@)iJBjv!&261E&pAKi>_Me1}gaAhS&2iJR%Gp=D(-C z@s%jVGMs%P7gmg)tk3E`ClWp7E-9tS%V`qX#Y`~ufs&D`VtkY?vQ;7{3HTImc}5~L z*RgQ^y!H@pY-#h3dFN*W0xcXI#@9==)iBG$$PSV?M`Qc1NgKd3wp+`os8N z_w8eqyMN2Y?T0W1H2JZQw>+HN@uFR1DM(N3esP1iEJ)4iSQ?a#b! zu=kJ(gS1^)A}rg<9v^Q5&UAF(W5i4k|zy=r#^$+4c zHZH%XZ;L8#^(&4TFC!n5uQE#YaONE>lbg#!`Jm>>l_H8Nb{Fqs^T`U$P%gBC*8mo6 zWQQ^T#q@IL?bPpmh$eAjSd5m;JG_@;j-OaW*OnWH+c-oamel$3(o5;vA;KwFT3!#A zb{A*258V*mNdY3p)|t+PvwX@-OYg3NTJ7SGz5Ppo752r&pODTBYsCWmvkI|dDgLjL zPHey$wB34c*4^9~^n=aR{%j*#sCP`+m275>uzXK>?ZQj&E{$emG2KR3h_^FJj_sm+ z5AD(C3yt?&UgO^g>BJXl^P?{2z4@TL?{sF1BYEz>LHqFCT^!(a#Xh>?B={E%Uw^?N z9i^Q2tLzeFu&2z0Wqzp%?tZtZD)%gy(mYG~uGg5?( z_<9$NM3RwY4{&r>n#E@S5`}~rCp@7`>M5V2sME`lpJJR~#`p`vH4Pcqr%ua@i`D-11YB80Lct|iNZ=?AN;7}Zz;;`1pbFxpUm0oKs))Y%9%D)<_@Z5sY zlx1{Gt*QIS6^FIk2m^;KL>puG)RydcK$OhWdx~3Bn*4gH>8l7IO1hxoF};@inp>oh zQ{+RB12g{fG`CS|EsvhFIK~uq(mlt=Y1}EU280?+p?>C{=ZvnGZgeFr8J&+^c{x6Yf4gRwrm zAqG2~^0Kw3Gc}ovto^ySa}e*cVRuQNRAA<3JGO2FOnio9H`_`XlI@tg5im^Ctnp$S zn-$w-PBU_}b-{aC*iQwSflhL3{vLS{sDA>5SI3hBh@e32+dZ@64;q?rGQ_&P=OwHX z-Z?lHcKC(En*IY06x00-4v+9j+Wb-6xcN-d3AQGkveU~$EB}~k!`6e0)y0UDD+Sjww(7Ic5u^aclf{8`YUz) zh4>PrEqw{^tptSb&MkUvpqTOx!(9qyTW|E>+dSUzZE;KCRdhg?Ygz2|H$E@Ui90A; z5|es(?lGK}+4UIi>mDdb9$wW5r%mZ41r!r%A0j(^MSwx$2V#HoqE9o~^bE!FryTFp zp3}45$`n*7`3Z*+OEw9StlJ^*isqn<<;qL`_-VvY?TnlTQ@wv^*5bt=Y1w-arBlZ1 z_!3$^%4io`I`{8H2OsRQ0_6t5L0N4YzU-g9rIcrbse>E-14=Jvt1Up~&dlsPF1qU_ zgO51ysL5O(i`M6r=SjL=2K_bf47|v?LH~@yc2fTQacWO$n;)C*5~6Cd8kzU{wmav| z=nQuj`*#5gS-isX2e}pE+q)!$Vge;Zz1~y1l4r?z-HdB!(f!D(uV$?}!Oxs?3gt4@ zrn_L@`XC#bB93s$7aeVN<$&GU&Y;GrJ^&T6`GCMqZ3a;dXiykZ?+TY&@ zOUp}B@Px3IKOkz)YV*JXZ%3wVumXk6&w$cGFe+8YHw%oA@!EVfjK|-xIiM9Qo^^!v z8XW(rdB~khe4qyEic87F8YZ;&c>S`=`zQeT8+A8a;`dMyI+XHRDqcyHT$b5CP70E7 z)GAmI17yuR(Ohi991Eoc2FqP4X_eo-WbTa+8;fJGEPqVq|B|<>$m-u)m@Ye{n(bfL zp=%WWl*%rHu(BD{=ItQDF?%Zd<`W&c3l-dbSi*8KDJ<-?2j zHCy$Qzi)dRY!rWzALTtnoLG=>$bt^6e7jry4Rk1+dIVc#I(pPd8TLpx)qTB-+=`VS z;wcSuJEx0|I)1Nv=e32Jj&Xde^9k06&&WRMnj|egQHgRcujnKYi2ux|&=Kx!b!DD` zQ~J@_cc_t)fmAu_B->U@Vi{IkQNCvF z`!8>>#y{C&C^B~=P-=%Wa6LjK97z3gL)e0$SGY@@k%7Ylc=z`?@H&Eh3u~>zJ6FPrTr+ z!Sz)T&Dtg4@|MojTEU($k(`cHU8Y`!J(>owWpwV9-OV zu!G(^+AcEZ!}aROaCgZ`_G7Xz)6&#7 zudCXt$TU9}wq!T%$VYfNWA?d!?T#}^Uxgk=Np4whMoU`HQdRpLklHfYex*t2&e1kT z`INiuoB6#`k<4-cR&cM)?#M&KOIS^ff$uj$#=##{uG}z0C-m!V;^N!$uMK^JCB8UD zgR3BY>zb64h&u4#*@7T5b`13m*{Cf`0R%Z+q<{rPCIn1lW<|B)zBwzFDZZZygT{oQ2jeDT%qWxF)t z)DDNQZv27D%!S*x*OcrL{$A$U1o)z-h6jd3V3p%RxP05OXLEuP>8=q!KQqr1%hDI? zYaw%zu({2F8l3%Jd~i9XGh%DyBu}|HwSog@aq$dYle#CAq-ote=_lL-!UAWotzTWR zPQ4e(C;9XUJ;STkv+lf}@NcDh8hDZg)COnmXloOO&QCgVd?YMiZ#E)LIq9{&!AVye z-5eG?%Nb8}scvrSnW0E?A%b7npm>BPDo0jyGfQDwcJSl@i$dbgTdb8x{AGoYI)`T# zr{-T{dhOe$8_q5&R8i!-al0Y}^CffpCl0%!0=wjO%C%noVC0Ri9`0vP?7OkI_`t3V z#_Y)v=zqcY`+dm7lUSxAP_qJ~asGTzR_X$iIjI}&Wqsp;zR?L;DE7SWJGw89oDgvh zz?(zj;*G5V!7RN`t^i;q)3hepj6bm&nu=@4CyZs>8 zq3{kFOw>-*IIYWZSx|_5+pp6OkmM1`G(}lT41Az2e$f(UL#Rl#eu}p7=C(wf-dxCo zZ=FNoi&CakSjieDOaI-1{>}Y&52n}UoT`dBJ(y1d(9}oR27=#oG}YxeHMlWrAak{) zvwO;B__Zo-6S(_^vdNPy32!wf{I<;L0koUA**lfu+fJhsd;g*HiH*?E&0pidJ#915 z5VL2Bg6}jJZgKpPA9e4XE=QqBC=j?3AjhcbP47V*hE9yK}m11$_D zasNFa2K%Y;g+4DWRDTORrn^+Ue&V55`r!xBL6wB*P9OaHM$AqRXEfJ>9w$TFM7g{q zD2S4uaR_k>A}a3yZ7TS()uSh|Hn2mAN%?Nf=uv_ zbykvEnrVHS(5#ASg#uL94#AkL%K@r^Ut-zY%}iP*6F+&m+a&?Dz3LDA9)>p=DJiG> zHlTdzPJ`28aCk{Jo>irUG$^H?HkWYyYC-G&tg5g$n^a&{vJ%${+9|JDx`oBb7@N_a zW9q=j$d3pzh^LR95>FDw!*%?$1m8-zN@kP19zUPHsN?^>@@9Lot9*Vj?&~s7QWbaf z;;&uqWJScVi|DM#7<}1UCPVBT0s)pj5kP=_LQ$_6ZgfJWq{m`>J;%>qCGrv8HmeGAp*{Jvf}^5mTo_oHXH3uL=U0AL zFRb$%$uj1DpBcqA)Pma-Z4-6iq9xfF7jlU3R3%@|H4fzjA;XY1Y*ev*xaN0OA(yTx zk9=iV?Zx5AfwTYO4&Wq(AzNo;PqChg1XMc(nW5jLM; z%~v3^QdV6qr%N~iXN^x8Q7%&0*HPq;KFfmp?86S;{Qm{n{LupabJ$Aw|0I7xln#7D=y>T0(cx(s zYa>Z|PW}E=e1qM5d)OQP7KwZZe-f1f@#o*1wy*no!@w_Kt&A@L(RbJ5!kwtLS9Fwh_^N(n&sngq7_*z~7m;Ij>_aS@XxvT^w^dbCc2+U+ zbeZTI%c^;dtNl^6;#DMHf%d2@-w0Fuo}bB6yI(4>6v;@cKPqIjWjth=%kE9k{(YT& z%{HIvslEkxg-|_A&P>FlHecD;xh&r4iB!V$Bsnt?o7(4svc9y++Ky~rPB!_dg);7T z&7>5bjzd2<4nI)j*#PH zX$0pIp6T|kIJyDK8~kI&;KXRF?QiPf??6li{M1L-o2#Mf1_}Wp-s;C{mHPJ`=6Y&( z%~up{)_AF%R`b6aTZD9lZ6Z^LMF7Q2)Re3UpG5>iy%@U|r#zOyojK6grwd8AQ|L~Z zxLBGKt;~zUD=iBJmrsEI2!LmH$xTGAKP&tJ?#BytzDeMtQlMK%WyJ> z439+ra8oL8dd_$pmN6cx93u*Z*)d(aqnoyVnWq7ZJ1%*kMYgfRNBvt7cOP5Ps&$=4 z^ToRy&ZnI0dCVV+;GzMVs{lT(2&6s4xaK>t21aACnjze+{>hIg@{^@SaF?@2)?=7e zS|>+`LeVjFek4xA_pUPps_n)3)wIXK!Bx&vPI~gWTk{@oZ*3`eAAiY%oKE++eXZ?> z%9oe=E&7sTL%h#xZ~aZ&Bh(w*Eoq!ziCbY3AM-h@o1^PlH6f&~=1;%>v}Jzayw87%nR z5~R6otGjcQH~3`PxXbB#&TW2Rrt|S$`uH#N_{fH9WOes}Wdsj*5?iwdN2R@Te-R2t z0Mo|2_$6*Smr>8mYDv<{H&7*Fl9|iiQ4d|}`-b#kKk67+BK`*2r*7(;o5MizSDCGR zclrW(Oj_KY#J6(EECA(PDiA|`-A3n{ov}zu@`^`Z(%tP>@13sHUTGmN6YQ=iUkV#p zLDhI=rS$F6eV%vIQiveR%hKw_ULUAB#fuPPfD2s!7cD?fUSnpuN_E%JI=clku((lWM-4ql=2}cVq7k0gM&SL&S04Y3s*KT%z&iOY4S4(Uv=4~sH= zoaBYMsF_kzT|RRqJ#2+X{ejNq>gm94G4jJCyOFh}gB8C#a`qN^(D(bXE*!vxRc>I% z!^h@m?L>O`u?%yw2*}hn;vdDBZh!&V&yrbwbjP;Bqr6FPwT%Ee_}hj&<&>+Nf!lTLvPEZ~+@%oQ3 zSYGPwLbx6M$Cub%X=!rQSW>%JIFkeAiZz%?P1M~1%5aN$%sL=?BI}c*aV~yi{F}FY z*WJZQS92X3$x*-|G`Jh@5U7{A=oJw3>3Q-slXC;jLy?QtyDQi3x|*1&Ce|u0v6*>= znLTL6#f4zc=a|G2c48>=ej;2TnZTanvagtZTZL+#)iFwbzfq_Qilos-82Pt<>^WDT zASA1obE#DW6z-H-OZSMXXsK;Se_0dBS`6+x1=BjTsu|~kKHk%#bNIDFUmry_pO>6D zZ+hy(M)#y5Y!t2+S$P(>)v^@%0!h4_Liu5rMQ61rMLuFfL6=glQsqi!n5r<)yOG^- z4sJV9G)OP&@Fr}SN>g1vA(vZk>;(0dl&SAA+qLPUT-eVpT(gkh*Uk-hIqdhe;TLkJ z2b`Y1?}G26Wrty`ArOt7L^r!A4DW`GN;??iG6`9+tF5e*aeLIqJ2Kb0u9uHoCk%tN zx|#*eu@ZaNo?<={)=zGP)9OZcdCIRX-;P(HJ?Mm)>(&VvW-D?SK_|y_kUL~?^_k(~ z6$V`Bk~5y3B##%)P*s2mvO!DM#sy6Lf;tUHr|)W4OG5EhND9@JMbYD~gC(}T;4fLl zZ##te#8oJoa#aE=5}YEOsCF>@eli9wt$kIT!YwZ%3wN;kbc4)EL#^Kgtq7Xfgg$XD zNkv%{M8XZRAq5JgaJ1lk3*DZkYP&*E$5O8=~E#**KX?k zh^jE)S6r(W#Dh-@0dedk22l?+l}5(N9NnJi^i{}QJ6$pD~v6=>q7|(k;uXwgjiV{Kh)C-Qo@J} zg*Qq$rK-kj1M1({lT+X;QMvA`BG_rr)}}ih9y&7&+NMg`NqjX{5dUv>*~(j4b`TbO zqM}T*yvrnQtS|ZjUw_A)U@N`1Vn09MoV4bW7r~TA_vj)1<4bPoD*JP;wn|c?$5;srVS9a+ZO%?i_o0xIlLy&akk2L?zU=D zeV|LNH1ZRV7|-78KmD+|5XNk|mqKv1^-%&rMZX`icmq$mbQ1ni*XHF|aM#R2Odf3k za@kvwXx?BRbPxZ5X-GDdovmcYl1p-PU49<@*x_Af$^O-ns&+9XaYCN8Sr6kf9%b+; zY@UkPp2ao8sHhA$psL!7Fifl{c1Oy%By0x2@K<*`q~fxUT=3#Z9=3osgsnZ%=e{mY zV7e5-h?~4*0x>RUa0}{DIaPautM5|Bt)XIa8SKzyOV%pVj2S8e_&SmRXg8OM5|_4` zH6%ox-nu<|1PYh&iyvP3?A&md`SCZWSaw8ZysOW}sXu`?3JH#cmYrLn3gz>d8!=d| z*S?b`KdX*o3Nd~zuFTSE9v<~pWzB=7FoNE@-?mcb3MK&i5)<4wzraOn<>+gR+C0R~ zq$n7LNM}va*9*xl@}52S3zm;YJNtXq^s={0Cx`n__lsU$ZLlPkJ2#bh8CBmUZrz(w z9hZNsd=*X96NBJ04n)1rpSk7R-JIh?GHyUO+?k!MP{@Cts(8KF0JNd8=rc?hH95^~uxg~!K1+~J>0b6L1=4T?s z>k@1c7DB8y9_dppx3X$6#7JJHaUdn`5|i-$bw$_}HQs{J>5q63ys}y8C(N9vfFTyo zyy5I!ZrP96?IwIDpe|UW(R|)+kNSLi+ES*;Xb0%gz3H-p7z=i7)QOBpb##3BM(auR z_+K31Q>c~7;rO|6?SR<7kSjQL6O`tlKz%ZX{Cj1?&6qJj6^CyZb?WD;<-d?#^a=WJ z23(K@%zCxt4|_Jr(O6UyD(KN*=0ss8OaMLc%=In$IWo+pr=$ADDp@JWOa56#qMgN7 zKQ9IT=?AkYb~4a_UQ*ugG{wlJTCSsL7TH1PeyzW$E>OZhpm4D{m?hh{ zGM7fcWyu9xN)nb6Ypxd2{GPaQh6q!OgD&RMzMqu9W%TBxTq<5klw7VNFFbU4n87{m z5+)w_OllU8<`lM^K85Mb-i+)qpZ7o57%#f{q@I8pMfiiw$7qjnHAc^sO0eCMf8cFU zt5A(M(|iZ2D)-5hArXlexw#AK?&SZAfD^Usfw%^aKlzz555FdC$ZL|atjni86k0G( zI7l`@+dJutTKeBN{M8oad~hiaYM4Rpa`RvLZJr$!aAIJGAbYmQ=KGQRtY~7l$0%07 z<(x{B+sWt#4D(9w~RUx{hc3*F!rwPq`>vHYI|3X(oX;{L-!zNfAFZ+e)w zDgx~crGlg;p`q!c1Nt;2W>uv1WCL!-1tsavw@$5zVf(GpZ^q{87ubL0R?GT)YI+gn ztc)YbQ*i$!tlHrPfP#GgkGa2Ey};tNj_86H5-%q|5kXq{rNUc7)f9&!-rJ=9AP?&k zy~v=(7!v(6+xR0lZ3$rEazqQ)vSZ-N_3xZCo`ixb2zji{oYgpN%6FQPUF8hvlJd-sF<7?hF>*GQTb`32IuNi8U;Y0gk9%c1isS@FB=Ia(<~ z60~s%z&XGzE_H*y>0?KHXqo3Ts2!VSInExlPJNw$?`rsnpI*n8oKYj3yk;&w?(L)) z(`|&tOG)6^jHG5#oh02eT zQ&#fP#}N`@+68wx&6cdm*saN=dLKrT71>s$%hMHaR@`2(WyRErJLBsb4~i3Juyc{9 z1`q1X?b??+go$tRug5*<@HJew`pZN+mcc{v<-zZ(tn?giG;X8=4qG=8!bAEMJ@_(z z5*5K^K}SeP$@K$?iu(Ew$k+UN1^DG)) z8K>QekqMaK$jlJPU3L;x(s?=83Wyho;cEgPkCD}&@&9Lyqp2ve!3T4y8SB;2Cg8a#Y_LToMHuNf{rbx^2 zz4NSupvKc>^{DZ-r9&P7@xau|NAGKX_!4n;Lx`1t5jiu-pF-pb)h|J~Yl%v*QAc8L z(89_zejOisusGuSp9fNfiOYW_zS3^5KYU28(Id5iueO?=fVi$IAFb1;J?+zGLmavm z*lSXdPQOougX{8=*BGQ143V8I8Ef54v*Pe>{gzEeeu^9>f;4m!F46Yn2O_0Sf=O#` z5J+05Z~=x|DM=b;*=|IKv06AqKJ%8Q+<6X}Z5eJ`;l2Xc&G(}f`;6mF?kq?C#%UT#nM0OtNr00PN@l>F+LTY(NHRDHCo>QWZO(5h5muJ31#4ouyp8GP zcf|k=3V}+59C-UK`34;g$nnh z?!JeH#+M_(T>}wkBfBaVEG$+K>e z%3g1v`L)8W*)hUK%vCJTYT(6<93X1eaB5*BcM3=NX0$}U`d2#~a=7@`4`W7Fo%T32 ztM4gXyV&iS+Nzv+pcFpPBRFjrnbzHLX@|N&LApi{rQx zvT9Fu&I{i>KE(99Ry*Qq0)Px?Jz|F)6D=AWlt(5UPK|~fYl)eS<+=7a7WtH=VfLaY_W`;IboPn4=9E_7?J_H{ zWP&hCE{~y3s5K007QXyknfn3S;Uz>R>o=P=?KW%RuJa_aTUY;#Us%{PHMG4MS8m#4 z&UIy=C1BmtYp?7y|DF%uj(pqEXFIlRN769PEnaW2_HrmvV)5F%GHIVwQRwX`eD-5B zO&mymQsF)>sI4JBr^B9mJ?b8|r;PPztWb@hR0Vhy)kpV98l8I-K~hl&cEDTX9E6M*{8M4W!et0-jLY-uqjNYhr>6g z+Cc}TZ%4+_QMpOD_vF(6PlqG`4pcXRvQ84r%Hw?wVM=cTvwSH%Wj4B%eK1LqgyQ1% zEjz_O9ssJay#B!0YN61HA#YLx$C|O*t{H* z!Z?xi6Qs7%Z8|~e^o@AePO&uz6*qfx2s1@5Ai9yxoNr3HL%9{)r-L7zT2YlIMy1pS zf@eKj59d{PB`M&nlj-B-7`UXXW+f;XJcad;d`{>{>+h&hPW@bUDtdyOUoZmGZk5d0 z;_$sZ_sUNm>c-AIbRygnjuhU{_*#gjZq3=5D7rN9yU@d6TNnE*9qGJI@yDG#Jg>~~ zd4eD%Yd8n1PUD}w^C?FS;mvS9d`(T8nx@Wlj!X~O zLkLfXCC|SPV~3r2-}Zf#!jNNvv*kmM^mPN!<9B;{s<3OKbKk=tf#OF*>0{?PO&*ruM$X*;f?U9;6?Ugkf=OB5f-!pAAQTYdEXw~nx9LX6qTQ$jN>-_aP zZH#u{&wd^YovDF?sFVM8nB3lEr=?;L&W9lMsdeYk{Ck-I{_@oo^Vc;Uy+h*QlG%Km zK#kG`{p`l8{09-{$7MQ24}cSe`KdanaO`Ou7;2$jl42e+?`+zGj$xABV4PWYj91v zvTq%US;kp>pRLlB=&f_vKl0i<&ya?N@oP3z!Sj+4GQ`!3`ox}g0nn_YRxBQdp?f?Er4dNzW)nq|`rWH{v%!2$~=4`n!LS<9j>2EAk zWU39(OGY@D$cFsck|SqCHbm~@kFrBBq6KP06jZSaI+RZzd$V1qh_G92(N~ZD(i$wy zEg9vZYy)8{_Fo|eO%JJ~^aW4wpwCcFMSle`H!wFSr#mgp_p|#5Z z#~xdRTDky>WbqhBQfxeVJR-5*5Jh$z0dZ_`f~+y$@z#W2sLCmXX$Go#;iHXB(-y%u zFhaKGv`E~X{P0qAQ|{7)`})1g_F}YdpVm@{ITM7G8HXt_-U;6_rz@FHdZTtx94SH7F$?7*TFN9$yU2X9ehy%^Zy=fbS_z( z`yl^$oPBVGZUf|H#^v%tD93i=-BybxpoO>F6?F@3>clq@R86ECqR-~ zs6>+MRK9$Kis<@z@qm#nuTS{1@gDJ+VYmdvoAO9*U$mi}qC@cEupUD8ztAMNLm$^Z zr+mN%A`mv8sM&u^XzGdB-IVR`bm(tUz3_~?Se9gw&nikDuMbhTG6`hON zoZ(fXw9d2PE)@GN{iGeTa+qZKs+uT1(Xnp=#Aq!L2&HUUQX6L8)WHmP1|X2RZ!@2G z!9&wwkdo+Oc5$#Z1Wizs&tx*t{HilH5a^NvaAk3;kOV$@AsXH?-(H}gr?(EL?Gj8p!8Qda8 znb_)Gb>!wSsILGxV#Y*|sI8o9YTBvxpCmsD^X*I~&pdMvJMQ#k$BvLPyhj&>cgUvm zYI8ViC8!B;9Nk5HZ>2})2$tLMUCns!Q9T=;nc~?$)=c7Sp$*XssZ;dFZADE3Og+IAyTMzwn(%vnz$tL>C^uP)V}#%D4u;E$IHT#jN=r6 z=eVVLUcfA`U#d#Mcm9u;+E30VF?)U^2f!({#it4T#N{aMP_$*?am^}{a`3p+>6d}W zudlcKJ5LP;GMl_^i*}~ZH*Sifa(Cb_2ah^YoshfHoP$XMe-U)RJz{53K%Nfpib7%s zfK@V^WV-)_sPPv@*CM-z{INz^Cn@=d+GIFJF`KAUZcRt%n*6ZRdH zx`)#u-+5-)(ne6Qpzu4fmTTvmjj)@;(fgSb_5sP#Z1+>xhh~h=W_qO8r?c4~`6kpx zjQ09ZmPb_VnU&2FqZ3L%aL@S^w}^1_!mb^mHZi9I%HbnSc0-uvD7#H4kxpNa$j!+* zJPhjS?p7Pg+C(~%e&*mwGMYTIuyB6~9SD#7SMpgmxUUbWi6lH~#-RTr$Ebj7iLESE zUb<=Cb`njR{DO3j=EN}KCe#Qt-z#d;#8@1IV)Z-?T=x66fvg%~&- znTYJS{J7_by2lracagM8j`nayz_^}mdD;5c%w_yx%qJ_x#Q8fRhkFuE6$lLhi#*hv z_7t1*dgpJrBYduVkwwuvpxuTOFggze58SvFMcaWklujDB%`#31z5{$a{%d}cELgk~ z7L{ex{mEoMYMLYF`creuwoYel@}r0a)+~PWGh0h5xS=`aSQ8AsHL`SKBMNIi6@5sYbUZP2^lS5VWYq%zn73e(TwSArbm8#>I6RuZb!`C4l;F^xS)KoOQNWku!8c! zNmCG0SsYpOU*W{Bvk@pBsV_B*gBsSpzWW%-ZY*S1A4KzbY4Gi!b);H@<;Xlo(RM66 zGrXu58LXf+BHMe#!0PfS`uj7?>U>>U-D82d=~xOnk-eVz(_s**z2nVsTY%f>mOczd z^l6%VRQL+uN#uydKX4P*AvE*pm1!u<8es9lF{Cv%M2_$+k{Fto6I`DNg!Pn&!abQ& zTeAUPHls}Wvpu`PyuI>5oVllu+inRjbexMA8F8GTQNpmuaQegQnlZIbk0P#o2z}ij zwf|C@;1fJfZ}5td91^7kbpC^mA(6z&zrwctD{a~Pt7j6> zaL}8OLT*8a_VJMdd5YU+gX+0bR5Kxjz9t`<+(1}iI$3B-q$8ZqrsNU~W{t<4G2c~0 zVOjI31?>L58SdR3x*Olur7Ci|KvE-OKlg%#Y@huSdP!`RIiIPny))(lOpomC>Qg4& zgCbay_VaWVy}Td_}Krt;>C}XL$hhG1*RYSuw@r4_cWpN{{PfDhCgO(;R9YnwUEL*@k1JVr`$=CU~{O11M5KSXU`KUC0rGuE)_#+ zr5)|LBWpofKL$AoN88NKc}XQ~_;ASHxt_``fqz-?iBPw0+gedKROP&|Zio`KV))MJ z%!9qP-L0N}tw!@zp<98*L7?7!5j|+@;uNnYZ)bUSYZwa*$&4N}p5;#9)HwD1K~3i3 z6xOnl*>@I1r3^;pkG$_e&)Vt%ju^Yohwcz^?TycjKx4i8ZtiqfIDOVK4&f&D4rT`0 zwiMsI1n8*0W=!02Pj>1Kp*uA0PPqMGv41$1WraIdRB)gN*WHz4MT6?TEJt`y?QA)Y z^AHBlCk7^qyu?{r{H7qOh^8h&#@OyELUI8?cqm4y98^lS2qnmAlcY_7hubq7dD2ZJ-!#h z#EKR!Ocv69*w_z*>G}d_*&BR89o=m@(U9a|8+xc@ExG4go4FpWooJr@1?k1Pw{|6M7bpm+g<`FsmTVKmxVL^?tHLsam7gr?B!`ZTy2Rvg1W_ zfM&uc@AJhQPIorFaHO&wSM~dT#Zk(pO~t4^$r*3NL}j|QA}#u*yLlLo`2q?421oPC z!%lAt3@4*_E7xT-&YwH)m>VCs9~24U0!J^pKQXLMtkchV>(`C(e=N58u3|wAy;zYD zeqmVs9rr(bsOolw);+=>haU2u5G{mr9!o2pvuI2 z2ItLRTJqvbLLlx{0(r2FXHlce01 znSE{<8s|4(7~2WarPRz+=k-OS5Ap4zgU>iYD*yb>`P371iyZhaw$9j3{e%U3Bx*#; zf+-BPVmNF6u!TMT-N%C#QU~DG^7pA!eDDW!jd{AALCFrb%=i;bSh~L_rNAP!-y%AP z@dhq7adwgfU*mOzVI#K#XJqY#Dbc?lbtT?jB?Ysj`Q8RpBN)iesdmTXB4lL+&pB3| z2IrHm4vf_ahE$_YGyeQorNHppJwnjz+KL3Y>$wgDwtGd6z-wN^C%uJGZZ3Q>6;nN6 zQW(z(Q+`tg%soy<$Cn?k?kvj7bm+C~*evM{PoITc-o|?O;I}??>GI|$9zKa8;5H*C zby2^swHhfIUn!>a5!$&Et8Hl<=MA4d(Zw}uu~SlaoEOeq#v_Z<4o^9JlCpK}J9B1d z&>#UcfV*mGDoQT!Aad0;xy0-(x6Rm0PX{^ZI#|B;E)|d+YRL|nJD~XP1TCi2az1pRzl59QZ7HjX#qak_4pD51q+yACHri3&shwfLPcRD|$wI=f5L)q@??a`_j{u(TJMXe47lb)AP@5 zVQ2$v!;m>`PtjxsIcY1&arme%&F^zjoJpozr6vOLbfaC<_Bk}Y>>g#Te_7h|QYC`& z`P2GSpw2q{^GoM!R}`vb%1sTq%z{2#dw6h-V+%|7vu&#^5zXft)!PdF>j3tKWiF`_ z*;DH5eu1N0aRj^$B7GXi4dPg%e@AU8*AL31uQ1www!!H(#VKC^ZsD1IcywqsjW4nJ zj+cS`2XWFbn1r>5$0RRJ7*lONU-k7k=EZ{=gpC}fCj;`43&tl>E0-$Oea{sj6sAo1 zHWJ}R*OispCs7-Hgl$_t%Q2X4zS4J9Oy7*r{rhF%GbZAS{qt55ShIZ7bevabv3{hv zF^w=K#rUZ!Va*u!nr$*p(3MmNZ*i|MB%h<-p%K%tNBV0Ym!eZ~cRx_n?ad`jqZ zGs;<~$Le4IdY^y~)b5=Yeoqr9*u>j!P;^ilP>+HS5?1c&8tub3MDzIya}2xbbr*R! z6dYC+eV-SW@#b`&+L~qJ?qoMkXVb8wyBeZF)RuX~+b^^@;NHuo1dfdk=D{nFO)tT^ z(X#0qNYcMu9P#%00nN04!vX?aC3EzIxWmxAS-d{Nt@=k~VPE zp0tlY{?8{->tx$&3~RW~kJFi~<&DbuJF#GTqby8Es|~{@E_AR05XtAQrj8+U(CX$S zT<0Zjk=*pXPd_<8pRS_fPA_~GjSFwfkKBi@pcxnV-|Phz&8Y<8WZcMdrogzKq`~e= zyIOLAsL{a3-Wga{85p<|H6HW-)99JsnFJR*jfxi8+8_#0zUb+!>~(#b-ScA#EM4>d z&rcsW5F00ZsS{WO#$z;}5KT5n8Xd0o(0@`AcoZ^I5I|cdqkGLL@69A29r?Oucl`GY zb>1)P#uxSQ7xlg;`Q3+cQr2~NYjJi?tdz-id8;aT?5)(K^X}@w#)c-6(9X5xj%T~~ zeq&FnHtW<%mcbbE+3u{v-K?;)vwJOOr+*0u9vYZ8a$l)302-cK1tQqw)Kcgl_IqZv zV@|nluG!`-Z9n`MjHs43ZvZTA>eNd|H2_5IdskgFD(t3qCQ za9=H}x|M2NR)aC@q4OpNs`Ji0BI?(Ru7FGSRdlG&&poQ+84Z1kuAPXrgU~%X?lE+U zW8RbKuFk8b8uU<+a0+B|l?UmvihhSGQUxvUjTzjZdR(Q=N`R3c@vRm5{I{YZC>8i z>%wT<+PA4S+x?`Hj_wR2wW`Fnm90xlo?w!0rY|Y0iva~FZq7Bg%IP+hScuWUzYCeFlT$)(H8$qbu6g0K?x{hUltcQluoYmipB{0|io)}R@Xe~W8(}%r zobk;+S5@X>2v)DFbTJd2mLX$Y(XBk$AKKMtmXH7 z3*y)6MSbY&psq50kyr9On;}?p@$%+{Nqf^Cc+vgQn!)?@mU-`FM!$EK`Q$|X0c3Y# zFuxymezdas;Vk7XeBTGQuQn$*->hYw+CU+<)VdL=O09cF)qO>U_e*Nq|BGRU^%?4a z*0;C@brTLegFX@VdiVtWqVv>T@YLOO#P#dJ3d?;0u4YiL_%z4)knPL1eU-Pb#Gxza zzJ!=nL{2jI@Fm1z3bLviOh3H%RpPz%bz)M+F>?`^tCU2@zZNP_rr?b<6j|QBB!@1l zut5?nW+M(#)37Wnx#LxGq0e~o`XelPN^})DFyg|~%BNtP8wYgBi1M^??8hF3ow)J} zTqI9GEVvj~DT9Mvd~eg$-3EI|0#gDk^xpMuU)m?HgtLAM$o<>4{V}I;!?S*k7ya0} z=u575bzyH@w}o#Y=yq)JuwN$lJCkL?ASZafF#vkFAGcC;W$sY*6>E?^HFI+b0=;IP z6b1aCgnsh{Y0JHtX@c1mbxo{?iED7b5)5caZp((u9Fv41+9t{%+g_hbw+$%%wIzTN zDGuU49m@u&#rMo zyVL7H73wso@qt+qG=j6J+6*{K)v^X+_XImi#)jh6ZFT$dFAjb1xq1L&;cyP(#EByN z4Z_P_n8gm0CB$)jP1gftg{V3&s??r{KQt0Ei+={6$j1`wOj-AwbbrRStb6No(wGZR zGU99~d9*BS;5ajPE+@Bwsk!b;&D$SBxJdV%XY>U^0==G#Txb`}Oz#nB&uH zC$o-cRLRBfwQ}h(mFnziwuRNxvX)*9!dmb=X9R{q`lTGDl9jAs*E8g2pzB(QCu_!D z=>A66y?Y|a8w4j<>vT1B&8_KrV<>uiz_Q&QtLab%+w|@cYnrg=>^~*!wCe=<0=R1H zyZ6UxY1P^Y?18cgGmu_pIGowPx_|@8X@=-+C$2&;4cOz?O)n$-zaf{4A#?T;+2uL; z@>vPljSTdYS3x_I}J5Oj(5WyaXoNjCe?T%b#p+{oWR2((nsA*yanu1mHre-JrM z>B*(={}Hb8D_!U2r-;JbeSV>h?MGxjXW&?j-M!APa1W&}!*LdaMc8a-4D@pKNH21? z`X*tqk>WR}D6h{EeND{%%+yk?7?CX6gepca{v{^(dhaJku`P3@!48`*WPwNon`;hl zpOftMQNsxi?$fteTX*i`b}Sgr@fb> zd3JY`XmJme(wgXJ!)_RTsNb2y^n<4Tmbl~Q2Z+~fRvHn#P+K|{BuhDdojZx78h47= zV5<;PefMWPa8$>E*g1H-ey-BwMEf~^)1tZ7HJHt3kKFkRLNFbwS+J>d|6$wJ{Teg2 z-k!OuTN*(;_E9U(7XK~Xk$~-gdjhAt{noj-<~7l1U0Rv@vvX1N%YZ(Bn*X#YvrRCC zPz#!`$TqIkq6F!Z$jQ2+qqA~JLL-5euxTC@$OM)C7_=a!I$Q~wRfD)-mO5K1i73`3~^ctNI@A#WY_Wq0YK>Hg=cA-B+Cn=)f-BHCRY~ z!OU`y&p-dF@nh*Xzd8K%!CiiG6a$x8`wRh3T5&l*RX}s2F}8klr8UzS%2v9^tf5I^7sr~MxLxBAp&RMmVPlr$MB5{9qV_L zSdvLwJPXCKl&QNKAmk`&rcKil>og5x@n%WL_GJOrodB!F zVvy;7k5Xxe-zSO`^NdVuX|84CySN($;1T`+uZ$$ zv^f0^D5#V;!lUu>fVCd+uU8ytAa$--!m8lIGh?>yR_joYxRdTK;p|nSJjI0>uOQTs z(H`Juw_Zx3?yS}PRRo%&3;UEfx^Si*C0HIr()JWg(u2^pH)(a^mHE*-_mw8|h_ss5 zugdmZ%%KGJ@${}!>%i@f9O;@mObql7Jx`D5v)@|=)kji02b%71d-pV?8XHHzCfkfJ zT&s&qmtRl@LJMalGr(MSwQ7o@XosbDhJ@<4%6L38_03DwRepD+{c#kX`ldZH zb%A8{u~1zxSKo?|+q(y77v$ zY?f&28KaGIIvy)wd`6nOy!pbo?%#XmJms@a^A^vc;{3r+{E}{L(H)kX&H;|Tg^TzD~?~a5s9IB-t1J4V8#B>%Q92F7bo*bUMtn= z5$hXkx_yfa^@}h0_JfnwPeDyc}uT%=U9pbb|^I;*ugs?SQAisH_ejGVy_FQ`Lc56;8^ zGmUctLZ+IjRQOc9Ma*M|&ISq;Q6+w#1&=nT7>CY}WrYJb7zfi;u%K{SC`_kIjyzT$ zLT#|vuvsd?14*LCw-Dd<+VfXnDoGbqyW^JBPnTZuan zr`1F7z?S>)o+%@&l#M^%D~eMVY3ON*8^>jotm$BTjgBqLFXls-(U7@U(M;+5sw!Fj z>MkFubaKdX9Nn7z@>8$tbCbfJwQE)%G#6fz@}q`wOdLua>DfRNs<50@ zU5V_Z<*{ubYxBT|uAbmC!yVL}P*k%CQ@ZNVj0!pG4JYNeUGgBg55)#<=u>*$kfSSp zBt&0rS~$t}VScJ-V{>ek-va@q)5=ub}HA#bq7*9%&1>0F?83cCJNSJ@Rk>OeX| zgB!k4KOY-1sAOhvj8xA-N}F%Brvlqwb!qaG2@9X5Vk~$*)pbCiB-o*`cKg-pSP$MW zLdXLq9K%Mk8IR}_+1QqKtLS&z|3jW$o*w%BhgP1p@tL?hOEop_)j_-JxUl3h!*d^ z2b&c;mKlPOrj+mRc1jAJs{d#zqqt3n;4ZI#Q&YaueHi<0l-w4krb`WUk`?Xyh5J0q z_NGkF?M`FHVxs!k<{gq8n}T0x`SSzH9+*>1{n1=Vz7~b@G8r}A;mPqHi(;nLu$Nni z>HfozyJobYkiDyWYWop->{fx<%js+9M*o_hcHV;IXLZ7npL=*=X z$6Id;n0UyzgXEe`v19+o5hI=bp1jrV(sU1`1^Ae+_ju3U=cPe33S~H!(==JvEmt(x zO|JW}f2IyXX%RGGiuC$elb(*MU`l>zwii)(u|(rJG&Xv0@%=FZ?H(2D&TJx`^gesy zZIlEJ!T?vL(ty4fSMo*1&|__5=}n_@4uTKSn)410Jyl#tiI&2Khg_G#ACO2@yUpI1 zi(=i6fCO(%_K1#|l>!mcr|Xp_zbo$1x{V&cVETP_nMM%`(kG|M@8x&r`uzIk?fPZH zwS=-QUtcri|JGNz1mq{hR#s*wL`f?-sIKA;3Qrsb8uE#(USj;3LYp>uNN7u36aE8b za{rPScvV!xL*vSk*wHCTyeBBYqMp_%p*A~XxUwZ~t^IQw!bEj~2o1zXj-4#2|+e~beVfd+EQWQ)ghFX!nK+~pV4IVYMgDSB7NeYn7! ztvCff2VPjuIS0OZ?)ZNIc3QDLv54tBu2dLLvvIc~1WNB3!@b zFq+FWIZtVz3TE>=H=>T7=$bw?e|(I>2sw#i$@o57%lA0!wvCAhP- zESqlPnK3S18`sBPiTHJ(Y%~J=|@k^JJG+#6UcDAJvo^HxnlHHU( zEj?T2*zvlumx-LZH3ATv+;M)r5i=+v?qhQWaTc@RO=)YEcu8o89h}mKiNY0N3MM|p zrZyZaYEE$~QSFJ|>Pz!~qyr&^%x-Q7JmJ~cMr2L*M}!<`rr@f}?cu^uxxY1Ny;5?- z`Uir$#94np{{Pf6?_aUTR!G|RzAQ#3Ud+kZFHC6kb25Nv+Z|Fp?wLsyNmK^9T>1M0 z=vbB+PnZ-R^0R7H;1yKN4mDe-*av5rUj+&5H5ZC&I6m8=%Bughf zljfIG+m?In`Qs6_mcHvr%@08N?euu{co*Io9O-p^4WGw&*(I+uP&f+>K z3#gA)Gof2~GW~Fjsqj;v(T1T$O^`eVv6Ui+!#iVWf_4e^uz&6Y=unG4q>w(`14i5G zAMT}+04;vCbPfCfig3lGX%Y9WsMYYRxNXR1LWa9UIxl9da9;=LsoARh%A2EHjr_q> zS-tQ%5oUiQ@q819t9-s4djchuTjfvcUE&WoR4#*7^18tu?I=#+@=}@vHnx?PE7inp zBH<_DUnjH)$s3_=qvURLzi5?_{V{7)sOSe_?#(4pF3Gha%2=eVr6z@B#bv z1r_80L&jm9zXB$51$vM1x?e0Htd|eyT0wzXizbk#U%~tWL=h0F!w|4t(3q^?aHSA; zeOjvF!|31Y(}t0dzVYxYzZ2lmJ%JDYFcasM(6g?zU zjWok=H}Kqh{aIAS{c#r}7MRh;jEtT|LI6|3=1}v#(B?U?N4x}JOWgcVDR8kt$@HO* z6u?_PSS;g&X0jSU7d_;NkqN-Q0Y9oowI2Q}bK!Ut6E9)W*ym}o>xEk*FQ@(5Z-33H zUH_csr>plY?-X59?Gv)%qe$}Swf_0H{iDdAf7brL{F=q0?sBTi8wAN763H!rnA72D zQ%mJ+%G}E-qo+UcR0a{DR*4^jYvouedmG%h{^Kd7LN%1 zh~^w`u0=mXY{}qQW{QQGix~suRKhjgDxVNJV^Yv58I6*)BGAv`mv2A5DMiwia4g^#ng~Tk!_;MuQFhU~uJq zS=1j)9u0$>TG755#b{+TfjMhZ(rXSVrE!PV&=?5%_+KUSfiomaCB=24QDi% z{9Z2vm?E~IWVmJsMI4MwdoAr2P>Q6qmCPk93=2Ou^WHOKHqpaBu>sc(1FwD-n;axZ z5tKvx)aPm1w5Fymm)Row>h|9&<8&1Y>+Yim213Fp)0r_;W7BcN+PO_RXX@JM~4Ae#x8@yKL;2fmv5 zppm+jWs>|2=~Ev&TmxVtcLPn-4UmuXR;eb#117F;pt1aF{A>eg@Lja^KH%UcbD zifb4AtbLh}zP(8e_>$%rTtjI?yKF6nJ{k~D61pBWV*O73SX)bqtigXv+37>3TWNbx zD|S!eOWacpQbU&YGQk$sT%Q{?30cP7!d&7G7)f1wXHxf$yN=U)N9m>yN!>fKbxdo= z` zK%-=47L1h9@8aj$S|Hh2X)PjdtSjh6L`=M7)H$BBnd~;|a`{aO47jCNczU^Epb?A( zJHZ{H+Ntf!%ffliS$9f;*cTnDoZS)_wy{?c44v}ji!g?VH>V~J>X9;Z2Q87ExR{FB z$%P)tXI6sjpjBKxDagkg!65o@H3I;W!s(K|OC~awQ}0y{4BAffdw*q^j=yix?4#P|83JGdC zY~1-BX5A*|Kmi^Mcr^t=jHo#Mf+(mzA)?CV6%DAM`(gocVt9y|DV?f%F3q(e^(-}Q zJJ7S2MiL`c@^`bH2EC=X-DlzVq+S2*)TTR-r^Ap0Z{DlAQ`|IcgQoAcF&)l z`6P#+h2c1)Q9aB{M$SSh7&i-CpC)E5 z0cd8h=ueJaH$oNdO1SAYM#SA-~TF?iY%9ZL-i>?ZG0a)#Amt1*4%aJ#hRJV`b9+$74? z*=Fs$xy5QI_R(ey+A4#Be6T3)mNPFF^vM^~)$A^J++LVOTrZBV_Nsbxj@KYv3K&c! zS>9@d2`!kn^A{FqFF3OZ{TXazabgkLj529&*`TC(x%{z!Jle5^EXX9VSG@~<1lnL9 zhuE3weh);0%!-)+ZWZ?7xA&z+o+#IsxNUMs_)HICBLgq%&@Qn&Sm>65WS1F!Q&Lq3 z!mjD+7Z_I(c<~R$K@G1HQ-wy4gPzYoMRkHp5x<-9xh8$3l*2As86g;+t7*Jj5FkdA| zw*UpS3#Z4lbvR+x5)iX3dwM93Kl!GRX<5Iyi}@D z%%rkRcOtWfa&|RX%8u=PZq=0%ozH}@=|8jQ*CwTtU5Gg>41&OY+)5@jY^;9lNn0b<07 zFqW*LXD=I-7v#Y03ktwPI4uiAHiSf2$!-tgyHCJp{YzF8+rjST#1%ur zqC{2@j{0Yj1Qiuyo{1Q_wO1lppY$x3n(o9`#A)2^beOb#RF@Ag^$`HLF0k9TML>*% zIO8LxXdpHHU@@9DH$2Hxw8$qsn{=l}L%iqEDi~{0Cex>w(RZtRqY74_)I;N*CP~Ps zr(UqPl#B8+a)Aj4CcpuIb=6o@j(LrZNyC2XAMxu<_fxA58s0MaRYknK&c@M|mBD*% zpaQWCA5^D3=QE8Hj55n+y{|Zf`o$3gCREcqS>lQ5OtOT(#4!gki;rKkM-rExDr=sJ z>zSp1-iJFn!-SzFG^^6}YA{?VPxoVTiGmLU9zi|;jk6Pf)QIr;YynHLc6K}uswvOu zFe1MybS(=bBYq)Jp4H4)V}YvLywiwBCd+$8@is4bXZO4C_-RpF3BjWEYx+c0Lz~6D z&UymNP#0mTW*KoyBI==Sx-w2XmO}!}C2~*{=kTav@_#I4QFOV&wql;f6)rR`Tw9#- zQo_6uZN6M}PXJnncZ%4nL+8z7maXEEUItg!WF>J{KsJ=l(l_&R`7O-6w$eU zvNF?S{9|meUg*J&CNYfJeqE~hpb}GLRkj+W+Hk4wKJZw(uIS2s5=_8GV>ZnHAagG+ z=`EU`2ouqV`p6K>P{{R*vJO|=3t4U`t7Lsi{xV$-!L@oFAbT@7LVIA+b9Hh9kF22~ zQ&rY_yi$=8y@%rmfeVy3rTQ301digb=GHVaf_Q^AjWLz>djpQb30qbq6U)#?g9P+_ zhf8286jQP^2^R4F=qIQOCqqfF(^FNK!58p8_?sgL{D#o<-??q*a_$)x{5Rn{kNT30 zmw3{#sDQ~@Xmj=0myzvaAtQ1t3_t9UGobXbH*bDYiSTb{!FhcW;|ydwM*^9Zu4K}d z)7)&>^#ZVZP9UJFUrv0j8feJMsQcfOMu$)l^$CAn5%mQ~J(q(hx$?jJ%kWA{HgF#` z>{_ZJ_}hM>vic)3AlCA|k#l-gVNwT_cKFBFD$#0azc8EZh&9L8moc@Sgb8q^CE=m) zBF(WmXBNVgS1kijx^rKggszc`un3qkp`5EqapJQgijU;e4ucW!s}(-C4#Z%D)-vu~ zlOh6?zt(805SIbTapa#Y(D;sje)?anaZp4URV zUswA}lVW_q~e0|zrF!6TzZHM(IUXjoc8vVJ{dxauQzjGwJ$E^O%c=__-qymdlQ zYeg!Ah`IB_{H!TP)Y8~=wi72HzVf)pkN)^eio4(-#WYsO_KJ9S=ybDZQY*;@2;38# zmeETtUIvoDSaBS^N=EWk3QR?=QK?>lUp1hK(-;gCsi(O2pVM6I(Vq?$BB8l^{bGRnSj1msKR&2r z{931KAH4YMk%b=hWX){j$q@SWD2e>2vyPOi6H_8SD1}(d+Bm)@nzs*LdNrbybV@Qi zyTPJf2=XP&CvD3;{;EE{M)QLc2-G{-kP>|DGoP<=q9+Z}OFPG=0bykn=sk*O3FW`w zv1?!K=;$^wk^|*|5tl`=71G&x-{)?_@g?@cqnSHWeM_i(ltTQsYZdiRQ8?n*ek&+P z9y`XqIqDvYsDJ5m_QPDdk@X+rP#r3GiUMPAAS z0Jlo5&aPMog`A7DLL<-c(f~VHS1`NGWQ@xc+6>3KzB|5AY(X&uBM9XjK+<;%8A zozau{^+D{cd|q36`&lRjHqP7Jr{H;WfaD5fD652cQlER zIs+Jc>DtQz)qbxX671~FsY%SkS+^L`I zTddNwUZX$ZGEN+!?NZ^*VynN?j9(q<)w~ns#E|~UQ^NgK2Yd#rtq1Mp%f2B_mW|KV zzYQ>Ee3rtL49ntK;X46@<`k!72EXcn)uwiU(T?I9KGWz?f*%S_i^s4w0$M@Ez(Qg~5ndL{*9?Aq5aZX1`=%LdA+}^kW@A6P!gXL_q{Z z+wz*li%q*rEn=B#D<~KdeP1NMafH5{I2s;mCaAwXM7)qKUMg8$`df_ni*SKr+ky{} z-*V}Nzx)9BV$vROmBRMxJrWQ9XMPQ7U?`Uk=dZx-KeFpE1nSTbz=L@Xst30v?EJ!O zquq>6*mDI?cCO4Q^beHynPG=u%E1CIa8FfeaFbnIx5c7y2N})sNCx5BlIVm`;$1K= z1Jx#s?}6&?R>g)JuH}444KQ{Kn9YOL*DPc*1%0K9Ib60*N3t5N)8Y3vy8(?(U0S48 zHnt`n1dJp5(~L{&UV3(y7r$xxQ*(wq=Hu?9TqeFziT`Dl0b;0a8Gu`@24in_S^}Wh z(0GL-H@qrl$R-F$KP(S@w9Gct#NI&rLsp+|X&34XebDP7h9w9N+|V1g+&7ao%3E3wQ>C3dRsDY7N$!lyOc$^cl{ zh}(Rk+E(&%i<{(5|HGZ3-=((ii8g$DY>y(*VW1nRSrlI-Trkb8OfePT2jJ!+$o{rX zvf)ytIh_k3tNf@UJ?2Q0>m`>3D2D(bubY7o;Crg=J6HW~Yx`c+<{MJhe2-KBRY0o0 zf4Ay)rpj=5;g1EYN|DsT@lo8cReM-EU#KIXOfSn^q!Ch^lOz^C2;YS}Q#aj)qYN!wbe`2%OB0M^@xJ z%GmiuWoV{sV?9IIi+of<@v zzqBn+#`ymK46MfcRWYY7DR+PmV?S7J(swD9_4yYSTk0z1i?Ah7{Hxz`o0EeEu; z6yk zQFgP})4~iRMOy(I<<(lv@>9&AqDL;cmf(Rkr!@!>S&h!cZ#2JQRz}Rhr{Y;Tak}w^ zF$z5FgyO)MCuTta%wA9m)7V5E#|*jmvM&B>7v_P2KoF3|~l^W_Qaw zoAIG%EJNDd@L|=W?P{Tp#rAx8_mZghDI&N@ba&+oCpw+>5}=Ke8i)3n^=V{x7{a~s zQ|9RHgFha2AGBWVp7xb(mJi8~IIPa)8&6!%F%!+e*kATmzU&4^*Bc0y;fRQF0Kz)p zx&ly9X%<#$*X|2!tF$foP43M2CX!mxRyL=jM&!CC6X( zlL%}EQ&kKjb-J#os(Xxt9h*uNR!f6V>j!N@3Oz{J=-?zLWjIA5Ym;X9Tq}6yK~F%X zPL|sIRTD3<(!6~|15ry~cAM`q zedcBh9V@im@7~0pfFx^3HK&sc4pgy|$?NlkM`{@4Z{TVtUkzP;X;>&KG=nLsTX|EP zv-TEa47$<|oXdk>z0)0SaJ-;YJYlq8q3jp<(xdh>cT!1is#iA@h?8|@2pJ|2+5|Tb zEO8nSB3sbR!y&#|waF_!2ijc8KZlP+hDb)6blorE&=isW-*P3KE|6)#QgsTz^6E=g+bqL9T`JeYb}Kl@u8mlJ5*zUX@}P3Qv_X{*FuNp`N^atJ9QIogmiG2@edN=UsH@)dUU$+e2um#T_F()RTo> z{7YKzwFKW!e`^%#SR{PJKJc@+;7G^5$BlN!OLoCWqur0S6i4&p4eB?o*x5i^@Z=^% z?d=@%h+@0HW_&t|cW^h@E5~%i8rmlpTmqZzk17tK0#`!H*@eHzg=WkK0**r>v03>* zSIbIM_MeGa)ya%O1x`0dVp97`x%-V^c4wYR zFA0sG`g;db0p%@JST^|Py&T&S&QgEuIFBQl!rRN#0Qc>_d}ztUj`?N?Ddv$4i-aX> z4Mr-a$Zj7l@|glC6Ts&bL<$8XqHc5X%0NQLlr&o(BrZn)%Ra~?x4!O0x#(Mo7ac2d zIkc>Ftj}?9glt=&J1xWtX!RRdqm5-*iC>m50K##vtvI*2go;}Avzk?4pmHF>6^!51 zJ2)gSA$BZ@-3mi=YB&ee5Ya4w)vx;8;F+k7!uWdRp+AjpHO>2|wXMU|vkA4F4-~lMoGDb}td3Zmah%~i zX~@6%HUr!@XCA^ggGjyEI1X=?gXpFYg_|=4O`A6QY!*Vx=7S3uHtQp}X2T$@>BFz) z>>#YB4HlYj`;a>`gr}JgR#?o`xR?0=#141KnpB zm_3^ZM$h_~=Xs*w%`=Wko~7vH*(|nrmLhnkEg!GNnAT|ngyYPiCTH`I!07|}W*j9q z%lpDL>2h7OU9Kj{mcPk307p~%6UW$>NZ&N}C8RfvTmbY^|MGzLSrd~SifQ;$HvbUz zSrK^+*rjf6zx57D8D{HD=xgWJa%!P7n|ET|dD3YiXVnV7vap+haEphT3@<;dYC@+& zF|~Zk)^xa`tYFw7ON%jEIk#wt7KEU%fkpL#*g8%A%{W#CF3?=9fvK=ET9wJahOkaP zbBl(EP+BH1IihB4jhc2e#HIkZ#ha-uFb24qYA9QWX5(k_@HB&+UqHEzyduyHlM+h6 zGwcframP4bZOp5so;OamYd*R3-8$B}j-RnYA8ye@-fb--jn!XKe^5B6@j*dsU@dire5dG#m5^^%hrwE#iXsVyn8zJ^JOT zv^JbpvX||QHjm<2bGmgHHa~|W!+fH--#Nam^W%0$RZz~+w6lKBaUxr7GO%Z2T`bi<PPiky+Y#rGB(G32%xUEA?_rU?}IwFq*l_- zs~B{_kw`7Ovom57p%j-Fdc7l9Yu{H|lYc3Mud{C}h!S4Tl;!v!b$aU{#KkHho`Ql@ z`y}pEb#vx4NKs-rQs9fG_P%(_ZbLUGD#orgnXQgpL4$4KV)G2v0tejJ-bca&=7H#l z?@763$#Z7~0pG)#_ivQBS@IRxr&!?kMh!<-f*sLDKlu_|;)1>xZrS)JAG3Ii?N=_C zFL4!(70KbR%$4rX%k+@PQJ3Q4GhJpJ{MZXz+`1Jd{6P+Q7^6!P$SA!Zk@g9M&F})~ zPg=~en#XZPf7>==1wm7)kzvWx;6uGvf=t++sm3@2-qShoy~u32vBebgk2Ay4w-yj< zXzKgCfkQ*%2#>;V^U)YQQu~$mYpuOEW$K()7GnzmCK&%KRxK+3wa>=4f#RgUliAF< z`V^T6Tj$7P;-kyPBJ#;7%EpysySPsTv>5JDd4(3>SywDStk*V3?6k*@(?#r%X0@bn zcG~SHw@b3LHca6Y(+dnS0lciV{fFB{v>jD^u&JTls#c<$k#N{QL>=vSs<249t)PWx z?>BvwvFhuO&v4O91|1jFztq!78ue~xuy%t)ru`?kn~1g1rEk=nMnhT7Zx63-sd|fE z+R(bQTUEbvEMLI_?4h7RfQF3*3pngG%X-4mE|>svKHhDu*v<>rXs)EUfzX0#Tr)0m zyZmv#sh25eJs{O~IDP@~XdTP=3>7~onQ$}Z432Eh1$ZoeEwT9yOAk1bI*x7fikJwS zN-RO$550Stx`U-9*LaMPZNEIXx0g__0ETTc{6;mX&KM7X*JrfaClyi*Z;Y`c1?(o3 zh+6XP&4I(clpjW1?Ob%;0YR>1&Rnw$O7+;MB>1e+lt8@2c?DWIT)aELr8c{yfndkx z{7zKAqiQ_u zeVDX~??EoV|LG0^%Ql2QY)LMp3qn#dj&$#Z z$q=-eMHNYRGxVR;uI38AA5S;=8do>@TBBr*k~O+q-AhPQAF6<*!(XB?u5efbd)8(( z7R&5lVgc=ORSnfRs_zWW@~e)?uG$AHVCndZP8(rT7(F3gGO_}C$hY^2`K3qZ>fc;5 zj@gC9hJ&ld@JCgRoz6bO5VY&XgK7P1kerpQ3M@bt`T&*=jmIBBQr0MWZn7$4N9-WF zIo6|pdZkX&LI&P4sU>Oc;aW*;4D*7_vT5A{FaFeX-sE-gNh9D6Z9DuiAC-R4P07b@ zB7g#o!49~yB7^xU3~KKgl#?AkCa^lnl?OYUPelQm6N^YS^DbRdjhZ)mJ~LldnxGzKecjOfyLSniao z+h8_cT>Smj!V7n115$V@%P#%u=e~-Q#FpKvab}nC%M(Ec$WR#6-ZQ8Vj^4KXE`nnG z*a!h13Wqv+4rLcX44H1o*t5T>!(vyDd9kY}da4&Y7!p*2K(zo+jUH1) zeckI`7 zv|sKgBic25>^%vmw!XcxQ_OxCeWO32;avi@O@gV?w^nwNNh1IwA ztzL3gvT{PebHbsHoL$-kR!#_dOE}cgb146TDM+*m zoJi$Y5|MahXSqy2LXQ*2X!miZouMm5kclLm+WPj|@#I>52rqgX61qbInMT41S}QwZ zpWpn|HP_@!0ldrOy@(jm+{&))>u-amy}LpGaP`vm8&!xTKceDFliD1Euwm^aWt5FS zx!RUN=1&~e=uxSr20Hh6$S-8ac^^Rgp4P|RvvIl_9U~`*bN*5CbIZng`qi_X(}a>^ zuB{w^b9T<6i^jIH9T*5YU7FjS6Bwa*ew7)kJLhTVQ0Z064Ps*KP;fkM78SRaa zO!*6N=c8xXX*GS9sp4wa>RrEBe^tHeRO2Zp4VfyIOBai3bCiWlf(#uk;FvWX(=%H1LHeCbAbbphQOY+ZncqRkmw%dE0uo)k|w+gl`%uaFWopPI)8=#GZ>#i z^z4pwb)dAeZKXuL@js_j@zt0J8`Y;!*I4BY@?M#lFNL-yY_crz$FVBLG=AfDPgZMy z&)L>}jC1_HCWK5TTmp_9c-i&$ELzDLatr%Ox-x#UsUsLMIUIE;iWql4DS-0tgf-2p zDQlcpTP9HTPa2B~P84_kG;kY6siQeDM;U7(5Sin>2E~eNnL*6T8IgzkY5F`DJ>ikT z=V2<^P)nBv_?Jv8)7n^ZRq4$55O%^LgO1;SrWu&&5WsAF#lXPc=oM1XdBP^kl7l`C zOcTCatv>Hb-_N)My-5bfPk00j@q18mk-dXPx}Sn}sVm5yiVxr?JTmzB{b<@-mZ5ZQ zk92cwkJ`k6y#F5G3S_L_*PP<_q2EI%Q28U2{zn+Q?0Iku+Xg@%`^#0)fP+s*_+xbdgWdP=cNroT~BPydyteb(%0{ZaOgg2iIo!oJr8P#!;&R+@^ zb%ZTcD<^Mta9S2syPX@=j>^Do1|iY_O(&}xAx=2pyaTtRhJdcw6wgLwU`hjPgTqdL z!h$UURWW-UHYfpiOlux&Qz{_ag4jPndv*i7fej+QNBn(Nnaz`E+;sK?QXM7!5_ot^ zN1q*J=2fFVfr3!PZYnr%4&{eO0p!a#%jyf7yrsZ&K55M?(Vp(^%c7=gd#5Wv{DJ}gagCZJCG6cDpATe!{ zHTp~E6VpGj;;1C6Dc6+LrM!n;piKEcv3fuL6RU%%@~PG$Q&WD5)id{>SgEAuK&yeN=K$Pn`7VgS~Mj1-=z>guoA6lRRiapI*K zgWfL#_zYY!1o8WHtLhRPQP@0qwysV%IV_E!UnAgXN}Vaz7%FZBR(K51l=4g(c7hN2 zeq9J{5V!;!@pp0MVwhM%=MEFwClg<@4}8bhp@GSRFr`T2fXqdD#@n0u-Gu;QH{7ik zCQVRb+2B{lu+o-xv_nJM2R2i(I}X&_zug{0s14j~i8Hd%lC^CNMa$IubYa zd&}o!2eQqq;M;Z{lbeVWVk>{aPD*visRrwszQMWV zVd5JWMyf9}Jix~2Yr~E3@x$`!W2U2tfEYArj6SHwfR0(&(MIqxU*v^C3@5foA{;xVY;&(v8CI zY=dlzoviSCAZoI@sR*#>-9zW@S%WwQs+S4l$O_s?zU$?~0|1XshkH+IoOW+XztVHwQbh33Sk6isVOABv z0SFK>UJhHcwlZKv0Gp5}{vje}NLK0wDQHK=)zO07 z&D60cB+#fDO*ukkT`yT0U1m)WZZcN~{YRbyL;jY_3Y|!1B*0 z4_gjKAE!q&sx^6veuqwIa=n9J&})iwz@(0R{%o==@rPqYOp7}Vtx1e1fOWcHvA3C7 zAaiK(=MgZ(@A<7(SKQ<4#6^s|-J~|ZJmAo?bNs+w=<;SIXp)1;hKfN9EE(91OU)Y@eUeZr`j>xo>!?VfvvB z+gbupW;f5U{;EYuC@jm)7J|r}tu{-bHh372eO7?6FlY?(aRn|Mvh53Vf*B>?6m=`I~4~iH%u(O*1UW zv0nFdK;^{h48F;u;R}DaVo5^|-5=QcCzb_R`ZA>E&yuT$wSzSIPOuu+?8lVW2l0N} z>e8$^5#1lK`mz4D8~l*fT?>ffjh`n2 zrYePCUl}b`d8nsq-w;5~7Yt^}B;cy*b(r$s$A+M`r`Mq(XQ0d3`_BkxWiPB8umBEpj%7`cRjkGujYWR zvN|7Y!Y&Rm!m#xYcJI-Ii z<}bCQz)KmCE)DUPOT%{<7#2u~m+Bp;+~&Van}d;Imxee~0Kzkn1lp6AQ5X2ygBLjA zp!7u`tL8gc>*1qd4;$!uAhDG&`}@88KS3taWOy^FJQIp<$S4K(C0$iN>hx*qIdjT|P5 z!&0=%C6TMx)FgRiY)r!4$DQ_=Qb~MA5d*HuCFyTZdiR9du$u;V;%8a~ttKZ^$#e{E zFGU2nVZJbXXwP4cPalX_oCCoUE`z$WuW887A_~t4f*D7+V}o`+9ee0NL{^Zr=bShk z-s7;5HG0xQ5Q7bS=DUZtvUYAnyk?&e@dwKOt@4grF(-&|^oTO?!mIW!0*h+-8g+#X z)aHd11mM5Mtut&WJ{hj!99iJ@ia4zwt0{vMRK9aSVll@Z_@D+DVme4+_@0^qeeL9` z8bYOgaD(*reOcjX_HeG2emrbA@sW&?Fnn+wgj|$H7@o5zbQhRm@zb1{HU`Q)4!YW& zX6)=)+GfJkWph+y7RUHi4P>n~Or1mtP5ZFT9q3T4UN#O>witqkNI|OoFVQ^b+ziz@ zAEqrq%B0yx3ovH);flGmiQh(PK&zZnmXnqXc_yVIW7pXG1%khV>UIwHtnLqtz!mYD z+>fO=$;m$+CQWdZ6=f~}V_EnQKr}l}kpFM( zfmG|{xj8ICt+h)K8NyGmp{2DE#~&nvLS`z5TUcw~s>yFyw->-}hYTrH1M~ex@Em&q zEjqYSkI>sQ>m%g?l7XIksWd?ExVX`hdXh#&9&hr>>A%4`tZTW1)3L}?c8oCU?hcGEAghzEr(Ha6db5c zt_Upk+~)_mdO*hr-}o?!MaHyBAmK_6=x)BoMJ@+{6WsvUQa|(jpwmM@!L|`!QLD6J zGMe_cJxH|D!?E#osH!KQ3s>uQ$I^xxY3v4z=t$a>3#hxyhDn&FWf0#GEw{+H!{$m% z@dyk=z9{qy%4{|wnmk-mXe zAvH4_wciqhuln{2f(Hv8CUB~iQN82f#u z`ny$G`D%DRgVm{Gz}-hkUDabbL@NdJf(L2nu@lmbYJfC~kF=Jq5#U+fv8lJ-N z`GN13NLWH9z;BM7@V}#!hE$t|^BelYf-2k_GE(1bC^gTp8MBWvP7>(i#8mh~Q&DCl zARf(iZ_;v21;|mtB!T7L@;%blob4)iD`Jjubwj0qb`lvMXn>q@#R&F24MZ({e&E<0 zVc@efX~iUT*;7K)vX8^jNAq+(f`c9;-<`Z#52dm&3SJpCQ|r_rkQDlG`I3}?+k9X` zVik%4<`tPZ_x?#4I}ccZrkgKzp9xbX47(;_ptqPY{eBDgaHytBkvRAt;mR3zAA+() z=m+#L?pW2x;e|J>&Jy&dK4QLBz;7Fz;lvd24h`yOrnydC9LEA@rm}RrG7M?a3alm8 z7c72$7)L1SgHxJ>&Uh%q;P?l{*x7c{$Li5|4U0(#f-P4VeWDrSIyb54jttN`M%j5% zsYror(+Z<3i~2(kJ8e+6KQONwnXMVxl^e(hMy?q!S{N1^<}U&pNQ5tMZNPotN0@@x z%F)B6`WCA)k5F?@YPH!yrkw0X*YbvP?vJq~ui`r|tm#8jH&2jcbOlz>K5L5KpdAY8 zP`z2%VT>`4BQt*8gz=6Go_(=w&4I?Ur+M*YjkZr#4UoGy`gsLegE#mlO$=wn^D*@; zn47{ld|#^mZdGn#ja=fbIb7@tuXc2Q_R`Zi>i;&SpD@NyoekhLW8I0q}}!xnrzDr)>(?d=Fo4$5Xu8zq9P_=OM={Tfojf0 z54lf1zB@(8+;Y9qGQIKI?4~XQLW-6I3vP!?tE8IiXq`lql9I(*9C~-3#b0ClOTnKz zpw%c1C#U7R99mK&DQ^#C$2RnI9GoBjkWvQ2DDBS=*9$<&n&=?T+dn@X2ON}Yg4u*< zm3X;OKiGla%kXorK-N(iwg2BpM*k*d^anWfo8u|geg7V%cfa}J!f5{hjMmZUB|8f- zmkP|hEcQUmL}4K$O>m87PAnc;&39p@ariRcfe)0GTMJ(ZW_P1gCOPTI@-$=^_jfa& zMux^2l=qDy+QsmqtMTPtiGOWQ|1&8W*4(8VX$2`ZF+wm=KGeBI9AeP=H-5=_ED}gv zOU(JWzi^as)uk$?cyX9A%OzNo6G~Oe(kiXeE=Tolx=nUzyz25I*wu(_5(AG$!?8K8 zh-X{UUYE};tuknltZX)G+e~l0nW465(;C7&52CjD_QXQg>rODtOT)`O!O`(F>5YVZ zUJtV{$iucmJk92y5bM)74ObWVX1#~?%+*o`4O;v#xlO4=V9~dWMG<3I61LBw(ZORj zXqobp^9hi4^rv#1TWMXmFE#fR5p4{ko}*P;edRukUFVjyW~i{5$h4G=wM1>P*9uLK zrrdDb>Wf@CZ|-HD{5*$oTsoOK-j$_}J%4B`6R5mcWupumkcxE>2M30+tu2;%YUUKy zrcQJZQ#d#bD3L+$T+`$xI!#v{i&lsndsx%lu{<7{4dIbOX?Pd%uA8Pxlw(-uw@xQB1Jyz}wME*5;kAHT&||vy5Yj zb$`D8$lkA&EWiKNx*dI6aEi`GX=M?``2I8e64~?hFMIwH+t;@)8fAQR=}n8c zhvMlL@}t(%dUwB8Z@eWHi{E(bgI1rbJ*ZPKx?BA86UaP8Qe!p1H__TY7o-+}8kuMI zd8NLYeG;kOh(Nro+XP;4)279?)!COe?pD_U32Rc0vOJr{_K{rKj5+(aNg zgpB`;3Jnzy7sZp)oUTD|p+z6P38QXaN?*$O=7AX2j%(e2YIW z-69!lcLdbjT3HDt5Ow7Y+QY8)Nu%@Nm&!<8kslvoatxY8HbG)9bdHt#h#~kgu=IJx zKn-kS^o#REo3lMg6S9|5NLfQb5*V@J0ayWw#n9XIu1O)Lsi0Aj;YaUG-~D$3fGhqz zO{96H?#kv#Xn3**m;t_pRuR;pXXkcGBfXE0XXgk45`b@PcfKnBpn}xM5W=))j9Ry} zE{*ChP&W(g)+qaF{PuD_ z-=FTlzB`^e$R#|}Vf?fGI)T{YV?c94xcTc)?KsnT`T7xq9}b1Jpa0FAzfmjMG$C{1 ztflG)h3BdK*p3P_n7}Xt0N-CCU0w%Xmg4C%urSVxe3Cz*Q%Z1YvkD~Gh1Mx6J)a%tj3vfz$y3jZBT!Fng1m?L=I5=bl>+mwK&+*j|PikQK zbS4jGx0UF#jrM~TWE@{|mkutb?pB!sSQ2&(0*|~yYJad5SI^p$W&Wk#f8l0yyWuof zb;AkV&RA=fB9P!3FZjXNkdvJ;z)%SMneS8yp11ymN&#`&*K=2|!%w^TSO2d;>n4Yb zVrgyMdDrc{Pj>rsHH3e(%jIJ4mx#*`=bzv6=LK80cfbdo;P}9^;`s9Pi{&EkiaHY` zXx5Nl8+%n2Sl7X{?N>QN0Jb9Ax4BM*t6k%QQ>B=;jt0xm`ml@Ip-u-Yx02)C?ip6Y z)CYFar)k3^=hiPGi6LS?hE5EYV=OIaGq{oRhRv96Uk{}kW>Fit2GXlkGZlmqLSY~? zT{oyn$y5{t?W1pRs%Fn-)&+KA(7`wWrgWrz>U}Y^2e~X=$MWXVtR#G9Cg(=|w9j&60MCDy2uN8kDv3ha%aQ^<+Hm{b;A&EQaBiH&CN1^o) z&pYw=%`~d?<9qrYdN0+1FTc#(N#}F;(JPUpDVah^VOVJ%&lPJ3{juQ3%5c#&waNV3 zOc60>m4n_fGH@Hh8yXJ?)pw7905Z+9Z1f-wPk9LRvS=M46;3p`+F@ZU8z*D1rreA! zKxR7wL}i?kbBPAkF$fE5WJ*ev=Q#@#jYNA=r&k*eV3T5Dk?qVM#YZ+^a&R8&`3Zt$5m(gP)v z&Fhx1sf8QU4DR#xgLHozMmS4$He?mk*~O)y4Bx7aVHcjh66wLafPEW)FLU^@lz6qX zF_hZTrW<=;D6IFjt=cjSg9e1u5D4A_%(Rfdy52k&-mYYjZ_hdlcK-LXWXENOz}xA6)Z|@Dz2xBNFbXKU4Vd!^B+O4;6QLRPBuR>5D4uC1PX@TFAcf3U8QiPI@+D!Rzm{(QvjTGqvQC_HC8(Eha64rPrqKa@-Vh1Nd* zY^JM@cC18^p#MZgc^P4r!tU?yDJxb$x^4&0mzK4+zhJ5*H8fB8jl88p8^M^c{xftRRl2wiRhLyv3)Y?}L8-hp0Fzp0gP&Dj?;H*~2@B$2$f>wb7AxVGL^m#~ zz8aa|s7;MNQT@HLv4r|33t+hs&3 zeQ&r-o*tH1O{i;`&ngQ5^#^?r`EHT_TUrbPp}@-lK&t)FFyo^OWX~W*q$CBGL68<> z51D-W8Z2nCD99(D@{Fr+K*uJ*^%-dS0vb zRTFVLta+Crkwk4wh5TWU#7KCkkGLMxfJfGIIVj(=%Cg;6zNV-dAP^cyk5xckUedq$ z!Z{j%Zpo!ErnA>iFrMu&lPGcnE5O|Dt{g;As@XcP%`Sj3a=N8$K>rlhi_r7))^^I2 z8^qQ(DS~TW`|3UOf)Z0~mes)=mv@0>)y{)AVO!LW668_})QsA>TYak!226N59Y5rN-& zM!Xs=gQKtyqI53+@ct+1otOKi_r(fel82^4{H&~K9)DkDeq78FT{ru8B)mzg4xmmjx{8OA}TUa>xpuX1+-~;21LU3B@X8}r&vAlmUi0C zG5Xi5-QB7ZbCA4fbCvDLEGFNvEDg)vE$#6$MZgfDc~V=({-bk)Zbs73;h96 z2jNQ31CK;gsH?Edm93|H`x*IjwpO$F@j}#**Ud}s|G-ljvg1LkWJ|sP)MSE`Dp!xzOn#5k^O3i8kfELNu z0A}PWX{?iLkj?NIPKbBpwJ9|`DSN~7IV2U@*6K~u4(?F_-%$cR~#BY6(kw=><1f-eVUHR;bmbIYsCmml01g> zt{LjI5kkB&MDHva$3}o$EtS><)zL;04Xbz*L z83}Dkkl}V5b6L^gtddvh5C;Y?(qe1R8WEs&vM<=gqJ8T;+rnlnWOvI&{}efp)Y>0^ z&7!ecTidqfBsa*kJsqgJCRss^8dZ<$s&R@x=&b)2<4;IUWeu{X-(Fzbw3sX5HQms* zILcqhChG7vwrt-HOV#odEKS+4YqM-Q&mjeQwZkRyhqCxgET8rm30Le~jxky|N3+6y zzYr_6uI>11?B&PZ1SbDxfhPpPNp$A|^*T0~V3<>dUoUtq@+kxt2V0Hu_R9m_dx@|$ zR^Fid{LEAcJJy4Cj@s6qJEO3ayz%oUIVwWp|KWloA&FF}B7jMr8H;62s?{1DrG+65 z&~y1{Yw4I5J{J1xBD65m?2(jWCQr( zsIK?v^A~d-=Tq@20f@;8JWSTn6a|WzIW>o;G#%>>mu~ne7L?6u%LB7)ZwgDj&{1Cs z8*X%vplrU+TEqj*f3tO2kq@g|cz*m1pl>Ki3HsPSgU{l4%i{|}L4GQg1al^Ck?QgAWGLG1)8u7=(u2U){4)aM~!@-M$8l|(CLiCM$ zIf#5_>;igh;?jM{^y17i3Uf;9j}Ou|s9QD{obd3wg!khg7T&dqxEU7`%=bC!pMklF zc@3E-5lbA6T3y*(R8sEsn1X)N6Q#HdvAx!Xe+dH#0IhTpR@y}b{76(om5HvcQl69i z?}ig%U0^!6UC6+V)pt7m>8?pb@x9(Y;{5bsI`_gRPQ%`9Y{E8~s2kM9N`od}^34x+ zKYdG2WhhQkUfQ8{C$`Q%_?fwCofu;0V6s_&SfT=+K{D0^SpQbs zp^=BpKzKc3$;q&Snl*v=YmT3+l|1gE`u|V#605-I)uN$SSjBktX#{t)WV6nnXjUVf zn^X;IpfE){wmvGl0x!VfNM|8xGk~N%C*Us3%7@lRa4Vi za1g(mr?+M42(|((y*S0iRB-N!v&UQ(w(nh4V>_+D5y}$}-}Cg_XZ41RjtI}UMRWJ_ zvhPpQhJRyAr1{$HiFNe{so~#_uqFi1D}UKD4jr*JQrm>C4dK+D1>A7jk`g_e{ONBE zJYQs@h4zagcG8lSzGGbiuhK$N2 z*vLN5c6fTjl{Pw2*yk$?F!3s1%Sut31kCaVFIXaja5oIp*N_Z=jVogIfy{Zke{T(c zlqyMkO0(`_Wb;Os$B6Le)qTYQ##n`3q)p#AHHwTe9mswIOZ|g|0poVr)lo0Wg5ICv zNi!#OU~q`jUC`8={6Tjo(bkA>APUX@^qibFQe2m8l$Y{R$HzC#qmGXq9Qi2GtL>w( zUZ+YuMnf!0!_yRm^}x{f^s@~Fi}a!03aK2-P$-K_w%E9L#L#4o1LmILFocHG&QrtQ zwH(4)8Y@zvDJmjX04=5qo28<#LH-7|O`B)GxS z8vh1|^1nfuy5B&Pzm?zD6@{><`Q)Z%D(KrMvf2#P;4+Q*Znpx4fAJ4&6%9qWpdY8k zI&RIn1_+aMzL?A2`$*LuuT(XD)equHo&oK+FjQIdYZb+JRVx9%K=%nVeZjmiIA3Nx zR9^84IU#B>|87(<6%~wkNU7h$pe2gO7EC;z1>5^>jl4-FGQ!J`e(lK%{zJ4qvgG;5 zPnY>bxck z#+;H;;%Ba2i#wWVnBlijG47Lc~X%U%%U(O|r; zcdZ*kgJd)@B-NnLYe)-`QH8RgHTa%@aN_#-GD_Yak5#hbr7u>f&zZqo3TQ;iFYWQ|H zS%@z*O84cTthIG_JzcCuoq;@!0}p}vrw08;fJS5I+(+F=YDlT0_4>C!B)KR9Ev!n) z35k9oS|!ihzrix7IGAt%`zv;?)vRkxugWZSF9;TP?$Trd?4V+~1C7EA`bSVSvnPcn zq@lrzjBlUK+e@K``4{LCOk)AHeryqclK=yWI+TWy=NEtyh}PkJ7#)DlIiz;}>G>P6 z=PtUJ6QBre-EO6SbKN95!;!bAU|BdooSI)$lLge^qJlc6^Ex74@)m%{YH013mjdsr zlF0c_N~5-?`3+-lo%nn2zKd{WCj!zbg$Ck7yBS>1;G4SYfgZ3iL&3zK1T=skZ%f)& zhaJ9MUsu5z2%0h=OzbQ5v9H33s*gP%e{Lv)CPv-ur}zq;T4!K=i$bH6(d9&Em&KMH znX86;Y}O&u7_VJ$)@otOYo=a7O_tbOs-PeL0h|Ri8leLSLb1cz z5yv~)`77W~$-Wu_Ckir#vH92pDtD%PK(SMWuoU9>K@%)%pu41H96vsY0Q7yrlNHjL zLqEk;Y{Lws_8@S}r$MXbBXlgJx9V}Gdd+*{ayQ*ba;|$O9OyUS>%?2xKoBwt-ml4H z6-C5*{WM}oC1&s4b|_ovXN|%Xpd#sQKJ76DNTfB-kdt3EAKtm(uNb;6A=y`p`_0?-K25-^CHlO$Ue zVAxe^;7F@QL(=~Jv;u;A7C8#i8M5yewz>l)x=B>LBulq%)?^vVZly5Q^_x|gHkxUo z6?R;fmm|n)8)9b$55_=UY5*+eB7p&j()2qU8F2JHm~(RpMyH{sG*Yg%C)lTJ@RJJhOcxDJZJxCZDqwBY^bkz725NZ7M?Q^E z-VvyvpZ>lOSssdRAlr|{bwz}}N@~d(r$nXxRcpXULF=*lA1jFd%p{ajM zj=%t=CLWzV_MLNm3_%lMWQ5ELfmViMfaXn}2U;^$E7qG4PBgY16t4kjFc_}S@M}Re zM!s5bk_H#q+(NNlJrh#C>$Eg)3%iWzW3Vb`c0}W|6|M{BmdUSB$P9}Lm9pp<^#vhw zPwNIZDe@G_tCg0JCA-tp*&`k#Rp6|fF3=z~)`^9<*3NEqPq6cj%c%H!MbIV4Q?x!| znzKhFjyTMWMT=+#?PF^A2GS*TNBgpVvex=C>M!A0u*5K(&`bW8;@$e{W=xjBV&3K7;+L$59R}*x;nU=eWaBjsN2QL{9?;A`k!|v(FNnC( zJhaLGu*rK)j8zb*huLZ;Z!uF}3ULOhj(&?YrMIu9H=VA8fk?u>g|N9%*3wOyEPF~o z{ZM!DESM;p%OB!M3W)BDz=z@1bR&32+R7K8kiWOp#^p(=q23dycfJ+v=^T@yq_Oz% z=LharoNaIKlByLvSSx6M&|2x5V6bunGD$VX*i=_wom~)4cP1rfu=PIrJe6h{GYwXm zWXaUrJFi{9;}?sF`LTMl*%-sQ)VF!PIaJMAb`LDc@=ET#C4;wYY6>gG&fM$5U}%+Eb5|{Dx^^rz6}A>pEvfzUDENFCmgotnDMUbQB+62Ag|!7rk`Ga+o+T6qbY-@*> z$OWi)%3MN{eueyi338wL(4|%W%4R-*lp_wKH=0`G8qZ@HVNHkc>VJJTN_M7prDdfl za^DdZK|?Zho>kqS{YisdYO@s3JXC%11pGKK?F%84y+rjNgtp6|R-H=#n8b?hRe4Bq zHV3a*ZQQ4uUnR?!Fu?1s^g3hS{WEbNza?^koT^5V3t5lSmQ|4w?i=mA;qP$->fs*N z5zQE_0uGsvGb(#8SuA8~Pe}o%hG3`O*&*Fq%F;$ z_~}mM^g6p7O^=1((AHxwwV$s+h_nH_@|qc!b1n927dWC7dGK>wanF1HlQFQ)Uo-K= zi3>|e=6dgy6X-;*PGTnVU)MAM^lhx>!$RlR6k=DWBZPrr$*C2i#9UieetR!$2IxJo ztu+iZvz%v^oky#`t0$!eX*|u+4Z7t>x#TYtg_X&pFB}Ne(I}`lBPX6Ic`!kKU5n~U zkl2)Y1N=L{hmWf}{hh*6H`!^twy}711k8`K$ihe`9wIGx$F$|$xuhw*7py{t=7WDY z^AFW;hoh+fKl2e`sCP`s*Bn1Hr94b6SNwJ4)j)aj&h*6!^E{TVw4bBuD^50^ufEaF|FpdXoh)xs`;-koBPW_|dYomHtg}!)m9FCf zB;?B3)2df{9Y5s0?Xl0pM`nHy{O~{KN+T9c9=1HRL|5Nn=VTOCf+~gC)h;?=wNz>T z`&J1jvCq0<>cpn+Ch8Vj&qG9-DsxDaQ@LS93AsGQIkC&|?cnl+PcsU6%H7Mxa_Ync0>9+OE3{NJK z#|RC|)CvS<#Ar-NHr@hah^hHHMFw0p;!!vFb-?Wf^zO&&5S*;JS!&`|F zoA%>}`0D$bSF}}bdxyu^;iL2q(4Qb9HHYSE&lcMG-|wL)}oM zfOa($Wf7CUL7m(!6wLY4*4Xhw=zGsU9%4E8%G@H5v^QJQ#-3w8IOiK$81W7IlbK?i ze~sDsCz}N}RKP3k1~0mBH)xJ&7tNLks%jb(ge><;;7mMtXrWt2H9Ttqg4$kD9epm+ zNYt*Y7o2Cmi~-t!zeA*9-R%7*nbU!#>XkRA0kRogX=C56;g}K7=k1@{5V9!W1g09o zW$tD;^d6iTBoAR60XVQ|MWOh7RHH0`9ss{&0Roz1z-V%9dSz`9 z>uILt!TLrl1@_Ijq0VKzKuAc-OpoDfH`Vedd=c;6eC-`?{)bH_Eyebil&g`V2^`di zy*inO{NfBHBy@tt5EvM&-`)~o#^F+J>LtkoC@s|O6FEM+B`|JNmC5hlWK2ejVK3?K zgs+$zWE)sC<#)bC26UlE{Q9>l;ERzr7g5=l8IC8(?mb*{2q^t@sW=HQPpMfTpgyg+R@}eYVyH8&ppRb|Ugb|Q zj^v>8+b{n`G6q7Ha3m=`_jr7R$Lsd``={oD=e8COE0T5{YmMCXjiSe`cHxp{S9{u+ z4*UrE?YF3W92!uqJe*RrBw1l`}2p*{FJR~vVuxtl*=ktbT0 z*TJZsT?!riNlU>(iYDxDzu{*p{81cI8yl^I&J@WNoG}XY*86MzgOTmU{=iiE=V<>| z{h-$WzMc}QUEgrj|H!vLXMv-1`E#T$gX(X;7)SSgCcAuia=V9jnaqgiLm~u9gUM05 zOqRhm4+L1xB=*FMkJv?|@p`)mjUCpdT<7pE{6sr*Lio<;Ys1`V`^C)Jt75qIB|c3f z*$e3Fr2AYj_N*StS|HD@vH`~QYK5Ns`XtK%xL|Vu=DNF-qT2RdF=TPbcYbv4!Ds(R z@=0E4p?{hig{PzWB(I12Bs^R%;nPe9?gO6dabHWOS6)}L*Cq3`53>WNh{x4J+zFEr zJrp6D+j;DA%^epFA7a?(eYf#+@^;B&NTaRIqp^y3vJ6r-j47KWlzx-*L)UOS36`X1 z+)%gHLYy4RKMy3Jt&;9^wh6s!0?m_VAETdW`#LghZi7IS)?3Q}#!&Lyc1ypJ97pl= zd%)n#ScDW?%uA6vnmC0T-#zp(*AMey!+n_~*ROpxpCI_#=49)wzxn?%_Ae1S1CuTz z1Q&Ukv?$$_d6u|0PhGSE|FGP?x(M;w6F_Dy=!OmK2@f0!U=0(Aa-4LJA3MJCQ1{}Q zGs!*tnDm(PReNjYy1}AAhH?q!q3O>Vhth=?!OzJtE^dt6*BfqjE=%e657*PEWfrV` zP-10F;JMswS$){ca|_1tOyvd~+gbU|sq*+KYW@0GZ10zZ>(eixCG{0l1ybaS=e_k| z{0vmbF8j+7 zKE%erQx!^>C_Yzhhg4^~w`1*&Cm4{~AFZNHX#QD}^jD!X=Z{t5nltPpgrTTIYdS2n z#JKG=2;?!Mtx?tTifVzFZ4Br4b+`DLWYfY=_Ix_PHFQLyT`8L3 z8TuiI5J3zQxcIB^6hBEdsD*&Vr&U&XTCv-~wjY@#y*;{RA1&^Cz-|%TIZ;h z1=Y6;QPH0ZT9dpobaZyee#E2k`YO3&cAGhHV7j@XDbhEG7D+rzB=L;lpJD}l@cM)O z5;YjCqE5_BoHK^Zyz6H3`Gs1dI7IQY!WB>A8i6*vpi4=;?(K8K#A*KxeuAHOn0 z$2Wh~fNq#uG+>yb9v^Fwd>Fxgf)Y+7!?xaEQ4wP*?e?2h+Y-QGWy2?Em<2*FG zV96fo9cK3+Wg(X!ioW&tG?gIR9*3Wkeea0@gTZyl<>W^fccipNtSNQS^si)c-~NLa z0_XGQpjRcxJkNUPnVk?GL1FXbMmLAxdxLJKVqOk;d4iyD-`lV_P z4};H)QCwCrQMpx&K~C{wIa`EC!E?p!jL(dpAh(*@I^?O zB`Yg|@-xShy0ge1bD!rtR^zGOl^Y21ZNK-as5$qqWFz=UViz7qERjn-Q_X_S2RqS> z6tpU+6-_{sIP8(|C5}Jb8Hb+t-|5FX#2#4+@21uE|2Kh6U{c&w56eUDE|aAY-bLdf zu={q~tsCLI3#YG1^fVjfyJ+!knMvW2)1swJp@3SN)|{3rQ?KRg#R;BLrNK&;`R$cX z12+L!gEg3U=;cK_eZN=eAvc(~R#4Rdii5F;-7`sHn&w9#x`VO)pt3Tg2V-j`bsp-2 ziR%PaB?u74)(2nBqd^!=8akoc$PmVM|3Ay6MTsyrW7u&uj~HRfjQb{xD(Df$4i4Xl z07=5whTY@@y+oBlfb<`$YzT#$5wr@(ZU$$Va?1lz* z>@t+x#2J#TtU{u^Vhyy^uGIou%H#T=LTD!XbEb^4v?#OquiPaFFk;>-Up86L7>+ZE z=-ZG-3>0%4=T@j#x-j=EFtg+>Qrj^3@k@txROr(&8>!bq00vq(sOxhduAaud0h&D{ z#y*VvcqF@i0W+&nM(A0tse@f{vCpTr)uEdjj0a-f!0|Fr`G_1@bhf?%kQuu&D3m!* zt|#~TH|{oQJ?69||3IzU?qK*VEZe_C`go}7Q9fw83bfbA$v+da1(suh`Vr~0b+A!M zypof+OMv{yhqiSprD2IOw;Xsz?jwphdoTXu=I97S{TXIsq5$e2dtr6cI1Velv+Ba1 z2O!cYf&0d_Cgs&?)bw`0hFahGfDHzI*2$tGIE@r{&qYhs&6cJc_YcGQZTa(VW9Xu` z7o?qR=XaJgDZuZ2QVb}UPQZq!AvB{__o4EDQ~Z$0B6&I*vMx8FgC9E(Ea9^mgwIOc zc)V5)>>b4=CvHXT^WXAHXWiA$JP$yF7VCtxLbp7ADD)VddaxrIxhZ@ntI2b(QQ-E{jn9Ch2G?H?;-_!Va+%nD(%5q5dtW@F95@+w_ z3eaW6bNRzu>ambvl`>p=0|eq1wf2r`Vu}i762sB*ejJ*9?kVThyJEjh6}By>*1>eY z4Rdab>`TXHyFFH>eMP{?dpZq=R@7uM9FCqIW0W&$kAry+GZ*#PL-jDXA+4W*lky}( zUG8o;EEv6ifi2eoRL${I&y4cC%OhxOTsy4dUp;Od^s0M$$ z6Nrk=c;)fL0Q_aVb3&D&VS_?FBcg#ZqH`ai`JbK16Aku~BAw+buhIlvP071Ga)<(~c=uko0R| zT>H7L);U%!7#$i+-`q+RpXj-pOo?k#UFkFGr6Fca)#x1dl6lXb7DRgzZ`Ue z+fP3Iw^gY{V20I$y|{xy(DJw22U$xxrYND`+gXOS9M=6jWTZT%_@-TZF5m4x%HvX> z@4uZZ+`ds&|7o7{gVr>~pLMGL#92Q63b*hWclgF*1&~9*3Fo`{NT8f&{B6(W*VLNo zV3+?5tNIVj>YSNTPk*+e|HH~?*Dut`7uU~;9y=?WZi-iOygHnC=4&Elu~cQ8R?HW5 zjQwx~DqY{fnd1%-kfk`ZM*&C1hht5dR`eHYBIPFuQ7o(%U^-ZE+L=DeH{Z%J zjb&{w!?*t~DqdlYxrV8=Ev=I{do=)EGD*a+87c|Yu~Hv*;P{K&*#_8Eiv+C619OZu zbXF^HDQMrnf}k!uqju23PT!WKexzCxlL`>h_8|RSJEieG(W?|LNkgmIrY5SsbJ2e1 zrK*c)3;HHT%?x1&^Gm6!{MzPn=_n>uf3zE$sbTmfiVIkC%hb}pQ zcN*5M>#6gpn4<@67iikg1^W^oeB?xer&yOA%lFza3Oh32)fBCP&U4VKz%H%HpU7U4 zh0R-fw4O>s73~+c2LT(1$OlL}%H8PzzR#LsaV;Vo$7|L2q9Y|G;caHzgt^^hEDSw9#5 zfW~8I`Qaq18Al)7-k3)UeQ%VNOSWoH`trtXy?G_d6LKSS=2ETo5~Hj0gE*{v=g$7j z)8T|-aGaf9bP#!mwq5Va_$4_7j~=t;=Ahk$NH;vwd9(1)VCVKYV|i(Ob3B-#Ie!mg zCp_+?#mYo+q^+LkYbKu5YQ0+gkesuLzNN+d%sEpp)~et}4>liMWemv`hqI<=ehsR- zh(T}vq5U_THj={{Yl*wpQ>`_M;x8Zof6OmO^vLrR(Y2Lt88;IuGu>wNv)SdL+P*iT zJ9U|Yd3s}^Ma_|o$p7D7`gCz}u!7GiWMuEdL;R~>8OAB2#t4g>nhsIXryC3&lM7!P z0vQ#3B`d?HAF8dpH{X{`kh^mDL2Wzx`+u$wiQ5GO$c9y*zcl(9a7e@alj_JF_~LtB ztu9gkLtNyjNfhYYupX$tl<~mz$|?(2v3wO*-`PMvHPS1wU}OFfO`fBu{v~Hx9K2Y` zF7y!Aj2dZLhF_8NShNYWNRCxBhTvvabQVdecwHh>)|NNYG0CxNPGRurTV{MmTlunq z1WxCQEK2<)Tvm+B-x?;@cA1u5(AGQUiqhNr0ow95G{BB))rQZ<64VkViO$`s2(qM? zw~MXzz$S1kF>wvXUG_uAPau+1&S{fXa&pvrql_JNms*PEL%E!vrE>dgitx>zgeQ7N2TEeP;8aABPp;%R zjbJ;`U#?=3;rJEIW?Ozp0!yHWO!Wzu@u#R4@i@2nKF|JY7y9Vl4`22{_g#Q`f6>Rg zhl1#pQGo0|pH_BYqS)1Xt+h8n(SolPfwLR!H0*nv@2PFY7If=6R(mu=US|Rw*w^m( z#=~;D10zhOHvJn<;|?s6dSg1!U~rBuHdT`;nI-uE_6%l?NtCorraX)T@uTmc@KOxA z@9S8n%Jhlt^V56}>dT!gSXHg6RpBaL!Bsn8c7^r(+sWPBL|2j-0o8W)S&E|pG>8%2 z>XHc_j?{bFcd;&Jpzjal4Zg4SO<%%!4Jz7!%*o&S2-Jrk_Yp@{}uU0 z8JagcAKbwAiLq={33K^mp7DP6y8F z#N3kVQN=B$niFUEwIGVh-5Q4fcyPfk0%tOqQ`3=&+-y5ykkUit`bxgH(!HXY*!-9c ze_IeELpm`CfFJsvak3phEfFD}-7S&T^{^e-=vDG=jT%kEWIOzNR48^Zy%SFqXuSS^nDVS2cZ{z3ap_XB|4sHx3HEamb1G$~*_FeCp9z0QBQ z->6-@Qm+@_2M!)MGL1TpF*J2E+GJZ}z5b1zA_4r>>CV1^xyw3%E$Mx-qwpJ^n0er2 zDDX!!{mKL-XlrPQqYyHaDoX}lxR-cKs^iO2S!Ol-M#N|OMhc~c`3}wnfI!J-9Tc+9@d9zf=uQqNu zYmTJsD_#T+?-ic;>krFND-5ktm(ksSh24)a$oqSxP)e-nfH_}J@KppSL(a==?gL4y zpu|^|T6fbX`vY-C%YEg2B!(~gr7D%z&+6GQH=7bK&9nkx(bWb8b*NOxU^Xi;gY|Lq z0=?S$J9r;efs1YFIAmA%BG*dTf!#5;VY`xV-~~Pgr<{ zf(HhWFtvOXP^jsSUQ$tjQXTOa+9&{St%xH$jy6ZO?-i zvvQw*lsFrA$zW%ARo&BoEqyOYg$z($d>zS)cpg@pD3z7jdh{*L&}_I#uR+Cli8K3w)tzlZvp znuYih1nMuKz(?yZ2+&`;zbXIFsbR#+1fHwPjkX|}b!&&pYTvScLGv%DaPRJab3ONL z`(J#~&D0W0lh%j`U^1n*+wlA)f8Z-{z8LO$v9J0{SL=$`q2FM$3z*apc6Ld!#5Y4y z7pU^2LB5f(%a`lqMbe6yPhu522t&xpDI167gu75U=MTe4#T)KQ}lN6vR8>*(IR?!iLpk5ZH8tMjB zvnc)emEbB0+Fr83nJTJl!1lr~Y0_^bPb>1YPNct8EAq5l9%uivtp@xni)vmLtVT31 z18X$%5;{_<(k*itFUfLFW)aLSL+Is2)i*aw*`<$R{806lvuW(|a!g4`JnaGm#!*h0 z_cmj`gN+xl#J*=%7jM~6+swXTm6zdV9tKfa`*+l5k)EWP%K$85NGI0e9r+I4yG-{x zCJsjE#-%8aivJXYr58|JjF?`auzb5W497ZwW~>(`j97rvOTIGpd6C%ZWzbv0;CE~u z*jb_lHoZX37$Z@8W<#&SEqn16I|(>?L0x+1;mhZ2^xKAP4nS`iaoxuF>SLHT4$Fkn zx?BV9{NK!&ht328_WIrr**KEzPmF1o(}biVzDuEZPbQXEylDOX$ruAPi>gqugxNL} zib#ACW<^wmLlF`SNq~+DnA(S+js)nU?HRvufy|2?jTuY(FzAu65o5Xo!Xl6@zVV_6 ztgB539+IhH4CPzghIA3EaXHtEia;&`wUuMI2x*MMW)U+2$|CHb_qhx}bF=NXl91W! zzLMx>x-p&lA=I0Oa8rKpiWh0ri*ws={Of^52Dx0mfIu3eshhrt^?POkX}zk34An~Y z-1Ohk%1+K9EAxr8zY+(UFovy)T!$7*45^i2yhE_!bIB?tuwzKt`f5eB>b}1oy6uwm za8|Z>B#&2cQ!Lmq%y?en;#*UmJb zJV{APHtJja9j$rOoB>F=qS<^IUEnUgasa(@dEQL;p>;MJxN+Aqzd(Y&JEV220MXb| zeR;CxxmA6XD843JJ;P6ARKGwPkWd%!B`i6svZLDinBVDPb@s+O%jfxP&xopW$gm4+ z@fC`E%Wuh6ro|Q~%Du21n5szet&XK|Ll4^G#8V7rVdC}TAIh4Lv12**miSG=Ow`oA z^MQqJ{{Us2)AdW@J49kN^)zxOcJWO)2ldgfYg4Gc5{IAd{L9yqrF`eF!_>L()4&S> zr^cG1><*ix(i%)b491?~97eTWKTd2K9!0H6HC+w5dDPI4M@!u-uY;%zyb!v_&pPz| z=Bn;V?wO2Nofuw+C3m0SoMHJjOknWT@s$ z6C^?`%GX6&oEpE&YWDx)@BRZOqv=lVglUy!FrBS`xanN+|1mwJs7*2XEgo0OH-gJD zLaIrZV!(8*M;;7v*2_=1_Ak%0}lQH_j3iD=M~=^)jlUt z>A68?Xv%wD#Jt76tl2itAi3G9T4Qig4iI#XATXs63|bIRu_AWg7uNlOi3LK6$pvq03ldPa zn-@ySabz%`cI{4Dj^y8c6|P2CG9s5sGl~}(QB{ACVi?QvpxE8iTzl4g5@+C)CivTo z_-K*EX9P&tvUihLp&;Y?Qu7B_ttNNk@nrJ0nE>EJNQz=ki12lUdu!wOU$Ufy|SxR9*M2uaBZiqud!{|Y~5^{uCMWZ$jKkU0fukQ z3aC})sFF^30RWG@hNy1{t$n!q4^W}TA|CB{_20;rs;*@z9F4;SI{n8vMx;3>wxQKO8ldR3W3Mx=NGp8VCS1~jx zedzd8RSJ<}H4|LhN$V2Z|2O~t`ldPikAg*-uv$+v#0nD~I+^_^S;k;!wv%3Oz-4_@ z3U-VIsE6@?#Q z!TXr@11cWO)2@4qz}I=AGxUOympts^!DSIBYM?b%!9VhxguIAzgAxr)qiE8AdDAi* z0w8u_Wci~-^F(F}*(aCp^f%Mz4lvub+8crv1#m3%5?R zSnbMI?SX4-FZ0PwsjJ#|E6VWxwA`~#uOGPzk|?e8>QTNEq zxyphVhBX&&gQb}r-dCFebu*#6KXJQ?F>%EgMn|Lq^K1O0S0B=vZ3_i7q&HXx5_*{F z3+w*)bs5@naHv#0G`&Kx3s4&yvo|#6Y-Bvb)(`{q`PBf}c@BNFPC$y+*mhTqy;tw= zts?s_Rlo05|KjgL^}C^ZYX2Qozc*BWr|NpiLB`lQopfAi2V4(c1r8{W1{LVg&Eto6 za063hn1UaBtA99m#}3TwHHN-E?sakC*VPIu!%8YPAvDP-sbz zx?{hlLUXF6pnq79Khz987u8{f28i|q;>&%hVq#LnPx50#BM<0^1Q(zz;mSwWK*X{B zPxQXuQODPwtpWI9-#CHvyH!6lT#|`$RW$%x z>@!Rv#{l8w0_3W4;;+Ce5M@M_KiUhSC^Piyw@&!0sx?avpYtCEl$I-3zUT{AMpvS2 z_Kouz$;FGTbR-Bs$IUNL2LHYY5%wfAwe~u;1mL5fqcs8D7gl*uSA0>NIB{q2WPf#w zH7JaRLXIk>_+=hf&VIv|vKRWp%i&HaaFu0}N4fN8rO79!xRf4}Q`5G=?}LHWn=cdU z`F~+vp(HR?q+zzEdYSM<`E&sbCr0Q`(T2`Jsm9688@FWq)dXE8KeN$FTr;F~d753d zp-R^B6|!NL9qck|(%QVchwMg>fXn#?c#ux$IGpv}HV{>IjNee9*Z7vyJYc|Ym<+=y zXdcvYb<{vVaFNtga)f@dE5bIHN85+O;OrtX|kvZyLJJR7IwH&mlV&N^7riZ8c34O znqzdyg3S#&^d_4?jb<}G@U)<}&<^D)OD?#7r2_5WDjmB45r*UVyqT=QF^x=KF8S8CM2rT(cpR;9jWl0)aeorWIdcIrK@G1G{aLCzM#YySHSm??yB? zcfk&q-^XFquRwR|tm)eSCuO?_C0Ex(b4|DDeC-`euPgi+Kw&31K|}iFLpXr$Nh=qe zCxRd5JDr!oY?-4~fC1$yW_Mm(1r(ioFN(GR-F=Z1pZhO%b0v4+c{6v!3rgmyjB5^1 zP^*BA$buL94%;tu&WSIrG|NpcUKPV3_0p;%dRjq-)Z#Tx8}aY|!89sg;8y5mX_%I; zV3AU!u8#*j$qFzoK6RVso;(+?XD}ydFy^O`6BG_AiTpjdWwD*Tcty!2UY;rxLSlu8 z(Ly5u4wE{m63Pb~hFw{6ms%l8RDqFb`{q;4uS?sl25@ilZR6k?g*N4YammemBQUPs zMiybDN7EaLnI+cRZn63_&_#1c^L(!moRnLr-3*1)fN$1*U1;16s)CdNa!RSGs3Z zFZw~ywhoN38e0v)%=kE)o_oe;3$<9+GRk-M2teXGJDk=_7%7)7n!i>Lq<;T@9e>HQo#Z!A%VHe?`6*X7ID54&jrBXo zcp*P}<;bcUeSSH@ow#T(T1R5J3^xz+bEcxN*a8gs@a!$~VC`IcGJLau&)MnuW>e_G z7DJAv%DMh5up>d3G~Y81VKKs+{(b}+HVfa5eLN!(<;dq285%=eUN;Vhb9UP1Wg0`4 zV1O=zT0A#bc+{e8pB#7P?!O2y!Esi$`PX5S5ei^b6x89-3N)9m29O^2wdJ^;6uP|5 z0>W?GO03~}hWhV4)pr%&=c;iJ3|n#ydN7?Z+5Wy){R>qVB1$1_kTnWYKk#ydxDOW* zt_I=yu&Yzveb~a97Z6a7OrK2Q^l!L2{?SaI*&uL zs;ZdO0tC*DoWEd$I9e@oZ(e2ujE6@J-*?(EeU~p1*&*|@c}{*8s^3SdI_mF+zuEE9 z&l`R~c~>!hZ^EO4A@-|}cRtq0@c@ba>f?QBK(Z~3B7&k0SyU&peDJf1Hy(J=M6{U~ zYo^kn_!NgN*{nAjDyiCBE!Dck%LfbC;!WOZydKnQF%Q$chQrr)Z73ooL1n%WCS<=`|y{pN5b$oQSKfRc8eRS)r`A`7doSxRi94JFXPmP0~U3rb$Cm7 zMw(B>zfPY*2I|PtON>wy+x!LmQX4%jJhGrO1Ehu>AW21+8MI^xY2iQuBz{{CSlvsDUXFp)o5FV|do$%vhf2g2I zX-=zR<@ftT$gS#k2riYj`=lu7*z=pq$9W@|2KMfV)-zGAX2;Y&yv z>MI2+N0^k>2S#&fDp=#wH&b5blv$KU{?8ka6w=O+cB{zOx&=IFd^OjQMY0{>4!5T) zI!3jF@I<6`DX{Yg!3?O=F8V_QV6kEP7f0(~t1?|!Zdcg%`ggvw0O*gzb8WFYet&VE zA-;1zNBS>fayr`^3(aIjnr)!+`%6p$uox04iVg8B43=lhuRJS*wX!o9&(fgI)}S9z zr7go&-u9}1?L*85{!L+Vj8^z8&qbl;4V%Zhv<{wV>?dRlz1P7n#6T~wdaYm+Zx3iA z%WxjxS9nx5Qa@PqeG~CATe^f?_aeJkoKbupY^|pAo;8ZR^=w~Idd6M(`ilJ95fz8Y zNZhP*jI$&TFS?UC%`?TTZ@u$*ki;C{LbQyGdvzc~*qW}UfX!=yNyRZD{XMh6l%&;U zaA^oVgGEC~h1}Eu_1W+I>^F(uD+cEOX`aYm&w?2EDHk$>l@?C*b>%H>~uKLbW7M-yEmlQa70q@ni< zllJ~0qV?17z#&l8(+(Q~fo60zw?w0A@n2sKO?==SFx)#iOuwjOUL}PBCl$_@?qP6%o}5GC;#a` zHXR-NFLJJPA6dy}N3l}$frPUyr;l6R6L!{S%IPXK0h$t54sFATbl9^(?-tT}7G6SV zRt=C|jfK^sT4Me|nC=2BkP%_Sz$7-l-9P8!fo+HN_lz|5ROqfkM6FBz|ZcU$> zzLjCU__SVZ)lM{M?yi%8@TK~)r@+In%-rxV(LS}xPakeBsoCZlr$Z+&rfI{eld4QO zuX88Nu@dczvSfIXpBQ#47$!e4?DmNP26iUMAoPRsXC&Wc(6_z6k3NLnNm>8VD%=U- zGB~)jBN4?(=F`=Zw!)zh`BU2?XM$CUeTSu@b#hIH_?SGzhnTJk<3wxPaJW+ro-lRK*%}%U#^}{a!MS zL-yBJZ>qNB8&$dKS2VYV{T&Y;9dB#r?L&OvqR@~wlH{hp<8(0wUw_Hx|MB39m?%^ZR{d)P~2e+Ba!r+gw#u!7)Z@dUY1mzztPz63roK`SY(;jB{ z(UOS2`>$()5P&5e^Qs;=f=BHDYqe z+VQr?1eLJYVO)6-lz04FNZO zS#t#3g1!;6!TS~)pli^;vxS7j5{+BAz|hekBW~)t2{Nfp@C|bIvIZ#6a59Yc>(lW zc%1J*YtKU7yBW^ZICV~XW8hj$PuSZOvZAW;AsSu&1Zvg|OMO9PdJ750K9)i?xC1C$ z1DkIwKYDTRdwoBq`HpWOpZHtb3BlTbkxi4aHxRyCMWPgI24nE|KX)1{wzc($R@(mK z5Wo4i_{|3Zm#f>gemSYcEX(ZW&Oc~MS^2wowTS)-c4AHdPgy?BZwC7~La?L?*)Y!w zE&Gf){)sohil3#{JJ1B5@YAT`528I7Y;#_f0*z?!X*S0nGiP{yMfwX>9?foXMQS_Y zPaXd#P*d5Hw*L)uT9k;_I#crrWX;YU_*q}JWUy(=57m0(*xdP)Vy^MZpFDTC+Xi%F z-6Au2JaFtbCT;YEg9kDsI`V*OR`|QWt_+4FxR~IKQisb_&m(Q&7v>7DTs^FL>r(nT zq9XH2P!>?B$fxv>{g=6GX-RXiL4^IBimZTGc~cReo$?J+fS=8?!rAJxzDj5=95}0W zGxf|1XPVSjnz$@`$6%f5lpD(UIA(bsvOEF_Fssdv&2Oa9*f3P9Cw=M3W0m+@zpUe{ z(MQxX~e`v;K*Oi^>B1#3A3-d=^#*K%Z6npa0$a2rU_>;Je%~C?uYtrGr9pWjJ z!LM_ZzW7Gp=&3$njv3AH72a&se;Cb-Fm_}!Qd6oV|9-E4g-#C?E~j+;Kb7)v>Qm5_ zAq<_$SQ9Yg>BTdax)2|IC_koOTuHxUJJm9CkUnH$Voi>n52VrCpTk11E|d1%6)8B3#(d(49wN{_j!#Z^nh z?1R)X*miH@jVPX#S9jVK@gtiq%I^H515K+G|semnH#BP7Ue<(E@F|f z*s`b*_^UJqv`V`=2Fa1*W1J>pPqRv9gxZT0&%MshQX;*8Xp|ZMEnhreuD_!g@E8pa zdCfyIe{zqmFR z`CEse)gJ#mcEfX62@!STpKSAM6j~=9(`$&^6e>!Sb zEb|pEAbY()Q5|O$zC^8{eMy3D&nECCUj1@vz=hpFtMi;vDci-+n8on2H;T+CjT-u-W`Bo>n`HGBP_bgrpcH`*TJoERGGJ&Pd;q8j3=M&HpxwJtZK=kOoZTJh z{pl7BQ*!!pOTdYkgBIKVpGxj@N>{};?|8aajkuSi zV41Kq=D}AyabIJf5`mE(*2~)ai^6s7F(g5%hj9(|%?s{3RsFtFl_w*8K`K+$Y{#so zI&vnGt=hN8P{kJZL+>%mrU*b-gTh6QD8=$4kD zRU(K(F0VqjBnp9NT{4K9GXxl8^(fd`Vf66x2(RRtJ07)^iIIpi>r)UL=T=m%bbw zyl?&$g$TR0E%pdJNgwn9?h=I8NBQKg4x~G>Sm?{IQba~x zz+Yt0We!;=Q)IHE3I)J@BZJo1z*u!mfEbMhDRsq*tE@sbr4@=+B3wcBwQhBkDT2b;(#$s#3D{EmAI)LQ#tkoD~efQdQ!s9L1!Mi@1s*v&!FZ zlRtCYXjqy(p!1VW-yjVb#Uu$hM?FDuBHILHQH*aGBvB(mDv*PGz@|C*tJpz<1CDx& z(ZVz)Ea>M(!Jx$>TT-*$^G9_I(G<2{hOcC`Tg8 zVwx^O8c$AlJT1Q?xWBJqHK^3`-ewWk-*-+OqNt@l+#B%U`jSNC9hT_!);ql&bTI*b zSy*yzqs+&^?o?d*V~i!lklOwQPUAmF4PQW|(nf9KuK&UaXzK3QV`mR`a?%_eXEX;t zdV3J$r+oXfQ!cMZEYVlaDTkLfT>Om@{2qXkAVOMQf%t8Ql@gVO6m?xoZ2|rI$#|Qw z&L-uBN13x0>ZW)Dv}^t7LylTG>cF^)S^?kmo~0xB-Tc*m*uP02X`g#jm2A*;j;-L$ z@^UtK*&sW;RM2vpXB=tllAIK%lcVJU-Djd=Y-aA3ev)Tqt6fqEpx&bJoLWpU?g29V`tcc07*hrt z+sAaQPd?{{5@53vS5QnfymfJ|hh=M<0*{aDX1LeUBe7HIIa#>{hB=+zl6)_=AoYY2dNbcOy{);Ju z_Pz`iHwPZ3I}gBg>@^ZC!DXwOXBMLA4wdvFGE@Ht;~6gKI5yKD0a4QcpGQzJ9U*&k zAADF?kZw~T3~4^cV4=-uY%vQ>wG}*ZgiP8cP13u)%8itSXXz^>Sac{ERyCrWTSp~ zbz8+Ed#F`v?l~uEmjU4*9^c&#-o#gBbv;b&tM`;w768$HTd$3`%zhm2-nHUXqw?Ti ztBlt%@Yr$%U48xb+x#roCzmF~JXy5a?Yh{uZzfTIZDnFNZn{2f*yvCIBU7_^)ZQ&ZTf>e9O%G#54VP3$!(q`5AwXCu8C=TK_{1fuXQRaRDvzNGI zAOqMT<;|~n904jiX4s2x>wnmn!xVZ&9{xX{T8+&tm0f<1f_dX5xxKcC7^&}|gDC`^ ztlSV6e|i+ki4PJi3YGwXPD95jMBk0ZVVD_Ur7kgKrb<@wWQs_bbr>o=iK)6oCCqfv zrT4a}rIp!)IaxiUreehu8?BhB`W2>FHv7o}>;Hxb@LDzI=TWY$fjKEj`j8Z_sT>ru zJMFU1sTIHFm9G!=@tSH0>DBD8YPLg`pi_vPFt}v4#Hn_IxN7KsymV zXB&VW%5?(2ZsNXn!oJ}vu%~!I9G5H4?jXJqUai6;pJ>6toQ9x^O18QD+Uh2n7O@>< zbewFNql(02S1^C=aXVXiI*2K%TT<6|(qrZJjnRqUZ^v+IAZfXBD1dVCq1-~7XLl-7Hh<@ZYVOhp@dSqB3Z`-G zx0veveMOJ3x860BjXtg}7Xqn=|N1D@+I5pzjO>)jscko~dMe-S@?P*k2-u$|*aV6k zKQ+{|bVV-KY4XH)1Y9!7y`ePLcG^xbg%mBhy4VV~QV?jc!R=o>A}SKAMv1+uOr5TK z2k}Q<^j{MUHWBHHU6+14*E@haSpQXEH#ZuvfiwnhLyHMi8Ew%)?@9*^@8*pY0Lm=9 zr!qE(6q(CUN?d?W!FbJY{{<=riYk5y1#B5B9OPT2%Qq%%Tg`MY!4)Sy%$*G)MJ7M4 zMkTn5%D?hm7N(-CU-nuzx9hWRZZIQp0YKqE*ruHCLOJ)(tjj)VX1RBKhB5L;!-Zae z!e-H3)DS%TJ{tgv%-T{#r%>6JE#0TZk7@@w`UQ)fM$$sf=-JXYnLnnV4G(pd__X^XZ>^#py^3+GUm z6AXnd3t_A@02$_TEkNt{fu;UCi?l%5*`0c@$rkUsjjyzwMm!Ny>S9 z9eBxcr?_6zd>UNUoPRD)`A1a-QTvH|)PJ>@g)o#d%${CFAFx!>-_L^|qyiL!W+4u8 z49T@lZ==h4(i#143#Wtw6vJF<7?Ti$AG+qlj#E$o2+$sbacc5nF z+pth-s0p0Qnt;})bm%L-?7r_H_nGo07g*kQ0%vxZnc-v&@qugZ`eNXxc6YE1?bcrc z_mc8>uwlwMaY4pvH8wvceAP5NKca`5pP#yolSe<2mD;U@#O-|^+4(XAnF^v>Xvp|e zBQxn&n=;x|qIpK7*5j8()q?z3iFiycUwnuum6?Rt9aC|7x*R1;JvaAi%0>aVwaT+X z6SfIF*xGP06tyWE1qf%ALyoE)=VzF?h8|O^ZO3Sdn-8PgI`sRoj4Q)RdU=Uq-fJ?|U?$Ob^EIxLlFlYiey~Y@u>&^ zQIbaF%$8f#tWaTjszvY2i5gh4#STmcB|smqBrq!3K-u|F>1tBoz-vBT0Zj?*-i)0r zO}l*}u0D!Z2s{XL+FU5a*H&xf#3-@5+Z#IH^+r-eA|!nNQ^$UXctZ#1;NZQ^tIZ*q zQ4KJ@7VnoO*>nT|$WT?)Au6g}c~^ym?X&rIEV~-%Lq9>b6U%Zl^n>maoo>!7C3ZDX zXdkg1yCXwT(xTsW1ynR^1nc}_pjn&C7cTrXCNrJcGi;3@l=?1gXF(rk0pHz2Uf2X|P*9Ss zfd0Tmj)g}rFEP~sgZ1+2wMy=qw>&qfukE_^k#Gqy1c54IfiKu0;@~ed(LBZfcB%I| z@t+%vI%}8XJ#VtW`9=fYyurND5W87}KNPpA5*wKN$$EpuNIZcXzklp9nr(O*AM;t49dQbW#{279Ngtz&P{G~%uuX36#-%VtbZBzMcVj|;PLU^Hbphx%`#xuLw#=4!z*-D zk}bbbLw(H)lmb+8&y{d+_FE~;UviNh8(DkTk&B*aOs{BVyKo{9Jp$fb03z zmD!}<=tod3Khuv`?oBd0}WQ{FLP4sT}z-%IDI`y$~62pq8E~RGQ%i;~f95eyWUrUHzsxCXk_}}czy?vDz~z3=GyHIDGKVdkfchz)#brVF6)U&wh&#~y@4N*R5%^xUW!EQIi^|#=yh*RNIb;0FC~xM5@rfk; znq0>__*qmPa_G!iwYH@RL_enQORpD^D}$uumlgxpY=`7PnaS7s^a;Nv879B9_={G?_p$%5IaEXWhK* z_2wO3-dB-7B^I1&uLFLLR0CN?MftXw|K3#nj;i04>i1mrj~{L^WYIGn{L~2Osi(9s z<=JtUWlRiil%POWh_U<+6sZx5(TqGx`?zMTuZjm}l0qOM<%*LfUnHlMCSuBVC-Do3~6%IfsWUY6$IZCI*a)r)=ZKIT9RY^HK$!)~>-vcXM-Oo<=btZjrajSS(xiw!D33|13 zM??8?EDv$+=rHRxlE4-%_>Mgr5$kRa@q{!hv_IQbf5ly}tB@-O*E^TaEmG9|;Gk>a zki_h69Yf!=Alk#`TtVDk#kq7oAw@6Q*cr>(thVfK2oN}-zNf%pS`CTiFoyJ4Wk~kC zqX@qJm?Wu`sLRFI4NA7!R*M&|Zt35A@t-*SzTIRF@Uwz2Z`ETP8A7)b%{%I+{vnsZ zDe_zk&<(AY-I6l3?U*-n?Vfo7vVN-MyCXU(Lx;#8oSVeBQ+BW0*s``tOJE|-i1 zj!11Twc>H2G$H}2FFC$asmnMmWLPSRnSj`4R^Oq3hqof}xwiF=4sFIVe1GXFQ7mj+ z-^;do+;|EdnEmn8&W)jvi!N9XZeR%4>{dBG)p)|;!`CVDVKvv1lwS@z)i7Ag{k!;S``T!Qq zwfr@1IB-S79na2v;0B z9vDN#X4}JUz4bZAH2mGG($k+FBBDbK$E})SmFTCgM|JG1CA9i|(#FJfXyMMF_(__r@h>Wqut$?W{ zImqZMP+T1{obfi#IGyq~4*ga#yEwa-M>+e4QrQUe8;UxvgmUv`;Byj+*yxdTF*{Cu zB57vKb6z<5@~DqG1z{C-+B#A270ZyTF*S7YbvWW{ws@|CStP*AL>uPSx=;&?bR3(+ zvOhE10z0Knso^CM6>4YRCR77FYrplq?%WDkv z7{H!k;C}P>SJxT9M9_$fc$ojzs$Y^7KM3a0e!hXuOtwlt6K0t9Ku%x zLZK}~(7>_WDJC5lp@PJ0jo-y+5o2KdE=T*_A1zbr z5p_%xx#um2&z^Z==KK+nYmG>z`H`u6AdKJXXj%R^aygjmPS=^`=xG*6Yod;9&d?ES zGch8CbFG~BMaHs(9zTAJHXoNyK6+}I0Br7WZhj$8OD9G|1R4Jlxb7{#p+>bn)$S`r z7;C}vp6a7&r*yA+gtRNK)(R5%X#;DURDCg=4W>aMJ=+%%hItYlvcm69Nk3&h~#S@U!I@p{XSW$*V%M}64S^|T79gO|H$X5+#Z)t7~E zRZ@v`__CkEE*F{4r$bW>!$ud$*!r-4GGyTz?|}ov_)obSYhG=_?zPibpC_ZEE#Dh8 z&rpGO^x$&oe}KD(-k*QCdNTEekr+l{JJf%a-a{S@bp@538;iB~P<_+ZON*%exli>4 z=-bnpyrUWO9@n0(6l~41D_bkxISa+KF+3r@cR7ls>Cm?UT|c@MdFP*`WV|*jVjucE z>XyJ@cfy7HYTDRj6~N74sl2BeX>Fq-&(u0kl0xbn4b8eF)=#&wKmXEqqnF)9YsCD# zr?<|cI%wDNjl^9xL65CwR?NPiB6PNe^}+*6YV;6Z>%ISc({kFSeh~VZATps>|BHH^ ztEwS($?SojFYv$MwZ_bNc^%7LmFBDRweeG)l9}e&yz7MKd70AQgr)lkrdU-8c5D|I zNIizx29bHa5Q{jd+SD$le~Sc$=*7$=d^b%ZFfcyFt}%%jdRuPZcb) z7>;Ap`B1#eCtjc>I27St#a%3w63^?UoYl{(tfhX87I zhoA0JXF=+A6wxc+X-a9f$2Dj?KR!m+vo}PVQg3H*v|)o^nMcEbzKCO{n+(Ma@f;(^ z@D=X2gx;^J39hbc2juIFk8Nc>nsG1>sZ<_E{!8_cu@dW(`;69IL5J5l=V$ax1_M&J z9k8(~MgYfu#Z7xteHfEB1GjhRRA`en&#!OSi1mwK0&l7km)%?aaV|mg>OL{Xl{TUvyI*#=uzYBS7M72zmksE|UN9XU*#C^`0q=W#2V~ai^up@Oo#M4%7lC~=pDsnr zR!N5p_3Gaa%ABhy4)0w~GtNo8MZVY&xUZsenzEhgDOZsJ&=NDr0MZEp(z~Hyz<{o3 z(@7K5os)>EFk_bHANni-Q{i4$=}<>$pg7&2cDWrVHJy?WDrQm^cF0RF>!||GQ|%9e z!E4Hgv|a}uq6S5LSW7$Hs2=U`c%`dy5K3~|nru?06fznr`XR70dRcQ~`9w>~T4@N7 zmmq(4^xd+y=}6!)KT3fhM)iOAW2BF9Q^f0lVv)$I+m?-@6A zl$@VNtjaC34PxpgPR_;9KEX7QN7@3_=^i<@T|Gt9EVBOwnILua@}zB$F^&8{!@B3o zngg-%OkGB~lR{h@aMBclCv6pXr@W+{B&DV!s22h23DRw2d?%)p@4^f0&z4dT1$WHM z@3y7hsE2IThTUW%`d->{$)7+e56FTU`m^hv8nSzIb~-)K5or6K)cqaM>XRZdig}d- zr?3|aN-4f_AFA`xna2ag*AaJ#w+&y8nUF|* zU_|rL;`zh4=JlnAjkQ+O$6E|~3yk`yC#oV#sF~2KW{fK)`C$VJBx9$lhS=Y0Q8;Rf z5}8!Yg-5b2LXK7+S!3@caGo)Yx=6G}7NUhC$LlGUS#h!2_#{fgb5~dKagHkUb`H7A z3(}c+DKC4w;NE2#)ShTm-y=8P2C{}8TUCknVeePQNN!+~5ogU+$d$Zk&e;8o7V);I zRKHj)-C4JsVx#35i$tife)Hn@Wb@K#W!1qW{Sz^Cw%g!Z8YmdIxjc_Wl<#O|xxQd+ zhXl<(8|P-2X5aM}8cq@M$W*0X$0dl7wUN1+_Bz>iz~FMG^SjX52qw0%0x@U`pV23o zrP@kn5Yzi4SU;l|{2U;&EhX~c(7dI>Ax7I1Utw4e%tG|eMIz13|J^oyrQH*z+IHSR z*9nYg{`)ia@C`rf;oJ%A0|ufWtPxrC{|1;S?!o)&d@kIh#N3X%+Sq|$G~bq54vl;) zEY(aJKzu>9hO^-B<3-MIE(?pbp`+(`mq`uo-Le1MZJ+907Awf>x5UydBAfwA*rc=&bA3?&IydSpSNMQfseRd#9$rxGD6^iLT$0~U6=YpMMfYRE)=h( ze#9ywrrV=1V^uk(+^89fW?8*=Cpk3=fBb4P7{n}7;+WM)SaVKI)h40q3bv|UD5K^v zS3jrkEL2VQR1RcxxrVAb*7K5agODF>6ogo29tCvA{fhK&z+Q8m^b6>HWa%w8-zsnpif?asH5Q8P&^=wGn3Y9dKX@j0JpY;0=={~@io&7CEV+hiQ5tTgyHav zM1y8Qmgw*8fnproniuZzut?^c4#ywq63}0}ZTHq6^~gR~MNH29swDB~%%1M)&1iGjxs{B{iIUbG3o96{ zCbs{Ai*CHj4ACRr$Gc=c=ARIe_S7)K&!mSq?VBWT3(bUz*217X`N}qn^~|GA#=^1R zcastauLBld8Yv_Tj^iT}Nk*QPZU*FFi8Ro(Ch^4V=+ysJv#9E2zFxhA7MYI0v0T09 zo<3>Wju(W5no8urK@txmFt;qXoRu>-(NCE`N2 zOYVeIsF+X{7npdOOt(U2?P>=%EQ7BhbLU|M&Lw14^byZ&>JqBjy^F``x!$6xO%#80 z4RLFmd^T>k&aB!sZ8~%nacf(?CYTh9zxE%YAO-Kl^BGT=1I2op2 zG%$(n;9KJOi;3O)?lyK{v1b&_2C9T3Z*=aEiI!;_8@td7%Q zp9>=sVZtt;jAOQ$6U{bl(7m$PA9PHr6QeL~u?U7tE@TmTm^|gez{$(J7*1^Jd@PK2 zKv~vb4xpR4N&uX z6)1<+^CqH#@@nxPf`O;^BUgU$yJVITuP{~DAuI%+IZf O3r^o`uSFL?k?@?9Th5gL literal 140000 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