Skip to content

Commit b7985a2

Browse files
author
StackMemory Bot (CLI)
committed
chore: handoff checkpoint on main
1 parent 1b7a90c commit b7985a2

5 files changed

Lines changed: 326 additions & 0 deletions

File tree

docs/graphiti-integration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Graphiti Integration (Temporal Knowledge Graph)
2+
3+
This spike adds a minimal Graphiti integration to StackMemory to capture episodic events and enable temporal queries.
4+
5+
What’s included
6+
- Types: `src/integrations/graphiti/types.ts` (Episode, EntityNode, RelationEdge, TemporalQuery)
7+
- Config: `src/integrations/graphiti/config.ts` with `GRAPHITI_ENDPOINT`, `GRAPHITI_BACKEND`
8+
- Client: `src/integrations/graphiti/client.ts` stub for REST-style endpoints
9+
- Hooks: `src/hooks/graphiti-hooks.ts` to emit episodes on `session_start`, `file_change`, `session_end`
10+
- Daemon wiring: `src/hooks/daemon.ts` registers Graphiti hooks if `GRAPHITI_ENDPOINT` is set
11+
12+
Enable
13+
- Set `GRAPHITI_ENDPOINT=http://localhost:8080` (or your deployment) in environment
14+
- Optionally set `GRAPHITI_BACKEND=neo4j|falkordb|kuzu|neptune`
15+
- Start hooks daemon via existing CLI to emit episodes from file watching and lifecycle events
16+
17+
Mapping to Zep/Graphiti model
18+
- Episode subgraph: Hook events are sent as non-lossy `Episode` records (`session_start`, `file_change`, `session_end`).
19+
- Semantic entity subgraph: Use `GraphitiClient.upsertEntities` and `upsertRelations` from scanners/integrations (Stripe, Salesforce, etc.) to create typed entities and bi-temporal edges with validity windows.
20+
- Community subgraph: Handled by Graphiti backend (community detection/summary), later exposed via MCP or CLI.
21+
22+
Next steps
23+
- Wire scanner events: Emit episodes from external system syncs (Stripe, Salesforce, GitHub) and upsert entities/edges per event.
24+
- Context tools: Add MCP handlers for temporal queries (e.g., “changes for Customer X in last 30 days”).
25+
- Hybrid retrieval: Combine graph traversal with FTS/embedding results to construct compact prompt context.
26+
- Tests: Add unit tests for hook-to-episode conversion and client request flows.
27+
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Graphiti API Client (stub)
3+
* Wraps Graphiti REST endpoints or MCP tools with simple methods
4+
*/
5+
6+
import type {
7+
Episode,
8+
EntityNode,
9+
RelationEdge,
10+
TemporalQuery,
11+
GraphContext,
12+
GraphitiStatus,
13+
} from './types.js';
14+
import type { GraphitiIntegrationConfig } from './config.js';
15+
import { DEFAULT_GRAPHITI_CONFIG } from './config.js';
16+
17+
export class GraphitiClientError extends Error {
18+
constructor(
19+
message: string,
20+
public readonly code?: string,
21+
public readonly statusCode?: number
22+
) {
23+
super(message);
24+
this.name = 'GraphitiClientError';
25+
}
26+
}
27+
28+
export class GraphitiClient {
29+
private readonly endpoint: string;
30+
private readonly timeout: number;
31+
private readonly maxRetries: number;
32+
private readonly namespace: string;
33+
34+
constructor(config: Partial<GraphitiIntegrationConfig> = {}) {
35+
const merged = { ...DEFAULT_GRAPHITI_CONFIG, ...config };
36+
this.endpoint = merged.endpoint.replace(/\/$/, '');
37+
this.timeout = merged.timeoutMs;
38+
this.maxRetries = merged.maxRetries;
39+
this.namespace = merged.projectNamespace || 'default';
40+
}
41+
42+
private async request<T>(
43+
path: string,
44+
options: RequestInit = {}
45+
): Promise<T> {
46+
const controller = new AbortController();
47+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
48+
49+
let lastError: Error | undefined;
50+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
51+
try {
52+
const res = await fetch(`${this.endpoint}${path}`, {
53+
...options,
54+
signal: controller.signal,
55+
headers: {
56+
'Content-Type': 'application/json',
57+
...(options.headers || {}),
58+
},
59+
});
60+
clearTimeout(timeoutId);
61+
if (!res.ok) {
62+
const msg = await res.text().catch(() => res.statusText);
63+
throw new GraphitiClientError(
64+
`Request failed: ${msg}`,
65+
'HTTP_ERROR',
66+
res.status
67+
);
68+
}
69+
return (await res.json()) as T;
70+
} catch (err) {
71+
lastError = err as Error;
72+
if (err instanceof GraphitiClientError) throw err;
73+
if ((err as Error).name === 'AbortError') {
74+
throw new GraphitiClientError('Request timeout', 'TIMEOUT');
75+
}
76+
if (attempt < this.maxRetries) {
77+
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 100));
78+
continue;
79+
}
80+
}
81+
}
82+
clearTimeout(timeoutId);
83+
throw new GraphitiClientError(
84+
lastError?.message || 'Network error',
85+
'NETWORK_ERROR'
86+
);
87+
}
88+
89+
// Episodes
90+
async upsertEpisode(episode: Episode): Promise<{ id: string }> {
91+
const payload = { ...episode, namespace: this.namespace };
92+
const res = await this.request<{ id: string }>(`/episodes`, {
93+
method: 'POST',
94+
body: JSON.stringify(payload),
95+
});
96+
return res;
97+
}
98+
99+
// Entities
100+
async upsertEntities(entities: EntityNode[]): Promise<{ ids: string[] }> {
101+
const payload = { entities, namespace: this.namespace };
102+
return this.request<{ ids: string[] }>(`/entities:batchUpsert`, {
103+
method: 'POST',
104+
body: JSON.stringify(payload),
105+
});
106+
}
107+
108+
// Relations
109+
async upsertRelations(edges: RelationEdge[]): Promise<{ ids: string[] }> {
110+
const payload = { edges, namespace: this.namespace };
111+
return this.request<{ ids: string[] }>(`/relations:batchUpsert`, {
112+
method: 'POST',
113+
body: JSON.stringify(payload),
114+
});
115+
}
116+
117+
// Temporal query + hybrid retrieval
118+
async queryTemporal(query: TemporalQuery): Promise<GraphContext> {
119+
const payload = { ...query, namespace: this.namespace };
120+
return this.request<GraphContext>(`/query/temporal`, {
121+
method: 'POST',
122+
body: JSON.stringify(payload),
123+
});
124+
}
125+
126+
// Health/status
127+
async getStatus(): Promise<GraphitiStatus> {
128+
try {
129+
return await this.request<GraphitiStatus>(
130+
`/status?namespace=${encodeURIComponent(this.namespace)}`
131+
);
132+
} catch {
133+
return { connected: false };
134+
}
135+
}
136+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Graphiti Integration Config
3+
*/
4+
5+
import type { GraphitiBackend } from './types.js';
6+
7+
export interface GraphitiIntegrationConfig {
8+
enabled: boolean;
9+
endpoint: string; // Graphiti REST/MCP endpoint (BYO deployment)
10+
backend: GraphitiBackend;
11+
projectNamespace?: string; // logical segregation per StackMemory project
12+
timeoutMs: number;
13+
maxRetries: number;
14+
// Context construction
15+
maxTokens: number;
16+
maxHops: number;
17+
}
18+
19+
export const DEFAULT_GRAPHITI_CONFIG: GraphitiIntegrationConfig = {
20+
enabled: !!process.env.GRAPHITI_ENDPOINT,
21+
endpoint:
22+
process.env.GRAPHITI_ENDPOINT?.replace(/\/$/, '') ||
23+
'http://localhost:8080',
24+
backend: (process.env.GRAPHITI_BACKEND as GraphitiBackend) || 'neo4j',
25+
projectNamespace: process.env.STACKMEMORY_PROJECT_ID || 'default',
26+
timeoutMs: 5000,
27+
maxRetries: 2,
28+
maxTokens: 1600,
29+
maxHops: 2,
30+
};

src/integrations/graphiti/types.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Graphiti Integration Types
3+
* Temporal knowledge graph primitives and query options
4+
*/
5+
6+
export type GraphitiBackend = 'neo4j' | 'falkordb' | 'kuzu' | 'neptune';
7+
8+
// Raw episodic input (non-lossy)
9+
export interface Episode {
10+
id?: string;
11+
type: string; // e.g., 'file_change', 'commit', 'api_event', 'prompt', 'email'
12+
content: string | Record<string, unknown>;
13+
timestamp: number; // event time (T)
14+
transactionTime?: number; // ingestion time (T')
15+
source?: string; // system/source identifier
16+
metadata?: Record<string, unknown>;
17+
}
18+
19+
// Semantic layer
20+
export interface EntityNode {
21+
id?: string;
22+
type: string; // e.g., 'Person', 'Customer', 'Repo', 'Issue', 'File'
23+
name: string;
24+
summary?: string;
25+
embedding?: number[];
26+
properties?: Record<string, unknown>;
27+
}
28+
29+
export interface RelationEdge {
30+
id?: string;
31+
fromId: string;
32+
toId: string;
33+
type: string; // e.g., 'USES', 'WORKS_ON', 'OWNS', 'CHURNING', 'STALE_DEAL'
34+
// Bi-temporal validity
35+
validFrom: number; // t_valid
36+
validTo?: number | null; // t_invalid
37+
createdAt?: number; // t'_created
38+
expiredAt?: number | null; // t'_expired
39+
properties?: Record<string, unknown>;
40+
}
41+
42+
export interface CommunityCluster {
43+
id: string;
44+
label?: string;
45+
summary?: string;
46+
size: number;
47+
}
48+
49+
export interface TemporalQuery {
50+
// Entity or relation search
51+
query?: string; // semantic text query
52+
entityTypes?: string[];
53+
relationTypes?: string[];
54+
// Time window on event timeline
55+
validFrom?: number;
56+
validTo?: number;
57+
// Retrieval modes
58+
maxHops?: number; // graph traversal depth
59+
k?: number; // top-k
60+
rerank?: boolean; // allow reranking
61+
}
62+
63+
export interface GraphContextChunk {
64+
text: string;
65+
citations?: Array<{ episodeId?: string; edgeId?: string; entityId?: string }>;
66+
tokens?: number;
67+
}
68+
69+
export interface GraphContext {
70+
chunks: GraphContextChunk[];
71+
totalTokens: number;
72+
}
73+
74+
export interface GraphitiStatus {
75+
connected: boolean;
76+
backend?: GraphitiBackend;
77+
nodes?: number;
78+
edges?: number;
79+
communities?: number;
80+
version?: string;
81+
}

tomorrow.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Tomorrow — 2026-02-12
2+
3+
## Publish v1.0.1
4+
5+
npm token expired. Re-auth and publish:
6+
7+
```bash
8+
npm login --scope=@stackmemoryai
9+
npm publish --access public
10+
```
11+
12+
v1.0.1 is committed, pushed, and pre-publish verified (652 tests, lint clean, build OK).
13+
14+
## Graphiti Integration
15+
16+
Untracked files on main — work in progress:
17+
18+
- `src/integrations/graphiti/` — client, types, config
19+
- `src/hooks/graphiti-hooks.ts` — session/file change episode hooks (lint-fixed)
20+
- `docs/graphiti-integration.md` — integration spec
21+
22+
Next steps:
23+
- Wire graphiti client into MCP tools or expose as new tool
24+
- Add tests for graphiti hooks
25+
- Decide: optional dependency or always-on?
26+
27+
## Remaining Doc Cleanup
28+
29+
Lower priority items not addressed in the 1.0 docs refresh:
30+
31+
- `docs/archives/` has 10+ old reports (security, cleanup, migration) — audit for relevance
32+
- `docs/STORAGE_COMPARISON.md` — may be stale (references Redis/S3 tiers)
33+
- `docs/FEATURES.md` — check against actual feature set
34+
- `docs/AGENTIC_PATTERNS_IMPLEMENTATION.md` — check if current
35+
- `docs/testing-agent.md` — check if current
36+
- `docs/session-persistence-design.md` — check if current
37+
- `docs/query-language.md` — check if current
38+
- `vision.md` — confirmed current, keep as-is
39+
40+
## Codex Linear Sync — Verify
41+
42+
The fix is deployed (gated on `LINEAR_API_KEY`, 10s timeout, non-fatal). After publishing v1.0.1:
43+
44+
1. `npm install -g @stackmemoryai/stackmemory@1.0.1`
45+
2. Run `codex-sm` with `LINEAR_API_KEY` set
46+
3. Exit codex and verify Linear sync fires
47+
48+
## Ideas
49+
50+
- Shared `onSessionExit()` utility to deduplicate exit logic across claude-sm/codex-sm/pty-wrapper
51+
- `session_end` hook event should trigger Linear sync via hook system (not just inline execSync)
52+
- Consider adding `CHANGELOG.md` back with proper v0.6-v1.0.1 entries (the old one was deleted because it stopped at v0.5.51)

0 commit comments

Comments
 (0)