memorix 1.6.0 → 1.6.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 +24 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +317 -62
  4. package/dist/cli/memcode.js +12 -1
  5. package/dist/index.js +56 -9
  6. package/dist/maintenance-runner.js +54 -7
  7. package/dist/media-audio-runner.js +54 -7
  8. package/dist/media-video-runner.js +54 -7
  9. package/dist/memcode-runtime/CHANGELOG.md +24 -0
  10. package/dist/sdk.js +55 -8
  11. package/dist/vector-backfill-runner.js +54 -7
  12. package/docs/AGENT_OPERATOR_PLAYBOOK.md +11 -3
  13. package/docs/API_REFERENCE.md +1 -1
  14. package/docs/DESIGN_DECISIONS.md +357 -357
  15. package/docs/DOCKER.md +5 -1
  16. package/docs/PERFORMANCE.md +2 -2
  17. package/docs/README.md +3 -1
  18. package/docs/SETUP.md +8 -2
  19. package/package.json +1 -1
  20. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  21. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  22. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  23. package/plugins/copilot/memorix/plugin.json +1 -1
  24. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  25. package/plugins/omp/memorix/package.json +1 -1
  26. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/pi/memorix/package.json +1 -1
  28. package/server.json +2 -2
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/commands/agent-integrations.ts +9 -3
  31. package/src/cli/commands/doctor.ts +46 -15
  32. package/src/cli/commands/formation.ts +48 -48
  33. package/src/cli/commands/git-hook-install.ts +111 -111
  34. package/src/cli/commands/handoff.ts +54 -54
  35. package/src/cli/commands/hook.ts +13 -1
  36. package/src/cli/commands/hooks-status.ts +63 -63
  37. package/src/cli/commands/ingest.ts +44 -44
  38. package/src/cli/commands/integrate-shared.ts +15 -15
  39. package/src/cli/commands/lock.ts +82 -82
  40. package/src/cli/commands/message.ts +104 -104
  41. package/src/cli/commands/poll.ts +58 -58
  42. package/src/cli/commands/reasoning.ts +118 -118
  43. package/src/cli/commands/serve-http.ts +68 -7
  44. package/src/cli/commands/skills.ts +114 -114
  45. package/src/cli/commands/task.ts +167 -167
  46. package/src/cli/commands/transfer.ts +47 -47
  47. package/src/cli/heap.ts +76 -0
  48. package/src/cli/index.ts +406 -406
  49. package/src/cli/invocation.ts +7 -4
  50. package/src/cli/tui/ChatView.tsx +234 -234
  51. package/src/cli/tui/CommandBar.tsx +312 -312
  52. package/src/cli/tui/ContextRail.tsx +118 -118
  53. package/src/cli/tui/HeaderBar.tsx +72 -72
  54. package/src/cli/tui/LogoBanner.tsx +51 -51
  55. package/src/cli/tui/index.ts +41 -41
  56. package/src/cli/version.ts +7 -7
  57. package/src/cli/workbench.ts +1 -1
  58. package/src/dashboard/project-classification.ts +64 -64
  59. package/src/embedding/provider.ts +76 -6
  60. package/src/git/hooks-path.ts +85 -85
  61. package/src/hooks/handler.ts +9 -18
  62. package/src/hooks/lifecycle.ts +173 -0
  63. package/src/hooks/pattern-detector.ts +173 -173
  64. package/src/hooks/significance-filter.ts +250 -250
  65. package/src/llm/memory-manager.ts +328 -328
  66. package/src/llm/quality.ts +248 -248
  67. package/src/memory/attribution-guard.ts +249 -249
  68. package/src/memory/disclosure-policy.ts +135 -135
  69. package/src/memory/entity-extractor.ts +197 -197
  70. package/src/memory/formation/evaluate.ts +217 -217
  71. package/src/memory/freshness.ts +122 -122
  72. package/src/memory/graph.ts +197 -197
  73. package/src/memory/refs.ts +94 -94
  74. package/src/memory/secret-filter.ts +79 -79
  75. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  76. package/src/orchestrate/adapters/claude.ts +111 -111
  77. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  78. package/src/orchestrate/adapters/codex.ts +41 -41
  79. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  80. package/src/orchestrate/adapters/gemini.ts +42 -42
  81. package/src/orchestrate/adapters/index.ts +73 -73
  82. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  83. package/src/orchestrate/adapters/opencode.ts +47 -47
  84. package/src/orchestrate/adapters/types.ts +77 -77
  85. package/src/orchestrate/capability-router.ts +284 -284
  86. package/src/orchestrate/cost-tracker.ts +219 -219
  87. package/src/orchestrate/error-recovery.ts +191 -191
  88. package/src/orchestrate/evidence.ts +140 -140
  89. package/src/orchestrate/ledger.ts +110 -110
  90. package/src/orchestrate/memorix-bridge.ts +343 -343
  91. package/src/orchestrate/output-budget.ts +80 -80
  92. package/src/orchestrate/permission.ts +152 -152
  93. package/src/orchestrate/pipeline-trace.ts +131 -131
  94. package/src/orchestrate/prompt-builder.ts +155 -155
  95. package/src/orchestrate/ring-buffer.ts +37 -37
  96. package/src/orchestrate/task-graph.ts +389 -389
  97. package/src/project/aliases.ts +374 -374
  98. package/src/rules/adapters/trae.ts +56 -56
  99. package/src/rules/syncer.ts +235 -235
  100. package/src/search/query-expansion.ts +52 -52
  101. package/src/server/formation-timeout.ts +27 -27
  102. package/src/skills/mini-skills.ts +386 -386
  103. package/src/store/chat-store.ts +119 -119
  104. package/src/store/graph-store.ts +249 -249
  105. package/src/store/mini-skill-store.ts +349 -349
  106. package/src/store/persistence-json.ts +212 -212
  107. package/src/store/persistence.ts +291 -291
  108. package/src/store/project-affinity.ts +195 -195
  109. package/src/team/event-bus.ts +76 -76
  110. package/src/team/handoff.ts +161 -161
  111. package/src/team/poll.ts +132 -132
  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,32 +1,32 @@
1
- import { defineCommand } from 'citty';
2
- import { computePoll, computeWatermark } from '../../team/poll.js';
3
- import { getObservationStore } from '../../store/obs-store.js';
1
+ import { defineCommand } from 'citty';
2
+ import { computePoll, computeWatermark } from '../../team/poll.js';
3
+ import { getObservationStore } from '../../store/obs-store.js';
4
4
  import { getAllObservations } from '../../memory/observations.js';
5
5
  import { withFreshIndex } from '../../memory/freshness.js';
6
6
  import { filterReadableObservations } from '../../memory/visibility.js';
7
7
  import { emitError, emitResult, getCliProjectContext, resolveCliActorId, shortId } from './operator-shared.js';
8
-
9
- export default defineCommand({
10
- meta: {
11
- name: 'poll',
12
- description: 'Get a full project coordination snapshot for an agent',
13
- },
14
- args: {
15
- agentId: { type: 'string', description: 'Agent ID for personalized situational awareness' },
16
- markInboxRead: { type: 'boolean', description: 'Mark all inbox messages as read after returning them' },
17
- json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
18
- },
19
- run: async ({ args }) => {
20
- const asJson = !!args.json;
21
-
22
- try {
8
+
9
+ export default defineCommand({
10
+ meta: {
11
+ name: 'poll',
12
+ description: 'Get a full project coordination snapshot for an agent',
13
+ },
14
+ args: {
15
+ agentId: { type: 'string', description: 'Agent ID for personalized situational awareness' },
16
+ markInboxRead: { type: 'boolean', description: 'Mark all inbox messages as read after returning them' },
17
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
18
+ },
19
+ run: async ({ args }) => {
20
+ const asJson = !!args.json;
21
+
22
+ try {
23
23
  const { project, teamStore, identity, reader: activeReader } = await getCliProjectContext();
24
24
  const agentId = resolveCliActorId(args.agentId, identity);
25
25
  let reader = activeReader;
26
-
27
- let watermark = computeWatermark(0, 0, 0);
28
- if (agentId) {
29
- const agent = teamStore.getAgent(agentId);
26
+
27
+ let watermark = computeWatermark(0, 0, 0);
28
+ if (agentId) {
29
+ const agent = teamStore.getAgent(agentId);
30
30
  if (!agent || agent.project_id !== project.id) {
31
31
  emitError(`Unknown agent "${agentId}"`, asJson);
32
32
  return;
@@ -36,45 +36,45 @@ export default defineCommand({
36
36
  agentId,
37
37
  isTeamMember: agent.status === 'active',
38
38
  };
39
-
40
- const lastSeen = agent.last_seen_obs_generation;
41
- const currentGen = getObservationStore().getGeneration();
42
- const projectObs = await withFreshIndex(() =>
39
+
40
+ const lastSeen = agent.last_seen_obs_generation;
41
+ const currentGen = getObservationStore().getGeneration();
42
+ const projectObs = await withFreshIndex(() =>
43
43
  filterReadableObservations(
44
44
  getAllObservations().filter(
45
45
  (obs) => obs.projectId === project.id && (obs.writeGeneration ?? 0) > lastSeen,
46
46
  ),
47
47
  reader,
48
48
  ),
49
- );
50
- watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
51
- teamStore.updateWatermark(agentId, currentGen);
52
- teamStore.heartbeat(agentId);
53
- }
54
-
55
- const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
56
- if (args.markInboxRead && agentId) {
57
- teamStore.markAllRead(project.id, agentId);
58
- }
59
-
60
- const text = [
61
- poll.agent ? `You: ${shortId(poll.agent.agentId)} (${poll.agent.status})` : 'Project-level overview',
62
- poll.watermark.newObservationCount > 0
63
- ? `${poll.watermark.newObservationCount} new observation(s)`
64
- : 'No unseen observations',
65
- poll.inbox.unreadCount > 0 ? `${poll.inbox.unreadCount} unread message(s)` : 'Inbox clear',
66
- poll.tasks.myInProgress.length > 0
67
- ? `${poll.tasks.myInProgress.length} in-progress task(s)`
68
- : 'No in-progress tasks',
69
- poll.tasks.availableToClaim.length > 0
70
- ? `${poll.tasks.availableToClaim.length} task(s) available to claim`
71
- : 'No claimable tasks',
72
- `Team: ${poll.team.activeAgents.length} active / ${poll.team.totalAgents} total`,
73
- ].join('\n');
74
-
75
- emitResult({ project, poll }, text, asJson);
76
- } catch (error) {
77
- emitError(error instanceof Error ? error.message : String(error), asJson);
78
- }
79
- },
80
- });
49
+ );
50
+ watermark = computeWatermark(lastSeen, currentGen, projectObs.length);
51
+ teamStore.updateWatermark(agentId, currentGen);
52
+ teamStore.heartbeat(agentId);
53
+ }
54
+
55
+ const poll = computePoll(teamStore, project.id, agentId ?? null, watermark);
56
+ if (args.markInboxRead && agentId) {
57
+ teamStore.markAllRead(project.id, agentId);
58
+ }
59
+
60
+ const text = [
61
+ poll.agent ? `You: ${shortId(poll.agent.agentId)} (${poll.agent.status})` : 'Project-level overview',
62
+ poll.watermark.newObservationCount > 0
63
+ ? `${poll.watermark.newObservationCount} new observation(s)`
64
+ : 'No unseen observations',
65
+ poll.inbox.unreadCount > 0 ? `${poll.inbox.unreadCount} unread message(s)` : 'Inbox clear',
66
+ poll.tasks.myInProgress.length > 0
67
+ ? `${poll.tasks.myInProgress.length} in-progress task(s)`
68
+ : 'No in-progress tasks',
69
+ poll.tasks.availableToClaim.length > 0
70
+ ? `${poll.tasks.availableToClaim.length} task(s) available to claim`
71
+ : 'No claimable tasks',
72
+ `Team: ${poll.team.activeAgents.length} active / ${poll.team.totalAgents} total`,
73
+ ].join('\n');
74
+
75
+ emitResult({ project, poll }, text, asJson);
76
+ } catch (error) {
77
+ emitError(error instanceof Error ? error.message : String(error), asJson);
78
+ }
79
+ },
80
+ });
@@ -1,6 +1,6 @@
1
- import { defineCommand } from 'citty';
2
- import { compactSearch } from '../../compact/engine.js';
3
- import { storeObservation } from '../../memory/observations.js';
1
+ import { defineCommand } from 'citty';
2
+ import { compactSearch } from '../../compact/engine.js';
3
+ import { storeObservation } from '../../memory/observations.js';
4
4
  import {
5
5
  emitError,
6
6
  emitResult,
@@ -9,65 +9,65 @@ import {
9
9
  parsePositiveInt,
10
10
  resolveCliWriteScope,
11
11
  } from './operator-shared.js';
12
-
13
- export default defineCommand({
14
- meta: {
15
- name: 'reasoning',
16
- description: 'Store and search decision rationale from the operator CLI',
17
- },
18
- args: {
19
- entity: { type: 'string', description: 'Entity this reasoning applies to' },
20
- decision: { type: 'string', description: 'Decision that was made' },
21
- rationale: { type: 'string', description: 'Why this approach was chosen' },
22
- alternatives: { type: 'string', description: 'Comma-separated alternatives that were considered' },
23
- constraints: { type: 'string', description: 'Comma-separated constraints' },
24
- expectedOutcome: { type: 'string', description: 'Expected outcome from this decision' },
25
- risks: { type: 'string', description: 'Comma-separated risks or downsides' },
26
- concepts: { type: 'string', description: 'Comma-separated related concepts' },
27
- files: { type: 'string', description: 'Comma-separated file list' },
28
- relatedCommits: { type: 'string', description: 'Comma-separated related commit hashes' },
12
+
13
+ export default defineCommand({
14
+ meta: {
15
+ name: 'reasoning',
16
+ description: 'Store and search decision rationale from the operator CLI',
17
+ },
18
+ args: {
19
+ entity: { type: 'string', description: 'Entity this reasoning applies to' },
20
+ decision: { type: 'string', description: 'Decision that was made' },
21
+ rationale: { type: 'string', description: 'Why this approach was chosen' },
22
+ alternatives: { type: 'string', description: 'Comma-separated alternatives that were considered' },
23
+ constraints: { type: 'string', description: 'Comma-separated constraints' },
24
+ expectedOutcome: { type: 'string', description: 'Expected outcome from this decision' },
25
+ risks: { type: 'string', description: 'Comma-separated risks or downsides' },
26
+ concepts: { type: 'string', description: 'Comma-separated related concepts' },
27
+ files: { type: 'string', description: 'Comma-separated file list' },
28
+ relatedCommits: { type: 'string', description: 'Comma-separated related commit hashes' },
29
29
  relatedEntities: { type: 'string', description: 'Comma-separated related entity names' },
30
30
  visibility: { type: 'string', description: 'Reasoning visibility: project (default), personal, or team' },
31
- query: { type: 'string', description: 'Search query for reasoning traces' },
32
- limit: { type: 'string', description: 'Search result limit' },
33
- scope: { type: 'string', description: 'project or global scope for search' },
34
- json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
35
- },
36
- run: async ({ args }) => {
37
- const action = (args._ as string[])?.[0] || '';
38
- const asJson = !!args.json;
39
-
40
- try {
31
+ query: { type: 'string', description: 'Search query for reasoning traces' },
32
+ limit: { type: 'string', description: 'Search result limit' },
33
+ scope: { type: 'string', description: 'project or global scope for search' },
34
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
35
+ },
36
+ run: async ({ args }) => {
37
+ const action = (args._ as string[])?.[0] || '';
38
+ const asJson = !!args.json;
39
+
40
+ try {
41
41
  const { project, reader, identity } = await getCliProjectContext({ searchIndex: action === 'search' });
42
-
43
- switch (action) {
44
- case 'store': {
45
- const entityName = (args.entity as string | undefined)?.trim();
46
- const decision = (args.decision as string | undefined)?.trim();
47
- const rationale = (args.rationale as string | undefined)?.trim();
48
- if (!entityName || !decision || !rationale) {
49
- emitError('entity, decision, and rationale are required for "memorix reasoning store"', asJson);
50
- return;
51
- }
52
-
53
- const alternatives = parseCsvList(args.alternatives as string | undefined);
54
- const constraints = parseCsvList(args.constraints as string | undefined);
55
- const risks = parseCsvList(args.risks as string | undefined);
56
- const concepts = parseCsvList(args.concepts as string | undefined);
57
- const filesModified = parseCsvList(args.files as string | undefined);
58
- const relatedCommits = parseCsvList(args.relatedCommits as string | undefined);
59
- const relatedEntities = parseCsvList(args.relatedEntities as string | undefined);
60
-
61
- const narrativeParts: string[] = [rationale];
62
- if (alternatives.length > 0) {
63
- narrativeParts.push(`Alternatives considered: ${alternatives.join('; ')}`);
64
- }
65
- if (constraints.length > 0) {
66
- narrativeParts.push(`Constraints: ${constraints.join('; ')}`);
67
- }
68
- if (args.expectedOutcome) {
69
- narrativeParts.push(`Expected outcome: ${String(args.expectedOutcome).trim()}`);
70
- }
42
+
43
+ switch (action) {
44
+ case 'store': {
45
+ const entityName = (args.entity as string | undefined)?.trim();
46
+ const decision = (args.decision as string | undefined)?.trim();
47
+ const rationale = (args.rationale as string | undefined)?.trim();
48
+ if (!entityName || !decision || !rationale) {
49
+ emitError('entity, decision, and rationale are required for "memorix reasoning store"', asJson);
50
+ return;
51
+ }
52
+
53
+ const alternatives = parseCsvList(args.alternatives as string | undefined);
54
+ const constraints = parseCsvList(args.constraints as string | undefined);
55
+ const risks = parseCsvList(args.risks as string | undefined);
56
+ const concepts = parseCsvList(args.concepts as string | undefined);
57
+ const filesModified = parseCsvList(args.files as string | undefined);
58
+ const relatedCommits = parseCsvList(args.relatedCommits as string | undefined);
59
+ const relatedEntities = parseCsvList(args.relatedEntities as string | undefined);
60
+
61
+ const narrativeParts: string[] = [rationale];
62
+ if (alternatives.length > 0) {
63
+ narrativeParts.push(`Alternatives considered: ${alternatives.join('; ')}`);
64
+ }
65
+ if (constraints.length > 0) {
66
+ narrativeParts.push(`Constraints: ${constraints.join('; ')}`);
67
+ }
68
+ if (args.expectedOutcome) {
69
+ narrativeParts.push(`Expected outcome: ${String(args.expectedOutcome).trim()}`);
70
+ }
71
71
  if (risks.length > 0) {
72
72
  narrativeParts.push(`Risks: ${risks.join('; ')}`);
73
73
  }
@@ -77,70 +77,70 @@ export default defineCommand({
77
77
  );
78
78
 
79
79
  const result = await storeObservation({
80
- entityName,
81
- type: 'reasoning',
82
- title: decision,
83
- narrative: narrativeParts.join('. '),
84
- facts: [
85
- `Decision: ${decision}`,
86
- ...alternatives.map((item) => `Alternative: ${item}`),
87
- ...constraints.map((item) => `Constraint: ${item}`),
88
- ...(args.expectedOutcome ? [`Expected outcome: ${String(args.expectedOutcome).trim()}`] : []),
89
- ...risks.map((item) => `Risk: ${item}`),
90
- ...relatedCommits.map((item) => `Commit: ${item}`),
91
- ...relatedEntities.map((item) => `Related entity: ${item}`),
92
- ],
93
- concepts,
94
- filesModified,
95
- relatedCommits,
96
- relatedEntities,
80
+ entityName,
81
+ type: 'reasoning',
82
+ title: decision,
83
+ narrative: narrativeParts.join('. '),
84
+ facts: [
85
+ `Decision: ${decision}`,
86
+ ...alternatives.map((item) => `Alternative: ${item}`),
87
+ ...constraints.map((item) => `Constraint: ${item}`),
88
+ ...(args.expectedOutcome ? [`Expected outcome: ${String(args.expectedOutcome).trim()}`] : []),
89
+ ...risks.map((item) => `Risk: ${item}`),
90
+ ...relatedCommits.map((item) => `Commit: ${item}`),
91
+ ...relatedEntities.map((item) => `Related entity: ${item}`),
92
+ ],
93
+ concepts,
94
+ filesModified,
95
+ relatedCommits,
96
+ relatedEntities,
97
97
  projectId: project.id,
98
98
  source: 'manual',
99
99
  ...writeScope,
100
- });
101
-
102
- emitResult(
103
- { project, observation: result.observation, upserted: result.upserted },
104
- `${result.upserted ? 'Updated' : 'Stored'} reasoning trace #${result.observation.id}: ${decision}`,
105
- asJson,
106
- );
107
- return;
108
- }
109
-
110
- case 'search': {
111
- const query = (args.query as string | undefined)?.trim();
112
- if (!query) {
113
- emitError('query is required for "memorix reasoning search"', asJson);
114
- return;
115
- }
116
- const scope = (args.scope as string | undefined) === 'global' ? 'global' : 'project';
117
- const limit = parsePositiveInt(args.limit as string | undefined, 10);
118
- const result = await compactSearch({
119
- query,
120
- limit,
100
+ });
101
+
102
+ emitResult(
103
+ { project, observation: result.observation, upserted: result.upserted },
104
+ `${result.upserted ? 'Updated' : 'Stored'} reasoning trace #${result.observation.id}: ${decision}`,
105
+ asJson,
106
+ );
107
+ return;
108
+ }
109
+
110
+ case 'search': {
111
+ const query = (args.query as string | undefined)?.trim();
112
+ if (!query) {
113
+ emitError('query is required for "memorix reasoning search"', asJson);
114
+ return;
115
+ }
116
+ const scope = (args.scope as string | undefined) === 'global' ? 'global' : 'project';
117
+ const limit = parsePositiveInt(args.limit as string | undefined, 10);
118
+ const result = await compactSearch({
119
+ query,
120
+ limit,
121
121
  type: 'reasoning',
122
122
  projectId: scope === 'global' ? undefined : project.id,
123
123
  status: 'active',
124
124
  reader: scope === 'global' ? {} : reader,
125
- });
126
- emitResult(
127
- { project, scope, entries: result.entries },
128
- result.entries.length === 0 ? 'No reasoning traces found.' : `[REASONING] Reasoning traces\n${result.formatted}`,
129
- asJson,
130
- );
131
- return;
132
- }
133
-
134
- default:
135
- console.log('Memorix Reasoning Commands');
136
- console.log('');
137
- console.log('Usage:');
125
+ });
126
+ emitResult(
127
+ { project, scope, entries: result.entries },
128
+ result.entries.length === 0 ? 'No reasoning traces found.' : `[REASONING] Reasoning traces\n${result.formatted}`,
129
+ asJson,
130
+ );
131
+ return;
132
+ }
133
+
134
+ default:
135
+ console.log('Memorix Reasoning Commands');
136
+ console.log('');
137
+ console.log('Usage:');
138
138
  console.log(' memorix reasoning store --entity auth --decision "Use SQLite" --rationale "..." [--visibility project|personal|team]');
139
- console.log(' memorix reasoning search --query "why sqlite" [--scope project|global --limit 10]');
140
- }
141
- } catch (error) {
142
- emitError(error instanceof Error ? error.message : String(error), asJson);
143
- }
144
- },
145
- });
146
-
139
+ console.log(' memorix reasoning search --query "why sqlite" [--scope project|global --limit 10]');
140
+ }
141
+ } catch (error) {
142
+ emitError(error instanceof Error ? error.message : String(error), asJson);
143
+ }
144
+ },
145
+ });
146
+
@@ -29,13 +29,19 @@ import { canManageObservation, filterReadableObservations } from '../../memory/v
29
29
  import { parseTcpPortOrReport } from '../port.js';
30
30
  import { mcpFileUriToPath } from '../mcp-root-path.js';
31
31
 
32
- export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
32
+ /**
33
+ * HTTP MCP is a shared control plane. A normal thinking break should not
34
+ * silently invalidate a client session. Operators can choose a shorter GC
35
+ * window, or set 0 to disable idle GC entirely.
36
+ */
37
+ export const DEFAULT_SESSION_TIMEOUT_MS = 12 * 60 * 60 * 1000;
38
+ export const EXPIRED_SESSION_TTL_MS = 10 * 60 * 1000;
33
39
 
34
40
  export function parseSessionTimeoutMs(raw: string | undefined): number {
35
41
  const value = raw?.trim();
36
42
  if (!value) return DEFAULT_SESSION_TIMEOUT_MS;
37
43
  const parsed = Number(value);
38
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_SESSION_TIMEOUT_MS;
44
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_SESSION_TIMEOUT_MS;
39
45
  return Math.floor(parsed);
40
46
  }
41
47
 
@@ -132,7 +138,11 @@ export default defineCommand({
132
138
  console.error(`[memorix] HTTP transport starting on ${host}:${port}`);
133
139
  console.error(`[memorix] Project root: ${projectRoot}`);
134
140
  const sessionTimeoutMs = parseSessionTimeoutMs(process.env.MEMORIX_SESSION_TIMEOUT_MS);
135
- console.error(`[memorix] HTTP session idle timeout: ${Math.round(sessionTimeoutMs / 60000)}min (${sessionTimeoutMs}ms)`);
141
+ console.error(
142
+ sessionTimeoutMs === 0
143
+ ? '[memorix] HTTP session idle timeout: disabled'
144
+ : `[memorix] HTTP session idle timeout: ${Math.round(sessionTimeoutMs / 60000)}min (${sessionTimeoutMs}ms)`,
145
+ );
136
146
 
137
147
  // Per-project TeamStore cache (Option B) — each dataDir gets its own TeamStore instance.
138
148
  // This ensures true isolation between projects in HTTP control-plane mode.
@@ -166,6 +176,9 @@ export default defineCommand({
166
176
 
167
177
  // Session activity tracking (for GC timeout)
168
178
  const sessionLastActivity = new Map<string, number>();
179
+ // Keep a short-lived tombstone so an expired client gets an actionable
180
+ // reinitialize response instead of a generic bad request.
181
+ const expiredSessions = new Map<string, number>();
169
182
 
170
183
  // Probe session noise reduction: defer init logs, suppress short-lived sessions
171
184
  let suppressedProbeCount = 0;
@@ -182,6 +195,30 @@ export default defineCommand({
182
195
  }
183
196
  }
184
197
 
198
+ function forgetExpiredSessions(now = Date.now()): void {
199
+ for (const [sessionId, expiresAt] of expiredSessions) {
200
+ if (expiresAt <= now) expiredSessions.delete(sessionId);
201
+ }
202
+ }
203
+
204
+ function sendUnknownSession(res: ServerResponse, sessionId: string | undefined, jsonRpc = false): void {
205
+ const expired = Boolean(sessionId && expiredSessions.has(sessionId));
206
+ const message = expired
207
+ ? 'MCP session expired after inactivity. Initialize a new session and retry the request.'
208
+ : 'MCP session not found. Initialize a new session and retry the request.';
209
+ if (jsonRpc) {
210
+ res.writeHead(404, { 'Content-Type': 'application/json' });
211
+ res.end(JSON.stringify({
212
+ jsonrpc: '2.0',
213
+ error: { code: -32001, message },
214
+ id: null,
215
+ }));
216
+ return;
217
+ }
218
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
219
+ res.end(message);
220
+ }
221
+
185
222
  /**
186
223
  * Parse JSON body from IncomingMessage
187
224
  */
@@ -360,6 +397,11 @@ export default defineCommand({
360
397
  return;
361
398
  }
362
399
 
400
+ if (sessionId) {
401
+ sendUnknownSession(res, sessionId, true);
402
+ return;
403
+ }
404
+
363
405
  if (!sessionId && isInitializeRequest(body)) {
364
406
  // New session — create transport + server
365
407
  let createdState: SessionState | null = null;
@@ -488,8 +530,12 @@ export default defineCommand({
488
530
  async function handleGet(req: IncomingMessage, res: ServerResponse) {
489
531
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
490
532
  if (!sessionId || !sessions.has(sessionId)) {
491
- res.writeHead(400, { 'Content-Type': 'text/plain' });
492
- res.end('Invalid or missing session ID');
533
+ if (!sessionId) {
534
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
535
+ res.end('Missing MCP session ID');
536
+ return;
537
+ }
538
+ sendUnknownSession(res, sessionId);
493
539
  return;
494
540
  }
495
541
 
@@ -502,8 +548,12 @@ export default defineCommand({
502
548
  async function handleDelete(req: IncomingMessage, res: ServerResponse) {
503
549
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
504
550
  if (!sessionId || !sessions.has(sessionId)) {
505
- res.writeHead(400, { 'Content-Type': 'text/plain' });
506
- res.end('Invalid or missing session ID');
551
+ if (!sessionId) {
552
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
553
+ res.end('Missing MCP session ID');
554
+ return;
555
+ }
556
+ sendUnknownSession(res, sessionId);
507
557
  return;
508
558
  }
509
559
 
@@ -1414,6 +1464,7 @@ export default defineCommand({
1414
1464
  // Lightweight health check — responds immediately, no heavy init required.
1415
1465
  // This is the readiness signal for background start / status.
1416
1466
  if (url.pathname === '/health') {
1467
+ const { getEmbeddingRuntimeHealth } = await import('../../embedding/provider.js');
1417
1468
  res.writeHead(200, { 'Content-Type': 'application/json' });
1418
1469
  res.end(JSON.stringify({
1419
1470
  status: 'ok',
@@ -1421,6 +1472,7 @@ export default defineCommand({
1421
1472
  port,
1422
1473
  uptime: Math.round(process.uptime()),
1423
1474
  pid: process.pid,
1475
+ embedding: getEmbeddingRuntimeHealth(),
1424
1476
  }));
1425
1477
  return;
1426
1478
  }
@@ -1491,6 +1543,12 @@ export default defineCommand({
1491
1543
  await serveDashStatic(req, res);
1492
1544
  });
1493
1545
 
1546
+ // MCP session continuity is independent from one TCP socket. Keep Node's
1547
+ // socket timeout above its 5s default while the session map remains the
1548
+ // actual continuity boundary.
1549
+ httpServer.keepAliveTimeout = 60_000;
1550
+ httpServer.headersTimeout = 65_000;
1551
+
1494
1552
  httpServer.listen(port, host, () => {
1495
1553
  // Write readiness file — background start polls this for out-of-band readiness detection
1496
1554
  try {
@@ -1521,10 +1579,13 @@ export default defineCommand({
1521
1579
  const gcInterval = setInterval(() => {
1522
1580
  maybeProbeSummary(); // Flush suppressed probe count periodically
1523
1581
  const now = Date.now();
1582
+ forgetExpiredSessions(now);
1583
+ if (SESSION_TIMEOUT_MS === 0) return;
1524
1584
  for (const [sid, state] of sessions) {
1525
1585
  const lastActive = sessionLastActivity.get(sid) ?? 0;
1526
1586
  if (now - lastActive > SESSION_TIMEOUT_MS) {
1527
1587
  console.error(`[memorix] Session ${sid.slice(0, 8)}… timed out (idle ${Math.round((now - lastActive) / 60000)}min), closing`);
1588
+ expiredSessions.set(sid, now + EXPIRED_SESSION_TTL_MS);
1528
1589
  state.transport.close().catch(() => {});
1529
1590
  sessions.delete(sid);
1530
1591
  sessionLastActivity.delete(sid);