@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.
@@ -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
@@ -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
- conversationId,
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;
@@ -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
+ }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.4-next.0",
3
+ "version": "0.17.4-next.1",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {