@bitkyc08/opencodex 2.27.0 → 2.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-7jlKgmJd.js → index-D2sP-biU.js} +11 -11
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +59 -0
- package/src/adapters/base.ts +2 -0
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +21 -4
- package/src/adapters/openai-chat.ts +151 -54
- package/src/adapters/openai-responses.ts +37 -0
- package/src/cli/index.ts +19 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +55 -2
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/sync.ts +57 -11
- package/src/codex/convergence.ts +61 -13
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/generated/compatibility-version.json +51 -39
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +74 -5
- package/src/providers/service-tier.ts +16 -8
- package/src/responses/parser.ts +3 -9
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +7 -0
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +41 -3
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +54 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +246 -39
- package/src/server/responses/responses-field-backfill.ts +88 -6
- package/src/server/responses/terminal-guard.ts +10 -0
- package/src/server/responses-tool-search-repair.ts +217 -0
- package/src/server/system-env.ts +74 -5
- package/src/usage/log.ts +4 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isTranslatorBudgetExceededError,
|
|
3
|
+
type TranslatorBudget,
|
|
4
|
+
} from "../lib/translator-budget";
|
|
5
|
+
import {
|
|
6
|
+
restoreRoutedToolSearchCalls,
|
|
7
|
+
} from "../responses/tool-search-compat";
|
|
8
|
+
import {
|
|
9
|
+
replaceSseDataPayload,
|
|
10
|
+
sseDataPayload,
|
|
11
|
+
type SseBlockRewrite,
|
|
12
|
+
} from "./sse-payload-rewrite";
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PendingArgumentBlock = {
|
|
19
|
+
block: string;
|
|
20
|
+
itemId?: string;
|
|
21
|
+
outputIndex?: number;
|
|
22
|
+
retainedBytes: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const MAX_PENDING_ARGUMENT_FRAMES = 256;
|
|
26
|
+
const MAX_PENDING_ARGUMENT_BYTES = 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Public Responses gateways stream a lowered search as a normal function lifecycle. Codex expects
|
|
30
|
+
* only `tool_search_call` items, so classify each item before dropping its function-argument
|
|
31
|
+
* frames. Unknown early argument frames stay bounded until their item arrives.
|
|
32
|
+
*/
|
|
33
|
+
export function createRoutedToolSearchRestoreBlockRewrite(
|
|
34
|
+
names: ReadonlySet<string>,
|
|
35
|
+
budget?: TranslatorBudget,
|
|
36
|
+
): SseBlockRewrite {
|
|
37
|
+
const routedItemIds = new Set<string>();
|
|
38
|
+
const ordinaryItemIds = new Set<string>();
|
|
39
|
+
let pendingArguments: PendingArgumentBlock[] = [];
|
|
40
|
+
let pendingArgumentBytes = 0;
|
|
41
|
+
let passthrough = false;
|
|
42
|
+
let disposed = false;
|
|
43
|
+
|
|
44
|
+
const releaseAll = (): void => {
|
|
45
|
+
if (disposed) return;
|
|
46
|
+
disposed = true;
|
|
47
|
+
if (pendingArgumentBytes > 0) {
|
|
48
|
+
budget?.releaseRetained(pendingArgumentBytes, { kind: "retained_collectors" });
|
|
49
|
+
}
|
|
50
|
+
pendingArguments = [];
|
|
51
|
+
pendingArgumentBytes = 0;
|
|
52
|
+
routedItemIds.clear();
|
|
53
|
+
ordinaryItemIds.clear();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const retainPending = (
|
|
57
|
+
block: string,
|
|
58
|
+
itemId: string | undefined,
|
|
59
|
+
outputIndex: number | undefined,
|
|
60
|
+
): readonly string[] | null => {
|
|
61
|
+
const retainedBytes = Buffer.byteLength(block, "utf8");
|
|
62
|
+
const overflow = pendingArguments.length >= MAX_PENDING_ARGUMENT_FRAMES
|
|
63
|
+
|| pendingArgumentBytes + retainedBytes > MAX_PENDING_ARGUMENT_BYTES;
|
|
64
|
+
if (overflow) {
|
|
65
|
+
const flushed = [...pendingArguments.map(pending => pending.block), block];
|
|
66
|
+
if (pendingArgumentBytes > 0) {
|
|
67
|
+
budget?.releaseRetained(pendingArgumentBytes, { kind: "retained_collectors" });
|
|
68
|
+
}
|
|
69
|
+
pendingArguments = [];
|
|
70
|
+
pendingArgumentBytes = 0;
|
|
71
|
+
passthrough = true;
|
|
72
|
+
// Deliberately KEEP routedItemIds. Overflow means we stop buffering UNKNOWN frames, not
|
|
73
|
+
// that we forget what we already classified: an item restored to `tool_search_call`
|
|
74
|
+
// upstream of here would otherwise start emitting `function_call_arguments.*` again and
|
|
75
|
+
// the client would see a mixed private/public lifecycle for one call.
|
|
76
|
+
ordinaryItemIds.clear();
|
|
77
|
+
return flushed;
|
|
78
|
+
}
|
|
79
|
+
if (retainedBytes > 0) {
|
|
80
|
+
try {
|
|
81
|
+
budget?.chargeRetained(retainedBytes, { kind: "retained_collectors" });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (!isTranslatorBudgetExceededError(error)) throw error;
|
|
84
|
+
const flushed = [...pendingArguments.map(pending => pending.block), block];
|
|
85
|
+
if (pendingArgumentBytes > 0) {
|
|
86
|
+
budget?.releaseRetained(pendingArgumentBytes, { kind: "retained_collectors" });
|
|
87
|
+
}
|
|
88
|
+
pendingArguments = [];
|
|
89
|
+
pendingArgumentBytes = 0;
|
|
90
|
+
passthrough = true;
|
|
91
|
+
// Same reasoning as the frame/byte overflow above: an already-restored routed item
|
|
92
|
+
// must keep its frames suppressed even once buffering stops.
|
|
93
|
+
ordinaryItemIds.clear();
|
|
94
|
+
return flushed;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
pendingArguments.push({ block, itemId, outputIndex, retainedBytes });
|
|
98
|
+
pendingArgumentBytes += retainedBytes;
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const takePending = (
|
|
103
|
+
itemId: string | undefined,
|
|
104
|
+
outputIndex: number | undefined,
|
|
105
|
+
): string[] => {
|
|
106
|
+
const matched: PendingArgumentBlock[] = [];
|
|
107
|
+
const remaining: PendingArgumentBlock[] = [];
|
|
108
|
+
for (const pending of pendingArguments) {
|
|
109
|
+
const matches = pending.itemId !== undefined
|
|
110
|
+
? itemId !== undefined && pending.itemId === itemId
|
|
111
|
+
: outputIndex !== undefined && pending.outputIndex === outputIndex;
|
|
112
|
+
(matches ? matched : remaining).push(pending);
|
|
113
|
+
}
|
|
114
|
+
pendingArguments = remaining;
|
|
115
|
+
const retainedBytes = matched.reduce((total, pending) => total + pending.retainedBytes, 0);
|
|
116
|
+
if (retainedBytes > 0) {
|
|
117
|
+
budget?.releaseRetained(retainedBytes, { kind: "retained_collectors" });
|
|
118
|
+
pendingArgumentBytes = Math.max(0, pendingArgumentBytes - retainedBytes);
|
|
119
|
+
}
|
|
120
|
+
return matched.map(pending => {
|
|
121
|
+
if (pending.itemId !== undefined || itemId === undefined) return pending.block;
|
|
122
|
+
const payload = sseDataPayload(pending.block);
|
|
123
|
+
if (payload === null) return pending.block;
|
|
124
|
+
try {
|
|
125
|
+
const parsed: unknown = JSON.parse(payload);
|
|
126
|
+
return isPlainObject(parsed)
|
|
127
|
+
? replaceSseDataPayload(pending.block, JSON.stringify({ ...parsed, item_id: itemId }))
|
|
128
|
+
: pending.block;
|
|
129
|
+
} catch {
|
|
130
|
+
return pending.block;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const rewrite: SseBlockRewrite = (block: string): readonly string[] => {
|
|
136
|
+
if (disposed) return [block];
|
|
137
|
+
const payload = sseDataPayload(block);
|
|
138
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
139
|
+
let parsed: unknown;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(payload);
|
|
142
|
+
} catch {
|
|
143
|
+
return [block];
|
|
144
|
+
}
|
|
145
|
+
if (!isPlainObject(parsed)) return [block];
|
|
146
|
+
|
|
147
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
148
|
+
// After overflow we stop BUFFERING unknown frames, but an item already restored to
|
|
149
|
+
// `tool_search_call` must keep its public argument frames suppressed — otherwise the client
|
|
150
|
+
// receives a private item followed by `function_call_arguments.*` for the same id, which is
|
|
151
|
+
// exactly the mixed lifecycle this rewrite exists to prevent. Everything else passes through.
|
|
152
|
+
if (passthrough) {
|
|
153
|
+
const passthroughItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
|
|
154
|
+
const isArgumentEvent = type === "response.function_call_arguments.delta"
|
|
155
|
+
|| type === "response.function_call_arguments.done";
|
|
156
|
+
if (isArgumentEvent && passthroughItemId && routedItemIds.has(passthroughItemId)) return [];
|
|
157
|
+
return [block];
|
|
158
|
+
}
|
|
159
|
+
const outputIndex = typeof parsed.output_index === "number"
|
|
160
|
+
&& Number.isInteger(parsed.output_index)
|
|
161
|
+
&& parsed.output_index >= 0
|
|
162
|
+
? parsed.output_index
|
|
163
|
+
: undefined;
|
|
164
|
+
if (
|
|
165
|
+
(type === "response.output_item.added" || type === "response.output_item.done")
|
|
166
|
+
&& isPlainObject(parsed.item)
|
|
167
|
+
&& parsed.item.type === "function_call"
|
|
168
|
+
&& typeof parsed.item.name === "string"
|
|
169
|
+
) {
|
|
170
|
+
const itemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
|
|
171
|
+
const routed = names.has(parsed.item.name);
|
|
172
|
+
if (itemId) {
|
|
173
|
+
if (routed) {
|
|
174
|
+
routedItemIds.add(itemId);
|
|
175
|
+
ordinaryItemIds.delete(itemId);
|
|
176
|
+
} else {
|
|
177
|
+
ordinaryItemIds.add(itemId);
|
|
178
|
+
routedItemIds.delete(itemId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const pending = takePending(itemId, outputIndex);
|
|
182
|
+
const restored = routed ? restoreRoutedToolSearchCalls(parsed, names) : { value: parsed, changed: false };
|
|
183
|
+
const restoredBlock = restored.changed
|
|
184
|
+
? replaceSseDataPayload(block, JSON.stringify(restored.value))
|
|
185
|
+
: block;
|
|
186
|
+
// Classification is retained past `output_item.done` for BOTH kinds, until the terminal
|
|
187
|
+
// event releases everything.
|
|
188
|
+
//
|
|
189
|
+
// `done` ends the item, not the id's relevance. Forgetting a ROUTED id let a trailing
|
|
190
|
+
// `function_call_arguments.*` — which some upstreams emit after done — fall through to
|
|
191
|
+
// the unknown-id branch and reach the client as a public frame for an item the client
|
|
192
|
+
// was told is a private `tool_search_call`: the mixed lifecycle this rewrite exists to
|
|
193
|
+
// prevent. Forgetting an ORDINARY id is not leak-shaped but is not free either, because
|
|
194
|
+
// the same fall-through buffers its trailing frames as unknown and delays them until an
|
|
195
|
+
// item that will never arrive. Neither id is dropped early.
|
|
196
|
+
return routed ? [restoredBlock] : [...pending, restoredBlock];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const itemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
|
|
200
|
+
const argumentEvent = type === "response.function_call_arguments.delta"
|
|
201
|
+
|| type === "response.function_call_arguments.done";
|
|
202
|
+
if (argumentEvent && (!itemId || (!routedItemIds.has(itemId) && !ordinaryItemIds.has(itemId)))) {
|
|
203
|
+
return retainPending(block, itemId, outputIndex) ?? [];
|
|
204
|
+
}
|
|
205
|
+
if (argumentEvent && itemId && routedItemIds.has(itemId)) return [];
|
|
206
|
+
|
|
207
|
+
const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
|
|
208
|
+
if (!terminal) return [block];
|
|
209
|
+
const restored = restoreRoutedToolSearchCalls(parsed, names);
|
|
210
|
+
releaseAll();
|
|
211
|
+
return restored.changed
|
|
212
|
+
? [replaceSseDataPayload(block, JSON.stringify(restored.value))]
|
|
213
|
+
: [block];
|
|
214
|
+
};
|
|
215
|
+
rewrite.dispose = releaseAll;
|
|
216
|
+
return rewrite;
|
|
217
|
+
}
|
package/src/server/system-env.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
5
|
import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
6
6
|
import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens } from "../claude/auth-detect";
|
|
@@ -118,15 +118,84 @@ export function uninstallShellHook(): { removed: boolean; reason?: string } {
|
|
|
118
118
|
try {
|
|
119
119
|
const content = readFileSync(zshrcPath, "utf8");
|
|
120
120
|
if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" };
|
|
121
|
-
//
|
|
122
|
-
|
|
121
|
+
// Match CR?LF, not LF alone. A .zshrc with CRLF line endings — ordinary on a home
|
|
122
|
+
// directory an editor or another OS has touched — did not match, so the file was
|
|
123
|
+
// rewritten unchanged and the caller was told the hook was removed. Reporting success
|
|
124
|
+
// while the hook still sources on every new shell is the worse of the two failures.
|
|
125
|
+
const cleaned = content.replace(/\r?\n?# opencodex claude-env hook\r?\n\[.*claude-env\.sh.*(?:\r?\n)?/g, "\n");
|
|
126
|
+
// Verify instead of assuming: if the marker survives, the block is shaped in a way this
|
|
127
|
+
// pattern does not own, and the honest answer is failure rather than a silent no-op.
|
|
128
|
+
if (cleaned.includes(SHELL_HOOK_MARKER)) {
|
|
129
|
+
return { removed: false, reason: "hook block present but not in the expected shape; remove it manually" };
|
|
130
|
+
}
|
|
123
131
|
writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 });
|
|
124
132
|
return { removed: true };
|
|
125
|
-
} catch {
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") {
|
|
135
|
+
return { removed: false, reason: "not installed" };
|
|
136
|
+
}
|
|
126
137
|
return { removed: false, reason: "read/write failed" };
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
/** Whether a real `claude` executable is discoverable from this process's PATH. */
|
|
142
|
+
export function claudeCodeCliInstalled(pathValue = process.env.PATH): boolean {
|
|
143
|
+
if (!pathValue) return false;
|
|
144
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
145
|
+
// An empty PATH segment means the current directory. Do not let the proxy treat a
|
|
146
|
+
// workspace-local file as a durable user installation.
|
|
147
|
+
if (!directory) continue;
|
|
148
|
+
const candidate = join(directory, "claude");
|
|
149
|
+
try {
|
|
150
|
+
if (!statSync(candidate).isFile()) continue;
|
|
151
|
+
accessSync(candidate, constants.X_OK);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
// Keep scanning PATH after missing, non-file, and non-executable entries.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Keep the shell hook aligned with the integration that can actually consume it.
|
|
162
|
+
* Claude Desktop uses its own profile and does not source `.zshrc`; this hook exists
|
|
163
|
+
* only for plain Claude Code CLI launches.
|
|
164
|
+
*
|
|
165
|
+
* Reconciliation is PATH-sensitive by construction: "Claude Code is installed" is answered
|
|
166
|
+
* from the PATH of whichever process calls this. A launchd/service context with a stripped
|
|
167
|
+
* PATH can therefore fail to see a `claude` the user's interactive shell finds, and this will
|
|
168
|
+
* remove the hook. That is the intended failure direction — removing an OpenCodex-owned block
|
|
169
|
+
* is reversible on the next foreground `ocx start`, whereas leaving a hook pointing at an
|
|
170
|
+
* uninstalled CLI is the stale state this reconciliation exists to clear. Only the block
|
|
171
|
+
* carrying our own marker is ever touched; user lines are preserved.
|
|
172
|
+
*/
|
|
173
|
+
export function reconcileShellHook(systemEnvInjected: boolean): {
|
|
174
|
+
changed: boolean;
|
|
175
|
+
state: "installed" | "absent" | "failed";
|
|
176
|
+
reason?: string;
|
|
177
|
+
} {
|
|
178
|
+
if (process.platform !== "darwin") return { changed: false, state: "absent", reason: "not macOS" };
|
|
179
|
+
if (systemEnvInjected && claudeCodeCliInstalled()) {
|
|
180
|
+
const result = installShellHook();
|
|
181
|
+
if (result.installed) return { changed: true, state: "installed" };
|
|
182
|
+
if (result.reason === "already installed") {
|
|
183
|
+
return { changed: false, state: "installed", reason: result.reason };
|
|
184
|
+
}
|
|
185
|
+
return { changed: false, state: "failed", reason: result.reason ?? "install failed" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = uninstallShellHook();
|
|
189
|
+
if (!result.removed && result.reason !== "not installed") {
|
|
190
|
+
return { changed: false, state: "failed", reason: result.reason ?? "remove failed" };
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
changed: result.removed,
|
|
194
|
+
state: "absent",
|
|
195
|
+
reason: systemEnvInjected ? "Claude Code not installed" : "system environment inactive",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
const SYSTEM_ENV_NAMES = [
|
|
131
200
|
"ANTHROPIC_BASE_URL",
|
|
132
201
|
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
|
package/src/usage/log.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface PersistedUsageEntry {
|
|
|
84
84
|
conversationId?: string;
|
|
85
85
|
resolvedModel?: string;
|
|
86
86
|
requestedModel?: string;
|
|
87
|
+
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
88
|
+
shadowCallRewrittenFrom?: string;
|
|
87
89
|
/** Reasoning effort / service-tier metadata for GUI Logs after restart. */
|
|
88
90
|
requestedEffort?: string;
|
|
89
91
|
/** Adapter-normalized tier and exact upstream parameter emitted for this request. */
|
|
@@ -427,6 +429,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
427
429
|
const tierOutcome = entry.tierOutcome ? normalizeAttemptTierOutcome(entry.tierOutcome) : undefined;
|
|
428
430
|
const callerServiceTier = sanitizeLogMetadataString(entry.callerServiceTier);
|
|
429
431
|
const responseServiceTier = sanitizeLogMetadataString(entry.responseServiceTier);
|
|
432
|
+
const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
|
|
430
433
|
const routeDecision = entry.routeDecision
|
|
431
434
|
? normalizeRouteDecisionTrace(entry.routeDecision)
|
|
432
435
|
: undefined;
|
|
@@ -453,6 +456,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
453
456
|
: {}),
|
|
454
457
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
455
458
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
459
|
+
...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
|
|
456
460
|
...(typeof entry.requestedEffort === "string" && entry.requestedEffort
|
|
457
461
|
? { requestedEffort: capMetadataString(entry.requestedEffort) }
|
|
458
462
|
: {}),
|