shennian 0.2.60 → 0.2.62

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.
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
4
4
  import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
- import { buildUserMessagePayload, isToolPayload } from '@shennian/wire';
7
+ import { buildUserMessagePayload, isSystemEnvironmentContextPayload, 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
10
  const DEFAULT_NATIVE_SCAN_IGNORED_PATHS = [
@@ -342,6 +342,8 @@ function parseCodexUserMessage(payload) {
342
342
  : [];
343
343
  if (!text && attachments.length === 0)
344
344
  return null;
345
+ if (isSystemEnvironmentContextPayload(text) && attachments.length === 0)
346
+ return null;
345
347
  if (attachments.length > 0) {
346
348
  return {
347
349
  payload: buildUserMessagePayload(text, attachments),
@@ -350,6 +352,115 @@ function parseCodexUserMessage(payload) {
350
352
  }
351
353
  return { payload: text, titleText: text };
352
354
  }
355
+ function isCodexTerminalEventType(eventType) {
356
+ return eventType === 'turn_completed' || eventType === 'turn_complete' || eventType === 'task_complete';
357
+ }
358
+ function codexMessageContentText(content, textType) {
359
+ if (!Array.isArray(content))
360
+ return '';
361
+ return normalizeText(content
362
+ .map((part) => {
363
+ if (typeof part === 'string')
364
+ return part;
365
+ if (typeof part !== 'object' || part === null)
366
+ return '';
367
+ const record = part;
368
+ if (record.type !== textType)
369
+ return '';
370
+ return typeof record.text === 'string' ? record.text : '';
371
+ })
372
+ .filter(Boolean)
373
+ .join('\n\n'));
374
+ }
375
+ function codexMessageInputImageAttachments(content) {
376
+ if (!Array.isArray(content))
377
+ return [];
378
+ const attachments = [];
379
+ for (const part of content) {
380
+ if (typeof part !== 'object' || part === null)
381
+ continue;
382
+ const record = part;
383
+ if (record.type !== 'input_image')
384
+ continue;
385
+ const imagePath = typeof record.path === 'string' ? record.path
386
+ : typeof record.file_path === 'string' ? record.file_path
387
+ : typeof record.saved_path === 'string' ? record.saved_path
388
+ : typeof record.image_path === 'string' ? record.image_path
389
+ : '';
390
+ if (!imagePath.trim())
391
+ continue;
392
+ attachments.push({
393
+ path: imagePath,
394
+ name: path.basename(imagePath) || 'image.png',
395
+ mimeType: inferMimeType(imagePath),
396
+ kind: 'image',
397
+ });
398
+ }
399
+ return attachments;
400
+ }
401
+ function parseCodexResponseMessage(payload) {
402
+ if (payload.type !== 'message')
403
+ return null;
404
+ const role = payload.role === 'assistant' ? 'agent' : payload.role === 'user' ? 'user' : null;
405
+ if (!role)
406
+ return null;
407
+ if (role === 'agent') {
408
+ const text = codexMessageContentText(payload.content, 'output_text');
409
+ if (isSystemEnvironmentContextPayload(text))
410
+ return null;
411
+ return text ? { role, payload: text, titleText: text } : null;
412
+ }
413
+ const text = codexMessageContentText(payload.content, 'input_text');
414
+ const attachments = codexMessageInputImageAttachments(payload.content);
415
+ if (!text && attachments.length === 0)
416
+ return null;
417
+ if (isSystemEnvironmentContextPayload(text) && attachments.length === 0)
418
+ return null;
419
+ return {
420
+ role,
421
+ payload: attachments.length > 0 ? buildUserMessagePayload(text, attachments) : text,
422
+ titleText: text,
423
+ };
424
+ }
425
+ function codexDedupeText(payload) {
426
+ if (!payload)
427
+ return '';
428
+ try {
429
+ const parsed = JSON.parse(payload);
430
+ if (typeof parsed === 'object' && parsed !== null) {
431
+ const record = parsed;
432
+ if (record.type === 'user' && typeof record.content === 'string')
433
+ return normalizeText(record.content);
434
+ }
435
+ }
436
+ catch {
437
+ /* plain text payload */
438
+ }
439
+ return normalizeText(payload);
440
+ }
441
+ function isDuplicateCodexChatEvent(event, role, payload, ts) {
442
+ if (event.agentType !== 'codex')
443
+ return false;
444
+ if (event.role !== role)
445
+ return false;
446
+ if (isToolPayload(event.payload))
447
+ return false;
448
+ if (Math.abs(event.ts - ts) > 5 * 60 * 1000)
449
+ return false;
450
+ return codexDedupeText(event.payload) === codexDedupeText(payload);
451
+ }
452
+ function findDuplicateCodexChatEventIndex(events, role, payload, ts) {
453
+ for (let i = events.length - 1; i >= 0; i -= 1) {
454
+ const event = events[i];
455
+ if (!event)
456
+ continue;
457
+ if (isDuplicateCodexChatEvent(event, role, payload, ts))
458
+ return i;
459
+ if (event.agentType === 'codex' && Math.abs(event.ts - ts) > 5 * 60 * 1000)
460
+ break;
461
+ }
462
+ return -1;
463
+ }
353
464
  function pushCodexEvent(events, filePath, lineOffset, kind, sourceSessionKey, ts, payload, title, modelId, workDir, role = 'agent', terminal = false) {
354
465
  if (!payload)
355
466
  return;
@@ -490,6 +601,16 @@ function parseCodexResponseItem(events, filePath, lineOffset, payload, sourceSes
490
601
  const itemType = typeof payload.type === 'string' ? payload.type : '';
491
602
  if (!itemType)
492
603
  return;
604
+ if (itemType === 'message') {
605
+ const parsedMessage = parseCodexResponseMessage(payload);
606
+ if (!parsedMessage)
607
+ return;
608
+ const duplicateIndex = findDuplicateCodexChatEventIndex(events, parsedMessage.role, parsedMessage.payload, ts);
609
+ if (duplicateIndex >= 0)
610
+ return;
611
+ pushCodexEvent(events, filePath, lineOffset, `${itemType}:${parsedMessage.role}`, sourceSessionKey, ts, parsedMessage.payload, title, modelId, workDir, parsedMessage.role);
612
+ return;
613
+ }
493
614
  if (itemType === 'function_call') {
494
615
  const name = typeof payload.name === 'string' ? payload.name : 'function_call';
495
616
  pushCodexToolEvent(events, filePath, lineOffset, itemType, sourceSessionKey, ts, name, title, modelId, workDir, parseStructuredString(payload.arguments));
@@ -532,7 +653,7 @@ function parseCodexEventMessage(events, filePath, lineOffset, payload, sourceSes
532
653
  }
533
654
  if (eventType === 'agent_message') {
534
655
  const text = typeof payload.message === 'string' ? normalizeText(payload.message) : '';
535
- if (!text)
656
+ if (!text || isSystemEnvironmentContextPayload(text))
536
657
  return;
537
658
  pushCodexEvent(events, filePath, lineOffset, eventType, sourceSessionKey, ts, text, title, modelId, workDir);
538
659
  return;
@@ -803,12 +924,16 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
803
924
  if (!sourceSessionKey)
804
925
  return;
805
926
  if (type === 'response_item') {
927
+ const parsedMessage = parseCodexResponseMessage(payload);
928
+ if (parsedMessage?.role === 'user' && parsedMessage.titleText && !title) {
929
+ title = parsedMessage.titleText.slice(0, 80);
930
+ }
806
931
  parseCodexResponseItem(events, filePath, lineOffset, payload, sourceSessionKey, ts, title, modelId, workDir);
807
932
  return;
808
933
  }
809
934
  if (type === 'event_msg') {
810
935
  const eventType = typeof payload.type === 'string' ? payload.type : '';
811
- if (eventType === 'turn_completed') {
936
+ if (isCodexTerminalEventType(eventType)) {
812
937
  for (let i = events.length - 1; i >= 0; i -= 1) {
813
938
  const event = events[i];
814
939
  if (event?.agentType !== 'codex')
@@ -827,6 +952,19 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
827
952
  const parsedUser = parseCodexUserMessage(payload);
828
953
  if (parsedUser?.titleText && !title)
829
954
  title = parsedUser.titleText.slice(0, 80);
955
+ if (parsedUser) {
956
+ const duplicateIndex = findDuplicateCodexChatEventIndex(events, 'user', parsedUser.payload, ts);
957
+ if (duplicateIndex >= 0)
958
+ events.splice(duplicateIndex, 1);
959
+ }
960
+ }
961
+ else if (eventType === 'agent_message') {
962
+ const text = typeof payload.message === 'string' ? normalizeText(payload.message) : '';
963
+ if (text && !isSystemEnvironmentContextPayload(text)) {
964
+ const duplicateIndex = findDuplicateCodexChatEventIndex(events, 'agent', text, ts);
965
+ if (duplicateIndex >= 0)
966
+ events.splice(duplicateIndex, 1);
967
+ }
830
968
  }
831
969
  const beforeCount = events.length;
832
970
  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.60",
3
+ "version": "0.2.62",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {