memorix 1.6.1 → 1.6.2

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 (113) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +1574 -1063
  4. package/dist/index.js +588 -107
  5. package/dist/maintenance-runner.js +565 -84
  6. package/dist/media-audio-runner.js +565 -84
  7. package/dist/media-video-runner.js +565 -84
  8. package/dist/memcode-runtime/CHANGELOG.md +22 -0
  9. package/dist/sdk.js +621 -111
  10. package/dist/vector-backfill-runner.js +565 -84
  11. package/docs/DESIGN_DECISIONS.md +357 -357
  12. package/docs/PERFORMANCE.md +15 -0
  13. package/package.json +2 -1
  14. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  15. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  16. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  17. package/plugins/copilot/memorix/plugin.json +1 -1
  18. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  19. package/plugins/omp/memorix/package.json +1 -1
  20. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/pi/memorix/package.json +1 -1
  22. package/server.json +2 -2
  23. package/src/audit/index.ts +156 -156
  24. package/src/cli/commands/background.ts +34 -2
  25. package/src/cli/commands/formation.ts +48 -48
  26. package/src/cli/commands/git-hook-install.ts +111 -111
  27. package/src/cli/commands/handoff.ts +54 -54
  28. package/src/cli/commands/hooks-status.ts +63 -63
  29. package/src/cli/commands/ingest.ts +44 -44
  30. package/src/cli/commands/integrate-shared.ts +15 -15
  31. package/src/cli/commands/lock.ts +82 -82
  32. package/src/cli/commands/message.ts +104 -104
  33. package/src/cli/commands/operator-shared.ts +6 -3
  34. package/src/cli/commands/poll.ts +58 -58
  35. package/src/cli/commands/reasoning.ts +118 -118
  36. package/src/cli/commands/serve-http.ts +4 -0
  37. package/src/cli/commands/skills.ts +114 -114
  38. package/src/cli/commands/task.ts +167 -167
  39. package/src/cli/commands/transfer.ts +47 -47
  40. package/src/cli/index.ts +406 -406
  41. package/src/cli/tui/ChatView.tsx +234 -234
  42. package/src/cli/tui/CommandBar.tsx +312 -312
  43. package/src/cli/tui/ContextRail.tsx +118 -118
  44. package/src/cli/tui/HeaderBar.tsx +72 -72
  45. package/src/cli/tui/LogoBanner.tsx +51 -51
  46. package/src/cli/tui/index.ts +41 -41
  47. package/src/cli/version.ts +7 -7
  48. package/src/cli/workbench.ts +1 -1
  49. package/src/dashboard/project-classification.ts +64 -64
  50. package/src/embedding/api-provider.ts +503 -27
  51. package/src/embedding/provider.ts +20 -3
  52. package/src/git/hooks-path.ts +85 -85
  53. package/src/hooks/lifecycle.ts +1 -0
  54. package/src/hooks/pattern-detector.ts +173 -173
  55. package/src/hooks/significance-filter.ts +250 -250
  56. package/src/llm/memory-manager.ts +328 -328
  57. package/src/llm/quality.ts +248 -248
  58. package/src/memory/attribution-guard.ts +249 -249
  59. package/src/memory/disclosure-policy.ts +135 -135
  60. package/src/memory/entity-extractor.ts +197 -197
  61. package/src/memory/formation/evaluate.ts +217 -217
  62. package/src/memory/freshness.ts +122 -122
  63. package/src/memory/graph.ts +197 -197
  64. package/src/memory/observations.ts +68 -18
  65. package/src/memory/refs.ts +94 -94
  66. package/src/memory/secret-filter.ts +79 -79
  67. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  68. package/src/orchestrate/adapters/claude.ts +111 -111
  69. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  70. package/src/orchestrate/adapters/codex.ts +41 -41
  71. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  72. package/src/orchestrate/adapters/gemini.ts +42 -42
  73. package/src/orchestrate/adapters/index.ts +73 -73
  74. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  75. package/src/orchestrate/adapters/opencode.ts +47 -47
  76. package/src/orchestrate/adapters/types.ts +77 -77
  77. package/src/orchestrate/capability-router.ts +284 -284
  78. package/src/orchestrate/cost-tracker.ts +219 -219
  79. package/src/orchestrate/error-recovery.ts +191 -191
  80. package/src/orchestrate/evidence.ts +140 -140
  81. package/src/orchestrate/ledger.ts +110 -110
  82. package/src/orchestrate/memorix-bridge.ts +343 -343
  83. package/src/orchestrate/output-budget.ts +80 -80
  84. package/src/orchestrate/permission.ts +152 -152
  85. package/src/orchestrate/pipeline-trace.ts +131 -131
  86. package/src/orchestrate/prompt-builder.ts +155 -155
  87. package/src/orchestrate/ring-buffer.ts +37 -37
  88. package/src/orchestrate/task-graph.ts +389 -389
  89. package/src/project/aliases.ts +374 -374
  90. package/src/rules/adapters/trae.ts +56 -56
  91. package/src/rules/syncer.ts +235 -235
  92. package/src/sdk.ts +12 -8
  93. package/src/search/query-expansion.ts +52 -52
  94. package/src/server/formation-timeout.ts +27 -27
  95. package/src/skills/mini-skills.ts +386 -386
  96. package/src/store/chat-store.ts +119 -119
  97. package/src/store/graph-store.ts +249 -249
  98. package/src/store/mini-skill-store.ts +349 -349
  99. package/src/store/orama-store.ts +21 -3
  100. package/src/store/persistence-json.ts +212 -212
  101. package/src/store/persistence.ts +291 -291
  102. package/src/store/project-affinity.ts +195 -195
  103. package/src/team/event-bus.ts +76 -76
  104. package/src/team/handoff.ts +161 -161
  105. package/src/team/poll.ts +132 -132
  106. package/src/workspace/mcp-adapters/codex.ts +191 -191
  107. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  108. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  109. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  110. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  111. package/src/workspace/mcp-adapters/trae.ts +134 -134
  112. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  113. package/src/workspace/sanitizer.ts +60 -60
@@ -1,87 +1,87 @@
1
- /**
2
- * Team Handoff — Structured context transfer between agents.
3
- *
4
- * Phase 4b: Creates a handoff artifact as a standard observation
5
- * (canonical path — no new JSON/file persistence).
6
- *
7
- * Architectural boundary (B4): The canonical record is an Observation
8
- * stored via storeObservation(). TeamStore only holds a reference pointer.
9
- * Handoff artifacts use valueCategory: 'core' so they are immune to
10
- * retention archival (Phase 1 provenance: core = immune).
11
- */
12
-
13
- import type { TeamStore } from './team-store.js';
14
- import type { ObservationType } from '../types.js';
15
-
16
- // ── Types ──────────────────────────────────────────────────────────
17
-
18
- export interface HandoffInput {
19
- projectId: string;
20
- fromAgentId: string;
21
- toAgentId?: string; // null = anyone can pick up
22
- taskId?: string; // link to specific task
23
- summary: string; // human-readable summary of what was done
24
- context: string; // machine-readable context for the next agent
25
- filesModified?: string[]; // files touched during the work
26
- concepts?: string[]; // key concepts for search discoverability
27
- }
28
-
29
- export interface HandoffResult {
30
- observationId: number;
31
- taskId?: string;
32
- fromAgentId: string;
33
- toAgentId?: string;
34
- summary: string;
35
- }
36
-
37
- // ── createHandoffArtifact ──────────────────────────────────────────
38
-
39
- /**
40
- * Create a structured handoff artifact stored as an observation.
41
- *
42
- * @param input - Handoff details
43
- * @param storeObservation - The canonical storeObservation function (injected to avoid circular deps)
44
- * @param teamStore - For sending notification message to recipient
45
- */
46
- export async function createHandoffArtifact(
47
- input: HandoffInput,
48
- storeObservation: (params: {
49
- entityName: string;
50
- type: ObservationType;
51
- title: string;
52
- narrative: string;
53
- facts?: string[];
54
- filesModified?: string[];
55
- concepts?: string[];
56
- projectId: string;
57
- topicKey?: string;
1
+ /**
2
+ * Team Handoff — Structured context transfer between agents.
3
+ *
4
+ * Phase 4b: Creates a handoff artifact as a standard observation
5
+ * (canonical path — no new JSON/file persistence).
6
+ *
7
+ * Architectural boundary (B4): The canonical record is an Observation
8
+ * stored via storeObservation(). TeamStore only holds a reference pointer.
9
+ * Handoff artifacts use valueCategory: 'core' so they are immune to
10
+ * retention archival (Phase 1 provenance: core = immune).
11
+ */
12
+
13
+ import type { TeamStore } from './team-store.js';
14
+ import type { ObservationType } from '../types.js';
15
+
16
+ // ── Types ──────────────────────────────────────────────────────────
17
+
18
+ export interface HandoffInput {
19
+ projectId: string;
20
+ fromAgentId: string;
21
+ toAgentId?: string; // null = anyone can pick up
22
+ taskId?: string; // link to specific task
23
+ summary: string; // human-readable summary of what was done
24
+ context: string; // machine-readable context for the next agent
25
+ filesModified?: string[]; // files touched during the work
26
+ concepts?: string[]; // key concepts for search discoverability
27
+ }
28
+
29
+ export interface HandoffResult {
30
+ observationId: number;
31
+ taskId?: string;
32
+ fromAgentId: string;
33
+ toAgentId?: string;
34
+ summary: string;
35
+ }
36
+
37
+ // ── createHandoffArtifact ──────────────────────────────────────────
38
+
39
+ /**
40
+ * Create a structured handoff artifact stored as an observation.
41
+ *
42
+ * @param input - Handoff details
43
+ * @param storeObservation - The canonical storeObservation function (injected to avoid circular deps)
44
+ * @param teamStore - For sending notification message to recipient
45
+ */
46
+ export async function createHandoffArtifact(
47
+ input: HandoffInput,
48
+ storeObservation: (params: {
49
+ entityName: string;
50
+ type: ObservationType;
51
+ title: string;
52
+ narrative: string;
53
+ facts?: string[];
54
+ filesModified?: string[];
55
+ concepts?: string[];
56
+ projectId: string;
57
+ topicKey?: string;
58
58
  sourceDetail?: 'explicit' | 'hook' | 'git-ingest';
59
59
  valueCategory?: 'core' | 'contextual' | 'ephemeral';
60
60
  visibility?: 'personal' | 'project' | 'team';
61
61
  sharedWithAgentIds?: string[];
62
62
  createdByAgentId?: string;
63
- }) => Promise<{ observation: { id: number }; upserted: boolean }>,
64
- teamStore: TeamStore,
65
- ): Promise<HandoffResult> {
66
- // Build facts from handoff context
67
- const facts: string[] = [
68
- `Handoff from agent ${input.fromAgentId}`,
69
- ];
70
- if (input.toAgentId) facts.push(`Intended recipient: ${input.toAgentId}`);
71
- if (input.taskId) facts.push(`Related task: ${input.taskId}`);
72
- facts.push(`Context: ${input.context}`);
73
-
74
- // Store as observation — this IS the canonical record
75
- const { observation } = await storeObservation({
76
- entityName: 'team-handoff',
77
- type: 'what-changed',
78
- title: `[Handoff] ${input.summary}`,
79
- narrative: input.context,
80
- facts,
81
- filesModified: input.filesModified,
82
- concepts: ['handoff', ...(input.concepts ?? [])],
83
- projectId: input.projectId,
84
- topicKey: input.taskId ? `handoff:${input.taskId}:${input.fromAgentId}` : undefined,
63
+ }) => Promise<{ observation: { id: number }; upserted: boolean }>,
64
+ teamStore: TeamStore,
65
+ ): Promise<HandoffResult> {
66
+ // Build facts from handoff context
67
+ const facts: string[] = [
68
+ `Handoff from agent ${input.fromAgentId}`,
69
+ ];
70
+ if (input.toAgentId) facts.push(`Intended recipient: ${input.toAgentId}`);
71
+ if (input.taskId) facts.push(`Related task: ${input.taskId}`);
72
+ facts.push(`Context: ${input.context}`);
73
+
74
+ // Store as observation — this IS the canonical record
75
+ const { observation } = await storeObservation({
76
+ entityName: 'team-handoff',
77
+ type: 'what-changed',
78
+ title: `[Handoff] ${input.summary}`,
79
+ narrative: input.context,
80
+ facts,
81
+ filesModified: input.filesModified,
82
+ concepts: ['handoff', ...(input.concepts ?? [])],
83
+ projectId: input.projectId,
84
+ topicKey: input.taskId ? `handoff:${input.taskId}:${input.fromAgentId}` : undefined,
85
85
  sourceDetail: 'explicit',
86
86
  valueCategory: 'core', // immune to retention archival
87
87
  // A direct handoff is not a project-wide memo. The sender retains access
@@ -90,85 +90,85 @@ export async function createHandoffArtifact(
90
90
  visibility: input.toAgentId ? 'personal' : 'team',
91
91
  ...(input.toAgentId ? { sharedWithAgentIds: [input.toAgentId] } : {}),
92
92
  createdByAgentId: input.fromAgentId,
93
- });
94
-
95
- // Emit handoff:created event (best-effort, process-local only).
96
- // Happens AFTER canonical write succeeds — listener errors never affect the handoff.
97
- try {
98
- teamStore.getEventBus()?.emit('handoff:created', {
99
- observationId: observation.id,
100
- projectId: input.projectId,
101
- fromAgent: input.fromAgentId,
102
- toAgent: input.toAgentId,
103
- taskId: input.taskId,
104
- });
105
- } catch {
106
- // Best-effort: event emission failure never affects canonical handoff
107
- }
108
-
109
- // Send notification message to recipient.
110
- // Broadcast handoff uses fanout-on-write (方案 A): instead of one NULL-recipient
111
- // shared message, each active agent (excluding sender) gets their own copy.
112
- // This prevents Agent A's markRead from consuming Agent B's unread state.
113
- try {
114
- if (input.toAgentId) {
115
- // Role validation: if the handoff is linked to a task with required_role,
116
- // verify the recipient's role matches before sending.
117
- if (input.taskId) {
118
- const task = teamStore.getTask(input.taskId);
119
- if (task?.required_role) {
120
- const recipient = teamStore.getAgent(input.toAgentId);
121
- if (recipient && recipient.role !== task.required_role) {
122
- // Log warning but don't block — handoff observation is still created
123
- console.error(
124
- `[memorix] Handoff role mismatch: task "${input.taskId}" requires role "${task.required_role}" but recipient "${input.toAgentId}" has role "${recipient.role}". Handoff proceeds but claim will be rejected.`
125
- );
126
- }
127
- }
128
- }
129
- // Targeted handoff — single recipient-specific message
130
- teamStore.sendMessage({
131
- projectId: input.projectId,
132
- senderAgentId: input.fromAgentId,
133
- recipientAgentId: input.toAgentId,
134
- type: 'handoff',
135
- content: `Handoff: ${input.summary}`,
136
- payload: {
137
- observationId: observation.id,
138
- taskId: input.taskId,
139
- filesModified: input.filesModified,
140
- },
141
- taskId: input.taskId,
142
- });
143
- } else {
144
- // Broadcast handoff — fanout to each active agent (excluding sender)
145
- const agents = teamStore.listAgents(input.projectId, { status: 'active' });
146
- for (const agent of agents) {
147
- if (agent.agent_id === input.fromAgentId) continue;
148
- teamStore.sendMessage({
149
- projectId: input.projectId,
150
- senderAgentId: input.fromAgentId,
151
- recipientAgentId: agent.agent_id,
152
- type: 'handoff',
153
- content: `Handoff: ${input.summary}`,
154
- payload: {
155
- observationId: observation.id,
156
- taskId: input.taskId,
157
- filesModified: input.filesModified,
158
- },
159
- taskId: input.taskId,
160
- });
161
- }
162
- }
163
- } catch {
164
- // Message sending is best-effort — handoff observation is the canonical record
165
- }
166
-
167
- return {
168
- observationId: observation.id,
169
- taskId: input.taskId,
170
- fromAgentId: input.fromAgentId,
171
- toAgentId: input.toAgentId,
172
- summary: input.summary,
173
- };
174
- }
93
+ });
94
+
95
+ // Emit handoff:created event (best-effort, process-local only).
96
+ // Happens AFTER canonical write succeeds — listener errors never affect the handoff.
97
+ try {
98
+ teamStore.getEventBus()?.emit('handoff:created', {
99
+ observationId: observation.id,
100
+ projectId: input.projectId,
101
+ fromAgent: input.fromAgentId,
102
+ toAgent: input.toAgentId,
103
+ taskId: input.taskId,
104
+ });
105
+ } catch {
106
+ // Best-effort: event emission failure never affects canonical handoff
107
+ }
108
+
109
+ // Send notification message to recipient.
110
+ // Broadcast handoff uses fanout-on-write (方案 A): instead of one NULL-recipient
111
+ // shared message, each active agent (excluding sender) gets their own copy.
112
+ // This prevents Agent A's markRead from consuming Agent B's unread state.
113
+ try {
114
+ if (input.toAgentId) {
115
+ // Role validation: if the handoff is linked to a task with required_role,
116
+ // verify the recipient's role matches before sending.
117
+ if (input.taskId) {
118
+ const task = teamStore.getTask(input.taskId);
119
+ if (task?.required_role) {
120
+ const recipient = teamStore.getAgent(input.toAgentId);
121
+ if (recipient && recipient.role !== task.required_role) {
122
+ // Log warning but don't block — handoff observation is still created
123
+ console.error(
124
+ `[memorix] Handoff role mismatch: task "${input.taskId}" requires role "${task.required_role}" but recipient "${input.toAgentId}" has role "${recipient.role}". Handoff proceeds but claim will be rejected.`
125
+ );
126
+ }
127
+ }
128
+ }
129
+ // Targeted handoff — single recipient-specific message
130
+ teamStore.sendMessage({
131
+ projectId: input.projectId,
132
+ senderAgentId: input.fromAgentId,
133
+ recipientAgentId: input.toAgentId,
134
+ type: 'handoff',
135
+ content: `Handoff: ${input.summary}`,
136
+ payload: {
137
+ observationId: observation.id,
138
+ taskId: input.taskId,
139
+ filesModified: input.filesModified,
140
+ },
141
+ taskId: input.taskId,
142
+ });
143
+ } else {
144
+ // Broadcast handoff — fanout to each active agent (excluding sender)
145
+ const agents = teamStore.listAgents(input.projectId, { status: 'active' });
146
+ for (const agent of agents) {
147
+ if (agent.agent_id === input.fromAgentId) continue;
148
+ teamStore.sendMessage({
149
+ projectId: input.projectId,
150
+ senderAgentId: input.fromAgentId,
151
+ recipientAgentId: agent.agent_id,
152
+ type: 'handoff',
153
+ content: `Handoff: ${input.summary}`,
154
+ payload: {
155
+ observationId: observation.id,
156
+ taskId: input.taskId,
157
+ filesModified: input.filesModified,
158
+ },
159
+ taskId: input.taskId,
160
+ });
161
+ }
162
+ }
163
+ } catch {
164
+ // Message sending is best-effort — handoff observation is the canonical record
165
+ }
166
+
167
+ return {
168
+ observationId: observation.id,
169
+ taskId: input.taskId,
170
+ fromAgentId: input.fromAgentId,
171
+ toAgentId: input.toAgentId,
172
+ summary: input.summary,
173
+ };
174
+ }
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
+ }