@projectpac/source-conversation-notes
v0.6.0
Published
Part of PAC: @projectpac/source-conversation-notes.
Readme
@projectpac/source-conversation-notes
The principal's regular Claude conversations, as sources this adapter keeps in its own folder -- by way of files Claude writes about itself.
@projectpac/source-agent-history ends its README naming the gap this fills: a
node has no way to read claude.ai. Everything under ~/.claude is Claude Code,
the CLI exposes no conversation retrieval, and the account's data export is the
only route out. So the way around it is not to reach further. It is to ask the
other side to write something down -- a skill, on a schedule, inside Claude --
and then to read a folder.
Why this is not a second agent-history
The two look alike and are opposite. agent-history reads raw transcripts and
spends model runs turning them into a profile: ten runs over three quarters of a
megabyte, $9.84 for a first pass, which is why it has no schedule and waits to
be asked. Here a model already did that work, on the other side of the seam, and
what lands on the machine is the conclusion. So this adapter injects no
intelligence, spends nothing, and a poll is a directory listing -- which is
what earns it a schedule where the other one cannot have any.
It copies an outside system's records rather than reasoning over them, in other words. The outside system is a folder.
One file per run
The skill writes one small file each time it runs, named for the moment it ran, holding nothing but topic paragraphs. It never edits or deletes one. That shape is the whole contract, and it is what this adapter used to lack.
It used to read one document with a section per day, which the skill appended
to and revised in place. That document grew without bound, so it had to be
split here -- along headings a model was asked to keep exactly, with a parser
willing to be wrong about everything else -- and the schedule could not be
finer than the heading: a day was the unit only because ## YYYY-MM-DD named
one. A file per run leaves nothing to split and nothing to keep exact but a
name, and the schedule is whatever the schedule is. The one document a flow
might still want is assembled here, out of the files, rather than maintained
over there.
What comes out
Two verbs, the same two dobj offers: inventory() answers with names and a
sentence about each, and stage() copies the named ones into the calling
plugin's folder and answers with relative paths. A flow learns what its
principal has by asking, and gets bytes only by staging them somewhere it can
point a box at.
There is no source registry to write into any more, so a source is a file in
this adapter's own folder -- <dataDir>/conversation-notes/sources/ -- and
these two verbs are how anything else learns it exists.
conversation-notes-<stamp>, one per note, for every note withinkeepDaysof the newest. Its note says when it was written and names its topics, and quotes none of it. A note that ages out of the window stops being nameable -- a source a flow could stake that nothing refreshes is worse than none -- and its bytes here are deleted with it. The file the skill wrote is not touched.conversation-notes, one document: every kept note, newest first, under a heading a person reads. Assembled here, so the skill writes and never edits, and the thing that wants one document is the thing that makes it.
Stamps name these, never topics. agent-history documents what the
alternative costs: names taken from a model's prose drift between passes
("pikanet" became "Pikanet"), and the removal sweep reads a drifted name as one
source vanishing and another appearing -- eleven deleted and recreated after two
of two hundred and forty files had changed. A time cannot drift.
The skill
This is the half that runs inside Claude. Paste it into a Claude environment
that has a filesystem and scheduled tasks -- Cowork, not plain chat, which has
neither -- as daily-profile, run it once, then ask for a schedule: make a
schedule to run this every hour.
---
name: daily-profile
description: Write a short note about what the user has talked about in their recent Claude conversations, as a new file in the conversation-notes plugin's own folder. Use when the user asks to update their conversation notes, profile their recent chats, or record what they have been working on.
---
# Daily Profile
Write a short note about what the user has talked about in their recent Claude
conversations, as a new file in the folder the node's `conversation-notes`
plugin reads.
## Where the notes go
The folder connected to this session, which is this plugin's own folder on the
node. Whoever asked for the skill names it when they ask -- a home is not always
`~/.pac` and the data directory under it is not always where a guess would put
it, so the path is given rather than worked out. Notes go in a `notes/`
subfolder of it; create that subfolder if it is not there yet.
Being given it is not a convenience. The run that matters is a scheduled one,
and a scheduled run happens in an isolated machine with that folder mounted and
nothing else on it: no node to ask, and no `pac` to ask with. A skill that
insists on looking the path up stops there and writes nothing, which is the one
outcome worse than writing to the wrong place, because it looks like nothing
happened.
Where there _is_ a node to ask -- a session on the same machine, run by hand --
the folder can be confirmed rather than trusted:
```bash
pac api GET /plugins
```
Take the `dir` of the row whose `plugin` is `conversation-notes`. If it
disagrees with the folder you were given, say so and write neither: notes in
two places are a log that is missing half of itself, whichever one gets read.
If no folder was named and there is no `pac` to ask, stop and say so rather than
picking somewhere -- a file in the wrong place is read by nothing.
## The format is a contract
These files are read by a program, not only by a person. Two things about them
are fixed, and getting either wrong costs the reader the note:
- The file name is the current time in UTC to the minute, with hyphens where
the colon would be: `2026-09-07T11-16Z.md`. Nothing else in the name.
- A topic paragraph opens with its name in bold: `**The topic.** Then prose.`
Everything else is ordinary markdown and can be written however reads best. No
title, no heading, no footer: the file is its paragraphs.
## Each run is its own file
Write one new file per run and never edit, rename or delete an existing one.
If you have more to say about a topic that an earlier note covered, say it in
the new note. If nothing new has been discussed since the newest note, write no
file and say so -- an empty note is worse than none.
## What to write
Group by topic. Bold the topic name, then 2-4 sentences of substance: what the
thing is, what was decided, what is still open.
Cover only conversations since the time in the newest file's name. On a first
run, when the folder is empty, cover the last 7 days.
## Rules
- Never write API keys, tokens, passwords, or private keys into a note.
Nothing downstream strips them: this rule is the whole of what stands between
a secret and a file that becomes readable to a peer.
- Replace the user's home directory path with `~` anywhere it appears.
- Write the file only. Do not stage, commit, or push anything.
When done, report the path written and the topics recorded.Where it lands, and why there
This adapter's own folder: <data dir>/conversation-notes/notes/, beside the
sources/ it keeps there already. A dir in the row overrides it, and an
absolute one is taken as given.
It used to sit beside the node's home, and the argument for that has since expired. It was: something outside the node writes these, and a folder a plugin owns is not a drop box for a program that has never heard of the node. Two things made that true, and both changed.
A writer could not find the folder. Composing <home>/data/<id> meant
hardcoding the node's layout into something outside it, and getting it wrong for
a daemon started bare, where PAC_DATA_DIR defaults to the home rather than
<home>/data. GET /plugins now reports each plugin's dir, so a writer can
be told.
And removal deleted the folder. Months of the principal's own account of their
weeks were one Remove click from gone, for a plugin that only ever read them.
The host now dispatches core/removing and deletes nothing, so what is here
outlives the plugin and a later install of it finds the notes again.
What is left of the old reason is worth keeping in view: the folder is still this plugin's, and a program writing into it is being trusted with somewhere that is not its own. It is told where rather than guessing, which is the whole of the difference.
Not a path under ~ outside any home, either: that is shared by every node on
the machine, so two of them would each claim the other's conversations as their
principal's.
This used to be the private directory, and that mattered. The sandbox was denied it both ways, which made it the one place the node could read and a model's subprocess could not -- the right side of the line for a document that is the principal's account of their own week. There is no private directory now and the sandbox denies nothing, so that property is gone rather than moved. It comes back when per-folder permissioning does; until then, the notes are as readable to a turn this node runs as anything else under the home.
Getting the files here
Deliberately not this adapter's business. Its whole contract is that files
appeared in a folder -- the same contract agent-history has with ~/.claude.
Whether the skill writes there directly, something syncs them down from a
remote sandbox, or the principal drops them in, the adapter sees a folder whose
listing moved. Anything in the folder that is not named as a note -- a draft,
an editor's backup, a subfolder -- is left alone without comment.
Configuration
| field | default | why |
| -------------- | ------- | ------------------------------------------------------------------------------------------------------------ |
| dir | notes | in this plugin's own folder -- see above. Relative resolves against it; absolute is taken as given |
| maxFileBytes | 1 MiB | a note past this is a runaway rather than a summary; it is refused, once per version, and the others survive |
| keepDays | 14 | how far back a note gets a source of its own, counted from the newest note rather than from the clock |
The poll is a minute, on a timer of the adapter's own: the folder moves at most hourly, so a minute picks a run up promptly and the other fifty-nine cost a listing each. A poll that finds nothing changed reads nothing and writes nothing, and one that finds a new note reads that note and rewrites the document -- never the other notes.
keepDays is counted from the newest note rather than from the clock so that a
node that was off for a month does not wake up naming nothing, and so a suite
can pin it.
What it does not do
No redaction. The skill's rules above are the only control. That is a
deliberate choice and not the same one agent-history made -- it reads raw
transcripts, where a secret arrives whether anyone meant it to or not, so it
carries a redaction table as a backstop. Here each file is a summary a model
wrote to instructions that tell it not to write secrets. If that assumption ever
stops holding, the backstop belongs in the sdk rather than copied out of
agent-history: two copies of a security control are two things to keep in
step, and the sdk is where the one copy of a path check already lives.
Topic names still come out of the files and still reach a model, because a note does. Those are held to one line with no angle brackets -- not a filter on what a name says, only on what shape it can take.
And a flow that stages a source can read it. The bytes land in the flow's
own folder, which is the only place a box can be pointed at, and nothing stops
the flow reading what it put there. data.apply used to be what made "names
what it cannot read" true; with that gone, staging is the honest shape, and this
is the property it costs. It comes back with the check.
Material, for a plugin that reasons over it
@projectpac/source-intents reads the notes, and rather than stage the document
and parse it, it asks: this adapter also implements @projectpac/material, so
units() lists the kept notes and read() hands one over as observations, one
per topic paragraph, each marked as a summary -- an assistant's account of the
conversations, not the principal's words -- at the moment the note was written.
units() syncs first, so a consumer sees the folder as it is rather than as the
last tick found it. A note is never edited, so a unit whose text has not changed
is one nothing has to reason over again. No redaction on the way, for the reason
above; what is safe to put in a prompt is the consumer's question, and it is the
consumer that answers it.
It also says when the folder moved. watch(listener) fires from the minute's
poll whenever the listing changed -- a note landed, one was taken away -- and
answers with the unsubscribe. It does not fire from a consumer's own units(),
because a consumer that just listed the folder has already seen the change, and
it does not fire for the first pass after activation, which is a baseline rather
than a change. The poll rather than fs.watch, because the folder often does not
exist yet when this activates and the skill writes at most hourly, so a minute
of latency costs nothing a watcher would save.
