@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.
- package/dist/index.js +258 -50
- 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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
34431
|
-
|
|
34432
|
-
|
|
34433
|
-
|
|
34434
|
-
|
|
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
|
|
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 =
|
|
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
|
|
37739
|
-
const
|
|
37740
|
-
|
|
37741
|
-
|
|
37742
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
37806
|
-
|
|
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 <
|
|
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(
|
|
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
|
-
}, [
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
45780
|
+
const child = spawn11(cmd, [], {
|
|
45573
45781
|
stdio: "inherit",
|
|
45574
45782
|
env: { ...process.env, [envVar]: dir2 }
|
|
45575
45783
|
});
|