memorix 1.2.2 → 1.2.3

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.
Files changed (139) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +34 -18
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/index.js +17 -8
  6. package/dist/index.js.map +1 -1
  7. package/dist/maintenance-runner.js.map +1 -1
  8. package/dist/memcode-runtime/CHANGELOG.md +7 -0
  9. package/dist/sdk.js +17 -8
  10. package/dist/sdk.js.map +1 -1
  11. package/docs/DESIGN_DECISIONS.md +357 -357
  12. package/docs/dev-log/progress.txt +18 -8
  13. package/package.json +1 -1
  14. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  15. package/src/audit/index.ts +156 -156
  16. package/src/cli/commands/audit-list.ts +89 -89
  17. package/src/cli/commands/background.ts +659 -659
  18. package/src/cli/commands/formation.ts +48 -48
  19. package/src/cli/commands/git-hook-install.ts +111 -111
  20. package/src/cli/commands/handoff.ts +54 -54
  21. package/src/cli/commands/hooks-status.ts +63 -63
  22. package/src/cli/commands/ingest-commit.ts +153 -153
  23. package/src/cli/commands/ingest-image.ts +66 -66
  24. package/src/cli/commands/ingest-log.ts +180 -180
  25. package/src/cli/commands/ingest.ts +44 -44
  26. package/src/cli/commands/integrate-shared.ts +15 -15
  27. package/src/cli/commands/lock.ts +82 -82
  28. package/src/cli/commands/message.ts +104 -104
  29. package/src/cli/commands/poll.ts +58 -58
  30. package/src/cli/commands/purge-all-memory.ts +85 -85
  31. package/src/cli/commands/purge-project-memory.ts +83 -83
  32. package/src/cli/commands/reasoning.ts +118 -118
  33. package/src/cli/commands/serve-shared.ts +118 -118
  34. package/src/cli/commands/session.ts +15 -7
  35. package/src/cli/commands/skills.ts +114 -114
  36. package/src/cli/commands/task.ts +167 -167
  37. package/src/cli/commands/transfer.ts +47 -47
  38. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  39. package/src/cli/tui/ChatView.tsx +234 -234
  40. package/src/cli/tui/CommandBar.tsx +312 -312
  41. package/src/cli/tui/ContextRail.tsx +118 -118
  42. package/src/cli/tui/HeaderBar.tsx +72 -72
  43. package/src/cli/tui/LogoBanner.tsx +51 -51
  44. package/src/cli/tui/Sidebar.tsx +179 -179
  45. package/src/cli/tui/index.ts +41 -41
  46. package/src/cli/tui/markdown-render.tsx +371 -371
  47. package/src/cli/tui/session-service.ts +3 -2
  48. package/src/cli/tui/use-mouse.ts +157 -157
  49. package/src/cli/tui/useNavigation.ts +56 -56
  50. package/src/cli/update-checker.ts +211 -211
  51. package/src/cli/version.ts +7 -7
  52. package/src/cli/workbench.ts +1 -1
  53. package/src/compact/token-budget.ts +74 -74
  54. package/src/dashboard/project-classification.ts +64 -64
  55. package/src/embedding/fastembed-provider.ts +142 -142
  56. package/src/embedding/transformers-provider.ts +111 -111
  57. package/src/git/extractor.ts +209 -209
  58. package/src/git/hooks-path.ts +85 -85
  59. package/src/hooks/pattern-detector.ts +173 -173
  60. package/src/hooks/significance-filter.ts +250 -250
  61. package/src/llm/memory-manager.ts +328 -328
  62. package/src/llm/provider.ts +885 -885
  63. package/src/llm/quality.ts +248 -248
  64. package/src/memory/attribution-guard.ts +249 -249
  65. package/src/memory/disclosure-policy.ts +135 -135
  66. package/src/memory/entity-extractor.ts +197 -197
  67. package/src/memory/formation/evaluate.ts +217 -217
  68. package/src/memory/formation/extract.ts +361 -361
  69. package/src/memory/formation/index.ts +417 -417
  70. package/src/memory/formation/resolve.ts +344 -344
  71. package/src/memory/formation/types.ts +315 -315
  72. package/src/memory/freshness.ts +122 -122
  73. package/src/memory/graph.ts +197 -197
  74. package/src/memory/refs.ts +94 -94
  75. package/src/memory/secret-filter.ts +79 -79
  76. package/src/memory/session.ts +24 -9
  77. package/src/multimodal/image-loader.ts +143 -143
  78. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  79. package/src/orchestrate/adapters/claude.ts +111 -111
  80. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  81. package/src/orchestrate/adapters/codex.ts +41 -41
  82. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  83. package/src/orchestrate/adapters/gemini.ts +42 -42
  84. package/src/orchestrate/adapters/index.ts +73 -73
  85. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  86. package/src/orchestrate/adapters/opencode.ts +47 -47
  87. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  88. package/src/orchestrate/adapters/types.ts +77 -77
  89. package/src/orchestrate/capability-router.ts +284 -284
  90. package/src/orchestrate/context-compact.ts +188 -188
  91. package/src/orchestrate/cost-tracker.ts +219 -219
  92. package/src/orchestrate/error-recovery.ts +191 -191
  93. package/src/orchestrate/evidence.ts +140 -140
  94. package/src/orchestrate/ledger.ts +110 -110
  95. package/src/orchestrate/memorix-bridge.ts +343 -343
  96. package/src/orchestrate/output-budget.ts +80 -80
  97. package/src/orchestrate/permission.ts +152 -152
  98. package/src/orchestrate/pipeline-trace.ts +131 -131
  99. package/src/orchestrate/prompt-builder.ts +155 -155
  100. package/src/orchestrate/ring-buffer.ts +37 -37
  101. package/src/orchestrate/task-graph.ts +389 -389
  102. package/src/orchestrate/worktree.ts +232 -232
  103. package/src/project/aliases.ts +374 -374
  104. package/src/project/detector.ts +268 -268
  105. package/src/rules/adapters/claude-code.ts +99 -99
  106. package/src/rules/adapters/codex.ts +97 -97
  107. package/src/rules/adapters/copilot.ts +124 -124
  108. package/src/rules/adapters/cursor.ts +114 -114
  109. package/src/rules/adapters/kiro.ts +126 -126
  110. package/src/rules/adapters/trae.ts +56 -56
  111. package/src/rules/adapters/windsurf.ts +83 -83
  112. package/src/rules/syncer.ts +235 -235
  113. package/src/sdk.ts +299 -299
  114. package/src/search/intent-detector.ts +289 -289
  115. package/src/search/query-expansion.ts +52 -52
  116. package/src/server/formation-timeout.ts +27 -27
  117. package/src/server.ts +7 -2
  118. package/src/skills/mini-skills.ts +386 -386
  119. package/src/store/chat-store.ts +119 -119
  120. package/src/store/graph-store.ts +249 -249
  121. package/src/store/mini-skill-store.ts +349 -349
  122. package/src/store/persistence-json.ts +212 -212
  123. package/src/store/persistence.ts +291 -291
  124. package/src/store/project-affinity.ts +195 -195
  125. package/src/team/event-bus.ts +76 -76
  126. package/src/team/file-locks.ts +173 -173
  127. package/src/team/handoff.ts +161 -161
  128. package/src/team/messages.ts +203 -203
  129. package/src/team/poll.ts +132 -132
  130. package/src/team/tasks.ts +211 -211
  131. package/src/workspace/mcp-adapters/codex.ts +191 -191
  132. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  133. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  134. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  135. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  136. package/src/workspace/mcp-adapters/trae.ts +134 -134
  137. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  138. package/src/workspace/sanitizer.ts +60 -60
  139. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,203 +1,203 @@
1
- /**
2
- * Message Bus — Agent-to-agent communication
3
- *
4
- * In-memory message queue for team coordination.
5
- * Messages are temporary (not persisted as observations).
6
- * Supports direct send, broadcast, inbox retrieval, and read tracking.
7
- */
8
-
9
- import { randomUUID } from 'node:crypto';
10
- import type { AgentRegistry } from './registry.js';
11
-
12
- // ─── Types ───────────────────────────────────────────────────────────
13
-
14
- export type MessageType = 'request' | 'response' | 'info' | 'announcement' | 'contract' | 'error';
15
-
16
- export interface MessageSendInput {
17
- from: string;
18
- to: string;
19
- type: MessageType;
20
- content: string;
21
- }
22
-
23
- export interface MessageBroadcastInput {
24
- from: string;
25
- type: MessageType;
26
- content: string;
27
- }
28
-
29
- export interface Message {
30
- id: string;
31
- from: string;
32
- to: string;
33
- type: MessageType;
34
- content: string;
35
- timestamp: Date;
36
- read: boolean;
37
- }
38
-
39
- // ─── Message Bus ─────────────────────────────────────────────────────
40
-
41
- /** Max messages per inbox. Oldest read messages are evicted first. */
42
- const MAX_INBOX_SIZE = 200;
43
- /** Max message content length in bytes */
44
- const MAX_CONTENT_LENGTH = 10_000;
45
-
46
- export class MessageBus {
47
- private inboxes = new Map<string, Message[]>();
48
- private registry: AgentRegistry;
49
-
50
- constructor(registry: AgentRegistry) {
51
- this.registry = registry;
52
- }
53
-
54
- /**
55
- * Send a message to a specific agent.
56
- * Throws if receiver is unknown.
57
- */
58
- send(input: MessageSendInput): Message {
59
- const receiver = this.registry.getAgent(input.to);
60
- if (!receiver) {
61
- throw new Error(`Unknown receiver agent: ${input.to}`);
62
- }
63
- if (receiver.status !== 'active') {
64
- throw new Error(`Receiver agent is inactive: ${receiver.name}`);
65
- }
66
-
67
- const msg: Message = {
68
- id: randomUUID(),
69
- from: input.from,
70
- to: input.to,
71
- type: input.type,
72
- content: input.content,
73
- timestamp: new Date(),
74
- read: false,
75
- };
76
-
77
- const inbox = this.inboxes.get(input.to) ?? [];
78
- inbox.push(msg);
79
-
80
- // Evict oldest read messages if inbox exceeds limit
81
- if (inbox.length > MAX_INBOX_SIZE) {
82
- const readIndices: number[] = [];
83
- for (let i = 0; i < inbox.length; i++) {
84
- if (inbox[i].read) readIndices.push(i);
85
- }
86
- // Remove oldest read messages first
87
- const toRemove = inbox.length - MAX_INBOX_SIZE;
88
- for (let i = Math.min(toRemove, readIndices.length) - 1; i >= 0; i--) {
89
- inbox.splice(readIndices[i], 1);
90
- }
91
- // If still over limit, remove oldest unread
92
- while (inbox.length > MAX_INBOX_SIZE) {
93
- inbox.shift();
94
- }
95
- }
96
-
97
- this.inboxes.set(input.to, inbox);
98
- return msg;
99
- }
100
-
101
- /**
102
- * Broadcast a message to all active agents except the sender.
103
- */
104
- broadcast(input: MessageBroadcastInput): Message[] {
105
- const activeAgents = this.registry.listAgents({ status: 'active' });
106
- const messages: Message[] = [];
107
-
108
- for (const agent of activeAgents) {
109
- if (agent.id === input.from) continue;
110
-
111
- const msg = this.send({
112
- from: input.from,
113
- to: agent.id,
114
- type: input.type,
115
- content: input.content,
116
- });
117
- messages.push(msg);
118
- }
119
-
120
- return messages;
121
- }
122
-
123
- /**
124
- * Get all messages in an agent's inbox (both read and unread).
125
- */
126
- getInbox(agentId: string): Message[] {
127
- return [...(this.inboxes.get(agentId) ?? [])];
128
- }
129
-
130
- /**
131
- * Mark specific messages as read.
132
- */
133
- markRead(agentId: string, messageIds: string[]): number {
134
- const inbox = this.inboxes.get(agentId);
135
- if (!inbox) return 0;
136
-
137
- const idSet = new Set(messageIds);
138
- let count = 0;
139
- for (const msg of inbox) {
140
- if (idSet.has(msg.id) && !msg.read) {
141
- msg.read = true;
142
- count++;
143
- }
144
- }
145
- return count;
146
- }
147
-
148
- /**
149
- * Count unread messages for an agent.
150
- */
151
- getUnreadCount(agentId: string): number {
152
- const inbox = this.inboxes.get(agentId);
153
- if (!inbox) return 0;
154
- return inbox.filter(m => !m.read).length;
155
- }
156
-
157
- /**
158
- * Remove all read messages from an agent's inbox.
159
- * Returns the number of messages pruned.
160
- */
161
- pruneRead(agentId: string): number {
162
- const inbox = this.inboxes.get(agentId);
163
- if (!inbox) return 0;
164
- const before = inbox.length;
165
- const unread = inbox.filter(m => !m.read);
166
- this.inboxes.set(agentId, unread);
167
- return before - unread.length;
168
- }
169
-
170
- /**
171
- * Clear all messages for an agent (used on agent leave).
172
- */
173
- clearInbox(agentId: string): void {
174
- this.inboxes.delete(agentId);
175
- }
176
-
177
- /** Max allowed content length */
178
- static get MAX_CONTENT_LENGTH() { return MAX_CONTENT_LENGTH; }
179
-
180
- /** Serialize state for file persistence */
181
- serialize(): { inboxes: Record<string, unknown[]> } {
182
- const inboxes: Record<string, unknown[]> = {};
183
- for (const [agentId, msgs] of this.inboxes) {
184
- inboxes[agentId] = msgs.map(m => ({
185
- ...m,
186
- timestamp: m.timestamp.toISOString(),
187
- }));
188
- }
189
- return { inboxes };
190
- }
191
-
192
- /** Hydrate state from file persistence */
193
- hydrate(data: { inboxes?: Record<string, any[]> }): void {
194
- this.inboxes.clear();
195
- if (!data?.inboxes) return;
196
- for (const [agentId, msgs] of Object.entries(data.inboxes)) {
197
- this.inboxes.set(agentId, msgs.map(m => ({
198
- ...m,
199
- timestamp: new Date(m.timestamp),
200
- })));
201
- }
202
- }
203
- }
1
+ /**
2
+ * Message Bus — Agent-to-agent communication
3
+ *
4
+ * In-memory message queue for team coordination.
5
+ * Messages are temporary (not persisted as observations).
6
+ * Supports direct send, broadcast, inbox retrieval, and read tracking.
7
+ */
8
+
9
+ import { randomUUID } from 'node:crypto';
10
+ import type { AgentRegistry } from './registry.js';
11
+
12
+ // ─── Types ───────────────────────────────────────────────────────────
13
+
14
+ export type MessageType = 'request' | 'response' | 'info' | 'announcement' | 'contract' | 'error';
15
+
16
+ export interface MessageSendInput {
17
+ from: string;
18
+ to: string;
19
+ type: MessageType;
20
+ content: string;
21
+ }
22
+
23
+ export interface MessageBroadcastInput {
24
+ from: string;
25
+ type: MessageType;
26
+ content: string;
27
+ }
28
+
29
+ export interface Message {
30
+ id: string;
31
+ from: string;
32
+ to: string;
33
+ type: MessageType;
34
+ content: string;
35
+ timestamp: Date;
36
+ read: boolean;
37
+ }
38
+
39
+ // ─── Message Bus ─────────────────────────────────────────────────────
40
+
41
+ /** Max messages per inbox. Oldest read messages are evicted first. */
42
+ const MAX_INBOX_SIZE = 200;
43
+ /** Max message content length in bytes */
44
+ const MAX_CONTENT_LENGTH = 10_000;
45
+
46
+ export class MessageBus {
47
+ private inboxes = new Map<string, Message[]>();
48
+ private registry: AgentRegistry;
49
+
50
+ constructor(registry: AgentRegistry) {
51
+ this.registry = registry;
52
+ }
53
+
54
+ /**
55
+ * Send a message to a specific agent.
56
+ * Throws if receiver is unknown.
57
+ */
58
+ send(input: MessageSendInput): Message {
59
+ const receiver = this.registry.getAgent(input.to);
60
+ if (!receiver) {
61
+ throw new Error(`Unknown receiver agent: ${input.to}`);
62
+ }
63
+ if (receiver.status !== 'active') {
64
+ throw new Error(`Receiver agent is inactive: ${receiver.name}`);
65
+ }
66
+
67
+ const msg: Message = {
68
+ id: randomUUID(),
69
+ from: input.from,
70
+ to: input.to,
71
+ type: input.type,
72
+ content: input.content,
73
+ timestamp: new Date(),
74
+ read: false,
75
+ };
76
+
77
+ const inbox = this.inboxes.get(input.to) ?? [];
78
+ inbox.push(msg);
79
+
80
+ // Evict oldest read messages if inbox exceeds limit
81
+ if (inbox.length > MAX_INBOX_SIZE) {
82
+ const readIndices: number[] = [];
83
+ for (let i = 0; i < inbox.length; i++) {
84
+ if (inbox[i].read) readIndices.push(i);
85
+ }
86
+ // Remove oldest read messages first
87
+ const toRemove = inbox.length - MAX_INBOX_SIZE;
88
+ for (let i = Math.min(toRemove, readIndices.length) - 1; i >= 0; i--) {
89
+ inbox.splice(readIndices[i], 1);
90
+ }
91
+ // If still over limit, remove oldest unread
92
+ while (inbox.length > MAX_INBOX_SIZE) {
93
+ inbox.shift();
94
+ }
95
+ }
96
+
97
+ this.inboxes.set(input.to, inbox);
98
+ return msg;
99
+ }
100
+
101
+ /**
102
+ * Broadcast a message to all active agents except the sender.
103
+ */
104
+ broadcast(input: MessageBroadcastInput): Message[] {
105
+ const activeAgents = this.registry.listAgents({ status: 'active' });
106
+ const messages: Message[] = [];
107
+
108
+ for (const agent of activeAgents) {
109
+ if (agent.id === input.from) continue;
110
+
111
+ const msg = this.send({
112
+ from: input.from,
113
+ to: agent.id,
114
+ type: input.type,
115
+ content: input.content,
116
+ });
117
+ messages.push(msg);
118
+ }
119
+
120
+ return messages;
121
+ }
122
+
123
+ /**
124
+ * Get all messages in an agent's inbox (both read and unread).
125
+ */
126
+ getInbox(agentId: string): Message[] {
127
+ return [...(this.inboxes.get(agentId) ?? [])];
128
+ }
129
+
130
+ /**
131
+ * Mark specific messages as read.
132
+ */
133
+ markRead(agentId: string, messageIds: string[]): number {
134
+ const inbox = this.inboxes.get(agentId);
135
+ if (!inbox) return 0;
136
+
137
+ const idSet = new Set(messageIds);
138
+ let count = 0;
139
+ for (const msg of inbox) {
140
+ if (idSet.has(msg.id) && !msg.read) {
141
+ msg.read = true;
142
+ count++;
143
+ }
144
+ }
145
+ return count;
146
+ }
147
+
148
+ /**
149
+ * Count unread messages for an agent.
150
+ */
151
+ getUnreadCount(agentId: string): number {
152
+ const inbox = this.inboxes.get(agentId);
153
+ if (!inbox) return 0;
154
+ return inbox.filter(m => !m.read).length;
155
+ }
156
+
157
+ /**
158
+ * Remove all read messages from an agent's inbox.
159
+ * Returns the number of messages pruned.
160
+ */
161
+ pruneRead(agentId: string): number {
162
+ const inbox = this.inboxes.get(agentId);
163
+ if (!inbox) return 0;
164
+ const before = inbox.length;
165
+ const unread = inbox.filter(m => !m.read);
166
+ this.inboxes.set(agentId, unread);
167
+ return before - unread.length;
168
+ }
169
+
170
+ /**
171
+ * Clear all messages for an agent (used on agent leave).
172
+ */
173
+ clearInbox(agentId: string): void {
174
+ this.inboxes.delete(agentId);
175
+ }
176
+
177
+ /** Max allowed content length */
178
+ static get MAX_CONTENT_LENGTH() { return MAX_CONTENT_LENGTH; }
179
+
180
+ /** Serialize state for file persistence */
181
+ serialize(): { inboxes: Record<string, unknown[]> } {
182
+ const inboxes: Record<string, unknown[]> = {};
183
+ for (const [agentId, msgs] of this.inboxes) {
184
+ inboxes[agentId] = msgs.map(m => ({
185
+ ...m,
186
+ timestamp: m.timestamp.toISOString(),
187
+ }));
188
+ }
189
+ return { inboxes };
190
+ }
191
+
192
+ /** Hydrate state from file persistence */
193
+ hydrate(data: { inboxes?: Record<string, any[]> }): void {
194
+ this.inboxes.clear();
195
+ if (!data?.inboxes) return;
196
+ for (const [agentId, msgs] of Object.entries(data.inboxes)) {
197
+ this.inboxes.set(agentId, msgs.map(m => ({
198
+ ...m,
199
+ timestamp: new Date(m.timestamp),
200
+ })));
201
+ }
202
+ }
203
+ }
package/src/team/poll.ts CHANGED
@@ -1,132 +1,132 @@
1
- /**
2
- * Team Poll — Situational awareness for agents.
3
- *
4
- * Phase 4b: Provides `computeWatermark` (extracted from server.ts session_start)
5
- * and `computePoll` (full situational awareness in one call).
6
- *
7
- * Architectural boundary (B3): These are cross-process consistency mechanisms.
8
- * They read from SQLite (canonical truth), never from EventBus.
9
- */
10
-
11
- import type { TeamStore, TeamAgentRow, TeamTaskRow, TeamMessageRow } from './team-store.js';
12
-
13
- // ── Types ──────────────────────────────────────────────────────────
14
-
15
- export interface WatermarkResult {
16
- lastSeenGeneration: number;
17
- currentGeneration: number;
18
- newObservationCount: number;
19
- }
20
-
21
- export interface PollResult {
22
- agent: {
23
- agentId: string;
24
- status: string;
25
- lastHeartbeat: number;
26
- } | null;
27
- watermark: WatermarkResult;
28
- inbox: {
29
- unreadCount: number;
30
- messages: TeamMessageRow[];
31
- };
32
- tasks: {
33
- myInProgress: TeamTaskRow[];
34
- availableToClaim: TeamTaskRow[];
35
- recentlyCompleted: TeamTaskRow[];
36
- recentlyFailed: TeamTaskRow[];
37
- };
38
- team: {
39
- activeAgents: TeamAgentRow[];
40
- totalAgents: number;
41
- };
42
- }
43
-
44
- // ── computeWatermark ───────────────────────────────────────────────
45
-
46
- /**
47
- * Compute the watermark for an agent: how many new observations
48
- * have appeared in this project since the agent's last session.
49
- *
50
- * Extracted from server.ts session_start to be shared by poll and session_start.
51
- *
52
- * @param lastSeenGeneration - Agent's last_seen_obs_generation from TeamStore
53
- * @param currentGeneration - Current global generation from observation store
54
- * @param projectObservations - Pre-filtered observations for this project
55
- * with writeGeneration > lastSeenGeneration
56
- */
57
- export function computeWatermark(
58
- lastSeenGeneration: number,
59
- currentGeneration: number,
60
- newObservationCount: number,
61
- ): WatermarkResult {
62
- return {
63
- lastSeenGeneration,
64
- currentGeneration,
65
- newObservationCount,
66
- };
67
- }
68
-
69
- // ── computePoll ────────────────────────────────────────────────────
70
-
71
- /**
72
- * Compute full situational awareness for an agent in one call.
73
- * Reads only from SQLite (TeamStore) — never from EventBus.
74
- *
75
- * @param teamStore - The TeamStore instance
76
- * @param projectId - Current project ID
77
- * @param agentId - The requesting agent's ID (null if no agent registered)
78
- * @param watermark - Pre-computed watermark result
79
- */
80
- export function computePoll(
81
- teamStore: TeamStore,
82
- projectId: string,
83
- agentId: string | null,
84
- watermark: WatermarkResult,
85
- ): PollResult {
86
- // Agent info
87
- let agent: PollResult['agent'] = null;
88
- if (agentId) {
89
- const row = teamStore.getAgent(agentId);
90
- if (row) {
91
- agent = {
92
- agentId: row.agent_id,
93
- status: row.status,
94
- lastHeartbeat: row.last_heartbeat,
95
- };
96
- }
97
- }
98
-
99
- // Inbox
100
- const unreadCount = agentId ? teamStore.getUnreadCount(projectId, agentId) : 0;
101
- const messages = agentId ? teamStore.getInbox(projectId, agentId) : [];
102
-
103
- // Tasks
104
- const allTasks = teamStore.listTasks(projectId);
105
- const myInProgress = agentId
106
- ? allTasks.filter(t => t.assignee_agent_id === agentId && t.status === 'in_progress')
107
- : [];
108
-
109
- // Available to claim: pending, no assignee, all deps met
110
- const pendingTasks = allTasks.filter(t => t.status === 'pending' && !t.assignee_agent_id);
111
- const availableToClaim = pendingTasks.filter(t => {
112
- const unmet = teamStore.getTaskDeps(t.task_id)
113
- .map(depId => teamStore.getTask(depId))
114
- .filter(dep => dep && dep.status !== 'completed');
115
- return unmet.length === 0;
116
- });
117
-
118
- const recentlyCompleted = allTasks.filter(t => t.status === 'completed');
119
- const recentlyFailed = allTasks.filter(t => t.status === 'failed');
120
-
121
- // Team
122
- const allAgents = teamStore.listAgents(projectId);
123
- const activeAgents = allAgents.filter(a => a.status === 'active');
124
-
125
- return {
126
- agent,
127
- watermark,
128
- inbox: { unreadCount, messages },
129
- tasks: { myInProgress, availableToClaim, recentlyCompleted, recentlyFailed },
130
- team: { activeAgents, totalAgents: allAgents.length },
131
- };
132
- }
1
+ /**
2
+ * Team Poll — Situational awareness for agents.
3
+ *
4
+ * Phase 4b: Provides `computeWatermark` (extracted from server.ts session_start)
5
+ * and `computePoll` (full situational awareness in one call).
6
+ *
7
+ * Architectural boundary (B3): These are cross-process consistency mechanisms.
8
+ * They read from SQLite (canonical truth), never from EventBus.
9
+ */
10
+
11
+ import type { TeamStore, TeamAgentRow, TeamTaskRow, TeamMessageRow } from './team-store.js';
12
+
13
+ // ── Types ──────────────────────────────────────────────────────────
14
+
15
+ export interface WatermarkResult {
16
+ lastSeenGeneration: number;
17
+ currentGeneration: number;
18
+ newObservationCount: number;
19
+ }
20
+
21
+ export interface PollResult {
22
+ agent: {
23
+ agentId: string;
24
+ status: string;
25
+ lastHeartbeat: number;
26
+ } | null;
27
+ watermark: WatermarkResult;
28
+ inbox: {
29
+ unreadCount: number;
30
+ messages: TeamMessageRow[];
31
+ };
32
+ tasks: {
33
+ myInProgress: TeamTaskRow[];
34
+ availableToClaim: TeamTaskRow[];
35
+ recentlyCompleted: TeamTaskRow[];
36
+ recentlyFailed: TeamTaskRow[];
37
+ };
38
+ team: {
39
+ activeAgents: TeamAgentRow[];
40
+ totalAgents: number;
41
+ };
42
+ }
43
+
44
+ // ── computeWatermark ───────────────────────────────────────────────
45
+
46
+ /**
47
+ * Compute the watermark for an agent: how many new observations
48
+ * have appeared in this project since the agent's last session.
49
+ *
50
+ * Extracted from server.ts session_start to be shared by poll and session_start.
51
+ *
52
+ * @param lastSeenGeneration - Agent's last_seen_obs_generation from TeamStore
53
+ * @param currentGeneration - Current global generation from observation store
54
+ * @param projectObservations - Pre-filtered observations for this project
55
+ * with writeGeneration > lastSeenGeneration
56
+ */
57
+ export function computeWatermark(
58
+ lastSeenGeneration: number,
59
+ currentGeneration: number,
60
+ newObservationCount: number,
61
+ ): WatermarkResult {
62
+ return {
63
+ lastSeenGeneration,
64
+ currentGeneration,
65
+ newObservationCount,
66
+ };
67
+ }
68
+
69
+ // ── computePoll ────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Compute full situational awareness for an agent in one call.
73
+ * Reads only from SQLite (TeamStore) — never from EventBus.
74
+ *
75
+ * @param teamStore - The TeamStore instance
76
+ * @param projectId - Current project ID
77
+ * @param agentId - The requesting agent's ID (null if no agent registered)
78
+ * @param watermark - Pre-computed watermark result
79
+ */
80
+ export function computePoll(
81
+ teamStore: TeamStore,
82
+ projectId: string,
83
+ agentId: string | null,
84
+ watermark: WatermarkResult,
85
+ ): PollResult {
86
+ // Agent info
87
+ let agent: PollResult['agent'] = null;
88
+ if (agentId) {
89
+ const row = teamStore.getAgent(agentId);
90
+ if (row) {
91
+ agent = {
92
+ agentId: row.agent_id,
93
+ status: row.status,
94
+ lastHeartbeat: row.last_heartbeat,
95
+ };
96
+ }
97
+ }
98
+
99
+ // Inbox
100
+ const unreadCount = agentId ? teamStore.getUnreadCount(projectId, agentId) : 0;
101
+ const messages = agentId ? teamStore.getInbox(projectId, agentId) : [];
102
+
103
+ // Tasks
104
+ const allTasks = teamStore.listTasks(projectId);
105
+ const myInProgress = agentId
106
+ ? allTasks.filter(t => t.assignee_agent_id === agentId && t.status === 'in_progress')
107
+ : [];
108
+
109
+ // Available to claim: pending, no assignee, all deps met
110
+ const pendingTasks = allTasks.filter(t => t.status === 'pending' && !t.assignee_agent_id);
111
+ const availableToClaim = pendingTasks.filter(t => {
112
+ const unmet = teamStore.getTaskDeps(t.task_id)
113
+ .map(depId => teamStore.getTask(depId))
114
+ .filter(dep => dep && dep.status !== 'completed');
115
+ return unmet.length === 0;
116
+ });
117
+
118
+ const recentlyCompleted = allTasks.filter(t => t.status === 'completed');
119
+ const recentlyFailed = allTasks.filter(t => t.status === 'failed');
120
+
121
+ // Team
122
+ const allAgents = teamStore.listAgents(projectId);
123
+ const activeAgents = allAgents.filter(a => a.status === 'active');
124
+
125
+ return {
126
+ agent,
127
+ watermark,
128
+ inbox: { unreadCount, messages },
129
+ tasks: { myInProgress, availableToClaim, recentlyCompleted, recentlyFailed },
130
+ team: { activeAgents, totalAgents: allAgents.length },
131
+ };
132
+ }