@yagni-app/code-staging 0.3.2-staging.1119.1 → 0.3.2-staging.1120.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.
- package/dist/extension/index.js +23 -0
- package/dist/extension/turnLog.d.ts +38 -0
- package/dist/extension/turnLog.js +93 -0
- package/package.json +2 -2
package/dist/extension/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerCostCommand } from "./costHud.js";
|
|
|
20
20
|
import { isDebug } from "./diagnostics.js";
|
|
21
21
|
import { droppedSessionRuns, sessionRunIds } from "./sessionRuns.js";
|
|
22
22
|
import { codeStateHome } from "./stateHome.js";
|
|
23
|
+
import { logTurnLifecycle } from "./turnLog.js";
|
|
23
24
|
import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_PATTERN, isShiftTab } from "./footer.js";
|
|
24
25
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
25
26
|
import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
@@ -581,6 +582,28 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
581
582
|
rulesSection,
|
|
582
583
|
}),
|
|
583
584
|
});
|
|
585
|
+
// Turn-lifecycle WAL: a `turn_start` with no matching `turn_end` is the
|
|
586
|
+
// silent-freeze tell (the turn that left the UI stuck on "Working…"), even
|
|
587
|
+
// if the process is later killed. `turn_end` records the final stopReason so
|
|
588
|
+
// an `error` / `aborted` end is visible at a glance without reading the raw
|
|
589
|
+
// session .jsonl. Fail-soft; nothing here may break a turn.
|
|
590
|
+
const turnStartMs = new Map();
|
|
591
|
+
const sessionIdForLog = () => deps.env?.YAGNI_SESSION_ID;
|
|
592
|
+
pi.on("turn_start", (event) => {
|
|
593
|
+
turnStartMs.set(event.turnIndex, Date.now());
|
|
594
|
+
logTurnLifecycle({ kind: "turn_start", sessionId: sessionIdForLog(), turnIndex: event.turnIndex });
|
|
595
|
+
});
|
|
596
|
+
pi.on("turn_end", (event) => {
|
|
597
|
+
const startedAt = turnStartMs.get(event.turnIndex);
|
|
598
|
+
turnStartMs.delete(event.turnIndex);
|
|
599
|
+
logTurnLifecycle({
|
|
600
|
+
kind: "turn_end",
|
|
601
|
+
sessionId: sessionIdForLog(),
|
|
602
|
+
turnIndex: event.turnIndex,
|
|
603
|
+
stopReason: event.message?.stopReason,
|
|
604
|
+
elapsedMs: startedAt !== undefined ? Date.now() - startedAt : undefined,
|
|
605
|
+
});
|
|
606
|
+
});
|
|
584
607
|
// Two finalized-message guards share this handler (their conditions are
|
|
585
608
|
// mutually exclusive: YAG-460 takes error-stopped messages, YAG-466 takes
|
|
586
609
|
// stop/length ones without an errorMessage).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side turn-lifecycle log — makes a "stuck on Working…" turn legible
|
|
3
|
+
* without spelunking the raw session `.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* A session write-only WAL, not a buffered in-memory structure: the whole
|
|
6
|
+
* point is that a turn which STARTS but never ENDS (the silent-freeze failure
|
|
7
|
+
* this log exists to catch) still leaves a `turn_start` record with no
|
|
8
|
+
* matching `turn_end`, even if the process is later killed. Each record is a
|
|
9
|
+
* single sanitized JSON line (no message content, tokens, or tool payloads —
|
|
10
|
+
* only a timestamp, the session id, a stable event name, and the turn's final
|
|
11
|
+
* stop reason / error class when it ends). Follows `diagnostics.ts` exactly:
|
|
12
|
+
* rotating file under `~/.yagni-code/logs/`, fail-soft, hermetic under
|
|
13
|
+
* `node --test`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function _setTurnLogHomeForTest(dir: string | null): void;
|
|
16
|
+
export declare function turnLogPath(): string;
|
|
17
|
+
export type TurnLifecycleEvent = {
|
|
18
|
+
kind: "turn_start";
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
turnIndex?: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "turn_end";
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
turnIndex?: number;
|
|
25
|
+
/** The fixed stopReason enum from the final assistant message — never a raw error message. */
|
|
26
|
+
stopReason?: string;
|
|
27
|
+
/** Time since the matching turn_start, ms. */
|
|
28
|
+
elapsedMs?: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Append one sanitized lifecycle record. Fail-soft — a logging failure must
|
|
32
|
+
* never break the turn. `errorClass` is a caller-mapped category, never a
|
|
33
|
+
* raw error message.
|
|
34
|
+
*/
|
|
35
|
+
export declare function logTurnLifecycle(ev: TurnLifecycleEvent): void;
|
|
36
|
+
/** List existing lifecycle log files (active + rotations), for a report. */
|
|
37
|
+
export declare function listTurnLogFiles(): string[];
|
|
38
|
+
//# sourceMappingURL=turnLog.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side turn-lifecycle log — makes a "stuck on Working…" turn legible
|
|
3
|
+
* without spelunking the raw session `.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* A session write-only WAL, not a buffered in-memory structure: the whole
|
|
6
|
+
* point is that a turn which STARTS but never ENDS (the silent-freeze failure
|
|
7
|
+
* this log exists to catch) still leaves a `turn_start` record with no
|
|
8
|
+
* matching `turn_end`, even if the process is later killed. Each record is a
|
|
9
|
+
* single sanitized JSON line (no message content, tokens, or tool payloads —
|
|
10
|
+
* only a timestamp, the session id, a stable event name, and the turn's final
|
|
11
|
+
* stop reason / error class when it ends). Follows `diagnostics.ts` exactly:
|
|
12
|
+
* rotating file under `~/.yagni-code/logs/`, fail-soft, hermetic under
|
|
13
|
+
* `node --test`.
|
|
14
|
+
*/
|
|
15
|
+
import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { codeStateHome } from "./stateHome.js";
|
|
18
|
+
/** Test seam: point the log at a tmpdir (mirrors _setDiagnosticsHomeForTest). */
|
|
19
|
+
let homeOverride = null;
|
|
20
|
+
export function _setTurnLogHomeForTest(dir) {
|
|
21
|
+
homeOverride = dir;
|
|
22
|
+
}
|
|
23
|
+
function turnLogDir() {
|
|
24
|
+
return join(codeStateHome(homeOverride), "logs");
|
|
25
|
+
}
|
|
26
|
+
export function turnLogPath() {
|
|
27
|
+
return join(turnLogDir(), "turn-lifecycle.log");
|
|
28
|
+
}
|
|
29
|
+
const MAX_LOG_BYTES = 256 * 1024;
|
|
30
|
+
const KEEP_ROTATIONS = 2;
|
|
31
|
+
/** Shift turn-lifecycle.log -> .1 -> .2 once the active file passes the cap. */
|
|
32
|
+
function rotateIfNeeded(path) {
|
|
33
|
+
try {
|
|
34
|
+
if (!statSync(path).isFile() || statSync(path).size < MAX_LOG_BYTES)
|
|
35
|
+
return;
|
|
36
|
+
for (let i = KEEP_ROTATIONS; i >= 1; i--) {
|
|
37
|
+
const from = i === 1 ? path : `${path}.${i - 1}`;
|
|
38
|
+
const to = `${path}.${i}`;
|
|
39
|
+
try {
|
|
40
|
+
renameSync(from, to);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* absent source — fine */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* rotation is best-effort */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Append one sanitized lifecycle record. Fail-soft — a logging failure must
|
|
53
|
+
* never break the turn. `errorClass` is a caller-mapped category, never a
|
|
54
|
+
* raw error message.
|
|
55
|
+
*/
|
|
56
|
+
export function logTurnLifecycle(ev) {
|
|
57
|
+
try {
|
|
58
|
+
// Hermetic under `node --test`: never touch the real home dir unless a test
|
|
59
|
+
// explicitly overrode it (same rule as diagnostics.ts, which prevents the
|
|
60
|
+
// suite's exit from depending on the CI filesystem).
|
|
61
|
+
if (process.env.NODE_TEST_CONTEXT && homeOverride === null)
|
|
62
|
+
return;
|
|
63
|
+
const line = {
|
|
64
|
+
ts: new Date().toISOString(),
|
|
65
|
+
event: ev.kind,
|
|
66
|
+
...(ev.sessionId ? { sessionId: ev.sessionId } : {}),
|
|
67
|
+
...(ev.turnIndex !== undefined ? { turnIndex: ev.turnIndex } : {}),
|
|
68
|
+
...(ev.kind === "turn_end" && ev.stopReason !== undefined ? { stopReason: ev.stopReason } : {}),
|
|
69
|
+
...(ev.kind === "turn_end" && ev.elapsedMs !== undefined ? { elapsedMs: ev.elapsedMs } : {}),
|
|
70
|
+
};
|
|
71
|
+
const path = turnLogPath();
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
73
|
+
rotateIfNeeded(path);
|
|
74
|
+
appendFileSync(path, JSON.stringify(line) + "\n", "utf8");
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* logging must never throw into the editor */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** List existing lifecycle log files (active + rotations), for a report. */
|
|
81
|
+
export function listTurnLogFiles() {
|
|
82
|
+
try {
|
|
83
|
+
const dir = turnLogDir();
|
|
84
|
+
return readdirSync(dir)
|
|
85
|
+
.filter((f) => f.startsWith("turn-lifecycle.log"))
|
|
86
|
+
.sort()
|
|
87
|
+
.map((f) => join(dir, f));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=turnLog.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "0.3.2-staging.
|
|
3
|
+
"version": "0.3.2-staging.1120.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)",
|
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"smol-toml": "^1.8.0",
|
|
40
40
|
"typebox": "^1.3.11"
|
|
41
41
|
},
|
|
42
|
-
"yagniSourceSha": "
|
|
42
|
+
"yagniSourceSha": "a65ced918d4a73680643f5d0bf08f33f7f043e9b"
|
|
43
43
|
}
|