agent-office 0.3.2 → 0.4.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/README.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # agent-office
2
2
 
3
- An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) coding sessions as named coworkers with inter-agent messaging, scheduled tasks, persistent memory, and a terminal UI for human oversight.
3
+ An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) coding sessions as named coworkers with inter-agent messaging, scheduled tasks, and a terminal UI for human oversight.
4
4
 
5
5
  ## Overview
6
6
 
7
- `agent-office` sits between a human operator and one or more AI coding agents running on an OpenCode server. It wraps raw OpenCode sessions with named identities, a messaging system, cron-based scheduling, and per-agent memory -- creating an "office" where AI agents are coworkers that communicate, take direction, and operate autonomously.
7
+ `agent-office` sits between a human operator and one or more AI coding agents running on an OpenCode server. It wraps raw OpenCode sessions with named identities, a messaging system, and cron-based scheduling -- creating an "office" where AI agents are coworkers that communicate, take direction, and operate autonomously.
8
8
 
9
9
  **For humans**, there are three interfaces:
10
10
 
11
- - **`serve`** -- an HTTP server that manages sessions, messages, cron jobs, and memory, backed by PostgreSQL and a running OpenCode server
11
+ - **`serve`** -- an HTTP server that manages sessions, messages, and cron jobs, backed by PostgreSQL and a running OpenCode server
12
12
  - **`manage`** -- a full-screen terminal UI (React Ink) for creating coworkers, sending messages, browsing mail, managing cron jobs, and observing agent activity
13
13
  - **`communicator web`** -- a browser-based chat interface for conversing with a specific agent in real time
14
14
 
15
15
  **For AI agents**, there is a CLI:
16
16
 
17
- - **`worker`** -- subcommands that agents invoke from within their OpenCode sessions to clock in, message coworkers, set status, manage cron jobs, and store memories
17
+ - **`worker`** -- subcommands that agents invoke from within their OpenCode sessions to clock in, message coworkers, set status, and manage cron jobs
18
18
 
19
19
  ```
20
20
  +---------------------+
@@ -27,10 +27,10 @@ An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) co
27
27
  | Ink/React | | :7654 | | |
28
28
  +--------------+ | | +--------------+
29
29
  | CronScheduler |
30
- +--------------+ | MemoryManager | +--------------+
31
- | Communicator |<-->| |<-->| .memory/ |
32
- | Web (HTMX) | | | | SQLite DBs |
33
- | :7655 | +---------+-----------+ +--------------+
30
+ +--------------+ | |
31
+ | Communicator |<-->| |
32
+ | Web (HTMX) | | |
33
+ | :7655 | +---------+-----------+
34
34
  +--------------+ |
35
35
  | /worker/* endpoints
36
36
  +---------+-----------+
@@ -69,7 +69,7 @@ agent-office serve \
69
69
  --password mysecret
70
70
  ```
71
71
 
72
- The server runs migrations automatically on startup, initializes the cron scheduler, and warms up the embedding model for agent memory.
72
+ The server runs migrations automatically on startup and initializes the cron scheduler.
73
73
 
74
74
  ### 2. Open the manager
75
75
 
@@ -97,11 +97,11 @@ Opens a web-based chat interface at `http://127.0.0.1:7655` for real-time conver
97
97
 
98
98
  2. **Clock In** -- The AI agent sees the enrollment message in its session, runs `agent-office worker clock-in <token>`, and receives a welcome briefing with its name, the human manager's identity, all available CLI commands, and a privacy notice.
99
99
 
100
- 3. **Work** -- The agent operates in its OpenCode session. It communicates with the human and other agents by running `agent-office worker send-message`. It can set its status, create cron jobs, and store persistent memories.
100
+ 3. **Work** - - The agent operates in its OpenCode session. It communicates with the human and other agents by running `agent-office worker send-message`. It can set its status and create cron jobs.
101
101
 
102
102
  4. **Message Delivery** -- Messages are stored in PostgreSQL and simultaneously injected as prompts into the recipient's OpenCode session. This means agents see messages immediately in their active session context.
103
103
 
104
- 5. **Reset** -- A session can be reverted to its initial state (clearing all conversation history) and re-enrolled. Memories persist across resets since they are stored separately.
104
+ 5. **Reset** - - A session can be reverted to its initial state (clearing all conversation history) and re-enrolled.
105
105
 
106
106
  6. **Delete** -- Deleting a coworker removes both the OpenCode session and the database record.
107
107
 
@@ -139,7 +139,7 @@ cp .env.example .env
139
139
 
140
140
  ### `agent-office serve`
141
141
 
142
- Starts the HTTP server. Connects to PostgreSQL, runs migrations, initializes the cron scheduler, and warms up the embedding model.
142
+ Starts the HTTP server. Connects to PostgreSQL, runs migrations, and initializes the cron scheduler.
143
143
 
144
144
  ```
145
145
  Options:
@@ -147,7 +147,6 @@ Options:
147
147
  --opencode-url <url> OpenCode server URL (default: http://localhost:4096)
148
148
  --host <host> Bind host (default: 127.0.0.1)
149
149
  --port <port> Bind port (default: 7654)
150
- --memory-path <path> Directory for memory storage (default: ./.memory)
151
150
  --password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
152
151
  ```
153
152
 
@@ -165,7 +164,7 @@ Options:
165
164
 
166
165
  | Screen | Description |
167
166
  |---|---|
168
- | **Coworkers** | Table of all agents showing name, status, mode, session ID, and masked agent code. Keyboard: `c` create, `d` delete, `r` reveal code, `g` regenerate code, `x` revert session, `X` revert all, `t` tail messages, `i` inject text, `m` coworker mail, `M` memories |
167
+ | **Coworkers** | Table of all agents showing name, status, mode, session ID, and masked agent code. Keyboard: `c` create, `d` delete, `r` reveal code, `g` regenerate code, `x` revert session, `X` revert all, `t` tail messages, `i` inject text, `m` coworker mail |
169
168
  | **Send message** | Select a recipient and compose a message |
170
169
  | **My mail** | View received and sent messages. `r` reply, `m` mark read, `a` mark all read. Tab between received/sent |
171
170
  | **Cron jobs** | Table of scheduled tasks with name, coworker, schedule, next run, and status. Create, delete, enable/disable, view history |
@@ -217,11 +216,6 @@ agent-office worker cron enable <token> <id>
217
216
  agent-office worker cron disable <token> <id>
218
217
  agent-office worker cron history <token> <id>
219
218
 
220
- # Persistent memory (survives session resets)
221
- agent-office worker memory add --content "The auth module uses JWT with RS256" <token>
222
- agent-office worker memory search --query "authentication" <token>
223
- agent-office worker memory list <token>
224
- agent-office worker memory forget <token> <memory-id>
225
219
  ```
226
220
 
227
221
  ## REST API
@@ -251,12 +245,7 @@ agent-office worker memory forget <token> <memory-id>
251
245
  | `POST` | `/crons/:id/enable` | Enable a cron job |
252
246
  | `POST` | `/crons/:id/disable` | Disable a cron job |
253
247
  | `GET` | `/crons/:id/history` | Cron execution history. Query: `?limit=N` |
254
- | `GET` | `/sessions/:name/memories` | List memories. Query: `?limit=N` |
255
- | `POST` | `/sessions/:name/memories` | Add a memory. Body: `{ content, metadata? }` |
256
- | `GET` | `/sessions/:name/memories/:id` | Get a memory |
257
- | `PUT` | `/sessions/:name/memories/:id` | Update a memory. Body: `{ content, metadata? }` |
258
- | `DELETE` | `/sessions/:name/memories/:id` | Delete a memory |
259
- | `POST` | `/sessions/:name/memories/search` | Search memories. Body: `{ query, limit? }` |
248
+
260
249
 
261
250
  ### Worker Endpoints (agent code auth via `?code=<uuid>`)
262
251
 
@@ -272,10 +261,7 @@ agent-office worker memory forget <token> <memory-id>
272
261
  | `POST` | `/worker/crons/:id/enable` | Enable own cron job |
273
262
  | `POST` | `/worker/crons/:id/disable` | Disable own cron job |
274
263
  | `GET` | `/worker/crons/:id/history` | View own cron job history |
275
- | `POST` | `/worker/memory/add` | Add a memory |
276
- | `POST` | `/worker/memory/search` | Search memories |
277
- | `GET` | `/worker/memory/list` | List memories |
278
- | `DELETE` | `/worker/memory/:memoryId` | Delete a memory |
264
+
279
265
 
280
266
  ## Architecture
281
267
 
@@ -289,14 +275,6 @@ The server uses PostgreSQL with automatic migrations. Tables:
289
275
  - **`cron_jobs`** -- Scheduled tasks tied to sessions with cron expressions and timezone support
290
276
  - **`cron_history`** -- Execution log for cron jobs with success/failure tracking
291
277
 
292
- ### Memory System
293
-
294
- Each agent gets a private SQLite database (via [fastmemory](https://github.com/nichochar/fastmemory)) stored in the `--memory-path` directory. Memories support:
295
-
296
- - **Hybrid search** -- BM25 full-text search combined with vector semantic search using Reciprocal Rank Fusion
297
- - **Persistence across resets** -- Memories are stored outside the OpenCode session, so they survive session reverts
298
- - **Quantized embeddings** -- Uses `q4` dtype for efficient storage
299
-
300
278
  ### Agentic Coding Server Abstraction
301
279
 
302
280
  The application does not depend on the OpenCode SDK directly. Instead, all OpenCode interactions go through an `AgenticCodingServer` interface (`src/lib/agentic-coding-server.ts`) with six methods:
package/dist/cli.js CHANGED
@@ -13,10 +13,8 @@ program
13
13
  .option("--sqlite <path>", "SQLite database file path (alternative to PostgreSQL)", process.env.AGENT_OFFICE_SQLITE)
14
14
  .option("--host <host>", "Host to bind to", "127.0.0.1")
15
15
  .option("--port <port>", "Port to serve on", "7654")
16
- .option("--memory-path <path>", "Directory for memory storage (default: ./.memory)", "./.memory")
17
16
  .option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
18
17
  .option("--opencode-url <url>", "URL of the OpenCode server (default: http://127.0.0.1:4096)", process.env.OPENCODE_URL ?? "http://127.0.0.1:4096")
19
- .option("--simple-memory", "Use lightweight SQLite+BM25 memory instead of the default fastmemory (no embeddings, no model download)")
20
18
  .action(async (options) => {
21
19
  const { serve } = await import("./commands/serve.js");
22
20
  await serve(options);
@@ -34,8 +32,8 @@ const appCmd = program
34
32
  .command("app")
35
33
  .description("[HUMAN ONLY] Interactive visual applications");
36
34
  appCmd
37
- .command("coworker-chat-web")
38
- .description("[HUMAN ONLY] Launch a web chat interface for coworkers")
35
+ .command("chat")
36
+ .description("Web chat interface for human to chat to coworkers")
39
37
  .option("--url <url>", "URL of the agent-office serve endpoint (e.g. http://127.0.0.1:7654)", process.env.AGENT_OFFICE_URL ?? "http://127.0.0.1:7654")
40
38
  .option("--password <password>", "API password for the agent-office server", process.env.AGENT_OFFICE_PASSWORD ?? "secret")
41
39
  .option("--host <host>", "Host to bind the web server to", "127.0.0.1")
@@ -44,9 +42,8 @@ appCmd
44
42
  const { appCoworkerChatWeb } = await import("./commands/communicator.js");
45
43
  await appCoworkerChatWeb(options);
46
44
  });
47
- appCmd
48
- .command("screensaver")
49
- .description("[HUMAN ONLY] Launch a visualization of recent mail activity (live screensaver)")
45
+ appCmd.command("screensaver")
46
+ .description("3D visualization of recent mail activity")
50
47
  .option("--url <url>", "URL of the agent-office serve endpoint (e.g. http://127.0.0.1:7654)", process.env.AGENT_OFFICE_URL ?? "http://127.0.0.1:7654")
51
48
  .option("--password <password>", "API password for the agent-office server", process.env.AGENT_OFFICE_PASSWORD ?? "secret")
52
49
  .option("--host <host>", "Host to bind the screensaver web server to", "127.0.0.1")
@@ -55,6 +52,19 @@ appCmd
55
52
  const { appScreensaver } = await import("./commands/screensaver.js");
56
53
  await appScreensaver(options);
57
54
  });
55
+ appCmd
56
+ .command('notifier')
57
+ .description('Notify human by email when unread messages have been waiting over certain amount of time')
58
+ .option('--agent-office-url <url>', 'Agent Office server URL', process.env.AGENT_OFFICE_URL ?? 'http://127.0.0.1:7654')
59
+ .option('--password <pw>', 'API password', process.env.AGENT_OFFICE_PASSWORD)
60
+ .option('--to-email <email>', 'Recipient email address', process.env.TO_EMAIL)
61
+ .option('--resend-api-key <key>', 'Resend API key', process.env.RESEND_API_KEY)
62
+ .option('--domain <domain>', 'Sender domain (e.g. coworker.innercontext.com)', process.env.EMAIL_DOMAIN)
63
+ .option('--wait-minutes <minutes>', 'Minutes a message must be unread before notifying', '15')
64
+ .action(async (options) => {
65
+ const { notifier } = await import('./commands/notifier.js');
66
+ await notifier(options);
67
+ });
58
68
  const workerCmd = program
59
69
  .command("worker")
60
70
  .description("Worker agent commands");
@@ -178,47 +188,4 @@ cronCmd
178
188
  const { cronHistory } = await import("./commands/worker.js");
179
189
  await cronHistory(token, cronId);
180
190
  });
181
- // ── Worker Memory Commands (nested) ──────────────────────────────────────────
182
- const memoryCmd = workerCmd
183
- .command("memory")
184
- .description("Manage your persistent memories");
185
- memoryCmd
186
- .command("add")
187
- .argument("<token>", "Agent token in the format <agent_code>@<server-url>")
188
- .description("Add a new memory")
189
- .requiredOption("--content <content>", "Memory content to store")
190
- .action(async (token, options) => {
191
- const { memoryAdd } = await import("./commands/worker.js");
192
- await memoryAdd(token, options.content);
193
- });
194
- memoryCmd
195
- .command("search")
196
- .argument("<token>", "Agent token in the format <agent_code>@<server-url>")
197
- .description("Search memories using hybrid search (keyword + semantic)")
198
- .requiredOption("--query <query>", "Search query")
199
- .option("--limit <limit>", "Maximum results (default 10)", "10")
200
- .action(async (token, options) => {
201
- const limit = parseInt(options.limit ?? "10", 10);
202
- const { memorySearch } = await import("./commands/worker.js");
203
- await memorySearch(token, options.query, limit);
204
- });
205
- memoryCmd
206
- .command("list")
207
- .argument("<token>", "Agent token in the format <agent_code>@<server-url>")
208
- .description("List all stored memories")
209
- .option("--limit <limit>", "Maximum memories to list (default 50)", "50")
210
- .action(async (token, options) => {
211
- const limit = parseInt(options.limit ?? "50", 10);
212
- const { memoryList } = await import("./commands/worker.js");
213
- await memoryList(token, limit);
214
- });
215
- memoryCmd
216
- .command("forget")
217
- .argument("<token>", "Agent token in the format <agent_code>@<server-url>")
218
- .argument("<memoryId>", "ID of the memory to forget")
219
- .description("Delete a memory by ID")
220
- .action(async (token, memoryId) => {
221
- const { memoryForget } = await import("./commands/worker.js");
222
- await memoryForget(token, memoryId);
223
- });
224
191
  program.parse();
@@ -0,0 +1,11 @@
1
+ import { type AgentOfficeNotifier } from "../lib/notifier.js";
2
+ type Options = {
3
+ agentOfficeUrl?: string;
4
+ password?: string;
5
+ toEmail?: string;
6
+ resendApiKey?: string;
7
+ domain?: string;
8
+ waitMinutes?: string;
9
+ };
10
+ export declare function notifier(options: Options, customNotifier?: AgentOfficeNotifier): Promise<void>;
11
+ export {};
@@ -0,0 +1,100 @@
1
+ import { Cron } from "croner";
2
+ import { ResendNotifier } from "../lib/notifier.js";
3
+ export async function notifier(options, customNotifier) {
4
+ const toEmail = options.toEmail;
5
+ if (!toEmail) {
6
+ console.error("Error: --to-email or TO_EMAIL env required");
7
+ process.exit(1);
8
+ }
9
+ const domain = options.domain;
10
+ if (!domain) {
11
+ console.error("Error: --domain or EMAIL_DOMAIN env required");
12
+ process.exit(1);
13
+ }
14
+ const agentOfficeUrl = options.agentOfficeUrl ?? "http://127.0.0.1:7654";
15
+ const password = options.password;
16
+ if (!password) {
17
+ console.error("Error: --password or AGENT_OFFICE_PASSWORD env required");
18
+ process.exit(1);
19
+ }
20
+ const waitMinutes = parseInt(options.waitMinutes ?? "15", 10);
21
+ const waitHours = waitMinutes / 60;
22
+ let notify;
23
+ if (customNotifier) {
24
+ notify = customNotifier;
25
+ }
26
+ else {
27
+ const resendApiKey = options.resendApiKey;
28
+ if (!resendApiKey) {
29
+ console.error("Error: --resend-api-key or RESEND_API_KEY env required");
30
+ process.exit(1);
31
+ }
32
+ notify = new ResendNotifier(resendApiKey);
33
+ }
34
+ const authHeaders = {
35
+ Authorization: `Bearer ${password}`,
36
+ "Content-Type": "application/json",
37
+ };
38
+ const check = async () => {
39
+ try {
40
+ // Fetch messages old enough to notify about
41
+ const resp = await fetch(`${agentOfficeUrl}/human/unread-old?hours=${waitHours}`, {
42
+ headers: authHeaders,
43
+ });
44
+ if (!resp.ok) {
45
+ console.error(`GET /human/unread-old failed: ${resp.status}`);
46
+ return;
47
+ }
48
+ const qualifying = (await resp.json());
49
+ if (qualifying.length === 0) {
50
+ // Check if there are any unread messages at all (just not old enough yet)
51
+ const allResp = await fetch(`${agentOfficeUrl}/human/unread-old?hours=0`, {
52
+ headers: authHeaders,
53
+ });
54
+ if (allResp.ok) {
55
+ const allUnread = (await allResp.json());
56
+ if (allUnread.length > 0) {
57
+ console.log(`${allUnread.length} unread message(s) exist but haven't been waiting >${waitMinutes}m yet — skipping notification`);
58
+ }
59
+ else {
60
+ console.log("No unread messages — nothing to notify");
61
+ }
62
+ }
63
+ return;
64
+ }
65
+ const senders = [...new Set(qualifying.map((m) => m.from_name))];
66
+ for (const sender of senders) {
67
+ const fromAddress = `${sender} <${sender.replace(/\s+/g, "+")}@${domain}>`;
68
+ await notify.send({
69
+ from: fromAddress,
70
+ to: toEmail,
71
+ subject: "Agent Office: Message Waiting",
72
+ text: "There is a message waiting for you at Agent Office.",
73
+ });
74
+ console.log(`Sent out notification for waiting mail | from: ${fromAddress} | to: ${toEmail}`);
75
+ }
76
+ const ids = qualifying.map((m) => m.id);
77
+ const markResp = await fetch(`${agentOfficeUrl}/human/mark-notified`, {
78
+ method: "POST",
79
+ headers: authHeaders,
80
+ body: JSON.stringify({ ids }),
81
+ });
82
+ if (!markResp.ok) {
83
+ console.error(`POST /human/mark-notified failed: ${markResp.status}`);
84
+ }
85
+ }
86
+ catch (e) {
87
+ console.error("Notifier cron error:", e);
88
+ }
89
+ };
90
+ const cron = new Cron("0 * * * *", check);
91
+ console.log(`Agent Office notifier started. Notifying for messages unread >${waitMinutes}m. Checking ${agentOfficeUrl} every hour. ^C to stop.`);
92
+ await check();
93
+ const shutdown = async () => {
94
+ console.log("\nShutting down...");
95
+ cron.stop();
96
+ process.exit(0);
97
+ };
98
+ process.on("SIGINT", shutdown);
99
+ process.on("SIGTERM", shutdown);
100
+ }
@@ -3,10 +3,8 @@ interface ServeOptions {
3
3
  sqlite?: string;
4
4
  host: string;
5
5
  port: string;
6
- memoryPath: string;
7
6
  password?: string;
8
7
  opencodeUrl: string;
9
- simpleMemory?: boolean;
10
8
  }
11
9
  export declare function serve(options: ServeOptions): Promise<void>;
12
10
  export {};
@@ -3,7 +3,6 @@ import { runMigrations } from "../db/migrate.js";
3
3
  import { OpenCodeCodingServer } from "../lib/opencode-coding-server.js";
4
4
  import { createApp } from "../server/index.js";
5
5
  import { CronScheduler } from "../server/cron.js";
6
- import { AgentOfficeFastMemory, AgentOfficeSimpleMemory } from "../server/memory.js";
7
6
  export async function serve(options) {
8
7
  const password = options.password;
9
8
  if (!password) {
@@ -44,33 +43,10 @@ export async function serve(options) {
44
43
  const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
45
44
  console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
46
45
  const serverUrl = `http://${options.host}:${port}`;
47
- // Create memory manager
48
- let memoryManager;
49
- if (options.simpleMemory) {
50
- console.log("Using simple SQLite+BM25 memory backend.");
51
- memoryManager = new AgentOfficeSimpleMemory(options.memoryPath);
52
- await memoryManager.warmup();
53
- }
54
- else {
55
- memoryManager = new AgentOfficeFastMemory(options.memoryPath);
56
- console.log("Warming up embedding model...");
57
- try {
58
- await memoryManager.warmup();
59
- console.log("Embedding model ready.");
60
- }
61
- catch (err) {
62
- console.error("Embedding model failed to load:", err);
63
- console.error("Try deleting the model cache and restarting:");
64
- console.error(` rm -rf ${options.memoryPath}/.model-cache`);
65
- memoryManager.closeAll();
66
- await storage.close();
67
- process.exit(1);
68
- }
69
- }
70
46
  // Create cron scheduler
71
47
  const cronScheduler = new CronScheduler();
72
48
  // Create Express app
73
- const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler, memoryManager);
49
+ const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler);
74
50
  // Start cron scheduler
75
51
  await cronScheduler.start(storage, agenticCodingServer, serverUrl);
76
52
  // Start server
@@ -82,7 +58,6 @@ export async function serve(options) {
82
58
  console.log("\nShutting down...");
83
59
  server.close(async () => {
84
60
  cronScheduler.stop();
85
- memoryManager.closeAll();
86
61
  await storage.close();
87
62
  console.log("Goodbye.");
88
63
  process.exit(0);
@@ -13,7 +13,3 @@ export declare function deleteCron(token: string, cronId: number): Promise<void>
13
13
  export declare function enableCron(token: string, cronId: number): Promise<void>;
14
14
  export declare function disableCron(token: string, cronId: number): Promise<void>;
15
15
  export declare function cronHistory(token: string, cronId: number): Promise<void>;
16
- export declare function memoryAdd(token: string, content: string): Promise<void>;
17
- export declare function memorySearch(token: string, query: string, limit: number): Promise<void>;
18
- export declare function memoryList(token: string, limit: number): Promise<void>;
19
- export declare function memoryForget(token: string, memoryId: string): Promise<void>;
@@ -139,66 +139,3 @@ export async function cronHistory(token, cronId) {
139
139
  const history = await fetchWorker(token, `/worker/crons/${cronId}/history`);
140
140
  console.log(JSON.stringify(history, null, 2));
141
141
  }
142
- // ── Memory Commands ──────────────────────────────────────────────────────────
143
- export async function memoryAdd(token, content) {
144
- const result = await postWorker(token, "/worker/memory/add", { content });
145
- console.log(JSON.stringify(result, null, 2));
146
- }
147
- export async function memorySearch(token, query, limit) {
148
- const result = await postWorker(token, "/worker/memory/search", { query, limit });
149
- console.log(JSON.stringify(result, null, 2));
150
- }
151
- export async function memoryList(token, limit) {
152
- const { agentCode, serverUrl } = parseToken(token);
153
- const url = `${serverUrl}/worker/memory/list?code=${encodeURIComponent(agentCode)}&limit=${limit}`;
154
- let res;
155
- try {
156
- res = await fetch(url);
157
- }
158
- catch (err) {
159
- console.error(`Error: could not reach ${serverUrl}`);
160
- console.error(err instanceof Error ? err.message : String(err));
161
- process.exit(1);
162
- }
163
- let body;
164
- try {
165
- body = await res.json();
166
- }
167
- catch {
168
- console.error(`Error: invalid response from server`);
169
- process.exit(1);
170
- }
171
- if (!res.ok) {
172
- const msg = body.error ?? `HTTP ${res.status}`;
173
- console.error(`Error: ${msg}`);
174
- process.exit(1);
175
- }
176
- console.log(JSON.stringify(body, null, 2));
177
- }
178
- export async function memoryForget(token, memoryId) {
179
- const { agentCode, serverUrl } = parseToken(token);
180
- const url = `${serverUrl}/worker/memory/${encodeURIComponent(memoryId)}?code=${encodeURIComponent(agentCode)}`;
181
- let res;
182
- try {
183
- res = await fetch(url, { method: "DELETE" });
184
- }
185
- catch (err) {
186
- console.error(`Error: could not reach ${serverUrl}`);
187
- console.error(err instanceof Error ? err.message : String(err));
188
- process.exit(1);
189
- }
190
- let body;
191
- try {
192
- body = await res.json();
193
- }
194
- catch {
195
- console.error(`Error: invalid response from server`);
196
- process.exit(1);
197
- }
198
- if (!res.ok) {
199
- const msg = body.error ?? `HTTP ${res.status}`;
200
- console.error(`Error: ${msg}`);
201
- process.exit(1);
202
- }
203
- console.log(JSON.stringify(body, null, 2));
204
- }
@@ -24,6 +24,7 @@ export interface MessageRow {
24
24
  body: string;
25
25
  read: boolean;
26
26
  injected: boolean;
27
+ notified?: boolean;
27
28
  created_at: Date;
28
29
  }
29
30
  export interface CronJobRow {
@@ -19,12 +19,17 @@ export declare class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase
19
19
  getAllConfig(): Promise<ConfigRow[]>;
20
20
  getConfig(key: string): Promise<string | null>;
21
21
  setConfig(key: string, value: string): Promise<void>;
22
- listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
22
+ listMessagesForRecipient(name: string, filters?: {
23
+ unread?: boolean;
24
+ olderThanHours?: number;
25
+ notified?: boolean;
26
+ }): Promise<MessageRow[]>;
23
27
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
24
28
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
25
29
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
26
30
  markMessageAsRead(id: number): Promise<MessageRow | null>;
27
31
  markMessageAsInjected(id: number): Promise<void>;
32
+ markMessagesAsNotified(ids: number[]): Promise<void>;
28
33
  listCronJobs(): Promise<CronJobRow[]>;
29
34
  listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
30
35
  getCronJobById(id: number): Promise<CronJobRow | null>;
@@ -107,21 +107,19 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
107
107
  `;
108
108
  }
109
109
  // Messages
110
- async listMessagesForRecipient(name, unreadOnly) {
111
- if (unreadOnly) {
112
- return this.sql `
113
- SELECT id, from_name, to_name, body, read, injected, created_at
114
- FROM messages
115
- WHERE to_name = ${name} AND read = FALSE
116
- ORDER BY created_at DESC
117
- `;
110
+ async listMessagesForRecipient(name, filters) {
111
+ const whereClauses = ["to_name = $1"];
112
+ const params = [name];
113
+ if (filters?.unread)
114
+ whereClauses.push("read = FALSE");
115
+ if (filters?.notified === false)
116
+ whereClauses.push("notified = FALSE");
117
+ if (filters?.olderThanHours !== undefined) {
118
+ whereClauses.push(`created_at < NOW() - INTERVAL '${filters.olderThanHours} hours'`);
118
119
  }
119
- return this.sql `
120
- SELECT id, from_name, to_name, body, read, injected, created_at
121
- FROM messages
122
- WHERE to_name = ${name}
123
- ORDER BY created_at DESC
124
- `;
120
+ const whereSQL = whereClauses.join(" AND ");
121
+ const rows = await this.sql.unsafe(`SELECT id, from_name, to_name, body, read, injected, created_at, notified FROM messages WHERE ${whereSQL} ORDER BY created_at DESC`, params);
122
+ return rows;
125
123
  }
126
124
  async listMessagesFromSender(name) {
127
125
  return this.sql `
@@ -162,6 +160,11 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
162
160
  async markMessageAsInjected(id) {
163
161
  await this.sql `UPDATE messages SET injected = TRUE WHERE id = ${id}`;
164
162
  }
163
+ async markMessagesAsNotified(ids) {
164
+ if (ids.length === 0)
165
+ return;
166
+ await this.sql `UPDATE messages SET notified = TRUE WHERE id = ANY(${ids})`;
167
+ }
165
168
  // Cron Jobs
166
169
  async listCronJobs() {
167
170
  return this.sql `
@@ -343,6 +346,13 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
343
346
  name: "rename_mode_to_agent",
344
347
  sql: `
345
348
  ALTER TABLE sessions RENAME COLUMN mode TO agent;
349
+ `,
350
+ },
351
+ {
352
+ version: 9,
353
+ name: "add_notified_to_messages",
354
+ sql: `
355
+ ALTER TABLE messages ADD COLUMN IF NOT EXISTS notified BOOLEAN NOT NULL DEFAULT FALSE;
346
356
  `,
347
357
  },
348
358
  ];
@@ -20,12 +20,17 @@ export declare class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
20
20
  getAllConfig(): Promise<ConfigRow[]>;
21
21
  getConfig(key: string): Promise<string | null>;
22
22
  setConfig(key: string, value: string): Promise<void>;
23
- listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
23
+ listMessagesForRecipient(name: string, filters?: {
24
+ unread?: boolean;
25
+ olderThanHours?: number;
26
+ notified?: boolean;
27
+ }): Promise<MessageRow[]>;
24
28
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
25
29
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
26
30
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
27
31
  markMessageAsRead(id: number): Promise<MessageRow | null>;
28
32
  markMessageAsInjected(id: number): Promise<void>;
33
+ markMessagesAsNotified(ids: number[]): Promise<void>;
29
34
  listCronJobs(): Promise<CronJobRow[]>;
30
35
  listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
31
36
  getCronJobById(id: number): Promise<CronJobRow | null>;