@yagni-app/code-staging 0.3.0-staging.1061.1 → 0.3.0-staging.1064.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.
|
@@ -125,8 +125,6 @@ export function makeCrashReporter(opts) {
|
|
|
125
125
|
if (crashReportsDisabled(env))
|
|
126
126
|
return;
|
|
127
127
|
const token = opts.getToken();
|
|
128
|
-
if (!token)
|
|
129
|
-
return;
|
|
130
128
|
const sanitized = sanitizeCrashError(error, { env, repoRoot });
|
|
131
129
|
const payload = {
|
|
132
130
|
client: isDesktopSurface() ? "desktop" : "cli",
|
|
@@ -148,7 +146,7 @@ export function makeCrashReporter(opts) {
|
|
|
148
146
|
method: "POST",
|
|
149
147
|
headers: {
|
|
150
148
|
"content-type": "application/json",
|
|
151
|
-
authorization: `Bearer ${token}
|
|
149
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
152
150
|
},
|
|
153
151
|
body: JSON.stringify(payload),
|
|
154
152
|
signal: controller.signal,
|
|
@@ -63,6 +63,12 @@ export interface RegisterYagniDeps {
|
|
|
63
63
|
tokenProvider?: TokenProvider;
|
|
64
64
|
/** The spool flush (R4 write half), injectable so tests never touch disk. */
|
|
65
65
|
flushSpool?: (opts: SpoolClientOpts) => Promise<FlushOutcome>;
|
|
66
|
+
/**
|
|
67
|
+
* Non-fatal auth-event reporter (YAG-500 Fix E). Defaults to
|
|
68
|
+
* `makeCrashReporter` gated on `!evalMode`; inject a spy in tests to assert
|
|
69
|
+
* the report is fired with `context: "auth-failure"` and the refresh outcome.
|
|
70
|
+
*/
|
|
71
|
+
authReporter?: (error: unknown, context?: string) => Promise<void>;
|
|
66
72
|
env?: NodeJS.ProcessEnv;
|
|
67
73
|
}
|
|
68
74
|
/**
|
package/dist/extension/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import { registerDecisionCommands } from "./decisions.js";
|
|
|
30
30
|
import { makeDecisionCapture } from "./decisionCapture.js";
|
|
31
31
|
import { registerAmbientRecall } from "./recall.js";
|
|
32
32
|
import { resilientFetch } from "./resilientFetch.js";
|
|
33
|
-
import { installUncaughtExceptionMonitor } from "./crashReport.js";
|
|
33
|
+
import { installUncaughtExceptionMonitor, makeCrashReporter } from "./crashReport.js";
|
|
34
34
|
import { flushSpool as defaultFlushSpool } from "./spool.js";
|
|
35
35
|
import { makeAuthedFetch, makeTokenProvider } from "./tokenProvider.js";
|
|
36
36
|
import { attributionHeaders, fetchCatalog as defaultFetchCatalog, fetchContextBrief as defaultFetchContextBrief, getToken, getTokenExpiresAt as defaultGetTokenExpiresAt, getWorkspaceId as defaultGetWorkspaceId, isDriverCaller, resolveBaseUrl, tokenExpiryNotice, } from "./config.js";
|
|
@@ -113,6 +113,19 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
113
113
|
if (!evalMode) {
|
|
114
114
|
installUncaughtExceptionMonitor({ baseUrl, getToken: getTokenFn, env: deps.env });
|
|
115
115
|
}
|
|
116
|
+
// YAG-500 Fix E: non-fatal auth-event reporter for 401s on the model path.
|
|
117
|
+
// Reuses the crash endpoint (/api/yagni-code/crash) with a distinct context
|
|
118
|
+
// so auth failures are visible in Sentry/backend logs. Gated on !evalMode
|
|
119
|
+
// like every other external side effect; injectable for tests.
|
|
120
|
+
const authReporter = deps.authReporter ??
|
|
121
|
+
(!evalMode
|
|
122
|
+
? makeCrashReporter({ baseUrl, getToken: getTokenFn, fetchImpl: deps.fetchImpl, env: deps.env })
|
|
123
|
+
: async () => { });
|
|
124
|
+
// YAG-500 Fix A+C: the model-path 401 recovery outcome, set by the
|
|
125
|
+
// message_end handler so it can produce the right user-facing message. The
|
|
126
|
+
// after_provider_response event does NOT fire on a 401 (the OpenAI SDK throws
|
|
127
|
+
// before onResponse is reached), so message_end is the only seam.
|
|
128
|
+
let lastAuthRecovery = null;
|
|
116
129
|
const fullCatalog = await fetchCatalog({ baseUrl, getToken: getTokenFn, fetchImpl: authedFetch });
|
|
117
130
|
// Lock the interactive session to the `advanced` tier only. The backend
|
|
118
131
|
// catalog returns all tiers, but only `advanced` is registered with the
|
|
@@ -435,7 +448,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
435
448
|
// renders. The message text deliberately matches neither pi's overflow nor
|
|
436
449
|
// retryable-error patterns: a deterministic empty response should not burn
|
|
437
450
|
// auto-retries or trigger compaction — the user decides what to do next.
|
|
438
|
-
pi.on("message_end", (event, ctx) => {
|
|
451
|
+
pi.on("message_end", async (event, ctx) => {
|
|
439
452
|
const msg = event.message;
|
|
440
453
|
if (msg.role !== "assistant")
|
|
441
454
|
return;
|
|
@@ -450,6 +463,71 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
450
463
|
// post-replacement message, and dropping the marker would defeat the very
|
|
451
464
|
// recovery this error exists to trigger.
|
|
452
465
|
if (msg.stopReason === "error" && msg.errorMessage) {
|
|
466
|
+
// YAG-500: a 401 from the model proxy means the session token expired
|
|
467
|
+
// (or was revoked). Unlike tool 401s (handled by makeAuthedFetch), the
|
|
468
|
+
// model completion path has no 401-retry seam — pi's retryProviderRequest
|
|
469
|
+
// treats 401 as non-retryable, and after_provider_response never fires
|
|
470
|
+
// (the SDK throws before onResponse is reached). So message_end is the
|
|
471
|
+
// only place to detect it and trigger recovery. The regex matches
|
|
472
|
+
// "yagni login" (the backend's auth-error message) but NOT
|
|
473
|
+
// "request_too_large" (YAG-460's overflow marker).
|
|
474
|
+
const isAuthError = /yagni login/i.test(msg.errorMessage)
|
|
475
|
+
&& !/request_too_large|context_too_large/i.test(msg.errorMessage);
|
|
476
|
+
if (isAuthError) {
|
|
477
|
+
lastAuthRecovery = null;
|
|
478
|
+
let rotated = false;
|
|
479
|
+
try {
|
|
480
|
+
rotated = await tokenProvider.refresh();
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
rotated = false;
|
|
484
|
+
}
|
|
485
|
+
lastAuthRecovery = rotated ? "refreshed" : "failed";
|
|
486
|
+
const explanation = rotated
|
|
487
|
+
? "Your session token expired but was refreshed automatically. Re-send your prompt to continue."
|
|
488
|
+
: "Your session token expired and could not be refreshed. Run `yagni login`, then re-send your prompt. If the issue persists, restart YAGNI Code.";
|
|
489
|
+
if (ctx.hasUI) {
|
|
490
|
+
try {
|
|
491
|
+
ctx.ui.notify(explanation, rotated ? "info" : "error");
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
// Surfacing the problem must never break the session itself.
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// YAG-500 Fix E: fire a non-fatal crash report so auth failures are
|
|
498
|
+
// visible in Sentry/backend logs. Reuses the crash endpoint with a
|
|
499
|
+
// distinct context. Fire-and-forget, fail-soft. The crash endpoint is
|
|
500
|
+
// now public (no token required), so the report lands even when the
|
|
501
|
+
// session token is expired and refresh failed — the most critical
|
|
502
|
+
// failure signal is no longer silently dropped.
|
|
503
|
+
void authReporter(new Error(`auth_401 on model path; refresh=${rotated ? "succeeded" : "failed"}`), "auth-failure").catch(() => { });
|
|
504
|
+
// YAG-500 Fix F: local diagnostics log under YAGNI_DEBUG.
|
|
505
|
+
if (isDebug(env)) {
|
|
506
|
+
try {
|
|
507
|
+
const logPath = join(codeStateHome(null, env), "logs", "auth-events.log");
|
|
508
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
509
|
+
appendFileSync(logPath, JSON.stringify({
|
|
510
|
+
ts: new Date().toISOString(),
|
|
511
|
+
status: 401,
|
|
512
|
+
refresh: rotated ? "succeeded" : "failed",
|
|
513
|
+
}) + "\n", "utf8");
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// A diagnostic must never break the session.
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { message: { ...msg, errorMessage: explanation } };
|
|
520
|
+
}
|
|
521
|
+
// YAG-460: the backend proxy answers an oversized conversation with an
|
|
522
|
+
// OpenAI-format 413 whose type is `request_too_large`. pi's own overflow
|
|
523
|
+
// detection matches that marker and runs full recovery — compact, then
|
|
524
|
+
// auto-retry the failed turn — so this branch must NOT call ctx.compact()
|
|
525
|
+
// (it would race the built-in recovery and lose the retry). Its only job
|
|
526
|
+
// is UX: replace the raw `413: {"error":{...}}` JSON with a readable
|
|
527
|
+
// message and tell the user what is happening. The rewritten text KEEPS
|
|
528
|
+
// the `request_too_large` marker verbatim: pi's _checkCompaction reads the
|
|
529
|
+
// post-replacement message, and dropping the marker would defeat the very
|
|
530
|
+
// recovery this error exists to trigger.
|
|
453
531
|
const isContextTooLarge = /request_too_large|context_too_large/i.test(msg.errorMessage);
|
|
454
532
|
if (!isContextTooLarge)
|
|
455
533
|
return;
|
|
@@ -130,7 +130,7 @@ export function makeTokenProvider(deps) {
|
|
|
130
130
|
});
|
|
131
131
|
}, delay);
|
|
132
132
|
}
|
|
133
|
-
function applyRotation(rotation) {
|
|
133
|
+
function applyRotation(rotation, skipPersist = false) {
|
|
134
134
|
token = rotation.token;
|
|
135
135
|
if (rotation.expiresAt)
|
|
136
136
|
expiresAt = rotation.expiresAt;
|
|
@@ -139,13 +139,45 @@ export function makeTokenProvider(deps) {
|
|
|
139
139
|
env.YAGNI_TOKEN = rotation.token;
|
|
140
140
|
if (rotation.expiresAt)
|
|
141
141
|
env.YAGNI_TOKEN_EXPIRES_AT = rotation.expiresAt;
|
|
142
|
+
if (!skipPersist) {
|
|
143
|
+
try {
|
|
144
|
+
persistProfile(rotation);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* fail-soft */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
armProactiveTimer();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Read the token from the launcher's profile file on disk. Used as a fallback
|
|
154
|
+
* when the server-side refresh fails (the old token is also invalid for
|
|
155
|
+
* /auth/refresh): an external `yagni login` writes a fresh token to the same
|
|
156
|
+
* file, so re-reading it can recover a session that the server refresh cannot.
|
|
157
|
+
* Returns null when the file is missing, unreadable, or carries the same token
|
|
158
|
+
* already in memory.
|
|
159
|
+
*/
|
|
160
|
+
function readTokenFromDisk() {
|
|
161
|
+
const profilePath = env.YAGNI_PROFILE_PATH?.trim();
|
|
162
|
+
if (!profilePath)
|
|
163
|
+
return null;
|
|
142
164
|
try {
|
|
143
|
-
|
|
165
|
+
const parsed = JSON.parse(readFileSync(profilePath, "utf8"));
|
|
166
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
167
|
+
return null;
|
|
168
|
+
const obj = parsed;
|
|
169
|
+
const diskToken = typeof obj.token === "string" ? obj.token : undefined;
|
|
170
|
+
if (!diskToken || diskToken === token)
|
|
171
|
+
return null;
|
|
172
|
+
return {
|
|
173
|
+
token: diskToken,
|
|
174
|
+
expiresAt: typeof obj.expiresAt === "string" ? obj.expiresAt : undefined,
|
|
175
|
+
workspaceId: typeof obj.workspaceId === "string" ? obj.workspaceId : undefined,
|
|
176
|
+
};
|
|
144
177
|
}
|
|
145
178
|
catch {
|
|
146
|
-
|
|
179
|
+
return null;
|
|
147
180
|
}
|
|
148
|
-
armProactiveTimer();
|
|
149
181
|
}
|
|
150
182
|
async function doRefresh() {
|
|
151
183
|
const current = token;
|
|
@@ -161,8 +193,17 @@ export function makeTokenProvider(deps) {
|
|
|
161
193
|
body: "{}",
|
|
162
194
|
signal: AbortSignal.timeout(REFRESH_REQUEST_TIMEOUT_MS),
|
|
163
195
|
});
|
|
164
|
-
if (!res.ok)
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
// Server refresh failed (the old token is also invalid for /auth/refresh).
|
|
198
|
+
// Fall back to the profile file on disk: an external `yagni login` may
|
|
199
|
+
// have written a fresh token there that this running session hasn't seen.
|
|
200
|
+
const disk = readTokenFromDisk();
|
|
201
|
+
if (disk) {
|
|
202
|
+
applyRotation(disk, true);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
165
205
|
return false;
|
|
206
|
+
}
|
|
166
207
|
const data = (await res.json());
|
|
167
208
|
if (!data || typeof data.token !== "string" || data.token.length === 0)
|
|
168
209
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.3.0-staging.
|
|
3
|
+
"version": "0.3.0-staging.1064.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)",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "0.84.1",
|
|
39
39
|
"typebox": "^1.3.11"
|
|
40
40
|
},
|
|
41
|
-
"yagniSourceSha": "
|
|
41
|
+
"yagniSourceSha": "e38a99de02a31e45c61d006943113caf8817e0bd"
|
|
42
42
|
}
|