| 1 | import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress"; |
| 2 | import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; |
| 3 | import { |
| 4 | XAiPromptFailureText, |
| 5 | isXAiTaskCompletedWakeNotification, |
| 6 | xAiRateLimitedErrorCode, |
| 7 | } from "./xaiAcpExtension.ts"; |
| 8 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 9 | import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; |
| 10 | import { |
| 11 | defaultInstanceIdForDriver, |
| 12 | ProviderDriverKind, |
| 13 | type OrchestrationV2ProviderCapabilities, |
| 14 | type RuntimeMode, |
| 15 | } from "@t3tools/contracts"; |
| 16 | import { GrokSettings } from "../settings.ts"; |
| 17 | import * as Crypto from "effect/Crypto"; |
| 18 | import * as Effect from "effect/Effect"; |
| 19 | import * as FileSystem from "effect/FileSystem"; |
| 20 | import * as Path from "effect/Path"; |
| 21 | import * as Layer from "effect/Layer"; |
| 22 | import * as Schema from "effect/Schema"; |
| 23 | import type * as Scope from "effect/Scope"; |
| 24 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 25 | import * as EffectAcpErrors from "effect-acp/errors"; |
| 26 | |
| 27 | import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; |
| 28 | import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 29 | import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging"; |
| 30 | import { |
| 31 | applyGrokAcpModelSelection, |
| 32 | currentGrokModelIdFromSessionSetup, |
| 33 | grokApprovalOptions, |
| 34 | makeGrokAcpRuntime, |
| 35 | resolveGrokAcpBaseModelId, |
| 36 | } from "./acpSupport.ts"; |
| 37 | import { |
| 38 | extractGrokPlanMarkdownFromToolCallData, |
| 39 | extractXAiAcpBackgroundToolMutation, |
| 40 | extractXAiAcpSubagentEndNotice, |
| 41 | extractXAiAcpSubagentUpdate, |
| 42 | extractXAiAskUserQuestionIdentity, |
| 43 | extractXAiAskUserQuestions, |
| 44 | extractXAiBackgroundTaskCompletion, |
| 45 | extractXAiKilledBackgroundTasks, |
| 46 | extractXAiMonitorTaskId, |
| 47 | isXAiPersistentMonitor, |
| 48 | extractXAiExitPlanMarkdown, |
| 49 | registerXAiSubagentFinished, |
| 50 | makeXAiAskUserQuestionCancelledResponse, |
| 51 | makeXAiAskUserQuestionResponse, |
| 52 | makeXAiExitPlanModeCapturedResponse, |
| 53 | normalizeXAiAcpToolCallState, |
| 54 | registerXAiBackgroundTaskTracking, |
| 55 | XAiAskUserQuestionRequest, |
| 56 | XAiExitPlanModeRequest, |
| 57 | } from "./xaiAcpExtension.ts"; |
| 58 | import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; |
| 59 | import { acpPermissionDisposition } from "@t3tools/provider-acp/server/clientPolicy"; |
| 60 | import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; |
| 61 | import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; |
| 62 | import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; |
| 63 | import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; |
| 64 | import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; |
| 65 | import { |
| 66 | ProviderAdapterDriverCreateError, |
| 67 | type ProviderAdapterDriver, |
| 68 | type ProviderAdapterDriverCreateInput, |
| 69 | } from "@t3tools/provider-core/server/adapterDriver"; |
| 70 | import { |
| 71 | AcpProviderCapabilitiesV2, |
| 72 | makeAcpAdapterV2, |
| 73 | type AcpAdapterV2ExtensionContext, |
| 74 | type AcpAdapterV2Flavor, |
| 75 | type AcpAdapterV2RuntimeInput, |
| 76 | } from "@t3tools/provider-acp/server/adapter"; |
| 77 | |
| 78 | export const GROK_PROVIDER = ProviderDriverKind.make("grok"); |
| 79 | const GROK_DRIVER_KIND = GROK_PROVIDER; |
| 80 | export const GROK_DEFAULT_INSTANCE_ID = defaultInstanceIdForDriver(GROK_DRIVER_KIND); |
| 81 | const DEFAULT_GROK_SETTINGS = Schema.decodeSync(GrokSettings)({}); |
| 82 | const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError); |
| 83 | |
| 84 | export const GrokProviderCapabilitiesV2 = { |
| 85 | ...AcpProviderCapabilitiesV2, |
| 86 | sessions: { |
| 87 | ...AcpProviderCapabilitiesV2.sessions, |
| 88 | supportsModelSwitchInSession: true, |
| 89 | supportsRuntimeModeSwitchInSession: false, |
| 90 | }, |
| 91 | threads: { |
| 92 | ...AcpProviderCapabilitiesV2.threads, |
| 93 | canReadThreadSnapshot: true, |
| 94 | canForkThread: false, |
| 95 | canForkFromTurn: false, |
| 96 | }, |
| 97 | subagents: { |
| 98 | ...AcpProviderCapabilitiesV2.subagents, |
| 99 | supportsSubagents: true, |
| 100 | exposesSubagentThreadIds: true, |
| 101 | emitsSubagentLifecycle: true, |
| 102 | }, |
| 103 | tools: { |
| 104 | ...AcpProviderCapabilitiesV2.tools, |
| 105 | supportsMcpTools: true, |
| 106 | }, |
| 107 | checkpointing: { |
| 108 | ...AcpProviderCapabilitiesV2.checkpointing, |
| 109 | providerCanReadConversationSnapshot: true, |
| 110 | }, |
| 111 | } satisfies OrchestrationV2ProviderCapabilities; |
| 112 | |
| 113 | export interface GrokAdapterV2Options { |
| 114 | readonly instanceId: Parameters<typeof makeAcpAdapterV2>[0]["instanceId"]; |
| 115 | readonly settings: GrokSettings; |
| 116 | readonly environment: NodeJS.ProcessEnv; |
| 117 | readonly hostPlatform: NodeJS.Platform; |
| 118 | readonly selfInvocation: SelfInvocation; |
| 119 | readonly nativeLogging?: Parameters<typeof makeAcpAdapterV2>[0]["nativeLogging"]; |
| 120 | readonly continuationRequests?: Parameters<typeof makeAcpAdapterV2>[0]["continuationRequests"]; |
| 121 | readonly testHooks?: Parameters<typeof makeAcpAdapterV2>[0]["testHooks"]; |
| 122 | readonly makeRuntime?: ( |
| 123 | input: AcpAdapterV2RuntimeInput, |
| 124 | ) => Effect.Effect< |
| 125 | AcpSessionRuntime.AcpSessionRuntime["Service"], |
| 126 | EffectAcpErrors.AcpError, |
| 127 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope |
| 128 | >; |
| 129 | readonly assertComplete?: Effect.Effect<void, EffectAcpErrors.AcpError>; |
| 130 | } |
| 131 | |
| 132 | const registerGrokAcpExtensions: NonNullable<AcpAdapterV2Flavor["registerExtensions"]> = ({ |
| 133 | runtime, |
| 134 | requestUserInput, |
| 135 | applyBackgroundTaskMutation, |
| 136 | finishSubagent, |
| 137 | captureProposedPlan, |
| 138 | lastProposedPlanMarkdown, |
| 139 | }) => |
| 140 | registerXAiBackgroundTaskTracking(runtime, applyBackgroundTaskMutation).pipe( |
| 141 | Effect.andThen(registerXAiSubagentFinished(runtime, finishSubagent)), |
| 142 | Effect.andThen(registerGrokAskUserQuestionExtensions({ runtime, requestUserInput })), |
| 143 | Effect.andThen( |
| 144 | registerGrokExitPlanModeExtensions({ |
| 145 | runtime, |
| 146 | captureProposedPlan, |
| 147 | lastProposedPlanMarkdown, |
| 148 | }), |
| 149 | ), |
| 150 | ); |
| 151 | |
| 152 | /** |
| 153 | * Grok intercepts exit_plan_mode and reverse-requests client approval. Capture |
| 154 | * the plan into T3's proposed-plan card and abandon the native gate so the |
| 155 | * turn does not hang (#8358; mirrors the Claude ExitPlanMode pattern). Plan |
| 156 | * content preference: the request payload, then the plan.md contents sniffed |
| 157 | * from tool calls this turn, then the empty-state placeholder. |
| 158 | */ |
| 159 | const registerGrokExitPlanModeExtensions = ({ |
| 160 | runtime, |
| 161 | captureProposedPlan, |
| 162 | lastProposedPlanMarkdown, |
| 163 | }: Pick< |
| 164 | AcpAdapterV2ExtensionContext, |
| 165 | "runtime" | "captureProposedPlan" | "lastProposedPlanMarkdown" |
| 166 | >) => |
| 167 | Effect.forEach( |
| 168 | ["x.ai/exit_plan_mode", "_x.ai/exit_plan_mode"] as const, |
| 169 | (method) => |
| 170 | runtime.handleExtRequest(method, XAiExitPlanModeRequest, (params) => |
| 171 | Effect.gen(function* () { |
| 172 | const fallback = yield* lastProposedPlanMarkdown; |
| 173 | yield* captureProposedPlan({ |
| 174 | planMarkdown: extractXAiExitPlanMarkdown(params, fallback), |
| 175 | }); |
| 176 | return makeXAiExitPlanModeCapturedResponse(); |
| 177 | }), |
| 178 | ), |
| 179 | { discard: true }, |
| 180 | ); |
| 181 | |
| 182 | const registerGrokAskUserQuestionExtensions = ({ |
| 183 | runtime, |
| 184 | requestUserInput, |
| 185 | }: Pick<AcpAdapterV2ExtensionContext, "runtime" | "requestUserInput">) => |
| 186 | Effect.forEach( |
| 187 | ["x.ai/ask_user_question", "_x.ai/ask_user_question"] as const, |
| 188 | (method) => |
| 189 | runtime.handleExtRequest(method, XAiAskUserQuestionRequest, (params, requestContext) => { |
| 190 | const identity = extractXAiAskUserQuestionIdentity(params); |
| 191 | const questions = extractXAiAskUserQuestions(params).map((question) => ({ |
| 192 | id: question.id, |
| 193 | header: question.header, |
| 194 | question: question.question, |
| 195 | options: [...question.options], |
| 196 | })); |
| 197 | return requestUserInput( |
| 198 | { |
| 199 | nativeItemId: `${identity.sessionId}:xai-question:${identity.toolCallId}`, |
| 200 | nativeRequestId: identity.toolCallId, |
| 201 | questions, |
| 202 | }, |
| 203 | requestContext, |
| 204 | ).pipe( |
| 205 | Effect.flatMap(({ acknowledgeNativeResponse, answers }) => |
| 206 | Effect.succeed( |
| 207 | answers === null |
| 208 | ? makeXAiAskUserQuestionCancelledResponse() |
| 209 | : makeXAiAskUserQuestionResponse(params, answers), |
| 210 | ).pipe(Effect.tap(() => acknowledgeNativeResponse)), |
| 211 | ), |
| 212 | ); |
| 213 | }), |
| 214 | { discard: true }, |
| 215 | ); |
| 216 | |
| 217 | /** |
| 218 | * Grok's permission mode is fixed at launch. Explicit approval or sandbox |
| 219 | * overrides launch it asking, so every mutating prompt reaches T3's policy |
| 220 | * check instead of being bypassed by always-approve or Grok's auto classifier. |
| 221 | */ |
| 222 | export function grokLaunchRuntimeMode( |
| 223 | runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, |
| 224 | ): RuntimeMode { |
| 225 | return runtimePolicy.approvalPolicy === undefined && runtimePolicy.sandboxPolicy === undefined |
| 226 | ? runtimePolicy.runtimeMode |
| 227 | : "approval-required"; |
| 228 | } |
| 229 | |
| 230 | /** The flavor runs Grok's launcher through the adapter's spawner. */ |
| 231 | export function makeGrokAcpAdapterFlavor( |
| 232 | options: GrokAdapterV2Options, |
| 233 | homeDirectory: string, |
| 234 | ): AcpAdapterV2Flavor { |
| 235 | return { |
| 236 | driver: GROK_PROVIDER, |
| 237 | runtimeHarness: "Grok", |
| 238 | capabilities: GrokProviderCapabilitiesV2, |
| 239 | interruptPromptOnCancel: false, |
| 240 | // User Stop (requestRuntimeRestart) still hard-kills the process group and |
| 241 | // respawns so existing background tasks stop too. Older 0.2.x builds could |
| 242 | // detach a cancelled foreground command (E3 harness 2026-07-18); current |
| 243 | // source kills foreground work but intentionally preserves already- |
| 244 | // backgrounded tasks. Non-Stop interrupts (mid-prompt steering, |
| 245 | // restart_active) omit requestRuntimeRestart and stay soft: session/cancel |
| 246 | // carries cancelTrigger=ctrl_c, the session survives, and background work |
| 247 | // remains available to the replacement turn. |
| 248 | restartRuntimeAfterInterrupt: true, |
| 249 | terminateRuntimeProcessGroupOnInterrupt: true, |
| 250 | // Steering restarts on a settled turn additionally skip session/cancel so |
| 251 | // fire-and-forget subagents survive the steer (E1 harness confirmed the |
| 252 | // Grok CLI accepts a concurrent session/prompt in that state). |
| 253 | preserveRuntimeOnSettledInterrupt: true, |
| 254 | // Grok ACP initialize reports promptCapabilities.image:false but the agent |
| 255 | // still accepts image content blocks (verified with real screenshots). |
| 256 | supportsImagePrompts: true, |
| 257 | supportsCompaction: true, |
| 258 | resolveModelId: (selection) => resolveGrokAcpBaseModelId(selection.model), |
| 259 | applyModelSelection: ({ runtime, startResult, modelSelection }) => |
| 260 | Effect.gen(function* () { |
| 261 | const legacy = startResult.initializeResult.protocolVersion === 1; |
| 262 | const options = legacy ? [] : yield* runtime.getConfigOptions; |
| 263 | const configuredModel = options.find((option) => option.category === "model")?.currentValue; |
| 264 | return yield* applyGrokAcpModelSelection({ |
| 265 | runtime: legacy |
| 266 | ? runtime |
| 267 | : { setSessionModel: (model) => runtime.setModel(model).pipe(Effect.as({})) }, |
| 268 | currentModelId: legacy |
| 269 | ? currentGrokModelIdFromSessionSetup(startResult.sessionSetupResult) |
| 270 | : typeof configuredModel === "string" |
| 271 | ? configuredModel |
| 272 | : undefined, |
| 273 | requestedModelId: resolveGrokAcpBaseModelId(modelSelection.model), |
| 274 | mapError: (cause) => cause, |
| 275 | }); |
| 276 | }), |
| 277 | makeRuntime: |
| 278 | options.makeRuntime ?? |
| 279 | (({ runtimePolicy, ...input }) => |
| 280 | makeGrokAcpRuntime({ |
| 281 | ...input, |
| 282 | interruptPromptOnCancel: input.interruptPromptOnCancel ?? false, |
| 283 | grokSettings: options.settings, |
| 284 | environment: ScriptProgress.withScriptProgressEnvironment( |
| 285 | options.environment, |
| 286 | input.processEnvironment ?? {}, |
| 287 | ), |
| 288 | runtimeMode: grokLaunchRuntimeMode(runtimePolicy), |
| 289 | })), |
| 290 | // In its Auto mode Grok decides routine actions itself and only asks about |
| 291 | // what its classifier blocked, so every prompt it sends goes to the user. |
| 292 | permissionDisposition: (policy, request) => |
| 293 | grokLaunchRuntimeMode(policy) === "auto" ? "ask" : acpPermissionDisposition(policy, request), |
| 294 | approvalOptions: grokApprovalOptions, |
| 295 | promptFailure: (cause) => |
| 296 | makeProviderFailure({ |
| 297 | cause, |
| 298 | ...(isAcpRequestError(cause) |
| 299 | ? { |
| 300 | // Grok's own failure text rides on the cause; makeProviderFailure |
| 301 | // redacts and bounds it before it reaches the user. |
| 302 | message: |
| 303 | cause.cause instanceof XAiPromptFailureText |
| 304 | ? cause.cause.message |
| 305 | : cause.errorMessage, |
| 306 | code: String(cause.code), |
| 307 | class: cause.code === xAiRateLimitedErrorCode ? "usage_limit" : "provider_error", |
| 308 | } |
| 309 | : { class: "provider_error" }), |
| 310 | }), |
| 311 | registerExtensions: registerGrokAcpExtensions, |
| 312 | extractSubagentUpdate: extractXAiAcpSubagentUpdate, |
| 313 | extractSubagentEndNotice: extractXAiAcpSubagentEndNotice, |
| 314 | normalizeToolCall: normalizeXAiAcpToolCallState, |
| 315 | // Show the plan while Grok is still writing it: plan.md writes under the |
| 316 | // Grok session dir surface as the proposed-plan card before exit (#8358). |
| 317 | extractProposedPlanMarkdown: (toolCall) => |
| 318 | extractGrokPlanMarkdownFromToolCallData(toolCall.data, { |
| 319 | platform: options.hostPlatform, |
| 320 | environment: options.environment, |
| 321 | homeDirectory, |
| 322 | }), |
| 323 | extractBackgroundTaskId: extractXAiMonitorTaskId, |
| 324 | extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation, |
| 325 | extractBackgroundTaskCompletion: (toolCall) => [ |
| 326 | ...extractXAiBackgroundTaskCompletion(toolCall), |
| 327 | ...extractXAiKilledBackgroundTasks(toolCall), |
| 328 | ], |
| 329 | isPersistentBackgroundTool: isXAiPersistentMonitor, |
| 330 | isProviderWakeNotification: isXAiTaskCompletedWakeNotification, |
| 331 | deferFinalizeForBackgroundWork: true, |
| 332 | enablePostSettleContinuation: true, |
| 333 | ...(options.assertComplete === undefined ? {} : { assertComplete: options.assertComplete }), |
| 334 | }; |
| 335 | } |
| 336 | |
| 337 | export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* ( |
| 338 | options: GrokAdapterV2Options, |
| 339 | ) { |
| 340 | return yield* makeAcpAdapterV2({ |
| 341 | instanceId: options.instanceId, |
| 342 | flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcess.HomeDirectory), |
| 343 | selfInvocation: options.selfInvocation, |
| 344 | ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }), |
| 345 | ...(options.continuationRequests === undefined |
| 346 | ? {} |
| 347 | : { continuationRequests: options.continuationRequests }), |
| 348 | ...(options.testHooks === undefined ? {} : { testHooks: options.testHooks }), |
| 349 | }); |
| 350 | }); |
| 351 | |
| 352 | export type GrokAdapterV2DriverEnv = |
| 353 | | ChildProcessSpawner.ChildProcessSpawner |
| 354 | | Crypto.Crypto |
| 355 | | FileSystem.FileSystem |
| 356 | | IdAllocator.IdAllocatorV2 |
| 357 | | Path.Path |
| 358 | | ScriptProgress.ScriptProgress |
| 359 | | McpProviderSessions.McpProviderSessions |
| 360 | | ProviderEventLoggers.ProviderEventLoggers |
| 361 | | ProviderHost.ProviderHost; |
| 362 | |
| 363 | export const GrokAdapterV2Driver: ProviderAdapterDriver<GrokSettings, GrokAdapterV2DriverEnv> = { |
| 364 | driverKind: GROK_DRIVER_KIND, |
| 365 | configSchema: GrokSettings, |
| 366 | defaultConfig: (): GrokSettings => DEFAULT_GROK_SETTINGS, |
| 367 | create: Effect.fn("GrokAdapterV2Driver.create")( |
| 368 | function* (input: ProviderAdapterDriverCreateInput<GrokSettings>) { |
| 369 | const hostEnvironment = yield* HostProcess.Environment; |
| 370 | const hostPlatform = yield* HostProcess.Platform; |
| 371 | const selfInvocation = yield* resolveSelfInvocation(); |
| 372 | const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; |
| 373 | const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; |
| 374 | const makeNativeLogger = yield* makeAcpNativeLoggerFactory(); |
| 375 | return yield* makeGrokAdapterV2({ |
| 376 | instanceId: input.instanceId, |
| 377 | settings: { ...input.config, enabled: input.enabled }, |
| 378 | environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), |
| 379 | hostPlatform, |
| 380 | selfInvocation, |
| 381 | continuationRequests, |
| 382 | nativeLogging: (threadId) => |
| 383 | makeNativeLogger({ |
| 384 | nativeEventLogger: providerEventLoggers.native, |
| 385 | provider: GROK_PROVIDER, |
| 386 | threadId, |
| 387 | }), |
| 388 | }); |
| 389 | }, |
| 390 | (effect, input) => |
| 391 | effect.pipe( |
| 392 | Effect.mapError( |
| 393 | (cause) => |
| 394 | new ProviderAdapterDriverCreateError({ |
| 395 | driver: GROK_DRIVER_KIND, |
| 396 | instanceId: input.instanceId, |
| 397 | detail: "Failed to create Grok ACP adapter.", |
| 398 | cause, |
| 399 | }), |
| 400 | ), |
| 401 | ), |
| 402 | ), |
| 403 | }; |
| 404 | |
| 405 | const layer: Layer.Layer< |
| 406 | ProviderAdapter.ProviderAdapterV2, |
| 407 | never, |
| 408 | | Path.Path |
| 409 | | ChildProcessSpawner.ChildProcessSpawner |
| 410 | | Crypto.Crypto |
| 411 | | FileSystem.FileSystem |
| 412 | | IdAllocator.IdAllocatorV2 |
| 413 | | ScriptProgress.ScriptProgress |
| 414 | | McpProviderSessions.McpProviderSessions |
| 415 | | ProviderEventLoggers.ProviderEventLoggers |
| 416 | | ProviderHost.ProviderHost |
| 417 | > = Layer.effect( |
| 418 | ProviderAdapter.ProviderAdapterV2, |
| 419 | Effect.gen(function* () { |
| 420 | const hostEnvironment = yield* HostProcess.Environment; |
| 421 | const hostPlatform = yield* HostProcess.Platform; |
| 422 | const selfInvocation = yield* resolveSelfInvocation(); |
| 423 | const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; |
| 424 | const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; |
| 425 | const makeNativeLogger = yield* makeAcpNativeLoggerFactory(); |
| 426 | return yield* makeGrokAdapterV2({ |
| 427 | instanceId: GROK_DEFAULT_INSTANCE_ID, |
| 428 | settings: DEFAULT_GROK_SETTINGS, |
| 429 | environment: hostEnvironment, |
| 430 | hostPlatform, |
| 431 | selfInvocation, |
| 432 | continuationRequests, |
| 433 | nativeLogging: (threadId) => |
| 434 | makeNativeLogger({ |
| 435 | nativeEventLogger: providerEventLoggers.native, |
| 436 | provider: GROK_PROVIDER, |
| 437 | threadId, |
| 438 | }), |
| 439 | }); |
| 440 | }), |
| 441 | ); |