niahere 0.2.48 → 0.2.50

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.2.48",
3
+ "version": "0.2.50",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -367,7 +367,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
367
367
  messageId = await Message.save({ ...saveParams, metadata: undefined });
368
368
  }
369
369
  await Session.touch(sessionId);
370
- await Session.accumulateMetadata(sessionId, { ...metadata, channel }).catch(() => {});
370
+ Session.accumulateMetadata(sessionId, { ...metadata, channel }).catch(() => {});
371
371
  }
372
372
 
373
373
  await ActiveEngine.unregister(room);
package/src/cli/index.ts CHANGED
@@ -276,11 +276,11 @@ switch (command) {
276
276
 
277
277
  case "chat": {
278
278
  const chatArgs = process.argv.slice(3);
279
- const mode = (chatArgs.includes("--new") || chatArgs.includes("-n"))
280
- ? "new" as const
279
+ const mode = (chatArgs.includes("--continue") || chatArgs.includes("-c"))
280
+ ? "continue" as const
281
281
  : (chatArgs.includes("--resume") || chatArgs.includes("-r"))
282
282
  ? "pick" as const
283
- : "continue" as const;
283
+ : "new" as const;
284
284
  const chIdx = chatArgs.indexOf("--channel");
285
285
  const simChannel = chIdx !== -1 && chatArgs[chIdx + 1] ? chatArgs[chIdx + 1] : undefined;
286
286
  await startRepl(mode, simChannel);
@@ -79,10 +79,10 @@ async function tick(): Promise<void> {
79
79
 
80
80
  for (const job of dueJobs) {
81
81
  if (!job.always && !isWithinActiveHours()) {
82
- const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
83
- if (nextRun) {
84
- await Job.markRun(job.name, nextRun).catch(() => {});
85
- }
82
+ try {
83
+ const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
84
+ if (nextRun) await Job.markRun(job.name, nextRun).catch(() => {});
85
+ } catch {}
86
86
  log.info({ job: job.name }, "scheduler: skipping — outside active hours");
87
87
  continue;
88
88
  }
@@ -95,7 +95,14 @@ async function tick(): Promise<void> {
95
95
  log.error({ err, job: job.name }, "scheduler: job failed");
96
96
  });
97
97
 
98
- const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
98
+ let nextRun: Date | null = null;
99
+ try {
100
+ nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
101
+ } catch (err) {
102
+ log.error({ err, job: job.name, schedule: job.schedule }, "scheduler: invalid schedule, disabling job");
103
+ await Job.update(job.name, { enabled: false }).catch(() => {});
104
+ continue;
105
+ }
99
106
  await Job.markRun(job.name, nextRun).catch((err) => {
100
107
  log.error({ err, job: job.name }, "scheduler: failed to update next_run_at");
101
108
  });
@@ -115,18 +115,16 @@ export async function getRecentSummaries(room: string, limit = 3): Promise<Array
115
115
 
116
116
  export async function accumulateMetadata(id: string, resultMeta: Record<string, unknown>): Promise<void> {
117
117
  const sql = getSql();
118
- const rows = await sql`SELECT metadata FROM sessions WHERE id = ${id}`;
119
- const existing = (rows[0]?.metadata as Record<string, unknown>) || {};
120
118
 
121
119
  const modelUsage = resultMeta.model_usage as Record<string, Record<string, number>> | undefined;
122
- const modelsUsed = new Set<string>((existing.models_used as string[]) || []);
123
120
  let inputTokens = 0;
124
121
  let outputTokens = 0;
125
122
  let cacheReadTokens = 0;
126
123
  let cacheCreationTokens = 0;
124
+ const newModels: string[] = [];
127
125
  if (modelUsage) {
128
126
  for (const [model, usage] of Object.entries(modelUsage)) {
129
- modelsUsed.add(model);
127
+ newModels.push(model);
130
128
  inputTokens += usage.inputTokens || 0;
131
129
  outputTokens += usage.outputTokens || 0;
132
130
  cacheReadTokens += usage.cacheReadInputTokens || 0;
@@ -134,21 +132,37 @@ export async function accumulateMetadata(id: string, resultMeta: Record<string,
134
132
  }
135
133
  }
136
134
 
137
- const updated: Record<string, unknown> = {
138
- total_cost_usd: ((existing.total_cost_usd as number) || 0) + ((resultMeta.cost_usd as number) || 0),
139
- total_turns: ((existing.total_turns as number) || 0) + ((resultMeta.turns as number) || 0),
140
- total_duration_ms: ((existing.total_duration_ms as number) || 0) + ((resultMeta.duration_ms as number) || 0),
141
- total_duration_api_ms: ((existing.total_duration_api_ms as number) || 0) + ((resultMeta.duration_api_ms as number) || 0),
142
- total_input_tokens: ((existing.total_input_tokens as number) || 0) + inputTokens,
143
- total_output_tokens: ((existing.total_output_tokens as number) || 0) + outputTokens,
144
- total_cache_read_tokens: ((existing.total_cache_read_tokens as number) || 0) + cacheReadTokens,
145
- total_cache_creation_tokens: ((existing.total_cache_creation_tokens as number) || 0) + cacheCreationTokens,
146
- message_count: ((existing.message_count as number) || 0) + 1,
147
- models_used: [...modelsUsed],
148
- channel: existing.channel || resultMeta.channel,
149
- };
135
+ const delta = JSON.stringify({
136
+ total_cost_usd: (resultMeta.cost_usd as number) || 0,
137
+ total_turns: (resultMeta.turns as number) || 0,
138
+ total_duration_ms: (resultMeta.duration_ms as number) || 0,
139
+ total_duration_api_ms: (resultMeta.duration_api_ms as number) || 0,
140
+ total_input_tokens: inputTokens,
141
+ total_output_tokens: outputTokens,
142
+ total_cache_read_tokens: cacheReadTokens,
143
+ total_cache_creation_tokens: cacheCreationTokens,
144
+ message_count: 1,
145
+ models_used: newModels,
146
+ channel: resultMeta.channel || null,
147
+ });
150
148
 
151
- await sql`UPDATE sessions SET metadata = ${JSON.stringify(updated)} WHERE id = ${id}`;
149
+ // Atomic accumulate no read-then-write race
150
+ await sql`
151
+ UPDATE sessions SET metadata = jsonb_build_object(
152
+ 'total_cost_usd', COALESCE((metadata->>'total_cost_usd')::real, 0) + (${delta}::jsonb->>'total_cost_usd')::real,
153
+ 'total_turns', COALESCE((metadata->>'total_turns')::int, 0) + (${delta}::jsonb->>'total_turns')::int,
154
+ 'total_duration_ms', COALESCE((metadata->>'total_duration_ms')::real, 0) + (${delta}::jsonb->>'total_duration_ms')::real,
155
+ 'total_duration_api_ms', COALESCE((metadata->>'total_duration_api_ms')::real, 0) + (${delta}::jsonb->>'total_duration_api_ms')::real,
156
+ 'total_input_tokens', COALESCE((metadata->>'total_input_tokens')::int, 0) + (${delta}::jsonb->>'total_input_tokens')::int,
157
+ 'total_output_tokens', COALESCE((metadata->>'total_output_tokens')::int, 0) + (${delta}::jsonb->>'total_output_tokens')::int,
158
+ 'total_cache_read_tokens', COALESCE((metadata->>'total_cache_read_tokens')::int, 0) + (${delta}::jsonb->>'total_cache_read_tokens')::int,
159
+ 'total_cache_creation_tokens', COALESCE((metadata->>'total_cache_creation_tokens')::int, 0) + (${delta}::jsonb->>'total_cache_creation_tokens')::int,
160
+ 'message_count', COALESCE((metadata->>'message_count')::int, 0) + 1,
161
+ 'models_used', COALESCE(metadata->'models_used', '[]'::jsonb) || ${JSON.stringify(newModels)}::jsonb,
162
+ 'channel', COALESCE(metadata->>'channel', ${(resultMeta.channel as string) || null})
163
+ )
164
+ WHERE id = ${id}
165
+ `;
152
166
  }
153
167
 
154
168
  export async function getLatestRoomIndex(prefix: string): Promise<number> {