@rubytech/create-maxy-code 0.1.144 → 0.1.146
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/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +154 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/post-turn-graph-pass.sh +251 -0
- package/payload/platform/plugins/admin/skills/datetime/SKILL.md +63 -5
- package/payload/platform/plugins/docs/references/internals.md +16 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +10 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +25 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +309 -11
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js +30 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js +51 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js +68 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +41 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +43 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +18 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +64 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts +44 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js +14 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +26 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +219 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +42 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts +21 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js +27 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js +60 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +20 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +47 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts +13 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js +191 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +7 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +37 -9
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +81 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +115 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +50 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js +170 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js +64 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts +24 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js +49 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +34 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +99 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts +29 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +66 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +122 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +49 -13
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +184 -16
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +53 -10
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +6 -0
- package/payload/platform/plugins/memory/references/schema-base.md +4 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +3 -4
- package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +13 -2
- package/payload/platform/scripts/seed-neo4j.sh +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +9 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +30 -2
- package/payload/server/server.js +195 -91
- package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +0 -309
- package/payload/platform/templates/specialists/agents/signal-detector.md +0 -129
package/package.json
CHANGED
|
@@ -363,11 +363,19 @@ ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
|
|
|
363
363
|
n.subject, n.bodyPreview, n.fromName, n.fromAddress, n.agentAddress, n.screeningReason,
|
|
364
364
|
n.authority, n.contactValue, n.toolName,
|
|
365
365
|
n.displayName, n.slug, n.role,
|
|
366
|
-
n.compiledTruth];
|
|
366
|
+
n.compiledTruth, n.compiledTruthPublic];
|
|
367
367
|
// `compiledTruth` (Task 306) is the authoritative-summary property the
|
|
368
368
|
// compiled-truth + timeline knowledge model writes on Person/Company/Concept.
|
|
369
369
|
// Indexed here so Task 308's compiledTruth boost path also benefits from BM25
|
|
370
370
|
// hits against the summary text. No-op until 306 writes the property.
|
|
371
|
+
//
|
|
372
|
+
// `compiledTruthPublic` (Task 392) is the customer-facing twin on the four
|
|
373
|
+
// public-twin labels (Organization | Concept | Project | LocalBusiness). The
|
|
374
|
+
// public-agent's `memory-search` projection swaps `compiledTruth` →
|
|
375
|
+
// `compiledTruthPublic` on the way out for scope='public' reads (see
|
|
376
|
+
// platform/plugins/memory/mcp/src/tools/memory-search.ts). Both fields stay
|
|
377
|
+
// in the BM25 union so admin queries rank on the operator's view and public
|
|
378
|
+
// queries rank on the customer-facing summary.
|
|
371
379
|
|
|
372
380
|
// Project node — a standalone creative-output node distinct from
|
|
373
381
|
// :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
|
|
@@ -1328,15 +1336,27 @@ OPTIONS {
|
|
|
1328
1336
|
};
|
|
1329
1337
|
|
|
1330
1338
|
// ----------------------------------------------------------
|
|
1331
|
-
// Compiled-truth + timeline knowledge model (Task 306) —
|
|
1339
|
+
// Compiled-truth + timeline knowledge model (Task 306, extended Task 392) —
|
|
1332
1340
|
// gbrain's "compiled truth above the rule, append-only timeline
|
|
1333
|
-
// below" pattern. Every
|
|
1341
|
+
// below" pattern. Every entity subject in the admin-twin set
|
|
1342
|
+
// (:Person | :Organization | :Concept | :Project | :LocalBusiness) carries
|
|
1334
1343
|
// an agent-rewritten summary (`compiledTruth`, ≤2000 chars) plus
|
|
1335
1344
|
// a history of dated `:TimelineEvent` rows path-anchored via
|
|
1336
1345
|
// `(entity)-[:HAS_TIMELINE_EVENT]->(:TimelineEvent)`. The
|
|
1337
1346
|
// compiledTruth is what the agent reads when it needs a summary;
|
|
1338
1347
|
// the timeline is what it cites and appends to.
|
|
1339
1348
|
//
|
|
1349
|
+
// Public-twin (Task 392): four of the five subject labels also carry
|
|
1350
|
+
// `compiledTruthPublic` (≤2000 chars) — the customer-facing summary
|
|
1351
|
+
// the public agent surfaces. The public-twin set is
|
|
1352
|
+
// :Organization | :Concept | :Project | :LocalBusiness. :Person is
|
|
1353
|
+
// deliberately excluded: an operator's notes on a contact are private
|
|
1354
|
+
// by definition and have no neutral customer-facing form. The
|
|
1355
|
+
// public-agent `memory-search` projection (scope='public') rewrites
|
|
1356
|
+
// the returned property bag so `compiledTruth` is renamed to
|
|
1357
|
+
// `compiledTruthPublic` on public-twin labels and stripped entirely
|
|
1358
|
+
// on :Person.
|
|
1359
|
+
//
|
|
1340
1360
|
// Naming:
|
|
1341
1361
|
// :TimelineEvent — distinct from :Event (scheduling plugin's
|
|
1342
1362
|
// schema:Event with eventId-unique). Collisions were the reason
|
|
@@ -1357,8 +1377,19 @@ OPTIONS {
|
|
|
1357
1377
|
// `compiledTruth` source:
|
|
1358
1378
|
// compiledTruthSource = "rewriter" (default) — compiled-truth-rewriter
|
|
1359
1379
|
// library wrote it.
|
|
1360
|
-
// compiledTruthSource = "operator" — operator overrode via the graph UI
|
|
1380
|
+
// compiledTruthSource = "operator" — operator overrode via the graph UI
|
|
1381
|
+
// (legacy admin-server only) or via memory-update direct-write.
|
|
1361
1382
|
// Rewriter refuses to overwrite for 7 days unless forceRewrite=true.
|
|
1383
|
+
//
|
|
1384
|
+
// `compiledTruthPublic` source (Task 392):
|
|
1385
|
+
// compiledTruthPublicSource = "rewriter" (default) — compiled-truth-rewriter
|
|
1386
|
+
// `mode: 'public'` pass wrote it. Fires inline after a successful admin
|
|
1387
|
+
// pass when the label is in the public-twin set and the admin pass was
|
|
1388
|
+
// not a no-op.
|
|
1389
|
+
// compiledTruthPublicSource = "operator" — operator overrode via chat
|
|
1390
|
+
// (memory-update `compiledTruthPublic` direct-write argument). Public
|
|
1391
|
+
// rewriter refuses to overwrite for 7 days unless forceRewrite=true.
|
|
1392
|
+
// Cooldown is independent of the admin cooldown.
|
|
1362
1393
|
// ----------------------------------------------------------
|
|
1363
1394
|
|
|
1364
1395
|
CREATE CONSTRAINT timeline_event_id_unique IF NOT EXISTS
|
|
@@ -1392,3 +1423,122 @@ FOR (o:Organization) ON (o.compiledTruthUpdatedAt);
|
|
|
1392
1423
|
|
|
1393
1424
|
CREATE INDEX concept_compiled_truth_updated_at IF NOT EXISTS
|
|
1394
1425
|
FOR (c:Concept) ON (c.compiledTruthUpdatedAt);
|
|
1426
|
+
|
|
1427
|
+
// Admin-twin extension (Task 392) — :Project and :LocalBusiness joined the
|
|
1428
|
+
// admin-twin set when the public-twin landed. Same stale-detection index.
|
|
1429
|
+
CREATE INDEX project_compiled_truth_updated_at IF NOT EXISTS
|
|
1430
|
+
FOR (p:Project) ON (p.compiledTruthUpdatedAt);
|
|
1431
|
+
|
|
1432
|
+
CREATE INDEX local_business_compiled_truth_updated_at IF NOT EXISTS
|
|
1433
|
+
FOR (b:LocalBusiness) ON (b.compiledTruthUpdatedAt);
|
|
1434
|
+
|
|
1435
|
+
// Public-twin stale-detection indexes (Task 392). Same role as the admin
|
|
1436
|
+
// indexes above — find public-twin nodes whose public summary lags the
|
|
1437
|
+
// admin summary by N days. :Person omitted by design (no public twin).
|
|
1438
|
+
CREATE INDEX organization_compiled_truth_public_updated_at IF NOT EXISTS
|
|
1439
|
+
FOR (o:Organization) ON (o.compiledTruthPublicUpdatedAt);
|
|
1440
|
+
|
|
1441
|
+
CREATE INDEX concept_compiled_truth_public_updated_at IF NOT EXISTS
|
|
1442
|
+
FOR (c:Concept) ON (c.compiledTruthPublicUpdatedAt);
|
|
1443
|
+
|
|
1444
|
+
CREATE INDEX project_compiled_truth_public_updated_at IF NOT EXISTS
|
|
1445
|
+
FOR (p:Project) ON (p.compiledTruthPublicUpdatedAt);
|
|
1446
|
+
|
|
1447
|
+
CREATE INDEX local_business_compiled_truth_public_updated_at IF NOT EXISTS
|
|
1448
|
+
FOR (b:LocalBusiness) ON (b.compiledTruthPublicUpdatedAt);
|
|
1449
|
+
|
|
1450
|
+
// ----------------------------------------------------------
|
|
1451
|
+
// :CompiledTruthRevision (Task 391) — audit history of every
|
|
1452
|
+
// `compiledTruth` value an entity has ever held. Captured in the
|
|
1453
|
+
// same transaction as the rewriter SET so revision history can
|
|
1454
|
+
// never desync from the live property. Pruned by dream-cycle
|
|
1455
|
+
// phase 5 (keep newest 20 per entity) — never written-and-pruned
|
|
1456
|
+
// in the same TX as the main rewrite.
|
|
1457
|
+
//
|
|
1458
|
+
// Read-only after write: graph-write-gate rejects property
|
|
1459
|
+
// updates and label-anchored MERGE on this label. The dream-cycle
|
|
1460
|
+
// prune is the only mutation path.
|
|
1461
|
+
//
|
|
1462
|
+
// MERGE doctrine: writers MUST path-anchor from the entity to
|
|
1463
|
+
// defend cross-account collisions:
|
|
1464
|
+
// MATCH (e) WHERE elementId(e) = $entityId AND e.accountId = $accountId
|
|
1465
|
+
// MERGE (e)-[:HAS_REVISION]->(r:CompiledTruthRevision {revisionId: $id})
|
|
1466
|
+
// Never label-only MERGE on :CompiledTruthRevision.
|
|
1467
|
+
//
|
|
1468
|
+
// `source` enum (exact, no catch-all):
|
|
1469
|
+
// 'rewriter' — compiled-truth-rewriter library wrote the truth.
|
|
1470
|
+
// 'operator' — operator overrode via a sanctioned write route.
|
|
1471
|
+
// ----------------------------------------------------------
|
|
1472
|
+
|
|
1473
|
+
CREATE CONSTRAINT compiled_truth_revision_id_unique IF NOT EXISTS
|
|
1474
|
+
FOR (r:CompiledTruthRevision) REQUIRE r.revisionId IS UNIQUE;
|
|
1475
|
+
|
|
1476
|
+
CREATE INDEX compiled_truth_revision_account IF NOT EXISTS
|
|
1477
|
+
FOR (r:CompiledTruthRevision) ON (r.accountId);
|
|
1478
|
+
|
|
1479
|
+
CREATE INDEX compiled_truth_revision_written_at IF NOT EXISTS
|
|
1480
|
+
FOR (r:CompiledTruthRevision) ON (r.writtenAt);
|
|
1481
|
+
|
|
1482
|
+
// ----------------------------------------------------------
|
|
1483
|
+
// :Report node — durable, keyword-routed output of admin
|
|
1484
|
+
// workflows (daily briefing, dream cycle, ad-hoc analyses).
|
|
1485
|
+
// Task 332. Append-only by design: reports are not edited
|
|
1486
|
+
// after write. Parented to the active :AdminConversation
|
|
1487
|
+
// (via :PRODUCED, injected at write time) or to the
|
|
1488
|
+
// account's :AdminUser when no conversation context exists.
|
|
1489
|
+
// `keywords` is a string array; `sourceWorkflow` names the
|
|
1490
|
+
// originating skill or scheduled workflow (e.g.
|
|
1491
|
+
// "daily-briefing", "dream-cycle"). Embedded so the existing
|
|
1492
|
+
// `memory-search` hybrid pipeline can surface reports
|
|
1493
|
+
// alongside other knowledge.
|
|
1494
|
+
//
|
|
1495
|
+
// Dream-cycle queues (Task 327): :OrphanCandidate and
|
|
1496
|
+
// :CitationProposal hold phase-1 and phase-4 candidates the
|
|
1497
|
+
// operator triages via `memory-review-queue`. Edges to
|
|
1498
|
+
// `:Trashed` targets get a `staleTarget: true` property
|
|
1499
|
+
// from phase 3 (audit preserved; no deletion). :TimelineEvent
|
|
1500
|
+
// citations are written additively as `citations: [string]`
|
|
1501
|
+
// — no new label needed.
|
|
1502
|
+
// ----------------------------------------------------------
|
|
1503
|
+
|
|
1504
|
+
CREATE CONSTRAINT report_id_unique IF NOT EXISTS
|
|
1505
|
+
FOR (r:Report) REQUIRE r.reportId IS UNIQUE;
|
|
1506
|
+
|
|
1507
|
+
CREATE INDEX report_account IF NOT EXISTS
|
|
1508
|
+
FOR (r:Report) ON (r.accountId);
|
|
1509
|
+
|
|
1510
|
+
CREATE INDEX report_occurred_at IF NOT EXISTS
|
|
1511
|
+
FOR (r:Report) ON (r.occurredAt);
|
|
1512
|
+
|
|
1513
|
+
CREATE INDEX report_source_workflow IF NOT EXISTS
|
|
1514
|
+
FOR (r:Report) ON (r.sourceWorkflow);
|
|
1515
|
+
|
|
1516
|
+
CREATE INDEX report_title IF NOT EXISTS
|
|
1517
|
+
FOR (r:Report) ON (r.title);
|
|
1518
|
+
|
|
1519
|
+
CREATE VECTOR INDEX report_embedding IF NOT EXISTS
|
|
1520
|
+
FOR (r:Report) ON (r.embedding)
|
|
1521
|
+
OPTIONS {
|
|
1522
|
+
indexConfig: {
|
|
1523
|
+
`vector.dimensions`: 768,
|
|
1524
|
+
`vector.similarity_function`: 'cosine'
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
CREATE CONSTRAINT orphan_candidate_id_unique IF NOT EXISTS
|
|
1529
|
+
FOR (o:OrphanCandidate) REQUIRE o.orphanCandidateId IS UNIQUE;
|
|
1530
|
+
|
|
1531
|
+
CREATE INDEX orphan_candidate_account IF NOT EXISTS
|
|
1532
|
+
FOR (o:OrphanCandidate) ON (o.accountId);
|
|
1533
|
+
|
|
1534
|
+
CREATE INDEX orphan_candidate_target IF NOT EXISTS
|
|
1535
|
+
FOR (o:OrphanCandidate) ON (o.targetElementId);
|
|
1536
|
+
|
|
1537
|
+
CREATE CONSTRAINT citation_proposal_id_unique IF NOT EXISTS
|
|
1538
|
+
FOR (p:CitationProposal) REQUIRE p.citationProposalId IS UNIQUE;
|
|
1539
|
+
|
|
1540
|
+
CREATE INDEX citation_proposal_account IF NOT EXISTS
|
|
1541
|
+
FOR (p:CitationProposal) ON (p.accountId);
|
|
1542
|
+
|
|
1543
|
+
CREATE INDEX citation_proposal_target IF NOT EXISTS
|
|
1544
|
+
FOR (p:CitationProposal) ON (p.timelineEventId);
|
|
@@ -140,7 +140,7 @@ Tools are available via the `admin` MCP server.
|
|
|
140
140
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
141
141
|
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
|
|
142
142
|
- `hooks/session-end-retrospective.sh` — **Stop hook.** Fires on every assistant end_turn but exits 0 with `trigger-skipped reason=<role-not-admin|is-specialist|empty-stdin|missing-transcript|no-intent-match>` on every path EXCEPT when the operator's latest real-user message carries an end-intent token (`/end`, `/archive`, `end session`, `archive this session`, case-insensitive, must match the whole message or a standalone line — not embedded in prose). On that path the hook blocks (exit 2 + structured retrospective instruction on stderr per Stop-hook contract) and emits `[session-retrospective] gate-blocked sessionId=<id> reason=sentinel-absent` until the admin agent calls the `session-retrospective-mark-complete` MCP sentinel tool. Completion is recognised by greping the operator's own JSONL (`transcript_path` on the Stop envelope) for a `tool_use` block whose `name` equals that exact string — never by parsing prose ([[feedback_no_stdout_parsing_for_control_flow]], [[feedback_doctrine_paragraph_is_not_a_gate]]). The sentinel-grep doubles as the re-entry guard: every Stop after the sentinel call sees it and exits 0 with `gate-released sessionId=<id>`. Recursion guard: any session whose `MAXY_SPECIALIST` env is set (specialist subagent dispatched via Task tool) skips the gate. The Sidebar archive button POSTs `/:sessionId/archive` directly to the manager which kills the PTY — no Stop fires after that, so clicking archive is the operator's current escape hatch (opt-out remains deferred per the task spec). All emits go through `POST /api/admin/log-ingest`; the only stderr writer is the gate-blocked path (the instruction block the agent reads). The retrospective itself runs as one or more additional turns inside the operator's existing admin session — Task-tool delegation to `database-operator` is the existing IDENTITY.md "Recording" route, not a parallel admin session ([[feedback_deterministic_means_remove_llm]]).
|
|
143
|
-
- `hooks/
|
|
143
|
+
- `hooks/post-turn-graph-pass.sh` — **Stop hook.** Task 412 — supersedes the original signal-detector-stop hook (Task 303) which had three structural defects (idle gate measured assistant response duration not operator pause, recursion guard greped the wrong tool name so `newUserTurnsSinceLastRun` never reset, single-excerpt throttle dropped 2 of every 3 operator turns). Fires on every admin-agent `end_turn` alongside `session-end-retrospective.sh`. **No throttle, no idle gate, no in-transcript recursion-guard grep.** One dispatch per `end_turn`, every time. Walks the operator's JSONL once, collects every assistant `text` block and every non-`tool_result` user message as `[role: text]` pairs (oldest first), then `GET /api/admin/post-turn-context?conversationId&accountId` to fetch the actual content of every node already written under this conversationId, and dispatches `database-operator` via exit 2 + stderr directive `[system: post-turn-graph-pass-stop-hook]` per Stop-hook contract. The dispatch prompt carries two interpolated blocks — `<conversation>` and `<prior-writes>` — and the specialist body's background-pass mode reads both verbatim and decides what new graph rows the conversation supports (mentions, ideas, learnings, contacts, tasks, anything the schema admits). Duplicate writes between this background pass and the admin agent's discretionary inline `database-operator` dispatch are accepted at write time; cleanup is delegated to the dream-cycle / hygiene sweeps (Tasks 410, 411). Remaining early exits (the only two): `reason=role-not-admin` (the hook fires only on admin-agent stops by doctrine) and `reason=is-specialist` (`MAXY_SPECIALIST` env-stamped on every subagent PTY blocks the hook from recursing inside the specialist's own stop). All emits go through `POST /api/admin/log-ingest` under tag `post-turn-graph-pass`. The canonical fire line is `dispatch sessionId=<id> conversationId=<id> conversationTurns=<n> priorWritesCount=<n> contextBytes=<n> ms=<n>`; failure to see a matching `db-op: writes=<n> ms=<n>` line in the admin-resume stream within 30 s means the specialist crashed or `memory-write` rejected silently.
|
|
144
144
|
- `hooks/turn-completed-graph-write.sh` — **Dormant since Task 214** — the Stop-hook registration is no longer written to account settings.json; admin delegates graph writes to `database-operator` via the Task tool. The script, envelope walker, loopback `/api/admin/claude-sessions` 127.0.0.1 bypass, `[turn-recorder]` emitters, recorder-auto-archive subscriber, and `initialMessage` envelope spec are preserved as reusable infrastructure for any future autonomous post-turn flow. Historical contract (still describes the dormant path): Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `conversationId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** — every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` → `spawn-request` → `spawn-success` → `input-delivered` → `tool-call` × N → `tool-result` × N → `write-complete` → `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes — `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` — each emit one named line; absence is itself a defect.
|
|
145
145
|
|
|
146
146
|
## Session identifiers (Task 135)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook — post-turn graph pass. Task 412.
|
|
3
|
+
#
|
|
4
|
+
# Fires on every admin-agent assistant end_turn. Dispatches the
|
|
5
|
+
# `database-operator` specialist with the full conversation (as
|
|
6
|
+
# `[role: text]` pairs) plus the actual content of every node already
|
|
7
|
+
# written under this `conversationId`. The specialist decides what new
|
|
8
|
+
# graph rows the conversation supports and writes them.
|
|
9
|
+
#
|
|
10
|
+
# No throttle, no idle gate, no recursion-guard transcript grep. The two
|
|
11
|
+
# remaining early exits are doctrinal:
|
|
12
|
+
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
13
|
+
# - MAXY_SPECIALIST must be empty → reason=is-specialist
|
|
14
|
+
# (Same `MAXY_SPECIALIST` env-stamp model that blocks the hook from
|
|
15
|
+
# recursing inside a subagent's own stop. The specialist sees the
|
|
16
|
+
# operator's transcript via the dispatch prompt, not via its own PTY.)
|
|
17
|
+
#
|
|
18
|
+
# Duplicate writes between this background pass and the admin agent's
|
|
19
|
+
# discretionary inline `database-operator` dispatch are accepted at
|
|
20
|
+
# write time; cleanup is delegated to the dream-cycle / hygiene sweeps
|
|
21
|
+
# (Tasks 410, 411).
|
|
22
|
+
#
|
|
23
|
+
# Input: Claude Code's Stop hook stdin shape
|
|
24
|
+
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
25
|
+
#
|
|
26
|
+
# Output: exit 2 + the dispatch directive on stderr per Stop-hook
|
|
27
|
+
# contract. The admin IDENTITY.md "Stop-hook system directives" section
|
|
28
|
+
# binds the agent's response: perform the single Agent dispatch, emit a
|
|
29
|
+
# single-space ack, stop.
|
|
30
|
+
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
|
|
34
|
+
if [ -z "$UI_PORT" ]; then
|
|
35
|
+
echo "[post-turn-graph-pass] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
UI_BASE="http://127.0.0.1:${UI_PORT}"
|
|
39
|
+
LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
|
|
40
|
+
CONTEXT_URL="${UI_BASE}/api/admin/post-turn-context"
|
|
41
|
+
|
|
42
|
+
emit_log() {
|
|
43
|
+
local line="$1"
|
|
44
|
+
curl -sS -o /dev/null -X POST \
|
|
45
|
+
-H 'Content-Type: application/json' \
|
|
46
|
+
--max-time 2 \
|
|
47
|
+
--data "$(python3 -c '
|
|
48
|
+
import sys, json
|
|
49
|
+
print(json.dumps({"tag": "post-turn-graph-pass", "level": "info", "line": sys.argv[1]}))
|
|
50
|
+
' "$line")" \
|
|
51
|
+
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
INPUT=""
|
|
55
|
+
if [ ! -t 0 ]; then
|
|
56
|
+
INPUT=$(cat)
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
PARSED=$(printf '%s' "$INPUT" | python3 -c '
|
|
60
|
+
import sys, json
|
|
61
|
+
try:
|
|
62
|
+
d = json.load(sys.stdin)
|
|
63
|
+
sid = d.get("session_id", "") or ""
|
|
64
|
+
tpath = d.get("transcript_path", "") or ""
|
|
65
|
+
print(f"{sid}\t{tpath}")
|
|
66
|
+
except Exception:
|
|
67
|
+
print("\t")
|
|
68
|
+
' 2>/dev/null)
|
|
69
|
+
SESSION_ID="${PARSED%% *}"
|
|
70
|
+
TRANSCRIPT_PATH="${PARSED#* }"
|
|
71
|
+
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
72
|
+
|
|
73
|
+
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
74
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
78
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
if [ -z "$INPUT" ]; then
|
|
82
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
86
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
87
|
+
exit 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
CONVERSATION_ID="${CONVERSATION_NODE_ID:-}"
|
|
91
|
+
|
|
92
|
+
# Walk the transcript and emit the conversation as `[role: text]` pairs,
|
|
93
|
+
# oldest first. User turns whose only content is `tool_result` blocks are
|
|
94
|
+
# skipped (they carry the Stop-hook feedback envelope and other tool
|
|
95
|
+
# plumbing, not operator intent). Multi-block assistant turns concatenate
|
|
96
|
+
# their `text` blocks; `tool_use` blocks are dropped.
|
|
97
|
+
CONVERSATION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
98
|
+
import os, json, sys
|
|
99
|
+
|
|
100
|
+
def assistant_text(rec):
|
|
101
|
+
msg = rec.get("message", {}) or {}
|
|
102
|
+
if msg.get("role") != "assistant":
|
|
103
|
+
return ""
|
|
104
|
+
content = msg.get("content")
|
|
105
|
+
if isinstance(content, str):
|
|
106
|
+
return content
|
|
107
|
+
if isinstance(content, list):
|
|
108
|
+
parts = []
|
|
109
|
+
for b in content:
|
|
110
|
+
if isinstance(b, dict) and b.get("type") == "text":
|
|
111
|
+
t = b.get("text")
|
|
112
|
+
if isinstance(t, str):
|
|
113
|
+
parts.append(t)
|
|
114
|
+
return "".join(parts)
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
def user_text(rec):
|
|
118
|
+
msg = rec.get("message", {}) or {}
|
|
119
|
+
if msg.get("role") != "user":
|
|
120
|
+
return None
|
|
121
|
+
content = msg.get("content")
|
|
122
|
+
if isinstance(content, str):
|
|
123
|
+
return content
|
|
124
|
+
if isinstance(content, list):
|
|
125
|
+
parts = []
|
|
126
|
+
saw_non_tool_result = False
|
|
127
|
+
for b in content:
|
|
128
|
+
if not isinstance(b, dict):
|
|
129
|
+
continue
|
|
130
|
+
btype = b.get("type")
|
|
131
|
+
if btype == "tool_result":
|
|
132
|
+
continue
|
|
133
|
+
saw_non_tool_result = True
|
|
134
|
+
if btype == "text":
|
|
135
|
+
t = b.get("text")
|
|
136
|
+
if isinstance(t, str):
|
|
137
|
+
parts.append(t)
|
|
138
|
+
if not saw_non_tool_result:
|
|
139
|
+
return None
|
|
140
|
+
return "".join(parts)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
path = os.environ["TRANSCRIPT_PATH"]
|
|
144
|
+
out_lines = []
|
|
145
|
+
try:
|
|
146
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
147
|
+
for raw in f:
|
|
148
|
+
raw = raw.strip()
|
|
149
|
+
if not raw:
|
|
150
|
+
continue
|
|
151
|
+
try:
|
|
152
|
+
rec = json.loads(raw)
|
|
153
|
+
except Exception:
|
|
154
|
+
continue
|
|
155
|
+
if not isinstance(rec, dict):
|
|
156
|
+
continue
|
|
157
|
+
rtype = rec.get("type")
|
|
158
|
+
if rtype == "user":
|
|
159
|
+
t = user_text(rec)
|
|
160
|
+
if t is None:
|
|
161
|
+
continue
|
|
162
|
+
out_lines.append(f"[user: {t.strip()}]")
|
|
163
|
+
elif rtype == "assistant":
|
|
164
|
+
t = assistant_text(rec)
|
|
165
|
+
if not t.strip():
|
|
166
|
+
continue
|
|
167
|
+
out_lines.append(f"[assistant: {t.strip()}]")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
sys.stderr.write(f"transcript-walk-failed: {e}\n")
|
|
170
|
+
sys.exit(0)
|
|
171
|
+
|
|
172
|
+
print("\n\n".join(out_lines))
|
|
173
|
+
PY
|
|
174
|
+
)
|
|
175
|
+
CONVERSATION_TURNS=$(printf '%s\n' "$CONVERSATION" | grep -c '^\[' || true)
|
|
176
|
+
CONTEXT_BYTES=${#CONVERSATION}
|
|
177
|
+
|
|
178
|
+
# Fetch the actual content of every node already written under this
|
|
179
|
+
# conversation, account-scoped. The route runs loopback-only — the hook
|
|
180
|
+
# is in-process on the device, same trust boundary as `/log-ingest`.
|
|
181
|
+
PRIOR_WRITES="(none)"
|
|
182
|
+
PRIOR_WRITES_COUNT=0
|
|
183
|
+
if [ -n "$CONVERSATION_ID" ]; then
|
|
184
|
+
CONTEXT_JSON=$(curl -sS --max-time 5 -G \
|
|
185
|
+
--data-urlencode "conversationId=${CONVERSATION_ID}" \
|
|
186
|
+
--data-urlencode "accountId=${ACCOUNT_ID:-}" \
|
|
187
|
+
"$CONTEXT_URL" 2>/dev/null || echo '')
|
|
188
|
+
if [ -n "$CONTEXT_JSON" ]; then
|
|
189
|
+
PARSED_CTX=$(printf '%s' "$CONTEXT_JSON" | python3 -c '
|
|
190
|
+
import sys, json
|
|
191
|
+
try:
|
|
192
|
+
d = json.load(sys.stdin)
|
|
193
|
+
except Exception:
|
|
194
|
+
print("0\t")
|
|
195
|
+
sys.exit(0)
|
|
196
|
+
writes = d.get("writes") or []
|
|
197
|
+
if not isinstance(writes, list):
|
|
198
|
+
print("0\t")
|
|
199
|
+
sys.exit(0)
|
|
200
|
+
lines = []
|
|
201
|
+
for w in writes:
|
|
202
|
+
if not isinstance(w, dict):
|
|
203
|
+
continue
|
|
204
|
+
eid = w.get("elementId") or ""
|
|
205
|
+
labels = w.get("labels") or []
|
|
206
|
+
props = w.get("properties") or {}
|
|
207
|
+
if isinstance(labels, list):
|
|
208
|
+
labels_str = ":".join(str(x) for x in labels)
|
|
209
|
+
else:
|
|
210
|
+
labels_str = str(labels)
|
|
211
|
+
try:
|
|
212
|
+
props_str = json.dumps(props, ensure_ascii=False, sort_keys=True)
|
|
213
|
+
except Exception:
|
|
214
|
+
props_str = "{}"
|
|
215
|
+
lines.append(f"- ({eid}) :{labels_str} {props_str}")
|
|
216
|
+
print(f"{len(writes)}\t" + "\n".join(lines))
|
|
217
|
+
' 2>/dev/null)
|
|
218
|
+
PRIOR_WRITES_COUNT="${PARSED_CTX%% *}"
|
|
219
|
+
PRIOR_WRITES_BODY="${PARSED_CTX#* }"
|
|
220
|
+
if [ -n "$PRIOR_WRITES_BODY" ]; then
|
|
221
|
+
PRIOR_WRITES="$PRIOR_WRITES_BODY"
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
emit_log "dispatch sessionId=${SESSION_ID} conversationId=${CONVERSATION_ID:-<unset>} conversationTurns=${CONVERSATION_TURNS} priorWritesCount=${PRIOR_WRITES_COUNT} contextBytes=${CONTEXT_BYTES} ms=0"
|
|
227
|
+
|
|
228
|
+
# Emit the dispatch directive on stderr per Stop-hook contract.
|
|
229
|
+
cat >&2 <<DIRECTIVE
|
|
230
|
+
[system: post-turn-graph-pass-stop-hook]
|
|
231
|
+
Dispatch the database-operator specialist NOW using the Task tool. Do not respond to the operator; this is a background graph-write pass and the operator does not see this turn. After the specialist returns its one-line summary, emit a single space character and stop.
|
|
232
|
+
|
|
233
|
+
Agent dispatch parameters:
|
|
234
|
+
subagent_type: database-operator
|
|
235
|
+
prompt: |
|
|
236
|
+
accountId: ${ACCOUNT_ID:-<unset>}
|
|
237
|
+
sessionId: ${SESSION_ID}
|
|
238
|
+
conversationId: ${CONVERSATION_ID:-<unset>}
|
|
239
|
+
|
|
240
|
+
Background post-turn graph pass. Given the conversation below and the list of nodes already written under this conversationId, derive any new graph rows the conversation supports (mentions, ideas, learnings, contacts, tasks — anything the schema admits) and write them. Skip writes the prior list already covers; rely on MERGE semantics for the rest. Return one line in the form: db-op: writes=<n> ms=<n>
|
|
241
|
+
|
|
242
|
+
<conversation>
|
|
243
|
+
${CONVERSATION}
|
|
244
|
+
</conversation>
|
|
245
|
+
|
|
246
|
+
<prior-writes>
|
|
247
|
+
${PRIOR_WRITES}
|
|
248
|
+
</prior-writes>
|
|
249
|
+
DIRECTIVE
|
|
250
|
+
|
|
251
|
+
exit 2
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: datetime
|
|
3
3
|
description: >
|
|
4
|
-
Timezone queries
|
|
5
|
-
|
|
6
|
-
"
|
|
4
|
+
Timezone queries (current time, conversion, UTC offset, DST) and
|
|
5
|
+
relative-date arithmetic (last Tuesday, two weeks ago, Q3 last year).
|
|
6
|
+
Trigger phrases: "what time is it in", "convert time", "timezone",
|
|
7
|
+
"time difference", "DST", "last X", "N days/weeks/months ago",
|
|
8
|
+
"this/next/previous X", "Qn YYYY", "Qn last year".
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
# Datetime & Timezone Queries
|
|
10
12
|
|
|
11
|
-
Answer timezone-related questions using the
|
|
13
|
+
Answer timezone-related questions and relative-date arithmetic using Node.js — never freelance the math. Relative dates and quarter forms must go through the deterministic one-liner in this skill; the same algorithm runs server-side in the memory plugin's `relative-date.ts`, so chat answers stay consistent with timeline rows the operator sees in the graph.
|
|
12
14
|
|
|
13
15
|
## When to Activate
|
|
14
16
|
|
|
@@ -16,6 +18,7 @@ Answer timezone-related questions using the server's `<datetime>` context and No
|
|
|
16
18
|
- User asks to convert a time between timezones
|
|
17
19
|
- User asks about UTC offset, time difference, or DST status for a location
|
|
18
20
|
- User asks "what time is it?" with a location qualifier
|
|
21
|
+
- User uses a relative date: "last Tuesday", "two weeks ago", "yesterday", "this Christmas", "Q3 last year", "Q1 2025"
|
|
19
22
|
|
|
20
23
|
For simple "what time is it?" without a location, answer directly from the `<datetime>` block in the system prompt — no tool use needed.
|
|
21
24
|
|
|
@@ -84,8 +87,63 @@ For less common cities, use Bash to search the IANA database:
|
|
|
84
87
|
node -e "console.log(Intl.supportedValuesOf('timeZone').filter(z => z.toLowerCase().includes('SEARCH_TERM'.toLowerCase())).join('\n'))"
|
|
85
88
|
```
|
|
86
89
|
|
|
90
|
+
## Relative date arithmetic
|
|
91
|
+
|
|
92
|
+
When the operator references a relative date — "last Tuesday", "two weeks ago", "yesterday", "this Christmas", "Q3 2024", "Q1 last year" — never compute the result by hand. The agent loses track of weekday offsets, leap-day arithmetic, and quarter-start months. Use this deterministic one-liner. The reference timestamp defaults to now; pass an ISO string as the second argument to anchor against an earlier moment (e.g. when the operator's message timestamp matters).
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
node -e "
|
|
96
|
+
const REF = process.argv[2] ? new Date(process.argv[2]) : new Date();
|
|
97
|
+
const TXT = process.argv[1];
|
|
98
|
+
const WEEKDAYS = {sun:0,sunday:0,mon:1,monday:1,tue:2,tues:2,tuesday:2,wed:3,weds:3,wednesday:3,thu:4,thur:4,thurs:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};
|
|
99
|
+
const NUMS = {one:1,two:2,three:3,four:4,five:5,six:6,seven:7,eight:8,nine:9,ten:10,eleven:11,twelve:12};
|
|
100
|
+
const sod = d => { const o=new Date(d); o.setUTCHours(0,0,0,0); return o; };
|
|
101
|
+
const addD = (d,n) => { const o=new Date(d); o.setUTCDate(o.getUTCDate()+n); return o; };
|
|
102
|
+
const addM = (d,n) => { const o=new Date(d); o.setUTCMonth(o.getUTCMonth()+n); return o; };
|
|
103
|
+
const addY = (d,n) => { const o=new Date(d); o.setUTCFullYear(o.getUTCFullYear()+n); return o; };
|
|
104
|
+
const base = sod(REF);
|
|
105
|
+
let m;
|
|
106
|
+
if (/\\bevery(\\s+other)?\\s+/i.test(TXT)) { console.log('null (recurring)'); process.exit(0); }
|
|
107
|
+
if (m = TXT.match(/\\b(yesterday|today|tomorrow)\\b/i)) {
|
|
108
|
+
const w = m[1].toLowerCase();
|
|
109
|
+
console.log((w==='yesterday'?addD(base,-1):w==='tomorrow'?addD(base,1):base).toISOString());
|
|
110
|
+
} else if (m = TXT.match(/\\b(last|next|this)\\s+([A-Za-z]+)\\b/i)) {
|
|
111
|
+
const q = m[1].toLowerCase(), t = WEEKDAYS[m[2].toLowerCase()];
|
|
112
|
+
if (t === undefined) { console.log('null'); process.exit(0); }
|
|
113
|
+
const dow = base.getUTCDay();
|
|
114
|
+
let delta;
|
|
115
|
+
if (q==='last') { delta = t-dow; if (delta>=0) delta-=7; }
|
|
116
|
+
else if (q==='next') { delta = t-dow; if (delta<=0) delta+=7; }
|
|
117
|
+
else { const id = dow===0?7:dow, it = t===0?7:t; delta = it-id; }
|
|
118
|
+
console.log(addD(base, delta).toISOString());
|
|
119
|
+
} else if (m = TXT.match(/\\b(a|an|\\d+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\\s+(day|week|month|year)s?\\s+ago\\b/i)) {
|
|
120
|
+
const w = m[1].toLowerCase();
|
|
121
|
+
const n = (w==='a'||w==='an')?1:(NUMS[w]??Number(w));
|
|
122
|
+
const u = m[2].toLowerCase();
|
|
123
|
+
const d = u==='day'?addD(base,-n):u==='week'?addD(base,-7*n):u==='month'?addM(base,-n):addY(base,-n);
|
|
124
|
+
console.log(d.toISOString());
|
|
125
|
+
} else if (TXT.match(/\\b(?:this\\s+)?Christmas\\b/i)) {
|
|
126
|
+
console.log(new Date(Date.UTC(base.getUTCFullYear(),11,25)).toISOString());
|
|
127
|
+
} else if (TXT.match(/\\b(?:this\\s+)?New\\s+Year(?:'s)?(?:\\s+Day)?\\b/i)) {
|
|
128
|
+
console.log(new Date(Date.UTC(base.getUTCFullYear(),0,1)).toISOString());
|
|
129
|
+
} else if (m = TXT.match(/\\bQ([1-4])\\s+(\\d{2}|\\d{4})\\b/i)) {
|
|
130
|
+
let y = Number(m[2]); if (y<100) y+=2000;
|
|
131
|
+
console.log(new Date(Date.UTC(y,(Number(m[1])-1)*3,1)).toISOString());
|
|
132
|
+
} else if (m = TXT.match(/\\bQ([1-4])\\s+(last|this|next)\\s+year\\b/i)) {
|
|
133
|
+
const refY = base.getUTCFullYear();
|
|
134
|
+
const y = m[2].toLowerCase()==='last'?refY-1:m[2].toLowerCase()==='next'?refY+1:refY;
|
|
135
|
+
console.log(new Date(Date.UTC(y,(Number(m[1])-1)*3,1)).toISOString());
|
|
136
|
+
} else { console.log('null'); }
|
|
137
|
+
" "PHRASE" "OPTIONAL_REF_ISO"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Replace `PHRASE` with the relative phrase ("last Tuesday", "Q3 2024") and optionally `OPTIONAL_REF_ISO` with the ISO reference timestamp.
|
|
141
|
+
|
|
142
|
+
The algorithm mirrors `platform/plugins/memory/mcp/src/lib/relative-date.ts` — the server-side parser the timeline extractor uses. Recurring forms ("every other Friday") return `null` by design. Phrases that don't match return `null`.
|
|
143
|
+
|
|
87
144
|
## Boundaries
|
|
88
145
|
|
|
89
|
-
- Answers timezone questions only — not scheduling, reminders, or alarms.
|
|
146
|
+
- Answers timezone questions and relative-date arithmetic only — not scheduling, reminders, or alarms.
|
|
90
147
|
- Uses the server's system clock. If the server clock is wrong, answers will be wrong.
|
|
91
148
|
- Historical timezone data (e.g., "what time was it in Tokyo on 15 March 1990?") depends on the Node.js ICU dataset and may be inaccurate for dates before 1970.
|
|
149
|
+
- Relative-date parser handles single-date phrases only; multi-date ranges ("from March through May") are out of scope.
|
|
@@ -297,6 +297,22 @@ This makes the failure visible to the agent, which can relay it to the user. Deg
|
|
|
297
297
|
|
|
298
298
|
---
|
|
299
299
|
|
|
300
|
+
## Reports — durable workflow output (Task 332)
|
|
301
|
+
|
|
302
|
+
The `:Report` label is the platform's durable shape for workflow output the operator may want back later — daily briefings, dream cycle runs, ad-hoc analyses. Three MCP tools own the surface, all on the memory plugin:
|
|
303
|
+
|
|
304
|
+
- `memory-report-write` — append-only writer. Validates body ≤ 10,000 chars, embeds title+body, and CREATEs a `:Report` node. Idempotent on `(accountId, title, occurredAt-within-same-minute)` — a second call with the same title in the same minute returns the existing node instead of duplicating. Parented to the active `:Conversation` via `:PRODUCED` when `CONVERSATION_NODE_ID` is set (the chat-driven default); falls back to the account's `:AdminUser` so the graph-hierarchy doctrine holds even outside a conversation.
|
|
305
|
+
- `memory-report-read-latest` — fetches the newest `:Report` (default `limit=1`) tagged with a given keyword. The expected route for any operator phrasing of "latest X", "last night's X", "show me X report".
|
|
306
|
+
- `memory-report-list` — metadata-only paginated listing (newest first), with optional `keyword` and `sourceWorkflow` filters. Use to scan the catalogue without paying for full bodies.
|
|
307
|
+
|
|
308
|
+
Every operation emits one log line: `[reports] op=<write|read-latest|list> reportId=<short> keywords=<csv> ms=<n>` (with `idempotent=1` on a write that resolved to an existing node, `hits=<n>` on reads, `total=<n>` on list).
|
|
309
|
+
|
|
310
|
+
Routing is not classifier-side. The admin agent's `IDENTITY.md` carries the rule under **Recalling reports**: "latest <X>" / "last night's <X>" / "show me <X> report" → first tool call is `memory-report-read-latest`. The intent classifier (Task 304's `retrievalClass`) already differentiates temporal vs entity vs event reads; reports route off the literal phrase, not a new class.
|
|
311
|
+
|
|
312
|
+
The first caller is the `briefing` skill (`platform/plugins/scheduling/skills/briefing/SKILL.md`), which persists each run as a `:Report` with `title: "Daily briefing <YYYY-MM-DD>"`, `keywords: ["daily-briefing", "<YYYY-MM-DD>"]`, `sourceWorkflow: "daily-briefing"`. Dream-cycle (Task 327) and ad-hoc analyses are expected to follow the same pattern.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
300
316
|
## Graph Expansion
|
|
301
317
|
|
|
302
318
|
After the top results are selected (by combined score or by LLM ranking), each result node is expanded by traversing its immediate relationships.
|
|
@@ -116,6 +116,16 @@ Every node also carries a provenance stamp — which agent wrote it, in which se
|
|
|
116
116
|
|
|
117
117
|
**Two write surfaces, one substrate.** General agents write through schema-aware helpers — {{productName}} can record a new contact, a new commitment, a new preference without ever typing a database query, and the helper enforces the connection-and-provenance rule above structurally. The graph-steward role (the specialist {{productName}} dispatches when you ask for graph hygiene — "merge those two duplicate contacts," "wire those four tasks to the meeting," "rename the legacy label across the graph") additionally has a raw Cypher write tool for the multi-step operations the helpers cannot express. The steward role internalises the same connection-and-provenance discipline in its prompt; a post-write audit emits a warning on every breach so the same rules apply to both surfaces. Both paths feed the same hourly orphan trend and the same forensic provenance fields — read-side, you cannot tell the two apart, and that is the point.
|
|
118
118
|
|
|
119
|
+
## Public-facing summaries for customer-readable subjects
|
|
120
|
+
|
|
121
|
+
Some entities in your graph are knowable by people outside your team — companies you work with, projects you've delivered, the business itself. For those entities ({{productName}} treats `:Organization`, `:Concept`, `:Project`, and `:LocalBusiness` this way), {{productName}} maintains two summaries: a private one only you and your specialist agents see, and a customer-facing public one your public agents are allowed to surface.
|
|
122
|
+
|
|
123
|
+
Whenever {{productName}} updates the private summary on one of these entities, it automatically rewrites the public summary in the same step using a separate prompt that strips operator-voice ("needs follow-up", "action: chase next week"), internal sentiment, and anything that reads like a note-to-self. The two summaries stay in lockstep without you doing anything.
|
|
124
|
+
|
|
125
|
+
If you want to write the public summary yourself — for instance, because the auto-generated version misses something you want customers to see — just tell {{productName}} the wording you want for the public summary on that entity, and {{productName}} will write it directly. It stays locked in for seven days; after that, the next automatic refresh can take over again, unless you re-pin it.
|
|
126
|
+
|
|
127
|
+
People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.
|
|
128
|
+
|
|
119
129
|
## Privacy
|
|
120
130
|
|
|
121
131
|
All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
|
|
@@ -54,7 +54,7 @@ Roles are installed during setup and listed when {{productName}} introduces itse
|
|
|
54
54
|
|
|
55
55
|
{{productName}} maintains a graph database (Neo4j) of everything you've told it. People, conversations, preferences, and context are stored as connected nodes. When you ask {{productName}} something, it searches this graph to retrieve relevant context before responding.
|
|
56
56
|
|
|
57
|
-
**The recording loop.**
|
|
57
|
+
**The recording loop.** Once your turn finishes and the reply is on screen, a Stop hook (`post-turn-graph-pass.sh`) fires the `database-operator` specialist as a background dispatch. The dispatch carries the full conversation as `[role: text]` pairs plus the list of every node already written under this conversation, fetched via `GET /api/admin/post-turn-context`. The specialist decides what new graph rows the conversation supports — mentions of named entities, ideas, learnings, contacts, tasks, anything the schema admits — and writes them. Duplicate writes between this background pass and any inline write the admin agent dispatched mid-turn are accepted and cleaned up by the dream-cycle / hygiene sweeps. There is no throttle and no idle gate: one dispatch per `end_turn`. The admin agent can also dispatch `database-operator` inline at its own discretion when a write must complete before the assistant response ends. To disable the background pass for an account, remove the `post-turn-graph-pass.sh` Stop-hook entry from that account's `~/.<brand>/.claude/settings.json`; there is no toggle.
|
|
58
58
|
|
|
59
59
|
The memory graph is stored on your Pi. It never leaves your network.
|
|
60
60
|
|