Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ dist/
*.db-journal
cdk.out/
.env
.env.discord
.codegraph/
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@ in S3. No database server, no VPC.
```bash
npm install
npm test
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..." npm run local-fetch

# Put your webhook URL in an untracked .env (see docs/06-discord-webhook-setup.md),
# then source it and run the writer — keeping the URL out of shell history.
set -a; . ./.env; set +a # .env is gitignored
npm run local-fetch
```

That runs the writer against a local SQLite file with no AWS involved (Phase 1). To
deploy the real thing:
get a Discord webhook URL, see
[docs/06-discord-webhook-setup.md](docs/06-discord-webhook-setup.md). To deploy the
real thing:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
equationalapplications marked this conversation as resolved.

```bash
export AWS_PROFILE=your-profile
# Source the webhook URL from an untracked file rather than echoing it inline —
# `infra/stack.ts` reads DISCORD_WEBHOOK_URL at synth time and throws if it is unset.
set -a; . ./.env.discord; set +a # .env.discord is gitignored
npm run deploy
npm run smoke
```
Expand Down
15 changes: 7 additions & 8 deletions docs/01-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ durable, versioned, and — critically for this pattern — supports conditional

## Why one Lambda, not two

`aws-cloud-agent`, the sibling project this tutorial is drawn from, uses two Lambdas — a
writer and a reader — because its reader also runs semantic search backed by a vector
index that needs its own warm-container lifecycle tuning. This tutorial's reader is a
much smaller job: query two tables and return JSON. Splitting it into a second Lambda
would mean a second container image, a second set of IAM grants, and a second cold-start
budget — for a query that returns in single-digit milliseconds once hydrated. One function
with an `op` field is simpler and the tutorial's job is to teach the storage pattern, not
Lambda topology.
A more ambitious agent might split the reader into its own Lambda — say, when the reader
also runs semantic search backed by a vector index that needs its own warm-container
lifecycle tuning. This tutorial's reader is a much smaller job: query two tables and
return JSON. Splitting it into a second Lambda would mean a second container image, a
second set of IAM grants, and a second cold-start budget — for a query that returns in
single-digit milliseconds once hydrated. One function with an `op` field is simpler and
the tutorial's job is to teach the storage pattern, not Lambda topology.

## The single-writer invariant

Expand Down
2 changes: 2 additions & 0 deletions docs/02-rehydration.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ Marketplace subscription must already exist on the account, or `bedrock:InvokeMo
returns `AccessDeniedException` regardless of what the IAM policy says. `cdk deploy` does
not check for the subscription, so the stack deploys cleanly and the first `fetch` fails —
which is why this tutorial calls it out before the first deploy rather than after.

For Discord webhook setup, see [docs/06-discord-webhook-setup.md](06-discord-webhook-setup.md).
13 changes: 7 additions & 6 deletions docs/05-from-tutorial-to-prod.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ fixed daily schedule. A production agent is more likely to need multiple write p
scheduled job and a manually-triggered one, say — which raises the question of whether
`reservedConcurrentExecutions: 1` is still sufficient once two *different* Lambda
functions might both want to write. It isn't, on its own: reserved concurrency only
serializes invocations of one function. Giving every
writer path the same conditional-write discipline this tutorial uses, so the S3 `If-Match`
serializes invocations of one function. The fix is to give every writer path the same
conditional-write discipline this tutorial uses, so the S3 `If-Match`
precondition — not Lambda's concurrency control — is what actually prevents two writers
from clobbering each other, regardless of how many entry points call into that logic.

Expand All @@ -28,7 +28,8 @@ from clobbering each other, regardless of how many entry points call into that l
This tutorial's `fetch` and `status` share one function because the reader's query is
cheap. If your reader starts doing real work — search, aggregation, anything with its own
latency and memory profile — split it into its own function. The two functions still share the storage pattern in this
tutorial's `docs/02-rehydration.md`; only the deployment topology changes.
tutorial's `docs/02-rehydration.md`; only the deployment
topology changes.

## Model selection

Expand All @@ -42,6 +43,6 @@ else" reasoning that doc records.

The rehydration protocol — bootstrap, conditional writes, version-cached reads — doesn't
change shape as the system grows. That's the point of the pattern: it's the same mechanism
whether the payload is a two-table dedup cache or full knowledge graph
with a vector index. What changes is how much work happens between hydrate and publish,
not how hydrate and publish themselves work.
whether the payload is a two-table dedup cache or a larger state file with more tables and
indices. What changes is how much work happens between hydrate and publish, not how
hydrate and publish themselves work.
98 changes: 98 additions & 0 deletions docs/06-discord-webhook-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Discord webhook setup

This tutorial posts to a Discord channel via a **webhook** — a per-channel URL that
anyone with the URL can use to post messages into that channel. Each webhook is scoped
to one channel; the URL is the secret.

## Step 1: Create a Discord channel for the bot

If you don't already have a channel you'd like the bot to post to, create one in your
Discord server. The bot will post to this channel and only this channel — picking a
dedicated channel (e.g. `#weather-bot`) keeps its posts separate from general
discussion.

## Step 2: Open the channel's integrations settings

1. Open the Discord client (desktop or web) and navigate to the channel.
2. Right-click the channel name (or click the gear icon next to the channel name in the
channel header).
3. Select **Edit Channel**.
4. In the left sidebar, click **Integrations**.

## Step 3: Create a webhook

1. Under **Webhooks**, click **New Webhook**.
2. Give the webhook a name (e.g. `Weather Bot`). The name appears as the "username" on
posts the bot makes.
3. Optionally, set an avatar by uploading an image.
4. Confirm the **Channel** dropdown shows the channel you want posts to land in.
5. Click **Copy Webhook URL**. The URL has the form
`https://discord.com/api/webhooks/<id>/<token>` — treat the entire URL as a secret.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Anyone with the URL can post to the channel.

> **Permission required.** Creating, editing, or deleting a webhook needs the
> `MANAGE_WEBHOOKS` permission for the target channel. If the **New Webhook** button
> is greyed out or missing, you don't have that permission in the channel — contact a
> server administrator and ask them to either grant it or create the webhook on your
> behalf.

## Step 4: Configure the tutorial

Export the URL as the `DISCORD_WEBHOOK_URL` environment variable before running
locally. To keep the value out of shell history, source it from an untracked file
(`.env` is already in `.gitignore`):

```bash
# Local development — load from an untracked .env, then run:
set -a; . ./.env; set +a
npm run local-fetch
```

Where `.env` contains:

```bash
DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/<id>/<token>'
```

When deploying via CDK, do the same — `infra/stack.ts` reads `DISCORD_WEBHOOK_URL`
at synth time (lines 55-63) and embeds it as a Lambda environment variable, so the
value should never appear on a command line that gets logged or shared:

```bash
# CI / local deploy — source from a secret store or masked CI variable, then deploy:
set -a; . ./.env.discord; set +a # .env.discord is gitignored
npm run deploy
Comment thread
equationalapplications marked this conversation as resolved.
```

For production deployments, prefer **SSM Parameter Store** or **Secrets Manager**
over an inline Lambda environment value — `cdk.out/` and CloudFormation templates
echo environment values, and any operator with `lambda:GetFunctionConfiguration`
(or equivalent read access to the function's configuration) can read the same
value back. CloudWatch log access (`logs:GetLogEvents`) is a separate concern:
Lambda does not log environment variables by default, but any code that prints
or otherwise echoes `DISCORD_WEBHOOK_URL` will surface it in the log stream.
Never commit `.env`, `cdk.out/`, or logs that contain the webhook URL.

The URL is the only credential the Lambda needs — its IAM role does not require any
Discord permissions.

## Step 5: Verify

Run `npm run local-fetch` once. Within a few seconds you should see a post in the
Discord channel. If you don't see one, check the CloudWatch logs (when deployed) or
the script's stdout (when running locally) — the `agent_runs.error` column captures
per-source failures including Discord post failures.

## Rotating the webhook

If the webhook URL is compromised (e.g. accidentally logged, pasted into a public
forum), the recovery is to delete the compromised webhook in the same **Integrations**
panel and create a new one. Update `DISCORD_WEBHOOK_URL` and redeploy.

Webhook executions remain subject to Discord's normal rate limits and can return
HTTP 429. The current poster treats 429 the same as any other non-2xx after its
single fixed 250 ms 5xx retry — it throws `DiscordPostError` and the run records
a per-source failure in `agent_runs.error`. Discord does not publish the exact
limits and they vary by channel and account; if bounded 429 handling is needed,
honour the `Retry-After` response header (and the `X-RateLimit-*` family) rather
than retrying on a fixed cadence.
39 changes: 20 additions & 19 deletions docs/bedrock-model-comparison.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
# Bedrock model comparison (us-east-1)

> **Provenance.** This file is research from a sibling project (`aws-cloud-agent`,
> `@equationalapplications/core-llm-wiki`) where `low`/`med`/`high` tier switching and a
> `TIER_DEFAULTS` constant live in `src/config.ts`. It is kept in this PR as background
> reading for PR2/PR3's `BedrockFormatter` work — **not** because this tutorial defines
> those tiers. The tutorial's actual Bedrock configuration surface is the single
> `bedrockModelId` field documented in `docs/superpowers/specs/2026-08-08-sqlite-s3-agent-tutorial-design.md`
> §11 (default `zai.glm-4.7-flash`).
>
> References to `src/judge/assess.ts`, `infra/stack.ts`, `MAX_TOKENS_MED`, `doRunHeal`,
> `maintain`, `g3UntypedFacts`, and the `src/bedrock/families.ts` family registry all
> belong to the sibling project and do not exist in this repo.

Reference for picking/repointing tier models in the sibling project. Update this table
when tiers change or when re-probing. For this tutorial, start with the recommended
`med`/`low` pick below (`zai.glm-4.7-flash`) and revisit only if Bedrock integration
(`PR2`) needs a different model.
> **Provenance.** This file is general-purpose Bedrock model research. Model pricing,
> capability, and latency characteristics are not project-specific, so the table below
> is a starting point for any reader picking a Bedrock model. The tier recommendations
> at the bottom of the file are tutorial-specific — they cover the `low`/`med`/`high`
> tiers the tutorial uses. Adding a new tier or pointing an existing tier at a model
> from a different family requires an entry in `src/format/families.ts` (verified by a
> live probe with a negative control, not by reading model cards) and a matching
> resource ARN in `infra/stack.ts`.

Reference for picking a Bedrock model in any project. Update this table
when pricing changes or when re-probing. For this tutorial, start with the recommended
`med`/`low` pick below (`zai.glm-4.7-flash`) and revisit only if the deployed model's
behaviour regresses.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

**Methodology:** prices are pulled from the AWS Pricing API
(`aws pricing list-price-lists` / `get-price-list-file-url`, `AmazonBedrock` service code,
Expand Down Expand Up @@ -107,8 +104,12 @@ rather than ranked by headline price.
| C | Genuine supersession, detector correctly picked the older/lower-confidence side | `uphold` |

Case C is the control: without it, a model biased toward `overturn` scores well on B by luck.
Plus an ingest test using the library's real `INGEST_SYSTEM_PROMPT`
(`@equationalapplications/core-llm-wiki`) on a document chunk.
Plus an ingest test against a document chunk using a typical ingest prompt —
treated as a **synthetic proxy** rather than a measurement of this tutorial's
real request. The tutorial's `INGEST_SYSTEM_PROMPT` itself is too tightly bound to
the writer's schema to reuse as a generic benchmark, so the proxy is used only to
sanity-check output-token counts; the application-specific ingest cost row is
explicitly **excluded** from the recommendations below.

**Every candidate was run at least 3 times.** This mattered — see Nemotron Super below.

Expand Down Expand Up @@ -182,7 +183,7 @@ rather than a single figure.
- GLM 4.7 Flash is a small MoE model. The `doRunHeal` prompt (a full fact dump) is materially
harder than anything tested here. If heal quality regresses, GLM 4.7 (non-Flash, $0.60/$2.20)
is the natural fallback — same family, same request shape, 3/3 on these cases.
- Adopting these requires new `zai` and `deepseek` family entries in `src/bedrock/families.ts`
- Adopting these requires new `zai` and `deepseek` family entries in `src/format/families.ts`
and matching resource ARNs in `infra/stack.ts`, or invocation fails with AccessDenied.

## Anthropic models (kept for future reference — not currently in use)
Expand Down
Loading