@rubytech/create-maxy 1.0.421 → 1.0.422

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.421",
3
+ "version": "1.0.422",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -3822,7 +3822,7 @@ async function loadSessionContext(accountId) {
3822
3822
  try {
3823
3823
  const summaryResult = await session.run(
3824
3824
  `MATCH (n:CreativeWork {accountId: $accountId})
3825
- WHERE n.title STARTS WITH 'Session Summary'
3825
+ WHERE n.title STARTS WITH 'Session Summary' OR n.title = 'Session Snapshot'
3826
3826
  RETURN n.title AS title, n.createdAt AS createdAt, n.abstract AS abstract
3827
3827
  ORDER BY n.createdAt DESC LIMIT 1`,
3828
3828
  { accountId }
@@ -3852,14 +3852,24 @@ async function loadSessionContext(accountId) {
3852
3852
  { accountId, limit: neo4j.int(MAX_SESSION_TASKS) }
3853
3853
  );
3854
3854
  const sections = [];
3855
+ const STALENESS_LIMIT_MS = 48 * 60 * 60 * 1e3;
3855
3856
  if (summaryResult.records.length > 0) {
3856
3857
  const rec = summaryResult.records[0];
3857
3858
  const title = rec.get("title");
3858
3859
  const createdAt = rec.get("createdAt");
3859
3860
  const abstract = rec.get("abstract");
3860
- const dateSuffix = createdAt ? ` (${createdAt.slice(0, 10)})` : "";
3861
- sections.push(`## Last Session${dateSuffix}
3861
+ let isStale = false;
3862
+ if (createdAt) {
3863
+ const ageMs = Date.now() - new Date(createdAt).getTime();
3864
+ isStale = !isNaN(ageMs) && ageMs > STALENESS_LIMIT_MS;
3865
+ }
3866
+ if (isStale) {
3867
+ console.error(`[session-context] Skipped stale summary for ${accountId.slice(0, 8)}\u2026: title="${title}" createdAt=${createdAt}`);
3868
+ } else {
3869
+ const dateSuffix = createdAt ? ` (${createdAt.slice(0, 10)})` : "";
3870
+ sections.push(`## Last Session${dateSuffix}
3862
3871
  ${abstract}`);
3872
+ }
3863
3873
  }
3864
3874
  if (digestResult.records.length > 0) {
3865
3875
  const rec = digestResult.records[0];
@@ -3885,8 +3895,9 @@ ${abstract}`);
3885
3895
  ${taskLines.join("\n")}`);
3886
3896
  }
3887
3897
  if (sections.length === 0) return null;
3898
+ const summaryCreatedAt = summaryResult.records.length > 0 ? summaryResult.records[0].get("createdAt") ?? "unknown" : "n/a";
3888
3899
  console.error(
3889
- `[session-context] Loaded for ${accountId.slice(0, 8)}\u2026: summary=${summaryResult.records.length > 0 ? "yes" : "no"}, digest=${digestResult.records.length > 0 ? "yes" : "no"}, tasks=${tasksResult.records.length}`
3900
+ `[session-context] Loaded for ${accountId.slice(0, 8)}\u2026: summary=${summaryResult.records.length > 0 ? "yes" : "no"}, summaryDate=${summaryCreatedAt}, digest=${digestResult.records.length > 0 ? "yes" : "no"}, tasks=${tasksResult.records.length}`
3890
3901
  );
3891
3902
  return `<previous-context>
3892
3903
  ${sections.join("\n\n")}
@@ -3898,6 +3909,22 @@ ${sections.join("\n\n")}
3898
3909
  await session.close();
3899
3910
  }
3900
3911
  }
3912
+ async function writeSessionSummary(accountId, summary) {
3913
+ const session = getSession();
3914
+ try {
3915
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
3916
+ await session.run(
3917
+ `MERGE (n:CreativeWork {accountId: $accountId, title: 'Session Snapshot'})
3918
+ SET n.abstract = $summary, n.createdAt = $createdAt, n.scope = 'admin'`,
3919
+ { accountId, summary, createdAt }
3920
+ );
3921
+ console.error(`[session-summary] Written for ${accountId.slice(0, 8)}\u2026`);
3922
+ } catch (err) {
3923
+ console.error(`[session-summary] Failed for ${accountId.slice(0, 8)}\u2026: ${err instanceof Error ? err.message : String(err)}`);
3924
+ } finally {
3925
+ await session.close();
3926
+ }
3927
+ }
3901
3928
  async function loadOnboardingStep(accountId) {
3902
3929
  const session = getSession();
3903
3930
  try {
@@ -4777,7 +4804,16 @@ function validateSession(sessionKey, agentType) {
4777
4804
  if (!session) return false;
4778
4805
  if (session.agentType !== agentType) return false;
4779
4806
  if (Date.now() - session.createdAt > 24 * 60 * 60 * 1e3) {
4807
+ const history = session.messageHistory;
4808
+ const expiredAccountId = session.accountId;
4780
4809
  sessionStore.delete(sessionKey);
4810
+ if (history && history.length >= 2) {
4811
+ writeEndOfTurnSummary(expiredAccountId, history).catch(
4812
+ (err) => console.error(`[session-summary] expiry write failed: ${err instanceof Error ? err.message : String(err)}`)
4813
+ );
4814
+ } else {
4815
+ console.error(`[session-summary] Session ${sessionKey.slice(0, 8)}\u2026 expired without summary \u2014 ${history?.length ?? 0} messages`);
4816
+ }
4781
4817
  return false;
4782
4818
  }
4783
4819
  if (session.grantExpiresAt && Date.now() > session.grantExpiresAt) {
@@ -5061,6 +5097,80 @@ function getAdminAllowedTools(enabledPlugins) {
5061
5097
  }
5062
5098
  return tools;
5063
5099
  }
5100
+ var QUERY_CLASSIFIER_MODEL = "claude-haiku-4-5-20251001";
5101
+ var QUERY_CLASSIFIER_TIMEOUT_MS = 3e3;
5102
+ var QUERY_CLASSIFIER_MSG_CAP = 500;
5103
+ var QUERY_CLASSIFIER_HISTORY_TURNS = 4;
5104
+ var QUERY_CLASSIFIER_FALLBACK = {
5105
+ search: true,
5106
+ query: null,
5107
+ // null means "use raw message"
5108
+ reason: "classifier-fallback"
5109
+ };
5110
+ var QUERY_CLASSIFIER_SYSTEM = `You decide whether a user message to a business assistant requires a knowledge base search.
5111
+
5112
+ The knowledge base contains the business's products, services, pricing, policies, and domain expertise. It does NOT contain conversation history \u2014 the assistant already has that.
5113
+
5114
+ Respond with ONLY a JSON object: {"search": boolean, "query": string | null, "reason": string}
5115
+
5116
+ Set search to false when:
5117
+ - The message is a greeting, farewell, acknowledgement, or filler ("hello", "thanks", "ok", "bye")
5118
+ - The message is about session state ("continue where we left off", "what were we discussing")
5119
+ - The conversation history already contains sufficient information to answer the question
5120
+ - The message is a conversational response that doesn't require new factual knowledge
5121
+
5122
+ Set search to true when:
5123
+ - The message asks about products, services, pricing, capabilities, or business information
5124
+ - The message introduces a new topic not already covered in the conversation
5125
+ - The message requires factual knowledge the assistant wouldn't have from conversation alone
5126
+
5127
+ When search is true, set query to a search-optimised phrase that captures the information need. Resolve references from conversation history \u2014 e.g. if the user says "tell me more about the first one" and the history mentions "property valuations", set query to "property valuations details". If the raw message is already a good search query, use it as-is.
5128
+
5129
+ When search is false, set query to null and reason to a brief category (e.g. "greeting", "acknowledgement", "session-reference", "context-sufficient").`;
5130
+ async function classifyMemoryQuery(message, history) {
5131
+ let apiKey = process.env.ANTHROPIC_API_KEY;
5132
+ if (!apiKey) {
5133
+ try {
5134
+ apiKey = readFileSync5(API_KEY_FILE, "utf-8").trim();
5135
+ } catch {
5136
+ }
5137
+ }
5138
+ if (!apiKey) return QUERY_CLASSIFIER_FALLBACK;
5139
+ const recentHistory = history.slice(-QUERY_CLASSIFIER_HISTORY_TURNS);
5140
+ const transcript = recentHistory.length > 0 ? recentHistory.map((m) => `[${m.role}] ${m.content.slice(0, QUERY_CLASSIFIER_MSG_CAP)}`).join("\n") : "(no conversation history \u2014 this is the first message)";
5141
+ const userContent = [
5142
+ `Conversation history:
5143
+ ${transcript}`,
5144
+ `
5145
+ Current message:
5146
+ ${message.slice(0, QUERY_CLASSIFIER_MSG_CAP)}`
5147
+ ].join("\n");
5148
+ const controller = new AbortController();
5149
+ const timer = setTimeout(() => controller.abort(), QUERY_CLASSIFIER_TIMEOUT_MS);
5150
+ try {
5151
+ const client = new Anthropic({ apiKey });
5152
+ const response = await client.messages.create({
5153
+ model: QUERY_CLASSIFIER_MODEL,
5154
+ max_tokens: 150,
5155
+ system: QUERY_CLASSIFIER_SYSTEM,
5156
+ messages: [{ role: "user", content: userContent }]
5157
+ }, { signal: controller.signal });
5158
+ const text = response.content.filter((b) => b.type === "text").map((b) => "text" in b ? b.text : "").join("").trim();
5159
+ if (!text) return QUERY_CLASSIFIER_FALLBACK;
5160
+ const jsonStr = text.replace(/^```json\s*/i, "").replace(/```\s*$/, "").trim();
5161
+ const parsed = JSON.parse(jsonStr);
5162
+ if (typeof parsed.search !== "boolean") return QUERY_CLASSIFIER_FALLBACK;
5163
+ return {
5164
+ search: parsed.search,
5165
+ query: parsed.search && typeof parsed.query === "string" ? parsed.query.slice(0, QUERY_CLASSIFIER_MSG_CAP) : null,
5166
+ reason: typeof parsed.reason === "string" ? parsed.reason.replace(/[\n\r]/g, " ").slice(0, 100) : "unknown"
5167
+ };
5168
+ } catch {
5169
+ return QUERY_CLASSIFIER_FALLBACK;
5170
+ } finally {
5171
+ clearTimeout(timer);
5172
+ }
5173
+ }
5064
5174
  async function fetchMemoryContext(accountId, query, sessionKey, options) {
5065
5175
  const serverPath = resolve4(PLATFORM_ROOT3, "plugins/memory/mcp/dist/index.js");
5066
5176
  if (!existsSync5(serverPath)) {
@@ -5456,6 +5566,65 @@ Extract preference updates as JSON array.`
5456
5566
  clearTimeout(timer);
5457
5567
  }
5458
5568
  }
5569
+ var SESSION_SUMMARY_PROMPT = `You are summarising a conversation between a business assistant and its owner. Write a 3-5 sentence summary covering:
5570
+ - What was discussed and accomplished this session
5571
+ - Any tasks created, decisions made, or state changes
5572
+ - Current state of any ongoing work
5573
+ - What the owner is likely to want to continue next
5574
+
5575
+ Be factual and specific. Use present tense for current state. Do not include greetings or meta-commentary.`;
5576
+ var SESSION_SUMMARY_TIMEOUT_MS = 1e4;
5577
+ var lastSummaryWrite = /* @__PURE__ */ new Map();
5578
+ var SUMMARY_RATE_LIMIT_MS = 5 * 60 * 1e3;
5579
+ async function generateSessionSummary(history) {
5580
+ if (history.length < 2) return null;
5581
+ let apiKey = process.env.ANTHROPIC_API_KEY;
5582
+ if (!apiKey) {
5583
+ try {
5584
+ apiKey = readFileSync5(API_KEY_FILE, "utf-8").trim();
5585
+ } catch {
5586
+ return null;
5587
+ }
5588
+ }
5589
+ if (!apiKey) {
5590
+ console.error("[session-summary] Skipping \u2014 no API key available");
5591
+ return null;
5592
+ }
5593
+ const transcript = history.slice(-20).map((m) => `[${m.role}] ${m.content}`).join("\n\n");
5594
+ const controller = new AbortController();
5595
+ const timer = setTimeout(() => controller.abort(), SESSION_SUMMARY_TIMEOUT_MS);
5596
+ try {
5597
+ const client = new Anthropic({ apiKey });
5598
+ const response = await client.messages.create({
5599
+ model: REFLECTION_MODEL,
5600
+ max_tokens: 500,
5601
+ system: SESSION_SUMMARY_PROMPT,
5602
+ messages: [{ role: "user", content: `Conversation:
5603
+ ${transcript}` }]
5604
+ }, { signal: controller.signal });
5605
+ return response.content.filter((b) => b.type === "text").map((b) => "text" in b ? b.text : "").join("");
5606
+ } catch (err) {
5607
+ if (err instanceof Error && err.name === "AbortError") {
5608
+ console.error(`[session-summary] Haiku call timed out after ${SESSION_SUMMARY_TIMEOUT_MS}ms`);
5609
+ } else {
5610
+ console.error(`[session-summary] Haiku call failed: ${err instanceof Error ? err.message : String(err)}`);
5611
+ }
5612
+ return null;
5613
+ } finally {
5614
+ clearTimeout(timer);
5615
+ }
5616
+ }
5617
+ async function writeEndOfTurnSummary(accountId, history) {
5618
+ const now = Date.now();
5619
+ const lastWrite = lastSummaryWrite.get(accountId) ?? 0;
5620
+ if (now - lastWrite < SUMMARY_RATE_LIMIT_MS) {
5621
+ return;
5622
+ }
5623
+ const summary = await generateSessionSummary(history);
5624
+ if (!summary) return;
5625
+ await writeSessionSummary(accountId, summary);
5626
+ lastSummaryWrite.set(accountId, Date.now());
5627
+ }
5459
5628
  var MANAGED_WORK_BUDGET_RATIO = 0.5;
5460
5629
  var TOOL_SCHEMA_OVERHEAD_TOKENS = 15e3;
5461
5630
  function estimateTokens(text) {
@@ -6503,6 +6672,9 @@ async function* invokeManagedAdminAgent(message, systemPrompt, accountDir, accou
6503
6672
  if (responseText) persistMessage(convId, "assistant", responseText, accountId, capturedTokens, assistantTimestamp).catch(() => {
6504
6673
  });
6505
6674
  }
6675
+ writeEndOfTurnSummary(accountId, afterHistory).catch(
6676
+ (err) => console.error(`[session-summary] end-of-turn write failed: ${err instanceof Error ? err.message : String(err)}`)
6677
+ );
6506
6678
  }
6507
6679
  yield event;
6508
6680
  }
@@ -6581,18 +6753,32 @@ async function* invokePublicAgent(message, systemPrompt, accountId, accountDir,
6581
6753
  }
6582
6754
  }
6583
6755
  }
6756
+ const history = sessionKey ? getMessageHistory(sessionKey) : [];
6584
6757
  const fetchOptions = {
6585
6758
  ...agentSlug ? { agentSlug } : {},
6586
6759
  ...knowledgeKeywords && knowledgeKeywords.length > 0 ? { knowledgeKeywords } : {}
6587
6760
  };
6761
+ const needsClassification = !(knowledgeBaked && !liveMemory);
6762
+ const classifyStartMs = Date.now();
6763
+ const classification = needsClassification ? await classifyMemoryQuery(message, history) : QUERY_CLASSIFIER_FALLBACK;
6764
+ const classifyMs = Date.now() - classifyStartMs;
6765
+ const effectiveQuery = classification.query ?? message;
6766
+ if (needsClassification) {
6767
+ streamLog.write(`[${isoTs()}] [public-query-classifier] search=${classification.search} effectiveQuery=${JSON.stringify(effectiveQuery.slice(0, 200))} reason=${classification.reason} latencyMs=${classifyMs}
6768
+ `);
6769
+ }
6588
6770
  let system;
6589
6771
  if (knowledgeBaked && !liveMemory) {
6590
6772
  streamLog.write(`[${isoTs()}] [public-knowledge-source] baked \u2014 skipping memory-search (liveMemory=false)
6773
+ `);
6774
+ system = systemPrompt;
6775
+ } else if (!classification.search) {
6776
+ streamLog.write(`[${isoTs()}] [public-knowledge-source] skipped \u2014 classifier (reason=${classification.reason})
6591
6777
  `);
6592
6778
  system = systemPrompt;
6593
6779
  } else if (knowledgeBaked && liveMemory) {
6594
- const memoryContext = await fetchMemoryContext(accountId, message, sessionKey, fetchOptions);
6595
- streamLog.write(`[${isoTs()}] [public-knowledge-source] baked+live agentSlug=${agentSlug ?? "none"} keywords=${knowledgeKeywords?.length ?? 0} result=${memoryContext ? `ok (${memoryContext.length} chars)` : "null"}
6780
+ const memoryContext = await fetchMemoryContext(accountId, effectiveQuery, sessionKey, fetchOptions);
6781
+ streamLog.write(`[${isoTs()}] [public-knowledge-source] baked+live agentSlug=${agentSlug ?? "none"} keywords=${knowledgeKeywords?.length ?? 0} effectiveQuery=${JSON.stringify(effectiveQuery.slice(0, 200))} result=${memoryContext ? `ok (${memoryContext.length} chars)` : "null"}
6596
6782
  `);
6597
6783
  system = memoryContext ? `${systemPrompt}
6598
6784
 
@@ -6600,8 +6786,8 @@ async function* invokePublicAgent(message, systemPrompt, accountId, accountDir,
6600
6786
  ${memoryContext}
6601
6787
  </live-memory>` : systemPrompt;
6602
6788
  } else {
6603
- const memoryContext = await fetchMemoryContext(accountId, message, sessionKey, fetchOptions);
6604
- streamLog.write(`[${isoTs()}] [public-knowledge-source] memory-search agentSlug=${agentSlug ?? "none"} keywords=${knowledgeKeywords?.length ?? 0} result=${memoryContext ? `ok (${memoryContext.length} chars)` : "null \u2014 agent has no knowledge"}
6789
+ const memoryContext = await fetchMemoryContext(accountId, effectiveQuery, sessionKey, fetchOptions);
6790
+ streamLog.write(`[${isoTs()}] [public-knowledge-source] memory-search agentSlug=${agentSlug ?? "none"} keywords=${knowledgeKeywords?.length ?? 0} effectiveQuery=${JSON.stringify(effectiveQuery.slice(0, 200))} result=${memoryContext ? `ok (${memoryContext.length} chars)` : "null \u2014 agent has no knowledge"}
6605
6791
  `);
6606
6792
  if (memoryContext) {
6607
6793
  streamLog.write(`[${isoTs()}] [public-memory-context] ${JSON.stringify(memoryContext.slice(0, 500))}${memoryContext.length > 500 ? "\u2026" : ""}
@@ -6613,7 +6799,6 @@ ${memoryContext}
6613
6799
  ${memoryContext}
6614
6800
  </memory>` : systemPrompt;
6615
6801
  }
6616
- const history = sessionKey ? getMessageHistory(sessionKey) : [];
6617
6802
  const sdkMessages = [
6618
6803
  ...history.map((m) => ({ role: m.role, content: m.content })),
6619
6804
  { role: "user", content: message }
@@ -87,9 +87,14 @@ For the most recent session: call `session-list` with `limit: 1`, then call `ses
87
87
 
88
88
  ## Session Memory
89
89
 
90
- The platform automatically injects recalled context into your system prompt as a `<previous-context>` section. This includes the most recent session summary and all open/active tasks. Treat this as background context it informs your awareness but does not override the current conversation.
90
+ The platform automatically injects recalled context into your system prompt as a `<previous-context>` section. This includes the most recent session summary and all open/active tasks. The platform rejects stale summaries (older than 48 hours) when present, the summary is recent and trustworthy.
91
91
 
92
- When `<previous-context>` is present, use it to orient yourself and greet the owner with awareness of prior work and outstanding tasks. When absent, Neo4j was unreachable or no prior context exists — proceed normally.
92
+ When `<previous-context>` is present:
93
+ - **Trust the session summary for state orientation.** It reflects the end of the most recent session. Do not re-verify claims already described — if the summary says onboarding is complete, do not call `onboarding-get`. If it names tasks in progress, acknowledge and resume them.
94
+ - **Do not re-research context captured in the summary.** When the summary describes work in progress, outstanding tasks, or recent decisions, pick up from that state. Redundant memory-search or tool calls for information already in the summary waste the owner's time.
95
+ - Use it to greet the owner with awareness of prior work and outstanding tasks.
96
+
97
+ When `<previous-context>` is absent, Neo4j was unreachable or no prior context exists — proceed normally, using tool calls to establish state.
93
98
 
94
99
  In managed context mode, conversation history is provided within `<conversation-history>` tags. Use `session-compact-status` to retrieve older archived context if needed.
95
100