@saadnvd1/agent-os 0.3.506 → 0.3.507
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.
|
@@ -123,11 +123,17 @@ const usd = (x: number, floor: boolean) =>
|
|
|
123
123
|
const url = (u: string) =>
|
|
124
124
|
/^https?:\/\/[^\s<>()[\]`]+$/.test(u) ? `<${u}>` : codeSpan(u);
|
|
125
125
|
|
|
126
|
+
// A commit hash is not a secret, so it skips redact(): only a plain hash
|
|
127
|
+
// (or one the log labels as the PR head) is printed, anything else is not.
|
|
128
|
+
const SHA = /^[0-9a-f]{7,40}( \(PR head\))?$/;
|
|
129
|
+
export const commitText = (sha: string | null): string =>
|
|
130
|
+
sha && SHA.test(sha) ? `\`${sha}\`` : "unknown";
|
|
131
|
+
|
|
126
132
|
function header(d: LogData): string {
|
|
127
133
|
const start = sqliteMs(d.startedAt);
|
|
128
134
|
const end = sqliteMs(d.mergedAt);
|
|
129
135
|
const duration =
|
|
130
|
-
start !== null && end !== null ? formatDuration(end - start) :
|
|
136
|
+
start !== null && end !== null ? formatDuration(end - start) : null;
|
|
131
137
|
const rows: [string, string][] = [
|
|
132
138
|
["Task", `${codeSpan(d.name)} (${codeSpan(d.id)})`],
|
|
133
139
|
["Workspace", d.workspace ? codeSpan(d.workspace) : "none"],
|
|
@@ -139,10 +145,10 @@ function header(d: LogData): string {
|
|
|
139
145
|
? `#${d.prNumber}${d.prUrl ? ` ${url(d.prUrl)}` : ""}`
|
|
140
146
|
: "none recorded",
|
|
141
147
|
],
|
|
142
|
-
["Merge commit",
|
|
148
|
+
["Merge commit", commitText(d.mergeCommit)],
|
|
143
149
|
["Started", when(d.startedAt)],
|
|
144
|
-
["Merged", when(d.mergedAt)],
|
|
145
|
-
["Duration", duration],
|
|
150
|
+
["Merged", d.mergedAt ? when(d.mergedAt) : "unknown"],
|
|
151
|
+
...(duration ? [["Duration", duration] as [string, string]] : []),
|
|
146
152
|
];
|
|
147
153
|
return rows.map(([k, v]) => `- **${k}:** ${v}`).join("\n");
|
|
148
154
|
}
|
|
@@ -140,3 +140,8 @@ export function writeLogFileTo(
|
|
|
140
140
|
|
|
141
141
|
export const writeLogFile = (t: FileTarget, markdown: string): string =>
|
|
142
142
|
writeLogFileTo(t, markdown).file;
|
|
143
|
+
|
|
144
|
+
// Rewrites a log's own file in place (never creates one, never moves it).
|
|
145
|
+
export function overwriteLogFile(file: string, markdown: string): void {
|
|
146
|
+
if (fs.existsSync(file)) fs.writeFileSync(file, markdown, { mode: 0o600 });
|
|
147
|
+
}
|
|
@@ -14,10 +14,22 @@ import { db, type Session } from "../db";
|
|
|
14
14
|
import { getProject } from "../projects";
|
|
15
15
|
import { run } from "../tasks/gh";
|
|
16
16
|
import { buildLogMarkdown, sqliteMs } from "./build";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
overwriteLogFile,
|
|
19
|
+
destination,
|
|
20
|
+
writeLogFileTo,
|
|
21
|
+
type Destination,
|
|
22
|
+
} from "./folder";
|
|
18
23
|
import { commitLogs } from "./hq";
|
|
19
24
|
import { gatherLogData, readPlanFile } from "./gather";
|
|
20
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getLog,
|
|
27
|
+
hasLog,
|
|
28
|
+
headerOf,
|
|
29
|
+
insertLog,
|
|
30
|
+
setFilePath,
|
|
31
|
+
updateLog,
|
|
32
|
+
} from "./store";
|
|
21
33
|
|
|
22
34
|
export * from "./store";
|
|
23
35
|
export { buildLogMarkdown } from "./build";
|
|
@@ -43,6 +55,10 @@ export interface CaptureOpts {
|
|
|
43
55
|
// Given, the file is handed over instead of committed: a batch commits
|
|
44
56
|
// once at its end.
|
|
45
57
|
onFile?: (file: string, hq: NonNullable<Destination["hq"]>) => void;
|
|
58
|
+
// Regenerate a log that exists: update its row and file in place.
|
|
59
|
+
rewrite?: boolean;
|
|
60
|
+
// With rewrite: report whether the log would change, write nothing.
|
|
61
|
+
dryRun?: boolean;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
function eligible(s: Session | undefined): s is Session {
|
|
@@ -55,31 +71,51 @@ function eligible(s: Session | undefined): s is Session {
|
|
|
55
71
|
);
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
// GitHub's ISO time as SQLite's UTC "YYYY-MM-DD HH:MM:SS"; null when absent.
|
|
75
|
+
export function ghTimeToSqlite(iso: unknown): string | null {
|
|
76
|
+
if (typeof iso !== "string") return null;
|
|
77
|
+
const ms = Date.parse(iso);
|
|
78
|
+
return Number.isNaN(ms)
|
|
79
|
+
? null
|
|
80
|
+
: new Date(ms).toISOString().replace("T", " ").slice(0, 19);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function mergeInfoOf(
|
|
84
|
+
s: Session
|
|
85
|
+
): Promise<{ commit: string | null; mergedAt: string | null }> {
|
|
86
|
+
const none = { commit: null, mergedAt: null };
|
|
59
87
|
const project = s.project_id ? getProject(s.project_id) : undefined;
|
|
60
88
|
if (!project || !s.pr_number || process.env.AGENTOS_SESSION_LOGS_NO_GH)
|
|
61
|
-
return
|
|
89
|
+
return none;
|
|
62
90
|
try {
|
|
63
91
|
const out = await run(
|
|
64
92
|
"gh",
|
|
65
|
-
[
|
|
66
|
-
"pr",
|
|
67
|
-
"view",
|
|
68
|
-
String(s.pr_number),
|
|
69
|
-
"--json",
|
|
70
|
-
"mergeCommit",
|
|
71
|
-
"-q",
|
|
72
|
-
".mergeCommit.oid",
|
|
73
|
-
],
|
|
93
|
+
["pr", "view", String(s.pr_number), "--json", "mergeCommit,mergedAt"],
|
|
74
94
|
project.working_directory.replace(/^~/, os.homedir()),
|
|
75
95
|
15_000
|
|
76
96
|
);
|
|
77
|
-
|
|
97
|
+
const j = JSON.parse(out) as {
|
|
98
|
+
mergeCommit?: { oid?: string } | null;
|
|
99
|
+
mergedAt?: string | null;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
commit: j.mergeCommit?.oid?.trim() || null,
|
|
103
|
+
mergedAt: ghTimeToSqlite(j.mergedAt),
|
|
104
|
+
};
|
|
78
105
|
} catch {
|
|
79
|
-
return
|
|
106
|
+
return none;
|
|
80
107
|
}
|
|
81
108
|
}
|
|
82
109
|
|
|
110
|
+
const planSection = (md: string) =>
|
|
111
|
+
/^## Plan\n[\s\S]*?(?=\n## Timeline\n)/m.exec(md)?.[0] ?? null;
|
|
112
|
+
|
|
113
|
+
function keepPlan(fresh: string, old: string): string {
|
|
114
|
+
const was = planSection(old);
|
|
115
|
+
const now = planSection(fresh);
|
|
116
|
+
return was && now ? fresh.replace(now, () => was) : fresh;
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
export async function writeLog(
|
|
84
120
|
id: string,
|
|
85
121
|
opts: CaptureOpts & { plan?: string | null } = {}
|
|
@@ -87,20 +123,77 @@ export async function writeLog(
|
|
|
87
123
|
const row = db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(id) as
|
|
88
124
|
| Session
|
|
89
125
|
| undefined;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
126
|
+
const existing = getLog(id);
|
|
127
|
+
if (!eligible(row) || (existing && !opts.rewrite)) return false;
|
|
128
|
+
const info = opts.noGh
|
|
129
|
+
? { commit: null, mergedAt: null }
|
|
130
|
+
: await mergeInfoOf(row);
|
|
131
|
+
const mergeCommit = info.commit;
|
|
132
|
+
const s = { ...row, merged_at: row.merged_at ?? info.mergedAt };
|
|
133
|
+
const kept = existing ? headerOf(existing) : null;
|
|
93
134
|
const data = gatherLogData(s, {
|
|
94
135
|
plan: opts.plan ?? null,
|
|
95
|
-
planKept: opts.planKept ?? true,
|
|
136
|
+
planKept: kept ? kept.planKept : (opts.planKept ?? true),
|
|
96
137
|
head: opts.head ?? null,
|
|
97
138
|
mergeCommit: mergeCommit ?? (opts.head ? `${opts.head} (PR head)` : null),
|
|
98
139
|
});
|
|
99
|
-
|
|
140
|
+
let markdown = buildLogMarkdown(data);
|
|
141
|
+
// The plan file went with the worktree: keep the plan the log has.
|
|
142
|
+
if (existing && kept?.hasPlan)
|
|
143
|
+
markdown = keepPlan(markdown, existing.markdown);
|
|
100
144
|
const start = sqliteMs(data.startedAt);
|
|
101
145
|
const end = sqliteMs(data.mergedAt);
|
|
102
146
|
const workspaceId =
|
|
103
147
|
s.workspace_id ?? getProject(s.project_id ?? "")?.workspace_id ?? null;
|
|
148
|
+
if (existing) {
|
|
149
|
+
const changed = existing.markdown !== markdown;
|
|
150
|
+
if (opts.dryRun || !changed) return changed;
|
|
151
|
+
// Only the generated text changes; the row, its id and its file stay.
|
|
152
|
+
updateLog(id, markdown, {
|
|
153
|
+
id,
|
|
154
|
+
name: data.name,
|
|
155
|
+
workspace: data.workspace,
|
|
156
|
+
project: data.project,
|
|
157
|
+
branch: data.branch,
|
|
158
|
+
prNumber: data.prNumber,
|
|
159
|
+
prUrl: data.prUrl,
|
|
160
|
+
mergeCommit: data.mergeCommit,
|
|
161
|
+
startedAt: data.startedAt,
|
|
162
|
+
mergedAt: data.mergedAt,
|
|
163
|
+
durationMs: start !== null && end !== null ? end - start : null,
|
|
164
|
+
costUsd: data.usage.reduce((t, u) => t + u.costUsd, 0),
|
|
165
|
+
planKept: data.planKept,
|
|
166
|
+
hasPlan: kept?.hasPlan ?? data.plan !== null,
|
|
167
|
+
});
|
|
168
|
+
if (existing.file_path) {
|
|
169
|
+
try {
|
|
170
|
+
overwriteLogFile(existing.file_path, markdown);
|
|
171
|
+
const { hq } = destination({
|
|
172
|
+
workspace: data.workspace,
|
|
173
|
+
workspaceId,
|
|
174
|
+
project: data.project,
|
|
175
|
+
projectId: s.project_id,
|
|
176
|
+
name: data.name,
|
|
177
|
+
id,
|
|
178
|
+
day: "",
|
|
179
|
+
});
|
|
180
|
+
if (hq && existing.file_path.startsWith(hq.dir)) {
|
|
181
|
+
if (opts.onFile) opts.onFile(existing.file_path, hq);
|
|
182
|
+
else
|
|
183
|
+
await commitLogs(
|
|
184
|
+
hq.dir,
|
|
185
|
+
[existing.file_path],
|
|
186
|
+
`session log rewritten: ${data.name.replace(/\s+/g, " ").trim().slice(0, 100)}`,
|
|
187
|
+
hq.visibility
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(`[session-logs] file for ${id}:`, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (opts.dryRun) return true;
|
|
104
197
|
const stored = insertLog({
|
|
105
198
|
sessionId: id,
|
|
106
199
|
projectId: s.project_id,
|
|
@@ -70,6 +70,17 @@ export function insertLog(row: {
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// Replaces the text and header of a log that exists; the row stays.
|
|
74
|
+
export function updateLog(
|
|
75
|
+
sessionId: string,
|
|
76
|
+
markdown: string,
|
|
77
|
+
header: Omit<LogHeader, "createdAt" | "filePath">
|
|
78
|
+
): void {
|
|
79
|
+
db.prepare(
|
|
80
|
+
`UPDATE session_logs SET markdown = ?, data = ? WHERE session_id = ?`
|
|
81
|
+
).run(markdown, JSON.stringify(header), sessionId);
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
export function setFilePath(sessionId: string, filePath: string): void {
|
|
74
85
|
db.prepare(`UPDATE session_logs SET file_path = ? WHERE session_id = ?`).run(
|
|
75
86
|
filePath,
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Writes session logs for tasks that merged before logs existed, from what
|
|
3
3
|
* the database still holds. Their plan files went with their worktrees, so
|
|
4
|
-
* each says "plan not kept". Idempotent: a task with a log is skipped.
|
|
4
|
+
* each says "plan not kept". Idempotent: a task with a log is skipped, unless --rewrite.
|
|
5
5
|
*
|
|
6
|
-
* npx tsx scripts/backfill-session-logs.ts --db <path to agent-os.db> [--no-gh] [--dry] [--reexport]
|
|
6
|
+
* npx tsx scripts/backfill-session-logs.ts --db <path to agent-os.db> [--no-gh] [--dry] [--dry-run] [--rewrite] [--reexport]
|
|
7
7
|
*
|
|
8
8
|
* --db is required so a run never lands on a database by accident. Files go
|
|
9
9
|
* where new logs go: a project's folder, else the workspace's HQ repo (one
|
|
10
10
|
* commit per HQ at the end of the run), else ~/.agent-os/session-logs; run
|
|
11
11
|
* with HOME set elsewhere to keep a trial away from the real folder.
|
|
12
|
-
* --no-gh skips asking GitHub for each merge commit.
|
|
12
|
+
* --no-gh skips asking GitHub for each merge commit and time.
|
|
13
13
|
* --reexport writes the files of logs already in the database to where they
|
|
14
14
|
* go now, from the stored markdown (no rebuild, no model), and writes no new
|
|
15
15
|
* logs. Old files are left in place.
|
|
16
|
+
* --rewrite regenerates logs that exist (row and file updated in place, a
|
|
17
|
+
* kept plan stays); no model is called, only the one gh call per PR.
|
|
18
|
+
* --dry-run prints how many logs would be written or change, and writes none.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
const args = process.argv.slice(2);
|
|
@@ -21,7 +24,7 @@ const dbPath = args[args.indexOf("--db") + 1];
|
|
|
21
24
|
|
|
22
25
|
if (!flag("--db") || !dbPath || dbPath.startsWith("--")) {
|
|
23
26
|
console.error(
|
|
24
|
-
"usage: backfill-session-logs.ts --db <path> [--no-gh] [--dry] [--reexport]"
|
|
27
|
+
"usage: backfill-session-logs.ts --db <path> [--no-gh] [--dry] [--dry-run] [--rewrite] [--reexport]"
|
|
25
28
|
);
|
|
26
29
|
process.exit(2);
|
|
27
30
|
}
|
|
@@ -50,17 +53,15 @@ async function main() {
|
|
|
50
53
|
ORDER BY COALESCE(merged_at, updated_at)`
|
|
51
54
|
)
|
|
52
55
|
.all() as { id: string; name: string }[];
|
|
53
|
-
const
|
|
56
|
+
const rewrite = flag("--rewrite");
|
|
57
|
+
const dry = flag("--dry") || flag("--dry-run");
|
|
58
|
+
const todo = rewrite ? rows : rows.filter((r) => !hasLog(r.id));
|
|
54
59
|
console.log(
|
|
55
|
-
`${rows.length} merged tasks, ${rows.length -
|
|
60
|
+
`${rows.length} merged tasks, ${rows.length - rows.filter((r) => !hasLog(r.id)).length} already logged, ${todo.length} to ${rewrite ? "regenerate" : "write"}${dry ? " (dry run)" : ""}`
|
|
56
61
|
);
|
|
57
62
|
let written = 0;
|
|
58
63
|
const byHq = new Map<string, { files: string[]; visibility: Visibility }>();
|
|
59
64
|
for (const r of todo) {
|
|
60
|
-
if (flag("--dry")) {
|
|
61
|
-
console.log(` would log ${r.name} (${r.id.slice(0, 8)})`);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
65
|
try {
|
|
65
66
|
const ok = await writeLog(r.id, {
|
|
66
67
|
planKept: false,
|
|
@@ -73,10 +74,12 @@ async function main() {
|
|
|
73
74
|
e.files.push(file);
|
|
74
75
|
byHq.set(hq.dir, e);
|
|
75
76
|
},
|
|
77
|
+
rewrite,
|
|
78
|
+
dryRun: dry,
|
|
76
79
|
});
|
|
77
80
|
if (ok) written++;
|
|
78
81
|
console.log(
|
|
79
|
-
` ${ok ? "logged" : "skipped"} ${r.name} (${r.id.slice(0, 8)})`
|
|
82
|
+
` ${ok ? (dry ? "would change" : rewrite ? "rewrote" : "logged") : "skipped"} ${r.name} (${r.id.slice(0, 8)})`
|
|
80
83
|
);
|
|
81
84
|
} catch (error) {
|
|
82
85
|
console.error(` FAILED ${r.name} (${r.id.slice(0, 8)}):`, error);
|
|
@@ -89,7 +92,9 @@ async function main() {
|
|
|
89
92
|
`session logs: ${files.length} backfilled`,
|
|
90
93
|
visibility
|
|
91
94
|
);
|
|
92
|
-
console.log(
|
|
95
|
+
console.log(
|
|
96
|
+
`${dry ? `${written} logs would change` : `wrote ${written} logs`}`
|
|
97
|
+
);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
main().then(
|