memorix 1.4.6 → 1.5.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +225 -70
  4. package/dist/cli/memcode.js +0 -0
  5. package/dist/index.js +225 -70
  6. package/dist/maintenance-runner.js +183 -38
  7. package/dist/media-audio-runner.js +183 -38
  8. package/dist/media-video-runner.js +183 -38
  9. package/dist/memcode-runtime/CHANGELOG.md +38 -0
  10. package/dist/sdk.js +224 -69
  11. package/dist/types.d.ts +2 -0
  12. package/dist/vector-backfill-runner.js +183 -38
  13. package/docs/CONFIGURATION.md +12 -0
  14. package/docs/DESIGN_DECISIONS.md +357 -357
  15. package/package.json +1 -1
  16. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  17. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  18. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  19. package/plugins/copilot/memorix/plugin.json +1 -1
  20. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  21. package/plugins/omp/memorix/package.json +1 -1
  22. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  23. package/plugins/pi/memorix/package.json +1 -1
  24. package/server.json +2 -2
  25. package/src/audit/index.ts +156 -156
  26. package/src/cli/commands/formation.ts +48 -48
  27. package/src/cli/commands/git-hook-install.ts +111 -111
  28. package/src/cli/commands/handoff.ts +54 -54
  29. package/src/cli/commands/hooks-status.ts +63 -63
  30. package/src/cli/commands/ingest.ts +44 -44
  31. package/src/cli/commands/integrate-shared.ts +15 -15
  32. package/src/cli/commands/lock.ts +82 -82
  33. package/src/cli/commands/message.ts +104 -104
  34. package/src/cli/commands/poll.ts +58 -58
  35. package/src/cli/commands/reasoning.ts +118 -118
  36. package/src/cli/commands/skills.ts +114 -114
  37. package/src/cli/commands/task.ts +167 -167
  38. package/src/cli/commands/transfer.ts +47 -47
  39. package/src/cli/index.ts +406 -406
  40. package/src/cli/tui/ChatView.tsx +234 -234
  41. package/src/cli/tui/CommandBar.tsx +312 -312
  42. package/src/cli/tui/ContextRail.tsx +118 -118
  43. package/src/cli/tui/HeaderBar.tsx +72 -72
  44. package/src/cli/tui/LogoBanner.tsx +51 -51
  45. package/src/cli/tui/index.ts +41 -41
  46. package/src/cli/version.ts +7 -7
  47. package/src/cli/workbench.ts +1 -1
  48. package/src/config.ts +23 -2
  49. package/src/dashboard/project-classification.ts +64 -64
  50. package/src/embedding/api-provider.ts +105 -22
  51. package/src/embedding/provider.ts +2 -0
  52. package/src/git/hooks-path.ts +85 -85
  53. package/src/hooks/pattern-detector.ts +173 -173
  54. package/src/hooks/significance-filter.ts +250 -250
  55. package/src/llm/memory-manager.ts +328 -328
  56. package/src/llm/provider.ts +17 -8
  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/formation/extract.ts +6 -3
  63. package/src/memory/formation/index.ts +2 -1
  64. package/src/memory/formation/resolve.ts +10 -6
  65. package/src/memory/formation/types.ts +3 -1
  66. package/src/memory/freshness.ts +122 -122
  67. package/src/memory/graph.ts +197 -197
  68. package/src/memory/long-term.ts +7 -1
  69. package/src/memory/refs.ts +94 -94
  70. package/src/memory/secret-filter.ts +79 -79
  71. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  72. package/src/orchestrate/adapters/claude.ts +111 -111
  73. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  74. package/src/orchestrate/adapters/codex.ts +41 -41
  75. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  76. package/src/orchestrate/adapters/gemini.ts +42 -42
  77. package/src/orchestrate/adapters/index.ts +73 -73
  78. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  79. package/src/orchestrate/adapters/opencode.ts +47 -47
  80. package/src/orchestrate/adapters/types.ts +77 -77
  81. package/src/orchestrate/capability-router.ts +284 -284
  82. package/src/orchestrate/cost-tracker.ts +219 -219
  83. package/src/orchestrate/error-recovery.ts +191 -191
  84. package/src/orchestrate/evidence.ts +140 -140
  85. package/src/orchestrate/ledger.ts +110 -110
  86. package/src/orchestrate/memorix-bridge.ts +343 -343
  87. package/src/orchestrate/output-budget.ts +80 -80
  88. package/src/orchestrate/permission.ts +152 -152
  89. package/src/orchestrate/pipeline-trace.ts +131 -131
  90. package/src/orchestrate/prompt-builder.ts +155 -155
  91. package/src/orchestrate/ring-buffer.ts +37 -37
  92. package/src/orchestrate/task-graph.ts +389 -389
  93. package/src/project/aliases.ts +374 -374
  94. package/src/rules/adapters/trae.ts +56 -56
  95. package/src/rules/syncer.ts +235 -235
  96. package/src/search/query-expansion.ts +52 -52
  97. package/src/server/formation-timeout.ts +27 -27
  98. package/src/server.ts +14 -10
  99. package/src/skills/mini-skills.ts +386 -386
  100. package/src/store/chat-store.ts +119 -119
  101. package/src/store/graph-store.ts +249 -249
  102. package/src/store/mini-skill-store.ts +349 -349
  103. package/src/store/orama-store.ts +6 -4
  104. package/src/store/persistence-json.ts +212 -212
  105. package/src/store/persistence.ts +291 -291
  106. package/src/store/project-affinity.ts +195 -195
  107. package/src/team/event-bus.ts +76 -76
  108. package/src/team/handoff.ts +161 -161
  109. package/src/team/poll.ts +132 -132
  110. package/src/timeout.ts +67 -0
  111. package/src/types.ts +2 -0
  112. package/src/workspace/mcp-adapters/codex.ts +191 -191
  113. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  114. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  115. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  116. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  117. package/src/workspace/mcp-adapters/trae.ts +134 -134
  118. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  119. 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
+ }
package/src/timeout.ts CHANGED
@@ -21,3 +21,70 @@ export function withTimeout<T>(promise: Promise<T>, ms: number, label: string):
21
21
  );
22
22
  });
23
23
  }
24
+
25
+ /**
26
+ * Run an operation with a deadline and an AbortSignal for cancellable work.
27
+ * Callers should pass the signal to fetch/embedding/LLM APIs they own.
28
+ */
29
+ export function withTimeoutSignal<T>(
30
+ operation: (signal: AbortSignal) => Promise<T>,
31
+ ms: number,
32
+ label: string,
33
+ parentSignal?: AbortSignal,
34
+ ): Promise<T> {
35
+ const controller = new AbortController();
36
+ const signal = parentSignal
37
+ ? AbortSignal.any([parentSignal, controller.signal])
38
+ : controller.signal;
39
+
40
+ return new Promise<T>((resolve, reject) => {
41
+ let settled = false;
42
+ let timedOut = false;
43
+ let timer: ReturnType<typeof setTimeout> | undefined;
44
+ const cleanup = () => {
45
+ if (timer !== undefined) clearTimeout(timer);
46
+ parentSignal?.removeEventListener('abort', onParentAbort);
47
+ };
48
+ const onParentAbort = () => {
49
+ if (settled || timedOut) return;
50
+ settled = true;
51
+ cleanup();
52
+ reject(parentSignal?.reason ?? new Error(`${label} aborted`));
53
+ };
54
+ parentSignal?.addEventListener('abort', onParentAbort, { once: true });
55
+
56
+ timer = setTimeout(() => {
57
+ if (settled) return;
58
+ timedOut = true;
59
+ controller.abort(new Error(`${label} timed out after ${ms}ms`));
60
+ settled = true;
61
+ cleanup();
62
+ reject(new Error(`${label} timed out after ${ms}ms`));
63
+ }, ms);
64
+
65
+ let pending: Promise<T>;
66
+ try {
67
+ pending = operation(signal);
68
+ } catch (error) {
69
+ settled = true;
70
+ cleanup();
71
+ reject(error);
72
+ return;
73
+ }
74
+
75
+ pending.then(
76
+ (value) => {
77
+ if (settled) return;
78
+ settled = true;
79
+ cleanup();
80
+ resolve(value);
81
+ },
82
+ (error: unknown) => {
83
+ if (settled) return;
84
+ settled = true;
85
+ cleanup();
86
+ reject(timedOut ? new Error(`${label} timed out after ${ms}ms`) : error);
87
+ },
88
+ );
89
+ });
90
+ }