@mnemom/mnemom 0.17.0-next.8 → 0.17.0-next.9
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/dist/commands/agent-sessions.d.ts +99 -0
- package/dist/commands/agent-sessions.js +271 -0
- package/dist/commands/agent.d.ts +19 -0
- package/dist/commands/agent.js +53 -1
- package/dist/commands/validate.js +1 -16
- package/dist/index.js +30 -1
- package/dist/lib/agent-ledger.d.ts +91 -0
- package/dist/lib/agent-ledger.js +126 -0
- package/dist/lib/api.d.ts +62 -0
- package/dist/lib/api.js +11 -0
- package/dist/lib/format.d.ts +6 -0
- package/dist/lib/format.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent sessions [--all] [--json]` and `mnemom agent show [ref] [--json]`
|
|
3
|
+
* — the read side of the local session ledger (`~/.mnemom/sessions.jsonl`, one
|
|
4
|
+
* line per real launch) merged with the gateway's live goal state for one
|
|
5
|
+
* session (`GET /v1/agent/sessions/{conversationId}` via mnemom-api).
|
|
6
|
+
*
|
|
7
|
+
* Pure apart from the injected deps (`readRecords`, `fetchSession`, `out`,
|
|
8
|
+
* `err`), so every arm is unit-tested; `liveSessionDeps` is the thin driver
|
|
9
|
+
* index.ts wires in.
|
|
10
|
+
*
|
|
11
|
+
* Hash note: the CLI's launch-time envelope sha will NOT equal the gateway's
|
|
12
|
+
* sealed hash in general (the gateway normalises absent arrays to `[]`), so the
|
|
13
|
+
* local sha is never printed as "the sealed hash" and no mismatch is flagged —
|
|
14
|
+
* only the server's hash is shown, labelled "sealed hash".
|
|
15
|
+
*/
|
|
16
|
+
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
17
|
+
import { type AgentSessionResponse } from "../lib/api.js";
|
|
18
|
+
export interface SessionCommandDeps {
|
|
19
|
+
/** The ledger, newest first. */
|
|
20
|
+
readRecords: () => SessionRecord[];
|
|
21
|
+
/** The live state from mnemom-api; rejects when unreachable / not found. */
|
|
22
|
+
fetchSession: (conversationId: string, agentId: string, orgId: string) => Promise<AgentSessionResponse>;
|
|
23
|
+
out: (line: string) => void;
|
|
24
|
+
err: (line: string) => void;
|
|
25
|
+
}
|
|
26
|
+
/** The production wiring (index.ts). Lazy so a test that mocks lib/api without
|
|
27
|
+
* this export can still import index.ts. */
|
|
28
|
+
export declare const liveSessionDeps: SessionCommandDeps;
|
|
29
|
+
export declare const NO_SESSIONS_MESSAGE = "No sessions yet \u2014 run `mnemom agent` to start one.";
|
|
30
|
+
export declare const DEFAULT_SESSIONS_LIMIT = 20;
|
|
31
|
+
export interface SessionRefMatch {
|
|
32
|
+
record: SessionRecord;
|
|
33
|
+
/** Older records sharing the same session name (only for name matches). */
|
|
34
|
+
others: SessionRecord[];
|
|
35
|
+
}
|
|
36
|
+
export interface SessionRefAmbiguous {
|
|
37
|
+
record: null;
|
|
38
|
+
/** Every record whose conversation id starts with the ref. */
|
|
39
|
+
candidates: SessionRecord[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve `[ref]` against the ledger (newest first):
|
|
43
|
+
* - no ref → the newest record;
|
|
44
|
+
* - an exact session name → the newest record with that name, older ones in `others`;
|
|
45
|
+
* - else a conversation-id prefix → the unique match, or `{record: null, candidates}`
|
|
46
|
+
* when several share the prefix;
|
|
47
|
+
* - nothing matches (or the ledger is empty) → null.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveSessionRef(records: SessionRecord[], ref?: string): SessionRefMatch | SessionRefAmbiguous | null;
|
|
50
|
+
/** Goal alignment as the ledger recorded it: a contract at launch ⇒ on. */
|
|
51
|
+
export declare function localGoalFlag(record: SessionRecord): "on" | "off";
|
|
52
|
+
/**
|
|
53
|
+
* Render the ledger as a table, newest first — the newest
|
|
54
|
+
* `DEFAULT_SESSIONS_LIMIT` rows unless `all`, with a footer naming the rest.
|
|
55
|
+
*/
|
|
56
|
+
export declare function formatSessionsTable(rows: SessionRecord[], opts: {
|
|
57
|
+
all?: boolean;
|
|
58
|
+
}): string[];
|
|
59
|
+
/** `turns 3/10 (crossed)` — value against its ceiling (none ⇒ just the value). */
|
|
60
|
+
export declare function gauge(label: string, value: number, ceiling: number | null, fired: boolean | undefined): string;
|
|
61
|
+
/**
|
|
62
|
+
* The human view for `mnemom agent show`: the local record, then the live
|
|
63
|
+
* gateway state (or a dim "live state unavailable (<reason>)" line when
|
|
64
|
+
* `remote` is null).
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildShowView(local: SessionRecord, remote: AgentSessionResponse | null, reason?: string): string[];
|
|
67
|
+
/** The `--json` shape: the local record and the live section each tagged with its source. */
|
|
68
|
+
export interface ShowJson {
|
|
69
|
+
session: SessionRecord & {
|
|
70
|
+
source: "local";
|
|
71
|
+
};
|
|
72
|
+
goal_alignment: "on" | "off";
|
|
73
|
+
live: {
|
|
74
|
+
source: "gateway";
|
|
75
|
+
available: true;
|
|
76
|
+
sealed: AgentSessionResponse["sealed"];
|
|
77
|
+
verdict: AgentSessionResponse["verdict"];
|
|
78
|
+
turn: AgentSessionResponse["turn"];
|
|
79
|
+
guardrails: AgentSessionResponse["guardrails"];
|
|
80
|
+
} | {
|
|
81
|
+
source: "gateway";
|
|
82
|
+
available: false;
|
|
83
|
+
reason: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export declare function buildShowJson(local: SessionRecord, remote: AgentSessionResponse | null, reason?: string): ShowJson;
|
|
87
|
+
export interface AgentSessionsOptions {
|
|
88
|
+
all?: boolean;
|
|
89
|
+
json?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** `mnemom agent sessions [--all] [--json]` */
|
|
92
|
+
export declare function agentSessionsCommand(opts: AgentSessionsOptions, deps: SessionCommandDeps): Promise<void>;
|
|
93
|
+
export interface AgentShowOptions {
|
|
94
|
+
json?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/** Why the live read could not happen, from the thrown error. */
|
|
97
|
+
export declare function unavailableReason(err: unknown): string;
|
|
98
|
+
/** `mnemom agent show [ref] [--json]` */
|
|
99
|
+
export declare function agentShowCommand(ref: string | undefined, opts: AgentShowOptions, deps: SessionCommandDeps): Promise<void>;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent sessions [--all] [--json]` and `mnemom agent show [ref] [--json]`
|
|
3
|
+
* — the read side of the local session ledger (`~/.mnemom/sessions.jsonl`, one
|
|
4
|
+
* line per real launch) merged with the gateway's live goal state for one
|
|
5
|
+
* session (`GET /v1/agent/sessions/{conversationId}` via mnemom-api).
|
|
6
|
+
*
|
|
7
|
+
* Pure apart from the injected deps (`readRecords`, `fetchSession`, `out`,
|
|
8
|
+
* `err`), so every arm is unit-tested; `liveSessionDeps` is the thin driver
|
|
9
|
+
* index.ts wires in.
|
|
10
|
+
*
|
|
11
|
+
* Hash note: the CLI's launch-time envelope sha will NOT equal the gateway's
|
|
12
|
+
* sealed hash in general (the gateway normalises absent arrays to `[]`), so the
|
|
13
|
+
* local sha is never printed as "the sealed hash" and no mismatch is flagged —
|
|
14
|
+
* only the server's hash is shown, labelled "sealed hash".
|
|
15
|
+
*/
|
|
16
|
+
import { readSessionRecords } from "../lib/agent-ledger.js";
|
|
17
|
+
import { MNEMOM_DIR } from "../lib/config.js";
|
|
18
|
+
import { getAgentSession, MnemomApiError } from "../lib/api.js";
|
|
19
|
+
import { fmt, formatRelativeAge } from "../lib/format.js";
|
|
20
|
+
/** The production wiring (index.ts). Lazy so a test that mocks lib/api without
|
|
21
|
+
* this export can still import index.ts. */
|
|
22
|
+
export const liveSessionDeps = {
|
|
23
|
+
// The ledger is oldest-first on disk; every consumer here wants newest-first.
|
|
24
|
+
readRecords: () => readSessionRecords({ dir: MNEMOM_DIR }).reverse(),
|
|
25
|
+
fetchSession: (conversationId, agentId, orgId) => getAgentSession(conversationId, agentId, orgId),
|
|
26
|
+
out: (line) => console.log(line),
|
|
27
|
+
err: (line) => console.error(line),
|
|
28
|
+
};
|
|
29
|
+
export const NO_SESSIONS_MESSAGE = "No sessions yet — run `mnemom agent` to start one.";
|
|
30
|
+
export const DEFAULT_SESSIONS_LIMIT = 20;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve `[ref]` against the ledger (newest first):
|
|
33
|
+
* - no ref → the newest record;
|
|
34
|
+
* - an exact session name → the newest record with that name, older ones in `others`;
|
|
35
|
+
* - else a conversation-id prefix → the unique match, or `{record: null, candidates}`
|
|
36
|
+
* when several share the prefix;
|
|
37
|
+
* - nothing matches (or the ledger is empty) → null.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveSessionRef(records, ref) {
|
|
40
|
+
const needle = ref?.trim() ?? "";
|
|
41
|
+
if (needle === "") {
|
|
42
|
+
const newest = records[0];
|
|
43
|
+
return newest ? { record: newest, others: [] } : null;
|
|
44
|
+
}
|
|
45
|
+
const newestByName = records.find((r) => r.session === needle);
|
|
46
|
+
if (newestByName) {
|
|
47
|
+
const others = records.filter((r) => r !== newestByName && r.session === needle);
|
|
48
|
+
return { record: newestByName, others };
|
|
49
|
+
}
|
|
50
|
+
const byConv = records.filter((r) => r.conversation_id.startsWith(needle));
|
|
51
|
+
if (byConv.length === 1)
|
|
52
|
+
return { record: byConv[0], others: [] };
|
|
53
|
+
if (byConv.length > 1)
|
|
54
|
+
return { record: null, candidates: byConv };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// ─── sessions table ───────────────────────────────────────────────────────────
|
|
58
|
+
const COL = { session: 26, started: 16, agent: 22, goal: 6, conv: 8 };
|
|
59
|
+
function cell(text, width) {
|
|
60
|
+
const max = width - 2;
|
|
61
|
+
const shown = text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
62
|
+
return shown.padEnd(width);
|
|
63
|
+
}
|
|
64
|
+
/** Goal alignment as the ledger recorded it: a contract at launch ⇒ on. */
|
|
65
|
+
export function localGoalFlag(record) {
|
|
66
|
+
return record.contract ? "on" : "off";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Render the ledger as a table, newest first — the newest
|
|
70
|
+
* `DEFAULT_SESSIONS_LIMIT` rows unless `all`, with a footer naming the rest.
|
|
71
|
+
*/
|
|
72
|
+
export function formatSessionsTable(rows, opts) {
|
|
73
|
+
const shown = opts.all ? rows : rows.slice(0, DEFAULT_SESSIONS_LIMIT);
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push(" " +
|
|
76
|
+
cell("Session", COL.session) +
|
|
77
|
+
cell("Started", COL.started) +
|
|
78
|
+
cell("Agent", COL.agent) +
|
|
79
|
+
cell("Goal", COL.goal) +
|
|
80
|
+
"Conv");
|
|
81
|
+
lines.push(" " + "─".repeat(COL.session + COL.started + COL.agent + COL.goal + COL.conv));
|
|
82
|
+
for (const r of shown) {
|
|
83
|
+
lines.push(" " +
|
|
84
|
+
cell(r.session, COL.session) +
|
|
85
|
+
cell(formatRelativeAge(r.started_at), COL.started) +
|
|
86
|
+
cell(r.agent_slug, COL.agent) +
|
|
87
|
+
cell(localGoalFlag(r), COL.goal) +
|
|
88
|
+
r.conversation_id.slice(0, 8));
|
|
89
|
+
}
|
|
90
|
+
if (shown.length < rows.length) {
|
|
91
|
+
lines.push(fmt.dim(` showing ${shown.length} of ${rows.length} sessions — pass --all for every session`));
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
// ─── show view ────────────────────────────────────────────────────────────────
|
|
96
|
+
function listOrDash(items) {
|
|
97
|
+
return items && items.length > 0 ? items.join(", ") : "—";
|
|
98
|
+
}
|
|
99
|
+
function numOrDash(n) {
|
|
100
|
+
return n === null || n === undefined ? "—" : String(n);
|
|
101
|
+
}
|
|
102
|
+
/** `turns 3/10 (crossed)` — value against its ceiling (none ⇒ just the value). */
|
|
103
|
+
export function gauge(label, value, ceiling, fired) {
|
|
104
|
+
const ratio = ceiling === null ? String(value) : `${value}/${ceiling}`;
|
|
105
|
+
return `${label} ${ratio}${fired ? " (crossed)" : ""}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The human view for `mnemom agent show`: the local record, then the live
|
|
109
|
+
* gateway state (or a dim "live state unavailable (<reason>)" line when
|
|
110
|
+
* `remote` is null).
|
|
111
|
+
*/
|
|
112
|
+
export function buildShowView(local, remote, reason) {
|
|
113
|
+
const L = [];
|
|
114
|
+
L.push(fmt.header(`Session ${local.session}`));
|
|
115
|
+
L.push(fmt.label("started: ", `${local.started_at} (${formatRelativeAge(local.started_at)})`));
|
|
116
|
+
L.push(fmt.label("agent: ", `${local.agent_slug} ${local.agent_id ? `(${local.agent_id})` : fmt.dim("(id unresolved at launch)")}`));
|
|
117
|
+
L.push(fmt.label("org: ", `${local.org_label} (${local.org_id})`));
|
|
118
|
+
L.push(fmt.label("gateway: ", local.gateway_host));
|
|
119
|
+
const launchBits = [local.launch_shape, `cli ${local.cli}`];
|
|
120
|
+
if (local.model)
|
|
121
|
+
launchBits.push(`model ${local.model}`);
|
|
122
|
+
if (local.effort)
|
|
123
|
+
launchBits.push(`effort ${local.effort}`);
|
|
124
|
+
L.push(fmt.label("launch: ", launchBits.join(" · ")));
|
|
125
|
+
L.push(fmt.label("conversation: ", local.conversation_id));
|
|
126
|
+
const goalOn = remote ? remote.sealed !== null : local.contract !== null;
|
|
127
|
+
L.push(fmt.label("goal alignment:", goalOn ? fmt.badge("ON", "green") : fmt.badge("OFF", "yellow")));
|
|
128
|
+
if (local.contract) {
|
|
129
|
+
L.push(fmt.label("goal (at launch):", local.contract.statement));
|
|
130
|
+
}
|
|
131
|
+
L.push(fmt.section("Live state (gateway)"));
|
|
132
|
+
if (!remote) {
|
|
133
|
+
L.push(fmt.dim(` live state unavailable (${reason ?? "unknown"})`));
|
|
134
|
+
return L;
|
|
135
|
+
}
|
|
136
|
+
if (remote.sealed) {
|
|
137
|
+
const c = remote.sealed.contract;
|
|
138
|
+
L.push(fmt.label("statement: ", c.statement));
|
|
139
|
+
L.push(fmt.label("requirements: ", listOrDash(c.requirements)));
|
|
140
|
+
L.push(fmt.label("allowed paths: ", listOrDash(c.allowed_paths)));
|
|
141
|
+
L.push(fmt.label("forbidden paths:", listOrDash(c.forbidden_paths)));
|
|
142
|
+
L.push(fmt.label("ceilings: ", `max_turns ${numOrDash(c.max_turns)} · budget_usd ${numOrDash(c.budget_usd)} · stall_turns ${numOrDash(c.stall_turns)}`));
|
|
143
|
+
L.push(fmt.label("sealed hash: ", remote.sealed.hash));
|
|
144
|
+
L.push(fmt.label("sealed at: ", remote.sealed.sealed_at));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
L.push(fmt.dim(" no sealed contract on the gateway for this conversation"));
|
|
148
|
+
}
|
|
149
|
+
if (remote.verdict) {
|
|
150
|
+
const v = remote.verdict;
|
|
151
|
+
L.push(fmt.label("verdict: ", `${v.verdict.toUpperCase()} at turn ${v.turn} — ${v.reason}`));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
L.push(fmt.label("verdict: ", fmt.dim("none yet")));
|
|
155
|
+
}
|
|
156
|
+
if (remote.guardrails) {
|
|
157
|
+
const { ceilings, state } = remote.guardrails;
|
|
158
|
+
if (state) {
|
|
159
|
+
const parts = [
|
|
160
|
+
gauge("turns", state.turns, ceilings.max_turns, state.fired.turns),
|
|
161
|
+
gauge("spend $", state.spend_usd, ceilings.budget_usd, state.fired.budget),
|
|
162
|
+
gauge("stall", state.stall_run, ceilings.stall_turns, state.fired.stall),
|
|
163
|
+
];
|
|
164
|
+
L.push(fmt.label("guardrails: ", parts.join(" · ")));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
L.push(fmt.label("guardrails: ", `ceilings max_turns ${numOrDash(ceilings.max_turns)} · budget_usd ${numOrDash(ceilings.budget_usd)} · stall_turns ${numOrDash(ceilings.stall_turns)} ${fmt.dim("(no turns counted yet)")}`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
L.push(fmt.label("guardrails: ", fmt.dim("none")));
|
|
172
|
+
}
|
|
173
|
+
if (remote.turn !== null) {
|
|
174
|
+
L.push(fmt.label("turn: ", String(remote.turn)));
|
|
175
|
+
}
|
|
176
|
+
return L;
|
|
177
|
+
}
|
|
178
|
+
export function buildShowJson(local, remote, reason) {
|
|
179
|
+
const goalOn = remote ? remote.sealed !== null : local.contract !== null;
|
|
180
|
+
return {
|
|
181
|
+
session: { ...local, source: "local" },
|
|
182
|
+
goal_alignment: goalOn ? "on" : "off",
|
|
183
|
+
live: remote
|
|
184
|
+
? {
|
|
185
|
+
source: "gateway",
|
|
186
|
+
available: true,
|
|
187
|
+
sealed: remote.sealed,
|
|
188
|
+
verdict: remote.verdict,
|
|
189
|
+
turn: remote.turn,
|
|
190
|
+
guardrails: remote.guardrails,
|
|
191
|
+
}
|
|
192
|
+
: { source: "gateway", available: false, reason: reason ?? "unknown" },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** `mnemom agent sessions [--all] [--json]` */
|
|
196
|
+
export async function agentSessionsCommand(opts, deps) {
|
|
197
|
+
const records = deps.readRecords();
|
|
198
|
+
const rows = opts.all ? records : records.slice(0, DEFAULT_SESSIONS_LIMIT);
|
|
199
|
+
if (opts.json) {
|
|
200
|
+
deps.out(JSON.stringify(rows, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (records.length === 0) {
|
|
204
|
+
deps.out(NO_SESSIONS_MESSAGE);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
deps.out(fmt.header("Mnemom Agent sessions"));
|
|
208
|
+
deps.out("");
|
|
209
|
+
for (const line of formatSessionsTable(records, { all: opts.all }))
|
|
210
|
+
deps.out(line);
|
|
211
|
+
deps.out("");
|
|
212
|
+
}
|
|
213
|
+
/** Why the live read could not happen, from the thrown error. */
|
|
214
|
+
export function unavailableReason(err) {
|
|
215
|
+
if (err instanceof MnemomApiError && err.code)
|
|
216
|
+
return `${err.code}: ${err.message}`;
|
|
217
|
+
return err instanceof Error ? err.message : String(err);
|
|
218
|
+
}
|
|
219
|
+
/** `mnemom agent show [ref] [--json]` */
|
|
220
|
+
export async function agentShowCommand(ref, opts, deps) {
|
|
221
|
+
const records = deps.readRecords();
|
|
222
|
+
if (records.length === 0) {
|
|
223
|
+
if (opts.json)
|
|
224
|
+
deps.out(JSON.stringify(null));
|
|
225
|
+
else
|
|
226
|
+
deps.out(NO_SESSIONS_MESSAGE);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const resolved = resolveSessionRef(records, ref);
|
|
230
|
+
if (resolved === null) {
|
|
231
|
+
deps.err(fmt.error(`No session matches '${ref ?? ""}' — try \`mnemom agent sessions\`.`));
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (resolved.record === null) {
|
|
236
|
+
deps.err(fmt.error(`'${ref ?? ""}' matches ${resolved.candidates.length} sessions — give more of the conversation id:`));
|
|
237
|
+
for (const c of resolved.candidates) {
|
|
238
|
+
deps.err(` ${c.conversation_id} ${c.session} ${formatRelativeAge(c.started_at)}`);
|
|
239
|
+
}
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const { record, others } = resolved;
|
|
244
|
+
let remote = null;
|
|
245
|
+
let reason;
|
|
246
|
+
if (record.agent_id === null) {
|
|
247
|
+
reason = "agent id was not resolved at launch";
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
try {
|
|
251
|
+
remote = await deps.fetchSession(record.conversation_id, record.agent_id, record.org_id);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
reason = unavailableReason(e);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
deps.out(JSON.stringify(buildShowJson(record, remote, reason), null, 2));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (const line of buildShowView(record, remote, reason))
|
|
262
|
+
deps.out(line);
|
|
263
|
+
if (others.length > 0) {
|
|
264
|
+
deps.out("");
|
|
265
|
+
deps.out(fmt.dim(` ${others.length} older session(s) share this name — \`mnemom agent show <conv-id prefix>\` picks one:`));
|
|
266
|
+
for (const o of others) {
|
|
267
|
+
deps.out(fmt.dim(` ${o.conversation_id.slice(0, 8)} ${formatRelativeAge(o.started_at)}`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
deps.out("");
|
|
271
|
+
}
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -73,6 +73,25 @@ export interface AgentOptions {
|
|
|
73
73
|
}
|
|
74
74
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
75
75
|
export declare function agentCommand(scenarioArg: string | undefined, options?: AgentOptions): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
78
|
+
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
79
|
+
* ledger write failure warns to stderr and the launch proceeds regardless.
|
|
80
|
+
*/
|
|
81
|
+
export declare function recordSessionLaunch(ctx: {
|
|
82
|
+
session: string;
|
|
83
|
+
conversationId: string;
|
|
84
|
+
agent: string;
|
|
85
|
+
account: {
|
|
86
|
+
orgId: string;
|
|
87
|
+
orgLabel: string;
|
|
88
|
+
};
|
|
89
|
+
gatewayHost: string;
|
|
90
|
+
shape: LaunchShape;
|
|
91
|
+
options: AgentOptions;
|
|
92
|
+
cli: ResolvedCli;
|
|
93
|
+
envelope: SealedContractEnvelope | null;
|
|
94
|
+
}): Promise<void>;
|
|
76
95
|
/**
|
|
77
96
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
78
97
|
* customer adoption), in order:
|
package/dist/commands/agent.js
CHANGED
|
@@ -31,6 +31,8 @@ import { tmpdir, homedir } from "node:os";
|
|
|
31
31
|
import { basename, join, dirname } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { MNEMOM_DIR } from "../lib/config.js";
|
|
34
|
+
import { getAgentByName } from "../lib/api.js";
|
|
35
|
+
import { LEDGER_FILE, appendSessionRecord, toLedgerContract } from "../lib/agent-ledger.js";
|
|
34
36
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
35
37
|
import { fmt } from "../lib/format.js";
|
|
36
38
|
import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
@@ -160,8 +162,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
160
162
|
// there isn't one — and (2) enforce a positive µ balance on the org the agent
|
|
161
163
|
// lives in (default: the user's personal org). No µ ⇒ blocked with a
|
|
162
164
|
// portal-style top-up message. Skipped only on --dry-run (handled above).
|
|
165
|
+
let account;
|
|
163
166
|
try {
|
|
164
|
-
await ensureSessionAndBalance({ interactive: isInteractive() });
|
|
167
|
+
account = await ensureSessionAndBalance({ interactive: isInteractive() });
|
|
165
168
|
}
|
|
166
169
|
catch (err) {
|
|
167
170
|
if (isQuietPreambleExit(err))
|
|
@@ -195,6 +198,21 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
195
198
|
say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
|
|
196
199
|
}
|
|
197
200
|
}
|
|
201
|
+
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
202
|
+
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
203
|
+
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
204
|
+
// NO secret (never the key, the headers, or the child env); best-effort.
|
|
205
|
+
await recordSessionLaunch({
|
|
206
|
+
session: scenario,
|
|
207
|
+
conversationId,
|
|
208
|
+
agent,
|
|
209
|
+
account,
|
|
210
|
+
gatewayHost,
|
|
211
|
+
shape,
|
|
212
|
+
options,
|
|
213
|
+
cli,
|
|
214
|
+
envelope,
|
|
215
|
+
});
|
|
198
216
|
logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
|
|
199
217
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
200
218
|
if (shape === "terminal") {
|
|
@@ -227,6 +245,40 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
227
245
|
});
|
|
228
246
|
}
|
|
229
247
|
// ───────────────────────────────────────────────────────────────────────────
|
|
248
|
+
// Session ledger (thin driver over lib/agent-ledger.ts)
|
|
249
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
252
|
+
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
253
|
+
* ledger write failure warns to stderr and the launch proceeds regardless.
|
|
254
|
+
*/
|
|
255
|
+
export async function recordSessionLaunch(ctx) {
|
|
256
|
+
let agentId;
|
|
257
|
+
try {
|
|
258
|
+
agentId = (await getAgentByName(ctx.agent))?.id ?? null;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
agentId = null; // unresolvable right now — the ledger still records the launch
|
|
262
|
+
}
|
|
263
|
+
const result = appendSessionRecord({
|
|
264
|
+
session: ctx.session,
|
|
265
|
+
conversation_id: ctx.conversationId,
|
|
266
|
+
agent_slug: ctx.agent,
|
|
267
|
+
agent_id: agentId,
|
|
268
|
+
org_id: ctx.account.orgId,
|
|
269
|
+
org_label: ctx.account.orgLabel,
|
|
270
|
+
gateway_host: ctx.gatewayHost,
|
|
271
|
+
launch_shape: ctx.shape,
|
|
272
|
+
model: ctx.options.model?.trim() || null,
|
|
273
|
+
effort: ctx.options.effort?.trim() || null,
|
|
274
|
+
cli: ctx.cli.name,
|
|
275
|
+
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
276
|
+
}, { dir: MNEMOM_DIR, now: () => new Date(), pid: process.pid });
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
say(fmt.warn(`mnemom agent: could not record this session in ~/.mnemom/${LEDGER_FILE} (${result.error}) — launching anyway.`));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
230
282
|
// Credential resolution (zero 1Password)
|
|
231
283
|
// ───────────────────────────────────────────────────────────────────────────
|
|
232
284
|
/**
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* green — useful for shell scripts and the `mnemom/deploy` gate-guard
|
|
13
13
|
* workflow which can call the CLI rather than parse the JSON itself.
|
|
14
14
|
*/
|
|
15
|
-
import { fmt } from "../lib/format.js";
|
|
15
|
+
import { fmt, formatRelativeAge } from "../lib/format.js";
|
|
16
16
|
import { requireAuth } from "../lib/auth.js";
|
|
17
17
|
import { getSafeHouseHarnessState } from "../lib/api.js";
|
|
18
18
|
/**
|
|
@@ -32,21 +32,6 @@ function parseMaxAge(input) {
|
|
|
32
32
|
const ms = n * { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
|
|
33
33
|
return ms;
|
|
34
34
|
}
|
|
35
|
-
function formatRelativeAge(startedAt) {
|
|
36
|
-
const ageMs = Date.now() - Date.parse(startedAt);
|
|
37
|
-
if (ageMs < 0)
|
|
38
|
-
return "in the future?";
|
|
39
|
-
const m = Math.floor(ageMs / 60_000);
|
|
40
|
-
if (m < 1)
|
|
41
|
-
return "<1m ago";
|
|
42
|
-
if (m < 60)
|
|
43
|
-
return `${m}m ago`;
|
|
44
|
-
const h = Math.floor(m / 60);
|
|
45
|
-
if (h < 24)
|
|
46
|
-
return `${h}h ${m % 60}m ago`;
|
|
47
|
-
const d = Math.floor(h / 24);
|
|
48
|
-
return `${d}d ${h % 24}h ago`;
|
|
49
|
-
}
|
|
50
35
|
function evaluateLane(lane, run, maxAgeMs) {
|
|
51
36
|
if (!run) {
|
|
52
37
|
return {
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditComma
|
|
|
10
10
|
import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
|
|
11
11
|
import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
|
|
12
12
|
import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
|
|
13
|
+
import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
|
|
13
14
|
import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
|
|
14
15
|
import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
|
|
15
16
|
import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
|
|
@@ -103,7 +104,7 @@ const collectRepeatable = (value, previous) => {
|
|
|
103
104
|
const agentCmd = program
|
|
104
105
|
.command("agent [session] [passthrough...]")
|
|
105
106
|
.description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name and an optional goal. " +
|
|
106
|
-
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs.")
|
|
107
|
+
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state.")
|
|
107
108
|
.option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
|
|
108
109
|
.option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
|
|
109
110
|
.option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
|
|
@@ -698,6 +699,34 @@ agentCmd
|
|
|
698
699
|
process.exit(1);
|
|
699
700
|
}
|
|
700
701
|
});
|
|
702
|
+
agentCmd
|
|
703
|
+
.command("sessions")
|
|
704
|
+
.description("List your Mnemom Agent sessions from the local ledger (~/.mnemom/sessions.jsonl), newest first — the last 20 unless --all")
|
|
705
|
+
.option("--all", "Every session, not just the newest 20")
|
|
706
|
+
.option("--json", "Output the rows as JSON instead of a table")
|
|
707
|
+
.action(async (opts) => {
|
|
708
|
+
try {
|
|
709
|
+
await agentSessionsCommand({ all: opts.all, json: opts.json }, liveSessionDeps);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
agentCmd
|
|
717
|
+
.command("show [ref]")
|
|
718
|
+
.description("Show one session — the local launch record merged with the gateway's live goal state (sealed contract, latest verdict, guardrails). " +
|
|
719
|
+
"`ref` is a session name (newest match wins) or a unique conversation-id prefix; omitted = the newest session")
|
|
720
|
+
.option("--json", "Output the merged view as JSON (each section tagged with its source)")
|
|
721
|
+
.action(async (ref, opts) => {
|
|
722
|
+
try {
|
|
723
|
+
await agentShowCommand(ref, { json: opts.json }, liveSessionDeps);
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
701
730
|
// ============================================================================
|
|
702
731
|
// Organization management (ADR-044, Piece 1 of T1-3.1)
|
|
703
732
|
// ============================================================================
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local session ledger for `mnemom agent` — `~/.mnemom/sessions.jsonl`, one
|
|
3
|
+
* JSON line per REAL launch (never `--dry-run`), newest last.
|
|
4
|
+
*
|
|
5
|
+
* Pure module: the directory, the clock and the pid are injected so the append
|
|
6
|
+
* and the read are unit-testable with a temp dir. The record is deliberately a
|
|
7
|
+
* closed shape — the Anthropic key, the custom-headers line and the child env
|
|
8
|
+
* are never part of it (see `SessionRecord`), so nothing secret can ride along.
|
|
9
|
+
*
|
|
10
|
+
* Writes are best-effort: `appendSessionRecord` never throws — a failure comes
|
|
11
|
+
* back as `{ ok: false, error }` and the caller decides how loudly to warn.
|
|
12
|
+
*/
|
|
13
|
+
import type { LaunchShape, SealedContract } from "./agent.js";
|
|
14
|
+
/** The ledger's file name under MNEMOM_DIR. */
|
|
15
|
+
export declare const LEDGER_FILE = "sessions.jsonl";
|
|
16
|
+
/** The ledger keeps only the newest this-many launches. */
|
|
17
|
+
export declare const LEDGER_CAP = 200;
|
|
18
|
+
/** The sealed-contract fields the ledger keeps (no goal_id — the statement is the identity). */
|
|
19
|
+
export interface SessionRecordContract {
|
|
20
|
+
statement: string;
|
|
21
|
+
requirements?: string[];
|
|
22
|
+
allowed_paths?: string[];
|
|
23
|
+
forbidden_paths?: string[];
|
|
24
|
+
max_turns?: number;
|
|
25
|
+
budget_usd?: number;
|
|
26
|
+
stall_turns?: number;
|
|
27
|
+
}
|
|
28
|
+
/** One launch, as written to the ledger. */
|
|
29
|
+
export interface SessionRecord {
|
|
30
|
+
v: 1;
|
|
31
|
+
/** ISO-8601 UTC. */
|
|
32
|
+
started_at: string;
|
|
33
|
+
/** The session (scenario) name. */
|
|
34
|
+
session: string;
|
|
35
|
+
conversation_id: string;
|
|
36
|
+
/** The x-mnemom-agent slug the launch ran under. */
|
|
37
|
+
agent_slug: string;
|
|
38
|
+
/** The agent record id when it resolved at launch, else null. */
|
|
39
|
+
agent_id: string | null;
|
|
40
|
+
org_id: string;
|
|
41
|
+
org_label: string;
|
|
42
|
+
gateway_host: string;
|
|
43
|
+
launch_shape: LaunchShape;
|
|
44
|
+
model: string | null;
|
|
45
|
+
effort: string | null;
|
|
46
|
+
/** The coding-agent CLI's basename, e.g. "claude". */
|
|
47
|
+
cli: string;
|
|
48
|
+
contract: SessionRecordContract | null;
|
|
49
|
+
}
|
|
50
|
+
/** What the call site supplies — the ledger stamps `v` and `started_at`. */
|
|
51
|
+
export type SessionRecordInput = Omit<SessionRecord, "v" | "started_at">;
|
|
52
|
+
/** The injected environment of the ledger. */
|
|
53
|
+
export interface LedgerDeps {
|
|
54
|
+
/** The directory holding the ledger (MNEMOM_DIR in production). */
|
|
55
|
+
dir: string;
|
|
56
|
+
/** The clock (stamps `started_at` and names the rewrite temp file). */
|
|
57
|
+
now: () => Date;
|
|
58
|
+
/** The writing process's pid (names the rewrite temp file). */
|
|
59
|
+
pid: number;
|
|
60
|
+
}
|
|
61
|
+
export type AppendResult = {
|
|
62
|
+
ok: true;
|
|
63
|
+
record: SessionRecord;
|
|
64
|
+
path: string;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
error: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Project a sealed contract onto the ledger's contract shape (drops `goal_id`,
|
|
71
|
+
* keeps only defined fields). `null` in, `null` out.
|
|
72
|
+
*/
|
|
73
|
+
export declare function toLedgerContract(sealed: SealedContract | null): SessionRecordContract | null;
|
|
74
|
+
/** The ledger's path under `dir`. */
|
|
75
|
+
export declare function ledgerPath(dir: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Append one launch to the ledger, atomically and capped:
|
|
78
|
+
* - `dir` is created 0o700 when missing; the file is created 0o600;
|
|
79
|
+
* - the new line is appended when the ledger stays within LEDGER_CAP lines,
|
|
80
|
+
* otherwise the newest LEDGER_CAP lines (this one included) are rewritten
|
|
81
|
+
* via a temp file + rename, so a reader never sees a torn file.
|
|
82
|
+
* Never throws — a failure is returned as `{ ok: false, error }`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function appendSessionRecord(input: SessionRecordInput, deps: LedgerDeps): AppendResult;
|
|
85
|
+
/**
|
|
86
|
+
* Read the ledger, oldest first (file order). A missing file is an empty
|
|
87
|
+
* ledger; a line that is not a v1 record object is skipped, never fatal.
|
|
88
|
+
*/
|
|
89
|
+
export declare function readSessionRecords(deps: Pick<LedgerDeps, "dir">): SessionRecord[];
|
|
90
|
+
/** One ledger line → a record, or null when it is not a v1 record object. */
|
|
91
|
+
export declare function parseRecord(line: string): SessionRecord | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local session ledger for `mnemom agent` — `~/.mnemom/sessions.jsonl`, one
|
|
3
|
+
* JSON line per REAL launch (never `--dry-run`), newest last.
|
|
4
|
+
*
|
|
5
|
+
* Pure module: the directory, the clock and the pid are injected so the append
|
|
6
|
+
* and the read are unit-testable with a temp dir. The record is deliberately a
|
|
7
|
+
* closed shape — the Anthropic key, the custom-headers line and the child env
|
|
8
|
+
* are never part of it (see `SessionRecord`), so nothing secret can ride along.
|
|
9
|
+
*
|
|
10
|
+
* Writes are best-effort: `appendSessionRecord` never throws — a failure comes
|
|
11
|
+
* back as `{ ok: false, error }` and the caller decides how loudly to warn.
|
|
12
|
+
*/
|
|
13
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
/** The ledger's file name under MNEMOM_DIR. */
|
|
16
|
+
export const LEDGER_FILE = "sessions.jsonl";
|
|
17
|
+
/** The ledger keeps only the newest this-many launches. */
|
|
18
|
+
export const LEDGER_CAP = 200;
|
|
19
|
+
/**
|
|
20
|
+
* Project a sealed contract onto the ledger's contract shape (drops `goal_id`,
|
|
21
|
+
* keeps only defined fields). `null` in, `null` out.
|
|
22
|
+
*/
|
|
23
|
+
export function toLedgerContract(sealed) {
|
|
24
|
+
if (!sealed)
|
|
25
|
+
return null;
|
|
26
|
+
const out = { statement: sealed.statement };
|
|
27
|
+
if (sealed.requirements !== undefined)
|
|
28
|
+
out.requirements = sealed.requirements;
|
|
29
|
+
if (sealed.allowed_paths !== undefined)
|
|
30
|
+
out.allowed_paths = sealed.allowed_paths;
|
|
31
|
+
if (sealed.forbidden_paths !== undefined)
|
|
32
|
+
out.forbidden_paths = sealed.forbidden_paths;
|
|
33
|
+
if (sealed.max_turns !== undefined)
|
|
34
|
+
out.max_turns = sealed.max_turns;
|
|
35
|
+
if (sealed.budget_usd !== undefined)
|
|
36
|
+
out.budget_usd = sealed.budget_usd;
|
|
37
|
+
if (sealed.stall_turns !== undefined)
|
|
38
|
+
out.stall_turns = sealed.stall_turns;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** The ledger's path under `dir`. */
|
|
42
|
+
export function ledgerPath(dir) {
|
|
43
|
+
return join(dir, LEDGER_FILE);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the ledger text, or null when the file does not exist. Read-and-catch
|
|
47
|
+
* rather than exists-then-read so there is no window in which the file can
|
|
48
|
+
* change between the check and the read (CodeQL js/file-system-race).
|
|
49
|
+
*/
|
|
50
|
+
function readLedgerText(path) {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(path, "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err.code === "ENOENT")
|
|
56
|
+
return null;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Append one launch to the ledger, atomically and capped:
|
|
62
|
+
* - `dir` is created 0o700 when missing; the file is created 0o600;
|
|
63
|
+
* - the new line is appended when the ledger stays within LEDGER_CAP lines,
|
|
64
|
+
* otherwise the newest LEDGER_CAP lines (this one included) are rewritten
|
|
65
|
+
* via a temp file + rename, so a reader never sees a torn file.
|
|
66
|
+
* Never throws — a failure is returned as `{ ok: false, error }`.
|
|
67
|
+
*/
|
|
68
|
+
export function appendSessionRecord(input, deps) {
|
|
69
|
+
const path = ledgerPath(deps.dir);
|
|
70
|
+
try {
|
|
71
|
+
const record = { v: 1, started_at: deps.now().toISOString(), ...input };
|
|
72
|
+
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
74
|
+
const text = readLedgerText(path);
|
|
75
|
+
const existing = text === null ? [] : nonEmptyLines(text);
|
|
76
|
+
if (existing.length + 1 > LEDGER_CAP) {
|
|
77
|
+
const kept = [...existing, JSON.stringify(record)].slice(-LEDGER_CAP);
|
|
78
|
+
const tmp = join(deps.dir, `.${LEDGER_FILE}.tmp-${deps.pid}-${deps.now().getTime()}`);
|
|
79
|
+
writeFileSync(tmp, `${kept.join("\n")}\n`, { mode: 0o600 });
|
|
80
|
+
renameSync(tmp, path);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
appendFileSync(path, line, { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, record, path };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read the ledger, oldest first (file order). A missing file is an empty
|
|
93
|
+
* ledger; a line that is not a v1 record object is skipped, never fatal.
|
|
94
|
+
*/
|
|
95
|
+
export function readSessionRecords(deps) {
|
|
96
|
+
const path = ledgerPath(deps.dir);
|
|
97
|
+
const text = readLedgerText(path);
|
|
98
|
+
if (text === null)
|
|
99
|
+
return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const line of nonEmptyLines(text)) {
|
|
102
|
+
const parsed = parseRecord(line);
|
|
103
|
+
if (parsed)
|
|
104
|
+
out.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** One ledger line → a record, or null when it is not a v1 record object. */
|
|
109
|
+
export function parseRecord(line) {
|
|
110
|
+
let value;
|
|
111
|
+
try {
|
|
112
|
+
value = JSON.parse(line);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
118
|
+
return null;
|
|
119
|
+
const rec = value;
|
|
120
|
+
if (rec.v !== 1)
|
|
121
|
+
return null;
|
|
122
|
+
return rec;
|
|
123
|
+
}
|
|
124
|
+
function nonEmptyLines(text) {
|
|
125
|
+
return text.split("\n").filter((l) => l.trim().length > 0);
|
|
126
|
+
}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1219,6 +1219,68 @@ export declare const CODE_NOT_ENABLED = "code_not_enabled";
|
|
|
1219
1219
|
* for membership/transport failures.
|
|
1220
1220
|
*/
|
|
1221
1221
|
export declare function getCodeEntitlement(orgId?: string): Promise<CodeEntitlement>;
|
|
1222
|
+
/** The gateway's sealed contract for a conversation (absent arrays are `[]`). */
|
|
1223
|
+
export interface AgentSessionSealed {
|
|
1224
|
+
contract: {
|
|
1225
|
+
goal_id?: string;
|
|
1226
|
+
statement: string;
|
|
1227
|
+
requirements: string[];
|
|
1228
|
+
allowed_paths: string[];
|
|
1229
|
+
forbidden_paths: string[];
|
|
1230
|
+
max_turns?: number;
|
|
1231
|
+
budget_usd?: number;
|
|
1232
|
+
stall_turns?: number;
|
|
1233
|
+
};
|
|
1234
|
+
/** The gateway's hash of the sealed contract — NOT the CLI's launch-time sha. */
|
|
1235
|
+
hash: string;
|
|
1236
|
+
sealed_at: string;
|
|
1237
|
+
}
|
|
1238
|
+
/** The latest goal-judge verdict stored for the conversation. */
|
|
1239
|
+
export interface AgentSessionVerdict {
|
|
1240
|
+
turn: number;
|
|
1241
|
+
verdict: "aligned" | "drifting" | "violation";
|
|
1242
|
+
reason: string;
|
|
1243
|
+
at: string;
|
|
1244
|
+
streak_start?: number;
|
|
1245
|
+
}
|
|
1246
|
+
export interface AgentSessionGuardrails {
|
|
1247
|
+
ceilings: {
|
|
1248
|
+
/** null = inactive */
|
|
1249
|
+
max_turns: number | null;
|
|
1250
|
+
budget_usd: number | null;
|
|
1251
|
+
stall_turns: number | null;
|
|
1252
|
+
};
|
|
1253
|
+
state: {
|
|
1254
|
+
turns: number;
|
|
1255
|
+
spend_usd: number;
|
|
1256
|
+
stall_run: number;
|
|
1257
|
+
fired: {
|
|
1258
|
+
turns?: boolean;
|
|
1259
|
+
budget?: boolean;
|
|
1260
|
+
stall?: boolean;
|
|
1261
|
+
};
|
|
1262
|
+
} | null;
|
|
1263
|
+
}
|
|
1264
|
+
/** `GET /v1/agent/sessions/{conversationId}` 200 body. Nulls mean "nothing stored" — absence is data. */
|
|
1265
|
+
export interface AgentSessionResponse {
|
|
1266
|
+
conversation_id: string;
|
|
1267
|
+
agent: {
|
|
1268
|
+
id: string;
|
|
1269
|
+
name: string | null;
|
|
1270
|
+
};
|
|
1271
|
+
org_id: string;
|
|
1272
|
+
sealed: AgentSessionSealed | null;
|
|
1273
|
+
verdict: AgentSessionVerdict | null;
|
|
1274
|
+
turn: number | null;
|
|
1275
|
+
guardrails: AgentSessionGuardrails | null;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* The live goal state of one `mnemom agent` session, read through mnemom-api
|
|
1279
|
+
* (org-scoped; the api proxies the gateway's internal read). Non-OK responses
|
|
1280
|
+
* keep the server's `code` + `details` (`agent_not_found`, `gateway_unavailable`)
|
|
1281
|
+
* so `mnemom agent show` can name the reason.
|
|
1282
|
+
*/
|
|
1283
|
+
export declare function getAgentSession(conversationId: string, agentId: string, orgId: string): Promise<AgentSessionResponse>;
|
|
1222
1284
|
/** `POST /v1/invites/accept` 200 body. Never carries an amount — only whether the
|
|
1223
1285
|
* starter grant fired or was deferred for a later backfill. */
|
|
1224
1286
|
export interface AcceptProductInviteResult {
|
package/dist/lib/api.js
CHANGED
|
@@ -1538,6 +1538,17 @@ export async function getCodeEntitlement(orgId) {
|
|
|
1538
1538
|
const q = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
|
|
1539
1539
|
return codeFetch(`/v1/code/entitlement${q}`, {}, "Could not check Mnemom Agent access");
|
|
1540
1540
|
}
|
|
1541
|
+
/**
|
|
1542
|
+
* The live goal state of one `mnemom agent` session, read through mnemom-api
|
|
1543
|
+
* (org-scoped; the api proxies the gateway's internal read). Non-OK responses
|
|
1544
|
+
* keep the server's `code` + `details` (`agent_not_found`, `gateway_unavailable`)
|
|
1545
|
+
* so `mnemom agent show` can name the reason.
|
|
1546
|
+
*/
|
|
1547
|
+
export async function getAgentSession(conversationId, agentId, orgId) {
|
|
1548
|
+
// `org_id` is the org the launch resolved (the ledger record's), so an agent that
|
|
1549
|
+
// lives in a non-primary org resolves instead of 404ing against the caller's primary org.
|
|
1550
|
+
return codeFetch(`/v1/agent/sessions/${encodeURIComponent(conversationId)}?agent_id=${encodeURIComponent(agentId)}&org_id=${encodeURIComponent(orgId)}`, {}, "Could not read the session's live state");
|
|
1551
|
+
}
|
|
1541
1552
|
/** Redeem a `pinv_…` product invite (a Mnemom Agent invitation) for the signed-in user. */
|
|
1542
1553
|
export async function acceptProductInvite(token) {
|
|
1543
1554
|
return codeFetch("/v1/invites/accept", { method: "POST", body: JSON.stringify({ token }) }, "Could not redeem the invitation");
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -37,3 +37,9 @@ export declare const fmt: {
|
|
|
37
37
|
*/
|
|
38
38
|
badge(text: string, color?: BadgeColor): string;
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Human-friendly age of an ISO timestamp relative to now — "<1m ago", "42m ago",
|
|
42
|
+
* "3h 5m ago", "2d 4h ago"; a timestamp ahead of the clock reads "in the future?".
|
|
43
|
+
* Shared by `mnemom validate` and `mnemom agent sessions` / `show`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatRelativeAge(startedAt: string): string;
|
package/dist/lib/format.js
CHANGED
|
@@ -64,3 +64,23 @@ export const fmt = {
|
|
|
64
64
|
return colorFn(`[${text}]`);
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Human-friendly age of an ISO timestamp relative to now — "<1m ago", "42m ago",
|
|
69
|
+
* "3h 5m ago", "2d 4h ago"; a timestamp ahead of the clock reads "in the future?".
|
|
70
|
+
* Shared by `mnemom validate` and `mnemom agent sessions` / `show`.
|
|
71
|
+
*/
|
|
72
|
+
export function formatRelativeAge(startedAt) {
|
|
73
|
+
const ageMs = Date.now() - Date.parse(startedAt);
|
|
74
|
+
if (ageMs < 0)
|
|
75
|
+
return "in the future?";
|
|
76
|
+
const m = Math.floor(ageMs / 60_000);
|
|
77
|
+
if (m < 1)
|
|
78
|
+
return "<1m ago";
|
|
79
|
+
if (m < 60)
|
|
80
|
+
return `${m}m ago`;
|
|
81
|
+
const h = Math.floor(m / 60);
|
|
82
|
+
if (h < 24)
|
|
83
|
+
return `${h}h ${m % 60}m ago`;
|
|
84
|
+
const d = Math.floor(h / 24);
|
|
85
|
+
return `${d}d ${h % 24}h ago`;
|
|
86
|
+
}
|