niahere 0.4.3 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -131,8 +131,7 @@ class SmsChannel implements Channel {
131
131
 
132
132
  private async sendTo(remoteE164: string, body: string): Promise<void> {
133
133
  if (!this.twilio.sid || !this.twilio.secret) {
134
- log.warn("sms: twilio sid/secret missing, cannot send");
135
- return;
134
+ throw new Error("sms: twilio sid/secret missing, cannot send");
136
135
  }
137
136
  try {
138
137
  const res = await twilioSendMessage({
@@ -147,6 +146,10 @@ class SmsChannel implements Channel {
147
146
  log.info({ to: remoteE164, sid: res.messageSid, status: res.status }, "sms: sent");
148
147
  } catch (err) {
149
148
  log.error({ err, to: remoteE164 }, "sms: send failed");
149
+ // Rethrow so deliver() propagates the failure: the inbound handler falls
150
+ // back to an error reply, and the send_message tool reports "Failed to
151
+ // send" instead of falsely claiming delivery.
152
+ throw err;
150
153
  }
151
154
  }
152
155
 
@@ -121,7 +121,10 @@ class WhatsAppChannel implements Channel {
121
121
  const state = await this.getState(from);
122
122
  chainLock(state, async () => {
123
123
  const newState = await this.restartChat(from);
124
- await this.sendTextTo(from, `New conversation started (room ${this.roomPrefix(from)}-${newState.roomIndex}).`);
124
+ await this.sendTextTo(
125
+ from,
126
+ `New conversation started (room ${this.roomPrefix(from)}-${newState.roomIndex}).`,
127
+ ).catch((err) => log.warn({ err, from }, "whatsapp: failed to send reset confirmation"));
125
128
  });
126
129
  return new Response(EMPTY_TWIML, { status: 200, headers: { "Content-Type": "text/xml" } });
127
130
  }
@@ -231,7 +234,7 @@ class WhatsAppChannel implements Channel {
231
234
  // --- Outbound ---
232
235
 
233
236
  private async sendTextTo(remoteE164: string, body: string): Promise<void> {
234
- if (!this.canSend(remoteE164)) return;
237
+ if (!this.canSend(remoteE164)) throw new Error("whatsapp: cannot send (missing creds or outside 24h window)");
235
238
  const converted = toWhatsAppMarkdown(body);
236
239
  const chunks = chunkText(converted, CHUNK_LIMIT);
237
240
  for (const chunk of chunks) {
@@ -240,14 +243,14 @@ class WhatsAppChannel implements Channel {
240
243
  }
241
244
 
242
245
  private async sendMediaTo(remoteE164: string, data: Buffer, mimeType: string, filename?: string): Promise<void> {
243
- if (!this.canSend(remoteE164)) return;
246
+ if (!this.canSend(remoteE164)) throw new Error("whatsapp: cannot send (missing creds or outside 24h window)");
244
247
  const ext = filename ? extOf(filename) : undefined;
245
248
  let mediaUrl: string;
246
249
  try {
247
250
  mediaUrl = await getTwilioServer().serveMedia(new Uint8Array(data), mimeType, ext);
248
251
  } catch (err) {
249
252
  log.error({ err }, "whatsapp: serveMedia failed");
250
- return;
253
+ throw err;
251
254
  }
252
255
  await this.postMessage(remoteE164, "", [mediaUrl]);
253
256
  }
@@ -37,11 +37,10 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
37
37
  const { room, channel, resume, mcpServers } = opts;
38
38
  let systemPrompt = buildSystemPrompt("chat", channel);
39
39
 
40
- // Inject recent session summaries for continuity
40
+ // Recent session summaries for continuity. Appended AFTER persona selection
41
+ // below — the employee/agent branches replace systemPrompt wholesale, so
42
+ // injecting here would be discarded for those contexts.
41
43
  const sessionContext = await getSessionContext(room);
42
- if (sessionContext) {
43
- systemPrompt += "\n\n" + sessionContext;
44
- }
45
44
 
46
45
  // Context overrides: employee > agent > job > default
47
46
  let cwd = homedir();
@@ -86,6 +85,11 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
86
85
  }
87
86
  }
88
87
 
88
+ // Continuity summaries — appended after persona so employee/agent contexts keep them.
89
+ if (sessionContext) {
90
+ systemPrompt += "\n\n" + sessionContext;
91
+ }
92
+
89
93
  // Watch mode: inject behavior into system prompt
90
94
  if (opts.watchBehavior) {
91
95
  const { channel: watchChannel, behavior } = opts.watchBehavior;
@@ -312,6 +316,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
312
316
  log.warn({ room, to: backends[backendIndex]!.name }, "chat provider down, failing over to next backend");
313
317
  await teardown(); // close the dead session so ensureSession opens the next backend
314
318
  sessionId = null; // a cross-backend session id is meaningless; start fresh
319
+ userSaved = false; // re-save the user turn under the new backend's session
315
320
  continue;
316
321
  }
317
322
  break;
@@ -40,10 +40,10 @@ async function tick(): Promise<void> {
40
40
 
41
41
  for (const job of dueJobs) {
42
42
  if (!job.always && !isWithinActiveHours()) {
43
- try {
44
- const nextRun = computeNextRun(job.scheduleType, job.schedule, config.timezone, new Date());
45
- if (nextRun) await Job.markRun(job.name, nextRun).catch(() => {});
46
- } catch {}
43
+ // Leave next_run_at untouched so the job stays due and fires as soon as
44
+ // active hours resume. Rescheduling here would advance a cron job whose
45
+ // only fire time sits outside the window to the next (also-outside)
46
+ // occurrence — starving it forever.
47
47
  log.info({ job: job.name }, "scheduler: skipping — outside active hours");
48
48
  continue;
49
49
  }
@@ -26,31 +26,6 @@ export async function updateDeliveryStatus(id: number, status: DeliveryStatus):
26
26
  await sql`UPDATE messages SET delivery_status = ${status} WHERE id = ${id}`;
27
27
  }
28
28
 
29
- export async function getUndelivered(
30
- room?: string,
31
- ): Promise<Array<{ id: number; room: string; content: string; createdAt: string }>> {
32
- const sql = getSql();
33
- const rows = room
34
- ? await sql`
35
- SELECT id, room, content, created_at
36
- FROM messages
37
- WHERE delivery_status = 'failed' AND is_from_agent = true AND room = ${room}
38
- ORDER BY created_at ASC
39
- `
40
- : await sql`
41
- SELECT id, room, content, created_at
42
- FROM messages
43
- WHERE delivery_status = 'failed' AND is_from_agent = true
44
- ORDER BY created_at ASC
45
- `;
46
- return rows.map((r) => ({
47
- id: r.id,
48
- room: r.room,
49
- content: r.content,
50
- createdAt: String(r.created_at),
51
- }));
52
- }
53
-
54
29
  export async function getRecent(limit = 20, room?: string): Promise<RecentMessage[]> {
55
30
  const sql = getSql();
56
31
  const rows = room
@@ -171,7 +171,8 @@ export async function accumulateMetadata(id: string, resultMeta: Record<string,
171
171
  'total_cache_read_tokens', COALESCE((metadata->>'total_cache_read_tokens')::int, 0) + (${delta}->>'total_cache_read_tokens')::int,
172
172
  'total_cache_creation_tokens', COALESCE((metadata->>'total_cache_creation_tokens')::int, 0) + (${delta}->>'total_cache_creation_tokens')::int,
173
173
  'message_count', COALESCE((metadata->>'message_count')::int, 0) + 1,
174
- 'models_used', COALESCE(metadata->'models_used', '[]'::jsonb) || ${modelsDelta},
174
+ 'models_used', (SELECT COALESCE(jsonb_agg(DISTINCT e), '[]'::jsonb)
175
+ FROM jsonb_array_elements(COALESCE(metadata->'models_used', '[]'::jsonb) || ${modelsDelta}) AS e),
175
176
  'channel', COALESCE(metadata->>'channel', ${(resultMeta.channel as string) || null})
176
177
  )
177
178
  WHERE id = ${id}