@thegitai/cli 1.0.0-preview.39 → 1.0.0-preview.40

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.
@@ -4,7 +4,7 @@ import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '..
4
4
  import { executeLocalToolCall } from '../tool-executor.js';
5
5
  import { saveGeneratedImage } from '../tools/save-generated-image.js';
6
6
  import { isUserInputQuestionArray } from './contracts.js';
7
- import { createTraceContext, gatewayFailureCategory, normalizeServerUrl, readErrorResponse, } from './http.js';
7
+ import { createTraceContext, gatewayFailureCategory, transportFailureDetail, normalizeServerUrl, readErrorResponse, } from './http.js';
8
8
  import { collectClientEnvironment } from '../client-environment.js';
9
9
  import { collectProjectOrientation } from '../project-orientation.js';
10
10
  import { describeSessionImageStore } from '../core/session-image-store.js';
@@ -126,6 +126,18 @@ function preserveFailedTurnInput(session, input, category) {
126
126
  });
127
127
  appendTurnFailureMarker(session, category);
128
128
  }
129
+ function tagTurnPhase(phase, run) {
130
+ return run().catch((error) => {
131
+ if (error && typeof error === 'object' && !error.thegitaiPhase) {
132
+ error.thegitaiPhase = phase;
133
+ }
134
+ throw error;
135
+ });
136
+ }
137
+ function turnPhaseOf(error) {
138
+ const phase = error?.thegitaiPhase;
139
+ return typeof phase === 'string' ? phase : null;
140
+ }
129
141
  function historyHasToolCall(session, callId) {
130
142
  return session.history.some((entry) => (entry.parts ?? []).some((part) => String(part?.functionCall?.id ?? '') === callId));
131
143
  }
@@ -216,7 +228,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
216
228
  toolStateSeq: ++toolStateSeqCounter,
217
229
  };
218
230
  const trace = createTraceContext(traceId);
219
- const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
231
+ const response = await tagTurnPhase('posting a tool result', () => fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
220
232
  method: 'POST',
221
233
  headers: {
222
234
  authorization: `Bearer ${config.token}`,
@@ -224,7 +236,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
224
236
  ...trace.headers,
225
237
  },
226
238
  body: JSON.stringify(payload),
227
- });
239
+ }));
228
240
  if (response.status === 410) {
229
241
  return;
230
242
  }
@@ -566,7 +578,7 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
566
578
  signal?.addEventListener('abort', preserveOnAbort, { once: true });
567
579
  }
568
580
  try {
569
- const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
581
+ const response = await tagTurnPhase('opening the turn', () => fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
570
582
  method: 'POST',
571
583
  headers: {
572
584
  accept: 'text/event-stream',
@@ -576,11 +588,11 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
576
588
  },
577
589
  body: JSON.stringify(request),
578
590
  signal,
579
- });
591
+ }));
580
592
  if (!response.ok) {
581
593
  throw await readErrorResponse(response, trace.traceId);
582
594
  }
583
- const result = await consumeTurnStream({
595
+ const result = await tagTurnPhase('reading the turn stream', () => consumeTurnStream({
584
596
  response,
585
597
  config,
586
598
  session,
@@ -590,7 +602,7 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
590
602
  traceId: trace.traceId,
591
603
  onTurnStart,
592
604
  onInterjectionDelivered,
593
- });
605
+ }));
594
606
  applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
595
607
  return {
596
608
  text: result.text,
@@ -606,6 +618,11 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
606
618
  ? error
607
619
  : new TurnCancelledError();
608
620
  }
621
+ const transportDetail = error instanceof ChatTurnFailedError ? null : transportFailureDetail(error);
622
+ if (transportDetail && error instanceof Error) {
623
+ const phase = turnPhaseOf(error) ?? 'reaching the server';
624
+ error.message = `connection lost while ${phase}: ${transportDetail} [trace ${trace.traceId}]`;
625
+ }
609
626
  const category = error instanceof ChatTurnFailedError
610
627
  ? error.category
611
628
  :
@@ -126,7 +126,65 @@ export function nonJsonErrorMessage(body, status) {
126
126
  const base = gateway ?? (summary || `Request failed with ${status}`);
127
127
  return rayId ? `${base} (Cloudflare Ray ID: ${rayId})` : base;
128
128
  }
129
+ const TRANSPORT_CODES = new Set([
130
+ 'ECONNRESET',
131
+ 'ECONNREFUSED',
132
+ 'ECONNABORTED',
133
+ 'EPIPE',
134
+ 'ETIMEDOUT',
135
+ 'ENOTFOUND',
136
+ 'EAI_AGAIN',
137
+ 'EHOSTUNREACH',
138
+ 'ENETUNREACH',
139
+ 'EPROTO',
140
+ 'ERR_STREAM_PREMATURE_CLOSE',
141
+ ]);
142
+ const TRANSPORT_PHRASES = [
143
+ [/\bsocket hang up\b/i, 'socket hang up'],
144
+ [/\bother side closed\b/i, 'other side closed'],
145
+ [/\bpremature close\b/i, 'premature close'],
146
+ [/\bterminated\b/i, 'terminated'],
147
+ [/\bfetch failed\b/i, 'fetch failed'],
148
+ [/\bnetwork timeout\b/i, 'network timeout'],
149
+ [/\bnetwork error\b/i, 'network error'],
150
+ [/\bheaders timeout\b/i, 'headers timeout'],
151
+ [/\bbody timeout\b/i, 'body timeout'],
152
+ ];
153
+ export function transportFailureDetail(error) {
154
+ if (error instanceof ServerApiError)
155
+ return null;
156
+ const parts = [];
157
+ const seen = new Set();
158
+ const queue = [error];
159
+ while (queue.length && seen.size < 24) {
160
+ const current = queue.shift();
161
+ if (!current || typeof current !== 'object' || seen.has(current))
162
+ continue;
163
+ seen.add(current);
164
+ const code = String(current.code ?? '');
165
+ if (TRANSPORT_CODES.has(code) || /^UND_ERR_[A-Z_]+$/.test(code)) {
166
+ if (!parts.includes(code))
167
+ parts.push(code);
168
+ }
169
+ const message = String(current.message ?? '');
170
+ for (const [pattern, label] of TRANSPORT_PHRASES) {
171
+ if (pattern.test(message) && !parts.includes(label))
172
+ parts.push(label);
173
+ }
174
+ if (current.cause)
175
+ queue.push(current.cause);
176
+ const nested = current.errors;
177
+ if (Array.isArray(nested))
178
+ queue.push(...nested);
179
+ }
180
+ return parts.length ? parts.slice(0, 3).join(' <- ') : null;
181
+ }
182
+ function isTransportFailure(error) {
183
+ return transportFailureDetail(error) !== null;
184
+ }
129
185
  export function gatewayFailureCategory(error) {
186
+ if (isTransportFailure(error))
187
+ return 'connection_error';
130
188
  if (!(error instanceof ServerApiError))
131
189
  return null;
132
190
  return error.status in GATEWAY_STATUS_MESSAGES ? 'gateway_error' : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.39",
3
+ "version": "1.0.0-preview.40",
4
4
  "description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
5
5
  "keywords": [
6
6
  "agentic-ai",
@@ -44,11 +44,11 @@
44
44
  "@lydell/node-pty-linux-x64": "1.1.0",
45
45
  "@lydell/node-pty-win32-arm64": "1.1.0",
46
46
  "@lydell/node-pty-win32-x64": "1.1.0",
47
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.39",
48
- "@thegitai/tui-darwin-x64": "1.0.0-preview.39",
49
- "@thegitai/tui-linux-arm64": "1.0.0-preview.39",
50
- "@thegitai/tui-linux-x64": "1.0.0-preview.39",
51
- "@thegitai/tui-win32-x64": "1.0.0-preview.39",
47
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.40",
48
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.40",
49
+ "@thegitai/tui-linux-arm64": "1.0.0-preview.40",
50
+ "@thegitai/tui-linux-x64": "1.0.0-preview.40",
51
+ "@thegitai/tui-win32-x64": "1.0.0-preview.40",
52
52
  "@vscode/ripgrep": "1.18.0"
53
53
  },
54
54
  "repository": {