@caupulican/pi-adaptative 0.80.4 → 0.80.6

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 (32) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/dist/core/keybindings.d.ts +1 -1
  3. package/dist/core/keybindings.d.ts.map +1 -1
  4. package/dist/core/keybindings.js +1 -1
  5. package/dist/core/keybindings.js.map +1 -1
  6. package/dist/core/reload-blockers.d.ts +36 -0
  7. package/dist/core/reload-blockers.d.ts.map +1 -0
  8. package/dist/core/reload-blockers.js +164 -0
  9. package/dist/core/reload-blockers.js.map +1 -0
  10. package/dist/core/session-manager.d.ts +1 -0
  11. package/dist/core/session-manager.d.ts.map +1 -1
  12. package/dist/core/session-manager.js +5 -1
  13. package/dist/core/session-manager.js.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/modes/interactive/interactive-mode.d.ts +38 -0
  19. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.js +496 -117
  21. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  22. package/docs/keybindings.md +1 -1
  23. package/docs/settings.md +1 -1
  24. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  25. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  26. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  27. package/examples/extensions/sandbox/package-lock.json +2 -2
  28. package/examples/extensions/sandbox/package.json +1 -1
  29. package/examples/extensions/with-deps/package-lock.json +2 -2
  30. package/examples/extensions/with-deps/package.json +1 -1
  31. package/npm-shrinkwrap.json +12 -12
  32. package/package.json +4 -4
@@ -10,6 +10,7 @@ import { getProviders, } from "@earendil-works/pi-ai";
10
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
11
11
  import chalk from "chalk";
12
12
  import { spawn, spawnSync } from "child_process";
13
+ import lockfile from "proper-lockfile";
13
14
  import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
14
15
  import { parseSkillBlock } from "../../core/agent-session.js";
15
16
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
@@ -20,8 +21,9 @@ import { createCompactionSummaryMessage } from "../../core/messages.js";
20
21
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
21
22
  import { DefaultPackageManager } from "../../core/package-manager.js";
22
23
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
24
+ import { getPendingReloadBlockers } from "../../core/reload-blockers.js";
23
25
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
24
- import { SessionManager } from "../../core/session-manager.js";
26
+ import { isAutoLearnSessionId, SessionManager } from "../../core/session-manager.js";
25
27
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
26
28
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
27
29
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
@@ -144,6 +146,154 @@ const AUTONOMY_AUTO_LEARN_PRESETS = {
144
146
  },
145
147
  };
146
148
  const AUTONOMY_MODES = ["off", "safe", "balanced", "full"];
149
+ const AUTO_LEARN_RESERVATION_MS = 2 * 60 * 1000;
150
+ export const AUTO_LEARN_HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
151
+ function definedStringSet(values) {
152
+ const set = new Set();
153
+ if (!values)
154
+ return set;
155
+ for (const value of values) {
156
+ if (typeof value === "string" && value.length > 0)
157
+ set.add(value);
158
+ }
159
+ return set;
160
+ }
161
+ function isOldAutoLearnArtifact(filePath, now, retentionMs) {
162
+ const stats = fs.lstatSync(filePath);
163
+ return stats.isFile() && now - stats.mtimeMs > retentionMs;
164
+ }
165
+ function removeOldAutoLearnArtifact(filePath, result, counter) {
166
+ try {
167
+ fs.rmSync(filePath, { force: true });
168
+ result[counter]++;
169
+ }
170
+ catch {
171
+ result.errors++;
172
+ }
173
+ }
174
+ function readAutoLearnSessionIdFromFile(filePath) {
175
+ let fd;
176
+ try {
177
+ fd = fs.openSync(filePath, "r");
178
+ const buffer = Buffer.alloc(64 * 1024);
179
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
180
+ const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n", 1)[0]?.trim();
181
+ if (!firstLine)
182
+ return undefined;
183
+ const header = JSON.parse(firstLine);
184
+ return header.type === "session" && typeof header.id === "string" ? header.id : undefined;
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ finally {
190
+ if (fd !== undefined) {
191
+ try {
192
+ fs.closeSync(fd);
193
+ }
194
+ catch {
195
+ // Ignore close errors while pruning best-effort history artifacts.
196
+ }
197
+ }
198
+ }
199
+ }
200
+ function getAutoLearnSessionIdFromFileName(fileName) {
201
+ return fileName.match(/_(auto-learn-[A-Za-z0-9._-]+)\.jsonl$/)?.[1];
202
+ }
203
+ function pruneAutoLearnSessionFiles(dir, activeSessionIds, now, retentionMs, result) {
204
+ let entries;
205
+ try {
206
+ entries = fs.readdirSync(dir, { withFileTypes: true });
207
+ }
208
+ catch {
209
+ return;
210
+ }
211
+ for (const entry of entries) {
212
+ const filePath = path.join(dir, entry.name);
213
+ if (entry.isDirectory()) {
214
+ pruneAutoLearnSessionFiles(filePath, activeSessionIds, now, retentionMs, result);
215
+ continue;
216
+ }
217
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
218
+ continue;
219
+ let shouldPrune = false;
220
+ try {
221
+ shouldPrune = isOldAutoLearnArtifact(filePath, now, retentionMs);
222
+ }
223
+ catch {
224
+ result.errors++;
225
+ continue;
226
+ }
227
+ if (!shouldPrune)
228
+ continue;
229
+ const sessionId = readAutoLearnSessionIdFromFile(filePath) ?? getAutoLearnSessionIdFromFileName(entry.name);
230
+ if (!sessionId || !isAutoLearnSessionId(sessionId) || activeSessionIds.has(sessionId))
231
+ continue;
232
+ removeOldAutoLearnArtifact(filePath, result, "sessionFiles");
233
+ }
234
+ }
235
+ export function pruneAutoLearnConversationHistory(options) {
236
+ const result = { promptFiles: 0, logFiles: 0, sessionFiles: 0, errors: 0 };
237
+ const dataDir = path.resolve(options.dataDir);
238
+ const now = options.now ?? Date.now();
239
+ const retentionMs = options.retentionMs ?? AUTO_LEARN_HISTORY_RETENTION_MS;
240
+ const activeRunIds = definedStringSet(options.activeRunIds);
241
+ const activeSessionIds = definedStringSet(options.activeSessionIds);
242
+ if (retentionMs <= 0 || !fs.existsSync(dataDir))
243
+ return result;
244
+ let entries;
245
+ try {
246
+ entries = fs.readdirSync(dataDir, { withFileTypes: true });
247
+ }
248
+ catch {
249
+ result.errors++;
250
+ return result;
251
+ }
252
+ for (const entry of entries) {
253
+ if (!entry.isFile())
254
+ continue;
255
+ const promptRunId = entry.name.endsWith(".prompt.md") ? entry.name.slice(0, -".prompt.md".length) : undefined;
256
+ const logRunId = entry.name.endsWith(".log") ? entry.name.slice(0, -".log".length) : undefined;
257
+ const runId = promptRunId ?? logRunId;
258
+ if (!runId || activeRunIds.has(runId))
259
+ continue;
260
+ const filePath = path.join(dataDir, entry.name);
261
+ let shouldPrune = false;
262
+ try {
263
+ shouldPrune = isOldAutoLearnArtifact(filePath, now, retentionMs);
264
+ }
265
+ catch {
266
+ result.errors++;
267
+ continue;
268
+ }
269
+ if (!shouldPrune)
270
+ continue;
271
+ removeOldAutoLearnArtifact(filePath, result, promptRunId ? "promptFiles" : "logFiles");
272
+ }
273
+ pruneAutoLearnSessionFiles(path.join(dataDir, "sessions"), activeSessionIds, now, retentionMs, result);
274
+ return result;
275
+ }
276
+ export function buildAutoLearnSpawnArgs(spawnTarget, options) {
277
+ return [
278
+ ...spawnTarget.argsPrefix,
279
+ "--print",
280
+ "--name",
281
+ options.name,
282
+ "--model",
283
+ options.modelPattern,
284
+ "--session-dir",
285
+ options.sessionDir,
286
+ "--session-id",
287
+ options.sessionId,
288
+ `@${options.promptPath}`,
289
+ ];
290
+ }
291
+ export function findAutoLearnSpawnNullByteInput(command, args) {
292
+ if (command.includes("\0"))
293
+ return "command";
294
+ const argIndex = args.findIndex((arg) => arg.includes("\0"));
295
+ return argIndex === -1 ? undefined : `args[${argIndex}]`;
296
+ }
147
297
  function isAnthropicSubscriptionAuthKey(apiKey) {
148
298
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
149
299
  }
@@ -2109,7 +2259,6 @@ export class InteractiveMode {
2109
2259
  try {
2110
2260
  const image = await readClipboardImage();
2111
2261
  if (!image) {
2112
- this.showStatus("No image found on the clipboard");
2113
2262
  return;
2114
2263
  }
2115
2264
  const label = this.nextClipboardImageLabel();
@@ -3472,6 +3621,14 @@ export class InteractiveMode {
3472
3621
  getAutoLearnStatePath() {
3473
3622
  return path.join(this.getAutoLearnDataDir(), "state.json");
3474
3623
  }
3624
+ ensureAutoLearnStateFile() {
3625
+ const dir = this.getAutoLearnDataDir();
3626
+ fs.mkdirSync(dir, { recursive: true });
3627
+ const statePath = this.getAutoLearnStatePath();
3628
+ if (!fs.existsSync(statePath)) {
3629
+ fs.writeFileSync(statePath, "{}\n", "utf-8");
3630
+ }
3631
+ }
3475
3632
  readAutoLearnState() {
3476
3633
  try {
3477
3634
  const statePath = this.getAutoLearnStatePath();
@@ -3488,6 +3645,52 @@ export class InteractiveMode {
3488
3645
  fs.mkdirSync(dir, { recursive: true });
3489
3646
  fs.writeFileSync(this.getAutoLearnStatePath(), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
3490
3647
  }
3648
+ acquireAutoLearnStateLock() {
3649
+ this.ensureAutoLearnStateFile();
3650
+ const statePath = this.getAutoLearnStatePath();
3651
+ const maxAttempts = 20;
3652
+ const delayMs = 25;
3653
+ let lastError;
3654
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3655
+ try {
3656
+ return lockfile.lockSync(statePath, { realpath: false, stale: 30000 });
3657
+ }
3658
+ catch (error) {
3659
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
3660
+ if (code !== "ELOCKED" || attempt === maxAttempts) {
3661
+ throw error;
3662
+ }
3663
+ lastError = error;
3664
+ const start = Date.now();
3665
+ while (Date.now() - start < delayMs) {
3666
+ // Synchronous callers need a synchronous lock retry loop.
3667
+ }
3668
+ }
3669
+ }
3670
+ throw lastError instanceof Error ? lastError : new Error("Failed to acquire Auto Learn state lock");
3671
+ }
3672
+ withAutoLearnStateLock(fn) {
3673
+ let release;
3674
+ try {
3675
+ release = this.acquireAutoLearnStateLock();
3676
+ const { result, next } = fn(this.readAutoLearnState());
3677
+ if (next !== undefined) {
3678
+ this.writeAutoLearnState(next);
3679
+ }
3680
+ return result;
3681
+ }
3682
+ finally {
3683
+ release?.();
3684
+ }
3685
+ }
3686
+ appendAutoLearnLog(logPath, message) {
3687
+ try {
3688
+ fs.appendFileSync(logPath, `${message}\n`, "utf-8");
3689
+ }
3690
+ catch {
3691
+ // Logging must never turn a background learner startup failure into an interactive crash.
3692
+ }
3693
+ }
3491
3694
  isAutoLearnPidAlive(pid) {
3492
3695
  if (typeof pid !== "number" || pid <= 0)
3493
3696
  return false;
@@ -3503,12 +3706,35 @@ export class InteractiveMode {
3503
3706
  pruneAutoLearnState(state, now = Date.now()) {
3504
3707
  const runs = { ...(state.runs ?? {}) };
3505
3708
  for (const [id, run] of Object.entries(runs)) {
3506
- if (run.expiresAt <= now || !this.isAutoLearnPidAlive(run.pid)) {
3709
+ if (run.expiresAt <= now) {
3710
+ delete runs[id];
3711
+ continue;
3712
+ }
3713
+ if (run.status === "reserved" && run.pid === undefined) {
3714
+ continue;
3715
+ }
3716
+ if (!this.isAutoLearnPidAlive(run.pid)) {
3507
3717
  delete runs[id];
3508
3718
  }
3509
3719
  }
3510
3720
  return { ...state, runs };
3511
3721
  }
3722
+ pruneAutoLearnHistoryFromState(state, now = Date.now()) {
3723
+ const prunedState = this.pruneAutoLearnState(state, now);
3724
+ pruneAutoLearnConversationHistory({
3725
+ dataDir: this.getAutoLearnDataDir(),
3726
+ now,
3727
+ activeRunIds: Object.keys(prunedState.runs ?? {}),
3728
+ activeSessionIds: Object.values(prunedState.runs ?? {}).map((run) => run.sessionId),
3729
+ });
3730
+ return prunedState;
3731
+ }
3732
+ getPrunedAutoLearnState() {
3733
+ return this.withAutoLearnStateLock((current) => {
3734
+ const state = this.pruneAutoLearnHistoryFromState(current);
3735
+ return { result: state, next: state };
3736
+ });
3737
+ }
3512
3738
  getAutoLearnPresetForAutonomyMode(mode, current = {}) {
3513
3739
  const preset = AUTONOMY_AUTO_LEARN_PRESETS[mode] ?? AUTONOMY_AUTO_LEARN_PRESETS.off;
3514
3740
  return { ...preset, model: current.model?.trim() || preset.model };
@@ -3536,64 +3762,7 @@ export class InteractiveMode {
3536
3762
  getAutoLearnMessageCount() {
3537
3763
  return this.sessionManager.getBranch().filter((entry) => entry.type === "message").length;
3538
3764
  }
3539
- resolveAutoLearnModelPattern(settings) {
3540
- if (settings.model === "active") {
3541
- return this.session.model ? `${this.session.model.provider}/${this.session.model.id}` : undefined;
3542
- }
3543
- return settings.model;
3544
- }
3545
- getAutoLearnSpawnTarget() {
3546
- const overridePath = process.env.PI_AUTO_LEARN_CLI_PATH?.trim();
3547
- if (overridePath) {
3548
- return { command: overridePath, argsPrefix: [] };
3549
- }
3550
- const execBase = path.basename(process.execPath).toLowerCase();
3551
- const isScriptRuntime = execBase === "node" || execBase === "node.exe" || execBase === "bun" || execBase === "bun.exe";
3552
- if (!isScriptRuntime) {
3553
- return { command: process.execPath, argsPrefix: [] };
3554
- }
3555
- const cliPath = process.argv[1];
3556
- if (!cliPath || cliPath.startsWith("-")) {
3557
- return undefined;
3558
- }
3559
- return { command: process.execPath, argsPrefix: [cliPath] };
3560
- }
3561
- validateAutoLearnModelValue(value) {
3562
- const modelValue = value?.trim();
3563
- if (!modelValue || modelValue === "active")
3564
- return undefined;
3565
- const available = this.session.modelRegistry.getAvailable();
3566
- if (modelValue.includes("/")) {
3567
- const [provider, modelId] = modelValue.split("/", 2);
3568
- if (available.some((model) => model.provider === provider && model.id === modelId))
3569
- return undefined;
3570
- return `Auto Learn model "${modelValue}" is not in configured subscription/API models; saved as manual/unverified.`;
3571
- }
3572
- if (available.some((model) => model.id === modelValue))
3573
- return undefined;
3574
- return `Auto Learn model "${modelValue}" is not in configured subscription/API models; saved as manual/unverified.`;
3575
- }
3576
- validateSelfModificationSource(settings) {
3577
- if (!settings.enabled)
3578
- return undefined;
3579
- const rawPath = settings.sourcePath?.trim();
3580
- if (!rawPath)
3581
- return "Self modification is enabled, but no pi-adaptative source path is set.";
3582
- const sourcePath = resolvePath(rawPath, this.sessionManager.getCwd(), { trim: true });
3583
- if (!fs.existsSync(sourcePath))
3584
- return `Self modification source path does not exist: ${sourcePath}`;
3585
- if (!fs.existsSync(path.join(sourcePath, "package.json"))) {
3586
- return `Self modification source path has no package.json: ${sourcePath}`;
3587
- }
3588
- if (!fs.existsSync(path.join(sourcePath, "packages", "coding-agent"))) {
3589
- return `Self modification source path does not look like pi-adaptative (missing packages/coding-agent): ${sourcePath}`;
3590
- }
3591
- return undefined;
3592
- }
3593
- evaluateAutoLearn(force = false) {
3594
- const settings = this.getEffectiveAutoLearnSettings();
3595
- const state = this.pruneAutoLearnState(this.readAutoLearnState());
3596
- this.writeAutoLearnState(state);
3765
+ buildAutoLearnDecisionFromState(state, settings, force = false) {
3597
3766
  const now = Date.now();
3598
3767
  const tenant = this.getAutoLearnTenantKey();
3599
3768
  const runningCount = Object.keys(state.runs ?? {}).length;
@@ -3664,6 +3833,67 @@ export class InteractiveMode {
3664
3833
  runningCount,
3665
3834
  };
3666
3835
  }
3836
+ resolveAutoLearnModelPattern(settings) {
3837
+ if (settings.model === "active") {
3838
+ return this.session.model ? `${this.session.model.provider}/${this.session.model.id}` : undefined;
3839
+ }
3840
+ return settings.model;
3841
+ }
3842
+ getAutoLearnSpawnTarget() {
3843
+ const overridePath = process.env.PI_AUTO_LEARN_CLI_PATH?.trim();
3844
+ if (overridePath) {
3845
+ return { command: overridePath, argsPrefix: [] };
3846
+ }
3847
+ const execBase = path.basename(process.execPath).toLowerCase();
3848
+ const isScriptRuntime = execBase === "node" || execBase === "node.exe" || execBase === "bun" || execBase === "bun.exe";
3849
+ if (!isScriptRuntime) {
3850
+ return { command: process.execPath, argsPrefix: [] };
3851
+ }
3852
+ const cliPath = process.argv[1];
3853
+ if (!cliPath || cliPath.startsWith("-")) {
3854
+ return undefined;
3855
+ }
3856
+ return { command: process.execPath, argsPrefix: [cliPath] };
3857
+ }
3858
+ validateAutoLearnModelValue(value) {
3859
+ const modelValue = value?.trim();
3860
+ if (!modelValue || modelValue === "active")
3861
+ return undefined;
3862
+ const available = this.session.modelRegistry.getAvailable();
3863
+ if (modelValue.includes("/")) {
3864
+ const [provider, modelId] = modelValue.split("/", 2);
3865
+ if (available.some((model) => model.provider === provider && model.id === modelId))
3866
+ return undefined;
3867
+ return `Auto Learn model "${modelValue}" is not in configured subscription/API models; saved as manual/unverified.`;
3868
+ }
3869
+ if (available.some((model) => model.id === modelValue))
3870
+ return undefined;
3871
+ return `Auto Learn model "${modelValue}" is not in configured subscription/API models; saved as manual/unverified.`;
3872
+ }
3873
+ validateSelfModificationSource(settings) {
3874
+ if (!settings.enabled)
3875
+ return undefined;
3876
+ const rawPath = settings.sourcePath?.trim();
3877
+ if (!rawPath)
3878
+ return "Self modification is enabled, but no pi-adaptative source path is set.";
3879
+ const sourcePath = resolvePath(rawPath, this.sessionManager.getCwd(), { trim: true });
3880
+ if (!fs.existsSync(sourcePath))
3881
+ return `Self modification source path does not exist: ${sourcePath}`;
3882
+ if (!fs.existsSync(path.join(sourcePath, "package.json"))) {
3883
+ return `Self modification source path has no package.json: ${sourcePath}`;
3884
+ }
3885
+ if (!fs.existsSync(path.join(sourcePath, "packages", "coding-agent"))) {
3886
+ return `Self modification source path does not look like pi-adaptative (missing packages/coding-agent): ${sourcePath}`;
3887
+ }
3888
+ return undefined;
3889
+ }
3890
+ evaluateAutoLearn(force = false) {
3891
+ const settings = this.getEffectiveAutoLearnSettings();
3892
+ return this.withAutoLearnStateLock((current) => {
3893
+ const state = this.pruneAutoLearnHistoryFromState(current);
3894
+ return { result: this.buildAutoLearnDecisionFromState(state, settings, force), next: state };
3895
+ });
3896
+ }
3667
3897
  buildAutonomyAuthorityPrompt() {
3668
3898
  const autonomy = this.settingsManager.getAutonomySettings();
3669
3899
  const selfModification = this.settingsManager.getSelfModificationSettings();
@@ -3698,12 +3928,98 @@ export class InteractiveMode {
3698
3928
  : "run one bounded continuous-learning pass for this Pi tenant";
3699
3929
  return `You are Pi Auto Learn running as a background learner.\n\nObjective: ${objective}.\nTrigger: ${reason}.\n\n${authorityBlock}\n\nRequired workflow:\n1. Query existing durable memory/rules first when tools allow it.\n2. Run the available Auto Learn tooling, preferably learning_run_auto, with applyHighConfidence=${settings.applyHighConfidence}.\n3. Treat the latest-turn digest as current-session evidence only; do not auto-commit one-off cues unless deterministic tooling corroborates them.\n4. In mode=full, apply safe memory/skill/user-extension/authorized-source improvements under the standing grant above; otherwise keep them proposal-gated.\n5. Never cross hard-stop boundaries from the authority policy.\n6. If the learning tools are unavailable, report BLOCKED with the missing tool names and do not improvise.\n7. Finish with PASS, BLOCKED, or FAIL and concise evidence.${reflectionBlock}`;
3700
3930
  }
3931
+ reserveAutoLearnRun(params) {
3932
+ return this.withAutoLearnStateLock((current) => {
3933
+ const now = Date.now();
3934
+ const state = this.pruneAutoLearnHistoryFromState(current, now);
3935
+ const tenant = this.getAutoLearnTenantKey();
3936
+ if (params.cooldownKind === "reflection") {
3937
+ const lastReflection = state.lastReflectionByTenant?.[tenant] ?? 0;
3938
+ const cooldownMs = params.settings.reflectionCooldownMinutes * 60 * 1000;
3939
+ if (Math.max(0, lastReflection + cooldownMs - now) > 0) {
3940
+ return { result: { ok: false, reason: "reflection cooldown" }, next: state };
3941
+ }
3942
+ }
3943
+ const decision = this.buildAutoLearnDecisionFromState(state, params.settings, params.force);
3944
+ if (!decision.shouldRun) {
3945
+ return { result: { ok: false, reason: decision.reason }, next: state };
3946
+ }
3947
+ const run = {
3948
+ tenant,
3949
+ model: params.modelPattern,
3950
+ reason: params.reason,
3951
+ startedAt: now,
3952
+ expiresAt: now + AUTO_LEARN_RESERVATION_MS,
3953
+ cwd: this.sessionManager.getCwd(),
3954
+ logPath: params.logPath,
3955
+ sessionDir: params.sessionDir,
3956
+ sessionId: params.sessionId,
3957
+ promptPath: params.promptPath,
3958
+ kind: params.kind,
3959
+ autonomyMode: this.settingsManager.getAutonomySettings().mode,
3960
+ authority: this.settingsManager.getAutonomySettings().mode === "full"
3961
+ ? "standing-full-autonomous"
3962
+ : "proposal-gated",
3963
+ status: "reserved",
3964
+ };
3965
+ const next = {
3966
+ ...state,
3967
+ runs: { ...(state.runs ?? {}), [params.runId]: run },
3968
+ };
3969
+ if (params.cooldownKind === "reflection") {
3970
+ next.lastReflectionByTenant = { ...(state.lastReflectionByTenant ?? {}), [tenant]: now };
3971
+ }
3972
+ else {
3973
+ next.lastLaunchByTenant = { ...(state.lastLaunchByTenant ?? {}), [tenant]: now };
3974
+ }
3975
+ return { result: { ok: true, reservation: { runId: params.runId, startedAt: now } }, next };
3976
+ });
3977
+ }
3978
+ releaseAutoLearnReservation(reservation, cooldownKind) {
3979
+ this.withAutoLearnStateLock((current) => {
3980
+ const state = this.pruneAutoLearnHistoryFromState(current);
3981
+ const tenant = this.getAutoLearnTenantKey();
3982
+ const runs = { ...(state.runs ?? {}) };
3983
+ delete runs[reservation.runId];
3984
+ const next = { ...state, runs };
3985
+ if (cooldownKind === "reflection" && next.lastReflectionByTenant?.[tenant] === reservation.startedAt) {
3986
+ next.lastReflectionByTenant = { ...next.lastReflectionByTenant };
3987
+ delete next.lastReflectionByTenant[tenant];
3988
+ }
3989
+ else if (cooldownKind !== "reflection" && next.lastLaunchByTenant?.[tenant] === reservation.startedAt) {
3990
+ next.lastLaunchByTenant = { ...next.lastLaunchByTenant };
3991
+ delete next.lastLaunchByTenant[tenant];
3992
+ }
3993
+ return { result: undefined, next };
3994
+ });
3995
+ }
3996
+ markAutoLearnReservationRunning(reservation, pid, settings) {
3997
+ this.withAutoLearnStateLock((current) => {
3998
+ const now = Date.now();
3999
+ const state = this.pruneAutoLearnHistoryFromState(current, now);
4000
+ const run = state.runs?.[reservation.runId];
4001
+ if (!run) {
4002
+ return { result: undefined, next: state };
4003
+ }
4004
+ return {
4005
+ result: undefined,
4006
+ next: {
4007
+ ...state,
4008
+ runs: {
4009
+ ...(state.runs ?? {}),
4010
+ [reservation.runId]: {
4011
+ ...run,
4012
+ pid,
4013
+ expiresAt: now + settings.leaseMinutes * 60 * 1000,
4014
+ status: "running",
4015
+ },
4016
+ },
4017
+ },
4018
+ };
4019
+ });
4020
+ }
3701
4021
  launchAutoLearn(reason, force = false, options = {}) {
3702
4022
  const settings = this.getEffectiveAutoLearnSettings();
3703
- const decision = this.evaluateAutoLearn(force);
3704
- if (!decision.shouldRun) {
3705
- return `Auto Learn not started: ${decision.reason}`;
3706
- }
3707
4023
  const modelPattern = this.resolveAutoLearnModelPattern(settings);
3708
4024
  if (!modelPattern) {
3709
4025
  return "Auto Learn not started: no active model is available for model=active.";
@@ -3717,61 +4033,104 @@ export class InteractiveMode {
3717
4033
  const runId = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
3718
4034
  const logPath = path.join(dir, `${runId}.log`);
3719
4035
  const promptPath = path.join(dir, `${runId}.prompt.md`);
3720
- const outFd = fs.openSync(logPath, "a");
3721
4036
  const kind = options.promptKind ?? "auto";
4037
+ const sessionDir = path.join(dir, "sessions");
4038
+ const sessionId = `auto-learn-${kind}-${runId}`;
4039
+ fs.mkdirSync(sessionDir, { recursive: true });
3722
4040
  const prompt = this.buildAutoLearnPrompt(reason, settings, {
3723
4041
  kind,
3724
4042
  turnDigest: options.turnDigest,
3725
4043
  });
3726
- fs.writeFileSync(promptPath, prompt, "utf-8");
3727
- const args = [
3728
- ...spawnTarget.argsPrefix,
3729
- "--print",
3730
- "--name",
3731
- `Auto Learn ${runId}`,
3732
- "--model",
4044
+ const args = buildAutoLearnSpawnArgs(spawnTarget, {
4045
+ name: `Auto Learn ${runId}`,
3733
4046
  modelPattern,
3734
- prompt,
3735
- ];
3736
- const child = spawn(spawnTarget.command, args, {
3737
- cwd: this.sessionManager.getCwd(),
3738
- detached: true,
3739
- stdio: ["ignore", outFd, outFd],
3740
- env: { ...process.env, PI_AUTO_LEARN_CHILD: "1" },
4047
+ sessionDir,
4048
+ sessionId,
4049
+ promptPath,
3741
4050
  });
3742
- child.unref();
3743
- fs.closeSync(outFd);
3744
- const now = Date.now();
3745
- const state = this.pruneAutoLearnState(this.readAutoLearnState(), now);
3746
- if (options.cooldownKind === "reflection") {
3747
- state.lastReflectionByTenant = {
3748
- ...(state.lastReflectionByTenant ?? {}),
3749
- [this.getAutoLearnTenantKey()]: now,
3750
- };
4051
+ const invalidSpawnInput = findAutoLearnSpawnNullByteInput(spawnTarget.command, args);
4052
+ if (invalidSpawnInput) {
4053
+ const message = `Auto Learn not started: ${invalidSpawnInput} contains a null byte.`;
4054
+ this.appendAutoLearnLog(logPath, message);
4055
+ this.autoLearnLastStatus = "start failed";
4056
+ this.updateAutoLearnFooter();
4057
+ return `${message} Log: ${logPath}`;
3751
4058
  }
3752
- else {
3753
- state.lastLaunchByTenant = { ...(state.lastLaunchByTenant ?? {}), [this.getAutoLearnTenantKey()]: now };
3754
- }
3755
- state.runs = {
3756
- ...(state.runs ?? {}),
3757
- [runId]: {
3758
- tenant: this.getAutoLearnTenantKey(),
3759
- pid: child.pid,
3760
- model: modelPattern,
3761
- reason,
3762
- startedAt: now,
3763
- expiresAt: now + settings.leaseMinutes * 60 * 1000,
4059
+ const reservationResult = this.reserveAutoLearnRun({
4060
+ settings,
4061
+ force,
4062
+ cooldownKind: options.cooldownKind,
4063
+ runId,
4064
+ modelPattern,
4065
+ reason,
4066
+ logPath,
4067
+ sessionDir,
4068
+ sessionId,
4069
+ promptPath,
4070
+ kind,
4071
+ });
4072
+ if (!reservationResult.ok) {
4073
+ return `Auto Learn not started: ${reservationResult.reason}`;
4074
+ }
4075
+ const { reservation } = reservationResult;
4076
+ try {
4077
+ fs.writeFileSync(promptPath, prompt, "utf-8");
4078
+ }
4079
+ catch (error) {
4080
+ const message = error instanceof Error ? error.message : String(error);
4081
+ this.releaseAutoLearnReservation(reservation, options.cooldownKind);
4082
+ this.appendAutoLearnLog(logPath, `Auto Learn failed to write prompt file: ${message}`);
4083
+ this.autoLearnLastStatus = "start failed";
4084
+ this.updateAutoLearnFooter();
4085
+ return `Auto Learn not started: failed to write prompt file (${message}). Log: ${logPath}`;
4086
+ }
4087
+ let child;
4088
+ let outFd;
4089
+ try {
4090
+ outFd = fs.openSync(logPath, "a");
4091
+ const sourceSessionFile = this.sessionManager.getSessionFile();
4092
+ child = spawn(spawnTarget.command, args, {
3764
4093
  cwd: this.sessionManager.getCwd(),
3765
- logPath,
3766
- promptPath,
3767
- kind,
3768
- autonomyMode: this.settingsManager.getAutonomySettings().mode,
3769
- authority: this.settingsManager.getAutonomySettings().mode === "full"
3770
- ? "standing-full-autonomous"
3771
- : "proposal-gated",
3772
- },
3773
- };
3774
- this.writeAutoLearnState(state);
4094
+ detached: true,
4095
+ stdio: ["ignore", outFd, outFd],
4096
+ env: {
4097
+ ...process.env,
4098
+ PI_AUTO_LEARN_CHILD: "1",
4099
+ ...(sourceSessionFile ? { PI_AUTO_LEARN_SOURCE_SESSION_FILE: sourceSessionFile } : {}),
4100
+ },
4101
+ });
4102
+ child.once("error", (error) => {
4103
+ const message = error instanceof Error ? error.message : String(error);
4104
+ this.appendAutoLearnLog(logPath, `Auto Learn failed to start: ${message}`);
4105
+ });
4106
+ }
4107
+ catch (error) {
4108
+ const message = error instanceof Error ? error.message : String(error);
4109
+ this.releaseAutoLearnReservation(reservation, options.cooldownKind);
4110
+ this.appendAutoLearnLog(logPath, `Auto Learn failed to start: ${message}`);
4111
+ this.autoLearnLastStatus = "start failed";
4112
+ this.updateAutoLearnFooter();
4113
+ return `Auto Learn not started: failed to spawn background learner (${message}). Log: ${logPath}`;
4114
+ }
4115
+ finally {
4116
+ if (outFd !== undefined) {
4117
+ try {
4118
+ fs.closeSync(outFd);
4119
+ }
4120
+ catch {
4121
+ // The child has already been spawned or startup has already failed; ignore close errors here.
4122
+ }
4123
+ }
4124
+ }
4125
+ if (!child || typeof child.pid !== "number" || child.pid <= 0) {
4126
+ this.releaseAutoLearnReservation(reservation, options.cooldownKind);
4127
+ this.autoLearnLastStatus = "start failed";
4128
+ this.updateAutoLearnFooter();
4129
+ return `Auto Learn not started: failed to spawn background learner. Log: ${logPath}`;
4130
+ }
4131
+ const childPid = child.pid;
4132
+ child.unref();
4133
+ this.markAutoLearnReservationRunning(reservation, childPid, settings);
3775
4134
  this.autoLearnLastStatus = `running ${modelPattern}`;
3776
4135
  this.updateAutoLearnFooter();
3777
4136
  return `Auto Learn started (${reason}) with ${modelPattern}. Log: ${logPath}`;
@@ -3847,8 +4206,10 @@ export class InteractiveMode {
3847
4206
  evaluateAutonomyReview(messages) {
3848
4207
  const settings = this.getEffectiveAutoLearnSettings();
3849
4208
  const autonomy = this.settingsManager.getAutonomySettings();
3850
- const state = this.pruneAutoLearnState(this.readAutoLearnState());
3851
- this.writeAutoLearnState(state);
4209
+ const state = this.withAutoLearnStateLock((current) => {
4210
+ const pruned = this.pruneAutoLearnHistoryFromState(current);
4211
+ return { result: pruned, next: pruned };
4212
+ });
3852
4213
  const now = Date.now();
3853
4214
  const tenant = this.getAutoLearnTenantKey();
3854
4215
  const runningCount = Object.keys(state.runs ?? {}).length;
@@ -3943,24 +4304,41 @@ export class InteractiveMode {
3943
4304
  formatAutoLearnStatus() {
3944
4305
  const settings = this.getEffectiveAutoLearnSettings();
3945
4306
  const decision = this.evaluateAutoLearn(false);
3946
- const state = this.pruneAutoLearnState(this.readAutoLearnState());
4307
+ const state = this.getPrunedAutoLearnState();
3947
4308
  const runs = Object.entries(state.runs ?? {});
3948
4309
  const contextText = decision.contextPercent === null ? "unknown" : `${decision.contextPercent.toFixed(1)}%`;
3949
4310
  const cooldownText = decision.cooldownRemainingMs > 0 ? `${Math.ceil(decision.cooldownRemainingMs / 60000)}m remaining` : "ready";
3950
4311
  const runLines = runs.length
3951
4312
  ? runs
3952
- .map(([id, run]) => `- ${id}: ${run.model}, kind=${run.kind ?? "auto"}, authority=${run.authority ?? "unknown"}, pid=${run.pid ?? "?"}, log=${run.logPath}`)
4313
+ .map(([id, run]) => {
4314
+ const session = [
4315
+ run.sessionId ? `session=${run.sessionId}` : "",
4316
+ run.sessionDir ? `sessionDir=${run.sessionDir}` : "",
4317
+ ]
4318
+ .filter(Boolean)
4319
+ .join(", ");
4320
+ const sessionText = session ? `, ${session}` : "";
4321
+ return `- ${id}: ${run.model}, kind=${run.kind ?? "auto"}, status=${run.status ?? "running"}, authority=${run.authority ?? "unknown"}, pid=${run.pid ?? "?"}${sessionText}, log=${run.logPath}`;
4322
+ })
3953
4323
  .join("\n")
3954
4324
  : "- none";
4325
+ const reloadBlockers = getPendingReloadBlockers({
4326
+ ownPid: process.pid,
4327
+ ownSessionId: this.sessionManager.getSessionId(),
4328
+ ownSessionFile: this.sessionManager.getSessionFile(),
4329
+ });
4330
+ const reloadBlockerLines = reloadBlockers.pending
4331
+ ? reloadBlockers.descriptions.map((description) => `- ${description}`).join("\n")
4332
+ : "- none";
3955
4333
  const reflectionLast = state.lastReflectionByTenant?.[this.getAutoLearnTenantKey()] ?? 0;
3956
4334
  const reflectionCooldownRemainingMs = Math.max(0, reflectionLast + settings.reflectionCooldownMinutes * 60 * 1000 - Date.now());
3957
4335
  const reflectionCooldownText = reflectionCooldownRemainingMs > 0 ? `${Math.ceil(reflectionCooldownRemainingMs / 60000)}m remaining` : "ready";
3958
- return `Auto Learn status\nEnabled: ${settings.enabled}\nModel: ${settings.model}\nNext decision: ${decision.shouldRun ? "ready" : decision.reason}\nMessages: ${decision.messageCount}/${settings.longSessionMessages}\nContext: ${contextText}/${settings.longSessionContextPercent}%\nCooldown: ${cooldownText}\nReflection review: ${settings.reflectionReview ? "enabled" : "disabled"} (tool trigger ${settings.reflectionMinToolCalls}, cooldown ${reflectionCooldownText})\nRunning leases: ${runs.length}/${settings.maxConcurrentLearners}\nRuns:\n${runLines}`;
4336
+ return `Auto Learn status\nEnabled: ${settings.enabled}\nModel: ${settings.model}\nNext decision: ${decision.shouldRun ? "ready" : decision.reason}\nMessages: ${decision.messageCount}/${settings.longSessionMessages}\nContext: ${contextText}/${settings.longSessionContextPercent}%\nCooldown: ${cooldownText}\nReflection review: ${settings.reflectionReview ? "enabled" : "disabled"} (tool trigger ${settings.reflectionMinToolCalls}, cooldown ${reflectionCooldownText})\nHistory retention: 7 days for internal Auto Learn prompts/logs/sessions\nRunning leases: ${runs.length}/${settings.maxConcurrentLearners}\nPi auto-reload blockers: ${reloadBlockers.pending ? reloadBlockers.reason : "none"}\n${reloadBlockerLines}\nRuns:\n${runLines}`;
3959
4337
  }
3960
4338
  formatAutonomyStatus() {
3961
4339
  const autonomy = this.settingsManager.getAutonomySettings();
3962
4340
  const settings = this.getEffectiveAutoLearnSettings();
3963
- const autoLearnState = this.pruneAutoLearnState(this.readAutoLearnState());
4341
+ const autoLearnState = this.getPrunedAutoLearnState();
3964
4342
  const running = Object.entries(autoLearnState.runs ?? {});
3965
4343
  const safety = autonomy.mode === "full"
3966
4344
  ? "standing grant for memory, skills, user/project extensions, autonomy/autoLearn tuning, and authorized selfModification.sourcePath edits; hard stops still require explicit foreground approval"
@@ -3975,6 +4353,7 @@ export class InteractiveMode {
3975
4353
  `Long-session trigger: ${settings.longSessionMessages} messages or ${settings.longSessionContextPercent}% context; cooldown=${settings.cooldownMinutes}m`,
3976
4354
  reflectionLine,
3977
4355
  `Running learners: ${running.length}/${settings.maxConcurrentLearners}`,
4356
+ "History retention: 7 days for internal Auto Learn prompts/logs/sessions",
3978
4357
  `Standing authority: ${safety}`,
3979
4358
  `Audit/log dir: ${this.getAutoLearnDataDir()}`,
3980
4359
  "Use /autonomy off|safe|balanced|full to switch presets. Advanced overrides remain in /settings → Auto Learn Advanced.",