@ouro.bot/cli 0.1.0-alpha.314 → 0.1.0-alpha.316
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 +12 -0
- package/dist/heart/daemon/cli-exec.js +37 -16
- package/dist/heart/daemon/startup-tui.js +68 -30
- package/dist/heart/daemon/up-progress.js +126 -0
- package/dist/heart/outlook/outlook-read.js +55 -78
- package/dist/heart/outlook/outlook-types.js +20 -0
- package/dist/heart/session-activity.js +33 -15
- package/dist/heart/session-events.js +673 -0
- package/dist/heart/session-recall.js +23 -77
- package/dist/heart/start-of-turn-packet.js +2 -0
- package/dist/mind/context.js +67 -182
- package/dist/mind/prompt.js +14 -2
- package/dist/nerves/coverage/file-completeness.js +4 -0
- package/dist/senses/bluebubbles/index.js +1 -0
- package/dist/senses/cli/ouro-tui.js +10 -0
- package/dist/senses/cli.js +4 -0
- package/dist/senses/pipeline.js +4 -0
- package/dist/senses/shared-turn.js +1 -0
- package/dist/senses/teams.js +1 -0
- package/package.json +10 -1
- package/dist/outlook-ui/assets/index-Ck8agNeO.js +0 -61
- package/dist/outlook-ui/assets/index-LwChZTgL.css +0 -1
- package/dist/outlook-ui/index.html +0 -15
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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.316",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat(daemon): unified progress TUI for ouro up lifecycle. New UpProgress accumulated-checklist renderer replaces disconnected writeStdout calls with a cohesive progress display showing completed phases with checkmarks and active phase with spinner. In-place ANSI overwrite in TTY mode, static lines in non-TTY (CI/pipes). Wired into daemon.up handler for all phases: update check, system setup, agent updates, bundle pruning, daemon start. Emits nerves events for each phase completion."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.315",
|
|
12
|
+
"changes": [
|
|
13
|
+
"fix(tui): fix startup TUI ANSI overwrite, daemon death detection, and log visibility. Resolves visual corruption from overlapping ANSI escape sequences, improves process death detection reliability, and ensures daemon log output is consistently visible during startup."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.314",
|
|
6
18
|
"changes": [
|
|
@@ -75,6 +75,7 @@ const interactive_repair_1 = require("./interactive-repair");
|
|
|
75
75
|
const agentic_repair_1 = require("./agentic-repair");
|
|
76
76
|
const startup_tui_1 = require("./startup-tui");
|
|
77
77
|
const stale_bundle_prune_1 = require("./stale-bundle-prune");
|
|
78
|
+
const up_progress_1 = require("./up-progress");
|
|
78
79
|
// ── ensureDaemonRunning ──
|
|
79
80
|
async function ensureDaemonRunning(deps) {
|
|
80
81
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
@@ -118,24 +119,38 @@ async function ensureDaemonRunning(deps) {
|
|
|
118
119
|
const stability = await (0, startup_tui_1.pollDaemonStartup)({
|
|
119
120
|
sendCommand: deps.sendCommand,
|
|
120
121
|
socketPath: deps.socketPath,
|
|
121
|
-
|
|
122
|
+
daemonPid: started.pid ?? null,
|
|
123
|
+
/* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
|
|
124
|
+
writeRaw: (text) => process.stdout.write(text),
|
|
122
125
|
/* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
|
|
123
126
|
now: () => Date.now(),
|
|
124
127
|
/* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
|
|
125
128
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
126
|
-
/* v8 ignore start -- daemon log tail: reads real filesystem, tested via deployment @preserve */
|
|
129
|
+
/* v8 ignore start -- daemon log tail + pid check: reads real filesystem, tested via deployment @preserve */
|
|
127
130
|
readLatestDaemonEvent: () => {
|
|
128
131
|
try {
|
|
129
|
-
|
|
132
|
+
// The daemon writes structured events to daemon.ndjson in the first
|
|
133
|
+
// agent bundle's state/daemon/logs/ directory. Read the last line to
|
|
134
|
+
// surface what it's currently doing (e.g., "starting auto-start agents").
|
|
135
|
+
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
136
|
+
if (!fs.existsSync(bundlesRoot))
|
|
137
|
+
return null;
|
|
138
|
+
const agents = fs.readdirSync(bundlesRoot).filter((d) => d.endsWith(".ouro"));
|
|
130
139
|
for (const agent of agents) {
|
|
131
|
-
const logPath = path.join(
|
|
140
|
+
const logPath = path.join(bundlesRoot, agent, "state", "daemon", "logs", "daemon.ndjson");
|
|
132
141
|
if (!fs.existsSync(logPath))
|
|
133
142
|
continue;
|
|
134
|
-
const
|
|
143
|
+
const stat = fs.statSync(logPath);
|
|
144
|
+
if (stat.size === 0)
|
|
145
|
+
continue;
|
|
146
|
+
// Only read logs from the last 30 seconds (daemon just started)
|
|
147
|
+
const mtime = stat.mtimeMs;
|
|
148
|
+
if (Date.now() - mtime > 30_000)
|
|
149
|
+
continue;
|
|
150
|
+
const buf = Buffer.alloc(4096);
|
|
135
151
|
const fd = fs.openSync(logPath, "r");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
fs.readSync(fd, buf, 0, 2048, readFrom);
|
|
152
|
+
const readFrom = Math.max(0, stat.size - 4096);
|
|
153
|
+
fs.readSync(fd, buf, 0, 4096, readFrom);
|
|
139
154
|
fs.closeSync(fd);
|
|
140
155
|
const lines = buf.toString("utf-8").trim().split("\n").filter(Boolean);
|
|
141
156
|
const last = lines[lines.length - 1];
|
|
@@ -800,19 +815,19 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
800
815
|
}
|
|
801
816
|
}
|
|
802
817
|
const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
|
|
818
|
+
const progress = new up_progress_1.UpProgress({ write: deps.writeStdout, isTTY: false });
|
|
803
819
|
// ── versioned CLI update check ──
|
|
804
820
|
if (deps.checkForCliUpdate) {
|
|
805
|
-
|
|
821
|
+
progress.startPhase("update check");
|
|
806
822
|
let pendingReExec = false;
|
|
807
823
|
try {
|
|
808
824
|
const updateResult = await deps.checkForCliUpdate();
|
|
809
825
|
if (updateResult.available && updateResult.latestVersion) {
|
|
810
|
-
deps.writeStdout(`installing ${updateResult.latestVersion}...`);
|
|
811
826
|
/* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
|
|
812
827
|
const currentVersion = linkedVersionBeforeUp ?? "unknown";
|
|
813
828
|
await deps.installCliVersion(updateResult.latestVersion);
|
|
814
829
|
deps.activateCliVersion(updateResult.latestVersion);
|
|
815
|
-
|
|
830
|
+
progress.completePhase("update check", `installed ${updateResult.latestVersion}`);
|
|
816
831
|
const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
|
|
817
832
|
/* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
|
|
818
833
|
if (changelogCommand) {
|
|
@@ -833,13 +848,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
833
848
|
}
|
|
834
849
|
/* v8 ignore stop */
|
|
835
850
|
if (pendingReExec) {
|
|
851
|
+
progress.end();
|
|
836
852
|
deps.reExecFromNewVersion(args);
|
|
837
853
|
}
|
|
838
854
|
else {
|
|
839
|
-
|
|
855
|
+
progress.completePhase("update check", "up to date");
|
|
840
856
|
}
|
|
841
857
|
}
|
|
858
|
+
progress.startPhase("system setup");
|
|
842
859
|
await performSystemSetup(deps);
|
|
860
|
+
progress.completePhase("system setup");
|
|
843
861
|
// Track whether we've already printed the "ouro updated to" message
|
|
844
862
|
// this turn so the bundle-meta-fallback path below doesn't double-print.
|
|
845
863
|
// There are three independent paths that can detect "the binary just
|
|
@@ -905,15 +923,18 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
905
923
|
const to = updateSummary.updated[0].to;
|
|
906
924
|
const fromStr = from ? ` (was ${from})` : "";
|
|
907
925
|
const count = agents.length;
|
|
908
|
-
|
|
926
|
+
progress.startPhase("agent updates");
|
|
927
|
+
progress.completePhase("agent updates", `${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
|
|
909
928
|
}
|
|
910
929
|
// ── stale bundle pruning ──
|
|
911
930
|
const prunedBundles = (0, stale_bundle_prune_1.pruneStaleEphemeralBundles)({ bundlesRoot: deps.bundlesRoot });
|
|
912
|
-
|
|
913
|
-
|
|
931
|
+
if (prunedBundles.length > 0) {
|
|
932
|
+
progress.startPhase("bundle cleanup");
|
|
933
|
+
progress.completePhase("bundle cleanup", `pruned ${prunedBundles.length} stale bundle${prunedBundles.length === 1 ? "" : "s"}`);
|
|
914
934
|
}
|
|
915
|
-
|
|
935
|
+
progress.startPhase("starting daemon");
|
|
916
936
|
const daemonResult = await ensureDaemonRunning(deps);
|
|
937
|
+
progress.end();
|
|
917
938
|
deps.writeStdout(daemonResult.message);
|
|
918
939
|
// Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
|
|
919
940
|
if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.assessStability = assessStability;
|
|
15
15
|
exports.renderStartupProgress = renderStartupProgress;
|
|
16
|
+
exports.renderWaitingForDaemon = renderWaitingForDaemon;
|
|
16
17
|
exports.pollDaemonStartup = pollDaemonStartup;
|
|
17
18
|
const cli_render_1 = require("./cli-render");
|
|
18
19
|
const runtime_1 = require("../../nerves/runtime");
|
|
@@ -66,19 +67,13 @@ function assessStability(payload, now) {
|
|
|
66
67
|
/**
|
|
67
68
|
* Build an ANSI string for in-place terminal display during polling.
|
|
68
69
|
* Uses cursor-up and line-clear escapes to overwrite previous output.
|
|
69
|
-
*
|
|
70
|
-
* @param payload Current daemon status
|
|
71
|
-
* @param elapsed Milliseconds since polling started
|
|
72
|
-
* @param prevLineCount Number of lines written in the previous render (0 on first)
|
|
73
70
|
*/
|
|
74
71
|
function renderStartupProgress(payload, elapsed, prevLineCount = 0) {
|
|
75
72
|
const frameIndex = Math.floor(elapsed / 100) % SPINNER_FRAMES.length;
|
|
76
73
|
const spinner = SPINNER_FRAMES[frameIndex];
|
|
77
74
|
const lines = [];
|
|
78
|
-
// Header line
|
|
79
75
|
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
80
76
|
lines.push(`${spinner} ${BOLD}waiting for agents${RESET} ${DIM}(${elapsedSec}s)${RESET}`);
|
|
81
|
-
// Per-worker status lines
|
|
82
77
|
for (const worker of payload.workers) {
|
|
83
78
|
const statusColor = worker.status === "running" ? GREEN
|
|
84
79
|
: worker.status === "crashed" ? RED
|
|
@@ -86,7 +81,27 @@ function renderStartupProgress(payload, elapsed, prevLineCount = 0) {
|
|
|
86
81
|
const statusText = `${statusColor}${worker.status}${RESET}`;
|
|
87
82
|
lines.push(` ${worker.agent}/${worker.worker}: ${statusText}`);
|
|
88
83
|
}
|
|
89
|
-
|
|
84
|
+
let output = "";
|
|
85
|
+
if (prevLineCount > 0) {
|
|
86
|
+
output += `\x1b[${prevLineCount}A`;
|
|
87
|
+
}
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
output += `\x1b[2K${line}\n`;
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Render a pre-socket status line showing what the daemon is doing.
|
|
95
|
+
*/
|
|
96
|
+
function renderWaitingForDaemon(elapsed, latestEvent, prevLineCount = 0) {
|
|
97
|
+
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
98
|
+
const frameIndex = Math.floor(elapsed / 100) % SPINNER_FRAMES.length;
|
|
99
|
+
const spinner = SPINNER_FRAMES[frameIndex];
|
|
100
|
+
const lines = [];
|
|
101
|
+
lines.push(`${spinner} ${BOLD}waiting for daemon${RESET} ${DIM}(${elapsedSec}s)${RESET}`);
|
|
102
|
+
if (latestEvent) {
|
|
103
|
+
lines.push(` ${DIM}${latestEvent}${RESET}`);
|
|
104
|
+
}
|
|
90
105
|
let output = "";
|
|
91
106
|
if (prevLineCount > 0) {
|
|
92
107
|
output += `\x1b[${prevLineCount}A`;
|
|
@@ -119,15 +134,19 @@ function renderFinalSummary(result) {
|
|
|
119
134
|
/**
|
|
120
135
|
* Poll the daemon's status socket until all agents are stable or definitively
|
|
121
136
|
* failed, rendering real-time progress to the terminal.
|
|
137
|
+
*
|
|
138
|
+
* Detects daemon process death: if the spawned PID is no longer alive and the
|
|
139
|
+
* socket never came up, reports the failure immediately instead of spinning.
|
|
122
140
|
*/
|
|
123
141
|
async function pollDaemonStartup(deps) {
|
|
124
142
|
const startTime = deps.now();
|
|
125
143
|
let prevLineCount = 0;
|
|
144
|
+
const isAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
126
145
|
(0, runtime_1.emitNervesEvent)({
|
|
127
146
|
component: "daemon",
|
|
128
147
|
event: "daemon.startup_poll_start",
|
|
129
148
|
message: "beginning startup stability polling",
|
|
130
|
-
meta: { socketPath: deps.socketPath },
|
|
149
|
+
meta: { socketPath: deps.socketPath, daemonPid: deps.daemonPid },
|
|
131
150
|
});
|
|
132
151
|
while (true) {
|
|
133
152
|
const now = deps.now();
|
|
@@ -138,39 +157,47 @@ async function pollDaemonStartup(deps) {
|
|
|
138
157
|
payload = (0, cli_render_1.parseStatusPayload)(response.data);
|
|
139
158
|
}
|
|
140
159
|
catch {
|
|
141
|
-
// Socket not yet available —
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
// Socket not yet available — check if the daemon process is still alive
|
|
161
|
+
if (deps.daemonPid !== null && !isAlive(deps.daemonPid)) {
|
|
162
|
+
const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
|
|
163
|
+
const errorMsg = latestEvent ?? "daemon process died during startup";
|
|
164
|
+
(0, runtime_1.emitNervesEvent)({
|
|
165
|
+
level: "error",
|
|
166
|
+
component: "daemon",
|
|
167
|
+
event: "daemon.startup_process_died",
|
|
168
|
+
message: "daemon process died before socket came up",
|
|
169
|
+
meta: { pid: deps.daemonPid, lastEvent: latestEvent },
|
|
170
|
+
});
|
|
171
|
+
// Clear the waiting line
|
|
172
|
+
if (prevLineCount > 0) {
|
|
173
|
+
let clear = `\x1b[${prevLineCount}A`;
|
|
174
|
+
for (let i = 0; i < prevLineCount; i++)
|
|
175
|
+
clear += `\x1b[2K\n`;
|
|
176
|
+
deps.writeRaw(clear);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
stable: [],
|
|
180
|
+
degraded: [{ agent: "daemon", errorReason: errorMsg, fixHint: "check daemon logs or run `ouro doctor`" }],
|
|
181
|
+
};
|
|
157
182
|
}
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
// Show what the daemon is doing from its log
|
|
184
|
+
const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
|
|
185
|
+
const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount);
|
|
186
|
+
deps.writeRaw(output);
|
|
187
|
+
prevLineCount = latestEvent ? 2 : 1;
|
|
160
188
|
}
|
|
161
189
|
if (payload) {
|
|
162
190
|
const output = renderStartupProgress(payload, elapsed, prevLineCount);
|
|
163
|
-
deps.
|
|
164
|
-
prevLineCount = payload.workers.length + 1;
|
|
191
|
+
deps.writeRaw(output);
|
|
192
|
+
prevLineCount = payload.workers.length + 1;
|
|
165
193
|
const assessment = assessStability(payload, now);
|
|
166
194
|
if (assessment.resolved) {
|
|
167
195
|
const result = {
|
|
168
196
|
stable: assessment.stable,
|
|
169
197
|
degraded: assessment.degraded,
|
|
170
198
|
};
|
|
171
|
-
// Clear progress lines and render final summary
|
|
172
199
|
const summary = renderFinalSummary(result);
|
|
173
|
-
deps.
|
|
200
|
+
deps.writeRaw(summary);
|
|
174
201
|
(0, runtime_1.emitNervesEvent)({
|
|
175
202
|
component: "daemon",
|
|
176
203
|
event: "daemon.startup_poll_end",
|
|
@@ -187,3 +214,14 @@ async function pollDaemonStartup(deps) {
|
|
|
187
214
|
await deps.sleep(POLL_INTERVAL_MS);
|
|
188
215
|
}
|
|
189
216
|
}
|
|
217
|
+
/* v8 ignore start -- process liveness check: uses real process.kill(0), tested via deployment @preserve */
|
|
218
|
+
function defaultIsProcessAlive(pid) {
|
|
219
|
+
try {
|
|
220
|
+
process.kill(pid, 0);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/* v8 ignore stop */
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* UpProgress — accumulated-checklist progress renderer for `ouro up`.
|
|
4
|
+
*
|
|
5
|
+
* Displays completed phases with checkmarks, the current phase with a
|
|
6
|
+
* spinner and elapsed time, and pending phases as plain text. Uses ANSI
|
|
7
|
+
* cursor control for in-place overwriting in TTY mode, and falls back to
|
|
8
|
+
* static line-per-phase output in non-TTY mode.
|
|
9
|
+
*
|
|
10
|
+
* The caller drives animation by calling `render(now)` on a setInterval.
|
|
11
|
+
* This module owns no timers.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.UpProgress = void 0;
|
|
15
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
16
|
+
// ── ANSI constants (shared with startup-tui.ts pattern) ──
|
|
17
|
+
const SPINNER_FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
|
|
18
|
+
const RESET = "\x1b[0m";
|
|
19
|
+
const BOLD = "\x1b[1m";
|
|
20
|
+
const DIM = "\x1b[2m";
|
|
21
|
+
const GREEN = "\x1b[38;2;46;204;64m";
|
|
22
|
+
// ── UpProgress class ──
|
|
23
|
+
class UpProgress {
|
|
24
|
+
write;
|
|
25
|
+
isTTY;
|
|
26
|
+
completed = [];
|
|
27
|
+
currentPhase = null;
|
|
28
|
+
prevLineCount = 0;
|
|
29
|
+
ended = false;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
/* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
|
|
32
|
+
this.write = options?.write ?? ((text) => process.stdout.write(text));
|
|
33
|
+
/* v8 ignore next -- thin wrapper: real isTTY check injected for testability @preserve */
|
|
34
|
+
this.isTTY = options?.isTTY ?? (process.stdout.isTTY === true);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Begin a new phase with spinner. If a phase is already active, it is
|
|
38
|
+
* auto-completed (no detail text).
|
|
39
|
+
*/
|
|
40
|
+
startPhase(label) {
|
|
41
|
+
if (this.currentPhase) {
|
|
42
|
+
this.completePhase(this.currentPhase.label);
|
|
43
|
+
}
|
|
44
|
+
this.currentPhase = { label, startedAt: Date.now() };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Mark the current phase as done. In non-TTY mode, immediately writes
|
|
48
|
+
* a static line. Emits a nerves event for observability.
|
|
49
|
+
*/
|
|
50
|
+
completePhase(label, detail) {
|
|
51
|
+
if (!this.currentPhase) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const elapsedMs = Date.now() - this.currentPhase.startedAt;
|
|
55
|
+
this.completed.push({ label, detail });
|
|
56
|
+
this.currentPhase = null;
|
|
57
|
+
(0, runtime_1.emitNervesEvent)({
|
|
58
|
+
component: "daemon",
|
|
59
|
+
event: "daemon.up_phase_complete",
|
|
60
|
+
message: `phase complete: ${label}`,
|
|
61
|
+
meta: { phase: label, detail: detail ?? null, elapsedMs },
|
|
62
|
+
});
|
|
63
|
+
if (!this.isTTY) {
|
|
64
|
+
const detailStr = detail ? ` \u2014 ${detail}` : "";
|
|
65
|
+
this.write(` \u2713 ${label}${detailStr}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build an ANSI string for in-place terminal display. Returns empty
|
|
70
|
+
* string in non-TTY mode (output is written eagerly in completePhase).
|
|
71
|
+
*/
|
|
72
|
+
render(now) {
|
|
73
|
+
if (!this.isTTY) {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
// Completed phases
|
|
78
|
+
for (const phase of this.completed) {
|
|
79
|
+
const detailStr = phase.detail ? ` ${DIM}\u2014 ${phase.detail}${RESET}` : "";
|
|
80
|
+
lines.push(` ${GREEN}\u2713${RESET} ${phase.label}${detailStr}`);
|
|
81
|
+
}
|
|
82
|
+
// Current phase with spinner
|
|
83
|
+
if (this.currentPhase) {
|
|
84
|
+
const elapsed = now - this.currentPhase.startedAt;
|
|
85
|
+
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
86
|
+
const frameIndex = Math.floor(elapsed / 80) % SPINNER_FRAMES.length;
|
|
87
|
+
const spinner = SPINNER_FRAMES[frameIndex];
|
|
88
|
+
lines.push(` ${BOLD}${spinner}${RESET} ${this.currentPhase.label} ${DIM}(${elapsedSec}s)${RESET}`);
|
|
89
|
+
}
|
|
90
|
+
let output = "";
|
|
91
|
+
if (this.prevLineCount > 0) {
|
|
92
|
+
output += `\x1b[${this.prevLineCount}A`;
|
|
93
|
+
}
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
output += `\x1b[2K${line}\n`;
|
|
96
|
+
}
|
|
97
|
+
// Clear any leftover lines from previous render that are no longer needed
|
|
98
|
+
if (lines.length < this.prevLineCount) {
|
|
99
|
+
for (let i = 0; i < this.prevLineCount - lines.length; i++) {
|
|
100
|
+
output += `\x1b[2K\n`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.prevLineCount = lines.length;
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Finalize the progress display. Clears the current phase (if any) and
|
|
108
|
+
* writes the final checklist state. Idempotent.
|
|
109
|
+
*/
|
|
110
|
+
end() {
|
|
111
|
+
if (this.ended) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.ended = true;
|
|
115
|
+
if (this.currentPhase) {
|
|
116
|
+
this.currentPhase = null;
|
|
117
|
+
}
|
|
118
|
+
if (this.isTTY) {
|
|
119
|
+
const output = this.render(Date.now());
|
|
120
|
+
if (output) {
|
|
121
|
+
this.write(output);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.UpProgress = UpProgress;
|
|
@@ -68,6 +68,7 @@ const outlook_types_1 = require("./outlook-types");
|
|
|
68
68
|
const presence_1 = require("../../arc/presence");
|
|
69
69
|
const cares_1 = require("../../arc/cares");
|
|
70
70
|
const episodes_1 = require("../../arc/episodes");
|
|
71
|
+
const session_events_1 = require("../session-events");
|
|
71
72
|
const LIVE_TASK_STATUSES = ["processing", "validating", "collaborating", "blocked"];
|
|
72
73
|
const ACTIVE_CODING_STATUSES = new Set(["spawning", "running", "waiting_input", "stalled"]);
|
|
73
74
|
const BLOCKED_CODING_STATUSES = new Set(["waiting_input", "stalled"]);
|
|
@@ -378,14 +379,18 @@ function readOutlookMachineState(options = {}) {
|
|
|
378
379
|
agents: agentStates.map(summarizeAgent),
|
|
379
380
|
};
|
|
380
381
|
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Session inventory — enumerate all sessions with summary metadata
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
381
385
|
/* v8 ignore start — session envelope parsing utilities */
|
|
382
386
|
function parseSessionUsage(raw) {
|
|
383
|
-
if (!raw)
|
|
387
|
+
if (!raw || typeof raw !== "object")
|
|
384
388
|
return null;
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
+
const record = raw;
|
|
390
|
+
const inputTokens = typeof record.input_tokens === "number" ? record.input_tokens : 0;
|
|
391
|
+
const outputTokens = typeof record.output_tokens === "number" ? record.output_tokens : 0;
|
|
392
|
+
const reasoningTokens = typeof record.reasoning_tokens === "number" ? record.reasoning_tokens : 0;
|
|
393
|
+
const totalTokens = typeof record.total_tokens === "number" ? record.total_tokens : 0;
|
|
389
394
|
if (inputTokens === 0 && outputTokens === 0 && totalTokens === 0)
|
|
390
395
|
return null;
|
|
391
396
|
return { input_tokens: inputTokens, output_tokens: outputTokens, reasoning_tokens: reasoningTokens, total_tokens: totalTokens };
|
|
@@ -393,33 +398,29 @@ function parseSessionUsage(raw) {
|
|
|
393
398
|
function parseSessionContinuity(raw) {
|
|
394
399
|
if (!raw)
|
|
395
400
|
return null;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
401
|
+
if (typeof raw !== "object")
|
|
402
|
+
return null;
|
|
403
|
+
const record = raw;
|
|
404
|
+
const continuity = {
|
|
405
|
+
mustResolveBeforeHandoff: record.mustResolveBeforeHandoff === true,
|
|
406
|
+
lastFriendActivityAt: typeof record.lastFriendActivityAt === "string" ? record.lastFriendActivityAt : null,
|
|
399
407
|
};
|
|
408
|
+
if (!continuity.mustResolveBeforeHandoff && continuity.lastFriendActivityAt === null)
|
|
409
|
+
return null;
|
|
410
|
+
return continuity;
|
|
400
411
|
}
|
|
401
|
-
function extractContent(
|
|
402
|
-
if (
|
|
403
|
-
return
|
|
404
|
-
|
|
412
|
+
function extractContent(event) {
|
|
413
|
+
if (!event)
|
|
414
|
+
return null;
|
|
415
|
+
const text = (0, session_events_1.extractEventText)(event);
|
|
416
|
+
return text.length > 0 ? text : null;
|
|
405
417
|
}
|
|
406
|
-
function extractToolCallNames(
|
|
407
|
-
|
|
408
|
-
if (!Array.isArray(toolCalls))
|
|
418
|
+
function extractToolCallNames(event) {
|
|
419
|
+
if (!event)
|
|
409
420
|
return [];
|
|
410
|
-
return toolCalls
|
|
411
|
-
.map((call) =>
|
|
412
|
-
|
|
413
|
-
const fn = call.function;
|
|
414
|
-
if (fn && typeof fn === "object" && "name" in fn) {
|
|
415
|
-
return typeof fn.name === "string" ? fn.name : null;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
/* v8 ignore start */
|
|
419
|
-
return null;
|
|
420
|
-
/* v8 ignore stop */
|
|
421
|
-
})
|
|
422
|
-
.filter((name) => name !== null);
|
|
421
|
+
return event.toolCalls
|
|
422
|
+
.map((call) => call.function.name)
|
|
423
|
+
.filter((name) => typeof name === "string" && name.length > 0);
|
|
423
424
|
}
|
|
424
425
|
/* v8 ignore stop */
|
|
425
426
|
function estimateTokenCount(messages) {
|
|
@@ -428,21 +429,13 @@ function estimateTokenCount(messages) {
|
|
|
428
429
|
const content = extractContent(msg);
|
|
429
430
|
if (content)
|
|
430
431
|
charCount += content.length;
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
charCount += JSON.stringify(toolCalls).length;
|
|
434
|
-
}
|
|
432
|
+
if (msg.toolCalls.length > 0)
|
|
433
|
+
charCount += JSON.stringify(msg.toolCalls).length;
|
|
435
434
|
}
|
|
436
435
|
return Math.ceil(charCount / 4);
|
|
437
436
|
}
|
|
438
437
|
function readSessionEnvelope(sessionPath) {
|
|
439
|
-
|
|
440
|
-
const raw = fs.readFileSync(sessionPath, "utf-8");
|
|
441
|
-
return JSON.parse(raw);
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
438
|
+
return (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
|
|
446
439
|
}
|
|
447
440
|
/* v8 ignore start — filesystem traversal with defensive isDirectory checks */
|
|
448
441
|
function resolveAllSessionPaths(sessionsDir) {
|
|
@@ -516,18 +509,26 @@ function readSessionInventory(agentName, options = {}) {
|
|
|
516
509
|
if (friendId === "self" && channel === "inner")
|
|
517
510
|
continue;
|
|
518
511
|
const envelope = readSessionEnvelope(sessionPath);
|
|
519
|
-
const
|
|
512
|
+
const events = envelope?.events ?? [];
|
|
513
|
+
const chronology = (0, session_events_1.deriveSessionChronology)(events);
|
|
520
514
|
const lastUsage = parseSessionUsage(envelope?.lastUsage);
|
|
521
515
|
const continuity = parseSessionContinuity(envelope?.state);
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
516
|
+
const hasObservedEventTiming = events.some((event) => event.time.authoredAt !== null || event.time.observedAt !== null);
|
|
517
|
+
const lastActivityAt = hasObservedEventTiming
|
|
518
|
+
? (chronology.lastActivityAt ?? continuity?.lastFriendActivityAt ?? safeFileMtime(sessionPath) ?? now.toISOString())
|
|
519
|
+
: (continuity?.lastFriendActivityAt ?? safeFileMtime(sessionPath) ?? now.toISOString());
|
|
520
|
+
const activitySource = hasObservedEventTiming && chronology.lastActivityAt
|
|
521
|
+
? "event-timeline"
|
|
522
|
+
: continuity?.lastFriendActivityAt
|
|
523
|
+
? "friend-facing"
|
|
524
|
+
: "mtime-fallback";
|
|
525
|
+
const userMessages = events.filter((m) => m.role === "user");
|
|
526
|
+
const assistantMessages = events.filter((m) => m.role === "assistant");
|
|
526
527
|
const lastUser = userMessages.length > 0 ? userMessages[userMessages.length - 1] : null;
|
|
527
528
|
const lastAssistant = assistantMessages.length > 0 ? assistantMessages[assistantMessages.length - 1] : null;
|
|
528
529
|
const latestToolCallNames = [];
|
|
529
|
-
for (let i =
|
|
530
|
-
const names = extractToolCallNames(
|
|
530
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
531
|
+
const names = extractToolCallNames(events[i]);
|
|
531
532
|
if (names.length > 0) {
|
|
532
533
|
latestToolCallNames.push(...names);
|
|
533
534
|
break;
|
|
@@ -535,7 +536,7 @@ function readSessionInventory(agentName, options = {}) {
|
|
|
535
536
|
}
|
|
536
537
|
const friendName = resolveFriendName(friendsDir, friendId);
|
|
537
538
|
// Derive reply state from message pattern
|
|
538
|
-
const lastMsg =
|
|
539
|
+
const lastMsg = events.length > 0 ? events[events.length - 1] : null;
|
|
539
540
|
const mustResolve = continuity?.mustResolveBeforeHandoff === true;
|
|
540
541
|
let replyState = "idle";
|
|
541
542
|
if (mustResolve) {
|
|
@@ -544,7 +545,7 @@ function readSessionInventory(agentName, options = {}) {
|
|
|
544
545
|
else if (lastMsg?.role === "user") {
|
|
545
546
|
replyState = "needs-reply";
|
|
546
547
|
}
|
|
547
|
-
else if (
|
|
548
|
+
else if (events.length > 0) {
|
|
548
549
|
replyState = "monitoring";
|
|
549
550
|
}
|
|
550
551
|
items.push({
|
|
@@ -556,13 +557,13 @@ function readSessionInventory(agentName, options = {}) {
|
|
|
556
557
|
lastActivityAt,
|
|
557
558
|
activitySource,
|
|
558
559
|
replyState,
|
|
559
|
-
messageCount:
|
|
560
|
+
messageCount: events.length,
|
|
560
561
|
lastUsage,
|
|
561
562
|
continuity,
|
|
562
|
-
latestUserExcerpt: truncateExcerpt(extractContent(lastUser
|
|
563
|
-
latestAssistantExcerpt: truncateExcerpt(extractContent(lastAssistant
|
|
563
|
+
latestUserExcerpt: truncateExcerpt(extractContent(lastUser)),
|
|
564
|
+
latestAssistantExcerpt: truncateExcerpt(extractContent(lastAssistant)),
|
|
564
565
|
latestToolCallNames,
|
|
565
|
-
estimatedTokens:
|
|
566
|
+
estimatedTokens: events.length > 0 ? estimateTokenCount(events) : null,
|
|
566
567
|
});
|
|
567
568
|
}
|
|
568
569
|
items.sort((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt));
|
|
@@ -606,34 +607,10 @@ function readSessionTranscript(agentName, friendId, channel, key, options = {})
|
|
|
606
607
|
const envelope = readSessionEnvelope(sessionPath);
|
|
607
608
|
if (!envelope)
|
|
608
609
|
return null;
|
|
609
|
-
const rawMessages =
|
|
610
|
+
const rawMessages = envelope.events;
|
|
610
611
|
const friendsDir = path.join(agentRoot, "friends");
|
|
611
612
|
const friendName = resolveFriendName(friendsDir, friendId);
|
|
612
|
-
const messages = rawMessages
|
|
613
|
-
const role = typeof msg.role === "string" ? msg.role : "user";
|
|
614
|
-
const content = extractContent(msg);
|
|
615
|
-
const result = { index, role, content };
|
|
616
|
-
if (typeof msg.name === "string")
|
|
617
|
-
result.name = msg.name;
|
|
618
|
-
if (typeof msg.tool_call_id === "string")
|
|
619
|
-
result.tool_call_id = msg.tool_call_id;
|
|
620
|
-
if (Array.isArray(msg.tool_calls)) {
|
|
621
|
-
result.tool_calls = msg.tool_calls
|
|
622
|
-
.filter((call) => call != null && typeof call === "object")
|
|
623
|
-
.map((call) => {
|
|
624
|
-
const fn = call.function;
|
|
625
|
-
return {
|
|
626
|
-
id: typeof call.id === "string" ? call.id : "",
|
|
627
|
-
type: typeof call.type === "string" ? call.type : "function",
|
|
628
|
-
function: {
|
|
629
|
-
name: typeof fn?.name === "string" ? fn.name : "unknown",
|
|
630
|
-
arguments: typeof fn?.arguments === "string" ? fn.arguments : JSON.stringify(fn?.arguments ?? ""),
|
|
631
|
-
},
|
|
632
|
-
};
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
return result;
|
|
636
|
-
});
|
|
613
|
+
const messages = rawMessages;
|
|
637
614
|
return {
|
|
638
615
|
friendId,
|
|
639
616
|
friendName,
|