@yagni-app/code-staging 1.2.2-staging.1530.1 → 1.2.2-staging.1555.1

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.
@@ -37,6 +37,8 @@ export type RunStageFn = (stage: PipelineStage, ctx: {
37
37
  lens?: ReviewLens;
38
38
  grounded?: boolean;
39
39
  }, deps: RunStageDeps) => Promise<StageResult>;
40
+ /** True when at least {@link MODEL_BOUND_SHARE} of a stage's elapsed time was spent outside tools. */
41
+ export declare function isModelBound(elapsedMs: number, toolMs: number): boolean;
40
42
  /** One telemetry record per attempt (routed through the orchestrator logger). */
41
43
  export interface ResilienceAttemptRecord {
42
44
  stageId: string;
@@ -54,6 +56,10 @@ export interface ResilienceAttemptRecord {
54
56
  outcome: "ok" | "transient" | "fatal" | "timeout" | "aborted";
55
57
  /** Which timer fired, when the attempt timed out. */
56
58
  timeoutKind?: "idle" | "wall";
59
+ /** On a timeout: ms the attempt spent with a tool running (the rest went to the model). */
60
+ toolMs?: number;
61
+ /** On a timeout: whether the model, not tools, held most of the attempt's time. */
62
+ modelBound?: boolean;
57
63
  exitCode: number;
58
64
  stopReason?: string;
59
65
  elapsedMs: number;
@@ -32,7 +32,34 @@ const WRITE_STAGE_IDS = ["implement", "fix"];
32
32
  /** Honest message stamped on a stage we aborted for exceeding its time budget. */
33
33
  const TIMEOUT_MESSAGE = "stage exceeded its idle or wall-clock timeout";
34
34
  const WALL_TIMEOUT_MESSAGE = "stage exceeded its wall-clock timeout";
35
+ /**
36
+ * The wall clock fired while most of the stage's time went to waiting on model
37
+ * responses rather than running tools: a slow model, not a stage too big for one
38
+ * pass. Keeps {@link WALL_TIMEOUT_MESSAGE} as its prefix so a reader that only
39
+ * knows the plain sentence still reads it as a wall-clock stop.
40
+ */
41
+ const WALL_TIMEOUT_MODEL_MESSAGE = `${WALL_TIMEOUT_MESSAGE} while waiting on the model`;
42
+ /**
43
+ * Share of a timed-out stage's elapsed time outside tools at which the model,
44
+ * not the work, used the clock. The non-tool time also holds the child's
45
+ * startup and its teardown after the abort (seconds), which is noise against a
46
+ * real wall window of many minutes.
47
+ */
48
+ const MODEL_BOUND_SHARE = 0.6;
35
49
  const IDLE_TIMEOUT_MESSAGE = "stage exceeded its idle timeout";
50
+ /** True when at least {@link MODEL_BOUND_SHARE} of a stage's elapsed time was spent outside tools. */
51
+ export function isModelBound(elapsedMs, toolMs) {
52
+ if (elapsedMs <= 0)
53
+ return false;
54
+ return (elapsedMs - toolMs) / elapsedMs >= MODEL_BOUND_SHARE;
55
+ }
56
+ function timeoutMessage(kind, modelBound) {
57
+ if (kind === "wall")
58
+ return modelBound ? WALL_TIMEOUT_MODEL_MESSAGE : WALL_TIMEOUT_MESSAGE;
59
+ if (kind === "idle")
60
+ return IDLE_TIMEOUT_MESSAGE;
61
+ return TIMEOUT_MESSAGE;
62
+ }
36
63
  /** stderr fingerprints of a transient transport/provider blip worth retrying. */
37
64
  const TRANSIENT_STDERR = /\b429\b|\b5\d\d\b|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket hang up/i;
38
65
  /**
@@ -117,6 +144,11 @@ export function withResilience(base, policy, opts = {}) {
117
144
  // and re-arms instead of aborting; the wall-clock timer stays the
118
145
  // backstop for a tool that is genuinely hung.
119
146
  let inFlightTools = 0;
147
+ // Time spent with at least one tool running. The rest of the stage's
148
+ // elapsed time is the model thinking or streaming, which is what tells a
149
+ // slow model apart from a stage that had too much work to do.
150
+ let toolMs = 0;
151
+ let toolsBusySince;
120
152
  let idleTimer;
121
153
  const armIdle = () => {
122
154
  if (idleTimer)
@@ -137,10 +169,18 @@ export function withResilience(base, policy, opts = {}) {
137
169
  const originalOnEvent = deps.onEvent;
138
170
  const onEvent = (ev) => {
139
171
  sawAnyEvent = true;
140
- if (ev.type === "tool_execution_start")
172
+ if (ev.type === "tool_execution_start") {
173
+ if (inFlightTools === 0)
174
+ toolsBusySince = now();
141
175
  inFlightTools += 1;
142
- else if (ev.type === "tool_execution_end")
176
+ }
177
+ else if (ev.type === "tool_execution_end") {
143
178
  inFlightTools = Math.max(0, inFlightTools - 1);
179
+ if (inFlightTools === 0 && toolsBusySince !== undefined) {
180
+ toolMs += now() - toolsBusySince;
181
+ toolsBusySince = undefined;
182
+ }
183
+ }
144
184
  armIdle(); // reset the idle window on every live event
145
185
  originalOnEvent?.(ev);
146
186
  };
@@ -154,6 +194,10 @@ export function withResilience(base, policy, opts = {}) {
154
194
  clearTimeout(idleTimer);
155
195
  }
156
196
  const elapsedMs = now() - startedAt;
197
+ // A tool still running when the stage ended counts up to now.
198
+ if (toolsBusySince !== undefined)
199
+ toolMs += now() - toolsBusySince;
200
+ const modelBound = isModelBound(elapsedMs, toolMs);
157
201
  const callerAborted = callerSignal?.aborted === true;
158
202
  const record = (outcome, willRetry) => {
159
203
  telemetry?.({
@@ -164,6 +208,7 @@ export function withResilience(base, policy, opts = {}) {
164
208
  attempt,
165
209
  outcome,
166
210
  ...(timedOut && timeoutKind ? { timeoutKind } : {}),
211
+ ...(timedOut ? { toolMs, modelBound } : {}),
167
212
  exitCode: last.exitCode,
168
213
  ...(last.stopReason ? { stopReason: last.stopReason } : {}),
169
214
  elapsedMs,
@@ -179,9 +224,8 @@ export function withResilience(base, policy, opts = {}) {
179
224
  record("ok", false);
180
225
  return last;
181
226
  }
182
- if (timedOut) {
183
- last.errorMessage = timeoutKind === "wall" ? WALL_TIMEOUT_MESSAGE : timeoutKind === "idle" ? IDLE_TIMEOUT_MESSAGE : TIMEOUT_MESSAGE;
184
- }
227
+ if (timedOut)
228
+ last.errorMessage = timeoutMessage(timeoutKind, modelBound);
185
229
  const transient = classifyTransient(last, timedOut);
186
230
  // A wall-clock exhaustion is a "task too big" signal, not a stall: a
187
231
  // lane that opts out (subagents) fails honestly instead of re-burning
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.2.2-staging.1530.1",
3
+ "version": "1.2.2-staging.1555.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -58,5 +58,5 @@
58
58
  "turndown": "^7.2.4",
59
59
  "typebox": "^1.3.15"
60
60
  },
61
- "yagniSourceSha": "cbe4409f1c4a9b1916438a3899957168c5c30ac5"
61
+ "yagniSourceSha": "8e8cae193cda58ced857dd7c5f832e83d29f3e5b"
62
62
  }