shennian 0.2.58 → 0.2.61

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.
@@ -471,12 +471,12 @@ export class PiAdapter extends AgentAdapter {
471
471
  return;
472
472
  this.terminalState = 'error';
473
473
  const message = err instanceof Error ? err.message : String(err);
474
- if (message.includes('429') || message.includes('daily_quota_exceeded')) {
474
+ if (message.includes('429') || message.includes('daily_quota_exceeded') || message.includes('nian_quota_exceeded')) {
475
475
  this.emit('agentEvent', {
476
476
  state: 'error',
477
477
  runId,
478
478
  seq: ++this.seq,
479
- message: '今日免费额度已用完,可在 ~/.shennian/config.json 中配置 apiKey 字段(兼容 OpenAI 格式)继续使用。',
479
+ message: 'Nian 今日额度已用完,次日自动恢复。',
480
480
  });
481
481
  }
482
482
  else {
@@ -257,7 +257,7 @@ export class ManagerRegistry {
257
257
  function readableText(message) {
258
258
  if (!message || isToolPayload(message.payload))
259
259
  return null;
260
- const text = extractPayloadText(message.payload).replace(/\s+/g, ' ').trim();
260
+ const text = extractPayloadText(message.payload).replace(/\r\n/g, '\n').trim();
261
261
  return text || null;
262
262
  }
263
263
  function clip(text, max) {
@@ -29,6 +29,16 @@ function runIdFromMessageId(id) {
29
29
  const match = /^agent-(.+)-\d+$/.exec(id);
30
30
  return match?.[1] ?? null;
31
31
  }
32
+ function seqFromMessageId(id) {
33
+ const match = /^agent-.+-(\d+)$/.exec(id);
34
+ if (!match)
35
+ return null;
36
+ const seq = Number(match[1]);
37
+ return Number.isInteger(seq) && seq >= 0 ? seq : null;
38
+ }
39
+ function normalizeMarkdownForWorkerSummary(text) {
40
+ return text.replace(/\r\n/g, '\n').trim();
41
+ }
32
42
  function toolSummary(payload) {
33
43
  try {
34
44
  const parsed = JSON.parse(payload);
@@ -62,6 +72,7 @@ function compactWorkerTranscript(rawMessages, limit) {
62
72
  const compacted = [];
63
73
  let buffer = null;
64
74
  let bufferRunId = null;
75
+ let bufferSeq = null;
65
76
  let bufferText = '';
66
77
  const flush = () => {
67
78
  if (!buffer)
@@ -76,6 +87,7 @@ function compactWorkerTranscript(rawMessages, limit) {
76
87
  }
77
88
  buffer = null;
78
89
  bufferRunId = null;
90
+ bufferSeq = null;
79
91
  bufferText = '';
80
92
  };
81
93
  for (const message of chronological) {
@@ -96,14 +108,17 @@ function compactWorkerTranscript(rawMessages, limit) {
96
108
  if (!text.trim())
97
109
  continue;
98
110
  const runId = runIdFromMessageId(message.id);
99
- if (buffer && buffer.role === message.role && bufferRunId === runId) {
111
+ const seq = seqFromMessageId(message.id);
112
+ if (buffer && buffer.role === message.role && bufferRunId === runId && runId && seq !== null && bufferSeq !== null && seq === bufferSeq + 1) {
100
113
  bufferText += text;
101
114
  buffer.ts = message.ts;
115
+ bufferSeq = seq;
102
116
  }
103
117
  else {
104
118
  flush();
105
119
  buffer = message;
106
120
  bufferRunId = runId;
121
+ bufferSeq = seq;
107
122
  bufferText = text;
108
123
  }
109
124
  }
@@ -261,14 +276,14 @@ export class ManagerRuntimeService {
261
276
  if (event.state === 'delta' && event.text && !event.thinking) {
262
277
  const nextText = (this.workerTextAcc.get(textKey) ?? '') + event.text;
263
278
  this.workerTextAcc.set(textKey, nextText);
264
- const normalized = nextText.replace(/\s+/g, ' ').trim();
279
+ const normalized = normalizeMarkdownForWorkerSummary(nextText);
265
280
  if (normalized) {
266
281
  patch.summary = normalized.length > 160 ? `${normalized.slice(0, 160)}...` : normalized;
267
282
  }
268
283
  }
269
284
  if (event.state === 'final' || event.state === 'error' || event.state === 'aborted') {
270
285
  patch.status = event.state;
271
- const accumulated = this.workerTextAcc.get(textKey)?.replace(/\s+/g, ' ').trim();
286
+ const accumulated = normalizeMarkdownForWorkerSummary(this.workerTextAcc.get(textKey) ?? '');
272
287
  if (accumulated) {
273
288
  patch.summary = accumulated.length > 240 ? `${accumulated.slice(0, 240)}...` : accumulated;
274
289
  }
@@ -350,6 +350,111 @@ function parseCodexUserMessage(payload) {
350
350
  }
351
351
  return { payload: text, titleText: text };
352
352
  }
353
+ function isCodexTerminalEventType(eventType) {
354
+ return eventType === 'turn_completed' || eventType === 'turn_complete' || eventType === 'task_complete';
355
+ }
356
+ function codexMessageContentText(content, textType) {
357
+ if (!Array.isArray(content))
358
+ return '';
359
+ return normalizeText(content
360
+ .map((part) => {
361
+ if (typeof part === 'string')
362
+ return part;
363
+ if (typeof part !== 'object' || part === null)
364
+ return '';
365
+ const record = part;
366
+ if (record.type !== textType)
367
+ return '';
368
+ return typeof record.text === 'string' ? record.text : '';
369
+ })
370
+ .filter(Boolean)
371
+ .join('\n\n'));
372
+ }
373
+ function codexMessageInputImageAttachments(content) {
374
+ if (!Array.isArray(content))
375
+ return [];
376
+ const attachments = [];
377
+ for (const part of content) {
378
+ if (typeof part !== 'object' || part === null)
379
+ continue;
380
+ const record = part;
381
+ if (record.type !== 'input_image')
382
+ continue;
383
+ const imagePath = typeof record.path === 'string' ? record.path
384
+ : typeof record.file_path === 'string' ? record.file_path
385
+ : typeof record.saved_path === 'string' ? record.saved_path
386
+ : typeof record.image_path === 'string' ? record.image_path
387
+ : '';
388
+ if (!imagePath.trim())
389
+ continue;
390
+ attachments.push({
391
+ path: imagePath,
392
+ name: path.basename(imagePath) || 'image.png',
393
+ mimeType: inferMimeType(imagePath),
394
+ kind: 'image',
395
+ });
396
+ }
397
+ return attachments;
398
+ }
399
+ function parseCodexResponseMessage(payload) {
400
+ if (payload.type !== 'message')
401
+ return null;
402
+ const role = payload.role === 'assistant' ? 'agent' : payload.role === 'user' ? 'user' : null;
403
+ if (!role)
404
+ return null;
405
+ if (role === 'agent') {
406
+ const text = codexMessageContentText(payload.content, 'output_text');
407
+ return text ? { role, payload: text, titleText: text } : null;
408
+ }
409
+ const text = codexMessageContentText(payload.content, 'input_text');
410
+ const attachments = codexMessageInputImageAttachments(payload.content);
411
+ if (!text && attachments.length === 0)
412
+ return null;
413
+ return {
414
+ role,
415
+ payload: attachments.length > 0 ? buildUserMessagePayload(text, attachments) : text,
416
+ titleText: text,
417
+ };
418
+ }
419
+ function codexDedupeText(payload) {
420
+ if (!payload)
421
+ return '';
422
+ try {
423
+ const parsed = JSON.parse(payload);
424
+ if (typeof parsed === 'object' && parsed !== null) {
425
+ const record = parsed;
426
+ if (record.type === 'user' && typeof record.content === 'string')
427
+ return normalizeText(record.content);
428
+ }
429
+ }
430
+ catch {
431
+ /* plain text payload */
432
+ }
433
+ return normalizeText(payload);
434
+ }
435
+ function isDuplicateCodexChatEvent(event, role, payload, ts) {
436
+ if (event.agentType !== 'codex')
437
+ return false;
438
+ if (event.role !== role)
439
+ return false;
440
+ if (isToolPayload(event.payload))
441
+ return false;
442
+ if (Math.abs(event.ts - ts) > 5 * 60 * 1000)
443
+ return false;
444
+ return codexDedupeText(event.payload) === codexDedupeText(payload);
445
+ }
446
+ function findDuplicateCodexChatEventIndex(events, role, payload, ts) {
447
+ for (let i = events.length - 1; i >= 0; i -= 1) {
448
+ const event = events[i];
449
+ if (!event)
450
+ continue;
451
+ if (isDuplicateCodexChatEvent(event, role, payload, ts))
452
+ return i;
453
+ if (event.agentType === 'codex' && Math.abs(event.ts - ts) > 5 * 60 * 1000)
454
+ break;
455
+ }
456
+ return -1;
457
+ }
353
458
  function pushCodexEvent(events, filePath, lineOffset, kind, sourceSessionKey, ts, payload, title, modelId, workDir, role = 'agent', terminal = false) {
354
459
  if (!payload)
355
460
  return;
@@ -490,6 +595,16 @@ function parseCodexResponseItem(events, filePath, lineOffset, payload, sourceSes
490
595
  const itemType = typeof payload.type === 'string' ? payload.type : '';
491
596
  if (!itemType)
492
597
  return;
598
+ if (itemType === 'message') {
599
+ const parsedMessage = parseCodexResponseMessage(payload);
600
+ if (!parsedMessage)
601
+ return;
602
+ const duplicateIndex = findDuplicateCodexChatEventIndex(events, parsedMessage.role, parsedMessage.payload, ts);
603
+ if (duplicateIndex >= 0)
604
+ return;
605
+ pushCodexEvent(events, filePath, lineOffset, `${itemType}:${parsedMessage.role}`, sourceSessionKey, ts, parsedMessage.payload, title, modelId, workDir, parsedMessage.role);
606
+ return;
607
+ }
493
608
  if (itemType === 'function_call') {
494
609
  const name = typeof payload.name === 'string' ? payload.name : 'function_call';
495
610
  pushCodexToolEvent(events, filePath, lineOffset, itemType, sourceSessionKey, ts, name, title, modelId, workDir, parseStructuredString(payload.arguments));
@@ -803,12 +918,16 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
803
918
  if (!sourceSessionKey)
804
919
  return;
805
920
  if (type === 'response_item') {
921
+ const parsedMessage = parseCodexResponseMessage(payload);
922
+ if (parsedMessage?.role === 'user' && parsedMessage.titleText && !title) {
923
+ title = parsedMessage.titleText.slice(0, 80);
924
+ }
806
925
  parseCodexResponseItem(events, filePath, lineOffset, payload, sourceSessionKey, ts, title, modelId, workDir);
807
926
  return;
808
927
  }
809
928
  if (type === 'event_msg') {
810
929
  const eventType = typeof payload.type === 'string' ? payload.type : '';
811
- if (eventType === 'turn_completed') {
930
+ if (isCodexTerminalEventType(eventType)) {
812
931
  for (let i = events.length - 1; i >= 0; i -= 1) {
813
932
  const event = events[i];
814
933
  if (event?.agentType !== 'codex')
@@ -827,6 +946,19 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
827
946
  const parsedUser = parseCodexUserMessage(payload);
828
947
  if (parsedUser?.titleText && !title)
829
948
  title = parsedUser.titleText.slice(0, 80);
949
+ if (parsedUser) {
950
+ const duplicateIndex = findDuplicateCodexChatEventIndex(events, 'user', parsedUser.payload, ts);
951
+ if (duplicateIndex >= 0)
952
+ events.splice(duplicateIndex, 1);
953
+ }
954
+ }
955
+ else if (eventType === 'agent_message') {
956
+ const text = typeof payload.message === 'string' ? normalizeText(payload.message) : '';
957
+ if (text) {
958
+ const duplicateIndex = findDuplicateCodexChatEventIndex(events, 'agent', text, ts);
959
+ if (duplicateIndex >= 0)
960
+ events.splice(duplicateIndex, 1);
961
+ }
830
962
  }
831
963
  const beforeCount = events.length;
832
964
  parseCodexEventMessage(events, filePath, lineOffset, payload, sourceSessionKey, ts, title, modelId, workDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.58",
3
+ "version": "0.2.61",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {