memorix 1.8.0 → 1.8.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.
@@ -889,6 +889,15 @@ export default defineCommand({
889
889
  }
890
890
 
891
891
  if (apiPath === '/team') {
892
+ if (req.method !== 'GET') {
893
+ sendJson({
894
+ status: 'error',
895
+ readOnly: true,
896
+ error: 'Coordination status is read-only',
897
+ errorCode: 'COORDINATION_STATUS_READ_ONLY',
898
+ }, 405);
899
+ return;
900
+ }
892
901
  // Phase 4a: All team state is in SQLite via TeamStore
893
902
  const { projectId: teamProjectId, dataDir: teamDataDir } = await resolveRequestProject(url);
894
903
  const ts = await getTeamStore(teamDataDir);
@@ -967,8 +976,9 @@ export default defineCommand({
967
976
  const openHandoffs = handoffs.filter((handoff: any) => handoff.status !== 'completed').length;
968
977
 
969
978
  sendJson({
979
+ status: 'ok',
970
980
  mode: 'control-plane',
971
- readOnly: false,
981
+ readOnly: true,
972
982
  scope,
973
983
  agents: withTier,
974
984
  // Primary headline number
@@ -1650,6 +1660,23 @@ export default defineCommand({
1650
1660
 
1651
1661
  sendJson({ error: 'Not found' }, 404);
1652
1662
  } catch (err) {
1663
+ if (apiPath === '/team') {
1664
+ const scope = url.searchParams.get('scope') === 'global' ? 'global' : 'project';
1665
+ console.error('[memorix] coordination snapshot unavailable', { status: 'error', scope });
1666
+ sendJson({
1667
+ status: 'error',
1668
+ mode: 'control-plane',
1669
+ readOnly: true,
1670
+ scope,
1671
+ agents: [],
1672
+ locks: [],
1673
+ tasks: [],
1674
+ handoffs: [],
1675
+ error: 'Coordination state unavailable',
1676
+ errorCode: 'COORDINATION_STATE_UNAVAILABLE',
1677
+ }, 503);
1678
+ return;
1679
+ }
1653
1680
  sendJson({ error: err instanceof Error ? err.message : 'Unknown error' }, 500);
1654
1681
  }
1655
1682
  }
@@ -958,18 +958,10 @@ interface DashboardState {
958
958
  dataDir: string;
959
959
  projectRoot: string | null;
960
960
  projectResolved: boolean;
961
- mode: 'standalone' | 'control-plane';
961
+ mode: 'standalone';
962
962
  port: number;
963
963
  }
964
964
 
965
- /** Optional coordination instances passed from MCP server */
966
- export interface TeamInstances {
967
- registry: { listAgents: (filter?: any) => any[]; getActiveCount: () => number; getAgent: (id: string) => any };
968
- fileLocks: { listLocks: (agentId?: string) => any[]; cleanExpired: () => void };
969
- taskManager: { list: (filter?: any) => any[]; getAvailable: () => any[] };
970
- messageBus: { getUnreadCount: (agentId: string) => number };
971
- }
972
-
973
965
  function parseJsonField(value: unknown, fallback: unknown): unknown {
974
966
  if (typeof value !== 'string') return value ?? fallback;
975
967
  try {
@@ -1027,7 +1019,72 @@ function normalizeDashboardTask(task: any) {
1027
1019
  };
1028
1020
  }
1029
1021
 
1030
- async function buildTeamSnapshot(dataDir: string, projectId: string, scope: string, mode: DashboardState['mode']) {
1022
+ type TeamSnapshotStatus = 'ok' | 'degraded' | 'error';
1023
+
1024
+ interface TeamSnapshot {
1025
+ status: TeamSnapshotStatus;
1026
+ mode: DashboardState['mode'];
1027
+ readOnly: true;
1028
+ scope: string;
1029
+ agents: any[];
1030
+ activeCount: number;
1031
+ recentCount: number;
1032
+ historicalCount: number;
1033
+ totalAgents: number;
1034
+ recentWindowDays: number;
1035
+ locks: any[];
1036
+ tasks: any[];
1037
+ availableTasks: number;
1038
+ sessions: number;
1039
+ roles: any[];
1040
+ roleOccupancy: any[];
1041
+ handoffs: any[];
1042
+ openTasks: number;
1043
+ openHandoffs: number;
1044
+ totalUnread: number;
1045
+ activeSessions: number;
1046
+ error?: string;
1047
+ errorCode?: string;
1048
+ }
1049
+
1050
+ function emptyTeamSnapshot(
1051
+ mode: DashboardState['mode'],
1052
+ scope: string,
1053
+ status: Exclude<TeamSnapshotStatus, 'ok'>,
1054
+ ): TeamSnapshot {
1055
+ return {
1056
+ status,
1057
+ mode,
1058
+ readOnly: true,
1059
+ scope,
1060
+ agents: [],
1061
+ activeCount: 0,
1062
+ recentCount: 0,
1063
+ historicalCount: 0,
1064
+ totalAgents: 0,
1065
+ recentWindowDays: 7,
1066
+ locks: [],
1067
+ tasks: [],
1068
+ availableTasks: 0,
1069
+ sessions: 0,
1070
+ roles: [],
1071
+ roleOccupancy: [],
1072
+ handoffs: [],
1073
+ openTasks: 0,
1074
+ openHandoffs: 0,
1075
+ totalUnread: 0,
1076
+ activeSessions: 0,
1077
+ error: status === 'error' ? 'Coordination state unavailable' : 'Coordination state is incomplete',
1078
+ errorCode: status === 'error' ? 'COORDINATION_STATE_UNAVAILABLE' : 'COORDINATION_STATE_DEGRADED',
1079
+ };
1080
+ }
1081
+
1082
+ function logTeamSnapshotFailure(scope: string, status: Exclude<TeamSnapshotStatus, 'ok'>): void {
1083
+ const safeScope = scope === 'global' ? 'global' : 'project';
1084
+ console.error('[dashboard] coordination snapshot unavailable', { status, scope: safeScope });
1085
+ }
1086
+
1087
+ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: string, mode: DashboardState['mode']): Promise<TeamSnapshot> {
1031
1088
  try {
1032
1089
  const { initTeamStore } = await import('../team/team-store.js');
1033
1090
  const teamStore = await initTeamStore(dataDir);
@@ -1053,8 +1110,9 @@ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: stri
1053
1110
  const roleOccupancy = effectiveProjectId ? teamStore.getRoleOccupancy(effectiveProjectId) : [];
1054
1111
  const handoffs = effectiveProjectId ? teamStore.listHandoffs(effectiveProjectId) : [];
1055
1112
  return {
1113
+ status: 'ok',
1056
1114
  mode,
1057
- readOnly: mode === 'standalone',
1115
+ readOnly: true,
1058
1116
  scope,
1059
1117
  agents: withTier,
1060
1118
  activeCount,
@@ -1075,28 +1133,8 @@ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: stri
1075
1133
  activeSessions: activeCount,
1076
1134
  };
1077
1135
  } catch {
1078
- return {
1079
- mode,
1080
- readOnly: mode === 'standalone',
1081
- scope,
1082
- agents: [],
1083
- activeCount: 0,
1084
- recentCount: 0,
1085
- historicalCount: 0,
1086
- totalAgents: 0,
1087
- recentWindowDays: 7,
1088
- locks: [],
1089
- tasks: [],
1090
- availableTasks: 0,
1091
- sessions: 0,
1092
- roles: [],
1093
- roleOccupancy: [],
1094
- handoffs: [],
1095
- openTasks: 0,
1096
- openHandoffs: 0,
1097
- totalUnread: 0,
1098
- activeSessions: 0,
1099
- };
1136
+ logTeamSnapshotFailure(scope, 'error');
1137
+ return emptyTeamSnapshot(mode, scope, 'error');
1100
1138
  }
1101
1139
  }
1102
1140
 
@@ -1125,7 +1163,6 @@ export async function startDashboard(
1125
1163
  projectId: string,
1126
1164
  projectName: string,
1127
1165
  autoOpen = true,
1128
- teamInstances?: TeamInstances,
1129
1166
  projectRoot: string | null = null,
1130
1167
  projectResolved = true,
1131
1168
  ): Promise<void> {
@@ -1136,8 +1173,7 @@ export async function startDashboard(
1136
1173
  const baseDir = getBaseDataDir();
1137
1174
 
1138
1175
  // Mutable state — can be updated via /api/set-current-project
1139
- const isControlPlane = !!teamInstances;
1140
- const state: DashboardState = { projectId, projectName, dataDir, projectRoot, projectResolved, mode: isControlPlane ? 'control-plane' : 'standalone', port };
1176
+ const state: DashboardState = { projectId, projectName, dataDir, projectRoot, projectResolved, mode: 'standalone', port };
1141
1177
 
1142
1178
  const server = createServer(async (req, res) => {
1143
1179
  const url = req.url || '/';
@@ -1164,56 +1200,20 @@ export async function startDashboard(
1164
1200
  return;
1165
1201
  }
1166
1202
 
1167
- if (url.startsWith('/api/team')) {
1168
- if (!teamInstances) {
1169
- const parsedUrl = new URL(url, `http://127.0.0.1:${port}`);
1170
- const scope = parsedUrl.searchParams.get('scope') || 'project';
1171
- sendJson(res, await buildTeamSnapshot(state.dataDir, state.projectId, scope, state.mode));
1172
- return;
1173
- }
1174
- try {
1175
- teamInstances.fileLocks.cleanExpired();
1176
- const agents = teamInstances.registry.listAgents();
1177
- const locks = teamInstances.fileLocks.listLocks();
1178
- const tasks = teamInstances.taskManager.list();
1179
- const available = teamInstances.taskManager.getAvailable();
1180
-
1181
- // Role occupancy and handoffs from TeamStore (if available)
1182
- let roles: any[] = [];
1183
- let roleOccupancy: any[] = [];
1184
- let handoffs: any[] = [];
1185
- try {
1186
- const { getTeamStore, isTeamStoreInitialized } = await import('../team/team-store.js');
1187
- if (isTeamStoreInitialized()) {
1188
- const teamStore = getTeamStore();
1189
- const projectId = state.projectId;
1190
- roles = teamStore.listRoles(projectId);
1191
- roleOccupancy = teamStore.getRoleOccupancy(projectId);
1192
- handoffs = teamStore.listHandoffs(projectId);
1193
- }
1194
- } catch { /* team store not available in standalone mode */ }
1195
-
1203
+ const parsedUrl = new URL(url, `http://127.0.0.1:${port}`);
1204
+ if (parsedUrl.pathname === '/api/team') {
1205
+ if (req.method !== 'GET') {
1196
1206
  sendJson(res, {
1197
- agents: agents.map((a: any) => ({
1198
- ...a,
1199
- unread: teamInstances!.messageBus.getUnreadCount(a.id),
1200
- })),
1201
- activeCount: teamInstances.registry.getActiveCount(),
1202
- locks,
1203
- tasks,
1204
- availableTasks: available.length,
1205
- roles,
1206
- roleOccupancy,
1207
- handoffs,
1208
- // Resume data for "Continue this project" area
1209
- openTasks: tasks.filter((t: any) => t.status === 'pending' || t.status === 'in_progress').length,
1210
- openHandoffs: handoffs.filter((h: any) => h.handoff_status === 'open' || h.handoffStatus === 'open').length,
1211
- totalUnread: agents.reduce((sum: number, a: any) => sum + teamInstances!.messageBus.getUnreadCount(a.id), 0),
1212
- activeSessions: agents.filter((a: any) => a.status === 'active').length,
1213
- });
1214
- } catch {
1215
- sendJson(res, { agents: [], activeCount: 0, locks: [], tasks: [], availableTasks: 0, roles: [], roleOccupancy: [], handoffs: [] });
1207
+ status: 'error',
1208
+ readOnly: true,
1209
+ error: 'Coordination status is read-only',
1210
+ errorCode: 'COORDINATION_STATUS_READ_ONLY',
1211
+ }, 405);
1212
+ return;
1216
1213
  }
1214
+ const scope = parsedUrl.searchParams.get('scope') || 'project';
1215
+ const snapshot = await buildTeamSnapshot(state.dataDir, state.projectId, scope, state.mode);
1216
+ sendJson(res, snapshot, snapshot.status === 'error' ? 503 : 200);
1217
1217
  return;
1218
1218
  }
1219
1219
 
@@ -1237,13 +1237,12 @@ export async function startDashboard(
1237
1237
  server.listen(port, '127.0.0.1', () => {
1238
1238
  const url = `http://127.0.0.1:${port}`;
1239
1239
  const resolvedLabel = projectResolved ? 'resolved' : 'unresolved';
1240
- const modeLabel = isControlPlane ? 'Control Plane' : 'Standalone';
1240
+ const modeLabel = 'Standalone';
1241
1241
  console.error(` Memorix Dashboard [${modeLabel}]`);
1242
1242
  console.error(` ───────────────────────`);
1243
1243
  console.error(` Mode: ${modeLabel}`);
1244
1244
  console.error(` Project: ${projectName} (${projectId}) [${resolvedLabel}]`);
1245
1245
  console.error(` Local: ${url}`);
1246
- if (isControlPlane) console.error(` MCP: ${url}/mcp`);
1247
1246
  console.error(` Data dir: ${dataDir}`);
1248
1247
  console.error(`\n Press Ctrl+C to stop\n`);
1249
1248
  if (autoOpen) openBrowser(url);
@@ -135,7 +135,7 @@
135
135
  </svg>
136
136
  <span class="nav-label">Sessions</span>
137
137
  </button>
138
- <button class="nav-btn" data-page="team" title="Coordination" aria-label="Coordination">
138
+ <button class="nav-btn" data-page="team" title="Coordination Status" aria-label="Coordination Status">
139
139
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
140
140
  <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
141
141
  <circle cx="9" cy="7" r="4"/>
package/src/server.ts CHANGED
@@ -198,7 +198,7 @@ function createDeterministicInstanceId(projectId: string, agentType: string, age
198
198
  * Create and configure the Memorix MCP Server.
199
199
  */
200
200
  /** Optional shared TeamStore — passed by serve-http so all sessions share state */
201
- export interface SharedTeamInstances {
201
+ export interface SharedTeamStore {
202
202
  teamStore: import('./team/team-store.js').TeamStore;
203
203
  }
204
204
 
@@ -245,7 +245,7 @@ export function shouldAwaitProjectRuntime(toolName: string): boolean {
245
245
  export async function createMemorixServer(
246
246
  cwd?: string,
247
247
  existingServer?: McpServer,
248
- sharedTeam?: SharedTeamInstances,
248
+ sharedTeam?: SharedTeamStore,
249
249
  options: CreateMemorixServerOptions = {},
250
250
  ): Promise<{
251
251
  server: McpServer;
@@ -4420,7 +4420,7 @@ export async function createMemorixServer(
4420
4420
  console.error(`[memorix] Dashboard staticDir: ${staticDir}`);
4421
4421
 
4422
4422
  // Start in background (non-blocking), disable auto-open (we'll open it ourselves)
4423
- startDashboard(projectDir, portNum, staticDir, project.id, project.name, false, undefined, project.rootPath, projectResolved)
4423
+ startDashboard(projectDir, portNum, staticDir, project.id, project.name, false, project.rootPath, projectResolved)
4424
4424
  .then(() => { dashboardRunning = true; })
4425
4425
  .catch((err) => { console.error('[memorix] Dashboard error:', err); dashboardRunning = false; });
4426
4426
 
@@ -4786,7 +4786,7 @@ export async function createMemorixServer(
4786
4786
  server.registerTool(
4787
4787
  'team_task',
4788
4788
  {
4789
- title: 'Task Board',
4789
+ title: 'Task Status',
4790
4790
  description:
4791
4791
  'Create, claim, complete, or list tasks in the team task board. Supports dependencies. ' +
4792
4792
  'Action "create": create a task. Action "claim": assign to yourself (atomic, race-safe). ' +