@scotthuang/agent-knock-knock 0.13.7 → 0.13.8
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/CHANGELOG.md +17 -0
- package/dist/src/callback-outbox-policy.d.ts +1 -1
- package/dist/src/callback-outbox-policy.js +7 -3
- package/dist/src/callback-outbox-policy.js.map +1 -1
- package/dist/src/callback-outbox-service.js +3 -0
- package/dist/src/callback-outbox-service.js.map +1 -1
- package/dist/src/cli-core.js +1 -1
- package/dist/src/codex-astra-composer-proof.d.ts +10 -0
- package/dist/src/codex-astra-composer-proof.js +149 -0
- package/dist/src/codex-astra-composer-proof.js.map +1 -0
- package/dist/src/codex-async-question-adapter.d.ts +156 -0
- package/dist/src/codex-async-question-adapter.js +756 -0
- package/dist/src/codex-async-question-adapter.js.map +1 -0
- package/dist/src/codex-terminal-agent-adapter.js +1 -1
- package/dist/src/codex-terminal-agent-adapter.js.map +1 -1
- package/dist/src/host-profile-callback-transport.d.ts +4 -2
- package/dist/src/host-profile-callback-transport.js +9 -6
- package/dist/src/host-profile-callback-transport.js.map +1 -1
- package/dist/src/semantic-tool-command-helpers.js +2 -0
- package/dist/src/semantic-tool-command-helpers.js.map +1 -1
- package/dist/src/semantic-tool-private-authority.js +4 -1
- package/dist/src/semantic-tool-private-authority.js.map +1 -1
- package/dist/src/semantic-tool-runtime.js +1 -1
- package/dist/src/semantic-tool-runtime.js.map +1 -1
- package/dist/src/semantic-tool-schemas.d.ts +5 -0
- package/dist/src/semantic-tool-schemas.js +6 -1
- package/dist/src/semantic-tool-schemas.js.map +1 -1
- package/dist/src/terminal-agent-adapter.d.ts +9 -0
- package/dist/src/terminal-agent-adapter.js +10 -0
- package/dist/src/terminal-agent-adapter.js.map +1 -1
- package/dist/src/terminal-agent-bridge.d.ts +2 -0
- package/dist/src/terminal-agent-bridge.js +9 -11
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-command-cli-ports.d.ts +1 -0
- package/dist/src/terminal-command-dispatch-preparation.d.ts +1 -0
- package/dist/src/terminal-command-dispatch-preparation.js +2 -1
- package/dist/src/terminal-command-dispatch-preparation.js.map +1 -1
- package/dist/src/terminal-composer-classifier.d.ts +9 -1
- package/dist/src/terminal-composer-classifier.js +19 -1
- package/dist/src/terminal-composer-classifier.js.map +1 -1
- package/dist/src/terminal-composer-readiness-bridge.d.ts +14 -0
- package/dist/src/terminal-composer-readiness-bridge.js +21 -0
- package/dist/src/terminal-composer-readiness-bridge.js.map +1 -0
- package/dist/src/terminal-dispatch-execution.js +11 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -1
- package/dist/src/terminal-handoff-cli-adapter.js +4 -1
- package/dist/src/terminal-handoff-cli-adapter.js.map +1 -1
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -1
- package/dist/src/terminal-identity-authority-cli-adapter.js +15 -16
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
- package/dist/src/terminal-interaction-core.d.ts +30 -5
- package/dist/src/terminal-interaction-core.js +94 -8
- package/dist/src/terminal-interaction-core.js.map +1 -1
- package/dist/src/terminal-interaction-protocol.d.ts +9 -1
- package/dist/src/terminal-interaction-protocol.js +71 -9
- package/dist/src/terminal-interaction-protocol.js.map +1 -1
- package/dist/src/terminal-interaction-response-bridge.d.ts +19 -6
- package/dist/src/terminal-interaction-response-bridge.js +345 -17
- package/dist/src/terminal-interaction-response-bridge.js.map +1 -1
- package/dist/src/terminal-list-renderer.js +4 -3
- package/dist/src/terminal-list-renderer.js.map +1 -1
- package/dist/src/terminal-monitor-application-service.d.ts +4 -0
- package/dist/src/terminal-monitor-application-service.js +61 -7
- package/dist/src/terminal-monitor-application-service.js.map +1 -1
- package/dist/src/terminal-monitor-cli-adapter.d.ts +2 -0
- package/dist/src/terminal-monitor-cli-adapter.js +16 -3
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
- package/dist/src/terminal-monitor-interaction-store.d.ts +9 -0
- package/dist/src/terminal-monitor-interaction-store.js +65 -0
- package/dist/src/terminal-monitor-interaction-store.js.map +1 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js +11 -12
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
- package/dist/src/terminal-questionnaire-adapter.d.ts +1 -0
- package/dist/src/terminal-questionnaire-adapter.js +1 -1
- package/dist/src/terminal-questionnaire-adapter.js.map +1 -1
- package/dist/src/terminal-submission-acceptance.d.ts +12 -0
- package/dist/src/terminal-submission-acceptance.js +152 -0
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-text-submission-bridge.d.ts +1 -1
- package/dist/src/terminal-text-submission-bridge.js +1 -1
- package/dist/src/terminal-text-submission-bridge.js.map +1 -1
- package/dist/src/terminal-watch-cli-adapter.js +61 -78
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
- package/dist/src/terminal-watch-codec.js +1 -1
- package/dist/src/terminal-watch-codec.js.map +1 -1
- package/dist/src/terminal-watch-interaction-runtime.d.ts +10 -0
- package/dist/src/terminal-watch-interaction-runtime.js +88 -0
- package/dist/src/terminal-watch-interaction-runtime.js.map +1 -0
- package/dist/src/terminal-watch-record.d.ts +1 -1
- package/dist/src/terminal-watch-service.d.ts +7 -0
- package/dist/src/terminal-watch-service.js +88 -35
- package/dist/src/terminal-watch-service.js.map +1 -1
- package/docs/host-bridge-profiles.md +9 -0
- package/docs/quickstart-herdr.md +4 -2
- package/docs/quickstart-tmux.md +5 -3
- package/package.json +1 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +9 -9
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* This adapter is deliberately version-bound. Later Codex releases gained
|
|
4
|
+
* different durable answer identities; callers must not silently reuse this
|
|
5
|
+
* TUI contract for an unverified release.
|
|
6
|
+
*/
|
|
7
|
+
export const CODEX_ASYNC_QUESTION_PROFILES = Object.freeze({
|
|
8
|
+
"0.154.0": "codex/0.154.0/request-user-input-async-v1",
|
|
9
|
+
"0.155.1": "codex/0.155.1/request-user-input-async-v1"
|
|
10
|
+
});
|
|
11
|
+
export const CODEX_ASYNC_QUESTION_LIMITS = Object.freeze({
|
|
12
|
+
maxRecords: 4_096,
|
|
13
|
+
maxArgumentsBytes: 64 * 1024,
|
|
14
|
+
maxQuestions: 16,
|
|
15
|
+
maxOptions: 32,
|
|
16
|
+
maxTitleCharacters: 4_096,
|
|
17
|
+
maxOptionCharacters: 512,
|
|
18
|
+
maxIdentifierCharacters: 256,
|
|
19
|
+
maxScreenCharacters: 128 * 1024,
|
|
20
|
+
maxScreenLines: 160
|
|
21
|
+
});
|
|
22
|
+
const ANSI_SEQUENCE_PATTERN = /[\u001B\u009B](?:(?:\[[0-?]*[ -/]*[@-~])|(?:\][^\u0007]*(?:\u0007|\u001B\\))|.)/gu;
|
|
23
|
+
const UNSAFE_CONTROL_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/u;
|
|
24
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
|
|
25
|
+
export function codexAsyncQuestionProfile(version) {
|
|
26
|
+
return Object.hasOwn(CODEX_ASYNC_QUESTION_PROFILES, version)
|
|
27
|
+
? CODEX_ASYNC_QUESTION_PROFILES[version]
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse already-bounded, already-read rollout records. `function_call_output`
|
|
32
|
+
* with `{accepted:true}` is deliberately ignored: request_user_input_async
|
|
33
|
+
* returns immediately, so that output does not mean the user answered.
|
|
34
|
+
*/
|
|
35
|
+
export function parseCodexAsyncQuestionRecordCandidates(input) {
|
|
36
|
+
if (!codexAsyncQuestionProfile(input.version)) {
|
|
37
|
+
return { status: "unsupported_version" };
|
|
38
|
+
}
|
|
39
|
+
if (input.records.length > CODEX_ASYNC_QUESTION_LIMITS.maxRecords) {
|
|
40
|
+
return { status: "ambiguous", reason: "record_limit_exceeded" };
|
|
41
|
+
}
|
|
42
|
+
const evidence = [];
|
|
43
|
+
const itemIds = new Set();
|
|
44
|
+
for (const value of input.records) {
|
|
45
|
+
if (!isRecord(value) || value.type !== "response_item")
|
|
46
|
+
continue;
|
|
47
|
+
const payload = isRecord(value.payload) ? value.payload : undefined;
|
|
48
|
+
if (!payload || payload.type !== "function_call" ||
|
|
49
|
+
payload.name !== "request_user_input_async") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const itemId = boundedIdentifier(payload.call_id);
|
|
53
|
+
const metadata = isRecord(payload.internal_chat_message_metadata_passthrough)
|
|
54
|
+
? payload.internal_chat_message_metadata_passthrough
|
|
55
|
+
: undefined;
|
|
56
|
+
const turnId = boundedIdentifier(metadata?.turn_id) ??
|
|
57
|
+
boundedIdentifier(input.fallbackTurnId);
|
|
58
|
+
if (!itemId || !turnId) {
|
|
59
|
+
return { status: "ambiguous", reason: "invalid_identity" };
|
|
60
|
+
}
|
|
61
|
+
if (itemIds.has(itemId)) {
|
|
62
|
+
return { status: "ambiguous", reason: "duplicate_item_id" };
|
|
63
|
+
}
|
|
64
|
+
const questions = parseArguments(payload.arguments);
|
|
65
|
+
if (!questions) {
|
|
66
|
+
return { status: "ambiguous", reason: "invalid_arguments" };
|
|
67
|
+
}
|
|
68
|
+
itemIds.add(itemId);
|
|
69
|
+
evidence.push({ itemId, turnId, questions });
|
|
70
|
+
}
|
|
71
|
+
return evidence.length === 0
|
|
72
|
+
? { status: "none" }
|
|
73
|
+
: { status: "candidates", evidence };
|
|
74
|
+
}
|
|
75
|
+
export function inspectCodexAsyncQuestion(input) {
|
|
76
|
+
const profile = codexAsyncQuestionProfile(input.version);
|
|
77
|
+
if (!profile) {
|
|
78
|
+
return { state: "ambiguous", reason: "unsupported_version" };
|
|
79
|
+
}
|
|
80
|
+
const captured = captureLines(input.screen);
|
|
81
|
+
if (captured.status !== "ok") {
|
|
82
|
+
return { state: "ambiguous", reason: captured.reason, profile };
|
|
83
|
+
}
|
|
84
|
+
const normalizedEvidence = normalizeEvidence(input.evidence ?? [], profile);
|
|
85
|
+
if (!normalizedEvidence) {
|
|
86
|
+
return { state: "ambiguous", reason: "invalid_durable_evidence", profile };
|
|
87
|
+
}
|
|
88
|
+
const collapsed = parseCollapsed(captured.lines);
|
|
89
|
+
const expandedMarker = hasExpandedMarker(captured.lines);
|
|
90
|
+
if (collapsed.status === "exact" && !expandedMarker) {
|
|
91
|
+
return inspectCollapsedQuestion(collapsed, normalizedEvidence, profile);
|
|
92
|
+
}
|
|
93
|
+
if (collapsed.status === "partial" && !expandedMarker) {
|
|
94
|
+
return { state: "ambiguous", reason: "partial_or_unknown_surface", profile };
|
|
95
|
+
}
|
|
96
|
+
const expanded = parseExpanded(captured.lines, normalizedEvidence, profile);
|
|
97
|
+
if (expanded.status === "none") {
|
|
98
|
+
return collapsed.status === "none"
|
|
99
|
+
? { state: "absent", reason: "no_async_question_surface" }
|
|
100
|
+
: { state: "ambiguous", reason: "partial_or_unknown_surface", profile };
|
|
101
|
+
}
|
|
102
|
+
if (expanded.status === "ambiguous") {
|
|
103
|
+
return { state: "ambiguous", reason: expanded.reason, profile };
|
|
104
|
+
}
|
|
105
|
+
return inspectExpandedQuestion(expanded, profile);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Owner-private pre-submit proof after one text injection. The public observer
|
|
109
|
+
* continues to reject drafts. Text must be wholly visible on one row: terminal
|
|
110
|
+
* wrapping cannot prove the original buffer's whitespace or hidden contents.
|
|
111
|
+
*/
|
|
112
|
+
export function verifyCodexAsyncQuestionInjectedText(input) {
|
|
113
|
+
const text = input.expectedText;
|
|
114
|
+
if (!text || text !== text.trim() || /[\r\n\t]/u.test(text) ||
|
|
115
|
+
UNSAFE_CONTROL_PATTERN.test(text) || [...text].length > 4_096) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const captured = captureLines(input.screen);
|
|
119
|
+
if (captured.status !== "ok")
|
|
120
|
+
return false;
|
|
121
|
+
let matches = 0;
|
|
122
|
+
for (const [index, line] of captured.lines.entries()) {
|
|
123
|
+
for (const replacement of injectedTextPlaceholderRows(line, text)) {
|
|
124
|
+
const restored = [...captured.lines];
|
|
125
|
+
restored[index] = replacement;
|
|
126
|
+
const inspection = inspectCodexAsyncQuestion({
|
|
127
|
+
version: input.version,
|
|
128
|
+
screen: restored.join("\n"),
|
|
129
|
+
evidence: input.evidence
|
|
130
|
+
});
|
|
131
|
+
if (inspection.state === "expanded" &&
|
|
132
|
+
inspection.match.question.response_kind === "free_text" &&
|
|
133
|
+
inspection.match.question.question_id === input.expectedQuestionId) {
|
|
134
|
+
matches += 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return matches === 1;
|
|
139
|
+
}
|
|
140
|
+
function injectedTextPlaceholderRows(line, text) {
|
|
141
|
+
if (line.trimEnd() === ` ${text}`) {
|
|
142
|
+
return text === "Type your answer" ? [] : [" Type your answer"];
|
|
143
|
+
}
|
|
144
|
+
const optionPrefix = /^ {2}› [1-9]\d*\. /u.exec(line)?.[0];
|
|
145
|
+
if (!optionPrefix || line.slice(optionPrefix.length).trimEnd() !== text) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return ["Other", "Other (write an answer)"]
|
|
149
|
+
.filter((placeholder) => placeholder !== text)
|
|
150
|
+
.map((placeholder) => `${optionPrefix}${placeholder}`);
|
|
151
|
+
}
|
|
152
|
+
/** Withdraw-only observation; this never grants main-Composer input authority. */
|
|
153
|
+
export function codexAsyncQuestionMainComposerVisible(input) {
|
|
154
|
+
const captured = captureLines(input.screen);
|
|
155
|
+
if (captured.status !== "ok" ||
|
|
156
|
+
inspectCodexAsyncQuestion(input).state !== "absent") {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const footer = captured.lines.at(-1)?.trim() ?? "";
|
|
160
|
+
return /^(?:gpt-[\w.-]+(?:\s+\S+)?|[-\w.]+ default)\s+·\s+\S.*$/u
|
|
161
|
+
.test(footer) && captured.lines.slice(0, -1).some((line) => /^[›»](?:\s|$)/u.test(line));
|
|
162
|
+
}
|
|
163
|
+
function inspectCollapsedQuestion(collapsed, evidence, profile) {
|
|
164
|
+
const knownRemaining = evidence.length > 0 &&
|
|
165
|
+
evidence.every((entry) => entry.remainingCount !== undefined)
|
|
166
|
+
? evidence.reduce((total, entry) => total + entry.remainingCount, 0)
|
|
167
|
+
: undefined;
|
|
168
|
+
if (knownRemaining !== undefined && knownRemaining < collapsed.pendingCount) {
|
|
169
|
+
return { state: "ambiguous", reason: "pending_count_mismatch", profile };
|
|
170
|
+
}
|
|
171
|
+
const match = collapsedMatch(evidence, collapsed.pendingCount, profile);
|
|
172
|
+
const exactDigest = sha256("codex-async-question-region", collapsed.exactLines.join("\n"));
|
|
173
|
+
return {
|
|
174
|
+
state: "collapsed",
|
|
175
|
+
profile,
|
|
176
|
+
pending_count: collapsed.pendingCount,
|
|
177
|
+
...(collapsed.countdownSeconds === undefined
|
|
178
|
+
? {}
|
|
179
|
+
: { countdown_seconds: collapsed.countdownSeconds }),
|
|
180
|
+
...(match ? { match } : {}),
|
|
181
|
+
prompt_evidence: {
|
|
182
|
+
profile,
|
|
183
|
+
exact_region_sha256: exactDigest,
|
|
184
|
+
semantic_sha256: sha256("codex-async-question-semantic", {
|
|
185
|
+
kind: "collapsed",
|
|
186
|
+
pending_count: collapsed.pendingCount,
|
|
187
|
+
match: match?.question.question_id
|
|
188
|
+
})
|
|
189
|
+
},
|
|
190
|
+
owner_private_action_plan: {
|
|
191
|
+
kind: "open_async_question_editor",
|
|
192
|
+
binding: collapsed.binding,
|
|
193
|
+
expected_pending_count: collapsed.pendingCount,
|
|
194
|
+
expected_region_sha256: exactDigest
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function inspectExpandedQuestion(expanded, profile) {
|
|
199
|
+
const exactDigest = sha256("codex-async-question-region", expanded.exactLines.join("\n"));
|
|
200
|
+
const question = expanded.match.question;
|
|
201
|
+
const options = question.options ?? [];
|
|
202
|
+
const selectedOption = expanded.match.selected_option_id;
|
|
203
|
+
const choicePlans = options.map((option, index) => ({
|
|
204
|
+
option_id: option.option_id,
|
|
205
|
+
native_ordinal: index + 1,
|
|
206
|
+
intent: option.kind === "free_text_entry"
|
|
207
|
+
? "open_free_text"
|
|
208
|
+
: "submit_choice"
|
|
209
|
+
}));
|
|
210
|
+
const freeTextEntry = options.findIndex((option) => option.kind === "free_text_entry");
|
|
211
|
+
return {
|
|
212
|
+
state: "expanded",
|
|
213
|
+
profile,
|
|
214
|
+
current_step: expanded.currentStep,
|
|
215
|
+
total_steps: expanded.totalSteps,
|
|
216
|
+
match: expanded.match,
|
|
217
|
+
prompt_evidence: {
|
|
218
|
+
profile,
|
|
219
|
+
exact_region_sha256: exactDigest,
|
|
220
|
+
semantic_sha256: sha256("codex-async-question-semantic", {
|
|
221
|
+
source_id: expanded.match.source_id,
|
|
222
|
+
question_id: question.question_id,
|
|
223
|
+
selected_option_id: selectedOption
|
|
224
|
+
})
|
|
225
|
+
},
|
|
226
|
+
owner_private_action_plan: {
|
|
227
|
+
kind: "answer_async_question",
|
|
228
|
+
question_id: question.question_id,
|
|
229
|
+
choices: choicePlans,
|
|
230
|
+
free_text: freeTextEntry >= 0
|
|
231
|
+
? {
|
|
232
|
+
intent: "select_other_then_enter_text",
|
|
233
|
+
native_ordinal: freeTextEntry + 1,
|
|
234
|
+
max_characters: 4_096
|
|
235
|
+
}
|
|
236
|
+
: {
|
|
237
|
+
intent: "enter_free_text",
|
|
238
|
+
max_characters: 4_096
|
|
239
|
+
},
|
|
240
|
+
delivery: {
|
|
241
|
+
steer_current_turn: "submit",
|
|
242
|
+
queue_next_turn: "queue"
|
|
243
|
+
},
|
|
244
|
+
expected_region_sha256: exactDigest
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function normalizeEvidence(values, profile) {
|
|
249
|
+
const sourceIds = new Set();
|
|
250
|
+
const normalized = [];
|
|
251
|
+
for (const value of values) {
|
|
252
|
+
const entry = normalizeEvidenceEntry(value, profile);
|
|
253
|
+
if (!entry || sourceIds.has(entry.sourceId))
|
|
254
|
+
return undefined;
|
|
255
|
+
sourceIds.add(entry.sourceId);
|
|
256
|
+
normalized.push(entry);
|
|
257
|
+
}
|
|
258
|
+
return normalized;
|
|
259
|
+
}
|
|
260
|
+
function normalizeEvidenceEntry(value, profile) {
|
|
261
|
+
const itemId = boundedIdentifier(value.itemId);
|
|
262
|
+
const turnId = boundedIdentifier(value.turnId);
|
|
263
|
+
if (!itemId || !turnId || !validQuestionArray(value.questions)) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
if (!validOptionalProgress(value.currentIndex, 0, value.questions.length - 1) ||
|
|
267
|
+
!validOptionalProgress(value.remainingCount, 1, value.questions.length)) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
const sourceId = `cai_${sha256("codex-async-question-source", {
|
|
271
|
+
profile,
|
|
272
|
+
item_id: itemId,
|
|
273
|
+
turn_id: turnId
|
|
274
|
+
}).slice(0, 40)}`;
|
|
275
|
+
const questions = [];
|
|
276
|
+
for (const [index, question] of value.questions.entries()) {
|
|
277
|
+
const normalized = normalizeQuestion(question, sourceId, index);
|
|
278
|
+
if (!normalized)
|
|
279
|
+
return undefined;
|
|
280
|
+
questions.push(normalized);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
itemId,
|
|
284
|
+
turnId,
|
|
285
|
+
sourceId,
|
|
286
|
+
...(value.currentIndex === undefined
|
|
287
|
+
? {}
|
|
288
|
+
: { currentIndex: value.currentIndex }),
|
|
289
|
+
...(value.remainingCount === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { remainingCount: value.remainingCount }),
|
|
292
|
+
questions
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function validOptionalProgress(value, minimum, maximum) {
|
|
296
|
+
return value === undefined ||
|
|
297
|
+
(Number.isSafeInteger(value) && value >= minimum && value <= maximum);
|
|
298
|
+
}
|
|
299
|
+
function normalizeQuestion(question, sourceId, sourceIndex) {
|
|
300
|
+
const title = safeSemanticText(question.title, CODEX_ASYNC_QUESTION_LIMITS.maxTitleCharacters);
|
|
301
|
+
const modelOptions = parseOptionLabels(question.options ?? []);
|
|
302
|
+
if (!title || !modelOptions)
|
|
303
|
+
return undefined;
|
|
304
|
+
const normalizedModelOptions = modelOptions.map(normalizeText);
|
|
305
|
+
if (new Set(normalizedModelOptions).size !== modelOptions.length) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
const questionId = `question_${sha256("codex-async-question", {
|
|
309
|
+
source_id: sourceId,
|
|
310
|
+
source_index: sourceIndex,
|
|
311
|
+
title: normalizeText(title),
|
|
312
|
+
options: normalizedModelOptions
|
|
313
|
+
}).slice(0, 40)}`;
|
|
314
|
+
const semanticOptions = buildSemanticOptions(questionId, modelOptions);
|
|
315
|
+
return {
|
|
316
|
+
sourceIndex,
|
|
317
|
+
title,
|
|
318
|
+
normalizedTitle: normalizeText(title),
|
|
319
|
+
modelOptions,
|
|
320
|
+
normalizedModelOptions,
|
|
321
|
+
semantic: {
|
|
322
|
+
question_id: questionId,
|
|
323
|
+
prompt: title,
|
|
324
|
+
response_kind: modelOptions.length > 0 ? "single_select" : "free_text",
|
|
325
|
+
required: true,
|
|
326
|
+
...(semanticOptions.length > 0 ? { options: semanticOptions } : {})
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function nativeOtherLabel(normalizedModelOptions) {
|
|
331
|
+
return normalizedModelOptions.some((label) => label.toLocaleLowerCase("en-US") === "other") ? "Other (write an answer)" : "Other";
|
|
332
|
+
}
|
|
333
|
+
function buildSemanticOptions(questionId, modelOptions) {
|
|
334
|
+
const result = modelOptions.map((label, optionIndex) => ({
|
|
335
|
+
option_id: `option_${sha256("codex-async-question-option", {
|
|
336
|
+
question_id: questionId,
|
|
337
|
+
option_index: optionIndex,
|
|
338
|
+
label: normalizeText(label)
|
|
339
|
+
}).slice(0, 40)}`,
|
|
340
|
+
label,
|
|
341
|
+
kind: "suggested"
|
|
342
|
+
}));
|
|
343
|
+
if (modelOptions.length > 0) {
|
|
344
|
+
result.push({
|
|
345
|
+
option_id: `option_${sha256("codex-async-question-option", {
|
|
346
|
+
question_id: questionId,
|
|
347
|
+
option_index: modelOptions.length,
|
|
348
|
+
kind: "free_text_entry"
|
|
349
|
+
}).slice(0, 40)}`,
|
|
350
|
+
label: nativeOtherLabel(modelOptions.map(normalizeText)),
|
|
351
|
+
kind: "free_text_entry"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
function collapsedMatch(evidence, pendingCount, profile) {
|
|
357
|
+
if (evidence.length === 0 ||
|
|
358
|
+
!evidence.every((entry) => entry.currentIndex !== undefined && entry.remainingCount !== undefined) ||
|
|
359
|
+
evidence.reduce((sum, entry) => sum + entry.remainingCount, 0) <
|
|
360
|
+
pendingCount)
|
|
361
|
+
return undefined;
|
|
362
|
+
const ordered = evidence.flatMap((entry) => entry.questions
|
|
363
|
+
.slice(entry.currentIndex)
|
|
364
|
+
.map((question) => ({ entry, question })));
|
|
365
|
+
const consumedCount = ordered.length - pendingCount;
|
|
366
|
+
const current = ordered[consumedCount];
|
|
367
|
+
if (!current || consumedCount < 0)
|
|
368
|
+
return undefined;
|
|
369
|
+
// The profile participates through sourceId/questionId. Keeping the argument
|
|
370
|
+
// explicit makes accidental cross-version reuse visible to reviewers.
|
|
371
|
+
void profile;
|
|
372
|
+
return {
|
|
373
|
+
source_id: current.entry.sourceId,
|
|
374
|
+
source_question_index: current.question.sourceIndex,
|
|
375
|
+
question: current.question.semantic
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function captureLines(screen) {
|
|
379
|
+
if (typeof screen !== "string" ||
|
|
380
|
+
screen.length > CODEX_ASYNC_QUESTION_LIMITS.maxScreenCharacters) {
|
|
381
|
+
return { status: "error", reason: "capture_limit_exceeded" };
|
|
382
|
+
}
|
|
383
|
+
const plain = screen.replace(ANSI_SEQUENCE_PATTERN, "")
|
|
384
|
+
.replace(/\r\n?/gu, "\n")
|
|
385
|
+
.replace(/\u00a0/gu, " ");
|
|
386
|
+
if (UNSAFE_CONTROL_PATTERN.test(plain)) {
|
|
387
|
+
return { status: "error", reason: "unsafe_control" };
|
|
388
|
+
}
|
|
389
|
+
const lines = plain.split("\n")
|
|
390
|
+
.slice(-CODEX_ASYNC_QUESTION_LIMITS.maxScreenLines);
|
|
391
|
+
while (lines.length > 0 && lines.at(-1)?.trim().length === 0)
|
|
392
|
+
lines.pop();
|
|
393
|
+
return { status: "ok", lines };
|
|
394
|
+
}
|
|
395
|
+
function parseCollapsed(lines) {
|
|
396
|
+
let header = -1;
|
|
397
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
398
|
+
if (lines[index] === "• Queued follow-up inputs") {
|
|
399
|
+
header = index;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (header < 0) {
|
|
404
|
+
return lines.some((line) => /^\s*\?\s+\d+\s+questions?/iu.test(line) ||
|
|
405
|
+
/\bto answer\s*$/iu.test(line))
|
|
406
|
+
? { status: "partial" }
|
|
407
|
+
: { status: "none" };
|
|
408
|
+
}
|
|
409
|
+
let summary = -1;
|
|
410
|
+
let summaryMatch = null;
|
|
411
|
+
for (let index = header + 1; index < lines.length; index += 1) {
|
|
412
|
+
const match = /^ \? ([1-9]\d{0,2}) (question|questions)(?: · ([1-9]\d{0,2})s)?$/u
|
|
413
|
+
.exec(lines[index]);
|
|
414
|
+
if (match) {
|
|
415
|
+
summary = index;
|
|
416
|
+
summaryMatch = match;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (summary < 0 || !summaryMatch) {
|
|
421
|
+
const positive = lines.slice(header + 1).some((line) => /^\s*\?/u.test(line) || /\bto answer\s*$/iu.test(line));
|
|
422
|
+
return positive ? { status: "partial" } : { status: "none" };
|
|
423
|
+
}
|
|
424
|
+
const pendingCount = Number(summaryMatch[1]);
|
|
425
|
+
if ((pendingCount === 1) !== (summaryMatch[2] === "question"))
|
|
426
|
+
return { status: "partial" };
|
|
427
|
+
let hint = summary + 1;
|
|
428
|
+
while (hint < lines.length && lines[hint].trim().length === 0)
|
|
429
|
+
hint += 1;
|
|
430
|
+
const hintLine = lines[hint] ?? "";
|
|
431
|
+
const binding = hintLine === " shift + ← to answer"
|
|
432
|
+
? "shift_left"
|
|
433
|
+
: hintLine === " ⌥ + ↑ to answer"
|
|
434
|
+
? "alt_up"
|
|
435
|
+
: undefined;
|
|
436
|
+
if (!binding)
|
|
437
|
+
return { status: "partial" };
|
|
438
|
+
return {
|
|
439
|
+
status: "exact",
|
|
440
|
+
pendingCount,
|
|
441
|
+
...(summaryMatch[3] === undefined
|
|
442
|
+
? {}
|
|
443
|
+
: { countdownSeconds: Number(summaryMatch[3]) }),
|
|
444
|
+
binding,
|
|
445
|
+
exactLines: lines.slice(header, hint + 1)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function hasExpandedMarker(lines) {
|
|
449
|
+
return lines.some((line) => /^ {2}enter submit\b/u.test(line) ||
|
|
450
|
+
/^ {2}\d+ of(?: \d+)?/u.test(line) ||
|
|
451
|
+
/^ {2}(?:ente|ctrl)…\s*$/u.test(line) ||
|
|
452
|
+
/^ {2}Expand terminal to read the entire option\s*$/u.test(line));
|
|
453
|
+
}
|
|
454
|
+
function parseExpanded(lines, evidence, profile) {
|
|
455
|
+
let footerStart = -1;
|
|
456
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
457
|
+
if (/^ {2}\S.*\bsubmit\b/u.test(lines[index])) {
|
|
458
|
+
footerStart = index;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (footerStart < 0) {
|
|
463
|
+
if (lines.some((line) => /^ {2}Expand terminal to read the entire option\s*$/u.test(line)))
|
|
464
|
+
return { status: "ambiguous", reason: "clipped_question" };
|
|
465
|
+
return hasExpandedMarker(lines)
|
|
466
|
+
? { status: "ambiguous", reason: "partial_or_unknown_surface" }
|
|
467
|
+
: { status: "none" };
|
|
468
|
+
}
|
|
469
|
+
const footerLines = lines.slice(footerStart).filter((line) => line.trim());
|
|
470
|
+
const footer = footerLines.map((line) => normalizeText(line)).join(" ");
|
|
471
|
+
if (/\boption \d+\/\d+\b/iu.test(footer) ||
|
|
472
|
+
lines.slice(Math.max(0, footerStart - 5), footerStart).some((line) => normalizeText(line) === "Expand terminal to read the entire option")) {
|
|
473
|
+
return { status: "ambiguous", reason: "clipped_question" };
|
|
474
|
+
}
|
|
475
|
+
if (!/^enter submit ctrl \+ \] skip ⌥ \+ ↓ (?:main prompt|prev question)(?: (?:⌥ \+ ↑|shift \+ ←) (?:next question|queued messages))?$/u
|
|
476
|
+
.test(footer)) {
|
|
477
|
+
return { status: "ambiguous", reason: "unsupported_keymap" };
|
|
478
|
+
}
|
|
479
|
+
if (evidence.length === 0) {
|
|
480
|
+
return { status: "ambiguous", reason: "question_match_missing" };
|
|
481
|
+
}
|
|
482
|
+
const matches = [];
|
|
483
|
+
const content = lines.slice(0, footerStart);
|
|
484
|
+
const maximumPending = evidence.reduce((total, source) => total + source.questions.length, 0);
|
|
485
|
+
for (const source of evidence) {
|
|
486
|
+
for (const question of source.questions) {
|
|
487
|
+
for (let start = 0; start < content.length; start += 1) {
|
|
488
|
+
if (!lineCanStartText(content[start]))
|
|
489
|
+
continue;
|
|
490
|
+
const consumed = consumeWrappedText(content, start, question.normalizedTitle);
|
|
491
|
+
if (consumed < 0)
|
|
492
|
+
continue;
|
|
493
|
+
const parsed = parseQuestionBody(content, consumed, question, source, start, footerStart, maximumPending);
|
|
494
|
+
if (parsed)
|
|
495
|
+
matches.push(parsed);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const unique = uniqueExpandedMatches(matches);
|
|
500
|
+
if (unique.length === 0) {
|
|
501
|
+
const draftLikely = evidence.some((source) => source.questions.some((question) => content.some((line) => normalizeText(line) === question.normalizedTitle)));
|
|
502
|
+
return {
|
|
503
|
+
status: "ambiguous",
|
|
504
|
+
reason: draftLikely ? "existing_answer_draft" : "question_match_missing"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (unique.length !== 1) {
|
|
508
|
+
return { status: "ambiguous", reason: "question_match_ambiguous" };
|
|
509
|
+
}
|
|
510
|
+
const exact = unique[0];
|
|
511
|
+
void profile;
|
|
512
|
+
return {
|
|
513
|
+
status: "exact",
|
|
514
|
+
currentStep: exact.currentStep,
|
|
515
|
+
totalSteps: exact.totalSteps,
|
|
516
|
+
match: exact.match,
|
|
517
|
+
exactLines: lines.slice(exact.start, lines.length)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function parseQuestionBody(lines, bodyStart, question, source, questionStart, footerStart, maximumPending) {
|
|
521
|
+
let cursor = bodyStart;
|
|
522
|
+
while (cursor < lines.length && lines[cursor].trim().length === 0)
|
|
523
|
+
cursor += 1;
|
|
524
|
+
const match = parseAnswerRegion(lines.slice(cursor), question, source);
|
|
525
|
+
const progress = progressForQuestion(lines, questionStart, maximumPending);
|
|
526
|
+
if (!match || !progress)
|
|
527
|
+
return undefined;
|
|
528
|
+
return {
|
|
529
|
+
start: progress.start,
|
|
530
|
+
end: footerStart,
|
|
531
|
+
currentStep: progress.current,
|
|
532
|
+
totalSteps: progress.total,
|
|
533
|
+
match
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function parseAnswerRegion(lines, question, source) {
|
|
537
|
+
const match = {
|
|
538
|
+
source_id: source.sourceId,
|
|
539
|
+
source_question_index: question.sourceIndex,
|
|
540
|
+
question: question.semantic
|
|
541
|
+
};
|
|
542
|
+
if (question.modelOptions.length === 0) {
|
|
543
|
+
return normalizeText(lines[0] ?? "") === "Type your answer" &&
|
|
544
|
+
lines.slice(1).every((line) => line.trim().length === 0)
|
|
545
|
+
? match
|
|
546
|
+
: undefined;
|
|
547
|
+
}
|
|
548
|
+
const selectedOption = parseSelectedOption(lines, question);
|
|
549
|
+
if (!selectedOption)
|
|
550
|
+
return undefined;
|
|
551
|
+
if (selectedOption.kind !== "free_text_entry") {
|
|
552
|
+
return { ...match, selected_option_id: selectedOption.option_id };
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
...match,
|
|
556
|
+
question: {
|
|
557
|
+
question_id: `question_${sha256("codex-async-question-free-text", {
|
|
558
|
+
source_id: source.sourceId,
|
|
559
|
+
source_index: question.sourceIndex,
|
|
560
|
+
parent_question_id: question.semantic.question_id
|
|
561
|
+
}).slice(0, 40)}`,
|
|
562
|
+
prompt: question.semantic.prompt,
|
|
563
|
+
response_kind: "free_text",
|
|
564
|
+
required: true
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function parseSelectedOption(lines, question) {
|
|
569
|
+
const rows = parseOptionRows(lines);
|
|
570
|
+
const expectedLabels = [
|
|
571
|
+
...question.normalizedModelOptions,
|
|
572
|
+
normalizeText(nativeOtherLabel(question.normalizedModelOptions))
|
|
573
|
+
];
|
|
574
|
+
if (!rows || rows.length !== expectedLabels.length ||
|
|
575
|
+
rows.some((row, index) => row.number !== index + 1 || row.label !== expectedLabels[index]) || rows.filter((row) => row.selected).length !== 1) {
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
return question.semantic.options?.[rows.findIndex((row) => row.selected)];
|
|
579
|
+
}
|
|
580
|
+
function parseOptionRows(lines) {
|
|
581
|
+
const rows = [];
|
|
582
|
+
let cursor = 0;
|
|
583
|
+
while (cursor < lines.length) {
|
|
584
|
+
if (lines[cursor].trim().length === 0) {
|
|
585
|
+
cursor += 1;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
const row = /^ {2}(›| ) (\d+)\. (\S.*)$/u.exec(lines[cursor]);
|
|
589
|
+
if (!row)
|
|
590
|
+
return undefined;
|
|
591
|
+
cursor += 1;
|
|
592
|
+
let label = row[3];
|
|
593
|
+
while (cursor < lines.length && /^ {7,}\S/u.test(lines[cursor])) {
|
|
594
|
+
label += ` ${lines[cursor].trim()}`;
|
|
595
|
+
cursor += 1;
|
|
596
|
+
}
|
|
597
|
+
rows.push({
|
|
598
|
+
number: Number(row[2]),
|
|
599
|
+
selected: row[1] === "›",
|
|
600
|
+
label: normalizeText(label)
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return rows;
|
|
604
|
+
}
|
|
605
|
+
function progressForQuestion(lines, questionStart, maximumPending) {
|
|
606
|
+
let previous = questionStart - 1;
|
|
607
|
+
while (previous >= 0 && lines[previous].trim().length === 0)
|
|
608
|
+
previous -= 1;
|
|
609
|
+
const match = previous >= 0
|
|
610
|
+
? /^ {2}([1-9]\d*) of ([1-9]\d*)\s*$/u.exec(lines[previous])
|
|
611
|
+
: null;
|
|
612
|
+
if (!match) {
|
|
613
|
+
if (previous >= 0 && /^ {2}\d+ of\b/u.test(lines[previous])) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
return { start: questionStart, current: 1, total: 1 };
|
|
617
|
+
}
|
|
618
|
+
const current = Number(match[1]);
|
|
619
|
+
const total = Number(match[2]);
|
|
620
|
+
if (current > total || total > maximumPending) {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
// Codex removes answered questions from its local pending queue. This counter
|
|
624
|
+
// describes that mutable queue, not the index/count in the durable item.
|
|
625
|
+
// Identity comes from the uniquely matched title and complete option region.
|
|
626
|
+
return { start: previous, current, total };
|
|
627
|
+
}
|
|
628
|
+
function uniqueExpandedMatches(values) {
|
|
629
|
+
const unique = new Map();
|
|
630
|
+
for (const value of values) {
|
|
631
|
+
const key = [
|
|
632
|
+
value.match.source_id,
|
|
633
|
+
value.match.question.question_id,
|
|
634
|
+
value.start,
|
|
635
|
+
value.end
|
|
636
|
+
].join(":");
|
|
637
|
+
unique.set(key, value);
|
|
638
|
+
}
|
|
639
|
+
return [...unique.values()];
|
|
640
|
+
}
|
|
641
|
+
function consumeWrappedText(lines, start, expected) {
|
|
642
|
+
let combined = "";
|
|
643
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
644
|
+
const line = normalizeText(lines[index]);
|
|
645
|
+
if (!line)
|
|
646
|
+
return -1;
|
|
647
|
+
combined = combined ? `${combined} ${line}` : line;
|
|
648
|
+
if (combined === expected)
|
|
649
|
+
return index + 1;
|
|
650
|
+
if (!expected.startsWith(`${combined} `))
|
|
651
|
+
return -1;
|
|
652
|
+
}
|
|
653
|
+
return -1;
|
|
654
|
+
}
|
|
655
|
+
function lineCanStartText(line) {
|
|
656
|
+
return /^ {2}\S/u.test(line) &&
|
|
657
|
+
!/^ {2}(?:\d+ of \d+|[› ] \d+\.|enter submit|\?|↳)/u.test(line);
|
|
658
|
+
}
|
|
659
|
+
function parseArguments(value) {
|
|
660
|
+
if (typeof value !== "string" || value.length === 0 ||
|
|
661
|
+
Buffer.byteLength(value, "utf8") >
|
|
662
|
+
CODEX_ASYNC_QUESTION_LIMITS.maxArgumentsBytes)
|
|
663
|
+
return undefined;
|
|
664
|
+
let parsed;
|
|
665
|
+
try {
|
|
666
|
+
parsed = JSON.parse(value);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
if (!isRecord(parsed) || Object.keys(parsed).some((key) => key !== "questions")) {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
const questions = parsed.questions;
|
|
675
|
+
if (!validQuestionArray(questions))
|
|
676
|
+
return undefined;
|
|
677
|
+
const result = [];
|
|
678
|
+
for (const value of questions) {
|
|
679
|
+
const question = parseArgumentQuestion(value);
|
|
680
|
+
if (!question)
|
|
681
|
+
return undefined;
|
|
682
|
+
result.push(question);
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
function validQuestionArray(value) {
|
|
687
|
+
return Array.isArray(value) && value.length > 0 &&
|
|
688
|
+
value.length <= CODEX_ASYNC_QUESTION_LIMITS.maxQuestions;
|
|
689
|
+
}
|
|
690
|
+
function parseArgumentQuestion(value) {
|
|
691
|
+
if (!isRecord(value) ||
|
|
692
|
+
Object.keys(value).some((key) => key !== "title" && key !== "options")) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
const title = safeSemanticText(value.title, CODEX_ASYNC_QUESTION_LIMITS.maxTitleCharacters);
|
|
696
|
+
if (!title)
|
|
697
|
+
return undefined;
|
|
698
|
+
if (value.options === undefined || value.options === null)
|
|
699
|
+
return { title };
|
|
700
|
+
const options = parseOptionLabels(value.options);
|
|
701
|
+
return options && options.length > 0 ? { title, options } : undefined;
|
|
702
|
+
}
|
|
703
|
+
function parseOptionLabels(value) {
|
|
704
|
+
if (!Array.isArray(value) ||
|
|
705
|
+
value.length > CODEX_ASYNC_QUESTION_LIMITS.maxOptions) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
const result = [];
|
|
709
|
+
for (const option of value) {
|
|
710
|
+
const label = safeSemanticText(option, CODEX_ASYNC_QUESTION_LIMITS.maxOptionCharacters);
|
|
711
|
+
if (!label)
|
|
712
|
+
return undefined;
|
|
713
|
+
result.push(label);
|
|
714
|
+
}
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
function boundedIdentifier(value) {
|
|
718
|
+
if (typeof value !== "string")
|
|
719
|
+
return undefined;
|
|
720
|
+
const trimmed = value.trim();
|
|
721
|
+
return trimmed.length > 0 &&
|
|
722
|
+
trimmed.length <= CODEX_ASYNC_QUESTION_LIMITS.maxIdentifierCharacters &&
|
|
723
|
+
IDENTIFIER_PATTERN.test(trimmed)
|
|
724
|
+
? trimmed
|
|
725
|
+
: undefined;
|
|
726
|
+
}
|
|
727
|
+
function safeSemanticText(value, maxCharacters) {
|
|
728
|
+
if (typeof value !== "string" || UNSAFE_CONTROL_PATTERN.test(value)) {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
const trimmed = value.trim().normalize("NFC");
|
|
732
|
+
return trimmed.length > 0 && [...trimmed].length <= maxCharacters
|
|
733
|
+
? trimmed
|
|
734
|
+
: undefined;
|
|
735
|
+
}
|
|
736
|
+
function normalizeText(value) {
|
|
737
|
+
return value.normalize("NFC").trim().replace(/\s+/gu, " ");
|
|
738
|
+
}
|
|
739
|
+
function sha256(domain, value) {
|
|
740
|
+
return createHash("sha256")
|
|
741
|
+
.update(`${domain}\0${typeof value === "string" ? value : stableJson(value)}`)
|
|
742
|
+
.digest("hex");
|
|
743
|
+
}
|
|
744
|
+
function stableJson(value) {
|
|
745
|
+
if (Array.isArray(value)) {
|
|
746
|
+
return `[${value.map((entry) => stableJson(entry)).join(",")}]`;
|
|
747
|
+
}
|
|
748
|
+
if (isRecord(value)) {
|
|
749
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
750
|
+
}
|
|
751
|
+
return JSON.stringify(value);
|
|
752
|
+
}
|
|
753
|
+
function isRecord(value) {
|
|
754
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
755
|
+
}
|
|
756
|
+
//# sourceMappingURL=codex-async-question-adapter.js.map
|