@saadnvd1/agent-os 0.3.828 → 0.3.836
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/README.md +2 -0
- package/app/api/sessions/route.ts +7 -1
- package/app/globals.css +14 -0
- package/app/layout.tsx +3 -0
- package/components/Chat/ChatPanel.tsx +110 -27
- package/components/Chat/EventRow.tsx +15 -1
- package/components/Chat/ForkResults.tsx +109 -0
- package/components/Chat/Forks.tsx +69 -103
- package/components/Chat/Messages.tsx +14 -0
- package/components/Chat/QuoteButton.tsx +59 -21
- package/components/Chat/TimelineRows.tsx +1 -0
- package/components/Chat/WorktreeRemovedNotice.tsx +17 -0
- package/components/Chat/composer/ComposerBody.tsx +23 -2
- package/components/Chat/composer/ForkHeader.tsx +66 -0
- package/components/Chat/composer/Toolbar.tsx +43 -7
- package/components/Chat/fork-draft.test.ts +57 -0
- package/components/Chat/fork-draft.ts +34 -0
- package/components/Chat/quote-selection.ts +6 -2
- package/content/whats-new.json +24 -0
- package/data/fork/index.ts +6 -1
- package/lib/chat/commands.ts +0 -15
- package/lib/chat/events.ts +3 -0
- package/lib/chat/socket.ts +7 -3
- package/lib/chat/start-error.ts +13 -0
- package/lib/chat/worker/cwd.ts +77 -0
- package/lib/chat/worker/host.ts +11 -2
- package/lib/chat/worker/main.ts +11 -2
- package/lib/db/types.ts +2 -0
- package/lib/fork/brief.ts +4 -2
- package/lib/fork/deliver.ts +6 -1
- package/lib/fork/ledger.ts +5 -0
- package/lib/fork/note.ts +6 -0
- package/lib/fork/settle.ts +8 -4
- package/lib/fork/start.ts +6 -2
- package/lib/fork/types.ts +7 -1
- package/lib/fork/view.ts +18 -2
- package/package.json +1 -1
- package/scripts/lib/build-swap.sh +1 -1
- package/scripts/update-e2e.test.ts +2 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// What a person reads when a chat's worker can't start. The raw error (a
|
|
2
|
+
// socket ENOENT, usually) stays in the server log.
|
|
3
|
+
export function startFailureMessage(error: unknown): string {
|
|
4
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
5
|
+
if (!/^The chat worker didn't start/.test(raw)) return raw;
|
|
6
|
+
const reason = /ENOENT|ECONNREFUSED/.test(raw)
|
|
7
|
+
? "it stopped while starting"
|
|
8
|
+
: raw
|
|
9
|
+
.replace(/^The chat worker didn't start:?\s*/, "")
|
|
10
|
+
.split("\n")[0]
|
|
11
|
+
.slice(0, 120) || "it didn't answer";
|
|
12
|
+
return `Couldn't start the agent: ${reason}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getProject } from "../../projects";
|
|
5
|
+
import type { Session } from "../../db";
|
|
6
|
+
|
|
7
|
+
const expand = (p: string) => p.replace(/^~/, os.homedir());
|
|
8
|
+
const isDir = (p: string) => {
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(p).isDirectory();
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const WORKTREE_REMOVED_NOTE =
|
|
17
|
+
"(This task's PR merged and its worktree was removed. You are now running in the project's main checkout: do not edit files or commit here. Questions are fine; if more changes are needed, say a follow-up task is required.)";
|
|
18
|
+
|
|
19
|
+
// The project's own checkout, for a session whose folder is gone; null when
|
|
20
|
+
// that is missing too or the session never had a project.
|
|
21
|
+
function projectRoot(session: Session): string | null {
|
|
22
|
+
const project = session.project_id ? getProject(session.project_id) : null;
|
|
23
|
+
const root = project ? expand(project.working_directory) : null;
|
|
24
|
+
return root && isDir(root) ? root : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Where a session's agent runs. A task's worktree is removed when it
|
|
28
|
+
// merges, but its chat can still be asked a question: it runs in the
|
|
29
|
+
// project's checkout instead.
|
|
30
|
+
export function resolveChatCwd(session: Session): {
|
|
31
|
+
session: Session;
|
|
32
|
+
removedFrom: string | null;
|
|
33
|
+
} {
|
|
34
|
+
const dir = expand(session.working_directory);
|
|
35
|
+
if (isDir(dir)) return { session, removedFrom: null };
|
|
36
|
+
const root = projectRoot(session);
|
|
37
|
+
if (!root) return { session, removedFrom: null };
|
|
38
|
+
return {
|
|
39
|
+
session: { ...session, working_directory: root },
|
|
40
|
+
removedFrom: dir,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Claude finds a conversation under its folder's name in its config dir, so
|
|
45
|
+
// a resume from a new folder needs the transcript copied there.
|
|
46
|
+
export function carryClaudeTranscript(
|
|
47
|
+
from: string,
|
|
48
|
+
to: string,
|
|
49
|
+
claudeSessionId: string | null
|
|
50
|
+
): void {
|
|
51
|
+
if (!claudeSessionId) return;
|
|
52
|
+
const base = path.join(
|
|
53
|
+
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude"),
|
|
54
|
+
"projects"
|
|
55
|
+
);
|
|
56
|
+
const name = (d: string) => d.replace(/[^a-zA-Z0-9]/g, "-");
|
|
57
|
+
const src = path.join(base, name(from), `${claudeSessionId}.jsonl`);
|
|
58
|
+
const dest = path.join(base, name(to), `${claudeSessionId}.jsonl`);
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(src) || fs.existsSync(dest)) return;
|
|
61
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
62
|
+
fs.copyFileSync(src, dest);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(
|
|
65
|
+
"Couldn't carry the transcript to the project folder:",
|
|
66
|
+
error
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A finished task whose worktree is gone: the sidebar's cue for the notice.
|
|
72
|
+
export function worktreeRemoved(session: Session): boolean {
|
|
73
|
+
if (!session.worktree_path || !session.task_status) return false;
|
|
74
|
+
if (session.task_status === "running") return false;
|
|
75
|
+
if (session.host_id && session.host_id !== "local") return false;
|
|
76
|
+
return !isDir(expand(session.worktree_path));
|
|
77
|
+
}
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -8,6 +8,7 @@ import os from "os";
|
|
|
8
8
|
import { db, type Session } from "../../db";
|
|
9
9
|
import { agentEnv } from "../../agents/launch";
|
|
10
10
|
import { forkBrief } from "../../fork/extras";
|
|
11
|
+
import { WORKTREE_REMOVED_NOTE } from "./cwd";
|
|
11
12
|
import { sessionRunningBrief } from "../../project-config/session";
|
|
12
13
|
import { BUS_BRIEF } from "../../agents/brief";
|
|
13
14
|
import { resolveModelForAgent } from "../../model-catalog";
|
|
@@ -196,7 +197,9 @@ export class ChatHost {
|
|
|
196
197
|
private session: Session,
|
|
197
198
|
private emit: (e: WorkerEvent) => void,
|
|
198
199
|
// What a session's role adds: its brief and its own tools.
|
|
199
|
-
extras: RoleExtras = {}
|
|
200
|
+
extras: RoleExtras = {},
|
|
201
|
+
// Its worktree was removed: it runs in the project's checkout.
|
|
202
|
+
private worktreeGone = false
|
|
200
203
|
) {
|
|
201
204
|
this.gate = new GuestGate((note) => {
|
|
202
205
|
if (!this.busy()) return;
|
|
@@ -611,7 +614,13 @@ export class ChatHost {
|
|
|
611
614
|
if (handOver) this.stopsUpTo = this.turnsStarted;
|
|
612
615
|
if (now) this.nowPending++;
|
|
613
616
|
this.stopping = null;
|
|
614
|
-
const note =
|
|
617
|
+
const note =
|
|
618
|
+
[
|
|
619
|
+
this.worktreeGone && WORKTREE_REMOVED_NOTE,
|
|
620
|
+
this.refreshedNote ?? (this.restartedNote && RESTARTED_NOTE),
|
|
621
|
+
]
|
|
622
|
+
.filter(Boolean)
|
|
623
|
+
.join("\n") || null;
|
|
615
624
|
const text = agentText(note ? `${note}\n\n${m.text}` : m.text, m.files);
|
|
616
625
|
if (this.refreshedNote) {
|
|
617
626
|
this.refreshedNote = null;
|
package/lib/chat/worker/main.ts
CHANGED
|
@@ -41,9 +41,17 @@ async function main(sessionId: string) {
|
|
|
41
41
|
console.error(`No session ${sessionId}`);
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|
|
44
|
-
const
|
|
44
|
+
const routed = await (
|
|
45
45
|
await import("../../models/launch")
|
|
46
46
|
).routeOrchestrator(row);
|
|
47
|
+
const { resolveChatCwd, carryClaudeTranscript } = await import("./cwd");
|
|
48
|
+
const { session, removedFrom } = resolveChatCwd(routed);
|
|
49
|
+
if (removedFrom && session.agent_type === "claude")
|
|
50
|
+
carryClaudeTranscript(
|
|
51
|
+
removedFrom,
|
|
52
|
+
session.working_directory,
|
|
53
|
+
session.claude_session_id
|
|
54
|
+
);
|
|
47
55
|
|
|
48
56
|
const clients = new Set<net.Socket>();
|
|
49
57
|
const emit = (e: WorkerEvent) => {
|
|
@@ -60,7 +68,8 @@ async function main(sessionId: string) {
|
|
|
60
68
|
emit(e);
|
|
61
69
|
if (e.type === "state") armIdle();
|
|
62
70
|
},
|
|
63
|
-
extras
|
|
71
|
+
extras,
|
|
72
|
+
!!removedFrom
|
|
64
73
|
);
|
|
65
74
|
function armIdle() {
|
|
66
75
|
clearTimeout(idleTimer);
|
package/lib/db/types.ts
CHANGED
|
@@ -87,6 +87,8 @@ export interface Session {
|
|
|
87
87
|
// Task priority (null reads as normal) and timed pause (lib/tasks/pause.ts):
|
|
88
88
|
// pausing, paused or resuming, until when (null: until resumed).
|
|
89
89
|
priority?: string | null;
|
|
90
|
+
// Set only by GET /api/sessions: a finished task whose worktree is gone.
|
|
91
|
+
worktree_removed?: boolean;
|
|
90
92
|
pause_state?: "pausing" | "paused" | "resuming" | null;
|
|
91
93
|
paused_until?: string | null;
|
|
92
94
|
pause_id?: number | null;
|
package/lib/fork/brief.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { FORK_LIMITS } from "./options";
|
|
4
4
|
import type { ChildRow } from "./types";
|
|
5
5
|
|
|
6
|
-
export const PARENT_BRIEF = `Forks: when you have independent side work, \`aos fork "<instruction>" ["<instruction>" …]\` starts that many background copies of you (up to ${FORK_LIMITS.maxPerFork}) that know what you know. It returns at once; you keep working. Their results arrive as a message when they finish, or after \`aos wait children --all|--any [--timeout 30m]\` (end your turn; you're woken with them). \`aos children\` lists them, \`aos children <n>\` shows a full result, \`aos cancel <n>|--all\` stops them. Children have your abilities and work in your checkout: tell one that has to edit code to \`--worktree\` (its own throwaway checkout). Fork only work that doesn't depend on your next step.`;
|
|
6
|
+
export const PARENT_BRIEF = `Forks: when the user asks you to fork, or to do things in parallel in background copies, run \`aos fork "<instruction>" ...\` (one instruction per fork), keep working, and results arrive as a message. Also when you have independent side work, \`aos fork "<instruction>" ["<instruction>" …]\` starts that many background copies of you (up to ${FORK_LIMITS.maxPerFork}) that know what you know. It returns at once; you keep working. Their results arrive as a message when they finish, or after \`aos wait children --all|--any [--timeout 30m]\` (end your turn; you're woken with them). \`aos children\` lists them, \`aos children <n>\` shows a full result, \`aos cancel <n>|--all\` stops them. Children have your abilities and work in your checkout: tell one that has to edit code to \`--worktree\` (its own throwaway checkout). Fork only work that doesn't depend on your next step.`;
|
|
7
7
|
|
|
8
8
|
export const CHILD_BRIEF = `You are a fork child: a background copy of another session, started to do one piece of work and report back. You have its abilities, not its checkout: don't change the parent's files, push, open a PR or fork. Put temp work (scripts, repros, notes) in $AOS_SCRATCH. Run builds and tests only through \`aos heavy -- <command>\`, which queues you with the other forks. To ask the parent something, \`aos send <parent> "<question>"\`; that doesn't finish you. When done, \`aos return "<result>"\` (first line: the answer in a sentence; then the detail; \`--failed\` if you couldn't do it). Your reply when you end your turn without it is used as the result, after a short wait.`;
|
|
9
9
|
|
|
@@ -41,7 +41,9 @@ const outcomeWord = (c: ChildRow) =>
|
|
|
41
41
|
? "failed"
|
|
42
42
|
: c.outcome === "cancelled"
|
|
43
43
|
? "cancelled"
|
|
44
|
-
: "
|
|
44
|
+
: c.outcome === "empty"
|
|
45
|
+
? "ended with no result"
|
|
46
|
+
: "done";
|
|
45
47
|
|
|
46
48
|
// One delivery: every result in the batch, whole when short, else its
|
|
47
49
|
// summary and the command that reads the rest.
|
package/lib/fork/deliver.ts
CHANGED
|
@@ -34,7 +34,12 @@ async function send(parentId: string, deliveryId: string, rows: ChildRow[]) {
|
|
|
34
34
|
text,
|
|
35
35
|
queue: true,
|
|
36
36
|
from: "AgentOS",
|
|
37
|
-
origin: {
|
|
37
|
+
origin: {
|
|
38
|
+
kind: "event",
|
|
39
|
+
label: "Forks",
|
|
40
|
+
body,
|
|
41
|
+
forks: { parent: parentId, ids: rows.map((c) => c.child_session_id) },
|
|
42
|
+
},
|
|
38
43
|
});
|
|
39
44
|
markDelivered(deliveryId);
|
|
40
45
|
}
|
package/lib/fork/ledger.ts
CHANGED
|
@@ -176,6 +176,11 @@ export function summaryOf(text: string): string {
|
|
|
176
176
|
return one.length <= max ? one : `${one.slice(0, max - 1)}…`;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// A result that says nothing: blank, or one bare flag (`--help`) a child took
|
|
180
|
+
// for its answer. A quoted sentence that starts with dashes is still an answer.
|
|
181
|
+
export const isNonAnswer = (text: string | null | undefined): boolean =>
|
|
182
|
+
!text?.trim() || /^--?[a-z][a-z-]*$/i.test(text.trim());
|
|
183
|
+
|
|
179
184
|
export const capResult = (text: string): string =>
|
|
180
185
|
text.length <= FORK_LIMITS.resultChars
|
|
181
186
|
? text
|
package/lib/fork/note.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The one line the parent's chat keeps where forks were started. */
|
|
2
|
+
|
|
3
|
+
export const FORK_NOTE_PREFIX = "fork-";
|
|
4
|
+
|
|
5
|
+
export const startedText = (ns: number[], labels: string[]): string =>
|
|
6
|
+
`${ns.length === 1 ? `Fork ${ns[0]}` : `Forks ${ns.join(", ")}`} started · ${labels.join(" · ")}`;
|
package/lib/fork/settle.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
updateResult,
|
|
23
23
|
childrenOf,
|
|
24
24
|
findChild,
|
|
25
|
+
isNonAnswer,
|
|
25
26
|
markDeliveredIds,
|
|
26
27
|
} from "./ledger";
|
|
27
28
|
import { FORK_LIMITS } from "./options";
|
|
@@ -120,7 +121,11 @@ export async function returnChild(
|
|
|
120
121
|
if (!row) throw new Error("Only a fork child can return a result");
|
|
121
122
|
const body = text.trim();
|
|
122
123
|
if (!body) throw new Error('Give the result: aos return "<result>"');
|
|
123
|
-
const outcome: ChildOutcome = failed
|
|
124
|
+
const outcome: ChildOutcome = failed
|
|
125
|
+
? "failed"
|
|
126
|
+
: isNonAnswer(body)
|
|
127
|
+
? "empty"
|
|
128
|
+
: "ok";
|
|
124
129
|
if (
|
|
125
130
|
await settleAndDeliver(childId, {
|
|
126
131
|
outcome,
|
|
@@ -205,10 +210,9 @@ export async function lookAtChild(row: ChildRow, now: number): Promise<void> {
|
|
|
205
210
|
// It asked its parent something and is waiting for the answer.
|
|
206
211
|
if (openWaits().some((w) => w.waiter_id === row.child_session_id)) return;
|
|
207
212
|
const reply = lastReply(row.child_session_id);
|
|
208
|
-
if (!reply) return;
|
|
209
213
|
await settleAndDeliver(row.child_session_id, {
|
|
210
|
-
outcome: "ok",
|
|
211
|
-
summary: summaryOf(reply),
|
|
214
|
+
outcome: isNonAnswer(reply) ? "empty" : "ok",
|
|
215
|
+
summary: reply ? summaryOf(reply) : "ended without a result",
|
|
212
216
|
result: reply,
|
|
213
217
|
kind: "implicit",
|
|
214
218
|
});
|
package/lib/fork/start.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getProject } from "../projects";
|
|
|
18
18
|
import { limitOf, runningIn, startingIn } from "../tasks/queue-rows";
|
|
19
19
|
import { currentLevel, loadEnabled } from "../load/monitor";
|
|
20
20
|
import { childFirstMessage } from "./brief";
|
|
21
|
+
import { FORK_NOTE_PREFIX, startedText } from "./note";
|
|
21
22
|
import { ForkError, forkConversation } from "./conversation";
|
|
22
23
|
import {
|
|
23
24
|
childrenOf,
|
|
@@ -120,7 +121,7 @@ export function startFork(req: ForkRequest): ForkStarted {
|
|
|
120
121
|
instruction,
|
|
121
122
|
worktree,
|
|
122
123
|
}));
|
|
123
|
-
const noteId =
|
|
124
|
+
const noteId = `${FORK_NOTE_PREFIX}${groupId}`;
|
|
124
125
|
insertGroup({
|
|
125
126
|
id: groupId,
|
|
126
127
|
parentId: parent.id,
|
|
@@ -136,7 +137,10 @@ export function startFork(req: ForkRequest): ForkStarted {
|
|
|
136
137
|
kind: "note" as const,
|
|
137
138
|
tone: "note" as const,
|
|
138
139
|
createdAt: Date.now(),
|
|
139
|
-
text:
|
|
140
|
+
text: startedText(
|
|
141
|
+
children.map((c) => numberOf(parent.id, c.childId)),
|
|
142
|
+
children.map((c) => c.label)
|
|
143
|
+
),
|
|
140
144
|
};
|
|
141
145
|
saveItem(parent.id, item);
|
|
142
146
|
emit(parent.id, { type: "item", item });
|
package/lib/fork/types.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export type ChildState =
|
|
2
2
|
"queued" | "starting" | "running" | "needs_input" | "settled";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// `empty`: it ended, but with nothing to read (blank, or a bare flag like
|
|
5
|
+
// "--help"), so it isn't shown as a finished answer.
|
|
6
|
+
export type ChildOutcome = "ok" | "failed" | "cancelled" | "empty";
|
|
5
7
|
|
|
6
8
|
// `implicit`: the child's turn ended without `aos return`, so its final
|
|
7
9
|
// answer stands in (kept apart so stats can tell them).
|
|
@@ -59,6 +61,8 @@ export interface ChildView {
|
|
|
59
61
|
hold: string | null;
|
|
60
62
|
outcome: ChildOutcome | null;
|
|
61
63
|
summary: string | null;
|
|
64
|
+
// The start of its result, for the card in the parent's chat.
|
|
65
|
+
excerpt: string | null;
|
|
62
66
|
resultKind: ResultKind | null;
|
|
63
67
|
worktree: boolean;
|
|
64
68
|
fellBack: boolean;
|
|
@@ -67,6 +71,8 @@ export interface ChildView {
|
|
|
67
71
|
delivered: boolean;
|
|
68
72
|
// A question or approval it's waiting on.
|
|
69
73
|
asks: string | null;
|
|
74
|
+
// What it is doing now (its latest tool), while it runs.
|
|
75
|
+
activity: string | null;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
export interface GroupView {
|
package/lib/fork/view.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** What clients and `aos children` show, and the numbers behind them. */
|
|
2
2
|
|
|
3
|
+
import { lastItems } from "../chat/store";
|
|
3
4
|
import { childrenOf, getGroup } from "./ledger";
|
|
4
5
|
import type { ChildRow, ChildView, GroupView } from "./types";
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ export const childView = (c: ChildRow): ChildView => ({
|
|
|
13
14
|
hold: c.hold,
|
|
14
15
|
outcome: c.outcome,
|
|
15
16
|
summary: c.summary,
|
|
17
|
+
excerpt: c.result ? c.result.slice(0, 600) : null,
|
|
16
18
|
resultKind: c.result_kind,
|
|
17
19
|
worktree: !!c.worktree,
|
|
18
20
|
fellBack: !!c.fell_back,
|
|
@@ -20,8 +22,16 @@ export const childView = (c: ChildRow): ChildView => ({
|
|
|
20
22
|
settledAt: c.settled_at,
|
|
21
23
|
delivered: c.delivered_at != null,
|
|
22
24
|
asks: c.state === "needs_input" ? c.hold : null,
|
|
25
|
+
activity: null,
|
|
23
26
|
});
|
|
24
27
|
|
|
28
|
+
// The title of the latest tool a running child used, to say what it is doing.
|
|
29
|
+
const activityOf = (c: ChildRow): string | null => {
|
|
30
|
+
if (c.state !== "running") return null;
|
|
31
|
+
const tool = lastItems(c.child_session_id, 1, "tool")[0];
|
|
32
|
+
return tool && tool.kind === "tool" ? tool.title : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
25
35
|
export function childrenView(parentId: string): {
|
|
26
36
|
children: ChildView[];
|
|
27
37
|
groups: GroupView[];
|
|
@@ -44,7 +54,11 @@ export function childrenView(parentId: string): {
|
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
56
|
return {
|
|
47
|
-
children: rows.map((c, i) => ({
|
|
57
|
+
children: rows.map((c, i) => ({
|
|
58
|
+
...childView(c),
|
|
59
|
+
n: i + 1,
|
|
60
|
+
activity: activityOf(c),
|
|
61
|
+
})),
|
|
48
62
|
groups,
|
|
49
63
|
};
|
|
50
64
|
}
|
|
@@ -61,6 +75,7 @@ export interface ForkStats {
|
|
|
61
75
|
ok: number;
|
|
62
76
|
failed: number;
|
|
63
77
|
cancelled: number;
|
|
78
|
+
empty: number;
|
|
64
79
|
implicit: number;
|
|
65
80
|
fellBack: number;
|
|
66
81
|
medianSeconds: number | null;
|
|
@@ -83,6 +98,7 @@ export function statsOf(rows: ChildRow[]): ForkStats {
|
|
|
83
98
|
ok: done.filter((c) => c.outcome === "ok").length,
|
|
84
99
|
failed: done.filter((c) => c.outcome === "failed").length,
|
|
85
100
|
cancelled: done.filter((c) => c.outcome === "cancelled").length,
|
|
101
|
+
empty: done.filter((c) => c.outcome === "empty").length,
|
|
86
102
|
implicit: done.filter(
|
|
87
103
|
(c) => c.result_kind === "implicit" && c.outcome === "ok"
|
|
88
104
|
).length,
|
|
@@ -106,7 +122,7 @@ export function statsText(s: ForkStats): string {
|
|
|
106
122
|
const pct = (n: number) =>
|
|
107
123
|
s.settled ? `${Math.round((n / s.settled) * 100)}%` : "-";
|
|
108
124
|
return [
|
|
109
|
-
`${s.children} children, ${s.settled} settled (${s.ok} ok, ${s.failed} failed, ${s.cancelled} cancelled)`,
|
|
125
|
+
`${s.children} children, ${s.settled} settled (${s.ok} ok, ${s.failed} failed, ${s.cancelled} cancelled, ${s.empty} no result)`,
|
|
110
126
|
`returned without aos return: ${s.implicit} (${pct(s.implicit)}); started from a brief, not a fork: ${s.fellBack}`,
|
|
111
127
|
`median time to settle: ${s.medianSeconds ?? "-"}s`,
|
|
112
128
|
`tokens: ${s.tokensIn} in, ${s.tokensOut} out, ${s.cacheRead} cache read`,
|
package/package.json
CHANGED