@lmctl-ai/lmctl 0.2.10 → 0.2.12

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.
@@ -162,8 +162,14 @@ export interface TurnResult {
162
162
  /** "ok" (default/omitted) = normal completion.
163
163
  * "stalled" = idle timeout fired — provider went silent for the
164
164
  * configured duration. The child process is still alive. The text
165
- * field contains whatever was produced before the stall. */
166
- status?: "ok" | "stalled";
165
+ * field contains whatever was produced before the stall.
166
+ * "empty" = ACP transports only (see transport-wrappers.ts's
167
+ * emptyTurnStatus): the provider's own protocol reported a normal
168
+ * completion, but produced no text and no observed activity -- almost
169
+ * always an upstream failure (rate limit, quota, auth) that the
170
+ * provider's ACP layer swallowed instead of surfacing as an error.
171
+ * Never set for non-ACP transports (stream-json, codex-app-server). */
172
+ status?: "ok" | "stalled" | "empty";
167
173
  /** When status === "stalled": how long the provider was idle (ms). */
168
174
  stalledAfterMs?: number;
169
175
  /** Human-readable strings when this turn's provider silently dropped a
@@ -70,6 +70,12 @@ export interface BundleTurnResult {
70
70
  * provider configuration is applied once at open, not per spawn.
71
71
  */
72
72
  warnings?: string[];
73
+ /**
74
+ * ACP bundle sessions only (see AcpOutputProcessorResult.sawActivity) --
75
+ * whether this turn saw any real agent activity, as opposed to a clean
76
+ * but empty completion. Omitted for non-ACP bundle implementations.
77
+ */
78
+ sawActivity?: boolean;
73
79
  }
74
80
  export interface BundleSession {
75
81
  /** Provider key this session serves (e.g. "opencode"). */
@@ -124,7 +124,7 @@ export interface ChatResult {
124
124
  durationMs: number;
125
125
  stopReason?: string;
126
126
  numTurns?: number;
127
- status?: "ok" | "stalled";
127
+ status?: "ok" | "stalled" | "empty";
128
128
  stalledAfterMs?: number;
129
129
  /** Human-readable strings when this turn's provider silently dropped a
130
130
  * declared model=/effort= (task #232). Omitted when nothing was dropped. */
@@ -9,11 +9,19 @@ export interface AcpResult {
9
9
  * rejection the turn proceeded past anyway. Omitted when nothing was
10
10
  * dropped. */
11
11
  warnings?: string[];
12
+ /** See AcpOutputProcessorResult.sawActivity. */
13
+ sawActivity: boolean;
12
14
  }
13
15
  export interface AcpOutputProcessorResult {
14
16
  result: string;
15
17
  sessionId: string;
16
18
  stopReason: string;
19
+ /** Whether THIS turn saw any real agent activity (message/thought text,
20
+ * a tool call or its result, a plan) -- as opposed to pure session
21
+ * bookkeeping (session_info_update, current_mode_update,
22
+ * config_option_update, available_commands_update, usage_update,
23
+ * user_message_chunk) or nothing at all. See ACP_MEANINGFUL_UPDATE_KINDS. */
24
+ sawActivity: boolean;
17
25
  }
18
26
  export interface AcpOutputProcessor {
19
27
  setSessionId(sessionId: string): void;
@@ -32,6 +40,22 @@ export interface AcpOutputProcessor {
32
40
  export declare function isolateGeminiLatestAgentMessage(text: string): string;
33
41
  export declare function acpSessionIdFromResponse(response: Record<string, unknown>, fallback?: string): string;
34
42
  export declare function acpStopReasonFromResponse(response: Record<string, unknown>): string;
43
+ /**
44
+ * `sessionUpdate` kinds that represent real agent work, grounded in actual
45
+ * production ACP traffic (checked against a full day's ~/.lmctl/debug-*.log,
46
+ * 2026-09-29) plus the ACP spec's own "plan" kind (Zed and friends). Every
47
+ * OTHER kind seen in that traffic -- session_info_update, current_mode_
48
+ * update, config_option_update, available_commands_update, usage_update,
49
+ * user_message_chunk (an echo of the user's OWN input, not agent work) -- is
50
+ * pure session bookkeeping, and any future/unrecognized kind is treated the
51
+ * same conservative way: NOT activity. This is a whitelist, not a blacklist,
52
+ * on purpose -- the failure mode of missing a real silent-failure turn is
53
+ * bad, but the failure mode of flagging a legitimate turn as broken is
54
+ * worse (it would fire fleet-wide, for every provider, on every genuinely
55
+ * empty-but-fine turn), so activity only counts when there is positive
56
+ * evidence of it.
57
+ */
58
+ export declare const ACP_MEANINGFUL_UPDATE_KINDS: Set<string>;
35
59
  export declare function createAcpOutputProcessor(opts?: {
36
60
  tool?: string;
37
61
  onDelta?: (text: string) => void;
@@ -155,6 +155,8 @@ export interface AcpSessionTurnResult {
155
155
  * profile's sessionSetup runs once at open, not per spawn.
156
156
  */
157
157
  warnings?: string[];
158
+ /** See AcpOutputProcessorResult.sawActivity. */
159
+ sawActivity: boolean;
158
160
  }
159
161
  /**
160
162
  * An open ACP bundle session: one provider process serving many prompts.
@@ -413,7 +413,29 @@ export declare class Environment {
413
413
  readonly id: number;
414
414
  private readonly values;
415
415
  readonly parent?: Environment;
416
- constructor(parent?: Environment);
416
+ /**
417
+ * True only for the environment a function CALL creates (invokeNamed's
418
+ * fnEnv) -- never for a plain nested block (if/while/for body). Marks
419
+ * where `set()`'s upward search for an EXISTING binding must stop.
420
+ *
421
+ * Real incident, 2026-09-29 (operator finding): `fn f(x) { outer = 1; }`
422
+ * silently mutated an outer/global `outer` whenever one already existed,
423
+ * purely because the name collided -- "dynamic scoping with implicit
424
+ * global mutation," math's words, and no `local`/`my`/`let` to defend
425
+ * against it (a *working* `let` already existed in the grammar, but was
426
+ * entirely undocumented, which is why it went unnoticed). A name that
427
+ * happens to already exist as a global is not evidence the function
428
+ * meant to reference it. `set()` now treats a name not yet bound WITHIN
429
+ * the current function's own scope chain as a fresh function-local,
430
+ * matching every language cited for comparison (Perl's `my`, JS's
431
+ * function/`let` scoping, Python's function-scope-by-default) --
432
+ * without this flag, an if/while/for block's `set()` calls would ALSO
433
+ * stop at their own Environment, breaking ordinary control flow (e.g.
434
+ * `if (true) { x = 2; }` updating a `x` declared earlier in the SAME
435
+ * function must keep working).
436
+ */
437
+ readonly isFunctionBoundary: boolean;
438
+ constructor(parent?: Environment, isFunctionBoundary?: boolean);
417
439
  resolveBinding(name: string): {
418
440
  env: Environment;
419
441
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lmctl-ai/lmctl",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "A provider-agnostic control plane for teams of AI coding agents — across providers, with independent review and durable memory.",
5
5
  "type": "module",
6
6
  "homepage": "https://lmctl.com",