@poncho-ai/harness 0.37.2 → 0.38.0

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": "@poncho-ai/harness",
3
- "version": "0.37.2",
3
+ "version": "0.38.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
package/src/harness.ts CHANGED
@@ -1477,9 +1477,10 @@ export class AgentHarness {
1477
1477
  );
1478
1478
  // Register VFS tools
1479
1479
  this.registerIfMissing(createBashTool(this.bashManager));
1480
- this.registerIfMissing(createReadFileTool(engine));
1481
- this.registerIfMissing(createEditFileTool(engine));
1482
- this.registerIfMissing(createWriteFileTool(engine));
1480
+ const getFs = (tenantId: string) => this.bashManager!.getFs(tenantId);
1481
+ this.registerIfMissing(createReadFileTool(getFs));
1482
+ this.registerIfMissing(createEditFileTool(getFs));
1483
+ this.registerIfMissing(createWriteFileTool(getFs));
1483
1484
 
1484
1485
  // --- Isolate (V8 sandboxed code execution) ---
1485
1486
  if (config?.isolate) {
@@ -6,6 +6,22 @@ import type { StateConfig } from "./state.js";
6
6
 
7
7
  export type ReminderStatus = "pending" | "fired" | "cancelled";
8
8
 
9
+ export type RecurrenceType = "daily" | "weekly" | "monthly" | "cron";
10
+
11
+ export interface Recurrence {
12
+ type: RecurrenceType;
13
+ /** Repeat every N units (e.g. every 2 days). Defaults to 1. */
14
+ interval?: number;
15
+ /** For weekly: which days (0=Sun … 6=Sat). */
16
+ daysOfWeek?: number[];
17
+ /** For type "cron": a 5-field cron expression. */
18
+ expression?: string;
19
+ /** Stop recurring after this epoch-ms timestamp. */
20
+ endsAt?: number;
21
+ /** Stop recurring after this many total firings. */
22
+ maxOccurrences?: number;
23
+ }
24
+
9
25
  export interface Reminder {
10
26
  id: string;
11
27
  task: string;
@@ -16,18 +32,24 @@ export interface Reminder {
16
32
  conversationId: string;
17
33
  ownerId?: string;
18
34
  tenantId?: string | null;
35
+ recurrence?: Recurrence | null;
36
+ occurrenceCount?: number;
37
+ }
38
+
39
+ export interface ReminderCreateInput {
40
+ task: string;
41
+ scheduledAt: number;
42
+ timezone?: string;
43
+ conversationId: string;
44
+ ownerId?: string;
45
+ tenantId?: string | null;
46
+ recurrence?: Recurrence | null;
19
47
  }
20
48
 
21
49
  export interface ReminderStore {
22
50
  list(): Promise<Reminder[]>;
23
- create(input: {
24
- task: string;
25
- scheduledAt: number;
26
- timezone?: string;
27
- conversationId: string;
28
- ownerId?: string;
29
- tenantId?: string | null;
30
- }): Promise<Reminder>;
51
+ create(input: ReminderCreateInput): Promise<Reminder>;
52
+ update(id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder>;
31
53
  cancel(id: string): Promise<Reminder>;
32
54
  delete(id: string): Promise<void>;
33
55
  }
@@ -63,14 +85,7 @@ class InMemoryReminderStore implements ReminderStore {
63
85
  return [...this.reminders];
64
86
  }
65
87
 
66
- async create(input: {
67
- task: string;
68
- scheduledAt: number;
69
- timezone?: string;
70
- conversationId: string;
71
- ownerId?: string;
72
- tenantId?: string | null;
73
- }): Promise<Reminder> {
88
+ async create(input: ReminderCreateInput): Promise<Reminder> {
74
89
  const reminder: Reminder = {
75
90
  id: generateId(),
76
91
  task: input.task,
@@ -81,12 +96,23 @@ class InMemoryReminderStore implements ReminderStore {
81
96
  conversationId: input.conversationId,
82
97
  ownerId: input.ownerId,
83
98
  tenantId: input.tenantId,
99
+ recurrence: input.recurrence ?? null,
100
+ occurrenceCount: 0,
84
101
  };
85
102
  this.reminders = pruneStale(this.reminders);
86
103
  this.reminders.push(reminder);
87
104
  return reminder;
88
105
  }
89
106
 
107
+ async update(id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder> {
108
+ const reminder = this.reminders.find((r) => r.id === id);
109
+ if (!reminder) throw new Error(`Reminder "${id}" not found`);
110
+ if (fields.scheduledAt !== undefined) reminder.scheduledAt = fields.scheduledAt;
111
+ if (fields.occurrenceCount !== undefined) reminder.occurrenceCount = fields.occurrenceCount;
112
+ if (fields.status !== undefined) reminder.status = fields.status;
113
+ return reminder;
114
+ }
115
+
90
116
  async cancel(id: string): Promise<Reminder> {
91
117
  const reminder = this.reminders.find((r) => r.id === id);
92
118
  if (!reminder) throw new Error(`Reminder "${id}" not found`);
@@ -102,6 +128,147 @@ class InMemoryReminderStore implements ReminderStore {
102
128
  }
103
129
  }
104
130
 
131
+ // ---------------------------------------------------------------------------
132
+ // Recurrence helpers
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * Given a reminder's current scheduledAt and its recurrence config, compute the
137
+ * next fire time. Returns null if the recurrence is exhausted (maxOccurrences
138
+ * reached or endsAt passed).
139
+ */
140
+ export const computeNextOccurrence = (reminder: Reminder): number | null => {
141
+ const rec = reminder.recurrence;
142
+ if (!rec) return null;
143
+
144
+ const fired = (reminder.occurrenceCount ?? 0) + 1;
145
+ if (rec.maxOccurrences && fired >= rec.maxOccurrences) return null;
146
+
147
+ const interval = rec.interval ?? 1;
148
+ const prev = reminder.scheduledAt;
149
+ let next: number;
150
+
151
+ switch (rec.type) {
152
+ case "daily": {
153
+ next = prev + interval * 24 * 60 * 60 * 1000;
154
+ break;
155
+ }
156
+ case "weekly": {
157
+ if (rec.daysOfWeek && rec.daysOfWeek.length > 0) {
158
+ // Advance to next matching day-of-week
159
+ const d = new Date(prev);
160
+ const days = [...rec.daysOfWeek].sort((a, b) => a - b);
161
+ const currentDay = d.getUTCDay();
162
+ // Find the next day in the list that is strictly after currentDay
163
+ let nextDay = days.find((day) => day > currentDay);
164
+ if (nextDay !== undefined) {
165
+ // Same week
166
+ const delta = nextDay - currentDay;
167
+ next = prev + delta * 24 * 60 * 60 * 1000;
168
+ } else {
169
+ // Wrap to next week (+ interval weeks if interval > 1)
170
+ const delta = (7 * interval) - currentDay + days[0];
171
+ next = prev + delta * 24 * 60 * 60 * 1000;
172
+ }
173
+ } else {
174
+ // No specific days — just advance by N weeks
175
+ next = prev + interval * 7 * 24 * 60 * 60 * 1000;
176
+ }
177
+ break;
178
+ }
179
+ case "monthly": {
180
+ const d = new Date(prev);
181
+ d.setUTCMonth(d.getUTCMonth() + interval);
182
+ next = d.getTime();
183
+ break;
184
+ }
185
+ case "cron": {
186
+ // Minimal cron: parse 5-field expression and find the next matching
187
+ // minute after `prev`. We support basic values, ranges, steps and *.
188
+ if (!rec.expression) return null;
189
+ const parsed = parseCronExpression(rec.expression);
190
+ if (!parsed) return null;
191
+ next = nextCronOccurrence(prev, parsed);
192
+ if (next <= prev) return null; // safety
193
+ break;
194
+ }
195
+ default:
196
+ return null;
197
+ }
198
+
199
+ if (rec.endsAt && next > rec.endsAt) return null;
200
+ return next;
201
+ };
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Minimal cron parser (5-field: min hour dom month dow)
205
+ // ---------------------------------------------------------------------------
206
+
207
+ interface CronFields {
208
+ minutes: Set<number>;
209
+ hours: Set<number>;
210
+ daysOfMonth: Set<number>;
211
+ months: Set<number>;
212
+ daysOfWeek: Set<number>;
213
+ }
214
+
215
+ const expandField = (field: string, min: number, max: number): Set<number> | null => {
216
+ const values = new Set<number>();
217
+ for (const part of field.split(",")) {
218
+ const stepMatch = part.match(/^(.+)\/(\d+)$/);
219
+ const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
220
+ const range = stepMatch ? stepMatch[1] : part;
221
+
222
+ if (range === "*") {
223
+ for (let i = min; i <= max; i += step) values.add(i);
224
+ } else if (range.includes("-")) {
225
+ const [lo, hi] = range.split("-").map(Number);
226
+ if (isNaN(lo) || isNaN(hi)) return null;
227
+ for (let i = lo; i <= hi; i += step) values.add(i);
228
+ } else {
229
+ const n = parseInt(range, 10);
230
+ if (isNaN(n)) return null;
231
+ values.add(n);
232
+ }
233
+ }
234
+ return values;
235
+ };
236
+
237
+ const parseCronExpression = (expr: string): CronFields | null => {
238
+ const parts = expr.trim().split(/\s+/);
239
+ if (parts.length !== 5) return null;
240
+ const minutes = expandField(parts[0], 0, 59);
241
+ const hours = expandField(parts[1], 0, 23);
242
+ const daysOfMonth = expandField(parts[2], 1, 31);
243
+ const months = expandField(parts[3], 1, 12);
244
+ const daysOfWeek = expandField(parts[4], 0, 6);
245
+ if (!minutes || !hours || !daysOfMonth || !months || !daysOfWeek) return null;
246
+ return { minutes, hours, daysOfMonth, months, daysOfWeek };
247
+ };
248
+
249
+ const nextCronOccurrence = (afterMs: number, fields: CronFields): number => {
250
+ // Start from the minute after `afterMs`
251
+ const d = new Date(afterMs);
252
+ d.setUTCSeconds(0, 0);
253
+ d.setUTCMinutes(d.getUTCMinutes() + 1);
254
+
255
+ // Cap search to ~1 year to avoid infinite loops
256
+ const limit = afterMs + 366 * 24 * 60 * 60 * 1000;
257
+ while (d.getTime() < limit) {
258
+ if (
259
+ fields.months.has(d.getUTCMonth() + 1) &&
260
+ fields.daysOfMonth.has(d.getUTCDate()) &&
261
+ fields.daysOfWeek.has(d.getUTCDay()) &&
262
+ fields.hours.has(d.getUTCHours()) &&
263
+ fields.minutes.has(d.getUTCMinutes())
264
+ ) {
265
+ return d.getTime();
266
+ }
267
+ d.setUTCMinutes(d.getUTCMinutes() + 1);
268
+ }
269
+ return afterMs; // no match within a year — treat as exhausted
270
+ };
271
+
105
272
  // ---------------------------------------------------------------------------
106
273
  // Factory
107
274
  // ---------------------------------------------------------------------------
@@ -1,16 +1,18 @@
1
1
  import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
2
- import type { ReminderStore, ReminderStatus } from "./reminder-store.js";
2
+ import type { ReminderStore, ReminderStatus, Recurrence, RecurrenceType } from "./reminder-store.js";
3
3
 
4
4
  const VALID_STATUSES: ReminderStatus[] = ["pending", "cancelled"];
5
+ const VALID_RECURRENCE_TYPES: RecurrenceType[] = ["daily", "weekly", "monthly", "cron"];
5
6
 
6
7
  export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
7
8
  defineTool({
8
9
  name: "set_reminder",
9
10
  description:
10
- "Set a one-time reminder that will fire at the specified date and time. " +
11
+ "Set a reminder that will fire at the specified date and time. " +
11
12
  "Use this when the user asks to be reminded about something. " +
12
13
  "The datetime must be an ISO 8601 string in the future. " +
13
- "When the reminder fires, the task message will be delivered to the user.",
14
+ "When the reminder fires, the task message will be delivered to the user. " +
15
+ "Supports optional recurrence for recurring reminders (daily, weekly, monthly, or cron).",
14
16
  inputSchema: {
15
17
  type: "object",
16
18
  properties: {
@@ -21,13 +23,54 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
21
23
  datetime: {
22
24
  type: "string",
23
25
  description:
24
- "ISO 8601 datetime for when the reminder should fire (e.g. '2026-03-23T09:00:00Z')",
26
+ "ISO 8601 datetime for when the reminder should first fire (e.g. '2026-03-23T09:00:00Z')",
25
27
  },
26
28
  timezone: {
27
29
  type: "string",
28
30
  description:
29
31
  "IANA timezone for interpreting the datetime if it lacks an offset (e.g. 'America/New_York'). Defaults to UTC.",
30
32
  },
33
+ recurrence: {
34
+ type: "object",
35
+ description:
36
+ "Optional. Set this to make the reminder repeat. Omit for a one-time reminder.",
37
+ properties: {
38
+ type: {
39
+ type: "string",
40
+ enum: VALID_RECURRENCE_TYPES,
41
+ description:
42
+ "How often to repeat: 'daily', 'weekly', 'monthly', or 'cron'.",
43
+ },
44
+ interval: {
45
+ type: "number",
46
+ description:
47
+ "Repeat every N units (e.g. 2 = every 2 days/weeks/months). Defaults to 1.",
48
+ },
49
+ daysOfWeek: {
50
+ type: "array",
51
+ items: { type: "number" },
52
+ description:
53
+ "For weekly: which days to fire (0=Sunday, 1=Monday, ..., 6=Saturday).",
54
+ },
55
+ expression: {
56
+ type: "string",
57
+ description:
58
+ "For type 'cron': a 5-field cron expression (e.g. '0 9 * * 1-5' for weekdays at 9am).",
59
+ },
60
+ endsAt: {
61
+ type: "string",
62
+ description:
63
+ "ISO 8601 datetime after which the recurrence should stop.",
64
+ },
65
+ maxOccurrences: {
66
+ type: "number",
67
+ description:
68
+ "Maximum number of times the reminder should fire before stopping.",
69
+ },
70
+ },
71
+ required: ["type"],
72
+ additionalProperties: false,
73
+ },
31
74
  },
32
75
  required: ["task", "datetime"],
33
76
  additionalProperties: false,
@@ -78,6 +121,50 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
78
121
  throw new Error("Reminder datetime must be in the future");
79
122
  }
80
123
 
124
+ // Parse recurrence if provided
125
+ let recurrence: Recurrence | null = null;
126
+ if (input.recurrence && typeof input.recurrence === "object") {
127
+ const rec = input.recurrence as Record<string, unknown>;
128
+ const recType = rec.type as string;
129
+ if (!VALID_RECURRENCE_TYPES.includes(recType as RecurrenceType)) {
130
+ throw new Error(`Invalid recurrence type: "${recType}". Must be one of: ${VALID_RECURRENCE_TYPES.join(", ")}`);
131
+ }
132
+ recurrence = { type: recType as RecurrenceType };
133
+ if (rec.interval !== undefined) {
134
+ const interval = Number(rec.interval);
135
+ if (!Number.isInteger(interval) || interval < 1) {
136
+ throw new Error("recurrence.interval must be a positive integer");
137
+ }
138
+ recurrence.interval = interval;
139
+ }
140
+ if (rec.daysOfWeek !== undefined) {
141
+ if (!Array.isArray(rec.daysOfWeek)) throw new Error("recurrence.daysOfWeek must be an array");
142
+ const days = (rec.daysOfWeek as unknown[]).map(Number);
143
+ if (days.some((d) => !Number.isInteger(d) || d < 0 || d > 6)) {
144
+ throw new Error("recurrence.daysOfWeek values must be integers 0-6");
145
+ }
146
+ recurrence.daysOfWeek = days;
147
+ }
148
+ if (rec.expression !== undefined) {
149
+ if (typeof rec.expression !== "string") throw new Error("recurrence.expression must be a string");
150
+ recurrence.expression = rec.expression;
151
+ }
152
+ if (rec.endsAt !== undefined) {
153
+ const endsAtDate = new Date(rec.endsAt as string);
154
+ if (isNaN(endsAtDate.getTime())) {
155
+ throw new Error(`Invalid recurrence.endsAt: "${rec.endsAt}"`);
156
+ }
157
+ recurrence.endsAt = endsAtDate.getTime();
158
+ }
159
+ if (rec.maxOccurrences !== undefined) {
160
+ const max = Number(rec.maxOccurrences);
161
+ if (!Number.isInteger(max) || max < 1) {
162
+ throw new Error("recurrence.maxOccurrences must be a positive integer");
163
+ }
164
+ recurrence.maxOccurrences = max;
165
+ }
166
+ }
167
+
81
168
  const conversationId = context.conversationId || context.runId;
82
169
  const reminder = await store.create({
83
170
  task,
@@ -85,6 +172,7 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
85
172
  timezone,
86
173
  conversationId,
87
174
  tenantId: context.tenantId,
175
+ recurrence,
88
176
  });
89
177
 
90
178
  return {
@@ -95,6 +183,8 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
95
183
  scheduledAt: new Date(reminder.scheduledAt).toISOString(),
96
184
  timezone: reminder.timezone ?? "UTC",
97
185
  status: reminder.status,
186
+ recurrence: reminder.recurrence ?? undefined,
187
+ occurrenceCount: reminder.occurrenceCount ?? 0,
98
188
  },
99
189
  };
100
190
  },
@@ -105,7 +195,8 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
105
195
  description:
106
196
  "List reminders for this agent. Returns all reminders by default; " +
107
197
  "use the status filter to show only pending or cancelled ones. " +
108
- "Fired reminders are automatically deleted after delivery.",
198
+ "Fired one-time reminders are automatically deleted after delivery. " +
199
+ "Recurring reminders stay active and show their recurrence config and fire count.",
109
200
  inputSchema: {
110
201
  type: "object",
111
202
  properties: {
@@ -135,6 +226,8 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
135
226
  timezone: r.timezone ?? "UTC",
136
227
  status: r.status,
137
228
  createdAt: new Date(r.createdAt).toISOString(),
229
+ recurrence: r.recurrence ?? undefined,
230
+ occurrenceCount: r.occurrenceCount ?? 0,
138
231
  })),
139
232
  count: reminders.length,
140
233
  };
@@ -143,7 +236,10 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
143
236
 
144
237
  defineTool({
145
238
  name: "cancel_reminder",
146
- description: "Cancel a pending reminder by its ID.",
239
+ description:
240
+ "Cancel a pending reminder by its ID. " +
241
+ "This works for both one-time and recurring reminders — " +
242
+ "cancelling a recurring reminder stops all future occurrences.",
147
243
  inputSchema: {
148
244
  type: "object",
149
245
  properties: {
package/src/state.ts CHANGED
@@ -77,11 +77,23 @@ export interface Conversation {
77
77
  updatedAt: number;
78
78
  }
79
79
 
80
+ export interface ConversationCreateInit {
81
+ parentConversationId?: string;
82
+ subagentMeta?: Conversation["subagentMeta"];
83
+ messages?: Message[];
84
+ channelMeta?: Conversation["channelMeta"];
85
+ }
86
+
80
87
  export interface ConversationStore {
81
88
  list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]>;
82
89
  listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
83
90
  get(conversationId: string): Promise<Conversation | undefined>;
84
- create(ownerId?: string, title?: string, tenantId?: string | null): Promise<Conversation>;
91
+ create(
92
+ ownerId?: string,
93
+ title?: string,
94
+ tenantId?: string | null,
95
+ init?: ConversationCreateInit,
96
+ ): Promise<Conversation>;
85
97
  update(conversation: Conversation): Promise<void>;
86
98
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
87
99
  delete(conversationId: string): Promise<boolean>;
@@ -197,16 +209,30 @@ export class InMemoryConversationStore implements ConversationStore {
197
209
  return this.conversations.get(conversationId);
198
210
  }
199
211
 
200
- async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
212
+ async create(
213
+ ownerId = DEFAULT_OWNER,
214
+ title?: string,
215
+ tenantId: string | null = null,
216
+ init?: ConversationCreateInit,
217
+ ): Promise<Conversation> {
201
218
  const now = Date.now();
202
219
  const conversation: Conversation = {
203
220
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
204
221
  title: normalizeTitle(title),
205
- messages: [],
222
+ messages: init?.messages ?? [],
206
223
  ownerId,
207
224
  tenantId,
208
225
  createdAt: now,
209
226
  updatedAt: now,
227
+ ...(init?.parentConversationId !== undefined
228
+ ? { parentConversationId: init.parentConversationId }
229
+ : {}),
230
+ ...(init?.subagentMeta !== undefined
231
+ ? { subagentMeta: init.subagentMeta }
232
+ : {}),
233
+ ...(init?.channelMeta !== undefined
234
+ ? { channelMeta: init.channelMeta }
235
+ : {}),
210
236
  };
211
237
  this.conversations.set(conversation.conversationId, conversation);
212
238
  return conversation;
@@ -1,11 +1,12 @@
1
1
  import type {
2
2
  Conversation,
3
+ ConversationCreateInit,
3
4
  ConversationSummary,
4
5
  PendingSubagentResult,
5
6
  } from "../state.js";
6
7
  import type { MainMemory } from "../memory.js";
7
8
  import type { TodoItem } from "../todo-tools.js";
8
- import type { Reminder } from "../reminder-store.js";
9
+ import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
9
10
 
10
11
  // ---------------------------------------------------------------------------
11
12
  // VFS types
@@ -40,7 +41,12 @@ export interface StorageEngine {
40
41
  conversations: {
41
42
  list(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
42
43
  get(conversationId: string): Promise<Conversation | undefined>;
43
- create(ownerId?: string, title?: string, tenantId?: string | null): Promise<Conversation>;
44
+ create(
45
+ ownerId?: string,
46
+ title?: string,
47
+ tenantId?: string | null,
48
+ init?: ConversationCreateInit,
49
+ ): Promise<Conversation>;
44
50
  update(conversation: Conversation): Promise<void>;
45
51
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
46
52
  delete(conversationId: string): Promise<boolean>;
@@ -67,14 +73,8 @@ export interface StorageEngine {
67
73
  // --- Reminders (replaces ReminderStore) ---
68
74
  reminders: {
69
75
  list(tenantId?: string | null): Promise<Reminder[]>;
70
- create(input: {
71
- task: string;
72
- scheduledAt: number;
73
- timezone?: string;
74
- conversationId: string;
75
- ownerId?: string;
76
- tenantId?: string | null;
77
- }): Promise<Reminder>;
76
+ create(input: ReminderCreateInput): Promise<Reminder>;
77
+ update(id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder>;
78
78
  cancel(id: string): Promise<Reminder>;
79
79
  delete(id: string): Promise<void>;
80
80
  };
@@ -5,12 +5,13 @@
5
5
  import { randomUUID } from "node:crypto";
6
6
  import type {
7
7
  Conversation,
8
+ ConversationCreateInit,
8
9
  ConversationSummary,
9
10
  PendingSubagentResult,
10
11
  } from "../state.js";
11
12
  import type { MainMemory } from "../memory.js";
12
13
  import type { TodoItem } from "../todo-tools.js";
13
- import type { Reminder } from "../reminder-store.js";
14
+ import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
14
15
  import type { StorageEngine, VfsDirEntry, VfsStat } from "./engine.js";
15
16
 
16
17
  // ---------------------------------------------------------------------------
@@ -105,16 +106,26 @@ export class InMemoryEngine implements StorageEngine {
105
106
  ownerId?: string,
106
107
  title?: string,
107
108
  tenantId?: string | null,
109
+ init?: ConversationCreateInit,
108
110
  ): Promise<Conversation> => {
109
111
  const now = Date.now();
110
112
  const conv: Conversation = {
111
113
  conversationId: randomUUID(),
112
114
  title: normalizeTitle(title),
113
- messages: [],
115
+ messages: init?.messages ?? [],
114
116
  ownerId: ownerId ?? DEFAULT_OWNER,
115
117
  tenantId: tenantId === undefined ? null : tenantId,
116
118
  createdAt: now,
117
119
  updatedAt: now,
120
+ ...(init?.parentConversationId !== undefined
121
+ ? { parentConversationId: init.parentConversationId }
122
+ : {}),
123
+ ...(init?.subagentMeta !== undefined
124
+ ? { subagentMeta: init.subagentMeta }
125
+ : {}),
126
+ ...(init?.channelMeta !== undefined
127
+ ? { channelMeta: init.channelMeta }
128
+ : {}),
118
129
  };
119
130
  this.convs.set(conv.conversationId, conv);
120
131
  return conv;
@@ -237,14 +248,7 @@ export class InMemoryEngine implements StorageEngine {
237
248
  return results;
238
249
  },
239
250
 
240
- create: async (input: {
241
- task: string;
242
- scheduledAt: number;
243
- timezone?: string;
244
- conversationId: string;
245
- ownerId?: string;
246
- tenantId?: string | null;
247
- }): Promise<Reminder> => {
251
+ create: async (input: ReminderCreateInput): Promise<Reminder> => {
248
252
  const r: Reminder = {
249
253
  id: randomUUID(),
250
254
  task: input.task,
@@ -255,11 +259,22 @@ export class InMemoryEngine implements StorageEngine {
255
259
  conversationId: input.conversationId,
256
260
  ownerId: input.ownerId,
257
261
  tenantId: input.tenantId,
262
+ recurrence: input.recurrence ?? null,
263
+ occurrenceCount: 0,
258
264
  };
259
265
  this.reminderData.set(r.id, r);
260
266
  return r;
261
267
  },
262
268
 
269
+ update: async (id: string, fields: { scheduledAt?: number; occurrenceCount?: number; status?: ReminderStatus }): Promise<Reminder> => {
270
+ const r = this.reminderData.get(id);
271
+ if (!r) throw new Error(`Reminder ${id} not found`);
272
+ if (fields.scheduledAt !== undefined) r.scheduledAt = fields.scheduledAt;
273
+ if (fields.occurrenceCount !== undefined) r.occurrenceCount = fields.occurrenceCount;
274
+ if (fields.status !== undefined) r.status = fields.status;
275
+ return r;
276
+ },
277
+
263
278
  cancel: async (id: string): Promise<Reminder> => {
264
279
  const r = this.reminderData.get(id);
265
280
  if (!r) throw new Error(`Reminder ${id} not found`);
@@ -174,4 +174,15 @@ export const migrations: Migration[] = [
174
174
  ];
175
175
  },
176
176
  },
177
+ {
178
+ version: 5,
179
+ name: "add_reminder_recurrence",
180
+ up: (d) => {
181
+ const jsonType = d === "sqlite" ? "TEXT" : "JSONB";
182
+ return [
183
+ `ALTER TABLE reminders ADD COLUMN recurrence ${jsonType}`,
184
+ `ALTER TABLE reminders ADD COLUMN occurrence_count INTEGER NOT NULL DEFAULT 0`,
185
+ ];
186
+ },
187
+ },
177
188
  ];