context-mode 1.0.157 → 1.0.159
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/codex/index.d.ts +4 -1
- package/build/adapters/codex/index.js +237 -45
- package/build/session/db.d.ts +35 -0
- package/build/session/db.js +71 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/cli.bundle.mjs +222 -202
- package/hooks/session-db.bundle.mjs +23 -6
- package/hooks/session-loaders.mjs +310 -6
- package/hooks/sessionstart.mjs +52 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +145 -125
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.159"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.159",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.159",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.159",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.159",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.159",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -42,7 +42,7 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
|
42
42
|
generateHookConfig(_pluginRoot: string): HookRegistration;
|
|
43
43
|
readSettings(): Record<string, unknown> | null;
|
|
44
44
|
writeSettings(_settings: Record<string, unknown>): void;
|
|
45
|
-
validateHooks(
|
|
45
|
+
validateHooks(pluginRoot: string): DiagnosticResult[];
|
|
46
46
|
checkPluginRegistration(): DiagnosticResult;
|
|
47
47
|
getInstalledVersion(): string;
|
|
48
48
|
configureAllHooks(pluginRoot: string): string[];
|
|
@@ -63,6 +63,9 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
|
|
|
63
63
|
private readHooksConfig;
|
|
64
64
|
private writeHooksConfig;
|
|
65
65
|
private upsertManagedHookEntry;
|
|
66
|
+
private removeManagedHookEntries;
|
|
67
|
+
private hasCodexPluginHookManifest;
|
|
68
|
+
private pruneStaleUserHookTrustState;
|
|
66
69
|
private isExpectedHookEntry;
|
|
67
70
|
private isManagedContextModeEntry;
|
|
68
71
|
private entryContainsManagedCommand;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Track: https://github.com/openai/codex/issues/18491
|
|
15
15
|
*/
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
|
|
18
18
|
import { resolve, dirname, join } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
|
|
@@ -100,6 +100,13 @@ function hasDeprecatedCodexHooksFeature(raw) {
|
|
|
100
100
|
const features = getTomlSection(raw, "features");
|
|
101
101
|
return features !== null && /^\s*codex_hooks\s*=\s*true\s*(?:#.*)?$/mi.test(features);
|
|
102
102
|
}
|
|
103
|
+
function hasCodexPluginEnabled(raw) {
|
|
104
|
+
const plugin = getTomlSection(raw, 'plugins."context-mode@context-mode"');
|
|
105
|
+
return plugin !== null && /^\s*enabled\s*=\s*true\s*(?:#.*)?$/mi.test(plugin);
|
|
106
|
+
}
|
|
107
|
+
function hasStandaloneContextModeMcp(raw) {
|
|
108
|
+
return getTomlSection(raw, "mcp_servers.context-mode") !== null;
|
|
109
|
+
}
|
|
103
110
|
function ensureCodexHooksFeature(raw) {
|
|
104
111
|
if (hasCodexHooksFeature(raw))
|
|
105
112
|
return { text: raw, changed: false };
|
|
@@ -129,6 +136,56 @@ function ensureCodexHooksFeature(raw) {
|
|
|
129
136
|
lines.splice(featuresIndex + 1, 0, "hooks = true");
|
|
130
137
|
return { text: lines.join(newline), changed: true };
|
|
131
138
|
}
|
|
139
|
+
function removeTomlSections(raw, shouldRemove) {
|
|
140
|
+
const newline = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
141
|
+
const lines = raw.split(/\r?\n/);
|
|
142
|
+
const out = [];
|
|
143
|
+
const removed = [];
|
|
144
|
+
let skipping = false;
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
147
|
+
if (section) {
|
|
148
|
+
const sectionName = section[1]?.trim() ?? "";
|
|
149
|
+
skipping = shouldRemove(sectionName);
|
|
150
|
+
if (skipping)
|
|
151
|
+
removed.push(sectionName);
|
|
152
|
+
}
|
|
153
|
+
if (!skipping)
|
|
154
|
+
out.push(line);
|
|
155
|
+
}
|
|
156
|
+
return { text: out.join(newline), removed };
|
|
157
|
+
}
|
|
158
|
+
function parseTomlQuotedString(raw) {
|
|
159
|
+
const trimmed = raw.trim();
|
|
160
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
|
|
161
|
+
return null;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(trimmed);
|
|
164
|
+
return typeof parsed === "string" ? parsed : null;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Codex hook-state keys are TOML quoted keys, not guaranteed JSON strings.
|
|
168
|
+
// Preserve Windows backslashes such as C:\Users\... even when they are not
|
|
169
|
+
// valid JSON escapes, while still handling the common escaped quote/slash.
|
|
170
|
+
let out = "";
|
|
171
|
+
let escaping = false;
|
|
172
|
+
for (const ch of trimmed.slice(1, -1)) {
|
|
173
|
+
if (escaping) {
|
|
174
|
+
out += ch === '"' || ch === "\\" ? ch : `\\${ch}`;
|
|
175
|
+
escaping = false;
|
|
176
|
+
}
|
|
177
|
+
else if (ch === "\\") {
|
|
178
|
+
escaping = true;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
out += ch;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (escaping)
|
|
185
|
+
out += "\\";
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
132
189
|
// ─────────────────────────────────────────────────────────
|
|
133
190
|
// Adapter implementation
|
|
134
191
|
// ─────────────────────────────────────────────────────────
|
|
@@ -384,9 +441,11 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
384
441
|
// manually or via the `codex` CLI tool.
|
|
385
442
|
}
|
|
386
443
|
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
387
|
-
validateHooks(
|
|
444
|
+
validateHooks(pluginRoot) {
|
|
388
445
|
const results = [];
|
|
389
446
|
const codexCliVersion = probeCodexCliVersion();
|
|
447
|
+
let settingsRaw = "";
|
|
448
|
+
let settingsReadable = false;
|
|
390
449
|
results.push({
|
|
391
450
|
check: "Codex CLI binary",
|
|
392
451
|
status: codexCliVersion ? "pass" : "warn",
|
|
@@ -396,9 +455,10 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
396
455
|
...(codexCliVersion ? {} : { fix: "Install Codex CLI or make codex available on PATH" }),
|
|
397
456
|
});
|
|
398
457
|
try {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
458
|
+
settingsRaw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
459
|
+
settingsReadable = true;
|
|
460
|
+
const enabled = hasCodexHooksFeature(settingsRaw);
|
|
461
|
+
const deprecatedOnly = !enabled && hasDeprecatedCodexHooksFeature(settingsRaw);
|
|
402
462
|
results.push({
|
|
403
463
|
check: "Codex hooks feature flag",
|
|
404
464
|
status: enabled ? "pass" : "fail",
|
|
@@ -418,8 +478,35 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
418
478
|
fix: "context-mode upgrade",
|
|
419
479
|
});
|
|
420
480
|
}
|
|
481
|
+
const expected = this.generateHookConfig("");
|
|
482
|
+
const codexPluginEnabled = settingsReadable && hasCodexPluginEnabled(settingsRaw);
|
|
483
|
+
const codexPluginHooksAvailable = codexPluginEnabled && this.hasCodexPluginHookManifest(pluginRoot);
|
|
484
|
+
if (codexPluginEnabled && !codexPluginHooksAvailable) {
|
|
485
|
+
results.push({
|
|
486
|
+
check: "Codex plugin hooks",
|
|
487
|
+
status: "fail",
|
|
488
|
+
message: `context-mode Codex plugin is enabled, but ${join(pluginRoot, ".codex-plugin", "hooks.json")} is missing`,
|
|
489
|
+
fix: "Reinstall or upgrade the context-mode Codex plugin",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (codexPluginEnabled && hasStandaloneContextModeMcp(settingsRaw)) {
|
|
493
|
+
results.push({
|
|
494
|
+
check: "Standalone MCP duplicate",
|
|
495
|
+
status: "warn",
|
|
496
|
+
message: "[mcp_servers.context-mode] is still registered while context-mode@context-mode is enabled; Codex may start both plugin and standalone MCP surfaces",
|
|
497
|
+
fix: "context-mode upgrade (removes the standalone Codex MCP registration when the plugin owns context-mode)",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
421
500
|
const hookConfig = this.readHooksConfig();
|
|
422
501
|
if (!hookConfig.ok) {
|
|
502
|
+
if (hookConfig.reason === "missing" && codexPluginHooksAvailable) {
|
|
503
|
+
const pluginHookChecks = Object.keys(expected).map((hookName) => ({
|
|
504
|
+
check: `${hookName} hook`,
|
|
505
|
+
status: "pass",
|
|
506
|
+
message: `${hookName} hook provided by context-mode@context-mode plugin`,
|
|
507
|
+
}));
|
|
508
|
+
return results.concat(pluginHookChecks);
|
|
509
|
+
}
|
|
423
510
|
if (hookConfig.reason === "missing") {
|
|
424
511
|
return results.concat([{
|
|
425
512
|
check: "Hooks config",
|
|
@@ -443,7 +530,7 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
443
530
|
fix: "Check permissions and file accessibility for hooks.json, then rerun context-mode upgrade if needed",
|
|
444
531
|
}]);
|
|
445
532
|
}
|
|
446
|
-
if (!hookConfig.config.hooks) {
|
|
533
|
+
if (!hookConfig.config.hooks && !codexPluginHooksAvailable) {
|
|
447
534
|
return results.concat([{
|
|
448
535
|
check: "Hooks config",
|
|
449
536
|
status: "fail",
|
|
@@ -451,24 +538,29 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
451
538
|
fix: `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
|
|
452
539
|
}]);
|
|
453
540
|
}
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
const actualEntries = hookConfig.config.hooks?.[hookName];
|
|
457
|
-
const expectedEntry = entries[0];
|
|
458
|
-
const ok = Array.isArray(actualEntries)
|
|
459
|
-
&& actualEntries.some((entry) => this.isExpectedHookEntry(hookName, entry, expectedEntry));
|
|
460
|
-
const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
|
|
461
|
-
return {
|
|
541
|
+
const hookChecks = codexPluginHooksAvailable
|
|
542
|
+
? Object.keys(expected).map((hookName) => ({
|
|
462
543
|
check: `${hookName} hook`,
|
|
463
|
-
status:
|
|
464
|
-
message:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
544
|
+
status: "pass",
|
|
545
|
+
message: `${hookName} hook provided by context-mode@context-mode plugin`,
|
|
546
|
+
}))
|
|
547
|
+
: Object.entries(expected).map(([hookName, entries]) => {
|
|
548
|
+
const actualEntries = hookConfig.config.hooks?.[hookName];
|
|
549
|
+
const expectedEntry = entries[0];
|
|
550
|
+
const ok = Array.isArray(actualEntries)
|
|
551
|
+
&& actualEntries.some((entry) => this.isExpectedHookEntry(hookName, entry, expectedEntry));
|
|
552
|
+
const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
|
|
553
|
+
return {
|
|
554
|
+
check: `${hookName} hook`,
|
|
555
|
+
status: (ok ? "pass" : missingStatus),
|
|
556
|
+
message: ok
|
|
557
|
+
? `${hookName} hook configured in ${this.getHooksPath()}`
|
|
558
|
+
: hookName === "PreCompact"
|
|
559
|
+
? `${hookName} hook missing or not pointing to context-mode; compaction snapshots require a Codex build that emits PreCompact`
|
|
560
|
+
: `${hookName} hook missing or not pointing to context-mode`,
|
|
561
|
+
fix: ok ? undefined : `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
|
|
562
|
+
};
|
|
563
|
+
});
|
|
472
564
|
// #603: surface duplicate context-mode entries per hook event. Codex fires
|
|
473
565
|
// every matching entry, so duplicates double the work, can saturate the
|
|
474
566
|
// MCP transport (`Transport closed`), and have been observed to inflate
|
|
@@ -488,16 +580,39 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
488
580
|
fix: "context-mode upgrade (collapses duplicate context-mode entries; preserves unrelated hooks)",
|
|
489
581
|
});
|
|
490
582
|
}
|
|
583
|
+
else if (codexPluginHooksAvailable && managedCount === 1) {
|
|
584
|
+
duplicateChecks.push({
|
|
585
|
+
check: `${hookName} plugin duplicate`,
|
|
586
|
+
status: "warn",
|
|
587
|
+
message: `${hookName} is configured in both ${this.getHooksPath()} and the context-mode Codex plugin; Codex will fire both hooks`,
|
|
588
|
+
fix: "context-mode upgrade (removes user config context-mode hooks; preserves unrelated hooks)",
|
|
589
|
+
});
|
|
590
|
+
}
|
|
491
591
|
}
|
|
492
592
|
return results.concat(hookChecks, duplicateChecks);
|
|
493
593
|
}
|
|
494
594
|
checkPluginRegistration() {
|
|
495
|
-
// Check for context-mode in [mcp_servers] section of config.toml
|
|
496
595
|
try {
|
|
497
596
|
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
498
|
-
const
|
|
597
|
+
const pluginEnabled = hasCodexPluginEnabled(raw);
|
|
598
|
+
const standaloneMcp = hasStandaloneContextModeMcp(raw);
|
|
499
599
|
const hasMcpSection = raw.includes("[mcp_servers]") || raw.includes("[mcp_servers.");
|
|
500
|
-
if (
|
|
600
|
+
if (pluginEnabled && standaloneMcp) {
|
|
601
|
+
return {
|
|
602
|
+
check: "MCP registration",
|
|
603
|
+
status: "warn",
|
|
604
|
+
message: "context-mode@context-mode plugin is enabled, but standalone [mcp_servers.context-mode] is also configured",
|
|
605
|
+
fix: "context-mode upgrade",
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if (pluginEnabled) {
|
|
609
|
+
return {
|
|
610
|
+
check: "MCP registration",
|
|
611
|
+
status: "pass",
|
|
612
|
+
message: "context-mode@context-mode plugin enabled",
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (standaloneMcp) {
|
|
501
616
|
return {
|
|
502
617
|
check: "MCP registration",
|
|
503
618
|
status: "pass",
|
|
@@ -536,6 +651,15 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
536
651
|
configureAllHooks(pluginRoot) {
|
|
537
652
|
const hookConfig = this.readHooksConfig();
|
|
538
653
|
const changes = [];
|
|
654
|
+
const settingsPath = this.getSettingsPath();
|
|
655
|
+
let settingsRaw = "";
|
|
656
|
+
try {
|
|
657
|
+
settingsRaw = readFileSync(settingsPath, "utf-8");
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
settingsRaw = "";
|
|
661
|
+
}
|
|
662
|
+
const codexPluginOwnsHooks = hasCodexPluginEnabled(settingsRaw) && this.hasCodexPluginHookManifest(pluginRoot);
|
|
539
663
|
let hookFile;
|
|
540
664
|
if (hookConfig.ok) {
|
|
541
665
|
hookFile = hookConfig.config;
|
|
@@ -555,31 +679,48 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
555
679
|
? hookFile.hooks
|
|
556
680
|
: {};
|
|
557
681
|
const desiredHooks = this.generateHookConfig(pluginRoot);
|
|
558
|
-
|
|
559
|
-
|
|
682
|
+
const hookChangeStart = changes.length;
|
|
683
|
+
if (codexPluginOwnsHooks) {
|
|
684
|
+
for (const hookName of Object.keys(desiredHooks)) {
|
|
685
|
+
this.removeManagedHookEntries(hooks, hookName, changes);
|
|
686
|
+
}
|
|
560
687
|
}
|
|
561
|
-
|
|
688
|
+
else {
|
|
689
|
+
for (const [hookName, entries] of Object.entries(desiredHooks)) {
|
|
690
|
+
this.upsertManagedHookEntry(hooks, hookName, entries[0], changes);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (changes.length > hookChangeStart) {
|
|
562
694
|
hookFile.hooks = hooks;
|
|
563
695
|
this.writeHooksConfig(hookFile);
|
|
564
|
-
changes.push(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
696
|
+
changes.push(codexPluginOwnsHooks
|
|
697
|
+
? `Removed duplicate context-mode user hooks from ${this.getHooksPath()}`
|
|
698
|
+
: `Wrote native Codex hooks to ${this.getHooksPath()}`);
|
|
699
|
+
}
|
|
700
|
+
let settingsText = ensureCodexHooksFeature(settingsRaw).text;
|
|
701
|
+
const enabledSettingsChanged = settingsText !== settingsRaw;
|
|
702
|
+
if (codexPluginOwnsHooks) {
|
|
703
|
+
const removedMcp = removeTomlSections(settingsText, (sectionName) => sectionName === "mcp_servers.context-mode"
|
|
704
|
+
|| sectionName.startsWith("mcp_servers.context-mode.tools."));
|
|
705
|
+
if (removedMcp.removed.length > 0) {
|
|
706
|
+
settingsText = removedMcp.text;
|
|
707
|
+
changes.push("Removed standalone Codex context-mode MCP registration");
|
|
708
|
+
}
|
|
709
|
+
const prunedTrust = this.pruneStaleUserHookTrustState(settingsText, hooks);
|
|
710
|
+
if (prunedTrust.removed.length > 0) {
|
|
711
|
+
settingsText = prunedTrust.text;
|
|
712
|
+
changes.push(`Removed ${prunedTrust.removed.length} stale Codex hook trust entr${prunedTrust.removed.length === 1 ? "y" : "ies"}`);
|
|
713
|
+
}
|
|
573
714
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
: `${enabledSettings.text}${newline}`;
|
|
715
|
+
if (settingsText !== settingsRaw) {
|
|
716
|
+
const newline = settingsText.includes("\r\n") ? "\r\n" : "\n";
|
|
717
|
+
const text = settingsText.endsWith("\n")
|
|
718
|
+
? settingsText
|
|
719
|
+
: `${settingsText}${newline}`;
|
|
580
720
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
581
721
|
writeFileSync(settingsPath, text, "utf-8");
|
|
582
|
-
|
|
722
|
+
if (enabledSettingsChanged)
|
|
723
|
+
changes.push("Enabled Codex hooks feature flag");
|
|
583
724
|
}
|
|
584
725
|
return changes;
|
|
585
726
|
}
|
|
@@ -681,6 +822,57 @@ export class CodexAdapter extends BaseAdapter {
|
|
|
681
822
|
}
|
|
682
823
|
hooks[hookName] = currentEntries;
|
|
683
824
|
}
|
|
825
|
+
removeManagedHookEntries(hooks, hookName, changes) {
|
|
826
|
+
const currentEntries = Array.isArray(hooks[hookName]) ? [...hooks[hookName]] : [];
|
|
827
|
+
const filtered = currentEntries.filter((entry) => !this.isManagedContextModeEntry(hookName, entry));
|
|
828
|
+
const removed = currentEntries.length - filtered.length;
|
|
829
|
+
if (removed === 0)
|
|
830
|
+
return;
|
|
831
|
+
if (filtered.length > 0) {
|
|
832
|
+
hooks[hookName] = filtered;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
delete hooks[hookName];
|
|
836
|
+
}
|
|
837
|
+
changes.push(`Removed ${removed} ${hookName} context-mode user hook${removed === 1 ? "" : "s"}`);
|
|
838
|
+
}
|
|
839
|
+
hasCodexPluginHookManifest(pluginRoot) {
|
|
840
|
+
return existsSync(join(pluginRoot, ".codex-plugin", "hooks.json"));
|
|
841
|
+
}
|
|
842
|
+
pruneStaleUserHookTrustState(settingsRaw, hooks) {
|
|
843
|
+
const hooksPath = this.normalizeCommand(this.getHooksPath());
|
|
844
|
+
const eventNames = {
|
|
845
|
+
post_compact: "PostCompact",
|
|
846
|
+
post_tool_use: "PostToolUse",
|
|
847
|
+
pre_compact: "PreCompact",
|
|
848
|
+
pre_tool_use: "PreToolUse",
|
|
849
|
+
session_start: "SessionStart",
|
|
850
|
+
stop: "Stop",
|
|
851
|
+
user_prompt_submit: "UserPromptSubmit",
|
|
852
|
+
};
|
|
853
|
+
return removeTomlSections(settingsRaw, (sectionName) => {
|
|
854
|
+
const prefix = "hooks.state.";
|
|
855
|
+
if (!sectionName.startsWith(prefix))
|
|
856
|
+
return false;
|
|
857
|
+
const key = parseTomlQuotedString(sectionName.slice(prefix.length));
|
|
858
|
+
if (key === null)
|
|
859
|
+
return false;
|
|
860
|
+
const normalized = this.normalizeCommand(key);
|
|
861
|
+
const parts = normalized.split(":");
|
|
862
|
+
const hookIndex = Number(parts.pop());
|
|
863
|
+
const entryIndex = Number(parts.pop());
|
|
864
|
+
const eventName = eventNames[parts.pop() ?? ""];
|
|
865
|
+
const stateHooksPath = parts.join(":");
|
|
866
|
+
if (stateHooksPath !== hooksPath
|
|
867
|
+
|| !eventName
|
|
868
|
+
|| !Number.isInteger(entryIndex)
|
|
869
|
+
|| !Number.isInteger(hookIndex)) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
const entry = hooks[eventName]?.[entryIndex];
|
|
873
|
+
return !entry || !Array.isArray(entry.hooks) || !entry.hooks[hookIndex];
|
|
874
|
+
});
|
|
875
|
+
}
|
|
684
876
|
isExpectedHookEntry(hookName, entry, expectedEntry) {
|
|
685
877
|
if (!entry || typeof entry !== "object")
|
|
686
878
|
return false;
|
package/build/session/db.d.ts
CHANGED
|
@@ -164,6 +164,28 @@ export interface SessionMeta {
|
|
|
164
164
|
event_count: number;
|
|
165
165
|
compact_count: number;
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Session rollup snapshot (seed-parity aggregate).
|
|
169
|
+
*
|
|
170
|
+
* 12 fields that mirror the platform's `session_summary` + `session_metadata`
|
|
171
|
+
* stamps from src/routes/seed.ts. Each outgoing canonical event carries
|
|
172
|
+
* this snapshot computed at the moment of forward so the analytics engine
|
|
173
|
+
* can run its SUM/AVG/MAX rollups across per-event rows.
|
|
174
|
+
*/
|
|
175
|
+
export interface SessionRollup {
|
|
176
|
+
tool_calls: number;
|
|
177
|
+
errors: number;
|
|
178
|
+
unique_tools: number;
|
|
179
|
+
unique_files: number;
|
|
180
|
+
max_file_edits: number;
|
|
181
|
+
has_commit: 0 | 1;
|
|
182
|
+
edit_test_cycles: number;
|
|
183
|
+
duration_min: number;
|
|
184
|
+
compact_count: number;
|
|
185
|
+
sources_indexed: number;
|
|
186
|
+
total_chunks: number;
|
|
187
|
+
search_queries: number;
|
|
188
|
+
}
|
|
167
189
|
/** Resume snapshot row from the session_resume table. */
|
|
168
190
|
export interface ResumeRow {
|
|
169
191
|
snapshot: string;
|
|
@@ -340,6 +362,19 @@ export declare class SessionDB extends SQLiteBase {
|
|
|
340
362
|
* Get session statistics/metadata.
|
|
341
363
|
*/
|
|
342
364
|
getSessionStats(sessionId: string): SessionMeta | null;
|
|
365
|
+
/**
|
|
366
|
+
* Session rollup snapshot — 12 aggregate fields the analytics platform
|
|
367
|
+
* stamps onto every outgoing event row (seed.ts shape parity).
|
|
368
|
+
*
|
|
369
|
+
* Called from session-loaders BEFORE `maybeForward`; the snapshot is
|
|
370
|
+
* computed against the LOCAL SessionDB and threaded into the canonical
|
|
371
|
+
* event so the platform-side Zod schema receives the rich shape without
|
|
372
|
+
* the bridge ever hand-mapping fields (PRD §5.4 ABI passthrough).
|
|
373
|
+
*
|
|
374
|
+
* Returns zeroed defaults for unknown sessions — callers MUST tolerate
|
|
375
|
+
* a snapshot from an empty session (first event into a fresh DB).
|
|
376
|
+
*/
|
|
377
|
+
getSessionRollup(sessionId: string): SessionRollup;
|
|
343
378
|
/**
|
|
344
379
|
* Increment the compact_count for a session (tracks snapshot rebuilds).
|
|
345
380
|
*/
|
package/build/session/db.js
CHANGED
|
@@ -477,6 +477,8 @@ const S = {
|
|
|
477
477
|
updateMetaLastEvent: "updateMetaLastEvent",
|
|
478
478
|
ensureSession: "ensureSession",
|
|
479
479
|
getSessionStats: "getSessionStats",
|
|
480
|
+
getSessionRollup: "getSessionRollup",
|
|
481
|
+
getMaxFileEdits: "getMaxFileEdits",
|
|
480
482
|
incrementCompactCount: "incrementCompactCount",
|
|
481
483
|
upsertResume: "upsertResume",
|
|
482
484
|
getResume: "getResume",
|
|
@@ -716,6 +718,35 @@ export class SessionDB extends SQLiteBase {
|
|
|
716
718
|
p(S.ensureSession, `INSERT OR IGNORE INTO session_meta (session_id, project_dir) VALUES (?, ?)`);
|
|
717
719
|
p(S.getSessionStats, `SELECT session_id, project_dir, started_at, last_event_at, event_count, compact_count
|
|
718
720
|
FROM session_meta WHERE session_id = ?`);
|
|
721
|
+
// ── Session rollup (seed-parity aggregator) ────────────────────────
|
|
722
|
+
// Single query producing 9 of the 12 platform-side session_summary +
|
|
723
|
+
// session_metadata fields. Computed against the local SessionDB
|
|
724
|
+
// session_events table at forward time so every outgoing canonical
|
|
725
|
+
// event carries a session-wide snapshot at that moment — matches the
|
|
726
|
+
// seed.ts shape where each event row has tool_calls/errors/etc. stamped.
|
|
727
|
+
// max_file_edits and edit_test_cycles need separate GROUP BY queries
|
|
728
|
+
// (below). compact_count is read from session_meta (already in getSessionStats).
|
|
729
|
+
p(S.getSessionRollup, `SELECT
|
|
730
|
+
COUNT(*) AS tool_calls,
|
|
731
|
+
COALESCE(SUM(CASE WHEN category = 'error' THEN 1 ELSE 0 END), 0) AS errors,
|
|
732
|
+
COUNT(DISTINCT type) AS unique_tools,
|
|
733
|
+
COUNT(DISTINCT CASE WHEN category = 'file' THEN data END) AS unique_files,
|
|
734
|
+
CASE WHEN SUM(CASE WHEN category = 'git' THEN 1 ELSE 0 END) > 0 THEN 1 ELSE 0 END AS has_commit,
|
|
735
|
+
CAST(COALESCE((MAX(strftime('%s', created_at)) - MIN(strftime('%s', created_at))) / 60.0, 0) AS INTEGER) AS duration_min,
|
|
736
|
+
COALESCE(SUM(CASE WHEN type = 'external_ref' THEN 1 ELSE 0 END), 0) AS sources_indexed,
|
|
737
|
+
CAST(COALESCE(SUM(bytes_avoided) / 1024.0, 0) AS INTEGER) AS total_chunks,
|
|
738
|
+
COALESCE(SUM(CASE WHEN type IN ('file_search', 'file_glob') THEN 1 ELSE 0 END), 0) AS search_queries
|
|
739
|
+
FROM session_events
|
|
740
|
+
WHERE session_id = ?`);
|
|
741
|
+
// max_file_edits: max edits on any single file path in the session.
|
|
742
|
+
// Two-level aggregation — GROUP BY data first, then MAX of those counts.
|
|
743
|
+
p(S.getMaxFileEdits, `SELECT COALESCE(MAX(c), 0) AS max_file_edits
|
|
744
|
+
FROM (
|
|
745
|
+
SELECT COUNT(*) AS c
|
|
746
|
+
FROM session_events
|
|
747
|
+
WHERE session_id = ? AND category = 'file' AND type IN ('file_edit', 'file_write')
|
|
748
|
+
GROUP BY data
|
|
749
|
+
)`);
|
|
719
750
|
p(S.incrementCompactCount, `UPDATE session_meta SET compact_count = compact_count + 1 WHERE session_id = ?`);
|
|
720
751
|
// ── Resume ──
|
|
721
752
|
p(S.upsertResume, `INSERT INTO session_resume (session_id, snapshot, event_count)
|
|
@@ -1017,6 +1048,46 @@ export class SessionDB extends SQLiteBase {
|
|
|
1017
1048
|
const row = this.stmt(S.getSessionStats).get(sessionId);
|
|
1018
1049
|
return row ?? null;
|
|
1019
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Session rollup snapshot — 12 aggregate fields the analytics platform
|
|
1053
|
+
* stamps onto every outgoing event row (seed.ts shape parity).
|
|
1054
|
+
*
|
|
1055
|
+
* Called from session-loaders BEFORE `maybeForward`; the snapshot is
|
|
1056
|
+
* computed against the LOCAL SessionDB and threaded into the canonical
|
|
1057
|
+
* event so the platform-side Zod schema receives the rich shape without
|
|
1058
|
+
* the bridge ever hand-mapping fields (PRD §5.4 ABI passthrough).
|
|
1059
|
+
*
|
|
1060
|
+
* Returns zeroed defaults for unknown sessions — callers MUST tolerate
|
|
1061
|
+
* a snapshot from an empty session (first event into a fresh DB).
|
|
1062
|
+
*/
|
|
1063
|
+
getSessionRollup(sessionId) {
|
|
1064
|
+
const main = this.stmt(S.getSessionRollup).get(sessionId);
|
|
1065
|
+
const maxRow = this.stmt(S.getMaxFileEdits).get(sessionId);
|
|
1066
|
+
const meta = this.getSessionStats(sessionId);
|
|
1067
|
+
// edit_test_cycles: heuristic — min(file edits, errors) approximates
|
|
1068
|
+
// the number of edit-then-test attempts in a session. Exact pattern
|
|
1069
|
+
// detection (consecutive file_edit followed by error_tool) would need
|
|
1070
|
+
// a windowed query; this scalar pair under-counts but never overshoots.
|
|
1071
|
+
const fileEdits = (main?.tool_calls ?? 0) > 0
|
|
1072
|
+
? (main?.unique_files ?? 0)
|
|
1073
|
+
: 0;
|
|
1074
|
+
const errors = main?.errors ?? 0;
|
|
1075
|
+
const editTestCycles = Math.min(fileEdits, errors);
|
|
1076
|
+
return {
|
|
1077
|
+
tool_calls: main?.tool_calls ?? 0,
|
|
1078
|
+
errors: main?.errors ?? 0,
|
|
1079
|
+
unique_tools: main?.unique_tools ?? 0,
|
|
1080
|
+
unique_files: main?.unique_files ?? 0,
|
|
1081
|
+
max_file_edits: maxRow?.max_file_edits ?? 0,
|
|
1082
|
+
has_commit: main?.has_commit ?? 0,
|
|
1083
|
+
edit_test_cycles: editTestCycles,
|
|
1084
|
+
duration_min: main?.duration_min ?? 0,
|
|
1085
|
+
compact_count: meta?.compact_count ?? 0,
|
|
1086
|
+
sources_indexed: main?.sources_indexed ?? 0,
|
|
1087
|
+
total_chunks: main?.total_chunks ?? 0,
|
|
1088
|
+
search_queries: main?.search_queries ?? 0,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1020
1091
|
/**
|
|
1021
1092
|
* Increment the compact_count for a session (tracks snapshot rebuilds).
|
|
1022
1093
|
*/
|