@caupulican/pi-adaptative 0.80.5 → 0.80.7

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 (36) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +14 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/keybindings.d.ts +1 -1
  7. package/dist/core/keybindings.d.ts.map +1 -1
  8. package/dist/core/keybindings.js +1 -1
  9. package/dist/core/keybindings.js.map +1 -1
  10. package/dist/core/model-resolver.d.ts +7 -1
  11. package/dist/core/model-resolver.d.ts.map +1 -1
  12. package/dist/core/model-resolver.js +85 -16
  13. package/dist/core/model-resolver.js.map +1 -1
  14. package/dist/core/session-manager.d.ts +1 -0
  15. package/dist/core/session-manager.d.ts.map +1 -1
  16. package/dist/core/session-manager.js +5 -1
  17. package/dist/core/session-manager.js.map +1 -1
  18. package/dist/main.d.ts.map +1 -1
  19. package/dist/main.js +14 -8
  20. package/dist/main.js.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.d.ts +40 -0
  22. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  23. package/dist/modes/interactive/interactive-mode.js +539 -136
  24. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  25. package/docs/keybindings.md +1 -1
  26. package/docs/providers.md +27 -2
  27. package/docs/settings.md +1 -1
  28. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  29. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  30. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  31. package/examples/extensions/sandbox/package-lock.json +2 -2
  32. package/examples/extensions/sandbox/package.json +1 -1
  33. package/examples/extensions/with-deps/package-lock.json +2 -2
  34. package/examples/extensions/with-deps/package.json +1 -1
  35. package/npm-shrinkwrap.json +12 -12
  36. 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";
@@ -17,12 +18,12 @@ import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
18
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
18
19
  import { KeybindingsManager } from "../../core/keybindings.js";
19
20
  import { createCompactionSummaryMessage } from "../../core/messages.js";
20
- import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
21
+ import { cliProviderAliases, 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";
23
24
  import { getPendingReloadBlockers } from "../../core/reload-blockers.js";
24
25
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
25
- import { SessionManager } from "../../core/session-manager.js";
26
+ import { isAutoLearnSessionId, SessionManager } from "../../core/session-manager.js";
26
27
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
27
28
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
28
29
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
@@ -145,6 +146,154 @@ const AUTONOMY_AUTO_LEARN_PRESETS = {
145
146
  },
146
147
  };
147
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
+ }
148
297
  function isAnthropicSubscriptionAuthKey(apiKey) {
149
298
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
150
299
  }
@@ -2110,7 +2259,6 @@ export class InteractiveMode {
2110
2259
  try {
2111
2260
  const image = await readClipboardImage();
2112
2261
  if (!image) {
2113
- this.showStatus("No image found on the clipboard");
2114
2262
  return;
2115
2263
  }
2116
2264
  const label = this.nextClipboardImageLabel();
@@ -2241,13 +2389,13 @@ export class InteractiveMode {
2241
2389
  this.editor.setText("");
2242
2390
  return;
2243
2391
  }
2244
- if (text === "/login") {
2245
- this.showOAuthSelector("login");
2392
+ if (text === "/login" || text.startsWith("/login ")) {
2393
+ await this.showOAuthSelector("login", text.slice("/login".length).trim() || undefined);
2246
2394
  this.editor.setText("");
2247
2395
  return;
2248
2396
  }
2249
- if (text === "/logout") {
2250
- this.showOAuthSelector("logout");
2397
+ if (text === "/logout" || text.startsWith("/logout ")) {
2398
+ await this.showOAuthSelector("logout", text.slice("/logout".length).trim() || undefined);
2251
2399
  this.editor.setText("");
2252
2400
  return;
2253
2401
  }
@@ -3473,6 +3621,14 @@ export class InteractiveMode {
3473
3621
  getAutoLearnStatePath() {
3474
3622
  return path.join(this.getAutoLearnDataDir(), "state.json");
3475
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
+ }
3476
3632
  readAutoLearnState() {
3477
3633
  try {
3478
3634
  const statePath = this.getAutoLearnStatePath();
@@ -3489,6 +3645,52 @@ export class InteractiveMode {
3489
3645
  fs.mkdirSync(dir, { recursive: true });
3490
3646
  fs.writeFileSync(this.getAutoLearnStatePath(), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
3491
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
+ }
3492
3694
  isAutoLearnPidAlive(pid) {
3493
3695
  if (typeof pid !== "number" || pid <= 0)
3494
3696
  return false;
@@ -3504,12 +3706,35 @@ export class InteractiveMode {
3504
3706
  pruneAutoLearnState(state, now = Date.now()) {
3505
3707
  const runs = { ...(state.runs ?? {}) };
3506
3708
  for (const [id, run] of Object.entries(runs)) {
3507
- 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)) {
3508
3717
  delete runs[id];
3509
3718
  }
3510
3719
  }
3511
3720
  return { ...state, runs };
3512
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
+ }
3513
3738
  getAutoLearnPresetForAutonomyMode(mode, current = {}) {
3514
3739
  const preset = AUTONOMY_AUTO_LEARN_PRESETS[mode] ?? AUTONOMY_AUTO_LEARN_PRESETS.off;
3515
3740
  return { ...preset, model: current.model?.trim() || preset.model };
@@ -3537,64 +3762,7 @@ export class InteractiveMode {
3537
3762
  getAutoLearnMessageCount() {
3538
3763
  return this.sessionManager.getBranch().filter((entry) => entry.type === "message").length;
3539
3764
  }
3540
- resolveAutoLearnModelPattern(settings) {
3541
- if (settings.model === "active") {
3542
- return this.session.model ? `${this.session.model.provider}/${this.session.model.id}` : undefined;
3543
- }
3544
- return settings.model;
3545
- }
3546
- getAutoLearnSpawnTarget() {
3547
- const overridePath = process.env.PI_AUTO_LEARN_CLI_PATH?.trim();
3548
- if (overridePath) {
3549
- return { command: overridePath, argsPrefix: [] };
3550
- }
3551
- const execBase = path.basename(process.execPath).toLowerCase();
3552
- const isScriptRuntime = execBase === "node" || execBase === "node.exe" || execBase === "bun" || execBase === "bun.exe";
3553
- if (!isScriptRuntime) {
3554
- return { command: process.execPath, argsPrefix: [] };
3555
- }
3556
- const cliPath = process.argv[1];
3557
- if (!cliPath || cliPath.startsWith("-")) {
3558
- return undefined;
3559
- }
3560
- return { command: process.execPath, argsPrefix: [cliPath] };
3561
- }
3562
- validateAutoLearnModelValue(value) {
3563
- const modelValue = value?.trim();
3564
- if (!modelValue || modelValue === "active")
3565
- return undefined;
3566
- const available = this.session.modelRegistry.getAvailable();
3567
- if (modelValue.includes("/")) {
3568
- const [provider, modelId] = modelValue.split("/", 2);
3569
- if (available.some((model) => model.provider === provider && model.id === modelId))
3570
- return undefined;
3571
- return `Auto Learn model "${modelValue}" is not in configured subscription/API models; saved as manual/unverified.`;
3572
- }
3573
- if (available.some((model) => model.id === modelValue))
3574
- return undefined;
3575
- return `Auto Learn model "${modelValue}" is not in configured subscription/API models; saved as manual/unverified.`;
3576
- }
3577
- validateSelfModificationSource(settings) {
3578
- if (!settings.enabled)
3579
- return undefined;
3580
- const rawPath = settings.sourcePath?.trim();
3581
- if (!rawPath)
3582
- return "Self modification is enabled, but no pi-adaptative source path is set.";
3583
- const sourcePath = resolvePath(rawPath, this.sessionManager.getCwd(), { trim: true });
3584
- if (!fs.existsSync(sourcePath))
3585
- return `Self modification source path does not exist: ${sourcePath}`;
3586
- if (!fs.existsSync(path.join(sourcePath, "package.json"))) {
3587
- return `Self modification source path has no package.json: ${sourcePath}`;
3588
- }
3589
- if (!fs.existsSync(path.join(sourcePath, "packages", "coding-agent"))) {
3590
- return `Self modification source path does not look like pi-adaptative (missing packages/coding-agent): ${sourcePath}`;
3591
- }
3592
- return undefined;
3593
- }
3594
- evaluateAutoLearn(force = false) {
3595
- const settings = this.getEffectiveAutoLearnSettings();
3596
- const state = this.pruneAutoLearnState(this.readAutoLearnState());
3597
- this.writeAutoLearnState(state);
3765
+ buildAutoLearnDecisionFromState(state, settings, force = false) {
3598
3766
  const now = Date.now();
3599
3767
  const tenant = this.getAutoLearnTenantKey();
3600
3768
  const runningCount = Object.keys(state.runs ?? {}).length;
@@ -3665,6 +3833,67 @@ export class InteractiveMode {
3665
3833
  runningCount,
3666
3834
  };
3667
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
+ }
3668
3897
  buildAutonomyAuthorityPrompt() {
3669
3898
  const autonomy = this.settingsManager.getAutonomySettings();
3670
3899
  const selfModification = this.settingsManager.getSelfModificationSettings();
@@ -3699,12 +3928,98 @@ export class InteractiveMode {
3699
3928
  : "run one bounded continuous-learning pass for this Pi tenant";
3700
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}`;
3701
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
+ }
3702
4021
  launchAutoLearn(reason, force = false, options = {}) {
3703
4022
  const settings = this.getEffectiveAutoLearnSettings();
3704
- const decision = this.evaluateAutoLearn(force);
3705
- if (!decision.shouldRun) {
3706
- return `Auto Learn not started: ${decision.reason}`;
3707
- }
3708
4023
  const modelPattern = this.resolveAutoLearnModelPattern(settings);
3709
4024
  if (!modelPattern) {
3710
4025
  return "Auto Learn not started: no active model is available for model=active.";
@@ -3718,7 +4033,6 @@ export class InteractiveMode {
3718
4033
  const runId = `${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
3719
4034
  const logPath = path.join(dir, `${runId}.log`);
3720
4035
  const promptPath = path.join(dir, `${runId}.prompt.md`);
3721
- const outFd = fs.openSync(logPath, "a");
3722
4036
  const kind = options.promptKind ?? "auto";
3723
4037
  const sessionDir = path.join(dir, "sessions");
3724
4038
  const sessionId = `auto-learn-${kind}-${runId}`;
@@ -3727,61 +4041,96 @@ export class InteractiveMode {
3727
4041
  kind,
3728
4042
  turnDigest: options.turnDigest,
3729
4043
  });
3730
- fs.writeFileSync(promptPath, prompt, "utf-8");
3731
- const args = [
3732
- ...spawnTarget.argsPrefix,
3733
- "--print",
3734
- "--name",
3735
- `Auto Learn ${runId}`,
3736
- "--model",
4044
+ const args = buildAutoLearnSpawnArgs(spawnTarget, {
4045
+ name: `Auto Learn ${runId}`,
3737
4046
  modelPattern,
3738
- "--session-dir",
3739
4047
  sessionDir,
3740
- "--session-id",
3741
4048
  sessionId,
3742
- prompt,
3743
- ];
3744
- const child = spawn(spawnTarget.command, args, {
3745
- cwd: this.sessionManager.getCwd(),
3746
- detached: true,
3747
- stdio: ["ignore", outFd, outFd],
3748
- env: { ...process.env, PI_AUTO_LEARN_CHILD: "1" },
4049
+ promptPath,
3749
4050
  });
3750
- child.unref();
3751
- fs.closeSync(outFd);
3752
- const now = Date.now();
3753
- const state = this.pruneAutoLearnState(this.readAutoLearnState(), now);
3754
- if (options.cooldownKind === "reflection") {
3755
- state.lastReflectionByTenant = {
3756
- ...(state.lastReflectionByTenant ?? {}),
3757
- [this.getAutoLearnTenantKey()]: now,
3758
- };
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}`;
3759
4058
  }
3760
- else {
3761
- state.lastLaunchByTenant = { ...(state.lastLaunchByTenant ?? {}), [this.getAutoLearnTenantKey()]: now };
3762
- }
3763
- state.runs = {
3764
- ...(state.runs ?? {}),
3765
- [runId]: {
3766
- tenant: this.getAutoLearnTenantKey(),
3767
- pid: child.pid,
3768
- model: modelPattern,
3769
- reason,
3770
- startedAt: now,
3771
- 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, {
3772
4093
  cwd: this.sessionManager.getCwd(),
3773
- logPath,
3774
- sessionDir,
3775
- sessionId,
3776
- promptPath,
3777
- kind,
3778
- autonomyMode: this.settingsManager.getAutonomySettings().mode,
3779
- authority: this.settingsManager.getAutonomySettings().mode === "full"
3780
- ? "standing-full-autonomous"
3781
- : "proposal-gated",
3782
- },
3783
- };
3784
- 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);
3785
4134
  this.autoLearnLastStatus = `running ${modelPattern}`;
3786
4135
  this.updateAutoLearnFooter();
3787
4136
  return `Auto Learn started (${reason}) with ${modelPattern}. Log: ${logPath}`;
@@ -3857,8 +4206,10 @@ export class InteractiveMode {
3857
4206
  evaluateAutonomyReview(messages) {
3858
4207
  const settings = this.getEffectiveAutoLearnSettings();
3859
4208
  const autonomy = this.settingsManager.getAutonomySettings();
3860
- const state = this.pruneAutoLearnState(this.readAutoLearnState());
3861
- this.writeAutoLearnState(state);
4209
+ const state = this.withAutoLearnStateLock((current) => {
4210
+ const pruned = this.pruneAutoLearnHistoryFromState(current);
4211
+ return { result: pruned, next: pruned };
4212
+ });
3862
4213
  const now = Date.now();
3863
4214
  const tenant = this.getAutoLearnTenantKey();
3864
4215
  const runningCount = Object.keys(state.runs ?? {}).length;
@@ -3953,7 +4304,7 @@ export class InteractiveMode {
3953
4304
  formatAutoLearnStatus() {
3954
4305
  const settings = this.getEffectiveAutoLearnSettings();
3955
4306
  const decision = this.evaluateAutoLearn(false);
3956
- const state = this.pruneAutoLearnState(this.readAutoLearnState());
4307
+ const state = this.getPrunedAutoLearnState();
3957
4308
  const runs = Object.entries(state.runs ?? {});
3958
4309
  const contextText = decision.contextPercent === null ? "unknown" : `${decision.contextPercent.toFixed(1)}%`;
3959
4310
  const cooldownText = decision.cooldownRemainingMs > 0 ? `${Math.ceil(decision.cooldownRemainingMs / 60000)}m remaining` : "ready";
@@ -3967,7 +4318,7 @@ export class InteractiveMode {
3967
4318
  .filter(Boolean)
3968
4319
  .join(", ");
3969
4320
  const sessionText = session ? `, ${session}` : "";
3970
- return `- ${id}: ${run.model}, kind=${run.kind ?? "auto"}, authority=${run.authority ?? "unknown"}, pid=${run.pid ?? "?"}${sessionText}, log=${run.logPath}`;
4321
+ return `- ${id}: ${run.model}, kind=${run.kind ?? "auto"}, status=${run.status ?? "running"}, authority=${run.authority ?? "unknown"}, pid=${run.pid ?? "?"}${sessionText}, log=${run.logPath}`;
3971
4322
  })
3972
4323
  .join("\n")
3973
4324
  : "- none";
@@ -3982,12 +4333,12 @@ export class InteractiveMode {
3982
4333
  const reflectionLast = state.lastReflectionByTenant?.[this.getAutoLearnTenantKey()] ?? 0;
3983
4334
  const reflectionCooldownRemainingMs = Math.max(0, reflectionLast + settings.reflectionCooldownMinutes * 60 * 1000 - Date.now());
3984
4335
  const reflectionCooldownText = reflectionCooldownRemainingMs > 0 ? `${Math.ceil(reflectionCooldownRemainingMs / 60000)}m remaining` : "ready";
3985
- 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}\nPi auto-reload blockers: ${reloadBlockers.pending ? reloadBlockers.reason : "none"}\n${reloadBlockerLines}\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}`;
3986
4337
  }
3987
4338
  formatAutonomyStatus() {
3988
4339
  const autonomy = this.settingsManager.getAutonomySettings();
3989
4340
  const settings = this.getEffectiveAutoLearnSettings();
3990
- const autoLearnState = this.pruneAutoLearnState(this.readAutoLearnState());
4341
+ const autoLearnState = this.getPrunedAutoLearnState();
3991
4342
  const running = Object.entries(autoLearnState.runs ?? {});
3992
4343
  const safety = autonomy.mode === "full"
3993
4344
  ? "standing grant for memory, skills, user/project extensions, autonomy/autoLearn tuning, and authorized selfModification.sourcePath edits; hard stops still require explicit foreground approval"
@@ -4002,6 +4353,7 @@ export class InteractiveMode {
4002
4353
  `Long-session trigger: ${settings.longSessionMessages} messages or ${settings.longSessionContextPercent}% context; cooldown=${settings.cooldownMinutes}m`,
4003
4354
  reflectionLine,
4004
4355
  `Running learners: ${running.length}/${settings.maxConcurrentLearners}`,
4356
+ "History retention: 7 days for internal Auto Learn prompts/logs/sessions",
4005
4357
  `Standing authority: ${safety}`,
4006
4358
  `Audit/log dir: ${this.getAutoLearnDataDir()}`,
4007
4359
  "Use /autonomy off|safe|balanced|full to switch presets. Advanced overrides remain in /settings → Auto Learn Advanced.",
@@ -4669,6 +5021,35 @@ export class InteractiveMode {
4669
5021
  }
4670
5022
  return options.sort((a, b) => a.name.localeCompare(b.name));
4671
5023
  }
5024
+ resolveAuthProviderOption(providerReference, providerOptions) {
5025
+ const normalized = providerReference.trim().toLowerCase();
5026
+ if (!normalized)
5027
+ return undefined;
5028
+ const exactMatch = providerOptions.find((provider) => {
5029
+ const id = provider.id.toLowerCase();
5030
+ const name = provider.name.toLowerCase();
5031
+ return id === normalized || name === normalized;
5032
+ });
5033
+ if (exactMatch)
5034
+ return exactMatch;
5035
+ const aliasTarget = cliProviderAliases[normalized] ?? normalized;
5036
+ return providerOptions.find((provider) => {
5037
+ const id = provider.id.toLowerCase();
5038
+ const name = provider.name.toLowerCase();
5039
+ return id === aliasTarget || name === aliasTarget;
5040
+ });
5041
+ }
5042
+ async startProviderLogin(providerOption) {
5043
+ if (providerOption.authType === "oauth") {
5044
+ await this.showLoginDialog(providerOption.id, providerOption.name);
5045
+ }
5046
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
5047
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
5048
+ }
5049
+ else {
5050
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
5051
+ }
5052
+ }
4672
5053
  showLoginAuthTypeSelector() {
4673
5054
  const subscriptionLabel = "Use a subscription";
4674
5055
  const apiKeyLabel = "Use an API key";
@@ -4697,15 +5078,7 @@ export class InteractiveMode {
4697
5078
  if (!providerOption) {
4698
5079
  return;
4699
5080
  }
4700
- if (providerOption.authType === "oauth") {
4701
- await this.showLoginDialog(providerOption.id, providerOption.name);
4702
- }
4703
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4704
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4705
- }
4706
- else {
4707
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4708
- }
5081
+ await this.startProviderLogin(providerOption);
4709
5082
  }, () => {
4710
5083
  done();
4711
5084
  this.showLoginAuthTypeSelector();
@@ -4713,8 +5086,18 @@ export class InteractiveMode {
4713
5086
  return { component: selector, focus: selector };
4714
5087
  });
4715
5088
  }
4716
- async showOAuthSelector(mode) {
5089
+ async showOAuthSelector(mode, providerReference) {
4717
5090
  if (mode === "login") {
5091
+ if (providerReference) {
5092
+ const providerOptions = this.getLoginProviderOptions();
5093
+ const providerOption = this.resolveAuthProviderOption(providerReference, providerOptions);
5094
+ if (!providerOption) {
5095
+ this.showError(`Unknown login provider "${providerReference}". Use /login to select from available providers.`);
5096
+ return;
5097
+ }
5098
+ await this.startProviderLogin(providerOption);
5099
+ return;
5100
+ }
4718
5101
  this.showLoginAuthTypeSelector();
4719
5102
  return;
4720
5103
  }
@@ -4723,6 +5106,26 @@ export class InteractiveMode {
4723
5106
  this.showStatus("No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.");
4724
5107
  return;
4725
5108
  }
5109
+ if (providerReference) {
5110
+ const providerOption = this.resolveAuthProviderOption(providerReference, providerOptions);
5111
+ if (!providerOption) {
5112
+ this.showError(`No stored credentials found for "${providerReference}". Use /logout to select a saved provider.`);
5113
+ return;
5114
+ }
5115
+ try {
5116
+ this.session.modelRegistry.authStorage.logout(providerOption.id);
5117
+ this.session.modelRegistry.refresh();
5118
+ await this.updateAvailableProviderCount();
5119
+ const message = providerOption.authType === "oauth"
5120
+ ? `Logged out of ${providerOption.name}`
5121
+ : `Removed stored API key for ${providerOption.name}. Environment variables and models.json config are unchanged.`;
5122
+ this.showStatus(message);
5123
+ }
5124
+ catch (error) {
5125
+ this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
5126
+ }
5127
+ return;
5128
+ }
4726
5129
  this.showSelector((done) => {
4727
5130
  const selector = new OAuthSelectorComponent(mode, this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4728
5131
  done();