cogpit-memory 0.4.1 → 0.4.2

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/cli.js CHANGED
@@ -1871,6 +1871,62 @@ function appendAssistantText(turn, text, timestamp) {
1871
1871
  turn.contentBlocks.push({ kind: "text", text: [text], timestamp });
1872
1872
  }
1873
1873
 
1874
+ // src/lib/automatedPrompts.ts
1875
+ var CREW_CHAT_DELIVERY_RE = /^Crew chat · ([^\n]+) · message (\d+)(?:\n|$)/;
1876
+ var TASK_WAKEUP_START_RE = /^Delegated task \S+ (?:running|completed|error|cancelled|needs attention)\. Child session: /;
1877
+ function isReaderPrompt(text) {
1878
+ const trimmed = text.trim();
1879
+ if (!trimmed) return false;
1880
+ if (CREW_CHAT_DELIVERY_RE.test(trimmed) || TASK_WAKEUP_START_RE.test(trimmed)) return false;
1881
+ return parseAgentEnvelope(trimmed).sender === null;
1882
+ }
1883
+
1884
+ // src/lib/asyncAnswers.ts
1885
+ function promptText(content) {
1886
+ if (content === null) return "";
1887
+ if (typeof content === "string") return content;
1888
+ return content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
1889
+ }
1890
+ function linkAsyncAnswers(turns) {
1891
+ const answers = /* @__PURE__ */ new Map();
1892
+ let open4 = [];
1893
+ const answer = (text) => {
1894
+ if (open4.length === 0 || !isReaderPrompt(text)) return;
1895
+ for (const id of open4) answers.set(id, text.trim());
1896
+ open4 = [];
1897
+ };
1898
+ for (const turn of turns) {
1899
+ answer(promptText(turn.userMessage));
1900
+ for (const block of turn.contentBlocks) {
1901
+ if (block.kind === "queued_prompt") answer(block.content);
1902
+ else if (block.kind === "tool_calls") {
1903
+ for (const call of block.toolCalls) if (call.asyncQuestion) open4.push(call.id);
1904
+ }
1905
+ }
1906
+ }
1907
+ const stale = (call) => call.asyncQuestion === true && call.asyncAnswer !== answers.get(call.id);
1908
+ const relinked = /* @__PURE__ */ new Map();
1909
+ const relink = (call) => {
1910
+ if (!stale(call)) return call;
1911
+ let next = relinked.get(call);
1912
+ if (!next) {
1913
+ const { asyncAnswer: _previous, ...rest } = call;
1914
+ const reply = answers.get(call.id);
1915
+ next = reply === void 0 ? rest : { ...rest, asyncAnswer: reply };
1916
+ relinked.set(call, next);
1917
+ }
1918
+ return next;
1919
+ };
1920
+ return turns.map((turn) => {
1921
+ if (!turn.toolCalls.some(stale)) return turn;
1922
+ return {
1923
+ ...turn,
1924
+ toolCalls: turn.toolCalls.map(relink),
1925
+ contentBlocks: turn.contentBlocks.map((block) => block.kind === "tool_calls" ? { ...block, toolCalls: block.toolCalls.map(relink) } : block)
1926
+ };
1927
+ });
1928
+ }
1929
+
1874
1930
  // src/lib/codex-patches.ts
1875
1931
  function isApplyPatchText(value) {
1876
1932
  return value.includes("*** Begin Patch") && value.includes("*** End Patch");
@@ -2652,6 +2708,7 @@ function walkCodexRecords(records, options) {
2652
2708
  }
2653
2709
  let current = null;
2654
2710
  let currentTurnId = null;
2711
+ let queuedPromptEcho = false;
2655
2712
  let currentModel = metadata.model || null;
2656
2713
  let lastTurnTimestamp = "";
2657
2714
  let pendingCompaction = null;
@@ -2706,6 +2763,7 @@ function walkCodexRecords(records, options) {
2706
2763
  recordIndex = index;
2707
2764
  const payload = isObject3(record2.payload) ? record2.payload : void 0;
2708
2765
  const timestamp = record2.timestamp ?? "";
2766
+ if (record2.type !== "event_msg") queuedPromptEcho = false;
2709
2767
  if (record2.type === "compacted") {
2710
2768
  current = commitTurn(current);
2711
2769
  pendingCompaction = "Conversation compacted";
@@ -2723,11 +2781,17 @@ function walkCodexRecords(records, options) {
2723
2781
  continue;
2724
2782
  }
2725
2783
  if (record2.type === "event_msg" && payload?.type === "user_message" && typeof payload.message === "string") {
2784
+ if (queuedPromptEcho) {
2785
+ queuedPromptEcho = false;
2786
+ continue;
2787
+ }
2726
2788
  if (current && (current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0)) {
2727
2789
  current = commitTurn(current);
2728
2790
  }
2791
+ const opensWindow = !sawTurnStart;
2729
2792
  sawTurnStart = true;
2730
2793
  current ??= createActiveTurn(currentTurnId, timestamp, currentModel);
2794
+ if (opensWindow) current.opensOnPrompt = true;
2731
2795
  const localImages = (Array.isArray(payload.local_images) ? payload.local_images : []).filter(
2732
2796
  (p) => typeof p === "string" && p.length > 0
2733
2797
  );
@@ -2842,8 +2906,15 @@ function walkCodexRecords(records, options) {
2842
2906
  appendAssistantText(current, extractMessageText(payload, "output_text"), timestamp);
2843
2907
  continue;
2844
2908
  }
2845
- if (payload.type === "message" && payload.role === "user" && current.userMessage === null) {
2909
+ if (payload.type === "message" && payload.role === "user") {
2846
2910
  const text = normalizePromptText(extractMessageText(payload, "input_text"));
2911
+ if (current.userMessage !== null || current.contentBlocks.length > 0) {
2912
+ if (text) {
2913
+ current.contentBlocks.push({ kind: "queued_prompt", content: text, timestamp });
2914
+ queuedPromptEcho = true;
2915
+ }
2916
+ continue;
2917
+ }
2847
2918
  const images = extractResponseMessageImages(payload);
2848
2919
  const audio = extractResponseMessageAudio(payload);
2849
2920
  if (text || images.length > 0 || audio.length > 0) {
@@ -3085,6 +3156,7 @@ function walkCodexRecords(records, options) {
3085
3156
  }
3086
3157
  }
3087
3158
  commitTurn(current);
3159
+ const linkedTurns = linkAsyncAnswers(turns);
3088
3160
  return {
3089
3161
  turnStartIndices,
3090
3162
  sessionId: metadata.sessionId,
@@ -3094,8 +3166,8 @@ function walkCodexRecords(records, options) {
3094
3166
  slug: metadata.slug,
3095
3167
  name: "",
3096
3168
  model: metadata.model,
3097
- turns,
3098
- stats: options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns),
3169
+ turns: linkedTurns,
3170
+ stats: options?.skipStats ? createEmptySessionStats(linkedTurns.length) : computeStats(linkedTurns),
3099
3171
  rawMessages: options?.skipStats ? [] : records,
3100
3172
  branchedFrom: metadata.branchedFrom,
3101
3173
  agentKind: "codex"
package/dist/index.js CHANGED
@@ -1880,6 +1880,62 @@ function appendAssistantText(turn, text, timestamp) {
1880
1880
  turn.contentBlocks.push({ kind: "text", text: [text], timestamp });
1881
1881
  }
1882
1882
 
1883
+ // src/lib/automatedPrompts.ts
1884
+ var CREW_CHAT_DELIVERY_RE = /^Crew chat · ([^\n]+) · message (\d+)(?:\n|$)/;
1885
+ var TASK_WAKEUP_START_RE = /^Delegated task \S+ (?:running|completed|error|cancelled|needs attention)\. Child session: /;
1886
+ function isReaderPrompt(text) {
1887
+ const trimmed = text.trim();
1888
+ if (!trimmed) return false;
1889
+ if (CREW_CHAT_DELIVERY_RE.test(trimmed) || TASK_WAKEUP_START_RE.test(trimmed)) return false;
1890
+ return parseAgentEnvelope(trimmed).sender === null;
1891
+ }
1892
+
1893
+ // src/lib/asyncAnswers.ts
1894
+ function promptText(content) {
1895
+ if (content === null) return "";
1896
+ if (typeof content === "string") return content;
1897
+ return content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
1898
+ }
1899
+ function linkAsyncAnswers(turns) {
1900
+ const answers = /* @__PURE__ */ new Map();
1901
+ let open4 = [];
1902
+ const answer = (text) => {
1903
+ if (open4.length === 0 || !isReaderPrompt(text)) return;
1904
+ for (const id of open4) answers.set(id, text.trim());
1905
+ open4 = [];
1906
+ };
1907
+ for (const turn of turns) {
1908
+ answer(promptText(turn.userMessage));
1909
+ for (const block of turn.contentBlocks) {
1910
+ if (block.kind === "queued_prompt") answer(block.content);
1911
+ else if (block.kind === "tool_calls") {
1912
+ for (const call of block.toolCalls) if (call.asyncQuestion) open4.push(call.id);
1913
+ }
1914
+ }
1915
+ }
1916
+ const stale = (call) => call.asyncQuestion === true && call.asyncAnswer !== answers.get(call.id);
1917
+ const relinked = /* @__PURE__ */ new Map();
1918
+ const relink = (call) => {
1919
+ if (!stale(call)) return call;
1920
+ let next = relinked.get(call);
1921
+ if (!next) {
1922
+ const { asyncAnswer: _previous, ...rest } = call;
1923
+ const reply = answers.get(call.id);
1924
+ next = reply === void 0 ? rest : { ...rest, asyncAnswer: reply };
1925
+ relinked.set(call, next);
1926
+ }
1927
+ return next;
1928
+ };
1929
+ return turns.map((turn) => {
1930
+ if (!turn.toolCalls.some(stale)) return turn;
1931
+ return {
1932
+ ...turn,
1933
+ toolCalls: turn.toolCalls.map(relink),
1934
+ contentBlocks: turn.contentBlocks.map((block) => block.kind === "tool_calls" ? { ...block, toolCalls: block.toolCalls.map(relink) } : block)
1935
+ };
1936
+ });
1937
+ }
1938
+
1883
1939
  // src/lib/codex-patches.ts
1884
1940
  function isApplyPatchText(value) {
1885
1941
  return value.includes("*** Begin Patch") && value.includes("*** End Patch");
@@ -2661,6 +2717,7 @@ function walkCodexRecords(records, options) {
2661
2717
  }
2662
2718
  let current = null;
2663
2719
  let currentTurnId = null;
2720
+ let queuedPromptEcho = false;
2664
2721
  let currentModel = metadata.model || null;
2665
2722
  let lastTurnTimestamp = "";
2666
2723
  let pendingCompaction = null;
@@ -2715,6 +2772,7 @@ function walkCodexRecords(records, options) {
2715
2772
  recordIndex = index;
2716
2773
  const payload = isObject3(record2.payload) ? record2.payload : void 0;
2717
2774
  const timestamp = record2.timestamp ?? "";
2775
+ if (record2.type !== "event_msg") queuedPromptEcho = false;
2718
2776
  if (record2.type === "compacted") {
2719
2777
  current = commitTurn(current);
2720
2778
  pendingCompaction = "Conversation compacted";
@@ -2732,11 +2790,17 @@ function walkCodexRecords(records, options) {
2732
2790
  continue;
2733
2791
  }
2734
2792
  if (record2.type === "event_msg" && payload?.type === "user_message" && typeof payload.message === "string") {
2793
+ if (queuedPromptEcho) {
2794
+ queuedPromptEcho = false;
2795
+ continue;
2796
+ }
2735
2797
  if (current && (current.assistantText.length > 0 || current.toolCalls.length > 0 || current.thinking.length > 0)) {
2736
2798
  current = commitTurn(current);
2737
2799
  }
2800
+ const opensWindow = !sawTurnStart;
2738
2801
  sawTurnStart = true;
2739
2802
  current ??= createActiveTurn(currentTurnId, timestamp, currentModel);
2803
+ if (opensWindow) current.opensOnPrompt = true;
2740
2804
  const localImages = (Array.isArray(payload.local_images) ? payload.local_images : []).filter(
2741
2805
  (p) => typeof p === "string" && p.length > 0
2742
2806
  );
@@ -2851,8 +2915,15 @@ function walkCodexRecords(records, options) {
2851
2915
  appendAssistantText(current, extractMessageText(payload, "output_text"), timestamp);
2852
2916
  continue;
2853
2917
  }
2854
- if (payload.type === "message" && payload.role === "user" && current.userMessage === null) {
2918
+ if (payload.type === "message" && payload.role === "user") {
2855
2919
  const text = normalizePromptText(extractMessageText(payload, "input_text"));
2920
+ if (current.userMessage !== null || current.contentBlocks.length > 0) {
2921
+ if (text) {
2922
+ current.contentBlocks.push({ kind: "queued_prompt", content: text, timestamp });
2923
+ queuedPromptEcho = true;
2924
+ }
2925
+ continue;
2926
+ }
2856
2927
  const images = extractResponseMessageImages(payload);
2857
2928
  const audio = extractResponseMessageAudio(payload);
2858
2929
  if (text || images.length > 0 || audio.length > 0) {
@@ -3094,6 +3165,7 @@ function walkCodexRecords(records, options) {
3094
3165
  }
3095
3166
  }
3096
3167
  commitTurn(current);
3168
+ const linkedTurns = linkAsyncAnswers(turns);
3097
3169
  return {
3098
3170
  turnStartIndices,
3099
3171
  sessionId: metadata.sessionId,
@@ -3103,8 +3175,8 @@ function walkCodexRecords(records, options) {
3103
3175
  slug: metadata.slug,
3104
3176
  name: "",
3105
3177
  model: metadata.model,
3106
- turns,
3107
- stats: options?.skipStats ? createEmptySessionStats(turns.length) : computeStats(turns),
3178
+ turns: linkedTurns,
3179
+ stats: options?.skipStats ? createEmptySessionStats(linkedTurns.length) : computeStats(linkedTurns),
3108
3180
  rawMessages: options?.skipStats ? [] : records,
3109
3181
  branchedFrom: metadata.branchedFrom,
3110
3182
  agentKind: "codex"
@@ -0,0 +1,10 @@
1
+ import type { Turn } from "./types";
2
+ /**
3
+ * Records on each async question the reader's first prompt after it.
4
+ *
5
+ * Nothing links a reply to its question, so that prompt answers every question
6
+ * still open when it arrives. A pure recompute over the list handed in, so the
7
+ * page stitcher re-runs it across a join and gets what a single parse would
8
+ * have produced. Turns whose answers did not change come back by reference.
9
+ */
10
+ export declare function linkAsyncAnswers(turns: readonly Turn[]): Turn[];
@@ -0,0 +1,4 @@
1
+ export declare const CREW_CHAT_DELIVERY_RE: RegExp;
2
+ export declare const TASK_WAKEUP_START_RE: RegExp;
3
+ /** Whether a prompt came from the reader rather than a crew, a child task or another agent. */
4
+ export declare function isReaderPrompt(text: string): boolean;
@@ -411,6 +411,8 @@ export interface ToolCall {
411
411
  * answered here.
412
412
  */
413
413
  asyncQuestion?: boolean;
414
+ /** The reader's reply to an async question, once the transcript holds one. */
415
+ asyncAnswer?: string;
414
416
  /** Set by parser when a PostToolUse hook replaced this tool's output */
415
417
  outputReplacedByHook?: boolean;
416
418
  /** Total duration of PostToolUse hooks attached to this call, summed in ms */
@@ -656,6 +658,13 @@ export interface Turn {
656
658
  * must be stitched onto the previous turn once the older page arrives.
657
659
  */
658
660
  isFragment?: boolean;
661
+ /**
662
+ * Set when the parse window opened on this turn's prompt record, with no turn
663
+ * context before it. An older CLI's echo of a prompt steered into the previous
664
+ * page's last turn looks exactly like this, so the page stitcher may fold it
665
+ * back into that steer.
666
+ */
667
+ opensOnPrompt?: boolean;
659
668
  }
660
669
  export interface SessionStats {
661
670
  totalInputTokens: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogpit-memory",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "CLI tool for Claude Code, Codex, and GitHub Copilot CLI session introspection",
5
5
  "license": "MIT",
6
6
  "repository": {