@aiden-ade/sandbox-agent 0.1.67 → 0.1.68

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 (2) hide show
  1. package/dist/index.cjs +647 -54
  2. package/package.json +4 -4
package/dist/index.cjs CHANGED
@@ -22087,7 +22087,7 @@ function describeError(error2) {
22087
22087
  }
22088
22088
 
22089
22089
  // src/version.ts
22090
- var AGENT_VERSION = "0.1.67";
22090
+ var AGENT_VERSION = "0.1.68";
22091
22091
 
22092
22092
  // src/daemon-worktree.ts
22093
22093
  var import_node_child_process3 = require("child_process");
@@ -43094,11 +43094,11 @@ var SocketWithoutUpgrade = class _SocketWithoutUpgrade extends Emitter {
43094
43094
  */
43095
43095
  _resetPingTimeout() {
43096
43096
  this.clearTimeoutFn(this._pingTimeoutTimer);
43097
- const delay2 = this._pingInterval + this._pingTimeout;
43098
- this._pingTimeoutTime = Date.now() + delay2;
43097
+ const delay3 = this._pingInterval + this._pingTimeout;
43098
+ this._pingTimeoutTime = Date.now() + delay3;
43099
43099
  this._pingTimeoutTimer = this.setTimeoutFn(() => {
43100
43100
  this._onClose("ping timeout");
43101
- }, delay2);
43101
+ }, delay3);
43102
43102
  if (this.opts.autoUnref) {
43103
43103
  this._pingTimeoutTimer.unref();
43104
43104
  }
@@ -45079,8 +45079,8 @@ var Manager = class extends Emitter {
45079
45079
  this.emitReserved("reconnect_failed");
45080
45080
  this._reconnecting = false;
45081
45081
  } else {
45082
- const delay2 = this.backoff.duration();
45083
- debug10("will wait %dms before reconnect attempt", delay2);
45082
+ const delay3 = this.backoff.duration();
45083
+ debug10("will wait %dms before reconnect attempt", delay3);
45084
45084
  this._reconnecting = true;
45085
45085
  const timer = this.setTimeoutFn(() => {
45086
45086
  if (self.skipReconnect)
@@ -45100,7 +45100,7 @@ var Manager = class extends Emitter {
45100
45100
  self.onreconnect();
45101
45101
  }
45102
45102
  });
45103
- }, delay2);
45103
+ }, delay3);
45104
45104
  if (this.opts.autoUnref) {
45105
45105
  timer.unref();
45106
45106
  }
@@ -61929,13 +61929,13 @@ var StreamableHTTPClientTransport = class {
61929
61929
  this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
61930
61930
  return;
61931
61931
  }
61932
- const delay2 = this._getNextReconnectionDelay(attemptCount);
61932
+ const delay3 = this._getNextReconnectionDelay(attemptCount);
61933
61933
  this._reconnectionTimeout = setTimeout(() => {
61934
61934
  this._startOrAuthSse(options).catch((error2) => {
61935
61935
  this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error2 instanceof Error ? error2.message : String(error2)}`));
61936
61936
  this._scheduleReconnection(options, attemptCount + 1);
61937
61937
  });
61938
- }, delay2);
61938
+ }, delay3);
61939
61939
  }
61940
61940
  _handleSseStream(stream, options, isReconnectable) {
61941
61941
  if (!stream) {
@@ -64748,38 +64748,96 @@ var publishSkillInputSchema = defineWireSchema()(external_exports.object({
64748
64748
  expectedContentHash: sha256Schema,
64749
64749
  uploadGrant: runtimeUploadGrantSchema
64750
64750
  }));
64751
- var boundedMetadataSchema = external_exports.record(external_exports.string().max(1024)).default({}).superRefine((value2, context) => {
64752
- const entries = Object.entries(value2);
64753
- if (entries.length > 64) {
64754
- context.addIssue({
64755
- code: external_exports.ZodIssueCode.custom,
64756
- message: "metadata has more than 64 entries"
64757
- });
64758
- }
64759
- for (const [key] of entries) {
64760
- if (new TextEncoder().encode(key).byteLength > 128) {
64761
- context.addIssue({
64762
- code: external_exports.ZodIssueCode.custom,
64763
- message: `metadata key exceeds 128 UTF-8 bytes: ${key.slice(0, 32)}`
64764
- });
64751
+ var skillFrontmatterAdmissionSchema = external_exports.object({
64752
+ name: external_exports.string().min(1).max(64).regex(SKILL_NAME_PATTERN),
64753
+ description: external_exports.string().trim().min(1).max(1024)
64754
+ }).passthrough();
64755
+ var PROJECTED_VALUE_MAX_BYTES = 16384;
64756
+ function formatKeys(keys) {
64757
+ const shown = keys.slice(0, 3).map((key) => `metadata.${key}`);
64758
+ const remaining = keys.length - shown.length;
64759
+ const listed = shown.join(", ");
64760
+ return remaining > 0 ? `${listed} and ${remaining} more` : listed;
64761
+ }
64762
+ function unsupported(message) {
64763
+ return { severity: "warning", code: "UNSUPPORTED_FRONTMATTER", message, path: "SKILL.md" };
64764
+ }
64765
+ function projectSkillFrontmatter(admitted) {
64766
+ const conformance = [];
64767
+ const raw = admitted;
64768
+ const optionalText = (key, max) => {
64769
+ const value2 = raw[key];
64770
+ if (value2 === void 0)
64771
+ return void 0;
64772
+ if (typeof value2 !== "string") {
64773
+ conformance.push(unsupported(`${key} is not a string; the Agent Skills spec expects text`));
64774
+ return void 0;
64775
+ }
64776
+ const trimmed = value2.trim();
64777
+ if (!trimmed)
64778
+ return void 0;
64779
+ if (trimmed.length > max) {
64780
+ conformance.push(unsupported(`${key} exceeds ${max} characters and was not indexed`));
64781
+ return void 0;
64782
+ }
64783
+ return trimmed;
64784
+ };
64785
+ const license = optionalText("license", 1024);
64786
+ const compatibility = optionalText("compatibility", 500);
64787
+ const allowedTools = optionalText("allowed-tools", 1024);
64788
+ const metadata = {};
64789
+ const rawMetadata = raw.metadata;
64790
+ if (rawMetadata !== void 0) {
64791
+ if (typeof rawMetadata !== "object" || rawMetadata === null || Array.isArray(rawMetadata)) {
64792
+ conformance.push(unsupported("metadata is not a mapping and was not indexed"));
64793
+ } else {
64794
+ const encoder = new TextEncoder();
64795
+ const notStrings = [];
64796
+ const selfReferential = [];
64797
+ const tooLarge = [];
64798
+ for (const [key, value2] of Object.entries(rawMetadata)) {
64799
+ if (typeof value2 !== "string")
64800
+ notStrings.push(key);
64801
+ let encoded;
64802
+ if (typeof value2 === "string") {
64803
+ encoded = value2;
64804
+ } else {
64805
+ try {
64806
+ encoded = JSON.stringify(value2) ?? "";
64807
+ } catch {
64808
+ selfReferential.push(key);
64809
+ continue;
64810
+ }
64811
+ }
64812
+ if (encoder.encode(encoded).byteLength > PROJECTED_VALUE_MAX_BYTES) {
64813
+ tooLarge.push(key);
64814
+ continue;
64815
+ }
64816
+ metadata[key] = value2;
64817
+ }
64818
+ if (notStrings.length > 0) {
64819
+ conformance.push(unsupported(`${formatKeys(notStrings)} ${notStrings.length === 1 ? "is" : "are"} not ${notStrings.length === 1 ? "a string" : "strings"}; the Agent Skills spec expects text`));
64820
+ }
64821
+ if (selfReferential.length > 0) {
64822
+ conformance.push(unsupported(`${formatKeys(selfReferential)} ${selfReferential.length === 1 ? "refers" : "refer"} to itself and ${selfReferential.length === 1 ? "was" : "were"} not indexed`));
64823
+ }
64824
+ if (tooLarge.length > 0) {
64825
+ conformance.push(unsupported(`${formatKeys(tooLarge)} ${tooLarge.length === 1 ? "is" : "are"} too large to index`));
64826
+ }
64765
64827
  }
64766
64828
  }
64767
- });
64768
- var skillFrontmatterSchema = external_exports.object({
64769
- name: external_exports.string().min(1).max(64).regex(SKILL_NAME_PATTERN),
64770
- description: external_exports.string().trim().min(1).max(1024),
64771
- license: external_exports.string().trim().min(1).max(1024).optional(),
64772
- compatibility: external_exports.string().trim().min(1).max(500).optional(),
64773
- metadata: boundedMetadataSchema,
64774
- "allowed-tools": external_exports.string().trim().min(1).max(1024).optional()
64775
- }).passthrough().transform((value2) => ({
64776
- name: value2.name,
64777
- description: value2.description,
64778
- ...value2.license ? { license: value2.license } : {},
64779
- ...value2.compatibility ? { compatibility: value2.compatibility } : {},
64780
- metadata: value2.metadata,
64781
- ...value2["allowed-tools"] ? { allowedTools: value2["allowed-tools"] } : {}
64782
- }));
64829
+ return {
64830
+ metadata: {
64831
+ name: admitted.name,
64832
+ description: admitted.description,
64833
+ ...license ? { license } : {},
64834
+ ...compatibility ? { compatibility } : {},
64835
+ metadata,
64836
+ ...allowedTools ? { allowedTools } : {}
64837
+ },
64838
+ conformance
64839
+ };
64840
+ }
64783
64841
  var skillPackageDiagnosticSchema = external_exports.object({
64784
64842
  severity: external_exports.literal("warning"),
64785
64843
  code: external_exports.enum(["BROKEN_REFERENCE", "UNSUPPORTED_FRONTMATTER"]),
@@ -65052,7 +65110,10 @@ async function inspectSkillPackage(packageRoot, options = {}) {
65052
65110
  inspectedFiles.sort(comparePackageFiles);
65053
65111
  const parsedMetadata = parseSkillMetadata(inspectedFiles.find((file2) => file2.manifest.path === "SKILL.md")?.bytes, options.expectedName === void 0 ? (0, import_node_path15.basename)(root) : options.expectedName);
65054
65112
  const manifestFiles = inspectedFiles.map((file2) => file2.manifest);
65055
- const diagnostics = collectDiagnostics(inspectedFiles.find((file2) => file2.manifest.path === "SKILL.md")?.bytes, parsedMetadata.metadata.license, new Set(manifestFiles.map((file2) => file2.path)));
65113
+ const diagnostics = [
65114
+ ...parsedMetadata.conformance,
65115
+ ...collectDiagnostics(inspectedFiles.find((file2) => file2.manifest.path === "SKILL.md")?.bytes, parsedMetadata.metadata.license, new Set(manifestFiles.map((file2) => file2.path)))
65116
+ ];
65056
65117
  return {
65057
65118
  schemaVersion: 1,
65058
65119
  entrypoint: "SKILL.md",
@@ -65294,14 +65355,14 @@ function parseSkillMetadata(bytes, expectedName) {
65294
65355
  if (!isRecord2(parsed)) {
65295
65356
  throw new SkillPackageError("INVALID_FRONTMATTER", "SKILL.md frontmatter must be a YAML mapping", "SKILL.md");
65296
65357
  }
65297
- const result = skillFrontmatterSchema.safeParse(parsed);
65298
- if (!result.success) {
65299
- throw new SkillPackageError("INVALID_FRONTMATTER", `SKILL.md frontmatter is invalid: ${result.error.issues.map(formatFrontmatterIssue).join("; ")}`, "SKILL.md");
65358
+ const admitted = skillFrontmatterAdmissionSchema.safeParse(parsed);
65359
+ if (!admitted.success) {
65360
+ throw new SkillPackageError("INVALID_FRONTMATTER", `SKILL.md frontmatter is invalid: ${admitted.error.issues.map(formatFrontmatterIssue).join("; ")}`, "SKILL.md");
65300
65361
  }
65301
- if (expectedName !== null && result.data.name !== expectedName.normalize("NFC")) {
65302
- throw new SkillPackageError("DIRECTORY_NAME_MISMATCH", `Skill name ${result.data.name} must match its directory ${expectedName}`, "SKILL.md");
65362
+ if (expectedName !== null && admitted.data.name !== expectedName.normalize("NFC")) {
65363
+ throw new SkillPackageError("DIRECTORY_NAME_MISMATCH", `Skill name ${admitted.data.name} must match its directory ${expectedName}`, "SKILL.md");
65303
65364
  }
65304
- return { metadata: result.data };
65365
+ return projectSkillFrontmatter(admitted.data);
65305
65366
  }
65306
65367
  function formatFrontmatterIssue(issue2) {
65307
65368
  const field = issue2.path.map(String).join(".");
@@ -68694,6 +68755,332 @@ function decodeResumeFallbackContext(encoded) {
68694
68755
  // src/sandbox.ts
68695
68756
  var import_node_os12 = require("os");
68696
68757
 
68758
+ // src/execution-activity-tracker.ts
68759
+ var ExecutionActivityTracker = class {
68760
+ constructor(onChange) {
68761
+ this.onChange = onChange;
68762
+ }
68763
+ activities = /* @__PURE__ */ new Map();
68764
+ register(input) {
68765
+ const normalized = normalizeRegistration(input);
68766
+ const existing = this.activities.get(normalized.ownerId);
68767
+ if (existing) {
68768
+ if (!sameRegistration(existing.input, normalized)) {
68769
+ throw new Error(`Execution activity owner ${normalized.ownerId} changed identity`);
68770
+ }
68771
+ existing.refCount += 1;
68772
+ } else {
68773
+ const activeLease = this.firstActivity()?.input.leaseRunId;
68774
+ if (activeLease && activeLease !== normalized.leaseRunId) {
68775
+ throw new Error(
68776
+ `Cannot track execution leases ${activeLease} and ${normalized.leaseRunId} concurrently`
68777
+ );
68778
+ }
68779
+ this.activities.set(normalized.ownerId, { input: normalized, refCount: 1 });
68780
+ }
68781
+ this.emitChange();
68782
+ let released = false;
68783
+ return {
68784
+ release: () => {
68785
+ if (released) return;
68786
+ released = true;
68787
+ const tracked = this.activities.get(normalized.ownerId);
68788
+ if (!tracked) return;
68789
+ tracked.refCount -= 1;
68790
+ if (tracked.refCount <= 0) this.activities.delete(normalized.ownerId);
68791
+ this.emitChange();
68792
+ }
68793
+ };
68794
+ }
68795
+ snapshot() {
68796
+ const first = this.firstActivity();
68797
+ if (!first) return null;
68798
+ const activeRunIds = /* @__PURE__ */ new Set([first.input.leaseRunId]);
68799
+ let trackedChildCount = 0;
68800
+ let trackedWorkCount = 0;
68801
+ let childAlive = false;
68802
+ let phase;
68803
+ for (const { input, refCount } of this.activities.values()) {
68804
+ activeRunIds.add(input.activeRunId ?? input.leaseRunId);
68805
+ trackedWorkCount += refCount;
68806
+ if (input.kind === "child") trackedChildCount += refCount;
68807
+ childAlive ||= input.childAlive === true;
68808
+ phase ??= input.phase;
68809
+ }
68810
+ return {
68811
+ runId: first.input.leaseRunId,
68812
+ conversationId: first.input.conversationId,
68813
+ taskId: first.input.taskId,
68814
+ sandboxId: first.input.sandboxId,
68815
+ activeRunIds: [...activeRunIds].sort(),
68816
+ phase,
68817
+ childAlive,
68818
+ trackedChildCount,
68819
+ trackedWorkCount
68820
+ };
68821
+ }
68822
+ firstActivity() {
68823
+ return this.activities.values().next().value;
68824
+ }
68825
+ emitChange() {
68826
+ this.onChange(this.snapshot());
68827
+ }
68828
+ };
68829
+ function normalizeRegistration(input) {
68830
+ const ownerId = input.ownerId.trim();
68831
+ const leaseRunId = input.leaseRunId.trim();
68832
+ const activeRunId = input.activeRunId?.trim() || void 0;
68833
+ const conversationId = input.conversationId.trim();
68834
+ const taskId = input.taskId.trim();
68835
+ const sandboxId = input.sandboxId.trim();
68836
+ if (!ownerId || !leaseRunId || !conversationId || !taskId || !sandboxId) {
68837
+ throw new Error("Execution activity identity fields must be non-empty");
68838
+ }
68839
+ return {
68840
+ ...input,
68841
+ ownerId,
68842
+ leaseRunId,
68843
+ activeRunId,
68844
+ conversationId,
68845
+ taskId,
68846
+ sandboxId
68847
+ };
68848
+ }
68849
+ function sameRegistration(left, right) {
68850
+ return left.ownerId === right.ownerId && left.leaseRunId === right.leaseRunId && left.activeRunId === right.activeRunId && left.conversationId === right.conversationId && left.taskId === right.taskId && left.sandboxId === right.sandboxId && left.kind === right.kind;
68851
+ }
68852
+
68853
+ // src/execution-lease-client.ts
68854
+ var EXECUTION_LEASE_RENEW_INTERVAL_MS = 3e4;
68855
+ var EXECUTION_LEASE_REQUEST_TIMEOUT_MS = 1e4;
68856
+ var DEFAULT_RETRY_DELAYS_MS = [250, 1e3];
68857
+ var ExecutionLeaseClient = class {
68858
+ constructor(options) {
68859
+ this.options = options;
68860
+ this.apiUrl = options.apiUrl.replace(/\/+$/, "");
68861
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
68862
+ this.renewIntervalMs = options.renewIntervalMs ?? EXECUTION_LEASE_RENEW_INTERVAL_MS;
68863
+ this.requestTimeoutMs = options.requestTimeoutMs ?? EXECUTION_LEASE_REQUEST_TIMEOUT_MS;
68864
+ this.retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
68865
+ this.enabled = options.enabled !== false;
68866
+ }
68867
+ apiUrl;
68868
+ fetchImpl;
68869
+ renewIntervalMs;
68870
+ requestTimeoutMs;
68871
+ retryDelaysMs;
68872
+ activity = null;
68873
+ activityGeneration = 0;
68874
+ interval = null;
68875
+ inFlight = null;
68876
+ trailingRenewal = false;
68877
+ nextSequence = 1;
68878
+ enabled;
68879
+ closed = false;
68880
+ terminalSessionFailure = false;
68881
+ terminalRunIds = /* @__PURE__ */ new Set();
68882
+ setActivity(activity) {
68883
+ if (this.closed || this.terminalSessionFailure) return;
68884
+ const previousRunId = this.activity?.runId ?? null;
68885
+ const nextRunId = activity?.runId ?? null;
68886
+ if (previousRunId !== nextRunId) this.activityGeneration += 1;
68887
+ this.activity = activity;
68888
+ if (!activity) {
68889
+ this.stopInterval();
68890
+ this.trailingRenewal = false;
68891
+ this.options.lifecycle?.debug("sandbox_agent_execution_lease_stopped", {
68892
+ reason: "idle"
68893
+ });
68894
+ return;
68895
+ }
68896
+ if (this.terminalRunIds.has(activity.runId)) {
68897
+ this.stopInterval();
68898
+ return;
68899
+ }
68900
+ if (!this.enabled) return;
68901
+ this.startInterval();
68902
+ if (previousRunId !== activity.runId || !this.inFlight) this.requestRenewal();
68903
+ else this.trailingRenewal = true;
68904
+ }
68905
+ /** Enable renewals after the connected WS server proves route support. */
68906
+ enable() {
68907
+ if (this.enabled || this.closed || this.terminalSessionFailure) return;
68908
+ this.enabled = true;
68909
+ this.options.lifecycle?.info("sandbox_agent_execution_lease_enabled");
68910
+ if (!this.activity || this.terminalRunIds.has(this.activity.runId)) return;
68911
+ this.startInterval();
68912
+ this.requestRenewal();
68913
+ }
68914
+ close() {
68915
+ this.closed = true;
68916
+ this.activityGeneration += 1;
68917
+ this.activity = null;
68918
+ this.trailingRenewal = false;
68919
+ this.stopInterval();
68920
+ }
68921
+ startInterval() {
68922
+ if (this.interval) return;
68923
+ this.interval = setInterval(() => this.requestRenewal(), this.renewIntervalMs);
68924
+ this.interval.unref?.();
68925
+ }
68926
+ stopInterval() {
68927
+ if (!this.interval) return;
68928
+ clearInterval(this.interval);
68929
+ this.interval = null;
68930
+ }
68931
+ loseAuthority(activity, seq, status, code) {
68932
+ this.terminalSessionFailure = true;
68933
+ this.stopInterval();
68934
+ this.options.lifecycle?.error("sandbox_agent_execution_lease_authority_lost", {
68935
+ runId: activity.runId,
68936
+ seq,
68937
+ status,
68938
+ code
68939
+ });
68940
+ this.options.onAuthorityLost?.({ runId: activity.runId, code, status });
68941
+ }
68942
+ requestRenewal() {
68943
+ if (this.closed || !this.enabled || this.terminalSessionFailure || !this.activity || this.terminalRunIds.has(this.activity.runId)) {
68944
+ return;
68945
+ }
68946
+ if (this.inFlight) {
68947
+ this.trailingRenewal = true;
68948
+ return;
68949
+ }
68950
+ const activity = this.activity;
68951
+ const activityGeneration = this.activityGeneration;
68952
+ const seq = this.nextSequence;
68953
+ this.nextSequence += 1;
68954
+ this.inFlight = this.renewWithRetry(activity, activityGeneration, seq).finally(() => {
68955
+ this.inFlight = null;
68956
+ if (!this.trailingRenewal) return;
68957
+ this.trailingRenewal = false;
68958
+ if (this.activity) this.requestRenewal();
68959
+ });
68960
+ }
68961
+ isCurrentRun(activity, generation) {
68962
+ return !this.closed && this.activityGeneration === generation && this.activity?.runId === activity.runId;
68963
+ }
68964
+ async renewWithRetry(activity, activityGeneration, seq) {
68965
+ const body = {
68966
+ conversationId: activity.conversationId,
68967
+ taskId: activity.taskId,
68968
+ sandboxId: activity.sandboxId,
68969
+ agentSessionId: this.options.agentSessionId,
68970
+ seq,
68971
+ activeRunIds: activity.activeRunIds,
68972
+ agentVersion: this.options.agentVersion,
68973
+ phase: activity.phase,
68974
+ childAlive: activity.childAlive,
68975
+ trackedChildCount: activity.trackedChildCount
68976
+ };
68977
+ const url3 = `${this.apiUrl}/public/agent-runs/${encodeURIComponent(activity.runId)}/execution-lease`;
68978
+ for (let attempt = 0; attempt <= this.retryDelaysMs.length; attempt += 1) {
68979
+ if (!this.isCurrentRun(activity, activityGeneration)) return;
68980
+ try {
68981
+ const response = await this.fetchImpl(url3, {
68982
+ method: "POST",
68983
+ headers: {
68984
+ authorization: `Bearer ${this.options.sessionToken}`,
68985
+ "content-type": "application/json"
68986
+ },
68987
+ body: JSON.stringify(body),
68988
+ signal: AbortSignal.timeout(this.requestTimeoutMs)
68989
+ });
68990
+ if (response.ok) {
68991
+ this.options.lifecycle?.debug("sandbox_agent_execution_lease_renewed", {
68992
+ runId: activity.runId,
68993
+ seq,
68994
+ attempt: attempt + 1
68995
+ });
68996
+ return;
68997
+ }
68998
+ const errorBody = await readErrorBody(response);
68999
+ const code = errorBody.code;
69000
+ const sessionAuthorityLost = response.status === 409 && code === "EXECUTION_LEASE_FENCED" || response.status === 401 && code === "EXECUTION_LEASE_INVALID_TOKEN";
69001
+ if (sessionAuthorityLost && code) {
69002
+ this.loseAuthority(activity, seq, response.status, code);
69003
+ return;
69004
+ }
69005
+ const runAuthorityLost = response.status === 409 && code === "EXECUTION_LEASE_INACTIVE_RUN" || response.status === 403 && code === "EXECUTION_LEASE_BINDING_MISMATCH" || response.status === 404 && code === "EXECUTION_LEASE_RUN_NOT_FOUND";
69006
+ if (runAuthorityLost && code) {
69007
+ if (!this.isCurrentRun(activity, activityGeneration)) {
69008
+ this.options.lifecycle?.debug("sandbox_agent_execution_lease_stale_response_ignored", {
69009
+ runId: activity.runId,
69010
+ seq,
69011
+ status: response.status,
69012
+ code
69013
+ });
69014
+ return;
69015
+ }
69016
+ this.loseAuthority(activity, seq, response.status, code);
69017
+ return;
69018
+ }
69019
+ if (response.status === 409 && code === "EXECUTION_LEASE_PROVIDER_UNRESOLVED") {
69020
+ this.terminalRunIds.add(activity.runId);
69021
+ if (this.activity?.runId === activity.runId) this.stopInterval();
69022
+ this.options.lifecycle?.warn("sandbox_agent_execution_lease_run_rejected", {
69023
+ runId: activity.runId,
69024
+ seq,
69025
+ code
69026
+ });
69027
+ return;
69028
+ }
69029
+ const takeoverPending = response.status === 409 && code === "EXECUTION_LEASE_TAKEOVER_PENDING";
69030
+ const mixedVersionUnavailable = (response.status === 401 || response.status === 403 || response.status === 404) && !code?.startsWith("EXECUTION_LEASE_");
69031
+ const retryable = takeoverPending || mixedVersionUnavailable || isRetryableStatus(response.status);
69032
+ if (!retryable || attempt === this.retryDelaysMs.length) {
69033
+ this.options.lifecycle?.warn("sandbox_agent_execution_lease_renew_failed", {
69034
+ runId: activity.runId,
69035
+ seq,
69036
+ status: response.status,
69037
+ code,
69038
+ attempt: attempt + 1
69039
+ });
69040
+ return;
69041
+ }
69042
+ if (takeoverPending) {
69043
+ this.options.lifecycle?.debug("sandbox_agent_execution_lease_takeover_pending", {
69044
+ runId: activity.runId,
69045
+ seq,
69046
+ attempt: attempt + 1
69047
+ });
69048
+ }
69049
+ } catch (error2) {
69050
+ if (attempt === this.retryDelaysMs.length) {
69051
+ this.options.lifecycle?.warn("sandbox_agent_execution_lease_renew_failed", {
69052
+ runId: activity.runId,
69053
+ seq,
69054
+ attempt: attempt + 1,
69055
+ error: error2 instanceof Error ? error2 : new Error(String(error2))
69056
+ });
69057
+ return;
69058
+ }
69059
+ }
69060
+ await delay2(this.retryDelaysMs[attempt] ?? 0);
69061
+ }
69062
+ }
69063
+ };
69064
+ function isRetryableStatus(status) {
69065
+ return status === 408 || status === 429 || status >= 500;
69066
+ }
69067
+ async function readErrorBody(response) {
69068
+ try {
69069
+ const body = await response.json();
69070
+ if (!body || typeof body !== "object") return {};
69071
+ const record2 = body;
69072
+ return {
69073
+ code: typeof record2.code === "string" ? record2.code : void 0,
69074
+ message: typeof record2.message === "string" ? record2.message : void 0
69075
+ };
69076
+ } catch {
69077
+ return {};
69078
+ }
69079
+ }
69080
+ function delay2(ms) {
69081
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
69082
+ }
69083
+
68697
69084
  // src/web-presenter.ts
68698
69085
  var import_node_crypto15 = require("crypto");
68699
69086
  var WebPresenter = class {
@@ -69086,6 +69473,21 @@ var SandboxEventDispatcher = class {
69086
69473
 
69087
69474
  // src/ws-client.ts
69088
69475
  var ACCEPTED_MESSAGE_ID_CACHE_SIZE = 200;
69476
+ var SUPERVISED_RECONNECT_BASE_DELAY_MS = 1e3;
69477
+ var SUPERVISED_RECONNECT_MAX_DELAY_MS = 5e3;
69478
+ var SUPERVISED_RECONNECT_MAX_JITTER_MS = 500;
69479
+ function parseConnectionDirective(data) {
69480
+ if (typeof data !== "object" || data === null || typeof data.code !== "string" || typeof data.retryable !== "boolean") {
69481
+ return null;
69482
+ }
69483
+ const candidate = data;
69484
+ return {
69485
+ code: candidate.code,
69486
+ retryable: candidate.retryable,
69487
+ reason: typeof candidate.reason === "string" ? candidate.reason : void 0,
69488
+ retryAfterMs: typeof candidate.retryAfterMs === "number" && Number.isFinite(candidate.retryAfterMs) && candidate.retryAfterMs >= 0 ? candidate.retryAfterMs : void 0
69489
+ };
69490
+ }
69089
69491
  var WSClient = class {
69090
69492
  constructor(wsUrl, sessionId, taskId, token, callbacks, agentVersion, lifecycle) {
69091
69493
  this.lifecycle = lifecycle;
@@ -69094,7 +69496,7 @@ var WSClient = class {
69094
69496
  this.getRunState = callbacks.getRunState;
69095
69497
  this.socket = lookup(wsUrl, {
69096
69498
  query: { type: "agent", sessionId, taskId, agentVersion: agentVersion ?? "" },
69097
- auth: { token },
69499
+ auth: { token, agentSessionId: this.agentSessionId },
69098
69500
  transports: ["websocket"],
69099
69501
  reconnection: true,
69100
69502
  reconnectionAttempts: Infinity,
@@ -69126,6 +69528,10 @@ var WSClient = class {
69126
69528
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
69127
69529
  heartbeatSeq = 0;
69128
69530
  heartbeatTimer = null;
69531
+ supervisedReconnectTimer = null;
69532
+ supervisedReconnectAttempt = 0;
69533
+ connectionDirective = null;
69534
+ intentionalClose = false;
69129
69535
  agentVersion;
69130
69536
  getRunState;
69131
69537
  /**
@@ -69232,16 +69638,82 @@ var WSClient = class {
69232
69638
  get connected() {
69233
69639
  return this.socket.connected;
69234
69640
  }
69641
+ getAgentSessionId() {
69642
+ return this.agentSessionId;
69643
+ }
69235
69644
  setLifecycleContext(context) {
69236
69645
  this.lifecycle?.setContext(context);
69237
69646
  }
69238
69647
  close() {
69239
69648
  this.lifecycle?.info("sandbox_agent_ws_close_requested");
69649
+ this.intentionalClose = true;
69650
+ this.clearSupervisedReconnect();
69240
69651
  this.stopHeartbeat();
69241
69652
  this.socket.disconnect();
69242
69653
  }
69654
+ clearSupervisedReconnect() {
69655
+ if (this.supervisedReconnectTimer) {
69656
+ clearTimeout(this.supervisedReconnectTimer);
69657
+ this.supervisedReconnectTimer = null;
69658
+ }
69659
+ }
69660
+ /**
69661
+ * Socket.IO reconnects transport failures through its Manager. It does not
69662
+ * reconnect an `io server disconnect`, where `socket.active` is false. Only
69663
+ * supervise that terminal transport state, and only after the server has
69664
+ * explicitly classified the disconnect as retryable (including the legacy
69665
+ * `server_shutdown` advisory emitted by older WS servers).
69666
+ */
69667
+ scheduleSupervisedReconnect(reason) {
69668
+ if (this.intentionalClose || this.socket.connected || this.socket.active || this.supervisedReconnectTimer || this.connectionDirective?.retryable !== true) {
69669
+ return;
69670
+ }
69671
+ const attempt = this.supervisedReconnectAttempt + 1;
69672
+ this.supervisedReconnectAttempt = attempt;
69673
+ const exponentialDelay = Math.min(
69674
+ SUPERVISED_RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1),
69675
+ SUPERVISED_RECONNECT_MAX_DELAY_MS
69676
+ );
69677
+ const requestedDelay = Math.max(exponentialDelay, this.connectionDirective.retryAfterMs ?? 0);
69678
+ const jitterRange = Math.min(SUPERVISED_RECONNECT_MAX_JITTER_MS, requestedDelay / 2);
69679
+ const delayMs = requestedDelay + Math.floor(Math.random() * jitterRange);
69680
+ this.lifecycle?.warn("sandbox_agent_ws_supervised_reconnect_scheduled", {
69681
+ reason,
69682
+ directiveCode: this.connectionDirective.code,
69683
+ attempt,
69684
+ delayMs
69685
+ });
69686
+ this.supervisedReconnectTimer = setTimeout(() => {
69687
+ this.supervisedReconnectTimer = null;
69688
+ if (this.intentionalClose || this.socket.connected || this.socket.active) return;
69689
+ this.lifecycle?.info("sandbox_agent_ws_supervised_reconnect_attempt", {
69690
+ directiveCode: this.connectionDirective?.code,
69691
+ attempt
69692
+ });
69693
+ this.connectionDirective = null;
69694
+ this.socket.connect();
69695
+ }, delayMs);
69696
+ this.supervisedReconnectTimer.unref?.();
69697
+ }
69243
69698
  setupListeners(callbacks) {
69699
+ this.socket.io.on("reconnect_attempt", (attempt) => {
69700
+ this.lifecycle?.info("sandbox_agent_ws_manager_reconnect_attempt", { attempt });
69701
+ });
69702
+ this.socket.io.on("reconnect", (attempt) => {
69703
+ this.lifecycle?.info("sandbox_agent_ws_manager_reconnected", { attempt });
69704
+ });
69705
+ this.socket.io.on("reconnect_error", (error2) => {
69706
+ this.lifecycle?.warn("sandbox_agent_ws_manager_reconnect_error", {
69707
+ error: error2 instanceof Error ? error2 : new Error(String(error2))
69708
+ });
69709
+ });
69710
+ this.socket.io.on("reconnect_failed", () => {
69711
+ this.lifecycle?.error("sandbox_agent_ws_manager_reconnect_failed");
69712
+ });
69244
69713
  this.socket.on("connect", () => {
69714
+ this.clearSupervisedReconnect();
69715
+ this.supervisedReconnectAttempt = 0;
69716
+ this.connectionDirective = null;
69245
69717
  this.lifecycle?.info("sandbox_agent_ws_socket_connected", { socketId: this.socket.id });
69246
69718
  this.startHeartbeat();
69247
69719
  this.eventDispatcher.flush();
@@ -69252,6 +69724,36 @@ var WSClient = class {
69252
69724
  this.stopHeartbeat();
69253
69725
  this.eventDispatcher.disconnect();
69254
69726
  callbacks.onDisconnect?.(reason);
69727
+ this.scheduleSupervisedReconnect(reason);
69728
+ });
69729
+ this.socket.on("server_shutdown", (data) => {
69730
+ this.connectionDirective = {
69731
+ code: "server_shutdown",
69732
+ reason: data?.reason,
69733
+ retryable: true,
69734
+ retryAfterMs: data?.retryAfterMs ?? 1e3
69735
+ };
69736
+ });
69737
+ this.socket.on("agent.capabilities", (data) => {
69738
+ if (typeof data !== "object" || data === null || typeof data.executionLeaseV1 !== "boolean") {
69739
+ return;
69740
+ }
69741
+ const capabilities = {
69742
+ executionLeaseV1: data.executionLeaseV1
69743
+ };
69744
+ this.lifecycle?.info("sandbox_agent_ws_capabilities_received", {
69745
+ executionLeaseV1: capabilities.executionLeaseV1
69746
+ });
69747
+ callbacks.onCapabilities?.(capabilities);
69748
+ });
69749
+ this.socket.on("connection_directive", (data) => {
69750
+ const directive = parseConnectionDirective(data);
69751
+ if (!directive) return;
69752
+ this.connectionDirective = directive;
69753
+ if (!directive.retryable) {
69754
+ this.clearSupervisedReconnect();
69755
+ callbacks.onTerminalDirective?.({ code: directive.code, reason: directive.reason });
69756
+ }
69255
69757
  });
69256
69758
  this.socket.on("agent.probe", (_data, ack) => {
69257
69759
  ack?.({
@@ -69261,9 +69763,24 @@ var WSClient = class {
69261
69763
  });
69262
69764
  });
69263
69765
  this.socket.on("connect_error", (error2) => {
69766
+ const directive = parseConnectionDirective(
69767
+ error2?.data
69768
+ );
69769
+ if (directive) {
69770
+ this.connectionDirective = directive;
69771
+ if (!directive.retryable) {
69772
+ this.clearSupervisedReconnect();
69773
+ callbacks.onTerminalDirective?.({ code: directive.code, reason: directive.reason });
69774
+ }
69775
+ } else {
69776
+ this.connectionDirective = null;
69777
+ }
69264
69778
  this.lifecycle?.warn("sandbox_agent_ws_socket_connect_error", {
69265
- error: error2 instanceof Error ? error2 : new Error(String(error2))
69779
+ error: error2 instanceof Error ? error2 : new Error(String(error2)),
69780
+ directiveCode: directive?.code,
69781
+ retryable: directive?.retryable
69266
69782
  });
69783
+ this.scheduleSupervisedReconnect("connect_error");
69267
69784
  });
69268
69785
  this.socket.on(
69269
69786
  "user_message",
@@ -69367,10 +69884,14 @@ async function runSandbox(config2) {
69367
69884
  let currentImages = [];
69368
69885
  let currentFiles = [];
69369
69886
  let currentParsedAttachments = [];
69370
- const stopped = false;
69887
+ let stopped = false;
69371
69888
  let pendingMessageResolve = null;
69372
69889
  const queuedMessages = [];
69373
69890
  let currentAgent = null;
69891
+ let executionLeaseClient = null;
69892
+ let executionLeaseV1Supported = false;
69893
+ let activeRootExecutionActivity = null;
69894
+ let reportActiveRun = false;
69374
69895
  let currentTaskMeta;
69375
69896
  let currentPrMeta;
69376
69897
  let currentWorkflowTools;
@@ -69398,6 +69919,32 @@ async function runSandbox(config2) {
69398
69919
  lifecycle.info("sandbox_agent_ws_connect_attempt", {
69399
69920
  wsUrl: config2.wsUrl
69400
69921
  });
69922
+ const stopActiveAgent = () => {
69923
+ reportActiveRun = false;
69924
+ activeRootExecutionActivity?.release();
69925
+ activeRootExecutionActivity = null;
69926
+ const agent = currentAgent;
69927
+ if (!agent) return;
69928
+ const killed = agent.kill();
69929
+ if (!killed) {
69930
+ agent.abort();
69931
+ return;
69932
+ }
69933
+ const abortDeadline = setTimeout(() => {
69934
+ if (currentAgent === agent) agent.abort();
69935
+ }, 5e3);
69936
+ abortDeadline.unref?.();
69937
+ };
69938
+ const terminateAgentProcess = (reason) => {
69939
+ if (stopped) return;
69940
+ lifecycle.warn("sandbox_agent_process_fenced", reason);
69941
+ stopped = true;
69942
+ executionLeaseClient?.close();
69943
+ stopActiveAgent();
69944
+ const resolvePendingMessage = pendingMessageResolve;
69945
+ pendingMessageResolve = null;
69946
+ resolvePendingMessage?.(null);
69947
+ };
69401
69948
  const wsClient = new WSClient(
69402
69949
  config2.wsUrl,
69403
69950
  config2.sessionId,
@@ -69440,10 +69987,7 @@ async function runSandbox(config2) {
69440
69987
  },
69441
69988
  onStop: () => {
69442
69989
  lifecycle.info("sandbox_agent_stop_received");
69443
- if (currentAgent) {
69444
- const killed = currentAgent.kill();
69445
- if (!killed) currentAgent.abort();
69446
- }
69990
+ stopActiveAgent();
69447
69991
  },
69448
69992
  onToolResponse: (toolId, response) => {
69449
69993
  if (currentAgent) {
@@ -69457,12 +70001,40 @@ async function runSandbox(config2) {
69457
70001
  },
69458
70002
  onConnect: () => lifecycle.info("sandbox_agent_ws_connected"),
69459
70003
  onDisconnect: (reason) => lifecycle.info("sandbox_agent_ws_disconnected", { reason }),
70004
+ onCapabilities: (capabilities) => {
70005
+ if (!capabilities.executionLeaseV1) return;
70006
+ executionLeaseV1Supported = true;
70007
+ executionLeaseClient?.enable();
70008
+ },
70009
+ onTerminalDirective: terminateAgentProcess,
69460
70010
  // Liveness heartbeat run state: a run is active while CoreAgent is executing.
69461
- getRunState: () => currentAgent ? { idle: false, activeRunIds: currentRunId ? [currentRunId] : [] } : { idle: true, activeRunIds: [] }
70011
+ getRunState: () => {
70012
+ const activeRunId = currentRunId ?? config2.runId;
70013
+ return currentAgent && reportActiveRun ? { idle: false, activeRunIds: activeRunId ? [activeRunId] : [] } : { idle: true, activeRunIds: [] };
70014
+ }
69462
70015
  },
69463
70016
  AGENT_VERSION,
69464
70017
  lifecycle
69465
70018
  );
70019
+ const agentSessionId = wsClient.getAgentSessionId();
70020
+ executionLeaseClient = config2.apiUrl && config2.sandboxId && agentSessionId ? new ExecutionLeaseClient({
70021
+ apiUrl: config2.apiUrl,
70022
+ sessionToken: config2.sessionToken,
70023
+ agentSessionId,
70024
+ agentVersion: AGENT_VERSION,
70025
+ lifecycle,
70026
+ enabled: false,
70027
+ onAuthorityLost: ({ code }) => terminateAgentProcess({ code })
70028
+ }) : null;
70029
+ if (executionLeaseV1Supported) executionLeaseClient?.enable();
70030
+ const executionActivityTracker = executionLeaseClient ? new ExecutionActivityTracker((activity) => executionLeaseClient.setActivity(activity)) : null;
70031
+ if (!executionLeaseClient && (currentRunId ?? config2.runId)) {
70032
+ lifecycle.warn("sandbox_agent_execution_lease_unavailable", {
70033
+ hasApiUrl: Boolean(config2.apiUrl),
70034
+ hasSandboxId: Boolean(config2.sandboxId),
70035
+ hasAgentSessionId: Boolean(agentSessionId)
70036
+ });
70037
+ }
69466
70038
  try {
69467
70039
  await wsClient.waitForConnection();
69468
70040
  } catch (error2) {
@@ -69490,6 +70062,18 @@ async function runSandbox(config2) {
69490
70062
  backendKind: activeBackendKind
69491
70063
  });
69492
70064
  currentAgent = agent;
70065
+ const activeRunId = currentRunId ?? config2.runId;
70066
+ const rootExecutionActivity = executionActivityTracker && activeRunId && currentTaskId ? executionActivityTracker.register({
70067
+ ownerId: `root:${activeRunId}`,
70068
+ leaseRunId: activeRunId,
70069
+ conversationId: currentConversationId,
70070
+ taskId: currentTaskId,
70071
+ sandboxId: config2.sandboxId,
70072
+ kind: "root",
70073
+ phase: "agent_run"
70074
+ }) : null;
70075
+ activeRootExecutionActivity = rootExecutionActivity;
70076
+ reportActiveRun = true;
69493
70077
  wsClient.setCurrentRunId(currentRunId ?? config2.runId);
69494
70078
  presenter.setRunContext?.(currentRunId ?? config2.runId, activeBackendKind);
69495
70079
  const correlation = correlationLogFields2({
@@ -69642,6 +70226,12 @@ async function runSandbox(config2) {
69642
70226
  presenter.onError(errorMessage, false);
69643
70227
  }
69644
70228
  presenter.onComplete(result);
70229
+ } finally {
70230
+ reportActiveRun = false;
70231
+ rootExecutionActivity?.release();
70232
+ if (activeRootExecutionActivity === rootExecutionActivity) {
70233
+ activeRootExecutionActivity = null;
70234
+ }
69645
70235
  }
69646
70236
  currentAgent = null;
69647
70237
  resumeFallbackContext = void 0;
@@ -69733,6 +70323,7 @@ async function runSandbox(config2) {
69733
70323
  }
69734
70324
  presenter.setSuppressSessionLifecycle(false);
69735
70325
  lifecycle.info("sandbox_agent_process_exit");
70326
+ executionLeaseClient?.close();
69736
70327
  wsClient.close();
69737
70328
  }
69738
70329
 
@@ -70098,6 +70689,7 @@ function logEffectiveGitIdentity(lifecycle, projectPath) {
70098
70689
  }
70099
70690
  async function runSessionFromEnv() {
70100
70691
  const wsUrl = process.env.ALAN_WS_URL;
70692
+ const apiUrl = process.env.ALAN_API_URL || void 0;
70101
70693
  const sessionId = process.env.ALAN_SESSION_ID;
70102
70694
  const taskId = process.env.ALAN_TASK_ID;
70103
70695
  const sessionToken = process.env.ALAN_SESSION_TOKEN;
@@ -70143,6 +70735,7 @@ async function runSessionFromEnv() {
70143
70735
  logEffectiveGitIdentity(lifecycle, projectPath);
70144
70736
  await runSandbox({
70145
70737
  wsUrl,
70738
+ apiUrl,
70146
70739
  sessionId,
70147
70740
  taskId,
70148
70741
  sessionToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiden-ade/sandbox-agent",
3
- "version": "0.1.67",
3
+ "version": "0.1.68",
4
4
  "type": "module",
5
5
  "description": "Alan agent runtime — cloud sandbox and local daemon (alan-agent CLI)",
6
6
  "bin": {
@@ -25,8 +25,8 @@
25
25
  "tsup": "^8.5.1",
26
26
  "tsx": "^4.19.0",
27
27
  "typescript": "~5.9.3",
28
- "@alan-ai/agent-core": "0.1.0",
29
- "@alan-ai/shared": "0.1.0"
28
+ "@alan-ai/shared": "0.1.0",
29
+ "@alan-ai/agent-core": "0.1.0"
30
30
  },
31
31
  "deprecated": "Use @alan-ai-hq/agent-manager instead.",
32
32
  "scripts": {
@@ -35,7 +35,7 @@
35
35
  "lint": "biome check .",
36
36
  "lint:fix": "biome check --write .",
37
37
  "format": "biome format --write .",
38
- "test": "vitest run src/__tests__/daemon-socket.integration.test.ts && vitest run --exclude src/__tests__/daemon-socket.integration.test.ts",
38
+ "test": "vitest run",
39
39
  "test:coverage": "vitest run --coverage",
40
40
  "test:watch": "vitest",
41
41
  "type-check": "tsc --noEmit",