u-foo 3.0.25 → 3.0.27

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.
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.25",
3
+ "version": "3.0.27",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -106,27 +106,20 @@ function createUcodeController({
106
106
  */
107
107
  function createThinkingStatusPublisher(publish, options = {}) {
108
108
  const intervalMs = Number(options.intervalMs) > 0 ? Number(options.intervalMs) : 120;
109
- let tail = "";
110
109
  let timer = null;
111
110
  let lastFlush = 0;
112
111
 
113
- function collapse(text) {
114
- const raw = String(text || "").replace(/\s+/g, " ").trim();
115
- if (!raw) return "Thinking…";
116
- return raw.length > 72 ? `${raw.slice(-72)}` : raw;
117
- }
118
-
119
112
  function flush() {
120
113
  timer = null;
121
114
  lastFlush = Date.now();
122
115
  publish("status.set", {
123
- text: collapse(tail),
116
+ text: "Thinking…",
124
117
  busy: true,
125
118
  });
126
119
  }
127
120
 
128
121
  function onThinkingDelta(chunk) {
129
- tail += String(chunk || "");
122
+ if (!String(chunk || "")) return;
130
123
  const elapsed = Date.now() - lastFlush;
131
124
  if (elapsed >= intervalMs) {
132
125
  flush();
@@ -143,7 +136,6 @@ function createThinkingStatusPublisher(publish, options = {}) {
143
136
  clearTimeout(timer);
144
137
  timer = null;
145
138
  }
146
- tail = "";
147
139
  lastFlush = 0;
148
140
  }
149
141
 
package/src/code/agent.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { randomUUID } = require("crypto");
3
4
  const { runToolCall } = require("./dispatch");
4
5
  const { runNativeAgentTask } = require("./nativeRunner");
5
6
  const { runDecomposedTask } = require("./taskDecomposer");
@@ -13,13 +14,13 @@ const {
13
14
  const { buildSkillInjections } = require("./skills");
14
15
  const {
15
16
  assembleModelContext,
16
- syncMessagesToTranscript,
17
17
  applyContextSideEffects,
18
18
  ensureProjectSnapshot,
19
19
  recordToolCallInSession,
20
20
  commitAfterSegmentEnd,
21
- sanitizeModelMessages,
22
21
  } = require("./context/assembler");
22
+ const { appendTurnMessages } = require("./conversation/sessionJournal");
23
+ const { loadTranscript, transcriptEventsToMessages } = require("./context/transcript");
23
24
  const { buildLayeredSystemPrompt } = require("./context/promptLayers");
24
25
  const {
25
26
  createProjectPreflightContextV2,
@@ -539,6 +540,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
539
540
  const analysisTask = isProjectAnalysisTask(taskText);
540
541
  const workspaceRoot = String(state.workspaceRoot || process.cwd());
541
542
  ensureContextSessionState(state);
543
+ state.sessionId = resolveSessionId(state.sessionId);
542
544
 
543
545
  let projectSnapshot = state.projectSnapshot || null;
544
546
  if (analysisTask) {
@@ -591,6 +593,26 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
591
593
  });
592
594
  const systemContext = assembled.systemPrompt;
593
595
  state.summary = assembled.summary || state.summary;
596
+ const turnId = `turn_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
597
+ const inputAppend = appendTurnMessages(
598
+ workspaceRoot,
599
+ String(state.sessionId || ""),
600
+ turnId,
601
+ [{ role: "user", content: taskText }],
602
+ { scope: "input" },
603
+ );
604
+ if (!inputAppend.ok) {
605
+ return {
606
+ ok: false,
607
+ summary: "",
608
+ artifacts: [],
609
+ logs: logs.slice(),
610
+ error: `failed to persist conversation input: ${inputAppend.error}`,
611
+ metrics: {},
612
+ streamed: false,
613
+ streamLastChar: "",
614
+ };
615
+ }
594
616
 
595
617
  const onStream = onDelta
596
618
  ? (delta) => {
@@ -625,13 +647,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
625
647
  }
626
648
  return state.contextMeter;
627
649
  };
628
- let lastTranscriptBaseline = 0;
629
650
  const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
630
651
  toolEventsThisAttempt = 0;
631
652
  const historyMessages = assembled.messages;
632
- // Sanitized length matches what nativeRunner clones before appending this
633
- // turn's user/tool/assistant messages — used as the transcript sync baseline.
634
- lastTranscriptBaseline = sanitizeModelMessages(historyMessages).length;
635
653
  return runNativeAgentImpl({
636
654
  workspaceRoot,
637
655
  provider,
@@ -706,12 +724,13 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
706
724
  ok: true,
707
725
  output: decomposedResult.summary,
708
726
  sessionId: state.sessionId,
709
- messages: state.nlMessages,
727
+ turnItems: decomposedResult.turnItems,
710
728
  };
711
729
  } else {
712
730
  cliRes = {
713
731
  ok: false,
714
732
  error: decomposedResult.error,
733
+ turnItems: decomposedResult.turnItems,
715
734
  };
716
735
  }
717
736
  } else {
@@ -729,6 +748,34 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
729
748
  }
730
749
  }
731
750
 
751
+ const outputItems = cliRes && Array.isArray(cliRes.turnItems) ? cliRes.turnItems : [];
752
+ if (outputItems.length > 0) {
753
+ const outputAppend = appendTurnMessages(
754
+ workspaceRoot,
755
+ String(state.sessionId || ""),
756
+ turnId,
757
+ outputItems,
758
+ { scope: "output" },
759
+ );
760
+ if (!outputAppend.ok) {
761
+ return {
762
+ ok: false,
763
+ summary: "",
764
+ artifacts: [],
765
+ logs: logs.slice(),
766
+ error: `failed to persist conversation output: ${outputAppend.error}`,
767
+ metrics: {},
768
+ streamed: Boolean(streamed || (cliRes && cliRes.streamed)),
769
+ streamLastChar,
770
+ };
771
+ }
772
+ }
773
+ const transcript = loadTranscript(workspaceRoot, String(state.sessionId || ""));
774
+ state.transcriptEvents = transcript.events;
775
+ state.nlMessages = stripSkillBlocksFromMessages(
776
+ transcriptEventsToMessages(transcript.events, { preferArtifact: true }),
777
+ );
778
+
732
779
  if (!cliRes || cliRes.ok === false) {
733
780
  const errMsg = String((cliRes && cliRes.error) || "");
734
781
  if (isCliCancelledError(errMsg) && state.executionState) {
@@ -746,9 +793,8 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
746
793
  streamLastChar,
747
794
  };
748
795
  }
749
- if (cliRes && typeof cliRes.sessionId === "string" && cliRes.sessionId.trim()) {
750
- state.sessionId = cliRes.sessionId.trim();
751
- }
796
+ // The turn coordinator owns session identity. Providers/runners receive it
797
+ // as correlation metadata but cannot redirect durable writes mid-turn.
752
798
  if (cliRes && cliRes.executionState && typeof cliRes.executionState === "object") {
753
799
  // Preserve planMode if the runner returned a fresh empty state without it.
754
800
  const priorPlanMode = Boolean(state.executionState && state.executionState.planMode);
@@ -763,18 +809,6 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
763
809
  state.executionState.planModeSource = priorSource;
764
810
  }
765
811
  }
766
- if (cliRes && Array.isArray(cliRes.messages)) {
767
- // Sync first so ensureTranscript does not migrate the just-assigned
768
- // nlMessages and then append the same delta again.
769
- syncMessagesToTranscript(state, cliRes.messages, workspaceRoot, {
770
- baselineCount: lastTranscriptBaseline,
771
- });
772
- state.nlMessages = stripSkillBlocksFromMessages(
773
- Array.isArray(state.nlMessages) && state.nlMessages.length > 0
774
- ? state.nlMessages
775
- : cliRes.messages,
776
- );
777
- }
778
812
  const normalized = String(cliRes.output || "").trim();
779
813
  const sideEffects = parseStructuredSideEffects(normalized);
780
814
  if (sideEffects) {
@@ -1028,6 +1062,9 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
1028
1062
  state.contextMeter = snapshot.contextMeter && typeof snapshot.contextMeter === "object"
1029
1063
  ? snapshot.contextMeter
1030
1064
  : null;
1065
+ // A resume is a pure projection rebuild. Never reuse events belonging to
1066
+ // the session that happened to be active before /resume.
1067
+ state.transcriptEvents = [];
1031
1068
  ensureContextSessionState(state);
1032
1069
  const { ensureTranscript } = require("./context/assembler");
1033
1070
  ensureTranscript(state, state.workspaceRoot);
@@ -1051,6 +1088,7 @@ function resumeSessionState(state = {}, sessionId = "", workspaceRoot = process.
1051
1088
  */
1052
1089
  async function resumeAfterUserInteraction(answerText = "", state = {}, options = {}) {
1053
1090
  ensureContextSessionState(state);
1091
+ state.sessionId = resolveSessionId(state.sessionId);
1054
1092
  const { resolveUserInteraction } = require("./context/userInteraction");
1055
1093
  const { appendAnswerToolResult } = require("./nativeRunner");
1056
1094
  const resolved = resolveUserInteraction(state.executionState, answerText);
@@ -1071,6 +1109,7 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1071
1109
  };
1072
1110
 
1073
1111
  let messages = Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [];
1112
+ const inputStart = messages.length;
1074
1113
  if (resolved.continueMode === "tool_result" && resolved.resume && resolved.resume.call) {
1075
1114
  const appended = appendAnswerToolResult(messages, resolved.resume, resolved.answer);
1076
1115
  if (!appended.ok) {
@@ -1083,9 +1122,28 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1083
1122
  content: JSON.stringify(resolved.answer),
1084
1123
  });
1085
1124
  }
1086
- state.nlMessages = messages;
1087
-
1088
1125
  const workspaceRoot = state.workspaceRoot || process.cwd();
1126
+ const turnId = `turn_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
1127
+ const interactionItems = messages.slice(inputStart);
1128
+ const interactionAppend = appendTurnMessages(
1129
+ workspaceRoot,
1130
+ String(state.sessionId || ""),
1131
+ turnId,
1132
+ interactionItems,
1133
+ { scope: "interaction" },
1134
+ );
1135
+ if (!interactionAppend.ok) {
1136
+ return {
1137
+ ok: false,
1138
+ error: `failed to persist interaction answer: ${interactionAppend.error}`,
1139
+ };
1140
+ }
1141
+ let transcript = loadTranscript(workspaceRoot, String(state.sessionId || ""));
1142
+ state.transcriptEvents = transcript.events;
1143
+ state.nlMessages = stripSkillBlocksFromMessages(
1144
+ transcriptEventsToMessages(transcript.events, { preferArtifact: true }),
1145
+ );
1146
+
1089
1147
  const assembled = assembleModelContext(state, {
1090
1148
  workspaceRoot,
1091
1149
  provider: state.provider,
@@ -1131,9 +1189,30 @@ async function resumeAfterUserInteraction(answerText = "", state = {}, options =
1131
1189
  if (cliRes && cliRes.executionState) {
1132
1190
  state.executionState = cliRes.executionState;
1133
1191
  }
1134
- if (cliRes && Array.isArray(cliRes.messages)) {
1135
- state.nlMessages = stripSkillBlocksFromMessages(cliRes.messages);
1192
+ if (cliRes && Array.isArray(cliRes.turnItems) && cliRes.turnItems.length > 0) {
1193
+ const outputAppend = appendTurnMessages(
1194
+ workspaceRoot,
1195
+ String(state.sessionId || ""),
1196
+ turnId,
1197
+ cliRes.turnItems,
1198
+ { scope: "output" },
1199
+ );
1200
+ if (!outputAppend.ok) {
1201
+ return {
1202
+ ok: false,
1203
+ error: `failed to persist interaction output: ${outputAppend.error}`,
1204
+ logs,
1205
+ waitingUserInteraction: false,
1206
+ streamed: Boolean(cliRes.streamed),
1207
+ streamLastChar,
1208
+ };
1209
+ }
1136
1210
  }
1211
+ transcript = loadTranscript(workspaceRoot, String(state.sessionId || ""));
1212
+ state.transcriptEvents = transcript.events;
1213
+ state.nlMessages = stripSkillBlocksFromMessages(
1214
+ transcriptEventsToMessages(transcript.events, { preferArtifact: true }),
1215
+ );
1137
1216
  if (cliRes && cliRes.contextMeter) {
1138
1217
  state.contextMeter = cliRes.contextMeter;
1139
1218
  }
@@ -5,7 +5,6 @@ const {
5
5
  transcriptEventsToMessages,
6
6
  migrateNlMessagesToTranscript,
7
7
  } = require("./transcript");
8
- const { appendTranscriptMessagesForStorage } = require("./transcriptSync");
9
8
  const { reduceToolResult } = require("./reducers");
10
9
  const { saveArtifact } = require("./artifacts");
11
10
  const { buildLayeredSystemPrompt } = require("./promptLayers");
@@ -551,104 +550,6 @@ function recordToolCallInSession(session = {}, persisted = {}, workspaceRoot = p
551
550
  }
552
551
  }
553
552
 
554
- function syncMessagesToTranscript(session = {}, messages = [], workspaceRoot = process.cwd(), options = {}) {
555
- const sessionId = String(session.sessionId || "").trim();
556
- if (!sessionId) return [];
557
- const prior = ensureTranscript(session, workspaceRoot);
558
- const full = Array.isArray(messages) ? messages : [];
559
- if (full.length === 0) return prior;
560
-
561
- // `messages` is often a WINDOWED model view (plus this turn's delta), not the
562
- // full transcript. Comparing lengths to prior transcript events dropped every
563
- // new user turn once the transcript grew past the window — resume then lost
564
- // green › user rows. Prefer an explicit baseline, else suffix/prefix match.
565
- let baseline = Number.isFinite(options.baselineCount)
566
- ? Math.max(0, Math.floor(options.baselineCount))
567
- : null;
568
- if (baseline == null) {
569
- const existingMessages = transcriptEventsToMessages(prior, {
570
- preferArtifact: true,
571
- });
572
- baseline = matchTranscriptBaseline(existingMessages, full);
573
- }
574
- baseline = Math.max(0, Math.min(full.length, baseline));
575
-
576
- if (full.length <= baseline) {
577
- session.nlMessages = transcriptEventsToMessages(session.transcriptEvents, {
578
- preferArtifact: true,
579
- });
580
- return session.transcriptEvents || prior;
581
- }
582
-
583
- const delta = full.slice(baseline);
584
- const extra = {
585
- segmentId: session.executionState && session.executionState.currentSegmentId
586
- ? session.executionState.currentSegmentId
587
- : "",
588
- };
589
- appendTranscriptMessagesForStorage(workspaceRoot, sessionId, delta, extra);
590
- session.transcriptEvents = loadTranscript(workspaceRoot, sessionId).events;
591
- session.nlMessages = transcriptEventsToMessages(session.transcriptEvents, {
592
- preferArtifact: true,
593
- });
594
- session.summary = buildRollingSummary(session.transcriptEvents, session.summary, session);
595
- return session.transcriptEvents;
596
- }
597
-
598
- /**
599
- * Fingerprint a chat message for transcript alignment. Tool payloads are
600
- * compared by call id (content is often artifact-compressed on disk).
601
- */
602
- function messageSyncFingerprint(message = {}) {
603
- if (!message || typeof message !== "object") return "";
604
- const role = String(message.role || "").trim().toLowerCase();
605
- if (role === "tool") {
606
- return `tool|${String(message.tool_call_id || "").trim()}`;
607
- }
608
- if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
609
- const ids = message.tool_calls
610
- .map((call) => String((call && call.id) || "").trim())
611
- .filter(Boolean)
612
- .join(",");
613
- return `assistant_tools|${ids}`;
614
- }
615
- let content = "";
616
- if (typeof message.content === "string") content = message.content;
617
- else if (message.content != null) {
618
- try {
619
- content = JSON.stringify(message.content);
620
- } catch {
621
- content = String(message.content);
622
- }
623
- }
624
- const compact = content.replace(/\s+/g, " ").trim();
625
- return `${role}|${compact.length}|${compact.slice(0, 240)}`;
626
- }
627
-
628
- /**
629
- * How much of `next` is already covered by the end of `existing`.
630
- * Returns the length of the matched prefix of `next` (baseline for slicing).
631
- */
632
- function matchTranscriptBaseline(existing = [], next = []) {
633
- const prior = Array.isArray(existing) ? existing : [];
634
- const incoming = Array.isArray(next) ? next : [];
635
- if (incoming.length === 0) return 0;
636
- const priorFingerprints = prior.map(messageSyncFingerprint);
637
- const nextFingerprints = incoming.map(messageSyncFingerprint);
638
- const max = Math.min(priorFingerprints.length, nextFingerprints.length);
639
- for (let k = max; k >= 0; k -= 1) {
640
- let matched = true;
641
- for (let i = 0; i < k; i += 1) {
642
- if (priorFingerprints[priorFingerprints.length - k + i] !== nextFingerprints[i]) {
643
- matched = false;
644
- break;
645
- }
646
- }
647
- if (matched) return k;
648
- }
649
- return 0;
650
- }
651
-
652
553
  function applyContextSideEffects(session = {}, sideEffects = {}, workspaceRoot = process.cwd()) {
653
554
  const effects = sideEffects && typeof sideEffects === "object" ? sideEffects : {};
654
555
  if (effects.stateCommit) {
@@ -710,10 +611,7 @@ module.exports = {
710
611
  assembleModelContext,
711
612
  persistToolResultToContext,
712
613
  recordToolCallInSession,
713
- syncMessagesToTranscript,
714
614
  applyContextSideEffects,
715
615
  commitAfterSegmentEnd,
716
616
  ensureProjectSnapshot,
717
- messageSyncFingerprint,
718
- matchTranscriptBaseline,
719
617
  };
@@ -74,6 +74,11 @@ function readTranscriptFile(filePath = "") {
74
74
  }
75
75
 
76
76
  function loadTranscript(workspaceRoot = process.cwd(), sessionId = "") {
77
+ const { loadTranscriptProjection } = require("../conversation/sessionJournal");
78
+ const projected = loadTranscriptProjection(workspaceRoot, sessionId);
79
+ if (projected.source === "journal-v3" || projected.events.length > 0) {
80
+ return projected;
81
+ }
77
82
  const filePath = getTranscriptFilePath(workspaceRoot, sessionId);
78
83
  return {
79
84
  filePath,