@gethmy/mcp 2.24.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +191 -76
- package/dist/index.js +191 -76
- package/dist/lib/api-client.js +68 -72
- package/package.json +1 -1
- package/src/api-client.ts +178 -6
- package/src/playbook-metric-warnings.ts +56 -0
- package/src/prompt-builder.ts +9 -120
- package/src/read-consumer.ts +16 -0
- package/src/server.ts +41 -11
package/dist/lib/api-client.js
CHANGED
|
@@ -488,15 +488,7 @@ ${lines.join(`
|
|
|
488
488
|
`)}`;
|
|
489
489
|
}
|
|
490
490
|
function generatePrompt(options) {
|
|
491
|
-
const {
|
|
492
|
-
card,
|
|
493
|
-
column,
|
|
494
|
-
variant,
|
|
495
|
-
customConstraints,
|
|
496
|
-
memories,
|
|
497
|
-
assembledContext,
|
|
498
|
-
assemblyId
|
|
499
|
-
} = options;
|
|
491
|
+
const { card, column, variant, customConstraints, memories, assemblyId } = options;
|
|
500
492
|
const contextOpts = {
|
|
501
493
|
includeTitle: true,
|
|
502
494
|
includeDescription: true,
|
|
@@ -574,10 +566,7 @@ ${card.description}`);
|
|
|
574
566
|
roleFraming.outputSuggestions.forEach((s) => {
|
|
575
567
|
sections.push(`- ${s}`);
|
|
576
568
|
});
|
|
577
|
-
if (
|
|
578
|
-
sections.push(`
|
|
579
|
-
${assembledContext}`);
|
|
580
|
-
} else if (memories && memories.length > 0) {
|
|
569
|
+
if (memories && memories.length > 0) {
|
|
581
570
|
sections.push(`
|
|
582
571
|
## Relevant Memories`);
|
|
583
572
|
sections.push(`*${memories.length} memories recalled from knowledge graph:*`);
|
|
@@ -588,7 +577,7 @@ ${assembledContext}`);
|
|
|
588
577
|
sections.push(memory.content);
|
|
589
578
|
}
|
|
590
579
|
}
|
|
591
|
-
const oneThingLine = synthesizeOneThing(card, subtasks, links
|
|
580
|
+
const oneThingLine = synthesizeOneThing(card, subtasks, links);
|
|
592
581
|
if (oneThingLine) {
|
|
593
582
|
sections.push(`
|
|
594
583
|
## Recommended Next Step
|
|
@@ -615,7 +604,7 @@ ${customConstraints}`);
|
|
|
615
604
|
*Card #${card.short_id} | Generated for ${variant} mode*`);
|
|
616
605
|
const prompt = sections.join(`
|
|
617
606
|
`);
|
|
618
|
-
const memoryCount =
|
|
607
|
+
const memoryCount = memories?.length ?? 0;
|
|
619
608
|
return {
|
|
620
609
|
prompt,
|
|
621
610
|
variant,
|
|
@@ -636,40 +625,7 @@ ${customConstraints}`);
|
|
|
636
625
|
version: PROMPT_TEMPLATE_VERSION
|
|
637
626
|
};
|
|
638
627
|
}
|
|
639
|
-
function
|
|
640
|
-
const result = {
|
|
641
|
-
lastSessionStatus: null,
|
|
642
|
-
lastSessionTask: null,
|
|
643
|
-
lastSessionProgress: null,
|
|
644
|
-
blockers: [],
|
|
645
|
-
procedureNextStep: null
|
|
646
|
-
};
|
|
647
|
-
const sessionMatches = assembledContext.match(/### Session:.*?\n([\s\S]*?)(?=\n###|\n## |\n---|\n\*Assembly|$)/g);
|
|
648
|
-
if (sessionMatches && sessionMatches.length > 0) {
|
|
649
|
-
const latest = sessionMatches[0];
|
|
650
|
-
if (/Completed work on/i.test(latest)) {
|
|
651
|
-
result.lastSessionStatus = "completed";
|
|
652
|
-
} else if (/Paused work on|status:\s*paused/i.test(latest)) {
|
|
653
|
-
result.lastSessionStatus = "paused";
|
|
654
|
-
}
|
|
655
|
-
const taskMatch = latest.match(/Final task:\s*(.+)/);
|
|
656
|
-
if (taskMatch)
|
|
657
|
-
result.lastSessionTask = taskMatch[1].trim();
|
|
658
|
-
const progressMatch = latest.match(/Progress:\s*(\d+)%/);
|
|
659
|
-
if (progressMatch)
|
|
660
|
-
result.lastSessionProgress = parseInt(progressMatch[1], 10);
|
|
661
|
-
}
|
|
662
|
-
const blockerMatches = assembledContext.match(/(?:blocker|blocked by|blocking):\s*(.+)/gi);
|
|
663
|
-
if (blockerMatches) {
|
|
664
|
-
result.blockers = blockerMatches.map((m) => m.replace(/(?:blocker|blocked by|blocking):\s*/i, "").trim());
|
|
665
|
-
}
|
|
666
|
-
const stepMatches = assembledContext.match(/^\d+\.\s+(?!.*\*\*\[key step\]\*\*.*✓)(.+?)(?:\s*\*\*\[key step\]\*\*)?$/gm);
|
|
667
|
-
if (stepMatches && stepMatches.length > 0) {
|
|
668
|
-
result.procedureNextStep = stepMatches[0].replace(/^\d+\.\s+/, "").replace(/\s*\*\*\[key step\]\*\*.*$/, "").trim();
|
|
669
|
-
}
|
|
670
|
-
return result;
|
|
671
|
-
}
|
|
672
|
-
function synthesizeOneThing(card, subtasks, links, assembledContext) {
|
|
628
|
+
function synthesizeOneThing(card, subtasks, links) {
|
|
673
629
|
if (card.done)
|
|
674
630
|
return null;
|
|
675
631
|
const blockers = links.filter((l) => l.display_type === "is_blocked_by" && l.direction === "incoming");
|
|
@@ -677,14 +633,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
|
|
|
677
633
|
const blocker = blockers[0];
|
|
678
634
|
return `Unblock first: resolve #${blocker.target_card.short_id} "${blocker.target_card.title}" which is blocking this card.`;
|
|
679
635
|
}
|
|
680
|
-
const session = assembledContext ? extractSessionInsights(assembledContext) : null;
|
|
681
|
-
if (session?.blockers && session.blockers.length > 0) {
|
|
682
|
-
return `Resolve blocker: ${session.blockers[0]}`;
|
|
683
|
-
}
|
|
684
|
-
if (session?.lastSessionStatus === "paused" && session.lastSessionTask) {
|
|
685
|
-
const progress = session.lastSessionProgress ? ` (was ${session.lastSessionProgress}% complete)` : "";
|
|
686
|
-
return `Resume previous session${progress}: "${session.lastSessionTask}".`;
|
|
687
|
-
}
|
|
688
636
|
if (subtasks.length > 0) {
|
|
689
637
|
const completed = subtasks.filter((s) => s.completed).length;
|
|
690
638
|
if (completed === subtasks.length) {
|
|
@@ -695,12 +643,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
|
|
|
695
643
|
return `Work on next subtask: "${nextSubtask.title}" (${completed}/${subtasks.length} done).`;
|
|
696
644
|
}
|
|
697
645
|
}
|
|
698
|
-
if (session?.procedureNextStep) {
|
|
699
|
-
return `Follow procedure: ${session.procedureNextStep}`;
|
|
700
|
-
}
|
|
701
|
-
if (session?.lastSessionStatus === "completed" && session.lastSessionTask) {
|
|
702
|
-
return `Previous session completed ("${session.lastSessionTask}"). Review results and continue with remaining work.`;
|
|
703
|
-
}
|
|
704
646
|
if (card.due_date && (card.priority === "urgent" || card.priority === "high")) {
|
|
705
647
|
return `High-priority task with deadline ${card.due_date}. Start implementation immediately.`;
|
|
706
648
|
}
|
|
@@ -885,6 +827,9 @@ var init_prompt_builder = __esm(() => {
|
|
|
885
827
|
execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
|
|
886
828
|
};
|
|
887
829
|
});
|
|
830
|
+
|
|
831
|
+
// src/api-client.ts
|
|
832
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
888
833
|
// ../harmony-shared/dist/agentStaleness.js
|
|
889
834
|
var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
|
|
890
835
|
var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
@@ -1006,6 +951,12 @@ var TIMINGS = {
|
|
|
1006
951
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
1007
952
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
1008
953
|
};
|
|
954
|
+
// ../harmony-shared/dist/fanoutSource.js
|
|
955
|
+
var FANOUT_KEY_MARKER = "harmony:fanout-item";
|
|
956
|
+
var FANOUT_KEY_RE = new RegExp(`^\\[${FANOUT_KEY_MARKER}\\]:\\s*#(\\S+)\\s*$`, "m");
|
|
957
|
+
// ../harmony-shared/dist/gateConfigError.js
|
|
958
|
+
var GATE_CONFIG_ERROR_KEY = "configError";
|
|
959
|
+
var GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
|
|
1009
960
|
// ../harmony-shared/dist/playbookStage.js
|
|
1010
961
|
var STAGE_DAEMON_OWNED_TOOLS = [
|
|
1011
962
|
"mcp__harmony__harmony_end_agent_session",
|
|
@@ -1046,6 +997,19 @@ function getRetryDelay(attempt) {
|
|
|
1046
997
|
return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
|
|
1047
998
|
}
|
|
1048
999
|
var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1000
|
+
function buildMemoryQuery(title, description) {
|
|
1001
|
+
const DESCRIPTION_CAP = 600;
|
|
1002
|
+
const trimmedTitle = title.trim();
|
|
1003
|
+
const trimmedBody = (description ?? "").trim();
|
|
1004
|
+
if (!trimmedTitle && !trimmedBody)
|
|
1005
|
+
return "";
|
|
1006
|
+
if (!trimmedTitle)
|
|
1007
|
+
return trimmedBody.slice(0, DESCRIPTION_CAP);
|
|
1008
|
+
if (!trimmedBody)
|
|
1009
|
+
return trimmedTitle;
|
|
1010
|
+
return `${trimmedTitle}
|
|
1011
|
+
${trimmedBody.slice(0, DESCRIPTION_CAP)}`;
|
|
1012
|
+
}
|
|
1049
1013
|
|
|
1050
1014
|
class Semaphore {
|
|
1051
1015
|
permits;
|
|
@@ -1351,8 +1315,12 @@ class HarmonyApiClient {
|
|
|
1351
1315
|
async updateCard(cardId, updates) {
|
|
1352
1316
|
return this.request("PATCH", `/cards/${cardId}`, updates);
|
|
1353
1317
|
}
|
|
1354
|
-
async claimCard(cardId, agentId) {
|
|
1355
|
-
const res = await this.request("PATCH", `/cards/${cardId}`, {
|
|
1318
|
+
async claimCard(cardId, agentId, opts) {
|
|
1319
|
+
const res = await this.request("PATCH", `/cards/${cardId}`, {
|
|
1320
|
+
assignedAgentId: agentId,
|
|
1321
|
+
ifAssignedAgentNull: true,
|
|
1322
|
+
...opts?.requireUnassigned ? { ifAssigneeNull: true } : {}
|
|
1323
|
+
});
|
|
1356
1324
|
return { claimed: res.claimed !== false };
|
|
1357
1325
|
}
|
|
1358
1326
|
async moveCard(cardId, columnId, position) {
|
|
@@ -1516,6 +1484,14 @@ class HarmonyApiClient {
|
|
|
1516
1484
|
params.set("sinceSeq", String(sinceSeq));
|
|
1517
1485
|
return this.request("GET", `/cards/${cardId}/agent-messages?${params.toString()}`);
|
|
1518
1486
|
}
|
|
1487
|
+
async postBudgetDecision(cardId, data) {
|
|
1488
|
+
return this.request("POST", `/cards/${cardId}/budget-decisions`, data);
|
|
1489
|
+
}
|
|
1490
|
+
async getBudgetDecisions(cardId, sinceIso) {
|
|
1491
|
+
const params = new URLSearchParams;
|
|
1492
|
+
params.set("sinceIso", sinceIso);
|
|
1493
|
+
return this.request("GET", `/cards/${cardId}/budget-decisions?${params.toString()}`);
|
|
1494
|
+
}
|
|
1519
1495
|
async updateAgentProgress(cardId, data) {
|
|
1520
1496
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1521
1497
|
}
|
|
@@ -1561,6 +1537,8 @@ class HarmonyApiClient {
|
|
|
1561
1537
|
params.set("offset", String(options.offset));
|
|
1562
1538
|
if (options.include_superseded)
|
|
1563
1539
|
params.set("include_superseded", "true");
|
|
1540
|
+
if (options.consumer)
|
|
1541
|
+
params.set("consumer", options.consumer);
|
|
1564
1542
|
if (options.include_episodes)
|
|
1565
1543
|
params.set("include_episodes", "true");
|
|
1566
1544
|
return this.request("GET", `/memory/entities?${params.toString()}`);
|
|
@@ -1616,6 +1594,12 @@ class HarmonyApiClient {
|
|
|
1616
1594
|
if (options.topK !== undefined) {
|
|
1617
1595
|
entities = entities.slice(0, options.topK);
|
|
1618
1596
|
}
|
|
1597
|
+
if (options.consumer) {
|
|
1598
|
+
const deliveredIds = entities.map((e) => e.id).filter((id) => typeof id === "string");
|
|
1599
|
+
if (deliveredIds.length > 0) {
|
|
1600
|
+
this.batchTouchMemoryEntities(deliveredIds, options.consumer).catch(() => {});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1619
1603
|
return { entities };
|
|
1620
1604
|
}
|
|
1621
1605
|
async deleteMemoryEntity(entityId) {
|
|
@@ -1624,9 +1608,10 @@ class HarmonyApiClient {
|
|
|
1624
1608
|
async touchMemoryEntity(entityId) {
|
|
1625
1609
|
return this.request("POST", `/memory/entities/${entityId}/touch`);
|
|
1626
1610
|
}
|
|
1627
|
-
async batchTouchMemoryEntities(entityIds) {
|
|
1611
|
+
async batchTouchMemoryEntities(entityIds, consumer) {
|
|
1628
1612
|
return this.request("POST", "/memory/entities/batch-touch", {
|
|
1629
|
-
entity_ids: entityIds
|
|
1613
|
+
entity_ids: entityIds,
|
|
1614
|
+
...consumer ? { consumer } : {}
|
|
1630
1615
|
});
|
|
1631
1616
|
}
|
|
1632
1617
|
async createMemoryRelation(data) {
|
|
@@ -1652,8 +1637,12 @@ class HarmonyApiClient {
|
|
|
1652
1637
|
params.append("tags", tag);
|
|
1653
1638
|
if (options?.include_superseded)
|
|
1654
1639
|
params.set("include_superseded", "true");
|
|
1640
|
+
if (options?.consumer)
|
|
1641
|
+
params.set("consumer", options.consumer);
|
|
1655
1642
|
if (options?.include_episodes)
|
|
1656
1643
|
params.set("include_episodes", "true");
|
|
1644
|
+
if (options?.assembly_id)
|
|
1645
|
+
params.set("assembly_id", options.assembly_id);
|
|
1657
1646
|
return this.request("GET", `/memory/search?${params.toString()}`);
|
|
1658
1647
|
}
|
|
1659
1648
|
async getVaultIndex(options) {
|
|
@@ -1665,6 +1654,8 @@ class HarmonyApiClient {
|
|
|
1665
1654
|
params.set("type", options.type);
|
|
1666
1655
|
if (options.limit !== undefined)
|
|
1667
1656
|
params.set("limit", String(options.limit));
|
|
1657
|
+
if (options.consumer)
|
|
1658
|
+
params.set("consumer", options.consumer);
|
|
1668
1659
|
if (options.include_episodes)
|
|
1669
1660
|
params.set("include_episodes", "true");
|
|
1670
1661
|
return this.request("GET", `/memory/index?${params.toString()}`);
|
|
@@ -1678,6 +1669,8 @@ class HarmonyApiClient {
|
|
|
1678
1669
|
params.set("type", options.type);
|
|
1679
1670
|
if (options.limit !== undefined)
|
|
1680
1671
|
params.set("limit", String(options.limit));
|
|
1672
|
+
if (options.consumer)
|
|
1673
|
+
params.set("consumer", options.consumer);
|
|
1681
1674
|
if (options.include_episodes)
|
|
1682
1675
|
params.set("include_episodes", "true");
|
|
1683
1676
|
return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
|
|
@@ -1737,6 +1730,8 @@ class HarmonyApiClient {
|
|
|
1737
1730
|
params.set("type", options.type);
|
|
1738
1731
|
if (options?.limit !== undefined)
|
|
1739
1732
|
params.set("limit", String(options.limit));
|
|
1733
|
+
if (options?.consumer)
|
|
1734
|
+
params.set("consumer", options.consumer);
|
|
1740
1735
|
if (options?.include_episodes)
|
|
1741
1736
|
params.set("include_episodes", "true");
|
|
1742
1737
|
return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
|
|
@@ -1842,14 +1837,15 @@ class HarmonyApiClient {
|
|
|
1842
1837
|
} catch {}
|
|
1843
1838
|
}
|
|
1844
1839
|
const variant = options.variant || "execute";
|
|
1845
|
-
const
|
|
1846
|
-
const assemblyId = undefined;
|
|
1840
|
+
const assemblyId = randomUUID2();
|
|
1847
1841
|
let memories;
|
|
1848
1842
|
try {
|
|
1849
1843
|
if (options.workspaceId && cardData.title) {
|
|
1850
|
-
const memoryResult = await this.searchMemoryEntities(options.workspaceId, cardData.title, {
|
|
1844
|
+
const memoryResult = await this.searchMemoryEntities(options.workspaceId, buildMemoryQuery(cardData.title, cardData.description), {
|
|
1851
1845
|
project_id: options.projectId,
|
|
1852
|
-
limit: 5
|
|
1846
|
+
limit: 5,
|
|
1847
|
+
consumer: "agent-prompt",
|
|
1848
|
+
assembly_id: assemblyId
|
|
1853
1849
|
});
|
|
1854
1850
|
if (memoryResult.entities?.length > 0) {
|
|
1855
1851
|
memories = memoryResult.entities.map((e) => ({
|
|
@@ -1873,7 +1869,6 @@ class HarmonyApiClient {
|
|
|
1873
1869
|
contextOptions: options.contextOptions,
|
|
1874
1870
|
customConstraints: options.customConstraints,
|
|
1875
1871
|
memories,
|
|
1876
|
-
assembledContext: assembledContextStr,
|
|
1877
1872
|
assemblyId
|
|
1878
1873
|
});
|
|
1879
1874
|
try {
|
|
@@ -1983,6 +1978,7 @@ export {
|
|
|
1983
1978
|
resetClient,
|
|
1984
1979
|
requestWithBearer,
|
|
1985
1980
|
getClient,
|
|
1981
|
+
buildMemoryQuery,
|
|
1986
1982
|
HarmonyUnauthorizedError,
|
|
1987
1983
|
HarmonyApiClient
|
|
1988
1984
|
};
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
type AgentRunEventDraft,
|
|
3
4
|
type Comment,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
type WorkspaceAgent,
|
|
10
11
|
} from "@harmony/shared";
|
|
11
12
|
import { getApiKey, getApiUrl } from "./config.js";
|
|
13
|
+
import type { ReadConsumer } from "./read-consumer.js";
|
|
12
14
|
|
|
13
15
|
export interface ApiResponse<T = unknown> {
|
|
14
16
|
success?: boolean;
|
|
@@ -49,6 +51,27 @@ function getRetryDelay(attempt: number): number {
|
|
|
49
51
|
|
|
50
52
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Build the recall query for a card. The title alone was the whole query, which
|
|
56
|
+
* is too thin to rank against. The description is capped because a card body
|
|
57
|
+
* runs to 10,000 characters, the embedding truncates at END, and an uncapped
|
|
58
|
+
* body drowns the title it is meant to sharpen.
|
|
59
|
+
*/
|
|
60
|
+
export function buildMemoryQuery(
|
|
61
|
+
title: string,
|
|
62
|
+
description: string | null | undefined,
|
|
63
|
+
): string {
|
|
64
|
+
const DESCRIPTION_CAP = 600;
|
|
65
|
+
const trimmedTitle = title.trim();
|
|
66
|
+
const trimmedBody = (description ?? "").trim();
|
|
67
|
+
|
|
68
|
+
if (!trimmedTitle && !trimmedBody) return "";
|
|
69
|
+
if (!trimmedTitle) return trimmedBody.slice(0, DESCRIPTION_CAP);
|
|
70
|
+
if (!trimmedBody) return trimmedTitle;
|
|
71
|
+
|
|
72
|
+
return `${trimmedTitle}\n${trimmedBody.slice(0, DESCRIPTION_CAP)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
// Semaphore for concurrency control
|
|
53
76
|
class Semaphore {
|
|
54
77
|
private permits: number;
|
|
@@ -532,7 +555,17 @@ export class HarmonyApiClient {
|
|
|
532
555
|
/** Register/upsert this daemon's virtual agent. Idempotent by (workspace, identifier). */
|
|
533
556
|
async registerWorkspaceAgent(
|
|
534
557
|
workspaceId: string,
|
|
535
|
-
data: {
|
|
558
|
+
data: {
|
|
559
|
+
identifier: string;
|
|
560
|
+
name: string;
|
|
561
|
+
color?: string;
|
|
562
|
+
/**
|
|
563
|
+
* Gate metric NAMES the daemon declares under `agent.playbooks.metrics`
|
|
564
|
+
* (#922) — names only, never the commands behind them. Omit to leave the
|
|
565
|
+
* stored report untouched.
|
|
566
|
+
*/
|
|
567
|
+
declaredGateMetrics?: string[];
|
|
568
|
+
},
|
|
536
569
|
): Promise<{ agent: WorkspaceAgent }> {
|
|
537
570
|
return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
|
|
538
571
|
}
|
|
@@ -691,15 +724,27 @@ export class HarmonyApiClient {
|
|
|
691
724
|
* Compare-and-set claim of a card for a virtual agent: sets assigned_agent_id
|
|
692
725
|
* only if it is currently NULL (server-side guard). Returns `{ claimed: false }`
|
|
693
726
|
* when another daemon already owns it. Used by the agent-agnostic review pickup.
|
|
727
|
+
*
|
|
728
|
+
* `requireUnassigned` additionally guards `assignee_id IS NULL`, so the claim
|
|
729
|
+
* also loses to a PERSON who took the card. Self-selection (sweep, card #978)
|
|
730
|
+
* needs it: this write clears `assignee_id` as a side effect, so the agent-only
|
|
731
|
+
* guard does not merely let the daemon co-own a person's card — it lets the
|
|
732
|
+
* daemon erase their assignment. Default false keeps the review-strand claim
|
|
733
|
+
* (which reaches only cards no one holds) byte-unchanged.
|
|
694
734
|
*/
|
|
695
735
|
async claimCard(
|
|
696
736
|
cardId: string,
|
|
697
737
|
agentId: string,
|
|
738
|
+
opts?: { requireUnassigned?: boolean },
|
|
698
739
|
): Promise<{ claimed: boolean }> {
|
|
699
740
|
const res = await this.request<{ card?: unknown; claimed?: boolean }>(
|
|
700
741
|
"PATCH",
|
|
701
742
|
`/cards/${cardId}`,
|
|
702
|
-
{
|
|
743
|
+
{
|
|
744
|
+
assignedAgentId: agentId,
|
|
745
|
+
ifAssignedAgentNull: true,
|
|
746
|
+
...(opts?.requireUnassigned ? { ifAssigneeNull: true } : {}),
|
|
747
|
+
},
|
|
703
748
|
);
|
|
704
749
|
// Success body is `{ card }` (no `claimed` key) → won. Lost-race body is
|
|
705
750
|
// `{ claimed: false }`.
|
|
@@ -1097,6 +1142,15 @@ export class HarmonyApiClient {
|
|
|
1097
1142
|
* working); the MCP tool dispatch defaults to `interactive`.
|
|
1098
1143
|
*/
|
|
1099
1144
|
driver?: "daemon" | "interactive" | "script";
|
|
1145
|
+
/**
|
|
1146
|
+
* When a human budget decision is pending on this session, as an ISO
|
|
1147
|
+
* timestamp (card #915). A run START passes `null` explicitly: the route
|
|
1148
|
+
* REUSES a live session row, so a card whose previous run parked would
|
|
1149
|
+
* otherwise inherit that park's deadline — the panel offering Continue
|
|
1150
|
+
* over a run that is already going again, and the sweep exemption
|
|
1151
|
+
* masking a genuinely dead run for the rest of the window.
|
|
1152
|
+
*/
|
|
1153
|
+
awaitingDecisionUntil?: string | null;
|
|
1100
1154
|
},
|
|
1101
1155
|
): Promise<{
|
|
1102
1156
|
session: unknown;
|
|
@@ -1147,6 +1201,53 @@ export class HarmonyApiClient {
|
|
|
1147
1201
|
);
|
|
1148
1202
|
}
|
|
1149
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* Post a human's durable answer to a parked run's budget question (card
|
|
1206
|
+
* #915): grant more turns and continue, or stop and hand the card back.
|
|
1207
|
+
*
|
|
1208
|
+
* Unlike pause/resume/stop — an ephemeral Realtime broadcast nothing hears
|
|
1209
|
+
* once the daemon is offline — this MUST survive a daemon restart hours
|
|
1210
|
+
* later, so it lands as a `budget_decision` row rather than a broadcast.
|
|
1211
|
+
* The server resolves which run to decide for from the card's one active
|
|
1212
|
+
* session; the caller does not (and, deciding from the board, cannot) name
|
|
1213
|
+
* a session id.
|
|
1214
|
+
*/
|
|
1215
|
+
async postBudgetDecision(
|
|
1216
|
+
cardId: string,
|
|
1217
|
+
data: {
|
|
1218
|
+
decision: "continue" | "stop";
|
|
1219
|
+
extraTurns: number;
|
|
1220
|
+
message?: string;
|
|
1221
|
+
},
|
|
1222
|
+
): Promise<{ id: string; seq: number; createdAt: string }> {
|
|
1223
|
+
return this.request("POST", `/cards/${cardId}/budget-decisions`, data);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Drain `budget_decision` events for a card since `sinceIso` (card #915).
|
|
1228
|
+
* Keyed on card + time, not session + seq like `getPendingMessages`: the
|
|
1229
|
+
* daemon asking on startup may not know the session id of a run it parked
|
|
1230
|
+
* hours or days earlier, but it always knows the card.
|
|
1231
|
+
*/
|
|
1232
|
+
async getBudgetDecisions(
|
|
1233
|
+
cardId: string,
|
|
1234
|
+
sinceIso: string,
|
|
1235
|
+
): Promise<{
|
|
1236
|
+
decisions: Array<{
|
|
1237
|
+
decision: "continue" | "stop";
|
|
1238
|
+
extraTurns: number;
|
|
1239
|
+
message?: string;
|
|
1240
|
+
createdAt: string;
|
|
1241
|
+
}>;
|
|
1242
|
+
}> {
|
|
1243
|
+
const params = new URLSearchParams();
|
|
1244
|
+
params.set("sinceIso", sinceIso);
|
|
1245
|
+
return this.request(
|
|
1246
|
+
"GET",
|
|
1247
|
+
`/cards/${cardId}/budget-decisions?${params.toString()}`,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1150
1251
|
async updateAgentProgress(
|
|
1151
1252
|
cardId: string,
|
|
1152
1253
|
data: {
|
|
@@ -1205,6 +1306,16 @@ export class HarmonyApiClient {
|
|
|
1205
1306
|
* deferred the respawn by 10 minutes (#770). Reply carries `stopped: true`.
|
|
1206
1307
|
*/
|
|
1207
1308
|
implicitCreate?: boolean;
|
|
1309
|
+
/**
|
|
1310
|
+
* Set while the session waits for a human budget decision (#915),
|
|
1311
|
+
* cleared (null) once one arrives. Exempts the row from the daemon's
|
|
1312
|
+
* stale-session sweep until this instant — a parked run's deliberate
|
|
1313
|
+
* silence would otherwise read as abandonment well before a person can
|
|
1314
|
+
* answer. ISO timestamp string; the daemon-side mirror
|
|
1315
|
+
* (`StateStore.parkRun`'s `awaitingDecisionUntil`) is the same instant
|
|
1316
|
+
* in epoch ms, not this string.
|
|
1317
|
+
*/
|
|
1318
|
+
awaitingDecisionUntil?: string | null;
|
|
1208
1319
|
},
|
|
1209
1320
|
): Promise<{
|
|
1210
1321
|
session: unknown;
|
|
@@ -1351,6 +1462,9 @@ export class HarmonyApiClient {
|
|
|
1351
1462
|
// Opt in to agent-run episodes (default excluded, #677). Only the daemon's
|
|
1352
1463
|
// own rolling-episode lookup should set this.
|
|
1353
1464
|
include_episodes?: boolean;
|
|
1465
|
+
/** Read-accounting label (task 4) — omitted means the read is not
|
|
1466
|
+
* recorded. Callers that know their own identity should pass it. */
|
|
1467
|
+
consumer?: ReadConsumer;
|
|
1354
1468
|
}): Promise<{ entities: unknown[]; count: number }> {
|
|
1355
1469
|
const params = new URLSearchParams();
|
|
1356
1470
|
params.set("workspace_id", options.workspace_id);
|
|
@@ -1367,6 +1481,7 @@ export class HarmonyApiClient {
|
|
|
1367
1481
|
if (options.offset !== undefined)
|
|
1368
1482
|
params.set("offset", String(options.offset));
|
|
1369
1483
|
if (options.include_superseded) params.set("include_superseded", "true");
|
|
1484
|
+
if (options.consumer) params.set("consumer", options.consumer);
|
|
1370
1485
|
if (options.include_episodes) params.set("include_episodes", "true");
|
|
1371
1486
|
return this.request("GET", `/memory/entities?${params.toString()}`);
|
|
1372
1487
|
}
|
|
@@ -1419,9 +1534,17 @@ export class HarmonyApiClient {
|
|
|
1419
1534
|
// Opt in to agent-run episodes (default excluded server-side, #677). The
|
|
1420
1535
|
// daemon sets this when looking up its own rolling implement/review episode.
|
|
1421
1536
|
includeEpisodes?: boolean;
|
|
1537
|
+
/** Read-accounting label (task 4) — omitted means the read is not
|
|
1538
|
+
* recorded. Callers that know their own identity should pass it. */
|
|
1539
|
+
consumer?: ReadConsumer;
|
|
1422
1540
|
}): Promise<{ entities: unknown[] }> {
|
|
1423
1541
|
// Over-fetch beyond topK so client-side filters (multi-type, memory_tier,
|
|
1424
1542
|
// tags) have headroom — matches the MCP server's recall path (server.ts).
|
|
1543
|
+
// `consumer` is deliberately NOT forwarded on this over-fetch: the server
|
|
1544
|
+
// would record read-accounting for every over-fetched row, not just the
|
|
1545
|
+
// ones actually delivered. Instead we record the delivered set below,
|
|
1546
|
+
// after client-side filtering and the topK trim (mirrors the mcp-tool
|
|
1547
|
+
// path in server.ts).
|
|
1425
1548
|
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1426
1549
|
let entities: Array<Record<string, unknown>> = [];
|
|
1427
1550
|
|
|
@@ -1483,6 +1606,21 @@ export class HarmonyApiClient {
|
|
|
1483
1606
|
entities = entities.slice(0, options.topK);
|
|
1484
1607
|
}
|
|
1485
1608
|
|
|
1609
|
+
// Read accounting: record only the entities actually delivered to the
|
|
1610
|
+
// caller, after all client-side filtering and the topK trim — never the
|
|
1611
|
+
// larger over-fetched candidate pool the calls above deliberately left
|
|
1612
|
+
// unrecorded. Best-effort; a recording failure must not fail the recall.
|
|
1613
|
+
if (options.consumer) {
|
|
1614
|
+
const deliveredIds = entities
|
|
1615
|
+
.map((e) => e.id)
|
|
1616
|
+
.filter((id): id is string => typeof id === "string");
|
|
1617
|
+
if (deliveredIds.length > 0) {
|
|
1618
|
+
this.batchTouchMemoryEntities(deliveredIds, options.consumer).catch(
|
|
1619
|
+
() => {},
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1486
1624
|
return { entities };
|
|
1487
1625
|
}
|
|
1488
1626
|
|
|
@@ -1496,9 +1634,16 @@ export class HarmonyApiClient {
|
|
|
1496
1634
|
|
|
1497
1635
|
async batchTouchMemoryEntities(
|
|
1498
1636
|
entityIds: string[],
|
|
1637
|
+
/** Read-accounting label (task 4, fix round 1). Omitted → the route keeps
|
|
1638
|
+
* its original increment-only `batch_touch_knowledge_entities` behaviour
|
|
1639
|
+
* (legacy context-assembly callers). Given → the route instead records
|
|
1640
|
+
* through `record_entity_reads`, the same path the search chokepoint
|
|
1641
|
+
* uses, so this touch is properly attributed rather than anonymous. */
|
|
1642
|
+
consumer?: ReadConsumer,
|
|
1499
1643
|
): Promise<{ success: boolean; count: number }> {
|
|
1500
1644
|
return this.request("POST", "/memory/entities/batch-touch", {
|
|
1501
1645
|
entity_ids: entityIds,
|
|
1646
|
+
...(consumer ? { consumer } : {}),
|
|
1502
1647
|
});
|
|
1503
1648
|
}
|
|
1504
1649
|
|
|
@@ -1533,6 +1678,16 @@ export class HarmonyApiClient {
|
|
|
1533
1678
|
tags?: string[];
|
|
1534
1679
|
include_superseded?: boolean;
|
|
1535
1680
|
include_episodes?: boolean;
|
|
1681
|
+
/** Read-accounting label (task 4) — omitted means the read is not
|
|
1682
|
+
* recorded (there is deliberately no server-side default: it used to
|
|
1683
|
+
* default to "search" and inflated access_count on every
|
|
1684
|
+
* `harmony_remember` write-side probe). Callers that know their own
|
|
1685
|
+
* identity (harmony_recall) should pass it explicitly. */
|
|
1686
|
+
consumer?: ReadConsumer;
|
|
1687
|
+
/** Per-prompt-build id (task 6b) — shared by every read row recorded
|
|
1688
|
+
* during one `generateCardPrompt` call, so a card can later show which
|
|
1689
|
+
* memories shaped its prompt. */
|
|
1690
|
+
assembly_id?: string;
|
|
1536
1691
|
},
|
|
1537
1692
|
): Promise<{ entities: unknown[]; count: number }> {
|
|
1538
1693
|
const params = new URLSearchParams();
|
|
@@ -1546,7 +1701,9 @@ export class HarmonyApiClient {
|
|
|
1546
1701
|
// matches against the canonical `tags_normalized` column (#299).
|
|
1547
1702
|
for (const tag of options?.tags ?? []) params.append("tags", tag);
|
|
1548
1703
|
if (options?.include_superseded) params.set("include_superseded", "true");
|
|
1704
|
+
if (options?.consumer) params.set("consumer", options.consumer);
|
|
1549
1705
|
if (options?.include_episodes) params.set("include_episodes", "true");
|
|
1706
|
+
if (options?.assembly_id) params.set("assembly_id", options.assembly_id);
|
|
1550
1707
|
return this.request("GET", `/memory/search?${params.toString()}`);
|
|
1551
1708
|
}
|
|
1552
1709
|
|
|
@@ -1558,12 +1715,16 @@ export class HarmonyApiClient {
|
|
|
1558
1715
|
type?: string;
|
|
1559
1716
|
limit?: number;
|
|
1560
1717
|
include_episodes?: boolean;
|
|
1718
|
+
/** Read-accounting label (task 4) — omitted means the read is not
|
|
1719
|
+
* recorded. Callers that know their own identity should pass it. */
|
|
1720
|
+
consumer?: ReadConsumer;
|
|
1561
1721
|
}): Promise<{ entities: unknown[]; count: number }> {
|
|
1562
1722
|
const params = new URLSearchParams();
|
|
1563
1723
|
params.set("workspace_id", options.workspace_id);
|
|
1564
1724
|
if (options.project_id) params.set("project_id", options.project_id);
|
|
1565
1725
|
if (options.type) params.set("type", options.type);
|
|
1566
1726
|
if (options.limit !== undefined) params.set("limit", String(options.limit));
|
|
1727
|
+
if (options.consumer) params.set("consumer", options.consumer);
|
|
1567
1728
|
if (options.include_episodes) params.set("include_episodes", "true");
|
|
1568
1729
|
return this.request("GET", `/memory/index?${params.toString()}`);
|
|
1569
1730
|
}
|
|
@@ -1574,12 +1735,16 @@ export class HarmonyApiClient {
|
|
|
1574
1735
|
type?: string;
|
|
1575
1736
|
limit?: number;
|
|
1576
1737
|
include_episodes?: boolean;
|
|
1738
|
+
/** Read-accounting label (task 4) — omitted means the read is not
|
|
1739
|
+
* recorded. Callers that know their own identity should pass it. */
|
|
1740
|
+
consumer?: ReadConsumer;
|
|
1577
1741
|
}): Promise<string> {
|
|
1578
1742
|
const params = new URLSearchParams();
|
|
1579
1743
|
params.set("workspace_id", options.workspace_id);
|
|
1580
1744
|
if (options.project_id) params.set("project_id", options.project_id);
|
|
1581
1745
|
if (options.type) params.set("type", options.type);
|
|
1582
1746
|
if (options.limit !== undefined) params.set("limit", String(options.limit));
|
|
1747
|
+
if (options.consumer) params.set("consumer", options.consumer);
|
|
1583
1748
|
if (options.include_episodes) params.set("include_episodes", "true");
|
|
1584
1749
|
return this.requestRaw(
|
|
1585
1750
|
"GET",
|
|
@@ -1668,6 +1833,9 @@ export class HarmonyApiClient {
|
|
|
1668
1833
|
type?: string;
|
|
1669
1834
|
limit?: number;
|
|
1670
1835
|
include_episodes?: boolean;
|
|
1836
|
+
/** Read-accounting label (task 4) — omitted means the read is not
|
|
1837
|
+
* recorded. Callers that know their own identity should pass it. */
|
|
1838
|
+
consumer?: ReadConsumer;
|
|
1671
1839
|
},
|
|
1672
1840
|
): Promise<string> {
|
|
1673
1841
|
const params = new URLSearchParams();
|
|
@@ -1677,6 +1845,7 @@ export class HarmonyApiClient {
|
|
|
1677
1845
|
if (options?.type) params.set("type", options.type);
|
|
1678
1846
|
if (options?.limit !== undefined)
|
|
1679
1847
|
params.set("limit", String(options.limit));
|
|
1848
|
+
if (options?.consumer) params.set("consumer", options.consumer);
|
|
1680
1849
|
if (options?.include_episodes) params.set("include_episodes", "true");
|
|
1681
1850
|
return this.requestRaw(
|
|
1682
1851
|
"GET",
|
|
@@ -1978,18 +2147,22 @@ export class HarmonyApiClient {
|
|
|
1978
2147
|
// Phase 0 (memory architecture v2): full context assembly removed.
|
|
1979
2148
|
// Use the basic memory search path so callers still get _some_ memory
|
|
1980
2149
|
// hints. Phase 1 will reintroduce a session-scoped working memory layer.
|
|
1981
|
-
|
|
1982
|
-
|
|
2150
|
+
|
|
2151
|
+
// One id per prompt build. Every read row recorded during this build carries
|
|
2152
|
+
// it, which is what lets a card show the memories that shaped its prompt.
|
|
2153
|
+
const assemblyId = randomUUID();
|
|
1983
2154
|
let memories: MemoryItem[] | undefined;
|
|
1984
2155
|
|
|
1985
2156
|
try {
|
|
1986
2157
|
if (options.workspaceId && cardData.title) {
|
|
1987
2158
|
const memoryResult = await this.searchMemoryEntities(
|
|
1988
2159
|
options.workspaceId,
|
|
1989
|
-
cardData.title,
|
|
2160
|
+
buildMemoryQuery(cardData.title, cardData.description),
|
|
1990
2161
|
{
|
|
1991
2162
|
project_id: options.projectId,
|
|
1992
2163
|
limit: 5,
|
|
2164
|
+
consumer: "agent-prompt",
|
|
2165
|
+
assembly_id: assemblyId,
|
|
1993
2166
|
},
|
|
1994
2167
|
);
|
|
1995
2168
|
if (memoryResult.entities?.length > 0) {
|
|
@@ -2015,7 +2188,6 @@ export class HarmonyApiClient {
|
|
|
2015
2188
|
contextOptions: options.contextOptions,
|
|
2016
2189
|
customConstraints: options.customConstraints,
|
|
2017
2190
|
memories,
|
|
2018
|
-
assembledContext: assembledContextStr,
|
|
2019
2191
|
assemblyId,
|
|
2020
2192
|
});
|
|
2021
2193
|
|