niahere 0.5.1 → 0.5.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niahere",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"private": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
48
|
-
"@anthropic-ai/sdk": "
|
|
49
|
-
"@modelcontextprotocol/sdk": "
|
|
47
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
|
48
|
+
"@anthropic-ai/sdk": "0.115.0",
|
|
49
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
50
50
|
"@slack/bolt": "^4.6.0",
|
|
51
51
|
"cron-parser": "^5.5.0",
|
|
52
52
|
"grammy": "^1.41.1",
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { AgentEvent, Normalizer } from "../types";
|
|
2
2
|
import { truncate } from "../../utils/format-activity";
|
|
3
3
|
import { isRetryable, scopeOf, parseFailure } from "../failure";
|
|
4
|
+
import type { FailoverScope } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Terminal reasons that mean the turn died. The SDK used to report several of
|
|
8
|
+
* these as `completed`, so a dead turn landed in the audit as a successful run.
|
|
9
|
+
* The value is how far the chain should skip; undefined means stop.
|
|
10
|
+
*/
|
|
11
|
+
const DEAD_TURNS: Record<string, FailoverScope | undefined> = {
|
|
12
|
+
api_error: "provider",
|
|
13
|
+
budget_exhausted: undefined,
|
|
14
|
+
malformed_tool_use_exhausted: undefined,
|
|
15
|
+
structured_output_retry_exhausted: undefined,
|
|
16
|
+
tool_deferred_unavailable: undefined,
|
|
17
|
+
turn_setup_failed: undefined,
|
|
18
|
+
};
|
|
4
19
|
|
|
5
20
|
/**
|
|
6
21
|
* Pure reducer: Claude Agent SDK messages → normalized `AgentEvent`s.
|
|
@@ -41,6 +56,17 @@ export class SdkNormalizer implements Normalizer {
|
|
|
41
56
|
return [];
|
|
42
57
|
}
|
|
43
58
|
|
|
59
|
+
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
60
|
+
// Long sessions compact silently; surface it so the transcript records
|
|
61
|
+
// that history was summarized and how much was dropped.
|
|
62
|
+
const meta = msg.compact_metadata ?? {};
|
|
63
|
+
const trigger = meta.trigger === "manual" ? "manual" : "auto";
|
|
64
|
+
const pre = meta.pre_tokens ?? 0;
|
|
65
|
+
const post = meta.post_tokens;
|
|
66
|
+
const shrink = post !== undefined ? `${pre} → ${post} tokens` : `${pre} tokens`;
|
|
67
|
+
return [{ type: "thinking", delta: `context compacted (${trigger}, ${shrink})` }];
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
if (msg.type === "system") {
|
|
45
71
|
// Subagent/task lifecycle (subtype init handled above).
|
|
46
72
|
if (msg.subtype === "task_started" && msg.description) {
|
|
@@ -98,6 +124,16 @@ export class SdkNormalizer implements Normalizer {
|
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
private consumeResult(msg: any): AgentEvent {
|
|
127
|
+
const deadTurn = msg.terminal_reason as string | undefined;
|
|
128
|
+
if (!msg.is_error && deadTurn && deadTurn in DEAD_TURNS) {
|
|
129
|
+
return {
|
|
130
|
+
type: "error",
|
|
131
|
+
message: (msg.errors?.join(", ") as string) || `turn ended: ${deadTurn}`,
|
|
132
|
+
retryable: false,
|
|
133
|
+
failover: DEAD_TURNS[deadTurn],
|
|
134
|
+
terminalReason: deadTurn,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
101
137
|
if (!msg.is_error) {
|
|
102
138
|
return {
|
|
103
139
|
type: "result",
|
|
@@ -126,7 +162,11 @@ export class SdkNormalizer implements Normalizer {
|
|
|
126
162
|
type: "error",
|
|
127
163
|
message: raw,
|
|
128
164
|
retryable: isRetryable(raw),
|
|
129
|
-
|
|
165
|
+
// api_error_status is the reliable signal; prose is the fallback.
|
|
166
|
+
failover: scopeOf(
|
|
167
|
+
{ ...parseFailure(raw), status: typeof msg.api_error_status === "number" ? msg.api_error_status : undefined },
|
|
168
|
+
"provider",
|
|
169
|
+
),
|
|
130
170
|
terminalReason: msg.terminal_reason,
|
|
131
171
|
};
|
|
132
172
|
}
|
|
@@ -14,6 +14,10 @@ import { scopeOf, parseFailure } from "../failure";
|
|
|
14
14
|
* skill budget) and are dropped.
|
|
15
15
|
*/
|
|
16
16
|
export class CodexNormalizer implements Normalizer {
|
|
17
|
+
/** The model this run was launched on, for usage attribution — codex's own
|
|
18
|
+
* events don't name it. */
|
|
19
|
+
constructor(private readonly model?: string) {}
|
|
20
|
+
|
|
17
21
|
private threadId = "";
|
|
18
22
|
private agentText = "";
|
|
19
23
|
private failed = false;
|
|
@@ -35,20 +39,30 @@ export class CodexNormalizer implements Normalizer {
|
|
|
35
39
|
return this.fail(typeof e.message === "string" ? e.message : "");
|
|
36
40
|
case "turn.failed":
|
|
37
41
|
return this.fail(typeof e.error?.message === "string" ? e.error.message : "");
|
|
38
|
-
case "turn.completed":
|
|
42
|
+
case "turn.completed": {
|
|
43
|
+
const input = e.usage?.input_tokens ?? 0;
|
|
44
|
+
const output = e.usage?.output_tokens ?? 0;
|
|
39
45
|
return [
|
|
40
46
|
{
|
|
41
47
|
type: "result",
|
|
42
48
|
text: this.agentText,
|
|
43
|
-
usage: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
usage: { tokens: { input, output } },
|
|
50
|
+
backendSessionId: this.threadId,
|
|
51
|
+
// Same shape the Claude path emits, so one accumulator serves both
|
|
52
|
+
// and a failed-over turn is attributable to the provider that ran it.
|
|
53
|
+
metadata: {
|
|
54
|
+
model_usage: {
|
|
55
|
+
[this.model || "default"]: {
|
|
56
|
+
provider: "codex",
|
|
57
|
+
inputTokens: input,
|
|
58
|
+
outputTokens: output,
|
|
59
|
+
cacheReadInputTokens: e.usage?.cached_input_tokens ?? 0,
|
|
60
|
+
},
|
|
47
61
|
},
|
|
48
62
|
},
|
|
49
|
-
backendSessionId: this.threadId,
|
|
50
63
|
},
|
|
51
64
|
];
|
|
65
|
+
}
|
|
52
66
|
default:
|
|
53
67
|
return [];
|
|
54
68
|
}
|
|
@@ -201,7 +201,7 @@ class CodexSession implements AgentSession {
|
|
|
201
201
|
// Started now, not after exit: an undrained pipe blocks the child.
|
|
202
202
|
const stderr = drainStderr(proc.stderr);
|
|
203
203
|
|
|
204
|
-
const normalizer = new CodexNormalizer();
|
|
204
|
+
const normalizer = new CodexNormalizer(this.ctx.model);
|
|
205
205
|
const stdout = proc.stdout.getReader();
|
|
206
206
|
const lines = readLines(stdout)[Symbol.asyncIterator]();
|
|
207
207
|
let sawTerminal = false;
|
package/src/agent/failure.ts
CHANGED
|
@@ -82,8 +82,10 @@ function scopeOfStatus(status: number, message: string): FailoverScope | undefin
|
|
|
82
82
|
*/
|
|
83
83
|
export function scopeOf(failure: Failure, blank?: FailoverScope): FailoverScope | undefined {
|
|
84
84
|
const t = failure.message.trim();
|
|
85
|
-
|
|
85
|
+
// A status is authoritative even when the message is empty — the prose that
|
|
86
|
+
// accompanies a 429 or 529 is routinely unhelpful.
|
|
86
87
|
if (failure.status !== undefined) return scopeOfStatus(failure.status, t);
|
|
88
|
+
if (!t || t.toLowerCase() === "unknown error") return blank;
|
|
87
89
|
if (MODEL_SCOPED.some((p) => p.test(t))) return "model";
|
|
88
90
|
if (PROVIDER_SCOPED.some((p) => p.test(t))) return "provider";
|
|
89
91
|
return undefined;
|
package/src/db/models/session.ts
CHANGED
|
@@ -122,24 +122,62 @@ export async function getRecentSummaries(
|
|
|
122
122
|
}));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export interface UsageTotals {
|
|
126
|
+
inputTokens: number;
|
|
127
|
+
outputTokens: number;
|
|
128
|
+
cacheReadTokens: number;
|
|
129
|
+
cacheCreationTokens: number;
|
|
130
|
+
models: string[];
|
|
131
|
+
providers: string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
135
|
+
const str = (v: unknown): string | null => (typeof v === "string" && v ? v : null);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fold a result's per-model usage into totals. `provider` matters because a
|
|
139
|
+
* chain that fails over makes the model name alone ambiguous about who served
|
|
140
|
+
* the turn.
|
|
141
|
+
*/
|
|
142
|
+
export function summarizeModelUsage(modelUsage: unknown): UsageTotals {
|
|
143
|
+
const totals: UsageTotals = {
|
|
144
|
+
inputTokens: 0,
|
|
145
|
+
outputTokens: 0,
|
|
146
|
+
cacheReadTokens: 0,
|
|
147
|
+
cacheCreationTokens: 0,
|
|
148
|
+
models: [],
|
|
149
|
+
providers: [],
|
|
150
|
+
};
|
|
151
|
+
if (!modelUsage || typeof modelUsage !== "object") return totals;
|
|
152
|
+
|
|
153
|
+
const models = new Set<string>();
|
|
154
|
+
const providers = new Set<string>();
|
|
155
|
+
for (const [key, raw] of Object.entries(modelUsage as Record<string, unknown>)) {
|
|
156
|
+
const usage = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
|
157
|
+
models.add(str(usage.canonicalModel) ?? key);
|
|
158
|
+
const provider = str(usage.provider);
|
|
159
|
+
if (provider) providers.add(provider);
|
|
160
|
+
totals.inputTokens += num(usage.inputTokens);
|
|
161
|
+
totals.outputTokens += num(usage.outputTokens);
|
|
162
|
+
totals.cacheReadTokens += num(usage.cacheReadInputTokens);
|
|
163
|
+
totals.cacheCreationTokens += num(usage.cacheCreationInputTokens);
|
|
164
|
+
}
|
|
165
|
+
totals.models = [...models];
|
|
166
|
+
totals.providers = [...providers];
|
|
167
|
+
return totals;
|
|
168
|
+
}
|
|
169
|
+
|
|
125
170
|
export async function accumulateMetadata(id: string, resultMeta: Record<string, unknown>): Promise<void> {
|
|
126
171
|
const sql = getSql();
|
|
127
172
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
newModels.push(model);
|
|
137
|
-
inputTokens += usage.inputTokens || 0;
|
|
138
|
-
outputTokens += usage.outputTokens || 0;
|
|
139
|
-
cacheReadTokens += usage.cacheReadInputTokens || 0;
|
|
140
|
-
cacheCreationTokens += usage.cacheCreationInputTokens || 0;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
173
|
+
const {
|
|
174
|
+
inputTokens,
|
|
175
|
+
outputTokens,
|
|
176
|
+
cacheReadTokens,
|
|
177
|
+
cacheCreationTokens,
|
|
178
|
+
models: newModels,
|
|
179
|
+
providers: newProviders,
|
|
180
|
+
} = summarizeModelUsage(resultMeta.model_usage);
|
|
143
181
|
|
|
144
182
|
// Bind deltas as jsonb via sql.json — pre-stringifying would store a
|
|
145
183
|
// double-encoded string scalar, making every `->>` extraction return NULL
|
|
@@ -155,9 +193,11 @@ export async function accumulateMetadata(id: string, resultMeta: Record<string,
|
|
|
155
193
|
total_cache_creation_tokens: cacheCreationTokens,
|
|
156
194
|
message_count: 1,
|
|
157
195
|
models_used: newModels,
|
|
196
|
+
providers_used: newProviders,
|
|
158
197
|
channel: (resultMeta.channel as string) || null,
|
|
159
198
|
});
|
|
160
199
|
const modelsDelta = sql.json(newModels);
|
|
200
|
+
const providersDelta = sql.json(newProviders);
|
|
161
201
|
|
|
162
202
|
// Atomic accumulate — no read-then-write race
|
|
163
203
|
await sql`
|
|
@@ -173,6 +213,8 @@ export async function accumulateMetadata(id: string, resultMeta: Record<string,
|
|
|
173
213
|
'message_count', COALESCE((metadata->>'message_count')::int, 0) + 1,
|
|
174
214
|
'models_used', (SELECT COALESCE(jsonb_agg(DISTINCT e), '[]'::jsonb)
|
|
175
215
|
FROM jsonb_array_elements(COALESCE(metadata->'models_used', '[]'::jsonb) || ${modelsDelta}) AS e),
|
|
216
|
+
'providers_used', (SELECT COALESCE(jsonb_agg(DISTINCT e), '[]'::jsonb)
|
|
217
|
+
FROM jsonb_array_elements(COALESCE(metadata->'providers_used', '[]'::jsonb) || ${providersDelta}) AS e),
|
|
176
218
|
'channel', COALESCE(metadata->>'channel', ${(resultMeta.channel as string) || null})
|
|
177
219
|
)
|
|
178
220
|
WHERE id = ${id}
|