@ouro.bot/cli 0.1.0-alpha.86 → 0.1.0-alpha.88

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/changelog.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.88",
6
+ "changes": [
7
+ "CLI session repair now strips orphaned tool-result messages when a saved chat history lost the matching assistant tool call, so a broken prior turn no longer bricks every future `ouro chat` message with a tool-call replay error.",
8
+ "Recovered sessions keep valid tool call/result pairs intact while dropping only the stale outputs, which lets live operator chats resume from repaired history instead of forcing a brand-new thread."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.87",
13
+ "changes": [
14
+ "CLI chat now keeps model reasoning private again. The visible surface stays on spinners, tool updates, and actual replies instead of dim internal-thinking text leaking into the operator conversation.",
15
+ "Safe workspace selection now persists across daemon restarts and `ouro up`, so repo-local reads, edits, and shell commands keep targeting the same acquired scratch clone or worktree after a runtime update."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.86",
6
20
  "changes": [
@@ -45,6 +45,75 @@ const identity_1 = require("./identity");
45
45
  const runtime_1 = require("../nerves/runtime");
46
46
  let activeSelection = null;
47
47
  let cleanupHookRegistered = false;
48
+ function workspaceSelectionStateFile(workspaceBase) {
49
+ return path.join(workspaceBase, ".active-safe-workspace.json");
50
+ }
51
+ function getOptionalFsFn(name) {
52
+ try {
53
+ return fs[name];
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ function shouldPersistSelection(options) {
60
+ return options.persistSelection ?? options.workspaceRoot === undefined;
61
+ }
62
+ function isPersistedSelectionShape(value) {
63
+ if (!value || typeof value !== "object")
64
+ return false;
65
+ const candidate = value;
66
+ return (typeof candidate.runtimeKind === "string"
67
+ && typeof candidate.repoRoot === "string"
68
+ && typeof candidate.workspaceRoot === "string"
69
+ && typeof candidate.workspaceBranch === "string"
70
+ && (candidate.sourceBranch === null || typeof candidate.sourceBranch === "string")
71
+ && typeof candidate.sourceCloneUrl === "string"
72
+ && typeof candidate.cleanupAfterMerge === "boolean"
73
+ && typeof candidate.created === "boolean"
74
+ && typeof candidate.note === "string");
75
+ }
76
+ function loadPersistedSelection(workspaceBase, options) {
77
+ const existsSync = options.existsSync ?? fs.existsSync;
78
+ const readFileSync = options.readFileSync ?? getOptionalFsFn("readFileSync");
79
+ const unlinkSync = options.unlinkSync ?? getOptionalFsFn("unlinkSync");
80
+ const stateFile = workspaceSelectionStateFile(workspaceBase);
81
+ if (!existsSync(stateFile))
82
+ return null;
83
+ if (!readFileSync)
84
+ return null;
85
+ try {
86
+ const raw = readFileSync(stateFile, "utf-8");
87
+ const parsed = JSON.parse(raw);
88
+ if (!isPersistedSelectionShape(parsed) || !existsSync(parsed.workspaceRoot)) {
89
+ try {
90
+ unlinkSync?.(stateFile);
91
+ }
92
+ catch {
93
+ // best effort
94
+ }
95
+ return null;
96
+ }
97
+ return parsed;
98
+ }
99
+ catch {
100
+ try {
101
+ unlinkSync?.(stateFile);
102
+ }
103
+ catch {
104
+ // best effort
105
+ }
106
+ return null;
107
+ }
108
+ }
109
+ function persistSelectionState(workspaceBase, selection, options) {
110
+ const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
111
+ const writeFileSync = options.writeFileSync ?? getOptionalFsFn("writeFileSync");
112
+ if (!writeFileSync)
113
+ return;
114
+ mkdirSync(workspaceBase, { recursive: true });
115
+ writeFileSync(workspaceSelectionStateFile(workspaceBase), JSON.stringify(selection, null, 2), "utf-8");
116
+ }
48
117
  function defaultNow() {
49
118
  return Date.now();
50
119
  }
@@ -146,6 +215,7 @@ function ensureSafeRepoWorkspace(options = {}) {
146
215
  const agentName = resolveAgentName(options.agentName);
147
216
  const canonicalRepoUrl = options.canonicalRepoUrl ?? identity_1.HARNESS_CANONICAL_REPO_URL;
148
217
  const workspaceBase = options.workspaceRoot ?? (0, identity_1.getAgentRepoWorkspacesRoot)(agentName);
218
+ const persistSelection = shouldPersistSelection(options);
149
219
  const spawnSync = options.spawnSync ?? child_process_1.spawnSync;
150
220
  const existsSync = options.existsSync ?? fs.existsSync;
151
221
  const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
@@ -153,6 +223,26 @@ function ensureSafeRepoWorkspace(options = {}) {
153
223
  const now = options.now ?? defaultNow;
154
224
  const stamp = String(now());
155
225
  registerCleanupHook({ rmSync });
226
+ if (persistSelection) {
227
+ const restored = loadPersistedSelection(workspaceBase, options);
228
+ if (restored) {
229
+ activeSelection = restored;
230
+ (0, runtime_1.emitNervesEvent)({
231
+ component: "workspace",
232
+ event: "workspace.safe_repo_restored",
233
+ message: "restored safe repo workspace after runtime restart",
234
+ meta: {
235
+ runtimeKind: restored.runtimeKind,
236
+ repoRoot: restored.repoRoot,
237
+ workspaceRoot: restored.workspaceRoot,
238
+ workspaceBranch: restored.workspaceBranch,
239
+ sourceBranch: restored.sourceBranch,
240
+ cleanupAfterMerge: restored.cleanupAfterMerge,
241
+ },
242
+ });
243
+ return restored;
244
+ }
245
+ }
156
246
  let selection;
157
247
  if (isGitClone(repoRoot, spawnSync)) {
158
248
  const branch = readCurrentBranch(repoRoot, spawnSync);
@@ -205,6 +295,9 @@ function ensureSafeRepoWorkspace(options = {}) {
205
295
  };
206
296
  }
207
297
  activeSelection = selection;
298
+ if (persistSelection) {
299
+ persistSelectionState(workspaceBase, selection, options);
300
+ }
208
301
  (0, runtime_1.emitNervesEvent)({
209
302
  component: "workspace",
210
303
  event: "workspace.safe_repo_acquired",
@@ -227,6 +227,34 @@ function repairSessionMessages(messages) {
227
227
  });
228
228
  return result;
229
229
  }
230
+ function stripOrphanedToolResults(messages) {
231
+ const validCallIds = new Set();
232
+ for (const msg of messages) {
233
+ if (msg.role !== "assistant" || !Array.isArray(msg.tool_calls))
234
+ continue;
235
+ for (const toolCall of msg.tool_calls)
236
+ validCallIds.add(toolCall.id);
237
+ }
238
+ let removed = 0;
239
+ const repaired = messages.filter((msg) => {
240
+ if (msg.role !== "tool")
241
+ return true;
242
+ const keep = validCallIds.has(msg.tool_call_id);
243
+ if (!keep)
244
+ removed++;
245
+ return keep;
246
+ });
247
+ if (removed > 0) {
248
+ (0, runtime_1.emitNervesEvent)({
249
+ level: "warn",
250
+ event: "mind.session_orphan_tool_result_repair",
251
+ component: "mind",
252
+ message: "removed orphaned tool results from session history",
253
+ meta: { removed },
254
+ });
255
+ }
256
+ return repaired;
257
+ }
230
258
  function saveSession(filePath, messages, lastUsage, state) {
231
259
  const violations = validateSessionMessages(messages);
232
260
  if (violations.length > 0) {
@@ -239,6 +267,7 @@ function saveSession(filePath, messages, lastUsage, state) {
239
267
  });
240
268
  messages = repairSessionMessages(messages);
241
269
  }
270
+ messages = stripOrphanedToolResults(messages);
242
271
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
243
272
  const envelope = { version: 1, messages };
244
273
  if (lastUsage)
@@ -269,6 +298,7 @@ function loadSession(filePath) {
269
298
  });
270
299
  messages = repairSessionMessages(messages);
271
300
  }
301
+ messages = stripOrphanedToolResults(messages);
272
302
  const rawState = data?.state && typeof data.state === "object" && data.state !== null
273
303
  ? data.state
274
304
  : undefined;
@@ -346,7 +346,6 @@ function createCliCallbacks() {
346
346
  });
347
347
  let currentSpinner = null;
348
348
  function setSpinner(s) { currentSpinner = s; setActiveSpinner(s); }
349
- let hadReasoning = false;
350
349
  let hadToolRun = false;
351
350
  let textDirty = false; // true when text/reasoning was written without a trailing newline
352
351
  const streamer = new MarkdownStreamer();
@@ -355,7 +354,6 @@ function createCliCallbacks() {
355
354
  onModelStart: () => {
356
355
  currentSpinner?.stop();
357
356
  setSpinner(null);
358
- hadReasoning = false;
359
357
  textDirty = false;
360
358
  streamer.reset();
361
359
  wrapper.reset();
@@ -382,12 +380,6 @@ function createCliCallbacks() {
382
380
  currentSpinner.stop();
383
381
  setSpinner(null);
384
382
  }
385
- if (hadReasoning) {
386
- // Single newline to separate reasoning from reply — reasoning
387
- // output often ends with its own trailing newline(s)
388
- process.stdout.write("\n");
389
- hadReasoning = false;
390
- }
391
383
  const rendered = streamer.push(text);
392
384
  /* v8 ignore start -- wrapper integration: tested via cli.test.ts onTextChunk tests @preserve */
393
385
  if (rendered) {
@@ -398,14 +390,9 @@ function createCliCallbacks() {
398
390
  /* v8 ignore stop */
399
391
  textDirty = text.length > 0 && !text.endsWith("\n");
400
392
  },
401
- onReasoningChunk: (text) => {
402
- if (currentSpinner) {
403
- currentSpinner.stop();
404
- setSpinner(null);
405
- }
406
- hadReasoning = true;
407
- process.stdout.write(`\x1b[2m${text}\x1b[0m`);
408
- textDirty = text.length > 0 && !text.endsWith("\n");
393
+ onReasoningChunk: (_text) => {
394
+ // Keep reasoning private in the CLI surface. The spinner continues to
395
+ // represent active thinking until actual tool or answer output arrives.
409
396
  },
410
397
  onToolStart: (_name, _args) => {
411
398
  // Stop the model-start spinner: when the model returns only tool calls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.86",
3
+ "version": "0.1.0-alpha.88",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",