@oai404iao/pi-subagent 0.4.0-alpha.0 → 0.4.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/src/agent-sync.ts CHANGED
@@ -30,6 +30,11 @@ interface AgentManifest {
30
30
  version: 1;
31
31
  packageVersion: string;
32
32
  files: Record<string, string>;
33
+ /**
34
+ * Bundled preset names the user deleted. Initialization never restores
35
+ * them, so deleting a preset stays effective across package versions.
36
+ */
37
+ retired: string[];
33
38
  }
34
39
 
35
40
  type ManifestReadResult =
@@ -74,7 +79,14 @@ export interface AgentSyncResult {
74
79
  manifestPath: string;
75
80
  installed: string[];
76
81
  updated: string[];
82
+ /** Bundled presets retired upstream and removed from the user directory. */
77
83
  removed: string[];
84
+ /** Presets the user deleted; initialization keeps them absent. */
85
+ retired: string[];
86
+ /** Previously deleted presets that reappeared on disk as managed again. */
87
+ restored: string[];
88
+ /** True when this run recorded a new deletion or a recreation. */
89
+ retirementChanged: boolean;
78
90
  preserved: string[];
79
91
  backups: AgentBackup[];
80
92
  diagnostics: string[];
@@ -135,10 +147,27 @@ function parseManifest(value: unknown, manifestPath: string): AgentManifest {
135
147
  }
136
148
  files[name] = hash;
137
149
  }
150
+ const retired: string[] = [];
151
+ if (input.retired !== undefined) {
152
+ if (!Array.isArray(input.retired)) {
153
+ throw new Error("retired must be an array of bundled preset file names");
154
+ }
155
+ for (const name of input.retired) {
156
+ if (
157
+ typeof name !== "string" ||
158
+ basename(name) !== name ||
159
+ !name.endsWith(".md")
160
+ ) {
161
+ throw new Error(`retired contains an invalid entry for "${String(name)}"`);
162
+ }
163
+ if (!retired.includes(name)) retired.push(name);
164
+ }
165
+ }
138
166
  return {
139
167
  version: MANIFEST_VERSION,
140
168
  packageVersion: input.packageVersion.trim(),
141
169
  files,
170
+ retired,
142
171
  };
143
172
  }
144
173
 
@@ -441,6 +470,9 @@ function syncBundledAgentsLocked(
441
470
  installed: [],
442
471
  updated: [],
443
472
  removed: [],
473
+ retired: [],
474
+ restored: [],
475
+ retirementChanged: false,
444
476
  preserved: [],
445
477
  backups: [],
446
478
  diagnostics: [manifestRead.diagnostic],
@@ -448,10 +480,38 @@ function syncBundledAgentsLocked(
448
480
  }
449
481
  const previous = manifestRead.kind === "valid" ? manifestRead.manifest : undefined;
450
482
 
483
+ const files = bundledAgentFiles(options.bundledDir);
484
+ const currentNames = new Set(files.map((file) => file.name));
485
+ const previousFiles = previous?.files ?? {};
486
+ const retiredNames = new Set<string>();
487
+ const restored: string[] = [];
488
+
489
+ // A preset that was managed before and is missing now was deleted by the
490
+ // user. Record that deletion at every startup so a later package version
491
+ // cannot resurrect it; an explicit recreation restores management.
492
+ for (const name of previous?.retired ?? []) {
493
+ if (currentNames.has(name) && existsSync(join(userAgentsDir, name))) {
494
+ restored.push(name);
495
+ }
496
+ }
497
+ for (const name of Object.keys(previousFiles)) {
498
+ if (!currentNames.has(name)) continue;
499
+ if (restored.includes(name)) continue;
500
+ if ((previous?.retired ?? []).includes(name)) {
501
+ retiredNames.add(name);
502
+ continue;
503
+ }
504
+ if (!existsSync(join(userAgentsDir, name))) retiredNames.add(name);
505
+ }
506
+ const recordedRetired = [...retiredNames].sort();
507
+ const retirementChanged =
508
+ restored.length > 0 ||
509
+ recordedRetired.join("\n") !== (previous?.retired ?? []).slice().sort().join("\n");
510
+
451
511
  // The package copies are initialization templates, not a runtime fallback.
452
512
  // Once this package version has been initialized, the user directory is
453
513
  // authoritative: edits and deletions must survive every same-version start.
454
- if (previous?.packageVersion === packageVersion) {
514
+ if (previous?.packageVersion === packageVersion && !retirementChanged) {
455
515
  return {
456
516
  packageVersion,
457
517
  userAgentsDir,
@@ -459,68 +519,77 @@ function syncBundledAgentsLocked(
459
519
  installed: [],
460
520
  updated: [],
461
521
  removed: [],
522
+ retired: recordedRetired,
523
+ restored: [],
524
+ retirementChanged: false,
462
525
  preserved: [],
463
526
  backups: [],
464
527
  diagnostics,
465
528
  };
466
529
  }
467
530
 
468
- const files = bundledAgentFiles(options.bundledDir);
469
- const currentNames = new Set(files.map((file) => file.name));
470
531
  const preserved: string[] = [];
471
532
  const actions: PlannedAction[] = [];
533
+ const versionChanged = previous?.packageVersion !== packageVersion;
534
+
535
+ // Agent files change only on first install and package-version changes. A
536
+ // same-version startup that merely recorded a deletion or a recreation only
537
+ // rewrites the manifest below.
538
+ if (versionChanged) {
539
+ // Plan the complete operation before changing any user agent file.
540
+ for (const file of files) {
541
+ if (retiredNames.has(file.name)) continue;
542
+ const destination = join(userAgentsDir, file.name);
543
+ const destinationState = destinationKind(destination);
544
+ if (destinationState.kind === "missing") {
545
+ actions.push({
546
+ name: file.name,
547
+ kind: "install",
548
+ destination,
549
+ destinationKind: "missing",
550
+ content: file.content,
551
+ });
552
+ continue;
553
+ }
472
554
 
473
- // Plan the complete first-install or version-change operation before
474
- // changing any user agent file.
475
- for (const file of files) {
476
- const destination = join(userAgentsDir, file.name);
477
- const destinationState = destinationKind(destination);
478
- if (destinationState.kind === "missing") {
555
+ if (
556
+ destinationState.kind === "file"
557
+ && sameRegularFile(destination, destinationState.size, file.content)
558
+ ) {
559
+ preserved.push(file.name);
560
+ continue;
561
+ }
479
562
  actions.push({
480
563
  name: file.name,
481
- kind: "install",
564
+ kind: "replace",
482
565
  destination,
483
- destinationKind: "missing",
566
+ destinationKind: destinationState.kind,
484
567
  content: file.content,
485
568
  });
486
- continue;
487
569
  }
488
570
 
489
- if (
490
- destinationState.kind === "file" &&
491
- sameRegularFile(destination, destinationState.size, file.content)
492
- ) {
493
- preserved.push(file.name);
494
- continue;
571
+ // Retired bundled presets must not remain silently active. They are
572
+ // backed up like replacements, then removed; unrelated user-defined
573
+ // names remain.
574
+ for (const name of Object.keys(previousFiles).sort((left, right) => left.localeCompare(right))) {
575
+ if (currentNames.has(name)) continue;
576
+ const destination = join(userAgentsDir, name);
577
+ const destinationState = destinationKind(destination);
578
+ if (destinationState.kind === "missing") continue;
579
+ actions.push({
580
+ name,
581
+ kind: "remove",
582
+ destination,
583
+ destinationKind: destinationState.kind,
584
+ });
495
585
  }
496
- actions.push({
497
- name: file.name,
498
- kind: "replace",
499
- destination,
500
- destinationKind: destinationState.kind,
501
- content: file.content,
502
- });
503
- }
504
-
505
- // Retired bundled presets must not remain silently active. They are backed
506
- // up like replacements, then removed; unrelated user-defined names remain.
507
- for (const name of Object.keys(previous?.files ?? {}).sort((left, right) => left.localeCompare(right))) {
508
- if (currentNames.has(name)) continue;
509
- const destination = join(userAgentsDir, name);
510
- const destinationState = destinationKind(destination);
511
- if (destinationState.kind === "missing") continue;
512
- actions.push({
513
- name,
514
- kind: "remove",
515
- destination,
516
- destinationKind: destinationState.kind,
517
- });
518
586
  }
519
587
 
520
588
  const manifest: AgentManifest = {
521
589
  version: MANIFEST_VERSION,
522
590
  packageVersion,
523
591
  files: Object.fromEntries(files.map((file) => [file.name, file.hash])),
592
+ retired: recordedRetired,
524
593
  };
525
594
  const stagedPaths: string[] = [];
526
595
  const backups: AgentBackup[] = [];
@@ -587,6 +656,9 @@ function syncBundledAgentsLocked(
587
656
  installed: actions.filter((action) => action.kind === "install").map((action) => action.name),
588
657
  updated: actions.filter((action) => action.kind === "replace").map((action) => action.name),
589
658
  removed: actions.filter((action) => action.kind === "remove").map((action) => action.name),
659
+ retired: recordedRetired,
660
+ restored: [...restored].sort(),
661
+ retirementChanged,
590
662
  preserved,
591
663
  backups,
592
664
  diagnostics,
package/src/catalog.ts CHANGED
@@ -61,24 +61,22 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
61
61
  return;
62
62
  }
63
63
  let pendingMessages = 0;
64
- if (folded.descriptor.runtime.backgroundProtocol === "mailbox-v2") {
65
- const mailbox = foldOwnedMailbox(manager.getEntries(), {
66
- parentAgentId: folded.descriptor.parentAgentId,
67
- agentId: folded.descriptor.agentId,
64
+ const mailbox = foldOwnedMailbox(manager.getEntries(), {
65
+ parentAgentId: folded.descriptor.parentAgentId,
66
+ agentId: folded.descriptor.agentId,
67
+ });
68
+ if (mailbox.kind === "corrupt") {
69
+ diagnostics.push({
70
+ kind: "diagnostic",
71
+ piSessionId: manager.getSessionId(),
72
+ reason: "corrupt",
73
+ sessionFile: info.path,
74
+ ...(headerParent ? { parentSessionFile: headerParent } : {}),
75
+ message: `corrupt subagent mailbox: ${mailbox.message}`,
68
76
  });
69
- if (mailbox.kind === "corrupt") {
70
- diagnostics.push({
71
- kind: "diagnostic",
72
- piSessionId: manager.getSessionId(),
73
- reason: "corrupt",
74
- sessionFile: info.path,
75
- ...(headerParent ? { parentSessionFile: headerParent } : {}),
76
- message: `corrupt subagent mailbox: ${mailbox.message}`,
77
- });
78
- return;
79
- }
80
- pendingMessages = mailbox.snapshot.pending.length;
77
+ return;
81
78
  }
79
+ pendingMessages = mailbox.snapshot.pending.length;
82
80
  let unreadUpdatesByChild = new Map<string, number>();
83
81
  const completions = foldCompletionMailbox(
84
82
  manager.getEntries(),
package/src/config.ts CHANGED
@@ -1,24 +1,16 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
4
- import type {
5
- AgentScope,
6
- BackgroundProtocol,
7
- ReportDelivery,
8
- SubagentSettings,
9
- } from "./types.ts";
4
+ import type { AgentScope, RuntimeMode, SubagentSettings } from "./types.ts";
10
5
 
11
6
  export const CONFIG_FILE_NAME = "subagent.json";
12
7
 
13
8
  export const DEFAULT_SETTINGS: Readonly<SubagentSettings> = {
14
9
  agentScope: "user",
15
10
  maxDepth: 3,
16
- enableRunInBackground: true,
17
- defaultBackground: true,
11
+ runtimeMode: "background",
18
12
  maxConcurrentBackgroundRuns: 4,
19
13
  maxIdleRuntimes: 0,
20
- backgroundProtocol: "legacy",
21
- reportDelivery: "wakeup",
22
14
  inheritExtensions: false,
23
15
  openAIIdentity: false,
24
16
  maxOutputBytes: 50 * 1024,
@@ -27,14 +19,10 @@ export const DEFAULT_SETTINGS: Readonly<SubagentSettings> = {
27
19
  const CONFIG_KEYS = new Set([
28
20
  "$schema",
29
21
  "agentScope",
30
- "syncBundledAgents",
31
22
  "maxDepth",
32
- "enableRunInBackground",
33
- "defaultBackground",
23
+ "runtimeMode",
34
24
  "maxConcurrentBackgroundRuns",
35
25
  "maxIdleRuntimes",
36
- "backgroundProtocol",
37
- "reportDelivery",
38
26
  "inheritExtensions",
39
27
  "openAIIdentity",
40
28
  "maxOutputBytes",
@@ -91,14 +79,9 @@ function parseAgentScope(value: unknown, source: string): AgentScope {
91
79
  throw new Error(`${source}: agentScope must be "user", "project", or "both"`);
92
80
  }
93
81
 
94
- function parseReportDelivery(value: unknown, source: string): ReportDelivery {
95
- if (value === "wakeup" || value === "quiet") return value;
96
- throw new Error(`${source}: reportDelivery must be "wakeup" or "quiet"`);
97
- }
98
-
99
- function parseBackgroundProtocol(value: unknown, source: string): BackgroundProtocol {
100
- if (value === "legacy" || value === "mailbox-v2") return value;
101
- throw new Error(`${source}: backgroundProtocol must be "legacy" or "mailbox-v2"`);
82
+ function parseRuntimeMode(value: unknown, source: string): RuntimeMode {
83
+ if (value === "foreground" || value === "background") return value;
84
+ throw new Error(`${source}: runtimeMode must be "foreground" or "background"`);
102
85
  }
103
86
 
104
87
  function parseBoolean(value: unknown, key: string, source: string): boolean {
@@ -129,20 +112,12 @@ function applyConfig(
129
112
  settings: SubagentSettings,
130
113
  config: ConfigRecord,
131
114
  source: string,
132
- options: { allowSyncBundledAgents: boolean },
133
115
  ): SubagentSettings {
134
- if (config.syncBundledAgents !== undefined && !options.allowSyncBundledAgents) {
135
- throw new Error(`${source}: syncBundledAgents may be configured only in the user-level subagent.json`);
136
- }
137
- if (config.syncBundledAgents !== undefined) {
138
- // Compatibility with 0.2/0.3 configuration files. Bundled templates are
139
- // now always initialized on first install/version change, and this
140
- // retired switch no longer controls runtime discovery or writes.
141
- parseBoolean(config.syncBundledAgents, "syncBundledAgents", source);
142
- }
143
116
  return {
144
117
  agentScope:
145
- config.agentScope === undefined ? settings.agentScope : parseAgentScope(config.agentScope, source),
118
+ config.agentScope === undefined
119
+ ? settings.agentScope
120
+ : parseAgentScope(config.agentScope, source),
146
121
  maxDepth:
147
122
  config.maxDepth === undefined
148
123
  ? settings.maxDepth
@@ -150,14 +125,10 @@ function applyConfig(
150
125
  minimum: 0,
151
126
  maximum: Number.MAX_SAFE_INTEGER,
152
127
  }),
153
- enableRunInBackground:
154
- config.enableRunInBackground === undefined
155
- ? settings.enableRunInBackground
156
- : parseBoolean(config.enableRunInBackground, "enableRunInBackground", source),
157
- defaultBackground:
158
- config.defaultBackground === undefined
159
- ? settings.defaultBackground
160
- : parseBoolean(config.defaultBackground, "defaultBackground", source),
128
+ runtimeMode:
129
+ config.runtimeMode === undefined
130
+ ? settings.runtimeMode
131
+ : parseRuntimeMode(config.runtimeMode, source),
161
132
  maxConcurrentBackgroundRuns:
162
133
  config.maxConcurrentBackgroundRuns === undefined
163
134
  ? settings.maxConcurrentBackgroundRuns
@@ -182,14 +153,6 @@ function applyConfig(
182
153
  maximum: Number.MAX_SAFE_INTEGER,
183
154
  },
184
155
  ),
185
- backgroundProtocol:
186
- config.backgroundProtocol === undefined
187
- ? settings.backgroundProtocol
188
- : parseBackgroundProtocol(config.backgroundProtocol, source),
189
- reportDelivery:
190
- config.reportDelivery === undefined
191
- ? settings.reportDelivery
192
- : parseReportDelivery(config.reportDelivery, source),
193
156
  inheritExtensions:
194
157
  config.inheritExtensions === undefined
195
158
  ? settings.inheritExtensions
@@ -214,7 +177,7 @@ export function loadSettings(options: LoadSettingsOptions): LoadedSettings {
214
177
  const userPath = join(options.agentDir ?? getAgentDir(), CONFIG_FILE_NAME);
215
178
  const userConfig = readConfig(userPath);
216
179
  if (userConfig) {
217
- settings = applyConfig(settings, userConfig, userPath, { allowSyncBundledAgents: true });
180
+ settings = applyConfig(settings, userConfig, userPath);
218
181
  sources.push(userPath);
219
182
  }
220
183
 
@@ -223,7 +186,7 @@ export function loadSettings(options: LoadSettingsOptions): LoadedSettings {
223
186
  if (projectPath) {
224
187
  const projectConfig = readConfig(projectPath);
225
188
  if (projectConfig) {
226
- settings = applyConfig(settings, projectConfig, projectPath, { allowSyncBundledAgents: false });
189
+ settings = applyConfig(settings, projectConfig, projectPath);
227
190
  sources.push(projectPath);
228
191
  }
229
192
  }