impel-cli 0.20.17 → 0.20.19

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,24 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.19 — Recovered Codex stream accounting
4
+
5
+ - Counts a zero-exit, completed Codex turn with a final agent message and one
6
+ native-agent start as successful even when the host reports transient stream
7
+ retry events before recovering.
8
+ - Records recovered error-event counts separately in private attempt records
9
+ and aggregate analysis instead of silently discarding the successful sample.
10
+ - Prevents generated Codex specialists from using collaboration messages to
11
+ report their terminal result, and refreshes already-fresh managed profiles
12
+ onto the tightened adapter instructions.
13
+
14
+ ## 0.20.18 — Recoverable Codex profiling cohorts
15
+
16
+ - Widens the bounded retry window for transient current-tenant reads while
17
+ still rejecting an exact tenant mismatch before a Codex attempt starts.
18
+ - Stops scheduling new work after an attempt-infrastructure failure, waits for
19
+ active workers to settle, and retains an explicitly incomplete private
20
+ aggregate so valid samples are not silently lost.
21
+
3
22
  ## 0.20.17 — Managed desktop task views
4
23
 
5
24
  - Enables the reviewed MCP Apps renderer only in exact managed Codex Desktop
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.17",
3
+ "version": "0.20.19",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -89,6 +89,7 @@ export function summarizeCodexEvents(events) {
89
89
  let outputTokens = 0;
90
90
  let turnCompleted = false;
91
91
  let errorEvents = 0;
92
+ let agentMessages = 0;
92
93
  let codeCells = 0;
93
94
  let codeWaits = 0;
94
95
 
@@ -102,6 +103,7 @@ export function summarizeCodexEvents(events) {
102
103
 
103
104
  const item = eventItem(event);
104
105
  const itemType = safeEventType(item.type);
106
+ if (type === "item.completed" && itemType === "agent_message") agentMessages += 1;
105
107
  const tool = eventTool(item);
106
108
  increment(toolCounts, tool);
107
109
  if (itemType && /(?:code_mode|command_execution|code_cell)/u.test(itemType)) codeCells += 1;
@@ -123,6 +125,7 @@ export function summarizeCodexEvents(events) {
123
125
  threadCount: threads.size,
124
126
  turnCompleted,
125
127
  errorEvents,
128
+ agentMessages,
126
129
  codeCells,
127
130
  codeWaits,
128
131
  inputTokens,
@@ -410,6 +413,8 @@ export function analyzeAggregates(aggregates) {
410
413
  timeout: attempts.filter((attempt) => attempt.timedOut).length,
411
414
  invalidJson: attempts.filter((attempt) => !attempt.jsonValid).length,
412
415
  descendantsRemaining: attempts.filter((attempt) => attempt.descendantsRemaining === true).length,
416
+ attemptsWithRecoveredErrors: successful.filter((attempt) => (attempt.errorEvents || 0) > 0).length,
417
+ recoveredErrorEvents: successful.reduce((total, attempt) => total + (attempt.errorEvents || 0), 0),
413
418
  },
414
419
  };
415
420
  report.gates = report.mode === "direct" ? directGate(report) : compatibleGate(report);
@@ -14,6 +14,7 @@ const PROFILE_SCHEMA = "impel.native-codex-profile.v1";
14
14
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
15
15
  const DEFAULT_GRACE_MS = 5_000;
16
16
  const MAX_CAPTURE_BYTES = 256 * 1024 * 1024;
17
+ const TENANT_PREFLIGHT_RETRY_DELAYS_MS = [250, 750, 1_500];
17
18
 
18
19
  function privateDirectory(directory) {
19
20
  if (fs.existsSync(directory) && fs.lstatSync(directory).isSymbolicLink()) {
@@ -211,16 +212,17 @@ function runCaptured(command, args, { environment = process.env, timeoutMs = 60_
211
212
  }
212
213
 
213
214
  async function currentTenant(impelBinary, environment) {
214
- for (let attempt = 0; attempt < 2; attempt += 1) {
215
+ for (let attempt = 0; attempt <= TENANT_PREFLIGHT_RETRY_DELAYS_MS.length; attempt += 1) {
215
216
  const invocation = impelInvocation(impelBinary, ["tenant", "current"], environment);
216
217
  const result = await runCaptured(invocation.command, invocation.args, { environment });
217
218
  const lines = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
218
219
  if (result.code === 0 && lines.length === 1 && /^[A-Za-z0-9_.:-]{1,160}$/u.test(lines[0])) {
219
220
  return lines[0];
220
221
  }
221
- if (attempt === 0) await delay(250);
222
+ const retryDelay = TENANT_PREFLIGHT_RETRY_DELAYS_MS[attempt];
223
+ if (retryDelay !== undefined) await delay(retryDelay);
222
224
  }
223
- throw new Error("could not establish one exact current Impel tenant after two bounded attempts");
225
+ throw new Error("could not establish one exact current Impel tenant after bounded attempts");
224
226
  }
225
227
 
226
228
  export async function assertExpectedTenant({ expectedTenant, impelBinary = "impel", environment = process.env }) {
@@ -372,7 +374,7 @@ function parseArguments(argv) {
372
374
  return { ...options, help: false };
373
375
  }
374
376
 
375
- function safeAttemptRecord({ options, index, processResult, summary, jsonValid, parseError }) {
377
+ export function safeAttemptRecord({ options, index, processResult, summary, jsonValid, parseError }) {
376
378
  const codex = summary?.codex || {};
377
379
  const telemetry = summary?.telemetry || {};
378
380
  const durationMs = processResult.endedAtMs - processResult.startedAtMs;
@@ -387,7 +389,7 @@ function safeAttemptRecord({ options, index, processResult, summary, jsonValid,
387
389
  && processResult.descendantsRemaining !== true
388
390
  && jsonValid
389
391
  && codex.turnCompleted === true
390
- && (codex.errorEvents || 0) === 0
392
+ && (codex.agentMessages || 0) >= 1
391
393
  && (telemetry.localFailures || 0) === 0
392
394
  && startCalls === 1;
393
395
  return {
@@ -410,6 +412,8 @@ function safeAttemptRecord({ options, index, processResult, summary, jsonValid,
410
412
  parseFailureClass: parseError ? "invalid-jsonl" : null,
411
413
  success,
412
414
  eventCount: codex.eventCount || 0,
415
+ agentMessages: codex.agentMessages || 0,
416
+ errorEvents: codex.errorEvents || 0,
413
417
  threadCount: codex.threadCount || 0,
414
418
  toolCounts: codex.toolCounts || {},
415
419
  parentToolCounts: codex.parentToolCounts || {},
@@ -495,19 +499,38 @@ async function profileAttempt(options, sessionDir, index) {
495
499
  return record;
496
500
  }
497
501
 
498
- async function runWorkers(options, sessionDir) {
502
+ function profilerFailureClass(error) {
503
+ if (/could not establish one exact current Impel tenant/u.test(error?.message || "")) {
504
+ return "tenant-preflight-unavailable";
505
+ }
506
+ if (/selected tenant .* does not match expected tenant/u.test(error?.message || "")) {
507
+ return "tenant-mismatch";
508
+ }
509
+ return "attempt-infrastructure";
510
+ }
511
+
512
+ export async function runWorkers(options, sessionDir, profileAttemptImpl = profileAttempt) {
499
513
  const results = new Array(options.attempts);
500
514
  let next = 0;
515
+ let abort = null;
501
516
  const worker = async () => {
502
517
  for (;;) {
518
+ if (abort) return;
503
519
  const index = next;
504
520
  next += 1;
505
521
  if (index >= options.attempts) return;
506
- results[index] = await profileAttempt(options, sessionDir, index);
522
+ try {
523
+ results[index] = await profileAttemptImpl(options, sessionDir, index);
524
+ } catch (error) {
525
+ abort ||= {
526
+ failureClass: profilerFailureClass(error),
527
+ attemptId: `${options.cohort}-${String(index + 1 + options.attemptOffset).padStart(3, "0")}`,
528
+ };
529
+ }
507
530
  }
508
531
  };
509
532
  await Promise.all(Array.from({ length: options.concurrency }, () => worker()));
510
- return results;
533
+ return { attempts: results.filter(Boolean), abort };
511
534
  }
512
535
 
513
536
  async function restoreTenant(originalTenant, options) {
@@ -553,7 +576,7 @@ async function main(argv) {
553
576
  privateDirectory(sessionDir);
554
577
  let restored = false;
555
578
  try {
556
- const attempts = await runWorkers(options, sessionDir);
579
+ const result = await runWorkers(options, sessionDir);
557
580
  const aggregate = {
558
581
  schema: PROFILE_SCHEMA,
559
582
  sessionId,
@@ -564,12 +587,18 @@ async function main(argv) {
564
587
  cohort: options.cohort,
565
588
  cliVersion: options.hostBuild,
566
589
  promptSha256: options.promptSha256,
567
- attempts,
590
+ complete: result.abort === null && result.attempts.length === options.attempts,
591
+ expectedAttempts: options.attempts,
592
+ abort: result.abort,
593
+ attempts: result.attempts,
568
594
  };
569
595
  const aggregatePath = path.join(sessionDir, "aggregate.json");
570
596
  privateWrite(aggregatePath, `${JSON.stringify(aggregate, null, 2)}\n`);
571
597
  process.stderr.write(`Private Codex profile written under ${sessionDir}\n`);
572
598
  process.stdout.write(`${JSON.stringify(aggregate)}\n`);
599
+ if (!aggregate.complete) {
600
+ throw new Error(`cohort aborted (${aggregate.abort?.failureClass || "incomplete"}); partial aggregate retained`);
601
+ }
573
602
  } finally {
574
603
  restored = await restoreTenant(originalTenant, options);
575
604
  if (!restored) process.stderr.write("profile-native-codex: could not verify restoration of the original tenant\n");
package/src/agents.js CHANGED
@@ -52,7 +52,7 @@ export const NATIVE_AGENT_RUN_TOOL = "run_native_agent";
52
52
  export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
53
53
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
54
54
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
55
- export const MANAGED_AGENT_MANIFEST_VERSION = 10;
55
+ export const MANAGED_AGENT_MANIFEST_VERSION = 11;
56
56
 
57
57
  const NATIVE_AGENT_TOOL_NAMES = [
58
58
  NATIVE_AGENT_RUN_TOOL,
@@ -650,6 +650,7 @@ function codexAdapterInstructions(tenantId, agent) {
650
650
  const callerGuidance = usesVerbatimRelay(agent)
651
651
  ? adapterCallerSpawnGuidance("Codex")
652
652
  : null;
653
+ const collaborationConstraint = `Never call spawn_agent, wait_agent, send_message, followup_task, interrupt_agent, or any other collaboration tool. Communicate with the caller only by returning one final response after the native-agent transport reaches terminal.`;
653
654
  if (usesDirectAnswer(agent)) {
654
655
  const completionGuidance = usesVerbatimRelay(agent)
655
656
  ? adapterAnswerVerbatimCompletionGuidance()
@@ -657,6 +658,7 @@ function codexAdapterInstructions(tenantId, agent) {
657
658
  return [
658
659
  `You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
659
660
  ...(callerGuidance ? [callerGuidance] : []),
661
+ collaborationConstraint,
660
662
  `Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize the result.`,
661
663
  `Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before answering.${contextRequirement}`,
662
664
  sideEffectInstruction,
@@ -671,6 +673,7 @@ function codexAdapterInstructions(tenantId, agent) {
671
673
  return [
672
674
  `You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
673
675
  ...(callerGuidance ? [callerGuidance] : []),
676
+ collaborationConstraint,
674
677
  `Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize or rewrite the result.`,
675
678
  `Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before starting.${contextRequirement}`,
676
679
  sideEffectInstruction,
@@ -683,6 +686,7 @@ function codexAdapterInstructions(tenantId, agent) {
683
686
  function retiredAdapterInstructions(tenantId, agent) {
684
687
  return [
685
688
  `You are a recovery-only transport adapter for retired Impel native-agent binding ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
689
+ `Never call spawn_agent, wait_agent, send_message, followup_task, interrupt_agent, or any other collaboration tool. Communicate with the caller only by returning one final response after the native-agent transport reaches terminal.`,
686
690
  `You cannot start new work. Never call run_native_agent and never perform or recreate the assigned task yourself.`,
687
691
  `Call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} to obtain integrity-validated pending handles for this fixed binding, then call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with the intended handle until it returns a terminal result. If multiple handles cannot be safely associated with the request, report them instead of choosing.`,
688
692
  `Return successful finalText byte-for-byte. For failure, return the preserved runId, output, and error without inventing a replacement result.`,