shennian 0.2.53 → 0.2.55

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.
@@ -25,6 +25,7 @@ export interface AgentAdapterEvents {
25
25
  export declare abstract class AgentAdapter extends EventEmitter<AgentAdapterEvents> {
26
26
  abstract readonly type: AgentType;
27
27
  configure?(options: {
28
+ sessionId?: string;
28
29
  externalChannel?: ExternalChannelSessionStatus | null;
29
30
  env?: NodeJS.ProcessEnv;
30
31
  }): void;
@@ -18,8 +18,10 @@ export declare class ClaudeAdapter extends AgentAdapter {
18
18
  hidden?: boolean;
19
19
  });
20
20
  private externalChannel;
21
+ private shennianSessionId;
21
22
  private extraEnv;
22
23
  configure(options: {
24
+ sessionId?: string;
23
25
  externalChannel?: ExternalChannelSessionStatus | null;
24
26
  env?: NodeJS.ProcessEnv;
25
27
  }): void;
@@ -33,8 +33,10 @@ export class ClaudeAdapter extends AgentAdapter {
33
33
  this.options = options;
34
34
  }
35
35
  externalChannel = null;
36
+ shennianSessionId = null;
36
37
  extraEnv = {};
37
38
  configure(options) {
39
+ this.shennianSessionId = options.sessionId ?? null;
38
40
  this.externalChannel = options.externalChannel ?? null;
39
41
  this.extraEnv = options.env ?? {};
40
42
  }
@@ -50,7 +52,7 @@ export class ClaudeAdapter extends AgentAdapter {
50
52
  this.runId = randomUUID();
51
53
  this.resetRunState();
52
54
  const args = ['-p', text, '--output-format', 'stream-json', '--verbose'];
53
- const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel);
55
+ const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel, this.shennianSessionId ?? undefined);
54
56
  if (systemPrompt) {
55
57
  args.push('--append-system-prompt', systemPrompt);
56
58
  }
@@ -76,7 +78,7 @@ export class ClaudeAdapter extends AgentAdapter {
76
78
  this.runId = randomUUID();
77
79
  this.resetRunState();
78
80
  const resumeArgs = ['--resume', agentSessionId, '--output-format', 'stream-json', '--verbose'];
79
- const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel);
81
+ const systemPrompt = this.options.systemPrompt ?? buildPlatformInstructions(this.workDir ?? process.cwd(), this.externalChannel, this.shennianSessionId ?? undefined);
80
82
  if (systemPrompt) {
81
83
  resumeArgs.push('--append-system-prompt', systemPrompt);
82
84
  }
@@ -27,8 +27,10 @@ export declare class CodexAdapter extends AgentAdapter {
27
27
  hidden?: boolean;
28
28
  });
29
29
  private externalChannel;
30
+ private shennianSessionId;
30
31
  private extraEnv;
31
32
  configure(options: {
33
+ sessionId?: string;
32
34
  externalChannel?: ExternalChannelSessionStatus | null;
33
35
  env?: NodeJS.ProcessEnv;
34
36
  }): void;
@@ -33,8 +33,10 @@ export class CodexAdapter extends AgentAdapter {
33
33
  this.options = options;
34
34
  }
35
35
  externalChannel = null;
36
+ shennianSessionId = null;
36
37
  extraEnv = {};
37
38
  configure(options) {
39
+ this.shennianSessionId = options.sessionId ?? null;
38
40
  this.externalChannel = options.externalChannel ?? null;
39
41
  this.extraEnv = options.env ?? {};
40
42
  }
@@ -133,7 +135,7 @@ export class CodexAdapter extends AgentAdapter {
133
135
  return;
134
136
  }
135
137
  const args = ['app-server', '--listen', 'stdio://'];
136
- const modelInstructionsFile = this.options.modelInstructionsFile ?? ensurePlatformInstructionsFile(this.workDir ?? process.cwd(), this.externalChannel);
138
+ const modelInstructionsFile = this.options.modelInstructionsFile ?? ensurePlatformInstructionsFile(this.workDir ?? process.cwd(), this.externalChannel, this.shennianSessionId ?? undefined);
137
139
  if (modelInstructionsFile) {
138
140
  args.push('-c', `model_instructions_file=${JSON.stringify(modelInstructionsFile)}`);
139
141
  }
@@ -1,2 +1,2 @@
1
1
  import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
- export declare function buildExternalChannelInstructions(channel?: ExternalChannelSessionStatus | null): string;
2
+ export declare function buildExternalChannelInstructions(channel?: ExternalChannelSessionStatus | null, workDir?: string, sessionId?: string): string;
@@ -1,10 +1,16 @@
1
1
  // @arch docs/features/wecom-managed-channel.md
2
2
  // @test src/__tests__/platform-instructions.test.ts
3
- export function buildExternalChannelInstructions(channel) {
3
+ export function buildExternalChannelInstructions(channel, workDir, sessionId) {
4
4
  if (!channel?.configured && !channel?.connected)
5
5
  return '';
6
6
  const channelName = channel.name?.trim() || '外部消息通道';
7
7
  const customPrompt = channel.systemPrompt?.trim();
8
+ const sessionHint = sessionId?.trim()
9
+ ? `当前对话 ID:${sessionId}。如果命令提示缺少外部通道上下文,使用:shennian external send --session-id ${sessionId} --text "<要发送的消息>"`
10
+ : '';
11
+ const workdirHint = workDir?.trim()
12
+ ? `如果使用 shell_command,请设置 workdir 为 ${workDir}。不要用裸 /bin/zsh -lc 或 command_execution 运行此命令,因为那种环境可能拿不到当前对话的外部通道身份。`
13
+ : '如果使用 shell_command,请设置 workdir 为当前对话的项目目录。不要用裸 /bin/zsh -lc 或 command_execution 运行此命令,因为那种环境可能拿不到当前对话的外部通道身份。';
8
14
  const sections = [
9
15
  `当前对话已接入外部消息通道:${channelName}。`,
10
16
  `外部消息会以如下格式进入对话:\n外部消息\n<时间> <用户昵称>: <内容>`,
@@ -13,9 +19,12 @@ export function buildExternalChannelInstructions(channel) {
13
19
  : [
14
20
  '当用户明确要求你向外部消息通道发送内容,或你需要回复一条外部消息时,调用:',
15
21
  'shennian external send --text "<要发送的消息>"',
22
+ sessionHint,
23
+ workdirHint,
16
24
  '只发送用户可见的最终内容,不要发送内部推理、工具日志或实现细节。',
25
+ '对外消息必须像真人聊天:简短、概要、单段;不要使用 Markdown、编号列表、项目符号、真实换行或字面 \\n。',
17
26
  ].join('\n'),
18
- '如果外部消息和当前任务无关,可以忽略或简短说明无需处理;如果处理需要时间,先简短确认,再继续完成任务。',
27
+ '如果外部消息和当前任务无关,可以忽略或简短说明无需处理;如果处理需要时间,先用一句话确认,再继续完成任务。',
19
28
  customPrompt ? `本通道附加约束:${customPrompt}` : '',
20
29
  ].filter(Boolean);
21
30
  return sections.join('\n\n');
@@ -2,10 +2,10 @@
2
2
  // @test src/__tests__/model-switching.test.ts
3
3
  import { createInterface } from 'node:readline';
4
4
  import { resolveBuiltinCommand, spawnResolvedCommand } from '../command-spec.js';
5
- import { fallbackClaudeAliasModels, fallbackGeminiModels, parseCodexAppServerModels, parseCursorModels, parseOpenCodeModels, } from './parsers.js';
5
+ import { fallbackClaudeAliasModels, discoverClaudeAliasModelsFromEnv, fallbackGeminiModels, parseCodexAppServerModels, parseCursorModels, parseOpenCodeModels, } from './parsers.js';
6
6
  import { runResolvedCommand } from './runner.js';
7
7
  import { DISCOVERY_WORKDIR } from './types.js';
8
- import { buildAgentProcessEnv } from '../../agent-env.js';
8
+ import { buildAgentProcessEnv, readLatestUserEnv } from '../../agent-env.js';
9
9
  function sendAppServerRpc(proc, pending, id, method, params, timeoutMs) {
10
10
  if (!proc.stdin)
11
11
  return Promise.reject(new Error('codex app-server stdin unavailable'));
@@ -108,6 +108,9 @@ async function discoverOpenCodeModels() {
108
108
  async function discoverClaudeModels() {
109
109
  if (!resolveBuiltinCommand('claude'))
110
110
  return [];
111
+ const shellEnvModels = discoverClaudeAliasModelsFromEnv(readLatestUserEnv());
112
+ if (shellEnvModels.length > 0)
113
+ return shellEnvModels;
111
114
  return fallbackClaudeAliasModels();
112
115
  }
113
116
  async function discoverCodexModels() {
@@ -4,12 +4,14 @@ export declare function parseCursorModels(raw: string): ModelInfo[];
4
4
  export declare function parseOpenClawModels(raw: string): ModelInfo[];
5
5
  export declare function parseOpenCodeModels(raw: string): ModelInfo[];
6
6
  export declare function parseClaudeModels(raw: string): ModelInfo[];
7
- type EnvLike = Record<string, string | undefined>;
7
+ export type EnvLike = Record<string, string | undefined>;
8
+ export declare function readEnvValue(env: EnvLike, key: string): string | null;
9
+ export declare function inferClaudeOverrideProvider(modelName: string, env: EnvLike): string;
10
+ export declare function discoverClaudeAliasModelsFromEnv(env?: EnvLike): ModelInfo[];
8
11
  export declare function applyClaudeModelEnvOverrides(models: ModelInfo[], env?: EnvLike): ModelInfo[];
9
12
  export declare function parseClaudeBinaryModels(raw: string): ModelInfo[];
10
- export declare function fallbackClaudeAliasModels(): ModelInfo[];
13
+ export declare function fallbackClaudeAliasModels(env?: EnvLike): ModelInfo[];
11
14
  export declare function parseCodexModels(raw: string): ModelInfo[];
12
15
  export declare function parseCodexAppServerModels(raw: unknown): ModelInfo[];
13
16
  export declare function parseGeminiModels(raw: string): ModelInfo[];
14
17
  export declare function fallbackGeminiModels(): ModelInfo[];
15
- export {};
@@ -193,6 +193,9 @@ const CLAUDE_ALIAS_MODEL_ENV = {
193
193
  opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
194
194
  haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
195
195
  };
196
+ const CLAUDE_DEEPSEEK_REASONING_EFFORTS = [
197
+ { id: 'max', name: 'Max' },
198
+ ];
196
199
  const CLAUDE_REASONING_EFFORTS = [
197
200
  { id: 'low', name: 'Low' },
198
201
  { id: 'medium', name: 'Medium' },
@@ -207,17 +210,46 @@ function withClaudeReasoningEfforts(model) {
207
210
  defaultReasoningEffort: 'medium',
208
211
  };
209
212
  }
210
- function readEnvValue(env, key) {
213
+ export function readEnvValue(env, key) {
211
214
  const value = env[key]?.trim();
212
215
  return value || null;
213
216
  }
214
- function inferClaudeOverrideProvider(modelName, env) {
217
+ export function inferClaudeOverrideProvider(modelName, env) {
215
218
  const baseUrl = readEnvValue(env, 'ANTHROPIC_BASE_URL')?.toLowerCase() ?? '';
216
219
  if (baseUrl.includes('deepseek.com') || modelName.toLowerCase().startsWith('deepseek-')) {
217
220
  return 'deepseek';
218
221
  }
219
222
  return baseUrl ? 'custom' : 'anthropic';
220
223
  }
224
+ function configuredClaudeAliasModels(env) {
225
+ const configured = new Set();
226
+ for (const key of Object.values(CLAUDE_ALIAS_MODEL_ENV)) {
227
+ const modelName = readEnvValue(env, key);
228
+ if (modelName)
229
+ configured.add(modelName);
230
+ }
231
+ const models = Array.from(configured).map((modelName, index) => {
232
+ const provider = inferClaudeOverrideProvider(modelName, env);
233
+ const model = {
234
+ id: modelName,
235
+ name: modelName,
236
+ provider,
237
+ isDefault: index === 0,
238
+ };
239
+ if (provider === 'deepseek') {
240
+ return {
241
+ ...model,
242
+ supportedReasoningEfforts: CLAUDE_DEEPSEEK_REASONING_EFFORTS,
243
+ defaultReasoningEffort: readEnvValue(env, 'CLAUDE_CODE_EFFORT_LEVEL') ?? 'max',
244
+ };
245
+ }
246
+ return withClaudeReasoningEfforts(model);
247
+ });
248
+ return uniqueModels(models);
249
+ }
250
+ export function discoverClaudeAliasModelsFromEnv(env = process.env) {
251
+ return configuredClaudeAliasModels(env);
252
+ }
221
253
  export function applyClaudeModelEnvOverrides(models, env = process.env) {
222
254
  return models.map((model) => {
223
255
  if (!['default', 'sonnet', 'opus', 'haiku'].includes(model.id))
@@ -268,13 +300,16 @@ export function parseClaudeBinaryModels(raw) {
268
300
  }
269
301
  return uniqueModels(models);
270
302
  }
271
- export function fallbackClaudeAliasModels() {
303
+ export function fallbackClaudeAliasModels(env = process.env) {
304
+ const configuredModels = configuredClaudeAliasModels(env);
305
+ if (configuredModels.length > 0)
306
+ return configuredModels;
272
307
  return applyClaudeModelEnvOverrides([
273
308
  { id: 'default', name: 'Default (recommended)', provider: 'anthropic', isDefault: true },
274
309
  { id: 'sonnet', name: 'Sonnet', provider: 'anthropic' },
275
310
  { id: 'opus', name: 'Opus', provider: 'anthropic' },
276
311
  { id: 'haiku', name: 'Haiku', provider: 'anthropic' },
277
- ]).map(withClaudeReasoningEfforts);
312
+ ], env).map(withClaudeReasoningEfforts);
278
313
  }
279
314
  export function parseCodexModels(raw) {
280
315
  const clean = stripAnsi(raw);
@@ -1,8 +1,10 @@
1
1
  export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos, } from './model-registry/service.js';
2
- import { applyClaudeModelEnvOverrides, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi } from './model-registry/parsers.js';
2
+ import { applyClaudeModelEnvOverrides, discoverClaudeAliasModelsFromEnv, fallbackClaudeAliasModels, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi } from './model-registry/parsers.js';
3
3
  /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
4
4
  export declare const modelSwitchingTestExports: {
5
5
  readonly applyClaudeModelEnvOverrides: typeof applyClaudeModelEnvOverrides;
6
+ readonly discoverClaudeAliasModelsFromEnv: typeof discoverClaudeAliasModelsFromEnv;
7
+ readonly fallbackClaudeAliasModels: typeof fallbackClaudeAliasModels;
6
8
  readonly parseClaudeBinaryModels: typeof parseClaudeBinaryModels;
7
9
  readonly parseClaudeModels: typeof parseClaudeModels;
8
10
  readonly parseCodexModels: typeof parseCodexModels;
@@ -1,10 +1,12 @@
1
1
  // @arch docs/architecture/cli/model-discovery.md
2
2
  // @test src/__tests__/model-switching.test.ts
3
3
  export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos, } from './model-registry/service.js';
4
- import { applyClaudeModelEnvOverrides, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi, } from './model-registry/parsers.js';
4
+ import { applyClaudeModelEnvOverrides, discoverClaudeAliasModelsFromEnv, fallbackClaudeAliasModels, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi, } from './model-registry/parsers.js';
5
5
  /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
6
6
  export const modelSwitchingTestExports = {
7
7
  applyClaudeModelEnvOverrides,
8
+ discoverClaudeAliasModelsFromEnv,
9
+ fallbackClaudeAliasModels,
8
10
  parseClaudeBinaryModels,
9
11
  parseClaudeModels,
10
12
  parseCodexModels,
@@ -29,9 +29,11 @@ export declare class PiAdapter extends AgentAdapter {
29
29
  private finalizePromise;
30
30
  private sendGeneration;
31
31
  private externalChannel;
32
+ private shennianSessionId;
32
33
  private extraEnv;
33
34
  private pendingSendStart;
34
35
  configure(options: {
36
+ sessionId?: string;
35
37
  externalChannel?: ExternalChannelSessionStatus | null;
36
38
  env?: NodeJS.ProcessEnv;
37
39
  }): void;
@@ -398,9 +398,11 @@ export class PiAdapter extends AgentAdapter {
398
398
  finalizePromise = Promise.resolve();
399
399
  sendGeneration = 0;
400
400
  externalChannel = null;
401
+ shennianSessionId = null;
401
402
  extraEnv = {};
402
403
  pendingSendStart = null;
403
404
  configure(options) {
405
+ this.shennianSessionId = options.sessionId ?? null;
404
406
  this.externalChannel = options.externalChannel ?? null;
405
407
  this.extraEnv = options.env ?? {};
406
408
  }
@@ -497,7 +499,7 @@ export class PiAdapter extends AgentAdapter {
497
499
  systemPrompt: [
498
500
  SYSTEM_PROMPT,
499
501
  `当前工作目录:${workDir}`,
500
- buildExternalChannelInstructions(this.externalChannel),
502
+ buildExternalChannelInstructions(this.externalChannel, workDir, this.shennianSessionId ?? undefined),
501
503
  ].filter(Boolean).join('\n\n'),
502
504
  model: createPiModel(),
503
505
  tools,
@@ -1,4 +1,4 @@
1
1
  import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
2
  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.";
3
- export declare function buildPlatformInstructions(workDir: string, externalChannel?: ExternalChannelSessionStatus | null): string;
4
- export declare function ensurePlatformInstructionsFile(workDir: string, externalChannel?: ExternalChannelSessionStatus | null): string;
3
+ export declare function buildPlatformInstructions(workDir: string, externalChannel?: ExternalChannelSessionStatus | null, sessionId?: string): string;
4
+ export declare function ensurePlatformInstructionsFile(workDir: string, externalChannel?: ExternalChannelSessionStatus | null, sessionId?: string): string;
@@ -23,9 +23,9 @@ function readProjectAgentsMd(workDir) {
23
23
  return '';
24
24
  }
25
25
  }
26
- export function buildPlatformInstructions(workDir, externalChannel) {
26
+ export function buildPlatformInstructions(workDir, externalChannel, sessionId) {
27
27
  const projectInstructions = readProjectAgentsMd(workDir);
28
- const channelInstructions = buildExternalChannelInstructions(externalChannel);
28
+ const channelInstructions = buildExternalChannelInstructions(externalChannel, path.resolve(workDir), sessionId);
29
29
  const sections = [
30
30
  '# Shennian Agent Instructions',
31
31
  projectInstructions
@@ -38,13 +38,13 @@ export function buildPlatformInstructions(workDir, externalChannel) {
38
38
  ].filter(Boolean);
39
39
  return `${sections.join('\n\n')}\n`;
40
40
  }
41
- export function ensurePlatformInstructionsFile(workDir, externalChannel) {
41
+ export function ensurePlatformInstructionsFile(workDir, externalChannel, sessionId) {
42
42
  const key = createHash('sha256')
43
- .update(`${path.resolve(workDir)}:${JSON.stringify(externalChannel ?? null)}`)
43
+ .update(`${path.resolve(workDir)}:${sessionId ?? ''}:${JSON.stringify(externalChannel ?? null)}`)
44
44
  .digest('hex')
45
45
  .slice(0, 16);
46
46
  const filePath = resolveShennianPath('runtime', 'agent-instructions', `${key}.md`);
47
- const content = buildPlatformInstructions(workDir, externalChannel);
47
+ const content = buildPlatformInstructions(workDir, externalChannel, sessionId);
48
48
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
49
49
  try {
50
50
  if (fs.readFileSync(filePath, 'utf8') === content)
@@ -1,18 +1,35 @@
1
1
  // @arch docs/features/wecom-managed-channel.md
2
- // @test src/__tests__/platform-instructions.test.ts
2
+ // @test src/__tests__/external-command.test.ts
3
+ import fs from 'node:fs';
3
4
  import chalk from 'chalk';
4
- function requireExternalContext() {
5
- const url = process.env.SHENNIAN_MANAGER_IPC_URL;
6
- const token = process.env.SHENNIAN_MANAGER_IPC_TOKEN;
7
- const sessionId = process.env.SHENNIAN_EXTERNAL_SESSION_ID || process.env.SHENNIAN_MANAGER_SESSION_ID;
5
+ import { resolveShennianPath } from '../config/index.js';
6
+ function loadManagerIpcFromRuntimeFile() {
7
+ try {
8
+ const parsed = JSON.parse(fs.readFileSync(resolveShennianPath('runtime', 'manager-ipc.json'), 'utf-8'));
9
+ return {
10
+ url: typeof parsed.url === 'string' ? parsed.url : undefined,
11
+ token: typeof parsed.token === 'string' ? parsed.token : undefined,
12
+ };
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ function requireExternalContext(explicitSessionId) {
19
+ const runtimeIpc = !process.env.SHENNIAN_MANAGER_IPC_URL || !process.env.SHENNIAN_MANAGER_IPC_TOKEN
20
+ ? loadManagerIpcFromRuntimeFile()
21
+ : {};
22
+ const url = process.env.SHENNIAN_MANAGER_IPC_URL || runtimeIpc.url;
23
+ const token = process.env.SHENNIAN_MANAGER_IPC_TOKEN || runtimeIpc.token;
24
+ const sessionId = explicitSessionId?.trim() || process.env.SHENNIAN_EXTERNAL_SESSION_ID || process.env.SHENNIAN_MANAGER_SESSION_ID;
8
25
  if (!url || !token || !sessionId) {
9
- console.error(chalk.red('✗ This command must run inside a Shennian conversation with an external channel.'));
26
+ console.error(chalk.red('✗ This command must run inside a Shennian conversation with an external channel, or pass --session-id <id>.'));
10
27
  process.exit(1);
11
28
  }
12
29
  return { url, token, sessionId };
13
30
  }
14
- async function sendExternal(text, idempotencyKey) {
15
- const ctx = requireExternalContext();
31
+ async function sendExternal(text, idempotencyKey, sessionId) {
32
+ const ctx = requireExternalContext(sessionId);
16
33
  const response = await fetch(`${ctx.url}/external/reply`, {
17
34
  method: 'POST',
18
35
  headers: {
@@ -38,8 +55,9 @@ export function registerExternalCommand(program) {
38
55
  .command('send')
39
56
  .description('Send a message to the external channel bound to this conversation')
40
57
  .requiredOption('--text <text>', 'Message text')
58
+ .option('--session-id <id>', 'Shennian conversation/session id; defaults to injected current-session env')
41
59
  .option('--idempotency-key <key>', 'Idempotency key')
42
60
  .action(async (opts) => {
43
- await sendExternal(opts.text, opts.idempotencyKey);
61
+ await sendExternal(opts.text, opts.idempotencyKey, opts.sessionId);
44
62
  });
45
63
  }
@@ -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 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";
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\u201D\u540E\u8DDF\u6D88\u606F\u5185\u5BB9\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- \u5BF9\u5916\u56DE\u590D\u5FC5\u987B\u50CF\u771F\u4EBA\u804A\u5929\uFF1A\u7B80\u77ED\u3001\u6982\u8981\u3001\u5355\u6BB5\uFF1B\u4E0D\u8981\u4F7F\u7528 Markdown\u3001\u7F16\u53F7\u5217\u8868\u3001\u9879\u76EE\u7B26\u53F7\u3001\u771F\u5B9E\u6362\u884C\u6216\u5B57\u9762 \\n\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;
@@ -20,8 +20,9 @@ export const MANAGER_SYSTEM_PROMPT = `你是项目经理,是当前项目的管
20
20
  - 只能管理与你处于同一台机器、同一项目目录的会话;不要跨机器或跨项目调度。
21
21
  - 不要无限循环;没有明确下一步时询问用户或结束当前 turn 等待系统事件。
22
22
  - 不要自己设置定时唤醒;神念会在用户消息、worker 终态或 worker 长运行健康摘要到来时唤醒你。
23
- - 外部消息通道事件会像普通用户消息一样送达,格式类似“外部消息 / 发送人\\n内容”,可能是合并消息,也可能包含图片、视频或文件 URL。
23
+ - 外部消息通道事件会像普通用户消息一样送达,格式类似“外部消息 / 发送人”后跟消息内容,可能是合并消息,也可能包含图片、视频或文件 URL。
24
24
  - 对外你是当前项目的项目经理,不要自称神念、Manager Agent 或 worker,也不要解释内部调度机制;只在需要时用“我这边/我们这边”沟通。
25
+ - 对外回复必须像真人聊天:简短、概要、单段;不要使用 Markdown、编号列表、项目符号、真实换行或字面 \\n。
25
26
  - 外部消息与当前项目无关时可以忽略;需要较长处理时,先简短回复“收到,我先处理/安排一下”,再创建或指派 worker。
26
27
  - 向外部群发消息一律调用 shennian manager external send --text "<消息内容>"
27
28
  - 不要把所有细节塞进对话上下文;需要长期保存的信息写到项目 .shennian/ 下。
@@ -25,6 +25,8 @@ export declare class ManagerRuntimeService {
25
25
  private doStart;
26
26
  ready(): Promise<void>;
27
27
  stop(): Promise<void>;
28
+ private writeIpcRuntimeFile;
29
+ private removeIpcRuntimeFile;
28
30
  getInjectedEnv(managerSessionId: string, agentSessionId: string | null, workDir: string, modelId: string): NodeJS.ProcessEnv;
29
31
  registerManager(input: {
30
32
  sessionId: string;
@@ -2,12 +2,14 @@
2
2
  // @test src/__tests__/manager-runtime.test.ts
3
3
  import http from 'node:http';
4
4
  import { randomBytes, randomUUID } from 'node:crypto';
5
+ import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import { extractPayloadText, isToolPayload } from '@shennian/wire';
8
9
  import { ManagerRegistry } from './registry.js';
9
10
  import { readMessages } from '../session/store.js';
10
11
  import { ChannelRuntime } from '../channels/runtime.js';
12
+ import { resolveShennianPath } from '../config/index.js';
11
13
  let singleton = null;
12
14
  export function setManagerRuntimeService(service) {
13
15
  singleton = service;
@@ -110,6 +112,9 @@ function appJson(runtime, reqId, ok, payload) {
110
112
  function shouldFallbackToLocalChannel(error) {
111
113
  return /binding not found|unknown method|not supported|relay is not connected|no external channel/i.test(error);
112
114
  }
115
+ function managerIpcRuntimePath() {
116
+ return resolveShennianPath('runtime', 'manager-ipc.json');
117
+ }
113
118
  export class ManagerRuntimeService {
114
119
  opts;
115
120
  registry = new ManagerRegistry();
@@ -144,6 +149,7 @@ export class ManagerRuntimeService {
144
149
  const address = this.server.address();
145
150
  if (typeof address === 'object' && address) {
146
151
  this.ipcUrl = `http://127.0.0.1:${address.port}`;
152
+ this.writeIpcRuntimeFile();
147
153
  }
148
154
  this.server.unref();
149
155
  await this.channelRuntime.start();
@@ -166,6 +172,36 @@ export class ManagerRuntimeService {
166
172
  });
167
173
  this.server = null;
168
174
  this.ipcUrl = null;
175
+ this.removeIpcRuntimeFile();
176
+ }
177
+ writeIpcRuntimeFile() {
178
+ if (!this.ipcUrl)
179
+ return;
180
+ try {
181
+ const filePath = managerIpcRuntimePath();
182
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
183
+ fs.writeFileSync(filePath, JSON.stringify({
184
+ url: this.ipcUrl,
185
+ token: this.ipcToken,
186
+ updatedAt: new Date().toISOString(),
187
+ }, null, 2), { mode: 0o600 });
188
+ fs.chmodSync(filePath, 0o600);
189
+ }
190
+ catch {
191
+ // Best effort. Injected env remains the primary path for managed agents.
192
+ }
193
+ }
194
+ removeIpcRuntimeFile() {
195
+ try {
196
+ const filePath = managerIpcRuntimePath();
197
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
198
+ if (parsed.url === this.ipcUrl || parsed.token === this.ipcToken) {
199
+ fs.unlinkSync(filePath);
200
+ }
201
+ }
202
+ catch {
203
+ // The file may not exist or may already have been replaced by a newer daemon.
204
+ }
169
205
  }
170
206
  getInjectedEnv(managerSessionId, agentSessionId, workDir, modelId) {
171
207
  if (!this.ipcUrl)
@@ -90,6 +90,7 @@ function externalChannelEnv(sessionId, channel) {
90
90
  }
91
91
  function configureAdapterForSession(adapter, sessionId, channel) {
92
92
  adapter.configure?.({
93
+ sessionId,
93
94
  externalChannel: channel ?? null,
94
95
  env: externalChannelEnv(sessionId, channel),
95
96
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.53",
3
+ "version": "0.2.55",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {