@narumitw/pi-langfuse 0.31.0 → 0.38.0
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/README.md +10 -3
- package/package.json +2 -1
- package/src/config.ts +76 -9
- package/src/langfuse.ts +135 -79
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Run the interactive manager, then choose **Set up Langfuse for this Pi agent dir
|
|
|
48
48
|
/langfuse
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
The setup flow prompts for the secret key, public key, and base URL in the same order Langfuse presents them. Leave either key blank to preserve its existing value when updating a valid config. Leave the base URL blank to use `https://us.cloud.langfuse.com`. The file
|
|
51
|
+
The setup flow prompts for the secret key, public key, and base URL in the same order Langfuse presents them. Leave either key blank to preserve its existing value when updating a valid config. Leave the base URL blank to use `https://us.cloud.langfuse.com`. The first successful setup creates the file. Within one Pi process, updates run in invocation order, reread the latest valid private document, preserve unknown fields, and save atomically with mode `0600`. Malformed JSON or invalid recognized fields block setup and update writes until repaired; errors redact entered and stored credentials. A failed save leaves the current session and prior file unchanged.
|
|
52
52
|
|
|
53
53
|
Configuration belongs to the displayed Pi agent directory, not just the current conversation. Restart each running Pi process after saving; the new connection applies to subsequent sessions in that process. `/reload` is not sufficient because the isolated Langfuse runtime is initialized once per process. In print or JSON mode, edit the file manually because the interactive manager is unavailable.
|
|
54
54
|
|
|
@@ -144,7 +144,11 @@ Completed observations are exported in batches while Pi remains live. Neither `a
|
|
|
144
144
|
/langfuse
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
The command opens one context-aware menu
|
|
147
|
+
The command opens one context-aware standard menu in TUI or RPC mode. Its state lines show the
|
|
148
|
+
current session's tracing state, endpoint, content-capture mode, initialization failure when
|
|
149
|
+
applicable, and private configuration path. It never displays credentials. Escape closes the menu.
|
|
150
|
+
Credential and endpoint text inputs remain extension-owned because they enforce secret-preserving
|
|
151
|
+
setup/update semantics rather than standard navigation.
|
|
148
152
|
|
|
149
153
|
Available actions depend on that state:
|
|
150
154
|
|
|
@@ -153,7 +157,10 @@ Available actions depend on that state:
|
|
|
153
157
|
- **Update Langfuse for this Pi agent directory** appears when a valid config exists.
|
|
154
158
|
- **Show setup and privacy help** explains the agent-directory scope, manual configuration path, and content-capture risk.
|
|
155
159
|
|
|
156
|
-
Connection actions state their agent-directory scope and per-process restart requirement before
|
|
160
|
+
Connection actions state their agent-directory scope and per-process restart requirement before
|
|
161
|
+
selection. Command arguments are intentionally ignored so remembered subcommands cannot silently
|
|
162
|
+
bypass the menu. Print and JSON modes reject the interactive command with an observable error that
|
|
163
|
+
includes current tracing state and the manual configuration path.
|
|
157
164
|
|
|
158
165
|
## 🔐 Privacy
|
|
159
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-langfuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@langfuse/otel": "^5.9.1",
|
|
33
33
|
"@langfuse/tracing": "^5.9.1",
|
|
34
|
+
"@narumitw/pi-tui-kit": "<1",
|
|
34
35
|
"@opentelemetry/api": "^1.9.1",
|
|
35
36
|
"@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
|
|
36
37
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
const CONFIG_FILE_NAME = "pi-langfuse.json";
|
|
7
7
|
export const DEFAULT_BASE_URL = "https://us.cloud.langfuse.com";
|
|
@@ -15,31 +15,66 @@ export interface LangfuseConfig {
|
|
|
15
15
|
captureContent: boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export type LangfuseConfigPatch = Partial<LangfuseConfig>;
|
|
19
|
+
|
|
18
20
|
export type LangfuseConfigResult =
|
|
19
21
|
| { ok: true; config: LangfuseConfig; path: string; warnings: string[] }
|
|
20
22
|
| { ok: false; path: string; warnings: string[]; reason: string };
|
|
21
23
|
|
|
22
24
|
export function langfuseConfigPath(): string {
|
|
23
|
-
return join(
|
|
25
|
+
return join(getAgentDir(), CONFIG_FILE_NAME);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const configSaveQueues = new Map<string, Promise<void>>();
|
|
29
|
+
|
|
30
|
+
export async function waitForLangfuseConfigWrites(path = langfuseConfigPath()): Promise<void> {
|
|
31
|
+
await configSaveQueues.get(path);
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
export
|
|
27
|
-
|
|
34
|
+
export function writeLangfuseConfig(
|
|
35
|
+
patch: LangfuseConfigPatch,
|
|
28
36
|
path = langfuseConfigPath(),
|
|
29
37
|
): Promise<LangfuseConfig> {
|
|
30
|
-
const
|
|
38
|
+
const previous = configSaveQueues.get(path) ?? Promise.resolve();
|
|
39
|
+
const operation = previous.then(() => writeLangfuseConfigNow(patch, path));
|
|
40
|
+
const tail = operation.then(
|
|
41
|
+
() => undefined,
|
|
42
|
+
() => undefined,
|
|
43
|
+
);
|
|
44
|
+
configSaveQueues.set(path, tail);
|
|
45
|
+
void tail.then(() => {
|
|
46
|
+
if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);
|
|
47
|
+
});
|
|
48
|
+
return operation;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function writeLangfuseConfigNow(
|
|
52
|
+
patch: LangfuseConfigPatch,
|
|
53
|
+
path: string,
|
|
54
|
+
): Promise<LangfuseConfig> {
|
|
55
|
+
const currentDocument = await readLangfuseDocumentForUpdate(path);
|
|
56
|
+
const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });
|
|
31
57
|
if (!normalized.ok) throw new Error(normalized.reason);
|
|
58
|
+
const {
|
|
59
|
+
publicKey: _publicKey,
|
|
60
|
+
secretKey: _secretKey,
|
|
61
|
+
baseUrl: _baseUrl,
|
|
62
|
+
environment: _environment,
|
|
63
|
+
release: _release,
|
|
64
|
+
captureContent: _captureContent,
|
|
65
|
+
...unknownFields
|
|
66
|
+
} = currentDocument;
|
|
67
|
+
const nextDocument = { ...unknownFields, ...normalized.config };
|
|
32
68
|
|
|
33
69
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
34
70
|
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
35
71
|
try {
|
|
36
|
-
await writeFile(tempPath, `${JSON.stringify(
|
|
72
|
+
await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}\n`, {
|
|
37
73
|
encoding: "utf8",
|
|
38
74
|
mode: 0o600,
|
|
39
75
|
});
|
|
40
76
|
await chmod(tempPath, 0o600);
|
|
41
77
|
await rename(tempPath, path);
|
|
42
|
-
await chmod(path, 0o600);
|
|
43
78
|
return normalized.config;
|
|
44
79
|
} catch (error) {
|
|
45
80
|
await rm(tempPath, { force: true }).catch(() => undefined);
|
|
@@ -47,9 +82,29 @@ export async function writeLangfuseConfig(
|
|
|
47
82
|
}
|
|
48
83
|
}
|
|
49
84
|
|
|
85
|
+
async function readLangfuseDocumentForUpdate(path: string): Promise<Record<string, unknown>> {
|
|
86
|
+
let parsed: unknown;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (isNodeError(error) && error.code === "ENOENT") return {};
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const normalized = normalizeLangfuseConfig(parsed);
|
|
96
|
+
if (!normalized.ok) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return { ...(parsed as Record<string, unknown>) };
|
|
102
|
+
}
|
|
103
|
+
|
|
50
104
|
export async function loadLangfuseConfig(
|
|
51
105
|
path = langfuseConfigPath(),
|
|
52
106
|
): Promise<LangfuseConfigResult> {
|
|
107
|
+
await configSaveQueues.get(path);
|
|
53
108
|
const warnings: string[] = [];
|
|
54
109
|
const permissionFailure = await ensurePrivatePermissions(path, warnings);
|
|
55
110
|
if (permissionFailure) {
|
|
@@ -61,9 +116,9 @@ export async function loadLangfuseConfig(
|
|
|
61
116
|
};
|
|
62
117
|
}
|
|
63
118
|
|
|
64
|
-
let
|
|
119
|
+
let text: string;
|
|
65
120
|
try {
|
|
66
|
-
|
|
121
|
+
text = await readFile(path, "utf8");
|
|
67
122
|
} catch (error) {
|
|
68
123
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
69
124
|
return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };
|
|
@@ -76,6 +131,18 @@ export async function loadLangfuseConfig(
|
|
|
76
131
|
};
|
|
77
132
|
}
|
|
78
133
|
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text) as unknown;
|
|
137
|
+
} catch {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
path,
|
|
141
|
+
warnings,
|
|
142
|
+
reason: `Failed to parse ${path} as JSON; repair the file before retrying.`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
79
146
|
const normalized = normalizeLangfuseConfig(parsed);
|
|
80
147
|
if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };
|
|
81
148
|
return { ok: true, config: normalized.config, path, warnings };
|
package/src/langfuse.ts
CHANGED
|
@@ -3,9 +3,11 @@ import type {
|
|
|
3
3
|
ExtensionCommandContext,
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_BASE_URL,
|
|
8
9
|
type LangfuseConfig,
|
|
10
|
+
type LangfuseConfigPatch,
|
|
9
11
|
type LangfuseConfigResult,
|
|
10
12
|
loadLangfuseConfig,
|
|
11
13
|
normalizeLangfuseConfig,
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
|
|
21
23
|
interface ExtensionDependencies {
|
|
22
24
|
loadConfig(path?: string): Promise<LangfuseConfigResult>;
|
|
23
|
-
writeConfig(config:
|
|
25
|
+
writeConfig(config: LangfuseConfigPatch, path?: string): Promise<unknown>;
|
|
24
26
|
createBackend(config: LangfuseConfig): Promise<TraceBackend>;
|
|
25
27
|
resolveGitMetadata(cwd: string): Promise<GitMetadata | undefined>;
|
|
26
28
|
}
|
|
@@ -54,79 +56,118 @@ export function createLangfuseExtension(
|
|
|
54
56
|
let hasStoredConfig = false;
|
|
55
57
|
let configurationNotice: string | undefined;
|
|
56
58
|
let sessionGeneration = 0;
|
|
59
|
+
let menuController = new AbortController();
|
|
60
|
+
let configWriteQueue = Promise.resolve();
|
|
57
61
|
let nextAttemptReason: string | undefined;
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
formatNonInteractiveStatus(activeConfig, configPath, initializationError),
|
|
65
|
-
"warning",
|
|
66
|
-
);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const menuGeneration = sessionGeneration;
|
|
71
|
-
const menuRecorder = recorder;
|
|
72
|
-
const menuConfig = activeConfig;
|
|
73
|
-
const choice = await ctx.ui.select(
|
|
74
|
-
formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice),
|
|
75
|
-
[
|
|
76
|
-
...(menuRecorder ? [FLUSH_ACTION] : []),
|
|
77
|
-
hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
|
|
78
|
-
HELP_ACTION,
|
|
79
|
-
],
|
|
63
|
+
async function showLangfuseMenu(ctx: ExtensionCommandContext) {
|
|
64
|
+
if (!ctx.hasUI) {
|
|
65
|
+
ctx.ui.notify(
|
|
66
|
+
formatNonInteractiveStatus(activeConfig, configPath, initializationError),
|
|
67
|
+
"warning",
|
|
80
68
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const menuGeneration = sessionGeneration;
|
|
72
|
+
type Screen = "main";
|
|
73
|
+
type Action = "flush" | "configure" | "help";
|
|
74
|
+
const menu = defineMenu<undefined, Screen, Action>({
|
|
75
|
+
start: "main",
|
|
76
|
+
screens: {
|
|
77
|
+
main: () => ({
|
|
78
|
+
kind: "actions",
|
|
79
|
+
title: "Langfuse",
|
|
80
|
+
lines: formatMenuTitle(
|
|
81
|
+
activeConfig,
|
|
82
|
+
configPath,
|
|
83
|
+
initializationError,
|
|
84
|
+
configurationNotice,
|
|
85
|
+
)
|
|
86
|
+
.split("\n")
|
|
87
|
+
.slice(1),
|
|
88
|
+
items: [
|
|
89
|
+
...(recorder ? [{ id: "flush", label: FLUSH_ACTION, action: "flush" as const }] : []),
|
|
90
|
+
{
|
|
91
|
+
id: "configure",
|
|
92
|
+
label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
|
|
93
|
+
action: "configure",
|
|
94
|
+
},
|
|
95
|
+
{ id: "help", label: HELP_ACTION, action: "help" },
|
|
96
|
+
],
|
|
97
|
+
hint: "close",
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
actions: {
|
|
101
|
+
flush: async () => {
|
|
102
|
+
const menuRecorder = recorder;
|
|
103
|
+
const menuConfig = activeConfig;
|
|
104
|
+
if (!menuRecorder) {
|
|
105
|
+
ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
|
|
106
|
+
return { kind: "close" };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await menuRecorder.flush();
|
|
110
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
111
|
+
ctx.ui.notify("Langfuse traces flushed for this session.", "info");
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
114
|
+
ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
|
|
115
|
+
}
|
|
116
|
+
return { kind: "close" };
|
|
117
|
+
},
|
|
118
|
+
configure: async () => {
|
|
119
|
+
const loaded = await loadConfig(configPath);
|
|
120
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
121
|
+
configPath = loaded.path;
|
|
122
|
+
const next = await promptForConfig(
|
|
123
|
+
ctx,
|
|
124
|
+
loaded.ok ? loaded.config : undefined,
|
|
125
|
+
() => menuGeneration === sessionGeneration,
|
|
114
126
|
);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
if (!next || menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
128
|
+
try {
|
|
129
|
+
const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
|
|
130
|
+
configWriteQueue = write.then(
|
|
131
|
+
() => undefined,
|
|
132
|
+
() => undefined,
|
|
133
|
+
);
|
|
134
|
+
await write;
|
|
135
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
136
|
+
hasStoredConfig = true;
|
|
137
|
+
configurationNotice =
|
|
138
|
+
"Saved; restart each Pi process to use it in subsequent sessions.";
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
`Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
|
|
141
|
+
"info",
|
|
142
|
+
);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
145
|
+
ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
|
|
146
|
+
}
|
|
147
|
+
return { kind: "close" };
|
|
148
|
+
},
|
|
149
|
+
help: async () => {
|
|
150
|
+
ctx.ui.notify(formatHelp(configPath), "info");
|
|
151
|
+
return { kind: "close" };
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
await runMenu(ctx, menu, {
|
|
156
|
+
getState: () => undefined,
|
|
157
|
+
signal: menuController.signal,
|
|
158
|
+
isCurrent: () => menuGeneration === sessionGeneration,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
162
|
+
pi.registerCommand("langfuse", {
|
|
163
|
+
description: "Open interactive Langfuse tracing controls",
|
|
164
|
+
handler: async (_args, ctx) => showLangfuseMenu(ctx),
|
|
126
165
|
});
|
|
127
166
|
|
|
128
167
|
pi.on("session_start", async (_event, ctx) => {
|
|
129
|
-
|
|
168
|
+
const generation = ++sessionGeneration;
|
|
169
|
+
menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
|
|
170
|
+
menuController = new AbortController();
|
|
130
171
|
recorder = undefined;
|
|
131
172
|
activeConfig = undefined;
|
|
132
173
|
configPath = undefined;
|
|
@@ -135,7 +176,10 @@ export function createLangfuseExtension(
|
|
|
135
176
|
configurationNotice = undefined;
|
|
136
177
|
nextAttemptReason = undefined;
|
|
137
178
|
|
|
179
|
+
await configWriteQueue;
|
|
180
|
+
if (generation !== sessionGeneration) return;
|
|
138
181
|
const result = await loadConfig();
|
|
182
|
+
if (generation !== sessionGeneration) return;
|
|
139
183
|
configPath = result.path;
|
|
140
184
|
for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
|
|
141
185
|
if (!result.ok) {
|
|
@@ -147,6 +191,10 @@ export function createLangfuseExtension(
|
|
|
147
191
|
hasStoredConfig = true;
|
|
148
192
|
try {
|
|
149
193
|
const backend = await createBackend(result.config);
|
|
194
|
+
if (generation !== sessionGeneration) {
|
|
195
|
+
await backend.shutdown().catch(() => undefined);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
150
198
|
activeConfig = result.config;
|
|
151
199
|
recorder = new TraceRecorder(backend, {
|
|
152
200
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -155,6 +203,7 @@ export function createLangfuseExtension(
|
|
|
155
203
|
captureContent: result.config.captureContent,
|
|
156
204
|
});
|
|
157
205
|
} catch (error) {
|
|
206
|
+
if (generation !== sessionGeneration) return;
|
|
158
207
|
initializationError = `Langfuse tracing could not start: ${formatError(error, result.config)}`;
|
|
159
208
|
ctx.ui.notify(initializationError, "warning");
|
|
160
209
|
}
|
|
@@ -280,23 +329,27 @@ export function createLangfuseExtension(
|
|
|
280
329
|
});
|
|
281
330
|
|
|
282
331
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
283
|
-
|
|
332
|
+
const generation = ++sessionGeneration;
|
|
333
|
+
menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
|
|
284
334
|
const activeRecorder = recorder;
|
|
285
335
|
const shutdownConfig = activeConfig;
|
|
336
|
+
const pendingConfigWrite = configWriteQueue;
|
|
337
|
+
const shutdownSnapshot = activeRecorder ? contextSnapshot(ctx) : undefined;
|
|
286
338
|
recorder = undefined;
|
|
287
339
|
activeConfig = undefined;
|
|
340
|
+
await pendingConfigWrite;
|
|
288
341
|
if (!activeRecorder) return;
|
|
289
342
|
try {
|
|
290
|
-
|
|
291
|
-
if (event.reason === "quit") await activeRecorder.shutdown(snapshot);
|
|
343
|
+
if (event.reason === "quit") await activeRecorder.shutdown(shutdownSnapshot);
|
|
292
344
|
else {
|
|
293
345
|
activeRecorder.interrupt(
|
|
294
346
|
`Pi session ended before settlement (${event.reason}).`,
|
|
295
|
-
|
|
347
|
+
shutdownSnapshot,
|
|
296
348
|
);
|
|
297
349
|
await activeRecorder.flush();
|
|
298
350
|
}
|
|
299
351
|
} catch (error) {
|
|
352
|
+
if (generation !== sessionGeneration) return;
|
|
300
353
|
ctx.ui.notify(
|
|
301
354
|
`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`,
|
|
302
355
|
"error",
|
|
@@ -417,13 +470,16 @@ function normalizeGitBranch(value: string): string | undefined {
|
|
|
417
470
|
async function promptForConfig(
|
|
418
471
|
ctx: ExtensionCommandContext,
|
|
419
472
|
current: LangfuseConfig | undefined,
|
|
420
|
-
|
|
473
|
+
isCurrent: () => boolean,
|
|
474
|
+
): Promise<LangfuseConfigPatch | undefined> {
|
|
421
475
|
const secretKey = await ctx.ui.input("Langfuse secret key (leave blank to keep existing):");
|
|
476
|
+
if (!isCurrent()) return undefined;
|
|
422
477
|
if (secretKey === undefined) {
|
|
423
478
|
ctx.ui.notify("Cancelled", "info");
|
|
424
479
|
return undefined;
|
|
425
480
|
}
|
|
426
481
|
const publicKey = await ctx.ui.input("Langfuse public key (leave blank to keep existing):");
|
|
482
|
+
if (!isCurrent()) return undefined;
|
|
427
483
|
if (publicKey === undefined) {
|
|
428
484
|
ctx.ui.notify("Cancelled", "info");
|
|
429
485
|
return undefined;
|
|
@@ -432,23 +488,23 @@ async function promptForConfig(
|
|
|
432
488
|
`Langfuse base URL (leave blank for default ${DEFAULT_BASE_URL}):`,
|
|
433
489
|
DEFAULT_BASE_URL,
|
|
434
490
|
);
|
|
491
|
+
if (!isCurrent()) return undefined;
|
|
435
492
|
if (baseUrl === undefined) {
|
|
436
493
|
ctx.ui.notify("Cancelled", "info");
|
|
437
494
|
return undefined;
|
|
438
495
|
}
|
|
439
496
|
|
|
440
|
-
const
|
|
441
|
-
...
|
|
442
|
-
|
|
443
|
-
secretKey: secretKey.trim() || current?.secretKey || "",
|
|
497
|
+
const patch: LangfuseConfigPatch = {
|
|
498
|
+
...(publicKey.trim() ? { publicKey: publicKey.trim() } : {}),
|
|
499
|
+
...(secretKey.trim() ? { secretKey: secretKey.trim() } : {}),
|
|
444
500
|
baseUrl: baseUrl.trim() || DEFAULT_BASE_URL,
|
|
445
|
-
|
|
446
|
-
});
|
|
501
|
+
};
|
|
502
|
+
const normalized = normalizeLangfuseConfig({ ...current, ...patch });
|
|
447
503
|
if (!normalized.ok) {
|
|
448
504
|
ctx.ui.notify(`Invalid Langfuse config: ${normalized.reason}`, "error");
|
|
449
505
|
return undefined;
|
|
450
506
|
}
|
|
451
|
-
return
|
|
507
|
+
return patch;
|
|
452
508
|
}
|
|
453
509
|
|
|
454
510
|
function formatMenuTitle(
|
|
@@ -524,7 +580,7 @@ function formatConfigError(result: Extract<LangfuseConfigResult, { ok: false }>)
|
|
|
524
580
|
return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
|
|
525
581
|
}
|
|
526
582
|
|
|
527
|
-
function formatError(error: unknown, config?:
|
|
583
|
+
function formatError(error: unknown, config?: LangfuseConfigPatch): string {
|
|
528
584
|
let message = error instanceof Error ? error.message : String(error);
|
|
529
585
|
const secrets = [config?.publicKey, config?.secretKey]
|
|
530
586
|
.filter((secret): secret is string => Boolean(secret))
|