shennian 0.2.43 → 0.2.47
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/dist/src/agents/claude.d.ts +5 -0
- package/dist/src/agents/claude.js +17 -0
- package/dist/src/agents/codex.d.ts +5 -0
- package/dist/src/agents/codex.js +12 -1
- package/dist/src/agents/command-spec.js +74 -3
- package/dist/src/agents/detect.js +12 -2
- package/dist/src/agents/manager.d.ts +16 -0
- package/dist/src/agents/manager.js +145 -0
- package/dist/src/agents/model-registry/discovery.js +6 -0
- package/dist/src/agents/pi.d.ts +1 -0
- package/dist/src/agents/pi.js +18 -2
- package/dist/src/channels/base.d.ts +62 -0
- package/dist/src/channels/base.js +3 -0
- package/dist/src/channels/registry.d.ts +7 -0
- package/dist/src/channels/registry.js +30 -0
- package/dist/src/channels/runtime.d.ts +60 -0
- package/dist/src/channels/runtime.js +158 -0
- package/dist/src/channels/secret-registry.d.ts +17 -0
- package/dist/src/channels/secret-registry.js +44 -0
- package/dist/src/channels/websocket.d.ts +48 -0
- package/dist/src/channels/websocket.js +314 -0
- package/dist/src/channels/wecom.d.ts +48 -0
- package/dist/src/channels/wecom.js +315 -0
- package/dist/src/commands/manager.d.ts +2 -0
- package/dist/src/commands/manager.js +168 -0
- package/dist/src/config/index.js +5 -1
- package/dist/src/index.js +3 -1
- package/dist/src/manager/prompt.d.ts +2 -0
- package/dist/src/manager/prompt.js +46 -0
- package/dist/src/manager/registry.d.ts +92 -0
- package/dist/src/manager/registry.js +267 -0
- package/dist/src/manager/runtime.d.ts +59 -0
- package/dist/src/manager/runtime.js +534 -0
- package/dist/src/native-fusion/parsers.js +6 -0
- package/dist/src/native-fusion/service.d.ts +1 -0
- package/dist/src/native-fusion/service.js +5 -3
- package/dist/src/native-fusion/types.d.ts +2 -1
- package/dist/src/region.js +7 -19
- package/dist/src/session/handlers/agents.js +1 -1
- package/dist/src/session/handlers/chat.js +121 -2
- package/dist/src/session/handlers/control.js +1 -1
- package/dist/src/session/manager.d.ts +2 -0
- package/dist/src/session/manager.js +16 -0
- package/dist/src/session/projection.d.ts +3 -0
- package/dist/src/session/projection.js +54 -0
- package/dist/src/session/store.d.ts +24 -1
- package/dist/src/session/store.js +66 -3
- package/dist/src/session/types.d.ts +2 -0
- package/package.json +5 -5
|
@@ -4,11 +4,65 @@ import os from 'node:os';
|
|
|
4
4
|
import { createAgent } from '../../agents/adapter.js';
|
|
5
5
|
import { reportLog } from '../../log-reporter.js';
|
|
6
6
|
import { lookupClaudeTranscriptCwd } from '../../native-fusion/parsers.js';
|
|
7
|
+
import { appendMessage, recordSession } from '../store.js';
|
|
8
|
+
import { mergeProjectedSessions } from '../projection.js';
|
|
7
9
|
function extractSummary(text) {
|
|
8
10
|
const newline = text.indexOf('\n');
|
|
9
11
|
const end = newline > 0 ? Math.min(newline, 80) : Math.min(text.length, 80);
|
|
10
12
|
return text.slice(0, end);
|
|
11
13
|
}
|
|
14
|
+
function getNativeSourceAgentType(agentType, modelId) {
|
|
15
|
+
if (agentType !== 'manager')
|
|
16
|
+
return agentType;
|
|
17
|
+
return modelId === 'claude' ? 'claude' : 'codex';
|
|
18
|
+
}
|
|
19
|
+
function sendSessionMessageEvent(runtime, envelope, session) {
|
|
20
|
+
runtime.client.sendEvent({
|
|
21
|
+
type: 'event',
|
|
22
|
+
event: 'session.message',
|
|
23
|
+
payload: {
|
|
24
|
+
sessionId: envelope.sessionId,
|
|
25
|
+
message: envelope,
|
|
26
|
+
session: {
|
|
27
|
+
id: envelope.sessionId,
|
|
28
|
+
agentType: session.agentType,
|
|
29
|
+
agentSessionId: session.agentSessionId ?? null,
|
|
30
|
+
modelId: session.modelId ?? null,
|
|
31
|
+
workDir: session.workDir,
|
|
32
|
+
status: 'active',
|
|
33
|
+
...(session.agentType === 'manager'
|
|
34
|
+
? { externalChannel: runtime.managerRuntime?.getExternalChannelStatus(envelope.sessionId) ?? null }
|
|
35
|
+
: {}),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function sendSessionUpdateEvent(runtime, input) {
|
|
41
|
+
recordSession({
|
|
42
|
+
sessionId: input.sessionId,
|
|
43
|
+
agentType: input.agentType,
|
|
44
|
+
workDir: input.workDir,
|
|
45
|
+
agentSessionId: input.agentSessionId ?? null,
|
|
46
|
+
modelId: input.modelId ?? null,
|
|
47
|
+
});
|
|
48
|
+
runtime.client.sendEvent({
|
|
49
|
+
type: 'event',
|
|
50
|
+
event: 'session.update',
|
|
51
|
+
payload: {
|
|
52
|
+
session: {
|
|
53
|
+
id: input.sessionId,
|
|
54
|
+
agentType: input.agentType,
|
|
55
|
+
agentSessionId: input.agentSessionId ?? null,
|
|
56
|
+
modelId: input.modelId ?? null,
|
|
57
|
+
workDir: input.workDir,
|
|
58
|
+
status: 'active',
|
|
59
|
+
...(input.agentType === 'manager'
|
|
60
|
+
? { externalChannel: runtime.managerRuntime?.getExternalChannelStatus(input.sessionId) ?? null }
|
|
61
|
+
: {}),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
12
66
|
function maybeResolveClaudeImportedWorkDir(agentType, workDir, agentSessionId) {
|
|
13
67
|
if (agentType !== 'claude')
|
|
14
68
|
return workDir;
|
|
@@ -52,6 +106,7 @@ function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
|
|
|
52
106
|
if (event.agentSessionId)
|
|
53
107
|
activeSession.agentSessionId = event.agentSessionId;
|
|
54
108
|
}
|
|
109
|
+
runtime.managerRuntime?.noteAgentEvent(sessionId, event);
|
|
55
110
|
if (event.state !== 'delta') {
|
|
56
111
|
reportLog({
|
|
57
112
|
level: 'info',
|
|
@@ -61,13 +116,54 @@ function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
|
|
|
61
116
|
metadata: { runId: event.runId, seq: event.seq, agentType },
|
|
62
117
|
});
|
|
63
118
|
}
|
|
119
|
+
if (event.state === 'delta' && event.text && !event.thinking) {
|
|
120
|
+
appendMessage(sessionId, {
|
|
121
|
+
id: `agent-${event.runId}-${event.seq}`,
|
|
122
|
+
sessionId,
|
|
123
|
+
role: 'agent',
|
|
124
|
+
ts: Date.now(),
|
|
125
|
+
payload: event.text,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else if (event.state === 'tool-call' || event.state === 'tool-result') {
|
|
129
|
+
appendMessage(sessionId, {
|
|
130
|
+
id: `agent-${event.runId}-${event.seq}`,
|
|
131
|
+
sessionId,
|
|
132
|
+
role: 'agent',
|
|
133
|
+
ts: Date.now(),
|
|
134
|
+
payload: JSON.stringify({
|
|
135
|
+
v: 1,
|
|
136
|
+
type: event.state === 'tool-call' ? 'tool_use' : 'tool_result',
|
|
137
|
+
name: event.name,
|
|
138
|
+
args: event.args,
|
|
139
|
+
result: event.result,
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else if ((event.state === 'error' || event.state === 'aborted') && event.message) {
|
|
144
|
+
appendMessage(sessionId, {
|
|
145
|
+
id: `agent-${event.runId || 'run'}-${event.seq}`,
|
|
146
|
+
sessionId,
|
|
147
|
+
role: 'agent',
|
|
148
|
+
ts: Date.now(),
|
|
149
|
+
payload: event.message,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
64
152
|
const runKey = `${sessionId}:${event.runId}`;
|
|
65
153
|
if (event.state === 'delta' && !event.thinking && event.text) {
|
|
66
154
|
runtime.runTextAcc.set(runKey, (runtime.runTextAcc.get(runKey) ?? '') + event.text);
|
|
67
155
|
}
|
|
68
156
|
if (event.agentSessionId && event.agentSessionId !== emittedAgentSessionId) {
|
|
69
157
|
emittedAgentSessionId = event.agentSessionId;
|
|
70
|
-
runtime.nativeFusion?.noteManagedSourceSession(sessionId, agentType, event.agentSessionId);
|
|
158
|
+
runtime.nativeFusion?.noteManagedSourceSession(sessionId, getNativeSourceAgentType(agentType, event.source), event.agentSessionId);
|
|
159
|
+
if (activeSession) {
|
|
160
|
+
recordSession({
|
|
161
|
+
sessionId,
|
|
162
|
+
agentType,
|
|
163
|
+
workDir: activeSession.workDir,
|
|
164
|
+
agentSessionId: event.agentSessionId,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
71
167
|
runtime.client.sendEvent({
|
|
72
168
|
type: 'event',
|
|
73
169
|
event: 'session.update',
|
|
@@ -186,7 +282,8 @@ export async function handleChatSend(runtime, req) {
|
|
|
186
282
|
return;
|
|
187
283
|
}
|
|
188
284
|
rememberProcessedReqId(runtime, req.id);
|
|
189
|
-
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId } = req.params;
|
|
285
|
+
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection } = req.params;
|
|
286
|
+
mergeProjectedSessions(sessionListProjection);
|
|
190
287
|
if (!sessionId || !text) {
|
|
191
288
|
runtime.processedReqIds.delete(req.id);
|
|
192
289
|
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'sessionId and text are required' });
|
|
@@ -256,15 +353,37 @@ export async function handleChatSend(runtime, req) {
|
|
|
256
353
|
return;
|
|
257
354
|
}
|
|
258
355
|
}
|
|
356
|
+
sendSessionUpdateEvent(runtime, {
|
|
357
|
+
sessionId,
|
|
358
|
+
agentType: requestedAgentType,
|
|
359
|
+
workDir: resolvedWorkDir,
|
|
360
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
361
|
+
modelId,
|
|
362
|
+
});
|
|
259
363
|
session.currentRunId = null;
|
|
260
364
|
session.nextEventSeq = 0;
|
|
261
365
|
runtime.nativeFusion?.registerManagedSend({
|
|
262
366
|
sessionId,
|
|
263
367
|
agentType: requestedAgentType,
|
|
368
|
+
sourceAgentType: getNativeSourceAgentType(requestedAgentType, modelId),
|
|
264
369
|
canonicalMessageId: clientMessageId ?? null,
|
|
265
370
|
sourceSessionKey: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
266
371
|
text,
|
|
267
372
|
});
|
|
373
|
+
const userEnvelope = {
|
|
374
|
+
id: clientMessageId ?? `user-${req.id}`,
|
|
375
|
+
sessionId,
|
|
376
|
+
role: 'user',
|
|
377
|
+
ts: Date.now(),
|
|
378
|
+
payload: text,
|
|
379
|
+
};
|
|
380
|
+
appendMessage(sessionId, userEnvelope);
|
|
381
|
+
sendSessionMessageEvent(runtime, userEnvelope, {
|
|
382
|
+
agentType: requestedAgentType,
|
|
383
|
+
workDir: resolvedWorkDir,
|
|
384
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
385
|
+
modelId,
|
|
386
|
+
});
|
|
268
387
|
reportLog({
|
|
269
388
|
level: 'info',
|
|
270
389
|
sessionId,
|
|
@@ -4,7 +4,7 @@ import { SERVERS } from '../../region.js';
|
|
|
4
4
|
import { loadConfig, saveConfig } from '../../config/index.js';
|
|
5
5
|
export async function handleRegionProbe(runtime, req) {
|
|
6
6
|
const config = loadConfig();
|
|
7
|
-
const currentUrl = config.serverUrl ?? SERVERS.
|
|
7
|
+
const currentUrl = config.serverUrl ?? SERVERS.cn.url;
|
|
8
8
|
const currentRegion = currentUrl.includes('shennian.net') ? 'cn' : 'global';
|
|
9
9
|
const results = await Promise.all(Object.keys(SERVERS).map(async (region) => {
|
|
10
10
|
const { url, label } = SERVERS[region];
|
|
@@ -7,6 +7,7 @@ import '../agents/gemini.js';
|
|
|
7
7
|
import '../agents/cursor.js';
|
|
8
8
|
import '../agents/opencode.js';
|
|
9
9
|
import '../agents/pi.js';
|
|
10
|
+
import '../agents/manager.js';
|
|
10
11
|
export declare function resolveSessionWorkDir(input: string): string;
|
|
11
12
|
export declare class SessionManager {
|
|
12
13
|
private client;
|
|
@@ -19,6 +20,7 @@ export declare class SessionManager {
|
|
|
19
20
|
private runTextAcc;
|
|
20
21
|
/** In-flight chunked uploads: transferId → metadata */
|
|
21
22
|
private pendingTransfers;
|
|
23
|
+
private managerRuntime;
|
|
22
24
|
constructor(client: CliRelayClient, nativeFusion?: NativeSessionFusionService | null, cliVersion?: string | undefined);
|
|
23
25
|
private getRuntime;
|
|
24
26
|
private reloadCustomAgents;
|
|
@@ -10,6 +10,7 @@ import { handleAgentsRefresh, handleModelsRefresh } from './handlers/agents.js';
|
|
|
10
10
|
import { handleChatAbort, handleChatSend } from './handlers/chat.js';
|
|
11
11
|
import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsTransfer, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, } from './handlers/fs.js';
|
|
12
12
|
import { handleRegionProbe, handleRegionSwitch, handleUpgradeSetPolicy } from './handlers/control.js';
|
|
13
|
+
import { ManagerRuntimeService, setManagerRuntimeService } from '../manager/runtime.js';
|
|
13
14
|
// Side-effect imports to register built-in agent adapters.
|
|
14
15
|
import '../agents/claude.js';
|
|
15
16
|
import '../agents/codex.js';
|
|
@@ -19,6 +20,7 @@ import '../agents/cursor.js';
|
|
|
19
20
|
// import '../agents/openclaw.js'
|
|
20
21
|
import '../agents/opencode.js';
|
|
21
22
|
import '../agents/pi.js';
|
|
23
|
+
import '../agents/manager.js';
|
|
22
24
|
import { registerCustomAgent } from '../agents/custom.js';
|
|
23
25
|
const MAX_SESSIONS = 50;
|
|
24
26
|
function isWindowsAbsolutePath(input) {
|
|
@@ -58,10 +60,17 @@ export class SessionManager {
|
|
|
58
60
|
runTextAcc = new Map();
|
|
59
61
|
/** In-flight chunked uploads: transferId → metadata */
|
|
60
62
|
pendingTransfers = new Map();
|
|
63
|
+
managerRuntime;
|
|
61
64
|
constructor(client, nativeFusion = null, cliVersion) {
|
|
62
65
|
this.client = client;
|
|
63
66
|
this.nativeFusion = nativeFusion;
|
|
64
67
|
this.cliVersion = cliVersion;
|
|
68
|
+
this.managerRuntime = new ManagerRuntimeService({
|
|
69
|
+
getRuntime: () => this.getRuntime(),
|
|
70
|
+
dispatchReq: (req) => this.handleReq(req),
|
|
71
|
+
});
|
|
72
|
+
setManagerRuntimeService(this.managerRuntime);
|
|
73
|
+
void this.managerRuntime.start();
|
|
65
74
|
this.reloadCustomAgents();
|
|
66
75
|
}
|
|
67
76
|
getRuntime() {
|
|
@@ -75,6 +84,7 @@ export class SessionManager {
|
|
|
75
84
|
sessions: this.sessions,
|
|
76
85
|
evictIdleSessions: () => this.evictIdleSessions(),
|
|
77
86
|
nativeFusion: this.nativeFusion,
|
|
87
|
+
managerRuntime: this.managerRuntime,
|
|
78
88
|
};
|
|
79
89
|
}
|
|
80
90
|
reloadCustomAgents() {
|
|
@@ -142,6 +152,10 @@ export class SessionManager {
|
|
|
142
152
|
case 'models.refresh':
|
|
143
153
|
await handleModelsRefresh(runtime, req);
|
|
144
154
|
break;
|
|
155
|
+
case 'manager.channel.get':
|
|
156
|
+
case 'manager.channel.upsert':
|
|
157
|
+
await runtime.managerRuntime?.handleAppReq(req);
|
|
158
|
+
break;
|
|
145
159
|
default:
|
|
146
160
|
this.client.sendRes({ type: 'res', id: req.id, ok: false, error: `Unknown method: ${req.method}` });
|
|
147
161
|
}
|
|
@@ -180,5 +194,7 @@ export class SessionManager {
|
|
|
180
194
|
this.sessions.clear();
|
|
181
195
|
this.runTextAcc.clear();
|
|
182
196
|
cleanupPendingTransfers(this.getRuntime());
|
|
197
|
+
void this.managerRuntime.stop();
|
|
198
|
+
setManagerRuntimeService(null);
|
|
183
199
|
}
|
|
184
200
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @arch docs/features/manager-agent.md
|
|
2
|
+
// @test src/__tests__/manager-runtime.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { resolveShennianPath } from '../config/index.js';
|
|
6
|
+
const PROJECTION_FILE = resolveShennianPath('session-list-projection.json');
|
|
7
|
+
function emptyProjection() {
|
|
8
|
+
return { updatedAt: new Date(0).toISOString(), sessions: {} };
|
|
9
|
+
}
|
|
10
|
+
function readProjectionFile() {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(fs.readFileSync(PROJECTION_FILE, 'utf-8'));
|
|
13
|
+
return {
|
|
14
|
+
updatedAt: parsed.updatedAt ?? new Date(0).toISOString(),
|
|
15
|
+
sessions: parsed.sessions ?? {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return emptyProjection();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeProjectionFile(file) {
|
|
23
|
+
fs.mkdirSync(path.dirname(PROJECTION_FILE), { recursive: true });
|
|
24
|
+
fs.writeFileSync(PROJECTION_FILE, JSON.stringify(file, null, 2));
|
|
25
|
+
}
|
|
26
|
+
export function listProjectedSessions() {
|
|
27
|
+
return Object.values(readProjectionFile().sessions);
|
|
28
|
+
}
|
|
29
|
+
export function mergeProjectedSessions(sessions) {
|
|
30
|
+
if (!sessions?.length)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
const file = readProjectionFile();
|
|
34
|
+
for (const session of sessions) {
|
|
35
|
+
if (!session?.id)
|
|
36
|
+
continue;
|
|
37
|
+
if (session.deletedAt) {
|
|
38
|
+
delete file.sessions[session.id];
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const existing = file.sessions[session.id];
|
|
42
|
+
const existingMs = Date.parse(existing?.updatedAt ?? existing?.lastActivityAt ?? '');
|
|
43
|
+
const incomingMs = Date.parse(session.updatedAt ?? session.lastActivityAt ?? '');
|
|
44
|
+
if (!existing || !Number.isFinite(existingMs) || !Number.isFinite(incomingMs) || incomingMs >= existingMs) {
|
|
45
|
+
file.sessions[session.id] = session;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
file.updatedAt = new Date().toISOString();
|
|
49
|
+
writeProjectionFile(file);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Projection is a local cache. Losing it should not break chat delivery.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
import type { Envelope } from '@shennian/wire';
|
|
1
|
+
import type { AgentType, Envelope } from '@shennian/wire';
|
|
2
|
+
export type LocalSessionRecord = {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
agentType: AgentType;
|
|
5
|
+
workDir: string;
|
|
6
|
+
agentSessionId?: string | null;
|
|
7
|
+
modelId?: string | null;
|
|
8
|
+
status?: 'active' | 'completed' | 'failed';
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
lastActivityAt: string;
|
|
12
|
+
lastMessagePreview?: string | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function recordSession(input: {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
agentType: AgentType;
|
|
17
|
+
workDir: string;
|
|
18
|
+
agentSessionId?: string | null;
|
|
19
|
+
modelId?: string | null;
|
|
20
|
+
status?: 'active' | 'completed' | 'failed';
|
|
21
|
+
lastActivityAt?: string;
|
|
22
|
+
lastMessagePreview?: string | null;
|
|
23
|
+
}): void;
|
|
24
|
+
export declare function listSessionRecords(): LocalSessionRecord[];
|
|
2
25
|
export declare function appendMessage(sessionId: string, envelope: Envelope): void;
|
|
3
26
|
export declare function readMessages(sessionId: string, opts?: {
|
|
4
27
|
limit?: number;
|
|
@@ -2,16 +2,79 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { resolveShennianPath } from '../config/index.js';
|
|
4
4
|
const SESSIONS_DIR = resolveShennianPath('sessions');
|
|
5
|
+
const SESSIONS_INDEX_FILE = resolveShennianPath('sessions-index.json');
|
|
5
6
|
function ensureSessionsDir() {
|
|
6
7
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
7
8
|
}
|
|
8
9
|
function sessionFile(sessionId) {
|
|
9
10
|
return path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
|
|
10
11
|
}
|
|
12
|
+
function readSessionIndex() {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(fs.readFileSync(SESSIONS_INDEX_FILE, 'utf-8'));
|
|
15
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeSessionIndex(index) {
|
|
22
|
+
fs.mkdirSync(path.dirname(SESSIONS_INDEX_FILE), { recursive: true });
|
|
23
|
+
fs.writeFileSync(SESSIONS_INDEX_FILE, JSON.stringify(index, null, 2));
|
|
24
|
+
}
|
|
25
|
+
function previewFromEnvelope(envelope) {
|
|
26
|
+
const text = envelope.payload.replace(/\s+/g, ' ').trim();
|
|
27
|
+
if (!text || text.startsWith('{"v":1,"type":"tool'))
|
|
28
|
+
return null;
|
|
29
|
+
return text.length > 120 ? `${text.slice(0, 120)}...` : text;
|
|
30
|
+
}
|
|
31
|
+
export function recordSession(input) {
|
|
32
|
+
try {
|
|
33
|
+
const index = readSessionIndex();
|
|
34
|
+
const existing = index[input.sessionId];
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
index[input.sessionId] = {
|
|
37
|
+
sessionId: input.sessionId,
|
|
38
|
+
agentType: input.agentType,
|
|
39
|
+
workDir: input.workDir,
|
|
40
|
+
agentSessionId: input.agentSessionId ?? existing?.agentSessionId ?? null,
|
|
41
|
+
modelId: input.modelId ?? existing?.modelId ?? null,
|
|
42
|
+
status: input.status ?? existing?.status ?? 'active',
|
|
43
|
+
createdAt: existing?.createdAt ?? now,
|
|
44
|
+
updatedAt: now,
|
|
45
|
+
lastActivityAt: input.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
46
|
+
lastMessagePreview: input.lastMessagePreview ?? existing?.lastMessagePreview ?? null,
|
|
47
|
+
};
|
|
48
|
+
writeSessionIndex(index);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Local session indexing is best-effort; relay delivery remains authoritative.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function listSessionRecords() {
|
|
55
|
+
return Object.values(readSessionIndex());
|
|
56
|
+
}
|
|
11
57
|
export function appendMessage(sessionId, envelope) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
58
|
+
try {
|
|
59
|
+
ensureSessionsDir();
|
|
60
|
+
const line = JSON.stringify(envelope) + '\n';
|
|
61
|
+
fs.appendFileSync(sessionFile(sessionId), line, 'utf-8');
|
|
62
|
+
const index = readSessionIndex();
|
|
63
|
+
const existing = index[sessionId];
|
|
64
|
+
if (existing) {
|
|
65
|
+
const preview = previewFromEnvelope(envelope);
|
|
66
|
+
index[sessionId] = {
|
|
67
|
+
...existing,
|
|
68
|
+
updatedAt: new Date().toISOString(),
|
|
69
|
+
lastActivityAt: new Date(envelope.ts).toISOString(),
|
|
70
|
+
lastMessagePreview: preview ?? existing.lastMessagePreview ?? null,
|
|
71
|
+
};
|
|
72
|
+
writeSessionIndex(index);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Local transcript persistence is best-effort; relay delivery remains authoritative.
|
|
77
|
+
}
|
|
15
78
|
}
|
|
16
79
|
export function readMessages(sessionId, opts) {
|
|
17
80
|
const file = sessionFile(sessionId);
|
|
@@ -2,6 +2,7 @@ import type { AgentType } from '@shennian/wire';
|
|
|
2
2
|
import type { AgentAdapter } from '../agents/adapter.js';
|
|
3
3
|
import type { CliRelayClient } from '../relay/client.js';
|
|
4
4
|
import type { NativeSessionFusionService } from '../native-fusion/service.js';
|
|
5
|
+
import type { ManagerRuntimeService } from '../manager/runtime.js';
|
|
5
6
|
export type ActiveSession = {
|
|
6
7
|
adapter: AgentAdapter;
|
|
7
8
|
workDir: string;
|
|
@@ -32,4 +33,5 @@ export type SessionManagerRuntime = {
|
|
|
32
33
|
sessions: Map<string, ActiveSession>;
|
|
33
34
|
evictIdleSessions: () => void;
|
|
34
35
|
nativeFusion: NativeSessionFusionService | null;
|
|
36
|
+
managerRuntime: ManagerRuntimeService | null;
|
|
35
37
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shennian",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
4
4
|
"description": "Shennian — AI Agent Control Plane CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@mariozechner/pi-agent-core": "^0.64.0",
|
|
38
38
|
"@sinclair/typebox": "^0.34.49",
|
|
39
|
-
"@shennian/wire": "^0.1.3",
|
|
40
39
|
"chalk": "^5.4.1",
|
|
41
40
|
"commander": "^13.1.0",
|
|
42
41
|
"qrcode-terminal": "^0.12.0",
|
|
43
|
-
"ws": "^8.18.1"
|
|
42
|
+
"ws": "^8.18.1",
|
|
43
|
+
"@shennian/wire": "0.1.3"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^20",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"typescript": "^5.9.3"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"build": "tsc",
|
|
54
|
-
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json",
|
|
53
|
+
"build": "tsc && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
54
|
+
"build:publish": "node -e \"const fs=require('node:fs'); fs.rmSync('dist', { recursive: true, force: true }); fs.rmSync('.tsbuildinfo.publish', { force: true })\" && tsc -p tsconfig.publish.json && node -e \"require('node:fs').chmodSync('dist/bin/shennian.js', 0o755)\"",
|
|
55
55
|
"dev": "tsc --watch"
|
|
56
56
|
}
|
|
57
57
|
}
|