@poncho-ai/harness 0.57.0 → 0.58.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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +16 -0
- package/dist/index.js +127 -88
- package/package.json +1 -1
- package/src/state.ts +11 -2
- package/src/storage/entries.ts +57 -2
- package/src/storage/memory-engine.ts +18 -3
- package/src/storage/sql-dialect.ts +16 -3
- package/test/entries-read-cutover.test.ts +96 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.58.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m567.32 KB[39m
|
|
12
12
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 177ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 7320ms
|
|
16
16
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m104.68 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.58.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#155](https://github.com/cesr/poncho-ai/pull/155) [`9939955`](https://github.com/cesr/poncho-ai/commit/9939955585f0ea204e070192827f0b213e84d283) Thanks [@cesr](https://github.com/cesr)! - Phase 3c read cutover: conversation reads rebuild from the append-only
|
|
8
|
+
`conversation_entries` log. Both engines' `get`/`getWithArchive` paths now
|
|
9
|
+
call `rebuildConversationFromEntries`, which overrides `_harnessMessages`
|
|
10
|
+
(via `buildLlmContext`), `messages` (via `buildDisplaySnapshot`, full
|
|
11
|
+
transcript), and `pendingSubagentResults` (via `getPendingSubagentResults`)
|
|
12
|
+
when the entry log is non-empty. Conversations that predate dual-write have
|
|
13
|
+
no entries and fall back to the mutable blob untouched — no migration
|
|
14
|
+
script needed. The rebuild is wrapped in try/catch and never throws on the
|
|
15
|
+
read path. A kill-switch (`PONCHO_READ_ENTRIES=0`) instantly reverts to
|
|
16
|
+
blob reads without a deploy. `_continuationMessages` and `pendingApprovals`
|
|
17
|
+
remain blob fields (not yet modeled as entries).
|
|
18
|
+
|
|
3
19
|
## 0.57.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/dist/index.js
CHANGED
|
@@ -2443,7 +2443,7 @@ var ponchoDocsTool = defineTool({
|
|
|
2443
2443
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2444
2444
|
import { readFile as readFile9 } from "fs/promises";
|
|
2445
2445
|
import { resolve as resolve11 } from "path";
|
|
2446
|
-
import { defineTool as defineTool12, getTextContent as getTextContent2, createLogger as
|
|
2446
|
+
import { defineTool as defineTool12, getTextContent as getTextContent2, createLogger as createLogger7, formatError as fmtErr, url as urlColor } from "@poncho-ai/sdk";
|
|
2447
2447
|
|
|
2448
2448
|
// src/upload-store.ts
|
|
2449
2449
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2776,6 +2776,87 @@ var createUploadStore = async (config, workingDir) => {
|
|
|
2776
2776
|
|
|
2777
2777
|
// src/storage/memory-engine.ts
|
|
2778
2778
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2779
|
+
|
|
2780
|
+
// src/storage/entries.ts
|
|
2781
|
+
import { createLogger as createLogger2 } from "@poncho-ai/sdk";
|
|
2782
|
+
var entriesReadLog = createLogger2("entries-read");
|
|
2783
|
+
function buildLlmContext(entries) {
|
|
2784
|
+
let latestCompaction;
|
|
2785
|
+
for (const e of entries) {
|
|
2786
|
+
if (e.type === "compaction" && (!latestCompaction || e.seq > latestCompaction.seq)) {
|
|
2787
|
+
latestCompaction = e;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
const harnessMsgs = entries.filter(
|
|
2791
|
+
(e) => e.type === "harness_message"
|
|
2792
|
+
);
|
|
2793
|
+
if (latestCompaction) {
|
|
2794
|
+
const kept = harnessMsgs.filter((e) => e.seq >= latestCompaction.firstKeptSeq).map((e) => e.message);
|
|
2795
|
+
return [latestCompaction.summaryMessage, ...kept];
|
|
2796
|
+
}
|
|
2797
|
+
return harnessMsgs.map((e) => e.message);
|
|
2798
|
+
}
|
|
2799
|
+
function buildDisplaySnapshot(entries, tailN) {
|
|
2800
|
+
const amendmentsByTarget = /* @__PURE__ */ new Map();
|
|
2801
|
+
for (const e of entries) {
|
|
2802
|
+
if (e.type === "assistant_amendment") {
|
|
2803
|
+
const list = amendmentsByTarget.get(e.targetEntryId) ?? [];
|
|
2804
|
+
list.push(e);
|
|
2805
|
+
amendmentsByTarget.set(e.targetEntryId, list);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
const built = [];
|
|
2809
|
+
for (const e of entries) {
|
|
2810
|
+
if (e.type === "user_message") {
|
|
2811
|
+
if (e.hidden) continue;
|
|
2812
|
+
built.push({ seq: e.seq, message: e.message });
|
|
2813
|
+
} else if (e.type === "assistant_message") {
|
|
2814
|
+
let content = typeof e.message.content === "string" ? e.message.content : "";
|
|
2815
|
+
const amendments = amendmentsByTarget.get(e.id);
|
|
2816
|
+
if (amendments) {
|
|
2817
|
+
for (const a of amendments.sort((x, y) => x.seq - y.seq)) {
|
|
2818
|
+
if (a.appendText) content += a.appendText;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
built.push({ seq: e.seq, message: { ...e.message, content } });
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
const total = built.length;
|
|
2825
|
+
const tail = tailN >= total ? built : built.slice(total - tailN);
|
|
2826
|
+
return {
|
|
2827
|
+
messages: tail.map((b) => b.message),
|
|
2828
|
+
totalMessages: total,
|
|
2829
|
+
headSeq: tail.length > 0 ? tail[0].seq : null
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function getPendingSubagentResults(entries) {
|
|
2833
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
2834
|
+
for (const e of entries) {
|
|
2835
|
+
if (e.type === "callback_started") {
|
|
2836
|
+
for (const s of e.consumedSeqs) consumed.add(s);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return entries.filter((e) => e.type === "subagent_result").filter((e) => !consumed.has(e.seq)).map((e) => e.result);
|
|
2840
|
+
}
|
|
2841
|
+
var FULL_TRANSCRIPT_TAIL = 1e5;
|
|
2842
|
+
async function rebuildConversationFromEntries(conversation, readEntries) {
|
|
2843
|
+
if (process.env.PONCHO_READ_ENTRIES === "0") return conversation;
|
|
2844
|
+
try {
|
|
2845
|
+
const entries = await readEntries(conversation.conversationId);
|
|
2846
|
+
if (entries.length === 0) return conversation;
|
|
2847
|
+
conversation._harnessMessages = buildLlmContext(entries);
|
|
2848
|
+
conversation.messages = buildDisplaySnapshot(entries, FULL_TRANSCRIPT_TAIL).messages;
|
|
2849
|
+
conversation.pendingSubagentResults = getPendingSubagentResults(entries);
|
|
2850
|
+
return conversation;
|
|
2851
|
+
} catch (err) {
|
|
2852
|
+
entriesReadLog.warn(
|
|
2853
|
+
`[entries-read] ${conversation.conversationId} rebuild failed, using blob: ${err instanceof Error ? err.message : String(err)}`
|
|
2854
|
+
);
|
|
2855
|
+
return conversation;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// src/storage/memory-engine.ts
|
|
2779
2860
|
var DEFAULT_TENANT = "__default__";
|
|
2780
2861
|
var DEFAULT_OWNER = "local-owner";
|
|
2781
2862
|
var normalizeTenant = (tenantId) => tenantId ?? DEFAULT_TENANT;
|
|
@@ -2826,12 +2907,22 @@ var InMemoryEngine = class {
|
|
|
2826
2907
|
return results;
|
|
2827
2908
|
},
|
|
2828
2909
|
get: async (conversationId) => {
|
|
2829
|
-
|
|
2910
|
+
const c = this.convs.get(conversationId);
|
|
2911
|
+
if (!c) return void 0;
|
|
2912
|
+
return rebuildConversationFromEntries(
|
|
2913
|
+
{ ...c },
|
|
2914
|
+
(id) => this.conversations.readEntries(id)
|
|
2915
|
+
);
|
|
2830
2916
|
},
|
|
2831
2917
|
// In-memory storage has no separate archive blob, so both variants
|
|
2832
2918
|
// return the same conversation object.
|
|
2833
2919
|
getWithArchive: async (conversationId) => {
|
|
2834
|
-
|
|
2920
|
+
const c = this.convs.get(conversationId);
|
|
2921
|
+
if (!c) return void 0;
|
|
2922
|
+
return rebuildConversationFromEntries(
|
|
2923
|
+
{ ...c },
|
|
2924
|
+
(id) => this.conversations.readEntries(id)
|
|
2925
|
+
);
|
|
2835
2926
|
},
|
|
2836
2927
|
getStatusSnapshot: async (conversationId) => {
|
|
2837
2928
|
const c = this.convs.get(conversationId);
|
|
@@ -3292,7 +3383,7 @@ import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
|
3292
3383
|
|
|
3293
3384
|
// src/storage/sql-dialect.ts
|
|
3294
3385
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3295
|
-
import { createLogger as
|
|
3386
|
+
import { createLogger as createLogger3 } from "@poncho-ai/sdk";
|
|
3296
3387
|
|
|
3297
3388
|
// src/storage/schema.ts
|
|
3298
3389
|
var migrations = [
|
|
@@ -3517,7 +3608,7 @@ var migrations = [
|
|
|
3517
3608
|
];
|
|
3518
3609
|
|
|
3519
3610
|
// src/storage/sql-dialect.ts
|
|
3520
|
-
var egressLog =
|
|
3611
|
+
var egressLog = createLogger3("egress");
|
|
3521
3612
|
var sqliteDialect = {
|
|
3522
3613
|
tag: "sqlite",
|
|
3523
3614
|
param: () => "?",
|
|
@@ -3707,7 +3798,10 @@ var SqlStorageEngine = class {
|
|
|
3707
3798
|
if (row.continuation_messages) {
|
|
3708
3799
|
conv._continuationMessages = typeof row.continuation_messages === "string" ? JSON.parse(row.continuation_messages) : row.continuation_messages;
|
|
3709
3800
|
}
|
|
3710
|
-
return
|
|
3801
|
+
return rebuildConversationFromEntries(
|
|
3802
|
+
conv,
|
|
3803
|
+
(id) => this.conversations.readEntries(id)
|
|
3804
|
+
);
|
|
3711
3805
|
},
|
|
3712
3806
|
getStatusSnapshot: async (conversationId) => {
|
|
3713
3807
|
const runStatusExpr = this.dialect.tag === "sqlite" ? "json_extract(data, '$.runStatus')" : "data->>'runStatus'";
|
|
@@ -3757,7 +3851,10 @@ var SqlStorageEngine = class {
|
|
|
3757
3851
|
if (row.continuation_messages) {
|
|
3758
3852
|
conv._continuationMessages = typeof row.continuation_messages === "string" ? JSON.parse(row.continuation_messages) : row.continuation_messages;
|
|
3759
3853
|
}
|
|
3760
|
-
return
|
|
3854
|
+
return rebuildConversationFromEntries(
|
|
3855
|
+
conv,
|
|
3856
|
+
(id) => this.conversations.readEntries(id)
|
|
3857
|
+
);
|
|
3761
3858
|
},
|
|
3762
3859
|
create: async (ownerId, title, tenantId, init) => {
|
|
3763
3860
|
const id = randomUUID4();
|
|
@@ -6580,8 +6677,8 @@ var createReminderTools = (store) => [
|
|
|
6580
6677
|
];
|
|
6581
6678
|
|
|
6582
6679
|
// src/mcp.ts
|
|
6583
|
-
import { createLogger as
|
|
6584
|
-
var mcpLog =
|
|
6680
|
+
import { createLogger as createLogger4 } from "@poncho-ai/sdk";
|
|
6681
|
+
var mcpLog = createLogger4("mcp");
|
|
6585
6682
|
var McpHttpError = class extends Error {
|
|
6586
6683
|
status;
|
|
6587
6684
|
constructor(status, message) {
|
|
@@ -7574,8 +7671,8 @@ var createModelProvider = (provider, config) => {
|
|
|
7574
7671
|
import { readFile as readFile8, readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
7575
7672
|
import { dirname as dirname5, resolve as resolve9, normalize as normalize2 } from "path";
|
|
7576
7673
|
import YAML3 from "yaml";
|
|
7577
|
-
import { createLogger as
|
|
7578
|
-
var logger =
|
|
7674
|
+
import { createLogger as createLogger5 } from "@poncho-ai/sdk";
|
|
7675
|
+
var logger = createLogger5("skills");
|
|
7579
7676
|
var DEFAULT_SKILL_DIRS = ["skills"];
|
|
7580
7677
|
var resolveSkillDirs = (workingDir, extraPaths) => {
|
|
7581
7678
|
const dirs = [...DEFAULT_SKILL_DIRS];
|
|
@@ -8691,9 +8788,9 @@ import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace
|
|
|
8691
8788
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
8692
8789
|
|
|
8693
8790
|
// src/telemetry.ts
|
|
8694
|
-
import { createLogger as
|
|
8695
|
-
var eventLog =
|
|
8696
|
-
var telemetryLog =
|
|
8791
|
+
import { createLogger as createLogger6 } from "@poncho-ai/sdk";
|
|
8792
|
+
var eventLog = createLogger6("event");
|
|
8793
|
+
var telemetryLog = createLogger6("telemetry");
|
|
8697
8794
|
var MAX_FIELD_LENGTH = 200;
|
|
8698
8795
|
var OMIT_FROM_LOG = /* @__PURE__ */ new Set(["continuationMessages", "_harnessMessages", "messages", "compactedHistory"]);
|
|
8699
8796
|
function sanitizeEventForLog(event) {
|
|
@@ -8864,11 +8961,11 @@ var ToolDispatcher = class {
|
|
|
8864
8961
|
};
|
|
8865
8962
|
|
|
8866
8963
|
// src/harness.ts
|
|
8867
|
-
var harnessLog =
|
|
8868
|
-
var telemetryLog2 =
|
|
8869
|
-
var costLog =
|
|
8870
|
-
var mcpLog2 =
|
|
8871
|
-
var modelLog =
|
|
8964
|
+
var harnessLog = createLogger7("harness");
|
|
8965
|
+
var telemetryLog2 = createLogger7("telemetry");
|
|
8966
|
+
var costLog = createLogger7("cost");
|
|
8967
|
+
var mcpLog2 = createLogger7("mcp");
|
|
8968
|
+
var modelLog = createLogger7("model");
|
|
8872
8969
|
function formatOtlpError(err) {
|
|
8873
8970
|
if (!(err instanceof Error)) return String(err);
|
|
8874
8971
|
const parts = [];
|
|
@@ -9840,7 +9937,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
9840
9937
|
const key = `${effectiveTenant}:${skipped.name}`;
|
|
9841
9938
|
if (this.vfsSkillCollisionWarnings.has(key)) return;
|
|
9842
9939
|
this.vfsSkillCollisionWarnings.add(key);
|
|
9843
|
-
|
|
9940
|
+
createLogger7("skills").warn(
|
|
9844
9941
|
`VFS skill "${skipped.name}" for tenant ${effectiveTenant} ignored: a repo skill with the same name takes precedence.`
|
|
9845
9942
|
);
|
|
9846
9943
|
});
|
|
@@ -10093,7 +10190,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
10093
10190
|
this.agentFileFingerprint = rawContent;
|
|
10094
10191
|
return true;
|
|
10095
10192
|
} catch (error) {
|
|
10096
|
-
|
|
10193
|
+
createLogger7("agent").warn(`failed to refresh AGENT.md in dev: ${fmtErr(error)}`);
|
|
10097
10194
|
return false;
|
|
10098
10195
|
}
|
|
10099
10196
|
}
|
|
@@ -10141,7 +10238,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
10141
10238
|
await this.refreshMcpTools("skills:changed");
|
|
10142
10239
|
return true;
|
|
10143
10240
|
} catch (error) {
|
|
10144
|
-
|
|
10241
|
+
createLogger7("skills").warn(`failed to refresh skills in dev: ${fmtErr(error)}`);
|
|
10145
10242
|
return false;
|
|
10146
10243
|
}
|
|
10147
10244
|
}
|
|
@@ -10251,7 +10348,7 @@ var AgentHarness = class _AgentHarness {
|
|
|
10251
10348
|
}
|
|
10252
10349
|
if (config?.browser) {
|
|
10253
10350
|
await this.initBrowserTools(config).catch((e) => {
|
|
10254
|
-
|
|
10351
|
+
createLogger7("browser").warn(`failed to load browser tools: ${fmtErr(e)}`);
|
|
10255
10352
|
});
|
|
10256
10353
|
}
|
|
10257
10354
|
const stateConfig = resolveStateConfig(config);
|
|
@@ -12149,7 +12246,9 @@ var InMemoryConversationStore = class {
|
|
|
12149
12246
|
}
|
|
12150
12247
|
async get(conversationId) {
|
|
12151
12248
|
this.purgeExpired();
|
|
12152
|
-
|
|
12249
|
+
const c = this.conversations.get(conversationId);
|
|
12250
|
+
if (!c) return void 0;
|
|
12251
|
+
return rebuildConversationFromEntries({ ...c }, (id) => this.readEntries(id));
|
|
12153
12252
|
}
|
|
12154
12253
|
// In-memory stores already hold the full conversation object, so there's
|
|
12155
12254
|
// no separate archive blob to load. Both variants return the same data.
|
|
@@ -12276,66 +12375,6 @@ var createConversationStore = (config, _options) => {
|
|
|
12276
12375
|
return new InMemoryConversationStore(ttl);
|
|
12277
12376
|
};
|
|
12278
12377
|
|
|
12279
|
-
// src/storage/entries.ts
|
|
12280
|
-
function buildLlmContext(entries) {
|
|
12281
|
-
let latestCompaction;
|
|
12282
|
-
for (const e of entries) {
|
|
12283
|
-
if (e.type === "compaction" && (!latestCompaction || e.seq > latestCompaction.seq)) {
|
|
12284
|
-
latestCompaction = e;
|
|
12285
|
-
}
|
|
12286
|
-
}
|
|
12287
|
-
const harnessMsgs = entries.filter(
|
|
12288
|
-
(e) => e.type === "harness_message"
|
|
12289
|
-
);
|
|
12290
|
-
if (latestCompaction) {
|
|
12291
|
-
const kept = harnessMsgs.filter((e) => e.seq >= latestCompaction.firstKeptSeq).map((e) => e.message);
|
|
12292
|
-
return [latestCompaction.summaryMessage, ...kept];
|
|
12293
|
-
}
|
|
12294
|
-
return harnessMsgs.map((e) => e.message);
|
|
12295
|
-
}
|
|
12296
|
-
function buildDisplaySnapshot(entries, tailN) {
|
|
12297
|
-
const amendmentsByTarget = /* @__PURE__ */ new Map();
|
|
12298
|
-
for (const e of entries) {
|
|
12299
|
-
if (e.type === "assistant_amendment") {
|
|
12300
|
-
const list = amendmentsByTarget.get(e.targetEntryId) ?? [];
|
|
12301
|
-
list.push(e);
|
|
12302
|
-
amendmentsByTarget.set(e.targetEntryId, list);
|
|
12303
|
-
}
|
|
12304
|
-
}
|
|
12305
|
-
const built = [];
|
|
12306
|
-
for (const e of entries) {
|
|
12307
|
-
if (e.type === "user_message") {
|
|
12308
|
-
if (e.hidden) continue;
|
|
12309
|
-
built.push({ seq: e.seq, message: e.message });
|
|
12310
|
-
} else if (e.type === "assistant_message") {
|
|
12311
|
-
let content = typeof e.message.content === "string" ? e.message.content : "";
|
|
12312
|
-
const amendments = amendmentsByTarget.get(e.id);
|
|
12313
|
-
if (amendments) {
|
|
12314
|
-
for (const a of amendments.sort((x, y) => x.seq - y.seq)) {
|
|
12315
|
-
if (a.appendText) content += a.appendText;
|
|
12316
|
-
}
|
|
12317
|
-
}
|
|
12318
|
-
built.push({ seq: e.seq, message: { ...e.message, content } });
|
|
12319
|
-
}
|
|
12320
|
-
}
|
|
12321
|
-
const total = built.length;
|
|
12322
|
-
const tail = tailN >= total ? built : built.slice(total - tailN);
|
|
12323
|
-
return {
|
|
12324
|
-
messages: tail.map((b) => b.message),
|
|
12325
|
-
totalMessages: total,
|
|
12326
|
-
headSeq: tail.length > 0 ? tail[0].seq : null
|
|
12327
|
-
};
|
|
12328
|
-
}
|
|
12329
|
-
function getPendingSubagentResults(entries) {
|
|
12330
|
-
const consumed = /* @__PURE__ */ new Set();
|
|
12331
|
-
for (const e of entries) {
|
|
12332
|
-
if (e.type === "callback_started") {
|
|
12333
|
-
for (const s of e.consumedSeqs) consumed.add(s);
|
|
12334
|
-
}
|
|
12335
|
-
}
|
|
12336
|
-
return entries.filter((e) => e.type === "subagent_result").filter((e) => !consumed.has(e.seq)).map((e) => e.result);
|
|
12337
|
-
}
|
|
12338
|
-
|
|
12339
12378
|
// src/tenant-token.ts
|
|
12340
12379
|
import { jwtVerify } from "jose";
|
|
12341
12380
|
async function verifyTenantToken(signingKey, token) {
|
|
@@ -12668,7 +12707,7 @@ var CALLBACK_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
|
12668
12707
|
var STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
12669
12708
|
|
|
12670
12709
|
// src/orchestrator/orchestrator.ts
|
|
12671
|
-
import { createLogger as
|
|
12710
|
+
import { createLogger as createLogger8, getTextContent as getTextContent4 } from "@poncho-ai/sdk";
|
|
12672
12711
|
|
|
12673
12712
|
// src/orchestrator/entries-dual-write.ts
|
|
12674
12713
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
@@ -12802,7 +12841,7 @@ var verifyEntriesParity = async (store, conversationId, blob, log2) => {
|
|
|
12802
12841
|
};
|
|
12803
12842
|
|
|
12804
12843
|
// src/orchestrator/orchestrator.ts
|
|
12805
|
-
var dualWriteLog =
|
|
12844
|
+
var dualWriteLog = createLogger8("orchestrator:entries");
|
|
12806
12845
|
var assistantMessageText = (message) => {
|
|
12807
12846
|
const raw = getTextContent4(message).trim();
|
|
12808
12847
|
if (raw.startsWith("{") && raw.includes('"tool_calls"')) {
|
|
@@ -14393,8 +14432,8 @@ ${resultBody}`,
|
|
|
14393
14432
|
|
|
14394
14433
|
// src/orchestrator/run-conversation-turn.ts
|
|
14395
14434
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
14396
|
-
import { createLogger as
|
|
14397
|
-
var log =
|
|
14435
|
+
import { createLogger as createLogger9 } from "@poncho-ai/sdk";
|
|
14436
|
+
var log = createLogger9("orchestrator");
|
|
14398
14437
|
var runConversationTurn = async (opts) => {
|
|
14399
14438
|
const conversation = await opts.conversationStore.getWithArchive(opts.conversationId);
|
|
14400
14439
|
if (!conversation) {
|
package/package.json
CHANGED
package/src/state.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Message } from "@poncho-ai/sdk";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type ConversationEntry,
|
|
4
|
+
type NewConversationEntry,
|
|
5
|
+
rebuildConversationFromEntries,
|
|
6
|
+
} from "./storage/entries.js";
|
|
3
7
|
|
|
4
8
|
export interface ConversationState {
|
|
5
9
|
runId: string;
|
|
@@ -269,7 +273,12 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
269
273
|
|
|
270
274
|
async get(conversationId: string): Promise<Conversation | undefined> {
|
|
271
275
|
this.purgeExpired();
|
|
272
|
-
|
|
276
|
+
const c = this.conversations.get(conversationId);
|
|
277
|
+
if (!c) return undefined;
|
|
278
|
+
// Phase 3c read cutover: rebuild reader-facing fields from the entry log
|
|
279
|
+
// (blob fallback for un-migrated conversations). Clone first — the map
|
|
280
|
+
// holds a live mutable reference and the rebuild overrides fields.
|
|
281
|
+
return rebuildConversationFromEntries({ ...c }, (id) => this.readEntries(id));
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
// In-memory stores already hold the full conversation object, so there's
|
package/src/storage/entries.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { PendingSubagentResult } from "../state.js";
|
|
1
|
+
import { createLogger, type Message } from "@poncho-ai/sdk";
|
|
2
|
+
import type { Conversation, PendingSubagentResult } from "../state.js";
|
|
3
|
+
|
|
4
|
+
const entriesReadLog = createLogger("entries-read");
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Append-only conversation entries (Phase 3 substrate).
|
|
@@ -215,3 +217,56 @@ export function getPendingSubagentResults(
|
|
|
215
217
|
.filter((e) => !consumed.has(e.seq))
|
|
216
218
|
.map((e) => e.result);
|
|
217
219
|
}
|
|
220
|
+
|
|
221
|
+
// A very large tail so the rebuilt display snapshot is the full transcript.
|
|
222
|
+
// Display callers slice to whatever window they actually render.
|
|
223
|
+
const FULL_TRANSCRIPT_TAIL = 100_000;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Phase 3c read cutover: rebuild a conversation's reader-facing fields from
|
|
227
|
+
* the append-only entry log, with a blob fallback for conversations that
|
|
228
|
+
* predate dual-write.
|
|
229
|
+
*
|
|
230
|
+
* Call this in every conversation `get`/`getWithArchive` path AFTER the
|
|
231
|
+
* Conversation has been constructed from the stored row/blob. It:
|
|
232
|
+
* - reads the entry log via `readEntries`,
|
|
233
|
+
* - if NON-EMPTY, overrides `_harnessMessages`, `messages`, and
|
|
234
|
+
* `pendingSubagentResults` with entry-derived values,
|
|
235
|
+
* - if EMPTY (un-migrated conversation), leaves the blob-derived fields
|
|
236
|
+
* untouched (fallback),
|
|
237
|
+
* - on ANY error, logs and falls back to the blob (never throws — this is
|
|
238
|
+
* a hot read path).
|
|
239
|
+
*
|
|
240
|
+
* `_continuationMessages` and `pendingApprovals` are NOT modeled as entries
|
|
241
|
+
* yet and are intentionally left as blob fields.
|
|
242
|
+
*
|
|
243
|
+
* Kill-switch: set `PONCHO_READ_ENTRIES=0` to instantly revert to pure blob
|
|
244
|
+
* reads without a deploy (rebuild is ON by default).
|
|
245
|
+
*
|
|
246
|
+
* NOTE: mutates `conversation` in place and returns it. Callers that hand
|
|
247
|
+
* back a shared/mutable Conversation reference (the in-memory stores) MUST
|
|
248
|
+
* pass a clone, or the override will corrupt their stored object.
|
|
249
|
+
*/
|
|
250
|
+
export async function rebuildConversationFromEntries(
|
|
251
|
+
conversation: Conversation,
|
|
252
|
+
readEntries: (conversationId: string) => Promise<ConversationEntry[]>,
|
|
253
|
+
): Promise<Conversation> {
|
|
254
|
+
// Kill-switch: ON by default; PONCHO_READ_ENTRIES="0" reverts to blob reads.
|
|
255
|
+
if (process.env.PONCHO_READ_ENTRIES === "0") return conversation;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const entries = await readEntries(conversation.conversationId);
|
|
259
|
+
if (entries.length === 0) return conversation; // fallback: pre-dual-write
|
|
260
|
+
conversation._harnessMessages = buildLlmContext(entries);
|
|
261
|
+
conversation.messages = buildDisplaySnapshot(entries, FULL_TRANSCRIPT_TAIL).messages;
|
|
262
|
+
conversation.pendingSubagentResults = getPendingSubagentResults(entries);
|
|
263
|
+
return conversation;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
entriesReadLog.warn(
|
|
266
|
+
`[entries-read] ${conversation.conversationId} rebuild failed, using blob: ${
|
|
267
|
+
err instanceof Error ? err.message : String(err)
|
|
268
|
+
}`,
|
|
269
|
+
);
|
|
270
|
+
return conversation;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -14,7 +14,11 @@ import type { MainMemory } from "../memory.js";
|
|
|
14
14
|
import type { TodoItem } from "../todo-tools.js";
|
|
15
15
|
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
16
16
|
import type { StorageEngine, VfsDirEntry, VfsStat } from "./engine.js";
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
type ConversationEntry,
|
|
19
|
+
type NewConversationEntry,
|
|
20
|
+
rebuildConversationFromEntries,
|
|
21
|
+
} from "./entries.js";
|
|
18
22
|
|
|
19
23
|
// ---------------------------------------------------------------------------
|
|
20
24
|
// Internal VFS entry type
|
|
@@ -103,13 +107,24 @@ export class InMemoryEngine implements StorageEngine {
|
|
|
103
107
|
},
|
|
104
108
|
|
|
105
109
|
get: async (conversationId: string): Promise<Conversation | undefined> => {
|
|
106
|
-
|
|
110
|
+
const c = this.convs.get(conversationId);
|
|
111
|
+
if (!c) return undefined;
|
|
112
|
+
// Phase 3c read cutover: rebuild reader-facing fields from the entry
|
|
113
|
+
// log (blob fallback for un-migrated conversations). Clone first — the
|
|
114
|
+
// map holds a live mutable reference and the rebuild overrides fields.
|
|
115
|
+
return rebuildConversationFromEntries({ ...c }, (id) =>
|
|
116
|
+
this.conversations.readEntries(id),
|
|
117
|
+
);
|
|
107
118
|
},
|
|
108
119
|
|
|
109
120
|
// In-memory storage has no separate archive blob, so both variants
|
|
110
121
|
// return the same conversation object.
|
|
111
122
|
getWithArchive: async (conversationId: string): Promise<Conversation | undefined> => {
|
|
112
|
-
|
|
123
|
+
const c = this.convs.get(conversationId);
|
|
124
|
+
if (!c) return undefined;
|
|
125
|
+
return rebuildConversationFromEntries({ ...c }, (id) =>
|
|
126
|
+
this.conversations.readEntries(id),
|
|
127
|
+
);
|
|
113
128
|
},
|
|
114
129
|
|
|
115
130
|
getStatusSnapshot: async (
|
|
@@ -22,7 +22,11 @@ import type { MainMemory } from "../memory.js";
|
|
|
22
22
|
import type { TodoItem } from "../todo-tools.js";
|
|
23
23
|
import type { Reminder, ReminderCreateInput, ReminderStatus } from "../reminder-store.js";
|
|
24
24
|
import type { StorageEngine, VfsDirEntry, VfsStat } from "./engine.js";
|
|
25
|
-
import
|
|
25
|
+
import {
|
|
26
|
+
type ConversationEntry,
|
|
27
|
+
type NewConversationEntry,
|
|
28
|
+
rebuildConversationFromEntries,
|
|
29
|
+
} from "./entries.js";
|
|
26
30
|
import { type DialectTag, migrations } from "./schema.js";
|
|
27
31
|
|
|
28
32
|
// ---------------------------------------------------------------------------
|
|
@@ -325,7 +329,12 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
325
329
|
? JSON.parse(row.continuation_messages)
|
|
326
330
|
: row.continuation_messages;
|
|
327
331
|
}
|
|
328
|
-
|
|
332
|
+
// Phase 3c read cutover: rebuild reader-facing fields from the
|
|
333
|
+
// append-only entry log, falling back to the blob for un-migrated
|
|
334
|
+
// conversations. parseConversation returns a fresh object, so no clone.
|
|
335
|
+
return rebuildConversationFromEntries(conv, (id) =>
|
|
336
|
+
this.conversations.readEntries(id),
|
|
337
|
+
);
|
|
329
338
|
},
|
|
330
339
|
|
|
331
340
|
getStatusSnapshot: async (
|
|
@@ -408,7 +417,11 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
408
417
|
? JSON.parse(row.continuation_messages)
|
|
409
418
|
: row.continuation_messages;
|
|
410
419
|
}
|
|
411
|
-
|
|
420
|
+
// Phase 3c read cutover: rebuild reader-facing fields from the entry
|
|
421
|
+
// log (blob fallback for un-migrated conversations).
|
|
422
|
+
return rebuildConversationFromEntries(conv, (id) =>
|
|
423
|
+
this.conversations.readEntries(id),
|
|
424
|
+
);
|
|
412
425
|
},
|
|
413
426
|
|
|
414
427
|
create: async (
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { InMemoryConversationStore } from "../src/state.js";
|
|
3
|
+
import {
|
|
4
|
+
buildLlmContext,
|
|
5
|
+
buildDisplaySnapshot,
|
|
6
|
+
type NewConversationEntry,
|
|
7
|
+
} from "../src/storage/entries.js";
|
|
8
|
+
import type { Message } from "@poncho-ai/sdk";
|
|
9
|
+
|
|
10
|
+
const msg = (role: Message["role"], content: string): Message => ({ role, content });
|
|
11
|
+
|
|
12
|
+
// A turn's worth of entries: a user display message, the harness (LLM
|
|
13
|
+
// transcript) messages for that turn, and the final assistant bubble.
|
|
14
|
+
function turnEntries(): NewConversationEntry[] {
|
|
15
|
+
return [
|
|
16
|
+
{ type: "user_message", id: "u1", message: msg("user", "hello"), turnId: "t1" },
|
|
17
|
+
{ type: "harness_message", id: "h1", message: msg("user", "hello"), turnId: "t1" },
|
|
18
|
+
{ type: "harness_message", id: "h2", message: msg("assistant", "hi there"), turnId: "t1" },
|
|
19
|
+
{
|
|
20
|
+
type: "assistant_message",
|
|
21
|
+
id: "a1",
|
|
22
|
+
message: msg("assistant", "hi there"),
|
|
23
|
+
turnId: "t1",
|
|
24
|
+
runId: "r1",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("Phase 3c read cutover", () => {
|
|
30
|
+
const prevFlag = process.env.PONCHO_READ_ENTRIES;
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (prevFlag === undefined) delete process.env.PONCHO_READ_ENTRIES;
|
|
34
|
+
else process.env.PONCHO_READ_ENTRIES = prevFlag;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("get() rebuilds _harnessMessages/messages from entries when present", async () => {
|
|
38
|
+
delete process.env.PONCHO_READ_ENTRIES; // ON by default
|
|
39
|
+
const store = new InMemoryConversationStore();
|
|
40
|
+
const conv = await store.create("owner", "title", null);
|
|
41
|
+
|
|
42
|
+
// Seed the blob with stale messages so we can prove the override happened.
|
|
43
|
+
conv.messages = [msg("assistant", "STALE BLOB")];
|
|
44
|
+
conv._harnessMessages = [msg("assistant", "STALE BLOB HARNESS")];
|
|
45
|
+
await store.update(conv);
|
|
46
|
+
|
|
47
|
+
const entries = await store.appendEntries(conv.conversationId, "agent", null, turnEntries());
|
|
48
|
+
|
|
49
|
+
const loaded = await store.get(conv.conversationId);
|
|
50
|
+
expect(loaded).toBeDefined();
|
|
51
|
+
expect(loaded!._harnessMessages).toEqual(buildLlmContext(entries));
|
|
52
|
+
expect(loaded!.messages).toEqual(buildDisplaySnapshot(entries, 100000).messages);
|
|
53
|
+
// Display transcript drops the harness-only messages; keeps user + assistant bubble.
|
|
54
|
+
expect(loaded!.messages.map((m) => m.content)).toEqual(["hello", "hi there"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("get() falls back to the blob when there are no entries", async () => {
|
|
58
|
+
delete process.env.PONCHO_READ_ENTRIES;
|
|
59
|
+
const store = new InMemoryConversationStore();
|
|
60
|
+
const conv = await store.create("owner", "title", null);
|
|
61
|
+
conv.messages = [msg("user", "blob only")];
|
|
62
|
+
conv._harnessMessages = [msg("user", "blob only harness")];
|
|
63
|
+
await store.update(conv);
|
|
64
|
+
|
|
65
|
+
const loaded = await store.get(conv.conversationId);
|
|
66
|
+
expect(loaded!.messages).toEqual([msg("user", "blob only")]);
|
|
67
|
+
expect(loaded!._harnessMessages).toEqual([msg("user", "blob only harness")]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("kill-switch PONCHO_READ_ENTRIES=0 reverts to blob reads even with entries", async () => {
|
|
71
|
+
process.env.PONCHO_READ_ENTRIES = "0";
|
|
72
|
+
const store = new InMemoryConversationStore();
|
|
73
|
+
const conv = await store.create("owner", "title", null);
|
|
74
|
+
conv.messages = [msg("user", "blob wins")];
|
|
75
|
+
await store.update(conv);
|
|
76
|
+
await store.appendEntries(conv.conversationId, "agent", null, turnEntries());
|
|
77
|
+
|
|
78
|
+
const loaded = await store.get(conv.conversationId);
|
|
79
|
+
expect(loaded!.messages).toEqual([msg("user", "blob wins")]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("get() does not mutate the stored blob conversation (clone)", async () => {
|
|
83
|
+
delete process.env.PONCHO_READ_ENTRIES;
|
|
84
|
+
const store = new InMemoryConversationStore();
|
|
85
|
+
const conv = await store.create("owner", "title", null);
|
|
86
|
+
conv.messages = [msg("assistant", "STALE BLOB")];
|
|
87
|
+
await store.update(conv);
|
|
88
|
+
await store.appendEntries(conv.conversationId, "agent", null, turnEntries());
|
|
89
|
+
|
|
90
|
+
await store.get(conv.conversationId);
|
|
91
|
+
// Re-read with the kill-switch on: should still see the untouched blob.
|
|
92
|
+
process.env.PONCHO_READ_ENTRIES = "0";
|
|
93
|
+
const blob = await store.get(conv.conversationId);
|
|
94
|
+
expect(blob!.messages).toEqual([msg("assistant", "STALE BLOB")]);
|
|
95
|
+
});
|
|
96
|
+
});
|