claude-code-swarm 0.3.12 → 0.3.17
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-plugin/marketplace.json +1 -1
- package/.claude-plugin/mcp-launcher.mjs +17 -4
- package/.claude-plugin/plugin.json +1 -1
- package/e2e/tier7-memory-sync.test.mjs +259 -0
- package/hooks/hooks.json +31 -22
- package/package.json +4 -4
- package/scripts/map-hook.mjs +14 -0
- package/scripts/map-sidecar.mjs +132 -0
- package/src/__tests__/memory-sync.test.mjs +132 -0
- package/src/__tests__/memory-watcher.test.mjs +104 -0
- package/src/__tests__/opentasks-connector.test.mjs +216 -0
- package/src/__tests__/sessionlog.test.mjs +40 -0
- package/src/__tests__/sidecar-server.test.mjs +2 -2
- package/src/bootstrap.mjs +27 -2
- package/src/content-provider.mjs +176 -0
- package/src/map-connection.mjs +6 -2
- package/src/map-events.mjs +30 -0
- package/src/memory-watcher.mjs +73 -0
- package/src/opentasks-connector.mjs +86 -0
- package/src/sessionlog.mjs +19 -0
- package/src/sidecar-server.mjs +54 -7
package/src/bootstrap.mjs
CHANGED
|
@@ -431,12 +431,37 @@ export async function bootstrap(pluginDirOverride, sessionId) {
|
|
|
431
431
|
|
|
432
432
|
let minimemStatus = "disabled";
|
|
433
433
|
if (config.minimem?.enabled) {
|
|
434
|
-
|
|
434
|
+
// Check if minimem CLI is available and memory directory exists
|
|
435
|
+
try {
|
|
436
|
+
const { execFileSync } = await import("node:child_process");
|
|
437
|
+
const { existsSync } = await import("node:fs");
|
|
438
|
+
execFileSync("which", ["minimem"], { stdio: "pipe" });
|
|
439
|
+
const dir = config.minimem?.dir || ".swarm/minimem";
|
|
440
|
+
if (existsSync(dir)) {
|
|
441
|
+
minimemStatus = "ready";
|
|
442
|
+
} else {
|
|
443
|
+
// Create the directory and initialize so MCP server can start
|
|
444
|
+
const { mkdirSync } = await import("node:fs");
|
|
445
|
+
mkdirSync(dir + "/memory", { recursive: true });
|
|
446
|
+
try {
|
|
447
|
+
execFileSync("minimem", ["init", dir], { stdio: "pipe", timeout: 10_000 });
|
|
448
|
+
} catch { /* init may fail if already initialized */ }
|
|
449
|
+
minimemStatus = "ready";
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
minimemStatus = "enabled"; // CLI not found — MCP server won't start
|
|
453
|
+
}
|
|
435
454
|
}
|
|
436
455
|
|
|
437
456
|
let skilltreeStatus = "disabled";
|
|
438
457
|
if (config.skilltree?.enabled) {
|
|
439
|
-
|
|
458
|
+
try {
|
|
459
|
+
const { execFileSync } = await import("node:child_process");
|
|
460
|
+
execFileSync("which", ["skill-tree"], { stdio: "pipe" });
|
|
461
|
+
skilltreeStatus = "ready";
|
|
462
|
+
} catch {
|
|
463
|
+
skilltreeStatus = "enabled";
|
|
464
|
+
}
|
|
440
465
|
}
|
|
441
466
|
|
|
442
467
|
return {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* content-provider.mjs — Trajectory content provider for claude-code-swarm
|
|
3
|
+
*
|
|
4
|
+
* Provides session transcript content for on-demand trajectory/content requests
|
|
5
|
+
* from the hub. Reads from sessionlog's session state to find the transcript,
|
|
6
|
+
* then serves the raw Claude Code JSONL.
|
|
7
|
+
*
|
|
8
|
+
* Two content sources:
|
|
9
|
+
* 1. Live session: reads directly from state.transcriptPath (active JSONL file)
|
|
10
|
+
* 2. Committed checkpoint: reads from sessionlog's checkpoint store (full.jsonl)
|
|
11
|
+
*
|
|
12
|
+
* Returns { metadata, transcript, prompts, context } matching the
|
|
13
|
+
* SessionContentProvider type from @multi-agent-protocol/sdk.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { SESSIONLOG_DIR } from "./paths.mjs";
|
|
19
|
+
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
20
|
+
import { createLogger } from "./log.mjs";
|
|
21
|
+
|
|
22
|
+
const log = createLogger("content-provider");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a content provider function for the sidecar.
|
|
26
|
+
* The provider receives a checkpointId and returns transcript content.
|
|
27
|
+
*
|
|
28
|
+
* For live sessions, checkpointId may be the session ID or a checkpoint ID.
|
|
29
|
+
* We search sessionlog state to find the transcript path.
|
|
30
|
+
*
|
|
31
|
+
* @returns {Function} SessionContentProvider-compatible async function
|
|
32
|
+
*/
|
|
33
|
+
export function createContentProvider() {
|
|
34
|
+
return async function provideContent(checkpointId) {
|
|
35
|
+
try {
|
|
36
|
+
// 1. Try to find a live session with this checkpoint or session ID
|
|
37
|
+
const liveContent = await readLiveSessionContent(checkpointId);
|
|
38
|
+
if (liveContent) return liveContent;
|
|
39
|
+
|
|
40
|
+
// 2. Try to read from committed checkpoint store
|
|
41
|
+
const committedContent = await readCommittedContent(checkpointId);
|
|
42
|
+
if (committedContent) return committedContent;
|
|
43
|
+
|
|
44
|
+
log.warn("content not found for checkpoint", { checkpointId });
|
|
45
|
+
return null;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.warn("content provider error", { checkpointId, error: err.message });
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read transcript content from a live (non-ended) sessionlog session.
|
|
55
|
+
* Searches all session state files for one that matches the checkpoint ID
|
|
56
|
+
* or has a matching session ID, then reads the transcript from disk.
|
|
57
|
+
*/
|
|
58
|
+
async function readLiveSessionContent(checkpointId) {
|
|
59
|
+
if (!fs.existsSync(SESSIONLOG_DIR)) return null;
|
|
60
|
+
|
|
61
|
+
let files;
|
|
62
|
+
try {
|
|
63
|
+
files = fs.readdirSync(SESSIONLOG_DIR).filter(f => f.endsWith(".json"));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
try {
|
|
70
|
+
const statePath = path.join(SESSIONLOG_DIR, f);
|
|
71
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
72
|
+
|
|
73
|
+
// Match by session ID, checkpoint ID, or checkpoint in turnCheckpointIDs
|
|
74
|
+
const isMatch =
|
|
75
|
+
state.sessionID === checkpointId ||
|
|
76
|
+
state.lastCheckpointID === checkpointId ||
|
|
77
|
+
(state.turnCheckpointIDs || []).includes(checkpointId);
|
|
78
|
+
|
|
79
|
+
if (!isMatch) continue;
|
|
80
|
+
|
|
81
|
+
// Read the transcript from the path stored in session state
|
|
82
|
+
const transcriptPath = state.transcriptPath;
|
|
83
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
84
|
+
log.warn("transcript path not found", { sessionID: state.sessionID, transcriptPath });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const transcript = fs.readFileSync(transcriptPath, "utf-8");
|
|
89
|
+
|
|
90
|
+
// Extract prompts from transcript (user messages)
|
|
91
|
+
const prompts = extractPrompts(transcript);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
metadata: {
|
|
95
|
+
sessionID: state.sessionID,
|
|
96
|
+
phase: state.phase,
|
|
97
|
+
stepCount: state.stepCount || 0,
|
|
98
|
+
filesTouched: state.filesTouched || [],
|
|
99
|
+
tokenUsage: state.tokenUsage || {},
|
|
100
|
+
startedAt: state.startedAt,
|
|
101
|
+
endedAt: state.endedAt,
|
|
102
|
+
source: "live",
|
|
103
|
+
},
|
|
104
|
+
transcript,
|
|
105
|
+
prompts,
|
|
106
|
+
context: `Session ${state.sessionID} (${state.phase})`,
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
// Skip malformed files
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read transcript content from sessionlog's committed checkpoint store.
|
|
118
|
+
* Uses sessionlog's library API via resolvePackage.
|
|
119
|
+
*/
|
|
120
|
+
async function readCommittedContent(checkpointId) {
|
|
121
|
+
try {
|
|
122
|
+
const sessionlogMod = await resolvePackage("sessionlog");
|
|
123
|
+
if (!sessionlogMod?.createCheckpointStore) return null;
|
|
124
|
+
|
|
125
|
+
const store = sessionlogMod.createCheckpointStore();
|
|
126
|
+
if (!store?.readSessionContent) return null;
|
|
127
|
+
|
|
128
|
+
// Try reading committed content (session index 0)
|
|
129
|
+
const content = await store.readSessionContent(checkpointId, 0);
|
|
130
|
+
if (!content) return null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
metadata: {
|
|
134
|
+
...content.metadata,
|
|
135
|
+
source: "committed",
|
|
136
|
+
},
|
|
137
|
+
transcript: content.transcript,
|
|
138
|
+
prompts: content.prompts,
|
|
139
|
+
context: content.context,
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract user prompts from a Claude Code JSONL transcript.
|
|
148
|
+
*/
|
|
149
|
+
function extractPrompts(transcript) {
|
|
150
|
+
const prompts = [];
|
|
151
|
+
for (const line of transcript.split("\n")) {
|
|
152
|
+
if (!line.trim()) continue;
|
|
153
|
+
try {
|
|
154
|
+
const entry = JSON.parse(line);
|
|
155
|
+
if (entry.type === "user") {
|
|
156
|
+
const msg = entry.message;
|
|
157
|
+
if (typeof msg === "string") {
|
|
158
|
+
prompts.push(msg);
|
|
159
|
+
} else if (msg?.content) {
|
|
160
|
+
if (typeof msg.content === "string") {
|
|
161
|
+
prompts.push(msg.content);
|
|
162
|
+
} else if (Array.isArray(msg.content)) {
|
|
163
|
+
const text = msg.content
|
|
164
|
+
.filter(b => b.type === "text" && b.text)
|
|
165
|
+
.map(b => b.text)
|
|
166
|
+
.join("\n");
|
|
167
|
+
if (text) prompts.push(text);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Skip malformed lines
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return prompts.join("\n---\n");
|
|
176
|
+
}
|
package/src/map-connection.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const log = createLogger("map");
|
|
|
24
24
|
* authRequired challenge with the server's preferred method + this credential.
|
|
25
25
|
* When absent, uses the standard SDK connect() for open mode servers.
|
|
26
26
|
*/
|
|
27
|
-
export async function connectToMAP({ server, scope, systemId, onMessage, credential }) {
|
|
27
|
+
export async function connectToMAP({ server, scope, systemId, onMessage, credential, projectContext }) {
|
|
28
28
|
try {
|
|
29
29
|
const mapSdk = await resolvePackage("@multi-agent-protocol/sdk");
|
|
30
30
|
if (!mapSdk) throw new Error("@multi-agent-protocol/sdk not available");
|
|
@@ -38,12 +38,16 @@ export async function connectToMAP({ server, scope, systemId, onMessage, credent
|
|
|
38
38
|
role: "sidecar",
|
|
39
39
|
scopes: [scope],
|
|
40
40
|
capabilities: {
|
|
41
|
-
trajectory: { canReport: true },
|
|
41
|
+
trajectory: { canReport: true, canServeContent: true },
|
|
42
42
|
tasks: { canCreate: true, canAssign: true, canUpdate: true, canList: true },
|
|
43
|
+
...(projectContext?.task_graph ? {
|
|
44
|
+
opentasks: { canQuery: true, canLink: true, canAnnotate: true, canTask: true },
|
|
45
|
+
} : {}),
|
|
43
46
|
},
|
|
44
47
|
metadata: {
|
|
45
48
|
systemId,
|
|
46
49
|
type: "claude-code-swarm-sidecar",
|
|
50
|
+
...(projectContext || {}),
|
|
47
51
|
},
|
|
48
52
|
reconnection: {
|
|
49
53
|
enabled: true,
|
package/src/map-events.mjs
CHANGED
|
@@ -443,3 +443,33 @@ export async function handleNativeTaskUpdatedEvent(config, hookData, sessionId)
|
|
|
443
443
|
}, sessionId);
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
// ── minimem/skill-tree sync → MAP ──────────────────────────────────────────────
|
|
448
|
+
//
|
|
449
|
+
// When agents use minimem MCP tools that write data (append, upsert),
|
|
450
|
+
// emit x-openhive/memory.sync so OpenHive can update its content cache.
|
|
451
|
+
// This is a best-effort notification — the source of truth is the filesystem.
|
|
452
|
+
|
|
453
|
+
/** Tools that indicate a memory write (vs read-only search) */
|
|
454
|
+
const MINIMEM_WRITE_TOOLS = new Set([
|
|
455
|
+
"minimem__memory_append",
|
|
456
|
+
"minimem__memory_upsert",
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Build bridge command for minimem MCP tool use.
|
|
461
|
+
* Only emits for write operations (append/upsert), not reads.
|
|
462
|
+
*/
|
|
463
|
+
export function buildMinimemBridgeCommand(hookData) {
|
|
464
|
+
const toolName = hookData.tool_name || "";
|
|
465
|
+
// Only emit for write operations
|
|
466
|
+
if (!MINIMEM_WRITE_TOOLS.has(toolName) && !toolName.includes("append") && !toolName.includes("upsert")) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
action: "bridge-memory-sync",
|
|
472
|
+
agentId: hookData.session_id || "minimem",
|
|
473
|
+
timestamp: new Date().toISOString(),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-watcher.mjs — Watches the minimem memory directory for file changes
|
|
3
|
+
* and sends bridge-memory-sync commands to notify OpenHive via MAP.
|
|
4
|
+
*
|
|
5
|
+
* This bridges the gap between filesystem writes (Write tool, manual edits)
|
|
6
|
+
* and MAP sync notifications. minimem's MCP tools are read-only, so this
|
|
7
|
+
* watcher is the only way to detect when an agent writes to memory.
|
|
8
|
+
*
|
|
9
|
+
* Runs inside the MAP sidecar process (persistent for the session).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import chokidar from "chokidar";
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import { createLogger } from "./log.mjs";
|
|
15
|
+
|
|
16
|
+
const log = createLogger("memory-watcher");
|
|
17
|
+
|
|
18
|
+
const DEBOUNCE_MS = 2000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start watching a minimem directory for file changes.
|
|
22
|
+
* When changes are detected (debounced), calls the provided callback.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} memoryDir - Path to the minimem directory (e.g., ".swarm/minimem")
|
|
25
|
+
* @param {(event: { type: string; path: string }) => void} onSync - Called when sync should be emitted
|
|
26
|
+
* @returns {{ close: () => void } | null} Watcher handle, or null if directory doesn't exist
|
|
27
|
+
*/
|
|
28
|
+
export function startMemoryWatcher(memoryDir, onSync) {
|
|
29
|
+
if (!memoryDir || !existsSync(memoryDir)) {
|
|
30
|
+
log.debug("memory watcher skipped — directory not found", { dir: memoryDir });
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let debounceTimer = null;
|
|
35
|
+
|
|
36
|
+
const watcher = chokidar.watch(memoryDir, {
|
|
37
|
+
ignoreInitial: true,
|
|
38
|
+
ignored: [/node_modules/, /\.git/, /index\.db/, /\.cache/, /\.minimem/],
|
|
39
|
+
depth: 3,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function debouncedSync(eventType, filePath) {
|
|
43
|
+
// Only react to .md file changes
|
|
44
|
+
if (!filePath.endsWith(".md")) return;
|
|
45
|
+
|
|
46
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
47
|
+
debounceTimer = setTimeout(() => {
|
|
48
|
+
debounceTimer = null;
|
|
49
|
+
log.debug("memory change detected", { event: eventType, path: filePath });
|
|
50
|
+
onSync({ type: eventType, path: filePath });
|
|
51
|
+
}, DEBOUNCE_MS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
watcher.on("add", (p) => debouncedSync("add", p));
|
|
55
|
+
watcher.on("change", (p) => debouncedSync("change", p));
|
|
56
|
+
watcher.on("unlink", (p) => debouncedSync("unlink", p));
|
|
57
|
+
|
|
58
|
+
watcher.on("ready", () => {
|
|
59
|
+
log.info("memory watcher started", { dir: memoryDir });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
watcher.on("error", (err) => {
|
|
63
|
+
log.warn("memory watcher error", { error: err.message });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
close() {
|
|
68
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
69
|
+
watcher.close();
|
|
70
|
+
log.debug("memory watcher stopped");
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opentasks-connector.mjs — MAP connector registration for opentasks
|
|
3
|
+
*
|
|
4
|
+
* Extracted from map-sidecar.mjs for testability.
|
|
5
|
+
* Registers notification handlers on a MAP connection so that when the hub
|
|
6
|
+
* (or another agent) sends opentasks/*.request notifications, the connector
|
|
7
|
+
* queries the local daemon and sends back opentasks/*.response.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createLogger } from "./log.mjs";
|
|
11
|
+
|
|
12
|
+
const log = createLogger("opentasks-connector");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register opentasks notification handlers on a MAP connection.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} conn - MAP connection with onNotification/sendNotification
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* @param {string} options.scope - MAP scope (e.g. "swarm:gsd")
|
|
20
|
+
* @param {() => void} [options.onActivity] - Called on each notification (for inactivity timer reset)
|
|
21
|
+
* @param {() => Promise<object>} [options.importOpentasks] - Override for dynamic import("opentasks")
|
|
22
|
+
* @param {() => Promise<object>} [options.importOpentasksClient] - Override for dynamic import of opentasks-client
|
|
23
|
+
*/
|
|
24
|
+
export async function registerOpenTasksHandler(conn, options = {}) {
|
|
25
|
+
if (!conn || typeof conn.onNotification !== "function") return;
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
scope = "swarm:default",
|
|
29
|
+
onActivity,
|
|
30
|
+
importOpentasks,
|
|
31
|
+
importOpentasksClient,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const opentasks = importOpentasks
|
|
36
|
+
? await importOpentasks()
|
|
37
|
+
: await import("opentasks");
|
|
38
|
+
|
|
39
|
+
if (!opentasks?.createMAPConnector || !opentasks?.createClient) {
|
|
40
|
+
log.debug("opentasks MAP connector not available (missing exports)");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { createMAPConnector, createClient, MAP_CONNECTOR_METHODS } = opentasks;
|
|
45
|
+
|
|
46
|
+
const opentasksClient = importOpentasksClient
|
|
47
|
+
? await importOpentasksClient()
|
|
48
|
+
: await import("./opentasks-client.mjs");
|
|
49
|
+
|
|
50
|
+
const { findSocketPath } = opentasksClient;
|
|
51
|
+
const socketPath = findSocketPath();
|
|
52
|
+
const client = createClient({ socketPath, autoConnect: true });
|
|
53
|
+
|
|
54
|
+
const connector = createMAPConnector({
|
|
55
|
+
client,
|
|
56
|
+
send: (method, params) => {
|
|
57
|
+
try {
|
|
58
|
+
conn.sendNotification(method, params);
|
|
59
|
+
} catch {
|
|
60
|
+
log.debug("failed to send opentasks response", { method });
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
agentId: `${scope}-sidecar`,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Register handlers for all 4 request methods
|
|
67
|
+
const requestMethods = [
|
|
68
|
+
MAP_CONNECTOR_METHODS.QUERY_REQUEST,
|
|
69
|
+
MAP_CONNECTOR_METHODS.LINK_REQUEST,
|
|
70
|
+
MAP_CONNECTOR_METHODS.ANNOTATE_REQUEST,
|
|
71
|
+
MAP_CONNECTOR_METHODS.TASK_REQUEST,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const method of requestMethods) {
|
|
75
|
+
conn.onNotification(method, async (params) => {
|
|
76
|
+
log.debug("opentasks request received", { method, requestId: params?.request_id });
|
|
77
|
+
if (onActivity) onActivity();
|
|
78
|
+
connector.handleNotification(method, params || {});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
log.info("opentasks connector registered", { methods: requestMethods.length });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.debug("opentasks connector not available", { error: err.message });
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/sessionlog.mjs
CHANGED
|
@@ -16,6 +16,20 @@ import { sendToSidecar, ensureSidecar } from "./sidecar-client.mjs";
|
|
|
16
16
|
import { fireAndForgetTrajectory } from "./map-connection.mjs";
|
|
17
17
|
import { resolvePackage } from "./swarmkit-resolver.mjs";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Get the current git branch name. Returns null if not in a git repo.
|
|
21
|
+
*/
|
|
22
|
+
function getGitBranch() {
|
|
23
|
+
try {
|
|
24
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
27
|
+
}).trim() || null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* Check if sessionlog is installed and active.
|
|
21
35
|
* Returns 'active', 'installed but not enabled', or 'not installed'.
|
|
@@ -151,6 +165,7 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
|
|
|
151
165
|
id,
|
|
152
166
|
session_id: state.sessionID,
|
|
153
167
|
agent: `${teamName}-sidecar`,
|
|
168
|
+
branch: getGitBranch(),
|
|
154
169
|
files_touched: [],
|
|
155
170
|
checkpoints_count: 0,
|
|
156
171
|
};
|
|
@@ -161,6 +176,10 @@ export function buildTrajectoryCheckpoint(state, syncLevel, config) {
|
|
|
161
176
|
turnId: state.turnID,
|
|
162
177
|
startedAt: state.startedAt,
|
|
163
178
|
label: `Turn ${state.turnID || "?"} (step ${state.stepCount || 0}, ${state.phase || "unknown"})`,
|
|
179
|
+
// Project context for display
|
|
180
|
+
project: path.basename(process.cwd()),
|
|
181
|
+
firstPrompt: state.firstPrompt ? state.firstPrompt.slice(0, 200) : undefined,
|
|
182
|
+
template: config.template || undefined,
|
|
164
183
|
};
|
|
165
184
|
if (state.endedAt) metadata.endedAt = state.endedAt;
|
|
166
185
|
|
package/src/sidecar-server.mjs
CHANGED
|
@@ -94,6 +94,8 @@ export function createSocketServer(socketPath, onCommand) {
|
|
|
94
94
|
export function createCommandHandler(connection, scope, registeredAgents, opts = {}) {
|
|
95
95
|
// Use a getter pattern so the connection ref can be updated
|
|
96
96
|
let conn = connection;
|
|
97
|
+
let _trajectoryResourceId = null; // Cached resource_id from server response
|
|
98
|
+
let _memoryResourceId = null; // Cached resource_id for memory sync
|
|
97
99
|
const { inboxInstance, meshPeer, transportMode = "websocket" } = opts;
|
|
98
100
|
const useMeshRegistry = transportMode === "mesh" && inboxInstance;
|
|
99
101
|
|
|
@@ -291,10 +293,17 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
291
293
|
const c = conn || await waitForConn();
|
|
292
294
|
if (c) {
|
|
293
295
|
try {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
// Include cached resource_id if available from a previous response
|
|
297
|
+
const payload = { checkpoint: command.checkpoint };
|
|
298
|
+
if (_trajectoryResourceId) {
|
|
299
|
+
payload.resource_id = _trajectoryResourceId;
|
|
300
|
+
}
|
|
301
|
+
const result = await c.callExtension("trajectory/checkpoint", payload);
|
|
302
|
+
// Cache resource_id from server response for subsequent calls
|
|
303
|
+
if (result?.resource_id) {
|
|
304
|
+
_trajectoryResourceId = result.resource_id;
|
|
305
|
+
}
|
|
306
|
+
respond(client, { ok: true, method: "trajectory", resource_id: result?.resource_id });
|
|
298
307
|
} catch (err) {
|
|
299
308
|
log.warn("trajectory/checkpoint not supported, falling back to broadcast", { error: err.message });
|
|
300
309
|
await c.send(
|
|
@@ -303,9 +312,10 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
303
312
|
type: "trajectory.checkpoint",
|
|
304
313
|
checkpoint: {
|
|
305
314
|
id: command.checkpoint.id,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
agent: command.checkpoint.agent,
|
|
316
|
+
session_id: command.checkpoint.session_id,
|
|
317
|
+
files_touched: command.checkpoint.files_touched,
|
|
318
|
+
token_usage: command.checkpoint.token_usage,
|
|
309
319
|
metadata: command.checkpoint.metadata,
|
|
310
320
|
},
|
|
311
321
|
},
|
|
@@ -380,6 +390,43 @@ export function createCommandHandler(connection, scope, registeredAgents, opts =
|
|
|
380
390
|
break;
|
|
381
391
|
}
|
|
382
392
|
|
|
393
|
+
// --- Memory/skill sync → MAP (x-openhive vendor extensions) ---
|
|
394
|
+
|
|
395
|
+
case "bridge-memory-sync": {
|
|
396
|
+
const c = conn || await waitForConn();
|
|
397
|
+
if (c) {
|
|
398
|
+
try {
|
|
399
|
+
// Use callExtension for JSON-RPC vendor-prefixed method
|
|
400
|
+
// Same pattern as trajectory/checkpoint
|
|
401
|
+
const params = {
|
|
402
|
+
resource_id: _memoryResourceId || "",
|
|
403
|
+
agent_id: command.agentId || "minimem",
|
|
404
|
+
commit_hash: `memory-${Date.now()}`,
|
|
405
|
+
timestamp: command.timestamp || new Date().toISOString(),
|
|
406
|
+
};
|
|
407
|
+
const result = await c.callExtension("x-openhive/memory.sync", params);
|
|
408
|
+
// Cache resource_id from server response
|
|
409
|
+
if (result?.resource_id) {
|
|
410
|
+
_memoryResourceId = result.resource_id;
|
|
411
|
+
}
|
|
412
|
+
respond(client, { ok: true, method: "memory-sync", resource_id: result?.resource_id });
|
|
413
|
+
} catch (err) {
|
|
414
|
+
log.warn("x-openhive/memory.sync not supported, falling back to broadcast", { error: err.message });
|
|
415
|
+
// Fallback: emit as a regular message
|
|
416
|
+
await c.send({ scope }, {
|
|
417
|
+
type: "memory.sync",
|
|
418
|
+
agent_id: command.agentId || "minimem",
|
|
419
|
+
timestamp: command.timestamp,
|
|
420
|
+
_origin: command.agentId || "minimem",
|
|
421
|
+
}, { relationship: "broadcast" });
|
|
422
|
+
respond(client, { ok: true, method: "broadcast-fallback" });
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
respond(client, { ok: false, error: "no connection" });
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
383
430
|
case "state": {
|
|
384
431
|
const c = conn || await waitForConn();
|
|
385
432
|
if (c) {
|