@byok-sdk/client 0.4.2 → 0.6.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.
package/README.md CHANGED
@@ -63,6 +63,24 @@ validated registry. Only those logical IDs are advertised in `conn.hello`
63
63
  and hosted presence; command, args, environment, headers, and credentials
64
64
  remain local.
65
65
 
66
+ Hosted deployments that enforce an activity-ingress byte ceiling should inject
67
+ the same ceiling into the daemon. The byte count is the UTF-8 length of
68
+ `JSON.stringify(events)`; it does not include envelope or transport overhead.
69
+ One event that cannot fit fails the task locally without truncation or network
70
+ delivery.
71
+
72
+ ```ts
73
+ createDaemon({
74
+ // ...normal device and transport configuration
75
+ progressBatch: {
76
+ maxBatchBytes: 64 * 1024,
77
+ },
78
+ });
79
+ ```
80
+
81
+ The value is intentionally host-owned and has no SDK default because it is a
82
+ deployment/read-model policy, not a frozen protocol limit.
83
+
66
84
  For a concrete private host composition, see the
67
85
  [`examples/salesko-connector-broker`](../../examples/salesko-connector-broker)
68
86
  reference. It keeps `@byok-sdk/client` credential-blind while combining
@@ -5,7 +5,7 @@ import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, con
5
5
  import path20, { isAbsolute, join } from 'path';
6
6
  import os from 'os';
7
7
  import { DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
8
- import { TASK_STATES, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
8
+ import { TASK_STATES, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
9
9
  import { promisify } from 'util';
10
10
  import { fileURLToPath } from 'url';
11
11
  import 'readline';
@@ -77,6 +77,28 @@ var SteerUnsupportedError = class extends Error {
77
77
  }
78
78
  };
79
79
 
80
+ // src/release-identity.ts
81
+ var LOCAL_AGENT_RELEASE_VERSION_MAX_LENGTH = 128;
82
+ var LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH = 128;
83
+ var STRICT_SEMVER_PATTERN = new RegExp(
84
+ "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$"
85
+ );
86
+ var BUILD_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
87
+ function resolveLocalAgentReleaseIdentity(input) {
88
+ if (input === void 0 || typeof input.version !== "string" || input.version.length > LOCAL_AGENT_RELEASE_VERSION_MAX_LENGTH || !STRICT_SEMVER_PATTERN.test(input.version)) {
89
+ throw new Error("DaemonConfig.localAgentRelease.version must be canonical strict SemVer");
90
+ }
91
+ if (input.buildId !== void 0 && (typeof input.buildId !== "string" || input.buildId.length > LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH || !BUILD_ID_PATTERN.test(input.buildId))) {
92
+ throw new Error(
93
+ `DaemonConfig.localAgentRelease.buildId must be 1-${LOCAL_AGENT_RELEASE_BUILD_ID_MAX_LENGTH} safe opaque characters`
94
+ );
95
+ }
96
+ return Object.freeze({
97
+ version: input.version,
98
+ ...input.buildId === void 0 ? {} : { buildId: input.buildId }
99
+ });
100
+ }
101
+
80
102
  // src/runtime-failure.ts
81
103
  var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
82
104
  var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
@@ -4419,7 +4441,20 @@ var PresencePublisher = class {
4419
4441
  headers: { "content-type": "application/json" },
4420
4442
  body: JSON.stringify({
4421
4443
  level: "online",
4422
- ...this.opts.configuredToolsets === void 0 ? {} : { configuredToolsets: this.opts.configuredToolsets }
4444
+ ...this.opts.configuredToolsets === void 0 ? {} : { configuredToolsets: this.opts.configuredToolsets },
4445
+ ...this.opts.clientVersion === void 0 ? {} : { clientVersion: this.opts.clientVersion },
4446
+ ...this.opts.protocolVersions === void 0 ? {} : { protocolVersions: [...this.opts.protocolVersions] },
4447
+ // Presence is a lightweight readiness fact, not a second
4448
+ // capability-negotiation channel. Keep its projection identical
4449
+ // to the persisted presence contract: the conn.hello snapshot's
4450
+ // runtime identity/version/auth facts, with no capabilities blob.
4451
+ ...this.opts.runtimes === void 0 ? {} : {
4452
+ runtimes: this.opts.runtimes.map(({ id, version, authPresent }) => ({
4453
+ id,
4454
+ ...version === void 0 ? {} : { version },
4455
+ ...authPresent === void 0 ? {} : { authPresent }
4456
+ }))
4457
+ }
4423
4458
  })
4424
4459
  },
4425
4460
  this.opts.auth
@@ -5145,6 +5180,7 @@ var WsTransport = class {
5145
5180
  capabilities: this.opts.capabilities,
5146
5181
  deviceId: this.opts.deviceId,
5147
5182
  productId: this.opts.productId,
5183
+ clientVersion: this.opts.clientVersion,
5148
5184
  runtimes: this.opts.runtimes,
5149
5185
  configuredToolsets: this.opts.configuredToolsets === void 0 ? void 0 : [...this.opts.configuredToolsets],
5150
5186
  cursor: this.opts.getCursor?.()
@@ -5235,6 +5271,7 @@ var ConnectionManager = class {
5235
5271
  deviceId: opts.deviceId,
5236
5272
  productId: opts.productId,
5237
5273
  capabilities: opts.capabilities,
5274
+ clientVersion: opts.clientVersion,
5238
5275
  runtimes: opts.runtimes,
5239
5276
  configuredToolsets: opts.configuredToolsets,
5240
5277
  getCursor: () => this.cursor,
@@ -8275,6 +8312,11 @@ function createStatfsFreeBytesProvider(dir) {
8275
8312
  var BASE_PLATFORM_ALLOWLIST = [
8276
8313
  "PATH",
8277
8314
  "HOME",
8315
+ // macOS credential-store discovery used by subscription-authenticated
8316
+ // agent CLIs depends on the login account name as well as HOME. Omitting
8317
+ // USER makes `claude auth status` report logged out under the filtered
8318
+ // child environment even when the host CLI is logged in.
8319
+ "USER",
8278
8320
  "USERPROFILE",
8279
8321
  "TMPDIR",
8280
8322
  "TEMP",
@@ -8382,11 +8424,37 @@ function computeEffectivePolicy(offered, ceiling) {
8382
8424
  }
8383
8425
 
8384
8426
  // src/daemon/progress-batcher.ts
8427
+ var ProgressEventTooLargeError = class extends Error {
8428
+ constructor(actualBytes, maxBatchBytes) {
8429
+ super(`Progress event requires ${actualBytes} UTF-8 bytes, exceeding maxBatchBytes ${maxBatchBytes}.`);
8430
+ this.actualBytes = actualBytes;
8431
+ this.maxBatchBytes = maxBatchBytes;
8432
+ this.name = "ProgressEventTooLargeError";
8433
+ }
8434
+ actualBytes;
8435
+ maxBatchBytes;
8436
+ };
8437
+ var encoder = new TextEncoder();
8438
+ function assertPositiveSafeInteger(value, name) {
8439
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) {
8440
+ throw new TypeError(`${name} must be a positive safe integer when configured.`);
8441
+ }
8442
+ }
8443
+ function validateProgressBatcherOptions(options = {}) {
8444
+ assertPositiveSafeInteger(options.maxBatchSize, "maxBatchSize");
8445
+ assertPositiveSafeInteger(options.flushIntervalMs, "flushIntervalMs");
8446
+ assertPositiveSafeInteger(options.maxBatchBytes, "maxBatchBytes");
8447
+ }
8448
+ function encodedEventsBytes(events) {
8449
+ return encoder.encode(JSON.stringify(events)).length;
8450
+ }
8385
8451
  var ProgressBatcher = class {
8386
8452
  constructor(emit, options = {}) {
8387
8453
  this.emit = emit;
8454
+ validateProgressBatcherOptions(options);
8388
8455
  this.maxBatchSize = options.maxBatchSize ?? 10;
8389
8456
  this.flushIntervalMs = options.flushIntervalMs ?? 250;
8457
+ this.maxBatchBytes = options.maxBatchBytes;
8390
8458
  }
8391
8459
  emit;
8392
8460
  buffer = [];
@@ -8394,7 +8462,17 @@ var ProgressBatcher = class {
8394
8462
  timer;
8395
8463
  maxBatchSize;
8396
8464
  flushIntervalMs;
8465
+ maxBatchBytes;
8397
8466
  push(event) {
8467
+ if (this.maxBatchBytes !== void 0) {
8468
+ const eventBytes = encodedEventsBytes([event]);
8469
+ if (eventBytes > this.maxBatchBytes) {
8470
+ throw new ProgressEventTooLargeError(eventBytes, this.maxBatchBytes);
8471
+ }
8472
+ if (this.buffer.length > 0 && encodedEventsBytes([...this.buffer, event]) > this.maxBatchBytes) {
8473
+ this.flush();
8474
+ }
8475
+ }
8398
8476
  this.buffer.push(event);
8399
8477
  if (this.buffer.length >= this.maxBatchSize) {
8400
8478
  this.flush();
@@ -8447,6 +8525,7 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
8447
8525
  var MAX_TRACKED_TASK_IDS = 2e3;
8448
8526
  var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
8449
8527
  var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
8528
+ var MAX_PROGRESS_BATCH_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: progressBatch.maxBatchBytes";
8450
8529
  var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
8451
8530
  function resultDocumentRejectionDetail(check) {
8452
8531
  switch (check.reason) {
@@ -8466,6 +8545,9 @@ var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
8466
8545
  function isKnownRuntimeId(id) {
8467
8546
  return RuntimeIdSchema.safeParse(id).success;
8468
8547
  }
8548
+ function terminalUsageNumber(value, maximum) {
8549
+ return value !== void 0 && Number.isSafeInteger(value) && value >= 0 && value <= maximum ? value : void 0;
8550
+ }
8469
8551
  var DEFAULT_RUNTIME_PREFERENCE = ["claude", "codex", "pi"];
8470
8552
  function orderByPreference(candidates, preference) {
8471
8553
  const rank = new Map(preference.map((id, index) => [id, index]));
@@ -8765,7 +8847,13 @@ var TaskRunner = class {
8765
8847
  const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
8766
8848
  await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
8767
8849
  if (this.tasks.get(active.taskId) !== active) return true;
8768
- this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId: active.taskId }));
8850
+ this.deps.send(
8851
+ createEnvelope(
8852
+ "task.fail",
8853
+ { reason, retryable, ...this.terminalInferenceUsagePayload(active) },
8854
+ { taskId: active.taskId }
8855
+ )
8856
+ );
8769
8857
  return this.finish(active.taskId);
8770
8858
  }
8771
8859
  /** Graceful-shutdown caller of {@link teardownActiveTask} — see `shutdownActiveTasks`'s own doc comment. `retryable: true`: nothing about the task/policy itself was ever at fault, only this device's own availability right now. */
@@ -9143,7 +9231,8 @@ var TaskRunner = class {
9143
9231
  this.deps.batcherOptions
9144
9232
  ),
9145
9233
  approvalQueue: [],
9146
- outputBytesSoFar: 0
9234
+ outputBytesSoFar: 0,
9235
+ startedAtMs: Date.now()
9147
9236
  };
9148
9237
  if (this.pendingCancelled.has(taskId)) {
9149
9238
  const reason = this.pendingCancelled.get(taskId);
@@ -9155,7 +9244,13 @@ var TaskRunner = class {
9155
9244
  } catch {
9156
9245
  }
9157
9246
  await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
9158
- this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
9247
+ this.deps.send(
9248
+ createEnvelope(
9249
+ "task.cancelled",
9250
+ { reason, ...this.terminalInferenceUsagePayload(active) },
9251
+ { taskId }
9252
+ )
9253
+ );
9159
9254
  await this.finish(taskId);
9160
9255
  return;
9161
9256
  }
@@ -9228,6 +9323,9 @@ var TaskRunner = class {
9228
9323
  );
9229
9324
  return;
9230
9325
  }
9326
+ if (event.type === "usage") {
9327
+ active.lastUsage = event;
9328
+ }
9231
9329
  if (event.type === "needs_approval") {
9232
9330
  active.batcher.flush();
9233
9331
  const { taskId } = active;
@@ -9280,7 +9378,8 @@ var TaskRunner = class {
9280
9378
  // (where the codec actually serializes it), so a contextual
9281
9379
  // `toJSON(key)` or an unstable getter cannot make the wire
9282
9380
  // bytes differ from what the cap gate approved.
9283
- ...outcome.document !== void 0 ? { document: outcome.document } : {}
9381
+ ...outcome.document !== void 0 ? { document: outcome.document } : {},
9382
+ ...this.terminalInferenceUsagePayload(active)
9284
9383
  },
9285
9384
  { taskId: active.taskId, sessionRef: active.session.sessionRef }
9286
9385
  )
@@ -9303,6 +9402,13 @@ var TaskRunner = class {
9303
9402
  } catch (err) {
9304
9403
  if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
9305
9404
  active.batcher.flush();
9405
+ if (err instanceof ProgressEventTooLargeError) {
9406
+ await this.failActiveTaskForResourceLimit(
9407
+ active.taskId,
9408
+ `${MAX_PROGRESS_BATCH_BYTES_EXCEEDED_REASON_PREFIX}: event requires ${err.actualBytes} UTF-8 bytes, exceeding the configured limit of ${err.maxBatchBytes} bytes`
9409
+ );
9410
+ return;
9411
+ }
9306
9412
  const failure = projectRuntimeBoundaryFailure(err, "run");
9307
9413
  if (failure.contractViolation) {
9308
9414
  console.error("[byok/client] runtime adapter events iterable returned an untyped failure", err);
@@ -9390,7 +9496,13 @@ var TaskRunner = class {
9390
9496
  } catch {
9391
9497
  }
9392
9498
  await this.observeGit(active, "salvage");
9393
- this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
9499
+ this.deps.send(
9500
+ createEnvelope(
9501
+ "task.cancelled",
9502
+ { reason, ...this.terminalInferenceUsagePayload(active) },
9503
+ { taskId }
9504
+ )
9505
+ );
9394
9506
  await this.finish(taskId);
9395
9507
  }
9396
9508
  /** M3-B: bounded insert for `pendingCancelled` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest SAFE-TO-EVICT entry once over cap — see `evictPendingCancelled` (finding #5: not simply "the oldest entry", which could be an in-flight offer's own cancel marker). */
@@ -9828,7 +9940,13 @@ var TaskRunner = class {
9828
9940
  } catch {
9829
9941
  }
9830
9942
  await this.observeGit(active, "salvage");
9831
- this.deps.send(createEnvelope("task.fail", { reason: reason ?? "rejected", retryable: false }, { taskId }));
9943
+ this.deps.send(
9944
+ createEnvelope(
9945
+ "task.fail",
9946
+ { reason: reason ?? "rejected", retryable: false, ...this.terminalInferenceUsagePayload(active) },
9947
+ { taskId }
9948
+ )
9949
+ );
9832
9950
  await this.finish(taskId);
9833
9951
  }
9834
9952
  /** Pre-claim, fail-closed rejection (protocol §3.2) — never claims first. */
@@ -9846,9 +9964,44 @@ var TaskRunner = class {
9846
9964
  return;
9847
9965
  }
9848
9966
  if (active) await this.observeGit(active, "salvage");
9849
- this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
9967
+ this.deps.send(
9968
+ createEnvelope(
9969
+ "task.fail",
9970
+ active === void 0 ? { reason, retryable } : { reason, retryable, ...this.terminalInferenceUsagePayload(active) },
9971
+ { taskId }
9972
+ )
9973
+ );
9850
9974
  await this.finish(taskId);
9851
9975
  }
9976
+ /**
9977
+ * Build the optional terminal observation from facts this running daemon
9978
+ * actually has. No offered `dispatchSelection` is echoed here: it is a
9979
+ * requested execution target, not an adapter-reported provider/model fact.
9980
+ * The bundled adapter event contracts currently expose token observations
9981
+ * (Codex and Claude) but no provider/model observation, so those keys stay
9982
+ * absent. Pi exposes no native usage observation, so its terminal payload
9983
+ * omits this optional block rather than fabricating a usage observation from
9984
+ * independently known runtime, elapsed duration, or Local Agent version.
9985
+ */
9986
+ terminalInferenceUsagePayload(active) {
9987
+ const release = this.deps.localAgentRelease;
9988
+ const runtimeId = active.adapter.descriptor.id;
9989
+ if (release === void 0 || active.lastUsage === void 0 || !isKnownRuntimeId(runtimeId)) return {};
9990
+ const nowMs = Date.now();
9991
+ const durationMs = terminalUsageNumber(nowMs - active.startedAtMs, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS);
9992
+ const promptTokens = terminalUsageNumber(active.lastUsage?.inputTokens, TERMINAL_INFERENCE_USAGE_MAX_TOKENS);
9993
+ const completionTokens = terminalUsageNumber(active.lastUsage?.outputTokens, TERMINAL_INFERENCE_USAGE_MAX_TOKENS);
9994
+ return {
9995
+ usage: {
9996
+ runtime: runtimeId,
9997
+ clientVersion: release.version,
9998
+ reportedAt: new Date(nowMs).toISOString(),
9999
+ ...promptTokens === void 0 ? {} : { promptTokens },
10000
+ ...completionTokens === void 0 ? {} : { completionTokens },
10001
+ ...durationMs === void 0 ? {} : { durationMs }
10002
+ }
10003
+ };
10004
+ }
9852
10005
  /**
9853
10006
  * additive-minor (`task.complete.document`): the whole daemon-side gate
9854
10007
  * between a configured {@link ResultDocumentExtractor} and the wire —
@@ -10377,6 +10530,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
10377
10530
  return buildDaemonWithAdapters(config, adapters, overrides);
10378
10531
  }
10379
10532
  function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
10533
+ const localAgentRelease = resolveLocalAgentReleaseIdentity(config.localAgentRelease);
10380
10534
  const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
10381
10535
  const configuredToolsets = Object.freeze(
10382
10536
  [...mcpToolsets?.keys() ?? []].sort()
@@ -10389,6 +10543,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10389
10543
  `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".`
10390
10544
  );
10391
10545
  }
10546
+ validateProgressBatcherOptions(config.progressBatch);
10392
10547
  const presenceCadence = {
10393
10548
  intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
10394
10549
  ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
@@ -10489,6 +10644,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10489
10644
  let controlServerHandle;
10490
10645
  let daemonOwnerLease;
10491
10646
  let presencePublisher;
10647
+ let detectedRuntimeFacts = [];
10492
10648
  let presenceDiscovery;
10493
10649
  let presenceDiscoveryInFlight = false;
10494
10650
  let shutdownPromise;
@@ -10622,6 +10778,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10622
10778
  Promise.resolve(new BlobClient(config.serverUrl, auth))
10623
10779
  ]);
10624
10780
  observer.noteRuntimesDetected(runtimes);
10781
+ detectedRuntimeFacts = runtimes;
10625
10782
  const capabilities = computeCapabilities(adapters);
10626
10783
  const journalIdentity = config.hostedJournal ? { tenantId: config.hostedJournal.tenantId, productId: config.productId, deviceId: record.deviceId } : void 0;
10627
10784
  const sendEnvelope = activeJournal && journalIdentity ? (envelope) => {
@@ -10674,7 +10831,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10674
10831
  // untouched. See `observer.ts`'s module doc comment.
10675
10832
  send: sendEnvelope,
10676
10833
  blobClient,
10677
- batcherOptions: overrides.batch,
10834
+ batcherOptions: config.progressBatch,
10678
10835
  sessionWorkspaces,
10679
10836
  gitWorkspaceManager,
10680
10837
  gitWorkspaceStore,
@@ -10699,6 +10856,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10699
10856
  approvalRegistry,
10700
10857
  storeDir,
10701
10858
  productId: config.productId,
10859
+ // U2 terminal inference usage consumes the one U4a-resolved,
10860
+ // process-immutable identity captured above. This is composition-only:
10861
+ // TaskRunner receives no config, manifest, or second identity resolver.
10862
+ localAgentRelease,
10702
10863
  approvalTimeoutMs: overrides.approvalTimeoutMs,
10703
10864
  // Finding F5(a): see TaskRunnerDeps.shutdownInterruptTimeoutMs's own doc comment.
10704
10865
  shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
@@ -10744,6 +10905,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10744
10905
  deviceId: record.deviceId,
10745
10906
  productId: config.productId,
10746
10907
  capabilities,
10908
+ clientVersion: localAgentRelease.version,
10747
10909
  runtimes,
10748
10910
  configuredToolsets,
10749
10911
  auth,
@@ -10833,6 +10995,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
10833
10995
  serverUrl: config.serverUrl,
10834
10996
  auth,
10835
10997
  configuredToolsets,
10998
+ clientVersion: localAgentRelease.version,
10999
+ protocolVersions: [PROTOCOL_VERSION],
11000
+ runtimes: detectedRuntimeFacts,
10836
11001
  ...presenceCadence,
10837
11002
  onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
10838
11003
  });
@@ -11014,6 +11179,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
11014
11179
  const activeTasks = observer.tasks().filter((task) => TASK_TRANSITIONS[task.state].length > 0).map((task) => ({ taskId: task.taskId, state: task.state }));
11015
11180
  const pendingApprovals = approvalRegistry.list();
11016
11181
  return {
11182
+ localAgentRelease,
11017
11183
  pid: process.pid,
11018
11184
  uptimeMs: startedAt !== void 0 ? Date.now() - startedAt : 0,
11019
11185
  paired: auth.deviceId !== void 0,
@@ -11212,6 +11378,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
11212
11378
  }
11213
11379
  function status() {
11214
11380
  return {
11381
+ localAgentRelease,
11215
11382
  paired: auth.deviceId !== void 0,
11216
11383
  connected: connectionState === "open",
11217
11384
  degraded: connection?.isTransportDegraded() ?? false,
@@ -11841,6 +12008,13 @@ function createServiceLifecycle(def, opts = {}) {
11841
12008
  throw new UnsupportedServicePlatformError(platform);
11842
12009
  }
11843
12010
  }
12011
+
12012
+ // src/bin/official-release.ts
12013
+ var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
12014
+ version: "0.6.0"
12015
+ });
12016
+
12017
+ // src/bin/config.ts
11844
12018
  var REQUIRED_FIELDS = ["productName", "productId", "serverUrl", "workspaceRoot"];
11845
12019
  var ConfigError = class extends Error {
11846
12020
  constructor(message) {
@@ -11863,6 +12037,9 @@ function loadConfig(configPath, overrides = {}) {
11863
12037
  throw new ConfigError(`config at "${configPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
11864
12038
  }
11865
12039
  }
12040
+ if (Object.prototype.hasOwnProperty.call(base, "localAgentRelease") || Object.prototype.hasOwnProperty.call(overrides, "localAgentRelease")) {
12041
+ throw new ConfigError('config field "localAgentRelease" is distribution-owned and must not be supplied');
12042
+ }
11866
12043
  const merged = { ...base, ...overrides };
11867
12044
  if (merged.gitWorkspace !== void 0) {
11868
12045
  try {
@@ -11876,7 +12053,7 @@ function loadConfig(configPath, overrides = {}) {
11876
12053
  throw new ConfigError(`config is missing required field "${field}"`);
11877
12054
  }
11878
12055
  }
11879
- return merged;
12056
+ return { ...merged, localAgentRelease: OFFICIAL_LOCAL_AGENT_RELEASE };
11880
12057
  }
11881
12058
  function resolveStoreDir(config) {
11882
12059
  return DeviceStore.resolveDir(config.productId, config.storeDir);
@@ -12050,6 +12227,9 @@ function formatStatusLines(view) {
12050
12227
  const lines = [];
12051
12228
  const label = view.branding?.displayName ?? view.productName;
12052
12229
  lines.push(`product: ${label} (${view.productId})`);
12230
+ lines.push(
12231
+ `local-agent-release: ${view.localAgentRelease.version}${view.localAgentRelease.buildId ? ` buildId=${view.localAgentRelease.buildId}` : ""}`
12232
+ );
12053
12233
  if (view.branding?.supportUrl) lines.push(`support: ${view.branding.supportUrl}`);
12054
12234
  lines.push(`paired: ${view.paired ? "yes" : "no"}${view.deviceId ? ` deviceId=${view.deviceId}` : ""}`);
12055
12235
  lines.push(
@@ -12065,8 +12245,10 @@ function formatStatusLines(view) {
12065
12245
  return lines;
12066
12246
  }
12067
12247
  function formatLiveStatusLines(live) {
12248
+ const liveRelease = live.localAgentRelease;
12068
12249
  const lines = [
12069
12250
  `live: pid=${live.pid} uptimeMs=${live.uptimeMs} transport=${live.transport}`,
12251
+ liveRelease ? `live-local-agent-release: ${liveRelease.version}${liveRelease.buildId ? ` buildId=${liveRelease.buildId}` : ""}` : "live-local-agent-release: unknown",
12070
12252
  `live-paired: ${live.paired ? "yes" : "no"}${live.deviceId ? ` deviceId=${live.deviceId}` : ""}`,
12071
12253
  `live-runtimes: ${live.runtimeIds.length ? live.runtimeIds.join(",") : "(none)"}`
12072
12254
  ];
@@ -13755,6 +13937,7 @@ async function runStatusCommand(config, deps = {}) {
13755
13937
  ]);
13756
13938
  const tasks = deriveTasksFromEvents(events);
13757
13939
  const view = {
13940
+ localAgentRelease: config.localAgentRelease,
13758
13941
  productName: config.productName,
13759
13942
  productId: config.productId,
13760
13943
  branding: config.branding,
@@ -14183,11 +14366,17 @@ async function runWorkspacesCommand(config, deps = {}) {
14183
14366
  for (const record of records) log(formatWorkspaceLine(record, deps.showPaths === true));
14184
14367
  }
14185
14368
 
14369
+ // src/bin/version.ts
14370
+ function runVersionCommand(log = console.log) {
14371
+ log(OFFICIAL_LOCAL_AGENT_RELEASE.version);
14372
+ }
14373
+
14186
14374
  // src/bin/byok-agent.ts
14187
14375
  function usage() {
14188
14376
  console.error(
14189
14377
  [
14190
14378
  "Usage:",
14379
+ " byok-agent --version",
14191
14380
  " byok-agent pair <code> --server <url> [--config <path>]",
14192
14381
  " byok-agent start [--config <path>] (or BYOK_CONFIG env var)",
14193
14382
  " byok-agent status [--config <path>]",
@@ -14223,6 +14412,10 @@ function abortOnSignal() {
14223
14412
  }
14224
14413
  async function main() {
14225
14414
  const [, , command, ...rest] = process.argv;
14415
+ if (command === "--version") {
14416
+ runVersionCommand();
14417
+ return;
14418
+ }
14226
14419
  if (command === "pair") {
14227
14420
  const [code] = positionalArgs(rest, ["--server", "--config"]);
14228
14421
  const server = argValue(rest, "--server");