shennian 0.2.51 → 0.2.53
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/adapter.d.ts +6 -2
- package/dist/src/agents/claude.d.ts +9 -1
- package/dist/src/agents/claude.js +23 -4
- package/dist/src/agents/codex.d.ts +10 -1
- package/dist/src/agents/codex.js +43 -16
- package/dist/src/agents/external-channel-instructions.d.ts +2 -0
- package/dist/src/agents/external-channel-instructions.js +22 -0
- package/dist/src/agents/manager.js +2 -0
- package/dist/src/agents/model-registry/parsers.js +21 -7
- package/dist/src/agents/pi.d.ts +7 -0
- package/dist/src/agents/pi.js +15 -3
- package/dist/src/agents/platform-instructions.d.ts +3 -2
- package/dist/src/agents/platform-instructions.js +12 -4
- package/dist/src/channels/base.d.ts +8 -0
- package/dist/src/channels/runtime.d.ts +11 -2
- package/dist/src/channels/runtime.js +53 -19
- package/dist/src/commands/daemon.d.ts +1 -1
- package/dist/src/commands/daemon.js +10 -24
- package/dist/src/commands/external.d.ts +2 -0
- package/dist/src/commands/external.js +45 -0
- package/dist/src/commands/pair.js +3 -1
- package/dist/src/index.js +3 -2
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +100 -12
- package/dist/src/native-fusion/parsers.js +46 -5
- package/dist/src/relay/client.d.ts +2 -0
- package/dist/src/relay/client.js +32 -0
- package/dist/src/session/handlers/chat.js +68 -13
- package/dist/src/session/queue.d.ts +1 -0
- package/dist/src/session/queue.js +29 -1
- package/dist/src/session/types.d.ts +3 -1
- package/package.json +1 -1
|
@@ -7,6 +7,10 @@ import path from 'node:path';
|
|
|
7
7
|
import { buildUserMessagePayload, isToolPayload } from '@shennian/wire';
|
|
8
8
|
import { resolveBuiltinCommand, spawnResolvedCommandSync } from '../agents/command-spec.js';
|
|
9
9
|
const MAX_JSONL_LINE_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_NATIVE_SCAN_IGNORED_PATHS = [
|
|
11
|
+
'/root/.claude-mem/observer-session',
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_NATIVE_SCAN_IGNORED_CLAUDE_PROJECT_DIRS = DEFAULT_NATIVE_SCAN_IGNORED_PATHS.map(encodeClaudeProjectDir);
|
|
10
14
|
function normalizeText(text) {
|
|
11
15
|
return stripGitDirectiveArtifacts(text.replace(/\r\n/g, '\n').trim());
|
|
12
16
|
}
|
|
@@ -103,6 +107,33 @@ function readClaudeEventCwd(parsed) {
|
|
|
103
107
|
function isClaudeSubagentTranscript(filePath) {
|
|
104
108
|
return path.basename(path.dirname(filePath)) === 'subagents';
|
|
105
109
|
}
|
|
110
|
+
function normalizePathForCompare(filePath) {
|
|
111
|
+
const normalized = path.resolve(filePath);
|
|
112
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
113
|
+
}
|
|
114
|
+
function isSameOrChildPath(filePath, parentPath) {
|
|
115
|
+
const normalizedFilePath = normalizePathForCompare(filePath);
|
|
116
|
+
const normalizedParentPath = normalizePathForCompare(parentPath);
|
|
117
|
+
return normalizedFilePath === normalizedParentPath
|
|
118
|
+
|| normalizedFilePath.startsWith(normalizedParentPath + path.sep);
|
|
119
|
+
}
|
|
120
|
+
function encodeClaudeProjectDir(filePath) {
|
|
121
|
+
return path.resolve(filePath).replace(/\//g, '-');
|
|
122
|
+
}
|
|
123
|
+
function shouldIgnoreNativeScanPath(filePath) {
|
|
124
|
+
return DEFAULT_NATIVE_SCAN_IGNORED_PATHS.some((ignoredPath) => isSameOrChildPath(filePath, ignoredPath));
|
|
125
|
+
}
|
|
126
|
+
function shouldIgnoreClaudeProjectDir(projectDirName) {
|
|
127
|
+
return DEFAULT_NATIVE_SCAN_IGNORED_CLAUDE_PROJECT_DIRS.includes(projectDirName);
|
|
128
|
+
}
|
|
129
|
+
function shouldIgnoreClaudeTranscriptPath(filePath) {
|
|
130
|
+
const root = path.join(os.homedir(), '.claude', 'projects');
|
|
131
|
+
const relative = path.relative(root, filePath);
|
|
132
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
133
|
+
return false;
|
|
134
|
+
const [projectDirName] = relative.split(path.sep);
|
|
135
|
+
return projectDirName ? shouldIgnoreClaudeProjectDir(projectDirName) : false;
|
|
136
|
+
}
|
|
106
137
|
function makeCursor(filePath, offset) {
|
|
107
138
|
return `${filePath}:${offset}`;
|
|
108
139
|
}
|
|
@@ -715,8 +746,11 @@ export function listClaudeTranscriptFiles() {
|
|
|
715
746
|
const walk = (dir) => {
|
|
716
747
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
717
748
|
const full = path.join(dir, entry.name);
|
|
718
|
-
if (entry.isDirectory())
|
|
749
|
+
if (entry.isDirectory()) {
|
|
750
|
+
if (dir === root && shouldIgnoreClaudeProjectDir(entry.name))
|
|
751
|
+
continue;
|
|
719
752
|
walk(full);
|
|
753
|
+
}
|
|
720
754
|
else if (entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
721
755
|
files.push(full);
|
|
722
756
|
}
|
|
@@ -805,12 +839,16 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
|
|
|
805
839
|
return { nextOffset, events };
|
|
806
840
|
}
|
|
807
841
|
export function parseClaudeTranscriptChunk(filePath, startOffset) {
|
|
808
|
-
|
|
809
|
-
|
|
842
|
+
const fileSize = fs.statSync(filePath).size;
|
|
843
|
+
if (isClaudeSubagentTranscript(filePath) || shouldIgnoreClaudeTranscriptPath(filePath)) {
|
|
844
|
+
return { nextOffset: fileSize, events: [] };
|
|
810
845
|
}
|
|
811
846
|
const events = [];
|
|
812
847
|
const sourceSessionKey = path.basename(filePath, '.jsonl');
|
|
813
848
|
const fallbackWorkDir = relativeProjectDir(path.dirname(filePath).replace(path.join(os.homedir(), '.claude', 'projects') + path.sep, ''));
|
|
849
|
+
if (shouldIgnoreNativeScanPath(fallbackWorkDir)) {
|
|
850
|
+
return { nextOffset: fileSize, events: [] };
|
|
851
|
+
}
|
|
814
852
|
let title = '';
|
|
815
853
|
const nextOffset = readJsonlLines(filePath, startOffset, (line, lineOffset) => {
|
|
816
854
|
const parsed = safeParse(line);
|
|
@@ -820,6 +858,9 @@ export function parseClaudeTranscriptChunk(filePath, startOffset) {
|
|
|
820
858
|
const type = typeof parsed.type === 'string' ? parsed.type : '';
|
|
821
859
|
if (!ts || !type)
|
|
822
860
|
return;
|
|
861
|
+
const eventCwd = readClaudeEventCwd(parsed);
|
|
862
|
+
if (eventCwd && shouldIgnoreNativeScanPath(eventCwd))
|
|
863
|
+
return;
|
|
823
864
|
if (type === 'user') {
|
|
824
865
|
const message = typeof parsed.message === 'object' && parsed.message !== null
|
|
825
866
|
? parsed.message
|
|
@@ -839,7 +880,7 @@ export function parseClaudeTranscriptChunk(filePath, startOffset) {
|
|
|
839
880
|
ts,
|
|
840
881
|
payload: text,
|
|
841
882
|
title,
|
|
842
|
-
workDir:
|
|
883
|
+
workDir: eventCwd ?? fallbackWorkDir,
|
|
843
884
|
});
|
|
844
885
|
}
|
|
845
886
|
else if (type === 'assistant') {
|
|
@@ -867,7 +908,7 @@ export function parseClaudeTranscriptChunk(filePath, startOffset) {
|
|
|
867
908
|
payload: text,
|
|
868
909
|
title,
|
|
869
910
|
modelId,
|
|
870
|
-
workDir:
|
|
911
|
+
workDir: eventCwd ?? fallbackWorkDir,
|
|
871
912
|
});
|
|
872
913
|
}
|
|
873
914
|
});
|
|
@@ -33,11 +33,13 @@ export declare class CliRelayClient {
|
|
|
33
33
|
/** Buffered agent events awaiting server ack, keyed by event id */
|
|
34
34
|
private sendBuffer;
|
|
35
35
|
private pendingAcks;
|
|
36
|
+
private pendingRequests;
|
|
36
37
|
constructor(options: CliRelayOptions);
|
|
37
38
|
connect(): void;
|
|
38
39
|
disconnect(): void;
|
|
39
40
|
sendRes(res: ResFrame): void;
|
|
40
41
|
sendEvent(event: EventFrame): void;
|
|
42
|
+
sendReq(req: ReqFrame, timeoutMs?: number): Promise<ResFrame>;
|
|
41
43
|
sendBufferedEvent(event: EventFrame, timeoutMs?: number): Promise<void>;
|
|
42
44
|
/**
|
|
43
45
|
* Send an agent event with at-least-once delivery guarantee.
|
package/dist/src/relay/client.js
CHANGED
|
@@ -26,6 +26,7 @@ export class CliRelayClient {
|
|
|
26
26
|
/** Buffered agent events awaiting server ack, keyed by event id */
|
|
27
27
|
sendBuffer = new Map();
|
|
28
28
|
pendingAcks = new Map();
|
|
29
|
+
pendingRequests = new Map();
|
|
29
30
|
constructor(options) {
|
|
30
31
|
this.options = options;
|
|
31
32
|
}
|
|
@@ -121,6 +122,26 @@ export class CliRelayClient {
|
|
|
121
122
|
event.traceId = generateTraceId();
|
|
122
123
|
this.ws.send(JSON.stringify(event));
|
|
123
124
|
}
|
|
125
|
+
sendReq(req, timeoutMs = 60_000) {
|
|
126
|
+
if (this.state !== 'connected' || !this.ws) {
|
|
127
|
+
return Promise.reject(new Error('Relay is not connected'));
|
|
128
|
+
}
|
|
129
|
+
if (!req.traceId)
|
|
130
|
+
req.traceId = generateTraceId();
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const existing = this.pendingRequests.get(req.id);
|
|
133
|
+
if (existing) {
|
|
134
|
+
clearTimeout(existing.timer);
|
|
135
|
+
existing.reject(new Error('Superseded by a newer relay request'));
|
|
136
|
+
}
|
|
137
|
+
const timer = setTimeout(() => {
|
|
138
|
+
this.pendingRequests.delete(req.id);
|
|
139
|
+
reject(new Error('Relay request timed out'));
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
this.pendingRequests.set(req.id, { resolve, reject, timer });
|
|
142
|
+
this.ws?.send(JSON.stringify(req));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
124
145
|
sendBufferedEvent(event, timeoutMs = 120_000) {
|
|
125
146
|
if (!event.traceId)
|
|
126
147
|
event.traceId = generateTraceId();
|
|
@@ -172,6 +193,12 @@ export class CliRelayClient {
|
|
|
172
193
|
else
|
|
173
194
|
pending.reject(new Error(frame.error ?? 'Relay event failed'));
|
|
174
195
|
}
|
|
196
|
+
const pendingRequest = this.pendingRequests.get(frame.id);
|
|
197
|
+
if (pendingRequest) {
|
|
198
|
+
this.pendingRequests.delete(frame.id);
|
|
199
|
+
clearTimeout(pendingRequest.timer);
|
|
200
|
+
pendingRequest.resolve(frame);
|
|
201
|
+
}
|
|
175
202
|
return;
|
|
176
203
|
}
|
|
177
204
|
if (frame.type === 'req') {
|
|
@@ -300,6 +327,11 @@ export class CliRelayClient {
|
|
|
300
327
|
// already closed
|
|
301
328
|
}
|
|
302
329
|
}
|
|
330
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
331
|
+
clearTimeout(pending.timer);
|
|
332
|
+
pending.reject(new Error('Relay client disconnected'));
|
|
333
|
+
this.pendingRequests.delete(id);
|
|
334
|
+
}
|
|
303
335
|
if (rejectAll) {
|
|
304
336
|
for (const [id, pending] of this.pendingAcks) {
|
|
305
337
|
clearTimeout(pending.timer);
|
|
@@ -6,6 +6,7 @@ import { reportLog } from '../../log-reporter.js';
|
|
|
6
6
|
import { lookupClaudeTranscriptCwd } from '../../native-fusion/parsers.js';
|
|
7
7
|
import { appendMessage, recordSession } from '../store.js';
|
|
8
8
|
import { mergeProjectedSessions } from '../projection.js';
|
|
9
|
+
import { getManagerRuntimeService } from '../../manager/runtime.js';
|
|
9
10
|
function extractSummary(text) {
|
|
10
11
|
const newline = text.indexOf('\n');
|
|
11
12
|
const end = newline > 0 ? Math.min(newline, 80) : Math.min(text.length, 80);
|
|
@@ -30,9 +31,7 @@ function sendSessionMessageEvent(runtime, envelope, session) {
|
|
|
30
31
|
modelId: session.modelId ?? null,
|
|
31
32
|
workDir: session.workDir,
|
|
32
33
|
status: 'active',
|
|
33
|
-
|
|
34
|
-
? { externalChannel: runtime.managerRuntime?.getExternalChannelStatus(envelope.sessionId) ?? null }
|
|
35
|
-
: {}),
|
|
34
|
+
externalChannel: getSessionExternalChannel(runtime, envelope.sessionId, session.agentType),
|
|
36
35
|
},
|
|
37
36
|
},
|
|
38
37
|
});
|
|
@@ -56,13 +55,53 @@ function sendSessionUpdateEvent(runtime, input) {
|
|
|
56
55
|
modelId: input.modelId ?? null,
|
|
57
56
|
workDir: input.workDir,
|
|
58
57
|
status: 'active',
|
|
59
|
-
|
|
60
|
-
? { externalChannel: runtime.managerRuntime?.getExternalChannelStatus(input.sessionId) ?? null }
|
|
61
|
-
: {}),
|
|
58
|
+
externalChannel: getSessionExternalChannel(runtime, input.sessionId, input.agentType),
|
|
62
59
|
},
|
|
63
60
|
},
|
|
64
61
|
});
|
|
65
62
|
}
|
|
63
|
+
function normalizeExternalChannel(value) {
|
|
64
|
+
if (!value || typeof value !== 'object')
|
|
65
|
+
return null;
|
|
66
|
+
const raw = value;
|
|
67
|
+
return {
|
|
68
|
+
configured: raw.configured === undefined ? undefined : Boolean(raw.configured),
|
|
69
|
+
connected: Boolean(raw.connected),
|
|
70
|
+
type: typeof raw.type === 'string' ? raw.type : null,
|
|
71
|
+
channelId: typeof raw.channelId === 'string' ? raw.channelId : null,
|
|
72
|
+
name: typeof raw.name === 'string' ? raw.name : null,
|
|
73
|
+
canReply: raw.canReply === undefined || raw.canReply === null ? null : Boolean(raw.canReply),
|
|
74
|
+
systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function externalChannelEnabled(channel) {
|
|
78
|
+
return Boolean(channel?.configured ?? channel?.connected);
|
|
79
|
+
}
|
|
80
|
+
function externalChannelEnv(sessionId, channel) {
|
|
81
|
+
if (!externalChannelEnabled(channel))
|
|
82
|
+
return {};
|
|
83
|
+
const service = getManagerRuntimeService();
|
|
84
|
+
const injected = service?.getInjectedEnv(sessionId, null, process.cwd(), 'external') ?? {};
|
|
85
|
+
return {
|
|
86
|
+
...injected,
|
|
87
|
+
SHENNIAN_EXTERNAL_SESSION_ID: sessionId,
|
|
88
|
+
SHENNIAN_MANAGER_SESSION_ID: sessionId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function configureAdapterForSession(adapter, sessionId, channel) {
|
|
92
|
+
adapter.configure?.({
|
|
93
|
+
externalChannel: channel ?? null,
|
|
94
|
+
env: externalChannelEnv(sessionId, channel),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function getSessionExternalChannel(runtime, sessionId, agentType) {
|
|
98
|
+
const active = runtime.sessions.get(sessionId);
|
|
99
|
+
if (active?.externalChannel)
|
|
100
|
+
return active.externalChannel;
|
|
101
|
+
if (agentType === 'manager')
|
|
102
|
+
return runtime.managerRuntime?.getExternalChannelStatus(sessionId) ?? null;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
66
105
|
function maybeResolveClaudeImportedWorkDir(agentType, workDir, agentSessionId) {
|
|
67
106
|
if (agentType !== 'claude')
|
|
68
107
|
return workDir;
|
|
@@ -239,11 +278,12 @@ async function disposeSession(session) {
|
|
|
239
278
|
session.adapter.removeAllListeners();
|
|
240
279
|
await session.adapter.stop().catch(() => { });
|
|
241
280
|
}
|
|
242
|
-
async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDir, incomingAgentSid) {
|
|
281
|
+
async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDir, incomingAgentSid, externalChannel) {
|
|
243
282
|
runtime.evictIdleSessions();
|
|
244
283
|
const adapter = createAgent(agentType);
|
|
245
284
|
if (!adapter)
|
|
246
285
|
throw new Error(`Unsupported agent: ${agentType}`);
|
|
286
|
+
configureAdapterForSession(adapter, sessionId, externalChannel);
|
|
247
287
|
await adapter.start(sessionId, resolvedWorkDir, incomingAgentSid);
|
|
248
288
|
const session = {
|
|
249
289
|
adapter,
|
|
@@ -254,6 +294,8 @@ async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDi
|
|
|
254
294
|
currentRunId: null,
|
|
255
295
|
nextEventSeq: 0,
|
|
256
296
|
pendingTextEvent: null,
|
|
297
|
+
externalChannel: externalChannel ?? null,
|
|
298
|
+
externalChannelEnv: externalChannelEnv(sessionId, externalChannel),
|
|
257
299
|
};
|
|
258
300
|
runtime.sessions.set(sessionId, session);
|
|
259
301
|
bindAdapterEvents(runtime, sessionId, agentType, adapter);
|
|
@@ -283,20 +325,27 @@ export async function handleChatSend(runtime, req) {
|
|
|
283
325
|
return;
|
|
284
326
|
}
|
|
285
327
|
rememberProcessedReqId(runtime, req.id);
|
|
286
|
-
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
|
|
328
|
+
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, reasoningEffort, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
|
|
287
329
|
mergeProjectedSessions(sessionListProjection);
|
|
330
|
+
const incomingExternalChannel = normalizeExternalChannel(req.params.externalChannel);
|
|
288
331
|
if (!sessionId || !text) {
|
|
289
332
|
runtime.processedReqIds.delete(req.id);
|
|
290
333
|
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'sessionId and text are required' });
|
|
291
334
|
return;
|
|
292
335
|
}
|
|
293
336
|
const requestedAgentType = agentType;
|
|
337
|
+
const resolvedReasoningEffort = (requestedAgentType === 'claude' || requestedAgentType === 'codex') &&
|
|
338
|
+
typeof reasoningEffort === 'string' &&
|
|
339
|
+
reasoningEffort.trim()
|
|
340
|
+
? reasoningEffort.trim()
|
|
341
|
+
: undefined;
|
|
294
342
|
const resolvedWorkDir = runtime.resolvePath(maybeResolveClaudeImportedWorkDir(requestedAgentType, workDir || os.homedir(), incomingAgentSid) || os.homedir());
|
|
295
343
|
let session = runtime.sessions.get(sessionId);
|
|
296
344
|
if (session) {
|
|
297
345
|
session.lastActiveAt = Date.now();
|
|
298
346
|
const sessionDrifted = session.agentType !== requestedAgentType ||
|
|
299
|
-
session.workDir !== resolvedWorkDir
|
|
347
|
+
session.workDir !== resolvedWorkDir ||
|
|
348
|
+
JSON.stringify(session.externalChannel ?? null) !== JSON.stringify(incomingExternalChannel ?? null);
|
|
300
349
|
if (sessionDrifted) {
|
|
301
350
|
runtime.sessions.delete(sessionId);
|
|
302
351
|
try {
|
|
@@ -326,7 +375,7 @@ export async function handleChatSend(runtime, req) {
|
|
|
326
375
|
}
|
|
327
376
|
if (!session) {
|
|
328
377
|
try {
|
|
329
|
-
session = await createActiveSession(runtime, sessionId, requestedAgentType, resolvedWorkDir, incomingAgentSid);
|
|
378
|
+
session = await createActiveSession(runtime, sessionId, requestedAgentType, resolvedWorkDir, incomingAgentSid, incomingExternalChannel);
|
|
330
379
|
}
|
|
331
380
|
catch (err) {
|
|
332
381
|
const message = err instanceof Error && err.message.startsWith('Unsupported agent:')
|
|
@@ -365,7 +414,7 @@ export async function handleChatSend(runtime, req) {
|
|
|
365
414
|
level: 'info',
|
|
366
415
|
sessionId,
|
|
367
416
|
wsEvent: 'chat.send.start',
|
|
368
|
-
metadata: { reqId: req.id, agentType: requestedAgentType, modelId },
|
|
417
|
+
metadata: { reqId: req.id, agentType: requestedAgentType, modelId, reasoningEffort: resolvedReasoningEffort },
|
|
369
418
|
});
|
|
370
419
|
const markAccepted = () => {
|
|
371
420
|
sendSessionUpdateEvent(runtime, {
|
|
@@ -424,7 +473,10 @@ export async function handleChatSend(runtime, req) {
|
|
|
424
473
|
};
|
|
425
474
|
if (waitForDispatch) {
|
|
426
475
|
try {
|
|
427
|
-
|
|
476
|
+
if (resolvedReasoningEffort)
|
|
477
|
+
await session.adapter.send(text, modelId, resolvedReasoningEffort);
|
|
478
|
+
else
|
|
479
|
+
await session.adapter.send(text, modelId);
|
|
428
480
|
reportLog({
|
|
429
481
|
level: 'info',
|
|
430
482
|
sessionId,
|
|
@@ -454,7 +506,10 @@ export async function handleChatSend(runtime, req) {
|
|
|
454
506
|
wsEvent: 'chat.send.res',
|
|
455
507
|
metadata: { reqId: req.id, ok: true },
|
|
456
508
|
});
|
|
457
|
-
|
|
509
|
+
const sendPromise = resolvedReasoningEffort
|
|
510
|
+
? session.adapter.send(text, modelId, resolvedReasoningEffort)
|
|
511
|
+
: session.adapter.send(text, modelId);
|
|
512
|
+
void sendPromise
|
|
458
513
|
.then(() => {
|
|
459
514
|
reportLog({
|
|
460
515
|
level: 'info',
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { resolveShennianPath } from '../config/index.js';
|
|
6
|
+
import { mergeProjectedSessions } from './projection.js';
|
|
6
7
|
const QUEUE_FILE = resolveShennianPath('chat-queue.json');
|
|
7
8
|
function emptyQueue() {
|
|
8
9
|
return { sessions: {} };
|
|
@@ -53,8 +54,11 @@ function queueMessageFromParams(params) {
|
|
|
53
54
|
workDir: params.workDir,
|
|
54
55
|
agentSessionId: params.agentSessionId ?? null,
|
|
55
56
|
modelId: params.modelId ?? null,
|
|
57
|
+
reasoningEffort: params.reasoningEffort ?? null,
|
|
56
58
|
clientMessageId: params.clientMessageId ?? null,
|
|
57
59
|
attachments: normalizeAttachments(params.attachments),
|
|
60
|
+
externalChannel: params.externalChannel ?? null,
|
|
61
|
+
origin: params.origin,
|
|
58
62
|
createdAt: timestamp,
|
|
59
63
|
updatedAt: timestamp,
|
|
60
64
|
};
|
|
@@ -75,6 +79,7 @@ export class ChatQueueManager {
|
|
|
75
79
|
async handleEnqueue(req) {
|
|
76
80
|
const runtime = this.opts.getRuntime();
|
|
77
81
|
const params = req.params;
|
|
82
|
+
mergeProjectedSessions(params.sessionListProjection);
|
|
78
83
|
if (!params.sessionId || !params.text || !params.agentType || !params.workDir) {
|
|
79
84
|
runtime.client.sendRes({
|
|
80
85
|
type: 'res',
|
|
@@ -207,6 +212,9 @@ export class ChatQueueManager {
|
|
|
207
212
|
this.broadcast(sessionId);
|
|
208
213
|
return;
|
|
209
214
|
}
|
|
215
|
+
const dispatchMessage = next.origin === 'external'
|
|
216
|
+
? this.mergeExternalMessages(next, pending)
|
|
217
|
+
: next;
|
|
210
218
|
if (pending.length)
|
|
211
219
|
queue.sessions[sessionId] = pending;
|
|
212
220
|
else
|
|
@@ -215,12 +223,30 @@ export class ChatQueueManager {
|
|
|
215
223
|
this.broadcast(sessionId);
|
|
216
224
|
this.draining.add(sessionId);
|
|
217
225
|
try {
|
|
218
|
-
await this.dispatchQueuedMessage(
|
|
226
|
+
await this.dispatchQueuedMessage(dispatchMessage);
|
|
219
227
|
}
|
|
220
228
|
finally {
|
|
221
229
|
this.draining.delete(sessionId);
|
|
222
230
|
}
|
|
223
231
|
}
|
|
232
|
+
mergeExternalMessages(first, pending) {
|
|
233
|
+
const batch = [first];
|
|
234
|
+
while (pending[0]?.origin === 'external') {
|
|
235
|
+
batch.push(pending.shift());
|
|
236
|
+
}
|
|
237
|
+
if (batch.length === 1)
|
|
238
|
+
return first;
|
|
239
|
+
return {
|
|
240
|
+
...first,
|
|
241
|
+
id: `external-batch-${first.id}`,
|
|
242
|
+
text: batch.map((message, index) => {
|
|
243
|
+
const label = batch.length > 1 ? `外部消息 ${index + 1}/${batch.length}` : '外部消息';
|
|
244
|
+
return `${label}\n${message.text}`;
|
|
245
|
+
}).join('\n\n'),
|
|
246
|
+
attachments: batch.flatMap((message) => message.attachments ?? []),
|
|
247
|
+
updatedAt: nowIso(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
224
250
|
async dispatchQueuedMessage(message) {
|
|
225
251
|
await this.opts.dispatchReq({
|
|
226
252
|
type: 'req',
|
|
@@ -233,8 +259,10 @@ export class ChatQueueManager {
|
|
|
233
259
|
workDir: message.workDir,
|
|
234
260
|
agentSessionId: message.agentSessionId ?? null,
|
|
235
261
|
modelId: message.modelId ?? undefined,
|
|
262
|
+
reasoningEffort: message.reasoningEffort ?? undefined,
|
|
236
263
|
clientMessageId: message.clientMessageId ?? message.id,
|
|
237
264
|
attachments: message.attachments,
|
|
265
|
+
externalChannel: message.externalChannel,
|
|
238
266
|
},
|
|
239
267
|
});
|
|
240
268
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentType } from '@shennian/wire';
|
|
1
|
+
import type { AgentType, ExternalChannelSessionStatus } 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';
|
|
@@ -17,6 +17,8 @@ export type ActiveSession = {
|
|
|
17
17
|
text: string;
|
|
18
18
|
thinking: boolean;
|
|
19
19
|
} | null;
|
|
20
|
+
externalChannel?: ExternalChannelSessionStatus | null;
|
|
21
|
+
externalChannelEnv?: NodeJS.ProcessEnv;
|
|
20
22
|
};
|
|
21
23
|
export type PendingTransfer = {
|
|
22
24
|
tempPath: string;
|