(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string)
| 1578 | // ─── Unified scope API ─── |
| 1579 | |
| 1580 | private handleMemoryScope(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void { |
| 1581 | const chunkId = urlPath.split("/")[3]; |
| 1582 | this.readBody(req, async (body) => { |
| 1583 | try { |
| 1584 | const parsed = JSON.parse(body || "{}"); |
| 1585 | const scope = parsed.scope as string; |
| 1586 | if (!["private", "local", "team"].includes(scope)) { |
| 1587 | return this.jsonResponse(res, { ok: false, error: "scope must be 'private', 'local', or 'team'" }, 400); |
| 1588 | } |
| 1589 | const db = (this.store as any).db; |
| 1590 | const chunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any; |
| 1591 | if (!chunk) return this.jsonResponse(res, { ok: false, error: "not_found" }, 404); |
| 1592 | |
| 1593 | if (chunk.dedup_status && chunk.dedup_status !== "active") { |
| 1594 | return this.jsonResponse(res, { ok: false, error: "inactive_memory", message: "Merged/duplicate memories cannot be shared" }, 400); |
| 1595 | } |
| 1596 | |
| 1597 | const isLocalShared = chunk.owner === "public"; |
| 1598 | const hubMemory = this.getHubMemoryForChunk(chunkId); |
| 1599 | const isTeamShared = !!hubMemory; |
| 1600 | const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private"; |
| 1601 | |
| 1602 | if (scope === currentScope) { |
| 1603 | return this.jsonResponse(res, { ok: true, scope, changed: false }); |
| 1604 | } |
| 1605 | |
| 1606 | let hubSynced = false; |
| 1607 | |
| 1608 | if (scope === "team") { |
| 1609 | if (!isTeamShared) { |
| 1610 | const hubClient = await this.resolveHubClientAware(); |
| 1611 | const refreshedChunk = db.prepare("SELECT * FROM chunks WHERE id = ?").get(chunkId) as any; |
| 1612 | const response = await hubRequestJson(hubClient.hubUrl, hubClient.userToken, "/api/v1/hub/memories/share", { |
| 1613 | method: "POST", |
| 1614 | body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, sourceAgent: refreshedChunk.owner || "", role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }), |
| 1615 | }); |
| 1616 | if (!isLocalShared) this.store.markMemorySharedLocally(chunkId); |
| 1617 | const memoryId = String((response as any)?.memoryId ?? ""); |
| 1618 | const isHubRole = this.ctx?.config?.sharing?.role === "hub"; |
| 1619 | if (hubClient.userId && isHubRole) { |
| 1620 | const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId); |
| 1621 | this.store.upsertHubMemory({ |
| 1622 | id: memoryId || existing?.id || crypto.randomUUID(), |
| 1623 | sourceChunkId: chunkId, sourceUserId: hubClient.userId, |
| 1624 | sourceAgent: refreshedChunk.owner || "", |
| 1625 | role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary ?? "", |
| 1626 | kind: refreshedChunk.kind, groupId: null, visibility: "public", |
| 1627 | createdAt: existing?.createdAt ?? Date.now(), updatedAt: Date.now(), |
| 1628 | }); |
| 1629 | } else if (hubClient.userId) { |
| 1630 | const conn = this.store.getClientHubConnection(); |
| 1631 | this.store.upsertTeamSharedChunk(chunkId, { hubMemoryId: memoryId, visibility: "public", groupId: null, hubInstanceId: conn?.hubInstanceId ?? "" }); |
| 1632 | } |
| 1633 | hubSynced = true; |
| 1634 | } else { |
| 1635 | if (!isLocalShared) this.store.markMemorySharedLocally(chunkId); |
| 1636 | } |
| 1637 | } else if (scope === "local") { |
no test coverage detected