@pcircle/memesh 4.8.3 → 4.9.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/mcp.json +6 -4
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +68 -17
- package/README.de.md +83 -555
- package/README.md +83 -581
- package/README.zh-TW.md +84 -572
- package/dashboard/dist/index.html +10 -10
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +154 -384
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/agent-message-inbox.d.ts +2 -1
- package/dist/core/agent-message-inbox.d.ts.map +1 -1
- package/dist/core/agent-message-inbox.js +23 -4
- package/dist/core/agent-message-inbox.js.map +1 -1
- package/dist/core/agent-messaging.d.ts.map +1 -1
- package/dist/core/agent-messaging.js +23 -15
- package/dist/core/agent-messaging.js.map +1 -1
- package/dist/core/agent-router.d.ts +11 -10
- package/dist/core/agent-router.d.ts.map +1 -1
- package/dist/core/agent-router.js +29 -14
- package/dist/core/agent-router.js.map +1 -1
- package/dist/core/agent-scope-id.d.ts +11 -0
- package/dist/core/agent-scope-id.d.ts.map +1 -0
- package/dist/core/agent-scope-id.js +40 -0
- package/dist/core/agent-scope-id.js.map +1 -0
- package/dist/core/analytics.d.ts.map +1 -1
- package/dist/core/analytics.js.map +1 -1
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +8 -2
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/config.d.ts +4 -40
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +75 -141
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo.d.ts.map +1 -1
- package/dist/core/demo.js +6 -6
- package/dist/core/demo.js.map +1 -1
- package/dist/core/doctor.d.ts +2 -6
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +120 -154
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +32 -47
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +214 -704
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/install-channel.d.ts.map +1 -1
- package/dist/core/install-channel.js +4 -47
- package/dist/core/install-channel.js.map +1 -1
- package/dist/core/install-id.d.ts.map +1 -1
- package/dist/core/install-id.js.map +1 -1
- package/dist/core/kg-backfill.d.ts.map +1 -1
- package/dist/core/kg-backfill.js.map +1 -1
- package/dist/core/lesson-engine.d.ts +0 -5
- package/dist/core/lesson-engine.d.ts.map +1 -1
- package/dist/core/lesson-engine.js +0 -25
- package/dist/core/lesson-engine.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +58 -49
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core/memory-tool.d.ts.map +1 -1
- package/dist/core/memory-tool.js +20 -18
- package/dist/core/memory-tool.js.map +1 -1
- package/dist/core/operations.d.ts +3 -27
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +10 -245
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts +4 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +88 -14
- package/dist/core/paths.js.map +1 -1
- package/dist/core/product-improvements.js +2 -2
- package/dist/core/product-improvements.js.map +1 -1
- package/dist/core/project-tags.d.ts +2 -0
- package/dist/core/project-tags.d.ts.map +1 -1
- package/dist/core/project-tags.js +29 -1
- package/dist/core/project-tags.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +13 -3
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/semver.d.ts +7 -0
- package/dist/core/semver.d.ts.map +1 -0
- package/dist/core/semver.js +49 -0
- package/dist/core/semver.js.map +1 -0
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +69 -57
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/signal-scorer.d.ts.map +1 -1
- package/dist/core/signal-scorer.js.map +1 -1
- package/dist/core/transcript-extractor.d.ts +1 -85
- package/dist/core/transcript-extractor.d.ts.map +1 -1
- package/dist/core/transcript-extractor.js +5 -364
- package/dist/core/transcript-extractor.js.map +1 -1
- package/dist/core/transcript-source.d.ts +22 -6
- package/dist/core/transcript-source.d.ts.map +1 -1
- package/dist/core/transcript-source.js +108 -69
- package/dist/core/transcript-source.js.map +1 -1
- package/dist/core/types.d.ts +1 -17
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/version-check.d.ts +1 -0
- package/dist/core/version-check.d.ts.map +1 -1
- package/dist/core/version-check.js +46 -1
- package/dist/core/version-check.js.map +1 -1
- package/dist/db.d.ts +0 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +6 -287
- package/dist/db.js.map +1 -1
- package/dist/host-runtime/acp.d.ts.map +1 -1
- package/dist/host-runtime/acp.js +4 -3
- package/dist/host-runtime/acp.js.map +1 -1
- package/dist/host-runtime/claude.d.ts.map +1 -1
- package/dist/host-runtime/claude.js +11 -11
- package/dist/host-runtime/claude.js.map +1 -1
- package/dist/host-runtime/codex-session.d.ts +9 -1
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +474 -29
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/host-runtime/codex.d.ts.map +1 -1
- package/dist/host-runtime/codex.js +4 -3
- package/dist/host-runtime/codex.js.map +1 -1
- package/dist/host-runtime/config.d.ts +1 -0
- package/dist/host-runtime/config.d.ts.map +1 -1
- package/dist/host-runtime/config.js +4 -0
- package/dist/host-runtime/config.js.map +1 -1
- package/dist/host-runtime/entry.d.ts +5 -0
- package/dist/host-runtime/entry.d.ts.map +1 -0
- package/dist/host-runtime/entry.js +11 -0
- package/dist/host-runtime/entry.js.map +1 -0
- package/dist/host-runtime/router-client.d.ts.map +1 -1
- package/dist/host-runtime/router-client.js +62 -15
- package/dist/host-runtime/router-client.js.map +1 -1
- package/dist/host-runtime/router.js +2 -2
- package/dist/host-runtime/router.js.map +1 -1
- package/dist/knowledge-graph.d.ts +0 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +77 -60
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +217 -0
- package/dist/mcp/server.js +30685 -38
- package/dist/mcp/server.js.map +6 -1
- package/dist/skills-manifest.json +39 -34
- package/dist/storage/entity-index.d.ts +3 -0
- package/dist/storage/entity-index.d.ts.map +1 -0
- package/dist/storage/entity-index.js +8 -0
- package/dist/storage/entity-index.js.map +1 -0
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +11 -7
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/storage/graph-repairs.d.ts +7 -2
- package/dist/storage/graph-repairs.d.ts.map +1 -1
- package/dist/storage/graph-repairs.js +89 -13
- package/dist/storage/graph-repairs.js.map +1 -1
- package/dist/storage/schema.d.ts +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +1 -2
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/sqlite.d.ts +0 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transports/agent-messaging.d.ts.map +1 -1
- package/dist/transports/agent-messaging.js +5 -7
- package/dist/transports/agent-messaging.js.map +1 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +75 -686
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/retired-routes.js +1 -1
- package/dist/transports/http/retired-routes.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +13 -232
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +130 -4
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +56 -7
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +71 -19
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +57 -11
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +6 -5
- package/docs/platforms/agent-messaging.md +241 -20
- package/hooks/hooks.json +23 -2
- package/llms-install.md +62 -30
- package/package.json +12 -9
- package/scripts/hooks/_generated/agent-message-inbox.js +23 -4
- package/scripts/hooks/_generated/core-paths.js +88 -14
- package/scripts/hooks/_generated/fts-index.js +11 -7
- package/scripts/hooks/_generated/schema.js +1 -2
- package/scripts/hooks/_shared.js +65 -23
- package/scripts/hooks/decision-nudge.js +152 -0
- package/scripts/hooks/post-commit.js +11 -0
- package/scripts/hooks/pre-compact.js +12 -4
- package/scripts/hooks/session-start.js +32 -9
- package/scripts/hooks/session-summary.js +19 -374
- package/scripts/upgrade-plugin.sh +71 -2
- package/skills/memesh/SKILL.md +24 -15
- package/skills/memesh-review/SKILL.md +7 -6
- package/dist/core/auto-tagger.d.ts +0 -10
- package/dist/core/auto-tagger.d.ts.map +0 -1
- package/dist/core/auto-tagger.js +0 -63
- package/dist/core/auto-tagger.js.map +0 -1
- package/dist/core/conflict-candidates.d.ts +0 -20
- package/dist/core/conflict-candidates.d.ts.map +0 -1
- package/dist/core/conflict-candidates.js +0 -71
- package/dist/core/conflict-candidates.js.map +0 -1
- package/dist/core/conflict-judge.d.ts +0 -58
- package/dist/core/conflict-judge.d.ts.map +0 -1
- package/dist/core/conflict-judge.js +0 -189
- package/dist/core/conflict-judge.js.map +0 -1
- package/dist/core/digest-validator.d.ts +0 -18
- package/dist/core/digest-validator.d.ts.map +0 -1
- package/dist/core/digest-validator.js +0 -85
- package/dist/core/digest-validator.js.map +0 -1
- package/dist/core/embedder.d.ts +0 -20
- package/dist/core/embedder.d.ts.map +0 -1
- package/dist/core/embedder.js +0 -242
- package/dist/core/embedder.js.map +0 -1
- package/dist/core/failure-analyzer.d.ts +0 -19
- package/dist/core/failure-analyzer.d.ts.map +0 -1
- package/dist/core/failure-analyzer.js +0 -83
- package/dist/core/failure-analyzer.js.map +0 -1
- package/dist/core/json-utils.d.ts +0 -3
- package/dist/core/json-utils.d.ts.map +0 -1
- package/dist/core/json-utils.js +0 -46
- package/dist/core/json-utils.js.map +0 -1
- package/dist/core/llm-client.d.ts +0 -22
- package/dist/core/llm-client.d.ts.map +0 -1
- package/dist/core/llm-client.js +0 -203
- package/dist/core/llm-client.js.map +0 -1
- package/dist/core/llm-telemetry.d.ts +0 -47
- package/dist/core/llm-telemetry.d.ts.map +0 -1
- package/dist/core/llm-telemetry.js +0 -117
- package/dist/core/llm-telemetry.js.map +0 -1
- package/dist/core/llm-validator.d.ts +0 -20
- package/dist/core/llm-validator.d.ts.map +0 -1
- package/dist/core/llm-validator.js +0 -231
- package/dist/core/llm-validator.js.map +0 -1
- package/dist/core/ollama-host.d.ts +0 -6
- package/dist/core/ollama-host.d.ts.map +0 -1
- package/dist/core/ollama-host.js +0 -30
- package/dist/core/ollama-host.js.map +0 -1
- package/dist/core/output-language.d.ts +0 -6
- package/dist/core/output-language.d.ts.map +0 -1
- package/dist/core/output-language.js +0 -25
- package/dist/core/output-language.js.map +0 -1
- package/dist/core/prompt-safety.d.ts +0 -4
- package/dist/core/prompt-safety.d.ts.map +0 -1
- package/dist/core/prompt-safety.js +0 -20
- package/dist/core/prompt-safety.js.map +0 -1
- package/dist/storage/vector-index.d.ts +0 -3
- package/dist/storage/vector-index.d.ts.map +0 -1
- package/dist/storage/vector-index.js +0 -13
- package/dist/storage/vector-index.js.map +0 -1
- /package/{.mcp.json → .claude-plugin/mcp.json} +0 -0
|
@@ -11,6 +11,8 @@ import os from 'os';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
|
+
export const AGENT_ROUTER_SOCKET_FILENAME = 'agent-router-v2.sock';
|
|
15
|
+
const LEGACY_AGENT_ROUTER_SOCKET_FILENAME = 'agent-router.sock';
|
|
14
16
|
export function homeDir() {
|
|
15
17
|
const home = process.env.HOME;
|
|
16
18
|
if (home && home.length > 0)
|
|
@@ -31,6 +33,15 @@ export function getMemeshDirFromDbPath() {
|
|
|
31
33
|
? path.dirname(process.env.MEMESH_DB_PATH)
|
|
32
34
|
: memeshDir();
|
|
33
35
|
}
|
|
36
|
+
export function getAgentRouterSocketPath() {
|
|
37
|
+
return path.join(getMemeshDirFromDbPath(), AGENT_ROUTER_SOCKET_FILENAME);
|
|
38
|
+
}
|
|
39
|
+
export function normalizeAgentRouterSocketPath(socketPath) {
|
|
40
|
+
const dataDir = getMemeshDirFromDbPath();
|
|
41
|
+
return socketPath === path.join(dataDir, LEGACY_AGENT_ROUTER_SOCKET_FILENAME)
|
|
42
|
+
? getAgentRouterSocketPath()
|
|
43
|
+
: socketPath;
|
|
44
|
+
}
|
|
34
45
|
export function getProjectName(cwdInput) {
|
|
35
46
|
const cwd = cwdInput && cwdInput.length > 0 ? cwdInput : process.cwd();
|
|
36
47
|
const cached = projectNameCache.get(cwd);
|
|
@@ -44,22 +55,36 @@ const projectNameCache = new Map();
|
|
|
44
55
|
function resolveProjectIdentity(cwd) {
|
|
45
56
|
const remote = tryGit(cwd, ['config', '--get', 'remote.origin.url']);
|
|
46
57
|
if (remote) {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
58
|
+
const locator = canonicalRemoteLocator(remote);
|
|
59
|
+
if (locator) {
|
|
60
|
+
const label = path.posix.basename(locator).replace(/\.git$/i, '');
|
|
61
|
+
return projectIdentity(label, locator);
|
|
62
|
+
}
|
|
50
63
|
}
|
|
51
64
|
const root = tryGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
const commonDir = root
|
|
66
|
+
? tryGit(cwd, ['rev-parse', '--git-common-dir'])
|
|
67
|
+
: null;
|
|
68
|
+
const absoluteCommonDir = commonDir ? path.resolve(cwd, commonDir) : null;
|
|
69
|
+
const localPath = absoluteCommonDir && path.basename(absoluteCommonDir) === '.git'
|
|
70
|
+
? path.dirname(absoluteCommonDir)
|
|
71
|
+
: (root ?? cwd);
|
|
54
72
|
let real;
|
|
55
73
|
try {
|
|
56
|
-
real = fs.realpathSync.native(
|
|
74
|
+
real = fs.realpathSync.native(localPath);
|
|
57
75
|
}
|
|
58
76
|
catch {
|
|
59
|
-
real = path.resolve(
|
|
77
|
+
real = path.resolve(localPath);
|
|
60
78
|
}
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
return projectIdentity(path.basename(real), real);
|
|
80
|
+
}
|
|
81
|
+
const PROJECT_HASH_HEX_LENGTH = 32;
|
|
82
|
+
const PROJECT_ID_MAX_LENGTH = 200;
|
|
83
|
+
const PROJECT_LABEL_MAX_LENGTH = PROJECT_ID_MAX_LENGTH - PROJECT_HASH_HEX_LENGTH - 1;
|
|
84
|
+
function projectIdentity(label, locator) {
|
|
85
|
+
const readable = label.normalize('NFC').slice(0, PROJECT_LABEL_MAX_LENGTH) || 'project';
|
|
86
|
+
const suffix = createHash('sha256').update(locator).digest('hex').slice(0, PROJECT_HASH_HEX_LENGTH);
|
|
87
|
+
return `${readable}~${suffix}`;
|
|
63
88
|
}
|
|
64
89
|
function tryGit(cwd, args) {
|
|
65
90
|
try {
|
|
@@ -75,12 +100,61 @@ function tryGit(cwd, args) {
|
|
|
75
100
|
return null;
|
|
76
101
|
}
|
|
77
102
|
}
|
|
78
|
-
export function
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
103
|
+
export function canonicalRemoteLocator(remote) {
|
|
104
|
+
const value = remote.trim();
|
|
105
|
+
if (!value)
|
|
106
|
+
return null;
|
|
107
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value))
|
|
108
|
+
return null;
|
|
109
|
+
let host;
|
|
110
|
+
let port = '';
|
|
111
|
+
let user;
|
|
112
|
+
let remotePath;
|
|
113
|
+
let transport;
|
|
114
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value)) {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = new URL(value);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (parsed.protocol === 'file:' || !parsed.hostname)
|
|
123
|
+
return null;
|
|
124
|
+
host = parsed.hostname.toLowerCase();
|
|
125
|
+
port = parsed.port;
|
|
126
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
127
|
+
if ((protocol === 'ssh:' || protocol === 'git+ssh:') && port === '22')
|
|
128
|
+
port = '';
|
|
129
|
+
user = parsed.username;
|
|
130
|
+
remotePath = parsed.pathname;
|
|
131
|
+
transport = protocol === 'ssh:' || protocol === 'git+ssh:'
|
|
132
|
+
? 'ssh-absolute'
|
|
133
|
+
: protocol.slice(0, -1);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const scp = /^(?:([^@]+)@)?(\[[^\]]+\]|[^:/]+):(.+)$/.exec(value);
|
|
137
|
+
if (!scp)
|
|
138
|
+
return null;
|
|
139
|
+
user = scp[1] ?? '';
|
|
140
|
+
host = scp[2].toLowerCase();
|
|
141
|
+
remotePath = scp[3];
|
|
142
|
+
transport = remotePath.startsWith('/') ? 'ssh-absolute' : 'ssh-relative';
|
|
143
|
+
}
|
|
144
|
+
const pathWithoutSlashes = remotePath.replace(/^\/+|\/+$/g, '');
|
|
145
|
+
if (!host || !pathWithoutSlashes)
|
|
81
146
|
return null;
|
|
82
|
-
const
|
|
83
|
-
|
|
147
|
+
const endpoint = `${host}${port ? `:${port}` : ''}`;
|
|
148
|
+
const standardGithub = host === 'github.com'
|
|
149
|
+
&& port === ''
|
|
150
|
+
&& (transport === 'https' || ((transport === 'ssh-relative' || transport === 'ssh-absolute') && user === 'git'));
|
|
151
|
+
const normalizedPath = standardGithub
|
|
152
|
+
? pathWithoutSlashes.replace(/\.git$/i, '')
|
|
153
|
+
: pathWithoutSlashes;
|
|
154
|
+
if (standardGithub)
|
|
155
|
+
return `${endpoint}/${normalizedPath}`;
|
|
156
|
+
const authority = transport.startsWith('ssh-') && user ? `${user}@${endpoint}` : endpoint;
|
|
157
|
+
return `${transport}://${authority}/${normalizedPath}`;
|
|
84
158
|
}
|
|
85
159
|
export function _clearProjectNameCache() {
|
|
86
160
|
projectNameCache.clear();
|
|
@@ -67,18 +67,22 @@ function foldTitleIntoObservations(title, observationsText) {
|
|
|
67
67
|
}
|
|
68
68
|
export function removeFromFts(db, entityId, name, prevObsText, prevTitle) {
|
|
69
69
|
try {
|
|
70
|
+
const indexed = db
|
|
71
|
+
.prepare('SELECT COUNT(*) AS c FROM entities_fts WHERE rowid = ?')
|
|
72
|
+
.get(entityId);
|
|
73
|
+
if (!indexed || indexed.c === 0)
|
|
74
|
+
return;
|
|
70
75
|
db.prepare("INSERT INTO entities_fts (entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entityId, toIndexForm(name), toIndexForm(foldTitleIntoObservations(prevTitle, prevObsText)));
|
|
71
76
|
}
|
|
72
77
|
catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
try {
|
|
79
|
+
process.stderr.write(`[memesh fts-index] removeFromFts(rowid=${entityId}) failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
|
-
function isBenignFtsDeleteError(err) {
|
|
79
|
-
const msg = err?.message ?? '';
|
|
80
|
-
return /no such rowid|values do not match|no such row\b/i.test(msg);
|
|
81
|
-
}
|
|
82
86
|
export function joinIndexedObservations(contents) {
|
|
83
87
|
return contents.join(' ');
|
|
84
88
|
}
|
|
@@ -56,8 +56,7 @@ CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
|
|
|
56
56
|
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
|
|
57
57
|
CREATE INDEX IF NOT EXISTS idx_entities_type_created ON entities(type, created_at);
|
|
58
58
|
|
|
59
|
-
-- Migration markers and small bits of persistent state
|
|
60
|
-
-- version, embedding dimension, pending-reindex flags, backfill markers).
|
|
59
|
+
-- Migration markers and small bits of persistent state.
|
|
61
60
|
--
|
|
62
61
|
-- This used to be created ad hoc by each helper that needed it — four inline
|
|
63
62
|
-- CREATE TABLE IF NOT EXISTS copies in src/db.ts, none of them visible to
|
package/scripts/hooks/_shared.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
getMemeshDirFromDbPath,
|
|
32
32
|
getProjectName,
|
|
33
33
|
redactSecrets,
|
|
34
|
-
|
|
34
|
+
canonicalRemoteLocator,
|
|
35
35
|
} from './_generated/core-paths.js';
|
|
36
36
|
import { autoCaptureDecision } from './_generated/capture-flag.js';
|
|
37
37
|
export { assembleTopologyBlock, buildReferenceContext, extractCitedMemoryIds, DEFAULT_TOPOLOGY_BUDGET, GLOBAL_TOPOLOGY_LIMIT, SNIPPET_FETCH_CHARS, TOPOLOGY_CANDIDATE_CAP } from './_generated/work-topology.js';
|
|
@@ -126,15 +126,15 @@ import {
|
|
|
126
126
|
tokenizeQuery,
|
|
127
127
|
} from './_generated/fts-index.js';
|
|
128
128
|
|
|
129
|
-
export { homeDir, memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, redactSecrets,
|
|
129
|
+
export { homeDir, memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, redactSecrets, canonicalRemoteLocator };
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Resolve the package root from a hook file's `import.meta.url`.
|
|
133
133
|
*
|
|
134
134
|
* Hooks live at `<pkgRoot>/scripts/hooks/<file>.js`, so the path needs
|
|
135
135
|
* three `dirname()` hops to reach `<pkgRoot>`. Centralising this here
|
|
136
|
-
* prevents the off-by-one regression that silently disabled
|
|
137
|
-
*
|
|
136
|
+
* prevents the off-by-one regression that silently disabled hook-to-core
|
|
137
|
+
* imports between 4.0.4–4.1.0 (the inline
|
|
138
138
|
* `dirname(dirname(...))` only reached `<pkgRoot>/scripts`, and the
|
|
139
139
|
* surrounding `catch` swallowed the resulting ENOENT).
|
|
140
140
|
*
|
|
@@ -163,7 +163,7 @@ export function resolvePluginRoot(metaUrl) {
|
|
|
163
163
|
*
|
|
164
164
|
* That error is caught by each caller's surrounding try/catch and only
|
|
165
165
|
* traced to stderr, so on Windows every hook that reaches for a dist module
|
|
166
|
-
* —
|
|
166
|
+
* — lifecycle reads, lesson creation, and auto-decay —
|
|
167
167
|
* silently did nothing, while macOS/Linux and `memesh doctor` stayed green.
|
|
168
168
|
* A textbook fake-working boundary: the discipline of converting the path
|
|
169
169
|
* (already applied to the install-channel import above via
|
|
@@ -363,19 +363,15 @@ export function openHookDb(env = process.env, opts = {}) {
|
|
|
363
363
|
const dbDir = env.MEMESH_DB_PATH ? dirname(env.MEMESH_DB_PATH) : memeshDir();
|
|
364
364
|
if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
|
|
365
365
|
|
|
366
|
-
|
|
367
|
-
// `enableLoadExtension(true)`, and session-summary.js needs one to load
|
|
368
|
-
// sqlite-vec through this handle. The switch itself stays off.
|
|
369
|
-
const db = new MemeshDatabase(dbPath, { allowExtension: true });
|
|
366
|
+
const db = new MemeshDatabase(dbPath);
|
|
370
367
|
db.pragma('journal_mode = WAL');
|
|
371
368
|
db.pragma('foreign_keys = ON');
|
|
372
369
|
// A hook waits for a held write lock for less time than Claude Code will
|
|
373
370
|
// wait for the hook.
|
|
374
371
|
//
|
|
375
|
-
// `MemeshDatabase` sets `busy_timeout = 30000`,
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// server should WAIT for it rather than fail. A hook cannot. Its budget in
|
|
372
|
+
// `MemeshDatabase` sets `busy_timeout = 30000`, which lets ordinary CLI,
|
|
373
|
+
// MCP and HTTP operations wait through brief write contention. A hook
|
|
374
|
+
// cannot use that whole budget. Its timeout in
|
|
379
375
|
// `hooks/hooks.json` is 3s (UserPromptSubmit) to 10s (Stop, PreCompact),
|
|
380
376
|
// so a 30s wait has exactly one possible ending: the harness kills the
|
|
381
377
|
// hook. The capture is lost either way — the difference is that the user
|
|
@@ -559,9 +555,8 @@ export { truncateTitle } from './_generated/title.js';
|
|
|
559
555
|
* as two implementations of the same contract).
|
|
560
556
|
*
|
|
561
557
|
* The caller MUST open its DB with `openHookDb(env, { fts: true })` so the FTS
|
|
562
|
-
* table is guaranteed present.
|
|
563
|
-
*
|
|
564
|
-
* the heavier, user-initiated `remember` concerns (core owns them).
|
|
558
|
+
* table is guaranteed present. Signal scoring is deliberately not done here:
|
|
559
|
+
* hooks stay a cheap always-on capture path and core owns later enrichment.
|
|
565
560
|
*
|
|
566
561
|
* @param {import('./_generated/sqlite.js').MemeshDatabase} db - an open hook DB handle
|
|
567
562
|
* @param {{name: string, type: string, observations?: string[], tags?: string[], title?: string | null, metadata?: Record<string, unknown>}} entity
|
|
@@ -585,6 +580,11 @@ export function captureEntity(db, { name, type, observations = [], tags = [], ti
|
|
|
585
580
|
// retried, because the callers dedupe on the entity NAME existing —
|
|
586
581
|
// `INSERT OR IGNORE` reports "already there" on the next run and the
|
|
587
582
|
// half-written state is permanent.
|
|
583
|
+
//
|
|
584
|
+
// Returns `{ id, isNew, observationsWritten }`, or null when the entity row
|
|
585
|
+
// could not be resolved. `observationsWritten` may be lower than
|
|
586
|
+
// `observations.length`: an observation whose exact content is already on
|
|
587
|
+
// the entity is not stored again (see the dedupe in captureEntityInner).
|
|
588
588
|
return db.transaction(() => captureEntityInner(db, { name, type, observations, tags, title, metadata }))();
|
|
589
589
|
}
|
|
590
590
|
|
|
@@ -596,10 +596,9 @@ function captureEntityInner(db, { name, type, observations, tags, title, metadat
|
|
|
596
596
|
// earlier writer (possibly another host, via MCP) already recorded.
|
|
597
597
|
//
|
|
598
598
|
// title_source: every title a hook writes is machine-derived, so it is
|
|
599
|
-
// marked 'heuristic'. The mark
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
// here would make today's date+verb titles permanent.
|
|
599
|
+
// marked 'heuristic'. The mark distinguishes generated display text from an
|
|
600
|
+
// unmarked human-provided title, so later reviewed edits can preserve the
|
|
601
|
+
// ownership boundary.
|
|
603
602
|
const insertMetadata = { ...(metadata ?? {}), provenance: { source_host: 'claude-code' } };
|
|
604
603
|
if (title != null) insertMetadata.title_source = 'heuristic';
|
|
605
604
|
const insertResult = db
|
|
@@ -655,8 +654,47 @@ function captureEntityInner(db, { name, type, observations, tags, title, metadat
|
|
|
655
654
|
// BY + the one join rule), via the generated fts-index copy.
|
|
656
655
|
const prevObsText = isNew ? undefined : indexedObservationText(db, id);
|
|
657
656
|
|
|
657
|
+
// Never store the same sentence twice on one entity (#240, widened).
|
|
658
|
+
//
|
|
659
|
+
// #240 was fixed in session-summary.js alone, with an EXISTENCE guard: "if
|
|
660
|
+
// this session already has an entity, bail". Its two siblings never got one
|
|
661
|
+
// and wrote 2,202 duplicate rows on the maintainer's graph —
|
|
662
|
+
// `pre-compact-<sessionId>` 2,188 of them (worst entity: 220 observations,
|
|
663
|
+
// 2 distinct), `commit-<sha>` 14.
|
|
664
|
+
//
|
|
665
|
+
// The guard belongs HERE, on CONTENT, not there, on existence, because a
|
|
666
|
+
// pre-compact entity is per-session and a session legitimately compacts
|
|
667
|
+
// more than once: entity 1947's 110 captures span four days. An existence
|
|
668
|
+
// guard would have dropped a second, genuinely different compaction
|
|
669
|
+
// ("Tool calls: 47", "Files edited: …") along with the identical ones. A
|
|
670
|
+
// content guard drops only rows that add nothing — a re-write of a sentence
|
|
671
|
+
// already stored carries no information, whatever produced it.
|
|
672
|
+
//
|
|
673
|
+
// Unconditional rather than opt-in: every caller of this function is an
|
|
674
|
+
// auto-capture hook writing machine-derived facts, and none of them has a
|
|
675
|
+
// case where re-storing an identical string means something. A flag two of
|
|
676
|
+
// three callers pass would be one more proxy for the question.
|
|
677
|
+
//
|
|
678
|
+
// Filtered ONCE, up front, because `allObsText` below composes the FTS text
|
|
679
|
+
// from this list rather than re-reading the rows. Filtering only at the
|
|
680
|
+
// insert loop would index text for rows that do not exist, and
|
|
681
|
+
// `entities_fts` is contentless: the next delete would not match, which is
|
|
682
|
+
// the "database disk image is malformed" failure this file warns about
|
|
683
|
+
// above. The `seen` set also collapses repeats WITHIN one call.
|
|
684
|
+
const seen = new Set(
|
|
685
|
+
isNew
|
|
686
|
+
? []
|
|
687
|
+
: db.prepare('SELECT content FROM observations WHERE entity_id = ?').all(id).map((r) => r.content),
|
|
688
|
+
);
|
|
689
|
+
const freshObservations = [];
|
|
690
|
+
for (const obs of observations) {
|
|
691
|
+
if (seen.has(obs)) continue;
|
|
692
|
+
seen.add(obs);
|
|
693
|
+
freshObservations.push(obs);
|
|
694
|
+
}
|
|
695
|
+
|
|
658
696
|
const insertObs = db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)');
|
|
659
|
-
for (const obs of
|
|
697
|
+
for (const obs of freshObservations) insertObs.run(id, obs);
|
|
660
698
|
const insertTag = db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)');
|
|
661
699
|
for (const tag of tags) insertTag.run(id, tag);
|
|
662
700
|
|
|
@@ -673,7 +711,7 @@ function captureEntityInner(db, { name, type, observations, tags, title, metadat
|
|
|
673
711
|
// upserted entity's accumulated observation count.
|
|
674
712
|
const obsParts = [];
|
|
675
713
|
if (prevObsText) obsParts.push(prevObsText);
|
|
676
|
-
if (
|
|
714
|
+
if (freshObservations.length) obsParts.push(joinIndexedObservations(freshObservations));
|
|
677
715
|
const allObsText = joinIndexedObservations(obsParts);
|
|
678
716
|
// Current title is fully determined by the branches above — no re-read.
|
|
679
717
|
const currentTitle = isNew
|
|
@@ -681,7 +719,11 @@ function captureEntityInner(db, { name, type, observations, tags, title, metadat
|
|
|
681
719
|
: ((title !== undefined && title !== previousTitle) ? title : previousTitle);
|
|
682
720
|
insertFtsRow(db, id, name, allObsText, currentTitle);
|
|
683
721
|
|
|
684
|
-
|
|
722
|
+
// `observationsWritten` is what actually landed, which is no longer the same
|
|
723
|
+
// as `observations.length` once the dedupe above can drop rows. A caller
|
|
724
|
+
// that reports a count to the user must read this one — pre-compact.js
|
|
725
|
+
// printed "Saved 2 observations" on a run that stored none.
|
|
726
|
+
return { id, isNew, observationsWritten: freshObservations.length };
|
|
685
727
|
}
|
|
686
728
|
|
|
687
729
|
const PRIVATE_DIR_MODE = 0o700;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Decision Nudge — PostToolUse hook for ExitPlanMode and AskUserQuestion (#277).
|
|
4
|
+
//
|
|
5
|
+
// MeMesh's read side is automatic (SessionStart, PreToolUse Edit/Write inject
|
|
6
|
+
// memories), but the write side had no in-flow trigger: an agent could make
|
|
7
|
+
// several decisions in a session and store none of them until the user said
|
|
8
|
+
// "remember this". `ExitPlanMode` (a plan just got approved) and
|
|
9
|
+
// `AskUserQuestion` (the user just chose between options) are the two tool
|
|
10
|
+
// calls where a decision is most likely to have just been made, so this hook
|
|
11
|
+
// fires there and reminds the model — once per tool per session — to use
|
|
12
|
+
// `remember` if the decision is worth keeping.
|
|
13
|
+
//
|
|
14
|
+
// Contract, in order of importance:
|
|
15
|
+
// 1. NEVER block or slow the tool call: every failure path is a silent
|
|
16
|
+
// pass (with a stderr trace for anything that looks like schema
|
|
17
|
+
// drift), and this hook only ever emits additionalContext — exit code
|
|
18
|
+
// stays 0 no matter what.
|
|
19
|
+
// 2. NEVER open the database. Unlike the capture hooks (post-commit,
|
|
20
|
+
// session-summary, pre-compact), this hook writes nothing to MeMesh
|
|
21
|
+
// itself — it only reminds the model to. Nothing here needs a
|
|
22
|
+
// knowledge-graph handle, so nothing opens one; that keeps the hook
|
|
23
|
+
// fast (well under its 5s budget) and removes an entire class of
|
|
24
|
+
// failure (lock contention, migration, corrupt schema) from a hook
|
|
25
|
+
// whose only job is a one-line reminder.
|
|
26
|
+
// 3. Rate-limited to at most once per tool per session, or a plan-heavy
|
|
27
|
+
// session would see the same line after every ExitPlanMode call. A
|
|
28
|
+
// per-(session, tool) flag file under MEMESH_DIR is the mechanism —
|
|
29
|
+
// see claimNudge() for why, and why it is a NEW mechanism rather than
|
|
30
|
+
// a revived one.
|
|
31
|
+
|
|
32
|
+
import { openSync, closeSync, writeSync, constants as fsConstants } from 'fs';
|
|
33
|
+
import { join } from 'path';
|
|
34
|
+
import { ensurePrivateDir, getMemeshDirFromDbPath } from './_shared.js';
|
|
35
|
+
|
|
36
|
+
// The only two tools this hook is wired to in hooks/hooks.json — kept as an
|
|
37
|
+
// explicit allowlist (not "any PostToolUse call") so a future matcher typo
|
|
38
|
+
// or a Claude Code tool rename fails closed (no nudge) rather than nudging
|
|
39
|
+
// on every tool call.
|
|
40
|
+
const TARGET_TOOLS = new Set(['ExitPlanMode', 'AskUserQuestion']);
|
|
41
|
+
|
|
42
|
+
// Real Claude Code session ids are UUIDs; test fixtures use short slugs like
|
|
43
|
+
// `contract-8`. Restricting to this set is what makes the id safe to embed
|
|
44
|
+
// directly in a filename below — anything that fails this (including a
|
|
45
|
+
// missing/non-string id) exits quietly rather than touching the filesystem.
|
|
46
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
47
|
+
|
|
48
|
+
// Bounded stdin read. The real payload here (tool_name, tool_input,
|
|
49
|
+
// session_id, cwd) is a few KB at most — ExitPlanMode's plan text is the
|
|
50
|
+
// largest field — so 1 MiB is generous headroom, not a working limit. This
|
|
51
|
+
// stops a malformed/runaway payload from growing an unbounded string before
|
|
52
|
+
// JSON.parse ever runs.
|
|
53
|
+
const MAX_STDIN_BYTES = 1_048_576;
|
|
54
|
+
|
|
55
|
+
let input = '';
|
|
56
|
+
let overflowed = false;
|
|
57
|
+
process.stdin.setEncoding('utf8');
|
|
58
|
+
process.stdin.on('data', (chunk) => {
|
|
59
|
+
if (overflowed) return;
|
|
60
|
+
input += chunk;
|
|
61
|
+
if (input.length > MAX_STDIN_BYTES) overflowed = true;
|
|
62
|
+
});
|
|
63
|
+
process.stdin.on('end', () => {
|
|
64
|
+
try {
|
|
65
|
+
if (overflowed) return pass();
|
|
66
|
+
|
|
67
|
+
const data = JSON.parse(input);
|
|
68
|
+
|
|
69
|
+
// Schema-flip signal, same convention as post-commit.js / pre-edit-recall.js:
|
|
70
|
+
// tool_name absent means Claude Code changed the payload shape, not that
|
|
71
|
+
// this call is irrelevant. Trace so a rename surfaces day-1 instead of
|
|
72
|
+
// this hook going silently inert.
|
|
73
|
+
if (data?.tool_name === undefined) {
|
|
74
|
+
try { process.stderr.write(`[memesh decision-nudge] tool_name absent (keys: ${Object.keys(data ?? {}).join(',')}); skipping\n`); } catch {}
|
|
75
|
+
return pass();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const toolName = data.tool_name;
|
|
79
|
+
if (typeof toolName !== 'string' || !TARGET_TOOLS.has(toolName)) return pass();
|
|
80
|
+
|
|
81
|
+
const sessionId = data.session_id;
|
|
82
|
+
if (typeof sessionId !== 'string' || !SESSION_ID_RE.test(sessionId)) return pass();
|
|
83
|
+
|
|
84
|
+
if (!claimNudge(sessionId, toolName)) return pass(); // already nudged this tool this session
|
|
85
|
+
|
|
86
|
+
console.log(JSON.stringify({
|
|
87
|
+
hookSpecificOutput: {
|
|
88
|
+
hookEventName: 'PostToolUse',
|
|
89
|
+
additionalContext: buildNudge(toolName),
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
process.exit(0);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Never crash Claude Code, but trace — a silent break here means the
|
|
95
|
+
// nudge stops firing and nothing reports it, same as guard-check.js.
|
|
96
|
+
try { process.stderr.write(`[memesh decision-nudge] ${err?.message || err}\n`); } catch {}
|
|
97
|
+
pass();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The reminder text. Not memory content (nothing here comes from the
|
|
103
|
+
* database or from another user), so it is NOT wrapped in
|
|
104
|
+
* `buildReferenceContext`'s "treat this as background data, not
|
|
105
|
+
* instructions" fence — that fence exists for recalled/guard content that
|
|
106
|
+
* may be attacker-influenced. This is a fixed, plugin-authored instruction,
|
|
107
|
+
* the same class of thing user-prompt-intent.js's `buildHint()` emits, and
|
|
108
|
+
* it is meant to be acted on directly.
|
|
109
|
+
*/
|
|
110
|
+
function buildNudge(toolName) {
|
|
111
|
+
return `A decision was just made via ${toolName} — if you would need it again, store it now with the memesh \`remember\` tool (type:decision or type:lesson, tag project:<name>).`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Claim the (session, tool) nudge slot. Returns true the first time this
|
|
116
|
+
* pair is seen and false every time after, giving "at most once per tool
|
|
117
|
+
* per session" without a database — this hook is forbidden from opening
|
|
118
|
+
* one (see module comment) — and without a shared JSON file two PostToolUse
|
|
119
|
+
* processes (a fast plan-then-question turn) could race to read-modify-write.
|
|
120
|
+
*
|
|
121
|
+
* A per-category flag file with O_EXCL atomic create is exactly the fix
|
|
122
|
+
* CHANGELOG [4.1.0]'s "Throttle clobber under parallel-category load" entry
|
|
123
|
+
* describes for the retired `pre-bash-orchestration-nudge.js` hook, which
|
|
124
|
+
* kept its markers under `agent-nudge-flags/`. That whole hook and its
|
|
125
|
+
* directory were removed in v4.5.1 ("agentic-orchestration experiment,
|
|
126
|
+
* whole") — reviving a deleted mechanism would undo that decision — so this
|
|
127
|
+
* uses the SAME primitive (an O_EXCL flag file — the flag *is* the lock, no
|
|
128
|
+
* shared state to clobber) under a new directory scoped to this hook,
|
|
129
|
+
* `decision-nudge-flags/`. The directory is created private (0700) via
|
|
130
|
+
* `ensurePrivateDir`, matching every other per-hook marker under MEMESH_DIR.
|
|
131
|
+
*/
|
|
132
|
+
function claimNudge(sessionId, toolName) {
|
|
133
|
+
const dir = join(getMemeshDirFromDbPath(), 'decision-nudge-flags');
|
|
134
|
+
const flagPath = join(dir, `${sessionId}-${toolName}.flag`);
|
|
135
|
+
try {
|
|
136
|
+
ensurePrivateDir(dir);
|
|
137
|
+
const fd = openSync(flagPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
|
|
138
|
+
try { writeSync(fd, String(Date.now())); } finally { closeSync(fd); }
|
|
139
|
+
return true;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err?.code === 'EEXIST') return false; // already nudged this session/tool
|
|
142
|
+
// Any other failure (disk full, permission denied) must not block the
|
|
143
|
+
// tool call, and the safe default for a hint is silence rather than a
|
|
144
|
+
// duplicate-nudge storm — so this counts as "already claimed".
|
|
145
|
+
try { process.stderr.write(`[memesh decision-nudge] could not claim ${flagPath}: ${err?.message || err}\n`); } catch {}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pass() {
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
@@ -168,6 +168,17 @@ process.stdin.on('end', () => {
|
|
|
168
168
|
// TYPE, which a hand-typed `memesh learn` satisfied all by itself.
|
|
169
169
|
// The commit subject IS the title — git authors already wrote a
|
|
170
170
|
// one-line human summary; nothing to synthesize.
|
|
171
|
+
//
|
|
172
|
+
// Re-capture of an already-stored commit is expected and harmless: this
|
|
173
|
+
// hook runs on PostToolUse, so any later Bash call while HEAD is
|
|
174
|
+
// unchanged rebuilds the same `commit-<sha>` payload. It used to APPEND
|
|
175
|
+
// it — three commit entities on the maintainer's graph reached 6
|
|
176
|
+
// observations / 3 distinct, one triple re-written 107 seconds after the
|
|
177
|
+
// first. captureEntity now refuses to store an observation whose exact
|
|
178
|
+
// content is already on the entity (#240, widened), which is the right
|
|
179
|
+
// guard here rather than "skip if the entity exists": a sha is immutable,
|
|
180
|
+
// so any line that DOES differ on a later run (diff stats that failed the
|
|
181
|
+
// first time and succeeded now) is new information and must still land.
|
|
171
182
|
const written = captureEntity(db, {
|
|
172
183
|
name: entityName,
|
|
173
184
|
type: 'commit',
|
|
@@ -159,12 +159,20 @@ process.stdin.on('end', () => {
|
|
|
159
159
|
// it announced a save that may not have happened, with a number that never
|
|
160
160
|
// matched the one entity and handful of observations actually written.
|
|
161
161
|
//
|
|
162
|
-
// `
|
|
163
|
-
//
|
|
164
|
-
//
|
|
162
|
+
// `written.observationsWritten` is the honest count, NOT `obsLines.length`:
|
|
163
|
+
// a session compacts more than once, and the second compaction's lines are
|
|
164
|
+
// usually word-for-word the first's ("Compaction reason: auto", "Tool
|
|
165
|
+
// calls: 0"), which captureEntity now declines to store twice. Reporting
|
|
166
|
+
// the built count would announce "Saved 2 observations" on a run that
|
|
167
|
+
// stored none — the same success-shaped lie this comment block exists to
|
|
168
|
+
// stop. Zero written is still a successful run: the memory is already
|
|
169
|
+
// there.
|
|
170
|
+
const stored = written ? written.observationsWritten : 0;
|
|
165
171
|
const hookOutput = {
|
|
166
172
|
systemMessage: written
|
|
167
|
-
?
|
|
173
|
+
? (stored > 0
|
|
174
|
+
? `Saved ${stored} observations to MeMesh before compaction`
|
|
175
|
+
: 'MeMesh: this compaction added nothing new (already captured)')
|
|
168
176
|
: 'MeMesh: could not save pre-compaction insights (see stderr)',
|
|
169
177
|
};
|
|
170
178
|
console.log(JSON.stringify(hookOutput));
|
|
@@ -334,11 +334,24 @@ function detectInstallChannelHook(pluginRoot) {
|
|
|
334
334
|
* Claude Code wording rather than suppressing the hint entirely — the
|
|
335
335
|
* banner is more useful naming the majority host than naming none.
|
|
336
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* Which plugin host is this install under — or `'unknown'` when we could not
|
|
339
|
+
* ask.
|
|
340
|
+
*
|
|
341
|
+
* `null` is a real answer from `detectPluginHost`: "this path is not under any
|
|
342
|
+
* plugin cache". So `detectPluginHost?.(root) ?? null` inside a bare catch,
|
|
343
|
+
* which is what this was, gave a module that failed to load and a detector
|
|
344
|
+
* that threw the SAME value as a confident "not Codex" — and
|
|
345
|
+
* `pluginUpgradeLine` then handed every Codex user the Claude Code command.
|
|
346
|
+
* `getCurrentInstallChannel` above already returns `'unknown'` for exactly
|
|
347
|
+
* this state; this now matches it.
|
|
348
|
+
*/
|
|
337
349
|
function pluginHostOf(pluginRoot) {
|
|
350
|
+
if (typeof _installChannelMod?.detectPluginHost !== 'function') return 'unknown';
|
|
338
351
|
try {
|
|
339
|
-
return _installChannelMod
|
|
352
|
+
return _installChannelMod.detectPluginHost(pluginRoot);
|
|
340
353
|
} catch {
|
|
341
|
-
return
|
|
354
|
+
return 'unknown';
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
|
|
@@ -354,9 +367,17 @@ function pluginHostOf(pluginRoot) {
|
|
|
354
367
|
* happened. One owner, so it cannot happen again.
|
|
355
368
|
*/
|
|
356
369
|
function pluginUpgradeLine(pluginRoot) {
|
|
357
|
-
|
|
370
|
+
const host = pluginHostOf(pluginRoot);
|
|
371
|
+
if (host === 'codex') {
|
|
358
372
|
return ` Run: codex plugin marketplace upgrade pcircle-memesh && codex plugin add memesh@pcircle-memesh`;
|
|
359
373
|
}
|
|
374
|
+
if (host === 'unknown') {
|
|
375
|
+
// Naming one host's command here would be a guess presented as an
|
|
376
|
+
// instruction. Rare — it needs `dist/core/install-channel.js` to be
|
|
377
|
+
// missing or unloadable — but that is a broken install, which is exactly
|
|
378
|
+
// when wrong upgrade advice costs the most.
|
|
379
|
+
return ` Run: memesh upgrade-plugin (on Codex: codex plugin marketplace upgrade pcircle-memesh && codex plugin add memesh@pcircle-memesh)`;
|
|
380
|
+
}
|
|
360
381
|
return ` Run: memesh upgrade-plugin (no CLI? npx @pcircle/memesh upgrade-plugin — or reinstall from /plugin UI)`;
|
|
361
382
|
}
|
|
362
383
|
|
|
@@ -1021,7 +1042,7 @@ process.stdin.on('end', async () => {
|
|
|
1021
1042
|
// nothing — a fenced block containing only a branch name tells the
|
|
1022
1043
|
// agent something it can already see. briefing.test.ts's parity case
|
|
1023
1044
|
// is what keeps this identical to the tool side.
|
|
1024
|
-
let memoryContext =
|
|
1045
|
+
let memoryContext = workPackageGuidance;
|
|
1025
1046
|
if (memoryLines.length > 0) {
|
|
1026
1047
|
const repoLines = repoStateLines(readRepoState(data.cwd));
|
|
1027
1048
|
if (repoLines.length > 0) memoryLines.unshift(...repoLines, '');
|
|
@@ -1034,7 +1055,7 @@ process.stdin.on('end', async () => {
|
|
|
1034
1055
|
// charges task state plus project/foreign sections against the main
|
|
1035
1056
|
// ceiling and global context against its small additive ceiling. It
|
|
1036
1057
|
// returns whole lines only, so the closing fence cannot be cut.
|
|
1037
|
-
memoryContext = buildReferenceContext(memoryLines);
|
|
1058
|
+
memoryContext = buildReferenceContext(memoryLines) + '\n\n' + workPackageGuidance;
|
|
1038
1059
|
// The citation contract — OUTSIDE the fence on purpose: the fence
|
|
1039
1060
|
// declares its content "background data, not instructions", and
|
|
1040
1061
|
// this line IS an instruction. One line is the entire write side of
|
|
@@ -1200,9 +1221,9 @@ process.stdin.on('end', async () => {
|
|
|
1200
1221
|
} catch (err) {
|
|
1201
1222
|
// Non-critical — noise compression failed, will retry next session.
|
|
1202
1223
|
// Trace because this catch previously hid an off-by-one regression
|
|
1203
|
-
// in resolvePluginRoot (4.0.4-4.1.0) that silently disabled
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1224
|
+
// in resolvePluginRoot (4.0.4-4.1.0) that silently disabled noise
|
|
1225
|
+
// compression for three minor releases. A one-line stderr would have
|
|
1226
|
+
// surfaced it on day 1.
|
|
1206
1227
|
try { process.stderr.write(`[memesh session-start] noise-compression: ${err?.message || err}\n`); } catch {}
|
|
1207
1228
|
}
|
|
1208
1229
|
|
|
@@ -1246,7 +1267,9 @@ process.stdin.on('end', async () => {
|
|
|
1246
1267
|
*
|
|
1247
1268
|
* The shape is asserted by tests/helpers/hook-output-contract.ts.
|
|
1248
1269
|
*/
|
|
1249
|
-
|
|
1270
|
+
const workPackageGuidance = 'Work packages: check work_package prepare for this project (digest or transcript). When available, offer a concise host-native interactive choice in the user’s conversation language: dispatch an agent task, later (defer not_now), or stop suggesting for this session. Never dispatch without the user choosing it. The Dashboard cannot dispatch agents, and no durable opt-out is implied.';
|
|
1271
|
+
|
|
1272
|
+
function output(text, memoryContext = workPackageGuidance) {
|
|
1250
1273
|
const payload = { systemMessage: text };
|
|
1251
1274
|
if (memoryContext) {
|
|
1252
1275
|
payload.hookSpecificOutput = {
|