claude-flow 3.62.0 → 3.63.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.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/.helpers-version +1 -0
- package/.claude/helpers/helpers.manifest.json +13 -0
- package/.claude/proven-config.json +42 -0
- package/.claude/settings.local.json +8 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +2 -2
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +2 -2
- package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +7 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.d.ts +1 -20
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.js +347 -108
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.js +46 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +6 -3
- package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +12 -12
- package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.d.ts +78 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.js +310 -0
- package/v3/@claude-flow/cli/dist/src/services/flywheel-transaction.js +5 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.d.ts +79 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.js +76 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.d.ts +187 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.js +238 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning.d.ts +108 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning.js +197 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.d.ts +72 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.js +236 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.d.ts +56 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.js +161 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.d.ts +93 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.js +181 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-store.d.ts +52 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-store.js +119 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-workflows.d.ts +101 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-workflows.js +183 -0
- package/v3/@claude-flow/cli/package.json +1 -1
- package/v3/@claude-flow/shared/dist/events/event-store.test.d.ts +8 -0
- package/v3/@claude-flow/shared/dist/events/event-store.test.js +293 -0
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.d.ts +9 -0
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.js +322 -0
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.d.ts +9 -0
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +93 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** Fixed child entry. Native dependency loading must never move into the MCP parent. */
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
const NAMESPACE = 'managed-workflow-evidence';
|
|
8
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
9
|
+
const UUID = /^[a-f0-9-]{36}$/;
|
|
10
|
+
const TYPES = ['code-review', 'migration', 'research', 'test-generation', 'implementation', 'other'];
|
|
11
|
+
function requestOf(raw) {
|
|
12
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) || Buffer.byteLength(JSON.stringify(raw)) > 80_000)
|
|
13
|
+
throw new Error('invalid-request');
|
|
14
|
+
const r = raw;
|
|
15
|
+
const keys = r.operation === 'index' ? ['v', 'requestId', 'operation', 'id', 'value', 'taskType'] : ['v', 'requestId', 'operation', 'taskType', 'allowedIds'];
|
|
16
|
+
if (r.v !== 1 || typeof r.requestId !== 'string' || !UUID.test(r.requestId) || !['index', 'recall'].includes(String(r.operation)) || Object.keys(r).length !== keys.length || Object.keys(r).some(k => !keys.includes(k)) || !TYPES.includes(String(r.taskType)))
|
|
17
|
+
throw new Error('invalid-request');
|
|
18
|
+
if (r.operation === 'index' && (typeof r.id !== 'string' || !HASH.test(r.id) || typeof r.value !== 'string' || Buffer.byteLength(r.value) > 8192))
|
|
19
|
+
throw new Error('invalid-request');
|
|
20
|
+
if (r.operation === 'recall' && (!Array.isArray(r.allowedIds) || r.allowedIds.length > 500 || r.allowedIds.some(id => typeof id !== 'string' || !HASH.test(id))))
|
|
21
|
+
throw new Error('invalid-request');
|
|
22
|
+
return r;
|
|
23
|
+
}
|
|
24
|
+
const validRow = (row, key) => !!row && row.key === key && row.namespace === NAMESPACE && typeof row.content === 'string' && Buffer.byteLength(row.content) <= 8192;
|
|
25
|
+
/** Pure operation seam for host tests. Serialized callers cannot supply an adapter. */
|
|
26
|
+
export async function executeManagedMemory(raw, adapter, dbPath, beforeWrite) {
|
|
27
|
+
const r = requestOf(raw);
|
|
28
|
+
if (r.operation === 'index') {
|
|
29
|
+
const existing = await adapter.getEntry({ key: r.id, namespace: NAMESPACE, dbPath });
|
|
30
|
+
if (!existing?.success)
|
|
31
|
+
throw new Error('backend-unavailable');
|
|
32
|
+
if (existing.found) {
|
|
33
|
+
if (!validRow(existing.entry, r.id) || existing.entry.content !== r.value)
|
|
34
|
+
throw new Error('conflicting-index');
|
|
35
|
+
return { v: 1, requestId: r.requestId, ok: true, operation: 'index', durable: true, hasEmbedding: existing.entry.hasEmbedding === true };
|
|
36
|
+
}
|
|
37
|
+
await beforeWrite?.();
|
|
38
|
+
const result = await adapter.storeEntry({ key: r.id, namespace: NAMESPACE, value: r.value, tags: ['managed-workflow', r.taskType], generateEmbeddingFlag: true, upsert: false, requireNative: true, appendOnly: true, dbPath, provenanceType: 'system_observation' });
|
|
39
|
+
if (!result?.success || result.persistWarning)
|
|
40
|
+
throw new Error('persistence-unconfirmed');
|
|
41
|
+
// Observe the exact durable row after the append. Neither success nor a
|
|
42
|
+
// signed local ledger alone establishes that the native row exists.
|
|
43
|
+
const stored = await adapter.getEntry({ key: r.id, namespace: NAMESPACE, dbPath });
|
|
44
|
+
if (!stored?.success || !stored.found || !validRow(stored.entry, r.id) || stored.entry.content !== r.value)
|
|
45
|
+
throw new Error('persistence-unconfirmed');
|
|
46
|
+
return { v: 1, requestId: r.requestId, ok: true, operation: 'index', durable: true, hasEmbedding: stored.entry.hasEmbedding === true };
|
|
47
|
+
}
|
|
48
|
+
const result = await adapter.searchEntries({ query: `managed workflow ${r.taskType}`, namespace: NAMESPACE, limit: 20, threshold: 0, dbPath });
|
|
49
|
+
if (!result?.success || !Array.isArray(result.results))
|
|
50
|
+
return { v: 1, requestId: r.requestId, ok: true, operation: 'recall', available: false, rows: [] };
|
|
51
|
+
const allowed = new Set(r.allowedIds), seen = new Set();
|
|
52
|
+
const rows = [];
|
|
53
|
+
for (const hit of result.results.slice(0, 20)) {
|
|
54
|
+
if (!hit || hit.namespace !== NAMESPACE || typeof hit.key !== 'string' || !HASH.test(hit.key) || !allowed.has(hit.key) || seen.has(hit.key) || !Number.isFinite(hit.score) || hit.score < -1 || hit.score > 1)
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(hit.key);
|
|
57
|
+
const hydrated = await adapter.getEntry({ key: hit.key, namespace: NAMESPACE, dbPath });
|
|
58
|
+
if (hydrated?.success && hydrated.found && validRow(hydrated.entry, hit.key))
|
|
59
|
+
rows.push({ key: hit.key, namespace: NAMESPACE, score: hit.score, content: hydrated.entry.content });
|
|
60
|
+
}
|
|
61
|
+
return { v: 1, requestId: r.requestId, ok: true, operation: 'recall', available: true, rows };
|
|
62
|
+
}
|
|
63
|
+
function databasePath() {
|
|
64
|
+
const root = realpathSync(process.cwd()), dir = join(root, '.swarm');
|
|
65
|
+
if (!existsSync(dir))
|
|
66
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
67
|
+
if (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory())
|
|
68
|
+
throw new Error('backend-unavailable');
|
|
69
|
+
const path = join(dir, 'memory.db');
|
|
70
|
+
if (existsSync(path) && (lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()))
|
|
71
|
+
throw new Error('backend-unavailable');
|
|
72
|
+
return path;
|
|
73
|
+
}
|
|
74
|
+
async function loadNative(dbPath) {
|
|
75
|
+
// Transformers.js does not consistently honor the Python offline env flags.
|
|
76
|
+
// Configure the exact module identity AgentDB loads, before registry init.
|
|
77
|
+
const require = createRequire(import.meta.url);
|
|
78
|
+
const memoryEntry = require.resolve('@claude-flow/memory');
|
|
79
|
+
const agentEntry = createRequire(memoryEntry).resolve('agentdb');
|
|
80
|
+
const owner = createRequire(agentEntry);
|
|
81
|
+
for (const name of ['@huggingface/transformers', '@xenova/transformers']) {
|
|
82
|
+
let path;
|
|
83
|
+
try {
|
|
84
|
+
path = owner.resolve(name);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const modulePath = pathToFileURL(path).href;
|
|
90
|
+
const model = await import(modulePath);
|
|
91
|
+
if (model.env)
|
|
92
|
+
model.env.allowRemoteModels = false;
|
|
93
|
+
}
|
|
94
|
+
const js = new URL('../memory/memory-bridge.js', import.meta.url);
|
|
95
|
+
const moduleUrl = existsSync(fileURLToPath(js)) ? js : new URL('../memory/memory-bridge.ts', import.meta.url);
|
|
96
|
+
// Deliberately nonliteral import: targeted boundary builds do not pull the
|
|
97
|
+
// optional native dependency graph into the parent compilation.
|
|
98
|
+
const modulePath = moduleUrl.href;
|
|
99
|
+
const bridge = await import(modulePath);
|
|
100
|
+
const registry = await bridge.getControllerRegistry(dbPath);
|
|
101
|
+
const agentdb = registry?.getAgentDB?.(), db = agentdb?.database;
|
|
102
|
+
if (!db || agentdb.isWasm === true || typeof db.inTransaction !== 'boolean' || typeof db.transaction !== 'function' || db.memory !== false || typeof db.name !== 'string' || resolve(db.name) !== resolve(dbPath))
|
|
103
|
+
throw new Error('backend-unavailable');
|
|
104
|
+
return { getEntry: bridge.bridgeGetEntry, searchEntries: bridge.bridgeSearchEntries, storeEntry: bridge.bridgeStoreEntry };
|
|
105
|
+
}
|
|
106
|
+
if (typeof process.send === 'function' && process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
107
|
+
let busy = false;
|
|
108
|
+
let idle;
|
|
109
|
+
let adapter;
|
|
110
|
+
let pendingGrant;
|
|
111
|
+
const ask = (id, phase) => new Promise(resolve => {
|
|
112
|
+
pendingGrant = { id, grant: phase === 'ready' ? 'execute' : 'write', resolve };
|
|
113
|
+
process.send?.({ v: 1, requestId: id, phase });
|
|
114
|
+
});
|
|
115
|
+
process.once('disconnect', () => process.exit(0));
|
|
116
|
+
process.on('message', async (value) => {
|
|
117
|
+
if (value && typeof value === 'object' && 'grant' in value) {
|
|
118
|
+
const message = value;
|
|
119
|
+
if (Object.keys(message).length === 3 && message.v === 1 && message.requestId === pendingGrant?.id && message.grant === pendingGrant?.grant) {
|
|
120
|
+
const grant = pendingGrant;
|
|
121
|
+
pendingGrant = undefined;
|
|
122
|
+
grant?.resolve();
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (idle)
|
|
127
|
+
clearTimeout(idle);
|
|
128
|
+
let request;
|
|
129
|
+
try {
|
|
130
|
+
request = requestOf(value);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
process.send?.({ v: 1, ok: false, error: 'invalid-request' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (busy) {
|
|
137
|
+
process.send?.({ v: 1, requestId: request.requestId, ok: false, error: 'worker-busy' });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
busy = true;
|
|
141
|
+
try {
|
|
142
|
+
const path = databasePath();
|
|
143
|
+
adapter ??= loadNative(path);
|
|
144
|
+
const native = await adapter;
|
|
145
|
+
await ask(request.requestId, 'ready');
|
|
146
|
+
const result = await executeManagedMemory(request, native, path, () => ask(request.requestId, 'before-write'));
|
|
147
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 200_000)
|
|
148
|
+
throw new Error('result-limit');
|
|
149
|
+
process.send?.(result);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Upstream errors may carry prompts, SQL, paths or credentials.
|
|
153
|
+
process.send?.({ v: 1, requestId: request.requestId, ok: false, error: 'native-operation-unavailable' });
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
busy = false;
|
|
157
|
+
idle = setTimeout(() => process.exit(0), 5000);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=managed-memory-worker.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type ManagedEvent } from './managed-workflows.js';
|
|
2
|
+
export type ManagedSessionFacts = {
|
|
3
|
+
status?: string;
|
|
4
|
+
budget?: {
|
|
5
|
+
max_list_cost?: {
|
|
6
|
+
amount?: string;
|
|
7
|
+
currency?: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
usage?: {
|
|
11
|
+
list_cost?: {
|
|
12
|
+
amount?: string;
|
|
13
|
+
currency?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type ManagedCollectorTransport = {
|
|
18
|
+
readEvents: (since?: string) => Promise<ManagedEvent[]>;
|
|
19
|
+
readSession: () => Promise<ManagedSessionFacts>;
|
|
20
|
+
};
|
|
21
|
+
type Phase = {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
status: string;
|
|
25
|
+
};
|
|
26
|
+
type Thread = {
|
|
27
|
+
id: string;
|
|
28
|
+
runId: string | null;
|
|
29
|
+
status: string;
|
|
30
|
+
phaseId?: string;
|
|
31
|
+
};
|
|
32
|
+
export type ManagedRunSnapshot = {
|
|
33
|
+
sessionId: string;
|
|
34
|
+
status: string;
|
|
35
|
+
stopReason: string | null;
|
|
36
|
+
budgetCents: number | null;
|
|
37
|
+
listCostCents: number | null;
|
|
38
|
+
providerFinished: boolean;
|
|
39
|
+
workersInspected: false;
|
|
40
|
+
acceptancePassed: false;
|
|
41
|
+
verification: 'unverified';
|
|
42
|
+
runs: Array<{
|
|
43
|
+
id: string;
|
|
44
|
+
status: string;
|
|
45
|
+
result?: string;
|
|
46
|
+
phases: Phase[];
|
|
47
|
+
}>;
|
|
48
|
+
threads: Thread[];
|
|
49
|
+
threadOffset: number;
|
|
50
|
+
threadCount: number;
|
|
51
|
+
threadCounts: {
|
|
52
|
+
running: number;
|
|
53
|
+
ended: number;
|
|
54
|
+
failed: number;
|
|
55
|
+
idle: number;
|
|
56
|
+
};
|
|
57
|
+
blockedEventIds: string[];
|
|
58
|
+
observedFailures: number;
|
|
59
|
+
updatedMs: number;
|
|
60
|
+
};
|
|
61
|
+
export type ManagedCollection = {
|
|
62
|
+
events: ManagedEvent[];
|
|
63
|
+
snapshot: ManagedRunSnapshot;
|
|
64
|
+
};
|
|
65
|
+
export declare const COLLECTOR_LIMITS: {
|
|
66
|
+
readonly sessions: 6;
|
|
67
|
+
readonly events: 10000;
|
|
68
|
+
readonly bytes: 16000000;
|
|
69
|
+
readonly shownThreads: 60;
|
|
70
|
+
readonly staleMs: 300000;
|
|
71
|
+
};
|
|
72
|
+
/** This cache is an observation aid. Cached facts never authorize an action. */
|
|
73
|
+
export declare class ManagedRunCollector {
|
|
74
|
+
private readonly states;
|
|
75
|
+
private readonly now;
|
|
76
|
+
private readonly ttlMs;
|
|
77
|
+
constructor(options?: {
|
|
78
|
+
now?: () => number;
|
|
79
|
+
ttlMs?: number;
|
|
80
|
+
});
|
|
81
|
+
/** Singleflight includes event and session reads; a failed refresh is never cached as success. */
|
|
82
|
+
refresh(sessionId: string, transport: ManagedCollectorTransport): Promise<ManagedCollection>;
|
|
83
|
+
/** Pure observation, suitable for disk snapshot publication. Never invokes a transport. */
|
|
84
|
+
snapshots(): ManagedRunSnapshot[];
|
|
85
|
+
/** A local worker page only. Aggregate counts and observation timestamp do not change. */
|
|
86
|
+
page(sessionId: string, threadOffset?: number): ManagedRunSnapshot | undefined;
|
|
87
|
+
/** A collector doing work cannot be forgotten midflight. No remote work is interrupted. */
|
|
88
|
+
forget(sessionId: string): boolean;
|
|
89
|
+
private observe;
|
|
90
|
+
}
|
|
91
|
+
export declare const sharedManagedCollector: ManagedRunCollector;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=managed-run-collector.d.ts.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/** One explicit, bounded event collector per managed session. No background traffic. */
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { eventPage, ManagedProgress } from './managed-workflows.js';
|
|
4
|
+
export const COLLECTOR_LIMITS = { sessions: 6, events: 10_000, bytes: 16_000_000, shownThreads: 60, staleMs: 300_000 };
|
|
5
|
+
const idOf = (v) => typeof v === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(v) ? v : undefined;
|
|
6
|
+
const record = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
7
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
8
|
+
const cents = (v) => {
|
|
9
|
+
const r = record(v);
|
|
10
|
+
return r && (r.currency === undefined || r.currency === 'USD') && typeof r.amount === 'string' && /^(?:0|[1-9][0-9]{0,8})$/.test(r.amount) ? Number(r.amount) : null;
|
|
11
|
+
};
|
|
12
|
+
const phaseName = (v, fallback) => {
|
|
13
|
+
if (typeof v !== 'string')
|
|
14
|
+
return fallback;
|
|
15
|
+
if (/sk-|ghp_|github_pat_|hf_|npm_|xox|AKIA|AIza|Bearer|PRIVATE KEY|password|secret|credential|[A-Za-z0-9_=-]{24,}|token["']?\s*[:=]/i.test(v))
|
|
16
|
+
return '[redacted phase]';
|
|
17
|
+
return v.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, '').slice(0, 80) || fallback;
|
|
18
|
+
};
|
|
19
|
+
/** This cache is an observation aid. Cached facts never authorize an action. */
|
|
20
|
+
export class ManagedRunCollector {
|
|
21
|
+
states = new Map();
|
|
22
|
+
now;
|
|
23
|
+
ttlMs;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.now = options.now ?? Date.now;
|
|
26
|
+
this.ttlMs = options.ttlMs ?? 1000;
|
|
27
|
+
if (!Number.isFinite(this.ttlMs) || this.ttlMs < 0 || this.ttlMs > 60_000)
|
|
28
|
+
throw new Error('Invalid collector TTL');
|
|
29
|
+
}
|
|
30
|
+
/** Singleflight includes event and session reads; a failed refresh is never cached as success. */
|
|
31
|
+
refresh(sessionId, transport) {
|
|
32
|
+
if (!idOf(sessionId))
|
|
33
|
+
return Promise.reject(new Error('Invalid managed session ID'));
|
|
34
|
+
let state = this.states.get(sessionId);
|
|
35
|
+
if (!state) {
|
|
36
|
+
if (this.states.size >= COLLECTOR_LIMITS.sessions) {
|
|
37
|
+
const oldest = [...this.states.entries()].filter(([, s]) => !s.flight && (this.now() - s.lastUsed >= COLLECTOR_LIMITS.staleMs || ['idle', 'terminated', 'error'].includes(s.result?.snapshot.status ?? 'unknown'))).sort((a, b) => a[1].lastUsed - b[1].lastUsed)[0];
|
|
38
|
+
if (!oldest)
|
|
39
|
+
return Promise.reject(new Error('Managed collector session limit reached'));
|
|
40
|
+
this.states.delete(oldest[0]);
|
|
41
|
+
}
|
|
42
|
+
state = { events: new Map(), bytes: 0, progress: new ManagedProgress(), threads: new Map(), phases: new Map(), blocked: new Set(), session: {}, lastRead: -Infinity, lastUsed: this.now() };
|
|
43
|
+
this.states.set(sessionId, state);
|
|
44
|
+
}
|
|
45
|
+
state.lastUsed = this.now();
|
|
46
|
+
if (state.flight)
|
|
47
|
+
return state.flight.then(clone);
|
|
48
|
+
if (state.result && this.now() - state.lastRead < this.ttlMs)
|
|
49
|
+
return Promise.resolve(clone(state.result));
|
|
50
|
+
const s = state;
|
|
51
|
+
// Start transport in a microtask so reentrant callers also see the flight.
|
|
52
|
+
s.flight = Promise.resolve().then(async () => {
|
|
53
|
+
const [events, session] = await Promise.all([transport.readEvents(s.progress.watermark()), transport.readSession()]);
|
|
54
|
+
if (!record(session))
|
|
55
|
+
throw new Error('Invalid managed session response');
|
|
56
|
+
const validated = eventPage({ data: events }).data;
|
|
57
|
+
const additions = [];
|
|
58
|
+
let bytes = s.bytes, total = s.events.size;
|
|
59
|
+
const batch = new Map();
|
|
60
|
+
for (const input of validated) {
|
|
61
|
+
const e = clone(input), previous = batch.get(e.id) ?? s.events.get(e.id);
|
|
62
|
+
if (previous?.processed_at && !isDeepStrictEqual(previous, e))
|
|
63
|
+
throw new Error('Conflicting managed event replay');
|
|
64
|
+
if (previous && isDeepStrictEqual(previous, e))
|
|
65
|
+
continue;
|
|
66
|
+
bytes += Buffer.byteLength(JSON.stringify(e)) - (previous ? Buffer.byteLength(JSON.stringify(previous)) : 0);
|
|
67
|
+
if (!previous)
|
|
68
|
+
total++;
|
|
69
|
+
if (total > COLLECTOR_LIMITS.events || bytes > COLLECTOR_LIMITS.bytes)
|
|
70
|
+
throw new Error('Managed collector event limit exceeded');
|
|
71
|
+
batch.set(e.id, e);
|
|
72
|
+
}
|
|
73
|
+
for (const e of batch.values())
|
|
74
|
+
additions.push(e);
|
|
75
|
+
s.progress.accept(additions);
|
|
76
|
+
s.bytes = bytes;
|
|
77
|
+
for (const e of additions) {
|
|
78
|
+
s.events.set(e.id, e);
|
|
79
|
+
if (e.processed_at)
|
|
80
|
+
this.observe(s, e);
|
|
81
|
+
}
|
|
82
|
+
s.session = clone(session);
|
|
83
|
+
s.lastRead = this.now();
|
|
84
|
+
const p = s.progress.snapshot();
|
|
85
|
+
const threadCounts = { running: 0, ended: 0, failed: 0, idle: 0 };
|
|
86
|
+
for (const t of s.threads.values()) {
|
|
87
|
+
if (t.status === 'running')
|
|
88
|
+
threadCounts.running++;
|
|
89
|
+
else if (t.status === 'failed')
|
|
90
|
+
threadCounts.failed++;
|
|
91
|
+
else if (t.status === 'ended' || t.status === 'terminated')
|
|
92
|
+
threadCounts.ended++;
|
|
93
|
+
else
|
|
94
|
+
threadCounts.idle++;
|
|
95
|
+
}
|
|
96
|
+
const snapshot = {
|
|
97
|
+
sessionId, status: ['idle', 'running', 'rescheduling', 'terminated', 'error'].includes(session.status ?? '') ? session.status : p.status,
|
|
98
|
+
stopReason: p.stopReason === null || ['end_turn', 'budget_reached', 'requires_action'].includes(p.stopReason) ? p.stopReason : 'unknown', budgetCents: cents(session.budget?.max_list_cost), listCostCents: cents(session.usage?.list_cost) ?? cents(p.listCost),
|
|
99
|
+
providerFinished: p.finished && session.status !== 'running' && session.status !== 'rescheduling' && session.status !== 'terminated',
|
|
100
|
+
workersInspected: false, acceptancePassed: false, verification: 'unverified',
|
|
101
|
+
runs: p.workflowRuns.map(r => ({ ...r, phases: [...(s.phases.get(r.id)?.values() ?? [])] })),
|
|
102
|
+
threads: [...s.threads.values()].slice(0, COLLECTOR_LIMITS.shownThreads), threadOffset: 0, threadCount: s.threads.size, threadCounts,
|
|
103
|
+
blockedEventIds: [...s.blocked], observedFailures: Math.max(p.observedFailures, threadCounts.failed), updatedMs: s.lastRead,
|
|
104
|
+
};
|
|
105
|
+
s.result = { events: [...s.events.values()], snapshot };
|
|
106
|
+
return s.result;
|
|
107
|
+
}).finally(() => { s.flight = undefined; });
|
|
108
|
+
return s.flight.then(clone);
|
|
109
|
+
}
|
|
110
|
+
/** Pure observation, suitable for disk snapshot publication. Never invokes a transport. */
|
|
111
|
+
snapshots() { return [...this.states.values()].flatMap(s => s.result ? [clone(s.result.snapshot)] : []); }
|
|
112
|
+
/** A local worker page only. Aggregate counts and observation timestamp do not change. */
|
|
113
|
+
page(sessionId, threadOffset = 0) {
|
|
114
|
+
if (!Number.isSafeInteger(threadOffset) || threadOffset < 0 || threadOffset >= COLLECTOR_LIMITS.events)
|
|
115
|
+
throw new Error('Invalid managed thread offset');
|
|
116
|
+
const state = this.states.get(sessionId);
|
|
117
|
+
if (!state?.result)
|
|
118
|
+
return undefined;
|
|
119
|
+
return { ...clone(state.result.snapshot), threadOffset, threads: clone([...state.threads.values()].slice(threadOffset, threadOffset + COLLECTOR_LIMITS.shownThreads)) };
|
|
120
|
+
}
|
|
121
|
+
/** A collector doing work cannot be forgotten midflight. No remote work is interrupted. */
|
|
122
|
+
forget(sessionId) {
|
|
123
|
+
const s = this.states.get(sessionId);
|
|
124
|
+
return s?.flight ? false : this.states.delete(sessionId);
|
|
125
|
+
}
|
|
126
|
+
observe(s, e) {
|
|
127
|
+
const runId = idOf(e.workflow_run_id);
|
|
128
|
+
if (runId && e.type === 'workflow_run.created' && Array.isArray(e.phases)) {
|
|
129
|
+
const phases = new Map();
|
|
130
|
+
for (const value of e.phases.slice(0, 100)) {
|
|
131
|
+
const phase = record(value), id = idOf(phase?.id);
|
|
132
|
+
if (id)
|
|
133
|
+
phases.set(id, { id, name: phaseName(phase?.name, id), status: 'queued' });
|
|
134
|
+
}
|
|
135
|
+
s.phases.set(runId, phases);
|
|
136
|
+
}
|
|
137
|
+
if (runId && (e.type === 'workflow_run.phase_started' || e.type === 'workflow_run.phase_ended')) {
|
|
138
|
+
const id = idOf(e.workflow_run_phase_id);
|
|
139
|
+
if (id) {
|
|
140
|
+
const phases = s.phases.get(runId) ?? new Map();
|
|
141
|
+
if (phases.size < 100 || phases.has(id))
|
|
142
|
+
phases.set(id, { id, name: phases.get(id)?.name ?? id, status: e.type.endsWith('started') ? 'running' : 'ended' });
|
|
143
|
+
s.phases.set(runId, phases);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const threadId = idOf(e.session_thread_id);
|
|
147
|
+
if (threadId && e.type.startsWith('session.thread_')) {
|
|
148
|
+
const t = s.threads.get(threadId) ?? { id: threadId, runId: runId ?? null, status: 'idle' };
|
|
149
|
+
if (runId)
|
|
150
|
+
t.runId = runId;
|
|
151
|
+
const phaseId = idOf(e.workflow_run_phase_id);
|
|
152
|
+
if (phaseId)
|
|
153
|
+
t.phaseId = phaseId;
|
|
154
|
+
if (e.type === 'session.thread_status_running')
|
|
155
|
+
t.status = 'running';
|
|
156
|
+
else if (e.type === 'session.thread_status_idle')
|
|
157
|
+
t.status = 'idle';
|
|
158
|
+
else if (e.type === 'session.thread_status_terminated')
|
|
159
|
+
t.status = e.error || t.status === 'failed' ? 'failed' : 'ended';
|
|
160
|
+
s.threads.set(threadId, t);
|
|
161
|
+
}
|
|
162
|
+
if (threadId && e.type === 'session.error') {
|
|
163
|
+
const t = s.threads.get(threadId) ?? { id: threadId, runId: runId ?? null, status: 'failed' };
|
|
164
|
+
t.status = 'failed';
|
|
165
|
+
s.threads.set(threadId, t);
|
|
166
|
+
}
|
|
167
|
+
const permission = typeof e.evaluated_permission === 'string' ? e.evaluated_permission : record(e.evaluated_permission)?.type;
|
|
168
|
+
if (permission === 'ask' || e.type === 'agent.custom_tool_use')
|
|
169
|
+
s.blocked.add(e.id);
|
|
170
|
+
const answered = idOf(e.tool_use_id) ?? idOf(e.custom_tool_use_id);
|
|
171
|
+
if (answered && ['user.tool_confirmation', 'user.custom_tool_result', 'user.tool_result'].includes(e.type))
|
|
172
|
+
s.blocked.delete(answered);
|
|
173
|
+
const stop = record(e.stop_reason);
|
|
174
|
+
if (stop?.type === 'requires_action' && Array.isArray(stop.event_ids))
|
|
175
|
+
for (const id of stop.event_ids)
|
|
176
|
+
if (idOf(id))
|
|
177
|
+
s.blocked.add(id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export const sharedManagedCollector = new ManagedRunCollector();
|
|
181
|
+
//# sourceMappingURL=managed-run-collector.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ManagedRunSnapshot } from './managed-run-collector.js';
|
|
2
|
+
/** Other processes fail rather than poll twice. Never automatically steal a lease. */
|
|
3
|
+
export declare function collectWithLease<T>(root: string, sessionId: string, action: () => Promise<T>, scope?: string): Promise<T>;
|
|
4
|
+
export declare function publishManagedSnapshot(root: string, snapshot: ManagedRunSnapshot): void;
|
|
5
|
+
export type ControlAction = 'inspect' | 'interrupt' | 'terminate';
|
|
6
|
+
/** Host settings are read again immediately before side effects, never from console facts. */
|
|
7
|
+
export declare function executeManagedControl(input: Record<string, unknown>, deps: {
|
|
8
|
+
policy: () => {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
sessions: string[];
|
|
11
|
+
};
|
|
12
|
+
read: (sessionId: string) => Promise<{
|
|
13
|
+
status?: string;
|
|
14
|
+
}>;
|
|
15
|
+
inspect: (sessionId: string, offset: number) => Promise<unknown>;
|
|
16
|
+
interrupt: (sessionId: string) => Promise<unknown>;
|
|
17
|
+
terminate: (sessionId: string) => Promise<unknown>;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
currentStatus: string | undefined;
|
|
21
|
+
snapshot: unknown;
|
|
22
|
+
cancellationConfirmed?: undefined;
|
|
23
|
+
interruptRequested?: undefined;
|
|
24
|
+
result?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
success: boolean;
|
|
27
|
+
currentStatus: string;
|
|
28
|
+
cancellationConfirmed: boolean;
|
|
29
|
+
snapshot?: undefined;
|
|
30
|
+
interruptRequested?: undefined;
|
|
31
|
+
result?: undefined;
|
|
32
|
+
} | {
|
|
33
|
+
success: boolean;
|
|
34
|
+
currentStatus: string | undefined;
|
|
35
|
+
interruptRequested: boolean;
|
|
36
|
+
cancellationConfirmed: boolean;
|
|
37
|
+
result: unknown;
|
|
38
|
+
snapshot?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
success: boolean;
|
|
41
|
+
currentStatus: string | undefined;
|
|
42
|
+
cancellationConfirmed: boolean;
|
|
43
|
+
result: unknown;
|
|
44
|
+
snapshot?: undefined;
|
|
45
|
+
interruptRequested?: undefined;
|
|
46
|
+
}>;
|
|
47
|
+
/** Successful provider deletion is a session fact, never an acceptance verdict. */
|
|
48
|
+
export declare function publishManagedTermination(root: string, sessionId: string, collector: {
|
|
49
|
+
page: (id: string) => ManagedRunSnapshot | undefined;
|
|
50
|
+
forget: (id: string) => boolean;
|
|
51
|
+
}): void;
|
|
52
|
+
//# sourceMappingURL=managed-run-store.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Bounded console projections and a fail-closed cross-process collector lease. */
|
|
2
|
+
import { closeSync, constants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const flights = new Map();
|
|
6
|
+
function folder(root) {
|
|
7
|
+
let p = resolve(root);
|
|
8
|
+
if (!lstatSync(p).isDirectory())
|
|
9
|
+
throw new Error('Invalid managed project root');
|
|
10
|
+
for (const name of ['.claude-flow', 'managed-workflows']) {
|
|
11
|
+
p = join(p, name);
|
|
12
|
+
if (!existsSync(p))
|
|
13
|
+
mkdirSync(p, { mode: 0o700 });
|
|
14
|
+
if (!lstatSync(p).isDirectory() || lstatSync(p).isSymbolicLink())
|
|
15
|
+
throw new Error('Managed snapshot directory must be regular');
|
|
16
|
+
}
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
/** Other processes fail rather than poll twice. Never automatically steal a lease. */
|
|
20
|
+
export function collectWithLease(root, sessionId, action, scope = '') {
|
|
21
|
+
if (!/^sesn_[A-Za-z0-9]+$/.test(sessionId))
|
|
22
|
+
return Promise.reject(new Error('Invalid managed session ID'));
|
|
23
|
+
const path = join(folder(root), `${sessionId}.collector.lock`);
|
|
24
|
+
const pending = flights.get(path);
|
|
25
|
+
if (pending)
|
|
26
|
+
return pending.scope === scope ? pending.promise.then(v => structuredClone(v)) : Promise.reject(new Error('Managed collector belongs to another credential scope'));
|
|
27
|
+
let fd;
|
|
28
|
+
try {
|
|
29
|
+
fd = openSync(path, 'wx', 0o600);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return Promise.reject(new Error('Managed collector is leased by another process; inspect before recovering a stale lease'));
|
|
33
|
+
}
|
|
34
|
+
const flight = Promise.resolve().then(action).finally(() => { closeSync(fd); unlinkSync(path); flights.delete(path); });
|
|
35
|
+
flights.set(path, { scope, promise: flight });
|
|
36
|
+
return flight.then(v => structuredClone(v));
|
|
37
|
+
}
|
|
38
|
+
/** Read only the fixed, capped projection. It has no authorization role. */
|
|
39
|
+
function previous(path) {
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
return [];
|
|
42
|
+
if (!lstatSync(path).isFile() || lstatSync(path).isSymbolicLink())
|
|
43
|
+
throw new Error('Invalid managed snapshot file');
|
|
44
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
45
|
+
try {
|
|
46
|
+
const buffer = Buffer.alloc(128_001);
|
|
47
|
+
const count = readSync(fd, buffer, 0, buffer.length, 0);
|
|
48
|
+
if (count > 128_000)
|
|
49
|
+
throw new Error('Managed snapshot exceeds limit');
|
|
50
|
+
const value = JSON.parse(buffer.subarray(0, count).toString('utf8'));
|
|
51
|
+
if (value.version !== 1 || !Array.isArray(value.sessions) || value.sessions.length > 6)
|
|
52
|
+
throw new Error('Invalid managed snapshot');
|
|
53
|
+
return value.sessions;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function publishManagedSnapshot(root, snapshot) {
|
|
60
|
+
const dir = folder(root), lockPath = join(dir, 'snapshot.lock');
|
|
61
|
+
const fd = openSync(lockPath, 'wx', 0o600), temp = join(dir, `${randomUUID()}.tmp`);
|
|
62
|
+
try {
|
|
63
|
+
const path = join(dir, 'snapshot.json');
|
|
64
|
+
const old = previous(path);
|
|
65
|
+
const sessions = [snapshot, ...old.filter(s => s.sessionId !== snapshot.sessionId)].slice(0, 6);
|
|
66
|
+
if (JSON.stringify(sessions) === JSON.stringify(old))
|
|
67
|
+
return;
|
|
68
|
+
const text = JSON.stringify({ version: 1, updatedMs: Date.now(), sessions });
|
|
69
|
+
if (Buffer.byteLength(text) > 128_000)
|
|
70
|
+
throw new Error('Managed snapshot exceeds limit');
|
|
71
|
+
const out = openSync(temp, 'wx', 0o600);
|
|
72
|
+
try {
|
|
73
|
+
writeFileSync(out, text);
|
|
74
|
+
fsyncSync(out);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
closeSync(out);
|
|
78
|
+
}
|
|
79
|
+
renameSync(temp, path);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (existsSync(temp))
|
|
83
|
+
unlinkSync(temp);
|
|
84
|
+
closeSync(fd);
|
|
85
|
+
unlinkSync(lockPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Host settings are read again immediately before side effects, never from console facts. */
|
|
89
|
+
export async function executeManagedControl(input, deps) {
|
|
90
|
+
const sessionId = input.sessionId, action = input.action;
|
|
91
|
+
if (Object.keys(input).some(k => !['sessionId', 'action', 'threadOffset'].includes(k)) || typeof sessionId !== 'string' || !/^sesn_[A-Za-z0-9]+$/.test(sessionId) || !['inspect', 'interrupt', 'terminate'].includes(String(action)))
|
|
92
|
+
throw new Error('Invalid managed control request');
|
|
93
|
+
const offset = input.threadOffset ?? 0;
|
|
94
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset >= 10_000 || (action !== 'inspect' && input.threadOffset !== undefined))
|
|
95
|
+
throw new Error('Invalid managed worker page');
|
|
96
|
+
const allowed = () => { const p = deps.policy(); return p.enabled && p.sessions.includes(sessionId); };
|
|
97
|
+
if (!allowed())
|
|
98
|
+
throw new Error('Host managed control policy denies this session');
|
|
99
|
+
const current = await deps.read(sessionId);
|
|
100
|
+
if (!current || !['idle', 'running', 'rescheduling', 'terminated'].includes(current.status ?? ''))
|
|
101
|
+
throw new Error('Unknown provider session state');
|
|
102
|
+
if (!allowed())
|
|
103
|
+
throw new Error('Host managed control policy changed before dispatch');
|
|
104
|
+
if (action === 'inspect')
|
|
105
|
+
return { success: true, currentStatus: current.status, snapshot: await deps.inspect(sessionId, offset) };
|
|
106
|
+
if (current.status === 'terminated')
|
|
107
|
+
return { success: true, currentStatus: 'terminated', cancellationConfirmed: true };
|
|
108
|
+
if (action === 'interrupt')
|
|
109
|
+
return { success: true, currentStatus: current.status, interruptRequested: true, cancellationConfirmed: false, result: await deps.interrupt(sessionId) };
|
|
110
|
+
return { success: true, currentStatus: current.status, cancellationConfirmed: true, result: await deps.terminate(sessionId) };
|
|
111
|
+
}
|
|
112
|
+
/** Successful provider deletion is a session fact, never an acceptance verdict. */
|
|
113
|
+
export function publishManagedTermination(root, sessionId, collector) {
|
|
114
|
+
const snapshot = collector.page(sessionId);
|
|
115
|
+
if (snapshot)
|
|
116
|
+
publishManagedSnapshot(root, { ...snapshot, status: 'terminated', providerFinished: false, updatedMs: Date.now() });
|
|
117
|
+
collector.forget(sessionId);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=managed-run-store.js.map
|