| ... | @@ -1880,6 +1880,45 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -1880,6 +1880,45 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 1880 | const pendingRuntimeRequests = yield* Ref.make( | 1880 | const pendingRuntimeRequests = yield* Ref.make( |
| 1881 | new Map<string, PendingCodexRuntimeRequest>(), | 1881 | new Map<string, PendingCodexRuntimeRequest>(), |
| 1882 | ); | 1882 | ); |
| | 1883 | /** |
| | 1884 | * Approval kinds the user chose "Always allow this session" for, by |
| | 1885 | * native thread. Codex's own acceptForSession only skips the exact |
| | 1886 | * command or patch it answered, so every new command would ask again. |
| | 1887 | * A turn in another runtime mode ends the allowance, and so does the |
| | 1888 | * thread unloading, like Codex's own session approvals. |
| | 1889 | */ |
| | 1890 | const sessionApprovals = new Map< |
| | 1891 | string, |
| | 1892 | { readonly runtimeMode: RuntimeMode; readonly kinds: Set<ProviderRequestKind> } |
| | 1893 | >(); |
| | 1894 | /** |
| | 1895 | * The allowed kinds for an approval request's turn. Read it when the |
| | 1896 | * request arrives and add to that same set when the user answers, so a |
| | 1897 | * late answer from a turn whose mode has since ended is dropped. |
| | 1898 | */ |
| | 1899 | /** Every native turn registers here. A turn in another mode ends the allowance. */ |
| | 1900 | const endSessionApprovalsOnModeChange = (context: ActiveCodexTurnContext) => { |
| | 1901 | const nativeThreadId = context.providerThread.nativeThreadRef?.nativeId; |
| | 1902 | if ( |
| | 1903 | nativeThreadId != null && |
| | 1904 | sessionApprovals.get(nativeThreadId)?.runtimeMode !== |
| | 1905 | context.input.runtimePolicy.runtimeMode |
| | 1906 | ) { |
| | 1907 | sessionApprovals.delete(nativeThreadId); |
| | 1908 | } |
| | 1909 | }; |
| | 1910 | const sessionAllowance = ( |
| | 1911 | nativeThreadId: string, |
| | 1912 | context: ActiveCodexTurnContext, |
| | 1913 | ): Set<ProviderRequestKind> => { |
| | 1914 | const runtimeMode = context.input.runtimePolicy.runtimeMode; |
| | 1915 | const allowed = sessionApprovals.get(nativeThreadId); |
| | 1916 | if (allowed?.runtimeMode === runtimeMode) return allowed.kinds; |
| | 1917 | if (allowed !== undefined) return new Set(); |
| | 1918 | const kinds = new Set<ProviderRequestKind>(); |
| | 1919 | sessionApprovals.set(nativeThreadId, { runtimeMode, kinds }); |
| | 1920 | return kinds; |
| | 1921 | }; |
| 1883 | /** | 1922 | /** |
| 1884 | * Turn contexts retained past turn/completed while background command | 1923 | * Turn contexts retained past turn/completed while background command |
| 1885 | * items started in that turn are still running, so late item events | 1924 | * items started in that turn are still running, so late item events |
| ... | @@ -2030,6 +2069,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -2030,6 +2069,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 2030 | return next; | 2069 | return next; |
| 2031 | }); | 2070 | }); |
| 2032 | beginTurnTokenUsage(context); | 2071 | beginTurnTokenUsage(context); |
| | 2072 | endSessionApprovalsOnModeChange(context); |
| 2033 | yield* Ref.update(activeTurns, (current) => { | 2073 | yield* Ref.update(activeTurns, (current) => { |
| 2034 | const updated = new Map(current); | 2074 | const updated = new Map(current); |
| 2035 | updated.set(input.nativeTurnId, context); | 2075 | updated.set(input.nativeTurnId, context); |
| ... | @@ -2615,6 +2655,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -2615,6 +2655,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 2615 | itemPositions: new Map(), | 2655 | itemPositions: new Map(), |
| 2616 | }; | 2656 | }; |
| 2617 | beginTurnTokenUsage(activeContext); | 2657 | beginTurnTokenUsage(activeContext); |
| | 2658 | endSessionApprovalsOnModeChange(activeContext); |
| 2618 | yield* Ref.update(activeTurns, (current) => { | 2659 | yield* Ref.update(activeTurns, (current) => { |
| 2619 | const updated = new Map(current); | 2660 | const updated = new Map(current); |
| 2620 | updated.set(turn.nativeTurnId, activeContext); | 2661 | updated.set(turn.nativeTurnId, activeContext); |
| ... | @@ -5076,6 +5117,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5076,6 +5117,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5076 | payload, | 5117 | payload, |
| 5077 | ); | 5118 | ); |
| 5078 | } | 5119 | } |
| | 5120 | const allowance = sessionAllowance(payload.threadId, context); |
| | 5121 | if (allowance.has("command")) { |
| | 5122 | return { |
| | 5123 | decision: "acceptForSession", |
| | 5124 | } satisfies CodexSchema.CommandExecutionRequestApprovalResponse; |
| | 5125 | } |
| 5079 | | 5126 | |
| 5080 | const nativeRequestId = payload.approvalId ?? payload.itemId; | 5127 | const nativeRequestId = payload.approvalId ?? payload.itemId; |
| 5081 | const artifacts = yield* buildApprovalRequestArtifacts({ | 5128 | const artifacts = yield* buildApprovalRequestArtifacts({ |
| ... | @@ -5124,6 +5171,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5124,6 +5171,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5124 | }), | 5171 | }), |
| 5125 | ), | 5172 | ), |
| 5126 | ); | 5173 | ); |
| | 5174 | if (resolved === "acceptForSession" || resolved === "acceptAlways") { |
| | 5175 | allowance.add("command"); |
| | 5176 | } |
| 5127 | return { | 5177 | return { |
| 5128 | decision: resolved === "acceptAlways" ? "acceptForSession" : resolved, | 5178 | decision: resolved === "acceptAlways" ? "acceptForSession" : resolved, |
| 5129 | } satisfies CodexSchema.CommandExecutionRequestApprovalResponse; | 5179 | } satisfies CodexSchema.CommandExecutionRequestApprovalResponse; |
| ... | @@ -5139,6 +5189,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5139,6 +5189,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5139 | payload, | 5189 | payload, |
| 5140 | ); | 5190 | ); |
| 5141 | } | 5191 | } |
| | 5192 | const allowance = sessionAllowance(payload.threadId, context); |
| | 5193 | if (allowance.has("file-change")) { |
| | 5194 | return { |
| | 5195 | decision: "acceptForSession", |
| | 5196 | } satisfies CodexSchema.FileChangeRequestApprovalResponse; |
| | 5197 | } |
| 5142 | | 5198 | |
| 5143 | const artifacts = yield* buildApprovalRequestArtifacts({ | 5199 | const artifacts = yield* buildApprovalRequestArtifacts({ |
| 5144 | context, | 5200 | context, |
| ... | @@ -5184,6 +5240,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5184,6 +5240,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5184 | }), | 5240 | }), |
| 5185 | ), | 5241 | ), |
| 5186 | ); | 5242 | ); |
| | 5243 | if (resolved === "acceptForSession" || resolved === "acceptAlways") { |
| | 5244 | allowance.add("file-change"); |
| | 5245 | } |
| 5187 | return { | 5246 | return { |
| 5188 | decision: resolved === "acceptAlways" ? "acceptForSession" : resolved, | 5247 | decision: resolved === "acceptAlways" ? "acceptForSession" : resolved, |
| 5189 | } satisfies CodexSchema.FileChangeRequestApprovalResponse; | 5248 | } satisfies CodexSchema.FileChangeRequestApprovalResponse; |
| ... | @@ -5343,6 +5402,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5343,6 +5402,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5343 | payload, | 5402 | payload, |
| 5344 | ); | 5403 | ); |
| 5345 | } | 5404 | } |
| | 5405 | const allowance = sessionAllowance(payload.conversationId, context); |
| | 5406 | if (allowance.has("command")) { |
| | 5407 | return { |
| | 5408 | decision: "approved_for_session", |
| | 5409 | } satisfies CodexSchema.ExecCommandApprovalResponse; |
| | 5410 | } |
| 5346 | | 5411 | |
| 5347 | const nativeRequestId = payload.approvalId ?? payload.callId; | 5412 | const nativeRequestId = payload.approvalId ?? payload.callId; |
| 5348 | const artifacts = yield* buildApprovalRequestArtifacts({ | 5413 | const artifacts = yield* buildApprovalRequestArtifacts({ |
| ... | @@ -5389,6 +5454,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5389,6 +5454,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5389 | }), | 5454 | }), |
| 5390 | ), | 5455 | ), |
| 5391 | ); | 5456 | ); |
| | 5457 | if (resolved === "acceptForSession" || resolved === "acceptAlways") { |
| | 5458 | allowance.add("command"); |
| | 5459 | } |
| 5392 | return { | 5460 | return { |
| 5393 | decision: approvalDecisionToLegacyReviewDecision(resolved), | 5461 | decision: approvalDecisionToLegacyReviewDecision(resolved), |
| 5394 | } satisfies CodexSchema.ExecCommandApprovalResponse; | 5462 | } satisfies CodexSchema.ExecCommandApprovalResponse; |
| ... | @@ -5404,6 +5472,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5404,6 +5472,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5404 | payload, | 5472 | payload, |
| 5405 | ); | 5473 | ); |
| 5406 | } | 5474 | } |
| | 5475 | const allowance = sessionAllowance(payload.conversationId, context); |
| | 5476 | if (allowance.has("file-change")) { |
| | 5477 | return { |
| | 5478 | decision: "approved_for_session", |
| | 5479 | } satisfies CodexSchema.ApplyPatchApprovalResponse; |
| | 5480 | } |
| 5407 | | 5481 | |
| 5408 | const artifacts = yield* buildApprovalRequestArtifacts({ | 5482 | const artifacts = yield* buildApprovalRequestArtifacts({ |
| 5409 | context, | 5483 | context, |
| ... | @@ -5449,6 +5523,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -5449,6 +5523,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 5449 | }), | 5523 | }), |
| 5450 | ), | 5524 | ), |
| 5451 | ); | 5525 | ); |
| | 5526 | if (resolved === "acceptForSession" || resolved === "acceptAlways") { |
| | 5527 | allowance.add("file-change"); |
| | 5528 | } |
| 5452 | return { | 5529 | return { |
| 5453 | decision: approvalDecisionToLegacyReviewDecision(resolved), | 5530 | decision: approvalDecisionToLegacyReviewDecision(resolved), |
| 5454 | } satisfies CodexSchema.ApplyPatchApprovalResponse; | 5531 | } satisfies CodexSchema.ApplyPatchApprovalResponse; |
| ... | @@ -6706,6 +6783,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | ... | @@ -6706,6 +6783,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( |
| 6706 | unloadThread: (unloadInput) => | 6783 | unloadThread: (unloadInput) => |
| 6707 | Effect.gen(function* () { | 6784 | Effect.gen(function* () { |
| 6708 | const nativeThreadId = yield* getNativeThreadId(unloadInput.providerThread); | 6785 | const nativeThreadId = yield* getNativeThreadId(unloadInput.providerThread); |
| | 6786 | sessionApprovals.delete(nativeThreadId); |
| 6709 | yield* client.request("thread/unsubscribe", { threadId: nativeThreadId }); | 6787 | yield* client.request("thread/unsubscribe", { threadId: nativeThreadId }); |
| 6710 | }).pipe( | 6788 | }).pipe( |
| 6711 | Effect.mapError((cause) => | 6789 | Effect.mapError((cause) => |