niahere 0.4.2 → 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 +1 -1
- package/src/channels/sms.ts +5 -2
- package/src/channels/whatsapp.ts +7 -4
- package/src/chat/engine.ts +9 -4
- package/src/core/scheduler.ts +4 -4
- package/src/db/models/message.ts +7 -25
- package/src/db/models/session.ts +16 -11
package/package.json
CHANGED
package/src/channels/sms.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/channels/whatsapp.ts
CHANGED
|
@@ -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(
|
|
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))
|
|
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))
|
|
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
|
-
|
|
253
|
+
throw err;
|
|
251
254
|
}
|
|
252
255
|
await this.postMessage(remoteE164, "", [mediaUrl]);
|
|
253
256
|
}
|
package/src/chat/engine.ts
CHANGED
|
@@ -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
|
-
//
|
|
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;
|
package/src/core/scheduler.ts
CHANGED
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
}
|
package/src/db/models/message.ts
CHANGED
|
@@ -4,10 +4,15 @@ import type { SaveMessageParams, RoomStats, RecentMessage, SearchResult, Session
|
|
|
4
4
|
|
|
5
5
|
export type DeliveryStatus = "pending" | "sent" | "failed";
|
|
6
6
|
|
|
7
|
-
export async function save(
|
|
7
|
+
export async function save(
|
|
8
|
+
params: SaveMessageParams & { deliveryStatus?: DeliveryStatus; metadata?: Record<string, unknown> },
|
|
9
|
+
): Promise<number> {
|
|
8
10
|
const sql = getSql();
|
|
9
11
|
const status = params.deliveryStatus || "sent";
|
|
10
|
-
|
|
12
|
+
// Pass the object straight through: the postgres driver serializes it to a
|
|
13
|
+
// real jsonb object. Pre-stringifying stores a double-encoded string scalar,
|
|
14
|
+
// which silently breaks every `metadata->>'key'` filter (e.g. notifications).
|
|
15
|
+
const meta = params.metadata ? sql.json(params.metadata as Parameters<typeof sql.json>[0]) : null;
|
|
11
16
|
const rows = await sql`
|
|
12
17
|
INSERT INTO messages (session_id, room, sender, content, is_from_agent, delivery_status, metadata)
|
|
13
18
|
VALUES (${params.sessionId}, ${params.room}, ${params.sender}, ${params.content}, ${params.isFromAgent}, ${status}, ${meta})
|
|
@@ -21,29 +26,6 @@ export async function updateDeliveryStatus(id: number, status: DeliveryStatus):
|
|
|
21
26
|
await sql`UPDATE messages SET delivery_status = ${status} WHERE id = ${id}`;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
export async function getUndelivered(room?: string): Promise<Array<{ id: number; room: string; content: string; createdAt: string }>> {
|
|
25
|
-
const sql = getSql();
|
|
26
|
-
const rows = room
|
|
27
|
-
? await sql`
|
|
28
|
-
SELECT id, room, content, created_at
|
|
29
|
-
FROM messages
|
|
30
|
-
WHERE delivery_status = 'failed' AND is_from_agent = true AND room = ${room}
|
|
31
|
-
ORDER BY created_at ASC
|
|
32
|
-
`
|
|
33
|
-
: await sql`
|
|
34
|
-
SELECT id, room, content, created_at
|
|
35
|
-
FROM messages
|
|
36
|
-
WHERE delivery_status = 'failed' AND is_from_agent = true
|
|
37
|
-
ORDER BY created_at ASC
|
|
38
|
-
`;
|
|
39
|
-
return rows.map((r) => ({
|
|
40
|
-
id: r.id,
|
|
41
|
-
room: r.room,
|
|
42
|
-
content: r.content,
|
|
43
|
-
createdAt: String(r.created_at),
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
29
|
export async function getRecent(limit = 20, room?: string): Promise<RecentMessage[]> {
|
|
48
30
|
const sql = getSql();
|
|
49
31
|
const rows = room
|
package/src/db/models/session.ts
CHANGED
|
@@ -141,7 +141,10 @@ export async function accumulateMetadata(id: string, resultMeta: Record<string,
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
// Bind deltas as jsonb via sql.json — pre-stringifying would store a
|
|
145
|
+
// double-encoded string scalar, making every `->>` extraction return NULL
|
|
146
|
+
// and silently zeroing all accumulated totals.
|
|
147
|
+
const delta = sql.json({
|
|
145
148
|
total_cost_usd: (resultMeta.cost_usd as number) || 0,
|
|
146
149
|
total_turns: (resultMeta.turns as number) || 0,
|
|
147
150
|
total_duration_ms: (resultMeta.duration_ms as number) || 0,
|
|
@@ -152,22 +155,24 @@ export async function accumulateMetadata(id: string, resultMeta: Record<string,
|
|
|
152
155
|
total_cache_creation_tokens: cacheCreationTokens,
|
|
153
156
|
message_count: 1,
|
|
154
157
|
models_used: newModels,
|
|
155
|
-
channel: resultMeta.channel || null,
|
|
158
|
+
channel: (resultMeta.channel as string) || null,
|
|
156
159
|
});
|
|
160
|
+
const modelsDelta = sql.json(newModels);
|
|
157
161
|
|
|
158
162
|
// Atomic accumulate — no read-then-write race
|
|
159
163
|
await sql`
|
|
160
164
|
UPDATE sessions SET metadata = jsonb_build_object(
|
|
161
|
-
'total_cost_usd', COALESCE((metadata->>'total_cost_usd')::real, 0) + (${delta}
|
|
162
|
-
'total_turns', COALESCE((metadata->>'total_turns')::int, 0) + (${delta}
|
|
163
|
-
'total_duration_ms', COALESCE((metadata->>'total_duration_ms')::real, 0) + (${delta}
|
|
164
|
-
'total_duration_api_ms', COALESCE((metadata->>'total_duration_api_ms')::real, 0) + (${delta}
|
|
165
|
-
'total_input_tokens', COALESCE((metadata->>'total_input_tokens')::int, 0) + (${delta}
|
|
166
|
-
'total_output_tokens', COALESCE((metadata->>'total_output_tokens')::int, 0) + (${delta}
|
|
167
|
-
'total_cache_read_tokens', COALESCE((metadata->>'total_cache_read_tokens')::int, 0) + (${delta}
|
|
168
|
-
'total_cache_creation_tokens', COALESCE((metadata->>'total_cache_creation_tokens')::int, 0) + (${delta}
|
|
165
|
+
'total_cost_usd', COALESCE((metadata->>'total_cost_usd')::real, 0) + (${delta}->>'total_cost_usd')::real,
|
|
166
|
+
'total_turns', COALESCE((metadata->>'total_turns')::int, 0) + (${delta}->>'total_turns')::int,
|
|
167
|
+
'total_duration_ms', COALESCE((metadata->>'total_duration_ms')::real, 0) + (${delta}->>'total_duration_ms')::real,
|
|
168
|
+
'total_duration_api_ms', COALESCE((metadata->>'total_duration_api_ms')::real, 0) + (${delta}->>'total_duration_api_ms')::real,
|
|
169
|
+
'total_input_tokens', COALESCE((metadata->>'total_input_tokens')::int, 0) + (${delta}->>'total_input_tokens')::int,
|
|
170
|
+
'total_output_tokens', COALESCE((metadata->>'total_output_tokens')::int, 0) + (${delta}->>'total_output_tokens')::int,
|
|
171
|
+
'total_cache_read_tokens', COALESCE((metadata->>'total_cache_read_tokens')::int, 0) + (${delta}->>'total_cache_read_tokens')::int,
|
|
172
|
+
'total_cache_creation_tokens', COALESCE((metadata->>'total_cache_creation_tokens')::int, 0) + (${delta}->>'total_cache_creation_tokens')::int,
|
|
169
173
|
'message_count', COALESCE((metadata->>'message_count')::int, 0) + 1,
|
|
170
|
-
'models_used', COALESCE(
|
|
174
|
+
'models_used', (SELECT COALESCE(jsonb_agg(DISTINCT e), '[]'::jsonb)
|
|
175
|
+
FROM jsonb_array_elements(COALESCE(metadata->'models_used', '[]'::jsonb) || ${modelsDelta}) AS e),
|
|
171
176
|
'channel', COALESCE(metadata->>'channel', ${(resultMeta.channel as string) || null})
|
|
172
177
|
)
|
|
173
178
|
WHERE id = ${id}
|