@selesai/code 0.13.33 → 0.13.35
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/CHANGELOG.md +21 -0
- package/dist/extensions/capability-gateway/catalog.ts +4 -1
- package/dist/extensions/capability-gateway/index.test.ts +93 -4
- package/dist/extensions/capability-gateway/index.ts +295 -59
- package/dist/extensions/capability-gateway/integration.test.ts +35 -0
- package/dist/extensions/capability-gateway/routing.test.ts +154 -1
- package/dist/extensions/capability-gateway/routing.ts +227 -45
- package/dist/extensions/jev/decisions.test.ts +37 -0
- package/dist/extensions/jev/decisions.ts +161 -43
- package/dist/extensions/jev-ask-tool.test.ts +501 -0
- package/dist/extensions/jev-ask-tool.ts +952 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-hermes-memory/README.md +11 -36
- package/dist/extensions/pi-hermes-memory/src/config.ts +36 -8
- package/dist/extensions/pi-hermes-memory/src/constants.ts +5 -5
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +60 -46
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +26 -2
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +7 -1
- package/dist/extensions/pi-intercom/index.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +6 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +26 -1
- package/dist/extensions/pi-subagents/src/runs/shared/jev-subagent-routing.ts +255 -0
- package/dist/extensions/pi-subagents/test/unit/jev-subagent-routing.test.ts +117 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +3 -1
- package/dist/extensions/rtk.test.ts +21 -13
- package/dist/extensions/tps.test.ts +32 -1
- package/dist/extensions/tps.ts +3 -1
- package/docs/settings.md +64 -7
- package/package.json +3 -3
|
@@ -483,45 +483,19 @@ Move behavior:
|
|
|
483
483
|
|
|
484
484
|
## Configuration
|
|
485
485
|
|
|
486
|
-
|
|
486
|
+
Set extension options under `hermesMemory` in `~/.selesai/agent/settings.json`:
|
|
487
487
|
|
|
488
488
|
```json
|
|
489
489
|
{
|
|
490
|
-
"
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"projectCharLimit": 5000,
|
|
495
|
-
"memoryDir": "~/.selesai/agent/pi-hermes-memory",
|
|
496
|
-
"projectsMemoryDir": "projects-memory",
|
|
497
|
-
"sessionSearch": { "variant": "legacy" },
|
|
498
|
-
"sessionRetentionDays": 0,
|
|
499
|
-
"quickCheckOnOpen": true,
|
|
500
|
-
"llmModelOverride": "openrouter/deepseek/deepseek-v4-flash",
|
|
501
|
-
"llmThinkingOverride": "off",
|
|
502
|
-
"childExtensionPaths": ["~/.selesai/agent/git/github.com/example/custom-provider-extension/index.ts"],
|
|
503
|
-
"nudgeInterval": 10,
|
|
504
|
-
"nudgeToolCalls": 15,
|
|
505
|
-
"reviewRecentMessages": 0,
|
|
506
|
-
"reviewEnabled": true,
|
|
507
|
-
"reviewTransport": "direct",
|
|
508
|
-
"memoryOverflowStrategy": "auto-consolidate",
|
|
509
|
-
"autoConsolidate": true,
|
|
510
|
-
"correctionDetection": true,
|
|
511
|
-
"failureInjectionEnabled": true,
|
|
512
|
-
"failureInjectionMaxAgeDays": 7,
|
|
513
|
-
"failureInjectionMaxEntries": 5,
|
|
514
|
-
"consolidationTimeoutMs": 180000,
|
|
515
|
-
"overflowGraceMs": 180000,
|
|
516
|
-
"autoConsolidationWarnOnFailure": true,
|
|
517
|
-
"flushOnCompact": true,
|
|
518
|
-
"flushOnShutdown": true,
|
|
519
|
-
"flushMinTurns": 6,
|
|
520
|
-
"flushRecentMessages": 0,
|
|
521
|
-
"standingInstructionsEnabled": true
|
|
490
|
+
"hermesMemory": {
|
|
491
|
+
"llmThinkingOverride": "off",
|
|
492
|
+
"consolidationTimeoutMs": 300000
|
|
493
|
+
}
|
|
522
494
|
}
|
|
523
495
|
```
|
|
524
496
|
|
|
497
|
+
The extension reads these options at startup. Values in `settings.json` override the legacy `hermes-memory-config.json`, which remains a fallback for existing installations. All settings below are keys inside `hermesMemory`. `llmModelOverride` is optional and uses `provider/model` format (for example, `tokenin/deepseek-v4.1-flash` if your account has access); leave it unset to use the active model.
|
|
498
|
+
|
|
525
499
|
| Setting | Default | Description |
|
|
526
500
|
|---|---|---|
|
|
527
501
|
| `memoryMode` | `policy-only` | Prompt behavior: `policy-only` injects only memory policy; `legacy-inject` restores full memory prompt injection |
|
|
@@ -536,7 +510,7 @@ Create `~/.selesai/agent/hermes-memory-config.json`:
|
|
|
536
510
|
| `sessionSearch` | `{ "variant": "legacy" }` | Session search implementation: `legacy` keeps the existing SQLite/FTS snippet search; `anchors` uses the opt-in Markdown request surface and returns compact JSONL line-range anchors from `~/.selesai/agent/sessions/` |
|
|
537
511
|
| `sessionRetentionDays` | `0` | Opt-in SQLite session retention, in days. `0` (default) disables pruning entirely and keeps the legacy count-only backfill preflight. When positive, sessions whose JSONL source file was last modified longer ago than the window are pruned from SQLite at startup — **rows only; the JSONL files in `~/.selesai/agent/sessions/` are never deleted** — and both the deferred backfill and `/memory-index-sessions` skip files outside the window, so pruned sessions stay pruned instead of being re-indexed |
|
|
538
512
|
| `quickCheckOnOpen` | `true` | Run a full SQLite integrity check asynchronously after opening the database; set to `false` to skip the startup scan (operation-time recovery remains enabled) |
|
|
539
|
-
| `llmModelOverride` | unset | Optional model override for background review (direct and subprocess), correction save, session flush, and consolidation |
|
|
513
|
+
| `llmModelOverride` | unset | Optional `provider/model` override for background review (direct and subprocess), correction save, session flush, and consolidation; unset uses the active model |
|
|
540
514
|
| `llmThinkingOverride` | unset | Optional thinking override for those LLM calls; valid values are `off`, `minimal`, `low`, `medium`, `high`, and `xhigh`. If `llmModelOverride` is set and this is omitted, review/child calls default to `off` |
|
|
541
515
|
| `childExtensionPaths` | unset | Trusted provider/auth extension sources explicitly allowed in isolated child Pi processes. Values are passed to Pi's standard `-e` resolver, so absolute paths, `~/...`, paths relative to the child working directory, and `git:`/`npm:` package sources are supported. Sibling packages matching the `*-oauth-adapter`/`*-auth-adapter` naming convention (including scoped packages, via their `package.json` `pi.extensions` manifest) are detected automatically. This setting is only needed for custom providers or adapters that are not detected. In-process direct transport (the default for review/flush/correction/consolidation) doesn't need it, since it reads whatever provider auth is already registered. |
|
|
542
516
|
| `nudgeInterval` | `10` | Turns between auto-reviews |
|
|
@@ -546,7 +520,7 @@ Create `~/.selesai/agent/hermes-memory-config.json`:
|
|
|
546
520
|
| `reviewTransport` | `direct` | LLM transport for background review, session flush, correction save, and manual consolidation: `direct` uses in-process `completeSimple()` with subprocess fallback; `subprocess` forces legacy `pi -p` only |
|
|
547
521
|
| `memoryOverflowStrategy` | `auto-consolidate` | Legacy-inject behavior when a Markdown memory file reaches its character limit: `auto-consolidate` runs the existing consolidation flow; `reject` returns an error; `fifo-evict` rotates older entries in file order until the new entry fits |
|
|
548
522
|
| `autoConsolidate` | `true` | Legacy alias for `memoryOverflowStrategy` when `memoryOverflowStrategy` is not set (`true` = `auto-consolidate`, `false` = `reject`) |
|
|
549
|
-
| `consolidationTimeoutMs` | `
|
|
523
|
+
| `consolidationTimeoutMs` | `300000` | Maximum time in milliseconds for a consolidation run (auto and `/memory-consolidate` alike). Configured values are used verbatim; a consolidation pays child-process boot plus a full LLM turn, so values below the default are frequently killed mid-run and log a warning at startup |
|
|
550
524
|
| `overflowGraceMs` | `180000` | Wall-clock grace period after a memory overflow before automatic consolidation is retried; this gives the active agent time to consolidate manually. Set to `0` to disable the grace period |
|
|
551
525
|
| `autoConsolidationWarnOnFailure` | `true` | Log failed automatic consolidation attempts to the session console. Set to `false` to suppress only this warning; the memory tool result still reports the failure reason |
|
|
552
526
|
| `correctionDetection` | `true` | Detect user corrections and save immediately |
|
|
@@ -602,7 +576,8 @@ The deferred backfill, live-index, and integrity-check spans may appear after st
|
|
|
602
576
|
│ │ └── SKILL.md
|
|
603
577
|
│ └── another-project/
|
|
604
578
|
│ └── MEMORY.md
|
|
605
|
-
├──
|
|
579
|
+
├── settings.json ← Global settings; extension options are under hermesMemory
|
|
580
|
+
├── hermes-memory-config.json ← Legacy fallback for older extension builds
|
|
606
581
|
└── ...
|
|
607
582
|
```
|
|
608
583
|
|
|
@@ -75,13 +75,42 @@ export const DEFAULT_CONFIG_PATH = path.join(
|
|
|
75
75
|
AGENT_ROOT,
|
|
76
76
|
"hermes-memory-config.json",
|
|
77
77
|
);
|
|
78
|
+
export const DEFAULT_SETTINGS_PATH = path.join(AGENT_ROOT, "settings.json");
|
|
79
|
+
export const HERMES_MEMORY_SETTINGS_KEY = "hermesMemory";
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
function readConfigObject(configPath: string | undefined): Record<string, unknown> | undefined {
|
|
82
|
+
if (!configPath) return undefined;
|
|
80
83
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
85
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
86
|
+
? parsed as Record<string, unknown>
|
|
87
|
+
: undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read extension settings from settings.json, falling back to the legacy file. */
|
|
98
|
+
export function loadConfig(
|
|
99
|
+
configPath = DEFAULT_CONFIG_PATH,
|
|
100
|
+
settingsPath?: string,
|
|
101
|
+
): MemoryConfig {
|
|
102
|
+
try {
|
|
103
|
+
const legacyConfig = readConfigObject(configPath);
|
|
104
|
+
const resolvedSettingsPath = settingsPath
|
|
105
|
+
?? (configPath === DEFAULT_CONFIG_PATH ? DEFAULT_SETTINGS_PATH : undefined);
|
|
106
|
+
const settings = readConfigObject(resolvedSettingsPath);
|
|
107
|
+
const extensionSettings = settings?.[HERMES_MEMORY_SETTINGS_KEY];
|
|
108
|
+
if (legacyConfig || isRecord(extensionSettings)) {
|
|
109
|
+
const parsed: Record<string, unknown> = {
|
|
110
|
+
...(legacyConfig ?? {}),
|
|
111
|
+
...(isRecord(extensionSettings) ? extensionSettings : {}),
|
|
112
|
+
};
|
|
113
|
+
// New settings.json values take precedence over the legacy file.
|
|
85
114
|
const config: MemoryConfig = { ...DEFAULT_CONFIG };
|
|
86
115
|
const isNonNegativeNumber = (value: unknown): value is number => (
|
|
87
116
|
typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
@@ -156,8 +185,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
156
185
|
if (normalizedProjectsMemoryDir) config.projectsMemoryDir = normalizedProjectsMemoryDir;
|
|
157
186
|
}
|
|
158
187
|
if (
|
|
159
|
-
|
|
160
|
-
parsed.sessionSearch !== null &&
|
|
188
|
+
isRecord(parsed.sessionSearch) &&
|
|
161
189
|
isSessionSearchVariant(parsed.sessionSearch.variant)
|
|
162
190
|
) {
|
|
163
191
|
config.sessionSearch = { variant: parsed.sessionSearch.variant };
|
|
@@ -204,7 +232,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
204
232
|
return config;
|
|
205
233
|
}
|
|
206
234
|
} catch {
|
|
207
|
-
// Fall back to defaults on parse
|
|
235
|
+
// Fall back to defaults on unexpected parse or access issues.
|
|
208
236
|
}
|
|
209
237
|
return { ...DEFAULT_CONFIG };
|
|
210
238
|
}
|
|
@@ -29,12 +29,12 @@ export const DEFAULT_NUDGE_TOOL_CALLS = 15;
|
|
|
29
29
|
export const DEFAULT_REVIEW_RECENT_MESSAGES = 0;
|
|
30
30
|
export const DEFAULT_FLUSH_RECENT_MESSAGES = 0;
|
|
31
31
|
/**
|
|
32
|
-
* A consolidation run pays child-process boot plus a full LLM turn
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* A consolidation run pays child-process boot plus a full LLM turn. Five
|
|
33
|
+
* minutes leaves room for slower models and larger memory stores. Configured
|
|
34
|
+
* values are honored verbatim, including lower ones; `loadConfig` warns when a
|
|
35
|
+
* value below this is set.
|
|
36
36
|
*/
|
|
37
|
-
export const DEFAULT_CONSOLIDATION_TIMEOUT_MS =
|
|
37
|
+
export const DEFAULT_CONSOLIDATION_TIMEOUT_MS = 300000;
|
|
38
38
|
/** Wall-clock grace after overflow before an automatic consolidation may run. */
|
|
39
39
|
export const DEFAULT_OVERFLOW_GRACE_MS = 180000;
|
|
40
40
|
export const DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS = 7;
|
|
@@ -337,64 +337,78 @@ export function registerConsolidateCommand(
|
|
|
337
337
|
});
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
} catch {
|
|
346
|
-
// Best-effort only. If the command context is already stale, continue
|
|
347
|
-
// with the consolidation work rather than failing before it starts.
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
for (const item of targets) {
|
|
351
|
-
const entries = entriesForTarget(item.store, item.target);
|
|
352
|
-
|
|
353
|
-
if (entries.length === 0) {
|
|
354
|
-
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
355
|
-
continue;
|
|
340
|
+
const setStatus = (text?: string) => {
|
|
341
|
+
try {
|
|
342
|
+
ctx.ui.setStatus("pi-hermes-memory:consolidation", text);
|
|
343
|
+
} catch {
|
|
344
|
+
// Best-effort progress feedback only.
|
|
356
345
|
}
|
|
346
|
+
};
|
|
357
347
|
|
|
348
|
+
setStatus("Preparing memory consolidation…");
|
|
349
|
+
try {
|
|
358
350
|
try {
|
|
359
351
|
ctx.ui.notify(
|
|
360
|
-
|
|
352
|
+
`🔄 Starting memory consolidation for ${targets.length} target${targets.length === 1 ? "" : "s"}...`,
|
|
361
353
|
"info",
|
|
362
354
|
);
|
|
363
355
|
} catch {
|
|
364
|
-
// Best-effort
|
|
356
|
+
// Best-effort only. If the command context is already stale, continue
|
|
357
|
+
// with the consolidation work rather than failing before it starts.
|
|
365
358
|
}
|
|
366
359
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
360
|
+
for (const [index, item] of targets.entries()) {
|
|
361
|
+
const entries = entriesForTarget(item.store, item.target);
|
|
362
|
+
|
|
363
|
+
if (entries.length === 0) {
|
|
364
|
+
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
setStatus(`Consolidating ${item.label} (${index + 1}/${targets.length})…`);
|
|
369
|
+
try {
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
`⏳ Consolidating ${item.label}...`,
|
|
372
|
+
"info",
|
|
373
|
+
);
|
|
374
|
+
} catch {
|
|
375
|
+
// Best-effort progress feedback only.
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = await triggerConsolidation(
|
|
379
|
+
pi,
|
|
380
|
+
item.store,
|
|
381
|
+
item.target,
|
|
382
|
+
ctx.signal,
|
|
383
|
+
timeoutMs,
|
|
384
|
+
item.toolTarget,
|
|
385
|
+
llmConfig,
|
|
386
|
+
ctx,
|
|
387
|
+
dbManager,
|
|
388
|
+
activeProjectName,
|
|
389
|
+
deps,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (result.consolidated) {
|
|
393
|
+
await item.store.loadFromDisk();
|
|
394
|
+
results.push(`${item.label}: ✅ consolidated`);
|
|
395
|
+
} else {
|
|
396
|
+
results.push(`${item.label}: ❌ ${result.error}`);
|
|
397
|
+
}
|
|
386
398
|
}
|
|
387
|
-
}
|
|
388
399
|
|
|
389
|
-
|
|
400
|
+
const summary = `\n 🔄 Memory Consolidation\n ${"─".repeat(30)}\n${results.map((r) => ` ${r}`).join("\n")}`;
|
|
390
401
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
try {
|
|
403
|
+
ctx.ui.notify(summary, "info");
|
|
404
|
+
} catch {
|
|
405
|
+
// Child consolidation can indirectly trigger a runtime reload/session
|
|
406
|
+
// replacement. If that happens, the original command ctx is stale by
|
|
407
|
+
// the time we reach the final summary, so the command should exit
|
|
408
|
+
// quietly instead of surfacing a stale-ctx error.
|
|
409
|
+
}
|
|
410
|
+
} finally {
|
|
411
|
+
setStatus(undefined);
|
|
398
412
|
}
|
|
399
413
|
},
|
|
400
414
|
});
|
|
@@ -7,9 +7,11 @@ import { loadConfig } from "../src/config.js";
|
|
|
7
7
|
import { AGENT_ROOT } from "../src/paths.js";
|
|
8
8
|
|
|
9
9
|
const TEST_CONFIG_PATH = path.join(os.tmpdir(), `hermes-memory-config-test-${process.pid}.json`);
|
|
10
|
+
const TEST_SETTINGS_PATH = path.join(os.tmpdir(), `hermes-settings-test-${process.pid}.json`);
|
|
10
11
|
|
|
11
12
|
afterEach(() => {
|
|
12
13
|
fs.rmSync(TEST_CONFIG_PATH, { force: true });
|
|
14
|
+
fs.rmSync(TEST_SETTINGS_PATH, { force: true });
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
describe("loadConfig", () => {
|
|
@@ -30,7 +32,7 @@ describe("loadConfig", () => {
|
|
|
30
32
|
assert.strictEqual(config.flushRecentMessages, 0);
|
|
31
33
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
32
34
|
assert.strictEqual(config.autoConsolidate, true);
|
|
33
|
-
assert.strictEqual(config.consolidationTimeoutMs,
|
|
35
|
+
assert.strictEqual(config.consolidationTimeoutMs, 300000);
|
|
34
36
|
assert.strictEqual(config.overflowGraceMs, 180000);
|
|
35
37
|
assert.strictEqual(config.autoConsolidationWarnOnFailure, true);
|
|
36
38
|
assert.strictEqual(config.failureInjectionEnabled, true);
|
|
@@ -65,7 +67,7 @@ describe("loadConfig", () => {
|
|
|
65
67
|
"a lower configured value must be honored, not clamped",
|
|
66
68
|
);
|
|
67
69
|
assert.strictEqual(warnings.length, 1, "a sub-default value should warn once");
|
|
68
|
-
assert.match(warnings[0], /60000ms.*below the
|
|
70
|
+
assert.match(warnings[0], /60000ms.*below the 300000ms default/);
|
|
69
71
|
} finally {
|
|
70
72
|
console.warn = originalWarn;
|
|
71
73
|
}
|
|
@@ -112,6 +114,28 @@ describe("loadConfig", () => {
|
|
|
112
114
|
assert.strictEqual(config.reviewEnabled, true);
|
|
113
115
|
});
|
|
114
116
|
|
|
117
|
+
it("loads hermesMemory from settings.json with precedence over the legacy config", () => {
|
|
118
|
+
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
119
|
+
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
120
|
+
llmThinkingOverride: "high",
|
|
121
|
+
consolidationTimeoutMs: 240000,
|
|
122
|
+
}));
|
|
123
|
+
fs.writeFileSync(TEST_SETTINGS_PATH, JSON.stringify({
|
|
124
|
+
theme: "dark",
|
|
125
|
+
hermesMemory: {
|
|
126
|
+
llmThinkingOverride: "off",
|
|
127
|
+
llmModelOverride: " tokenin/deepseek-v4.1-flash ",
|
|
128
|
+
consolidationTimeoutMs: 300000,
|
|
129
|
+
},
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const config = loadConfig(TEST_CONFIG_PATH, TEST_SETTINGS_PATH);
|
|
133
|
+
assert.strictEqual(config.llmThinkingOverride, "off");
|
|
134
|
+
assert.strictEqual(config.llmModelOverride, "tokenin/deepseek-v4.1-flash");
|
|
135
|
+
assert.strictEqual(config.consolidationTimeoutMs, 300000);
|
|
136
|
+
assert.strictEqual(config.memoryMode, "policy-only");
|
|
137
|
+
});
|
|
138
|
+
|
|
115
139
|
it("only accepts boolean quickCheckOnOpen overrides", () => {
|
|
116
140
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
117
141
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ quickCheckOnOpen: "false" }));
|
|
@@ -631,6 +631,7 @@ describe("registerConsolidateCommand", () => {
|
|
|
631
631
|
it("includes project memory when a project store is available", async () => {
|
|
632
632
|
let handler: any;
|
|
633
633
|
const notifications: string[] = [];
|
|
634
|
+
const statuses: Array<[string, string | undefined]> = [];
|
|
634
635
|
let projectReloaded = false;
|
|
635
636
|
|
|
636
637
|
const pi = {
|
|
@@ -655,7 +656,10 @@ describe("registerConsolidateCommand", () => {
|
|
|
655
656
|
registerConsolidateCommand(pi, mockStore, 60000, projectStore, "demo-project");
|
|
656
657
|
await handler({}, {
|
|
657
658
|
signal: undefined,
|
|
658
|
-
ui: {
|
|
659
|
+
ui: {
|
|
660
|
+
notify: (message: string) => { notifications.push(message); },
|
|
661
|
+
setStatus: (key: string, text: string | undefined) => { statuses.push([key, text]); },
|
|
662
|
+
},
|
|
659
663
|
});
|
|
660
664
|
|
|
661
665
|
assert.strictEqual(execCalls.length, 4, "should consolidate memory, user, failure, and project stores");
|
|
@@ -670,6 +674,8 @@ describe("registerConsolidateCommand", () => {
|
|
|
670
674
|
assert.ok(projectReloaded, "project store should reload after consolidation");
|
|
671
675
|
assert.ok(notifications.some((message) => message.includes("Starting memory consolidation")), "should show an initial progress notification");
|
|
672
676
|
assert.ok(notifications.some((message) => message.includes("⏳ Consolidating memory")), "should show per-target progress");
|
|
677
|
+
assert.ok(statuses.some(([key, text]) => key === "pi-hermes-memory:consolidation" && text?.includes("Consolidating memory")), "should publish busy progress");
|
|
678
|
+
assert.deepStrictEqual(statuses.at(-1), ["pi-hermes-memory:consolidation", undefined], "should clear busy progress when done");
|
|
673
679
|
const finalNotification = notifications[notifications.length - 1] ?? "";
|
|
674
680
|
assert.ok(finalNotification.includes("failure: ✅ consolidated"), "final notification should include failure result");
|
|
675
681
|
assert.ok(finalNotification.includes("project:demo-project: ✅ consolidated"), "final notification should include project result");
|
|
@@ -1186,7 +1186,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1186
1186
|
const deliveredEntry = { ...entry, message: injectedMessage, replyCommand };
|
|
1187
1187
|
replyTracker.queueTurnContext({ from: entry.from, message: injectedMessage, receivedAt: Date.now() });
|
|
1188
1188
|
const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
|
|
1189
|
-
|
|
1189
|
+
// The tool may be an inactive optional capability: say how to activate it, not just its call.
|
|
1190
|
+
const activateHint = pi.getActiveTools().includes("intercom")
|
|
1191
|
+
? ""
|
|
1192
|
+
: ' (not active yet: call capability_discover with name "intercom" first)';
|
|
1193
|
+
const replyInstruction = replyCommand ? `\n\nTo reply, use the intercom tool${activateHint}: ${replyCommand}` : "";
|
|
1190
1194
|
const deliveryMetadata = formatInboundDeliveryMetadata(injectedMessage);
|
|
1191
1195
|
pi.sendMessage(
|
|
1192
1196
|
{
|
|
@@ -201,17 +201,19 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
201
201
|
return { ok: false, error: "Structured single-child execution cannot be combined with workflow, workflowScript, or workflowScriptPath.", mode: "workflow" };
|
|
202
202
|
}
|
|
203
203
|
if (params.agent !== undefined || params.task !== undefined) {
|
|
204
|
-
if (typeof params.agent !== "string" || !params.agent.trim()) {
|
|
205
|
-
return { ok: false, error: "Structured single-child execution requires agent to be a non-empty string.", mode: "workflow" };
|
|
206
|
-
}
|
|
207
204
|
if (params.task !== undefined && typeof params.task !== "string") {
|
|
208
205
|
return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
|
|
209
206
|
}
|
|
207
|
+
// A task without an agent is left for Jev subagent routing; the executor rejects it when routing cannot choose.
|
|
208
|
+
const agentless = params.agent === undefined && typeof params.task === "string" && params.task.trim() !== "";
|
|
209
|
+
if (!agentless && (typeof params.agent !== "string" || !params.agent.trim())) {
|
|
210
|
+
return { ok: false, error: "Structured single-child execution requires agent to be a non-empty string.", mode: "workflow" };
|
|
211
|
+
}
|
|
210
212
|
return {
|
|
211
213
|
ok: true,
|
|
212
214
|
params: {
|
|
213
215
|
...params,
|
|
214
|
-
agent: params.agent.trim(),
|
|
216
|
+
...(typeof params.agent === "string" ? { agent: params.agent.trim() } : {}),
|
|
215
217
|
output: params.output === undefined ? true : params.output,
|
|
216
218
|
} as T,
|
|
217
219
|
};
|
|
@@ -281,7 +281,7 @@ const ControlOverrides = Type.Object({
|
|
|
281
281
|
|
|
282
282
|
const SubagentParamProperties = {
|
|
283
283
|
agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
|
|
284
|
-
task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action, workflowScript, or workflowScriptPath." })),
|
|
284
|
+
task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent unless Jev subagent routing is enabled, in which case agent may be omitted to let Jev choose by function; cannot combine with action, workflowScript, or workflowScriptPath." })),
|
|
285
285
|
extensionBindings: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Namespaced, bounded plain-JSON metadata delivered only to the child runtime. Namespace keys use package.name/1 syntax." })),
|
|
286
286
|
// Management action (when present, tool operates in management mode)
|
|
287
287
|
action: Type.Optional(Type.String({ minLength: 1,
|
|
@@ -66,6 +66,7 @@ import { retainLiveForegroundNestedRoute } from "../../integrations/pi-web-sessi
|
|
|
66
66
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
67
67
|
import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
|
|
68
68
|
import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
69
|
+
import { describeSubagentRouting, emitSubagentRoutingTelemetry, readSubagentRoutingSettings, routeSubagentLaunch, warnJevUnavailableOnce } from "../shared/jev-subagent-routing.ts";
|
|
69
70
|
import { isAgentContract } from "../shared/agent-contract.ts";
|
|
70
71
|
import { normalizeExtensionBindings, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
71
72
|
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, outputPathMappingFromTask, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
@@ -6637,11 +6638,35 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
6637
6638
|
if (cwdError) return buildRequestedModeError(effectiveParams, cwdError);
|
|
6638
6639
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
6639
6640
|
const discovered = deps.discoverAgents(effectiveCwd, scope, requestParentModel?.provider);
|
|
6640
|
-
|
|
6641
|
+
let discoveredAgents = discovered.agents;
|
|
6641
6642
|
const unknownAgentDiagnosticContext = diagnosticContextFromDiscovery(discovered, effectiveCwd, scope);
|
|
6642
6643
|
const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics, unknownAgentDiagnosticContext);
|
|
6643
6644
|
if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
|
|
6644
6645
|
effectiveParams = canonicalParams.params!;
|
|
6646
|
+
// Jev fills in what a public single-child call left open (agent, tools, model tier); opt-in.
|
|
6647
|
+
const routedSingle = publicExecution && !effectiveParams.chain?.length && !effectiveParams.tasks?.length && typeof effectiveParams.task === "string" && effectiveParams.task.trim() !== "";
|
|
6648
|
+
const jevRouting = routedSingle ? readSubagentRoutingSettings() : undefined;
|
|
6649
|
+
if (jevRouting) {
|
|
6650
|
+
const routed = await routeSubagentLaunch({
|
|
6651
|
+
task: effectiveParams.task!,
|
|
6652
|
+
agent: effectiveParams.agent,
|
|
6653
|
+
model: effectiveParams.model,
|
|
6654
|
+
agents: discoveredAgents,
|
|
6655
|
+
ceiling: intersectSubagentCapabilityCeilings(effectiveParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager))),
|
|
6656
|
+
// pi-subagents' ModelRegistry typings predate `complete`; the host registry has it, and a missing one is an abstention.
|
|
6657
|
+
}, ctx as unknown as Parameters<typeof routeSubagentLaunch>[1], jevRouting);
|
|
6658
|
+
emitSubagentRoutingTelemetry(deps.pi.events, routed);
|
|
6659
|
+
if (routed.abstained === "no-credential" && ctx.hasUI) warnJevUnavailableOnce(ctx.ui, jevRouting.connection.provider);
|
|
6660
|
+
discoveredAgents = routed.agents;
|
|
6661
|
+
if (routed.agent !== undefined) effectiveParams = { ...effectiveParams, agent: routed.agent };
|
|
6662
|
+
const summary = describeSubagentRouting(routed, jevRouting.tiers);
|
|
6663
|
+
if (summary && ctx.hasUI) ctx.ui.notify(summary, "info");
|
|
6664
|
+
}
|
|
6665
|
+
if (routedSingle && !effectiveParams.agent) {
|
|
6666
|
+
return buildRequestedModeError(effectiveParams, jevRouting
|
|
6667
|
+
? "Jev could not choose an agent for this task. Name one: call subagent with { action: \"list\", capabilities: true } to see them."
|
|
6668
|
+
: "Structured single-child execution requires agent; Jev subagent routing (jevAdvisory.routes.subagent.enabled) is off. Name an agent.");
|
|
6669
|
+
}
|
|
6645
6670
|
if (effectiveParams.worktree === undefined && deps.config.worktree !== undefined) {
|
|
6646
6671
|
effectiveParams = { ...effectiveParams, worktree: deps.config.worktree };
|
|
6647
6672
|
}
|