engineering-behavior-observatory 0.2.1
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/LICENSE +202 -0
- package/README.md +151 -0
- package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
- package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
- package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
- package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
- package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
- package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
- package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
- package/contracts/openhands-agent-server-v1.44.1.json +65 -0
- package/contracts/openhands-agent-server-v1.46.0.json +46 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
- package/dist/src/agent-sdk-normalizer.d.ts +22 -0
- package/dist/src/agent-sdk-normalizer.js +995 -0
- package/dist/src/agent-sdk-run.d.ts +31 -0
- package/dist/src/agent-sdk-run.js +242 -0
- package/dist/src/agent-sdk-runner.d.ts +96 -0
- package/dist/src/agent-sdk-runner.js +407 -0
- package/dist/src/agent-sdk.d.ts +294 -0
- package/dist/src/agent-sdk.js +644 -0
- package/dist/src/aggregation.d.ts +183 -0
- package/dist/src/aggregation.js +513 -0
- package/dist/src/artifacts.d.ts +36 -0
- package/dist/src/artifacts.js +1372 -0
- package/dist/src/atlas-grafana.d.ts +377 -0
- package/dist/src/atlas-grafana.js +47 -0
- package/dist/src/atlas-html.d.ts +2 -0
- package/dist/src/atlas-html.js +34 -0
- package/dist/src/atlas.d.ts +130 -0
- package/dist/src/atlas.js +235 -0
- package/dist/src/behavior-assertions.d.ts +95 -0
- package/dist/src/behavior-assertions.js +154 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.js +957 -0
- package/dist/src/codex-judge.d.ts +44 -0
- package/dist/src/codex-judge.js +214 -0
- package/dist/src/codex-run.d.ts +114 -0
- package/dist/src/codex-run.js +519 -0
- package/dist/src/codex.d.ts +187 -0
- package/dist/src/codex.js +1028 -0
- package/dist/src/contracts.d.ts +155 -0
- package/dist/src/contracts.js +866 -0
- package/dist/src/corpus.d.ts +67 -0
- package/dist/src/corpus.js +540 -0
- package/dist/src/cursor-sdk-runner.d.ts +81 -0
- package/dist/src/cursor-sdk-runner.js +362 -0
- package/dist/src/cursor-sdk.d.ts +81 -0
- package/dist/src/cursor-sdk.js +1078 -0
- package/dist/src/deepseek-adapter.d.ts +152 -0
- package/dist/src/deepseek-adapter.js +777 -0
- package/dist/src/exports.d.ts +69 -0
- package/dist/src/exports.js +966 -0
- package/dist/src/human-calibration.d.ts +167 -0
- package/dist/src/human-calibration.js +618 -0
- package/dist/src/index.d.ts +71 -0
- package/dist/src/index.js +38 -0
- package/dist/src/lifecycle.d.ts +196 -0
- package/dist/src/lifecycle.js +1889 -0
- package/dist/src/normalization-integrity.d.ts +128 -0
- package/dist/src/normalization-integrity.js +429 -0
- package/dist/src/openhands-run.d.ts +36 -0
- package/dist/src/openhands-run.js +415 -0
- package/dist/src/openhands.d.ts +130 -0
- package/dist/src/openhands.js +859 -0
- package/dist/src/pi.d.ts +203 -0
- package/dist/src/pi.js +1345 -0
- package/dist/src/process-protocol.d.ts +245 -0
- package/dist/src/process-protocol.js +1261 -0
- package/dist/src/retained-evidence.d.ts +12 -0
- package/dist/src/retained-evidence.js +201 -0
- package/dist/src/run-bundles.d.ts +193 -0
- package/dist/src/run-bundles.js +993 -0
- package/dist/src/scheduler.d.ts +109 -0
- package/dist/src/scheduler.js +1080 -0
- package/dist/src/semantic-judge.d.ts +194 -0
- package/dist/src/semantic-judge.js +875 -0
- package/dist/src/structural-observations.d.ts +124 -0
- package/dist/src/structural-observations.js +671 -0
- package/dist/src/task-packets.d.ts +115 -0
- package/dist/src/task-packets.js +683 -0
- package/dist/src/uniform-events.d.ts +120 -0
- package/dist/src/uniform-events.js +158 -0
- package/dist/src/verifiers.d.ts +116 -0
- package/dist/src/verifiers.js +819 -0
- package/dist/src/workspaces.d.ts +56 -0
- package/dist/src/workspaces.js +1314 -0
- package/docs/README.md +60 -0
- package/docs/development/README.md +71 -0
- package/docs/development/documentation-sync.md +58 -0
- package/docs/development/extension-contracts.md +307 -0
- package/docs/evaluation/README.md +48 -0
- package/docs/evaluation/aggregation.md +112 -0
- package/docs/evaluation/behavior-assertions.md +45 -0
- package/docs/evaluation/human-calibration.md +170 -0
- package/docs/evaluation/normalization-integrity.md +80 -0
- package/docs/evaluation/semantic-judge.md +188 -0
- package/docs/evaluation/structural-observations.md +84 -0
- package/docs/evaluation/uniform-events.md +131 -0
- package/docs/guides/atlas.md +144 -0
- package/docs/guides/evidence-and-sharing.md +100 -0
- package/docs/guides/operator-guide.md +424 -0
- package/docs/guides/quickstart.md +152 -0
- package/docs/guides/telemetry.md +66 -0
- package/docs/harnesses/README.md +31 -0
- package/docs/harnesses/claude-agent-sdk.md +61 -0
- package/docs/harnesses/codex-harness.md +174 -0
- package/docs/harnesses/cursor-sdk.md +149 -0
- package/docs/harnesses/deepseek-harness.md +134 -0
- package/docs/harnesses/openhands-agent-server.md +153 -0
- package/docs/harnesses/pi-sdk.md +66 -0
- package/docs/reference/README.md +19 -0
- package/docs/reference/agent-sdk-operational-runner.md +118 -0
- package/docs/reference/cli.md +114 -0
- package/docs/reference/contracts.md +222 -0
- package/docs/reference/run-bundle-contract.md +354 -0
- package/docs/reference/run-lifecycle.md +54 -0
- package/examples/cursor-sdk/README.md +11 -0
- package/examples/cursor-sdk/capture-profile.json +1 -0
- package/examples/cursor-sdk/harness.json +1 -0
- package/examples/cursor-sdk/model.json +1 -0
- package/examples/cursor-sdk/native-limits.json +1 -0
- package/examples/cursor-sdk/native-tool-policy.json +1 -0
- package/ontology/behavior-categories.v1.json +46 -0
- package/package.json +66 -0
- package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
- package/release/0.1.0/README.md +46 -0
- package/release/0.1.0/reproducibility.json +61 -0
- package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
- package/release/0.2.0/README.md +42 -0
- package/release/0.2.0/reproducibility.json +73 -0
- package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
- package/release/0.2.1/README.md +63 -0
- package/release/0.2.1/reproducibility.json +73 -0
- package/release/README.md +13 -0
- package/schemas/aggregation.v1.json +293 -0
- package/schemas/behavior-assertions.v1.json +220 -0
- package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
- package/schemas/experiment.v1.schema.json +188 -0
- package/schemas/human-calibration.v1.json +336 -0
- package/schemas/normalization-integrity.v1.json +302 -0
- package/schemas/run-bundles/v1.json +1040 -0
- package/schemas/run-queue.v1.schema.json +206 -0
- package/schemas/semantic-judge.v1.json +380 -0
- package/schemas/structural-observations.v1.json +131 -0
- package/schemas/task-packet-freeze.v1.schema.json +106 -0
- package/schemas/task-packet.v1.schema.json +234 -0
- package/schemas/uniform-events/v1.json +289 -0
- package/scripts/atlas-grafana.sh +18 -0
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { lstat, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { spawnProtocolProcess, } from "./process-protocol.js";
|
|
7
|
+
import { createCapturedNativeEvidenceResolver, describeNormalizedDataset, validateNormalizedDataset, } from "./normalization-integrity.js";
|
|
8
|
+
import { validateUniformEvents, } from "./uniform-events.js";
|
|
9
|
+
export const CODEX_APP_SERVER_VERSION = "0.153.4";
|
|
10
|
+
export const CODEX_ADAPTER_VERSION = "0.1.0";
|
|
11
|
+
export const CODEX_HARNESS = "codex-app-server";
|
|
12
|
+
export const CODEX_DEFAULT_SHUTDOWN_GRACE_MS = 2_000;
|
|
13
|
+
const CODEX_0_150_1_CAPABILITIES = {
|
|
14
|
+
schemaVersion: "ebo.adapter-capability-profile/v1",
|
|
15
|
+
adapterId: "ebo-codex-app-server-v0.150.1",
|
|
16
|
+
harness: CODEX_HARNESS,
|
|
17
|
+
nativeTypes: [
|
|
18
|
+
"item/completed",
|
|
19
|
+
"turn/completed",
|
|
20
|
+
"turn/plan/updated",
|
|
21
|
+
"thread/compacted",
|
|
22
|
+
"thread/tokenUsage/updated",
|
|
23
|
+
"model/rerouted",
|
|
24
|
+
"hook/completed",
|
|
25
|
+
"server-request",
|
|
26
|
+
],
|
|
27
|
+
families: {
|
|
28
|
+
message: { status: "partial", detail: "Completed user and agent message items are projected." },
|
|
29
|
+
"model-request": { status: "unsupported", detail: "Inference requests are not inferred from turns or model changes." },
|
|
30
|
+
tool: { status: "partial", detail: "Only completed native tool items are projected; deltas stay native." },
|
|
31
|
+
context: { status: "partial", detail: "Plans and compaction are projected when emitted." },
|
|
32
|
+
permission: { status: "partial", detail: "Server requests and unattended decisions stay native; request events are projected." },
|
|
33
|
+
delegation: { status: "partial", detail: "Completed exposed collaboration items are projected when present." },
|
|
34
|
+
artifact: { status: "partial", detail: "Completed file-change items are projected." },
|
|
35
|
+
validation: { status: "unsupported", detail: "Verifier evidence is packaged outside the Codex native normalizer." },
|
|
36
|
+
runtime: { status: "partial", detail: "Usage, reroutes, and completed hooks are projected when emitted." },
|
|
37
|
+
outcome: { status: "available", detail: "Matching turn/completed is authoritative for turn outcome." },
|
|
38
|
+
},
|
|
39
|
+
evidence: {
|
|
40
|
+
nativeOrder: { status: "available", detail: "The retained receive/write sequence is one stdio ordering domain." },
|
|
41
|
+
nativeTime: { status: "partial", detail: "Item lifecycle timestamps are native; other records retain observation time separately." },
|
|
42
|
+
parentage: { status: "partial", detail: "Thread and turn scopes are retained; item relations remain source-specific." },
|
|
43
|
+
content: { status: "partial", detail: "Mapped content references retained native payloads without copying bodies." },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
export const CODEX_APP_SERVER_CAPABILITIES = {
|
|
47
|
+
...CODEX_0_150_1_CAPABILITIES,
|
|
48
|
+
adapterId: "ebo-codex-app-server-v0.153.4",
|
|
49
|
+
};
|
|
50
|
+
export function createCodexHarnessAdapter() {
|
|
51
|
+
return {
|
|
52
|
+
capture: {
|
|
53
|
+
id: CODEX_APP_SERVER_CAPABILITIES.adapterId,
|
|
54
|
+
harness: CODEX_HARNESS,
|
|
55
|
+
capture: captureCodexAppServer,
|
|
56
|
+
},
|
|
57
|
+
normalization: {
|
|
58
|
+
id: CODEX_APP_SERVER_CAPABILITIES.adapterId,
|
|
59
|
+
harness: CODEX_HARNESS,
|
|
60
|
+
capabilityProfile: CODEX_APP_SERVER_CAPABILITIES,
|
|
61
|
+
normalize: normalizeCodexCapture,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Drive one owned Codex app-server thread and finish only on its matching turn/completed notification. */
|
|
66
|
+
export async function captureCodexAppServer(request) {
|
|
67
|
+
requireText(request.runId, "Codex run ID");
|
|
68
|
+
requireText(request.attemptId, "Codex attempt ID");
|
|
69
|
+
requireText(request.workspacePath, "Codex workspace path");
|
|
70
|
+
requireText(request.prompt, "Codex prompt");
|
|
71
|
+
requireText(request.configuration.executable, "Codex executable");
|
|
72
|
+
if (request.configuration.version !== CODEX_APP_SERVER_VERSION)
|
|
73
|
+
throw new Error(`Codex capture requires pinned runtime ${CODEX_APP_SERVER_VERSION}.`);
|
|
74
|
+
const shutdownGraceMs = request.shutdownGraceMs ?? CODEX_DEFAULT_SHUTDOWN_GRACE_MS;
|
|
75
|
+
let abortRequested = request.signal?.aborted ?? false;
|
|
76
|
+
let abortDeadline = abortRequested ? performance.now() + shutdownGraceMs : undefined;
|
|
77
|
+
let abortAction;
|
|
78
|
+
const abortListener = () => {
|
|
79
|
+
abortRequested = true;
|
|
80
|
+
abortDeadline ??= performance.now() + shutdownGraceMs;
|
|
81
|
+
if (abortAction !== undefined)
|
|
82
|
+
void abortAction();
|
|
83
|
+
};
|
|
84
|
+
request.signal?.addEventListener("abort", abortListener, { once: true });
|
|
85
|
+
if (request.signal?.aborted)
|
|
86
|
+
abortListener();
|
|
87
|
+
const telemetry = await openOtlpReceiver(request.configuration.telemetry?.signals ?? [], request.now).catch((error) => {
|
|
88
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
let isolatedCodexHome;
|
|
92
|
+
let localLoginReference = "unavailable";
|
|
93
|
+
try {
|
|
94
|
+
isolatedCodexHome = await mkdtemp(join(tmpdir(), "ebo-codex-home-"));
|
|
95
|
+
await writeFile(join(isolatedCodexHome, "instructions.md"), "Work only in the supplied workspace. Do not request interactive input or broaden permissions.\n", { mode: 0o600 });
|
|
96
|
+
const authSource = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "auth.json");
|
|
97
|
+
try {
|
|
98
|
+
await lstat(authSource);
|
|
99
|
+
await symlink(authSource, join(isolatedCodexHome, "auth.json"));
|
|
100
|
+
localLoginReference = "available";
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code !== "ENOENT")
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
109
|
+
await telemetry.close();
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
const instructionsPath = join(isolatedCodexHome, "instructions.md");
|
|
113
|
+
const environment = isolatedEnvironment(isolatedCodexHome);
|
|
114
|
+
const gaps = [];
|
|
115
|
+
const addGap = (gap) => recordCaptureGap(gaps, gap);
|
|
116
|
+
let threadId;
|
|
117
|
+
let turnId;
|
|
118
|
+
let terminal;
|
|
119
|
+
let history;
|
|
120
|
+
const remainingAbortGrace = () => abortDeadline === undefined
|
|
121
|
+
? shutdownGraceMs
|
|
122
|
+
: Math.max(0, abortDeadline - performance.now());
|
|
123
|
+
const interruptWithinAbortGrace = async () => {
|
|
124
|
+
const terminationBudget = Math.floor(remainingAbortGrace() * 2 / 3);
|
|
125
|
+
const signalGrace = Math.floor(terminationBudget / 2);
|
|
126
|
+
await protocolProcess.interrupt(signalGrace, terminationBudget - signalGrace);
|
|
127
|
+
};
|
|
128
|
+
let nextRequestId = 1;
|
|
129
|
+
const pending = new Map();
|
|
130
|
+
let resolveTerminal;
|
|
131
|
+
let resolveOwnedTurn;
|
|
132
|
+
const terminalPromise = new Promise((resolvePromise) => {
|
|
133
|
+
resolveTerminal = resolvePromise;
|
|
134
|
+
});
|
|
135
|
+
const ownedTurnPromise = new Promise((resolvePromise) => {
|
|
136
|
+
resolveOwnedTurn = resolvePromise;
|
|
137
|
+
});
|
|
138
|
+
let protocolProcess;
|
|
139
|
+
const stderrDecoder = new TextDecoder("utf-8");
|
|
140
|
+
try {
|
|
141
|
+
protocolProcess = spawnProtocolProcess({
|
|
142
|
+
command: request.configuration.executable,
|
|
143
|
+
args: [
|
|
144
|
+
...(request.configuration.executableArgs ?? []),
|
|
145
|
+
"app-server",
|
|
146
|
+
"--listen",
|
|
147
|
+
"stdio://",
|
|
148
|
+
"--strict-config",
|
|
149
|
+
...telemetry.configArgs,
|
|
150
|
+
],
|
|
151
|
+
cwd: request.workspacePath,
|
|
152
|
+
env: environment,
|
|
153
|
+
source: CODEX_HARNESS,
|
|
154
|
+
evidencePath: request.evidencePath,
|
|
155
|
+
...(request.stderrPath === undefined ? {} : { stderrPath: request.stderrPath }),
|
|
156
|
+
...(request.maxLineBytes === undefined ? {} : { maxLineBytes: request.maxLineBytes }),
|
|
157
|
+
...(request.maxInMemoryObservations === undefined ? {} : { maxInMemoryObservations: request.maxInMemoryObservations }),
|
|
158
|
+
shutdownGraceMs,
|
|
159
|
+
...(request.now === undefined ? {} : { now: request.now }),
|
|
160
|
+
onStderr: async (chunk, final, recorder) => {
|
|
161
|
+
const diagnostic = stderrDecoder.decode(chunk, { stream: !final });
|
|
162
|
+
if (diagnostic !== "") {
|
|
163
|
+
await recorder.recordNotification({
|
|
164
|
+
source: CODEX_HARNESS,
|
|
165
|
+
method: "diagnostic/stderr",
|
|
166
|
+
payload: { text: diagnostic },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
onFrame: async (payload, recorder) => {
|
|
171
|
+
if (!isRecord(payload))
|
|
172
|
+
return;
|
|
173
|
+
const id = protocolId(payload.id);
|
|
174
|
+
const method = typeof payload.method === "string" && payload.method !== "" ? payload.method : undefined;
|
|
175
|
+
if (method !== undefined && id !== undefined) {
|
|
176
|
+
const sourceIdentity = scopedThreadId(payload.params);
|
|
177
|
+
await recorder.recordRequest({
|
|
178
|
+
source: CODEX_HARNESS,
|
|
179
|
+
method,
|
|
180
|
+
id,
|
|
181
|
+
...(sourceIdentity === undefined ? {} : { sourceIdentity }),
|
|
182
|
+
payload: payload.params ?? null,
|
|
183
|
+
});
|
|
184
|
+
const answer = unattendedServerResponse(method);
|
|
185
|
+
await recorder.recordResponse({
|
|
186
|
+
source: "ebo-codex-client",
|
|
187
|
+
method,
|
|
188
|
+
id,
|
|
189
|
+
...(sourceIdentity === undefined ? {} : { sourceIdentity }),
|
|
190
|
+
payload: answer,
|
|
191
|
+
});
|
|
192
|
+
await writeProtocolLine(protocolProcess.stdin, answer.error === undefined
|
|
193
|
+
? { id, result: answer.result }
|
|
194
|
+
: { id, error: answer.error });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (id !== undefined && method === undefined) {
|
|
198
|
+
const waiter = pending.get(id);
|
|
199
|
+
const responseMethod = waiter?.method ?? "unknown-response";
|
|
200
|
+
const responsePayload = payload.error === undefined ? payload.result : { error: payload.error };
|
|
201
|
+
const sourceIdentity = responseSourceIdentity(responseMethod, responsePayload);
|
|
202
|
+
await recorder.recordResponse({
|
|
203
|
+
source: CODEX_HARNESS,
|
|
204
|
+
method: responseMethod,
|
|
205
|
+
id,
|
|
206
|
+
...(sourceIdentity === undefined ? {} : { sourceIdentity }),
|
|
207
|
+
payload: responsePayload ?? null,
|
|
208
|
+
});
|
|
209
|
+
if (waiter !== undefined) {
|
|
210
|
+
pending.delete(id);
|
|
211
|
+
if (payload.error !== undefined)
|
|
212
|
+
waiter.reject(new Error(jsonRpcError(responseMethod, payload.error)));
|
|
213
|
+
else if (isRecord(payload.result))
|
|
214
|
+
waiter.resolve(payload.result);
|
|
215
|
+
else
|
|
216
|
+
waiter.reject(new Error(`Codex ${responseMethod} returned a non-object result.`));
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (method === undefined || id !== undefined)
|
|
221
|
+
return;
|
|
222
|
+
const params = isRecord(payload.params) ? payload.params : {};
|
|
223
|
+
const sourceIdentity = scopedThreadId(params) ?? threadId;
|
|
224
|
+
await recorder.recordNotification({
|
|
225
|
+
source: CODEX_HARNESS,
|
|
226
|
+
method,
|
|
227
|
+
...(sourceIdentity === undefined ? {} : { sourceIdentity }),
|
|
228
|
+
payload: params,
|
|
229
|
+
});
|
|
230
|
+
if (method === "turn/completed") {
|
|
231
|
+
const turn = isRecord(params.turn) ? params.turn : undefined;
|
|
232
|
+
const completedThreadId = text(params.threadId);
|
|
233
|
+
const completedTurnId = text(turn?.id);
|
|
234
|
+
if (turn !== undefined && threadId !== undefined && turnId !== undefined
|
|
235
|
+
&& completedThreadId === threadId && completedTurnId === turnId && terminal === undefined) {
|
|
236
|
+
terminal = structuredClone(turn);
|
|
237
|
+
await recorder.recordCompletion({
|
|
238
|
+
source: CODEX_HARNESS,
|
|
239
|
+
method,
|
|
240
|
+
sourceIdentity: threadId,
|
|
241
|
+
status: text(turn.status) ?? "unknown",
|
|
242
|
+
evidence: turn,
|
|
243
|
+
});
|
|
244
|
+
resolveTerminal?.(turn);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
addGap({ kind: "foreign-turn-completion", detail: "Ignored turn/completed for a non-owned thread or turn." });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
request.registerShutdown?.(async () => {
|
|
253
|
+
abortRequested = true;
|
|
254
|
+
abortDeadline ??= performance.now() + shutdownGraceMs;
|
|
255
|
+
await interruptWithinAbortGrace();
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
260
|
+
await telemetry.close();
|
|
261
|
+
await rm(isolatedCodexHome, { recursive: true, force: true });
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
const sendRequest = async (method, params) => {
|
|
265
|
+
const id = nextRequestId++;
|
|
266
|
+
await protocolProcess.evidence.recordRequest({
|
|
267
|
+
source: "ebo-codex-client",
|
|
268
|
+
method,
|
|
269
|
+
id,
|
|
270
|
+
...(threadId === undefined ? {} : { sourceIdentity: threadId }),
|
|
271
|
+
payload: params,
|
|
272
|
+
});
|
|
273
|
+
const response = new Promise((resolvePromise, reject) => {
|
|
274
|
+
pending.set(id, { method, resolve: resolvePromise, reject });
|
|
275
|
+
});
|
|
276
|
+
await writeProtocolLine(protocolProcess.stdin, { method, id, params });
|
|
277
|
+
return Promise.race([
|
|
278
|
+
response,
|
|
279
|
+
protocolProcess.wait().then((result) => {
|
|
280
|
+
throw new Error(`Codex app-server exited before ${method} completed (${result.status}).`);
|
|
281
|
+
}),
|
|
282
|
+
]);
|
|
283
|
+
};
|
|
284
|
+
const sendNotification = async (method, params = {}) => {
|
|
285
|
+
await protocolProcess.evidence.recordNotification({ source: "ebo-codex-client", method, payload: params });
|
|
286
|
+
await writeProtocolLine(protocolProcess.stdin, { method, params });
|
|
287
|
+
};
|
|
288
|
+
const abort = async () => {
|
|
289
|
+
abortRequested = true;
|
|
290
|
+
abortDeadline ??= performance.now() + shutdownGraceMs;
|
|
291
|
+
if ((threadId === undefined || turnId === undefined) && terminal === undefined) {
|
|
292
|
+
await settleOrDelay(ownedTurnPromise, Math.min(250, remainingAbortGrace()));
|
|
293
|
+
}
|
|
294
|
+
if (threadId !== undefined && turnId !== undefined && terminal === undefined) {
|
|
295
|
+
try {
|
|
296
|
+
await withTimeout(sendRequest("turn/interrupt", { threadId, turnId }), Math.floor(remainingAbortGrace() / 4), "Codex turn/interrupt acknowledgement timed out.");
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
addGap({ kind: "interrupt-acknowledgement", detail: errorMessage(error) });
|
|
300
|
+
}
|
|
301
|
+
if (terminal === undefined) {
|
|
302
|
+
await settleOrDelay(terminalPromise, Math.min(250, Math.floor(remainingAbortGrace() / 4)));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (terminal === undefined)
|
|
306
|
+
await interruptWithinAbortGrace();
|
|
307
|
+
};
|
|
308
|
+
abortAction = abort;
|
|
309
|
+
if (abortRequested)
|
|
310
|
+
void abort();
|
|
311
|
+
let processResult;
|
|
312
|
+
let captureError;
|
|
313
|
+
let threadStart;
|
|
314
|
+
try {
|
|
315
|
+
await sendRequest("initialize", {
|
|
316
|
+
clientInfo: { name: "ebo", title: "Engineering Behavior Observatory", version: CODEX_ADAPTER_VERSION },
|
|
317
|
+
capabilities: { experimentalApi: true },
|
|
318
|
+
});
|
|
319
|
+
await sendNotification("initialized");
|
|
320
|
+
threadStart = await sendRequest("thread/start", {
|
|
321
|
+
model: request.configuration.model,
|
|
322
|
+
cwd: request.workspacePath,
|
|
323
|
+
approvalPolicy: request.configuration.approvalPolicy,
|
|
324
|
+
sandbox: request.configuration.sandbox,
|
|
325
|
+
serviceName: "ebo",
|
|
326
|
+
ephemeral: false,
|
|
327
|
+
historyMode: "legacy",
|
|
328
|
+
config: { model_instructions_file: instructionsPath, model_reasoning_effort: request.configuration.effort,
|
|
329
|
+
mcp_servers: {}, hooks: {}, plugins: {},
|
|
330
|
+
...(request.configuration.sandbox === "workspace-write" ? { sandbox_workspace_write: {
|
|
331
|
+
writable_roots: [request.workspacePath], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true,
|
|
332
|
+
} } : {}),
|
|
333
|
+
},
|
|
334
|
+
developerInstructions: "Work only in the supplied workspace. Do not request interactive input or broaden permissions.",
|
|
335
|
+
});
|
|
336
|
+
threadId = text(isRecord(threadStart.thread) ? threadStart.thread.id : undefined);
|
|
337
|
+
if (threadId === undefined)
|
|
338
|
+
throw new Error("Codex thread/start did not return a thread identity.");
|
|
339
|
+
if (!isRecord(threadStart.thread) || threadStart.thread.historyMode !== "legacy") {
|
|
340
|
+
addGap({ kind: "history-mode-mismatch", detail: "Codex did not apply the requested legacy history mode." });
|
|
341
|
+
}
|
|
342
|
+
if (text(threadStart.model) !== request.configuration.model) {
|
|
343
|
+
addGap({ kind: "model-mismatch", detail: `Requested ${request.configuration.model}; launched ${String(threadStart.model)}.` });
|
|
344
|
+
}
|
|
345
|
+
if (text(threadStart.reasoningEffort) !== request.configuration.effort) {
|
|
346
|
+
addGap({ kind: "effort-mismatch", detail: `Requested ${request.configuration.effort}; launched ${String(threadStart.reasoningEffort)}.` });
|
|
347
|
+
}
|
|
348
|
+
if (text(threadStart.modelProvider) !== request.configuration.provider) {
|
|
349
|
+
addGap({ kind: "provider-mismatch", detail: `Requested ${request.configuration.provider}; launched ${String(threadStart.modelProvider)}.` });
|
|
350
|
+
}
|
|
351
|
+
if (threadStart.approvalPolicy !== request.configuration.approvalPolicy) {
|
|
352
|
+
addGap({ kind: "approval-policy-mismatch", detail: `Requested ${request.configuration.approvalPolicy}; applied ${JSON.stringify(threadStart.approvalPolicy)}.` });
|
|
353
|
+
}
|
|
354
|
+
if (!sandboxMatches(request.configuration.sandbox, threadStart.sandbox, request.workspacePath, threadStart.cwd)) {
|
|
355
|
+
addGap({ kind: "sandbox-mismatch", detail: `Requested ${request.configuration.sandbox}; applied ${JSON.stringify(threadStart.sandbox)}.` });
|
|
356
|
+
}
|
|
357
|
+
const started = await sendRequest("turn/start", {
|
|
358
|
+
threadId,
|
|
359
|
+
input: [{ type: "text", text: request.prompt }],
|
|
360
|
+
cwd: request.workspacePath,
|
|
361
|
+
approvalPolicy: request.configuration.approvalPolicy,
|
|
362
|
+
sandboxPolicy: turnSandboxPolicy(request.configuration.sandbox, request.workspacePath),
|
|
363
|
+
model: request.configuration.model,
|
|
364
|
+
effort: request.configuration.effort,
|
|
365
|
+
});
|
|
366
|
+
turnId = text(isRecord(started.turn) ? started.turn.id : undefined);
|
|
367
|
+
if (turnId === undefined)
|
|
368
|
+
throw new Error("Codex turn/start did not return a turn identity.");
|
|
369
|
+
resolveOwnedTurn?.();
|
|
370
|
+
terminal = await Promise.race([
|
|
371
|
+
terminalPromise,
|
|
372
|
+
protocolProcess.wait().then((result) => {
|
|
373
|
+
throw new Error(`Codex app-server exited before matching turn/completed (${result.status}).`);
|
|
374
|
+
}),
|
|
375
|
+
]);
|
|
376
|
+
try {
|
|
377
|
+
const historyRequest = sendRequest("thread/read", { threadId, includeTurns: true });
|
|
378
|
+
// The lifecycle owner starts the same grace window when it aborts us.
|
|
379
|
+
// Spend at most one quarter of what remains on optional history so
|
|
380
|
+
// process teardown and evidence finalization can finish inside it.
|
|
381
|
+
const historyGraceMs = abortDeadline === undefined
|
|
382
|
+
? shutdownGraceMs
|
|
383
|
+
: Math.max(0, Math.floor((abortDeadline - performance.now()) / 4));
|
|
384
|
+
history = await withAbortAwareTimeout(historyRequest, historyGraceMs, "Persisted history readback timed out.", request.signal, () => {
|
|
385
|
+
abortDeadline ??= performance.now() + shutdownGraceMs;
|
|
386
|
+
return Math.max(0, Math.floor((abortDeadline - performance.now()) / 4));
|
|
387
|
+
});
|
|
388
|
+
if (!historyMatches(history, threadId, turnId)) {
|
|
389
|
+
addGap({ kind: "history-mismatch", detail: "thread/read did not return full legacy history for the owned terminal turn." });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
addGap({ kind: "history-readback", detail: errorMessage(error) });
|
|
394
|
+
if (abortRequested)
|
|
395
|
+
await interruptWithinAbortGrace();
|
|
396
|
+
else if (errorMessage(error).includes("timed out"))
|
|
397
|
+
await protocolProcess.interrupt();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
captureError = errorMessage(error);
|
|
402
|
+
addGap({ kind: "capture-error", detail: captureError });
|
|
403
|
+
}
|
|
404
|
+
finally {
|
|
405
|
+
request.signal?.removeEventListener("abort", abortListener);
|
|
406
|
+
if (abortRequested)
|
|
407
|
+
await interruptWithinAbortGrace();
|
|
408
|
+
else
|
|
409
|
+
await protocolProcess.shutdown();
|
|
410
|
+
processResult = await protocolProcess.wait();
|
|
411
|
+
pending.clear();
|
|
412
|
+
await telemetry.close();
|
|
413
|
+
await rm(isolatedCodexHome, { recursive: true, force: true });
|
|
414
|
+
}
|
|
415
|
+
const records = processResult.observations;
|
|
416
|
+
if (processResult.droppedObservations > 0)
|
|
417
|
+
addGap({
|
|
418
|
+
kind: "normalization-projection-truncated",
|
|
419
|
+
detail: `${processResult.droppedObservations} earlier observations remain authoritative in session.jsonl but are outside the bounded in-memory projection.`,
|
|
420
|
+
});
|
|
421
|
+
const terminalStatus = text(terminal?.status);
|
|
422
|
+
const telemetryEvidence = telemetry.evidence({
|
|
423
|
+
attemptId: request.attemptId,
|
|
424
|
+
executable: request.configuration.executable,
|
|
425
|
+
version: request.configuration.version,
|
|
426
|
+
model: request.configuration.model,
|
|
427
|
+
provider: request.configuration.provider,
|
|
428
|
+
effort: request.configuration.effort,
|
|
429
|
+
approvalPolicy: request.configuration.approvalPolicy,
|
|
430
|
+
sandbox: request.configuration.sandbox,
|
|
431
|
+
workspace: request.workspacePath,
|
|
432
|
+
threadStart,
|
|
433
|
+
records,
|
|
434
|
+
localLoginReference,
|
|
435
|
+
environmentKeys: Object.keys(environment).sort(),
|
|
436
|
+
threadId,
|
|
437
|
+
turnId,
|
|
438
|
+
});
|
|
439
|
+
if (processResult.error !== undefined) {
|
|
440
|
+
addGap({ kind: "process-error", detail: processResult.error });
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
runId: request.runId,
|
|
444
|
+
attemptId: request.attemptId,
|
|
445
|
+
qualification: captureError === undefined && terminalStatus === "completed" && gaps.length === 0
|
|
446
|
+
? "qualified" : "qualified-with-gaps",
|
|
447
|
+
records: records.map((record) => ({
|
|
448
|
+
reference: { artifactId: "session", recordLocator: `line:${record.sequence}` },
|
|
449
|
+
record,
|
|
450
|
+
})),
|
|
451
|
+
...(threadId === undefined ? {} : { threadId }),
|
|
452
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
453
|
+
...(terminalStatus === "completed" || terminalStatus === "interrupted" || terminalStatus === "failed"
|
|
454
|
+
? { terminalStatus } : {}),
|
|
455
|
+
...(terminal === undefined ? {} : { terminal }),
|
|
456
|
+
...(history === undefined ? {} : { history }),
|
|
457
|
+
process: processResult,
|
|
458
|
+
gaps,
|
|
459
|
+
telemetry: telemetryEvidence,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
export async function normalizeCodexCapture(capture) {
|
|
463
|
+
const events = [];
|
|
464
|
+
const mapped = new Set();
|
|
465
|
+
for (const captured of capture.records) {
|
|
466
|
+
const event = mapCodexRecord(capture, captured);
|
|
467
|
+
if (event === undefined)
|
|
468
|
+
continue;
|
|
469
|
+
events.push(event);
|
|
470
|
+
mapped.add(captured.record.sequence);
|
|
471
|
+
}
|
|
472
|
+
await validateUniformEvents(events, {
|
|
473
|
+
resolve: (reference) => resolvesCaptureReference(capture, reference),
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
events,
|
|
477
|
+
unmapped: capture.records
|
|
478
|
+
.filter(({ record }) => !mapped.has(record.sequence))
|
|
479
|
+
.map(({ reference }) => ({ reference, reason: "native record has no supported uniform mapping" })),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
export async function describeAndValidateCodexDataset(capture, runtimeVersion = capture.telemetry?.runtime?.version ?? CODEX_APP_SERVER_VERSION) {
|
|
483
|
+
if (runtimeVersion !== CODEX_APP_SERVER_VERSION && runtimeVersion !== "0.150.1")
|
|
484
|
+
throw new Error(`Unsupported retained Codex runtime ${runtimeVersion}.`);
|
|
485
|
+
const normalization = await normalizeCodexCapture(capture);
|
|
486
|
+
const dataset = describeNormalizedDataset({
|
|
487
|
+
capture,
|
|
488
|
+
normalization,
|
|
489
|
+
capabilityProfile: runtimeVersion === "0.150.1" ? CODEX_0_150_1_CAPABILITIES : CODEX_APP_SERVER_CAPABILITIES,
|
|
490
|
+
adapterVersion: CODEX_ADAPTER_VERSION,
|
|
491
|
+
nativeType: codexNativeType,
|
|
492
|
+
});
|
|
493
|
+
const coverage = await validateNormalizedDataset(dataset, createCapturedNativeEvidenceResolver(capture));
|
|
494
|
+
return { dataset, coverage };
|
|
495
|
+
}
|
|
496
|
+
function mapCodexRecord(capture, captured) {
|
|
497
|
+
const record = captured.record;
|
|
498
|
+
const method = record.method;
|
|
499
|
+
if (method === undefined)
|
|
500
|
+
return undefined;
|
|
501
|
+
const payload = isRecord(record.payload) ? record.payload : {};
|
|
502
|
+
let family;
|
|
503
|
+
let actor = "harness";
|
|
504
|
+
let phase = "instant";
|
|
505
|
+
let scope = { kind: "attempt", id: capture.attemptId };
|
|
506
|
+
let contentPath;
|
|
507
|
+
if (record.kind === "request" && record.source === CODEX_HARNESS && isPermissionRequest(method)
|
|
508
|
+
&& matchesOwnedScope(capture, payload)) {
|
|
509
|
+
family = "permission";
|
|
510
|
+
actor = "harness";
|
|
511
|
+
phase = "before";
|
|
512
|
+
scope = scopedTurn(payload);
|
|
513
|
+
}
|
|
514
|
+
else if (record.kind !== "notification" || record.source !== CODEX_HARNESS)
|
|
515
|
+
return undefined;
|
|
516
|
+
else if (method === "item/completed") {
|
|
517
|
+
if (!matchesOwnedScope(capture, payload))
|
|
518
|
+
return undefined;
|
|
519
|
+
const item = isRecord(payload.item) ? payload.item : {};
|
|
520
|
+
const type = text(item.type);
|
|
521
|
+
family = itemFamily(type);
|
|
522
|
+
if (family === undefined)
|
|
523
|
+
return undefined;
|
|
524
|
+
actor = family === "message" && type === "userMessage" ? "user"
|
|
525
|
+
: family === "message" ? "agent" : family === "tool" ? "tool" : "agent";
|
|
526
|
+
phase = "after";
|
|
527
|
+
scope = { kind: "turn", id: text(payload.turnId) };
|
|
528
|
+
contentPath = "#/payload/item";
|
|
529
|
+
}
|
|
530
|
+
else if (method === "turn/completed") {
|
|
531
|
+
const turn = isRecord(payload.turn) ? payload.turn : {};
|
|
532
|
+
if (!matchesOwnedScope(capture, { threadId: payload.threadId, turnId: turn.id }))
|
|
533
|
+
return undefined;
|
|
534
|
+
family = "outcome";
|
|
535
|
+
actor = "harness";
|
|
536
|
+
phase = "after";
|
|
537
|
+
scope = { kind: "turn", id: text(turn.id) };
|
|
538
|
+
contentPath = "#/payload/turn";
|
|
539
|
+
}
|
|
540
|
+
else if (method === "turn/plan/updated" || method === "thread/compacted") {
|
|
541
|
+
if (!matchesOwnedScope(capture, payload))
|
|
542
|
+
return undefined;
|
|
543
|
+
family = "context";
|
|
544
|
+
actor = "agent";
|
|
545
|
+
scope = scopedTurn(payload);
|
|
546
|
+
contentPath = "#/payload";
|
|
547
|
+
}
|
|
548
|
+
else if (method === "thread/tokenUsage/updated" || method === "model/rerouted" || method === "hook/completed") {
|
|
549
|
+
if (!matchesOwnedScope(capture, payload, method === "hook/completed"))
|
|
550
|
+
return undefined;
|
|
551
|
+
family = "runtime";
|
|
552
|
+
scope = scopedTurn(payload);
|
|
553
|
+
contentPath = "#/payload";
|
|
554
|
+
}
|
|
555
|
+
else
|
|
556
|
+
return undefined;
|
|
557
|
+
const attributes = { method };
|
|
558
|
+
if (method === "turn/completed" && isRecord(payload.turn))
|
|
559
|
+
copyScalar(attributes, "status", payload.turn.status);
|
|
560
|
+
if (method === "item/completed" && isRecord(payload.item)) {
|
|
561
|
+
copyScalar(attributes, "itemType", payload.item.type);
|
|
562
|
+
copyScalar(attributes, "itemId", payload.item.id);
|
|
563
|
+
copyScalar(attributes, "status", payload.item.status);
|
|
564
|
+
if (payload.item.type === "fileChange" && payload.item.status === "completed")
|
|
565
|
+
attributes.mutation = true;
|
|
566
|
+
}
|
|
567
|
+
if (method === "thread/tokenUsage/updated" && isRecord(payload.tokenUsage)) {
|
|
568
|
+
const total = numberRecord(payload.tokenUsage.total);
|
|
569
|
+
if (total !== undefined) {
|
|
570
|
+
for (const key of ["totalTokens", "inputTokens", "cachedInputTokens", "cacheWriteInputTokens", "outputTokens", "reasoningOutputTokens"]) {
|
|
571
|
+
copyScalar(attributes, key, total[key]);
|
|
572
|
+
}
|
|
573
|
+
attributes.resourceSemantics = "cumulative-snapshot";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
schemaVersion: "ebo.uniform-event/v1",
|
|
578
|
+
id: `codex:${record.sequence}:${method.replaceAll("/", "-")}`,
|
|
579
|
+
runId: capture.runId,
|
|
580
|
+
attemptId: capture.attemptId,
|
|
581
|
+
source: {
|
|
582
|
+
harness: CODEX_HARNESS,
|
|
583
|
+
nativeType: codexNativeType(record),
|
|
584
|
+
nativeReference: captured.reference,
|
|
585
|
+
},
|
|
586
|
+
nativeOrder: { status: "known", value: record.sequence, domain: "codex-app-server-stdio" },
|
|
587
|
+
nativeTime: nativeTimestamp(method, payload),
|
|
588
|
+
actor: { kind: actor },
|
|
589
|
+
family,
|
|
590
|
+
phase,
|
|
591
|
+
scope,
|
|
592
|
+
relations: {
|
|
593
|
+
parent: { status: "unknown", reason: "Codex native records retain scope IDs but no uniform parent event identity." },
|
|
594
|
+
known: [],
|
|
595
|
+
},
|
|
596
|
+
attributes,
|
|
597
|
+
content: contentPath === undefined
|
|
598
|
+
? { status: "unknown", reason: "This mapped record has no projected content body." }
|
|
599
|
+
: {
|
|
600
|
+
status: "known",
|
|
601
|
+
value: [{
|
|
602
|
+
nativeReference: {
|
|
603
|
+
artifactId: captured.reference.artifactId,
|
|
604
|
+
recordLocator: `${captured.reference.recordLocator}${contentPath}`,
|
|
605
|
+
},
|
|
606
|
+
mediaType: "application/json",
|
|
607
|
+
}],
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function codexNativeType(record) {
|
|
612
|
+
if (record.kind === "request" && record.source === CODEX_HARNESS)
|
|
613
|
+
return "server-request";
|
|
614
|
+
return record.method ?? record.kind;
|
|
615
|
+
}
|
|
616
|
+
function captureIdentity(capture) {
|
|
617
|
+
const value = capture;
|
|
618
|
+
const threadId = text(value.threadId);
|
|
619
|
+
const turnId = text(value.turnId);
|
|
620
|
+
return {
|
|
621
|
+
...(threadId === undefined ? {} : { threadId }),
|
|
622
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function matchesOwnedScope(capture, payload, allowSessionScope = false) {
|
|
626
|
+
const owned = captureIdentity(capture);
|
|
627
|
+
if (owned.threadId === undefined || owned.turnId === undefined || text(payload.threadId) !== owned.threadId)
|
|
628
|
+
return false;
|
|
629
|
+
const recordTurnId = text(payload.turnId);
|
|
630
|
+
return recordTurnId === owned.turnId || allowSessionScope && (payload.turnId === null || payload.turnId === undefined);
|
|
631
|
+
}
|
|
632
|
+
function itemFamily(type) {
|
|
633
|
+
if (type === "userMessage" || type === "agentMessage")
|
|
634
|
+
return "message";
|
|
635
|
+
if (type === "commandExecution" || type === "mcpToolCall" || type === "dynamicToolCall" || type === "webSearch")
|
|
636
|
+
return "tool";
|
|
637
|
+
if (type === "fileChange")
|
|
638
|
+
return "artifact";
|
|
639
|
+
if (type === "collabAgentToolCall")
|
|
640
|
+
return "delegation";
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
function nativeTimestamp(method, payload) {
|
|
644
|
+
const milliseconds = method === "item/completed" ? payload.completedAtMs : undefined;
|
|
645
|
+
if (typeof milliseconds === "number" && Number.isFinite(milliseconds)) {
|
|
646
|
+
const date = new Date(milliseconds);
|
|
647
|
+
if (!Number.isNaN(date.getTime()))
|
|
648
|
+
return { status: "known", value: date.toISOString() };
|
|
649
|
+
}
|
|
650
|
+
return milliseconds === undefined
|
|
651
|
+
? { status: "unknown", reason: "Native record omitted a lifecycle timestamp." }
|
|
652
|
+
: { status: "unknown", reason: "Native lifecycle timestamp is invalid or outside the supported range." };
|
|
653
|
+
}
|
|
654
|
+
function scopedTurn(payload) {
|
|
655
|
+
return text(payload.turnId) === undefined
|
|
656
|
+
? { kind: "session", id: text(payload.threadId) }
|
|
657
|
+
: { kind: "turn", id: text(payload.turnId) };
|
|
658
|
+
}
|
|
659
|
+
function copyScalar(target, key, value) {
|
|
660
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
661
|
+
target[key] = value;
|
|
662
|
+
}
|
|
663
|
+
function resolvesCaptureReference(capture, reference) {
|
|
664
|
+
return capture.records.some(({ reference: base }) => base.artifactId === reference.artifactId
|
|
665
|
+
&& (reference.recordLocator === base.recordLocator || reference.recordLocator.startsWith(`${base.recordLocator}#`)));
|
|
666
|
+
}
|
|
667
|
+
function responseSourceIdentity(method, payload) {
|
|
668
|
+
if (!isRecord(payload))
|
|
669
|
+
return undefined;
|
|
670
|
+
if (method === "thread/start")
|
|
671
|
+
return text(isRecord(payload.thread) ? payload.thread.id : undefined);
|
|
672
|
+
if (method === "turn/start")
|
|
673
|
+
return text(isRecord(payload.turn) ? payload.turn.threadId : undefined);
|
|
674
|
+
if (method === "thread/read")
|
|
675
|
+
return text(isRecord(payload.thread) ? payload.thread.id : undefined);
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
function turnSandboxPolicy(sandbox, workspace) {
|
|
679
|
+
if (sandbox === "danger-full-access")
|
|
680
|
+
return { type: "dangerFullAccess" };
|
|
681
|
+
if (sandbox === "read-only")
|
|
682
|
+
return { type: "readOnly", networkAccess: false };
|
|
683
|
+
return {
|
|
684
|
+
type: "workspaceWrite",
|
|
685
|
+
writableRoots: [workspace],
|
|
686
|
+
networkAccess: false,
|
|
687
|
+
excludeTmpdirEnvVar: true,
|
|
688
|
+
excludeSlashTmp: true,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
function sandboxMatches(requested, applied, workspace, appliedCwd) {
|
|
692
|
+
if (!isRecord(applied) || appliedCwd !== workspace)
|
|
693
|
+
return false;
|
|
694
|
+
if (requested === "danger-full-access")
|
|
695
|
+
return applied.type === "dangerFullAccess";
|
|
696
|
+
if (requested === "read-only")
|
|
697
|
+
return applied.type === "readOnly" && applied.networkAccess === false;
|
|
698
|
+
return applied.type === "workspaceWrite"
|
|
699
|
+
&& applied.networkAccess === false
|
|
700
|
+
&& Array.isArray(applied.writableRoots)
|
|
701
|
+
// Codex 0.153.4 roots are additional to cwd; it removes redundant cwd entries.
|
|
702
|
+
&& applied.writableRoots.length <= 1
|
|
703
|
+
&& applied.writableRoots.every((root) => root === workspace)
|
|
704
|
+
&& applied.excludeTmpdirEnvVar === true
|
|
705
|
+
&& applied.excludeSlashTmp === true;
|
|
706
|
+
}
|
|
707
|
+
function scopedThreadId(payload) {
|
|
708
|
+
if (!isRecord(payload))
|
|
709
|
+
return undefined;
|
|
710
|
+
return text(payload.threadId)
|
|
711
|
+
?? text(isRecord(payload.turn) ? payload.turn.threadId : undefined)
|
|
712
|
+
?? text(isRecord(payload.thread) ? payload.thread.id : undefined);
|
|
713
|
+
}
|
|
714
|
+
function isPermissionRequest(method) {
|
|
715
|
+
return [
|
|
716
|
+
"item/commandExecution/requestApproval",
|
|
717
|
+
"item/fileChange/requestApproval",
|
|
718
|
+
"item/permissions/requestApproval",
|
|
719
|
+
"item/tool/requestUserInput",
|
|
720
|
+
"mcpServer/elicitation/request",
|
|
721
|
+
"applyPatchApproval",
|
|
722
|
+
"execCommandApproval",
|
|
723
|
+
].includes(method);
|
|
724
|
+
}
|
|
725
|
+
function unattendedServerResponse(method) {
|
|
726
|
+
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval"
|
|
727
|
+
|| method === "execCommandApproval" || method === "applyPatchApproval") {
|
|
728
|
+
return { result: { decision: "decline" } };
|
|
729
|
+
}
|
|
730
|
+
if (method === "mcpServer/elicitation/request")
|
|
731
|
+
return { result: { action: "decline", content: null, _meta: null } };
|
|
732
|
+
if (method === "item/tool/call")
|
|
733
|
+
return { result: { contentItems: [], success: false } };
|
|
734
|
+
if (method === "currentTime/read")
|
|
735
|
+
return { result: { currentTimeAt: Math.floor(Date.now() / 1_000) } };
|
|
736
|
+
return { error: { code: -32000, message: `EBO unattended policy does not provide ${method}.` } };
|
|
737
|
+
}
|
|
738
|
+
function historyMatches(history, threadId, turnId) {
|
|
739
|
+
const thread = isRecord(history.thread) ? history.thread : undefined;
|
|
740
|
+
if (text(thread?.id) !== threadId || thread?.historyMode !== "legacy" || !Array.isArray(thread.turns))
|
|
741
|
+
return false;
|
|
742
|
+
return thread.turns.some((candidate) => isRecord(candidate) && text(candidate.id) === turnId
|
|
743
|
+
&& candidate.itemsView === "full" && Array.isArray(candidate.items));
|
|
744
|
+
}
|
|
745
|
+
export async function writeProtocolLine(stream, message) {
|
|
746
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
747
|
+
await new Promise((resolvePromise, reject) => {
|
|
748
|
+
const writable = stream;
|
|
749
|
+
writable.write(line, (error) => error === undefined || error === null ? resolvePromise() : reject(error));
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function protocolId(value) {
|
|
753
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value) || value === null
|
|
754
|
+
? value : undefined;
|
|
755
|
+
}
|
|
756
|
+
function jsonRpcError(method, value) {
|
|
757
|
+
const message = isRecord(value) && typeof value.message === "string" ? value.message : JSON.stringify(value);
|
|
758
|
+
return `Codex ${method} failed: ${message}`;
|
|
759
|
+
}
|
|
760
|
+
async function openOtlpReceiver(signals, now = () => new Date().toISOString()) {
|
|
761
|
+
const enabled = [...new Set(signals)];
|
|
762
|
+
if (enabled.some((signal) => !["logs", "traces", "metrics"].includes(signal))) {
|
|
763
|
+
throw new Error("Codex telemetry contains an unsupported signal.");
|
|
764
|
+
}
|
|
765
|
+
const records = [];
|
|
766
|
+
const receiverErrors = [];
|
|
767
|
+
const receiverState = { records, receiverErrors, bytes: 0, inFlightBytes: 0, inFlightRecords: 0 };
|
|
768
|
+
let server;
|
|
769
|
+
const configArgs = ["-c", "otel.log_user_prompt=false", "-c", 'otel.environment="ebo"'];
|
|
770
|
+
if (enabled.length === 0) {
|
|
771
|
+
configArgs.push("-c", 'otel.exporter="none"', "-c", 'otel.trace_exporter="none"', "-c", 'otel.metrics_exporter="none"');
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
server = createServer((incoming, response) => {
|
|
775
|
+
void receiveOtlp(incoming, enabled, receiverState, now).then((status) => {
|
|
776
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
777
|
+
response.end("{}");
|
|
778
|
+
}, () => {
|
|
779
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
780
|
+
response.end("{}");
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
await new Promise((resolvePromise, reject) => {
|
|
784
|
+
server.once("error", reject);
|
|
785
|
+
server.listen(0, "127.0.0.1", () => {
|
|
786
|
+
server.off("error", reject);
|
|
787
|
+
resolvePromise();
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
const address = server.address();
|
|
791
|
+
if (address === null || typeof address === "string")
|
|
792
|
+
throw new Error("Codex OTLP receiver did not bind a TCP port.");
|
|
793
|
+
for (const signal of ["logs", "traces", "metrics"]) {
|
|
794
|
+
const field = signal === "logs" ? "exporter" : signal === "traces" ? "trace_exporter" : "metrics_exporter";
|
|
795
|
+
if (!enabled.includes(signal))
|
|
796
|
+
configArgs.push("-c", `otel.${field}=\"none\"`);
|
|
797
|
+
else
|
|
798
|
+
configArgs.push("-c", `otel.${field}={ otlp-http = { endpoint = \"http://127.0.0.1:${address.port}/v1/${signal}\", protocol = \"json\" } }`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
configArgs,
|
|
803
|
+
close: async () => {
|
|
804
|
+
if (server === undefined)
|
|
805
|
+
return;
|
|
806
|
+
await new Promise((resolvePromise, reject) => server.close((error) => error === undefined ? resolvePromise() : reject(error)));
|
|
807
|
+
},
|
|
808
|
+
evidence: (input) => {
|
|
809
|
+
const statuses = Object.fromEntries(["logs", "traces", "metrics"].map((signal) => {
|
|
810
|
+
const count = records.filter((record) => record.signal === signal && record.parseError === undefined && record.payload !== undefined).length;
|
|
811
|
+
return [signal, { status: enabled.includes(signal) ? count > 0 ? "received" : "missing" : "disabled", count }];
|
|
812
|
+
}));
|
|
813
|
+
const usageUpdates = input.records.flatMap((record) => {
|
|
814
|
+
if (record.kind !== "notification" || record.method !== "thread/tokenUsage/updated" || !isRecord(record.payload))
|
|
815
|
+
return [];
|
|
816
|
+
const tokenUsage = isRecord(record.payload.tokenUsage) ? record.payload.tokenUsage : undefined;
|
|
817
|
+
const total = numberRecord(tokenUsage?.total);
|
|
818
|
+
const last = numberRecord(tokenUsage?.last);
|
|
819
|
+
const seenThreadId = text(record.payload.threadId);
|
|
820
|
+
const seenTurnId = text(record.payload.turnId);
|
|
821
|
+
if (total === undefined || last === undefined || seenThreadId === undefined || seenTurnId === undefined)
|
|
822
|
+
return [];
|
|
823
|
+
if (seenThreadId !== input.threadId || seenTurnId !== input.turnId)
|
|
824
|
+
return [];
|
|
825
|
+
return [{
|
|
826
|
+
sequence: record.sequence,
|
|
827
|
+
threadId: seenThreadId,
|
|
828
|
+
turnId: seenTurnId,
|
|
829
|
+
total,
|
|
830
|
+
last,
|
|
831
|
+
...(typeof tokenUsage?.modelContextWindow === "number" ? { modelContextWindow: tokenUsage.modelContextWindow } : {}),
|
|
832
|
+
}];
|
|
833
|
+
});
|
|
834
|
+
return {
|
|
835
|
+
schemaVersion: "ebo.codex-telemetry/v1",
|
|
836
|
+
attemptId: input.attemptId,
|
|
837
|
+
runtime: {
|
|
838
|
+
executable: input.executable,
|
|
839
|
+
version: input.version,
|
|
840
|
+
adapterVersion: CODEX_ADAPTER_VERSION,
|
|
841
|
+
userConfiguration: "isolated",
|
|
842
|
+
localLoginReference: input.localLoginReference,
|
|
843
|
+
},
|
|
844
|
+
effectiveConfiguration: {
|
|
845
|
+
model: input.model,
|
|
846
|
+
provider: input.provider,
|
|
847
|
+
effort: input.effort,
|
|
848
|
+
approvalPolicy: input.approvalPolicy,
|
|
849
|
+
sandbox: input.sandbox,
|
|
850
|
+
workspace: input.workspace,
|
|
851
|
+
environmentKeys: input.environmentKeys,
|
|
852
|
+
...(input.threadStart === undefined ? {} : { threadStart: input.threadStart }),
|
|
853
|
+
},
|
|
854
|
+
telemetry: {
|
|
855
|
+
receipt: {
|
|
856
|
+
status: enabled.length === 0 ? "not-checked"
|
|
857
|
+
: enabled.every((signal) => statuses[signal].status === "received") ? "received" : "missing",
|
|
858
|
+
signals: statuses,
|
|
859
|
+
},
|
|
860
|
+
records,
|
|
861
|
+
receiverErrors,
|
|
862
|
+
},
|
|
863
|
+
usage: {
|
|
864
|
+
updates: usageUpdates,
|
|
865
|
+
...(usageUpdates.length === 0 ? {} : {
|
|
866
|
+
final: {
|
|
867
|
+
total: usageUpdates.at(-1).total,
|
|
868
|
+
last: usageUpdates.at(-1).last,
|
|
869
|
+
...(usageUpdates.at(-1).modelContextWindow === undefined ? {} : {
|
|
870
|
+
modelContextWindow: usageUpdates.at(-1).modelContextWindow,
|
|
871
|
+
}),
|
|
872
|
+
},
|
|
873
|
+
}),
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
async function receiveOtlp(incoming, enabled, state, now) {
|
|
880
|
+
const signal = ["logs", "traces", "metrics"].find((candidate) => incoming.url === `/v1/${candidate}`);
|
|
881
|
+
if (incoming.method !== "POST" || signal === undefined || !enabled.includes(signal)) {
|
|
882
|
+
incoming.resume();
|
|
883
|
+
recordReceiverError(state, "Rejected an unconfigured OTLP route or method.");
|
|
884
|
+
return 404;
|
|
885
|
+
}
|
|
886
|
+
if (state.records.length + state.inFlightRecords >= 256) {
|
|
887
|
+
incoming.resume();
|
|
888
|
+
recordReceiverError(state, `Rejected ${signal} after the 256-record receiver limit.`);
|
|
889
|
+
return 429;
|
|
890
|
+
}
|
|
891
|
+
state.inFlightRecords += 1;
|
|
892
|
+
const chunks = [];
|
|
893
|
+
let bytes = 0;
|
|
894
|
+
try {
|
|
895
|
+
for await (const chunk of incoming) {
|
|
896
|
+
const value = Buffer.from(chunk);
|
|
897
|
+
if (bytes + value.length > 4 * 1024 * 1024
|
|
898
|
+
|| state.bytes + state.inFlightBytes + value.length > 16 * 1024 * 1024) {
|
|
899
|
+
incoming.destroy();
|
|
900
|
+
recordReceiverError(state, `Rejected oversized ${signal} OTLP evidence.`);
|
|
901
|
+
return 413;
|
|
902
|
+
}
|
|
903
|
+
bytes += value.length;
|
|
904
|
+
state.inFlightBytes += value.length;
|
|
905
|
+
chunks.push(value);
|
|
906
|
+
}
|
|
907
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
908
|
+
const record = {
|
|
909
|
+
signal,
|
|
910
|
+
receivedAt: now(),
|
|
911
|
+
...(typeof incoming.headers["content-type"] === "string" ? { contentType: incoming.headers["content-type"] } : {}),
|
|
912
|
+
};
|
|
913
|
+
try {
|
|
914
|
+
record.payload = JSON.parse(raw);
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
record.raw = raw;
|
|
918
|
+
record.parseError = errorMessage(error);
|
|
919
|
+
recordReceiverError(state, `Rejected malformed ${signal} OTLP JSON.`);
|
|
920
|
+
}
|
|
921
|
+
state.records.push(record);
|
|
922
|
+
state.bytes += bytes;
|
|
923
|
+
return record.parseError === undefined ? 200 : 400;
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
recordReceiverError(state, `Failed receiving ${signal} OTLP body: ${errorMessage(error).slice(0, 256)}`);
|
|
927
|
+
return 400;
|
|
928
|
+
}
|
|
929
|
+
finally {
|
|
930
|
+
state.inFlightRecords -= 1;
|
|
931
|
+
state.inFlightBytes -= bytes;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function recordReceiverError(state, message) {
|
|
935
|
+
if (state.receiverErrors.length < 64)
|
|
936
|
+
state.receiverErrors.push(message);
|
|
937
|
+
else if (state.receiverErrors.length === 64)
|
|
938
|
+
state.receiverErrors.push("Additional OTLP receiver errors were truncated.");
|
|
939
|
+
}
|
|
940
|
+
function recordCaptureGap(gaps, gap) {
|
|
941
|
+
if (gaps.some((existing) => existing.kind === gap.kind && existing.detail === gap.detail))
|
|
942
|
+
return;
|
|
943
|
+
if (gaps.length < 64)
|
|
944
|
+
gaps.push(gap);
|
|
945
|
+
else if (!gaps.some(({ kind }) => kind === "capture-gap-limit")) {
|
|
946
|
+
gaps.push({ kind: "capture-gap-limit", detail: "Additional capture gaps remain in native session evidence." });
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function numberRecord(value) {
|
|
950
|
+
if (!isRecord(value))
|
|
951
|
+
return undefined;
|
|
952
|
+
const entries = Object.entries(value);
|
|
953
|
+
if (entries.some(([, item]) => typeof item !== "number" || !Number.isFinite(item)))
|
|
954
|
+
return undefined;
|
|
955
|
+
return Object.fromEntries(entries);
|
|
956
|
+
}
|
|
957
|
+
function isolatedEnvironment(codexHome) {
|
|
958
|
+
const environment = { CODEX_HOME: codexHome, HOME: codexHome };
|
|
959
|
+
for (const key of ["PATH", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "TERM", "NO_COLOR", "USER", "LOGNAME", "SHELL"]) {
|
|
960
|
+
if (process.env[key] !== undefined)
|
|
961
|
+
environment[key] = process.env[key];
|
|
962
|
+
}
|
|
963
|
+
return environment;
|
|
964
|
+
}
|
|
965
|
+
async function withTimeout(promise, milliseconds, message) {
|
|
966
|
+
let timer;
|
|
967
|
+
const timedOut = new Promise((_, reject) => {
|
|
968
|
+
timer = setTimeout(() => reject(new Error(message)), Math.max(0, milliseconds));
|
|
969
|
+
});
|
|
970
|
+
try {
|
|
971
|
+
return await Promise.race([promise, timedOut]);
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
if (timer !== undefined)
|
|
975
|
+
clearTimeout(timer);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function withAbortAwareTimeout(promise, milliseconds, message, signal, abortTimeoutMs) {
|
|
979
|
+
let timer;
|
|
980
|
+
let rejectTimeout;
|
|
981
|
+
const schedule = (delay) => {
|
|
982
|
+
if (timer !== undefined)
|
|
983
|
+
clearTimeout(timer);
|
|
984
|
+
timer = setTimeout(() => rejectTimeout?.(new Error(message)), Math.max(0, delay));
|
|
985
|
+
};
|
|
986
|
+
const onAbort = () => schedule(abortTimeoutMs());
|
|
987
|
+
const timedOut = new Promise((_, reject) => {
|
|
988
|
+
rejectTimeout = reject;
|
|
989
|
+
schedule(milliseconds);
|
|
990
|
+
});
|
|
991
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
992
|
+
if (signal?.aborted)
|
|
993
|
+
onAbort();
|
|
994
|
+
try {
|
|
995
|
+
return await Promise.race([promise, timedOut]);
|
|
996
|
+
}
|
|
997
|
+
finally {
|
|
998
|
+
signal?.removeEventListener("abort", onAbort);
|
|
999
|
+
if (timer !== undefined)
|
|
1000
|
+
clearTimeout(timer);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function settleOrDelay(promise, milliseconds) {
|
|
1004
|
+
let timer;
|
|
1005
|
+
try {
|
|
1006
|
+
await Promise.race([
|
|
1007
|
+
promise.then(() => undefined),
|
|
1008
|
+
new Promise((resolvePromise) => { timer = setTimeout(resolvePromise, Math.max(0, milliseconds)); }),
|
|
1009
|
+
]);
|
|
1010
|
+
}
|
|
1011
|
+
finally {
|
|
1012
|
+
if (timer !== undefined)
|
|
1013
|
+
clearTimeout(timer);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function requireText(value, label) {
|
|
1017
|
+
if (value.trim() === "")
|
|
1018
|
+
throw new Error(`${label} is required.`);
|
|
1019
|
+
}
|
|
1020
|
+
function text(value) {
|
|
1021
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
1022
|
+
}
|
|
1023
|
+
function isRecord(value) {
|
|
1024
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1025
|
+
}
|
|
1026
|
+
function errorMessage(error) {
|
|
1027
|
+
return error instanceof Error ? error.message : String(error);
|
|
1028
|
+
}
|