praxis-agent 0.20.2 → 0.20.4
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/application/session-cost-tracker.d.ts +11 -2
- package/dist/application/session-cost-tracker.js +1 -0
- package/dist/cli.js +1 -0
- package/dist/persistence/claude-cost-state-store.d.ts +4 -0
- package/dist/persistence/claude-cost-state-store.js +24 -1
- package/dist/persistence/unknown-cost-sidecar.d.ts +23 -0
- package/dist/persistence/unknown-cost-sidecar.js +125 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModelUsage } from '../core/runtime.js';
|
|
2
|
-
import type { ClaudeSessionCostState } from '../persistence/claude-cost-state-store.js';
|
|
2
|
+
import type { ClaudeSessionCostState, ClaudeStoredModelCostUsage } from '../persistence/claude-cost-state-store.js';
|
|
3
3
|
export interface ClaudeSessionCostTrackerOptions {
|
|
4
4
|
readonly sessionId: string;
|
|
5
5
|
readonly restored?: ClaudeSessionCostState | null;
|
|
@@ -25,7 +25,16 @@ export interface ClaudeSessionDurationsInput {
|
|
|
25
25
|
readonly apiDurationWithoutRetriesMs?: number;
|
|
26
26
|
readonly toolDurationMs?: number;
|
|
27
27
|
}
|
|
28
|
-
export interface ClaudeSessionCostSnapshot
|
|
28
|
+
export interface ClaudeSessionCostSnapshot {
|
|
29
|
+
readonly sessionId: string;
|
|
30
|
+
readonly totalCostUsd: number;
|
|
31
|
+
readonly apiDurationMs: number;
|
|
32
|
+
readonly apiDurationWithoutRetriesMs: number;
|
|
33
|
+
readonly toolDurationMs: number;
|
|
34
|
+
readonly wallDurationMs: number;
|
|
35
|
+
readonly linesAdded: number;
|
|
36
|
+
readonly linesRemoved: number;
|
|
37
|
+
readonly modelUsage: Readonly<Record<string, ClaudeStoredModelCostUsage>>;
|
|
29
38
|
readonly hasUnknownModelCost: boolean;
|
|
30
39
|
}
|
|
31
40
|
export declare class ClaudeSessionCostTracker {
|
|
@@ -71,6 +71,7 @@ export class ClaudeSessionCostTracker {
|
|
|
71
71
|
this.toolDurationMs = restored.toolDurationMs;
|
|
72
72
|
this.linesAdded = restored.linesAdded;
|
|
73
73
|
this.linesRemoved = restored.linesRemoved;
|
|
74
|
+
this.hasUnknownModelCost = restored.hasUnknownModelCost === true;
|
|
74
75
|
for (const [model, usage] of Object.entries(restored.modelUsage)) {
|
|
75
76
|
this.modelUsage.set(model, {
|
|
76
77
|
inputTokens: usage.inputTokens,
|
package/dist/cli.js
CHANGED
|
@@ -776,6 +776,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
776
776
|
const costStateStore = new ClaudeCostStateStore({
|
|
777
777
|
statePath: claudeStatePath,
|
|
778
778
|
projectIdentity: await resolveClaudeProjectIdentity({ cwd }),
|
|
779
|
+
sidecarPath: join(configRoot, 'praxis', 'unknown-cost-sidecar.json'),
|
|
779
780
|
});
|
|
780
781
|
const options = {
|
|
781
782
|
configRoot,
|
|
@@ -16,11 +16,14 @@ export interface ClaudeSessionCostState {
|
|
|
16
16
|
readonly linesAdded: number;
|
|
17
17
|
readonly linesRemoved: number;
|
|
18
18
|
readonly modelUsage: Readonly<Record<string, ClaudeStoredModelCostUsage>>;
|
|
19
|
+
readonly hasUnknownModelCost?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export interface ClaudeCostStateStoreOptions {
|
|
21
22
|
statePath: string;
|
|
22
23
|
projectIdentity: string;
|
|
23
24
|
lockFile?: string;
|
|
25
|
+
sidecarPath?: string;
|
|
26
|
+
sidecarLockFile?: string;
|
|
24
27
|
}
|
|
25
28
|
export interface ClaudeCostStateSaveHooks {
|
|
26
29
|
readonly afterValidation?: () => Promise<void>;
|
|
@@ -30,6 +33,7 @@ export declare class ClaudeCostStateStore {
|
|
|
30
33
|
readonly projectIdentity: string;
|
|
31
34
|
readonly lockFile: string;
|
|
32
35
|
private readonly lease;
|
|
36
|
+
private readonly sidecar;
|
|
33
37
|
constructor(options: ClaudeCostStateStoreOptions);
|
|
34
38
|
load(sessionId: string): Promise<ClaudeSessionCostState | null>;
|
|
35
39
|
save(state: ClaudeSessionCostState, hooks?: ClaudeCostStateSaveHooks): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
6
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
7
7
|
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
8
|
+
import { UnknownCostSidecar } from './unknown-cost-sidecar.js';
|
|
8
9
|
function isErrnoException(err) {
|
|
9
10
|
return (typeof err === 'object' &&
|
|
10
11
|
err !== null &&
|
|
@@ -64,6 +65,7 @@ export class ClaudeCostStateStore {
|
|
|
64
65
|
projectIdentity;
|
|
65
66
|
lockFile;
|
|
66
67
|
lease;
|
|
68
|
+
sidecar;
|
|
67
69
|
constructor(options) {
|
|
68
70
|
assertNonBlank(options.statePath, 'statePath');
|
|
69
71
|
assertNonBlank(options.projectIdentity, 'projectIdentity');
|
|
@@ -73,6 +75,15 @@ export class ClaudeCostStateStore {
|
|
|
73
75
|
options.lockFile ??
|
|
74
76
|
join(dirname(options.statePath), '.praxis-claude-state.lock');
|
|
75
77
|
this.lease = new ExclusiveFileLease(this.lockFile);
|
|
78
|
+
this.sidecar =
|
|
79
|
+
options.sidecarPath === undefined
|
|
80
|
+
? null
|
|
81
|
+
: new UnknownCostSidecar({
|
|
82
|
+
sidecarPath: options.sidecarPath,
|
|
83
|
+
...(options.sidecarLockFile === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { lockFile: options.sidecarLockFile }),
|
|
86
|
+
});
|
|
76
87
|
}
|
|
77
88
|
async load(sessionId) {
|
|
78
89
|
assertNonBlank(sessionId, 'sessionId');
|
|
@@ -80,7 +91,14 @@ export class ClaudeCostStateStore {
|
|
|
80
91
|
if (content === null) {
|
|
81
92
|
return null;
|
|
82
93
|
}
|
|
83
|
-
|
|
94
|
+
const native = this.restoreMatchingSession(content, sessionId);
|
|
95
|
+
if (native === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const hasUnknownModelCost = this.sidecar
|
|
99
|
+
? await this.sidecar.readFlag(sessionId)
|
|
100
|
+
: false;
|
|
101
|
+
return { ...native, hasUnknownModelCost };
|
|
84
102
|
}
|
|
85
103
|
async save(state, hooks = {}) {
|
|
86
104
|
this.validateState(state);
|
|
@@ -92,6 +110,7 @@ export class ClaudeCostStateStore {
|
|
|
92
110
|
finally {
|
|
93
111
|
await handle.release();
|
|
94
112
|
}
|
|
113
|
+
await this.sidecar?.writeFlag(state.sessionId, state.hasUnknownModelCost ?? false);
|
|
95
114
|
}
|
|
96
115
|
async readStateFile() {
|
|
97
116
|
let handle;
|
|
@@ -161,6 +180,10 @@ export class ClaudeCostStateStore {
|
|
|
161
180
|
requireMetric(state.wallDurationMs, 'wallDurationMs');
|
|
162
181
|
requireCounter(state.linesAdded, 'linesAdded');
|
|
163
182
|
requireCounter(state.linesRemoved, 'linesRemoved');
|
|
183
|
+
if (state.hasUnknownModelCost !== undefined &&
|
|
184
|
+
typeof state.hasUnknownModelCost !== 'boolean') {
|
|
185
|
+
throw new TypeError('hasUnknownModelCost must be a boolean');
|
|
186
|
+
}
|
|
164
187
|
const rawUsage = requireObject(state.modelUsage, 'modelUsage');
|
|
165
188
|
for (const [modelKey, rawValue] of Object.entries(rawUsage)) {
|
|
166
189
|
assertNonBlank(modelKey, `modelUsage.${modelKey}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface UnknownCostSidecarOptions {
|
|
2
|
+
sidecarPath: string;
|
|
3
|
+
lockFile?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Praxis-private sidecar for the `/cost` unknown-model flag. The native
|
|
7
|
+
* `.claude.json` project record must stay Claude-compatible, so this flag lives
|
|
8
|
+
* in a disposable JSON document keyed by session ID. Reads fail closed to
|
|
9
|
+
* `false` when the sidecar is missing or malformed.
|
|
10
|
+
*/
|
|
11
|
+
export declare class UnknownCostSidecar {
|
|
12
|
+
private readonly sidecarPath;
|
|
13
|
+
private readonly lockFile;
|
|
14
|
+
private readonly lease;
|
|
15
|
+
constructor(options: UnknownCostSidecarOptions);
|
|
16
|
+
readFlag(sessionId: string): Promise<boolean>;
|
|
17
|
+
writeFlag(sessionId: string, flag: boolean): Promise<void>;
|
|
18
|
+
private readDocument;
|
|
19
|
+
private readContent;
|
|
20
|
+
private parseDocument;
|
|
21
|
+
private acquireLease;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=unknown-cost-sidecar.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
6
|
+
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
7
|
+
const SIDECAR_VERSION = 1;
|
|
8
|
+
function isObject(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function assertNonBlank(value, field) {
|
|
12
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
13
|
+
throw new TypeError(`${field} must not be blank`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function emptyDocument() {
|
|
17
|
+
return { version: SIDECAR_VERSION, sessions: {} };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Praxis-private sidecar for the `/cost` unknown-model flag. The native
|
|
21
|
+
* `.claude.json` project record must stay Claude-compatible, so this flag lives
|
|
22
|
+
* in a disposable JSON document keyed by session ID. Reads fail closed to
|
|
23
|
+
* `false` when the sidecar is missing or malformed.
|
|
24
|
+
*/
|
|
25
|
+
export class UnknownCostSidecar {
|
|
26
|
+
sidecarPath;
|
|
27
|
+
lockFile;
|
|
28
|
+
lease;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
assertNonBlank(options.sidecarPath, 'sidecarPath');
|
|
31
|
+
this.sidecarPath = options.sidecarPath;
|
|
32
|
+
this.lockFile =
|
|
33
|
+
options.lockFile ??
|
|
34
|
+
join(dirname(options.sidecarPath), 'locks', 'unknown-cost-sidecar.lock');
|
|
35
|
+
this.lease = new ExclusiveFileLease(this.lockFile);
|
|
36
|
+
}
|
|
37
|
+
async readFlag(sessionId) {
|
|
38
|
+
assertNonBlank(sessionId, 'sessionId');
|
|
39
|
+
const document = await this.readDocument();
|
|
40
|
+
if (document === null)
|
|
41
|
+
return false;
|
|
42
|
+
return document.sessions[sessionId]?.hasUnknownModelCost === true;
|
|
43
|
+
}
|
|
44
|
+
async writeFlag(sessionId, flag) {
|
|
45
|
+
assertNonBlank(sessionId, 'sessionId');
|
|
46
|
+
if (typeof flag !== 'boolean') {
|
|
47
|
+
throw new TypeError('flag must be a boolean');
|
|
48
|
+
}
|
|
49
|
+
const handle = await this.acquireLease();
|
|
50
|
+
try {
|
|
51
|
+
const document = (await this.readDocument()) ?? emptyDocument();
|
|
52
|
+
const sessions = {
|
|
53
|
+
...document.sessions,
|
|
54
|
+
[sessionId]: { hasUnknownModelCost: flag },
|
|
55
|
+
};
|
|
56
|
+
const content = `${JSON.stringify({ version: SIDECAR_VERSION, sessions }, null, 2)}\n`;
|
|
57
|
+
await writeFileAtomically(this.sidecarPath, content, { mode: 0o600 });
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await handle.release();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async readDocument() {
|
|
64
|
+
const content = await this.readContent();
|
|
65
|
+
if (content === null)
|
|
66
|
+
return null;
|
|
67
|
+
return this.parseDocument(content);
|
|
68
|
+
}
|
|
69
|
+
async readContent() {
|
|
70
|
+
let handle;
|
|
71
|
+
try {
|
|
72
|
+
handle = await open(this.sidecarPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const stat = await handle.stat();
|
|
79
|
+
if (!stat.isFile())
|
|
80
|
+
return null;
|
|
81
|
+
return await handle.readFile('utf8');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await handle.close().catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
parseDocument(content) {
|
|
91
|
+
let root;
|
|
92
|
+
try {
|
|
93
|
+
root = JSON.parse(content);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!isObject(root) || root.version !== SIDECAR_VERSION)
|
|
99
|
+
return null;
|
|
100
|
+
const rawSessions = root.sessions;
|
|
101
|
+
if (!isObject(rawSessions))
|
|
102
|
+
return null;
|
|
103
|
+
const sessions = {};
|
|
104
|
+
for (const [sessionId, value] of Object.entries(rawSessions)) {
|
|
105
|
+
if (sessionId.trim() === '')
|
|
106
|
+
return null;
|
|
107
|
+
if (!isObject(value))
|
|
108
|
+
return null;
|
|
109
|
+
if (typeof value.hasUnknownModelCost !== 'boolean')
|
|
110
|
+
return null;
|
|
111
|
+
sessions[sessionId] = { hasUnknownModelCost: value.hasUnknownModelCost };
|
|
112
|
+
}
|
|
113
|
+
return { version: SIDECAR_VERSION, sessions };
|
|
114
|
+
}
|
|
115
|
+
async acquireLease() {
|
|
116
|
+
for (let attempt = 0; attempt < 400; attempt += 1) {
|
|
117
|
+
const handle = await this.lease.tryAcquire();
|
|
118
|
+
if (handle)
|
|
119
|
+
return handle;
|
|
120
|
+
await sleep(5);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Timed out acquiring unknown-cost sidecar lock for ${this.sidecarPath}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=unknown-cost-sidecar.js.map
|