@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 +14 -0
- package/dist/heart/safe-workspace.js +93 -0
- package/dist/mind/context.js +30 -0
- package/dist/senses/cli.js +3 -16
- package/package.json +1 -1
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",
|
package/dist/mind/context.js
CHANGED
|
@@ -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;
|
package/dist/senses/cli.js
CHANGED
|
@@ -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: (
|
|
402
|
-
|
|
403
|
-
|
|
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
|