@zq-silk/yui 0.15.3 → 0.15.6
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/dist/agent/managedRuntimeEnvironment.js +3 -0
- package/dist/cli.js +35 -127
- package/dist/commands/executionAuditCommands.js +6 -0
- package/dist/commands/taskContextCommand.js +4 -2
- package/dist/context/sessionBootstrapManifest.js +12 -21
- package/dist/controller/clientRuntime.js +1 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +270 -162
- package/dist/controller/runtimeEventInbox.js +8 -0
- package/dist/controller/runtimeEventProcessor.js +31 -4
- package/dist/controller/runtimeHookTurnFence.js +101 -62
- package/dist/controller/runtimeLaunchCoordinator.js +38 -12
- package/dist/controller/runtimeObservationHook.js +17 -1
- package/dist/controller/structuredProviderObservation.js +39 -27
- package/dist/core/controllerClient.js +5 -0
- package/dist/core/controllerServer.js +7 -4
- package/dist/domain/agentResultTransport.js +2 -2
- package/dist/executor/agentExecutor.js +22 -38
- package/dist/executor/executorRegistry.js +16 -5
- package/dist/executor/fileRoleLaunchPlanner.js +22 -28
- package/dist/lifecycle/exactTurnTerminalization.js +3 -3
- package/dist/observability/executionAudit.js +12 -0
- package/dist/repository/executionLaneGitSnapshot.js +4 -3
- package/dist/repository/taskWorkspacePreparer.js +12 -4
- package/dist/review/taskFinalReviewContract.js +13 -32
- package/dist/runtime/agentError.js +299 -12
- package/dist/runtime/agentHost.js +419 -41
- package/dist/runtime/builtinAgentDrivers.js +5 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +16 -2
- package/dist/runtime/providerRuntimeIdentity.js +34 -28
- package/dist/runtime/runtimeCoherence.js +91 -0
- package/dist/runtime/runtimeObservation.js +8 -5
- package/dist/runtime/structuredProviderHost.js +53 -44
- package/dist/runtime/tmuxAdapters.js +72 -43
- package/dist/scheduler/activeRoleTurnDelivery.js +59 -11
- package/dist/scheduler/leaderWakeupProcessor.js +61 -7
- package/dist/storage/sqliteSchema.js +9 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/turn/turn.js +7 -1
- package/package.json +1 -1
- package/dist/runtime/exactControlPlane.js +0 -232
|
@@ -31,8 +31,11 @@ export function standardAgentError(input) {
|
|
|
31
31
|
phase: input.phase,
|
|
32
32
|
category: classification.category,
|
|
33
33
|
code: requiredErrorText(classification.code, "Agent error code"),
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Both readable fields pass the same redaction boundary. `message` is
|
|
35
|
+
// often a Provider string interpolated by a caller, so it can carry a
|
|
36
|
+
// credential even when `raw` is already clean.
|
|
37
|
+
message: redactAgentErrorText(requiredErrorText(input.message, "Agent error message")),
|
|
38
|
+
raw: redactAgentErrorText(requiredErrorText(input.raw, "Agent error raw payload")),
|
|
36
39
|
inputDisposition: input.inputDisposition
|
|
37
40
|
?? classification.inputDisposition
|
|
38
41
|
?? "unknown",
|
|
@@ -46,6 +49,117 @@ export const UNKNOWN_AGENT_ERROR_CLASSIFICATION = Object.freeze({
|
|
|
46
49
|
category: "unknown",
|
|
47
50
|
code: "unknown"
|
|
48
51
|
});
|
|
52
|
+
export function providerDeliveryFailure(input) {
|
|
53
|
+
const raw = input.raw === undefined ? undefined : serializeAgentErrorRaw(input.raw);
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
...definedDeliveryFields(input),
|
|
56
|
+
detail: boundDisplayText(redactAgentErrorText(requiredErrorText(input.detail, "Provider delivery failure detail"))),
|
|
57
|
+
...(raw === undefined ? {} : { raw }),
|
|
58
|
+
phase: input.phase,
|
|
59
|
+
inputDisposition: input.inputDisposition
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Forward facts owned by the failing operation without parsing its prose. */
|
|
63
|
+
export function providerDeliveryFailureFacts(failure) {
|
|
64
|
+
if (failure === undefined)
|
|
65
|
+
return {};
|
|
66
|
+
return {
|
|
67
|
+
...(failure.sessionDisposition === undefined ? {} : { sessionDisposition: failure.sessionDisposition }),
|
|
68
|
+
...(failure.registrationDisposition === undefined ? {} : { registrationDisposition: failure.registrationDisposition }),
|
|
69
|
+
...(failure.errorName === undefined ? {} : { errorName: failure.errorName }),
|
|
70
|
+
...(failure.causeName === undefined ? {} : { causeName: failure.causeName }),
|
|
71
|
+
...(failure.hostState === undefined ? {} : { hostState: failure.hostState }),
|
|
72
|
+
...(failure.expectedRuntimeGenerationId === undefined ? {} : { expectedRuntimeGenerationId: failure.expectedRuntimeGenerationId }),
|
|
73
|
+
...(failure.observedRuntimeGenerationId === undefined ? {} : { observedRuntimeGenerationId: failure.observedRuntimeGenerationId }),
|
|
74
|
+
...(failure.attemptId === undefined ? {} : { attemptId: failure.attemptId })
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Builds a delivery failure from a thrown error, keeping the structured cause.
|
|
79
|
+
*
|
|
80
|
+
* Call sites used to interpolate `error.message` into a sentence, which
|
|
81
|
+
* discarded the class name and the whole `cause` chain. Here `detail` stays the
|
|
82
|
+
* readable projection while `raw`/`errorName`/`causeName` carry the structure.
|
|
83
|
+
*/
|
|
84
|
+
export function providerDeliveryFailureFrom(error, input) {
|
|
85
|
+
const causeName = innermostCauseName(error);
|
|
86
|
+
return providerDeliveryFailure({
|
|
87
|
+
...input,
|
|
88
|
+
detail: input.detail ?? errorMessageText(error),
|
|
89
|
+
raw: serializeAgentErrorRaw(error),
|
|
90
|
+
...(error instanceof Error ? { errorName: error.name } : {}),
|
|
91
|
+
...(causeName === undefined ? {} : { causeName })
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Class name of the innermost `cause`, which is usually the real reason a
|
|
96
|
+
* wrapped failure occurred. Bounded: a malformed chain must not spin here.
|
|
97
|
+
*/
|
|
98
|
+
export function innermostCauseName(error) {
|
|
99
|
+
let current = error instanceof Error ? error.cause : undefined;
|
|
100
|
+
let name;
|
|
101
|
+
for (let depth = 0; depth < 16 && current instanceof Error; depth += 1) {
|
|
102
|
+
name = current.name;
|
|
103
|
+
current = current.cause;
|
|
104
|
+
}
|
|
105
|
+
return name;
|
|
106
|
+
}
|
|
107
|
+
function errorMessageText(error) {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
return error.message.trim().length > 0 ? error.message : error.name;
|
|
110
|
+
}
|
|
111
|
+
return String(error ?? "unknown error");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Renders a delivery failure as one readable line for a Turn summary. The
|
|
115
|
+
* complete record stays available on `runtime.agent-error`; this is the
|
|
116
|
+
* bounded projection, never a replacement for the original cause.
|
|
117
|
+
*/
|
|
118
|
+
export function formatProviderDeliveryFailure(failure) {
|
|
119
|
+
const fields = [
|
|
120
|
+
`phase=${failure.phase}`,
|
|
121
|
+
`inputDisposition=${failure.inputDisposition}`
|
|
122
|
+
];
|
|
123
|
+
if (failure.hostState !== undefined)
|
|
124
|
+
fields.push(`hostState=${failure.hostState}`);
|
|
125
|
+
if (failure.errorName !== undefined)
|
|
126
|
+
fields.push(`error=${failure.errorName}`);
|
|
127
|
+
if (failure.causeName !== undefined)
|
|
128
|
+
fields.push(`cause=${failure.causeName}`);
|
|
129
|
+
if (failure.expectedRuntimeGenerationId !== undefined) {
|
|
130
|
+
fields.push(`expectedGeneration=${failure.expectedRuntimeGenerationId}`);
|
|
131
|
+
}
|
|
132
|
+
if (failure.observedRuntimeGenerationId !== undefined) {
|
|
133
|
+
fields.push(`observedGeneration=${failure.observedRuntimeGenerationId}`);
|
|
134
|
+
}
|
|
135
|
+
if (failure.attemptId !== undefined)
|
|
136
|
+
fields.push(`attemptId=${failure.attemptId}`);
|
|
137
|
+
if (failure.registrationDisposition !== undefined) {
|
|
138
|
+
fields.push(`registration=${failure.registrationDisposition}`);
|
|
139
|
+
}
|
|
140
|
+
if (failure.sessionDisposition !== undefined) {
|
|
141
|
+
fields.push(`sessionDisposition=${failure.sessionDisposition}`);
|
|
142
|
+
}
|
|
143
|
+
// A clipped projection must say where the complete cause is, or a reader
|
|
144
|
+
// takes the visible fragment for the whole reason.
|
|
145
|
+
if (isTruncatedText(failure.detail))
|
|
146
|
+
fields.push("detailTruncated=see-raw");
|
|
147
|
+
return `${failure.detail} (${fields.join(" ")})`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Drops absent fields and passes every retained readable one through the same
|
|
151
|
+
* redaction and display bound as `detail`. These fields carry a class name or
|
|
152
|
+
* an id today, but they are persisted and publicly readable, so they must not
|
|
153
|
+
* be the one path that skips the boundary.
|
|
154
|
+
*/
|
|
155
|
+
function definedDeliveryFields(value) {
|
|
156
|
+
return Object.fromEntries(Object.entries(value)
|
|
157
|
+
.filter(([, member]) => member !== undefined)
|
|
158
|
+
.map(([key, member]) => [
|
|
159
|
+
key,
|
|
160
|
+
typeof member === "string" ? boundDisplayText(redactAgentErrorText(member)) : member
|
|
161
|
+
]));
|
|
162
|
+
}
|
|
49
163
|
export function isAgentErrorCategory(value) {
|
|
50
164
|
return typeof value === "string"
|
|
51
165
|
&& AGENT_ERROR_CATEGORIES.includes(value);
|
|
@@ -73,14 +187,57 @@ export function isStandardAgentError(value) {
|
|
|
73
187
|
&& (error.retryAfterMs === undefined
|
|
74
188
|
|| (Number.isSafeInteger(error.retryAfterMs) && error.retryAfterMs >= 0));
|
|
75
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Upper bound for one persisted raw payload. A Provider stack trace or a
|
|
192
|
+
* transport dump stays readable well below this; anything larger keeps its
|
|
193
|
+
* head and its tail with an explicit marker between them.
|
|
194
|
+
*/
|
|
195
|
+
const MAX_RAW_CHARS = 16_000;
|
|
196
|
+
/**
|
|
197
|
+
* Bound for a one-line readable projection such as `detail` or a summary. The
|
|
198
|
+
* complete redacted chain lives in `raw`, so clipping here loses nothing.
|
|
199
|
+
*/
|
|
200
|
+
const MAX_DISPLAY_CHARS = 2_000;
|
|
201
|
+
/**
|
|
202
|
+
* Bound for one field inside a serialized payload. Sized so a deep chain of
|
|
203
|
+
* wrapped errors still fits under `MAX_RAW_CHARS` with its causes intact.
|
|
204
|
+
*/
|
|
205
|
+
const MAX_FIELD_CHARS = 2_000;
|
|
206
|
+
const TRUNCATION_MARKER = "…[truncated";
|
|
207
|
+
/**
|
|
208
|
+
* Serializes any failure into one bounded, secret-redacted payload.
|
|
209
|
+
*
|
|
210
|
+
* `raw` is the authoritative cause and is persisted verbatim on
|
|
211
|
+
* `runtime.agent-error`, so it is the last boundary before a Provider
|
|
212
|
+
* credential could reach durable storage or a public read. Redaction happens
|
|
213
|
+
* here rather than at each call site: an unredacted path added later would
|
|
214
|
+
* otherwise silently leak. `cause` chains and non-enumerable Error fields are
|
|
215
|
+
* retained because they usually carry the real reason.
|
|
216
|
+
*/
|
|
76
217
|
export function serializeAgentErrorRaw(value) {
|
|
218
|
+
return boundRawPayload(redactAgentErrorText(rawPayloadText(value)));
|
|
219
|
+
}
|
|
220
|
+
function rawPayloadText(value) {
|
|
77
221
|
if (typeof value === "string")
|
|
78
|
-
return
|
|
222
|
+
return value;
|
|
79
223
|
if (value === undefined)
|
|
80
224
|
return "Agent operation failed without an error payload.";
|
|
81
225
|
try {
|
|
82
226
|
const seen = new WeakSet();
|
|
83
|
-
const serialized = JSON.stringify(value, (
|
|
227
|
+
const serialized = JSON.stringify(value, (key, member) => {
|
|
228
|
+
// Turn input is the Agent's prompt: Task context, quoted files, whatever
|
|
229
|
+
// the Role was told. It is not a failure fact, and it reaches the same
|
|
230
|
+
// durable, publicly-readable record as the rest of this payload. A
|
|
231
|
+
// transport client that echoes its own request into the thrown error
|
|
232
|
+
// carries it here without any call site intending to persist it.
|
|
233
|
+
if (PROVIDER_INPUT_KEYS.has(key))
|
|
234
|
+
return "[PROVIDER-INPUT-OMITTED]";
|
|
235
|
+
// Each field is clipped on its own so no single oversized message or
|
|
236
|
+
// stack can push the rest of the chain past the payload bound. Clipping
|
|
237
|
+
// the whole payload instead lost the innermost `cause`, which is the one
|
|
238
|
+
// field that usually states the real reason.
|
|
239
|
+
if (typeof member === "string")
|
|
240
|
+
return boundFieldText(member);
|
|
84
241
|
if (typeof member === "bigint")
|
|
85
242
|
return member.toString();
|
|
86
243
|
if (member !== null && typeof member === "object") {
|
|
@@ -88,21 +245,151 @@ export function serializeAgentErrorRaw(value) {
|
|
|
88
245
|
return "[Circular]";
|
|
89
246
|
seen.add(member);
|
|
90
247
|
if (member instanceof Error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
248
|
+
// Error's own fields are non-enumerable, and `cause` is where a
|
|
249
|
+
// wrapped transport/controller failure keeps its real reason.
|
|
250
|
+
// `stack` is emitted last so a clip at any level takes the trace
|
|
251
|
+
// before it takes the identifying fields or the nested cause.
|
|
252
|
+
const own = Object.getOwnPropertyNames(member)
|
|
253
|
+
.filter((name) => name !== "stack");
|
|
254
|
+
return Object.fromEntries([
|
|
255
|
+
...own.map((name) => [
|
|
256
|
+
name,
|
|
257
|
+
member[name]
|
|
258
|
+
]),
|
|
259
|
+
...(typeof member.stack === "string" ? [["stack", member.stack]] : [])
|
|
260
|
+
]);
|
|
95
261
|
}
|
|
96
262
|
}
|
|
97
263
|
return member;
|
|
98
264
|
});
|
|
99
|
-
return serialized === undefined
|
|
100
|
-
? String(value)
|
|
101
|
-
: requiredErrorText(serialized, "Agent error raw payload");
|
|
265
|
+
return serialized === undefined ? String(value) : serialized;
|
|
102
266
|
}
|
|
103
267
|
catch {
|
|
104
|
-
return
|
|
268
|
+
return String(value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Payload keys that carry Provider input rather than a failure fact. `text`
|
|
273
|
+
* and `body` are the shapes an HTTP or SDK client uses when it attaches the
|
|
274
|
+
* failed request to its error.
|
|
275
|
+
*/
|
|
276
|
+
const PROVIDER_INPUT_KEYS = new Set([
|
|
277
|
+
"boundedText",
|
|
278
|
+
"text",
|
|
279
|
+
"prompt",
|
|
280
|
+
"input",
|
|
281
|
+
"body",
|
|
282
|
+
"content",
|
|
283
|
+
"messages"
|
|
284
|
+
]);
|
|
285
|
+
const SECRET_LABEL = "api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password|passwd|cookie|authorization";
|
|
286
|
+
const SECRET_ASSIGNMENT_START = new RegExp(`(${SECRET_LABEL})(\\\\*["']?\\s*[=:]\\s*)`, "gi");
|
|
287
|
+
/**
|
|
288
|
+
* `Authorization: Basic <base64>` and friends. The generic assignment pattern
|
|
289
|
+
* consumed only the scheme word and left the credential itself readable.
|
|
290
|
+
*/
|
|
291
|
+
const AUTH_SCHEME_PATTERN = /((?:authorization|proxy-authorization)(?:\\?["']?\s*[=:]\s*\\?["']?)\s*)(basic|bearer|token|digest|negotiate)(\s+)[A-Za-z0-9._~+/=-]{4,}/gi;
|
|
292
|
+
// Word boundary keeps "task-5-…" workspace paths from being mistaken for keys.
|
|
293
|
+
const PROVIDER_KEY_PATTERN = /\b(?:(?:sk|pat)-|(?:ghp|gho|ghs|github_pat)_)[A-Za-z0-9_-]{6,}/gu;
|
|
294
|
+
// Digest credentials are an entire parameter list, not a single scheme token.
|
|
295
|
+
// Cover raw headers and escaped headers embedded in JSON error strings without
|
|
296
|
+
// consuming the enclosing serialized error and its subsequent cause fields.
|
|
297
|
+
const DIGEST_AUTH_PATTERN = /((?:proxy-)?authorization(?:\\?["']?\s*[=:]\s*\\?["']?)\s*digest\s+)(?:[\w-]+\s*=\s*(?:\\"(?:[^"\\]|\\(?!"))*\\"|"(?:\\.|[^"\\])*"|'[^']*'|[^,\s"}]+)\s*(?:,\s*)?)+/gi;
|
|
298
|
+
const BEARER_PATTERN = /\b(bearer\s+)[A-Za-z0-9._~+/-]{8,}=*/gi;
|
|
299
|
+
/**
|
|
300
|
+
* Redacts credential-shaped text from a failure payload. This mirrors the
|
|
301
|
+
* launch-diagnostic redaction but is applied to the Agent error chain, whose
|
|
302
|
+
* payloads are persisted and publicly readable through the Task event.
|
|
303
|
+
*/
|
|
304
|
+
export function redactAgentErrorText(value) {
|
|
305
|
+
return redactSecretAssignments(value
|
|
306
|
+
.replace(PROVIDER_KEY_PATTERN, "[REDACTED]")
|
|
307
|
+
.replace(DIGEST_AUTH_PATTERN, "$1 [REDACTED]")
|
|
308
|
+
.replace(AUTH_SCHEME_PATTERN, "$1$2$3[REDACTED]")
|
|
309
|
+
.replace(BEARER_PATTERN, "$1[REDACTED]"));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* A quoted diagnostic may itself be inside a JSON string. Its delimiter is
|
|
313
|
+
* then `\"`, not `"`, and a generic backslash escape regex can eat the closing
|
|
314
|
+
* delimiter and the rest of the cause. Match the opening escape depth, retain
|
|
315
|
+
* both delimiters, and replace only their secret value. This is also stable
|
|
316
|
+
* when already redacted raw crosses another error boundary.
|
|
317
|
+
*/
|
|
318
|
+
function redactSecretAssignments(value) {
|
|
319
|
+
let cursor = 0;
|
|
320
|
+
let redacted = "";
|
|
321
|
+
for (const match of value.matchAll(SECRET_ASSIGNMENT_START)) {
|
|
322
|
+
if (match.index < cursor)
|
|
323
|
+
continue;
|
|
324
|
+
const start = match.index + match[0].length;
|
|
325
|
+
if (value.startsWith("[REDACTED]", start))
|
|
326
|
+
continue;
|
|
327
|
+
const opening = /^(\\*)(["'])/u.exec(value.slice(start));
|
|
328
|
+
let end = start;
|
|
329
|
+
let replacement = "[REDACTED]";
|
|
330
|
+
if (opening !== null) {
|
|
331
|
+
const delimiter = opening[0];
|
|
332
|
+
const quote = opening[2];
|
|
333
|
+
const escapeDepth = opening[1].length;
|
|
334
|
+
let backslashes = 0;
|
|
335
|
+
end += delimiter.length;
|
|
336
|
+
for (; end < value.length; end += 1) {
|
|
337
|
+
const char = value[end];
|
|
338
|
+
if (char === "\\") {
|
|
339
|
+
backslashes += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (char === quote && backslashes % (2 * (escapeDepth + 1)) === escapeDepth)
|
|
343
|
+
break;
|
|
344
|
+
backslashes = 0;
|
|
345
|
+
}
|
|
346
|
+
replacement = `${delimiter}[REDACTED]${end < value.length ? delimiter : ""}`;
|
|
347
|
+
if (end < value.length)
|
|
348
|
+
end += 1;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// No explicit quote means spaces may be part of the credential.
|
|
352
|
+
while (end < value.length && !/[,;"'\\\n\r}\]]/u.test(value[end]))
|
|
353
|
+
end += 1;
|
|
354
|
+
}
|
|
355
|
+
if (end === start)
|
|
356
|
+
continue;
|
|
357
|
+
redacted += value.slice(cursor, start) + replacement;
|
|
358
|
+
cursor = end;
|
|
105
359
|
}
|
|
360
|
+
return redacted + value.slice(cursor);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Clips a persisted payload while keeping both ends.
|
|
364
|
+
*
|
|
365
|
+
* Per-field clipping already keeps the cause chain within bounds, so this only
|
|
366
|
+
* guards a pathologically wide payload. Keeping a tail slice means a reader
|
|
367
|
+
* still sees how the record ends rather than only its head.
|
|
368
|
+
*/
|
|
369
|
+
function boundRawPayload(value) {
|
|
370
|
+
const text = isErrorText(value) ? value : "Agent operation failed without a readable error payload.";
|
|
371
|
+
if (text.length <= MAX_RAW_CHARS)
|
|
372
|
+
return text;
|
|
373
|
+
const headChars = Math.floor(MAX_RAW_CHARS * 0.75);
|
|
374
|
+
const tailChars = MAX_RAW_CHARS - headChars;
|
|
375
|
+
return `${text.slice(0, headChars)}${TRUNCATION_MARKER} ${text.length - MAX_RAW_CHARS} chars of ${text.length}]${text.slice(text.length - tailChars)}`;
|
|
376
|
+
}
|
|
377
|
+
function boundFieldText(value) {
|
|
378
|
+
if (value.length <= MAX_FIELD_CHARS)
|
|
379
|
+
return value;
|
|
380
|
+
return `${value.slice(0, MAX_FIELD_CHARS)}${TRUNCATION_MARKER} ${value.length - MAX_FIELD_CHARS} chars of ${value.length}]`;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Clips a one-line projection. Unlike `raw` this may lose content, so the
|
|
384
|
+
* marker is the reader's signal to consult the complete payload.
|
|
385
|
+
*/
|
|
386
|
+
function boundDisplayText(value) {
|
|
387
|
+
if (value.length <= MAX_DISPLAY_CHARS)
|
|
388
|
+
return value;
|
|
389
|
+
return `${value.slice(0, MAX_DISPLAY_CHARS)}${TRUNCATION_MARKER} ${value.length - MAX_DISPLAY_CHARS} chars of ${value.length}]`;
|
|
390
|
+
}
|
|
391
|
+
export function isTruncatedText(value) {
|
|
392
|
+
return value.includes(TRUNCATION_MARKER);
|
|
106
393
|
}
|
|
107
394
|
function isErrorText(value) {
|
|
108
395
|
return typeof value === "string" && value.trim().length > 0 && !value.includes("\0");
|