@mnemom/mnemom 0.17.4-next.0 → 0.17.4-next.1
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.d.ts +29 -0
- package/dist/commands/agent.js +78 -4
- package/dist/lib/agent-ledger.d.ts +27 -0
- package/dist/lib/agent-ledger.js +27 -0
- package/dist/lib/agent.d.ts +9 -0
- package/dist/lib/agent.js +9 -0
- package/package.json +1 -1
package/dist/commands/agent.d.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
28
|
import { type MnemomKeyDeps } from "../lib/subscription-login.js";
|
|
29
|
+
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
29
30
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
30
31
|
import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
31
32
|
export interface AgentOptions {
|
|
@@ -103,6 +104,34 @@ export interface AgentOptions {
|
|
|
103
104
|
}
|
|
104
105
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
105
106
|
export declare function agentCommand(scenarioArg: string | undefined, options?: AgentOptions): Promise<void>;
|
|
107
|
+
/** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
|
|
108
|
+
export declare function passthroughResumeId(passthrough: readonly string[]): string | null;
|
|
109
|
+
/**
|
|
110
|
+
* The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
|
|
111
|
+
* implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
|
|
112
|
+
* - `--conversation-id` → that id, no lookup;
|
|
113
|
+
* - not implicit (a sealed contract, or alignment off) → a new id;
|
|
114
|
+
* - only an earlier implicit launch with the same session name, org, agent and gateway host
|
|
115
|
+
* is continued; any mismatch → a new id, no prompt;
|
|
116
|
+
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
117
|
+
* (else the newest matching row), reused without asking;
|
|
118
|
+
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
119
|
+
* conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
|
|
120
|
+
* resumes inside it with /resume). No TTY: Y.
|
|
121
|
+
* The ledger read is best-effort: a failure means a new id.
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveLaunchConversationId(input: {
|
|
124
|
+
session: string;
|
|
125
|
+
orgId: string;
|
|
126
|
+
agent: string;
|
|
127
|
+
gatewayHost: string;
|
|
128
|
+
explicit?: string;
|
|
129
|
+
implicit: boolean;
|
|
130
|
+
passthrough: readonly string[];
|
|
131
|
+
interactive: boolean;
|
|
132
|
+
records: () => SessionRecord[];
|
|
133
|
+
askYesNo: (question: string, defaultYes: boolean) => Promise<boolean>;
|
|
134
|
+
}): Promise<string>;
|
|
106
135
|
/**
|
|
107
136
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
108
137
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
package/dist/commands/agent.js
CHANGED
|
@@ -34,7 +34,7 @@ import { fileURLToPath } from "node:url";
|
|
|
34
34
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
35
35
|
import { createApiKey, getAgentByName } from "../lib/api.js";
|
|
36
36
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
37
|
-
import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
37
|
+
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
38
38
|
import { loadCliConfig } from "../lib/cli-config.js";
|
|
39
39
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
40
40
|
import { experimentalNotice } from "../lib/experimental.js";
|
|
@@ -42,7 +42,7 @@ import { fmt } from "../lib/format.js";
|
|
|
42
42
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
43
43
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
44
44
|
import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
45
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
45
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, GATEWAY_HINT_HEADERS_ENV, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
46
46
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
47
47
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
48
48
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
@@ -89,7 +89,6 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
89
89
|
if (!agent) {
|
|
90
90
|
throw new Error("mnemom agent: --agent sanitised to an empty string — pick a value with at least one letter or digit.");
|
|
91
91
|
}
|
|
92
|
-
const conversationId = resolveConversationId(options.conversationId);
|
|
93
92
|
// ── contract (built only if a contract flag was given; --goal then required) ──
|
|
94
93
|
const envelope = assembleContract({
|
|
95
94
|
goal: session.goal,
|
|
@@ -173,7 +172,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
173
172
|
scenario,
|
|
174
173
|
shape: previewShape,
|
|
175
174
|
agent,
|
|
176
|
-
|
|
175
|
+
// The org is only known after sign-in, so a dry run never offers a resume (IG5 #1).
|
|
176
|
+
conversationId: resolveConversationId(options.conversationId),
|
|
177
177
|
door,
|
|
178
178
|
cellNote,
|
|
179
179
|
envelope,
|
|
@@ -305,6 +305,22 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
305
305
|
say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
+
// ── conversation id ── an implicit relaunch continues its earlier conversation (IG5 #1, D1)
|
|
309
|
+
// so the goal the gateway inferred there is still the one it steers by. Resolved here, after
|
|
310
|
+
// sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
|
|
311
|
+
// match the earlier launch.
|
|
312
|
+
const conversationId = await resolveLaunchConversationId({
|
|
313
|
+
session: scenario,
|
|
314
|
+
orgId: account.orgId,
|
|
315
|
+
agent,
|
|
316
|
+
gatewayHost,
|
|
317
|
+
explicit: options.conversationId,
|
|
318
|
+
implicit: goalAlignmentMode(envelope, goalMode) === "implicit",
|
|
319
|
+
passthrough: options.passthrough ?? [],
|
|
320
|
+
interactive: isInteractive(),
|
|
321
|
+
records: () => readSessionRecords({ dir: MNEMOM_DIR }),
|
|
322
|
+
askYesNo: (q, d) => askYesNo(q, d),
|
|
323
|
+
});
|
|
308
324
|
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
309
325
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
310
326
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
@@ -409,6 +425,63 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
409
425
|
// ───────────────────────────────────────────────────────────────────────────
|
|
410
426
|
// Session ledger (thin driver over lib/agent-ledger.ts)
|
|
411
427
|
// ───────────────────────────────────────────────────────────────────────────
|
|
428
|
+
/** The Claude Code session id a passthrough `--resume <id>` / `-r <id>` / `--resume=<id>` names. */
|
|
429
|
+
export function passthroughResumeId(passthrough) {
|
|
430
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
431
|
+
const a = passthrough[i];
|
|
432
|
+
if (a === "--resume" || a === "-r") {
|
|
433
|
+
const next = passthrough[i + 1];
|
|
434
|
+
return next !== undefined && !next.startsWith("-") && next.trim() !== "" ? next.trim() : null;
|
|
435
|
+
}
|
|
436
|
+
if (a.startsWith("--resume="))
|
|
437
|
+
return a.slice("--resume=".length).trim() || null;
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The conversation id this launch runs under (IG5 #1, decision D1). The gateway keys the
|
|
443
|
+
* implicit goal by conversation id, so an implicit relaunch reuses the earlier one:
|
|
444
|
+
* - `--conversation-id` → that id, no lookup;
|
|
445
|
+
* - not implicit (a sealed contract, or alignment off) → a new id;
|
|
446
|
+
* - only an earlier implicit launch with the same session name, org, agent and gateway host
|
|
447
|
+
* is continued; any mismatch → a new id, no prompt;
|
|
448
|
+
* - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
|
|
449
|
+
* (else the newest matching row), reused without asking;
|
|
450
|
+
* - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
|
|
451
|
+
* conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
|
|
452
|
+
* resumes inside it with /resume). No TTY: Y.
|
|
453
|
+
* The ledger read is best-effort: a failure means a new id.
|
|
454
|
+
*/
|
|
455
|
+
export async function resolveLaunchConversationId(input) {
|
|
456
|
+
if ((input.explicit ?? "").trim())
|
|
457
|
+
return resolveConversationId(input.explicit);
|
|
458
|
+
if (!input.implicit)
|
|
459
|
+
return resolveConversationId(undefined);
|
|
460
|
+
let records;
|
|
461
|
+
try {
|
|
462
|
+
records = input.records();
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return resolveConversationId(undefined);
|
|
466
|
+
}
|
|
467
|
+
const found = findResumeCandidate(records, {
|
|
468
|
+
session: input.session,
|
|
469
|
+
orgId: input.orgId,
|
|
470
|
+
agent: input.agent,
|
|
471
|
+
gatewayHost: input.gatewayHost,
|
|
472
|
+
resumeId: passthroughResumeId(input.passthrough),
|
|
473
|
+
});
|
|
474
|
+
if (!found)
|
|
475
|
+
return resolveConversationId(undefined);
|
|
476
|
+
const conv = found.record.conversation_id;
|
|
477
|
+
if (found.source === "session_name" && input.interactive) {
|
|
478
|
+
const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
|
|
479
|
+
if (!yes)
|
|
480
|
+
return resolveConversationId(undefined);
|
|
481
|
+
}
|
|
482
|
+
say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
|
|
483
|
+
return resolveConversationId(conv);
|
|
484
|
+
}
|
|
412
485
|
/**
|
|
413
486
|
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
414
487
|
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
@@ -738,6 +811,7 @@ function launchTerminal(ctx) {
|
|
|
738
811
|
}
|
|
739
812
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
740
813
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
814
|
+
childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
|
|
741
815
|
if (ctx.options.slack)
|
|
742
816
|
args.push(...slackChannelArgs(sessionName));
|
|
743
817
|
if (ctx.contextHint) {
|
|
@@ -133,3 +133,30 @@ export declare function appendSessionRecord(input: SessionRecordInput, deps: Led
|
|
|
133
133
|
export declare function readSessionRecords(deps: Pick<LedgerDeps, "dir">): SessionRecord[];
|
|
134
134
|
/** One ledger line → a record, or null when it is not a v1 record object. */
|
|
135
135
|
export declare function parseRecord(line: string): SessionRecord | null;
|
|
136
|
+
/**
|
|
137
|
+
* Which earlier launch a new one continues (IG5 #1, decision D1). The gateway keys the implicit
|
|
138
|
+
* goal by conversation id, so a relaunch that mints a fresh id starts goal-less. A row
|
|
139
|
+
* qualifies only when it is an implicit-mode launch (a sealed contract is sealed on its own
|
|
140
|
+
* conversation and is never carried) with the same session name, org, agent and gateway host as
|
|
141
|
+
* this launch; an empty org never matches. `records` is file order (oldest first); the newest
|
|
142
|
+
* qualifying row wins.
|
|
143
|
+
* - `resumeId` (the `--resume <cc-session-id>` passed through to Claude Code) → the qualifying
|
|
144
|
+
* row that pinned that Claude Code session, else the newest qualifying row; `source:
|
|
145
|
+
* "resume_flag"` — reused without asking.
|
|
146
|
+
* - no `resumeId` → the newest qualifying row; `source: "session_name"` — the launcher asks
|
|
147
|
+
* first.
|
|
148
|
+
* - nothing qualifies → null (a new conversation id).
|
|
149
|
+
*/
|
|
150
|
+
export interface ResumeCandidate {
|
|
151
|
+
record: SessionRecord;
|
|
152
|
+
source: "resume_flag" | "session_name";
|
|
153
|
+
/** True when the row was found by its Claude Code session id (not by the name fallback). */
|
|
154
|
+
byCcSession: boolean;
|
|
155
|
+
}
|
|
156
|
+
export declare function findResumeCandidate(records: readonly SessionRecord[], want: {
|
|
157
|
+
session: string;
|
|
158
|
+
orgId: string;
|
|
159
|
+
agent: string;
|
|
160
|
+
gatewayHost: string;
|
|
161
|
+
resumeId?: string | null;
|
|
162
|
+
}): ResumeCandidate | null;
|
package/dist/lib/agent-ledger.js
CHANGED
|
@@ -170,3 +170,30 @@ export function parseRecord(line) {
|
|
|
170
170
|
function nonEmptyLines(text) {
|
|
171
171
|
return text.split("\n").filter((l) => l.trim().length > 0);
|
|
172
172
|
}
|
|
173
|
+
export function findResumeCandidate(records, want) {
|
|
174
|
+
const implicitRows = records.filter((r) => r.goal_mode === "implicit" &&
|
|
175
|
+
typeof r.conversation_id === "string" &&
|
|
176
|
+
r.conversation_id &&
|
|
177
|
+
r.session === want.session &&
|
|
178
|
+
!!want.orgId &&
|
|
179
|
+
r.org_id === want.orgId &&
|
|
180
|
+
r.agent_slug === want.agent &&
|
|
181
|
+
r.gateway_host === want.gatewayHost);
|
|
182
|
+
const newest = (pred) => {
|
|
183
|
+
for (let i = implicitRows.length - 1; i >= 0; i--) {
|
|
184
|
+
const r = implicitRows[i];
|
|
185
|
+
if (pred(r))
|
|
186
|
+
return r;
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
};
|
|
190
|
+
const latest = newest(() => true);
|
|
191
|
+
const resumeId = want.resumeId?.trim();
|
|
192
|
+
if (resumeId) {
|
|
193
|
+
const byCc = newest((r) => r.cc_session_id === resumeId);
|
|
194
|
+
if (byCc)
|
|
195
|
+
return { record: byCc, source: "resume_flag", byCcSession: true };
|
|
196
|
+
return latest ? { record: latest, source: "resume_flag", byCcSession: false } : null;
|
|
197
|
+
}
|
|
198
|
+
return latest ? { record: latest, source: "session_name", byCcSession: false } : null;
|
|
199
|
+
}
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -116,6 +116,15 @@ export declare function sanitizeAgentName(raw: string): string;
|
|
|
116
116
|
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
117
117
|
*/
|
|
118
118
|
export declare const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
119
|
+
/**
|
|
120
|
+
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
|
|
121
|
+
* `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
|
|
122
|
+
* its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
|
|
123
|
+
* them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
|
|
124
|
+
* only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
|
|
125
|
+
* teammates off the main implicit goal.
|
|
126
|
+
*/
|
|
127
|
+
export declare const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
119
128
|
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
120
129
|
export declare function sessionDisplayName(agent: string, scenario: string): string;
|
|
121
130
|
/** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
|
package/dist/lib/agent.js
CHANGED
|
@@ -117,6 +117,15 @@ export function sanitizeAgentName(raw) {
|
|
|
117
117
|
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
118
118
|
*/
|
|
119
119
|
export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
120
|
+
/**
|
|
121
|
+
* Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
|
|
122
|
+
* `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
|
|
123
|
+
* its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
|
|
124
|
+
* them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
|
|
125
|
+
* only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
|
|
126
|
+
* teammates off the main implicit goal.
|
|
127
|
+
*/
|
|
128
|
+
export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
|
|
120
129
|
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
121
130
|
export function sessionDisplayName(agent, scenario) {
|
|
122
131
|
return sanitizeAgentName(`${agent}-${scenario}`) || agent;
|