agent-office 0.4.0 → 0.4.1

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.
@@ -26,6 +26,7 @@ export declare class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase
26
26
  }): Promise<MessageRow[]>;
27
27
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
28
28
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
29
+ lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
29
30
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
30
31
  markMessageAsRead(id: number): Promise<MessageRow | null>;
31
32
  markMessageAsInjected(id: number): Promise<void>;
@@ -142,6 +142,21 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
142
142
  }
143
143
  return result;
144
144
  }
145
+ async lastMessageAtByCoworker(humanName) {
146
+ const rows = await this.sql `
147
+ SELECT
148
+ CASE WHEN from_name = ${humanName} THEN to_name ELSE from_name END AS coworker,
149
+ MAX(created_at) AS last_at
150
+ FROM messages
151
+ WHERE from_name = ${humanName} OR to_name = ${humanName}
152
+ GROUP BY coworker
153
+ `;
154
+ const result = new Map();
155
+ for (const row of rows) {
156
+ result.set(row.coworker, row.last_at);
157
+ }
158
+ return result;
159
+ }
145
160
  async createMessageImpl(from, to, body) {
146
161
  const [row] = await this.sql `
147
162
  INSERT INTO messages (from_name, to_name, body)
@@ -27,6 +27,7 @@ export declare class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
27
27
  }): Promise<MessageRow[]>;
28
28
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
29
29
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
30
+ lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
30
31
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
31
32
  markMessageAsRead(id: number): Promise<MessageRow | null>;
32
33
  markMessageAsInjected(id: number): Promise<void>;
@@ -197,6 +197,22 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
197
197
  }
198
198
  return result;
199
199
  }
200
+ async lastMessageAtByCoworker(humanName) {
201
+ const stmt = this.db.prepare(`
202
+ SELECT
203
+ CASE WHEN from_name = ? THEN to_name ELSE from_name END AS coworker,
204
+ MAX(created_at) AS last_at
205
+ FROM messages
206
+ WHERE from_name = ? OR to_name = ?
207
+ GROUP BY coworker
208
+ `);
209
+ const rows = stmt.all(humanName, humanName, humanName);
210
+ const result = new Map();
211
+ for (const row of rows) {
212
+ result.set(row.coworker, new Date(row.last_at + 'Z'));
213
+ }
214
+ return result;
215
+ }
200
216
  async createMessageImpl(from, to, body) {
201
217
  const stmt = this.db.prepare(`
202
218
  INSERT INTO messages (from_name, to_name, body)
@@ -28,6 +28,7 @@ export declare abstract class AgentOfficeStorageBase implements AgentOfficeStora
28
28
  }): Promise<MessageRow[]>;
29
29
  abstract listMessagesFromSender(name: string): Promise<MessageRow[]>;
30
30
  abstract countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
31
+ abstract lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
31
32
  abstract markMessageAsRead(id: number): Promise<MessageRow | null>;
32
33
  abstract markMessageAsInjected(id: number): Promise<void>;
33
34
  abstract markMessagesAsNotified(ids: number[]): Promise<void>;
@@ -37,6 +37,7 @@ export interface AgentOfficeStorage {
37
37
  }): Promise<MessageRow[]>;
38
38
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
39
39
  countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
40
+ lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
40
41
  createMessage(from: string, to: string, body: string): Promise<MessageRow>;
41
42
  markMessageAsRead(id: number): Promise<MessageRow | null>;
42
43
  markMessageAsInjected(id: number): Promise<void>;
@@ -190,17 +190,34 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler)
190
190
  try {
191
191
  const sessions = await storage.listSessions();
192
192
  const humanName = await storage.getConfig('human_name') ?? "Human";
193
- const unreadCounts = await storage.countUnreadBySender(humanName);
193
+ const [unreadCounts, lastMessageAt] = await Promise.all([
194
+ storage.countUnreadBySender(humanName),
195
+ storage.lastMessageAtByCoworker(humanName),
196
+ ]);
194
197
  const coworkers = [
195
- { name: humanName, status: null, isHuman: true, unreadMessages: 0 },
198
+ { name: humanName, status: null, isHuman: true, unreadMessages: 0, lastMessageAt: null },
196
199
  ...sessions.map(s => ({
197
200
  name: s.name,
198
201
  status: s.status,
199
202
  isHuman: false,
200
- unreadMessages: unreadCounts.get(s.name) ?? 0
203
+ unreadMessages: unreadCounts.get(s.name) ?? 0,
204
+ lastMessageAt: lastMessageAt.get(s.name)?.toISOString() ?? null,
201
205
  }))
202
206
  ];
203
- res.json(coworkers);
207
+ // Sort non-human coworkers by most recent message (most recent first),
208
+ // coworkers with no messages appear last (sorted by name as tiebreaker)
209
+ const [human, ...agents] = coworkers;
210
+ agents.sort((a, b) => {
211
+ if (a.lastMessageAt && b.lastMessageAt) {
212
+ return new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime();
213
+ }
214
+ if (a.lastMessageAt)
215
+ return -1;
216
+ if (b.lastMessageAt)
217
+ return 1;
218
+ return a.name.localeCompare(b.name);
219
+ });
220
+ res.json([human, ...agents]);
204
221
  }
205
222
  catch (err) {
206
223
  console.error("GET /coworkers error:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "An office for your AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",