Server-beta: Postgres storage + independent runtime + BullMQ queue (Phases 1–3) (#2351)

* Add server beta runtime foundation

* Address server beta review findings

* Resolve server beta review comments

* Tighten server beta review follow-ups

* Harden server beta auth and search

* Avoid unnecessary FTS rebuilds

* Block scoped keys from creating projects

* Release BullMQ claims best effort on close

* Address server beta review blockers

* Reset BullMQ claims best effort

* Add Postgres observation storage foundation

* feat(server-beta): add independent runtime service

Introduce src/server/runtime/ as a self-contained server-beta runtime
that owns its lifecycle, Postgres bootstrap, and HTTP boundary without
depending on WorkerService.

ServerBetaService wraps the existing Server class, exposes
/healthz and /v1/info with runtime="server-beta", and persists state
to dedicated paths (.server-beta.pid|.port|.runtime.json). The four
boundary managers (queue, generation worker, provider registry, event
broadcaster) are intentionally disabled in this phase and report their
status through /v1/info; later phases activate them.

Adds plans/2026-05-07-finish-bullmq-branch-ship-plan.md to track the
remaining work for this branch.

Phase 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): route CLI lifecycle and bundle separate runtime

scripts/build-hooks.js now produces plugin/scripts/server-beta-service.cjs
as a separate Node CJS bundle, alongside the existing worker-service
bundle. The server-beta runtime is now installable independently.

src/npx-cli/commands/server.ts routes start|stop|restart|status to the
server-beta lifecycle instead of the legacy worker. The worker keeps its
own start|stop|restart|status under the worker namespace; the two
runtimes can be operated independently.

src/services/worker-service.ts adds a server-* command parser branch
that delegates to the sibling server-beta-service.cjs bundle so
direct worker-service invocations still route to the right runtime.

tests/npx-cli-server-namespace.test.ts updated to expect server-beta
lifecycle routing.

Includes rebuilt plugin/scripts/*.cjs bundles produced by
build-and-sync.

Phase 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): add BullMQ job queue primitives

Introduce src/server/jobs/ as the queue-side primitives that Phase 3 of
the server-beta runtime needs to operate.

types.ts defines a discriminated union over the four job kinds (event,
event-batch, summary, reindex) and maps each to a per-kind BullMQ queue
name and deterministic-ID prefix.

job-id.ts builds deterministic, colon-free BullMQ jobIds from
(kind, team, project, source). The colon ban exists because BullMQ uses
':' as a Redis key separator internally; embedding ':' in jobIds
breaks scan and state lookups.

ServerJobQueue.ts is a thin wrapper over BullMQ Queue + Worker that
enforces autorun:false, default concurrency 1, and an attached error
listener — all per BullMQ docs requirements. Test seams accept queue
and worker factories so unit tests do not need Redis.

outbox.ts publishes through the Postgres ObservationGenerationJob
repository as canonical history. enqueueOutbox writes the row first,
then publishes to BullMQ; if BullMQ throws, the row is transitioned to
failed and a failed event is appended. reconcileOnStartup re-enqueues
queued + processing rows after a restart, replacing terminal BullMQ
jobs that may still be holding the deterministic ID slot. markCompleted
and markFailed wrap transitionStatus and append the matching event row.

Includes 20 unit tests covering deterministic ID stability, colon-free
output, queue lifecycle, error-listener attachment, double-start
refusal, idempotent enqueue, BullMQ failure rollback, startup
reconciliation, max-attempts skipping, and completion / failure /
retry transitions.

Phase 3 commit 1 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server-beta): activate queue boundary in runtime service

Wire ActiveServerBetaQueueManager into the server-beta runtime graph.
The active manager owns one ServerJobQueue per generation kind (event,
event-batch, summary, reindex) and surfaces lane metadata through
boundary health.

Selection is opt-in and fail-fast: if CLAUDE_MEM_QUEUE_ENGINE is set to
bullmq the active manager is constructed (and any Redis/config error
throws — no silent fallback to SQLite, per Phase 3 anti-pattern guard).
For any other engine the disabled boundary remains so worker-era and
test setups stay compatible.

Widens ServerBetaBoundaryHealth.status to a discriminated union
('disabled' | 'active' | 'errored') with optional details. The disabled
adapter still emits status='disabled', which keeps the existing
server-beta-service test green.

ServerBetaService receives the manager through a new optional
queueManager field on CreateServerBetaServiceOptions so test graphs
and Phase 4 wiring can inject custom managers.

Adds tests/server/runtime/active-queue-manager.test.ts covering bullmq
guard, active health shape, per-kind queue access, close behavior, and
post-close errored health.

Phase 3 commit 2 of plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server-beta): cap /v1/events/batch at 500 events

Prevents unbounded array DoS surface flagged in PR review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-05-08 01:20:07 -07:00
committed by GitHub
parent 0a43ab7632
commit 36b0929fae
183 changed files with 35709 additions and 2033 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"name": "Alex Newman"
},
"repository": "https://github.com/thedotmack/claude-mem",
"license": "AGPL-3.0",
"license": "Apache-2.0",
"keywords": [
"claude",
"claude-code",
+1 -1
View File
@@ -8,7 +8,7 @@
},
"homepage": "https://github.com/thedotmack/claude-mem#readme",
"repository": "https://github.com/thedotmack/claude-mem",
"license": "AGPL-3.0",
"license": "Apache-2.0",
"keywords": [
"claude",
"claude-code",
+202 -630
View File
@@ -1,630 +1,202 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+8
View File
@@ -0,0 +1,8 @@
Claude-Mem
Copyright 2026 Alex Newman
This product includes software developed for the Claude-Mem project.
Licensed under the Apache License, Version 2.0.
If other attributions are required by dependencies or included code, add them here.
+10 -13
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -385,20 +385,17 @@ See [Development Guide](https://docs.claude-mem.ai/development) for contribution
## License
This project is licensed under the **GNU Affero General Public License v3.0** (AGPL-3.0).
Claude-Mem is licensed under the Apache License 2.0.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
We chose Apache-2.0 because durable agentic memory should be easy to embed in
developer tools, local agents, MCP servers, enterprise systems, robotics stacks,
and production agent harnesses.
See the [LICENSE](LICENSE) file for full details.
See the [LICENSE](LICENSE) file for full details. See [docs/license.md](docs/license.md)
and [docs/ip-boundary.md](docs/ip-boundary.md) for licensing scope and the
open/commercial boundary.
**What This Means:**
- You can use, modify, and distribute this software freely
- If you modify and deploy on a network server, you must make your source code available
- Derivative works must also be licensed under AGPL-3.0
- There is NO WARRANTY for this software
**Note on Ragtime**: The `ragtime/` directory is licensed separately under the **PolyForm Noncommercial License 1.0.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
**Note on Ragtime**: The `ragtime/` directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
@@ -413,7 +410,7 @@ See the [LICENSE](LICENSE) file for full details.
---
**Built with Claude Agent SDK** | **Powered by Claude Code** | **Made with TypeScript**
**Built with Claude Agent SDK** | **Works with Claude Code** | **Made with TypeScript**
---
+16
View File
@@ -0,0 +1,16 @@
services:
server-beta-e2e:
image: node:20-alpine
depends_on:
claude-mem-server:
condition: service_healthy
valkey:
condition: service_healthy
environment:
E2E_BASE_URL: http://claude-mem-server:37777
E2E_REDIS_HOST: valkey
E2E_REDIS_PORT: 6379
volumes:
- ./docker/e2e:/e2e:ro
working_dir: /e2e
command: ["node", "/e2e/server-beta-e2e.mjs"]
+45
View File
@@ -0,0 +1,45 @@
services:
valkey:
image: valkey/valkey:8-alpine
command: ["valkey-server", "--appendonly", "yes"]
volumes:
- valkey-data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
claude-mem-server:
build:
context: .
dockerfile: docker/claude-mem/Dockerfile
depends_on:
valkey:
condition: service_healthy
environment:
CLAUDE_MEM_HOST: 0.0.0.0
CLAUDE_MEM_WORKER_HOST: 0.0.0.0
CLAUDE_MEM_WORKER_PORT: 37777
CLAUDE_MEM_DATA_DIR: /data/claude-mem
CLAUDE_MEM_QUEUE_ENGINE: bullmq
CLAUDE_MEM_REDIS_URL: redis://valkey:6379
CLAUDE_MEM_REDIS_MODE: docker
CLAUDE_MEM_AUTH_MODE: api-key
CLAUDE_MEM_CHROMA_ENABLED: "false"
ports:
- "37777:37777"
volumes:
- claude-mem-data:/data/claude-mem
- ${HOME}/.claude:/home/node/.claude:ro
command: ["bun", "/opt/claude-mem/scripts/worker-service.cjs", "--daemon"]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:37777/healthz"]
interval: 10s
timeout: 3s
retries: 12
start_period: 20s
volumes:
claude-mem-data:
valkey-data:
+9 -4
View File
@@ -41,14 +41,19 @@ USER root
COPY plugin/ /opt/claude-mem/
RUN chown -R node:node /opt/claude-mem
RUN mkdir -p /home/node/.claude /home/node/.claude-mem \
&& chown -R node:node /home/node/.claude /home/node/.claude-mem
USER node
WORKDIR /home/node
RUN cd /opt/claude-mem \
&& npm install --omit=dev --legacy-peer-deps
USER root
RUN mkdir -p /home/node/.claude /home/node/.claude-mem /data/claude-mem \
&& chown -R node:node /home/node/.claude /home/node/.claude-mem /data/claude-mem
COPY --chown=node:node docker/claude-mem/entrypoint.sh /usr/local/bin/claude-mem-entrypoint
RUN chmod +x /usr/local/bin/claude-mem-entrypoint
USER node
WORKDIR /home/node
ENTRYPOINT ["/usr/local/bin/claude-mem-entrypoint"]
CMD ["bash"]
+307
View File
@@ -0,0 +1,307 @@
import net from 'node:net';
const baseUrl = process.env.E2E_BASE_URL ?? 'http://claude-mem-server:37777';
const redisHost = process.env.E2E_REDIS_HOST ?? 'valkey';
const redisPort = Number.parseInt(process.env.E2E_REDIS_PORT ?? '6379', 10);
const phase = process.env.E2E_PHASE ?? 'phase1';
const apiKey = requiredEnv('E2E_API_KEY');
const readOnlyKey = process.env.E2E_READ_ONLY_API_KEY ?? '';
const revokedKey = process.env.E2E_REVOKED_API_KEY ?? '';
const runId = process.env.E2E_RUN_ID ?? `e2e-${Date.now()}`;
const projectRoot = `/tmp/claude-mem-server-beta-${runId}`;
function requiredEnv(key) {
const value = process.env[key];
if (!value) {
throw new Error(`${key} is required`);
}
return value;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
async function sleep(ms) {
await new Promise(resolve => setTimeout(resolve, ms));
}
async function request(path, options = {}) {
const headers = {
...(options.json !== undefined ? { 'content-type': 'application/json' } : {}),
...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),
...(options.headers ?? {}),
};
return fetch(`${baseUrl}${path}`, {
method: options.method ?? (options.json === undefined ? 'GET' : 'POST'),
headers,
body: options.json === undefined ? undefined : JSON.stringify(options.json),
});
}
async function json(response) {
const text = await response.text();
try {
return text ? JSON.parse(text) : null;
} catch (error) {
throw new Error(`Invalid JSON response (${response.status}): ${text}\n${error instanceof Error ? error.message : String(error)}`);
}
}
async function requestJson(path, options = {}) {
const response = await request(path, options);
const body = await json(response);
return { response, body };
}
async function expectStatus(path, status, options = {}) {
const response = await request(path, options);
assert(response.status === status, `${path} expected HTTP ${status}, got ${response.status}: ${await response.text()}`);
}
async function waitForReadiness() {
const deadline = Date.now() + 120_000;
let lastError = '';
while (Date.now() < deadline) {
try {
const health = await request('/healthz');
const readiness = await request('/api/readiness');
if (health.ok && readiness.ok) {
return;
}
lastError = `health=${health.status} readiness=${readiness.status}`;
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
await sleep(1000);
}
throw new Error(`Server did not become ready: ${lastError}`);
}
async function assertRedisPing() {
const result = await new Promise((resolve, reject) => {
const socket = net.createConnection({ host: redisHost, port: redisPort });
socket.setTimeout(3000);
let data = '';
socket.on('connect', () => socket.write('*1\r\n$4\r\nPING\r\n'));
socket.on('data', chunk => {
data += chunk.toString('utf8');
if (data.includes('PONG')) {
socket.end();
resolve(data);
}
});
socket.on('timeout', () => {
socket.destroy();
reject(new Error('Redis PING timed out'));
});
socket.on('error', reject);
socket.on('close', () => {
if (!data.includes('PONG')) {
reject(new Error(`Redis PING failed: ${data}`));
}
});
});
assert(String(result).includes('PONG'), `Redis did not return PONG: ${result}`);
}
async function assertQueueHealth() {
const { response, body } = await requestJson('/api/health');
assert(response.ok, `/api/health expected OK, got ${response.status}`);
assert(body.queue?.engine === 'bullmq', `expected BullMQ queue engine, got ${JSON.stringify(body.queue)}`);
assert(body.queue?.redis?.status === 'ok', `expected Redis health ok, got ${JSON.stringify(body.queue?.redis)}`);
assert(body.queue?.redis?.mode === 'docker', `expected docker Redis mode, got ${JSON.stringify(body.queue?.redis)}`);
}
async function phase1() {
console.log(`[e2e] phase1 starting (${runId})`);
await waitForReadiness();
await assertQueueHealth();
await assertRedisPing();
await expectStatus('/v1/projects', 401, {
method: 'POST',
json: { name: 'unauthenticated' },
});
await expectStatus('/v1/projects', 403, {
method: 'POST',
apiKey: 'cmem_invalid_key',
json: { name: 'invalid' },
});
if (readOnlyKey) {
await expectStatus('/v1/projects', 403, {
method: 'POST',
apiKey: readOnlyKey,
json: { name: 'read-only denied' },
});
const readOnlyProjects = await request('/v1/projects', { apiKey: readOnlyKey });
assert(readOnlyProjects.ok, `read-only key should read projects, got ${readOnlyProjects.status}`);
}
const createdProject = await requestJson('/v1/projects', {
apiKey,
json: {
name: `Server Beta E2E ${runId}`,
rootPath: projectRoot,
metadata: { runId },
},
});
assert(createdProject.response.status === 201, `project create failed: ${JSON.stringify(createdProject.body)}`);
const project = createdProject.body.project;
assert(project?.id, 'project response missing id');
const createdSession = await requestJson('/v1/sessions/start', {
apiKey,
json: {
projectId: project.id,
contentSessionId: `content-${runId}`,
memorySessionId: `memory-${runId}`,
platformSource: 'docker-e2e',
title: 'Docker E2E session',
},
});
assert(createdSession.response.status === 201, `session create failed: ${JSON.stringify(createdSession.body)}`);
const session = createdSession.body.session;
const createdEvent = await requestJson('/v1/events', {
apiKey,
json: {
projectId: project.id,
serverSessionId: session.id,
sourceType: 'api',
eventType: 'observation.created',
contentSessionId: `content-${runId}`,
memorySessionId: `memory-${runId}`,
payload: { tool_name: 'Read', runId },
occurredAtEpoch: Date.now(),
},
});
assert(createdEvent.response.status === 201, `event create failed: ${JSON.stringify(createdEvent.body)}`);
const event = createdEvent.body.event;
const batchEvents = await requestJson('/v1/events/batch', {
apiKey,
json: [
{
projectId: project.id,
sourceType: 'api',
eventType: 'observation.created',
payload: { index: 1, runId },
occurredAtEpoch: Date.now(),
},
{
projectId: project.id,
sourceType: 'api',
eventType: 'observation.created',
payload: { index: 2, runId },
occurredAtEpoch: Date.now(),
},
],
});
assert(batchEvents.response.status === 201, `event batch failed: ${JSON.stringify(batchEvents.body)}`);
assert(batchEvents.body.events.length === 2, 'event batch did not return two events');
const fetchedEvent = await requestJson(`/v1/events/${event.id}`, { apiKey });
assert(fetchedEvent.response.ok, `event fetch failed: ${JSON.stringify(fetchedEvent.body)}`);
const createdMemory = await requestJson('/v1/memories', {
apiKey,
json: {
projectId: project.id,
serverSessionId: session.id,
kind: 'manual',
type: 'decision',
title: `Docker E2E memory ${runId}`,
narrative: `Server beta Docker E2E memory survives restart for ${runId}.`,
facts: ['BullMQ health is backed by Valkey', `run:${runId}`],
concepts: ['server-beta', 'docker-e2e'],
metadata: { runId },
},
});
assert(createdMemory.response.status === 201, `memory create failed: ${JSON.stringify(createdMemory.body)}`);
const memory = createdMemory.body.memory;
const patchedMemory = await requestJson(`/v1/memories/${memory.id}`, {
method: 'PATCH',
apiKey,
json: {
projectId: project.id,
kind: 'manual',
type: 'decision',
narrative: `Patched Docker E2E memory survives restart for ${runId}.`,
facts: ['patched', `run:${runId}`],
},
});
assert(patchedMemory.response.ok, `memory patch failed: ${JSON.stringify(patchedMemory.body)}`);
assert(patchedMemory.body.memory.narrative.includes('Patched'), 'patched memory narrative was not returned');
const fetchedMemory = await requestJson(`/v1/memories/${memory.id}`, { apiKey });
assert(fetchedMemory.response.ok, `memory fetch failed: ${JSON.stringify(fetchedMemory.body)}`);
const search = await requestJson('/v1/search', {
apiKey,
json: { projectId: project.id, query: runId, limit: 10 },
});
assert(search.response.ok, `search failed: ${JSON.stringify(search.body)}`);
assert(search.body.memories.some(item => item.id === memory.id), 'search did not return created memory');
const context = await requestJson('/v1/context', {
apiKey,
json: { projectId: project.id, query: 'patched', limit: 5 },
});
assert(context.response.ok, `context failed: ${JSON.stringify(context.body)}`);
assert(context.body.context.includes(runId), 'context did not include created memory text');
const endedSession = await requestJson(`/v1/sessions/${session.id}/end`, {
method: 'POST',
apiKey,
json: {},
});
assert(endedSession.response.ok, `session end failed: ${JSON.stringify(endedSession.body)}`);
assert(endedSession.body.session.status === 'completed', 'session did not complete');
const audit = await requestJson(`/v1/audit?projectId=${encodeURIComponent(project.id)}`, { apiKey });
assert(audit.response.ok, `audit failed: ${JSON.stringify(audit.body)}`);
assert(audit.body.audit.some(row => row.action === 'memory.write'), 'audit log missing memory.write');
console.log(`[e2e] phase1 passed project=${project.id} memory=${memory.id}`);
}
async function phase2() {
console.log(`[e2e] phase2 after restart starting (${runId})`);
await waitForReadiness();
await assertQueueHealth();
await assertRedisPing();
if (revokedKey) {
await expectStatus('/v1/projects', 403, { apiKey: revokedKey });
}
const projects = await requestJson('/v1/projects', { apiKey });
assert(projects.response.ok, `project list failed after restart: ${JSON.stringify(projects.body)}`);
const project = projects.body.projects.find(item => item.rootPath === projectRoot);
assert(project?.id, `persisted project not found for ${projectRoot}`);
const search = await requestJson('/v1/search', {
apiKey,
json: { projectId: project.id, query: runId, limit: 10 },
});
assert(search.response.ok, `search failed after restart: ${JSON.stringify(search.body)}`);
assert(search.body.memories.some(item => String(item.narrative ?? '').includes(runId)), 'persisted memory not found after restart');
const audit = await requestJson(`/v1/audit?projectId=${encodeURIComponent(project.id)}`, { apiKey });
assert(audit.response.ok, `audit failed after restart: ${JSON.stringify(audit.body)}`);
assert(audit.body.audit.length > 0, 'audit log did not persist after restart');
console.log(`[e2e] phase2 passed project=${project.id}`);
}
if (phase === 'phase1') {
await phase1();
} else if (phase === 'phase2') {
await phase2();
} else {
throw new Error(`Unknown E2E_PHASE: ${phase}`);
}
+5
View File
@@ -0,0 +1,5 @@
# Adapters
Claude Code hook payloads are mapped through `src/adapters/claude-code/mapper.ts` into `AgentEvent` records. The mapper preserves legacy fields such as `contentSessionId`, `tool_name`, `tool_input`, `tool_response`, `cwd`, `agentId`, `agentType`, `platformSource`, and both `tool_use_id` and `toolUseId`.
Generic agent examples live in `src/adapters/generic-rest/examples.ts` for Codex, OpenCode, and custom REST ingestion. New adapters should emit the REST V1 event shape instead of coupling their payloads to Claude Code internals.
+25
View File
@@ -0,0 +1,25 @@
# Server API
REST V1 is mounted under `/v1`; legacy worker routes remain under `/api`.
Available beta endpoints:
- `GET /healthz`
- `GET /v1/info`
- `GET /v1/projects`
- `POST /v1/projects`
- `GET /v1/projects/:id`
- `POST /v1/sessions/start`
- `POST /v1/sessions/:id/end`
- `GET /v1/sessions/:id`
- `POST /v1/events`
- `POST /v1/events/batch`
- `GET /v1/events/:id`
- `POST /v1/memories`
- `GET /v1/memories/:id`
- `PATCH /v1/memories/:id`
- `POST /v1/search`
- `POST /v1/context`
- `GET /v1/audit?projectId=<id>`
When `CLAUDE_MEM_AUTH_MODE=api-key`, send `Authorization: Bearer <key>`. Read endpoints require `memories:read`; write endpoints require `memories:write`.
+18
View File
@@ -0,0 +1,18 @@
# Docker
The root `docker-compose.yml` starts Claude-Mem Server beta with a persistent Valkey sidecar.
```sh
docker compose up --build
curl http://127.0.0.1:37777/healthz
```
The server container uses:
- `CLAUDE_MEM_WORKER_HOST=0.0.0.0`
- `CLAUDE_MEM_DATA_DIR=/data/claude-mem`
- `CLAUDE_MEM_QUEUE_ENGINE=bullmq`
- `CLAUDE_MEM_REDIS_URL=redis://valkey:6379`
- `CLAUDE_MEM_AUTH_MODE=api-key`
Create an API key inside the container before using protected V1 write routes.
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -275,25 +275,21 @@ npm run bug-report
---
## الترخيص (License)
## License
هذا المشروع مرخص بموجب **ترخيص GNU Affero العام الإصدار 3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
حقوق النشر (C) 2025 Alex Newman (@thedotmack). جميع الحقوق محفوظة.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
انظر ملف [LICENSE](LICENSE) للتفاصيل الكاملة.
See the [LICENSE](LICENSE) file for full details.
**ماذا يعني هذا:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- يمكنك استخدام وتعديل وتوزيع هذا البرنامج بحرية
- إذا قمت بتعديل ونشر على خادم شبكة، يجب أن تتيح كود المصدر الخاص بك
- الأعمال المشتقة يجب أن تكون مرخصة أيضًا تحت AGPL-3.0
- لا يوجد ضمان لهذا البرنامج
**ملاحظة حول Ragtime**: دليل `ragtime/` مرخص بشكل منفصل تحت **ترخيص PolyForm Noncommercial 1.0.0**. انظر [ragtime/LICENSE](ragtime/LICENSE) للتفاصيل.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## الدعم
- **التوثيق**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## লাইসেন্স
## License
এই প্রকল্পটি **GNU Affero General Public License v3.0** (AGPL-3.0) এর অধীনে লাইসেন্সপ্রাপ্ত।
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). সর্বস্বত্ব সংরক্ষিত।
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
সম্পূর্ণ বিবরণের জন্য [LICENSE](LICENSE) ফাইল দেখুন।
See the [LICENSE](LICENSE) file for full details.
**এর অর্থ কী:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- আপনি এই সফটওয়্যারটি অবাধে ব্যবহার, পরিবর্তন এবং বিতরণ করতে পারেন
- যদি আপনি পরিবর্তন করেন এবং একটি নেটওয়ার্ক সার্ভারে ডিপ্লয় করেন, তাহলে আপনাকে আপনার সোর্স কোড উপলব্ধ করতে হবে
- ডেরিভেটিভ কাজগুলিও AGPL-3.0 এর অধীনে লাইসেন্সপ্রাপ্ত হতে হবে
- এই সফটওয়্যারের জন্য কোনও ওয়ারেন্টি নেই
**Ragtime সম্পর্কে নোট**: `ragtime/` ডিরেক্টরি আলাদাভাবে **PolyForm Noncommercial License 1.0.0** এর অধীনে লাইসেন্সপ্রাপ্ত। বিস্তারিত জানতে [ragtime/LICENSE](ragtime/LICENSE) দেখুন।
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## সাপোর্ট
- **ডকুমেন্টেশন**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Pracovní postup pro přispívání najdete v [Průvodci vývojem](https://docs.
---
## Licence
## License
Tento projekt je licencován pod **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Všechna práva vyhrazena.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Úplné podrobnosti najdete v souboru [LICENSE](LICENSE).
See the [LICENSE](LICENSE) file for full details.
**Co to znamená:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Software můžete volně používat, upravovat a distribuovat
- Pokud jej upravíte a nasadíte na síťovém serveru, musíte zpřístupnit svůj zdrojový kód
- Odvozená díla musí být také licencována pod AGPL-3.0
- Pro tento software neexistuje ŽÁDNÁ ZÁRUKA
**Poznámka k Ragtime**: Adresář `ragtime/` je licencován samostatně pod **PolyForm Noncommercial License 1.0.0**. Podrobnosti najdete v [ragtime/LICENSE](ragtime/LICENSE).
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Podpora
- **Dokumentace**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Se [Udviklingsguide](https://docs.claude-mem.ai/development) for bidragsworkflow
---
## Licens
## License
Dette projekt er licenseret under **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Alle rettigheder forbeholdes.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Se [LICENSE](LICENSE)-filen for fulde detaljer.
See the [LICENSE](LICENSE) file for full details.
**Hvad Dette Betyder:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Du kan bruge, modificere og distribuere denne software frit
- Hvis du modificerer og implementerer på en netværksserver, skal du gøre din kildekode tilgængelig
- Afledte værker skal også licenseres under AGPL-3.0
- Der er INGEN GARANTI for denne software
**Bemærkning om Ragtime**: `ragtime/`-kataloget er licenseret separat under **PolyForm Noncommercial License 1.0.0**. Se [ragtime/LICENSE](ragtime/LICENSE) for detaljer.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Support
- **Dokumentation**: [docs/](docs/)
+9 -13
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Siehe [Entwicklungsanleitung](https://docs.claude-mem.ai/development) für den B
---
## Lizenz
## License
Dieses Projekt ist unter der **GNU Affero General Public License v3.0** (AGPL-3.0) lizenziert.
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Alle Rechte vorbehalten.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Siehe die [LICENSE](LICENSE)-Datei für vollständige Details.
See the [LICENSE](LICENSE) file for full details.
**Was das bedeutet:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Sie können diese Software frei verwenden, modifizieren und verteilen
- Wenn Sie sie modifizieren und auf einem Netzwerkserver bereitstellen, müssen Sie Ihren Quellcode verfügbar machen
- Abgeleitete Werke müssen ebenfalls unter AGPL-3.0 lizenziert werden
- Es gibt KEINE GARANTIE für diese Software
**Hinweis zu Ragtime**: Das `ragtime/`-Verzeichnis ist separat unter der **PolyForm Noncommercial License 1.0.0** lizenziert. Siehe [ragtime/LICENSE](ragtime/LICENSE) für Details.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Support
- **Dokumentation**: [docs/](docs/)
@@ -301,4 +297,4 @@ Siehe die [LICENSE](LICENSE)-Datei für vollständige Details.
---
**Erstellt mit Claude Agent SDK** | **Powered by Claude Code** | **Made with TypeScript**
**Erstellt mit Claude Agent SDK** | **Works with Claude Code** | **Made with TypeScript**
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## Άδεια Χρήσης
## License
Αυτό το έργο διατίθεται με άδεια **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Με επιφύλαξη παντός δικαιώματος.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Δείτε το αρχείο [LICENSE](LICENSE) για πλήρεις λεπτομέρειες.
See the [LICENSE](LICENSE) file for full details.
**Τι Σημαίνει Αυτό:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Μπορείτε να χρησιμοποιήσετε, να τροποποιήσετε και να διανείμετε ελεύθερα αυτό το λογισμικό
- Εάν τροποποιήσετε και αναπτύξετε σε διακομιστή δικτύου, πρέπει να καταστήσετε διαθέσιμο τον πηγαίο κώδικά σας
- Τα παράγωγα έργα πρέπει επίσης να διατίθενται με άδεια AGPL-3.0
- ΔΕΝ υπάρχει ΕΓΓΥΗΣΗ για αυτό το λογισμικό
**Σημείωση για το Ragtime**: Ο κατάλογος `ragtime/` διατίθεται χωριστά με άδεια **PolyForm Noncommercial License 1.0.0**. Δείτε το [ragtime/LICENSE](ragtime/LICENSE) για λεπτομέρειες.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Υποστήριξη
- **Τεκμηρίωση**: [docs/](docs/)
+8 -12
View File
@@ -51,7 +51,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -274,25 +274,21 @@ Ver [Guía de Desarrollo](https://docs.claude-mem.ai/development) para el flujo
---
## Licencia
## License
Este proyecto está licenciado bajo la **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Todos los derechos reservados.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Ver el archivo [LICENSE](LICENSE) para detalles completos.
See the [LICENSE](LICENSE) file for full details.
**Lo Que Esto Significa:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Puedes usar, modificar y distribuir este software libremente
- Si modificas y despliegas en un servidor de red, debes hacer tu código fuente disponible
- Los trabajos derivados también deben estar licenciados bajo AGPL-3.0
- NO hay GARANTÍA para este software
**Nota sobre Ragtime**: El directorio `ragtime/` está licenciado por separado bajo la **PolyForm Noncommercial License 1.0.0**. Ver [ragtime/LICENSE](ragtime/LICENSE) para detalles.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Soporte
- **Documentación**: [docs/](docs/)
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -272,25 +272,21 @@ Katso [Kehitysopas](https://docs.claude-mem.ai/development) osallistumisen työn
---
## Lisenssi
## License
Tämä projekti on lisensoitu **GNU Affero General Public License v3.0** (AGPL-3.0) -lisenssillä.
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Kaikki oikeudet pidätetään.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Katso [LICENSE](LICENSE)-tiedosto täydellisistä yksityiskohdista.
See the [LICENSE](LICENSE) file for full details.
**Mitä tämä tarkoittaa:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Voit käyttää, muokata ja jakaa tätä ohjelmistoa vapaasti
- Jos muokkaat ja otat käyttöön verkkopalvelimella, sinun on asetettava lähdekoodisi saataville
- Johdannaisten teosten on myös oltava AGPL-3.0-lisensoituja
- Tälle ohjelmistolle EI OLE TAKUUTA
**Huomautus Ragtimesta**: `ragtime/`-hakemisto on erikseen lisensoitu **PolyForm Noncommercial License 1.0.0** -lisenssillä. Katso [ragtime/LICENSE](ragtime/LICENSE) yksityiskohdista.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Tuki
- **Dokumentaatio**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Voir le [Guide de développement](https://docs.claude-mem.ai/development) pour l
---
## Licence
## License
Ce projet est sous licence **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Tous droits réservés.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Voir le fichier [LICENSE](LICENSE) pour tous les détails.
See the [LICENSE](LICENSE) file for full details.
**Ce que cela signifie :**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Vous pouvez utiliser, modifier et distribuer ce logiciel librement
- Si vous modifiez et déployez sur un serveur réseau, vous devez rendre votre code source disponible
- Les œuvres dérivées doivent également être sous licence AGPL-3.0
- Il n'y a AUCUNE GARANTIE pour ce logiciel
**Note sur Ragtime** : Le répertoire `ragtime/` est sous licence séparée sous la **PolyForm Noncommercial License 1.0.0**. Voir [ragtime/LICENSE](ragtime/LICENSE) pour plus de détails.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Support
- **Documentation** : [docs/](docs/)
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -272,25 +272,21 @@ npm run bug-report
---
## רישיון
## License
פרויקט זה מורשה תחת **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
זכויות יוצרים (C) 2025 Alex Newman (@thedotmack). כל הזכויות שמורות.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
ראה את קובץ [LICENSE](LICENSE) לפרטים מלאים.
See the [LICENSE](LICENSE) file for full details.
**משמעות הדבר:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- אתה יכול לשימוש, שינוי והפצה של תוכנה זו בחופשיות
- אם אתה משנה ופורס על שרת רשת, עליך להנגיש את קוד המקור שלך
- עבודות נגזרות חייבות להיות מורשות גם כן תחת AGPL-3.0
- אין אחריות לתוכנה זו
**הערה על Ragtime**: ספריית `ragtime/` מורשית בנפרד תחת **PolyForm Noncommercial License 1.0.0**. ראה [ragtime/LICENSE](ragtime/LICENSE) לפרטים.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## תמיכה
- **תיעוד**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## लाइसेंस
## License
यह प्रोजेक्ट **GNU Affero General Public License v3.0** (AGPL-3.0) के तहत लाइसेंस प्राप्त है।
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack)। सर्वाधिकार सुरक्षित।
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
पूर्ण विवरण के लिए [LICENSE](LICENSE) फ़ाइल देखें।
See the [LICENSE](LICENSE) file for full details.
**इसका क्या अर्थ है:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- आप इस सॉफ़्टवेयर को स्वतंत्र रूप से उपयोग, संशोधित और वितरित कर सकते हैं
- यदि आप नेटवर्क सर्वर पर संशोधित और तैनात करते हैं, तो आपको अपना स्रोत कोड उपलब्ध कराना होगा
- व्युत्पन्न कार्यों को भी AGPL-3.0 के तहत लाइसेंस प्राप्त होना चाहिए
- इस सॉफ़्टवेयर के लिए कोई वारंटी नहीं है
**Ragtime पर नोट**: `ragtime/` डायरेक्टरी को **PolyForm Noncommercial License 1.0.0** के तहत अलग से लाइसेंस प्राप्त है। विवरण के लिए [ragtime/LICENSE](ragtime/LICENSE) देखें।
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## समर्थन
- **दस्तावेज़ीकरण**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ A hozzájárulási munkafolyamatért lásd a [Fejlesztési útmutatót](https://
---
## Licenc
## License
Ez a projekt a **GNU Affero General Public License v3.0** (AGPL-3.0) alatt licencelt.
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Minden jog fenntartva.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
A teljes részletekért lásd a [LICENSE](LICENSE) fájlt.
See the [LICENSE](LICENSE) file for full details.
**Mit jelent ez:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Szabadon használhatja, módosíthatja és terjesztheti ezt a szoftvert
- Ha módosítja és hálózati szerveren telepíti, elérhetővé kell tennie a forráskódot
- A származékos munkáknak szintén AGPL-3.0 alatt kell licencelve lenniük
- Ehhez a szoftverhez NINCS GARANCIA
**Megjegyzés a Ragtime-ról**: A `ragtime/` könyvtár külön licencelt a **PolyForm Noncommercial License 1.0.0** alatt. Részletekért lásd a [ragtime/LICENSE](ragtime/LICENSE) fájlt.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Támogatás
- **Dokumentáció**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Lihat [Panduan Pengembangan](https://docs.claude-mem.ai/development) untuk alur
---
## Lisensi
## License
Proyek ini dilisensikan di bawah **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Lihat file [LICENSE](LICENSE) untuk detail lengkap.
See the [LICENSE](LICENSE) file for full details.
**Apa Artinya:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Anda dapat menggunakan, memodifikasi, dan mendistribusikan perangkat lunak ini dengan bebas
- Jika Anda memodifikasi dan men-deploy di server jaringan, Anda harus membuat kode sumber Anda tersedia
- Karya turunan juga harus dilisensikan di bawah AGPL-3.0
- TIDAK ADA JAMINAN untuk perangkat lunak ini
**Catatan tentang Ragtime**: Direktori `ragtime/` dilisensikan secara terpisah di bawah **PolyForm Noncommercial License 1.0.0**. Lihat [ragtime/LICENSE](ragtime/LICENSE) untuk detail.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Dukungan
- **Dokumentasi**: [docs/](docs/)
@@ -301,6 +297,6 @@ Lihat file [LICENSE](LICENSE) untuk detail lengkap.
---
**Built with Claude Agent SDK** | **Powered by Claude Code** | **Made with TypeScript**
**Built with Claude Agent SDK** | **Works with Claude Code** | **Made with TypeScript**
---
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Vedi [Guida allo Sviluppo](https://docs.claude-mem.ai/development) per il flusso
---
## Licenza
## License
Questo progetto è rilasciato sotto la **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Tutti i diritti riservati.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Vedi il file [LICENSE](LICENSE) per i dettagli completi.
See the [LICENSE](LICENSE) file for full details.
**Cosa Significa:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Puoi usare, modificare e distribuire questo software liberamente
- Se modifichi e distribuisci su un server di rete, devi rendere disponibile il tuo codice sorgente
- Le opere derivate devono anche essere rilasciate sotto AGPL-3.0
- NON c'è GARANZIA per questo software
**Nota su Ragtime**: La directory `ragtime/` è rilasciata separatamente sotto la **PolyForm Noncommercial License 1.0.0**. Vedi [ragtime/LICENSE](ragtime/LICENSE) per i dettagli.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Supporto
- **Documentazione**: [docs/](docs/)
+7 -11
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## ライセンス
## License
このプロジェクトは**GNU Affero General Public License v3.0**(AGPL-3.0)の下でライセンスされています。
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
詳細は[LICENSE](LICENSE)ファイルを参照してください。
See the [LICENSE](LICENSE) file for full details.
**これが意味すること:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- このソフトウェアを自由に使用、変更、配布できます
- ネットワークサーバーで変更して展開する場合、ソースコードを利用可能にする必要があります
- 派生作品もAGPL-3.0の下でライセンスする必要があります
- このソフトウェアには保証がありません
**Ragtimeに関する注意**: `ragtime/`ディレクトリは **PolyForm Noncommercial License 1.0.0** の下で個別にライセンスされています。詳細は[ragtime/LICENSE](ragtime/LICENSE)を参照してください。
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## サポート
- **ドキュメント**: [docs/](docs/)
+7 -11
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## 라이선스
## License
이 프로젝트는 **GNU Affero General Public License v3.0** (AGPL-3.0)에 따라 라이선스가 부여됩니다.
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
전체 세부 정보는 [LICENSE](LICENSE) 파일을 참조하세요.
See the [LICENSE](LICENSE) file for full details.
**의미:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- 이 소프트웨어를 자유롭게 사용, 수정 및 배포할 수 있습니다
- 수정하여 네트워크 서버에 배포하는 경우 소스 코드를 공개해야 합니다
- 파생 작업물도 AGPL-3.0에 따라 라이선스가 부여되어야 합니다
- 이 소프트웨어에는 보증이 없습니다
**Ragtime에 대한 참고 사항**: `ragtime/` 디렉토리는 **PolyForm Noncommercial License 1.0.0**에 따라 별도로 라이선스가 부여됩니다. 자세한 내용은 [ragtime/LICENSE](ragtime/LICENSE)를 참조하세요.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## 지원
- **문서**: [docs/](docs/)
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -272,25 +272,21 @@ Zie [Ontwikkelingsgids](https://docs.claude-mem.ai/development) voor bijdragewor
---
## Licentie
## License
Dit project is gelicentieerd onder de **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Alle rechten voorbehouden.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Zie het [LICENSE](LICENSE) bestand voor volledige details.
See the [LICENSE](LICENSE) file for full details.
**Wat Dit Betekent:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Je kunt deze software vrijelijk gebruiken, aanpassen en distribueren
- Als je aanpast en implementeert op een netwerkserver, moet je je broncode beschikbaar maken
- Afgeleide werken moeten ook gelicentieerd zijn onder AGPL-3.0
- Er is GEEN GARANTIE voor deze software
**Opmerking over Ragtime**: De `ragtime/` directory is afzonderlijk gelicentieerd onder de **PolyForm Noncommercial License 1.0.0**. Zie [ragtime/LICENSE](ragtime/LICENSE) voor details.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Ondersteuning
- **Documentatie**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Se [Utviklingsveiledning](https://docs.claude-mem.ai/development) for bidragsfly
---
## Lisens
## License
Dette prosjektet er lisensiert under **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Alle rettigheter reservert.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Se [LICENSE](LICENSE)-filen for fullstendige detaljer.
See the [LICENSE](LICENSE) file for full details.
**Hva Dette Betyr:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Du kan bruke, modifisere og distribuere denne programvaren fritt
- Hvis du modifiserer og distribuerer på en nettverkstjener, må du gjøre kildekoden din tilgjengelig
- Avledede verk må også være lisensiert under AGPL-3.0
- Det er INGEN GARANTI for denne programvaren
**Merknad om Ragtime**: `ragtime/`-katalogen er lisensiert separat under **PolyForm Noncommercial License 1.0.0**. Se [ragtime/LICENSE](ragtime/LICENSE) for detaljer.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Støtte
- **Dokumentasjon**: [docs/](docs/)
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -272,25 +272,21 @@ Zobacz [Przewodnik Rozwoju](https://docs.claude-mem.ai/development) dla przepły
---
## Licencja
## License
Ten projekt jest licencjonowany na podstawie **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Wszelkie prawa zastrzeżone.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Zobacz plik [LICENSE](LICENSE) dla pełnych szczegółów.
See the [LICENSE](LICENSE) file for full details.
**Co To Oznacza:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Możesz używać, modyfikować i dystrybuować to oprogramowanie swobodnie
- Jeśli zmodyfikujesz i wdrożysz na serwerze sieciowym, musisz udostępnić swój kod źródłowy
- Dzieła pochodne muszą być również licencjonowane na podstawie AGPL-3.0
- Nie ma GWARANCJI dla tego oprogramowania
**Uwaga o Ragtime**: Katalog `ragtime/` jest licencjonowany osobno na podstawie **PolyForm Noncommercial License 1.0.0**. Zobacz [ragtime/LICENSE](ragtime/LICENSE) dla szczegółów.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Wsparcie
- **Dokumentacja**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Veja [Guia de Desenvolvimento](https://docs.claude-mem.ai/development) para o fl
---
## Licença
## License
Este projeto está licenciado sob a **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Todos os direitos reservados.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Veja o arquivo [LICENSE](LICENSE) para detalhes completos.
See the [LICENSE](LICENSE) file for full details.
**O Que Isso Significa:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Você pode usar, modificar e distribuir este software livremente
- Se você modificar e implantar em um servidor de rede, você deve disponibilizar seu código-fonte
- Trabalhos derivados também devem ser licenciados sob AGPL-3.0
- NÃO HÁ GARANTIA para este software
**Nota sobre Ragtime**: O diretório `ragtime/` é licenciado separadamente sob a **PolyForm Noncommercial License 1.0.0**. Veja [ragtime/LICENSE](ragtime/LICENSE) para detalhes.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Suporte
- **Documentação**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Consultați [Ghidul de Dezvoltare](https://docs.claude-mem.ai/development) pentr
---
## Licență
## License
Acest proiect este licențiat sub **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Toate drepturile rezervate.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Consultați fișierul [LICENSE](LICENSE) pentru detalii complete.
See the [LICENSE](LICENSE) file for full details.
**Ce Înseamnă Asta:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Puteți folosi, modifica și distribui acest software liber
- Dacă modificați și implementați pe un server de rețea, trebuie să faceți disponibil codul sursă
- Lucrările derivate trebuie să fie licențiate și ele sub AGPL-3.0
- NU EXISTĂ NICIO GARANȚIE pentru acest software
**Notă despre Ragtime**: Directorul `ragtime/` este licențiat separat sub **PolyForm Noncommercial License 1.0.0**. Consultați [ragtime/LICENSE](ragtime/LICENSE) pentru detalii.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Suport
- **Documentație**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## Лицензия
## License
Этот проект лицензирован под **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Все права защищены.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Полные сведения см. в файле [LICENSE](LICENSE).
See the [LICENSE](LICENSE) file for full details.
**Что это означает:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Вы можете свободно использовать, модифицировать и распространять это программное обеспечение
- Если вы модифицируете и развертываете на сетевом сервере, вы должны сделать свой исходный код доступным
- Производные работы также должны быть лицензированы под AGPL-3.0
- Для этого программного обеспечения НЕТ ГАРАНТИЙ
**Примечание о Ragtime**: Директория `ragtime/` лицензирована отдельно под **PolyForm Noncommercial License 1.0.0**. Подробности см. в [ragtime/LICENSE](ragtime/LICENSE).
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Поддержка
- **Документация**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Se [Utvecklingsguide](https://docs.claude-mem.ai/development) för bidragsarbets
---
## Licens
## License
Detta projekt är licensierat under **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Alla rättigheter förbehållna.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Se [LICENSE](LICENSE)-filen för fullständiga detaljer.
See the [LICENSE](LICENSE) file for full details.
**Vad detta betyder:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Du kan använda, modifiera och distribuera denna programvara fritt
- Om du modifierar och distribuerar på en nätverksserver måste du göra din källkod tillgänglig
- Härledda verk måste också licensieras under AGPL-3.0
- Det finns INGEN GARANTI för denna programvara
**Notering om Ragtime**: Katalogen `ragtime/` är licensierad separat under **PolyForm Noncommercial License 1.0.0**. Se [ragtime/LICENSE](ragtime/LICENSE) för detaljer.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Support
- **Dokumentation**: [docs/](docs/)
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -272,25 +272,21 @@ npm run bug-report
---
## ใบอนุญาต
## License
โปรเจกต์นี้ได้รับอนุญาตภายใต้ **GNU Affero General Public License v3.0** (AGPL-3.0)
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack) สงวนลิขสิทธิ์ทั้งหมด
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
ดูไฟล์ [LICENSE](LICENSE) สำหรับรายละเอียดทั้งหมด
See the [LICENSE](LICENSE) file for full details.
**ความหมาย:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- คุณสามารถใช้ ดัดแปลง และแจกจ่ายซอฟต์แวร์นี้ได้อย่างอิสระ
- หากคุณดัดแปลงและปรับใช้บนเซิร์ฟเวอร์เครือข่าย คุณต้องทำให้ซอร์สโค้ดของคุณพร้อมใช้งาน
- งานที่เป็นอนุพันธ์ต้องได้รับอนุญาตภายใต้ AGPL-3.0 ด้วย
- ไม่มีการรับประกันสำหรับซอฟต์แวร์นี้
**หมายเหตุเกี่ยวกับ Ragtime**: ไดเรกทอรี `ragtime/` ได้รับอนุญาตแยกต่างหากภายใต้ **PolyForm Noncommercial License 1.0.0** ดู [ragtime/LICENSE](ragtime/LICENSE) สำหรับรายละเอียด
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## การสนับสนุน
- **เอกสาร**: [docs/](docs/)
+8 -12
View File
@@ -51,7 +51,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -297,25 +297,21 @@ Tingnan ang [Gabay nang pagbuo](https://docs.claude-mem.ai/development) para sa
---
## Lisensya
## License
Ang proyektong ito ay licensed sa ilalim ng **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Tingnan ang [LICENSE](LICENSE) file para sa buong detalye.
See the [LICENSE](LICENSE) file for full details.
**Ano ang ibig sabihin nito:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Maaari mong gamitin, baguhin, at ipamahagi ang software na ito nang libre
- Kung babaguhin mo at i-deploy sa isang network server, kailangan mong gawing available ang iyong source code
- Dapat ding naka-license sa AGPL-3.0 ang mga derivative works
- WALANG WARRANTY para sa software na ito
**Tala tungkol sa Ragtime**: Ang `ragtime/` directory ay may hiwalay na lisensya sa ilalim ng **PolyForm Noncommercial License 1.0.0**. Tingnan ang [ragtime/LICENSE](ragtime/LICENSE) para sa detalye.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Suporta
- **Dokumentasyon**: [docs/](docs/)
@@ -325,4 +321,4 @@ Tingnan ang [LICENSE](LICENSE) file para sa buong detalye.
---
**Built with Claude Agent SDK** | **Powered by Claude Code** | **Made with TypeScript**
**Built with Claude Agent SDK** | **Works with Claude Code** | **Made with TypeScript**
+8 -12
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -272,25 +272,21 @@ Katkı iş akışı için [Geliştirme Kılavuzu](https://docs.claude-mem.ai/dev
---
## Lisans
## License
Bu proje **GNU Affero General Public License v3.0** (AGPL-3.0) altında lisanslanmıştır.
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Telif Hakkı (C) 2025 Alex Newman (@thedotmack). Tüm hakları saklıdır.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Tam detaylar için [LICENSE](LICENSE) dosyasına bakın.
See the [LICENSE](LICENSE) file for full details.
**Bu Ne Anlama Gelir:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Bu yazılımı özgürce kullanabilir, değiştirebilir ve dağıtabilirsiniz
- Değiştirip bir ağ sunucusunda dağıtırsanız, kaynak kodunuzu kullanılabilir hale getirmelisiniz
- Türev çalışmalar da AGPL-3.0 altında lisanslanmalıdır
- Bu yazılım için HİÇBİR GARANTİ yoktur
**Ragtime Hakkında Not**: `ragtime/` dizini ayrı olarak **PolyForm Noncommercial License 1.0.0** altında lisanslanmıştır. Detaylar için [ragtime/LICENSE](ragtime/LICENSE) dosyasına bakın.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Destek
- **Dokümantasyon**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## Ліцензія
## License
Цей проєкт ліцензовано під **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Авторське право (C) 2025 Alex Newman (@thedotmack). Всі права захищені.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Дивіться файл [LICENSE](LICENSE) для повних деталей.
See the [LICENSE](LICENSE) file for full details.
**Що це означає:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Ви можете використовувати, модифікувати та поширювати це програмне забезпечення вільно
- Якщо ви модифікуєте та розгортаєте на мережевому сервері, ви повинні зробити свій вихідний код доступним
- Похідні роботи також повинні бути ліцензовані під AGPL-3.0
- Для цього програмного забезпечення НЕМАЄ ГАРАНТІЇ
**Примітка про Ragtime**: Каталог `ragtime/` ліцензовано окремо під **PolyForm Noncommercial License 1.0.0**. Дивіться [ragtime/LICENSE](ragtime/LICENSE) для деталей.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Підтримка
- **Документація**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -278,25 +278,21 @@ npm run bug-report
---
## لائسنس
## License
یہ منصوبہ **GNU Affero General Public License v3.0** (AGPL-3.0) کے تحت لائسنس ہے۔
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack)۔ تمام حقوق محفوظ ہیں۔
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
مکمل تفصیلات کے لیے [LICENSE](LICENSE) فائل دیکھیں۔
See the [LICENSE](LICENSE) file for full details.
**اس کا مطلب کیا ہے:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- آپ اس سافٹ ویئر کو آزادی سے استعمال، تبدیل اور تقسیم کر سکتے ہیں
- اگر آپ اسے تبدیل کریں اور نیٹ ورک سرور میں نشر کریں تو آپ کو اپنا سورس کوڈ دستیاب کرنا ہوگا
- ماخوذ کام بھی AGPL-3.0 کے تحت لائسنس ہونے چاہیں
- اس سافٹ ویئر کے لیے کوئی وارنٹی نہیں
**Ragtime کے بارے میں نوٹ**: `ragtime/` ڈائریکٹری الگ سے **PolyForm Noncommercial License 1.0.0** کے تحت لائسنس ہے۔ تفصیلات کے لیے [ragtime/LICENSE](ragtime/LICENSE) دیکھیں۔
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## معاونت
- **دستاویزات**: [docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ Xem [Hướng Dẫn Phát Triển](https://docs.claude-mem.ai/development) để
---
## Giấy Phép
## License
Dự án này được cấp phép theo **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Bảo lưu mọi quyền.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Xem tệp [LICENSE](LICENSE) để biết chi tiết đầy đủ.
See the [LICENSE](LICENSE) file for full details.
**Điều Này Có Nghĩa Là:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Bạn có thể sử dụng, sửa đổi và phân phối phần mềm này tự do
- Nếu bạn sửa đổi và triển khai trên máy chủ mạng, bạn phải cung cấp mã nguồn của mình
- Các tác phẩm phái sinh cũng phải được cấp phép theo AGPL-3.0
- KHÔNG CÓ BẢO HÀNH cho phần mềm này
**Lưu Ý Về Ragtime**: Thư mục `ragtime/` được cấp phép riêng theo **PolyForm Noncommercial License 1.0.0**. Xem [ragtime/LICENSE](ragtime/LICENSE) để biết chi tiết.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Hỗ Trợ
- **Tài Liệu**: [docs/](docs/)
+7 -11
View File
@@ -49,7 +49,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -278,25 +278,21 @@ npm run bug-report
---
## 授權條款
## License
本專案採用 **GNU Affero 通用公共授權條款 v3.0**AGPL-3.0)授權。
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
完整詳情請參閱 [LICENSE](LICENSE) 檔案。
See the [LICENSE](LICENSE) file for full details.
**這代表什麼:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- 您可以自由使用、修改與散佈此軟體
- 如果您修改並部署於網路伺服器上,您必須公開您的原始碼
- 衍生作品也必須採用 AGPL-3.0 授權
- 本軟體不提供任何擔保
**關於 Ragtime 的說明**`ragtime/` 目錄採用 **PolyForm Noncommercial License 1.0.0** 另行授權。詳情請參閱 [ragtime/LICENSE](ragtime/LICENSE)。
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## 支援
- **文件**[docs/](docs/)
+8 -12
View File
@@ -50,7 +50,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -273,25 +273,21 @@ npm run bug-report
---
## 许可证
## License
本项目采用 **GNU Affero General Public License v3.0** (AGPL-3.0) 许可。
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack)。保留所有权利。
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
详见 [LICENSE](LICENSE) 文件了解完整详情。
See the [LICENSE](LICENSE) file for full details.
**这意味着什么:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- 您可以自由使用、修改和分发本软件
- 如果您修改并部署到网络服务器上,必须公开您的源代码
- 衍生作品也必须采用 AGPL-3.0 许可
- 本软件不提供任何保证
**关于 Ragtime 的说明**: `ragtime/` 目录单独采用 **PolyForm Noncommercial License 1.0.0** 许可。详见 [ragtime/LICENSE](ragtime/LICENSE)。
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## 支持
- **文档**: [docs/](docs/)
+8 -12
View File
@@ -51,7 +51,7 @@
<p align="center">
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-AGPL%203.0-blue.svg" alt="License">
<img src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" alt="License">
</a>
<a href="package.json">
<img src="https://img.shields.io/badge/version-6.5.0-green.svg" alt="Version">
@@ -274,25 +274,21 @@ Veja [Guia de Desenvolvimento](https://docs.claude-mem.ai/development) para o fl
---
## Licença
## License
Este projeto está licenciado sob a **GNU Affero General Public License v3.0** (AGPL-3.0).
This project is licensed under the **Apache License 2.0** (Apache-2.0).
Copyright (C) 2025 Alex Newman (@thedotmack). Todos os direitos reservados.
Copyright (C) 2025 Alex Newman (@thedotmack). All rights reserved.
Veja o arquivo [LICENSE](LICENSE) para detalhes completos.
See the [LICENSE](LICENSE) file for full details.
**O Que Isso Significa:**
Apache-2.0 allows broad use, modification, distribution, and commercial use, subject to its terms.
- Você pode usar, modificar e distribuir este software livremente
- Se você modificar e implantar em um servidor de rede, você deve disponibilizar seu código-fonte
- Trabalhos derivados também devem ser licenciados sob AGPL-3.0
- NÃO HÁ GARANTIA para este software
**Nota sobre Ragtime**: O diretório `ragtime/` é licenciado separadamente sob a **PolyForm Noncommercial License 1.0.0**. Veja [ragtime/LICENSE](ragtime/LICENSE) para detalhes.
**Ragtime note**: The ragtime/ directory is licensed under the **Apache License 2.0**. See [ragtime/LICENSE](ragtime/LICENSE) for details.
---
## Suporte
- **Documentação**: [docs/](docs/)
+45
View File
@@ -0,0 +1,45 @@
# IP Boundary
Claude-Mem uses an open-core structure.
## Apache-2.0 components
- Core memory engine
- Claude-Mem Server
- CLI
- SDKs
- REST API schemas
- MCP tools/resources/prompts
- Claude Code adapter
- Generic agent adapters
- Storage adapters
- Reference knowledge agents
- Tests
- Examples
- Public documentation
## Reserved commercial/private areas
These areas are not shipped by Claude-Mem Server v0.1 and should remain outside
the Apache-2.0 public implementation unless maintainers explicitly open-source
them later.
- Magic Recall hosted cloud
- Team/org memory sync
- Admin dashboard
- SSO/SAML/SCIM
- Enterprise RBAC
- Enterprise audit log UI
- DLP/policy engine
- Premium knowledge agents
- Managed evals
- Customer deployment tooling
- Enterprise observability
- Support/SLA workflows
- Internal eval datasets
- Private customer connectors
## Rule
Do not put commercial/private implementation code into the Apache-2.0 public repo
unless the maintainers intentionally decide to open-source it.
+29
View File
@@ -0,0 +1,29 @@
# License
Claude-Mem is licensed under the Apache License 2.0.
The Apache-2.0 license applies to the open-source core, including the memory
engine, Claude-Mem Server, CLI, SDKs, adapters, MCP tools, schemas, tests,
examples, and public documentation.
Apache-2.0 allows broad use, modification, distribution, and commercial use,
subject to the license terms.
## Why Apache-2.0?
Claude-Mem is intended to be embedded broadly inside developer tools, local
agents, MCP clients, enterprise systems, robotics stacks, and production agent
harnesses. Apache-2.0 supports that goal while preserving attribution and
including explicit patent license terms.
## Reserved commercial/private areas
Claude-Mem Server v0.1 does not ship hosted cloud, team sync, enterprise
features, premium knowledge agents, private evals, or customer deployment
tooling. Those areas are reserved outside the Apache-2.0 public implementation
unless maintainers explicitly open-source them later.
## Third-party marks
Apache-2.0 licenses code. It does not grant rights to third-party trademarks or
brand names.
+13
View File
@@ -0,0 +1,13 @@
# Worker To Server Migration
Claude-Mem 13 keeps the worker path in place. Server beta is an additional runtime option for teams, deployable containers, API keys, and BullMQ/Valkey queues.
Compatibility commands remain available:
```sh
claude-mem start
claude-mem worker start
claude-mem server start
```
The server storage boundary reads legacy worker data while adding server-owned projects, sessions, agent events, memory items, teams, API keys, and audit logs. Migrate adapters gradually by writing to `/v1/events` and `/v1/memories`; keep existing `/api/*` hook routes enabled until all clients move.
+1 -1
View File
@@ -127,7 +127,7 @@
"header": "Legal",
"items": [
{
"label": "License (AGPL-3.0)",
"label": "License (Apache-2.0)",
"href": "https://github.com/thedotmack/claude-mem/blob/main/LICENSE"
}
]
+7
View File
@@ -0,0 +1,7 @@
# Security
Server beta defaults to API-key auth. `CLAUDE_MEM_AUTH_MODE=local-dev` only enables the loopback development bypass when `CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS=1` is also set; do not use it behind a reverse proxy or on a publicly reachable bind address.
API keys are generated with the `cmem_` prefix and displayed once. Claude-Mem stores only a SHA-256 hash, prefix metadata, scopes, status, and timestamps in SQLite.
BullMQ mode requires Redis or Valkey. Queue payloads are limited to work needed to resume observation processing; SQLite remains the canonical memory store. Use Redis persistence for deployable examples and avoid placing server ports on public networks without auth.
+52
View File
@@ -0,0 +1,52 @@
# Server Storage Boundary
Phase 4 adds the contracts and SQLite tables for the future server-owned storage model. It is additive only: worker routes, providers, existing search, and legacy observation writes still use the current `sdk_sessions`, `observations`, `session_summaries`, `user_prompts`, and `pending_messages` tables.
## Tables
Server-owned tables are created by `ensureServerStorageSchema()` in `src/storage/sqlite/schema.ts`:
- `projects`
- `server_sessions`
- `agent_events`
- `memory_items`
- `memory_sources`
- `teams`
- `team_members`
- `api_keys`
- `audit_log`
`MigrationRunner` records these tables as schema version 33. Repositories also call the same helper so future server bootstrap code can use the storage boundary without depending on worker initialization.
## Contracts
Shared Zod contracts live under `src/core/schemas/`. Repository methods parse inputs and outputs through these schemas and store structured fields as JSON `TEXT`, matching the existing Bun SQLite style.
## Observation To Memory Translation
The translation layer is intentionally documented but not wired into existing search in this phase.
Decision: legacy `observations` remain the source of truth until a later migration explicitly backfills and switches readers. A future translator should create one `memory_items` row per legacy `observations` row with:
- `memory_items.kind = 'observation'`
- `memory_items.type = observations.type`
- `memory_items.project_id` resolved from the canonical `projects` row for `observations.project`
- `memory_items.server_session_id` resolved through `server_sessions.memory_session_id = observations.memory_session_id`
- `memory_items.legacy_observation_id = observations.id`
- `title`, `subtitle`, `text`, `narrative`, `facts`, `concepts`, `files_read`, and `files_modified` copied from the legacy row
- one `memory_sources` row with `source_type = 'observation'`, `legacy_table = 'observations'`, and `legacy_id = observations.id`
The schema enforces this as an idempotent backfill target with partial unique
indexes on `memory_items.legacy_observation_id` and
`memory_sources(source_type, legacy_table, legacy_id)` when legacy source IDs are
present.
Until that backfill exists, new repositories may write `memory_items` directly for server-owned workflows, but no worker path should read from `memory_items` as a replacement for `observations`.
Rows that reference `server_sessions` must stay inside the same `project_id`.
SQLite triggers reject cross-project `agent_events` and `memory_items` links so
project-scoped reads cannot accidentally mix memories from another project.
## Auth Placeholder
`api_keys` is a local placeholder for future Better Auth integration. This phase stores hashes, prefixes, scopes, and status locally; it does not introduce a Better Auth runtime dependency or middleware wiring.
+13
View File
@@ -0,0 +1,13 @@
# Claude-Mem Server
Claude-Mem Server is the beta server runtime for Claude-Mem 13. The existing worker remains available for compatibility; server beta adds API-key auth, team/project-aware storage contracts, REST V1 routes, and an optional BullMQ queue backend.
Local development can use SQLite queues and the explicit `CLAUDE_MEM_AUTH_MODE=local-dev` plus `CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS=1` loopback bypass. Deployable mode should use:
```sh
CLAUDE_MEM_QUEUE_ENGINE=bullmq
CLAUDE_MEM_REDIS_URL=redis://127.0.0.1:6379
CLAUDE_MEM_AUTH_MODE=api-key
```
Use `claude-mem server api-key create --scope memories:read,memories:write` to create a bearer key. The raw key is shown once; only a SHA-256 hash is stored.
+2 -1
View File
@@ -1,9 +1,10 @@
{
"id": "claude-mem",
"name": "Claude-Mem (Persistent Memory)",
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
"description": "OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
"kind": "memory",
"version": "12.7.5",
"license": "Apache-2.0",
"author": "thedotmack",
"homepage": "https://claude-mem.com",
"skills": ["skills/make-plan", "skills/do"],
+1
View File
@@ -2,6 +2,7 @@
"name": "@openclaw/claude-mem",
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
+11 -3
View File
@@ -16,7 +16,7 @@
"nodejs"
],
"author": "Alex Newman",
"license": "AGPL-3.0",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/thedotmack/claude-mem.git"
@@ -66,7 +66,7 @@
"scripts": {
"dev": "npm run build-and-sync",
"build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js",
"build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && (cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart) && npm run queue:clear",
"build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && (cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart)",
"sync-marketplace": "node scripts/sync-marketplace.cjs",
"sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
"build:binaries": "node scripts/build-worker-binary.js",
@@ -81,7 +81,7 @@
"worker:status": "bun plugin/scripts/worker-service.cjs status",
"queue": "bun scripts/check-pending-queue.ts",
"queue:process": "bun scripts/check-pending-queue.ts --process",
"queue:clear": "bun scripts/clear-failed-queue.ts --all --force",
"queue:clear:pending": "bun scripts/clear-pending-queue.ts --all --force",
"pr:status": "bun scripts/pr-babysit-status.ts",
"claude-md:regenerate": "bun scripts/regenerate-claude-md.ts",
"claude-md:dry-run": "bun scripts/regenerate-claude-md.ts --dry-run",
@@ -109,6 +109,7 @@
"test:context": "bun test tests/context/",
"test:infra": "bun test tests/infrastructure/",
"test:server": "bun test tests/server/",
"e2e:server-beta:docker": "bash scripts/e2e-server-beta-docker.sh",
"prepublishOnly": "npm run build",
"release": "np",
"release:patch": "np patch --no-cleanup",
@@ -123,13 +124,19 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
"@better-auth/api-key": "^1.6.9",
"@clack/prompts": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"ansi-to-html": "^0.7.2",
"better-auth": "^1.6.9",
"bullmq": "^5.76.6",
"cors": "^2.8.6",
"dompurify": "^3.4.1",
"express": "^5.2.1",
"glob": "^13.0.6",
"handlebars": "^4.7.9",
"ioredis": "^5.10.1",
"pg": "^8.20.0",
"picocolors": "^1.1.1",
"react": "^19.2.5",
"react-dom": "^19.2.5",
@@ -150,6 +157,7 @@
"@types/dompurify": "^3.2.0",
"@types/express": "^5.0.6",
"@types/node": "^25.6.0",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"esbuild": "^0.28.0",
@@ -0,0 +1,249 @@
# Observation Queue Engine Deep Dive: BullMQ vs Bee-Queue
Date: 2026-05-06
## Executive decision
If claude-mem replaces its observation queue with one of the two Redis-backed libraries, choose **BullMQ**, not Bee-Queue.
That said, the current observation queue is not a generic background job queue. It is a durable, per-session input stream feeding long-lived provider generators. Replacing it with Redis should not be the default local install path unless claude-mem is willing to require, bundle, or supervise Redis. If Redis is not acceptable as a new operational dependency, the better path is to keep the SQLite queue and fix the contract/test drift.
Recommended path:
1. Stabilize the current queue contract and tests.
2. Add a queue-engine adapter boundary.
3. Keep SQLite as the default backend.
4. Add BullMQ as an optional backend for users who explicitly configure Redis.
5. Do not adopt Bee-Queue.
## Current claude-mem queue shape
The active queue path is:
- `src/services/worker/http/shared.ts` and `SessionRoutes.ts` ingest observations/summarize requests.
- `SessionManager.queueObservation()` and `queueSummarize()` persist rows through `PendingMessageStore.enqueue()`.
- `SessionQueueProcessor.createIterator()` claims one row at a time and wakes via a per-session `EventEmitter`.
- Provider loops in `ClaudeProvider`, `GeminiProvider`, and `OpenRouterProvider` consume `sessionManager.getMessageIterator(sessionDbId)`.
- Parsed agent output is stored through `processAgentResponse()`, then `SessionManager.clearPendingForSession()` clears that session's pending rows.
Key semantics that must survive any replacement:
- Per-session FIFO ordering.
- At-most-one active consumer per session.
- Durable queue across worker restarts.
- Startup recovery from `processing` back to `pending`.
- Low-latency wakeup when new tool observations arrive.
- Deduplication by `content_session_id + tool_use_id`.
- Original observation timestamp preservation for storage/broadcast.
- Queue depth for `/api/processing-status` and SSE.
- Local-first behavior and simple install are product requirements, not just implementation details.
Important mismatch found during the dive:
- Current `PendingMessageStore` only models `pending` and `processing`.
- Older migrations, tests, and scripts still reference `processed`, `failed`, `retry_count`, `completed_at_epoch`, `failed_at_epoch`, and `worker_pid`.
- `storeObservationsAndMarkComplete()` still updates a row to `processed`, while the currently visible queue path clears all pending messages for the session after parsing.
- `src/services/sqlite/schema.sql` still creates `idx_pending_messages_worker_pid` even though the visible table definition has no `worker_pid`.
Focused test run:
```sh
bun test tests/services/sqlite/PendingMessageStore.test.ts tests/services/queue/SessionQueueProcessor.test.ts
```
Result: 10 pass, 6 fail. Failures show stale tests/contract drift:
- `PendingMessageStore.test.ts` passes `3` as constructor arg, but constructor now expects `onMutate?: () => void`.
- `SessionQueueProcessor.test.ts` expects retry-after-store-error behavior, but current implementation logs and exits the iterator on claim failure.
This needs to be reconciled before swapping engines; otherwise the migration will encode inconsistent behavior.
## BullMQ deep dive
Sources checked:
- GitHub: https://github.com/taskforcesh/bullmq
- NPM: https://www.npmjs.com/package/bullmq
- Docs: https://docs.bullmq.io/
- Queues: https://docs.bullmq.io/guide/queues
- Connections/Redis constraints: https://docs.bullmq.io/guide/connections
- Production notes: https://docs.bullmq.io/guide/going-to-production
- Manual processing: https://docs.bullmq.io/patterns/manually-fetching-jobs
- Job IDs/dedupe: https://docs.bullmq.io/guide/jobs/job-ids
- Stalled jobs: https://docs.bullmq.io/guide/workers/stalled-jobs
Current package/repo facts captured on 2026-05-06:
- NPM latest: `bullmq@5.76.5`.
- NPM modified: 2026-05-02.
- GitHub pushed: 2026-05-05.
- GitHub stars/forks/open issues at capture time: 8808 / 606 / 414.
- License: MIT.
- Unpacked size: about 2.5 MB.
- Dependencies: `ioredis`, `cron-parser`, `msgpackr`, `node-abort-controller`, `semver`, `tslib`.
- TypeScript types are bundled.
- A Bun import smoke test succeeded for `import { Queue } from 'bullmq'`.
Strengths for claude-mem:
- Actively maintained and widely used.
- Built-in TypeScript API.
- Redis-backed durability and distributed workers.
- Built-in stalled-job recovery, retry attempts, fixed/exponential backoff, delays, priorities, FIFO/LIFO, auto-removal, QueueEvents, manual processing APIs, and job ID based dedupe.
- BullMQ docs explicitly support manual job fetching with `Worker#getNextJob()`, `moveToCompleted()`, `moveToFailed()`, and lock extension. This matters because claude-mem's provider loop is closer to a stream consumer than a normal job processor.
Costs and risks:
- Redis becomes required for the queue backend. BullMQ docs require a Redis connection to use queues and recommend Redis compatibility 6.2+.
- Redis must be configured like durable infrastructure, not cache: AOF persistence and `maxmemory-policy=noeviction` are recommended/required for correctness.
- Connection count increases. BullMQ docs note each class consumes at least one Redis connection; `Worker` and `QueueEvents` need blocking/duplicated connections in some cases.
- Jobs store data in Redis in clear text unless claude-mem encrypts or avoids sensitive payload fields. Tool input/output can be sensitive.
- BullMQ job completion/failure semantics do not map directly to claude-mem's current "provider consumes many messages, parses one response, then clears the session" behavior.
- Per-session FIFO with parallel sessions is not free in OSS BullMQ. A single global queue with worker concurrency > 1 can violate same-session ordering unless we add a scheduler. BullMQ Pro groups would address this, but claude-mem should not depend on Pro.
- Custom `jobId` is useful for `tool_use_id` dedupe, but BullMQ custom job IDs must not contain `:`. Use a hash or safe delimiter.
- Manual processing requires lock management. BullMQ docs call out that manually fetched jobs do not get automatic lock renewal like standard processors; claude-mem would need `extendLock()` for long provider calls or a large lock duration.
Best BullMQ shape if adopted:
- Prefer **one queue per active session** over one global queue initially:
- Queue name: `claude-mem:session:<safe-session-db-id>` or a hashed content-session suffix.
- Worker/manual consumer concurrency: `1`.
- Preserves per-session FIFO without BullMQ Pro groups.
- Active session counts are naturally low for local claude-mem usage.
- Cleanup queue keys when a session is deleted or after idle timeout.
- Use `jobId` for observation dedupe:
- `obs_<sha256(contentSessionId + "\0" + toolUseId)>`.
- Summaries should use a distinct id scheme and usually should not dedupe unless the current summarize semantics require it.
- Use `removeOnComplete` aggressively if SQLite remains the source of truth for stored observations.
- Keep only bounded failed jobs for debugging.
- Treat Redis as queue state only; SQLite remains the canonical observation/session store.
- Add config:
- `CLAUDE_MEM_QUEUE_ENGINE=sqlite|bullmq`
- `CLAUDE_MEM_REDIS_URL`
- `CLAUDE_MEM_QUEUE_REDIS_PREFIX`
- `CLAUDE_MEM_QUEUE_ENCRYPT_PAYLOADS=true|false` if sensitive fields are stored.
## Bee-Queue deep dive
Sources checked:
- GitHub: https://github.com/bee-queue/bee-queue
- NPM: https://www.npmjs.com/package/bee-queue
- README/API docs in repository.
Current package/repo facts captured on 2026-05-06:
- NPM latest: `bee-queue@2.0.0`.
- NPM modified: 2025-12-08.
- GitHub pushed: 2026-04-10.
- GitHub stars/forks/open issues at capture time: 4027 / 221 / 47.
- License field from NPM: MIT. GitHub API license metadata returned `NOASSERTION`.
- Unpacked size: about 107 KB.
- Dependencies: `redis@^3.1.2`, `p-finally`, `promise-callbacks`.
- NPM package exposes `./index.d.ts`.
- A Bun import smoke test succeeded for `import BeeQueue from 'bee-queue'`.
Strengths for claude-mem:
- Very small and simple.
- Designed for short, real-time jobs.
- Redis-backed with Lua/pipelining and low overhead.
- Supports concurrency, retries, retry strategies, timeouts, scheduled jobs, pub/sub events, results to producers, and stalled job retry.
- Redis requirement is lighter in docs: Redis 2.8+, with Redis 3.2+ recommended for delayed jobs.
Costs and risks:
- Narrower feature set by design. The README says priorities and repeatable jobs are not currently supported.
- CommonJS-first API; workable, but less idiomatic for this ESM TypeScript codebase.
- Uses the older `redis` v3 client line, not modern `redis` v4/v5 or `ioredis`.
- Observability and operational tooling are thinner than BullMQ.
- Same per-session ordering mismatch exists as BullMQ, but with fewer escape hatches.
- Delayed retry behavior requires `activateDelayedJobs` on at least one queue instance.
- The package is newly revived, but not as active/mature as BullMQ for a queue-engine foundation.
Conclusion: Bee-Queue is attractive if the only goal is "small Redis queue for short jobs." claude-mem needs a durable session stream with strict per-session semantics, good TypeScript ergonomics, explicit recovery behavior, and long-term maintenance. Bee-Queue is the wrong tradeoff.
## Scorecard
| Area | Current SQLite | BullMQ | Bee-Queue |
| --- | --- | --- | --- |
| Local-first install | Strong | Weak unless Redis is bundled/optional | Weak unless Redis is bundled/optional |
| Per-session FIFO | Strong | Medium with per-session queues; weak with one global queue | Medium with per-session queues; weak with one global queue |
| Restart durability | Strong, SQLite-backed | Strong if Redis persistence configured | Strong if Redis persistence configured |
| Stalled recovery | Custom/simple | Strong built-in | Built-in |
| TypeScript fit | Strong | Strong | Medium |
| Maintenance/activity | Internal | Strong | Medium |
| Operational complexity | Low | High | Medium-high |
| Queue observability | Custom/basic | Strong | Medium |
| Dependency footprint | Low | Larger | Small |
| Privacy/data locality | SQLite local file | Redis clear-text unless handled | Redis clear-text unless handled |
| Best use in claude-mem | Default | Optional advanced backend | Do not use |
## Migration plan
Phase 0: Fix the existing contract
- Decide whether `pending_messages.status` is only `pending|processing`, or whether `processed|failed` is coming back.
- Fix `schema.sql` and migrations so `worker_pid` indexes are not created after `worker_pid` is dropped.
- Fix `storeObservationsAndMarkComplete()` or remove it if no longer used.
- Update queue tests to match real behavior:
- constructor signature;
- claim error behavior;
- reset-on-start behavior;
- dedupe by `tool_use_id`;
- clear-session behavior.
Phase 1: Add an adapter boundary
Define a small interface around current behavior, not around BullMQ:
```ts
interface ObservationQueueEngine {
enqueue(sessionDbId: number, contentSessionId: string, message: PendingMessage): Promise<EnqueueResult>;
createIterator(sessionDbId: number, signal: AbortSignal, onIdleTimeout?: () => void): AsyncIterableIterator<PendingMessageWithId>;
clearPendingForSession(sessionDbId: number): Promise<number>;
resetProcessingToPending(sessionDbId: number): Promise<number>;
getPendingCount(sessionDbId: number): Promise<number>;
getTotalQueueDepth(): Promise<number>;
close(): Promise<void>;
}
```
Keep `SqliteObservationQueueEngine` as the first implementation by moving the current `PendingMessageStore + SessionQueueProcessor` behavior behind this interface.
Phase 2: Add BullMQ backend behind feature flag
- Add `BullMqObservationQueueEngine`.
- Use per-session queues with concurrency/manual fetch of 1.
- Use safe hashed `jobId` for observation dedupe.
- Preserve `_originalTimestamp` in job data.
- Keep provider loops unchanged by preserving the async iterator interface.
- Implement lock extension if manual processing can exceed the configured lock duration.
- Keep SQLite as the observation/session truth; Redis is transport.
- Add Redis connectivity health to `/api/health` only when BullMQ backend is enabled.
Phase 3: Migration and fallback
- On startup with BullMQ enabled, migrate existing SQLite `pending_messages` rows into BullMQ once, then mark/delete migrated rows.
- If Redis is unavailable at startup, fail loudly for `CLAUDE_MEM_QUEUE_ENGINE=bullmq`; do not silently drop observations.
- For default `sqlite`, do not require Redis.
Phase 4: Tests
- Unit-test the adapter contract with a shared test suite.
- Run the suite against SQLite always.
- Run BullMQ tests only when Redis is available, or spin Redis in CI.
- Add crash/restart tests:
- enqueue, kill worker, restart, process;
- claimed job stalls and returns;
- duplicate `tool_use_id` is suppressed;
- per-session FIFO across concurrent sessions;
- idle timeout still aborts provider subprocesses.
## Final recommendation
Do not do a direct swap from SQLite to either library.
If the product goal is to keep claude-mem easy to install and local-first, invest in the current SQLite queue: clean up the schema/status drift, restore tests, add explicit retries/failure rows if needed, and keep the in-process wakeup path.
If the product goal is to support distributed workers or stronger queue observability, add **BullMQ as an optional backend** through an adapter. It has the right maintenance profile, TypeScript support, recovery primitives, and docs. Bee-Queue is too narrow and too legacy-client-oriented for this role.
@@ -0,0 +1,237 @@
# Redis-Compatible Dependency Strategy for Claude-Mem
Date: 2026-05-06
## Recommendation
Make BullMQ the queue engine, but do **not** treat Redis like a user-managed global service. Treat it like part of claude-mem's runtime.
Best fit for the "auto-install / it just works" product energy:
1. Prefer a claude-mem-owned local Redis-compatible sidecar process.
2. Prefer **Valkey** as the bundled/default local server where practical.
3. Accept an existing Redis/Valkey/Dragonfly URL when the user already has one.
4. Use package managers only as installers for the sidecar binary, not as long-running service managers.
5. Keep Windows as Docker/external-URL first unless we choose a supported native Redis-compatible build.
In settings and docs, call the capability `redis-compatible queue store`, but keep env names familiar:
```sh
CLAUDE_MEM_QUEUE_ENGINE=bullmq
CLAUDE_MEM_REDIS_MODE=managed|external|docker
CLAUDE_MEM_REDIS_URL=redis://127.0.0.1:<allocated-port>
```
## Why Valkey-first for managed local mode
Valkey is a Redis-compatible fork under the Linux Foundation ecosystem, has current releases, Homebrew/package-manager install paths, Docker images, and Linux binary artifacts. It also gives claude-mem a cleaner dependency story for a managed local queue store.
Redis itself is still viable. Redis Open Source 8 has changed licensing over time, while Valkey keeps the local managed dependency straightforward for "we run a Redis-compatible queue store locally."
BullMQ's own docs say BullMQ is Redis-compliant with Redis 6.2+ but warns that not all Redis alternatives work properly. So this needs CI coverage. Dragonfly is officially called out by BullMQ as a supported/tested Redis-compatible alternative, but Dragonfly's own local install path is Docker-first, which is heavier than Valkey for claude-mem's installer.
## Install decision tree
### Interactive install
1. Probe for external config:
- If `CLAUDE_MEM_REDIS_URL` exists, test `PING`, `INFO`, BullMQ Lua/script compatibility, and `maxmemory-policy`.
- If valid, use it and do not manage the process.
2. Probe for local compatible binaries:
- `valkey-server`
- `redis-server`
- known Homebrew paths: `/opt/homebrew/bin`, `/usr/local/bin`
- Linux package paths: `/usr/bin`, `/usr/local/bin`
3. If a binary exists, create claude-mem's own config and data dir:
- `~/.claude-mem/redis/redis.conf`
- `~/.claude-mem/redis/data/`
- `~/.claude-mem/redis/redis.pid`
- `~/.claude-mem/logs/redis-YYYY-MM-DD.log`
4. If no binary exists:
- macOS with Homebrew: install `valkey` with `brew install valkey`.
- Linux with supported package manager: install `valkey` using apt/dnf/yum/apk/pacman when available.
- Linux without package support but supported Ubuntu base: download Valkey binary artifact, verify SHA256, unpack under `~/.claude-mem/bin/valkey/<version>/`.
- Windows: use Docker if Docker is already present and running, otherwise ask for an external Redis URL or keep SQLite fallback.
5. Start the managed sidecar, then start the worker.
### Non-interactive install
Default should not block on prompts:
- If `CLAUDE_MEM_REDIS_URL` works, use it.
- Else if a local `valkey-server` or `redis-server` exists, manage it.
- Else if `--install-redis` was passed, attempt platform install.
- Else fail with a precise command to run.
Do not surprise-run `sudo apt install` or install Docker in non-interactive mode.
## Managed sidecar config
Use a private port, not global `6379`.
Allocate and persist a queue-store port the same way claude-mem persists the worker port:
```sh
CLAUDE_MEM_REDIS_HOST=127.0.0.1
CLAUDE_MEM_REDIS_PORT=<free-port>
CLAUDE_MEM_REDIS_URL=redis://127.0.0.1:<free-port>
```
Suggested config:
```conf
bind 127.0.0.1 ::1
protected-mode yes
port <allocated-port>
dir ~/.claude-mem/redis/data
daemonize no
appendonly yes
appendfsync everysec
save 60 1
maxmemory-policy noeviction
```
BullMQ specifically requires `maxmemory-policy=noeviction` for correct queue behavior and recommends AOF persistence for production durability.
Do not use the user's global Redis config. Generate a claude-mem config so the queue store has the settings BullMQ needs.
## Process model
Add `RedisManager` / `QueueStoreManager` alongside the worker supervisor:
- `ensureQueueStoreStarted()`
- `stopQueueStore()`
- `queueStoreStatus()`
- PID file with start-token validation, mirroring worker PID safety.
- Health probe:
- TCP connect
- `PING`
- `INFO server`
- `CONFIG GET maxmemory-policy`
- `CONFIG GET appendonly`
- BullMQ smoke queue add/get/remove in a namespaced key prefix
Worker startup sequence:
1. Load settings.
2. Ensure queue store is ready.
3. Initialize BullMQ connection.
4. Run SQLite migrations.
5. Start HTTP worker.
Shutdown sequence:
1. Stop providers/workers.
2. Close BullMQ connections.
3. Stop managed queue store only if claude-mem owns it.
## Why not global service management
Avoid making the installer do this as the default:
- `brew services start redis`
- `systemctl enable redis`
- `systemctl start valkey`
Those mutate the user's machine globally, conflict with existing Redis installs, require sudo/admin flows, and make uninstall messy.
The better UX is a private local sidecar owned by claude-mem. It starts when claude-mem starts, stores data in `~/.claude-mem`, and is removed by `npx claude-mem uninstall`.
## Platform notes
### macOS
Best path:
- If Homebrew exists: `brew install valkey`.
- Start `valkey-server` directly with claude-mem's generated config.
- Do not use `brew services`.
Redis official macOS install now uses `brew tap redis/redis` and `brew install --cask redis`, but Redis notes that this cask is not integrated with `brew services`. For claude-mem, that's fine because we should not rely on `brew services` anyway.
### Linux
Best path:
- Prefer package-manager Valkey when available.
- On Ubuntu/Debian, Valkey docs list `apt install valkey`; Ubuntu also has `valkey-redis-compat` for `redis-*` symlinks.
- For Jammy/Noble, Valkey publishes binary artifacts, which are good candidates for a claude-mem-managed install under `~/.claude-mem/bin`.
### Windows
Hardest platform.
Redis official docs say Windows Redis Open Source requires Docker, with Memurai as a Windows compatibility partner. Valkey docs say Windows is not officially supported and suggest WSL for development.
Pragmatic options:
- If Docker is installed/running, launch `valkey/valkey:<pinned>` or `redis:<pinned>` with a named volume.
- If WSL is configured, install/run Valkey inside WSL and connect from Windows.
- Otherwise require `CLAUDE_MEM_REDIS_URL` or use temporary SQLite fallback until native Windows support is chosen.
Do not auto-install Docker Desktop. It is too invasive for an "it just works" CLI installer.
## User-facing UX
Interactive:
```text
Queue engine
BullMQ needs a local Redis-compatible queue store.
claude-mem can manage one for you under ~/.claude-mem.
[recommended] Manage local Valkey for me
Use existing Redis URL
Keep SQLite queue for now
```
Non-interactive:
```sh
npx claude-mem install --queue bullmq --install-redis
npx claude-mem install --queue bullmq --redis-url redis://127.0.0.1:6379
```
Status:
```sh
npx claude-mem status
Worker: running on 127.0.0.1:37777
Queue: BullMQ
Store: managed Valkey 9.0.3 on 127.0.0.1:39241
Persistence: AOF everysec
Policy: noeviction
```
Uninstall:
- Stop managed queue store.
- Remove managed PID/config/logs as requested.
- Preserve queue data by default unless user passes `--purge-data`.
## Implementation phases
1. Add queue-store settings and status plumbing.
2. Add `QueueStoreManager` with process spawn, PID validation, port allocation, and probes.
3. Add Valkey/Redis binary detection.
4. Add macOS/Linux install helpers.
5. Add BullMQ queue backend using managed store.
6. Add Windows Docker/external URL path.
7. Add uninstall cleanup.
8. Add CI matrix:
- Redis 7.2 or Redis 8
- Valkey 8/9
- optional Dragonfly smoke test
## Final call
For claude-mem's desired UX, the winning approach is:
**BullMQ + claude-mem-managed Valkey sidecar by default, external Redis URL as an escape hatch, SQLite as short-term fallback only.**
This gives the speed and correctness of Redis/BullMQ without making users become Redis operators.
@@ -0,0 +1,700 @@
# Claude-Mem 13 Server Beta: Worker Parity Plus Team Features
Status: implementation plan
Date: 2026-05-07
Release target: claude-mem 13
Primary goal: add `server (beta)` as an installer-selectable runtime while leaving the existing worker in place
Relationship to prior plan: follows `plans/2026-05-07-claude-mem-server-apache-bullmq-team-auth.md`, but narrows the release strategy to full worker parity plus additive team features
## Executive Decision
Claude-Mem 13 should ship **Server (beta)** as an opt-in runtime, not as a worker replacement.
The existing worker remains:
- the default installer runtime;
- the stable compatibility path for current users;
- the implementation that current Claude Code hooks can continue to call;
- the fallback when Server beta is disabled, unhealthy, or not installed.
Server beta must reach feature parity by wrapping or copying worker behavior behind shared services before it claims to be a viable runtime. New team features are additive and must not break single-user local worker flows.
## Phase 0: Documentation Discovery
### Local Sources Read
- `plans/2026-05-07-claude-mem-server-apache-bullmq-team-auth.md`
- `/Users/alexnewman/Downloads/claude-mem-handoff-docs/claude-mem-server-plan.md`
- `/Users/alexnewman/Downloads/claude-mem-handoff-docs/apache-2-plan.md`
- `src/npx-cli/index.ts`
- `src/npx-cli/commands/install.ts`
- `src/npx-cli/commands/runtime.ts`
- `src/services/worker-service.ts`
- `src/services/worker-spawner.ts`
- `src/services/server/Server.ts`
- `src/services/worker/http/middleware.ts`
- `src/services/worker/http/routes/ViewerRoutes.ts`
- `src/services/worker/http/routes/SessionRoutes.ts`
- `src/services/worker/http/routes/DataRoutes.ts`
- `src/services/worker/http/routes/SearchRoutes.ts`
- `src/services/worker/http/routes/SettingsRoutes.ts`
- `src/services/worker/http/routes/LogsRoutes.ts`
- `src/services/worker/http/routes/MemoryRoutes.ts`
- `src/services/worker/http/routes/CorpusRoutes.ts`
- `src/services/worker/http/routes/ChromaRoutes.ts`
- `src/services/worker/http/shared.ts`
- `src/services/worker/SessionManager.ts`
- `src/services/sqlite/PendingMessageStore.ts`
- `src/services/queue/SessionQueueProcessor.ts`
- `src/services/worker/agents/ResponseProcessor.ts`
- `src/servers/mcp-server.ts`
- `plugin/hooks/hooks.json`
- `docker/claude-mem/Dockerfile`
- `docker/claude-mem/entrypoint.sh`
- `docker/claude-mem/run.sh`
### External Docs Read
- BullMQ Queues: https://docs.bullmq.io/guide/queues
- BullMQ Job IDs: https://docs.bullmq.io/guide/jobs/job-ids
- BullMQ Stalled Jobs: https://docs.bullmq.io/guide/jobs/stalled
- Better Auth Express Integration: https://better-auth.com/docs/integrations/express
- Better Auth API Key Plugin: https://better-auth.com/docs/plugins/api-key
- Better Auth Organization Plugin: https://better-auth.com/docs/plugins/organization
### Allowed APIs And Patterns
- Installer prompts use `@clack/prompts` through `p.select`, `p.confirm`, `p.tasks`, and `p.note` in `src/npx-cli/commands/install.ts`.
- Runtime commands delegate to installed plugin bundles through `spawnBunWorkerCommand(command, extraArgs)` in `src/npx-cli/commands/runtime.ts`.
- Worker lifecycle uses `ensureWorkerStarted(port, workerScriptPath)` from `src/services/worker-spawner.ts`.
- HTTP routes use `RouteHandler.setupRoutes(app)` and `Server.registerRoutes(handler)`.
- Route validation uses `zod` plus `validateBody(schema)`.
- Current ingestion should be copied through `ingestObservation`, `ingestPrompt`, and `ingestSummary` in `src/services/worker/http/shared.ts`.
- Current queue semantics should be copied from `PendingMessageStore`, `SessionQueueProcessor`, and `SessionManager.getMessageIterator`.
- Current MCP server uses low-level `@modelcontextprotocol/sdk` `Server`, `ListToolsRequestSchema`, and `CallToolRequestSchema`, with hand-written tool schemas in `src/servers/mcp-server.ts`.
- BullMQ jobs should be added through `Queue.add`, with custom job IDs or dedupe IDs for duplicate suppression.
- Better Auth Express handler must be mounted before `express.json()`. Express 5 catch-all docs use `/api/auth/*splat`.
- Better Auth API-key plugin supports create, verify, update, delete, list, permissions, metadata, rate limits, and organization-owned keys.
- Better Auth organization plugin supports organizations, members, teams, roles, permissions, and team configuration.
### Anti-Pattern Guards
- Do not remove, rename, or deprecate the worker in claude-mem 13.
- Do not make Server beta the default installer choice.
- Do not alter existing plugin hooks to require team auth.
- Do not ship Server beta without a route/command/MCP parity matrix.
- Do not put Better Auth behind global `express.json()`.
- Do not use BullMQ as the memory source of truth.
- Do not silently fall back when the user explicitly selects Server beta and BullMQ cannot start.
- Do not claim full team sync, hosted cloud, SSO, billing, or enterprise admin UI in claude-mem 13.
## Worker Parity Matrix
Server beta is not parity-complete until every item below is implemented or explicitly routed to the worker compatibility path.
### Lifecycle And CLI
- `npx claude-mem install`
- `npx claude-mem repair`
- `npx claude-mem update`
- `npx claude-mem uninstall`
- `npx claude-mem start`
- `npx claude-mem stop`
- `npx claude-mem restart`
- `npx claude-mem status`
- `npx claude-mem search <query>`
- `npx claude-mem adopt [--dry-run] [--branch <name>]`
- `npx claude-mem cleanup [--dry-run]`
- `npx claude-mem transcript watch`
- `plugin/scripts/worker-service.cjs start|stop|restart|status|cursor|gemini-cli|hook|generate|clean|adopt|cleanup|--daemon`
### Hook Compatibility
- SessionStart worker autostart hook
- SessionStart context injection hook
- UserPromptSubmit session-init hook
- PostToolUse observation hook
- file context hook
- Stop/Summarize hook
- Current JSON hook outputs with `continue: true` and `suppressOutput: true`
### HTTP Routes
- Viewer and stream: `GET /`, `GET /health`, `GET /stream`
- Core status: `GET /api/health`, `GET /api/readiness`, `GET /api/version`, `GET /api/instructions`
- Admin: `POST /api/admin/restart`, `POST /api/admin/shutdown`, `GET /api/admin/doctor`
- Session ingest: `POST /api/sessions/init`, `POST /api/sessions/observations`, `POST /api/sessions/summarize`, `GET /api/sessions/status`
- Data: `GET /api/observations`, `GET /api/summaries`, `GET /api/prompts`, `GET /api/observation/:id`, `GET /api/observations/by-file`, `POST /api/observations/batch`, `GET /api/session/:id`, `POST /api/sdk-sessions/batch`, `GET /api/prompt/:id`, `GET /api/stats`, `GET /api/projects`, `GET /api/processing-status`, `POST /api/processing`, `POST /api/import`
- Search/context: `GET /api/search`, `GET /api/timeline`, `GET /api/decisions`, `GET /api/changes`, `GET /api/how-it-works`, `GET /api/search/observations`, `GET /api/search/sessions`, `GET /api/search/prompts`, `GET /api/search/by-concept`, `GET /api/search/by-file`, `GET /api/search/by-type`, `GET /api/context/recent`, `GET /api/context/timeline`, `GET /api/context/preview`, `GET /api/context/inject`, `POST /api/context/semantic`, `GET /api/onboarding/explainer`, `GET /api/timeline/by-query`, `GET /api/search/help`
- Settings/admin UI: `GET /api/settings`, `POST /api/settings`, `GET /api/mcp/status`, `POST /api/mcp/toggle`, `GET /api/branch/status`, `POST /api/branch/switch`, `POST /api/branch/update`
- Logs: `GET /api/logs`, `POST /api/logs/clear`
- Memory: `POST /api/memory/save`
- Corpus: `POST /api/corpus`, `GET /api/corpus`, `GET /api/corpus/:name`, `DELETE /api/corpus/:name`, `POST /api/corpus/:name/rebuild`, `POST /api/corpus/:name/prime`, `POST /api/corpus/:name/query`, `POST /api/corpus/:name/reprime`
- Chroma: `GET /api/chroma/status`
### MCP Tools
- `__IMPORTANT`
- `search`
- `timeline`
- `get_observations`
- `smart_search`
- `smart_unfold`
- `smart_outline`
- `build_corpus`
- `list_corpora`
- `prime_corpus`
- `query_corpus`
- `rebuild_corpus`
- `reprime_corpus`
### Runtime Capabilities
- SQLite observation/session/source storage
- Chroma optional semantic search and health probe
- Claude/Gemini/OpenRouter providers
- Provider auth methods and env isolation
- User prompts, summaries, observations, session summaries
- Project catalog and platform source filtering
- Context injection and welcome hint behavior
- Smart file read/search tools
- Knowledge corpus build/prime/query lifecycle
- SSE viewer updates and processing-status broadcasts
- Settings file creation/update/validation
- Branch status/switch/update compatibility
- Logs tail and clear behavior
- Transcript watcher
- Worktree adoption and v12.4.3 cleanup
- MCP status/toggle compatibility
- Privacy skip behavior with `<private>` and excluded projects
- Tool skip rules and `session-memory` meta skip rules
- Queue dedupe, FIFO, idle timeout, restart recovery, and queue depth
## Phase 1: Runtime Selection In Installer
What to implement:
- Add installer runtime selection after provider/model prompts and before worker autostart:
- `Worker (stable, recommended)`
- `Server (beta)`
- Add non-interactive flags:
- `--runtime worker`
- `--runtime server-beta`
- `--queue sqlite|bullmq`
- `--redis-url <url>`
- `--no-server-beta-autostart`
- Persist runtime settings:
- `CLAUDE_MEM_RUNTIME=worker|server-beta`
- `CLAUDE_MEM_SERVER_BETA_ENABLED=true|false`
- `CLAUDE_MEM_SERVER_PORT`
- `CLAUDE_MEM_SERVER_HOST`
- `CLAUDE_MEM_QUEUE_ENGINE=sqlite|bullmq`
- For claude-mem 13, default to worker in interactive and non-interactive installs unless the user explicitly selects Server beta.
- If Server beta is selected, still install the worker bundle and keep worker commands available.
Documentation references:
- Copy prompt style from `promptProvider()` and `promptClaudeModel()` in `src/npx-cli/commands/install.ts`.
- Copy settings merge pattern from `mergeSettings(...)` in `src/npx-cli/commands/install.ts`.
- Copy autostart task structure from the existing `Starting worker daemon` task.
Verification checklist:
- Add tests for interactive runtime selection by unit-testing option parsing and settings writes.
- Add non-interactive tests for `--runtime worker`, `--runtime server-beta`, unknown runtime values, and default behavior.
- Verify `npx claude-mem install --no-auto-start` still skips worker/server startup.
Anti-pattern guards:
- Do not make Server beta the default.
- Do not skip worker installation when Server beta is selected.
- Do not change current provider/model prompt order unless tests cover it.
## Phase 2: Dual Runtime Lifecycle
What to implement:
- Add `src/npx-cli/commands/server-runtime.ts` or extend runtime command helpers with server-aware variants.
- Add `claude-mem server start|stop|restart|status|doctor|logs`.
- Keep `claude-mem start|stop|restart|status` mapped to the selected runtime, but in claude-mem 13 default that selection is worker.
- Add explicit stable aliases:
- `claude-mem worker start`
- `claude-mem worker stop`
- `claude-mem worker restart`
- `claude-mem worker status`
- Add PID/port files for server beta separate from worker:
- `.server.pid`
- `.server.port`
- `server-YYYY-MM-DD.log`
- Add server-spawner equivalent to `ensureWorkerStarted(...)`.
Documentation references:
- Copy `spawnBunWorkerCommand` from `src/npx-cli/commands/runtime.ts`.
- Copy PID safety and daemon spawn patterns from `src/services/worker-spawner.ts` and `src/services/infrastructure/ProcessManager.ts`.
Verification checklist:
- `claude-mem worker status` works with existing worker.
- `claude-mem server status` does not lie when server beta is not installed.
- Starting server beta does not stop the worker unless the user explicitly asks.
- PID files do not conflict.
Anti-pattern guards:
- Do not overload `.worker.pid` or `.worker.port` for server beta.
- Do not change plugin hook autostart to server beta in this phase.
## Phase 3: Compatibility Router For `/api/*`
What to implement:
- Create `src/server/compat/worker-api-routes.ts`.
- Register all current `/api/*`, `/`, `/health`, and `/stream` routes in Server beta.
- For the first implementation, copy route classes and inject shared dependencies rather than rewriting route behavior.
- Keep output shapes byte-compatible enough for existing viewer, MCP, hooks, and docs.
- Add parity snapshots for response fields on health, readiness, stats, projects, processing status, search, observations batch, settings, logs, and Chroma status.
Documentation references:
- Copy route class pattern from every file under `src/services/worker/http/routes`.
- Copy `Server.registerRoutes(...)` usage from `WorkerService.registerRoutes()`.
- Copy middleware gates from `src/services/worker-service.ts` for initialization readiness.
Verification checklist:
- Add `tests/server/parity/routes.test.ts` that asserts every route from the parity matrix is registered.
- Add integration smoke tests for representative GET/POST routes.
- Run existing worker route tests against worker and server beta where feasible.
Anti-pattern guards:
- Do not introduce `/v1` routes as a substitute for `/api/*` parity.
- Do not change viewer or MCP clients before compatibility routes pass.
## Phase 4: Shared Runtime Services
What to implement:
- Extract a runtime composition layer that both worker and server beta can use:
- database manager;
- session manager;
- provider agents;
- SSE broadcaster;
- settings manager;
- corpus store/builder/knowledge agent;
- Chroma manager;
- transcript watcher.
- Keep `WorkerService` as the stable wrapper.
- Add `ServerBetaService` as a parallel wrapper that composes the same service graph.
Documentation references:
- Copy constructor wiring from `WorkerService.constructor`.
- Copy background initialization from `WorkerService.initializeBackground()`.
- Copy provider status logic from `WorkerService` server options.
Verification checklist:
- Worker tests still pass after extraction.
- Server beta starts with the same DB/search initialization behavior.
- Chroma disabled/enabled behavior remains unchanged.
Anti-pattern guards:
- Do not create a second implementation of search, context injection, corpus, or settings logic.
- Do not move Bun-only imports into the Node MCP bundle.
## Phase 5: Queue Parity Then BullMQ
What to implement:
- First, put the current SQLite queue behind an `ObservationQueueEngine` interface.
- Run the same queue contract tests against worker and server beta using SQLite.
- Only after SQLite parity is green, add `BullMqObservationQueueEngine` for Server beta.
- For claude-mem 13, allow:
- worker plus SQLite queue;
- server beta plus SQLite queue;
- server beta plus BullMQ queue.
- Preserve:
- per-session FIFO;
- one active provider consumer per session;
- `_persistentId`;
- `_originalTimestamp`;
- duplicate suppression by `tool_use_id`;
- idle timeout;
- restart reset/reclaim;
- queue depth and processing status.
Documentation references:
- Copy current queue behavior from `PendingMessageStore`, `SessionQueueProcessor`, and `SessionManager.getMessageIterator`.
- Copy BullMQ job-id guidance from BullMQ docs.
- Copy BullMQ stalled-job handling assumptions from BullMQ docs.
Verification checklist:
- Shared queue contract test suite:
- enqueue;
- claim;
- FIFO;
- dedupe;
- idle timeout;
- crash/restart;
- queue depth;
- clear on response.
- BullMQ tests gated by `CLAUDE_MEM_REDIS_URL` or Docker Compose.
Anti-pattern guards:
- Do not introduce BullMQ until SQLite queue parity is green.
- Do not use BullMQ Pro-only grouping features.
- Do not store canonical memories in Redis.
## Phase 6: Team Data Model
What to implement:
- Add team-aware tables while preserving local single-user behavior:
- `users`
- `organizations`
- `teams`
- `team_members`
- `projects.team_id`
- `memory_items.team_id`
- `agent_events.team_id`
- `audit_log.team_id`
- Create a default local user, organization, and team for existing data:
- `local-user`
- `local-org`
- `personal`
- Backfill existing observations/sessions/projects to the default team without changing existing project names or search output.
- Add migration guardrails and rollback-safe backups.
Documentation references:
- Use team/org concepts from Better Auth organization docs.
- Copy migration style from `src/services/sqlite/migrations/runner.ts`.
- Copy existing data access patterns from `src/services/sqlite/SessionStore.ts`.
Verification checklist:
- Fresh DB creates default team.
- Existing DB migration backfills all rows.
- Existing search/context routes still return the same single-user results when no team filter is provided.
- Team IDs are indexed on all new team-scoped tables.
Anti-pattern guards:
- Do not require login for existing local worker usage.
- Do not rewrite historical project names during backfill.
## Phase 7: Better Auth Integration For Server Beta
What to implement:
- Add Better Auth only to Server beta in claude-mem 13.
- Add auth route mount before JSON middleware.
- Add API-key auth middleware for Server beta `/v1/*` and team admin routes.
- Keep legacy `/api/*` compatibility routes in local-dev mode by default for current hooks.
- Add API-key CLI:
- `claude-mem server api-key create`
- `claude-mem server api-key list`
- `claude-mem server api-key revoke`
- Add team CLI:
- `claude-mem server team create`
- `claude-mem server team list`
- `claude-mem server team invite`
- `claude-mem server team members`
- `claude-mem server team switch`
- Add permissions:
- `memories:read`
- `memories:write`
- `memories:forget`
- `events:write`
- `projects:read`
- `projects:write`
- `admin:read`
- `admin:write`
Documentation references:
- Better Auth Express docs for handler order.
- Better Auth API Key plugin docs for create/verify/list/update/delete and permissions.
- Better Auth Organization plugin docs for organizations, teams, and roles.
Verification checklist:
- Auth handler works before body parser.
- API keys are shown once on creation.
- Raw API keys are not stored.
- Revoked keys fail.
- Read-only keys cannot write.
- Team-scoped keys cannot read another team's memories.
- Worker local routes still work without API keys.
Anti-pattern guards:
- Do not enable Better Auth on the worker stable path in claude-mem 13.
- Do not require browser login for CLI/hook flows.
- Do not add SSO/SAML/SCIM.
## Phase 8: Team-Aware REST API
What to implement:
- Add `/v1/*` routes for Server beta:
- `GET /v1/info`
- `GET /v1/me`
- `GET /v1/teams`
- `POST /v1/teams`
- `GET /v1/projects`
- `POST /v1/projects`
- `POST /v1/events`
- `POST /v1/events/batch`
- `POST /v1/memories`
- `GET /v1/memories/:id`
- `PATCH /v1/memories/:id`
- `POST /v1/search`
- `POST /v1/context`
- `POST /v1/forget`
- `GET /v1/audit`
- `POST /v1/export`
- `POST /v1/import`
- Make every `/v1` route team-aware through `authContext.teamId`.
- Keep `/api/*` as compatibility routes and do not force team scoping into their public response shape.
Documentation references:
- Copy Zod validation style from worker route schemas.
- Use data contracts from `claude-mem-server-plan.md`.
Verification checklist:
- OpenAPI or JSON schema generated from Zod schemas.
- Every `/v1` write requires auth.
- Every `/v1` read is scoped to the active team.
- `/api/search` still behaves like worker for local compatibility.
Anti-pattern guards:
- Do not make `/v1` silently fall back to unscoped global reads.
- Do not break MCP tool response formats while adding `/v1`.
## Phase 9: MCP Parity Plus Team MCP
What to implement:
- Keep all current MCP tools working against worker and server beta:
- `search`
- `timeline`
- `get_observations`
- smart file tools
- corpus tools
- Add optional team-aware MCP tools for Server beta:
- `memory_add`
- `memory_search`
- `memory_context`
- `memory_forget`
- `memory_list_recent`
- `memory_record_decision`
- `team_list`
- `team_switch`
- Add auth config path for MCP clients:
- local worker mode: no API key required;
- server beta team mode: API key required for team tools.
Documentation references:
- Copy existing tool declaration and request handler pattern from `src/servers/mcp-server.ts`.
- Copy MCP schema tests from `tests/servers/mcp-tool-schemas.test.ts`.
Verification checklist:
- Existing MCP tool snapshots unchanged.
- New team tools require API key when server beta team mode is enabled.
- MCP server bundle size guard still passes.
Anti-pattern guards:
- Do not import server beta auth or BullMQ into smart file-read tools unless needed.
- Do not remove the 3-layer search workflow guidance.
## Phase 10: Hook Routing Strategy
What to implement:
- Keep plugin hooks calling `worker-service.cjs` in claude-mem 13 by default.
- When installer selects Server beta, write setting `CLAUDE_MEM_RUNTIME=server-beta` and have hook handlers route to server beta only after server beta health is confirmed.
- If server beta is unhealthy, hook handler should fall back to worker and log a warning.
- Preserve hook JSON output and timeout behavior.
- Add a runtime status line in `/api/health` and `/v1/info`:
- `runtime: worker|server-beta`
- `compatWorkerAvailable: boolean`
- `serverBetaEnabled: boolean`
Documentation references:
- Copy hook commands from `plugin/hooks/hooks.json`.
- Copy hook handler routing from `src/services/worker-service.ts` cases `hook`, `generate`, and daemon startup.
Verification checklist:
- Existing hooks still pass lifecycle tests.
- Server beta selected install routes hooks to server only when healthy.
- Server beta down means worker fallback and no hook failure.
- Hook outputs remain valid JSON where expected.
Anti-pattern guards:
- Do not make user sessions fail because Server beta is down.
- Do not change hook command lines until fallback tests exist.
## Phase 11: Viewer And Admin UX Parity
What to implement:
- Make existing viewer work against worker and server beta compatibility routes.
- Add non-invasive Server beta status to the existing viewer:
- runtime;
- queue engine;
- Redis/Valkey status when BullMQ is enabled;
- active team in server beta mode.
- Add team switcher only when Server beta team features are enabled.
- Keep current single-user worker UI unchanged by default.
Documentation references:
- Copy viewer route behavior from `ViewerRoutes`.
- Copy current UI API calls from `src/ui/viewer`.
Verification checklist:
- Viewer loads at `/` in worker.
- Viewer loads at `/` in server beta.
- SSE stream works in both.
- Team UI is hidden in worker mode.
Anti-pattern guards:
- Do not turn the viewer into an enterprise admin console in claude-mem 13.
- Do not require auth for local worker viewer.
## Phase 12: Docker Compose Beta Profile
What to implement:
- Add Docker Compose profile for Server beta:
- `claude-mem-server-beta`
- `valkey`
- optional `chroma`
- Keep existing `docker/claude-mem` harness available.
- Persist:
- SQLite data;
- logs;
- Valkey AOF data;
- generated API keys/auth DB.
- Add healthchecks:
- `GET /healthz`
- Valkey `PING`
- Add docs for local-only vs container-bound host settings.
Documentation references:
- Copy current Docker auth/credential mounting from `docker/claude-mem/entrypoint.sh` and `run.sh`.
- Copy Valkey config guidance from `plans/2026-05-06-redis-dependency-strategy.md`.
Verification checklist:
- `docker compose --profile server-beta up --build`
- Server beta health passes.
- Queue survives server container restart.
- API-key protected `/v1/info` works.
Anti-pattern guards:
- Do not remove existing Docker harness.
- Do not bind unauthenticated team server to public interfaces.
## Phase 13: Docs, Labels, And Release Guardrails
What to implement:
- Update install docs with:
- Worker stable path;
- Server beta option;
- what beta means;
- fallback behavior;
- known gaps;
- team feature scope.
- Add `docs/server-beta.md`.
- Add `docs/server-beta-parity.md` with the parity matrix and test commands.
- Add `docs/team-features.md`.
- Add changelog language for claude-mem 13:
- "Server beta is opt-in."
- "Worker remains the stable default."
- "Team features are beta and server-only."
Documentation references:
- Copy public docs tone from `docs/public/*.mdx`.
- Copy product boundary from handoff docs.
Verification checklist:
- `rg -n "server beta|Server \\(beta\\)|worker stable|team features" README.md docs`
- No docs imply worker removal.
- No docs imply hosted cloud/SSO/billing is included.
Anti-pattern guards:
- Do not call Server beta production-stable.
- Do not imply team memory sync is complete unless implemented.
## Final Verification Phase
Run:
```sh
npm run typecheck:root
bun test tests/server/ tests/services/queue/ tests/services/sqlite/ tests/servers/
bun test tests/integration/worker-api-endpoints.test.ts
bun test tests/hook-lifecycle.test.ts tests/worker-spawn.test.ts tests/services/worker-spawner.test.ts
npm run build
docker compose --profile server-beta up --build
```
Parity acceptance:
- Worker is still default after `npx claude-mem install`.
- Installer can select `Server (beta)`.
- `claude-mem worker start|status|stop` works.
- `claude-mem server start|status|stop` works.
- Current Claude Code hooks continue working in worker mode.
- Server beta can run the current hook ingestion path.
- Every route in the parity matrix is present in Server beta or explicitly proxied to the worker.
- Every current MCP tool still works.
- Viewer and SSE work in both runtimes.
- SQLite queue parity passes before BullMQ is enabled.
- BullMQ mode passes Redis/Valkey integration tests.
- Team API keys enforce read/write/team boundaries.
- Existing local single-user workflows do not require API keys.
## Recommended Execution Order
1. Installer runtime selection with worker as default.
2. Dual runtime lifecycle and separate PID/port files.
3. `/api/*` compatibility router and route parity tests.
4. Shared runtime service extraction.
5. SQLite queue parity interface.
6. Server beta starts with SQLite queue and full worker parity.
7. Better Auth team model and API keys in Server beta only.
8. `/v1` team-aware API.
9. BullMQ/Valkey Server beta queue option.
10. MCP team additions.
11. Hook runtime routing with health-checked fallback.
12. Viewer beta status and optional team switcher.
13. Docker Compose Server beta profile.
14. Docs and release guardrails.
The key release rule: **claude-mem 13 can ship Server beta only when worker remains fully intact and Server beta has an explicit parity test report.**
@@ -0,0 +1,582 @@
# Claude-Mem Server: Apache-2.0, BullMQ, Team Auth Plan
Status: implementation plan
Date: 2026-05-07
Primary command: `claude-mem server`
Target runtime: deployable Docker container plus local compatibility path
Target license: Apache-2.0 for embeddable/core code
## Executive Decision
Build Claude-Mem Server inside this repo as the canonical next runtime. Keep the current worker as a compatibility shim while moving shared server logic into typed server/core/storage modules.
Use:
- Express 5 initially, because the repo already depends on it and all current routes use `RouteHandler.setupRoutes(app)`.
- BullMQ as the queue engine for deployable server mode.
- Valkey/Redis as the Redis-compatible queue store, with Docker Compose as the first deployable path.
- Better Auth for user/org/team auth and API-key management, after the Express middleware bootstrap is refactored to satisfy Better Auth's handler-order requirements.
- SQLite as the source of truth for memory records in v0.1, with Redis/BullMQ treated as queue state.
- Apache-2.0 for the open core, server, CLI, SDKs, schemas, adapters, MCP tools, tests, examples, and public docs.
Do not build hosted Magic Recall cloud, billing, SSO/SAML/SCIM, enterprise RBAC UI, or cross-customer managed sync in this pass.
## Phase 0: Documentation Discovery
### Local Sources Read
- `/Users/alexnewman/Downloads/claude-mem-handoff-docs/apache-2-plan.md`
- `/Users/alexnewman/Downloads/claude-mem-handoff-docs/claude-mem-server-plan.md`
- `package.json`
- `src/npx-cli/index.ts`
- `src/npx-cli/commands/runtime.ts`
- `src/services/server/Server.ts`
- `src/services/worker-service.ts`
- `src/services/worker/http/middleware.ts`
- `src/services/worker/http/routes/SessionRoutes.ts`
- `src/services/worker/http/routes/SearchRoutes.ts`
- `src/services/worker/http/routes/DataRoutes.ts`
- `src/services/worker/http/routes/MemoryRoutes.ts`
- `src/services/sqlite/PendingMessageStore.ts`
- `src/services/queue/SessionQueueProcessor.ts`
- `src/services/worker/SessionManager.ts`
- `src/services/sqlite/schema.sql`
- `src/services/sqlite/migrations/runner.ts`
- `src/servers/mcp-server.ts`
- `docker/claude-mem/Dockerfile`
- `docker/claude-mem/README.md`
- `plans/2026-05-06-observation-queue-engine-deep-dive.md`
- `plans/2026-05-06-redis-dependency-strategy.md`
### External Docs Read
- BullMQ Queues: https://docs.bullmq.io/guide/queues
- BullMQ Stalled Jobs: https://docs.bullmq.io/guide/jobs/stalled
- BullMQ Job IDs: https://docs.bullmq.io/guide/jobs/job-ids
- Better Auth Express Integration: https://better-auth.com/docs/integrations/express
- Better Auth API Key Plugin: https://better-auth.com/docs/plugins/api-key
- Better Auth Organization Plugin: https://better-auth.com/docs/plugins/organization
### Allowed APIs And Patterns
- Existing route pattern: implement route classes with `setupRoutes(app: express.Application): void`, then register through `Server.registerRoutes(handler)`.
- Existing validation pattern: use `zod` schemas with `validateBody(schema)` from `src/services/worker/http/middleware/validateBody.ts`.
- Existing MCP pattern: add tools to the `tools` array in `src/servers/mcp-server.ts`, with plain JSON Schema `inputSchema` and handlers that call server/core logic.
- BullMQ queue creation: use `new Queue(name, { connection })`, then enqueue jobs with `queue.add(name, data, options)`. BullMQ stores jobs in Redis and workers can pick them up later.
- BullMQ dedupe: use a custom `jobId` or deduplication id for observation dedupe. Custom job IDs are unique per queue and duplicate adds are ignored while the previous job still exists.
- BullMQ stalled-job recovery: active jobs are locked and moved back to waiting or failed if the worker stops renewing the lock.
- Better Auth Express mount: mount `app.all("/api/auth/*splat", toNodeHandler(auth))` before `express.json()` on Express 5. Do not place global `express.json()` before the Better Auth handler.
- Better Auth API keys: use API-key plugin server methods for create, verify, update, delete, list; API keys can carry permissions and org ownership.
- Better Auth org/team support: use organization plugin with `teams: { enabled: true }` and custom project/memory permissions.
### Anti-Pattern Guards
- Do not create a second repo or primary `claude-mem-server` package.
- Do not replace all current worker routes at once.
- Do not put auth routes behind global `express.json()` if using Better Auth.
- Do not make MCP duplicate retrieval/storage logic.
- Do not treat Redis as the memory source of truth.
- Do not use Bee-Queue.
- Do not put sensitive prompt/tool payloads in Redis without a clear retention and redaction policy.
- Do not silently fall back to SQLite when `CLAUDE_MEM_QUEUE_ENGINE=bullmq` is explicitly configured.
- Do not claim team/org memory sync or enterprise SaaS is shipped in v0.1.
## Phase 1: License And Product Boundary
What to implement:
- Replace root `LICENSE` with official Apache License 2.0 text.
- Update `package.json` and nested manifests intended for public/core distribution to `"license": "Apache-2.0"`.
- Add `NOTICE`.
- Add `docs/license.md` and `docs/ip-boundary.md` using the handoff language from `apache-2-plan.md`.
- Update README license language.
- Add scoped SPDX headers to new `src/server`, `src/core`, `src/storage`, `src/sdk`, and `src/adapters` files as they are created.
Documentation references:
- Copy exact license text from https://www.apache.org/licenses/LICENSE-2.0.
- Use scope and commercial boundary from `/Users/alexnewman/Downloads/claude-mem-handoff-docs/apache-2-plan.md`.
Verification:
- `rg -n "AGPL|GNU Affero|Affero|GPL|copyleft|license" .`
- `rg -n "Claude-Mem™|trademark|official Anthropic|endorsed by Anthropic" .`
- `bun test tests/infrastructure/version-consistency.test.ts`
- Human review still required for contributor rights and dependency-license audit.
Anti-pattern guards:
- Do not add trademark claims around `Claude-Mem`.
- Do not move commercial/private features into the Apache-2.0 repo.
## Phase 2: Server Namespace And Compatibility CLI
What to implement:
- Add `src/npx-cli/commands/server.ts`.
- Teach `src/npx-cli/index.ts` to route:
- `claude-mem server start`
- `claude-mem server stop`
- `claude-mem server restart`
- `claude-mem server status`
- `claude-mem server doctor`
- `claude-mem server logs`
- `claude-mem server migrate`
- `claude-mem server export`
- `claude-mem server import`
- `claude-mem server api-key create|list|revoke`
- Keep existing `start|stop|restart|status` as worker compatibility aliases.
- Add `claude-mem worker start|stop|restart|status` aliases that call the same command implementation as `server`.
- Teach `src/services/worker-service.ts`'s internal command switch to accept the same `server` subcommands where installed plugin scripts invoke the worker bundle directly.
Documentation references:
- Copy process delegation pattern from `src/npx-cli/commands/runtime.ts`.
- Keep help formatting from `src/npx-cli/index.ts`.
- Keep worker script path conventions from `plugin/scripts/worker-service.cjs`.
- Copy worker-service command parsing shape from `src/services/worker-service.ts`.
Verification:
- `bun test tests/install-non-tty.test.ts tests/infrastructure/worker-json-status.test.ts`
- Add CLI parser tests for `server` and `worker` namespaces.
- Manual smoke:
- `node dist/npx-cli/index.js --help`
- `node dist/npx-cli/index.js server status`
Anti-pattern guards:
- Do not remove `npx claude-mem install`.
- Do not rename the primary npm binary.
## Phase 3: Server Bootstrap Refactor
What to implement:
- Create `src/server/create-server.ts` as the new composition root.
- Move the generic `Server` shell from `src/services/server/Server.ts` toward `src/server/http-server.ts`, but keep compatibility exports during migration.
- Split middleware registration into ordered buckets:
- pre-body-parser routes, including Better Auth later;
- body parser and CORS;
- request logging and static UI;
- route registration;
- not-found/error handlers.
- Fix runtime dependency drift by adding `cors` to production dependencies or removing the runtime import. Current code imports `cors` but only `@types/cors` is declared.
- Update CORS `allowedHeaders` to include `Authorization` before API-key routes ship.
Documentation references:
- Copy existing server lifecycle from `src/services/server/Server.ts`.
- Copy CORS behavior from `src/services/worker/http/middleware.ts`.
- Follow Better Auth Express docs: auth handler before `express.json()`, Express 5 catch-all route uses `*splat`.
Verification:
- `bun test tests/server/server.test.ts tests/worker/middleware/cors-restriction.test.ts`
- Add a regression test proving auth routes are mounted before JSON middleware.
- `npm run typecheck:root`
Anti-pattern guards:
- Do not put global `express.json()` before Better Auth.
- Do not change current default host from `127.0.0.1` without a migration and explicit config.
## Phase 4: Core Contracts And Storage Boundary
What to implement:
- Add shared Zod schemas under `src/core/schemas/`:
- `agent-event.ts`
- `memory-item.ts`
- `context-pack.ts`
- `project.ts`
- `session.ts`
- `team.ts`
- `auth.ts`
- Add `src/storage/sqlite/` repositories for new server-owned tables:
- `projects`
- `server_sessions`
- `agent_events`
- `memory_items`
- `memory_sources`
- `teams`
- `team_members`
- `api_keys` or Better Auth tables
- `audit_log`
- Keep existing `sdk_sessions`, `observations`, `session_summaries`, `user_prompts`, and `pending_messages` readable during migration.
- Decide and document the translation layer between existing `observations` and new `memory_items`.
Documentation references:
- Use data contracts from `/Users/alexnewman/Downloads/claude-mem-handoff-docs/claude-mem-server-plan.md`.
- Copy repository style from `src/services/sqlite/*` and migration style from `src/services/sqlite/migrations/runner.ts`.
Verification:
- Add migration tests in `tests/services/sqlite/migration-runner.test.ts`.
- Add schema tests for fresh DB and upgraded DB.
- `bun test tests/services/sqlite/ tests/sqlite/`
Anti-pattern guards:
- Do not make Redis the source of truth for memories.
- Do not break existing `observations` search while adding `memory_items`.
## Phase 5: Queue Engine Boundary
What to implement:
- Add `src/server/queue/ObservationQueueEngine.ts` with an interface shaped around current behavior:
```ts
export interface ObservationQueueEngine {
enqueue(sessionDbId: number, contentSessionId: string, message: PendingMessage): Promise<EnqueueResult>;
createIterator(sessionDbId: number, signal: AbortSignal, onIdleTimeout?: () => void): AsyncIterableIterator<PendingMessageWithId>;
clearPendingForSession(sessionDbId: number): Promise<number>;
resetProcessingToPending(sessionDbId: number): Promise<number>;
getPendingCount(sessionDbId: number): Promise<number>;
getTotalQueueDepth(): Promise<number>;
close(): Promise<void>;
}
```
- Implement `SqliteObservationQueueEngine` by wrapping `PendingMessageStore` and `SessionQueueProcessor`.
- Update `SessionManager` to depend on the interface instead of directly constructing `PendingMessageStore`.
- Clean up schema drift before BullMQ:
- remove or restore `worker_pid` consistently;
- reconcile `pending|processing` with stale `processed|failed` references;
- remove or fix `storeObservationsAndMarkComplete()` dead-code writes.
Documentation references:
- Copy current semantics from `src/services/sqlite/PendingMessageStore.ts`.
- Copy async iterator behavior from `src/services/queue/SessionQueueProcessor.ts`.
- Preserve provider contract consumed by Claude/Gemini/OpenRouter providers through `SessionManager`.
Verification:
- Shared queue contract test suite.
- `bun test tests/services/sqlite/PendingMessageStore.test.ts tests/services/queue/SessionQueueProcessor.test.ts`
- Add tests for dedupe, FIFO, restart reset, idle timeout, and queue depth.
Anti-pattern guards:
- Do not model this as generic stateless jobs only. The current queue is a per-session stream feeding provider generators.
- Do not change `_persistentId` and `_originalTimestamp` semantics.
## Phase 6: BullMQ And Valkey Runtime
What to implement:
- Add dependencies:
- `bullmq`
- `ioredis` if BullMQ usage requires direct connection management beyond BullMQ exports.
- Add settings:
- `CLAUDE_MEM_QUEUE_ENGINE=sqlite|bullmq`
- `CLAUDE_MEM_REDIS_URL`
- `CLAUDE_MEM_REDIS_HOST`
- `CLAUDE_MEM_REDIS_PORT`
- `CLAUDE_MEM_REDIS_MODE=external|managed|docker`
- `CLAUDE_MEM_QUEUE_REDIS_PREFIX`
- Add `BullMqObservationQueueEngine`.
- Use one queue per active session at first, with effective concurrency `1`, to preserve per-session FIFO without BullMQ Pro groups.
- Use safe hashed job IDs:
- observation: `obs_${sha256(contentSessionId + "\0" + toolUseId)}`
- summarize: `sum_${sha256(contentSessionId + "\0" + createdAtEpoch + "\0" + messageKind)}`
- Store only queue payloads needed to resume processing. Keep memory records in SQLite.
- Add Redis health to `/api/health` and `server status` only when BullMQ is enabled.
Documentation references:
- BullMQ docs: `Queue.add(...)` stores jobs in Redis and workers can process later.
- BullMQ job IDs: duplicate custom IDs are ignored while the prior job still exists.
- BullMQ stalled jobs: active jobs are lock-renewed and moved back or failed when stalled.
- Copy configuration strategy from `plans/2026-05-06-redis-dependency-strategy.md`.
Verification:
- Unit tests with mocked BullMQ queue where possible.
- Integration tests gated by `CLAUDE_MEM_REDIS_URL`.
- Docker Compose test with Valkey:
- enqueue, kill server, restart, process;
- duplicate `tool_use_id` suppressed;
- per-session FIFO;
- stalled job returns or fails as configured.
Anti-pattern guards:
- Do not use one high-concurrency global queue until same-session ordering is proven.
- Do not silently drop messages if Redis is unavailable.
- Do not use `:` in custom job IDs.
## Phase 7: Auth, Teams, And API Keys
What to implement:
- Add Better Auth after Phase 3 server bootstrap is complete.
- Add auth dependencies:
- `better-auth`
- `@better-auth/api-key`
- Create `src/server/auth/auth.ts` with:
- Better Auth core config;
- API-key plugin;
- organization plugin with teams enabled;
- custom access statements for projects and memories.
- Create `src/server/middleware/auth.ts`:
- read `Authorization: Bearer <key>`;
- verify API keys with Better Auth;
- attach `authContext` containing `userId`, `organizationId`, `teamId`, scopes, and key id;
- allow localhost unauthenticated reads only if `CLAUDE_MEM_AUTH_MODE=local-dev`.
- Create CLI commands:
- `claude-mem server api-key create --team <team> --scope memories:read,memories:write`
- `claude-mem server api-key list`
- `claude-mem server api-key revoke <id>`
- Add team/project scoping to memory storage and retrieval.
- Add audit rows when memories are served, written, forgotten, imported, or exported.
Documentation references:
- Better Auth API Key plugin supports create, manage, verify, rate limiting, permissions, metadata, custom prefixes, and organization-owned keys.
- Better Auth Organization plugin supports organizations, members, teams, roles, permissions, and `hasPermission`.
- Better Auth Express integration requires the auth handler before body parser.
Verification:
- Auth route tests:
- unauthenticated write denied;
- API key with read scope cannot write;
- revoked key denied;
- team A key cannot read team B memory;
- local-only mode does not bind publicly.
- `bun test tests/server/ tests/worker/middleware/`
Anti-pattern guards:
- Do not add SSO/SAML/SCIM in v0.1.
- Do not make API keys plaintext in SQLite. Store only hashed keys and show the raw key once at creation.
- Do not expose memory over LAN by default.
## Phase 8: REST API V1
What to implement:
- Add `src/server/routes/v1/*`:
- `GET /healthz`
- `GET /v1/info`
- `GET /v1/projects`
- `POST /v1/projects`
- `GET /v1/projects/:id`
- `POST /v1/sessions/start`
- `POST /v1/sessions/:id/end`
- `GET /v1/sessions/:id`
- `POST /v1/events`
- `POST /v1/events/batch`
- `GET /v1/events/:id`
- `POST /v1/memories`
- `GET /v1/memories/:id`
- `PATCH /v1/memories/:id`
- `POST /v1/memories/:id/supersede`
- `POST /v1/forget`
- `POST /v1/search`
- `POST /v1/context`
- `GET /v1/audit`
- `POST /v1/export`
- `POST /v1/import`
- `POST /v1/reindex`
- Keep legacy `/api/*` routes for current hooks, MCP, and viewer.
- Add OpenAPI generation from Zod schemas using existing `zod-to-json-schema`, or add a focused OpenAPI helper only if needed.
Documentation references:
- Copy route class and validation patterns from existing worker route files.
- Copy endpoint list from `claude-mem-server-plan.md`.
Verification:
- Add REST integration tests under `tests/server/v1/`.
- Add OpenAPI snapshot/schema tests.
- Legacy smoke: current `/api/sessions/init`, `/api/sessions/observations`, `/api/search`, `/api/context/inject` still work.
Anti-pattern guards:
- Do not delete `/api/*` compatibility routes in this phase.
- Do not implement MCP as a separate memory stack.
## Phase 9: Adapter Migration
What to implement:
- Add `src/adapters/claude-code/mapper.ts` to map existing hook payloads to `AgentEvent`.
- Add `src/adapters/generic-rest/examples.ts` with Codex/OpenCode/OpenClaw/custom examples.
- Refactor `SessionRoutes` ingestion to call the same event-ingestion service used by `POST /v1/events`.
- Preserve current hook fields:
- `contentSessionId`
- `tool_name`
- `tool_input`
- `tool_response`
- `cwd`
- `agentId`
- `agentType`
- `platformSource`
- `tool_use_id` / `toolUseId`
Documentation references:
- Copy field handling from `src/services/worker/http/routes/SessionRoutes.ts`.
- Copy platform normalization from `src/shared/platform-source.ts`.
Verification:
- Existing hook tests continue passing.
- Add mapper tests for Claude Code, Codex transcript watcher, and generic REST event payloads.
Anti-pattern guards:
- Do not make Claude Code the core data model.
- Do not throw away raw event payloads before redaction/classification decisions are applied.
## Phase 10: MCP Surface On Server Core
What to implement:
- Add `src/server/mcp/tools.ts`, `resources.ts`, `prompts.ts`, and `register.ts`.
- Keep existing `src/servers/mcp-server.ts` as a thin stdio entrypoint.
- Implement tools:
- `memory_add`
- `memory_search`
- `memory_context`
- `memory_forget`
- `memory_list_recent`
- `memory_record_decision`
- Keep existing search/timeline/get-observations tools during migration.
Documentation references:
- Copy low-level SDK usage from `src/servers/mcp-server.ts`.
- Use MCP tool schema tests from `tests/servers/mcp-tool-schemas.test.ts`.
Verification:
- MCP list/call tests for new tools.
- Build guard in `scripts/build-hooks.js` still prevents Bun-only worker code from bloating the MCP bundle.
Anti-pattern guards:
- Do not import Bun-only SQLite/worker internals into the Node MCP bundle.
- Do not bypass auth/team scoping in MCP tools.
## Phase 11: Docker Deployment
What to implement:
- Add `docker/server/Dockerfile` or update `docker/claude-mem/Dockerfile` for server mode.
- Add `docker-compose.yml` with services:
- `claude-mem-server`
- `valkey`
- optional `chroma` if Chroma remains enabled in server profile
- Server container defaults:
- `CLAUDE_MEM_HOST=0.0.0.0` inside container
- published port explicitly configured by compose
- `CLAUDE_MEM_QUEUE_ENGINE=bullmq`
- `CLAUDE_MEM_REDIS_URL=redis://valkey:6379`
- persisted `/data/claude-mem`
- Add healthcheck using `GET /healthz`.
- Keep local auth credential mounting patterns from current Docker docs.
Documentation references:
- Copy Bun/uv/Claude Code install style from `docker/claude-mem/Dockerfile`.
- Copy credential handling conventions from `docker/claude-mem/README.md` and `entrypoint.sh`.
- Copy Valkey config guidance from `plans/2026-05-06-redis-dependency-strategy.md`.
Verification:
- `docker compose up --build`
- `curl http://127.0.0.1:<port>/healthz`
- `curl -H "Authorization: Bearer <key>" http://127.0.0.1:<port>/v1/info`
- Kill/restart server container and verify queued events survive.
Anti-pattern guards:
- Do not auto-install Docker Desktop.
- Do not bind public host ports without documented auth.
- Do not run Redis/Valkey without persistence in deployable examples.
## Phase 12: Docs And Migration
What to implement:
- Add:
- `docs/server.md`
- `docs/api.md`
- `docs/adapters.md`
- `docs/security.md`
- `docs/docker.md`
- `docs/migration-worker-to-server.md`
- Update README to introduce Claude-Mem Server first and worker as compatibility language.
- Document:
- local dev mode;
- Docker deployment;
- API-key creation;
- team/project scoping;
- generic agent ingestion;
- queue engine settings;
- privacy/redaction baseline.
Documentation references:
- Use public docs style from `docs/public/*.mdx`.
- Use handoff docs for product wording and explicit non-goals.
Verification:
- `rg -n "worker service|Worker Service|worker-first" README.md docs`
- Ensure docs still mention compatibility commands where needed.
Anti-pattern guards:
- Do not imply hosted cloud or enterprise features are available.
- Do not call Claude-Mem an official Anthropic project.
## Final Verification Phase
Run:
```sh
npm run typecheck:root
bun test tests/server/ tests/services/queue/ tests/services/sqlite/ tests/servers/
bun test tests/integration/worker-api-endpoints.test.ts
npm run build
docker compose up --build
```
Manual acceptance checklist:
- `npx claude-mem install` still works.
- `claude-mem server start|status|stop` works.
- `claude-mem worker start|status|stop` aliases work.
- Existing Claude Code hooks still write observations.
- Generic REST client can write and search memory.
- MCP tools use the same server/core logic.
- API-key protected writes fail without auth.
- Team-scoped search cannot cross team boundaries.
- BullMQ/Valkey mode survives server container restart.
- SQLite remains canonical source of memory truth.
- Apache-2.0 migration is complete and stale AGPL messaging is removed from public package/docs.
## Suggested Execution Order
1. Phase 1: Apache-2.0 boundary.
2. Phase 2: CLI namespace and aliases.
3. Phase 3: Server bootstrap refactor.
4. Phase 5: Queue boundary and SQLite contract cleanup.
5. Phase 6: BullMQ/Valkey backend.
6. Phase 4: New core/storage contracts.
7. Phase 7: Auth/team/API-key layer.
8. Phase 8: REST V1.
9. Phase 9: Adapter migration.
10. Phase 10: MCP server-core surface.
11. Phase 11: Docker deployment.
12. Phase 12: Docs and migration guide.
The order intentionally moves the middleware and queue boundaries before Better Auth and REST V1. Those two boundaries are the highest-risk coupling points in the current codebase.
@@ -0,0 +1,331 @@
# Finish BullMQ Observation Queue Branch — Ship Plan
Date: 2026-05-07
Branch: `bullmq-vs-bee-queue-for-claude-mem-observation-que`
Base: `origin/main` @ `0a43ab76`
Parent plan: `plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md`
## Reframe
The prior session believed Phase 1 was ungated because two reviewer agents failed (one returned not_found, "Carver" was user-aborted at 111.9s). That belief was based on a stale snapshot that predated commit `4e0fc77a Add Postgres observation storage foundation`. **Phase 1 is committed.** `git status` shows zero uncommitted changes under `src/storage/postgres/`.
What is actually dirty in the worktree is **Phase 2: Define Server Runtime Boundary**. The dirty files map 1:1 to that phase's "What To Implement" section. The remaining work to "finish this branch" is: confirm Phase 1 with concrete checks (not another reviewer agent), land Phase 2, push.
Phases 313 (BullMQ queue, event-to-job pipeline, provider extraction, hook routing, MCP, compat, Docker, team auth, observability, final verification) are explicitly **out of scope** for this branch. The PR is already 167 files / 23.5K insertions. Continuing past Phase 2 here would make review impossible.
## Phase 0: Documentation Discovery
### Sources Read
- `plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md` (parent plan, 987 lines, all 14 sections from Phase 0 through Phase 13)
- `PR_REORIENTATION_REPORT.md` (660 lines) — independent inventory of committed + dirty surfaces
- `git status`, `git log --oneline -15`, `git diff --stat HEAD`
- Worktree: `src/server/runtime/{ServerBetaService.ts,create-server-beta-service.ts,types.ts}`
- Worktree: `src/storage/postgres/` — already in commit `4e0fc77a`
### Concrete Findings
- Phase 1 (Postgres storage foundation) is committed in `4e0fc77a`. Includes scoped `addSource`, `transitionStatus`, generation-job event `append`, FTS via generated `content_search` tsvector + GIN index, tenant-scoped uniqueness constraints, and 20 integration tests including the negative-scope mutation test.
- Phase 2 (server runtime boundary) is implemented but uncommitted. Files match the parent plan's Phase 2 deliverables exactly: independent `ServerBetaService`, `create-server-beta-service`, disabled boundary types, `.server-beta.{pid,port,runtime.json}` paths, runtime labels in `/api/health` and `/v1/info`, server-beta CLI lifecycle, build-hooks split into a separate `server-beta-service.cjs` bundle, ephemeral-port test for `/api/health` and `/v1/info`.
- Two doc artifacts (`AGENTS.md`, `PR_REORIENTATION_REPORT.md`) are also untracked. Decide before push.
### Anti-Pattern Guards (carried from parent plan)
- Do not spawn a third reviewer agent to "gate" Phase 1. The integration test suite plus the plan's grep checklist is the gate. Reviewer agents are a second opinion, not the primary gate.
- Do not pull Phase 3+ work into this branch.
- Do not amend `4e0fc77a` to "tidy" Phase 1; create new commits.
- Do not couple Phase 2 to `WorkerService` (the entire point of Phase 2 is independence).
## Phase A: Re-Confirm Phase 1 Gate (Deterministic, No Reviewer Agent)
### What To Run
1. `tsc --noEmit` scoped to Postgres storage:
```bash
bunx tsc --noEmit src/storage/postgres/*.ts
```
2. Postgres integration suite (requires `DATABASE_URL` or local Postgres on default port):
```bash
bun test tests/storage/postgres
```
3. Anti-pattern greps (must all return zero matches in `src/storage/postgres/`):
```bash
rg -n "UNIQUE\s*\(\s*source_type\s*,\s*source_id\s*,\s*job_type\s*\)" src/storage/postgres
rg -n "UNIQUE\s*\(\s*observation_id\s*,\s*source_type\s*,\s*source_id\s*\)" src/storage/postgres
```
4. Scoped-mutation grep (must show `projectId`/`teamId` parameters):
```bash
rg -n "addSource|transitionStatus|append" src/storage/postgres
```
### Verification Checklist
- TypeScript clean.
- All 20 Postgres integration tests pass, including the negative-scope mutation test.
- Both anti-pattern greps return empty.
- Scoped-mutation grep shows `projectId`/`teamId` in every signature.
### Anti-Pattern Guards
- Do not edit `src/storage/postgres/*.ts` in this phase. If Phase A fails, open a separate fix-up commit; do not amend `4e0fc77a`.
## Phase B: Land Phase 2 (Server Runtime Boundary)
### What To Run
1. Phase 2 independence grep — Server beta runtime must not import worker:
```bash
rg -n "WorkerService|services/worker-service|worker/http" \
src/server/runtime src/npx-cli/commands/server.ts
```
Allowed: matches inside `src/services/worker-service.ts` itself (delegation back to server-beta is fine). Forbidden: any import inside `src/server/runtime/`.
2. Server-beta service test:
```bash
bun test tests/server/server-beta-service.test.ts
```
3. CLI namespace test:
```bash
bun test tests/npx-cli-server-namespace.test.ts
```
4. Build verifies `server-beta-service.cjs` bundle is produced:
```bash
npm run build-and-sync
ls -la plugin/scripts/server-beta-service.cjs
```
5. Smoke test independence:
```bash
npx claude-mem server status # before start
npx claude-mem server start
npx claude-mem server status # running, runtime=server-beta
curl -s http://127.0.0.1:$(cat ~/.claude-mem/.server-beta.port)/healthz
curl -s http://127.0.0.1:$(cat ~/.claude-mem/.server-beta.port)/v1/info
npx claude-mem server stop
```
Worker `start|stop|status` must remain functional throughout.
### Commit Layout
Two commits, in order:
1. **`feat(server-beta): add independent runtime service`**
- `src/server/runtime/ServerBetaService.ts`
- `src/server/runtime/create-server-beta-service.ts`
- `src/server/runtime/types.ts`
- `src/server/routes/v1/ServerV1Routes.ts` (runtime label)
- `src/services/server/Server.ts` (runtime option)
- `src/shared/paths.ts` (`.server-beta.{pid,port,runtime.json}`)
- `tests/server/server-beta-service.test.ts`
2. **`feat(server-beta): route CLI lifecycle and build a separate bundle`**
- `scripts/build-hooks.js` (server-beta bundle output)
- `src/npx-cli/commands/runtime.ts` (server-beta lifecycle commands)
- `src/npx-cli/commands/server.ts` (CLI routing)
- `src/services/worker-service.ts` (delegate `server-start|stop|restart|status` to sibling bundle)
- `tests/npx-cli-server-namespace.test.ts`
### Documentation References
- Parent plan, lines 469514: Phase 2 deliverables and verification checklist.
- `src/services/server/Server.ts`: existing route-composition style to copy.
- `src/services/infrastructure/ProcessManager.ts`: PID-file safety patterns.
### Verification Checklist
- All five Phase B steps pass.
- Worker lifecycle still works while server-beta is running, and vice versa.
- Two commits land cleanly with no `--amend` or force operations.
### Anti-Pattern Guards
- Do not import `WorkerService` from `src/server/runtime/`.
- Do not overload worker PID/port files.
- Do not boot worker as a background dependency of server-beta.
- Do not silently fall back from server-beta to worker.
## Phase C: Decide Doc Artifacts
### What To Decide
| File | Recommendation | Rationale |
|------|---------------|-----------|
| `PR_REORIENTATION_REPORT.md` | Use as PR body, then delete (or move to `docs/internal/`). | It's a snapshot, not durable docs. Useful for the PR reviewer; rots in-tree. |
| `AGENTS.md` | Read first, then either commit (if generally useful guidance) or move under `.scratch/`. | Decision depends on content. |
### Verification
- Final `git status` shows only intended doc artifacts (or none).
- `.scratch/` is gitignored if used.
### Anti-Pattern Guard
- Do not push `PR_REORIENTATION_REPORT.md` to main as a doc; it has a date and a HEAD SHA, it ages immediately.
## Phase D: Push and Open/Update PR
### What To Run
1. `git push -u origin bullmq-vs-bee-queue-for-claude-mem-observation-que`
2. `gh pr view --web` (if PR exists) or `gh pr create` with body sourced from `PR_REORIENTATION_REPORT.md`.
3. PR body must explicitly carve scope: "Includes Phase 1 + Phase 2 from `plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md`. Phases 313 are follow-ups on separate branches."
### Verification Checklist
- PR title is short (under 70 chars) and reflects scope: e.g., "Add Postgres storage + independent server-beta runtime (Phases 12)".
- PR body lists out-of-scope phases.
- CI is green.
### Anti-Pattern Guards
- Do not force-push to main.
- Do not merge without CI green.
## Phase E: Branch Closeout
Once the PR merges, this branch is done. Phase 3 (BullMQ-First Server Queue) starts on a fresh branch off main. Do not reuse this branch for Phase 3 work — keep the queue/runtime split visible in history.
## Final Verification (cross-phase)
Run after Phases AD:
```bash
git status # clean or only intended doc artifacts
git log --oneline origin/main..HEAD # 4e0fc77a + Phase 2 commits, no force-push markers
bun test tests/storage/postgres tests/server tests/npx-cli-server-namespace.test.ts
rg -n "WorkerService|services/worker-service|worker/http" src/server/runtime
rg -n "PendingMessageStore|SessionQueueProcessor" src/server/runtime
```
Expected:
- All three test paths green.
- Both greps return zero matches.
- Branch ready to merge.
## Decisions Locked
1. Phase 1 gate: orchestrator-managed deterministic checks (no reviewer agent).
2. `AGENTS.md` + `PR_REORIENTATION_REPORT.md`: **discard** before commit.
3. Scope: this branch ships Phases 1 + 2 + **3** (BullMQ-First Server Queue). Phase E becomes Phase 3 work, push moves to Phase F.
## Phase D (revised): Discard Untracked Doc Artifacts
```bash
rm AGENTS.md PR_REORIENTATION_REPORT.md
```
Verification: `git status` shows neither file.
## Phase E: Implement Phase 3 — BullMQ-First Server Queue
Source: parent plan lines 515570.
### What To Implement
- `src/server/jobs/types.ts` — job-shape types:
- `ServerGenerationJob` (base)
- `GenerateObservationsForEventJob`
- `GenerateObservationsForEventBatchJob`
- `GenerateSessionSummaryJob`
- `ReindexObservationJob`
- Every job carries `team_id`, `project_id`, `source_type`, `source_id`, `generation_job_id`. Event jobs add `agent_event_id`. Summary jobs add `server_session_id`. Reindex jobs add target observation ID or deterministic reindex scope ID.
- `src/server/jobs/job-id.ts` — deterministic, colon-free job IDs (port the SHA-256-safe pattern from `src/server/queue/BullMqObservationQueueEngine.ts`).
- `src/server/jobs/ServerJobQueue.ts` — thin wrapper around BullMQ `Queue`, `Worker`, `QueueEvents`. Use `autorun: false`, explicit `concurrency: 1` default per lane, and an `error` listener on every `Worker`.
- `src/server/jobs/outbox.ts` — durable outbox over `ObservationGenerationJobRepository`. Statuses: `queued`, `processing`, `completed`, `failed`, `cancelled`. Tracks attempts, last error, timestamps, and tenant/project/session IDs.
- Startup reconciliation:
- Re-enqueue rows in `queued` or stale `processing`.
- Skip rows already `completed`.
- Replace terminal BullMQ jobs before reusing deterministic IDs.
- Wire queue health into `/v1/info`, `/api/health`, and `claude-mem server status` via the existing runtime label hook.
- Activate the queue boundary in `ServerBetaService` (Phase 2 left it disabled). Provide a real adapter when `CLAUDE_MEM_QUEUE_ENGINE=bullmq` and `REDIS_URL` are present; keep the disabled adapter as the fallback.
### Documentation References
- BullMQ Workers: https://docs.bullmq.io/guide/workers
- BullMQ Concurrency: https://docs.bullmq.io/guide/workers/concurrency
- BullMQ Stalled Jobs: https://docs.bullmq.io/guide/jobs/stalled
- `src/server/queue/BullMqObservationQueueEngine.ts` — copy deterministic job-ID + Redis health patterns; do **not** copy the worker-iterator compatibility shape.
- `src/server/queue/redis-config.ts` — Valkey/Redis health checks.
- `src/storage/postgres/generation-jobs.ts` — outbox repository (already committed in 4e0fc77a).
### Verification Checklist
Unit tests under `tests/server/jobs/`:
- `job-id.test.ts` — deterministic IDs, no colons, stable across runs, content-derived.
- `server-job-queue.test.ts` — Queue/Worker lifecycle, `error` listener attached, concurrency honored, autorun false.
- `outbox.test.ts` — duplicate enqueue suppression, terminal job replacement, status transitions, attempt counting.
Integration tests under `tests/server/queue-bootstrap/`:
- Start `ServerBetaService` with Postgres + Valkey + queue boundary enabled.
- Insert outbox rows directly through `ObservationGenerationJobRepository`.
- Enqueue fake jobs; restart before fake processing completes.
- Assert reconciliation re-enqueues exactly once and outbox status reaches `completed` exactly once.
- Assert Redis-down fails Server beta startup when `CLAUDE_MEM_QUEUE_ENGINE=bullmq`; no silent fallback to SQLite.
Greps:
```bash
rg -n "Bull(MQ|Mq).*\.add\(" src/server/jobs # uses BullMQ Queue.add
rg -n "autorun" src/server/jobs # workers explicitly set autorun
rg -n "on\(['\"]error" src/server/jobs # error listener attached
rg -n ":job:|:obs:" src/server/jobs # NO colons in deterministic IDs
```
The colon-grep must return zero matches.
### Anti-Pattern Guards
- Do not treat BullMQ completed/failed state as canonical history — Postgres outbox is canonical.
- Do not require event-route wiring or provider generation here (Phase 4 territory).
- Do not allow duplicate processor side effects on retry — keep observation writes idempotent by deterministic key.
- Do not use BullMQ Pro-only features (groups).
- Do not leave pending work only in Redis.
- Do not silently fall back from BullMQ to SQLite when `CLAUDE_MEM_QUEUE_ENGINE=bullmq` is set.
### Commit Layout
Two commits:
1. **`feat(server-beta): add BullMQ job queue primitives`**
- `src/server/jobs/types.ts`
- `src/server/jobs/job-id.ts`
- `src/server/jobs/ServerJobQueue.ts`
- `src/server/jobs/outbox.ts`
- `tests/server/jobs/*.test.ts`
2. **`feat(server-beta): activate queue boundary in runtime service`**
- `src/server/runtime/ServerBetaService.ts` (queue boundary wiring)
- `src/server/runtime/create-server-beta-service.ts` (boundary selection from env)
- `src/server/runtime/types.ts` (active queue manager interface)
- Health surface updates in `/v1/info` and `/api/health` if not already covered by Phase 2 runtime label.
- `tests/server/queue-bootstrap/*.test.ts`
## Phase F: Push and Open/Update PR
```bash
git push -u origin bullmq-vs-bee-queue-for-claude-mem-observation-que
gh pr view --web # if PR exists
# else:
gh pr create --title "Server-beta: Postgres storage + independent runtime + BullMQ queue (Phases 13)"
```
PR body must list:
- Scope: Phases 1, 2, 3 of `plans/2026-05-07-server-beta-independent-bullmq-observation-runtime.md`.
- Out of scope: Phases 413 (event-to-job pipeline, provider extraction, hook routing, MCP, compat, Docker, team auth, observability, final verification).
### Verification Checklist
- `git status` clean.
- `git log --oneline origin/main..HEAD` shows all expected commits, no force-push markers.
- CI green.
## Final Cross-Phase Verification
```bash
git status # clean
bun test tests/storage/postgres tests/server tests/npx-cli-server-namespace.test.ts
rg -n "WorkerService|services/worker-service|worker/http" src/server/runtime # zero
rg -n "PendingMessageStore|SessionQueueProcessor" src/server/runtime src/server/jobs # zero
```
@@ -0,0 +1,987 @@
# Claude-Mem 13 Server Beta: Independent BullMQ Observation Runtime
Status: implementation plan
Date: 2026-05-07
Release target: claude-mem 13 Server (beta)
Relationship to prior plans:
- Extends `plans/2026-05-07-claude-mem-server-apache-bullmq-team-auth.md`.
- Supersedes the worker-parity parts of `plans/2026-05-07-claude-mem-13-server-beta-full-worker-parity.md` where that plan allowed Server beta to wrap/copy `WorkerService`.
- Keeps the existing worker in place, but makes Server beta a fully independent runtime, not a facade over worker internals.
## Executive Decision
Server beta must own its runtime end to end:
```text
REST/MCP/hooks -> Server beta HTTP/API layer -> BullMQ observation jobs -> provider generation -> server storage/search
```
The worker remains the stable legacy runtime, but Server beta must not depend on `WorkerService`, worker HTTP routes, worker queue consumers, or worker process lifecycle to generate observations.
Server beta should use BullMQ/Valkey as its canonical queue and Postgres as its canonical observation store. SQLite remains the legacy worker/local compatibility store only. Redis/Valkey is runtime infrastructure for jobs, retries, concurrency, and observability, not the source of truth for observations.
## Terminology Decision
Claude-mem's domain object is an **observation**. Server beta must preserve that wording in user-facing APIs, docs, jobs, storage names, tests, logs, and implementation plans.
Use "memory" only for legacy compatibility names that already exist in worker-era code or for external library/API concepts that cannot be renamed cleanly. New Server beta/Postgres concepts should be named around observations:
- `observations`, not `memory_items`
- `observation_sources`, not `memory_sources`
- `ObservationRepository`, not `MemoryItemsRepository`
- `GenerateObservationsForEventJob`, not generic memory generation
- `/v1/observations` and observation-focused MCP tools as the canonical surface
If any compatibility endpoint still uses `/v1/memories`, it should be treated as an alias over observations, not the canonical Server beta model.
## Phase 0: Documentation Discovery
### Local Sources Read
- `plans/2026-05-07-claude-mem-server-apache-bullmq-team-auth.md`
- `plans/2026-05-07-claude-mem-13-server-beta-full-worker-parity.md`
- `/Users/alexnewman/Downloads/claude-mem-handoff-docs/claude-mem-server-plan.md`
- `src/server/routes/v1/ServerV1Routes.ts`
- `src/server/queue/BullMqObservationQueueEngine.ts`
- `src/server/queue/ObservationQueueEngine.ts`
- `src/services/worker-service.ts`
- `src/services/worker/SessionManager.ts`
- `src/services/worker/agents/ResponseProcessor.ts`
- `src/services/worker/ClaudeProvider.ts`
- `src/services/worker/GeminiProvider.ts`
- `src/services/worker/OpenRouterProvider.ts`
- `src/services/worker/http/shared.ts`
- `src/storage/sqlite/agent-events.ts`
- `src/storage/sqlite/memory-items.ts`
- `src/core/schemas/agent-event.ts`
- `src/core/schemas/memory-item.ts`
- `scripts/e2e-server-beta-docker.sh`
- `docker/e2e/server-beta-e2e.mjs`
### External Docs Read
- BullMQ Workers: https://docs.bullmq.io/guide/workers
- BullMQ Worker Concurrency: https://docs.bullmq.io/guide/workers/concurrency
- BullMQ Stalled Jobs: https://docs.bullmq.io/guide/jobs/stalled
- Better Auth Express integration: https://better-auth.com/docs/integrations/express
### Concrete Findings
- The current `/v1` server route stores supplied events and direct observation records under legacy "memory" route/repository names:
- `src/server/routes/v1/ServerV1Routes.ts` registers `POST /v1/events`, `POST /v1/events/batch`, and `POST /v1/memories`.
- Those routes call `AgentEventsRepository.create(...)` and `MemoryItemsRepository.create(...)`.
- They do not currently enqueue a provider generation job.
- The current AI observation generation path is worker-owned:
- `src/services/worker/SessionManager.ts` consumes queued messages through `getMessageIterator(...)`.
- `src/services/worker-service.ts` starts provider sessions through `startSessionProcessor(...)`.
- `src/services/worker/agents/ResponseProcessor.ts` parses provider XML with `parseAgentXml(...)` and writes observations through `sessionStore.storeObservations(...)`.
- The existing v2 parity plan names `Claude/Gemini/OpenRouter providers`, session ingest routes, queue semantics, and hook routing as parity requirements, but it does not explicitly require `/v1/events` to generate observations.
- BullMQ official docs establish the primitives Server beta should use directly:
- `Worker` processes jobs and moves successful jobs to completed or thrown jobs to failed.
- BullMQ workers should attach an `error` listener.
- Workers support `autorun: false`.
- Workers support concurrency via the worker options object.
- Multiple workers are the recommended way to improve availability.
- Active jobs can stall and be retried when workers stop renewing locks.
- Better Auth Express docs require the auth handler to mount before `express.json()` and use `/api/auth/*splat` for Express 5.
### Allowed APIs And Patterns
- Copy Express pre-body route mounting from `src/services/server/Server.ts` plus Better Auth docs.
- Copy API-key auth from `src/server/middleware/auth.ts` and `src/server/auth/api-key-service.ts`.
- Copy repository behavior where useful, but implement Server beta repositories against Postgres; do not reuse worker legacy `SessionStore` as the server observation model.
- Copy provider request construction from `src/services/worker/ClaudeProvider.ts`, `GeminiProvider.ts`, and `OpenRouterProvider.ts`, then move shared logic into `src/server/generation` or `src/core/generation`.
- Copy XML parsing from `src/sdk/parser.ts` and current post-processing rules from `src/services/worker/agents/ResponseProcessor.ts`.
- Use BullMQ `Queue`, `Worker`, and `QueueEvents` directly for Server beta generation queues.
- Keep Valkey/Redis health checks from `src/server/queue/redis-config.ts` and existing Docker E2E setup.
### Anti-Pattern Guards
- Do not make Server beta call `new WorkerService()`.
- Do not make Server beta depend on worker HTTP route classes for generation.
- Do not make `/v1` a write-only event archive while claiming Server beta generates observations.
- Do not use the legacy SQLite pending-message queue for Server beta generation.
- Do not store canonical observation records in Redis.
- Do not remove or destabilize the existing worker.
- Do not silently fall back from explicit Server beta BullMQ mode to SQLite.
- Do not mount Better Auth after `express.json()`.
## Target Architecture
### Runtime Separation
```text
src/services/worker-service.ts
Legacy worker runtime. Stable compatibility path. May import shared core pieces later.
src/server/runtime/ServerBetaService.ts
Independent server runtime. Owns HTTP server, BullMQ queues, provider generation workers,
server storage repositories, auth, health, and Docker deployment.
```
### Server Beta Flow
```text
POST /v1/events
POST /v1/events/batch
Claude Code hook routed to Server beta
MCP observation_record_* tool
|
v
AgentEventsRepository transaction
|
v
ObservationGenerationJobRepository outbox row
|
v
BullMQ Queue.add(...)
|
v
BullMQ Worker processor
|
v
ProviderObservationGenerator
|
v
parseAgentXml / structured parser
|
v
ObservationRepository.create(...) + ObservationSourcesRepository.addSource(...)
|
v
QueueEvents/SSE/audit/search index update
```
## Phase 1: Postgres Observation Storage Foundation
### What To Implement
- Add Server beta Postgres configuration:
- add package dependencies `pg` and `@types/pg` to the Node/Bun TypeScript package manifest used by this repo;
- centralize Postgres storage code under:
- `src/storage/postgres/config.ts` for environment parsing, pool sizing, timeouts, and SSL settings;
- `src/storage/postgres/pool.ts` for the shared `pg.Pool` factory, health check, transactions, and graceful shutdown;
- `src/storage/postgres/schema.ts` for migration/bootstrap SQL and schema version constants;
- `src/storage/postgres/index.ts` for exports used by Server beta runtime wiring;
- `CLAUDE_MEM_SERVER_DATABASE_URL`;
- connection pool size and timeout settings;
- startup validation that fails Server beta when Postgres is required but unavailable;
- graceful shutdown that drains and closes the Postgres pool.
- Add a migration/bootstrap helper for Server beta storage:
- creates required schemas/tables/indexes;
- records applied migration versions;
- is safe to run repeatedly on startup and in tests.
- Define canonical Postgres tables:
- `teams`;
- `projects`;
- `team_members`;
- `api_keys`;
- `audit_log`;
- `server_sessions`;
- `agent_events`;
- `observations`;
- `observation_sources`;
- `observation_generation_jobs`;
- `observation_generation_job_events`.
- Implement the initial schema contract explicitly in Phase 1 migrations. Column names can be refined only if all repository contracts and tests are updated in the same phase:
```sql
CREATE TABLE teams (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE projects (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (id, team_id)
);
CREATE TABLE team_members (
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
user_id TEXT NOT NULL,
role TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (team_id, user_id)
);
CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
key_hash TEXT NOT NULL UNIQUE,
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
actor_id TEXT NOT NULL,
scopes JSONB NOT NULL DEFAULT '[]'::jsonb,
revoked_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (project_id IS NULL OR team_id IS NOT NULL),
FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE CASCADE
);
CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
actor_id TEXT,
api_key_id TEXT REFERENCES api_keys(id) ON DELETE SET NULL,
action TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT,
details JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (project_id IS NULL OR team_id IS NOT NULL),
FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE SET NULL
);
CREATE TABLE server_sessions (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
external_session_id TEXT,
content_session_id TEXT,
agent_id TEXT,
agent_type TEXT,
platform_source TEXT,
generation_status TEXT NOT NULL DEFAULT 'idle',
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
ended_at TIMESTAMPTZ,
last_generated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (project_id, external_session_id),
FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE CASCADE
);
CREATE TABLE agent_events (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
server_session_id TEXT REFERENCES server_sessions(id) ON DELETE SET NULL,
source_adapter TEXT NOT NULL,
source_event_id TEXT,
idempotency_key TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
occurred_at TIMESTAMPTZ NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (idempotency_key),
UNIQUE (id, project_id, team_id),
FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE CASCADE
);
CREATE TABLE observation_generation_jobs (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
agent_event_id TEXT REFERENCES agent_events(id) ON DELETE CASCADE,
source_type TEXT NOT NULL CHECK (source_type IN ('agent_event', 'session_summary', 'observation_reindex')),
source_id TEXT NOT NULL,
server_session_id TEXT REFERENCES server_sessions(id) ON DELETE SET NULL,
job_type TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('queued', 'processing', 'completed', 'failed', 'cancelled')),
idempotency_key TEXT NOT NULL UNIQUE,
bullmq_job_id TEXT UNIQUE,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
next_attempt_at TIMESTAMPTZ,
locked_at TIMESTAMPTZ,
locked_by TEXT,
completed_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
last_error JSONB,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (team_id, project_id, source_type, source_id, job_type),
CHECK (
(source_type = 'agent_event' AND agent_event_id IS NOT NULL AND source_id = agent_event_id)
OR
(source_type = 'session_summary' AND agent_event_id IS NULL AND server_session_id IS NOT NULL AND source_id = server_session_id)
OR
(source_type = 'observation_reindex' AND agent_event_id IS NULL)
),
FOREIGN KEY (agent_event_id, project_id, team_id) REFERENCES agent_events(id, project_id, team_id) ON DELETE CASCADE,
FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE CASCADE
);
CREATE TABLE observations (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
server_session_id TEXT REFERENCES server_sessions(id) ON DELETE SET NULL,
kind TEXT NOT NULL DEFAULT 'observation',
content TEXT NOT NULL,
content_search TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
generation_key TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
embedding JSONB,
created_by_job_id TEXT REFERENCES observation_generation_jobs(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (team_id, project_id, generation_key),
FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id) ON DELETE CASCADE
);
CREATE TABLE observation_sources (
id TEXT PRIMARY KEY,
observation_id TEXT NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
agent_event_id TEXT REFERENCES agent_events(id) ON DELETE CASCADE,
generation_job_id TEXT REFERENCES observation_generation_jobs(id) ON DELETE SET NULL,
source_type TEXT NOT NULL CHECK (source_type IN ('agent_event', 'session_summary', 'observation_reindex', 'manual')),
source_id TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (observation_id, source_type, source_id),
UNIQUE (source_type, source_id, generation_job_id, observation_id),
CHECK (
(source_type = 'agent_event' AND agent_event_id IS NOT NULL AND source_id = agent_event_id)
OR
(source_type <> 'agent_event' AND agent_event_id IS NULL)
)
);
CREATE TABLE observation_generation_job_events (
id TEXT PRIMARY KEY,
generation_job_id TEXT NOT NULL REFERENCES observation_generation_jobs(id) ON DELETE CASCADE,
event_type TEXT NOT NULL CHECK (event_type IN ('queued', 'enqueued', 'processing', 'retry_scheduled', 'completed', 'failed', 'cancelled')),
status_after TEXT NOT NULL CHECK (status_after IN ('queued', 'processing', 'completed', 'failed', 'cancelled')),
attempt INTEGER NOT NULL DEFAULT 0,
details JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_agent_events_project_session ON agent_events(project_id, server_session_id, occurred_at);
CREATE INDEX idx_projects_team ON projects(team_id, id);
CREATE INDEX idx_agent_events_team_project ON agent_events(team_id, project_id, occurred_at);
CREATE INDEX idx_observations_project_session ON observations(project_id, server_session_id, created_at);
CREATE INDEX idx_observations_team_project ON observations(team_id, project_id, created_at);
CREATE INDEX idx_observations_content_search ON observations USING GIN (content_search);
CREATE INDEX idx_observation_sources_event ON observation_sources(agent_event_id);
CREATE INDEX idx_observation_sources_source ON observation_sources(source_type, source_id);
CREATE INDEX idx_observation_jobs_status_next_attempt ON observation_generation_jobs(status, next_attempt_at, created_at);
CREATE INDEX idx_observation_jobs_team_project ON observation_generation_jobs(team_id, project_id, status, created_at);
CREATE INDEX idx_observation_jobs_event ON observation_generation_jobs(agent_event_id);
CREATE INDEX idx_observation_jobs_source ON observation_generation_jobs(source_type, source_id);
CREATE INDEX idx_observation_job_events_job_created ON observation_generation_job_events(generation_job_id, created_at);
CREATE INDEX idx_audit_log_scope_created ON audit_log(project_id, team_id, created_at);
```
- Define event/outbox relationships:
- `agent_events` is the canonical Postgres table for raw ingested agent events and their project/session/team ownership;
- every project is owned by exactly one team through `projects.team_id`; Server beta has no unowned/default project mode in the Postgres canonical store;
- repositories and routes must resolve project ownership from `projects.team_id`, require the caller's team/API-key scope to match it, and reject any request body or repository write where `team_id` disagrees with the project's owner;
- project-scoped rows that carry both `project_id` and `team_id` must use FK-backed ownership validation through `FOREIGN KEY (project_id, team_id) REFERENCES projects(id, team_id)`;
- `observation_generation_jobs.source_type` and `observation_generation_jobs.source_id` identify the durable source of work for event, summary, and reindex jobs without overloading event-only columns;
- event generation jobs use `source_type = 'agent_event'`, `source_id = agent_event_id`, and a non-null `agent_event_id` FK to the source `agent_events` row being processed;
- session summary jobs use `source_type = 'session_summary'`, `source_id = server_session_id`, and `agent_event_id = NULL`;
- reindex jobs use `source_type = 'observation_reindex'`, `source_id` set to the target observation ID or deterministic reindex scope ID, and `agent_event_id = NULL`;
- repositories must validate non-event `source_id` ownership before job insert: session summary jobs must load the `server_sessions` row under the same `project_id`/`team_id`, and observation reindex jobs must load the target observation or documented reindex scope under the same `project_id`/`team_id`;
- `observation_generation_job_events` records durable lifecycle/outbox events for each observation generation job, including enqueue, processing, retry, completion, and failure state changes;
- `observation_generation_job_events` may reference `agent_events` through its job relationship, but it is not a replacement for `agent_events` and must not store raw event payloads as the canonical event record.
- Define outbox status and idempotency rules:
- `observation_generation_jobs.status` is constrained to `queued`, `processing`, `completed`, `failed`, or `cancelled`;
- legal lifecycle is `queued -> processing -> completed`, `queued -> processing -> failed`, `queued -> cancelled`, and retry transitions from stale/failed retryable work back to `queued` only when `attempts < max_attempts`;
- `attempts` increments only when a worker transitions a job to `processing`;
- `next_attempt_at` gates retry/reconciliation eligibility;
- `locked_at` and `locked_by` are set while a worker owns processing and are cleared or superseded on completion, failure, cancellation, or stale-lock recovery;
- `completed_at`, `failed_at`, and `cancelled_at` are terminal timestamps and exactly one may be non-null for terminal jobs;
- `agent_events.source_event_id` is optional adapter metadata only and must not be used as the sole idempotency authority;
- `agent_events.idempotency_key` is required and deterministic: when `source_event_id` is present, derive it from `team_id`, `project_id`, `source_adapter`, and `source_event_id`; when omitted, derive it from `team_id`, `project_id`, `source_adapter`, `server_session_id`, `event_type`, `occurred_at`, and a canonical JSON hash of `payload`;
- `UNIQUE (idempotency_key)` on `agent_events` suppresses duplicate ingestion for native event IDs, batch imports, and clients with omitted source event IDs;
- job `idempotency_key` must be deterministic from `team_id`, `project_id`, `source_type`, `source_id`, and `job_type`, and `UNIQUE (idempotency_key)` suppresses duplicate outbox rows;
- `UNIQUE (team_id, project_id, source_type, source_id, job_type)` guarantees one source/job relationship per generation kind within the owning project/team scope across event, summary, and reindex jobs;
- `bullmq_job_id` must be deterministic and unique when present so reconciliation can safely re-add or replace terminal BullMQ jobs;
- `observations.generation_key` is nullable for direct/manual observations and required for provider/generated observations;
- provider-generated `generation_key` must be deterministic as `generation:v1:{generation_job_id}:{parsed_observation_index}:{canonical_content_fingerprint}` where the content fingerprint is computed after parser normalization and before persistence;
- `UNIQUE (team_id, project_id, generation_key)` on `observations` is the primary retry idempotency guard within the owning project/team scope: retrying the same job and parsed observation must upsert/reload the existing observation instead of creating a new row;
- `observations.created_by_job_id` is a nullable foreign key to `observation_generation_jobs(id)`; provider-generated observations must set it to the durable Postgres generation job that created the observation;
- `observation_sources.generation_job_id` is a nullable foreign key to `observation_generation_jobs(id)`; generated observation source rows must set it when the observation came from a generation job;
- `observation_sources.source_type` and `observation_sources.source_id` mirror the job source model so generated observations can link to events, session summaries, reindex scopes, or manual/direct sources without ambiguous nullable uniqueness;
- `UNIQUE (observation_id, source_type, source_id)` guarantees a source cannot be linked to the same observation more than once;
- generated observation writes must also be idempotent through `observation_sources`: the same `source_type`, `source_id`, `generation_job_id`, and `observation_id` relationship must not be inserted twice;
- mutation APIs that touch observation sources, generation job status, or generation job lifecycle events must require `project_id` and `team_id` and include them in the mutating SQL predicate before changing rows;
- `ObservationRepository.search(...)` must use the generated `observations.content_search` `tsvector`, the GIN index on `content_search`, and `websearch_to_tsquery('english', query)` for scoped full-text search;
- provider retries must reload the Postgres job row and the authoritative source row before side effects; for event jobs that source row is `agent_events`, for summary jobs it is `server_sessions`, and for reindex jobs it is the target observation or documented reindex scope. BullMQ payload data is advisory execution data, not authority.
- Define repository interfaces and Postgres implementations:
- `ProjectRepository`;
- `TeamRepository`;
- `ObservationRepository`;
- `ObservationSourcesRepository`;
- `ObservationGenerationJobRepository`;
- `ObservationGenerationJobEventsRepository` for durable lifecycle/outbox events such as queued, enqueued, processing, retry scheduled, completed, failed, and cancelled;
- `AgentEventsRepository` backed by the Server beta Postgres connection.
- Keep legacy names as adapters only:
- existing `memory_items` data can be migrated or viewed as observations;
- existing `MemoryItemsRepository` remains a current-code compatibility reference, not the Server beta repository contract.
- Add test helpers that skip Postgres-backed integration tests when no test Postgres URL is configured.
### Documentation References
- Copy current repository behavior and field validation from existing storage code, but implement the canonical Server beta storage in Postgres.
- Copy compatible field constraints from `src/core/schemas/memory-item.ts` only to preserve legacy import/alias behavior; new Server beta schemas should be named around observations.
- Copy migration idempotency patterns from existing storage bootstrap code where applicable.
- Use prior SQLite storage decisions as superseded context only where they conflict with Postgres as canonical Server beta storage.
### Verification Checklist
- Unit tests for repository interfaces using fake adapters where useful.
- Postgres integration tests for:
- migration/bootstrap idempotency;
- `ProjectRepository.create(...)` requires a valid `team_id`, lookup returns the owning team, and project-scoped repository writes reject mismatched `team_id`/`project_id` pairs;
- `ObservationRepository.create(...)` and lookup by project/session/team;
- `ObservationRepository.search(...)` uses the generated `content_search` column with the GIN-backed `websearch_to_tsquery` path and returns only rows for the requested project/team scope;
- `ObservationSourcesRepository.addSource(...)` idempotency;
- `ObservationSourcesRepository.addSource(...)` requires project/team scope and rejects wrong-scope observation/source/job relationships without inserting rows;
- `AgentEventsRepository.create(...)`, batch insert/reload, lookup by project/session/team, deterministic `idempotency_key` generation when `source_event_id` is present, and deterministic `idempotency_key` fallback when `source_event_id` is omitted;
- ingesting the same event twice with omitted source event IDs must not create duplicate `agent_events` rows and must not duplicate generation jobs;
- `ObservationGenerationJobRepository` create/status transition/reload and duplicate-job suppression for event, session summary, and reindex jobs using deterministic `source_type`, `source_id`, and `idempotency_key`;
- `ObservationGenerationJobRepository.transitionStatus(...)` requires project/team scope in both the conditional update and fallback reload and must not mutate rows when called with the wrong scope;
- generated observation retry idempotency through `observations.generation_key`, including retrying the same job and parsed observation index/content without creating a duplicate observation;
- `ObservationGenerationJobEventsRepository` lifecycle append/list tests and outbox event linking through `observation_generation_job_events`;
- `ObservationGenerationJobEventsRepository.append(...)` requires project/team scope and appends only when the referenced job belongs to that project/team.
- Integration tests skip cleanly with an explicit skip reason when no Postgres test URL is configured.
- `rg -n "MemoryItemsRepository" src/server`
- new Server beta implementation source must not use legacy repository contracts except in explicit compatibility adapters.
### Anti-Pattern Guards
- Do not make SQLite the canonical Server beta observation store.
- Do not add new Server beta tables named `memory_items` or new repositories named `MemoryItemsRepository`.
- Do not let BullMQ or Redis/Valkey be the source of truth for observations or outbox history.
- Do not hide missing Postgres by silently falling back to worker SQLite.
## Phase 2: Define Server Runtime Boundary
### What To Implement
- Add `src/server/runtime/ServerBetaService.ts`.
- Add `src/server/runtime/create-server-beta-service.ts`.
- Add `src/server/runtime/types.ts` for the service graph:
- Postgres connection pool;
- initialized Phase 1 storage bootstrap/migration status;
- auth mode;
- queue manager boundary as an inert interface with a disabled/no-op adapter;
- generation worker manager boundary as an inert interface with a disabled/no-op adapter;
- provider registry boundary as an inert interface with a disabled/no-op adapter;
- SSE/event broadcaster boundary as an inert interface with a disabled/no-op adapter;
- server storage repositories.
- Phase 2 creates lifecycle/runtime boundaries only. It must not implement BullMQ queue processing, provider-backed observation generation, generation workers, or SSE broadcasting; actual queue manager implementation starts in Phase 3, provider/generation implementation starts in later generation phases, and the real event broadcaster is wired only when its phase requires it.
- Route `claude-mem server start|stop|restart|status` to `ServerBetaService`, not `WorkerService`.
- Keep worker commands routed to `WorkerService`.
- Add separate runtime state files:
- `.server-beta.pid`
- `.server-beta.port`
- `.server-beta.runtime.json`
- Add `/v1/info.runtime = "server-beta"` and `/api/health.runtime = "server-beta"` in Server beta.
### Documentation References
- Copy the route-handler composition style from `src/services/server/Server.ts`.
- Copy only lifecycle primitives from `src/services/worker-service.ts`; do not copy the full worker class.
- Copy PID-file safety patterns from `src/services/infrastructure/ProcessManager.ts`.
- Use the prior parity plan section "Phase 2: Independent Server Beta Lifecycle" as the baseline, but strengthen it: independent means no `WorkerService` dependency.
### Verification Checklist
- `rg -n "WorkerService|services/worker-service|worker/http" src/server src/npx-cli/commands/server.ts src/npx-cli/commands/worker.ts`
- Server runtime source must not import or instantiate `WorkerService`.
- `npx claude-mem server status` reports server-beta state independently of worker state.
- Worker `start|stop|status` commands still work.
- Server beta can start while worker is stopped.
- Server beta can stop without touching worker.
### Anti-Pattern Guards
- Do not overload worker PID/port files.
- Do not implement Server beta by booting worker in the background.
- Do not use worker health as the server health source.
## Phase 3: BullMQ-First Server Queue
### What To Implement
- Add `src/server/jobs/types.ts`:
- `ServerGenerationJob`
- `GenerateObservationsForEventJob`
- `GenerateObservationsForEventBatchJob`
- `GenerateSessionSummaryJob`
- `ReindexObservationJob`
- every job type must carry `team_id`, `project_id`, `source_type`, `source_id`, and `generation_job_id`; event jobs additionally carry `agent_event_id`, summary jobs carry `server_session_id`, and reindex jobs carry the target observation ID or deterministic reindex scope ID.
- Add `src/server/jobs/ServerJobQueue.ts` wrapping BullMQ `Queue`, `Worker`, and `QueueEvents`.
- Add `src/server/jobs/job-id.ts` for deterministic, colon-free job IDs.
- Add `src/server/jobs/outbox.ts` using `ObservationGenerationJobRepository`:
- durable rows live in `observation_generation_jobs`;
- source identity lives in `source_type`/`source_id`; lifecycle events live in `observation_generation_job_events`;
- status fields: `queued`, `processing`, `completed`, `failed`, `cancelled`;
- attempts, last error, timestamps, project/session/team IDs.
- Make the outbox the durable source of "what should be generated"; BullMQ is the execution transport.
- Add startup reconciliation:
- enqueue outbox rows in `queued` or stale `processing`;
- do not enqueue rows for already completed jobs;
- remove or replace terminal BullMQ jobs before deterministic job ID reuse.
- Add queue health to `/v1/info`, `/api/health`, and `claude-mem server status`.
### Documentation References
- BullMQ Workers docs: use `new Worker(queueName, async job => ...)`, attach `worker.on('error', ...)`, and use worker events for completion/failure.
- BullMQ Concurrency docs: use explicit worker `concurrency`, default conservative value `1` per provider/session lane, configurable later.
- BullMQ Stalled Jobs docs: design jobs as idempotent because active jobs may be moved back to waiting.
- Existing `src/server/queue/BullMqObservationQueueEngine.ts` has tested deterministic job IDs and Redis health wiring; copy its safe ID and health patterns, not its worker-iterator compatibility shape.
### Verification Checklist
- Unit tests for:
- job ID stability;
- duplicate enqueue suppression;
- terminal job replacement;
- outbox restart reconciliation;
- failed job retained in Postgres and BullMQ;
- Redis unavailable fails Server beta startup when BullMQ is selected.
- Integration tests with a fake processor:
- start Server beta queue manager + Postgres + Valkey;
- create outbox rows directly through `ObservationGenerationJobRepository`;
- enqueue fake jobs;
- restart before fake processing completes;
- assert reconciliation resumes jobs and marks the outbox exactly once.
### Anti-Pattern Guards
- Do not treat BullMQ completed/failed state as canonical history.
- Do not require event route wiring or provider generation for this phase to pass.
- Do not allow duplicate processor side effects on retry; later observation writes must be idempotent by deterministic observation generation key and source/job ID.
- Do not use BullMQ Pro-only groups.
- Do not leave pending work only in Redis.
## Phase 4: Server-Owned Event-To-Generation-Job Pipeline
### What To Implement
- Change `POST /v1/events` and `POST /v1/events/batch` to:
1. validate auth and project/team scope;
2. insert events transactionally;
3. create server outbox generation jobs in the same transaction;
4. enqueue corresponding BullMQ jobs after commit.
- Add opt-in request control:
- default: enqueue generation asynchronously;
- `?generate=false`: store event only;
- `?wait=true`: if implemented in this phase, wait only for bounded queue acceptance or job status and return queued/accepted/job status. It must not claim observations were generated.
- Add `GET /v1/jobs/:id` for generation status.
- Keep `POST /v1/memories` only as a compatibility alias for manual/direct observation insertion. It must not call the generator.
### Documentation References
- Copy current REST validation/auth style from `src/server/routes/v1/ServerV1Routes.ts`.
- Copy atomic write approach from the existing fixed `/v1/events/batch` transaction.
- Copy JSON serde and repository behavior from current storage implementations while implementing Postgres-backed Server beta repositories.
- Copy Docker E2E style from `docker/e2e/server-beta-e2e.mjs`.
### Verification Checklist
- `POST /v1/events` returns `event` and `generationJob`.
- `POST /v1/events?generate=false` returns no generation job.
- Event insert and outbox generation-job creation are committed transactionally: no event without its required outbox/job row, and no outbox/job row without its event link.
- A successful event request enqueues the corresponding BullMQ job after commit.
- Mixed-project batch pre-validation rejects the request before any event, outbox/job, or BullMQ enqueue side effect occurs.
- `POST /v1/events?wait=true`, if implemented, returns queued/accepted/job status only; it does not return generated observation IDs or imply provider generation completed.
- Project-scoped API key cannot enqueue generation for another project.
### Anti-Pattern Guards
- Do not call worker `/api/sessions/observations`.
- Do not make `/v1/events` depend on Claude Code-specific hook payload shape.
- Do not generate observations inside the HTTP request without queueing first.
- Do not require provider generation, generated observation IDs, or generated observation duplicate checks for Phase 4 verification.
## Phase 5: Extract Provider Generation Without Worker Coupling
### What To Implement
- Add `src/server/generation/ProviderObservationGenerator.ts`.
- Add provider adapters under `src/server/generation/providers/`:
- `ClaudeObservationProvider`
- `GeminiObservationProvider`
- `OpenRouterObservationProvider`
- Extract common prompt construction and provider-call code from worker providers into reusable modules.
- Keep worker providers as compatibility wrappers that can call the shared provider adapters later.
- Add `src/server/generation/processGeneratedResponse.ts`:
- parse response with `parseAgentXml(...)`;
- map parsed observations to a new server observation create schema/repository input;
- store via `ObservationRepository`;
- link sources to event/job IDs;
- update outbox status;
- audit observation generation.
- Add `GET /v1/events/:id/observations` to inspect generated observations for an event.
- Add `observation_sources.sourceType = "agent_event"` support if not already present, or add a server-specific source table mapping event IDs to observation IDs.
- Add a stable server generation prompt:
- input: list of `AgentEvent` records plus project/session metadata;
- output: XML or structured JSON accepted by existing parser;
- include `<private>` skip behavior.
### Documentation References
- Copy parse/store behavior from `src/services/worker/agents/ResponseProcessor.ts`.
- Copy provider-specific auth and request construction from:
- `src/services/worker/ClaudeProvider.ts`
- `src/services/worker/GeminiProvider.ts`
- `src/services/worker/OpenRouterProvider.ts`
- Copy compatible field constraints from the existing legacy observation schema in `src/core/schemas/memory-item.ts`, but expose the Server beta create contract as an observation schema.
- Keep provider error classification semantics from `src/services/worker/provider-errors.ts`.
### Verification Checklist
- Unit tests using fake provider:
- valid XML yields an observation;
- skip/private response marks job completed with no observation;
- malformed response fails job or marks retryable according to policy;
- generated observation preserves project/session/source metadata.
- `POST /v1/events?wait=true` returns generated observation IDs only after Phase 5 provider generation and persistence are wired and the job finishes within timeout.
- Replaying the same event/job after restart does not duplicate generated observations.
- Provider classification tests still pass.
- Worker response processor tests still pass.
- `rg -n "services/worker/(ClaudeProvider|GeminiProvider|OpenRouterProvider|agents/ResponseProcessor)" src/server`
- must return no direct imports from Server beta generation.
### Anti-Pattern Guards
- Do not import `WorkerRef`, `ActiveSession`, or legacy worker session types into server generation.
- Do not mutate legacy `SessionStore` tables from Server beta generation.
- Do not make server provider code assume a Claude Code transcript.
## Phase 6: Server Session Semantics Independent Of Worker Sessions
### What To Implement
- Treat `server_sessions` as the canonical Server beta session model.
- Add fields needed for generation:
- `contentSessionId` or generic external session ID;
- `agentId`;
- `agentType`;
- `platformSource`;
- `generationStatus`;
- `lastGeneratedAtEpoch`.
- Add `ServerSessionRuntimeRepository` helpers:
- get active session;
- list unprocessed events;
- mark generation started/completed/failed.
- Add session-level generation policies:
- generate per event;
- batch small event bursts by short debounce window;
- generate summary on `/v1/sessions/:id/end`.
- Make this policy configurable with server settings.
### Documentation References
- Copy server session repository behavior from current storage code while implementing the Server beta session repository against Postgres.
- Copy queue idle/claim semantics from current BullMQ tests only where they serve idempotency and retry behavior.
- Copy current summary behavior from worker providers, but store summaries as observation records with kind/type `"summary"`.
### Verification Checklist
- Starting/ending a server session does not touch legacy worker session rows except through explicit migration/import code.
- Ending a session enqueues a summary generation job.
- Re-ending a session is idempotent.
- Session-scoped API keys remain project-scoped.
### Anti-Pattern Guards
- Do not require a legacy worker session ID to generate Server beta observations.
- Do not use worker `ActiveSession` as the server runtime state object.
## Phase 7: Hook Routing To Server Beta Without Worker Dependency
### What To Implement
- When installer selects Server beta, hooks should call Server beta endpoints directly:
- SessionStart -> `/v1/sessions/start` or compatibility endpoint;
- PostToolUse -> `/v1/events`;
- Stop/Summarize -> `/v1/sessions/:id/end`.
- Keep worker fallback only as fallback:
- if Server beta is selected but unhealthy, hook can fall back to worker and log a warning;
- fallback must be observable in hook output/logs.
- Add a server API-key bootstrap for local hooks:
- install creates a local hook API key scoped to local project/user;
- key is stored in local settings with correct file permissions;
- key rotation command exists.
- Keep existing hook JSON outputs unchanged.
### Documentation References
- Copy hook commands and expected outputs from `plugin/hooks/hooks.json`.
- Copy current hook HTTP call patterns from source files that generate the worker-service bundle, not from the generated bundle itself.
- Copy current installer prompt/setting pattern from `src/npx-cli/commands/install.ts`.
### Verification Checklist
- Lifecycle hook tests pass in worker mode.
- Lifecycle hook tests pass in server-beta mode.
- Server-beta mode with server down falls back to worker and logs one warning.
- Server-beta mode with server healthy does not start worker.
- Generated observation appears after a PostToolUse hook using only Server beta.
### Anti-Pattern Guards
- Do not route Server beta hooks through worker `/api/sessions/observations`.
- Do not silently start worker when Server beta is healthy.
- Do not store hook API keys in generated bundles.
## Phase 8: MCP Uses Server Runtime Directly
### What To Implement
- Add MCP tools backed by Server beta APIs/core logic:
- `observation_add`
- `observation_record_event`
- `observation_search`
- `observation_context`
- `observation_generation_status`
- Existing `memory_*` MCP names may remain only as compatibility aliases over the observation tools.
- Existing MCP search tools may continue to work with worker, but Server beta mode must not require worker.
- MCP write tools should create events or direct observations through the same service methods as REST.
### Documentation References
- Copy current MCP tool schema style from `src/servers/mcp-server.ts`.
- Copy new REST schemas from `src/core/schemas/*`.
- Copy auth mode rules from Server beta API-key middleware.
### Verification Checklist
- MCP client can record an event and retrieve generated context without worker running.
- MCP client can search generated observations.
- Existing MCP search tests remain green.
### Anti-Pattern Guards
- Do not duplicate generation logic in MCP tools.
- Do not import `WorkerService` into MCP server mode.
## Phase 9: Compatibility Without Coupling
### What To Implement
- Keep compatibility routes only as adapters:
- `/api/sessions/observations` -> convert legacy payload to `AgentEvent` -> enqueue Server beta generation job.
- `/api/sessions/summarize` -> convert legacy payload to session-end/summary job.
- legacy data/search routes -> read from Server beta repositories or explicit migration views.
- Compatibility adapters may live in `src/server/compat/*`.
- They must call Server beta services, not worker route classes.
- Add a parity map documenting each legacy route:
- native server implementation;
- adapter implementation;
- intentionally unsupported in Server beta.
### Documentation References
- Copy payload normalization from `src/services/worker/http/shared.ts`.
- Copy Claude Code mapper style from `src/adapters/claude-code/mapper.ts`.
- Copy route response snapshots from existing worker route tests.
### Verification Checklist
- `rg -n "services/worker/http/routes|WorkerService" src/server/compat src/server/runtime`
- must return no imports.
- Legacy PostToolUse route on Server beta creates an event and generation job.
- Viewer compatibility routes do not require worker.
### Anti-Pattern Guards
- Do not copy worker route classes wholesale into Server beta.
- Do not let compatibility adapters become the canonical Server API.
## Phase 10: Docker And Deployable Runtime
### What To Implement
- Docker image starts Server beta only:
- no worker process;
- no worker PID;
- no worker health dependency.
- Compose stack includes:
- Server beta container;
- Postgres container for canonical observation/job/session storage;
- Valkey container for BullMQ.
- Add env validation:
- `CLAUDE_MEM_RUNTIME=server-beta`
- `CLAUDE_MEM_QUEUE_ENGINE=bullmq`
- Postgres URL required.
- Redis/Valkey URL required.
- API-key auth required by default.
- Add optional separate generation worker process mode:
- `claude-mem server worker start`
- same codebase, separate process, same BullMQ queues.
### Documentation References
- Copy current Docker E2E style from `scripts/e2e-server-beta-docker.sh`.
- Copy current Docker image layout from `docker/claude-mem/Dockerfile`.
- Copy Valkey settings from `plans/2026-05-06-redis-dependency-strategy.md`.
### Verification Checklist
- Docker E2E starts no worker.
- `docker compose ps` shows server + Postgres + Valkey.
- `/v1/events?wait=true` creates generated observations.
- Restart server mid-job and verify retry/idempotency.
- Revoke API key and verify write/search denial.
### Anti-Pattern Guards
- Do not install or spawn worker in the Server beta container.
- Do not use local-dev auth in Docker.
- Do not use a process-local queue in Docker.
## Phase 11: Team-Aware Generation
### What To Implement
- Ensure every generation job carries:
- `team_id`;
- `project_id`;
- actor/API-key ID;
- source adapter.
- Enforce scopes before event insert and before job execution.
- Store generated observations with team/project metadata.
- Audit:
- event received;
- job queued;
- provider generation started;
- observation generated;
- observation served.
- Add team-level queue status endpoint:
- `/v1/teams/:id/jobs`
- `/v1/projects/:id/jobs`
### Documentation References
- Copy API-key/team storage patterns from `src/storage/sqlite/teams.ts` and `src/storage/sqlite/auth.ts`.
- Copy project-scoping guards from `src/server/routes/v1/ServerV1Routes.ts`.
- Copy audit repository style from current server storage.
### Verification Checklist
- Team-scoped key cannot read/write/generate outside team projects.
- Project-scoped key cannot enqueue generation for another project.
- Generated observation includes correct team/project IDs.
- Audit records include generation job IDs.
### Anti-Pattern Guards
- Do not let BullMQ job data become an auth bypass.
- Do not trust job payload project/team IDs without reloading the outbox row from Postgres.
## Phase 12: Observability And Operations
### What To Implement
- Add `claude-mem server jobs status`.
- Add `claude-mem server jobs retry <id>`.
- Add `claude-mem server jobs cancel <id>`.
- Add `claude-mem server jobs failed`.
- Add queue metrics:
- waiting;
- active;
- completed;
- failed;
- delayed;
- stalled event count.
- Add logs with request ID/job ID correlation.
- Add `/v1/jobs` list endpoint.
### Documentation References
- BullMQ Workers docs for worker `completed`, `failed`, `progress`, and `error` events.
- BullMQ Stalled Jobs docs for stalled event behavior and rare-stall assumption.
- Existing `src/services/worker/http/routes/LogsRoutes.ts` for log tailing style.
### Verification Checklist
- Failed provider response appears in `server jobs failed`.
- Retry moves job back to queued and generates an observation once.
- Cancel prevents later generation.
- Stalled events are logged with job ID.
### Anti-Pattern Guards
- Do not expose full sensitive event payloads in queue status by default.
- Do not retry non-idempotently.
## Phase 13: Final Verification Gate
Phase 13 is not an implementation phase and does not need the implementation-phase template. It is the final release gate for proving the independently implemented Server beta runtime is complete, durable, and still compatible with the legacy worker runtime.
### Required Automated Tests
- Unit:
- provider generation parser;
- event-to-job transaction;
- job ID/idempotency;
- team/project auth on generation;
- compatibility route adapters.
- Integration:
- Server beta starts without worker;
- `/v1/events` generates observations;
- hook PostToolUse generates observations through Server beta;
- MCP event write generates observations through Server beta;
- restart during active generation retries safely.
- Docker:
- Server beta + Postgres + Valkey;
- API-key auth;
- event generation;
- restart persistence;
- revoked-key denial;
- no worker process.
### Required Greps
```bash
rg -n "new WorkerService|services/worker-service|services/worker/http/routes" src/server
rg -n "PendingMessageStore|SessionQueueProcessor" src/server
rg -n "CLAUDE_MEM_AUTH_MODE=local-dev|ALLOW_LOCAL_DEV_BYPASS" docker docs/server.md
rg -n "POST /v1/events|generationJob|wait=true" docs README.md
```
Expected:
- First two greps return no Server beta runtime imports.
- Docker docs do not recommend local-dev auth.
- Docs mention event generation semantics.
### Manual Verification
1. Start worker, confirm existing worker flow still works.
2. Stop worker.
3. Start Server beta with Valkey.
4. Submit a generic REST event.
5. Confirm observations appear without worker running.
6. Submit a Claude Code PostToolUse payload through Server beta hook routing.
7. Confirm observations appear without worker running.
8. Restart Server beta during a provider call.
9. Confirm the job retries and generates once.
### Exit Criteria
Server beta is independent when all are true:
- Server beta can generate observations while worker is stopped.
- Docker Server beta image does not spawn worker.
- `/v1/events` can enqueue and generate observations.
- Hook routing to Server beta generates observations when healthy.
- BullMQ queue state survives restart and retries safely.
- Postgres server storage is the source of truth for observations and generation job history.
- The worker remains available as a separate stable runtime.
+1 -1
View File
@@ -6,7 +6,7 @@
"name": "Alex Newman"
},
"repository": "https://github.com/thedotmack/claude-mem",
"license": "AGPL-3.0",
"license": "Apache-2.0",
"keywords": [
"claude",
"claude-code",
+1 -1
View File
@@ -8,7 +8,7 @@
},
"homepage": "https://github.com/thedotmack/claude-mem#readme",
"repository": "https://github.com/thedotmack/claude-mem",
"license": "AGPL-3.0",
"license": "Apache-2.0",
"keywords": [
"claude",
"claude-code",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+169 -104
View File
@@ -1,137 +1,202 @@
# PolyForm Noncommercial License 1.0.0
<https://polyformproject.org/licenses/noncommercial/1.0.0>
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
## Acceptance
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
In order to get any license under these terms, you must agree
to them as both strict obligations and conditions to all
your licenses.
1. Definitions.
## Copyright License
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The licensor grants you a copyright license for the
software to do everything you might do with the software
that would otherwise infringe the licensor's copyright
in it for any permitted purpose. However, you may
only distribute the software according to [Distribution
License](#distribution-license) and make changes or new works
based on the software according to [Changes and New Works
License](#changes-and-new-works-license).
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
## Distribution License
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
The licensor grants you an additional copyright license
to distribute copies of the software. Your license
to distribute covers distributing the software with
changes and new works permitted by [Changes and New Works
License](#changes-and-new-works-license).
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
## Notices
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
You must ensure that anyone who gets a copy of any part of
the software from you also gets a copy of these terms or the
URL for them above, as well as copies of any plain-text lines
beginning with `Required Notice:` that the licensor provided
with the software. For example:
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
> Required Notice: Copyright Alex Newman (https://github.com/thedotmack)
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
## Changes and New Works License
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
The licensor grants you an additional copyright license to
make changes and new works based on the software for any
permitted purpose.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
## Patent License
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
## Noncommercial Purposes
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
Any noncommercial purpose is a permitted purpose.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
## Personal Uses
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
Personal use for research, experiment, and testing for
the benefit of public knowledge, personal study, private
entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
## Noncommercial Organizations
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
## Fair Use
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
You may have "fair use" rights for the software under the
law. These terms do not limit them.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
## No Other Rights
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
any other licenses.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
## Patent Defense
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
If you make any written claim that the software infringes or
contributes to infringement of any patent, your patent license
for the software granted under these terms ends immediately. If
your company makes such a claim, your patent license ends
immediately for work on behalf of your company.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
## Violations
END OF TERMS AND CONDITIONS
The first time you are notified in writing that you have
violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
end immediately.
APPENDIX: How to apply the Apache License to your work.
## No Liability
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
***As far as the law allows, the software comes as is, without
any warranty or condition, and the licensor will not be liable
to you for any damages arising out of these terms or the use
or nature of the software, under any kind of legal claim.***
Copyright [yyyy] [name of copyright owner]
## Definitions
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
The **licensor** is the individual or entity offering these
terms, and the **software** is the software the licensor makes
available under these terms.
http://www.apache.org/licenses/LICENSE-2.0
**You** refers to the individual or entity agreeing to these
terms.
**Your company** is any legal entity, sole proprietorship,
or other kind of organization that you work for, plus all
organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.
---
Required Notice: Copyright 2025 Alex Newman (https://github.com/thedotmack)
For commercial licensing inquiries, contact: thedotmack@gmail.com
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+6 -6
View File
@@ -64,20 +64,20 @@ Each markdown file should contain a single email or document to analyze.
## License
This directory is licensed under the **PolyForm Noncommercial License 1.0.0**.
This directory is licensed under the **Apache License 2.0**.
See [LICENSE](./LICENSE) for full terms.
### What this means:
- You can use ragtime for noncommercial purposes
- You can use ragtime for personal, research, and commercial purposes
- You can modify and distribute it
- You cannot use it for commercial purposes without permission
- You must preserve the license and required notices
### Why a different license?
### License alignment
The main claude-mem repository is licensed under AGPL 3.0, but ragtime uses the more restrictive PolyForm Noncommercial license to ensure it remains freely available for personal and educational use while preventing commercial exploitation.
The main claude-mem repository is licensed under Apache-2.0, and ragtime now uses the same license for this migration.
---
For questions about commercial licensing, please contact the project maintainer.
For licensing questions, please contact the project maintainer.
+39
View File
@@ -12,6 +12,11 @@ const WORKER_SERVICE = {
source: 'src/services/worker-service.ts'
};
const SERVER_BETA_SERVICE = {
name: 'server-beta-service',
source: 'src/server/runtime/ServerBetaService.ts'
};
const MCP_SERVER = {
name: 'mcp-server',
source: 'src/servers/mcp-server.ts'
@@ -139,6 +144,7 @@ async function buildHooks() {
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
external: [
'bun:sqlite',
'zod',
'cohere-ai',
'ollama',
'@chroma-core/default-embed',
@@ -162,6 +168,38 @@ async function buildHooks() {
const workerStats = fs.statSync(`${hooksDir}/${WORKER_SERVICE.name}.cjs`);
console.log(`✓ worker-service built (${(workerStats.size / 1024).toFixed(2)} KB)`);
console.log(`\n🔧 Building server beta service...`);
await build({
entryPoints: [SERVER_BETA_SERVICE.source],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs',
outfile: `${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`,
minify: true,
logLevel: 'error',
external: [
'bun:sqlite',
'zod',
],
define: {
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
},
banner: {
js: [
'#!/usr/bin/env bun',
'var __filename = __filename || require("node:path").resolve(process.argv[1] || "");',
'var __dirname = __dirname || require("node:path").dirname(__filename);'
].join('\n')
}
});
stripHardcodedDirname(`${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`);
fs.chmodSync(`${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`, 0o755);
const serverBetaStats = fs.statSync(`${hooksDir}/${SERVER_BETA_SERVICE.name}.cjs`);
console.log(`✓ server-beta-service built (${(serverBetaStats.size / 1024).toFixed(2)} KB)`);
console.log(`\n🔧 Building MCP server...`);
await build({
entryPoints: [MCP_SERVER.source],
@@ -406,6 +444,7 @@ async function buildHooks() {
console.log('\n✅ All build targets compiled successfully!');
console.log(` Output: ${hooksDir}/`);
console.log(` - Worker: worker-service.cjs`);
console.log(` - Server beta: server-beta-service.cjs`);
console.log(` - MCP Server: mcp-server.cjs`);
console.log(` - Context Generator: context-generator.cjs`);
console.log(` Output: ${npxCliOutDir}/`);
+14 -14
View File
@@ -39,19 +39,19 @@ Claude-Mem Queue Clearer
Clear orphaned messages from the pending_messages SQLite table.
Usage:
bun scripts/clear-failed-queue.ts [options]
bun scripts/clear-pending-queue.ts [options]
Options:
--help, -h Show this help message
--all Clear ALL messages (pending, processing, processed, failed)
--all Clear ALL messages (pending and processing)
--force Clear without prompting for confirmation
Examples:
# Clear failed messages interactively
bun scripts/clear-failed-queue.ts
# Clear processing messages interactively
bun scripts/clear-pending-queue.ts
# Clear ALL messages without confirmation
bun scripts/clear-failed-queue.ts --all --force
bun scripts/clear-pending-queue.ts --all --force
Notes:
Operates directly on ~/.claude-mem/claude-mem.db (or \$CLAUDE_MEM_DATA_DIR).
@@ -65,7 +65,7 @@ Notes:
console.log(clearAll
? '\n=== Claude-Mem Queue Clearer (ALL) ===\n'
: '\n=== Claude-Mem Queue Clearer (Failed) ===\n');
: '\n=== Claude-Mem Queue Clearer (Processing) ===\n');
const dbPath = resolveDbPath();
if (!existsSync(dbPath)) {
@@ -81,20 +81,20 @@ Notes:
).all() as StatusRow[];
const total = counts.reduce((sum, row) => sum + row.count, 0);
const failed = counts.find(r => r.status === 'failed')?.count ?? 0;
const processing = counts.find(r => r.status === 'processing')?.count ?? 0;
console.log('Queue Summary:');
for (const status of ['pending', 'processing', 'processed', 'failed'] as const) {
for (const status of ['pending', 'processing'] as const) {
const row = counts.find(r => r.status === status);
console.log(` ${status.padEnd(11)} ${row?.count ?? 0}`);
}
console.log('');
const willClear = clearAll ? total : failed;
const willClear = clearAll ? total : processing;
if (willClear === 0) {
console.log(clearAll
? 'No messages in queue. Nothing to clear.\n'
: 'No failed messages in queue. Nothing to clear.\n');
: 'No processing messages in queue. Nothing to clear.\n');
db.close();
process.exit(0);
}
@@ -102,8 +102,8 @@ Notes:
if (!force) {
const answer = await prompt(
clearAll
? `Clear ${willClear} messages (all statuses)? [y/N]: `
: `Clear ${willClear} failed messages? [y/N]: `
? `Clear ${willClear} messages (pending and processing)? [y/N]: `
: `Clear ${willClear} processing messages? [y/N]: `
);
if (answer.toLowerCase() !== 'y') {
console.log('\nCancelled. Run with --force to skip confirmation.\n');
@@ -114,8 +114,8 @@ Notes:
}
const stmt = clearAll
? db.prepare('DELETE FROM pending_messages')
: db.prepare("DELETE FROM pending_messages WHERE status = 'failed'");
? db.prepare("DELETE FROM pending_messages WHERE status IN ('pending', 'processing')")
: db.prepare("DELETE FROM pending_messages WHERE status = 'processing'");
const cleared = stmt.run().changes;
const remaining = (db.prepare(
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PROJECT_NAME="${COMPOSE_PROJECT_NAME:-claude-mem-server-beta-e2e-$(date +%s)}"
RUN_ID="${E2E_RUN_ID:-$(date +%s)-$RANDOM}"
COMPOSE_FILES=(-f docker-compose.yml -f docker-compose.e2e.yml)
COMPOSE=(docker compose -p "$PROJECT_NAME" "${COMPOSE_FILES[@]}")
SERVER_SCRIPT="/opt/claude-mem/scripts/worker-service.cjs"
cd "$ROOT_DIR"
cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "[e2e] failure; recent server logs:" >&2
"${COMPOSE[@]}" logs --no-color --tail=200 claude-mem-server valkey >&2 || true
fi
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
}
trap cleanup EXIT
wait_for_container_readiness() {
local deadline=$((SECONDS + 120))
until "${COMPOSE[@]}" exec -T claude-mem-server curl -fsS http://127.0.0.1:37777/api/readiness >/dev/null 2>&1; do
if (( SECONDS > deadline )); then
echo "[e2e] server did not become ready" >&2
return 1
fi
sleep 1
done
}
json_field() {
local field="$1"
node -e '
const field = process.argv[1];
let raw = "";
process.stdin.on("data", chunk => raw += chunk);
process.stdin.on("end", () => {
const value = JSON.parse(raw)[field];
if (value === undefined || value === null) process.exit(1);
process.stdout.write(String(value));
});
' "$field"
}
create_key() {
local name="$1"
local scopes="$2"
"${COMPOSE[@]}" exec -T claude-mem-server \
bun "$SERVER_SCRIPT" server api-key create --name "$name" --scope "$scopes"
}
echo "[e2e] building plugin bundles"
npm run build
echo "[e2e] starting Docker stack project=$PROJECT_NAME run=$RUN_ID"
"${COMPOSE[@]}" up --build -d valkey claude-mem-server
wait_for_container_readiness
echo "[e2e] creating API keys inside server container"
FULL_KEY_JSON="$(create_key "docker-e2e-full-$RUN_ID" "memories:read,memories:write")"
READ_ONLY_KEY_JSON="$(create_key "docker-e2e-read-$RUN_ID" "memories:read")"
FULL_KEY="$(printf '%s' "$FULL_KEY_JSON" | json_field key)"
READ_ONLY_KEY="$(printf '%s' "$READ_ONLY_KEY_JSON" | json_field key)"
READ_ONLY_KEY_ID="$(printf '%s' "$READ_ONLY_KEY_JSON" | json_field id)"
echo "[e2e] running phase1 functional paths in test container"
"${COMPOSE[@]}" run --rm \
-e E2E_PHASE=phase1 \
-e E2E_RUN_ID="$RUN_ID" \
-e E2E_API_KEY="$FULL_KEY" \
-e E2E_READ_ONLY_API_KEY="$READ_ONLY_KEY" \
server-beta-e2e
echo "[e2e] revoking read-only key inside server container"
"${COMPOSE[@]}" exec -T claude-mem-server \
bun "$SERVER_SCRIPT" server api-key revoke "$READ_ONLY_KEY_ID" >/dev/null
echo "[e2e] restarting server container to verify persisted state"
"${COMPOSE[@]}" restart claude-mem-server
wait_for_container_readiness
echo "[e2e] running phase2 persistence and revoked-key checks in test container"
"${COMPOSE[@]}" run --rm \
-e E2E_PHASE=phase2 \
-e E2E_RUN_ID="$RUN_ID" \
-e E2E_API_KEY="$FULL_KEY" \
-e E2E_REVOKED_API_KEY="$READ_ONLY_KEY" \
server-beta-e2e
echo "[e2e] Docker server beta E2E passed for run=$RUN_ID"
+1 -21
View File
@@ -15,14 +15,6 @@ interface AffectedObservation {
title: string;
}
interface ProcessedMessage {
id: number;
session_db_id: number;
tool_name: string;
created_at_epoch: number;
completed_at_epoch: number;
}
interface SessionMapping {
session_db_id: number;
memory_session_id: string;
@@ -78,19 +70,7 @@ function main() {
return;
}
console.log('Step 2: Finding pending messages processed during bad window...');
const processedMessages = db.query<ProcessedMessage, []>(`
SELECT id, session_db_id, tool_name, created_at_epoch, completed_at_epoch
FROM pending_messages
WHERE status = 'processed'
AND completed_at_epoch >= ${BAD_WINDOW_START}
AND completed_at_epoch <= ${BAD_WINDOW_END}
ORDER BY completed_at_epoch
`).all();
console.log(`Found ${processedMessages.length} processed messages\n`);
console.log('Step 3: Matching observations to session start times...');
console.log('Step 2: Matching observations to session start times...');
const fixes: TimestampFix[] = [];
interface ObsWithSession {
-12
View File
@@ -89,17 +89,6 @@ function main() {
console.log();
}
console.log('Check 4: Verifying processed pending_messages...');
const processedCount = db.query<{ count: number }, []>(`
SELECT COUNT(*) as count
FROM pending_messages
WHERE status = 'processed'
AND completed_at_epoch >= ${BAD_WINDOW_START}
AND completed_at_epoch <= ${BAD_WINDOW_END}
`).get();
console.log(`${processedCount?.count || 0} pending messages were processed during bad window\n`);
console.log('═══════════════════════════════════════════════════════════════════════');
console.log('VERIFICATION SUMMARY:');
console.log('═══════════════════════════════════════════════════════════════════════\n');
@@ -108,7 +97,6 @@ function main() {
console.log('✅ SUCCESS: Timestamp fix appears to be working correctly!');
console.log(` - No observations remain in bad window (Dec 24 19:45-20:31)`);
console.log(` - ${originalWindowObs?.count} observations restored to Dec 17-20`);
console.log(` - Processed ${processedCount?.count} pending messages`);
console.log('\n💡 Safe to re-enable orphan processing in worker-service.ts\n');
} else if (badWindowObs.length > 0) {
console.log('⚠️ WARNING: Some observations still have incorrect timestamps!');
+68
View File
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: Apache-2.0
import type { CreateAgentEvent } from '../../core/schemas/agent-event.js';
import { normalizePlatformSource } from '../../shared/platform-source.js';
export interface ClaudeCodeBasePayload {
contentSessionId: string;
memorySessionId?: string | null;
platformSource?: string | null;
cwd?: string;
agentId?: string;
agentType?: string;
[key: string]: unknown;
}
export interface ClaudeCodeObservationPayload extends ClaudeCodeBasePayload {
tool_name: string;
tool_input?: unknown;
tool_response?: unknown;
tool_use_id?: string;
toolUseId?: string;
}
export function mapClaudeCodeSessionInitToAgentEvent(
projectId: string,
payload: ClaudeCodeBasePayload,
occurredAtEpoch = Date.now(),
): CreateAgentEvent {
return mapClaudeCodePayload(projectId, payload, 'session.init', occurredAtEpoch);
}
export function mapClaudeCodeObservationToAgentEvent(
projectId: string,
payload: ClaudeCodeObservationPayload,
occurredAtEpoch = Date.now(),
): CreateAgentEvent {
return mapClaudeCodePayload(projectId, payload, 'observation.created', occurredAtEpoch);
}
export function mapClaudeCodeSummaryToAgentEvent(
projectId: string,
payload: ClaudeCodeBasePayload,
occurredAtEpoch = Date.now(),
): CreateAgentEvent {
return mapClaudeCodePayload(projectId, payload, 'session.summary', occurredAtEpoch);
}
function mapClaudeCodePayload(
projectId: string,
payload: ClaudeCodeBasePayload,
eventType: string,
occurredAtEpoch: number,
): CreateAgentEvent {
const platformSource = normalizePlatformSource(payload.platformSource);
return {
projectId,
sourceType: 'hook',
eventType,
payload: {
...payload,
platformSource,
toolUseId: payload.toolUseId ?? payload.tool_use_id ?? null,
},
contentSessionId: payload.contentSessionId,
memorySessionId: payload.memorySessionId ?? null,
occurredAtEpoch,
};
}
+42
View File
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
export const genericRestEventExamples = {
codexObservation: {
projectId: 'project-id',
sourceType: 'api',
eventType: 'observation.created',
contentSessionId: 'codex-session-id',
payload: {
platformSource: 'codex',
tool_name: 'shell',
cwd: '/workspace/project',
agentId: 'codex-agent-id',
agentType: 'codex',
toolUseId: 'tool-call-id',
tool_input: { command: 'bun test' },
tool_response: { exitCode: 0 },
},
occurredAtEpoch: 1760000000000,
},
opencodeObservation: {
projectId: 'project-id',
sourceType: 'api',
eventType: 'observation.created',
contentSessionId: 'opencode-session-id',
payload: {
platformSource: 'opencode',
tool_name: 'edit',
cwd: '/workspace/project',
toolUseId: 'tool-call-id',
},
occurredAtEpoch: 1760000000000,
},
customMemory: {
projectId: 'project-id',
kind: 'manual',
type: 'note',
title: 'Decision',
narrative: 'Store canonical memory records in SQLite; Redis is queue state only.',
facts: ['SQLite is the source of truth for memories'],
},
} as const;
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
export const AgentEventSourceTypeSchema = z.enum(['hook', 'worker', 'provider', 'server', 'api']);
export const AgentEventSchema = z.object({
id: z.string().min(1),
projectId: z.string().min(1),
serverSessionId: z.string().min(1).nullable().default(null),
sourceType: AgentEventSourceTypeSchema,
eventType: z.string().min(1),
payload: z.unknown().default({}),
contentSessionId: z.string().min(1).nullable().default(null),
memorySessionId: z.string().min(1).nullable().default(null),
occurredAtEpoch: z.number().int().nonnegative(),
createdAtEpoch: z.number().int().nonnegative()
});
export const CreateAgentEventSchema = AgentEventSchema.omit({
id: true,
createdAtEpoch: true
}).partial({
serverSessionId: true,
payload: true,
contentSessionId: true,
memorySessionId: true
});
export type AgentEventSourceType = z.infer<typeof AgentEventSourceTypeSchema>;
export type AgentEvent = z.infer<typeof AgentEventSchema>;
export type CreateAgentEvent = z.infer<typeof CreateAgentEventSchema>;
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
export const ApiKeyStatusSchema = z.enum(['active', 'revoked']);
export const AuditActorTypeSchema = z.enum(['user', 'api_key', 'system']);
export const ApiKeySchema = z.object({
id: z.string().min(1),
teamId: z.string().min(1).nullable().default(null),
projectId: z.string().min(1).nullable().default(null),
name: z.string().min(1),
keyHash: z.string().min(1),
prefix: z.string().min(1).nullable().default(null),
scopes: z.array(z.string()).default([]),
status: ApiKeyStatusSchema.default('active'),
lastUsedAtEpoch: z.number().int().nonnegative().nullable().default(null),
expiresAtEpoch: z.number().int().nonnegative().nullable().default(null),
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative(),
updatedAtEpoch: z.number().int().nonnegative()
});
export const CreateApiKeySchema = ApiKeySchema.omit({
id: true,
status: true,
lastUsedAtEpoch: true,
createdAtEpoch: true,
updatedAtEpoch: true
}).partial({
teamId: true,
projectId: true,
prefix: true,
scopes: true,
expiresAtEpoch: true,
metadata: true
});
export const AuditLogSchema = z.object({
id: z.string().min(1),
teamId: z.string().min(1).nullable().default(null),
projectId: z.string().min(1).nullable().default(null),
actorType: AuditActorTypeSchema,
actorId: z.string().min(1).nullable().default(null),
action: z.string().min(1),
targetType: z.string().min(1).nullable().default(null),
targetId: z.string().min(1).nullable().default(null),
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative()
});
export const CreateAuditLogSchema = AuditLogSchema.omit({
id: true,
createdAtEpoch: true
}).partial({
teamId: true,
projectId: true,
actorId: true,
targetType: true,
targetId: true,
metadata: true
});
export type ApiKeyStatus = z.infer<typeof ApiKeyStatusSchema>;
export type ApiKey = z.infer<typeof ApiKeySchema>;
export type CreateApiKey = z.infer<typeof CreateApiKeySchema>;
export type AuditActorType = z.infer<typeof AuditActorTypeSchema>;
export type AuditLog = z.infer<typeof AuditLogSchema>;
export type CreateAuditLog = z.infer<typeof CreateAuditLogSchema>;
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
import { MemoryItemSchema } from './memory-item.js';
export const ContextPackSchema = z.object({
projectId: z.string().min(1),
serverSessionId: z.string().min(1).nullable().default(null),
generatedAtEpoch: z.number().int().nonnegative(),
tokenBudget: z.number().int().positive().nullable().default(null),
items: z.array(MemoryItemSchema).default([]),
metadata: z.record(z.string(), z.unknown()).default({})
});
export type ContextPack = z.infer<typeof ContextPackSchema>;
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
export * from './agent-event.js';
export * from './auth.js';
export * from './context-pack.js';
export * from './memory-item.js';
export * from './project.js';
export * from './session.js';
export * from './team.js';
+72
View File
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
export const MemoryItemKindSchema = z.enum(['observation', 'summary', 'prompt', 'manual']);
export const MemorySourceTypeSchema = z.enum(['observation', 'session_summary', 'user_prompt', 'manual', 'import']);
export const MemoryItemSchema = z.object({
id: z.string().min(1),
projectId: z.string().min(1),
serverSessionId: z.string().min(1).nullable().default(null),
legacyObservationId: z.number().int().positive().nullable().default(null),
kind: MemoryItemKindSchema,
type: z.string().min(1),
title: z.string().min(1).nullable().default(null),
subtitle: z.string().min(1).nullable().default(null),
text: z.string().nullable().default(null),
narrative: z.string().nullable().default(null),
facts: z.array(z.string()).default([]),
concepts: z.array(z.string()).default([]),
filesRead: z.array(z.string()).default([]),
filesModified: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative(),
updatedAtEpoch: z.number().int().nonnegative()
});
export const CreateMemoryItemSchema = MemoryItemSchema.omit({
id: true,
createdAtEpoch: true,
updatedAtEpoch: true
}).partial({
serverSessionId: true,
legacyObservationId: true,
title: true,
subtitle: true,
text: true,
narrative: true,
facts: true,
concepts: true,
filesRead: true,
filesModified: true,
metadata: true
});
export const MemorySourceSchema = z.object({
id: z.string().min(1),
memoryItemId: z.string().min(1),
sourceType: MemorySourceTypeSchema,
legacyTable: z.string().min(1).nullable().default(null),
legacyId: z.number().int().positive().nullable().default(null),
sourceUri: z.string().min(1).nullable().default(null),
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative()
});
export const CreateMemorySourceSchema = MemorySourceSchema.omit({
id: true,
createdAtEpoch: true
}).partial({
legacyTable: true,
legacyId: true,
sourceUri: true,
metadata: true
});
export type MemoryItemKind = z.infer<typeof MemoryItemKindSchema>;
export type MemoryItem = z.infer<typeof MemoryItemSchema>;
export type CreateMemoryItem = z.infer<typeof CreateMemoryItemSchema>;
export type MemorySourceType = z.infer<typeof MemorySourceTypeSchema>;
export type MemorySource = z.infer<typeof MemorySourceSchema>;
export type CreateMemorySource = z.infer<typeof CreateMemorySourceSchema>;
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
export const ProjectSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
slug: z.string().min(1).nullable().default(null),
rootPath: z.string().min(1).nullable().default(null),
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative(),
updatedAtEpoch: z.number().int().nonnegative()
});
export const CreateProjectSchema = ProjectSchema.omit({
id: true,
createdAtEpoch: true,
updatedAtEpoch: true
}).partial({
slug: true,
rootPath: true,
metadata: true
});
export type Project = z.infer<typeof ProjectSchema>;
export type CreateProject = z.infer<typeof CreateProjectSchema>;
+37
View File
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
export const ServerSessionStatusSchema = z.enum(['active', 'completed', 'failed']);
export const ServerSessionSchema = z.object({
id: z.string().min(1),
projectId: z.string().min(1),
contentSessionId: z.string().min(1).nullable().default(null),
memorySessionId: z.string().min(1).nullable().default(null),
platformSource: z.string().min(1).default('claude'),
title: z.string().min(1).nullable().default(null),
status: ServerSessionStatusSchema.default('active'),
metadata: z.record(z.string(), z.unknown()).default({}),
startedAtEpoch: z.number().int().nonnegative(),
completedAtEpoch: z.number().int().nonnegative().nullable().default(null),
updatedAtEpoch: z.number().int().nonnegative()
});
export const CreateServerSessionSchema = ServerSessionSchema.omit({
id: true,
startedAtEpoch: true,
status: true,
completedAtEpoch: true,
updatedAtEpoch: true
}).partial({
contentSessionId: true,
memorySessionId: true,
platformSource: true,
title: true,
metadata: true
});
export type ServerSessionStatus = z.infer<typeof ServerSessionStatusSchema>;
export type ServerSession = z.infer<typeof ServerSessionSchema>;
export type CreateServerSession = z.infer<typeof CreateServerSessionSchema>;
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
import { z } from 'zod';
export const TeamRoleSchema = z.enum(['owner', 'admin', 'member', 'viewer']);
export const TeamSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
slug: z.string().min(1).nullable().default(null),
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative(),
updatedAtEpoch: z.number().int().nonnegative()
});
export const CreateTeamSchema = TeamSchema.omit({
id: true,
createdAtEpoch: true,
updatedAtEpoch: true
}).partial({
slug: true,
metadata: true
});
export const TeamMemberSchema = z.object({
id: z.string().min(1),
teamId: z.string().min(1),
userId: z.string().min(1),
role: TeamRoleSchema,
metadata: z.record(z.string(), z.unknown()).default({}),
createdAtEpoch: z.number().int().nonnegative()
});
export const CreateTeamMemberSchema = TeamMemberSchema.omit({
id: true,
createdAtEpoch: true
}).partial({
metadata: true
});
export type TeamRole = z.infer<typeof TeamRoleSchema>;
export type Team = z.infer<typeof TeamSchema>;
export type CreateTeam = z.infer<typeof CreateTeamSchema>;
export type TeamMember = z.infer<typeof TeamMemberSchema>;
export type CreateTeamMember = z.infer<typeof CreateTeamMemberSchema>;
+38 -8
View File
@@ -619,6 +619,7 @@ function mergeSettings(updates: Record<string, string>): boolean {
type ProviderId = 'claude' | 'gemini' | 'openrouter';
type ClaudeAccessMode = 'subscription' | 'api-key';
type ClaudeApiMode = 'direct' | 'gateway';
type RuntimeId = 'worker' | 'server-beta';
function readRawStoredAuthMethod(): 'subscription' | 'api-key' | 'gateway' | undefined {
try {
@@ -642,6 +643,32 @@ function resolveClaudeAuthMethod(): 'subscription' | 'api-key' | 'gateway' {
return 'subscription';
}
async function promptRuntime(): Promise<RuntimeId> {
if (!isInteractive) {
mergeSettings({ CLAUDE_MEM_RUNTIME: 'worker' });
return 'worker';
}
const selected = await p.select<RuntimeId>({
message: 'Which runtime should claude-mem start after install?',
options: [
{ value: 'worker', label: 'Worker', hint: 'stable compatibility path' },
{ value: 'server-beta', label: 'Server (beta)', hint: 'REST V1, API keys, team-ready storage' },
],
initialValue: 'worker',
});
if (p.isCancel(selected)) {
p.cancel('Installation cancelled.');
process.exit(0);
}
mergeSettings({
CLAUDE_MEM_RUNTIME: selected,
});
return selected;
}
async function promptProvider(options: InstallOptions): Promise<ProviderId> {
const initialProvider = (getSetting('CLAUDE_MEM_PROVIDER') as ProviderId) || 'claude';
@@ -1025,6 +1052,7 @@ export async function runInstallCommand(options: InstallOptions = {}): Promise<v
selectedIDEs = ['claude-code'];
}
const selectedRuntime = await promptRuntime();
const selectedProvider = await promptProvider(options);
if (selectedProvider === 'claude') {
await promptClaudeModel(options);
@@ -1169,7 +1197,7 @@ export async function runInstallCommand(options: InstallOptions = {}): Promise<v
await runTasks([
{
title: 'Starting worker daemon',
title: selectedRuntime === 'server-beta' ? 'Starting server beta daemon' : 'Starting worker daemon',
task: async (message) => {
if (autoStartSkipped) {
return isInteractive
@@ -1180,15 +1208,15 @@ export async function runInstallCommand(options: InstallOptions = {}): Promise<v
const marketplaceScriptPath = join(marketplaceDirectory(), 'plugin', 'scripts', 'worker-service.cjs');
const cacheScriptPath = join(pluginCacheDirectory(version), 'scripts', 'worker-service.cjs');
const scriptPath = existsSync(marketplaceScriptPath) ? marketplaceScriptPath : cacheScriptPath;
message(`Spawning worker on port ${port}...`);
message(`Spawning ${selectedRuntime === 'server-beta' ? 'server beta' : 'worker'} on port ${port}...`);
workerStartResult = await ensureWorkerStarted(port, scriptPath);
switch (workerStartResult) {
case 'ready':
return `Worker ready at http://localhost:${port} ${pc.green('OK')}`;
return `${selectedRuntime === 'server-beta' ? 'Server beta' : 'Worker'} ready at http://localhost:${port} ${pc.green('OK')}`;
case 'warming':
return `Worker starting on port ${port} — finishing in background ${pc.yellow('⏳')}`;
return `${selectedRuntime === 'server-beta' ? 'Server beta' : 'Worker'} starting on port ${port} — finishing in background ${pc.yellow('⏳')}`;
case 'dead':
return `Worker did not start — try \`npx claude-mem start\` manually ${pc.yellow('!')}`;
return `${selectedRuntime === 'server-beta' ? 'Server beta' : 'Worker'} did not start — try \`${selectedRuntime === 'server-beta' ? 'npx claude-mem server start' : 'npx claude-mem start'}\` manually ${pc.yellow('!')}`;
}
},
},
@@ -1256,11 +1284,13 @@ export async function runInstallCommand(options: InstallOptions = {}): Promise<v
const finalWorkerState = workerStartResult as WorkerStartResult;
const workerAlive = finalWorkerState !== 'dead' || workerReady;
const runtimeLabel = selectedRuntime === 'server-beta' ? 'Server beta' : 'Worker';
const runtimeStartCommand = selectedRuntime === 'server-beta' ? 'npx claude-mem server start' : 'npx claude-mem start';
const workerHeadline = autoStartSkipped
? `${pc.yellow('!')} Worker autostart skipped — start it manually with ${pc.bold('npx claude-mem start')}`
? `${pc.yellow('!')} ${runtimeLabel} autostart skipped — start it manually with ${pc.bold(runtimeStartCommand)}`
: workerReady || finalWorkerState === 'ready'
? `${pc.green('✓')} Worker running at ${pc.underline(`http://localhost:${actualPort}`)}`
: `${pc.yellow('⏳')} Worker starting at ${pc.underline(`http://localhost:${actualPort}`)} — give it ~30s, then refresh`;
? `${pc.green('✓')} ${runtimeLabel} running at ${pc.underline(`http://localhost:${actualPort}`)}`
: `${pc.yellow('⏳')} ${runtimeLabel} starting at ${pc.underline(`http://localhost:${actualPort}`)} — give it ~30s, then refresh`;
const nextSteps = autoStartSkipped
? [
workerHeadline,
+51
View File
@@ -29,6 +29,10 @@ function workerServiceScriptPath(): string {
return join(marketplaceDirectory(), 'plugin', 'scripts', 'worker-service.cjs');
}
function serverBetaServiceScriptPath(): string {
return join(marketplaceDirectory(), 'plugin', 'scripts', 'server-beta-service.cjs');
}
function spawnBunWorkerCommand(command: string, extraArgs: string[] = []): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
@@ -58,6 +62,49 @@ function spawnBunWorkerCommand(command: string, extraArgs: string[] = []): void
});
}
function spawnBunServerBetaCommand(command: string): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
const serverScript = serverBetaServiceScriptPath();
if (!existsSync(serverScript)) {
console.error(pc.red(`Server beta script not found at: ${serverScript}`));
console.error('The installation may be corrupted. Try: npx claude-mem install');
process.exit(1);
}
const child = spawnHidden(bunPath, [serverScript, command], {
stdio: 'inherit',
cwd: marketplaceDirectory(),
env: process.env,
});
child.on('error', (error) => {
console.error(pc.red(`Failed to start Bun: ${error.message}`));
process.exit(1);
});
child.on('close', (exitCode) => {
process.exit(exitCode ?? 0);
});
}
export function runServerBetaStartCommand(): void {
spawnBunServerBetaCommand('start');
}
export function runServerBetaStopCommand(): void {
spawnBunServerBetaCommand('stop');
}
export function runServerBetaRestartCommand(): void {
spawnBunServerBetaCommand('restart');
}
export function runServerBetaStatusCommand(): void {
spawnBunServerBetaCommand('status');
}
export function runStartCommand(): void {
spawnBunWorkerCommand('start');
}
@@ -74,6 +121,10 @@ export function runStatusCommand(): void {
spawnBunWorkerCommand('status');
}
export function runServerApiKeyCommand(extraArgs: string[] = []): void {
spawnBunWorkerCommand('server', ['api-key', ...extraArgs]);
}
export function runAdoptCommand(extraArgs: string[] = []): void {
ensureInstalledOrExit();
const bunPath = resolveBunOrExit();
+111
View File
@@ -0,0 +1,111 @@
import pc from 'picocolors';
import {
runServerBetaRestartCommand,
runServerBetaStartCommand,
runServerBetaStatusCommand,
runServerBetaStopCommand,
runRestartCommand,
runServerApiKeyCommand,
runStartCommand,
runStatusCommand,
runStopCommand,
} from './runtime.js';
const UNSUPPORTED_SERVER_COMMANDS = new Set([
'logs',
'doctor',
'migrate',
'export',
'import',
]);
function printServerUsage(): void {
console.error(`Usage: ${pc.bold('npx claude-mem server <command>')}`);
console.error('Commands: start, stop, restart, status, logs, doctor, migrate, export, import, api-key create|list|revoke');
}
function failUnsupported(command: string): never {
console.error(pc.red(`Server command not implemented yet: ${command}`));
console.error('This CLI route is reserved for the server runtime, but no backend API exists for it yet.');
process.exit(1);
}
function runWorkerLifecycleCommand(command: string): boolean {
switch (command) {
case 'start':
runStartCommand();
return true;
case 'stop':
runStopCommand();
return true;
case 'restart':
runRestartCommand();
return true;
case 'status':
runStatusCommand();
return true;
default:
return false;
}
}
function runServerBetaLifecycleCommand(command: string): boolean {
switch (command) {
case 'start':
runServerBetaStartCommand();
return true;
case 'stop':
runServerBetaStopCommand();
return true;
case 'restart':
runServerBetaRestartCommand();
return true;
case 'status':
runServerBetaStatusCommand();
return true;
default:
return false;
}
}
export async function runServerCommand(argv: string[] = []): Promise<void> {
const subCommand = argv[0]?.toLowerCase();
if (!subCommand) {
printServerUsage();
process.exit(1);
}
if (UNSUPPORTED_SERVER_COMMANDS.has(subCommand)) {
failUnsupported(`server ${subCommand}`);
}
if (runServerBetaLifecycleCommand(subCommand)) {
return;
}
if (subCommand === 'api-key') {
const apiKeyCommand = argv[1]?.toLowerCase();
if (apiKeyCommand === 'create' || apiKeyCommand === 'list' || apiKeyCommand === 'revoke') {
runServerApiKeyCommand(argv.slice(1));
return;
}
console.error(pc.red(`Unknown server api-key subcommand: ${apiKeyCommand ?? '(none)'}`));
console.error('Usage: npx claude-mem server api-key create|list|revoke');
process.exit(1);
}
console.error(pc.red(`Unknown server command: ${subCommand}`));
printServerUsage();
process.exit(1);
}
export function runWorkerAliasCommand(argv: string[] = []): void {
const subCommand = argv[0]?.toLowerCase();
if (!subCommand || !runWorkerLifecycleCommand(subCommand)) {
console.error(pc.red(`Unknown worker command: ${subCommand ?? '(none)'}`));
console.error('Usage: npx claude-mem worker start|stop|restart|status');
process.exit(1);
}
}
+23
View File
@@ -36,6 +36,17 @@ ${pc.bold('Runtime Commands')} (requires Bun, delegates to installed plugin):
${pc.cyan('npx claude-mem stop')} Stop worker service
${pc.cyan('npx claude-mem restart')} Restart worker service
${pc.cyan('npx claude-mem status')} Show worker status
${pc.cyan('npx claude-mem server start')} Start server service
${pc.cyan('npx claude-mem server stop')} Stop server service
${pc.cyan('npx claude-mem server restart')} Restart server service
${pc.cyan('npx claude-mem server status')} Show server status
${pc.cyan('npx claude-mem server logs')} Show recent server logs
${pc.cyan('npx claude-mem server doctor')} Check server configuration (not yet implemented)
${pc.cyan('npx claude-mem server migrate')} Run server migrations (not yet implemented)
${pc.cyan('npx claude-mem server export')} Export server data (not yet implemented)
${pc.cyan('npx claude-mem server import')} Import server data (not yet implemented)
${pc.cyan('npx claude-mem server api-key create|list|revoke')} Manage API keys (not yet implemented)
${pc.cyan('npx claude-mem worker start|stop|restart|status')} Worker compatibility aliases
${pc.cyan('npx claude-mem search <query>')} Search observations
${pc.cyan('npx claude-mem adopt [--dry-run] [--branch <name>]')} Stamp merged worktrees into parent project
${pc.cyan('npx claude-mem cleanup [--dry-run]')} Run one-time v12.4.3 pollution cleanup (or preview counts)
@@ -139,6 +150,18 @@ async function main(): Promise<void> {
break;
}
case 'server': {
const { runServerCommand } = await import('./commands/server.js');
await runServerCommand(args.slice(1));
break;
}
case 'worker': {
const { runWorkerAliasCommand } = await import('./commands/server.js');
runWorkerAliasCommand(args.slice(1));
break;
}
case 'search': {
const { runSearchCommand } = await import('./commands/runtime.js');
await runSearchCommand(args.slice(1));
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
import type { Application } from 'express';
import type { Database } from 'bun:sqlite';
import type { RouteHandler } from '../../services/server/Server.js';
type NodeHandler = ReturnType<typeof import('better-auth/node').toNodeHandler>;
const cachedHandlers = new WeakMap<Database, NodeHandler>();
async function getBetterAuthHandler(database: Database): Promise<NodeHandler> {
const cachedHandler = cachedHandlers.get(database);
if (cachedHandler) {
return cachedHandler;
}
const [{ toNodeHandler }, { createAuth }] = await Promise.all([
import('better-auth/node'),
import('./auth.js'),
]);
const handler = toNodeHandler(createAuth(database));
cachedHandlers.set(database, handler);
return handler;
}
export class BetterAuthRoutes implements RouteHandler {
constructor(private readonly getDatabase: () => Database) {}
setupRoutes(app: Application): void {
app.all('/api/auth/*splat', async (req, res, next) => {
try {
const handler = await getBetterAuthHandler(this.getDatabase());
await handler(req, res);
} catch (error) {
next(error);
}
});
}
}
+118
View File
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
import { createHash, randomBytes } from 'crypto';
import { Database } from 'bun:sqlite';
import { AuthRepository, ensureServerStorageSchema } from '../../storage/sqlite/index.js';
import type { ApiKey } from '../../core/schemas/auth.js';
export interface CreatedServerApiKey {
rawKey: string;
record: ApiKey;
}
export interface VerifiedServerApiKey {
record: ApiKey;
teamId: string | null;
projectId: string | null;
scopes: string[];
}
export interface CreateServerApiKeyInput {
name: string;
teamId?: string | null;
projectId?: string | null;
scopes?: string[];
expiresAtEpoch?: number | null;
metadata?: Record<string, unknown>;
}
export function hashServerApiKey(rawKey: string): string {
return createHash('sha256').update(rawKey).digest('hex');
}
export function createRawServerApiKey(): string {
return `cmem_${randomBytes(32).toString('base64url')}`;
}
export function createServerApiKey(db: Database, input: CreateServerApiKeyInput): CreatedServerApiKey {
ensureServerStorageSchema(db);
const rawKey = createRawServerApiKey();
const repo = new AuthRepository(db);
const record = repo.createApiKey({
name: input.name,
teamId: input.teamId ?? null,
projectId: input.projectId ?? null,
keyHash: hashServerApiKey(rawKey),
prefix: rawKey.slice(0, 10),
scopes: input.scopes ?? [],
expiresAtEpoch: input.expiresAtEpoch ?? null,
metadata: input.metadata ?? {},
});
repo.createAuditLog({
teamId: record.teamId,
projectId: record.projectId,
actorType: 'system',
action: 'api_key.create',
targetType: 'api_key',
targetId: record.id,
});
return { rawKey, record };
}
export function verifyServerApiKey(
db: Database,
rawKey: string,
requiredScopes: string[] = [],
): VerifiedServerApiKey | null {
ensureServerStorageSchema(db);
const repo = new AuthRepository(db);
const record = repo.getApiKeyByHash(hashServerApiKey(rawKey));
if (!record || record.status !== 'active') {
return null;
}
if (record.expiresAtEpoch !== null && record.expiresAtEpoch <= Date.now()) {
return null;
}
if (!hasRequiredScopes(record.scopes, requiredScopes)) {
return null;
}
repo.markApiKeyUsed(record.id);
return {
record,
teamId: record.teamId,
projectId: record.projectId,
scopes: record.scopes,
};
}
export function listServerApiKeys(db: Database): ApiKey[] {
ensureServerStorageSchema(db);
return new AuthRepository(db).listApiKeys();
}
export function revokeServerApiKey(db: Database, id: string): ApiKey | null {
ensureServerStorageSchema(db);
const repo = new AuthRepository(db);
const record = repo.revokeApiKey(id);
if (record) {
repo.createAuditLog({
teamId: record.teamId,
projectId: record.projectId,
actorType: 'system',
action: 'api_key.revoke',
targetType: 'api_key',
targetId: record.id,
});
}
return record;
}
function hasRequiredScopes(grantedScopes: string[], requiredScopes: string[]): boolean {
if (requiredScopes.length === 0 || grantedScopes.includes('*')) {
return true;
}
return requiredScopes.every(scope => grantedScopes.includes(scope));
}
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
import type { Database } from 'bun:sqlite';
import { betterAuth } from 'better-auth';
import { apiKey } from '@better-auth/api-key';
import { organization } from 'better-auth/plugins';
import { DATA_DIR, ensureDir } from '../../shared/paths.js';
export function createAuth(database: Database) {
ensureDir(DATA_DIR);
return betterAuth({
database,
baseURL: process.env.BETTER_AUTH_URL ?? process.env.CLAUDE_MEM_SERVER_URL ?? 'http://127.0.0.1:37777',
basePath: '/api/auth',
plugins: [
apiKey(),
organization({
teams: {
enabled: true,
},
}),
],
});
}
+215
View File
@@ -0,0 +1,215 @@
// SPDX-License-Identifier: Apache-2.0
import {
Queue,
Worker,
type Job,
type JobsOptions,
type Processor,
type QueueOptions,
type WorkerOptions
} from 'bullmq';
import { logger } from '../../utils/logger.js';
import type { RedisQueueConfig } from '../queue/redis-config.js';
// BullMQ Worker docs: https://docs.bullmq.io/guide/workers
// BullMQ Concurrency: https://docs.bullmq.io/guide/workers/concurrency
// BullMQ Stalled Jobs: https://docs.bullmq.io/guide/jobs/stalled
//
// ServerJobQueue is a thin wrapper around the BullMQ Queue + Worker pair for
// one named queue. It enforces:
// - autorun: false on every Worker (start() is called explicitly)
// - default concurrency: 1 (per-kind concurrency tuning happens later)
// - an attached `error` listener on every Worker (BullMQ docs require this
// to avoid unhandled-error crashes when a job throws)
// Postgres outbox is canonical history; BullMQ is the execution transport
// only. Do not treat completed/failed Worker state as authoritative.
export interface ServerJobCounts {
waiting: number;
active: number;
delayed: number;
failed: number;
completed: number;
}
export interface ServerJobQueueOptions<TPayload> {
name: string;
config: RedisQueueConfig;
concurrency?: number;
lockDurationMs?: number;
defaultJobOptions?: JobsOptions;
// Test seams: allow injecting fakes without touching Redis.
queueFactory?: (name: string, options: QueueOptions) => Pick<
Queue<TPayload>,
'add' | 'getJob' | 'getJobCounts' | 'remove' | 'close'
>;
workerFactory?: (
name: string,
processor: Processor<TPayload> | null,
options: WorkerOptions
) => Pick<Worker<TPayload>, 'on' | 'run' | 'close'>;
}
const DEFAULT_LOCK_DURATION_MS = 5 * 60 * 1000;
export class ServerJobQueue<TPayload extends object = object> {
readonly name: string;
private readonly config: RedisQueueConfig;
private readonly concurrency: number;
private readonly lockDurationMs: number;
private readonly defaultJobOptions: JobsOptions;
private readonly queueFactory?: ServerJobQueueOptions<TPayload>['queueFactory'];
private readonly workerFactory?: ServerJobQueueOptions<TPayload>['workerFactory'];
private queue: ReturnType<NonNullable<ServerJobQueueOptions<TPayload>['queueFactory']>> | Queue<TPayload> | null = null;
private worker: ReturnType<NonNullable<ServerJobQueueOptions<TPayload>['workerFactory']>> | Worker<TPayload> | null = null;
private started = false;
constructor(options: ServerJobQueueOptions<TPayload>) {
this.name = options.name;
this.config = options.config;
this.concurrency = options.concurrency ?? 1;
this.lockDurationMs = options.lockDurationMs ?? DEFAULT_LOCK_DURATION_MS;
this.defaultJobOptions = options.defaultJobOptions ?? {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
removeOnComplete: { age: 7 * 24 * 60 * 60, count: 1000 },
removeOnFail: { age: 30 * 24 * 60 * 60, count: 1000 }
};
this.queueFactory = options.queueFactory;
this.workerFactory = options.workerFactory;
}
private getQueue(): NonNullable<typeof this.queue> {
if (this.queue) {
return this.queue;
}
const queueOptions: QueueOptions = {
connection: this.config.connection,
prefix: this.config.prefix,
defaultJobOptions: this.defaultJobOptions
};
this.queue = this.queueFactory
? this.queueFactory(this.name, queueOptions)
: new Queue<TPayload>(this.name, queueOptions);
return this.queue;
}
async add(jobId: string, payload: TPayload, options?: JobsOptions): Promise<void> {
if (jobId.includes(':')) {
throw new Error(`server job ID must not contain ':' (got ${jobId})`);
}
try {
await (this.getQueue().add as (
name: string,
data: TPayload,
opts?: JobsOptions
) => Promise<unknown>)(this.name, payload, {
...this.defaultJobOptions,
...options,
jobId
});
} catch (error) {
throw this.toRedisUnavailableError(error);
}
}
async getJob(jobId: string): Promise<Job<TPayload> | null | undefined> {
try {
return (await this.getQueue().getJob(jobId)) as Job<TPayload> | null | undefined;
} catch (error) {
throw this.toRedisUnavailableError(error);
}
}
async remove(jobId: string): Promise<void> {
try {
await this.getQueue().remove(jobId);
} catch (error) {
throw this.toRedisUnavailableError(error);
}
}
async getCounts(): Promise<ServerJobCounts> {
try {
const counts = await this.getQueue().getJobCounts(
'waiting',
'active',
'delayed',
'failed',
'completed'
);
return {
waiting: counts.waiting ?? 0,
active: counts.active ?? 0,
delayed: counts.delayed ?? 0,
failed: counts.failed ?? 0,
completed: counts.completed ?? 0
};
} catch (error) {
throw this.toRedisUnavailableError(error);
}
}
// BullMQ docs require `worker.on('error', ...)` to avoid unhandled rejections
// when a job throws. We construct the Worker with autorun: false so the
// caller controls startup explicitly via run().
start(processor: Processor<TPayload>): void {
if (this.started) {
throw new Error(`ServerJobQueue ${this.name} is already started`);
}
const workerOptions: WorkerOptions = {
connection: this.config.connection,
prefix: this.config.prefix,
autorun: false,
concurrency: this.concurrency,
lockDuration: this.lockDurationMs
};
const worker = this.workerFactory
? this.workerFactory(this.name, processor, workerOptions)
: new Worker<TPayload>(this.name, processor, workerOptions);
worker.on('error', (error: unknown) => {
logger.warn('QUEUE', `${this.name} worker error`, {
error: error instanceof Error ? error.message : String(error)
});
});
worker.run();
this.worker = worker;
this.started = true;
}
isStarted(): boolean {
return this.started;
}
async close(): Promise<void> {
const errors: Error[] = [];
if (this.worker) {
try {
await this.worker.close();
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
this.worker = null;
this.started = false;
}
if (this.queue) {
try {
await this.queue.close();
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
this.queue = null;
}
if (errors.length > 0) {
throw errors[0];
}
}
private toRedisUnavailableError(error: unknown): Error {
const message = error instanceof Error ? error.message : String(error);
return new Error(
`ServerJobQueue ${this.name} requires Redis/Valkey when CLAUDE_MEM_QUEUE_ENGINE=bullmq: ${message}`
);
}
}
+30
View File
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
import { createHash } from 'crypto';
import { SERVER_JOB_KIND_PREFIX, type ServerGenerationJobKind } from './types.js';
export interface ServerJobIdParts {
kind: ServerGenerationJobKind;
team_id: string;
project_id: string;
source_type: string;
source_id: string;
}
// SHA-256-derived deterministic IDs avoid Redis key collisions across tenants
// and keep BullMQ jobId deduplication intact across process restarts.
// Format: `${kindPrefix}_${sha256hex}` with NO ':' characters (BullMQ uses ':'
// internally as a key separator; embedding ':' in jobIds causes scan/state
// confusion).
export function buildServerJobId(parts: ServerJobIdParts): string {
const prefix = SERVER_JOB_KIND_PREFIX[parts.kind];
const canonical = JSON.stringify({
kind: parts.kind,
team_id: parts.team_id,
project_id: parts.project_id,
source_type: parts.source_type,
source_id: parts.source_id
});
const digest = createHash('sha256').update(canonical).digest('hex');
return `${prefix}_${digest}`;
}
+295
View File
@@ -0,0 +1,295 @@
// SPDX-License-Identifier: Apache-2.0
import type {
PostgresObservationGenerationJob,
PostgresObservationGenerationJobEventsRepository,
PostgresObservationGenerationJobRepository
} from '../../storage/postgres/generation-jobs.js';
import type { JsonObject } from '../../storage/postgres/utils.js';
import { logger } from '../../utils/logger.js';
import { buildServerJobId } from './job-id.js';
import type { ServerJobQueue } from './ServerJobQueue.js';
import type {
GenerateObservationsForEventJob,
GenerateSessionSummaryJob,
ReindexObservationJob,
ServerGenerationJobKind
} from './types.js';
// Postgres outbox is canonical history; BullMQ is the execution transport.
// Each outbox row corresponds to one observation_generation_jobs row, keyed
// by a deterministic BullMQ jobId so duplicate enqueues collapse on the
// transport side and dedup is enforced again by the row's idempotency_key.
export type SingleSourceJobPayload =
| GenerateObservationsForEventJob
| GenerateSessionSummaryJob
| ReindexObservationJob;
const KIND_TO_JOB_TYPE: Record<SingleSourceJobPayload['kind'], string> = {
event: 'observation_generate_for_event',
summary: 'observation_generate_session_summary',
reindex: 'observation_reindex'
};
export interface OutboxScope {
projectId: string;
teamId: string;
}
export interface EnqueueOutboxRowInput {
payload: SingleSourceJobPayload;
agentEventId?: string | null;
serverSessionId?: string | null;
maxAttempts?: number;
}
// `enqueueOutbox` writes the canonical row first, then publishes to BullMQ.
// If the BullMQ add() throws (for example Redis is unavailable), the row is
// transitioned to `failed` so the next reconciliation pass can resurrect it
// rather than leaving stale `queued` rows that never enter the transport.
export async function enqueueOutbox(
jobRepo: PostgresObservationGenerationJobRepository,
eventsRepo: PostgresObservationGenerationJobEventsRepository,
queue: ServerJobQueue<SingleSourceJobPayload>,
input: EnqueueOutboxRowInput
): Promise<{ row: PostgresObservationGenerationJob; bullmqJobId: string }> {
const { payload } = input;
const bullmqJobId = buildServerJobId({
kind: payload.kind,
team_id: payload.team_id,
project_id: payload.project_id,
source_type: payload.source_type,
source_id: payload.source_id
});
const row = await jobRepo.create({
projectId: payload.project_id,
teamId: payload.team_id,
sourceType: payload.source_type,
sourceId: payload.source_id,
agentEventId: input.agentEventId ?? extractAgentEventId(payload),
serverSessionId: input.serverSessionId ?? extractServerSessionId(payload),
jobType: KIND_TO_JOB_TYPE[payload.kind],
bullmqJobId,
maxAttempts: input.maxAttempts,
payload: payload as unknown as JsonObject
});
await eventsRepo.append({
generationJobId: row.id,
projectId: row.projectId,
teamId: row.teamId,
eventType: 'queued',
statusAfter: row.status,
attempt: row.attempts
});
try {
await queue.add(bullmqJobId, payload);
await eventsRepo.append({
generationJobId: row.id,
projectId: row.projectId,
teamId: row.teamId,
eventType: 'enqueued',
statusAfter: row.status,
attempt: row.attempts
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.warn('QUEUE', `failed to publish to BullMQ for job ${row.id}: ${message}`);
await jobRepo.transitionStatus({
id: row.id,
projectId: row.projectId,
teamId: row.teamId,
status: 'failed',
lastError: { message, source: 'bullmq_publish' }
});
await eventsRepo.append({
generationJobId: row.id,
projectId: row.projectId,
teamId: row.teamId,
eventType: 'failed',
statusAfter: 'failed',
attempt: row.attempts,
details: { source: 'bullmq_publish', message }
});
throw error;
}
return { row, bullmqJobId };
}
// `reconcileOnStartup` re-enqueues outbox rows that were left in `queued` or
// `processing` after a crash or restart. For each row we replace any
// terminal BullMQ job that may still be holding the deterministic ID slot
// (BullMQ refuses to re-add a jobId that already exists in `completed` or
// `failed` lists). Reconciliation is a no-op for rows past max_attempts.
export async function reconcileOnStartup(
jobRepo: PostgresObservationGenerationJobRepository,
eventsRepo: PostgresObservationGenerationJobEventsRepository,
queue: ServerJobQueue<SingleSourceJobPayload>,
scope: OutboxScope,
options?: { limit?: number }
): Promise<{ requeued: number; skipped: number }> {
const limit = options?.limit ?? 500;
const queued = await jobRepo.listByStatusForScope({
status: 'queued',
projectId: scope.projectId,
teamId: scope.teamId,
limit
});
const processing = await jobRepo.listByStatusForScope({
status: 'processing',
projectId: scope.projectId,
teamId: scope.teamId,
limit
});
let requeued = 0;
let skipped = 0;
for (const row of [...processing, ...queued]) {
if (row.attempts >= row.maxAttempts) {
skipped += 1;
continue;
}
const bullmqJobId = row.bullmqJobId ?? buildServerJobId(extractIdParts(row));
try {
await queue.remove(bullmqJobId);
} catch (error) {
logger.debug?.('QUEUE', `remove before re-add ignored for ${bullmqJobId}`, {
error: error instanceof Error ? error.message : String(error)
});
}
if (row.status === 'processing') {
await jobRepo.transitionStatus({
id: row.id,
projectId: row.projectId,
teamId: row.teamId,
status: 'queued'
});
await eventsRepo.append({
generationJobId: row.id,
projectId: row.projectId,
teamId: row.teamId,
eventType: 'queued',
statusAfter: 'queued',
attempt: row.attempts,
details: { source: 'reconcile_on_startup' }
});
}
await queue.add(bullmqJobId, row.payload as unknown as SingleSourceJobPayload);
await eventsRepo.append({
generationJobId: row.id,
projectId: row.projectId,
teamId: row.teamId,
eventType: 'enqueued',
statusAfter: 'queued',
attempt: row.attempts,
details: { source: 'reconcile_on_startup' }
});
requeued += 1;
}
return { requeued, skipped };
}
export async function markCompleted(
jobRepo: PostgresObservationGenerationJobRepository,
eventsRepo: PostgresObservationGenerationJobEventsRepository,
input: { id: string; projectId: string; teamId: string; details?: JsonObject }
): Promise<void> {
const updated = await jobRepo.transitionStatus({
id: input.id,
projectId: input.projectId,
teamId: input.teamId,
status: 'completed'
});
if (!updated) {
throw new Error(`generation job ${input.id} not found for scope`);
}
await eventsRepo.append({
generationJobId: updated.id,
projectId: updated.projectId,
teamId: updated.teamId,
eventType: 'completed',
statusAfter: 'completed',
attempt: updated.attempts,
details: input.details ?? {}
});
}
export async function markFailed(
jobRepo: PostgresObservationGenerationJobRepository,
eventsRepo: PostgresObservationGenerationJobEventsRepository,
input: {
id: string;
projectId: string;
teamId: string;
error: { message: string; source?: string };
nextAttemptAt?: Date | null;
}
): Promise<void> {
const status = input.nextAttemptAt ? 'queued' : 'failed';
const updated = await jobRepo.transitionStatus({
id: input.id,
projectId: input.projectId,
teamId: input.teamId,
status,
nextAttemptAt: input.nextAttemptAt ?? null,
lastError: { message: input.error.message, source: input.error.source ?? 'processor' }
});
if (!updated) {
throw new Error(`generation job ${input.id} not found for scope`);
}
await eventsRepo.append({
generationJobId: updated.id,
projectId: updated.projectId,
teamId: updated.teamId,
eventType: status === 'queued' ? 'retry_scheduled' : 'failed',
statusAfter: status,
attempt: updated.attempts,
details: { message: input.error.message, source: input.error.source ?? 'processor' }
});
}
function extractAgentEventId(payload: SingleSourceJobPayload): string | null {
return payload.kind === 'event' ? payload.agent_event_id : null;
}
function extractServerSessionId(payload: SingleSourceJobPayload): string | null {
return payload.kind === 'summary' ? payload.server_session_id : null;
}
function extractIdParts(row: PostgresObservationGenerationJob): {
kind: ServerGenerationJobKind;
team_id: string;
project_id: string;
source_type: string;
source_id: string;
} {
const kind = jobTypeToKind(row.jobType);
return {
kind,
team_id: row.teamId,
project_id: row.projectId,
source_type: row.sourceType,
source_id: row.sourceId
};
}
function jobTypeToKind(jobType: string): ServerGenerationJobKind {
for (const [kind, type] of Object.entries(KIND_TO_JOB_TYPE) as Array<
[SingleSourceJobPayload['kind'], string]
>) {
if (type === jobType) {
return kind;
}
}
throw new Error(`unknown observation generation job_type: ${jobType}`);
}
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
import type {
ObservationGenerationJobSourceType,
ObservationGenerationJobStatus
} from '../../storage/postgres/generation-jobs.js';
export type ServerGenerationJobKind = 'event' | 'event-batch' | 'summary' | 'reindex';
export type ServerGenerationJobStatus = ObservationGenerationJobStatus;
export interface ServerGenerationJob {
kind: ServerGenerationJobKind;
team_id: string;
project_id: string;
source_type: ObservationGenerationJobSourceType;
source_id: string;
generation_job_id: string;
}
export interface GenerateObservationsForEventJob extends ServerGenerationJob {
kind: 'event';
agent_event_id: string;
}
export interface GenerateObservationsForEventBatchJob extends ServerGenerationJob {
kind: 'event-batch';
agent_event_ids: string[];
}
export interface GenerateSessionSummaryJob extends ServerGenerationJob {
kind: 'summary';
server_session_id: string;
}
export interface ReindexObservationJob extends ServerGenerationJob {
kind: 'reindex';
observation_id: string;
}
export type ServerGenerationJobPayload =
| GenerateObservationsForEventJob
| GenerateObservationsForEventBatchJob
| GenerateSessionSummaryJob
| ReindexObservationJob;
export const SERVER_JOB_QUEUE_NAMES: Record<ServerGenerationJobKind, string> = {
event: 'server_beta_generate_event',
'event-batch': 'server_beta_generate_event_batch',
summary: 'server_beta_generate_summary',
reindex: 'server_beta_reindex'
};
export const SERVER_JOB_KIND_PREFIX: Record<ServerGenerationJobKind, string> = {
event: 'evt',
'event-batch': 'evtb',
summary: 'sum',
reindex: 'rdx'
};
+12
View File
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
export const serverMemoryPrompts = [
{
name: 'record_decision',
description: 'Capture a project decision in Claude-Mem Server memory.',
arguments: [
{ name: 'projectId', description: 'Server project id', required: true },
{ name: 'decision', description: 'Decision text', required: true },
],
},
] as const;
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
import { serverMemoryPrompts } from './prompts.js';
import { serverMemoryResources } from './resources.js';
import { serverMemoryTools } from './tools.js';
export function getServerMcpSurface() {
return {
tools: serverMemoryTools,
resources: serverMemoryResources,
prompts: serverMemoryPrompts,
};
}
+16
View File
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
export const serverMemoryResources = [
{
uri: 'claude-mem://server/projects',
name: 'Claude-Mem Server Projects',
description: 'Authorized project list exposed by Claude-Mem Server.',
mimeType: 'application/json',
},
{
uri: 'claude-mem://server/memories/recent',
name: 'Recent Claude-Mem Server Memories',
description: 'Recent authorized memory items from the server core.',
mimeType: 'application/json',
},
] as const;
+96
View File
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: Apache-2.0
export interface ServerMcpToolDefinition {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, unknown>;
required?: string[];
};
}
export const serverMemoryTools: ServerMcpToolDefinition[] = [
{
name: 'memory_add',
description: 'Add a team-scoped memory item to Claude-Mem Server.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
kind: { type: 'string', enum: ['observation', 'summary', 'prompt', 'manual'] },
type: { type: 'string' },
title: { type: 'string' },
narrative: { type: 'string' },
facts: { type: 'array', items: { type: 'string' } },
},
required: ['projectId', 'kind', 'type'],
},
},
{
name: 'memory_search',
description: 'Search server memory items within the authorized project/team scope.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
query: { type: 'string' },
limit: { type: 'number', minimum: 1, maximum: 100 },
},
required: ['projectId', 'query'],
},
},
{
name: 'memory_context',
description: 'Build a compact context pack from matching server memories.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
query: { type: 'string' },
limit: { type: 'number', minimum: 1, maximum: 50 },
},
required: ['projectId', 'query'],
},
},
{
name: 'memory_forget',
description: 'Forget or tombstone a memory item in the authorized project/team scope.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
memoryId: { type: 'string' },
reason: { type: 'string' },
},
required: ['projectId', 'memoryId'],
},
},
{
name: 'memory_list_recent',
description: 'List recent server memories for an authorized project.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
limit: { type: 'number', minimum: 1, maximum: 100 },
},
required: ['projectId'],
},
},
{
name: 'memory_record_decision',
description: 'Record an architectural or product decision as a server memory.',
inputSchema: {
type: 'object',
properties: {
projectId: { type: 'string' },
title: { type: 'string' },
decision: { type: 'string' },
rationale: { type: 'string' },
consequences: { type: 'array', items: { type: 'string' } },
},
required: ['projectId', 'title', 'decision'],
},
},
];
+125
View File
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: Apache-2.0
import type { Database } from 'bun:sqlite';
import type { NextFunction, Request, RequestHandler, Response } from 'express';
import { verifyServerApiKey } from '../auth/api-key-service.js';
export interface AuthContext {
userId: string | null;
organizationId: string | null;
teamId: string | null;
projectId: string | null;
scopes: string[];
apiKeyId: string | null;
mode: 'api-key' | 'local-dev';
}
declare module 'express-serve-static-core' {
interface Request {
authContext?: AuthContext;
}
}
export interface RequireAuthOptions {
requiredScopes?: string[];
authMode?: string;
allowLocalDevBypass?: boolean;
}
export function requireServerAuth(
getDatabase: () => Database,
options: RequireAuthOptions = {},
): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
const authMode = options.authMode ?? process.env.CLAUDE_MEM_AUTH_MODE ?? 'api-key';
const authorization = req.header('authorization') ?? '';
const rawKey = parseBearerToken(authorization);
const allowLocalDevBypass = options.allowLocalDevBypass ?? process.env.CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS === '1';
if (
!rawKey
&& authMode === 'local-dev'
&& allowLocalDevBypass
&& isLocalhost(req)
&& hasLoopbackHostHeader(req)
&& !hasForwardedClientHeaders(req)
) {
req.authContext = {
userId: null,
organizationId: null,
teamId: null,
projectId: null,
scopes: ['local-dev'],
apiKeyId: null,
mode: 'local-dev',
};
next();
return;
}
if (!rawKey) {
res.status(401).json({ error: 'Unauthorized', message: 'Missing bearer API key' });
return;
}
const verified = verifyServerApiKey(getDatabase(), rawKey, options.requiredScopes ?? []);
if (!verified) {
res.status(403).json({ error: 'Forbidden', message: 'Invalid API key or insufficient scope' });
return;
}
req.authContext = {
userId: null,
organizationId: null,
teamId: verified.teamId,
projectId: verified.projectId,
scopes: verified.scopes,
apiKeyId: verified.record.id,
mode: 'api-key',
};
next();
};
}
function parseBearerToken(header: string): string | null {
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
return match?.[1]?.trim() || null;
}
function isLocalhost(req: Request): boolean {
const clientIp = req.ip || req.socket.remoteAddress || '';
return clientIp === '127.0.0.1'
|| clientIp === '::1'
|| clientIp === '::ffff:127.0.0.1'
|| clientIp === 'localhost';
}
function hasLoopbackHostHeader(req: Request): boolean {
const host = parseHostWithoutPort(req.header('host') ?? '');
return host === '127.0.0.1'
|| host === 'localhost'
|| host === '::1';
}
function parseHostWithoutPort(rawHost: string): string {
const host = rawHost.trim().toLowerCase();
if (host.startsWith('[')) {
const closeBracketIndex = host.indexOf(']');
return closeBracketIndex === -1 ? host : host.slice(1, closeBracketIndex);
}
const lastColonIndex = host.lastIndexOf(':');
if (lastColonIndex > -1 && /^\d+$/.test(host.slice(lastColonIndex + 1))) {
return host.slice(0, lastColonIndex);
}
return host;
}
function hasForwardedClientHeaders(req: Request): boolean {
return Boolean(
req.header('forwarded')
|| req.header('x-forwarded-for')
|| req.header('x-forwarded-host')
|| req.header('x-real-ip')
);
}

Some files were not shown because too many files have changed in this diff Show More