Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ targets:
- name: npm
id: "@sentry/junior-linear"
includeNames: /^sentry-junior-linear-\d.*\.tgz$/
- name: npm
id: "@sentry/junior-memory"
includeNames: /^sentry-junior-memory-\d.*\.tgz$/
- name: npm
id: "@sentry/junior-notion"
includeNames: /^sentry-junior-notion-\d.*\.tgz$/
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ jobs:
pnpm --filter @sentry/junior-github pack --pack-destination artifacts
pnpm --filter @sentry/junior-hex pack --pack-destination artifacts
pnpm --filter @sentry/junior-linear pack --pack-destination artifacts
pnpm --filter @sentry/junior-memory pack --pack-destination artifacts
pnpm --filter @sentry/junior-notion pack --pack-destination artifacts
pnpm --filter @sentry/junior-scheduler pack --pack-destination artifacts
pnpm --filter @sentry/junior-maintenance pack --pack-destination artifacts
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ This repo uses Craft for manual lockstep npm releases of:
- `@sentry/junior-hex`
- `@sentry/junior-linear`
- `@sentry/junior-maintenance`
- `@sentry/junior-memory`
- `@sentry/junior-notion`
- `@sentry/junior-scheduler`
- `@sentry/junior-sentry`
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Start here:
| `@sentry/junior-github` | GitHub plugin package for issue workflows |
| `@sentry/junior-hex` | Hex plugin package for data warehouse query workflows |
| `@sentry/junior-linear` | Linear plugin package for issue workflows |
| `@sentry/junior-memory` | Memory plugin package for long-term Junior memory storage |
| `@sentry/junior-notion` | Notion plugin package for page search workflows |
| `@sentry/junior-scheduler` | Scheduler plugin package for scheduled Junior tasks |
| `@sentry/junior-maintenance` | Maintenance plugin package for updating and improving Junior apps |
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
"docs:dev": "pnpm --filter @sentry/junior-docs dev",
"docs:build": "pnpm --filter @sentry/junior-docs build",
"docs:check": "pnpm --filter @sentry/junior-docs check",
"package:lint": "for pkg in packages/junior packages/junior-plugin-api packages/junior-scheduler packages/junior-dashboard packages/junior-github packages/junior-agent-browser packages/junior-datadog packages/junior-hex packages/junior-linear packages/junior-maintenance packages/junior-notion packages/junior-sentry packages/junior-vercel; do pnpm exec publint \"$pkg\" || exit $?; done",
"package:lint": "for pkg in packages/junior packages/junior-plugin-api packages/junior-scheduler packages/junior-memory packages/junior-dashboard packages/junior-github packages/junior-agent-browser packages/junior-datadog packages/junior-hex packages/junior-linear packages/junior-maintenance packages/junior-notion packages/junior-sentry packages/junior-vercel; do pnpm exec publint \"$pkg\" || exit $?; done",
"release:check": "node scripts/check-release-config.mjs",
"start": "pnpm --filter @sentry/junior-example dev",
"test": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test && pnpm --filter @sentry/junior-dashboard test",
"test:e2e:dashboard": "pnpm --filter @sentry/junior-dashboard build && playwright test -c packages/junior-dashboard/playwright.config.ts",
"test:watch": "pnpm --filter @sentry/junior test:watch",
"evals": "pnpm --filter @sentry/junior-evals evals",
"evals:record": "pnpm --filter @sentry/junior-evals evals:record",
"typecheck": "pnpm --filter @sentry/junior-plugin-api typecheck && pnpm --filter @sentry/junior-scheduler typecheck && pnpm --filter @sentry/junior typecheck && pnpm --filter @sentry/junior-dashboard typecheck && pnpm --filter @sentry/junior-testing typecheck && pnpm --filter @sentry/junior-example typecheck",
"typecheck": "pnpm --filter @sentry/junior-plugin-api typecheck && pnpm --filter @sentry/junior-scheduler typecheck && pnpm --filter @sentry/junior-memory typecheck && pnpm --filter @sentry/junior typecheck && pnpm --filter @sentry/junior-dashboard typecheck && pnpm --filter @sentry/junior-testing typecheck && pnpm --filter @sentry/junior-example typecheck",
"skills:check": "pnpm --filter @sentry/junior skills:check",
"test:ci": "pnpm --filter @sentry/junior build && pnpm --filter @sentry/junior-dashboard build && pnpm --filter @sentry/junior test:coverage && pnpm --filter @sentry/junior-dashboard test:coverage"
},
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/content/docs/contribute/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Junior uses lockstep package releases for:
- `@sentry/junior-hex`
- `@sentry/junior-linear`
- `@sentry/junior-maintenance`
- `@sentry/junior-memory`
- `@sentry/junior-notion`
- `@sentry/junior-scheduler`
- `@sentry/junior-sentry`
Expand Down
7 changes: 7 additions & 0 deletions packages/junior-memory/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
dialect: "postgresql",
out: "./migrations",
schema: "./src/db/schema.ts",
});
35 changes: 35 additions & 0 deletions packages/junior-memory/migrations/0001_memory.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS junior_memory_memories (
id TEXT PRIMARY KEY,
scope TEXT NOT NULL,
scope_key TEXT NOT NULL,
type TEXT NOT NULL,
sensitivity TEXT NOT NULL,
content TEXT NOT NULL,
content_hash TEXT NOT NULL,
source_platform TEXT NOT NULL,
source_key TEXT NOT NULL,
idempotency_key TEXT,
observed_at_ms BIGINT NOT NULL,
created_at_ms BIGINT NOT NULL,
expires_at_ms BIGINT,
superseded_at_ms BIGINT,
superseded_by_id TEXT,
archived_at_ms BIGINT,
archive_reason TEXT
);

CREATE INDEX IF NOT EXISTS junior_memory_memories_visible_idx
ON junior_memory_memories (scope, scope_key, created_at_ms DESC, id)
WHERE archived_at_ms IS NULL AND superseded_at_ms IS NULL AND superseded_by_id IS NULL;

CREATE INDEX IF NOT EXISTS junior_memory_memories_expiration_idx
ON junior_memory_memories (expires_at_ms)
WHERE archived_at_ms IS NULL AND expires_at_ms IS NOT NULL;

CREATE UNIQUE INDEX IF NOT EXISTS junior_memory_memories_active_hash_idx
ON junior_memory_memories (scope, scope_key, content_hash)
WHERE archived_at_ms IS NULL AND superseded_at_ms IS NULL AND superseded_by_id IS NULL;

CREATE UNIQUE INDEX IF NOT EXISTS junior_memory_memories_idempotency_idx
ON junior_memory_memories (scope, scope_key, idempotency_key)
WHERE idempotency_key IS NOT NULL;
42 changes: 42 additions & 0 deletions packages/junior-memory/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@sentry/junior-memory",
"version": "0.75.0",
"private": false,
"publishConfig": {
"access": "public"
},
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/getsentry/junior.git",
"directory": "packages/junior-memory"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"migrations",
"src"
],
"scripts": {
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
"db:generate": "pnpm dlx drizzle-kit@0.31.10 generate --config drizzle.config.ts",
"prepare": "pnpm run build",
"prepack": "pnpm run build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@sentry/junior-plugin-api": "workspace:*",
"drizzle-orm": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/node": "^25.9.1",
"tsup": "^8.5.1",
"typescript": "^6.0.3"
}
}
45 changes: 45 additions & 0 deletions packages/junior-memory/src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { sql } from "drizzle-orm";
import { bigint, index, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";

export const juniorMemoryMemories = pgTable(
"junior_memory_memories",
{
id: text("id").primaryKey(),
scope: text("scope").notNull(),
scopeKey: text("scope_key").notNull(),
type: text("type").notNull(),
sensitivity: text("sensitivity").notNull(),
content: text("content").notNull(),
contentHash: text("content_hash").notNull(),
sourcePlatform: text("source_platform").notNull(),
sourceKey: text("source_key").notNull(),
idempotencyKey: text("idempotency_key"),
observedAtMs: bigint("observed_at_ms", { mode: "number" }).notNull(),
createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
expiresAtMs: bigint("expires_at_ms", { mode: "number" }),
supersededAtMs: bigint("superseded_at_ms", { mode: "number" }),
supersededById: text("superseded_by_id"),
archivedAtMs: bigint("archived_at_ms", { mode: "number" }),
archiveReason: text("archive_reason"),
},
(table) => [
index("junior_memory_memories_visible_idx")
.on(table.scope, table.scopeKey, table.createdAtMs.desc(), table.id)
.where(
sql`${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`,
),
index("junior_memory_memories_expiration_idx")
.on(table.expiresAtMs)
.where(
sql`${table.archivedAtMs} IS NULL AND ${table.expiresAtMs} IS NOT NULL`,
),
uniqueIndex("junior_memory_memories_active_hash_idx")
.on(table.scope, table.scopeKey, table.contentHash)
.where(
sql`${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`,
),
uniqueIndex("junior_memory_memories_idempotency_idx")
.on(table.scope, table.scopeKey, table.idempotencyKey)
.where(sql`${table.idempotencyKey} IS NOT NULL`),
],
);
17 changes: 17 additions & 0 deletions packages/junior-memory/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export { createMemoryPlugin, memoryPlugin } from "./plugin";
export { createMemoryStore } from "./store";
export type {
ArchiveMemoryInput,
CreateMemoryInput,
CreateMemoryResult,
ListMemoriesInput,
MemoryStore,
SearchMemoriesInput,
} from "./store";
export type {
MemoryRecord,
MemoryRuntimeContext,
MemoryScope,
MemorySensitivity,
MemoryType,
} from "./types";
16 changes: 16 additions & 0 deletions packages/junior-memory/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineJuniorPlugin } from "@sentry/junior-plugin-api";

/** Create Junior's trusted long-term memory plugin registration. */
export function createMemoryPlugin() {
return defineJuniorPlugin({
database: {},
manifest: {
name: "memory",
displayName: "Memory",
description: "Long-term Junior memory storage and recall",
},
packageName: "@sentry/junior-memory",
});
}

export const memoryPlugin = createMemoryPlugin();
36 changes: 36 additions & 0 deletions packages/junior-memory/src/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { MemoryScope, MemorySensitivity } from "./types";

const SECRET_PATTERNS = [
/\b(?:api[_-]?key|secret|token|password|passwd|private[_-]?key)\b/i,
/\b(?:xox[baprs]-|gh[pousr]_|sk-[A-Za-z0-9_-]{12,})/,
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
];

/** Return whether content matches the plugin's deterministic secret rejection. */
export function containsMemorySecret(content: string): boolean {
return SECRET_PATTERNS.some((pattern) => pattern.test(content));
}

/** Validate deterministic write policy before memory content reaches storage. */
export function validateMemoryWritePolicy(args: {
content: string;
scope: MemoryScope;
sensitivity: MemorySensitivity;
}): { ok: true } | { ok: false; reason: string } {
if (!args.content.trim()) {
return { ok: false, reason: "Memory content is required." };
}
if (containsMemorySecret(args.content)) {
return {
ok: false,
reason: "Memory content appears to contain a secret.",
};
}
if (args.scope === "conversation" && args.sensitivity === "sensitive") {
return {
ok: false,
reason: "Sensitive memories can only be stored personally.",
};
}
return { ok: true };
}
66 changes: 66 additions & 0 deletions packages/junior-memory/src/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { MemoryRuntimeContext, MemoryScope } from "./types";

export interface ResolvedMemoryScope {
scope: MemoryScope;
scopeKey: string;
}

function sourceConversationKey(ctx: MemoryRuntimeContext): string | undefined {
if (ctx.source.platform === "local") {
return ctx.source.conversationId;
}
const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
if (!threadKey) {
return undefined;
}
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
Comment thread
cursor[bot] marked this conversation as resolved.
}

function requesterScopeKey(ctx: MemoryRuntimeContext): string | undefined {
const requester = ctx.requester;
if (!requester?.userId) {
return undefined;
}
if (requester.platform === "slack") {
return `slack:${requester.teamId}:${requester.userId}`;
}
return `local:${requester.userId}`;
}

/** Derive the authority-bearing key for a requested memory scope. */
export function deriveMemoryScope(
ctx: MemoryRuntimeContext,
scope: MemoryScope,
): ResolvedMemoryScope {
if (scope === "personal") {
const scopeKey = requesterScopeKey(ctx);
if (!scopeKey) {
throw new Error("Personal memory requires requester context.");
}
return { scope, scopeKey };
}

const scopeKey = sourceConversationKey(ctx);
if (!scopeKey) {
throw new Error("Conversation memory requires conversation context.");
}
return { scope, scopeKey };
}

/** Return every visible scope for memory retrieval in the current context. */
export function deriveVisibleMemoryScopes(
ctx: MemoryRuntimeContext,
): ResolvedMemoryScope[] {
const scopes: ResolvedMemoryScope[] = [];
try {
scopes.push(deriveMemoryScope(ctx, "personal"));
} catch {
// Personal memory is optional when a runtime surface has no requester.
}
try {
scopes.push(deriveMemoryScope(ctx, "conversation"));
} catch {
// Conversation memory is optional for synthetic invocations.
}
return scopes;
}
Loading
Loading