@pcircle/memesh 4.9.4 → 4.10.1
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +35 -4
- package/README.de.md +3 -3
- package/README.md +3 -3
- package/README.zh-TW.md +3 -3
- package/dashboard/dist/index.html +8 -8
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +0 -427
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/briefing-index.d.ts +30 -0
- package/dist/core/briefing-index.d.ts.map +1 -0
- package/dist/core/briefing-index.js +140 -0
- package/dist/core/briefing-index.js.map +1 -0
- package/dist/core/briefing.d.ts +4 -0
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +42 -4
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/capture-liveness.d.ts +136 -0
- package/dist/core/capture-liveness.d.ts.map +1 -0
- package/dist/core/capture-liveness.js +447 -0
- package/dist/core/capture-liveness.js.map +1 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +3 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/delegation.d.ts +47 -0
- package/dist/core/delegation.d.ts.map +1 -0
- package/dist/core/delegation.js +174 -0
- package/dist/core/delegation.js.map +1 -0
- package/dist/core/doctor-fixes.d.ts +1 -0
- package/dist/core/doctor-fixes.d.ts.map +1 -1
- package/dist/core/doctor-fixes.js +21 -0
- package/dist/core/doctor-fixes.js.map +1 -1
- package/dist/core/doctor.d.ts +8 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +148 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/note-derive.d.ts +14 -0
- package/dist/core/note-derive.d.ts.map +1 -0
- package/dist/core/note-derive.js +66 -0
- package/dist/core/note-derive.js.map +1 -0
- package/dist/core/note-ingest.d.ts +34 -0
- package/dist/core/note-ingest.d.ts.map +1 -0
- package/dist/core/note-ingest.js +445 -0
- package/dist/core/note-ingest.js.map +1 -0
- package/dist/core/operations.d.ts +10 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +129 -14
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +10 -4
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +3 -0
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/session-insight.d.ts +44 -0
- package/dist/core/session-insight.d.ts.map +1 -0
- package/dist/core/session-insight.js +216 -0
- package/dist/core/session-insight.js.map +1 -0
- package/dist/core/task-state-store.d.ts +4 -0
- package/dist/core/task-state-store.d.ts.map +1 -1
- package/dist/core/task-state-store.js +18 -5
- package/dist/core/task-state-store.js.map +1 -1
- package/dist/core/turn-signal.d.ts +22 -0
- package/dist/core/turn-signal.d.ts.map +1 -0
- package/dist/core/turn-signal.js +89 -0
- package/dist/core/turn-signal.js.map +1 -0
- package/dist/core/types.d.ts +12 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/update-entrypoint.d.ts +19 -0
- package/dist/core/update-entrypoint.d.ts.map +1 -0
- package/dist/core/update-entrypoint.js +222 -0
- package/dist/core/update-entrypoint.js.map +1 -0
- package/dist/core/update-notice.d.ts +69 -0
- package/dist/core/update-notice.d.ts.map +1 -0
- package/dist/core/update-notice.js +188 -0
- package/dist/core/update-notice.js.map +1 -0
- package/dist/host-runtime/codex-session.d.ts +10 -0
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +1 -1
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +24 -3
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
- package/dist/mcp/server.js +1079 -90
- package/dist/mcp/server.js.map +3 -3
- package/dist/skills-manifest.json +46 -26
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +7702 -5764
- package/dist/transports/cli/cli.js.map +3 -3
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +38 -27
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +25 -5
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +43 -5
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +4 -2
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +29 -3
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +1 -0
- package/docs/platforms/agent-messaging.md +22 -5
- package/package.json +14 -4
- package/scripts/hooks/_generated/briefing-index.js +147 -0
- package/scripts/hooks/_generated/capture-liveness.js +454 -0
- package/scripts/hooks/_generated/update-notice.js +195 -0
- package/scripts/hooks/_shared.js +302 -14
- package/scripts/hooks/_stop-notes.js +504 -0
- package/scripts/hooks/auto-update-runner.mjs +22 -0
- package/scripts/hooks/decision-nudge.js +32 -5
- package/scripts/hooks/guard-check.js +30 -3
- package/scripts/hooks/post-commit.js +329 -133
- package/scripts/hooks/pre-compact.js +17 -2
- package/scripts/hooks/pre-edit-recall.js +32 -1
- package/scripts/hooks/session-start.js +361 -78
- package/scripts/hooks/session-summary.js +217 -41
- package/scripts/hooks/user-prompt-intent.js +89 -19
- package/skills/memesh/SKILL.md +20 -8
- package/dist/cli/assets/d3.v7.min.js +0 -2
- package/dist/core/graph.d.ts +0 -26
- package/dist/core/graph.d.ts.map +0 -1
- package/dist/core/graph.js +0 -74
- package/dist/core/graph.js.map +0 -1
|
@@ -23,7 +23,18 @@ import {
|
|
|
23
23
|
// Aliased: this file already has a local `const memeshDir` (a resolved
|
|
24
24
|
// db-path-derived directory string) — the helper here is the MEMESH_DIR/
|
|
25
25
|
// home resolver the update-check cache itself uses.
|
|
26
|
+
advanceGraceState,
|
|
27
|
+
captureLivenessNotice,
|
|
28
|
+
captureLivenessVerdict,
|
|
29
|
+
graceInEffect,
|
|
30
|
+
parseGraceState,
|
|
31
|
+
ensurePrivateDir as ensurePrivateDirShared,
|
|
26
32
|
memeshDir as memeshHomeDir,
|
|
33
|
+
parseHookOutcomes,
|
|
34
|
+
hookErrorReason,
|
|
35
|
+
recordHookOutcome,
|
|
36
|
+
summarizeHookOutcomes,
|
|
37
|
+
HOOK_OUTCOMES_FILENAME,
|
|
27
38
|
parseTaskState,
|
|
28
39
|
readRepoState,
|
|
29
40
|
readAutoUpdateConsent,
|
|
@@ -31,6 +42,11 @@ import {
|
|
|
31
42
|
finalizeUpdatePromptClaim,
|
|
32
43
|
readUpdatePromptClaim,
|
|
33
44
|
readUpdateCheckCache,
|
|
45
|
+
resolveUpdateNotice,
|
|
46
|
+
shouldRefreshUpdateCache,
|
|
47
|
+
claimJustUpgradedMarker,
|
|
48
|
+
isStrictlyOlder,
|
|
49
|
+
isUpdateCheckEnabled,
|
|
34
50
|
repoStateLines,
|
|
35
51
|
resolvePluginRoot,
|
|
36
52
|
resolveSessionLimit,
|
|
@@ -43,6 +59,12 @@ import {
|
|
|
43
59
|
} from './_shared.js';
|
|
44
60
|
import { MemeshDatabase } from './_generated/sqlite.js';
|
|
45
61
|
import { unreadDeliveryCount, unreadInboxLines } from './_generated/agent-message-inbox.js';
|
|
62
|
+
import {
|
|
63
|
+
buildBriefingIndex,
|
|
64
|
+
INDEX_CANDIDATE_CAP,
|
|
65
|
+
INDEX_EXCLUDED_TYPES,
|
|
66
|
+
INDEX_SNIPPET_FETCH_CHARS,
|
|
67
|
+
} from './_generated/briefing-index.js';
|
|
46
68
|
|
|
47
69
|
const require = createRequire(import.meta.url);
|
|
48
70
|
|
|
@@ -191,33 +213,59 @@ function buildDeprecationBanner(currentVersion, cache) {
|
|
|
191
213
|
*/
|
|
192
214
|
const UPDATE_BANNER_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
193
215
|
|
|
216
|
+
// `isStrictlyOlder` now comes from the shared update-notice leaf (one
|
|
217
|
+
// semver rule for hooks, CLI and the resolver).
|
|
218
|
+
|
|
219
|
+
const CHECK_FAILED_BANNER_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
220
|
+
|
|
194
221
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* trailing fragment — fine for the prerelease / 4-segment build tags
|
|
200
|
-
* memesh uses (e.g. 4.2.5-rc.1).
|
|
222
|
+
* Banner lines for the two update states that are NOT a consent prompt:
|
|
223
|
+
* JUST_UPGRADED (said once, then the receipt is consumed) and CHECK_FAILED
|
|
224
|
+
* (unknown is not "up to date"; throttled to once a day). Everything else
|
|
225
|
+
* returns [] so the caller falls through to its consent/banner logic.
|
|
201
226
|
*/
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
function updateNoticeBanner(installedVersion, cache) {
|
|
228
|
+
if (!installedVersion) return { kind: 'UNKNOWN_VERSION', lines: [] };
|
|
229
|
+
const notice = resolveUpdateNotice({
|
|
230
|
+
dir: memeshHomeDir(), currentVersion: installedVersion, cache, updateCheckEnabled: isUpdateCheckEnabled(),
|
|
231
|
+
});
|
|
232
|
+
if (notice.kind === 'JUST_UPGRADED') {
|
|
233
|
+
// Take the receipt atomically: two host hooks starting together must not
|
|
234
|
+
// both announce. The loser sees null and says nothing about it.
|
|
235
|
+
const claimed = claimJustUpgradedMarker(memeshHomeDir());
|
|
236
|
+
if (!claimed) return { kind: 'UP_TO_DATE', lines: [] };
|
|
237
|
+
return {
|
|
238
|
+
kind: notice.kind,
|
|
239
|
+
lines: [`✅ MeMesh upgraded ${claimed.from} → ${claimed.to}. Hosts already running keep the old version until they restart; this session's hooks and MCP server pick up ${claimed.to} on the next start.`],
|
|
240
|
+
};
|
|
215
241
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
242
|
+
if (notice.kind === 'CHECK_FAILED') return { kind: notice.kind, lines: buildCheckFailedBanner(installedVersion, notice.reason) };
|
|
243
|
+
return { kind: notice.kind, lines: [] };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The update status is UNKNOWN (no completed check, or the registry lookup
|
|
248
|
+
* failed and the last good answer is older than a day). Silence here would
|
|
249
|
+
* read as "up to date", so say it — once a day per installed version.
|
|
250
|
+
*/
|
|
251
|
+
function buildCheckFailedBanner(currentVersion, reason) {
|
|
252
|
+
try {
|
|
253
|
+
const fs = require('fs');
|
|
254
|
+
const dir = memeshHomeDir();
|
|
255
|
+
try { ensurePrivateDir(dir); } catch { /* best-effort */ }
|
|
256
|
+
const versionTag = /^[0-9A-Za-z.+-]+$/.test(currentVersion) ? currentVersion : 'unknown';
|
|
257
|
+
const markerPath = join(dir, `last-check-failed-banner.${versionTag}.lock`);
|
|
258
|
+
let stat;
|
|
259
|
+
try { stat = fs.statSync(markerPath); } catch { stat = null; }
|
|
260
|
+
if (stat && Date.now() - stat.mtimeMs < CHECK_FAILED_BANNER_THROTTLE_MS) return [];
|
|
261
|
+
try {
|
|
262
|
+
fs.writeFileSync(markerPath, String(Date.now()), { mode: 0o600 });
|
|
263
|
+
try { fs.chmodSync(markerPath, 0o600); } catch { /* non-POSIX */ }
|
|
264
|
+
} catch { /* best-effort */ }
|
|
265
|
+
} catch {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
return [`ℹ️ MeMesh could not confirm whether an update exists (${reason}). Update status is unknown, not current — run \`memesh status\` to retry.`];
|
|
221
269
|
}
|
|
222
270
|
|
|
223
271
|
function buildUpdateAvailableBanner(currentVersion, cache, getChannel) {
|
|
@@ -333,7 +381,13 @@ function detectInstallChannelHook(pluginRoot) {
|
|
|
333
381
|
|
|
334
382
|
function buildUpdateConsentPrompt(sessionId, currentVersion, cache, channel) {
|
|
335
383
|
if (!sessionId || sessionId === 'unknown' || !cache || cache.currentVersion !== currentVersion) return null;
|
|
336
|
-
|
|
384
|
+
// One resolver decides. Snoozed ("Not now" within its window), disabled
|
|
385
|
+
// ("Never ask again"), a just-landed upgrade, or a failed check all mean:
|
|
386
|
+
// no consent prompt this session. Those states get their own banner line.
|
|
387
|
+
const notice = resolveUpdateNotice({
|
|
388
|
+
dir: memeshHomeDir(), currentVersion, cache, updateCheckEnabled: isUpdateCheckEnabled(),
|
|
389
|
+
});
|
|
390
|
+
if (notice.kind !== 'UPGRADE_AVAILABLE') return null;
|
|
337
391
|
const existing = readAutoUpdateConsent(sessionId, currentVersion, cache.latestVersion, channel);
|
|
338
392
|
if (existing?.decision) return null;
|
|
339
393
|
// Claim the session-level notice before emitting it. A resumed hook or a
|
|
@@ -354,7 +408,7 @@ function buildUpdateConsentPrompt(sessionId, currentVersion, cache, channel) {
|
|
|
354
408
|
? ' Project-local install: run `npm install @pcircle/memesh@latest` in the project that installed it.'
|
|
355
409
|
: ' Update it through the tool or package manager that installed MeMesh.';
|
|
356
410
|
return {
|
|
357
|
-
system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. This installation cannot be upgraded automatically from this session.\n${action}
|
|
411
|
+
system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. This installation cannot be upgraded automatically from this session.\n${action}\n Reply “Not now” to snooze this version (24h, then longer), or “Never ask again” to stop these checks.`,
|
|
358
412
|
context: `MeMesh ${cache.latestVersion} is available for ${target}, but this channel has no safe in-session installer. Show the user the channel-specific update action and do not claim that an Upgrade reply will install it.`,
|
|
359
413
|
};
|
|
360
414
|
}
|
|
@@ -363,8 +417,8 @@ function buildUpdateConsentPrompt(sessionId, currentVersion, cache, channel) {
|
|
|
363
417
|
// session notice has been claimed.
|
|
364
418
|
if (!writeAutoUpdateConsent(sessionId, currentVersion, cache.latestVersion, channel, 'pending')) return null;
|
|
365
419
|
return {
|
|
366
|
-
system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. Reply “Upgrade” to install it,
|
|
367
|
-
context: `MeMesh update consent is pending for this session. Ask the user whether to upgrade from ${currentVersion} to ${cache.latestVersion} for the ${target}. Wait for an explicit Upgrade
|
|
420
|
+
system: `\nℹ️ MeMesh ${cache.latestVersion} is available (you're on ${currentVersion}) for ${target}. Reply “Upgrade” to install it, “Not now” to snooze this version (24h, then longer), or “Never ask again” to stop these checks.`,
|
|
421
|
+
context: `MeMesh update consent is pending for this session. Ask the user whether to upgrade from ${currentVersion} to ${cache.latestVersion} for the ${target}. Wait for an explicit Upgrade, Not now, or Never ask again response; do not install without affirmative consent.`,
|
|
368
422
|
};
|
|
369
423
|
}
|
|
370
424
|
|
|
@@ -563,6 +617,11 @@ function runPostBannerUpdateTasks() {
|
|
|
563
617
|
if (!installedVersion) return;
|
|
564
618
|
// Auto-update spawn moved to Stop hook (v4.1.4) to avoid TOCTOU race
|
|
565
619
|
// where npm install -g overwrites dist/ while peer hooks are still reading it.
|
|
620
|
+
// Two TTLs (update-notice.ts): a current answer is re-verified hourly, a
|
|
621
|
+
// known upgrade is not re-fetched for 12h; a failed or missing check is
|
|
622
|
+
// always retried. "Never ask again" also stops the background refresh.
|
|
623
|
+
if (!isUpdateCheckEnabled()) return;
|
|
624
|
+
if (!shouldRefreshUpdateCache(installedVersion, readUpdateCheckCache(installedVersion))) return;
|
|
566
625
|
spawnFreshUpdateCheck(installedVersion);
|
|
567
626
|
} catch {
|
|
568
627
|
// Best-effort — never crash the hook on a network or fs hiccup.
|
|
@@ -611,27 +670,120 @@ function captureTargetUnwritable() {
|
|
|
611
670
|
return null;
|
|
612
671
|
}
|
|
613
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Once per 24h, one line when automatic capture has gone quiet (#327).
|
|
675
|
+
*
|
|
676
|
+
* The verdict is computed in the HOOK layer because a hook cannot import
|
|
677
|
+
* `src/` — but from the SAME leaf `memesh doctor` uses, so the banner and
|
|
678
|
+
* the report can never disagree about whether capture is alive. The hook
|
|
679
|
+
* sees only `hook-outcomes.jsonl`: doctor additionally reads the database for
|
|
680
|
+
* per-type trends and the `hook_runs` heartbeats, so doctor can reach a FAIL
|
|
681
|
+
* this line never will. That is the right asymmetry — a SessionStart line is
|
|
682
|
+
* a nudge towards `memesh doctor`, not a replacement for it.
|
|
683
|
+
*
|
|
684
|
+
* Suppression after the next successful write needs no marker of its own: a
|
|
685
|
+
* `wrote` record makes the hook non-silent and the verdict returns to PASS,
|
|
686
|
+
* so this returns null on its own. Only the throttle needs a file.
|
|
687
|
+
*
|
|
688
|
+
* It reads the JSONL and nothing else — never the database. SessionStart is
|
|
689
|
+
* on the critical path of every session start with a 10s budget, and a
|
|
690
|
+
* banner line is not worth a query against a graph that may be mid-write.
|
|
691
|
+
*/
|
|
692
|
+
const CAPTURE_LIVENESS_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
693
|
+
const CAPTURE_GRACE_FILE = 'capture-liveness-grace.json';
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Count this session against the post-install / post-upgrade grace, and say
|
|
697
|
+
* whether the grace is still on.
|
|
698
|
+
*
|
|
699
|
+
* The counter is keyed to the installed version, so an upgrade resets it —
|
|
700
|
+
* an upgrade replaces the hooks, which is exactly when a legitimate run of
|
|
701
|
+
* skips is expected and a warning would be noise.
|
|
702
|
+
*/
|
|
703
|
+
function captureGraceInEffect(dir, installedVersion) {
|
|
704
|
+
try {
|
|
705
|
+
let raw = null;
|
|
706
|
+
try { raw = readFileSync(join(dir, CAPTURE_GRACE_FILE), 'utf8'); } catch { /* first session */ }
|
|
707
|
+
const next = advanceGraceState(parseGraceState(raw), installedVersion, Date.now());
|
|
708
|
+
try {
|
|
709
|
+
ensurePrivateDirShared(dir);
|
|
710
|
+
const gracePath = join(dir, CAPTURE_GRACE_FILE);
|
|
711
|
+
require('fs').writeFileSync(gracePath, JSON.stringify(next), { mode: 0o600 });
|
|
712
|
+
try { require('fs').chmodSync(gracePath, 0o600); } catch { /* best-effort hardening */ }
|
|
713
|
+
} catch { /* best-effort — a lost count only shortens the grace */ }
|
|
714
|
+
return graceInEffect(next, Date.now());
|
|
715
|
+
} catch {
|
|
716
|
+
// Cannot tell how new this install is → assume it is new. Silence is
|
|
717
|
+
// the safe default for a nudge; doctor still reports the truth.
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function captureLivenessBannerLine(installedVersion) {
|
|
723
|
+
try {
|
|
724
|
+
// The banner must read the outcome file from the SAME place the hooks
|
|
725
|
+
// write it. recordHookOutcome writes beside the database
|
|
726
|
+
// (getMemeshDirFromDbPath = dirname(MEMESH_DB_PATH)); reading via
|
|
727
|
+
// memeshHomeDir (= MEMESH_DIR) instead split the two whenever a DB path
|
|
728
|
+
// override pointed elsewhere, and the banner silently read nothing —
|
|
729
|
+
// the exact "banner and report can never disagree" failure this exists
|
|
730
|
+
// to prevent. The grace counter and throttle lock move with it so the
|
|
731
|
+
// three pieces of this mechanism stay beside the same file.
|
|
732
|
+
const dir = getMemeshDirFromDbPath();
|
|
733
|
+
if (captureGraceInEffect(dir, installedVersion ?? 'unknown')) return null;
|
|
734
|
+
let raw = null;
|
|
735
|
+
try { raw = readFileSync(join(dir, HOOK_OUTCOMES_FILENAME), 'utf8'); } catch { return null; }
|
|
736
|
+
const verdict = captureLivenessVerdict({ hooks: summarizeHookOutcomes(parseHookOutcomes(raw)), types: [] });
|
|
737
|
+
const line = captureLivenessNotice(verdict);
|
|
738
|
+
if (!line) return null;
|
|
739
|
+
|
|
740
|
+
// Same throttle primitive as the update banner: file mtime, touched
|
|
741
|
+
// BEFORE the line is emitted so two sessions starting at once cannot
|
|
742
|
+
// both print it.
|
|
743
|
+
const markerPath = join(dir, 'last-capture-liveness-notice.lock');
|
|
744
|
+
let stat;
|
|
745
|
+
try { stat = require('fs').statSync(markerPath); } catch { stat = null; }
|
|
746
|
+
if (stat && Date.now() - stat.mtimeMs < CAPTURE_LIVENESS_THROTTLE_MS) return null;
|
|
747
|
+
try {
|
|
748
|
+
ensurePrivateDirShared(dir);
|
|
749
|
+
require('fs').writeFileSync(markerPath, String(Date.now()), { mode: 0o600 });
|
|
750
|
+
try { require('fs').chmodSync(markerPath, 0o600); } catch { /* best-effort hardening */ }
|
|
751
|
+
} catch { /* best-effort — worst case the line shows twice */ }
|
|
752
|
+
return line;
|
|
753
|
+
} catch {
|
|
754
|
+
// Diagnostics must never cost a session its banner.
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
614
759
|
/**
|
|
615
760
|
* Build a "base message + optional deprecation banner" combined
|
|
616
761
|
* single-line systemMessage payload. Keeps stdout a single JSON
|
|
617
762
|
* object on every empty/no-DB exit path so Claude Code's hook
|
|
618
763
|
* contract holds.
|
|
619
764
|
*/
|
|
620
|
-
function combineWithBanner(baseMessage) {
|
|
765
|
+
function combineWithBanner(baseMessage, { skipUpdateBanner = false } = {}) {
|
|
621
766
|
let lines = [];
|
|
767
|
+
// Hoisted out of the try: the capture-liveness grace is keyed to the
|
|
768
|
+
// installed version, and it must still be counted when the update-banner
|
|
769
|
+
// block below throws (a missing package.json must not silently disable the
|
|
770
|
+
// grace and start warning on a fresh install).
|
|
771
|
+
let livenessVersion = null;
|
|
622
772
|
try {
|
|
623
773
|
const pluginRoot = resolvePluginRoot(import.meta.url);
|
|
624
774
|
const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'));
|
|
625
775
|
const installedVersion = typeof pkg.version === 'string' ? pkg.version : null;
|
|
776
|
+
livenessVersion = installedVersion;
|
|
626
777
|
const cache = readUpdateCheckCache(installedVersion);
|
|
627
778
|
if (installedVersion) {
|
|
628
779
|
const deprecation = buildDeprecationBanner(installedVersion, cache);
|
|
629
|
-
|
|
630
|
-
// banner is the fallback for the much more common "not flagged,
|
|
631
|
-
// just out of date" case.
|
|
780
|
+
const notice = updateNoticeBanner(installedVersion, cache);
|
|
632
781
|
if (deprecation.length > 0) {
|
|
633
782
|
lines = deprecation;
|
|
634
|
-
} else {
|
|
783
|
+
} else if (notice.lines.length > 0) {
|
|
784
|
+
lines = notice.lines;
|
|
785
|
+
} else if (notice.kind === 'UPGRADE_AVAILABLE' && !skipUpdateBanner) {
|
|
786
|
+
// Snoozed, disabled, failed or current: the resolver already said no.
|
|
635
787
|
lines = buildUpdateAvailableBanner(
|
|
636
788
|
installedVersion, cache, () => detectInstallChannelHook(pluginRoot));
|
|
637
789
|
}
|
|
@@ -639,6 +791,10 @@ function combineWithBanner(baseMessage) {
|
|
|
639
791
|
} catch {
|
|
640
792
|
// Best-effort — fall through to base message only.
|
|
641
793
|
}
|
|
794
|
+
// The capture-liveness line rides the same single systemMessage as every
|
|
795
|
+
// other banner — stdout must stay one JSON document.
|
|
796
|
+
const liveness = captureLivenessBannerLine(livenessVersion);
|
|
797
|
+
if (liveness) lines = [...lines, liveness];
|
|
642
798
|
if (lines.length === 0) return baseMessage;
|
|
643
799
|
return [...lines.filter((l) => l.length > 0), '', baseMessage].join('\n');
|
|
644
800
|
}
|
|
@@ -730,7 +886,6 @@ process.stdin.on('end', async () => {
|
|
|
730
886
|
// With no database there is nothing to recall either — the warning IS
|
|
731
887
|
// the whole truth, and "memories will be created as you work" would
|
|
732
888
|
// contradict it one line later.
|
|
733
|
-
const emptySummary = combineWithBanner(captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work');
|
|
734
889
|
let consent = null;
|
|
735
890
|
let consentVersion = null;
|
|
736
891
|
let consentCache = null;
|
|
@@ -742,6 +897,20 @@ process.stdin.on('end', async () => {
|
|
|
742
897
|
const channel = detectInstallChannelHook(pluginRoot);
|
|
743
898
|
consent = buildUpdateConsentPrompt(data.session_id, consentVersion, consentCache, channel);
|
|
744
899
|
} catch { /* best-effort */ }
|
|
900
|
+
// The consent prompt IS the update message for this session; the
|
|
901
|
+
// routine "update available" banner must not repeat it one line later
|
|
902
|
+
// (the database path has had this rule since the notice was added; the
|
|
903
|
+
// no-database path printed both). Deprecation, a just-landed upgrade
|
|
904
|
+
// and a failed check still render through combineWithBanner.
|
|
905
|
+
// A session that was already shown the notice (claim exists, answered
|
|
906
|
+
// or not) must not get the routine banner either — same rule as the
|
|
907
|
+
// database path.
|
|
908
|
+
const alreadyNoticed = consent !== null || (consentVersion !== null
|
|
909
|
+
&& readUpdatePromptClaim(data.session_id, consentVersion, consentCache?.latestVersion) !== null);
|
|
910
|
+
const emptySummary = combineWithBanner(
|
|
911
|
+
captureWarning ?? '◉ MeMesh ready · no database yet, memories will be created as you work',
|
|
912
|
+
{ skipUpdateBanner: alreadyNoticed },
|
|
913
|
+
);
|
|
745
914
|
output(consent ? `${consent.system}\n${emptySummary}` : emptySummary,
|
|
746
915
|
consent ? `${consent.context}\n\n${workPackageGuidance}` : workPackageGuidance);
|
|
747
916
|
if (consent) finalizeUpdatePromptClaim(data.session_id, consentVersion, consentCache?.latestVersion);
|
|
@@ -1101,45 +1270,122 @@ process.stdin.on('end', async () => {
|
|
|
1101
1270
|
try { process.stderr.write(`[memesh session-start] memory-context: ${err?.message || err}\n`); } catch {}
|
|
1102
1271
|
}
|
|
1103
1272
|
|
|
1273
|
+
// --- The durable-memory index (#323) -----------------------------
|
|
1274
|
+
// One line per decision / lesson / pattern / reference for this
|
|
1275
|
+
// project, newest first, hard-capped — what is known, visible without
|
|
1276
|
+
// a query. Same read as core's `readBriefingIndex` (this hook owns its
|
|
1277
|
+
// own SQL, with the legacy-schema guards the queries above use); every
|
|
1278
|
+
// decision about what to show lives in the generated leaf. A read that
|
|
1279
|
+
// fails renders as "could not be read", never as the empty-state line:
|
|
1280
|
+
// an empty index is a claim about the user's data.
|
|
1281
|
+
let indexLines;
|
|
1282
|
+
// id → name for the rows the index rendered, so the injected-set
|
|
1283
|
+
// record below can credit a citation of an index line the same way it
|
|
1284
|
+
// credits a ranked one — the line carries a `[mem:id]` handle, it was
|
|
1285
|
+
// shown, and a cite of it must not earn nothing.
|
|
1286
|
+
const indexEntities = [];
|
|
1287
|
+
try {
|
|
1288
|
+
const excluded = INDEX_EXCLUDED_TYPES.map(() => '?').join(',');
|
|
1289
|
+
const indexRows = db.prepare(
|
|
1290
|
+
`SELECT e.id, e.name, e.type,${hasTitle ? ' e.title,' : ''} e.metadata,
|
|
1291
|
+
(SELECT substr(o.content, 1, ${INDEX_SNIPPET_FETCH_CHARS}) FROM observations o
|
|
1292
|
+
WHERE o.entity_id = e.id ORDER BY o.id ASC LIMIT 1) AS snippet,
|
|
1293
|
+
max(e.created_at, COALESCE((SELECT MAX(o2.created_at) FROM observations o2
|
|
1294
|
+
WHERE o2.entity_id = e.id), e.created_at)) AS last_activity
|
|
1295
|
+
FROM entities e
|
|
1296
|
+
WHERE e.id IN (SELECT entity_id FROM tags WHERE tag = ?)
|
|
1297
|
+
${statusFilter} ${notGlobal}
|
|
1298
|
+
AND e.type NOT IN (${excluded})
|
|
1299
|
+
ORDER BY last_activity DESC, e.id DESC
|
|
1300
|
+
LIMIT ?`,
|
|
1301
|
+
).all(projectTag, ...INDEX_EXCLUDED_TYPES, INDEX_CANDIDATE_CAP);
|
|
1302
|
+
const index = buildBriefingIndex(
|
|
1303
|
+
indexRows.map((row) => ({
|
|
1304
|
+
id: row.id,
|
|
1305
|
+
type: row.type,
|
|
1306
|
+
title: row.title ?? null,
|
|
1307
|
+
snippet: row.snippet,
|
|
1308
|
+
lastActivity: row.last_activity,
|
|
1309
|
+
// The RAW column: the index's gate must tell an absent metadata
|
|
1310
|
+
// column (allowed) from unparseable JSON (refused), exactly as
|
|
1311
|
+
// `isTrustedForAutoContext` does on the ranked path. Parsing here
|
|
1312
|
+
// would collapse both to null and fail OPEN.
|
|
1313
|
+
metadata: row.metadata,
|
|
1314
|
+
})),
|
|
1315
|
+
projectName,
|
|
1316
|
+
Date.now(),
|
|
1317
|
+
{ truncated: indexRows.length >= INDEX_CANDIDATE_CAP },
|
|
1318
|
+
);
|
|
1319
|
+
indexLines = index.lines;
|
|
1320
|
+
const rendered = new Set(index.ids);
|
|
1321
|
+
for (const row of indexRows) if (rendered.has(row.id)) indexEntities.push(row);
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
const reason = String(err?.message || err);
|
|
1324
|
+
try { process.stderr.write(`[memesh session-start] briefing-index: ${reason}\n`); } catch {}
|
|
1325
|
+
recordHookOutcome(process.env, {
|
|
1326
|
+
hook: 'session-start',
|
|
1327
|
+
outcome: 'error',
|
|
1328
|
+
// The locus, plus a LABEL for the exception — never its message.
|
|
1329
|
+
// `hook-outcomes.jsonl` is permanent, exportable and meant to be
|
|
1330
|
+
// pasteable into an issue, and a message is a copy of whatever the
|
|
1331
|
+
// failure echoed: SQLite quotes the statement, execFileSync carries
|
|
1332
|
+
// absolute paths. `redactSecrets` runs on this field but
|
|
1333
|
+
// `redactUserPaths` does not. The full text is already on stderr
|
|
1334
|
+
// one line above, so nothing is lost.
|
|
1335
|
+
reason: `briefing-index: ${hookErrorReason(err)}`,
|
|
1336
|
+
});
|
|
1337
|
+
indexLines = [`Index of durable memories for "${projectName}": could not be read this session — run \`memesh doctor\`.`];
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Every `[mem:id]` handle a rendered line ends with. Anchored to the
|
|
1341
|
+
// end of the line on purpose: a handle is what the renderer printed,
|
|
1342
|
+
// not a citation someone wrote inside an observation.
|
|
1343
|
+
const renderedHandles = (lines) => lines.flatMap((line) => {
|
|
1344
|
+
const match = line.match(/ \[mem:(\d{1,10})\]$/);
|
|
1345
|
+
return match ? [Number(match[1])] : [];
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1104
1348
|
// Same prefix, same rule, as `assembleBriefing`: repository facts are
|
|
1105
|
-
// context for memories, never a briefing on their own
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
|
|
1349
|
+
// context for memories, never a briefing on their own — they prefix
|
|
1350
|
+
// only a block with ranked memories; the index's empty-state line is
|
|
1351
|
+
// not a reason to tell the agent its own branch name. The index itself
|
|
1352
|
+
// always closes the block (#323: an empty project shows the empty-state
|
|
1353
|
+
// line, not nothing). briefing.test.ts's parity case is what keeps this
|
|
1354
|
+
// identical to the tool side.
|
|
1111
1355
|
if (memoryLines.length > 0) {
|
|
1112
1356
|
const repoLines = repoStateLines(readRepoState(data.cwd));
|
|
1113
1357
|
if (repoLines.length > 0) memoryLines.unshift(...repoLines, '');
|
|
1114
|
-
|
|
1115
|
-
// not instructions" preamble plus a fenced block. Memory content is
|
|
1116
|
-
// attacker-influenced in the general case (anything the agent has
|
|
1117
|
-
// ever been told can end up in an observation), so it must be
|
|
1118
|
-
// delimited the same way on every injection path — not hand-rolled
|
|
1119
|
-
// per hook. The lines arrive already budgeted — assembleTopologyBlock
|
|
1120
|
-
// charges task state plus project/foreign sections against the main
|
|
1121
|
-
// ceiling and global context against its small additive ceiling. It
|
|
1122
|
-
// returns whole lines only, so the closing fence cannot be cut.
|
|
1123
|
-
memoryContext = buildReferenceContext(memoryLines) + '\n\n' + workPackageGuidance;
|
|
1124
|
-
// The citation contract — OUTSIDE the fence on purpose: the fence
|
|
1125
|
-
// declares its content "background data, not instructions", and
|
|
1126
|
-
// this line IS an instruction. One line is the entire write side of
|
|
1127
|
-
// the injection-ROI signal; the Stop hook credits recall_hits only
|
|
1128
|
-
// from these markers (self-reported: undercounts, never overcounts).
|
|
1129
|
-
// The citation instruction used to be appended here, outside the
|
|
1130
|
-
// fence, so it would read as an instruction rather than as data.
|
|
1131
|
-
// It never worked: Claude Code wraps a hook's additionalContext in a
|
|
1132
|
-
// system-reminder ending "you should not respond to this context
|
|
1133
|
-
// unless it is highly relevant", so the whole block — instruction
|
|
1134
|
-
// included — arrives as data. Measured on a real database:
|
|
1135
|
-
// citation_sessions_total=4, sessions WITH a citation = 0.
|
|
1136
|
-
//
|
|
1137
|
-
// The contract now lives in `.claude/rules/memesh-citations.md`,
|
|
1138
|
-
// which Claude Code loads as an instruction. Writing it is the
|
|
1139
|
-
// self-heal below; the line here is gone rather than duplicated,
|
|
1140
|
-
// because a per-session copy of an instruction that is read as data
|
|
1141
|
-
// is a per-session cost with no effect.
|
|
1358
|
+
memoryLines.push('');
|
|
1142
1359
|
}
|
|
1360
|
+
memoryLines.push(...indexLines);
|
|
1361
|
+
// Same wrapper pre-edit-recall uses: an explicit "background data,
|
|
1362
|
+
// not instructions" preamble plus a fenced block. Memory content is
|
|
1363
|
+
// attacker-influenced in the general case (anything the agent has
|
|
1364
|
+
// ever been told can end up in an observation), so it must be
|
|
1365
|
+
// delimited the same way on every injection path — not hand-rolled
|
|
1366
|
+
// per hook. The lines arrive already budgeted — assembleTopologyBlock
|
|
1367
|
+
// charges task state plus project/foreign sections against the main
|
|
1368
|
+
// ceiling and global context against its small additive ceiling. It
|
|
1369
|
+
// returns whole lines only, so the closing fence cannot be cut.
|
|
1370
|
+
const memoryContext = buildReferenceContext(memoryLines) + '\n\n' + workPackageGuidance;
|
|
1371
|
+
// The citation contract — OUTSIDE the fence on purpose: the fence
|
|
1372
|
+
// declares its content "background data, not instructions", and
|
|
1373
|
+
// this line IS an instruction. One line is the entire write side of
|
|
1374
|
+
// the injection-ROI signal; the Stop hook credits recall_hits only
|
|
1375
|
+
// from these markers (self-reported: undercounts, never overcounts).
|
|
1376
|
+
// The citation instruction used to be appended here, outside the
|
|
1377
|
+
// fence, so it would read as an instruction rather than as data.
|
|
1378
|
+
// It never worked: Claude Code wraps a hook's additionalContext in a
|
|
1379
|
+
// system-reminder ending "you should not respond to this context
|
|
1380
|
+
// unless it is highly relevant", so the whole block — instruction
|
|
1381
|
+
// included — arrives as data. Measured on a real database:
|
|
1382
|
+
// citation_sessions_total=4, sessions WITH a citation = 0.
|
|
1383
|
+
//
|
|
1384
|
+
// The contract now lives in `.claude/rules/memesh-citations.md`,
|
|
1385
|
+
// which Claude Code loads as an instruction. Writing it is the
|
|
1386
|
+
// self-heal below; the line here is gone rather than duplicated,
|
|
1387
|
+
// because a per-session copy of an instruction that is read as data
|
|
1388
|
+
// is a per-session cost with no effect.
|
|
1143
1389
|
|
|
1144
1390
|
// --- Record injected entity IDs for recall effectiveness tracking ---
|
|
1145
1391
|
// The Stop hook credits recall_hits from EXPLICIT `[mem:id]` citations
|
|
@@ -1151,16 +1397,16 @@ process.stdin.on('end', async () => {
|
|
|
1151
1397
|
// kept as the record of what was shown.
|
|
1152
1398
|
//
|
|
1153
1399
|
// The set below is every pool the topology block draws from — the
|
|
1154
|
-
// lessons pool
|
|
1155
|
-
// so clipped or budgeted-away
|
|
1400
|
+
// lessons pool and the durable-memory index (#323) included. It is
|
|
1401
|
+
// derived from rendered citation handles, so clipped or budgeted-away
|
|
1402
|
+
// candidates cannot be credited as shown.
|
|
1156
1403
|
try {
|
|
1157
|
-
const renderedEntityIds = memoryLines
|
|
1158
|
-
|
|
1159
|
-
return match ? [Number(match[1])] : [];
|
|
1160
|
-
});
|
|
1161
|
-
const poolEntities = [...topLessons, ...projectEntities, ...globalEntities, ...recentEntities];
|
|
1404
|
+
const renderedEntityIds = renderedHandles(memoryLines);
|
|
1405
|
+
const poolEntities = [...topLessons, ...projectEntities, ...globalEntities, ...recentEntities, ...indexEntities];
|
|
1162
1406
|
const entitiesById = new Map(poolEntities.map((entity) => [entity.id, entity]));
|
|
1163
|
-
|
|
1407
|
+
// A memory can appear in the ranked block AND the index; it was
|
|
1408
|
+
// injected once.
|
|
1409
|
+
const allInjected = [...new Set(renderedEntityIds)]
|
|
1164
1410
|
.map((id) => entitiesById.get(id))
|
|
1165
1411
|
.filter(Boolean);
|
|
1166
1412
|
|
|
@@ -1218,8 +1464,16 @@ process.stdin.on('end', async () => {
|
|
|
1218
1464
|
let updateConsentContext = null;
|
|
1219
1465
|
if (installedVersion) {
|
|
1220
1466
|
const deprecation = buildDeprecationBanner(installedVersion, updateCache);
|
|
1467
|
+
const notice = updateNoticeBanner(installedVersion, updateCache);
|
|
1221
1468
|
if (deprecation.length > 0) {
|
|
1222
1469
|
bannerLines = deprecation;
|
|
1470
|
+
} else if (notice.lines.length > 0) {
|
|
1471
|
+
bannerLines = notice.lines;
|
|
1472
|
+
} else if (notice.kind !== 'UPGRADE_AVAILABLE') {
|
|
1473
|
+
// Snoozed ("Not now"), disabled ("Never ask again"), current, or
|
|
1474
|
+
// unknown-but-throttled: the resolver decided, so the routine
|
|
1475
|
+
// banner below must not undo it.
|
|
1476
|
+
bannerLines = [];
|
|
1223
1477
|
} else {
|
|
1224
1478
|
const channel = detectInstallChannelHook(resolvePluginRoot(import.meta.url));
|
|
1225
1479
|
const consent = buildUpdateConsentPrompt(data.session_id, installedVersion, updateCache, channel);
|
|
@@ -1240,6 +1494,13 @@ process.stdin.on('end', async () => {
|
|
|
1240
1494
|
}
|
|
1241
1495
|
}
|
|
1242
1496
|
}
|
|
1497
|
+
// The populated-database path builds its banner locally rather than
|
|
1498
|
+
// going through combineWithBanner() (the no-database path does). Keep
|
|
1499
|
+
// the capture-liveness notice in this path too: otherwise the warning
|
|
1500
|
+
// works only for a fresh graph and disappears precisely for users whose
|
|
1501
|
+
// existing memories make capture failure most costly.
|
|
1502
|
+
const captureLiveness = captureLivenessBannerLine(installedVersion);
|
|
1503
|
+
if (captureLiveness) bannerLines.push(captureLiveness);
|
|
1243
1504
|
const finalMessage = bannerLines.length > 0
|
|
1244
1505
|
? [...bannerLines.filter(l => l.length > 0), '', summary].join('\n')
|
|
1245
1506
|
: summary;
|
|
@@ -1317,7 +1578,14 @@ process.stdin.on('end', async () => {
|
|
|
1317
1578
|
// Hooks must never crash Claude Code — but report honestly.
|
|
1318
1579
|
// Inner catch so the outer finally can still run the post-
|
|
1319
1580
|
// banner update tasks even when the recall flow blew up.
|
|
1320
|
-
|
|
1581
|
+
// Through output(), like every other emit: the gate proves "every
|
|
1582
|
+
// stdout write lies inside output()", and output() records exactly one
|
|
1583
|
+
// outcome — here an error, not the default `wrote`.
|
|
1584
|
+
output(
|
|
1585
|
+
withCaptureWarning(`MeMesh: memories not loaded this session (${err?.message || 'unknown error'}) — everything else works; run \`memesh doctor\` if this repeats.`),
|
|
1586
|
+
null,
|
|
1587
|
+
{ outcome: 'error', reason: hookErrorReason(err) },
|
|
1588
|
+
);
|
|
1321
1589
|
}
|
|
1322
1590
|
} finally {
|
|
1323
1591
|
// ── Auto-update + cache refresh ──────────────────────────────
|
|
@@ -1355,7 +1623,15 @@ process.stdin.on('end', async () => {
|
|
|
1355
1623
|
*/
|
|
1356
1624
|
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.';
|
|
1357
1625
|
|
|
1358
|
-
function output(text, memoryContext = workPackageGuidance) {
|
|
1626
|
+
function output(text, memoryContext = workPackageGuidance, recorded = null) {
|
|
1627
|
+
// session-start's only effect is the context it injects, so it records
|
|
1628
|
+
// `notified`, not `wrote`: doctor's `writes` answers "is memory capture
|
|
1629
|
+
// still alive", and injected context is something this hook READ, not
|
|
1630
|
+
// something it stored. Recorded here rather than at each of the handler's many
|
|
1631
|
+
// returns because output() is the single emit point they all funnel
|
|
1632
|
+
// through, so no path can add itself later and stay invisible (#327).
|
|
1633
|
+
// `recorded` overrides the outcome for the one path that is not a write
|
|
1634
|
+
// (the recall flow threw): still one emit, still exactly one record.
|
|
1359
1635
|
const payload = { systemMessage: text };
|
|
1360
1636
|
if (memoryContext) {
|
|
1361
1637
|
payload.hookSpecificOutput = {
|
|
@@ -1364,4 +1640,11 @@ function output(text, memoryContext = workPackageGuidance) {
|
|
|
1364
1640
|
};
|
|
1365
1641
|
}
|
|
1366
1642
|
console.log(JSON.stringify(payload));
|
|
1643
|
+
recordHookOutcome(process.env, recorded
|
|
1644
|
+
? { hook: 'session-start', outcome: recorded.outcome, reason: recorded.reason }
|
|
1645
|
+
: {
|
|
1646
|
+
hook: 'session-start',
|
|
1647
|
+
outcome: 'notified',
|
|
1648
|
+
entity: memoryContext ? 'session-start-context' : 'session-start-banner',
|
|
1649
|
+
});
|
|
1367
1650
|
}
|