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.
- package/CHANGELOG.md +35 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +2582 -1845
- package/dist/index.js +1355 -683
- package/dist/maintenance-runner.js +1621 -1096
- package/dist/media-audio-runner.js +1612 -1088
- package/dist/media-video-runner.js +1612 -1088
- package/dist/memcode-runtime/CHANGELOG.md +35 -0
- package/dist/sdk.js +2007 -1396
- package/dist/vector-backfill-runner.js +1610 -1086
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/PERFORMANCE.md +15 -0
- package/package.json +2 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/background.ts +51 -5
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +54 -54
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +82 -82
- package/src/cli/commands/message.ts +104 -104
- package/src/cli/commands/operator-shared.ts +18 -11
- package/src/cli/commands/poll.ts +58 -58
- package/src/cli/commands/reasoning.ts +118 -118
- package/src/cli/commands/serve-http.ts +29 -7
- package/src/cli/commands/serve-shared.ts +49 -6
- package/src/cli/commands/serve.ts +4 -0
- package/src/cli/commands/skills.ts +114 -114
- package/src/cli/commands/task.ts +167 -167
- package/src/cli/commands/transfer.ts +47 -47
- package/src/cli/index.ts +406 -406
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/dashboard/project-classification.ts +64 -64
- package/src/embedding/api-provider.ts +503 -27
- package/src/embedding/provider.ts +20 -3
- package/src/git/hooks-path.ts +85 -85
- package/src/hooks/lifecycle.ts +1 -0
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/disclosure-policy.ts +135 -135
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/observations.ts +68 -18
- package/src/memory/refs.ts +94 -94
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +13 -3
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +343 -343
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/project/aliases.ts +374 -374
- package/src/project/launch-root.ts +115 -0
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/syncer.ts +235 -235
- package/src/runtime/isolated-maintenance.ts +12 -1
- package/src/sdk.ts +12 -8
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/server.ts +38 -5
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +2 -2
- package/src/store/orama-store.ts +21 -3
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/sqlite-db.ts +2 -0
- package/src/store/sqlite-store.ts +19 -4
- package/src/team/event-bus.ts +76 -76
- package/src/team/handoff.ts +161 -161
- package/src/team/poll.ts +132 -132
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
package/src/team/handoff.ts
CHANGED
|
@@ -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
|
+
}
|