@byok-sdk/client 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ import { type LocalTaskJournal } from './journal/journal';
11
11
  import { type JournalOpenFaultSeam } from './journal/sqlite-support';
12
12
  import { LocalStoragePressureEngine, type LocalStoragePolicyInput } from './journal/storage-policy';
13
13
  import { type ResultDocumentExtractor } from './task-runner';
14
- import type { ProgressBatcherOptions } from './progress-batcher';
14
+ import { type ProgressBatcherOptions } from './progress-batcher';
15
15
  /**
16
16
  * Optional white-label product display info — purely opaque passthrough
17
17
  * (never interpreted, validated, or rendered by the daemon itself). Carried
@@ -223,6 +223,14 @@ export interface DaemonConfig {
223
223
  * explicitly instead to opt out of enforcement altogether.
224
224
  */
225
225
  maxTaskOutputBytes?: number;
226
+ /**
227
+ * Host-owned batching policy for normalized `task.progress` events.
228
+ * `maxBatchBytes`, when set, measures exactly the UTF-8 bytes of
229
+ * `JSON.stringify(events)` and must match the deployment's activity-ingress
230
+ * budget. It is deliberately unset by default because that ingress ceiling
231
+ * is deployment policy, not a frozen protocol constant.
232
+ */
233
+ progressBatch?: ProgressBatcherOptions;
226
234
  /**
227
235
  * additive-minor (`task.complete.document`): the seam through which this
228
236
  * product turns a finished task's final output text into the STRUCTURED
@@ -415,7 +423,6 @@ export interface Daemon {
415
423
  /** Internal seam so tests can substitute stub adapters / faster backoff+batch+liveness+long-poll timing. `createDaemonWithAdapters` (which takes this) is also the real entry point for products supplying a hand-built adapter set `createDaemon` can't construct on its own — e.g. custom adapter options, or an adapter that REPLACES a bundled runtime's implementation under the same id. Honest limit: an adapter id outside `pi`/`claude`/`codex` cannot pass wire validation today — `RuntimeIdSchema` (`@byok-sdk/protocol`) is a closed `z.enum(['pi', 'claude', 'codex'])`, and `isRuntimeId` filtering below (see `detectRuntimes`) drops any detected adapter outside that set before it ever reaches a wire-visible field. A genuinely fourth/namespaced runtime id is a future protocol change, not something this seam enables today. */
416
424
  export interface DaemonOverrides {
417
425
  backoff?: BackoffOptions;
418
- batch?: ProgressBatcherOptions;
419
426
  liveness?: LivenessOptions;
420
427
  /** M4 Phase 3: overrides `TaskRunner`'s default out-of-band approval wait (`DEFAULT_APPROVAL_TIMEOUT_MS`, 10 minutes) before an unanswered `requestApproval` force-resolves as a fail-closed rejection. */
421
428
  approvalTimeoutMs?: number;
@@ -5,7 +5,19 @@ export interface ProgressBatcherOptions {
5
5
  maxBatchSize?: number;
6
6
  /** Otherwise flush at most this often (ms) while events are pending. Default 250 (~4/sec). */
7
7
  flushIntervalMs?: number;
8
+ /**
9
+ * Optional deployment-owned ceiling for the UTF-8 bytes in the serialized
10
+ * `events[]` array. Unset means no byte ceiling; hosts should inject the
11
+ * same value their activity ingress enforces.
12
+ */
13
+ maxBatchBytes?: number;
8
14
  }
15
+ export declare class ProgressEventTooLargeError extends Error {
16
+ readonly actualBytes: number;
17
+ readonly maxBatchBytes: number;
18
+ constructor(actualBytes: number, maxBatchBytes: number);
19
+ }
20
+ export declare function validateProgressBatcherOptions(options?: ProgressBatcherOptions): void;
9
21
  /**
10
22
  * Coalesces a task's `AgentEvent`s into seq-ordered `task.progress` batches:
11
23
  * flush immediately at `maxBatchSize` events, otherwise at most every
@@ -19,6 +31,7 @@ export declare class ProgressBatcher {
19
31
  private timer;
20
32
  private readonly maxBatchSize;
21
33
  private readonly flushIntervalMs;
34
+ private readonly maxBatchBytes;
22
35
  constructor(emit: ProgressEmitter, options?: ProgressBatcherOptions);
23
36
  push(event: AgentEvent): void;
24
37
  /** M4 Phase 4 (part B.3, observability): events buffered right now, not yet flushed as a `task.progress` batch — a cheap per-task queue-depth watermark for the daemon's control-socket `status` result (see `task-runner.ts`'s `getQueueWatermarks`). */
@@ -102,6 +102,8 @@ export declare const MAX_TRACKED_TASK_IDS = 2000;
102
102
  export declare const MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
103
103
  /** M5 batch-3 (workstream 2): same contract as {@link MAX_DURATION_EXCEEDED_REASON_PREFIX}, for `DaemonConfig.maxTaskOutputBytes` — see `TaskRunner.pump`'s own per-event byte counting. */
104
104
  export declare const MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
105
+ /** Stable fail-closed reason for one normalized event that cannot fit the configured activity batch budget. */
106
+ export declare const MAX_PROGRESS_BATCH_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: progressBatch.maxBatchBytes";
105
107
  /**
106
108
  * additive-minor (`task.complete.document`): same stable-PREFIX contract as
107
109
  * {@link MAX_DURATION_EXCEEDED_REASON_PREFIX} above, carried by every
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { GitWorkspaceStore } from './daemon/git-workspace-store';
9
9
  export type { GitWorkspaceLedger, GitWorkspaceLedgerRecord, GitWorkspacePhase } from './daemon/git-workspace-store';
10
10
  export { createDaemon, createDaemonWithAdapters } from './daemon/create-daemon';
11
11
  export type { Daemon, DaemonConfig, DaemonStatus, DaemonOverrides, DaemonBranding, HostedJournalConfig, DeviceAssertionConfig } from './daemon/create-daemon';
12
+ export type { ProgressBatcherOptions } from './daemon/progress-batcher';
12
13
  /**
13
14
  * Plan `device-assertion-broker`: the ONLY control-socket capability this
14
15
  * package exposes publicly. `connectControlClient`/`ControlClient` are
package/dist/index.js CHANGED
@@ -878,6 +878,24 @@ function mapPermissionPolicyToPiArgs(policy) {
878
878
  }
879
879
 
880
880
  // src/adapters/pi/events.ts
881
+ function requireToolCallId(msg) {
882
+ if (typeof msg.toolCallId === "string" && msg.toolCallId.trim().length > 0) return msg.toolCallId;
883
+ throw new RuntimeExecutionFailure({
884
+ phase: "run",
885
+ category: "authority",
886
+ retry: "non-retryable",
887
+ reason: `pi ${msg.type} frame had no authoritative tool call id`
888
+ });
889
+ }
890
+ function requireToolResultOutcome(msg) {
891
+ if (typeof msg.isError === "boolean") return msg.isError;
892
+ throw new RuntimeExecutionFailure({
893
+ phase: "run",
894
+ category: "authority",
895
+ retry: "non-retryable",
896
+ reason: "pi tool_execution_end frame had no authoritative isError outcome"
897
+ });
898
+ }
881
899
  function mapPiMessageToAgentEvent(msg) {
882
900
  switch (msg.type) {
883
901
  case "message_update": {
@@ -888,16 +906,22 @@ function mapPiMessageToAgentEvent(msg) {
888
906
  return void 0;
889
907
  }
890
908
  case "tool_execution_start": {
909
+ const toolCallId = requireToolCallId(msg);
891
910
  if (typeof msg.toolName !== "string") return void 0;
892
- return { type: "tool_use", tool: msg.toolName, input: msg.args };
911
+ return { type: "tool_use", tool: msg.toolName, input: msg.args, toolCallId };
893
912
  }
894
913
  case "tool_execution_end": {
914
+ const toolCallId = requireToolCallId(msg);
915
+ const isError = requireToolResultOutcome(msg);
895
916
  if (typeof msg.toolName !== "string") return void 0;
896
- return {
917
+ const event = {
897
918
  type: "tool_result",
898
919
  tool: msg.toolName,
899
- output: { result: msg.result, isError: msg.isError === true }
920
+ output: { result: msg.result },
921
+ toolCallId,
922
+ isError
900
923
  };
924
+ return event;
901
925
  }
902
926
  case "agent_settled":
903
927
  return { type: "turn_end" };
@@ -1909,6 +1933,17 @@ function subtractDenied(tools, denyTools) {
1909
1933
  function createToolUseCorrelation() {
1910
1934
  return { toolNameByUseId: /* @__PURE__ */ new Map() };
1911
1935
  }
1936
+ function missingToolCallIdFailure(frame) {
1937
+ return new RuntimeExecutionFailure({
1938
+ phase: "run",
1939
+ category: "authority",
1940
+ retry: "non-retryable",
1941
+ reason: `claude ${frame} frame had no authoritative tool call id`
1942
+ });
1943
+ }
1944
+ function isAuthoritativeToolCallId(value) {
1945
+ return typeof value === "string" && value.trim().length > 0;
1946
+ }
1912
1947
  var ROUTINE_CLAUDE_SYSTEM_SUBTYPES = /* @__PURE__ */ new Set([
1913
1948
  "init",
1914
1949
  "hook_started",
@@ -1952,9 +1987,12 @@ function mapAssistant(msg, correlation) {
1952
1987
  }
1953
1988
  break;
1954
1989
  case "tool_use":
1955
- if (typeof block.id === "string" && typeof block.name === "string") {
1990
+ if (!isAuthoritativeToolCallId(block.id)) {
1991
+ return { events: [], terminalFailure: missingToolCallIdFailure("tool_use") };
1992
+ }
1993
+ if (typeof block.name === "string") {
1956
1994
  correlation.toolNameByUseId.set(block.id, block.name);
1957
- events.push({ type: "tool_use", tool: block.name, input: block.input });
1995
+ events.push({ type: "tool_use", tool: block.name, input: block.input, toolCallId: block.id });
1958
1996
  }
1959
1997
  break;
1960
1998
  // Deliberately NOT mapped to `progress` — mirrors pi's own choice to
@@ -1990,11 +2028,20 @@ function mapUser(msg, correlation, options) {
1990
2028
  unmappedLabel = unmappedLabel ?? `user-block:${String(block.type)}`;
1991
2029
  continue;
1992
2030
  }
1993
- const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : void 0;
1994
- const tool = toolUseId && correlation.toolNameByUseId.get(toolUseId) || "unknown";
1995
- const isError = block.is_error === true;
1996
- events.push({ type: "tool_result", tool, output: { content: block.content, isError } });
1997
- if (!isError && FILE_WRITING_TOOLS.has(tool)) {
2031
+ if (!isAuthoritativeToolCallId(block.tool_use_id)) {
2032
+ return { events: [], terminalFailure: missingToolCallIdFailure("tool_result") };
2033
+ }
2034
+ const tool = correlation.toolNameByUseId.get(block.tool_use_id) ?? "unknown";
2035
+ const isError = typeof block.is_error === "boolean" ? block.is_error : void 0;
2036
+ const event = {
2037
+ type: "tool_result",
2038
+ tool,
2039
+ output: { content: block.content },
2040
+ toolCallId: block.tool_use_id
2041
+ };
2042
+ if (isError !== void 0) event.isError = isError;
2043
+ events.push(event);
2044
+ if (isError === false && FILE_WRITING_TOOLS.has(tool)) {
1998
2045
  const artifact = tryBuildArtifactEvent(msg, options.workspaceDir);
1999
2046
  if (artifact) events.push(artifact);
2000
2047
  }
@@ -2617,8 +2664,8 @@ var ClaudeSession = class {
2617
2664
  for (; ; ) {
2618
2665
  const buffered = pending.shift();
2619
2666
  if (buffered) return { value: buffered, done: false };
2667
+ if (terminalFailure) throw terminalFailure;
2620
2668
  if (turnSettled) {
2621
- if (terminalFailure) throw terminalFailure;
2622
2669
  return { value: void 0, done: true };
2623
2670
  }
2624
2671
  let raw;
@@ -2820,6 +2867,15 @@ function extractCodexUsageEvent(rawUsage) {
2820
2867
  function toNonNegativeInt2(value) {
2821
2868
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
2822
2869
  }
2870
+ function requireToolCallId2(item) {
2871
+ if (typeof item.id === "string" && item.id.trim().length > 0) return item.id;
2872
+ throw new RuntimeExecutionFailure({
2873
+ phase: "run",
2874
+ category: "authority",
2875
+ retry: "non-retryable",
2876
+ reason: "codex tool item had no authoritative tool call id"
2877
+ });
2878
+ }
2823
2879
  function mapItem(rawItem, phase, workspaceDir) {
2824
2880
  if (!rawItem || typeof rawItem !== "object") return [];
2825
2881
  const item = rawItem;
@@ -2830,9 +2886,10 @@ function mapItem(rawItem, phase, workspaceDir) {
2830
2886
  return typeof item.text === "string" ? [{ type: "progress", text: item.text }] : [];
2831
2887
  }
2832
2888
  case "command_execution": {
2889
+ const toolCallId = requireToolCallId2(item);
2833
2890
  const command = typeof item.command === "string" ? item.command : void 0;
2834
2891
  if (phase === "started") {
2835
- return command !== void 0 ? [{ type: "tool_use", tool: "command_execution", input: { command } }] : [];
2892
+ return command !== void 0 ? [{ type: "tool_use", tool: "command_execution", input: { command }, toolCallId }] : [];
2836
2893
  }
2837
2894
  return [
2838
2895
  {
@@ -2843,17 +2900,19 @@ function mapItem(rawItem, phase, workspaceDir) {
2843
2900
  aggregatedOutput: item.aggregated_output,
2844
2901
  exitCode: item.exit_code,
2845
2902
  status: item.status
2846
- }
2903
+ },
2904
+ toolCallId
2847
2905
  }
2848
2906
  ];
2849
2907
  }
2850
2908
  case "file_change": {
2909
+ const toolCallId = requireToolCallId2(item);
2851
2910
  const changes = Array.isArray(item.changes) ? item.changes : [];
2852
2911
  if (phase === "started") {
2853
- return [{ type: "tool_use", tool: "file_change", input: { changes } }];
2912
+ return [{ type: "tool_use", tool: "file_change", input: { changes }, toolCallId }];
2854
2913
  }
2855
2914
  return [
2856
- { type: "tool_result", tool: "file_change", output: { changes, status: item.status } },
2915
+ { type: "tool_result", tool: "file_change", output: { changes, status: item.status }, toolCallId },
2857
2916
  ...extractArtifactEvents(changes, workspaceDir)
2858
2917
  ];
2859
2918
  }
@@ -3276,7 +3335,15 @@ async function runCodexTurn(params) {
3276
3335
  }
3277
3336
  return;
3278
3337
  }
3279
- const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3338
+ let mapped;
3339
+ try {
3340
+ mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
3341
+ } catch (cause) {
3342
+ if (!isRuntimeExecutionFailure(cause)) throw cause;
3343
+ params.terminal.failure = cause;
3344
+ params.queue.end();
3345
+ return;
3346
+ }
3280
3347
  for (const agentEvent of mapped) {
3281
3348
  if (agentEvent.type === "turn_end") turnEnded = true;
3282
3349
  params.queue.push(agentEvent);
@@ -8121,6 +8188,11 @@ function createStatfsFreeBytesProvider(dir) {
8121
8188
  var BASE_PLATFORM_ALLOWLIST = [
8122
8189
  "PATH",
8123
8190
  "HOME",
8191
+ // macOS credential-store discovery used by subscription-authenticated
8192
+ // agent CLIs depends on the login account name as well as HOME. Omitting
8193
+ // USER makes `claude auth status` report logged out under the filtered
8194
+ // child environment even when the host CLI is logged in.
8195
+ "USER",
8124
8196
  "USERPROFILE",
8125
8197
  "TMPDIR",
8126
8198
  "TEMP",
@@ -8228,11 +8300,37 @@ function computeEffectivePolicy(offered, ceiling) {
8228
8300
  }
8229
8301
 
8230
8302
  // src/daemon/progress-batcher.ts
8303
+ var ProgressEventTooLargeError = class extends Error {
8304
+ constructor(actualBytes, maxBatchBytes) {
8305
+ super(`Progress event requires ${actualBytes} UTF-8 bytes, exceeding maxBatchBytes ${maxBatchBytes}.`);
8306
+ this.actualBytes = actualBytes;
8307
+ this.maxBatchBytes = maxBatchBytes;
8308
+ this.name = "ProgressEventTooLargeError";
8309
+ }
8310
+ actualBytes;
8311
+ maxBatchBytes;
8312
+ };
8313
+ var encoder = new TextEncoder();
8314
+ function assertPositiveSafeInteger(value, name) {
8315
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) {
8316
+ throw new TypeError(`${name} must be a positive safe integer when configured.`);
8317
+ }
8318
+ }
8319
+ function validateProgressBatcherOptions(options = {}) {
8320
+ assertPositiveSafeInteger(options.maxBatchSize, "maxBatchSize");
8321
+ assertPositiveSafeInteger(options.flushIntervalMs, "flushIntervalMs");
8322
+ assertPositiveSafeInteger(options.maxBatchBytes, "maxBatchBytes");
8323
+ }
8324
+ function encodedEventsBytes(events) {
8325
+ return encoder.encode(JSON.stringify(events)).length;
8326
+ }
8231
8327
  var ProgressBatcher = class {
8232
8328
  constructor(emit, options = {}) {
8233
8329
  this.emit = emit;
8330
+ validateProgressBatcherOptions(options);
8234
8331
  this.maxBatchSize = options.maxBatchSize ?? 10;
8235
8332
  this.flushIntervalMs = options.flushIntervalMs ?? 250;
8333
+ this.maxBatchBytes = options.maxBatchBytes;
8236
8334
  }
8237
8335
  emit;
8238
8336
  buffer = [];
@@ -8240,7 +8338,17 @@ var ProgressBatcher = class {
8240
8338
  timer;
8241
8339
  maxBatchSize;
8242
8340
  flushIntervalMs;
8341
+ maxBatchBytes;
8243
8342
  push(event) {
8343
+ if (this.maxBatchBytes !== void 0) {
8344
+ const eventBytes = encodedEventsBytes([event]);
8345
+ if (eventBytes > this.maxBatchBytes) {
8346
+ throw new ProgressEventTooLargeError(eventBytes, this.maxBatchBytes);
8347
+ }
8348
+ if (this.buffer.length > 0 && encodedEventsBytes([...this.buffer, event]) > this.maxBatchBytes) {
8349
+ this.flush();
8350
+ }
8351
+ }
8244
8352
  this.buffer.push(event);
8245
8353
  if (this.buffer.length >= this.maxBatchSize) {
8246
8354
  this.flush();
@@ -8293,6 +8401,7 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
8293
8401
  var MAX_TRACKED_TASK_IDS = 2e3;
8294
8402
  var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
8295
8403
  var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
8404
+ var MAX_PROGRESS_BATCH_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: progressBatch.maxBatchBytes";
8296
8405
  var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
8297
8406
  function resultDocumentRejectionDetail(check) {
8298
8407
  switch (check.reason) {
@@ -9149,6 +9258,13 @@ var TaskRunner = class {
9149
9258
  } catch (err) {
9150
9259
  if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
9151
9260
  active.batcher.flush();
9261
+ if (err instanceof ProgressEventTooLargeError) {
9262
+ await this.failActiveTaskForResourceLimit(
9263
+ active.taskId,
9264
+ `${MAX_PROGRESS_BATCH_BYTES_EXCEEDED_REASON_PREFIX}: event requires ${err.actualBytes} UTF-8 bytes, exceeding the configured limit of ${err.maxBatchBytes} bytes`
9265
+ );
9266
+ return;
9267
+ }
9152
9268
  const failure = projectRuntimeBoundaryFailure(err, "run");
9153
9269
  if (failure.contractViolation) {
9154
9270
  console.error("[byok/client] runtime adapter events iterable returned an untyped failure", err);
@@ -10235,6 +10351,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10235
10351
  `DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
10236
10352
  );
10237
10353
  }
10354
+ validateProgressBatcherOptions(config.progressBatch);
10238
10355
  const presenceCadence = {
10239
10356
  intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
10240
10357
  ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
@@ -10520,7 +10637,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10520
10637
  // untouched. See `observer.ts`'s module doc comment.
10521
10638
  send: sendEnvelope,
10522
10639
  blobClient,
10523
- batcherOptions: overrides.batch,
10640
+ batcherOptions: config.progressBatch,
10524
10641
  sessionWorkspaces,
10525
10642
  gitWorkspaceManager,
10526
10643
  gitWorkspaceStore,