peaks-loop 4.0.12 → 4.0.14
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/CHANGELOG.md +29 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/core/doctor-command.js +26 -3
- package/dist/cli/commands/outer-cache-commands.d.ts +11 -0
- package/dist/cli/commands/outer-cache-commands.js +120 -0
- package/dist/services/doctor/doctor-service/checks/multi-binary-drift.d.ts +61 -0
- package/dist/services/doctor/doctor-service/checks/multi-binary-drift.js +268 -0
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/report/final-summary.d.ts +13 -5
- package/dist/services/doctor/doctor-service/report/final-summary.js +19 -9
- package/dist/services/doctor/doctor-service/types.d.ts +55 -0
- package/dist/services/doctor/doctor-service.d.ts +2 -1
- package/dist/services/doctor/doctor-service.js +1 -0
- package/dist/services/doctor/index.d.ts +1 -1
- package/dist/services/doctor/index.js +1 -1
- package/dist/services/session/session-binding-bridge.js +58 -5
- package/dist/services/skills/hooks-settings-service.js +22 -2
- package/dist/services/skills/outer-cache-hook-constants.d.ts +24 -0
- package/dist/services/skills/outer-cache-hook-constants.js +24 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +3 -1
- package/dist/services/skills/skill-statusline-renderer.js +37 -1
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/skills/skill-statusline-service.js +40 -16
- package/dist/services/skills/skill-statusline-sid-suffix.d.ts +55 -0
- package/dist/services/skills/skill-statusline-sid-suffix.js +65 -0
- package/package.json +4 -4
|
@@ -12,10 +12,32 @@
|
|
|
12
12
|
* flow through the same names so the main package's `import { runDoctor }`
|
|
13
13
|
* keeps working unchanged.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Severity for a single doctor check. `'error'` flips the exit code
|
|
17
|
+
* (via `buildReport` and the CLI dispatcher); `'warning'` surfaces
|
|
18
|
+
* the finding in the JSON envelope (`ok: false` + `severity: 'warning'`)
|
|
19
|
+
* but does NOT escalate the doctor exit code to 1.
|
|
20
|
+
*
|
|
21
|
+
* Optional in the type for back-compat with older check plugins that
|
|
22
|
+
* pre-date the severity-aware summary (slice
|
|
23
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
24
|
+
* repair cycle). When omitted, the dispatcher treats the check as
|
|
25
|
+
* `'error'` — i.e. `ok: false` escalates the exit code the same way
|
|
26
|
+
* it did before this slice.
|
|
27
|
+
*/
|
|
28
|
+
export type DoctorCheckSeverity = 'error' | 'warning';
|
|
15
29
|
export type DoctorCheck = {
|
|
16
30
|
id: string;
|
|
17
31
|
ok: boolean;
|
|
18
32
|
message: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional severity tag. When `'warning'`, the check still reports
|
|
35
|
+
* `ok: false` (so operators see the finding in the JSON envelope)
|
|
36
|
+
* but `buildReport` does NOT count it as a failure for the
|
|
37
|
+
* `summary.ok` exit-code calculation. Default-omitted checks
|
|
38
|
+
* behave as `'error'`.
|
|
39
|
+
*/
|
|
40
|
+
severity?: DoctorCheckSeverity;
|
|
19
41
|
};
|
|
20
42
|
export type DoctorReport = {
|
|
21
43
|
checks: DoctorCheck[];
|
|
@@ -23,6 +45,15 @@ export type DoctorReport = {
|
|
|
23
45
|
ok: boolean;
|
|
24
46
|
passed: number;
|
|
25
47
|
failed: number;
|
|
48
|
+
/**
|
|
49
|
+
* Severity-aware summary (slice
|
|
50
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
51
|
+
* repair cycle): count of findings tagged
|
|
52
|
+
* `severity: 'warning'`. Warnings surface in the JSON envelope
|
|
53
|
+
* (`ok: false`) but do NOT flip `summary.ok` and therefore do
|
|
54
|
+
* NOT flip the doctor exit code.
|
|
55
|
+
*/
|
|
56
|
+
warnings: number;
|
|
26
57
|
};
|
|
27
58
|
};
|
|
28
59
|
export type CodegraphCapabilityProbe = {
|
|
@@ -38,6 +69,24 @@ export type DistVersionComparison = {
|
|
|
38
69
|
distReadable: boolean;
|
|
39
70
|
};
|
|
40
71
|
export type DistVersionProbe = () => DistVersionComparison;
|
|
72
|
+
/**
|
|
73
|
+
* Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
74
|
+
* (G3/G4) — probe for the multi-binary drift check. The probe returns
|
|
75
|
+
* the discovered peaks-loop binaries on PATH (with their resolved
|
|
76
|
+
* version + install date) and a `driftDetected` flag. Injected so tests
|
|
77
|
+
* can drive the filesystem walk without monkey-patching
|
|
78
|
+
* `process.env.PATH` or `realpathSync`.
|
|
79
|
+
*/
|
|
80
|
+
export type MultiBinaryDriftInspection = {
|
|
81
|
+
readonly binaries: ReadonlyArray<{
|
|
82
|
+
readonly path: string;
|
|
83
|
+
readonly version: string | null;
|
|
84
|
+
readonly installDate: string | null;
|
|
85
|
+
}>;
|
|
86
|
+
readonly driftDetected: boolean;
|
|
87
|
+
readonly uniqueVersions: ReadonlyArray<string>;
|
|
88
|
+
};
|
|
89
|
+
export type MultiBinaryDriftProbe = () => MultiBinaryDriftInspection;
|
|
41
90
|
export type WorkspaceLayoutInspection = {
|
|
42
91
|
topLevelSessionDirs: string[];
|
|
43
92
|
legacyDotfiles: string[];
|
|
@@ -150,6 +199,12 @@ export type DoctorOptions = {
|
|
|
150
199
|
platform?: NodeJS.Platform;
|
|
151
200
|
/** Injected for the build:dist-version-matches-source check (defaults to compareDistVersion on disk). */
|
|
152
201
|
distVersionProbe?: DistVersionProbe;
|
|
202
|
+
/**
|
|
203
|
+
* Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
204
|
+
* (G3/G4) — injected for the build:multi-binary-drift check (defaults
|
|
205
|
+
* to inspectMultiBinaryDrift against `process.env.PATH`).
|
|
206
|
+
*/
|
|
207
|
+
multiBinaryDriftProbe?: MultiBinaryDriftProbe;
|
|
153
208
|
/** Injected for the build:workspace-layout-canonical check (defaults to inspectWorkspaceLayout on disk). */
|
|
154
209
|
workspaceLayoutProbe?: WorkspaceLayoutProbe;
|
|
155
210
|
/** Injected for the integration:gateguard-peaks-conflict check (defaults to defaultGateguardProbe on disk). */
|
|
@@ -14,5 +14,6 @@ export * from './doctor-service/index.js';
|
|
|
14
14
|
export { compareDistVersion } from './doctor-service/checks/dist-source-version.js';
|
|
15
15
|
export { inspectWorkspaceLayout } from './doctor-service/checks/workspace-layout.js';
|
|
16
16
|
export { collectGateguardEntries } from './doctor-service/checks/gateguard-conflict.js';
|
|
17
|
+
export { inspectMultiBinaryDrift, type PeaksBinaryRecord } from './doctor-service/checks/multi-binary-drift.js';
|
|
17
18
|
export { PLUGINS } from './doctor-service/plugin-registry.js';
|
|
18
|
-
export type { DoctorCheck, DoctorReport, DoctorOptions, DoctorCheckPlugin, DoctorContext, DoctorSkillEntry, DoctorSkillLoadFailure, DoctorSkillsResult, DoctorSkillPresence, CodegraphCapabilityProbe, DistVersionComparison, DistVersionProbe, WorkspaceLayoutInspection, WorkspaceLayoutProbe, GateguardHookLocation, GateguardProbeResult, GateguardProbe } from './doctor-service/types.js';
|
|
19
|
+
export type { DoctorCheck, DoctorReport, DoctorOptions, DoctorCheckPlugin, DoctorContext, DoctorSkillEntry, DoctorSkillLoadFailure, DoctorSkillsResult, DoctorSkillPresence, CodegraphCapabilityProbe, DistVersionComparison, DistVersionProbe, MultiBinaryDriftInspection, MultiBinaryDriftProbe, WorkspaceLayoutInspection, WorkspaceLayoutProbe, GateguardHookLocation, GateguardProbeResult, GateguardProbe } from './doctor-service/types.js';
|
|
@@ -19,6 +19,7 @@ export * from './doctor-service/index.js';
|
|
|
19
19
|
export { compareDistVersion } from './doctor-service/checks/dist-source-version.js';
|
|
20
20
|
export { inspectWorkspaceLayout } from './doctor-service/checks/workspace-layout.js';
|
|
21
21
|
export { collectGateguardEntries } from './doctor-service/checks/gateguard-conflict.js';
|
|
22
|
+
export { inspectMultiBinaryDrift } from './doctor-service/checks/multi-binary-drift.js';
|
|
22
23
|
// Re-export the fixed plugin registry so characterization tests and
|
|
23
24
|
// future CLI surfaces (e.g. `peaks doctor --list-checks`) can enumerate
|
|
24
25
|
// the check pipeline without reaching into a private module path.
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
* See .peaks/_runtime/2026-07-17-session-1d5ac0/rd/slice-3b-doctor.md
|
|
20
20
|
* for the slice-3b Option C decision rationale.
|
|
21
21
|
*/
|
|
22
|
-
export { runDoctor, isWorkspaceInitializedAt, compareDistVersion, inspectWorkspaceLayout, collectGateguardEntries, type DoctorCheck, type DoctorReport, type DoctorOptions, type CodegraphCapabilityProbe, type DistVersionComparison, type DistVersionProbe, type WorkspaceLayoutInspection, type WorkspaceLayoutProbe, type GateguardHookLocation, type GateguardProbeResult, type GateguardProbe, } from './doctor-service.js';
|
|
22
|
+
export { runDoctor, isWorkspaceInitializedAt, compareDistVersion, inspectWorkspaceLayout, inspectMultiBinaryDrift, collectGateguardEntries, type DoctorCheck, type DoctorReport, type DoctorOptions, type CodegraphCapabilityProbe, type DistVersionComparison, type DistVersionProbe, type MultiBinaryDriftInspection, type MultiBinaryDriftProbe, type PeaksBinaryRecord, type WorkspaceLayoutInspection, type WorkspaceLayoutProbe, type GateguardHookLocation, type GateguardProbeResult, type GateguardProbe, } from './doctor-service.js';
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
* See .peaks/_runtime/2026-07-17-session-1d5ac0/rd/slice-3b-doctor.md
|
|
20
20
|
* for the slice-3b Option C decision rationale.
|
|
21
21
|
*/
|
|
22
|
-
export { runDoctor, isWorkspaceInitializedAt, compareDistVersion, inspectWorkspaceLayout, collectGateguardEntries, } from './doctor-service.js';
|
|
22
|
+
export { runDoctor, isWorkspaceInitializedAt, compareDistVersion, inspectWorkspaceLayout, inspectMultiBinaryDrift, collectGateguardEntries, } from './doctor-service.js';
|
|
@@ -23,11 +23,18 @@ import { dirname, join } from 'node:path';
|
|
|
23
23
|
import { randomBytes } from 'node:crypto';
|
|
24
24
|
import { initWorkspace } from '../workspace/workspace-service.js';
|
|
25
25
|
import { projectRootsMatch, stableRealPath } from '../../shared/path-utils.js';
|
|
26
|
-
import { getSessionId, getSessionIdCanonical, getSessionMeta, rotateSessionBinding } from './session-manager.js';
|
|
26
|
+
import { getSessionId, getSessionIdCanonical, getSessionMeta, rotateSessionBinding, setSessionMeta } from './session-manager.js';
|
|
27
27
|
// --- Lower-level helpers the bridge needs (moved verbatim) ---
|
|
28
28
|
const SESSION_FILE = join('_runtime', 'session.json');
|
|
29
29
|
const LEGACY_SESSION_FILE = '.session.json';
|
|
30
30
|
const META_FILE = 'session.json';
|
|
31
|
+
// Slice 2026-08-06-session-outer-cache: per-project file cache for the
|
|
32
|
+
// outer (Claude Code / Trae / IDE) session id. Written by the
|
|
33
|
+
// SessionStart hook via `peaks outer-cache write` so that peaks CLI
|
|
34
|
+
// sub-processes (which do NOT inherit CLAUDE_CODE_SESSION_ID) can still
|
|
35
|
+
// resolve the current outer session. Lives under the gitignored
|
|
36
|
+
// `.peaks/_runtime/` tree, so no .gitignore change is required.
|
|
37
|
+
const OUTER_SESSION_CACHE_FILE = join('_runtime', '.outer-session-cache.json');
|
|
31
38
|
function getLegacySessionFilePath(projectRoot) {
|
|
32
39
|
return join(projectRoot, '.peaks', LEGACY_SESSION_FILE);
|
|
33
40
|
}
|
|
@@ -124,18 +131,58 @@ function writeSessionMeta(projectRoot, sessionId, meta) {
|
|
|
124
131
|
}
|
|
125
132
|
writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
126
133
|
}
|
|
127
|
-
function getCurrentOuterSessionId() {
|
|
134
|
+
function getCurrentOuterSessionId(projectRoot) {
|
|
128
135
|
const peaks = process.env.PEAKS_OUTER_SESSION_ID;
|
|
129
136
|
if (typeof peaks === 'string' && peaks.length > 0)
|
|
130
137
|
return peaks;
|
|
131
138
|
const claude = process.env.CLAUDE_CODE_SESSION_ID;
|
|
132
139
|
if (typeof claude === 'string' && claude.length > 0)
|
|
133
140
|
return claude;
|
|
141
|
+
// Slice 2026-08-06-session-outer-cache (G1): when the peaks CLI runs
|
|
142
|
+
// as a sub-process of Claude Code (or any IDE that does not export
|
|
143
|
+
// CLAUDE_CODE_SESSION_ID into the child env), the env vars above are
|
|
144
|
+
// undefined. Fall back to the per-project file cache written by the
|
|
145
|
+
// SessionStart hook via `peaks outer-cache write`. The file lives
|
|
146
|
+
// under `.peaks/_runtime/` (gitignored) so no .gitignore change is
|
|
147
|
+
// required. Any IO error or non-string `outerSessionId` field is
|
|
148
|
+
// treated as a cache miss — never throw.
|
|
149
|
+
if (projectRoot !== undefined) {
|
|
150
|
+
const cachePath = join(projectRoot, '.peaks', OUTER_SESSION_CACHE_FILE);
|
|
151
|
+
if (existsSync(cachePath)) {
|
|
152
|
+
try {
|
|
153
|
+
const raw = readFileSync(cachePath, 'utf8');
|
|
154
|
+
const parsed = JSON.parse(raw);
|
|
155
|
+
if (parsed !== null &&
|
|
156
|
+
typeof parsed === 'object' &&
|
|
157
|
+
typeof parsed.outerSessionId === 'string' &&
|
|
158
|
+
(parsed.outerSessionId).length > 0) {
|
|
159
|
+
return parsed.outerSessionId;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
163
|
+
// fall through — file missing / malformed JSON / IO error → undefined
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
134
167
|
return undefined;
|
|
135
168
|
}
|
|
136
169
|
export async function ensureSession(projectRoot) {
|
|
137
170
|
const existing = readSessionFile(projectRoot);
|
|
138
171
|
if (existing) {
|
|
172
|
+
// Slice 2026-08-06-session-outer-cache (G3 / AC8-AC11): on every
|
|
173
|
+
// already-bound invocation, stamp the current outer-session-id
|
|
174
|
+
// onto the bound session's meta so the on-disk
|
|
175
|
+
// `.peaks/_runtime/<sid>/session.json` always reflects the latest
|
|
176
|
+
// outer signal — not a stale value captured at session creation
|
|
177
|
+
// time. `setSessionMeta` is read-modify-write: every other field
|
|
178
|
+
// (title / skill / mode / gate / createdAt / lastActivity) is
|
|
179
|
+
// preserved. `getCurrentOuterSessionId(projectRoot)` reads env
|
|
180
|
+
// → file-cache → undefined; an undefined value is NOT written,
|
|
181
|
+
// so the previously-stamped outer (if any) survives.
|
|
182
|
+
const outerSessionId = getCurrentOuterSessionId(projectRoot);
|
|
183
|
+
if (outerSessionId !== undefined) {
|
|
184
|
+
setSessionMeta(projectRoot, existing.sessionId, { outerSessionId });
|
|
185
|
+
}
|
|
139
186
|
return existing.sessionId;
|
|
140
187
|
}
|
|
141
188
|
// Slice 007 — sub-agent session sharing. When the strict-equality
|
|
@@ -165,8 +212,11 @@ export async function ensureSession(projectRoot) {
|
|
|
165
212
|
};
|
|
166
213
|
writeSessionFile(projectRoot, info);
|
|
167
214
|
await initWorkspace({ projectRoot, sessionId });
|
|
168
|
-
// Initialize session metadata inside the session directory
|
|
169
|
-
|
|
215
|
+
// Initialize session metadata inside the session directory.
|
|
216
|
+
// Slice 2026-08-06-session-outer-cache (G1): pass `projectRoot` so
|
|
217
|
+
// the env → file-cache → undefined resolution chain also reads the
|
|
218
|
+
// SessionStart-written cache on first bind.
|
|
219
|
+
const outerSessionId = getCurrentOuterSessionId(projectRoot);
|
|
170
220
|
writeSessionMeta(projectRoot, sessionId, {
|
|
171
221
|
sessionId,
|
|
172
222
|
projectRoot,
|
|
@@ -211,7 +261,10 @@ export async function ensureSession(projectRoot) {
|
|
|
211
261
|
*/
|
|
212
262
|
export async function ensureSessionWithRotation(projectRoot, options) {
|
|
213
263
|
const skipRotate = options?.skipRotateOnOuterMismatch === true;
|
|
214
|
-
|
|
264
|
+
// Slice 2026-08-06-session-outer-cache (G1): pass `projectRoot` so
|
|
265
|
+
// the rotation-decision comparison sees the SessionStart cache
|
|
266
|
+
// when the env vars are unset (sub-process / nested invocation).
|
|
267
|
+
const currentOuterSessionId = getCurrentOuterSessionId(projectRoot);
|
|
215
268
|
// Compute the rotation decision up front. We only rotate when ALL
|
|
216
269
|
// three pre-conditions hold: (a) the current outer session id is
|
|
217
270
|
// defined, (b) the bound session has a recorded outer session id,
|
|
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import { assertSafeSettingsFile } from '../ide/shared/safe-path.js';
|
|
5
5
|
import { atomicWriteJson, readJsonObjectFile } from '../ide/shared/atomic-json.js';
|
|
6
6
|
import { getAdapter } from '../ide/ide-registry.js';
|
|
7
|
+
import { HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL } from './outer-cache-hook-constants.js';
|
|
7
8
|
// --- Module-level defaults (claude-code) -----------------------------------
|
|
8
9
|
// These exports remain for backward compat — tests and downstream callers
|
|
9
10
|
// that only care about Claude Code can keep importing them. The per-IDE
|
|
@@ -181,12 +182,25 @@ export function readInstalledEntriesFromSettings(settings, ide) {
|
|
|
181
182
|
* entry's sentinel is included in the legacy sentinel set so uninstall
|
|
182
183
|
* can find + remove any stale progress-start entry that an older
|
|
183
184
|
* `peaks hooks install` may have written before this slice.
|
|
185
|
+
*
|
|
186
|
+
* Slice 2026-08-06-session-outer-cache: a SessionStart outer-cache primer
|
|
187
|
+
* entry is appended for Claude Code only (SessionStart is a Claude-Code-
|
|
188
|
+
* specific event key today).
|
|
184
189
|
*/
|
|
185
190
|
function resolveHookEntries(ide, _skipProgress = false) {
|
|
186
191
|
const spec = resolveHookSpec(ide);
|
|
187
|
-
|
|
192
|
+
const entries = [
|
|
188
193
|
{ sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
|
|
189
194
|
];
|
|
195
|
+
if (ide === 'claude-code') {
|
|
196
|
+
entries.push({
|
|
197
|
+
sentinel: HOOK_OUTER_CACHE_SENTINEL,
|
|
198
|
+
matcher: '',
|
|
199
|
+
command: HOOK_OUTER_CACHE_COMMAND,
|
|
200
|
+
event: HOOK_OUTER_CACHE_EVENT
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return entries;
|
|
190
204
|
}
|
|
191
205
|
/**
|
|
192
206
|
* Legacy sentinel set used by uninstall + status to find and remove stale
|
|
@@ -198,7 +212,13 @@ function resolveLegacySentinels(ide) {
|
|
|
198
212
|
if (ide === 'trae') {
|
|
199
213
|
return ['peaks hook handle', LEGACY_PROGRESS_START_SENTINEL];
|
|
200
214
|
}
|
|
201
|
-
|
|
215
|
+
// Slice 2026-08-06-session-outer-cache: include the SessionStart outer-cache
|
|
216
|
+
// sentinel so uninstall strips it alongside the gate-enforce entry.
|
|
217
|
+
const base = [HOOK_ENFORCE_SENTINEL, LEGACY_PROGRESS_START_SENTINEL];
|
|
218
|
+
if (ide === 'claude-code') {
|
|
219
|
+
return [...base, HOOK_OUTER_CACHE_SENTINEL];
|
|
220
|
+
}
|
|
221
|
+
return base;
|
|
202
222
|
}
|
|
203
223
|
// --- Slice 2026-07-29-worktree-layer3-deny -----------------------------------
|
|
204
224
|
// Layer 3 of the Worktree Governance 3-layer design (see
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-06-session-outer-cache (G2): per-IDE SessionStart hook
|
|
3
|
+
* constants for the outer-session cache primer. Extracted from
|
|
4
|
+
* `hooks-settings-service.ts` to keep that file under the Karpathy
|
|
5
|
+
* 800 LOC cap.
|
|
6
|
+
*
|
|
7
|
+
* The SessionStart hook is fired by Claude Code (and any future IDE
|
|
8
|
+
* that ships a SessionStart-style event) every time a new outer
|
|
9
|
+
* session begins. It runs `peaks outer-cache write`, which reads
|
|
10
|
+
* PEAKS_OUTER_SESSION_ID ?? CLAUDE_CODE_SESSION_ID from env and
|
|
11
|
+
* persists it to `.peaks/_runtime/.outer-session-cache.json` so the
|
|
12
|
+
* next peaks CLI sub-process can resolve the current outer session
|
|
13
|
+
* via `getCurrentOuterSessionId(projectRoot)`.
|
|
14
|
+
*
|
|
15
|
+
* Only Claude Code supports SessionStart today; future adapters opt
|
|
16
|
+
* in by extending `resolveHookEntries` and adding an adapter-driven
|
|
17
|
+
* substitution for the `${...}` project-dir placeholder.
|
|
18
|
+
*/
|
|
19
|
+
/** Sentinel substring identifying a SessionStart outer-cache hook entry. */
|
|
20
|
+
export declare const HOOK_OUTER_CACHE_SENTINEL = "peaks outer-cache write";
|
|
21
|
+
/** Default (claude-code) SessionStart hook command. */
|
|
22
|
+
export declare const HOOK_OUTER_CACHE_COMMAND = "peaks outer-cache write --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
23
|
+
/** SessionStart hook event key for Claude Code. */
|
|
24
|
+
export declare const HOOK_OUTER_CACHE_EVENT = "SessionStart";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-08-06-session-outer-cache (G2): per-IDE SessionStart hook
|
|
3
|
+
* constants for the outer-session cache primer. Extracted from
|
|
4
|
+
* `hooks-settings-service.ts` to keep that file under the Karpathy
|
|
5
|
+
* 800 LOC cap.
|
|
6
|
+
*
|
|
7
|
+
* The SessionStart hook is fired by Claude Code (and any future IDE
|
|
8
|
+
* that ships a SessionStart-style event) every time a new outer
|
|
9
|
+
* session begins. It runs `peaks outer-cache write`, which reads
|
|
10
|
+
* PEAKS_OUTER_SESSION_ID ?? CLAUDE_CODE_SESSION_ID from env and
|
|
11
|
+
* persists it to `.peaks/_runtime/.outer-session-cache.json` so the
|
|
12
|
+
* next peaks CLI sub-process can resolve the current outer session
|
|
13
|
+
* via `getCurrentOuterSessionId(projectRoot)`.
|
|
14
|
+
*
|
|
15
|
+
* Only Claude Code supports SessionStart today; future adapters opt
|
|
16
|
+
* in by extending `resolveHookEntries` and adding an adapter-driven
|
|
17
|
+
* substitution for the `${...}` project-dir placeholder.
|
|
18
|
+
*/
|
|
19
|
+
/** Sentinel substring identifying a SessionStart outer-cache hook entry. */
|
|
20
|
+
export const HOOK_OUTER_CACHE_SENTINEL = 'peaks outer-cache write';
|
|
21
|
+
/** Default (claude-code) SessionStart hook command. */
|
|
22
|
+
export const HOOK_OUTER_CACHE_COMMAND = `peaks outer-cache write --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
23
|
+
/** SessionStart hook event key for Claude Code. */
|
|
24
|
+
export const HOOK_OUTER_CACHE_EVENT = 'SessionStart';
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { StatusLineModel } from './skill-statusline-service.js';
|
|
2
|
+
import { computeRootSuffix as computeRootSuffixImpl } from './skill-statusline-sid-suffix.js';
|
|
3
|
+
export { formatShortSid } from './skill-statusline-sid-suffix.js';
|
|
4
|
+
export declare const computeRootSuffix: typeof computeRootSuffixImpl;
|
|
2
5
|
/**
|
|
3
6
|
* Pure formatting layer for the Peaks statusLine. Takes the read-only status
|
|
4
7
|
* model and produces the single line Claude Code paints at the bottom of the
|
|
@@ -76,4 +79,3 @@ export declare function applyMarquee(s: string, nowMs: number, capability: Statu
|
|
|
76
79
|
* states all keep the marquee.
|
|
77
80
|
*/
|
|
78
81
|
export declare function renderStatusLine(model: StatusLineModel, options?: StatusLineRenderOptions, env?: NodeJS.ProcessEnv): string;
|
|
79
|
-
export {};
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
+
import { computeRootSuffix as computeRootSuffixImpl, } from './skill-statusline-sid-suffix.js';
|
|
3
|
+
// Re-export so existing test imports
|
|
4
|
+
// (`import { formatShortSid, computeRootSuffix } from '.../skill-statusline-renderer'`)
|
|
5
|
+
// keep working byte-identically after the helper extraction in slice
|
|
6
|
+
// 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
7
|
+
// repair cycle.
|
|
8
|
+
export { formatShortSid } from './skill-statusline-sid-suffix.js';
|
|
9
|
+
export const computeRootSuffix = computeRootSuffixImpl;
|
|
2
10
|
/**
|
|
3
11
|
* Internal: the brand prefix is plain ASCII so a tiny terminal without
|
|
4
12
|
* monospaced UTF-8 still sees a recognizable string. The mountain glyphs
|
|
@@ -186,6 +194,14 @@ function rootLabel(projectRoot) {
|
|
|
186
194
|
return '';
|
|
187
195
|
return basename(projectRoot);
|
|
188
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* `formatShortSid` + `computeRootSuffix` were extracted to
|
|
199
|
+
* `./skill-statusline-sid-suffix.ts` in the slice
|
|
200
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
201
|
+
* repair cycle so the renderer file stays under the Karpathy
|
|
202
|
+
* 800-line cap. Both helpers are re-exported above for back-compat
|
|
203
|
+
* with existing test imports.
|
|
204
|
+
*/
|
|
189
205
|
/**
|
|
190
206
|
* Build the "middle" of the status line — everything between the brand
|
|
191
207
|
* prefix and the project root label. Kept separate so the token layout
|
|
@@ -568,7 +584,27 @@ export function renderStatusLine(model, options, env) {
|
|
|
568
584
|
const noColor = env !== undefined ? isNoColor(env) : false;
|
|
569
585
|
const palette = paletteFor(capability, noColor);
|
|
570
586
|
const root = rootLabel(model.projectRoot);
|
|
571
|
-
|
|
587
|
+
// short-sid suffix (slices
|
|
588
|
+
// - 2026-08-05-statusline-empty-render-and-short-sid-suffix (active only)
|
|
589
|
+
// - 2026-08-05-statusline-sid-only-marker (idle + stale also)
|
|
590
|
+
// ):
|
|
591
|
+
// the project root cell carries ` [shortSid]` after `peaks-loop` whenever
|
|
592
|
+
// a canonical session id resolves. Per-state matrix:
|
|
593
|
+
//
|
|
594
|
+
// active → append sid when sessionId !== null (AC5/PB1)
|
|
595
|
+
// idle → append sid when sessionId !== null (AC1)
|
|
596
|
+
// stale → append sid when sessionId !== null (AC2)
|
|
597
|
+
// invalid-presence → NEVER append sid (G2 — read-error signal must
|
|
598
|
+
// stay loud; showing sid would mask the warning)
|
|
599
|
+
// (AC4)
|
|
600
|
+
//
|
|
601
|
+
// Compact state with presence is still rendered with the active sid logic
|
|
602
|
+
// (the active branch already covered this in the prior slice). Compact state
|
|
603
|
+
// without presence is the same as the underlying idle / invalid-presence /
|
|
604
|
+
// stale branch; the helper inherits that. Pure ASCII (no narrow space /
|
|
605
|
+
// smart quotes), so Windows PowerShell + Git Bash + zsh render byte-
|
|
606
|
+
// identical (AC7 of the prior slice + AC10 of this slice).
|
|
607
|
+
const rootSuffix = computeRootSuffix(model, root, palette);
|
|
572
608
|
const brand = brandText(capability, noColor);
|
|
573
609
|
// Breathing pulse: key off a single wall-clock read at the start of
|
|
574
610
|
// render so the output is deterministic for a given invocation. The
|
|
@@ -27,6 +27,20 @@ export type StatusLineModel = {
|
|
|
27
27
|
ageMs: number | null;
|
|
28
28
|
compact: CompactStatuslineState;
|
|
29
29
|
activeLeaf: StatusLineActiveLeaf | null;
|
|
30
|
+
/**
|
|
31
|
+
* Slice 2026-08-05-statusline-sid-only-marker: the canonical session
|
|
32
|
+
* id resolved for the project root (`getSessionIdCanonical`), or
|
|
33
|
+
* `null` when no project root is bound or no `.peaks/_runtime/<sid>/`
|
|
34
|
+
* session is on disk. The renderer reads this to append ` [shortSid]`
|
|
35
|
+
* to the project name cell for idle / stale states (G1); invalid-
|
|
36
|
+
* presence still suppresses the suffix (G2).
|
|
37
|
+
*
|
|
38
|
+
* Carry-forward for AC1/AC2/AC3: the value is `null` whenever
|
|
39
|
+
* `projectRoot === null` — the renderer relies on this to skip the
|
|
40
|
+
* suffix without re-running `getSessionIdCanonical` against a non-
|
|
41
|
+
* existent root.
|
|
42
|
+
*/
|
|
43
|
+
sessionId: string | null;
|
|
30
44
|
};
|
|
31
45
|
export declare function parseStatusLineStdin(raw: string): StatusLineStdin | null;
|
|
32
46
|
export declare function buildStatusLineModel(stdin: StatusLineStdin | null, nowMs: number): StatusLineModel;
|
|
@@ -122,7 +122,13 @@ function readActiveLeaf(projectRoot, sessionId) {
|
|
|
122
122
|
* Both branches now route through the canonical sid-scoped lease projection
|
|
123
123
|
* (slice 2026-08-05-statusline-sid-scoped-lease-B):
|
|
124
124
|
* - `callerId !== null` → `resolveActiveSkillForCaller` with the canonical
|
|
125
|
-
* (non-legacy) lease projection, filtered to this callerId.
|
|
125
|
+
* (non-legacy) lease projection, filtered to this callerId. When the
|
|
126
|
+
* callerId-filtered resolution returns `source: 'none'` (no lease under
|
|
127
|
+
* this callerId), retry once with `callerId: null` so the read falls back
|
|
128
|
+
* to the session's most-recent in-flight lease. AC4 multi-tenant isolation
|
|
129
|
+
* is preserved: when callerId A DOES have a lease, the first call returns
|
|
130
|
+
* it and the fallback never fires — callerId B's lease is never surfaced
|
|
131
|
+
* to callerId A. (Slice 2026-08-05-statusline-empty-render-and-short-sid-suffix.)
|
|
126
132
|
* - `callerId === null` → enumerate `listPresenceLeases` for the
|
|
127
133
|
* canonical session and pick the most recent in-flight lease. Back-compat
|
|
128
134
|
* for non-IDE callers (e.g. legacy CLI invocations) that have no callerId.
|
|
@@ -134,22 +140,28 @@ function readActiveLeaf(projectRoot, sessionId) {
|
|
|
134
140
|
*/
|
|
135
141
|
function readPresenceReadOnly(projectRoot, callerId) {
|
|
136
142
|
if (callerId !== null) {
|
|
143
|
+
let firstResolution = null;
|
|
137
144
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
145
|
+
firstResolution = resolveActiveSkillForCaller(projectRoot, { callerId });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return { presence: null, invalid: true };
|
|
149
|
+
}
|
|
150
|
+
if (firstResolution.source !== 'none' && firstResolution.skill !== null) {
|
|
142
151
|
return {
|
|
143
152
|
presence: {
|
|
144
|
-
skill:
|
|
145
|
-
...(
|
|
153
|
+
skill: firstResolution.skill,
|
|
154
|
+
...(firstResolution.mode !== null ? { mode: firstResolution.mode } : {}),
|
|
146
155
|
},
|
|
147
156
|
invalid: false,
|
|
148
157
|
};
|
|
149
158
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
// callerId didn't match any lease — fall back to the session's most
|
|
160
|
+
// recent in-flight lease (the callerId === null branch below). This
|
|
161
|
+
// rescues the case where the harness pipes a `CLAUDE_CODE_SESSION_ID`
|
|
162
|
+
// that differs from the active lease's callerId (peaks-code sessions
|
|
163
|
+
// started from a different outer session id, etc.).
|
|
164
|
+
return readPresenceReadOnly(projectRoot, null);
|
|
153
165
|
}
|
|
154
166
|
// callerId === null branch: enumerate the canonical session dir's leases
|
|
155
167
|
// and pick the most recent in-flight lease. This is the back-compat path
|
|
@@ -208,17 +220,30 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
208
220
|
// and reads `.peaks/_runtime/<sid>/compact-lifecycle.json` (or its legacy
|
|
209
221
|
// fallbacks). It replaces the active skill content when kind != 'none'.
|
|
210
222
|
const compact = readCompactState(projectRoot, nowMs);
|
|
223
|
+
// Slice 2026-08-05-statusline-sid-only-marker: resolve the canonical
|
|
224
|
+
// session id once, up front, so the renderer can read it from the model
|
|
225
|
+
// without re-running `getSessionIdCanonical`. `null` when the project
|
|
226
|
+
// root is unbound OR when no `.peaks/_runtime/<sid>/` session exists.
|
|
227
|
+
let sessionId = null;
|
|
228
|
+
if (projectRoot !== null) {
|
|
229
|
+
try {
|
|
230
|
+
sessionId = getSessionIdCanonical(projectRoot);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
sessionId = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
211
236
|
if (projectRoot === null) {
|
|
212
|
-
return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
237
|
+
return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact, activeLeaf: null, sessionId: null };
|
|
213
238
|
}
|
|
214
239
|
// callerId resolves the read-side isolation; back-compat is `null`.
|
|
215
240
|
const callerId = resolveCallerId(stdin);
|
|
216
241
|
const { presence, invalid } = readPresenceReadOnly(projectRoot, callerId);
|
|
217
242
|
if (invalid) {
|
|
218
|
-
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
243
|
+
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
|
|
219
244
|
}
|
|
220
245
|
if (presence === null) {
|
|
221
|
-
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
246
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
|
|
222
247
|
}
|
|
223
248
|
// Session binding: when the presence was stamped with a Claude session id and
|
|
224
249
|
// the live session (from stdin) is a different one, the recorded skill belongs
|
|
@@ -227,7 +252,7 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
227
252
|
// fall back to the time-based behavior below for backward compatibility.
|
|
228
253
|
const liveSessionId = typeof stdin?.session_id === 'string' && stdin.session_id.length > 0 ? stdin.session_id : null;
|
|
229
254
|
if (presence.claudeSessionId && liveSessionId && presence.claudeSessionId !== liveSessionId) {
|
|
230
|
-
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
255
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
|
|
231
256
|
}
|
|
232
257
|
const setAtMs = presence.setAt ? Date.parse(presence.setAt) : Number.NaN;
|
|
233
258
|
const ageMs = Number.isNaN(setAtMs) ? null : nowMs - setAtMs;
|
|
@@ -238,13 +263,12 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
238
263
|
// bee skill (e.g. `peaks-rd`) alongside the orchestrator (e.g. `peaks-code`).
|
|
239
264
|
let activeLeaf = null;
|
|
240
265
|
try {
|
|
241
|
-
const sessionId = getSessionIdCanonical(projectRoot);
|
|
242
266
|
activeLeaf = readActiveLeaf(projectRoot, sessionId);
|
|
243
267
|
}
|
|
244
268
|
catch {
|
|
245
269
|
activeLeaf = null;
|
|
246
270
|
}
|
|
247
|
-
return { state, projectRoot, presence, ageMs, compact, activeLeaf };
|
|
271
|
+
return { state, projectRoot, presence, ageMs, compact, activeLeaf, sessionId };
|
|
248
272
|
}
|
|
249
273
|
/**
|
|
250
274
|
* Read-only compact state resolver. Resolves the canonical session id for the
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure short-sid suffix helpers for the Peaks statusLine.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `skill-statusline-renderer.ts` (slice
|
|
5
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
6
|
+
* repair cycle) to keep the renderer under the Karpathy 800-line cap.
|
|
7
|
+
* The module is I/O-free and only depends on the read-only
|
|
8
|
+
* `StatusLineModel` shape from `skill-statusline-service.ts` plus the
|
|
9
|
+
* `StatusPalette` interface still owned by the renderer. Re-exports
|
|
10
|
+
* both helpers so the existing test surface (`formatShortSid` direct
|
|
11
|
+
* tests + `computeRootSuffix` helper-level tests in
|
|
12
|
+
* `tests/unit/skills/skill-statusline-sid-only-marker.test.ts`) keeps
|
|
13
|
+
* importing from the renderer for back-compat.
|
|
14
|
+
*
|
|
15
|
+
* Pure module — no I/O, no clock dependency, no side effects.
|
|
16
|
+
*/
|
|
17
|
+
import type { StatusLineModel } from './skill-statusline-service.js';
|
|
18
|
+
/**
|
|
19
|
+
* Minimal palette slice consumed by {@link computeRootSuffix}. The full
|
|
20
|
+
* `StatusPalette` is defined as a module-private interface inside the
|
|
21
|
+
* renderer; the suffix helper only needs `trailSeparator`. Re-declared
|
|
22
|
+
* structurally so the helper has no upward import on the renderer.
|
|
23
|
+
*/
|
|
24
|
+
export interface SidSuffixPalette {
|
|
25
|
+
readonly trailSeparator: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format a session id as a short, terminal-friendly tag.
|
|
29
|
+
*
|
|
30
|
+
* Returns the last kebab-segment of `sessionId` (e.g.
|
|
31
|
+
* `2026-08-04-session-3fe1be` → `3fe1be`), or the empty string when
|
|
32
|
+
* the input is empty. Pure; safe for ASCII rendering across
|
|
33
|
+
* PowerShell, Git Bash, and zsh (AC7).
|
|
34
|
+
*
|
|
35
|
+
* Slice 2026-08-05-statusline-empty-render-and-short-sid-suffix.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatShortSid(sessionId: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Compute the project-root cell of the status line. Returns the
|
|
40
|
+
* `<trailSep><root>` portion (and optionally the ` [shortSid]` suffix).
|
|
41
|
+
*
|
|
42
|
+
* Pure helper — no I/O. The `sessionId` is read from the model that the
|
|
43
|
+
* service already resolved once via `getSessionIdCanonical`. Per-state
|
|
44
|
+
* matrix (see the docstring inside `renderStatusLine` in
|
|
45
|
+
* `skill-statusline-renderer.ts` for the AC/PB references):
|
|
46
|
+
*
|
|
47
|
+
* - state === 'invalid-presence' → no sid (G2 invariant)
|
|
48
|
+
* - state === 'idle' | 'stale' | 'active' → sid iff model.sessionId resolves
|
|
49
|
+
* to a non-empty shortSid string
|
|
50
|
+
* - rootLabelText === '' → empty string
|
|
51
|
+
*
|
|
52
|
+
* Exported so tests can drive the helper without spinning up the full
|
|
53
|
+
* renderer (marquee, palette, etc.).
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeRootSuffix(model: StatusLineModel, rootLabelText: string, palette: SidSuffixPalette): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure short-sid suffix helpers for the Peaks statusLine.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `skill-statusline-renderer.ts` (slice
|
|
5
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
6
|
+
* repair cycle) to keep the renderer under the Karpathy 800-line cap.
|
|
7
|
+
* The module is I/O-free and only depends on the read-only
|
|
8
|
+
* `StatusLineModel` shape from `skill-statusline-service.ts` plus the
|
|
9
|
+
* `StatusPalette` interface still owned by the renderer. Re-exports
|
|
10
|
+
* both helpers so the existing test surface (`formatShortSid` direct
|
|
11
|
+
* tests + `computeRootSuffix` helper-level tests in
|
|
12
|
+
* `tests/unit/skills/skill-statusline-sid-only-marker.test.ts`) keeps
|
|
13
|
+
* importing from the renderer for back-compat.
|
|
14
|
+
*
|
|
15
|
+
* Pure module — no I/O, no clock dependency, no side effects.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Format a session id as a short, terminal-friendly tag.
|
|
19
|
+
*
|
|
20
|
+
* Returns the last kebab-segment of `sessionId` (e.g.
|
|
21
|
+
* `2026-08-04-session-3fe1be` → `3fe1be`), or the empty string when
|
|
22
|
+
* the input is empty. Pure; safe for ASCII rendering across
|
|
23
|
+
* PowerShell, Git Bash, and zsh (AC7).
|
|
24
|
+
*
|
|
25
|
+
* Slice 2026-08-05-statusline-empty-render-and-short-sid-suffix.
|
|
26
|
+
*/
|
|
27
|
+
export function formatShortSid(sessionId) {
|
|
28
|
+
if (sessionId.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const last = sessionId.split('-').pop();
|
|
31
|
+
return last ?? sessionId;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the project-root cell of the status line. Returns the
|
|
35
|
+
* `<trailSep><root>` portion (and optionally the ` [shortSid]` suffix).
|
|
36
|
+
*
|
|
37
|
+
* Pure helper — no I/O. The `sessionId` is read from the model that the
|
|
38
|
+
* service already resolved once via `getSessionIdCanonical`. Per-state
|
|
39
|
+
* matrix (see the docstring inside `renderStatusLine` in
|
|
40
|
+
* `skill-statusline-renderer.ts` for the AC/PB references):
|
|
41
|
+
*
|
|
42
|
+
* - state === 'invalid-presence' → no sid (G2 invariant)
|
|
43
|
+
* - state === 'idle' | 'stale' | 'active' → sid iff model.sessionId resolves
|
|
44
|
+
* to a non-empty shortSid string
|
|
45
|
+
* - rootLabelText === '' → empty string
|
|
46
|
+
*
|
|
47
|
+
* Exported so tests can drive the helper without spinning up the full
|
|
48
|
+
* renderer (marquee, palette, etc.).
|
|
49
|
+
*/
|
|
50
|
+
export function computeRootSuffix(model, rootLabelText, palette) {
|
|
51
|
+
if (!rootLabelText)
|
|
52
|
+
return '';
|
|
53
|
+
let suffix = `${palette.trailSeparator}${rootLabelText}`;
|
|
54
|
+
if (model.state === 'invalid-presence')
|
|
55
|
+
return suffix;
|
|
56
|
+
// Defensive: older callers may build a model without `sessionId`
|
|
57
|
+
// (the field is optional in some test fixtures predating the
|
|
58
|
+
// sid-only-marker slice). Treat undefined the same as null.
|
|
59
|
+
if (model.sessionId === null || model.sessionId === undefined)
|
|
60
|
+
return suffix;
|
|
61
|
+
const short = formatShortSid(model.sessionId);
|
|
62
|
+
if (short.length === 0)
|
|
63
|
+
return suffix;
|
|
64
|
+
return `${suffix} [${short}]`;
|
|
65
|
+
}
|