@yagni-app/code-staging 1.1.3-staging.1378.1 → 1.1.3-staging.1382.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.
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* `backend/src/yagniCode/crashReports.ts`. Spec:
|
|
25
25
|
* docs/superpowers/specs/2026-08-08-crash-reporting-design.md
|
|
26
26
|
*/
|
|
27
|
+
import { type SinkEvent } from "./errorSink.js";
|
|
27
28
|
export declare const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
28
29
|
export declare const CRASH_REPORT_TIMEOUT_MS = 1500;
|
|
29
30
|
export declare const MAX_CRASH_MESSAGE = 512;
|
|
@@ -97,13 +98,30 @@ export type SpawnLike = (command: string, args: string[], options: {
|
|
|
97
98
|
export interface FatalCrashOpts extends CrashReporterOpts {
|
|
98
99
|
/** Spawn seam for tests (defaults to node:child_process spawn). */
|
|
99
100
|
spawnImpl?: SpawnLike;
|
|
101
|
+
/** Local error-trail seam for tests (defaults to errorSink's logEvent). */
|
|
102
|
+
trail?: (event: SinkEvent) => void;
|
|
100
103
|
}
|
|
104
|
+
/** Test seam: forget that a fatal report went out, and that a reporter is installed. */
|
|
105
|
+
export declare function resetFatalReportedForTests(): void;
|
|
101
106
|
/**
|
|
102
107
|
* Deliver a crash report from a process that is about to die: build the
|
|
103
108
|
* sanitized payload in-process (cheap, synchronous), then spawn a detached
|
|
104
109
|
* one-shot sender that outlives the crash. Never throws.
|
|
105
110
|
*/
|
|
106
111
|
export declare function reportFatalCrash(error: unknown, opts: FatalCrashOpts, context?: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* Report a pi process that ends with a non-zero code WITHOUT an uncaught
|
|
114
|
+
* exception. pi's own fatal paths print to stderr and `process.exit(1)`, which
|
|
115
|
+
* the exception monitor never sees, so until now those deaths were invisible
|
|
116
|
+
* unless the user pasted their terminal. An `exit` listener observes only: it
|
|
117
|
+
* cannot keep the process alive or change the code, and the detached sender
|
|
118
|
+
* is spawned synchronously, which is all an `exit` handler is allowed to do.
|
|
119
|
+
*
|
|
120
|
+
* Skipped on the desktop surface, where the shell files a richer report built
|
|
121
|
+
* from the driver's stderr tail (one incident, one report), and for the two
|
|
122
|
+
* "stopped on purpose" codes.
|
|
123
|
+
*/
|
|
124
|
+
export declare function installNonZeroExitReporter(opts: FatalCrashOpts, proc?: Pick<NodeJS.Process, "on">): void;
|
|
107
125
|
/**
|
|
108
126
|
* Observe (never alter) a fatal crash in pi's process. Uses
|
|
109
127
|
* `uncaughtExceptionMonitor`, which fires before the process dies without
|
|
@@ -170,7 +170,7 @@ export function makeCrashReporter(opts) {
|
|
|
170
170
|
const token = opts.getToken();
|
|
171
171
|
const sanitized = sanitizeCrashError(error, { env, repoRoot });
|
|
172
172
|
const payload = {
|
|
173
|
-
client: isDesktopSurface() ? "desktop" : "cli",
|
|
173
|
+
client: isDesktopSurface(env) ? "desktop" : "cli",
|
|
174
174
|
clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
175
175
|
platform: platformLabel(),
|
|
176
176
|
...sanitized,
|
|
@@ -226,6 +226,19 @@ const DETACHED_SENDER_SRC = [
|
|
|
226
226
|
" signal: AbortSignal.timeout(4000),",
|
|
227
227
|
"}).catch(() => {}).finally(done);",
|
|
228
228
|
].join("\n");
|
|
229
|
+
/**
|
|
230
|
+
* Set once a fatal report has been handed to a sender, so the exit reporter
|
|
231
|
+
* never files a second report for the same death (an uncaught exception is
|
|
232
|
+
* followed by an exit-1, and both hooks would otherwise fire).
|
|
233
|
+
*/
|
|
234
|
+
let fatalReported = false;
|
|
235
|
+
/** The exit reporter is per process, not per extension load (a reload re-runs the factory). */
|
|
236
|
+
let exitReporterInstalled = false;
|
|
237
|
+
/** Test seam: forget that a fatal report went out, and that a reporter is installed. */
|
|
238
|
+
export function resetFatalReportedForTests() {
|
|
239
|
+
fatalReported = false;
|
|
240
|
+
exitReporterInstalled = false;
|
|
241
|
+
}
|
|
229
242
|
/**
|
|
230
243
|
* Deliver a crash report from a process that is about to die: build the
|
|
231
244
|
* sanitized payload in-process (cheap, synchronous), then spawn a detached
|
|
@@ -241,7 +254,7 @@ export function reportFatalCrash(error, opts, context) {
|
|
|
241
254
|
return;
|
|
242
255
|
const sanitized = sanitizeCrashError(error, { env });
|
|
243
256
|
const payload = {
|
|
244
|
-
client: isDesktopSurface() ? "desktop" : "cli",
|
|
257
|
+
client: isDesktopSurface(env) ? "desktop" : "cli",
|
|
245
258
|
clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
246
259
|
platform: platformLabel(),
|
|
247
260
|
...sanitized,
|
|
@@ -263,11 +276,62 @@ export function reportFatalCrash(error, opts, context) {
|
|
|
263
276
|
},
|
|
264
277
|
});
|
|
265
278
|
child.unref();
|
|
279
|
+
fatalReported = true;
|
|
266
280
|
}
|
|
267
281
|
catch {
|
|
268
282
|
// a crash reporter must never add its own crash
|
|
269
283
|
}
|
|
270
284
|
}
|
|
285
|
+
/** Exit codes that mean "someone stopped it" (Ctrl+C, SIGTERM), not "it broke". */
|
|
286
|
+
const STOPPED_EXIT_CODES = new Set([130, 143]);
|
|
287
|
+
/**
|
|
288
|
+
* Report a pi process that ends with a non-zero code WITHOUT an uncaught
|
|
289
|
+
* exception. pi's own fatal paths print to stderr and `process.exit(1)`, which
|
|
290
|
+
* the exception monitor never sees, so until now those deaths were invisible
|
|
291
|
+
* unless the user pasted their terminal. An `exit` listener observes only: it
|
|
292
|
+
* cannot keep the process alive or change the code, and the detached sender
|
|
293
|
+
* is spawned synchronously, which is all an `exit` handler is allowed to do.
|
|
294
|
+
*
|
|
295
|
+
* Skipped on the desktop surface, where the shell files a richer report built
|
|
296
|
+
* from the driver's stderr tail (one incident, one report), and for the two
|
|
297
|
+
* "stopped on purpose" codes.
|
|
298
|
+
*/
|
|
299
|
+
export function installNonZeroExitReporter(opts, proc = process) {
|
|
300
|
+
// Once per process: pi re-runs the extension factory on every reload, and
|
|
301
|
+
// a listener per reload would both pile up and race each other on exit.
|
|
302
|
+
if (exitReporterInstalled)
|
|
303
|
+
return;
|
|
304
|
+
exitReporterInstalled = true;
|
|
305
|
+
proc.on("exit", (code) => {
|
|
306
|
+
try {
|
|
307
|
+
if (code === 0 || STOPPED_EXIT_CODES.has(code) || fatalReported)
|
|
308
|
+
return;
|
|
309
|
+
const env = opts.env ?? process.env;
|
|
310
|
+
// The same source the payload's client label reads, so the skip and the
|
|
311
|
+
// label can never disagree about which surface this is.
|
|
312
|
+
if (isDesktopSurface(env))
|
|
313
|
+
return;
|
|
314
|
+
const error = new Error(`pi exited with code ${code}`);
|
|
315
|
+
error.name = "ProcessExit";
|
|
316
|
+
// The listener's own frames say nothing about why pi exited.
|
|
317
|
+
error.stack = undefined;
|
|
318
|
+
reportFatalCrash(error, opts, `process-exit:${code}`);
|
|
319
|
+
// The local trail too, so a logged-out user (no token, nothing sent)
|
|
320
|
+
// still has a record /feedback can bind next session.
|
|
321
|
+
(opts.trail ?? logEvent)({
|
|
322
|
+
source: "tool",
|
|
323
|
+
level: "error",
|
|
324
|
+
event: "process_exit",
|
|
325
|
+
sessionId: env.YAGNI_SESSION_ID ?? undefined,
|
|
326
|
+
flush: "sync",
|
|
327
|
+
fields: { code },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// an exit hook must never throw
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
271
335
|
/**
|
|
272
336
|
* Observe (never alter) a fatal crash in pi's process. Uses
|
|
273
337
|
* `uncaughtExceptionMonitor`, which fires before the process dies without
|
|
@@ -225,6 +225,6 @@ export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.j
|
|
|
225
225
|
export type { RowKind, ToolRow, ToolRowPatch } from "./toolRuns.js";
|
|
226
226
|
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
227
227
|
export type { WorkingLineHandle, RegisterWorkingLineDeps } from "./workingLine.js";
|
|
228
|
-
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
228
|
+
export { crashReportsDisabled, installNonZeroExitReporter, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
229
229
|
export type { CrashReporter, CrashReporterOpts, FatalCrashOpts, SanitizedCrash } from "./crashReport.js";
|
|
230
230
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/extension/index.js
CHANGED
|
@@ -53,7 +53,7 @@ import { loadGroundingEnabled } from "./grounding.js";
|
|
|
53
53
|
import { attributionPromptSection, loadAttributionSettings, } from "./attribution.js";
|
|
54
54
|
import { registerAmbientRecall } from "./recall.js";
|
|
55
55
|
import { resilientFetch } from "./resilientFetch.js";
|
|
56
|
-
import { installUncaughtExceptionMonitor, makeCrashReporter, runningUnderTest } from "./crashReport.js";
|
|
56
|
+
import { installNonZeroExitReporter, installUncaughtExceptionMonitor, makeCrashReporter, runningUnderTest, } from "./crashReport.js";
|
|
57
57
|
import { createToolOutcomeBatcher } from "./toolOutcomes.js";
|
|
58
58
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
59
59
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
@@ -252,7 +252,12 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
252
252
|
// soft, opt-out via YAGNI_DISABLE_CRASH_REPORTS=1; off in eval mode like
|
|
253
253
|
// every other external side effect.
|
|
254
254
|
if (!evalMode) {
|
|
255
|
-
|
|
255
|
+
const fatalOpts = { baseUrl, getToken: getTokenFn, env: deps.env };
|
|
256
|
+
installUncaughtExceptionMonitor(fatalOpts);
|
|
257
|
+
// A pi that exits non-zero without throwing (its own fatal paths) is a
|
|
258
|
+
// death the monitor never sees; report it too, so a terminal user's
|
|
259
|
+
// "it just quit" reaches Sentry without a pasted transcript.
|
|
260
|
+
installNonZeroExitReporter(fatalOpts);
|
|
256
261
|
}
|
|
257
262
|
// YAG-500 Fix E: non-fatal auth-event reporter for 401s on the model path.
|
|
258
263
|
// Reuses the crash endpoint (/api/yagni-code/crash) with a distinct context
|
|
@@ -1870,5 +1875,5 @@ export { appendToSpool, loadSpool, flushSpool, sendOrSpool, spoolFile, MAX_SPOOL
|
|
|
1870
1875
|
export { registerCondensedTools, displayPath, isScratchpadPath, primaryArg, formatRowTitle, formatWriteBody, formatEditBody, formatBashErrorBody, formatBashPartialBody, formatExpandedOutput, splitBashError, countPatchAdditions, WRITE_PREVIEW_LINES, DIFF_PREVIEW_LINES, } from "./condensedTools.js";
|
|
1871
1876
|
export { ToolRunTracker, summarizeRun, isQuiet, kindForTool } from "./toolRuns.js";
|
|
1872
1877
|
export { registerWorkingLine, composeWorkingMessage, NULL_WORKING_LINE, WORKING_INDICATOR_FRAMES, WORKING_VERBS, } from "./workingLine.js";
|
|
1873
|
-
export { crashReportsDisabled, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
1878
|
+
export { crashReportsDisabled, installNonZeroExitReporter, installUncaughtExceptionMonitor, makeCrashReporter, reportFatalCrash, sanitizeCrashError, sanitizeCrashText, } from "./crashReport.js";
|
|
1874
1879
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* Desktop-facing widgets are structured single-line JSON records the app
|
|
7
7
|
* parses, not themed terminal lines.
|
|
8
8
|
*/
|
|
9
|
-
export declare function isDesktopSurface(): boolean;
|
|
9
|
+
export declare function isDesktopSurface(env?: NodeJS.ProcessEnv): boolean;
|
|
10
10
|
//# sourceMappingURL=surface.d.ts.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Desktop-facing widgets are structured single-line JSON records the app
|
|
7
7
|
* parses, not themed terminal lines.
|
|
8
8
|
*/
|
|
9
|
-
export function isDesktopSurface() {
|
|
10
|
-
return
|
|
9
|
+
export function isDesktopSurface(env = process.env) {
|
|
10
|
+
return env.YAGNI_SURFACE === "desktop";
|
|
11
11
|
}
|
|
12
12
|
//# sourceMappingURL=surface.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.3-staging.
|
|
3
|
+
"version": "1.1.3-staging.1382.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": "
|
|
61
|
+
"yagniSourceSha": "d05478fe95b7c47498cd44b4cd37c20b711724b7"
|
|
62
62
|
}
|