shennian 0.2.51 → 0.2.52

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,7 +25,7 @@ export interface AgentAdapterEvents {
25
25
  export declare abstract class AgentAdapter extends EventEmitter<AgentAdapterEvents> {
26
26
  abstract readonly type: AgentType;
27
27
  abstract start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
28
- abstract send(text: string, modelId?: string): Promise<void>;
28
+ abstract send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
29
29
  abstract resume(agentSessionId: string): Promise<void>;
30
30
  abstract stop(): Promise<void>;
31
31
  }
@@ -1,5 +1,6 @@
1
1
  import { AgentAdapter } from './adapter.js';
2
2
  export declare function normalizeClaudeModelId(modelId?: string | null): string;
3
+ export declare function normalizeClaudeReasoningEffort(reasoningEffort?: string | null): string | undefined;
3
4
  export declare class ClaudeAdapter extends AgentAdapter {
4
5
  private readonly options;
5
6
  readonly type: "claude";
@@ -16,7 +17,7 @@ export declare class ClaudeAdapter extends AgentAdapter {
16
17
  hidden?: boolean;
17
18
  });
18
19
  start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
19
- send(text: string, modelId?: string): Promise<void>;
20
+ send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
20
21
  resume(agentSessionId: string): Promise<void>;
21
22
  stop(): Promise<void>;
22
23
  private spawnAndParse;
@@ -8,6 +8,15 @@ export function normalizeClaudeModelId(modelId) {
8
8
  const trimmed = modelId?.trim();
9
9
  return trimmed || 'default';
10
10
  }
11
+ const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
12
+ export function normalizeClaudeReasoningEffort(reasoningEffort) {
13
+ const trimmed = reasoningEffort?.trim();
14
+ if (!trimmed)
15
+ return undefined;
16
+ if (CLAUDE_REASONING_EFFORTS.has(trimmed))
17
+ return trimmed;
18
+ throw new Error(`Unsupported Claude reasoning effort "${trimmed}". Supported values: low, medium, high, xhigh, max.`);
19
+ }
11
20
  export class ClaudeAdapter extends AgentAdapter {
12
21
  options;
13
22
  type = 'claude';
@@ -30,7 +39,7 @@ export class ClaudeAdapter extends AgentAdapter {
30
39
  if (agentSessionId)
31
40
  this.agentSessionId = agentSessionId;
32
41
  }
33
- async send(text, modelId) {
42
+ async send(text, modelId, reasoningEffort) {
34
43
  await this.killProcess();
35
44
  this.runId = randomUUID();
36
45
  this.resetRunState();
@@ -46,6 +55,10 @@ export class ClaudeAdapter extends AgentAdapter {
46
55
  args.push('--dangerously-skip-permissions');
47
56
  }
48
57
  args.push('--model', normalizeClaudeModelId(modelId));
58
+ const effort = normalizeClaudeReasoningEffort(reasoningEffort);
59
+ if (effort) {
60
+ args.push('--effort', effort);
61
+ }
49
62
  if (this.agentSessionId) {
50
63
  args.push('--resume', this.agentSessionId);
51
64
  }
@@ -26,7 +26,7 @@ export declare class CodexAdapter extends AgentAdapter {
26
26
  hidden?: boolean;
27
27
  });
28
28
  start(_sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
29
- send(text: string, modelId?: string): Promise<void>;
29
+ send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
30
30
  resume(agentSessionId: string): Promise<void>;
31
31
  stop(): Promise<void>;
32
32
  private spawnCodex;
@@ -58,4 +58,6 @@ export declare class CodexAdapter extends AgentAdapter {
58
58
  private clearForceCloseTimer;
59
59
  }
60
60
  export declare function normalizeCodexModelId(modelId?: string | null): string | undefined;
61
+ export declare function normalizeCodexReasoningEffort(reasoningEffort?: string | null): string | undefined;
61
62
  export declare function isMissingCodexRolloutError(error: unknown): boolean;
63
+ export declare function isCodexUnsupportedEffortError(error: unknown): boolean;
@@ -38,12 +38,13 @@ export class CodexAdapter extends AgentAdapter {
38
38
  if (agentSessionId)
39
39
  this.agentSessionId = agentSessionId;
40
40
  }
41
- async send(text, modelId) {
41
+ async send(text, modelId, reasoningEffort) {
42
42
  if (this.activeTurnId) {
43
43
  await this.interruptActiveTurn().catch(() => { });
44
44
  await this.killProcess();
45
45
  }
46
46
  const codexModelId = normalizeCodexModelId(modelId);
47
+ const codexReasoningEffort = normalizeCodexReasoningEffort(reasoningEffort);
47
48
  this.runId = randomUUID();
48
49
  this.seq = 0;
49
50
  this.resetRunState();
@@ -59,7 +60,7 @@ export class CodexAdapter extends AgentAdapter {
59
60
  }).catch(() => { });
60
61
  this.namedThread = true;
61
62
  }
62
- const response = await this.startTurnWithRecovery(threadId, text, codexModelId);
63
+ const response = await this.startTurnWithRecovery(threadId, text, codexModelId, codexReasoningEffort);
63
64
  this.activeTurnId = response.turn?.id ?? null;
64
65
  }
65
66
  async resume(agentSessionId) {
@@ -227,26 +228,35 @@ export class CodexAdapter extends AgentAdapter {
227
228
  this.agentSessionId = threadId;
228
229
  this.namedThread = !!response.thread?.name;
229
230
  }
230
- async startTurnWithRecovery(threadId, text, codexModelId) {
231
+ async startTurnWithRecovery(threadId, text, codexModelId, reasoningEffort) {
231
232
  try {
232
- return await this.startTurn(threadId, text, codexModelId);
233
+ return await this.startTurn(threadId, text, codexModelId, reasoningEffort);
233
234
  }
234
235
  catch (error) {
235
236
  if (!isMissingCodexRolloutError(error))
236
237
  throw error;
237
238
  await this.killProcess();
238
239
  await this.ensureAppServer(codexModelId);
239
- return await this.startTurn(threadId, text, codexModelId);
240
+ return await this.startTurn(threadId, text, codexModelId, reasoningEffort);
240
241
  }
241
242
  }
242
- async startTurn(threadId, text, codexModelId) {
243
- return await this.sendRpc('turn/start', {
244
- threadId,
245
- input: [{ type: 'text', text, text_elements: [] }],
246
- approvalPolicy: 'never',
247
- sandboxPolicy: { type: 'dangerFullAccess' },
248
- ...(codexModelId ? { model: codexModelId } : {}),
249
- });
243
+ async startTurn(threadId, text, codexModelId, reasoningEffort) {
244
+ try {
245
+ return await this.sendRpc('turn/start', {
246
+ threadId,
247
+ input: [{ type: 'text', text, text_elements: [] }],
248
+ approvalPolicy: 'never',
249
+ sandboxPolicy: { type: 'dangerFullAccess' },
250
+ ...(codexModelId ? { model: codexModelId } : {}),
251
+ ...(reasoningEffort ? { effort: reasoningEffort } : {}),
252
+ });
253
+ }
254
+ catch (error) {
255
+ if (reasoningEffort && isCodexUnsupportedEffortError(error)) {
256
+ throw new Error(`Codex app-server does not accept reasoning effort "${reasoningEffort}" for this turn. Refresh models or upgrade Codex CLI, then retry.`);
257
+ }
258
+ throw error;
259
+ }
250
260
  }
251
261
  async interruptActiveTurn() {
252
262
  const threadId = this.agentSessionId;
@@ -793,10 +803,21 @@ export function normalizeCodexModelId(modelId) {
793
803
  return undefined;
794
804
  return trimmed.toLowerCase() === 'openai' ? undefined : trimmed;
795
805
  }
806
+ export function normalizeCodexReasoningEffort(reasoningEffort) {
807
+ const trimmed = reasoningEffort?.trim();
808
+ return trimmed || undefined;
809
+ }
796
810
  export function isMissingCodexRolloutError(error) {
797
811
  const message = error instanceof Error ? error.message : String(error ?? '');
798
812
  return /\bno rollout found for thread id\b/i.test(message);
799
813
  }
814
+ export function isCodexUnsupportedEffortError(error) {
815
+ const message = error instanceof Error ? error.message : String(error ?? '');
816
+ return (/\bunknown field\b.*\beffort\b/i.test(message) ||
817
+ /\binvalid.*\beffort\b/i.test(message) ||
818
+ /\bunsupported.*\beffort\b/i.test(message) ||
819
+ /\breasoning effort\b/i.test(message));
820
+ }
800
821
  function extractAppServerErrorMessage(params) {
801
822
  if (typeof params.message === 'string' && params.message.trim())
802
823
  return params.message.trim();
@@ -123,13 +123,13 @@ export function parseClaudeModels(raw) {
123
123
  for (const [pattern, id, name] of patterns) {
124
124
  const match = rawClean.match(pattern);
125
125
  if (match) {
126
- models.push({
126
+ models.push(withClaudeReasoningEfforts({
127
127
  id,
128
128
  name,
129
129
  description: `v${match[1]}`,
130
130
  provider: 'anthropic',
131
131
  isDefault: id === 'default',
132
- });
132
+ }));
133
133
  }
134
134
  }
135
135
  if (models.length > 0)
@@ -144,12 +144,12 @@ export function parseClaudeModels(raw) {
144
144
  continue;
145
145
  const version = match[2] ?? '';
146
146
  const isDefault = defaultMatch && defaultMatch[1]?.toLowerCase() === family && defaultMatch[2] === version;
147
- fallback.push({
147
+ fallback.push(withClaudeReasoningEfforts({
148
148
  id: alias,
149
149
  name: `${titleCaseSegment(family)} ${version}`,
150
150
  provider: 'anthropic',
151
151
  isDefault: Boolean(isDefault),
152
- });
152
+ }));
153
153
  }
154
154
  return uniqueModels(fallback);
155
155
  }
@@ -193,6 +193,20 @@ const CLAUDE_ALIAS_MODEL_ENV = {
193
193
  opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
194
194
  haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
195
195
  };
196
+ const CLAUDE_REASONING_EFFORTS = [
197
+ { id: 'low', name: 'Low' },
198
+ { id: 'medium', name: 'Medium' },
199
+ { id: 'high', name: 'High' },
200
+ { id: 'xhigh', name: 'Extra High' },
201
+ { id: 'max', name: 'Max' },
202
+ ];
203
+ function withClaudeReasoningEfforts(model) {
204
+ return {
205
+ ...model,
206
+ supportedReasoningEfforts: CLAUDE_REASONING_EFFORTS,
207
+ defaultReasoningEffort: 'medium',
208
+ };
209
+ }
196
210
  function readEnvValue(env, key) {
197
211
  const value = env[key]?.trim();
198
212
  return value || null;
@@ -244,13 +258,13 @@ export function parseClaudeBinaryModels(raw) {
244
258
  if (!new RegExp(`\\b${alias}\\b`, 'i').test(clean))
245
259
  continue;
246
260
  const version = (alias === 'default' ? familyVersions.get('sonnet') : familyVersions.get(alias)) ?? null;
247
- models.push({
261
+ models.push(withClaudeReasoningEfforts({
248
262
  id: alias,
249
263
  name: CLAUDE_ALIAS_LABELS[alias],
250
264
  description: formatClaudeVersion(version),
251
265
  provider: 'anthropic',
252
266
  isDefault: alias === 'default',
253
- });
267
+ }));
254
268
  }
255
269
  return uniqueModels(models);
256
270
  }
@@ -260,7 +274,7 @@ export function fallbackClaudeAliasModels() {
260
274
  { id: 'sonnet', name: 'Sonnet', provider: 'anthropic' },
261
275
  { id: 'opus', name: 'Opus', provider: 'anthropic' },
262
276
  { id: 'haiku', name: 'Haiku', provider: 'anthropic' },
263
- ]);
277
+ ]).map(withClaudeReasoningEfforts);
264
278
  }
265
279
  export function parseCodexModels(raw) {
266
280
  const clean = stripAnsi(raw);
@@ -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
- if (isClaudeSubagentTranscript(filePath)) {
809
- return { nextOffset: fs.statSync(filePath).size, events: [] };
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: readClaudeEventCwd(parsed) ?? fallbackWorkDir,
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: readClaudeEventCwd(parsed) ?? fallbackWorkDir,
911
+ workDir: eventCwd ?? fallbackWorkDir,
871
912
  });
872
913
  }
873
914
  });
@@ -283,7 +283,7 @@ export async function handleChatSend(runtime, req) {
283
283
  return;
284
284
  }
285
285
  rememberProcessedReqId(runtime, req.id);
286
- const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
286
+ const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, reasoningEffort, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
287
287
  mergeProjectedSessions(sessionListProjection);
288
288
  if (!sessionId || !text) {
289
289
  runtime.processedReqIds.delete(req.id);
@@ -291,6 +291,11 @@ export async function handleChatSend(runtime, req) {
291
291
  return;
292
292
  }
293
293
  const requestedAgentType = agentType;
294
+ const resolvedReasoningEffort = (requestedAgentType === 'claude' || requestedAgentType === 'codex') &&
295
+ typeof reasoningEffort === 'string' &&
296
+ reasoningEffort.trim()
297
+ ? reasoningEffort.trim()
298
+ : undefined;
294
299
  const resolvedWorkDir = runtime.resolvePath(maybeResolveClaudeImportedWorkDir(requestedAgentType, workDir || os.homedir(), incomingAgentSid) || os.homedir());
295
300
  let session = runtime.sessions.get(sessionId);
296
301
  if (session) {
@@ -365,7 +370,7 @@ export async function handleChatSend(runtime, req) {
365
370
  level: 'info',
366
371
  sessionId,
367
372
  wsEvent: 'chat.send.start',
368
- metadata: { reqId: req.id, agentType: requestedAgentType, modelId },
373
+ metadata: { reqId: req.id, agentType: requestedAgentType, modelId, reasoningEffort: resolvedReasoningEffort },
369
374
  });
370
375
  const markAccepted = () => {
371
376
  sendSessionUpdateEvent(runtime, {
@@ -424,7 +429,10 @@ export async function handleChatSend(runtime, req) {
424
429
  };
425
430
  if (waitForDispatch) {
426
431
  try {
427
- await session.adapter.send(text, modelId);
432
+ if (resolvedReasoningEffort)
433
+ await session.adapter.send(text, modelId, resolvedReasoningEffort);
434
+ else
435
+ await session.adapter.send(text, modelId);
428
436
  reportLog({
429
437
  level: 'info',
430
438
  sessionId,
@@ -454,7 +462,10 @@ export async function handleChatSend(runtime, req) {
454
462
  wsEvent: 'chat.send.res',
455
463
  metadata: { reqId: req.id, ok: true },
456
464
  });
457
- void session.adapter.send(text, modelId)
465
+ const sendPromise = resolvedReasoningEffort
466
+ ? session.adapter.send(text, modelId, resolvedReasoningEffort)
467
+ : session.adapter.send(text, modelId);
468
+ void sendPromise
458
469
  .then(() => {
459
470
  reportLog({
460
471
  level: 'info',
@@ -53,6 +53,7 @@ function queueMessageFromParams(params) {
53
53
  workDir: params.workDir,
54
54
  agentSessionId: params.agentSessionId ?? null,
55
55
  modelId: params.modelId ?? null,
56
+ reasoningEffort: params.reasoningEffort ?? null,
56
57
  clientMessageId: params.clientMessageId ?? null,
57
58
  attachments: normalizeAttachments(params.attachments),
58
59
  createdAt: timestamp,
@@ -233,6 +234,7 @@ export class ChatQueueManager {
233
234
  workDir: message.workDir,
234
235
  agentSessionId: message.agentSessionId ?? null,
235
236
  modelId: message.modelId ?? undefined,
237
+ reasoningEffort: message.reasoningEffort ?? undefined,
236
238
  clientMessageId: message.clientMessageId ?? message.id,
237
239
  attachments: message.attachments,
238
240
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.51",
3
+ "version": "0.2.52",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {