fluffy-context 0.6.0 → 0.7.6
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/README.md +494 -115
- package/dist/src/agent/api.d.ts +2 -0
- package/dist/src/agent/api.js +5 -0
- package/dist/src/agent/index.d.ts +3 -0
- package/dist/src/agent/index.js +2 -0
- package/dist/src/cli/main.js +125 -6
- package/dist/src/cognition/filesystem-observer.d.ts +41 -0
- package/dist/src/cognition/filesystem-observer.js +164 -0
- package/dist/src/cognition/git-observer.d.ts +1 -0
- package/dist/src/cognition/git-observer.js +3 -2
- package/dist/src/cognition/journal.d.ts +4 -1
- package/dist/src/cognition/journal.js +100 -4
- package/dist/src/cognition/migration-v1.js +1 -1
- package/dist/src/cognition/orient.js +4 -3
- package/dist/src/cognition/policy.d.ts +4 -0
- package/dist/src/cognition/policy.js +49 -0
- package/dist/src/cognition/projections.js +15 -1
- package/dist/src/cognition/runtime.d.ts +20 -1
- package/dist/src/cognition/runtime.js +229 -53
- package/dist/src/cognition/types.d.ts +16 -0
- package/dist/src/cognition/v1-bridge.js +3 -2
- package/dist/src/compiler/activity.d.ts +3 -0
- package/dist/src/compiler/activity.js +79 -0
- package/dist/src/compiler/compile.d.ts +2 -0
- package/dist/src/compiler/compile.js +353 -0
- package/dist/src/compiler/expand.d.ts +2 -0
- package/dist/src/compiler/expand.js +187 -0
- package/dist/src/compiler/identity.d.ts +6 -0
- package/dist/src/compiler/identity.js +36 -0
- package/dist/src/compiler/sources.d.ts +19 -0
- package/dist/src/compiler/sources.js +126 -0
- package/dist/src/compiler/types.d.ts +233 -0
- package/dist/src/compiler/types.js +1 -0
- package/dist/src/hooks/claude-code.js +7 -15
- package/dist/src/mcp/main.js +2 -0
- package/dist/src/mcp/server.js +167 -11
- package/dist/src/runtime/runtime.js +10 -0
- package/dist/src/runtime/types.d.ts +1 -1
- package/dist/src/storage/layout.d.ts +1 -0
- package/dist/src/storage/layout.js +3 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/fluffy-context/SKILL.md +10 -3
|
@@ -105,7 +105,7 @@ async function sourceRecords(projectRoot) {
|
|
|
105
105
|
async function writeV2Metadata(projectRoot) {
|
|
106
106
|
const v1 = await loadManifest(projectRoot);
|
|
107
107
|
const manifest = { schemaVersion: 2, layoutVersion: COGNITION_LAYOUT_VERSION, projectId: v1.projectId, createdAt: new Date().toISOString(), policyVersion: 1 };
|
|
108
|
-
const policy = { schemaVersion: 2, enabled:
|
|
108
|
+
const policy = { schemaVersion: 2, enabled: true, autoStart: true, allowHookStartup: true, inactivityTimeoutMs: 300_000, promptIntentMaxChars: 500, filesystemEventsEnabled: false, retentionMaxBytes: 64 * 1024 * 1024 };
|
|
109
109
|
await Promise.all([atomicWriteJson(cognitionManifestPath(projectRoot), manifest), atomicWriteJson(cognitionPolicyPath(projectRoot), policy)]);
|
|
110
110
|
}
|
|
111
111
|
async function quarantineReadiness(projectRoot, readiness) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
3
|
-
import { cognitionManifestPath
|
|
3
|
+
import { cognitionManifestPath } from '../storage/layout.js';
|
|
4
4
|
import { readJson } from '../storage/json-store.js';
|
|
5
5
|
import { loadManifest } from '../runtime/init.js';
|
|
6
6
|
import { readJournal } from './journal.js';
|
|
7
7
|
import { projectEvents, workspaceKey } from './projections.js';
|
|
8
8
|
import { buildRetrievalProjection } from './retrieval.js';
|
|
9
|
+
import { loadRuntimePolicy } from './policy.js';
|
|
9
10
|
import { v1Synced } from './v1-bridge.js';
|
|
10
11
|
function contextReference(value) {
|
|
11
12
|
const boundary = value.lastIndexOf(':');
|
|
@@ -48,7 +49,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
48
49
|
const [manifest, cognition, policy] = await Promise.all([
|
|
49
50
|
loadManifest(projectRoot),
|
|
50
51
|
readJson(cognitionManifestPath(projectRoot)),
|
|
51
|
-
|
|
52
|
+
loadRuntimePolicy(projectRoot),
|
|
52
53
|
]);
|
|
53
54
|
if (cognition.schemaVersion !== 2 || cognition.projectId !== manifest.projectId || !policy.enabled || !await v1Synced(projectRoot))
|
|
54
55
|
return null;
|
|
@@ -86,7 +87,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
|
|
|
86
87
|
export async function v2Enabled(projectRoot) {
|
|
87
88
|
try {
|
|
88
89
|
await access(cognitionManifestPath(projectRoot));
|
|
89
|
-
return (await
|
|
90
|
+
return (await loadRuntimePolicy(projectRoot)).enabled;
|
|
90
91
|
}
|
|
91
92
|
catch {
|
|
92
93
|
return false;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CognitionRuntimePolicy } from './types.js';
|
|
2
|
+
export declare function normalizeRuntimePolicy(value: Partial<CognitionRuntimePolicy>): CognitionRuntimePolicy;
|
|
3
|
+
export declare function loadRuntimePolicy(projectRoot: string): Promise<CognitionRuntimePolicy>;
|
|
4
|
+
export declare function writeRuntimePolicy(projectRoot: string, policy: CognitionRuntimePolicy): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
2
|
+
import { cognitionPolicyPath } from '../storage/layout.js';
|
|
3
|
+
import { readJson } from '../storage/json-store.js';
|
|
4
|
+
const defaults = {
|
|
5
|
+
schemaVersion: 2,
|
|
6
|
+
enabled: true,
|
|
7
|
+
autoStart: true,
|
|
8
|
+
allowHookStartup: true,
|
|
9
|
+
inactivityTimeoutMs: 300_000,
|
|
10
|
+
promptIntentMaxChars: 500,
|
|
11
|
+
filesystemEventsEnabled: false,
|
|
12
|
+
retentionMaxBytes: 64 * 1024 * 1024,
|
|
13
|
+
};
|
|
14
|
+
function booleanValue(value, fallback, field) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return fallback;
|
|
17
|
+
if (typeof value !== 'boolean')
|
|
18
|
+
throw new Error(`runtime policy field ${field} must be boolean`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function nonNegativeInteger(value, fallback, field) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return fallback;
|
|
24
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
25
|
+
throw new Error(`runtime policy field ${field} must be a non-negative integer`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
export function normalizeRuntimePolicy(value) {
|
|
29
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
throw new Error('runtime policy must be an object');
|
|
31
|
+
if (value.schemaVersion !== undefined && value.schemaVersion !== 2)
|
|
32
|
+
throw new Error('unsupported runtime policy schema');
|
|
33
|
+
return {
|
|
34
|
+
...defaults,
|
|
35
|
+
enabled: booleanValue(value.enabled, defaults.enabled, 'enabled'),
|
|
36
|
+
autoStart: booleanValue(value.autoStart, defaults.autoStart, 'autoStart'),
|
|
37
|
+
allowHookStartup: booleanValue(value.allowHookStartup, defaults.allowHookStartup, 'allowHookStartup'),
|
|
38
|
+
inactivityTimeoutMs: nonNegativeInteger(value.inactivityTimeoutMs, defaults.inactivityTimeoutMs, 'inactivityTimeoutMs'),
|
|
39
|
+
promptIntentMaxChars: nonNegativeInteger(value.promptIntentMaxChars, defaults.promptIntentMaxChars, 'promptIntentMaxChars'),
|
|
40
|
+
filesystemEventsEnabled: booleanValue(value.filesystemEventsEnabled, defaults.filesystemEventsEnabled, 'filesystemEventsEnabled'),
|
|
41
|
+
retentionMaxBytes: nonNegativeInteger(value.retentionMaxBytes, defaults.retentionMaxBytes, 'retentionMaxBytes'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function loadRuntimePolicy(projectRoot) {
|
|
45
|
+
return normalizeRuntimePolicy(await readJson(cognitionPolicyPath(projectRoot)));
|
|
46
|
+
}
|
|
47
|
+
export async function writeRuntimePolicy(projectRoot, policy) {
|
|
48
|
+
await atomicWriteJson(cognitionPolicyPath(projectRoot), normalizeRuntimePolicy(policy));
|
|
49
|
+
}
|
|
@@ -37,6 +37,17 @@ function recordId(record, kind) {
|
|
|
37
37
|
const field = kind === 'note' ? 'noteId' : kind === 'knowledge' ? 'knowledgeId' : 'deadendId';
|
|
38
38
|
return typeof record[field] === 'string' ? record[field] : null;
|
|
39
39
|
}
|
|
40
|
+
function pathFocusFrom(event) {
|
|
41
|
+
const payload = event.payload;
|
|
42
|
+
if (payload.schemaVersion !== 1 || !Array.isArray(payload.paths))
|
|
43
|
+
return [];
|
|
44
|
+
return [...new Set(payload.paths
|
|
45
|
+
.filter((value) => typeof value === 'string')
|
|
46
|
+
.map((value) => value.replaceAll('\\', '/'))
|
|
47
|
+
.filter((value) => value.length > 0 && !value.startsWith('/') && !value.includes('\0') && !value.split('/').includes('..')))]
|
|
48
|
+
.sort()
|
|
49
|
+
.slice(-100);
|
|
50
|
+
}
|
|
40
51
|
function latestOpenObservationIds(events, notes) {
|
|
41
52
|
const latest = new Map();
|
|
42
53
|
for (const event of events) {
|
|
@@ -70,7 +81,7 @@ export function projectEvents(events) {
|
|
|
70
81
|
workspace.activeContextRef = contextRef;
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
|
-
if (
|
|
84
|
+
if (['note.recorded', 'note.resolved', 'note.absorbed'].includes(event.type) && record) {
|
|
74
85
|
const noteId = recordId(record, 'note');
|
|
75
86
|
if (noteId)
|
|
76
87
|
projection.notes[noteId] = payload;
|
|
@@ -88,6 +99,9 @@ export function projectEvents(events) {
|
|
|
88
99
|
if (event.type === 'note.recorded' && openObservationIds.has(event.eventId)) {
|
|
89
100
|
workspace.openObservationEventIds = [...workspace.openObservationEventIds, event.eventId].slice(-50);
|
|
90
101
|
}
|
|
102
|
+
if (event.type === 'workspace.paths.changed') {
|
|
103
|
+
workspace.pathFocus = [...new Set([...workspace.pathFocus, ...pathFocusFrom(event)])].sort().slice(-100);
|
|
104
|
+
}
|
|
91
105
|
projection.workspaces[key] = workspace;
|
|
92
106
|
}
|
|
93
107
|
return projection;
|
|
@@ -3,10 +3,29 @@ export interface RuntimeServer {
|
|
|
3
3
|
lease: RuntimeLease;
|
|
4
4
|
close(reason?: string): Promise<void>;
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
interface RuntimeServeOptions {
|
|
7
|
+
inactivityTimeoutMs?: number;
|
|
8
|
+
heartbeatIntervalMs?: number;
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface EnsureRuntimeOptions {
|
|
12
|
+
source?: 'claude-hook' | 'mcp' | 'cli';
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface EnsureRuntimeResult {
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
started: boolean;
|
|
18
|
+
running: boolean;
|
|
19
|
+
skipped: 'disabled' | 'not-migrated' | 'already-running' | 'startup-in-progress' | 'startup-failed' | 'unavailable' | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function serveRuntime(startPath?: string, options?: RuntimeServeOptions): Promise<RuntimeServer>;
|
|
7
22
|
export declare function runtimeStatus(startPath?: string): Promise<RuntimeStatus>;
|
|
8
23
|
export declare function enableRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
9
24
|
export declare function disableRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
25
|
+
export declare function setFilesystemEvents(startPath: string | undefined, enabled: boolean): Promise<RuntimeStatus>;
|
|
10
26
|
export declare function startRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
27
|
+
export declare function ensureRuntimeForSession(startPath?: string, options?: EnsureRuntimeOptions): Promise<EnsureRuntimeResult>;
|
|
11
28
|
export declare function stopRuntime(startPath?: string): Promise<RuntimeStatus>;
|
|
12
29
|
export declare function observeRuntime(startPath?: string): Promise<boolean>;
|
|
30
|
+
export declare function touchRuntime(startPath?: string): Promise<boolean>;
|
|
31
|
+
export {};
|
|
@@ -9,8 +9,11 @@ import { loadManifest } from '../runtime/init.js';
|
|
|
9
9
|
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
10
10
|
import { readJson } from '../storage/json-store.js';
|
|
11
11
|
import { withLock } from '../storage/lock.js';
|
|
12
|
-
import { cognitionManifestPath,
|
|
12
|
+
import { cognitionManifestPath, cognitionRuntimeLeasePath, cognitionRuntimeSecretPath, cognitionRuntimeStartupPath, locksDirectory } from '../storage/layout.js';
|
|
13
13
|
import { appendEvent } from './journal.js';
|
|
14
|
+
import { rebuildProjections } from './projections.js';
|
|
15
|
+
import { loadRuntimePolicy, writeRuntimePolicy } from './policy.js';
|
|
16
|
+
import { FilesystemObserver } from './filesystem-observer.js';
|
|
14
17
|
import { GitObserver } from './git-observer.js';
|
|
15
18
|
import { reconcileV1State } from './migration-v1.js';
|
|
16
19
|
import { v1Synced } from './v1-bridge.js';
|
|
@@ -19,6 +22,8 @@ const protocolVersion = 1;
|
|
|
19
22
|
const maxFrameBytes = 16 * 1024;
|
|
20
23
|
const pollIntervalMs = 2_000;
|
|
21
24
|
const cliPath = fileURLToPath(new URL('../cli/main.js', import.meta.url));
|
|
25
|
+
const startupPromises = new Map();
|
|
26
|
+
const startupMarkerTimeoutMs = 10_000;
|
|
22
27
|
function createInstanceId() {
|
|
23
28
|
return crypto.randomBytes(12).toString('hex');
|
|
24
29
|
}
|
|
@@ -44,11 +49,21 @@ async function exists(filePath) {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
async function loadPolicy(projectRoot) {
|
|
47
|
-
return
|
|
52
|
+
return loadRuntimePolicy(projectRoot);
|
|
48
53
|
}
|
|
49
54
|
async function loadLease(projectRoot) {
|
|
50
55
|
return readJson(cognitionRuntimeLeasePath(projectRoot)).catch(() => null);
|
|
51
56
|
}
|
|
57
|
+
async function loadStartupMarker(projectRoot) {
|
|
58
|
+
return readJson(cognitionRuntimeStartupPath(projectRoot)).catch(() => null);
|
|
59
|
+
}
|
|
60
|
+
async function removeOwnedRuntimeFiles(projectRoot, instanceId) {
|
|
61
|
+
const current = await loadLease(projectRoot);
|
|
62
|
+
if (current?.instanceId !== instanceId)
|
|
63
|
+
return;
|
|
64
|
+
await rm(cognitionRuntimeLeasePath(projectRoot), { force: true });
|
|
65
|
+
await rm(cognitionRuntimeSecretPath(projectRoot), { force: true });
|
|
66
|
+
}
|
|
52
67
|
async function request(projectRoot, lease, method) {
|
|
53
68
|
let secret;
|
|
54
69
|
try {
|
|
@@ -92,23 +107,52 @@ async function liveLease(projectRoot, lease) {
|
|
|
92
107
|
async function writeLease(projectRoot, lease) {
|
|
93
108
|
await atomicWriteJson(cognitionRuntimeLeasePath(projectRoot), lease);
|
|
94
109
|
}
|
|
95
|
-
async function closeServer(projectRoot, server, observer, heartbeat, poller, inactivity, lease, reason) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
async function closeServer(projectRoot, server, observer, filesystem, heartbeat, poller, inactivity, background, leaseWrite, lease, reason) {
|
|
111
|
+
if (heartbeat)
|
|
112
|
+
clearInterval(heartbeat);
|
|
113
|
+
if (poller)
|
|
114
|
+
clearInterval(poller);
|
|
115
|
+
if (inactivity)
|
|
116
|
+
clearTimeout(inactivity);
|
|
117
|
+
let failure;
|
|
118
|
+
try {
|
|
119
|
+
filesystem.stop();
|
|
120
|
+
await filesystem.flush();
|
|
121
|
+
await observer.drain();
|
|
122
|
+
await filesystem.drain();
|
|
123
|
+
await background;
|
|
124
|
+
await rebuildProjections(projectRoot);
|
|
125
|
+
await leaseWrite;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failure = error;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await new Promise((resolve) => {
|
|
132
|
+
if (!server.listening) {
|
|
133
|
+
resolve();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
server.close(() => resolve());
|
|
137
|
+
});
|
|
138
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
139
|
+
await removeOwnedRuntimeFiles(projectRoot, lease.instanceId);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await appendEvent(projectRoot, {
|
|
143
|
+
type: 'runtime.stopped',
|
|
144
|
+
source: { kind: 'cli', instanceId: lease.instanceId },
|
|
145
|
+
idempotencyKey: `runtime:stopped:${lease.instanceId}`,
|
|
146
|
+
payload: { reason },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
failure ??= error;
|
|
151
|
+
}
|
|
152
|
+
if (failure)
|
|
153
|
+
throw failure;
|
|
110
154
|
}
|
|
111
|
-
function attachProtocol(
|
|
155
|
+
function attachProtocol(server, secret, lease, touch, observe, close) {
|
|
112
156
|
server.on('connection', (socket) => {
|
|
113
157
|
socket.setEncoding('utf8');
|
|
114
158
|
let buffer = '';
|
|
@@ -142,7 +186,7 @@ function attachProtocol(projectRoot, server, observer, secret, lease, touch, clo
|
|
|
142
186
|
}
|
|
143
187
|
if (request.method === 'observe') {
|
|
144
188
|
try {
|
|
145
|
-
const result = await
|
|
189
|
+
const result = await observe();
|
|
146
190
|
socket.end(`${JSON.stringify({ ok: true, result: { changed: result.changed } })}\n`);
|
|
147
191
|
}
|
|
148
192
|
catch {
|
|
@@ -159,11 +203,14 @@ function attachProtocol(projectRoot, server, observer, secret, lease, touch, clo
|
|
|
159
203
|
});
|
|
160
204
|
});
|
|
161
205
|
}
|
|
162
|
-
export async function serveRuntime(startPath) {
|
|
206
|
+
export async function serveRuntime(startPath, options = {}) {
|
|
163
207
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
164
208
|
const currentPolicy = await loadPolicy(projectRoot);
|
|
165
209
|
if (!currentPolicy.enabled)
|
|
166
210
|
throw new Error('runtime is disabled; run ctx runtime enable first');
|
|
211
|
+
const inactivityTimeoutMs = options.inactivityTimeoutMs ?? currentPolicy.inactivityTimeoutMs;
|
|
212
|
+
const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 5_000;
|
|
213
|
+
const runtimePollIntervalMs = options.pollIntervalMs ?? pollIntervalMs;
|
|
167
214
|
const existing = await liveLease(projectRoot);
|
|
168
215
|
if (existing)
|
|
169
216
|
throw new Error('runtime is already running');
|
|
@@ -182,21 +229,55 @@ export async function serveRuntime(startPath) {
|
|
|
182
229
|
const lease = { schemaVersion: 2, protocolVersion, projectId: manifest.projectId, instanceId: createInstanceId(), pid: process.pid, host, port: address.port, startedAt: now, heartbeatAt: now };
|
|
183
230
|
const secret = createToken();
|
|
184
231
|
const observer = new GitObserver(projectRoot);
|
|
185
|
-
let
|
|
186
|
-
let heartbeat;
|
|
187
|
-
let poller;
|
|
188
|
-
let inactivity;
|
|
232
|
+
let filesystem;
|
|
233
|
+
let heartbeat = null;
|
|
234
|
+
let poller = null;
|
|
235
|
+
let inactivity = null;
|
|
236
|
+
let background = Promise.resolve();
|
|
237
|
+
let leaseWrite = Promise.resolve();
|
|
238
|
+
let filesystemEventPending = false;
|
|
239
|
+
let closing = null;
|
|
240
|
+
const enqueueObservation = (operation) => {
|
|
241
|
+
const next = background.then(operation, operation);
|
|
242
|
+
background = next.then(() => undefined, () => undefined);
|
|
243
|
+
return next;
|
|
244
|
+
};
|
|
189
245
|
const close = async (reason) => {
|
|
190
|
-
if (
|
|
191
|
-
return;
|
|
192
|
-
|
|
193
|
-
|
|
246
|
+
if (closing)
|
|
247
|
+
return closing;
|
|
248
|
+
closing = closeServer(projectRoot, server, observer, filesystem, heartbeat, poller, inactivity, background, leaseWrite, lease, reason);
|
|
249
|
+
return closing;
|
|
194
250
|
};
|
|
195
251
|
const touch = () => {
|
|
196
|
-
|
|
197
|
-
|
|
252
|
+
if (inactivity)
|
|
253
|
+
clearTimeout(inactivity);
|
|
254
|
+
inactivity = setTimeout(() => { void close('inactivity').catch(() => undefined); }, inactivityTimeoutMs);
|
|
198
255
|
};
|
|
199
|
-
|
|
256
|
+
const observe = () => enqueueObservation(async () => {
|
|
257
|
+
const [gitResult, filesystemResult] = await Promise.all([
|
|
258
|
+
observer.observeNow(),
|
|
259
|
+
filesystem.observeNow(),
|
|
260
|
+
]);
|
|
261
|
+
const synced = await v1Synced(projectRoot);
|
|
262
|
+
if (!synced)
|
|
263
|
+
await reconcileV1State(projectRoot);
|
|
264
|
+
if (gitResult.changed || filesystemResult.changed || filesystemEventPending || !synced) {
|
|
265
|
+
await rebuildProjections(projectRoot);
|
|
266
|
+
filesystemEventPending = false;
|
|
267
|
+
}
|
|
268
|
+
const changed = gitResult.changed || filesystemResult.changed || filesystemEventPending || !synced;
|
|
269
|
+
if (changed)
|
|
270
|
+
touch();
|
|
271
|
+
return { changed };
|
|
272
|
+
});
|
|
273
|
+
filesystem = new FilesystemObserver(projectRoot, {
|
|
274
|
+
anchor: () => ({ worktreeId: observer.current?.worktreeId ?? null, branch: observer.current?.branch ?? null, ref: observer.current?.ref ?? null, head: observer.current?.commit ?? null }),
|
|
275
|
+
onChanged: () => {
|
|
276
|
+
filesystemEventPending = true;
|
|
277
|
+
touch();
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
attachProtocol(server, secret, lease, touch, observe, close);
|
|
200
281
|
try {
|
|
201
282
|
await withLock(`${locksDirectory(projectRoot)}/cognition-runtime-start.lock`, async () => {
|
|
202
283
|
const live = await liveLease(projectRoot);
|
|
@@ -213,16 +294,18 @@ export async function serveRuntime(startPath) {
|
|
|
213
294
|
}
|
|
214
295
|
heartbeat = setInterval(() => {
|
|
215
296
|
lease.heartbeatAt = new Date().toISOString();
|
|
216
|
-
|
|
217
|
-
},
|
|
297
|
+
leaseWrite = leaseWrite.then(() => writeLease(projectRoot, lease)).catch(() => undefined);
|
|
298
|
+
}, heartbeatIntervalMs);
|
|
218
299
|
poller = setInterval(() => {
|
|
219
|
-
void
|
|
220
|
-
|
|
221
|
-
return reconcileV1State(projectRoot); });
|
|
222
|
-
}, pollIntervalMs);
|
|
223
|
-
touch();
|
|
300
|
+
void observe().catch(() => undefined);
|
|
301
|
+
}, runtimePollIntervalMs);
|
|
224
302
|
await appendEvent(projectRoot, { type: 'runtime.started', source: { kind: 'cli', instanceId: lease.instanceId }, idempotencyKey: `runtime:started:${lease.instanceId}`, payload: {} });
|
|
225
|
-
|
|
303
|
+
if (currentPolicy.filesystemEventsEnabled)
|
|
304
|
+
filesystem.start();
|
|
305
|
+
await observe();
|
|
306
|
+
await writeLease(projectRoot, lease);
|
|
307
|
+
await rm(cognitionRuntimeStartupPath(projectRoot), { force: true });
|
|
308
|
+
touch();
|
|
226
309
|
return { lease, close };
|
|
227
310
|
}
|
|
228
311
|
export async function runtimeStatus(startPath) {
|
|
@@ -240,8 +323,8 @@ export async function enableRuntime(startPath) {
|
|
|
240
323
|
if (!await exists(cognitionManifestPath(projectRoot)))
|
|
241
324
|
throw new Error('v2 migration is required before enabling the runtime');
|
|
242
325
|
const current = await loadPolicy(projectRoot);
|
|
243
|
-
if (!current.enabled) {
|
|
244
|
-
await
|
|
326
|
+
if (!current.enabled || !current.autoStart) {
|
|
327
|
+
await writeRuntimePolicy(projectRoot, { ...current, enabled: true, autoStart: true });
|
|
245
328
|
await appendEvent(projectRoot, { type: 'runtime.enabled', source: { kind: 'cli' }, idempotencyKey: 'runtime:enabled', payload: {} });
|
|
246
329
|
}
|
|
247
330
|
return runtimeStatus(projectRoot);
|
|
@@ -252,25 +335,106 @@ export async function disableRuntime(startPath) {
|
|
|
252
335
|
throw new Error('v2 migration is required before disabling the runtime');
|
|
253
336
|
await stopRuntime(projectRoot);
|
|
254
337
|
const current = await loadPolicy(projectRoot);
|
|
255
|
-
await
|
|
338
|
+
await writeRuntimePolicy(projectRoot, { ...current, enabled: false, autoStart: false });
|
|
256
339
|
return runtimeStatus(projectRoot);
|
|
257
340
|
}
|
|
341
|
+
export async function setFilesystemEvents(startPath, enabled) {
|
|
342
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
343
|
+
if (!await exists(cognitionManifestPath(projectRoot)))
|
|
344
|
+
throw new Error('v2 migration is required before configuring filesystem events');
|
|
345
|
+
const wasRunning = (await runtimeStatus(projectRoot)).running;
|
|
346
|
+
if (wasRunning)
|
|
347
|
+
await stopRuntime(projectRoot);
|
|
348
|
+
const current = await loadPolicy(projectRoot);
|
|
349
|
+
await writeRuntimePolicy(projectRoot, { ...current, filesystemEventsEnabled: enabled });
|
|
350
|
+
if (wasRunning)
|
|
351
|
+
return startRuntime(projectRoot);
|
|
352
|
+
return runtimeStatus(projectRoot);
|
|
353
|
+
}
|
|
354
|
+
async function waitForRuntime(projectRoot, timeoutMs) {
|
|
355
|
+
const deadline = Date.now() + timeoutMs;
|
|
356
|
+
while (Date.now() < deadline) {
|
|
357
|
+
const status = await runtimeStatus(projectRoot);
|
|
358
|
+
if (status.running && !await loadStartupMarker(projectRoot))
|
|
359
|
+
return status;
|
|
360
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
async function startRuntimeCoordinated(projectRoot, timeoutMs) {
|
|
365
|
+
const existing = await liveLease(projectRoot);
|
|
366
|
+
if (existing)
|
|
367
|
+
return runtimeStatus(projectRoot);
|
|
368
|
+
const attemptId = createInstanceId();
|
|
369
|
+
let owner = false;
|
|
370
|
+
try {
|
|
371
|
+
await withLock(`${locksDirectory(projectRoot)}/cognition-runtime-start.lock`, async () => {
|
|
372
|
+
const live = await liveLease(projectRoot);
|
|
373
|
+
if (live)
|
|
374
|
+
return;
|
|
375
|
+
const marker = await loadStartupMarker(projectRoot);
|
|
376
|
+
if (marker && Date.now() - Date.parse(marker.createdAt) <= startupMarkerTimeoutMs)
|
|
377
|
+
return;
|
|
378
|
+
await atomicWriteJson(cognitionRuntimeStartupPath(projectRoot), { attemptId, pid: process.pid, createdAt: new Date().toISOString() });
|
|
379
|
+
owner = true;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Another process owns the startup lock; it will publish the lease or the marker will expire.
|
|
384
|
+
}
|
|
385
|
+
if (owner) {
|
|
386
|
+
const child = spawn(process.execPath, [cliPath, 'runtime', 'serve', '--path', projectRoot], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
387
|
+
child.unref();
|
|
388
|
+
}
|
|
389
|
+
const status = await waitForRuntime(projectRoot, timeoutMs);
|
|
390
|
+
const marker = await loadStartupMarker(projectRoot);
|
|
391
|
+
if (marker?.attemptId === attemptId)
|
|
392
|
+
await rm(cognitionRuntimeStartupPath(projectRoot), { force: true });
|
|
393
|
+
if (status)
|
|
394
|
+
return status;
|
|
395
|
+
throw new Error('runtime failed to start');
|
|
396
|
+
}
|
|
258
397
|
export async function startRuntime(startPath) {
|
|
259
398
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
260
399
|
const current = await loadPolicy(projectRoot);
|
|
261
400
|
if (!current.enabled)
|
|
262
401
|
throw new Error('runtime is disabled; run ctx runtime enable first');
|
|
402
|
+
const pending = startupPromises.get(projectRoot);
|
|
403
|
+
if (pending)
|
|
404
|
+
return pending;
|
|
405
|
+
const operation = startRuntimeCoordinated(projectRoot, 1_500).finally(() => startupPromises.delete(projectRoot));
|
|
406
|
+
startupPromises.set(projectRoot, operation);
|
|
407
|
+
return operation;
|
|
408
|
+
}
|
|
409
|
+
export async function ensureRuntimeForSession(startPath, options = {}) {
|
|
410
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
411
|
+
if (!await exists(cognitionManifestPath(projectRoot)))
|
|
412
|
+
return { projectRoot, started: false, running: false, skipped: 'not-migrated' };
|
|
413
|
+
const current = await loadPolicy(projectRoot);
|
|
414
|
+
if (!current.enabled || !current.autoStart || (options.source === 'claude-hook' && !current.allowHookStartup))
|
|
415
|
+
return { projectRoot, started: false, running: false, skipped: 'disabled' };
|
|
263
416
|
if (await liveLease(projectRoot))
|
|
264
|
-
return
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
417
|
+
return { projectRoot, started: false, running: true, skipped: 'already-running' };
|
|
418
|
+
const pending = startupPromises.get(projectRoot);
|
|
419
|
+
if (pending) {
|
|
420
|
+
try {
|
|
421
|
+
const status = await pending;
|
|
422
|
+
return { projectRoot, started: false, running: status.running, skipped: 'startup-in-progress' };
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return { projectRoot, started: false, running: false, skipped: 'unavailable' };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const operation = startRuntimeCoordinated(projectRoot, options.timeoutMs ?? 1_000)
|
|
429
|
+
.finally(() => startupPromises.delete(projectRoot));
|
|
430
|
+
startupPromises.set(projectRoot, operation);
|
|
431
|
+
try {
|
|
432
|
+
const status = await operation;
|
|
433
|
+
return { projectRoot, started: true, running: status.running, skipped: null };
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return { projectRoot, started: false, running: false, skipped: 'unavailable' };
|
|
272
437
|
}
|
|
273
|
-
throw new Error('runtime failed to start');
|
|
274
438
|
}
|
|
275
439
|
export async function stopRuntime(startPath) {
|
|
276
440
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
@@ -278,10 +442,10 @@ export async function stopRuntime(startPath) {
|
|
|
278
442
|
if (!lease)
|
|
279
443
|
return runtimeStatus(projectRoot);
|
|
280
444
|
await request(projectRoot, lease, 'stop');
|
|
281
|
-
for (let attempt = 0; attempt <
|
|
445
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
282
446
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
283
447
|
const status = await runtimeStatus(projectRoot);
|
|
284
|
-
if (!status.running)
|
|
448
|
+
if (!status.running && !status.lease)
|
|
285
449
|
return status;
|
|
286
450
|
}
|
|
287
451
|
throw new Error('runtime failed to stop');
|
|
@@ -293,3 +457,15 @@ export async function observeRuntime(startPath) {
|
|
|
293
457
|
return false;
|
|
294
458
|
return Boolean((await request(projectRoot, lease, 'observe'))?.ok);
|
|
295
459
|
}
|
|
460
|
+
export async function touchRuntime(startPath) {
|
|
461
|
+
try {
|
|
462
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
463
|
+
const lease = await loadLease(projectRoot);
|
|
464
|
+
if (!lease)
|
|
465
|
+
return false;
|
|
466
|
+
return Boolean((await request(projectRoot, lease, 'ping'))?.ok);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -32,6 +32,7 @@ export interface CognitionEvent<T = Record<string, unknown>> {
|
|
|
32
32
|
export interface CognitionRuntimePolicy {
|
|
33
33
|
schemaVersion: 2;
|
|
34
34
|
enabled: boolean;
|
|
35
|
+
autoStart: boolean;
|
|
35
36
|
allowHookStartup: boolean;
|
|
36
37
|
inactivityTimeoutMs: number;
|
|
37
38
|
promptIntentMaxChars: number;
|
|
@@ -141,6 +142,21 @@ export interface CognitionRecordPayload<T extends Record<string, unknown> = Reco
|
|
|
141
142
|
sourceHash?: string;
|
|
142
143
|
anchorProvenance?: 'snapshot' | 'live' | 'global';
|
|
143
144
|
}
|
|
145
|
+
export interface WorkspacePathChange {
|
|
146
|
+
path: string;
|
|
147
|
+
kind: 'created' | 'modified' | 'deleted' | 'renamed' | 'unknown';
|
|
148
|
+
fileType: 'file';
|
|
149
|
+
sizeBytes?: number;
|
|
150
|
+
mtimeMs?: number;
|
|
151
|
+
}
|
|
152
|
+
export interface WorkspacePathsChangedPayload {
|
|
153
|
+
schemaVersion: 1;
|
|
154
|
+
paths: string[];
|
|
155
|
+
changes: WorkspacePathChange[];
|
|
156
|
+
pathCount: number;
|
|
157
|
+
coalesced: boolean;
|
|
158
|
+
truncated: boolean;
|
|
159
|
+
}
|
|
144
160
|
export interface CognitionProjection {
|
|
145
161
|
schemaVersion: 2;
|
|
146
162
|
contexts: Record<string, CognitionRecordPayload>;
|
|
@@ -2,11 +2,12 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { access, readFile, readdir } from 'node:fs/promises';
|
|
4
4
|
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
5
|
-
import { cognitionManifestPath,
|
|
5
|
+
import { cognitionManifestPath, cognitionV1SyncPath, contextsRoot, deadendsPath, knowledgePath, notesPath, snapshotsDirectory } from '../storage/layout.js';
|
|
6
6
|
import { readJson } from '../storage/json-store.js';
|
|
7
7
|
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
8
8
|
import { appendEvent } from './journal.js';
|
|
9
9
|
import { rebuildProjections } from './projections.js';
|
|
10
|
+
import { loadRuntimePolicy } from './policy.js';
|
|
10
11
|
function digest(record) {
|
|
11
12
|
return crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex');
|
|
12
13
|
}
|
|
@@ -34,7 +35,7 @@ export async function v1Synced(projectRoot) {
|
|
|
34
35
|
async function enabled(projectRoot) {
|
|
35
36
|
try {
|
|
36
37
|
await access(cognitionManifestPath(projectRoot));
|
|
37
|
-
return (await
|
|
38
|
+
return (await loadRuntimePolicy(projectRoot)).enabled;
|
|
38
39
|
}
|
|
39
40
|
catch {
|
|
40
41
|
return false;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
|
|
2
|
+
import type { ContextActivityRecord, ContextAnchor } from './types.js';
|
|
3
|
+
export declare function extractActivity(projectRoot: string, events: CognitionEvent[], anchor: ContextAnchor, workspace: BranchWorkspace | null, limit: number): Promise<ContextActivityRecord[]>;
|