@4pm/cli 1.4.20 → 1.5.0

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.
Files changed (2) hide show
  1. package/dist/index.js +258 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28315,14 +28315,42 @@ function defaultProfileConfig() {
28315
28315
  autoClearIdleMinutes: 10,
28316
28316
  // Terminate a single AI run after 5 min by default (ADR-0243) so a hung/looping AI CLI
28317
28317
  // (e.g. a heavy spec review/compose) can't spin the dispatcher forever; 0 = no limit.
28318
- aiRunTimeoutSec: 300
28318
+ aiRunTimeoutSec: 300,
28319
+ // Shared AI memory (ADR-0245) — off by default (opt-in; costs a compaction call per turn).
28320
+ aiMemoryEnabled: false,
28321
+ aiMemoryBudgetChars: 6e3
28319
28322
  };
28320
28323
  }
28324
+ function resolveMemoryConfig(config) {
28325
+ const mode = config.projectAiMemoryMode ?? "inherit";
28326
+ const enabled = mode === "on" ? true : mode === "off" ? false : config.aiMemoryEnabled ?? false;
28327
+ const projectBudget = config.projectAiMemoryBudgetChars ?? 0;
28328
+ const budgetChars = projectBudget > 0 ? projectBudget : config.aiMemoryBudgetChars ?? 6e3;
28329
+ return { enabled, budgetChars };
28330
+ }
28331
+ function resolveIdleAutoClearMinutes(config) {
28332
+ const project = config.projectAutoClearIdleMinutes ?? 0;
28333
+ return project > 0 ? project : config.autoClearIdleMinutes ?? 10;
28334
+ }
28321
28335
  function ensureProfileConfig(dir2) {
28322
28336
  if (existsSync(join(dir2, "config.json"))) return false;
28323
28337
  writeProfileConfig(dir2, defaultProfileConfig());
28324
28338
  return true;
28325
28339
  }
28340
+ function backfillProfileConfig(dir2) {
28341
+ if (!existsSync(join(dir2, "config.json"))) return false;
28342
+ const current = readProfileConfig(dir2);
28343
+ const next = { ...current };
28344
+ let changed = false;
28345
+ for (const [key, value] of Object.entries(defaultProfileConfig())) {
28346
+ if (next[key] === void 0) {
28347
+ next[key] = value;
28348
+ changed = true;
28349
+ }
28350
+ }
28351
+ if (changed) overwriteProfileConfig(dir2, next);
28352
+ return changed;
28353
+ }
28326
28354
  function readProfileConfig(dir2) {
28327
28355
  const path = join(dir2, "config.json");
28328
28356
  if (!existsSync(path)) return {};
@@ -28560,7 +28588,7 @@ async function runLink(serverUrl, explicitProfile, token = null) {
28560
28588
  }
28561
28589
 
28562
28590
  // src/commands/start.ts
28563
- import { spawn as spawn9 } from "child_process";
28591
+ import { spawn as spawn10 } from "child_process";
28564
28592
  import { join as join29 } from "path";
28565
28593
 
28566
28594
  // src/core/update.ts
@@ -33705,7 +33733,11 @@ var WsChannels = {
33705
33733
  SUPPORT_ANSWER: "support.answer",
33706
33734
  // AI knowledge distill (ADR-0190): server → project cli request–reply. The worker runs the AI CLI
33707
33735
  // in the project's working dir with a distillation prompt and replies with the composed markdown.
33708
- KNOWLEDGE_COMPOSE: "knowledge.compose"
33736
+ KNOWLEDGE_COMPOSE: "knowledge.compose",
33737
+ // Shared AI memory (ADR-0245): cli → server one-way write-back of the compacted rolling memory for
33738
+ // the project it serves (persisted per project × machine-user link). The read direction rides
33739
+ // `ws_token` (seeds the cli cache on connect); this channel only carries updates.
33740
+ MEMORY_UPDATE: "memory.update"
33709
33741
  };
33710
33742
 
33711
33743
  // vendor/@4pm/ws/crypto.ts
@@ -34084,7 +34116,9 @@ function createAiStreamParser(cli) {
34084
34116
  const isJson = isClaude || isCodex;
34085
34117
  let buffer = "";
34086
34118
  const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
34119
+ let sessionId = "";
34087
34120
  function handleClaude(ev) {
34121
+ if (typeof ev.session_id === "string" && ev.session_id) sessionId = ev.session_id;
34088
34122
  if (ev.type === "result" && ev.usage) {
34089
34123
  acc.input = ev.usage.input_tokens ?? 0;
34090
34124
  acc.output = ev.usage.output_tokens ?? 0;
@@ -34150,7 +34184,8 @@ function createAiStreamParser(cli) {
34150
34184
  const handled = handleLine(line);
34151
34185
  return handled === null ? line : handled;
34152
34186
  },
34153
- usage: () => ({ ...acc })
34187
+ usage: () => ({ ...acc }),
34188
+ sessionId: () => sessionId
34154
34189
  };
34155
34190
  }
34156
34191
  function estimateTokens(text) {
@@ -34205,14 +34240,21 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
34205
34240
  const usage = parser.usage();
34206
34241
  if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
34207
34242
  reportToolResult(attempt.cmd, true);
34208
- return { exitCode: 0, workedDir: attempt.dir, workedKey: attempt.key, workedCmd: attempt.cmd, usage };
34243
+ return {
34244
+ exitCode: 0,
34245
+ workedDir: attempt.dir,
34246
+ workedKey: attempt.key,
34247
+ workedCmd: attempt.cmd,
34248
+ usage,
34249
+ sessionId: parser.sessionId()
34250
+ };
34209
34251
  }
34210
34252
  const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : "other";
34211
34253
  if (i === total - 1) break;
34212
34254
  handlers.onAttemptFail(attempt.label, reason);
34213
34255
  }
34214
34256
  reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
34215
- return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
34257
+ return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "" };
34216
34258
  }
34217
34259
  function summarizeAiFailure(captured, exitCode) {
34218
34260
  if (isAuthFailure(captured)) return "Not logged in";
@@ -34330,11 +34372,12 @@ function folderScopeGuard(folder, prompt) {
34330
34372
  prompt
34331
34373
  ].join("\n");
34332
34374
  }
34333
- function buildRunArgs(profile, cmd, prompt) {
34375
+ function buildRunArgs(profile, cmd, prompt, resumeId) {
34334
34376
  const extras = profile.args ?? [];
34335
34377
  const model = profile.model?.trim();
34336
34378
  const modelArgs = model ? ["--model", model] : [];
34337
- return [...requiredArgs(cmd), ...extras, ...modelArgs, prompt];
34379
+ const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
34380
+ return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, prompt];
34338
34381
  }
34339
34382
  function claudeHomeDirs(config) {
34340
34383
  const dirs = profileDirs(claudeProfilesOf(config));
@@ -34373,10 +34416,10 @@ function resolveClaudeProfiles(config, workingDir) {
34373
34416
  if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
34374
34417
  return profiles;
34375
34418
  }
34376
- function planAiRun(prompt, config, hint = {}) {
34377
- return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null) : planLegacyRun(prompt, config, hint.dir ?? null);
34419
+ function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map()) {
34420
+ return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume) : planLegacyRun(prompt, config, hint.dir ?? null, resume);
34378
34421
  }
34379
- function planUnifiedRun(prompt, config, workingCredential) {
34422
+ function planUnifiedRun(prompt, config, workingCredential, resume) {
34380
34423
  const baseEnv = config.aiEnv ?? {};
34381
34424
  const active = activeProvider(config);
34382
34425
  const usable = config.aiProfiles.filter(isUsableCredential);
@@ -34403,12 +34446,12 @@ function planUnifiedRun(prompt, config, workingCredential) {
34403
34446
  label: cred.label?.trim() || profileDisplayLabel(dir2),
34404
34447
  dir: dir2,
34405
34448
  key,
34406
- args: buildRunArgs(cred, cmd, prompt),
34449
+ args: buildRunArgs(cred, cmd, prompt, resume.get(key)),
34407
34450
  env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
34408
34451
  }));
34409
34452
  return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
34410
34453
  }
34411
- function planLegacyRun(prompt, config, workingDir) {
34454
+ function planLegacyRun(prompt, config, workingDir, resume) {
34412
34455
  const cmd = config.aiCli || DEFAULT_AI_CLI;
34413
34456
  const baseEnv = config.aiEnv ?? {};
34414
34457
  const envVar = profileEnvVar(cmd);
@@ -34425,14 +34468,17 @@ function planLegacyRun(prompt, config, workingDir) {
34425
34468
  resolved.find((r) => r.dir === workingDir),
34426
34469
  ...resolved.filter((r) => r.dir !== workingDir)
34427
34470
  ] : resolved;
34428
- const attempts = ordered.map(({ profile, dir: dir2 }) => ({
34429
- cmd,
34430
- label: profileDisplayLabel(dir2),
34431
- dir: dir2,
34432
- key: credentialKey(cmd, dir2),
34433
- args: buildRunArgs(profile, cmd, prompt),
34434
- env: { ...baseEnv, [envVar]: dir2 }
34435
- }));
34471
+ const attempts = ordered.map(({ profile, dir: dir2 }) => {
34472
+ const key = credentialKey(cmd, dir2);
34473
+ return {
34474
+ cmd,
34475
+ label: profileDisplayLabel(dir2),
34476
+ dir: dir2,
34477
+ key,
34478
+ args: buildRunArgs(profile, cmd, prompt, resume.get(key)),
34479
+ env: { ...baseEnv, [envVar]: dir2 }
34480
+ };
34481
+ });
34436
34482
  return { cmd, attempts };
34437
34483
  }
34438
34484
 
@@ -36622,6 +36668,75 @@ async function addProject(payload, profileDir2, onProgress) {
36622
36668
  }
36623
36669
  }
36624
36670
 
36671
+ // src/core/memory-compact.ts
36672
+ import { spawn as spawn8 } from "child_process";
36673
+ var COMPACT_TIMEOUT_MS = 6e4;
36674
+ function buildPrompt3(input) {
36675
+ return [
36676
+ "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
36677
+ "across sessions and accounts. Merge the latest exchange into the existing memory and return the",
36678
+ `UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
36679
+ "as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
36680
+ "now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
36681
+ "",
36682
+ "=== EXISTING MEMORY ===",
36683
+ input.oldMemory || "(empty)",
36684
+ "",
36685
+ "=== LATEST USER PROMPT ===",
36686
+ input.prompt,
36687
+ "",
36688
+ "=== LATEST ASSISTANT ANSWER ===",
36689
+ input.answer
36690
+ ].join("\n");
36691
+ }
36692
+ function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
36693
+ return new Promise((resolve8, reject) => {
36694
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
36695
+ const env3 = {
36696
+ ...process.env,
36697
+ ...extraEnv,
36698
+ ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
36699
+ };
36700
+ const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
36701
+ let out = "";
36702
+ let err = "";
36703
+ const timer = setTimeout(() => {
36704
+ child.kill("SIGKILL");
36705
+ reject(new Error("memory compaction timed out"));
36706
+ }, COMPACT_TIMEOUT_MS);
36707
+ timer.unref();
36708
+ child.stdout.on("data", (d) => out += d.toString());
36709
+ child.stderr.on("data", (d) => err += d.toString());
36710
+ child.on("error", (e) => {
36711
+ clearTimeout(timer);
36712
+ reject(e);
36713
+ });
36714
+ child.on("close", (code) => {
36715
+ clearTimeout(timer);
36716
+ resolve8({ code: code ?? -1, out, err });
36717
+ });
36718
+ child.stdin.write(prompt);
36719
+ child.stdin.end();
36720
+ });
36721
+ }
36722
+ async function runMemoryCompaction(ai, cwd2, input) {
36723
+ const prompt = buildPrompt3(input);
36724
+ const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
36725
+ for (let i = 0; i < attempts.length; i++) {
36726
+ try {
36727
+ const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
36728
+ if (code === 0 && out.trim()) {
36729
+ reportToolResult(ai.cmd, true);
36730
+ return out.trim().slice(0, input.budgetChars);
36731
+ }
36732
+ if (i === attempts.length - 1) break;
36733
+ } catch {
36734
+ break;
36735
+ }
36736
+ }
36737
+ return "";
36738
+ }
36739
+
36625
36740
  // src/core/config-sync.ts
36626
36741
  import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
36627
36742
  import { join as join23 } from "path";
@@ -36631,7 +36746,14 @@ var SERVER_MANAGED_KEYS = [
36631
36746
  "perPromptTokenLimit",
36632
36747
  // Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
36633
36748
  // edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
36634
- "projectAiRunTimeoutSec"
36749
+ "projectAiRunTimeoutSec",
36750
+ // Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
36751
+ // `autoClearIdleMinutes` is operator-editable and NOT listed here.
36752
+ "projectAutoClearIdleMinutes",
36753
+ // Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
36754
+ // / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
36755
+ "projectAiMemoryMode",
36756
+ "projectAiMemoryBudgetChars"
36635
36757
  ];
36636
36758
  function readConfigText(profileDir2) {
36637
36759
  const path = join23(profileDir2, "config.json");
@@ -36664,7 +36786,7 @@ function applyConfigText(profileDir2, text) {
36664
36786
  }
36665
36787
 
36666
36788
  // src/core/worker-tools.ts
36667
- import { spawn as spawn8 } from "child_process";
36789
+ import { spawn as spawn9 } from "child_process";
36668
36790
  var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
36669
36791
  function run10(cmd, args, onLine, timeoutMs = 18e4) {
36670
36792
  return new Promise((resolve8) => {
@@ -36695,7 +36817,7 @@ function run10(cmd, args, onLine, timeoutMs = 18e4) {
36695
36817
  }
36696
36818
  };
36697
36819
  try {
36698
- child = spawn8(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
36820
+ child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
36699
36821
  child.stdout?.on("data", feed);
36700
36822
  child.stderr?.on("data", feed);
36701
36823
  child.on("exit", (code) => done(code ?? 0));
@@ -36783,6 +36905,7 @@ async function runWorkerToolOp(op, name, manager, onLine) {
36783
36905
  }
36784
36906
 
36785
36907
  // src/core/ws-client.ts
36908
+ var MEMORY_SEED_HEADER = "CONTEXT MEMORY from earlier in this conversation (may span prior sessions/accounts). Use it as background; do not repeat it back unless relevant:";
36786
36909
  var HEARTBEAT_MS = 3e4;
36787
36910
  var USAGE_POLL_MS = 5 * 6e4;
36788
36911
  var LOG_UPLOAD_MS = 10 * 6e4;
@@ -36822,6 +36945,7 @@ var WsClient = class {
36822
36945
  (entry) => this.emitConsole({ kind: "update", entry: toDtoEntry(entry) })
36823
36946
  );
36824
36947
  context.bus.onClear(() => this.emitConsoleClear());
36948
+ context.bus.onClearSession(() => this.resetMemorySession());
36825
36949
  }
36826
36950
  context;
36827
36951
  socket = null;
@@ -36847,6 +36971,13 @@ var WsClient = class {
36847
36971
  * when true the cli prepends a folder-scope guard to every AI prompt. Fed by each ws_token.
36848
36972
  */
36849
36973
  restrictToFolder = false;
36974
+ /**
36975
+ * Shared AI memory (ADR-0245) cache for the served project: the rolling compacted text (seeded by
36976
+ * `ws_token`, updated after each run) + the last native `session_id` per credential key (for
36977
+ * `--resume` on the same profile). Per (project × link) — this cli instance is one link.
36978
+ */
36979
+ aiMemory = "";
36980
+ sessionIdByKey = /* @__PURE__ */ new Map();
36850
36981
  stopped = false;
36851
36982
  /** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
36852
36983
  wakeReconnect = null;
@@ -37059,8 +37190,14 @@ var WsClient = class {
37059
37190
  sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
37060
37191
  perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0,
37061
37192
  // Project AI-run timeout override (ADR-0243) — 0 ⇒ inherit the machine-user setting.
37062
- projectAiRunTimeoutSec: token.projectTokens?.aiRunTimeoutSec ?? 0
37193
+ projectAiRunTimeoutSec: token.projectTokens?.aiRunTimeoutSec ?? 0,
37194
+ // Project idle auto-clear override (ADR-0244) — 0 ⇒ inherit the machine-user setting.
37195
+ projectAutoClearIdleMinutes: token.projectTokens?.autoClearIdleMinutes ?? 0,
37196
+ // Project shared-AI-memory override mirrors (ADR-0245).
37197
+ projectAiMemoryMode: token.aiMemory?.mode ?? "inherit",
37198
+ projectAiMemoryBudgetChars: token.aiMemory?.budgetChars ?? 0
37063
37199
  });
37200
+ if (token.aiMemory && !this.aiMemory) this.aiMemory = token.aiMemory.text;
37064
37201
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
37065
37202
  this.restrictToFolder = token.aiRestrictToFolder === true;
37066
37203
  applyGitAuth(token.gitAuth ?? null);
@@ -37735,12 +37872,22 @@ var WsClient = class {
37735
37872
  text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${labelFromCredentialKey(startCred)}"`
37736
37873
  });
37737
37874
  }
37738
- const effectivePrompt = this.restrictToFolder && this.physicRoot ? folderScopeGuard(this.physicRoot, prompt) : prompt;
37739
- const plan = planAiRun(
37740
- effectivePrompt,
37741
- config,
37742
- unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir }
37743
- );
37875
+ const guardedPrompt = this.restrictToFolder && this.physicRoot ? folderScopeGuard(this.physicRoot, prompt) : prompt;
37876
+ const hint = unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir };
37877
+ const memCfg = resolveMemoryConfig(config);
37878
+ const firstAttempt = planAiRun(guardedPrompt, config, hint).attempts[0];
37879
+ const resumeId = memCfg.enabled && firstAttempt?.key && firstAttempt.cmd === "claude" ? this.sessionIdByKey.get(firstAttempt.key) : void 0;
37880
+ let effectivePrompt = guardedPrompt;
37881
+ let resume = /* @__PURE__ */ new Map();
37882
+ if (resumeId) {
37883
+ resume = this.sessionIdByKey;
37884
+ } else if (memCfg.enabled && this.aiMemory) {
37885
+ effectivePrompt = `${MEMORY_SEED_HEADER}
37886
+ ${this.aiMemory}
37887
+
37888
+ ${guardedPrompt}`;
37889
+ }
37890
+ const plan = planAiRun(effectivePrompt, config, hint, resume);
37744
37891
  const markerArgs = plan.attempts[0]?.args ?? [];
37745
37892
  this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
37746
37893
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
@@ -37762,12 +37909,14 @@ var WsClient = class {
37762
37909
  let displayMode = "pending";
37763
37910
  let resultId = null;
37764
37911
  let resultBuf = "";
37765
- const result = await runAiFailover(plan, commandId, cwd2, {
37912
+ let answerText = "";
37913
+ const handlers = {
37766
37914
  onChunk: (text) => {
37767
37915
  if (!responseHeaderShown) {
37768
37916
  this.bus.push({ source: origin, kind: "aires", text: `${plan.cmd} \u203A` });
37769
37917
  responseHeaderShown = true;
37770
37918
  }
37919
+ answerText += text;
37771
37920
  appendCommandOutput(commandId, text);
37772
37921
  this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: text });
37773
37922
  if (displayMode === "result") {
@@ -37802,8 +37951,13 @@ var WsClient = class {
37802
37951
  this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
37803
37952
  `, log: true });
37804
37953
  }
37805
- }, aiRunTimeoutMs);
37806
- this.bus.endBusy(plan.cmd);
37954
+ };
37955
+ let result;
37956
+ try {
37957
+ result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs);
37958
+ } finally {
37959
+ this.bus.endBusy(plan.cmd);
37960
+ }
37807
37961
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
37808
37962
  if (result.workedDir) {
37809
37963
  if (unified && result.workedKey) {
@@ -37867,6 +38021,43 @@ var WsClient = class {
37867
38021
  text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
37868
38022
  level: result.exitCode === 0 ? "info" : "error"
37869
38023
  });
38024
+ if (memCfg.enabled && result.exitCode === 0) {
38025
+ if (result.workedKey && result.sessionId) {
38026
+ this.sessionIdByKey.set(result.workedKey, result.sessionId);
38027
+ }
38028
+ void this.updateSharedMemory(config, prompt, answerText, memCfg.budgetChars).catch(() => {
38029
+ });
38030
+ }
38031
+ }
38032
+ /**
38033
+ * Fold the latest exchange into the shared AI memory (ADR-0245): a background claude compaction to a
38034
+ * budget-bounded summary, cached locally + written back to the server (`memory.update`). Best-effort —
38035
+ * a failed/empty compaction keeps the previous memory (no write). Claude-only (mirrors the design's
38036
+ * native-session focus); a non-claude worker simply never compacts.
38037
+ */
38038
+ async updateSharedMemory(config, userPrompt, answer, budgetChars) {
38039
+ if (!answer.trim()) return;
38040
+ const profiles = resolveClaudeProfiles(config, this.physicRoot);
38041
+ const cwd2 = this.physicRoot ?? process.cwd();
38042
+ const newMemory = await runMemoryCompaction(
38043
+ { cmd: "claude", profiles, env: config.aiEnv },
38044
+ cwd2,
38045
+ { oldMemory: this.aiMemory, prompt: userPrompt, answer, budgetChars }
38046
+ );
38047
+ if (!newMemory) return;
38048
+ this.aiMemory = newMemory;
38049
+ this.send(WsChannels.MEMORY_UPDATE, { text: newMemory });
38050
+ }
38051
+ /**
38052
+ * Reset the shared AI memory + native sessions for a "new conversation" (ADR-0245) — a manual
38053
+ * `/clear`. Drops the cached memory + every remembered `session_id` (so the next run starts a fresh
38054
+ * native session) and clears the server-stored memory. Idle auto-clear does NOT call this
38055
+ * (display-only — ADR-0244).
38056
+ */
38057
+ resetMemorySession() {
38058
+ this.aiMemory = "";
38059
+ this.sessionIdByKey.clear();
38060
+ this.send(WsChannels.MEMORY_UPDATE, { text: "" });
37870
38061
  }
37871
38062
  /**
37872
38063
  * Pick the Claude profile to try first when under session pressure (ADR-0081). Returns
@@ -38427,6 +38618,18 @@ var SessionBus = class {
38427
38618
  this.emitter.on("clear", fn);
38428
38619
  return () => this.emitter.off("clear", fn);
38429
38620
  }
38621
+ /**
38622
+ * Signal a "new conversation" (a MANUAL `/clear`, ADR-0245) — distinct from `clear()`, which the
38623
+ * idle auto-clear (ADR-0244) also calls. Consumers reset the shared AI memory + native session.
38624
+ */
38625
+ clearSession() {
38626
+ this.emitter.emit("clearSession");
38627
+ }
38628
+ /** Subscribe to a manual `/clear` reset (shared-memory + native-session reset — ADR-0245). */
38629
+ onClearSession(fn) {
38630
+ this.emitter.on("clearSession", fn);
38631
+ return () => this.emitter.off("clearSession", fn);
38632
+ }
38430
38633
  /** The current transcript entries (a copy) — the console-sync snapshot source (ADR-0150). */
38431
38634
  snapshot() {
38432
38635
  return [...this.history];
@@ -38732,14 +38935,13 @@ function startControlServer(bus, profileDir2, info) {
38732
38935
 
38733
38936
  // src/core/idle-auto-clear.ts
38734
38937
  function startIdleAutoClear(bus, idleMinutes) {
38735
- const idleMs = Math.max(0, idleMinutes) * 6e4;
38736
- if (idleMs <= 0) return () => {
38737
- };
38738
38938
  let lastActivity = Date.now();
38739
38939
  const off = bus.onTranscript(() => {
38740
38940
  lastActivity = Date.now();
38741
38941
  });
38742
38942
  const timer = setInterval(() => {
38943
+ const idleMs = Math.max(0, idleMinutes()) * 6e4;
38944
+ if (idleMs <= 0) return;
38743
38945
  if (bus.busy !== null || bus.snapshot().length === 0) return;
38744
38946
  if (Date.now() - lastActivity < idleMs) return;
38745
38947
  bus.clear();
@@ -45131,10 +45333,6 @@ function App2({
45131
45333
  const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
45132
45334
  const [startedAt] = (0, import_react26.useState)(() => Date.now());
45133
45335
  const [pendingConfirm, setPendingConfirm] = (0, import_react26.useState)(null);
45134
- const [autoClearIdleMs] = (0, import_react26.useState)(() => {
45135
- const min = readProfileConfig(info.profileDir).autoClearIdleMinutes ?? 10;
45136
- return min > 0 ? min * 6e4 : 0;
45137
- });
45138
45336
  const lastActivityRef = (0, import_react26.useRef)(startedAt);
45139
45337
  const busyRef = (0, import_react26.useRef)(busy);
45140
45338
  busyRef.current = busy;
@@ -45199,20 +45397,21 @@ function App2({
45199
45397
  };
45200
45398
  }, [bus, flushUpdates]);
45201
45399
  (0, import_react26.useEffect)(() => {
45202
- if (autoClearIdleMs <= 0) return;
45203
45400
  const timer = setInterval(() => {
45401
+ const idleMs = resolveIdleAutoClearMinutes(readProfileConfig(info.profileDir)) * 6e4;
45402
+ if (idleMs <= 0) return;
45204
45403
  if (busyRef.current !== null || pendingConfirmRef.current || entriesLenRef.current === 0) return;
45205
- if (Date.now() - lastActivityRef.current < autoClearIdleMs) return;
45404
+ if (Date.now() - lastActivityRef.current < idleMs) return;
45206
45405
  clearTranscript();
45207
45406
  bus.push({
45208
45407
  source: "system",
45209
45408
  kind: "log",
45210
- text: `Transcript auto-cleared after ${Math.round(autoClearIdleMs / 6e4)} min idle.`
45409
+ text: `Transcript auto-cleared after ${Math.round(idleMs / 6e4)} min idle.`
45211
45410
  });
45212
45411
  lastActivityRef.current = Date.now();
45213
45412
  }, 6e4);
45214
45413
  return () => clearInterval(timer);
45215
- }, [autoClearIdleMs, bus, clearTranscript]);
45414
+ }, [info.profileDir, bus, clearTranscript]);
45216
45415
  const frameRows = Math.max(4, rows - 1);
45217
45416
  const bodyHeight = Math.max(3, frameRows - CHROME_ROWS);
45218
45417
  const textWidth = Math.max(10, cols - 1 - 8);
@@ -45255,7 +45454,12 @@ function App2({
45255
45454
  runSlashCommand(trimmed, {
45256
45455
  info,
45257
45456
  print: (text, level) => bus.push({ source: "local", kind: "log", text, level }),
45258
- clear: clearTranscript,
45457
+ // A manual `/clear` also resets the shared AI memory + native session (ADR-0245); the idle
45458
+ // auto-clear path calls clearTranscript() directly (display-only), so it does NOT reset.
45459
+ clear: () => {
45460
+ clearTranscript();
45461
+ bus.clearSession();
45462
+ },
45259
45463
  quit: () => exit(),
45260
45464
  confirm: (prompt, onYes) => {
45261
45465
  bus.push({ source: "local", kind: "log", text: prompt });
@@ -45333,6 +45537,7 @@ async function runStart(profileDir2, profileName) {
45333
45537
  });
45334
45538
  logger.info("cli.start", { serverUrl: credential.serverUrl });
45335
45539
  ensureProfileConfig(profileDir2);
45540
+ backfillProfileConfig(profileDir2);
45336
45541
  const config = readProfileConfig(profileDir2);
45337
45542
  const autoUpdate = config.autoUpdate !== false && process.env.FOURPM_NO_UPDATE !== "1";
45338
45543
  const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
@@ -45347,7 +45552,7 @@ async function runStart(profileDir2, profileName) {
45347
45552
  if (result.action === "updated") {
45348
45553
  logger.info("update.updated", { version: result.version });
45349
45554
  console.log("Restarting with the new version\u2026");
45350
- const child = spawn9(process.execPath, process.argv.slice(1), {
45555
+ const child = spawn10(process.execPath, process.argv.slice(1), {
45351
45556
  stdio: "inherit",
45352
45557
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
45353
45558
  });
@@ -45421,7 +45626,10 @@ async function runStart(profileDir2, profileName) {
45421
45626
  await tui;
45422
45627
  } else {
45423
45628
  attachConsoleSink(bus);
45424
- stopIdleClear = startIdleAutoClear(bus, config.autoClearIdleMinutes ?? 10);
45629
+ stopIdleClear = startIdleAutoClear(
45630
+ bus,
45631
+ () => resolveIdleAutoClearMinutes(readProfileConfig(profileDir2))
45632
+ );
45425
45633
  bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
45426
45634
  await runResilient();
45427
45635
  }
@@ -45564,12 +45772,12 @@ async function runAttach(profileDir2, profileName) {
45564
45772
  }
45565
45773
 
45566
45774
  // src/commands/ai-login.ts
45567
- import { spawn as spawn10 } from "child_process";
45775
+ import { spawn as spawn11 } from "child_process";
45568
45776
  import { mkdirSync as mkdirSync6 } from "fs";
45569
45777
  import { basename as basename5 } from "path";
45570
45778
  function runProviderLogin(cmd, envVar, dir2) {
45571
45779
  return new Promise((resolve8) => {
45572
- const child = spawn10(cmd, [], {
45780
+ const child = spawn11(cmd, [], {
45573
45781
  stdio: "inherit",
45574
45782
  env: { ...process.env, [envVar]: dir2 }
45575
45783
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.4.20",
3
+ "version": "1.5.0",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",