shennian 0.2.48 → 0.2.50
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.js +7 -4
- package/dist/src/agents/codex.js +4 -2
- package/dist/src/agents/platform-instructions.d.ts +3 -0
- package/dist/src/agents/platform-instructions.js +50 -0
- package/dist/src/channels/secret-registry.js +3 -1
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/commands/manager.js +50 -2
- package/dist/src/manager/prompt.d.ts +1 -1
- package/dist/src/manager/prompt.js +6 -0
- package/dist/src/manager/runtime.d.ts +1 -0
- package/dist/src/manager/runtime.js +59 -1
- package/dist/src/session/handlers/chat.js +80 -40
- package/dist/src/session/manager.d.ts +1 -0
- package/dist/src/session/manager.js +19 -0
- package/dist/src/session/queue.d.ts +20 -0
- package/dist/src/session/queue.js +248 -0
- package/dist/src/session/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
4
4
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
5
5
|
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
6
|
+
import { buildPlatformInstructions } from './platform-instructions.js';
|
|
6
7
|
export function normalizeClaudeModelId(modelId) {
|
|
7
8
|
const trimmed = modelId?.trim();
|
|
8
9
|
return trimmed || 'default';
|
|
@@ -34,8 +35,9 @@ export class ClaudeAdapter extends AgentAdapter {
|
|
|
34
35
|
this.runId = randomUUID();
|
|
35
36
|
this.resetRunState();
|
|
36
37
|
const args = ['-p', text, '--output-format', 'stream-json', '--verbose'];
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd());
|
|
39
|
+
if (systemPrompt) {
|
|
40
|
+
args.push('--append-system-prompt', systemPrompt);
|
|
39
41
|
}
|
|
40
42
|
if (this.options.hidden) {
|
|
41
43
|
args.push('--name', 'Shennian Manager Substrate');
|
|
@@ -55,8 +57,9 @@ export class ClaudeAdapter extends AgentAdapter {
|
|
|
55
57
|
this.runId = randomUUID();
|
|
56
58
|
this.resetRunState();
|
|
57
59
|
const resumeArgs = ['--resume', agentSessionId, '--output-format', 'stream-json', '--verbose'];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd());
|
|
61
|
+
if (systemPrompt) {
|
|
62
|
+
resumeArgs.push('--append-system-prompt', systemPrompt);
|
|
60
63
|
}
|
|
61
64
|
if (this.options.hidden) {
|
|
62
65
|
resumeArgs.push('--name', 'Shennian Manager Substrate');
|
package/dist/src/agents/codex.js
CHANGED
|
@@ -5,6 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
6
6
|
import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
|
|
7
7
|
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
8
|
+
import { ensurePlatformInstructionsFile } from './platform-instructions.js';
|
|
8
9
|
export class CodexAdapter extends AgentAdapter {
|
|
9
10
|
options;
|
|
10
11
|
type = 'codex';
|
|
@@ -125,8 +126,9 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
127
128
|
const args = ['app-server', '--listen', 'stdio://'];
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
const modelInstructionsFile = this.options.modelInstructionsFile ?? ensurePlatformInstructionsFile(this.workDir ?? process.cwd());
|
|
130
|
+
if (modelInstructionsFile) {
|
|
131
|
+
args.push('-c', `model_instructions_file=${JSON.stringify(modelInstructionsFile)}`);
|
|
130
132
|
}
|
|
131
133
|
const proc = spawnResolvedCommand(spec, args, {
|
|
132
134
|
cwd: this.workDir ?? undefined,
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const PLATFORM_OUTPUT_INSTRUCTIONS = "## Shennian Output Instructions\n\nWhen you mention a local file that the user may want to open in Shennian, format it as a Markdown link using the exact local path:\n\n- Use [filename.ext](</absolute/path/to/filename.ext>) for absolute Unix, macOS, or Windows paths.\n- Use [filename.ext](<relative/path/to/filename.ext>) for paths relative to the current working directory.\n- Do not use file:// URLs for local files.\n- Do not put user-openable file paths only inside code blocks.\n- Keep normal http:// and https:// links unchanged.";
|
|
2
|
+
export declare function buildPlatformInstructions(workDir: string): string;
|
|
3
|
+
export declare function ensurePlatformInstructionsFile(workDir: string): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/agent-adapters.md#平台输出规范
|
|
2
|
+
// @test src/__tests__/platform-instructions.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { resolveShennianPath } from '../config/index.js';
|
|
7
|
+
export const PLATFORM_OUTPUT_INSTRUCTIONS = `## Shennian Output Instructions
|
|
8
|
+
|
|
9
|
+
When you mention a local file that the user may want to open in Shennian, format it as a Markdown link using the exact local path:
|
|
10
|
+
|
|
11
|
+
- Use [filename.ext](</absolute/path/to/filename.ext>) for absolute Unix, macOS, or Windows paths.
|
|
12
|
+
- Use [filename.ext](<relative/path/to/filename.ext>) for paths relative to the current working directory.
|
|
13
|
+
- Do not use file:// URLs for local files.
|
|
14
|
+
- Do not put user-openable file paths only inside code blocks.
|
|
15
|
+
- Keep normal http:// and https:// links unchanged.`;
|
|
16
|
+
function readProjectAgentsMd(workDir) {
|
|
17
|
+
const agentsPath = path.join(workDir, 'AGENTS.md');
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(agentsPath, 'utf8').trim();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function buildPlatformInstructions(workDir) {
|
|
26
|
+
const projectInstructions = readProjectAgentsMd(workDir);
|
|
27
|
+
const sections = [
|
|
28
|
+
'# Shennian Agent Instructions',
|
|
29
|
+
projectInstructions
|
|
30
|
+
? `## Project Instructions\n\n${projectInstructions}`
|
|
31
|
+
: '',
|
|
32
|
+
PLATFORM_OUTPUT_INSTRUCTIONS,
|
|
33
|
+
].filter(Boolean);
|
|
34
|
+
return `${sections.join('\n\n')}\n`;
|
|
35
|
+
}
|
|
36
|
+
export function ensurePlatformInstructionsFile(workDir) {
|
|
37
|
+
const key = createHash('sha256').update(path.resolve(workDir)).digest('hex').slice(0, 16);
|
|
38
|
+
const filePath = resolveShennianPath('runtime', 'agent-instructions', `${key}.md`);
|
|
39
|
+
const content = buildPlatformInstructions(workDir);
|
|
40
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
41
|
+
try {
|
|
42
|
+
if (fs.readFileSync(filePath, 'utf8') === content)
|
|
43
|
+
return filePath;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Write the generated instruction file below.
|
|
47
|
+
}
|
|
48
|
+
fs.writeFileSync(filePath, content);
|
|
49
|
+
return filePath;
|
|
50
|
+
}
|
|
@@ -550,7 +550,7 @@ export function installService() {
|
|
|
550
550
|
const spec = resolveCurrentServiceLaunchSpec();
|
|
551
551
|
if (spec.mode === 'direct' && isEphemeralCliPath(SHENNIAN_SCRIPT)) {
|
|
552
552
|
console.warn(chalk.yellow('⚠ Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n' +
|
|
553
|
-
' Run `npm install -g shennian` for reliable auto-start.'));
|
|
553
|
+
' Run `npm install -g shennian@latest` for reliable auto-start.'));
|
|
554
554
|
}
|
|
555
555
|
switch (platform) {
|
|
556
556
|
case 'darwin': {
|
|
@@ -78,12 +78,60 @@ export function registerManagerCommand(program) {
|
|
|
78
78
|
});
|
|
79
79
|
sessions
|
|
80
80
|
.command('send')
|
|
81
|
-
.description('
|
|
81
|
+
.description('Queue a message for a managed worker; runs immediately if idle')
|
|
82
82
|
.requiredOption('--session-id <id>', 'Worker session id')
|
|
83
83
|
.option('--message <text>', 'Message text')
|
|
84
84
|
.option('--message-file <path>', 'Read message from file')
|
|
85
|
+
.option('--direct', 'Bypass queue and send immediately')
|
|
85
86
|
.action(async (opts) => {
|
|
86
|
-
await ipc('/sessions/send', {
|
|
87
|
+
await ipc('/sessions/send', {
|
|
88
|
+
sessionId: opts.sessionId,
|
|
89
|
+
message: readMessage(opts),
|
|
90
|
+
enqueue: !opts.direct,
|
|
91
|
+
});
|
|
92
|
+
console.log('ok');
|
|
93
|
+
});
|
|
94
|
+
const queue = sessions.command('queue').description('Inspect and edit pending worker messages');
|
|
95
|
+
queue
|
|
96
|
+
.command('list')
|
|
97
|
+
.description('List pending messages for a managed worker')
|
|
98
|
+
.requiredOption('--session-id <id>', 'Worker session id')
|
|
99
|
+
.option('--json', 'Print JSON')
|
|
100
|
+
.action(async (opts) => {
|
|
101
|
+
const result = await ipc('/sessions/queue', { sessionId: opts.sessionId });
|
|
102
|
+
if (opts.json)
|
|
103
|
+
printJson(result);
|
|
104
|
+
else {
|
|
105
|
+
const pending = result.queue?.pending ?? [];
|
|
106
|
+
for (const message of pending)
|
|
107
|
+
console.log(`${message.id}\t${message.text.replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
queue
|
|
111
|
+
.command('edit')
|
|
112
|
+
.description('Edit a pending worker message')
|
|
113
|
+
.requiredOption('--session-id <id>', 'Worker session id')
|
|
114
|
+
.requiredOption('--message-id <id>', 'Queued message id')
|
|
115
|
+
.option('--message <text>', 'Replacement message text')
|
|
116
|
+
.option('--message-file <path>', 'Read replacement message from file')
|
|
117
|
+
.action(async (opts) => {
|
|
118
|
+
await ipc('/sessions/queue/edit', {
|
|
119
|
+
sessionId: opts.sessionId,
|
|
120
|
+
queueMessageId: opts.messageId,
|
|
121
|
+
message: readMessage(opts),
|
|
122
|
+
});
|
|
123
|
+
console.log('ok');
|
|
124
|
+
});
|
|
125
|
+
queue
|
|
126
|
+
.command('delete')
|
|
127
|
+
.description('Delete a pending worker message')
|
|
128
|
+
.requiredOption('--session-id <id>', 'Worker session id')
|
|
129
|
+
.requiredOption('--message-id <id>', 'Queued message id')
|
|
130
|
+
.action(async (opts) => {
|
|
131
|
+
await ipc('/sessions/queue/delete', {
|
|
132
|
+
sessionId: opts.sessionId,
|
|
133
|
+
queueMessageId: opts.messageId,
|
|
134
|
+
});
|
|
87
135
|
console.log('ok');
|
|
88
136
|
});
|
|
89
137
|
sessions
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const MANAGER_SYSTEM_PROMPT = "\u4F60\u662F\u9879\u76EE\u7ECF\u7406\uFF0C\u662F\u5F53\u524D\u9879\u76EE\u7684\u7BA1\u7406\u8005\u3002\n\n\u4F60\u7684\u804C\u8D23\uFF1A\n- \u7406\u89E3\u7528\u6237\u76EE\u6807\u3002\n- \u62C6\u89E3\u4EFB\u52A1\u3002\n- \u521B\u5EFA\u3001\u6307\u6D3E\u3001\u89C2\u5BDF\u548C\u505C\u6B62\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u4E0B\u7684 worker Agent session\u3002\n- \u6C47\u603B worker \u7ED3\u679C\u3002\n- \u5224\u65AD\u662F\u5426\u9700\u8981\u7EE7\u7EED\u7B49\u5F85\u3001\u8C03\u6574\u5B89\u6392\u3001\u8BE2\u95EE\u7528\u6237\u6216\u9A8C\u6536\u3002\n- \u5728\u9879\u76EE .shennian/ \u76EE\u5F55\u4E0B\u7EF4\u62A4\u5FC5\u8981\u7684\u8BA1\u5212\u3001\u8BB0\u5F55\u548C\u9879\u76EE\u8BB0\u5FC6\u3002\n\n\u4F60\u7684\u8FB9\u754C\uFF1A\n- \u4E0D\u8981\u628A\u81EA\u5DF1\u5F53\u4F5C\u4E3B\u8981\u6267\u884C\u8005\u3002\n- \u4E0D\u8981\u76F4\u63A5\u7F16\u8F91\u4E1A\u52A1\u4EE3\u7801\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u4EB2\u81EA\u6267\u884C\u3002\n- \u53EF\u4EE5\u8BFB\u53D6\u6587\u4EF6\u3001\u641C\u7D22\u9879\u76EE\u548C\u68C0\u67E5\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u4FBF\u505A\u5224\u65AD\u3002\n- \u9700\u8981\u4FEE\u6539\u4EE3\u7801\u3001\u8FD0\u884C\u6D4B\u8BD5\u3001\u8C03\u7814\u65B9\u6848\u65F6\uFF0C\u4F18\u5148\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u521B\u5EFA worker \u540E\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u5F53\u573A\u7EE7\u7EED\u8C03\u5EA6\uFF0C\u5426\u5219\u56DE\u590D\u7528\u6237\u5DF2\u5B89\u6392\u5E76\u7ED3\u675F\u5F53\u524D turn\uFF1B\u4E0D\u8981\u4E3B\u52A8\u8F6E\u8BE2 worker \u72B6\u6001\uFF0C\u795E\u5FF5\u4F1A\u5728 worker \u7EC8\u6001\u6216\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u91CD\u65B0\u5524\u9192\u4F60\u3002\n- sessions read \u8FD4\u56DE\u7684\u662F\u7ED9\u7BA1\u7406\u8005\u770B\u7684\u7B80\u6D01\u8FDB\u5C55\u3001\u5DE5\u5177\u6458\u8981\u548C\u6700\u7EC8\u7ED3\u679C\uFF0C\u4E0D\u662F\u539F\u59CB\u6D41\u5F0F token\uFF1B\u4E0D\u8981\u8981\u6C42\u8BFB\u53D6\u6216\u8F6C\u8FF0\u5B8C\u6574\u6D41\u5F0F\u65E5\u5FD7\u3002\n- \u53EA\u80FD\u7BA1\u7406\u4E0E\u4F60\u5904\u4E8E\u540C\u4E00\u53F0\u673A\u5668\u3001\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u7684\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u8DE8\u673A\u5668\u6216\u8DE8\u9879\u76EE\u8C03\u5EA6\u3002\n- \u4E0D\u8981\u65E0\u9650\u5FAA\u73AF\uFF1B\u6CA1\u6709\u660E\u786E\u4E0B\u4E00\u6B65\u65F6\u8BE2\u95EE\u7528\u6237\u6216\u7ED3\u675F\u5F53\u524D turn \u7B49\u5F85\u7CFB\u7EDF\u4E8B\u4EF6\u3002\n- \u4E0D\u8981\u81EA\u5DF1\u8BBE\u7F6E\u5B9A\u65F6\u5524\u9192\uFF1B\u795E\u5FF5\u4F1A\u5728\u7528\u6237\u6D88\u606F\u3001worker \u7EC8\u6001\u6216 worker \u957F\u8FD0\u884C\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u5524\u9192\u4F60\u3002\n- \u5916\u90E8\u6D88\u606F\u901A\u9053\u4E8B\u4EF6\u4F1A\u50CF\u666E\u901A\u7528\u6237\u6D88\u606F\u4E00\u6837\u9001\u8FBE\uFF0C\u683C\u5F0F\u7C7B\u4F3C\u201C\u5916\u90E8\u6D88\u606F / \u53D1\u9001\u4EBA\\n\u5185\u5BB9\u201D\uFF0C\u53EF\u80FD\u662F\u5408\u5E76\u6D88\u606F\uFF0C\u4E5F\u53EF\u80FD\u5305\u542B\u56FE\u7247\u3001\u89C6\u9891\u6216\u6587\u4EF6 URL\u3002\n- \u5BF9\u5916\u4F60\u662F\u5F53\u524D\u9879\u76EE\u7684\u9879\u76EE\u7ECF\u7406\uFF0C\u4E0D\u8981\u81EA\u79F0\u795E\u5FF5\u3001Manager Agent \u6216 worker\uFF0C\u4E5F\u4E0D\u8981\u89E3\u91CA\u5185\u90E8\u8C03\u5EA6\u673A\u5236\uFF1B\u53EA\u5728\u9700\u8981\u65F6\u7528\u201C\u6211\u8FD9\u8FB9/\u6211\u4EEC\u8FD9\u8FB9\u201D\u6C9F\u901A\u3002\n- \u5916\u90E8\u6D88\u606F\u4E0E\u5F53\u524D\u9879\u76EE\u65E0\u5173\u65F6\u53EF\u4EE5\u5FFD\u7565\uFF1B\u9700\u8981\u8F83\u957F\u5904\u7406\u65F6\uFF0C\u5148\u7B80\u77ED\u56DE\u590D\u201C\u6536\u5230\uFF0C\u6211\u5148\u5904\u7406/\u5B89\u6392\u4E00\u4E0B\u201D\uFF0C\u518D\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u5411\u5916\u90E8\u7FA4\u53D1\u6D88\u606F\u4E00\u5F8B\u8C03\u7528 shennian manager external send --text \"<\u6D88\u606F\u5185\u5BB9>\"\n- \u4E0D\u8981\u628A\u6240\u6709\u7EC6\u8282\u585E\u8FDB\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1B\u9700\u8981\u957F\u671F\u4FDD\u5B58\u7684\u4FE1\u606F\u5199\u5230\u9879\u76EE .shennian/ \u4E0B\u3002\n\n\u9700\u8981\u7BA1\u7406 worker \u6216\u5916\u90E8\u901A\u9053\u65F6\uFF0C\u4F7F\u7528\u672C\u5730\u547D\u4EE4\uFF1A\n- shennian manager sessions list --json\n- shennian manager sessions start --agent codex --workdir <path> --message <text>\n- shennian manager sessions send --session-id <id> --message <text>\n- shennian manager sessions stop --session-id <id>\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n- shennian manager external send --text <text>\n\n\u8FD9\u4E9B\u547D\u4EE4\u5DF2\u7ECF\u7531\u795E\u5FF5\u6CE8\u5165\u5F53\u524D Manager \u8EAB\u4EFD\u548C\u540C\u9879\u76EE\u6743\u9650\u8FB9\u754C\u3002\u4E0D\u8981\u5C1D\u8BD5\u4F2A\u9020 Manager session id\u3002";
|
|
1
|
+
export declare const MANAGER_SYSTEM_PROMPT = "\u4F60\u662F\u9879\u76EE\u7ECF\u7406\uFF0C\u662F\u5F53\u524D\u9879\u76EE\u7684\u7BA1\u7406\u8005\u3002\n\n\u4F60\u7684\u804C\u8D23\uFF1A\n- \u7406\u89E3\u7528\u6237\u76EE\u6807\u3002\n- \u62C6\u89E3\u4EFB\u52A1\u3002\n- \u521B\u5EFA\u3001\u6307\u6D3E\u3001\u89C2\u5BDF\u548C\u505C\u6B62\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u4E0B\u7684 worker Agent session\u3002\n- \u6C47\u603B worker \u7ED3\u679C\u3002\n- \u5224\u65AD\u662F\u5426\u9700\u8981\u7EE7\u7EED\u7B49\u5F85\u3001\u8C03\u6574\u5B89\u6392\u3001\u8BE2\u95EE\u7528\u6237\u6216\u9A8C\u6536\u3002\n- \u5728\u9879\u76EE .shennian/ \u76EE\u5F55\u4E0B\u7EF4\u62A4\u5FC5\u8981\u7684\u8BA1\u5212\u3001\u8BB0\u5F55\u548C\u9879\u76EE\u8BB0\u5FC6\u3002\n\n\u4F60\u7684\u8FB9\u754C\uFF1A\n- \u4E0D\u8981\u628A\u81EA\u5DF1\u5F53\u4F5C\u4E3B\u8981\u6267\u884C\u8005\u3002\n- \u4E0D\u8981\u76F4\u63A5\u7F16\u8F91\u4E1A\u52A1\u4EE3\u7801\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u4EB2\u81EA\u6267\u884C\u3002\n- \u53EF\u4EE5\u8BFB\u53D6\u6587\u4EF6\u3001\u641C\u7D22\u9879\u76EE\u548C\u68C0\u67E5\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u4FBF\u505A\u5224\u65AD\u3002\n- \u9700\u8981\u4FEE\u6539\u4EE3\u7801\u3001\u8FD0\u884C\u6D4B\u8BD5\u3001\u8C03\u7814\u65B9\u6848\u65F6\uFF0C\u4F18\u5148\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u521B\u5EFA worker \u540E\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u4F60\u5F53\u573A\u7EE7\u7EED\u8C03\u5EA6\uFF0C\u5426\u5219\u56DE\u590D\u7528\u6237\u5DF2\u5B89\u6392\u5E76\u7ED3\u675F\u5F53\u524D turn\uFF1B\u4E0D\u8981\u4E3B\u52A8\u8F6E\u8BE2 worker \u72B6\u6001\uFF0C\u795E\u5FF5\u4F1A\u5728 worker \u7EC8\u6001\u6216\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u91CD\u65B0\u5524\u9192\u4F60\u3002\n- sessions read \u8FD4\u56DE\u7684\u662F\u7ED9\u7BA1\u7406\u8005\u770B\u7684\u7B80\u6D01\u8FDB\u5C55\u3001\u5DE5\u5177\u6458\u8981\u548C\u6700\u7EC8\u7ED3\u679C\uFF0C\u4E0D\u662F\u539F\u59CB\u6D41\u5F0F token\uFF1B\u4E0D\u8981\u8981\u6C42\u8BFB\u53D6\u6216\u8F6C\u8FF0\u5B8C\u6574\u6D41\u5F0F\u65E5\u5FD7\u3002\n- \u53EA\u80FD\u7BA1\u7406\u4E0E\u4F60\u5904\u4E8E\u540C\u4E00\u53F0\u673A\u5668\u3001\u540C\u4E00\u9879\u76EE\u76EE\u5F55\u7684\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u8DE8\u673A\u5668\u6216\u8DE8\u9879\u76EE\u8C03\u5EA6\u3002\n- \u4E0D\u8981\u65E0\u9650\u5FAA\u73AF\uFF1B\u6CA1\u6709\u660E\u786E\u4E0B\u4E00\u6B65\u65F6\u8BE2\u95EE\u7528\u6237\u6216\u7ED3\u675F\u5F53\u524D turn \u7B49\u5F85\u7CFB\u7EDF\u4E8B\u4EF6\u3002\n- \u4E0D\u8981\u81EA\u5DF1\u8BBE\u7F6E\u5B9A\u65F6\u5524\u9192\uFF1B\u795E\u5FF5\u4F1A\u5728\u7528\u6237\u6D88\u606F\u3001worker \u7EC8\u6001\u6216 worker \u957F\u8FD0\u884C\u5065\u5EB7\u6458\u8981\u5230\u6765\u65F6\u5524\u9192\u4F60\u3002\n- \u5916\u90E8\u6D88\u606F\u901A\u9053\u4E8B\u4EF6\u4F1A\u50CF\u666E\u901A\u7528\u6237\u6D88\u606F\u4E00\u6837\u9001\u8FBE\uFF0C\u683C\u5F0F\u7C7B\u4F3C\u201C\u5916\u90E8\u6D88\u606F / \u53D1\u9001\u4EBA\\n\u5185\u5BB9\u201D\uFF0C\u53EF\u80FD\u662F\u5408\u5E76\u6D88\u606F\uFF0C\u4E5F\u53EF\u80FD\u5305\u542B\u56FE\u7247\u3001\u89C6\u9891\u6216\u6587\u4EF6 URL\u3002\n- \u5BF9\u5916\u4F60\u662F\u5F53\u524D\u9879\u76EE\u7684\u9879\u76EE\u7ECF\u7406\uFF0C\u4E0D\u8981\u81EA\u79F0\u795E\u5FF5\u3001Manager Agent \u6216 worker\uFF0C\u4E5F\u4E0D\u8981\u89E3\u91CA\u5185\u90E8\u8C03\u5EA6\u673A\u5236\uFF1B\u53EA\u5728\u9700\u8981\u65F6\u7528\u201C\u6211\u8FD9\u8FB9/\u6211\u4EEC\u8FD9\u8FB9\u201D\u6C9F\u901A\u3002\n- \u5916\u90E8\u6D88\u606F\u4E0E\u5F53\u524D\u9879\u76EE\u65E0\u5173\u65F6\u53EF\u4EE5\u5FFD\u7565\uFF1B\u9700\u8981\u8F83\u957F\u5904\u7406\u65F6\uFF0C\u5148\u7B80\u77ED\u56DE\u590D\u201C\u6536\u5230\uFF0C\u6211\u5148\u5904\u7406/\u5B89\u6392\u4E00\u4E0B\u201D\uFF0C\u518D\u521B\u5EFA\u6216\u6307\u6D3E worker\u3002\n- \u5411\u5916\u90E8\u7FA4\u53D1\u6D88\u606F\u4E00\u5F8B\u8C03\u7528 shennian manager external send --text \"<\u6D88\u606F\u5185\u5BB9>\"\n- \u4E0D\u8981\u628A\u6240\u6709\u7EC6\u8282\u585E\u8FDB\u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF1B\u9700\u8981\u957F\u671F\u4FDD\u5B58\u7684\u4FE1\u606F\u5199\u5230\u9879\u76EE .shennian/ \u4E0B\u3002\n\n\u9700\u8981\u7BA1\u7406 worker \u6216\u5916\u90E8\u901A\u9053\u65F6\uFF0C\u4F7F\u7528\u672C\u5730\u547D\u4EE4\uFF1A\n- shennian manager sessions list --json\n- shennian manager sessions start --agent codex --workdir <path> --message <text>\n- shennian manager sessions send --session-id <id> --message <text>\n- shennian manager sessions send --session-id <id> --message <text> --direct\n- shennian manager sessions queue list --session-id <id> --json\n- shennian manager sessions queue edit --session-id <id> --message-id <queueMessageId> --message <text>\n- shennian manager sessions queue delete --session-id <id> --message-id <queueMessageId>\n- shennian manager sessions stop --session-id <id>\n- shennian manager sessions read --session-id <id> --limit 200 --json\n- shennian manager memory path\n- shennian manager external send --text <text>\n\n\u9ED8\u8BA4\u7528 sessions send \u6392\u961F\u53D1\u9001 worker \u6D88\u606F\uFF1Aworker \u6B63\u5FD9\u65F6\u6D88\u606F\u4F1A\u5728\u672C\u673A daemon \u961F\u5217\u91CC\u7B49\u5F85\uFF0Cworker \u7A7A\u95F2\u65F6\u81EA\u52A8\u6267\u884C\u3002\u961F\u5217\u91CC\u7684\u672A\u6267\u884C\u6D88\u606F\u53EF\u4EE5 list/edit/delete\uFF1B\u5DF2\u7ECF\u5F00\u59CB\u6267\u884C\u7684\u6D88\u606F\u4E0D\u80FD\u7F16\u8F91\u6216\u5220\u9664\uFF0C\u53EA\u80FD stop \u540E\u91CD\u65B0\u53D1\u9001\u3002\u53EA\u6709\u660E\u786E\u9700\u8981\u6253\u65AD\u987A\u5E8F\u65F6\u624D\u4F7F\u7528 --direct\u3002\n\n\u8FD9\u4E9B\u547D\u4EE4\u5DF2\u7ECF\u7531\u795E\u5FF5\u6CE8\u5165\u5F53\u524D Manager \u8EAB\u4EFD\u548C\u540C\u9879\u76EE\u6743\u9650\u8FB9\u754C\u3002\u4E0D\u8981\u5C1D\u8BD5\u4F2A\u9020 Manager session id\u3002";
|
|
2
2
|
export declare function buildManagerPrompt(userText: string): string;
|
|
@@ -30,11 +30,17 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
|
|
|
30
30
|
- shennian manager sessions list --json
|
|
31
31
|
- shennian manager sessions start --agent codex --workdir <path> --message <text>
|
|
32
32
|
- shennian manager sessions send --session-id <id> --message <text>
|
|
33
|
+
- shennian manager sessions send --session-id <id> --message <text> --direct
|
|
34
|
+
- shennian manager sessions queue list --session-id <id> --json
|
|
35
|
+
- shennian manager sessions queue edit --session-id <id> --message-id <queueMessageId> --message <text>
|
|
36
|
+
- shennian manager sessions queue delete --session-id <id> --message-id <queueMessageId>
|
|
33
37
|
- shennian manager sessions stop --session-id <id>
|
|
34
38
|
- shennian manager sessions read --session-id <id> --limit 200 --json
|
|
35
39
|
- shennian manager memory path
|
|
36
40
|
- shennian manager external send --text <text>
|
|
37
41
|
|
|
42
|
+
默认用 sessions send 排队发送 worker 消息:worker 正忙时消息会在本机 daemon 队列里等待,worker 空闲时自动执行。队列里的未执行消息可以 list/edit/delete;已经开始执行的消息不能编辑或删除,只能 stop 后重新发送。只有明确需要打断顺序时才使用 --direct。
|
|
43
|
+
|
|
38
44
|
这些命令已经由神念注入当前 Manager 身份和同项目权限边界。不要尝试伪造 Manager session id。`;
|
|
39
45
|
export function buildManagerPrompt(userText) {
|
|
40
46
|
return userText;
|
|
@@ -41,6 +41,7 @@ export declare class ManagerRuntimeService {
|
|
|
41
41
|
private broadcastManagerChannelStatus;
|
|
42
42
|
private handleIpc;
|
|
43
43
|
private dispatchChatSend;
|
|
44
|
+
private dispatchChatEnqueue;
|
|
44
45
|
private wakeManagerForWorker;
|
|
45
46
|
private handleExternalMessage;
|
|
46
47
|
private scanWorkerHealth;
|
|
@@ -341,13 +341,63 @@ export class ManagerRuntimeService {
|
|
|
341
341
|
if (url.pathname === '/sessions/send') {
|
|
342
342
|
const sessionId = String(body.sessionId || '');
|
|
343
343
|
const message = String(body.message || '');
|
|
344
|
+
const enqueue = body.enqueue === undefined ? true : Boolean(body.enqueue);
|
|
344
345
|
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
345
346
|
if (!worker)
|
|
346
347
|
throw new Error('Worker not found in this manager scope');
|
|
347
|
-
|
|
348
|
+
if (enqueue) {
|
|
349
|
+
await this.dispatchChatEnqueue(worker.sessionId, worker.agentType, worker.workDir, message, worker.agentSessionId ?? null, String(body.modelId || ''));
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
await this.dispatchChatSend(worker.sessionId, worker.agentType, worker.workDir, message, worker.agentSessionId ?? null, String(body.modelId || ''));
|
|
353
|
+
}
|
|
348
354
|
json(res, 200, { ok: true });
|
|
349
355
|
return;
|
|
350
356
|
}
|
|
357
|
+
if (url.pathname === '/sessions/queue') {
|
|
358
|
+
const sessionId = String(body.sessionId || '');
|
|
359
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
360
|
+
if (!worker)
|
|
361
|
+
throw new Error('Worker not found in this manager scope');
|
|
362
|
+
const queue = this.opts.getRuntime().chatQueue?.getSnapshot(sessionId);
|
|
363
|
+
json(res, 200, { ok: true, queue });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (url.pathname === '/sessions/queue/edit') {
|
|
367
|
+
const sessionId = String(body.sessionId || '');
|
|
368
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
369
|
+
if (!worker)
|
|
370
|
+
throw new Error('Worker not found in this manager scope');
|
|
371
|
+
await this.opts.dispatchReq({
|
|
372
|
+
type: 'req',
|
|
373
|
+
id: `manager-queue-edit-${randomUUID()}`,
|
|
374
|
+
method: 'chat.queue.edit',
|
|
375
|
+
params: {
|
|
376
|
+
sessionId,
|
|
377
|
+
queueMessageId: String(body.queueMessageId || body.messageId || ''),
|
|
378
|
+
text: String(body.message || body.text || ''),
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
json(res, 200, { ok: true, queue: this.opts.getRuntime().chatQueue?.getSnapshot(sessionId) });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (url.pathname === '/sessions/queue/delete') {
|
|
385
|
+
const sessionId = String(body.sessionId || '');
|
|
386
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
387
|
+
if (!worker)
|
|
388
|
+
throw new Error('Worker not found in this manager scope');
|
|
389
|
+
await this.opts.dispatchReq({
|
|
390
|
+
type: 'req',
|
|
391
|
+
id: `manager-queue-delete-${randomUUID()}`,
|
|
392
|
+
method: 'chat.queue.delete',
|
|
393
|
+
params: {
|
|
394
|
+
sessionId,
|
|
395
|
+
queueMessageId: String(body.queueMessageId || body.messageId || ''),
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
json(res, 200, { ok: true, queue: this.opts.getRuntime().chatQueue?.getSnapshot(sessionId) });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
351
401
|
if (url.pathname === '/sessions/stop') {
|
|
352
402
|
const sessionId = String(body.sessionId || '');
|
|
353
403
|
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
@@ -442,6 +492,14 @@ export class ManagerRuntimeService {
|
|
|
442
492
|
params: { sessionId, text, agentType, workDir, agentSessionId, modelId },
|
|
443
493
|
});
|
|
444
494
|
}
|
|
495
|
+
async dispatchChatEnqueue(sessionId, agentType, workDir, text, agentSessionId, modelId) {
|
|
496
|
+
await this.opts.dispatchReq({
|
|
497
|
+
type: 'req',
|
|
498
|
+
id: `manager-enqueue-${randomUUID()}`,
|
|
499
|
+
method: 'chat.enqueue',
|
|
500
|
+
params: { sessionId, text, agentType, workDir, agentSessionId, modelId },
|
|
501
|
+
});
|
|
502
|
+
}
|
|
445
503
|
wakeManagerForWorker(managerSessionId, worker, state, message) {
|
|
446
504
|
const manager = this.registry.getManager(managerSessionId);
|
|
447
505
|
if (!manager)
|
|
@@ -214,6 +214,7 @@ function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
|
|
|
214
214
|
activeSession?.currentRunId === event.runId) {
|
|
215
215
|
activeSession.currentRunId = null;
|
|
216
216
|
activeSession.nextEventSeq = 0;
|
|
217
|
+
runtime.chatQueue?.noteTerminal(sessionId);
|
|
217
218
|
}
|
|
218
219
|
sendAgentEvent(event, extra);
|
|
219
220
|
});
|
|
@@ -282,7 +283,7 @@ export async function handleChatSend(runtime, req) {
|
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
285
|
rememberProcessedReqId(runtime, req.id);
|
|
285
|
-
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection } = req.params;
|
|
286
|
+
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
|
|
286
287
|
mergeProjectedSessions(sessionListProjection);
|
|
287
288
|
if (!sessionId || !text) {
|
|
288
289
|
runtime.processedReqIds.delete(req.id);
|
|
@@ -353,23 +354,6 @@ export async function handleChatSend(runtime, req) {
|
|
|
353
354
|
return;
|
|
354
355
|
}
|
|
355
356
|
}
|
|
356
|
-
sendSessionUpdateEvent(runtime, {
|
|
357
|
-
sessionId,
|
|
358
|
-
agentType: requestedAgentType,
|
|
359
|
-
workDir: resolvedWorkDir,
|
|
360
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
361
|
-
modelId,
|
|
362
|
-
});
|
|
363
|
-
session.currentRunId = null;
|
|
364
|
-
session.nextEventSeq = 0;
|
|
365
|
-
runtime.nativeFusion?.registerManagedSend({
|
|
366
|
-
sessionId,
|
|
367
|
-
agentType: requestedAgentType,
|
|
368
|
-
sourceAgentType: getNativeSourceAgentType(requestedAgentType, modelId),
|
|
369
|
-
canonicalMessageId: clientMessageId ?? null,
|
|
370
|
-
sourceSessionKey: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
371
|
-
text,
|
|
372
|
-
});
|
|
373
357
|
const userEnvelope = {
|
|
374
358
|
id: clientMessageId ?? `user-${req.id}`,
|
|
375
359
|
sessionId,
|
|
@@ -377,36 +361,39 @@ export async function handleChatSend(runtime, req) {
|
|
|
377
361
|
ts: Date.now(),
|
|
378
362
|
payload: text,
|
|
379
363
|
};
|
|
380
|
-
appendMessage(sessionId, userEnvelope);
|
|
381
|
-
sendSessionMessageEvent(runtime, userEnvelope, {
|
|
382
|
-
agentType: requestedAgentType,
|
|
383
|
-
workDir: resolvedWorkDir,
|
|
384
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
385
|
-
modelId,
|
|
386
|
-
});
|
|
387
364
|
reportLog({
|
|
388
365
|
level: 'info',
|
|
389
366
|
sessionId,
|
|
390
367
|
wsEvent: 'chat.send.start',
|
|
391
368
|
metadata: { reqId: req.id, agentType: requestedAgentType, modelId },
|
|
392
369
|
});
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
level: 'info',
|
|
396
|
-
sessionId,
|
|
397
|
-
wsEvent: 'chat.send.res',
|
|
398
|
-
metadata: { reqId: req.id, ok: true },
|
|
399
|
-
});
|
|
400
|
-
void session.adapter.send(text, modelId)
|
|
401
|
-
.then(() => {
|
|
402
|
-
reportLog({
|
|
403
|
-
level: 'info',
|
|
370
|
+
const markAccepted = () => {
|
|
371
|
+
sendSessionUpdateEvent(runtime, {
|
|
404
372
|
sessionId,
|
|
405
|
-
|
|
406
|
-
|
|
373
|
+
agentType: requestedAgentType,
|
|
374
|
+
workDir: resolvedWorkDir,
|
|
375
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
376
|
+
modelId,
|
|
407
377
|
});
|
|
408
|
-
|
|
409
|
-
.
|
|
378
|
+
session.currentRunId = null;
|
|
379
|
+
session.nextEventSeq = 0;
|
|
380
|
+
runtime.nativeFusion?.registerManagedSend({
|
|
381
|
+
sessionId,
|
|
382
|
+
agentType: requestedAgentType,
|
|
383
|
+
sourceAgentType: getNativeSourceAgentType(requestedAgentType, modelId),
|
|
384
|
+
canonicalMessageId: clientMessageId ?? null,
|
|
385
|
+
sourceSessionKey: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
386
|
+
text,
|
|
387
|
+
});
|
|
388
|
+
appendMessage(sessionId, userEnvelope);
|
|
389
|
+
sendSessionMessageEvent(runtime, userEnvelope, {
|
|
390
|
+
agentType: requestedAgentType,
|
|
391
|
+
workDir: resolvedWorkDir,
|
|
392
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
393
|
+
modelId,
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
const handleSendFailure = async (err, respondToReq) => {
|
|
410
397
|
const message = `Agent send failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
411
398
|
console.error(`[chat.send] send failed reqId=${req.id} sessionId=${sessionId} agentType=${agentType} workDir=${resolvedWorkDir} agentSessionId=${session.agentSessionId ?? incomingAgentSid ?? ''}: ${message}`);
|
|
412
399
|
runtime.sessions.delete(sessionId);
|
|
@@ -425,6 +412,59 @@ export async function handleChatSend(runtime, req) {
|
|
|
425
412
|
seq: 0,
|
|
426
413
|
},
|
|
427
414
|
});
|
|
415
|
+
if (respondToReq) {
|
|
416
|
+
runtime.processedReqIds.delete(req.id);
|
|
417
|
+
runtime.client.sendRes({
|
|
418
|
+
type: 'res',
|
|
419
|
+
id: req.id,
|
|
420
|
+
ok: false,
|
|
421
|
+
error: message,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
if (waitForDispatch) {
|
|
426
|
+
try {
|
|
427
|
+
await session.adapter.send(text, modelId);
|
|
428
|
+
reportLog({
|
|
429
|
+
level: 'info',
|
|
430
|
+
sessionId,
|
|
431
|
+
wsEvent: 'chat.send.done',
|
|
432
|
+
metadata: { reqId: req.id },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
await handleSendFailure(err, true);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
markAccepted();
|
|
440
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
441
|
+
reportLog({
|
|
442
|
+
level: 'info',
|
|
443
|
+
sessionId,
|
|
444
|
+
wsEvent: 'chat.send.res',
|
|
445
|
+
metadata: { reqId: req.id, ok: true },
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
markAccepted();
|
|
450
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
451
|
+
reportLog({
|
|
452
|
+
level: 'info',
|
|
453
|
+
sessionId,
|
|
454
|
+
wsEvent: 'chat.send.res',
|
|
455
|
+
metadata: { reqId: req.id, ok: true },
|
|
456
|
+
});
|
|
457
|
+
void session.adapter.send(text, modelId)
|
|
458
|
+
.then(() => {
|
|
459
|
+
reportLog({
|
|
460
|
+
level: 'info',
|
|
461
|
+
sessionId,
|
|
462
|
+
wsEvent: 'chat.send.done',
|
|
463
|
+
metadata: { reqId: req.id },
|
|
464
|
+
});
|
|
465
|
+
})
|
|
466
|
+
.catch((err) => {
|
|
467
|
+
void handleSendFailure(err, false);
|
|
428
468
|
});
|
|
429
469
|
}
|
|
430
470
|
export async function handleChatAbort(runtime, req) {
|
|
@@ -21,6 +21,7 @@ export declare class SessionManager {
|
|
|
21
21
|
/** In-flight chunked uploads: transferId → metadata */
|
|
22
22
|
private pendingTransfers;
|
|
23
23
|
private managerRuntime;
|
|
24
|
+
private chatQueue;
|
|
24
25
|
constructor(client: CliRelayClient, nativeFusion?: NativeSessionFusionService | null, cliVersion?: string | undefined);
|
|
25
26
|
private getRuntime;
|
|
26
27
|
private reloadCustomAgents;
|
|
@@ -11,6 +11,7 @@ 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
13
|
import { ManagerRuntimeService, setManagerRuntimeService } from '../manager/runtime.js';
|
|
14
|
+
import { ChatQueueManager } from './queue.js';
|
|
14
15
|
// Side-effect imports to register built-in agent adapters.
|
|
15
16
|
import '../agents/claude.js';
|
|
16
17
|
import '../agents/codex.js';
|
|
@@ -61,6 +62,7 @@ export class SessionManager {
|
|
|
61
62
|
/** In-flight chunked uploads: transferId → metadata */
|
|
62
63
|
pendingTransfers = new Map();
|
|
63
64
|
managerRuntime;
|
|
65
|
+
chatQueue;
|
|
64
66
|
constructor(client, nativeFusion = null, cliVersion) {
|
|
65
67
|
this.client = client;
|
|
66
68
|
this.nativeFusion = nativeFusion;
|
|
@@ -69,6 +71,10 @@ export class SessionManager {
|
|
|
69
71
|
getRuntime: () => this.getRuntime(),
|
|
70
72
|
dispatchReq: (req) => this.handleReq(req),
|
|
71
73
|
});
|
|
74
|
+
this.chatQueue = new ChatQueueManager({
|
|
75
|
+
getRuntime: () => this.getRuntime(),
|
|
76
|
+
dispatchReq: (req) => this.handleReq(req),
|
|
77
|
+
});
|
|
72
78
|
setManagerRuntimeService(this.managerRuntime);
|
|
73
79
|
void this.managerRuntime.start();
|
|
74
80
|
this.reloadCustomAgents();
|
|
@@ -85,6 +91,7 @@ export class SessionManager {
|
|
|
85
91
|
evictIdleSessions: () => this.evictIdleSessions(),
|
|
86
92
|
nativeFusion: this.nativeFusion,
|
|
87
93
|
managerRuntime: this.managerRuntime,
|
|
94
|
+
chatQueue: this.chatQueue,
|
|
88
95
|
};
|
|
89
96
|
}
|
|
90
97
|
reloadCustomAgents() {
|
|
@@ -105,6 +112,18 @@ export class SessionManager {
|
|
|
105
112
|
case 'chat.send':
|
|
106
113
|
await handleChatSend(runtime, req);
|
|
107
114
|
break;
|
|
115
|
+
case 'chat.enqueue':
|
|
116
|
+
await this.chatQueue.handleEnqueue(req);
|
|
117
|
+
break;
|
|
118
|
+
case 'chat.queue.get':
|
|
119
|
+
await this.chatQueue.handleGet(req);
|
|
120
|
+
break;
|
|
121
|
+
case 'chat.queue.edit':
|
|
122
|
+
await this.chatQueue.handleEdit(req);
|
|
123
|
+
break;
|
|
124
|
+
case 'chat.queue.delete':
|
|
125
|
+
await this.chatQueue.handleDelete(req);
|
|
126
|
+
break;
|
|
108
127
|
case 'chat.abort':
|
|
109
128
|
await handleChatAbort(runtime, req);
|
|
110
129
|
break;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChatQueueSnapshot, ReqFrame } from '@shennian/wire';
|
|
2
|
+
import type { LocalReqDispatcher } from '../manager/runtime.js';
|
|
3
|
+
import type { SessionManagerRuntime } from './types.js';
|
|
4
|
+
export declare class ChatQueueManager {
|
|
5
|
+
private opts;
|
|
6
|
+
private draining;
|
|
7
|
+
constructor(opts: {
|
|
8
|
+
getRuntime: () => SessionManagerRuntime;
|
|
9
|
+
dispatchReq: LocalReqDispatcher;
|
|
10
|
+
});
|
|
11
|
+
getSnapshot(sessionId: string): ChatQueueSnapshot;
|
|
12
|
+
handleEnqueue(req: ReqFrame): Promise<void>;
|
|
13
|
+
handleGet(req: ReqFrame): Promise<void>;
|
|
14
|
+
handleEdit(req: ReqFrame): Promise<void>;
|
|
15
|
+
handleDelete(req: ReqFrame): Promise<void>;
|
|
16
|
+
noteTerminal(sessionId: string): void;
|
|
17
|
+
private drainNext;
|
|
18
|
+
private dispatchQueuedMessage;
|
|
19
|
+
private broadcast;
|
|
20
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// @arch docs/features/session-message-queue.md
|
|
2
|
+
// @test src/__tests__/session-manager.test.ts
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { resolveShennianPath } from '../config/index.js';
|
|
6
|
+
const QUEUE_FILE = resolveShennianPath('chat-queue.json');
|
|
7
|
+
function emptyQueue() {
|
|
8
|
+
return { sessions: {} };
|
|
9
|
+
}
|
|
10
|
+
function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
function readQueue() {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(fs.readFileSync(QUEUE_FILE, 'utf-8'));
|
|
16
|
+
return {
|
|
17
|
+
sessions: parsed.sessions && typeof parsed.sessions === 'object' ? parsed.sessions : {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return emptyQueue();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writeQueue(queue) {
|
|
25
|
+
fs.mkdirSync(resolveShennianPath(''), { recursive: true });
|
|
26
|
+
fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2));
|
|
27
|
+
}
|
|
28
|
+
function normalizeAttachments(value) {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
return undefined;
|
|
31
|
+
const attachments = value
|
|
32
|
+
.map((item) => {
|
|
33
|
+
if (!item || typeof item !== 'object')
|
|
34
|
+
return null;
|
|
35
|
+
const entry = item;
|
|
36
|
+
const attachment = {
|
|
37
|
+
path: typeof entry.path === 'string' ? entry.path : '',
|
|
38
|
+
name: typeof entry.name === 'string' ? entry.name : '',
|
|
39
|
+
mimeType: typeof entry.mimeType === 'string' ? entry.mimeType : '',
|
|
40
|
+
};
|
|
41
|
+
return attachment.path && attachment.name && attachment.mimeType ? attachment : null;
|
|
42
|
+
})
|
|
43
|
+
.filter((item) => item != null);
|
|
44
|
+
return attachments.length ? attachments : undefined;
|
|
45
|
+
}
|
|
46
|
+
function queueMessageFromParams(params) {
|
|
47
|
+
const timestamp = nowIso();
|
|
48
|
+
return {
|
|
49
|
+
id: params.queueMessageId || params.clientMessageId || `queue-${randomUUID()}`,
|
|
50
|
+
sessionId: params.sessionId,
|
|
51
|
+
text: params.text,
|
|
52
|
+
agentType: params.agentType,
|
|
53
|
+
workDir: params.workDir,
|
|
54
|
+
agentSessionId: params.agentSessionId ?? null,
|
|
55
|
+
modelId: params.modelId ?? null,
|
|
56
|
+
clientMessageId: params.clientMessageId ?? null,
|
|
57
|
+
attachments: normalizeAttachments(params.attachments),
|
|
58
|
+
createdAt: timestamp,
|
|
59
|
+
updatedAt: timestamp,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export class ChatQueueManager {
|
|
63
|
+
opts;
|
|
64
|
+
draining = new Set();
|
|
65
|
+
constructor(opts) {
|
|
66
|
+
this.opts = opts;
|
|
67
|
+
}
|
|
68
|
+
getSnapshot(sessionId) {
|
|
69
|
+
return {
|
|
70
|
+
sessionId,
|
|
71
|
+
busy: Boolean(this.opts.getRuntime().sessions.get(sessionId)?.currentRunId),
|
|
72
|
+
pending: readQueue().sessions[sessionId] ?? [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async handleEnqueue(req) {
|
|
76
|
+
const runtime = this.opts.getRuntime();
|
|
77
|
+
const params = req.params;
|
|
78
|
+
if (!params.sessionId || !params.text || !params.agentType || !params.workDir) {
|
|
79
|
+
runtime.client.sendRes({
|
|
80
|
+
type: 'res',
|
|
81
|
+
id: req.id,
|
|
82
|
+
ok: false,
|
|
83
|
+
error: 'sessionId, text, agentType and workDir are required',
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const active = runtime.sessions.get(params.sessionId);
|
|
88
|
+
const isBusy = Boolean(active?.currentRunId);
|
|
89
|
+
if (!isBusy && !(readQueue().sessions[params.sessionId]?.length)) {
|
|
90
|
+
await this.opts.dispatchReq({
|
|
91
|
+
...req,
|
|
92
|
+
method: 'chat.send',
|
|
93
|
+
params: {
|
|
94
|
+
...params,
|
|
95
|
+
clientMessageId: params.clientMessageId ?? params.queueMessageId,
|
|
96
|
+
waitForDispatch: true,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const queue = readQueue();
|
|
102
|
+
const message = queueMessageFromParams(params);
|
|
103
|
+
queue.sessions[params.sessionId] = [...(queue.sessions[params.sessionId] ?? []), message];
|
|
104
|
+
writeQueue(queue);
|
|
105
|
+
this.broadcast(params.sessionId);
|
|
106
|
+
runtime.client.sendRes({
|
|
107
|
+
type: 'res',
|
|
108
|
+
id: req.id,
|
|
109
|
+
ok: true,
|
|
110
|
+
payload: { queued: true, queueMessageId: message.id, queue: this.getSnapshot(params.sessionId) },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async handleGet(req) {
|
|
114
|
+
const runtime = this.opts.getRuntime();
|
|
115
|
+
const params = req.params;
|
|
116
|
+
if (!params.sessionId) {
|
|
117
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'sessionId is required' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
runtime.client.sendRes({
|
|
121
|
+
type: 'res',
|
|
122
|
+
id: req.id,
|
|
123
|
+
ok: true,
|
|
124
|
+
payload: { queue: this.getSnapshot(params.sessionId) },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async handleEdit(req) {
|
|
128
|
+
const runtime = this.opts.getRuntime();
|
|
129
|
+
const params = req.params;
|
|
130
|
+
if (!params.sessionId || !params.queueMessageId || !params.text) {
|
|
131
|
+
runtime.client.sendRes({
|
|
132
|
+
type: 'res',
|
|
133
|
+
id: req.id,
|
|
134
|
+
ok: false,
|
|
135
|
+
error: 'sessionId, queueMessageId and text are required',
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const queue = readQueue();
|
|
140
|
+
const pending = queue.sessions[params.sessionId] ?? [];
|
|
141
|
+
const index = pending.findIndex((message) => message.id === params.queueMessageId);
|
|
142
|
+
if (index < 0) {
|
|
143
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Queued message not found' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
pending[index] = {
|
|
147
|
+
...pending[index],
|
|
148
|
+
text: params.text,
|
|
149
|
+
attachments: normalizeAttachments(params.attachments),
|
|
150
|
+
updatedAt: nowIso(),
|
|
151
|
+
};
|
|
152
|
+
queue.sessions[params.sessionId] = pending;
|
|
153
|
+
writeQueue(queue);
|
|
154
|
+
this.broadcast(params.sessionId);
|
|
155
|
+
runtime.client.sendRes({
|
|
156
|
+
type: 'res',
|
|
157
|
+
id: req.id,
|
|
158
|
+
ok: true,
|
|
159
|
+
payload: { queue: this.getSnapshot(params.sessionId) },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async handleDelete(req) {
|
|
163
|
+
const runtime = this.opts.getRuntime();
|
|
164
|
+
const params = req.params;
|
|
165
|
+
if (!params.sessionId || !params.queueMessageId) {
|
|
166
|
+
runtime.client.sendRes({
|
|
167
|
+
type: 'res',
|
|
168
|
+
id: req.id,
|
|
169
|
+
ok: false,
|
|
170
|
+
error: 'sessionId and queueMessageId are required',
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const queue = readQueue();
|
|
175
|
+
const pending = queue.sessions[params.sessionId] ?? [];
|
|
176
|
+
const next = pending.filter((message) => message.id !== params.queueMessageId);
|
|
177
|
+
if (next.length === pending.length) {
|
|
178
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'Queued message not found' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (next.length)
|
|
182
|
+
queue.sessions[params.sessionId] = next;
|
|
183
|
+
else
|
|
184
|
+
delete queue.sessions[params.sessionId];
|
|
185
|
+
writeQueue(queue);
|
|
186
|
+
this.broadcast(params.sessionId);
|
|
187
|
+
runtime.client.sendRes({
|
|
188
|
+
type: 'res',
|
|
189
|
+
id: req.id,
|
|
190
|
+
ok: true,
|
|
191
|
+
payload: { queue: this.getSnapshot(params.sessionId) },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
noteTerminal(sessionId) {
|
|
195
|
+
void this.drainNext(sessionId);
|
|
196
|
+
}
|
|
197
|
+
async drainNext(sessionId) {
|
|
198
|
+
if (this.draining.has(sessionId))
|
|
199
|
+
return;
|
|
200
|
+
const runtime = this.opts.getRuntime();
|
|
201
|
+
if (runtime.sessions.get(sessionId)?.currentRunId)
|
|
202
|
+
return;
|
|
203
|
+
const queue = readQueue();
|
|
204
|
+
const pending = queue.sessions[sessionId] ?? [];
|
|
205
|
+
const next = pending.shift();
|
|
206
|
+
if (!next) {
|
|
207
|
+
this.broadcast(sessionId);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (pending.length)
|
|
211
|
+
queue.sessions[sessionId] = pending;
|
|
212
|
+
else
|
|
213
|
+
delete queue.sessions[sessionId];
|
|
214
|
+
writeQueue(queue);
|
|
215
|
+
this.broadcast(sessionId);
|
|
216
|
+
this.draining.add(sessionId);
|
|
217
|
+
try {
|
|
218
|
+
await this.dispatchQueuedMessage(next);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
this.draining.delete(sessionId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async dispatchQueuedMessage(message) {
|
|
225
|
+
await this.opts.dispatchReq({
|
|
226
|
+
type: 'req',
|
|
227
|
+
id: `queue-send-${message.id}-${Date.now()}`,
|
|
228
|
+
method: 'chat.send',
|
|
229
|
+
params: {
|
|
230
|
+
sessionId: message.sessionId,
|
|
231
|
+
text: message.text,
|
|
232
|
+
agentType: message.agentType,
|
|
233
|
+
workDir: message.workDir,
|
|
234
|
+
agentSessionId: message.agentSessionId ?? null,
|
|
235
|
+
modelId: message.modelId ?? undefined,
|
|
236
|
+
clientMessageId: message.clientMessageId ?? message.id,
|
|
237
|
+
attachments: message.attachments,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
broadcast(sessionId) {
|
|
242
|
+
this.opts.getRuntime().client.sendEvent({
|
|
243
|
+
type: 'event',
|
|
244
|
+
event: 'session.queue.update',
|
|
245
|
+
payload: { queue: this.getSnapshot(sessionId) },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -23,6 +23,14 @@ export type PendingTransfer = {
|
|
|
23
23
|
targetPath: string;
|
|
24
24
|
totalSize: number;
|
|
25
25
|
};
|
|
26
|
+
export type ChatQueueService = {
|
|
27
|
+
handleEnqueue(req: import('@shennian/wire').ReqFrame): Promise<void>;
|
|
28
|
+
handleGet(req: import('@shennian/wire').ReqFrame): Promise<void>;
|
|
29
|
+
handleEdit(req: import('@shennian/wire').ReqFrame): Promise<void>;
|
|
30
|
+
handleDelete(req: import('@shennian/wire').ReqFrame): Promise<void>;
|
|
31
|
+
noteTerminal(sessionId: string): void;
|
|
32
|
+
getSnapshot(sessionId: string): import('@shennian/wire').ChatQueueSnapshot;
|
|
33
|
+
};
|
|
26
34
|
export type SessionManagerRuntime = {
|
|
27
35
|
client: CliRelayClient;
|
|
28
36
|
pendingTransfers: Map<string, PendingTransfer>;
|
|
@@ -34,4 +42,5 @@ export type SessionManagerRuntime = {
|
|
|
34
42
|
evictIdleSessions: () => void;
|
|
35
43
|
nativeFusion: NativeSessionFusionService | null;
|
|
36
44
|
managerRuntime: ManagerRuntimeService | null;
|
|
45
|
+
chatQueue: ChatQueueService | null;
|
|
37
46
|
};
|