@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.
@@ -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.1119.1",
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": "44ed4bcc995ffa2b075c202e5aa1a23f5ec808b6"
42
+ "yagniSourceSha": "a65ced918d4a73680643f5d0bf08f33f7f043e9b"
43
43
  }