diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..5603490 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "past", + "interface": { + "displayName": "past.dev" + }, + "plugins": [ + { + "name": "past", + "source": { + "source": "local", + "path": "./plugins/past" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..47e2b1e --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,42 @@ +name: check + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + script: + name: script on Node ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + matrix: + node: [14, 16, 22] + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + # Node 16 is the floor. On an older Node the hooks only have to stay silent, so 14 parses it. + - name: Parses + run: node --check plugins/past/bin/past-hook.mjs + - name: Runs with nothing configured + if: matrix.node != 14 + run: node plugins/past/bin/past-hook.mjs status --host codex + env: + HOME: ${{ runner.temp }}/home + + manifests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + # Codex has no validator. A manifest that does not parse loads no plugin, and says nothing. + - name: The manifests parse + run: | + for file in .agents/plugins/marketplace.json plugins/past/.codex-plugin/plugin.json plugins/past/hooks/hooks.json; do + node -e 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))' "$file" || exit 1 + echo "$file" + done diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c5452b0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,221 @@ +# past.dev for Codex — working on the plugin + +The past.dev plugin for Codex, and the marketplace that carries it. It runs on the developer's machine +and talks to past.dev's public Memory API. Nothing here is privileged: anyone can write the same +connector against the same endpoints with the same key. What a user reads is in `README.md`, and +in full in `plugins/past/README.md`; this file is the rules the code keeps, for whoever changes it. + +## How it fits together + +``` +Codex + hooks/hooks.json five hooks every one passes --host codex + skills/* six skills every command passes --host codex + | + bin/past-hook.mjs --host codex + | + session start brief recall into context; sends idle sessions left behind + every prompt prompt recall into context + every turn stop stamps the activity file, keeps one waiter on the session + compaction precompact sends the session so far + session end end makes sure a waiter runs + detached idle the waiter: sends once Codex's process is gone, or after idleMinutes + | + ~/.past/config.json the connection + ~/.past/state.json what was sent and what is open, under hosts.codex + ~/.past/activity/codex/ one file per open session + ~/.past/hosts/codex.json when the hooks last ran, for status + | + POST /api/v1/recall · POST /api/v1/ingest/batch · GET /api/v1/audiences/{slug} +``` + +``` +.agents/plugins/marketplace.json the catalog — this repository is the marketplace +plugins/past/ + .codex-plugin/plugin.json the manifest: names the hooks file and the skills folder + hooks/hooks.json SessionStart, UserPromptSubmit, Stop, PreCompact, SessionEnd + bin/past-hook.mjs the whole connector, one file, no dependencies + skills/ search, status, connect, cut, ingest, backfill + LICENSE the same license: an install copies this folder and nothing else +.github/workflows/check.yml every pull request: the script parses and runs, the manifests parse +``` + +## The script + +`bin/past-hook.mjs` is the whole plugin. The rules it keeps: + +1. **A hook never breaks the session.** Every mode exits 0, prints valid JSON or nothing, and + swallows its own failures. `$past:status` is where a person learns something is wrong, so a + failed send is kept in `state.json` (`lastError`) for it. +2. **Only prose travels.** On disk a session is mostly tool output. Tool calls and their output, + file contents, diffs, subagent threads and injected turns are cut: sending the transcript would + cost far more and recall worse, with the conversation buried in it. +3. **The hash gates the send.** `state.json` holds, per session, the hash of the whole session + rendered in one piece (`hash`) and one hash per sitting (`sittings`). A session whose whole hash + has not changed is not sent; one that changed sends only the sittings whose hash differs. The + server charges nothing for an unchanged re-send, but the call still spends a request from the + key's per-minute budget. +4. **A session is sent when it is over, goes idle or compacts; never from `Stop`.** `Stop` fires + every turn: it only stamps `~/.past/activity/codex/.json` and keeps one detached waiter + (`idle` mode) per session. The waiter sleeps in one-minute slices measured on the wall clock, + since a timer does not count the time the machine is asleep, and sends once Codex's process is + gone or the last turn is `idleMinutes` old (720 by default). `ensureWaiters` (`Stop`, + `SessionStart`, `$past:status`) replaces a waiter that died or that an older `WAITER_VERSION` + started (bump it whenever the waiter changes), and a replacement past its deadline sends at + once. `PreCompact` sends before the context is summarised. `SessionStart` sends the idle + sessions that never reached past.dev, and `tidyPending` drops an open session that is gone, empty, + or in past.dev unchanged and idle. Why `SessionEnd` sends nothing is under "Codex" below. +5. **A recall query is redacted like the content it would send.** It crosses the network exactly + as ingested content does, and both go through `redact()`. +6. **Backfill estimates before it sends, and never sends the session it runs in.** `backfill` + lists; `backfill --confirm` sends them all, leaving out the current thread: the hooks send it, + and a second send would pay for it twice. One session is `ingest`, which sends at once (running + it is the consent). `cut` defaults to the current thread, then to the most recently written + transcript. +7. **`state.json` changes only through `updateState`.** Hooks and waiters are separate processes + running at once. `updateState` takes a lock (`~/.past/state.lock`, a directory, taken over after + 5 s, skipped after 3 s so a hook never hangs), re-reads the file, applies the change and renames + the new file into place. Never save a copy read earlier: two waiters that sent together would + erase each other's "sent". A file that does not parse is read again a few times and, if it still + does not, left as it is: written over, it would lose every session, and the next backfill would + send them all again. +8. **No dependencies.** Node 16 or later, standard library only, so installing is a clone. `fetch` + is used where Node has it and the `http` modules where it does not: Codex runs hooks in a login + shell, whose `node` is often an old one. On Node 16 a stalled `http` call is destroyed when its + time is up, so it never holds a prompt or a waiter's send. Nothing in the script is newer than + Node 14 can parse (no `||=`), so on an older Node the hooks stay silent rather than fail. +9. **What is sent for a session does not change by a byte.** The heading and the speaker names + that `renderTranscript` prints are part of the content hash, and so are `SOURCE`, `AGENT` and + `SPEAKER` at the top of the script: a changed byte makes `backfill` send every session already + in past.dev again, as a paid revision. `sittingId` and the cut (`sittingsOf`, `silenceMeter`) are + fixed too: changed, they move the boundaries of every session that grows afterwards, and the old + sittings stay in past.dev beside the new ones. +10. **`~/.past` is the person's alone.** The folder is `700` and every file in it is created + `600`, never written open and narrowed afterwards. A plain-http `apiUrl` on another machine + sends the key unencrypted: `connect` warns, and `status` says so on its first line. +11. **A session is sent in sittings.** past.dev dates every memory at the time of its data point, and + decides which of two statements is the later from those times. A sitting starts at the first + turn after the thread was silent for `sittingMinutes` (30 by default, 5 at least), whoever + speaks; tool calls and their output count as activity, so a prompt stays with its reply while + Codex works. Each sitting is one data point timed at its first turn, and every changed sitting + goes in one `/api/v1/ingest/batch` call. `state.json` pins `sittingMinutes` per session at its + first send, so a changed value never moves the boundaries of a session already in past.dev. + +12. **The product is written past.dev.** Every sentence a person reads says past.dev: the READMEs, + the manifest's display names, the skills' text, what a mode prints. Identifiers keep `past`: + the plugin and marketplace names (`past@past`, which every hook's trust is keyed on), the skills + (`$past:status`), `~/.past`, the key prefix `past_sk_` and the data point ids. So does the recall + block's header, `=== past · recalled from memory ===`: the script drops a block by that exact + text from the sessions it sends, so a changed header would change how old sessions render and + send them all again (rule 9). The hooks file keeps its status message, "Recalling from past", for + the reason it is written once (see "The hooks file is written once"). + +## Beside other past.dev plugins + +`~/.past` can be shared with another past.dev plugin on the same machine. `config.json` is then one +connection for both. This plugin keeps its sessions under `hosts.codex` in `state.json`, its +activity files in `activity/codex/` and its hook stamp in `hosts/codex.json`. The rest of +`state.json` is never read here, and every write puts it back as it was (rule 7). + +## Codex + +- **The hooks file is written once.** Codex runs no hook that a person has not trusted, and keeps + the trust against a hash of each hook's definition, under a key made of the plugin's id, the + hooks file's path in the package, the event and the hook's position + (`past@past:hooks/hooks.json:stop:0:0`). A new version of the plugin and a changed script keep + the hash. An edited definition changes it, and that one hook stops without a word until the + person reviews it again. A renamed plugin or marketplace, a moved hooks file and a reordered + entry change the key. So `hooks/hooks.json` holds the five events and a mode each, its path and + the name `past@past` stay, and every change of behaviour goes into the script. Every hook in it + passes `--host codex`, so the flag stays too: the script takes it out before reading its + arguments, and the skills pass it as well, so a command reads the same wherever it comes from. +- **`SessionEnd` is a hint.** Codex also fires it when a thread has sat idle for half an hour. A + send there would be a new revision at every pause, so `end` only makes sure a waiter runs, and + the waiter decides: the owner is gone, or `idleMinutes` have passed. A session with no activity + file had no turn since its last send, and gets no waiter. +- **Only Codex counts as the owner.** Any other process above a hook may last one turn, and a + waiter that trusted it would send the session after every turn. A `codex exec` run ends with its + session. The desktop app holds one Codex process for every thread, for as long as it is open, so + there the owner goes when the app quits. +- **Codex reads only what its manifest names.** `hooks` and `skills` in + `.codex-plugin/plugin.json` name the hooks file and the skills folder. A `plugin.json` at the + package root would take precedence, and Codex would then load no hook. +- **A skill's command runs in the skill's folder.** So each skill calls the script by a relative + path, `node ../../bin/past-hook.mjs --host codex`, and the project and the branch of a + session come from its transcript (`ownProject`), since the directory says nothing. Codex + substitutes nothing in a skill file. +- **The model's commands are sandboxed; hooks are not.** No network unless the person allowed it, + no writing outside the workspace. A skill whose command reaches past.dev or writes `~/.past` tells + Codex to ask for the permission. A mode that finds itself inside the sandbox + (`CODEX_SANDBOX_NETWORK_DISABLED`) prints `NEEDS_OUTSIDE`, which tells the model to run it again + outside: a recall that could not leave must never read as "nothing matches". `status` starts no + waiter from inside the sandbox, since that waiter could never reach past.dev. +- **The three skills that take a key or send data run only when named** (`connect`, `ingest`, + `backfill`: `allow_implicit_invocation: false` in their `agents/openai.yaml`). +- **The current session** is the thread Codex names in the environment of every command it runs + (`CODEX_THREAD_ID`). `cut` and `ingest` act on it when no session is named; `backfill` leaves it + out. +- **The reader** keeps `response_item` messages of the user and the assistant. It leaves out the + developer's role (Codex's instructions, what hooks injected, past.dev's own recall block), the blocks + Codex writes under the user's role, the prompts that only run one of the plugin's skills, and + every line that is machinery. A recent Codex says what each user message is made of, and only + typed text (`user.text`) counts. A transcript that does not say (an older Codex, a legacy + thread) is filtered by `CODEX_SYNTHETIC`: the repository's AGENTS.md, a skill's whole file and a + shell command a person ran, with its output, all arrive under the user's role. A compaction + appends one line and rewrites nothing. A session is found by its thread id, in `sessions/` and + in `archived_sessions/`, since archiving moves the file. +- **A subagent's thread is never read.** Its header names a subagent as its source, its prompts + are the parent's instructions, and it is neither listed nor sent, from a hook or a command. A + source that names a custom client is a conversation like any other. +- **A copied conversation is never sent.** Two kinds of thread open with somebody else's session, + stamped with the one instant the copy was made. Codex can import another agent's sessions as + threads of its own and lists those in `external_agent_session_imports.json`. A fork of a legacy + thread repeats the parent's header and turns (a fork of a recent thread points at its parent and + copies nothing). The reader drops what was written within `COPY_WRITE_MS` of that instant, and + what a person says in the thread afterwards is this session. Only the thread's first header + names its project and branch. + +## Contracts this depends on + +- `POST /api/v1/ingest/batch` — one item per sitting. `id` gives replacement (`codex:` + for the first sitting, `codex::` for the others), `timestamp` dates every memory + drawn from the item, `label` names the session, `metadata` carries `conversationId`, + `sessionId`, `project`, `gitBranch`, `sitting`. +- `POST /api/v1/recall` — **`identity` is required**. Recall stays off until `$past:connect` sets + one. +- `audience` on ingest — a slug; absent means the whole project. `connect --audience` checks it + with `GET /api/v1/audiences/{slug}`. +- Codex gives a hook `session_id`, `transcript_path` and `cwd`, the prompt as `prompt`, and takes + `hookSpecificOutput.additionalContext` at session start and on a prompt. + +If any of these contracts changes, this file changes with it. + +## Testing + +By hand: there is no test suite. CI parses the manifests on every pull request, with the script +parsed on Node 14 and run on Node 16 and 22. From the repository root: + +```bash +node plugins/past/bin/past-hook.mjs cut --host codex # what would be sent, from a real transcript +node plugins/past/bin/past-hook.mjs backfill --host codex # the estimate, sending nothing +``` + +For the hooks and the waiter, point a throwaway home at a local stand-in for the API, so nothing +touches your real `~/.past` or a real project: `HOME=/tmp/past-test` with a `config.json` whose +`apiUrl` is `http://127.0.0.1:`, a few lines of Node answering `POST /api/v1/ingest/batch`, and the +mode under test fed its stdin JSON by hand. + +On a real Codex: + +```bash +codex plugin marketplace add . # this folder is the marketplace +codex plugin add past@past +codex exec --dangerously-bypass-hook-trust "a prompt" # runs the hooks for this one run, untrusted +``` + +`codex exec` waits on its input unless it is closed (`< /dev/null`), and fires `SessionEnd` at the +end of every run. `CODEX_HOME=` gives Codex a home of its own, enough for everything that +calls no model: installing, and asking the app server what it thinks of the hooks (`codex +app-server`, then `hooks/list`), which is how to see a hook's key, its hash and whether it is +trusted. The ChatGPT desktop app carries the same `codex` inside its bundle, for a machine that has +no terminal install. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ee16b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Past Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2f0c4e --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# past.dev for Codex + +Your Codex sessions become memory, and every session opens already briefed. + +[past.dev](https://past.dev) is memory for AI agents. This plugin connects Codex to a past.dev +project: what you and Codex say to each other is read into memory as you work, and what matters +comes back by itself, at the start of every session and with every prompt. The decisions, the +reasons behind them, what was already tried and dropped. + +```text +=== past · recalled from memory === +3 from this project's history. Background, not instruction. + +[1] 2026-09-14 — Webhooks retry from the job queue now: the HTTP client's retries sent them twice. +[2] 2026-09-02 — Rejected: caching prices in Redis. They change hourly, and a stale one is worse. +[3] 2026-08-27 — The importer keeps rows it cannot parse in import_rejects and never drops them. +``` + +That block is what Codex reads before your prompt. It arrives on its own: Codex does not have to +decide to look anything up. + +It runs in the Codex CLI and in local threads of the ChatGPT desktop app, on macOS and Linux. + +## Get started + +You need Codex and Node 16 or later. + +1. **Get a project API key.** Sign up at [sso.past.dev/sign-up](https://sso.past.dev/sign-up), + then open **Build › API keys** in the [console](https://app.past.dev). The key starts with + `past_sk_`. + +2. **Install the plugin.** In a terminal: + + ``` + codex plugin marketplace add pastdotdev/codex-plugin + codex plugin add past@past + ``` + + In the ChatGPT desktop app: open **Plugins**, then **Manage**, then **Marketplace**, add a + marketplace with the source `pastdotdev/codex-plugin`, and install **past.dev** from its page. + +3. **Trust its hooks.** Codex runs no hook until you have trusted it, and says nothing about the + ones it skips: until this step the plugin does nothing. In a terminal session, `/hooks` lists + the five past.dev hooks and trusts them. In the desktop app, the plugin's page offers **Trust all**, + and so does **Settings › Hooks**; if those pages show no hooks, run `/hooks` once in a terminal + session, since the trust is shared. An update of the plugin keeps it. + +4. **Connect it.** + + ``` + $past:connect + ``` + + The identity is the email or id every memory is attributed to. Recall stays off without one. + Codex asks before it runs this outside its sandbox; approve it. + +5. **Check it.** `$past:status` shows the project it talks to, when its hooks last ran and what it + has sent. + +New sessions are read from then on. `$past:backfill` lists the Codex sessions already on this +machine, estimates what sending them costs, and sends them when you confirm. + +To keep the key out of the conversation, set `PAST_API_KEY` and `PAST_IDENTITY` in your +environment instead of step 4. The desktop app reads them from a login shell, so put them in +`~/.zprofile`. + +## How it works + +```text +session opens recall what past.dev knows about this project ──► into context +every prompt recall what past.dev knows about this prompt ──► into context +every reply nothing is sent; the session is marked active +compaction the session so far is sent, before its detail is summarised away +session over the session is sent: its Codex process exited, or it was quiet for 12 hours +``` + +Five hooks do this, and one script runs them all. A recall that has not answered within 4 seconds +(8 at session start) is dropped, so it never holds a prompt up, and a prompt shorter than 12 +characters ("yes", "go on") recalls nothing. + +**When a session is over.** Codex says a session ended even when it only went quiet for half an +hour, so the plugin does not send on that. A session is sent within a minute after the Codex +process that ran it exits (the end of a `codex exec` run, the desktop app when you quit it), or +after 12 hours without a new turn. The next session to start also sends any earlier one that never +reached past.dev, which covers a crash or a machine that slept. + +**A session is sent in sittings.** Where a thread went quiet for 30 minutes, the next turn starts a +new sitting. Each sitting is dated at its own first turn, so a decision made on the third day of a +long thread is remembered on that day, and past.dev can tell which of two statements came later. A +session that continues sends its last sitting and the new ones again, nothing earlier. + +**Recall happens in the hooks.** A tool recalls only when the model decides to call one; a hook +recalls on every prompt. For questions about history, `$past:search` lets Codex also search past.dev on +demand, and Codex may pick it by itself when you ask what was decided or whether something was +tried before. + +## What leaves your machine + +Your prompts, Codex's replies, and the project and branch names. Nothing else. + +Never sent: tool calls and their output, file contents, diffs, command output, subagent threads, +and anything Codex or a hook injected rather than a person typing it. Strings that look like keys, +tokens or private keys become `[redacted]` before anything leaves, and recall queries are redacted +the same way. + +Codex can import your sessions from another agent, and a fork of such a thread repeats it. The +copied part of a thread is never sent: it is that agent's conversation, dated on the day Codex +copied it. What you say in the thread afterwards is sent like any Codex session. + +`$past:cut` prints exactly what would be sent from this session, sitting by sitting, before +anything is. + +On disk a session is mostly tool output, and only the prose travels. A credit is 350 bytes of it; +`$past:cut` and `$past:backfill` say what a send would cost before it happens. What a recall costs +depends on your plan: see [past.dev/pricing](https://past.dev/pricing). + +## Skills + +Codex plugins carry skills rather than commands. Name one with `$`: + +| Skill | What it does | +|---|---| +| `$past:connect [api-url]` | connect this machine; `--audience ` sets who sees what you send | +| `$past:status` | what it is connected to, when its hooks last ran, what it has sent | +| `$past:cut [session]` | print exactly what would be sent from this session, or from one you name | +| `$past:ingest [session]` | send one session now, without waiting for it to be over | +| `$past:backfill` | list and estimate the Codex sessions already on this machine, then send them | +| `$past:search ` | search past.dev on demand | + +Codex runs a command in a sandbox with no network, so the skills that reach past.dev ask for the +permission, and Codex asks you. `connect`, `ingest` and `backfill` run only when you name them. + +## Who sees what you send + +Everyone in the project, unless you choose an audience: `$past:connect --audience `, with a +slug from the console's **Audiences** page. `$past:connect --audience project` widens it back. + +## Settings + +`~/.past/config.json` holds the connection and a few switches: `recall` and `ingest` turn either +half off, `idleMinutes` (720) is how long a session stays quiet before it is sent, +`sittingMinutes` (30) is how long a pause starts a new sitting, and `deny` lists paths whose +sessions are never read. `PAST_API_KEY`, `PAST_IDENTITY` and `PAST_API_URL` override the file. +Each setting is described in the [plugin's README](plugins/past/README.md#configuration). + +For a self-hosted past.dev, pass your deployment's URL as the third argument of `$past:connect`. Use +`https`: over plain `http` to another machine the key travels unencrypted, and the plugin says so. + +## When something looks wrong + +Start with `$past:status`. A hook never interrupts a session, so a problem shows up there rather +than in the conversation. + +- **The hooks never ran.** Status says when they last did. If they never have, they are not + trusted yet (step 3). If they are trusted and still never run, Codex did not find `node`: it runs + hooks in a login shell, which reads `~/.zprofile` but not `~/.zshrc`. +- **Nothing is recalled.** Status has to show a key and an identity: recall needs both. A new + project has nothing to recall until its first sessions are in; `$past:ingest` sends the current + one now. +- **A send failed.** Status shows the last failed send and what the API answered. + +## Remove it + +`codex plugin remove past@past`, then delete `~/.past`. Revoking the key in **Build › API keys** +stops it at once from the server side. What was already sent stays in the project until you +delete it in the console. + +## This repository + +A Codex plugin marketplace. Codex reads it straight from GitHub; there is nothing to build. + +```text +.agents/plugins/marketplace.json the catalog +plugins/past/ + .codex-plugin/plugin.json the manifest: names the hooks file and the skills folder + hooks/hooks.json the five hooks + bin/past-hook.mjs the whole plugin: one file, Node 16, no dependencies + skills/ $past:search, :status, :connect, :cut, :ingest, :backfill +``` + +The plugin talks only to past.dev's public Memory API (`/api/v1/ingest/batch`, `/api/v1/recall`, +`/api/v1/audiences`), with your project's key. Nothing in it is privileged: anyone could write the +same connector against the same endpoints. + +## Contributing + +Issues and pull requests are welcome. [AGENTS.md](AGENTS.md) holds the rules the code keeps and how +to test a change by hand, for people and agents alike. Check a change by installing it from your +clone: + +```bash +codex plugin marketplace add . +codex plugin add past@past +``` + +## Links + +- Memory API documentation: [past.dev/docs/memory-api/overview](https://past.dev/docs/memory-api/overview) +- Console: [app.past.dev](https://app.past.dev) +- Sign up: [sso.past.dev/sign-up](https://sso.past.dev/sign-up) +- past.dev's MCP servers: [pastdotdev/mcp](https://github.com/pastdotdev/mcp) +- Community: [past.dev/slack](https://past.dev/slack) + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/plugins/past/.codex-plugin/plugin.json b/plugins/past/.codex-plugin/plugin.json new file mode 100644 index 0000000..c0ad78f --- /dev/null +++ b/plugins/past/.codex-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "past", + "version": "0.1.0", + "description": "Your Codex sessions become memory, and every session starts briefed.", + "author": { "name": "past.dev", "url": "https://past.dev" }, + "homepage": "https://github.com/pastdotdev/codex-plugin", + "repository": "https://github.com/pastdotdev/codex-plugin", + "license": "MIT", + "keywords": ["memory", "recall", "context", "past.dev"], + "hooks": "./hooks/hooks.json", + "skills": "./skills/", + "interface": { + "displayName": "past.dev", + "shortDescription": "Memory across your coding sessions", + "longDescription": "Your Codex sessions are read into a past.dev project when they are over, and every session and every prompt start with what past.dev recalls about the project: decisions, the reasons behind them, what was already tried. Only the prompts and the replies are sent, never tool output, file contents or diffs.", + "developerName": "past.dev", + "category": "Productivity", + "websiteURL": "https://past.dev", + "privacyPolicyURL": "https://past.dev/privacy", + "termsOfServiceURL": "https://past.dev/terms", + "defaultPrompt": [ + "Use $past:status to show what past.dev is connected to and what it has sent", + "Use $past:search to find what we decided about this, and why" + ] + } +} diff --git a/plugins/past/LICENSE b/plugins/past/LICENSE new file mode 100644 index 0000000..6ee16b8 --- /dev/null +++ b/plugins/past/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Past Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/past/README.md b/plugins/past/README.md new file mode 100644 index 0000000..95058a5 --- /dev/null +++ b/plugins/past/README.md @@ -0,0 +1,159 @@ +# past.dev for Codex + +Your Codex sessions become memory, and every session opens already briefed. + +It runs in the Codex CLI and in local threads of the ChatGPT desktop app, on macOS and Linux. + +## Install + +In a terminal: + +``` +codex plugin marketplace add pastdotdev/codex-plugin +codex plugin add past@past +``` + +In the ChatGPT desktop app, with no terminal: open **Plugins**, then **Manage**, then +**Marketplace**, add a marketplace with the source `pastdotdev/codex-plugin`, and install **past.dev** +from its page. + +## Trust the hooks + +Codex runs no hook until you have trusted it, and says nothing about the ones it skips, so until +this step the plugin does nothing. In a terminal session, `/hooks` lists the five past.dev hooks and +trusts them. In the desktop app, the past.dev plugin's page offers **Trust all**, and so does +**Settings › Hooks**; if those pages show no hooks, run `/hooks` once in a terminal session, since +the trust is shared. An update of the plugin keeps the trust. + +## Connect + +``` +$past:connect [api-url] +``` + +The project API key starts with `past_sk_` and comes from **Build › API keys** in the +[console](https://app.past.dev) (not the project's id, and not the organization's `past_mk_` +management key); the identity is who every memory is attributed to, and the API URL is for +self-host only. Codex asks before it runs this outside its sandbox; approve it. + +To keep the key out of the conversation, set `PAST_API_KEY` and `PAST_IDENTITY` in your +environment instead. The desktop app reads them from a login shell, so put them in `~/.zprofile`. + +The connection lives in `~/.past/config.json`. If this machine is already connected to past.dev, there +is nothing to do: `$past:status` shows it. + +## What it does + +| Hook | When | What | +|---|---|---| +| `SessionStart` | a session opens | recalls this project's history and puts it in context | +| `UserPromptSubmit` | every prompt | recalls against the prompt and puts it in context | +| `Stop` | after each reply | stamps the session's activity; sends nothing itself | +| `PreCompact` | the context is about to be compacted | sends the session so far, before the detail is summarised away | +| `SessionEnd` | Codex unloads a session | makes sure the session will be sent; sends nothing itself | + +**When a session is sent.** Codex says a session ended even when it only went quiet for half an +hour, so the plugin does not send on that. A session is sent within a minute after the Codex +process that ran it exits (the end of a `codex exec` run, the desktop app when you quit it), or +after 12 hours without a new turn (`idleMinutes`). It is sent before a compaction too. + +`SessionStart` also sends any earlier session that never reached past.dev and has gone idle, which is +what covers a crash or a machine that slept. + +**A session is sent in sittings.** A new sitting starts where the session went quiet for 30 +minutes (`sittingMinutes`): nobody wrote and Codex ran nothing. Each sitting is dated at its own +first turn, and what past.dev remembers takes the date of the sitting it came from, so something said +on the third day of a thread that ran for three days is dated that day. A prompt and its reply stay +together while Codex works on it, however long. When a session continues, only its last sitting +and the new ones are sent again. + +**Imported sessions.** Codex can import your sessions from another agent, and a fork of such a +thread repeats it. The plugin never sends the copied part of a thread: it is that agent's +conversation, and Codex dates the copy on the day it was made. What you say in the thread +afterwards is sent like any Codex session. + +## What leaves this machine + +The prompts, the replies, and the project and branch names. That is all. + +Not sent: tool calls and their output, file contents, diffs, command output, subagent threads, and +anything Codex or a hook injected rather than a person typing it. Strings that look like keys, +tokens or private keys are replaced with `[redacted]` before anything is sent. + +Run `$past:cut` to read exactly what would be sent, before you send it. + +## Skills + +Codex plugins carry skills rather than commands. Name one with `$`: + +| Skill | Does | +|---|---| +| `$past:connect` | store the key, identity and audience in `~/.past/config.json`, mode `600` | +| `$past:status` | what it is connected to, when its hooks last ran, what it has sent | +| `$past:cut [session]` | print exactly what would be sent from this session, or from one you name | +| `$past:ingest [session]` | send one session now | +| `$past:backfill` | list and estimate, then send, the Codex sessions already on this machine | +| `$past:search` | search past.dev on demand; Codex may pick it by itself when you ask about history | + +Codex runs a command in a sandbox with no network. The skills that reach past.dev ask for the +permission, and Codex asks you. `connect`, `ingest` and `backfill` run only when you name them. + +Without an audience everyone in the project sees what you send. `$past:connect --audience ` +narrows it to an audience from the console; `--audience project` widens it back. + +## Configuration + +`~/.past/config.json`: + +```json +{ + "apiKey": "past_sk_…", + "identity": "you@example.com", + "apiUrl": "https://api.past.dev", + "audience": "developers", + "recall": true, + "ingest": true, + "idleMinutes": 720, + "sittingMinutes": 30, + "deny": ["/Users/you/clients/acme"] +} +``` + +- `audience` is an audience slug from the console; leave it out and the whole project sees what + you send. +- `recall: false` stops the reading and keeps the sending. `ingest: false` does the opposite. +- `idleMinutes` is how long a session stays quiet before it is sent (720, twelve hours, by default). +- `sittingMinutes` is how long a session stays quiet before what follows starts a new sitting (30 + by default, 5 at least). Shorter dates what past.dev remembers more closely, in more and smaller + data points. A session keeps the length it was first sent with. +- `deny` holds path fragments. A session whose working directory matches one is ignored + completely — nothing is read and nothing is sent. +- `PAST_API_KEY`, `PAST_IDENTITY` and `PAST_API_URL` override the file. +- Self-host: point `apiUrl` at your own deployment. Nothing else changes. Use `https`: over plain + `http` to another machine the key travels unencrypted, and `connect` and `status` warn about it. + +## If nothing is recalled + +Ask for `$past:status`. It says when the hooks last ran. If they never ran, they are not trusted +yet. If they are trusted and still never run, Codex did not find `node`: it runs hooks in a login +shell, which does not read `~/.zshrc`. + +## Requirements + +Node 16 or later, on the path of a login shell. + +## Where it keeps things + +Everything lives in `~/.past/`, a folder only you can open: `config.json` (the connection), +`state.json` (what was sent, as content hashes), `activity/` (one small file per open session, for +the idle timer) and `hosts/` (when the hooks last ran). + +## Removing it + +`codex plugin remove past@past`, then delete `~/.past`. Revoking the key in **Build › API keys** +stops it immediately from the server side. What was already sent stays until you delete it in the +console. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/plugins/past/bin/past-hook.mjs b/plugins/past/bin/past-hook.mjs new file mode 100755 index 0000000..5e10f6e --- /dev/null +++ b/plugins/past/bin/past-hook.mjs @@ -0,0 +1,1399 @@ +#!/usr/bin/env node +// past.dev for Codex. +// +// Five hooks and six skills, over the public Memory API. Nothing here is privileged: a +// customer can write the same thing against the same endpoints with the same key. +// +// The one rule that governs every path: a hook must never break the session it runs in. Every +// mode exits 0, prints valid JSON or nothing, and swallows its own failures. + +import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, chmodSync, unlinkSync, renameSync, rmdirSync, openSync, readSync, closeSync } from 'node:fs'; +import { spawn, execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { fileURLToPath } from 'node:url'; +import { homedir } from 'node:os'; +import { join, basename } from 'node:path'; + +const HOME = join(homedir(), '.past'); +const CONFIG_PATH = join(HOME, 'config.json'); +const STATE_PATH = join(HOME, 'state.json'); +// One small file per open session, written at every turn: the idle timer reads it. This plugin's +// are in a folder of their own; the top of activity/ can belong to another past.dev plugin. +const ACTIVITY_DIR = join(HOME, 'activity', 'codex'); +// When this plugin's hooks last ran, for status: Codex runs no hook a person has not trusted, and +// says nothing of the ones it skips. +const STAMP_PATH = join(HOME, 'hosts', 'codex.json'); +const DEFAULT_IDLE_MINUTES = 720; // 12 hours +// A session is cut into sittings where it went quiet this long. Shorter dates memories more +// closely and makes more data points, each with less of the conversation around it; under five +// minutes nearly every sitting would be one exchange, so a smaller value is read as five. +const DEFAULT_SITTING_MINUTES = 30; +const MIN_SITTING_MINUTES = 5; +const DEFAULT_API_URL = 'https://api.past.dev'; +const MARKER = '=== past · recalled from memory ==='; +const VERSION = '0.1.0'; + +// ---------------------------------------------------------------------------------------- Codex + +// The source past.dev sees, and the prefix of every data point's id; the agent's name and its speaker +// are printed into the prose that is sent. Changing any of the three changes the content of every +// session already in past.dev, and the next backfill would send them all again as new revisions. +const SOURCE = 'codex'; +const AGENT = 'Codex'; +const SPEAKER = 'Codex'; + +// Codex writes blocks of its own under the user's role, and a recent Codex says of each message +// what it is made of. This list is for the transcripts that do not say (a thread from an older +// Codex, an imported or legacy one), and for the prompt hook, which sees the text alone: the +// repository's AGENTS.md, a skill's whole file, a shell command a person ran and its output. A +// prompt that runs one of the plugin's skills is not conversation either. +const SYNTHETIC = [ + '', '', '', '# AGENTS.md instructions for', + '', '', '', '', '', + '', '', '', '', + '', '', '$past:', +]; +// A Codex transcript is named rollout-