impel-cli 0.20.26 → 0.20.28

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,15 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.28 — Exact opaque answer resumes
4
+
5
+ - Aligns generated Claude and Codex direct-answer adapters with the opaque
6
+ `impel.native-agent-continuation.v1` contract instead of telling them to
7
+ reconstruct the durable fingerprint-bearing handle.
8
+ - Restricts the answer-only resume tool schema to the exact two-field opaque
9
+ continuation and regenerates existing managed agent profiles on update.
10
+ - Keeps durable handle integrity checks strict and rejects altered metadata
11
+ rather than restoring the deprecated direct-answer shape.
12
+
3
13
  ## 0.20.26 — Native task-board navigation
4
14
 
5
15
  - Embeds Tasks in each managed desktop app's content view instead of a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.26",
3
+ "version": "0.20.28",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -144,6 +144,27 @@ function rolloutSessionMetadata(events) {
144
144
  } : { threadId: null, parentThreadId: null };
145
145
  }
146
146
 
147
+ function nativeTerminalStatus(output) {
148
+ if (typeof output !== "string") return null;
149
+ const objectStart = output.indexOf("{");
150
+ if (objectStart < 0) return null;
151
+ let value;
152
+ try {
153
+ value = JSON.parse(output.slice(objectStart));
154
+ } catch {
155
+ return null;
156
+ }
157
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
158
+ if (value.schema === "impel.native-agent-answer.v1" && value.status === "succeeded") {
159
+ return "succeeded";
160
+ }
161
+ if (value.schema === "impel.native-agent-result.v1"
162
+ && ["succeeded", "failed", "cancelled", "canceled"].includes(value.status)) {
163
+ return value.status;
164
+ }
165
+ return null;
166
+ }
167
+
147
168
  export function summarizeCodexRollouts(rollouts) {
148
169
  const toolCounts = {};
149
170
  const parentToolCounts = {};
@@ -156,6 +177,10 @@ export function summarizeCodexRollouts(rollouts) {
156
177
  let outputTokens = 0;
157
178
  let mcpDurationMs = 0;
158
179
  let mcpCompletedCalls = 0;
180
+ let terminalResults = 0;
181
+ let successfulTerminalResults = 0;
182
+ let parentSuccessfulTerminalResults = 0;
183
+ let childSuccessfulTerminalResults = 0;
159
184
 
160
185
  for (const events of rollouts) {
161
186
  eventCount += events.length;
@@ -194,6 +219,17 @@ export function summarizeCodexRollouts(rollouts) {
194
219
  mcpStarts.delete(payload.call_id);
195
220
  }
196
221
  }
222
+ if (event.type === "response_item" && payload.type === "function_call_output") {
223
+ const terminalStatus = nativeTerminalStatus(payload.output);
224
+ if (terminalStatus) {
225
+ terminalResults += 1;
226
+ if (terminalStatus === "succeeded") {
227
+ successfulTerminalResults += 1;
228
+ if (metadata.parentThreadId) childSuccessfulTerminalResults += 1;
229
+ else parentSuccessfulTerminalResults += 1;
230
+ }
231
+ }
232
+ }
197
233
  if (event.type === "event_msg" && payload.type === "token_count") {
198
234
  const usage = payload.info?.total_token_usage;
199
235
  if (usage && typeof usage === "object") latestUsage = usage;
@@ -219,6 +255,10 @@ export function summarizeCodexRollouts(rollouts) {
219
255
  outputTokens,
220
256
  mcpDurationMs,
221
257
  mcpCompletedCalls,
258
+ terminalResults,
259
+ successfulTerminalResults,
260
+ parentSuccessfulTerminalResults,
261
+ childSuccessfulTerminalResults,
222
262
  };
223
263
  }
224
264
 
@@ -281,6 +321,10 @@ export function summarizeRawAttempt({ stdoutText, telemetryText = "", rolloutTex
281
321
  outputTokens: rollout.outputTokens,
282
322
  mcpDurationMs: rollout.mcpDurationMs,
283
323
  mcpCompletedCalls: rollout.mcpCompletedCalls,
324
+ terminalResults: rollout.terminalResults,
325
+ successfulTerminalResults: rollout.successfulTerminalResults,
326
+ parentSuccessfulTerminalResults: rollout.parentSuccessfulTerminalResults,
327
+ childSuccessfulTerminalResults: rollout.childSuccessfulTerminalResults,
284
328
  } : {}),
285
329
  parentToolCounts: rollout.parentToolCounts,
286
330
  childToolCounts: rollout.childToolCounts,
@@ -336,6 +380,9 @@ function directGate(report) {
336
380
  structure: report.structure.maximumThreadCount <= 1
337
381
  && report.structure.totalCodeCells === 0
338
382
  && report.structure.totalCodeWaits === 0
383
+ && report.structure.successfulTerminalResults === report.successfulSamples
384
+ && report.structure.parentSuccessfulTerminalResults === report.successfulSamples
385
+ && report.structure.childSuccessfulTerminalResults === 0
339
386
  && ["spawn_agent", "wait_agent", "send_message", "exec", "wait", "tool_search", "ALL_TOOLS"]
340
387
  .every((tool) => !report.structure.allToolCounts[tool])
341
388
  && (report.structure.toolCounts.answer_native_agent || 0) === report.successfulSamples
@@ -350,6 +397,8 @@ function compatibleGate(report) {
350
397
  && report.timings.durationMs.p50 <= (report.sloClass === "cibi" ? 55_000 : 100_000),
351
398
  childStructure: report.structure.totalCodeCells === 0
352
399
  && report.structure.totalCodeWaits === 0
400
+ && report.structure.successfulTerminalResults === report.successfulSamples
401
+ && report.structure.childSuccessfulTerminalResults === report.successfulSamples
353
402
  && ["exec", "wait", "send_message", "tool_search", "ALL_TOOLS"]
354
403
  .every((tool) => !report.structure.childToolCounts[tool]),
355
404
  };
@@ -405,6 +454,19 @@ export function analyzeAggregates(aggregates) {
405
454
  allToolCounts: mergedCounts(attempts, "toolCounts"),
406
455
  parentToolCounts: mergedCounts(successful, "parentToolCounts"),
407
456
  childToolCounts: mergedCounts(successful, "childToolCounts"),
457
+ terminalResults: successful.reduce((total, attempt) => total + (attempt.terminalResults || 0), 0),
458
+ successfulTerminalResults: successful.reduce(
459
+ (total, attempt) => total + (attempt.successfulTerminalResults || 0),
460
+ 0,
461
+ ),
462
+ parentSuccessfulTerminalResults: successful.reduce(
463
+ (total, attempt) => total + (attempt.parentSuccessfulTerminalResults || 0),
464
+ 0,
465
+ ),
466
+ childSuccessfulTerminalResults: successful.reduce(
467
+ (total, attempt) => total + (attempt.childSuccessfulTerminalResults || 0),
468
+ 0,
469
+ ),
408
470
  maximumThreadCount: successful.reduce((maximum, attempt) => Math.max(maximum, attempt.threadCount || 0), 0),
409
471
  totalCodeCells: successful.reduce((total, attempt) => total + (attempt.codeCells || 0), 0),
410
472
  totalCodeWaits: successful.reduce((total, attempt) => total + (attempt.codeWaits || 0), 0),
@@ -390,6 +390,7 @@ export function safeAttemptRecord({ options, index, processResult, summary, json
390
390
  && jsonValid
391
391
  && codex.turnCompleted === true
392
392
  && (codex.agentMessages || 0) >= 1
393
+ && (codex.successfulTerminalResults || 0) === 1
393
394
  && (telemetry.localFailures || 0) === 0
394
395
  && startCalls === 1;
395
396
  return {
@@ -420,6 +421,10 @@ export function safeAttemptRecord({ options, index, processResult, summary, json
420
421
  childToolCounts: codex.childToolCounts || {},
421
422
  codeCells: codex.codeCells || 0,
422
423
  codeWaits: codex.codeWaits || 0,
424
+ terminalResults: codex.terminalResults || 0,
425
+ successfulTerminalResults: codex.successfulTerminalResults || 0,
426
+ parentSuccessfulTerminalResults: codex.parentSuccessfulTerminalResults || 0,
427
+ childSuccessfulTerminalResults: codex.childSuccessfulTerminalResults || 0,
423
428
  inputTokens: codex.inputTokens || 0,
424
429
  cachedInputTokens: codex.cachedInputTokens || 0,
425
430
  outputTokens: codex.outputTokens || 0,
package/src/agents.js CHANGED
@@ -16,6 +16,8 @@ import {
16
16
  redactSecretText,
17
17
  } from "./config.js";
18
18
  import {
19
+ CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS,
20
+ IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
19
21
  IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
20
22
  IMPEL_NATIVE_AGENT_MCP_TARGET,
21
23
  impelNativeAgentMcpInvocation,
@@ -51,8 +53,9 @@ export const NATIVE_AGENT_ANSWER_TOOL = "answer_native_agent";
51
53
  export const NATIVE_AGENT_RUN_TOOL = "run_native_agent";
52
54
  export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
53
55
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
56
+ export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
54
57
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
55
- export const MANAGED_AGENT_MANIFEST_VERSION = 16;
58
+ export const MANAGED_AGENT_MANIFEST_VERSION = 18;
56
59
 
57
60
  // The host model only selects the fixed MCP tool and faithfully returns its
58
61
  // result. Luna preserves deterministic direct-only code-mode routing while the
@@ -618,7 +621,7 @@ function claudeAdapterInstructions(tenantId, agent) {
618
621
  `Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
619
622
  sideEffectInstruction,
620
623
  `Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
621
- `If the bounded answer returns an ${JSON.stringify("impel.native-agent-run.v1")} continuation handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that handle until terminal. Never call answer_native_agent again for this request.`,
624
+ `If the bounded answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal. Never change its schema or add handle metadata. Never call answer_native_agent again for this request.`,
622
625
  completionGuidance,
623
626
  ].join(" ");
624
627
  }
@@ -668,7 +671,7 @@ function codexAdapterInstructions(tenantId, agent) {
668
671
  `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}`,
669
672
  sideEffectInstruction,
670
673
  `Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
671
- `If the bounded answer returns an ${JSON.stringify("impel.native-agent-run.v1")} continuation handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that handle until terminal. Never call answer_native_agent again for this request.`,
674
+ `If the bounded answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal. Never change its schema or add handle metadata. Never call answer_native_agent again for this request.`,
672
675
  completionGuidance,
673
676
  ].join("\n\n");
674
677
  }
@@ -748,9 +751,11 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
748
751
 
749
752
  function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
750
753
  const entries = Object.entries(invocation.env || {});
751
- if (!durableProfile) return entries;
752
754
  const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
753
- return entries.filter(([key]) => !transient.has(key));
755
+ return [
756
+ ...(durableProfile ? entries.filter(([key]) => !transient.has(key)) : entries),
757
+ [IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV, String(CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS)],
758
+ ];
754
759
  }
755
760
 
756
761
  function renderCodexConfiguration({
@@ -800,6 +805,7 @@ function renderCodexConfiguration({
800
805
  `[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
801
806
  `command = ${JSON.stringify(invocation.command)}`,
802
807
  `args = [${invocation.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
808
+ "tool_timeout_sec = 120",
803
809
  ...(eager
804
810
  ? [`enabled_tools = [${toolNames.map((tool) => JSON.stringify(tool)).join(", ")}]`]
805
811
  : []),
@@ -10,6 +10,7 @@ import {
10
10
  } from "./config.js";
11
11
  import {
12
12
  NATIVE_AGENT_ANSWER_TOOL,
13
+ NATIVE_AGENT_CONTINUATION_SCHEMA,
13
14
  NATIVE_AGENT_GET_TOOL,
14
15
  NATIVE_AGENT_READ_TOOL,
15
16
  NATIVE_AGENT_RECOVER_TOOL,
@@ -21,17 +22,20 @@ import {
21
22
  normalizeNativeAgentCatalog,
22
23
  } from "./agents.js";
23
24
  import { extractAnswerFinalText } from "./directAnswer.js";
25
+ import {
26
+ IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
27
+ } from "./selfInvocation.js";
24
28
  import { normalizeTenantId } from "./tenants.js";
25
29
  import { renameWithWindowsRetry } from "./windowsFs.js";
26
30
 
27
31
  export {
28
32
  NATIVE_AGENT_ANSWER_TOOL,
33
+ NATIVE_AGENT_CONTINUATION_SCHEMA,
29
34
  NATIVE_AGENT_RECOVER_TOOL,
30
35
  NATIVE_AGENT_RESUME_TOOL,
31
36
  NATIVE_AGENT_RUN_TOOL,
32
37
  };
33
38
  export const NATIVE_AGENT_HANDLE_SCHEMA = "impel.native-agent-run.v1";
34
- export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
35
39
  export const NATIVE_AGENT_RESULT_SCHEMA = "impel.native-agent-result.v1";
36
40
  export const NATIVE_AGENT_RECOVERY_SCHEMA = "impel.native-agent-recovery.v1";
37
41
 
@@ -62,6 +66,19 @@ const CTOS_START_TIMEOUT_MESSAGE = `MCP tool call failed for ${NATIVE_AGENT_STAR
62
66
  const MCP_TOOL_RESULT_ERROR = Symbol("native-agent MCP tool result error");
63
67
  const TERMINAL_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
64
68
 
69
+ export function nativeAgentAttachmentWindowMs(environment = process.env) {
70
+ const configured = environment?.[IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV];
71
+ if (configured === undefined) return DEFAULT_ATTACHMENT_WINDOW_MS;
72
+ if (!/^\d{1,6}$/u.test(configured)) {
73
+ throw new Error(`${IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV} must be an integer from 1000 through 300000`);
74
+ }
75
+ const parsed = Number(configured);
76
+ if (!Number.isSafeInteger(parsed) || parsed < 1_000 || parsed > 300_000) {
77
+ throw new Error(`${IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV} must be an integer from 1000 through 300000`);
78
+ }
79
+ return parsed;
80
+ }
81
+
65
82
  function stableValue(value) {
66
83
  if (Array.isArray(value)) return value.map(stableValue);
67
84
  if (!value || typeof value !== "object") return value;
@@ -773,7 +790,7 @@ export class NativeAgentCompositeTransport {
773
790
  gatewayUrl,
774
791
  credential,
775
792
  runsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
776
- attachmentWindowMs = DEFAULT_ATTACHMENT_WINDOW_MS,
793
+ attachmentWindowMs = nativeAgentAttachmentWindowMs(),
777
794
  maxPolls = DEFAULT_MAX_POLLS,
778
795
  now = Date.now,
779
796
  randomUUID = crypto.randomUUID,
@@ -2204,11 +2221,8 @@ const ANSWER_RESUME_SCHEMA = {
2204
2221
  additionalProperties: false,
2205
2222
  required: ["schema", "invocationId"],
2206
2223
  properties: {
2207
- ...HANDLE_SCHEMA.properties,
2208
- schema: {
2209
- type: "string",
2210
- enum: [NATIVE_AGENT_CONTINUATION_SCHEMA, NATIVE_AGENT_HANDLE_SCHEMA],
2211
- },
2224
+ schema: { type: "string", const: NATIVE_AGENT_CONTINUATION_SCHEMA },
2225
+ invocationId: { type: "string" },
2212
2226
  },
2213
2227
  };
2214
2228
 
@@ -72,6 +72,8 @@ export const IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES = [
72
72
  "IMPEL_NATIVE_HOST",
73
73
  "IMPEL_NATIVE_HOST_BUILD",
74
74
  ];
75
+ export const IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV = "IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_MS";
76
+ export const CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS = 100_000;
75
77
 
76
78
  function managedMcpEnvironment(environment = process.env) {
77
79
  const telemetry = Object.fromEntries(