impel-cli 0.20.11 → 0.20.13

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/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.13 — Allocation publication race
4
+
5
+ - Retries allocation when a competing process moves the published lock between
6
+ the expected create collision and inspection.
7
+ - Prevents a harmless `ENOENT` publication race from escaping as an MCP error
8
+ or consuming an agent inference turn.
9
+ - Adds a deterministic regression test for the exact production interleaving.
10
+
11
+ ## 0.20.12 — Internal admission retry
12
+
13
+ - Absorbs brief local run-allocation lock contention inside the transport
14
+ instead of returning a model-visible busy error.
15
+ - Keeps cancellation bounded while preserving the same single persisted
16
+ idempotency key and authoritative run identity.
17
+ - Prevents a transient concurrency collision from adding a second agent tool
18
+ call or wrapper inference turn.
19
+
3
20
  ## 0.20.11 — Managed profile upgrade compatibility
4
21
 
5
22
  - Recognizes every discovery-root manifest from v2 through the current schema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.11",
3
+ "version": "0.20.13",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,6 +52,8 @@ const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
52
52
  const DEFAULT_MAX_STATE_BYTES = 512 * 1024;
53
53
  const DEFAULT_LOCK_LEASE_MS = 75_000;
54
54
  const DEFAULT_ADMISSION_LEASE_MS = 10_000;
55
+ const DEFAULT_ADMISSION_RETRY_ATTEMPTS = 40;
56
+ const DEFAULT_ADMISSION_RETRY_INTERVAL_MS = 25;
55
57
  const STATE_COMMIT_LEASE_MARGIN_MS = 5_000;
56
58
  const CIBI_START_IN_PROGRESS_CODE = "eve_run_drain_start_in_progress";
57
59
  const CIBI_START_IN_PROGRESS_MESSAGE = "The durable Eve drain workflow is still being enqueued; retry this idempotent start.";
@@ -167,6 +169,30 @@ function throwIfAborted(signal) {
167
169
  if (signal?.aborted) throw abortError();
168
170
  }
169
171
 
172
+ function abortableDelay(ms, signal) {
173
+ throwIfAborted(signal);
174
+ return new Promise((resolve, reject) => {
175
+ let settled = false;
176
+ let timeout = null;
177
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
178
+ const onAbort = () => {
179
+ if (settled) return;
180
+ settled = true;
181
+ clearTimeout(timeout);
182
+ cleanup();
183
+ reject(abortError());
184
+ };
185
+ timeout = setTimeout(() => {
186
+ if (settled) return;
187
+ settled = true;
188
+ cleanup();
189
+ resolve();
190
+ }, ms);
191
+ signal?.addEventListener("abort", onAbort, { once: true });
192
+ if (signal?.aborted) onAbort();
193
+ });
194
+ }
195
+
170
196
  function boundedAttachmentSignal(parentSignal, timeoutMs) {
171
197
  const controller = new AbortController();
172
198
  let timedOut = false;
@@ -1362,7 +1388,13 @@ export class NativeAgentCompositeTransport {
1362
1388
  throw error;
1363
1389
  }
1364
1390
  fs.rmSync(generationPath, { recursive: true, force: true });
1365
- const stat = fs.lstatSync(lockPath);
1391
+ let stat;
1392
+ try {
1393
+ stat = fs.lstatSync(lockPath);
1394
+ } catch (statError) {
1395
+ if (statError?.code === "ENOENT") continue;
1396
+ throw statError;
1397
+ }
1366
1398
  if (stat.isSymbolicLink()) throw new Error("refusing to use symlinked native-agent run lock");
1367
1399
  let current = null;
1368
1400
  let legacy = null;
@@ -1660,11 +1692,15 @@ export class NativeAgentCompositeTransport {
1660
1692
  };
1661
1693
  }
1662
1694
 
1663
- allocateState(startArguments, { startMode = "durable" } = {}) {
1695
+ async allocateState(startArguments, { startMode = "durable", signal } = {}) {
1664
1696
  let admission = null;
1665
1697
  try {
1666
1698
  this.ensureRoots();
1667
- admission = this.acquireAdmissionLock();
1699
+ for (let attempt = 0; attempt < DEFAULT_ADMISSION_RETRY_ATTEMPTS; attempt += 1) {
1700
+ admission = this.acquireAdmissionLock();
1701
+ if (admission) break;
1702
+ await abortableDelay(DEFAULT_ADMISSION_RETRY_INTERVAL_MS, signal);
1703
+ }
1668
1704
  if (!admission) {
1669
1705
  throw new Error("native-agent run allocation is busy; retry without changing the request");
1670
1706
  }
@@ -1726,7 +1762,7 @@ export class NativeAgentCompositeTransport {
1726
1762
  ...(args.context !== undefined ? { context: args.context } : {}),
1727
1763
  contextKeys: args.contextKeys ?? [],
1728
1764
  }, prepared.agent);
1729
- state = this.allocateState(startArguments, { startMode: "answer" });
1765
+ state = await this.allocateState(startArguments, { startMode: "answer", signal: attachment.signal });
1730
1766
  lock = this.acquireLock(state.invocationId);
1731
1767
  if (!lock) return handleForState(state);
1732
1768
  const result = await this.attach(state, prepared, {
@@ -1770,7 +1806,7 @@ export class NativeAgentCompositeTransport {
1770
1806
  try {
1771
1807
  const prepared = await this.prepareNewSession(attachment.signal);
1772
1808
  const startArguments = this.normalizeRunArguments(args, prepared.agent);
1773
- state = this.allocateState(startArguments);
1809
+ state = await this.allocateState(startArguments, { signal: attachment.signal });
1774
1810
  // The durable identity and exact retry arguments exist before the first
1775
1811
  // upstream byte is sent.
1776
1812
  lock = this.acquireLock(state.invocationId);