memorix 1.6.1 → 1.6.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 (122) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +2582 -1845
  4. package/dist/index.js +1355 -683
  5. package/dist/maintenance-runner.js +1621 -1096
  6. package/dist/media-audio-runner.js +1612 -1088
  7. package/dist/media-video-runner.js +1612 -1088
  8. package/dist/memcode-runtime/CHANGELOG.md +35 -0
  9. package/dist/sdk.js +2007 -1396
  10. package/dist/vector-backfill-runner.js +1610 -1086
  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 +51 -5
  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 +18 -11
  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 +29 -7
  37. package/src/cli/commands/serve-shared.ts +49 -6
  38. package/src/cli/commands/serve.ts +4 -0
  39. package/src/cli/commands/skills.ts +114 -114
  40. package/src/cli/commands/task.ts +167 -167
  41. package/src/cli/commands/transfer.ts +47 -47
  42. package/src/cli/index.ts +406 -406
  43. package/src/cli/tui/ChatView.tsx +234 -234
  44. package/src/cli/tui/CommandBar.tsx +312 -312
  45. package/src/cli/tui/ContextRail.tsx +118 -118
  46. package/src/cli/tui/HeaderBar.tsx +72 -72
  47. package/src/cli/tui/LogoBanner.tsx +51 -51
  48. package/src/cli/tui/index.ts +41 -41
  49. package/src/cli/version.ts +7 -7
  50. package/src/cli/workbench.ts +1 -1
  51. package/src/dashboard/project-classification.ts +64 -64
  52. package/src/embedding/api-provider.ts +503 -27
  53. package/src/embedding/provider.ts +20 -3
  54. package/src/git/hooks-path.ts +85 -85
  55. package/src/hooks/lifecycle.ts +1 -0
  56. package/src/hooks/pattern-detector.ts +173 -173
  57. package/src/hooks/significance-filter.ts +250 -250
  58. package/src/llm/memory-manager.ts +328 -328
  59. package/src/llm/quality.ts +248 -248
  60. package/src/memory/attribution-guard.ts +249 -249
  61. package/src/memory/disclosure-policy.ts +135 -135
  62. package/src/memory/entity-extractor.ts +197 -197
  63. package/src/memory/formation/evaluate.ts +217 -217
  64. package/src/memory/freshness.ts +122 -122
  65. package/src/memory/graph.ts +197 -197
  66. package/src/memory/observations.ts +68 -18
  67. package/src/memory/refs.ts +94 -94
  68. package/src/memory/secret-filter.ts +79 -79
  69. package/src/memory/session.ts +13 -3
  70. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  71. package/src/orchestrate/adapters/claude.ts +111 -111
  72. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  73. package/src/orchestrate/adapters/codex.ts +41 -41
  74. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  75. package/src/orchestrate/adapters/gemini.ts +42 -42
  76. package/src/orchestrate/adapters/index.ts +73 -73
  77. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  78. package/src/orchestrate/adapters/opencode.ts +47 -47
  79. package/src/orchestrate/adapters/types.ts +77 -77
  80. package/src/orchestrate/capability-router.ts +284 -284
  81. package/src/orchestrate/cost-tracker.ts +219 -219
  82. package/src/orchestrate/error-recovery.ts +191 -191
  83. package/src/orchestrate/evidence.ts +140 -140
  84. package/src/orchestrate/ledger.ts +110 -110
  85. package/src/orchestrate/memorix-bridge.ts +343 -343
  86. package/src/orchestrate/output-budget.ts +80 -80
  87. package/src/orchestrate/permission.ts +152 -152
  88. package/src/orchestrate/pipeline-trace.ts +131 -131
  89. package/src/orchestrate/prompt-builder.ts +155 -155
  90. package/src/orchestrate/ring-buffer.ts +37 -37
  91. package/src/orchestrate/task-graph.ts +389 -389
  92. package/src/project/aliases.ts +374 -374
  93. package/src/project/launch-root.ts +115 -0
  94. package/src/rules/adapters/trae.ts +56 -56
  95. package/src/rules/syncer.ts +235 -235
  96. package/src/runtime/isolated-maintenance.ts +12 -1
  97. package/src/sdk.ts +12 -8
  98. package/src/search/query-expansion.ts +52 -52
  99. package/src/server/formation-timeout.ts +27 -27
  100. package/src/server.ts +38 -5
  101. package/src/skills/mini-skills.ts +386 -386
  102. package/src/store/chat-store.ts +119 -119
  103. package/src/store/graph-store.ts +249 -249
  104. package/src/store/mini-skill-store.ts +349 -349
  105. package/src/store/obs-store.ts +2 -2
  106. package/src/store/orama-store.ts +21 -3
  107. package/src/store/persistence-json.ts +212 -212
  108. package/src/store/persistence.ts +291 -291
  109. package/src/store/project-affinity.ts +195 -195
  110. package/src/store/sqlite-db.ts +2 -0
  111. package/src/store/sqlite-store.ts +19 -4
  112. package/src/team/event-bus.ts +76 -76
  113. package/src/team/handoff.ts +161 -161
  114. package/src/team/poll.ts +132 -132
  115. package/src/workspace/mcp-adapters/codex.ts +191 -191
  116. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  117. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  118. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  119. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  120. package/src/workspace/mcp-adapters/trae.ts +134 -134
  121. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  122. 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
+ }