@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.
Files changed (41) hide show
  1. package/dist/agent/managedRuntimeEnvironment.js +3 -0
  2. package/dist/cli.js +35 -127
  3. package/dist/commands/executionAuditCommands.js +6 -0
  4. package/dist/commands/taskContextCommand.js +4 -2
  5. package/dist/context/sessionBootstrapManifest.js +12 -21
  6. package/dist/controller/clientRuntime.js +1 -1
  7. package/dist/controller/fileSchedulerStoreAdapter.js +270 -162
  8. package/dist/controller/runtimeEventInbox.js +8 -0
  9. package/dist/controller/runtimeEventProcessor.js +31 -4
  10. package/dist/controller/runtimeHookTurnFence.js +101 -62
  11. package/dist/controller/runtimeLaunchCoordinator.js +38 -12
  12. package/dist/controller/runtimeObservationHook.js +17 -1
  13. package/dist/controller/structuredProviderObservation.js +39 -27
  14. package/dist/core/controllerClient.js +5 -0
  15. package/dist/core/controllerServer.js +7 -4
  16. package/dist/domain/agentResultTransport.js +2 -2
  17. package/dist/executor/agentExecutor.js +22 -38
  18. package/dist/executor/executorRegistry.js +16 -5
  19. package/dist/executor/fileRoleLaunchPlanner.js +22 -28
  20. package/dist/lifecycle/exactTurnTerminalization.js +3 -3
  21. package/dist/observability/executionAudit.js +12 -0
  22. package/dist/repository/executionLaneGitSnapshot.js +4 -3
  23. package/dist/repository/taskWorkspacePreparer.js +12 -4
  24. package/dist/review/taskFinalReviewContract.js +13 -32
  25. package/dist/runtime/agentError.js +299 -12
  26. package/dist/runtime/agentHost.js +419 -41
  27. package/dist/runtime/builtinAgentDrivers.js +5 -0
  28. package/dist/runtime/index.js +1 -1
  29. package/dist/runtime/ports.js +16 -2
  30. package/dist/runtime/providerRuntimeIdentity.js +34 -28
  31. package/dist/runtime/runtimeCoherence.js +91 -0
  32. package/dist/runtime/runtimeObservation.js +8 -5
  33. package/dist/runtime/structuredProviderHost.js +53 -44
  34. package/dist/runtime/tmuxAdapters.js +72 -43
  35. package/dist/scheduler/activeRoleTurnDelivery.js +59 -11
  36. package/dist/scheduler/leaderWakeupProcessor.js +61 -7
  37. package/dist/storage/sqliteSchema.js +9 -0
  38. package/dist/storage/storageVersions.js +1 -1
  39. package/dist/turn/turn.js +7 -1
  40. package/package.json +1 -1
  41. 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
- message: requiredErrorText(input.message, "Agent error message"),
35
- raw: requiredErrorText(input.raw, "Agent error raw payload"),
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 requiredErrorText(value, "Agent error raw payload");
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, (_key, member) => {
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
- return Object.fromEntries(Object.getOwnPropertyNames(member).map((name) => [
92
- name,
93
- member[name]
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 requiredErrorText(String(value), "Agent error raw payload");
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");